mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05: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
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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…
Reference in a new issue