@peaceroad/markdown-it-strong-ja 0.4.4 → 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 +1301 -713
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -1,783 +1,1371 @@
1
- // Character code constants
2
- const CHAR_ASTERISK = 0x2A // *
3
- const CHAR_UNDERSCORE = 0x5F // _
4
- const CHAR_BACKSLASH = 0x5C // \
5
- const CHAR_BACKTICK = 0x60 // `
6
- const CHAR_DOLLAR = 0x24 // $
7
- const CHAR_LT = 0x3C // <
8
- const CHAR_GT = 0x3E // >
9
- const CHAR_SLASH = 0x2F // /
10
- const CHAR_SPACE = 0x20 // ' ' (space)
11
-
12
- const REG_ASTERISKS = /^\*+$/
13
- const REG_ATTRS = /{[^{}\n!@#%^&*()]+?}$/
14
- const REG_PUNCTUATION = /[!-/:-@[-`{-~ ]/
15
- 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 // ひらがな|カタカナ|漢字|句読点|記号|フォーマット文字|絵文字
16
-
17
- const REG_MARKDOWN_HTML = /^\[[^\[\]]+\]\([^)]+\)$|^<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([^<]+<\/\1>)$|^`[^`]+`$|^\$[^$]+\$$/ // for mixed-language context detection
18
-
19
- const hasBackslash = (state, start) => {
20
- let slashNum = 0
21
- let i = start - 1
22
- const src = state.src
23
- while(i >= 0) {
24
- if (src.charCodeAt(i) === CHAR_BACKSLASH) { slashNum++; i--; continue }
25
- break
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
+ }
26
73
  }
27
- return slashNum % 2 === 1
74
+ if (cache) cache.set(pos, -1)
75
+ return -1
28
76
  }
29
-
30
- const setToken = (state, inlines, opt) => {
77
+
78
+ // Detect reference-link label ranges within the current inline slice
79
+ const computeReferenceRanges = (state, start, max) => {
31
80
  const src = state.src
32
- let i = 0
33
- let attrsIsText = {
34
- val: false,
35
- tag: '',
36
- }
37
- while (i < inlines.length) {
38
- let type = inlines[i].type
39
- const tag = type.replace(/(?:_open|_close)$/, '')
40
-
41
- if (/_open$/.test(type)) {
42
- const startToken = state.push(type, tag, 1)
43
- startToken.markup = tag === 'strong' ? '**' : '*'
44
- attrsIsText = {
45
- val: true,
46
- tag: tag,
47
- }
48
- }
49
-
50
- if (type === 'html_inline') {
51
- type = 'text'
52
- }
53
- if (type === 'text') {
54
- let content = src.slice(inlines[i].s, inlines[i].e + 1)
55
- if (REG_ASTERISKS.test(content)) {
56
- const asteriskToken = state.push(type, '', 0)
57
- asteriskToken.content = content
58
- i++
59
- continue
60
- }
61
- if (opt.mditAttrs && attrsIsText.val && i + 1 < inlines.length) {
62
- const hasImmediatelyAfterAsteriskClose = inlines[i+1].type === attrsIsText.tag + '_close'
63
- if (hasImmediatelyAfterAsteriskClose && REG_ATTRS.test(content)) {
64
- const attrsToken = state.push(type, '', 0)
65
-
66
- const hasBackslashBeforeCurlyAttribute = content.match(/(\\+){/)
67
- if (hasBackslashBeforeCurlyAttribute) {
68
- if (hasBackslashBeforeCurlyAttribute[1].length === 1) {
69
- attrsToken.content = content.replace(/\\{/, '{')
70
- } else {
71
- let backSlashNum = Math.floor(hasBackslashBeforeCurlyAttribute[1].length / 2)
72
- let k = 0
73
- let backSlash = ''
74
- while (k < backSlashNum) {
75
- backSlash += '\\'
76
- 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]
77
105
  }
78
- attrsToken.content = content.replace(/\\+{/, backSlash + '{')
79
106
  }
80
- } else {
81
- attrsToken.content = content
82
- }
83
- attrsIsText.val = false
84
- i++
85
- continue
86
- }
87
- }
88
-
89
- const childTokens = state.md.parseInline(content, state.env)
90
- if (childTokens[0] && childTokens[0].children) {
91
- let j = 0
92
- while (j < childTokens[0].children.length) {
93
- const t = childTokens[0].children[j]
94
- if (t.type === 'softbreak' && !opt.mdBreaks) {
95
- t.type = 'text'
96
- t.tag = ''
97
- t.content = '\n'
98
- }
99
- if (!opt.mditAttrs && t.tag === 'br') {
100
- t.tag = ''
101
- 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
102
113
  }
103
- const token = state.push(t.type, t.tag, t.nesting)
104
- token.attrs = t.attrs
105
- token.map = t.map
106
- token.level = t.level
107
- token.children = t.children
108
- token.content = t.content
109
- token.markup = t.markup
110
- token.info = t.info
111
- token.meta = t.meta
112
- token.block = t.block
113
- token.hidden = t.hidden
114
- j++
115
114
  }
116
115
  }
117
116
  }
118
-
119
- if (/_close$/.test(type)) {
120
- const closeToken = state.push(type, tag, -1)
121
- closeToken.markup = tag === 'strong' ? '**' : '*'
122
- attrsIsText = {
123
- val: false,
124
- tag: '',
125
- }
126
- }
127
-
128
- i++
117
+ pos++
129
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
130
136
  }
131
137
 
132
- const pushInlines = (inlines, s, e, len, type, tag, tagType) => {
133
- const inline = {
134
- s: s,
135
- sp: s,
136
- e: e,
137
- ep: e,
138
- len: len,
139
- type: type,
140
- check: type === 'text',
138
+ const inlineHasCollapsedRef = (state) => {
139
+ if (state.__strongJaHasCollapsedRefs === undefined) {
140
+ state.__strongJaHasCollapsedRefs = /\[[^\]]*\]\s*\[[^\]]*\]/.test(state.src)
141
141
  }
142
- if (tag) inline.tag = [tag, tagType]
143
- inlines.push(inline)
142
+ return state.__strongJaHasCollapsedRefs
144
143
  }
145
144
 
146
- const hasNextSymbol = (state, n, max, symbol, noMark) => {
147
- let nextSymbolPos = -1
148
- const src = state.src
149
- if (src.charCodeAt(n) === symbol && !hasBackslash(state, n)) {
150
- for (let i = n + 1; i < max; i++) {
151
- noMark += src[i]
152
- if (src.charCodeAt(i) === symbol && !hasBackslash(state, i)) {
153
- noMark += src.substring(n, i + 1)
154
- nextSymbolPos = i
155
- break
156
- }
157
- }
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
158
158
  }
159
- return [nextSymbolPos, noMark]
159
+ targets.push(state.tokens)
160
160
  }
161
161
 
162
- const createInlines = (state, start, max, opt) => {
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) => {
163
270
  const src = state.src
164
- const srcLen = max
165
- const htmlEnabled = state.md.options.html
166
- let n = start
167
- let inlines = []
168
- let noMark = ''
169
- let textStart = n
170
-
171
- // Infinite loop prevention
172
- const maxIterations = srcLen * 2 // Safe upper bound
173
- let iterations = 0
174
-
175
- while (n < srcLen) {
176
- // Prevent infinite loops
177
- iterations++
178
- if (iterations > maxIterations) {
179
- // Add remaining text as-is and exit safely
180
- if (textStart < srcLen) {
181
- pushInlines(inlines, textStart, srcLen - 1, srcLen - textStart, 'text')
182
- }
183
- break
184
- }
185
-
186
- const currentChar = src.charCodeAt(n)
187
- let nextSymbolPos = -1
188
-
189
- // Inline code (backticks)
190
- if (currentChar === CHAR_BACKTICK && !hasBackslash(state, n)) {
191
- [nextSymbolPos, noMark] = hasNextSymbol(state, n, srcLen, CHAR_BACKTICK, noMark)
192
- if (nextSymbolPos !== -1) {
193
- if (nextSymbolPos === srcLen - 1) {
194
- pushInlines(inlines, textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
195
- break
196
- }
197
- n = nextSymbolPos + 1
198
- continue
199
- }
200
- }
201
-
202
- // Inline math ($...$)
203
- if (opt.dollarMath && currentChar === CHAR_DOLLAR && !hasBackslash(state, n)) {
204
- [nextSymbolPos, noMark] = hasNextSymbol(state, n, srcLen, CHAR_DOLLAR, noMark)
205
- if (nextSymbolPos !== -1) {
206
- if (nextSymbolPos === srcLen - 1) {
207
- pushInlines(inlines, textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
208
- break
209
- }
210
- n = nextSymbolPos + 1
211
- continue
212
- }
213
- }
214
-
215
- // HTML tags
216
- if (htmlEnabled && currentChar === CHAR_LT && !hasBackslash(state, n)) {
217
- let foundClosingTag = false
218
- for (let i = n + 1; i < srcLen; i++) {
219
- if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
220
- if (noMark.length !== 0) {
221
- pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
222
- noMark = ''
223
- }
224
- let tag = src.slice(n + 1, i)
225
- let tagType
226
- if (tag.charCodeAt(0) === CHAR_SLASH) {
227
- tag = tag.slice(1)
228
- tagType = 'close'
229
- } else {
230
- tagType = 'open'
231
- }
232
- pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
233
- textStart = i + 1
234
- n = i + 1
235
- foundClosingTag = true
236
- break
237
- }
238
- }
239
- if (foundClosingTag) {
240
- continue
241
- }
242
- // If no closing tag found, treat as regular character to prevent infinite loops
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
243
275
  }
244
-
245
- // Asterisk handling
246
- if (currentChar === CHAR_ASTERISK && !hasBackslash(state, n)) {
247
- if (n !== 0 && noMark.length !== 0) {
248
- pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
249
- noMark = ''
250
- }
251
- if (n === srcLen - 1) {
252
- pushInlines(inlines, n, n, 1, '')
253
- break
254
- }
255
- let i = n + 1
256
- while (i < srcLen && src.charCodeAt(i) === CHAR_ASTERISK) {
257
- i++
258
- }
259
- if (i === srcLen) {
260
- pushInlines(inlines, n, i - 1, i - n, '')
261
- } else {
262
- pushInlines(inlines, n, i - 1, i - n, '')
263
- textStart = i
264
- }
265
- n = i
266
- continue
267
- }
268
-
269
- // Regular character
270
- noMark += src[n]
271
- if (n === srcLen - 1) {
272
- pushInlines(inlines, textStart, n, n - textStart + 1, 'text')
273
- break
274
- }
275
- n++
276
276
  }
277
- return inlines
277
+ return -1
278
278
  }
279
279
 
280
- const pushMark = (marks, opts) => {
281
- let left = 0, right = marks.length
282
- while (left < right) {
283
- const mid = (left + right) >> 1
284
- if (marks[mid].s > opts.s) {
285
- right = mid
286
- } else {
287
- left = mid + 1
288
- }
280
+ const processSymbolPair = (state, n, srcLen, symbol, noMark, textStart, pushInlines) => {
281
+ const nextSymbolPos = findNextSymbolPos(state, n, srcLen, symbol)
282
+ if (nextSymbolPos === -1) {
283
+ return { shouldBreak: false, shouldContinue: false, newN: n, newNoMark: noMark }
289
284
  }
290
- marks.splice(left, 0, { ...opts });
291
- }
292
-
293
- const setStrong = (state, inlines, marks, n, memo, opt) => {
294
- if (opt.disallowMixed === true) {
295
- let i = n + 1
296
- const inlinesLength = inlines.length
297
- while (i < inlinesLength) {
298
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
299
- if (inlines[i].type !== '') { i++; continue }
300
-
301
- if (inlines[i].len > 1) {
302
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
303
- if (mixedCheck.shouldBlock) {
304
- return [n, 0]
305
- }
306
- break
307
- }
308
- i++
309
- }
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 }
310
292
  }
311
-
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)
312
476
  let i = n + 1
313
- let j = 0
314
- let nest = 0
315
- let insideTagsIsClose = 1
316
- const inlinesLength = inlines.length
317
- while (i < inlinesLength) {
318
- if (inlines[i].type !== '') { i++; continue }
319
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
320
- if (inlines[i].type === 'html_inline') {
321
- inlines[i].check = true
322
- insideTagsIsClose = checkInsideTags(inlines, i, memo)
323
- if (insideTagsIsClose === -1) return [n, nest]
324
- if (insideTagsIsClose === 0) { i++; continue }
325
- }
326
-
327
- nest = checkNest(inlines, marks, n, i)
328
- if (nest === -1) return [n, nest]
329
-
330
- if (inlines[i].len === 1 && inlines[n].len > 2) {
331
- pushMark(marks, {
332
- nest: nest,
333
- s: inlines[n].ep,
334
- e: inlines[n].ep,
335
- len: 1,
336
- oLen: inlines[n].len - 1,
337
- type: 'em_open'
338
- })
339
- pushMark(marks, {
340
- nest: nest,
341
- s: inlines[i].sp,
342
- e: inlines[i].ep,
343
- len: 1,
344
- oLen: inlines[i].len - 1,
345
- type: 'em_close'
346
- })
347
- inlines[n].len -= 1
348
- inlines[n].ep -= 1
349
- inlines[i].len -= 1
350
- if (inlines[i].len > 0) inlines[i].sp += 1
351
- if (insideTagsIsClose === 1) {
352
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt)
353
- n = newN
354
- nest = newNest
355
- }
356
- }
357
- let strongNum = Math.trunc(Math.min(inlines[n].len, inlines[i].len) / 2)
358
-
359
- if (inlines[i].len > 1) {
360
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt)) {
361
- if (memo.inlineMarkEnd) {
362
- marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt))
363
- if (inlines[i].len === 0) { i++; continue }
364
- } else {
365
- return [n, nest]
366
- }
367
- }
368
-
369
- j = 0
370
- while (j < strongNum) {
371
- pushMark(marks, {
372
- nest: nest + strongNum - 1 - j,
373
- s: inlines[n].ep - 1,
374
- e: inlines[n].ep,
375
- len: 2,
376
- oLen: inlines[n].len - 2,
377
- type: 'strong_open'
378
- })
379
- inlines[n].ep -= 2
380
- inlines[n].len -= 2
381
- pushMark(marks, {
382
- nest: nest + strongNum - 1 - j,
383
- s: inlines[i].sp,
384
- e: inlines[i].sp + 1,
385
- len: 2,
386
- oLen: inlines[i].len - 2,
387
- type: 'strong_close'
388
- })
389
- inlines[i].sp += 2
390
- inlines[i].len -= 2
391
- j++
392
- }
393
- if (inlines[n].len === 0) return [n, nest]
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) => {
663
+ const src = state.src
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
394
671
  }
395
-
396
- if (inlines[n].len === 1 && inlines[i].len > 0) {
397
- nest++
398
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, nest)
399
- n = newN
400
- nest = newNest
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
401
679
  }
402
-
403
- i++
404
680
  }
405
-
406
- if (n == 0 && memo.inlineMarkEnd) {
407
- marks.push(...createMarks(state, inlines, n + 1, inlinesLength - 1, memo, opt))
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
408
949
  }
409
- return [n, nest]
410
- }
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)
411
964
 
412
- const checkInsideTags = (inlines, i, memo) => {
413
- if (inlines[i].tag === undefined) return 0
414
- const tagName = inlines[i].tag[0].toLowerCase()
415
- if (memo.htmlTags[tagName] === undefined) {
416
- memo.htmlTags[tagName] = 0
417
- }
418
- if (inlines[i].tag[1] === 'open') {
419
- memo.htmlTags[tagName] += 1
420
- }
421
- if (inlines[i].tag[1] === 'close') {
422
- memo.htmlTags[tagName] -= 1
965
+ if (inlineHasCollapsedRef(state) && !state.__strongJaCollapsedRefRegistered) {
966
+ registerCollapsedRefTarget(state)
967
+ state.__strongJaCollapsedRefRegistered = true
423
968
  }
424
- if (memo.htmlTags[tagName] < 0) {
425
- return -1
969
+
970
+ if (opt.mditAttrs && max !== state.posMax) {
971
+ if (!attributesSrc) {
972
+ state.pos = max
973
+ return true
974
+ }
975
+ state.pos = attributesSrc[1].length > 1 ? max + attributesSrc[1].length : max
976
+ return true
426
977
  }
427
- const closeAllTags = Object.values(memo.htmlTags).every(val => val === 0)
428
- if (closeAllTags) return 1
429
- return 0
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(/[*_]+$/, '')
430
1001
  }
431
1002
 
432
- const isPunctuation = (ch) => {
433
- return REG_PUNCTUATION.test(ch)
434
- }
435
- const isJapanese = (ch) => {
436
- return REG_JAPANESE.test(ch)
1003
+ const normalizeReferenceCandidate = (state, text, { useClean = false } = {}) => {
1004
+ const source = useClean ? cleanLabelText(text) : text.replace(/\s+/g, ' ').trim()
1005
+ return normalizeRefKey(state, source)
437
1006
  }
438
1007
 
439
- const isEnglish = (ch) => {
440
- if (!ch) return false
441
- const code = ch.charCodeAt(0)
442
- if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)) {
443
- return true
444
- }
445
- if (code < 128) {
446
- return code === CHAR_SPACE || (code > 126)
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
447
1040
  }
448
- return !REG_JAPANESE.test(ch) && !REG_PUNCTUATION.test(ch)
449
- }
450
-
451
- const checkMixedLanguagePattern = (state, inlines, n, i, opt) => {
452
- const src = state.src
453
- const openPrevChar = src[inlines[n].s - 1] || ''
454
- const closeNextChar = src[inlines[i].e + 1] || ''
455
-
456
- const isEnglishPrefix = isEnglish(openPrevChar)
457
- const isEnglishSuffix = isEnglish(closeNextChar)
458
- if (!isEnglishPrefix && !isEnglishSuffix) {
459
- return { hasEnglishContext: false, hasMarkdownOrHtml: false, shouldBlock: false }
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 = ''
1049
+ }
1050
+ segments.push('[]')
1051
+ pos += 2
1052
+ continue
1053
+ }
1054
+ const ch = content[pos]
1055
+ if (ch === '[' || ch === ']') {
1056
+ if (buffer) {
1057
+ segments.push(buffer)
1058
+ buffer = ''
1059
+ }
1060
+ segments.push(ch)
1061
+ pos++
1062
+ continue
1063
+ }
1064
+ buffer += ch
1065
+ pos++
460
1066
  }
461
-
462
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
463
- const hasMarkdownOrHtml = REG_MARKDOWN_HTML.test(contentBetween)
464
-
465
- return {
466
- hasEnglishContext: true,
467
- hasMarkdownOrHtml,
468
- shouldBlock: hasMarkdownOrHtml
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++
469
1075
  }
1076
+ return true
1077
+ }
1078
+
1079
+ const isBracketToken = (token, bracket) => {
1080
+ return token && token.type === 'text' && token.content === bracket
470
1081
  }
471
1082
 
472
- const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt) => {
473
- const src = state.src
474
- const openPrevChar = src[inlines[n].s - 1] || ''
475
- const openNextChar = src[inlines[n].e + 1] || ''
476
- const checkOpenNextChar = isPunctuation(openNextChar)
477
- const closePrevChar = src[inlines[i].s - 1] || ''
478
- const checkClosePrevChar = isPunctuation(closePrevChar)
479
- const closeNextChar = src[inlines[i].e + 1] || ''
480
- const checkCloseNextChar = (isPunctuation(closeNextChar) || i === inlines.length - 1)
481
-
482
- if (opt.disallowMixed === false) {
483
- const openPrevChar = src[inlines[n].s - 1] || ''
484
- const closeNextChar = src[inlines[i].e + 1] || ''
485
-
486
- if (isEnglish(openPrevChar) || isEnglish(closeNextChar)) {
487
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
488
- if (REG_MARKDOWN_HTML.test(contentBetween)) {
489
- return false
490
- }
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
491
1091
  }
492
1092
  }
493
-
494
- const result = (checkOpenNextChar || checkClosePrevChar) && !checkCloseNextChar && !(isJapanese(openPrevChar) || isJapanese(closeNextChar))
495
- return result
1093
+ return -1
496
1094
  }
