@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.4.1 β†’ 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.
package/README.md CHANGED
@@ -45,6 +45,7 @@ The plugin supports the following marker types.
45
45
  - filled-squared-upper-latin: `πŸ…°`, `πŸ…±`, `πŸ…²`, pattern: enclosed
46
46
  - fullwidth-lower-roman: `β…°`, `β…±`, `β…²`, pattern: fullwidth
47
47
  - fullwidth-upper-roman: `β… `, `β…‘`, `β…’`, pattern: fullwidth
48
+ - fullwidth-decimal: `0`, `οΌ‘`, `οΌ’`, pattern: fullwidth
48
49
  - japanese-informal: `δΈ€`, `二`, `δΈ‰`, pattern: fullwidth
49
50
  - katakana: `γ‚’`, `γ‚€`, `ウ`, pattern: fullwidth
50
51
  - katakana-iroha: `γ‚€`, `γƒ­`, `ハ`, pattern: fullwidth
@@ -91,7 +92,7 @@ You can customize the conversion using options.
91
92
  - `omitMarkerMetadata` (boolean) β€” If `true`, omit the `data-marker-prefix` / `data-marker-suffix` attributes.
92
93
  - `addMarkerStyleToClass` (boolean) β€” When `true`, append suffix-style information to the generated class name (e.g. `ol-decimal-with-round-round`). When `false` (default) the class stays as `ol-decimal`.
93
94
  - `enableLiteralNumberingFix` (boolean) β€” Enable literal nested list recovery (for example, nested lists starting with 2 or greater). Default is `false` (legacy-compatible); set it to `true` to normalize literal nested numbering. It only applies inside list items, evaluates indentation relative to the parent list marker (marker width + 0–3 spaces), and does not convert code blocks (indent >= marker width + 4).
94
- - Compatibility note: compared with the default/legacy mode (`enableLiteralNumberingFix: false`), enabling `true` changes rendered HTML by design. On the current test corpus (`395` markdown cases), `19` cases change.
95
+ - Compatibility note: compared with the default/legacy mode (`enableLiteralNumberingFix: false`), enabling `true` changes rendered HTML by design. On the current test corpus (`399` markdown cases), `19` cases change.
95
96
  - Changed files in that comparison: `examples-default-14-repeated-numbers.txt` (`7`), `examples-option-literal-numbering-attrs-disabled.txt` (`1`), `examples-option-literal-numbering-attrs.txt` (`1`), `examples-option-literal-numbering-fix-disabled.txt` (`2`), `examples-option-literal-numbering-fix.txt` (`4`), `examples-option-literal-numbering-indent.txt` (`4`).
96
97
  - Migration tip: if you enable `enableLiteralNumberingFix: true`, treat it as a breaking-output change for snapshots/downstream HTML.
97
98
  - Detailed notes: `docs/enable-literal-numbering-fix.md`.
@@ -146,7 +147,7 @@ Custom marker conversion example:
146
147
  - β‘’ Third
147
148
 
148
149
  [HTML]
149
- <ol role="list" class="ol-filled-circled-decimal">
150
+ <ol role="list" class="ol-circled-decimal">
150
151
  <li><span class="li-num" aria-hidden="true">β‘ </span> First</li>
151
152
  <li><span class="li-num" aria-hidden="true">β‘‘</span> Second</li>
152
153
  <li><span class="li-num" aria-hidden="true">β‘’</span> Third</li>
@@ -269,9 +270,9 @@ When description lists are enabled the plugin can convert the following patterns
269
270
 
270
271
  ```
271
272
  [Markdown]
272
- - **Term 1**
273
+ - **Term 1**
273
274
  Description text for term 1
274
- - **Term 2**
275
+ - **Term 2**
275
276
  Description text for term 2
276
277
 
277
278
  [HTML]
@@ -350,3 +351,5 @@ md.use(mditNumberingUl)
350
351
  const html = md.render(`- a. First\n- b. Second`)
351
352
  console.log(html)
352
353
  ```
354
+
355
+ Register this plugin only once per `markdown-it` instance. A second registration fails fast because running the conversion pipeline twice on the same token stream would corrupt already-converted lists.
package/index.js CHANGED
@@ -7,7 +7,17 @@ import { processHtmlBlocks } from './src/phase4-html-blocks.js'
7
7
  import { generateSpans } from './src/phase5-spans.js'
