@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.4.0 β†’ 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ The plugin supports the following marker types.
45
45
  - filled-squared-upper-latin: `πŸ…°`, `πŸ…±`, `πŸ…²`, pattern: enclosed
46
46
  - fullwidth-lower-roman: `β…°`, `β…±`, `β…²`, pattern: fullwidth
47
47
  - fullwidth-upper-roman: `β… `, `β…‘`, `β…’`, pattern: fullwidth
48
+ - fullwidth-decimal: `0`, `οΌ‘`, `οΌ’`, pattern: fullwidth
48
49
  - japanese-informal: `δΈ€`, `二`, `δΈ‰`, pattern: fullwidth
49
50
  - katakana: `γ‚’`, `γ‚€`, `ウ`, pattern: fullwidth
50
51
  - katakana-iroha: `γ‚€`, `γƒ­`, `ハ`, pattern: fullwidth
@@ -90,10 +91,11 @@ You can customize the conversion using options.
90
91
  - `hasListStyleNone` (boolean) β€” When the plugin emits `role="list"`, also add `style="list-style: none;"` to the `<ol>`.
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
- - `enableLiteralNumberingFix` (boolean) β€” Enable literal nested list recovery (for example, nested lists starting with 2 or greater). This is opt-in; 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: switching the default from `false` to `true` changes rendered HTML by design. On the current test corpus (`394` markdown cases), `18` cases change.
95
- - Changed files in that comparison: `examples-default-14-repeated-numbers.txt` (`7`), `examples-option-literal-numbering-attrs.txt` (`1`), `examples-option-literal-numbering-fix-disabled.txt` (`2`), `examples-option-literal-numbering-fix.txt` (`3`), `examples-option-literal-numbering-indent.txt` (`4`).
96
- - Recommendation: keep the default as `false` for patch/minor releases; if changing the default to `true`, treat it as a breaking change and release a major version.
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).
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.
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`).
97
+ - Migration tip: if you enable `enableLiteralNumberingFix: true`, treat it as a breaking-output change for snapshots/downstream HTML.
98
+ - Detailed notes: `docs/enable-literal-numbering-fix.md`.
97
99
 
98
100
  ## Description lists conversion behavior
99
101
 
@@ -145,7 +147,7 @@ Custom marker conversion example:
145
147
  - β‘’ Third
146
148
 
147
149
  [HTML]
148
- <ol role="list" class="ol-filled-circled-decimal">
150
+ <ol role="list" class="ol-circled-decimal">
149
151
  <li><span class="li-num" aria-hidden="true">β‘ </span> First</li>
150
152
  <li><span class="li-num" aria-hidden="true">β‘‘</span> Second</li>
151
153
  <li><span class="li-num" aria-hidden="true">β‘’</span> Third</li>
@@ -268,9 +270,9 @@ When description lists are enabled the plugin can convert the following patterns
268
270
 
269
271
  ```
270
272
  [Markdown]
271
- - **Term 1**
273
+ - **Term 1**
272
274
  Description text for term 1
273
- - **Term 2**
275
+ - **Term 2**
274
276
  Description text for term 2
275
277
 
276
278
  [HTML]
@@ -349,3 +351,5 @@ md.use(mditNumberingUl)
349
351
  const html = md.render(`- a. First\n- b. Second`)
350
352
  console.log(html)
351
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,11 +31,19 @@ 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
  }
28
38
 
