@peaceroad/markdown-it-strong-ja 0.9.0 → 0.9.2

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.
@@ -115,14 +115,14 @@ const expandSegmentEndForWrapperBalance = (tokens, startIdx, endIdx) => {
115
115
  return balance.total > 0 ? -1 : expandedEnd
116
116
  }
117
117
 
118
- const bumpBrokenRefMetric = (metrics, bucket, key) => {
119
- if (!metrics || !bucket || !key) return
118
+ const bumpBrokenRefMetric = (metrics, bucket, key, delta = 1) => {
119
+ if (!metrics || !bucket || !key || delta <= 0) return
120
120
  let table = metrics[bucket]
121
121
  if (!table || typeof table !== 'object') {
122
122
  table = Object.create(null)
123
123
  metrics[bucket] = table
124
124
  }
125
- table[key] = (table[key] || 0) + 1
125
+ table[key] = (table[key] || 0) + delta
126
126
  }
127
127
 
128
128
  const ensureBrokenRefLinkCloseMap = (tokens, facts = null, hooks = null, fallbackCache = null) => {
@@ -190,6 +190,7 @@ const resolveBrokenRefCandidateGuardFlow = (
190
190
  children,
191
191
  brokenRefCandidate,
192
192
  segmentEnd,
193
+ metrics = null,
193
194
  facts = null,
194
195
  hooks = null,
195
196
  fallbackCache = null
@@ -203,6 +204,10 @@ const resolveBrokenRefCandidateGuardFlow = (
203
204
  if (!wrapperSignals.hasTextMarker) {
204
205
  return BROKEN_REF_FLOW_SKIP_NO_TEXT_MARKER
205
206
  }
207
+ if (!hasBrokenRefActiveFastPathTokenSignal(wrapperSignals)) {
208
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'no-active-signature')
209
+ return BROKEN_REF_FLOW_SKIP_NO_ACTIVE_SIGNATURE
210
+ }
206
211
  const wrapperPrefixStats = ensureBrokenRefWrapperPrefixStats(children, facts, hooks, fallbackCache)
207
212
  if (!shouldAttemptBrokenRefRewrite(
208
213
  children,
@@ -214,6 +219,7 @@ const resolveBrokenRefCandidateGuardFlow = (
214
219
  )) {
215
220
  return BROKEN_REF_FLOW_SKIP_GUARD
216
221
  }
222
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'guard-passed')
217
223
  return null
218
224
  }
219
225
 
@@ -232,12 +238,16 @@ const resolveBrokenRefFastPathFlow = (
232
238
  metrics,
233
239
  bumpBrokenRefMetric
234
240
  )
241
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'fastpath-dispatch')
235
242
  if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_ACTIVE_SIGNATURE) {
243
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'no-active-signature')
236
244
  return BROKEN_REF_FLOW_SKIP_NO_ACTIVE_SIGNATURE
237
245
  }
238
246
  if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_MATCH) {
247
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'no-fastpath-match')
239
248
  return BROKEN_REF_FLOW_SKIP_NO_FASTPATH_MATCH
240
249
  }
250
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'repaired')
241
251
  return BROKEN_REF_FLOW_REPAIRED
242
252
  }
243
253
 
