@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.4.0 → 0.4.2

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.
@@ -3,109 +3,27 @@
3
3
  import { detectMarkerType, detectMarkerTypeWithContext, detectSequencePattern } from './types-utility.js'
4
4
  import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
5
5
 
6
- /**
7
- * Pre-compute DL scope (identify all DL ranges in O(n))
8
- * @param {Array} tokens - Token array
9
- * @returns {Array<[number, number]>} DL range pairs [[start, end], ...]
10
- */
11
- function buildDLStateMap(tokens) {
12
- const state = new Array(tokens.length).fill(false)
13
- let dlDepth = 0
14
- let htmlDdDepth = 0
15
-
16
- for (let i = 0; i < tokens.length; i++) {
17
- const token = tokens[i]
18
-
19
- if (dlDepth > 0 || htmlDdDepth > 0) {
20
- state[i] = true
21
- }
22
-
23
- if (token.type === 'dl_open') {
24
- state[i] = true
25
- dlDepth++
26
- continue
27
- }
28
-
29
- if (token.type === 'dl_close') {
30
- state[i] = true
31
- if (dlDepth > 0) {
32
- dlDepth--
33
- }
34
- continue
35
- }
36
-
37
- if (token.type === 'html_block') {
38
- if (token.content === '<dd>\n') {
39
- state[i] = true
40
- htmlDdDepth++
41
- continue
42
- }
43
- if (token.content === '</dd>\n') {
44
- state[i] = true
45
- if (htmlDdDepth > 0) {
46
- htmlDdDepth--
47
- }
48
- continue
49
- }
50
- }
51
- }
52
-
53
- return state
54
- }
55
-
56
- /**
57
- * Check if token at specified index is inside DL
58
- * @param {number} index - Index to check
59
- * @param {Array<[number, number]>} dlRanges - DL range pairs
60
- * @returns {boolean} True if inside DL
61
- */
62
- function isInsideDL(index, dlState) {
63
- if (!dlState || index < 0 || index >= dlState.length) {
64
- return false
65
- }
66
- return dlState[index] === true
67
- }
68
-
69
6
  /**
70
7
  * Collect list information from token array
71
8
  * @param {Array} tokens - markdown-it token array
72
- * @param {Object} opt - Plugin options
73
9
  * @returns {Array} Flat array of list information (including nested lists)
74
10
  */