8
8
  import { normalizeLiteralOrderedLists } from './src/preprocess-literal-lists.js'
9
9
 
10
+ const INSTALL_FLAG = Symbol.for('@peaceroad/markdown-it-numbering-ul-regarded-as-ol/installed')
11
+
10
12
  const mditNumberingUl = (md, option) => {
13
+ if (md[INSTALL_FLAG]) {
14
+ throw new Error('@peaceroad/markdown-it-numbering-ul-regarded-as-ol is already registered on this markdown-it instance')
15
+ }
16
+ Object.defineProperty(md, INSTALL_FLAG, {
17
+ value: true,
18
+ configurable: false
19
+ })
20
+
11
21
  const opt = {
12
22
  // Core options
13
23
  descriptionList: false, // Convert **Term** patterns to <dl>/<dt>/<dd>
@@ -21,7 +31,7 @@ const mditNumberingUl = (md, option) => {
21
31
  useCounterStyle: false, // true=users will use @counter-style; suppress marker spans and role attr
22
32
  addMarkerStyleToClass: false, // true=append -with-* marker style suffix to class names
23
33
  enableLiteralNumberingFix: false, // true=normalize nested lists that don't start at 1 (opt-in)
24
-
34
+
25
35
  // Override with user options
26
36
  ...option
27
37
  }
@@ -68,11 +78,11 @@ const mditNumberingUl = (md, option) => {
68
78
  if (opt.enableLiteralNumberingFix) {
69
79
  normalizeLiteralOrderedLists(tokens, opt)
70
80
  }
71
-
81
+
72
82
  // ===== PHASE 1: List Structure Analysis =====
73
83
  // Analyze marker detection and structure without token conversion
74
84
  const listInfos = analyzeListStructure(tokens)
75
-
85
+
76
86
  // ===== PHASE 2: Token Conversion =====
77
87
  // Convert bullet_list to ordered_list based on Phase1 analysis
78
88
  // Note: simplifyNestedBulletLists removes tokens, changing indices
@@ -92,32 +102,33 @@ const mditNumberingUl = (md, option) => {
92
102
  break
93
103
  }
94
104
  }
95
-
105
+
96
106
  // ===== PHASE 3: Add Attributes =====
97
107
  // Add type, class, data-* attributes to converted lists
98
108
  // Use markerInfo stored on list tokens (safe after Phase2 mutations)
109
+ let closeMap = null
99
110
  if (hasOrderedList) {
100
- addAttributes(tokens, opt)
111
+ closeMap = addAttributes(tokens, opt)
101
112
  }
102
-
113
+
103
114
  // ===== PHASE 4: HTML Block Processing =====
104
115
  // Remove indents from HTML blocks in lists and normalize line breaks
105
116
  if (hasNestedHtmlBlock) {
106
117
  processHtmlBlocks(state)
107
118
  }
108
-
119
+
109
120
  // ===== PHASE 5: Span Generation =====
110
121
  // Generate marker spans in alwaysMarkerSpan mode
111
122
  if (hasOrderedList && !opt.useCounterStyle) {
112
- generateSpans(tokens, opt)
123
+ generateSpans(tokens, opt, closeMap?.listCloseByOpen || null)
113
124
  }
114
-
125
+
115
126
  return true
116
127
  }
117
128
 
118
129
  md.core.ruler.before('inline', 'numbering_dl_parser', dlProcessor)
119
130
  md.core.ruler.after('numbering_dl_parser', 'numbering_ul_phases', listProcessor)
120
-
131
+
121
132
  // Description list: Move paragraph attributes to dl and add custom renderers
122
133
  if (opt.descriptionList || opt.descriptionListWithDiv) {
123
134
  // Move paragraph attributes to dl (after inline and any attribute plugins)
@@ -125,7 +136,7 @@ const mditNumberingUl = (md, option) => {
125
136
  moveParagraphAttributesToDL(state.tokens)
126
137
  return true
127
138
  }
128
-
139
+
129
140
  addRuleAfter(md.core.ruler, 'curly_attributes', 'numbering_dl_attrs', dlAttrProcessor)
130
141
  }
131
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-numbering-ul-regarded-as-ol",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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,8 +27,8 @@
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-cjk-breaks-mod": "^0.1.8",
31
- "@peaceroad/markdown-it-strong-ja": "^0.8.0",
30
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.10",
31
+ "@peaceroad/markdown-it-strong-ja": "^0.9.0",
32
32
  "markdown-it": "^14.1.0",
33
33
  "markdown-it-attrs": "^4.3.1",
34
34
  "markdown-it-deflist": "^3.0.0"
@@ -49,11 +49,13 @@ export function findListItemEnd(tokens, startIndex) {
49
49
  /**
50
50
  * Build close-index maps for list and list_item tokens.
51
51
  * @param {Array} tokens - Token array
52
- * @returns {{ listCloseByOpen: number[], listItemCloseByOpen: number[] }}
52
+ * @returns {{ listCloseByOpen: Int32Array, listItemCloseByOpen: Int32Array }}
53
53
  */
54
54
  export function buildListCloseIndexMap(tokens) {
55
- const listCloseByOpen = new Array(tokens.length).fill(-1)
56
- const listItemCloseByOpen = new Array(tokens.length).fill(-1)
55
+ const listCloseByOpen = new Int32Array(tokens.length)
56
+ listCloseByOpen.fill(-1)
57
+ const listItemCloseByOpen = new Int32Array(tokens.length)
58
+ listItemCloseByOpen.fill(-1)
57
59
  const bulletStack = []
58
60
  const orderedStack = []
59
61
  const listItemStack = []
@@ -3,69 +3,6 @@
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 Uint8Array(tokens.length)
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] = 1
21
- }
22
-
23
- if (token.type === 'dl_open') {
24
- state[i] = 1
25
- dlDepth++
26
- continue
27
- }
28
-
29
- if (token.type === 'dl_close') {
30
- state[i] = 1
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] = 1
40
- htmlDdDepth++
41
- continue
42
- }
43
- if (token.content === '</dd>\n') {
44
- state[i] = 1
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] === 1
67
- }
68
-
69
6
  /**
70
7
  * Collect list information from token array
71
8
  * @param {Array} tokens - markdown-it token array
@@ -74,36 +11,18 @@ function isInsideDL(index, dlState) {
74
11
  export function analyzeListStructure(tokens) {
75
12
  const listInfos = []
76
13
  const closeMap = buildListCloseIndexMap(tokens)
77
-
78
- // Check DL existence (O(n) but optimized with early return)
79
- let hasDL = false
80
- for (let i = 0; i < tokens.length; i++) {
81
- if (tokens[i].type === 'dl_open' ||
82
- (tokens[i].type === 'html_block' && tokens[i].content === '<dd>\n')) {
83
- hasDL = true
84
- break
85
- }
86
- }
87
-
88
- // Pre-compute DL scope flags (only when DL exists)
89
- const dlScope = hasDL ? buildDLStateMap(tokens) : null
90
-
91
- // Process only top-level lists (nested lists collected recursively)
92
- // 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.
93
19
  for (let i = 0; i < tokens.length; i++) {
94
20
  const token = tokens[i]
95
21
  if (!token) {
96
22
  continue
97
23
  }
98
-
99
- // Process level-0 lists or lists at any level inside DD
100
- const isTopLevelList = (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') &&
101
- (token.level === 0 || token.level === undefined)
102
- const isListInDD = (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') &&
103
- token.level > 0 &&
104
- isInsideDL(i, dlScope)
105
-
106
- if (isTopLevelList || isListInDD) {
24
+
25
+ if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
107
26
  const listInfo = analyzeList(tokens, i, closeMap)
108
27
  if (listInfo) {
109
28
  listInfos.push(listInfo)
@@ -217,10 +136,9 @@ function analyzeListItems(tokens, startIndex, endIndex, closeMap) {
217
136
  * Analyze a single list item
218
137
  */
