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/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..28d2ba4625e 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -21,11 +21,12 @@ * */ -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' 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 */ @@ -36,7 +37,7 @@ describe('Preview extension', { retries: 0 }, () => { const editor = createCustomEditor({ content: '', extensions: [ - Markdown, + Serializer, Preview, Link, Italic, @@ -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) } }) @@ -186,7 +178,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..0c8355c0036 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/index.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..cc91b7fc220 100644 --- a/cypress/e2e/nodes/helpers.js +++ b/cypress/e2e/nodes/helpers.js @@ -22,23 +22,14 @@ 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' -/** - * - * @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,32 +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 - */ -function getMarkdown(editor) { - const serializer = createMarkdownSerializer(editor.schema) - return serializer.serialize(editor.state.doc) +export const getMarkdown = (editor) => { + return serializeEditorContent(editor) } 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..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", @@ -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 cdb667a5681..744f1d1d608 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -22,18 +22,15 @@ 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' 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 { PlainTextLowlight } from './nodes/PlainTextLowlight.js' -const loadSyntaxHighlight = async (language) => { +export const loadSyntaxHighlight = async (language) => { const list = hljs.listLanguages() logger.debug('Supported languages', { list }) if (!lowlight.listLanguages().includes(language)) { @@ -49,42 +46,36 @@ const loadSyntaxHighlight = async (language) => { } } -const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => { - let defaultExtensions - if (enableRichEditing) { - defaultExtensions = [ - 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, + extensions: [ + Mention.configure({ + suggestion: MentionSuggestion({ + session, }), - ], - }), - FocusTrap, - ] - } else { - defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })] - } - - return new Editor({ - onCreate, - onUpdate, - editorProps: { - scrollMargin: 50, - scrollThreshold: 50, - }, - extensions: defaultExtensions.concat(extensions || []), - }) + }), + ], + }), + ...extensions, + ]) } -const serializePlainText = (doc) => { - return doc.textContent +export const createPlainEditor = ({ language, extensions = [] } = {}) => { + return _createEditor([ + PlainText, + PlainTextLowlight + .configure({ lowlight, defaultLanguage: language }), + ...extensions, + ]) } -export default createEditor -export { createEditor, serializePlainText, loadSyntaxHighlight } +const _createEditor = extensions => { + return new Editor({ + editorProps: { scrollMargin: 50, scrollThreshold: 50 }, + extensions, + }) +} diff --git a/src/components/Editor.vue b/src/components/Editor.vue index d6a3482cc18..995209aaf16 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -114,8 +114,12 @@ 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 { createMarkdownSerializer } from './../extensions/Markdown.js' +import { + createRichEditor, + createPlainEditor, + loadSyntaxHighlight, +} from './../EditorFactory.js' +import { serializeEditorContent } from './../extensions/Serializer.js' import markdownit from './../markdownit/index.js' import { CollaborationCursor } from '../extensions/index.js' @@ -392,9 +396,7 @@ export default { filePath: this.relativePath, 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), + serialize: () => serializeEditorContent(this.$editor), getDocumentState: () => getDocumentState(this.$ydoc), }) @@ -416,10 +418,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() { @@ -491,17 +498,65 @@ 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({ - session: this.currentSession, + session, user: getCurrentUser(), shareToken: this.shareToken, currentDirectory: this.currentDirectory, }) + + this.hasConnectionIssue = false + if (this.$editor) { + // $editor already existed. So this is a reconnect. + this.$editor.setEditable(!this.readOnly) + 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({ + 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 + + if (this.isRichEditor) { + return createRichEditor({ + relativePath: this.relativePath, + session, + extensions, + isEmbedded: this.isEmbedded, + }) + } else { + await loadSyntaxHighlight(language) + return createPlainEditor({ language, extensions }) + } }, onLoaded({ documentSource, documentState }) { @@ -516,58 +571,6 @@ export default { } 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, - relativePath: this.relativePath, - session, - onCreate: ({ editor }) => { - this.$syncService.startSync() - }, - onUpdate: ({ editor }) => { - // this.debugContent(editor) - const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc) - this.emit('update:content', { - markdown: proseMirrorMarkdown, - }) - }, - 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() - } - - }) - }, onChange({ document, sessions }) { @@ -666,6 +669,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') }, @@ -736,15 +749,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/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 4bce626b90a..d40b5864d3a 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 @@ -110,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 { @@ -127,69 +92,23 @@ 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( - extractNodesToMarkdown(nodes), - extractToPlaintext(marks), - ), - serialize(content, options) { - return this.serializer.serialize(content, { ...options, tightLists: true }) - }, - } -} - -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 serializeSliceForClipboard = ({ schema }, { content }) => { + return createTextSerializer(schema) + .serialize(content, { tightLists: true }) } -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]), +/* + * 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 { 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..01717bface9 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' @@ -71,7 +72,6 @@ export default Extension.create({ return { editing: true, extensions: [], - component: null, relativePath: null, isEmbedded: false, } @@ -80,6 +80,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..b2a44dfd604 --- /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 const serializeEditorContent = ({ schema, state }) => { + return _createMarkdownSerializer(schema) + .serialize(state.doc, { tightLists: true }) +} + +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), + } + }, + +}) + +/* + * Create the markdown serializer. + * + * Only exported for tests, + */ +export const _createMarkdownSerializer = ({ nodes, marks }) => { + return new MarkdownSerializer( + extractNodesToMarkdown(nodes), + extractMarksToMarkdown(marks), + ) +} + +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/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/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/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, diff --git a/src/tests/builders.js b/src/tests/builders.js index 5f571f0de06..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/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: '

  • Hello

', - 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: '

Hello

', - extensions: [Markdown, TiptapImage.configure({ inline: true })], - }) - const serializer = createMarkdownSerializer(editor.schema) - expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)') - }) - - it('serializes block images with the default prosemirror way', () => { - const editor = createCustomEditor({ - content: '
Hello

hello

', - extensions: [Markdown, Image, ImageInline], - }) - const serializer = createMarkdownSerializer(editor.schema) - expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)\n\nhello') - }) - - it('serializes inline images with the default prosemirror way', () => { - const editor = createCustomEditor({ - content: '

inline image Hello inside text

', - extensions: [Markdown, Image, ImageInline], - }) - const serializer = createMarkdownSerializer(editor.schema) - expect(serializer.serialize(editor.state.doc)).toBe('inline image ![Hello](test) inside text') - }) - it('copies task lists to plaintext like markdown', () => { const editor = createCustomEditor({ content: '

  • Hello

', - 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: '

  • Hello
', - 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..7ec534427c6 --- /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.marks.underline + expect(underline).toEqual(Underline.config.toMarkdown) + const listItem = 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: '

  • Hello

', + extensions: [Serializer, TaskList, TaskItem], + }) + expect(serializeEditorContent(editor)) + .toBe('\n- [ ] Hello') + }) + + it('serializes images with the default prosemirror way', () => { + const editor = createCustomEditor({ + content: '

Hello

', + extensions: [Serializer, TiptapImage.configure({ inline: true })], + }) + expect(serializeEditorContent(editor)) + .toBe('![Hello](test)') + }) + + it('serializes block images with the default prosemirror way', () => { + const editor = createCustomEditor({ + content: '
Hello

hello

', + extensions: [Serializer, Image, ImageInline], + }) + expect(serializeEditorContent(editor)) + .toBe('![Hello](test)\n\nhello') + }) + + it('serializes inline images with the default prosemirror way', () => { + const editor = createCustomEditor({ + content: '

inline image Hello inside text

', + extensions: [Serializer, Image, ImageInline], + }) + expect(serializeEditorContent(editor)) + .toBe('inline image ![Hello](test) inside text') + }) +}) diff --git a/src/tests/helpers.js b/src/tests/helpers.js index 38c17153d6a..ae6bc6b5621 100644 --- a/src/tests/helpers.js +++ b/src/tests/helpers.js @@ -1,11 +1,11 @@ -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' import Text from '@tiptap/extension-text' -import createEditor from '../EditorFactory' +import { createRichEditor } from '../EditorFactory' import markdownit from '../markdownit' export function createCustomEditor({ content, extensions }) { @@ -27,12 +27,9 @@ 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)) - const serializer = createMarkdownSerializer(tiptap.schema) - return serializer.serialize(tiptap.state.doc) + return serializeEditorContent(tiptap) } /** @@ -42,12 +39,9 @@ export function markdownThroughEditor(markdown) { * @returns {string} */ export function markdownThroughEditorHtml(html) { - const tiptap = createEditor({ - enableRichEditing: true - }) + const tiptap = createRichEditor() tiptap.commands.setContent(html) - const serializer = createMarkdownSerializer(tiptap.schema) - return serializer.serialize(tiptap.state.doc) + return serializeEditorContent(tiptap) } /** @@ -57,10 +51,7 @@ export function markdownThroughEditorHtml(html) { * @returns {string} */ export function markdownFromPaste(html) { - const tiptap = createEditor({ - enableRichEditing: true - }) + const tiptap = createRichEditor() 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 7581ea66ee6..19f4db49fc1 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -5,8 +5,8 @@ import { markdownThroughEditorHtml, markdownFromPaste } from './helpers.js' -import { createMarkdownSerializer } from "../extensions/Markdown"; -import createEditor from "../EditorFactory"; +import { serializeEditorContent } from "../extensions/Serializer"; +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() @@ -205,8 +203,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..779f95a2a38 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 { createRichEditor } from '../../EditorFactory' +import { serializeEditorContent } from '../../extensions/Serializer' import { builders } from 'prosemirror-test-builder' import markdownit from '../../markdownit' @@ -63,16 +63,12 @@ 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) }) }) function editorWithContent(content) { - const editor = createEditor({ - enableRichEditing: true, - }) + const editor = createRichEditor() editor.commands.setContent(content) return editor } 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 fdd68fa0eb4..e78d380c21d 100644 --- a/src/tests/plaintext.spec.js +++ b/src/tests/plaintext.spec.js @@ -1,4 +1,5 @@ -import { createEditor, serializePlainText } from './../EditorFactory'; +import { createPlainEditor } from './../EditorFactory'; +import { serializeEditorContent } from './../extensions/Serializer.js' import spec from "./fixtures/spec" import xssFuzzVectors from './fixtures/xssFuzzVectors'; @@ -13,11 +14,9 @@ const escapeHTML = (s) => { const plaintextThroughEditor = (markdown) => { const content = '
' + escapeHTML(markdown) + '
' - const tiptap = createEditor({ - enableRichEditing: false - }) + const tiptap = createPlainEditor() tiptap.commands.setContent(content) - return serializePlainText(tiptap.state.doc) || 'failed' + return serializeEditorContent(tiptap) || 'failed' } describe('commonmark as plaintext', () => { diff --git a/src/tests/tiptap.spec.js b/src/tests/tiptap.spec.js index 1a5fbb27a97..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>$/, '')