@peaceroad/markdown-it-strong-ja 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,54 @@
1
1
  import Token from 'markdown-it/lib/token.mjs'
2
- import { parseLinkDestination, parseLinkTitle } from 'markdown-it/lib/helpers/index.mjs'
3
- import { isSpace, isWhiteSpace } from 'markdown-it/lib/common/utils.mjs'
2
+ import { isWhiteSpace } from 'markdown-it/lib/common/utils.mjs'
3
+ import { getReferenceCount } from './token-utils.js'
4
4
 
5
5
  const CHAR_OPEN_BRACKET = 0x5B // [
6
6
  const CHAR_CLOSE_BRACKET = 0x5D // ]
7
- const CHAR_OPEN_PAREN = 0x28 // (
8
- const CHAR_CLOSE_PAREN = 0x29 // )
9
7
 
10
8
  const isWhitespaceToken = (token) => {
11
9
  if (!token || token.type !== 'text') return false
12
10
  const content = token.content
13
- if (!content) return true
11
+ if (token.__strongJaWhitespaceSource === content &&
12
+ typeof token.__strongJaIsWhitespace === 'boolean') {
13
+ return token.__strongJaIsWhitespace
14
+ }
15
+ if (!content) {
16
+ token.__strongJaWhitespaceSource = content
17
+ token.__strongJaIsWhitespace = true
18
+ return true
19
+ }
20
+ let isWhitespace = true
14
21
  for (let i = 0; i < content.length; i++) {
15
- if (!isWhiteSpace(content.charCodeAt(i))) return false
22
+ if (!isWhiteSpace(content.charCodeAt(i))) {
23
+ isWhitespace = false
24
+ break
25
+ }
16
26
  }
17
- return true
27
+ token.__strongJaWhitespaceSource = content
28
+ token.__strongJaIsWhitespace = isWhitespace
29
+ return isWhitespace
30
+ }
31
+
32
+ const hasReferenceLabelMarkerRange = (tokens, startIdx, endIdx) => {
33
+ if (startIdx > endIdx) return false
34
+ for (let idx = startIdx; idx <= endIdx; idx++) {
35
+ const token = tokens[idx]
36
+ if (!token || !token.type) continue
37
+ if (token.type === 'text' || token.type === 'code_inline') {
38
+ const content = token.content
39
+ if (content && (content.indexOf('*') !== -1 || content.indexOf('_') !== -1)) return true
40
+ continue
41
+ }
42
+ if (token.type === 'softbreak' || token.type === 'hardbreak') continue
43
+ if (token.markup &&
44
+ (token.type.endsWith('_open') || token.type.endsWith('_close')) &&
45
+ (token.markup.indexOf('*') !== -1 || token.markup.indexOf('_') !== -1)) {
46
+ return true
47
+ }
48
+ }
49
+ return false
18
50
  }
19
51
 
20
- // Collapsed reference helpers
21
52
  const buildReferenceLabelRange = (tokens, startIdx, endIdx) => {
22
53
  if (startIdx > endIdx) return ''
23
54
  let label = ''
@@ -28,23 +59,15 @@ const buildReferenceLabelRange = (tokens, startIdx, endIdx) => {
28
59
  label += token.content
29
60
  } else if (token.type === 'softbreak' || token.type === 'hardbreak') {
30
61
  label += ' '
31
- } else if (token.type && token.type.endsWith('_open') && token.markup) {
32
- label += token.markup
33
- } else if (token.type && token.type.endsWith('_close') && token.markup) {
62
+ } else if (token.type && token.markup && (token.type.endsWith('_open') || token.type.endsWith('_close'))) {
34
63
  label += token.markup
35
64
  }
36
65
  }
37
66
  return label
38
67
  }
39
68
 
