@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/index.js +11 -12
- package/package.json +2 -2
- package/src/phase0-description-list.js +211 -107
- package/src/phase2-convert.js +133 -142
- package/src/phase3-attributes.js +24 -15
- package/src/phase5-spans.js +20 -7
- package/src/preprocess-literal-lists.js +123 -105
- package/src/types-utility.js +0 -15
- package/src/phase6-attrs-migration.js +0 -184
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ After the marker separator, an ASCII space is normally expected. For `fullwidth`
|
|
|
71
71
|
|
|
72
72
|
- Marker type detection is deterministic: gather all markers at a nesting level, match them against the canonical definitions, keep the type that explains the most items while preserving numeric continuity, and use the order in `listTypes.json` only as a final tiebreaker.
|
|
73
73
|
- Flattening: A pattern like `- 1.` is represented by the default `ul > li > ol` nesting structure in markdown-it, but this plugin simplifies it to a single `ol` by default to match the representation of other markers.
|
|
74
|
+
- With `markdown-it-attrs`, attr blocks follow that plugin's nearest-block behavior. This plugin does not reassign list attrs after flattening.
|
|
74
75
|
|
|
75
76
|
### Source map behavior
|
|
76
77
|
|
|
@@ -90,14 +91,22 @@ You can customize the conversion using options.
|
|
|
90
91
|
- `omitMarkerMetadata` (boolean) — If `true`, omit the `data-marker-prefix` / `data-marker-suffix` attributes.
|
|
91
92
|
- `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`.
|
|
92
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.
|
|
93
97
|
|
|
94
98
|
## Description lists conversion behavior
|
|
95
99
|
|
|
96
|
-
When
|
|
100
|
+
When `descriptionList` or `descriptionListWithDiv` is enabled, the plugin converts specially formatted bullet lists into HTML description lists (`<dl>`).
|
|
97
101
|
|
|
98
102
|
- Each list item must start with a `**Term**` line.
|
|
103
|
+
- The `**Term**` line must be the first direct block inside that list item (term-like text inside nested sub-lists does not trigger conversion).
|
|
99
104
|
- If the description continues on the next line without a blank line, the Term line must end with two ASCII spaces (a Markdown line-break) or a backslash `\`. Inline `{.attrs}` immediately after the term are allowed.
|
|
100
105
|
- If the Term line is followed by a blank line, the description can start in the next paragraph or list (line-break control characters are optional).
|
|
106
|
+
- Same-line descriptions such as `- **Term** Description` are not converted.
|
|
107
|
+
- Same-line backslash forms such as `- **Term**\ Description` are also not converted (the hard break marker must be followed by an actual newline).
|
|
108
|
+
- Term-line attrs can be written in multiple adjacent blocks (`**Term** {.a} {#id} {data-x=1}`) and are merged onto `<dt>`.
|
|
109
|
+
- Attr-like braces are kept as description text only when your plugin chain does not parse them as attributes. With `markdown-it-attrs`, forms like `{foo}` may be treated as boolean attributes.
|
|
101
110
|
|
|
102
111
|
In the conversion the `**Term**` line becomes a `<dt>` and the subsequent lines become the corresponding `<dd>`.
|
|
103
112
|
|
|
@@ -106,7 +115,7 @@ Note: Text descriptions are wrapped in `<p>` elements; additional paragraphs and
|
|
|
106
115
|
### Description list options
|
|
107
116
|
|
|
108
117
|
- `descriptionList` (boolean) — Enable conversion of `**Term**` list patterns into `<dl>` description lists.
|
|
109
|
-
- `descriptionListWithDiv` (boolean) — Wrap `<dt>/<dd>` pairs in a `<div>` when enabled.
|
|
118
|
+
- `descriptionListWithDiv` (boolean) — Wrap `<dt>/<dd>` pairs in a `<div>` when enabled. This option also enables description-list conversion even when `descriptionList` is `false`.
|
|
110
119
|
- `descriptionListDivClass` (string) — Class applied to the wrapper `<div>` when `descriptionListWithDiv` is enabled (empty string disables the class).
|
|
111
120
|
|
|
112
121
|
## Examples: Ordered Lists
|
package/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import { convertLists } from './src/phase2-convert.js'
|
|
|
5
5
|
import { addAttributes } from './src/phase3-attributes.js'
|
|
6
6
|
import { processHtmlBlocks } from './src/phase4-html-blocks.js'
|
|
7
7
|
import { generateSpans } from './src/phase5-spans.js'
|
|
8
|
-
import { moveNestedListAttributes } from './src/phase6-attrs-migration.js'
|
|
9
8
|
import { normalizeLiteralOrderedLists } from './src/preprocess-literal-lists.js'
|
|
10
9
|
|
|
11
10
|
const mditNumberingUl = (md, option) => {
|
|
@@ -73,7 +72,17 @@ const mditNumberingUl = (md, option) => {
|
|
|
73
72
|
|
|
74
73
|
// ===== PHASE 4: HTML Block Processing =====
|
|
75
74
|
// Remove indents from HTML blocks in lists and normalize line breaks
|
|
76
|
-
|
|
75
|
+
let hasNestedHtmlBlock = false
|
|
76
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
77
|
+
const token = tokens[i]
|
|
78
|
+
if (token.type === 'html_block' && token.level > 0) {
|
|
79
|
+
hasNestedHtmlBlock = true
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (hasNestedHtmlBlock) {
|
|
84
|
+
processHtmlBlocks(state)
|
|
85
|
+
}
|
|
77
86
|
|
|
78
87
|
// ===== PHASE 5: Span Generation =====
|
|
79
88
|
// Generate marker spans in alwaysMarkerSpan mode
|
|
@@ -85,16 +94,6 @@ const mditNumberingUl = (md, option) => {
|
|
|
85
94
|
md.core.ruler.before('inline', 'numbering_dl_parser', dlProcessor)
|
|
86
95
|
md.core.ruler.after('numbering_dl_parser', 'numbering_ul_phases', listProcessor)
|
|
87
96
|
|
|
88
|
-
if (!opt.unremoveUlNest) {
|
|
89
|
-
// Move nested list attributes only when flattening is enabled
|
|
90
|
-
const nestedListAttrProcessor = (state) => {
|
|
91
|
-
moveNestedListAttributes(state.tokens)
|
|
92
|
-
return true
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
addRuleAfter(md.core.ruler, 'curly_attributes', 'numbering_ul_nested_attrs', nestedListAttrProcessor)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
97
|
// Description list: Move paragraph attributes to dl and add custom renderers
|
|
99
98
|
if (opt.descriptionList || opt.descriptionListWithDiv) {
|
|
100
99
|
// Move paragraph attributes to dl (after inline and any attribute plugins)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-numbering-ul-regarded-as-ol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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,7 @@
|
|
|
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.
|
|
30
|
+
"@peaceroad/markdown-it-strong-ja": "^0.7.2",
|
|
31
31
|
"markdown-it": "^14.1.0",
|
|
32
32
|
"markdown-it-attrs": "^4.3.1",
|
|
33
33
|
"markdown-it-deflist": "^3.0.0"
|
|
@@ -8,34 +8,141 @@ import { findMatchingClose, findListEnd as coreFindListEnd } from './list-helper
|
|
|
8
8
|
* Parse attribute string like ".class1 .class2 #id data-foo="bar""
|
|
9
9
|
* Returns array of [key, value] pairs
|
|
10
10
|
*/
|
|
11
|
+
const ATTR_TOKEN_REGEX = /\s*(\.[\w-]+|#[\w-]+|[\w:-]+=(?:"[^"]*"|'[^']*'|[^\s"'{}]+)|[\w:-]+)/y
|
|
12
|
+
const ATTR_KEY_VALUE_REGEX = /^([\w:-]+)=(?:"([^"]*)"|'([^']*)'|([^\s"'{}]+))$/
|
|
13
|
+
const LEADING_ATTR_BLOCK_REGEX = /^[ \t]*\{([^}]+)\}/
|
|
14
|
+
const STANDALONE_ATTR_BLOCK_REGEX = /^\{([^}]+)\}$/
|
|
15
|
+
const TRAILING_TWO_SPACES_REGEX = / {2,}$/
|
|
16
|
+
const TRAILING_BACKSLASH_BREAK_REGEX = /\\\s*$/
|
|
17
|
+
|
|
11
18
|
const parseAttrString = (attrStr) => {
|
|
19
|
+
if (!attrStr || typeof attrStr !== 'string') {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const attrs = []
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const customAttrs = []
|
|
25
|
+
const classes = []
|
|
26
|
+
let id = null
|
|
27
|
+
let cursor = 0
|
|
28
|
+
|
|
29
|
+
while (cursor < attrStr.length) {
|
|
30
|
+
ATTR_TOKEN_REGEX.lastIndex = cursor
|
|
31
|
+
const match = ATTR_TOKEN_REGEX.exec(attrStr)
|
|
32
|
+
if (!match) {
|
|
33
|
+
// Allow only trailing whitespace; otherwise reject as invalid attr syntax.
|
|
34
|
+
if (/^\s*$/.test(attrStr.slice(cursor))) {
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
const token = match[1]
|
|
40
|
+
cursor = ATTR_TOKEN_REGEX.lastIndex
|
|
41
|
+
if (token[0] === '.') {
|
|
42
|
+
const cls = token.slice(1)
|
|
43
|
+
if (cls) {
|
|
44
|
+
classes.push(cls)
|
|
45
|
+
}
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (token[0] === '#') {
|
|
49
|
+
if (!id) {
|
|
50
|
+
id = token.slice(1)
|
|
51
|
+
}
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const kv = token.match(ATTR_KEY_VALUE_REGEX)
|
|
56
|
+
if (!kv) {
|
|
57
|
+
// markdown-it-attrs boolean attribute (e.g. {foo})
|
|
58
|
+
customAttrs.push([token, ''])
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
const key = kv[1]
|
|
62
|
+
const value = kv[2] ?? kv[3] ?? kv[4] ?? ''
|
|
63
|
+
customAttrs.push([key, value])
|
|
21
64
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
attrs.push(['id', idMatch[1]])
|
|
65
|
+
|
|
66
|
+
if (classes.length > 0) {
|
|
67
|
+
attrs.push(['class', classes.join(' ')])
|
|
26
68
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (dataMatches) {
|
|
30
|
-
dataMatches.forEach(match => {
|
|
31
|
-
const [, key, value] = match.match(/([\w-]+)="([^"]+)"/)
|
|
32
|
-
attrs.push([key, value])
|
|
33
|
-
})
|
|
69
|
+
if (id) {
|
|
70
|
+
attrs.push(['id', id])
|
|
34
71
|
}
|
|
35
|
-
|
|
72
|
+
if (customAttrs.length > 0) {
|
|
73
|
+
attrs.push(...customAttrs)
|
|
74
|
+
}
|
|
75
|
+
|
|
36
76
|
return attrs
|
|
37
77
|
}
|
|
38
78
|
|
|
79
|
+
const consumeLeadingValidAttrBlocks = (text) => {
|
|
80
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
81
|
+
return { rest: text || '', attrs: [] }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let rest = text
|
|
85
|
+
const attrs = []
|
|
86
|
+
while (true) {
|
|
87
|
+
const match = rest.match(LEADING_ATTR_BLOCK_REGEX)
|
|
88
|
+
if (!match) {
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
const parsed = parseAttrString(match[1])
|
|
92
|
+
if (parsed.length === 0) {
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
attrs.push(...parsed)
|
|
96
|
+
rest = rest.slice(match[0].length)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { rest, attrs }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasExplicitLineBreakMarker = (afterStrong) => {
|
|
103
|
+
if (typeof afterStrong !== 'string') {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
const lineBreakIndex = afterStrong.indexOf('\n')
|
|
107
|
+
if (lineBreakIndex === -1) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
const firstLine = afterStrong.slice(0, lineBreakIndex)
|
|
111
|
+
return TRAILING_TWO_SPACES_REGEX.test(firstLine) || TRAILING_BACKSLASH_BREAK_REGEX.test(firstLine)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const getDescriptionAfterExplicitLineBreak = (afterStrong) => {
|
|
115
|
+
if (!hasExplicitLineBreakMarker(afterStrong)) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
const lineBreakIndex = afterStrong.indexOf('\n')
|
|
119
|
+
return afterStrong.slice(lineBreakIndex + 1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hasMeaningfulDescriptionContent = (text) => {
|
|
123
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = text.split('\n')
|
|
128
|
+
const renderedLines = []
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const trimmed = line.trim()
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
const attrMatch = trimmed.match(STANDALONE_ATTR_BLOCK_REGEX)
|
|
135
|
+
if (attrMatch) {
|
|
136
|
+
const parsed = parseAttrString(attrMatch[1])
|
|
137
|
+
if (parsed.length > 0) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
renderedLines.push(trimmed)
|
|
142
|
+
}
|
|
143
|
+
return renderedLines.join('\n').trim().length > 0
|
|
144
|
+
}
|
|
145
|
+
|
|
39
146
|
const copyMap = (target, source) => {
|
|
40
147
|
if (!target || !source || !Array.isArray(source.map)) {
|
|
41
148
|
return
|
|
@@ -47,7 +154,6 @@ const copyMap = (target, source) => {
|
|
|
47
154
|
* Process description list patterns in tokens
|
|
48
155
|
* @param {Array} tokens - Token array
|
|
49
156
|
* @param {Object} opt - Options object
|
|
50
|
-
```
|
|
51
157
|
*/
|
|
52
158
|
export const processDescriptionList = (tokens, opt) => {
|
|
53
159
|
if (!opt.descriptionList && !opt.descriptionListWithDiv) {
|
|
@@ -61,7 +167,7 @@ export const processDescriptionList = (tokens, opt) => {
|
|
|
61
167
|
if (tokens[i].type === 'bullet_list_open') {
|
|
62
168
|
const listEnd = findListEnd(tokens, i)
|
|
63
169
|
const dlCheck = checkAndConvertToDL(tokens, i, listEnd, opt)
|
|
64
|
-
i = dlCheck.nextIndex
|
|
170
|
+
i = typeof dlCheck?.nextIndex === 'number' ? dlCheck.nextIndex : listEnd + 1
|
|
65
171
|
} else {
|
|
66
172
|
i++
|
|
67
173
|
}
|
|
@@ -108,6 +214,21 @@ const collectListItemRanges = (tokens, listStart, listEnd) => {
|
|
|
108
214
|
return ranges
|
|
109
215
|
}
|
|
110
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Return the first direct child token index of a list_item.
|
|
219
|
+
* Direct child means token.level === list_item.level + 1.
|
|
220
|
+
*/
|
|
221
|
+
const findFirstDirectChildInListItem = (tokens, itemStart, itemEnd) => {
|
|
222
|
+
const itemLevel = tokens[itemStart]?.level ?? 0
|
|
223
|
+
const childLevel = itemLevel + 1
|
|
224
|
+
for (let i = itemStart + 1; i < itemEnd; i++) {
|
|
225
|
+
if (tokens[i].level === childLevel) {
|
|
226
|
+
return i
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return -1
|
|
230
|
+
}
|
|
231
|
+
|
|
111
232
|
/**
|
|
112
233
|
* Find matching dl_close token
|
|
113
234
|
*/
|
|
@@ -137,59 +258,57 @@ const checkAndConvertToDL = (tokens, listStart, listEnd, opt) => {
|
|
|
137
258
|
const itemStart = range.open
|
|
138
259
|
const itemEnd = range.close
|
|
139
260
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (tokens[j].type === 'paragraph_open') {
|
|
144
|
-
firstPara = j
|
|
145
|
-
break
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (firstPara !== -1) {
|
|
261
|
+
const firstChild = findFirstDirectChildInListItem(tokens, itemStart, itemEnd)
|
|
262
|
+
if (firstChild !== -1 && tokens[firstChild].type === 'paragraph_open') {
|
|
263
|
+
const firstPara = firstChild
|
|
150
264
|
const inlineToken = tokens[firstPara + 1]
|
|
151
|
-
if (inlineToken
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
265
|
+
if (!inlineToken || inlineToken.type !== 'inline') {
|
|
266
|
+
return { nextIndex: listEnd + 1 }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const dlCheck = isDLPattern(inlineToken.content)
|
|
270
|
+
if (!dlCheck.isMatch) {
|
|
271
|
+
// Not all items are DL pattern - not a description list
|
|
272
|
+
return { nextIndex: listEnd + 1 }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if there's a description
|
|
276
|
+
let hasDescription = false
|
|
277
|
+
const afterStrong = dlCheck.afterStrong
|
|
278
|
+
|
|
279
|
+
// Pattern 1: Explicit line-break marker after term line (` ` or `\`) and
|
|
280
|
+
// description text in the remaining first paragraph content.
|
|
281
|
+
const explicitBreakDescription = getDescriptionAfterExplicitLineBreak(afterStrong)
|
|
282
|
+
if (explicitBreakDescription !== null && hasMeaningfulDescriptionContent(explicitBreakDescription)) {
|
|
283
|
+
hasDescription = true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Pattern 2: Description in next paragraph/list (term-only first paragraph).
|
|
287
|
+
if (!hasDescription) {
|
|
288
|
+
// Check for additional paragraphs/lists
|
|
289
|
+
const itemChildLevel = (tokens[itemStart]?.level ?? 0) + 1
|
|
290
|
+
for (let k = firstPara + 3; k < itemEnd; k++) {
|
|
291
|
+
if (tokens[k].level !== itemChildLevel) {
|
|
292
|
+
continue
|
|
180
293
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
294
|
+
if (tokens[k].type === 'paragraph_open' ||
|
|
295
|
+
tokens[k].type === 'bullet_list_open' ||
|
|
296
|
+
tokens[k].type === 'ordered_list_open') {
|
|
297
|
+
hasDescription = true
|
|
298
|
+
break
|
|
185
299
|
}
|
|
186
|
-
|
|
187
|
-
hasAnyDLItem = true
|
|
188
|
-
} else {
|
|
189
|
-
// Not all items are DL pattern - not a description list
|
|
190
|
-
return { nextIndex: listEnd + 1 }
|
|
191
300
|
}
|
|
192
301
|
}
|
|
302
|
+
|
|
303
|
+
// If no description, not a DL item
|
|
304
|
+
if (!hasDescription) {
|
|
305
|
+
return { nextIndex: listEnd + 1 }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
hasAnyDLItem = true
|
|
309
|
+
} else {
|
|
310
|
+
// Any list_item that doesn't start with a paragraph cannot be a DL item.
|
|
311
|
+
return { nextIndex: listEnd + 1 }
|
|
193
312
|
}
|
|
194
313
|
}
|
|
195
314
|
|
|
@@ -217,14 +336,11 @@ const isDLPattern = (content) => {
|
|
|
217
336
|
|
|
218
337
|
const afterStrong = match[2] // Text after closing **
|
|
219
338
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
const isMatch =
|
|
225
|
-
/^\\/.test(afterStrong) ||
|
|
226
|
-
/^\s*$/.test(afterStrong) ||
|
|
227
|
-
/^\s*\{[^}]+\}/.test(afterStrong) // {.class} or {#id} etc
|
|
339
|
+
// Match only strict description-list starts:
|
|
340
|
+
// 1) explicit line-break marker after term line (` ` or `\`) + newline
|
|
341
|
+
// 2) term-only first line (optionally with valid attrs only), expecting next block as description
|
|
342
|
+
const { rest: afterAttrs } = consumeLeadingValidAttrBlocks(afterStrong)
|
|
343
|
+
const isMatch = hasExplicitLineBreakMarker(afterStrong) || /^\s*$/.test(afterAttrs)
|
|
228
344
|
|
|
229
345
|
return { isMatch, afterStrong }
|
|
230
346
|
}
|
|
@@ -319,14 +435,10 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
319
435
|
const listItemToken = tokens[itemStart]
|
|
320
436
|
let dtAttrs = listItemToken.attrs ? [...listItemToken.attrs] : null
|
|
321
437
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
firstPara = i
|
|
327
|
-
break
|
|
328
|
-
}
|
|
329
|
-
}
|
|
438
|
+
const firstChild = findFirstDirectChildInListItem(tokens, itemStart, itemEnd)
|
|
439
|
+
const firstPara = firstChild !== -1 && tokens[firstChild].type === 'paragraph_open'
|
|
440
|
+
? firstChild
|
|
441
|
+
: -1
|
|
330
442
|
|
|
331
443
|
if (firstPara === -1) return { tokens: result, listAttrs }
|
|
332
444
|
|
|
@@ -345,21 +457,15 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
345
457
|
term = match[1].trim()
|
|
346
458
|
let afterStrong = match[2]
|
|
347
459
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const parsedAttrs = parseAttrString(attrString)
|
|
355
|
-
if (parsedAttrs.length > 0) {
|
|
356
|
-
if (!dtAttrs) {
|
|
357
|
-
dtAttrs = []
|
|
358
|
-
}
|
|
359
|
-
dtAttrs.push(...parsedAttrs)
|
|
460
|
+
// Pattern A: one or more leading attr blocks right after **Term**.
|
|
461
|
+
// These map to <dt> like markdown-it-attrs trailing attrs on a block line.
|
|
462
|
+
const leadingAttrs = consumeLeadingValidAttrBlocks(afterStrong)
|
|
463
|
+
if (leadingAttrs.attrs.length > 0) {
|
|
464
|
+
if (!dtAttrs) {
|
|
465
|
+
dtAttrs = []
|
|
360
466
|
}
|
|
361
|
-
|
|
362
|
-
afterStrong =
|
|
467
|
+
dtAttrs.push(...leadingAttrs.attrs)
|
|
468
|
+
afterStrong = leadingAttrs.rest
|
|
363
469
|
}
|
|
364
470
|
|
|
365
471
|
// Pattern B: {.attrs} on last line (e.g., "Description\n{.attrs}")
|
|
@@ -372,9 +478,9 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
372
478
|
const parsedAttrs = parseAttrString(attrString)
|
|
373
479
|
if (parsedAttrs.length > 0) {
|
|
374
480
|
listAttrs.push(...parsedAttrs)
|
|
481
|
+
// Remove {.attrs} line from afterStrong
|
|
482
|
+
afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
|
|
375
483
|
}
|
|
376
|
-
// Remove {.attrs} line from afterStrong
|
|
377
|
-
afterStrong = afterStrong.replace(/\n\s*\{[^}]+\}\s*$/, '')
|
|
378
484
|
}
|
|
379
485
|
|
|
380
486
|
// Clean up afterStrong: remove leading spaces/backslash, then trim each line
|
|
@@ -394,7 +500,9 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
394
500
|
divOpen.level = parentLevel + 1
|
|
395
501
|
divOpen.block = true
|
|
396
502
|
copyMap(divOpen, tokens[firstPara])
|
|
397
|
-
const divClass = typeof opt.descriptionListDivClass === 'string'
|
|
503
|
+
const divClass = typeof opt.descriptionListDivClass === 'string'
|
|
504
|
+
? opt.descriptionListDivClass.trim()
|
|
505
|
+
: ''
|
|
398
506
|
if (divClass) {
|
|
399
507
|
divOpen.attrs = [['class', divClass]]
|
|
400
508
|
}
|
|
@@ -434,7 +542,6 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
434
542
|
result.push(ddOpen)
|
|
435
543
|
|
|
436
544
|
// First paragraph in dd (if description exists)
|
|
437
|
-
let hasFirstParagraph = false
|
|
438
545
|
if (descStart.trim()) {
|
|
439
546
|
const pOpen = new tokens[firstPara].constructor('paragraph_open', 'p', 1)
|
|
440
547
|
pOpen.level = parentLevel + 2
|
|
@@ -457,7 +564,6 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
457
564
|
copyMap(pClose, tokens[firstPara])
|
|
458
565
|
result.push(pClose)
|
|
459
566
|
|
|
460
|
-
hasFirstParagraph = true
|
|
461
567
|
}
|
|
462
568
|
|
|
463
569
|
// Add remaining content in dd (paragraphs, lists, etc.)
|
|
@@ -467,8 +573,6 @@ const convertListItemToDtDd = (tokens, itemStart, itemEnd, parentLevel, opt) =>
|
|
|
467
573
|
|
|
468
574
|
// Handle paragraph
|
|
469
575
|
if (token.type === 'paragraph_open') {
|
|
470
|
-
hasFirstParagraph = true
|
|
471
|
-
|
|
472
576
|
result.push(tokens[i]) // paragraph_open
|
|
473
577
|
result.push(tokens[i + 1]) // inline
|
|
474
578
|
result.push(tokens[i + 2]) // paragraph_close
|