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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,12 @@ import { findMatchingClose, findListItemEnd } from './list-helpers.js'
5
5
 
6
6
  // markdown-it treats indent >= marker width + 4 as code blocks inside list items.
7
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]+$/
8
14
  const getIndentWidth = (indentText) => indentText.replace(/\t/g, ' ').length
9
15
  const buildLineMap = (startLine, endLine = null) => {
10
16
  if (typeof startLine !== 'number') {
@@ -24,6 +30,109 @@ const getListItemMarkerWidth = (listItem) => {
24
30
  return markerLength > 0 ? markerLength + 1 : 1
25
31
  }
26
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
+ }
135
+
27
136
  /**
28
137
  * Normalize literal nested ordered lists inside list items.
29
138
  * Converts indented numeric lines into proper ordered_list tokens before Phase 1.
@@ -71,11 +180,6 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
71
180
  let j = i + 1
72
181
  while (j < listItemClose) {
73
182
  const current = tokens[j]
74
- if (current.type !== 'paragraph_open') {
75
- j++
76
- continue
77
- }
78
-
79
183
  if (current.type === 'paragraph_open') {
80
184
  const inlineIdx = j + 1
81
185
  const paragraphCloseIdx = j + 2
@@ -92,6 +196,17 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
92
196
  j = paragraphCloseIdx + 1
93
197
  continue
94
198
  }
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
+ }
95
210
  const baseLine = tokens[j].map ? tokens[j].map[0] : null
96
211
  const segments = parseSegments(inlineToken.content, markerWidth, baseLine)
97
212
  if (!segments.hasLiteral) {
@@ -126,14 +241,6 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
126
241
  continue
127
242
  }
128
243
 
129
- if (current.type === 'ordered_list_open' &&
130
- current.level === (tokens[i].level ?? 0) + 1) {
131
- const delta = splitOrderedListForLiteralChildren(tokens, j, TokenClass)
132
- listItemClose += delta
133
- j++
134
- continue
135
- }
136
-
137
244
  j++
138
245
  }
139
246
  i = listItemClose
@@ -166,19 +273,16 @@ function parseSegments(content, markerWidth, baseLine = null) {
166
273
  }
167
274
  }
168
275
  const textValue = buffer.join('\n')
169
- const hasBlankLine = hadBlankLine
170
- segments.push({ type: 'text', text: textValue, tight: !hasBlankLine })
276
+ segments.push({ type: 'text', text: textValue, tight: !hadBlankLine })
171
277
  buffer = []
172
278
  blankLinesInBuffer = 0
173
279
  }
174
280
 
175
281
  while (idx < lines.length) {
176
282
  const isFirstLine = idx === 0
177
- if (isFirstLine && lines[idx].trim().length > 0) {
283
+ const trimmedLine = lines[idx].trim()
284
+ if (isFirstLine && trimmedLine.length > 0) {
178
285
  buffer.push(lines[idx])
179
- if (lines[idx].trim().length === 0) {
180
- blankLinesInBuffer++
181
- }
182
286
  idx++
183
287
  continue
184
288
  }
@@ -646,89 +750,3 @@ function markLiteralListLoose(tokens, listOpenIndex, listCloseIndex = null) {
646
750
  }
647
751
  }
648
752
  }
649
-
650
- function splitOrderedListForLiteralChildren(tokens, listOpenIndex, TokenClass) {
651
- const listToken = tokens[listOpenIndex]
652
- const listCloseIndex = findMatchingClose(tokens, listOpenIndex, 'ordered_list_open', 'ordered_list_close')
653
- if (listCloseIndex === -1) {
654
- return 0
655
- }
656
- const childLevel = (listToken.level ?? 0) + 1
657
- const ranges = []
658
- let currentOpen = -1
659
- for (let idx = listOpenIndex + 1; idx < listCloseIndex; idx++) {
660
- const token = tokens[idx]
661
- if (token.type === 'list_item_open' && token.level === childLevel) {
662
- currentOpen = idx
663
- continue
664
- }
665
- if (token.type === 'list_item_close' && token.level === childLevel) {
666
- if (currentOpen !== -1) {
667
- ranges.push({ open: currentOpen, close: idx })
668
- currentOpen = -1
669
- }
670
- }
671
- }
672
-
673
- if (ranges.length <= 1) {
674
- return 0
675
- }
676
-
677
- const extraRanges = ranges.slice(1)
678
- const childTokens = []
679
- let removedCount = 0
680
-
681
- for (const range of extraRanges) {
682
- childTokens.push(...tokens.slice(range.open, range.close + 1))
683
- }
684
-
685
- for (let k = extraRanges.length - 1; k >= 0; k--) {
686
- const range = extraRanges[k]
687
- const len = range.close - range.open + 1
688
- tokens.splice(range.open, len)
689
- removedCount += len
690
- }
691
-
692
- if (listToken._markerInfo) {
693
- delete listToken._markerInfo
694
- }
695
-
696
- const levelShift = 2
697
- for (const token of childTokens) {
698
- if (typeof token.level === 'number') {
699
- token.level += levelShift
700
- }
701
- }
702
-
703
- const nestedListOpen = new TokenClass('ordered_list_open', 'ol', 1)
704
- nestedListOpen.level = (listToken.level ?? 0) + 2
705
- nestedListOpen.block = true
706
- nestedListOpen.markup = listToken.markup
707
- nestedListOpen.attrs = null
708
- nestedListOpen._literalList = true
709
- if (Array.isArray(listToken.map)) {
710
- nestedListOpen.map = listToken.map.slice()
711
- }
712
-
713
- const firstChild = childTokens.find(t => t.type === 'list_item_open')
714
- if (firstChild?.info) {
715
- const num = parseInt(firstChild.info, 10)
716
- if (!Number.isNaN(num) && num !== 1) {
717
- nestedListOpen.attrs = [['start', String(num)]]
718
- }
719
- }
720
-
721
- const nestedListClose = new TokenClass('ordered_list_close', 'ol', -1)
722
- nestedListClose.level = nestedListOpen.level
723
- nestedListClose.block = true
724
- nestedListClose.markup = listToken.markup
725
- if (Array.isArray(listToken.map)) {
726
- nestedListClose.map = listToken.map.slice()
727
- }
728
-
729
- const insertionIndex = ranges[0].close
730
- tokens.splice(insertionIndex, 0, nestedListOpen, ...childTokens, nestedListClose)
731
-
732
- const addedCount = childTokens.length + 2
733
- return addedCount - removedCount
734
- }
@@ -371,21 +371,6 @@ const createMarkerResult = (type, marker, number, prefix, suffix) => ({
371
371
  suffix
372
372
  })
373
373
 
374
- /**
375
- * Try to match content against compiled type patterns
376
- * @param {string} trimmed - Trimmed content
377
- * @param {Object} compiledType - Compiled type info
378
- * @param {Object} typeInfo - Type info from listTypes.json
379
- * @returns {Object|null} Match result or null
380
- */
381
- const tryMatchPattern = (trimmed, compiledType, typeInfo) => {
382
- for (const pattern of compiledType.patterns) {
383
- const m = matchRegexEntry(trimmed, compiledType.name, pattern)
384
- if (m) return m
385
- }
386
- return null
387
- }
388
-
389
374
  // Enhanced marker type detection with context awareness
390
375
  export const detectMarkerType = (content, allContents = null) => {
391
376
  let contextResult = null
@@ -1,184 +0,0 @@
1
- // Phase 6: Attribute Migration
2
- // Runs after markdown-it-attrs processing to handle nested list attributes
3
- // This phase moves custom attributes from child lists to parent lists
4
- // in flattened `- 1. Parent\n - a. Child\n{.class}` patterns
5
-
6
- import { buildListCloseIndexMap } from './list-helpers.js'
7
-
8
- /**
9
- * Move custom attributes from nested child ordered_list to parent ordered_list
10
- *
11
- * Background:
12
- * - Plugin flattens `ul > li > ol` structure to `ol > li` (simplifyNestedBulletLists)
13
- * - markdown-it-attrs runs on original structure, applies {.class} to child list
14
- * - Users expect {.class} after nested list to apply to parent list
15
- *
16
- * Algorithm:
17
- * 1. Find top-level ordered_list_open tokens (level 0 or 2)
18
- * 2. Locate child ordered_list_open within first list_item
19
- * 3. Extract custom attributes (exclude plugin-generated: type, data-marker-*, role, ol-* classes)
20
- * 4. Move custom classes to parent's class list
21
- * 5. Move other custom attrs to parent
22
- * 6. Remove moved attrs from child
23
- *
24
- * @param {Array} tokens - Token array to process
25
- */
26
- export function moveNestedListAttributes(tokens) {
27
- const tokensLength = tokens.length
28
- const closeMap = buildListCloseIndexMap(tokens)
29
- const listCloseByOpen = closeMap.listCloseByOpen
30
- const listItemCloseByOpen = closeMap.listItemCloseByOpen
31
-
32
- // Single pass: find top-level ordered_lists and process immediately
33
- for (let i = 0; i < tokensLength; i++) {
34
- const token = tokens[i]
35
-
36
- // Skip non-top-level ordered lists
37
- if (token.type !== 'ordered_list_open' || (token.level !== 0 && token.level !== 2)) {
38
- continue
39
- }
40
-
41
- const parentToken = token
42
- const parentLevel = token.level
43
-
44
- // Find list end
45
- let listEndIndex = listCloseByOpen[i]
46
- if (typeof listEndIndex !== 'number' || listEndIndex === -1) {
47
- listEndIndex = tokensLength
48
- let depth = 1
49
- for (let j = i + 1; j < tokensLength; j++) {
50
- if (tokens[j].type === 'ordered_list_open') depth++
51
- else if (tokens[j].type === 'ordered_list_close') {
52
- depth--
53
- if (depth === 0) {
54
- listEndIndex = j
55
- break
56
- }
57
- }
58
- }
59
- }
60
-
61
- // Find first list_item
62
- let firstItemOpen = -1
63
- let firstItemClose = -1
64
-
65
- for (let j = i + 1; j < listEndIndex; j++) {
66
- const t = tokens[j]
67
-
68
- if (t.type === 'list_item_open' && t.level === parentLevel + 1) {
69
- firstItemOpen = j
70
- firstItemClose = listItemCloseByOpen[j]
71
- if (typeof firstItemClose !== 'number' || firstItemClose === -1) {
72
- // Find matching close
73
- let itemDepth = 1
74
- for (let k = j + 1; k < tokensLength; k++) {
75
- if (tokens[k].type === 'list_item_open') itemDepth++
76
- else if (tokens[k].type === 'list_item_close') {
77
- itemDepth--
78
- if (itemDepth === 0) {
79
- firstItemClose = k
80
- break
81
- }
82
- }
83
- }
84
- }
85
- break
86
- }
87
- }
88
-
89
- if (firstItemOpen === -1 || firstItemClose === -1) continue
90
-
91
- // Find child ordered_list within first list_item
92
- let childListOpen = -1
93
- for (let j = firstItemOpen + 1; j < firstItemClose && j < tokensLength; j++) {
94
- if (tokens[j].type === 'ordered_list_open' && tokens[j].level > parentLevel) {
95
- childListOpen = j
96
- break
97
- }
98
- }
99
-
100
- if (childListOpen === -1) continue
101
-
102
- const childToken = tokens[childListOpen]
103
- if (!childToken.attrs || childToken.attrs.length === 0) continue
104
-
105
- // Extract custom attributes (exclude plugin-generated)
106
- const customAttrs = []
107
- const remainingAttrs = []
108
-
109
- for (let j = 0; j < childToken.attrs.length; j++) {
110
- const [key, value] = childToken.attrs[j]
111
-
112
- // Exclude plugin-generated attributes
113
- if (key === 'type' || key === 'role' || key === 'style' || key === 'start' || key.startsWith('data-marker-')) {
114
- remainingAttrs.push([key, value])
115
- continue
116
- }
117
-
118
- // Handle class attribute specially
119
- if (key === 'class') {
120
- const classes = value.split(/\s+/)
121
- const pluginClasses = []
122
- const customClasses = []
123
-
124
- for (let k = 0; k < classes.length; k++) {
125
- // Treat ol-* as plugin-generated (should remain on child)
126
- if (classes[k].startsWith('ol-')) {
127
- pluginClasses.push(classes[k])
128
- } else {
129
- customClasses.push(classes[k])
130
- }
131
- }
132
-
133
- if (pluginClasses.length > 0) {
134
- remainingAttrs.push(['class', pluginClasses.join(' ')])
135
- }
136
- if (customClasses.length > 0) {
137
- customAttrs.push(['class', customClasses.join(' ')])
138
- }
139
- continue
140
- }
141
-
142
- // All other attributes are custom
143
- customAttrs.push([key, value])
144
- }
145
-
146
- if (customAttrs.length === 0) continue
147
-
148
- // Move custom attributes to parent
149
- if (!parentToken.attrs) {
150
- parentToken.attrs = []
151
- }
152
-
153
- // Merge attributes
154
- for (let j = 0; j < customAttrs.length; j++) {
155
- const [key, value] = customAttrs[j]
156
-
157
- if (key === 'class') {
158
- // Find existing class attribute
159
- let existingClassIdx = -1
160
- for (let k = 0; k < parentToken.attrs.length; k++) {
161
- if (parentToken.attrs[k][0] === 'class') {
162
- existingClassIdx = k
163
- break
164
- }
165
- }
166
-
167
- if (existingClassIdx !== -1) {
168
- const existingClasses = parentToken.attrs[existingClassIdx][1].split(/\s+/)
169
- const newClasses = value.split(/\s+/)
170
- // Use Set for deduplication
171
- const mergedClasses = [...new Set([...existingClasses, ...newClasses])]
172
- parentToken.attrs[existingClassIdx][1] = mergedClasses.join(' ')
173
- } else {
174
- parentToken.attrs.push(['class', value])
175
- }
176
- } else {
177
- parentToken.attrs.push([key, value])
178
- }
179
- }
180
-
181
- // Update child token attrs
182
- childToken.attrs = remainingAttrs.length > 0 ? remainingAttrs : null
183
- }
184
- }