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

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.
@@ -1,991 +1,995 @@
1
- // ListTypes.json related utilities and marker processing
2
-
3
- import types from '../listTypes.json' with { type: 'json' }
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
- // Exclude exotic markers that should remain as <ul> in default mode
15
- // These are rarely used and may not be well-supported
16
- const excludedTypes = [
17
- 'fullwidth-lower-roman',
18
- 'fullwidth-upper-roman',
19
- 'squared-upper-latin',
20
- 'filled-squared-upper-latin'
21
- ]
22
-
23
- return !excludedTypes.includes(markerType)
24
- }
25
-
26
- const escapeRegExp = (string) => {
27
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
28
- }
29
-
30
- // Normalize fullwidth digits to ASCII digits
31
- const normalizeFullwidthDigits = (str) => {
32
- if (!str || typeof str !== 'string') return str
33
- return str.replace(/[0-9]/g, ch => {
34
- const code = ch.codePointAt(0)
35
- return String.fromCharCode(code - 0xFF10 + 48)
36
- })
37
- }
38
-
39
- // Resolve top-level pattern groups once at module initialization.
40
- // Use `patternGroups` top-level key from `listTypes.json`.
41
- const PATTERN_GROUPS = Array.isArray(types.patternGroups) ? types.patternGroups : []
42
-
43
- // Map for O(1) name -> group lookup
44
- const PATTERN_GROUP_MAP = new Map((PATTERN_GROUPS || []).map(g => [g.name, g]))
45
-
46
- /**
47
- * Get pattern list by name.
48
- * Accepts a single name (string), an array of names, or a pattern-group-like object.
49
- */
50
- const getPatternsByName = (patternName) => {
51
- if (!patternName) return []
52
- // If caller passed the actual group object
53
- if (typeof patternName === 'object' && Array.isArray(patternName.patterns)) {
54
- return patternName.patterns
55
- }
56
-
57
- // If caller passed an array of group names, merge their patterns in order
58
- if (Array.isArray(patternName)) {
59
- const out = []
60
- for (const name of patternName) {
61
- if (typeof name !== 'string') continue
62
- const g = PATTERN_GROUP_MAP.get(name)
63
- if (g && Array.isArray(g.patterns)) out.push(...g.patterns)
64
- }
65
- return out
66
- }
67
-
68
- // Single name lookup (O(1) via map)
69
- if (typeof patternName === 'string') {
70
- const patternGroup = PATTERN_GROUP_MAP.get(patternName)
71
- return Array.isArray(patternGroup?.patterns) ? patternGroup.patterns : []
72
- }
73
-
74
- return []
75
- }
76
-
77
- // Precompiled common regexes to avoid recreating them repeatedly
78
- const ROMAN_LIKE_REGEX = /^[IVX]+\.?$/i
79
- const LATIN_LETTER_REGEX = /^[a-z]$/i
80
- const MARKER_FALLBACK_REGEX = /^(\S+)/
81
- const PURE_PREFIX_REMOVAL_REGEX = /^[\((]+/
82
-
83
- // Build dynamic suffix character classes from `listTypes.json` patterns so
84
- // edits to that file don't require touching this code.
85
- const _buildSuffixCharSets = () => {
86
- const all = new Set()
87
- const fullwidth = new Set()
88
- const groups = PATTERN_GROUPS
89
- if (groups.length > 0) {
90
- for (const group of groups) {
91
- if (!group || !Array.isArray(group.patterns)) continue
92
- for (const p of group.patterns) {
93
- if (!p || !p.suffix) continue
94
- for (const ch of Array.from(p.suffix)) {
95
- all.add(ch)
96
- const cp = ch.codePointAt(0)
97
- if (cp !== undefined && cp > 0xFF) fullwidth.add(ch)
98
- }
99
- }
100
- }
101
- }
102
- return { all, fullwidth }
103
- }
104
-
105
- const { all: _ALL_SUFFIX_CHARS, fullwidth: _FULLWIDTH_SUFFIX_CHARS } = _buildSuffixCharSets()
106
-
107
- // Reuse existing `escapeRegExp` for char-class escaping to avoid duplication
108
- const SUFFIX_CHAR_CLASS = [..._ALL_SUFFIX_CHARS].map(ch => escapeRegExp(ch)).join('')
109
-
110
- const SUFFIX_CLASS_FOR_REGEX = SUFFIX_CHAR_CLASS.length > 0 ? `[${SUFFIX_CHAR_CLASS}]` : "\\."
111
-
112
- const MARKER_SUFFIX_SPACE_REGEX = new RegExp(`^([^\\s]+?${SUFFIX_CLASS_FOR_REGEX})\\s`)
113
- const MARKER_SUFFIX_NO_SPACE_REGEX = new RegExp(`^([^\\s]+?${SUFFIX_CLASS_FOR_REGEX})(?=[^\\s])`)
114
- const PURE_SUFFIXES_REMOVAL_REGEX = new RegExp(`${SUFFIX_CLASS_FOR_REGEX}+$`)
115
-
116
- // Cached type separation
117
- let _symbolBasedTypes = null
118
- let _rangeBasedTypes = null
119
- let _sortedSymbolTypes = null
120
- let _typeInfoByName = null
121
-
122
- const getTypeSeparation = () => {
123
- if (_symbolBasedTypes === null) {
124
- const allTypes = compiledTypes()
125
- _symbolBasedTypes = []
126
- _rangeBasedTypes = []
127
- _typeInfoByName = new Map()
128
-
129
- // Create typeInfo lookup map
130
- for (const type of types.types) {
131
- _typeInfoByName.set(type.name, type)
132
- }
133
-
134
- for (const compiledType of allTypes) {
135
- const typeInfo = _typeInfoByName.get(compiledType.name)
136
- if (typeInfo?.symbols) {
137
- _symbolBasedTypes.push(compiledType)
138
- } else {
139
- _rangeBasedTypes.push(compiledType)
140
- }
141
- }
142
-
143
- // Sort symbol-based types to prioritize Roman numerals over Latin letters
144
- _sortedSymbolTypes = [..._symbolBasedTypes].sort((a, b) => {
145
- if (a.name.includes('roman') && b.name.includes('latin')) return -1
146
- if (a.name.includes('latin') && b.name.includes('roman')) return 1
147
- return 0
148
- })
149
- }
150
- return {
151
- symbolBasedTypes: _symbolBasedTypes,
152
- rangeBasedTypes: _rangeBasedTypes,
153
- sortedSymbolTypes: _sortedSymbolTypes,
154
- typeInfoByName: _typeInfoByName
155
- }
156
- }
157
-
158
- /**
159
- * Get start value from type info
160
- * @param {Object} typeInfo - Type information
161
- * @returns {number} Start value (default 1)
162
- */
163
- const getStartValue = (typeInfo) => typeInfo?.start !== undefined ? typeInfo.start : 1
164
-
165
- /**
166
- * Check if symbols match a specific sequence pattern
167
- * @param {Array<string>} pureSymbols - Array of pure symbols (without prefix/suffix)
168
- * @param {Array<string>} sequence - Expected sequence array
169
- * @param {string} typeName - Name of the type to return
170
- * @param {boolean} allSame - Whether all symbols are the same
171
- * @returns {Object|null} Type object or null
172
- */
173
- const checkSequenceMatch = (pureSymbols, sequence, typeName, allSame) => {
174
- if (allSame && pureSymbols.length >= 1) {
175
- if (sequence.indexOf(pureSymbols[0]) !== -1) {
176
- return { type: typeName }
177
- }
178
- } else if (pureSymbols.length >= 2) {
179
- // Find where the first symbol appears in the sequence
180
- const startIndex = sequence.indexOf(pureSymbols[0])
181
- if (startIndex === -1) return null
182
-
183
- // Check if all symbols match a consecutive part of the sequence
184
- let isValidSequence = true
185
- for (let i = 0; i < pureSymbols.length; i++) {
186
- const expectedIndex = startIndex + i
187
- if (expectedIndex >= sequence.length || pureSymbols[i] !== sequence[expectedIndex]) {
188
- isValidSequence = false
189
- break
190
- }
191
- }
192
- if (isValidSequence) {
193
- return { type: typeName }
194
- }
195
- }
196
- return null
197
- }
198
-
199
- /**
200
- * Check if array matches expected sequence from start
201
- * @param {Array<string>} actual - Actual symbols
202
- * @param {Array<string>} expected - Expected sequence
203
- * @returns {boolean} True if matches
204
- */
205
- const matchesSequence = (actual, expected) => {
206
- for (let i = 0; i < actual.length; i++) {
207
- if (actual[i] !== expected[i]) {
208
- return false
209
- }
210
- }
211
- return true
212
- }
213
-
214
- /**
215
- * Extract pure symbol by removing prefix and suffix
216
- * @param {string} marker - The marker with prefix/suffix
217
- * @param {string|null} prefix - The prefix to remove
218
- * @param {string|null} suffix - The suffix to remove
219
- * @returns {string} Pure symbol without prefix/suffix
220
- */
221
- const extractPureSymbol = (marker, prefix, suffix) => {
222
- let start = prefix ? prefix.length : 0
223
- let end = suffix ? marker.length - suffix.length : marker.length
224
- return marker.substring(start, end)
225
- }
226
-
227
- /**
228
- * Calculate number for a marker based on type info
229
- * @param {Object} typeInfo - Type information from listTypes.json
230
- * @param {string} pureSymbol - Pure symbol without prefix/suffix
231
- * @returns {number|undefined} The calculated number
232
- */
233
- const calculateNumber = (typeInfo, pureSymbol, compiled = null) => {
234
- if (!typeInfo || !pureSymbol) return undefined
235
-
236
- if (typeInfo.symbols) {
237
- // Symbol-based types (katakana, roman numerals, etc.)
238
- // Prefer a provided compiled object (with symbolIndexMap) to avoid map lookups.
239
- if (compiled && compiled.symbolIndexMap) {
240
- const idx = compiled.symbolIndexMap.get(pureSymbol)
241
- return idx !== undefined ? idx + getStartValue(typeInfo) : undefined
242
- }
243
-
244
- // Fall back to looking up compiled map once (lazy) if compiled not provided
245
- const compiledFallback = _COMPILED_BY_NAME.get(typeInfo.name)
246
- if (compiledFallback && compiledFallback.symbolIndexMap) {
247
- const idx = compiledFallback.symbolIndexMap.get(pureSymbol)
248
- return idx !== undefined ? idx + getStartValue(typeInfo) : undefined
249
- }
250
-
251
- const symbolIndex = typeInfo.symbols.indexOf(pureSymbol)
252
- return symbolIndex !== -1 ? symbolIndex + getStartValue(typeInfo) : undefined
253
- } else if (typeInfo.range) {
254
- // Range-based types (lower-latin, upper-latin, decimal)
255
- const startValue = getStartValue(typeInfo)
256
- if (Array.isArray(typeInfo.range) && typeof typeInfo.range[0] === 'string') {
257
- // Latin range: ["a", "z"] or ["A", "Z"]
258
- const startChar = typeInfo.range[0]
259
- if (pureSymbol.length > 0) {
260
- // Use codePointAt for proper Unicode handling (including surrogate pairs)
261
- const charCode = pureSymbol.codePointAt(0)
262
- const startCharCode = startChar.codePointAt(0)
263
- return charCode - startCharCode + startValue
264
- }
265
- } else {
266
- // Numeric range: [start, end]
267
- const norm = normalizeFullwidthDigits(pureSymbol)
268
- const numValue = parseInt(norm, 10)
269
- if (!isNaN(numValue)) {
270
- return numValue
271
- }
272
- }
273
- }
274
-
275
- return undefined
276
- }
277
-
278
- // Detect sequence pattern from multiple contents
279
- export const detectSequencePattern = (allContents) => {
280
- if (!allContents || allContents.length < 1) return null
281
-
282
- // Extract pure markers from all contents
283
- const markers = allContents.map(content => {
284
- const trimmed = content.trim()
285
- // First try: symbol(s) followed by suffix and optional space
286
- let match = trimmed.match(MARKER_SUFFIX_SPACE_REGEX)
287
- if (match) return match[1]
288
-
289
- // Second try: symbol(s) followed by suffix at end (no space after)
290
- match = trimmed.match(MARKER_SUFFIX_NO_SPACE_REGEX)
291
- if (match) return match[1]
292
-
293
- // Fallback: everything before first space
294
- match = trimmed.match(MARKER_FALLBACK_REGEX)
295
- return match ? match[1] : trimmed
296
- })
297
-
298
- // Extract pure symbols (remove prefixes/suffixes)
299
- const pureSymbols = markers.map(marker => {
300
- return marker.replace(PURE_SUFFIXES_REMOVAL_REGEX, '').replace(PURE_PREFIX_REMOVAL_REGEX, '')
301
- })
302
-
303
- // Check if all symbols are the same (repeated marker case)
304
- const allSame = pureSymbols.every(s => s === pureSymbols[0])
305
-
306
- // Cache type lookups
307
- const { typeInfoByName } = getTypeSeparation()
308
- const irohaType = typeInfoByName.get('katakana-iroha')
309
- const katakanaType = typeInfoByName.get('katakana')
310
- const upperRomanType = typeInfoByName.get('upper-roman')
311
-
312
- // Get symbol sequences from listTypes.json
313
- if (irohaType?.symbols) {
314
- const irohaResult = checkSequenceMatch(pureSymbols, irohaType.symbols, 'katakana-iroha', allSame)
315
- if (irohaResult) return irohaResult
316
- }
317
-
318
- if (katakanaType?.symbols) {
319
- const katakanaResult = checkSequenceMatch(pureSymbols, katakanaType.symbols, 'katakana', allSame)
320
- if (katakanaResult) return katakanaResult
321
- }
322
-
323
- // Check Roman numerals from listTypes.json
324
- if (upperRomanType?.symbols) {
325
- const allRomanLike = markers.every(marker => ROMAN_LIKE_REGEX.test(marker))
326
-
327
- if (allRomanLike) {
328
- const romanSymbols = markers.map(marker => marker.replace(/\.$/, '').toUpperCase())
329
- if (matchesSequence(romanSymbols, upperRomanType.symbols)) {
330
- return { type: 'upper-roman' }
331
- }
332
- }
333
- }
334
-
335
- // Check Latin letters (range-based types)
336
- const allLatinLike = pureSymbols.every(symbol => LATIN_LETTER_REGEX.test(symbol))
337
-
338
- if (allLatinLike && pureSymbols.length >= 2) {
339
- const firstSymbol = pureSymbols[0]
340
- const isLowerCase = firstSymbol === firstSymbol.toLowerCase()
341
- const baseCharCode = isLowerCase ? firstSymbol.charCodeAt(0) : firstSymbol.toUpperCase().charCodeAt(0)
342
-
343
- const expectedSequence = pureSymbols.map((_, i) => {
344
- const char = String.fromCharCode(baseCharCode + i)
345
- return isLowerCase ? char : char.toUpperCase()
346
- })
347
- const actualSequence = pureSymbols.map(s => isLowerCase ? s : s.toUpperCase())
348
-
349
- if (matchesSequence(actualSequence, expectedSequence)) {
350
- return { type: isLowerCase ? 'lower-latin' : 'upper-latin' }
351
- }
352
- }
353
-
354
- return null
355
- }
356
-
357
- /**
358
- * Create marker detection result object
359
- * @param {string} type - Marker type name
360
- * @param {string} marker - Detected marker
361
- * @param {number|undefined} number - Calculated number
362
- * @param {string|null} prefix - Prefix character
363
- * @param {string|null} suffix - Suffix character
364
- * @returns {Object} Detection result
365
- */
366
- const createMarkerResult = (type, marker, number, prefix, suffix) => ({
367
- type,
368
- marker,
369
- number,
370
- prefix,
371
- suffix
372
- })
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
- // Enhanced marker type detection with context awareness
390
- export const detectMarkerType = (content, allContents = null) => {
391
- if (!content || typeof content !== 'string') {
392
- return { type: null, marker: null }
393
- }
394
-
395
- const trimmed = content.trim()
396
- if (!trimmed) return { type: null, marker: null }
397
-
398
- const { sortedSymbolTypes, rangeBasedTypes, typeInfoByName } = getTypeSeparation()
399
-
400
- // If we have context (even single element), try to detect the overall pattern first
401
- if (allContents && Array.isArray(allContents) && allContents.length >= 1) {
402
- const contextResult = detectSequencePattern(allContents)
403
- if (contextResult) {
404
- // If context suggests a specific type, verify current content matches
405
- const typeInfo = typeInfoByName.get(contextResult.type)
406
- if (typeInfo) {
407
- // Check both symbol-based and range-based types
408
- const allCompiledTypes = [...sortedSymbolTypes, ...rangeBasedTypes]
409
- for (const compiledType of allCompiledTypes) {
410
- if (compiledType.name === contextResult.type) {
411
- const matchResult = tryMatchPattern(trimmed, compiledType, typeInfo)
412
- if (matchResult) return matchResult
413
- }
414
- }
415
- }
416
- }
417
- }
418
-
419
- // Fallback to original logic
420
- // Fast fallback: try a flattened precompiled pattern list to avoid nested loops
421
- const flatMatch = tryMatchAgainstFlattened(trimmed)
422
- if (flatMatch) return flatMatch
423
-
424
- return { type: null, marker: null }
425
- }
426
-
427
- /**
428
- * Get the symbol for a specific number in a marker type
429
- * @param {string} markerType - The marker type name (e.g., 'katakana-iroha', 'lower-roman')
430
- * @param {number} number - The 1-based number (e.g., 1 for first item, 2 for second)
431
- * @returns {string|null} The symbol for that number, or null if not found
432
- */
433
- export const getSymbolForNumber = (markerType, number) => {
434
- const { typeInfoByName } = getTypeSeparation()
435
- const typeInfo = typeInfoByName.get(markerType)
436
- if (!typeInfo) {
437
- return null
438
- }
439
-
440
- // For symbol-based types
441
- if (typeInfo.symbols && Array.isArray(typeInfo.symbols)) {
442
- const startValue = getStartValue(typeInfo)
443
- const index = number - startValue
444
- if (index >= 0 && index < typeInfo.symbols.length) {
445
- return typeInfo.symbols[index]
446
- }
447
- }
448
-
449
- // For range-based types (latin, decimal)
450
- if (typeInfo.range) {
451
- const startValue = getStartValue(typeInfo)
452
- if (Array.isArray(typeInfo.range) && typeInfo.range.length === 2) {
453
- // Latin range: ["a", "z"] or ["A", "Z"]
454
- const startChar = typeInfo.range[0]
455
- // Ensure startChar is a string
456
- if (typeof startChar === 'string' && startChar.length > 0) {
457
- // Use codePointAt and fromCodePoint for proper Unicode handling
458
- const startCharCode = startChar.codePointAt(0)
459
- const offset = number - startValue
460
- const targetCharCode = startCharCode + offset
461
- return String.fromCodePoint(targetCharCode)
462
- }
463
- }
464
- // Numeric range or fallback
465
- return String(number)
466
- }
467
-
468
- return null
469
- }
470
-
471
- /**
472
- * Get the default prefix/suffix pattern for a marker type
473
- * @param {string} markerType - The marker type name (e.g., 'lower-roman', 'decimal')
474
- * @returns {Object} Object with prefix and suffix properties
475
- */
476
- export const getDefaultPatternForType = (markerType) => {
477
- const { typeInfoByName } = getTypeSeparation()
478
- const typeInfo = typeInfoByName.get(markerType)
479
- if (!typeInfo) {
480
- return { prefix: '', suffix: '.' }
481
- }
482
-
483
- // Get patterns for this type (prefer `pattern` property)
484
- const patternRef = typeInfo.pattern || null
485
- const patterns = getPatternsByName(patternRef)
486
- if (!patterns || patterns.length === 0) {
487
- return { prefix: '', suffix: '.' }
488
- }
489
-
490
- // Return the first pattern as the default
491
- return {
492
- prefix: patterns[0].prefix || '',
493
- suffix: patterns[0].suffix || '.'
494
- }
495
- }
496
-
497
- const prefixs = [
498
- ['(', 'round'],
499
- //['[', 'square'],
500
- //['{', 'curly'],
501
- //['<', 'angle'],
502
- ['(', 'fullround'],
503
- ]
504
-
505
- const suffixs = [
506
- [')', 'round'],
507
- //[']', 'square'],
508
- //['}', 'curly'],
509
- //['>', 'angle'],
510
- [')', 'fullround'],
511
- ]
512
-
513
- // Build Maps for O(1) lookups (faster than .find on every call)
514
- const prefixMap = new Map(prefixs)
515
- const suffixMap = new Map(suffixs)
516
-
517
- const generateClassName = (baseClass, prefix, suffix) => {
518
- // fast path: no prefix and no suffix
519
- if (!prefix && !suffix) return baseClass
520
-
521
- // O(1) map lookups
522
- const prefixName = prefixMap.get(prefix) || null
523
- const suffixName = suffixMap.get(suffix) || null
524
-
525
- // If neither side matches known labels, return baseClass
526
- if (!prefixName && !suffixName) return baseClass
527
-
528
- const p = prefixName ? prefixName : 'none'
529
- const s = suffixName ? suffixName : 'none'
530
- return `${baseClass}-with-${p}-${s}`
531
- }
532
-
533
- export const getTypeAttributes = (markerType, markerInfo = null) => {
534
- const { typeInfoByName } = getTypeSeparation()
535
- const type = typeInfoByName.get(markerType)
536
- if (!type) {
537
- return { type: '1', class: 'ol-decimal', suffix: '.' }
538
- }
539
-
540
- // Get prefix/suffix from markerInfo if provided
541
- const detectedPrefix = markerInfo?.prefix || null
542
- const detectedSuffix = markerInfo?.suffix || '.'
543
-
544
- // Standard marker types mapping
545
- const standardTypes = {
546
- 'decimal': { type: '1', baseClass: 'ol-decimal' },
547
- 'lower-latin': { type: 'a', baseClass: 'ol-lower-latin' },
548
- 'upper-latin': { type: 'A', baseClass: 'ol-upper-latin' },
549
- 'lower-roman': { type: 'i', baseClass: 'ol-lower-roman' },
550
- 'upper-roman': { type: 'I', baseClass: 'ol-upper-roman' }
551
- }
552
-
553
- // Custom marker types with no suffix
554
- const customTypesNoSuffix = [
555
- 'filled-circled-decimal', 'circled-decimal',
556
- 'circled-upper-latin', 'filled-circled-upper-latin',
557
- 'circled-lower-latin', 'katakana', 'katakana-iroha'
558
- ]
559
-
560
- let mappedType
561
- let suffix = detectedSuffix
562
- let prefix = detectedPrefix
563
- let customMarker = false
564
-
565
- if (standardTypes[type.name]) {
566
- const std = standardTypes[type.name]
567
- mappedType = {
568
- type: std.type,
569
- class: generateClassName(std.baseClass, prefix, suffix)
570
- }
571
- } else {
572
- // Custom marker types
573
- mappedType = { type: '1', class: `ol-${type.name}` }
574
- customMarker = true
575
- if (customTypesNoSuffix.includes(type.name)) {
576
- suffix = null
577
- }
578
- }
579
-
580
- const result = {
581
- type: customMarker ? null : mappedType.type, // No type attribute for custom markers
582
- class: mappedType.class,
583
- suffix: suffix,
584
- customMarker: customMarker,
585
- start: getStartValue(type)
586
- }
587
-
588
- // Set role="list" for custom markers
589
- if (customMarker) {
590
- result.role = 'list'
591
- }
592
-
593
- if (prefix) {
594
- result.prefix = prefix
595
- }
596
-
597
- if (type.symbols) {
598
- result.symbols = type.symbols
599
- }
600
-
601
- return result
602
- }
603
-
604
-
605
-
606
- // Precompute regex tail (endCheck + spacePattern) for a pattern to avoid recomputing
607
- const createPatternTail = (pattern) => {
608
- // Determine if the pattern's suffix (if any) contains any fullwidth suffix
609
- // characters declared in `listTypes.json`.
610
- let isFullWidthSuffix = false
611
- if (pattern.suffix) {
612
- for (const ch of Array.from(pattern.suffix)) {
613
- if (_FULLWIDTH_SUFFIX_CHARS.has(ch)) {
614
- isFullWidthSuffix = true
615
- break
616
- }
617
- }
618
- }
619
-
620
- // Compute default space handling based on whether suffix exists and
621
- // whether suffix contains a fullwidth character. This is the fallback
622
- // behavior when `pattern.space` is not provided.
623
- const defaultSpaceIfSuffix = isFullWidthSuffix ? '([  ])?' : '([  ])+'
624
- const defaultSpaceNoSuffix = '(?=[  ]|$)([  ])*'
625
-
626
- let spacePattern
627
-
628
- if (pattern.space) {
629
- // Explicit directive from listTypes.json wins
630
- switch (pattern.space) {
631
- case 'half':
632
- spacePattern = '([ ])+'
633
- break
634
- case 'both':
635
- spacePattern = '([  ])+'
636
- break
637
- case 'none_or_both':
638
- spacePattern = pattern.suffix ? '([  ]+)?' : defaultSpaceNoSuffix
639
- break
640
- default:
641
- // Unknown explicit value: fall back to defaults
642
- spacePattern = pattern.suffix ? defaultSpaceIfSuffix : defaultSpaceNoSuffix
643
- }
644
- } else {
645
- // No explicit `pattern.space`: use defaults
646
- spacePattern = pattern.suffix ? defaultSpaceIfSuffix : defaultSpaceNoSuffix
647
- }
648
-
649
- let endCheck = ''
650
- if (!pattern.suffix && pattern.prefix) {
651
- endCheck = '(?=[  ]|$)'
652
- }
653
-
654
- return `${endCheck}${spacePattern}`
655
- }
656
-
657
- // Process patterns for symbols
658
- const processSymbolPatterns = (patterns, symbols, typePatterns, type) => {
659
- // Pre-compute escaped prefixes, suffixes and regex tail once
660
- const patternCache = new Map()
661
- typePatterns.forEach((pattern, index) => {
662
- const escapedPrefix = pattern.prefix ? escapeRegExp(pattern.prefix) : ''
663
- const escapedSuffix = pattern.suffix ? escapeRegExp(pattern.suffix) : ''
664
- const tail = createPatternTail(pattern)
665
- patternCache.set(index, {
666
- prefix: pattern.prefix,
667
- suffix: pattern.suffix,
668
- space: pattern.space,
669
- escapedPrefix,
670
- escapedSuffix,
671
- tail
672
- })
673
- })
674
-
675
- // Use pre-computed cache for faster pattern generation
676
- const symbolsLength = symbols.length
677
- const patternsLength = typePatterns.length
678
-
679
- for (let symbolIndex = 0; symbolIndex < symbolsLength; symbolIndex++) {
680
- const sym = symbols[symbolIndex]
681
- const processedSym = sym.replace(/^\\\\/,'\\')
682
-
683
- for (let patternIndex = 0; patternIndex < patternsLength; patternIndex++) {
684
- const cached = patternCache.get(patternIndex)
685
- // Original suffix variant
686
- const symbolPartOrig = cached.escapedPrefix + processedSym + cached.escapedSuffix
687
- const regexStrOrig = `^(${symbolPartOrig})${cached.tail}`
688
- patterns.push({
689
- regex: new RegExp(regexStrOrig, 'u'),
690
- prefix: cached.prefix,
691
- suffix: cached.suffix,
692
- symbolIndex,
693
- num: symbolIndex + type.start
694
- })
695
-
696
- // Do not generate additional suffix variants — respect patterns from listTypes.json only.
697
- }
698
-
699
- // Only generate patterns that directly correspond to `typePatterns` entries.
700
- }
701
- }
702
-
703
- // Process range patterns
704
- const processRangePatterns = (patterns, typePatterns, type) => {
705
- // Pre-calculate symbol pattern once
706
- let symbolPattern
707
- if (typeof type.range[0] === 'number') {
708
- // Allow only ASCII digits here do not synthesize fullwidth digit variants.
709
- symbolPattern = '(?:\\d+)'
710
- } else {
711
- const start = type.range[0].codePointAt(0)
712
- const end = type.range[1].codePointAt(0)
713
-
714
- // Handle Unicode characters properly
715
- if (start > 0xFFFF || end > 0xFFFF) {
716
- // Unicode characters beyond BMP - enumerate all characters
717
- const chars = []
718
- for (let code = start; code <= end; code++) {
719
- chars.push(String.fromCodePoint(code))
720
- }
721
- symbolPattern = `[${chars.join('').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}]`
722
- } else {
723
- // Standard ASCII/BMP characters
724
- symbolPattern = `[${String.fromCharCode(start)}-${String.fromCharCode(end)}]`
725
- }
726
- }
727
-
728
- // Manual loop instead of forEach for better performance
729
- const patternsLength = typePatterns.length
730
- for (let i = 0; i < patternsLength; i++) {
731
- const pattern = typePatterns[i]
732
- const escapedPrefix = pattern.prefix ? escapeRegExp(pattern.prefix) : ''
733
- const escapedSuffix = pattern.suffix ? escapeRegExp(pattern.suffix) : ''
734
- const tail = createPatternTail(pattern)
735
-
736
- const symbolPart = escapedPrefix + symbolPattern + escapedSuffix
737
- const regexStr = `^(${symbolPart})${tail}`
738
- patterns.push({
739
- regex: new RegExp(regexStr, 'u'),
740
- prefix: pattern.prefix,
741
- suffix: pattern.suffix,
742
- symbolIndex: 0,
743
- num: type.start,
744
- isRange: true,
745
- rangeType: typeof type.range[0] === 'number' ? 'numeric' : 'alphabetic'
746
- })
747
-
748
- // Note: Do not synthesize additional suffix variants here.
749
- // Matching must be driven solely by patterns defined in `listTypes.json`.
750
- }
751
- }
752
-
753
- // Compiled types with caching
754
- export const compiledTypes = (() => {
755
- let _cache = null
756
- return () => {
757
- if (_cache === null) {
758
- _cache = types.types.map(type => {
759
- const patterns = []
760
- // Per-type `pattern` references a `patternGroups` entry
761
- const typePatternRef = type.pattern || null
762
- const typePatterns = getPatternsByName(typePatternRef)
763
-
764
- // Build symbol->index map for symbol-based types to avoid indexOf scans
765
- let symbolIndexMap = null
766
- if (type.symbols && Array.isArray(type.symbols)) {
767
- symbolIndexMap = new Map()
768
- for (let i = 0; i < type.symbols.length; i++) {
769
- symbolIndexMap.set(type.symbols[i], i)
770
- }
771
- }
772
-
773
- if (type.symbols) {
774
- processSymbolPatterns(patterns, type.symbols, typePatterns, type)
775
- } else if (type.range) {
776
- processRangePatterns(patterns, typePatterns, type)
777
- }
778
-
779
- return {
780
- name: type.name,
781
- patterns,
782
- symbolIndexMap
783
- }
784
- })
785
- }
786
- return _cache
787
- }
788
- })()
789
-
790
- // Map of compiled types by name for O(1) lookup
791
- // Build a map of compiled types by name once for fast lookups
792
- const _COMPILED_BY_NAME = (() => {
793
- const m = new Map()
794
- for (const t of compiledTypes()) m.set(t.name, t)
795
- return m
796
- })()
797
-
798
- export const compiledTypesByName = () => _COMPILED_BY_NAME
799
-
800
- // Build a flattened pattern list (preserve previous priority: sortedSymbolTypes then rangeBasedTypes)
801
- const _FLATTENED_PATTERNS = (() => {
802
- const arr = []
803
- const { sortedSymbolTypes, rangeBasedTypes } = getTypeSeparation()
804
- const source = [...sortedSymbolTypes, ...rangeBasedTypes]
805
- for (const compiledType of source) {
806
- const compiled = _COMPILED_BY_NAME.get(compiledType.name)
807
- for (const p of compiledType.patterns) {
808
- arr.push({
809
- regex: p.regex,
810
- prefix: p.prefix,
811
- suffix: p.suffix,
812
- typeName: compiledType.name,
813
- symbolIndex: p.symbolIndex,
814
- num: p.num,
815
- isRange: p.isRange,
816
- compiled: compiled || null
817
- })
818
- }
819
- }
820
- return arr
821
- })()
822
-
823
- // Fast matcher over flattened list
824
- const tryMatchAgainstFlattened = (trimmed) => {
825
- for (const entry of _FLATTENED_PATTERNS) {
826
- const m = matchRegexEntry(trimmed, entry.typeName, entry)
827
- if (m) return m
828
- }
829
- return null
830
- }
831
-
832
- // Helper to match a trimmed string against a pattern entry and produce a result.
833
- // `entry` is expected to have `regex`, `prefix`, `suffix` and optionally `compiled`.
834
- const matchRegexEntry = (trimmed, typeName, entry) => {
835
- const result = trimmed.match(entry.regex)
836
- if (!result) return null
837
-
838
- const detectedMarker = result[1]
839
- const pureSymbol = extractPureSymbol(detectedMarker, entry.prefix, entry.suffix)
840
- const { typeInfoByName } = getTypeSeparation()
841
- const typeInfo = typeInfoByName.get(typeName)
842
- const compiledForCalc = entry.compiled || _COMPILED_BY_NAME.get(typeName)
843
- const number = calculateNumber(typeInfo, pureSymbol, compiledForCalc)
844
-
845
- return createMarkerResult(typeName, detectedMarker, number, entry.prefix, entry.suffix)
846
- }
847
-
848
- // Analyze list context to determine optimal marker type for ambiguous cases
849
- export const analyzeListMarkerContext = (markerInfos) => {
850
- if (!markerInfos || markerInfos.length === 0) return markerInfos
851
-
852
- const { symbolBasedTypes, typeInfoByName } = getTypeSeparation()
853
-
854
- // Create typeInfo lookup cache
855
- const typeInfoCache = new Map()
856
- for (const compiledType of symbolBasedTypes) {
857
- const typeInfo = typeInfoByName.get(compiledType.name)
858
- if (typeInfo?.symbols) {
859
- typeInfoCache.set(compiledType.name, typeInfo)
860
- }
861
- }
862
-
863
- // Group markers by possible types
864
- const candidateTypes = new Map()
865
-
866
- markerInfos.forEach((markerInfo, index) => {
867
- if (!markerInfo.marker) return
868
-
869
- // Extract the actual symbol without prefix/suffix
870
- const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
871
-
872
- // Find all possible types for this marker
873
- const possibleTypes = []
874
- for (const [typeName, typeInfo] of typeInfoCache) {
875
- let symbolIndex = -1
876
- const compiled = _COMPILED_BY_NAME.get(typeName)
877
- if (compiled && compiled.symbolIndexMap) {
878
- const idx = compiled.symbolIndexMap.get(actualSymbol)
879
- symbolIndex = idx !== undefined ? idx : -1
880
- } else {
881
- symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
882
- }
883
-
884
- if (symbolIndex !== -1) {
885
- const expectedNumber = symbolIndex + getStartValue(typeInfo)
886
-
887
- possibleTypes.push({
888
- typeName,
889
- symbolIndex,
890
- expectedNumber,
891
- actualPosition: index + 1
892
- })
893
- }
894
- }
895
-
896
- possibleTypes.forEach(pt => {
897
- if (!candidateTypes.has(pt.typeName)) {
898
- candidateTypes.set(pt.typeName, {
899
- matches: 0,
900
- totalItems: markerInfos.length,
901
- positions: []
902
- })
903
- }
904
-
905
- const candidate = candidateTypes.get(pt.typeName)
906
- candidate.matches++
907
- candidate.positions.push({
908
- index,
909
- expectedNumber: pt.expectedNumber,
910
- actualPosition: pt.actualPosition,
911
- marker: markerInfo.marker
912
- })
913
- })
914
- })
915
-
916
- // Score each candidate type
917
- let bestType = null
918
- let bestScore = -1
919
-
920
- for (const [typeName, candidate] of candidateTypes) {
921
- let score = 0
922
-
923
- // Check if positions form a consecutive sequence starting from 1
924
- candidate.positions.sort((a, b) => a.index - b.index)
925
- let isConsecutiveFrom1 = true
926
- let expectedStart = 1
927
-
928
- for (let i = 0; i < candidate.positions.length; i++) {
929
- const pos = candidate.positions[i]
930
- if (pos.expectedNumber !== expectedStart + i) {
931
- isConsecutiveFrom1 = false
932
- break
933
- }
934
- }
935
-
936
- // Higher score for consecutive sequences starting from 1
937
- if (isConsecutiveFrom1 && candidate.positions.length > 0 && candidate.positions[0].expectedNumber === 1) {
938
- score += 100
939
- }
940
-
941
- // Higher score for more matches
942
- score += candidate.matches * 10
943
-
944
- // Higher score for covering all items
945
- if (candidate.matches === candidate.totalItems) {
946
- score += 50
947
- }
948
-
949
- if (score > bestScore) {
950
- bestScore = score
951
- bestType = typeName
952
- }
953
- }
954
-
955
- // If we found a better type, update all marker infos
956
- if (bestType && candidateTypes.get(bestType).matches > 0) {
957
- const typeInfo = typeInfoCache.get(bestType)
958
- if (typeInfo) {
959
- const updatedMarkerInfos = markerInfos.map((markerInfo, index) => {
960
- if (!markerInfo.marker) return markerInfo
961
-
962
- // Extract the actual symbol without prefix/suffix
963
- const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
964
-
965
- // Use precomputed symbolIndexMap if available
966
- const compiled = _COMPILED_BY_NAME.get(bestType)
967
- let symbolIndex = -1
968
- if (compiled && compiled.symbolIndexMap) {
969
- const idx = compiled.symbolIndexMap.get(actualSymbol)
970
- symbolIndex = idx !== undefined ? idx : -1
971
- } else {
972
- symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
973
- }
974
- if (symbolIndex !== -1) {
975
- const number = calculateNumber(typeInfo, actualSymbol)
976
-
977
- return {
978
- ...markerInfo,
979
- type: bestType,
980
- number: number
981
- }
982
- }
983
- return markerInfo
984
- })
985
-
986
- return updatedMarkerInfos
987
- }
988
- }
989
-
990
- return markerInfos
991
- }
1
+ // ListTypes.json related utilities and marker processing
2
+
3
+ import types from '../listTypes.json' with { type: 'json' }
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
+ // Exclude exotic markers that should remain as <ul> in default mode
15
+ // These are rarely used and may not be well-supported
16
+ const excludedTypes = [
17
+ 'fullwidth-lower-roman',
18
+ 'fullwidth-upper-roman',
19
+ 'squared-upper-latin',
20
+ 'filled-squared-upper-latin'
21
+ ]
22
+
23
+ return !excludedTypes.includes(markerType)
24
+ }
25
+
26
+ const escapeRegExp = (string) => {
27
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
28
+ }
29
+
30
+ // Normalize fullwidth digits to ASCII digits
31
+ const normalizeFullwidthDigits = (str) => {
32
+ if (!str || typeof str !== 'string') return str
33
+ return str.replace(/[0-9]/g, ch => {
34
+ const code = ch.codePointAt(0)
35
+ return String.fromCharCode(code - 0xFF10 + 48)
36
+ })
37
+ }
38
+
39
+ // Resolve top-level pattern groups once at module initialization.
40
+ // Use `patternGroups` top-level key from `listTypes.json`.
41
+ const PATTERN_GROUPS = Array.isArray(types.patternGroups) ? types.patternGroups : []
42
+
43
+ // Map for O(1) name -> group lookup
44
+ const PATTERN_GROUP_MAP = new Map((PATTERN_GROUPS || []).map(g => [g.name, g]))
45
+
46
+ /**
47
+ * Get pattern list by name.
48
+ * Accepts a single name (string), an array of names, or a pattern-group-like object.
49
+ */
50
+ const getPatternsByName = (patternName) => {
51
+ if (!patternName) return []
52
+ // If caller passed the actual group object
53
+ if (typeof patternName === 'object' && Array.isArray(patternName.patterns)) {
54
+ return patternName.patterns
55
+ }
56
+
57
+ // If caller passed an array of group names, merge their patterns in order
58
+ if (Array.isArray(patternName)) {
59
+ const out = []
60
+ for (const name of patternName) {
61
+ if (typeof name !== 'string') continue
62
+ const g = PATTERN_GROUP_MAP.get(name)
63
+ if (g && Array.isArray(g.patterns)) out.push(...g.patterns)
64
+ }
65
+ return out
66
+ }
67
+
68
+ // Single name lookup (O(1) via map)
69
+ if (typeof patternName === 'string') {
70
+ const patternGroup = PATTERN_GROUP_MAP.get(patternName)
71
+ return Array.isArray(patternGroup?.patterns) ? patternGroup.patterns : []
72
+ }
73
+
74
+ return []
75
+ }
76
+
77
+ // Precompiled common regexes to avoid recreating them repeatedly
78
+ const ROMAN_LIKE_REGEX = /^[IVX]+\.?$/i
79
+ const LATIN_LETTER_REGEX = /^[a-z]$/i
80
+ const MARKER_FALLBACK_REGEX = /^(\S+)/
81
+ const PURE_PREFIX_REMOVAL_REGEX = /^[\((]+/
82
+
83
+ // Build dynamic suffix character classes from `listTypes.json` patterns so
84
+ // edits to that file don't require touching this code.
85
+ const _buildSuffixCharSets = () => {
86
+ const all = new Set()
87
+ const fullwidth = new Set()
88
+ const groups = PATTERN_GROUPS
89
+ if (groups.length > 0) {
90
+ for (const group of groups) {
91
+ if (!group || !Array.isArray(group.patterns)) continue
92
+ for (const p of group.patterns) {
93
+ if (!p || !p.suffix) continue
94
+ for (const ch of Array.from(p.suffix)) {
95
+ all.add(ch)
96
+ const cp = ch.codePointAt(0)
97
+ if (cp !== undefined && cp > 0xFF) fullwidth.add(ch)
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return { all, fullwidth }
103
+ }
104
+
105
+ const { all: _ALL_SUFFIX_CHARS, fullwidth: _FULLWIDTH_SUFFIX_CHARS } = _buildSuffixCharSets()
106
+
107
+ // Reuse existing `escapeRegExp` for char-class escaping to avoid duplication
108
+ const SUFFIX_CHAR_CLASS = [..._ALL_SUFFIX_CHARS].map(ch => escapeRegExp(ch)).join('')
109
+
110
+ const SUFFIX_CLASS_FOR_REGEX = SUFFIX_CHAR_CLASS.length > 0 ? `[${SUFFIX_CHAR_CLASS}]` : "\\."
111
+
112
+ const MARKER_SUFFIX_SPACE_REGEX = new RegExp(`^([^\\s]+?${SUFFIX_CLASS_FOR_REGEX})\\s`)
113
+ const MARKER_SUFFIX_NO_SPACE_REGEX = new RegExp(`^([^\\s]+?${SUFFIX_CLASS_FOR_REGEX})(?=[^\\s])`)
114
+ const PURE_SUFFIXES_REMOVAL_REGEX = new RegExp(`${SUFFIX_CLASS_FOR_REGEX}+$`)
115
+
116
+ // Cached type separation
117
+ let _symbolBasedTypes = null
118
+ let _rangeBasedTypes = null
119
+ let _sortedSymbolTypes = null
120
+ let _typeInfoByName = null
121
+
122
+ const getTypeSeparation = () => {
123
+ if (_symbolBasedTypes === null) {
124
+ const allTypes = compiledTypes()
125
+ _symbolBasedTypes = []
126
+ _rangeBasedTypes = []
127
+ _typeInfoByName = new Map()
128
+
129
+ // Create typeInfo lookup map
130
+ for (const type of types.types) {
131
+ _typeInfoByName.set(type.name, type)
132
+ }
133
+
134
+ for (const compiledType of allTypes) {
135
+ const typeInfo = _typeInfoByName.get(compiledType.name)
136
+ if (typeInfo?.symbols) {
137
+ _symbolBasedTypes.push(compiledType)
138
+ } else {
139
+ _rangeBasedTypes.push(compiledType)
140
+ }
141
+ }
142
+
143
+ // Sort symbol-based types to prioritize Roman numerals over Latin letters
144
+ _sortedSymbolTypes = [..._symbolBasedTypes].sort((a, b) => {
145
+ if (a.name.includes('roman') && b.name.includes('latin')) return -1
146
+ if (a.name.includes('latin') && b.name.includes('roman')) return 1
147
+ return 0
148
+ })
149
+ }
150
+ return {
151
+ symbolBasedTypes: _symbolBasedTypes,
152
+ rangeBasedTypes: _rangeBasedTypes,
153
+ sortedSymbolTypes: _sortedSymbolTypes,
154
+ typeInfoByName: _typeInfoByName
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get start value from type info
160
+ * @param {Object} typeInfo - Type information
161
+ * @returns {number} Start value (default 1)
162
+ */
163
+ const getStartValue = (typeInfo) => typeInfo?.start !== undefined ? typeInfo.start : 1
164
+
165
+ /**
166
+ * Check if symbols match a specific sequence pattern
167
+ * @param {Array<string>} pureSymbols - Array of pure symbols (without prefix/suffix)
168
+ * @param {Array<string>} sequence - Expected sequence array
169
+ * @param {string} typeName - Name of the type to return
170
+ * @param {boolean} allSame - Whether all symbols are the same
171
+ * @returns {Object|null} Type object or null
172
+ */
173
+ const checkSequenceMatch = (pureSymbols, sequence, typeName, allSame) => {
174
+ if (allSame && pureSymbols.length >= 1) {
175
+ if (sequence.indexOf(pureSymbols[0]) !== -1) {
176
+ return { type: typeName }
177
+ }
178
+ } else if (pureSymbols.length >= 2) {
179
+ // Find where the first symbol appears in the sequence
180
+ const startIndex = sequence.indexOf(pureSymbols[0])
181
+ if (startIndex === -1) return null
182
+
183
+ // Check if all symbols match a consecutive part of the sequence
184
+ let isValidSequence = true
185
+ for (let i = 0; i < pureSymbols.length; i++) {
186
+ const expectedIndex = startIndex + i
187
+ if (expectedIndex >= sequence.length || pureSymbols[i] !== sequence[expectedIndex]) {
188
+ isValidSequence = false
189
+ break
190
+ }
191
+ }
192
+ if (isValidSequence) {
193
+ return { type: typeName }
194
+ }
195
+ }
196
+ return null
197
+ }
198
+
199
+ /**
200
+ * Check if array matches expected sequence from start
201
+ * @param {Array<string>} actual - Actual symbols
202
+ * @param {Array<string>} expected - Expected sequence
203
+ * @returns {boolean} True if matches
204
+ */
205
+ const matchesSequence = (actual, expected) => {
206
+ for (let i = 0; i < actual.length; i++) {
207
+ if (actual[i] !== expected[i]) {
208
+ return false
209
+ }
210
+ }
211
+ return true
212
+ }
213
+
214
+ /**
215
+ * Extract pure symbol by removing prefix and suffix
216
+ * @param {string} marker - The marker with prefix/suffix
217
+ * @param {string|null} prefix - The prefix to remove
218
+ * @param {string|null} suffix - The suffix to remove
219
+ * @returns {string} Pure symbol without prefix/suffix
220
+ */
221
+ const extractPureSymbol = (marker, prefix, suffix) => {
222
+ let start = prefix ? prefix.length : 0
223
+ let end = suffix ? marker.length - suffix.length : marker.length
224
+ return marker.substring(start, end)
225
+ }
226
+
227
+ /**
228
+ * Calculate number for a marker based on type info
229
+ * @param {Object} typeInfo - Type information from listTypes.json
230
+ * @param {string} pureSymbol - Pure symbol without prefix/suffix
231
+ * @returns {number|undefined} The calculated number
232
+ */
233
+ const calculateNumber = (typeInfo, pureSymbol, compiled = null) => {
234
+ if (!typeInfo || !pureSymbol) return undefined
235
+
236
+ if (typeInfo.symbols) {
237
+ // Symbol-based types (katakana, roman numerals, etc.)
238
+ // Prefer a provided compiled object (with symbolIndexMap) to avoid map lookups.
239
+ if (compiled && compiled.symbolIndexMap) {
240
+ const idx = compiled.symbolIndexMap.get(pureSymbol)
241
+ return idx !== undefined ? idx + getStartValue(typeInfo) : undefined
242
+ }
243
+
244
+ // Fall back to looking up compiled map once (lazy) if compiled not provided
245
+ const compiledFallback = _COMPILED_BY_NAME.get(typeInfo.name)
246
+ if (compiledFallback && compiledFallback.symbolIndexMap) {
247
+ const idx = compiledFallback.symbolIndexMap.get(pureSymbol)
248
+ return idx !== undefined ? idx + getStartValue(typeInfo) : undefined
249
+ }
250
+
251
+ const symbolIndex = typeInfo.symbols.indexOf(pureSymbol)
252
+ return symbolIndex !== -1 ? symbolIndex + getStartValue(typeInfo) : undefined
253
+ } else if (typeInfo.range) {
254
+ // Range-based types (lower-latin, upper-latin, decimal)
255
+ const startValue = getStartValue(typeInfo)
256
+ if (Array.isArray(typeInfo.range) && typeof typeInfo.range[0] === 'string') {
257
+ // Latin range: ["a", "z"] or ["A", "Z"]
258
+ const startChar = typeInfo.range[0]
259
+ if (pureSymbol.length > 0) {
260
+ // Use codePointAt for proper Unicode handling (including surrogate pairs)
261
+ const charCode = pureSymbol.codePointAt(0)
262
+ const startCharCode = startChar.codePointAt(0)
263
+ return charCode - startCharCode + startValue
264
+ }
265
+ } else {
266
+ // Numeric range: [start, end]
267
+ const norm = normalizeFullwidthDigits(pureSymbol)
268
+ const numValue = parseInt(norm, 10)
269
+ if (!isNaN(numValue)) {
270
+ return numValue
271
+ }
272
+ }
273
+ }
274
+
275
+ return undefined
276
+ }
277
+
278
+ // Detect sequence pattern from multiple contents
279
+ export const detectSequencePattern = (allContents) => {
280
+ if (!allContents || allContents.length < 1) return null
281
+
282
+ // Extract pure markers from all contents
283
+ const markers = allContents.map(content => {
284
+ const trimmed = content.trim()
285
+ // First try: symbol(s) followed by suffix and optional space
286
+ let match = trimmed.match(MARKER_SUFFIX_SPACE_REGEX)
287
+ if (match) return match[1]
288
+
289
+ // Second try: symbol(s) followed by suffix at end (no space after)
290
+ match = trimmed.match(MARKER_SUFFIX_NO_SPACE_REGEX)
291
+ if (match) return match[1]
292
+
293
+ // Fallback: everything before first space
294
+ match = trimmed.match(MARKER_FALLBACK_REGEX)
295
+ return match ? match[1] : trimmed
296
+ })
297
+
298
+ // Extract pure symbols (remove prefixes/suffixes)
299
+ const pureSymbols = markers.map(marker => {
300
+ return marker.replace(PURE_SUFFIXES_REMOVAL_REGEX, '').replace(PURE_PREFIX_REMOVAL_REGEX, '')
301
+ })
302
+
303
+ // Check if all symbols are the same (repeated marker case)
304
+ const allSame = pureSymbols.every(s => s === pureSymbols[0])
305
+
306
+ // Cache type lookups
307
+ const { typeInfoByName } = getTypeSeparation()
308
+ const irohaType = typeInfoByName.get('katakana-iroha')
309
+ const katakanaType = typeInfoByName.get('katakana')
310
+ const upperRomanType = typeInfoByName.get('upper-roman')
311
+
312
+ // Get symbol sequences from listTypes.json
313
+ if (irohaType?.symbols) {
314
+ const irohaResult = checkSequenceMatch(pureSymbols, irohaType.symbols, 'katakana-iroha', allSame)
315
+ if (irohaResult) return irohaResult
316
+ }
317
+
318
+ if (katakanaType?.symbols) {
319
+ const katakanaResult = checkSequenceMatch(pureSymbols, katakanaType.symbols, 'katakana', allSame)
320
+ if (katakanaResult) return katakanaResult
321
+ }
322
+
323
+ // Check Roman numerals from listTypes.json
324
+ if (upperRomanType?.symbols) {
325
+ const allRomanLike = markers.every(marker => ROMAN_LIKE_REGEX.test(marker))
326
+
327
+ if (allRomanLike) {
328
+ const romanSymbols = markers.map(marker => marker.replace(/\.$/, '').toUpperCase())
329
+ if (matchesSequence(romanSymbols, upperRomanType.symbols)) {
330
+ return { type: 'upper-roman' }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Check Latin letters (range-based types)
336
+ const allLatinLike = pureSymbols.every(symbol => LATIN_LETTER_REGEX.test(symbol))
337
+
338
+ if (allLatinLike && pureSymbols.length >= 2) {
339
+ const firstSymbol = pureSymbols[0]
340
+ const isLowerCase = firstSymbol === firstSymbol.toLowerCase()
341
+ const baseCharCode = isLowerCase ? firstSymbol.charCodeAt(0) : firstSymbol.toUpperCase().charCodeAt(0)
342
+
343
+ const expectedSequence = pureSymbols.map((_, i) => {
344
+ const char = String.fromCharCode(baseCharCode + i)
345
+ return isLowerCase ? char : char.toUpperCase()
346
+ })
347
+ const actualSequence = pureSymbols.map(s => isLowerCase ? s : s.toUpperCase())
348
+
349
+ if (matchesSequence(actualSequence, expectedSequence)) {
350
+ return { type: isLowerCase ? 'lower-latin' : 'upper-latin' }
351
+ }
352
+ }
353
+
354
+ return null
355
+ }
356
+
357
+ /**
358
+ * Create marker detection result object
359
+ * @param {string} type - Marker type name
360
+ * @param {string} marker - Detected marker
361
+ * @param {number|undefined} number - Calculated number
362
+ * @param {string|null} prefix - Prefix character
363
+ * @param {string|null} suffix - Suffix character
364
+ * @returns {Object} Detection result
365
+ */
366
+ const createMarkerResult = (type, marker, number, prefix, suffix) => ({
367
+ type,
368
+ marker,
369
+ number,
370
+ prefix,
371
+ suffix
372
+ })
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
+ // Enhanced marker type detection with context awareness
390
+ export const detectMarkerType = (content, allContents = null) => {
391
+ if (!content || typeof content !== 'string') {
392
+ return { type: null, marker: null }
393
+ }
394
+
395
+ const trimmed = content.trim()
396
+ if (!trimmed) return { type: null, marker: null }
397
+
398
+ const { sortedSymbolTypes, rangeBasedTypes, typeInfoByName } = getTypeSeparation()
399
+
400
+ // If we have context (even single element), try to detect the overall pattern first
401
+ if (allContents && Array.isArray(allContents) && allContents.length >= 1) {
402
+ const contextResult = detectSequencePattern(allContents)
403
+ if (contextResult) {
404
+ // If context suggests a specific type, verify current content matches
405
+ const typeInfo = typeInfoByName.get(contextResult.type)
406
+ if (typeInfo) {
407
+ // Check both symbol-based and range-based types
408
+ const allCompiledTypes = [...sortedSymbolTypes, ...rangeBasedTypes]
409
+ for (const compiledType of allCompiledTypes) {
410
+ if (compiledType.name === contextResult.type) {
411
+ const matchResult = tryMatchPattern(trimmed, compiledType, typeInfo)
412
+ if (matchResult) return matchResult
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ // Fallback to original logic
420
+ // Fast fallback: try a flattened precompiled pattern list to avoid nested loops
421
+ const flatMatch = tryMatchAgainstFlattened(trimmed)
422
+ if (flatMatch) return flatMatch
423
+
424
+ return { type: null, marker: null }
425
+ }
426
+
427
+ /**
428
+ * Get the symbol for a specific number in a marker type
429
+ * @param {string} markerType - The marker type name (e.g., 'katakana-iroha', 'lower-roman')
430
+ * @param {number} number - The 1-based number (e.g., 1 for first item, 2 for second)
431
+ * @returns {string|null} The symbol for that number, or null if not found
432
+ */
433
+ export const getSymbolForNumber = (markerType, number) => {
434
+ const { typeInfoByName } = getTypeSeparation()
435
+ const typeInfo = typeInfoByName.get(markerType)
436
+ if (!typeInfo) {
437
+ return null
438
+ }
439
+
440
+ // For symbol-based types
441
+ if (typeInfo.symbols && Array.isArray(typeInfo.symbols)) {
442
+ const startValue = getStartValue(typeInfo)
443
+ const index = number - startValue
444
+ if (index >= 0 && index < typeInfo.symbols.length) {
445
+ return typeInfo.symbols[index]
446
+ }
447
+ }
448
+
449
+ // For range-based types (latin, decimal)
450
+ if (typeInfo.range) {
451
+ const startValue = getStartValue(typeInfo)
452
+ if (Array.isArray(typeInfo.range) && typeInfo.range.length === 2) {
453
+ // Latin range: ["a", "z"] or ["A", "Z"]
454
+ const startChar = typeInfo.range[0]
455
+ // Ensure startChar is a string
456
+ if (typeof startChar === 'string' && startChar.length > 0) {
457
+ // Use codePointAt and fromCodePoint for proper Unicode handling
458
+ const startCharCode = startChar.codePointAt(0)
459
+ const offset = number - startValue
460
+ const targetCharCode = startCharCode + offset
461
+ return String.fromCodePoint(targetCharCode)
462
+ }
463
+ }
464
+ // Numeric range or fallback
465
+ return String(number)
466
+ }
467
+
468
+ return null
469
+ }
470
+
471
+ /**
472
+ * Get the default prefix/suffix pattern for a marker type
473
+ * @param {string} markerType - The marker type name (e.g., 'lower-roman', 'decimal')
474
+ * @returns {Object} Object with prefix and suffix properties
475
+ */
476
+ export const getDefaultPatternForType = (markerType) => {
477
+ const { typeInfoByName } = getTypeSeparation()
478
+ const typeInfo = typeInfoByName.get(markerType)
479
+ if (!typeInfo) {
480
+ return { prefix: '', suffix: '.' }
481
+ }
482
+
483
+ // Get patterns for this type (prefer `pattern` property)
484
+ const patternRef = typeInfo.pattern || null
485
+ const patterns = getPatternsByName(patternRef)
486
+ if (!patterns || patterns.length === 0) {
487
+ return { prefix: '', suffix: '.' }
488
+ }
489
+
490
+ // Return the first pattern as the default
491
+ return {
492
+ prefix: patterns[0].prefix || '',
493
+ suffix: patterns[0].suffix || '.'
494
+ }
495
+ }
496
+
497
+ const prefixs = [
498
+ ['(', 'round'],
499
+ //['[', 'square'],
500
+ //['{', 'curly'],
501
+ //['<', 'angle'],
502
+ ['(', 'fullround'],
503
+ ]
504
+
505
+ const suffixs = [
506
+ [')', 'round'],
507
+ //[']', 'square'],
508
+ //['}', 'curly'],
509
+ //['>', 'angle'],
510
+ [')', 'fullround'],
511
+ ]
512
+
513
+ // Build Maps for O(1) lookups (faster than .find on every call)
514
+ const prefixMap = new Map(prefixs)
515
+ const suffixMap = new Map(suffixs)
516
+
517
+ const generateClassName = (baseClass, prefix, suffix) => {
518
+ // fast path: no prefix and no suffix
519
+ if (!prefix && !suffix) return baseClass
520
+
521
+ // O(1) map lookups
522
+ const prefixName = prefixMap.get(prefix) || null
523
+ const suffixName = suffixMap.get(suffix) || null
524
+
525
+ // If neither side matches known labels, return baseClass
526
+ if (!prefixName && !suffixName) return baseClass
527
+
528
+ const p = prefixName ? prefixName : 'none'
529
+ const s = suffixName ? suffixName : 'none'
530
+ return `${baseClass}-with-${p}-${s}`
531
+ }
532
+
533
+ export const getTypeAttributes = (markerType, markerInfo = null, opt = {}) => {
534
+ const { typeInfoByName } = getTypeSeparation()
535
+ const type = typeInfoByName.get(markerType)
536
+ if (!type) {
537
+ return { type: '1', class: 'ol-decimal', suffix: '.' }
538
+ }
539
+
540
+ // Get prefix/suffix from markerInfo if provided
541
+ const detectedPrefix = markerInfo?.prefix || null
542
+ const detectedSuffix = markerInfo?.suffix || '.'
543
+
544
+ // Standard marker types mapping
545
+ const standardTypes = {
546
+ 'decimal': { type: '1', baseClass: 'ol-decimal' },
547
+ 'lower-latin': { type: 'a', baseClass: 'ol-lower-latin' },
548
+ 'upper-latin': { type: 'A', baseClass: 'ol-upper-latin' },
549
+ 'lower-roman': { type: 'i', baseClass: 'ol-lower-roman' },
550
+ 'upper-roman': { type: 'I', baseClass: 'ol-upper-roman' }
551
+ }
552
+
553
+ // Custom marker types with no suffix
554
+ const customTypesNoSuffix = [
555
+ 'filled-circled-decimal', 'circled-decimal',
556
+ 'circled-upper-latin', 'filled-circled-upper-latin',
557
+ 'circled-lower-latin', 'katakana', 'katakana-iroha'
558
+ ]
559
+
560
+ let mappedType
561
+ let suffix = detectedSuffix
562
+ let prefix = detectedPrefix
563
+ let customMarker = false
564
+
565
+ if (standardTypes[type.name]) {
566
+ const std = standardTypes[type.name]
567
+ const baseClass = std.baseClass
568
+ const decoratedClass = opt.addMarkerStyleToClass
569
+ ? generateClassName(baseClass, prefix, suffix)
570
+ : baseClass
571
+ mappedType = {
572
+ type: std.type,
573
+ class: decoratedClass
574
+ }
575
+ } else {
576
+ // Custom marker types
577
+ mappedType = { type: '1', class: `ol-${type.name}` }
578
+ customMarker = true
579
+ if (customTypesNoSuffix.includes(type.name)) {
580
+ suffix = null
581
+ }
582
+ }
583
+
584
+ const result = {
585
+ type: customMarker ? null : mappedType.type, // No type attribute for custom markers
586
+ class: mappedType.class,
587
+ suffix: suffix,
588
+ customMarker: customMarker,
589
+ start: getStartValue(type)
590
+ }
591
+
592
+ // Set role="list" for custom markers
593
+ if (customMarker) {
594
+ result.role = 'list'
595
+ }
596
+
597
+ if (prefix) {
598
+ result.prefix = prefix
599
+ }
600
+
601
+ if (type.symbols) {
602
+ result.symbols = type.symbols
603
+ }
604
+
605
+ return result
606
+ }
607
+
608
+
609
+
610
+ // Precompute regex tail (endCheck + spacePattern) for a pattern to avoid recomputing
611
+ const createPatternTail = (pattern) => {
612
+ // Determine if the pattern's suffix (if any) contains any fullwidth suffix
613
+ // characters declared in `listTypes.json`.
614
+ let isFullWidthSuffix = false
615
+ if (pattern.suffix) {
616
+ for (const ch of Array.from(pattern.suffix)) {
617
+ if (_FULLWIDTH_SUFFIX_CHARS.has(ch)) {
618
+ isFullWidthSuffix = true
619
+ break
620
+ }
621
+ }
622
+ }
623
+
624
+ // Compute default space handling based on whether suffix exists and
625
+ // whether suffix contains a fullwidth character. This is the fallback
626
+ // behavior when `pattern.space` is not provided.
627
+ const defaultSpaceIfSuffix = isFullWidthSuffix ? '([  ])?' : '([  ])+'
628
+ const defaultSpaceNoSuffix = '(?=[  ]|$)([  ])*'
629
+
630
+ let spacePattern
631
+
632
+ if (pattern.space) {
633
+ // Explicit directive from listTypes.json wins
634
+ switch (pattern.space) {
635
+ case 'half':
636
+ spacePattern = '([ ])+'
637
+ break
638
+ case 'both':
639
+ spacePattern = '([  ])+'
640
+ break
641
+ case 'none_or_both':
642
+ spacePattern = pattern.suffix ? '([  ]+)?' : defaultSpaceNoSuffix
643
+ break
644
+ default:
645
+ // Unknown explicit value: fall back to defaults
646
+ spacePattern = pattern.suffix ? defaultSpaceIfSuffix : defaultSpaceNoSuffix
647
+ }
648
+ } else {
649
+ // No explicit `pattern.space`: use defaults
650
+ spacePattern = pattern.suffix ? defaultSpaceIfSuffix : defaultSpaceNoSuffix
651
+ }
652
+
653
+ let endCheck = ''
654
+ if (!pattern.suffix && pattern.prefix) {
655
+ endCheck = '(?=[  ]|$)'
656
+ }
657
+
658
+ return `${endCheck}${spacePattern}`
659
+ }
660
+
661
+ // Process patterns for symbols
662
+ const processSymbolPatterns = (patterns, symbols, typePatterns, type) => {
663
+ // Pre-compute escaped prefixes, suffixes and regex tail once
664
+ const patternCache = new Map()
665
+ typePatterns.forEach((pattern, index) => {
666
+ const escapedPrefix = pattern.prefix ? escapeRegExp(pattern.prefix) : ''
667
+ const escapedSuffix = pattern.suffix ? escapeRegExp(pattern.suffix) : ''
668
+ const tail = createPatternTail(pattern)
669
+ patternCache.set(index, {
670
+ prefix: pattern.prefix,
671
+ suffix: pattern.suffix,
672
+ space: pattern.space,
673
+ escapedPrefix,
674
+ escapedSuffix,
675
+ tail
676
+ })
677
+ })
678
+
679
+ // Use pre-computed cache for faster pattern generation
680
+ const symbolsLength = symbols.length
681
+ const patternsLength = typePatterns.length
682
+
683
+ for (let symbolIndex = 0; symbolIndex < symbolsLength; symbolIndex++) {
684
+ const sym = symbols[symbolIndex]
685
+ const processedSym = sym.replace(/^\\\\/,'\\')
686
+
687
+ for (let patternIndex = 0; patternIndex < patternsLength; patternIndex++) {
688
+ const cached = patternCache.get(patternIndex)
689
+ // Original suffix variant
690
+ const symbolPartOrig = cached.escapedPrefix + processedSym + cached.escapedSuffix
691
+ const regexStrOrig = `^(${symbolPartOrig})${cached.tail}`
692
+ patterns.push({
693
+ regex: new RegExp(regexStrOrig, 'u'),
694
+ prefix: cached.prefix,
695
+ suffix: cached.suffix,
696
+ symbolIndex,
697
+ num: symbolIndex + type.start
698
+ })
699
+
700
+ // Do not generate additional suffix variants — respect patterns from listTypes.json only.
701
+ }
702
+
703
+ // Only generate patterns that directly correspond to `typePatterns` entries.
704
+ }
705
+ }
706
+
707
+ // Process range patterns
708
+ const processRangePatterns = (patterns, typePatterns, type) => {
709
+ // Pre-calculate symbol pattern once
710
+ let symbolPattern
711
+ if (typeof type.range[0] === 'number') {
712
+ // Allow only ASCII digits here — do not synthesize fullwidth digit variants.
713
+ symbolPattern = '(?:\\d+)'
714
+ } else {
715
+ const start = type.range[0].codePointAt(0)
716
+ const end = type.range[1].codePointAt(0)
717
+
718
+ // Handle Unicode characters properly
719
+ if (start > 0xFFFF || end > 0xFFFF) {
720
+ // Unicode characters beyond BMP - enumerate all characters
721
+ const chars = []
722
+ for (let code = start; code <= end; code++) {
723
+ chars.push(String.fromCodePoint(code))
724
+ }
725
+ symbolPattern = `[${chars.join('').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}]`
726
+ } else {
727
+ // Standard ASCII/BMP characters
728
+ symbolPattern = `[${String.fromCharCode(start)}-${String.fromCharCode(end)}]`
729
+ }
730
+ }
731
+
732
+ // Manual loop instead of forEach for better performance
733
+ const patternsLength = typePatterns.length
734
+ for (let i = 0; i < patternsLength; i++) {
735
+ const pattern = typePatterns[i]
736
+ const escapedPrefix = pattern.prefix ? escapeRegExp(pattern.prefix) : ''
737
+ const escapedSuffix = pattern.suffix ? escapeRegExp(pattern.suffix) : ''
738
+ const tail = createPatternTail(pattern)
739
+
740
+ const symbolPart = escapedPrefix + symbolPattern + escapedSuffix
741
+ const regexStr = `^(${symbolPart})${tail}`
742
+ patterns.push({
743
+ regex: new RegExp(regexStr, 'u'),
744
+ prefix: pattern.prefix,
745
+ suffix: pattern.suffix,
746
+ symbolIndex: 0,
747
+ num: type.start,
748
+ isRange: true,
749
+ rangeType: typeof type.range[0] === 'number' ? 'numeric' : 'alphabetic'
750
+ })
751
+
752
+ // Note: Do not synthesize additional suffix variants here.
753
+ // Matching must be driven solely by patterns defined in `listTypes.json`.
754
+ }
755
+ }
756
+
757
+ // Compiled types with caching
758
+ export const compiledTypes = (() => {
759
+ let _cache = null
760
+ return () => {
761
+ if (_cache === null) {
762
+ _cache = types.types.map(type => {
763
+ const patterns = []
764
+ // Per-type `pattern` references a `patternGroups` entry
765
+ const typePatternRef = type.pattern || null
766
+ const typePatterns = getPatternsByName(typePatternRef)
767
+
768
+ // Build symbol->index map for symbol-based types to avoid indexOf scans
769
+ let symbolIndexMap = null
770
+ if (type.symbols && Array.isArray(type.symbols)) {
771
+ symbolIndexMap = new Map()
772
+ for (let i = 0; i < type.symbols.length; i++) {
773
+ symbolIndexMap.set(type.symbols[i], i)
774
+ }
775
+ }
776
+
777
+ if (type.symbols) {
778
+ processSymbolPatterns(patterns, type.symbols, typePatterns, type)
779
+ } else if (type.range) {
780
+ processRangePatterns(patterns, typePatterns, type)
781
+ }
782
+
783
+ return {
784
+ name: type.name,
785
+ patterns,
786
+ symbolIndexMap
787
+ }
788
+ })
789
+ }
790
+ return _cache
791
+ }
792
+ })()
793
+
794
+ // Map of compiled types by name for O(1) lookup
795
+ // Build a map of compiled types by name once for fast lookups
796
+ const _COMPILED_BY_NAME = (() => {
797
+ const m = new Map()
798
+ for (const t of compiledTypes()) m.set(t.name, t)
799
+ return m
800
+ })()
801
+
802
+ export const compiledTypesByName = () => _COMPILED_BY_NAME
803
+
804
+ // Build a flattened pattern list (preserve previous priority: sortedSymbolTypes then rangeBasedTypes)
805
+ const _FLATTENED_PATTERNS = (() => {
806
+ const arr = []
807
+ const { sortedSymbolTypes, rangeBasedTypes } = getTypeSeparation()
808
+ const source = [...sortedSymbolTypes, ...rangeBasedTypes]
809
+ for (const compiledType of source) {
810
+ const compiled = _COMPILED_BY_NAME.get(compiledType.name)
811
+ for (const p of compiledType.patterns) {
812
+ arr.push({
813
+ regex: p.regex,
814
+ prefix: p.prefix,
815
+ suffix: p.suffix,
816
+ typeName: compiledType.name,
817
+ symbolIndex: p.symbolIndex,
818
+ num: p.num,
819
+ isRange: p.isRange,
820
+ compiled: compiled || null
821
+ })
822
+ }
823
+ }
824
+ return arr
825
+ })()
826
+
827
+ // Fast matcher over flattened list
828
+ const tryMatchAgainstFlattened = (trimmed) => {
829
+ for (const entry of _FLATTENED_PATTERNS) {
830
+ const m = matchRegexEntry(trimmed, entry.typeName, entry)
831
+ if (m) return m
832
+ }
833
+ return null
834
+ }
835
+
836
+ // Helper to match a trimmed string against a pattern entry and produce a result.
837
+ // `entry` is expected to have `regex`, `prefix`, `suffix` and optionally `compiled`.
838
+ const matchRegexEntry = (trimmed, typeName, entry) => {
839
+ const result = trimmed.match(entry.regex)
840
+ if (!result) return null
841
+
842
+ const detectedMarker = result[1]
843
+ const pureSymbol = extractPureSymbol(detectedMarker, entry.prefix, entry.suffix)
844
+ const { typeInfoByName } = getTypeSeparation()
845
+ const typeInfo = typeInfoByName.get(typeName)
846
+ const compiledForCalc = entry.compiled || _COMPILED_BY_NAME.get(typeName)
847
+ const number = calculateNumber(typeInfo, pureSymbol, compiledForCalc)
848
+
849
+ return createMarkerResult(typeName, detectedMarker, number, entry.prefix, entry.suffix)
850
+ }
851
+
852
+ // Analyze list context to determine optimal marker type for ambiguous cases
853
+ export const analyzeListMarkerContext = (markerInfos) => {
854
+ if (!markerInfos || markerInfos.length === 0) return markerInfos
855
+
856
+ const { symbolBasedTypes, typeInfoByName } = getTypeSeparation()
857
+
858
+ // Create typeInfo lookup cache
859
+ const typeInfoCache = new Map()
860
+ for (const compiledType of symbolBasedTypes) {
861
+ const typeInfo = typeInfoByName.get(compiledType.name)
862
+ if (typeInfo?.symbols) {
863
+ typeInfoCache.set(compiledType.name, typeInfo)
864
+ }
865
+ }
866
+
867
+ // Group markers by possible types
868
+ const candidateTypes = new Map()
869
+
870
+ markerInfos.forEach((markerInfo, index) => {
871
+ if (!markerInfo.marker) return
872
+
873
+ // Extract the actual symbol without prefix/suffix
874
+ const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
875
+
876
+ // Find all possible types for this marker
877
+ const possibleTypes = []
878
+ for (const [typeName, typeInfo] of typeInfoCache) {
879
+ let symbolIndex = -1
880
+ const compiled = _COMPILED_BY_NAME.get(typeName)
881
+ if (compiled && compiled.symbolIndexMap) {
882
+ const idx = compiled.symbolIndexMap.get(actualSymbol)
883
+ symbolIndex = idx !== undefined ? idx : -1
884
+ } else {
885
+ symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
886
+ }
887
+
888
+ if (symbolIndex !== -1) {
889
+ const expectedNumber = symbolIndex + getStartValue(typeInfo)
890
+
891
+ possibleTypes.push({
892
+ typeName,
893
+ symbolIndex,
894
+ expectedNumber,
895
+ actualPosition: index + 1
896
+ })
897
+ }
898
+ }
899
+
900
+ possibleTypes.forEach(pt => {
901
+ if (!candidateTypes.has(pt.typeName)) {
902
+ candidateTypes.set(pt.typeName, {
903
+ matches: 0,
904
+ totalItems: markerInfos.length,
905
+ positions: []
906
+ })
907
+ }
908
+
909
+ const candidate = candidateTypes.get(pt.typeName)
910
+ candidate.matches++
911
+ candidate.positions.push({
912
+ index,
913
+ expectedNumber: pt.expectedNumber,
914
+ actualPosition: pt.actualPosition,
915
+ marker: markerInfo.marker
916
+ })
917
+ })
918
+ })
919
+
920
+ // Score each candidate type
921
+ let bestType = null
922
+ let bestScore = -1
923
+
924
+ for (const [typeName, candidate] of candidateTypes) {
925
+ let score = 0
926
+
927
+ // Check if positions form a consecutive sequence starting from 1
928
+ candidate.positions.sort((a, b) => a.index - b.index)
929
+ let isConsecutiveFrom1 = true
930
+ let expectedStart = 1
931
+
932
+ for (let i = 0; i < candidate.positions.length; i++) {
933
+ const pos = candidate.positions[i]
934
+ if (pos.expectedNumber !== expectedStart + i) {
935
+ isConsecutiveFrom1 = false
936
+ break
937
+ }
938
+ }
939
+
940
+ // Higher score for consecutive sequences starting from 1
941
+ if (isConsecutiveFrom1 && candidate.positions.length > 0 && candidate.positions[0].expectedNumber === 1) {
942
+ score += 100
943
+ }
944
+
945
+ // Higher score for more matches
946
+ score += candidate.matches * 10
947
+
948
+ // Higher score for covering all items
949
+ if (candidate.matches === candidate.totalItems) {
950
+ score += 50
951
+ }
952
+
953
+ if (score > bestScore) {
954
+ bestScore = score
955
+ bestType = typeName
956
+ }
957
+ }
958
+
959
+ // If we found a better type, update all marker infos
960
+ if (bestType && candidateTypes.get(bestType).matches > 0) {
961
+ const typeInfo = typeInfoCache.get(bestType)
962
+ if (typeInfo) {
963
+ const updatedMarkerInfos = markerInfos.map((markerInfo, index) => {
964
+ if (!markerInfo.marker) return markerInfo
965
+
966
+ // Extract the actual symbol without prefix/suffix
967
+ const actualSymbol = extractPureSymbol(markerInfo.marker, markerInfo.prefix, markerInfo.suffix)
968
+
969
+ // Use precomputed symbolIndexMap if available
970
+ const compiled = _COMPILED_BY_NAME.get(bestType)
971
+ let symbolIndex = -1
972
+ if (compiled && compiled.symbolIndexMap) {
973
+ const idx = compiled.symbolIndexMap.get(actualSymbol)
974
+ symbolIndex = idx !== undefined ? idx : -1
975
+ } else {
976
+ symbolIndex = typeInfo.symbols.indexOf(actualSymbol)
977
+ }
978
+ if (symbolIndex !== -1) {
979
+ const number = calculateNumber(typeInfo, actualSymbol)
980
+
981
+ return {
982
+ ...markerInfo,
983
+ type: bestType,
984
+ number: number
985
+ }
986
+ }
987
+ return markerInfo
988
+ })
989
+
990
+ return updatedMarkerInfos
991
+ }
992
+ }
993
+
994
+ return markerInfos
995
+ }