@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.3.0 → 0.4.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.
@@ -22,12 +22,28 @@ export function convertLists(tokens, listInfos, opt) {
22
22
  // Runs after conversion, so already-converted ordered_lists are also processed
23
23
  if (!opt.unremoveUlNest) {
24
24
  const hasBulletLists = listInfos.some(info => info.originalType === 'bullet_list_open')
25
- if (hasBulletLists) {
25
+ if (hasBulletLists && hasSimplifiableBulletStructure(tokens)) {
26
26
  simplifyNestedBulletLists(tokens)
27
27
  }
28
28
  }
29
29
  }
30
30
 
31
+ function hasSimplifiableBulletStructure(tokens) {
32
+ for (let i = 0; i < tokens.length - 2; i++) {
33
+ if (tokens[i].type !== 'bullet_list_open') {
34
+ continue
35
+ }
36
+ const firstItem = tokens[i + 1]
37
+ const firstChild = tokens[i + 2]
38
+ if (firstItem?.type === 'list_item_open' &&
39
+ firstChild?.type === 'ordered_list_open' &&
40
+ !firstChild._literalList) {
41
+ return true
42
+ }
43
+ }
44
+ return false
45
+ }
46
+
31
47
  /**
32
48
  * Convert bullet_list to ordered_list
33
49
  */
@@ -85,7 +101,7 @@ function convertBulletToOrdered(tokens, listInfo) {
85
101
  // Compare parent item's marker type with child list's marker type
86
102
  // Don't propagate if different (will be flattened)
87
103
  const childMarkerInfo = nestedList.items && nestedList.items[0] && nestedList.items[0].markerInfo
88
- if (!childMarkerInfo || item.markerInfo.markerType !== childMarkerInfo.markerType) {
104
+ if (!childMarkerInfo || item.markerInfo.type !== childMarkerInfo.type) {
89
105
  continue // Skip if marker types differ
90
106
  }
91
107
 
@@ -103,14 +119,14 @@ function convertBulletToOrdered(tokens, listInfo) {
103
119
 
104
120
  // Remove marker text from inline tokens
105
121
  if (markerInfo && markerInfo.markers) {
106
- removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo)
122
+ removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo)
107
123
  }
108
124
  }
109
125
 
110
126
  /**
111
127
  * Remove markers from inline token content
112
128
  */
113
- function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo) {
129
+ function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo) {
114
130
  const listToken = tokens[startIndex]
115
131
  const targetLevel = (listToken.level || 0) + 3 // list_open(0) -> list_item(1) -> paragraph(2) -> inline(3)
116
132
 
@@ -182,16 +198,28 @@ function simplifyNestedBulletLists(tokens) {
182
198
  // Check if this bullet_list is all ul>li>ol/ul pattern
183
199
  const listCloseIdx = getListClose(i)
184
200
  if (listCloseIdx === -1) continue
201
+
202
+ // Fast reject: flattening requires the first list item to start with ordered_list_open.
203
+ const firstItemOpen = i + 1
204
+ const firstItemChild = i + 2
205
+ if (tokens[firstItemOpen]?.type !== 'list_item_open' ||
206
+ tokens[firstItemChild]?.type !== 'ordered_list_open' ||
207
+ tokens[firstItemChild]?._literalList) {
208
+ continue
209
+ }
185
210
 
186
211
  // Check all list_items in bullet_list
187
212
  const itemIndices = []
188
- let totalItems = 0
189
213
  let idx = i + 1
214
+ let allItemsHaveDirectInnerList = true
190
215
 
191
216
  while (idx < listCloseIdx) {
192
217
  if (tokens[idx].type === 'list_item_open') {
193
- totalItems++
194
218
  const itemCloseIdx = getListItemClose(idx)
219
+ if (itemCloseIdx === -1) {
220
+ allItemsHaveDirectInnerList = false
221
+ break
222
+ }
195
223
 
196
224
 
197
225
  // Find inner list within list_item
@@ -216,43 +244,51 @@ function simplifyNestedBulletLists(tokens) {
216
244
  }
217
245
  }
218
246
 
219
- if (innerListOpen !== -1) {
220
- if (innerListCloseIdx === -1) {
221
- innerListCloseIdx = getListClose(innerListOpen)
222
- }
223
-
224
- // Check if there's extra content before/after ol (whether it's only ol)
225
- const beforeContent = innerListOpen - (idx + 1) // Token count from after list_item_open to ol
226
- const afterContent = itemCloseIdx - (innerListCloseIdx + 1) // Token count from after ol to list_item_close
227
- const hasExtraContent = beforeContent > 0 || afterContent > 0
228
- const literalNumber = extractFirstListItemNumber(tokens, innerListOpen, innerListCloseIdx)
229
-
230
- const innerListMarkerInfo = tokens[innerListOpen]?._markerInfo
231
- const isSimpleMarkerParagraph = (() => {
232
- const precedingCount = innerListOpen - (idx + 1)
233
- if (precedingCount !== 3) return false
234
- const paraOpen = tokens[idx + 1]
235
- const inlineToken = tokens[idx + 2]
236
- const paraClose = tokens[idx + 3]
237
- return paraOpen?.type === 'paragraph_open' &&
238
- inlineToken?.type === 'inline' &&
239
- paraClose?.type === 'paragraph_close'
240
- })()
247
+ if (innerListOpen === -1 || innerListOpen !== idx + 1) {
248
+ // Flattening requires every list item to contain only one direct child list.
249
+ allItemsHaveDirectInnerList = false
250
+ break
251
+ }
241
252
 
242
- itemIndices.push({
243
- outerItemOpen: idx,
244
- outerItemClose: itemCloseIdx,
245
- innerListOpen: innerListOpen,
246
- innerListClose: innerListCloseIdx,
247
- innerListType: innerListType,
248
- hasExtraContent: hasExtraContent,
249
- extraContentStart: innerListCloseIdx + 1,
250
- extraContentEnd: itemCloseIdx,
251
- innerListMarkerInfo,
252
- literalNumber,
253
- flattenFirstParagraph: isSimpleMarkerParagraph
254
- })
253
+ if (innerListCloseIdx === -1) {
254
+ innerListCloseIdx = getListClose(innerListOpen)
255
255
  }
256
+ if (innerListCloseIdx === -1) {
257
+ allItemsHaveDirectInnerList = false
258
+ break
259
+ }
260
+
261
+ // Check if there's extra content before/after ol (whether it's only ol)
262
+ const beforeContent = innerListOpen - (idx + 1) // Token count from after list_item_open to ol
263
+ const afterContent = itemCloseIdx - (innerListCloseIdx + 1) // Token count from after ol to list_item_close
264
+ const hasExtraContent = beforeContent > 0 || afterContent > 0
265
+ const literalNumber = extractFirstListItemNumber(tokens, innerListOpen, innerListCloseIdx)
266
+
267
+ const innerListMarkerInfo = tokens[innerListOpen]?._markerInfo
268
+ const isSimpleMarkerParagraph = (() => {
269
+ const precedingCount = innerListOpen - (idx + 1)
270
+ if (precedingCount !== 3) return false
271
+ const paraOpen = tokens[idx + 1]
272
+ const inlineToken = tokens[idx + 2]
273
+ const paraClose = tokens[idx + 3]
274
+ return paraOpen?.type === 'paragraph_open' &&
275
+ inlineToken?.type === 'inline' &&
276
+ paraClose?.type === 'paragraph_close'
277
+ })()
278
+
279
+ itemIndices.push({
280
+ outerItemOpen: idx,
281
+ outerItemClose: itemCloseIdx,
282
+ innerListOpen: innerListOpen,
283
+ innerListClose: innerListCloseIdx,
284
+ innerListType: innerListType,
285
+ hasExtraContent: hasExtraContent,
286
+ extraContentStart: innerListCloseIdx + 1,
287
+ extraContentEnd: itemCloseIdx,
288
+ innerListMarkerInfo,
289
+ literalNumber,
290
+ flattenFirstParagraph: isSimpleMarkerParagraph
291
+ })
256
292
 
257
293
  idx = itemCloseIdx + 1
258
294
  } else {
@@ -290,10 +326,7 @@ function simplifyNestedBulletLists(tokens) {
290
326
  // 1. Every list_item has a nested list (the classic `- 1.` pattern)
291
327
  // 2. That nested list appears as the very first child (no preceding paragraph text)
292
328
  // This prevents flattening normal `* Parent` lists that intentionally nest an ordered list.
293
- const allItemsHaveInnerList = itemIndices.length > 0 && itemIndices.length === totalItems
294
- const innerListIsFirstChild = allItemsHaveInnerList &&
295
- itemIndices.every(item => item.innerListOpen === item.outerItemOpen + 1)
296
- const shouldSimplify = allItemsHaveInnerList && innerListIsFirstChild
329
+ const shouldSimplify = allItemsHaveDirectInnerList && itemIndices.length > 0
297
330
 
298
331
  if (shouldSimplify && itemIndices.length > 0) {
299
332
  const allSameType = itemIndices.every(item => item.innerListType === itemIndices[0].innerListType)
@@ -353,13 +386,8 @@ function simplifyNestedBulletLists(tokens) {
353
386
  }
354
387
  }
355
388
 
356
- // Build new token array
357
- const newTokens = []
358
-
359
- // Tokens before bullet_list_open
360
- for (let j = 0; j < i; j++) {
361
- newTokens.push(tokens[j])
362
- }
389
+ // Build replacement tokens for this list block only.
390
+ const replacementTokens = []
363
391
 
364
392
  // Add first inner list open token and save merged marker info
365
393
  const firstListToken = tokens[itemIndices[0].innerListOpen]
@@ -381,7 +409,7 @@ function simplifyNestedBulletLists(tokens) {
381
409
  // If already ordered_list, use as is (already converted)
382
410
  }
383
411
 
384
- newTokens.push(firstListToken)
412
+ replacementTokens.push(firstListToken)
385
413
 
386
414
  // Merge and save marker info
387
415
  if (allMarkers.length > 0) {
@@ -421,6 +449,11 @@ function simplifyNestedBulletLists(tokens) {
421
449
  }
422
450
  }
423
451
  }
452
+
453
+ // Cache top-level list-item ranges for each inner list to avoid repeated scans.
454
+ const listItemRangesByItem = itemIndices.map(item =>
455
+ collectListItemRanges(tokens, item.innerListOpen, item.innerListClose, listItemCloseByOpen)
456
+ )
424
457
 
425
458
  // ===== Outer UL (Level 0 after conversion) loose/tight determination =====
426
459
  // In flattened pattern (`- 1.` etc), no direct paragraph in outer list_item,
@@ -479,24 +512,13 @@ function simplifyNestedBulletLists(tokens) {
479
512
  // When map is missing, skip map-based blank-line checks and fall back to paragraph.hidden.
480
513
  if (hasTokenMaps) {
481
514
  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
- }
493
- }
494
-
515
+ const listItemRanges = listItemRangesByItem[itemIdx]
516
+
495
517
  // 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]
518
+ if (listItemRanges.length > 1) {
519
+ for (let k = 0; k < listItemRanges.length - 1; k++) {
520
+ const currentItem = listItemRanges[k]
521
+ const nextItem = listItemRanges[k + 1]
500
522
 
501
523
  // Get currentItem end line
502
524
  let currentEndLine = null
@@ -526,21 +548,11 @@ function simplifyNestedBulletLists(tokens) {
526
548
  // If inner list has paragraph with hidden=false, it's loose
527
549
  if (!innerListIsLooseDueToBlankLines) {
528
550
  for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
529
- const item = itemIndices[itemIdx]
530
-
531
- let innerListItemOpen = -1
532
- let innerListItemClose = -1
533
-
534
- for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
535
- if (tokens[j].type === 'list_item_open' &&
536
- tokens[j].level === tokens[item.innerListOpen].level + 1) {
537
- innerListItemOpen = j
538
- innerListItemClose = getListItemClose(j)
539
- break
540
- }
541
- }
542
-
543
- if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
551
+ const listItemRanges = listItemRangesByItem[itemIdx]
552
+ const firstRange = listItemRanges[0]
553
+ if (firstRange) {
554
+ const innerListItemOpen = firstRange.open
555
+ const innerListItemClose = firstRange.close
544
556
  // Check hidden of first paragraph in inner list item
545
557
  let nestedListDepth = 0
546
558
  for (let j = innerListItemOpen + 1; j < innerListItemClose; j++) {
@@ -568,24 +580,20 @@ function simplifyNestedBulletLists(tokens) {
568
580
  // This must be executed AFTER innerListIsLooseDueToBlankLines determination
569
581
  if (outerUlIsLoose && !(itemIndices.length === 1)) {
570
582
  for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
571
- const item = itemIndices[itemIdx]
572
- // Find list_items in inner list
573
- for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
574
- if (tokens[j].type === 'list_item_open' &&
575
- tokens[j].level === tokens[item.innerListOpen].level + 1) {
576
- // Find first paragraph in list_item
577
- const listItemOpen = j
578
- const listItemClose = getListItemClose(j)
579
- for (let k = listItemOpen + 1; k < listItemClose; k++) {
580
- if (tokens[k].type === 'paragraph_open' &&
581
- tokens[k].level === tokens[listItemOpen].level + 1) {
582
- if (!tokens[k]._literalTight) {
583
- tokens[k].hidden = false
584
- }
585
- break // Only first paragraph
586
- }
583
+ const listItemRanges = listItemRangesByItem[itemIdx]
584
+ const firstRange = listItemRanges[0]
585
+ if (!firstRange) {
586
+ continue
587
+ }
588
+ const listItemOpen = firstRange.open
589
+ const listItemClose = firstRange.close
590
+ for (let k = listItemOpen + 1; k < listItemClose; k++) {
591
+ if (tokens[k].type === 'paragraph_open' &&
592
+ tokens[k].level === tokens[listItemOpen].level + 1) {
593
+ if (!tokens[k]._literalTight) {
594
+ tokens[k].hidden = false
587
595
  }
588
- break // Only first list_item of inner list
596
+ break // Only first paragraph
589
597
  }
590
598
  }
591
599
  }
@@ -597,18 +605,10 @@ function simplifyNestedBulletLists(tokens) {
597
605
  // ===== Merge each inner list's contents and place extra content appropriately =====
598
606
  for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
599
607
  const item = itemIndices[itemIdx]
600
-
601
- // Count list_items in this inner list (ordered_list)
602
- let innerListItemCount = 0
603
- for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
604
- if (tokens[j].type === 'list_item_open' &&
605
- tokens[j].level === tokens[item.innerListOpen].level + 1) {
606
- innerListItemCount++
607
- }
608
- }
609
-
608
+
609
+ const listItemRanges = listItemRangesByItem[itemIdx]
610
+ const innerListItemCount = listItemRanges.length
610
611
  const innerListToken = tokens[item.innerListOpen]
611
- const listItemRanges = collectListItemRanges(tokens, item.innerListOpen, item.innerListClose, listItemCloseByOpen)
612
612
  if (listItemRanges.length === 0) {
613
613
  continue
614
614
  }
@@ -620,7 +620,7 @@ function simplifyNestedBulletLists(tokens) {
620
620
  if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
621
621
  // Add list_item_open (use original token to preserve info)
622
622
  // Note: value attribute is added in Phase3 (not during simplification)
623
- newTokens.push(tokens[innerListItemOpen])
623
+ replacementTokens.push(tokens[innerListItemOpen])
624
624
 
625
625
  // This item's loose/tight determination
626
626
  // If entire list is loose, this item is also loose
@@ -669,7 +669,7 @@ function simplifyNestedBulletLists(tokens) {
669
669
  // - firstParagraphIsLoose: blank line after parent item's first paragraph (same marker type only)
670
670
  // Note: hasExtraContent makes parent item loose but doesn't affect child lists
671
671
  // Note: outerUlIsLoose excluded (Test 25: handle case where parent is loose but child is tight)
672
- const shouldPropagateLooseToChildren = innerListIsLooseDueToBlankLines || token._parentIsLoose || firstParagraphIsLoose || false
672
+ const shouldPropagateLooseToChildren = innerListIsLooseDueToBlankLines || token._parentIsLoose || firstParagraphIsLoose
673
673
 
674
674
  // Copy tokens in inner list item (exclude nested list paragraphs)
675
675
  let nestedListDepth = 0
@@ -687,13 +687,13 @@ function simplifyNestedBulletLists(tokens) {
687
687
  if (shouldPropagateLooseToChildren) {
688
688
  // Set _parentIsLoose flag (optimize with direct property assignment)
689
689
  tokenToPush._parentIsLoose = true
690
- newTokens.push(tokenToPush)
690
+ replacementTokens.push(tokenToPush)
691
691
  } else {
692
- newTokens.push(tokenToPush)
692
+ replacementTokens.push(tokenToPush)
693
693
  }
694
694
  } else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
695
695
  nestedListDepth--
696
- newTokens.push(tokenToPush)
696
+ replacementTokens.push(tokenToPush)
697
697
  } else if (nestedListDepth === 0 && (tokenToPush.type === 'paragraph_open' || tokenToPush.type === 'paragraph_close')) {
698
698
  // Update paragraph_open/close hidden state (only outside nested lists)
699
699
 
@@ -718,7 +718,7 @@ function simplifyNestedBulletLists(tokens) {
718
718
  } else {
719
719
  // Match paragraph_close hidden state to corresponding paragraph_open
720
720
  // Reference preceding paragraph_open's hidden state
721
- const prevToken = newTokens[newTokens.length - 2] // Skip preceding inline, get paragraph_open before it
721
+ const prevToken = replacementTokens[replacementTokens.length - 2] // Skip preceding inline, get paragraph_open before it
722
722
  if (prevToken && prevToken.type === 'paragraph_open') {
723
723
  tokenToPush.hidden = prevToken.hidden
724
724
  } else {
@@ -727,9 +727,9 @@ function simplifyNestedBulletLists(tokens) {
727
727
  }
728
728
  }
729
729
 
730
- newTokens.push(tokenToPush)
730
+ replacementTokens.push(tokenToPush)
731
731
  } else {
732
- newTokens.push(tokenToPush)
732
+ replacementTokens.push(tokenToPush)
733
733
  }
734
734
  }
735
735
 
@@ -764,7 +764,7 @@ function simplifyNestedBulletLists(tokens) {
764
764
  }
765
765
  }
766
766
 
767
- newTokens.push(tokenToPush)
767
+ replacementTokens.push(tokenToPush)
768
768
  }
769
769
  }
770
770
 
@@ -784,12 +784,12 @@ function simplifyNestedBulletLists(tokens) {
784
784
  item.innerListMarkerInfo
785
785
  )
786
786
  for (const nestedToken of nestedTokens) {
787
- newTokens.push(nestedToken)
787
+ replacementTokens.push(nestedToken)
788
788
  }
789
789
  }
790
790
 
791
791
  // Add list_item_close
792
- newTokens.push(tokens[innerListItemClose])
792
+ replacementTokens.push(tokens[innerListItemClose])
793
793
  }
794
794
  }
795
795
 
@@ -806,29 +806,20 @@ function simplifyNestedBulletLists(tokens) {
806
806
  // If already ordered_list_close, use as is
807
807
  }
808
808
 
809
- newTokens.push(lastListCloseToken)
809
+ replacementTokens.push(lastListCloseToken)
810
810
 
811
- // Tokens after bullet_list_close
812
- for (let j = listCloseIdx + 1; j < tokens.length; j++) {
813
- newTokens.push(tokens[j])
814
- }
815
-
816
- // Replace token array
817
- tokens.length = 0
818
- tokens.push(...newTokens)
811
+ // Replace only the current outer bullet-list range.
812
+ tokens.splice(i, listCloseIdx - i + 1, ...replacementTokens)
819
813
 
820
814
  // Remove markers from merged list
821
815
  if (firstListToken._markerInfo && firstListToken._markerInfo.markers) {
822
- let listStartIdx = 0
823
- while (listStartIdx < tokens.length && tokens[listStartIdx] !== firstListToken) {
824
- listStartIdx++
825
- }
816
+ const listStartIdx = i
826
817
  if (listStartIdx < tokens.length) {
827
818
  const listEndIdx = findMatchingClose(tokens, listStartIdx,
828
819
  firstListToken.type,
829
- firstListToken.type.replace('_open', '_close'), false)
820
+ firstListToken.type.replace('_open', '_close'))
830
821
  if (listEndIdx !== -1) {
831
- removeMarkersFromContent(tokens, listStartIdx, listEndIdx, firstListToken._markerInfo, null)
822
+ removeMarkersFromContent(tokens, listStartIdx, listEndIdx, firstListToken._markerInfo)
832
823
  }
833
824
  }
834
825
  }
@@ -933,7 +924,7 @@ function buildNestedListTokens(tokens, childRanges, innerListOpenIdx, innerListC
933
924
  nestedTokens.push(nestedOpen)
934
925
  for (const range of childRanges) {
935
926
  for (let i = range.open; i <= range.close; i++) {
936
- nestedTokens.push(cloneToken(tokens[i], { levelShift, deep: true }))
927
+ nestedTokens.push(cloneToken(tokens[i], { levelShift }))
937
928
  }
938
929
  }
939
930
  const nestedClose = cloneToken(tokens[innerListCloseIdx], { levelShift })
@@ -942,7 +933,7 @@ function buildNestedListTokens(tokens, childRanges, innerListOpenIdx, innerListC
942
933
  }
943
934
 
944
935
  function cloneToken(token, options = {}) {
945
- const { levelShift = 0, deep = false } = options
936
+ const { levelShift = 0 } = options
946
937
  const TokenClass = token.constructor
947
938
  const cloned = new TokenClass(token.type, token.tag, token.nesting)
948
939
  cloned.attrs = token.attrs ? token.attrs.map(([name, value]) => [name, value]) : null
@@ -956,7 +947,7 @@ function cloneToken(token, options = {}) {
956
947
  cloned.hidden = token.hidden
957
948
  if (Array.isArray(token.children)) {
958
949
  cloned.children = token.children.length > 0
959
- ? token.children.map(child => cloneToken(child, { levelShift, deep: true }))
950
+ ? token.children.map(child => cloneToken(child, { levelShift }))
960
951
  : []
961
952
  } else {
962
953
  cloned.children = token.children ?? null
@@ -12,6 +12,7 @@ import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
12
12
  export function addAttributes(tokens, opt) {
13
13
  const closeMap = buildListCloseIndexMap(tokens)
14
14
  const listCloseByOpen = closeMap.listCloseByOpen
15
+ let hasAnyListItemValue = false
15
16
 
16
17
  // Traverse token array and add attributes to ordered_list_open tokens
17
18
  // Token array may have been rebuilt in Phase2,
@@ -21,13 +22,31 @@ export function addAttributes(tokens, opt) {
21
22
 
22
23
  if (token.type === 'ordered_list_open') {
23
24
  addListAttributesForToken(tokens, token, i, opt, listCloseByOpen)
25
+ continue
26
+ }
27
+ if (!hasAnyListItemValue && token.type === 'list_item_open' && hasValueAttr(token)) {
28
+ hasAnyListItemValue = true
24
29
  }
25
30
  }
26
31
 
27
32
  // Normalize value attributes of ordered_list generated by markdown-it
28
33
  // Remove unnecessary value attributes after Phase2 simplification
29
34
  // Normalize value attributes and marker metadata for all ordered lists
30
- normalizeAndConvertValueAttributes(tokens, opt, listCloseByOpen)
35
+ if (hasAnyListItemValue) {
36
+ normalizeAndConvertValueAttributes(tokens, listCloseByOpen)
37
+ }
38
+ }
39
+
40
+ function hasValueAttr(token) {
41
+ if (!Array.isArray(token.attrs)) {
42
+ return false
43
+ }
44
+ for (const [name] of token.attrs) {
45
+ if (name === 'value') {
46
+ return true
47
+ }
48
+ }
49
+ return false
31
50
  }
32
51
 
33
52
  /**
@@ -75,13 +94,10 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
75
94
 
76
95
  // 1. type attribute or role attribute (add first)
77
96
  // Always use role="list" for alwaysMarkerSpan
78
- if (opt.useCounterStyle) {
79
- // When user chooses @counter-style, we avoid role and inline styles.
80
- // Still add class so users can target with CSS (e.g., counter-reset/counter-increment or @counter-style usage).
81
- // Do not add type attribute (counter-style will handle visuals)
82
- } else if (typeAttrs.type && !opt.alwaysMarkerSpan) {
97
+ if (!opt.useCounterStyle && typeAttrs.type && !opt.alwaysMarkerSpan) {
83
98
  addAttr(token, 'type', typeAttrs.type)
84
- } else {
99
+ } else if (!opt.useCounterStyle) {
100
+ // In non-counter-style mode, custom markers (or alwaysMarkerSpan) use role=list.
85
101
  addAttr(token, 'role', 'list')
86
102
  if (opt.hasListStyleNone) {
87
103
  addAttr(token, 'style', 'list-style: none;')
@@ -117,12 +133,6 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
117
133
  addAttr(token, 'class', typeAttrs.class)
118
134
  }
119
135
  }
120
-
121
- // If user requested @counter-style, add a helper class to identify lists
122
- if (opt.useCounterStyle) {
123
- // Do not add helper class when using counter-style; user CSS should target generated ol-* classes.
124
- }
125
-
126
136
  // 4. data-marker-prefix/suffix
127
137
  if (!opt.omitMarkerMetadata) {
128
138
  if (markerInfo.markers[0].prefix) {
@@ -197,9 +207,8 @@ function addListItemValues(tokens, listOpenIndex, markerInfo, listCloseByOpen =
197
207
  * Normalize value attributes generated by markdown-it for ordered lists.
198
208
  * Remove value attributes for consecutive numbers.
199
209
  * @param {Array} tokens - Token array
200
- * @param {Object} opt - Plugin options
201
210
  */
202
- function normalizeAndConvertValueAttributes(tokens, opt, listCloseByOpen = null) {
211
+ function normalizeAndConvertValueAttributes(tokens, listCloseByOpen = null) {
203
212
  for (let i = 0; i < tokens.length; i++) {
204
213
  const token = tokens[i]
205
214
 
@@ -13,8 +13,18 @@ export function generateSpans(tokens, opt) {
13
13
  if (opt.useCounterStyle) {
14
14
  return
15
15
  }
16
- const closeMap = buildListCloseIndexMap(tokens)
17
- const listCloseByOpen = closeMap.listCloseByOpen
16
+ const rawSpanClass = opt?.markerSpanClass
17
+ const normalizedSpanClass = typeof rawSpanClass === 'string'
18
+ ? rawSpanClass.trim()
19
+ : ''
20
+ const spanClass = normalizedSpanClass || 'li-num'
21
+ let listCloseByOpen = null
22
+ const getListCloseByOpen = () => {
23
+ if (!listCloseByOpen) {
24
+ listCloseByOpen = buildListCloseIndexMap(tokens).listCloseByOpen
25
+ }
26
+ return listCloseByOpen
27
+ }
18
28
 
19
29
  // Traverse token array and add spans to ordered_list_open tokens
20
30
  for (let i = 0; i < tokens.length; i++) {
@@ -22,12 +32,16 @@ export function generateSpans(tokens, opt) {
22
32
 
23
33
  if (token.type === 'ordered_list_open' && token._markerInfo) {
24
34
  const markerInfo = token._markerInfo
35
+ if (opt.alwaysMarkerSpan) {
36
+ addMarkerSpans(tokens, token, i, markerInfo, opt, spanClass, getListCloseByOpen())
37
+ continue
38
+ }
25
39
  const firstMarker = markerInfo.markers[0]
26
40
  const typeAttrs = getTypeAttributes(markerInfo.type, firstMarker, opt)
27
41
 
28
- // Generate span if no type attribute (custom marker) or alwaysMarkerSpan mode
29
- if (!typeAttrs.type || opt.alwaysMarkerSpan) {
30
- addMarkerSpans(tokens, token, i, markerInfo, opt, listCloseByOpen)
42
+ // Generate span for custom marker lists.
43
+ if (!typeAttrs.type) {
44
+ addMarkerSpans(tokens, token, i, markerInfo, opt, spanClass, getListCloseByOpen())
31
45
  }
32
46
  }
33
47
  }
@@ -36,7 +50,7 @@ export function generateSpans(tokens, opt) {
36
50
  /**
37
51
  * Add marker <span> to the first inline token of each list item.
38
52
  */
39
- function addMarkerSpans(tokens, listToken, listIndex, markerInfo, opt, listCloseByOpen = null) {
53
+ function addMarkerSpans(tokens, listToken, listIndex, markerInfo, opt, spanClass, listCloseByOpen = null) {
40
54
  // Find end position of this ordered_list
41
55
  let listCloseIndex = listCloseByOpen ? listCloseByOpen[listIndex] : -1
42
56
  if (typeof listCloseIndex !== 'number' || listCloseIndex === -1) {
@@ -65,7 +79,6 @@ function addMarkerSpans(tokens, listToken, listIndex, markerInfo, opt, listClose
65
79
  }
66
80
  // Insert span_open, text, span_close before inline token
67
81
  const spanOpen = new tokens[i].constructor('span_open', 'span', 1)
68
- const spanClass = (opt && opt.markerSpanClass) ? String(opt.markerSpanClass) : 'li-num'
69
82
  spanOpen.attrSet('class', spanClass)
70
83
  spanOpen.attrSet('aria-hidden', 'true')
71
84