From d275d0b1e94e3c5e37e9185ff48fc045cee6c29e Mon Sep 17 00:00:00 2001
From: Max
Date: Thu, 16 May 2024 15:51:01 +0200
Subject: [PATCH 01/17] enh(editorFactory): only use named exports
Signed-off-by: Max
---
src/EditorFactory.js | 10 +++-------
src/tests/builders.js | 2 +-
src/tests/helpers.js | 2 +-
src/tests/markdown.spec.js | 2 +-
src/tests/tiptap.spec.js | 2 +-
5 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index cdb667a5681..2c6472c1757 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -33,7 +33,7 @@ import { FocusTrap, Mention, PlainText, RichText } from './extensions/index.js'
// eslint-disable-next-line import/no-named-as-default
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
-const loadSyntaxHighlight = async (language) => {
+export const loadSyntaxHighlight = async (language) => {
const list = hljs.listLanguages()
logger.debug('Supported languages', { list })
if (!lowlight.listLanguages().includes(language)) {
@@ -49,7 +49,7 @@ const loadSyntaxHighlight = async (language) => {
}
}
-const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
+export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
let defaultExtensions
if (enableRichEditing) {
defaultExtensions = [
@@ -70,7 +70,6 @@ const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, exte
} else {
defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })]
}
-
return new Editor({
onCreate,
onUpdate,
@@ -82,9 +81,6 @@ const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, exte
})
}
-const serializePlainText = (doc) => {
+export const serializePlainText = (doc) => {
return doc.textContent
}
-
-export default createEditor
-export { createEditor, serializePlainText, loadSyntaxHighlight }
diff --git a/src/tests/builders.js b/src/tests/builders.js
index 5f571f0de06..a3f7de05526 100644
--- a/src/tests/builders.js
+++ b/src/tests/builders.js
@@ -1,7 +1,7 @@
import { expect } from '@jest/globals';
import { Mark, Node } from '@tiptap/pm/model'
import { builders } from 'prosemirror-test-builder'
-import createEditor from '../EditorFactory'
+import { createEditor } from '../EditorFactory'
export function getBuilders() {
diff --git a/src/tests/helpers.js b/src/tests/helpers.js
index 38c17153d6a..401dec40565 100644
--- a/src/tests/helpers.js
+++ b/src/tests/helpers.js
@@ -5,7 +5,7 @@ import Document from '@tiptap/extension-document'
import Paragraph from '../nodes/Paragraph'
import Text from '@tiptap/extension-text'
-import createEditor from '../EditorFactory'
+import { createEditor } from '../EditorFactory'
import markdownit from '../markdownit'
export function createCustomEditor({ content, extensions }) {
diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js
index 7581ea66ee6..d473116679b 100644
--- a/src/tests/markdown.spec.js
+++ b/src/tests/markdown.spec.js
@@ -6,7 +6,7 @@ import {
markdownFromPaste
} from './helpers.js'
import { createMarkdownSerializer } from "../extensions/Markdown";
-import createEditor from "../EditorFactory";
+import { createEditor } from "../EditorFactory";
/*
* This file is for various markdown tests, mainly testing if input and output stays the same.
diff --git a/src/tests/tiptap.spec.js b/src/tests/tiptap.spec.js
index 1a5fbb27a97..a0a615ef982 100644
--- a/src/tests/tiptap.spec.js
+++ b/src/tests/tiptap.spec.js
@@ -1,4 +1,4 @@
-import createEditor from '../EditorFactory'
+import { createEditor } from '../EditorFactory'
import markdownit from '../markdownit'
const renderedHTML = ( markdown ) => {
From 0d8f3b24c1613aff604c28c54a339a95474fc517 Mon Sep 17 00:00:00 2001
From: Max
Date: Thu, 16 May 2024 16:05:45 +0200
Subject: [PATCH 02/17] cleanup(deps): remove proxy-polyfill
Signed-off-by: Max
---
package-lock.json | 11 -----------
package.json | 1 -
src/EditorFactory.js | 2 --
3 files changed, 14 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d3796fdc28e..b00e21021bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -74,7 +74,6 @@
"mermaid": "^10.9.1",
"mitt": "^3.0.1",
"path-normalize": "^6.0.13",
- "proxy-polyfill": "^0.3.2",
"slug": "^9.0.0",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1",
@@ -22399,11 +22398,6 @@
"integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
"dev": true
},
- "node_modules/proxy-polyfill": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/proxy-polyfill/-/proxy-polyfill-0.3.2.tgz",
- "integrity": "sha512-ENKSXOMCewnQTOyqrQXxEjIhzT6dy572mtehiItbDoIUF5Sv5UkmRUc8kowg2MFvr232Uo8rwRpNg3V5kgTKbA=="
- },
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -43809,11 +43803,6 @@
"integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
"dev": true
},
- "proxy-polyfill": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/proxy-polyfill/-/proxy-polyfill-0.3.2.tgz",
- "integrity": "sha512-ENKSXOMCewnQTOyqrQXxEjIhzT6dy572mtehiItbDoIUF5Sv5UkmRUc8kowg2MFvr232Uo8rwRpNg3V5kgTKbA=="
- },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
diff --git a/package.json b/package.json
index 0165c5e427a..1a019341421 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,6 @@
"mermaid": "^10.9.1",
"mitt": "^3.0.1",
"path-normalize": "^6.0.13",
- "proxy-polyfill": "^0.3.2",
"slug": "^9.0.0",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1",
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 2c6472c1757..1924f5d6e0e 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -22,8 +22,6 @@
import MentionSuggestion from './components/Suggestion/Mention/suggestions.js'
-import 'proxy-polyfill'
-
import { Editor } from '@tiptap/core'
import { lowlight } from 'lowlight/lib/core.js'
import hljs from 'highlight.js/lib/core'
From f2609f5f69153e121f7e0a882aa7fac722e1de3e Mon Sep 17 00:00:00 2001
From: Max
Date: Thu, 16 May 2024 16:24:52 +0200
Subject: [PATCH 03/17] cleanup(EditorFactory): inline serializPlainText
Also drop the content arg to the serialize function
as it is never provided
Signed-off-by: Max
---
src/EditorFactory.js | 4 ----
src/components/Editor.vue | 6 +++---
src/tests/plaintext.spec.js | 4 ++--
3 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 1924f5d6e0e..42c3c3088db 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -78,7 +78,3 @@ export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {
extensions: defaultExtensions.concat(extensions || []),
})
}
-
-export const serializePlainText = (doc) => {
- return doc.textContent
-}
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index d6a3482cc18..c87e14a53d1 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -114,7 +114,7 @@ import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService
import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
import { extensionHighlight } from '../helpers/mappings.js'
-import { createEditor, serializePlainText, loadSyntaxHighlight } from './../EditorFactory.js'
+import { createEditor, loadSyntaxHighlight } from './../EditorFactory.js'
import { createMarkdownSerializer } from './../extensions/Markdown.js'
import markdownit from './../markdownit/index.js'
@@ -393,8 +393,8 @@ export default {
baseVersionEtag: this.$syncService?.baseVersionEtag,
forceRecreate: this.forceRecreate,
serialize: this.isRichEditor
- ? (content) => createMarkdownSerializer(this.$editor.schema).serialize(content ?? this.$editor.state.doc)
- : (content) => serializePlainText(content ?? this.$editor.state.doc),
+ ? () => createMarkdownSerializer(this.$editor.schema).serialize(this.$editor.state.doc)
+ : () => this.$editor.state.doc.textContent,
getDocumentState: () => getDocumentState(this.$ydoc),
})
diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js
index fdd68fa0eb4..6fffd404680 100644
--- a/src/tests/plaintext.spec.js
+++ b/src/tests/plaintext.spec.js
@@ -1,4 +1,4 @@
-import { createEditor, serializePlainText } from './../EditorFactory';
+import { createEditor } from './../EditorFactory';
import spec from "./fixtures/spec"
import xssFuzzVectors from './fixtures/xssFuzzVectors';
@@ -17,7 +17,7 @@ const plaintextThroughEditor = (markdown) => {
enableRichEditing: false
})
tiptap.commands.setContent(content)
- return serializePlainText(tiptap.state.doc) || 'failed'
+ return tiptap.state.doc.textContent || 'failed'
}
describe('commonmark as plaintext', () => {
From cb72b038da266ede245bbc85537e9f76626960f2 Mon Sep 17 00:00:00 2001
From: Max
Date: Thu, 16 May 2024 23:07:16 +0200
Subject: [PATCH 04/17] enh(serialize): also use markdown approach for
plaintext
We simply overwrite the `toMarkdown` function
and then rely on that.
Signed-off-by: Max
---
src/EditorFactory.js | 12 ++++++++----
src/components/Editor.vue | 6 ++----
src/extensions/Markdown.js | 7 ++++++-
src/nodes/PlainTextDocument.js | 1 -
src/nodes/PlainTextLowlight.js | 17 +++++++++++++++++
src/tests/plaintext.spec.js | 3 ++-
6 files changed, 35 insertions(+), 11 deletions(-)
create mode 100644 src/nodes/PlainTextLowlight.js
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 42c3c3088db..7ce74806200 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -27,9 +27,8 @@ import { lowlight } from 'lowlight/lib/core.js'
import hljs from 'highlight.js/lib/core'
import { logger } from './helpers/logger.js'
-import { FocusTrap, Mention, PlainText, RichText } from './extensions/index.js'
-// eslint-disable-next-line import/no-named-as-default
-import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
+import { FocusTrap, Mention, PlainText, RichText, Markdown } from './extensions/index.js'
+import { PlainTextLowlight } from './nodes/PlainTextLowlight.js'
export const loadSyntaxHighlight = async (language) => {
const list = hljs.listLanguages()
@@ -66,7 +65,12 @@ export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {
FocusTrap,
]
} else {
- defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })]
+ defaultExtensions = [
+ Markdown,
+ PlainText,
+ PlainTextLowlight
+ .configure({ lowlight, defaultLanguage: language }),
+ ]
}
return new Editor({
onCreate,
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index c87e14a53d1..33e9652b00d 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -115,7 +115,7 @@ import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
import { extensionHighlight } from '../helpers/mappings.js'
import { createEditor, loadSyntaxHighlight } from './../EditorFactory.js'
-import { createMarkdownSerializer } from './../extensions/Markdown.js'
+import { serializeEditorContent } from './../extensions/Markdown.js'
import markdownit from './../markdownit/index.js'
import { CollaborationCursor } from '../extensions/index.js'
@@ -392,9 +392,7 @@ export default {
filePath: this.relativePath,
baseVersionEtag: this.$syncService?.baseVersionEtag,
forceRecreate: this.forceRecreate,
- serialize: this.isRichEditor
- ? () => createMarkdownSerializer(this.$editor.schema).serialize(this.$editor.state.doc)
- : () => this.$editor.state.doc.textContent,
+ serialize: () => serializeEditorContent(this.$editor),
getDocumentState: () => getDocumentState(this.$ydoc),
})
diff --git a/src/extensions/Markdown.js b/src/extensions/Markdown.js
index 4bce626b90a..0031aabf7b8 100644
--- a/src/extensions/Markdown.js
+++ b/src/extensions/Markdown.js
@@ -191,5 +191,10 @@ const convertNames = (object) => {
)
}
-export { createMarkdownSerializer }
+function serializeEditorContent({ schema, state }) {
+ return createMarkdownSerializer(schema)
+ .serialize(state.doc)
+}
+
+export { serializeEditorContent, createMarkdownSerializer }
export default Markdown
diff --git a/src/nodes/PlainTextDocument.js b/src/nodes/PlainTextDocument.js
index 291c1d31457..6b95559ffda 100644
--- a/src/nodes/PlainTextDocument.js
+++ b/src/nodes/PlainTextDocument.js
@@ -30,5 +30,4 @@ export default Node.create({
Tab: () => this.editor.commands.insertContent('\t'),
}
},
-
})
diff --git a/src/nodes/PlainTextLowlight.js b/src/nodes/PlainTextLowlight.js
new file mode 100644
index 00000000000..48fa2682288
--- /dev/null
+++ b/src/nodes/PlainTextLowlight.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Max
+ * SPDX-License-Identifier: @license AGPL-3.0-or-later
+ *
+ */
+
+// eslint-disable-next-line import/no-named-as-default
+import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
+
+const PlainTextLowlight = CodeBlockLowlight.extend({
+ name: 'PlainTextLowlight',
+ toMarkdown(state, node) {
+ state.write(node.textContent)
+ },
+})
+
+export { PlainTextLowlight }
diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js
index 6fffd404680..bc60bc420aa 100644
--- a/src/tests/plaintext.spec.js
+++ b/src/tests/plaintext.spec.js
@@ -1,4 +1,5 @@
import { createEditor } from './../EditorFactory';
+import { serializeEditorContent } from './../extensions/Markdown.js'
import spec from "./fixtures/spec"
import xssFuzzVectors from './fixtures/xssFuzzVectors';
@@ -17,7 +18,7 @@ const plaintextThroughEditor = (markdown) => {
enableRichEditing: false
})
tiptap.commands.setContent(content)
- return tiptap.state.doc.textContent || 'failed'
+ return serializeEditorContent(tiptap) || 'failed'
}
describe('commonmark as plaintext', () => {
From 0c89a2e5b37f5e67a3cb904dcdfced49bf29c78c Mon Sep 17 00:00:00 2001
From: Max
Date: Fri, 17 May 2024 12:38:54 +0200
Subject: [PATCH 05/17] enh(Serializer): split from Markdown extension
We are now using this for serializing plaintext as well.
Signed-off-by: Max
---
cypress/e2e/nodes/ListItem.spec.js | 4 +-
cypress/e2e/nodes/Preview.spec.js | 6 +-
cypress/e2e/nodes/Table.spec.js | 9 +-
cypress/e2e/nodes/helpers.js | 7 +-
src/EditorFactory.js | 5 +-
src/components/Editor.vue | 2 +-
.../Editor/MarkdownContentEditor.vue | 4 +-
src/extensions/Markdown.js | 101 +-----------------
src/extensions/PlainText.js | 4 +-
src/extensions/RichText.js | 2 +
src/extensions/Serializer.js | 74 +++++++++++++
src/extensions/index.js | 2 +
src/helpers/serialize.js | 46 ++++++++
src/tests/extensions/Markdown.spec.js | 96 +++--------------
src/tests/extensions/Serializer.spec.js | 81 ++++++++++++++
src/tests/helpers.js | 11 +-
src/tests/markdown.spec.js | 6 +-
src/tests/nodes/Preview.spec.js | 4 +-
src/tests/nodes/Table.spec.js | 6 +-
src/tests/nodes/TaskItem.spec.js | 4 +-
src/tests/plaintext.spec.js | 2 +-
21 files changed, 256 insertions(+), 220 deletions(-)
create mode 100644 src/extensions/Serializer.js
create mode 100644 src/helpers/serialize.js
create mode 100644 src/tests/extensions/Serializer.spec.js
diff --git a/cypress/e2e/nodes/ListItem.spec.js b/cypress/e2e/nodes/ListItem.spec.js
index 3a02f5d9208..3b6fae80393 100644
--- a/cypress/e2e/nodes/ListItem.spec.js
+++ b/cypress/e2e/nodes/ListItem.spec.js
@@ -5,7 +5,7 @@ import ListItem from '@tiptap/extension-list-item'
import TaskList from './../../../src/nodes/TaskList.js'
import TaskItem from './../../../src/nodes/TaskItem.js'
import BulletList from './../../../src/nodes/BulletList.js'
-import Markdown from './../../../src/extensions/Markdown.js'
+import Serializer from './../../../src/extensions/Serializer.js'
import { createCustomEditor } from './../../support/components.js'
import { loadMarkdown, runCommands, expectMarkdown } from './helpers.js'
@@ -18,7 +18,7 @@ describe('ListItem extension integrated in the editor', () => {
const editor = createCustomEditor({
content: '',
extensions: [
- Markdown,
+ Serializer,
BulletList,
OrderedList,
ListItem,
diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js
index b6cca10eeae..bed1140a6ea 100644
--- a/cypress/e2e/nodes/Preview.spec.js
+++ b/cypress/e2e/nodes/Preview.spec.js
@@ -21,7 +21,7 @@
*
*/
-import Markdown from './../../../src/extensions/Markdown.js'
+import Serializer from './../../../src/extensions/Serializer.js'
import Preview from './../../../src/nodes/Preview.js'
import { Italic, Link } from './../../../src/marks/index.js'
import { createCustomEditor } from './../../support/components.js'
@@ -36,7 +36,7 @@ describe('Preview extension', { retries: 0 }, () => {
const editor = createCustomEditor({
content: '',
extensions: [
- Markdown,
+ Serializer,
Preview,
Link,
Italic,
@@ -186,7 +186,7 @@ describe('Markdown tests for Previews in the editor', { retries: 0 }, () => {
const editor = createCustomEditor({
content: '',
extensions: [
- Markdown,
+ Serializer,
Preview,
Link,
],
diff --git a/cypress/e2e/nodes/Table.spec.js b/cypress/e2e/nodes/Table.spec.js
index ca9550ab972..b168d9be03e 100644
--- a/cypress/e2e/nodes/Table.spec.js
+++ b/cypress/e2e/nodes/Table.spec.js
@@ -1,10 +1,11 @@
import { findChildren } from './../../../src/helpers/prosemirrorUtils.js'
import { initUserAndFiles, randUser } from '../../utils/index.js'
import { createCustomEditor } from './../../support/components.js'
+import { getMarkdown } from './helpers.js'
import markdownit from './../../../src/markdownit/index.js'
import EditableTable from './../../../src/nodes/EditableTable.js'
-import Markdown, { createMarkdownSerializer } from './../../../src/extensions/Markdown.js'
+import { Serializer } from './../../../src/extensions/Serializer.js'
// https://github.com/import-js/eslint-plugin-import/issues/1739
/* eslint-disable-next-line import/no-unresolved */
@@ -146,7 +147,7 @@ describe('Table extension integrated in the editor', () => {
const editor = createCustomEditor({
content: '',
extensions: [
- Markdown,
+ Serializer,
EditableTable,
],
})
@@ -198,8 +199,4 @@ describe('Table extension integrated in the editor', () => {
expect(getMarkdown().replace(/\n$/, '')).to.equal(markdown)
}
- const getMarkdown = () => {
- const serializer = createMarkdownSerializer(editor.schema)
- return serializer.serialize(editor.state.doc)
- }
})
diff --git a/cypress/e2e/nodes/helpers.js b/cypress/e2e/nodes/helpers.js
index 4886dd8243a..4c135b9b64b 100644
--- a/cypress/e2e/nodes/helpers.js
+++ b/cypress/e2e/nodes/helpers.js
@@ -22,7 +22,7 @@
import markdownit from './../../../src/markdownit/index.js'
import { findChildren } from './../../../src/helpers/prosemirrorUtils.js'
-import { createMarkdownSerializer } from './../../../src/extensions/Markdown.js'
+import { serializeEditorContent } from './../../../src/extensions/Serializer.js'
/**
*
@@ -74,7 +74,6 @@ export function expectMarkdown(editor, markdown) {
*
* @param editor
*/
-function getMarkdown(editor) {
- const serializer = createMarkdownSerializer(editor.schema)
- return serializer.serialize(editor.state.doc)
+export function getMarkdown(editor) {
+ return serializeEditorContent(editor)
}
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 7ce74806200..9c2a2a29338 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -27,7 +27,7 @@ import { lowlight } from 'lowlight/lib/core.js'
import hljs from 'highlight.js/lib/core'
import { logger } from './helpers/logger.js'
-import { FocusTrap, Mention, PlainText, RichText, Markdown } from './extensions/index.js'
+import { FocusTrap, Mention, PlainText, RichText } from './extensions/index.js'
import { PlainTextLowlight } from './nodes/PlainTextLowlight.js'
export const loadSyntaxHighlight = async (language) => {
@@ -50,6 +50,7 @@ export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {
let defaultExtensions
if (enableRichEditing) {
defaultExtensions = [
+ FocusTrap,
RichText.configure({
relativePath,
isEmbedded,
@@ -62,11 +63,9 @@ export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {
}),
],
}),
- FocusTrap,
]
} else {
defaultExtensions = [
- Markdown,
PlainText,
PlainTextLowlight
.configure({ lowlight, defaultLanguage: language }),
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 33e9652b00d..e079a9141bc 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -115,7 +115,7 @@ import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
import { extensionHighlight } from '../helpers/mappings.js'
import { createEditor, loadSyntaxHighlight } from './../EditorFactory.js'
-import { serializeEditorContent } from './../extensions/Markdown.js'
+import { serializeEditorContent } from './../extensions/Serializer.js'
import markdownit from './../markdownit/index.js'
import { CollaborationCursor } from '../extensions/index.js'
diff --git a/src/components/Editor/MarkdownContentEditor.vue b/src/components/Editor/MarkdownContentEditor.vue
index 68f95ff3ce6..6abf48d4383 100644
--- a/src/components/Editor/MarkdownContentEditor.vue
+++ b/src/components/Editor/MarkdownContentEditor.vue
@@ -42,7 +42,7 @@ import { Editor } from '@tiptap/core'
import History from '@tiptap/extension-history'
import { getCurrentUser } from '@nextcloud/auth'
import { ATTACHMENT_RESOLVER, EDITOR, IS_RICH_EDITOR } from '../Editor.provider.js'
-import { createMarkdownSerializer } from '../../extensions/Markdown.js'
+import { serializeEditorContent } from '../../extensions/Serializer.js'
import AttachmentResolver from '../../services/AttachmentResolver.js'
import markdownit from '../../markdownit/index.js'
import { RichText } from '../../extensions/index.js'
@@ -150,7 +150,7 @@ export default {
content: this.htmlContent,
extensions: this.extensions(),
onUpdate: ({ editor }) => {
- const markdown = (createMarkdownSerializer(this.$editor.schema)).serialize(editor.state.doc)
+ const markdown = serializeEditorContent(this.$editor)
this.emit('update:content', {
json: editor.state.doc,
markdown,
diff --git a/src/extensions/Markdown.js b/src/extensions/Markdown.js
index 0031aabf7b8..fc43d431821 100644
--- a/src/extensions/Markdown.js
+++ b/src/extensions/Markdown.js
@@ -21,56 +21,21 @@
*/
/*
- * Tiptap extension to ease customize the serialization to markdown
- *
- * Most markdown serialization can be handled by `prosemirror-markdown`.
- * In order to make it easier to add custom markdown rendering
- * this extension will extend the prosemirror schema for nodes and marks
- * with a `toMarkdown` specification if that is defined in a tiptap extension.
- *
- * For nodes `toMarkown` should be function
- * that take a serializer state and such a node, and serializes the node.
- *
- * For marks `toMarkdown` is an object with open and close properties,
- * which hold the strings that should appear before and after.
- *
- * For more details see
- * https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializer
+ * Tiptap extension to allow copy and paste of markdown
*/
-import { Extension, getExtensionField } from '@tiptap/core'
+import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
-import { MarkdownSerializer, defaultMarkdownSerializer } from '@tiptap/pm/markdown'
+import { MarkdownSerializer } from '@tiptap/pm/markdown'
import { DOMParser } from '@tiptap/pm/model'
import markdownit from '../markdownit/index.js'
import transformPastedHTML from './transformPastedHTML.js'
+import { extractNodesToMarkdown, extractToPlaintext } from '../helpers/serialize.js'
const Markdown = Extension.create({
name: 'markdown',
- extendMarkSchema(extension) {
- const context = {
- name: extension.name,
- options: extension.options,
- storage: extension.storage,
- }
- return {
- toMarkdown: getExtensionField(extension, 'toMarkdown', context),
- }
- },
-
- extendNodeSchema(extension) {
- const context = {
- name: extension.name,
- options: extension.options,
- storage: extension.storage,
- }
- return {
- toMarkdown: getExtensionField(extension, 'toMarkdown', context),
- }
- },
-
addProseMirrorPlugins() {
let shiftKey = false
@@ -127,18 +92,6 @@ const Markdown = Extension.create({
},
})
-const createMarkdownSerializer = ({ nodes, marks }) => {
- return {
- serializer: new MarkdownSerializer(
- extractNodesToMarkdown(nodes),
- extractMarksToMarkdown(marks),
- ),
- serialize(content, options) {
- return this.serializer.serialize(content, { ...options, tightLists: true })
- },
- }
-}
-
const clipboardSerializer = ({ nodes, marks }) => {
return {
serializer: new MarkdownSerializer(
@@ -151,50 +104,4 @@ const clipboardSerializer = ({ nodes, marks }) => {
}
}
-const extractToPlaintext = (marks) => {
- const blankMark = { open: '', close: '', mixable: true, expelEnclosingWhitespace: true }
- const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
- const markEntries = Object.entries({ ...defaultMarks, ...marks })
- .map(([name, _mark]) => [name, blankMark])
-
- return Object.fromEntries(markEntries)
-}
-
-const extractToMarkdown = (nodesOrMarks) => {
- const nodeOrMarkEntries = Object
- .entries(nodesOrMarks)
- .map(([name, nodeOrMark]) => [name, nodeOrMark.spec.toMarkdown])
- .filter(([, toMarkdown]) => toMarkdown)
-
- return Object.fromEntries(nodeOrMarkEntries)
-}
-
-const extractNodesToMarkdown = (nodes) => {
- const defaultNodes = convertNames(defaultMarkdownSerializer.nodes)
- const nodesToMarkdown = extractToMarkdown(nodes)
- return { ...defaultNodes, ...nodesToMarkdown }
-}
-
-const extractMarksToMarkdown = (marks) => {
- const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
- const marksToMarkdown = extractToMarkdown(marks)
- return { ...defaultMarks, ...marksToMarkdown }
-}
-
-const convertNames = (object) => {
- const convert = (name) => {
- return name.replace(/_(\w)/g, (_m, letter) => letter.toUpperCase())
- }
- return Object.fromEntries(
- Object.entries(object)
- .map(([name, value]) => [convert(name), value]),
- )
-}
-
-function serializeEditorContent({ schema, state }) {
- return createMarkdownSerializer(schema)
- .serialize(state.doc)
-}
-
-export { serializeEditorContent, createMarkdownSerializer }
export default Markdown
diff --git a/src/extensions/PlainText.js b/src/extensions/PlainText.js
index 305f24016b9..d13818593be 100644
--- a/src/extensions/PlainText.js
+++ b/src/extensions/PlainText.js
@@ -22,9 +22,10 @@
import { Extension } from '@tiptap/core'
+import PlainTextDocument from './../nodes/PlainTextDocument.js'
+import Serializer from './../extensions/Serializer.js'
/* eslint-disable import/no-named-as-default */
import Text from '@tiptap/extension-text'
-import PlainTextDocument from './../nodes/PlainTextDocument.js'
export default Extension.create({
name: 'PlainText',
@@ -32,6 +33,7 @@ export default Extension.create({
addExtensions() {
return [
PlainTextDocument,
+ Serializer,
Text,
]
},
diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js
index 1ab826202e6..651865de9fd 100644
--- a/src/extensions/RichText.js
+++ b/src/extensions/RichText.js
@@ -52,6 +52,7 @@ import OrderedList from '@tiptap/extension-ordered-list'
import Paragraph from './../nodes/Paragraph.js'
import Placeholder from '@tiptap/extension-placeholder'
import Preview from './../nodes/Preview.js'
+import Serializer from './../extensions/Serializer.js'
import Table from './../nodes/Table.js'
import TaskItem from './../nodes/TaskItem.js'
import TaskList from './../nodes/TaskList.js'
@@ -80,6 +81,7 @@ export default Extension.create({
addExtensions() {
const defaultExtensions = [
this.options.editing ? Markdown : null,
+ this.options.editing ? Serializer : null,
Document,
Text,
Paragraph,
diff --git a/src/extensions/Serializer.js b/src/extensions/Serializer.js
new file mode 100644
index 00000000000..2e6273d889a
--- /dev/null
+++ b/src/extensions/Serializer.js
@@ -0,0 +1,74 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Max
+ * SPDX-License-Identifier: @license AGPL-3.0-or-later
+ *
+ */
+
+/*
+ * Tiptap extension to ease customize the serialization to markdown
+ *
+ * Most markdown serialization can be handled by `prosemirror-markdown`.
+ * In order to make it easier to add custom markdown rendering
+ * this extension will extend the prosemirror schema for nodes and marks
+ * with a `toMarkdown` specification if that is defined in a tiptap extension.
+ *
+ * For nodes `toMarkown` should be function
+ * that take a serializer state and such a node, and serializes the node.
+ *
+ * For marks `toMarkdown` is an object with open and close properties,
+ * which hold the strings that should appear before and after.
+ *
+ * For more details see
+ * https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializer
+ */
+
+import { Extension, getExtensionField } from '@tiptap/core'
+import { MarkdownSerializer } from '@tiptap/pm/markdown'
+import { extractNodesToMarkdown, extractMarksToMarkdown } from '../helpers/serialize.js'
+
+export function serializeEditorContent({ schema, state }) {
+ return createMarkdownSerializer(schema)
+ .serialize(state.doc)
+}
+
+export const Serializer = Extension.create({
+
+ name: 'serializer',
+
+ extendMarkSchema(extension) {
+ const context = {
+ name: extension.name,
+ options: extension.options,
+ storage: extension.storage,
+ }
+ return {
+ toMarkdown: getExtensionField(extension, 'toMarkdown', context),
+ }
+ },
+
+ extendNodeSchema(extension) {
+ const context = {
+ name: extension.name,
+ options: extension.options,
+ storage: extension.storage,
+ }
+ return {
+ toMarkdown: getExtensionField(extension, 'toMarkdown', context),
+ }
+ },
+
+})
+
+export const createMarkdownSerializer = ({ nodes, marks }) => {
+ return {
+ serializer: new MarkdownSerializer(
+ extractNodesToMarkdown(nodes),
+ extractMarksToMarkdown(marks),
+ ),
+ serialize(content, options) {
+ return this.serializer.serialize(content, { ...options, tightLists: true })
+ },
+ }
+}
+
+export default Serializer
diff --git a/src/extensions/index.js b/src/extensions/index.js
index 0127386b12e..45fb0b48be0 100644
--- a/src/extensions/index.js
+++ b/src/extensions/index.js
@@ -30,6 +30,7 @@ import PlainText from './PlainText.js'
import RichText from './RichText.js'
import KeepSyntax from './KeepSyntax.js'
import Mention from './Mention.js'
+import Serializer from './Serializer.js'
export {
CollaborationCursor,
@@ -42,4 +43,5 @@ export {
RichText,
KeepSyntax,
Mention,
+ Serializer,
}
diff --git a/src/helpers/serialize.js b/src/helpers/serialize.js
new file mode 100644
index 00000000000..e1109735c3b
--- /dev/null
+++ b/src/helpers/serialize.js
@@ -0,0 +1,46 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Max
+ * SPDX-License-Identifier: @license AGPL-3.0-or-later
+ *
+ */
+
+import { defaultMarkdownSerializer } from '@tiptap/pm/markdown'
+
+export const extractNodesToMarkdown = (nodes) => {
+ const defaultNodes = convertNames(defaultMarkdownSerializer.nodes)
+ const nodesToMarkdown = extractToMarkdown(nodes)
+ return { ...defaultNodes, ...nodesToMarkdown }
+}
+
+export const extractMarksToMarkdown = (marks) => {
+ const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
+ const marksToMarkdown = extractToMarkdown(marks)
+ return { ...defaultMarks, ...marksToMarkdown }
+}
+
+export const extractToPlaintext = (marks) => {
+ const blankMark = { open: '', close: '', mixable: true, expelEnclosingWhitespace: true }
+ const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
+ const markEntries = Object.entries({ ...defaultMarks, ...marks })
+ .map(([name, _mark]) => [name, blankMark])
+ return Object.fromEntries(markEntries)
+}
+
+const convertNames = (object) => {
+ const convert = (name) => {
+ return name.replace(/_(\w)/g, (_m, letter) => letter.toUpperCase())
+ }
+ return Object.fromEntries(
+ Object.entries(object)
+ .map(([name, value]) => [convert(name), value]),
+ )
+}
+
+const extractToMarkdown = (nodesOrMarks) => {
+ const nodeOrMarkEntries = Object
+ .entries(nodesOrMarks)
+ .map(([name, nodeOrMark]) => [name, nodeOrMark.spec.toMarkdown])
+ .filter(([, toMarkdown]) => toMarkdown)
+
+ return Object.fromEntries(nodeOrMarkEntries)
+}
diff --git a/src/tests/extensions/Markdown.spec.js b/src/tests/extensions/Markdown.spec.js
index 6308348d32c..26a1f479f5d 100644
--- a/src/tests/extensions/Markdown.spec.js
+++ b/src/tests/extensions/Markdown.spec.js
@@ -1,89 +1,23 @@
-import { Markdown } from './../../extensions/index.js'
-import { createMarkdownSerializer } from './../../extensions/Markdown.js'
-import CodeBlock from '@tiptap/extension-code-block'
-import Blockquote from '@tiptap/extension-blockquote'
-import Image from './../../nodes/Image.js'
-import ImageInline from './../../nodes/ImageInline.js'
+/**
+ * SPDX-FileCopyrightText: 2024 Max
+ * SPDX-License-Identifier: @license AGPL-3.0-or-later
+ *
+ */
+
+import { Serializer, Markdown } from './../../extensions/index.js'
import TaskList from './../../nodes/TaskList.js'
import TaskItem from './../../nodes/TaskItem.js'
+import CodeBlock from '@tiptap/extension-code-block'
+import Blockquote from '@tiptap/extension-blockquote'
import { Italic, Strong, Underline, Link} from './../../marks/index.js'
-import TiptapImage from '@tiptap/extension-image'
-import { getExtensionField } from '@tiptap/core'
import { __serializeForClipboard as serializeForClipboard } from '@tiptap/pm/view'
import { createCustomEditor } from '../helpers.js'
-describe('Markdown extension unit', () => {
- it('has a config', () => {
- expect(Markdown.config.name).toBe('markdown')
- })
-
- it('exposes toMarkdown function in Prosemirror', () => {
- const extend = getExtensionField(Markdown, 'extendMarkSchema', Markdown)
- expect(extend(Underline).toMarkdown).toBeDefined()
- })
-
- it('makes toMarkdown available in prose mirror schema', () => {
- const editor = createCustomEditor({
- extensions: [Markdown, Underline],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- const underline = serializer.serializer.marks.underline
- expect(underline).toEqual(Underline.config.toMarkdown)
- const listItem = serializer.serializer.nodes.listItem
- expect(typeof listItem).toBe('function')
- })
-})
-
describe('Markdown extension integrated in the editor', () => {
- it('serializes marks according to their spec', () => {
- const editor = createCustomEditor({
- content: 'Test
',
- extensions: [Markdown, Underline],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- expect(serializer.serialize(editor.state.doc)).toBe('__Test__')
- })
-
- it('serializes nodes according to their spec', () => {
- const editor = createCustomEditor({
- content: '
',
- extensions: [Markdown, TaskList, TaskItem],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- expect(serializer.serialize(editor.state.doc)).toBe('\n- [ ] Hello')
- })
-
- it('serializes images with the default prosemirror way', () => {
- const editor = createCustomEditor({
- content: '
',
- extensions: [Markdown, TiptapImage.configure({ inline: true })],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- expect(serializer.serialize(editor.state.doc)).toBe('')
- })
-
- it('serializes block images with the default prosemirror way', () => {
- const editor = createCustomEditor({
- content: '
hello
',
- extensions: [Markdown, Image, ImageInline],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- expect(serializer.serialize(editor.state.doc)).toBe('\n\nhello')
- })
-
- it('serializes inline images with the default prosemirror way', () => {
- const editor = createCustomEditor({
- content: 'inline image
inside text
',
- extensions: [Markdown, Image, ImageInline],
- })
- const serializer = createMarkdownSerializer(editor.schema)
- expect(serializer.serialize(editor.state.doc)).toBe('inline image  inside text')
- })
-
it('copies task lists to plaintext like markdown', () => {
const editor = createCustomEditor({
content: '',
- extensions: [Markdown, TaskList, TaskItem],
+ extensions: [Markdown, Serializer, TaskList, TaskItem],
})
const text = copyEditorContent(editor)
expect(text).toBe('\n- [ ] Hello')
@@ -92,7 +26,7 @@ describe('Markdown extension integrated in the editor', () => {
it('copies code block content to plaintext according to their spec', () => {
const editor = createCustomEditor({
content: 'Hello
',
- extensions: [Markdown, CodeBlock],
+ extensions: [Markdown, Serializer, CodeBlock],
})
const text = copyEditorContent(editor)
expect(text).toBe('Hello')
@@ -101,7 +35,7 @@ describe('Markdown extension integrated in the editor', () => {
it('copies nested task list nodes to markdown like syntax', () => {
const editor = createCustomEditor({
content: '
',
- extensions: [Markdown, Blockquote, TaskList, TaskItem],
+ extensions: [Markdown, Serializer, Blockquote, TaskList, TaskItem],
})
const text = copyEditorContent(editor)
expect(text).toBe('\n- [ ] Hello')
@@ -110,7 +44,7 @@ describe('Markdown extension integrated in the editor', () => {
it('copies address from blockquote to markdown', () => {
const editor = createCustomEditor({
content: 'Hermannsreute 44A
',
- extensions: [Markdown, Blockquote],
+ extensions: [Markdown, Serializer, Blockquote],
})
const text = copyEditorContent(editor)
expect(text).toBe('Hermannsreute 44A')
@@ -128,7 +62,7 @@ describe('Markdown extension integrated in the editor', () => {
it('strips bold, italic, and other marks from paragraph', () => {
const editor = createCustomEditor({
content: 'Hello
lonely world
',
- extensions: [Markdown, Italic, Strong, Underline],
+ extensions: [Markdown, Serializer, Italic, Strong, Underline],
})
const text = copyEditorContent(editor)
expect(text).toBe('Hello\n\nlonely world')
@@ -137,7 +71,7 @@ describe('Markdown extension integrated in the editor', () => {
it('strips href and link formatting from email address', () => {
const editor = createCustomEditor({
content: 'Hello
example@example.com
',
- extensions: [Markdown, Link],
+ extensions: [Markdown, Serializer, Link],
})
const text = copyEditorContent(editor)
expect(text).toBe('Hello\n\nexample@example.com')
diff --git a/src/tests/extensions/Serializer.spec.js b/src/tests/extensions/Serializer.spec.js
new file mode 100644
index 00000000000..f4f400b5fd9
--- /dev/null
+++ b/src/tests/extensions/Serializer.spec.js
@@ -0,0 +1,81 @@
+import { Serializer } from './../../extensions/index.js'
+import {
+ createMarkdownSerializer,
+ serializeEditorContent
+} from './../../extensions/Serializer.js'
+import Image from './../../nodes/Image.js'
+import ImageInline from './../../nodes/ImageInline.js'
+import TaskList from './../../nodes/TaskList.js'
+import TaskItem from './../../nodes/TaskItem.js'
+import { Underline } from './../../marks/index.js'
+import TiptapImage from '@tiptap/extension-image'
+import { getExtensionField } from '@tiptap/core'
+import { createCustomEditor } from '../helpers.js'
+
+describe('Serializer extension unit', () => {
+ it('has a config', () => {
+ expect(Serializer.config.name).toBe('serializer')
+ })
+
+ it('exposes toMarkdown function in Prosemirror', () => {
+ const extend = getExtensionField(Serializer, 'extendMarkSchema', Serializer)
+ expect(extend(Underline).toMarkdown).toBeDefined()
+ })
+
+ it('makes toMarkdown available in prose mirror schema', () => {
+ const editor = createCustomEditor({
+ extensions: [Serializer, Underline],
+ })
+ const serializer = createMarkdownSerializer(editor.schema)
+ const underline = serializer.serializer.marks.underline
+ expect(underline).toEqual(Underline.config.toMarkdown)
+ const listItem = serializer.serializer.nodes.listItem
+ expect(typeof listItem).toBe('function')
+ })
+})
+
+describe('Markdown extension integrated in the editor', () => {
+ it('serializes marks according to their spec', () => {
+ const editor = createCustomEditor({
+ content: 'Test
',
+ extensions: [Serializer, Underline],
+ })
+ expect(serializeEditorContent(editor)).toBe('__Test__')
+ })
+
+ it('serializes nodes according to their spec', () => {
+ const editor = createCustomEditor({
+ content: '',
+ extensions: [Serializer, TaskList, TaskItem],
+ })
+ expect(serializeEditorContent(editor))
+ .toBe('\n- [ ] Hello')
+ })
+
+ it('serializes images with the default prosemirror way', () => {
+ const editor = createCustomEditor({
+ content: '
',
+ extensions: [Serializer, TiptapImage.configure({ inline: true })],
+ })
+ expect(serializeEditorContent(editor))
+ .toBe('')
+ })
+
+ it('serializes block images with the default prosemirror way', () => {
+ const editor = createCustomEditor({
+ content: '
hello
',
+ extensions: [Serializer, Image, ImageInline],
+ })
+ expect(serializeEditorContent(editor))
+ .toBe('\n\nhello')
+ })
+
+ it('serializes inline images with the default prosemirror way', () => {
+ const editor = createCustomEditor({
+ content: 'inline image
inside text
',
+ extensions: [Serializer, Image, ImageInline],
+ })
+ expect(serializeEditorContent(editor))
+ .toBe('inline image  inside text')
+ })
+})
diff --git a/src/tests/helpers.js b/src/tests/helpers.js
index 401dec40565..1f6f563b603 100644
--- a/src/tests/helpers.js
+++ b/src/tests/helpers.js
@@ -1,5 +1,5 @@
-import { createMarkdownSerializer } from '../extensions/Markdown'
import { Editor } from '@tiptap/core'
+import { serializeEditorContent } from '../extensions/Serializer'
import Document from '@tiptap/extension-document'
import Paragraph from '../nodes/Paragraph'
@@ -31,8 +31,7 @@ export function markdownThroughEditor(markdown) {
enableRichEditing: true
})
tiptap.commands.setContent(markdownit.render(markdown))
- const serializer = createMarkdownSerializer(tiptap.schema)
- return serializer.serialize(tiptap.state.doc)
+ return serializeEditorContent(tiptap)
}
/**
@@ -46,8 +45,7 @@ export function markdownThroughEditorHtml(html) {
enableRichEditing: true
})
tiptap.commands.setContent(html)
- const serializer = createMarkdownSerializer(tiptap.schema)
- return serializer.serialize(tiptap.state.doc)
+ return serializeEditorContent(tiptap)
}
/**
@@ -61,6 +59,5 @@ export function markdownFromPaste(html) {
enableRichEditing: true
})
tiptap.commands.insertContent(html)
- const serializer = createMarkdownSerializer(tiptap.schema)
- return serializer.serialize(tiptap.state.doc)
+ return serializeEditorContent(tiptap)
}
diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js
index d473116679b..18b1a571a00 100644
--- a/src/tests/markdown.spec.js
+++ b/src/tests/markdown.spec.js
@@ -5,7 +5,7 @@ import {
markdownThroughEditorHtml,
markdownFromPaste
} from './helpers.js'
-import { createMarkdownSerializer } from "../extensions/Markdown";
+import { serializeEditorContent } from "../extensions/Serializer";
import { createEditor } from "../EditorFactory";
/*
@@ -205,8 +205,6 @@ describe('Trailing nodes', () => {
const jsonAfter = tiptap.getJSON()
expect(jsonAfter).toStrictEqual(jsonBefore)
- const serializer = createMarkdownSerializer(tiptap.schema)
- const md = serializer.serialize(tiptap.state.doc)
- expect(md).toBe(source)
+ expect(serializeEditorContent(tiptap)).toBe(source)
})
})
diff --git a/src/tests/nodes/Preview.spec.js b/src/tests/nodes/Preview.spec.js
index 4b0107dc02b..1ac6cf167f4 100644
--- a/src/tests/nodes/Preview.spec.js
+++ b/src/tests/nodes/Preview.spec.js
@@ -1,5 +1,5 @@
import Preview from './../../nodes/Preview'
-import Markdown from './../../extensions/Markdown'
+import Serializer from './../../extensions/Serializer'
import Link from './../../marks/Link'
import { getExtensionField } from '@tiptap/core'
import { createCustomEditor, markdownThroughEditor, markdownThroughEditorHtml } from '../helpers'
@@ -43,6 +43,6 @@ describe('Preview extension', () => {
function createEditorWithPreview() {
return createCustomEditor({
- extensions: [Markdown, Preview, Link]
+ extensions: [Serializer, Preview, Link]
})
}
diff --git a/src/tests/nodes/Table.spec.js b/src/tests/nodes/Table.spec.js
index c152ba38440..b42297c511b 100644
--- a/src/tests/nodes/Table.spec.js
+++ b/src/tests/nodes/Table.spec.js
@@ -1,5 +1,5 @@
import { createEditor } from '../../EditorFactory'
-import { createMarkdownSerializer } from '../../extensions/Markdown'
+import { serializeEditorContent } from '../../extensions/Serializer'
import { builders } from 'prosemirror-test-builder'
import markdownit from '../../markdownit'
@@ -63,9 +63,7 @@ describe('Table', () => {
test('serialize from editor', () => {
const tiptap = editorWithContent(markdownit.render(input))
- const serializer = createMarkdownSerializer(tiptap.schema)
-
- expect(serializer.serialize(tiptap.state.doc)).toBe(input)
+ expect(serializeEditorContent(tiptap)).toBe(input)
})
})
diff --git a/src/tests/nodes/TaskItem.spec.js b/src/tests/nodes/TaskItem.spec.js
index abc0a6d7b6e..6b1f6b89881 100644
--- a/src/tests/nodes/TaskItem.spec.js
+++ b/src/tests/nodes/TaskItem.spec.js
@@ -1,6 +1,6 @@
import TaskList from './../../nodes/TaskList'
import TaskItem from './../../nodes/TaskItem'
-import Markdown from './../../extensions/Markdown'
+import Serializer from './../../extensions/Serializer'
import { getExtensionField } from '@tiptap/core'
import { createCustomEditor, markdownThroughEditor, markdownThroughEditorHtml } from '../helpers'
@@ -12,7 +12,7 @@ describe('TaskItem extension', () => {
it('exposes the toMarkdown function in the prosemirror schema', () => {
const editor = createCustomEditor({
- extensions: [Markdown, TaskList, TaskItem]
+ extensions: [Serializer, TaskList, TaskItem]
})
const taskItem = editor.schema.nodes.taskItem
expect(taskItem.spec.toMarkdown).toBeDefined()
diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js
index bc60bc420aa..f38982c03b2 100644
--- a/src/tests/plaintext.spec.js
+++ b/src/tests/plaintext.spec.js
@@ -1,5 +1,5 @@
import { createEditor } from './../EditorFactory';
-import { serializeEditorContent } from './../extensions/Markdown.js'
+import { serializeEditorContent } from './../extensions/Serializer.js'
import spec from "./fixtures/spec"
import xssFuzzVectors from './fixtures/xssFuzzVectors';
From d4788785602552e96ed45805fd6fe44f33ded19b Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 11:36:10 +0200
Subject: [PATCH 06/17] refactor(Editor): move initialization into onOpened
We do need the session for initialization
but we do not need the `documentSource` or `documentState` anymore
Signed-off-by: Max
---
src/components/Editor.vue | 30 ++++++++++++++----------------
1 file changed, 14 insertions(+), 16 deletions(-)
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index e079a9141bc..70931672a69 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -495,32 +495,17 @@ export default {
this.lock = this.$syncService.lock
localStorage.setItem('nick', this.currentSession.guestName)
this.$attachmentResolver = new AttachmentResolver({
- session: this.currentSession,
+ session,
user: getCurrentUser(),
shareToken: this.shareToken,
currentDirectory: this.currentDirectory,
})
- },
-
- onLoaded({ documentSource, documentState }) {
- if (documentState) {
- applyDocumentState(this.$ydoc, documentState, this.$providers[0])
- // distribute additional state that may exist locally
- const updateMessage = getUpdateMessage(this.$ydoc, documentState)
- if (updateMessage) {
- logger.debug('onLoaded: Pushing local changes to server')
- this.$queue.push(updateMessage)
- }
- } else {
- this.setInitialYjsState(documentSource, { isRichEditor: this.isRichEditor })
- }
this.hasConnectionIssue = false
const language = extensionHighlight[this.fileExtension] || this.fileExtension;
(this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language))
.then(() => {
- const session = this.currentSession
if (!this.$editor) {
this.$editor = createEditor({
language,
@@ -565,7 +550,20 @@ export default {
}
})
+ },
+ onLoaded({ documentSource, documentState }) {
+ if (documentState) {
+ applyDocumentState(this.$ydoc, documentState, this.$providers[0])
+ // distribute additional state that may exist locally
+ const updateMessage = getUpdateMessage(this.$ydoc, documentState)
+ if (updateMessage) {
+ logger.debug('onLoaded: Pushing local changes to server')
+ this.$queue.push(updateMessage)
+ }
+ } else {
+ this.setInitialYjsState(documentSource, { isRichEditor: this.isRichEditor })
+ }
},
onChange({ document, sessions }) {
From 00a82fbc086bac4fcfbde95be6ed481db1e0d01c Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 11:45:24 +0200
Subject: [PATCH 07/17] refactor(Serializer): make _createMarkdownSerializer
internal
Signed-off-by: Max
---
src/extensions/Serializer.js | 20 ++++++++++----------
src/tests/extensions/Serializer.spec.js | 8 ++++----
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/extensions/Serializer.js b/src/extensions/Serializer.js
index 2e6273d889a..2c0484bfbfd 100644
--- a/src/extensions/Serializer.js
+++ b/src/extensions/Serializer.js
@@ -27,8 +27,8 @@ import { MarkdownSerializer } from '@tiptap/pm/markdown'
import { extractNodesToMarkdown, extractMarksToMarkdown } from '../helpers/serialize.js'
export function serializeEditorContent({ schema, state }) {
- return createMarkdownSerializer(schema)
- .serialize(state.doc)
+ return _createMarkdownSerializer(schema)
+ .serialize(state.doc, { tightLists: true })
}
export const Serializer = Extension.create({
@@ -59,16 +59,16 @@ export const Serializer = Extension.create({
})
-export const createMarkdownSerializer = ({ nodes, marks }) => {
- return {
- serializer: new MarkdownSerializer(
+/*
+ * Create the markdown serializer.
+ *
+ * Only exported for tests,
+ */
+export const _createMarkdownSerializer = ({ nodes, marks }) => {
+ return new MarkdownSerializer(
extractNodesToMarkdown(nodes),
extractMarksToMarkdown(marks),
- ),
- serialize(content, options) {
- return this.serializer.serialize(content, { ...options, tightLists: true })
- },
- }
+ )
}
export default Serializer
diff --git a/src/tests/extensions/Serializer.spec.js b/src/tests/extensions/Serializer.spec.js
index f4f400b5fd9..7ec534427c6 100644
--- a/src/tests/extensions/Serializer.spec.js
+++ b/src/tests/extensions/Serializer.spec.js
@@ -1,6 +1,6 @@
import { Serializer } from './../../extensions/index.js'
import {
- createMarkdownSerializer,
+ _createMarkdownSerializer,
serializeEditorContent
} from './../../extensions/Serializer.js'
import Image from './../../nodes/Image.js'
@@ -26,10 +26,10 @@ describe('Serializer extension unit', () => {
const editor = createCustomEditor({
extensions: [Serializer, Underline],
})
- const serializer = createMarkdownSerializer(editor.schema)
- const underline = serializer.serializer.marks.underline
+ const serializer = _createMarkdownSerializer(editor.schema)
+ const underline = serializer.marks.underline
expect(underline).toEqual(Underline.config.toMarkdown)
- const listItem = serializer.serializer.nodes.listItem
+ const listItem = serializer.nodes.listItem
expect(typeof listItem).toBe('function')
})
})
From 43355d2c2f9ceca81cfd43b44599204241f08b09 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 11:49:54 +0200
Subject: [PATCH 08/17] fix(lint): only default export Serializer
Signed-off-by: Max
---
cypress/e2e/nodes/Table.spec.js | 2 +-
src/extensions/Serializer.js | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/cypress/e2e/nodes/Table.spec.js b/cypress/e2e/nodes/Table.spec.js
index b168d9be03e..0c8355c0036 100644
--- a/cypress/e2e/nodes/Table.spec.js
+++ b/cypress/e2e/nodes/Table.spec.js
@@ -5,7 +5,7 @@ import { getMarkdown } from './helpers.js'
import markdownit from './../../../src/markdownit/index.js'
import EditableTable from './../../../src/nodes/EditableTable.js'
-import { Serializer } from './../../../src/extensions/Serializer.js'
+import { Serializer } from './../../../src/extensions/index.js'
// https://github.com/import-js/eslint-plugin-import/issues/1739
/* eslint-disable-next-line import/no-unresolved */
diff --git a/src/extensions/Serializer.js b/src/extensions/Serializer.js
index 2c0484bfbfd..c767d6d6ae0 100644
--- a/src/extensions/Serializer.js
+++ b/src/extensions/Serializer.js
@@ -31,7 +31,7 @@ export function serializeEditorContent({ schema, state }) {
.serialize(state.doc, { tightLists: true })
}
-export const Serializer = Extension.create({
+const Serializer = Extension.create({
name: 'serializer',
@@ -66,9 +66,9 @@ export const Serializer = Extension.create({
*/
export const _createMarkdownSerializer = ({ nodes, marks }) => {
return new MarkdownSerializer(
- extractNodesToMarkdown(nodes),
- extractMarksToMarkdown(marks),
- )
+ extractNodesToMarkdown(nodes),
+ extractMarksToMarkdown(marks),
+ )
}
export default Serializer
From 3df029401e304bbd1590c2300e437483da2c48bd Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 14:35:19 +0200
Subject: [PATCH 09/17] fix(lint): fix all warnings
* Use const arrow functions when no doc comment is needed.
* Use the tiptap api to inspect editor state
rather than reaching deep into the editor state.
Signed-off-by: Max
---
cypress/e2e/directediting.spec.js | 2 +-
cypress/e2e/nodes/Preview.spec.js | 48 +++++++++++++------------------
cypress/e2e/nodes/helpers.js | 32 ++++-----------------
src/extensions/Serializer.js | 2 +-
4 files changed, 27 insertions(+), 57 deletions(-)
diff --git a/cypress/e2e/directediting.spec.js b/cypress/e2e/directediting.spec.js
index 9915ad8064d..94f0af9ca1c 100644
--- a/cypress/e2e/directediting.spec.js
+++ b/cypress/e2e/directediting.spec.js
@@ -2,7 +2,7 @@ import { initUserAndFiles, randUser } from '../utils/index.js'
const user = randUser()
-function enterContentAndClose() {
+const enterContentAndClose = () => {
cy.intercept({ method: 'POST', url: '**/session/*/close' }).as('closeRequest')
cy.intercept({ method: 'POST', url: '**/session/*/push' }).as('push')
cy.intercept({ method: 'POST', url: '**/session/*/sync' }).as('sync')
diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js
index bed1140a6ea..28d2ba4625e 100644
--- a/cypress/e2e/nodes/Preview.spec.js
+++ b/cypress/e2e/nodes/Preview.spec.js
@@ -26,6 +26,7 @@ import Preview from './../../../src/nodes/Preview.js'
import { Italic, Link } from './../../../src/marks/index.js'
import { createCustomEditor } from './../../support/components.js'
import { loadMarkdown, runCommands, expectMarkdown } from './helpers.js'
+import { isNodeActive, getMarkAttributes, getNodeAttributes } from '@tiptap/core'
// https://github.com/import-js/eslint-plugin-import/issues/1739
/* eslint-disable-next-line import/no-unresolved */
@@ -133,51 +134,42 @@ describe('Preview extension', { retries: 0 }, () => {
prepareEditor('[link text](https://nextcloud.com)\n')
editor.commands.setPreview()
editor.commands.unsetPreview()
- expect(getParentNode().type.name).to.equal('paragraph')
- })
-
- it('includes a link', () => {
- prepareEditor('[link text](https://nextcloud.com)\n')
- editor.commands.setPreview()
- editor.commands.unsetPreview()
- expect(getMark().attrs.href).to.equal('https://nextcloud.com')
+ expectParagraphWithLink()
})
})
/**
- * Expect a preview in the editor.
+ * Expect a preview at the current position.
*/
function expectPreview() {
- expect(getParentNode().type.name).to.equal('preview')
- expect(getParentNode().attrs.href).to.equal('https://nextcloud.com')
- expect(getMark().attrs.href).to.equal('https://nextcloud.com')
- }
-
- /**
- *
- */
- function getParentNode() {
- const { state: { selection } } = editor
- return selection.$head.parent
+ expect(isNodeActive(editor.state, 'paragraph'))
+ .to.be.false
+ expect(getNodeAttributes(editor.state, 'preview'))
+ .to.include({ href: 'https://nextcloud.com' })
+ expect(getMarkAttributes(editor.state, 'link'))
+ .to.include({ href: 'https://nextcloud.com' })
}
/**
- *
+ * Expect a paragraph with a link at the current position.
*/
- function getMark() {
- const { state: { selection } } = editor
- console.info(selection.$head)
- return selection.$head.nodeAfter.marks[0]
+ function expectParagraphWithLink() {
+ expect(isNodeActive(editor.state, 'preview'))
+ .to.be.false
+ expect(isNodeActive(editor.state, 'paragraph'))
+ .to.be.true
+ expect(getMarkAttributes(editor.state, 'link'))
+ .to.include({ href: 'https://nextcloud.com' })
}
/**
- *
- * @param input
+ * Load input and position the cursor inside.
+ * @param { string } input - markdown to load
*/
function prepareEditor(input) {
loadMarkdown(editor, input)
- editor.commands.setTextSelection(1)
+ editor.commands.setTextSelection(2)
}
})
diff --git a/cypress/e2e/nodes/helpers.js b/cypress/e2e/nodes/helpers.js
index 4c135b9b64b..cc91b7fc220 100644
--- a/cypress/e2e/nodes/helpers.js
+++ b/cypress/e2e/nodes/helpers.js
@@ -24,21 +24,12 @@ import markdownit from './../../../src/markdownit/index.js'
import { findChildren } from './../../../src/helpers/prosemirrorUtils.js'
import { serializeEditorContent } from './../../../src/extensions/Serializer.js'
-/**
- *
- * @param editor
- * @param markdown
- */
-export function loadMarkdown(editor, markdown) {
+export const loadMarkdown = (editor, markdown) => {
const stripped = markdown.replace(/\t*/g, '')
editor.commands.setContent(markdownit.render(stripped))
}
-/**
- *
- * @param editor
- */
-export function runCommands(editor) {
+export const runCommands = (editor) => {
let found
while ((found = findCommand(editor))) {
const { node, pos } = found
@@ -49,31 +40,18 @@ export function runCommands(editor) {
}
}
-/**
- *
- * @param editor
- */
-function findCommand(editor) {
+const findCommand = (editor) => {
const doc = editor.state.doc
return findChildren(doc, child => {
return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text)
})[0]
}
-/**
- *
- * @param editor
- * @param markdown
- */
-export function expectMarkdown(editor, markdown) {
+export const expectMarkdown = (editor, markdown) => {
const stripped = markdown.replace(/\t*/g, '')
expect(getMarkdown(editor)).to.equal(stripped)
}
-/**
- *
- * @param editor
- */
-export function getMarkdown(editor) {
+export const getMarkdown = (editor) => {
return serializeEditorContent(editor)
}
diff --git a/src/extensions/Serializer.js b/src/extensions/Serializer.js
index c767d6d6ae0..b2a44dfd604 100644
--- a/src/extensions/Serializer.js
+++ b/src/extensions/Serializer.js
@@ -26,7 +26,7 @@ import { Extension, getExtensionField } from '@tiptap/core'
import { MarkdownSerializer } from '@tiptap/pm/markdown'
import { extractNodesToMarkdown, extractMarksToMarkdown } from '../helpers/serialize.js'
-export function serializeEditorContent({ schema, state }) {
+export const serializeEditorContent = ({ schema, state }) => {
return _createMarkdownSerializer(schema)
.serialize(state.doc, { tightLists: true })
}
From 3559f13dec405a2708f811d09326aa7bc1dcf64a Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 14:47:39 +0200
Subject: [PATCH 10/17] refactor(syncService): do not expose serialize()
Keep as `#getContent` internally.
Use `serializeEditorContent(editor)`
outside of the `SyncService`.
Signed-off-by: Max
---
src/components/Editor.vue | 16 +++++++---------
src/services/SyncService.js | 9 +++------
2 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 70931672a69..7c4a0032844 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -516,10 +516,8 @@ export default {
},
onUpdate: ({ editor }) => {
// this.debugContent(editor)
- const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
- this.emit('update:content', {
- markdown: proseMirrorMarkdown,
- })
+ const markdown = serializeEditorContent(editor)
+ this.emit('update:content', { markdown })
},
extensions: [
Autofocus.configure({
@@ -732,15 +730,15 @@ export default {
* @param {object} editor The Tiptap editor
*/
debugContent(editor) {
- const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
- const markdownItHtml = markdownit.render(proseMirrorMarkdown)
+ const markdown = serializeEditorContent(editor)
+ const markdownItHtml = markdownit.render(markdown)
logger.debug('markdown, serialized from editor state by prosemirror-markdown')
- console.debug(proseMirrorMarkdown)
+ console.debug({ markdown })
logger.debug('HTML, serialized from markdown by markdown-it')
- console.debug(markdownItHtml)
+ console.debug({ markdownItHtml })
logger.debug('HTML, as rendered in the browser by Tiptap')
- console.debug(editor.getHTML())
+ console.debug({ editorHtml: editor.getHTML() })
},
outlineToggled(visible) {
diff --git a/src/services/SyncService.js b/src/services/SyncService.js
index b054603f184..88bf10e9ac8 100644
--- a/src/services/SyncService.js
+++ b/src/services/SyncService.js
@@ -67,12 +67,13 @@ const ERROR_TYPE = {
class SyncService {
#sendIntervalId
+ #getContent
constructor({ baseVersionEtag, serialize, getDocumentState, ...options }) {
/** @type {import('mitt').Emitter} _bus */
this._bus = mitt()
- this.serialize = serialize
+ this.#getContent = serialize
this.getDocumentState = getDocumentState
this._api = new SessionApi(options)
this.connection = null
@@ -240,16 +241,12 @@ class SyncService {
return false
}
- _getContent() {
- return this.serialize()
- }
-
async save({ force = false, manualSave = true } = {}) {
logger.debug('[SyncService] saving', arguments[0])
try {
const response = await this.connection.save({
version: this.version,
- autosaveContent: this._getContent(),
+ autosaveContent: this.#getContent(),
documentState: this.getDocumentState(),
force,
manualSave,
From 7af0c9e0af38ae70c0cfff5e38fbe0e54bea2216 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 15:11:20 +0200
Subject: [PATCH 11/17] refactor(Markdown): simplify serialization logic
The intermediate object with `serialize` and `serializer`
is not needed anymore.
Signed-off-by: Max
---
src/extensions/Markdown.js | 29 ++++++++++++++++++-----------
1 file changed, 18 insertions(+), 11 deletions(-)
diff --git a/src/extensions/Markdown.js b/src/extensions/Markdown.js
index fc43d431821..d40b5864d3a 100644
--- a/src/extensions/Markdown.js
+++ b/src/extensions/Markdown.js
@@ -75,7 +75,7 @@ const Markdown = Extension.create({
clipboardTextSerializer: (slice) => {
const traverseNodes = (slice) => {
if (slice.content.childCount > 1) {
- return clipboardSerializer(this.editor.schema).serialize(slice.content)
+ return serializeSliceForClipboard(this.editor, slice)
} else if (slice.isLeaf) {
return slice.textContent
} else {
@@ -92,16 +92,23 @@ const Markdown = Extension.create({
},
})
-const clipboardSerializer = ({ nodes, marks }) => {
- return {
- serializer: new MarkdownSerializer(
- extractNodesToMarkdown(nodes),
- extractToPlaintext(marks),
- ),
- serialize(content, options) {
- return this.serializer.serialize(content, { ...options, tightLists: true })
- },
- }
+const serializeSliceForClipboard = ({ schema }, { content }) => {
+ return createTextSerializer(schema)
+ .serialize(content, { tightLists: true })
+}
+
+/*
+ * Create a serializer for multiple nodes:
+ *
+ * * use markdown for nodes so lists show up as lists, etc..
+ * * ignore marks as these can be irritating.
+ *
+ */
+const createTextSerializer = ({ nodes, marks }) => {
+ return new MarkdownSerializer(
+ extractNodesToMarkdown(nodes),
+ extractToPlaintext(marks),
+ )
}
export default Markdown
From de6411d01cc0f603a010d28f3bbe3fb179b13217 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 16:48:18 +0200
Subject: [PATCH 12/17] refactor(Editor.vue): use editor events not callbacks
No need pass the callbacks in createEditor.
Signed-off-by: Max
---
src/EditorFactory.js | 4 +---
src/components/Editor.vue | 23 +++++++++++++++--------
2 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 9c2a2a29338..869b6a51e74 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -46,7 +46,7 @@ export const loadSyntaxHighlight = async (language) => {
}
}
-export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
+export const createEditor = ({ language, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
let defaultExtensions
if (enableRichEditing) {
defaultExtensions = [
@@ -72,8 +72,6 @@ export const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {
]
}
return new Editor({
- onCreate,
- onUpdate,
editorProps: {
scrollMargin: 50,
scrollThreshold: 50,
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 7c4a0032844..526657b8aa7 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -414,10 +414,15 @@ export default {
listenEditorEvents() {
this.$editor.on('focus', this.onFocus)
this.$editor.on('blur', this.onBlur)
+ this.$editor.on('create', this.onCreate)
+ this.$editor.on('update', this.onUpdate)
},
+
unlistenEditorEvents() {
this.$editor.off('focus', this.onFocus)
this.$editor.off('blur', this.onBlur)
+ this.$editor.off('create', this.onCreate)
+ this.$editor.off('update', this.onUpdate)
},
listenSyncServiceEvents() {
@@ -511,14 +516,6 @@ export default {
language,
relativePath: this.relativePath,
session,
- onCreate: ({ editor }) => {
- this.$syncService.startSync()
- },
- onUpdate: ({ editor }) => {
- // this.debugContent(editor)
- const markdown = serializeEditorContent(editor)
- this.emit('update:content', { markdown })
- },
extensions: [
Autofocus.configure({
fileId: this.fileId,
@@ -660,6 +657,16 @@ export default {
this.emit('blur')
},
+ onCreate() {
+ this.$syncService.startSync()
+ },
+
+ onUpdate(editor) {
+ // this.debugContent(editor)
+ const markdown = serializeEditorContent(editor)
+ this.emit('update:content', { markdown })
+ },
+
onAddImageNode() {
this.emit('add-image-node')
},
From 40847babda4150c89cbe824f49908f6749472481 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 20:56:57 +0200
Subject: [PATCH 13/17] refactor(Editor.vue): early return for reconnects
Signed-off-by: Max
---
src/components/Editor.vue | 69 +++++++++++++++++++--------------------
1 file changed, 33 insertions(+), 36 deletions(-)
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 526657b8aa7..b3a14c1e67c 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -494,9 +494,6 @@ export default {
this.currentSession = session
this.document = document
this.readOnly = document.readOnly
- if (this.$editor) {
- this.$editor.setEditable(!this.readOnly)
- }
this.lock = this.$syncService.lock
localStorage.setItem('nick', this.currentSession.guestName)
this.$attachmentResolver = new AttachmentResolver({
@@ -507,43 +504,43 @@ export default {
})
this.hasConnectionIssue = false
+ if (this.$editor) {
+ // $editor already existed. So this is a reconnect.
+ this.$editor.setEditable(!this.readOnly)
+ this.$syncService.startSync()
+ return
+ }
const language = extensionHighlight[this.fileExtension] || this.fileExtension;
(this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language))
.then(() => {
- if (!this.$editor) {
- this.$editor = createEditor({
- language,
- relativePath: this.relativePath,
- session,
- extensions: [
- Autofocus.configure({
- fileId: this.fileId,
- }),
- Collaboration.configure({
- document: this.$ydoc,
- }),
- CollaborationCursor.configure({
- provider: this.$providers[0],
- user: {
- name: session?.userId
- ? session.displayName
- : (session?.guestName || t('text', 'Guest')),
- color: session?.color,
- clientId: this.$ydoc.clientID,
- },
- }),
- ],
- enableRichEditing: this.isRichEditor,
- isEmbedded: this.isEmbedded,
- })
- this.hasEditor = true
- this.listenEditorEvents()
- } else {
- // $editor already existed. So this is a reconnect.
- this.$syncService.startSync()
- }
-
+ this.$editor = createEditor({
+ language,
+ relativePath: this.relativePath,
+ session,
+ extensions: [
+ Autofocus.configure({
+ fileId: this.fileId,
+ }),
+ Collaboration.configure({
+ document: this.$ydoc,
+ }),
+ CollaborationCursor.configure({
+ provider: this.$providers[0],
+ user: {
+ name: session?.userId
+ ? session.displayName
+ : (session?.guestName || t('text', 'Guest')),
+ color: session?.color,
+ clientId: this.$ydoc.clientID,
+ },
+ }),
+ ],
+ enableRichEditing: this.isRichEditor,
+ isEmbedded: this.isEmbedded,
+ })
+ this.hasEditor = true
+ this.listenEditorEvents()
})
},
From e74c520d0ef1982c3085b0b4599ff8745fc3b639 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 21:12:02 +0200
Subject: [PATCH 14/17] fix(lint): also lint .ts files
Signed-off-by: Max
---
package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 1a019341421..8a245a41e75 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,8 @@
"build": "NODE_OPTIONS='--max-old-space-size=4096' vite --mode production build",
"dev": "NODE_OPTIONS='--max-old-space-size=4096' vite --mode development build",
"watch": "NODE_OPTIONS='--max-old-space-size=4096' vite --mode development build --watch",
- "lint": "tsc && eslint --ext .js,.vue src cypress",
- "lint:fix": "tsc && eslint --ext .js,.vue src cypress --fix",
+ "lint": "tsc && eslint --ext .ts,.js,.vue src cypress",
+ "lint:fix": "tsc && eslint --ext .ts,.js,.vue src cypress --fix",
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css css/*.scss",
"stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css css/*.scss --fix",
"test": "NODE_ENV=test jest",
From d489836a7e9246318b1e9ffac8b8ae2583d681b8 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 21:12:48 +0200
Subject: [PATCH 15/17] refactor(EditorFactory): split createRichEditor and
createPlainEditor
Signed-off-by: Max
---
src/EditorFactory.js | 59 ++++++++++++++++++-----------------
src/components/Editor.vue | 59 ++++++++++++++++++++---------------
src/mixins/setContent.js | 8 ++---
src/tests/builders.js | 9 ++----
src/tests/helpers.js | 14 +++------
src/tests/markdown.spec.js | 6 ++--
src/tests/nodes/Table.spec.js | 6 ++--
src/tests/plaintext.spec.js | 6 ++--
src/tests/tiptap.spec.js | 6 ++--
9 files changed, 82 insertions(+), 91 deletions(-)
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 869b6a51e74..c47b7347634 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -46,36 +46,37 @@ export const loadSyntaxHighlight = async (language) => {
}
}
-export const createEditor = ({ language, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
- let defaultExtensions
- if (enableRichEditing) {
- defaultExtensions = [
- FocusTrap,
- RichText.configure({
- relativePath,
- isEmbedded,
- component: this,
- extensions: [
- Mention.configure({
- suggestion: MentionSuggestion({
- session,
- }),
+export const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded = false } = {}) => {
+ return _createEditor([
+ FocusTrap,
+ RichText.configure({
+ relativePath,
+ isEmbedded,
+ component: this,
+ extensions: [
+ Mention.configure({
+ suggestion: MentionSuggestion({
+ session,
}),
- ],
- }),
- ]
- } else {
- defaultExtensions = [
- PlainText,
- PlainTextLowlight
- .configure({ lowlight, defaultLanguage: language }),
- ]
- }
+ }),
+ ],
+ }),
+ ...extensions,
+ ])
+}
+
+export const createPlainEditor = ({ language, extensions = [] } = {}) => {
+ return _createEditor([
+ PlainText,
+ PlainTextLowlight
+ .configure({ lowlight, defaultLanguage: language }),
+ ...extensions,
+ ])
+}
+
+const _createEditor = extensions => {
return new Editor({
- editorProps: {
- scrollMargin: 50,
- scrollThreshold: 50,
- },
- extensions: defaultExtensions.concat(extensions || []),
+ editorProps: { scrollMargin: 50, scrollThreshold: 50 },
+ extensions,
})
}
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index b3a14c1e67c..ff5082d40e4 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -114,7 +114,11 @@ import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService
import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
import { extensionHighlight } from '../helpers/mappings.js'
-import { createEditor, loadSyntaxHighlight } from './../EditorFactory.js'
+import {
+ createRichEditor,
+ createPlainEditor,
+ loadSyntaxHighlight,
+} from './../EditorFactory.js'
import { serializeEditorContent } from './../extensions/Serializer.js'
import markdownit from './../markdownit/index.js'
@@ -510,35 +514,38 @@ export default {
this.$syncService.startSync()
return
}
+
+ const extensions = [
+ Autofocus.configure({
+ fileId: this.fileId,
+ }),
+ Collaboration.configure({
+ document: this.$ydoc,
+ }),
+ CollaborationCursor.configure({
+ provider: this.$providers[0],
+ user: {
+ name: session?.userId
+ ? session.displayName
+ : (session?.guestName || t('text', 'Guest')),
+ color: session?.color,
+ clientId: this.$ydoc.clientID,
+ },
+ }),
+ ]
+
const language = extensionHighlight[this.fileExtension] || this.fileExtension;
(this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language))
.then(() => {
- this.$editor = createEditor({
- language,
- relativePath: this.relativePath,
- session,
- extensions: [
- Autofocus.configure({
- fileId: this.fileId,
- }),
- Collaboration.configure({
- document: this.$ydoc,
- }),
- CollaborationCursor.configure({
- provider: this.$providers[0],
- user: {
- name: session?.userId
- ? session.displayName
- : (session?.guestName || t('text', 'Guest')),
- color: session?.color,
- clientId: this.$ydoc.clientID,
- },
- }),
- ],
- enableRichEditing: this.isRichEditor,
- isEmbedded: this.isEmbedded,
- })
+ this.$editor = this.isRichEditor
+ ? createRichEditor({
+ relativePath: this.relativePath,
+ session,
+ extensions,
+ isEmbedded: this.isEmbedded,
+ })
+ : createPlainEditor({ language, extensions })
this.hasEditor = true
this.listenEditorEvents()
})
diff --git a/src/mixins/setContent.js b/src/mixins/setContent.js
index 0126fde5668..cbc60cbf24e 100644
--- a/src/mixins/setContent.js
+++ b/src/mixins/setContent.js
@@ -26,7 +26,7 @@ import { Doc, encodeStateAsUpdate, XmlFragment, applyUpdate } from 'yjs'
import { generateJSON } from '@tiptap/core'
import { prosemirrorToYXmlFragment } from 'y-prosemirror'
import { Node } from '@tiptap/pm/model'
-import { createEditor } from '../EditorFactory.js'
+import { createRichEditor, createPlainEditor } from '../EditorFactory.js'
export default {
methods: {
@@ -48,9 +48,9 @@ export default {
? markdownit.render(content) + ''
: `${escapeHtml(content)}`
- const editor = createEditor({
- enableRichEditing: isRichEditor,
- })
+ const editor = isRichEditor
+ ? createRichEditor()
+ : createPlainEditor()
const json = generateJSON(html, editor.extensionManager.extensions)
const doc = Node.fromJSON(editor.schema, json)
diff --git a/src/tests/builders.js b/src/tests/builders.js
index a3f7de05526..297f0ae2d26 100644
--- a/src/tests/builders.js
+++ b/src/tests/builders.js
@@ -1,14 +1,11 @@
import { expect } from '@jest/globals';
import { Mark, Node } from '@tiptap/pm/model'
import { builders } from 'prosemirror-test-builder'
-import { createEditor } from '../EditorFactory'
+import { createRichEditor } from '../EditorFactory'
export function getBuilders() {
- const editor = createEditor({
- content: '',
- enableRichEditing: true
- })
+ const editor = createRichEditor()
return builders(editor.schema, {
tr: { nodeType: 'tableRow' },
td: { nodeType: 'tableCell' },
@@ -79,7 +76,7 @@ function createDocumentString(node) {
* @param {Node} subject The editor document
* @param {Node} expected The expected document
* @example
- * const editor = createEditor()
+ * const editor = createRichEditor()
* expectDocument(editor.state.doc, table(
* tr(
* td('foo')
diff --git a/src/tests/helpers.js b/src/tests/helpers.js
index 1f6f563b603..ae6bc6b5621 100644
--- a/src/tests/helpers.js
+++ b/src/tests/helpers.js
@@ -5,7 +5,7 @@ import Document from '@tiptap/extension-document'
import Paragraph from '../nodes/Paragraph'
import Text from '@tiptap/extension-text'
-import { createEditor } from '../EditorFactory'
+import { createRichEditor } from '../EditorFactory'
import markdownit from '../markdownit'
export function createCustomEditor({ content, extensions }) {
@@ -27,9 +27,7 @@ export function createCustomEditor({ content, extensions }) {
* @returns {string}
*/
export function markdownThroughEditor(markdown) {
- const tiptap = createEditor({
- enableRichEditing: true
- })
+ const tiptap = createRichEditor()
tiptap.commands.setContent(markdownit.render(markdown))
return serializeEditorContent(tiptap)
}
@@ -41,9 +39,7 @@ export function markdownThroughEditor(markdown) {
* @returns {string}
*/
export function markdownThroughEditorHtml(html) {
- const tiptap = createEditor({
- enableRichEditing: true
- })
+ const tiptap = createRichEditor()
tiptap.commands.setContent(html)
return serializeEditorContent(tiptap)
}
@@ -55,9 +51,7 @@ export function markdownThroughEditorHtml(html) {
* @returns {string}
*/
export function markdownFromPaste(html) {
- const tiptap = createEditor({
- enableRichEditing: true
- })
+ const tiptap = createRichEditor()
tiptap.commands.insertContent(html)
return serializeEditorContent(tiptap)
}
diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js
index 18b1a571a00..19f4db49fc1 100644
--- a/src/tests/markdown.spec.js
+++ b/src/tests/markdown.spec.js
@@ -6,7 +6,7 @@ import {
markdownFromPaste
} from './helpers.js'
import { serializeEditorContent } from "../extensions/Serializer";
-import { createEditor } from "../EditorFactory";
+import { createRichEditor } from "../EditorFactory";
/*
* This file is for various markdown tests, mainly testing if input and output stays the same.
@@ -190,9 +190,7 @@ describe('Markdown serializer from html', () => {
describe('Trailing nodes', () => {
test('No extra transaction is added after loading', () => {
const source = "# My heading\n\n* test\n* test2"
- const tiptap = createEditor({
- enableRichEditing: true,
- })
+ const tiptap = createRichEditor()
tiptap.commands.setContent(markdownit.render(source))
const jsonBefore = tiptap.getJSON()
diff --git a/src/tests/nodes/Table.spec.js b/src/tests/nodes/Table.spec.js
index b42297c511b..779f95a2a38 100644
--- a/src/tests/nodes/Table.spec.js
+++ b/src/tests/nodes/Table.spec.js
@@ -1,4 +1,4 @@
-import { createEditor } from '../../EditorFactory'
+import { createRichEditor } from '../../EditorFactory'
import { serializeEditorContent } from '../../extensions/Serializer'
import { builders } from 'prosemirror-test-builder'
@@ -68,9 +68,7 @@ describe('Table', () => {
})
function editorWithContent(content) {
- const editor = createEditor({
- enableRichEditing: true,
- })
+ const editor = createRichEditor()
editor.commands.setContent(content)
return editor
}
diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js
index f38982c03b2..e78d380c21d 100644
--- a/src/tests/plaintext.spec.js
+++ b/src/tests/plaintext.spec.js
@@ -1,4 +1,4 @@
-import { createEditor } from './../EditorFactory';
+import { createPlainEditor } from './../EditorFactory';
import { serializeEditorContent } from './../extensions/Serializer.js'
import spec from "./fixtures/spec"
import xssFuzzVectors from './fixtures/xssFuzzVectors';
@@ -14,9 +14,7 @@ const escapeHTML = (s) => {
const plaintextThroughEditor = (markdown) => {
const content = '' + escapeHTML(markdown) + '
'
- const tiptap = createEditor({
- enableRichEditing: false
- })
+ const tiptap = createPlainEditor()
tiptap.commands.setContent(content)
return serializeEditorContent(tiptap) || 'failed'
}
diff --git a/src/tests/tiptap.spec.js b/src/tests/tiptap.spec.js
index a0a615ef982..b4c6729599b 100644
--- a/src/tests/tiptap.spec.js
+++ b/src/tests/tiptap.spec.js
@@ -1,10 +1,8 @@
-import { createEditor } from '../EditorFactory'
+import { createRichEditor } from '../EditorFactory'
import markdownit from '../markdownit'
const renderedHTML = ( markdown ) => {
- const editor = createEditor({
- enableRichEditing: true
- })
+ const editor = createRichEditor()
editor.commands.setContent(markdownit.render(markdown))
// Remove TrailingNode
return editor.getHTML().replace(/<\/p>$/, '')
From 0cd8d8b48f72508c448ac970cf1231e7a2ff5645 Mon Sep 17 00:00:00 2001
From: Max
Date: Sat, 18 May 2024 21:21:53 +0200
Subject: [PATCH 16/17] cleanup(RichText): unused component option
Seems to be unused at the moment.
Also did not find any use for it in the commit that introduced it.
Signed-off-by: Max
---
src/EditorFactory.js | 1 -
src/extensions/RichText.js | 1 -
2 files changed, 2 deletions(-)
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index c47b7347634..744f1d1d608 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -52,7 +52,6 @@ export const createRichEditor = ({ extensions = [], session, relativePath, isEmb
RichText.configure({
relativePath,
isEmbedded,
- component: this,
extensions: [
Mention.configure({
suggestion: MentionSuggestion({
diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js
index 651865de9fd..01717bface9 100644
--- a/src/extensions/RichText.js
+++ b/src/extensions/RichText.js
@@ -72,7 +72,6 @@ export default Extension.create({
return {
editing: true,
extensions: [],
- component: null,
relativePath: null,
isEmbedded: false,
}
From fae0ddf6b9f602c869f1fb462d3736d567c8cd6a Mon Sep 17 00:00:00 2001
From: Max
Date: Tue, 21 May 2024 09:56:42 +0200
Subject: [PATCH 17/17] refactor(Editor): split async `createEditor` from
`onOpened`
Signed-off-by: Max
---
src/components/Editor.vue | 36 ++++++++++++++++++++++--------------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index ff5082d40e4..995209aaf16 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -514,6 +514,16 @@ export default {
this.$syncService.startSync()
return
}
+ this.createEditor()
+ .then(editor => {
+ this.$editor = editor
+ this.hasEditor = true
+ this.listenEditorEvents()
+ })
+ },
+
+ async createEditor() {
+ const session = this.currentSession
const extensions = [
Autofocus.configure({
@@ -534,21 +544,19 @@ export default {
}),
]
- const language = extensionHighlight[this.fileExtension] || this.fileExtension;
-
- (this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language))
- .then(() => {
- this.$editor = this.isRichEditor
- ? createRichEditor({
- relativePath: this.relativePath,
- session,
- extensions,
- isEmbedded: this.isEmbedded,
- })
- : createPlainEditor({ language, extensions })
- this.hasEditor = true
- this.listenEditorEvents()
+ const language = extensionHighlight[this.fileExtension] || this.fileExtension
+
+ if (this.isRichEditor) {
+ return createRichEditor({
+ relativePath: this.relativePath,
+ session,
+ extensions,
+ isEmbedded: this.isEmbedded,
})
+ } else {
+ await loadSyntaxHighlight(language)
+ return createPlainEditor({ language, extensions })
+ }
},
onLoaded({ documentSource, documentState }) {