Skip to content

Commit 18bc860

Browse files
committed
Fix lists collapsing into thematic breaks
This fixes two cases where lists collapsed into thematic breaks: Thematic break in list item as head: ```markdown - *** ``` Empty list item in two lists: ```markdown - * * ``` Closes GH-6.
1 parent 28ad2f3 commit 18bc860

File tree

5 files changed

+255
-8
lines changed

5 files changed

+255
-8
lines changed

lib/handle/list-item.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import {indentLines} from '../util/indent-lines.js'
1717
*/
1818
export function listItem(node, parent, context) {
1919
const listItemIndent = checkListItemIndent(context)
20-
/** @type {string} */
21-
let bullet = checkBullet(context)
20+
let bullet = context.currentBullet || checkBullet(context)
2221

22+
// Add the marker value for ordered lists.
2323
if (parent && parent.type === 'list' && parent.ordered) {
2424
bullet =
2525
(typeof parent.start === 'number' && parent.start > -1
@@ -28,15 +28,15 @@ export function listItem(node, parent, context) {
2828
(context.options.incrementListMarker === false
2929
? 0
3030
: parent.children.indexOf(node)) +
31-
'.'
31+
bullet
3232
}
3333

3434
let size = bullet.length + 1
3535

3636
if (
3737
listItemIndent === 'tab' ||
3838
(listItemIndent === 'mixed' &&
39-
((parent && 'spread' in parent && parent.spread) || node.spread))
39+
((parent && parent.type === 'list' && parent.spread) || node.spread))
4040
) {
4141
size = Math.ceil(size / 4) * 4
4242
}

lib/handle/list.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,83 @@
44
*/
55

66
import {containerFlow} from '../util/container-flow.js'
7+
import {checkBullet} from '../util/check-bullet.js'
8+
import {checkOtherBullet} from '../util/check-other-bullet.js'
9+
import {checkRule} from '../util/check-rule.js'
710

811
/**
912
* @type {Handle}
1013
* @param {List} node
1114
*/
1215
export function list(node, _, context) {
1316
const exit = context.enter('list')
17+
const currentBullet = context.currentBullet
18+
/** @type {string} */
19+
let bullet = checkBullet(context)
20+
const otherBullet = checkOtherBullet(context)
21+
22+
if (node.ordered) {
23+
bullet = '.'
24+
} else {
25+
const firstListItem = node.children ? node.children[0] : undefined
26+
let useDifferentMarker = false
27+
28+
// If there’s an empty first list item, directly in two list items,
29+
// we have to use a different bullet:
30+
//
31+
// ```markdown
32+
// * - *
33+
// ```
34+
//
35+
// …because otherwise it would become one big thematic break.
36+
if (
37+
firstListItem &&
38+
// Empty list item:
39+
(!firstListItem.children || !firstListItem.children[0]) &&
40+
// Directly in two other list items:
41+
context.stack[context.stack.length - 2] === 'listItem' &&
42+
context.stack[context.stack.length - 4] === 'listItem'
43+
) {
44+
// Note: this is only needed for first children of first children,
45+
// but the code checks for *children*, not *first*.
46+
// So this might generate different bullets where not really needed.
47+
useDifferentMarker = true
48+
}
49+
50+
// If there’s a thematic break at the start of the first list item,
51+
// we have to use a different bullet:
52+
//
53+
// ```markdown
54+
// * ---
55+
// ```
56+
//
57+
// …because otherwise it would become one big thematic break.
58+
if (checkRule(context) === bullet && firstListItem) {
59+
let index = -1
60+
61+
while (++index < node.children.length) {
62+
const item = node.children[index]
63+
if (
64+
item &&
65+
item.type === 'listItem' &&
66+
item.children &&
67+
item.children[0] &&
68+
item.children[0].type === 'thematicBreak'
69+
) {
70+
useDifferentMarker = true
71+
break
72+
}
73+
}
74+
}
75+
76+
if (useDifferentMarker) {
77+
bullet = otherBullet
78+
}
79+
}
80+
81+
context.currentBullet = bullet
1482
const value = containerFlow(node, context)
83+
context.currentBullet = currentBullet
1584
exit()
1685
return value
1786
}

lib/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* @property {Array.<Join>} join
3333
* @property {Handle} handle
3434
* @property {Handlers} handlers
35+
* @property {string|undefined} currentBullet
3536
*/
3637

3738
/**
@@ -71,6 +72,7 @@
7172
/**
7273
* @typedef Options
7374
* @property {'-'|'*'|'+'} [bullet]
75+
* @property {'-'|'*'|'+'} [otherBullet]
7476
* @property {boolean} [closeAtx]
7577
* @property {'_'|'*'} [emphasis]
7678
* @property {'~'|'`'} [fence]

lib/util/check-other-bullet.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @typedef {import('../types.js').Context} Context
3+
* @typedef {import('../types.js').Options} Options
4+
*/
5+
6+
import {checkBullet} from './check-bullet.js'
7+
8+
/**
9+
* @param {Context} context
10+
* @returns {Exclude<Options['bullet'], undefined>}
11+
*/
12+
export function checkOtherBullet(context) {
13+
const bullet = checkBullet(context)
14+
const otherBullet = context.options.otherBullet
15+
16+
if (!otherBullet) {
17+
return bullet === '*' ? '-' : '*'
18+
}
19+
20+
if (otherBullet !== '*' && otherBullet !== '+' && otherBullet !== '-') {
21+
throw new Error(
22+
'Cannot serialize items with `' +
23+
otherBullet +
24+
'` for `options.otherBullet`, expected `*`, `+`, or `-`'
25+
)
26+
}
27+
28+
if (otherBullet === bullet) {
29+
throw new Error(
30+
'Expected `bullet` (`' +
31+
bullet +
32+
'`) and `otherBullet` (`' +
33+
otherBullet +
34+
'`) to be different'
35+
)
36+
}
37+
38+
return otherBullet
39+
}

test/index.js

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @typedef {import('mdast').BlockContent} BlockContent
3+
* @typedef {import('mdast').List} List
4+
*/
5+
16
import test from 'tape'
27
import {removePosition} from 'unist-util-remove-position'
38
import {fromMarkdown as from} from 'mdast-util-from-markdown'
@@ -554,7 +559,7 @@ test('blockquote', (t) => {
554559
}
555560
]
556561
}),
557-
'> a\n> b\n>\n> * c\n> d\n>\n> * ***\n>\n> * e\n> f\n',
562+
'> a\n> b\n>\n> - c\n> d\n>\n> - ***\n>\n> - e\n> f\n',
558563
'should support a list in a block quote'
559564
)
560565

