diff --git a/CHANGELOG.md b/CHANGELOG.md index 5feeba477..ce6141efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/editor/editorTestContent.ts b/src/components/editor/editorTestContent.ts index 894bd7756..15ec8c0b5 100644 --- a/src/components/editor/editorTestContent.ts +++ b/src/components/editor/editorTestContent.ts @@ -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. diff --git a/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts b/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts index b42e0d3d8..6c8939ddd 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts @@ -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]) } } }) diff --git a/src/components/markdown-renderer/markdown-renderer.tsx b/src/components/markdown-renderer/markdown-renderer.tsx index de4139fac..fbc79b9a0 100644 --- a/src/components/markdown-renderer/markdown-renderer.tsx +++ b/src/components/markdown-renderer/markdown-renderer.tsx @@ -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 = ({ content, onM new PdfReplacer(), new ImageReplacer(), new TocReplacer(), + new CsvReplacer(), new HighlightedCodeReplacer(), new QuoteOptionsReplacer(), new KatexReplacer() diff --git a/src/components/markdown-renderer/replace-components/csv/csv-parser.test.ts b/src/components/markdown-renderer/replace-components/csv/csv-parser.test.ts new file mode 100644 index 000000000..6ab94d1f4 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/csv/csv-parser.test.ts @@ -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) + }) +}) diff --git a/src/components/markdown-renderer/replace-components/csv/csv-parser.ts b/src/components/markdown-renderer/replace-components/csv/csv-parser.ts new file mode 100644 index 000000000..3b9e5ebdf --- /dev/null +++ b/src/components/markdown-renderer/replace-components/csv/csv-parser.ts @@ -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)) +} diff --git a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx new file mode 100644 index 000000000..83b375e69 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx @@ -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 + } +} diff --git a/src/components/markdown-renderer/replace-components/csv/csv-table.tsx b/src/components/markdown-renderer/replace-components/csv/csv-table.tsx new file mode 100644 index 000000000..f678796f4 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/csv/csv-table.tsx @@ -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 = ({ 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 ( + + + { + row.map((column, columnNumber) => ( + + {column} + + )) + } + + + ) + } + } + + const renderTableBody = (rows: string[][]) => { + return ( + + { + rows.map((row, rowNumber) => ( + + { + row.map((column, columnIndex) => ( + + {column.replace(/^"|"$/g, '')} + + )) + } + + )) + } + + ) + } + + return ( + + {renderTableHeader(headerRow)} + {renderTableBody(rowsWithColumns)} +
+ ) +} diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx index 45a325d94..e8fafa264 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx @@ -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) {