Skip to content

Commit 6cb00be

Browse files
emmatowndcousens
authored andcommitted
Change markdown parsing when pasting plain text to be more conservative in the document editor (#7768)
1 parent 0114841 commit 6cb00be

File tree

5 files changed

+128
-69
lines changed

5 files changed

+128
-69
lines changed

.changeset/fast-gorillas-build.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@keystone-6/fields-document': patch
3+
---
4+
5+
Fixes pasting plain text in the document editor removing markdown link definition and usages

packages/fields-document/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"io-ts-excess": "^1.0.1",
4646
"is-hotkey": "^0.2.0",
4747
"match-sorter": "^6.3.1",
48-
"mdast-util-definitions": "^4.0.0",
4948
"mdast-util-from-markdown": "^0.8.5",
5049
"mdast-util-gfm-autolink-literal": "^0.1.3",
5150
"mdast-util-gfm-strikethrough": "^0.2.3",

packages/fields-document/src/DocumentEditor/pasting/markdown.test.tsx

+104-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @jest-environment jsdom */
22
/** @jsxRuntime classic */
33
/** @jsx jsx */
4+
import { Node } from 'slate';
45
import { makeEditor, jsx } from '../tests/utils';
56
import { MyDataTransfer } from './data-transfer';
67

@@ -247,20 +248,23 @@ there is a break before this
247248
</text>
248249
</paragraph>
249250
<paragraph>
250-
<text />
251-
<link
252-
@@isInline={true}
253-
href="http://keystonejs.com/link-reference"
254-
>
255-
<text>
256-
Link reference
257-
</text>
258-
</link>
259-
<text />
251+
<text>
252+
[Link reference][1]
253+
</text>
260254
</paragraph>
261255
<paragraph>
262256
<text>
263-
![Image reference](http://keystonejs.com/image-reference)
257+
![Image reference][2]
258+
</text>
259+
</paragraph>
260+
<paragraph>
261+
<text>
262+
[1]: http://keystonejs.com/link-reference
263+
</text>
264+
</paragraph>
265+
<paragraph>
266+
<text>
267+
[2]: http://keystonejs.com/image-reference
264268
<cursor />
265269
</text>
266270
</paragraph>
@@ -345,3 +349,92 @@ test('a link nested inside bold', () => {
345349
</editor>
346350
`);
347351
});
352+
353+
// this is written like this rather than a snapshot because the snapshot
354+
// formatting creates html escapes(which is nice for the formatting)
355+
// this test shows ensures that the snapshot formatting is not buggy
356+
// and we're not showing html escapes to users or something
357+
test('html in inline content is just written', () => {
358+
const input = `a<code>blah</code>b`;
359+
expect(Node.string(deserializeMarkdown(input))).toEqual(input);
360+
});
361+
362+
test('html in complex inline content', () => {
363+
expect(deserializeMarkdown(`__content [link<code>blah</code>](https://keystonejs.com) content__`))
364+
.toMatchInlineSnapshot(`
365+
<editor
366+
marks={
367+
Object {
368+
"bold": true,
369+
}
370+
}
371+
>
372+
<paragraph>
373+
<text
374+
bold={true}
375+
>
376+
content
377+
</text>
378+
<link
379+
@@isInline={true}
380+
href="https://keystonejs.com"
381+
>
382+
<text
383+
bold={true}
384+
>
385+
link&lt;code&gt;blah&lt;/code&gt;
386+
</text>
387+
</link>
388+
<text
389+
bold={true}
390+
>
391+
content
392+
<cursor />
393+
</text>
394+
</paragraph>
395+
</editor>
396+
`);
397+
});
398+
399+
// the difference between a delightful "oh, nice! the editor did the formatting i wanted"
400+
// and "UGH!! the editor just removed some of the content i wanted" can be really subtle
401+
// and while we want the delightful experiences, avoiding the bad experiences is _more important_
402+
403+
// so even though we could parse link references & definitions in some cases we don't because it feels a bit too magical
404+
// also note that so the workaround of "paste into some plain text place, copy it from there"
405+
// like html pasting doesn't exist here since this is parsing _from_ plain text
406+
// so erring on the side of "don't be too smart" is better
407+
test('link and image references and images are left alone', () => {
408+
const input = `[Link reference][1]
409+
410+
![Image reference][2]
411+
412+
![Image](http://keystonejs.com/image)
413+
414+
[1]: http://keystonejs.com/link-reference
415+
416+
[2]: http://keystonejs.com/image-reference`;
417+
418+
expect(
419+
deserializeMarkdown(input)
420+
.children.map(node => Node.string(node))
421+
.join('\n\n')
422+
).toEqual(input);
423+
});
424+
425+
// ideally, we would probably convert the mark here, but like the comment on the previous test says,
426+
// it being not perfect is fine, as long as it doesn't make things _worse_
427+
test('marks in image tags are converted', () => {
428+
const input = `![Image **blah**](https://keystonejs.com/image)`;
429+
430+
expect(deserializeMarkdown(input)).toMatchInlineSnapshot(`
431+
<editor>
432+
<paragraph>
433+
<text>
434+
![Image **blah**](https://keystonejs.com/image)
435+
<cursor />
436+
</text>
437+
</paragraph>
438+
</editor>
439+
`);
440+
});

packages/fields-document/src/DocumentEditor/pasting/markdown.ts

+19-41
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import autoLinkLiteralMarkdownSyntax from 'micromark-extension-gfm-autolink-lite
66
// @ts-ignore
77
import gfmStrikethroughFromMarkdownExtension from 'mdast-util-gfm-strikethrough/from-markdown';
88
import gfmStrikethroughMarkdownSyntax from 'micromark-extension-gfm-strikethrough';
9-
import definitions from 'mdast-util-definitions';
109
import { Descendant } from 'slate';
1110
import { getTextNodeForCurrentlyActiveMarks, addMarkToChildren } from './utils';
1211

@@ -17,22 +16,19 @@ const markdownConfig = {
1716

1817
export function deserializeMarkdown(markdown: string) {
1918
const root = mdASTUtilFromMarkdown(markdown, markdownConfig);
20-
const getDefinition = definitions(root);
2119
let nodes = root.children;
2220
if (nodes.length === 1 && nodes[0].type === 'paragraph') {
2321
nodes = nodes[0].children;
2422
}
25-
return deserializeChildren(nodes, getDefinition);
23+
return deserializeChildren(nodes, markdown);
2624
}
2725

28-
type GetDefinition = ReturnType<typeof definitions>;
29-
3026
type MDNode = ReturnType<typeof mdASTUtilFromMarkdown>['children'][number];
3127

32-
function deserializeChildren(nodes: MDNode[], getDefinition: GetDefinition) {
28+
function deserializeChildren(nodes: MDNode[], input: string) {
3329
const outputNodes: Descendant[] = [];
3430
for (const node of nodes) {
35-
const result = deserializeMarkdownNode(node, getDefinition);
31+
const result = deserializeMarkdownNode(node, input);
3632
if (result.length) {
3733
outputNodes.push(...result);
3834
}
@@ -43,64 +39,45 @@ function deserializeChildren(nodes: MDNode[], getDefinition: GetDefinition) {
4339
return outputNodes;
4440
}
4541

46-
function deserializeMarkdownNode(node: MDNode, getDefinition: GetDefinition): Descendant[] {
42+
function deserializeMarkdownNode(node: MDNode, input: string): Descendant[] {
4743
switch (node.type) {
4844
case 'blockquote': {
49-
return [{ type: 'blockquote', children: deserializeChildren(node.children, getDefinition) }];
50-
}
51-
case 'linkReference': {
52-
return [
53-
{
54-
type: 'link',
55-
href: getDefinition(node.identifier)?.url || '',
56-
children: deserializeChildren(node.children, getDefinition),
57-
},
58-
];
45+
return [{ type: 'blockquote', children: deserializeChildren(node.children, input) }];
5946
}
6047
case 'link': {
6148
return [
6249
{
6350
type: 'link',
6451
href: node.url,
65-
children: deserializeChildren(node.children, getDefinition),
52+
children: deserializeChildren(node.children, input),
6653
},
6754
];
6855
}
6956
case 'code': {
7057
return [{ type: 'code', children: [{ text: node.value }] }];
7158
}
7259
case 'paragraph': {
73-
return [{ type: 'paragraph', children: deserializeChildren(node.children, getDefinition) }];
60+
return [{ type: 'paragraph', children: deserializeChildren(node.children, input) }];
7461
}
7562
case 'heading': {
7663
return [
7764
{
7865
type: 'heading',
7966
level: node.depth,
80-
children: deserializeChildren(node.children, getDefinition),
67+
children: deserializeChildren(node.children, input),
8168
},
8269
];
8370
}
8471
case 'list': {
8572
return [
8673
{
8774
type: node.ordered ? 'ordered-list' : 'unordered-list',
88-
children: deserializeChildren(node.children, getDefinition),
75+
children: deserializeChildren(node.children, input),
8976
},
9077
];
9178
}
92-
case 'imageReference': {
93-
return [
94-
getTextNodeForCurrentlyActiveMarks(
95-
`![${node.alt || ''}](${getDefinition(node.identifier)?.url || ''})`
96-
),
97-
];
98-
}
99-
case 'image': {
100-
return [getTextNodeForCurrentlyActiveMarks(`![${node.alt || ''}](${node.url})`)];
101-
}
10279
case 'listItem': {
103-
return [{ type: 'list-item', children: deserializeChildren(node.children, getDefinition) }];
80+
return [{ type: 'list-item', children: deserializeChildren(node.children, input) }];
10481
}
10582
case 'thematicBreak': {
10683
return [{ type: 'divider', children: [{ text: '' }] }];
@@ -109,28 +86,29 @@ function deserializeMarkdownNode(node: MDNode, getDefinition: GetDefinition): De
10986
return [getTextNodeForCurrentlyActiveMarks('\n')];
11087
}
11188
case 'delete': {
112-
return addMarkToChildren('strikethrough', () =>
113-
deserializeChildren(node.children, getDefinition)
114-
);
89+
return addMarkToChildren('strikethrough', () => deserializeChildren(node.children, input));
11590
}
11691
case 'strong': {
117-
return addMarkToChildren('bold', () => deserializeChildren(node.children, getDefinition));
92+
return addMarkToChildren('bold', () => deserializeChildren(node.children, input));
11893
}
11994
case 'emphasis': {
120-
return addMarkToChildren('italic', () => deserializeChildren(node.children, getDefinition));
95+
return addMarkToChildren('italic', () => deserializeChildren(node.children, input));
12196
}
12297
case 'inlineCode': {
12398
return addMarkToChildren('code', () => [getTextNodeForCurrentlyActiveMarks(node.value)]);
12499
}
125-
// while it would be nice if we parsed the html here
100+
// while it might be nice if we parsed the html here
126101
// it's a bit more complicated than just parsing the html
127102
// because an html node might just be an opening/closing node
128103
// but we just have an opening/closing node here
129104
// not the opening and closing and children
130-
case 'html':
131105
case 'text': {
132106
return [getTextNodeForCurrentlyActiveMarks(node.value)];
133107
}
134108
}
135-
return [];
109+
return [
110+
getTextNodeForCurrentlyActiveMarks(
111+
input.slice(node.position!.start.offset, node.position!.end.offset)
112+
),
113+
];
136114
}

yarn.lock

-16
Original file line numberDiff line numberDiff line change
@@ -9750,13 +9750,6 @@ mdast-util-compact@^1.0.0:
97509750
dependencies:
97519751
unist-util-visit "^1.1.0"
97529752

9753-
mdast-util-definitions@^4.0.0:
9754-
version "4.0.0"
9755-
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
9756-
integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==
9757-
dependencies:
9758-
unist-util-visit "^2.0.0"
9759-
97609753
mdast-util-definitions@^5.0.0:
97619754
version "5.1.1"
97629755
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz#2c1d684b28e53f84938bb06317944bee8efa79db"
@@ -13915,15 +13908,6 @@ unist-util-visit@^1.0.0, unist-util-visit@^1.1.0:
1391513908
dependencies:
1391613909
unist-util-visit-parents "^2.0.0"
1391713910

13918-
unist-util-visit@^2.0.0:
13919-
version "2.0.3"
13920-
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
13921-
integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
13922-
dependencies:
13923-
"@types/unist" "^2.0.0"
13924-
unist-util-is "^4.0.0"
13925-
unist-util-visit-parents "^3.0.0"
13926-
1392713911
unist-util-visit@^4.0.0:
1392813912
version "4.1.0"
1392913913
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5"

0 commit comments

Comments
 (0)