@peaceroad/markdown-it-strong-ja 0.8.0 → 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.
package/README.md CHANGED
@@ -98,6 +98,14 @@ Terms used below:
98
98
  - Run: a contiguous group of the same marker (`*`, `**`, `***`, ...).
99
99
  - Line: text split by `\n`.
100
100
 
101
+ ### TL;DR
102
+
103
+ - Baseline: start from plain `markdown-it` delimiter pairing.
104
+ - Local helper path: only `*` runs with local Japanese context enter strong-ja boundary logic.
105
+ - Mixed-text guard: `japanese-boundary-guard` additionally suppresses mixed JA/EN over-conversion.
106
+ - Postprocess: token-only repairs run only for malformed link/reference-adjacent spans.
107
+
108
+
101
109
  ### Step 1: Build the baseline with plain `markdown-it`
102
110
 
103
111
  `markdown-it` runs first. If it can already parse a pattern (including cross-line `**...**`), that baseline structure is kept.
@@ -368,6 +376,7 @@ Supporting visuals:
368
376
  - Default: `true`
369
377
  - Set `false` to disable link/reference postprocess repairs.
370
378
  - In `mode: 'compatible'`, repairs are skipped even when this is `true`.
379
+ - Repairs stay local to malformed link/reference-adjacent spans; valid inputs such as `[w](u) *string* [w](u)` are left unchanged.
371
380
 
372
381
  ### `coreRulesBeforePostprocess`
373
382
 
@@ -388,6 +397,9 @@ Supporting visuals:
388
397
  ## Notes
389
398
 
390
399
  - Use `state.env.__strongJaTokenOpt` to override options per render.
391
- - Overrides are merged with plugin options, but setup-time behavior (such as rule registration/order) cannot be switched at render time.
392
- - This is an ESM plugin (`type: module`) and works in Node.js, browser bundlers, and VS Code extension pipelines that use `markdown-it` ESM.
400
+ - Overrides are merged with plugin options, but setup-time behavior (such as rule registration/order) cannot be switched at render time and cannot be retrofitted after the first `.use(...)` on the same `MarkdownIt` instance.
401
+ - `mode` and `postprocess` are runtime-effective. `mditAttrs`, `patchCorePush`, and `coreRulesBeforePostprocess` are setup-time effective after the first `.use(...)` on a `MarkdownIt` instance.
402
+ - This is an ESM plugin (`type: module`) and is tested against `markdown-it` 14.x in Node.js, browser bundlers, and VS Code extension pipelines that use `markdown-it` ESM.
403
+ - The implementation relies on `markdown-it` internal ESM modules / core rule internals (`lib/token.mjs`, `lib/common/utils.mjs`, `ruler.__rules__`) plus a `scanDelims` prototype patch, so internal `markdown-it` changes may require plugin updates.
393
404
  - `scanDelims` patch is applied once per `MarkdownIt` prototype in the same process.
405
+
package/index.js CHANGED
@@ -1,32 +1,49 @@
1
- import { hasCjkBreaksRule, normalizeCoreRulesBeforePostprocess, ensureCoreRuleOrder, deriveModeInfo } from './src/token-utils.js'
1
+ import { hasCjkBreaksRule, ensureCoreRuleOrder, deriveOptionInfo } from './src/token-utils.js'
2
2
  import { patchScanDelims } from './src/token-core.js'
3
3
  import { registerTokenCompat } from './src/token-compat.js'
4
4
  import { registerTokenPostprocess } from './src/token-postprocess.js'
5
5
 