75
- export function analyzeListStructure(tokens, opt) {
11
+ export function analyzeListStructure(tokens) {
76
12
  const listInfos = []
77
13
  const closeMap = buildListCloseIndexMap(tokens)
78
-
79
- // Check DL existence (O(n) but optimized with early return)
80
- let hasDL = false
81
- for (let i = 0; i < tokens.length; i++) {
82
- if (tokens[i].type === 'dl_open' ||
83
- (tokens[i].type === 'html_block' && tokens[i].content === '<dd>\n')) {
84
- hasDL = true
85
- break
86
- }
87
- }
88
-
89
- // Pre-compute DL scope flags (only when DL exists)
90
- const dlScope = hasDL ? buildDLStateMap(tokens) : null
91
-
92
- // Process only top-level lists (nested lists collected recursively)
93
- // Also process lists inside DL
14
+
15
+ // Process every list root in the token stream, including lists nested in
16
+ // containers such as blockquotes or description-list descriptions. Nested
17
+ // lists inside an already-analyzed list are collected recursively, then the
18
+ // scan jumps to that list's close token to avoid duplicate processing.
94
19
  for (let i = 0; i < tokens.length; i++) {
95
20
  const token = tokens[i]
96
21
  if (!token) {
97
22
  continue
98
23
  }
99
-
100
- // Process level-0 lists or lists at any level inside DD
101
- const isTopLevelList = (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') &&
102
- (token.level === 0 || token.level === undefined)
103
- const isListInDD = (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') &&
104
- token.level > 0 &&
105
- isInsideDL(i, dlScope)
106
-
107
- if (isTopLevelList || isListInDD) {
108
- const listInfo = analyzeList(tokens, i, opt, closeMap)
24
+
25
+ if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
26
+ const listInfo = analyzeList(tokens, i, closeMap)
109
27
  if (listInfo) {
110
28
  listInfos.push(listInfo)
111
29
 
@@ -139,7 +57,7 @@ function collectNestedLists(listInfo, listInfos) {
139
57
  /**
140
58
  * Analyze detailed information of a single list
141
59
  */
142
- function analyzeList(tokens, startIndex, opt, closeMap) {
60
+ function analyzeList(tokens, startIndex, closeMap) {
143
61
  const listToken = tokens[startIndex]
144
62
  const endIndex = findListEnd(tokens, startIndex, closeMap)
145
63
 
@@ -149,7 +67,7 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
149
67
 
150
68
  const level = listToken.level || 0
151
69
  const originalType = listToken.type
152
- const items = analyzeListItems(tokens, startIndex, endIndex, opt, closeMap)
70
+ const items = analyzeListItems(tokens, startIndex, endIndex, closeMap)
153
71
  const isLoose = detectLooseList(tokens, startIndex, endIndex, items)
154
72
  if (!isLoose) {
155
73
  hideFirstParagraphsForTightList(tokens, items, level)
@@ -159,7 +77,7 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
159
77
  listToken._isLoose = isLoose
160
78
 
161
79
  // Extract marker info (for both bullet_list and ordered_list)
162
- const markerInfo = extractMarkerInfo(tokens, startIndex, endIndex, opt)
80
+ const markerInfo = extractMarkerInfo(tokens, startIndex, endIndex)
163
81
 
164
82
  // Recheck marker consistency against item count
165
83
  if (markerInfo && markerInfo.count < items.length) {
@@ -170,7 +88,7 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
170
88
  // Conversion decision (only consider for bullet_list)
171
89
  // Convert even loose lists if markers are consistent
172
90
  const shouldConvert = originalType === 'bullet_list_open' &&
173
- shouldConvertToOrdered(originalType, markerInfo, opt)
91
+ shouldConvertToOrdered(originalType, markerInfo)
174
92
 
175
93
  if (markerInfo) {
176
94
  listToken._markerInfo = markerInfo
@@ -194,7 +112,7 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
194
112
  /**
195
113
  * Analyze list items
196
114
  */
197
- function analyzeListItems(tokens, startIndex, endIndex, opt, closeMap) {
115
+ function analyzeListItems(tokens, startIndex, endIndex, closeMap) {
198
116
  const items = []
199
117
  let i = startIndex + 1
200
118
 
@@ -203,7 +121,7 @@ function analyzeListItems(tokens, startIndex, endIndex, opt, closeMap) {
203
121
 
204
122
  if (token.type === 'list_item_open') {
205
123
  const itemEndIndex = findListItemEnd(tokens, i, closeMap)
206
- const item = analyzeListItem(tokens, i, itemEndIndex, opt, closeMap)
124
+ const item = analyzeListItem(tokens, i, itemEndIndex, closeMap)
207
125
  items.push(item)
208
126
  i = itemEndIndex + 1
209
127
  } else {
@@ -217,11 +135,10 @@ function analyzeListItems(tokens, startIndex, endIndex, opt, closeMap) {
217
135
  /**
218
136
  * Analyze a single list item
219
137
  */
220
- function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
221
- let content = ''
138
+ function analyzeListItem(tokens, startIndex, endIndex, closeMap) {
222
139
  let markerInfo = null
223
140
  let hasNestedList = false
224
- let nestedLists = []
141
+ const nestedLists = []
225
142
  let firstParagraphIsLoose = false
226
143
  let lastInlineContent = ''
227
144
 
@@ -259,7 +176,7 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
259
176
  }
260
177
  checkedFirstParagraphLoose = true
261
178
  hasNestedList = true
262
- const nestedListInfo = analyzeList(tokens, i, opt, closeMap)
179
+ const nestedListInfo = analyzeList(tokens, i, closeMap)
263
180
  nestedLists.push(nestedListInfo)
264
181
  i = nestedListInfo.endIndex
265
182
  }
@@ -268,7 +185,6 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
268
185
  if (firstParagraphIsLoose && hasNestedList && lastInlineContent) {
269
186
  markerInfo = detectMarkerType(lastInlineContent)
270
187
  }
271
- content = lastInlineContent
272
188
 
273
189
  if (tokens[startIndex]) {
274
190
  tokens[startIndex]._firstParagraphIsLoose = firstParagraphIsLoose
@@ -277,7 +193,7 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
277
193
  return {
278
194
  startIndex,
279
195
  endIndex,
280
- content,
196
+ content: lastInlineContent,
281
197
  markerInfo,
282
198
  hasNestedList,
283
199
  nestedLists,
@@ -288,7 +204,7 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
288
204
  /**
289
205
  * Extract marker information
290
206
  */
291
- function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
207
+ function extractMarkerInfo(tokens, startIndex, endIndex) {
292
208
  const listToken = tokens[startIndex]
293
209
  const markers = []
294
210
  const literalMarkerInfo = listToken._literalMarkerInfo || null
@@ -349,35 +265,32 @@ function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
349
265
  // Detect markers using full context
350
266
  let sequentialNumber = 1 // Sequential number counter
351
267
  for (const token of inlineTokens) {
352
- // Process only inline tokens of direct child items of this list
353
- if (token.type === 'inline' && token.content && token.level === targetLevel) {
354
- const markerInfo = detectMarkerTypeWithContext(token.content, contextResult)
355
- if (markerInfo && markerInfo.type) {
356
- // Use sequential numbers when same marker continues
357
- // (e.g., "イ. イ. イ." → interpreted as "イ、ロ、ハ")
358
- const adjustedMarkerInfo = { ...markerInfo }
359
- adjustedMarkerInfo.originalNumber = markerInfo.number
360
-
361
- // Assign sequential numbers based on first marker's number
362
- if (markers.length === 0) {
363
- // Use first marker as-is
364
- sequentialNumber = markerInfo.number || 1
268
+ const markerInfo = detectMarkerTypeWithContext(token.content, contextResult)
269
+ if (markerInfo && markerInfo.type) {
270
+ // Use sequential numbers when same marker continues
271
+ // (e.g., "イ. イ. イ." → interpreted as "イ、ロ、ハ")
272
+ const adjustedMarkerInfo = { ...markerInfo }
273
+ adjustedMarkerInfo.originalNumber = markerInfo.number
274
+
275
+ // Assign sequential numbers based on first marker's number
276
+ if (markers.length === 0) {
277
+ // Use first marker as-is
278
+ sequentialNumber = markerInfo.number || 1
279
+ } else {
280
+ // For 2nd and later, use sequential number if same as previous marker
281
+ const prevMarker = markers[markers.length - 1]
282
+ if (markerInfo.marker === prevMarker.marker &&
283
+ markerInfo.type === prevMarker.type) {
284
+ // Assign sequential number when same marker continues
285
+ sequentialNumber++
286
+ adjustedMarkerInfo.number = sequentialNumber
365
287
  } else {
366
- // For 2nd and later, use sequential number if same as previous marker
367
- const prevMarker = markers[markers.length - 1]
368
- if (markerInfo.marker === prevMarker.marker &&
369
- markerInfo.type === prevMarker.type) {
370
- // Assign sequential number when same marker continues
371
- sequentialNumber++
372
- adjustedMarkerInfo.number = sequentialNumber
373
- } else {
374
- // For different marker, use detected number
375
- sequentialNumber = markerInfo.number || sequentialNumber + 1
376
- }
288
+ // For different marker, use detected number
289
+ sequentialNumber = markerInfo.number || sequentialNumber + 1
377
290
  }
378
-
379
- markers.push(adjustedMarkerInfo)
380
291
  }
292
+
293
+ markers.push(adjustedMarkerInfo)
381
294
  }
382
295
  }
383
296
  }
@@ -411,7 +324,7 @@ function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
411
324
  /**
412
325
  * Determine if should convert to ordered list
413
326
  */
414
- function shouldConvertToOrdered(originalType, markerInfo, opt) {
327
+ function shouldConvertToOrdered(originalType, markerInfo) {
415
328
  // No conversion needed if already ordered_list
416
329
  if (originalType === 'ordered_list_open') {
417
330
  return false
@@ -12,7 +12,11 @@ import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
12
12
  export function convertLists(tokens, listInfos, opt) {
13
13
  // Convert bullet_list to ordered_list
14
14
  // listInfos already collected in depth-first order in Phase 1, no sorting needed
15
+ let hasBulletLists = false
15
16
  for (const listInfo of listInfos) {
17
+ if (!hasBulletLists && listInfo.originalType === 'bullet_list_open') {
18
+ hasBulletLists = true
19
+ }
16
20
  if (listInfo.shouldConvert) {
17
21
  convertBulletToOrdered(tokens, listInfo)
18
22
  }
@@ -20,11 +24,8 @@ export function convertLists(tokens, listInfos, opt) {
20
24
 
21
25
  // In default mode (unremoveUlNest=false), simplify ul>li>ol structure
22
26
  // Runs after conversion, so already-converted ordered_lists are also processed
23
- if (!opt.unremoveUlNest) {
24
- const hasBulletLists = listInfos.some(info => info.originalType === 'bullet_list_open')
25
- if (hasBulletLists && hasSimplifiableBulletStructure(tokens)) {
26
- simplifyNestedBulletLists(tokens)
27
- }
27
+ if (!opt.unremoveUlNest && hasBulletLists && hasSimplifiableBulletStructure(tokens)) {
28
+ simplifyNestedBulletLists(tokens)
28
29
  }
29
30
  }
30
31
 
@@ -44,6 +45,26 @@ function hasSimplifiableBulletStructure(tokens) {
44
45
  return false
45
46
  }
46
47
 
48
+ function resolveListClose(tokens, openIdx, listCloseByOpen = null) {
49
+ const mapped = listCloseByOpen ? listCloseByOpen[openIdx] : -1
50
+ if (typeof mapped === 'number' && mapped !== -1) {
51
+ return mapped
52
+ }
53
+ const openType = tokens[openIdx]?.type
54
+ if (!openType) {
55
+ return -1
56
+ }
57
+ return findMatchingClose(tokens, openIdx, openType, openType.replace('_open', '_close'))
58
+ }
59
+
60
+ function resolveListItemClose(tokens, openIdx, listItemCloseByOpen = null) {
61
+ const mapped = listItemCloseByOpen ? listItemCloseByOpen[openIdx] : -1
62
+ if (typeof mapped === 'number' && mapped !== -1) {
63
+ return mapped
64
+ }
65
+ return findMatchingClose(tokens, openIdx, 'list_item_open', 'list_item_close')
66
+ }
67
+
47
68
  /**
48
69
  * Convert bullet_list to ordered_list
49
70
  */
@@ -138,22 +159,17 @@ function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo) {
138
159
  if (token.type === 'inline' && token.content && token.level === targetLevel) {
139
160
  const marker = markers[markerIndex]
140
161
  if (marker && marker.marker && token.content.startsWith(marker.marker)) {
141
- let newContent = token.content.slice(marker.marker.length)
142
- newContent = newContent.replace(LEADING_SPACE_REGEX, '')
143
- token.content = newContent
162
+ token.content = token.content.slice(marker.marker.length).trimStart()
144
163
  markerIndex++
145
164
  }
146
165
  }
147
166
  }
148
167
  }
149
168
 
150
- const LEADING_SPACE_REGEX = /^\s+/
151
-
152
169
  /**
153
- * Simplify nested ul>li>ul and ul>li>ol structures.
170
+ * Simplify nested ul>li>ol structures.
154
171
  *
155
- * Pattern 1: bullet_list_open → list_item_open → bullet_list_open → ...
156
- * Pattern 2: bullet_list_open → list_item_open → ordered_list_open → ... (repeated)
172
+ * Pattern: bullet_list_open → list_item_open → ordered_list_open → ... (repeated)
157
173
  *
158
174
  * When the middle list_item is empty (contains only the inner list),
159
175
  * remove the outer ul and the intermediate li.
@@ -168,24 +184,6 @@ function simplifyNestedBulletLists(tokens) {
168
184
  while (modified) {
169
185
  modified = false
170
186
  const { listCloseByOpen, listItemCloseByOpen } = buildListCloseIndexMap(tokens)
171
- const getListClose = (openIdx) => {
172
- const mapped = listCloseByOpen[openIdx]
173
- if (typeof mapped === 'number' && mapped !== -1) {
174
- return mapped
175
- }
176
- const openType = tokens[openIdx]?.type
177
- if (!openType) {
178
- return -1
179
- }
180
- return findMatchingClose(tokens, openIdx, openType, openType.replace('_open', '_close'))
181
- }
182
- const getListItemClose = (openIdx) => {
183
- const mapped = listItemCloseByOpen[openIdx]
184
- if (typeof mapped === 'number' && mapped !== -1) {
185
- return mapped
186
- }
187
- return findMatchingClose(tokens, openIdx, 'list_item_open', 'list_item_close')
188
- }
189
187
 
190
188
  for (let i = 0; i < tokens.length; i++) {
191
189
  const token = tokens[i]
@@ -196,7 +194,7 @@ function simplifyNestedBulletLists(tokens) {
196
194
  }
197
195
 
198
196
  // Check if this bullet_list is all ul>li>ol/ul pattern
199
- const listCloseIdx = getListClose(i)
197
+ const listCloseIdx = resolveListClose(tokens, i, listCloseByOpen)
200
198
  if (listCloseIdx === -1) continue
201
199
 
202
200
  // Fast reject: flattening requires the first list item to start with ordered_list_open.
@@ -215,7 +213,7 @@ function simplifyNestedBulletLists(tokens) {
215
213
 
216
214
  while (idx < listCloseIdx) {
217
215
  if (tokens[idx].type === 'list_item_open') {
218
- const itemCloseIdx = getListItemClose(idx)
216
+ const itemCloseIdx = resolveListItemClose(tokens, idx, listItemCloseByOpen)
219
217
  if (itemCloseIdx === -1) {
220
218
  allItemsHaveDirectInnerList = false
221
219
  break
@@ -229,7 +227,7 @@ function simplifyNestedBulletLists(tokens) {
229
227
  for (let j = idx + 1; j < itemCloseIdx; j++) {
230
228
  if (tokens[j].type === 'bullet_list_open' || tokens[j].type === 'ordered_list_open') {
231
229
  const candidateType = tokens[j].type
232
- const candidateClose = getListClose(j)
230
+ const candidateClose = resolveListClose(tokens, j, listCloseByOpen)
233
231
  if (tokens[j]._literalList) {
234
232
  if (candidateClose === -1) {
235
233
  break
@@ -250,8 +248,8 @@ function simplifyNestedBulletLists(tokens) {
250
248
  break
251
249
  }
252
250
 
253
- if (innerListCloseIdx === -1) {
254
- innerListCloseIdx = getListClose(innerListOpen)
251
+ if (innerListCloseIdx === -1) {
252
+ innerListCloseIdx = resolveListClose(tokens, innerListOpen, listCloseByOpen)
255
253
  }
256
254
  if (innerListCloseIdx === -1) {
257
255
  allItemsHaveDirectInnerList = false
@@ -259,22 +257,16 @@ function simplifyNestedBulletLists(tokens) {
259
257
  }
260
258
 
261
259
  // 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
260
  const afterContent = itemCloseIdx - (innerListCloseIdx + 1) // Token count from after ol to list_item_close
264
- const hasExtraContent = beforeContent > 0 || afterContent > 0
261
+ const hasExtraContent = afterContent > 0
265
262
  const literalNumber = extractFirstListItemNumber(tokens, innerListOpen, innerListCloseIdx)
266
263
 
267
264
  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
- })()
265
+ const isSimpleMarkerParagraph =
266
+ innerListOpen - (idx + 1) === 3 &&
267
+ tokens[idx + 1]?.type === 'paragraph_open' &&
268
+ tokens[idx + 2]?.type === 'inline' &&
269
+ tokens[idx + 3]?.type === 'paragraph_close'
278
270
 
279
271
  itemIndices.push({
280
272
  outerItemOpen: idx,
@@ -687,10 +679,8 @@ function simplifyNestedBulletLists(tokens) {
687
679
  if (shouldPropagateLooseToChildren) {
688
680
  // Set _parentIsLoose flag (optimize with direct property assignment)
689
681
  tokenToPush._parentIsLoose = true
690
- replacementTokens.push(tokenToPush)
691
- } else {
692
- replacementTokens.push(tokenToPush)
693
682
  }
683
+ replacementTokens.push(tokenToPush)
694
684
  } else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
695
685
  nestedListDepth--
696
686
  replacementTokens.push(tokenToPush)
@@ -833,37 +823,31 @@ function simplifyNestedBulletLists(tokens) {
833
823
  // ===== Propagate ordered_list's _parentIsLoose flag to child lists =====
834
824
  // Check all ordered_list_open, and if _parentIsLoose flag exists,
835
825
  // propagate to child lists
836
- const postCloseMap = buildListCloseIndexMap(tokens)
837
- const getPostListClose = (openIdx) => {
838
- const mapped = postCloseMap.listCloseByOpen[openIdx]
839
- if (typeof mapped === 'number' && mapped !== -1) {
840
- return mapped
841
- }
842
- const openType = tokens[openIdx]?.type
843
- if (!openType) {
844
- return -1
826
+ let hasParentLooseList = false
827
+ for (let i = 0; i < tokens.length; i++) {
828
+ const token = tokens[i]
829
+ if ((token.type === 'ordered_list_open' || token.type === 'bullet_list_open') && token._parentIsLoose) {
830
+ hasParentLooseList = true
831
+ break
845
832
  }
846
- return findMatchingClose(tokens, openIdx, openType, openType.replace('_open', '_close'))
847
833
  }
848
- const getPostListItemClose = (openIdx) => {
849
- const mapped = postCloseMap.listItemCloseByOpen[openIdx]
850
- if (typeof mapped === 'number' && mapped !== -1) {
851
- return mapped
852
- }
853
- return findMatchingClose(tokens, openIdx, 'list_item_open', 'list_item_close')
834
+ if (!hasParentLooseList) {
835
+ return
854
836
  }
837
+
838
+ const postCloseMap = buildListCloseIndexMap(tokens)
855
839
  for (let i = 0; i < tokens.length; i++) {
856
840
  const token = tokens[i]
857
841
 
858
842
  if ((token.type === 'ordered_list_open' || token.type === 'bullet_list_open') && token._parentIsLoose) {
859
843
  // Find this list's close token
860
- const listCloseIdx = getPostListClose(i)
844
+ const listCloseIdx = resolveListClose(tokens, i, postCloseMap.listCloseByOpen)
861
845
  if (listCloseIdx === -1) continue
862
846
 
863
847
  // Search list_items in this list and set _parentIsLoose flag to child lists in those list_items
864
848
  for (let j = i + 1; j < listCloseIdx; j++) {
865
849
  if (tokens[j].type === 'list_item_open' && tokens[j].level === token.level + 1) {
866
- const itemCloseIdx = getPostListItemClose(j)
850
+ const itemCloseIdx = resolveListItemClose(tokens, j, postCloseMap.listItemCloseByOpen)
867
851
 
868
852
  // Search child lists in this list_item
869
853
  for (let k = j + 1; k < itemCloseIdx; k++) {
@@ -4,6 +4,8 @@
4
4
  import { getTypeAttributes } from './types-utility.js'
5
5
  import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
6
6
 
7
+ const WHITESPACE_SUFFIX_REGEX = /^[ \u3000]+$/
8
+
7
9
  /**
8
10
  * Add attributes to lists
9
11
  * @param {Array} tokens - Token array
@@ -35,6 +37,8 @@ export function addAttributes(tokens, opt) {
35
37
  if (hasAnyListItemValue) {
36
38
  normalizeAndConvertValueAttributes(tokens, listCloseByOpen)
37
39
  }
40
+
41
+ return closeMap
38
42
  }
39
43
 
40
44
  function hasValueAttr(token) {
@@ -53,15 +57,14 @@ function hasValueAttr(token) {
53
57
  * Add attributes to a single list token
54
58
  */
55
59
  function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOpen = null) {
56
- // Initialize attribute array
57
- if (!token.attrs) {
58
- token.attrs = []
59
- }
60
-
61
60
  // Get marker info
62
61
  const markerInfo = token._markerInfo
63
62
 
64
63
  if (!markerInfo) {
64
+ if (!token.attrs) {
65
+ token.attrs = []
66
+ }
67
+
65
68
  // Default attributes for lists without markerInfo
66
69
  if (opt.useCounterStyle) {
67
70
  // Do not add type attribute; add class so user CSS/@counter-style can target
@@ -83,7 +86,7 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
83
86
 
84
87
  // Attributes according to marker type
85
88
  // Pass first marker's prefix/suffix info to determine class name
86
- const firstMarker = markerInfo.markers[0]
89
+ const firstMarker = markerInfo.markers?.[0]
87
90
  const typeAttrs = getTypeAttributes(markerInfo.type, firstMarker, opt)
88
91
 
89
92
  // Reset attribute array
@@ -112,35 +115,23 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
112
115
  } else {
113
116
  startOverride = undefined
114
117
  }
115
- const firstNumber = startOverride ?? (markerInfo.markers[0]?.originalNumber ?? markerInfo.markers[0]?.number)
118
+ const firstNumber = startOverride ?? (firstMarker?.originalNumber ?? firstMarker?.number)
116
119
  if (firstNumber !== undefined && firstNumber !== 1) {
117
120
  addAttr(token, 'start', String(firstNumber))
118
- } else if (token.attrs) {
119
- const startIdx = token.attrs.findIndex(attr => attr[0] === 'start')
120
- if (startIdx >= 0) {
121
- token.attrs.splice(startIdx, 1)
122
- if (token.attrs.length === 0) token.attrs = null
123
- }
124
121
  }
125
122
 
126
123
  // 3. Add class attribute
127
124
  if (typeAttrs.class) {
128
- // Merge or add class; preserve existing classes and append
129
- const existing = token.attrs.find(a => a[0] === 'class')
130
- if (existing) {
131
- existing[1] = (existing[1] + ' ' + typeAttrs.class).trim()
132
- } else {
133
- addAttr(token, 'class', typeAttrs.class)
134
- }
125
+ addAttr(token, 'class', typeAttrs.class)
135
126
  }
136
127
  // 4. data-marker-prefix/suffix
137
128
  if (!opt.omitMarkerMetadata) {
138
- if (markerInfo.markers[0].prefix) {
139
- addAttr(token, 'data-marker-prefix', markerInfo.markers[0].prefix)
129
+ if (firstMarker?.prefix) {
130
+ addAttr(token, 'data-marker-prefix', firstMarker.prefix)
140
131
  }
141
132
  // Do not emit data-marker-suffix when suffix is only whitespace (halfwidth or fullwidth)
142
- const suffix = markerInfo.markers[0].suffix
143
- if (suffix && !/^[ \u3000]+$/.test(suffix)) {
133
+ const suffix = firstMarker?.suffix
134
+ if (suffix && !WHITESPACE_SUFFIX_REGEX.test(suffix)) {
144
135
  addAttr(token, 'data-marker-suffix', suffix)
145
136
  }
146
137
  }
@@ -263,10 +254,11 @@ function normalizeAndConvertValueAttributes(tokens, listCloseByOpen = null) {
263
254
  * Add or replace an attribute on a token (with duplicate check).
264
255
  */
265
256
  function addAttr(token, name, value) {
266
- const existingIndex = token.attrs.findIndex(attr => attr[0] === name)
267
- if (existingIndex >= 0) {
268
- token.attrs[existingIndex] = [name, value]
269
- } else {
270
- token.attrs.push([name, value])
257
+ for (let i = 0; i < token.attrs.length; i++) {
258
+ if (token.attrs[i][0] === name) {
259
+ token.attrs[i] = [name, value]
260
+ return
261
+ }
271
262
  }
263
+ token.attrs.push([name, value])
272
264
  }