@@ -1931,7 +1936,7 @@ test('list', (t) => {
19311936
}
19321937
]
19331938
}),
1934-
'* a\n\n* ***\n\n* b\n',
1939+
'- a\n\n- ***\n\n- b\n',
19351940
'should support a list w/ items'
19361941
)
19371942

@@ -1952,7 +1957,7 @@ test('list', (t) => {
19521957
}
19531958
]
19541959
}),
1955-
'* a\n* ***\n',
1960+
'- a\n- ***\n',
19561961
'should not use blank lines between items for lists w/ `spread: false`'
19571962
)
19581963

@@ -1974,7 +1979,7 @@ test('list', (t) => {
19741979
}
19751980
]
19761981
}),
1977-
'* a\n\n b\n* ***\n',
1982+
'- a\n\n b\n- ***\n',
19781983
'should support a list w/ `spread: false`, w/ a spread item'
19791984
)
19801985

@@ -2442,6 +2447,122 @@ test('listItem', (t) => {
24422447
'should not use blank lines between child blocks for items w/ `spread: false`'
24432448
)
24442449

2450+
/**
2451+
* @param {BlockContent|BlockContent[]} [d]
2452+
* @returns {List}
2453+
*/
2454+
function createList(d) {
2455+
return {
2456+
type: 'list',
2457+
children: [
2458+
{type: 'listItem', children: Array.isArray(d) ? d : d ? [d] : []}
2459+
]
2460+
}
2461+
}
2462+
2463+
t.equal(
2464+
to(createList(createList(createList())), {otherBullet: '+'}),
2465+
'* * +\n',
2466+
'should support `otherBullet`'
2467+
)
2468+
2469+
t.equal(
2470+
to(createList(createList(createList())), {bullet: '-'}),
2471+
'- - *\n',
2472+
'should default to an `otherBullet` different from `bullet` (1)'
2473+
)
2474+
2475+
t.equal(
2476+
to(createList(createList(createList())), {bullet: '*'}),
2477+
'* * -\n',
2478+
'should default to an `otherBullet` different from `bullet` (2)'
2479+
)
2480+
2481+
t.throws(
2482+
() => {
2483+
// @ts-expect-error: runtime.
2484+
to(createList(createList(createList())), {otherBullet: '?'})
2485+
},
2486+
/Cannot serialize items with `\?` for `options\.otherBullet`, expected/,
2487+
'should throw when given an incorrect `otherBullet`'
2488+
)
2489+
2490+
t.throws(
2491+
() => {
2492+
to(createList(createList(createList())), {bullet: '-', otherBullet: '-'})
2493+
},
2494+
/Expected `bullet` \(`-`\) and `otherBullet` \(`-`\) to be different/,
2495+
'should throw when an `otherBullet` is given equal to `bullet`'
2496+
)
2497+
2498+
t.equal(
2499+
to({
2500+
type: 'list',
2501+
children: [{type: 'listItem', children: [{type: 'thematicBreak'}]}]
2502+
}),
2503+
'- ***\n',
2504+
'should use a different bullet than a thematic rule marker, if the first child of a list item is a thematic break (1)'
2505+
)
2506+
2507+
t.equal(
2508+
to({
2509+
type: 'list',
2510+
children: [
2511+
{
2512+
type: 'listItem',
2513+
children: [
2514+
{type: 'paragraph', children: [{type: 'text', value: 'a'}]}
2515+
]
2516+
},
2517+
{type: 'listItem', children: [{type: 'thematicBreak'}]}
2518+
]
2519+
}),
2520+
'- a\n\n- ***\n',
2521+
'should use a different bullet than a thematic rule marker, if the first child of a list item is a thematic break (2)'
2522+
)
2523+
2524+
t.equal(
2525+
to(createList(createList())),
2526+
'* *\n',
2527+
'should *not* use a different bullet for an empty list item in two lists'
2528+
)
2529+
2530+
t.equal(
2531+
to(createList(createList(createList()))),
2532+
'* * -\n',
2533+
'should use a different bullet for an empty list item in three lists'
2534+
)
2535+
2536+
t.equal(
2537+
to(createList(createList(createList(createList())))),
2538+
'* * * -\n',
2539+
'should use a different bullet for an empty list item in four lists'
2540+
)
2541+
2542+
t.equal(
2543+
to(createList(createList(createList(createList(createList()))))),
2544+
'* * * * -\n',
2545+
'should use a different bullet for an empty list item in five lists'
2546+
)
2547+
2548+
// Note: this case isn’t needed, but there’s no way to check in the code
2549+
// that each list item is the head of its parental list.
2550+
t.equal(
2551+
to(
2552+
createList(
2553+
createList([
2554+
createList({
2555+
type: 'paragraph',
2556+
children: [{type: 'text', value: 'a'}]
2557+
}),
2558+
createList()
2559+
])
2560+
)
2561+
),
2562+
'* * * a\n\n <!---->\n\n -\n',
2563+
'should use a different bullet for an empty list item at non-head in two lists'
2564+
)
2565+
24452566
t.end()
24462567
})
24472568

@@ -3064,5 +3185,21 @@ test('roundtrip', (t) => {
30643185
'should roundtrip a sole blank line in fenced code'
30653186
)
30663187

3188+
doc = '* * -\n'
3189+
3190+
t.equal(
3191+
to(from(doc)),
3192+
doc,
3193+
'should roundtrip an empty list item in two more lists'
3194+
)
3195+
3196+
doc = '- ***\n'
3197+
3198+
t.equal(
3199+
to(from(doc)),
3200+
doc,
3201+
'should roundtrip a thematic break at the start of a list item'
3202+
)
3203+
30673204
t.end()
30683205
})

0 commit comments

Comments
 (0)