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
- 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
- Code blocks with 'csv' as language render as tables.
### Changed

View file

@ -9,6 +9,17 @@ opengraph:
# Embedding demo
[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
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'
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) {