@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.2.3 → 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.
@@ -3,15 +3,145 @@
3
3
  import { detectMarkerType } from './types-utility.js'
4
4
  import { findMatchingClose, findListItemEnd } from './list-helpers.js'
5
5
 
6
- const INLINE_LITERAL_HINT = /\n[ \t]+\S/
7
- const CODE_LITERAL_HINT = /^[ \t]+\S/m
6
+ // markdown-it treats indent >= marker width + 4 as code blocks inside list items.
7
+ const MAX_LITERAL_INLINE_INDENT = 3
8
+ const LITERAL_SUFFIX_CHARS = new Set(['.', ')', '.', ')', '、'])
9
+ const LITERAL_OPEN_CLOSE_PAIRS = new Map([
10
+ ['(', ')'],
11
+ ['(', ')']
12
+ ])
13
+ const ASCII_ALNUM_OR_FULLWIDTH_DIGIT_REGEX = /^[A-Za-z0-90-9]+$/
14
+ const getIndentWidth = (indentText) => indentText.replace(/\t/g, ' ').length
15
+ const buildLineMap = (startLine, endLine = null) => {
16
+ if (typeof startLine !== 'number') {
17
+ return null
18
+ }
19
+ const normalizedEnd = typeof endLine === 'number' ? endLine + 1 : startLine + 1
20
+ return [startLine, normalizedEnd]
21
+ }
22
+
23
+ const getListItemMarkerWidth = (listItem) => {
24
+ if (!listItem) {
25
+ return 1
26
+ }
27
+ const info = typeof listItem.info === 'string' ? listItem.info : ''
28
+ const markup = typeof listItem.markup === 'string' ? listItem.markup : ''
29
+ const markerLength = info.length + markup.length
30
+ return markerLength > 0 ? markerLength + 1 : 1
31
+ }
32
+
33
+ const getLineTokenWithIndent = (line) => {
34
+ if (typeof line !== 'string' || line.length === 0) {
35
+ return null
36
+ }
37
+ const match = line.match(/^([ \t]*)(\S+)(?:\s|$)/)
38
+ if (!match) {
39
+ return null
40
+ }
41
+ return {
42
+ indentWidth: getIndentWidth(match[1]),
43
+ token: match[2]
44
+ }
45
+ }
46
+
47
+ const hasLikelyLiteralMarkerToken = (token) => {
48
+ if (typeof token !== 'string' || token.length === 0) {
49
+ return false
50
+ }
51
+
52
+ let core = token
53
+ let hasSuffix = false
54
+ const firstChar = core[0]
55
+ const closingPair = LITERAL_OPEN_CLOSE_PAIRS.get(firstChar)
56
+ if (closingPair) {
57
+ const closeIdx = core.indexOf(closingPair, 1)
58
+ if (closeIdx <= 1) {
59
+ return false
60
+ }
61
+ const inner = core.slice(1, closeIdx)
62
+ const rest = core.slice(closeIdx + 1)
63
+ if (rest.length > 1) {
64
+ return false
65
+ }
66
+ if (rest.length === 1) {
67
+ if (!LITERAL_SUFFIX_CHARS.has(rest)) {
68
+ return false
69
+ }
70
+ hasSuffix = true
71
+ }
72
+ core = inner
73
+ } else {
74
+ const tail = core[core.length - 1]
75
+ if (LITERAL_SUFFIX_CHARS.has(tail)) {
76
+ core = core.slice(0, -1)
77
+ hasSuffix = true
78
+ }
79
+ }
80
+
81
+ if (!core) {
82
+ return false
83
+ }
84
+
85
+ // For ASCII tokens, require explicit suffix to avoid treating normal words as list markers.
86
+ if (ASCII_ALNUM_OR_FULLWIDTH_DIGIT_REGEX.test(core)) {
87
+ return hasSuffix
88
+ }
89
+
90
+ // Non-ASCII marker cores (circled digits, kana, kanji, etc.) are typically short.
91
+ return core.length <= 4
92
+ }
93
+
94
+ const hasLikelyLiteralLineHint = (content) => {
95
+ if (typeof content !== 'string' || !content.includes('\n')) {
96
+ return false
97
+ }
98
+ const lines = content.split('\n')
99
+ for (let i = 1; i < lines.length; i++) {
100
+ const line = lines[i]
101
+ if (!line || line.trim().length === 0) {
102
+ continue
103
+ }
104
+ const tokenInfo = getLineTokenWithIndent(line)
105
+ if (!tokenInfo || tokenInfo.indentWidth > MAX_LITERAL_INLINE_INDENT) {
106
+ continue
107
+ }
108
+ if (hasLikelyLiteralMarkerToken(tokenInfo.token)) {
109
+ return true
110
+ }
111
+ }
112
+ return false
113
+ }
114
+
115
+ const hasOverIndentedMarkerLikeLine = (content) => {
116
+ if (typeof content !== 'string' || !content.includes('\n')) {
117
+ return false
118
+ }
119
+ const lines = content.split('\n')
120
+ for (let i = 1; i < lines.length; i++) {
121
+ const line = lines[i]
122
+ if (!line || line.trim().length === 0) {
123
+ continue
124
+ }
125
+ const tokenInfo = getLineTokenWithIndent(line)
126
+ if (!tokenInfo || tokenInfo.indentWidth <= MAX_LITERAL_INLINE_INDENT) {
127
+ continue
128
+ }
129
+ if (hasLikelyLiteralMarkerToken(tokenInfo.token)) {
130
+ return true
131
+ }
132
+ }
133
+ return false
134
+ }
8
135
 
