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

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

View file

@ -56,6 +56,7 @@ import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
import { CsvReplacer } from './replace-components/csv/csv-replacer'
import { GistReplacer } from './replace-components/gist/gist-replacer'
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
import { ImageReplacer } from './replace-components/image/image-replacer'
@ -309,6 +310,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
new PdfReplacer(),
new ImageReplacer(),
new TocReplacer(),
new CsvReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
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 React from 'react'
import { HighlightedCode } from './highlighted-code/highlighted-code'
import { ComponentReplacer } from '../ComponentReplacer'
import { HighlightedCode } from './highlighted-code/highlighted-code'
export class HighlightedCodeReplacer implements ComponentReplacer {
private lastLineNumber = 0;
@ -12,11 +12,20 @@ export class HighlightedCodeReplacer implements ComponentReplacer {
}
const language = codeNode.attribs['data-highlight-language']
const showLineNumbers = codeNode.attribs['data-show-line-numbers'] !== undefined
const startLineNumberAttribute = codeNode.attribs['data-start-line-number']
const extraData = codeNode.attribs['data-extra']
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 wrapLines = codeNode.attribs['data-wrap-lines'] !== undefined
const code = codeNode.children[0].data as string
if (showLineNumbers) {