Skip to content

Commit cc6f3be

Browse files
authored
Merge pull request #7866 from nextcloud/backport/7865/stable32
[stable32] fix(Markdown): copy full block node if it has more than one child
2 parents 6621848 + 63d3059 commit cc6f3be

File tree

2 files changed

+97
-15
lines changed

2 files changed

+97
-15
lines changed

src/extensions/Markdown.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,20 @@ const Markdown = Extension.create({
9494
},
9595
clipboardTextSerializer: (slice) => {
9696
const traverseNodes = (slice) => {
97-
if (slice.content.childCount > 1) {
97+
if (
98+
slice.content.childCount > 1
99+
|| slice.content.firstChild?.childCount > 1
100+
) {
101+
// Selected several nodes or several children of one block node
98102
return clipboardSerializer(
99103
this.editor.schema,
100104
).serialize(slice.content)
101105
} else if (slice.isLeaf) {
102106
return slice.textContent
103107
} else {
108+
// Only one block node selected, copy it's child content
109+
// Required to not copy wrapping block node when selecting e.g. one table
110+
// cell, one list item or the content of block quotes/callouts.
104111
return traverseNodes(slice.content.firstChild)
105112
}
106113
}

src/tests/extensions/Markdown.spec.js

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import { getExtensionField } from '@tiptap/core'
77
import { Blockquote } from '@tiptap/extension-blockquote'
88
import { CodeBlock } from '@tiptap/extension-code-block'
99
import TiptapImage from '@tiptap/extension-image'
10+
import { ListItem } from '@tiptap/extension-list-item'
11+
import { Markdown } from '../../extensions/index.js'
12+
import { createMarkdownSerializer } from '../../extensions/Markdown.js'
13+
import { Italic, Link, Strong, Underline } from '../../marks/index.js'
14+
import Image from '../../nodes/Image.js'
15+
import OrderedList from '../../nodes/OrderedList.js'
16+
import Table from '../../nodes/Table.js'
17+
import TaskItem from '../../nodes/TaskItem.js'
18+
import TaskList from '../../nodes/TaskList.js'
1019
import createCustomEditor from '../testHelpers/createCustomEditor.ts'
11-
import { Markdown } from './../../extensions/index.js'
12-
import { createMarkdownSerializer } from './../../extensions/Markdown.js'
13-
import { Italic, Link, Strong, Underline } from './../../marks/index.js'
14-
import Image from './../../nodes/Image.js'
1520
import ImageInline from './../../nodes/ImageInline.js'
16-
import TaskItem from './../../nodes/TaskItem.js'
17-
import TaskList from './../../nodes/TaskList.js'
1821

1922
describe('Markdown extension unit', () => {
2023
it('has a config', () => {
@@ -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

@@ -44,6 +48,7 @@ describe('Markdown extension integrated in the editor', () => {
4448
])
4549
const serializer = createMarkdownSerializer(editor.schema)
4650
expect(serializer.serialize(editor.state.doc)).toBe('__Test__')
51+
editor.destroy()
4752
})
4853

4954
it('serializes nodes according to their spec', () => {
@@ -53,6 +58,7 @@ describe('Markdown extension integrated in the editor', () => {
5358
)
5459
const serializer = createMarkdownSerializer(editor.schema)
5560
expect(serializer.serialize(editor.state.doc)).toBe('\n- [ ] Hello')
61+
editor.destroy()
5662
})
5763

5864
it('serializes images with the default prosemirror way', () => {
@@ -62,6 +68,7 @@ describe('Markdown extension integrated in the editor', () => {
6268
])
6369
const serializer = createMarkdownSerializer(editor.schema)
6470
expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)')
71+
editor.destroy()
6572
})
6673

6774
it('serializes block images with the default prosemirror way', () => {
@@ -73,6 +80,7 @@ describe('Markdown extension integrated in the editor', () => {
7380
expect(serializer.serialize(editor.state.doc)).toBe(
7481
'![Hello](test)\n\nhello',
7582
)
83+
editor.destroy()
7684
})
7785

7886
it('serializes inline images with the default prosemirror way', () => {
@@ -84,33 +92,67 @@ describe('Markdown extension integrated in the editor', () => {
8492
expect(serializer.serialize(editor.state.doc)).toBe(
8593
'inline image ![Hello](test) inside text',
8694
)
95+
editor.destroy()
8796
})
8897

89-
it('copies task lists to plaintext like markdown', () => {
98+
it('copies markdown syntax for task list if selected together with a paragraph', () => {
9099
const editor = createCustomEditor(
91100
'<p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></p>',
92101
[Markdown, TaskList, TaskItem],
93102
)
94103
const text = copyEditorContent(editor)
95104
expect(text).toBe('\n- [ ] Hello')
105+
editor.destroy()
96106
})
97107

98-
it('copies code block content to plaintext according to their spec', () => {
108+
it('copies just the content of a block node', () => {
99109
const editor = createCustomEditor('<pre><code>Hello</code></pre>', [
100110
Markdown,
101111
CodeBlock,
102112
])
103113
const text = copyEditorContent(editor)
104114
expect(text).toBe('Hello')
115+
editor.destroy()
105116
})
106117

107-
it('copies nested task list nodes to markdown like syntax', () => {
118+
it('copies just the content of a single list item', () => {
108119
const editor = createCustomEditor(
109-
'<blockquote><p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></blockquote>',
120+
'<p>paragraph1</p><ol><li><p>first</p></li></ol><p>paragraph2</p>',
121+
[Markdown, ListItem, OrderedList],
122+
)
123+
const text = copyEditorContent(editor, editor.schema.nodes.orderedList)
124+
expect(text).toBe('first')
125+
editor.destroy()
126+
})
127+
128+
it('copies markdown syntax for multiple list items', () => {
129+
const editor = createCustomEditor(
130+
'<p>paragraph1</p><ol><li><p>first</p></li><li><p>second</p></li></ol><p>paragraph2</p>',
131+
[Markdown, ListItem, OrderedList],
132+
)
133+
const text = copyEditorContent(editor, editor.schema.nodes.orderedList)
134+
expect(text).toBe('1. first\n2. second')
135+
editor.destroy()
136+
})
137+
138+
it('copies just the content of a single nested task list item', () => {
139+
const editor = createCustomEditor(
140+
'<blockquote><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></blockquote>',
110141
[Markdown, Blockquote, TaskList, TaskItem],
111142
)
112143
const text = copyEditorContent(editor)
113-
expect(text).toBe('\n- [ ] Hello')
144+
expect(text).toBe('Hello')
145+
editor.destroy()
146+
})
147+
148+
it('copies markdown syntax for multiple nested task list items', () => {
149+
const editor = createCustomEditor(
150+
'<blockquote><ul class="contains-task-list"><li><input type="checkbox">Hello</li><li><input type="checkbox">World</li></ul></blockquote>',
151+
[Markdown, Blockquote, TaskList, TaskItem],
152+
)
153+
const text = copyEditorContent(editor)
154+
expect(text).toBe('- [ ] Hello\n- [ ] World')
155+
editor.destroy()
114156
})
115157

116158
it('copies address from blockquote to markdown', () => {
@@ -120,12 +162,34 @@ describe('Markdown extension integrated in the editor', () => {
120162
)
121163
const text = copyEditorContent(editor)
122164
expect(text).toBe('Hermannsreute 44A')
165+
editor.destroy()
123166
})
124167

125-
it('copy version number without escape character', () => {
168+
it('copies version number without escape character', () => {
126169
const editor = createCustomEditor('<p>Hello</p><p>28.0.4</p>', [Markdown])
127170
const text = copyEditorContent(editor)
128171
expect(text).toBe('Hello\n\n28.0.4')
172+
editor.destroy()
173+
})
174+
175+
it('copies just content for table cell', () => {
176+
const editor = createCustomEditor(
177+
'<p>paragraph</p><table><tr><th>headercell</th></tr><tr><td>contentcell</td></tr></table>',
178+
[Markdown, Table],
179+
)
180+
const text = copyEditorContent(editor, editor.schema.nodes.tableCell)
181+
expect(text).toBe('contentcell')
182+
editor.destroy()
183+
})
184+
185+
it('copies markdown syntax for full table', () => {
186+
const editor = createCustomEditor(
187+
'<p>paragraph</p><table><tr><th>headercell</th></tr><tr><td>contentcell</td></tr></table>',
188+
[Markdown, Table],
189+
)
190+
const text = copyEditorContent(editor, editor.schema.nodes.table)
191+
expect(text).toBe('| headercell |\n|-------------|\n| contentcell |\n')
192+
editor.destroy()
129193
})
130194

131195
it('strips bold, italic, and other marks from paragraph', () => {
@@ -135,6 +199,7 @@ describe('Markdown extension integrated in the editor', () => {
135199
)
136200
const text = copyEditorContent(editor)
137201
expect(text).toBe('Hello\n\nlonely world')
202+
editor.destroy()
138203
})
139204

140205
it('strips href and link formatting from email address', () => {
@@ -144,11 +209,21 @@ describe('Markdown extension integrated in the editor', () => {
144209
)
145210
const text = copyEditorContent(editor)
146211
expect(text).toBe('Hello\n\nexample@example.com')
212+
editor.destroy()
147213
})
148214
})
149215

150-
const copyEditorContent = (editor) => {
151-
editor.commands.selectAll()
216+
const copyEditorContent = (editor, nodeType = null) => {
217+
if (nodeType) {
218+
editor.state.doc.descendants((node, pos) => {
219+
if (node.type === nodeType) {
220+
editor.commands.setNodeSelection(pos)
221+
}
222+
})
223+
} else {
224+
editor.commands.selectAll()
225+
}
226+
152227
const slice = editor.state.selection.content()
153228
const { text } = editor.view.serializeForClipboard(slice)
154229
return text

0 commit comments

Comments
 (0)