@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.
package/src/token-core.js CHANGED
@@ -4,13 +4,171 @@ import {
4
4
  CHAR_ASTERISK,
5
5
  CHAR_SPACE,
6
6
  CHAR_TAB,
7
- findPrevNonSpace,
8
- findNextNonSpace,
9
- resolveMode,
10
- shouldUseJapaneseRule,
7
+ CHAR_NEWLINE,
8
+ CHAR_IDEOGRAPHIC_SPACE,
9
+ isJapaneseChar,
10
+ MODE_FLAG_COMPATIBLE,
11
+ MODE_FLAG_AGGRESSIVE,
12
+ MODE_FLAG_JAPANESE_PLUS,
11
13
  getRuntimeOpt
12
14
  } from './token-utils.js'
13
15
 
16
+ const SCAN_DELIMS_PATCHED = Symbol.for('strongJaTokenScanDelimsPatched')
17
+ const SINGLE_STAR_LOOKAROUND_MAX = 16
18
+ const PREV_STAR_HAS_OPENER = 1
19
+ const PREV_STAR_HAS_JP_BETWEEN = 2
20
+
21
+ const isSoftSpaceCode = (code) => {
22
+ return code === CHAR_SPACE || code === CHAR_TAB || code === CHAR_IDEOGRAPHIC_SPACE
23
+ }
24
+
25
+ const isPlusQuoteWrapperOpen = (code) => {
26
+ return code === 0x2018 || // ‘
27
+ code === 0x201C || // “
28
+ code === 0x301D || // 〝
29
+ code === 0x00AB // «
30
+ }
31
+
32
+ const isPlusQuoteWrapperClose = (code) => {
33
+ return code === 0x2019 || // ’
34
+ code === 0x201D || // ”
35
+ code === 0x301E || // 〞
36
+ code === 0x301F || // 〟
37
+ code === 0x00BB // »
38
+ }
39
+
40
+ const isBacktick = (code) => code === 0x60 // `
41
+
42
+ const isOpeningBracketLike = (code) => {
43
+ switch (code) {
44
+ // ASCII
45
+ case 0x28: // (
46
+ case 0x5B: // [
47
+ case 0x7B: // {
48
+ // Fullwidth/halfwidth commonly used in JP text
49
+ case 0xFF08: // (
50
+ case 0xFF3B: // [
51
+ case 0xFF5B: // {
52
+ case 0xFF5F: // ⦅
53
+ case 0xFF62: // 「
54
+ case 0xFF1C: // <
55
+ // CJK punctuation brackets/quotes
56
+ case 0x3008: // 〈
57
+ case 0x300A: // 《
58
+ case 0x300C: // 「
59
+ case 0x300E: // 『
60
+ case 0x3010: // 【
61
+ case 0x3014: // 〔
62
+ case 0x3016: // 〖
63
+ case 0x3018: // 〘
64
+ case 0x301A: // 〚
65
+ // Mathematical/typographic angle brackets used in docs
66
+ case 0x27E6: // ⟦
67
+ case 0x27E8: // ⟨
68
+ case 0x27EA: // ⟪
69
+ case 0x27EC: // ⟬
70
+ case 0x27EE: // ⟮
71
+ case 0x2985: // ⦅
72
+ case 0x2987: // ⦇
73
+ case 0x2989: // ⦉
74
+ case 0x298B: // ⦋
75
+ case 0x298D: // ⦍
76
+ case 0x298F: // ⦏
77
+ case 0x2991: // ⦑
78
+ case 0x2993: // ⦓
79
+ case 0x2995: // ⦕
80
+ case 0x2997: // ⦗
81
+ case 0x29D8: // ⧘
82
+ case 0x29DA: // ⧚
83
+ case 0x29FC: // ⧼
84
+ // Vertical/small presentation forms found in JP publishing text
85
+ case 0xFE35: // ︵
86
+ case 0xFE37: // ︷
87
+ case 0xFE39: // ︹
88
+ case 0xFE3B: // ︻
89
+ case 0xFE3D: // ︽
90
+ case 0xFE3F: // ︿
91
+ case 0xFE41: // ﹁
92
+ case 0xFE43: // ﹃
93
+ case 0xFE47: // ﹇
94
+ case 0xFE59: // ﹙
95
+ case 0xFE5B: // ﹛
96
+ case 0xFE5D: // ﹝
97
+ return true
98
+ default:
99
+ return false
100
+ }
101
+ }
102
+
103
+ const isClosingBracketLike = (code) => {
104
+ switch (code) {
105
+ // ASCII
106
+ case 0x29: // )
107
+ case 0x5D: // ]
108
+ case 0x7D: // }
109
+ // Fullwidth/halfwidth commonly used in JP text
110
+ case 0xFF09: // )
111
+ case 0xFF3D: // ]
112
+ case 0xFF5D: // }
113
+ case 0xFF60: // ⦆
114
+ case 0xFF63: // 」
115
+ case 0xFF1E: // >
116
+ // CJK punctuation brackets/quotes
117
+ case 0x3009: // 〉
118
+ case 0x300B: // 》
119
+ case 0x300D: // 」
120
+ case 0x300F: // 』
121
+ case 0x3011: // 】
122
+ case 0x3015: // 〕
123
+ case 0x3017: // 〗
124
+ case 0x3019: // 〙
125
+ case 0x301B: // 〛
126
+ // Mathematical/typographic angle brackets used in docs
127
+ case 0x27E7: // ⟧
128
+ case 0x27E9: // ⟩
129
+ case 0x27EB: // ⟫
130
+ case 0x27ED: // ⟭
131
+ case 0x27EF: // ⟯
132
+ case 0x2986: // ⦆
133
+ case 0x2988: // ⦈
134
+ case 0x298A: // ⦊
135
+ case 0x298C: // ⦌
136
+ case 0x298E: // ⦎
137
+ case 0x2990: // ⦐
138
+ case 0x2992: // ⦒
139
+ case 0x2994: // ⦔
140
+ case 0x2996: // ⦖
141
+ case 0x2998: // ⦘
142
+ case 0x29D9: // ⧙
143
+ case 0x29DB: // ⧛
144
+ case 0x29FD: // ⧽
145
+ // Vertical/small presentation forms found in JP publishing text
146
+ case 0xFE36: // ︶
147
+ case 0xFE38: // ︸
148
+ case 0xFE3A: // ︺
149
+ case 0xFE3C: // ︼
150
+ case 0xFE3E: // ︾
151
+ case 0xFE40: // ﹀
152
+ case 0xFE42: // ﹂
153
+ case 0xFE44: // ﹄
154
+ case 0xFE48: // ﹈
155
+ case 0xFE5A: // ﹚
156
+ case 0xFE5C: // ﹜
157
+ case 0xFE5E: // ﹞
158
+ return true
159
+ default:
160
+ return false
161
+ }
162
+ }
163
+
164
+ const isWrapperOpenLike = (code) => {
165
+ return isOpeningBracketLike(code) || isPlusQuoteWrapperOpen(code) || isBacktick(code)
166
+ }
167
+
168
+ const isWrapperCloseLike = (code) => {
169
+ return isClosingBracketLike(code) || isPlusQuoteWrapperClose(code) || isBacktick(code)
170
+ }
171
+
14
172
  const findMatchingEmOpen = (tokens, closeIdx) => {
15
173
  let depth = 0
16
174
  for (let i = closeIdx; i >= 0; i--) {
@@ -36,18 +194,26 @@ const rebuildInlineLevels = (tokens) => {
36
194
  }
37
195
  }
38
196
 
39
- const findLinkClose = (tokens, startIdx) => {
40
- let depth = 0
41
- for (let i = startIdx; i < tokens.length; i++) {
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++) {
42
211
  const t = tokens[i]
43
212
  if (!t) continue
44
- if (t.type === 'link_open') depth++
45
- if (t.type === 'link_close') {
46
- depth--
47
- if (depth === 0) return i
48
- }
213
+ t.level = level
214
+ if (t.nesting === 1) level++
215
+ else if (t.nesting === -1) level--
49
216
  }
50
- return -1
51
217
  }
52
218
 
53
219
  const findLinkOpen = (tokens, closeIdx) => {
@@ -74,7 +240,185 @@ const nextNonEmptyIndex = (tokens, startIdx) => {
74
240
  return -1
75
241
  }
76
242
 
77
- const fixTrailingStrong = (tokens) => {
243
+ const isSingleStarBoundary = (code) => {
244
+ return code === 0 ||
245
+ isSoftSpaceCode(code) ||
246
+ code === CHAR_NEWLINE ||
247
+ isOpeningBracketLike(code)
248
+ }
249
+
250
+ const isSingleStarClosingBoundary = (code) => {
251
+ return code === 0 ||
252
+ isSoftSpaceCode(code) ||
253
+ code === CHAR_NEWLINE ||
254
+ isClosingBracketLike(code)
255
+ }
256
+
257
+ const isAsciiAlphaNum = (code) => {
258
+ return (code >= 0x30 && code <= 0x39) ||
259
+ (code >= 0x41 && code <= 0x5A) ||
260
+ (code >= 0x61 && code <= 0x7A)
261
+ }
262
+
263
+ const isAsciiGuardOpenWrapper = (code) => {
264
+ return code === 0x22 || // "
265
+ code === 0x27 || // '
266
+ code === 0x28 || // (
267
+ code === 0x5B || // [
268
+ code === 0x7B || // {
269
+ code === 0x3C || // <
270
+ code === 0x60 // `
271
+ }
272
+
273
+ const isAsciiGuardCloseWrapper = (code) => {
274
+ return code === 0x22 || // "
275
+ code === 0x27 || // '
276
+ code === 0x29 || // )
277
+ code === 0x5D || // ]
278
+ code === 0x7D || // }
279
+ code === 0x3E || // >
280
+ code === 0x60 // `
281
+ }
282
+
283
+ const findPrevNonSpaceIndex = (src, start) => {
284
+ for (let i = start; i >= 0; i--) {
285
+ const code = src.charCodeAt(i)
286
+ if (code === CHAR_NEWLINE) return -1
287
+ if (isSoftSpaceCode(code)) continue
288
+ return i
289
+ }
290
+ return -1
291
+ }
292
+
293
+ const findNextNonSpaceIndex = (src, start, max) => {
294
+ for (let i = start; i < max; i++) {
295
+ const code = src.charCodeAt(i)
296
+ if (code === CHAR_NEWLINE) return -1
297
+ if (isSoftSpaceCode(code)) continue
298
+ return i
299
+ }
300
+ return -1
301
+ }
302
+
303
+ const hasAsciiStartAfterOptionalOpenWrappers = (src, index, max) => {
304
+ let i = index
305
+ // Two wrappers are enough for common shapes: * [ "word" ]*
306
+ for (let wrappers = 0; wrappers < 2 && i >= 0 && i < max; wrappers++) {
307
+ const code = src.charCodeAt(i)
308
+ if (!isAsciiGuardOpenWrapper(code)) break
309
+ i = findNextNonSpaceIndex(src, i + 1, max)
310
+ if (i === -1) return false
311
+ }
312
+ if (i < 0 || i >= max) return false
313
+ return isAsciiAlphaNum(src.charCodeAt(i))
314
+ }
315
+
316
+ const hasAsciiEndBeforeOptionalCloseWrappers = (src, index) => {
317
+ let i = index
318
+ // Two wrappers are enough for common shapes: *["word"] *
319
+ for (let wrappers = 0; wrappers < 2 && i >= 0; wrappers++) {
320
+ const code = src.charCodeAt(i)
321
+ if (!isAsciiGuardCloseWrapper(code)) break
322
+ i = findPrevNonSpaceIndex(src, i - 1)
323
+ if (i === -1) return false
324
+ }
325
+ if (i < 0) return false
326
+ return isAsciiAlphaNum(src.charCodeAt(i))
327
+ }
328
+
329
+ const isMarkdownStructuralOpenWrapper = (code) => {
330
+ return code === 0x28 || // (
331
+ code === 0x5B || // [
332
+ code === 0x7B // {
333
+ }
334
+
335
+ const isExtraSingleStarClosePunct = (code) => {
336
+ return code === 0x3F || // ?
337
+ code === 0x203C || // ‼
338
+ code === 0x2047 || // ⁇
339
+ code === 0x2048 || // ⁈
340
+ code === 0x2049 // ⁉
341
+ }
342
+
343
+ const isSentenceBoundaryStop = (code) => {
344
+ return code === 0x3002 || // 。
345
+ code === 0xFF01 || // !
346
+ code === 0xFF1F || // ?
347
+ code === 0x2E || // .
348
+ code === 0x21 || // !
349
+ code === 0x3F || // ?
350
+ code === 0x203C || // ‼
351
+ code === 0x2047 || // ⁇
352
+ code === 0x2048 || // ⁈
353
+ code === 0x2049 // ⁉
354
+ }
355
+
356
+ const findPrevNonSpaceLimited = (src, start, maxLook) => {
357
+ let looked = 0
358
+ for (let i = start; i >= 0; i--) {
359
+ if (looked >= maxLook) break
360
+ const code = src.charCodeAt(i)
361
+ looked++
362
+ if (code === CHAR_NEWLINE) return 0
363
+ if (isSoftSpaceCode(code)) continue
364
+ return code
365
+ }
366
+ return 0
367
+ }
368
+
369
+ const findNextNonSpaceLimited = (src, start, max, maxLook) => {
370
+ let looked = 0
371
+ for (let i = start; i < max; i++) {
372
+ if (looked >= maxLook) break
373
+ const code = src.charCodeAt(i)
374
+ looked++
375
+ if (code === CHAR_NEWLINE) return 0
376
+ if (isSoftSpaceCode(code)) continue
377
+ return code
378
+ }
379
+ return 0
380
+ }
381
+
382
+ const hasJapaneseContextForBracketWrapper = (src, start, pos, max, lastChar, nextChar) => {
383
+ if (isWrapperOpenLike(nextChar)) {
384
+ const right = findNextNonSpaceLimited(src, pos, max, SINGLE_STAR_LOOKAROUND_MAX)
385
+ if (isJapaneseChar(right)) return true
386
+ }
387
+ if (isWrapperCloseLike(lastChar)) {
388
+ const left = findPrevNonSpaceLimited(src, start - 2, SINGLE_STAR_LOOKAROUND_MAX)
389
+ if (isJapaneseChar(left)) return true
390
+ }
391
+ return false
392
+ }
393
+
394
+ const scanPrevSingleStarContextFlags = (src, start) => {
395
+ let hasJapaneseBetween = false
396
+ for (let i = start - 1; i >= 0; i--) {
397
+ const code = src.charCodeAt(i)
398
+ if (code === CHAR_NEWLINE) break
399
+ if (isSentenceBoundaryStop(code) && i < start - 1) break
400
+ if (code !== CHAR_ASTERISK) {
401
+ if (!hasJapaneseBetween && isJapaneseChar(code)) hasJapaneseBetween = true
402
+ continue
403
+ }
404
+ let backslashCount = 0
405
+ for (let b = i - 1; b >= 0 && src.charCodeAt(b) === 0x5C; b--) {
406
+ backslashCount++
407
+ }
408
+ if ((backslashCount % 2) === 1) continue
409
+ const prevCode = i > 0 ? src.charCodeAt(i - 1) : 0
410
+ const nextCode = i + 1 < src.length ? src.charCodeAt(i + 1) : 0
411
+ if (prevCode === CHAR_ASTERISK || nextCode === CHAR_ASTERISK) continue
412
+ return hasJapaneseBetween ? PREV_STAR_HAS_OPENER | PREV_STAR_HAS_JP_BETWEEN : PREV_STAR_HAS_OPENER
413
+ }
414
+ return 0
415
+ }
416
+
417
+ const ensurePrevStarFlags = (src, start, prevStarFlags) => {
418
+ return prevStarFlags >= 0 ? prevStarFlags : scanPrevSingleStarContextFlags(src, start)
419
+ }
420
+
421
+ const fixTrailingStrong = (tokens, onChangeStart = null) => {
78
422
  let changed = false
79
423
  for (let i = 1; i < tokens.length; i++) {
80
424
  const token = tokens[i]
@@ -113,21 +457,23 @@ const fixTrailingStrong = (tokens) => {
113
457
  tokens[innerOpenIdx + 2] && tokens[innerOpenIdx + 2].type === 'em_close' &&
114
458
  tokens[innerOpenIdx + 3] && tokens[innerOpenIdx + 3].type === 'text' &&
115
459
  closeIdx === innerOpenIdx + 4) {
116
- tokens.splice(innerOpenIdx + 2, 1)
117
- tokens.splice(innerOpenIdx, 1)
118
460
  const movedOpen = new Token('em_open', 'em', 1)
119
461
  movedOpen.markup = '*'
120
462
  const movedClose = new Token('em_close', 'em', -1)
121
463
  movedClose.markup = '*'
122
- tokens.splice(innerOpenIdx + 1, 0, movedOpen)
123
- 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)
124
471
  }
125
472
 
126
473
  const before = token.content.slice(0, starIdx)
127
474
  const after = token.content.slice(starIdx + 2)
128
475
 
129
- tokens.splice(closeIdx, 1)
130
- if (closeIdx < i) i--
476
+ if (onChangeStart) onChangeStart(openIdx)
131
477
 
132
478
  const openToken = tokens[openIdx]
133
479
  if (!openToken) continue
@@ -136,28 +482,27 @@ const fixTrailingStrong = (tokens) => {
136
482
  openToken.markup = '**'
137
483
  openToken.nesting = 1
138
484
 
485
+ const strongClose = new Token('strong_close', 'strong', -1)
486
+ strongClose.markup = '**'
487
+ const trailingReplacement = []
139
488
  if (before) {
140
489
  token.content = before
141
- } else {
142
- tokens.splice(i, 1)
143
- i--
490
+ trailingReplacement.push(token)
144
491
  }
145
-
146
- const insertAt = i + 1
147
- const strongClose = new Token('strong_close', 'strong', -1)
148
- strongClose.markup = '**'
149
- tokens.splice(insertAt, 0, strongClose)
492
+ trailingReplacement.push(strongClose)
150
493
  if (after) {
151
494
  const tail = new Token('text', '', 0)
152
495
  tail.content = after
153
- tokens.splice(insertAt + 1, 0, tail)
496
+ trailingReplacement.push(tail)
154
497
  }
498
+ tokens.splice(closeIdx, 2, ...trailingReplacement)
499
+ i = closeIdx + trailingReplacement.length - 1
155
500
  changed = true
156
501
  }
157
502
  return changed
158
503
  }
159
504
 
160
- function fixEmOuterStrongSequence(tokens) {
505
+ function fixEmOuterStrongSequence(tokens, onChangeStart = null) {
161
506
  let changed = false
162
507
  let i = 0
163
508
  while (i < tokens.length) {
@@ -214,6 +559,7 @@ function fixEmOuterStrongSequence(tokens) {
214
559
  continue
215
560
  }
216
561
 
562
+ if (onChangeStart) onChangeStart(idx0)
217
563
  t0.type = 'strong_open'
218
564
  t0.tag = 'strong'
219
565
  t0.markup = '**'
@@ -226,44 +572,23 @@ function fixEmOuterStrongSequence(tokens) {
226
572
  const strongClose = new Token('strong_close', 'strong', -1)
227
573
  strongClose.markup = '**'
228
574
 
229
- const removeIndices = [idx7, idx5, idx3, idx1].sort((a, b) => b - a)
230
- for (const removeIdx of removeIndices) {
231
- if (removeIdx >= 0 && removeIdx < tokens.length) {
232
- tokens.splice(removeIdx, 1)
233
- }
234
- }
235
-
236
- const idxT4 = tokens.indexOf(t4)
237
- if (idxT4 === -1) {
238
- changed = true
239
- i = idx0 + 1
240
- continue
241
- }
242
- tokens.splice(idxT4, 0, emOpen)
243
-
244
- let idxT6 = tokens.indexOf(t6)
245
- if (idxT6 === -1) {
246
- changed = true
247
- i = idx0 + 1
248
- continue
249
- }
250
- tokens.splice(idxT6, 0, emClose)
251
-
252
- idxT6 = tokens.indexOf(t6)
253
- if (idxT6 === -1) {
254
- changed = true
255
- i = idx0 + 1
256
- continue
257
- }
258
- tokens.splice(idxT6 + 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)
259
584
 
260
585
  changed = true
261
- i = idxT6 + 2
586
+ i = idx1 + replacement.length
262
587
  }
263
588
  return changed
264
589
  }
265
590
 
266
- const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
591
+ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx, onChangeStart = null) => {
267
592
  let openIdx = findMatchingEmOpen(tokens, closeIdx)
268
593
  if (openIdx === -1 || openIdx < rangeStart || openIdx >= rangeEnd) return false
269
594
 
@@ -279,10 +604,10 @@ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
279
604
  if (!t || t.type !== 'text' || !t.content) continue
280
605
  const pos = t.content.lastIndexOf('*')
281
606
  if (pos <= 0) continue
282
- const prevChar = t.content.charAt(pos - 1)
283
- const nextChar = pos + 1 < t.content.length ? t.content.charAt(pos + 1) : ''
284
- if (!/\s/.test(prevChar)) continue
285
- if (!nextChar || /\s/.test(nextChar) || nextChar === '*') continue
607
+ const prevCode = t.content.charCodeAt(pos - 1)
608
+ const nextCode = pos + 1 < t.content.length ? t.content.charCodeAt(pos + 1) : 0
609
+ if (!isWhiteSpace(prevCode)) continue
610
+ if (!nextCode || isWhiteSpace(nextCode) || nextCode === CHAR_ASTERISK) continue
286
611
  starTokenIdx = i
287
612
  starPos = pos
288
613
  break
@@ -292,33 +617,26 @@ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
292
617
  const starToken = tokens[starTokenIdx]
293
618
  const before = starToken.content.slice(0, starPos)
294
619
  const after = starToken.content.slice(starPos + 1)
295
- let insertAt = starTokenIdx
296
- if (before) {
297
- starToken.content = before
298
- insertAt = starTokenIdx + 1
299
- } else {
300
- tokens.splice(starTokenIdx, 1)
301
- if (starTokenIdx < openIdx) {
302
- openIdx--
303
- closeIdx--
304
- }
305
- }
620
+ if (onChangeStart) onChangeStart(starTokenIdx)
306
621
 
307
622
  const emOpen = new Token('em_open', 'em', 1)
308
623
  emOpen.markup = '*'
309
- tokens.splice(insertAt, 0, emOpen)
310
- if (insertAt <= openIdx) {
311
- openIdx++
312
- closeIdx++
624
+ const prefixReplacement = []
625
+ if (before) {
626
+ starToken.content = before
627
+ prefixReplacement.push(starToken)
313
628
  }
629
+ prefixReplacement.push(emOpen)
314
630
  if (after) {
315
631
  const afterToken = new Token('text', '', 0)
316
632
  afterToken.content = after
317
- tokens.splice(insertAt + 1, 0, afterToken)
318
- if (insertAt + 1 <= openIdx) {
319
- openIdx++
320
- closeIdx++
321
- }
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
322
640
  }
323
641
 
324
642
  const openToken = tokens[openIdx]
@@ -328,19 +646,20 @@ const shiftEmWithLeadingStar = (tokens, rangeStart, rangeEnd, closeIdx) => {
328
646
  openToken.markup = '*'
329
647
  openToken.nesting = -1
330
648
 
331
- tokens.splice(closeIdx, 1)
649
+ const tailReplacement = []
332
650
  const tailIdx = closeIdx - 1
333
651
  if (tailIdx >= 0 && tokens[tailIdx] && tokens[tailIdx].type === 'text') {
334
652
  tokens[tailIdx].content += '*'
335
653
  } else {
336
654
  const tail = new Token('text', '', 0)
337
655
  tail.content = '*'
338
- tokens.splice(closeIdx, 0, tail)
656
+ tailReplacement.push(tail)
339
657
  }
658
+ tokens.splice(closeIdx, 1, ...tailReplacement)
340
659
  return true
341
660
  }
342
661
 
343
- const fixLeadingAsteriskEm = (tokens) => {
662
+ const fixLeadingAsteriskEm = (tokens, onChangeStart = null) => {
344
663
  let changed = false
345
664
  for (let i = 0; i < tokens.length; i++) {
346
665
  const t = tokens[i]
@@ -350,7 +669,7 @@ const fixLeadingAsteriskEm = (tokens) => {
350
669
  const linkCloseIdx = nextIdx
351
670
  const linkOpenIdx = findLinkOpen(tokens, linkCloseIdx)
352
671
  if (linkOpenIdx === -1) continue
353
- if (shiftEmWithLeadingStar(tokens, linkOpenIdx + 1, linkCloseIdx, i)) {
672
+ if (shiftEmWithLeadingStar(tokens, linkOpenIdx + 1, linkCloseIdx, i, onChangeStart)) {
354
673
  changed = true
355
674
  i = linkCloseIdx
356
675
  }
@@ -359,77 +678,147 @@ const fixLeadingAsteriskEm = (tokens) => {
359
678
  }
360
679
 
361
680
  const patchScanDelims = (md) => {
362
- if (md.__strongJaTokenScanDelimsPatched) return
363
- md.__strongJaTokenScanDelimsPatched = true
681
+ if (!md || !md.inline || !md.inline.State || !md.inline.State.prototype) return
682
+ const proto = md.inline.State.prototype
683
+ if (proto[SCAN_DELIMS_PATCHED] === true) {
684
+ return
685
+ }
686
+ const original = proto.scanDelims
687
+ if (typeof original !== 'function') return
364
688
 
365
- const original = md.inline.State.prototype.scanDelims
366
- md.inline.State.prototype.scanDelims = function strongJaTokenScanDelims(start, canSplitWord) {
367
- const marker = this.src.charCodeAt(start)
689
+ proto.scanDelims = function strongJaTokenScanDelims(start, canSplitWord) {
690
+ const src = this.src
691
+ const marker = src.charCodeAt(start)
368
692
  if (marker !== CHAR_ASTERISK) {
369
693
  return original.call(this, start, canSplitWord)
370
694
  }
695
+ const base = original.call(this, start, canSplitWord)
371
696
 
372
- const baseOpt = this.md && this.md.__strongJaTokenOpt ? this.md.__strongJaTokenOpt : null
373
- const opt = getRuntimeOpt(this, baseOpt)
697
+ const baseOpt = this.md ? this.md.__strongJaTokenOpt : null
698
+ const overrideOpt = this.env && this.env.__strongJaTokenOpt
699
+ const opt = overrideOpt ? getRuntimeOpt(this, baseOpt) : baseOpt
374
700
  if (!opt) {
375
- return original.call(this, start, canSplitWord)
701
+ return base
376
702
  }
377
- const mode = resolveMode(opt)
378
- const useJapaneseRule = shouldUseJapaneseRule(this, opt, mode)
379
- if (!useJapaneseRule) {
380
- return original.call(this, start, canSplitWord)
703
+ const modeFlags = opt.__strongJaModeFlags
704
+ if (modeFlags & MODE_FLAG_COMPATIBLE) {
705
+ return base
381
706
  }
382
-
707
+ const plusMode = (modeFlags & MODE_FLAG_JAPANESE_PLUS) !== 0
708
+ const aggressiveMode = (modeFlags & MODE_FLAG_AGGRESSIVE) !== 0
383
709
  const max = this.posMax
384
- const lastChar = start > 0 ? this.src.charCodeAt(start - 1) : 0x20
710
+ const lastChar = start > 0 ? src.charCodeAt(start - 1) : 0x20
385
711
 
386
- let pos = start
387
- while (pos < max && this.src.charCodeAt(pos) === marker) { pos++ }
388
- const count = pos - start
712
+ const count = base && base.length ? base.length : 1
713
+ const pos = start + count
389
714
 
390
- const nextChar = pos < max ? this.src.charCodeAt(pos) : 0x20
715
+ const nextChar = pos < max ? src.charCodeAt(pos) : 0x20
716
+ let prevStarFlags = -1
391
717
 
392
- const isLastPunctChar = false
393
- const isNextPunctChar = false
718
+ const leftJapanese = isJapaneseChar(lastChar)
719
+ const rightJapanese = isJapaneseChar(nextChar)
720
+ let hasJapaneseContext = leftJapanese || rightJapanese
721
+ if (!hasJapaneseContext && count === 1) {
722
+ hasJapaneseContext = hasJapaneseContextForBracketWrapper(src, start, pos, max, lastChar, nextChar)
723
+ }
724
+ if (!hasJapaneseContext && count === 1 && isExtraSingleStarClosePunct(lastChar)) {
725
+ prevStarFlags = ensurePrevStarFlags(src, start, prevStarFlags)
726
+ hasJapaneseContext = (prevStarFlags & PREV_STAR_HAS_JP_BETWEEN) !== 0
727
+ }
728
+ const useRelaxed = aggressiveMode || hasJapaneseContext
729
+ if (!useRelaxed) {
730
+ return base
731
+ }
394
732
 
395
- let isLastWhiteSpace = isWhiteSpace(lastChar)
396
- let isNextWhiteSpace = isWhiteSpace(nextChar)
397
- if (useJapaneseRule) {
398
- if (isLastWhiteSpace && (lastChar === CHAR_SPACE || lastChar === CHAR_TAB)) {
399
- const prevNonSpace = findPrevNonSpace(this.src, start - 2)
400
- if (prevNonSpace && prevNonSpace !== CHAR_ASTERISK) {
733
+ // 1) Normalize soft-space neighborhood around the current delimiter run.
734
+ let isLastWhiteSpace = isWhiteSpace(lastChar) || isSoftSpaceCode(lastChar)
735
+ let isNextWhiteSpace = isWhiteSpace(nextChar) || isSoftSpaceCode(nextChar)
736
+ if (isLastWhiteSpace && isSoftSpaceCode(lastChar)) {
737
+ const prevNonSpaceIdx = findPrevNonSpaceIndex(src, start - 2)
738
+ if (prevNonSpaceIdx !== -1) {
739
+ const prevNonSpaceLocal = src.charCodeAt(prevNonSpaceIdx)
740
+ const plusStrictAsciiBoundary = plusMode &&
741
+ hasAsciiEndBeforeOptionalCloseWrappers(src, prevNonSpaceIdx)
742
+ if (prevNonSpaceLocal !== CHAR_ASTERISK && !plusStrictAsciiBoundary) {
401
743
  isLastWhiteSpace = false
402
744
  }
403
745
  }
404
- if (isNextWhiteSpace && (nextChar === CHAR_SPACE || nextChar === CHAR_TAB)) {
405
- const nextNonSpace = findNextNonSpace(this.src, pos, max)
406
- if (nextNonSpace && nextNonSpace !== CHAR_ASTERISK) {
746
+ }
747
+ if (isNextWhiteSpace && isSoftSpaceCode(nextChar)) {
748
+ const nextNonSpaceIdx = findNextNonSpaceIndex(src, pos, max)
749
+ if (nextNonSpaceIdx !== -1) {
750
+ const nextNonSpace = src.charCodeAt(nextNonSpaceIdx)
751
+ const plusStrictAsciiBoundary = plusMode &&
752
+ hasAsciiStartAfterOptionalOpenWrappers(src, nextNonSpaceIdx, max)
753
+ if (nextNonSpace !== CHAR_ASTERISK && !plusStrictAsciiBoundary) {
407
754
  isNextWhiteSpace = false
408
755
  }
409
756
  }
410
757
  }
411
758
 
412
- const left_flanking =
413
- !isNextWhiteSpace && (!isNextPunctChar || isLastWhiteSpace || isLastPunctChar)
414
- const right_flanking =
415
- !isLastWhiteSpace && (!isLastPunctChar || isNextWhiteSpace || isNextPunctChar)
416
-
417
- const can_open = left_flanking && (canSplitWord || !right_flanking || isLastPunctChar)
418
- const can_close = right_flanking && (canSplitWord || !left_flanking || isNextPunctChar)
419
-
420
- const forbidClose = lastChar === 0x5B || lastChar === 0x28
421
- const forbidOpen = nextChar === 0x5D || nextChar === 0x29
759
+ // 2) Compute markdown-it compatible flanking sides from normalized whitespace.
760
+ const left_flanking = !isNextWhiteSpace
761
+ const right_flanking = !isLastWhiteSpace
762
+ const can_open = left_flanking && (canSplitWord || !right_flanking)
763
+ const can_close = right_flanking && (canSplitWord || !left_flanking)
764
+
765
+ const forbidClose = isOpeningBracketLike(lastChar)
766
+ const forbidOpen = isClosingBracketLike(nextChar)
767
+ let relaxedOpen = forbidOpen ? false : can_open
768
+ let relaxedClose = forbidClose ? false : can_close
769
+ let forceOpen = null
770
+ let forceClose = null
771
+ if (!aggressiveMode && count === 1) {
772
+ // Keep local directionality to avoid degrading markdown-it-valid runs,
773
+ // e.g. `[。*a**](u)` where the first `*` should remain opener-only.
774
+ const rightIsBoundary = isSingleStarClosingBoundary(nextChar) || isWrapperOpenLike(nextChar)
775
+ const leftIsBoundary = isSingleStarBoundary(lastChar) || isWrapperCloseLike(lastChar)
776
+ if (leftJapanese && !rightJapanese && !rightIsBoundary) {
777
+ prevStarFlags = ensurePrevStarFlags(src, start, prevStarFlags)
778
+ if ((prevStarFlags & PREV_STAR_HAS_OPENER) === 0) {
779
+ relaxedClose = false
780
+ }
781
+ } else if (!leftJapanese && rightJapanese && !leftIsBoundary) {
782
+ relaxedOpen = false
783
+ }
784
+ const rightIsOpenWrapper = isWrapperOpenLike(nextChar)
785
+ const leftIsCloseWrapper = isWrapperCloseLike(lastChar)
786
+ prevStarFlags = ensurePrevStarFlags(src, start, prevStarFlags)
787
+ const hasPrevJapaneseOpener = (prevStarFlags & PREV_STAR_HAS_OPENER) !== 0
788
+ const hasJapaneseSincePrevStar = (prevStarFlags & PREV_STAR_HAS_JP_BETWEEN) !== 0
789
+ const leftIsExtraClosePunct = isExtraSingleStarClosePunct(lastChar)
790
+ const canForceCloseByPunct = leftIsExtraClosePunct && hasJapaneseSincePrevStar
791
+ if (leftJapanese &&
792
+ rightIsOpenWrapper &&
793
+ !hasPrevJapaneseOpener &&
794
+ !isMarkdownStructuralOpenWrapper(nextChar)) {
795
+ forceOpen = true
796
+ forceClose = false
797
+ } else if (leftIsCloseWrapper && rightJapanese && hasPrevJapaneseOpener) {
798
+ forceOpen = false
799
+ forceClose = true
800
+ } else if ((leftIsCloseWrapper || canForceCloseByPunct) &&
801
+ !rightJapanese &&
802
+ !rightIsBoundary &&
803
+ hasPrevJapaneseOpener) {
804
+ forceOpen = false
805
+ forceClose = true
806
+ }
807
+ }
808
+ const finalOpen = forceOpen === null ? ((base && base.can_open) || relaxedOpen) : forceOpen
809
+ const finalClose = forceClose === null ? ((base && base.can_close) || relaxedClose) : forceClose
422
810
  return {
423
- can_open: forbidOpen ? false : can_open,
424
- can_close: forbidClose ? false : can_close,
811
+ can_open: finalOpen,
812
+ can_close: finalClose,
425
813
  length: count
426
814
  }
427
815
  }
816
+ proto[SCAN_DELIMS_PATCHED] = true
428
817
  }
429
818
 
430
819
  export {
431
820
  rebuildInlineLevels,
432
- findLinkClose,
821
+ rebuildInlineLevelsFrom,
433
822
  findLinkOpen,
434
823
  nextNonEmptyIndex,
435
824
  fixTrailingStrong,