@peaceroad/markdown-it-strong-ja 0.7.2 → 0.8.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.
- package/README.md +314 -195
- package/index.js +18 -48
- package/package.json +23 -5
- package/src/token-compat.js +77 -22
- package/src/token-core.js +467 -92
- package/src/token-link-utils.js +104 -400
- package/src/token-postprocess/fastpaths.js +349 -0
- package/src/token-postprocess/guards.js +436 -0
- package/src/token-postprocess/orchestrator.js +733 -0
- package/src/token-postprocess.js +1 -334
- package/src/token-utils.js +192 -148
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import Token from 'markdown-it/lib/token.mjs'
|
|
2
|
+
import { buildLinkCloseMap, convertCollapsedReferenceLinks, mergeBrokenMarksAroundLinks } from '../token-link-utils.js'
|
|
3
|
+
import {
|
|
4
|
+
rebuildInlineLevels,
|
|
5
|
+
fixEmOuterStrongSequence,
|
|
6
|
+
fixLeadingAsteriskEm,
|
|
7
|
+
fixTrailingStrong
|
|
8
|
+
} from '../token-core.js'
|
|
9
|
+
import {
|
|
10
|
+
getInlineWrapperBase,
|
|
11
|
+
getRuntimeOpt,
|
|
12
|
+
MODE_FLAG_COMPATIBLE,
|
|
13
|
+
MODE_FLAG_AGGRESSIVE,
|
|
14
|
+
MODE_FLAG_JAPANESE_PLUS,
|
|
15
|
+
MODE_FLAG_JAPANESE_ANY
|
|
16
|
+
} from '../token-utils.js'
|
|
17
|
+
import {
|
|
18
|
+
hasMarkerChars,
|
|
19
|
+
isAsteriskEmphasisToken,
|
|
20
|
+
hasJapaneseContextInRange,
|
|
21
|
+
hasEmphasisSignalInRange,
|
|
22
|
+
hasTextMarkerCharsInRange,
|
|
23
|
+
buildAsteriskWrapperPrefixStats,
|
|
24
|
+
shouldAttemptBrokenRefRewrite,
|
|
25
|
+
scanInlinePostprocessSignals
|
|
26
|
+
} from './guards.js'
|
|
27
|
+
import {
|
|
28
|
+
BROKEN_REF_FAST_PATH_RESULT_NO_ACTIVE_SIGNATURE,
|
|
29
|
+
BROKEN_REF_FAST_PATH_RESULT_NO_MATCH,
|
|
30
|
+
applyBrokenRefTokenOnlyFastPath,
|
|
31
|
+
tryFixTailPatternTokenOnly,
|
|
32
|
+
tryFixTailDanglingStrongCloseTokenOnly
|
|
33
|
+
} from './fastpaths.js'
|
|
34
|
+
|
|
35
|
+
const scanBrokenRefState = (text, out) => {
|
|
36
|
+
if (!text || text.indexOf('[') === -1) {
|
|
37
|
+
out.depth = 0
|
|
38
|
+
out.brokenEnd = false
|
|
39
|
+
out.tailOpen = -1
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
let depth = 0
|
|
43
|
+
let lastOpen = -1
|
|
44
|
+
let lastClose = -1
|
|
45
|
+
for (let i = 0; i < text.length; i++) {
|
|
46
|
+
const ch = text.charCodeAt(i)
|
|
47
|
+
if (ch === 0x5B) {
|
|
48
|
+
depth++
|
|
49
|
+
lastOpen = i
|
|
50
|
+
} else if (ch === 0x5D) {
|
|
51
|
+
if (depth > 0) depth--
|
|
52
|
+
lastClose = i
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
out.depth = depth
|
|
56
|
+
out.brokenEnd = depth > 0 && lastOpen > lastClose
|
|
57
|
+
out.tailOpen = out.brokenEnd ? lastOpen : -1
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resetBrokenRefScanState = (scanState) => {
|
|
62
|
+
if (!scanState) return scanState
|
|
63
|
+
scanState.depth = 0
|
|
64
|
+
scanState.brokenEnd = false
|
|
65
|
+
scanState.tailOpen = -1
|
|
66
|
+
return scanState
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const updateBracketDepth = (text, depth) => {
|
|
70
|
+
if (!text || depth <= 0) return depth
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
const ch = text.charCodeAt(i)
|
|
73
|
+
if (ch === 0x5B) {
|
|
74
|
+
depth++
|
|
75
|
+
} else if (ch === 0x5D) {
|
|
76
|
+
if (depth > 0) {
|
|
77
|
+
depth--
|
|
78
|
+
if (depth === 0) return 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return depth
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const expandSegmentEndForWrapperBalance = (tokens, startIdx, endIdx) => {
|
|
86
|
+
if (!tokens || startIdx < 0 || endIdx < startIdx) return endIdx
|
|
87
|
+
const depthMap = new Map()
|
|
88
|
+
let openDepthTotal = 0
|
|
89
|
+
let expandedEnd = endIdx
|
|
90
|
+
|
|
91
|
+
for (let i = startIdx; i <= expandedEnd; i++) {
|
|
92
|
+
const token = tokens[i]
|
|
93
|
+
if (!token || !token.type) continue
|
|
94
|
+
if ((token.type === 'strong_open' || token.type === 'strong_close' || token.type === 'em_open' || token.type === 'em_close') &&
|
|
95
|
+
!isAsteriskEmphasisToken(token)) {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const base = getInlineWrapperBase(token.type)
|
|
99
|
+
if (!base) continue
|
|
100
|
+
if (token.type.endsWith('_open')) {
|
|
101
|
+
depthMap.set(base, (depthMap.get(base) || 0) + 1)
|
|
102
|
+
openDepthTotal++
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const prev = depthMap.get(base) || 0
|
|
106
|
+
if (prev > 0) {
|
|
107
|
+
depthMap.set(base, prev - 1)
|
|
108
|
+
openDepthTotal--
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
while (openDepthTotal > 0 && expandedEnd + 1 < tokens.length) {
|
|
113
|
+
expandedEnd++
|
|
114
|
+
const token = tokens[expandedEnd]
|
|
115
|
+
if (!token || !token.type) continue
|
|
116
|
+
if ((token.type === 'strong_open' || token.type === 'strong_close' || token.type === 'em_open' || token.type === 'em_close') &&
|
|
117
|
+
!isAsteriskEmphasisToken(token)) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
const base = getInlineWrapperBase(token.type)
|
|
121
|
+
if (!base) continue
|
|
122
|
+
if (token.type.endsWith('_open')) {
|
|
123
|
+
depthMap.set(base, (depthMap.get(base) || 0) + 1)
|
|
124
|
+
openDepthTotal++
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
const prev = depthMap.get(base) || 0
|
|
128
|
+
if (prev > 0) {
|
|
129
|
+
depthMap.set(base, prev - 1)
|
|
130
|
+
openDepthTotal--
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return openDepthTotal > 0 ? -1 : expandedEnd
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fallbackMarkupByType = (type) => {
|
|
138
|
+
if (type === 'strong_open' || type === 'strong_close') return '**'
|
|
139
|
+
if (type === 'em_open' || type === 'em_close') return '*'
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const makeTokenLiteralText = (token) => {
|
|
144
|
+
if (!token) return
|
|
145
|
+
const literal = token.markup || fallbackMarkupByType(token.type)
|
|
146
|
+
token.type = 'text'
|
|
147
|
+
token.tag = ''
|
|
148
|
+
token.nesting = 0
|
|
149
|
+
token.content = literal
|
|
150
|
+
token.markup = ''
|
|
151
|
+
token.info = ''
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sanitizeEmStrongBalance = (tokens) => {
|
|
155
|
+
if (!tokens || tokens.length === 0) return false
|
|
156
|
+
const stack = []
|
|
157
|
+
let changed = false
|
|
158
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
159
|
+
const token = tokens[i]
|
|
160
|
+
if (!token || !token.type) continue
|
|
161
|
+
if (token.type === 'strong_open' || token.type === 'em_open') {
|
|
162
|
+
stack.push({ type: token.type, idx: i })
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (token.type !== 'strong_close' && token.type !== 'em_close') continue
|
|
166
|
+
const expected = token.type === 'strong_close' ? 'strong_open' : 'em_open'
|
|
167
|
+
if (stack.length > 0 && stack[stack.length - 1].type === expected) {
|
|
168
|
+
stack.pop()
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
makeTokenLiteralText(token)
|
|
172
|
+
changed = true
|
|
173
|
+
}
|
|
174
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
175
|
+
const entry = stack[i]
|
|
176
|
+
const token = tokens[entry.idx]
|
|
177
|
+
if (!token) continue
|
|
178
|
+
makeTokenLiteralText(token)
|
|
179
|
+
changed = true
|
|
180
|
+
}
|
|
181
|
+
return changed
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const getPostprocessMetrics = (state) => {
|
|
185
|
+
if (!state || !state.env) return null
|
|
186
|
+
const metrics = state.env.__strongJaPostprocessMetrics
|
|
187
|
+
if (!metrics || typeof metrics !== 'object') return null
|
|
188
|
+
return metrics
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const bumpPostprocessMetric = (metrics, bucket, key) => {
|
|
192
|
+
if (!metrics || !bucket || !key) return
|
|
193
|
+
let table = metrics[bucket]
|
|
194
|
+
if (!table || typeof table !== 'object') {
|
|
195
|
+
table = Object.create(null)
|
|
196
|
+
metrics[bucket] = table
|
|
197
|
+
}
|
|
198
|
+
table[key] = (table[key] || 0) + 1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const runBrokenRefRepairPass = (children, scanState, metrics = null) => {
|
|
202
|
+
resetBrokenRefScanState(scanState)
|
|
203
|
+
let wrapperPrefixStats = null
|
|
204
|
+
let brokenRefStart = -1
|
|
205
|
+
let brokenRefDepth = 0
|
|
206
|
+
let brokenRefStartTextOffset = 0
|
|
207
|
+
let linkCloseMap = null
|
|
208
|
+
let hasBracketText = false
|
|
209
|
+
let hasEmphasis = false
|
|
210
|
+
let hasLinkClose = false
|
|
211
|
+
|
|
212
|
+
for (let j = 0; j < children.length; j++) {
|
|
213
|
+
const child = children[j]
|
|
214
|
+
if (!child) continue
|
|
215
|
+
|
|
216
|
+
if (child.type === 'text' && child.content) {
|
|
217
|
+
const text = child.content
|
|
218
|
+
const hasOpenBracket = text.indexOf('[') !== -1
|
|
219
|
+
const hasCloseBracket = text.indexOf(']') !== -1
|
|
220
|
+
if (!hasBracketText && (hasOpenBracket || hasCloseBracket)) {
|
|
221
|
+
hasBracketText = true
|
|
222
|
+
}
|
|
223
|
+
if (brokenRefStart === -1) {
|
|
224
|
+
if (hasOpenBracket) {
|
|
225
|
+
const scan = scanBrokenRefState(text, scanState)
|
|
226
|
+
if (scan.brokenEnd) {
|
|
227
|
+
brokenRefStart = j
|
|
228
|
+
brokenRefDepth = scan.depth
|
|
229
|
+
brokenRefStartTextOffset = scan.tailOpen > 0 ? scan.tailOpen : 0
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (hasOpenBracket || hasCloseBracket) {
|
|
234
|
+
brokenRefDepth = updateBracketDepth(text, brokenRefDepth)
|
|
235
|
+
if (brokenRefDepth <= 0) {
|
|
236
|
+
brokenRefStart = -1
|
|
237
|
+
brokenRefDepth = 0
|
|
238
|
+
brokenRefStartTextOffset = 0
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!hasEmphasis && isAsteriskEmphasisToken(child)) {
|
|
244
|
+
hasEmphasis = true
|
|
245
|
+
}
|
|
246
|
+
if (!hasLinkClose && child.type === 'link_close') {
|
|
247
|
+
hasLinkClose = true
|
|
248
|
+
}
|
|
249
|
+
if (brokenRefStart === -1 || child.type !== 'link_open') continue
|
|
250
|
+
if (brokenRefDepth <= 0) {
|
|
251
|
+
brokenRefStart = -1
|
|
252
|
+
brokenRefDepth = 0
|
|
253
|
+
brokenRefStartTextOffset = 0
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
if (linkCloseMap === null) {
|
|
257
|
+
linkCloseMap = buildLinkCloseMap(children, 0, children.length - 1)
|
|
258
|
+
}
|
|
259
|
+
const closeIdx = linkCloseMap.get(j) ?? -1
|
|
260
|
+
if (closeIdx === -1) continue
|
|
261
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'candidate')
|
|
262
|
+
let segmentEnd = expandSegmentEndForWrapperBalance(children, brokenRefStart, closeIdx)
|
|
263
|
+
if (segmentEnd === -1) {
|
|
264
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'wrapper-expand-fallback')
|
|
265
|
+
segmentEnd = closeIdx
|
|
266
|
+
}
|
|
267
|
+
if (!hasTextMarkerCharsInRange(children, brokenRefStart, segmentEnd, brokenRefStartTextOffset)) {
|
|
268
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'skip-no-text-marker')
|
|
269
|
+
brokenRefStart = -1
|
|
270
|
+
brokenRefDepth = 0
|
|
271
|
+
brokenRefStartTextOffset = 0
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
if (wrapperPrefixStats === null) {
|
|
275
|
+
wrapperPrefixStats = buildAsteriskWrapperPrefixStats(children)
|
|
276
|
+
}
|
|
277
|
+
if (!shouldAttemptBrokenRefRewrite(
|
|
278
|
+
children,
|
|
279
|
+
brokenRefStart,
|
|
280
|
+
segmentEnd,
|
|
281
|
+
brokenRefStartTextOffset,
|
|
282
|
+
wrapperPrefixStats
|
|
283
|
+
)) {
|
|
284
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'skip-guard')
|
|
285
|
+
brokenRefStart = -1
|
|
286
|
+
brokenRefDepth = 0
|
|
287
|
+
brokenRefStartTextOffset = 0
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
const fastPathResult = applyBrokenRefTokenOnlyFastPath(
|
|
291
|
+
children,
|
|
292
|
+
brokenRefStart,
|
|
293
|
+
segmentEnd,
|
|
294
|
+
linkCloseMap,
|
|
295
|
+
metrics,
|
|
296
|
+
bumpPostprocessMetric
|
|
297
|
+
)
|
|
298
|
+
if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_ACTIVE_SIGNATURE) {
|
|
299
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'skip-no-active-signature')
|
|
300
|
+
brokenRefStart = -1
|
|
301
|
+
brokenRefDepth = 0
|
|
302
|
+
brokenRefStartTextOffset = 0
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (fastPathResult === BROKEN_REF_FAST_PATH_RESULT_NO_MATCH) {
|
|
306
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'skip-no-fastpath-match')
|
|
307
|
+
brokenRefStart = -1
|
|
308
|
+
brokenRefDepth = 0
|
|
309
|
+
brokenRefStartTextOffset = 0
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
bumpPostprocessMetric(metrics, 'brokenRefFlow', 'repaired')
|
|
313
|
+
return {
|
|
314
|
+
didRepair: true,
|
|
315
|
+
hasBracketText,
|
|
316
|
+
hasEmphasis,
|
|
317
|
+
hasLinkClose
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
didRepair: false,
|
|
323
|
+
hasBracketText,
|
|
324
|
+
hasEmphasis,
|
|
325
|
+
hasLinkClose
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const computeMaxBrokenRefRepairPass = (children, scanState) => {
|
|
330
|
+
resetBrokenRefScanState(scanState)
|
|
331
|
+
let maxRepairPass = 0
|
|
332
|
+
for (let j = 0; j < children.length; j++) {
|
|
333
|
+
const child = children[j]
|
|
334
|
+
if (!child || child.type !== 'text' || !child.content) continue
|
|
335
|
+
if (child.content.indexOf('[') === -1) continue
|
|
336
|
+
if (scanBrokenRefState(child.content, scanState).brokenEnd) {
|
|
337
|
+
maxRepairPass++
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return maxRepairPass
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const runBrokenRefRepairs = (children, maxRepairPass, scanState, metrics = null) => {
|
|
344
|
+
let repairPassCount = 0
|
|
345
|
+
let changed = false
|
|
346
|
+
let hasBracketText = false
|
|
347
|
+
let hasEmphasis = false
|
|
348
|
+
let hasLinkClose = false
|
|
349
|
+
while (repairPassCount < maxRepairPass) {
|
|
350
|
+
const pass = runBrokenRefRepairPass(children, scanState, metrics)
|
|
351
|
+
hasBracketText = pass.hasBracketText
|
|
352
|
+
hasEmphasis = pass.hasEmphasis
|
|
353
|
+
hasLinkClose = pass.hasLinkClose
|
|
354
|
+
if (!pass.didRepair) break
|
|
355
|
+
changed = true
|
|
356
|
+
repairPassCount++
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
changed,
|
|
360
|
+
hasBracketText,
|
|
361
|
+
hasEmphasis,
|
|
362
|
+
hasLinkClose
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const scanTailRepairCandidateAfterLinkClose = (tokens, linkCloseIdx) => {
|
|
367
|
+
if (!tokens || linkCloseIdx < 0 || linkCloseIdx >= tokens.length) return null
|
|
368
|
+
let startIdx = -1
|
|
369
|
+
let foundStrongClose = -1
|
|
370
|
+
let foundStrongOpen = -1
|
|
371
|
+
for (let j = linkCloseIdx + 1; j < tokens.length; j++) {
|
|
372
|
+
const node = tokens[j]
|
|
373
|
+
if (!node) continue
|
|
374
|
+
if (node.type === 'strong_open') {
|
|
375
|
+
foundStrongOpen = j
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
if (node.type === 'strong_close') {
|
|
379
|
+
foundStrongClose = j
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
if (node.type === 'text' && node.content && startIdx === -1) {
|
|
383
|
+
startIdx = j
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (foundStrongClose === -1 || foundStrongOpen !== -1) return null
|
|
387
|
+
if (startIdx === -1) startIdx = foundStrongClose
|
|
388
|
+
return { startIdx, strongCloseIdx: foundStrongClose }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const tryRepairTailCandidate = (tokens, candidate, isJapaneseMode, metrics = null) => {
|
|
392
|
+
if (!tokens || !candidate) return false
|
|
393
|
+
const startIdx = candidate.startIdx
|
|
394
|
+
const strongCloseIdx = candidate.strongCloseIdx
|
|
395
|
+
const endIdx = tokens.length - 1
|
|
396
|
+
if (isJapaneseMode && !hasJapaneseContextInRange(tokens, startIdx, endIdx)) return false
|
|
397
|
+
if (!hasEmphasisSignalInRange(tokens, startIdx, endIdx)) return false
|
|
398
|
+
if (tryFixTailPatternTokenOnly(tokens, startIdx, endIdx)) {
|
|
399
|
+
bumpPostprocessMetric(metrics, 'tailFastPaths', 'tail-pattern')
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
if (tryFixTailDanglingStrongCloseTokenOnly(tokens, startIdx, strongCloseIdx)) {
|
|
403
|
+
bumpPostprocessMetric(metrics, 'tailFastPaths', 'tail-dangling-strong-close')
|
|
404
|
+
return true
|
|
405
|
+
}
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const fixTailAfterLinkStrongClose = (tokens, isJapaneseMode, metrics = null) => {
|
|
410
|
+
let strongDepth = 0
|
|
411
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
412
|
+
const t = tokens[i]
|
|
413
|
+
if (!t) continue
|
|
414
|
+
if (t.type === 'strong_open') strongDepth++
|
|
415
|
+
if (t.type === 'strong_close') {
|
|
416
|
+
if (strongDepth > 0) strongDepth--
|
|
417
|
+
}
|
|
418
|
+
if (t.type !== 'link_close') continue
|
|
419
|
+
if (strongDepth !== 0) continue
|
|
420
|
+
const candidate = scanTailRepairCandidateAfterLinkClose(tokens, i)
|
|
421
|
+
if (!candidate) continue
|
|
422
|
+
if (tryRepairTailCandidate(tokens, candidate, isJapaneseMode, metrics)) return true
|
|
423
|
+
}
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const cloneMap = (map) => {
|
|
428
|
+
if (!map || !Array.isArray(map)) return null
|
|
429
|
+
return [map[0], map[1]]
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const cloneTextToken = (source, content) => {
|
|
433
|
+
const token = new Token('text', '', 0)
|
|
434
|
+
Object.assign(token, source)
|
|
435
|
+
token.content = content
|
|
436
|
+
if (source.meta) token.meta = { ...source.meta }
|
|
437
|
+
return token
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const isSoftSpaceCode = (code) => {
|
|
441
|
+
return code === 0x20 || code === 0x09 || code === 0x3000
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const isAsciiWordCode = (code) => {
|
|
445
|
+
return (code >= 0x30 && code <= 0x39) ||
|
|
446
|
+
(code >= 0x41 && code <= 0x5A) ||
|
|
447
|
+
(code >= 0x61 && code <= 0x7A)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const textEndsAsciiWord = (text) => {
|
|
451
|
+
if (!text || text.length === 0) return false
|
|
452
|
+
return isAsciiWordCode(text.charCodeAt(text.length - 1))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const textStartsAsciiWord = (text) => {
|
|
456
|
+
if (!text || text.length === 0) return false
|
|
457
|
+
return isAsciiWordCode(text.charCodeAt(0))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const isEscapedMarkerAt = (content, index) => {
|
|
461
|
+
let slashCount = 0
|
|
462
|
+
for (let i = index - 1; i >= 0 && content.charCodeAt(i) === 0x5C; i--) {
|
|
463
|
+
slashCount++
|
|
464
|
+
}
|
|
465
|
+
return (slashCount % 2) === 1
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const findLastStandaloneStrongMarker = (content) => {
|
|
469
|
+
if (!content || content.length < 2) return -1
|
|
470
|
+
let pos = content.lastIndexOf('**')
|
|
471
|
+
while (pos !== -1) {
|
|
472
|
+
const prev = pos > 0 ? content.charCodeAt(pos - 1) : 0
|
|
473
|
+
const next = pos + 2 < content.length ? content.charCodeAt(pos + 2) : 0
|
|
474
|
+
if (prev !== 0x2A &&
|
|
475
|
+
next !== 0x2A &&
|
|
476
|
+
!isEscapedMarkerAt(content, pos)) {
|
|
477
|
+
return pos
|
|
478
|
+
}
|
|
479
|
+
pos = content.lastIndexOf('**', pos - 1)
|
|
480
|
+
}
|
|
481
|
+
return -1
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const hasLeadingStandaloneStrongMarker = (content) => {
|
|
485
|
+
if (!content || content.length < 2) return false
|
|
486
|
+
if (content.charCodeAt(0) !== 0x2A || content.charCodeAt(1) !== 0x2A) return false
|
|
487
|
+
if (content.length > 2 && content.charCodeAt(2) === 0x2A) return false
|
|
488
|
+
return true
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const tryPromoteStrongAroundInlineLink = (tokens, strictAsciiStrongGuard = false) => {
|
|
492
|
+
if (!tokens || tokens.length < 3) return false
|
|
493
|
+
let changed = false
|
|
494
|
+
let linkCloseMap = null
|
|
495
|
+
for (let i = 1; i < tokens.length - 1; i++) {
|
|
496
|
+
const linkOpen = tokens[i]
|
|
497
|
+
if (!linkOpen || linkOpen.type !== 'link_open') continue
|
|
498
|
+
if (linkCloseMap === null) {
|
|
499
|
+
linkCloseMap = buildLinkCloseMap(tokens, 0, tokens.length - 1)
|
|
500
|
+
}
|
|
501
|
+
const closeIdx = linkCloseMap.get(i) ?? -1
|
|
502
|
+
if (closeIdx === -1 || closeIdx + 1 >= tokens.length) continue
|
|
503
|
+
|
|
504
|
+
const left = tokens[i - 1]
|
|
505
|
+
const right = tokens[closeIdx + 1]
|
|
506
|
+
if (!left || left.type !== 'text' || !left.content) {
|
|
507
|
+
i = closeIdx
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
if (!right || right.type !== 'text' || !right.content) {
|
|
511
|
+
i = closeIdx
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
if (!hasLeadingStandaloneStrongMarker(right.content)) {
|
|
515
|
+
i = closeIdx
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
518
|
+
const markerPos = findLastStandaloneStrongMarker(left.content)
|
|
519
|
+
if (markerPos === -1) {
|
|
520
|
+
i = closeIdx
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const prefix = left.content.slice(0, markerPos)
|
|
525
|
+
const leftInner = left.content.slice(markerPos + 2)
|
|
526
|
+
if (leftInner && isSoftSpaceCode(leftInner.charCodeAt(0))) {
|
|
527
|
+
i = closeIdx
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
const rightTail = right.content.slice(2)
|
|
531
|
+
if (strictAsciiStrongGuard &&
|
|
532
|
+
(textEndsAsciiWord(prefix) || textStartsAsciiWord(rightTail))) {
|
|
533
|
+
i = closeIdx
|
|
534
|
+
continue
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const replacement = []
|
|
538
|
+
if (prefix) replacement.push(cloneTextToken(left, prefix))
|
|
539
|
+
|
|
540
|
+
const strongOpen = new Token('strong_open', 'strong', 1)
|
|
541
|
+
strongOpen.markup = '**'
|
|
542
|
+
strongOpen.map = cloneMap(left.map) || cloneMap(linkOpen.map) || cloneMap(right.map) || null
|
|
543
|
+
replacement.push(strongOpen)
|
|
544
|
+
|
|
545
|
+
if (leftInner) replacement.push(cloneTextToken(left, leftInner))
|
|
546
|
+
for (let j = i; j <= closeIdx; j++) replacement.push(tokens[j])
|
|
547
|
+
|
|
548
|
+
const strongClose = new Token('strong_close', 'strong', -1)
|
|
549
|
+
strongClose.markup = '**'
|
|
550
|
+
strongClose.map = cloneMap(right.map) || cloneMap(linkOpen.map) || cloneMap(left.map) || null
|
|
551
|
+
replacement.push(strongClose)
|
|
552
|
+
|
|
553
|
+
if (rightTail) replacement.push(cloneTextToken(right, rightTail))
|
|
554
|
+
|
|
555
|
+
tokens.splice(i - 1, closeIdx - i + 3, ...replacement)
|
|
556
|
+
changed = true
|
|
557
|
+
// Token indices changed; invalidate map for the next candidate.
|
|
558
|
+
linkCloseMap = null
|
|
559
|
+
i = i - 1 + replacement.length - 1
|
|
560
|
+
}
|
|
561
|
+
return changed
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const tryPromoteStrongAroundInlineCode = (
|
|
565
|
+
tokens,
|
|
566
|
+
strictAsciiCodeGuard = false,
|
|
567
|
+
strictAsciiStrongGuard = false
|
|
568
|
+
) => {
|
|
569
|
+
if (!tokens || tokens.length < 3) return false
|
|
570
|
+
let changed = false
|
|
571
|
+
for (let i = 0; i <= tokens.length - 3; i++) {
|
|
572
|
+
const left = tokens[i]
|
|
573
|
+
const code = tokens[i + 1]
|
|
574
|
+
const right = tokens[i + 2]
|
|
575
|
+
if (!left || !code || !right) continue
|
|
576
|
+
if (left.type !== 'text' || !left.content) continue
|
|
577
|
+
if (code.type !== 'code_inline') continue
|
|
578
|
+
if (right.type !== 'text' || !right.content) continue
|
|
579
|
+
if (!hasLeadingStandaloneStrongMarker(right.content)) continue
|
|
580
|
+
const markerPos = findLastStandaloneStrongMarker(left.content)
|
|
581
|
+
if (markerPos === -1) continue
|
|
582
|
+
|
|
583
|
+
const prefix = left.content.slice(0, markerPos)
|
|
584
|
+
const leftInner = left.content.slice(markerPos + 2)
|
|
585
|
+
const rightTail = right.content.slice(2)
|
|
586
|
+
if (strictAsciiStrongGuard &&
|
|
587
|
+
(textEndsAsciiWord(prefix) || textStartsAsciiWord(rightTail))) {
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
590
|
+
if (strictAsciiCodeGuard &&
|
|
591
|
+
leftInner &&
|
|
592
|
+
isSoftSpaceCode(leftInner.charCodeAt(0)) &&
|
|
593
|
+
code.content &&
|
|
594
|
+
isAsciiWordCode(code.content.charCodeAt(0))) {
|
|
595
|
+
continue
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const replacement = []
|
|
599
|
+
if (prefix) replacement.push(cloneTextToken(left, prefix))
|
|
600
|
+
|
|
601
|
+
const strongOpen = new Token('strong_open', 'strong', 1)
|
|
602
|
+
strongOpen.markup = '**'
|
|
603
|
+
strongOpen.map = cloneMap(left.map) || cloneMap(code.map) || cloneMap(right.map) || null
|
|
604
|
+
replacement.push(strongOpen)
|
|
605
|
+
|
|
606
|
+
if (leftInner) replacement.push(cloneTextToken(left, leftInner))
|
|
607
|
+
replacement.push(code)
|
|
608
|
+
|
|
609
|
+
const strongClose = new Token('strong_close', 'strong', -1)
|
|
610
|
+
strongClose.markup = '**'
|
|
611
|
+
strongClose.map = cloneMap(right.map) || cloneMap(code.map) || cloneMap(left.map) || null
|
|
612
|
+
replacement.push(strongClose)
|
|
613
|
+
|
|
614
|
+
if (rightTail) replacement.push(cloneTextToken(right, rightTail))
|
|
615
|
+
|
|
616
|
+
tokens.splice(i, 3, ...replacement)
|
|
617
|
+
changed = true
|
|
618
|
+
i += replacement.length - 1
|
|
619
|
+
}
|
|
620
|
+
return changed
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const processInlinePostprocessToken = (
|
|
624
|
+
token,
|
|
625
|
+
inlineContent,
|
|
626
|
+
state,
|
|
627
|
+
isJapaneseMode,
|
|
628
|
+
strictAsciiCodeGuard,
|
|
629
|
+
strictAsciiStrongGuard,
|
|
630
|
+
referenceCount,
|
|
631
|
+
metrics = null
|
|
632
|
+
) => {
|
|
633
|
+
if (!token || token.type !== 'inline' || !token.children || token.children.length === 0) return
|
|
634
|
+
const children = token.children
|
|
635
|
+
const hasBracketTextInContent = inlineContent.indexOf('[') !== -1 || inlineContent.indexOf(']') !== -1
|
|
636
|
+
const preScan = scanInlinePostprocessSignals(children, hasBracketTextInContent)
|
|
637
|
+
let hasBracketText = preScan.hasBracketText
|
|
638
|
+
let hasEmphasis = preScan.hasEmphasis
|
|
639
|
+
const hasLinkOpen = preScan.hasLinkOpen
|
|
640
|
+
let hasLinkClose = preScan.hasLinkClose
|
|
641
|
+
const hasCodeInline = preScan.hasCodeInline
|
|
642
|
+
if (!hasEmphasis && !hasBracketText && !hasLinkOpen && !hasLinkClose && !hasCodeInline) {
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
if (isJapaneseMode &&
|
|
646
|
+
!hasJapaneseContextInRange(children, 0, children.length - 1)) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
let changed = false
|
|
650
|
+
if (!hasEmphasis) {
|
|
651
|
+
if (hasLinkOpen &&
|
|
652
|
+
hasLinkClose &&
|
|
653
|
+
tryPromoteStrongAroundInlineLink(children, strictAsciiStrongGuard)) {
|
|
654
|
+
hasEmphasis = true
|
|
655
|
+
changed = true
|
|
656
|
+
} else if (!hasBracketText) {
|
|
657
|
+
if (!hasLinkOpen &&
|
|
658
|
+
!hasLinkClose &&
|
|
659
|
+
tryPromoteStrongAroundInlineCode(children, strictAsciiCodeGuard, strictAsciiStrongGuard)) {
|
|
660
|
+
hasEmphasis = true
|
|
661
|
+
changed = true
|
|
662
|
+
} else {
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
let shouldTryBrokenRefRepair = hasLinkOpen && hasLinkClose && hasBracketText && referenceCount > 0
|
|
668
|
+
if (shouldTryBrokenRefRepair && inlineContent.indexOf('***') !== -1) {
|
|
669
|
+
shouldTryBrokenRefRepair = false
|
|
670
|
+
}
|
|
671
|
+
if (shouldTryBrokenRefRepair) {
|
|
672
|
+
const scanState = { depth: 0, brokenEnd: false, tailOpen: -1 }
|
|
673
|
+
const maxRepairPass = computeMaxBrokenRefRepairPass(children, scanState)
|
|
674
|
+
if (maxRepairPass > 0) {
|
|
675
|
+
const repairs = runBrokenRefRepairs(children, maxRepairPass, scanState, metrics)
|
|
676
|
+
hasBracketText = repairs.hasBracketText
|
|
677
|
+
hasEmphasis = repairs.hasEmphasis
|
|
678
|
+
hasLinkClose = repairs.hasLinkClose
|
|
679
|
+
if (repairs.changed) changed = true
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (hasEmphasis) {
|
|
683
|
+
if (fixEmOuterStrongSequence(children)) changed = true
|
|
684
|
+
if (hasLinkClose && fixTailAfterLinkStrongClose(children, isJapaneseMode, metrics)) changed = true
|
|
685
|
+
if (hasLinkClose && fixLeadingAsteriskEm(children)) changed = true
|
|
686
|
+
if (fixTrailingStrong(children)) changed = true
|
|
687
|
+
if (sanitizeEmStrongBalance(children)) changed = true
|
|
688
|
+
}
|
|
689
|
+
if (changed) rebuildInlineLevels(children)
|
|
690
|
+
if (!hasBracketText) return
|
|
691
|
+
if (referenceCount > 0) convertCollapsedReferenceLinks(children, state)
|
|
692
|
+
if (referenceCount === 0 && !hasLinkClose) return
|
|
693
|
+
mergeBrokenMarksAroundLinks(children)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const registerTokenPostprocess = (md, baseOpt) => {
|
|
697
|
+
if (md.__strongJaTokenPostprocessRegistered) return
|
|
698
|
+
md.__strongJaTokenPostprocessRegistered = true
|
|
699
|
+
md.core.ruler.after('inline', 'strong_ja_token_postprocess', (state) => {
|
|
700
|
+
if (!state || !state.tokens) return
|
|
701
|
+
const opt = getRuntimeOpt(state, baseOpt)
|
|
702
|
+
const modeFlags = opt.__strongJaModeFlags
|
|
703
|
+
const isJapaneseMode = (modeFlags & MODE_FLAG_JAPANESE_ANY) !== 0
|
|
704
|
+
const strictAsciiCodeGuard = (modeFlags & MODE_FLAG_JAPANESE_PLUS) !== 0
|
|
705
|
+
const strictAsciiStrongGuard = (modeFlags & MODE_FLAG_AGGRESSIVE) === 0
|
|
706
|
+
if (modeFlags & MODE_FLAG_COMPATIBLE) return
|
|
707
|
+
if (opt.postprocess === false) return
|
|
708
|
+
const references = state.env && state.env.references ? state.env.references : null
|
|
709
|
+
if (state.__strongJaReferenceCount === undefined) {
|
|
710
|
+
state.__strongJaReferenceCount = references ? Object.keys(references).length : 0
|
|
711
|
+
}
|
|
712
|
+
const referenceCount = state.__strongJaReferenceCount
|
|
713
|
+
const metrics = getPostprocessMetrics(state)
|
|
714
|
+
for (let i = 0; i < state.tokens.length; i++) {
|
|
715
|
+
const token = state.tokens[i]
|
|
716
|
+
if (!token || token.type !== 'inline' || !token.children || token.children.length === 0) continue
|
|
717
|
+
const inlineContent = typeof token.content === 'string' ? token.content : ''
|
|
718
|
+
if (!hasMarkerChars(inlineContent)) continue
|
|
719
|
+
processInlinePostprocessToken(
|
|
720
|
+
token,
|
|
721
|
+
inlineContent,
|
|
722
|
+
state,
|
|
723
|
+
isJapaneseMode,
|
|
724
|
+
strictAsciiCodeGuard,
|
|
725
|
+
strictAsciiStrongGuard,
|
|
726
|
+
referenceCount,
|
|
727
|
+
metrics
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export { registerTokenPostprocess }
|