6
+ const DEFAULT_OPTION = {
7
+ mditAttrs: true, // assume markdown-it-attrs integration by default
8
+ mode: 'japanese', // 'japanese'(->japanese-boundary-guard) | 'japanese-boundary' | 'japanese-boundary-guard' | 'aggressive' | 'compatible'
9
+ coreRulesBeforePostprocess: [], // e.g. ['cjk_breaks'] to keep rules ahead of postprocess
10
+ postprocess: true, // enable link/ref reconstruction pass
11
+ patchCorePush: true // keep restore-softbreaks after late cjk_breaks
12
+ }
13
+
14
+ const buildNormalizedOption = (md, option) => {
15
+ const opt = { ...DEFAULT_OPTION }
16
+ if (option) Object.assign(opt, option)
17
+ opt.hasCjkBreaks = hasCjkBreaksRule(md)
18
+ deriveOptionInfo(opt)
19
+ return opt
20
+ }
21
+
22
+ const writeSharedOption = (target, source) => {
23
+ for (const key of Object.keys(target)) {
24
+ delete target[key]
25
+ }
26
+ Object.assign(target, source)
27
+ return target
28
+ }
29
+
6
30
  const mditStrongJa = (md, option) => {
7
- if (option && typeof option.engine === 'string' && option.engine !== 'token') {
8
- throw new Error('mditStrongJa: legacy engine was removed; use token (default)')
9
- }
10
- const opt = {
11
- mditAttrs: true, // assume markdown-it-attrs integration by default
12
- mode: 'japanese', // 'japanese'(->japanese-boundary-guard) | 'japanese-boundary' | 'japanese-boundary-guard' | 'aggressive' | 'compatible'
13
- coreRulesBeforePostprocess: [], // e.g. ['cjk_breaks'] to keep rules ahead of postprocess
14
- postprocess: true, // enable link/ref reconstruction pass
15
- patchCorePush: true // keep restore-softbreaks after late cjk_breaks
16
- }
17
- if (option) Object.assign(opt, option)
18
- opt.hasCjkBreaks = hasCjkBreaksRule(md)
19
- deriveModeInfo(opt)
20
-
31
+ if (option && typeof option.engine === 'string' && option.engine !== 'token') {
32
+ throw new Error('mditStrongJa: legacy engine was removed; use token (default)')
33
+ }
34
+ const nextOpt = buildNormalizedOption(md, option)
35
+ const opt = md.__strongJaTokenOpt && typeof md.__strongJaTokenOpt === 'object'
36
+ ? writeSharedOption(md.__strongJaTokenOpt, nextOpt)
37
+ : nextOpt
38
+
21
39
  md.__strongJaTokenOpt = opt
22
40
  patchScanDelims(md)
23
41
  registerTokenCompat(md, opt)
24
42
 
25
43
  registerTokenPostprocess(md, opt)
26
- const coreRulesBeforePostprocess = normalizeCoreRulesBeforePostprocess(opt.coreRulesBeforePostprocess)
27
- ensureCoreRuleOrder(md, coreRulesBeforePostprocess, 'strong_ja_token_postprocess')
44
+ ensureCoreRuleOrder(md, opt.__strongJaNormalizedCoreRulesBeforePostprocess, 'strong_ja_token_postprocess')
28
45
 
29
46
  return md
30
47
  }
31
-
32
- export default mditStrongJa
48
+
49
+ export default mditStrongJa
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-strong-ja",
3
- "description": "This is a plugin for markdown-it. It is an alternative to the standard `**` (strong) and `*` (em) processing. It also processes strings that cannot be converted by the standard.",
4
- "version": "0.8.0",
3
+ "description": "Extends asterisk emphasis handling for Japanese text while keeping markdown-it behavior as close as practical.",
4
+ "version": "0.8.1",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "files": [
@@ -18,6 +18,7 @@
18
18
  "test:postprocess-fastpath": "node test/post-processing-fastpath.test.js",
19
19
  "test:postprocess-fastpath-roster": "node test/post-processing-fastpath-roster.test.js",
20
20
  "test:postprocess-flow": "node test/post-processing-flow.test.js",
21
+ "test:postprocess-link-helper": "node test/post-processing-link-helper.test.js",
21
22
  "test:postprocess-gate": "node test/postprocess-gate.js",
22
23
  "test:tokenonly-progress": "node test/post-processing-progress.test.js",
23
24
  "test:readme": "node test/test-readme.js",
@@ -35,13 +36,14 @@
35
36
  "markdown-it": "^14.1.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.7",
39
- "@peaceroad/markdown-it-hr-sandwiched-semantic-container": "^0.8.5",
40
- "@peaceroad/markdown-it-renderer-image": "^0.10.0",
41
- "@peaceroad/markdown-it-renderer-inline-text": "^0.6.0",
39
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.8",
40
+ "@peaceroad/markdown-it-hr-sandwiched-semantic-container": "^0.10.0",
41
+ "@peaceroad/markdown-it-renderer-image": "^0.12.0",
42
+ "@peaceroad/markdown-it-renderer-inline-text": "^0.7.0",
42
43
  "markdown-it-attrs": "^4.3.1",
43
44
  "markdown-it-sub": "^2.0.0",
44
45
  "markdown-it-sup": "^2.0.0",
45
46
  "p7d-markdown-it-p-captions": "^0.21.0"
46
47
  }