40
- const cleanLabelText = (label) => {
41
- if (label.indexOf('*') === -1 && label.indexOf('_') === -1) return label
42
- return label.replace(/^[*_]+/, '').replace(/[*_]+$/, '')
43
- }
44
-
45
- const normalizeReferenceCandidate = (state, text, { useClean = false } = {}) => {
46
- const source = useClean ? cleanLabelText(text) : text
47
- return normalizeRefKey(state, source)
69
+ const normalizeReferenceCandidate = (state, text) => {
70
+ return getNormalizeRef(state)(text)
48
71
  }
49
72
 
50
73
  const getNormalizeRef = (state) => {
@@ -56,15 +79,6 @@ const getNormalizeRef = (state) => {
56
79
  return normalize
57
80
  }
58
81
 
59
- const normalizeRefKey = (state, label) => {
60
- return getNormalizeRef(state)(label)
61
- }
62
-
63
- const adjustTokenLevels = (tokens, startIdx, endIdx, delta) => {
64
- for (let i = startIdx; i < endIdx; i++) {
65
- if (tokens[i]) tokens[i].level += delta
66
- }
67
- }
68
82
 
69
83
  const cloneMap = (map) => {
70
84
  if (!map || !Array.isArray(map)) return null
@@ -102,12 +116,23 @@ const cloneTextToken = (source, content) => {
102
116
  Object.assign(newToken, source)
103
117
  newToken.content = content
104
118
  if (source.meta) newToken.meta = { ...source.meta }
105
- if (source.map) newToken.map = source.map
106
119
  return newToken
107
120
  }
108
121
 
109
- // Split only text tokens that actually contain bracket characters
110
- const splitBracketToken = (tokens, index, options) => {
122
+ const applyBracketSegmentFlags = (token, seg) => {
123
+ if (seg === '[' || seg === ']') {
124
+ token.__strongJaHasBracket = true
125
+ token.__strongJaBracketAtomic = true
126
+ } else if (seg === '[]') {
127
+ token.__strongJaHasBracket = true
128
+ token.__strongJaBracketAtomic = true
129
+ } else {
130
+ token.__strongJaHasBracket = false
131
+ token.__strongJaBracketAtomic = false
132
+ }
133
+ }
134
+
135
+ const splitBracketToken = (tokens, index) => {
111
136
  const token = tokens[index]
112
137
  if (!token || token.type !== 'text') return false
113
138
  if (token.__strongJaBracketAtomic) return false
@@ -126,82 +151,49 @@ const splitBracketToken = (tokens, index, options) => {
126
151
  }
127
152
  token.__strongJaHasBracket = true
128
153
  }
129
- const splitEmptyPair = options && options.splitEmptyPair
130
154
  const segments = []
131
- let buffer = ''
155
+ const contentLen = content.length
132
156
  let pos = 0
133
- while (pos < content.length) {
134
- if (!splitEmptyPair &&
135
- content.charCodeAt(pos) === CHAR_OPEN_BRACKET &&
157
+ let segmentStart = 0
158
+ while (pos < contentLen) {
159
+ const code = content.charCodeAt(pos)
160
+ if (code === CHAR_OPEN_BRACKET &&
136
161
  content.charCodeAt(pos + 1) === CHAR_CLOSE_BRACKET) {
137
- if (buffer) {
138
- segments.push(buffer)
139
- buffer = ''
162
+ if (segmentStart < pos) {
163
+ segments.push(content.slice(segmentStart, pos))
140
164
  }
141
165
  segments.push('[]')
142
166
  pos += 2
167
+ segmentStart = pos
143
168
  continue
144
169
  }
145
- const ch = content[pos]
146
- if (ch === '[' || ch === ']') {
147
- if (buffer) {
148
- segments.push(buffer)
149
- buffer = ''
170
+ if (code === CHAR_OPEN_BRACKET || code === CHAR_CLOSE_BRACKET) {
171
+ if (segmentStart < pos) {
172
+ segments.push(content.slice(segmentStart, pos))
150
173
  }
151
- segments.push(ch)
174
+ segments.push(code === CHAR_OPEN_BRACKET ? '[' : ']')
152
175
  pos++
176
+ segmentStart = pos
153
177
  continue
154
178
  }
155
- buffer += ch
156
179
  pos++
157
180
  }
158
- if (buffer) segments.push(buffer)
181
+ if (segmentStart < contentLen) segments.push(content.slice(segmentStart))
159
182
  if (segments.length <= 1) {
160
- if (segments.length === 0) {
161
- token.__strongJaHasBracket = false
162
- token.__strongJaBracketAtomic = false
163
- } else {
164
- const seg = segments[0]
165
- if (seg === '[' || seg === ']') {
166
- token.__strongJaHasBracket = true
167
- token.__strongJaBracketAtomic = true
168
- } else if (seg === '[]') {
169
- token.__strongJaHasBracket = true
170
- token.__strongJaBracketAtomic = false
171
- } else {
172
- token.__strongJaHasBracket = false
173
- token.__strongJaBracketAtomic = false
174
- }
175
- }
183
+ applyBracketSegmentFlags(token, segments[0])
176
184
  return false
177
185
  }
186
+
178
187
  token.content = segments[0]
179
- if (token.content === '[' || token.content === ']') {
180
- token.__strongJaHasBracket = true
181
- token.__strongJaBracketAtomic = true
182
- } else if (token.content === '[]') {
183
- token.__strongJaHasBracket = true
184
- token.__strongJaBracketAtomic = false
185
- } else {
186
- token.__strongJaHasBracket = false
187
- token.__strongJaBracketAtomic = false
188
- }
189
- let insertIdx = index + 1
188
+ applyBracketSegmentFlags(token, token.content)
189
+
190
+ const replacements = [token]
190
191
  for (let s = 1; s < segments.length; s++) {
191
192
  const newToken = cloneTextToken(token, segments[s])
192
- if (segments[s] === '[' || segments[s] === ']') {
193
- newToken.__strongJaHasBracket = true
194
- newToken.__strongJaBracketAtomic = true
195
- } else if (segments[s] === '[]') {
196
- newToken.__strongJaHasBracket = true
197
- newToken.__strongJaBracketAtomic = false
198
- } else {
199
- newToken.__strongJaHasBracket = false
200
- newToken.__strongJaBracketAtomic = false
201
- }
202
- tokens.splice(insertIdx, 0, newToken)
203
- insertIdx++
193
+ applyBracketSegmentFlags(newToken, segments[s])
194
+ replacements.push(newToken)
204
195
  }
196
+ tokens.splice(index, 1, ...replacements)
205
197
  return true
206
198
  }
207
199
 
@@ -209,48 +201,36 @@ const isBracketToken = (token, bracket) => {
209
201
  return token && token.type === 'text' && token.content === bracket
210
202
  }
211
203
 
212
- const findLinkCloseIndex = (tokens, startIdx) => {
213
- let depth = 0
214
- for (let idx = startIdx; idx < tokens.length; idx++) {
215
- const token = tokens[idx]
216
- if (token.type === 'link_open') depth++
217
- if (token.type === 'link_close') {
218
- depth--
219
- if (depth === 0) return idx
220
- }
221
- }
222
- return -1
223
- }
224
-
225
- const consumeCharactersFromTokens = (tokens, startIdx, count) => {
226
- let remaining = count
227
- let idx = startIdx
228
- while (idx < tokens.length && remaining > 0) {
229
- const token = tokens[idx]
230
- if (!token || token.type !== 'text') {
231
- return false
232
- }
233
- const len = token.content.length
234
- if (remaining >= len) {
235
- remaining -= len
236
- tokens.splice(idx, 1)
204
+ const buildLinkCloseMap = (tokens, startIdx, endIdx) => {
205
+ const closeMap = new Map()
206
+ const stack = []
207
+ const max = tokens ? tokens.length - 1 : -1
208
+ const from = startIdx > 0 ? startIdx : 0
209
+ const to = endIdx < max ? endIdx : max
210
+ for (let i = from; i <= to; i++) {
211
+ const token = tokens[i]
212
+ if (!token) continue
213
+ if (token.type === 'link_open') {
214
+ stack.push(i)
237
215
  continue
238
216
  }
239
- token.content = token.content.slice(remaining)
240
- remaining = 0
217
+ if (token.type !== 'link_close' || stack.length === 0) continue
218
+ closeMap.set(stack.pop(), i)
241
219
  }
242
- return remaining === 0
220
+ return closeMap
243
221
  }
244
222
 
245
- const wrapLabelTokensWithLink = (tokens, labelStartIdx, labelEndIdx, linkOpenToken, linkCloseToken, labelSource) => {
223
+ const collectWrappedLabelPairs = (tokens, collapsedStartIdx, collapsedEndIdx) => {
246
224
  const wrapperPairs = []
247
- let startIdx = labelStartIdx
248
- let endIdx = labelEndIdx
249
- while (startIdx > 0) {
250
- const prevToken = tokens[startIdx - 1]
251
- const nextToken = tokens[endIdx + 1]
225
+ while (true) {
226
+ const wrapperOffset = wrapperPairs.length
227
+ const closeIdx = collapsedStartIdx - 1 - wrapperOffset
228
+ const openIdx = collapsedEndIdx + 1 + wrapperOffset
229
+ if (closeIdx < 0 || openIdx >= tokens.length) break
230
+ const prevToken = tokens[closeIdx]
231
+ const nextToken = tokens[openIdx]
252
232
  if (!prevToken || !nextToken) break
253
- if (!/_close$/.test(prevToken.type)) break
233
+ if (!prevToken.type || !prevToken.type.endsWith('_close')) break
254
234
  const expectedOpen = prevToken.type.replace('_close', '_open')
255
235
  if (nextToken.type !== expectedOpen) break
256
236
  wrapperPairs.push({
@@ -258,483 +238,377 @@ const wrapLabelTokensWithLink = (tokens, labelStartIdx, labelEndIdx, linkOpenTok
258
238
  tag: prevToken.tag,
259
239
  markup: prevToken.markup,
260
240
  openMap: cloneMap(nextToken.map),
261
- closeMap: cloneMap(prevToken.map)
241
+ closeMap: cloneMap(prevToken.map),
242
+ closeIdx,
243
+ openIdx
262
244
  })
263
- tokens.splice(endIdx + 1, 1)
264
- tokens.splice(startIdx - 1, 1)
265
- startIdx -= 1
266
- endIdx -= 1
267
- }
268
-
269
- if (startIdx > endIdx) {
270
- if (labelSource !== undefined && labelSource !== null) {
271
- const placeholder = new Token('text', '', 0)
272
- placeholder.content = labelSource
273
- placeholder.level = linkOpenToken.level + 1
274
- tokens.splice(startIdx, 0, placeholder)
275
- endIdx = startIdx
276
- } else {
277
- return startIdx
245
+ }
246
+ return wrapperPairs
247
+ }
248
+
249
+ const resolveWrappedLabelReplaceRange = (wrapperPairs, collapsedStartIdx, collapsedEndIdx) => {
250
+ if (wrapperPairs.length === 0) {
251
+ return {
252
+ replaceStart: collapsedStartIdx,
253
+ replaceEnd: collapsedEndIdx
278
254
  }
279
255
  }
256
+ const outerPair = wrapperPairs[wrapperPairs.length - 1]
257
+ return {
258
+ replaceStart: outerPair.closeIdx,
259
+ replaceEnd: outerPair.openIdx
260
+ }
261
+ }
262
+
263
+ const resolveInsertedWrapperMap = (pairMap, labelMap) => {
264
+ return pairMap || labelMap
265
+ }
280
266
 
281
- let labelLength = endIdx - startIdx + 1
282
- const firstLabelToken = tokens[startIdx]
267
+ const buildWrappedLabelReplacement = (labelTokens, linkOpenToken, linkCloseToken, wrapperPairs, labelMap) => {
268
+ const firstLabelToken = labelTokens[0]
283
269
  const linkLevel = firstLabelToken ? Math.max(firstLabelToken.level - 1, 0) : 0
284
270
  linkOpenToken.level = linkLevel
285
271
  linkCloseToken.level = linkLevel
286
- const labelMap = getMapFromTokenRange(tokens, startIdx, endIdx) || getNearbyMap(tokens, startIdx, endIdx)
287
272
  if (labelMap) {
288
273
  if (!linkOpenToken.map) linkOpenToken.map = cloneMap(labelMap)
289
274
  if (!linkCloseToken.map) linkCloseToken.map = cloneMap(labelMap)
290
275
  }
291
- tokens.splice(startIdx, 0, linkOpenToken)
292
- tokens.splice(startIdx + labelLength + 1, 0, linkCloseToken)
293
-
294
- adjustTokenLevels(tokens, startIdx + 1, startIdx + labelLength + 1, 1)
295
-
296
- if (wrapperPairs.length > 0) {
297
- let insertIdx = startIdx + 1
298
- for (let wp = 0; wp < wrapperPairs.length; wp++) {
299
- const pair = wrapperPairs[wp]
300
- const innerOpen = new Token(pair.base + '_open', pair.tag, 1)
301
- innerOpen.markup = pair.markup
302
- innerOpen.level = linkLevel + 1 + wp
303
- if (pair.openMap && !innerOpen.map) innerOpen.map = cloneMap(pair.openMap)
304
- tokens.splice(insertIdx, 0, innerOpen)
305
- insertIdx++
306
- labelLength++
307
- }
308
- let linkClosePos = startIdx + labelLength + 1
309
- for (let wp = wrapperPairs.length - 1; wp >= 0; wp--) {
310
- const pair = wrapperPairs[wp]
311
- const innerClose = new Token(pair.base + '_close', pair.tag, -1)
312
- innerClose.markup = pair.markup
313
- innerClose.level = linkLevel + 1 + wp
314
- if (pair.closeMap && !innerClose.map) innerClose.map = cloneMap(pair.closeMap)
315
- tokens.splice(linkClosePos, 0, innerClose)
316
- labelLength++
317
- }
276
+ for (let idx = 0; idx < labelTokens.length; idx++) {
277
+ if (labelTokens[idx]) labelTokens[idx].level += 1
318
278
  }
319
279
 
320
- return startIdx + labelLength + 2
280
+ const replacement = [linkOpenToken]
281
+ for (let wp = 0; wp < wrapperPairs.length; wp++) {
282
+ const pair = wrapperPairs[wp]
283
+ const innerOpen = new Token(pair.base + '_open', pair.tag, 1)
284
+ innerOpen.markup = pair.markup
285
+ innerOpen.level = linkLevel + 1 + wp
286
+ const openMap = resolveInsertedWrapperMap(pair.openMap, labelMap)
287
+ if (openMap && !innerOpen.map) innerOpen.map = cloneMap(openMap)
288
+ replacement.push(innerOpen)
289
+ }
290
+ replacement.push(...labelTokens)
291
+ for (let wp = 0; wp < wrapperPairs.length; wp++) {
292
+ const pair = wrapperPairs[wp]
293
+ const innerClose = new Token(pair.base + '_close', pair.tag, -1)
294
+ innerClose.markup = pair.markup
295
+ innerClose.level = linkLevel + 1 + wp
296
+ const closeMap = resolveInsertedWrapperMap(pair.closeMap, labelMap)
297
+ if (closeMap && !innerClose.map) innerClose.map = cloneMap(closeMap)
298
+ replacement.push(innerClose)
299
+ }
300
+ replacement.push(linkCloseToken)
301
+ return replacement
321
302
  }
322
303
 
323
- const parseInlineLinkTail = (content, md) => {
324
- if (!content || content.charCodeAt(0) !== CHAR_OPEN_PAREN) return null
325
- const max = content.length
326
- let pos = 1
327
- while (pos < max) {
328
- const code = content.charCodeAt(pos)
329
- if (!isSpace(code) && code !== 0x0A) break
330
- pos++
331
- }
332
- if (pos >= max) return null
304
+ const wrapLabelTokensWithLink = (
305
+ tokens,
306
+ collapsedStartIdx,
307
+ collapsedEndIdx,
308
+ labelStartIdx,
309
+ labelEndIdx,
310
+ linkOpenToken,
311
+ linkCloseToken
312
+ ) => {
313
+ if (labelStartIdx > labelEndIdx) return collapsedStartIdx
314
+ const labelTokens = tokens.slice(labelStartIdx, labelEndIdx + 1)
315
+ const wrapperPairs = collectWrappedLabelPairs(tokens, collapsedStartIdx, collapsedEndIdx)
316
+ const { replaceStart, replaceEnd } = resolveWrappedLabelReplaceRange(
317
+ wrapperPairs,
318
+ collapsedStartIdx,
319
+ collapsedEndIdx
320
+ )
321
+ const labelMap = getMapFromTokenRange(tokens, labelStartIdx, labelEndIdx) || getNearbyMap(tokens, replaceStart, replaceEnd)
322
+ const replacement = buildWrappedLabelReplacement(
323
+ labelTokens,
324
+ linkOpenToken,
325
+ linkCloseToken,
326
+ wrapperPairs,
327
+ labelMap
328
+ )
329
+ tokens.splice(replaceStart, replaceEnd - replaceStart + 1, ...replacement)
330
+ return replaceStart + replacement.length
331
+ }
333
332
 
334
- let href = ''
335
- let destPos = pos
336
- if (pos < max && content.charCodeAt(pos) === CHAR_CLOSE_PAREN) {
337
- href = ''
333
+ const resolveCollapsedReferenceTarget = (
334
+ tokens,
335
+ state,
336
+ refRemoveStart,
337
+ getLabelText,
338
+ getLinkCloseMap
339
+ ) => {
340
+ let refKey = null
341
+ let refRemoveCount = 0
342
+ let existingLinkOpen = null
343
+ let existingLinkClose = null
344
+ const nextToken = tokens[refRemoveStart]
345
+ if (isBracketToken(nextToken, '[]')) {
346
+ refKey = normalizeReferenceCandidate(state, getLabelText())
347
+ refRemoveCount = 1
348
+ } else if (isBracketToken(nextToken, '[')) {
349
+ let refCloseIdx = refRemoveStart + 1
350
+ while (refCloseIdx < tokens.length && !isBracketToken(tokens[refCloseIdx], ']')) {
351
+ refCloseIdx++
352
+ }
353
+ if (refCloseIdx >= tokens.length) return null
354
+ const refStart = refRemoveStart + 1
355
+ const refEnd = refCloseIdx - 1
356
+ if (refStart > refEnd) {
357
+ refKey = normalizeReferenceCandidate(state, getLabelText())
358
+ } else {
359
+ const refLabelText = buildReferenceLabelRange(tokens, refStart, refEnd)
360
+ refKey = normalizeReferenceCandidate(state, refLabelText)
361
+ }
362
+ refRemoveCount = refCloseIdx - refRemoveStart + 1
363
+ } else if (nextToken && nextToken.type === 'link_open') {
364
+ const linkCloseMap = getLinkCloseMap(refRemoveStart)
365
+ const linkCloseIdx = linkCloseMap.get(refRemoveStart) ?? -1
366
+ if (linkCloseIdx === -1) return null
367
+ existingLinkOpen = tokens[refRemoveStart]
368
+ existingLinkClose = tokens[linkCloseIdx]
369
+ refRemoveCount = linkCloseIdx - refRemoveStart + 1
338
370
  } else {
339
- const dest = parseLinkDestination(content, pos, max)
340
- if (!dest.ok) return null
341
- href = md.normalizeLink(dest.str)
342
- if (!md.validateLink(href)) {
343
- return null
344
- }
345
- pos = dest.pos
346
- destPos = dest.pos
371
+ return null
347
372
  }
348
-
349
- while (pos < max) {
350
- const code = content.charCodeAt(pos)
351
- if (!isSpace(code) && code !== 0x0A) break
352
- pos++
373
+ return {
374
+ refKey,
375
+ refRemoveCount,
376
+ existingLinkOpen,
377
+ existingLinkClose
353
378
  }
379
+ }
354
380
 
355
- let title = ''
356
- const titleRes = parseLinkTitle(content, pos, max)
357
- if (pos < max && pos !== destPos && titleRes.ok) {
358
- title = titleRes.str
359
- pos = titleRes.pos
360
- while (pos < max) {
361
- const code = content.charCodeAt(pos)
362
- if (!isSpace(code) && code !== 0x0A) break
363
- pos++
381
+ const buildAutoCollapsedReferenceLinkPair = (ref) => {
382
+ if (!ref) return null
383
+ const linkOpenToken = new Token('link_open', 'a', 1)
384
+ linkOpenToken.attrs = [['href', ref.href]]
385
+ if (ref.title) linkOpenToken.attrPush(['title', ref.title])
386
+ linkOpenToken.markup = '[]'
387
+ linkOpenToken.info = 'auto'
388
+
389
+ const linkCloseToken = new Token('link_close', 'a', -1)
390
+ linkCloseToken.markup = '[]'
391
+ linkCloseToken.info = 'auto'
392
+ return { linkOpenToken, linkCloseToken }
393
+ }
394
+
395
+ const resolveCollapsedReferenceLinkPair = (references, target) => {
396
+ if (!target) return null
397
+ if (target.existingLinkOpen && target.existingLinkClose) {
398
+ return {
399
+ linkOpenToken: target.existingLinkOpen,
400
+ linkCloseToken: target.existingLinkClose
364
401
  }
365
402
  }
403
+ if (!target.refKey) return null
404
+ return buildAutoCollapsedReferenceLinkPair(references[target.refKey])
405
+ }
366
406
 
367
- if (pos >= max || content.charCodeAt(pos) !== CHAR_CLOSE_PAREN) {
368
- return null
369
- }
370
- pos++
371
- return { href, title, consumed: pos }
407
+ const applyCollapsedReferenceRewrite = (
408
+ tokens,
409
+ startIdx,
410
+ labelStart,
411
+ labelEnd,
412
+ suffixRemoveCount,
413
+ linkOpenToken,
414
+ linkCloseToken
415
+ ) => {
416
+ const labelLength = labelEnd - labelStart + 1
417
+ const collapsedReplaceCount = labelLength + 2 + suffixRemoveCount
418
+ const collapsedEnd = startIdx + collapsedReplaceCount - 1
419
+ linkOpenToken.__strongJaMergeMarksAroundLink = true
420
+ linkCloseToken.__strongJaMergeMarksAroundLink = true
421
+ return wrapLabelTokensWithLink(
422
+ tokens,
423
+ startIdx,
424
+ collapsedEnd,
425
+ labelStart,
426
+ labelEnd,
427
+ linkOpenToken,
428
+ linkCloseToken
429
+ )
372
430
  }
373
431
 
374
- const INLINE_LINK_BRACKET_SPLIT_OPTIONS = { splitEmptyPair: true }
432
+ const COLLAPSED_REFERENCE_SCAN_RETRY = Symbol('collapsed-reference-scan-retry')
433
+ const COLLAPSED_REFERENCE_SCAN_SKIP = Symbol('collapsed-reference-scan-skip')
375
434
 
376
- const removeGhostLabelText = (tokens, linkCloseIndex, labelText) => {
377
- if (!labelText) return
378
- if (linkCloseIndex === null || linkCloseIndex === undefined) return
379
- if (linkCloseIndex < 0 || linkCloseIndex >= tokens.length) return
380
- const closeToken = tokens[linkCloseIndex]
381
- if (!closeToken || closeToken.type !== 'link_close') return
382
- let idx = linkCloseIndex + 1
383
- while (idx < tokens.length) {
384
- const token = tokens[idx]
385
- if (!token) {
386
- idx++
387
- continue
388
- }
389
- if (token.type === 'text') {
390
- if (token.content.startsWith(labelText)) {
391
- if (token.content.length === labelText.length) {
392
- tokens.splice(idx, 1)
393
- } else {
394
- token.content = token.content.slice(labelText.length)
395
- }
435
+ const createCollapsedReferenceLinkCloseMapAccessors = (tokens, cache = null) => {
436
+ let linkCloseMap = null
437
+ const getLinkCloseMap = (startIdx = 0) => {
438
+ if (cache) {
439
+ if (cache.linkCloseMap === undefined) {
440
+ cache.linkCloseMap = buildLinkCloseMap(tokens, 0, tokens.length - 1)
396
441
  }
397
- break
442
+ return cache.linkCloseMap
398
443
  }
399
- if (!/_close$/.test(token.type)) {
400
- break
444
+ if (linkCloseMap === null) {
445
+ linkCloseMap = buildLinkCloseMap(tokens, startIdx, tokens.length - 1)
401
446
  }
402
- idx++
447
+ return linkCloseMap
448
+ }
449
+ const invalidateLinkCloseMap = () => {
450
+ linkCloseMap = null
451
+ if (cache) cache.linkCloseMap = undefined
403
452
  }
453
+ return { getLinkCloseMap, invalidateLinkCloseMap }
404
454
  }
405
455
 
406
- const restoreLabelWhitespace = (tokens, labelSources) => {
407
- if (!tokens || !labelSources || labelSources.length === 0) return
408
- let labelIdx = 0
409
- for (let i = 0; i < tokens.length && labelIdx < labelSources.length; i++) {
410
- if (tokens[i].type !== 'link_open') continue
411
- const closeIdx = findLinkCloseIndex(tokens, i)
412
- if (closeIdx === -1) continue
413
- const labelSource = labelSources[labelIdx] || ''
414
- if (!labelSource) {
415
- labelIdx++
416
- continue
417
- }
418
- let cursor = 0
419
- for (let pos = i + 1; pos < closeIdx; pos++) {
420
- const t = tokens[pos]
421
- const markup = t.markup || ''
422
- const text = t.content || ''
423
- const startPos = cursor
424
- if (t.type === 'text') {
425
- cursor += text.length
426
- } else if (t.type === 'code_inline') {
427
- cursor += markup.length + text.length + markup.length
428
- } else if (markup) {
429
- cursor += markup.length
430
- }
431
- if ((t.type === 'strong_open' || t.type === 'em_open') && startPos > 0) {
432
- const prevToken = tokens[pos - 1]
433
- if (prevToken && prevToken.type === 'text' && prevToken.content && !prevToken.content.endsWith(' ')) {
434
- const hasSpaceBefore = startPos - 1 >= 0 && startPos - 1 < labelSource.length && labelSource[startPos - 1] === ' '
435
- const hasSpaceAt = startPos >= 0 && startPos < labelSource.length && labelSource[startPos] === ' '
436
- if (hasSpaceBefore || hasSpaceAt) {
437
- prevToken.content += ' '
438
- }
439
- }
440
- }
441
- }
442
- labelIdx++
456
+ const findCollapsedReferenceLabelClose = (tokens, startIdx, invalidateLinkCloseMap) => {
457
+ let closeIdx = startIdx + 1
458
+ while (closeIdx < tokens.length) {
459
+ if (isBracketToken(tokens[closeIdx], ']')) return closeIdx
460
+ const closeToken = tokens[closeIdx]
461
+ if (closeToken && closeToken.type === 'text' && splitBracketToken(tokens, closeIdx)) {
462
+ invalidateLinkCloseMap()
463
+ return COLLAPSED_REFERENCE_SCAN_RETRY
464
+ }
465
+ if (closeToken && closeToken.type === 'link_open') return -1
466
+ closeIdx++
443
467
  }
468
+ return -1
444
469
  }
445
470
 
446
- const convertInlineLinks = (tokens, state) => {
447
- if (!tokens || tokens.length === 0) return
448
- let labelSources = tokens.__strongJaInlineLabelSources
449
- if ((!labelSources || labelSources.length === 0) && state && state.env && Array.isArray(state.env.__strongJaInlineLabelSourceList) && state.env.__strongJaInlineLabelSourceList.length > 0) {
450
- labelSources = state.env.__strongJaInlineLabelSourceList.shift()
471
+ const buildCollapsedReferenceCandidate = (
472
+ tokens,
473
+ state,
474
+ startIdx,
475
+ closeIdx,
476
+ getLinkCloseMap,
477
+ invalidateLinkCloseMap
478
+ ) => {
479
+ if (closeIdx === startIdx + 1) return null
480
+
481
+ const labelStart = startIdx + 1
482
+ const labelEnd = closeIdx - 1
483
+ if (!hasReferenceLabelMarkerRange(tokens, labelStart, labelEnd)) return null
484
+
485
+ let labelText = null
486
+ const getLabelText = () => {
487
+ if (labelText === null) labelText = buildReferenceLabelRange(tokens, labelStart, labelEnd)
488
+ return labelText
451
489
  }
452
- let labelSourceIndex = tokens.__strongJaInlineLabelIndex || 0
453
- let i = 0
454
- while (i < tokens.length) {
455
- if (splitBracketToken(tokens, i, INLINE_LINK_BRACKET_SPLIT_OPTIONS)) {
456
- continue
457
- }
458
- if (!isBracketToken(tokens[i], '[')) {
459
- i++
460
- continue
461
- }
462
- let closeIdx = i + 1
463
- let invalid = false
464
- while (closeIdx < tokens.length && !isBracketToken(tokens[closeIdx], ']')) {
465
- if (splitBracketToken(tokens, closeIdx, INLINE_LINK_BRACKET_SPLIT_OPTIONS)) {
466
- continue
467
- }
468
- if (tokens[closeIdx].type === 'link_open') {
469
- invalid = true
470
- break
471
- }
472
- closeIdx++
473
- }
474
- if (invalid || closeIdx >= tokens.length) {
475
- i++
476
- continue
477
- }
478
- const currentLabelSource = labelSources && labelSourceIndex < labelSources.length
479
- ? labelSources[labelSourceIndex]
480
- : undefined
481
490
 
482
- const labelLength = closeIdx - i - 1
483
- const needsPlaceholder = labelLength <= 0
484
- if (needsPlaceholder && !currentLabelSource) {
485
- i++
486
- continue
487
- }
488
-
489
- let tailIdx = closeIdx + 1
490
- let tailContent = ''
491
- let parsedTail = null
492
- let tailHasCloseParen = false
493
- while (tailIdx < tokens.length) {
494
- if (splitBracketToken(tokens, tailIdx, INLINE_LINK_BRACKET_SPLIT_OPTIONS)) {
495
- continue
496
- }
497
- const tailToken = tokens[tailIdx]
498
- if (tailToken.type !== 'text' || !tailToken.content) {
499
- break
500
- }
501
- tailContent += tailToken.content
502
- if (!tailHasCloseParen) {
503
- if (tailToken.content.indexOf(')') === -1) {
504
- tailIdx++
505
- continue
506
- }
507
- tailHasCloseParen = true
508
- }
509
- parsedTail = parseInlineLinkTail(tailContent, state.md)
510
- if (parsedTail) break
511
- tailIdx++
512
- }
513
-
514
- if (!parsedTail) {
515
- i++
516
- continue
517
- }
518
-
519
- if (!consumeCharactersFromTokens(tokens, closeIdx + 1, parsedTail.consumed)) {
520
- i++
521
- continue
522
- }
523
-
524
- tokens.splice(closeIdx, 1)
525
- tokens.splice(i, 1)
526
-
527
- const linkOpenToken = new Token('link_open', 'a', 1)
528
- linkOpenToken.attrs = [['href', parsedTail.href]]
529
- if (parsedTail.title) linkOpenToken.attrPush(['title', parsedTail.title])
530
- linkOpenToken.markup = '[]()'
531
- linkOpenToken.info = 'auto'
532
- const linkCloseToken = new Token('link_close', 'a', -1)
533
- linkCloseToken.markup = '[]()'
534
- linkCloseToken.info = 'auto'
535
-
536
- const nextIndex = wrapLabelTokensWithLink(tokens, i, i + labelLength - 1, linkOpenToken, linkCloseToken, currentLabelSource)
537
- if (nextIndex === i) {
538
- i++
539
- continue
540
- }
541
- if (currentLabelSource) {
542
- const linkCloseIdx = findLinkCloseIndex(tokens, i)
543
- if (linkCloseIdx !== -1) {
544
- let cursor = 0
545
- for (let pos = i + 1; pos < linkCloseIdx; pos++) {
546
- const t = tokens[pos]
547
- const markup = t.markup || ''
548
- const text = t.content || ''
549
- const startPos = cursor
550
- if (t.type === 'text') {
551
- cursor += text.length
552
- } else if (t.type === 'code_inline') {
553
- cursor += markup.length + text.length + markup.length
554
- } else if (markup) {
555
- cursor += markup.length
556
- }
557
- if ((t.type === 'strong_open' || t.type === 'em_open') && startPos > 0) {
558
- const prevToken = tokens[pos - 1]
559
- if (prevToken && prevToken.type === 'text' && prevToken.content && !prevToken.content.endsWith(' ')) {
560
- const labelHasSpaceBefore = startPos - 1 >= 0 && startPos - 1 < currentLabelSource.length && currentLabelSource[startPos - 1] === ' '
561
- const labelHasSpaceAt = startPos >= 0 && startPos < currentLabelSource.length && currentLabelSource[startPos] === ' '
562
- if (labelHasSpaceBefore || labelHasSpaceAt) {
563
- prevToken.content += ' '
564
- }
565
- }
566
- }
567
- }
568
- }
569
- }
570
- if (needsPlaceholder && currentLabelSource) {
571
- removeGhostLabelText(tokens, nextIndex - 1, currentLabelSource)
572
- }
491
+ const whitespaceStart = closeIdx + 1
492
+ let refRemoveStart = whitespaceStart
493
+ while (refRemoveStart < tokens.length && isWhitespaceToken(tokens[refRemoveStart])) {
494
+ refRemoveStart++
495
+ }
496
+ const refStartToken = tokens[refRemoveStart]
497
+ if (refStartToken && refStartToken.type === 'text' && splitBracketToken(tokens, refRemoveStart)) {
498
+ invalidateLinkCloseMap()
499
+ return COLLAPSED_REFERENCE_SCAN_RETRY
500
+ }
573
501
 
574
- if (labelSources && labelSources.length > 0) {
575
- if (labelSourceIndex < labelSources.length) {
576
- labelSourceIndex++
577
- }
578
- }
579
- i = nextIndex
502
+ const target = resolveCollapsedReferenceTarget(
503
+ tokens,
504
+ state,
505
+ refRemoveStart,
506
+ getLabelText,
507
+ getLinkCloseMap
508
+ )
509
+ if (!target) return null
510
+
511
+ return {
512
+ labelStart,
513
+ labelEnd,
514
+ suffixRemoveCount: (refRemoveStart - whitespaceStart) + target.refRemoveCount,
515
+ target
580
516
  }
581
- if (labelSources) {
582
- tokens.__strongJaInlineLabelIndex = labelSourceIndex
517
+ }
518
+
519
+ const tryConvertCollapsedReferenceAt = (
520
+ tokens,
521
+ state,
522
+ references,
523
+ startIdx,
524
+ getLinkCloseMap,
525
+ invalidateLinkCloseMap,
526
+ onChangeStart = null
527
+ ) => {
528
+ if (splitBracketToken(tokens, startIdx)) {
529
+ invalidateLinkCloseMap()
530
+ return COLLAPSED_REFERENCE_SCAN_RETRY
583
531
  }
532
+ if (!isBracketToken(tokens[startIdx], '[')) return COLLAPSED_REFERENCE_SCAN_SKIP
533
+
534
+ const closeIdx = findCollapsedReferenceLabelClose(tokens, startIdx, invalidateLinkCloseMap)
535
+ if (closeIdx === COLLAPSED_REFERENCE_SCAN_RETRY) return COLLAPSED_REFERENCE_SCAN_RETRY
536
+ if (closeIdx === -1) return COLLAPSED_REFERENCE_SCAN_SKIP
537
+
538
+ const candidate = buildCollapsedReferenceCandidate(
539
+ tokens,
540
+ state,
541
+ startIdx,
542
+ closeIdx,
543
+ getLinkCloseMap,
544
+ invalidateLinkCloseMap
545
+ )
546
+ if (candidate === COLLAPSED_REFERENCE_SCAN_RETRY) return COLLAPSED_REFERENCE_SCAN_RETRY
547
+ if (!candidate) return COLLAPSED_REFERENCE_SCAN_SKIP
548
+
549
+ const linkPair = resolveCollapsedReferenceLinkPair(references, candidate.target)
550
+ if (!linkPair) return COLLAPSED_REFERENCE_SCAN_SKIP
551
+
552
+ if (onChangeStart) onChangeStart(startIdx)
553
+ invalidateLinkCloseMap()
554
+ return applyCollapsedReferenceRewrite(
555
+ tokens,
556
+ startIdx,
557
+ candidate.labelStart,
558
+ candidate.labelEnd,
559
+ candidate.suffixRemoveCount,
560
+ linkPair.linkOpenToken,
561
+ linkPair.linkCloseToken
562
+ )
584
563
  }
585
564
 
586
- const convertCollapsedReferenceLinks = (tokens, state) => {
565
+ const convertCollapsedReferenceLinks = (tokens, state, cache = null, onChangeStart = null) => {
587
566
  const references = state.env && state.env.references
588
- if (!references) return
589
- const referenceCount = state.__strongJaReferenceCount
590
- if (referenceCount !== undefined) {
591
- if (referenceCount === 0) return
592
- } else if (Object.keys(references).length === 0) {
593
- return
567
+ if (!references) return false
568
+ if (getReferenceCount(state) === 0) {
569
+ return false
594
570
  }
595
571
 
572
+ let changed = false
596
573
  let i = 0
574
+ const { getLinkCloseMap, invalidateLinkCloseMap } = createCollapsedReferenceLinkCloseMapAccessors(tokens, cache)
597
575
  while (i < tokens.length) {
598
- if (splitBracketToken(tokens, i)) {
599
- continue
600
- }
601
- if (!isBracketToken(tokens[i], '[')) {
602
- i++
603
- continue
604
- }
605
- let closeIdx = i + 1
606
- while (closeIdx < tokens.length && !isBracketToken(tokens[closeIdx], ']')) {
607
- if (splitBracketToken(tokens, closeIdx)) {
608
- continue
609
- }
610
- if (tokens[closeIdx].type === 'link_open') {
611
- closeIdx = -1
612
- break
613
- }
614
- closeIdx++
615
- }
616
- if (closeIdx === -1 || closeIdx >= tokens.length) {
617
- i++
618
- continue
619
- }
620
-
621
- if (closeIdx === i + 1) {
576
+ const nextIndex = tryConvertCollapsedReferenceAt(
577
+ tokens,
578
+ state,
579
+ references,
580
+ i,
581
+ getLinkCloseMap,
582
+ invalidateLinkCloseMap,
583
+ onChangeStart
584
+ )
585
+ if (nextIndex === COLLAPSED_REFERENCE_SCAN_RETRY) continue
586
+ if (nextIndex === COLLAPSED_REFERENCE_SCAN_SKIP) {
622
587
  i++
623
588
  continue
624
589
  }
625
-
626
- const labelStart = i + 1
627
- const labelEnd = closeIdx - 1
628
- const labelLength = closeIdx - i - 1
629
- const labelText = buildReferenceLabelRange(tokens, labelStart, labelEnd)
630
- const cleanedLabel = cleanLabelText(labelText)
631
- const whitespaceStart = closeIdx + 1
632
- let refRemoveStart = whitespaceStart
633
- while (refRemoveStart < tokens.length && isWhitespaceToken(tokens[refRemoveStart])) {
634
- refRemoveStart++
635
- }
636
- if (splitBracketToken(tokens, refRemoveStart)) {
637
- continue
638
- }
639
- const whitespaceCount = refRemoveStart - whitespaceStart
640
- let refKey = null
641
- let refRemoveCount = 0
642
- let existingLinkOpen = null
643
- let existingLinkClose = null
644
- const nextToken = tokens[refRemoveStart]
645
- if (isBracketToken(nextToken, '[]')) {
646
- refKey = normalizeReferenceCandidate(state, cleanedLabel)
647
- refRemoveCount = 1
648
- } else if (isBracketToken(nextToken, '[')) {
649
- let refCloseIdx = refRemoveStart + 1
650
- while (refCloseIdx < tokens.length && !isBracketToken(tokens[refCloseIdx], ']')) {
651
- refCloseIdx++
652
- }
653
- if (refCloseIdx >= tokens.length) {
654
- i++
655
- continue
656
- }
657
- const refStart = refRemoveStart + 1
658
- const refEnd = refCloseIdx - 1
659
- if (refStart > refEnd) {
660
- refKey = normalizeReferenceCandidate(state, cleanedLabel)
661
- } else {
662
- const refLabelText = buildReferenceLabelRange(tokens, refStart, refEnd)
663
- refKey = normalizeReferenceCandidate(state, refLabelText)
664
- }
665
- refRemoveCount = refCloseIdx - refRemoveStart + 1
666
- } else if (nextToken && nextToken.type === 'link_open') {
667
- const linkCloseIdx = findLinkCloseIndex(tokens, refRemoveStart)
668
- if (linkCloseIdx === -1) {
669
- i++
670
- continue
671
- }
672
- existingLinkOpen = tokens[refRemoveStart]
673
- existingLinkClose = tokens[linkCloseIdx]
674
- refRemoveCount = linkCloseIdx - refRemoveStart + 1
675
- } else {
676
- i++
677
- continue
678
- }
679
- let linkOpenToken = null
680
- let linkCloseToken = null
681
- if (existingLinkOpen && existingLinkClose) {
682
- if (whitespaceCount > 0) {
683
- tokens.splice(whitespaceStart, whitespaceCount)
684
- refRemoveStart -= whitespaceCount
685
- }
686
- if (refRemoveCount > 0) {
687
- tokens.splice(refRemoveStart, refRemoveCount)
688
- }
689
- linkOpenToken = existingLinkOpen
690
- linkCloseToken = existingLinkClose
691
- } else {
692
- if (!refKey) {
693
- i++
694
- continue
695
- }
696
- const ref = references[refKey]
697
- if (!ref) {
698
- i++
699
- continue
700
- }
701
- if (whitespaceCount > 0) {
702
- tokens.splice(whitespaceStart, whitespaceCount)
703
- refRemoveStart -= whitespaceCount
704
- }
705
- if (refRemoveCount > 0) {
706
- tokens.splice(refRemoveStart, refRemoveCount)
707
- }
708
- linkOpenToken = new Token('link_open', 'a', 1)
709
- linkOpenToken.attrs = [['href', ref.href]]
710
- if (ref.title) linkOpenToken.attrPush(['title', ref.title])
711
- linkOpenToken.markup = '[]'
712
- linkOpenToken.info = 'auto'
713
- linkCloseToken = new Token('link_close', 'a', -1)
714
- linkCloseToken.markup = '[]'
715
- linkCloseToken.info = 'auto'
716
- }
717
- tokens.splice(closeIdx, 1)
718
- tokens.splice(i, 1)
719
-
720
- const nextIndex = wrapLabelTokensWithLink(tokens, i, i + labelLength - 1, linkOpenToken, linkCloseToken)
590
+ changed = true
721
591
  i = nextIndex
722
592
  }
593
+ return changed
723
594
  }
724
595
 
725
- // Link cleanup helpers
726
- const mergeBrokenMarksAroundLinks = (tokens) => {
596
+ const collectBrokenMarkLinkMergeRemovals = (tokens) => {
597
+ const removals = []
727
598
  let i = 0
728
599
  while (i < tokens.length) {
729
600
  const closeToken = tokens[i]
730
- if (!closeToken || !/_close$/.test(closeToken.type)) {
601
+ if (!closeToken || !closeToken.type ||
602
+ (closeToken.type !== 'em_close' && closeToken.type !== 'strong_close')) {
731
603
  i++
732
604
  continue
733
605
  }
734
606
  const openType = closeToken.type.replace('_close', '_open')
735
607
  let j = i + 1
736
608
  while (j < tokens.length && isWhitespaceToken(tokens[j])) j++
737
- if (j >= tokens.length || tokens[j].type !== 'link_open') {
609
+ if (j >= tokens.length ||
610
+ tokens[j].type !== 'link_open' ||
611
+ tokens[j].__strongJaMergeMarksAroundLink !== true) {
738
612
  i++
739
613
  continue
740
614
  }
@@ -759,15 +633,36 @@ const mergeBrokenMarksAroundLinks = (tokens) => {
759
633
  i++
760
634
  continue
761
635
  }
762
- tokens.splice(j, 1)
763
- tokens.splice(i, 1)
636
+ removals.push({ closeIdx: i, reopenIdx: j })
637
+ i = j + 1
764
638
  }
639
+ return removals
640
+ }
641
+
642
+ const applyBrokenMarkLinkMergeRemovals = (tokens, removals, onChangeStart = null) => {
643
+ if (!removals || removals.length === 0) return false
644
+ const removeFlags = new Array(tokens.length).fill(false)
645
+ for (let idx = removals.length - 1; idx >= 0; idx--) {
646
+ const removal = removals[idx]
647
+ if (onChangeStart) onChangeStart(removal.closeIdx)
648
+ removeFlags[removal.closeIdx] = true
649
+ removeFlags[removal.reopenIdx] = true
650
+ }
651
+ const kept = []
652
+ for (let idx = 0; idx < tokens.length; idx++) {
653
+ if (!removeFlags[idx]) kept.push(tokens[idx])
654
+ }
655
+ tokens.splice(0, tokens.length, ...kept)
656
+ return true
657
+ }
658
+
659
+ const mergeBrokenMarksAroundLinks = (tokens, onChangeStart = null) => {
660
+ return applyBrokenMarkLinkMergeRemovals(tokens, collectBrokenMarkLinkMergeRemovals(tokens), onChangeStart)
765
661
  }
766
662
 
767
663
  export {
768
664
  normalizeReferenceCandidate,
769
- restoreLabelWhitespace,
770
- convertInlineLinks,
665
+ buildLinkCloseMap,
771
666
  convertCollapsedReferenceLinks,
772
667
  mergeBrokenMarksAroundLinks,
773
668
  getMapFromTokenRange