@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-numbering-ul-regarded-as-ol",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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.1",
30
+ "@peaceroad/markdown-it-strong-ja": "^0.5.5",
31
31
  "markdown-it": "^14.1.0",
32
32
  "markdown-it-attrs": "^4.3.1",
33
33
  "markdown-it-deflist": "^3.0.0"
@@ -45,3 +45,51 @@ export function findListEnd(tokens, startIndex) {
45
45
  export function findListItemEnd(tokens, startIndex) {
46
46
  return findMatchingClose(tokens, startIndex, 'list_item_open', 'list_item_close')
47
47
  }
48
+
49
+ /**
50
+ * Build close-index maps for list and list_item tokens.
51
+ * @param {Array} tokens - Token array
52
+ * @returns {{ listCloseByOpen: number[], listItemCloseByOpen: number[] }}
53
+ */
54
+ export function buildListCloseIndexMap(tokens) {
55
+ const listCloseByOpen = new Array(tokens.length).fill(-1)
56
+ const listItemCloseByOpen = new Array(tokens.length).fill(-1)
57
+ const bulletStack = []
58
+ const orderedStack = []
59
+ const listItemStack = []
60
+
61
+ for (let i = 0; i < tokens.length; i++) {
62
+ const type = tokens[i]?.type
63
+ if (type === 'bullet_list_open') {
64
+ bulletStack.push(i)
65
+ continue
66
+ }
67
+ if (type === 'bullet_list_close') {
68
+ if (bulletStack.length > 0) {
69
+ listCloseByOpen[bulletStack.pop()] = i
70
+ }
71
+ continue
72
+ }
73
+ if (type === 'ordered_list_open') {
74
+ orderedStack.push(i)
75
+ continue
76
+ }
77
+ if (type === 'ordered_list_close') {
78
+ if (orderedStack.length > 0) {
79
+ listCloseByOpen[orderedStack.pop()] = i
80
+ }
81
+ continue
82
+ }
83
+ if (type === 'list_item_open') {
84
+ listItemStack.push(i)
85
+ continue
86
+ }
87
+ if (type === 'list_item_close') {
88
+ if (listItemStack.length > 0) {
89
+ listItemCloseByOpen[listItemStack.pop()] = i
90
+ }
91
+ }
92
+ }
93
+
94
+ return { listCloseByOpen, listItemCloseByOpen }
95
+ }
@@ -2,7 +2,7 @@
2
2
  // Converts bullet_list with **Term** pattern to description_list (dl/dt/dd)
3
3
  // This must run before Phase 1
4
4
 
5
- import { findMatchingClose, findListEnd as coreFindListEnd, findListItemEnd as coreFindListItemEnd } from './list-helpers.js'
5
+ import { findMatchingClose, findListEnd as coreFindListEnd } from './list-helpers.js'
6
6
 
7
7
  /**
8
8
  * Parse attribute string like ".class1 .class2 #id data-foo="bar""
@@ -73,11 +73,32 @@ const findListEnd = (tokens, startIndex) => {
73
73
  }
74
74
 
75
75
  /**
76
- * Find matching list_item close token
76
+ * Collect direct list_item ranges within a list in a single pass.
77
77
  */
78
- const findListItemEnd = (tokens, startIndex) => {
79
- const result = coreFindListItemEnd(tokens, startIndex)
80
- return result === -1 ? tokens.length - 1 : result
78
+ const collectListItemRanges = (tokens, listStart, listEnd) => {
79
+ const listToken = tokens[listStart]
80
+ if (!listToken) {
81
+ return []
82
+ }
83
+ const childLevel = (listToken.level ?? 0) + 1
84
+ const ranges = []
85
+ let currentOpen = -1
86
+
87
+ for (let i = listStart + 1; i < listEnd; i++) {
88
+ const token = tokens[i]
89
+ if (token.type === 'list_item_open' && token.level === childLevel) {
90
+ currentOpen = i
91
+ continue
92
+ }
93
+ if (token.type === 'list_item_close' && token.level === childLevel) {
94
+ if (currentOpen !== -1) {
95
+ ranges.push({ open: currentOpen, close: i })
96
+ currentOpen = -1
97
+ }
98
+ }
99
+ }
100
+
101
+ return ranges
81
102
  }
82
103
 
83
104
  /**
@@ -103,77 +124,72 @@ const findDDEnd = (tokens, startIndex) => {
103
124
  const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
104
125
  // First pass: validate all items match DL pattern
105
126
  let hasAnyDLItem = false
106
- let i = listStart + 1
107
-
108
- while (i < listEnd) {
109
- if (tokens[i].type === 'list_item_open') {
110
- const itemEnd = findListItemEnd(tokens, i)
111
-
112
- // Find first paragraph
113
- let firstPara = -1
114
- for (let j = i + 1; j < itemEnd; j++) {
115
- if (tokens[j].type === 'paragraph_open') {
116
- firstPara = j
117
- break
118
- }
127
+ const itemRanges = collectListItemRanges(tokens, listStart, listEnd)
128
+
129
+ for (const range of itemRanges) {
130
+ const itemStart = range.open
131
+ const itemEnd = range.close
132
+
133
+ // Find first paragraph
134
+ let firstPara = -1
135
+ for (let j = itemStart + 1; j < itemEnd; j++) {
136
+ if (tokens[j].type === 'paragraph_open') {
137
+ firstPara = j
138
+ break
119
139
  }
120
-
121
- if (firstPara !== -1) {
122
- const inlineToken = tokens[firstPara + 1]
123
- if (inlineToken && inlineToken.type === 'inline') {
124
- const dlCheck = isDLPattern(inlineToken.content)
125
- if (dlCheck.isMatch) {
126
- // Check if there's a description
127
- let hasDescription = false
128
-
129
- const afterStrong = dlCheck.afterStrong
130
-
131
- // Pattern 1: **Term** description (2+ spaces, including newlines)
132
- // Pattern 2: **Term**: description (colon)
133
- // Pattern 3: **Term**\ description (backslash escape)
134
- if (/^\s{2,}/.test(afterStrong) || /^\s*:/.test(afterStrong) || /^\\/.test(afterStrong)) {
135
- // Remove leading space/colon/backslash and check remaining text
136
- const cleaned = afterStrong.replace(/^[\s:]+/, '').replace(/^\\/, '').trim()
137
- if (cleaned) {
138
- hasDescription = true
139
- }
140
+ }
141
+
142
+ if (firstPara !== -1) {
143
+ const inlineToken = tokens[firstPara + 1]
144
+ if (inlineToken && inlineToken.type === 'inline') {
145
+ const dlCheck = isDLPattern(inlineToken.content)
146
+ if (dlCheck.isMatch) {
147
+ // Check if there's a description
148
+ let hasDescription = false
149
+
150
+ const afterStrong = dlCheck.afterStrong
151
+
152
+ // 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()
158
+ if (cleaned) {
159
+ hasDescription = true
140
160
  }
141
-
142
- // Pattern 4: Description in next paragraph (only **Term** in first para)
143
- if (!hasDescription) {
144
- // Check for additional paragraphs/lists
145
- for (let k = firstPara + 3; k < itemEnd; k++) {
146
- if (tokens[k].type === 'paragraph_open' ||
147
- tokens[k].type === 'bullet_list_open' ||
148
- tokens[k].type === 'ordered_list_open') {
149
- hasDescription = true
150
- break
151
- }
161
+ }
162
+
163
+ // Pattern 4: Description in next paragraph (only **Term** in first para)
164
+ if (!hasDescription) {
165
+ // Check for additional paragraphs/lists
166
+ for (let k = firstPara + 3; k < itemEnd; k++) {
167
+ if (tokens[k].type === 'paragraph_open' ||
168
+ tokens[k].type === 'bullet_list_open' ||
169
+ tokens[k].type === 'ordered_list_open') {
170
+ hasDescription = true
171
+ break
152
172
  }
153
173
  }
154
-
155
- // If no description, not a DL item
156
- if (!hasDescription) {
157
- return false
158
- }
159
-
160
- hasAnyDLItem = true
161
- } else {
162
- // Not all items are DL pattern - not a description list
163
- return { nextIndex: listEnd + 1 }
164
174
  }
175
+
176
+ // If no description, not a DL item
177
+ if (!hasDescription) {
178
+ return false
179
+ }
180
+
181
+ hasAnyDLItem = true
182
+ } else {
183
+ // Not all items are DL pattern - not a description list
184
+ return { nextIndex: listEnd + 1 }
165
185
  }
166
186
  }
167
-
168
- i = itemEnd + 1
169
- } else {
170
- i++
171
187
  }
172
188
  }
173
189
 
174
190
  // If valid DL, convert immediately (avoid re-scanning)
175
191
  if (hasAnyDLItem) {
176
- convertBulletListToDL(tokens, listStart, listEnd, opt)
192
+ convertBulletListToDL(tokens, listStart, listEnd, opt, itemRanges)
177
193
  // After conversion, tokens are replaced - continue from original listEnd position
178
194
  // Note: convertBulletListToDL may change token count, but we use original listEnd
179
195
  return { nextIndex: listStart + 1 } // Re-check from start since tokens changed
@@ -212,7 +228,7 @@ const isDLPattern = (content) => {
212
228
  /**
213
229
  * Convert bullet_list to dl/dt/dd structure using dl_open/dl_close tokens
214
230
  */
215
- const convertBulletListToDL = (tokens, listStart, listEnd, opt) => {
231
+ const convertBulletListToDL = (tokens, listStart, listEnd, opt, itemRanges = null) => {
216
232
  const newTokens = []
217
233
  const listLevel = tokens[listStart].level
218
234
 
@@ -242,33 +258,31 @@ const convertBulletListToDL = (tokens, listStart, listEnd, opt) => {
242
258
  const listAttrsFromItems = []
243
259
 
244
260
  // Process each list_item
245
- let i = listStart + 1
246
- while (i < listEnd) {
247
- if (tokens[i].type === 'list_item_open') {
248
- const itemEnd = findListItemEnd(tokens, i)
249
- const result = convertListItemToDtDd(tokens, i, itemEnd, listLevel, opt)
250
-
251
- // Update metadata
252
- dlOpen._dlMetadata.itemCount++
253
-
254
- // Check for list-level attrs returned from item
255
- if (result.listAttrs && result.listAttrs.length > 0) {
256
- listAttrsFromItems.push(...result.listAttrs)
257
- }
258
-
259
- if (result.tokens) {
260
- // Track last dd_open position (relative to newTokens)
261
- for (let j = 0; j < result.tokens.length; j++) {
262
- if (result.tokens[j].type === 'dd_open') {
263
- dlOpen._dlMetadata.lastDdTokenIndex = newTokens.length + j
264
- }
261
+ const ranges = Array.isArray(itemRanges) && itemRanges.length > 0
262
+ ? itemRanges
263
+ : collectListItemRanges(tokens, listStart, listEnd)
264
+
265
+ for (const range of ranges) {
266
+ const itemStart = range.open
267
+ const itemEnd = range.close
268
+ const result = convertListItemToDtDd(tokens, itemStart, itemEnd, listLevel, opt)
269
+
270
+ // Update metadata
271
+ dlOpen._dlMetadata.itemCount++
272
+
273
+ // Check for list-level attrs returned from item
274
+ if (result.listAttrs && result.listAttrs.length > 0) {
275
+ listAttrsFromItems.push(...result.listAttrs)
276
+ }
277
+
278
+ if (result.tokens) {
279
+ // Track last dd_open position (relative to newTokens)
280
+ for (let j = 0; j < result.tokens.length; j++) {
281
+ if (result.tokens[j].type === 'dd_open') {
282
+ dlOpen._dlMetadata.lastDdTokenIndex = newTokens.length + j
265
283
  }
266
- newTokens.push(...result.tokens)
267
284
  }
268
-
269
- i = itemEnd + 1
270
- } else {
271
- i++
285
+ newTokens.push(...result.tokens)
272
286
  }
273
287
  }
274
288
 
@@ -1,7 +1,7 @@
1
1
  // Phase 1: List Structure Analysis and Marker Detection
2
2
  // Analyze only, no token conversion
3
- import { detectMarkerType } from './types-utility.js'
4
- import { findMatchingClose } from './list-helpers.js'
3
+ import { detectMarkerType, detectMarkerTypeWithContext, detectSequencePattern } from './types-utility.js'
4
+ import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
5
5
 
6
6
  /**
7
7
  * Pre-compute DL scope (identify all DL ranges in O(n))
@@ -74,7 +74,7 @@ function isInsideDL(index, dlState) {
74
74
  */
75
75
  export function analyzeListStructure(tokens, opt) {
76
76
  const listInfos = []
77
- const processed = new Set()
77
+ const closeMap = buildListCloseIndexMap(tokens)
78
78
 
79
79
  // Check DL existence (O(n) but optimized with early return)
80
80
  let hasDL = false
@@ -92,12 +92,11 @@ export function analyzeListStructure(tokens, opt) {
92
92
  // Process only top-level lists (nested lists collected recursively)
93
93
  // Also process lists inside DL
94
94
  for (let i = 0; i < tokens.length; i++) {
95
- if (processed.has(i)) {
95
+ const token = tokens[i]
96
+ if (!token) {
96
97
  continue
97
98
  }
98
99
 
99
- const token = tokens[i]
100
-
101
100
  // Process level-0 lists or lists at any level inside DD
102
101
  const isTopLevelList = (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') &&
103
102
  (token.level === 0 || token.level === undefined)
@@ -106,16 +105,13 @@ export function analyzeListStructure(tokens, opt) {
106
105
  isInsideDL(i, dlScope)
107
106
 
108
107
  if (isTopLevelList || isListInDD) {
109
- const listInfo = analyzeList(tokens, i, opt)
108
+ const listInfo = analyzeList(tokens, i, opt, closeMap)
110
109
  if (listInfo) {
111
110
  listInfos.push(listInfo)
112
- // Mark this list range as processed
113
- for (let j = listInfo.startIndex; j <= listInfo.endIndex; j++) {
114
- processed.add(j)
115
- }
116
111
 
117
112
  // Recursively collect nested lists
118
113
  collectNestedLists(listInfo, listInfos)
114
+ i = listInfo.endIndex
119
115
  }
120
116
  }
121
117
  }
@@ -143,9 +139,9 @@ function collectNestedLists(listInfo, listInfos) {
143
139
  /**
144
140
  * Analyze detailed information of a single list
145
141
  */
146
- function analyzeList(tokens, startIndex, opt) {
142
+ function analyzeList(tokens, startIndex, opt, closeMap) {
147
143
  const listToken = tokens[startIndex]
148
- const endIndex = findListEnd(tokens, startIndex)
144
+ const endIndex = findListEnd(tokens, startIndex, closeMap)
149
145
 
150
146
  if (endIndex === -1) {
151
147
  return null
@@ -153,7 +149,7 @@ function analyzeList(tokens, startIndex, opt) {
153
149
 
154
150
  const level = listToken.level || 0
155
151
  const originalType = listToken.type
156
- const items = analyzeListItems(tokens, startIndex, endIndex, opt)
152
+ const items = analyzeListItems(tokens, startIndex, endIndex, opt, closeMap)
157
153
  const isLoose = detectLooseList(tokens, startIndex, endIndex, items)
158
154
  if (!isLoose) {
159
155
  hideFirstParagraphsForTightList(tokens, items, level)
@@ -188,7 +184,7 @@ function analyzeList(tokens, startIndex, opt) {
188
184
  /**
189
185
  * Analyze list items
190
186
  */
191
- function analyzeListItems(tokens, startIndex, endIndex, opt) {
187
+ function analyzeListItems(tokens, startIndex, endIndex, opt, closeMap) {
192
188
  const items = []
193
189
  let i = startIndex + 1
194
190
 
@@ -196,8 +192,8 @@ function analyzeListItems(tokens, startIndex, endIndex, opt) {
196
192
  const token = tokens[i]
197
193
 
198
194
  if (token.type === 'list_item_open') {
199
- const itemEndIndex = findListItemEnd(tokens, i)
200
- const item = analyzeListItem(tokens, i, itemEndIndex, opt)
195
+ const itemEndIndex = findListItemEnd(tokens, i, closeMap)
196
+ const item = analyzeListItem(tokens, i, itemEndIndex, opt, closeMap)
201
197
  items.push(item)
202
198
  i = itemEndIndex + 1
203
199
  } else {
@@ -211,66 +207,59 @@ function analyzeListItems(tokens, startIndex, endIndex, opt) {
211
207
  /**
212
208
  * Analyze a single list item
213
209
  */
214
- function analyzeListItem(tokens, startIndex, endIndex, opt) {
210
+ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
215
211
  let content = ''
216
212
  let markerInfo = null
217
213
  let hasNestedList = false
218
214
  let nestedLists = []
219
215
  let firstParagraphIsLoose = false
216
+ let lastInlineContent = ''
220
217
 
221
218
  // Check if blank line exists right after first paragraph (only before child lists)
222
219
  // paragraph.hidden alone is insufficient: when parent list is loose, all paragraphs have hidden=false
223
220
  // Need to use map info to verify actual blank line after paragraph
224
- let nestedDepth = 0
225
221
  let foundParagraph = false
226
222
  let firstParagraphIndex = -1
223
+ let checkedFirstParagraphLoose = false
227
224
 
228
225
  for (let i = startIndex + 1; i < endIndex; i++) {
229
226
  const token = tokens[i]
230
227
 
228
+ if (token.type === 'inline' && token.content) {
229
+ lastInlineContent = token.content
230
+ }
231
+
232
+ if (!checkedFirstParagraphLoose && token.type === 'paragraph_open' && !foundParagraph) {
233
+ foundParagraph = true
234
+ firstParagraphIndex = i
235
+ }
236
+
231
237
  if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
232
- // When child list starts, check if blank line exists right after first paragraph
233
- if (foundParagraph && firstParagraphIndex !== -1) {
238
+ if (!checkedFirstParagraphLoose && foundParagraph && firstParagraphIndex !== -1) {
234
239
  const paragraphToken = tokens[firstParagraphIndex]
235
240
  const paragraphEndLine = paragraphToken.map ? paragraphToken.map[1] : undefined
236
241
  const nestedListStartLine = token.map
237
242
  ? token.map[0]
238
243
  : (typeof token._literalStartLine === 'number' ? token._literalStartLine : undefined)
239
244
  if (typeof paragraphEndLine === 'number' && typeof nestedListStartLine === 'number') {
240
- // Check if blank line exists between paragraph end and child list start
241
245
  if (nestedListStartLine > paragraphEndLine) {
242
246
  firstParagraphIsLoose = true
243
247
  }
244
248
  }
245
- break
246
249
  }
247
- nestedDepth++
248
- } else if (token.type === 'bullet_list_close' || token.type === 'ordered_list_close') {
249
- nestedDepth--
250
- } else if (nestedDepth === 0 && token.type === 'paragraph_open') {
251
- // First paragraph outside nested lists
252
- foundParagraph = true
253
- firstParagraphIndex = i
254
- }
255
- }
256
-
257
- for (let i = startIndex + 1; i < endIndex; i++) {
258
- const token = tokens[i]
259
-
260
- if (token.type === 'inline' && token.content) {
261
- content = token.content
262
- // Detect marker
263
- markerInfo = detectMarkerType(content, opt)
264
- }
265
-
266
- if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
250
+ checkedFirstParagraphLoose = true
267
251
  hasNestedList = true
268
- const nestedListInfo = analyzeList(tokens, i, opt)
252
+ const nestedListInfo = analyzeList(tokens, i, opt, closeMap)
269
253
  nestedLists.push(nestedListInfo)
270
254
  i = nestedListInfo.endIndex
271
255
  }
272
256
  }
273
257
 
258
+ if (firstParagraphIsLoose && hasNestedList && lastInlineContent) {
259
+ markerInfo = detectMarkerType(lastInlineContent)
260
+ }
261
+ content = lastInlineContent
262
+
274
263
  return {
275
264
  startIndex,
276
265
  endIndex,
@@ -288,7 +277,6 @@ function analyzeListItem(tokens, startIndex, endIndex, opt) {
288
277
  function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
289
278
  const listToken = tokens[startIndex]
290
279
  const markers = []
291
- let level = 0
292
280
 
293
281
  // For ordered_list, get numbers from list_item_open's info
294
282
  if (listToken.type === 'ordered_list_open') {
@@ -314,23 +302,25 @@ function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
314
302
  // Target only direct children list_items of this list
315
303
  const targetLevel = (listToken.level || 0) + 3 // list_open(0) -> list_item(1) -> paragraph(2) -> inline(3)
316
304
 
317
- // First, collect all contents (for detecting iroha sequence etc.)
305
+ // Collect inline tokens and contents once (for detecting iroha sequence etc.)
306
+ const inlineTokens = []
318
307
  const allContents = []
319
308
  for (let i = startIndex + 1; i < endIndex; i++) {
320
309
  const token = tokens[i]
321
310
  if (token.type === 'inline' && token.content && token.level === targetLevel) {
311
+ inlineTokens.push(token)
322
312
  allContents.push(token.content)
323
313
  }
324
314
  }
325
315
 
316
+ const contextResult = allContents.length > 0 ? detectSequencePattern(allContents) : null
317
+
326
318
  // Detect markers using full context
327
319
  let sequentialNumber = 1 // Sequential number counter
328
- for (let i = startIndex + 1; i < endIndex; i++) {
329
- const token = tokens[i]
330
-
320
+ for (const token of inlineTokens) {
331
321
  // Process only inline tokens of direct child items of this list
332
322
  if (token.type === 'inline' && token.content && token.level === targetLevel) {
333
- const markerInfo = detectMarkerType(token.content, allContents)
323
+ const markerInfo = detectMarkerTypeWithContext(token.content, contextResult)
334
324
  if (markerInfo && markerInfo.type) {
335
325
  // Use sequential numbers when same marker continues
336
326
  // (e.g., "イ. イ. イ." → interpreted as "イ、ロ、ハ")
@@ -494,7 +484,11 @@ function hideFirstParagraphsForTightList(tokens, items, level) {
494
484
  /**
495
485
  * Find list end position
496
486
  */
497
- function findListEnd(tokens, startIndex) {
487
+ function findListEnd(tokens, startIndex, closeMap) {
488
+ const mapped = closeMap?.listCloseByOpen?.[startIndex]
489
+ if (typeof mapped === 'number' && mapped !== -1) {
490
+ return mapped
491
+ }
498
492
  const startToken = tokens[startIndex]
499
493
  const openType = startToken.type
500
494
  const closeType = openType.replace('_open', '_close')
@@ -505,7 +499,11 @@ function findListEnd(tokens, startIndex) {
505
499
  /**
506
500
  * Find list item end position
507
501
  */
508
- function findListItemEnd(tokens, startIndex) {
502
+ function findListItemEnd(tokens, startIndex, closeMap) {
503
+ const mapped = closeMap?.listItemCloseByOpen?.[startIndex]
504
+ if (typeof mapped === 'number' && mapped !== -1) {
505
+ return mapped
506
+ }
509
507
  const result = findMatchingClose(tokens, startIndex, 'list_item_open', 'list_item_close')
510
508
  return result === -1 ? tokens.length - 1 : result
511
509
  }