Skip to content

Commit b624d7d

Browse files
elzodymax-nextcloud
authored andcommitted
fix: improve node and mark copy-paste behavior
Signed-off-by: Elizabeth Danzberger <[email protected]>
1 parent 98640ee commit b624d7d

File tree

2 files changed

+86
-20
lines changed

2 files changed

+86
-20
lines changed

src/extensions/Markdown.js

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const Markdown = Extension.create({
110110
clipboardTextSerializer: (slice) => {
111111
const traverseNodes = (slice) => {
112112
if (slice.content.childCount > 1) {
113-
return createMarkdownSerializer(this.editor.schema).serialize(slice.content)
113+
return clipboardSerializer(this.editor.schema).serialize(slice.content)
114114
} else if (slice.isLeaf) {
115115
return slice.textContent
116116
} else {
@@ -128,28 +128,57 @@ const Markdown = Extension.create({
128128
})
129129

130130
const createMarkdownSerializer = ({ nodes, marks }) => {
131-
const defaultNodes = convertNames(defaultMarkdownSerializer.nodes)
132-
const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
133131
return {
134132
serializer: new MarkdownSerializer(
135-
{ ...defaultNodes, ...extractToMarkdown(nodes) },
136-
{ ...defaultMarks, ...extractToMarkdown(marks) },
133+
extractNodesToMarkdown(nodes),
134+
extractMarksToMarkdown(marks),
135+
),
136+
serialize(content, options) {
137+
return this.serializer.serialize(content, { ...options, tightLists: true })
138+
},
139+
}
140+
}
141+
142+
const clipboardSerializer = ({ nodes, marks }) => {
143+
return {
144+
serializer: new MarkdownSerializer(
145+
extractNodesToMarkdown(nodes),
146+
extractToPlaintext(marks),
137147
),
138148
serialize(content, options) {
139149
return this.serializer.serialize(content, { ...options, tightLists: true })
140150
},
141151
}
142152
}
143153

154+
const extractToPlaintext = (marks) => {
155+
const blankMark = { open: '', close: '', mixable: true, expelEnclosingWhitespace: true }
156+
const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
157+
const markEntries = Object.entries({ ...defaultMarks, ...marks })
158+
.map(([name, _mark]) => [name, blankMark])
159+
160+
return Object.fromEntries(markEntries)
161+
}
162+
144163
const extractToMarkdown = (nodesOrMarks) => {
145-
return Object
164+
const nodeOrMarkEntries = Object
146165
.entries(nodesOrMarks)
147166
.map(([name, nodeOrMark]) => [name, nodeOrMark.spec.toMarkdown])
148167
.filter(([, toMarkdown]) => toMarkdown)
149-
.reduce((items, [name, toMarkdown]) => ({
150-
...items,
151-
[name]: toMarkdown,
152-
}), {})
168+
169+
return Object.fromEntries(nodeOrMarkEntries)
170+
}
171+
172+
const extractNodesToMarkdown = (nodes) => {
173+
const defaultNodes = convertNames(defaultMarkdownSerializer.nodes)
174+
const nodesToMarkdown = extractToMarkdown(nodes)
175+
return { ...defaultNodes, ...nodesToMarkdown }
176+
}
177+
178+
const extractMarksToMarkdown = (marks) => {
179+
const defaultMarks = convertNames(defaultMarkdownSerializer.marks)
180+
const marksToMarkdown = extractToMarkdown(marks)
181+
return { ...defaultMarks, ...marksToMarkdown }
153182
}
154183

155184
const convertNames = (object) => {

src/tests/extensions/Markdown.spec.js

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Image from './../../nodes/Image.js'
66
import ImageInline from './../../nodes/ImageInline.js'
77
import TaskList from './../../nodes/TaskList.js'
88
import TaskItem from './../../nodes/TaskItem.js'
9-
import Underline from './../../marks/Underline.js'
9+
import { Italic, Strong, Underline, Link} from './../../marks/index.js'
1010
import TiptapImage from '@tiptap/extension-image'
1111
import { getExtensionField } from '@tiptap/core'
1212
import { __serializeForClipboard as serializeForClipboard } from '@tiptap/pm/view'
@@ -85,9 +85,7 @@ describe('Markdown extension integrated in the editor', () => {
8585
content: '<p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></p>',
8686
extensions: [Markdown, TaskList, TaskItem],
8787
})
88-
editor.commands.selectAll()
89-
const slice = editor.state.selection.content()
90-
const { text } = serializeForClipboard(editor.view, slice)
88+
const text = copyEditorContent(editor)
9189
expect(text).toBe('\n- [ ] Hello')
9290
})
9391

@@ -96,9 +94,7 @@ describe('Markdown extension integrated in the editor', () => {
9694
content: '<pre><code>Hello</code></pre>',
9795
extensions: [Markdown, CodeBlock],
9896
})
99-
editor.commands.selectAll()
100-
const slice = editor.state.selection.content()
101-
const { text } = serializeForClipboard(editor.view, slice)
97+
const text = copyEditorContent(editor)
10298
expect(text).toBe('Hello')
10399
})
104100

@@ -107,10 +103,51 @@ describe('Markdown extension integrated in the editor', () => {
107103
content: '<blockquote><p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></blockquote>',
108104
extensions: [Markdown, Blockquote, TaskList, TaskItem],
109105
})
110-
editor.commands.selectAll()
111-
const slice = editor.state.selection.content()
112-
const { text } = serializeForClipboard(editor.view, slice)
106+
const text = copyEditorContent(editor)
113107
expect(text).toBe('\n- [ ] Hello')
114108
})
115109

110+
it('copies address from blockquote to markdown', () => {
111+
const editor = createCustomEditor({
112+
content: '<blockquote><p>Hermannsreute 44A</p></blockquote>',
113+
extensions: [Markdown, Blockquote],
114+
})
115+
const text = copyEditorContent(editor)
116+
expect(text).toBe('Hermannsreute 44A')
117+
})
118+
119+
it('copy version number without escape character', () => {
120+
const editor = createCustomEditor({
121+
content: '<p>Hello</p><p>28.0.4</p>',
122+
extensions: [Markdown],
123+
})
124+
const text = copyEditorContent(editor)
125+
expect(text).toBe('Hello\n\n28.0.4')
126+
})
127+
128+
it('strips bold, italic, and other marks from paragraph', () => {
129+
const editor = createCustomEditor({
130+
content: '<p><strong>Hello</strong></p><p><span style="text-decoration: underline;">lonely </span><em>world</em></p>',
131+
extensions: [Markdown, Italic, Strong, Underline],
132+
})
133+
const text = copyEditorContent(editor)
134+
expect(text).toBe('Hello\n\nlonely world')
135+
})
136+
137+
it('strips href and link formatting from email address', () => {
138+
const editor = createCustomEditor({
139+
content: '<p>Hello</p><p><a href="mailto:[email protected]">[email protected]</a></p>',
140+
extensions: [Markdown, Link],
141+
})
142+
const text = copyEditorContent(editor)
143+
expect(text).toBe('Hello\n\[email protected]')
144+
})
145+
116146
})
147+
148+
function copyEditorContent(editor) {
149+
editor.commands.selectAll()
150+
const slice = editor.state.selection.content()
151+
const { text } = serializeForClipboard(editor.view, slice)
152+
return text
153+
}

0 commit comments

Comments
 (0)