mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04:00
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:
parent
6919f5e4fb
commit
d482065d72
9 changed files with 157 additions and 12 deletions
|
@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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))
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue