@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 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 Term line is not separated from the description by a blank line, then the Term line must end with two ASCII spaces (a Markdown line-break) or a backslash `\` to indicate the description follows.
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: Currently the content inside `<dd>` elements is always wrapped in `<p>` elements.
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
- // Check if markdown-it-attrs is loaded (detect once at plugin initialization)
30
- const hasAttrsPlugin = md.core.ruler.__rules__.some(rule => rule.name === 'curly_attributes')
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 original listInfos as tokens may have been removed in Phase2
60
- // (Uses markerInfo stored in tokens)
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, listInfos, opt)
80
+ generateSpans(tokens, opt)
70
81
 
71
82
  return true
72
83
  }
73
84
 
74
- md.core.ruler.before('inline', 'numbering_ul_phases', listProcessor)
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
- if (hasAttrsPlugin) {
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
- if (hasAttrsPlugin) {
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.2.3",
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.5.5",
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**: description (colon)
154
- // Pattern 3: **Term**\ description (backslash escape)
155
- if (/^\s{2,}/.test(afterStrong) || /^\s*:/.test(afterStrong) || /^\\/.test(afterStrong)) {
156
- // Remove leading space/colon/backslash and check remaining text
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**: description (colon)
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
- term = match[1] // Keep spaces for now (will be processed by inline parser)
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/colon/backslash, then trim each line
374
- let cleaned = afterStrong.replace(/^[\s:]+/, '').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
 
@@ -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
- markers.push({
292
- number: itemNumber,
293
- originalNumber: itemNumber,
294
- prefix: '',
295
- suffix: markup,
296
- type: 'decimal' // Default (can be extended to detect from markup)
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 {
@@ -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
- const listInfoMap = buildListInfoMap(listInfos)
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, opt, listInfoMap = null) {
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 innerListInfo = listInfoMap?.get(innerListOpen)
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
- innerListInfo,
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 outerListInfo = listInfoMap ? listInfoMap.get(i) : null
264
+ const outerListToken = tokens[i]
265
+ const outerShouldConvert = !!outerListToken?._shouldConvert
266
+ const outerMarkerInfo = outerListToken?._markerInfo || null
266
267
 
267
- if (outerListInfo?.shouldConvert) {
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 (outerListInfo && outerListInfo.markerInfo && outerListInfo.markerInfo.markers) {
313
- allMarkers.push(...outerListInfo.markerInfo.markers)
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 (!outerListInfo || !outerListInfo.markerInfo) {
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 (outerListInfo && outerListInfo.shouldConvert) {
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
- for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
476
- const item = itemIndices[itemIdx]
477
-
478
- // Collect list_items in inner list
479
- const innerListItems = []
480
- for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
481
- if (tokens[j].type === 'list_item_open' &&
482
- tokens[j].level === tokens[item.innerListOpen].level + 1) {
483
- const itemOpen = j
484
- const itemClose = getListItemClose(j)
485
- innerListItems.push({ open: itemOpen, close: itemClose })
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
- // Check blank lines between list_items in inner list
490
- if (innerListItems.length > 1) {
491
- for (let k = 0; k < innerListItems.length - 1; k++) {
492
- const currentItem = innerListItems[k]
493
- const nextItem = innerListItems[k + 1]
494
-
495
- // Get currentItem end line
496
- let currentEndLine = null
497
- for (let m = currentItem.close - 1; m > currentItem.open; m--) {
498
- if (tokens[m].map && tokens[m].map[1]) {
499
- currentEndLine = tokens[m].map[1]
500
- break
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
- const nextMap = tokens[nextItem.open].map
505
-
506
- if (currentEndLine !== null && nextMap) {
507
- const lineGap = nextMap[0] - currentEndLine
508
- if (lineGap > 0) {
509
- innerListIsLooseDueToBlankLines = true
510
- break
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
- // is obtained from Phase1-analyzed listInfo.items[itemIdx]
643
- const listItem = outerListInfo?.items?.[itemIdx]
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.innerListInfo
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 (outerListInfo && outerListInfo.shouldConvert) {
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, innerListInfo) {
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(innerListInfo?.markerInfo, markerStartIndex)
924
+ const markerInfoSlice = createMarkerInfoSlice(markerInfo, markerStartIndex)
932
925
  if (markerInfoSlice) {
933
926
  nestedOpen._markerInfo = markerInfoSlice
934
927
  const firstMarker = markerInfoSlice.markers?.[0]