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 Test${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
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
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: '
- Hello
', - 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: 'Hermannsreute 44A
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
', - 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, 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
' + 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>$/, '')