@peaceroad/markdown-it-strong-ja 0.4.5 → 0.5.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.
Files changed (2) hide show
  1. package/index.js +1298 -775
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -1,848 +1,1371 @@
1
- const CHAR_ASTERISK = 0x2A // *
2
- //const CHAR_UNDERSCORE = 0x5F // _
3
- const CHAR_BACKSLASH = 0x5C // \
4
- const CHAR_BACKTICK = 0x60 // `
5
- const CHAR_DOLLAR = 0x24 // $
6
- const CHAR_LT = 0x3C // <
7
- const CHAR_GT = 0x3E // >
8
- const CHAR_SLASH = 0x2F // /
9
- const CHAR_SPACE = 0x20 // ' ' (space)
10
-
11
- const REG_ASTERISKS = /^\*+$/
12
- const REG_ATTRS = /{[^{}\n!@#%^&*()]+?}$/
13
- const REG_PUNCTUATION = /[!-/:-@[-`{-~ ]/
14
- const REG_JAPANESE = /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}|\p{General_Category=Punctuation}|\p{General_Category=Symbol}|\p{General_Category=Format}|\p{Emoji}/u // ひらがな|カタカナ|漢字|句読点|記号|フォーマット文字|絵文字
15
-
16
- const REG_MARKDOWN_HTML = /^\[[^\[\]]+\]\([^)]+\)$|^<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([^<]+<\/\1>)$|^`[^`]+`$|^\$[^$]+\$$/ // for mixed-language context detection
17
-
18
- const hasBackslash = (state, start) => {
19
- let slashNum = 0
20
- let i = start - 1
21
- const src = state.src
22
- // Early exit if no backslash at all
23
- if (i < 0 || src.charCodeAt(i) !== CHAR_BACKSLASH) {
24
- return false
25
- }
26
- // Count consecutive backslashes efficiently
27
- while (i >= 0 && src.charCodeAt(i) === CHAR_BACKSLASH) {
28
- slashNum++
29
- i--
1
+ import Token from 'markdown-it/lib/token.mjs'
2
+
3
+ const CHAR_ASTERISK = 0x2A // *
4
+ //const CHAR_UNDERSCORE = 0x5F // _
5
+ const CHAR_BACKSLASH = 0x5C // \
6
+ const CHAR_BACKTICK = 0x60 // `
7
+ const CHAR_DOLLAR = 0x24 // $
8
+ const CHAR_LT = 0x3C // <
9
+ const CHAR_GT = 0x3E // >
10
+ const CHAR_SLASH = 0x2F // /
11
+ const CHAR_SPACE = 0x20 // ' ' (space)
12
+ const CHAR_OPEN_BRACKET = 0x5B // [
13
+ const CHAR_CLOSE_BRACKET = 0x5D // ]
14
+
15
+ const REG_ASTERISKS = /^\*+$/
16
+ const REG_ATTRS = /{[^{}\n!@#%^&*()]+?}$/
17
+ const REG_PUNCTUATION = /[!-/:-@[-`{-~ ]/
18
+ const REG_JAPANESE = /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}|\p{General_Category=Punctuation}|\p{General_Category=Symbol}|\p{General_Category=Format}|\p{Emoji}/u // ひらがな|カタカナ|漢字|句読点|記号|フォーマット文字|絵文字
19
+
20
+ const REG_MARKDOWN_HTML = /^\[[^\[\]]+\]\([^)]+\)$|^<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([^<]+<\/\1>)$|^`[^`]+`$|^\$[^$]+\$$/ // for mixed-language context detection
21
+
22
+ const hasBackslash = (state, start) => {
23
+ let slashNum = 0
24
+ let i = start - 1
25
+ const src = state.src
26
+ // Early exit if no backslash at all
27
+ if (i < 0 || src.charCodeAt(i) !== CHAR_BACKSLASH) {
28
+ return false
29
+ }
30
+ // Count consecutive backslashes efficiently
31
+ while (i >= 0 && src.charCodeAt(i) === CHAR_BACKSLASH) {
32
+ slashNum++
33
+ i--
34
+ }
35
+ return slashNum % 2 === 1
36
+ }
37
+
38
+ const findMatchingBracket = (state, start, max, openChar, closeChar) => {
39
+ let depth = 1
40
+ let pos = start + 1
41
+ const src = state.src
42
+ while (pos < max) {
43
+ const ch = src.charCodeAt(pos)
44
+ if (ch === openChar && !hasBackslash(state, pos)) {
45
+ depth++
46
+ } else if (ch === closeChar && !hasBackslash(state, pos)) {
47
+ depth--
48
+ if (depth === 0) return pos
49
+ }
50
+ pos++
51
+ }
52
+ return -1
53
+ }
54
+
55
+ const findRefRangeIndex = (pos, refRanges) => {
56
+ if (!refRanges || refRanges.length === 0) return -1
57
+ const cache = refRanges.__cache
58
+ if (cache && cache.has(pos)) return cache.get(pos)
59
+ let left = 0
60
+ let right = refRanges.length - 1
61
+ while (left <= right) {
62
+ const mid = left + Math.floor((right - left) / 2)
63
+ const range = refRanges[mid]
64
+ if (pos < range.start) {
65
+ right = mid - 1
66
+ } else if (pos > range.end) {
67
+ left = mid + 1
68
+ } else {
69
+ const result = range.hasReference ? mid : -1
70
+ if (cache) cache.set(pos, result)
71
+ return result
72
+ }
30
73
  }
31
- return slashNum % 2 === 1
74
+ if (cache) cache.set(pos, -1)
75
+ return -1
32
76
  }
33
-
34
- const setToken = (state, inlines, opt) => {
77
+
78
+ // Detect reference-link label ranges within the current inline slice
79
+ const computeReferenceRanges = (state, start, max) => {
35
80
  const src = state.src
36
- let i = 0
37
- let attrsIsText = {
38
- val: false,
39
- tag: '',
40
- }
41
- while (i < inlines.length) {
42
- let type = inlines[i].type
43
- const tag = type.replace(/(?:_open|_close)$/, '')
44
-
45
- if (/_open$/.test(type)) {
46
- const startToken = state.push(type, tag, 1)
47
- startToken.markup = tag === 'strong' ? '**' : '*'
48
- attrsIsText = {
49
- val: true,
50
- tag: tag,
51
- }
52
- }
53
-
54
- if (type === 'html_inline') {
55
- type = 'text'
56
- }
57
- if (type === 'text') {
58
- let content = src.slice(inlines[i].s, inlines[i].e + 1)
59
- if (REG_ASTERISKS.test(content)) {
60
- const asteriskToken = state.push(type, '', 0)
61
- asteriskToken.content = content
62
- i++
63
- continue
64
- }
65
- if (opt.mditAttrs && attrsIsText.val && i + 1 < inlines.length) {
66
- const hasImmediatelyAfterAsteriskClose = inlines[i+1].type === attrsIsText.tag + '_close'
67
- if (hasImmediatelyAfterAsteriskClose && REG_ATTRS.test(content)) {
68
- const attrsToken = state.push(type, '', 0)
69
-
70
- const hasBackslashBeforeCurlyAttribute = content.match(/(\\+){/)
71
- if (hasBackslashBeforeCurlyAttribute) {
72
- if (hasBackslashBeforeCurlyAttribute[1].length === 1) {
73
- attrsToken.content = content.replace(/\\{/, '{')
74
- } else {
75
- let backSlashNum = Math.floor(hasBackslashBeforeCurlyAttribute[1].length / 2)
76
- let k = 0
77
- let backSlash = ''
78
- while (k < backSlashNum) {
79
- backSlash += '\\'
80
- k++
81
+ const references = state.env && state.env.references
82
+ const hasReferences = references && Object.keys(references).length > 0
83
+ const firstBracket = src.indexOf('[', start)
84
+ if (firstBracket === -1 || firstBracket >= max) return []
85
+ const ranges = []
86
+ let pos = start
87
+ while (pos < max) {
88
+ if (src.charCodeAt(pos) === CHAR_OPEN_BRACKET && !hasBackslash(state, pos)) {
89
+ const labelClose = findMatchingBracket(state, pos, max, CHAR_OPEN_BRACKET, CHAR_CLOSE_BRACKET)
90
+ if (labelClose !== -1) {
91
+ const nextPos = labelClose + 1
92
+ if (nextPos < max && src.charCodeAt(nextPos) === CHAR_OPEN_BRACKET && !hasBackslash(state, nextPos)) {
93
+ const refClose = findMatchingBracket(state, nextPos, max, CHAR_OPEN_BRACKET, CHAR_CLOSE_BRACKET)
94
+ if (refClose !== -1) {
95
+ let hasReference = false
96
+ if (hasReferences) {
97
+ if (refClose === nextPos + 1) {
98
+ const labelRaw = src.slice(pos + 1, labelClose)
99
+ const normalizedLabel = normalizeReferenceCandidate(state, labelRaw, { useClean: true })
100
+ hasReference = !!references[normalizedLabel]
101
+ } else {
102
+ const refRaw = src.slice(nextPos + 1, refClose)
103
+ const normalizedRef = normalizeReferenceCandidate(state, refRaw)
104
+ hasReference = !!references[normalizedRef]
81
105
  }
82
- attrsToken.content = content.replace(/\\+{/, backSlash + '{')
83
106
  }
84
- } else {
85
- attrsToken.content = content
86
- }
87
- attrsIsText.val = false
88
- i++
89
- continue
90
- }
91
- }
92
-
93
- const childTokens = state.md.parseInline(content, state.env)
94
- if (childTokens[0] && childTokens[0].children) {
95
- let j = 0
96
- while (j < childTokens[0].children.length) {
97
- const t = childTokens[0].children[j]
98
- if (t.type === 'softbreak' && !opt.mdBreaks) {
99
- t.type = 'text'
100
- t.tag = ''
101
- t.content = '\n'
102
- }
103
- if (!opt.mditAttrs && t.tag === 'br') {
104
- t.tag = ''
105
- t.content = '\n'
107
+ if (hasReference) {
108
+ ranges.push({ start: pos, end: labelClose, hasReference: true })
109
+ ranges.push({ start: nextPos, end: refClose, hasReference: true })
110
+ }
111
+ pos = refClose
112
+ continue
106
113
  }
107
- const token = state.push(t.type, t.tag, t.nesting)
108
- token.attrs = t.attrs
109
- token.map = t.map
110
- token.level = t.level
111
- token.children = t.children
112
- token.content = t.content
113
- token.markup = t.markup
114
- token.info = t.info
115
- token.meta = t.meta
116
- token.block = t.block
117
- token.hidden = t.hidden
118
- j++
119
114
  }
120
115
  }
121
116
  }
117
+ pos++
118
+ }
119
+ if (ranges.length) {
120
+ ranges.__cache = new Map()
121
+ }
122
+ return ranges
123
+ }
124
+
125
+ const copyInlineTokenFields = (dest, src) => {
126
+ if (src.attrs) dest.attrs = src.attrs
127
+ if (src.map) dest.map = src.map
128
+ dest.level = src.level
129
+ if (src.children) dest.children = src.children
130
+ dest.content = src.content
131
+ dest.markup = src.markup
132
+ if (src.info) dest.info = src.info
133
+ if (src.meta) dest.meta = src.meta
134
+ dest.block = src.block
135
+ dest.hidden = src.hidden
136
+ }
122
137
 
123
- if (/_close$/.test(type)) {
124
- const closeToken = state.push(type, tag, -1)
125
- closeToken.markup = tag === 'strong' ? '**' : '*'
126
- attrsIsText = {
127
- val: false,
128
- tag: '',
129
- }
130
- }
131
-
132
- i++
138
+ const inlineHasCollapsedRef = (state) => {
139
+ if (state.__strongJaHasCollapsedRefs === undefined) {
140
+ state.__strongJaHasCollapsedRefs = /\[[^\]]*\]\s*\[[^\]]*\]/.test(state.src)
133
141
  }
142
+ return state.__strongJaHasCollapsedRefs
134
143
  }
135
144
 
136
- const pushInlines = (inlines, s, e, len, type, tag, tagType) => {
137
- const inline = {
138
- s: s,
139
- sp: s,
140
- e: e,
141
- ep: e,
142
- len: len,
143
- type: type,
144
- check: type === 'text',
145
+ const registerCollapsedRefTarget = (state) => {
146
+ const env = state.env
147
+ if (!env.__strongJaCollapsedTargets) {
148
+ env.__strongJaCollapsedTargets = []
149
+ env.__strongJaCollapsedTargetSet = typeof WeakSet !== 'undefined' ? new WeakSet() : null
150
+ }
151
+ const targets = env.__strongJaCollapsedTargets
152
+ const targetSet = env.__strongJaCollapsedTargetSet
153
+ if (targetSet) {
154
+ if (targetSet.has(state.tokens)) return
155
+ targetSet.add(state.tokens)
156
+ } else if (targets.includes(state.tokens)) {
157
+ return
145
158
  }
146
- if (tag) inline.tag = [tag, tagType]
147
- inlines.push(inline)
159
+ targets.push(state.tokens)
148
160
  }
149
161
 
150
- const hasNextSymbol = (state, n, max, symbol, noMark) => {
151
- let nextSymbolPos = -1
162
+ const setToken = (state, inlines, opt) => {
163
+ const src = state.src
164
+ let i = 0
165
+ let attrsIsText = {
166
+ val: false,
167
+ tag: '',
168
+ }
169
+ while (i < inlines.length) {
170
+ let type = inlines[i].type
171
+ const tag = type.replace(/(?:_open|_close)$/, '')
172
+
173
+ if (/_open$/.test(type)) {
174
+ const startToken = state.push(type, tag, 1)
175
+ startToken.markup = tag === 'strong' ? '**' : '*'
176
+ attrsIsText = {
177
+ val: true,
178
+ tag: tag,
179
+ }
180
+ }
181
+
182
+ if (type === 'html_inline') {
183
+ type = 'text'
184
+ }
185
+ if (type === 'text') {
186
+ let content = src.slice(inlines[i].s, inlines[i].e + 1)
187
+ if (REG_ASTERISKS.test(content)) {
188
+ const asteriskToken = state.push(type, '', 0)
189
+ asteriskToken.content = content
190
+ i++
191
+ continue
192
+ }
193
+ if (opt.mditAttrs && attrsIsText.val && i + 1 < inlines.length) {
194
+ const hasImmediatelyAfterAsteriskClose = inlines[i+1].type === attrsIsText.tag + '_close'
195
+ if (hasImmediatelyAfterAsteriskClose && REG_ATTRS.test(content)) {
196
+ const attrsToken = state.push(type, '', 0)
197
+
198
+ const hasBackslashBeforeCurlyAttribute = content.match(/(\\+){/)
199
+ if (hasBackslashBeforeCurlyAttribute) {
200
+ if (hasBackslashBeforeCurlyAttribute[1].length === 1) {
201
+ attrsToken.content = content.replace(/\\{/, '{')
202
+ } else {
203
+ let backSlashNum = Math.floor(hasBackslashBeforeCurlyAttribute[1].length / 2)
204
+ let k = 0
205
+ let backSlash = ''
206
+ while (k < backSlashNum) {
207
+ backSlash += '\\'
208
+ k++
209
+ }
210
+ attrsToken.content = content.replace(/\\+{/, backSlash + '{')
211
+ }
212
+ } else {
213
+ attrsToken.content = content
214
+ }
215
+ attrsIsText.val = false
216
+ i++
217
+ continue
218
+ }
219
+ }
220
+
221
+ const childTokens = state.md.parseInline(content, state.env)
222
+ if (childTokens[0] && childTokens[0].children) {
223
+ let j = 0
224
+ while (j < childTokens[0].children.length) {
225
+ const t = childTokens[0].children[j]
226
+ if (t.type === 'softbreak' && !opt.mdBreaks) {
227
+ t.type = 'text'
228
+ t.tag = ''
229
+ t.content = '\n'
230
+ }
231
+ if (!opt.mditAttrs && t.tag === 'br') {
232
+ t.tag = ''
233
+ t.content = '\n'
234
+ }
235
+ const token = state.push(t.type, t.tag, t.nesting)
236
+ copyInlineTokenFields(token, t)
237
+ j++
238
+ }
239
+ }
240
+ }
241
+
242
+ if (/_close$/.test(type)) {
243
+ const closeToken = state.push(type, tag, -1)
244
+ closeToken.markup = tag === 'strong' ? '**' : '*'
245
+ attrsIsText = {
246
+ val: false,
247
+ tag: '',
248
+ }
249
+ }
250
+
251
+ i++
252
+ }
253
+ }
254
+
255
+ const pushInlines = (inlines, s, e, len, type, tag, tagType) => {
256
+ const inline = {
257
+ s: s,
258
+ sp: s,
259
+ e: e,
260
+ ep: e,
261
+ len: len,
262
+ type: type,
263
+ check: type === 'text',
264
+ }
265
+ if (tag) inline.tag = [tag, tagType]
266
+ inlines.push(inline)
267
+ }
268
+
269
+ const findNextSymbolPos = (state, n, max, symbol) => {
152
270
  const src = state.src
153
- if (src.charCodeAt(n) === symbol && !hasBackslash(state, n)) {
154
- for (let i = n + 1; i < max; i++) {
155
- noMark += src[i]
156
- if (src.charCodeAt(i) === symbol && !hasBackslash(state, i)) {
157
- noMark += src.substring(n, i + 1)
158
- nextSymbolPos = i
159
- break
160
- }
271
+ if (src.charCodeAt(n) !== symbol || hasBackslash(state, n)) return -1
272
+ for (let i = n + 1; i < max; i++) {
273
+ if (src.charCodeAt(i) === symbol && !hasBackslash(state, i)) {
274
+ return i
161
275
  }
162
276
  }
163
- return [nextSymbolPos, noMark]
277
+ return -1
164
278
  }
165
279
 
166
280
  const processSymbolPair = (state, n, srcLen, symbol, noMark, textStart, pushInlines) => {
167
- const [nextSymbolPos, newNoMark] = hasNextSymbol(state, n, srcLen, symbol, noMark)
168
- if (nextSymbolPos !== -1) {
169
- if (nextSymbolPos === srcLen - 1) {
170
- pushInlines(textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
171
- return { shouldBreak: true, newN: nextSymbolPos + 1, newNoMark }
172
- }
173
- return { shouldBreak: false, shouldContinue: true, newN: nextSymbolPos + 1, newNoMark }
281
+ const nextSymbolPos = findNextSymbolPos(state, n, srcLen, symbol)
282
+ if (nextSymbolPos === -1) {
283
+ return { shouldBreak: false, shouldContinue: false, newN: n, newNoMark: noMark }
174
284
  }
175
- return { shouldBreak: false, shouldContinue: false, newN: n, newNoMark }
176
- }
177
-
178
- const processTextSegment = (inlines, textStart, n, noMark) => {
179
- if (n !== 0 && noMark.length !== 0) {
180
- pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
181
- return ''
285
+ const src = state.src
286
+ const innerText = src.slice(n + 1, nextSymbolPos)
287
+ const markup = src.slice(n, nextSymbolPos + 1)
288
+ const newNoMark = noMark + innerText + markup
289
+ if (nextSymbolPos === srcLen - 1) {
290
+ pushInlines(textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
291
+ return { shouldBreak: true, newN: nextSymbolPos + 1, newNoMark }
182
292
  }
183
- return noMark
184
- }
185
-
186
- const createInlines = (state, start, max, opt) => {
293
+ return { shouldBreak: false, shouldContinue: true, newN: nextSymbolPos + 1, newNoMark }
294
+ }
295
+
296
+ const processTextSegment = (inlines, textStart, n, noMark) => {
297
+ if (n !== 0 && noMark.length !== 0) {
298
+ pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
299
+ return ''
300
+ }
301
+ return noMark
302
+ }
303
+
304
+ const createInlines = (state, start, max, opt) => {
305
+ const src = state.src
306
+ const srcLen = max
307
+ const htmlEnabled = state.md.options.html
308
+ let n = start
309
+ let inlines = []
310
+ let noMark = ''
311
+ let textStart = n
312
+
313
+ // Infinite loop prevention
314
+ const maxIterations = srcLen * 2 // Safe upper bound
315
+ let iterations = 0
316
+
317
+ while (n < srcLen) {
318
+ // Prevent infinite loops
319
+ iterations++
320
+ if (iterations > maxIterations) {
321
+ // Add remaining text as-is and exit safely
322
+ if (textStart < srcLen) {
323
+ pushInlines(inlines, textStart, srcLen - 1, srcLen - textStart, 'text')
324
+ }
325
+ break
326
+ }
327
+
328
+ const currentChar = src.charCodeAt(n)
329
+
330
+ // Unified escape check
331
+ let isEscaped = false
332
+ if (currentChar === CHAR_ASTERISK || currentChar === CHAR_BACKTICK ||
333
+ (opt.dollarMath && currentChar === CHAR_DOLLAR) ||
334
+ (htmlEnabled && currentChar === CHAR_LT)) {
335
+ isEscaped = hasBackslash(state, n)
336
+ }
337
+
338
+ // Asterisk handling
339
+ if (currentChar === CHAR_ASTERISK) {
340
+ if (!isEscaped) {
341
+ noMark = processTextSegment(inlines, textStart, n, noMark)
342
+ if (n === srcLen - 1) {
343
+ pushInlines(inlines, n, n, 1, '')
344
+ break
345
+ }
346
+ let i = n + 1
347
+ while (i < srcLen && src.charCodeAt(i) === CHAR_ASTERISK) {
348
+ i++
349
+ }
350
+ if (i === srcLen) {
351
+ pushInlines(inlines, n, i - 1, i - n, '')
352
+ } else {
353
+ pushInlines(inlines, n, i - 1, i - n, '')
354
+ textStart = i
355
+ }
356
+ n = i
357
+ continue
358
+ }
359
+ }
360
+
361
+ // Inline code (backticks)
362
+ if (currentChar === CHAR_BACKTICK) {
363
+ if (!isEscaped) {
364
+ const result = processSymbolPair(state, n, srcLen, CHAR_BACKTICK, noMark, textStart,
365
+ (start, end, len, type) => pushInlines(inlines, start, end, len, type))
366
+ if (result.shouldBreak) break
367
+ if (result.shouldContinue) {
368
+ n = result.newN
369
+ noMark = result.newNoMark
370
+ continue
371
+ }
372
+ noMark = result.newNoMark
373
+ }
374
+ }
375
+
376
+ // Inline math ($...$)
377
+ if (opt.dollarMath && currentChar === CHAR_DOLLAR) {
378
+ if (!isEscaped) {
379
+ const result = processSymbolPair(state, n, srcLen, CHAR_DOLLAR, noMark, textStart,
380
+ (start, end, len, type) => pushInlines(inlines, start, end, len, type))
381
+ if (result.shouldBreak) break
382
+ if (result.shouldContinue) {
383
+ n = result.newN
384
+ noMark = result.newNoMark
385
+ continue
386
+ }
387
+ noMark = result.newNoMark
388
+ }
389
+ }
390
+
391
+ // HTML tags
392
+ if (htmlEnabled && currentChar === CHAR_LT) {
393
+ if (!isEscaped) {
394
+ let foundClosingTag = false
395
+ for (let i = n + 1; i < srcLen; i++) {
396
+ if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
397
+ noMark = processTextSegment(inlines, textStart, n, noMark)
398
+ let tag = src.slice(n + 1, i)
399
+ let tagType
400
+ if (tag.charCodeAt(0) === CHAR_SLASH) {
401
+ tag = tag.slice(1)
402
+ tagType = 'close'
403
+ } else {
404
+ tagType = 'open'
405
+ }
406
+ pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
407
+ textStart = i + 1
408
+ n = i + 1
409
+ foundClosingTag = true
410
+ break
411
+ }
412
+ }
413
+ if (foundClosingTag) {
414
+ continue
415
+ }
416
+ // If no closing tag found, treat as regular character to prevent infinite loops
417
+ }
418
+ }
419
+
420
+ // Regular character
421
+ noMark += src[n]
422
+ if (n === srcLen - 1) {
423
+ pushInlines(inlines, textStart, n, n - textStart + 1, 'text')
424
+ break
425
+ }
426
+ n++
427
+ }
428
+ return inlines
429
+ }
430
+
431
+ const pushMark = (marks, opts) => {
432
+ // Maintain sorted order during insertion
433
+ const newMark = {
434
+ nest: opts.nest,
435
+ s: opts.s,
436
+ e: opts.e,
437
+ len: opts.len,
438
+ oLen: opts.oLen,
439
+ type: opts.type
440
+ }
441
+ // Binary search for insertion point to maintain sorted order
442
+ let left = 0
443
+ let right = marks.length
444
+ while (left < right) {
445
+ const mid = Math.floor((left + right) / 2)
446
+ if (marks[mid].s <= newMark.s) {
447
+ left = mid + 1
448
+ } else {
449
+ right = mid
450
+ }
451
+ }
452
+
453
+ marks.splice(left, 0, newMark)
454
+ }
455
+
456
+ const setStrong = (state, inlines, marks, n, memo, opt, nestTracker, refRanges) => {
457
+ if (opt.disallowMixed === true) {
458
+ let i = n + 1
459
+ const inlinesLength = inlines.length
460
+ while (i < inlinesLength) {
461
+ if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
462
+ if (inlines[i].type !== '') { i++; continue }
463
+
464
+ if (inlines[i].len > 1) {
465
+ const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
466
+ if (mixedCheck.shouldBlock) {
467
+ return [n, 0]
468
+ }
469
+ break
470
+ }
471
+ i++
472
+ }
473
+ }
474
+
475
+ const strongOpenRange = findRefRangeIndex(inlines[n].s, refRanges)
476
+ let i = n + 1
477
+ let j = 0
478
+ let nest = 0
479
+ let insideTagsIsClose = 1
480
+ const inlinesLength = inlines.length
481
+ while (i < inlinesLength) {
482
+ if (inlines[i].type !== '') { i++; continue }
483
+ if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
484
+ if (inlines[i].type === 'html_inline') {
485
+ inlines[i].check = true
486
+ insideTagsIsClose = checkInsideTags(inlines, i, memo)
487
+ if (insideTagsIsClose === -1) return [n, nest]
488
+ if (insideTagsIsClose === 0) { i++; continue }
489
+ }
490
+
491
+ const closeRange = findRefRangeIndex(inlines[i].s, refRanges)
492
+ if (strongOpenRange !== closeRange) { i++; continue }
493
+
494
+ nest = checkNest(inlines, marks, n, i, nestTracker)
495
+ if (nest === -1) return [n, nest]
496
+
497
+ if (inlines[i].len === 1 && inlines[n].len > 2) {
498
+ pushMark(marks, {
499
+ nest: nest,
500
+ s: inlines[n].ep,
501
+ e: inlines[n].ep,
502
+ len: 1,
503
+ oLen: inlines[n].len - 1,
504
+ type: 'em_open'
505
+ })
506
+ pushMark(marks, {
507
+ nest: nest,
508
+ s: inlines[i].sp,
509
+ e: inlines[i].ep,
510
+ len: 1,
511
+ oLen: inlines[i].len - 1,
512
+ type: 'em_close'
513
+ })
514
+ inlines[n].len -= 1
515
+ inlines[n].ep -= 1
516
+ inlines[i].len -= 1
517
+ if (inlines[i].len > 0) inlines[i].sp += 1
518
+ if (insideTagsIsClose === 1) {
519
+ const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker, refRanges)
520
+ n = newN
521
+ nest = newNest
522
+ }
523
+ }
524
+ let strongNum = Math.trunc(Math.min(inlines[n].len, inlines[i].len) / 2)
525
+
526
+ if (inlines[i].len > 1) {
527
+ if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges)) {
528
+ if (memo.inlineMarkEnd) {
529
+ marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt, refRanges))
530
+ if (inlines[i].len === 0) { i++; continue }
531
+ } else {
532
+ return [n, nest]
533
+ }
534
+ }
535
+
536
+ j = 0
537
+ while (j < strongNum) {
538
+ pushMark(marks, {
539
+ nest: nest + strongNum - 1 - j,
540
+ s: inlines[n].ep - 1,
541
+ e: inlines[n].ep,
542
+ len: 2,
543
+ oLen: inlines[n].len - 2,
544
+ type: 'strong_open'
545
+ })
546
+ inlines[n].ep -= 2
547
+ inlines[n].len -= 2
548
+ pushMark(marks, {
549
+ nest: nest + strongNum - 1 - j,
550
+ s: inlines[i].sp,
551
+ e: inlines[i].sp + 1,
552
+ len: 2,
553
+ oLen: inlines[i].len - 2,
554
+ type: 'strong_close'
555
+ })
556
+ inlines[i].sp += 2
557
+ inlines[i].len -= 2
558
+ j++
559
+ }
560
+ if (inlines[n].len === 0) return [n, nest]
561
+ }
562
+
563
+ if (inlines[n].len === 1 && inlines[i].len > 0) {
564
+ nest++
565
+ const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, nest, nestTracker, refRanges)
566
+ n = newN
567
+ nest = newNest
568
+ }
569
+
570
+ i++
571
+ }
572
+
573
+ if (n == 0 && memo.inlineMarkEnd) {
574
+ marks.push(...createMarks(state, inlines, n + 1, inlinesLength - 1, memo, opt, refRanges))
575
+ }
576
+ return [n, nest]
577
+ }
578
+
579
+ const checkInsideTags = (inlines, i, memo) => {
580
+ if (inlines[i].tag === undefined) return 0
581
+ const tagName = inlines[i].tag[0].toLowerCase()
582
+ if (memo.htmlTags[tagName] === undefined) {
583
+ memo.htmlTags[tagName] = 0
584
+ }
585
+ if (inlines[i].tag[1] === 'open') {
586
+ memo.htmlTags[tagName] += 1
587
+ }
588
+ if (inlines[i].tag[1] === 'close') {
589
+ memo.htmlTags[tagName] -= 1
590
+ }
591
+ if (memo.htmlTags[tagName] < 0) {
592
+ return -1
593
+ }
594
+
595
+ // Direct check instead of Object.values().every()
596
+ for (const count of Object.values(memo.htmlTags)) {
597
+ if (count !== 0) return 0
598
+ }
599
+ return 1
600
+ }
601
+
602
+ // Check if character is ASCII punctuation or space
603
+ // Covers: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ and space
604
+ const isPunctuation = (ch) => {
605
+ if (!ch) return false
606
+ const code = ch.charCodeAt(0)
607
+ // ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
608
+ return (code >= 33 && code <= 47) || (code >= 58 && code <= 64) ||
609
+ (code >= 91 && code <= 96) || (code >= 123 && code <= 126) || code === 32
610
+ }
611
+
612
+ // Check if character is Japanese (hiragana, katakana, kanji, punctuation, symbols, format chars, emoji)
613
+ // Uses fast Unicode range checks for common cases, falls back to REG_JAPANESE for complex Unicode
614
+ const isJapanese = (ch) => {
615
+ if (!ch) return false
616
+ const code = ch.charCodeAt(0)
617
+ // Fast ASCII check first
618
+ if (code < 128) return false
619
+ // Hiragana: U+3040-U+309F, Katakana: U+30A0-U+30FF, Kanji: U+4E00-U+9FAF
620
+ return (code >= 0x3040 && code <= 0x309F) ||
621
+ (code >= 0x30A0 && code <= 0x30FF) ||
622
+ (code >= 0x4E00 && code <= 0x9FAF) ||
623
+ // Fallback to regex for complex Unicode cases
624
+ REG_JAPANESE.test(ch)
625
+ }
626
+
627
+ // Check if character is English (letters, numbers) or other non-Japanese characters
628
+ // Uses REG_JAPANESE and REG_PUNCTUATION to exclude Japanese and punctuation characters
629
+ const isEnglish = (ch) => {
630
+ if (!ch) return false
631
+ const code = ch.charCodeAt(0)
632
+ if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)) {
633
+ return true
634
+ }
635
+ if (code < 128) {
636
+ return code === CHAR_SPACE || (code > 126)
637
+ }
638
+ return !REG_JAPANESE.test(ch) && !REG_PUNCTUATION.test(ch)
639
+ }
640
+
641
+ const checkMixedLanguagePattern = (state, inlines, n, i, opt) => {
642
+ const src = state.src
643
+ const openPrevChar = src[inlines[n].s - 1] || ''
644
+ const closeNextChar = src[inlines[i].e + 1] || ''
645
+
646
+ const isEnglishPrefix = isEnglish(openPrevChar)
647
+ const isEnglishSuffix = isEnglish(closeNextChar)
648
+ if (!isEnglishPrefix && !isEnglishSuffix) {
649
+ return { hasEnglishContext: false, hasMarkdownOrHtml: false, shouldBlock: false }
650
+ }
651
+
652
+ const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
653
+ const hasMarkdownOrHtml = REG_MARKDOWN_HTML.test(contentBetween)
654
+
655
+ return {
656
+ hasEnglishContext: true,
657
+ hasMarkdownOrHtml,
658
+ shouldBlock: hasMarkdownOrHtml
659
+ }
660
+ }
661
+
662
+ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt, refRanges) => {
187
663
  const src = state.src
188
- const srcLen = max
189
- const htmlEnabled = state.md.options.html
190
- let n = start
191
- let inlines = []
192
- let noMark = ''
193
- let textStart = n
194
-
195
- // Infinite loop prevention
196
- const maxIterations = srcLen * 2 // Safe upper bound
197
- let iterations = 0
198
-
199
- while (n < srcLen) {
200
- // Prevent infinite loops
201
- iterations++
202
- if (iterations > maxIterations) {
203
- // Add remaining text as-is and exit safely
204
- if (textStart < srcLen) {
205
- pushInlines(inlines, textStart, srcLen - 1, srcLen - textStart, 'text')
206
- }
207
- break
208
- }
209
-
210
- const currentChar = src.charCodeAt(n)
211
-
212
- // Unified escape check
213
- let isEscaped = false
214
- if (currentChar === CHAR_ASTERISK || currentChar === CHAR_BACKTICK ||
215
- (opt.dollarMath && currentChar === CHAR_DOLLAR) ||
216
- (htmlEnabled && currentChar === CHAR_LT)) {
217
- isEscaped = hasBackslash(state, n)
218
- }
219
-
220
- // Asterisk handling
221
- if (currentChar === CHAR_ASTERISK) {
222
- if (!isEscaped) {
223
- noMark = processTextSegment(inlines, textStart, n, noMark)
224
- if (n === srcLen - 1) {
225
- pushInlines(inlines, n, n, 1, '')
226
- break
227
- }
228
- let i = n + 1
229
- while (i < srcLen && src.charCodeAt(i) === CHAR_ASTERISK) {
230
- i++
231
- }
232
- if (i === srcLen) {
233
- pushInlines(inlines, n, i - 1, i - n, '')
234
- } else {
235
- pushInlines(inlines, n, i - 1, i - n, '')
236
- textStart = i
237
- }
238
- n = i
239
- continue
240
- }
241
- }
242
-
243
- // Inline code (backticks)
244
- if (currentChar === CHAR_BACKTICK) {
245
- if (!isEscaped) {
246
- const result = processSymbolPair(state, n, srcLen, CHAR_BACKTICK, noMark, textStart,
247
- (start, end, len, type) => pushInlines(inlines, start, end, len, type))
248
- if (result.shouldBreak) break
249
- if (result.shouldContinue) {
250
- n = result.newN
251
- noMark = result.newNoMark
252
- continue
253
- }
254
- noMark = result.newNoMark
255
- }
256
- }
257
-
258
- // Inline math ($...$)
259
- if (opt.dollarMath && currentChar === CHAR_DOLLAR) {
260
- if (!isEscaped) {
261
- const result = processSymbolPair(state, n, srcLen, CHAR_DOLLAR, noMark, textStart,
262
- (start, end, len, type) => pushInlines(inlines, start, end, len, type))
263
- if (result.shouldBreak) break
264
- if (result.shouldContinue) {
265
- n = result.newN
266
- noMark = result.newNoMark
267
- continue
268
- }
269
- noMark = result.newNoMark
270
- }
271
- }
272
-
273
- // HTML tags
274
- if (htmlEnabled && currentChar === CHAR_LT) {
275
- if (!isEscaped) {
276
- let foundClosingTag = false
277
- for (let i = n + 1; i < srcLen; i++) {
278
- if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
279
- noMark = processTextSegment(inlines, textStart, n, noMark)
280
- let tag = src.slice(n + 1, i)
281
- let tagType
282
- if (tag.charCodeAt(0) === CHAR_SLASH) {
283
- tag = tag.slice(1)
284
- tagType = 'close'
285
- } else {
286
- tagType = 'open'
287
- }
288
- pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
289
- textStart = i + 1
290
- n = i + 1
291
- foundClosingTag = true
292
- break
293
- }
294
- }
295
- if (foundClosingTag) {
296
- continue
297
- }
298
- // If no closing tag found, treat as regular character to prevent infinite loops
299
- }
664
+ const openPrevChar = src[inlines[n].s - 1] || ''
665
+ const openNextChar = src[inlines[n].e + 1] || ''
666
+ let checkOpenNextChar = isPunctuation(openNextChar)
667
+ if (checkOpenNextChar && (openNextChar === '[' || openNextChar === ']')) {
668
+ const openNextRange = findRefRangeIndex(inlines[n].e + 1, refRanges)
669
+ if (openNextRange !== -1) {
670
+ checkOpenNextChar = false
300
671
  }
301
-
302
- // Regular character
303
- noMark += src[n]
304
- if (n === srcLen - 1) {
305
- pushInlines(inlines, textStart, n, n - textStart + 1, 'text')
306
- break
672
+ }
673
+ const closePrevChar = src[inlines[i].s - 1] || ''
674
+ let checkClosePrevChar = isPunctuation(closePrevChar)
675
+ if (checkClosePrevChar && (closePrevChar === '[' || closePrevChar === ']')) {
676
+ const closePrevRange = findRefRangeIndex(inlines[i].s - 1, refRanges)
677
+ if (closePrevRange !== -1) {
678
+ checkClosePrevChar = false
307
679
  }
308
- n++
309
680
  }
310
- return inlines
311
- }
681
+ const closeNextChar = src[inlines[i].e + 1] || ''
682
+ const checkCloseNextChar = (isPunctuation(closeNextChar) || i === inlines.length - 1)
683
+
684
+ if (opt.disallowMixed === false) {
685
+ if (isEnglish(openPrevChar) || isEnglish(closeNextChar)) {
686
+ const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
687
+ if (REG_MARKDOWN_HTML.test(contentBetween)) {
688
+ return false
689
+ }
690
+ }
691
+ }
692
+
693
+ const result = (checkOpenNextChar || checkClosePrevChar) && !checkCloseNextChar && !(isJapanese(openPrevChar) || isJapanese(closeNextChar))
694
+ return result
695
+ }
696
+
697
+ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRanges) => {
698
+ const emOpenRange = findRefRangeIndex(inlines[n].s, refRanges)
699
+ if (opt.disallowMixed === true && !sNest) {
700
+ let i = n + 1
701
+ const inlinesLength = inlines.length
702
+ while (i < inlinesLength) {
703
+ if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
704
+ if (inlines[i].type !== '') { i++; continue }
705
+
706
+ if (inlines[i].len > 0) {
707
+ const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
708
+ if (mixedCheck.shouldBlock) {
709
+ return [n, 0]
710
+ }
711
+ break
712
+ }
713
+ i++
714
+ }
715
+ }
716
+
717
+ let i = n + 1
718
+ let nest = 0
719
+ let strongPNum = 0
720
+ let insideTagsIsClose = 1
721
+ const inlinesLength = inlines.length
722
+ while (i < inlinesLength) {
723
+ if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
724
+ if (!sNest && inlines[i].type === 'html_inline') {
725
+ inlines.check = true
726
+ insideTagsIsClose = checkInsideTags(inlines, i, memo)
727
+ if (insideTagsIsClose === -1) return [n, nest]
728
+ if (insideTagsIsClose === 0) { i++; continue }
729
+ }
730
+ if (inlines[i].type !== '') { i++; continue }
731
+
732
+ const closeRange = findRefRangeIndex(inlines[i].s, refRanges)
733
+ if (emOpenRange !== closeRange) {
734
+ i++
735
+ continue
736
+ }
737
+
738
+ const emNum = Math.min(inlines[n].len, inlines[i].len)
739
+
740
+ if (!sNest && emNum !== 1) return [n, sNest, memo]
741
+
742
+ const hasMarkersAtStartAndEnd = (i) => {
743
+ let flag = memo.inlineMarkStart
744
+ if (!flag) return false
745
+ inlinesLength - 1 === i ? flag = true : flag = false
746
+ if (!flag) return false
747
+ inlines[i].len > 1 ? flag = true : flag = false
748
+ return flag
749
+ }
750
+ if (!sNest && inlines[i].len === 2 && !hasMarkersAtStartAndEnd(i)) {
751
+ strongPNum++
752
+ i++
753
+ continue
754
+ }
755
+
756
+ if (sNest) {
757
+ nest = sNest - 1
758
+ } else {
759
+ nest = checkNest(inlines, marks, n, i, nestTracker)
760
+ }
761
+ if (nest === -1) return [n, nest]
762
+
763
+ if (emNum === 1) {
764
+ if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges)) {
765
+ if (memo.inlineMarkEnd) {
766
+ marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt, refRanges))
767
+
768
+ if (inlines[i].len === 0) { i++; continue }
769
+ } else {
770
+ return [n, nest]
771
+ }
772
+ }
773
+ if (inlines[i].len < 1) {
774
+ i++; continue;
775
+ }
776
+
777
+ pushMark(marks, {
778
+ nest: nest,
779
+ s: inlines[n].ep,
780
+ e: inlines[n].ep,
781
+ len: 1,
782
+ oLen: inlines[n].len - 1,
783
+ type: 'em_open'
784
+ })
785
+ inlines[n].ep -= 1
786
+ inlines[n].len -= 1
787
+
788
+ if (strongPNum % 2 === 0 || inlines[i].len < 2) {
789
+ pushMark(marks, {
790
+ nest: nest,
791
+ s: inlines[i].sp,
792
+ e: inlines[i].sp,
793
+ len: 1,
794
+ oLen: inlines[i].len - 1,
795
+ type: 'em_close'
796
+ })
797
+ inlines[i].sp += 1
798
+ } else {
799
+ pushMark(marks, {
800
+ nest: nest,
801
+ s: inlines[i].ep,
802
+ e: inlines[i].ep,
803
+ len: 1,
804
+ oLen: inlines[i].len - 1,
805
+ type: 'em_close'
806
+ })
807
+ inlines[i].sp = inlines[i].ep - 1
808
+ inlines[i].ep -= 1
809
+ }
810
+ inlines[i].len -= 1
811
+ if (inlines[n].len === 0) return [n, nest]
812
+ }
813
+
814
+ i++
815
+ }
816
+ return [n, nest]
817
+ }
818
+
819
+ const setText = (inlines, marks, n, nest) => {
820
+ pushMark(marks, {
821
+ nest: nest,
822
+ s: inlines[n].sp,
823
+ e: inlines[n].ep,
824
+ len: inlines[n].len,
825
+ oLen: -1,
826
+ type: 'text'
827
+ })
828
+ inlines[n].len = 0
829
+ }
830
+
831
+ // Nest state management
832
+ const createNestTracker = () => {
833
+ return {
834
+ strongNest: 0,
835
+ emNest: 0,
836
+ markIndex: 0
837
+ }
838
+ }
839
+
840
+ const updateNestTracker = (tracker, marks, targetPos) => {
841
+ while (tracker.markIndex < marks.length && marks[tracker.markIndex].s <= targetPos) {
842
+ const mark = marks[tracker.markIndex]
843
+ if (mark.type === 'strong_open') tracker.strongNest++
844
+ else if (mark.type === 'strong_close') tracker.strongNest--
845
+ else if (mark.type === 'em_open') tracker.emNest++
846
+ else if (mark.type === 'em_close') tracker.emNest--
847
+ tracker.markIndex++
848
+ }
849
+ }
850
+
851
+ const checkNest = (inlines, marks, n, i, nestTracker) => {
852
+ if (marks.length === 0) return 1
853
+ // Update nest state up to current position
854
+ updateNestTracker(nestTracker, marks, inlines[n].s)
855
+
856
+ const parentNest = nestTracker.strongNest + nestTracker.emNest
857
+ // Check if there's a conflicting close mark before the end position
858
+ let parentCloseN = nestTracker.markIndex
859
+ while (parentCloseN < marks.length) {
860
+ if (marks[parentCloseN].nest === parentNest) break
861
+ parentCloseN++
862
+ }
863
+ if (parentCloseN < marks.length && marks[parentCloseN].s < inlines[i].s) {
864
+ return -1
865
+ }
866
+ return parentNest + 1
867
+ }
868
+
869
+ const createMarks = (state, inlines, start, end, memo, opt, refRanges) => {
870
+ let marks = []
871
+ let n = start
872
+ const nestTracker = createNestTracker()
873
+
874
+ while (n < end) {
875
+ if (inlines[n].type !== '') { n++; continue }
876
+ let nest = 0
877
+
878
+ if (inlines[n].len > 1) {
879
+ const [newN, newNest] = setStrong(state, inlines, marks, n, memo, opt, nestTracker, refRanges)
880
+ n = newN
881
+ nest = newNest
882
+ }
883
+ if (inlines[n].len !== 0) {
884
+ const [newN2, newNest2] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker, refRanges)
885
+ n = newN2
886
+ nest = newNest2
887
+ }
888
+ if (inlines[n].len !== 0) {
889
+ setText(inlines, marks, n, nest)
890
+ }
891
+ n++
892
+ }
893
+ return marks
894
+ }
895
+
896
+ const mergeInlinesAndMarks = (inlines, marks) => {
897
+ // marks array is already sorted, skip sorting
898
+ const merged = []
899
+ let markIndex = 0
900
+ for (const token of inlines) {
901
+ if (token.type === '') {
902
+ while (markIndex < marks.length && marks[markIndex].s >= token.s && marks[markIndex].e <= token.e) {
903
+ merged.push(marks[markIndex])
904
+ markIndex++
905
+ }
906
+ } else {
907
+ merged.push(token)
908
+ }
909
+ }
910
+ while (markIndex < marks.length) {
911
+ merged.push(marks[markIndex++])
912
+ }
913
+ return merged
914
+ }
915
+
916
+ const isWhitespaceToken = (token) => token && token.type === 'text' && token.content.trim() === ''
917
+
918
+ const strongJa = (state, silent, opt) => {
919
+ if (silent) return false
920
+ const start = state.pos
921
+ let max = state.posMax
922
+ const src = state.src
923
+ let attributesSrc
924
+ if (start > max) return false
925
+ if (src.charCodeAt(start) !== CHAR_ASTERISK) return false
926
+ if (hasBackslash(state, start)) return false
927
+
928
+ if (opt.mditAttrs) {
929
+ attributesSrc = src.match(/((\n)? *){([^{}\n!@#%^&*()]+?)} *$/)
930
+ if (attributesSrc && attributesSrc[3] !== '.') {
931
+ max = src.slice(0, attributesSrc.index).length
932
+ if (attributesSrc[2] === '\n') {
933
+ max = src.slice(0, attributesSrc.index - 1).length
934
+ }
935
+ if(hasBackslash(state, attributesSrc.index) && attributesSrc[2] === '' && attributesSrc[1].length === 0) {
936
+ max = state.posMax
937
+ }
938
+ } else {
939
+ let endCurlyKet = src.match(/(\n *){([^{}\n!@#%^&*()]*?)}.*(} *?)$/)
940
+ if (endCurlyKet) {
941
+ max -= endCurlyKet[3].length
942
+ }
943
+ }
944
+ }
945
+
946
+ const refRanges = computeReferenceRanges(state, start, max)
947
+ if (refRanges.length > 0) {
948
+ state.__strongJaHasCollapsedRefs = true
949
+ }
950
+ let inlines = createInlines(state, start, max, opt)
951
+
952
+ const memo = {
953
+ html: state.md.options.html,
954
+ htmlTags: {},
955
+ inlineMarkStart: src.charCodeAt(0) === CHAR_ASTERISK,
956
+ inlineMarkEnd: src.charCodeAt(max - 1) === CHAR_ASTERISK,
957
+ }
958
+
959
+ let marks = createMarks(state, inlines, 0, inlines.length, memo, opt, refRanges)
960
+
961
+ inlines = mergeInlinesAndMarks(inlines, marks)
962
+
963
+ setToken(state, inlines, opt)
312
964
 
313
- const pushMark = (marks, opts) => {
314
- // Maintain sorted order during insertion
315
- const newMark = {
316
- nest: opts.nest,
317
- s: opts.s,
318
- e: opts.e,
319
- len: opts.len,
320
- oLen: opts.oLen,
321
- type: opts.type
965
+ if (inlineHasCollapsedRef(state) && !state.__strongJaCollapsedRefRegistered) {
966
+ registerCollapsedRefTarget(state)
967
+ state.__strongJaCollapsedRefRegistered = true
322
968
  }
323
- // Binary search for insertion point to maintain sorted order
324
- let left = 0
325
- let right = marks.length
326
- while (left < right) {
327
- const mid = Math.floor((left + right) / 2)
328
- if (marks[mid].s <= newMark.s) {
329
- left = mid + 1
330
- } else {
331
- right = mid
969
+
970
+ if (opt.mditAttrs && max !== state.posMax) {
971
+ if (!attributesSrc) {
972
+ state.pos = max
973
+ return true
332
974
  }
975
+ state.pos = attributesSrc[1].length > 1 ? max + attributesSrc[1].length : max
976
+ return true
333
977
  }
334
-
335
- marks.splice(left, 0, newMark)
978
+ state.pos = max
979
+ return true
980
+ }
981
+
982
+ // Collapsed reference helpers
983
+ const buildReferenceLabel = (tokens) => {
984
+ let label = ''
985
+ for (const token of tokens) {
986
+ if (token.type === 'text' || token.type === 'code_inline') {
987
+ label += token.content
988
+ } else if (token.type === 'softbreak' || token.type === 'hardbreak') {
989
+ label += ' '
990
+ } else if (token.type && token.type.endsWith('_open') && token.markup) {
991
+ label += token.markup
992
+ } else if (token.type && token.type.endsWith('_close') && token.markup) {
993
+ label += token.markup
994
+ }
995
+ }
996
+ return label
997
+ }
998
+
999
+ const cleanLabelText = (label) => {
1000
+ return label.replace(/^[*_]+/, '').replace(/[*_]+$/, '')
336
1001
  }
337
1002
 
338
- const setStrong = (state, inlines, marks, n, memo, opt, nestTracker) => {
339
- if (opt.disallowMixed === true) {
340
- let i = n + 1
341
- const inlinesLength = inlines.length
342
- while (i < inlinesLength) {
343
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
344
- if (inlines[i].type !== '') { i++; continue }
345
-
346
- if (inlines[i].len > 1) {
347
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
348
- if (mixedCheck.shouldBlock) {
349
- return [n, 0]
350
- }
351
- break
352
- }
353
- i++
354
- }
355
- }
356
-
357
- let i = n + 1
358
- let j = 0
359
- let nest = 0
360
- let insideTagsIsClose = 1
361
- const inlinesLength = inlines.length
362
- while (i < inlinesLength) {
363
- if (inlines[i].type !== '') { i++; continue }
364
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
365
- if (inlines[i].type === 'html_inline') {
366
- inlines[i].check = true
367
- insideTagsIsClose = checkInsideTags(inlines, i, memo)
368
- if (insideTagsIsClose === -1) return [n, nest]
369
- if (insideTagsIsClose === 0) { i++; continue }
370
- }
371
-
372
- nest = checkNest(inlines, marks, n, i, nestTracker)
373
- if (nest === -1) return [n, nest]
1003
+ const normalizeReferenceCandidate = (state, text, { useClean = false } = {}) => {
1004
+ const source = useClean ? cleanLabelText(text) : text.replace(/\s+/g, ' ').trim()
1005
+ return normalizeRefKey(state, source)
1006
+ }
374
1007
 
375
- if (inlines[i].len === 1 && inlines[n].len > 2) {
376
- pushMark(marks, {
377
- nest: nest,
378
- s: inlines[n].ep,
379
- e: inlines[n].ep,
380
- len: 1,
381
- oLen: inlines[n].len - 1,
382
- type: 'em_open'
383
- })
384
- pushMark(marks, {
385
- nest: nest,
386
- s: inlines[i].sp,
387
- e: inlines[i].ep,
388
- len: 1,
389
- oLen: inlines[i].len - 1,
390
- type: 'em_close'
391
- })
392
- inlines[n].len -= 1
393
- inlines[n].ep -= 1
394
- inlines[i].len -= 1
395
- if (inlines[i].len > 0) inlines[i].sp += 1
396
- if (insideTagsIsClose === 1) {
397
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker)
398
- n = newN
399
- nest = newNest
1008
+ const normalizeRefKey = (state, label) => {
1009
+ const normalize = state.md && state.md.utils && state.md.utils.normalizeReference
1010
+ ? state.md.utils.normalizeReference
1011
+ : (str) => str.trim().replace(/\s+/g, ' ').toUpperCase()
1012
+ return normalize(label)
1013
+ }
1014
+
1015
+ const adjustTokenLevels = (tokens, startIdx, endIdx, delta) => {
1016
+ for (let i = startIdx; i < endIdx; i++) {
1017
+ if (tokens[i]) tokens[i].level += delta
1018
+ }
1019
+ }
1020
+
1021
+ const cloneTextToken = (source, content) => {
1022
+ const newToken = new Token('text', '', 0)
1023
+ newToken.content = content
1024
+ newToken.level = source.level
1025
+ newToken.markup = source.markup
1026
+ newToken.info = source.info
1027
+ newToken.meta = source.meta ? {...source.meta} : null
1028
+ newToken.block = source.block
1029
+ newToken.hidden = source.hidden
1030
+ return newToken
1031
+ }
1032
+
1033
+ // Split only text tokens that actually contain bracket characters
1034
+ const splitBracketToken = (tokens, index) => {
1035
+ const token = tokens[index]
1036
+ if (!token || token.type !== 'text') return false
1037
+ const content = token.content
1038
+ if (!content || (content.indexOf('[') === -1 && content.indexOf(']') === -1)) {
1039
+ return false
1040
+ }
1041
+ const segments = []
1042
+ let buffer = ''
1043
+ let pos = 0
1044
+ while (pos < content.length) {
1045
+ if (content.startsWith('[]', pos)) {
1046
+ if (buffer) {
1047
+ segments.push(buffer)
1048
+ buffer = ''
400
1049
  }
1050
+ segments.push('[]')
1051
+ pos += 2
1052
+ continue
401
1053
  }
402
- let strongNum = Math.trunc(Math.min(inlines[n].len, inlines[i].len) / 2)
403
-
404
- if (inlines[i].len > 1) {
405
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt)) {
406
- if (memo.inlineMarkEnd) {
407
- marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt))
408
- if (inlines[i].len === 0) { i++; continue }
409
- } else {
410
- return [n, nest]
411
- }
1054
+ const ch = content[pos]
1055
+ if (ch === '[' || ch === ']') {
1056
+ if (buffer) {
1057
+ segments.push(buffer)
1058
+ buffer = ''
412
1059
  }
413
-
414
- j = 0
415
- while (j < strongNum) {
416
- pushMark(marks, {
417
- nest: nest + strongNum - 1 - j,
418
- s: inlines[n].ep - 1,
419
- e: inlines[n].ep,
420
- len: 2,
421
- oLen: inlines[n].len - 2,
422
- type: 'strong_open'
423
- })
424
- inlines[n].ep -= 2
425
- inlines[n].len -= 2
426
- pushMark(marks, {
427
- nest: nest + strongNum - 1 - j,
428
- s: inlines[i].sp,
429
- e: inlines[i].sp + 1,
430
- len: 2,
431
- oLen: inlines[i].len - 2,
432
- type: 'strong_close'
433
- })
434
- inlines[i].sp += 2
435
- inlines[i].len -= 2
436
- j++
437
- }
438
- if (inlines[n].len === 0) return [n, nest]
439
- }
440
-
441
- if (inlines[n].len === 1 && inlines[i].len > 0) {
442
- nest++
443
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, nest, nestTracker)
444
- n = newN
445
- nest = newNest
1060
+ segments.push(ch)
1061
+ pos++
1062
+ continue
446
1063
  }
447
-
448
- i++
449
- }
450
-
451
- if (n == 0 && memo.inlineMarkEnd) {
452
- marks.push(...createMarks(state, inlines, n + 1, inlinesLength - 1, memo, opt))
453
- }
454
- return [n, nest]
455
- }
456
-
457
- const checkInsideTags = (inlines, i, memo) => {
458
- if (inlines[i].tag === undefined) return 0
459
- const tagName = inlines[i].tag[0].toLowerCase()
460
- if (memo.htmlTags[tagName] === undefined) {
461
- memo.htmlTags[tagName] = 0
1064
+ buffer += ch
1065
+ pos++
462
1066
  }
463
- if (inlines[i].tag[1] === 'open') {
464
- memo.htmlTags[tagName] += 1
1067
+ if (buffer) segments.push(buffer)
1068
+ if (segments.length <= 1) return false
1069
+ token.content = segments[0]
1070
+ let insertIdx = index + 1
1071
+ for (let s = 1; s < segments.length; s++) {
1072
+ const newToken = cloneTextToken(token, segments[s])
1073
+ tokens.splice(insertIdx, 0, newToken)
1074
+ insertIdx++
465
1075
  }
466
- if (inlines[i].tag[1] === 'close') {
467
- memo.htmlTags[tagName] -= 1
468
- }
469
- if (memo.htmlTags[tagName] < 0) {
470
- return -1
471
- }
472
-
473
- // Direct check instead of Object.values().every()
474
- for (const count of Object.values(memo.htmlTags)) {
475
- if (count !== 0) return 0
476
- }
477
- return 1
478
- }
479
-
480
- // Check if character is ASCII punctuation or space
481
- // Covers: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ and space
482
- const isPunctuation = (ch) => {
483
- if (!ch) return false
484
- const code = ch.charCodeAt(0)
485
- // ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
486
- return (code >= 33 && code <= 47) || (code >= 58 && code <= 64) ||
487
- (code >= 91 && code <= 96) || (code >= 123 && code <= 126) || code === 32
1076
+ return true
488
1077
  }
489
-
490
- // Check if character is Japanese (hiragana, katakana, kanji, punctuation, symbols, format chars, emoji)
491
- // Uses fast Unicode range checks for common cases, falls back to REG_JAPANESE for complex Unicode
492
- const isJapanese = (ch) => {
493
- if (!ch) return false
494
- const code = ch.charCodeAt(0)
495
- // Fast ASCII check first
496
- if (code < 128) return false
497
- // Hiragana: U+3040-U+309F, Katakana: U+30A0-U+30FF, Kanji: U+4E00-U+9FAF
498
- return (code >= 0x3040 && code <= 0x309F) ||
499
- (code >= 0x30A0 && code <= 0x30FF) ||
500
- (code >= 0x4E00 && code <= 0x9FAF) ||
501
- // Fallback to regex for complex Unicode cases
502
- REG_JAPANESE.test(ch)
1078
+
1079
+ const isBracketToken = (token, bracket) => {
1080
+ return token && token.type === 'text' && token.content === bracket
503
1081
  }
504
1082
 
505
- // Check if character is English (letters, numbers) or other non-Japanese characters
506
- // Uses REG_JAPANESE and REG_PUNCTUATION to exclude Japanese and punctuation characters
507
- const isEnglish = (ch) => {
508
- if (!ch) return false
509
- const code = ch.charCodeAt(0)
510
- if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)) {
511
- return true
512
- }
513
- if (code < 128) {
514
- return code === CHAR_SPACE || (code > 126)
515
- }
516
- return !REG_JAPANESE.test(ch) && !REG_PUNCTUATION.test(ch)
517
- }
518
-
519
- const checkMixedLanguagePattern = (state, inlines, n, i, opt) => {
520
- const src = state.src
521
- const openPrevChar = src[inlines[n].s - 1] || ''
522
- const closeNextChar = src[inlines[i].e + 1] || ''
523
-
524
- const isEnglishPrefix = isEnglish(openPrevChar)
525
- const isEnglishSuffix = isEnglish(closeNextChar)
526
- if (!isEnglishPrefix && !isEnglishSuffix) {
527
- return { hasEnglishContext: false, hasMarkdownOrHtml: false, shouldBlock: false }
528
- }
529
-
530
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
531
- const hasMarkdownOrHtml = REG_MARKDOWN_HTML.test(contentBetween)
532
-
533
- return {
534
- hasEnglishContext: true,
535
- hasMarkdownOrHtml,
536
- shouldBlock: hasMarkdownOrHtml
1083
+ const findLinkCloseIndex = (tokens, startIdx) => {
1084
+ let depth = 0
1085
+ for (let idx = startIdx; idx < tokens.length; idx++) {
1086
+ const token = tokens[idx]
1087
+ if (token.type === 'link_open') depth++
1088
+ if (token.type === 'link_close') {
1089
+ depth--
1090
+ if (depth === 0) return idx
1091
+ }
537
1092
  }
1093
+ return -1
538
1094
  }
539
1095
 
540
- const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt) => {
541
- const src = state.src
542
- const openPrevChar = src[inlines[n].s - 1] || ''
543
- const openNextChar = src[inlines[n].e + 1] || ''
544
- const checkOpenNextChar = isPunctuation(openNextChar)
545
- const closePrevChar = src[inlines[i].s - 1] || ''
546
- const checkClosePrevChar = isPunctuation(closePrevChar)
547
- const closeNextChar = src[inlines[i].e + 1] || ''
548
- const checkCloseNextChar = (isPunctuation(closeNextChar) || i === inlines.length - 1)
1096
+ const convertCollapsedReferenceLinks = (tokens, state) => {
1097
+ const references = state.env && state.env.references
1098
+ if (!references || Object.keys(references).length === 0) return
549
1099
 
550
- if (opt.disallowMixed === false) {
551
- if (isEnglish(openPrevChar) || isEnglish(closeNextChar)) {
552
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
553
- if (REG_MARKDOWN_HTML.test(contentBetween)) {
554
- return false
555
- }
1100
+ let i = 0
1101
+ while (i < tokens.length) {
1102
+ if (splitBracketToken(tokens, i)) {
1103
+ continue
556
1104
  }
557
- }
558
-
559
- const result = (checkOpenNextChar || checkClosePrevChar) && !checkCloseNextChar && !(isJapanese(openPrevChar) || isJapanese(closeNextChar))
560
- return result
561
- }
562
-
563
- const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker) => {
564
- if (opt.disallowMixed === true && !sNest) {
565
- let i = n + 1
566
- const inlinesLength = inlines.length
567
- while (i < inlinesLength) {
568
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
569
- if (inlines[i].type !== '') { i++; continue }
570
-
571
- if (inlines[i].len > 0) {
572
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
573
- if (mixedCheck.shouldBlock) {
574
- return [n, 0]
575
- }
1105
+ if (!isBracketToken(tokens[i], '[')) {
1106
+ i++
1107
+ continue
1108
+ }
1109
+ let closeIdx = i + 1
1110
+ while (closeIdx < tokens.length && !isBracketToken(tokens[closeIdx], ']')) {
1111
+ if (splitBracketToken(tokens, closeIdx)) {
1112
+ continue
1113
+ }
1114
+ if (tokens[closeIdx].type === 'link_open') {
1115
+ closeIdx = -1
576
1116
  break
577
1117
  }
1118
+ closeIdx++
1119
+ }
1120
+ if (closeIdx === -1 || closeIdx >= tokens.length) {
1121
+ i++
1122
+ continue
1123
+ }
1124
+
1125
+ if (closeIdx === i + 1) {
578
1126
  i++
1127
+ continue
579
1128
  }
580
- }
581
-
582
- let i = n + 1
583
- let nest = 0
584
- let strongPNum = 0
585
- let insideTagsIsClose = 1
586
- const inlinesLength = inlines.length
587
- while (i < inlinesLength) {
588
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
589
- if (!sNest && inlines[i].type === 'html_inline') {
590
- inlines.check = true
591
- insideTagsIsClose = checkInsideTags(inlines, i, memo)
592
- if (insideTagsIsClose === -1) return [n, nest]
593
- if (insideTagsIsClose === 0) { i++; continue }
594
- }
595
- if (inlines[i].type !== '') { i++; continue }
596
-
597
- const emNum = Math.min(inlines[n].len, inlines[i].len)
598
1129
 
599
- if (!sNest && emNum !== 1) return [n, sNest, memo]
600
-
601
- const hasMarkersAtStartAndEnd = (i) => {
602
- let flag = memo.inlineMarkStart
603
- if (!flag) return false
604
- inlinesLength - 1 === i ? flag = true : flag = false
605
- if (!flag) return false
606
- inlines[i].len > 1 ? flag = true : flag = false
607
- return flag
1130
+ const labelTokens = tokens.slice(i + 1, closeIdx)
1131
+ const labelText = buildReferenceLabel(labelTokens)
1132
+ const cleanedLabel = cleanLabelText(labelText)
1133
+ const whitespaceStart = closeIdx + 1
1134
+ let refRemoveStart = whitespaceStart
1135
+ while (refRemoveStart < tokens.length && isWhitespaceToken(tokens[refRemoveStart])) {
1136
+ refRemoveStart++
608
1137
  }
609
- if (!sNest && inlines[i].len === 2 && !hasMarkersAtStartAndEnd(i)) {
610
- strongPNum++
611
- i++
1138
+ if (splitBracketToken(tokens, refRemoveStart)) {
612
1139
  continue
613
1140
  }
614
-
615
- if (sNest) {
616
- nest = sNest - 1
617
- } else {
618
- nest = checkNest(inlines, marks, n, i, nestTracker)
1141
+ const whitespaceCount = refRemoveStart - whitespaceStart
1142
+ let refKey = null
1143
+ let refRemoveCount = 0
1144
+ let existingLinkOpen = null
1145
+ let existingLinkClose = null
1146
+ const nextToken = tokens[refRemoveStart]
1147
+ if (process.env.DEBUG_COLLAPSED === 'wide') {
1148
+ const debugSlice = tokens.slice(i, Math.min(tokens.length, refRemoveStart + 3)).map((t) => `${t.type}:${t.content || ''}`)
1149
+ console.log('debug collapsed ctx:', debugSlice)
1150
+ console.log('next token info:', nextToken && nextToken.type, nextToken && JSON.stringify(nextToken.content))
619
1151
  }
620
- if (nest === -1) return [n, nest]
621
-
622
- if (emNum === 1) {
623
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt)) {
624
- if (memo.inlineMarkEnd) {
625
- marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt))
626
1152
 
627
- if (inlines[i].len === 0) { i++; continue }
628
- } else {
629
- return [n, nest]
630
- }
1153
+ if (isBracketToken(nextToken, '[]')) {
1154
+ refKey = normalizeReferenceCandidate(state, cleanedLabel)
1155
+ refRemoveCount = 1
1156
+ } else if (isBracketToken(nextToken, '[')) {
1157
+ let refCloseIdx = refRemoveStart + 1
1158
+ while (refCloseIdx < tokens.length && !isBracketToken(tokens[refCloseIdx], ']')) {
1159
+ refCloseIdx++
631
1160
  }
632
- if (inlines[i].len < 1) {
633
- i++; continue;
1161
+ if (refCloseIdx >= tokens.length) {
1162
+ i++
1163
+ continue
634
1164
  }
635
-
636
- pushMark(marks, {
637
- nest: nest,
638
- s: inlines[n].ep,
639
- e: inlines[n].ep,
640
- len: 1,
641
- oLen: inlines[n].len - 1,
642
- type: 'em_open'
643
- })
644
- inlines[n].ep -= 1
645
- inlines[n].len -= 1
646
-
647
- if (strongPNum % 2 === 0 || inlines[i].len < 2) {
648
- pushMark(marks, {
649
- nest: nest,
650
- s: inlines[i].sp,
651
- e: inlines[i].sp,
652
- len: 1,
653
- oLen: inlines[i].len - 1,
654
- type: 'em_close'
655
- })
656
- inlines[i].sp += 1
1165
+ const refTokens = tokens.slice(refRemoveStart + 1, refCloseIdx)
1166
+ if (refTokens.length === 0) {
1167
+ refKey = normalizeReferenceCandidate(state, cleanedLabel)
657
1168
  } else {
658
- pushMark(marks, {
659
- nest: nest,
660
- s: inlines[i].ep,
661
- e: inlines[i].ep,
662
- len: 1,
663
- oLen: inlines[i].len - 1,
664
- type: 'em_close'
665
- })
666
- inlines[i].sp = inlines[i].ep - 1
667
- inlines[i].ep -= 1
1169
+ const refLabelText = buildReferenceLabel(refTokens)
1170
+ refKey = normalizeReferenceCandidate(state, refLabelText)
668
1171
  }
669
- inlines[i].len -= 1
670
- if (inlines[n].len === 0) return [n, nest]
671
- }
672
-
673
- i++
674
- }
675
- return [n, nest]
676
- }
677
-
678
- const setText = (inlines, marks, n, nest) => {
679
- pushMark(marks, {
680
- nest: nest,
681
- s: inlines[n].sp,
682
- e: inlines[n].ep,
683
- len: inlines[n].len,
684
- oLen: -1,
685
- type: 'text'
686
- })
687
- inlines[n].len = 0
688
- }
689
-
690
- // Nest state management
691
- const createNestTracker = () => {
692
- return {
693
- strongNest: 0,
694
- emNest: 0,
695
- markIndex: 0
696
- }
697
- }
698
-
699
- const updateNestTracker = (tracker, marks, targetPos) => {
700
- while (tracker.markIndex < marks.length && marks[tracker.markIndex].s <= targetPos) {
701
- const mark = marks[tracker.markIndex]
702
- if (mark.type === 'strong_open') tracker.strongNest++
703
- else if (mark.type === 'strong_close') tracker.strongNest--
704
- else if (mark.type === 'em_open') tracker.emNest++
705
- else if (mark.type === 'em_close') tracker.emNest--
706
- tracker.markIndex++
707
- }
708
- }
709
-
710
- const checkNest = (inlines, marks, n, i, nestTracker) => {
711
- if (marks.length === 0) return 1
712
- // Update nest state up to current position
713
- updateNestTracker(nestTracker, marks, inlines[n].s)
714
-
715
- const parentNest = nestTracker.strongNest + nestTracker.emNest
716
- // Check if there's a conflicting close mark before the end position
717
- let parentCloseN = nestTracker.markIndex
718
- while (parentCloseN < marks.length) {
719
- if (marks[parentCloseN].nest === parentNest) break
720
- parentCloseN++
721
- }
722
- if (parentCloseN < marks.length && marks[parentCloseN].s < inlines[i].s) {
723
- return -1
724
- }
725
- return parentNest + 1
726
- }
727
-
728
- const createMarks = (state, inlines, start, end, memo, opt) => {
729
- let marks = []
730
- let n = start
731
- const nestTracker = createNestTracker()
732
-
733
- while (n < end) {
734
- if (inlines[n].type !== '') { n++; continue }
735
- let nest = 0
736
-
737
- if (inlines[n].len > 1) {
738
- const [newN, newNest] = setStrong(state, inlines, marks, n, memo, opt, nestTracker)
739
- n = newN
740
- nest = newNest
741
- }
742
- if (inlines[n].len !== 0) {
743
- const [newN2, newNest2] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker)
744
- n = newN2
745
- nest = newNest2
746
- }
747
- if (inlines[n].len !== 0) {
748
- setText(inlines, marks, n, nest)
749
- }
750
- n++
751
- }
752
- return marks
753
- }
754
-
755
- const mergeInlinesAndMarks = (inlines, marks) => {
756
- // marks array is already sorted, skip sorting
757
- const merged = []
758
- let markIndex = 0
759
- for (const token of inlines) {
760
- if (token.type === '') {
761
- while (markIndex < marks.length && marks[markIndex].s >= token.s && marks[markIndex].e <= token.e) {
762
- merged.push(marks[markIndex])
763
- markIndex++
1172
+ refRemoveCount = refCloseIdx - refRemoveStart + 1
1173
+ } else if (nextToken && nextToken.type === 'link_open') {
1174
+ const linkCloseIdx = findLinkCloseIndex(tokens, refRemoveStart)
1175
+ if (linkCloseIdx === -1) {
1176
+ i++
1177
+ continue
764
1178
  }
1179
+ existingLinkOpen = tokens[refRemoveStart]
1180
+ existingLinkClose = tokens[linkCloseIdx]
1181
+ refRemoveCount = linkCloseIdx - refRemoveStart + 1
765
1182
  } else {
766
- merged.push(token)
1183
+ i++
1184
+ continue
767
1185
  }
768
- }
769
- while (markIndex < marks.length) {
770
- merged.push(marks[markIndex++])
771
- }
772
- return merged
773
- }
774
-
775
- const strongJa = (state, silent, opt) => {
776
- if (silent) return false
777
- const start = state.pos
778
- let max = state.posMax
779
- const src = state.src
780
- let attributesSrc
781
- if (start > max) return false
782
- if (src.charCodeAt(start) !== CHAR_ASTERISK) return false
783
- if (hasBackslash(state, start)) return false
784
-
785
- if (opt.mditAttrs) {
786
- attributesSrc = src.match(/((\n)? *){([^{}\n!@#%^&*()]+?)} *$/)
787
- if (attributesSrc && attributesSrc[3] !== '.') {
788
- max = src.slice(0, attributesSrc.index).length
789
- if (attributesSrc[2] === '\n') {
790
- max = src.slice(0, attributesSrc.index - 1).length
1186
+ if (process.env.DEBUG_COLLAPSED === '1') {
1187
+ const context = tokens.slice(Math.max(0, i - 2), Math.min(tokens.length, closeIdx + 3))
1188
+ console.log('[collapsed-ref] context:',
1189
+ context.map((t) => t.type + ':' + (t.content || '')))
1190
+ }
1191
+ let linkOpenToken = null
1192
+ let linkCloseToken = null
1193
+ if (existingLinkOpen && existingLinkClose) {
1194
+ if (whitespaceCount > 0) {
1195
+ tokens.splice(whitespaceStart, whitespaceCount)
1196
+ refRemoveStart -= whitespaceCount
791
1197
  }
792
- if(hasBackslash(state, attributesSrc.index) && attributesSrc[2] === '' && attributesSrc[1].length === 0) {
793
- max = state.posMax
1198
+ if (refRemoveCount > 0) {
1199
+ tokens.splice(refRemoveStart, refRemoveCount)
794
1200
  }
1201
+ linkOpenToken = existingLinkOpen
1202
+ linkCloseToken = existingLinkClose
795
1203
  } else {
796
- let endCurlyKet = src.match(/(\n *){([^{}\n!@#%^&*()]*?)}.*(} *?)$/)
797
- if (endCurlyKet) {
798
- max -= endCurlyKet[3].length
1204
+ if (!refKey) {
1205
+ i++
1206
+ continue
799
1207
  }
1208
+ const ref = references[refKey]
1209
+ if (!ref) {
1210
+ i++
1211
+ continue
1212
+ }
1213
+ if (whitespaceCount > 0) {
1214
+ tokens.splice(whitespaceStart, whitespaceCount)
1215
+ refRemoveStart -= whitespaceCount
1216
+ }
1217
+ if (refRemoveCount > 0) {
1218
+ tokens.splice(refRemoveStart, refRemoveCount)
1219
+ }
1220
+ linkOpenToken = new Token('link_open', 'a', 1)
1221
+ linkOpenToken.attrs = [['href', ref.href]]
1222
+ if (ref.title) linkOpenToken.attrPush(['title', ref.title])
1223
+ linkOpenToken.markup = '[]'
1224
+ linkOpenToken.info = 'auto'
1225
+ linkCloseToken = new Token('link_close', 'a', -1)
1226
+ linkCloseToken.markup = '[]'
1227
+ linkCloseToken.info = 'auto'
800
1228
  }
801
- }
802
-
803
- let inlines = createInlines(state, start, max, opt)
804
-
805
- const memo = {
806
- html: state.md.options.html,
807
- htmlTags: {},
808
- inlineMarkStart: src.charCodeAt(0) === CHAR_ASTERISK,
809
- inlineMarkEnd: src.charCodeAt(max - 1) === CHAR_ASTERISK,
810
- }
811
-
812
- let marks = createMarks(state, inlines, 0, inlines.length, memo, opt)
813
-
814
- inlines = mergeInlinesAndMarks(inlines, marks)
815
-
816
- setToken(state, inlines, opt)
817
-
818
- if (opt.mditAttrs && max !== state.posMax) {
819
- if (!attributesSrc) {
820
- state.pos = max
821
- return true
822
- }
823
- if (attributesSrc[1].length > 1) {
824
- state.pos = max + attributesSrc[1].length
825
- } else {
826
- state.pos = max
1229
+ tokens.splice(closeIdx, 1)
1230
+ tokens.splice(i, 1)
1231
+
1232
+ let labelStartIdx = i
1233
+ let labelEndIdx = i + labelTokens.length - 1
1234
+ if (labelStartIdx > labelEndIdx) {
1235
+ i++
1236
+ continue
1237
+ }
1238
+
1239
+ const wrapperPairs = []
1240
+ while (labelStartIdx > 0) {
1241
+ const prevToken = tokens[labelStartIdx - 1]
1242
+ const nextToken = tokens[labelEndIdx + 1]
1243
+ if (!prevToken || !nextToken) break
1244
+ if (!/_close$/.test(prevToken.type)) break
1245
+ const expectedOpen = prevToken.type.replace('_close', '_open')
1246
+ if (nextToken.type !== expectedOpen) break
1247
+ if (process.env.DEBUG_COLLAPSED === '1') {
1248
+ console.log('[collapsed-ref] wrapper pair:', prevToken.type, nextToken.type)
1249
+ }
1250
+ wrapperPairs.push({
1251
+ base: prevToken.type.replace('_close', ''),
1252
+ tag: prevToken.tag,
1253
+ markup: prevToken.markup
1254
+ })
1255
+ tokens.splice(labelEndIdx + 1, 1)
1256
+ tokens.splice(labelStartIdx - 1, 1)
1257
+ labelStartIdx -= 1
1258
+ labelEndIdx -= 1
1259
+ }
1260
+
1261
+ if (labelStartIdx > labelEndIdx) {
1262
+ i++
1263
+ continue
1264
+ }
1265
+
1266
+ let labelLength = labelEndIdx - labelStartIdx + 1
1267
+ const firstLabelToken = tokens[labelStartIdx]
1268
+ const linkLevel = firstLabelToken ? Math.max(firstLabelToken.level - 1, 0) : 0
1269
+ linkOpenToken.level = linkLevel
1270
+ linkCloseToken.level = linkLevel
1271
+ tokens.splice(labelStartIdx, 0, linkOpenToken)
1272
+ tokens.splice(labelStartIdx + labelLength + 1, 0, linkCloseToken)
1273
+
1274
+ adjustTokenLevels(tokens, labelStartIdx + 1, labelStartIdx + labelLength + 1, 1)
1275
+
1276
+ if (wrapperPairs.length > 0) {
1277
+ let insertIdx = labelStartIdx + 1
1278
+ for (let wp = 0; wp < wrapperPairs.length; wp++) {
1279
+ const pair = wrapperPairs[wp]
1280
+ const innerOpen = new Token(pair.base + '_open', pair.tag, 1)
1281
+ innerOpen.markup = pair.markup
1282
+ innerOpen.level = linkLevel + 1 + wp
1283
+ tokens.splice(insertIdx, 0, innerOpen)
1284
+ insertIdx++
1285
+ labelLength++
1286
+ }
1287
+ let linkClosePos = labelStartIdx + labelLength + 1
1288
+ for (let wp = wrapperPairs.length - 1; wp >= 0; wp--) {
1289
+ const pair = wrapperPairs[wp]
1290
+ const innerClose = new Token(pair.base + '_close', pair.tag, -1)
1291
+ innerClose.markup = pair.markup
1292
+ innerClose.level = linkLevel + 1 + wp
1293
+ tokens.splice(linkClosePos, 0, innerClose)
1294
+ labelLength++
1295
+ }
1296
+ }
1297
+
1298
+ i = labelStartIdx + labelLength + 2
1299
+ }
1300
+ }
1301
+
1302
+ // Link cleanup helpers
1303
+ const mergeBrokenMarksAroundLinks = (tokens) => {
1304
+ let i = 0
1305
+ while (i < tokens.length) {
1306
+ const closeToken = tokens[i]
1307
+ if (!closeToken || !/_close$/.test(closeToken.type)) {
1308
+ i++
1309
+ continue
1310
+ }
1311
+ const openType = closeToken.type.replace('_close', '_open')
1312
+ let j = i + 1
1313
+ while (j < tokens.length && isWhitespaceToken(tokens[j])) j++
1314
+ if (j >= tokens.length || tokens[j].type !== 'link_open') {
1315
+ i++
1316
+ continue
1317
+ }
1318
+ let linkDepth = 1
1319
+ j++
1320
+ while (j < tokens.length && linkDepth > 0) {
1321
+ if (tokens[j].type === 'link_open') linkDepth++
1322
+ if (tokens[j].type === 'link_close') linkDepth--
1323
+ j++
1324
+ }
1325
+ if (linkDepth !== 0) {
1326
+ i++
1327
+ continue
1328
+ }
1329
+ while (j < tokens.length && isWhitespaceToken(tokens[j])) j++
1330
+ if (j >= tokens.length) {
1331
+ i++
1332
+ continue
1333
+ }
1334
+ const reopenToken = tokens[j]
1335
+ if (reopenToken.type !== openType || reopenToken.level !== closeToken.level) {
1336
+ i++
1337
+ continue
1338
+ }
1339
+ tokens.splice(j, 1)
1340
+ tokens.splice(i, 1)
1341
+ }
1342
+ }
1343
+
1344
+
1345
+ const mditStrongJa = (md, option) => {
1346
+ const opt = {
1347
+ dollarMath: true, //inline math $...$
1348
+ mditAttrs: true, //markdown-it-attrs
1349
+ mdBreaks: md.options.breaks,
1350
+ disallowMixed: false, //Non-Japanese text handling
1351
+ }
1352
+ if (option) Object.assign(opt, option)
1353
+
1354
+ md.inline.ruler.before('emphasis', 'strong_ja', (state, silent) => {
1355
+ return strongJa(state, silent, opt)
1356
+ })
1357
+
1358
+ md.core.ruler.after('inline', 'strong_ja_collapsed_refs', (state) => {
1359
+ const targets = state.env.__strongJaCollapsedTargets
1360
+ if (!targets || targets.length === 0) return
1361
+ for (const tokens of targets) {
1362
+ if (!tokens || !tokens.length) continue
1363
+ convertCollapsedReferenceLinks(tokens, state)
1364
+ mergeBrokenMarksAroundLinks(tokens)
827
1365
  }
828
- } else {
829
- state.pos = max
830
- }
831
- return true
832
- }
833
-
834
- const mditStrongJa = (md, option) => {
835
- const opt = {
836
- dollarMath: true, //inline math $...$
837
- mditAttrs: true, //markdown-it-attrs
838
- mdBreaks: md.options.breaks,
839
- disallowMixed: false, //Non-Japanese text handling
840
- }
841
- if (option) Object.assign(opt, option)
842
-
843
- md.inline.ruler.before('emphasis', 'strong_ja', (state, silent) => {
844
- return strongJa(state, silent, opt)
1366
+ delete state.env.__strongJaCollapsedTargets
1367
+ delete state.env.__strongJaCollapsedTargetSet
845
1368
  })
846
- }
847
-
848
- export default mditStrongJa
1369
+ }
1370
+
1371
+ export default mditStrongJa