39
+ const normalizedMarkerSpanClass = typeof opt.markerSpanClass === 'string'
40
+ ? opt.markerSpanClass.trim()
41
+ : ''
42
+ opt.markerSpanClass = normalizedMarkerSpanClass || 'li-num'
43
+ opt.descriptionListDivClass = typeof opt.descriptionListDivClass === 'string'
44
+ ? opt.descriptionListDivClass.trim()
45
+ : ''
46
+
29
47
  const addRuleAfter = (ruler, afterName, ruleName, fn) => {
30
48
  try {
31
49
  ruler.after(afterName, ruleName, fn)
@@ -35,9 +53,6 @@ const mditNumberingUl = (md, option) => {
35
53
  }
36
54
 
37
55
  const dlProcessor = (state) => {
38
- if (!state.env) {
39
- state.env = {}
40
- }
41
56
  if (!opt.descriptionList && !opt.descriptionListWithDiv) {
42
57
  return true
43
58
  }
@@ -46,54 +61,74 @@ const mditNumberingUl = (md, option) => {
46
61
  }
47
62
 
48
63
  const listProcessor = (state) => {
49
- // Initialize state.env
50
- if (!state.env) {
51
- state.env = {}
52
- }
53
-
54
64
  const tokens = state.tokens
65
+ let hasAnyList = false
66
+ for (let i = 0; i < tokens.length; i++) {
67
+ const type = tokens[i]?.type
68
+ if (type === 'bullet_list_open' || type === 'ordered_list_open') {
69
+ hasAnyList = true
70
+ break
71
+ }
72
+ }
73
+ if (!hasAnyList) {
74
+ return true
75
+ }
55
76
 
56
77
  // Normalize literal nested ordered lists (markdown-it only creates nested lists when they start at 1)
57
- normalizeLiteralOrderedLists(tokens, opt)
58
-
78
+ if (opt.enableLiteralNumberingFix) {
79
+ normalizeLiteralOrderedLists(tokens, opt)
80
+ }
81
+
59
82
  // ===== PHASE 1: List Structure Analysis =====
60
83
  // Analyze marker detection and structure without token conversion
61
- const listInfos = analyzeListStructure(tokens, opt)
62
-
84
+ const listInfos = analyzeListStructure(tokens)
85
+
63
86
  // ===== PHASE 2: Token Conversion =====
64
87
  // Convert bullet_list to ordered_list based on Phase1 analysis
65
88
  // Note: simplifyNestedBulletLists removes tokens, changing indices
66
89
  convertLists(tokens, listInfos, opt)
67
-
68
- // ===== PHASE 3: Add Attributes =====
69
- // Add type, class, data-* attributes to converted lists
70
- // Use markerInfo stored on list tokens (safe after Phase2 mutations)
71
- addAttributes(tokens, opt)
72
-
73
- // ===== PHASE 4: HTML Block Processing =====
74
- // Remove indents from HTML blocks in lists and normalize line breaks
90
+
91
+ let hasOrderedList = false
75
92
  let hasNestedHtmlBlock = false
76
93
  for (let i = 0; i < tokens.length; i++) {
77
94
  const token = tokens[i]
78
- if (token.type === 'html_block' && token.level > 0) {
95
+ if (!hasOrderedList && token.type === 'ordered_list_open') {
96
+ hasOrderedList = true
97
+ }
98
+ if (!hasNestedHtmlBlock && token.type === 'html_block' && token.level > 0) {
79
99
  hasNestedHtmlBlock = true
100
+ }
101
+ if (hasOrderedList && hasNestedHtmlBlock) {
80
102
  break
81
103
  }
82
104
  }
105
+
106
+ // ===== PHASE 3: Add Attributes =====
107
+ // Add type, class, data-* attributes to converted lists
108
+ // Use markerInfo stored on list tokens (safe after Phase2 mutations)
109
+ let closeMap = null
110
+ if (hasOrderedList) {
111
+ closeMap = addAttributes(tokens, opt)
112
+ }
113
+
114
+ // ===== PHASE 4: HTML Block Processing =====
115
+ // Remove indents from HTML blocks in lists and normalize line breaks
83
116
  if (hasNestedHtmlBlock) {
84
117
  processHtmlBlocks(state)
85
118
  }
86
-
119
+
87
120
  // ===== PHASE 5: Span Generation =====
88
121
  // Generate marker spans in alwaysMarkerSpan mode
89
- generateSpans(tokens, opt)
90
-
122
+ if (hasOrderedList && !opt.useCounterStyle) {
123
+ generateSpans(tokens, opt, closeMap?.listCloseByOpen || null)
124
+ }
125
+
91
126
  return true
92
127
  }
93
128
 
94
129
  md.core.ruler.before('inline', 'numbering_dl_parser', dlProcessor)
95
130
  md.core.ruler.after('numbering_dl_parser', 'numbering_ul_phases', listProcessor)
96
-
131
+
97
132
  // Description list: Move paragraph attributes to dl and add custom renderers
98
133
  if (opt.descriptionList || opt.descriptionListWithDiv) {
99
134
  // Move paragraph attributes to dl (after inline and any attribute plugins)
@@ -101,7 +136,7 @@ const mditNumberingUl = (md, option) => {
101
136
  moveParagraphAttributesToDL(state.tokens)
102
137
  return true
103
138
  }
104
-
139
+
105
140
  addRuleAfter(md.core.ruler, 'curly_attributes', 'numbering_dl_attrs', dlAttrProcessor)
106
141
  }
107
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.0",
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,7 +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-strong-ja": "^0.7.2",
30
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.10",
31
+ "@peaceroad/markdown-it-strong-ja": "^0.9.0",
31
32
  "markdown-it": "^14.1.0",
32
33
  "markdown-it-attrs": "^4.3.1",
33
34
  "markdown-it-deflist": "^3.0.0"
@@ -49,11 +49,13 @@ export function findListItemEnd(tokens, startIndex) {
49
49
  /**
50
50
  * Build close-index maps for list and list_item tokens.
51
51
  * @param {Array} tokens - Token array
52
- * @returns {{ listCloseByOpen: number[], listItemCloseByOpen: number[] }}
52
+ * @returns {{ listCloseByOpen: Int32Array, listItemCloseByOpen: Int32Array }}
53
53
  */
54
54
  export function buildListCloseIndexMap(tokens) {
55
- const listCloseByOpen = new Array(tokens.length).fill(-1)
56
- const listItemCloseByOpen = new Array(tokens.length).fill(-1)
55
+ const listCloseByOpen = new Int32Array(tokens.length)
56
+ listCloseByOpen.fill(-1)
57
+ const listItemCloseByOpen = new Int32Array(tokens.length)
58
+ listItemCloseByOpen.fill(-1)
57
59
  const bulletStack = []
58
60
  const orderedStack = []
59
61
  const listItemStack = []
@@ -14,6 +14,8 @@ const LEADING_ATTR_BLOCK_REGEX = /^[ \t]*\{([^}]+)\}/
14
14
  const STANDALONE_ATTR_BLOCK_REGEX = /^\{([^}]+)\}$/
15
15
  const TRAILING_TWO_SPACES_REGEX = / {2,}$/
16
16
  const TRAILING_BACKSLASH_BREAK_REGEX = /\\\s*$/
17
+ const DL_TERM_INLINE_REGEX = /^\*\*(.*?)\*\*(.*)/s
18
+ const TRAILING_LIST_ATTR_LINE_REGEX = /\n\s*\{([^}]+)\}\s*$/
17
19
 
18
20
  const parseAttrString = (attrStr) => {
19
21
  if (!attrStr || typeof attrStr !== 'string') {
@@ -123,9 +125,11 @@ const hasMeaningfulDescriptionContent = (text) => {
123
125
  if (typeof text !== 'string' || text.trim().length === 0) {
124
126
  return false
125
127
  }
128
+ if (!text.includes('{')) {
129
+ return true
130
+ }
126
131
 
127
132
  const lines = text.split('\n')
128
- const renderedLines = []
129
133
  for (const line of lines) {
130
134
  const trimmed = line.trim()
131
135
  if (!trimmed) {
@@ -138,9 +142,9 @@ const hasMeaningfulDescriptionContent = (text) => {
138
142
  continue
139
143
  }
140
144
  }
141
- renderedLines.push(trimmed)
145
+ return true
142
146
  }
143
- return renderedLines.join('\n').trim().length > 0
147
+ return false
144
148
  }
145
149
 
146
150
  const copyMap = (target, source) => {
@@ -159,10 +163,24 @@ export const processDescriptionList = (tokens, opt) => {
159
163
  if (!opt.descriptionList && !opt.descriptionListWithDiv) {
160
164
  return
161
165
  }
166
+ if (!Array.isArray(tokens) || tokens.length === 0) {
167
+ return
168
+ }
169
+
170
+ let firstBulletIndex = -1
171
+ for (let i = 0; i < tokens.length; i++) {
172
+ if (tokens[i].type === 'bullet_list_open') {
173
+ firstBulletIndex = i
174
+ break
175
+ }
176
+ }
177
+ if (firstBulletIndex === -1) {
178
+ return
179
+ }
162
180
 
163
181
  // Find bullet_lists and check if they match DL pattern
164
182
  // Process in single pass: check and convert together to avoid duplicate scanning
165
- let i = 0
183
+ let i = firstBulletIndex
166
184
  while (i < tokens.length) {
167
185
  if (tokens[i].type === 'bullet_list_open') {
168
186
  const listEnd = findListEnd(tokens, i)
@@ -253,6 +271,9 @@ const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
253
271
  // First pass: validate all items match DL pattern
254
272
  let hasAnyDLItem = false
255
273
  const itemRanges = collectListItemRanges(tokens, listStart, listEnd)
274
+ if (itemRanges.length === 0) {
275
+ return { nextIndex: listEnd + 1 }
276
+ }
256
277
 
257
278
  for (const range of itemRanges) {
258
279
  const itemStart = range.open
@@ -329,9 +350,12 @@ const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
329
350
  */
330
351
  const isDLPattern = (content) => {
331
352
  if (!content) return { isMatch: false, afterStrong: null }
353
+ if (content.length < 4 || content[0] !== '*' || content[1] !== '*') {
354
+ return { isMatch: false, afterStrong: null }
355
+ }
332
356
 
333
357
  // Match **Term** pattern (allow spaces inside for markdown-it-strong-ja compatibility)
334
- const match = content.match(/^\*\*(.*?)\*\*(.*)/s) // s flag for including newlines
358
+ const match = content.match(DL_TERM_INLINE_REGEX)
335
359
  if (!match) return { isMatch: false, afterStrong: null }
336
360
 
337
361
  const afterStrong = match[2] // Text after closing **
@@ -450,7 +474,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
450
474
  let descStart = ''
451
475
 
452
476
  const content = inlineToken.content
453
- const match = content.match(/^\*\*(.*?)\*\*(.*)/s) // s flag for including newlines
477
+ const match = content.match(DL_TERM_INLINE_REGEX)
454
478
 
455
479
  if (match) {
456
480
  // Trim to avoid leading space when **Term** starts with whitespace (e.g. "** *term*")
@@ -471,7 +495,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
471
495
  // Pattern B: {.attrs} on last line (e.g., "Description\n{.attrs}")
472
496
  // This will be processed by markdown-it-attrs and applied to list, not paragraph
473
497
  // We need to remove it from description content and save for list-level attrs
474
- const lastLineAttrsMatch = afterStrong.match(/\n\s*\{([^}]+)\}\s*$/)
498
+ const lastLineAttrsMatch = afterStrong.match(TRAILING_LIST_ATTR_LINE_REGEX)
475
499
  if (lastLineAttrsMatch) {
476
500
  // Parse attributes
477
501
  const attrString = lastLineAttrsMatch[1]
@@ -479,7 +503,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
479
503
  if (parsedAttrs.length > 0) {
480
504
  listAttrs.push(...parsedAttrs)
481
505
  // Remove {.attrs} line from afterStrong
482
- afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
506
+ afterStrong = afterStrong.replace(TRAILING_LIST_ATTR_LINE_REGEX, '')
483
507
  }
484
508
  }
485
509
 
@@ -495,14 +519,12 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
495
519
  if (!term) return { tokens: result, listAttrs }
496
520
 
497
521
  // Create div_open if descriptionListWithDiv is enabled
498
- if (opt && opt.descriptionListWithDiv) {
522
+ if (opt.descriptionListWithDiv) {
499
523
  const divOpen = new tokens[firstPara].constructor('div_open', 'div', 1)
500
524
  divOpen.level = parentLevel + 1
501
525
  divOpen.block = true
502
526
  copyMap(divOpen, tokens[firstPara])
503
- const divClass = typeof opt.descriptionListDivClass === 'string'
504
- ? opt.descriptionListDivClass.trim()
505
- : ''
527
+ const divClass = opt.descriptionListDivClass
506
528
  if (divClass) {
507
529
  divOpen.attrs = [['class', divClass]]
508
530
  }
@@ -542,7 +564,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
542
564
  result.push(ddOpen)
543
565
 
544
566
  // First paragraph in dd (if description exists)
545
- if (descStart.trim()) {
567
+ if (descStart) {
546
568
  const pOpen = new tokens[firstPara].constructor('paragraph_open', 'p', 1)
547
569
  pOpen.level = parentLevel + 2
548
570
  pOpen.block = true // IMPORTANT: Enable block mode for proper newline rendering
@@ -554,7 +576,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
554
576
  pInline.level = parentLevel + 3
555
577
  pInline.block = true // IMPORTANT: Enable block mode
556
578
  const pText = new tokens[firstPara].constructor('text', '', 0)
557
- pText.content = descStart.trim()
579
+ pText.content = descStart
558
580
  pInline.children = [pText]
559
581
  result.push(pInline)
560
582
 
@@ -612,7 +634,7 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
612
634
  result.push(ddClose)
613
635
 
614
636
  // Create div_close if descriptionListWithDiv is enabled
615
- if (opt && opt.descriptionListWithDiv) {
637
+ if (opt.descriptionListWithDiv) {
616
638
  const divClose = new tokens[firstPara].constructor('div_close', 'div', -1)
617
639
  divClose.level = parentLevel + 1
618
640
  divClose.block = true