diff options
author | Max <max@nextcloud.com> | 2022-03-09 22:07:17 +0300 |
---|---|---|
committer | Max <max@nextcloud.com> | 2022-03-31 15:29:14 +0300 |
commit | 4629457932accaa24abefc9a8f444dc9398a94d5 (patch) | |
tree | 9d0a5d312e588f627ed905c25c8b13d34060610a /src | |
parent | 0db660bbe12c948e1964915f483ad8e406f72d4d (diff) |
feature: initial table support
First take at parsing and serializing markdown tables.
Use TableHead and TableBody nodes to mimic the markdown table structure
with a single heading line on top and multiple td lines following.
Signed-off-by: Max <max@nextcloud.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/EditorFactory.js | 12 | ||||
-rw-r--r-- | src/markdownit/index.js | 1 | ||||
-rw-r--r-- | src/nodes/Table.js | 41 | ||||
-rw-r--r-- | src/nodes/TableBody.js | 23 | ||||
-rw-r--r-- | src/nodes/TableCell.js | 8 | ||||
-rw-r--r-- | src/nodes/TableHead.js | 49 | ||||
-rw-r--r-- | src/nodes/TableHeader.js | 8 | ||||
-rw-r--r-- | src/nodes/TableRow.js | 8 | ||||
-rw-r--r-- | src/tests/fixtures/table.html | 14 | ||||
-rw-r--r-- | src/tests/fixtures/table.md | 3 | ||||
-rw-r--r-- | src/tests/fixtures/tableWithOtherStructure.html | 10 | ||||
-rw-r--r-- | src/tests/markdownit.spec.js | 7 | ||||
-rw-r--r-- | src/tests/tables.spec.js | 36 |
13 files changed, 220 insertions, 0 deletions
diff --git a/src/EditorFactory.js b/src/EditorFactory.js index ee31c96bf..ec681c489 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -32,6 +32,12 @@ import ListItem from '@tiptap/extension-list-item' import CodeBlock from '@tiptap/extension-code-block' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' import HorizontalRule from '@tiptap/extension-horizontal-rule' +import Table from './nodes/Table' +import TableBody from './nodes/TableBody' +import TableCell from './nodes/TableCell' +import TableHead from './nodes/TableHead' +import TableHeader from './nodes/TableHeader' +import TableRow from './nodes/TableRow' /* eslint-enable import/no-named-as-default */ import { Editor } from '@tiptap/core' @@ -88,6 +94,12 @@ const createEditor = ({ content, onCreate, onUpdate, extensions, enableRichEditi HorizontalRule, OrderedList, ListItem, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, TaskList, TaskItem, Callout, diff --git a/src/markdownit/index.js b/src/markdownit/index.js index 5c8d35b14..d4c559533 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.js @@ -6,6 +6,7 @@ import callouts from './callouts' const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('strikethrough') + .enable('table') .use(taskLists, { enable: true, labelAfter: true }) .use(splitMixedLists) .use(underline) diff --git a/src/nodes/Table.js b/src/nodes/Table.js new file mode 100644 index 000000000..12bdf6486 --- /dev/null +++ b/src/nodes/Table.js @@ -0,0 +1,41 @@ +import { Table } from '@tiptap/extension-table' +import { mergeAttributes } from '@tiptap/core' + +export default Table.extend({ + content: 'tableHead tableBody', + + renderHTML({ HTMLAttributes }) { + return ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + + toMarkdown(state, node) { + const widths = [] + const head = node.child(0) + const body = node.child(1) + head.forEach(row => { + state.write('|') + row.forEach(cell => { + widths.push(cell.textContent.length) + state.renderInline(cell) + state.write('|') + }) + }) + state.ensureNewLine() + state.write('|') + widths.forEach(width => { + state.write(state.repeat('-', width)) + state.write('|') + }) + body.forEach(row => { + state.ensureNewLine() + state.write('|') + row.forEach((cell, _, i) => { + state.renderInline(cell) + state.write(state.repeat(' ', widths[i] - cell.textContent.length)) + state.write('|') + }) + }) + state.closeBlock(node) + }, + +}) diff --git a/src/nodes/TableBody.js b/src/nodes/TableBody.js new file mode 100644 index 000000000..fa3cc83ba --- /dev/null +++ b/src/nodes/TableBody.js @@ -0,0 +1,23 @@ +import { Node, mergeAttributes } from '@tiptap/core' + +export default Node.create({ + name: 'tableBody', + content: 'tableRow', + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + parseHTML() { + return [ + { tag: 'tbody' }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['tbody', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + +}) diff --git a/src/nodes/TableCell.js b/src/nodes/TableCell.js new file mode 100644 index 000000000..606f2b428 --- /dev/null +++ b/src/nodes/TableCell.js @@ -0,0 +1,8 @@ +import { TableCell } from '@tiptap/extension-table-cell' + +export default TableCell.extend({ + content: 'inline*', + addAttributes() { + return {} + }, +}) diff --git a/src/nodes/TableHead.js b/src/nodes/TableHead.js new file mode 100644 index 000000000..6e45a3095 --- /dev/null +++ b/src/nodes/TableHead.js @@ -0,0 +1,49 @@ +import { Node, mergeAttributes } from '@tiptap/core' + +const tableHeadRow = Node.create({ + name: 'tableHeadRow', + content: 'tableHeader*', + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + parseHTML() { + return [ + { tag: 'tr' }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['tr', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + +}) + +export default Node.create({ + name: 'tableHead', + content: 'tableHeadRow', + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + addExtensions() { + return [tableHeadRow] + }, + + parseHTML() { + return [ + { tag: 'thead' }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['thead', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + +}) diff --git a/src/nodes/TableHeader.js b/src/nodes/TableHeader.js new file mode 100644 index 000000000..b50603bca --- /dev/null +++ b/src/nodes/TableHeader.js @@ -0,0 +1,8 @@ +import { TableHeader } from '@tiptap/extension-table-header' + +export default TableHeader.extend({ + content: 'inline*', + addAttributes() { + return {} + }, +}) diff --git a/src/nodes/TableRow.js b/src/nodes/TableRow.js new file mode 100644 index 000000000..c42aeee34 --- /dev/null +++ b/src/nodes/TableRow.js @@ -0,0 +1,8 @@ +import { TableRow } from '@tiptap/extension-table-row' + +export default TableRow.extend({ + content: 'tableCell*', + addAttributes() { + return {} + }, +}) diff --git a/src/tests/fixtures/table.html b/src/tests/fixtures/table.html new file mode 100644 index 000000000..f260c28a0 --- /dev/null +++ b/src/tests/fixtures/table.html @@ -0,0 +1,14 @@ +<table> +<thead> +<tr> +<th>heading</th> +<th>other heading</th> +</tr> +</thead> +<tbody> +<tr> +<td>cell</td> +<td>other cell</td> +</tr> +</tbody> +</table> diff --git a/src/tests/fixtures/table.md b/src/tests/fixtures/table.md new file mode 100644 index 000000000..1e7479f58 --- /dev/null +++ b/src/tests/fixtures/table.md @@ -0,0 +1,3 @@ +|heading|other heading| +|-------|-------------| +|cell |other cell | diff --git a/src/tests/fixtures/tableWithOtherStructure.html b/src/tests/fixtures/tableWithOtherStructure.html new file mode 100644 index 000000000..cc56a1799 --- /dev/null +++ b/src/tests/fixtures/tableWithOtherStructure.html @@ -0,0 +1,10 @@ +<table> + <tr> + <th>heading</th> + <td>other heading</td> + </tr> + <tr> + <th>cell</th> + <td>other cell</td> + </tr> +</table> diff --git a/src/tests/markdownit.spec.js b/src/tests/markdownit.spec.js index 69ce3673d..30748860a 100644 --- a/src/tests/markdownit.spec.js +++ b/src/tests/markdownit.spec.js @@ -1,5 +1,7 @@ import markdownit from './../markdownit' import { typesAvailable } from '../markdownit/callouts' +import tableMarkdown from './fixtures/table.md' +import tableHtml from './fixtures/table.html' describe('markdownit', () => { @@ -27,6 +29,11 @@ describe('markdownit', () => { `)) }) + it('renders tables', () => { + const rendered = markdownit.render(tableMarkdown) + expect(rendered).toBe(tableHtml) + }) + describe('callouts', () => { typesAvailable.forEach((type) => { it(`render ${type}`, () => { diff --git a/src/tests/tables.spec.js b/src/tests/tables.spec.js new file mode 100644 index 000000000..7a347eb9e --- /dev/null +++ b/src/tests/tables.spec.js @@ -0,0 +1,36 @@ +import { createEditor } from './../EditorFactory' +import { createMarkdownSerializer } from './../extensions/Markdown' +import markdownit from './../markdownit' +import input from './fixtures/table.md' +import output from './fixtures/table.html' +import otherStructure from './fixtures/tableWithOtherStructure.html' + +describe('Table', () => { + + test('load into editor', () => { + const tiptap = editorWithContent(markdownit.render(input)) + expect(tiptap.getHTML().replaceAll('><', ">\n<")).toBe(output.replace(/\n$/, '')) + }) + + test('serialize from editor', () => { + const tiptap = editorWithContent(markdownit.render(input)) + const serializer = createMarkdownSerializer(tiptap.schema) + expect(serializer.serialize(tiptap.state.doc)).toBe(input.replace(/\n$/, '')) + }) + + test('handle html table with other structure', () => { + const tiptap = editorWithContent( + otherStructure.replace(/\n\s*/g,'') + ) + expect(tiptap.getHTML().replaceAll('><', ">\n<")).toBe(output.replace(/\n$/, '')) + }) + +}) + +function editorWithContent(content) { + return createEditor({ + content, + enableRichEditing: true + }) +} + |