@peaceroad/markdown-it-strong-ja 0.5.2 → 0.5.4

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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/index.js +461 -207
  3. package/package.json +6 -4
package/README.md CHANGED
@@ -172,3 +172,18 @@ md.render('string**[text](url)**')
172
172
  // <p>string**<a href="url">text</a>**</p>
173
173
  ```
174
174
 
175
+ ### coreRulesBeforePostprocess
176
+
177
+ `strong_ja_postprocess` runs inside the markdown-it core pipeline. When other plugins register core rules, you can keep their rules ahead of `strong_ja_postprocess` by listing them in `coreRulesBeforePostprocess`. Each name is normalized, deduplicated, and re-ordered once during plugin setup.
178
+
179
+ ```js
180
+ const md = mdit()
181
+ .use(cjkBreaks)
182
+ .use(mditStrongJa, {
183
+ coreRulesBeforePostprocess: ['cjk_breaks', 'my_custom_rule']
184
+ })
185
+ ```
186
+
187
+ - Default: `[]`
188
+ - Specify `['cjk_breaks']` (or other rule names) when you rely on plugins such as `@peaceroad/markdown-it-cjk-breaks-mod` and need them to run first.
189
+ - Pass an empty array if you do not want `mditStrongJa` to reorder any core rules.
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Token from 'markdown-it/lib/token.mjs'
2
2
  import { parseLinkDestination, parseLinkTitle } from 'markdown-it/lib/helpers/index.mjs'
3
- import { isSpace } from 'markdown-it/lib/common/utils.mjs'
3
+ import { isSpace, isWhiteSpace } from 'markdown-it/lib/common/utils.mjs'
4
4
 
5
5
  const CHAR_ASTERISK = 0x2A // *
6
6
  //const CHAR_UNDERSCORE = 0x5F // _
@@ -15,16 +15,24 @@ const CHAR_OPEN_BRACKET = 0x5B // [
15
15
  const CHAR_CLOSE_BRACKET = 0x5D // ]
16
16
  const CHAR_OPEN_PAREN = 0x28 // (
17
17
  const CHAR_CLOSE_PAREN = 0x29 // )
18
+ const CHAR_NEWLINE = 0x0A // \n
19
+ const CHAR_TAB = 0x09 // tab
20
+ //const CHAR_OPEN_CURLY = 0x7B // {
21
+ const CHAR_CLOSE_CURLY = 0x7D // }
18
22
 
19
- const REG_ASTERISKS = /^\*+$/
20
23
  const REG_ATTRS = /{[^{}\n!@#%^&*()]+?}$/
21
- const REG_PUNCTUATION = /[!-/:-@[-`{-~ ]/
24
+ const REG_ASCII_PUNCT = /[!-/:-@[-`{-~]/g
22
25
  const REG_JAPANESE = /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}|\p{General_Category=Punctuation}|\p{General_Category=Symbol}|\p{General_Category=Format}|\p{Emoji}/u // ひらがな|カタカナ|漢字|句読点|記号|フォーマット文字|絵文字
23
26
 
24
27
  const REG_MARKDOWN_HTML = /^\[[^\[\]]+\]\([^)]+\)$|^<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([^<]+<\/\1>)$|^`[^`]+`$|^\$[^$]+\$$/ // for mixed-language context detection
25
28
 
26
29
  const hasBackslash = (state, start) => {
27
30
  if (start <= 0) return false
31
+ if (state.__strongJaHasBackslash === false) return false
32
+ if (state.__strongJaHasBackslash === undefined) {
33
+ state.__strongJaHasBackslash = state.src.indexOf('\\') !== -1
34
+ if (!state.__strongJaHasBackslash) return false
35
+ }
28
36
  const cache = state.__strongJaBackslashCache
29
37
  if (cache && cache.has(start)) {
30
38
  return cache.get(start)
@@ -169,12 +177,11 @@ const computeReferenceRanges = (state, start, max) => {
169
177
  ? referenceCount > 0
170
178
  : Object.keys(references).length > 0)
171
179
  if (!hasReferences) return []
172
- const firstBracket = src.indexOf('[', start)
173
- if (firstBracket === -1 || firstBracket >= max) return []
180
+ let pos = src.indexOf('[', start)
181
+ if (pos === -1 || pos >= max) return []
174
182
  const ranges = []
175
- let pos = start
176
- while (pos < max) {
177
- if (src.charCodeAt(pos) === CHAR_OPEN_BRACKET && !hasBackslash(state, pos)) {
183
+ while (pos !== -1 && pos < max) {
184
+ if (!hasBackslash(state, pos)) {
178
185
  const labelClose = findMatchingBracket(state, pos, max, CHAR_OPEN_BRACKET, CHAR_CLOSE_BRACKET)
179
186
  if (labelClose !== -1) {
180
187
  const nextPos = labelClose + 1
@@ -195,16 +202,13 @@ const computeReferenceRanges = (state, start, max) => {
195
202
  ranges.push({ start: pos, end: labelClose, hasReference: true })
196
203
  ranges.push({ start: nextPos, end: refClose, hasReference: true })
197
204
  }
198
- pos = refClose
205
+ pos = src.indexOf('[', refClose + 1)
199
206
  continue
200
207
  }
201
208
  }
202
209
  }
203
210
  }
204
- pos++
205
- }
206
- if (ranges.length) {
207
- ranges.__cache = new Map()
211
+ pos = src.indexOf('[', pos + 1)
208
212
  }
209
213
  return ranges
210
214
  }
@@ -213,10 +217,11 @@ const computeInlineLinkRanges = (state, start, max) => {
213
217
  const src = state.src
214
218
  const ranges = []
215
219
  const labelRanges = []
216
- let pos = start
220
+ let pos = src.indexOf('[', start)
221
+ if (pos === -1 || pos >= max) return []
217
222
  let rangeId = 0
218
- while (pos < max) {
219
- if (src.charCodeAt(pos) === CHAR_OPEN_BRACKET && !hasBackslash(state, pos)) {
223
+ while (pos !== -1 && pos < max) {
224
+ if (!hasBackslash(state, pos)) {
220
225
  const labelClose = findMatchingBracket(state, pos, max, CHAR_OPEN_BRACKET, CHAR_CLOSE_BRACKET)
221
226
  if (labelClose === -1) break
222
227
  let destStart = labelClose + 1
@@ -233,14 +238,14 @@ const computeInlineLinkRanges = (state, start, max) => {
233
238
  labelRanges.push(labelRange)
234
239
  ranges.push({ start: destStart, end: destClose, kind: 'dest', id: rangeId })
235
240
  rangeId++
236
- pos = destClose + 1
241
+ pos = src.indexOf('[', destClose + 1)
237
242
  continue
238
243
  }
239
244
  }
240
- pos = labelClose + 1
245
+ pos = src.indexOf('[', labelClose + 1)
241
246
  continue
242
247
  }
243
- pos++
248
+ pos = src.indexOf('[', pos + 1)
244
249
  }
245
250
  if (ranges.length && labelRanges.length) {
246
251
  ranges.__labelRanges = labelRanges
@@ -260,7 +265,8 @@ const getInlineRangeCacheMap = (ranges, kind, create) => {
260
265
 
261
266
  const findInlineLinkRange = (pos, ranges, kind) => {
262
267
  if (!ranges || ranges.length === 0) return null
263
- const cache = getInlineRangeCacheMap(ranges, kind, false)
268
+ const useCache = ranges.length > 32
269
+ const cache = useCache ? getInlineRangeCacheMap(ranges, kind, false) : null
264
270
  if (cache && cache.has(pos)) return cache.get(pos)
265
271
  let left = 0
266
272
  let right = ranges.length - 1
@@ -279,8 +285,10 @@ const findInlineLinkRange = (pos, ranges, kind) => {
279
285
  break
280
286
  }
281
287
  }
282
- const storeCache = getInlineRangeCacheMap(ranges, kind, true)
283
- storeCache.set(pos, found)
288
+ if (useCache) {
289
+ const storeCache = getInlineRangeCacheMap(ranges, kind, true)
290
+ storeCache.set(pos, found)
291
+ }
284
292
  return found
285
293
  }
286
294
 
@@ -314,24 +322,64 @@ const registerPostProcessTarget = (state) => {
314
322
  targets.push(state.tokens)
315
323
  }
316
324
 
317
- const setToken = (state, inlines, opt) => {
325
+ const hasMditAttrs = (state) => {
326
+ if (state.__strongJaHasAttrs !== undefined) return state.__strongJaHasAttrs
327
+ const rules = state.md && state.md.core && state.md.core.ruler && state.md.core.ruler.__rules__
328
+ if (!rules || !Array.isArray(rules)) {
329
+ state.__strongJaHasAttrs = false
330
+ return false
331
+ }
332
+ for (let i = 0; i < rules.length; i++) {
333
+ if (rules[i].name === 'curly_attributes') {
334
+ state.__strongJaHasAttrs = true
335
+ return true
336
+ }
337
+ }
338
+ state.__strongJaHasAttrs = false
339
+ return false
340
+ }
341
+
342
+ const isAllAsterisks = (content) => {
343
+ for (let i = 0; i < content.length; i++) {
344
+ if (content.charCodeAt(i) !== CHAR_ASTERISK) return false
345
+ }
346
+ return true
347
+ }
348
+
349
+ function isPlainTextContent(content) {
350
+ for (let idx = 0; idx < content.length; idx++) {
351
+ const code = content.charCodeAt(idx)
352
+ if (code === CHAR_BACKSLASH || code === CHAR_NEWLINE || code === CHAR_TAB || code === 0x0D) {
353
+ return false
354
+ }
355
+ if (code < 128 && isAsciiPunctuationCode(code)) return false
356
+ }
357
+ return true
358
+ }
359
+
360
+ const setToken = (state, inlines, opt, attrsEnabled) => {
318
361
  const src = state.src
319
362
  let i = 0
320
- let attrsIsText = {
321
- val: false,
322
- tag: '',
323
- }
363
+ let attrsIsText = false
364
+ let attrsIsTextTag = ''
324
365
  while (i < inlines.length) {
325
366
  let type = inlines[i].type
326
- const tag = type.replace(/(?:_open|_close)$/, '')
327
-
328
- if (/_open$/.test(type)) {
367
+ let tag = ''
368
+ let isOpen = false
369
+ let isClose = false
370
+ if (type.length > 5 && type.endsWith('_open')) {
371
+ isOpen = true
372
+ tag = type.slice(0, -5)
373
+ } else if (type.length > 6 && type.endsWith('_close')) {
374
+ isClose = true
375
+ tag = type.slice(0, -6)
376
+ }
377
+
378
+ if (isOpen) {
329
379
  const startToken = state.push(type, tag, 1)
330
380
  startToken.markup = tag === 'strong' ? '**' : '*'
331
- attrsIsText = {
332
- val: true,
333
- tag: tag,
334
- }
381
+ attrsIsText = true
382
+ attrsIsTextTag = tag
335
383
  }
336
384
 
337
385
  if (type === 'html_inline') {
@@ -339,18 +387,23 @@ const setToken = (state, inlines, opt) => {
339
387
  }
340
388
  if (type === 'text') {
341
389
  let content = src.slice(inlines[i].s, inlines[i].e + 1)
342
- if (REG_ASTERISKS.test(content)) {
343
- const asteriskToken = state.push(type, '', 0)
344
- asteriskToken.content = content
345
- i++
346
- continue
390
+ if (content.length > 0 && content.charCodeAt(0) === CHAR_ASTERISK) {
391
+ if (isAllAsterisks(content)) {
392
+ const asteriskToken = state.push(type, '', 0)
393
+ asteriskToken.content = content
394
+ i++
395
+ continue
396
+ }
347
397
  }
348
- if (opt.mditAttrs && attrsIsText.val && i + 1 < inlines.length) {
349
- const hasImmediatelyAfterAsteriskClose = inlines[i+1].type === attrsIsText.tag + '_close'
350
- if (hasImmediatelyAfterAsteriskClose && REG_ATTRS.test(content)) {
398
+ if (attrsEnabled && attrsIsText && i + 1 < inlines.length) {
399
+ const hasImmediatelyAfterAsteriskClose = inlines[i+1].type === attrsIsTextTag + '_close'
400
+ const maybeAttrs = content.length > 0 && content.charCodeAt(content.length - 1) === CHAR_CLOSE_CURLY
401
+ if (hasImmediatelyAfterAsteriskClose && maybeAttrs && REG_ATTRS.test(content)) {
351
402
  const attrsToken = state.push(type, '', 0)
352
-
353
- const hasBackslashBeforeCurlyAttribute = content.match(/(\\+){/)
403
+ let hasBackslashBeforeCurlyAttribute = null
404
+ if (content.indexOf('\\') !== -1) {
405
+ hasBackslashBeforeCurlyAttribute = content.match(/(\\+){/)
406
+ }
354
407
  if (hasBackslashBeforeCurlyAttribute) {
355
408
  if (hasBackslashBeforeCurlyAttribute[1].length === 1) {
356
409
  attrsToken.content = content.replace(/\\{/, '{')
@@ -367,11 +420,18 @@ const setToken = (state, inlines, opt) => {
367
420
  } else {
368
421
  attrsToken.content = content
369
422
  }
370
- attrsIsText.val = false
423
+ attrsIsText = false
424
+ attrsIsTextTag = ''
371
425
  i++
372
426
  continue
373
427
  }
374
428
  }
429
+ if (isPlainTextContent(content)) {
430
+ const textToken = state.push(type, '', 0)
431
+ textToken.content = content
432
+ i++
433
+ continue
434
+ }
375
435
 
376
436
  const childTokens = state.md.parseInline(content, state.env)
377
437
  if (childTokens[0] && childTokens[0].children) {
@@ -383,7 +443,7 @@ const setToken = (state, inlines, opt) => {
383
443
  t.tag = ''
384
444
  t.content = '\n'
385
445
  }
386
- if (!opt.mditAttrs && t.tag === 'br') {
446
+ if (!attrsEnabled && t.tag === 'br') {
387
447
  t.tag = ''
388
448
  t.content = '\n'
389
449
  }
@@ -394,13 +454,11 @@ const setToken = (state, inlines, opt) => {
394
454
  }
395
455
  }
396
456
 
397
- if (/_close$/.test(type)) {
457
+ if (isClose) {
398
458
  const closeToken = state.push(type, tag, -1)
399
459
  closeToken.markup = tag === 'strong' ? '**' : '*'
400
- attrsIsText = {
401
- val: false,
402
- tag: '',
403
- }
460
+ attrsIsText = false
461
+ attrsIsTextTag = ''
404
462
  }
405
463
 
406
464
  i++
@@ -421,71 +479,81 @@ const pushInlines = (inlines, s, e, len, type, tag, tagType) => {
421
479
  inlines.push(inline)
422
480
  }
423
481
 
424
- const findNextSymbolPos = (state, n, max, symbol) => {
482
+ const isAsciiPunctuationCode = (code) => {
483
+ return (code >= 33 && code <= 47) || (code >= 58 && code <= 64) ||
484
+ (code >= 91 && code <= 96) || (code >= 123 && code <= 126)
485
+ }
486
+
487
+ const findNextAsciiPunctuation = (src, start, max) => {
488
+ REG_ASCII_PUNCT.lastIndex = start
489
+ const match = REG_ASCII_PUNCT.exec(src)
490
+ if (!match || match.index >= max) return -1
491
+ return match.index
492
+ }
493
+
494
+ const findNextSymbolPos = (state, n, max, symbol, symbolChar) => {
425
495
  const src = state.src
426
496
  if (src.charCodeAt(n) !== symbol || hasBackslash(state, n)) return -1
427
- for (let i = n + 1; i < max; i++) {
428
- if (src.charCodeAt(i) === symbol && !hasBackslash(state, i)) {
429
- return i
430
- }
497
+ let i = src.indexOf(symbolChar, n + 1)
498
+ while (i !== -1 && i < max) {
499
+ if (!hasBackslash(state, i)) return i
500
+ i = src.indexOf(symbolChar, i + 1)
431
501
  }
432
502
  return -1
433
503
  }
434
504
 
435
- const processSymbolPair = (state, n, srcLen, symbol, noMark, textStart, pushInlines) => {
436
- const nextSymbolPos = findNextSymbolPos(state, n, srcLen, symbol)
505
+ const processSymbolPair = (state, n, srcLen, symbol, symbolChar, hasText, textStart, pushInlines) => {
506
+ const nextSymbolPos = findNextSymbolPos(state, n, srcLen, symbol, symbolChar)
437
507
  if (nextSymbolPos === -1) {
438
- return { shouldBreak: false, shouldContinue: false, newN: n, newNoMark: noMark }
508
+ return { shouldBreak: false, shouldContinue: false, newN: n, hasText: hasText }
439
509
  }
440
- const src = state.src
441
- const innerText = src.slice(n + 1, nextSymbolPos)
442
- const markup = src.slice(n, nextSymbolPos + 1)
443
- const newNoMark = noMark + innerText + markup
444
510
  if (nextSymbolPos === srcLen - 1) {
445
511
  pushInlines(textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
446
- return { shouldBreak: true, newN: nextSymbolPos + 1, newNoMark }
512
+ return { shouldBreak: true, newN: nextSymbolPos + 1, hasText: true }
447
513
  }
448
- return { shouldBreak: false, shouldContinue: true, newN: nextSymbolPos + 1, newNoMark }
514
+ return { shouldBreak: false, shouldContinue: true, newN: nextSymbolPos + 1, hasText: true }
449
515
  }
450
516
 
451
- const processTextSegment = (inlines, textStart, n, noMark) => {
452
- if (n !== 0 && noMark.length !== 0) {
517
+ const processTextSegment = (inlines, textStart, n, hasText) => {
518
+ if (n !== 0 && hasText) {
453
519
  pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
454
- return ''
520
+ return false
455
521
  }
456
- return noMark
522
+ return hasText
457
523
  }
458
524
 
459
525
  const createInlines = (state, start, max, opt) => {
460
526
  const src = state.src
461
527
  const srcLen = max
462
528
  const htmlEnabled = state.md.options.html
529
+ const dollarMath = opt.dollarMath
463
530
  let n = start
464
531
  let inlines = []
465
- let noMark = ''
532
+ let hasText = false
466
533
  let textStart = n
467
534
 
468
- // Infinite loop prevention
469
- const maxIterations = srcLen * 2 // Safe upper bound
470
- let iterations = 0
471
-
472
535
  while (n < srcLen) {
473
- // Prevent infinite loops
474
- iterations++
475
- if (iterations > maxIterations) {
476
- // Add remaining text as-is and exit safely
477
- if (textStart < srcLen) {
478
- pushInlines(inlines, textStart, srcLen - 1, srcLen - textStart, 'text')
536
+ let currentChar = src.charCodeAt(n)
537
+
538
+ if (!isAsciiPunctuationCode(currentChar)) {
539
+ const nextPunc = findNextAsciiPunctuation(src, n, srcLen)
540
+ if (nextPunc === -1) {
541
+ if (textStart < srcLen) {
542
+ pushInlines(inlines, textStart, srcLen - 1, srcLen - textStart, 'text')
543
+ }
544
+ break
545
+ }
546
+ if (nextPunc > n) {
547
+ hasText = true
548
+ n = nextPunc
549
+ currentChar = src.charCodeAt(n)
479
550
  }
480
- break
481
551
  }
482
552
 
483
- const currentChar = src.charCodeAt(n)
484
-
485
553
  // Unified escape check
486
554
  let isEscaped = false
487
555
  if (currentChar === CHAR_ASTERISK || currentChar === CHAR_BACKTICK ||
488
- (opt.dollarMath && currentChar === CHAR_DOLLAR) ||
556
+ (dollarMath && currentChar === CHAR_DOLLAR) ||
489
557
  (htmlEnabled && currentChar === CHAR_LT)) {
490
558
  isEscaped = hasBackslash(state, n)
491
559
  }
@@ -493,7 +561,7 @@ const createInlines = (state, start, max, opt) => {
493
561
  // Asterisk handling
494
562
  if (currentChar === CHAR_ASTERISK) {
495
563
  if (!isEscaped) {
496
- noMark = processTextSegment(inlines, textStart, n, noMark)
564
+ hasText = processTextSegment(inlines, textStart, n, hasText)
497
565
  if (n === srcLen - 1) {
498
566
  pushInlines(inlines, n, n, 1, '')
499
567
  break
@@ -507,6 +575,7 @@ const createInlines = (state, start, max, opt) => {
507
575
  } else {
508
576
  pushInlines(inlines, n, i - 1, i - n, '')
509
577
  textStart = i
578
+ hasText = false
510
579
  }
511
580
  n = i
512
581
  continue
@@ -516,30 +585,30 @@ const createInlines = (state, start, max, opt) => {
516
585
  // Inline code (backticks)
517
586
  if (currentChar === CHAR_BACKTICK) {
518
587
  if (!isEscaped) {
519
- const result = processSymbolPair(state, n, srcLen, CHAR_BACKTICK, noMark, textStart,
588
+ const result = processSymbolPair(state, n, srcLen, CHAR_BACKTICK, '`', hasText, textStart,
520
589
  (start, end, len, type) => pushInlines(inlines, start, end, len, type))
521
590
  if (result.shouldBreak) break
522
591
  if (result.shouldContinue) {
523
592
  n = result.newN
524
- noMark = result.newNoMark
593
+ hasText = result.hasText
525
594
  continue
526
595
  }
527
- noMark = result.newNoMark
596
+ hasText = result.hasText
528
597
  }
529
598
  }
530
599
 
531
600
  // Inline math ($...$)
532
- if (opt.dollarMath && currentChar === CHAR_DOLLAR) {
601
+ if (dollarMath && currentChar === CHAR_DOLLAR) {
533
602
  if (!isEscaped) {
534
- const result = processSymbolPair(state, n, srcLen, CHAR_DOLLAR, noMark, textStart,
603
+ const result = processSymbolPair(state, n, srcLen, CHAR_DOLLAR, '$', hasText, textStart,
535
604
  (start, end, len, type) => pushInlines(inlines, start, end, len, type))
536
605
  if (result.shouldBreak) break
537
606
  if (result.shouldContinue) {
538
607
  n = result.newN
539
- noMark = result.newNoMark
608
+ hasText = result.hasText
540
609
  continue
541
610
  }
542
- noMark = result.newNoMark
611
+ hasText = result.hasText
543
612
  }
544
613
  }
545
614
 
@@ -547,9 +616,12 @@ const createInlines = (state, start, max, opt) => {
547
616
  if (htmlEnabled && currentChar === CHAR_LT) {
548
617
  if (!isEscaped) {
549
618
  let foundClosingTag = false
550
- for (let i = n + 1; i < srcLen; i++) {
551
- if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
552
- noMark = processTextSegment(inlines, textStart, n, noMark)
619
+ let i = n + 1
620
+ while (i < srcLen) {
621
+ i = src.indexOf('>', i)
622
+ if (i === -1 || i >= srcLen) break
623
+ if (!hasBackslash(state, i)) {
624
+ hasText = processTextSegment(inlines, textStart, n, hasText)
553
625
  let tag = src.slice(n + 1, i)
554
626
  let tagType
555
627
  if (tag.charCodeAt(0) === CHAR_SLASH) {
@@ -560,10 +632,12 @@ const createInlines = (state, start, max, opt) => {
560
632
  }
561
633
  pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
562
634
  textStart = i + 1
635
+ hasText = false
563
636
  n = i + 1
564
637
  foundClosingTag = true
565
638
  break
566
639
  }
640
+ i += 1
567
641
  }
568
642
  if (foundClosingTag) {
569
643
  continue
@@ -573,7 +647,7 @@ const createInlines = (state, start, max, opt) => {
573
647
  }
574
648
 
575
649
  // Regular character
576
- noMark += src[n]
650
+ hasText = true
577
651
  if (n === srcLen - 1) {
578
652
  pushInlines(inlines, textStart, n, n - textStart + 1, 'text')
579
653
  break
@@ -593,6 +667,10 @@ const pushMark = (marks, opts) => {
593
667
  oLen: opts.oLen,
594
668
  type: opts.type
595
669
  }
670
+ if (marks.length === 0 || marks[marks.length - 1].s <= newMark.s) {
671
+ marks.push(newMark)
672
+ return
673
+ }
596
674
  // Binary search for insertion point to maintain sorted order
597
675
  let left = 0
598
676
  let right = marks.length
@@ -609,16 +687,17 @@ const pushMark = (marks, opts) => {
609
687
  }
610
688
 
611
689
  const setStrong = (state, inlines, marks, n, memo, opt, nestTracker, refRanges, inlineLinkRanges) => {
690
+ const hasInlineLinkRanges = inlineLinkRanges && inlineLinkRanges.length > 0
691
+ const hasRefRanges = refRanges && refRanges.length > 0
692
+ const inlinesLength = inlines.length
612
693
  if (opt.disallowMixed === true) {
613
694
  let i = n + 1
614
- const inlinesLength = inlines.length
615
695
  while (i < inlinesLength) {
616
696
  if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
617
697
  if (inlines[i].type !== '') { i++; continue }
618
698
 
619
699
  if (inlines[i].len > 1) {
620
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
621
- if (mixedCheck.shouldBlock) {
700
+ if (shouldBlockMixedLanguage(state, inlines, n, i)) {
622
701
  return [n, 0]
623
702
  }
624
703
  break
@@ -627,33 +706,25 @@ const setStrong = (state, inlines, marks, n, memo, opt, nestTracker, refRanges,
627
706
  }
628
707
  }
629
708
 
630
- const strongOpenRange = findRefRangeIndex(inlines[n].s, refRanges)
631
- const openLinkRange = findInlineLinkRange(inlines[n].s, inlineLinkRanges)
709
+ const strongOpenRange = hasRefRanges ? findRefRangeIndex(inlines[n].s, refRanges) : -1
710
+ const openLinkRange = hasInlineLinkRanges ? findInlineLinkRange(inlines[n].s, inlineLinkRanges) : null
632
711
  let i = n + 1
633
712
  let j = 0
634
713
  let nest = 0
635
- let insideTagsIsClose = 1
636
- const inlinesLength = inlines.length
637
714
  while (i < inlinesLength) {
638
715
  if (inlines[i].type !== '') { i++; continue }
639
716
  if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
640
- if (inlines[i].type === 'html_inline') {
641
- inlines[i].check = true
642
- insideTagsIsClose = checkInsideTags(inlines, i, memo)
643
- if (insideTagsIsClose === -1) return [n, nest]
644
- if (insideTagsIsClose === 0) { i++; continue }
645
- }
646
717
 
647
- if (inlineLinkRanges && inlineLinkRanges.length > 0 &&
718
+ if (hasInlineLinkRanges &&
648
719
  hasInlineLinkLabelCrossing(inlineLinkRanges, inlines[n].ep + 1, inlines[i].sp)) {
649
720
  i++
650
721
  continue
651
722
  }
652
723
 
653
- const closeRange = findRefRangeIndex(inlines[i].s, refRanges)
724
+ const closeRange = hasRefRanges ? findRefRangeIndex(inlines[i].s, refRanges) : -1
654
725
  if (strongOpenRange !== closeRange) { i++; continue }
655
726
 
656
- const closeLinkRange = findInlineLinkRange(inlines[i].s, inlineLinkRanges)
727
+ const closeLinkRange = hasInlineLinkRanges ? findInlineLinkRange(inlines[i].s, inlineLinkRanges) : null
657
728
  if (openLinkRange || closeLinkRange) {
658
729
  if (!openLinkRange || !closeLinkRange || openLinkRange.id !== closeLinkRange.id || openLinkRange.kind !== closeLinkRange.kind) {
659
730
  i++
@@ -685,16 +756,14 @@ const setStrong = (state, inlines, marks, n, memo, opt, nestTracker, refRanges,
685
756
  inlines[n].ep -= 1
686
757
  inlines[i].len -= 1
687
758
  if (inlines[i].len > 0) inlines[i].sp += 1
688
- if (insideTagsIsClose === 1) {
689
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker, refRanges, inlineLinkRanges)
690
- n = newN
691
- nest = newNest
692
- }
759
+ const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker, refRanges, inlineLinkRanges)
760
+ n = newN
761
+ nest = newNest
693
762
  }
694
763
  let strongNum = Math.trunc(Math.min(inlines[n].len, inlines[i].len) / 2)
695
764
 
696
765
  if (inlines[i].len > 1) {
697
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges)) {
766
+ if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges, hasRefRanges)) {
698
767
  if (memo.inlineMarkEnd) {
699
768
  marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt, refRanges, inlineLinkRanges))
700
769
  if (inlines[i].len === 0) { i++; continue }
@@ -752,21 +821,19 @@ const checkInsideTags = (inlines, i, memo) => {
752
821
  if (memo.htmlTags[tagName] === undefined) {
753
822
  memo.htmlTags[tagName] = 0
754
823
  }
755
- if (inlines[i].tag[1] === 'open') {
824
+ const tagType = inlines[i].tag[1]
825
+ if (tagType === 'open') {
756
826
  memo.htmlTags[tagName] += 1
827
+ memo.htmlTagDepth += 1
757
828
  }
758
- if (inlines[i].tag[1] === 'close') {
829
+ if (tagType === 'close') {
759
830
  memo.htmlTags[tagName] -= 1
831
+ memo.htmlTagDepth -= 1
760
832
  }
761
- if (memo.htmlTags[tagName] < 0) {
833
+ if (memo.htmlTags[tagName] < 0 || memo.htmlTagDepth < 0) {
762
834
  return -1
763
835
  }
764
-
765
- // Direct check instead of Object.values().every()
766
- for (const count of Object.values(memo.htmlTags)) {
767
- if (count !== 0) return 0
768
- }
769
- return 1
836
+ return memo.htmlTagDepth === 0 ? 1 : 0
770
837
  }
771
838
 
772
839
  // Check if character is ASCII punctuation or space
@@ -795,7 +862,7 @@ const isJapanese = (ch) => {
795
862
  }
796
863
 
797
864
  // Check if character is English (letters, numbers) or other non-Japanese characters
798
- // Uses REG_JAPANESE and REG_PUNCTUATION to exclude Japanese and punctuation characters
865
+ // Uses REG_JAPANESE to exclude Japanese characters
799
866
  const isEnglish = (ch) => {
800
867
  if (!ch) return false
801
868
  const code = ch.charCodeAt(0)
@@ -805,10 +872,10 @@ const isEnglish = (ch) => {
805
872
  if (code < 128) {
806
873
  return code === CHAR_SPACE || (code > 126)
807
874
  }
808
- return !REG_JAPANESE.test(ch) && !REG_PUNCTUATION.test(ch)
875
+ return !REG_JAPANESE.test(ch)
809
876
  }
810
877
 
811
- const checkMixedLanguagePattern = (state, inlines, n, i, opt) => {
878
+ const shouldBlockMixedLanguage = (state, inlines, n, i) => {
812
879
  const src = state.src
813
880
  const openPrevChar = src[inlines[n].s - 1] || ''
814
881
  const closeNextChar = src[inlines[i].e + 1] || ''
@@ -816,25 +883,17 @@ const checkMixedLanguagePattern = (state, inlines, n, i, opt) => {
816
883
  const isEnglishPrefix = isEnglish(openPrevChar)
817
884
  const isEnglishSuffix = isEnglish(closeNextChar)
818
885
  if (!isEnglishPrefix && !isEnglishSuffix) {
819
- return { hasEnglishContext: false, hasMarkdownOrHtml: false, shouldBlock: false }
820
- }
821
-
822
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
823
- const hasMarkdownOrHtml = REG_MARKDOWN_HTML.test(contentBetween)
824
-
825
- return {
826
- hasEnglishContext: true,
827
- hasMarkdownOrHtml,
828
- shouldBlock: hasMarkdownOrHtml
886
+ return false
829
887
  }
888
+ return hasMarkdownHtmlPattern(src, inlines[n].e + 1, inlines[i].s)
830
889
  }
831
890
 
832
- const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt, refRanges) => {
891
+ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt, refRanges, hasRefRanges) => {
833
892
  const src = state.src
834
893
  const openPrevChar = src[inlines[n].s - 1] || ''
835
894
  const openNextChar = src[inlines[n].e + 1] || ''
836
895
  let checkOpenNextChar = isPunctuation(openNextChar)
837
- if (checkOpenNextChar && (openNextChar === '[' || openNextChar === ']')) {
896
+ if (hasRefRanges && checkOpenNextChar && (openNextChar === '[' || openNextChar === ']')) {
838
897
  const openNextRange = findRefRangeIndex(inlines[n].e + 1, refRanges)
839
898
  if (openNextRange !== -1) {
840
899
  checkOpenNextChar = false
@@ -842,19 +901,19 @@ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt, refRanges) => {
842
901
  }
843
902
  const closePrevChar = src[inlines[i].s - 1] || ''
844
903
  let checkClosePrevChar = isPunctuation(closePrevChar)
845
- if (checkClosePrevChar && (closePrevChar === '[' || closePrevChar === ']')) {
904
+ if (hasRefRanges && checkClosePrevChar && (closePrevChar === '[' || closePrevChar === ']')) {
846
905
  const closePrevRange = findRefRangeIndex(inlines[i].s - 1, refRanges)
847
906
  if (closePrevRange !== -1) {
848
907
  checkClosePrevChar = false
849
908
  }
850
909
  }
851
910
  const closeNextChar = src[inlines[i].e + 1] || ''
852
- const checkCloseNextChar = (isPunctuation(closeNextChar) || i === inlines.length - 1)
911
+ const isLastInline = i === inlines.length - 1
912
+ const checkCloseNextChar = isLastInline || isPunctuation(closeNextChar)
853
913
 
854
914
  if (opt.disallowMixed === false) {
855
915
  if (isEnglish(openPrevChar) || isEnglish(closeNextChar)) {
856
- const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
857
- if (REG_MARKDOWN_HTML.test(contentBetween)) {
916
+ if (hasMarkdownHtmlPattern(src, inlines[n].e + 1, inlines[i].s)) {
858
917
  return false
859
918
  }
860
919
  }
@@ -865,18 +924,19 @@ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt, refRanges) => {
865
924
  }
866
925
 
867
926
  const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRanges, inlineLinkRanges) => {
868
- const emOpenRange = findRefRangeIndex(inlines[n].s, refRanges)
869
- const openLinkRange = findInlineLinkRange(inlines[n].s, inlineLinkRanges)
927
+ const hasInlineLinkRanges = inlineLinkRanges && inlineLinkRanges.length > 0
928
+ const hasRefRanges = refRanges && refRanges.length > 0
929
+ const inlinesLength = inlines.length
930
+ const emOpenRange = hasRefRanges ? findRefRangeIndex(inlines[n].s, refRanges) : -1
931
+ const openLinkRange = hasInlineLinkRanges ? findInlineLinkRange(inlines[n].s, inlineLinkRanges) : null
870
932
  if (opt.disallowMixed === true && !sNest) {
871
933
  let i = n + 1
872
- const inlinesLength = inlines.length
873
934
  while (i < inlinesLength) {
874
935
  if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
875
936
  if (inlines[i].type !== '') { i++; continue }
876
937
 
877
938
  if (inlines[i].len > 0) {
878
- const mixedCheck = checkMixedLanguagePattern(state, inlines, n, i, opt)
879
- if (mixedCheck.shouldBlock) {
939
+ if (shouldBlockMixedLanguage(state, inlines, n, i)) {
880
940
  return [n, 0]
881
941
  }
882
942
  break
@@ -889,7 +949,6 @@ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRange
889
949
  let nest = 0
890
950
  let strongPNum = 0
891
951
  let insideTagsIsClose = 1
892
- const inlinesLength = inlines.length
893
952
  while (i < inlinesLength) {
894
953
  if (inlines[i].len === 0 || inlines[i].check) { i++; continue }
895
954
  if (!sNest && inlines[i].type === 'html_inline') {
@@ -900,19 +959,19 @@ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRange
900
959
  }
901
960
  if (inlines[i].type !== '') { i++; continue }
902
961
 
903
- if (inlineLinkRanges && inlineLinkRanges.length > 0 &&
962
+ if (hasInlineLinkRanges &&
904
963
  hasInlineLinkLabelCrossing(inlineLinkRanges, inlines[n].ep + 1, inlines[i].sp)) {
905
964
  i++
906
965
  continue
907
966
  }
908
967
 
909
- const closeRange = findRefRangeIndex(inlines[i].s, refRanges)
968
+ const closeRange = hasRefRanges ? findRefRangeIndex(inlines[i].s, refRanges) : -1
910
969
  if (emOpenRange !== closeRange) {
911
970
  i++
912
971
  continue
913
972
  }
914
973
 
915
- const closeLinkRange = findInlineLinkRange(inlines[i].s, inlineLinkRanges)
974
+ const closeLinkRange = hasInlineLinkRanges ? findInlineLinkRange(inlines[i].s, inlineLinkRanges) : null
916
975
  if (openLinkRange || closeLinkRange) {
917
976
  if (!openLinkRange || !closeLinkRange || openLinkRange.id !== closeLinkRange.id || openLinkRange.kind !== closeLinkRange.kind) {
918
977
  i++
@@ -924,15 +983,10 @@ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRange
924
983
 
925
984
  if (!sNest && emNum !== 1) return [n, sNest, memo]
926
985
 
927
- const hasMarkersAtStartAndEnd = (i) => {
928
- let flag = memo.inlineMarkStart
929
- if (!flag) return false
930
- inlinesLength - 1 === i ? flag = true : flag = false
931
- if (!flag) return false
932
- inlines[i].len > 1 ? flag = true : flag = false
933
- return flag
934
- }
935
- if (!sNest && inlines[i].len === 2 && !hasMarkersAtStartAndEnd(i)) {
986
+ const isMarkerAtStartAndEnd = memo.inlineMarkStart &&
987
+ i === inlinesLength - 1 &&
988
+ inlines[i].len > 1
989
+ if (!sNest && inlines[i].len === 2 && !isMarkerAtStartAndEnd) {
936
990
  strongPNum++
937
991
  i++
938
992
  continue
@@ -946,7 +1000,7 @@ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker, refRange
946
1000
  if (nest === -1) return [n, nest]
947
1001
 
948
1002
  if (emNum === 1) {
949
- if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges)) {
1003
+ if (hasPunctuationOrNonJapanese(state, inlines, n, i, opt, refRanges, hasRefRanges)) {
950
1004
  if (memo.inlineMarkEnd) {
951
1005
  marks.push(...createMarks(state, inlines, i, inlinesLength - 1, memo, opt, refRanges, inlineLinkRanges))
952
1006
 
@@ -1098,44 +1152,93 @@ const mergeInlinesAndMarks = (inlines, marks) => {
1098
1152
  return merged
1099
1153
  }
1100
1154
 
1101
- const isWhitespaceToken = (token) => token && token.type === 'text' && token.content.trim() === ''
1155
+ const isWhitespaceToken = (token) => {
1156
+ if (!token || token.type !== 'text') return false
1157
+ const content = token.content
1158
+ if (!content) return true
1159
+ for (let i = 0; i < content.length; i++) {
1160
+ if (!isWhiteSpace(content.charCodeAt(i))) return false
1161
+ }
1162
+ return true
1163
+ }
1164
+
1165
+ const hasMarkdownHtmlPattern = (src, start, end) => {
1166
+ if (start >= end) return false
1167
+ const first = src.charCodeAt(start)
1168
+ const last = src.charCodeAt(end - 1)
1169
+ if (first === CHAR_OPEN_BRACKET) {
1170
+ if (last !== CHAR_CLOSE_PAREN) return false
1171
+ } else if (first === CHAR_LT) {
1172
+ if (last !== CHAR_GT) return false
1173
+ } else if (first === CHAR_BACKTICK) {
1174
+ if (last !== CHAR_BACKTICK) return false
1175
+ } else if (first === CHAR_DOLLAR) {
1176
+ if (last !== CHAR_DOLLAR) return false
1177
+ } else {
1178
+ return false
1179
+ }
1180
+ return REG_MARKDOWN_HTML.test(src.slice(start, end))
1181
+ }
1102
1182
 
1103
1183
  const strongJa = (state, silent, opt) => {
1104
1184
  if (silent) return false
1105
1185
  const start = state.pos
1106
1186
  let max = state.posMax
1187
+ const originalMax = max
1107
1188
  const src = state.src
1108
1189
  let attributesSrc
1109
1190
  if (start > max) return false
1110
1191
  if (src.charCodeAt(start) !== CHAR_ASTERISK) return false
1111
1192
  if (hasBackslash(state, start)) return false
1112
1193
 
1194
+ const attrsEnabled = opt.mditAttrs && hasMditAttrs(state)
1195
+
1113
1196
  if (start === 0) {
1114
1197
  state.__strongJaRefRangeCache = null
1115
1198
  state.__strongJaInlineLinkRangeCache = null
1116
1199
  state.__strongJaBackslashCache = undefined
1200
+ state.__strongJaHasBackslash = undefined
1117
1201
  }
1118
1202
 
1119
- if (opt.mditAttrs) {
1120
- attributesSrc = src.match(/((\n)? *){([^{}\n!@#%^&*()]+?)} *$/)
1121
- if (attributesSrc && attributesSrc[3] !== '.') {
1122
- max = src.slice(0, attributesSrc.index).length
1123
- if (attributesSrc[2] === '\n') {
1124
- max = src.slice(0, attributesSrc.index - 1).length
1203
+ if (attrsEnabled) {
1204
+ let attrCandidate = false
1205
+ let probe = originalMax - 1
1206
+ while (probe >= start) {
1207
+ const code = src.charCodeAt(probe)
1208
+ if (code === CHAR_CLOSE_CURLY) {
1209
+ attrCandidate = true
1210
+ break
1125
1211
  }
1126
- if(hasBackslash(state, attributesSrc.index) && attributesSrc[2] === '' && attributesSrc[1].length === 0) {
1127
- max = state.posMax
1212
+ if (code === CHAR_SPACE || code === CHAR_TAB || code === CHAR_NEWLINE) {
1213
+ probe--
1214
+ continue
1128
1215
  }
1129
- } else {
1130
- let endCurlyKet = src.match(/(\n *){([^{}\n!@#%^&*()]*?)}.*(} *?)$/)
1131
- if (endCurlyKet) {
1132
- max -= endCurlyKet[3].length
1216
+ break
1217
+ }
1218
+
1219
+ if (attrCandidate) {
1220
+ const attrScanTarget = originalMax === src.length ? src : src.slice(0, originalMax)
1221
+ attributesSrc = attrScanTarget.match(/((\n)? *){([^{}\n!@#%^&*()]+?)} *$/)
1222
+ if (attributesSrc && attributesSrc[3] !== '.') {
1223
+ max = attrScanTarget.slice(0, attributesSrc.index).length
1224
+ if (attributesSrc[2] === '\n') {
1225
+ max = attrScanTarget.slice(0, attributesSrc.index - 1).length
1226
+ }
1227
+ if (hasBackslash(state, attributesSrc.index) && attributesSrc[2] === '' && attributesSrc[1].length === 0) {
1228
+ max = state.posMax
1229
+ }
1230
+ } else {
1231
+ const endCurlyKet = attrScanTarget.match(/(\n *){([^{}\n!@#%^&*()]*?)}.*(} *?)$/)
1232
+ if (endCurlyKet) {
1233
+ max -= endCurlyKet[3].length
1234
+ }
1133
1235
  }
1134
1236
  }
1135
1237
  }
1136
1238
 
1137
1239
  if (state.__strongJaHasCollapsedRefs === undefined) {
1138
- state.__strongJaHasCollapsedRefs = /\[[^\]]*\]\s*\[[^\]]*\]/.test(state.src)
1240
+ state.__strongJaHasCollapsedRefs = src.indexOf('[') !== -1 &&
1241
+ /\[[^\]]*\]\s*\[[^\]]*\]/.test(src)
1139
1242
  }
1140
1243
 
1141
1244
  if (state.__strongJaReferenceCount === undefined) {
@@ -1143,13 +1246,16 @@ const strongJa = (state, silent, opt) => {
1143
1246
  state.__strongJaReferenceCount = references ? Object.keys(references).length : 0
1144
1247
  }
1145
1248
 
1146
- let refRanges
1147
- const refCache = state.__strongJaRefRangeCache
1148
- if (refCache && refCache.max === max && refCache.start <= start) {
1149
- refRanges = refCache.ranges
1150
- } else {
1151
- refRanges = computeReferenceRanges(state, start, max)
1152
- state.__strongJaRefRangeCache = { start, max, ranges: refRanges }
1249
+ let refRanges = []
1250
+ const hasReferenceDefinitions = state.__strongJaReferenceCount > 0
1251
+ if (hasReferenceDefinitions) {
1252
+ const refCache = state.__strongJaRefRangeCache
1253
+ if (refCache && refCache.max === max && refCache.start <= start) {
1254
+ refRanges = refCache.ranges
1255
+ } else {
1256
+ refRanges = computeReferenceRanges(state, start, max)
1257
+ state.__strongJaRefRangeCache = { start, max, ranges: refRanges }
1258
+ }
1153
1259
  }
1154
1260
  if (refRanges.length > 0) {
1155
1261
  state.__strongJaHasCollapsedRefs = true
@@ -1175,6 +1281,7 @@ const strongJa = (state, silent, opt) => {
1175
1281
  const memo = {
1176
1282
  html: state.md.options.html,
1177
1283
  htmlTags: {},
1284
+ htmlTagDepth: 0,
1178
1285
  inlineMarkStart: src.charCodeAt(0) === CHAR_ASTERISK,
1179
1286
  inlineMarkEnd: src.charCodeAt(max - 1) === CHAR_ASTERISK,
1180
1287
  }
@@ -1183,7 +1290,7 @@ const strongJa = (state, silent, opt) => {
1183
1290
 
1184
1291
  inlines = mergeInlinesAndMarks(inlines, marks)
1185
1292
 
1186
- setToken(state, inlines, opt)
1293
+ setToken(state, inlines, opt, attrsEnabled)
1187
1294
 
1188
1295
  if (inlineLinkRanges && inlineLinkRanges.length > 0) {
1189
1296
  const labelSources = []
@@ -1205,7 +1312,7 @@ const strongJa = (state, silent, opt) => {
1205
1312
  state.__strongJaPostProcessRegistered = true
1206
1313
  }
1207
1314
 
1208
- if (opt.mditAttrs && max !== state.posMax) {
1315
+ if (attrsEnabled && max !== state.posMax) {
1209
1316
  if (!attributesSrc) {
1210
1317
  state.pos = max
1211
1318
  return true
@@ -1218,9 +1325,12 @@ const strongJa = (state, silent, opt) => {
1218
1325
  }
1219
1326
 
1220
1327
  // Collapsed reference helpers
1221
- const buildReferenceLabel = (tokens) => {
1328
+ const buildReferenceLabelRange = (tokens, startIdx, endIdx) => {
1329
+ if (startIdx > endIdx) return ''
1222
1330
  let label = ''
1223
- for (const token of tokens) {
1331
+ for (let idx = startIdx; idx <= endIdx; idx++) {
1332
+ const token = tokens[idx]
1333
+ if (!token) continue
1224
1334
  if (token.type === 'text' || token.type === 'code_inline') {
1225
1335
  label += token.content
1226
1336
  } else if (token.type === 'softbreak' || token.type === 'hardbreak') {
@@ -1235,19 +1345,26 @@ const buildReferenceLabel = (tokens) => {
1235
1345
  }
1236
1346
 
1237
1347
  const cleanLabelText = (label) => {
1348
+ if (label.indexOf('*') === -1 && label.indexOf('_') === -1) return label
1238
1349
  return label.replace(/^[*_]+/, '').replace(/[*_]+$/, '')
1239
1350
  }
1240
1351
 
1241
1352
  const normalizeReferenceCandidate = (state, text, { useClean = false } = {}) => {
1242
- const source = useClean ? cleanLabelText(text) : text.replace(/\s+/g, ' ').trim()
1353
+ const source = useClean ? cleanLabelText(text) : text
1243
1354
  return normalizeRefKey(state, source)
1244
1355
  }
1245
1356
 
1246
- const normalizeRefKey = (state, label) => {
1357
+ const getNormalizeRef = (state) => {
1358
+ if (state.__strongJaNormalizeRef) return state.__strongJaNormalizeRef
1247
1359
  const normalize = state.md && state.md.utils && state.md.utils.normalizeReference
1248
1360
  ? state.md.utils.normalizeReference
1249
1361
  : (str) => str.trim().replace(/\s+/g, ' ').toUpperCase()
1250
- return normalize(label)
1362
+ state.__strongJaNormalizeRef = normalize
1363
+ return normalize
1364
+ }
1365
+
1366
+ const normalizeRefKey = (state, label) => {
1367
+ return getNormalizeRef(state)(label)
1251
1368
  }
1252
1369
 
1253
1370
  const adjustTokenLevels = (tokens, startIdx, endIdx, delta) => {
@@ -1272,16 +1389,30 @@ const cloneTextToken = (source, content) => {
1272
1389
  const splitBracketToken = (tokens, index, options) => {
1273
1390
  const token = tokens[index]
1274
1391
  if (!token || token.type !== 'text') return false
1392
+ if (token.__strongJaBracketAtomic) return false
1393
+ if (token.__strongJaHasBracket === false) return false
1275
1394
  const content = token.content
1276
- if (!content || (content.indexOf('[') === -1 && content.indexOf(']') === -1)) {
1395
+ if (!content) {
1396
+ token.__strongJaHasBracket = false
1397
+ token.__strongJaBracketAtomic = false
1277
1398
  return false
1278
1399
  }
1400
+ if (token.__strongJaHasBracket !== true) {
1401
+ if (content.indexOf('[') === -1 && content.indexOf(']') === -1) {
1402
+ token.__strongJaHasBracket = false
1403
+ token.__strongJaBracketAtomic = false
1404
+ return false
1405
+ }
1406
+ token.__strongJaHasBracket = true
1407
+ }
1279
1408
  const splitEmptyPair = options && options.splitEmptyPair
1280
1409
  const segments = []
1281
1410
  let buffer = ''
1282
1411
  let pos = 0
1283
1412
  while (pos < content.length) {
1284
- if (!splitEmptyPair && content.startsWith('[]', pos)) {
1413
+ if (!splitEmptyPair &&
1414
+ content.charCodeAt(pos) === CHAR_OPEN_BRACKET &&
1415
+ content.charCodeAt(pos + 1) === CHAR_CLOSE_BRACKET) {
1285
1416
  if (buffer) {
1286
1417
  segments.push(buffer)
1287
1418
  buffer = ''
@@ -1304,11 +1435,49 @@ const splitBracketToken = (tokens, index, options) => {
1304
1435
  pos++
1305
1436
  }
1306
1437
  if (buffer) segments.push(buffer)
1307
- if (segments.length <= 1) return false
1438
+ if (segments.length <= 1) {
1439
+ if (segments.length === 0) {
1440
+ token.__strongJaHasBracket = false
1441
+ token.__strongJaBracketAtomic = false
1442
+ } else {
1443
+ const seg = segments[0]
1444
+ if (seg === '[' || seg === ']') {
1445
+ token.__strongJaHasBracket = true
1446
+ token.__strongJaBracketAtomic = true
1447
+ } else if (seg === '[]') {
1448
+ token.__strongJaHasBracket = true
1449
+ token.__strongJaBracketAtomic = false
1450
+ } else {
1451
+ token.__strongJaHasBracket = false
1452
+ token.__strongJaBracketAtomic = false
1453
+ }
1454
+ }
1455
+ return false
1456
+ }
1308
1457
  token.content = segments[0]
1458
+ if (token.content === '[' || token.content === ']') {
1459
+ token.__strongJaHasBracket = true
1460
+ token.__strongJaBracketAtomic = true
1461
+ } else if (token.content === '[]') {
1462
+ token.__strongJaHasBracket = true
1463
+ token.__strongJaBracketAtomic = false
1464
+ } else {
1465
+ token.__strongJaHasBracket = false
1466
+ token.__strongJaBracketAtomic = false
1467
+ }
1309
1468
  let insertIdx = index + 1
1310
1469
  for (let s = 1; s < segments.length; s++) {
1311
1470
  const newToken = cloneTextToken(token, segments[s])
1471
+ if (segments[s] === '[' || segments[s] === ']') {
1472
+ newToken.__strongJaHasBracket = true
1473
+ newToken.__strongJaBracketAtomic = true
1474
+ } else if (segments[s] === '[]') {
1475
+ newToken.__strongJaHasBracket = true
1476
+ newToken.__strongJaBracketAtomic = false
1477
+ } else {
1478
+ newToken.__strongJaHasBracket = false
1479
+ newToken.__strongJaBracketAtomic = false
1480
+ }
1312
1481
  tokens.splice(insertIdx, 0, newToken)
1313
1482
  insertIdx++
1314
1483
  }
@@ -1474,11 +1643,13 @@ const parseInlineLinkTail = (content, md) => {
1474
1643
 
1475
1644
  const INLINE_LINK_BRACKET_SPLIT_OPTIONS = { splitEmptyPair: true }
1476
1645
 
1477
- const removeGhostLabelText = (tokens, linkCloseToken, labelText) => {
1646
+ const removeGhostLabelText = (tokens, linkCloseIndex, labelText) => {
1478
1647
  if (!labelText) return
1479
- const closeIdx = tokens.indexOf(linkCloseToken)
1480
- if (closeIdx === -1) return
1481
- let idx = closeIdx + 1
1648
+ if (linkCloseIndex === null || linkCloseIndex === undefined) return
1649
+ if (linkCloseIndex < 0 || linkCloseIndex >= tokens.length) return
1650
+ const closeToken = tokens[linkCloseIndex]
1651
+ if (!closeToken || closeToken.type !== 'link_close') return
1652
+ let idx = linkCloseIndex + 1
1482
1653
  while (idx < tokens.length) {
1483
1654
  const token = tokens[idx]
1484
1655
  if (!token) {
@@ -1545,6 +1716,7 @@ const convertInlineLinks = (tokens, state) => {
1545
1716
  let tailIdx = closeIdx + 1
1546
1717
  let tailContent = ''
1547
1718
  let parsedTail = null
1719
+ let tailHasCloseParen = false
1548
1720
  while (tailIdx < tokens.length) {
1549
1721
  if (splitBracketToken(tokens, tailIdx, INLINE_LINK_BRACKET_SPLIT_OPTIONS)) {
1550
1722
  continue
@@ -1554,6 +1726,13 @@ const convertInlineLinks = (tokens, state) => {
1554
1726
  break
1555
1727
  }
1556
1728
  tailContent += tailToken.content
1729
+ if (!tailHasCloseParen) {
1730
+ if (tailToken.content.indexOf(')') === -1) {
1731
+ tailIdx++
1732
+ continue
1733
+ }
1734
+ tailHasCloseParen = true
1735
+ }
1557
1736
  parsedTail = parseInlineLinkTail(tailContent, state.md)
1558
1737
  if (parsedTail) break
1559
1738
  tailIdx++
@@ -1587,7 +1766,7 @@ const convertInlineLinks = (tokens, state) => {
1587
1766
  continue
1588
1767
  }
1589
1768
  if (needsPlaceholder && currentLabelSource) {
1590
- removeGhostLabelText(tokens, linkCloseToken, currentLabelSource)
1769
+ removeGhostLabelText(tokens, nextIndex - 1, currentLabelSource)
1591
1770
  }
1592
1771
 
1593
1772
  if (labelSources && labelSources.length > 0) {
@@ -1642,8 +1821,10 @@ const convertCollapsedReferenceLinks = (tokens, state) => {
1642
1821
  continue
1643
1822
  }
1644
1823
 
1645
- const labelTokens = tokens.slice(i + 1, closeIdx)
1646
- const labelText = buildReferenceLabel(labelTokens)
1824
+ const labelStart = i + 1
1825
+ const labelEnd = closeIdx - 1
1826
+ const labelLength = closeIdx - i - 1
1827
+ const labelText = buildReferenceLabelRange(tokens, labelStart, labelEnd)
1647
1828
  const cleanedLabel = cleanLabelText(labelText)
1648
1829
  const whitespaceStart = closeIdx + 1
1649
1830
  let refRemoveStart = whitespaceStart
@@ -1671,11 +1852,12 @@ const convertCollapsedReferenceLinks = (tokens, state) => {
1671
1852
  i++
1672
1853
  continue
1673
1854
  }
1674
- const refTokens = tokens.slice(refRemoveStart + 1, refCloseIdx)
1675
- if (refTokens.length === 0) {
1855
+ const refStart = refRemoveStart + 1
1856
+ const refEnd = refCloseIdx - 1
1857
+ if (refStart > refEnd) {
1676
1858
  refKey = normalizeReferenceCandidate(state, cleanedLabel)
1677
1859
  } else {
1678
- const refLabelText = buildReferenceLabel(refTokens)
1860
+ const refLabelText = buildReferenceLabelRange(tokens, refStart, refEnd)
1679
1861
  refKey = normalizeReferenceCandidate(state, refLabelText)
1680
1862
  }
1681
1863
  refRemoveCount = refCloseIdx - refRemoveStart + 1
@@ -1733,7 +1915,7 @@ const convertCollapsedReferenceLinks = (tokens, state) => {
1733
1915
  tokens.splice(closeIdx, 1)
1734
1916
  tokens.splice(i, 1)
1735
1917
 
1736
- const nextIndex = wrapLabelTokensWithLink(tokens, i, i + labelTokens.length - 1, linkOpenToken, linkCloseToken)
1918
+ const nextIndex = wrapLabelTokensWithLink(tokens, i, i + labelLength - 1, linkOpenToken, linkCloseToken)
1737
1919
  i = nextIndex
1738
1920
  }
1739
1921
  }
@@ -1787,8 +1969,16 @@ const mditStrongJa = (md, option) => {
1787
1969
  mditAttrs: true, //markdown-it-attrs
1788
1970
  mdBreaks: md.options.breaks,
1789
1971
  disallowMixed: false, //Non-Japanese text handling
1972
+ coreRulesBeforePostprocess: [] // e.g. ['cjk_breaks'] when CJK line-break plugins are active
1790
1973
  }
1791
1974
  if (option) Object.assign(opt, option)
1975
+ const rawCoreRules = opt.coreRulesBeforePostprocess
1976
+ const hasCoreRuleConfig = Array.isArray(rawCoreRules)
1977
+ ? rawCoreRules.length > 0
1978
+ : !!rawCoreRules
1979
+ const coreRulesBeforePostprocess = hasCoreRuleConfig
1980
+ ? normalizeCoreRulesBeforePostprocess(rawCoreRules)
1981
+ : []
1792
1982
 
1793
1983
  md.inline.ruler.before('emphasis', 'strong_ja', (state, silent) => {
1794
1984
  return strongJa(state, silent, opt)
@@ -1799,6 +1989,22 @@ const mditStrongJa = (md, option) => {
1799
1989
  if (!targets || targets.length === 0) return
1800
1990
  for (const tokens of targets) {
1801
1991
  if (!tokens || !tokens.length) continue
1992
+ let hasBracketText = false
1993
+ for (let i = 0; i < tokens.length; i++) {
1994
+ const token = tokens[i]
1995
+ if (!token || token.type !== 'text') continue
1996
+ const content = token.content
1997
+ if (!content) continue
1998
+ if (content.indexOf('[') !== -1 || content.indexOf(']') !== -1) {
1999
+ hasBracketText = true
2000
+ break
2001
+ }
2002
+ }
2003
+ if (!hasBracketText) {
2004
+ delete tokens.__strongJaInlineLabelSources
2005
+ delete tokens.__strongJaInlineLabelIndex
2006
+ continue
2007
+ }
1802
2008
  convertInlineLinks(tokens, state)
1803
2009
  convertCollapsedReferenceLinks(tokens, state)
1804
2010
  mergeBrokenMarksAroundLinks(tokens)
@@ -1808,6 +2014,54 @@ const mditStrongJa = (md, option) => {
1808
2014
  delete state.env.__strongJaPostProcessTargets
1809
2015
  delete state.env.__strongJaPostProcessTargetSet
1810
2016
  })
2017
+
2018
+ if (coreRulesBeforePostprocess.length > 0) {
2019
+ ensureCoreRuleOrder(md, coreRulesBeforePostprocess)
2020
+ }
1811
2021
  }
1812
2022
 
1813
2023
  export default mditStrongJa
2024
+
2025
+
2026
+ function normalizeCoreRulesBeforePostprocess(value) {
2027
+ if (!value) return []
2028
+ const list = Array.isArray(value) ? value : [value]
2029
+ const normalized = []
2030
+ const seen = new Set()
2031
+ for (let idx = 0; idx < list.length; idx++) {
2032
+ const raw = list[idx]
2033
+ if (typeof raw !== 'string') continue
2034
+ const trimmed = raw.trim()
2035
+ if (!trimmed || seen.has(trimmed)) continue
2036
+ seen.add(trimmed)
2037
+ normalized.push(trimmed)
2038
+ }
2039
+ return normalized
2040
+ }
2041
+
2042
+
2043
+ function ensureCoreRuleOrder(md, ruleNames) {
2044
+ if (!md || !md.core || !md.core.ruler) return
2045
+ if (!ruleNames || ruleNames.length === 0) return
2046
+ for (let idx = 0; idx < ruleNames.length; idx++) {
2047
+ moveRuleBefore(md.core.ruler, ruleNames[idx], 'strong_ja_postprocess')
2048
+ }
2049
+ }
2050
+
2051
+
2052
+ function moveRuleBefore(ruler, ruleName, beforeName) {
2053
+ if (!ruler || !ruler.__rules__) return
2054
+ const rules = ruler.__rules__
2055
+ let fromIdx = -1
2056
+ let beforeIdx = -1
2057
+ for (let idx = 0; idx < rules.length; idx++) {
2058
+ if (rules[idx].name === ruleName) fromIdx = idx
2059
+ if (rules[idx].name === beforeName) beforeIdx = idx
2060
+ if (fromIdx !== -1 && beforeIdx !== -1) break
2061
+ }
2062
+ if (fromIdx === -1 || beforeIdx === -1 || fromIdx < beforeIdx) return
2063
+
2064
+ const rule = rules.splice(fromIdx, 1)[0]
2065
+ rules.splice(beforeIdx, 0, rule)
2066
+ ruler.__cache__ = null
2067
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-strong-ja",
3
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.5.2",
4
+ "version": "0.5.4",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "files": [
@@ -19,8 +19,10 @@
19
19
  "markdown-it": "^14.1.0"
20
20
  },
21
21
  "devDependencies": {
22
- "@peaceroad/markdown-it-hr-sandwiched-semantic-container": "^0.7.0",
23
- "@sup39/markdown-it-cjk-breaks": "^1.2.0",
24
- "markdown-it-attrs": "^4.2.0"
22
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.1",
23
+ "@peaceroad/markdown-it-hr-sandwiched-semantic-container": "^0.8.0",
24
+ "markdown-it-attrs": "^4.3.1",
25
+ "markdown-it-sub": "^2.0.0",
26
+ "markdown-it-sup": "^2.0.0"
25
27
  }
26
28
  }