497
1095
 
498
- const setEm = (state, inlines, marks, n, memo, opt, sNest) => {
499
- if (opt.disallowMixed === true && !sNest) {
500
- let i = n + 1
501
- const inlinesLength = inlines.length
502
- while (i < inlinesLength) {
503
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
504
- if (inlines[i].type !== '') { i++; continue }
505
-
506
- if (inlines[i].len > 0) {
507
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
508
- if (mixedCheck.shouldBlock) {
509
- return [n, 0]
510
- }
1096
+ const convertCollapsedReferenceLinks = (tokens, state) => {
1097
+ const references = state.env && state.env.references
1098
+ if (!references || Object.keys(references).length === 0) return
1099
+
1100
+ let i = 0
1101
+ while (i < tokens.length) {
1102
+ if (splitBracketToken(tokens, i)) {
1103
+ continue
1104
+ }
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
511
1116
  break
512
1117
  }
1118
+ closeIdx++
1119
+ }
1120
+ if (closeIdx === -1 || closeIdx >= tokens.length) {
1121
+ i++
1122
+ continue
1123
+ }
1124
+
1125
+ if (closeIdx === i + 1) {
513
1126
  i++
1127
+ continue
514
1128
  }
515
- }
516
-
517
- let i = n + 1
518
- let nest = 0
519
- let strongPNum = 0
520
- let insideTagsIsClose = 1
521
- const inlinesLength = inlines.length
522
- while (i < inlinesLength) {
523
- if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
524
- if (!sNest && inlines[i].type === 'html_inline') {
525
- inlines.check = true
526
- insideTagsIsClose = checkInsideTags(inlines, i, memo)
527
- if (insideTagsIsClose === -1) return [n, nest]
528
- if (insideTagsIsClose === 0) { i++; continue }
529
- }
530
- if (inlines[i].type !== '') { i++; continue }
531
-
532
- const emNum = Math.min(inlines[n].len, inlines[i].len)
533
1129
 
534
- if (!sNest && emNum !== 1) return [n, sNest, memo]
535
-
536
- const hasMarkersAtStartAndEnd = (i) => {
537
- let flag = memo.inlineMarkStart
538
- if (!flag) return false
539
- inlinesLength - 1 === i ? flag = true : flag = false
540
- if (!flag) return false
541
- inlines[i].len > 1 ? flag = true : flag = false
542
- 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++
543
1137
  }
544
- if (!sNest && inlines[i].len === 2 && !hasMarkersAtStartAndEnd(i)) {
545
- strongPNum++
546
- i++
1138
+ if (splitBracketToken(tokens, refRemoveStart)) {
547
1139
  continue
548
1140
  }
549
-
550
- if (sNest) {
551
- nest = sNest - 1
552
- } else {
553
- nest = checkNest(inlines, marks, n, i)
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))
554
1151
  }
555
- if (nest === -1) return [n, nest]
556
-
557
- if (emNum === 1) {
558
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt)) {
559
- if (memo.inlineMarkEnd) {
560
- marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt))
561
1152
 
562
- if (inlines[i].len === 0) { i++; continue }
563
- } else {
564
- return [n, nest]
565
- }
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++
566
1160
  }
