@peaceroad/markdown-it-strong-ja 0.4.4 → 0.4.5

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 (2) hide show
  1. package/index.js +193 -128
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
- // Character code constants
2
1
  const CHAR_ASTERISK = 0x2A // *
3
- const CHAR_UNDERSCORE = 0x5F // _
2
+ //const CHAR_UNDERSCORE = 0x5F // _
4
3
  const CHAR_BACKSLASH = 0x5C // \
5
4
  const CHAR_BACKTICK = 0x60 // `
6
5
  const CHAR_DOLLAR = 0x24 // $
@@ -20,9 +19,14 @@ const hasBackslash = (state, start) => {
20
19
  let slashNum = 0
21
20
  let i = start - 1
22
21
  const src = state.src
23
- while(i >= 0) {
24
- if (src.charCodeAt(i) === CHAR_BACKSLASH) { slashNum++; i--; continue }
25
- break
22
+ // Early exit if no backslash at all
23
+ if (i < 0 || src.charCodeAt(i) !== CHAR_BACKSLASH) {
24
+ return false
25
+ }
26
+ // Count consecutive backslashes efficiently
27
+ while (i >= 0 && src.charCodeAt(i) === CHAR_BACKSLASH) {
28
+ slashNum++
29
+ i--
26
30
  }
27
31
  return slashNum % 2 === 1
28
32
  }
@@ -159,6 +163,26 @@ const hasNextSymbol = (state, n, max, symbol, noMark) => {
159
163
  return [nextSymbolPos, noMark]
160
164
  }
161
165
 
166
+ const processSymbolPair = (state, n, srcLen, symbol, noMark, textStart, pushInlines) => {
167
+ const [nextSymbolPos, newNoMark] = hasNextSymbol(state, n, srcLen, symbol, noMark)
168
+ if (nextSymbolPos !== -1) {
169
+ if (nextSymbolPos === srcLen - 1) {
170
+ pushInlines(textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
171
+ return { shouldBreak: true, newN: nextSymbolPos + 1, newNoMark }
172
+ }
173
+ return { shouldBreak: false, shouldContinue: true, newN: nextSymbolPos + 1, newNoMark }
174
+ }
175
+ return { shouldBreak: false, shouldContinue: false, newN: n, newNoMark }
176
+ }
177
+
178
+ const processTextSegment = (inlines, textStart, n, noMark) => {
179
+ if (n !== 0 && noMark.length !== 0) {
180
+ pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
181
+ return ''
182
+ }
183
+ return noMark
184
+ }
185
+
162
186
  const createInlines = (state, start, max, opt) => {
163
187
  const src = state.src
164
188
  const srcLen = max
@@ -184,86 +208,95 @@ const createInlines = (state, start, max, opt) => {
184
208
  }
185
209
 
186
210
  const currentChar = src.charCodeAt(n)
187
- let nextSymbolPos = -1
211
+
212
+ // Unified escape check
213
+ let isEscaped = false
214
+ if (currentChar === CHAR_ASTERISK || currentChar === CHAR_BACKTICK ||
215
+ (opt.dollarMath && currentChar === CHAR_DOLLAR) ||
216
+ (htmlEnabled && currentChar === CHAR_LT)) {
217
+ isEscaped = hasBackslash(state, n)
218
+ }
188
219
 
189
- // Inline code (backticks)
190
- if (currentChar === CHAR_BACKTICK && !hasBackslash(state, n)) {
191
- [nextSymbolPos, noMark] = hasNextSymbol(state, n, srcLen, CHAR_BACKTICK, noMark)
192
- if (nextSymbolPos !== -1) {
193
- if (nextSymbolPos === srcLen - 1) {
194
- pushInlines(inlines, textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
220
+ // Asterisk handling
221
+ if (currentChar === CHAR_ASTERISK) {
222
+ if (!isEscaped) {
223
+ noMark = processTextSegment(inlines, textStart, n, noMark)
224
+ if (n === srcLen - 1) {
225
+ pushInlines(inlines, n, n, 1, '')
195
226
  break
196
227
  }
197
- n = nextSymbolPos + 1
228
+ let i = n + 1
229
+ while (i < srcLen && src.charCodeAt(i) === CHAR_ASTERISK) {
230
+ i++
231
+ }
232
+ if (i === srcLen) {
233
+ pushInlines(inlines, n, i - 1, i - n, '')
234
+ } else {
235
+ pushInlines(inlines, n, i - 1, i - n, '')
236
+ textStart = i
237
+ }
238
+ n = i
198
239
  continue
199
240
  }
200
241
  }
201
242
 
202
- // Inline math ($...$)
203
- if (opt.dollarMath && currentChar === CHAR_DOLLAR && !hasBackslash(state, n)) {
204
- [nextSymbolPos, noMark] = hasNextSymbol(state, n, srcLen, CHAR_DOLLAR, noMark)
205
- if (nextSymbolPos !== -1) {
206
- if (nextSymbolPos === srcLen - 1) {
207
- pushInlines(inlines, textStart, nextSymbolPos, nextSymbolPos - textStart + 1, 'text')
208
- break
243
+ // Inline code (backticks)
244
+ if (currentChar === CHAR_BACKTICK) {
245
+ if (!isEscaped) {
246
+ const result = processSymbolPair(state, n, srcLen, CHAR_BACKTICK, noMark, textStart,
247
+ (start, end, len, type) => pushInlines(inlines, start, end, len, type))
248
+ if (result.shouldBreak) break
249
+ if (result.shouldContinue) {
250
+ n = result.newN
251
+ noMark = result.newNoMark
252
+ continue
209
253
  }
210
- n = nextSymbolPos + 1
211
- continue
254
+ noMark = result.newNoMark
212
255
  }
213
256
  }
214
257
 
215
- // HTML tags
216
- if (htmlEnabled && currentChar === CHAR_LT && !hasBackslash(state, n)) {
217
- let foundClosingTag = false
218
- for (let i = n + 1; i < srcLen; i++) {
219
- if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
220
- if (noMark.length !== 0) {
221
- pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
222
- noMark = ''
223
- }
224
- let tag = src.slice(n + 1, i)
225
- let tagType
226
- if (tag.charCodeAt(0) === CHAR_SLASH) {
227
- tag = tag.slice(1)
228
- tagType = 'close'
229
- } else {
230
- tagType = 'open'
231
- }
232
- pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
233
- textStart = i + 1
234
- n = i + 1
235
- foundClosingTag = true
236
- break
258
+ // Inline math ($...$)
259
+ if (opt.dollarMath && currentChar === CHAR_DOLLAR) {
260
+ if (!isEscaped) {
261
+ const result = processSymbolPair(state, n, srcLen, CHAR_DOLLAR, noMark, textStart,
262
+ (start, end, len, type) => pushInlines(inlines, start, end, len, type))
263
+ if (result.shouldBreak) break
264
+ if (result.shouldContinue) {
265
+ n = result.newN
266
+ noMark = result.newNoMark
267
+ continue
237
268
  }
269
+ noMark = result.newNoMark
238
270
  }
239
- if (foundClosingTag) {
240
- continue
241
- }
242
- // If no closing tag found, treat as regular character to prevent infinite loops
243
271
  }
244
272
 
245
- // Asterisk handling
246
- if (currentChar === CHAR_ASTERISK && !hasBackslash(state, n)) {
247
- if (n !== 0 && noMark.length !== 0) {
248
- pushInlines(inlines, textStart, n - 1, n - textStart, 'text')
249
- noMark = ''
250
- }
251
- if (n === srcLen - 1) {
252
- pushInlines(inlines, n, n, 1, '')
253
- break
254
- }
255
- let i = n + 1
256
- while (i < srcLen && src.charCodeAt(i) === CHAR_ASTERISK) {
257
- i++
258
- }
259
- if (i === srcLen) {
260
- pushInlines(inlines, n, i - 1, i - n, '')
261
- } else {
262
- pushInlines(inlines, n, i - 1, i - n, '')
263
- textStart = i
273
+ // HTML tags
274
+ if (htmlEnabled && currentChar === CHAR_LT) {
275
+ if (!isEscaped) {
276
+ let foundClosingTag = false
277
+ for (let i = n + 1; i < srcLen; i++) {
278
+ if (src.charCodeAt(i) === CHAR_GT && !hasBackslash(state, i)) {
279
+ noMark = processTextSegment(inlines, textStart, n, noMark)
280
+ let tag = src.slice(n + 1, i)
281
+ let tagType
282
+ if (tag.charCodeAt(0) === CHAR_SLASH) {
283
+ tag = tag.slice(1)
284
+ tagType = 'close'
285
+ } else {
286
+ tagType = 'open'
287
+ }
288
+ pushInlines(inlines, n, i, i - n + 1, 'html_inline', tag, tagType)
289
+ textStart = i + 1
290
+ n = i + 1
291
+ foundClosingTag = true
292
+ break
293
+ }
294
+ }
295
+ if (foundClosingTag) {
296
+ continue
297
+ }
298
+ // If no closing tag found, treat as regular character to prevent infinite loops
264
299
  }
265
- n = i
266
- continue
267
300
  }
268
301
 
269
302
  // Regular character
@@ -278,19 +311,31 @@ const createInlines = (state, start, max, opt) => {
278
311
  }
279
312
 
280
313
  const pushMark = (marks, opts) => {
281
- let left = 0, right = marks.length
314
+ // Maintain sorted order during insertion
315
+ const newMark = {
316
+ nest: opts.nest,
317
+ s: opts.s,
318
+ e: opts.e,
319
+ len: opts.len,
320
+ oLen: opts.oLen,
321
+ type: opts.type
322
+ }
323
+ // Binary search for insertion point to maintain sorted order
324
+ let left = 0
325
+ let right = marks.length
282
326
  while (left < right) {
283
- const mid = (left + right) >> 1
284
- if (marks[mid].s > opts.s) {
285
- right = mid
286
- } else {
327
+ const mid = Math.floor((left + right) / 2)
328
+ if (marks[mid].s <= newMark.s) {
287
329
  left = mid + 1
330
+ } else {
331
+ right = mid
288
332
  }
289
333
  }
290
- marks.splice(left, 0, { ...opts });
334
+
335
+ marks.splice(left, 0, newMark)
291
336
  }
292
337
 
293
- const setStrong = (state, inlines, marks, n, memo, opt) => {
338
+ const setStrong = (state, inlines, marks, n, memo, opt, nestTracker) => {
294
339
  if (opt.disallowMixed === true) {
295
340
  let i = n + 1
296
341
  const inlinesLength = inlines.length
@@ -324,7 +369,7 @@ const setStrong = (state, inlines, marks, n, memo, opt) => {
324
369
  if (insideTagsIsClose === 0) { i++; continue }
325
370
  }
326
371
 
327
- nest = checkNest(inlines, marks, n, i)
372
+ nest = checkNest(inlines, marks, n, i, nestTracker)
328
373
  if (nest === -1) return [n, nest]
329
374
 
330
375
  if (inlines[i].len === 1 && inlines[n].len > 2) {
@@ -349,7 +394,7 @@ const setStrong = (state, inlines, marks, n, memo, opt) => {
349
394
  inlines[i].len -= 1
350
395
  if (inlines[i].len > 0) inlines[i].sp += 1
351
396
  if (insideTagsIsClose === 1) {
352
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt)
397
+ const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker)
353
398
  n = newN
354
399
  nest = newNest
355
400
  }
@@ -395,7 +440,7 @@ const setStrong = (state, inlines, marks, n, memo, opt) => {
395
440
 
396
441
  if (inlines[n].len === 1 && inlines[i].len > 0) {
397
442
  nest++
398
- const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, nest)
443
+ const [newN, newNest] = setEm(state, inlines, marks, n, memo, opt, nest, nestTracker)
399
444
  n = newN
400
445
  nest = newNest
401
446
  }
@@ -424,18 +469,41 @@ const checkInsideTags = (inlines, i, memo) => {
424
469
  if (memo.htmlTags[tagName] < 0) {
425
470
  return -1
426
471
  }
427
- const closeAllTags = Object.values(memo.htmlTags).every(val => val === 0)
428
- if (closeAllTags) return 1
429
- return 0
472
+
473
+ // Direct check instead of Object.values().every()
474
+ for (const count of Object.values(memo.htmlTags)) {
475
+ if (count !== 0) return 0
476
+ }
477
+ return 1
430
478
  }
431
479
 
480
+ // Check if character is ASCII punctuation or space
481
+ // Covers: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ and space
432
482
  const isPunctuation = (ch) => {
433
- return REG_PUNCTUATION.test(ch)
483
+ if (!ch) return false
484
+ const code = ch.charCodeAt(0)
485
+ // ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
486
+ return (code >= 33 && code <= 47) || (code >= 58 && code <= 64) ||
487
+ (code >= 91 && code <= 96) || (code >= 123 && code <= 126) || code === 32
434
488
  }
489
+
490
+ // Check if character is Japanese (hiragana, katakana, kanji, punctuation, symbols, format chars, emoji)
491
+ // Uses fast Unicode range checks for common cases, falls back to REG_JAPANESE for complex Unicode
435
492
  const isJapanese = (ch) => {
436
- return REG_JAPANESE.test(ch)
493
+ if (!ch) return false
494
+ const code = ch.charCodeAt(0)
495
+ // Fast ASCII check first
496
+ if (code < 128) return false
497
+ // Hiragana: U+3040-U+309F, Katakana: U+30A0-U+30FF, Kanji: U+4E00-U+9FAF
498
+ return (code >= 0x3040 && code <= 0x309F) ||
499
+ (code >= 0x30A0 && code <= 0x30FF) ||
500
+ (code >= 0x4E00 && code <= 0x9FAF) ||
501
+ // Fallback to regex for complex Unicode cases
502
+ REG_JAPANESE.test(ch)
437
503
  }
438
504
 
505
+ // Check if character is English (letters, numbers) or other non-Japanese characters
506
+ // Uses REG_JAPANESE and REG_PUNCTUATION to exclude Japanese and punctuation characters
439
507
  const isEnglish = (ch) => {
440
508
  if (!ch) return false
441
509
  const code = ch.charCodeAt(0)
@@ -480,9 +548,6 @@ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt) => {
480
548
  const checkCloseNextChar = (isPunctuation(closeNextChar) || i === inlines.length - 1)
481
549
 
482
550
  if (opt.disallowMixed === false) {
483
- const openPrevChar = src[inlines[n].s - 1] || ''
484
- const closeNextChar = src[inlines[i].e + 1] || ''
485
-
486
551
  if (isEnglish(openPrevChar) || isEnglish(closeNextChar)) {
487
552
  const contentBetween = src.slice(inlines[n].e + 1, inlines[i].s)
488
553
  if (REG_MARKDOWN_HTML.test(contentBetween)) {
@@ -495,7 +560,7 @@ const hasPunctuationOrNonJapanese = (state, inlines, n, i, opt) => {
495
560
  return result
496
561
  }
497
562
 
498
- const setEm = (state, inlines, marks, n, memo, opt, sNest) => {
563
+ const setEm = (state, inlines, marks, n, memo, opt, sNest, nestTracker) => {
499
564
  if (opt.disallowMixed === true && !sNest) {
500
565
  let i = n + 1
501
566
  const inlinesLength = inlines.length
@@ -550,7 +615,7 @@ const setEm = (state, inlines, marks, n, memo, opt, sNest) => {
550
615
  if (sNest) {
551
616
  nest = sNest - 1
552
617
  } else {
553
- nest = checkNest(inlines, marks, n, i)
618
+ nest = checkNest(inlines, marks, n, i, nestTracker)
554
619
  }
555
620
  if (nest === -1) return [n, nest]
556
621
 
@@ -622,60 +687,60 @@ const setText = (inlines, marks, n, nest) => {
622
687
  inlines[n].len = 0
623
688
  }
624
689
 
625
- const checkNest = (inlines, marks, n, i) => {
626
- let nest = 1
627
- let isRange = true
628
- if (marks.length === 0) return nest
629
- let strongNest = 0
630
- let emNest = 0
631
- let j = 0
632
- const marksLength = marks.length
633
- while (j < marksLength) {
634
- if (marks[j].s <= inlines[n].s) {
635
- if (marks[j].type === 'strong_open') strongNest++
636
- if (marks[j].type === 'strong_close') strongNest--
637
- if (marks[j].type === 'em_open') emNest++
638
- if (marks[j].type === 'em_close') emNest--
639
- } else { break }
640
- j++
641
- }
642
- let parentNest = strongNest + emNest
643
- let parentCloseN = j
644
- if (parentCloseN < marksLength) {
645
- while (parentCloseN < marksLength) {
646
- if (marks[parentCloseN].nest === parentNest) break
647
- parentCloseN++
648
- }
649
- if (parentCloseN > marksLength - 1) {
650
- isRange = true
651
- } else {
652
- if (marks[parentCloseN].s < inlines[i].s) isRange = false
653
- }
690
+ // Nest state management
691
+ const createNestTracker = () => {
692
+ return {
693
+ strongNest: 0,
694
+ emNest: 0,
695
+ markIndex: 0
654
696
  }
697
+ }
655
698
 
656
- if (isRange) {
657
- nest = parentNest + 1
658
- } else {
659
- nest = -1
699
+ const updateNestTracker = (tracker, marks, targetPos) => {
700
+ while (tracker.markIndex < marks.length && marks[tracker.markIndex].s <= targetPos) {
701
+ const mark = marks[tracker.markIndex]
702
+ if (mark.type === 'strong_open') tracker.strongNest++
703
+ else if (mark.type === 'strong_close') tracker.strongNest--
704
+ else if (mark.type === 'em_open') tracker.emNest++
705
+ else if (mark.type === 'em_close') tracker.emNest--
706
+ tracker.markIndex++
660
707
  }
661
- return nest
708
+ }
709
+
710
+ const checkNest = (inlines, marks, n, i, nestTracker) => {
711
+ if (marks.length === 0) return 1
712
+ // Update nest state up to current position
713
+ updateNestTracker(nestTracker, marks, inlines[n].s)
714
+
715
+ const parentNest = nestTracker.strongNest + nestTracker.emNest
716
+ // Check if there's a conflicting close mark before the end position
717
+ let parentCloseN = nestTracker.markIndex
718
+ while (parentCloseN < marks.length) {
719
+ if (marks[parentCloseN].nest === parentNest) break
720
+ parentCloseN++
721
+ }
722
+ if (parentCloseN < marks.length && marks[parentCloseN].s < inlines[i].s) {
723
+ return -1
724
+ }
725
+ return parentNest + 1
662
726
  }
663
727
 
664
728
  const createMarks = (state, inlines, start, end, memo, opt) => {
665
729
  let marks = []
666
730
  let n = start
731
+ const nestTracker = createNestTracker()
667
732
 
668
733
  while (n < end) {
669
734
  if (inlines[n].type !== '') { n++; continue }
670
735
  let nest = 0
671
736
 
672
737
  if (inlines[n].len > 1) {
673
- const [newN, newNest] = setStrong(state, inlines, marks, n, memo, opt)
738
+ const [newN, newNest] = setStrong(state, inlines, marks, n, memo, opt, nestTracker)
674
739
  n = newN
675
740
  nest = newNest
676
741
  }
677
742
  if (inlines[n].len !== 0) {
678
- const [newN2, newNest2] = setEm(state, inlines, marks, n, memo, opt)
743
+ const [newN2, newNest2] = setEm(state, inlines, marks, n, memo, opt, null, nestTracker)
679
744
  n = newN2
680
745
  nest = newNest2
681
746
  }
@@ -687,9 +752,8 @@ const createMarks = (state, inlines, start, end, memo, opt) => {
687
752
  return marks
688
753
  }
689
754
 
690
-
691
755
  const mergeInlinesAndMarks = (inlines, marks) => {
692
- marks.sort((a, b) => a.s - b.s)
756
+ // marks array is already sorted, skip sorting
693
757
  const merged = []
694
758
  let markIndex = 0
695
759
  for (const token of inlines) {
@@ -780,4 +844,5 @@ const mditStrongJa = (md, option) => {
780
844
  return strongJa(state, silent, opt)
781
845
  })
782
846
  }
783
- export default mditStrongJa
847
+
848
+ export default mditStrongJa
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.4.4",
4
+ "version": "0.4.5",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "files": [