@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 +7 -4
- package/index.js +22 -11
- package/package.json +3 -3
- package/src/list-helpers.js +5 -3
- package/src/phase1-analyze.js +32 -118
- package/src/phase2-convert.js +2 -3
- package/src/phase3-attributes.js +8 -19
- package/src/phase5-spans.js +2 -2
- package/src/types-utility.js +87 -224
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: `οΌ`, `οΌ`, `οΌ`, 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 (`
|
|
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-
|
|
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.
|
|
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.
|
|
31
|
-
"@peaceroad/markdown-it-strong-ja": "^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"
|
package/src/list-helpers.js
CHANGED
|
@@ -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:
|
|
52
|
+
* @returns {{ listCloseByOpen: Int32Array, listItemCloseByOpen: Int32Array }}
|
|
53
53
|
*/
|
|
54
54
|
export function buildListCloseIndexMap(tokens) {
|
|
55
|
-
const listCloseByOpen = new
|
|
56
|
-
|
|
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 = []
|
package/src/phase1-analyze.js
CHANGED
|
@@ -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
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
366
|
-
|
|
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
|
}
|
package/src/phase2-convert.js
CHANGED
|
@@ -167,10 +167,9 @@ function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* Simplify nested ul>li>
|
|
170
|
+
* Simplify nested ul>li>ol structures.
|
|
171
171
|
*
|
|
172
|
-
* Pattern
|
|
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.
|
package/src/phase3-attributes.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/src/phase5-spans.js
CHANGED
|
@@ -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 =
|
|
17
|
+
let listCloseByOpen = initialListCloseByOpen
|
|
18
18
|
const getListCloseByOpen = () => {
|
|
19
19
|
if (!listCloseByOpen) {
|
|
20
20
|
listCloseByOpen = buildListCloseIndexMap(tokens).listCloseByOpen
|
package/src/types-utility.js
CHANGED
|
@@ -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
|
-
|
|
472
|
-
//['{', 'curly'],
|
|
473
|
-
//['<', 'angle'],
|
|
474
|
-
['οΌ', 'fullround'],
|
|
426
|
+
['οΌ', 'fullround']
|
|
475
427
|
]
|
|
476
428
|
|
|
477
|
-
const
|
|
429
|
+
const suffixLabels = [
|
|
478
430
|
[')', 'round'],
|
|
479
|
-
|
|
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(
|
|
487
|
-
const suffixMap = new Map(
|
|
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 =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
}
|