@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
|
@@ -5,6 +5,12 @@ import { findMatchingClose, findListItemEnd } from './list-helpers.js'
|
|
|
5
5
|
|
|
6
6
|
// markdown-it treats indent >= marker width + 4 as code blocks inside list items.
|
|
7
7
|
const MAX_LITERAL_INLINE_INDENT = 3
|
|
8
|
+
const LITERAL_SUFFIX_CHARS = new Set(['.', ')', '.', ')', '、'])
|
|
9
|
+
const LITERAL_OPEN_CLOSE_PAIRS = new Map([
|
|
10
|
+
['(', ')'],
|
|
11
|
+
['(', ')']
|
|
12
|
+
])
|
|
13
|
+
const ASCII_ALNUM_OR_FULLWIDTH_DIGIT_REGEX = /^[A-Za-z0-90-9]+$/
|
|
8
14
|
const getIndentWidth = (indentText) => indentText.replace(/\t/g, ' ').length
|
|
9
15
|
const buildLineMap = (startLine, endLine = null) => {
|
|
10
16
|
if (typeof startLine !== 'number') {
|
|
@@ -24,6 +30,109 @@ const getListItemMarkerWidth = (listItem) => {
|
|
|
24
30
|
return markerLength > 0 ? markerLength + 1 : 1
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
const getLineTokenWithIndent = (line) => {
|
|
34
|
+
if (typeof line !== 'string' || line.length === 0) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
const match = line.match(/^([ \t]*)(\S+)(?:\s|$)/)
|
|
38
|
+
if (!match) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
indentWidth: getIndentWidth(match[1]),
|
|
43
|
+
token: match[2]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasLikelyLiteralMarkerToken = (token) => {
|
|
48
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let core = token
|
|
53
|
+
let hasSuffix = false
|
|
54
|
+
const firstChar = core[0]
|
|
55
|
+
const closingPair = LITERAL_OPEN_CLOSE_PAIRS.get(firstChar)
|
|
56
|
+
if (closingPair) {
|
|
57
|
+
const closeIdx = core.indexOf(closingPair, 1)
|
|
58
|
+
if (closeIdx <= 1) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
const inner = core.slice(1, closeIdx)
|
|
62
|
+
const rest = core.slice(closeIdx + 1)
|
|
63
|
+
if (rest.length > 1) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
if (rest.length === 1) {
|
|
67
|
+
if (!LITERAL_SUFFIX_CHARS.has(rest)) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
hasSuffix = true
|
|
71
|
+
}
|
|
72
|
+
core = inner
|
|
73
|
+
} else {
|
|
74
|
+
const tail = core[core.length - 1]
|
|
75
|
+
if (LITERAL_SUFFIX_CHARS.has(tail)) {
|
|
76
|
+
core = core.slice(0, -1)
|
|
77
|
+
hasSuffix = true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!core) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For ASCII tokens, require explicit suffix to avoid treating normal words as list markers.
|
|
86
|
+
if (ASCII_ALNUM_OR_FULLWIDTH_DIGIT_REGEX.test(core)) {
|
|
87
|
+
return hasSuffix
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Non-ASCII marker cores (circled digits, kana, kanji, etc.) are typically short.
|
|
91
|
+
return core.length <= 4
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hasLikelyLiteralLineHint = (content) => {
|
|
95
|
+
if (typeof content !== 'string' || !content.includes('\n')) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
const lines = content.split('\n')
|
|
99
|
+
for (let i = 1; i < lines.length; i++) {
|
|
100
|
+
const line = lines[i]
|
|
101
|
+
if (!line || line.trim().length === 0) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const tokenInfo = getLineTokenWithIndent(line)
|
|
105
|
+
if (!tokenInfo || tokenInfo.indentWidth > MAX_LITERAL_INLINE_INDENT) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (hasLikelyLiteralMarkerToken(tokenInfo.token)) {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hasOverIndentedMarkerLikeLine = (content) => {
|
|
116
|
+
if (typeof content !== 'string' || !content.includes('\n')) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
const lines = content.split('\n')
|
|
120
|
+
for (let i = 1; i < lines.length; i++) {
|
|
121
|
+
const line = lines[i]
|
|
122
|
+
if (!line || line.trim().length === 0) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
const tokenInfo = getLineTokenWithIndent(line)
|
|
126
|
+
if (!tokenInfo || tokenInfo.indentWidth <= MAX_LITERAL_INLINE_INDENT) {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (hasLikelyLiteralMarkerToken(tokenInfo.token)) {
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
27
136
|
/**
|
|
28
137
|
* Normalize literal nested ordered lists inside list items.
|
|
29
138
|
* Converts indented numeric lines into proper ordered_list tokens before Phase 1.
|
|
@@ -71,11 +180,6 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
|
|
|
71
180
|
let j = i + 1
|
|
72
181
|
while (j < listItemClose) {
|
|
73
182
|
const current = tokens[j]
|
|
74
|
-
if (current.type !== 'paragraph_open') {
|
|
75
|
-
j++
|
|
76
|
-
continue
|
|
77
|
-
}
|
|
78
|
-
|
|
79
183
|
if (current.type === 'paragraph_open') {
|
|
80
184
|
const inlineIdx = j + 1
|
|
81
185
|
const paragraphCloseIdx = j + 2
|
|
@@ -92,6 +196,17 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
|
|
|
92
196
|
j = paragraphCloseIdx + 1
|
|
93
197
|
continue
|
|
94
198
|
}
|
|
199
|
+
if (!hasLikelyLiteralLineHint(inlineToken.content)) {
|
|
200
|
+
j = paragraphCloseIdx + 1
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
// Be conservative when deeply-indented marker-like lines are present.
|
|
204
|
+
// These lines are often code blocks; partial literal conversion is more surprising
|
|
205
|
+
// than preserving markdown-it's original rendering in this ambiguous case.
|
|
206
|
+
if (hasOverIndentedMarkerLikeLine(inlineToken.content)) {
|
|
207
|
+
j = paragraphCloseIdx + 1
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
95
210
|
const baseLine = tokens[j].map ? tokens[j].map[0] : null
|
|
96
211
|
const segments = parseSegments(inlineToken.content, markerWidth, baseLine)
|
|
97
212
|
if (!segments.hasLiteral) {
|
|
@@ -126,14 +241,6 @@ export function normalizeLiteralOrderedLists(tokens, opt) {
|
|
|
126
241
|
continue
|
|
127
242
|
}
|
|
128
243
|
|
|
129
|
-
if (current.type === 'ordered_list_open' &&
|
|
130
|
-
current.level === (tokens[i].level ?? 0) + 1) {
|
|
131
|
-
const delta = splitOrderedListForLiteralChildren(tokens, j, TokenClass)
|
|
132
|
-
listItemClose += delta
|
|
133
|
-
j++
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
|
|
137
244
|
j++
|
|
138
245
|
}
|
|
139
246
|
i = listItemClose
|
|
@@ -166,19 +273,16 @@ function parseSegments(content, markerWidth, baseLine = null) {
|
|
|
166
273
|
}
|
|
167
274
|
}
|
|
168
275
|
const textValue = buffer.join('\n')
|
|
169
|
-
|
|
170
|
-
segments.push({ type: 'text', text: textValue, tight: !hasBlankLine })
|
|
276
|
+
segments.push({ type: 'text', text: textValue, tight: !hadBlankLine })
|
|
171
277
|
buffer = []
|
|
172
278
|
blankLinesInBuffer = 0
|
|
173
279
|
}
|
|
174
280
|
|
|
175
281
|
while (idx < lines.length) {
|
|
176
282
|
const isFirstLine = idx === 0
|
|
177
|
-
|
|
283
|
+
const trimmedLine = lines[idx].trim()
|
|
284
|
+
if (isFirstLine && trimmedLine.length > 0) {
|
|
178
285
|
buffer.push(lines[idx])
|
|
179
|
-
if (lines[idx].trim().length === 0) {
|
|
180
|
-
blankLinesInBuffer++
|
|
181
|
-
}
|
|
182
286
|
idx++
|
|
183
287
|
continue
|
|
184
288
|
}
|
|
@@ -646,89 +750,3 @@ function markLiteralListLoose(tokens, listOpenIndex, listCloseIndex = null) {
|
|
|
646
750
|
}
|
|
647
751
|
}
|
|
648
752
|
}
|
|
649
|
-
|
|
650
|
-
function splitOrderedListForLiteralChildren(tokens, listOpenIndex, TokenClass) {
|
|
651
|
-
const listToken = tokens[listOpenIndex]
|
|
652
|
-
const listCloseIndex = findMatchingClose(tokens, listOpenIndex, 'ordered_list_open', 'ordered_list_close')
|
|
653
|
-
if (listCloseIndex === -1) {
|
|
654
|
-
return 0
|
|
655
|
-
}
|
|
656
|
-
const childLevel = (listToken.level ?? 0) + 1
|
|
657
|
-
const ranges = []
|
|
658
|
-
let currentOpen = -1
|
|
659
|
-
for (let idx = listOpenIndex + 1; idx < listCloseIndex; idx++) {
|
|
660
|
-
const token = tokens[idx]
|
|
661
|
-
if (token.type === 'list_item_open' && token.level === childLevel) {
|
|
662
|
-
currentOpen = idx
|
|
663
|
-
continue
|
|
664
|
-
}
|
|
665
|
-
if (token.type === 'list_item_close' && token.level === childLevel) {
|
|
666
|
-
if (currentOpen !== -1) {
|
|
667
|
-
ranges.push({ open: currentOpen, close: idx })
|
|
668
|
-
currentOpen = -1
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (ranges.length <= 1) {
|
|
674
|
-
return 0
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const extraRanges = ranges.slice(1)
|
|
678
|
-
const childTokens = []
|
|
679
|
-
let removedCount = 0
|
|
680
|
-
|
|
681
|
-
for (const range of extraRanges) {
|
|
682
|
-
childTokens.push(...tokens.slice(range.open, range.close + 1))
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
for (let k = extraRanges.length - 1; k >= 0; k--) {
|
|
686
|
-
const range = extraRanges[k]
|
|
687
|
-
const len = range.close - range.open + 1
|
|
688
|
-
tokens.splice(range.open, len)
|
|
689
|
-
removedCount += len
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (listToken._markerInfo) {
|
|
693
|
-
delete listToken._markerInfo
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const levelShift = 2
|
|
697
|
-
for (const token of childTokens) {
|
|
698
|
-
if (typeof token.level === 'number') {
|
|
699
|
-
token.level += levelShift
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const nestedListOpen = new TokenClass('ordered_list_open', 'ol', 1)
|
|
704
|
-
nestedListOpen.level = (listToken.level ?? 0) + 2
|
|
705
|
-
nestedListOpen.block = true
|
|
706
|
-
nestedListOpen.markup = listToken.markup
|
|
707
|
-
nestedListOpen.attrs = null
|
|
708
|
-
nestedListOpen._literalList = true
|
|
709
|
-
if (Array.isArray(listToken.map)) {
|
|
710
|
-
nestedListOpen.map = listToken.map.slice()
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const firstChild = childTokens.find(t => t.type === 'list_item_open')
|
|
714
|
-
if (firstChild?.info) {
|
|
715
|
-
const num = parseInt(firstChild.info, 10)
|
|
716
|
-
if (!Number.isNaN(num) && num !== 1) {
|
|
717
|
-
nestedListOpen.attrs = [['start', String(num)]]
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const nestedListClose = new TokenClass('ordered_list_close', 'ol', -1)
|
|
722
|
-
nestedListClose.level = nestedListOpen.level
|
|
723
|
-
nestedListClose.block = true
|
|
724
|
-
nestedListClose.markup = listToken.markup
|
|
725
|
-
if (Array.isArray(listToken.map)) {
|
|
726
|
-
nestedListClose.map = listToken.map.slice()
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const insertionIndex = ranges[0].close
|
|
730
|
-
tokens.splice(insertionIndex, 0, nestedListOpen, ...childTokens, nestedListClose)
|
|
731
|
-
|
|
732
|
-
const addedCount = childTokens.length + 2
|
|
733
|
-
return addedCount - removedCount
|
|
734
|
-
}
|
package/src/types-utility.js
CHANGED
|
@@ -371,21 +371,6 @@ const createMarkerResult = (type, marker, number, prefix, suffix) => ({
|
|
|
371
371
|
suffix
|
|
372
372
|
})
|
|
373
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
374
|
// Enhanced marker type detection with context awareness
|
|
390
375
|
export const detectMarkerType = (content, allContents = null) => {
|
|
391
376
|
let contextResult = null
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// Phase 6: Attribute Migration
|
|
2
|
-
// Runs after markdown-it-attrs processing to handle nested list attributes
|
|
3
|
-
// This phase moves custom attributes from child lists to parent lists
|
|
4
|
-
// in flattened `- 1. Parent\n - a. Child\n{.class}` patterns
|
|
5
|
-
|
|
6
|
-
import { buildListCloseIndexMap } from './list-helpers.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Move custom attributes from nested child ordered_list to parent ordered_list
|
|
10
|
-
*
|
|
11
|
-
* Background:
|
|
12
|
-
* - Plugin flattens `ul > li > ol` structure to `ol > li` (simplifyNestedBulletLists)
|
|
13
|
-
* - markdown-it-attrs runs on original structure, applies {.class} to child list
|
|
14
|
-
* - Users expect {.class} after nested list to apply to parent list
|
|
15
|
-
*
|
|
16
|
-
* Algorithm:
|
|
17
|
-
* 1. Find top-level ordered_list_open tokens (level 0 or 2)
|
|
18
|
-
* 2. Locate child ordered_list_open within first list_item
|
|
19
|
-
* 3. Extract custom attributes (exclude plugin-generated: type, data-marker-*, role, ol-* classes)
|
|
20
|
-
* 4. Move custom classes to parent's class list
|
|
21
|
-
* 5. Move other custom attrs to parent
|
|
22
|
-
* 6. Remove moved attrs from child
|
|
23
|
-
*
|
|
24
|
-
* @param {Array} tokens - Token array to process
|
|
25
|
-
*/
|
|
26
|
-
export function moveNestedListAttributes(tokens) {
|
|
27
|
-
const tokensLength = tokens.length
|
|
28
|
-
const closeMap = buildListCloseIndexMap(tokens)
|
|
29
|
-
const listCloseByOpen = closeMap.listCloseByOpen
|
|
30
|
-
const listItemCloseByOpen = closeMap.listItemCloseByOpen
|
|
31
|
-
|
|
32
|
-
// Single pass: find top-level ordered_lists and process immediately
|
|
33
|
-
for (let i = 0; i < tokensLength; i++) {
|
|
34
|
-
const token = tokens[i]
|
|
35
|
-
|
|
36
|
-
// Skip non-top-level ordered lists
|
|
37
|
-
if (token.type !== 'ordered_list_open' || (token.level !== 0 && token.level !== 2)) {
|
|
38
|
-
continue
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const parentToken = token
|
|
42
|
-
const parentLevel = token.level
|
|
43
|
-
|
|
44
|
-
// Find list end
|
|
45
|
-
let listEndIndex = listCloseByOpen[i]
|
|
46
|
-
if (typeof listEndIndex !== 'number' || listEndIndex === -1) {
|
|
47
|
-
listEndIndex = tokensLength
|
|
48
|
-
let depth = 1
|
|
49
|
-
for (let j = i + 1; j < tokensLength; j++) {
|
|
50
|
-
if (tokens[j].type === 'ordered_list_open') depth++
|
|
51
|
-
else if (tokens[j].type === 'ordered_list_close') {
|
|
52
|
-
depth--
|
|
53
|
-
if (depth === 0) {
|
|
54
|
-
listEndIndex = j
|
|
55
|
-
break
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Find first list_item
|
|
62
|
-
let firstItemOpen = -1
|
|
63
|
-
let firstItemClose = -1
|
|
64
|
-
|
|
65
|
-
for (let j = i + 1; j < listEndIndex; j++) {
|
|
66
|
-
const t = tokens[j]
|
|
67
|
-
|
|
68
|
-
if (t.type === 'list_item_open' && t.level === parentLevel + 1) {
|
|
69
|
-
firstItemOpen = j
|
|
70
|
-
firstItemClose = listItemCloseByOpen[j]
|
|
71
|
-
if (typeof firstItemClose !== 'number' || firstItemClose === -1) {
|
|
72
|
-
// Find matching close
|
|
73
|
-
let itemDepth = 1
|
|
74
|
-
for (let k = j + 1; k < tokensLength; k++) {
|
|
75
|
-
if (tokens[k].type === 'list_item_open') itemDepth++
|
|
76
|
-
else if (tokens[k].type === 'list_item_close') {
|
|
77
|
-
itemDepth--
|
|
78
|
-
if (itemDepth === 0) {
|
|
79
|
-
firstItemClose = k
|
|
80
|
-
break
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (firstItemOpen === -1 || firstItemClose === -1) continue
|
|
90
|
-
|
|
91
|
-
// Find child ordered_list within first list_item
|
|
92
|
-
let childListOpen = -1
|
|
93
|
-
for (let j = firstItemOpen + 1; j < firstItemClose && j < tokensLength; j++) {
|
|
94
|
-
if (tokens[j].type === 'ordered_list_open' && tokens[j].level > parentLevel) {
|
|
95
|
-
childListOpen = j
|
|
96
|
-
break
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (childListOpen === -1) continue
|
|
101
|
-
|
|
102
|
-
const childToken = tokens[childListOpen]
|
|
103
|
-
if (!childToken.attrs || childToken.attrs.length === 0) continue
|
|
104
|
-
|
|
105
|
-
// Extract custom attributes (exclude plugin-generated)
|
|
106
|
-
const customAttrs = []
|
|
107
|
-
const remainingAttrs = []
|
|
108
|
-
|
|
109
|
-
for (let j = 0; j < childToken.attrs.length; j++) {
|
|
110
|
-
const [key, value] = childToken.attrs[j]
|
|
111
|
-
|
|
112
|
-
// Exclude plugin-generated attributes
|
|
113
|
-
if (key === 'type' || key === 'role' || key === 'style' || key === 'start' || key.startsWith('data-marker-')) {
|
|
114
|
-
remainingAttrs.push([key, value])
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Handle class attribute specially
|
|
119
|
-
if (key === 'class') {
|
|
120
|
-
const classes = value.split(/\s+/)
|
|
121
|
-
const pluginClasses = []
|
|
122
|
-
const customClasses = []
|
|
123
|
-
|
|
124
|
-
for (let k = 0; k < classes.length; k++) {
|
|
125
|
-
// Treat ol-* as plugin-generated (should remain on child)
|
|
126
|
-
if (classes[k].startsWith('ol-')) {
|
|
127
|
-
pluginClasses.push(classes[k])
|
|
128
|
-
} else {
|
|
129
|
-
customClasses.push(classes[k])
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (pluginClasses.length > 0) {
|
|
134
|
-
remainingAttrs.push(['class', pluginClasses.join(' ')])
|
|
135
|
-
}
|
|
136
|
-
if (customClasses.length > 0) {
|
|
137
|
-
customAttrs.push(['class', customClasses.join(' ')])
|
|
138
|
-
}
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// All other attributes are custom
|
|
143
|
-
customAttrs.push([key, value])
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (customAttrs.length === 0) continue
|
|
147
|
-
|
|
148
|
-
// Move custom attributes to parent
|
|
149
|
-
if (!parentToken.attrs) {
|
|
150
|
-
parentToken.attrs = []
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Merge attributes
|
|
154
|
-
for (let j = 0; j < customAttrs.length; j++) {
|
|
155
|
-
const [key, value] = customAttrs[j]
|
|
156
|
-
|
|
157
|
-
if (key === 'class') {
|
|
158
|
-
// Find existing class attribute
|
|
159
|
-
let existingClassIdx = -1
|
|
160
|
-
for (let k = 0; k < parentToken.attrs.length; k++) {
|
|
161
|
-
if (parentToken.attrs[k][0] === 'class') {
|
|
162
|
-
existingClassIdx = k
|
|
163
|
-
break
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (existingClassIdx !== -1) {
|
|
168
|
-
const existingClasses = parentToken.attrs[existingClassIdx][1].split(/\s+/)
|
|
169
|
-
const newClasses = value.split(/\s+/)
|
|
170
|
-
// Use Set for deduplication
|
|
171
|
-
const mergedClasses = [...new Set([...existingClasses, ...newClasses])]
|
|
172
|
-
parentToken.attrs[existingClassIdx][1] = mergedClasses.join(' ')
|
|
173
|
-
} else {
|
|
174
|
-
parentToken.attrs.push(['class', value])
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
parentToken.attrs.push([key, value])
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Update child token attrs
|
|
182
|
-
childToken.attrs = remainingAttrs.length > 0 ? remainingAttrs : null
|
|
183
|
-
}
|
|
184
|
-
}
|