Skip to content

Commit e372795

Browse files
committed
fix(Markdown): copy full block node if it has more than one child
* Single list item is selected: copy only it's content * Multiple list items are selected: copy list with markdown formatting * Single table cell is selected: copy only it's conten * Full table is selected: copy table with markdown formatting Fixes: #7826 Signed-off-by: Jonas <jonas@freesources.org>
1 parent 1840982 commit e372795

2 files changed

Lines changed: 84 additions & 7 deletions

File tree

src/extensions/Markdown.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,15 @@ const Markdown = Extension.create({
9292
},
9393
clipboardTextSerializer: (slice) => {
9494
const traverseNodes = (slice) => {
95-
if (slice.content.childCount > 1) {
95+
if (slice.content.childCount > 1 || slice.content.firstChild?.childCount > 1) {
96+
// Selected several nodes or several children of one block node
9697
return clipboardSerializer(this.editor.schema).serialize(slice.content)
9798
} else if (slice.isLeaf) {
9899
return slice.textContent
99100
} else {
101+
// Only one block node selected, copy it's child content
102+
// Required to not copy wrapping block node when selecting e.g. one table
103+
// cell, one list item or the content of block quotes/callouts.
100104
return traverseNodes(slice.content.firstChild)
101105
}
102106
}

src/tests/extensions/Markdown.spec.js

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { Markdown } from './../../extensions/index.js'
77
import { createMarkdownSerializer } from './../../extensions/Markdown.js'
88
import { CodeBlock } from '@tiptap/extension-code-block'
99
import { Blockquote } from '@tiptap/extension-blockquote'
10+
import { ListItem } from '@tiptap/extension-list-item'
1011
import Image from './../../nodes/Image.js'
1112
import ImageInline from './../../nodes/ImageInline.js'
13+
import OrderedList from './../../nodes/OrderedList.js'
14+
import Table from './../../nodes/Table.js'
1215
import TaskList from './../../nodes/TaskList.js'
1316
import TaskItem from './../../nodes/TaskItem.js'
1417
import { Italic, Strong, Underline, Link } from './../../marks/index.js'
@@ -33,6 +36,7 @@ describe('Markdown extension unit', () => {
3336
expect(underline).toEqual(Underline.config.toMarkdown)
3437
const listItem = serializer.serializer.nodes.listItem
3538
expect(typeof listItem).toBe('function')
39+
editor.destroy()
3640
})
3741
})
3842

@@ -41,6 +45,7 @@ describe('Markdown extension integrated in the editor', () => {
4145
const editor = createCustomEditor('<p><u>Test</u></p>', [Markdown, Underline])
4246
const serializer = createMarkdownSerializer(editor.schema)
4347
expect(serializer.serialize(editor.state.doc)).toBe('__Test__')
48+
editor.destroy()
4449
})
4550

4651
it('serializes nodes according to their spec', () => {
@@ -50,6 +55,7 @@ describe('Markdown extension integrated in the editor', () => {
5055
)
5156
const serializer = createMarkdownSerializer(editor.schema)
5257
expect(serializer.serialize(editor.state.doc)).toBe('\n- [ ] Hello')
58+
editor.destroy()
5359
})
5460

5561
it('serializes images with the default prosemirror way', () => {
@@ -59,6 +65,7 @@ describe('Markdown extension integrated in the editor', () => {
5965
)
6066
const serializer = createMarkdownSerializer(editor.schema)
6167
expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)')
68+
editor.destroy()
6269
})
6370

6471
it('serializes block images with the default prosemirror way', () => {
@@ -79,13 +86,14 @@ describe('Markdown extension integrated in the editor', () => {
7986
expect(serializer.serialize(editor.state.doc)).toBe('inline image ![Hello](test) inside text')
8087
})
8188

82-
it('copies task lists to plaintext like markdown', () => {
89+
it('copies markdown syntax for task list if selected together with a paragraph', () => {
8390
const editor = createCustomEditor(
8491
'<p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></p>',
8592
[Markdown, TaskList, TaskItem],
8693
)
8794
const text = copyEditorContent(editor)
8895
expect(text).toBe('\n- [ ] Hello')
96+
editor.destroy()
8997
})
9098

9199
it('copies code block content to plaintext according to their spec', () => {
@@ -95,15 +103,47 @@ describe('Markdown extension integrated in the editor', () => {
95103
)
96104
const text = copyEditorContent(editor)
97105
expect(text).toBe('Hello')
106+
editor.destroy()
98107
})
99108

100-
it('copies nested task list nodes to markdown like syntax', () => {
109+
it('copies just the content of a single list item', () => {
101110
const editor = createCustomEditor(
102-
'<blockquote><p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></blockquote>',
111+
'<p>paragraph1</p><ol><li><p>first</p></li></ol><p>paragraph2</p>',
112+
[Markdown, ListItem, OrderedList],
113+
)
114+
const text = copyEditorContent(editor, editor.schema.nodes.orderedList)
115+
expect(text).toBe('first')
116+
editor.destroy()
117+
})
118+
119+
it('copies markdown syntax for multiple list items', () => {
120+
const editor = createCustomEditor(
121+
'<p>paragraph1</p><ol><li><p>first</p></li><li><p>second</p></li></ol><p>paragraph2</p>',
122+
[Markdown, ListItem, OrderedList],
123+
)
124+
const text = copyEditorContent(editor, editor.schema.nodes.orderedList)
125+
expect(text).toBe('1. first\n2. second')
126+
editor.destroy()
127+
})
128+
129+
it('copies just the content of a single nested task list item', () => {
130+
const editor = createCustomEditor(
131+
'<blockquote><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></blockquote>',
103132
[Markdown, Blockquote, TaskList, TaskItem],
104133
)
105134
const text = copyEditorContent(editor)
106-
expect(text).toBe('\n- [ ] Hello')
135+
expect(text).toBe('Hello')
136+
editor.destroy()
137+
})
138+
139+
it('copies markdown syntax for multiple nested task list items', () => {
140+
const editor = createCustomEditor(
141+
'<blockquote><ul class="contains-task-list"><li><input type="checkbox">Hello</li><li><input type="checkbox">World</li></ul></blockquote>',
142+
[Markdown, Blockquote, TaskList, TaskItem],
143+
)
144+
const text = copyEditorContent(editor)
145+
expect(text).toBe('- [ ] Hello\n- [ ] World')
146+
editor.destroy()
107147
})
108148

109149
it('copies address from blockquote to markdown', () => {
@@ -113,6 +153,7 @@ describe('Markdown extension integrated in the editor', () => {
113153
)
114154
const text = copyEditorContent(editor)
115155
expect(text).toBe('Hermannsreute 44A')
156+
editor.destroy()
116157
})
117158

118159
it('copy version number without escape character', () => {
@@ -122,6 +163,27 @@ describe('Markdown extension integrated in the editor', () => {
122163
)
123164
const text = copyEditorContent(editor)
124165
expect(text).toBe('Hello\n\n28.0.4')
166+
editor.destroy()
167+
})
168+
169+
it('copies just content for table cell', () => {
170+
const editor = createCustomEditor(
171+
'<p>paragraph</p><table><tr><th>headercell</th></tr><tr><td>contentcell</td></tr></table>',
172+
[Markdown, Table],
173+
)
174+
const text = copyEditorContent(editor, editor.schema.nodes.tableCell)
175+
expect(text).toBe('contentcell')
176+
editor.destroy()
177+
})
178+
179+
it('copies markdown syntax for full table', () => {
180+
const editor = createCustomEditor(
181+
'<p>paragraph</p><table><tr><th>headercell</th></tr><tr><td>contentcell</td></tr></table>',
182+
[Markdown, Table],
183+
)
184+
const text = copyEditorContent(editor, editor.schema.nodes.table)
185+
expect(text).toBe('| headercell |\n|-------------|\n| contentcell |\n')
186+
editor.destroy()
125187
})
126188

127189
it('strips bold, italic, and other marks from paragraph', () => {
@@ -131,6 +193,7 @@ describe('Markdown extension integrated in the editor', () => {
131193
)
132194
const text = copyEditorContent(editor)
133195
expect(text).toBe('Hello\n\nlonely world')
196+
editor.destroy()
134197
})
135198

136199
it('strips href and link formatting from email address', () => {
@@ -140,12 +203,22 @@ describe('Markdown extension integrated in the editor', () => {
140203
)
141204
const text = copyEditorContent(editor)
142205
expect(text).toBe('Hello\n\nexample@example.com')
206+
editor.destroy()
143207
})
144208

145209
})
146210

147-
const copyEditorContent = (editor) => {
148-
editor.commands.selectAll()
211+
const copyEditorContent = (editor, nodeType = null) => {
212+
if (nodeType) {
213+
editor.state.doc.descendants((node, pos) => {
214+
if (node.type === nodeType) {
215+
editor.commands.setNodeSelection(pos)
216+
}
217+
})
218+
} else {
219+
editor.commands.selectAll()
220+
}
221+
149222
const slice = editor.state.selection.content()
150223
const { text } = editor.view.serializeForClipboard(slice)
151224
return text

0 commit comments

Comments
 (0)