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

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.
@@ -1,17 +1,11 @@
1
- import { isJapaneseChar, getInlineWrapperBase } from '../token-utils.js'
1
+ import { isJapaneseChar } from '../token-utils.js'
2
+
3
+ const CHAR_ASTERISK = 0x2A // *
2
4
 
3
5
  const hasMarkerChars = (text) => {
4
6
  return !!text && text.indexOf('*') !== -1
5
7
  }
6
8
 
7
- const contentHasMarkerCharsFrom = (content, from) => {
8
- if (!content) return false
9
- const start = from > 0 ? from : 0
10
- if (start === 0) return hasMarkerChars(content)
11
- if (start >= content.length) return false
12
- return content.indexOf('*', start) !== -1
13
- }
14
-
15
9
  const isAsteriskEmphasisToken = (token) => {
16
10
  if (!token || !token.type) return false
17
11
  if (token.type !== 'strong_open' &&
@@ -78,20 +72,6 @@ const hasEmphasisSignalInRange = (tokens, startIdx, endIdx) => {
78
72
  return false
79
73
  }
80
74
 
81
- const hasTextMarkerCharsInRange = (tokens, startIdx, endIdx, firstTextOffset = 0) => {
82
- if (!tokens || startIdx < 0 || endIdx < startIdx) return false
83
- for (let i = startIdx; i <= endIdx && i < tokens.length; i++) {
84
- const token = tokens[i]
85
- if (!token || token.type !== 'text' || !token.content) continue
86
- if (i === startIdx && firstTextOffset > 0) {
87
- if (contentHasMarkerCharsFrom(token.content, firstTextOffset)) return true
88
- continue
89
- }
90
- if (textTokenHasMarkerChars(token)) return true
91
- }
92
- return false
93
- }
94
-
95
75
  const isStrongRunSoftSpace = (code) => {
96
76
  return code === 0x20 || code === 0x09 || code === 0x0A || code === 0x3000
97
77
  }
@@ -107,21 +87,24 @@ const isStrongRunTextLike = (code) => {
107
87
  return isStrongRunAsciiWord(code) || isJapaneseChar(code)
108
88
  }
109
89
 
110
- const countDelimiterLikeStrongRuns = (content, marker, from = 0, limit = 0) => {
90
+ const countDelimiterLikeStrongRuns = (content, from = 0, limit = 0) => {
111
91
  let at = from > 0 ? from : 0
112
92
  const len = content.length
113
- const markerCode = marker.charCodeAt(0)
114
93
  let count = 0
115
- while (at < len) {
116
- const pos = content.indexOf(marker, at)
117
- if (pos === -1) break
94
+ while (at + 1 < len) {
95
+ if (content.charCodeAt(at) !== CHAR_ASTERISK ||
96
+ content.charCodeAt(at + 1) !== CHAR_ASTERISK) {
97
+ at++
98
+ continue
99
+ }
100
+ const pos = at
118
101
  const prevCode = pos > 0 ? content.charCodeAt(pos - 1) : 0
119
- const nextPos = pos + marker.length
102
+ const nextPos = pos + 2
120
103
  const nextCode = nextPos < len ? content.charCodeAt(nextPos) : 0
121
- const prevSameMarker = prevCode === markerCode
122
- const nextSameMarker = nextCode === markerCode
104
+ const prevSameMarker = prevCode === CHAR_ASTERISK
105
+ const nextSameMarker = nextCode === CHAR_ASTERISK
123
106
  if (prevSameMarker || nextSameMarker) {
124
- at = pos + marker.length
107
+ at = pos + 2
125
108
  continue
126
109
  }
127
110
  const prevSoft = prevCode !== 0 && isStrongRunSoftSpace(prevCode)
@@ -131,97 +114,55 @@ const countDelimiterLikeStrongRuns = (content, marker, from = 0, limit = 0) => {
131
114
  const nextTextLike = isStrongRunTextLike(nextCode)
132
115
  const hasTextNeighbor = prevTextLike || nextTextLike
133
116
  if (!hasTextNeighbor) {
134
- at = pos + marker.length
117
+ at = pos + 2
135
118
  continue
136
119
  }
137
120
  const atBoundary = prevCode === 0 || nextCode === 0
138
121
  if (!atBoundary && (!prevTextLike || !nextTextLike)) {
139
- at = pos + marker.length
122
+ at = pos + 2
140
123
  continue
141
124
  }
142
125
  if (hasPrevOrNext && !prevSoft && !nextSoft) {
143
126
  count++
144
127
  if (limit > 0 && count >= limit) return count
145
128
  }
146
- at = pos + marker.length
129
+ at = pos + 2
147
130
  }
148
131
  return count
149
132
  }
150
133
 
151
- const countStrongMarkerRunsInTextRange = (tokens, startIdx, endIdx, firstTextOffset = 0, limit = 0) => {
152
- if (!tokens || startIdx < 0 || endIdx < startIdx) return 0
153
- let total = 0
154
- for (let i = startIdx; i <= endIdx && i < tokens.length; i++) {
155
- const token = tokens[i]
156
- if (!token || token.type !== 'text' || !token.content) continue
157
- const content = token.content
158
- const scanFrom = i === startIdx && firstTextOffset > 0 ? firstTextOffset : 0
159
- if (scanFrom >= content.length) continue
160
- const remain = limit > 0 ? (limit - total) : 0
161
- total += countDelimiterLikeStrongRuns(content, '**', scanFrom, remain)
162
- if (limit > 0 && total >= limit) {
163
- return total
164
- }
165
- }
166
- return total
167
- }
168
-
169
134
  const buildAsteriskWrapperPrefixStats = (tokens) => {
170
135
  const len = Array.isArray(tokens) ? tokens.length : 0
171
136
  const strongDepthPrefix = new Array(len + 1)
172
137
  const emDepthPrefix = new Array(len + 1)
173
- const strongOpenPrefix = new Array(len + 1)
174
- const strongClosePrefix = new Array(len + 1)
175
- const emOpenPrefix = new Array(len + 1)
176
- const emClosePrefix = new Array(len + 1)
177
138
  let strongDepth = 0
178
139
  let emDepthCount = 0
179
- let strongOpenCount = 0
180
- let strongCloseCount = 0
181
- let emOpenCount = 0
182
- let emCloseCount = 0
183
140
  strongDepthPrefix[0] = 0
184
141
  emDepthPrefix[0] = 0
185
- strongOpenPrefix[0] = 0
186
- strongClosePrefix[0] = 0
187
- emOpenPrefix[0] = 0
188
- emClosePrefix[0] = 0
189
142
  for (let i = 0; i < len; i++) {
190
143
  const token = tokens[i]
191
144
  if (token && token.type && isAsteriskEmphasisToken(token)) {
192
145
  if (token.type === 'strong_open') {
193
146
  strongDepth++
194
- strongOpenCount++
195
147
  } else if (token.type === 'strong_close') {
196
148
  if (strongDepth > 0) strongDepth--
197
- strongCloseCount++
198
149
  } else if (token.type === 'em_open') {
199
150
  emDepthCount++
200
- emOpenCount++
201
151
  } else if (token.type === 'em_close') {
202
152
  if (emDepthCount > 0) emDepthCount--
203
- emCloseCount++
204
153
  }
205
154
  }
206
155
  strongDepthPrefix[i + 1] = strongDepth
207
156
  emDepthPrefix[i + 1] = emDepthCount
208
- strongOpenPrefix[i + 1] = strongOpenCount
209
- strongClosePrefix[i + 1] = strongCloseCount
210
- emOpenPrefix[i + 1] = emOpenCount
211
- emClosePrefix[i + 1] = emCloseCount
212
157
  }
213
158
  return {
214
159
  strongDepth: strongDepthPrefix,
215
- emDepth: emDepthPrefix,
216
- strongOpen: strongOpenPrefix,
217
- strongClose: strongClosePrefix,
218
- emOpen: emOpenPrefix,
219
- emClose: emClosePrefix
160
+ emDepth: emDepthPrefix
220
161
  }
221
162
  }
222
163
 
223
- const buildBrokenRefWrapperRangeSignals = (tokens, startIdx, endIdx, firstTextOffset = 0) => {
224
- const out = {
164
+ const createBrokenRefWrapperRangeSignals = () => {
165
+ return {
225
166
  hasLeadingUnmatchedClose: false,
226
167
  hasImbalance: false,
227
168
  hasAsteriskEmphasisToken: false,
@@ -229,123 +170,148 @@ const buildBrokenRefWrapperRangeSignals = (tokens, startIdx, endIdx, firstTextOf
229
170
  hasUnderscoreText: false,
230
171
  hasCodeInline: false,
231
172
  hasUnderscoreEmphasisToken: false,
173
+ hasTextMarker: false,
174
+ strongRunCount: 0,
232
175
  strongOpenInRange: 0,
233
176
  strongCloseInRange: 0,
234
177
  emOpenInRange: 0,
235
178
  emCloseInRange: 0
236
179
  }
237
- if (!tokens || startIdx < 0 || endIdx < startIdx) return out
238
- const depthMap = new Map()
239
- let sawWrapper = false
240
- let sawOpen = false
180
+ }
181
+
182
+ const updateBrokenRefTextRangeSignals = (signals, token, tokenIdx, startIdx, firstTextOffset) => {
183
+ if (!token || token.type !== 'text' || !token.content) return
184
+ const content = token.content
185
+ const scanFrom = tokenIdx === startIdx && firstTextOffset > 0 ? firstTextOffset : 0
186
+ // Keep this at 0 (instead of firstTextOffset) so historical fail-safe
187
+ // behavior around noisy leading chains in the first text token stays unchanged.
188
+ if (!signals.hasLongStarNoise && content.indexOf('***') !== -1) {
189
+ signals.hasLongStarNoise = true
190
+ }
191
+ if (!signals.hasUnderscoreText) {
192
+ if (scanFrom < content.length && content.indexOf('_', scanFrom) !== -1) {
193
+ signals.hasUnderscoreText = true
194
+ }
195
+ }
196
+ if (!signals.hasTextMarker) {
197
+ signals.hasTextMarker = scanFrom === 0
198
+ ? textTokenHasMarkerChars(token)
199
+ : content.indexOf('*', scanFrom) !== -1
200
+ }
201
+ if (signals.strongRunCount < 2 && scanFrom < content.length) {
202
+ signals.strongRunCount += countDelimiterLikeStrongRuns(content, scanFrom, 2 - signals.strongRunCount)
203
+ }
204
+ }
205
+
206
+ const updateBrokenRefWrapperTokenSignals = (signals, token, isAsteriskEmphasis) => {
207
+ if (!signals.hasCodeInline && token.type === 'code_inline') {
208
+ signals.hasCodeInline = true
209
+ }
210
+ if (isAsteriskEmphasis) {
211
+ signals.hasAsteriskEmphasisToken = true
212
+ }
213
+ if (!signals.hasUnderscoreEmphasisToken &&
214
+ (token.type === 'strong_open' ||
215
+ token.type === 'strong_close' ||
216
+ token.type === 'em_open' ||
217
+ token.type === 'em_close') &&
218
+ (token.markup === '_' || token.markup === '__')) {
219
+ signals.hasUnderscoreEmphasisToken = true
220
+ }
221
+ }
222
+
223
+ const updateBrokenRefWrapperRangeDepthSignals = (signals, token, wrapperState, isAsteriskEmphasis) => {
224
+ if (!isAsteriskEmphasis) return
225
+ let depthKey = ''
226
+ if (token.type === 'strong_open' || token.type === 'strong_close') {
227
+ depthKey = 'strongDepth'
228
+ } else if (token.type === 'em_open' || token.type === 'em_close') {
229
+ depthKey = 'emDepth'
230
+ } else {
231
+ return
232
+ }
233
+ const isOpen = token.type.endsWith('_open')
234
+ if (!wrapperState.sawWrapper) {
235
+ wrapperState.sawWrapper = true
236
+ if (!isOpen) signals.hasLeadingUnmatchedClose = true
237
+ }
238
+ if (isOpen) {
239
+ wrapperState.sawOpen = true
240
+ signals.hasLeadingUnmatchedClose = false
241
+ wrapperState[depthKey]++
242
+ } else if (wrapperState[depthKey] <= 0) {
243
+ signals.hasImbalance = true
244
+ } else {
245
+ wrapperState[depthKey]--
246
+ }
247
+ if (token.type === 'strong_open') signals.strongOpenInRange++
248
+ else if (token.type === 'strong_close') signals.strongCloseInRange++
249
+ else if (token.type === 'em_open') signals.emOpenInRange++
250
+ else if (token.type === 'em_close') signals.emCloseInRange++
251
+ }
252
+
253
+ const finalizeBrokenRefWrapperRangeSignals = (signals, wrapperState) => {
254
+ if (!wrapperState.sawWrapper || wrapperState.sawOpen) {
255
+ signals.hasLeadingUnmatchedClose = false
256
+ }
257
+ if (!signals.hasImbalance &&
258
+ (wrapperState.strongDepth !== 0 || wrapperState.emDepth !== 0)) {
259
+ signals.hasImbalance = true
260
+ }
261
+ return signals
262
+ }
263
+
264
+ const buildBrokenRefWrapperRangeSignals = (tokens, startIdx, endIdx, firstTextOffset = 0) => {
265
+ const signals = createBrokenRefWrapperRangeSignals()
266
+ if (!tokens || startIdx < 0 || endIdx < startIdx) return signals
267
+ const wrapperState = { sawWrapper: false, sawOpen: false, strongDepth: 0, emDepth: 0 }
241
268
  for (let i = startIdx; i <= endIdx && i < tokens.length; i++) {
242
269
  const token = tokens[i]
243
270
  if (!token || !token.type) continue
244
- if (!out.hasCodeInline && token.type === 'code_inline') {
245
- out.hasCodeInline = true
246
- }
247
271
  const isAsteriskEmphasis = isAsteriskEmphasisToken(token)
248
- if (isAsteriskEmphasis) out.hasAsteriskEmphasisToken = true
249
- if (!out.hasUnderscoreEmphasisToken &&
250
- (token.type === 'strong_open' ||
251
- token.type === 'strong_close' ||
252
- token.type === 'em_open' ||
253
- token.type === 'em_close') &&
254
- (token.markup === '_' || token.markup === '__')) {
255
- out.hasUnderscoreEmphasisToken = true
256
- }
257
- if (token.type === 'text' && token.content) {
258
- const content = token.content
259
- // Keep this at 0 (instead of firstTextOffset) so historical fail-safe
260
- // behavior around noisy leading chains in the first text token stays unchanged.
261
- if (!out.hasLongStarNoise && content.indexOf('***') !== -1) {
262
- out.hasLongStarNoise = true
263
- }
264
- if (!out.hasUnderscoreText) {
265
- const scanFrom = i === startIdx && firstTextOffset > 0 ? firstTextOffset : 0
266
- if (scanFrom < content.length && content.indexOf('_', scanFrom) !== -1) {
267
- out.hasUnderscoreText = true
268
- }
269
- }
270
- }
271
- if ((token.type === 'strong_open' || token.type === 'strong_close' || token.type === 'em_open' || token.type === 'em_close') &&
272
- !isAsteriskEmphasis) {
273
- continue
274
- }
275
- const base = getInlineWrapperBase(token.type)
276
- if (!base) continue
277
- const isOpen = token.type.endsWith('_open')
278
- if (!sawWrapper) {
279
- sawWrapper = true
280
- if (!isOpen) out.hasLeadingUnmatchedClose = true
281
- }
282
- if (isOpen) {
283
- sawOpen = true
284
- out.hasLeadingUnmatchedClose = false
285
- depthMap.set(base, (depthMap.get(base) || 0) + 1)
286
- } else {
287
- const prev = depthMap.get(base) || 0
288
- if (prev <= 0) {
289
- out.hasImbalance = true
290
- } else {
291
- depthMap.set(base, prev - 1)
292
- }
293
- }
294
- if (token.type === 'strong_open') out.strongOpenInRange++
295
- else if (token.type === 'strong_close') out.strongCloseInRange++
296
- else if (token.type === 'em_open') out.emOpenInRange++
297
- else if (token.type === 'em_close') out.emCloseInRange++
298
- }
299
- if (!sawWrapper || sawOpen) out.hasLeadingUnmatchedClose = false
300
- if (!out.hasImbalance) {
301
- for (const depth of depthMap.values()) {
302
- if (depth !== 0) {
303
- out.hasImbalance = true
304
- break
305
- }
306
- }
272
+ updateBrokenRefWrapperTokenSignals(signals, token, isAsteriskEmphasis)
273
+ updateBrokenRefTextRangeSignals(signals, token, i, startIdx, firstTextOffset)
274
+ updateBrokenRefWrapperRangeDepthSignals(signals, token, wrapperState, isAsteriskEmphasis)
307
275
  }
308
- return out
276
+ return finalizeBrokenRefWrapperRangeSignals(signals, wrapperState)
277
+ }
278
+
279
+ const hasRangeCloseOnlyWrapperSignals = (signals) => {
280
+ if (!signals) return false
281
+ return (signals.strongCloseInRange > 0 && signals.strongOpenInRange === 0) ||
282
+ (signals.emCloseInRange > 0 && signals.emOpenInRange === 0)
309
283
  }
310
284
 
311
285
  const hasPreexistingWrapperCloseOnlyInRange = (tokens, startIdx, endIdx, prefixStats = null, wrapperSignals = null) => {
312
286
  if (!tokens || startIdx <= 0 || endIdx < startIdx) return false
313
287
  const signals = wrapperSignals || buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, 0)
288
+ if (!hasRangeCloseOnlyWrapperSignals(signals)) return false
289
+ const needsStrongCloseOnly = signals.strongCloseInRange > 0 && signals.strongOpenInRange === 0
290
+ const needsEmCloseOnly = signals.emCloseInRange > 0 && signals.emOpenInRange === 0
314
291
 
315
292
  let preStrongDepth = 0
316
293
  let preEmDepth = 0
317
294
  const hasPrefix =
318
295
  !!prefixStats &&
319
296
  Array.isArray(prefixStats.strongDepth) &&
320
- Array.isArray(prefixStats.emDepth) &&
321
- Array.isArray(prefixStats.strongOpen) &&
322
- Array.isArray(prefixStats.strongClose) &&
323
- Array.isArray(prefixStats.emOpen) &&
324
- Array.isArray(prefixStats.emClose)
297
+ Array.isArray(prefixStats.emDepth)
325
298
  if (hasPrefix &&
326
299
  startIdx < prefixStats.strongDepth.length &&
327
- startIdx < prefixStats.emDepth.length &&
328
- (endIdx + 1) < prefixStats.strongOpen.length &&
329
- (endIdx + 1) < prefixStats.strongClose.length &&
330
- (endIdx + 1) < prefixStats.emOpen.length &&
331
- (endIdx + 1) < prefixStats.emClose.length) {
332
- preStrongDepth = prefixStats.strongDepth[startIdx] || 0
333
- preEmDepth = prefixStats.emDepth[startIdx] || 0
334
- if (preStrongDepth > 0) {
335
- const strongOpensInRange = signals.strongOpenInRange
336
- const strongClosesInRange = signals.strongCloseInRange
337
- if (strongClosesInRange > 0 && strongOpensInRange === 0) return true
300
+ startIdx < prefixStats.emDepth.length) {
301
+ if (needsStrongCloseOnly) {
302
+ preStrongDepth = prefixStats.strongDepth[startIdx] || 0
303
+ if (preStrongDepth > 0) return true
338
304
  }
339
- if (preEmDepth > 0) {
340
- const emOpensInRange = signals.emOpenInRange
341
- const emClosesInRange = signals.emCloseInRange
342
- if (emClosesInRange > 0 && emOpensInRange === 0) return true
305
+ if (needsEmCloseOnly) {
306
+ preEmDepth = prefixStats.emDepth[startIdx] || 0
307
+ if (preEmDepth > 0) return true
343
308
  }
344
309
  return false
345
- } else {
346
- for (let i = 0; i < startIdx && i < tokens.length; i++) {
347
- const token = tokens[i]
348
- if (!token || !token.type || !isAsteriskEmphasisToken(token)) continue
310
+ }
311
+ for (let i = 0; i < startIdx && i < tokens.length; i++) {
312
+ const token = tokens[i]
313
+ if (!token || !token.type || !isAsteriskEmphasisToken(token)) continue
314
+ if (needsStrongCloseOnly) {
349
315
  if (token.type === 'strong_open') {
350
316
  preStrongDepth++
351
317
  continue
@@ -354,42 +320,100 @@ const hasPreexistingWrapperCloseOnlyInRange = (tokens, startIdx, endIdx, prefixS
354
320
  if (preStrongDepth > 0) preStrongDepth--
355
321
  continue
356
322
  }
323
+ }
324
+ if (needsEmCloseOnly) {
357
325
  if (token.type === 'em_open') {
358
326
  preEmDepth++
359
327
  continue
360
328
  }
361
- if (token.type === 'em_close') {
362
- if (preEmDepth > 0) preEmDepth--
329
+ if (token.type === 'em_close' && preEmDepth > 0) {
330
+ preEmDepth--
363
331
  }
364
332
  }
365
333
  }
366
- if (preStrongDepth > 0 && signals.strongCloseInRange > 0 && signals.strongOpenInRange === 0) return true
367
- if (preEmDepth > 0 && signals.emCloseInRange > 0 && signals.emOpenInRange === 0) return true
334
+ if (needsStrongCloseOnly && preStrongDepth > 0) return true
335
+ if (needsEmCloseOnly && preEmDepth > 0) return true
368
336
  return false
369
337
  }
370
338
 
339
+ const hasBrokenRefLowConfidenceTextNoise = (signals) => {
340
+ return signals.hasLongStarNoise || signals.hasUnderscoreText
341
+ }
342
+
343
+ const hasBrokenRefLowConfidenceInlineSyntax = (signals) => {
344
+ return signals.hasCodeInline || signals.hasUnderscoreEmphasisToken
345
+ }
346
+
347
+ const hasBrokenRefLowConfidenceNoise = (signals) => {
348
+ return hasBrokenRefLowConfidenceTextNoise(signals) || hasBrokenRefLowConfidenceInlineSyntax(signals)
349
+ }
350
+
351
+ const hasBrokenRefCloseOnlyWrapperRisk = (
352
+ tokens,
353
+ startIdx,
354
+ endIdx,
355
+ wrapperPrefixStats = null,
356
+ wrapperSignals = null
357
+ ) => {
358
+ const signals = wrapperSignals || buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, 0)
359
+ return hasPreexistingWrapperCloseOnlyInRange(tokens, startIdx, endIdx, wrapperPrefixStats, signals)
360
+ }
361
+
362
+ const hasBrokenRefLowConfidenceWrapperRisk = (
363
+ tokens,
364
+ startIdx,
365
+ endIdx,
366
+ wrapperPrefixStats = null,
367
+ wrapperSignals = null
368
+ ) => {
369
+ const signals = wrapperSignals || buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, 0)
370
+ if (signals.hasLeadingUnmatchedClose) return true
371
+ return hasBrokenRefCloseOnlyWrapperRisk(tokens, startIdx, endIdx, wrapperPrefixStats, signals)
372
+ }
373
+
371
374
  const isLowConfidenceBrokenRefRange = (tokens, startIdx, endIdx, firstTextOffset = 0, wrapperPrefixStats = null, wrapperSignals = null) => {
372
375
  const signals = wrapperSignals || buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, firstTextOffset)
373
- if (signals.hasLongStarNoise) return true
374
- if (signals.hasUnderscoreText || signals.hasCodeInline || signals.hasUnderscoreEmphasisToken) return true
375
- if (signals.hasLeadingUnmatchedClose) return true
376
- if (hasPreexistingWrapperCloseOnlyInRange(tokens, startIdx, endIdx, wrapperPrefixStats, signals)) return true
377
- return false
376
+ if (hasBrokenRefLowConfidenceNoise(signals)) return true
377
+ return hasBrokenRefLowConfidenceWrapperRisk(tokens, startIdx, endIdx, wrapperPrefixStats, signals)
378
378
  }
379
379
 
380
- const shouldAttemptBrokenRefRewrite = (tokens, startIdx, endIdx, firstTextOffset = 0, wrapperPrefixStats = null) => {
381
- const wrapperSignals = buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, firstTextOffset)
382
- if (isLowConfidenceBrokenRefRange(tokens, startIdx, endIdx, firstTextOffset, wrapperPrefixStats, wrapperSignals)) return false
383
- if (wrapperSignals.hasImbalance) {
384
- if (wrapperSignals.hasAsteriskEmphasisToken) return true
385
- return countStrongMarkerRunsInTextRange(tokens, startIdx, endIdx, firstTextOffset, 2) >= 2
386
- }
387
- if (wrapperSignals.hasAsteriskEmphasisToken) return false
388
- return countStrongMarkerRunsInTextRange(tokens, startIdx, endIdx, firstTextOffset, 2) >= 2
380
+ const hasBrokenRefStrongRunEvidence = (wrapperSignals) => {
381
+ return !!wrapperSignals && wrapperSignals.strongRunCount >= 2
389
382
  }
390
383
 
391
- const scanInlinePostprocessSignals = (children, hasBracketTextInContent = false) => {
392
- let hasBracketText = hasBracketTextInContent
384
+ const hasBrokenRefExplicitAsteriskSignal = (wrapperSignals) => {
385
+ return wrapperSignals.hasAsteriskEmphasisToken
386
+ }
387
+
388
+ const hasBrokenRefImmediateRewriteSignal = (wrapperSignals) => {
389
+ return wrapperSignals.hasImbalance && hasBrokenRefExplicitAsteriskSignal(wrapperSignals)
390
+ }
391
+
392
+ const shouldRejectBalancedBrokenRefRewrite = (wrapperSignals) => {
393
+ return !wrapperSignals.hasImbalance && hasBrokenRefExplicitAsteriskSignal(wrapperSignals)
394
+ }
395
+
396
+ const shouldAttemptBrokenRefRewriteFromSignals = (wrapperSignals) => {
397
+ if (hasBrokenRefImmediateRewriteSignal(wrapperSignals)) return true
398
+ if (shouldRejectBalancedBrokenRefRewrite(wrapperSignals)) return false
399
+ return hasBrokenRefStrongRunEvidence(wrapperSignals)
400
+ }
401
+
402
+ const shouldAttemptBrokenRefRewrite = (
403
+ tokens,
404
+ startIdx,
405
+ endIdx,
406
+ firstTextOffset = 0,
407
+ wrapperPrefixStats = null,
408
+ wrapperSignals = null
409
+ ) => {
410
+ const signals = wrapperSignals || buildBrokenRefWrapperRangeSignals(tokens, startIdx, endIdx, firstTextOffset)
411
+ if (!signals.hasTextMarker) return false
412
+ if (isLowConfidenceBrokenRefRange(tokens, startIdx, endIdx, firstTextOffset, wrapperPrefixStats, signals)) return false
413
+ return shouldAttemptBrokenRefRewriteFromSignals(signals)
414
+ }
415
+
416
+ const scanInlinePostprocessSignals = (children) => {
393
417
  let hasEmphasis = false
394
418
  let hasLinkOpen = false
395
419
  let hasLinkClose = false
@@ -409,14 +433,9 @@ const scanInlinePostprocessSignals = (children, hasBracketTextInContent = false)
409
433
  if (!hasCodeInline && child.type === 'code_inline') {
410
434
  hasCodeInline = true
411
435
  }
412
- if (hasBracketText || child.type !== 'text' || !child.content) continue
413
- if (child.content.indexOf('[') !== -1 || child.content.indexOf(']') !== -1) {
414
- hasBracketText = true
415
- }
416
- if (hasEmphasis && hasBracketText && hasLinkOpen && hasLinkClose) break
436
+ if (hasEmphasis && hasLinkOpen && hasLinkClose) break
417
437
  }
418
438
  return {
419
- hasBracketText,
420
439
  hasEmphasis,
421
440
  hasLinkOpen,
422
441
  hasLinkClose,
@@ -429,8 +448,8 @@ export {
429
448
  isAsteriskEmphasisToken,
430
449
  hasJapaneseContextInRange,
431
450
  hasEmphasisSignalInRange,
432
- hasTextMarkerCharsInRange,
433
451
  buildAsteriskWrapperPrefixStats,
452
+ buildBrokenRefWrapperRangeSignals,
434
453
  shouldAttemptBrokenRefRewrite,
435
454
  scanInlinePostprocessSignals
436
455
  }