@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/index.js +28 -43
- package/package.json +2 -2
- package/src/phase0-description-list.js +26 -11
- package/src/phase1-analyze.js +39 -8
- package/src/phase2-convert.js +62 -69
- package/src/phase3-attributes.js +5 -37
- package/src/phase5-spans.js +54 -48
- package/src/preprocess-literal-lists.js +190 -275
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Markdown's default ordered list markers are limited. This plugin extends Markdown's unordered lists so they can use many kinds of ordered markers.
|
|
4
4
|
|
|
5
|
-
Also, this plugin option provides a description-list conversion. Unordered list items whose first line is wrapped in `**` and followed by two spaces are converted into `<dl>` elements.
|
|
5
|
+
Also, this plugin option provides a description-list conversion. Unordered list items whose first line is wrapped in `**` and followed by a description separator (two spaces, `\\`, or a blank line) are converted into `<dl>` elements.
|
|
6
6
|
|
|
7
7
|
## Ordered lists conversion behavior
|
|
8
8
|
|
|
@@ -72,6 +72,10 @@ After the marker separator, an ASCII space is normally expected. For `fullwidth`
|
|
|
72
72
|
- Marker type detection is deterministic: gather all markers at a nesting level, match them against the canonical definitions, keep the type that explains the most items while preserving numeric continuity, and use the order in `listTypes.json` only as a final tiebreaker.
|
|
73
73
|
- Flattening: A pattern like `- 1.` is represented by the default `ul > li > ol` nesting structure in markdown-it, but this plugin simplifies it to a single `ol` by default to match the representation of other markers.
|
|
74
74
|
|
|
75
|
+
### Source map behavior
|
|
76
|
+
|
|
77
|
+
The flattener uses `token.map` (markdown-it source line numbers) to preserve loose list output for `- 1.` style lists. markdown-it normally assigns `map` to block tokens, but it can be missing if tokens are constructed manually, cloned without `map`, or stripped by upstream plugins to save memory. In that case, the plugin falls back to markdown-it's `paragraph.hidden` flags and `- 1.` lists with blank lines may render as tight lists (no `<p>` wrappers). Avoid stripping `map` data or set `unremoveUlNest: true` if exact loose rendering is required.
|
|
78
|
+
|
|
75
79
|
Note: For custom marker lists (those rendered with `role="list"`) the plugin assumes you will hide the native marker via CSS (for example `ol[role="list"] { list-style: none; }`). The `hasListStyleNone` option can be enabled to add `style="list-style: none;"` directly to generated `<ol>` elements.
|
|
76
80
|
|
|
77
81
|
### Behavior customization
|
|
@@ -85,17 +89,19 @@ You can customize the conversion using options.
|
|
|
85
89
|
- `hasListStyleNone` (boolean) — When the plugin emits `role="list"`, also add `style="list-style: none;"` to the `<ol>`.
|
|
86
90
|
- `omitMarkerMetadata` (boolean) — If `true`, omit the `data-marker-prefix` / `data-marker-suffix` attributes.
|
|
87
91
|
- `addMarkerStyleToClass` (boolean) — When `true`, append suffix-style information to the generated class name (e.g. `ol-decimal-with-round-round`). When `false` (default) the class stays as `ol-decimal`.
|
|
92
|
+
- `enableLiteralNumberingFix` (boolean) — Enable literal nested list recovery (for example, nested lists starting with 2 or greater). This is opt-in; it only applies inside list items, evaluates indentation relative to the parent list marker (marker width + 0–3 spaces), and does not convert code blocks (indent >= marker width + 4).
|
|
88
93
|
|
|
89
94
|
## Description lists conversion behavior
|
|
90
95
|
|
|
91
96
|
When the `descriptionList` option is enabled the plugin converts specially formatted bullet lists into HTML description lists (`<dl>`).
|
|
92
97
|
|
|
93
98
|
- Each list item must start with a `**Term**` line.
|
|
94
|
-
- If the
|
|
99
|
+
- If the description continues on the next line without a blank line, the Term line must end with two ASCII spaces (a Markdown line-break) or a backslash `\`. Inline `{.attrs}` immediately after the term are allowed.
|
|
100
|
+
- If the Term line is followed by a blank line, the description can start in the next paragraph or list (line-break control characters are optional).
|
|
95
101
|
|
|
96
102
|
In the conversion the `**Term**` line becomes a `<dt>` and the subsequent lines become the corresponding `<dd>`.
|
|
97
103
|
|
|
98
|
-
Note:
|
|
104
|
+
Note: Text descriptions are wrapped in `<p>` elements; additional paragraphs and lists keep markdown-it's block structure.
|
|
99
105
|
|
|
100
106
|
### Description list options
|
|
101
107
|
|
package/index.js
CHANGED
|
@@ -21,13 +21,30 @@ const mditNumberingUl = (md, option) => {
|
|
|
21
21
|
omitMarkerMetadata: false, // true=omit data-marker-prefix/suffix attributes
|
|
22
22
|
useCounterStyle: false, // true=users will use @counter-style; suppress marker spans and role attr
|
|
23
23
|
addMarkerStyleToClass: false, // true=append -with-* marker style suffix to class names
|
|
24
|
+
enableLiteralNumberingFix: false, // true=normalize nested lists that don't start at 1 (opt-in)
|
|
24
25
|
|
|
25
26
|
// Override with user options
|
|
26
27
|
...option
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
|
|
30
|
+
const addRuleAfter = (ruler, afterName, ruleName, fn) => {
|
|
31
|
+
try {
|
|
32
|
+
ruler.after(afterName, ruleName, fn)
|
|
33
|
+
} catch {
|
|
34
|
+
ruler.push(ruleName, fn)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dlProcessor = (state) => {
|
|
39
|
+
if (!state.env) {
|
|
40
|
+
state.env = {}
|
|
41
|
+
}
|
|
42
|
+
if (!opt.descriptionList && !opt.descriptionListWithDiv) {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
processDescriptionList(state.tokens, opt)
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
31
48
|
|
|
32
49
|
const listProcessor = (state) => {
|
|
33
50
|
// Initialize state.env
|
|
@@ -36,14 +53,9 @@ const mditNumberingUl = (md, option) => {
|
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
const tokens = state.tokens
|
|
39
|
-
|
|
40
|
-
// ===== PHASE 0: Description List =====
|
|
41
|
-
// Convert **Term**: pattern from paragraph to bullet_list, then to dl/dt/dd
|
|
42
|
-
// Must run before Phase 1 (parsed as bullet_list)
|
|
43
|
-
processDescriptionList(tokens, opt)
|
|
44
56
|
|
|
45
57
|
// Normalize literal nested ordered lists (markdown-it only creates nested lists when they start at 1)
|
|
46
|
-
normalizeLiteralOrderedLists(tokens)
|
|
58
|
+
normalizeLiteralOrderedLists(tokens, opt)
|
|
47
59
|
|
|
48
60
|
// ===== PHASE 1: List Structure Analysis =====
|
|
49
61
|
// Analyze marker detection and structure without token conversion
|
|
@@ -56,9 +68,8 @@ const mditNumberingUl = (md, option) => {
|
|
|
56
68
|
|
|
57
69
|
// ===== PHASE 3: Add Attributes =====
|
|
58
70
|
// Add type, class, data-* attributes to converted lists
|
|
59
|
-
// Use
|
|
60
|
-
|
|
61
|
-
addAttributes(tokens, listInfos, opt)
|
|
71
|
+
// Use markerInfo stored on list tokens (safe after Phase2 mutations)
|
|
72
|
+
addAttributes(tokens, opt)
|
|
62
73
|
|
|
63
74
|
// ===== PHASE 4: HTML Block Processing =====
|
|
64
75
|
// Remove indents from HTML blocks in lists and normalize line breaks
|
|
@@ -66,12 +77,13 @@ const mditNumberingUl = (md, option) => {
|
|
|
66
77
|
|
|
67
78
|
// ===== PHASE 5: Span Generation =====
|
|
68
79
|
// Generate marker spans in alwaysMarkerSpan mode
|
|
69
|
-
generateSpans(tokens,
|
|
80
|
+
generateSpans(tokens, opt)
|
|
70
81
|
|
|
71
82
|
return true
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
md.core.ruler.before('inline', '
|
|
85
|
+
md.core.ruler.before('inline', 'numbering_dl_parser', dlProcessor)
|
|
86
|
+
md.core.ruler.after('numbering_dl_parser', 'numbering_ul_phases', listProcessor)
|
|
75
87
|
|
|
76
88
|
if (!opt.unremoveUlNest) {
|
|
77
89
|
// Move nested list attributes only when flattening is enabled
|
|
@@ -80,11 +92,7 @@ const mditNumberingUl = (md, option) => {
|
|
|
80
92
|
return true
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
md.core.ruler.after('curly_attributes', 'numbering_ul_nested_attrs', nestedListAttrProcessor)
|
|
85
|
-
} else {
|
|
86
|
-
md.core.ruler.push('numbering_ul_nested_attrs', nestedListAttrProcessor)
|
|
87
|
-
}
|
|
95
|
+
addRuleAfter(md.core.ruler, 'curly_attributes', 'numbering_ul_nested_attrs', nestedListAttrProcessor)
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
// Description list: Move paragraph attributes to dl and add custom renderers
|
|
@@ -95,30 +103,7 @@ const mditNumberingUl = (md, option) => {
|
|
|
95
103
|
return true
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
md.core.ruler.after('curly_attributes', 'numbering_dl_attrs', dlAttrProcessor)
|
|
100
|
-
} else {
|
|
101
|
-
md.core.ruler.push('numbering_dl_attrs', dlAttrProcessor)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Add custom renderers for description list tokens
|
|
105
|
-
// Helper function to render attributes
|
|
106
|
-
const renderAttrs = (token) => {
|
|
107
|
-
if (!token.attrs || token.attrs.length === 0) return ''
|
|
108
|
-
return ' ' + token.attrs.map(([key, value]) => `${key}="${value}"`).join(' ')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
md.renderer.rules.dl_open = (tokens, idx) => `<dl${renderAttrs(tokens[idx])}>\n`
|
|
112
|
-
md.renderer.rules.dl_close = () => '</dl>\n'
|
|
113
|
-
md.renderer.rules.dt_open = (tokens, idx) => `<dt${renderAttrs(tokens[idx])}>`
|
|
114
|
-
md.renderer.rules.dt_close = () => '</dt>\n'
|
|
115
|
-
md.renderer.rules.dd_open = (tokens, idx) => `<dd${renderAttrs(tokens[idx])}>\n`
|
|
116
|
-
md.renderer.rules.dd_close = () => '</dd>\n'
|
|
117
|
-
|
|
118
|
-
if (opt.descriptionListWithDiv) {
|
|
119
|
-
md.renderer.rules.div_open = (tokens, idx) => `<div${renderAttrs(tokens[idx])}>\n`
|
|
120
|
-
md.renderer.rules.div_close = () => '</div>\n'
|
|
121
|
-
}
|
|
106
|
+
addRuleAfter(md.core.ruler, 'curly_attributes', 'numbering_dl_attrs', dlAttrProcessor)
|
|
122
107
|
}
|
|
123
108
|
}
|
|
124
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-numbering-ul-regarded-as-ol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "This markdown-it plugin regard ul element with numbering lists as ol element.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/peaceroad/p7d-markdown-it-numbering-ul-regarded-as-ol#readme",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@peaceroad/markdown-it-strong-ja": "^0.
|
|
30
|
+
"@peaceroad/markdown-it-strong-ja": "^0.6.2",
|
|
31
31
|
"markdown-it": "^14.1.0",
|
|
32
32
|
"markdown-it-attrs": "^4.3.1",
|
|
33
33
|
"markdown-it-deflist": "^3.0.0"
|
|
@@ -36,6 +36,13 @@ const parseAttrString = (attrStr) => {
|
|
|
36
36
|
return attrs
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const copyMap = (target, source) => {
|
|
40
|
+
if (!target || !source || !Array.isArray(source.map)) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
target.map = source.map.slice()
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
/**
|
|
40
47
|
* Process description list patterns in tokens
|
|
41
48
|
* @param {Array} tokens - Token array
|
|
@@ -150,11 +157,10 @@ const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
|
|
|
150
157
|
const afterStrong = dlCheck.afterStrong
|
|
151
158
|
|
|
152
159
|
// Pattern 1: **Term** description (2+ spaces, including newlines)
|
|
153
|
-
// Pattern 2: **Term
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const cleaned = afterStrong.replace(/^[\s:]+/, '').replace(/^\\/, '').trim()
|
|
160
|
+
// Pattern 2: **Term**\ description (backslash escape)
|
|
161
|
+
if (/^\s{2,}/.test(afterStrong) || /^\\/.test(afterStrong)) {
|
|
162
|
+
// Remove leading space/backslash and check remaining text
|
|
163
|
+
const cleaned = afterStrong.replace(/^\s+/, '').replace(/^\\/, '').trim()
|
|
158
164
|
if (cleaned) {
|
|
159
165
|
hasDescription = true
|
|
160
166
|
}
|
|
@@ -212,12 +218,10 @@ const isDLPattern = (content) => {
|
|
|
212
218
|
const afterStrong = match[2] // Text after closing **
|
|
213
219
|
|
|
214
220
|
// Pattern 1: **Term** description (2+ spaces, including newlines)
|
|
215
|
-
// Pattern 2: **Term
|
|
216
|
-
// Pattern 3: **Term**\ description (backslash escape)
|
|
221
|
+
// Pattern 2: **Term**\ description (backslash escape)
|
|
217
222
|
// Pattern 4: **Term** only (no content after)
|
|
218
223
|
// Pattern 5: **Term** {.attrs} (markdown-it-attrs syntax, optionally with content after)
|
|
219
224
|
const isMatch = /^\s{2,}/.test(afterStrong) ||
|
|
220
|
-
/^\s*:/.test(afterStrong) ||
|
|
221
225
|
/^\\/.test(afterStrong) ||
|
|
222
226
|
/^\s*$/.test(afterStrong) ||
|
|
223
227
|
/^\s*\{[^}]+\}/.test(afterStrong) // {.class} or {#id} etc
|
|
@@ -236,6 +240,7 @@ const convertBulletListToDL = (tokens, listStart, listEnd, opt, itemRanges = nul
|
|
|
236
240
|
const dlOpen = new tokens[listStart].constructor('dl_open', 'dl', 1)
|
|
237
241
|
dlOpen.level = listLevel
|
|
238
242
|
dlOpen.block = true
|
|
243
|
+
copyMap(dlOpen, tokens[listStart])
|
|
239
244
|
|
|
240
245
|
// Copy attributes from bullet_list_open (e.g., {.class} from markdown-it-attrs)
|
|
241
246
|
if (tokens[listStart].attrs && tokens[listStart].attrs.length > 0) {
|
|
@@ -295,6 +300,7 @@ const convertBulletListToDL = (tokens, listStart, listEnd, opt, itemRanges = nul
|
|
|
295
300
|
const dlClose = new tokens[listStart].constructor('dl_close', 'dl', -1)
|
|
296
301
|
dlClose.level = listLevel
|
|
297
302
|
dlClose.block = true
|
|
303
|
+
copyMap(dlClose, tokens[listEnd])
|
|
298
304
|
newTokens.push(dlClose)
|
|
299
305
|
|
|
300
306
|
// Replace tokens
|
|
@@ -335,7 +341,8 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
335
341
|
const match = content.match(/^\*\*(.*?)\*\*(.*)/s) // s flag for including newlines
|
|
336
342
|
|
|
337
343
|
if (match) {
|
|
338
|
-
|
|
344
|
+
// Trim to avoid leading space when **Term** starts with whitespace (e.g. "** *term*")
|
|
345
|
+
term = match[1].trim()
|
|
339
346
|
let afterStrong = match[2]
|
|
340
347
|
|
|
341
348
|
// Extract {.attrs} from afterStrong if present (markdown-it-attrs hasn't processed yet)
|
|
@@ -370,8 +377,8 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
370
377
|
afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
|
|
371
378
|
}
|
|
372
379
|
|
|
373
|
-
// Clean up afterStrong: remove leading spaces/
|
|
374
|
-
let cleaned = afterStrong.replace(
|
|
380
|
+
// Clean up afterStrong: remove leading spaces/backslash, then trim each line
|
|
381
|
+
let cleaned = afterStrong.replace(/^\s+/, '').replace(/^\\/, '')
|
|
375
382
|
|
|
376
383
|
// Remove leading whitespace from each line (remove list indent)
|
|
377
384
|
cleaned = cleaned.split('\n').map(line => line.replace(/^\s+/, '')).join('\n').trim()
|
|
@@ -386,6 +393,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
386
393
|
const divOpen = new tokens[firstPara].constructor('div_open', 'div', 1)
|
|
387
394
|
divOpen.level = parentLevel + 1
|
|
388
395
|
divOpen.block = true
|
|
396
|
+
copyMap(divOpen, tokens[firstPara])
|
|
389
397
|
const divClass = typeof opt.descriptionListDivClass === 'string' ? opt.descriptionListDivClass : ''
|
|
390
398
|
if (divClass) {
|
|
391
399
|
divOpen.attrs = [['class', divClass]]
|
|
@@ -397,6 +405,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
397
405
|
const dtOpen = new tokens[firstPara].constructor('dt_open', 'dt', 1)
|
|
398
406
|
dtOpen.level = parentLevel + 1
|
|
399
407
|
dtOpen.block = true
|
|
408
|
+
copyMap(dtOpen, tokens[firstPara])
|
|
400
409
|
// Add collected attributes to dt_open
|
|
401
410
|
if (dtAttrs && dtAttrs.length > 0) {
|
|
402
411
|
dtOpen.attrs = dtAttrs
|
|
@@ -414,12 +423,14 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
414
423
|
const dtClose = new tokens[firstPara].constructor('dt_close', 'dt', -1)
|
|
415
424
|
dtClose.level = parentLevel + 1
|
|
416
425
|
dtClose.block = true
|
|
426
|
+
copyMap(dtClose, tokens[firstPara])
|
|
417
427
|
result.push(dtClose)
|
|
418
428
|
|
|
419
429
|
// Create dd_open token
|
|
420
430
|
const ddOpen = new tokens[firstPara].constructor('dd_open', 'dd', 1)
|
|
421
431
|
ddOpen.level = parentLevel + 1
|
|
422
432
|
ddOpen.block = true
|
|
433
|
+
copyMap(ddOpen, tokens[firstPara])
|
|
423
434
|
result.push(ddOpen)
|
|
424
435
|
|
|
425
436
|
// First paragraph in dd (if description exists)
|
|
@@ -428,6 +439,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
428
439
|
const pOpen = new tokens[firstPara].constructor('paragraph_open', 'p', 1)
|
|
429
440
|
pOpen.level = parentLevel + 2
|
|
430
441
|
pOpen.block = true // IMPORTANT: Enable block mode for proper newline rendering
|
|
442
|
+
copyMap(pOpen, tokens[firstPara])
|
|
431
443
|
result.push(pOpen)
|
|
432
444
|
|
|
433
445
|
const pInline = new tokens[firstPara].constructor('inline', '', 0)
|
|
@@ -442,6 +454,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
442
454
|
const pClose = new tokens[firstPara].constructor('paragraph_close', 'p', -1)
|
|
443
455
|
pClose.level = parentLevel + 2
|
|
444
456
|
pClose.block = true // IMPORTANT: Enable block mode for proper newline rendering
|
|
457
|
+
copyMap(pClose, tokens[firstPara])
|
|
445
458
|
result.push(pClose)
|
|
446
459
|
|
|
447
460
|
hasFirstParagraph = true
|
|
@@ -491,6 +504,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
491
504
|
const ddClose = new tokens[firstPara].constructor('dd_close', 'dd', -1)
|
|
492
505
|
ddClose.level = parentLevel + 1
|
|
493
506
|
ddClose.block = true
|
|
507
|
+
copyMap(ddClose, tokens[firstPara])
|
|
494
508
|
result.push(ddClose)
|
|
495
509
|
|
|
496
510
|
// Create div_close if descriptionListWithDiv is enabled
|
|
@@ -498,6 +512,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
498
512
|
const divClose = new tokens[firstPara].constructor('div_close', 'div', -1)
|
|
499
513
|
divClose.level = parentLevel + 1
|
|
500
514
|
divClose.block = true
|
|
515
|
+
copyMap(divClose, tokens[firstPara])
|
|
501
516
|
result.push(divClose)
|
|
502
517
|
}
|
|
503
518
|
|
package/src/phase1-analyze.js
CHANGED
|
@@ -154,6 +154,9 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
|
|
|
154
154
|
if (!isLoose) {
|
|
155
155
|
hideFirstParagraphsForTightList(tokens, items, level)
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
// Cache loose/tight state for later phases (mapless flattening fallback).
|
|
159
|
+
listToken._isLoose = isLoose
|
|
157
160
|
|
|
158
161
|
// Extract marker info (for both bullet_list and ordered_list)
|
|
159
162
|
const markerInfo = extractMarkerInfo(tokens, startIndex, endIndex, opt)
|
|
@@ -168,6 +171,13 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
|
|
|
168
171
|
// Convert even loose lists if markers are consistent
|
|
169
172
|
const shouldConvert = originalType === 'bullet_list_open' &&
|
|
170
173
|
shouldConvertToOrdered(originalType, markerInfo, opt)
|
|
174
|
+
|
|
175
|
+
if (markerInfo) {
|
|
176
|
+
listToken._markerInfo = markerInfo
|
|
177
|
+
}
|
|
178
|
+
if (originalType === 'bullet_list_open') {
|
|
179
|
+
listToken._shouldConvert = shouldConvert
|
|
180
|
+
}
|
|
171
181
|
|
|
172
182
|
return {
|
|
173
183
|
startIndex,
|
|
@@ -259,6 +269,10 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
|
|
|
259
269
|
markerInfo = detectMarkerType(lastInlineContent)
|
|
260
270
|
}
|
|
261
271
|
content = lastInlineContent
|
|
272
|
+
|
|
273
|
+
if (tokens[startIndex]) {
|
|
274
|
+
tokens[startIndex]._firstParagraphIsLoose = firstParagraphIsLoose
|
|
275
|
+
}
|
|
262
276
|
|
|
263
277
|
return {
|
|
264
278
|
startIndex,
|
|
@@ -277,9 +291,15 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
|
|
|
277
291
|
function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
|
|
278
292
|
const listToken = tokens[startIndex]
|
|
279
293
|
const markers = []
|
|
294
|
+
const literalMarkerInfo = listToken._literalMarkerInfo || null
|
|
280
295
|
|
|
281
296
|
// For ordered_list, get numbers from list_item_open's info
|
|
282
297
|
if (listToken.type === 'ordered_list_open') {
|
|
298
|
+
const literalMarkers = Array.isArray(literalMarkerInfo?.markers) ? literalMarkerInfo.markers : null
|
|
299
|
+
const literalType = literalMarkerInfo?.type || 'decimal'
|
|
300
|
+
const literalPrefix = literalMarkers?.[0]?.prefix ?? ''
|
|
301
|
+
const literalSuffix = literalMarkers?.[0]?.suffix ?? (listToken.markup || '.')
|
|
302
|
+
let listItemIndex = 0
|
|
283
303
|
for (let i = startIndex + 1; i < endIndex; i++) {
|
|
284
304
|
const token = tokens[i]
|
|
285
305
|
|
|
@@ -287,14 +307,25 @@ function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
|
|
|
287
307
|
// Get number from info
|
|
288
308
|
const itemNumber = token.info ? parseInt(token.info, 10) : markers.length + 1
|
|
289
309
|
const markup = token.markup || listToken.markup || '.'
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
number
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
310
|
+
if (literalMarkers && literalMarkers[listItemIndex]) {
|
|
311
|
+
const marker = { ...literalMarkers[listItemIndex] }
|
|
312
|
+
if (typeof marker.originalNumber !== 'number') {
|
|
313
|
+
marker.originalNumber = itemNumber
|
|
314
|
+
}
|
|
315
|
+
if (typeof marker.number !== 'number') {
|
|
316
|
+
marker.number = itemNumber
|
|
317
|
+
}
|
|
318
|
+
markers.push(marker)
|
|
319
|
+
} else {
|
|
320
|
+
markers.push({
|
|
321
|
+
number: itemNumber,
|
|
322
|
+
originalNumber: itemNumber,
|
|
323
|
+
prefix: literalPrefix,
|
|
324
|
+
suffix: literalSuffix || markup,
|
|
325
|
+
type: literalType
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
listItemIndex++
|
|
298
329
|
}
|
|
299
330
|
}
|
|
300
331
|
} else {
|
package/src/phase2-convert.js
CHANGED
|
@@ -23,8 +23,7 @@ export function convertLists(tokens, listInfos, opt) {
|
|
|
23
23
|
if (!opt.unremoveUlNest) {
|
|
24
24
|
const hasBulletLists = listInfos.some(info => info.originalType === 'bullet_list_open')
|
|
25
25
|
if (hasBulletLists) {
|
|
26
|
-
|
|
27
|
-
simplifyNestedBulletLists(tokens, opt, listInfoMap)
|
|
26
|
+
simplifyNestedBulletLists(tokens)
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -143,11 +142,11 @@ const LEADING_SPACE_REGEX = /^\s+/
|
|
|
143
142
|
* When the middle list_item is empty (contains only the inner list),
|
|
144
143
|
* remove the outer ul and the intermediate li.
|
|
145
144
|
* @param {Array} tokens - Token array
|
|
146
|
-
* @param {Object} opt - Options
|
|
147
|
-
* @param {Map} listInfoMap - Map of listInfo keyed by startIndex
|
|
148
145
|
*/
|
|
149
|
-
function simplifyNestedBulletLists(tokens
|
|
146
|
+
function simplifyNestedBulletLists(tokens) {
|
|
150
147
|
let modified = true
|
|
148
|
+
// token.map is stable across passes; used to decide map-based blank-line checks.
|
|
149
|
+
const hasTokenMaps = tokens.some(token => token && token.map && token.map.length)
|
|
151
150
|
|
|
152
151
|
// ===== Phase 1: Simplify ul>li>ol structure (multiple passes needed) =====
|
|
153
152
|
while (modified) {
|
|
@@ -228,7 +227,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
228
227
|
const hasExtraContent = beforeContent > 0 || afterContent > 0
|
|
229
228
|
const literalNumber = extractFirstListItemNumber(tokens, innerListOpen, innerListCloseIdx)
|
|
230
229
|
|
|
231
|
-
const
|
|
230
|
+
const innerListMarkerInfo = tokens[innerListOpen]?._markerInfo
|
|
232
231
|
const isSimpleMarkerParagraph = (() => {
|
|
233
232
|
const precedingCount = innerListOpen - (idx + 1)
|
|
234
233
|
if (precedingCount !== 3) return false
|
|
@@ -249,7 +248,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
249
248
|
hasExtraContent: hasExtraContent,
|
|
250
249
|
extraContentStart: innerListCloseIdx + 1,
|
|
251
250
|
extraContentEnd: itemCloseIdx,
|
|
252
|
-
|
|
251
|
+
innerListMarkerInfo,
|
|
253
252
|
literalNumber,
|
|
254
253
|
flattenFirstParagraph: isSimpleMarkerParagraph
|
|
255
254
|
})
|
|
@@ -262,9 +261,11 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
262
261
|
}
|
|
263
262
|
|
|
264
263
|
// Check if outer ul is convertible (has markers)
|
|
265
|
-
const
|
|
264
|
+
const outerListToken = tokens[i]
|
|
265
|
+
const outerShouldConvert = !!outerListToken?._shouldConvert
|
|
266
|
+
const outerMarkerInfo = outerListToken?._markerInfo || null
|
|
266
267
|
|
|
267
|
-
if (
|
|
268
|
+
if (outerShouldConvert) {
|
|
268
269
|
for (const flattenedItem of itemIndices) {
|
|
269
270
|
const closeIdx = flattenedItem.innerListOpen - 1
|
|
270
271
|
const inlineIdx = closeIdx - 1
|
|
@@ -309,8 +310,8 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
309
310
|
const allMarkers = []
|
|
310
311
|
|
|
311
312
|
// Use outer listInfo marker info if available
|
|
312
|
-
if (
|
|
313
|
-
allMarkers.push(...
|
|
313
|
+
if (outerMarkerInfo && outerMarkerInfo.markers) {
|
|
314
|
+
allMarkers.push(...outerMarkerInfo.markers)
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
for (const item of itemIndices) {
|
|
@@ -325,7 +326,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
325
326
|
allMarkers.push(...innerListToken._markerInfo.markers)
|
|
326
327
|
}
|
|
327
328
|
// If originally ordered_list, get marker info from start attribute and markup
|
|
328
|
-
else if (!
|
|
329
|
+
else if (!outerMarkerInfo) {
|
|
329
330
|
// Only get from inner if outer has no marker info
|
|
330
331
|
// Get start attribute and markup
|
|
331
332
|
const startAttr = innerListToken.attrs?.find(attr => attr[0] === 'start')
|
|
@@ -371,7 +372,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
371
372
|
|
|
372
373
|
// If outer ul is convertible, convert inner list to ordered_list
|
|
373
374
|
// (Don't limit to bullet_list as inner list might already be ordered_list)
|
|
374
|
-
if (
|
|
375
|
+
if (outerShouldConvert) {
|
|
375
376
|
if (firstListToken.type === 'bullet_list_open') {
|
|
376
377
|
firstListToken.type = 'ordered_list_open'
|
|
377
378
|
firstListToken.tag = 'ol'
|
|
@@ -430,7 +431,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
430
431
|
// Single item is always tight
|
|
431
432
|
if (itemIndices.length === 1) {
|
|
432
433
|
outerUlIsLoose = false
|
|
433
|
-
} else {
|
|
434
|
+
} else if (hasTokenMaps) {
|
|
434
435
|
// For multiple items, check blank lines with map info
|
|
435
436
|
for (let itemIdx = 0; itemIdx < itemIndices.length - 1; itemIdx++) {
|
|
436
437
|
const currentItem = itemIndices[itemIdx]
|
|
@@ -458,6 +459,9 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
461
|
}
|
|
462
|
+
} else {
|
|
463
|
+
// Mapless fallback: reuse Phase 1 loose/tight decision for the outer list.
|
|
464
|
+
outerUlIsLoose = !!outerListToken?._isLoose
|
|
461
465
|
}
|
|
462
466
|
|
|
463
467
|
// ===== Nested list overall loose/tight determination =====
|
|
@@ -472,46 +476,49 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
472
476
|
// In flattened pattern (`- 1.` etc), each outer list_item has one inner ol
|
|
473
477
|
// Check if there are blank lines between list_items in inner ol
|
|
474
478
|
// (Blank lines between outer list_items already checked by outerUlIsLoose)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
// When map is missing, skip map-based blank-line checks and fall back to paragraph.hidden.
|
|
480
|
+
if (hasTokenMaps) {
|
|
481
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
482
|
+
const item = itemIndices[itemIdx]
|
|
483
|
+
|
|
484
|
+
// Collect list_items in inner list
|
|
485
|
+
const innerListItems = []
|
|
486
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
487
|
+
if (tokens[j].type === 'list_item_open' &&
|
|
488
|
+
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
489
|
+
const itemOpen = j
|
|
490
|
+
const itemClose = getListItemClose(j)
|
|
491
|
+
innerListItems.push({ open: itemOpen, close: itemClose })
|
|
492
|
+
}
|
|
486
493
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
|
|
495
|
+
// Check blank lines between list_items in inner list
|
|
496
|
+
if (innerListItems.length > 1) {
|
|
497
|
+
for (let k = 0; k < innerListItems.length - 1; k++) {
|
|
498
|
+
const currentItem = innerListItems[k]
|
|
499
|
+
const nextItem = innerListItems[k + 1]
|
|
500
|
+
|
|
501
|
+
// Get currentItem end line
|
|
502
|
+
let currentEndLine = null
|
|
503
|
+
for (let m = currentItem.close - 1; m > currentItem.open; m--) {
|
|
504
|
+
if (tokens[m].map && tokens[m].map[1]) {
|
|
505
|
+
currentEndLine = tokens[m].map[1]
|
|
506
|
+
break
|
|
507
|
+
}
|
|
501
508
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
509
|
+
|
|
510
|
+
const nextMap = tokens[nextItem.open].map
|
|
511
|
+
|
|
512
|
+
if (currentEndLine !== null && nextMap) {
|
|
513
|
+
const lineGap = nextMap[0] - currentEndLine
|
|
514
|
+
if (lineGap > 0) {
|
|
515
|
+
innerListIsLooseDueToBlankLines = true
|
|
516
|
+
break
|
|
517
|
+
}
|
|
511
518
|
}
|
|
512
519
|
}
|
|
520
|
+
if (innerListIsLooseDueToBlankLines) break
|
|
513
521
|
}
|
|
514
|
-
if (innerListIsLooseDueToBlankLines) break
|
|
515
522
|
}
|
|
516
523
|
}
|
|
517
524
|
|
|
@@ -639,9 +646,8 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
639
646
|
}
|
|
640
647
|
|
|
641
648
|
// Whether this item's first paragraph is loose
|
|
642
|
-
//
|
|
643
|
-
const
|
|
644
|
-
const firstParagraphIsLoose = listItem?.firstParagraphIsLoose || false
|
|
649
|
+
// Use metadata recorded on the outer list_item token.
|
|
650
|
+
const firstParagraphIsLoose = !!tokens[item.outerItemOpen]?._firstParagraphIsLoose
|
|
645
651
|
|
|
646
652
|
// Make loose when parent structure is loose or this item has extra block elements
|
|
647
653
|
// - However, consider cases with multiple paragraphs in item (firstParagraphIsLoose) or
|
|
@@ -775,7 +781,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
775
781
|
item.innerListClose,
|
|
776
782
|
levelShift,
|
|
777
783
|
markerStartIndex,
|
|
778
|
-
item.
|
|
784
|
+
item.innerListMarkerInfo
|
|
779
785
|
)
|
|
780
786
|
for (const nestedToken of nestedTokens) {
|
|
781
787
|
newTokens.push(nestedToken)
|
|
@@ -792,7 +798,7 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
792
798
|
const lastListCloseToken = tokens[lastItem.innerListClose]
|
|
793
799
|
|
|
794
800
|
// If outer ul is convertible, also convert close token
|
|
795
|
-
if (
|
|
801
|
+
if (outerShouldConvert) {
|
|
796
802
|
if (lastListCloseToken.type === 'bullet_list_close') {
|
|
797
803
|
lastListCloseToken.type = 'ordered_list_close'
|
|
798
804
|
lastListCloseToken.tag = 'ol'
|
|
@@ -884,19 +890,6 @@ function simplifyNestedBulletLists(tokens, opt, listInfoMap = null) {
|
|
|
884
890
|
}
|
|
885
891
|
}
|
|
886
892
|
|
|
887
|
-
function buildListInfoMap(listInfos) {
|
|
888
|
-
const map = new Map()
|
|
889
|
-
if (!Array.isArray(listInfos)) {
|
|
890
|
-
return map
|
|
891
|
-
}
|
|
892
|
-
for (const info of listInfos) {
|
|
893
|
-
if (info && typeof info.startIndex === 'number') {
|
|
894
|
-
map.set(info.startIndex, info)
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
return map
|
|
898
|
-
}
|
|
899
|
-
|
|
900
893
|
function collectListItemRanges(tokens, listOpenIdx, listCloseIdx, listItemCloseByOpen = null) {
|
|
901
894
|
const ranges = []
|
|
902
895
|
if (listOpenIdx === -1 || listCloseIdx === -1 || listCloseIdx <= listOpenIdx) {
|
|
@@ -922,13 +915,13 @@ function collectListItemRanges(tokens, listOpenIdx, listCloseIdx, listItemCloseB
|
|
|
922
915
|
return ranges
|
|
923
916
|
}
|
|
924
917
|
|
|
925
|
-
function buildNestedListTokens(tokens, childRanges, innerListOpenIdx, innerListCloseIdx, levelShift, markerStartIndex,
|
|
918
|
+
function buildNestedListTokens(tokens, childRanges, innerListOpenIdx, innerListCloseIdx, levelShift, markerStartIndex, markerInfo) {
|
|
926
919
|
if (!Array.isArray(childRanges) || childRanges.length === 0) {
|
|
927
920
|
return []
|
|
928
921
|
}
|
|
929
922
|
const nestedTokens = []
|
|
930
923
|
const nestedOpen = cloneToken(tokens[innerListOpenIdx], { levelShift })
|
|
931
|
-
const markerInfoSlice = createMarkerInfoSlice(
|
|
924
|
+
const markerInfoSlice = createMarkerInfoSlice(markerInfo, markerStartIndex)
|
|
932
925
|
if (markerInfoSlice) {
|
|
933
926
|
nestedOpen._markerInfo = markerInfoSlice
|
|
934
927
|
const firstMarker = markerInfoSlice.markers?.[0]
|