219
138
  function analyzeListItem(tokens, startIndex, endIndex, closeMap) {
220
- let content = ''
221
139
  let markerInfo = null
222
140
  let hasNestedList = false
223
- let nestedLists = []
141
+ const nestedLists = []
224
142
  let firstParagraphIsLoose = false
225
143
  let lastInlineContent = ''
226
144
 
@@ -267,7 +185,6 @@ function analyzeListItem(tokens, startIndex, endIndex, closeMap) {
267
185
  if (firstParagraphIsLoose && hasNestedList && lastInlineContent) {
268
186
  markerInfo = detectMarkerType(lastInlineContent)
269
187
  }
270
- content = lastInlineContent
271
188
 
272
189
  if (tokens[startIndex]) {
273
190
  tokens[startIndex]._firstParagraphIsLoose = firstParagraphIsLoose
@@ -276,7 +193,7 @@ function analyzeListItem(tokens, startIndex, endIndex, closeMap) {
276
193
  return {
277
194
  startIndex,
278
195
  endIndex,
279
- content,
196
+ content: lastInlineContent,
280
197
  markerInfo,
281
198
  hasNestedList,
282
199
  nestedLists,
@@ -348,35 +265,32 @@ function extractMarkerInfo(tokens, startIndex, endIndex) {
348
265
  // Detect markers using full context
349
266
  let sequentialNumber = 1 // Sequential number counter
350
267
  for (const token of inlineTokens) {
351
- // Process only inline tokens of direct child items of this list
352
- if (token.type === 'inline' && token.content && token.level === targetLevel) {
353
- const markerInfo = detectMarkerTypeWithContext(token.content, contextResult)
354
- if (markerInfo && markerInfo.type) {
355
- // Use sequential numbers when same marker continues
356
- // (e.g., "γ‚€. γ‚€. γ‚€." β†’ interpreted as "む、ロ、ハ")
357
- const adjustedMarkerInfo = { ...markerInfo }
358
- adjustedMarkerInfo.originalNumber = markerInfo.number
359
-
360
- // Assign sequential numbers based on first marker's number
361
- if (markers.length === 0) {
362
- // Use first marker as-is
363
- 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
364
287
  } else {
365
- // For 2nd and later, use sequential number if same as previous marker
366
- const prevMarker = markers[markers.length - 1]
367
- if (markerInfo.marker === prevMarker.marker &&
368
- markerInfo.type === prevMarker.type) {
369
- // Assign sequential number when same marker continues
370
- sequentialNumber++
371
- adjustedMarkerInfo.number = sequentialNumber
372
- } else {
373
- // For different marker, use detected number
374
- sequentialNumber = markerInfo.number || sequentialNumber + 1
375
- }
288
+ // For different marker, use detected number
289
+ sequentialNumber = markerInfo.number || sequentialNumber + 1
376
290
  }
377
-
378
- markers.push(adjustedMarkerInfo)
379
291
  }
292
+
293
+ markers.push(adjustedMarkerInfo)
380
294
  }
381
295
  }
382
296
  }
@@ -167,10 +167,9 @@ function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo) {
167
167
  }
168
168
 
169
169
  /**
170
- * Simplify nested ul>li>ul and ul>li>ol structures.
170
+ * Simplify nested ul>li>ol structures.
171
171
  *
172
- * Pattern 1: bullet_list_open β†’ list_item_open β†’ bullet_list_open β†’ ...
173
- * Pattern 2: bullet_list_open β†’ list_item_open β†’ ordered_list_open β†’ ... (repeated)
172
+ * Pattern: bullet_list_open β†’ list_item_open β†’ ordered_list_open β†’ ... (repeated)
174
173
  *
175
174
  * When the middle list_item is empty (contains only the inner list),
176
175
  * remove the outer ul and the intermediate li.
@@ -37,6 +37,8 @@ export function addAttributes(tokens, opt) {
37
37
  if (hasAnyListItemValue) {
38
38
  normalizeAndConvertValueAttributes(tokens, listCloseByOpen)
39
39
  }
40
+
41
+ return closeMap
40
42
  }
41
43
 
42
44
  function hasValueAttr(token) {
@@ -55,15 +57,14 @@ function hasValueAttr(token) {
55
57
  * Add attributes to a single list token
56
58
  */
57
59
  function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOpen = null) {
58
- // Initialize attribute array
59
- if (!token.attrs) {
60
- token.attrs = []
61
- }
62
-
63
60
  // Get marker info
64
61
  const markerInfo = token._markerInfo
65
62
 
66
63
  if (!markerInfo) {
64
+ if (!token.attrs) {
65
+ token.attrs = []
66
+ }
67
+
67
68
  // Default attributes for lists without markerInfo
68
69
  if (opt.useCounterStyle) {
69
70
  // Do not add type attribute; add class so user CSS/@counter-style can target
@@ -85,7 +86,7 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
85
86
 
86
87
  // Attributes according to marker type
87
88
  // Pass first marker's prefix/suffix info to determine class name
88
- const firstMarker = markerInfo.markers[0]
89
+ const firstMarker = markerInfo.markers?.[0]
89
90
  const typeAttrs = getTypeAttributes(markerInfo.type, firstMarker, opt)
90
91
 
91
92
  // Reset attribute array
@@ -121,19 +122,7 @@ function addListAttributesForToken(tokens, token, tokenIndex, opt, listCloseByOp
121
122
 
122
123
  // 3. Add class attribute
123
124
  if (typeAttrs.class) {
124
- // Merge or add class; preserve existing classes and append
125
- let classAttr = null
126
- for (let i = 0; i < token.attrs.length; i++) {
127
- if (token.attrs[i][0] === 'class') {
128
- classAttr = token.attrs[i]
129
- break
130
- }
131
- }
132
- if (classAttr) {
133
- classAttr[1] = (classAttr[1] + ' ' + typeAttrs.class).trim()
134
- } else {
135
- addAttr(token, 'class', typeAttrs.class)
136
- }
125
+ addAttr(token, 'class', typeAttrs.class)
137
126
  }
138
127
  // 4. data-marker-prefix/suffix
139
128
  if (!opt.omitMarkerMetadata) {
@@ -9,12 +9,12 @@ import { buildListCloseIndexMap, findMatchingClose } from './list-helpers.js'
9
9
  * @param {Array} tokens - Token array
10
10
  * @param {Object} opt - Options
11
11
  */
12
- export function generateSpans(tokens, opt) {
12
+ export function generateSpans(tokens, opt, initialListCloseByOpen = null) {
13
13
  if (opt.useCounterStyle) {
14
14
  return
15
15
  }
16
16
  const spanClass = opt.markerSpanClass || 'li-num'
17
- let listCloseByOpen = null
17
+ let listCloseByOpen = initialListCloseByOpen
18
18
  const getListCloseByOpen = () => {
19
19
  if (!listCloseByOpen) {
20
20
  listCloseByOpen = buildListCloseIndexMap(tokens).listCloseByOpen
@@ -2,26 +2,6 @@
2
2
 
3
3
  import types from '../listTypes.json' with { type: 'json' }
4
4
 
5
- /**
6
- * Check if a marker type is convertible in default mode
7
- * Exotic markers that aren't commonly used are excluded from conversion
8
- * @param {string} markerType - The marker type name (e.g., 'decimal', 'lower-greek')
9
- * @returns {boolean} True if the marker type should be converted in default mode
10
- */
11
- export const isConvertibleMarkerType = (markerType) => {
12
- if (!markerType) return false
13
-
14
- return !EXCLUDED_MARKER_TYPES.has(markerType)
15
- }
16
-
17
- // Exclude exotic markers that should remain as <ul> in default mode.
18
- const EXCLUDED_MARKER_TYPES = new Set([
19
- 'fullwidth-lower-roman',
20
- 'fullwidth-upper-roman',
21
- 'squared-upper-latin',
22
- 'filled-squared-upper-latin'
23
- ])
24
-
25
5
  const escapeRegExp = (string) => {
26
6
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
27
7
  }
@@ -441,50 +421,19 @@ export const getSymbolForNumber = (markerType, number) => {
441
421
  return null
442
422
  }
443
423
 
444
- /**
445
- * Get the default prefix/suffix pattern for a marker type
446
- * @param {string} markerType - The marker type name (e.g., 'lower-roman', 'decimal')
447
- * @returns {Object} Object with prefix and suffix properties
448
- */
449
- export const getDefaultPatternForType = (markerType) => {
450
- const typeInfo = _TYPE_INFO_BY_NAME.get(markerType)
451
- if (!typeInfo) {
452
- return { prefix: '', suffix: '.' }
453
- }
454
-
455
- // Get patterns for this type (prefer `pattern` property)
456
- const patternRef = typeInfo.pattern || null
457
- const patterns = getPatternsByName(patternRef)
458
- if (!patterns || patterns.length === 0) {
459
- return { prefix: '', suffix: '.' }
460
- }
461
-
462
- // Return the first pattern as the default
463
- return {
464
- prefix: patterns[0].prefix || '',
465
- suffix: patterns[0].suffix || '.'
466
- }
467
- }
468
-
469
- const prefixs = [
424
+ const prefixLabels = [
470
425
  ['(', 'round'],
471
- //['[', 'square'],
472
- //['{', 'curly'],
473
- //['<', 'angle'],
474
- ['(', 'fullround'],
426
+ ['(', 'fullround']
475
427
  ]
476
428
 
477
- const suffixs = [
429
+ const suffixLabels = [
478
430
  [')', 'round'],
479
- //[']', 'square'],
480
- //['}', 'curly'],
481
- //['>', 'angle'],
482
- ['οΌ‰', 'fullround'],
431
+ ['οΌ‰', 'fullround']
483
432
  ]
484
433
 
485
434
  // Build Maps for O(1) lookups (faster than .find on every call)
486
- const prefixMap = new Map(prefixs)
487
- const suffixMap = new Map(suffixs)
435
+ const prefixMap = new Map(prefixLabels)
436
+ const suffixMap = new Map(suffixLabels)
488
437
 
489
438
  const generateClassName = (baseClass, prefix, suffix) => {
490
439
  // fast path: no prefix and no suffix
@@ -632,20 +581,13 @@ const createPatternTail = (pattern) => {
632
581
  // Process patterns for symbols
633
582
  const processSymbolPatterns = (patterns, symbols, typePatterns, type) => {
634
583
  // Pre-compute escaped prefixes, suffixes and regex tail once
635
- const patternCache = new Map()
636
- typePatterns.forEach((pattern, index) => {
637
- const escapedPrefix = pattern.prefix ? escapeRegExp(pattern.prefix) : ''
638
- const escapedSuffix = pattern.suffix ? escapeRegExp(pattern.suffix) : ''
639
- const tail = createPatternTail(pattern)
640
- patternCache.set(index, {
641
- prefix: pattern.prefix,
642
- suffix: pattern.suffix,
643
- space: pattern.space,
644
- escapedPrefix,
645
- escapedSuffix,
646
- tail
647
- })
648
- })
584
+ const patternCache = typePatterns.map(pattern => ({
585
+ prefix: pattern.prefix,
586
+ suffix: pattern.suffix,
587
+ escapedPrefix: pattern.prefix ? escapeRegExp(pattern.prefix) : '',
588
+ escapedSuffix: pattern.suffix ? escapeRegExp(pattern.suffix) : '',
589
+ tail: createPatternTail(pattern)
590
+ }))
649
591
 
650
592
  // Use pre-computed cache for faster pattern generation
651
593
  const symbolsLength = symbols.length
@@ -656,7 +598,7 @@ const processSymbolPatterns = (patterns, symbols, typePatterns, type) => {
656
598
  const processedSym = sym.replace(/^\\\\/,'\\')
657
599
 
658
600
  for (let patternIndex = 0; patternIndex < patternsLength; patternIndex++) {
659
- const cached = patternCache.get(patternIndex)
601
+ const cached = patternCache[patternIndex]
660
602
  // Original suffix variant
661
603
  const symbolPartOrig = cached.escapedPrefix + processedSym + cached.escapedSuffix
662
604
  const regexStrOrig = `^(${symbolPartOrig})${cached.tail}`
@@ -762,16 +704,13 @@ export const compiledTypes = (() => {
762
704
  }
763
705
  })()
764
706
 
765
- // Map of compiled types by name for O(1) lookup
766
- // Build a map of compiled types by name once for fast lookups
707
+ // Build a map of compiled types by name once for fast lookups.
767
708
  const _COMPILED_BY_NAME = (() => {
768
709
  const m = new Map()
769
710
  for (const t of compiledTypes()) m.set(t.name, t)
770
711
  return m
771
712
  })()
772
713
 
773
- export const compiledTypesByName = () => _COMPILED_BY_NAME
774
-
775
714
  // Build a flattened pattern list (preserve previous priority: sortedSymbolTypes then rangeBasedTypes)
776
715
  const _FLATTENED_PATTERNS = (() => {
777
716
  const arr = []
@@ -786,8 +725,6 @@ const _FLATTENED_PATTERNS = (() => {
786
725
  suffix: p.suffix,
787
726
  typeName: compiledType.name,
788
727
  symbolIndex: p.symbolIndex,
789
- num: p.num,
790
- isRange: p.isRange,
791
728
  compiled: compiled || null
792
729
  })
793
730
  }
@@ -796,6 +733,72 @@ const _FLATTENED_PATTERNS = (() => {
796
733
  })()
797
734
 
798
735
  const _TYPE_INFO_BY_NAME = getTypeSeparation().typeInfoByName
736
+ const ASCII_DIGIT_LEADS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
737
+
738
+ const getFirstCodePointChar = (text) => {
739
+ if (typeof text !== 'string' || text.length === 0) {
740
+ return null
741
+ }
742
+ const firstCodePoint = text.codePointAt(0)
743
+ if (firstCodePoint === undefined) {
744
+ return null
745
+ }
746
+ return firstCodePoint > 0xFFFF ? text.slice(0, 2) : text[0]
747
+ }
748
+
749
+ const buildEntryLeadingChars = (entry) => {
750
+ const prefixedLead = getFirstCodePointChar(entry.prefix)
751
+ if (prefixedLead) {
752
+ return [prefixedLead]
753
+ }
754
+
755
+ const typeInfo = _TYPE_INFO_BY_NAME.get(entry.typeName)
756
+ if (!typeInfo) {
757
+ return []
758
+ }
759
+
760
+ if (Array.isArray(typeInfo.symbols)) {
761
+ const symbol = typeInfo.symbols[entry.symbolIndex]
762
+ const symbolLead = getFirstCodePointChar(symbol)
763
+ return symbolLead ? [symbolLead] : []
764
+ }
765
+
766
+ if (!Array.isArray(typeInfo.range) || typeInfo.range.length !== 2) {
767
+ return []
768
+ }
769
+
770
+ if (typeof typeInfo.range[0] === 'number') {
771
+ return ASCII_DIGIT_LEADS
772
+ }
773
+
774
+ const start = typeInfo.range[0]?.codePointAt(0)
775
+ const end = typeInfo.range[1]?.codePointAt(0)
776
+ if (typeof start !== 'number' || typeof end !== 'number' || end < start) {
777
+ return []
778
+ }
779
+
780
+ const leadingChars = []
781
+ for (let codePoint = start; codePoint <= end; codePoint++) {
782
+ leadingChars.push(String.fromCodePoint(codePoint))
783
+ }
784
+ return leadingChars
785
+ }
786
+
787
+ const _FLATTENED_PATTERNS_BY_LEAD = (() => {
788
+ const buckets = new Map()
789
+ for (const entry of _FLATTENED_PATTERNS) {
790
+ const leadingChars = buildEntryLeadingChars(entry)
791
+ for (const leadingChar of leadingChars) {
792
+ let bucket = buckets.get(leadingChar)
793
+ if (!bucket) {
794
+ bucket = []
795
+ buckets.set(leadingChar, bucket)
796
+ }
797
+ bucket.push(entry)
798
+ }
799
+ }
800
+ return buckets
801
+ })()
799
802
 
800
803
  const tryMatchAgainstType = (trimmed, typeName) => {
801
804
  if (!typeName) return null
@@ -810,7 +813,12 @@ const tryMatchAgainstType = (trimmed, typeName) => {
810
813
 
811
814
  // Fast matcher over flattened list
812
815
  const tryMatchAgainstFlattened = (trimmed) => {
813
- for (const entry of _FLATTENED_PATTERNS) {
816
+ const leadingChar = getFirstCodePointChar(trimmed)
817
+ const candidates = leadingChar ? _FLATTENED_PATTERNS_BY_LEAD.get(leadingChar) : null
818
+ if (!candidates) {
819
+ return null
820
+ }
821
+ for (const entry of candidates) {
814
822
  const m = matchRegexEntry(trimmed, entry.typeName, entry)
815
823
  if (m) return m
816
824
  }
@@ -831,148 +839,3 @@ const matchRegexEntry = (trimmed, typeName, entry) => {
831
839
 
832
840
  return createMarkerResult(typeName, detectedMarker, number, entry.prefix, entry.suffix)
833
841
  }
834
-
835
- // Analyze list context to determine optimal marker type for ambiguous cases
836
- export const analyzeListMarkerContext = (markerInfos) => {
837
- if (!markerInfos || markerInfos.length === 0) return markerInfos
838
-
839
- const { symbolBasedTypes } = getTypeSeparation()
840
-
841
- // Create typeInfo lookup cache
842
- const typeInfoCache = new Map()
843
- for (const compiledType of symbolBasedTypes) {
844
- const typeInfo = _TYPE_INFO_BY_NAME.get(compiledType.name)
845
- if (typeInfo?.symbols) {
846
- typeInfoCache.set(compiledType.name, typeInfo)
847
- }
848
- }
849
-
850
- // Group markers by possible types
851
- const candidateTypes = new Map()
852
-
853
- markerInfos.forEach((markerInfo, index) => {
854
- if (!markerInfo.marker) return
855
-
856
- // Extract the actual symbol without prefix/suffix
857
- const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
858
-
859
- // Find all possible types for this marker
860
- const possibleTypes = []
861
- for (const [typeName, typeInfo] of typeInfoCache) {
862
- let symbolIndex = -1
863
- const compiled = _COMPILED_BY_NAME.get(typeName)
864
- if (compiled && compiled.symbolIndexMap) {
865
- const idx = compiled.symbolIndexMap.get(actualSymbol)
866
- symbolIndex = idx !== undefined ? idx : -1
867
- } else {
868
- symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
869
- }
870
-
871
- if (symbolIndex !== -1) {
872
- const expectedNumber = symbolIndex + getStartValue(typeInfo)
873
-
874
- possibleTypes.push({
875
- typeName,
876
- symbolIndex,
877
- expectedNumber,
878
- actualPosition: index + 1
879
- })
880
- }
881
- }
882
-
883
- possibleTypes.forEach(pt => {
884
- if (!candidateTypes.has(pt.typeName)) {
885
- candidateTypes.set(pt.typeName, {
886
- matches: 0,
887
- totalItems: markerInfos.length,
888
- positions: []
889
- })
890
- }
891
-
892
- const candidate = candidateTypes.get(pt.typeName)
893
- candidate.matches++
894
- candidate.positions.push({
895
- index,
896
- expectedNumber: pt.expectedNumber,
897
- actualPosition: pt.actualPosition,
898
- marker: markerInfo.marker
899
- })
900
- })
901
- })
902
-
903
- // Score each candidate type
904
- let bestType = null
905
- let bestScore = -1
906
-
907
- for (const [typeName, candidate] of candidateTypes) {
908
- let score = 0
909
-
910
- // Check if positions form a consecutive sequence starting from 1
911
- candidate.positions.sort((a, b) => a.index - b.index)
912
- let isConsecutiveFrom1 = true
913
- let expectedStart = 1
914
-
915
- for (let i = 0; i < candidate.positions.length; i++) {
916
- const pos = candidate.positions[i]
917
- if (pos.expectedNumber !== expectedStart + i) {
918
- isConsecutiveFrom1 = false
919
- break
920
- }
921
- }
922
-
923
- // Higher score for consecutive sequences starting from 1
924
- if (isConsecutiveFrom1 && candidate.positions.length > 0 && candidate.positions[0].expectedNumber === 1) {
925
- score += 100
926
- }
927
-
928
- // Higher score for more matches
929
- score += candidate.matches * 10
930
-
931
- // Higher score for covering all items
932
- if (candidate.matches === candidate.totalItems) {
933
- score += 50
934
- }
935
-
936
- if (score > bestScore) {
937
- bestScore = score
938
- bestType = typeName
939
- }
940
- }
941
-
942
- // If we found a better type, update all marker infos
943
- if (bestType && candidateTypes.get(bestType).matches > 0) {
944
- const typeInfo = typeInfoCache.get(bestType)
945
- if (typeInfo) {
946
- const updatedMarkerInfos = markerInfos.map((markerInfo, index) => {
947
- if (!markerInfo.marker) return markerInfo
948
-
949
- // Extract the actual symbol without prefix/suffix
950
- const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
951
-
952
- // Use precomputed symbolIndexMap if available
953
- const compiled = _COMPILED_BY_NAME.get(bestType)
954
- let symbolIndex = -1
955
- if (compiled && compiled.symbolIndexMap) {
956
- const idx = compiled.symbolIndexMap.get(actualSymbol)
957
- symbolIndex = idx !== undefined ? idx : -1
958
- } else {
959
- symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
960
- }
961
- if (symbolIndex !== -1) {
962
- const number = calculateNumber(typeInfo, actualSymbol)
963
-
964
- return {
965
- ...markerInfo,
966
- type: bestType,
967
- number: number
968
- }
969
- }
970
- return markerInfo
971
- })
972
-
973
- return updatedMarkerInfos
974
- }
975
- }
976
-
977
- return markerInfos
978
- }