Skip to content

Commit c305e49

Browse files
mejo-backportbot[bot]
authored andcommitted
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> [skip ci]
1 parent 1840982 commit c305e49

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

src/extensions/Markdown.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ const Markdown = Extension.create({
9797
} else if (slice.isLeaf) {
9898
return slice.textContent
9999
} else {
100+
// Only one block node selected, copy it's child content
101+
// Required to not copy wrapping block node when selecting e.g. one table
102+
// cell, one list item or the content of block quotes/callouts.
100103
return traverseNodes(slice.content.firstChild)
101104
}
102105
}

src/tests/extensions/Markdown.spec.js

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('Markdown extension unit', () => {
3333
expect(underline).toEqual(Underline.config.toMarkdown)
3434
const listItem = serializer.serializer.nodes.listItem
3535
expect(typeof listItem).toBe('function')
36+
editor.destroy()
3637
})
3738
})
3839

@@ -41,6 +42,7 @@ describe('Markdown extension integrated in the editor', () => {
4142
const editor = createCustomEditor('<p><u>Test</u></p>', [Markdown, Underline])
4243
const serializer = createMarkdownSerializer(editor.schema)
4344
expect(serializer.serialize(editor.state.doc)).toBe('__Test__')
45+
editor.destroy()
4446
})
4547

4648
it('serializes nodes according to their spec', () => {
@@ -50,6 +52,7 @@ describe('Markdown extension integrated in the editor', () => {
5052
)
5153
const serializer = createMarkdownSerializer(editor.schema)
5254
expect(serializer.serialize(editor.state.doc)).toBe('\n- [ ] Hello')
55+
editor.destroy()
5356
})
5457

5558
it('serializes images with the default prosemirror way', () => {
@@ -59,6 +62,7 @@ describe('Markdown extension integrated in the editor', () => {
5962
)
6063
const serializer = createMarkdownSerializer(editor.schema)
6164
expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)')
65+
editor.destroy()
6266
})
6367

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

82-
it('copies task lists to plaintext like markdown', () => {
86+
it('copies markdown syntax for task list if selected together with a paragraph', () => {
8387
const editor = createCustomEditor(
8488
'<p><ul class="contains-task-list"><li><input type="checkbox">Hello</li></ul></p>',
8589
[Markdown, TaskList, TaskItem],
8690
)
8791
const text = copyEditorContent(editor)
8892
expect(text).toBe('\n- [ ] Hello')
93+
editor.destroy()
8994
})
9095

9196
it('copies code block content to plaintext according to their spec', () => {
@@ -95,15 +100,47 @@ describe('Markdown extension integrated in the editor', () => {
95100
)
96101
const text = copyEditorContent(editor)
97102
expect(text).toBe('Hello')
103+
editor.destroy()
98104
})
99105

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

109146
it('copies address from blockquote to markdown', () => {
@@ -113,6 +150,7 @@ describe('Markdown extension integrated in the editor', () => {
113150
)
114151
const text = copyEditorContent(editor)
115152
expect(text).toBe('Hermannsreute 44A')
153+
editor.destroy()
116154
})
117155

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

127186
it('strips bold, italic, and other marks from paragraph', () => {
@@ -131,6 +190,7 @@ describe('Markdown extension integrated in the editor', () => {
131190
)
132191
const text = copyEditorContent(editor)
133192
expect(text).toBe('Hello\n\nlonely world')
193+
editor.destroy()
134194
})
135195

136196
it('strips href and link formatting from email address', () => {
@@ -140,12 +200,22 @@ describe('Markdown extension integrated in the editor', () => {
140200
)
141201
const text = copyEditorContent(editor)
142202
expect(text).toBe('Hello\n\nexample@example.com')
203+
editor.destroy()
143204
})
144205

145206
})
146207

147-
const copyEditorContent = (editor) => {
148-
editor.commands.selectAll()
208+
const copyEditorContent = (editor, nodeType = null) => {
209+
if (nodeType) {
210+
editor.state.doc.descendants((node, pos) => {
211+
if (node.type === nodeType) {
212+
editor.commands.setNodeSelection(pos)
213+
}
214+
})
215+
} else {
216+
editor.commands.selectAll()
217+
}
218+
149219
const slice = editor.state.selection.content()
150220
const { text } = editor.view.serializeForClipboard(slice)
151221
return text

0 commit comments

Comments
 (0)