Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax <max@nextcloud.com>2022-03-09 22:07:17 +0300
committerMax <max@nextcloud.com>2022-03-31 15:29:14 +0300
commit4629457932accaa24abefc9a8f444dc9398a94d5 (patch)
tree9d0a5d312e588f627ed905c25c8b13d34060610a
parent0db660bbe12c948e1964915f483ad8e406f72d4d (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>
-rw-r--r--package-lock.json120
-rw-r--r--package.json13
-rw-r--r--src/EditorFactory.js12
-rw-r--r--src/markdownit/index.js1
-rw-r--r--src/nodes/Table.js41
-rw-r--r--src/nodes/TableBody.js23
-rw-r--r--src/nodes/TableCell.js8
-rw-r--r--src/nodes/TableHead.js49
-rw-r--r--src/nodes/TableHeader.js8
-rw-r--r--src/nodes/TableRow.js8
-rw-r--r--src/tests/fixtures/table.html14
-rw-r--r--src/tests/fixtures/table.md3
-rw-r--r--src/tests/fixtures/tableWithOtherStructure.html10
-rw-r--r--src/tests/markdownit.spec.js7
-rw-r--r--src/tests/tables.spec.js36
15 files changed, 351 insertions, 2 deletions
diff --git a/package-lock.json b/package-lock.json
index 495bf7aa7..19413d4dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,10 @@
"@tiptap/extension-paragraph": "^2.0.0-beta.22",
"@tiptap/extension-placeholder": "^2.0.0-beta.44",
"@tiptap/extension-strike": "^2.0.0-beta.26",
+ "@tiptap/extension-table": "^2.0.0-beta.48",
+ "@tiptap/extension-table-cell": "^2.0.0-beta.20",
+ "@tiptap/extension-table-header": "^2.0.0-beta.22",
+ "@tiptap/extension-table-row": "^2.0.0-beta.19",
"@tiptap/extension-task-item": "^2.0.0-beta.31",
"@tiptap/extension-task-list": "^2.0.0-beta.26",
"@tiptap/extension-text": "^2.0.0-beta.15",
@@ -83,6 +87,7 @@
"eslint-plugin-cypress": "^2.12.1",
"jest": "^27.5.1",
"jest-environment-jsdom": "^27.5.1",
+ "jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^2.0.2",
"regenerator-runtime": "^0.13.9"
},
@@ -3758,6 +3763,58 @@
"@tiptap/core": "^2.0.0-beta.1"
}
},
+ "node_modules/@tiptap/extension-table": {
+ "version": "2.0.0-beta.48",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.0.0-beta.48.tgz",
+ "integrity": "sha512-Hcx3kOBQyazQ3dV0Oq4zKIl1og4EqUuZ5nEWxwcb8HgxSUYIhAJQ7pujPZiRLfkoFy92oVwmh9KhBRfQqRkUpQ==",
+ "dependencies": {
+ "prosemirror-tables": "^1.1.1",
+ "prosemirror-view": "^1.23.6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ }
+ },
+ "node_modules/@tiptap/extension-table-cell": {
+ "version": "2.0.0-beta.20",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.0.0-beta.20.tgz",
+ "integrity": "sha512-IllQyxLQvgm1FAewz3U+DkgNHRthmuVrtUQnG6la45qdUOLCOrpFbRRaQ1LJ/BpbvZ2Xs1o2yAa97BqZOPwovQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ }
+ },
+ "node_modules/@tiptap/extension-table-header": {
+ "version": "2.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.0.0-beta.22.tgz",
+ "integrity": "sha512-nMrghrfl+ZS4EDixs3lgXnHw1Q+ECyTugpRvS36rP7b8GFp3GXm9DfbIAUzwGGfcq1D7DwRnJUDM6ARdWXyw0w==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ }
+ },
+ "node_modules/@tiptap/extension-table-row": {
+ "version": "2.0.0-beta.19",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.0.0-beta.19.tgz",
+ "integrity": "sha512-ldEVDpIUX7ZqbViTy4c/RfyNGRv++O/r3A/Ivuon1PykaDDTbPlp5JM89FunAD39cLAbo2HKtweqdmzCMlZsqA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ }
+ },
"node_modules/@tiptap/extension-task-item": {
"version": "2.0.0-beta.31",
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.0-beta.31.tgz",
@@ -13256,6 +13313,12 @@
}
}
},
+ "node_modules/jest-raw-loader": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
+ "integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
+ "dev": true
+ },
"node_modules/jest-regex-util": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
@@ -16881,6 +16944,18 @@
"prosemirror-transform": "^1.0.0"
}
},
+ "node_modules/prosemirror-tables": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
+ "integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
+ "dependencies": {
+ "prosemirror-keymap": "^1.1.2",
+ "prosemirror-model": "^1.8.1",
+ "prosemirror-state": "^1.3.1",
+ "prosemirror-transform": "^1.2.1",
+ "prosemirror-view": "^1.13.3"
+ }
+ },
"node_modules/prosemirror-transform": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.4.0.tgz",
@@ -24058,6 +24133,33 @@
"integrity": "sha512-2dmCgtesuDdivM/54Q+Y6Tc3JbGz1SkHP6c62piuqBiYLWg3xa16zChZOhfN8szbbQlBgLT6XRTDt3c2Ux+Dug==",
"requires": {}
},
+ "@tiptap/extension-table": {
+ "version": "2.0.0-beta.48",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.0.0-beta.48.tgz",
+ "integrity": "sha512-Hcx3kOBQyazQ3dV0Oq4zKIl1og4EqUuZ5nEWxwcb8HgxSUYIhAJQ7pujPZiRLfkoFy92oVwmh9KhBRfQqRkUpQ==",
+ "requires": {
+ "prosemirror-tables": "^1.1.1",
+ "prosemirror-view": "^1.23.6"
+ }
+ },
+ "@tiptap/extension-table-cell": {
+ "version": "2.0.0-beta.20",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.0.0-beta.20.tgz",
+ "integrity": "sha512-IllQyxLQvgm1FAewz3U+DkgNHRthmuVrtUQnG6la45qdUOLCOrpFbRRaQ1LJ/BpbvZ2Xs1o2yAa97BqZOPwovQ==",
+ "requires": {}
+ },
+ "@tiptap/extension-table-header": {
+ "version": "2.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.0.0-beta.22.tgz",
+ "integrity": "sha512-nMrghrfl+ZS4EDixs3lgXnHw1Q+ECyTugpRvS36rP7b8GFp3GXm9DfbIAUzwGGfcq1D7DwRnJUDM6ARdWXyw0w==",
+ "requires": {}
+ },
+ "@tiptap/extension-table-row": {
+ "version": "2.0.0-beta.19",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.0.0-beta.19.tgz",
+ "integrity": "sha512-ldEVDpIUX7ZqbViTy4c/RfyNGRv++O/r3A/Ivuon1PykaDDTbPlp5JM89FunAD39cLAbo2HKtweqdmzCMlZsqA==",
+ "requires": {}
+ },
"@tiptap/extension-task-item": {
"version": "2.0.0-beta.31",
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.0-beta.31.tgz",
@@ -31486,6 +31588,12 @@
"dev": true,
"requires": {}
},
+ "jest-raw-loader": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
+ "integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
+ "dev": true
+ },
"jest-regex-util": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
@@ -34314,6 +34422,18 @@
"prosemirror-transform": "^1.0.0"
}
},
+ "prosemirror-tables": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
+ "integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
+ "requires": {
+ "prosemirror-keymap": "^1.1.2",
+ "prosemirror-model": "^1.8.1",
+ "prosemirror-state": "^1.3.1",
+ "prosemirror-transform": "^1.2.1",
+ "prosemirror-view": "^1.13.3"
+ }
+ },
"prosemirror-transform": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.4.0.tgz",
diff --git a/package.json b/package.json
index 872c15a2c..d77292092 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,10 @@
"@tiptap/extension-paragraph": "^2.0.0-beta.22",
"@tiptap/extension-placeholder": "^2.0.0-beta.44",
"@tiptap/extension-strike": "^2.0.0-beta.26",
+ "@tiptap/extension-table": "^2.0.0-beta.48",
+ "@tiptap/extension-table-cell": "^2.0.0-beta.20",
+ "@tiptap/extension-table-header": "^2.0.0-beta.22",
+ "@tiptap/extension-table-row": "^2.0.0-beta.19",
"@tiptap/extension-task-item": "^2.0.0-beta.31",
"@tiptap/extension-task-list": "^2.0.0-beta.26",
"@tiptap/extension-text": "^2.0.0-beta.15",
@@ -104,6 +108,7 @@
"eslint-plugin-cypress": "^2.12.1",
"jest": "^27.5.1",
"jest-environment-jsdom": "^27.5.1",
+ "jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^2.0.2",
"regenerator-runtime": "^0.13.9"
},
@@ -112,7 +117,9 @@
"testEnvironment": "jest-environment-jsdom",
"moduleFileExtensions": [
"js",
- "vue"
+ "vue",
+ "md",
+ "html"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
@@ -124,7 +131,9 @@
],
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
- ".*\\.(vue)$": "<rootDir>/node_modules/@vue/vue2-jest"
+ ".*\\.(vue)$": "<rootDir>/node_modules/@vue/vue2-jest",
+ ".*\\.md$": "jest-raw-loader",
+ ".*\\.html$": "jest-raw-loader"
},
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
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
+ })
+}
+