@peaceroad/markdown-it-numbering-ul-regarded-as-ol 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -335
- package/debug/dump_listinfos.mjs +65 -0
- package/debug/dump_tokens.mjs +72 -0
- package/index.js +5 -0
- package/package.json +5 -2
- package/src/phase0-description-list.js +654 -654
- package/src/phase1-analyze.js +90 -7
- package/src/phase2-convert.js +1037 -738
- package/src/phase3-attributes.js +260 -244
- package/src/phase5-spans.js +1 -1
- package/src/phase6-attrs-migration.js +1 -1
- package/src/preprocess-literal-lists.js +776 -0
- package/src/types-utility.js +995 -991
package/src/phase2-convert.js
CHANGED
|
@@ -1,744 +1,1043 @@
|
|
|
1
|
-
// Phase 2: Token Conversion
|
|
2
|
-
// Convert bullet_list to ordered_list based on Phase1 analysis, simplify nesting in default mode
|
|
3
|
-
|
|
4
|
-
import { findMatchingClose } from './list-helpers.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Convert tokens based on list information
|
|
8
|
-
* @param {Array} tokens - Token array
|
|
9
|
-
* @param {Array} listInfos - List information analyzed in Phase1
|
|
10
|
-
* @param {Object} opt - Options
|
|
11
|
-
*/
|
|
12
|
-
export function convertLists(tokens, listInfos, opt) {
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
1
|
+
// Phase 2: Token Conversion
|
|
2
|
+
// Convert bullet_list to ordered_list based on Phase1 analysis, simplify nesting in default mode
|
|
3
|
+
|
|
4
|
+
import { findMatchingClose } from './list-helpers.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert tokens based on list information
|
|
8
|
+
* @param {Array} tokens - Token array
|
|
9
|
+
* @param {Array} listInfos - List information analyzed in Phase1
|
|
10
|
+
* @param {Object} opt - Options
|
|
11
|
+
*/
|
|
12
|
+
export function convertLists(tokens, listInfos, opt) {
|
|
13
|
+
const listInfoMap = buildListInfoMap(listInfos)
|
|
14
|
+
// Convert bullet_list to ordered_list
|
|
15
|
+
// listInfos already collected in depth-first order in Phase 1, no sorting needed
|
|
16
|
+
for (const listInfo of listInfos) {
|
|
17
|
+
if (listInfo.shouldConvert) {
|
|
18
|
+
convertBulletToOrdered(tokens, listInfo)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// In default mode (unremoveUlNest=false), simplify ul>li>ol structure
|
|
23
|
+
// Runs after conversion, so already-converted ordered_lists are also processed
|
|
23
24
|
if (!opt.unremoveUlNest) {
|
|
24
25
|
const bulletListInfos = listInfos.filter(info => info.originalType === 'bullet_list_open')
|
|
25
26
|
if (bulletListInfos.length > 0) {
|
|
26
|
-
simplifyNestedBulletLists(tokens, bulletListInfos, opt)
|
|
27
|
+
simplifyNestedBulletLists(tokens, bulletListInfos, opt, listInfoMap)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert bullet_list to ordered_list
|
|
34
|
+
*/
|
|
35
|
+
function convertBulletToOrdered(tokens, listInfo) {
|
|
36
|
+
const { startIndex, endIndex, markerInfo, items } = listInfo
|
|
37
|
+
|
|
38
|
+
// Tokens may have been deleted by simplifyNestedBulletLists,
|
|
39
|
+
// check index validity
|
|
40
|
+
if (startIndex >= tokens.length || endIndex >= tokens.length) {
|
|
41
|
+
// This listInfo points to already deleted list
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert start token
|
|
46
|
+
if (tokens[startIndex].type === 'bullet_list_open') {
|
|
47
|
+
tokens[startIndex].type = 'ordered_list_open'
|
|
48
|
+
tokens[startIndex].tag = 'ol'
|
|
49
|
+
tokens[startIndex]._convertedFromBullet = true
|
|
50
|
+
// Save marker info (used in Phase3)
|
|
51
|
+
tokens[startIndex]._markerInfo = markerInfo
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Convert end token
|
|
55
|
+
if (tokens[endIndex] && tokens[endIndex].type === 'bullet_list_close') {
|
|
56
|
+
tokens[endIndex].type = 'ordered_list_close'
|
|
57
|
+
tokens[endIndex].tag = 'ol'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If item's first paragraph is loose, set _parentIsLoose flag to child lists
|
|
61
|
+
// Only when all following conditions are met:
|
|
62
|
+
// 1. Item has only single paragraph (exclude if multiple paragraphs)
|
|
63
|
+
// 2. Parent item's marker type matches child list's marker type
|
|
64
|
+
// (if different, not propagated as it will be flattened with `-1.` pattern)
|
|
65
|
+
if (items && items.length > 0) {
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
if (item.firstParagraphIsLoose && item.hasNestedList && item.nestedLists && item.markerInfo) {
|
|
68
|
+
// Check paragraph count in item
|
|
69
|
+
// Don't propagate if multiple paragraphs before first nested list
|
|
70
|
+
let paragraphCount = 0
|
|
71
|
+
let reachedNestedList = false
|
|
72
|
+
|
|
73
|
+
for (let i = item.startIndex + 1; i < item.endIndex && !reachedNestedList; i++) {
|
|
74
|
+
const token = tokens[i]
|
|
75
|
+
if (token.type === 'paragraph_open') {
|
|
76
|
+
paragraphCount++
|
|
77
|
+
} else if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
|
|
78
|
+
reachedNestedList = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Propagate to child lists only if single paragraph
|
|
83
|
+
if (paragraphCount === 1) {
|
|
84
|
+
for (const nestedList of item.nestedLists) {
|
|
85
|
+
if (nestedList && nestedList.startIndex < tokens.length) {
|
|
86
|
+
// Compare parent item's marker type with child list's marker type
|
|
87
|
+
// Don't propagate if different (will be flattened)
|
|
88
|
+
const childMarkerInfo = nestedList.items && nestedList.items[0] && nestedList.items[0].markerInfo
|
|
89
|
+
if (!childMarkerInfo || item.markerInfo.markerType !== childMarkerInfo.markerType) {
|
|
90
|
+
continue // Skip if marker types differ
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const nestedListToken = tokens[nestedList.startIndex]
|
|
94
|
+
if (nestedListToken && (nestedListToken.type === 'bullet_list_open' || nestedListToken.type === 'ordered_list_open')) {
|
|
95
|
+
// Set flag directly
|
|
96
|
+
nestedListToken._parentIsLoose = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove marker text from inline tokens
|
|
106
|
+
if (markerInfo && markerInfo.markers) {
|
|
107
|
+
removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove markers from inline token content
|
|
113
|
+
*/
|
|
114
|
+
function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo) {
|
|
115
|
+
const listToken = tokens[startIndex]
|
|
116
|
+
const targetLevel = (listToken.level || 0) + 3 // list_open(0) -> list_item(1) -> paragraph(2) -> inline(3)
|
|
117
|
+
|
|
118
|
+
// Pre-generate all marker regexes (avoid repeated regex creation in loop)
|
|
119
|
+
const markerPatterns = markerInfo.markers.map(marker =>
|
|
120
|
+
marker && marker.marker ? new RegExp(`^${escapeRegExp(marker.marker)}\\s*`) : null
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
let markerIndex = 0
|
|
124
|
+
for (let i = startIndex + 1; i < endIndex && markerIndex < markerPatterns.length; i++) {
|
|
125
|
+
const token = tokens[i]
|
|
126
|
+
|
|
127
|
+
if (token.type === 'inline' && token.content && token.level === targetLevel) {
|
|
128
|
+
const markerPattern = markerPatterns[markerIndex]
|
|
129
|
+
if (markerPattern) {
|
|
130
|
+
// Remove marker and trailing spaces
|
|
131
|
+
const newContent = token.content.replace(markerPattern, '')
|
|
132
|
+
|
|
133
|
+
// Only advance to next marker if actually removed
|
|
134
|
+
if (newContent !== token.content) {
|
|
135
|
+
token.content = newContent
|
|
136
|
+
markerIndex++
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 正規表現の特殊文字をエスケープ
|
|
145
|
+
*/
|
|
146
|
+
function escapeRegExp(string) {
|
|
147
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Simplify nested ul>li>ul and ul>li>ol structures.
|
|
152
|
+
*
|
|
153
|
+
* Pattern 1: bullet_list_open → list_item_open → bullet_list_open → ...
|
|
154
|
+
* Pattern 2: bullet_list_open → list_item_open → ordered_list_open → ... (repeated)
|
|
155
|
+
*
|
|
156
|
+
* When the middle list_item is empty (contains only the inner list),
|
|
157
|
+
* remove the outer ul and the intermediate li.
|
|
158
|
+
* @param {Array} tokens - Token array
|
|
159
|
+
* @param {Array} listInfos - List information analyzed in Phase 1
|
|
160
|
+
* @param {Object} opt - Options
|
|
161
|
+
* @param {Map} listInfoMap - Map of listInfo keyed by startIndex
|
|
162
|
+
*/
|
|
163
|
+
function simplifyNestedBulletLists(tokens, listInfos, opt, listInfoMap = null) {
|
|
164
|
+
let modified = true
|
|
165
|
+
|
|
166
|
+
// ===== Phase 1: Simplify ul>li>ol structure (multiple passes needed) =====
|
|
167
|
+
while (modified) {
|
|
168
|
+
modified = false
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
171
|
+
const token = tokens[i]
|
|
172
|
+
|
|
173
|
+
// Only detect bullet_list_open (ordered_list processing will be done later)
|
|
174
|
+
if (token.type !== 'bullet_list_open') {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check if this bullet_list is all ul>li>ol/ul pattern
|
|
179
|
+
const listCloseIdx = findMatchingClose(tokens, i, 'bullet_list_open', 'bullet_list_close')
|
|
180
|
+
if (listCloseIdx === -1) continue
|
|
181
|
+
|
|
182
|
+
// Check all list_items in bullet_list
|
|
183
|
+
const itemIndices = []
|
|
184
|
+
let totalItems = 0
|
|
185
|
+
let idx = i + 1
|
|
186
|
+
|
|
187
|
+
while (idx < listCloseIdx) {
|
|
188
|
+
if (tokens[idx].type === 'list_item_open') {
|
|
189
|
+
totalItems++
|
|
190
|
+
const itemCloseIdx = findMatchingClose(tokens, idx, 'list_item_open', 'list_item_close')
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
// Find inner list within list_item
|
|
194
|
+
let innerListOpen = -1
|
|
195
|
+
let innerListType = null
|
|
196
|
+
let innerListCloseIdx = -1
|
|
197
|
+
for (let j = idx + 1; j < itemCloseIdx; j++) {
|
|
198
|
+
if (tokens[j].type === 'bullet_list_open' || tokens[j].type === 'ordered_list_open') {
|
|
199
|
+
const candidateType = tokens[j].type
|
|
200
|
+
const candidateClose = findMatchingClose(tokens, j, candidateType, candidateType.replace('_open', '_close'))
|
|
201
|
+
if (tokens[j]._literalList) {
|
|
202
|
+
if (candidateClose === -1) {
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
j = candidateClose
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
innerListOpen = j
|
|
209
|
+
innerListType = candidateType
|
|
210
|
+
innerListCloseIdx = candidateClose
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (innerListOpen !== -1) {
|
|
216
|
+
const innerListCloseType = innerListType === 'bullet_list_open' ? 'bullet_list_close' : 'ordered_list_close'
|
|
217
|
+
if (innerListCloseIdx === -1) {
|
|
218
|
+
innerListCloseIdx = findMatchingClose(tokens, innerListOpen, innerListType, innerListCloseType)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if there's extra content before/after ol (whether it's only ol)
|
|
222
|
+
const beforeContent = innerListOpen - (idx + 1) // Token count from after list_item_open to ol
|
|
223
|
+
const afterContent = itemCloseIdx - (innerListCloseIdx + 1) // Token count from after ol to list_item_close
|
|
224
|
+
const hasExtraContent = beforeContent > 0 || afterContent > 0
|
|
225
|
+
const literalNumber = extractFirstListItemNumber(tokens, innerListOpen, innerListCloseIdx)
|
|
226
|
+
|
|
227
|
+
const innerListInfo = listInfoMap?.get(innerListOpen)
|
|
228
|
+
const isSimpleMarkerParagraph = (() => {
|
|
229
|
+
const precedingCount = innerListOpen - (idx + 1)
|
|
230
|
+
if (precedingCount !== 3) return false
|
|
231
|
+
const paraOpen = tokens[idx + 1]
|
|
232
|
+
const inlineToken = tokens[idx + 2]
|
|
233
|
+
const paraClose = tokens[idx + 3]
|
|
234
|
+
return paraOpen?.type === 'paragraph_open' &&
|
|
235
|
+
inlineToken?.type === 'inline' &&
|
|
236
|
+
paraClose?.type === 'paragraph_close'
|
|
237
|
+
})()
|
|
238
|
+
|
|
239
|
+
itemIndices.push({
|
|
240
|
+
outerItemOpen: idx,
|
|
241
|
+
outerItemClose: itemCloseIdx,
|
|
242
|
+
innerListOpen: innerListOpen,
|
|
243
|
+
innerListClose: innerListCloseIdx,
|
|
244
|
+
innerListType: innerListType,
|
|
245
|
+
hasExtraContent: hasExtraContent,
|
|
246
|
+
extraContentStart: innerListCloseIdx + 1,
|
|
247
|
+
extraContentEnd: itemCloseIdx,
|
|
248
|
+
innerListInfo,
|
|
249
|
+
literalNumber,
|
|
250
|
+
flattenFirstParagraph: isSimpleMarkerParagraph
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
idx = itemCloseIdx + 1
|
|
255
|
+
} else {
|
|
256
|
+
idx++
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if outer ul is convertible (has markers)
|
|
261
|
+
const outerListInfo = listInfos?.find(info => info.startIndex === i && info.shouldConvert)
|
|
262
|
+
const outerListHasMarkers = outerListInfo?.shouldConvert && outerListInfo.markerInfo
|
|
263
|
+
|
|
264
|
+
if (outerListInfo?.shouldConvert) {
|
|
265
|
+
for (const flattenedItem of itemIndices) {
|
|
266
|
+
const closeIdx = flattenedItem.innerListOpen - 1
|
|
267
|
+
const inlineIdx = closeIdx - 1
|
|
268
|
+
const openIdx = closeIdx - 2
|
|
269
|
+
if (openIdx >= flattenedItem.outerItemOpen + 1) {
|
|
270
|
+
const paraOpen = tokens[openIdx]
|
|
271
|
+
const inlineToken = tokens[inlineIdx]
|
|
272
|
+
const paraClose = tokens[closeIdx]
|
|
273
|
+
if (paraOpen?.type === 'paragraph_open' &&
|
|
274
|
+
inlineToken?.type === 'inline' &&
|
|
275
|
+
paraClose?.type === 'paragraph_close') {
|
|
276
|
+
const innerListToken = tokens[flattenedItem.innerListOpen]
|
|
277
|
+
if (innerListToken) {
|
|
278
|
+
innerListToken._convertedFromFlatten = true
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Simplification conditions:
|
|
286
|
+
// 1. All list_items have inner lists of the same type (traditional logic)
|
|
287
|
+
// 2. Outer list has markers and at least one item has inner list (new logic)
|
|
288
|
+
const allItemsHaveInnerList = itemIndices.length > 0 && itemIndices.length === totalItems
|
|
289
|
+
const shouldSimplify = allItemsHaveInnerList || outerListHasMarkers
|
|
290
|
+
|
|
291
|
+
if (shouldSimplify && itemIndices.length > 0) {
|
|
292
|
+
const allSameType = itemIndices.every(item => item.innerListType === itemIndices[0].innerListType)
|
|
293
|
+
const firstInnerType = itemIndices[0].innerListType
|
|
294
|
+
const hasExtraContent = itemIndices.some(item => item.hasExtraContent)
|
|
295
|
+
|
|
296
|
+
// Don't simplify if inner lists are mixed types
|
|
297
|
+
if (!allSameType) {
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Merge marker info from each inner list
|
|
302
|
+
const allMarkers = []
|
|
303
|
+
|
|
304
|
+
// Use outer listInfo marker info if available
|
|
305
|
+
if (outerListInfo && outerListInfo.markerInfo && outerListInfo.markerInfo.markers) {
|
|
306
|
+
allMarkers.push(...outerListInfo.markerInfo.markers)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const item of itemIndices) {
|
|
310
|
+
const innerListToken = tokens[item.innerListOpen]
|
|
311
|
+
|
|
312
|
+
// If went through Phase2 convertBulletToOrdered
|
|
313
|
+
if (innerListToken._literalList) {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (innerListToken._markerInfo && innerListToken._markerInfo.markers) {
|
|
318
|
+
allMarkers.push(...innerListToken._markerInfo.markers)
|
|
319
|
+
}
|
|
320
|
+
// If originally ordered_list, get marker info from start attribute and markup
|
|
321
|
+
else if (!outerListInfo || !outerListInfo.markerInfo) {
|
|
322
|
+
// Only get from inner if outer has no marker info
|
|
323
|
+
// Get start attribute and markup
|
|
324
|
+
const startAttr = innerListToken.attrs?.find(attr => attr[0] === 'start')
|
|
325
|
+
let startNumber = startAttr ? parseInt(startAttr[1], 10) : 1
|
|
326
|
+
const suffix = innerListToken.markup || '.' // markup is suffix
|
|
327
|
+
let itemCount = 0
|
|
328
|
+
|
|
329
|
+
// Get info from list_item_open (number set by markdown-it)
|
|
330
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
331
|
+
if (tokens[j].type === 'list_item_open' && tokens[j].level === innerListToken.level + 1) {
|
|
332
|
+
// Get number from info
|
|
333
|
+
const number = tokens[j].info ? parseInt(tokens[j].info, 10) : startNumber + itemCount
|
|
334
|
+
allMarkers.push({
|
|
335
|
+
type: 'decimal',
|
|
336
|
+
marker: number + suffix,
|
|
337
|
+
number: number,
|
|
338
|
+
originalNumber: number,
|
|
339
|
+
prefix: '',
|
|
340
|
+
suffix: suffix
|
|
341
|
+
})
|
|
342
|
+
itemCount++
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Build new token array
|
|
349
|
+
const newTokens = []
|
|
350
|
+
|
|
351
|
+
// Tokens before bullet_list_open
|
|
352
|
+
for (let j = 0; j < i; j++) {
|
|
353
|
+
newTokens.push(tokens[j])
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Add first inner list open token and save merged marker info
|
|
357
|
+
const firstListToken = tokens[itemIndices[0].innerListOpen]
|
|
358
|
+
if (firstListToken.attrs) {
|
|
359
|
+
const startAttr = firstListToken.attrs.find(attr => attr[0] === 'start')
|
|
360
|
+
if (startAttr) {
|
|
361
|
+
firstListToken._startOverride = startAttr[1]
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// If outer ul is convertible, convert inner list to ordered_list
|
|
366
|
+
// (Don't limit to bullet_list as inner list might already be ordered_list)
|
|
367
|
+
if (outerListInfo && outerListInfo.shouldConvert) {
|
|
368
|
+
if (firstListToken.type === 'bullet_list_open') {
|
|
369
|
+
firstListToken.type = 'ordered_list_open'
|
|
370
|
+
firstListToken.tag = 'ol'
|
|
371
|
+
firstListToken._convertedFromBullet = true
|
|
372
|
+
}
|
|
373
|
+
// If already ordered_list, use as is (already converted)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
newTokens.push(firstListToken)
|
|
377
|
+
|
|
378
|
+
// Merge and save marker info
|
|
379
|
+
if (allMarkers.length > 0) {
|
|
380
|
+
normalizeParentMarkers(allMarkers, itemIndices)
|
|
381
|
+
const firstMarkerType = allMarkers[0]?.type
|
|
382
|
+
const literalNumbers = allMarkers.map(m => (typeof m.originalNumber === 'number' ? m.originalNumber : undefined))
|
|
383
|
+
const hasLiteralNumbers = literalNumbers.length === allMarkers.length &&
|
|
384
|
+
literalNumbers.every(n => typeof n === 'number')
|
|
385
|
+
let allNumbersIdentical = false
|
|
386
|
+
if (hasLiteralNumbers) {
|
|
387
|
+
const firstLiteral = literalNumbers[0]
|
|
388
|
+
if (literalNumbers.every(n => n === firstLiteral) && firstLiteral === 1) {
|
|
389
|
+
allNumbersIdentical = true
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
firstListToken._markerInfo = {
|
|
393
|
+
markers: allMarkers,
|
|
394
|
+
type: firstMarkerType,
|
|
395
|
+
isConsistent: allMarkers.every(m => m.type === firstMarkerType),
|
|
396
|
+
count: allMarkers.length,
|
|
397
|
+
allNumbersIdentical
|
|
398
|
+
}
|
|
399
|
+
const firstNumber = allMarkers[0]?.originalNumber ?? allMarkers[0]?.number
|
|
400
|
+
if (typeof firstNumber === 'number' && firstNumber !== 1) {
|
|
401
|
+
if (!firstListToken.attrs) firstListToken.attrs = []
|
|
402
|
+
const startIndex = firstListToken.attrs.findIndex(attr => attr[0] === 'start')
|
|
403
|
+
if (startIndex >= 0) {
|
|
404
|
+
firstListToken.attrs[startIndex][1] = String(firstNumber)
|
|
405
|
+
} else {
|
|
406
|
+
firstListToken.attrs.push(['start', String(firstNumber)])
|
|
407
|
+
}
|
|
408
|
+
} else if (firstListToken.attrs) {
|
|
409
|
+
const idx = firstListToken.attrs.findIndex(attr => attr[0] === 'start')
|
|
410
|
+
if (idx >= 0) {
|
|
411
|
+
firstListToken.attrs.splice(idx, 1)
|
|
412
|
+
if (firstListToken.attrs.length === 0) firstListToken.attrs = null
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ===== Outer UL (Level 0 after conversion) loose/tight determination =====
|
|
418
|
+
// In flattened pattern (`- 1.` etc), no direct paragraph in outer list_item,
|
|
419
|
+
// so cannot detect with markdown-it's paragraph.hidden
|
|
420
|
+
// Instead, use map info of outer list_item to detect blank lines between items
|
|
421
|
+
let outerUlIsLoose = false
|
|
422
|
+
|
|
423
|
+
// Single item is always tight
|
|
424
|
+
if (itemIndices.length === 1) {
|
|
425
|
+
outerUlIsLoose = false
|
|
426
|
+
} else {
|
|
427
|
+
// For multiple items, check blank lines with map info
|
|
428
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length - 1; itemIdx++) {
|
|
429
|
+
const currentItem = itemIndices[itemIdx]
|
|
430
|
+
const nextItem = itemIndices[itemIdx + 1]
|
|
431
|
+
|
|
432
|
+
// Get currentItem end line: list_item_close map or last token map
|
|
433
|
+
let currentEndLine = null
|
|
434
|
+
// Find list_item_close map (usually null, so use last token instead)
|
|
435
|
+
for (let k = currentItem.outerItemClose - 1; k > currentItem.outerItemOpen; k--) {
|
|
436
|
+
if (tokens[k].map && tokens[k].map[1]) {
|
|
437
|
+
currentEndLine = tokens[k].map[1]
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const nextMap = tokens[nextItem.outerItemOpen].map
|
|
443
|
+
|
|
444
|
+
if (currentEndLine !== null && nextMap) {
|
|
445
|
+
// Compare currentItem end line and nextItem start line
|
|
446
|
+
// If line numbers differ, there's blank line
|
|
447
|
+
const lineGap = nextMap[0] - currentEndLine
|
|
448
|
+
if (lineGap > 0) {
|
|
449
|
+
outerUlIsLoose = true
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ===== Nested list overall loose/tight determination =====
|
|
457
|
+
// Inner list (converted ol) loose/tight determination
|
|
458
|
+
// If parent item is loose (_parentIsLoose flag), or
|
|
459
|
+
// if outer ul is loose, this inner list is also loose
|
|
460
|
+
|
|
461
|
+
// First, check if there are blank lines between converted ol list_items (independent of outerUlIsLoose)
|
|
462
|
+
// This is used for propagation to child lists
|
|
463
|
+
let innerListIsLooseDueToBlankLines = false
|
|
464
|
+
|
|
465
|
+
// In flattened pattern (`- 1.` etc), each outer list_item has one inner ol
|
|
466
|
+
// Check if there are blank lines between list_items in inner ol
|
|
467
|
+
// (Blank lines between outer list_items already checked by outerUlIsLoose)
|
|
468
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
469
|
+
const item = itemIndices[itemIdx]
|
|
470
|
+
|
|
471
|
+
// Collect list_items in inner list
|
|
472
|
+
const innerListItems = []
|
|
473
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
474
|
+
if (tokens[j].type === 'list_item_open' &&
|
|
475
|
+
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
476
|
+
const itemOpen = j
|
|
477
|
+
const itemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
478
|
+
innerListItems.push({ open: itemOpen, close: itemClose })
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check blank lines between list_items in inner list
|
|
483
|
+
if (innerListItems.length > 1) {
|
|
484
|
+
for (let k = 0; k < innerListItems.length - 1; k++) {
|
|
485
|
+
const currentItem = innerListItems[k]
|
|
486
|
+
const nextItem = innerListItems[k + 1]
|
|
487
|
+
|
|
488
|
+
// Get currentItem end line
|
|
489
|
+
let currentEndLine = null
|
|
490
|
+
for (let m = currentItem.close - 1; m > currentItem.open; m--) {
|
|
491
|
+
if (tokens[m].map && tokens[m].map[1]) {
|
|
492
|
+
currentEndLine = tokens[m].map[1]
|
|
493
|
+
break
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const nextMap = tokens[nextItem.open].map
|
|
498
|
+
|
|
499
|
+
if (currentEndLine !== null && nextMap) {
|
|
500
|
+
const lineGap = nextMap[0] - currentEndLine
|
|
501
|
+
if (lineGap > 0) {
|
|
502
|
+
innerListIsLooseDueToBlankLines = true
|
|
503
|
+
break
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (innerListIsLooseDueToBlankLines) break
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Check markdown-it's paragraph.hidden
|
|
512
|
+
// If inner list has paragraph with hidden=false, it's loose
|
|
513
|
+
if (!innerListIsLooseDueToBlankLines) {
|
|
514
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
515
|
+
const item = itemIndices[itemIdx]
|
|
516
|
+
|
|
517
|
+
let innerListItemOpen = -1
|
|
518
|
+
let innerListItemClose = -1
|
|
519
|
+
|
|
520
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
521
|
+
if (tokens[j].type === 'list_item_open' &&
|
|
522
|
+
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
523
|
+
innerListItemOpen = j
|
|
524
|
+
innerListItemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
525
|
+
break
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
|
|
530
|
+
// Check hidden of first paragraph in inner list item
|
|
531
|
+
let nestedListDepth = 0
|
|
532
|
+
for (let j = innerListItemOpen + 1; j < innerListItemClose; j++) {
|
|
533
|
+
const t = tokens[j]
|
|
534
|
+
if (t.type === 'bullet_list_open' || t.type === 'ordered_list_open') {
|
|
535
|
+
nestedListDepth++
|
|
536
|
+
} else if (t.type === 'bullet_list_close' || t.type === 'ordered_list_close') {
|
|
537
|
+
nestedListDepth--
|
|
538
|
+
} else if (nestedListDepth === 0 && t.type === 'paragraph_open') {
|
|
539
|
+
if (t.hidden === false) {
|
|
540
|
+
innerListIsLooseDueToBlankLines = true
|
|
541
|
+
}
|
|
542
|
+
break
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (innerListIsLooseDueToBlankLines) break
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// If outerUlIsLoose, force first paragraph of each inner list item to loose (hidden=false)
|
|
551
|
+
// markdown-it judges tight/loose by original ul structure, but after flattening to ol structure
|
|
552
|
+
// should reflect parent ul's loose state
|
|
553
|
+
// However, exclude case where both parent ul and each inner ol have single item
|
|
554
|
+
// This must be executed AFTER innerListIsLooseDueToBlankLines determination
|
|
555
|
+
if (outerUlIsLoose && !(itemIndices.length === 1)) {
|
|
556
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
557
|
+
const item = itemIndices[itemIdx]
|
|
558
|
+
// Find list_items in inner list
|
|
559
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
560
|
+
if (tokens[j].type === 'list_item_open' &&
|
|
561
|
+
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
562
|
+
// Find first paragraph in list_item
|
|
563
|
+
const listItemOpen = j
|
|
564
|
+
const listItemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
565
|
+
for (let k = listItemOpen + 1; k < listItemClose; k++) {
|
|
566
|
+
if (tokens[k].type === 'paragraph_open' &&
|
|
567
|
+
tokens[k].level === tokens[listItemOpen].level + 1) {
|
|
568
|
+
if (!tokens[k]._literalTight) {
|
|
569
|
+
tokens[k].hidden = false
|
|
570
|
+
}
|
|
571
|
+
break // Only first paragraph
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
break // Only first list_item of inner list
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Track loose state inherited from parent structures (outer lists or ancestors)
|
|
581
|
+
const parentStructureIsLoose = token._parentIsLoose || outerUlIsLoose
|
|
582
|
+
|
|
583
|
+
// ===== Merge each inner list's contents and place extra content appropriately =====
|
|
584
|
+
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
585
|
+
const item = itemIndices[itemIdx]
|
|
586
|
+
|
|
587
|
+
// Count list_items in this inner list (ordered_list)
|
|
588
|
+
let innerListItemCount = 0
|
|
589
|
+
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
590
|
+
if (tokens[j].type === 'list_item_open' &&
|
|
591
|
+
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
592
|
+
innerListItemCount++
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const innerListToken = tokens[item.innerListOpen]
|
|
597
|
+
const listItemRanges = collectListItemRanges(tokens, item.innerListOpen, item.innerListClose)
|
|
598
|
+
if (listItemRanges.length === 0) {
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
const parentRange = listItemRanges[0]
|
|
602
|
+
const childRanges = listItemRanges.slice(1)
|
|
603
|
+
const innerListItemOpen = parentRange.open
|
|
604
|
+
const innerListItemClose = parentRange.close
|
|
605
|
+
|
|
606
|
+
if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
|
|
607
|
+
// Add list_item_open (use original token to preserve info)
|
|
608
|
+
// Note: value attribute is added in Phase3 (not during simplification)
|
|
609
|
+
newTokens.push(tokens[innerListItemOpen])
|
|
610
|
+
|
|
611
|
+
// This item's loose/tight determination
|
|
612
|
+
// If entire list is loose, this item is also loose
|
|
613
|
+
// Determination when extraContent has block elements:
|
|
614
|
+
// - extraContent has paragraph(hidden=false) → loose
|
|
615
|
+
// - Has heading(heading_open) or HTML block(html_block) → loose
|
|
616
|
+
// - markdown-it sets paragraph.hidden=true → maintain tight
|
|
617
|
+
let hasBlockElement = false
|
|
618
|
+
if (item.hasExtraContent) {
|
|
619
|
+
for (let k = item.extraContentStart; k < item.extraContentEnd; k++) {
|
|
620
|
+
const tokenType = tokens[k].type
|
|
621
|
+
// Detect block elements:
|
|
622
|
+
// - paragraph(hidden=false): multiple paragraphs
|
|
623
|
+
// - heading_open: heading
|
|
624
|
+
// - html_block: HTML block
|
|
625
|
+
if ((tokenType === 'paragraph_open' && !tokens[k].hidden) ||
|
|
626
|
+
tokenType === 'heading_open' ||
|
|
627
|
+
tokenType === 'html_block') {
|
|
628
|
+
hasBlockElement = true
|
|
629
|
+
break
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Whether this item's first paragraph is loose
|
|
635
|
+
// is obtained from Phase1-analyzed listInfo.items[itemIdx]
|
|
636
|
+
const listItem = outerListInfo?.items?.[itemIdx]
|
|
637
|
+
const firstParagraphIsLoose = listItem?.firstParagraphIsLoose || false
|
|
638
|
+
|
|
639
|
+
// Make loose when parent structure is loose or this item has extra block elements
|
|
640
|
+
// - However, consider cases with multiple paragraphs in item (firstParagraphIsLoose) or
|
|
641
|
+
// block elements (hasBlockElement)
|
|
642
|
+
//
|
|
643
|
+
// Single item determination in flattened pattern (`- 1.` etc):
|
|
644
|
+
// - Parent ul single item AND each inner ol single item → exclude outerUlIsLoose
|
|
645
|
+
// - If parent ul has multiple items, consider outerUlIsLoose (reflect parent's blank lines)
|
|
646
|
+
let structuralLoose = parentStructureIsLoose || firstParagraphIsLoose
|
|
647
|
+
if (itemIndices.length === 1 && innerListItemCount === 1) {
|
|
648
|
+
// Exclude outerUlIsLoose if both parent ul and inner ol only have a single item
|
|
649
|
+
structuralLoose = (token._parentIsLoose || firstParagraphIsLoose)
|
|
650
|
+
}
|
|
651
|
+
const shouldBeLoose = structuralLoose || hasBlockElement
|
|
652
|
+
|
|
653
|
+
// Determine whether to propagate _parentIsLoose flag to child lists
|
|
654
|
+
// - innerListIsLooseDueToBlankLines: blank lines between converted ol list_items
|
|
655
|
+
// - token._parentIsLoose: already propagated from parent
|
|
656
|
+
// - firstParagraphIsLoose: blank line after parent item's first paragraph (same marker type only)
|
|
657
|
+
// Note: hasExtraContent makes parent item loose but doesn't affect child lists
|
|
658
|
+
// Note: outerUlIsLoose excluded (Test 25: handle case where parent is loose but child is tight)
|
|
659
|
+
const shouldPropagateLooseToChildren = innerListIsLooseDueToBlankLines || token._parentIsLoose || firstParagraphIsLoose || false
|
|
660
|
+
|
|
661
|
+
// Copy tokens in inner list item (exclude nested list paragraphs)
|
|
662
|
+
let nestedListDepth = 0
|
|
663
|
+
let firstParagraphInItem = true // Track first paragraph in each list_item
|
|
664
|
+
let shouldFlattenParagraph = !!item.flattenFirstParagraph
|
|
665
|
+
for (let j = innerListItemOpen + 1; j < innerListItemClose; j++) {
|
|
666
|
+
const tokenToPush = tokens[j]
|
|
667
|
+
|
|
668
|
+
// Track nested list depth
|
|
669
|
+
if (tokenToPush.type === 'bullet_list_open' || tokenToPush.type === 'ordered_list_open') {
|
|
670
|
+
nestedListDepth++
|
|
671
|
+
// If parent list is loose (due to blank lines), set _parentIsLoose flag to child lists
|
|
672
|
+
// Don't set if only outerUlIsLoose
|
|
673
|
+
// Propagate to all nest levels (support level >= 2)
|
|
674
|
+
if (shouldPropagateLooseToChildren) {
|
|
675
|
+
// Set _parentIsLoose flag (optimize with direct property assignment)
|
|
676
|
+
tokenToPush._parentIsLoose = true
|
|
677
|
+
newTokens.push(tokenToPush)
|
|
678
|
+
} else {
|
|
679
|
+
newTokens.push(tokenToPush)
|
|
680
|
+
}
|
|
681
|
+
} else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
|
|
682
|
+
nestedListDepth--
|
|
683
|
+
newTokens.push(tokenToPush)
|
|
684
|
+
} else if (nestedListDepth === 0 && (tokenToPush.type === 'paragraph_open' || tokenToPush.type === 'paragraph_close')) {
|
|
685
|
+
// Update paragraph_open/close hidden state (only outside nested lists)
|
|
686
|
+
|
|
687
|
+
if (tokenToPush.type === 'paragraph_open') {
|
|
688
|
+
// Process first paragraph
|
|
689
|
+
if (firstParagraphInItem) {
|
|
690
|
+
// If tight list (shouldBeLoose = false), hide first paragraph
|
|
691
|
+
if (shouldFlattenParagraph) {
|
|
692
|
+
tokenToPush.hidden = true
|
|
693
|
+
tokenToPush._literalTight = true
|
|
694
|
+
shouldFlattenParagraph = false
|
|
695
|
+
} else if (!tokenToPush._literalTight) {
|
|
696
|
+
tokenToPush.hidden = !shouldBeLoose
|
|
697
|
+
}
|
|
698
|
+
firstParagraphInItem = false
|
|
699
|
+
} else {
|
|
700
|
+
// Always show second and subsequent paragraphs
|
|
701
|
+
if (!tokenToPush._literalTight) {
|
|
702
|
+
tokenToPush.hidden = false
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
// Match paragraph_close hidden state to corresponding paragraph_open
|
|
707
|
+
// Reference preceding paragraph_open's hidden state
|
|
708
|
+
const prevToken = newTokens[newTokens.length - 2] // Skip preceding inline, get paragraph_open before it
|
|
709
|
+
if (prevToken && prevToken.type === 'paragraph_open') {
|
|
710
|
+
tokenToPush.hidden = prevToken.hidden
|
|
711
|
+
} else {
|
|
712
|
+
// Fallback: judge based on shouldBeLoose
|
|
713
|
+
tokenToPush.hidden = !shouldBeLoose && firstParagraphInItem
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
newTokens.push(tokenToPush)
|
|
718
|
+
} else {
|
|
719
|
+
newTokens.push(tokenToPush)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Add extra content (content in ul>li, outside ol) to this list_item
|
|
724
|
+
if (item.hasExtraContent) {
|
|
725
|
+
// Adjust level: ul list_item's level → ol list_item's level
|
|
726
|
+
const levelDiff = tokens[innerListItemOpen].level - tokens[item.outerItemOpen].level
|
|
727
|
+
|
|
728
|
+
// Track nested lists in extraContent
|
|
729
|
+
let extraNestedListDepth = 0
|
|
730
|
+
for (let k = item.extraContentStart; k < item.extraContentEnd; k++) {
|
|
731
|
+
const tokenToPush = tokens[k]
|
|
732
|
+
const originalLevel = tokenToPush.level
|
|
733
|
+
tokenToPush.level = originalLevel + levelDiff
|
|
734
|
+
|
|
735
|
+
// Track nested list depth
|
|
736
|
+
if (tokenToPush.type === 'bullet_list_open' || tokenToPush.type === 'ordered_list_open') {
|
|
737
|
+
extraNestedListDepth++
|
|
738
|
+
// If parent item is loose, set _parentIsLoose flag to child lists
|
|
739
|
+
// Don't set if only outerUlIsLoose
|
|
740
|
+
if (shouldPropagateLooseToChildren && extraNestedListDepth === 1) {
|
|
741
|
+
tokenToPush._parentIsLoose = true
|
|
742
|
+
}
|
|
743
|
+
} else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
|
|
744
|
+
extraNestedListDepth--
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// If shouldBeLoose, set paragraph hidden to false (only outside nested lists)
|
|
748
|
+
if (shouldBeLoose && extraNestedListDepth === 0 && (tokenToPush.type === 'paragraph_open' || tokenToPush.type === 'paragraph_close')) {
|
|
749
|
+
if (!tokenToPush._literalTight) {
|
|
750
|
+
tokenToPush.hidden = false
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
newTokens.push(tokenToPush)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (childRanges.length > 0) {
|
|
759
|
+
const parentLevel = tokens[innerListItemOpen].level || 0
|
|
760
|
+
const targetNestedLevel = parentLevel + 1
|
|
761
|
+
const originalListLevel = tokens[item.innerListOpen].level || 0
|
|
762
|
+
const levelShift = targetNestedLevel - originalListLevel
|
|
763
|
+
const markerStartIndex = childRanges[0].markerIndex
|
|
764
|
+
const nestedTokens = buildNestedListTokens(
|
|
765
|
+
tokens,
|
|
766
|
+
childRanges,
|
|
767
|
+
item.innerListOpen,
|
|
768
|
+
item.innerListClose,
|
|
769
|
+
levelShift,
|
|
770
|
+
markerStartIndex,
|
|
771
|
+
item.innerListInfo
|
|
772
|
+
)
|
|
773
|
+
for (const nestedToken of nestedTokens) {
|
|
774
|
+
newTokens.push(nestedToken)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Add list_item_close
|
|
779
|
+
newTokens.push(tokens[innerListItemClose])
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Add last inner list's close token
|
|
784
|
+
const lastItem = itemIndices[itemIndices.length - 1]
|
|
785
|
+
const lastListCloseToken = tokens[lastItem.innerListClose]
|
|
786
|
+
|
|
787
|
+
// If outer ul is convertible, also convert close token
|
|
788
|
+
if (outerListInfo && outerListInfo.shouldConvert) {
|
|
789
|
+
if (lastListCloseToken.type === 'bullet_list_close') {
|
|
790
|
+
lastListCloseToken.type = 'ordered_list_close'
|
|
791
|
+
lastListCloseToken.tag = 'ol'
|
|
792
|
+
}
|
|
793
|
+
// If already ordered_list_close, use as is
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
newTokens.push(lastListCloseToken)
|
|
797
|
+
|
|
798
|
+
// Tokens after bullet_list_close
|
|
799
|
+
for (let j = listCloseIdx + 1; j < tokens.length; j++) {
|
|
800
|
+
newTokens.push(tokens[j])
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Replace token array
|
|
804
|
+
tokens.length = 0
|
|
805
|
+
tokens.push(...newTokens)
|
|
806
|
+
|
|
807
|
+
// Remove markers from merged list
|
|
808
|
+
if (firstListToken._markerInfo && firstListToken._markerInfo.markers) {
|
|
809
|
+
let listStartIdx = 0
|
|
810
|
+
while (listStartIdx < tokens.length && tokens[listStartIdx] !== firstListToken) {
|
|
811
|
+
listStartIdx++
|
|
812
|
+
}
|
|
813
|
+
if (listStartIdx < tokens.length) {
|
|
814
|
+
const listEndIdx = findMatchingClose(tokens, listStartIdx,
|
|
815
|
+
firstListToken.type,
|
|
816
|
+
firstListToken.type.replace('_open', '_close'), false)
|
|
817
|
+
if (listEndIdx !== -1) {
|
|
818
|
+
removeMarkersFromContent(tokens, listStartIdx, listEndIdx, firstListToken._markerInfo, null)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
modified = true
|
|
824
|
+
break
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ===== Propagate ordered_list's _parentIsLoose flag to child lists =====
|
|
830
|
+
// Check all ordered_list_open, and if _parentIsLoose flag exists,
|
|
831
|
+
// propagate to child lists
|
|
832
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
833
|
+
const token = tokens[i]
|
|
834
|
+
|
|
835
|
+
if ((token.type === 'ordered_list_open' || token.type === 'bullet_list_open') && token._parentIsLoose) {
|
|
836
|
+
// Find this list's close token
|
|
837
|
+
const listCloseIdx = findMatchingClose(tokens, i, token.type, token.type.replace('_open', '_close'))
|
|
838
|
+
if (listCloseIdx === -1) continue
|
|
839
|
+
|
|
840
|
+
// Search list_items in this list and set _parentIsLoose flag to child lists in those list_items
|
|
841
|
+
for (let j = i + 1; j < listCloseIdx; j++) {
|
|
842
|
+
if (tokens[j].type === 'list_item_open' && tokens[j].level === token.level + 1) {
|
|
843
|
+
const itemCloseIdx = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
844
|
+
|
|
845
|
+
// Search child lists in this list_item
|
|
846
|
+
for (let k = j + 1; k < itemCloseIdx; k++) {
|
|
847
|
+
if ((tokens[k].type === 'bullet_list_open' || tokens[k].type === 'ordered_list_open') &&
|
|
848
|
+
tokens[k].level === token.level + 2) {
|
|
849
|
+
// Set _parentIsLoose flag to child list
|
|
850
|
+
tokens[k]._parentIsLoose = true
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
j = itemCloseIdx // Jump to next list_item
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildListInfoMap(listInfos) {
|
|
862
|
+
const map = new Map()
|
|
863
|
+
if (!Array.isArray(listInfos)) {
|
|
864
|
+
return map
|
|
865
|
+
}
|
|
866
|
+
for (const info of listInfos) {
|
|
867
|
+
if (info && typeof info.startIndex === 'number') {
|
|
868
|
+
map.set(info.startIndex, info)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return map
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function collectListItemRanges(tokens, listOpenIdx, listCloseIdx) {
|
|
875
|
+
const ranges = []
|
|
876
|
+
if (listOpenIdx === -1 || listCloseIdx === -1 || listCloseIdx <= listOpenIdx) {
|
|
877
|
+
return ranges
|
|
878
|
+
}
|
|
879
|
+
const baseLevel = tokens[listOpenIdx]?.level ?? 0
|
|
880
|
+
let markerIndex = 0
|
|
881
|
+
for (let i = listOpenIdx + 1; i < listCloseIdx; i++) {
|
|
882
|
+
const token = tokens[i]
|
|
883
|
+
if (token.type === 'list_item_open' && token.level === baseLevel + 1) {
|
|
884
|
+
const closeIdx = findMatchingClose(tokens, i, 'list_item_open', 'list_item_close')
|
|
885
|
+
if (closeIdx === -1) {
|
|
886
|
+
break
|
|
887
|
+
}
|
|
888
|
+
ranges.push({ open: i, close: closeIdx, markerIndex })
|
|
889
|
+
markerIndex++
|
|
890
|
+
i = closeIdx
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return ranges
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function buildNestedListTokens(tokens, childRanges, innerListOpenIdx, innerListCloseIdx, levelShift, markerStartIndex, innerListInfo) {
|
|
897
|
+
if (!Array.isArray(childRanges) || childRanges.length === 0) {
|
|
898
|
+
return []
|
|
899
|
+
}
|
|
900
|
+
const nestedTokens = []
|
|
901
|
+
const nestedOpen = cloneToken(tokens[innerListOpenIdx], { levelShift })
|
|
902
|
+
const markerInfoSlice = createMarkerInfoSlice(innerListInfo?.markerInfo, markerStartIndex)
|
|
903
|
+
if (markerInfoSlice) {
|
|
904
|
+
nestedOpen._markerInfo = markerInfoSlice
|
|
905
|
+
const firstMarker = markerInfoSlice.markers?.[0]
|
|
906
|
+
const firstNumber = firstMarker ? (firstMarker.originalNumber ?? firstMarker.number) : undefined
|
|
907
|
+
if (typeof firstNumber === 'number') {
|
|
908
|
+
nestedOpen._startOverride = firstNumber
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
nestedTokens.push(nestedOpen)
|
|
912
|
+
for (const range of childRanges) {
|
|
913
|
+
for (let i = range.open; i <= range.close; i++) {
|
|
914
|
+
nestedTokens.push(cloneToken(tokens[i], { levelShift, deep: true }))
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const nestedClose = cloneToken(tokens[innerListCloseIdx], { levelShift })
|
|
918
|
+
nestedTokens.push(nestedClose)
|
|
919
|
+
return nestedTokens
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function cloneToken(token, options = {}) {
|
|
923
|
+
const { levelShift = 0, deep = false } = options
|
|
924
|
+
const TokenClass = token.constructor
|
|
925
|
+
const cloned = new TokenClass(token.type, token.tag, token.nesting)
|
|
926
|
+
cloned.attrs = token.attrs ? token.attrs.map(([name, value]) => [name, value]) : null
|
|
927
|
+
cloned.map = token.map ? [...token.map] : null
|
|
928
|
+
cloned.level = (token.level || 0) + levelShift
|
|
929
|
+
cloned.content = token.content
|
|
930
|
+
cloned.markup = token.markup
|
|
931
|
+
cloned.info = token.info
|
|
932
|
+
cloned.meta = token.meta ? { ...token.meta } : null
|
|
933
|
+
cloned.block = token.block
|
|
934
|
+
cloned.hidden = token.hidden
|
|
935
|
+
if (Array.isArray(token.children)) {
|
|
936
|
+
cloned.children = token.children.length > 0
|
|
937
|
+
? token.children.map(child => cloneToken(child, { levelShift, deep: true }))
|
|
938
|
+
: []
|
|
939
|
+
} else {
|
|
940
|
+
cloned.children = token.children ?? null
|
|
941
|
+
}
|
|
942
|
+
if (token._markerInfo) {
|
|
943
|
+
cloned._markerInfo = cloneMarkerInfo(token._markerInfo)
|
|
944
|
+
}
|
|
945
|
+
if (token._convertedFromBullet) {
|
|
946
|
+
cloned._convertedFromBullet = token._convertedFromBullet
|
|
947
|
+
}
|
|
948
|
+
if (token._parentIsLoose) {
|
|
949
|
+
cloned._parentIsLoose = token._parentIsLoose
|
|
950
|
+
}
|
|
951
|
+
if (token._startOverride !== undefined) {
|
|
952
|
+
cloned._startOverride = token._startOverride
|
|
953
|
+
}
|
|
954
|
+
return cloned
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function cloneMarkerInfo(markerInfo) {
|
|
958
|
+
if (!markerInfo) {
|
|
959
|
+
return null
|
|
960
|
+
}
|
|
961
|
+
const cloned = { ...markerInfo }
|
|
962
|
+
cloned.markers = Array.isArray(markerInfo.markers)
|
|
963
|
+
? markerInfo.markers.map(marker => ({ ...marker }))
|
|
964
|
+
: null
|
|
965
|
+
return cloned
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function createMarkerInfoSlice(markerInfo, startIndex) {
|
|
969
|
+
if (!markerInfo || !Array.isArray(markerInfo.markers)) {
|
|
970
|
+
return null
|
|
971
|
+
}
|
|
972
|
+
const markers = markerInfo.markers.slice(startIndex)
|
|
973
|
+
if (markers.length === 0) {
|
|
974
|
+
return null
|
|
975
|
+
}
|
|
976
|
+
const clonedMarkers = markers.map(marker => ({ ...marker }))
|
|
977
|
+
const literalNumbers = clonedMarkers.map(m => (typeof m.originalNumber === 'number' ? m.originalNumber : m.number))
|
|
978
|
+
let allNumbersIdentical = false
|
|
979
|
+
if (literalNumbers.length === clonedMarkers.length && literalNumbers.every(n => typeof n === 'number')) {
|
|
980
|
+
const firstNumber = literalNumbers[0]
|
|
981
|
+
if (literalNumbers.every(n => n === firstNumber) && firstNumber === 1) {
|
|
982
|
+
allNumbersIdentical = true
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const slicedInfo = { ...markerInfo }
|
|
986
|
+
slicedInfo.markers = clonedMarkers
|
|
987
|
+
slicedInfo.count = clonedMarkers.length
|
|
988
|
+
slicedInfo.allNumbersIdentical = allNumbersIdentical
|
|
989
|
+
return slicedInfo
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function extractFirstListItemNumber(tokens, listOpenIdx, listCloseIdx) {
|
|
993
|
+
if (listOpenIdx === -1 || listCloseIdx === -1) {
|
|
994
|
+
return null
|
|
995
|
+
}
|
|
996
|
+
const baseLevel = tokens[listOpenIdx]?.level ?? 0
|
|
997
|
+
for (let i = listOpenIdx + 1; i < listCloseIdx; i++) {
|
|
998
|
+
const token = tokens[i]
|
|
999
|
+
if (token.type === 'list_item_open' && token.level === baseLevel + 1) {
|
|
1000
|
+
if (token.info) {
|
|
1001
|
+
const parsed = parseInt(token.info, 10)
|
|
1002
|
+
if (!Number.isNaN(parsed)) {
|
|
1003
|
+
return parsed
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
break
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function normalizeParentMarkers(allMarkers, itemIndices) {
|
|
1013
|
+
if (!Array.isArray(itemIndices) || itemIndices.length === 0) {
|
|
1014
|
+
return
|
|
1015
|
+
}
|
|
1016
|
+
const literalNumbers = []
|
|
1017
|
+
for (const item of itemIndices) {
|
|
1018
|
+
literalNumbers.push(typeof item.literalNumber === 'number' ? item.literalNumber : null)
|
|
1019
|
+
}
|
|
1020
|
+
if (literalNumbers.some(n => n === null)) {
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
const firstNumber = literalNumbers[0]
|
|
1024
|
+
const allSame = literalNumbers.every(n => n === firstNumber)
|
|
1025
|
+
if (!allSame) {
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
if (firstNumber !== 1) {
|
|
1029
|
+
return
|
|
1030
|
+
}
|
|
1031
|
+
for (let i = 0; i < itemIndices.length; i++) {
|
|
1032
|
+
const marker = allMarkers[i]
|
|
1033
|
+
if (!marker) {
|
|
1034
|
+
continue
|
|
27
1035
|
}
|
|
1036
|
+
const adjustedNumber = (firstNumber ?? 1) + i
|
|
1037
|
+
marker.originalNumber = literalNumbers[i]
|
|
1038
|
+
marker.number = adjustedNumber
|
|
1039
|
+
const prefix = marker.prefix || ''
|
|
1040
|
+
const suffix = marker.suffix || ''
|
|
1041
|
+
marker.marker = `${prefix}${adjustedNumber}${suffix}`
|
|
28
1042
|
}
|
|
29
1043
|
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Convert bullet_list to ordered_list
|
|
33
|
-
*/
|
|
34
|
-
function convertBulletToOrdered(tokens, listInfo) {
|
|
35
|
-
const { startIndex, endIndex, markerInfo, items } = listInfo
|
|
36
|
-
|
|
37
|
-
// Tokens may have been deleted by simplifyNestedBulletLists,
|
|
38
|
-
// check index validity
|
|
39
|
-
if (startIndex >= tokens.length || endIndex >= tokens.length) {
|
|
40
|
-
// This listInfo points to already deleted list
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Convert start token
|
|
45
|
-
if (tokens[startIndex].type === 'bullet_list_open') {
|
|
46
|
-
tokens[startIndex].type = 'ordered_list_open'
|
|
47
|
-
tokens[startIndex].tag = 'ol'
|
|
48
|
-
tokens[startIndex]._convertedFromBullet = true
|
|
49
|
-
// Save marker info (used in Phase3)
|
|
50
|
-
tokens[startIndex]._markerInfo = markerInfo
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Convert end token
|
|
54
|
-
if (tokens[endIndex] && tokens[endIndex].type === 'bullet_list_close') {
|
|
55
|
-
tokens[endIndex].type = 'ordered_list_close'
|
|
56
|
-
tokens[endIndex].tag = 'ol'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// If item's first paragraph is loose, set _parentIsLoose flag to child lists
|
|
60
|
-
// Only when all following conditions are met:
|
|
61
|
-
// 1. Item has only single paragraph (exclude if multiple paragraphs)
|
|
62
|
-
// 2. Parent item's marker type matches child list's marker type
|
|
63
|
-
// (if different, not propagated as it will be flattened with `-1.` pattern)
|
|
64
|
-
if (items && items.length > 0) {
|
|
65
|
-
for (const item of items) {
|
|
66
|
-
if (item.firstParagraphIsLoose && item.hasNestedList && item.nestedLists && item.markerInfo) {
|
|
67
|
-
// Check paragraph count in item
|
|
68
|
-
// Don't propagate if multiple paragraphs before first nested list
|
|
69
|
-
let paragraphCount = 0
|
|
70
|
-
let reachedNestedList = false
|
|
71
|
-
|
|
72
|
-
for (let i = item.startIndex + 1; i < item.endIndex && !reachedNestedList; i++) {
|
|
73
|
-
const token = tokens[i]
|
|
74
|
-
if (token.type === 'paragraph_open') {
|
|
75
|
-
paragraphCount++
|
|
76
|
-
} else if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
|
|
77
|
-
reachedNestedList = true
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Propagate to child lists only if single paragraph
|
|
82
|
-
if (paragraphCount === 1) {
|
|
83
|
-
for (const nestedList of item.nestedLists) {
|
|
84
|
-
if (nestedList && nestedList.startIndex < tokens.length) {
|
|
85
|
-
// Compare parent item's marker type with child list's marker type
|
|
86
|
-
// Don't propagate if different (will be flattened)
|
|
87
|
-
const childMarkerInfo = nestedList.items && nestedList.items[0] && nestedList.items[0].markerInfo
|
|
88
|
-
if (!childMarkerInfo || item.markerInfo.markerType !== childMarkerInfo.markerType) {
|
|
89
|
-
continue // Skip if marker types differ
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const nestedListToken = tokens[nestedList.startIndex]
|
|
93
|
-
if (nestedListToken && (nestedListToken.type === 'bullet_list_open' || nestedListToken.type === 'ordered_list_open')) {
|
|
94
|
-
// Set flag directly
|
|
95
|
-
nestedListToken._parentIsLoose = true
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Remove marker text from inline tokens
|
|
105
|
-
if (markerInfo && markerInfo.markers) {
|
|
106
|
-
removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Remove markers from inline token content
|
|
112
|
-
*/
|
|
113
|
-
function removeMarkersFromContent(tokens, startIndex, endIndex, markerInfo, listInfo) {
|
|
114
|
-
const listToken = tokens[startIndex]
|
|
115
|
-
const targetLevel = (listToken.level || 0) + 3 // list_open(0) -> list_item(1) -> paragraph(2) -> inline(3)
|
|
116
|
-
|
|
117
|
-
// Pre-generate all marker regexes (avoid repeated regex creation in loop)
|
|
118
|
-
const markerPatterns = markerInfo.markers.map(marker =>
|
|
119
|
-
marker && marker.marker ? new RegExp(`^${escapeRegExp(marker.marker)}\\s*`) : null
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
let markerIndex = 0
|
|
123
|
-
for (let i = startIndex + 1; i < endIndex && markerIndex < markerPatterns.length; i++) {
|
|
124
|
-
const token = tokens[i]
|
|
125
|
-
|
|
126
|
-
if (token.type === 'inline' && token.content && token.level === targetLevel) {
|
|
127
|
-
const markerPattern = markerPatterns[markerIndex]
|
|
128
|
-
if (markerPattern) {
|
|
129
|
-
// Remove marker and trailing spaces
|
|
130
|
-
const newContent = token.content.replace(markerPattern, '')
|
|
131
|
-
|
|
132
|
-
// Only advance to next marker if actually removed
|
|
133
|
-
if (newContent !== token.content) {
|
|
134
|
-
token.content = newContent
|
|
135
|
-
markerIndex++
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* 正規表現の特殊文字をエスケープ
|
|
144
|
-
*/
|
|
145
|
-
function escapeRegExp(string) {
|
|
146
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Simplify nested ul>li>ul and ul>li>ol structures.
|
|
151
|
-
*
|
|
152
|
-
* Pattern 1: bullet_list_open → list_item_open → bullet_list_open → ...
|
|
153
|
-
* Pattern 2: bullet_list_open → list_item_open → ordered_list_open → ... (repeated)
|
|
154
|
-
*
|
|
155
|
-
* When the middle list_item is empty (contains only the inner list),
|
|
156
|
-
* remove the outer ul and the intermediate li.
|
|
157
|
-
* @param {Array} tokens - Token array
|
|
158
|
-
* @param {Array} listInfos - List information analyzed in Phase 1
|
|
159
|
-
* @param {Object} opt - Options
|
|
160
|
-
* @param {Map} listInfoMap - Map of listInfo keyed by startIndex
|
|
161
|
-
*/
|
|
162
|
-
function simplifyNestedBulletLists(tokens, listInfos, opt, listInfoMap = null) {
|
|
163
|
-
let modified = true
|
|
164
|
-
|
|
165
|
-
// ===== Phase 1: Simplify ul>li>ol structure (multiple passes needed) =====
|
|
166
|
-
while (modified) {
|
|
167
|
-
modified = false
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
170
|
-
const token = tokens[i]
|
|
171
|
-
|
|
172
|
-
// Only detect bullet_list_open (ordered_list processing will be done later)
|
|
173
|
-
if (token.type !== 'bullet_list_open') {
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check if this bullet_list is all ul>li>ol/ul pattern
|
|
178
|
-
const listCloseIdx = findMatchingClose(tokens, i, 'bullet_list_open', 'bullet_list_close')
|
|
179
|
-
if (listCloseIdx === -1) continue
|
|
180
|
-
|
|
181
|
-
// Check all list_items in bullet_list
|
|
182
|
-
const itemIndices = []
|
|
183
|
-
let totalItems = 0
|
|
184
|
-
let idx = i + 1
|
|
185
|
-
|
|
186
|
-
while (idx < listCloseIdx) {
|
|
187
|
-
if (tokens[idx].type === 'list_item_open') {
|
|
188
|
-
totalItems++
|
|
189
|
-
const itemCloseIdx = findMatchingClose(tokens, idx, 'list_item_open', 'list_item_close')
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Find inner list within list_item
|
|
193
|
-
let innerListOpen = -1
|
|
194
|
-
let innerListType = null
|
|
195
|
-
for (let j = idx + 1; j < itemCloseIdx; j++) {
|
|
196
|
-
if (tokens[j].type === 'bullet_list_open' || tokens[j].type === 'ordered_list_open') {
|
|
197
|
-
innerListOpen = j
|
|
198
|
-
innerListType = tokens[j].type
|
|
199
|
-
break
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (innerListOpen !== -1) {
|
|
204
|
-
const innerListCloseType = innerListType === 'bullet_list_open' ? 'bullet_list_close' : 'ordered_list_close'
|
|
205
|
-
const innerListCloseIdx = findMatchingClose(tokens, innerListOpen, innerListType, innerListCloseType)
|
|
206
|
-
|
|
207
|
-
// Check if there's extra content before/after ol (whether it's only ol)
|
|
208
|
-
const beforeContent = innerListOpen - (idx + 1) // Token count from after list_item_open to ol
|
|
209
|
-
const afterContent = itemCloseIdx - (innerListCloseIdx + 1) // Token count from after ol to list_item_close
|
|
210
|
-
const hasExtraContent = beforeContent > 0 || afterContent > 0
|
|
211
|
-
|
|
212
|
-
itemIndices.push({
|
|
213
|
-
outerItemOpen: idx,
|
|
214
|
-
outerItemClose: itemCloseIdx,
|
|
215
|
-
innerListOpen: innerListOpen,
|
|
216
|
-
innerListClose: innerListCloseIdx,
|
|
217
|
-
innerListType: innerListType,
|
|
218
|
-
hasExtraContent: hasExtraContent,
|
|
219
|
-
extraContentStart: innerListCloseIdx + 1,
|
|
220
|
-
extraContentEnd: itemCloseIdx
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
idx = itemCloseIdx + 1
|
|
225
|
-
} else {
|
|
226
|
-
idx++
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Check if outer ul is convertible (has markers)
|
|
231
|
-
const outerListInfo = listInfos?.find(info => info.startIndex === i && info.shouldConvert)
|
|
232
|
-
const outerListHasMarkers = outerListInfo?.shouldConvert && outerListInfo.markerInfo
|
|
233
|
-
|
|
234
|
-
// Simplification conditions:
|
|
235
|
-
// 1. All list_items have inner lists of the same type (traditional logic)
|
|
236
|
-
// 2. Outer list has markers and at least one item has inner list (new logic)
|
|
237
|
-
const allItemsHaveInnerList = itemIndices.length > 0 && itemIndices.length === totalItems
|
|
238
|
-
const shouldSimplify = allItemsHaveInnerList || outerListHasMarkers
|
|
239
|
-
|
|
240
|
-
if (shouldSimplify && itemIndices.length > 0) {
|
|
241
|
-
const allSameType = itemIndices.every(item => item.innerListType === itemIndices[0].innerListType)
|
|
242
|
-
const firstInnerType = itemIndices[0].innerListType
|
|
243
|
-
const hasExtraContent = itemIndices.some(item => item.hasExtraContent)
|
|
244
|
-
|
|
245
|
-
// Don't simplify if inner lists are mixed types
|
|
246
|
-
if (!allSameType) {
|
|
247
|
-
continue
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Merge marker info from each inner list
|
|
251
|
-
const allMarkers = []
|
|
252
|
-
|
|
253
|
-
// Use outer listInfo marker info if available
|
|
254
|
-
if (outerListInfo && outerListInfo.markerInfo && outerListInfo.markerInfo.markers) {
|
|
255
|
-
allMarkers.push(...outerListInfo.markerInfo.markers)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
for (const item of itemIndices) {
|
|
259
|
-
const innerListToken = tokens[item.innerListOpen]
|
|
260
|
-
|
|
261
|
-
// If went through Phase2 convertBulletToOrdered
|
|
262
|
-
if (innerListToken._markerInfo && innerListToken._markerInfo.markers) {
|
|
263
|
-
allMarkers.push(...innerListToken._markerInfo.markers)
|
|
264
|
-
}
|
|
265
|
-
// If originally ordered_list, get marker info from start attribute and markup
|
|
266
|
-
else if (!outerListInfo || !outerListInfo.markerInfo) {
|
|
267
|
-
// Only get from inner if outer has no marker info
|
|
268
|
-
// Get start attribute and markup
|
|
269
|
-
const startAttr = innerListToken.attrs?.find(attr => attr[0] === 'start')
|
|
270
|
-
let startNumber = startAttr ? parseInt(startAttr[1], 10) : 1
|
|
271
|
-
const suffix = innerListToken.markup || '.' // markup is suffix
|
|
272
|
-
let itemCount = 0
|
|
273
|
-
|
|
274
|
-
// Get info from list_item_open (number set by markdown-it)
|
|
275
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
276
|
-
if (tokens[j].type === 'list_item_open' && tokens[j].level === innerListToken.level + 1) {
|
|
277
|
-
// Get number from info
|
|
278
|
-
const number = tokens[j].info ? parseInt(tokens[j].info, 10) : startNumber + itemCount
|
|
279
|
-
allMarkers.push({
|
|
280
|
-
type: 'decimal',
|
|
281
|
-
marker: number + suffix,
|
|
282
|
-
number: number,
|
|
283
|
-
prefix: '',
|
|
284
|
-
suffix: suffix
|
|
285
|
-
})
|
|
286
|
-
itemCount++
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Build new token array
|
|
293
|
-
const newTokens = []
|
|
294
|
-
|
|
295
|
-
// Tokens before bullet_list_open
|
|
296
|
-
for (let j = 0; j < i; j++) {
|
|
297
|
-
newTokens.push(tokens[j])
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Add first inner list open token and save merged marker info
|
|
301
|
-
const firstListToken = tokens[itemIndices[0].innerListOpen]
|
|
302
|
-
|
|
303
|
-
// If outer ul is convertible, convert inner list to ordered_list
|
|
304
|
-
// (Don't limit to bullet_list as inner list might already be ordered_list)
|
|
305
|
-
if (outerListInfo && outerListInfo.shouldConvert) {
|
|
306
|
-
if (firstListToken.type === 'bullet_list_open') {
|
|
307
|
-
firstListToken.type = 'ordered_list_open'
|
|
308
|
-
firstListToken.tag = 'ol'
|
|
309
|
-
firstListToken._convertedFromBullet = true
|
|
310
|
-
}
|
|
311
|
-
// If already ordered_list, use as is (already converted)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
newTokens.push(firstListToken)
|
|
315
|
-
|
|
316
|
-
// Merge and save marker info
|
|
317
|
-
if (allMarkers.length > 0) {
|
|
318
|
-
const firstMarkerType = allMarkers[0]?.type
|
|
319
|
-
firstListToken._markerInfo = {
|
|
320
|
-
markers: allMarkers,
|
|
321
|
-
type: firstMarkerType,
|
|
322
|
-
isConsistent: allMarkers.every(m => m.type === firstMarkerType),
|
|
323
|
-
count: allMarkers.length
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ===== Outer UL (Level 0 after conversion) loose/tight determination =====
|
|
328
|
-
// In flattened pattern (`- 1.` etc), no direct paragraph in outer list_item,
|
|
329
|
-
// so cannot detect with markdown-it's paragraph.hidden
|
|
330
|
-
// Instead, use map info of outer list_item to detect blank lines between items
|
|
331
|
-
let outerUlIsLoose = false
|
|
332
|
-
|
|
333
|
-
// Single item is always tight
|
|
334
|
-
if (itemIndices.length === 1) {
|
|
335
|
-
outerUlIsLoose = false
|
|
336
|
-
} else {
|
|
337
|
-
// For multiple items, check blank lines with map info
|
|
338
|
-
for (let itemIdx = 0; itemIdx < itemIndices.length - 1; itemIdx++) {
|
|
339
|
-
const currentItem = itemIndices[itemIdx]
|
|
340
|
-
const nextItem = itemIndices[itemIdx + 1]
|
|
341
|
-
|
|
342
|
-
// Get currentItem end line: list_item_close map or last token map
|
|
343
|
-
let currentEndLine = null
|
|
344
|
-
// Find list_item_close map (usually null, so use last token instead)
|
|
345
|
-
for (let k = currentItem.outerItemClose - 1; k > currentItem.outerItemOpen; k--) {
|
|
346
|
-
if (tokens[k].map && tokens[k].map[1]) {
|
|
347
|
-
currentEndLine = tokens[k].map[1]
|
|
348
|
-
break
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const nextMap = tokens[nextItem.outerItemOpen].map
|
|
353
|
-
|
|
354
|
-
if (currentEndLine !== null && nextMap) {
|
|
355
|
-
// Compare currentItem end line and nextItem start line
|
|
356
|
-
// If line numbers differ, there's blank line
|
|
357
|
-
const lineGap = nextMap[0] - currentEndLine
|
|
358
|
-
if (lineGap > 0) {
|
|
359
|
-
outerUlIsLoose = true
|
|
360
|
-
break
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// ===== Nested list overall loose/tight determination =====
|
|
367
|
-
// Inner list (converted ol) loose/tight determination
|
|
368
|
-
// If parent item is loose (_parentIsLoose flag), or
|
|
369
|
-
// if outer ul is loose, this inner list is also loose
|
|
370
|
-
|
|
371
|
-
// First, check if there are blank lines between converted ol list_items (independent of outerUlIsLoose)
|
|
372
|
-
// This is used for propagation to child lists
|
|
373
|
-
let innerListIsLooseDueToBlankLines = false
|
|
374
|
-
|
|
375
|
-
// In flattened pattern (`- 1.` etc), each outer list_item has one inner ol
|
|
376
|
-
// Check if there are blank lines between list_items in inner ol
|
|
377
|
-
// (Blank lines between outer list_items already checked by outerUlIsLoose)
|
|
378
|
-
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
379
|
-
const item = itemIndices[itemIdx]
|
|
380
|
-
|
|
381
|
-
// Collect list_items in inner list
|
|
382
|
-
const innerListItems = []
|
|
383
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
384
|
-
if (tokens[j].type === 'list_item_open' &&
|
|
385
|
-
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
386
|
-
const itemOpen = j
|
|
387
|
-
const itemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
388
|
-
innerListItems.push({ open: itemOpen, close: itemClose })
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Check blank lines between list_items in inner list
|
|
393
|
-
if (innerListItems.length > 1) {
|
|
394
|
-
for (let k = 0; k < innerListItems.length - 1; k++) {
|
|
395
|
-
const currentItem = innerListItems[k]
|
|
396
|
-
const nextItem = innerListItems[k + 1]
|
|
397
|
-
|
|
398
|
-
// Get currentItem end line
|
|
399
|
-
let currentEndLine = null
|
|
400
|
-
for (let m = currentItem.close - 1; m > currentItem.open; m--) {
|
|
401
|
-
if (tokens[m].map && tokens[m].map[1]) {
|
|
402
|
-
currentEndLine = tokens[m].map[1]
|
|
403
|
-
break
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const nextMap = tokens[nextItem.open].map
|
|
408
|
-
|
|
409
|
-
if (currentEndLine !== null && nextMap) {
|
|
410
|
-
const lineGap = nextMap[0] - currentEndLine
|
|
411
|
-
if (lineGap > 0) {
|
|
412
|
-
innerListIsLooseDueToBlankLines = true
|
|
413
|
-
break
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (innerListIsLooseDueToBlankLines) break
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Check markdown-it's paragraph.hidden
|
|
422
|
-
// If inner list has paragraph with hidden=false, it's loose
|
|
423
|
-
if (!innerListIsLooseDueToBlankLines) {
|
|
424
|
-
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
425
|
-
const item = itemIndices[itemIdx]
|
|
426
|
-
|
|
427
|
-
let innerListItemOpen = -1
|
|
428
|
-
let innerListItemClose = -1
|
|
429
|
-
|
|
430
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
431
|
-
if (tokens[j].type === 'list_item_open' &&
|
|
432
|
-
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
433
|
-
innerListItemOpen = j
|
|
434
|
-
innerListItemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
435
|
-
break
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
|
|
440
|
-
// Check hidden of first paragraph in inner list item
|
|
441
|
-
let nestedListDepth = 0
|
|
442
|
-
for (let j = innerListItemOpen + 1; j < innerListItemClose; j++) {
|
|
443
|
-
const t = tokens[j]
|
|
444
|
-
if (t.type === 'bullet_list_open' || t.type === 'ordered_list_open') {
|
|
445
|
-
nestedListDepth++
|
|
446
|
-
} else if (t.type === 'bullet_list_close' || t.type === 'ordered_list_close') {
|
|
447
|
-
nestedListDepth--
|
|
448
|
-
} else if (nestedListDepth === 0 && t.type === 'paragraph_open') {
|
|
449
|
-
if (t.hidden === false) {
|
|
450
|
-
innerListIsLooseDueToBlankLines = true
|
|
451
|
-
}
|
|
452
|
-
break
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
if (innerListIsLooseDueToBlankLines) break
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// If outerUlIsLoose, force first paragraph of each inner list item to loose (hidden=false)
|
|
461
|
-
// markdown-it judges tight/loose by original ul structure, but after flattening to ol structure
|
|
462
|
-
// should reflect parent ul's loose state
|
|
463
|
-
// However, exclude case where both parent ul and each inner ol have single item
|
|
464
|
-
// This must be executed AFTER innerListIsLooseDueToBlankLines determination
|
|
465
|
-
if (outerUlIsLoose && !(itemIndices.length === 1)) {
|
|
466
|
-
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
467
|
-
const item = itemIndices[itemIdx]
|
|
468
|
-
// Find list_items in inner list
|
|
469
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
470
|
-
if (tokens[j].type === 'list_item_open' &&
|
|
471
|
-
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
472
|
-
// Find first paragraph in list_item
|
|
473
|
-
const listItemOpen = j
|
|
474
|
-
const listItemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
475
|
-
for (let k = listItemOpen + 1; k < listItemClose; k++) {
|
|
476
|
-
if (tokens[k].type === 'paragraph_open' &&
|
|
477
|
-
tokens[k].level === tokens[listItemOpen].level + 1) {
|
|
478
|
-
tokens[k].hidden = false
|
|
479
|
-
break // Only first paragraph
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
break // Only first list_item of inner list
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Final innerListIsLoose: _parentIsLoose, outerUlIsLoose, or blank line determination
|
|
489
|
-
let innerListIsLoose = token._parentIsLoose || outerUlIsLoose || innerListIsLooseDueToBlankLines
|
|
490
|
-
|
|
491
|
-
// ===== Merge each inner list's contents and place extra content appropriately =====
|
|
492
|
-
for (let itemIdx = 0; itemIdx < itemIndices.length; itemIdx++) {
|
|
493
|
-
const item = itemIndices[itemIdx]
|
|
494
|
-
|
|
495
|
-
// Count list_items in this inner list (ordered_list)
|
|
496
|
-
let innerListItemCount = 0
|
|
497
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
498
|
-
if (tokens[j].type === 'list_item_open' &&
|
|
499
|
-
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
500
|
-
innerListItemCount++
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Process list_items in this inner list (ordered_list)
|
|
505
|
-
// Inner list usually contains only one list_item (ul>li>ol>li structure)
|
|
506
|
-
let innerListItemOpen = -1
|
|
507
|
-
let innerListItemClose = -1
|
|
508
|
-
|
|
509
|
-
for (let j = item.innerListOpen + 1; j < item.innerListClose; j++) {
|
|
510
|
-
if (tokens[j].type === 'list_item_open' &&
|
|
511
|
-
tokens[j].level === tokens[item.innerListOpen].level + 1) {
|
|
512
|
-
innerListItemOpen = j
|
|
513
|
-
innerListItemClose = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
514
|
-
break
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (innerListItemOpen !== -1 && innerListItemClose !== -1) {
|
|
519
|
-
// Add list_item_open (use original token to preserve info)
|
|
520
|
-
// Note: value attribute is added in Phase3 (not during simplification)
|
|
521
|
-
newTokens.push(tokens[innerListItemOpen])
|
|
522
|
-
|
|
523
|
-
// This item's loose/tight determination
|
|
524
|
-
// If entire list is loose, this item is also loose
|
|
525
|
-
// Determination when extraContent has block elements:
|
|
526
|
-
// - extraContent has paragraph(hidden=false) → loose
|
|
527
|
-
// - Has heading(heading_open) or HTML block(html_block) → loose
|
|
528
|
-
// - markdown-it sets paragraph.hidden=true → maintain tight
|
|
529
|
-
let hasBlockElement = false
|
|
530
|
-
if (item.hasExtraContent) {
|
|
531
|
-
for (let k = item.extraContentStart; k < item.extraContentEnd; k++) {
|
|
532
|
-
const tokenType = tokens[k].type
|
|
533
|
-
// Detect block elements:
|
|
534
|
-
// - paragraph(hidden=false): multiple paragraphs
|
|
535
|
-
// - heading_open: heading
|
|
536
|
-
// - html_block: HTML block
|
|
537
|
-
if ((tokenType === 'paragraph_open' && !tokens[k].hidden) ||
|
|
538
|
-
tokenType === 'heading_open' ||
|
|
539
|
-
tokenType === 'html_block') {
|
|
540
|
-
hasBlockElement = true
|
|
541
|
-
break
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Whether this item's first paragraph is loose
|
|
547
|
-
// is obtained from Phase1-analyzed listInfo.items[itemIdx]
|
|
548
|
-
const listItem = outerListInfo?.items?.[itemIdx]
|
|
549
|
-
const firstParagraphIsLoose = listItem?.firstParagraphIsLoose || false
|
|
550
|
-
|
|
551
|
-
// Make loose if outer ul, entire inner list, or this item has extraContent
|
|
552
|
-
// - However, consider cases with multiple paragraphs in item (innerListIsLoose) or
|
|
553
|
-
// block elements (hasBlockElement)
|
|
554
|
-
//
|
|
555
|
-
// Single item determination in flattened pattern (`- 1.` etc):
|
|
556
|
-
// - Parent ul single item AND each inner ol single item → exclude outerUlIsLoose
|
|
557
|
-
// - If parent ul has multiple items, consider outerUlIsLoose (reflect parent's blank lines)
|
|
558
|
-
let shouldBeLoose
|
|
559
|
-
if (itemIndices.length === 1 && innerListItemCount === 1) {
|
|
560
|
-
// Exclude outerUlIsLoose if both parent ul and each inner ol are single item
|
|
561
|
-
// Consider innerListIsLoose and hasBlockElement (block elements)
|
|
562
|
-
shouldBeLoose = innerListIsLoose || hasBlockElement
|
|
563
|
-
} else {
|
|
564
|
-
// Normal determination if multiple items or parent ul has multiple items
|
|
565
|
-
// Consider outerUlIsLoose (reflect parent list's blank lines)
|
|
566
|
-
shouldBeLoose = outerUlIsLoose || innerListIsLoose || hasBlockElement
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Determine whether to propagate _parentIsLoose flag to child lists
|
|
570
|
-
// - innerListIsLooseDueToBlankLines: blank lines between converted ol list_items
|
|
571
|
-
// - token._parentIsLoose: already propagated from parent
|
|
572
|
-
// - firstParagraphIsLoose: blank line after parent item's first paragraph (same marker type only)
|
|
573
|
-
// Note: hasExtraContent makes parent item loose but doesn't affect child lists
|
|
574
|
-
// Note: outerUlIsLoose excluded (Test 25: handle case where parent is loose but child is tight)
|
|
575
|
-
const shouldPropagateLooseToChildren = innerListIsLooseDueToBlankLines || token._parentIsLoose || firstParagraphIsLoose || false
|
|
576
|
-
|
|
577
|
-
// Copy tokens in inner list item (exclude nested list paragraphs)
|
|
578
|
-
let nestedListDepth = 0
|
|
579
|
-
let firstParagraphInItem = true // Track first paragraph in each list_item
|
|
580
|
-
for (let j = innerListItemOpen + 1; j < innerListItemClose; j++) {
|
|
581
|
-
const tokenToPush = tokens[j]
|
|
582
|
-
|
|
583
|
-
// Track nested list depth
|
|
584
|
-
if (tokenToPush.type === 'bullet_list_open' || tokenToPush.type === 'ordered_list_open') {
|
|
585
|
-
nestedListDepth++
|
|
586
|
-
// If parent list is loose (due to blank lines), set _parentIsLoose flag to child lists
|
|
587
|
-
// Don't set if only outerUlIsLoose
|
|
588
|
-
// Propagate to all nest levels (support level >= 2)
|
|
589
|
-
if (shouldPropagateLooseToChildren) {
|
|
590
|
-
// Set _parentIsLoose flag (optimize with direct property assignment)
|
|
591
|
-
tokenToPush._parentIsLoose = true
|
|
592
|
-
newTokens.push(tokenToPush)
|
|
593
|
-
} else {
|
|
594
|
-
newTokens.push(tokenToPush)
|
|
595
|
-
}
|
|
596
|
-
} else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
|
|
597
|
-
nestedListDepth--
|
|
598
|
-
newTokens.push(tokenToPush)
|
|
599
|
-
} else if (nestedListDepth === 0 && (tokenToPush.type === 'paragraph_open' || tokenToPush.type === 'paragraph_close')) {
|
|
600
|
-
// Update paragraph_open/close hidden state (only outside nested lists)
|
|
601
|
-
|
|
602
|
-
if (tokenToPush.type === 'paragraph_open') {
|
|
603
|
-
// Process first paragraph
|
|
604
|
-
if (firstParagraphInItem) {
|
|
605
|
-
// If tight list (shouldBeLoose = false), hide first paragraph
|
|
606
|
-
tokenToPush.hidden = !shouldBeLoose
|
|
607
|
-
firstParagraphInItem = false
|
|
608
|
-
} else {
|
|
609
|
-
// Always show second and subsequent paragraphs
|
|
610
|
-
tokenToPush.hidden = false
|
|
611
|
-
}
|
|
612
|
-
} else {
|
|
613
|
-
// Match paragraph_close hidden state to corresponding paragraph_open
|
|
614
|
-
// Reference preceding paragraph_open's hidden state
|
|
615
|
-
const prevToken = newTokens[newTokens.length - 2] // Skip preceding inline, get paragraph_open before it
|
|
616
|
-
if (prevToken && prevToken.type === 'paragraph_open') {
|
|
617
|
-
tokenToPush.hidden = prevToken.hidden
|
|
618
|
-
} else {
|
|
619
|
-
// Fallback: judge based on shouldBeLoose
|
|
620
|
-
tokenToPush.hidden = !shouldBeLoose && firstParagraphInItem
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
newTokens.push(tokenToPush)
|
|
625
|
-
} else {
|
|
626
|
-
newTokens.push(tokenToPush)
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Add extra content (content in ul>li, outside ol) to this list_item
|
|
631
|
-
if (item.hasExtraContent) {
|
|
632
|
-
// Adjust level: ul list_item's level → ol list_item's level
|
|
633
|
-
const levelDiff = tokens[innerListItemOpen].level - tokens[item.outerItemOpen].level
|
|
634
|
-
|
|
635
|
-
// Track nested lists in extraContent
|
|
636
|
-
let extraNestedListDepth = 0
|
|
637
|
-
for (let k = item.extraContentStart; k < item.extraContentEnd; k++) {
|
|
638
|
-
const tokenToPush = tokens[k]
|
|
639
|
-
const originalLevel = tokenToPush.level
|
|
640
|
-
tokenToPush.level = originalLevel + levelDiff
|
|
641
|
-
|
|
642
|
-
// Track nested list depth
|
|
643
|
-
if (tokenToPush.type === 'bullet_list_open' || tokenToPush.type === 'ordered_list_open') {
|
|
644
|
-
extraNestedListDepth++
|
|
645
|
-
// If parent item is loose, set _parentIsLoose flag to child lists
|
|
646
|
-
// Don't set if only outerUlIsLoose
|
|
647
|
-
if (shouldPropagateLooseToChildren && extraNestedListDepth === 1) {
|
|
648
|
-
tokenToPush._parentIsLoose = true
|
|
649
|
-
}
|
|
650
|
-
} else if (tokenToPush.type === 'bullet_list_close' || tokenToPush.type === 'ordered_list_close') {
|
|
651
|
-
extraNestedListDepth--
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// If shouldBeLoose, set paragraph hidden to false (only outside nested lists)
|
|
655
|
-
if (shouldBeLoose && extraNestedListDepth === 0 && (tokenToPush.type === 'paragraph_open' || tokenToPush.type === 'paragraph_close')) {
|
|
656
|
-
tokenToPush.hidden = false
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
newTokens.push(tokenToPush)
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Add list_item_close
|
|
664
|
-
newTokens.push(tokens[innerListItemClose])
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Add last inner list's close token
|
|
669
|
-
const lastItem = itemIndices[itemIndices.length - 1]
|
|
670
|
-
const lastListCloseToken = tokens[lastItem.innerListClose]
|
|
671
|
-
|
|
672
|
-
// If outer ul is convertible, also convert close token
|
|
673
|
-
if (outerListInfo && outerListInfo.shouldConvert) {
|
|
674
|
-
if (lastListCloseToken.type === 'bullet_list_close') {
|
|
675
|
-
lastListCloseToken.type = 'ordered_list_close'
|
|
676
|
-
lastListCloseToken.tag = 'ol'
|
|
677
|
-
}
|
|
678
|
-
// If already ordered_list_close, use as is
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
newTokens.push(lastListCloseToken)
|
|
682
|
-
|
|
683
|
-
// Tokens after bullet_list_close
|
|
684
|
-
for (let j = listCloseIdx + 1; j < tokens.length; j++) {
|
|
685
|
-
newTokens.push(tokens[j])
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Replace token array
|
|
689
|
-
tokens.length = 0
|
|
690
|
-
tokens.push(...newTokens)
|
|
691
|
-
|
|
692
|
-
// Remove markers from merged list
|
|
693
|
-
if (firstListToken._markerInfo && firstListToken._markerInfo.markers) {
|
|
694
|
-
let listStartIdx = 0
|
|
695
|
-
while (listStartIdx < tokens.length && tokens[listStartIdx] !== firstListToken) {
|
|
696
|
-
listStartIdx++
|
|
697
|
-
}
|
|
698
|
-
if (listStartIdx < tokens.length) {
|
|
699
|
-
const listEndIdx = findMatchingClose(tokens, listStartIdx,
|
|
700
|
-
firstListToken.type,
|
|
701
|
-
firstListToken.type.replace('_open', '_close'), false)
|
|
702
|
-
if (listEndIdx !== -1) {
|
|
703
|
-
removeMarkersFromContent(tokens, listStartIdx, listEndIdx, firstListToken._markerInfo, null)
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
modified = true
|
|
709
|
-
break
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// ===== Propagate ordered_list's _parentIsLoose flag to child lists =====
|
|
715
|
-
// Check all ordered_list_open, and if _parentIsLoose flag exists,
|
|
716
|
-
// propagate to child lists
|
|
717
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
718
|
-
const token = tokens[i]
|
|
719
|
-
|
|
720
|
-
if ((token.type === 'ordered_list_open' || token.type === 'bullet_list_open') && token._parentIsLoose) {
|
|
721
|
-
// Find this list's close token
|
|
722
|
-
const listCloseIdx = findMatchingClose(tokens, i, token.type, token.type.replace('_open', '_close'))
|
|
723
|
-
if (listCloseIdx === -1) continue
|
|
724
|
-
|
|
725
|
-
// Search list_items in this list and set _parentIsLoose flag to child lists in those list_items
|
|
726
|
-
for (let j = i + 1; j < listCloseIdx; j++) {
|
|
727
|
-
if (tokens[j].type === 'list_item_open' && tokens[j].level === token.level + 1) {
|
|
728
|
-
const itemCloseIdx = findMatchingClose(tokens, j, 'list_item_open', 'list_item_close')
|
|
729
|
-
|
|
730
|
-
// Search child lists in this list_item
|
|
731
|
-
for (let k = j + 1; k < itemCloseIdx; k++) {
|
|
732
|
-
if ((tokens[k].type === 'bullet_list_open' || tokens[k].type === 'ordered_list_open') &&
|
|
733
|
-
tokens[k].level === token.level + 2) {
|
|
734
|
-
// Set _parentIsLoose flag to child list
|
|
735
|
-
tokens[k]._parentIsLoose = true
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
j = itemCloseIdx // Jump to next list_item
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|