@peaceroad/markdown-it-renderer-fence 0.4.1 → 0.5.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.
@@ -0,0 +1,810 @@
1
+ import {
2
+ classifyShikiScopeKeyword,
3
+ getShikiRawScopeName,
4
+ normalizeShikiKeywordLangAliasMap,
5
+ resolveShikiKeywordLangForFence,
6
+ } from '../custom-highlight/shiki-keyword.js'
7
+ import {
8
+ escapeHtmlAttr,
9
+ getCustomHighlightPayloadMap,
10
+ renderCustomHighlightPayloadScript as renderPayloadScriptUtil,
11
+ } from '../custom-highlight/payload-utils.js'
12
+ import {
13
+ styleToHighlightCss,
14
+ } from '../custom-highlight/runtime-utils.js'
15
+ import {
16
+ validateCustomHighlightOptions,
17
+ } from '../custom-highlight/option-validator.js'
18
+ import {
19
+ commentLineClass,
20
+ normalizeEmphasisRanges,
21
+ } from './render-shared.js'
22
+ import {
23
+ parsePreCodeWrapper,
24
+ } from '../utils/pre-code-wrapper-parser.js'
25
+ import {
26
+ customHighlightDataEnvKey,
27
+ customHighlightDataScriptId,
28
+ customHighlightPayloadSchemaVersion,
29
+ customHighlightPayloadSupportedVersions,
30
+ fallbackOnDefault,
31
+ } from './render-api-constants.js'
32
+
33
+ const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
34
+ const hyphenMultiReg = /-+/g
35
+
36
+ const defaultCustomHighlightOpt = {
37
+ provider: 'shiki',
38
+ getRanges: null,
39
+ fallback: 'plain',
40
+ fallbackOn: fallbackOnDefault,
41
+ transport: 'env',
42
+ idPrefix: 'hl-',
43
+ lineFeatureStrategy: 'hybrid',
44
+ scopePrefix: 'hl',
45
+ includeScopeStyles: true,
46
+ shikiScopeMode: 'auto',
47
+ shikiKeywordClassifier: null,
48
+ shikiKeywordLangResolver: null,
49
+ shikiKeywordLangAliases: null,
50
+ }
51
+
52
+ const normalizeCustomHighlightProvider = (provider) => {
53
+ if (provider === 'custom') return 'custom'
54
+ if (provider === 'hljs') return 'hljs'
55
+ return 'shiki'
56
+ }
57
+
58
+ const normalizeShikiScopeMode = (mode) => {
59
+ const key = String(mode || '').trim().toLowerCase()
60
+ if (!key) return null
61
+ if (key === 'keyword') return 'keyword'
62
+ if (key === 'semantic') return 'semantic'
63
+ if (key === 'color') return 'color'
64
+ if (key === 'auto') return 'auto'
65
+ return null
66
+ }
67
+
68
+ const normalizeThemeName = (name) => {
69
+ if (typeof name !== 'string') return ''
70
+ return name.trim()
71
+ }
72
+
73
+ const normalizeCustomHighlightTheme = (theme) => {
74
+ const singleTheme = normalizeThemeName(theme)
75
+ if (singleTheme) {
76
+ return {
77
+ singleTheme,
78
+ themeVariants: null,
79
+ defaultVariant: '',
80
+ }
81
+ }
82
+ if (!theme || typeof theme !== 'object' || Array.isArray(theme)) {
83
+ return {
84
+ singleTheme: '',
85
+ themeVariants: null,
86
+ defaultVariant: '',
87
+ }
88
+ }
89
+ const lightTheme = normalizeThemeName(theme.light)
90
+ const darkTheme = normalizeThemeName(theme.dark)
91
+ if (!lightTheme && !darkTheme) {
92
+ return {
93
+ singleTheme: '',
94
+ themeVariants: null,
95
+ defaultVariant: '',
96
+ }
97
+ }
98
+ if (!lightTheme || !darkTheme) {
99
+ return {
100
+ singleTheme: lightTheme || darkTheme,
101
+ themeVariants: null,
102
+ defaultVariant: '',
103
+ }
104
+ }
105
+ let defaultVariant = String(theme.default || '').trim().toLowerCase()
106
+ if (defaultVariant !== 'light' && defaultVariant !== 'dark') defaultVariant = 'light'
107
+ return {
108
+ singleTheme: '',
109
+ themeVariants: { light: lightTheme, dark: darkTheme },
110
+ defaultVariant,
111
+ }
112
+ }
113
+
114
+ const buildShikiInternalLangAliasMap = (highlighter) => {
115
+ if (!highlighter || typeof highlighter.getLoadedLanguages !== 'function' || typeof highlighter.getLanguage !== 'function') {
116
+ return null
117
+ }
118
+ let loaded
119
+ try {
120
+ loaded = highlighter.getLoadedLanguages()
121
+ } catch (e) {
122
+ return null
123
+ }
124
+ if (!Array.isArray(loaded) || loaded.length === 0) return null
125
+ const rawMap = {}
126
+ for (let i = 0; i < loaded.length; i++) {
127
+ const rawName = String(loaded[i] || '')
128
+ if (!rawName) continue
129
+ let canonical = rawName
130
+ try {
131
+ const langDef = highlighter.getLanguage(rawName)
132
+ if (langDef && typeof langDef.name === 'string' && langDef.name) canonical = langDef.name
133
+ } catch (e) {}
134
+ rawMap[rawName] = canonical
135
+ }
136
+ return normalizeShikiKeywordLangAliasMap(rawMap)
137
+ }
138
+
139
+ const normalizeCustomHighlightOpt = (opt = {}) => {
140
+ const rawOpt = (opt && typeof opt === 'object') ? opt : {}
141
+ const next = Object.assign({}, defaultCustomHighlightOpt, rawOpt)
142
+ next.provider = normalizeCustomHighlightProvider(next.provider)
143
+ next.fallback = (next.fallback === 'markup') ? 'markup' : 'plain'
144
+ next.transport = (next.transport === 'inline-script') ? 'inline-script' : 'env'
145
+ next.lineFeatureStrategy = (next.lineFeatureStrategy === 'disable') ? 'disable' : 'hybrid'
146
+ next.idPrefix = String(next.idPrefix || 'hl-')
147
+ next.scopePrefix = next.scopePrefix == null ? 'hl' : String(next.scopePrefix)
148
+ next.includeScopeStyles = next.includeScopeStyles !== false
149
+ next.shikiScopeMode = normalizeShikiScopeMode(next.shikiScopeMode) || 'auto'
150
+ const normalizedTheme = normalizeCustomHighlightTheme(next.theme)
151
+ next._singleTheme = normalizedTheme.singleTheme
152
+ next._themeVariants = normalizedTheme.themeVariants
153
+ next._themeVariantDefault = normalizedTheme.defaultVariant
154
+ if (normalizedTheme.themeVariants) {
155
+ next.theme = {
156
+ light: normalizedTheme.themeVariants.light,
157
+ dark: normalizedTheme.themeVariants.dark,
158
+ default: normalizedTheme.defaultVariant,
159
+ }
160
+ } else {
161
+ next.theme = normalizedTheme.singleTheme || null
162
+ }
163
+ if (typeof next.shikiKeywordClassifier !== 'function') next.shikiKeywordClassifier = null
164
+ if (typeof next.shikiKeywordLangResolver !== 'function') next.shikiKeywordLangResolver = null
165
+ next.shikiKeywordLangAliases = normalizeShikiKeywordLangAliasMap(next.shikiKeywordLangAliases)
166
+ next._shikiInternalLangAliasMap =
167
+ (next.provider === 'shiki' && next.shikiScopeMode === 'keyword')
168
+ ? buildShikiInternalLangAliasMap(next.highlighter)
169
+ : null
170
+ const fallbackOn = Array.isArray(next.fallbackOn) ? next.fallbackOn : fallbackOnDefault
171
+ next._fallbackOnSet = new Set(fallbackOn)
172
+ validateCustomHighlightOptions(rawOpt, next)
173
+ return next
174
+ }
175
+
176
+ const isNormalizedCustomHighlightOpt = (opt) => {
177
+ return !!(opt && typeof opt === 'object' && opt._fallbackOnSet instanceof Set)
178
+ }
179
+
180
+ const shouldApplyApiFallbackForReason = (chOpt, reason) => {
181
+ if (!reason) return true
182
+ if (!chOpt || !chOpt._fallbackOnSet) return true
183
+ return chOpt._fallbackOnSet.has(reason)
184
+ }
185
+
186
+ const sanitizeHighlightName = (name, prefix = '') => {
187
+ const prefixBase = String(prefix == null ? '' : prefix).replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
188
+ const raw = String(name || '')
189
+ let safe = raw.replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
190
+ if (!safe) safe = 'scope'
191
+ if (/^[0-9]/.test(safe)) safe = 'x-' + safe
192
+ if (safe.startsWith('--')) safe = safe.slice(2) || 'scope'
193
+ return prefixBase ? `${prefixBase}-${safe}` : safe
194
+ }
195
+
196
+ const uniqueHighlightName = (base, usedMap) => {
197
+ if (!usedMap.has(base)) {
198
+ usedMap.set(base, 1)
199
+ return base
200
+ }
201
+ let seq = usedMap.get(base) + 1
202
+ usedMap.set(base, seq)
203
+ return `${base}-${seq}`
204
+ }
205
+
206
+ const normalizeScopeStyle = (style) => {
207
+ if (!style || typeof style !== 'object') return null
208
+ const next = {}
209
+ let hasValue = false
210
+ if (typeof style.color === 'string' && style.color) {
211
+ next.color = style.color
212
+ hasValue = true
213
+ }
214
+ if (typeof style.backgroundColor === 'string' && style.backgroundColor) {
215
+ next.backgroundColor = style.backgroundColor
216
+ hasValue = true
217
+ }
218
+ if (typeof style.textDecoration === 'string' && style.textDecoration) {
219
+ next.textDecoration = style.textDecoration
220
+ hasValue = true
221
+ }
222
+ if (typeof style.textShadow === 'string' && style.textShadow) {
223
+ next.textShadow = style.textShadow
224
+ hasValue = true
225
+ }
226
+ return hasValue ? next : null
227
+ }
228
+
229
+ const getScopeStyleKey = (style) => {
230
+ if (!style) return ''
231
+ let key = ''
232
+ if (style.color) key += `c:${style.color};`
233
+ if (style.backgroundColor) key += `bg:${style.backgroundColor};`
234
+ if (style.textDecoration) key += `td:${style.textDecoration};`
235
+ if (style.textShadow) key += `ts:${style.textShadow};`
236
+ return key
237
+ }
238
+
239
+ const sameStringArray = (a, b) => {
240
+ if (a === b) return true
241
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
242
+ for (let i = 0; i < a.length; i++) {
243
+ if (a[i] !== b[i]) return false
244
+ }
245
+ return true
246
+ }
247
+
248
+ const sameRangeTuples = (a, b) => {
249
+ if (a === b) return true
250
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
251
+ for (let i = 0; i < a.length; i++) {
252
+ const left = a[i]
253
+ const right = b[i]
254
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false
255
+ for (let j = 0; j < left.length; j++) {
256
+ if (left[j] !== right[j]) return false
257
+ }
258
+ }
259
+ return true
260
+ }
261
+
262
+ const sameScopeStyles = (a, b) => {
263
+ const left = Array.isArray(a) ? a : null
264
+ const right = Array.isArray(b) ? b : null
265
+ if (left === right) return true
266
+ if (!left || !right || left.length !== right.length) return false
267
+ for (let i = 0; i < left.length; i++) {
268
+ const lv = left[i]
269
+ const rv = right[i]
270
+ if (lv === rv) continue
271
+ if (!lv || !rv || typeof lv !== 'object' || typeof rv !== 'object') return false
272
+ const lk = Object.keys(lv)
273
+ const rk = Object.keys(rv)
274
+ if (lk.length !== rk.length) return false
275
+ for (let j = 0; j < lk.length; j++) {
276
+ const key = lk[j]
277
+ if (lv[key] !== rv[key]) return false
278
+ }
279
+ }
280
+ return true
281
+ }
282
+
283
+ const getLogicalLinesAndOffsets = (text) => {
284
+ const rawLines = text.split('\n')
285
+ const logicalLineCount = rawLines.length > 0 && rawLines[rawLines.length - 1] === '' ? rawLines.length - 1 : rawLines.length
286
+ const lines = new Array(logicalLineCount)
287
+ const offsets = new Array(logicalLineCount)
288
+ let cursor = 0
289
+ for (let i = 0; i < logicalLineCount; i++) {
290
+ const line = rawLines[i]
291
+ lines[i] = line
292
+ offsets[i] = [cursor, cursor + line.length]
293
+ cursor += line.length + 1
294
+ }
295
+ return { lines, offsets }
296
+ }
297
+
298
+ const getShikiTokenStyle = (token) => {
299
+ const style = {}
300
+ if (typeof token.color === 'string' && token.color) style.color = token.color
301
+ const fontStyle = Number.isFinite(token.fontStyle) ? token.fontStyle : 0
302
+ if (fontStyle & 1) style.fontStyle = 'italic'
303
+ if (fontStyle & 2) style.fontWeight = '700'
304
+ if (fontStyle & 4) style.textDecoration = 'underline'
305
+ return normalizeScopeStyle(style)
306
+ }
307
+
308
+ const toShikiTokenLines = (result) => {
309
+ if (!result) return null
310
+ if (Array.isArray(result)) return result
311
+ if (Array.isArray(result.tokens)) return result.tokens
312
+ return null
313
+ }
314
+
315
+ const highlightClassReg = /\bclass\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i
316
+
317
+ const decodeHtmlEntity = (entity) => {
318
+ if (entity === '&lt;') return '<'
319
+ if (entity === '&gt;') return '>'
320
+ if (entity === '&amp;') return '&'
321
+ if (entity === '&quot;') return '"'
322
+ if (entity === '&#39;' || entity === '&apos;') return '\''
323
+ if (entity.startsWith('&#x') || entity.startsWith('&#X')) {
324
+ const n = Number.parseInt(entity.slice(3, -1), 16)
325
+ if (!Number.isFinite(n)) return entity
326
+ try {
327
+ return String.fromCodePoint(n)
328
+ } catch (e) {
329
+ return entity
330
+ }
331
+ }
332
+ if (entity.startsWith('&#')) {
333
+ const n = Number.parseInt(entity.slice(2, -1), 10)
334
+ if (!Number.isFinite(n)) return entity
335
+ try {
336
+ return String.fromCodePoint(n)
337
+ } catch (e) {
338
+ return entity
339
+ }
340
+ }
341
+ return entity
342
+ }
343
+
344
+ const decodeHtmlEntities = (text) => {
345
+ if (!text || text.indexOf('&') === -1) return text
346
+ return text.replace(/&(?:#x[0-9a-fA-F]+|#\d+|[A-Za-z][A-Za-z0-9]+);/g, decodeHtmlEntity)
347
+ }
348
+
349
+ const hljsScopeToName = (scope, lang) => {
350
+ const raw = String(scope || '').trim()
351
+ if (raw) {
352
+ if (raw.startsWith('hljs-')) return raw
353
+ return 'hljs-' + raw.replace(/\./g, '-')
354
+ }
355
+ return `hljs-${lang || 'plain'}`
356
+ }
357
+
358
+ const getHljsScopeFromClassText = (classText, lang) => {
359
+ if (!classText) return null
360
+ const classes = String(classText).trim().split(/\s+/).filter(Boolean)
361
+ if (!classes.length) return null
362
+ for (let i = 0; i < classes.length; i++) {
363
+ const name = classes[i]
364
+ if (name.startsWith('hljs-') && name !== 'hljs') return name
365
+ }
366
+ if (classes[0] && classes[0] !== 'hljs') return hljsScopeToName(classes[0], lang)
367
+ return hljsScopeToName('', lang)
368
+ }
369
+
370
+ const createRangesFromHighlightHtml = (lang, html) => {
371
+ const entries = []
372
+ if (!html) return entries
373
+ const stack = []
374
+ const source = String(html)
375
+ let cursor = 0
376
+ let i = 0
377
+ while (i < source.length) {
378
+ const lt = source.indexOf('<', i)
379
+ const textEnd = lt === -1 ? source.length : lt
380
+ if (textEnd > i) {
381
+ const rawText = source.slice(i, textEnd)
382
+ const decoded = decodeHtmlEntities(rawText)
383
+ const len = decoded.length
384
+ if (len > 0) {
385
+ const scope = stack.length ? stack[stack.length - 1] : null
386
+ if (scope) entries.push({ scope, start: cursor, end: cursor + len })
387
+ cursor += len
388
+ }
389
+ }
390
+ if (lt === -1) break
391
+ const gt = source.indexOf('>', lt + 1)
392
+ if (gt === -1) {
393
+ const rest = decodeHtmlEntities(source.slice(lt))
394
+ const len = rest.length
395
+ if (len > 0) {
396
+ const scope = stack.length ? stack[stack.length - 1] : null
397
+ if (scope) entries.push({ scope, start: cursor, end: cursor + len })
398
+ cursor += len
399
+ }
400
+ break
401
+ }
402
+ const body = source.slice(lt + 1, gt).trim()
403
+ if (body.startsWith('/')) {
404
+ if (body.slice(1).trim().toLowerCase().startsWith('span') && stack.length) stack.pop()
405
+ } else {
406
+ const space = body.indexOf(' ')
407
+ const name = (space === -1 ? body : body.slice(0, space)).replace(/\/$/, '').toLowerCase()
408
+ if (name === 'span') {
409
+ const classMatch = highlightClassReg.exec(body)
410
+ const classText = classMatch ? (classMatch[1] ?? classMatch[2] ?? classMatch[3] ?? '') : ''
411
+ stack.push(getHljsScopeFromClassText(classText, lang))
412
+ } else if (name === 'br') {
413
+ const scope = stack.length ? stack[stack.length - 1] : null
414
+ if (scope) entries.push({ scope, start: cursor, end: cursor + 1 })
415
+ cursor += 1
416
+ }
417
+ }
418
+ i = gt + 1
419
+ }
420
+ return entries
421
+ }
422
+
423
+ const walkHljsEmitterNode = (node, activeScope, cursorRef, entries, lang) => {
424
+ if (typeof node === 'string') {
425
+ const len = node.length
426
+ if (len > 0 && activeScope) entries.push({ scope: activeScope, start: cursorRef.value, end: cursorRef.value + len })
427
+ cursorRef.value += len
428
+ return
429
+ }
430
+ if (!node || typeof node !== 'object' || !Array.isArray(node.children)) return
431
+ const nextScope = (typeof node.scope === 'string' && node.scope)
432
+ ? hljsScopeToName(node.scope, lang)
433
+ : activeScope
434
+ for (let i = 0; i < node.children.length; i++) {
435
+ walkHljsEmitterNode(node.children[i], nextScope, cursorRef, entries, lang)
436
+ }
437
+ }
438
+
439
+ const createRangesFromHljsResult = (lang, result) => {
440
+ if (Array.isArray(result) || (result && Array.isArray(result.ranges))) {
441
+ return normalizeCustomProviderRanges(result)
442
+ }
443
+ if (result && result._emitter && result._emitter.rootNode) {
444
+ const entries = []
445
+ const cursorRef = { value: 0 }
446
+ walkHljsEmitterNode(result._emitter.rootNode, null, cursorRef, entries, lang)
447
+ return entries
448
+ }
449
+ let html = ''
450
+ if (typeof result === 'string') html = result
451
+ else if (result && typeof result.value === 'string') html = result.value
452
+ if (!html) return []
453
+ const preMatch = parsePreCodeWrapper(html)
454
+ if (preMatch) html = preMatch.content
455
+ return createRangesFromHighlightHtml(lang, html)
456
+ }
457
+
458
+ const hasShikiOffsets = (tokenLines) => {
459
+ for (let i = 0; i < tokenLines.length; i++) {
460
+ const line = tokenLines[i]
461
+ if (!Array.isArray(line)) continue
462
+ for (let t = 0; t < line.length; t++) {
463
+ const tok = line[t]
464
+ if (tok && Number.isFinite(tok.offset)) return true
465
+ }
466
+ }
467
+ return false
468
+ }
469
+
470
+ const getShikiScopeName = (tok, lang, style, opt, preResolvedKeywordLang = '') => {
471
+ const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
472
+ const rawScope = getShikiRawScopeName(tok)
473
+ let customKeywordName = null
474
+ const isKeywordScopeMode = scopeMode === 'keyword'
475
+ if (isKeywordScopeMode && opt && typeof opt.shikiKeywordClassifier === 'function') {
476
+ try {
477
+ const customName = opt.shikiKeywordClassifier(rawScope, tok, { lang, style, scopeMode })
478
+ if (customName != null && customName !== '') customKeywordName = String(customName)
479
+ } catch (e) {}
480
+ }
481
+ if (isKeywordScopeMode) {
482
+ if (customKeywordName) return 'shiki-' + customKeywordName
483
+ const bucket = classifyShikiScopeKeyword(rawScope, tok, lang, opt, preResolvedKeywordLang)
484
+ return 'shiki-' + bucket
485
+ }
486
+ if (scopeMode === 'semantic') {
487
+ if (rawScope) return 'shiki-' + rawScope
488
+ if (style && style.color) {
489
+ const colorKey = style.color.toLowerCase().replace(highlightNameUnsafeReg, '-')
490
+ const fs = Number.isFinite(tok.fontStyle) ? `-f${tok.fontStyle}` : ''
491
+ return `shiki-${colorKey}${fs}`
492
+ }
493
+ return `shiki-${lang || 'plain'}`
494
+ }
495
+ if (scopeMode === 'auto') {
496
+ if (rawScope) return 'shiki-' + rawScope
497
+ }
498
+ if (scopeMode === 'color') {
499
+ if (style && style.color) {
500
+ const colorKey = style.color.toLowerCase().replace(highlightNameUnsafeReg, '-')
501
+ const fs = Number.isFinite(tok.fontStyle) ? `-f${tok.fontStyle}` : ''
502
+ return `shiki-${colorKey}${fs}`
503
+ }
504
+ if (rawScope) return 'shiki-' + rawScope
505
+ return `shiki-${lang || 'plain'}`
506
+ }
507
+ if (rawScope) return 'shiki-' + rawScope
508
+ if (style && style.color) {
509
+ const colorKey = style.color.toLowerCase().replace(highlightNameUnsafeReg, '-')
510
+ const fs = Number.isFinite(tok.fontStyle) ? `-f${tok.fontStyle}` : ''
511
+ return `shiki-${colorKey}${fs}`
512
+ }
513
+ return `shiki-${lang || 'plain'}`
514
+ }
515
+
516
+ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
517
+ if (!Array.isArray(tokenLines)) throw new Error('Invalid shiki token payload')
518
+ const entries = []
519
+ const hasOffsets = hasShikiOffsets(tokenLines)
520
+ const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
521
+ const includeScopeStyles = !opt || opt.includeScopeStyles !== false
522
+ const needStyleForScopeName =
523
+ scopeMode === 'auto' ||
524
+ scopeMode === 'color' ||
525
+ scopeMode === 'semantic' ||
526
+ (scopeMode === 'keyword' && !!(opt && typeof opt.shikiKeywordClassifier === 'function'))
527
+ const needTokenStyle = includeScopeStyles || needStyleForScopeName
528
+ let preResolvedKeywordLang = ''
529
+ if (scopeMode === 'keyword' && (!opt || typeof opt.shikiKeywordLangResolver !== 'function')) {
530
+ preResolvedKeywordLang = resolveShikiKeywordLangForFence(lang, opt)
531
+ }
532
+ let cursor = 0
533
+ for (let i = 0; i < tokenLines.length; i++) {
534
+ const line = tokenLines[i]
535
+ if (!Array.isArray(line)) throw new Error('Invalid shiki token line')
536
+ for (let t = 0; t < line.length; t++) {
537
+ const tok = line[t]
538
+ if (!tok || typeof tok !== 'object') continue
539
+ const content = String(tok.content || '')
540
+ if (!content) continue
541
+ const hasOffset = Number.isFinite(tok.offset)
542
+ const start = hasOffset ? tok.offset : cursor
543
+ const end = start + content.length
544
+ const style = needTokenStyle ? getShikiTokenStyle(tok) : null
545
+ const scope = getShikiScopeName(tok, lang, style, opt, preResolvedKeywordLang)
546
+ entries.push({ scope, start, end, style })
547
+ cursor = end
548
+ }
549
+ if (!hasOffsets && i < tokenLines.length - 1) cursor += 1
550
+ }
551
+ return entries
552
+ }
553
+
554
+ const normalizeCustomProviderRanges = (result) => {
555
+ const source = Array.isArray(result) ? { ranges: result } : result
556
+ if (!source || !Array.isArray(source.ranges)) throw new Error('custom getRanges must return ranges')
557
+ const scopes = Array.isArray(source.scopes) ? source.scopes : null
558
+ const scopeStyles = source.scopeStyles && typeof source.scopeStyles === 'object' ? source.scopeStyles : null
559
+ const entries = []
560
+ for (const range of source.ranges) {
561
+ let scope
562
+ let start
563
+ let end
564
+ let style
565
+ if (Array.isArray(range)) {
566
+ scope = range[0]
567
+ start = range[1]
568
+ end = range[2]
569
+ style = range[3]
570
+ } else if (range && typeof range === 'object') {
571
+ scope = range.scope
572
+ start = range.start
573
+ end = range.end
574
+ style = range.style
575
+ } else {
576
+ continue
577
+ }
578
+ if (typeof scope === 'number' && scopes && scopes[scope] != null) scope = scopes[scope]
579
+ if (scope == null) continue
580
+ if (!style && scopeStyles) {
581
+ if (typeof scope === 'number' && Array.isArray(scopeStyles)) style = scopeStyles[scope]
582
+ else style = scopeStyles[String(scope)]
583
+ }
584
+ entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
585
+ }
586
+ return entries
587
+ }
588
+
589
+ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeStyles = true) => {
590
+ const scopes = []
591
+ const ranges = []
592
+ const scopeStyles = includeScopeStyles ? [] : null
593
+ let hasScopeStyles = false
594
+ const scopeKeyToIndex = new Map()
595
+ const usedScopeNames = new Map()
596
+ const textLength = text.length
597
+ for (const entry of entries) {
598
+ if (!entry) continue
599
+ const start = Number(entry.start)
600
+ const end = Number(entry.end)
601
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start || end > textLength) {
602
+ throw new Error(`Invalid range [${entry.start}, ${entry.end}]`)
603
+ }
604
+ const style = includeScopeStyles ? normalizeScopeStyle(entry.style) : null
605
+ const key = includeScopeStyles
606
+ ? `${String(entry.scope)}\u0001${getScopeStyleKey(style)}`
607
+ : String(entry.scope)
608
+ let scopeIndex = scopeKeyToIndex.get(key)
609
+ if (scopeIndex === undefined) {
610
+ const baseName = sanitizeHighlightName(entry.scope, scopePrefix)
611
+ const scopeName = uniqueHighlightName(baseName, usedScopeNames)
612
+ scopeIndex = scopes.length
613
+ scopes.push(scopeName)
614
+ if (scopeStyles) {
615
+ scopeStyles.push(style)
616
+ if (style) hasScopeStyles = true
617
+ }
618
+ scopeKeyToIndex.set(key, scopeIndex)
619
+ }
620
+ ranges.push([scopeIndex, start, end])
621
+ }
622
+ const payload = {
623
+ v: customHighlightPayloadSchemaVersion,
624
+ engine,
625
+ lang: lang || '',
626
+ offsetEncoding: 'utf16',
627
+ newline: 'lf',
628
+ textLength,
629
+ scopes,
630
+ ranges,
631
+ }
632
+ if (scopeStyles && hasScopeStyles) payload.scopeStyles = scopeStyles
633
+ return payload
634
+ }
635
+
636
+ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLines, commentMarkValue) => {
637
+ const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
638
+ const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
639
+ if (!needsEmphasis && !needsCommentScan) return
640
+ const { lines, offsets } = getLogicalLinesAndOffsets(content)
641
+ const maxLine = lines.length
642
+ if (needsEmphasis) {
643
+ const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
644
+ for (const [s, e] of normalized) {
645
+ const start = offsets[s - 1][0]
646
+ const end = offsets[e - 1][1]
647
+ if (end > start) entries.push({ scope: 'pre-lines-emphasis', start, end })
648
+ }
649
+ }
650
+ if (needsCommentScan) {
651
+ for (let i = 0; i < lines.length; i++) {
652
+ if (lines[i].trimStart().startsWith(commentMarkValue)) {
653
+ const [start, end] = offsets[i]
654
+ if (end > start) entries.push({ scope: commentLineClass, start, end })
655
+ }
656
+ }
657
+ }
658
+ }
659
+
660
+ const buildShikiTokenOption = (chOpt, targetLang, themeOverride = '') => {
661
+ const base = chOpt && chOpt._shikiTokenOptionBase
662
+ const option = base ? Object.assign({ lang: targetLang }, base) : { lang: targetLang }
663
+ if (themeOverride) option.theme = themeOverride
664
+ return option
665
+ }
666
+
667
+ const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
668
+ const context = { token, md, env, option: chOpt }
669
+ if (chOpt.provider === 'custom') {
670
+ const getRanges = chOpt._customGetRanges
671
+ if (typeof getRanges !== 'function') throw new Error('customHighlight.getRanges must be a function')
672
+ const result = getRanges(token.content, lang, context)
673
+ if (result && typeof result.then === 'function') throw new Error('customHighlight.getRanges must be synchronous')
674
+ return normalizeCustomProviderRanges(result)
675
+ }
676
+ if (chOpt.provider === 'hljs') {
677
+ const highlightFn = chOpt._hljsHighlightFn
678
+ if (typeof highlightFn !== 'function') {
679
+ throw new Error('customHighlight.hljsHighlight (or customHighlight.highlight / md.options.highlight) must be a function when provider=hljs')
680
+ }
681
+ const resolvedLang = lang || chOpt.defaultLang || ''
682
+ let result
683
+ try {
684
+ result = highlightFn(token.content, resolvedLang, context)
685
+ } catch (err) {
686
+ if (resolvedLang && resolvedLang !== 'plaintext') {
687
+ result = highlightFn(token.content, 'plaintext', context)
688
+ } else {
689
+ throw err
690
+ }
691
+ }
692
+ if (result && typeof result.then === 'function') throw new Error('customHighlight hljs provider must be synchronous')
693
+ return createRangesFromHljsResult(resolvedLang, result)
694
+ }
695
+
696
+ if (chOpt._hasShikiHighlighter === false || !chOpt.highlighter || typeof chOpt.highlighter.codeToTokens !== 'function') {
697
+ throw new Error('customHighlight.highlighter.codeToTokens must be a function when provider=shiki')
698
+ }
699
+
700
+ const resolvedLang = lang || chOpt.defaultLang || 'text'
701
+ const overrideTheme = normalizeThemeName(override && override.theme)
702
+ const themeForPass = overrideTheme || chOpt._singleTheme
703
+ let tokenResult
704
+ try {
705
+ tokenResult = chOpt.highlighter.codeToTokens(token.content, buildShikiTokenOption(chOpt, resolvedLang, themeForPass))
706
+ } catch (err) {
707
+ if (resolvedLang !== 'text') {
708
+ tokenResult = chOpt.highlighter.codeToTokens(token.content, buildShikiTokenOption(chOpt, 'text', themeForPass))
709
+ } else {
710
+ throw err
711
+ }
712
+ }
713
+ if (tokenResult && typeof tokenResult.then === 'function') throw new Error('customHighlight highlighter provider must be synchronous')
714
+ const tokenLines = toShikiTokenLines(tokenResult)
715
+ return createRangesFromShikiTokens(resolvedLang, tokenLines, chOpt)
716
+ }
717
+
718
+ const buildApiPayloadVariantRecord = (variantPayload, basePayload) => {
719
+ const record = {}
720
+ if (!sameStringArray(variantPayload.scopes, basePayload.scopes)) record.scopes = variantPayload.scopes
721
+ if (!sameRangeTuples(variantPayload.ranges, basePayload.ranges)) record.ranges = variantPayload.ranges
722
+ const baseStyles = Array.isArray(basePayload.scopeStyles) ? basePayload.scopeStyles : null
723
+ const variantStyles = Array.isArray(variantPayload.scopeStyles) ? variantPayload.scopeStyles : null
724
+ if (!sameScopeStyles(variantStyles, baseStyles)) {
725
+ if (variantStyles) record.scopeStyles = variantStyles
726
+ else if (baseStyles) record.scopeStyles = []
727
+ }
728
+ return record
729
+ }
730
+
731
+ const createShikiDualThemePayloadForFence = (token, lang, opt, md, env, emphasizeLines, commentMarkValue) => {
732
+ const chOpt = opt.customHighlight
733
+ const themeVariants = chOpt._themeVariants
734
+ const lineFeatureEntries = []
735
+ if (chOpt.lineFeatureStrategy !== 'disable') {
736
+ pushLineFeatureRanges(lineFeatureEntries, token.content, emphasizeLines, opt.setEmphasizeLines, commentMarkValue)
737
+ }
738
+ const buildVariantPayload = (themeName) => {
739
+ const entries = getApiProviderEntries(token, lang, chOpt, md, env, { theme: themeName })
740
+ if (lineFeatureEntries.length) entries.push(...lineFeatureEntries)
741
+ return buildApiPayload(entries, token.content, lang, chOpt.provider, chOpt.scopePrefix, true)
742
+ }
743
+ const variants = {
744
+ light: buildVariantPayload(themeVariants.light),
745
+ dark: buildVariantPayload(themeVariants.dark),
746
+ }
747
+ const defaultVariant = chOpt._themeVariantDefault === 'dark' ? 'dark' : 'light'
748
+ const otherVariant = defaultVariant === 'dark' ? 'light' : 'dark'
749
+ const basePayload = variants[defaultVariant]
750
+ const payload = Object.assign({}, basePayload)
751
+ payload.defaultVariant = defaultVariant
752
+ payload.variants = {
753
+ [defaultVariant]: {},
754
+ [otherVariant]: buildApiPayloadVariantRecord(variants[otherVariant], basePayload),
755
+ }
756
+ return payload
757
+ }
758
+
759
+ const createApiPayloadForFence = (token, lang, opt, md, env, emphasizeLines, commentMarkValue) => {
760
+ const chOpt = opt.customHighlight
761
+ if (chOpt.provider === 'shiki' && chOpt.includeScopeStyles !== false && chOpt._themeVariants) {
762
+ return createShikiDualThemePayloadForFence(token, lang, opt, md, env, emphasizeLines, commentMarkValue)
763
+ }
764
+ const entries = getApiProviderEntries(token, lang, chOpt, md, env)
765
+ if (chOpt.lineFeatureStrategy !== 'disable') {
766
+ pushLineFeatureRanges(entries, token.content, emphasizeLines, opt.setEmphasizeLines, commentMarkValue)
767
+ }
768
+ return buildApiPayload(entries, token.content, lang, chOpt.provider, chOpt.scopePrefix, chOpt.includeScopeStyles)
769
+ }
770
+
771
+ const renderCustomHighlightPayloadScript = (env, scriptId = customHighlightDataScriptId) => {
772
+ return renderPayloadScriptUtil(env, scriptId, customHighlightDataEnvKey)
773
+ }
774
+
775
+ const renderCustomHighlightScopeStyleTag = (env, styleTagId = 'pre-highlight-scope-style') => {
776
+ const map = getCustomHighlightPayloadMap(env, customHighlightDataEnvKey)
777
+ const keys = Object.keys(map)
778
+ if (!keys.length) return ''
779
+ const styleId = escapeHtmlAttr(styleTagId || 'pre-highlight-scope-style')
780
+ const used = new Set()
781
+ const cssLines = []
782
+ for (const blockId of keys) {
783
+ const payload = map[blockId]
784
+ if (!payload || !Array.isArray(payload.scopes) || !Array.isArray(payload.scopeStyles)) continue
785
+ for (let i = 0; i < payload.scopes.length; i++) {
786
+ const scopeName = payload.scopes[i]
787
+ if (!scopeName) continue
788
+ const runtimeName = sanitizeHighlightName(scopeName)
789
+ if (used.has(runtimeName)) continue
790
+ const css = styleToHighlightCss(payload.scopeStyles[i])
791
+ if (!css) continue
792
+ used.add(runtimeName)
793
+ cssLines.push(`::highlight(${runtimeName}){${css};}`)
794
+ }
795
+ }
796
+ if (!cssLines.length) return ''
797
+ return `<style id="${styleId}">\n${cssLines.join('\n')}\n</style>`
798
+ }
799
+
800
+ export {
801
+ createApiPayloadForFence,
802
+ customHighlightPayloadSchemaVersion,
803
+ customHighlightPayloadSupportedVersions,
804
+ isNormalizedCustomHighlightOpt,
805
+ normalizeCustomHighlightOpt,
806
+ renderCustomHighlightPayloadScript,
807
+ renderCustomHighlightScopeStyleTag,
808
+ sanitizeHighlightName,
809
+ shouldApplyApiFallbackForReason,
810
+ }