@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.
@@ -8,39 +8,152 @@ import { findMatchingClose, findListEnd as coreFindListEnd } from './list-helper
8
8
  * Parse attribute string like ".class1 .class2 #id data-foo="bar""
9
9
  * Returns array of [key, value] pairs
10
10
  */
11
+ const ATTR_TOKEN_REGEX = /\s*(\.[\w-]+|#[\w-]+|[\w:-]+=(?:"[^"]*"|'[^']*'|[^\s"'{}]+)|[\w:-]+)/y
12
+ const ATTR_KEY_VALUE_REGEX = /^([\w:-]+)=(?:"([^"]*)"|'([^']*)'|([^\s"'{}]+))$/
13
+ const LEADING_ATTR_BLOCK_REGEX = /^[ \t]*\{([^}]+)\}/
14
+ const STANDALONE_ATTR_BLOCK_REGEX = /^\{([^}]+)\}$/
15
+ const TRAILING_TWO_SPACES_REGEX = / {2,}$/
16
+ const TRAILING_BACKSLASH_BREAK_REGEX = /\\\s*$/
17
+
11
18
  const parseAttrString = (attrStr) => {
19
+ if (!attrStr || typeof attrStr !== 'string') {
20
+ return []
21
+ }
22
+
12
23
  const attrs = []
13
- const classMatches = attrStr.match(/\.[\w-]+/g)
14
- const idMatch = attrStr.match(/#([\w-]+)/)
15
- const dataMatches = attrStr.match(/([\w-]+)="([^"]+)"/g)
16
-
17
- // Collect all classes
18
- if (classMatches) {
19
- const classes = classMatches.map(c => c.substring(1)).join(' ')
20
- attrs.push(['class', classes])
24
+ const customAttrs = []
25
+ const classes = []
26
+ let id = null
27
+ let cursor = 0
28
+
29
+ while (cursor < attrStr.length) {
30
+ ATTR_TOKEN_REGEX.lastIndex = cursor
31
+ const match = ATTR_TOKEN_REGEX.exec(attrStr)
32
+ if (!match) {
33
+ // Allow only trailing whitespace; otherwise reject as invalid attr syntax.
34
+ if (/^\s*$/.test(attrStr.slice(cursor))) {
35
+ break
36
+ }
37
+ return []
38
+ }
39
+ const token = match[1]
40
+ cursor = ATTR_TOKEN_REGEX.lastIndex
41
+ if (token[0] === '.') {
42
+ const cls = token.slice(1)
43
+ if (cls) {
44
+ classes.push(cls)
45
+ }
46
+ continue
47
+ }
48
+ if (token[0] === '#') {
49
+ if (!id) {
50
+ id = token.slice(1)
51
+ }
52
+ continue
53
+ }
54
+
55
+ const kv = token.match(ATTR_KEY_VALUE_REGEX)
56
+ if (!kv) {
57
+ // markdown-it-attrs boolean attribute (e.g. {foo})
58
+ customAttrs.push([token, ''])
59
+ continue
60
+ }
61
+ const key = kv[1]
62
+ const value = kv[2] ?? kv[3] ?? kv[4] ?? ''
63
+ customAttrs.push([key, value])
21
64
  }
22
-
23
- // Add id
24
- if (idMatch) {
25
- attrs.push(['id', idMatch[1]])
65
+
66
+ if (classes.length > 0) {
67
+ attrs.push(['class', classes.join(' ')])
26
68
  }
27
-
28
- // Add data attributes
29
- if (dataMatches) {
30
- dataMatches.forEach(match => {
31
- const [, key, value] = match.match(/([\w-]+)="([^"]+)"/)
32
- attrs.push([key, value])
33
- })
69
+ if (id) {
70
+ attrs.push(['id', id])
34
71
  }
35
-
72
+ if (customAttrs.length > 0) {
73
+ attrs.push(...customAttrs)
74
+ }
75
+
36
76
  return attrs
37
77
  }
38
78
 
79
+ const consumeLeadingValidAttrBlocks = (text) => {
80
+ if (typeof text !== 'string' || text.length === 0) {
81
+ return { rest: text || '', attrs: [] }
82
+ }
83
+
84
+ let rest = text
85
+ const attrs = []
86
+ while (true) {
87
+ const match = rest.match(LEADING_ATTR_BLOCK_REGEX)
88
+ if (!match) {
89
+ break
90
+ }
91
+ const parsed = parseAttrString(match[1])
92
+ if (parsed.length === 0) {
93
+ break
94
+ }
95
+ attrs.push(...parsed)
96
+ rest = rest.slice(match[0].length)
97
+ }
98
+
99
+ return { rest, attrs }
100
+ }
101
+
102
+ const hasExplicitLineBreakMarker = (afterStrong) => {
103
+ if (typeof afterStrong !== 'string') {
104
+ return false
105
+ }
106
+ const lineBreakIndex = afterStrong.indexOf('\n')
107
+ if (lineBreakIndex === -1) {
108
+ return false
109
+ }
110
+ const firstLine = afterStrong.slice(0, lineBreakIndex)
111
+ return TRAILING_TWO_SPACES_REGEX.test(firstLine) || TRAILING_BACKSLASH_BREAK_REGEX.test(firstLine)
112
+ }
113
+
114
+ const getDescriptionAfterExplicitLineBreak = (afterStrong) => {
115
+ if (!hasExplicitLineBreakMarker(afterStrong)) {
116
+ return null
117
+ }
118
+ const lineBreakIndex = afterStrong.indexOf('\n')
119
+ return afterStrong.slice(lineBreakIndex + 1)
120
+ }
121
+
122
+ const hasMeaningfulDescriptionContent = (text) => {
123
+ if (typeof text !== 'string' || text.trim().length === 0) {
124
+ return false
125
+ }
126
+
127
+ const lines = text.split('\n')
128
+ const renderedLines = []
129
+ for (const line of lines) {
130
+ const trimmed = line.trim()
131
+ if (!trimmed) {
132
+ continue
133
+ }
134
+ const attrMatch = trimmed.match(STANDALONE_ATTR_BLOCK_REGEX)
135
+ if (attrMatch) {
136
+ const parsed = parseAttrString(attrMatch[1])
137
+ if (parsed.length > 0) {
138
+ continue
139
+ }
140
+ }
141
+ renderedLines.push(trimmed)
142
+ }
143
+ return renderedLines.join('\n').trim().length > 0
144
+ }
145
+
146
+ const copyMap = (target, source) => {
147
+ if (!target || !source || !Array.isArray(source.map)) {
148
+ return
149
+ }
150
+ target.map = source.map.slice()
151
+ }
152
+
39
153
  /**
40
154
  * Process description list patterns in tokens
41
155
  * @param {Array} tokens - Token array
42
156
  * @param {Object} opt - Options object
43
- ```
44
157
  */
45
158
  export const processDescriptionList = (tokens, opt) => {
46
159
  if (!opt.descriptionList && !opt.descriptionListWithDiv) {
@@ -54,7 +167,7 @@ export const processDescriptionList = (tokens, opt) => {
54
167
  if (tokens[i].type === 'bullet_list_open') {
55
168
  const listEnd = findListEnd(tokens, i)
56
169
  const dlCheck = checkAndConvertToDL(tokens, i, listEnd, opt)
57
- i = dlCheck.nextIndex
170
+ i = typeof dlCheck?.nextIndex === 'number' ? dlCheck.nextIndex : listEnd + 1
58
171
  } else {
59
172
  i++
60
173
  }
@@ -101,6 +214,21 @@ const collectListItemRanges = (tokens, listStart, listEnd) => {
101
214
  return ranges
102
215
  }
103
216
 
217
+ /**
218
+ * Return the first direct child token index of a list_item.
219
+ * Direct child means token.level === list_item.level + 1.
220
+ */
221
+ const findFirstDirectChildInListItem = (tokens, itemStart, itemEnd) => {
222
+ const itemLevel = tokens[itemStart]?.level ?? 0
223
+ const childLevel = itemLevel + 1
224
+ for (let i = itemStart + 1; i < itemEnd; i++) {
225
+ if (tokens[i].level === childLevel) {
226
+ return i
227
+ }
228
+ }
229
+ return -1
230
+ }
231
+
104
232
  /**
105
233
  * Find matching dl_close token
106
234
  */
@@ -130,60 +258,57 @@ const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
130
258
  const itemStart = range.open
131
259
  const itemEnd = range.close
132
260
 
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
139
- }
140
- }
141
-
142
- if (firstPara !== -1) {
261
+ const firstChild = findFirstDirectChildInListItem(tokens, itemStart, itemEnd)
262
+ if (firstChild !== -1 && tokens[firstChild].type === 'paragraph_open') {
263
+ const firstPara = firstChild
143
264
  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
160
- }
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
172
- }
173
- }
265
+ if (!inlineToken || inlineToken.type !== 'inline') {
266
+ return { nextIndex: listEnd + 1 }
267
+ }
268
+
269
+ const dlCheck = isDLPattern(inlineToken.content)
270
+ if (!dlCheck.isMatch) {
271
+ // Not all items are DL pattern - not a description list
272
+ return { nextIndex: listEnd + 1 }
273
+ }
274
+
275
+ // Check if there's a description
276
+ let hasDescription = false
277
+ const afterStrong = dlCheck.afterStrong
278
+
279
+ // Pattern 1: Explicit line-break marker after term line (` ` or `\`) and
280
+ // description text in the remaining first paragraph content.
281
+ const explicitBreakDescription = getDescriptionAfterExplicitLineBreak(afterStrong)
282
+ if (explicitBreakDescription !== null && hasMeaningfulDescriptionContent(explicitBreakDescription)) {
283
+ hasDescription = true
284
+ }
285
+
286
+ // Pattern 2: Description in next paragraph/list (term-only first paragraph).
287
+ if (!hasDescription) {
288
+ // Check for additional paragraphs/lists
289
+ const itemChildLevel = (tokens[itemStart]?.level ?? 0) + 1
290
+ for (let k = firstPara + 3; k < itemEnd; k++) {
291
+ if (tokens[k].level !== itemChildLevel) {
292
+ continue
174
293
  }
175
-
176
- // If no description, not a DL item
177
- if (!hasDescription) {
178
- return false
294
+ if (tokens[k].type === 'paragraph_open' ||
295
+ tokens[k].type === 'bullet_list_open' ||
296
+ tokens[k].type === 'ordered_list_open') {
297
+ hasDescription = true
298
+ break
179
299
  }
180
-
181
- hasAnyDLItem = true
182
- } else {
183
- // Not all items are DL pattern - not a description list
184
- return { nextIndex: listEnd + 1 }
185
300
  }
186
301
  }
302
+
303
+ // If no description, not a DL item
304
+ if (!hasDescription) {
305
+ return { nextIndex: listEnd + 1 }
306
+ }
307
+
308
+ hasAnyDLItem = true
309
+ } else {
310
+ // Any list_item that doesn't start with a paragraph cannot be a DL item.
311
+ return { nextIndex: listEnd + 1 }
187
312
  }
188
313
  }
189
314
 
@@ -211,16 +336,11 @@ const isDLPattern = (content) => {
211
336
 
212
337
  const afterStrong = match[2] // Text after closing **
213
338
 
214
- // Pattern 1: **Term** description (2+ spaces, including newlines)
215
- // Pattern 2: **Term**: description (colon)
216
- // Pattern 3: **Term**\ description (backslash escape)
217
- // Pattern 4: **Term** only (no content after)
218
- // Pattern 5: **Term** {.attrs} (markdown-it-attrs syntax, optionally with content after)
219
- const isMatch = /^\s{2,}/.test(afterStrong) ||
220
- /^\s*:/.test(afterStrong) ||
221
- /^\\/.test(afterStrong) ||
222
- /^\s*$/.test(afterStrong) ||
223
- /^\s*\{[^}]+\}/.test(afterStrong) // {.class} or {#id} etc
339
+ // Match only strict description-list starts:
340
+ // 1) explicit line-break marker after term line (` ` or `\`) + newline
341
+ // 2) term-only first line (optionally with valid attrs only), expecting next block as description
342
+ const { rest: afterAttrs } = consumeLeadingValidAttrBlocks(afterStrong)
343
+ const isMatch = hasExplicitLineBreakMarker(afterStrong) || /^\s*$/.test(afterAttrs)
224
344
 
225
345
  return { isMatch, afterStrong }
226
346
  }
