Skip to content

Commit 70ee410

Browse files
authored
fix(Lists): preserve markup of list items (#278)
1 parent 2c435bd commit 70ee410

File tree

10 files changed

+147
-41
lines changed

10 files changed

+147
-41
lines changed

src/core/markdown/MarkdownSerializer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export class MarkdownSerializerState {
286286
this.inTightList = isTight;
287287
node.forEach((child, _, i) => {
288288
if (i && isTight) this.flushClose(1);
289-
this.wrapBlock(delim, firstDelim(i), node, () => this.render(child, node, i));
289+
this.wrapBlock(delim, firstDelim(i, child), node, () => this.render(child, node, i));
290290
});
291291
this.inTightList = prevTight;
292292
}

src/extensions/markdown/Lists/Lists.test.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {createMarkupChecker} from '../../../../tests/sameMarkup';
44
import {ExtensionsManager} from '../../../core';
55
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
66

7-
import {ListNode, ListsSpecs} from './ListsSpecs';
7+
import {ListNode, ListsAttr, ListsSpecs} from './ListsSpecs';
88

99
const {
1010
schema,
@@ -26,35 +26,94 @@ const {same} = createMarkupChecker({parser, serializer});
2626

2727
describe('Lists extension', () => {
2828
it('should parse bullet list', () => {
29-
same('* one\n\n* two', doc(ul(li(p('one')), li(p('two')))));
29+
same(
30+
'* one\n\n* two',
31+
doc(
32+
ul(
33+
{[ListsAttr.Bullet]: '*'},
34+
li({[ListsAttr.Markup]: '*'}, p('one')),
35+
li({[ListsAttr.Markup]: '*'}, p('two')),
36+
),
37+
),
38+
);
3039
});
3140

3241
it('should parse ordered list', () => {
33-
same('1. one\n\n2. two', doc(ol(li(p('one')), li(p('two')))));
42+
same(
43+
'1. one\n\n2. two',
44+
doc(
45+
ol(
46+
li({[ListsAttr.Markup]: '.'}, p('one')),
47+
li({[ListsAttr.Markup]: '.'}, p('two')),
48+
),
49+
),
50+
);
3451
});
3552

3653
it('should parse nested lists', () => {
3754
const markup = `
38-
* one
55+
- one
3956
4057
1. two
4158
42-
* three
59+
+ three
4360
4461
2. four
4562
46-
* five
63+
- five
4764
`.trim();
4865

4966
same(
5067
markup,
5168
doc(
5269
ul(
70+
{[ListsAttr.Bullet]: '-'},
5371
li(
72+
{[ListsAttr.Markup]: '-'},
5473
p('one'),
55-
ol(li(p('two'), ul({tight: true}, li(p('three')))), li(p('four'))),
74+
ol(
75+
li(
76+
{[ListsAttr.Markup]: '.'},
77+
p('two'),
78+
ul(
79+
{[ListsAttr.Tight]: true, [ListsAttr.Bullet]: '+'},
80+
li({[ListsAttr.Markup]: '+'}, p('three')),
81+
),
82+
),
83+
li({[ListsAttr.Markup]: '.'}, p('four')),
84+
),
85+
),
86+
li({[ListsAttr.Markup]: '-'}, p('five')),
87+
),
88+
),
89+
);
90+
});
91+
92+
it('should parse nested lists 2', () => {
93+
same(
94+
'- + * 2. item',
95+
doc(
96+
ul(
97+
{[ListsAttr.Bullet]: '-'},
98+
li(
99+
{[ListsAttr.Markup]: '-'},
100+
ul(
101+
{[ListsAttr.Bullet]: '+'},
102+
li(
103+
{[ListsAttr.Markup]: '+'},
104+
ul(
105+
{[ListsAttr.Bullet]: '*'},
106+
li(
107+
{[ListsAttr.Markup]: '*'},
108+
ol(
109+
{[ListsAttr.Order]: 2, [ListsAttr.Tight]: true},
110+
li({[ListsAttr.Markup]: '.'}, p('item')),
111+
),
112+
),
113+
),
114+
),
115+
),
56116
),
57-
li(p('five')),
58117
),
59118
),
60119
);

src/extensions/markdown/Lists/ListsSpecs/const.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@ export enum ListNode {
33
BulletList = 'bullet_list',
44
OrderedList = 'ordered_list',
55
}
6+
7+
export enum ListsAttr {
8+
Tight = 'tight',
9+
/** used in bullet list only */
10+
Bullet = 'bullet',
11+
/** used in ordered list only */
12+
Order = 'order',
13+
/** used in list item only */
14+
Markup = 'markup',
15+
}

src/extensions/markdown/Lists/ListsSpecs/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {parserTokens} from './parser';
66
import {schemaSpecs} from './schema';
77
import {serializerTokens} from './serializer';
88

9-
export {ListNode} from './const';
9+
export {ListsAttr, ListNode} from './const';
1010
export const liType = nodeTypeFactory(ListNode.ListItem);
1111
export const blType = nodeTypeFactory(ListNode.BulletList);
1212
export const olType = nodeTypeFactory(ListNode.OrderedList);

src/extensions/markdown/Lists/ListsSpecs/parser.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@ import type Token from 'markdown-it/lib/token';
22

33
import type {ParserToken} from '../../../../core';
44

5-
import {ListNode} from './const';
5+
import {ListNode, ListsAttr} from './const';
66

77
export const parserTokens: Record<ListNode, ParserToken> = {
8-
[ListNode.ListItem]: {name: ListNode.ListItem, type: 'block'},
8+
[ListNode.ListItem]: {
9+
name: ListNode.ListItem,
10+
type: 'block',
11+
getAttrs: (token) => ({[ListsAttr.Markup]: token.markup}),
12+
},
913

1014
[ListNode.BulletList]: {
1115
name: ListNode.BulletList,
1216
type: 'block',
13-
getAttrs: (_, tokens, i) => ({tight: listIsTight(tokens, i)}),
17+
getAttrs: (token, tokens, i) => ({
18+
[ListsAttr.Tight]: listIsTight(tokens, i),
19+
[ListsAttr.Bullet]: token.markup,
20+
}),
1421
},
1522

1623
[ListNode.OrderedList]: {
1724
name: ListNode.OrderedList,
1825
type: 'block',
19-
getAttrs: (tok, tokens, i) => ({
20-
order: Number(tok.attrGet('start')) || 1,
21-
tight: listIsTight(tokens, i),
26+
getAttrs: (token, tokens, i) => ({
27+
[ListsAttr.Order]: Number(token.attrGet('start')) || 1,
28+
[ListsAttr.Tight]: listIsTight(tokens, i),
2229
}),
2330
},
2431
};

src/extensions/markdown/Lists/ListsSpecs/schema.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type {NodeSpec} from 'prosemirror-model';
22

3-
import {ListNode} from './const';
3+
import {ListNode, ListsAttr} from './const';
44

55
export const schemaSpecs: Record<ListNode, NodeSpec> = {
66
[ListNode.ListItem]: {
7-
attrs: {tight: {default: false}},
7+
attrs: {[ListsAttr.Tight]: {default: false}, [ListsAttr.Markup]: {default: null}},
88
content: '(paragraph|block)+',
99
defining: true,
1010
parseDOM: [{tag: 'li'}],
@@ -20,34 +20,36 @@ export const schemaSpecs: Record<ListNode, NodeSpec> = {
2020
[ListNode.BulletList]: {
2121
content: `${ListNode.ListItem}+`,
2222
group: 'block',
23-
attrs: {tight: {default: false}},
23+
attrs: {[ListsAttr.Tight]: {default: false}, [ListsAttr.Bullet]: {default: '*'}},
2424
parseDOM: [
2525
{
2626
tag: 'ul',
27-
getAttrs: (dom) => ({tight: (dom as HTMLElement).hasAttribute('data-tight')}),
27+
getAttrs: (dom) => ({
28+
[ListsAttr.Tight]: (dom as HTMLElement).hasAttribute('data-tight'),
29+
}),
2830
},
2931
],
3032
toDOM(node) {
31-
return ['ul', {'data-tight': node.attrs.tight ? 'true' : null}, 0];
33+
return ['ul', {'data-tight': node.attrs[ListsAttr.Tight] ? 'true' : null}, 0];
3234
},
3335
selectable: false,
3436
allowSelection: false,
3537
complex: 'root',
3638
},
3739

3840
[ListNode.OrderedList]: {
39-
attrs: {order: {default: 1}, tight: {default: false}},
41+
attrs: {[ListsAttr.Order]: {default: 1}, [ListsAttr.Tight]: {default: false}},
4042
content: `${ListNode.ListItem}+`,
4143
group: 'block',
4244
parseDOM: [
4345
{
4446
tag: 'ol',
4547
getAttrs(dom) {
4648
return {
47-
order: (dom as HTMLElement).hasAttribute('start')
49+
[ListsAttr.Order]: (dom as HTMLElement).hasAttribute('start')
4850
? Number((dom as HTMLElement).getAttribute('start')!)
4951
: 1,
50-
tight: (dom as HTMLElement).hasAttribute('data-tight'),
52+
[ListsAttr.Tight]: (dom as HTMLElement).hasAttribute('data-tight'),
5153
};
5254
},
5355
},
@@ -56,8 +58,8 @@ export const schemaSpecs: Record<ListNode, NodeSpec> = {
5658
return [
5759
'ol',
5860
{
59-
start: node.attrs.order === 1 ? null : node.attrs.order,
60-
'data-tight': node.attrs.tight ? 'true' : null,
61+
start: node.attrs[ListsAttr.Order] === 1 ? null : node.attrs[ListsAttr.Order],
62+
'data-tight': node.attrs[ListsAttr.Tight] ? 'true' : null,
6163
},
6264
0,
6365
];

src/extensions/markdown/Lists/ListsSpecs/serializer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1+
import type {Node} from 'prosemirror-model';
2+
13
import type {SerializerNodeToken} from '../../../../core';
24

3-
import {ListNode} from './const';
5+
import {ListNode, ListsAttr} from './const';
46

57
export const serializerTokens: Record<ListNode, SerializerNodeToken> = {
68
[ListNode.ListItem]: (state, node) => {
79
state.renderContent(node);
810
},
911

1012
[ListNode.BulletList]: (state, node) => {
11-
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ');
13+
state.renderList(
14+
node,
15+
' ',
16+
(_i: number, li: Node) =>
17+
(li.attrs[ListsAttr.Markup] || node.attrs[ListsAttr.Bullet] || '*') + ' ',
18+
);
1219
},
1320

1421
[ListNode.OrderedList]: (state, node) => {
15-
const start = node.attrs.order || 1;
22+
const start = node.attrs[ListsAttr.Order] || 1;
1623
const maxW = String(start + node.childCount - 1).length;
1724
const space = state.repeat(' ', maxW + 2);
1825
state.renderList(node, space, (i: number) => {

src/extensions/markdown/Lists/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {ListAction} from './const';
1111
import {ListsInputRulesExtension, ListsInputRulesOptions} from './inputrules';
1212
import {mergeListsPlugin} from './plugins/MergeListsPlugin';
1313

14-
export {ListNode, blType, liType, olType} from './ListsSpecs';
14+
export {ListNode, ListsAttr, blType, liType, olType} from './ListsSpecs';
1515

1616
export type ListsOptions = {
1717
ulKey?: string | null;

src/extensions/markdown/Lists/inputrules.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {NodeType} from 'prosemirror-model';
33
import type {ExtensionWithOptions} from '../../../core';
44
import {wrappingInputRule} from '../../../utils/inputrules';
55

6+
import {ListsAttr} from './ListsSpecs';
67
import {blType, olType} from './utils';
78

89
export type ListsInputRulesOptions = {
@@ -31,8 +32,8 @@ export function orderedListRule(nodeType: NodeType) {
3132
return wrappingInputRule(
3233
/^(\d+)\.\s$/,
3334
nodeType,
34-
(match) => ({order: Number(match[1])}),
35-
(match, node) => node.childCount + node.attrs.order === Number(match[1]),
35+
(match) => ({[ListsAttr.Order]: Number(match[1])}),
36+
(match, node) => node.childCount + node.attrs[ListsAttr.Order] === Number(match[1]),
3637
);
3738
}
3839

@@ -58,5 +59,5 @@ export function bulletListRule(nodeType: NodeType, config?: BulletListInputRuleC
5859
if (bullets.length === 0) return null;
5960

6061
const regexp = new RegExp(`^\\s*([${bullets.join('')}])\\s$`); // same as /^\s*([-+*])\s$/
61-
return wrappingInputRule(regexp, nodeType);
62+
return wrappingInputRule(regexp, nodeType, (match) => ({[ListsAttr.Bullet]: match[1]}));
6263
}

src/extensions/markdown/Lists/plugins/MergeListsPlugin.test.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {EditorView} from 'prosemirror-view';
55
import {ExtensionsManager} from '../../../../core';
66
import {BaseNode, BaseSchemaSpecs} from '../../../base/specs';
77
import {CodeSpecs, codeMarkName} from '../../Code/CodeSpecs';
8-
import {ListNode, ListsSpecs} from '../ListsSpecs';
8+
import {ListNode, ListsAttr, ListsSpecs} from '../ListsSpecs';
99

1010
import {mergeListsPlugin} from './MergeListsPlugin';
1111

@@ -48,8 +48,12 @@ describe('Lists extension', () => {
4848
expect(view.state.doc).toMatchNode(
4949
doc(
5050
ul(
51-
{tight: true},
51+
{
52+
[ListsAttr.Tight]: true,
53+
[ListsAttr.Bullet]: '+',
54+
},
5255
li(
56+
{[ListsAttr.Markup]: '+'},
5357
p(
5458
'Create a list by starting a line with ',
5559
c('+'),
@@ -60,17 +64,33 @@ describe('Lists extension', () => {
6064
),
6165
),
6266
li(
67+
{[ListsAttr.Markup]: '-'},
6368
p('Sub-lists are made by indenting 2 spaces:'),
6469
ul(
65-
{tight: true},
66-
li(p('Marker character change forces new list start:')),
67-
li(p('Ac tristique libero volutpat at')),
68-
li(p('Facilisis in pretium nisl aliquet')),
69-
li(p('Nulla volutpat aliquam velit')),
70+
{
71+
[ListsAttr.Tight]: true,
72+
[ListsAttr.Bullet]: '-',
73+
},
74+
li(
75+
{[ListsAttr.Markup]: '-'},
76+
p('Marker character change forces new list start:'),
77+
),
78+
li({[ListsAttr.Markup]: '*'}, p('Ac tristique libero volutpat at')),
79+
li(
80+
{[ListsAttr.Markup]: '+'},
81+
p('Facilisis in pretium nisl aliquet'),
82+
),
83+
li({[ListsAttr.Markup]: '-'}, p('Nulla volutpat aliquam velit')),
7084
),
7185
),
7286
),
73-
ul({tight: true}, li(p('Very easy!'))),
87+
ul(
88+
{
89+
[ListsAttr.Tight]: true,
90+
[ListsAttr.Bullet]: '*',
91+
},
92+
li({[ListsAttr.Markup]: '*'}, p('Very easy!')),
93+
),
7494
),
7595
);
7696
});

0 commit comments

Comments
 (0)