567
- if (inlines[i].len < 1) {
568
- i++; continue;
1161
+ if (refCloseIdx >= tokens.length) {
1162
+ i++
1163
+ continue
569
1164
  }
570
-
571
- pushMark(marks, {
572
- nest: nest,
573
- s: inlines[n].ep,
574
- e: inlines[n].ep,
575
- len: 1,
576
- oLen: inlines[n].len - 1,
577
- type: 'em_open'
578
- })
579
- inlines[n].ep -= 1
580
- inlines[n].len -= 1
581
-
582
- if (strongPNum % 2 === 0 || inlines[i].len < 2) {
583
- pushMark(marks, {
584
- nest: nest,
585
- s: inlines[i].sp,
586
- e: inlines[i].sp,
587
- len: 1,
588
- oLen: inlines[i].len - 1,
589
- type: 'em_close'
590
- })
591
- inlines[i].sp += 1
1165
+ const refTokens = tokens.slice(refRemoveStart + 1, refCloseIdx)
1166
+ if (refTokens.length === 0) {
1167
+ refKey = normalizeReferenceCandidate(state, cleanedLabel)
592
1168
  } else {
593
- pushMark(marks, {
594
- nest: nest,
595
- s: inlines[i].ep,
596
- e: inlines[i].ep,
597
- len: 1,
598
- oLen: inlines[i].len - 1,
599
- type: 'em_close'
600
- })
601
- inlines[i].sp = inlines[i].ep - 1
602
- inlines[i].ep -= 1
1169
+ const refLabelText = buildReferenceLabel(refTokens)
1170
+ refKey = normalizeReferenceCandidate(state, refLabelText)
603
1171
  }
604
- inlines[i].len -= 1
605
- if (inlines[n].len === 0) return [n, nest]
606
- }
607
-
608
- i++
609
- }
610
- return [n, nest]
611
- }
612
-
613
- const setText = (inlines, marks, n, nest) => {
614
- pushMark(marks, {
615
- nest: nest,
616
- s: inlines[n].sp,
617
- e: inlines[n].ep,
618
- len: inlines[n].len,
619
- oLen: -1,
620
- type: 'text'
621
- })
622
- inlines[n].len = 0
623
- }
624
-
625
- const checkNest = (inlines, marks, n, i) => {
626
- let nest = 1
627
- let isRange = true
628
- if (marks.length === 0) return nest
629
- let strongNest = 0
630
- let emNest = 0
631
- let j = 0
632
- const marksLength = marks.length
633
- while (j < marksLength) {
634
- if (marks[j].s <= inlines[n].s) {
635
- if (marks[j].type === 'strong_open') strongNest++
636
- if (marks[j].type === 'strong_close') strongNest--
637
- if (marks[j].type === 'em_open') emNest++
638
- if (marks[j].type === 'em_close') emNest--
639
- } else { break }
640
- j++
641
- }
642
- let parentNest = strongNest + emNest
643
- let parentCloseN = j
644
- if (parentCloseN < marksLength) {
645
- while (parentCloseN < marksLength) {
646
- if (marks[parentCloseN].nest === parentNest) break
647
- parentCloseN++
648
- }
649
- if (parentCloseN > marksLength - 1) {
650
- isRange = true
651
- } else {
652
- if (marks[parentCloseN].s < inlines[i].s) isRange = false
653
- }
654
- }
655
-
656
- if (isRange) {
657
- nest = parentNest + 1
658
- } else {
659
- nest = -1
660
- }
661
- return nest
662
- }
663
-
664
- const createMarks = (state, inlines, start, end, memo, opt) => {
665
- let marks = []
666
- let n = start
667
-
668
- while (n < end) {
669
- if (inlines[n].type !== '') { n++; continue }
670
- let nest = 0
671
-
672
- if (inlines[n].len > 1) {
673
- const [newN, newNest] = setStrong(state, inlines, marks, n, memo, opt)
674
- n = newN
675
- nest = newNest
676
- }
677
- if (inlines[n].len !== 0) {
678
- const [newN2, newNest2] = setEm(state, inlines, marks, n, memo, opt)
679
- n = newN2
680
- nest = newNest2
681
- }
682
- if (inlines[n].len !== 0) {
683
- setText(inlines, marks, n, nest)
684
- }
685
- n++
686
- }
687
- return marks
688
- }
689
-
690
-
691
- const mergeInlinesAndMarks = (inlines, marks) => {
692
- marks.sort((a, b) => a.s - b.s)
693
- const merged = []
694
- let markIndex = 0
695
- for (const token of inlines) {
696
- if (token.type === '') {
697
- while (markIndex < marks.length && marks[markIndex].s >= token.s && marks[markIndex].e <= token.e) {
698
- merged.push(marks[markIndex])
699
- 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
700
1178
  }
1179
+ existingLinkOpen = tokens[refRemoveStart]
1180
+ existingLinkClose = tokens[linkCloseIdx]
1181
+ refRemoveCount = linkCloseIdx - refRemoveStart + 1
701
1182
  } else {
702
- merged.push(token)
1183
+ i++
1184
+ continue
703
1185
  }
704
- }
705
- while (markIndex < marks.length) {
706
- merged.push(marks[markIndex++])
707
- }
708
- return merged
709
- }
710
-
711
- const strongJa = (state, silent, opt) => {
712
- if (silent) return false
713
- const start = state.pos
714
- let max = state.posMax
715
- const src = state.src
716
- let attributesSrc
717
- if (start > max) return false
718
- if (src.charCodeAt(start) !== CHAR_ASTERISK) return false
719
- if (hasBackslash(state, start)) return false
720
-
721
- if (opt.mditAttrs) {
722
- attributesSrc = src.match(/((\n)? *){([^{}\n!@#%^&*()]+?)} *$/)
723
- if (attributesSrc && attributesSrc[3] !== '.') {
724
- max = src.slice(0, attributesSrc.index).length
725
- if (attributesSrc[2] === '\n') {
726
- 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
727
1197
  }
728
- if(hasBackslash(state, attributesSrc.index) && attributesSrc[2] === '' && attributesSrc[1].length === 0) {
729
- max = state.posMax
1198
+ if (refRemoveCount > 0) {
1199
+ tokens.splice(refRemoveStart, refRemoveCount)
730
1200
  }
1201
+ linkOpenToken = existingLinkOpen
1202
+ linkCloseToken = existingLinkClose
731
1203
  } else {
732
- let endCurlyKet = src.match(/(\n *){([^{}\n!@#%^&*()]*?)}.*(} *?)$/)
733
- if (endCurlyKet) {
734
- max -= endCurlyKet[3].length
1204
+ if (!refKey) {
1205
+ i++
1206
+ continue
735
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'
736
1228
  }
737
- }
738
-
739
- let inlines = createInlines(state, start, max, opt)
740
-
741
- const memo = {
742
- html: state.md.options.html,
743
- htmlTags: {},
744
- inlineMarkStart: src.charCodeAt(0) === CHAR_ASTERISK,
745
- inlineMarkEnd: src.charCodeAt(max - 1) === CHAR_ASTERISK,
746
- }
747
-
748
- let marks = createMarks(state, inlines, 0, inlines.length, memo, opt)
749
-
750
- inlines = mergeInlinesAndMarks(inlines, marks)
751
-
752
- setToken(state, inlines, opt)
753
-
754
- if (opt.mditAttrs && max !== state.posMax) {
755
- if (!attributesSrc) {
756
- state.pos = max
757
- return true
758
- }
759
- if (attributesSrc[1].length > 1) {
760
- state.pos = max + attributesSrc[1].length
761
- } else {
762
- 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)
763
1365
  }
764
- } else {
765
- state.pos = max
766
- }
767
- return true
768
- }
769
-
770
- const mditStrongJa = (md, option) => {
771
- const opt = {
772
- dollarMath: true, //inline math $...$
773
- mditAttrs: true, //markdown-it-attrs
774
- mdBreaks: md.options.breaks,
775
- disallowMixed: false, //Non-Japanese text handling
776
- }
777
- if (option) Object.assign(opt, option)
778
-
779
- md.inline.ruler.before('emphasis', 'strong_ja', (state, silent) => {
780
- return strongJa(state, silent, opt)
1366
+ delete state.env.__strongJaCollapsedTargets
1367
+ delete state.env.__strongJaCollapsedTargetSet
781
1368
  })
782
- }
783
- export default mditStrongJa
1369
+ }
1370
+
1371
+ export default mditStrongJa