diff --git a/src/extensions/Markdown.js b/src/extensions/Markdown.js index 6070a6465f..4bce626b90 100644 --- a/src/extensions/Markdown.js +++ b/src/extensions/Markdown.js @@ -110,7 +110,7 @@ const Markdown = Extension.create({ clipboardTextSerializer: (slice) => { const traverseNodes = (slice) => { if (slice.content.childCount > 1) { - return createMarkdownSerializer(this.editor.schema).serialize(slice.content) + return clipboardSerializer(this.editor.schema).serialize(slice.content) } else if (slice.isLeaf) { return slice.textContent } else { @@ -128,12 +128,22 @@ const Markdown = Extension.create({ }) const createMarkdownSerializer = ({ nodes, marks }) => { - const defaultNodes = convertNames(defaultMarkdownSerializer.nodes) - const defaultMarks = convertNames(defaultMarkdownSerializer.marks) return { serializer: new MarkdownSerializer( - { ...defaultNodes, ...extractToMarkdown(nodes) }, - { ...defaultMarks, ...extractToMarkdown(marks) }, + 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 }) @@ -141,15 +151,34 @@ const createMarkdownSerializer = ({ 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) => { - return Object + const nodeOrMarkEntries = Object .entries(nodesOrMarks) .map(([name, nodeOrMark]) => [name, nodeOrMark.spec.toMarkdown]) .filter(([, toMarkdown]) => toMarkdown) - .reduce((items, [name, toMarkdown]) => ({ - ...items, - [name]: 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) => { diff --git a/src/tests/extensions/Markdown.spec.js b/src/tests/extensions/Markdown.spec.js index a1bcc081b6..be11f40d0a 100644 --- a/src/tests/extensions/Markdown.spec.js +++ b/src/tests/extensions/Markdown.spec.js @@ -6,7 +6,7 @@ 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/Underline.js' +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' @@ -85,9 +85,7 @@ describe('Markdown extension integrated in the editor', () => { content: '

', extensions: [Markdown, TaskList, TaskItem], }) - editor.commands.selectAll() - const slice = editor.state.selection.content() - const { text } = serializeForClipboard(editor.view, slice) + const text = copyEditorContent(editor) expect(text).toBe('\n- [ ] Hello') }) @@ -96,9 +94,7 @@ describe('Markdown extension integrated in the editor', () => { content: '
Hello
', extensions: [Markdown, CodeBlock], }) - editor.commands.selectAll() - const slice = editor.state.selection.content() - const { text } = serializeForClipboard(editor.view, slice) + const text = copyEditorContent(editor) expect(text).toBe('Hello') }) @@ -107,10 +103,52 @@ describe('Markdown extension integrated in the editor', () => { content: '

', extensions: [Markdown, Blockquote, TaskList, TaskItem], }) - editor.commands.selectAll() - const slice = editor.state.selection.content() - const { text } = serializeForClipboard(editor.view, slice) + const text = copyEditorContent(editor) expect(text).toBe('\n- [ ] Hello') }) + it('copies address from blockquote to markdown', () => { + const editor = createCustomEditor({ + content: '

Hermannsreute 44A

', + extensions: [Markdown, Blockquote], + }) + const text = copyEditorContent(editor) + expect(text).toBe('Hermannsreute 44A') + }) + + // See https://github.com/nextcloud/text/issues/5660 + it.failing('copy version number without escape character', () => { + const editor = createCustomEditor({ + content: '

Hello

28.0.4

', + extensions: [Markdown], + }) + const text = copyEditorContent(editor) + expect(text).toBe('Hello\n\n28.0.4') + }) + + it('strips bold, italic, and other marks from paragraph', () => { + const editor = createCustomEditor({ + content: '

Hello

lonely world

', + extensions: [Markdown, Italic, Strong, Underline], + }) + const text = copyEditorContent(editor) + expect(text).toBe('Hello\n\nlonely world') + }) + + it('strips href and link formatting from email address', () => { + const editor = createCustomEditor({ + content: '

Hello

example@example.com

', + extensions: [Markdown, Link], + }) + const text = copyEditorContent(editor) + expect(text).toBe('Hello\n\nexample@example.com') + }) + }) + +function copyEditorContent(editor) { + editor.commands.selectAll() + const slice = editor.state.selection.content() + const { text } = serializeForClipboard(editor.view, slice) + return text +}