@@ -236,6 +356,7 @@ const convertBulletListToDL = (tokens, listStart, listEnd, opt, itemRanges = nul
236
356
  const dlOpen = new tokens[listStart].constructor('dl_open', 'dl', 1)
237
357
  dlOpen.level = listLevel
238
358
  dlOpen.block = true
359
+ copyMap(dlOpen, tokens[listStart])
239
360
 
240
361
  // Copy attributes from bullet_list_open (e.g., {.class} from markdown-it-attrs)
241
362
  if (tokens[listStart].attrs && tokens[listStart].attrs.length > 0) {
@@ -295,6 +416,7 @@ const convertBulletListToDL = (tokens, listStart, listEnd, opt, itemRanges = nul
295
416
  const dlClose = new tokens[listStart].constructor('dl_close', 'dl', -1)
296
417
  dlClose.level = listLevel
297
418
  dlClose.block = true
419
+ copyMap(dlClose, tokens[listEnd])
298
420
  newTokens.push(dlClose)
299
421
 
300
422
  // Replace tokens
@@ -313,14 +435,10 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
313
435
  const listItemToken = tokens[itemStart]
314
436
  let dtAttrs = listItemToken.attrs ? [...listItemToken.attrs] : null
315
437
 
316
- // Find first paragraph
317
- let firstPara = -1
318
- for (let i = itemStart + 1; i < itemEnd; i++) {
319
- if (tokens[i].type === 'paragraph_open') {
320
- firstPara = i
321
- break
322
- }
323
- }
438
+ const firstChild = findFirstDirectChildInListItem(tokens, itemStart, itemEnd)
439
+ const firstPara = firstChild !== -1 && tokens[firstChild].type === 'paragraph_open'
440
+ ? firstChild
441
+ : -1
324
442
 
325
443
  if (firstPara === -1) return { tokens: result, listAttrs }
326
444
 
@@ -335,24 +453,19 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
335
453
  const match = content.match(/^\*\*(.*?)\*\*(.*)/s) // s flag for including newlines
336
454
 
337
455
  if (match) {
338
- term = match[1] // Keep spaces for now (will be processed by inline parser)
456
+ // Trim to avoid leading space when **Term** starts with whitespace (e.g. "** *term*")
457
+ term = match[1].trim()
339
458
  let afterStrong = match[2]
340
459
 
341
- // Extract {.attrs} from afterStrong if present (markdown-it-attrs hasn't processed yet)
342
- // Pattern A: Inline {.attrs} immediately after **Term** like **Term** {.class}
343
- const inlineAttrsMatch = afterStrong.match(/^\s*\{([^}]+)\}/)
344
- if (inlineAttrsMatch) {
345
- // Parse attributes manually
346
- const attrString = inlineAttrsMatch[1]
347
- const parsedAttrs = parseAttrString(attrString)
348
- if (parsedAttrs.length > 0) {
349
- if (!dtAttrs) {
350
- dtAttrs = []
351
- }
352
- dtAttrs.push(...parsedAttrs)
460
+ // Pattern A: one or more leading attr blocks right after **Term**.
461
+ // These map to <dt> like markdown-it-attrs trailing attrs on a block line.
462
+ const leadingAttrs = consumeLeadingValidAttrBlocks(afterStrong)
463
+ if (leadingAttrs.attrs.length > 0) {
464
+ if (!dtAttrs) {
465
+ dtAttrs = []
353
466
  }
354
- // Remove {.attrs} from afterStrong (including trailing newline if attrs-only line)
355
- afterStrong = afterStrong.replace(/^\s*\{[^}]+\}\s*/, '')
467
+ dtAttrs.push(...leadingAttrs.attrs)
468
+ afterStrong = leadingAttrs.rest
356
469
  }
357
470
 
358
471
  // Pattern B: {.attrs} on last line (e.g., "Description\n{.attrs}")
@@ -365,13 +478,13 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
365
478
  const parsedAttrs = parseAttrString(attrString)
366
479
  if (parsedAttrs.length > 0) {
367
480
  listAttrs.push(...parsedAttrs)
481
+ // Remove {.attrs} line from afterStrong
482
+ afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
368
483
  }
369
- // Remove {.attrs} line from afterStrong
370
- afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
371
484
  }
372
485
 
373
- // Clean up afterStrong: remove leading spaces/colon/backslash, then trim each line
374
- let cleaned = afterStrong.replace(/^[\s:]+/, '').replace(/^\\/, '')
486
+ // Clean up afterStrong: remove leading spaces/backslash, then trim each line
487
+ let cleaned = afterStrong.replace(/^\s+/, '').replace(/^\\/, '')
375
488
 
376
489
  // Remove leading whitespace from each line (remove list indent)
377
490
  cleaned = cleaned.split('\n').map(line => line.replace(/^\s+/, '')).join('\n').trim()
@@ -386,7 +499,10 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
386
499
  const divOpen = new tokens[firstPara].constructor('div_open', 'div', 1)
387
500
  divOpen.level = parentLevel + 1
388
501
  divOpen.block = true
389
- const divClass = typeof opt.descriptionListDivClass === 'string' ? opt.descriptionListDivClass : ''
502
+ copyMap(divOpen, tokens[firstPara])
503
+ const divClass = typeof opt.descriptionListDivClass === 'string'
504
+ ? opt.descriptionListDivClass.trim()
505
+ : ''
390
506
  if (divClass) {
391
507
  divOpen.attrs = [['class', divClass]]
392
508
  }
@@ -397,6 +513,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
397
513
  const dtOpen = new tokens[firstPara].constructor('dt_open', 'dt', 1)
398
514
  dtOpen.level = parentLevel + 1
399
515
  dtOpen.block = true
516
+ copyMap(dtOpen, tokens[firstPara])
400
517
  // Add collected attributes to dt_open
401
518
  if (dtAttrs && dtAttrs.length > 0) {
402
519
  dtOpen.attrs = dtAttrs
@@ -414,20 +531,22 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
414
531
  const dtClose = new tokens[firstPara].constructor('dt_close', 'dt', -1)
415
532
  dtClose.level = parentLevel + 1
416
533
  dtClose.block = true
534
+ copyMap(dtClose, tokens[firstPara])
417
535
  result.push(dtClose)
418
536
 
419
537
  // Create dd_open token
420
538
  const ddOpen = new tokens[firstPara].constructor('dd_open', 'dd', 1)
421
539
  ddOpen.level = parentLevel + 1
422
540
  ddOpen.block = true
541
+ copyMap(ddOpen, tokens[firstPara])
423
542
  result.push(ddOpen)
424
543
 
425
544
  // First paragraph in dd (if description exists)
426
- let hasFirstParagraph = false
427
545
  if (descStart.trim()) {
428
546
  const pOpen = new tokens[firstPara].constructor('paragraph_open', 'p', 1)
429
547
  pOpen.level = parentLevel + 2
430
548
  pOpen.block = true // IMPORTANT: Enable block mode for proper newline rendering
549
+ copyMap(pOpen, tokens[firstPara])
431
550
  result.push(pOpen)
432
551
 
433
552
  const pInline = new tokens[firstPara].constructor('inline', '', 0)
@@ -442,9 +561,9 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
442
561
  const pClose = new tokens[firstPara].constructor('paragraph_close', 'p', -1)
443
562
  pClose.level = parentLevel + 2
444
563
  pClose.block = true // IMPORTANT: Enable block mode for proper newline rendering
564
+ copyMap(pClose, tokens[firstPara])
445
565
  result.push(pClose)
446
566
 
447
- hasFirstParagraph = true
448
567
  }
449
568
 
450
569
  // Add remaining content in dd (paragraphs, lists, etc.)
@@ -454,8 +573,6 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
454
573
 
455
574
  // Handle paragraph
456
575
  if (token.type === 'paragraph_open') {
457
- hasFirstParagraph = true
458
-
459
576
  result.push(tokens[i]) // paragraph_open
460
577
  result.push(tokens[i + 1]) // inline
461
578
  result.push(tokens[i + 2]) // paragraph_close
@@ -491,6 +608,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
491
608
  const ddClose = new tokens[firstPara].constructor('dd_close', 'dd', -1)
492
609
  ddClose.level = parentLevel + 1
493
610
  ddClose.block = true
611
+ copyMap(ddClose, tokens[firstPara])
494
612
  result.push(ddClose)
495
613
 
496
614
  // Create div_close if descriptionListWithDiv is enabled
@@ -498,6 +616,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
498
616
  const divClose = new tokens[firstPara].constructor('div_close', 'div', -1)
499
617
  divClose.level = parentLevel + 1
500
618
  divClose.block = true
619
+ copyMap(divClose, tokens[firstPara])
501
620
  result.push(divClose)
502
621
  }
503
622
 
@@ -154,6 +154,9 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
154
154
  if (!isLoose) {
155
155
  hideFirstParagraphsForTightList(tokens, items, level)
156
156
  }
157
+
158
+ // Cache loose/tight state for later phases (mapless flattening fallback).
159
+ listToken._isLoose = isLoose
157
160
 
158
161
  // Extract marker info (for both bullet_list and ordered_list)
159
162
  const markerInfo = extractMarkerInfo(tokens, startIndex, endIndex, opt)
@@ -168,6 +171,13 @@ function analyzeList(tokens, startIndex, opt, closeMap) {
168
171
  // Convert even loose lists if markers are consistent
169
172
  const shouldConvert = originalType === 'bullet_list_open' &&
170
173
  shouldConvertToOrdered(originalType, markerInfo, opt)
174
+
175
+ if (markerInfo) {
176
+ listToken._markerInfo = markerInfo
177
+ }
178
+ if (originalType === 'bullet_list_open') {
179
+ listToken._shouldConvert = shouldConvert
180
+ }
171
181
 
172
182
  return {
173
183
  startIndex,
@@ -259,6 +269,10 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
259
269
  markerInfo = detectMarkerType(lastInlineContent)
260
270
  }
261
271
  content = lastInlineContent
272
+
273
+ if (tokens[startIndex]) {
274
+ tokens[startIndex]._firstParagraphIsLoose = firstParagraphIsLoose
275
+ }
262
276
 
263
277
  return {
264
278
  startIndex,
@@ -277,9 +291,15 @@ function analyzeListItem(tokens, startIndex, endIndex, opt, closeMap) {
277
291
  function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
278
292
  const listToken = tokens[startIndex]
279
293
  const markers = []
294
+ const literalMarkerInfo = listToken._literalMarkerInfo || null
280
295
 
281
296
  // For ordered_list, get numbers from list_item_open's info
282
297
  if (listToken.type === 'ordered_list_open') {
298
+ const literalMarkers = Array.isArray(literalMarkerInfo?.markers) ? literalMarkerInfo.markers : null
299
+ const literalType = literalMarkerInfo?.type || 'decimal'
300
+ const literalPrefix = literalMarkers?.[0]?.prefix ?? ''
301
+ const literalSuffix = literalMarkers?.[0]?.suffix ?? (listToken.markup || '.')
302
+ let listItemIndex = 0
283
303
  for (let i = startIndex + 1; i < endIndex; i++) {
284
304
  const token = tokens[i]
285
305
 
@@ -287,14 +307,25 @@ function extractMarkerInfo(tokens, startIndex, endIndex, opt) {
287
307
  // Get number from info
288
308
  const itemNumber = token.info ? parseInt(token.info, 10) : markers.length + 1
289
309
  const markup = token.markup || listToken.markup || '.'
290
-
291
- markers.push({
292
- number: itemNumber,
293
- originalNumber: itemNumber,
294
- prefix: '',
295
- suffix: markup,
296
- type: 'decimal' // Default (can be extended to detect from markup)
297
- })
310
+ if (literalMarkers && literalMarkers[listItemIndex]) {
311
+ const marker = { ...literalMarkers[listItemIndex] }
312
+ if (typeof marker.originalNumber !== 'number') {
313
+ marker.originalNumber = itemNumber
314
+ }
315
+ if (typeof marker.number !== 'number') {
316
+ marker.number = itemNumber
317
+ }
318
+ markers.push(marker)
319
+ } else {
320
+ markers.push({
321
+ number: itemNumber,
322
+ originalNumber: itemNumber,
323
+ prefix: literalPrefix,
324
+ suffix: literalSuffix || markup,
325
+ type: literalType
326
+ })
327
+ }
328
+ listItemIndex++
298
329
  }
299
330
  }
300
331
  } else {