9
136
  /**
10
137
  * Normalize literal nested ordered lists inside list items.
11
138
  * Converts indented numeric lines into proper ordered_list tokens before Phase 1.
12
139
  * @param {Array} tokens
13
140
  */
14
- export function normalizeLiteralOrderedLists(tokens) {
141
+ export function normalizeLiteralOrderedLists(tokens, opt) {
142
+ if (!opt?.enableLiteralNumberingFix) {
143
+ return
144
+ }
15
145
  if (!Array.isArray(tokens) || tokens.length === 0) {
16
146
  return
17
147
  }
@@ -21,278 +151,103 @@ export function normalizeLiteralOrderedLists(tokens) {
21
151
  }
22
152
 
23
153
  let hasInlineLiteralHint = false
24
- let hasCodeLiteralHint = false
25
154
  for (let i = 0; i < tokens.length; i++) {
26
155
  const token = tokens[i]
27
156
  if (!hasInlineLiteralHint &&
28
157
  token.type === 'inline' &&
29
158
  token.content &&
30
- token.content.includes('\n') &&
31
- INLINE_LITERAL_HINT.test(token.content)) {
159
+ token.content.includes('\n')) {
32
160
  hasInlineLiteralHint = true
33
- } else if (!hasCodeLiteralHint &&
34
- token.type === 'code_block' &&
35
- token.content &&
36
- CODE_LITERAL_HINT.test(token.content)) {
37
- hasCodeLiteralHint = true
38
- }
39
- if (hasInlineLiteralHint && hasCodeLiteralHint) {
40
161
  break
41
162
  }
42
163
  }
43
164
 
44
- if (!hasInlineLiteralHint && !hasCodeLiteralHint) {
165
+ if (!hasInlineLiteralHint) {
45
166
  return
46
167
  }
47
168
 
48
- if (hasInlineLiteralHint) {
49
- for (let i = 0; i < tokens.length; i++) {
50
- const token = tokens[i]
51
- if (token.type !== 'list_item_open') {
52
- continue
53
- }
54
- let listItemClose = findListItemEnd(tokens, i)
55
- if (listItemClose === -1) {
56
- listItemClose = tokens.length - 1
57
- }
58
-
59
- let j = i + 1
60
- while (j < listItemClose) {
61
- const current = tokens[j]
62
- if (current.type !== 'paragraph_open') {
169
+ for (let i = 0; i < tokens.length; i++) {
170
+ const token = tokens[i]
171
+ if (token.type !== 'list_item_open') {
172
+ continue
173
+ }
174
+ let listItemClose = findListItemEnd(tokens, i)
175
+ if (listItemClose === -1) {
176
+ listItemClose = tokens.length - 1
177
+ }
178
+
179
+ const markerWidth = getListItemMarkerWidth(tokens[i])
180
+ let j = i + 1
181
+ while (j < listItemClose) {
182
+ const current = tokens[j]
183
+ if (current.type === 'paragraph_open') {
184
+ const inlineIdx = j + 1
185
+ const paragraphCloseIdx = j + 2
186
+ if (inlineIdx >= listItemClose ||
187
+ paragraphCloseIdx >= tokens.length ||
188
+ tokens[inlineIdx].type !== 'inline' ||
189
+ tokens[paragraphCloseIdx].type !== 'paragraph_close') {
63
190
  j++
64
191
  continue
65
192
  }
66
193
 
67
- if (current.type === 'paragraph_open') {
68
- const inlineIdx = j + 1
69
- const paragraphCloseIdx = j + 2
70
- if (inlineIdx >= listItemClose ||
71
- paragraphCloseIdx >= tokens.length ||
72
- tokens[inlineIdx].type !== 'inline' ||
73
- tokens[paragraphCloseIdx].type !== 'paragraph_close') {
74
- j++
75
- continue
76
- }
77
-
78
- const inlineToken = tokens[inlineIdx]
79
- if (!INLINE_LITERAL_HINT.test(inlineToken.content)) {
80
- j = paragraphCloseIdx + 1
81
- continue
82
- }
83
- const baseLine = tokens[j].map ? tokens[j].map[0] : null
84
- const segments = parseSegments(inlineToken.content, baseLine)
85
- if (!segments.hasLiteral) {
86
- j = paragraphCloseIdx + 1
87
- continue
88
- }
89
-
90
- const listItemLevel = tokens[i].level ?? 0
91
- const { tokens: replacementTokens, literalListPositions } = buildReplacementTokens(
92
- segments.list,
93
- listItemLevel,
94
- TokenClass,
95
- tokens[j],
96
- tokens[inlineIdx],
97
- tokens[paragraphCloseIdx]
98
- )
99
-
100
- const originalLength = paragraphCloseIdx - j + 1
101
- tokens.splice(j, originalLength, ...replacementTokens)
102
-
103
- const delta = replacementTokens.length - originalLength
104
- listItemClose += delta
105
-
106
- let mergeDelta = 0
107
- for (const info of literalListPositions) {
108
- const absoluteIdx = j + info.relativeIndex
109
- mergeDelta += mergeFollowingLists(tokens, absoluteIdx)
110
- }
111
- listItemClose += mergeDelta
112
-
113
- j = j + replacementTokens.length
194
+ const inlineToken = tokens[inlineIdx]
195
+ if (!inlineToken.content.includes('\n')) {
196
+ j = paragraphCloseIdx + 1
114
197
  continue
115
198
  }
116
-
117
- if (current.type === 'ordered_list_open' &&
118
- current.level === (tokens[i].level ?? 0) + 1) {
119
- const delta = splitOrderedListForLiteralChildren(tokens, j, TokenClass)
120
- listItemClose += delta
121
- j++
199
+ if (!hasLikelyLiteralLineHint(inlineToken.content)) {
200
+ j = paragraphCloseIdx + 1
201
+ continue
202
+ }
203
+ // Be conservative when deeply-indented marker-like lines are present.
204
+ // These lines are often code blocks; partial literal conversion is more surprising
205
+ // than preserving markdown-it's original rendering in this ambiguous case.
206
+ if (hasOverIndentedMarkerLikeLine(inlineToken.content)) {
207
+ j = paragraphCloseIdx + 1
208
+ continue
209
+ }
210
+ const baseLine = tokens[j].map ? tokens[j].map[0] : null
211
+ const segments = parseSegments(inlineToken.content, markerWidth, baseLine)
212
+ if (!segments.hasLiteral) {
213
+ j = paragraphCloseIdx + 1
122
214
  continue
123
215
  }
124
216
 
125
- j++
126
- }
127
- i = listItemClose
128
- }
129
- }
130
- if (hasCodeLiteralHint) {
131
- convertLiteralCodeBlocks(tokens, TokenClass)
132
- }
133
- }
134
-
135
- function convertLiteralCodeBlocks(tokens, TokenClass) {
136
- if (!TokenClass) {
137
- return
138
- }
139
- for (let i = 0; i < tokens.length; i++) {
140
- const token = tokens[i]
141
- if (!token || token.type !== 'code_block') {
142
- continue
143
- }
144
- if (token.level === undefined || token.level < 1) {
145
- continue
146
- }
147
- if (!CODE_LITERAL_HINT.test(token.content)) {
148
- continue
149
- }
150
- const rawLines = token.content.split(/\r?\n/)
151
- const literalCache = []
152
- const baseLine = Array.isArray(token.map) ? token.map[0] : null
153
- const { lists, nextIndex } = parseLiteralBlock(rawLines, 0, literalCache, baseLine)
154
- const hasRemainingContent = rawLines.slice(nextIndex).some(line => line.trim().length > 0)
155
- if (!Array.isArray(lists) || lists.length === 0 || hasRemainingContent) {
156
- if (tryConvertCodeBlockContinuation(tokens, i, TokenClass)) {
157
- i--
158
- }
159
- continue
160
- }
161
-
162
- const targetItemLevel = token.level + 1
163
- let listItemIdx = -1
164
- for (let k = i - 1; k >= 0; k--) {
165
- const tk = tokens[k]
166
- if (tk.type === 'list_item_open' && tk.level === targetItemLevel) {
167
- listItemIdx = k
168
- break
169
- }
170
- }
171
- if (listItemIdx === -1) {
172
- continue
173
- }
174
- let listItemCloseIdx = findMatchingClose(tokens, listItemIdx, 'list_item_open', 'list_item_close')
175
- if (listItemCloseIdx === -1) {
176
- continue
177
- }
178
-
179
- tokens.splice(i, 1)
180
- if (listItemCloseIdx > i) {
181
- listItemCloseIdx--
182
- }
183
- revealFirstParagraph(tokens, listItemIdx, listItemCloseIdx)
184
-
185
- const insertionStart = listItemCloseIdx
186
- const replacementTokens = []
187
- const newListLevel = targetItemLevel + 1
188
- for (const listNode of lists) {
189
- replacementTokens.push(...buildListTokens(listNode, newListLevel, TokenClass))
190
- }
191
- tokens.splice(insertionStart, 0, ...replacementTokens)
217
+ const listItemLevel = tokens[i].level ?? 0
218
+ const { tokens: replacementTokens, literalListPositions } = buildReplacementTokens(
219
+ segments.list,
220
+ listItemLevel,
221
+ TokenClass,
222
+ tokens[j],
223
+ tokens[inlineIdx],
224
+ tokens[paragraphCloseIdx]
225
+ )
226
+
227
+ const originalLength = paragraphCloseIdx - j + 1
228
+ tokens.splice(j, originalLength, ...replacementTokens)
229
+
230
+ const delta = replacementTokens.length - originalLength
231
+ listItemClose += delta
232
+
233
+ let mergeDelta = 0
234
+ for (const info of literalListPositions) {
235
+ const absoluteIdx = j + info.relativeIndex
236
+ mergeDelta += mergeFollowingLists(tokens, absoluteIdx)
237
+ }
238
+ listItemClose += mergeDelta
192
239
 
193
- for (let offset = 0; offset < replacementTokens.length; offset++) {
194
- if (replacementTokens[offset]?._literalList) {
195
- mergeFollowingLists(tokens, insertionStart + offset)
240
+ j = j + replacementTokens.length
241
+ continue
196
242
  }
197
- }
198
- i = insertionStart + replacementTokens.length - 1
199
- }
200
- }
201
243
 
202
- function tryConvertCodeBlockContinuation(tokens, codeBlockIndex, TokenClass) {
203
- const codeToken = tokens[codeBlockIndex]
204
- const targetLevel = (codeToken.level ?? 0) + 1
205
- let listItemCloseIdx = -1
206
- for (let k = codeBlockIndex - 1; k >= 0; k--) {
207
- const tk = tokens[k]
208
- if (tk.type === 'list_item_close' && tk.level === targetLevel) {
209
- listItemCloseIdx = k
210
- break
211
- }
212
- if (tk.type === 'list_item_open' && tk.level <= codeToken.level) {
213
- break
214
- }
215
- }
216
- if (listItemCloseIdx === -1) {
217
- return false
218
- }
219
- let listItemOpenIdx = -1
220
- let depth = 0
221
- for (let k = listItemCloseIdx; k >= 0; k--) {
222
- const tk = tokens[k]
223
- if (tk.type === 'list_item_close' && tk.level === targetLevel) {
224
- depth++
225
- continue
226
- }
227
- if (tk.type === 'list_item_open' && tk.level === targetLevel) {
228
- depth--
229
- if (depth === 0) {
230
- listItemOpenIdx = k
231
- break
232
- }
233
- continue
244
+ j++
234
245
  }
235
- if (tk.type === 'list_item_open' && tk.level <= codeToken.level) {
236
- break
237
- }
238
- }
239
- if (listItemOpenIdx === -1) {
240
- return false
241
- }
242
- revealFirstParagraph(tokens, listItemOpenIdx, listItemCloseIdx)
243
- const paragraphLevel = (tokens[listItemOpenIdx].level ?? 0) + 1
244
- const paragraphs = buildParagraphTokensFromCodeBlock(codeToken.content, paragraphLevel, TokenClass)
245
- if (paragraphs.length === 0) {
246
- return false
247
- }
248
- tokens.splice(listItemCloseIdx, 0, ...paragraphs)
249
- const newIndex = codeBlockIndex + paragraphs.length
250
- tokens.splice(newIndex, 1)
251
- return true
252
- }
253
-
254
- function buildParagraphTokensFromCodeBlock(content, paragraphLevel, TokenClass) {
255
- if (!content) {
256
- return []
257
- }
258
- const normalized = content.replace(/\r\n?/g, '\n')
259
- const lines = normalized.split('\n')
260
- const nonEmpty = lines.filter(line => line.trim().length > 0)
261
- if (nonEmpty.length === 0) {
262
- return []
263
- }
264
- const indent = Math.min(...nonEmpty.map(line => line.match(/^\s*/)[0].length))
265
- const dedented = lines
266
- .map(line => (indent > 0 ? line.slice(Math.min(indent, line.length)) : line))
267
- .join('\n')
268
- const blocks = dedented.split(/\n{2,}/).map(block => block.trim()).filter(Boolean)
269
- if (blocks.length === 0) {
270
- return []
246
+ i = listItemClose
271
247
  }
272
- const paragraphs = []
273
- for (const block of blocks) {
274
- const pOpen = new TokenClass('paragraph_open', 'p', 1)
275
- pOpen.level = paragraphLevel
276
- pOpen.block = true
277
- pOpen.hidden = false
278
- paragraphs.push(pOpen)
279
-
280
- const inline = new TokenClass('inline', '', 0)
281
- inline.level = paragraphLevel + 1
282
- inline.content = block
283
- inline.children = []
284
- paragraphs.push(inline)
285
-
286
- const pClose = new TokenClass('paragraph_close', 'p', -1)
287
- pClose.level = paragraphLevel
288
- pClose.block = true
289
- pClose.hidden = false
290
- paragraphs.push(pClose)
291
- }
292
- return paragraphs
293
248
  }
294
249
 
295
- function parseSegments(content, baseLine = null) {
250
+ function parseSegments(content, markerWidth, baseLine = null) {
296
251
  if (!content) {
297
252
  return { hasLiteral: false, list: [{ type: 'text', text: '', tight: false }] }
298
253
  }
@@ -318,14 +273,21 @@ function parseSegments(content, baseLine = null) {
318
273
  }
319
274
  }
320
275
  const textValue = buffer.join('\n')
321
- const hasBlankLine = hadBlankLine
322
- segments.push({ type: 'text', text: textValue, tight: !hasBlankLine })
276
+ segments.push({ type: 'text', text: textValue, tight: !hadBlankLine })
323
277
  buffer = []
324
278
  blankLinesInBuffer = 0
325
279
  }
326
280
 
327
281
  while (idx < lines.length) {
328
- const literalInfo = getLiteralInfo(lines, idx, literalCache)
282
+ const isFirstLine = idx === 0
283
+ const trimmedLine = lines[idx].trim()
284
+ if (isFirstLine && trimmedLine.length > 0) {
285
+ buffer.push(lines[idx])
286
+ idx++
287
+ continue
288
+ }
289
+
290
+ const literalInfo = getLiteralInfo(lines, idx, literalCache, markerWidth)
329
291
  if (!literalInfo) {
330
292
  buffer.push(lines[idx])
331
293
  if (lines[idx].trim().length === 0) {
@@ -337,7 +299,7 @@ function parseSegments(content, baseLine = null) {
337
299
 
338
300
  hasLiteral = true
339
301
  flushBuffer({ trimTrailing: true })
340
- const { lists, nextIndex } = parseLiteralBlock(lines, idx, literalCache, baseLine)
302
+ const { lists, nextIndex } = parseLiteralBlock(lines, idx, literalCache, markerWidth, baseLine)
341
303
  if (lists.length > 0) {
342
304
  segments.push({ type: 'literal', lists })
343
305
  }
@@ -348,20 +310,20 @@ function parseSegments(content, baseLine = null) {
348
310
  return { hasLiteral, list: segments }
349
311
  }
350
312
 
351
- function detectLiteralLine(line) {
313
+ function detectLiteralLine(line, markerWidth) {
352
314
  if (!line) return null
353
315
  if (/^\s*$/.test(line)) {
354
316
  return null
355
317
  }
356
- const match = line.match(/^([ \t]+)(.*)$/)
318
+ const match = line.match(/^([ \t]*)(\S.*)$/)
357
319
  if (!match) {
358
320
  return null
359
321
  }
360
- const indentWidth = match[1].replace(/\t/g, ' ').length
361
- if (indentWidth < 1) {
322
+ const indentWidth = getIndentWidth(match[1])
323
+ if (indentWidth > MAX_LITERAL_INLINE_INDENT) {
362
324
  return null
363
325
  }
364
- const trimmed = match[2].trimStart()
326
+ const trimmed = match[2]
365
327
  if (!trimmed) {
366
328
  return null
367
329
  }
@@ -373,14 +335,15 @@ function detectLiteralLine(line) {
373
335
  return null
374
336
  }
375
337
  const remainder = trimmed.slice(markerInfo.marker.length).replace(/^\s+/, '')
338
+ const safeMarkerWidth = Number.isFinite(markerWidth) ? markerWidth : 1
376
339
  return {
377
- indent: indentWidth,
340
+ indent: safeMarkerWidth + indentWidth,
378
341
  markerInfo,
379
342
  content: remainder
380
343
  }
381
344
  }
382
345
 
383
- function parseLiteralBlock(lines, startIndex, literalCache = null, baseLine = null) {
346
+ function parseLiteralBlock(lines, startIndex, literalCache = null, markerWidth = 1, baseLine = null) {
384
347
  const rootLists = []
385
348
  const stack = []
386
349
  let idx = startIndex
@@ -399,7 +362,7 @@ function parseLiteralBlock(lines, startIndex, literalCache = null, baseLine = nu
399
362
  continue
400
363
  }
401
364
 
402
- const literalInfo = getLiteralInfo(lines, idx, literalCache)
365
+ const literalInfo = getLiteralInfo(lines, idx, literalCache, markerWidth)
403
366
  if (!literalInfo) {
404
367
  break
405
368
  }
@@ -436,13 +399,15 @@ function parseLiteralBlock(lines, startIndex, literalCache = null, baseLine = nu
436
399
  break
437
400
  }
438
401
 
402
+ const lineNumber = typeof baseLine === 'number' ? baseLine + idx : null
439
403
  currentList.items.push({
440
404
  markerInfo: literalInfo.markerInfo,
441
405
  content: literalInfo.content,
442
- children: []
406
+ children: [],
407
+ line: lineNumber
443
408
  })
444
- if (baseLine !== null) {
445
- currentList.lastLine = baseLine + idx
409
+ if (typeof lineNumber === 'number') {
410
+ currentList.lastLine = lineNumber
446
411
  }
447
412
  idx++
448
413
  }
@@ -450,12 +415,12 @@ function parseLiteralBlock(lines, startIndex, literalCache = null, baseLine = nu
450
415
  return { lists: rootLists, nextIndex: idx }
451
416
  }
452
417
 
453
- function getLiteralInfo(lines, index, cache = null) {
418
+ function getLiteralInfo(lines, index, cache = null, markerWidth = 1) {
454
419
  if (!cache) {
455
- return detectLiteralLine(lines[index])
420
+ return detectLiteralLine(lines[index], markerWidth)
456
421
  }
457
422
  if (cache[index] === undefined) {
458
- cache[index] = detectLiteralLine(lines[index]) || null
423
+ cache[index] = detectLiteralLine(lines[index], markerWidth) || null
459
424
  }
460
425
  return cache[index]
461
426
  }
@@ -478,6 +443,7 @@ function buildReplacementTokens(segments, listItemLevel, TokenClass, paragraphOp
478
443
  const tokens = []
479
444
  const literalListPositions = []
480
445
  let templateUsed = false
446
+ const paragraphMap = paragraphOpen?.map ? paragraphOpen.map.slice() : null
481
447
 
482
448
  for (const segment of segments) {
483
449
  if (segment.type === 'text') {
@@ -486,7 +452,7 @@ function buildReplacementTokens(segments, listItemLevel, TokenClass, paragraphOp
486
452
  continue
487
453
  }
488
454
  const template = !templateUsed ? { open: paragraphOpen, inline: inlineToken, close: paragraphClose } : null
489
- tokens.push(...createParagraphTokens(segment.text, listItemLevel, TokenClass, template, segment.tight))
455
+ tokens.push(...createParagraphTokens(segment.text, listItemLevel, TokenClass, template, segment.tight, paragraphMap))
490
456
  templateUsed = true
491
457
  } else if (segment.type === 'literal') {
492
458
  for (const listNode of segment.lists) {
@@ -501,7 +467,7 @@ function buildReplacementTokens(segments, listItemLevel, TokenClass, paragraphOp
501
467
  return { tokens, literalListPositions }
502
468
  }
503
469
 
504
- function createParagraphTokens(text, listItemLevel, TokenClass, template, forceTight = false) {
470
+ function createParagraphTokens(text, listItemLevel, TokenClass, template, forceTight = false, mapFallback = null) {
505
471
  if (!text) {
506
472
  return []
507
473
  }
@@ -510,6 +476,9 @@ function createParagraphTokens(text, listItemLevel, TokenClass, template, forceT
510
476
  const open = template ? cloneToken(template.open) : new TokenClass('paragraph_open', 'p', 1)
511
477
  open.level = level
512
478
  open.block = true
479
+ if (Array.isArray(mapFallback) && !open.map) {
480
+ open.map = mapFallback.slice()
481
+ }
513
482
  const baseHidden = template && typeof template.open?.hidden === 'boolean' ? template.open.hidden : false
514
483
  const shouldHide = baseHidden
515
484
  open.hidden = shouldHide
@@ -536,6 +505,9 @@ function createParagraphTokens(text, listItemLevel, TokenClass, template, forceT
536
505
  close.level = level
537
506
  close.block = true
538
507
  close.hidden = shouldHide
508
+ if (Array.isArray(mapFallback) && !close.map) {
509
+ close.map = mapFallback.slice()
510
+ }
539
511
  if (forceTight) {
540
512
  close._literalTight = true
541
513
  }
@@ -551,6 +523,47 @@ function buildListTokens(listNode, listLevel, TokenClass) {
551
523
  listOpen.markup = listNode.suffix || '.'
552
524
  listOpen.attrs = null
553
525
  listOpen._literalList = true
526
+ const listMap = buildLineMap(listNode.startLine, listNode.lastLine)
527
+ if (listMap) {
528
+ listOpen.map = listMap.slice()
529
+ }
530
+ if (Array.isArray(listNode.items) && listNode.items.length > 0) {
531
+ const markers = listNode.items
532
+ .map(item => {
533
+ if (!item.markerInfo) {
534
+ return null
535
+ }
536
+ const marker = { ...item.markerInfo }
537
+ if (typeof marker.originalNumber !== 'number' && typeof marker.number === 'number') {
538
+ marker.originalNumber = marker.number
539
+ }
540
+ return marker
541
+ })
542
+ .filter(Boolean)
543
+ if (markers.length === listNode.items.length) {
544
+ const firstType = markers[0].type
545
+ const isConsistent = markers.every(marker => marker.type === firstType)
546
+ const literalNumbers = markers.map(marker =>
547
+ typeof marker.originalNumber === 'number' ? marker.originalNumber : marker.number
548
+ )
549
+ const hasLiteralNumbers = literalNumbers.length === markers.length &&
550
+ literalNumbers.every(value => typeof value === 'number')
551
+ let allNumbersIdentical = false
552
+ if (hasLiteralNumbers) {
553
+ const firstLiteral = literalNumbers[0]
554
+ if (literalNumbers.every(value => value === firstLiteral) && firstLiteral === 1) {
555
+ allNumbersIdentical = true
556
+ }
557
+ }
558
+ listOpen._literalMarkerInfo = {
559
+ markers,
560
+ type: firstType,
561
+ isConsistent,
562
+ count: markers.length,
563
+ allNumbersIdentical
564
+ }
565
+ }
566
+ }
554
567
  if (typeof listNode.startLine === 'number') {
555
568
  listOpen._literalStartLine = listNode.startLine
556
569
  }
@@ -571,6 +584,9 @@ function buildListTokens(listNode, listLevel, TokenClass) {
571
584
  listClose.level = listLevel
572
585
  listClose.block = true
573
586
  listClose.markup = listNode.suffix || '.'
587
+ if (listMap) {
588
+ listClose.map = listMap.slice()
589
+ }
574
590
  tokens.push(listClose)
575
591
  return tokens
576
592
  }
@@ -583,6 +599,10 @@ function buildListItemTokens(item, listLevel, TokenClass, parentListIsLoose = fa
583
599
  liOpen.block = true
584
600
  liOpen.markup = item.markerInfo?.suffix || '.'
585
601
  liOpen.info = item.markerInfo?.number !== undefined ? String(item.markerInfo.number) : ''
602
+ const itemMap = buildLineMap(item.line, item.line)
603
+ if (itemMap) {
604
+ liOpen.map = itemMap.slice()
605
+ }
586
606
  tokens.push(liOpen)
587
607
 
588
608
  const paragraphLevel = itemLevel + 1
@@ -593,6 +613,9 @@ function buildListItemTokens(item, listLevel, TokenClass, parentListIsLoose = fa
593
613
  if (!parentListIsLoose) {
594
614
  pOpen._literalTight = true
595
615
  }
616
+ if (itemMap) {
617
+ pOpen.map = itemMap.slice()
618
+ }
596
619
  tokens.push(pOpen)
597
620
 
598
621
  const inline = new TokenClass('inline', '', 0)
@@ -608,6 +631,9 @@ function buildListItemTokens(item, listLevel, TokenClass, parentListIsLoose = fa
608
631
  if (!parentListIsLoose) {
609
632
  pClose._literalTight = true
610
633
  }
634
+ if (itemMap) {
635
+ pClose.map = itemMap.slice()
636
+ }
611
637
  tokens.push(pClose)
612
638
 
613
639
  if (item.children && item.children.length > 0) {
@@ -619,6 +645,9 @@ function buildListItemTokens(item, listLevel, TokenClass, parentListIsLoose = fa
619
645
  const liClose = new TokenClass('list_item_close', 'li', -1)
620
646
  liClose.level = itemLevel
621
647
  liClose.block = true
648
+ if (itemMap) {
649
+ liClose.map = itemMap.slice()
650
+ }
622
651
  tokens.push(liClose)
623
652
  return tokens
624
653
  }
@@ -642,22 +671,6 @@ function cloneToken(token) {
642
671
  return cloned
643
672
  }
644
673
 
645
- function revealFirstParagraph(tokens, listItemOpenIdx, listItemCloseIdx) {
646
- const paragraphLevel = (tokens[listItemOpenIdx].level ?? 0) + 1
647
- for (let k = listItemOpenIdx + 1; k < listItemCloseIdx; k++) {
648
- if (tokens[k].type === 'paragraph_open' && tokens[k].level === paragraphLevel) {
649
- tokens[k].hidden = false
650
- for (let m = k + 1; m < listItemCloseIdx; m++) {
651
- if (tokens[m].type === 'paragraph_close' && tokens[m].level === paragraphLevel) {
652
- tokens[m].hidden = false
653
- break
654
- }
655
- }
656
- break
657
- }
658
- }
659
- }
660
-
661
674
  function mergeFollowingLists(tokens, listOpenIndex) {
662
675
  if (listOpenIndex < 0 || listOpenIndex >= tokens.length) {
663
676
  return 0
@@ -737,83 +750,3 @@ function markLiteralListLoose(tokens, listOpenIndex, listCloseIndex = null) {
737
750
  }
738
751
  }
739
752
  }
740
-
741
- function splitOrderedListForLiteralChildren(tokens, listOpenIndex, TokenClass) {
742
- const listToken = tokens[listOpenIndex]
743
- const listCloseIndex = findMatchingClose(tokens, listOpenIndex, 'ordered_list_open', 'ordered_list_close')
744
- if (listCloseIndex === -1) {
745
- return 0
746
- }
747
- const childLevel = (listToken.level ?? 0) + 1
748
- const ranges = []
749
- let currentOpen = -1
750
- for (let idx = listOpenIndex + 1; idx < listCloseIndex; idx++) {
751
- const token = tokens[idx]
752
- if (token.type === 'list_item_open' && token.level === childLevel) {
753
- currentOpen = idx
754
- continue
755
- }
756
- if (token.type === 'list_item_close' && token.level === childLevel) {
757
- if (currentOpen !== -1) {
758
- ranges.push({ open: currentOpen, close: idx })
759
- currentOpen = -1
760
- }
761
- }
762
- }
763
-
764
- if (ranges.length <= 1) {
765
- return 0
766
- }
767
-
768
- const extraRanges = ranges.slice(1)
769
- const childTokens = []
770
- let removedCount = 0
771
-
772
- for (const range of extraRanges) {
773
- childTokens.push(...tokens.slice(range.open, range.close + 1))
774
- }
775
-
776
- for (let k = extraRanges.length - 1; k >= 0; k--) {
777
- const range = extraRanges[k]
778
- const len = range.close - range.open + 1
779
- tokens.splice(range.open, len)
780
- removedCount += len
781
- }
782
-
783
- if (listToken._markerInfo) {
784
- delete listToken._markerInfo
785
- }
786
-
787
- const levelShift = 2
788
- for (const token of childTokens) {
789
- if (typeof token.level === 'number') {
790
- token.level += levelShift
791
- }
792
- }
793
-
794
- const nestedListOpen = new TokenClass('ordered_list_open', 'ol', 1)
795
- nestedListOpen.level = (listToken.level ?? 0) + 2
796
- nestedListOpen.block = true
797
- nestedListOpen.markup = listToken.markup
798
- nestedListOpen.attrs = null
799
- nestedListOpen._literalList = true
800
-
801
- const firstChild = childTokens.find(t => t.type === 'list_item_open')
802
- if (firstChild?.info) {
803
- const num = parseInt(firstChild.info, 10)
804
- if (!Number.isNaN(num) && num !== 1) {
805
- nestedListOpen.attrs = [['start', String(num)]]
806
- }
807
- }
808
-
809
- const nestedListClose = new TokenClass('ordered_list_close', 'ol', -1)
810
- nestedListClose.level = nestedListOpen.level
811
- nestedListClose.block = true
812
- nestedListClose.markup = listToken.markup
813
-
814
- const insertionIndex = ranges[0].close
815
- tokens.splice(insertionIndex, 0, nestedListOpen, ...childTokens, nestedListClose)
816
-
817
- const addedCount = childTokens.length + 2
818
- return addedCount - removedCount
819
- }