Skip to content

Commit 0148aa6

Browse files
committed
Add bulletOther option to use for adjacent lists
Closes GH-18.
1 parent f93dfff commit 0148aa6

11 files changed

+189
-59
lines changed

lib/handle/list-item.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {indentLines} from '../util/indent-lines.js'
1717
*/
1818
export function listItem(node, parent, context) {
1919
const listItemIndent = checkListItemIndent(context)
20-
let bullet = context.currentBullet || checkBullet(context)
20+
let bullet = context.bulletCurrent || checkBullet(context)
2121

2222
// Add the marker value for ordered lists.
2323
if (parent && parent.type === 'list' && parent.ordered) {

lib/handle/list.js

+32-13
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,38 @@
55

66
import {containerFlow} from '../util/container-flow.js'
77
import {checkBullet} from '../util/check-bullet.js'
8-
import {checkOtherBullet} from '../util/check-other-bullet.js'
8+
import {checkBulletOther} from '../util/check-bullet-other.js'
99
import {checkRule} from '../util/check-rule.js'
1010

1111
/**
1212
* @type {Handle}
1313
* @param {List} node
1414
*/
15-
export function list(node, _, context) {
15+
export function list(node, parent, context) {
1616
const exit = context.enter('list')
17-
const currentBullet = context.currentBullet
17+
const bulletCurrent = context.bulletCurrent
1818
/** @type {string} */
1919
let bullet = checkBullet(context)
20-
const otherBullet = checkOtherBullet(context)
20+
/** @type {string} */
21+
const bulletOther = checkBulletOther(context)
22+
const bulletLastUsed = context.bulletLastUsed
2123

2224
if (node.ordered) {
2325
bullet = '.'
2426
} else {
2527
const firstListItem = node.children ? node.children[0] : undefined
2628
let useDifferentMarker = false
2729

28-
// If there’s an empty first list item, directly in two list items,
30+
if (
31+
parent &&
32+
context.options.bulletOther &&
33+
bulletLastUsed &&
34+
bullet === bulletLastUsed
35+
) {
36+
useDifferentMarker = true
37+
}
38+
39+
// If there’s an empty first list item directly in two list items,
2940
// we have to use a different bullet:
3041
//
3142
// ```markdown
@@ -34,16 +45,22 @@ export function list(node, _, context) {
3445
//
3546
// …because otherwise it would become one big thematic break.
3647
if (
48+
// Bullet could be used as a thematic break marker:
49+
(bullet === '*' || bullet === '-') &&
50+
// Empty first list item:
3751
firstListItem &&
38-
// Empty list item:
3952
(!firstListItem.children || !firstListItem.children[0]) &&
4053
// Directly in two other list items:
54+
context.stack[context.stack.length - 1] === 'list' &&
4155
context.stack[context.stack.length - 2] === 'listItem' &&
42-
context.stack[context.stack.length - 4] === 'listItem'
56+
context.stack[context.stack.length - 3] === 'list' &&
57+
context.stack[context.stack.length - 4] === 'listItem' &&
58+
// That are each the first child.
59+
context.indexStack[context.indexStack.length - 1] === 0 &&
60+
context.indexStack[context.indexStack.length - 2] === 0 &&
61+
context.indexStack[context.indexStack.length - 3] === 0 &&
62+
context.indexStack[context.indexStack.length - 4] === 0
4363
) {
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.
4764
useDifferentMarker = true
4865
}
4966

@@ -60,6 +77,7 @@ export function list(node, _, context) {
6077

6178
while (++index < node.children.length) {
6279
const item = node.children[index]
80+
6381
if (
6482
item &&
6583
item.type === 'listItem' &&
@@ -74,13 +92,14 @@ export function list(node, _, context) {
7492
}
7593

7694
if (useDifferentMarker) {
77-
bullet = otherBullet
95+
bullet = bulletOther
7896
}
7997
}
8098

81-
context.currentBullet = bullet
99+
context.bulletCurrent = bullet
82100
const value = containerFlow(node, context)
83-
context.currentBullet = currentBullet
101+
context.bulletLastUsed = bullet
102+
context.bulletCurrent = bulletCurrent
84103
exit()
85104
return value
86105
}

lib/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function toMarkdown(tree, options = {}) {
2727
unsafe: [],
2828
join: [],
2929
handlers: {},
30-
options: {}
30+
options: {},
31+
indexStack: []
3132
}
3233

3334
configure(context, {unsafe, join, handlers: handle})

lib/join.js

+15-9
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ export const join = [joinDefaults]
1010

1111
/** @type {Join} */
1212
function joinDefaults(left, right, parent, context) {
13+
// Indented code after list or another indented code.
1314
if (
14-
// Two lists with the same marker.
15-
(right.type === 'list' &&
16-
right.type === left.type &&
17-
Boolean(left.ordered) === Boolean(right.ordered)) ||
18-
// Indented code after list or another indented code.
19-
(right.type === 'code' &&
20-
formatCodeAsIndented(right, context) &&
21-
(left.type === 'list' ||
22-
(left.type === right.type && formatCodeAsIndented(left, context))))
15+
right.type === 'code' &&
16+
formatCodeAsIndented(right, context) &&
17+
(left.type === 'list' ||
18+
(left.type === right.type && formatCodeAsIndented(left, context)))
19+
) {
20+
return false
21+
}
22+
23+
// Two lists with the same marker.
24+
if (
25+
left.type === 'list' &&
26+
left.type === right.type &&
27+
((left.ordered && right.ordered) ||
28+
(!left.ordered && !right.ordered && !context.options.bulletOther))
2329
) {
2430
return false
2531
}

lib/types.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,20 @@
2525

2626
/**
2727
* @typedef Context
28-
* @property {Array.<string>} stack
28+
* @property {string[]} stack
29+
* Stack of labels.
30+
* @property {number[]} indexStack
31+
* Positions of children in their parents.
2932
* @property {Enter} enter
3033
* @property {Options} options
3134
* @property {Array.<Unsafe>} unsafe
3235
* @property {Array.<Join>} join
3336
* @property {Handle} handle
3437
* @property {Handlers} handlers
35-
* @property {string|undefined} currentBullet
38+
* @property {string|undefined} bulletCurrent
39+
* The marker used by the current list.
40+
* @property {string|undefined} bulletLastUsed
41+
* The marker used by the previous list.
3642
*/
3743

3844
/**
@@ -72,7 +78,7 @@
7278
/**
7379
* @typedef Options
7480
* @property {'-'|'*'|'+'} [bullet]
75-
* @property {'-'|'*'|'+'} [otherBullet]
81+
* @property {'-'|'*'|'+'} [bulletOther]
7682
* @property {boolean} [closeAtx]
7783
* @property {'_'|'*'} [emphasis]
7884
* @property {'~'|'`'} [fence]

lib/util/check-other-bullet.js renamed to lib/util/check-bullet-other.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,31 @@ import {checkBullet} from './check-bullet.js'
99
* @param {Context} context
1010
* @returns {Exclude<Options['bullet'], undefined>}
1111
*/
12-
export function checkOtherBullet(context) {
12+
export function checkBulletOther(context) {
1313
const bullet = checkBullet(context)
14-
const otherBullet = context.options.otherBullet
14+
const bulletOther = context.options.bulletOther
1515

16-
if (!otherBullet) {
16+
if (!bulletOther) {
1717
return bullet === '*' ? '-' : '*'
1818
}
1919

20-
if (otherBullet !== '*' && otherBullet !== '+' && otherBullet !== '-') {
20+
if (bulletOther !== '*' && bulletOther !== '+' && bulletOther !== '-') {
2121
throw new Error(
2222
'Cannot serialize items with `' +
23-
otherBullet +
24-
'` for `options.otherBullet`, expected `*`, `+`, or `-`'
23+
bulletOther +
24+
'` for `options.bulletOther`, expected `*`, `+`, or `-`'
2525
)
2626
}
2727

28-
if (otherBullet === bullet) {
28+
if (bulletOther === bullet) {
2929
throw new Error(
3030
'Expected `bullet` (`' +
3131
bullet +
32-
'`) and `otherBullet` (`' +
33-
otherBullet +
32+
'`) and `bulletOther` (`' +
33+
bulletOther +
3434
'`) to be different'
3535
)
3636
}
3737

38-
return otherBullet
38+
return bulletOther
3939
}

lib/util/container-flow.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,34 @@
1111
* @returns {string}
1212
*/
1313
export function containerFlow(parent, context) {
14+
const indexStack = context.indexStack
1415
const children = parent.children || []
1516
/** @type {Array.<string>} */
1617
const results = []
1718
let index = -1
1819

20+
indexStack.push(-1)
21+
1922
while (++index < children.length) {
2023
const child = children[index]
2124

25+
indexStack[indexStack.length - 1] = index
26+
2227
results.push(
2328
context.handle(child, parent, context, {before: '\n', after: '\n'})
2429
)
2530

31+
if (child.type !== 'list') {
32+
context.bulletLastUsed = undefined
33+
}
34+
2635
if (index < children.length - 1) {
2736
results.push(between(child, children[index + 1]))
2837
}
2938
}
3039

40+
indexStack.pop()
41+
3142
return results.join('')
3243

3344
/**
@@ -37,11 +48,9 @@ export function containerFlow(parent, context) {
3748
*/
3849
function between(left, right) {
3950
let index = context.join.length
40-
/** @type {ReturnType<Join>} */
41-
let result
4251

4352
while (index--) {
44-
result = context.join[index](left, right, parent, context)
53+
const result = context.join[index](left, right, parent, context)
4554

4655
if (result === true || result === 1) {
4756
break

lib/util/container-phrasing.js

+7
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,22 @@
1212
* @returns {string}
1313
*/
1414
export function containerPhrasing(parent, context, safeOptions) {
15+
const indexStack = context.indexStack
1516
const children = parent.children || []
1617
/** @type {Array.<string>} */
1718
const results = []
1819
let index = -1
1920
let before = safeOptions.before
2021

22+
indexStack.push(-1)
23+
2124
while (++index < children.length) {
2225
const child = children[index]
2326
/** @type {string} */
2427
let after
2528

29+
indexStack[indexStack.length - 1] = index
30+
2631
if (index + 1 < children.length) {
2732
// @ts-expect-error: hush, it’s actually a `zwitch`.
2833
let handle = context.handle.handlers[children[index + 1].type]
@@ -60,5 +65,7 @@ export function containerPhrasing(parent, context, safeOptions) {
6065
before = results[results.length - 1].slice(-1)
6166
}
6267

68+
indexStack.pop()
69+
6370
return results.join('')
6471
}

lib/util/format-heading-as-setext.js

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export function formatHeadingAsSetext(node, context) {
1717
// Look for literals with a line break.
1818
// Note that this also
1919
visit(node, (node) => {
20-
console.log('n:', node.type)
2120
if (
2221
('value' in node && /\r?\n|\r/.test(node.value)) ||
2322
node.type === 'break'

readme.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,35 @@ Serialize **[mdast][]** to markdown.
8383

8484
###### `options.bullet`
8585

86-
Marker to use to for bullets of items in unordered lists (`'*'`, `'+'`, or `'-'`,
86+
Marker to use for bullets of items in unordered lists (`'*'`, `'+'`, or `'-'`,
8787
default: `'*'`).
8888

89+
###### `options.bulletOther`
90+
91+
Marker to use in certain cases where the primary bullet doesn’t work (`'*'`,
92+
`'+'`, or `'-'`, default: depends).
93+
94+
There are three cases where the primary bullet can’t be used:
95+
96+
* When three list items are on their own, the last one is empty, and `bullet`
97+
is also a valid `rule`: `* - +`.
98+
This would turn into a thematic break if serialized with three primary
99+
bullets.
100+
As this is an edge case unlikely to appear in normal markdown, the last list
101+
item will be given a different bullet.
102+
* When a thematic break is the first child of one of the list items, and
103+
`bullet` is the same character as `rule`: `- ***`.
104+
This would turn into a single thematic break if serialized with primary
105+
bullets.
106+
As this is an edge case unlikely to appear in normal markdown this markup is
107+
always fixed, even if `bulletOther` is not passed
108+
* When two unordered lists appear next to each other: `* a\n- b`.
109+
CommonMark sees different bullets as different lists, but several markdown
110+
parsers parse it as one list.
111+
To solve for both, we instead inject an empty comment between the two lists:
112+
`* a\n<!---->\n* b`, but if `bulletOther` is given explicitly, it will be
113+
used instead
114+
89115
###### `options.closeAtx`
90116

91117
Whether to add the same number of number signs (`#`) at the end of an ATX

0 commit comments

Comments
 (0)