@@ -256,6 +266,7 @@ const runBrokenRefCandidateRewrite = (
256
266
  children,
257
267
  brokenRefCandidate,
258
268
  segmentEnd,
269
+ metrics,
259
270
  facts,
260
271
  hooks,
261
272
  fallbackCache
@@ -308,7 +319,7 @@ const createBrokenRefPassSignals = (seedSignals = null) => {
308
319
  const observeBrokenRefTextToken = (passSignals, candidateState, text, tokenIdx, scanState) => {
309
320
  const hasOpenBracket = text.indexOf('[') !== -1
310
321
  const hasCloseBracket = text.indexOf(']') !== -1
311
- if (!passSignals.hasBracketText && (hasOpenBracket || hasCloseBracket)) {
322
+ if (passSignals && !passSignals.hasBracketText && (hasOpenBracket || hasCloseBracket)) {
312
323
  passSignals.hasBracketText = true
313
324
  }
314
325
  if (candidateState.start === -1) {
@@ -383,6 +394,7 @@ const tryRepairBrokenRefCandidateAtLinkOpen = (
383
394
  const closeIdx = linkCloseMap.get(childIdx) ?? -1
384
395
  if (closeIdx === -1) return null
385
396
  bumpBrokenRefMetric(metrics, 'brokenRefFlow', 'candidate')
397
+ bumpBrokenRefMetric(metrics, 'brokenRefCandidateFlow', 'candidate')
386
398
  const flowResult = runBrokenRefCandidateRewrite(
387
399
  children,
388
400
  brokenRefCandidate,
@@ -437,46 +449,123 @@ const runBrokenRefRepairPass = (children, scanState, metrics = null, facts = nul
437
449
  return buildBrokenRefRepairPassResult(false, passSignals)
438
450
  }
439
451
 
440
- const computeMaxBrokenRefRepairPass = (children, scanState) => {
452
+ const hasPotentialBrokenRefRepairPass = (children, scanState) => {
441
453
  resetBrokenRefScanState(scanState)
442
- let maxRepairPass = 0
443
454
  for (let j = 0; j < children.length; j++) {
444
455
  const child = children[j]
445
456
  if (!child || child.type !== 'text' || !child.content) continue
446
457
  if (child.content.indexOf('[') === -1) continue
447
458
  if (scanBrokenRefState(child.content, scanState).brokenEnd) {
448
- maxRepairPass++
459
+ return true
449
460
  }
450
461
  }
451
- return maxRepairPass
462
+ return false
463
+ }
464
+
465
+ const hasBrokenRefActiveFastPathTokenSignal = (wrapperSignals) => {
466
+ if (!wrapperSignals) return false
467
+ // Current broken-ref fast paths are all strong-token driven.
468
+ return wrapperSignals.strongOpenInRange > 0 || wrapperSignals.strongCloseInRange > 0
452
469
  }
453
470
 
454
- const runBrokenRefRepairs = (children, maxRepairPass, scanState, metrics = null, facts = null, hooks = null) => {
471
+ const countGuardedBrokenRefRepairPasses = (children, scanState, facts = null, hooks = null) => {
472
+ resetBrokenRefScanState(scanState)
473
+ const brokenRefCandidate = resetBrokenRefCandidateState({ start: -1, depth: 0, startTextOffset: 0 })
474
+ const fallbackCache = {
475
+ linkCloseMap: undefined,
476
+ wrapperPrefixStats: undefined
477
+ }
455
478
  let repairPassCount = 0
479
+ for (let j = 0; j < children.length; j++) {
480
+ const child = children[j]
481
+ if (!child) continue
482
+ if (child.type === 'text' && child.content) {
483
+ observeBrokenRefTextToken(null, brokenRefCandidate, child.content, j, scanState)
484
+ }
485
+ if (child.type !== 'link_open' || brokenRefCandidate.start === -1) continue
486
+ if (brokenRefCandidate.depth <= 0) {
487
+ resetBrokenRefCandidateState(brokenRefCandidate)
488
+ continue
489
+ }
490
+ const linkCloseMap = ensureBrokenRefLinkCloseMap(children, facts, hooks, fallbackCache)
491
+ const closeIdx = linkCloseMap.get(j) ?? -1
492
+ if (closeIdx === -1) continue
493
+ const segmentEnd = resolveBrokenRefSegmentEnd(children, brokenRefCandidate, closeIdx)
494
+ const wrapperSignals = buildBrokenRefWrapperRangeSignals(
495
+ children,
496
+ brokenRefCandidate.start,
497
+ segmentEnd,
498
+ brokenRefCandidate.startTextOffset
499
+ )
500
+ if (!wrapperSignals.hasTextMarker || !hasBrokenRefActiveFastPathTokenSignal(wrapperSignals)) {
501
+ resetBrokenRefCandidateState(brokenRefCandidate)
502
+ continue
503
+ }
504
+ const wrapperPrefixStats = ensureBrokenRefWrapperPrefixStats(children, facts, hooks, fallbackCache)
505
+ if (shouldAttemptBrokenRefRewrite(
506
+ children,
507
+ brokenRefCandidate.start,
508
+ segmentEnd,
509
+ brokenRefCandidate.startTextOffset,
510
+ wrapperPrefixStats,
511
+ wrapperSignals
512
+ )) {
513
+ repairPassCount++
514
+ }
515
+ resetBrokenRefCandidateState(brokenRefCandidate)
516
+ }
517
+ return repairPassCount
518
+ }
519
+
520
+ const buildBrokenRefRepairsResult = (changed, passSignals) => {
521
+ return {
522
+ changed,
523
+ hasBracketText: passSignals.hasBracketText,
524
+ hasEmphasis: passSignals.hasEmphasis,
525
+ hasLinkClose: passSignals.hasLinkClose
526
+ }
527
+ }
528
+
529
+ const runBrokenRefRepairs = (children, scanState, metrics = null, facts = null, hooks = null) => {
530
+ const seedSignals = createBrokenRefPassSignals(createBrokenRefSignalSeed(facts))
531
+ if (!hasPotentialBrokenRefRepairPass(children, scanState)) {
532
+ return buildBrokenRefRepairsResult(false, seedSignals)
533
+ }
534
+
456
535
  let changed = false
457
- while (repairPassCount < maxRepairPass) {
458
- const pass = runBrokenRefRepairPass(children, scanState, metrics, facts, hooks)
536
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'budgeted')
537
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'executed')
538
+
539
+ let pass = runBrokenRefRepairPass(children, scanState, metrics, facts, hooks)
540
+ if (!pass.didRepair) {
541
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'stopped-no-repair')
542
+ return buildBrokenRefRepairsResult(changed, pass)
543
+ }
544
+
545
+ changed = true
546
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'repaired')
547
+
548
+ const remainingBudget = countGuardedBrokenRefRepairPasses(children, scanState, facts, hooks)
549
+ if (remainingBudget > 0) {
550
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'budgeted', remainingBudget)
551
+ }
552
+
553
+ let repairPassCount = 0
554
+ while (repairPassCount < remainingBudget) {
555
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'executed')
556
+ pass = runBrokenRefRepairPass(children, scanState, metrics, facts, hooks)
459
557
  if (!pass.didRepair) {
460
- return {
461
- changed,
462
- hasBracketText: pass.hasBracketText,
463
- hasEmphasis: pass.hasEmphasis,
464
- hasLinkClose: pass.hasLinkClose
465
- }
558
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'stopped-no-repair')
559
+ return buildBrokenRefRepairsResult(changed, pass)
466
560
  }
467
561
  changed = true
468
562
  repairPassCount++
563
+ bumpBrokenRefMetric(metrics, 'brokenRefPasses', 'repaired')
469
564
  }
470
565
  const finalSignals = collectBrokenRefPassSignals(children, createBrokenRefSignalSeed(facts))
471
- return {
472
- changed,
473
- hasBracketText: finalSignals.hasBracketText,
474
- hasEmphasis: finalSignals.hasEmphasis,
475
- hasLinkClose: finalSignals.hasLinkClose
476
- }
566
+ return buildBrokenRefRepairsResult(changed, finalSignals)
477
567
  }
478
568
 
479
569
  export {
480
- computeMaxBrokenRefRepairPass,
481
570
  runBrokenRefRepairs
482
571
  }
@@ -0,0 +1,50 @@
1
+ const fallbackMarkupByType = (type) => {
2
+ if (type === 'strong_open' || type === 'strong_close') return '**'
3
+ if (type === 'em_open' || type === 'em_close') return '*'
4
+ return ''
5
+ }
6
+
7
+ const makeTokenLiteralText = (token) => {
8
+ if (!token) return
9
+ const literal = token.markup || fallbackMarkupByType(token.type)
10
+ token.type = 'text'
11
+ token.tag = ''
12
+ token.nesting = 0
13
+ token.content = literal
14
+ token.markup = ''
15
+ token.info = ''
16
+ }
17
+
18
+ const sanitizeEmStrongBalance = (tokens, onChangeStart = null) => {
19
+ if (!tokens || tokens.length === 0) return false
20
+ const stack = []
21
+ let changed = false
22
+ for (let i = 0; i < tokens.length; i++) {
23
+ const token = tokens[i]
24
+ if (!token || !token.type) continue
25
+ if (token.type === 'strong_open' || token.type === 'em_open') {
26
+ stack.push({ type: token.type, idx: i })
27
+ continue
28
+ }
29
+ if (token.type !== 'strong_close' && token.type !== 'em_close') continue
30
+ const expected = token.type === 'strong_close' ? 'strong_open' : 'em_open'
31
+ if (stack.length > 0 && stack[stack.length - 1].type === expected) {
32
+ stack.pop()
33
+ continue
34
+ }
35
+ if (onChangeStart) onChangeStart(i)
36
+ makeTokenLiteralText(token)
37
+ changed = true
38
+ }
39
+ for (let i = stack.length - 1; i >= 0; i--) {
40
+ const entry = stack[i]
41
+ const token = tokens[entry.idx]
42
+ if (!token) continue
43
+ if (onChangeStart) onChangeStart(entry.idx)
44
+ makeTokenLiteralText(token)
45
+ changed = true
46
+ }
47
+ return changed
48
+ }
49
+
50
+ export { sanitizeEmStrongBalance }
@@ -1,9 +1,5 @@
1
1
  import Token from 'markdown-it/lib/token.mjs'
2
-
3
- const cloneMap = (map) => {
4
- if (!map || !Array.isArray(map)) return null
5
- return [map[0], map[1]]
6
- }
2
+ import { cloneMap } from '../token-utils.js'
7
3
 
8
4
  const cloneTextLike = (source, content) => {
9
5
  const token = new Token('text', '', 0)
@@ -1,6 +1,11 @@
1
- import { isJapaneseChar } from '../token-utils.js'
1
+ import { codePointAtSafe, codePointBeforeSafe, codePointSize, isJapaneseChar } from '../token-utils.js'
2
2
 
3
3
  const CHAR_ASTERISK = 0x2A // *
4
+ const INLINE_REPAIR_EM_OUTER_STRONG_SEQUENCE = 1 << 0
5
+ const INLINE_REPAIR_TAIL_AFTER_LINK = 1 << 1
6
+ const INLINE_REPAIR_LEADING_ASTERISK_EM = 1 << 2
7
+ const INLINE_REPAIR_TRAILING_STRONG = 1 << 3
8
+ const INLINE_REPAIR_BALANCE_SANITIZE = 1 << 4
4
9
 
5
10
  const hasMarkerChars = (text) => {
6
11
  return !!text && text.indexOf('*') !== -1
@@ -41,11 +46,13 @@ const tokenHasJapaneseChars = (token) => {
41
46
  return token.__strongJaHasJapaneseChar
42
47
  }
43
48
  let hasJapanese = false
44
- for (let i = 0; i < content.length; i++) {
45
- if (isJapaneseChar(content.charCodeAt(i))) {
49
+ for (let i = 0; i < content.length;) {
50
+ const code = codePointAtSafe(content, i)
51
+ if (isJapaneseChar(code)) {
46
52
  hasJapanese = true
47
53
  break
48
54
  }
55
+ i += codePointSize(code)
49
56
  }
50
57
  token.__strongJaJapaneseSource = content
51
58
  token.__strongJaHasJapaneseChar = hasJapanese
@@ -98,9 +105,9 @@ const countDelimiterLikeStrongRuns = (content, from = 0, limit = 0) => {
98
105
  continue
99
106
  }
100
107
  const pos = at
101
- const prevCode = pos > 0 ? content.charCodeAt(pos - 1) : 0
108
+ const prevCode = codePointBeforeSafe(content, pos, 0)
102
109
  const nextPos = pos + 2
103
- const nextCode = nextPos < len ? content.charCodeAt(nextPos) : 0
110
+ const nextCode = codePointAtSafe(content, nextPos, 0)
104
111
  const prevSameMarker = prevCode === CHAR_ASTERISK
105
112
  const nextSameMarker = nextCode === CHAR_ASTERISK
106
113
  if (prevSameMarker || nextSameMarker) {
@@ -389,13 +396,9 @@ const hasBrokenRefImmediateRewriteSignal = (wrapperSignals) => {
389
396
  return wrapperSignals.hasImbalance && hasBrokenRefExplicitAsteriskSignal(wrapperSignals)
390
397
  }
391
398
 
392
- const shouldRejectBalancedBrokenRefRewrite = (wrapperSignals) => {
393
- return !wrapperSignals.hasImbalance && hasBrokenRefExplicitAsteriskSignal(wrapperSignals)
394
- }
395
-
396
399
  const shouldAttemptBrokenRefRewriteFromSignals = (wrapperSignals) => {
397
400
  if (hasBrokenRefImmediateRewriteSignal(wrapperSignals)) return true
398
- if (shouldRejectBalancedBrokenRefRewrite(wrapperSignals)) return false
401
+ if (!wrapperSignals.hasImbalance && hasBrokenRefExplicitAsteriskSignal(wrapperSignals)) return false
399
402
  return hasBrokenRefStrongRunEvidence(wrapperSignals)
400
403
  }
401
404
 
@@ -413,16 +416,47 @@ const shouldAttemptBrokenRefRewrite = (
413
416
  return shouldAttemptBrokenRefRewriteFromSignals(signals)
414
417
  }
415
418
 
416
- const scanInlinePostprocessSignals = (children) => {
419
+ const scanInlinePostprocessSignals = (children, collectJapaneseContext = false) => {
417
420
  let hasEmphasis = false
418
421
  let hasLinkOpen = false
419
422
  let hasLinkClose = false
420
423
  let hasCodeInline = false
424
+ let hasJapaneseContext = false
425
+ let hasTextStrongMarker = false
426
+ let strongOpenCount = 0
427
+ let strongCloseCount = 0
428
+ let emOpenCount = 0
429
+ let emCloseCount = 0
430
+ let hasAsteriskWrapperImbalance = false
431
+ const emphasisStack = []
421
432
  for (let j = 0; j < children.length; j++) {
422
433
  const child = children[j]
423
434
  if (!child) continue
424
- if (!hasEmphasis && isAsteriskEmphasisToken(child)) {
435
+ if (collectJapaneseContext && !hasJapaneseContext && tokenHasJapaneseChars(child)) {
436
+ hasJapaneseContext = true
437
+ }
438
+ if (!hasTextStrongMarker && child.type === 'text' && child.content && child.content.indexOf('**') !== -1) {
439
+ hasTextStrongMarker = true
440
+ }
441
+ const isAsteriskEmphasis = isAsteriskEmphasisToken(child)
442
+ if (isAsteriskEmphasis) {
425
443
  hasEmphasis = true
444
+ if (child.type === 'strong_open') strongOpenCount++
445
+ else if (child.type === 'strong_close') strongCloseCount++
446
+ else if (child.type === 'em_open') emOpenCount++
447
+ else if (child.type === 'em_close') emCloseCount++
448
+ if (!hasAsteriskWrapperImbalance) {
449
+ if (child.type === 'strong_open' || child.type === 'em_open') {
450
+ emphasisStack.push(child.type)
451
+ } else {
452
+ const expected = child.type === 'strong_close' ? 'strong_open' : 'em_open'
453
+ if (emphasisStack.length > 0 && emphasisStack[emphasisStack.length - 1] === expected) {
454
+ emphasisStack.pop()
455
+ } else {
456
+ hasAsteriskWrapperImbalance = true
457
+ }
458
+ }
459
+ }
426
460
  }
427
461
  if (!hasLinkOpen && child.type === 'link_open') {
428
462
  hasLinkOpen = true
@@ -433,13 +467,34 @@ const scanInlinePostprocessSignals = (children) => {
433
467
  if (!hasCodeInline && child.type === 'code_inline') {
434
468
  hasCodeInline = true
435
469
  }
436
- if (hasEmphasis && hasLinkOpen && hasLinkClose) break
470
+ }
471
+ if (!hasAsteriskWrapperImbalance && emphasisStack.length > 0) {
472
+ hasAsteriskWrapperImbalance = true
473
+ }
474
+ let repairMask = 0
475
+ if (emOpenCount >= 2 && emCloseCount >= 2 && strongOpenCount > 0) {
476
+ repairMask |= INLINE_REPAIR_EM_OUTER_STRONG_SEQUENCE
477
+ }
478
+ if (hasLinkClose && strongCloseCount > 0) {
479
+ repairMask |= INLINE_REPAIR_TAIL_AFTER_LINK
480
+ }
481
+ if (hasLinkClose && emCloseCount > 0) {
482
+ repairMask |= INLINE_REPAIR_LEADING_ASTERISK_EM
483
+ }
484
+ if (emOpenCount > 0 && emCloseCount > 0 && hasTextStrongMarker) {
485
+ repairMask |= INLINE_REPAIR_TRAILING_STRONG
486
+ }
487
+ if (hasAsteriskWrapperImbalance) {
488
+ repairMask |= INLINE_REPAIR_BALANCE_SANITIZE
437
489
  }
438
490
  return {
439
491
  hasEmphasis,
440
492
  hasLinkOpen,
441
493
  hasLinkClose,
442
- hasCodeInline
494
+ hasCodeInline,
495
+ hasJapaneseContext,
496
+ repairMask,
497
+ hasAsteriskWrapperImbalance
443
498
  }
444
499
  }
445
500
 
@@ -451,5 +506,10 @@ export {
451
506
  buildAsteriskWrapperPrefixStats,
452
507
  buildBrokenRefWrapperRangeSignals,
453
508
  shouldAttemptBrokenRefRewrite,
454
- scanInlinePostprocessSignals
509
+ scanInlinePostprocessSignals,
510
+ INLINE_REPAIR_EM_OUTER_STRONG_SEQUENCE,
511
+ INLINE_REPAIR_TAIL_AFTER_LINK,
512
+ INLINE_REPAIR_LEADING_ASTERISK_EM,
513
+ INLINE_REPAIR_TRAILING_STRONG,
514
+ INLINE_REPAIR_BALANCE_SANITIZE
455
515
  }