47
48
  }
49
+
@@ -4,8 +4,6 @@ import {
4
4
  isJapaneseChar,
5
5
  hasCjkBreaksRule,
6
6
  isCjkBreaksRuleName,
7
- deriveModeInfo,
8
- MODE_FLAG_COMPATIBLE,
9
7
  getRuntimeOpt,
10
8
  moveRuleAfter
11
9
  } from './token-utils.js'
@@ -28,15 +26,11 @@ const trimTrailingSpaceTab = (text) => {
28
26
  }
29
27
 
30
28
  const registerTokenCompat = (md, baseOpt) => {
31
- const baseModeFlags = typeof baseOpt.__strongJaModeFlags === 'number'
32
- ? baseOpt.__strongJaModeFlags
33
- : deriveModeInfo(baseOpt).__strongJaModeFlags
34
- const baseCompatible = (baseModeFlags & MODE_FLAG_COMPATIBLE) !== 0
35
29
  const isCompatibleMode = (state) => {
36
30
  const override = state && state.env && state.env.__strongJaTokenOpt
37
- if (!override) return baseCompatible
31
+ if (!override) return baseOpt.__strongJaIsCompatibleMode === true
38
32
  const opt = getRuntimeOpt(state, baseOpt)
39
- return (opt.__strongJaModeFlags & MODE_FLAG_COMPATIBLE) !== 0
33
+ return opt.__strongJaIsCompatibleMode === true
40
34
  }
41
35
 
42
36
  let hasTextJoinRule = false
package/src/token-core.js CHANGED
@@ -194,6 +194,28 @@ const rebuildInlineLevels = (tokens) => {
194
194
  }
195
195
  }
196
196
 
