@peaceroad/markdown-it-renderer-fence 0.4.1 → 0.6.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 +491 -114
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/index.js +33 -503
- package/package.json +28 -4
- package/src/custom-highlight/option-validator.js +152 -0
- package/src/custom-highlight/payload-utils.js +43 -0
- package/src/custom-highlight/runtime-utils.js +83 -0
- package/src/custom-highlight/shiki-keyword-rules.js +407 -0
- package/src/custom-highlight/shiki-keyword.js +417 -0
- package/src/entry/custom-highlight-runtime.js +11 -0
- package/src/entry/custom-highlight.js +25 -0
- package/src/entry/markup-highlight.js +27 -0
- package/src/fence/render-api-constants.js +34 -0
- package/src/fence/render-api-provider.js +810 -0
- package/src/fence/render-api-renderer.js +145 -0
- package/src/fence/render-api-runtime.js +712 -0
- package/src/fence/render-api.js +107 -0
- package/src/fence/render-markup.js +177 -0
- package/src/fence/render-shared.js +590 -0
- package/src/utils/attr-utils.js +120 -0
- package/src/utils/pre-code-wrapper-parser.js +114 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendStyleValue,
|
|
3
|
+
createAttrOrderIndexGetter,
|
|
4
|
+
getInfoAttr,
|
|
5
|
+
getLangFromClassAttr,
|
|
6
|
+
} from '../utils/attr-utils.js'
|
|
7
|
+
|
|
8
|
+
const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
|
|
9
|
+
const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
|
|
10
|
+
const preLineTag = '<span class="pre-line">'
|
|
11
|
+
const preLineNoNumberClass = 'pre-line-no-number'
|
|
12
|
+
const emphOpenTag = '<span class="pre-lines-emphasis">'
|
|
13
|
+
const commentLineClass = 'pre-line-comment'
|
|
14
|
+
const closeTag = '</span>'
|
|
15
|
+
const closeTagLen = closeTag.length
|
|
16
|
+
const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
|
|
17
|
+
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
18
|
+
const nonNegativeIntReg = /^\d+$/
|
|
19
|
+
const positiveIntReg = /^[1-9]\d*$/
|
|
20
|
+
|
|
21
|
+
const getLineVisualLengthIgnoringTags = (line, threshold) => {
|
|
22
|
+
let len = 0
|
|
23
|
+
let inTag = false
|
|
24
|
+
for (let i = 0; i < line.length; i++) {
|
|
25
|
+
const code = line.charCodeAt(i)
|
|
26
|
+
if (inTag) {
|
|
27
|
+
if (code === 62) inTag = false // >
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (code === 60) { // <
|
|
31
|
+
inTag = true
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
len += code > 255 ? 2 : 1
|
|
35
|
+
if (len >= threshold) return len
|
|
36
|
+
}
|
|
37
|
+
return len
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getNowMs = () => {
|
|
41
|
+
if (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') return performance.now()
|
|
42
|
+
return Date.now()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const roundTimingMs = (value) => {
|
|
46
|
+
return Math.round(value * 1000) / 1000
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const addTimingMs = (timings, key, value) => {
|
|
50
|
+
if (!timings || !key || !Number.isFinite(value) || value <= 0) return
|
|
51
|
+
timings[key] = (timings[key] || 0) + value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const finalizeFenceTimings = (timings, startedAtMs) => {
|
|
55
|
+
if (!timings || !Number.isFinite(startedAtMs)) return null
|
|
56
|
+
const totalMs = getNowMs() - startedAtMs
|
|
57
|
+
const out = {}
|
|
58
|
+
if (timings.attrNormalizeMs) out.attrNormalizeMs = roundTimingMs(timings.attrNormalizeMs)
|
|
59
|
+
if (timings.highlightMs) out.highlightMs = roundTimingMs(timings.highlightMs)
|
|
60
|
+
if (timings.providerMs) out.providerMs = roundTimingMs(timings.providerMs)
|
|
61
|
+
if (timings.lineSplitMs) out.lineSplitMs = roundTimingMs(timings.lineSplitMs)
|
|
62
|
+
out.totalMs = roundTimingMs(totalMs > 0 ? totalMs : 0)
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const applyLineEndAlias = (opt, sourceOption) => {
|
|
67
|
+
if (sourceOption && sourceOption.lineEndSpanThreshold == null && sourceOption.setLineEndSpan != null) {
|
|
68
|
+
opt.lineEndSpanThreshold = sourceOption.setLineEndSpan
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const createCommonFenceOptionDefaults = (md) => {
|
|
73
|
+
return {
|
|
74
|
+
attrsOrder: ['class', 'id', 'data-*', 'style'],
|
|
75
|
+
setHighlight: true,
|
|
76
|
+
setLineNumber: true,
|
|
77
|
+
setEmphasizeLines: true,
|
|
78
|
+
lineEndSpanThreshold: 0,
|
|
79
|
+
lineEndSpanClass: 'pre-lineend-spacer',
|
|
80
|
+
setPreWrapStyle: true,
|
|
81
|
+
useHighlightPre: false,
|
|
82
|
+
onFenceDecision: null,
|
|
83
|
+
onFenceDecisionTiming: false,
|
|
84
|
+
sampLang: 'shell,console',
|
|
85
|
+
langPrefix: md.options.langPrefix || 'language-',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const finalizeCommonFenceOption = (opt) => {
|
|
90
|
+
opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
|
|
91
|
+
opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
|
|
92
|
+
return opt
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const emitFenceDecision = (opt, data) => {
|
|
96
|
+
if (!opt || typeof opt.onFenceDecision !== 'function') return
|
|
97
|
+
try {
|
|
98
|
+
opt.onFenceDecision(data)
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parseStartNumber = (val) => {
|
|
103
|
+
const str = String(val ?? '')
|
|
104
|
+
if (!nonNegativeIntReg.test(str)) return null
|
|
105
|
+
const num = Number(str)
|
|
106
|
+
if (!Number.isSafeInteger(num) || num < 0) return null
|
|
107
|
+
return num
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parsePositiveLineIndex = (val) => {
|
|
111
|
+
const str = String(val ?? '').trim()
|
|
112
|
+
if (!positiveIntReg.test(str)) return null
|
|
113
|
+
const num = Number(str)
|
|
114
|
+
if (!Number.isSafeInteger(num) || num <= 0) return null
|
|
115
|
+
return num
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const getLogicalLineCount = (text) => {
|
|
119
|
+
const str = String(text ?? '')
|
|
120
|
+
if (!str) return 0
|
|
121
|
+
let count = 1
|
|
122
|
+
for (let i = 0; i < str.length; i++) {
|
|
123
|
+
const ch = str.charCodeAt(i)
|
|
124
|
+
if (ch !== 10 && ch !== 13) continue
|
|
125
|
+
if (ch === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
|
|
126
|
+
count++
|
|
127
|
+
}
|
|
128
|
+
const last = str.charCodeAt(str.length - 1)
|
|
129
|
+
if (last === 10 || last === 13) count--
|
|
130
|
+
return count
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parseLineRangeList = (attrVal, parseValue) => {
|
|
134
|
+
const str = String(attrVal ?? '')
|
|
135
|
+
if (!str) return []
|
|
136
|
+
const result = []
|
|
137
|
+
for (const partRaw of str.split(',')) {
|
|
138
|
+
const part = partRaw.trim()
|
|
139
|
+
if (!part) continue
|
|
140
|
+
const hyphen = part.indexOf('-')
|
|
141
|
+
if (hyphen === -1) {
|
|
142
|
+
const n = parseValue(part)
|
|
143
|
+
if (n != null) result.push([n, n])
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
const start = part.slice(0, hyphen).trim()
|
|
147
|
+
const end = part.slice(hyphen + 1).trim()
|
|
148
|
+
const s = start ? parseValue(start) : null
|
|
149
|
+
const e = end ? parseValue(end) : null
|
|
150
|
+
if (s == null && e == null) continue
|
|
151
|
+
if (start && s == null) continue
|
|
152
|
+
if (end && e == null) continue
|
|
153
|
+
result.push([s, e])
|
|
154
|
+
}
|
|
155
|
+
return result
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const parsePositiveLineNumber = (val) => {
|
|
159
|
+
const num = Number(val)
|
|
160
|
+
if (!Number.isFinite(num) || num <= 0) return null
|
|
161
|
+
return num
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const getEmphasizeLines = (attrVal) => {
|
|
165
|
+
return parseLineRangeList(attrVal, parsePositiveLineNumber)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const getLineNumberSkipRanges = (attrVal) => {
|
|
169
|
+
return parseLineRangeList(attrVal, parsePositiveLineIndex)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const normalizeEmphasisRanges = (emphasizeLines, maxLine) => {
|
|
173
|
+
const normalized = []
|
|
174
|
+
if (!emphasizeLines || !emphasizeLines.length || maxLine <= 0) return normalized
|
|
175
|
+
for (const range of emphasizeLines) {
|
|
176
|
+
let s = range[0]
|
|
177
|
+
let e = range[1]
|
|
178
|
+
if (s == null) s = 1
|
|
179
|
+
if (e == null) e = maxLine
|
|
180
|
+
if (!Number.isFinite(s) || !Number.isFinite(e) || s <= 0 || e <= 0) continue
|
|
181
|
+
if (s > e) {
|
|
182
|
+
const tmp = s
|
|
183
|
+
s = e
|
|
184
|
+
e = tmp
|
|
185
|
+
}
|
|
186
|
+
if (s > maxLine || e < 1) continue
|
|
187
|
+
if (e > maxLine) e = maxLine
|
|
188
|
+
normalized.push([s, e])
|
|
189
|
+
}
|
|
190
|
+
return normalized
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const getLineNumberResetEntries = (attrVal) => {
|
|
194
|
+
const str = String(attrVal ?? '')
|
|
195
|
+
if (!str) return []
|
|
196
|
+
const result = []
|
|
197
|
+
for (const partRaw of str.split(',')) {
|
|
198
|
+
const part = partRaw.trim()
|
|
199
|
+
if (!part) continue
|
|
200
|
+
const colon = part.indexOf(':')
|
|
201
|
+
if (colon === -1) continue
|
|
202
|
+
const lineNumber = parsePositiveLineIndex(part.slice(0, colon).trim())
|
|
203
|
+
const resetNumber = parseStartNumber(part.slice(colon + 1).trim())
|
|
204
|
+
if (lineNumber == null || resetNumber == null) continue
|
|
205
|
+
result.push([lineNumber, resetNumber])
|
|
206
|
+
}
|
|
207
|
+
return result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const createSparseLineFlagMap = (ranges, maxLine) => {
|
|
211
|
+
const normalized = normalizeEmphasisRanges(ranges, maxLine)
|
|
212
|
+
if (!normalized.length) return null
|
|
213
|
+
const flags = []
|
|
214
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
215
|
+
const range = normalized[i]
|
|
216
|
+
for (let line = range[0]; line <= range[1]; line++) flags[line - 1] = true
|
|
217
|
+
}
|
|
218
|
+
return flags
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const createSparseLineResetMap = (entries, maxLine) => {
|
|
222
|
+
if (!entries || !entries.length || maxLine <= 0) return null
|
|
223
|
+
const resets = []
|
|
224
|
+
let hasValue = false
|
|
225
|
+
for (let i = 0; i < entries.length; i++) {
|
|
226
|
+
const line = entries[i][0]
|
|
227
|
+
const value = entries[i][1]
|
|
228
|
+
if (!Number.isSafeInteger(line) || line <= 0 || line > maxLine) continue
|
|
229
|
+
resets[line - 1] = value
|
|
230
|
+
hasValue = true
|
|
231
|
+
}
|
|
232
|
+
return hasValue ? resets : null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const buildAdvancedLineNumberPlan = (skipRanges, resetEntries, sourceLineCount, renderedLineCount) => {
|
|
236
|
+
const hasSkip = !!(skipRanges && skipRanges.length)
|
|
237
|
+
const hasReset = !!(resetEntries && resetEntries.length)
|
|
238
|
+
if (!hasSkip && !hasReset) return null
|
|
239
|
+
if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
|
|
240
|
+
if (sourceLineCount !== renderedLineCount) return null
|
|
241
|
+
const hidden = hasSkip ? createSparseLineFlagMap(skipRanges, sourceLineCount) : null
|
|
242
|
+
const resets = hasReset ? createSparseLineResetMap(resetEntries, sourceLineCount) : null
|
|
243
|
+
if (!hidden && !resets) return null
|
|
244
|
+
return { hidden, resets }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const resolveAdvancedLineNumberPlan = (lineNumberSkipValue, lineNumberResetValue, sourceLineCount, renderedLineCount) => {
|
|
248
|
+
const hasSkipValue = lineNumberSkipValue !== undefined
|
|
249
|
+
const hasResetValue = lineNumberResetValue !== undefined
|
|
250
|
+
if (!hasSkipValue && !hasResetValue) return null
|
|
251
|
+
if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
|
|
252
|
+
if (sourceLineCount !== renderedLineCount) return null
|
|
253
|
+
const skipRanges = hasSkipValue ? getLineNumberSkipRanges(lineNumberSkipValue) : null
|
|
254
|
+
const resetEntries = hasResetValue ? getLineNumberResetEntries(lineNumberResetValue) : null
|
|
255
|
+
return buildAdvancedLineNumberPlan(skipRanges, resetEntries, sourceLineCount, renderedLineCount)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getPreLineOpenTag = (lineNumberPlan, lineIndex) => {
|
|
259
|
+
if (!lineNumberPlan) return preLineTag
|
|
260
|
+
const hidden = !!(lineNumberPlan.hidden && lineNumberPlan.hidden[lineIndex])
|
|
261
|
+
const resetNumber = lineNumberPlan.resets ? lineNumberPlan.resets[lineIndex] : undefined
|
|
262
|
+
if (!hidden && resetNumber == null) return preLineTag
|
|
263
|
+
let tag = '<span class="pre-line'
|
|
264
|
+
if (hidden) tag += ' ' + preLineNoNumberClass
|
|
265
|
+
tag += '"'
|
|
266
|
+
if (resetNumber != null) tag += ` style="counter-set:pre-line-number ${resetNumber};"`
|
|
267
|
+
return tag + '>'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan) => {
|
|
271
|
+
const lines = content.split(br)
|
|
272
|
+
const max = lines.length
|
|
273
|
+
const hasDynamicLineNumberPlan = !!lineNumberPlan
|
|
274
|
+
let emIdx = 0
|
|
275
|
+
let emStart = -1
|
|
276
|
+
let emEnd = -1
|
|
277
|
+
if (needEmphasis && emphasizeLines && emphasizeLines.length) {
|
|
278
|
+
emStart = emphasizeLines[0][0]
|
|
279
|
+
emEnd = emphasizeLines[0][1]
|
|
280
|
+
} else {
|
|
281
|
+
needEmphasis = false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
|
|
285
|
+
|
|
286
|
+
for (let n = 0; n < max; n++) {
|
|
287
|
+
let line = lines[n]
|
|
288
|
+
const notLastLine = n < max - 1
|
|
289
|
+
const doComment = commentLines && commentLines[n]
|
|
290
|
+
let hasLt = false
|
|
291
|
+
let hasLtChecked = false
|
|
292
|
+
|
|
293
|
+
if (needEndSpan && threshold > 0) {
|
|
294
|
+
if (!hasLtChecked) {
|
|
295
|
+
hasLt = line.indexOf('<') !== -1
|
|
296
|
+
hasLtChecked = true
|
|
297
|
+
}
|
|
298
|
+
let lineLen = 0
|
|
299
|
+
if (!hasLt) {
|
|
300
|
+
lineLen = line.length
|
|
301
|
+
} else {
|
|
302
|
+
lineLen = getLineVisualLengthIgnoringTags(line, threshold)
|
|
303
|
+
}
|
|
304
|
+
if (lineLen >= threshold) {
|
|
305
|
+
if (line.endsWith(closeTag)) {
|
|
306
|
+
line = line.slice(0, -closeTagLen) + endSpanTag + closeTag
|
|
307
|
+
} else {
|
|
308
|
+
line = line + endSpanTag
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (needLineNumber && notLastLine) {
|
|
314
|
+
if (!hasLtChecked) {
|
|
315
|
+
hasLt = line.indexOf('<') !== -1
|
|
316
|
+
hasLtChecked = true
|
|
317
|
+
}
|
|
318
|
+
if (hasLt && line.indexOf('>') !== -1) {
|
|
319
|
+
const tagStack = []
|
|
320
|
+
tagReg.lastIndex = 0
|
|
321
|
+
let match
|
|
322
|
+
while ((match = tagReg.exec(line)) !== null) {
|
|
323
|
+
const fullMatch = match[0]
|
|
324
|
+
const tagNameLower = match[1].toLowerCase()
|
|
325
|
+
if (fullMatch.startsWith('</')) {
|
|
326
|
+
if (tagStack[tagStack.length - 1] === tagNameLower) tagStack.pop()
|
|
327
|
+
} else if (!fullMatch.endsWith('/>') && !voidTags.has(tagNameLower)) {
|
|
328
|
+
tagStack.push(tagNameLower)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
332
|
+
const tagName = tagStack[i]
|
|
333
|
+
line += `</${tagName}>`
|
|
334
|
+
lines[n + 1] = `<${tagName}>` + lines[n + 1]
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (doComment) {
|
|
340
|
+
line = `<span class="${commentClass}">` + line + closeTag
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (needLineNumber && notLastLine) {
|
|
344
|
+
line = (hasDynamicLineNumberPlan ? getPreLineOpenTag(lineNumberPlan, n) : preLineTag) + line + closeTag
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
|
|
348
|
+
line = emphOpenTag + line
|
|
349
|
+
}
|
|
350
|
+
if (needEmphasis && emIdx < emphasizeLines.length && emEnd === n) {
|
|
351
|
+
line = closeTag + line
|
|
352
|
+
emIdx++
|
|
353
|
+
const nextEmphasis = emphasizeLines[emIdx] || []
|
|
354
|
+
emStart = nextEmphasis[0]
|
|
355
|
+
emEnd = nextEmphasis[1]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
lines[n] = line
|
|
359
|
+
}
|
|
360
|
+
return lines.join(br)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const orderTokenAttrs = (token, opt) => {
|
|
364
|
+
const attrs = token.attrs
|
|
365
|
+
if (!attrs || attrs.length < 2) return
|
|
366
|
+
const rankCache = new Map()
|
|
367
|
+
const getRank = (name) => {
|
|
368
|
+
let rank = rankCache.get(name)
|
|
369
|
+
if (rank === undefined) {
|
|
370
|
+
rank = opt._attrOrderIndex(name)
|
|
371
|
+
rankCache.set(name, rank)
|
|
372
|
+
}
|
|
373
|
+
return rank
|
|
374
|
+
}
|
|
375
|
+
attrs.sort((a, b) => getRank(a[0]) - getRank(b[0]))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
379
|
+
const token = tokens[idx]
|
|
380
|
+
const match = token.info.trim().match(infoReg)
|
|
381
|
+
let lang = match ? match[1] : ''
|
|
382
|
+
const timingEnabled = !!(opt.onFenceDecisionTiming && typeof opt.onFenceDecision === 'function')
|
|
383
|
+
const fenceStartedAt = timingEnabled ? getNowMs() : 0
|
|
384
|
+
const timings = timingEnabled ? {} : null
|
|
385
|
+
|
|
386
|
+
if (match && match[2]) {
|
|
387
|
+
const infoAttrs = getInfoAttr(match[2])
|
|
388
|
+
for (let i = 0; i < infoAttrs.length; i++) {
|
|
389
|
+
const attr = infoAttrs[i]
|
|
390
|
+
token.attrJoin(attr[0], attr[1])
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (lang && lang !== 'samp') {
|
|
394
|
+
const langClass = opt.langPrefix + lang
|
|
395
|
+
const existingClass = token.attrGet('class')
|
|
396
|
+
token.attrSet('class', existingClass ? langClass + ' ' + existingClass : langClass)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let startNumber = -1
|
|
400
|
+
let emphasizeLines = []
|
|
401
|
+
let lineNumberSkipValue
|
|
402
|
+
let lineNumberResetValue
|
|
403
|
+
let wrapEnabled = false
|
|
404
|
+
let preWrapValue
|
|
405
|
+
let commentMarkValue
|
|
406
|
+
const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
|
|
407
|
+
|
|
408
|
+
if (token.attrs) {
|
|
409
|
+
const newAttrs = []
|
|
410
|
+
let dataPreStartIndex = -1
|
|
411
|
+
let dataPreEmphasisIndex = -1
|
|
412
|
+
let styleIndex = -1
|
|
413
|
+
let dataPreCommentIndex = -1
|
|
414
|
+
let dataPreLineNumberSkipIndex = -1
|
|
415
|
+
let dataPreLineNumberResetIndex = -1
|
|
416
|
+
let startValue
|
|
417
|
+
let emphasisValue
|
|
418
|
+
let styleValue
|
|
419
|
+
let sawCommentMark = false
|
|
420
|
+
let sawStartAttr = false
|
|
421
|
+
let sawEmphasisAttr = false
|
|
422
|
+
let sawLineNumberSkipAttr = false
|
|
423
|
+
let sawLineNumberResetAttr = false
|
|
424
|
+
const appendOrder = []
|
|
425
|
+
|
|
426
|
+
for (const attr of token.attrs) {
|
|
427
|
+
const name = attr[0]
|
|
428
|
+
const val = attr[1]
|
|
429
|
+
|
|
430
|
+
switch (name) {
|
|
431
|
+
case 'class': {
|
|
432
|
+
const classLang = getLangFromClassAttr(val, opt.langPrefix)
|
|
433
|
+
if (classLang) lang = classLang
|
|
434
|
+
newAttrs.push(attr)
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
case 'style':
|
|
438
|
+
styleIndex = newAttrs.length
|
|
439
|
+
styleValue = val
|
|
440
|
+
newAttrs.push(attr)
|
|
441
|
+
break
|
|
442
|
+
case 'data-pre-start':
|
|
443
|
+
startNumber = parseStartNumber(val) ?? -1
|
|
444
|
+
startValue = val
|
|
445
|
+
dataPreStartIndex = newAttrs.length
|
|
446
|
+
newAttrs.push(attr)
|
|
447
|
+
break
|
|
448
|
+
case 'line-number-start':
|
|
449
|
+
case 'start':
|
|
450
|
+
case 'pre-start':
|
|
451
|
+
startNumber = parseStartNumber(val) ?? -1
|
|
452
|
+
startValue = val
|
|
453
|
+
if (!sawStartAttr) {
|
|
454
|
+
appendOrder.push('start')
|
|
455
|
+
sawStartAttr = true
|
|
456
|
+
}
|
|
457
|
+
break
|
|
458
|
+
case 'data-pre-emphasis':
|
|
459
|
+
dataPreEmphasisIndex = newAttrs.length
|
|
460
|
+
newAttrs.push(attr)
|
|
461
|
+
break
|
|
462
|
+
case 'data-pre-comment-mark':
|
|
463
|
+
dataPreCommentIndex = newAttrs.length
|
|
464
|
+
commentMarkValue = val
|
|
465
|
+
newAttrs.push(attr)
|
|
466
|
+
break
|
|
467
|
+
case 'data-pre-line-number-skip':
|
|
468
|
+
dataPreLineNumberSkipIndex = newAttrs.length
|
|
469
|
+
lineNumberSkipValue = val
|
|
470
|
+
newAttrs.push(attr)
|
|
471
|
+
break
|
|
472
|
+
case 'data-pre-line-number-reset':
|
|
473
|
+
dataPreLineNumberResetIndex = newAttrs.length
|
|
474
|
+
lineNumberResetValue = val
|
|
475
|
+
newAttrs.push(attr)
|
|
476
|
+
break
|
|
477
|
+
case 'em-lines':
|
|
478
|
+
case 'emphasize-lines':
|
|
479
|
+
if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
|
|
480
|
+
emphasisValue = val
|
|
481
|
+
if (!sawEmphasisAttr) {
|
|
482
|
+
appendOrder.push('emphasis')
|
|
483
|
+
sawEmphasisAttr = true
|
|
484
|
+
}
|
|
485
|
+
break
|
|
486
|
+
case 'comment-mark':
|
|
487
|
+
commentMarkValue = val
|
|
488
|
+
if (!sawCommentMark) {
|
|
489
|
+
appendOrder.push('comment')
|
|
490
|
+
sawCommentMark = true
|
|
491
|
+
}
|
|
492
|
+
break
|
|
493
|
+
case 'line-number-skip':
|
|
494
|
+
case 'pre-line-number-skip':
|
|
495
|
+
lineNumberSkipValue = val
|
|
496
|
+
if (!sawLineNumberSkipAttr) {
|
|
497
|
+
appendOrder.push('line-number-skip')
|
|
498
|
+
sawLineNumberSkipAttr = true
|
|
499
|
+
}
|
|
500
|
+
break
|
|
501
|
+
case 'line-number-reset':
|
|
502
|
+
case 'pre-line-number-reset':
|
|
503
|
+
lineNumberResetValue = val
|
|
504
|
+
if (!sawLineNumberResetAttr) {
|
|
505
|
+
appendOrder.push('line-number-reset')
|
|
506
|
+
sawLineNumberResetAttr = true
|
|
507
|
+
}
|
|
508
|
+
break
|
|
509
|
+
case 'data-pre-wrap':
|
|
510
|
+
preWrapValue = val
|
|
511
|
+
if (val === '' || val === 'true') wrapEnabled = true
|
|
512
|
+
break
|
|
513
|
+
case 'wrap':
|
|
514
|
+
case 'pre-wrap':
|
|
515
|
+
if (val === '' || val === 'true') wrapEnabled = true
|
|
516
|
+
break
|
|
517
|
+
default:
|
|
518
|
+
newAttrs.push(attr)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (startValue !== undefined && dataPreStartIndex >= 0) newAttrs[dataPreStartIndex][1] = startValue
|
|
523
|
+
if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) newAttrs[dataPreEmphasisIndex][1] = emphasisValue
|
|
524
|
+
if (commentMarkValue !== undefined && dataPreCommentIndex >= 0) newAttrs[dataPreCommentIndex][1] = commentMarkValue
|
|
525
|
+
if (lineNumberSkipValue !== undefined && dataPreLineNumberSkipIndex >= 0) newAttrs[dataPreLineNumberSkipIndex][1] = lineNumberSkipValue
|
|
526
|
+
if (lineNumberResetValue !== undefined && dataPreLineNumberResetIndex >= 0) newAttrs[dataPreLineNumberResetIndex][1] = lineNumberResetValue
|
|
527
|
+
|
|
528
|
+
for (const kind of appendOrder) {
|
|
529
|
+
if (kind === 'start' && dataPreStartIndex === -1 && startValue !== undefined) {
|
|
530
|
+
newAttrs.push(['data-pre-start', startValue])
|
|
531
|
+
} else if (kind === 'emphasis' && dataPreEmphasisIndex === -1 && emphasisValue !== undefined) {
|
|
532
|
+
newAttrs.push(['data-pre-emphasis', emphasisValue])
|
|
533
|
+
} else if (kind === 'comment' && dataPreCommentIndex === -1 && commentMarkValue !== undefined) {
|
|
534
|
+
newAttrs.push(['data-pre-comment-mark', commentMarkValue])
|
|
535
|
+
} else if (kind === 'line-number-skip' && dataPreLineNumberSkipIndex === -1 && lineNumberSkipValue !== undefined) {
|
|
536
|
+
newAttrs.push(['data-pre-line-number-skip', lineNumberSkipValue])
|
|
537
|
+
} else if (kind === 'line-number-reset' && dataPreLineNumberResetIndex === -1 && lineNumberResetValue !== undefined) {
|
|
538
|
+
newAttrs.push(['data-pre-line-number-reset', lineNumberResetValue])
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (startNumber !== -1) {
|
|
543
|
+
styleValue = appendStyleValue(styleValue, 'counter-set:pre-line-number ' + startNumber + ';')
|
|
544
|
+
}
|
|
545
|
+
if (styleIndex >= 0) {
|
|
546
|
+
if (styleValue !== undefined) newAttrs[styleIndex][1] = styleValue
|
|
547
|
+
} else if (styleValue) {
|
|
548
|
+
newAttrs.push(['style', styleValue])
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (wrapEnabled) preWrapValue = 'true'
|
|
552
|
+
token.attrs = newAttrs
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
token,
|
|
559
|
+
lang,
|
|
560
|
+
startNumber,
|
|
561
|
+
emphasizeLines,
|
|
562
|
+
lineNumberSkipValue,
|
|
563
|
+
lineNumberResetValue,
|
|
564
|
+
wrapEnabled,
|
|
565
|
+
preWrapValue,
|
|
566
|
+
commentMarkValue,
|
|
567
|
+
timingEnabled,
|
|
568
|
+
timings,
|
|
569
|
+
fenceStartedAt,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export {
|
|
574
|
+
addTimingMs,
|
|
575
|
+
applyLineEndAlias,
|
|
576
|
+
commentLineClass,
|
|
577
|
+
createCommonFenceOptionDefaults,
|
|
578
|
+
emitFenceDecision,
|
|
579
|
+
finalizeCommonFenceOption,
|
|
580
|
+
finalizeFenceTimings,
|
|
581
|
+
getLogicalLineCount,
|
|
582
|
+
getNowMs,
|
|
583
|
+
getEmphasizeLines,
|
|
584
|
+
normalizeEmphasisRanges,
|
|
585
|
+
orderTokenAttrs,
|
|
586
|
+
preWrapStyle,
|
|
587
|
+
prepareFenceRenderContext,
|
|
588
|
+
resolveAdvancedLineNumberPlan,
|
|
589
|
+
splitFenceBlockToLines,
|
|
590
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const attrReg = /^(?:([.#])(.+)|(.+?)(?:=(["'])?(.*?)\1)?)$/
|
|
2
|
+
const interAttrsSpaceReg = / +/
|
|
3
|
+
const htmlAttrReg = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
|
|
4
|
+
|
|
5
|
+
const createAttrOrderIndexGetter = (order) => {
|
|
6
|
+
const exact = new Map()
|
|
7
|
+
const prefixes = []
|
|
8
|
+
for (let i = 0; i < order.length; i++) {
|
|
9
|
+
const key = order[i]
|
|
10
|
+
if (key.endsWith('*')) {
|
|
11
|
+
prefixes.push([key.slice(0, -1), i])
|
|
12
|
+
} else if (!exact.has(key)) {
|
|
13
|
+
exact.set(key, i)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const fallback = order.length
|
|
17
|
+
return (name) => {
|
|
18
|
+
const exactIndex = exact.get(name)
|
|
19
|
+
if (exactIndex !== undefined) return exactIndex
|
|
20
|
+
for (let i = 0; i < prefixes.length; i++) {
|
|
21
|
+
if (name.startsWith(prefixes[i][0])) return prefixes[i][1]
|
|
22
|
+
}
|
|
23
|
+
return fallback
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const appendStyleValue = (style, addition) => {
|
|
28
|
+
if (!addition) return style
|
|
29
|
+
let next = addition.trim()
|
|
30
|
+
if (!next) return style
|
|
31
|
+
if (!next.endsWith(';')) next += ';'
|
|
32
|
+
if (!style) return next
|
|
33
|
+
const base = style.trimEnd()
|
|
34
|
+
const separator = base.endsWith(';') ? ' ' : '; '
|
|
35
|
+
return base + separator + next
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parseHtmlAttrs = (attrText) => {
|
|
39
|
+
const attrs = []
|
|
40
|
+
if (!attrText) return attrs
|
|
41
|
+
htmlAttrReg.lastIndex = 0
|
|
42
|
+
let match
|
|
43
|
+
while ((match = htmlAttrReg.exec(attrText)) !== null) {
|
|
44
|
+
const name = match[1]
|
|
45
|
+
const val = match[2] ?? match[3] ?? match[4] ?? ''
|
|
46
|
+
attrs.push([name, val])
|
|
47
|
+
}
|
|
48
|
+
return attrs
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mergeAttrSets = (target, source) => {
|
|
52
|
+
if (!source || source.length === 0) return
|
|
53
|
+
const index = new Map()
|
|
54
|
+
for (let i = 0; i < target.length; i++) index.set(target[i][0], i)
|
|
55
|
+
for (const [name, val] of source) {
|
|
56
|
+
const idx = index.get(name)
|
|
57
|
+
if (name === 'class') {
|
|
58
|
+
if (idx === undefined) {
|
|
59
|
+
target.push([name, val])
|
|
60
|
+
index.set(name, target.length - 1)
|
|
61
|
+
} else if (val) {
|
|
62
|
+
const existing = target[idx][1]
|
|
63
|
+
target[idx][1] = existing ? existing + ' ' + val : val
|
|
64
|
+
}
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (name === 'style') {
|
|
68
|
+
if (idx === undefined) {
|
|
69
|
+
target.push([name, val])
|
|
70
|
+
index.set(name, target.length - 1)
|
|
71
|
+
} else {
|
|
72
|
+
target[idx][1] = appendStyleValue(target[idx][1], val)
|
|
73
|
+
}
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (idx === undefined) {
|
|
77
|
+
target.push([name, val])
|
|
78
|
+
index.set(name, target.length - 1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getInfoAttr = (infoAttr) => {
|
|
84
|
+
const attrSets = infoAttr.trim().split(interAttrsSpaceReg)
|
|
85
|
+
const out = []
|
|
86
|
+
for (let attrSet of attrSets) {
|
|
87
|
+
const str = attrReg.exec(attrSet)
|
|
88
|
+
if (!str) continue
|
|
89
|
+
if (str[1] === '.') str[1] = 'class'
|
|
90
|
+
if (str[1] === '#') str[1] = 'id'
|
|
91
|
+
if (str[3]) {
|
|
92
|
+
str[1] = str[3]
|
|
93
|
+
str[2] = str[5] ? str[5].replace(/^['"]/, '').replace(/['"]$/, '') : ''
|
|
94
|
+
}
|
|
95
|
+
out.push([str[1], str[2]])
|
|
96
|
+
}
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const getLangFromClassAttr = (classText, langPrefix) => {
|
|
101
|
+
const value = String(classText || '')
|
|
102
|
+
if (!value || !langPrefix || value.indexOf(langPrefix) === -1) return ''
|
|
103
|
+
const list = value.trim().split(/\s+/)
|
|
104
|
+
for (let i = 0; i < list.length; i++) {
|
|
105
|
+
const name = list[i]
|
|
106
|
+
if (name.startsWith(langPrefix) && name.length > langPrefix.length) {
|
|
107
|
+
return name.slice(langPrefix.length)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
appendStyleValue,
|
|
115
|
+
createAttrOrderIndexGetter,
|
|
116
|
+
getInfoAttr,
|
|
117
|
+
getLangFromClassAttr,
|
|
118
|
+
mergeAttrSets,
|
|
119
|
+
parseHtmlAttrs,
|
|
120
|
+
}
|