@peaceroad/markdown-it-strong-ja 0.8.0 → 0.9.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.
@@ -0,0 +1,482 @@
1
+ import { buildLinkCloseMap } from '../token-link-utils.js'
2
+ import {
3
+ isAsteriskEmphasisToken,
4
+ buildBrokenRefWrapperRangeSignals,
5
+ buildAsteriskWrapperPrefixStats,
6
+ shouldAttemptBrokenRefRewrite
7
+ } from './guards.js'
8
+ import {
9
+ BROKEN_REF_FAST_PATH_RESULT_NO_ACTIVE_SIGNATURE,
10
+ BROKEN_REF_FAST_PATH_RESULT_NO_MATCH,
11
+ applyBrokenRefTokenOnlyFastPath
12
+ } from './fastpaths.js'
13
+
14
+ const scanBrokenRefState = (text, out) => {
15
+ if (!text || text.indexOf('[') === -1) {
16
+ out.depth = 0
17
+ out.brokenEnd = false
18
+ out.tailOpen = -1
19
+ return out
20
+ }
21
+ let depth = 0
22
+ let lastOpen = -1
23
+ let lastClose = -1
24
+ for (let i = 0; i < text.length; i++) {
25
+ const ch = text.charCodeAt(i)
26
+ if (ch === 0x5B) {
27
+ depth++
28
+ lastOpen = i
29
+ } else if (ch === 0x5D) {
30
+ if (depth > 0) depth--
31
+ lastClose = i
32
+ }
33
+ }
34
+ out.depth = depth
35
+ out.brokenEnd = depth > 0 && lastOpen > lastClose
36
+ out.tailOpen = out.brokenEnd ? lastOpen : -1
37
+ return out
38
+ }
39
+
40
+ const resetBrokenRefScanState = (scanState) => {
41
+ if (!scanState) return scanState
42
+ scanState.depth = 0
43
+ scanState.brokenEnd = false
44
+ scanState.tailOpen = -1
45
+ return scanState
46
+ }
47
+
48
+ const updateBracketDepth = (text, depth) => {
49
+ if (!text || depth <= 0) return depth
50
+ for (let i = 0; i < text.length; i++) {
51
+ const ch = text.charCodeAt(i)
52
+ if (ch === 0x5B) {
53
+ depth++
54
+ } else if (ch === 0x5D) {
55
+ if (depth > 0) {
56
+ depth--
57
+ if (depth === 0) return 0
58
+ }
59
+ }
60
+ }
61
+ return depth
62
+ }
63
+
64
+ const createBrokenRefWrapperBalance = () => {
65
+ return {
66
+ strong: 0,
67
+ em: 0,
68
+ total: 0
69
+ }
70
+ }
71
+
72
+ const updateBrokenRefWrapperBalance = (token, balance) => {
73
+ if (!token || !token.type) return
74
+ if ((token.type === 'strong_open' || token.type === 'strong_close' || token.type === 'em_open' || token.type === 'em_close') &&
75
+ !isAsteriskEmphasisToken(token)) {
76
+ return
77
+ }
78
+ if (token.type === 'strong_open') {
79
+ balance.strong++
80
+ balance.total++
81
+ return
82
+ }
83
+ if (token.type === 'em_open') {
84
+ balance.em++
85
+ balance.total++
86
+ return
87
+ }
88
+ if (token.type === 'strong_close') {
89
+ if (balance.strong > 0) {
90
+ balance.strong--
91
+ balance.total--
92
+ }
93
+ return
94
+ }
95
+ if (token.type === 'em_close' && balance.em > 0) {
96
+ balance.em--
97
+ balance.total--
98
+ }
99
+ }
100
+
101
+ const expandSegmentEndForWrapperBalance = (tokens, startIdx, endIdx) => {
102
+ if (!tokens || startIdx < 0 || endIdx < startIdx) return endIdx
103
+ const balance = createBrokenRefWrapperBalance()
104
+ let expandedEnd = endIdx
105
+
106
+ for (let i = startIdx; i <= expandedEnd; i++) {
107
+ updateBrokenRefWrapperBalance(tokens[i], balance)
108
+ }
109
+
110
+ while (balance.total > 0 && expandedEnd + 1 < tokens.length) {
111
+ expandedEnd++
112
+ updateBrokenRefWrapperBalance(tokens[expandedEnd], balance)
113
+ }
114
+
115
+ return balance.total > 0 ? -1 : expandedEnd
116
+ }
117
+
118
+ const bumpBrokenRefMetric = (metrics, bucket, key) => {
119
+ if (!metrics || !bucket || !key) return
120
+ let table = metrics[bucket]
121
+ if (!table || typeof table !== 'object') {
122
+ table = Object.create(null)
123
+ metrics[bucket] = table
124
+ }
125
+ table[key] = (table[key] || 0) + 1
126
+ }
127
+
128
+ const ensureBrokenRefLinkCloseMap = (tokens, facts = null, hooks = null, fallbackCache = null) => {
129
+ if (hooks && typeof hooks.ensureLinkCloseMap === 'function') {
130
+ return hooks.ensureLinkCloseMap(facts, tokens)
131
+ }
132
+ if (fallbackCache && fallbackCache.linkCloseMap !== undefined) {
133
+ return fallbackCache.linkCloseMap
134
+ }
135
+ const linkCloseMap = (!tokens || tokens.length === 0)
136
+ ? new Map()
137
+ : buildLinkCloseMap(tokens, 0, tokens.length - 1)
138
+ if (fallbackCache) {
139
+ fallbackCache.linkCloseMap = linkCloseMap
140
+ }
141
+ return linkCloseMap
142
+ }
143
+
144
+ const ensureBrokenRefWrapperPrefixStats = (tokens, facts = null, hooks = null, fallbackCache = null) => {
145
+ if (hooks && typeof hooks.ensureWrapperPrefixStats === 'function') {
146
+ return hooks.ensureWrapperPrefixStats(facts, tokens)
147
+ }
148
+ if (fallbackCache && fallbackCache.wrapperPrefixStats !== undefined) {
149
+ return fallbackCache.wrapperPrefixStats
150
+ }
151
+ const wrapperPrefixStats = (!tokens || tokens.length === 0)
152
+ ? null
153
+ : buildAsteriskWrapperPrefixStats(tokens)
154
+ if (fallbackCache) {
155
+ fallbackCache.wrapperPrefixStats = wrapperPrefixStats
156
+ }
157
+ return wrapperPrefixStats
158
+ }
159
+
160
+ const invalidateBrokenRefDerivedCaches = (facts = null, hooks = null, fallbackCache = null) => {
161
+ if (fallbackCache) {
162
+ fallbackCache.linkCloseMap = undefined
163
+ fallbackCache.wrapperPrefixStats = undefined
164
+ }
165
+ if (hooks && typeof hooks.invalidateDerivedCaches === 'function') {
166
+ hooks.invalidateDerivedCaches(facts)
167
+ }
168
+ }
169
+
170
+ const markBrokenRefLevelRebuildFrom = (facts = null, startIdx = 0, hooks = null) => {
171
+ if (hooks && typeof hooks.markLevelRebuildFrom === 'function') {
172
+ hooks.markLevelRebuildFrom(facts, startIdx)
173
+ }
174
+ }
175
+
176
+ const BROKEN_REF_FLOW_SKIP_NO_TEXT_MARKER = 'skip-no-text-marker'
177
+ const BROKEN_REF_FLOW_SKIP_GUARD = 'skip-guard'
178
+ const BROKEN_REF_FLOW_SKIP_NO_ACTIVE_SIGNATURE = 'skip-no-active-signature'
179
+ const BROKEN_REF_FLOW_SKIP_NO_FASTPATH_MATCH = 'skip-no-fastpath-match'
180
+ const BROKEN_REF_FLOW_REPAIRED = 'repaired'
181
+
182
+ const resolveBrokenRefSegmentEnd = (children, brokenRefCandidate, closeIdx, metrics = null) => {
183
+ let segmentEnd = expandSegmentEndForWrapperBalance(children, brokenRefCandidate.start, closeIdx)
184
+ if (segmentEnd !== -1) return segmentEnd
185
+ bumpBrokenRefMetric(metrics, 'brokenRefFlow', 'wrapper-expand-fallback')
186
+ return closeIdx
187
+ }
188
+
189
+ const resolveBrokenRefCandidateGuardFlow = (
190
+ children,
191
+ brokenRefCandidate,
192
+ segmentEnd,
193
+ facts = null,
194
+ hooks = null,
195
+ fallbackCache = null
196
+ ) => {
197
+ const wrapperSignals = buildBrokenRefWrapperRangeSignals(
198
+ children,
199
+ brokenRefCandidate.start,
200
+ segmentEnd,
201
+ brokenRefCandidate.startTextOffset
202
+ )
203
+ if (!wrapperSignals.hasTextMarker) {
204
+ return BROKEN_REF_FLOW_SKIP_NO_TEXT_MARKER
205
+ }
206
+ const wrapperPrefixStats = ensureBrokenRefWrapperPrefixStats(children, facts, hooks, fallbackCache)
207
+ if (!shouldAttemptBrokenRefRewrite(
208
+ children,
209
+ brokenRefCandidate.start,
210
+ segmentEnd,
211
+ brokenRefCandidate.startTextOffset,
212
+ wrapperPrefixStats,
213
+ wrapperSignals
214
+ )) {
215
+ return BROKEN_REF_FLOW_SKIP_GUARD
216
+ }
217
+ return null
218
+ }
219
+
220
+ const resolveBrokenRefFastPathFlow = (
221
+ children,
222
+ brokenRefCandidate,
223
+ segmentEnd,
224
+ linkCloseMap,
225
+ metrics = null
226
+ ) => {
227
+ const fastPathResult = applyBrokenRefTokenOnlyFastPath(
228
+ children,
229
+ brokenRefCandidate.start,
230
+ segmentEnd,
231
+ linkCloseMap,
232
+ metrics,
233
+ bumpBrokenRefMetric
234
+ )
235
+ if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_ACTIVE_SIGNATURE) {
236
+ return BROKEN_REF_FLOW_SKIP_NO_ACTIVE_SIGNATURE
237
+ }
238
+ if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_MATCH) {
239
+ return BROKEN_REF_FLOW_SKIP_NO_FASTPATH_MATCH
240
+ }
241
+ return BROKEN_REF_FLOW_REPAIRED
242
+ }
243
+
244
+ const runBrokenRefCandidateRewrite = (
245
+ children,
246
+ brokenRefCandidate,
247
+ closeIdx,
248
+ linkCloseMap,
249
+ metrics = null,
250
+ facts = null,
251
+ hooks = null,
252
+ fallbackCache = null
253
+ ) => {
254
+ const segmentEnd = resolveBrokenRefSegmentEnd(children, brokenRefCandidate, closeIdx, metrics)
255
+ const guardFlow = resolveBrokenRefCandidateGuardFlow(
256
+ children,
257
+ brokenRefCandidate,
258
+ segmentEnd,
259
+ facts,
260
+ hooks,
261
+ fallbackCache
262
+ )
263
+ if (guardFlow) return guardFlow
264
+ const fastPathFlow = resolveBrokenRefFastPathFlow(
265
+ children,
266
+ brokenRefCandidate,
267
+ segmentEnd,
268
+ linkCloseMap,
269
+ metrics
270
+ )
271
+ if (fastPathFlow !== BROKEN_REF_FLOW_REPAIRED) return fastPathFlow
272
+ invalidateBrokenRefDerivedCaches(facts, hooks, fallbackCache)
273
+ markBrokenRefLevelRebuildFrom(facts, brokenRefCandidate.start, hooks)
274
+ return BROKEN_REF_FLOW_REPAIRED
275
+ }
276
+
277
+ const resetBrokenRefCandidateState = (candidateState) => {
278
+ candidateState.start = -1
279
+ candidateState.depth = 0
280
+ candidateState.startTextOffset = 0
281
+ return candidateState
282
+ }
283
+
284
+ const startBrokenRefCandidateState = (candidateState, tokenIdx, scan) => {
285
+ candidateState.start = tokenIdx
286
+ candidateState.depth = scan.depth
287
+ candidateState.startTextOffset = scan.tailOpen > 0 ? scan.tailOpen : 0
288
+ return candidateState
289
+ }
290
+
291
+ const createBrokenRefSignalSeed = (facts = null) => {
292
+ return {
293
+ hasBracketText: !!(facts && facts.hasBracketText),
294
+ hasEmphasis: !!(facts && facts.hasEmphasis),
295
+ hasLinkClose: !!(facts && facts.hasLinkClose)
296
+ }
297
+ }
298
+
299
+ const createBrokenRefPassSignals = (seedSignals = null) => {
300
+ const seed = seedSignals || {}
301
+ return {
302
+ hasBracketText: !!seed.hasBracketText,
303
+ hasEmphasis: !!seed.hasEmphasis,
304
+ hasLinkClose: !!seed.hasLinkClose
305
+ }
306
+ }
307
+
308
+ const observeBrokenRefTextToken = (passSignals, candidateState, text, tokenIdx, scanState) => {
309
+ const hasOpenBracket = text.indexOf('[') !== -1
310
+ const hasCloseBracket = text.indexOf(']') !== -1
311
+ if (!passSignals.hasBracketText && (hasOpenBracket || hasCloseBracket)) {
312
+ passSignals.hasBracketText = true
313
+ }
314
+ if (candidateState.start === -1) {
315
+ if (!hasOpenBracket) return
316
+ const scan = scanBrokenRefState(text, scanState)
317
+ if (scan.brokenEnd) {
318
+ startBrokenRefCandidateState(candidateState, tokenIdx, scan)
319
+ }
320
+ return
321
+ }
322
+ if (!hasOpenBracket && !hasCloseBracket) return
323
+ candidateState.depth = updateBracketDepth(text, candidateState.depth)
324
+ if (candidateState.depth <= 0) {
325
+ resetBrokenRefCandidateState(candidateState)
326
+ }
327
+ }
328
+
329
+ const observeBrokenRefPassTokenFlags = (passSignals, child) => {
330
+ if (!passSignals.hasEmphasis && isAsteriskEmphasisToken(child)) {
331
+ passSignals.hasEmphasis = true
332
+ }
333
+ if (!passSignals.hasLinkClose && child.type === 'link_close') {
334
+ passSignals.hasLinkClose = true
335
+ }
336
+ }
337
+
338
+ const buildBrokenRefRepairPassResult = (didRepair, passSignals) => {
339
+ return {
340
+ didRepair,
341
+ hasBracketText: passSignals.hasBracketText,
342
+ hasEmphasis: passSignals.hasEmphasis,
343
+ hasLinkClose: passSignals.hasLinkClose
344
+ }
345
+ }
346
+
347
+ const collectBrokenRefPassSignals = (children, seedSignals = null) => {
348
+ const passSignals = createBrokenRefPassSignals(seedSignals)
349
+ if (!children || children.length === 0) return passSignals
350
+ for (let i = 0; i < children.length; i++) {
351
+ const child = children[i]
352
+ if (!child) continue
353
+ if (!passSignals.hasBracketText && child.type === 'text' && child.content) {
354
+ if (child.content.indexOf('[') !== -1 || child.content.indexOf(']') !== -1) {
355
+ passSignals.hasBracketText = true
356
+ }
357
+ }
358
+ observeBrokenRefPassTokenFlags(passSignals, child)
359
+ if (passSignals.hasBracketText && passSignals.hasEmphasis && passSignals.hasLinkClose) {
360
+ break
361
+ }
362
+ }
363
+ return passSignals
364
+ }
365
+
366
+ const tryRepairBrokenRefCandidateAtLinkOpen = (
367
+ children,
368
+ child,
369
+ childIdx,
370
+ brokenRefCandidate,
371
+ passSignals,
372
+ metrics = null,
373
+ facts = null,
374
+ hooks = null,
375
+ fallbackCache = null
376
+ ) => {
377
+ if (!child || child.type !== 'link_open' || brokenRefCandidate.start === -1) return null
378
+ if (brokenRefCandidate.depth <= 0) {
379
+ resetBrokenRefCandidateState(brokenRefCandidate)
380
+ return null
381
+ }
382
+ const linkCloseMap = ensureBrokenRefLinkCloseMap(children, facts, hooks, fallbackCache)
383
+ const closeIdx = linkCloseMap.get(childIdx) ?? -1
384
+ if (closeIdx === -1) return null
385
+ bumpBrokenRefMetric(metrics, 'brokenRefFlow', 'candidate')
386
+ const flowResult = runBrokenRefCandidateRewrite(
387
+ children,
388
+ brokenRefCandidate,
389
+ closeIdx,
390
+ linkCloseMap,
391
+ metrics,
392
+ facts,
393
+ hooks,
394
+ fallbackCache
395
+ )
396
+ if (flowResult !== BROKEN_REF_FLOW_REPAIRED) {
397
+ bumpBrokenRefMetric(metrics, 'brokenRefFlow', flowResult)
398
+ resetBrokenRefCandidateState(brokenRefCandidate)
399
+ return null
400
+ }
401
+ bumpBrokenRefMetric(metrics, 'brokenRefFlow', BROKEN_REF_FLOW_REPAIRED)
402
+ return buildBrokenRefRepairPassResult(true, passSignals)
403
+ }
404
+
405
+ const runBrokenRefRepairPass = (children, scanState, metrics = null, facts = null, hooks = null) => {
406
+ resetBrokenRefScanState(scanState)
407
+ const brokenRefCandidate = resetBrokenRefCandidateState({ start: -1, depth: 0, startTextOffset: 0 })
408
+ const passSignals = createBrokenRefPassSignals(createBrokenRefSignalSeed(facts))
409
+ const fallbackCache = {
410
+ linkCloseMap: undefined,
411
+ wrapperPrefixStats: undefined
412
+ }
413
+
414
+ for (let j = 0; j < children.length; j++) {
415
+ const child = children[j]
416
+ if (!child) continue
417
+
418
+ if (child.type === 'text' && child.content) {
419
+ observeBrokenRefTextToken(passSignals, brokenRefCandidate, child.content, j, scanState)
420
+ }
421
+
422
+ observeBrokenRefPassTokenFlags(passSignals, child)
423
+ const repaired = tryRepairBrokenRefCandidateAtLinkOpen(
424
+ children,
425
+ child,
426
+ j,
427
+ brokenRefCandidate,
428
+ passSignals,
429
+ metrics,
430
+ facts,
431
+ hooks,
432
+ fallbackCache
433
+ )
434
+ if (repaired) return repaired
435
+ }
436
+
437
+ return buildBrokenRefRepairPassResult(false, passSignals)
438
+ }
439
+
440
+ const computeMaxBrokenRefRepairPass = (children, scanState) => {
441
+ resetBrokenRefScanState(scanState)
442
+ let maxRepairPass = 0
443
+ for (let j = 0; j < children.length; j++) {
444
+ const child = children[j]
445
+ if (!child || child.type !== 'text' || !child.content) continue
446
+ if (child.content.indexOf('[') === -1) continue
447
+ if (scanBrokenRefState(child.content, scanState).brokenEnd) {
448
+ maxRepairPass++
449
+ }
450
+ }
451
+ return maxRepairPass
452
+ }
453
+
454
+ const runBrokenRefRepairs = (children, maxRepairPass, scanState, metrics = null, facts = null, hooks = null) => {
455
+ let repairPassCount = 0
456
+ let changed = false
457
+ while (repairPassCount < maxRepairPass) {
458
+ const pass = runBrokenRefRepairPass(children, scanState, metrics, facts, hooks)
459
+ if (!pass.didRepair) {
460
+ return {
461
+ changed,
462
+ hasBracketText: pass.hasBracketText,
463
+ hasEmphasis: pass.hasEmphasis,
464
+ hasLinkClose: pass.hasLinkClose
465
+ }
466
+ }
467
+ changed = true
468
+ repairPassCount++
469
+ }
470
+ const finalSignals = collectBrokenRefPassSignals(children, createBrokenRefSignalSeed(facts))
471
+ return {
472
+ changed,
473
+ hasBracketText: finalSignals.hasBracketText,
474
+ hasEmphasis: finalSignals.hasEmphasis,
475
+ hasLinkClose: finalSignals.hasLinkClose
476
+ }
477
+ }
478
+
479
+ export {
480
+ computeMaxBrokenRefRepairPass,
481
+ runBrokenRefRepairs
482
+ }