197
+ const rebuildInlineLevelsFrom = (tokens, startIdx = 0) => {
198
+ if (!tokens || tokens.length === 0) return
199
+ let from = startIdx > 0 ? startIdx : 0
200
+ if (from >= tokens.length) return
201
+ let level = 0
202
+ if (from > 0) {
203
+ const prev = tokens[from - 1]
204
+ if (prev) {
205
+ level = prev.level
206
+ if (prev.nesting === 1) level++
207
+ else if (prev.nesting === -1) level--
208
+ }
209
+ }
210
+ for (let i = from; i < tokens.length; i++) {
211
+ const t = tokens[i]
212
+ if (!t) continue
213
+ t.level = level
214
+ if (t.nesting === 1) level++
215
+ else if (t.nesting === -1) level--
216
+ }
217
+ }
218
+
197
219
  const findLinkOpen = (tokens, closeIdx) => {
198
220
  let depth = 0
199
221
  for (let i = closeIdx; i >= 0; i--) {
@@ -396,7 +418,7 @@ const ensurePrevStarFlags = (src, start, prevStarFlags) => {
396
418
  return prevStarFlags >= 0 ? prevStarFlags : scanPrevSingleStarContextFlags(src, start)
397
419
  }
398
420
 
399
- const fixTrailingStrong = (tokens) => {
421
+ const fixTrailingStrong = (tokens, onChangeStart = null) => {
400
422
  let changed = false
401
423
  for (let i = 1; i < tokens.length; i++) {
402
424
  const token = tokens[i]
@@ -435,21 +457,23 @@ const fixTrailingStrong = (tokens) => {
435
457
  tokens[innerOpenIdx + 2] && tokens[innerOpenIdx + 2].type === 'em_close' &&
436
458
  tokens[innerOpenIdx + 3] && tokens[innerOpenIdx + 3].type === 'text' &&
437
459
  closeIdx === innerOpenIdx + 4) {
438
- tokens.splice(innerOpenIdx + 2, 1)
439
- tokens.splice(innerOpenIdx, 1)
440
460
  const movedOpen = new Token('em_open', 'em', 1)
441
461
  movedOpen.markup = '*'
442
462
  const movedClose = new Token('em_close', 'em', -1)
443
463
  movedClose.markup = '*'
444
- tokens.splice(innerOpenIdx + 1, 0, movedOpen)
445
- tokens.splice(innerOpenIdx + 3, 0, movedClose)
464
+ const innerReplacement = [
465
+ tokens[innerOpenIdx + 1],
466
+ movedOpen,
467
+ tokens[innerOpenIdx + 3],
468
+ movedClose
469
+ ]
470
+ tokens.splice(innerOpenIdx, 4, ...innerReplacement)
446
471
  }
447
472
 
448
473
  const before = token.content.slice(0, starIdx)
449
474
  const after = token.content.slice(starIdx + 2)
450
475
 
451
- tokens.splice(closeIdx, 1)
452
- if (closeIdx < i) i--
476
+ if (onChangeStart) onChangeStart(openIdx)
453
477
 
454
478
  const openToken = tokens[openIdx]
455
479
  if (!openToken) continue
@@ -458,28 +482,27 @@ const fixTrailingStrong = (tokens) => {
458
482
  openToken.markup = '**'
459
483
  openToken.nesting = 1
460
484
 
485
+ const strongClose = new Token('strong_close', 'strong', -1)
486
+ strongClose.markup = '**'
487
+ const trailingReplacement = []
461
488
  if (before) {
462
489
  token.content = before
463
- } else {
464
- tokens.splice(i, 1)
465
- i--
490
+ trailingReplacement.push(token)
466
491
  }
467
-
468
- const insertAt = i + 1
469
- const strongClose = new Token('strong_close', 'strong', -1)
470
- strongClose.markup = '**'
471
- tokens.splice(insertAt, 0, strongClose)
492
+ trailingReplacement.push(strongClose)
472
493
  if (after) {
473
494
  const tail = new Token('text', '', 0)
474
495
  tail.content = after
475
- tokens.splice(insertAt + 1, 0, tail)
496
+ trailingReplacement.push(tail)
476
497
  }
498
+ tokens.splice(closeIdx, 2, ...trailingReplacement)
499
+ i = closeIdx + trailingReplacement.length - 1
477
500
  changed = true
478
501
  }
479
502
  return changed
480
503
  }
481
504
 
482
- function fixEmOuterStrongSequence(tokens) {
505
+ function fixEmOuterStrongSequence(tokens, onChangeStart = null) {
483
506
  let changed = false
484
507
  let i = 0
485
508
  while (i < tokens.length) {
@@ -536,6 +559,7 @@ function fixEmOuterStrongSequence(tokens) {
536
559
  continue
537
560
  }
538
561
 
562
+ if (onChangeStart) onChangeStart(idx0)
539
563
  t0.type = 'strong_open'
540
564
  t0.tag = 'strong'
541
565
  t0.markup = '**'
@@ -548,28 +572,23 @@ function fixEmOuterStrongSequence(tokens) {
548
572
  const strongClose = new Token('strong_close', 'strong', -1)
549
573
  strongClose.markup = '**'
550
574
 
551
- tokens.splice(idx7, 1)
552
- tokens.splice(idx5, 1)
553
- tokens.splice(idx3, 1)
554
- tokens.splice(idx1, 1)
555
-
556
- const idx4AfterRemove = idx4 - 2
557
- const idx6AfterRemove = idx6 - 3
558
- tokens.splice(idx4AfterRemove, 0, emOpen)
559
-
560
- const idx6BeforeEmClose = idx6AfterRemove + 1
561
- tokens.splice(idx6BeforeEmClose, 0, emClose)
562
-
563
- const idx6AfterEmClose = idx6BeforeEmClose + 1
564
- tokens.splice(idx6AfterEmClose + 1, 0, strongClose)
575
+ const replacement = [
576
+ ...tokens.slice(idx1 + 1, idx3),
577
+ emOpen,
578
+ ...tokens.slice(idx3 + 1, idx5),
579
+ emClose,
580
+ ...tokens.slice(idx5 + 1, idx7),
581
+ strongClose
582
+ ]
583
+ tokens.splice(idx1, idx7 - idx1 + 1, ...replacement)
565
584
 
566
585
  changed = true
567
- i = idx6AfterEmClose + 2
586
+ i = idx1 + replacement.length
568
587
  }
569
588
  return changed
570
589
  }
571
590
 
572
- const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
591
+ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx, onChangeStart = null) => {
573
592
  let openIdx = findMatchingEmOpen(tokens, closeIdx)
574
593
  if (openIdx === -1 || openIdx < rangeStart || openIdx >= rangeEnd) return false
575
594
 
@@ -598,33 +617,26 @@ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
598
617
  const starToken = tokens[starTokenIdx]
599
618
  const before = starToken.content.slice(0, starPos)
600
619
  const after = starToken.content.slice(starPos + 1)
601
- let insertAt = starTokenIdx
602
- if (before) {
603
- starToken.content = before
604
- insertAt = starTokenIdx + 1
605
- } else {
606
- tokens.splice(starTokenIdx, 1)
607
- if (starTokenIdx < openIdx) {
608
- openIdx--
609
- closeIdx--
610
- }
611
- }
620
+ if (onChangeStart) onChangeStart(starTokenIdx)
612
621
 
613
622
  const emOpen = new Token('em_open', 'em', 1)
614
623
  emOpen.markup = '*'
615
- tokens.splice(insertAt, 0, emOpen)
616
- if (insertAt <= openIdx) {
617
- openIdx++
618
- closeIdx++
624
+ const prefixReplacement = []
625
+ if (before) {
626
+ starToken.content = before
627
+ prefixReplacement.push(starToken)
619
628
  }
629
+ prefixReplacement.push(emOpen)
620
630
  if (after) {
621
631
  const afterToken = new Token('text', '', 0)
622
632
  afterToken.content = after
623
- tokens.splice(insertAt + 1, 0, afterToken)
624
- if (insertAt + 1 <= openIdx) {
625
- openIdx++
626
- closeIdx++
627
- }
633
+ prefixReplacement.push(afterToken)
634
+ }
635
+ const prefixDelta = prefixReplacement.length - 1
636
+ tokens.splice(starTokenIdx, 1, ...prefixReplacement)
637
+ if (starTokenIdx < openIdx) {
638
+ openIdx += prefixDelta
639
+ closeIdx += prefixDelta
628
640
  }
629
641
 
630
642
  const openToken = tokens[openIdx]
@@ -634,19 +646,20 @@ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
634
646
  openToken.markup = '*'
635
647
  openToken.nesting = -1
636
648
 
637
- tokens.splice(closeIdx, 1)
649
+ const tailReplacement = []
638
650
  const tailIdx = closeIdx - 1
639
651
  if (tailIdx >= 0 && tokens[tailIdx] && tokens[tailIdx].type === 'text') {
640
652
  tokens[tailIdx].content += '*'
641
653
  } else {
642
654
  const tail = new Token('text', '', 0)
643
655
  tail.content = '*'
644
- tokens.splice(closeIdx, 0, tail)
656
+ tailReplacement.push(tail)
645
657
  }
658
+ tokens.splice(closeIdx, 1, ...tailReplacement)
646
659
  return true
647
660
  }
648
661
 
649
- const fixLeadingAsteriskEm = (tokens) => {
662
+ const fixLeadingAsteriskEm = (tokens, onChangeStart = null) => {
650
663
  let changed = false
651
664
  for (let i = 0; i < tokens.length; i++) {
652
665
  const t = tokens[i]
@@ -656,7 +669,7 @@ const fixLeadingAsteriskEm = (tokens) => {
656
669
  const linkCloseIdx = nextIdx
657
670
  const linkOpenIdx = findLinkOpen(tokens, linkCloseIdx)
658
671
  if (linkOpenIdx === -1) continue
659
- if (shiftEmWithLeadingStar(tokens, linkOpenIdx + 1, linkCloseIdx, i)) {
672
+ if (shiftEmWithLeadingStar(tokens, linkOpenIdx + 1, linkCloseIdx, i, onChangeStart)) {
660
673
  changed = true
661
674
  i = linkCloseIdx
662
675
  }
@@ -805,6 +818,7 @@ const patchScanDelims = (md) => {
805
818
 
806
819
  export {
807
820
  rebuildInlineLevels,
821
+ rebuildInlineLevelsFrom,
808
822
  findLinkOpen,
809
823
  nextNonEmptyIndex,
810
824
  fixTrailingStrong,