Feature/csv table (#500)

- added csv-replacer
- changed highlghted-code plugin:
     each replacer extracts what he need from the data-extra attribute now
- changed CHANGELOG.md

Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Philip Molares 2020-08-29 15:55:42 +02:00 committed by GitHub
parent 6919f5e4fb
commit d482065d72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 12 deletions

View file

@ -37,6 +37,7 @@
- The Toolbar includes an EmojiPicker - The Toolbar includes an EmojiPicker
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`) - Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
- The code button now adds code fences even if the user selected nothing beforehand - The code button now adds code fences even if the user selected nothing beforehand
- Code blocks with 'csv' as language render as tables.
### Changed ### Changed

View file

@ -9,6 +9,17 @@ opengraph:
# Embedding demo # Embedding demo
[TOC] [TOC]
## CSV
\`\`\`csv delimiter=; header
Username; Identifier;First name;Last name
"booker12; rbooker";9012;Rachel;Booker
grey07;2070;Laura;Grey
johnson81;4081;Craig;Johnson
jenkins46;9346;Mary;Jenkins
smith79;5079;Jamie;Smith
\`\`\`
## some plain text ## some plain text
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

View file

@ -1,6 +1,6 @@
import MarkdownIt from 'markdown-it/lib' import MarkdownIt from 'markdown-it/lib'
const highlightRegex = /^ *(\w*)(=(\d*|\+))?(!?)$/ const highlightRegex = /^ *(\w*)(.*)$/
export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => { export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.core.ruler.push('highlighted-code', (state) => { md.core.ruler.push('highlighted-code', (state) => {
@ -14,13 +14,7 @@ export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
token.attrJoin('data-highlight-language', highlightInfos[1]) token.attrJoin('data-highlight-language', highlightInfos[1])
} }
if (highlightInfos[2]) { if (highlightInfos[2]) {
token.attrJoin('data-show-line-numbers', '') token.attrJoin('data-extra', highlightInfos[2])
}
if (highlightInfos[3]) {
token.attrJoin('data-start-line-number', highlightInfos[3])
}
if (highlightInfos[4]) {
token.attrJoin('data-wrap-lines', '')
} }
} }
}) })

View file

@ -56,6 +56,7 @@ import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link' import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer' import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer' import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
import { CsvReplacer } from './replace-components/csv/csv-replacer'
import { GistReplacer } from './replace-components/gist/gist-replacer' import { GistReplacer } from './replace-components/gist/gist-replacer'
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer' import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
import { ImageReplacer } from './replace-components/image/image-replacer' import { ImageReplacer } from './replace-components/image/image-replacer'
@ -309,6 +310,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
new PdfReplacer(), new PdfReplacer(),
new ImageReplacer(), new ImageReplacer(),
new TocReplacer(), new TocReplacer(),
new CsvReplacer(),
new HighlightedCodeReplacer(), new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(), new QuoteOptionsReplacer(),
new KatexReplacer() new KatexReplacer()

View file

@ -0,0 +1,21 @@
import { parseCsv } from './csv-parser'
describe('test CSV parser', () => {
it('normal table', () => {
const input = 'A;B;C\nD;E;F\nG;H;I'
const expected = [['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
expect(parseCsv(input, ';')).toEqual(expected)
})
it('blank lines', () => {
const input = 'A;B;C\n\nG;H;I'
const expected = [['A', 'B', 'C'], ['G', 'H', 'I']]
expect(parseCsv(input, ';')).toEqual(expected)
})
it('items with delimiter', () => {
const input = 'A;B;C\n"D;E;F"\nG;H;I'
const expected = [['A', 'B', 'C'], ['"D;E;F"'], ['G', 'H', 'I']]
expect(parseCsv(input, ';')).toEqual(expected)
})
})

View file

@ -0,0 +1,8 @@
export const parseCsv = (csvText: string, csvColumnDelimiter: string): string[][] => {
const rows = csvText.split('\n')
if (!rows || rows.length === 0) {
return []
}
const splitRegex = new RegExp(`${csvColumnDelimiter}(?=(?:[^"]*"[^"]*")*[^"]*$)`)
return rows.filter(row => row !== '').map(row => row.split(splitRegex))
}

View file

@ -0,0 +1,28 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
import { CsvTable } from './csv-table'
export class CsvReplacer implements ComponentReplacer {
getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'csv' || !codeNode.children || !codeNode.children[0]) {
return
}
const code = codeNode.children[0].data as string
const extraData = codeNode.attribs['data-extra']
const extraRegex = /\s*(delimiter=([^\s]*))?\s*(header)?/
const extraInfos = extraRegex.exec(extraData)
let delimiter = ','
let showHeader = false
if (extraInfos) {
delimiter = extraInfos[2] || delimiter
showHeader = extraInfos[3] !== undefined
}
return <CsvTable key={`csv-${index}`} code={code} delimiter={delimiter} showHeader={showHeader}/>
}
}

View file

@ -0,0 +1,71 @@
import React, { useMemo } from 'react'
import { parseCsv } from './csv-parser'
export interface CsvTableProps {
code: string
delimiter: string
showHeader: boolean
tableRowClassName?: string
tableColumnClassName?: string
}
export const CsvTable: React.FC<CsvTableProps> = ({ code, delimiter, showHeader, tableRowClassName, tableColumnClassName }) => {
const { rowsWithColumns, headerRow } = useMemo(() => {
const rowsWithColumns = parseCsv(code.trim(), delimiter)
let headerRow: string[] = []
if (showHeader) {
headerRow = rowsWithColumns.splice(0, 1)[0]
}
return { rowsWithColumns, headerRow }
}, [code, delimiter, showHeader])
const renderTableHeader = (row: string[]) => {
if (row !== []) {
return (
<thead>
<tr>
{
row.map((column, columnNumber) => (
<th
key={`header-${columnNumber}`}
>
{column}
</th>
))
}
</tr>
</thead>
)
}
}
const renderTableBody = (rows: string[][]) => {
return (
<tbody>
{
rows.map((row, rowNumber) => (
<tr className={tableRowClassName} key={`row-${rowNumber}`}>
{
row.map((column, columnIndex) => (
<td
className={tableColumnClassName}
key={`cell-${rowNumber}-${columnIndex}`}
>
{column.replace(/^"|"$/g, '')}
</td>
))
}
</tr>
))
}
</tbody>
)
}
return (
<table className={'csv-html-table'}>
{renderTableHeader(headerRow)}
{renderTableBody(rowsWithColumns)}
</table>
)
}

View file

@ -1,7 +1,7 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import React from 'react' import React from 'react'
import { HighlightedCode } from './highlighted-code/highlighted-code'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import { HighlightedCode } from './highlighted-code/highlighted-code'
export class HighlightedCodeReplacer implements ComponentReplacer { export class HighlightedCodeReplacer implements ComponentReplacer {
private lastLineNumber = 0; private lastLineNumber = 0;
@ -12,11 +12,20 @@ export class HighlightedCodeReplacer implements ComponentReplacer {
} }
const language = codeNode.attribs['data-highlight-language'] const language = codeNode.attribs['data-highlight-language']
const showLineNumbers = codeNode.attribs['data-show-line-numbers'] !== undefined const extraData = codeNode.attribs['data-extra']
const startLineNumberAttribute = codeNode.attribs['data-start-line-number'] const extraInfos = /(=(\d*|\+))?(!?)/.exec(extraData)
let showLineNumbers = false
let startLineNumberAttribute = ''
let wrapLines = false
if (extraInfos) {
showLineNumbers = extraInfos[0] !== undefined
startLineNumberAttribute = extraInfos[1]
wrapLines = extraInfos[2] !== undefined
}
const startLineNumber = startLineNumberAttribute === '+' ? this.lastLineNumber : (parseInt(startLineNumberAttribute) || 1) const startLineNumber = startLineNumberAttribute === '+' ? this.lastLineNumber : (parseInt(startLineNumberAttribute) || 1)
const wrapLines = codeNode.attribs['data-wrap-lines'] !== undefined
const code = codeNode.children[0].data as string const code = codeNode.children[0].data as string
if (showLineNumbers) { if (showLineNumbers) {