@peaceroad/markdown-it-renderer-fence 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -470
- package/docs/README.md +37 -0
- package/docs/code-highlighting-design.md +321 -0
- package/docs/custom-highlight-styling-guide.md +210 -0
- package/package.json +25 -13
- package/src/custom-highlight/option-validator.js +5 -5
- package/src/custom-highlight/payload-utils.js +4 -9
- package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +249 -54
- package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +107 -99
- package/src/custom-highlight/shiki-theme-role.js +176 -0
- package/src/fence/line-notes.js +27 -9
- package/src/fence/render-api-constants.js +1 -1
- package/src/fence/render-api-provider.js +61 -53
- package/src/fence/render-api-renderer.js +26 -10
- package/src/fence/render-api-runtime.js +29 -10
- package/src/fence/render-api.js +20 -12
- package/src/fence/render-markup.js +34 -24
- package/src/fence/render-shared.js +150 -30
- package/src/utils/attr-utils.js +10 -0
- package/theme/_shared/rf-core.css +170 -0
- package/theme/line-notes.css +94 -0
- package/theme/line-number.css +64 -0
- package/theme/rf-basic/README.md +117 -0
- package/theme/rf-basic/api/highlightjs.css +71 -0
- package/theme/rf-basic/api/shiki.css +67 -0
- package/theme/rf-basic/base.css +5 -0
- package/theme/rf-basic/index.css +7 -0
- package/theme/rf-basic/index.js +164 -0
- package/theme/rf-basic/markup/highlightjs.css +77 -0
- package/theme/rf-basic/markup/shiki.css +11 -0
- package/theme/rf-monochrome/README.md +84 -0
- package/theme/rf-monochrome/base.css +5 -0
- package/theme/rf-monochrome/index.css +5 -0
- package/theme/rf-monochrome/index.js +71 -0
- package/theme/rf-monochrome/markup/highlightjs.css +150 -0
- package/theme/rf-monochrome/markup/shiki.css +38 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
classifyShikiScopeRole,
|
|
3
3
|
getShikiRawScopeName,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '../custom-highlight/shiki-
|
|
4
|
+
normalizeShikiRoleLangAliasMap,
|
|
5
|
+
resolveShikiRoleLangForFence,
|
|
6
|
+
} from '../custom-highlight/shiki-role.js'
|
|
7
7
|
import {
|
|
8
8
|
escapeHtmlAttr,
|
|
9
9
|
getCustomHighlightPayloadMap,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../custom-highlight/scope-name.js'
|
|
21
21
|
import {
|
|
22
22
|
commentLineClass,
|
|
23
|
+
getCommentLineRanges,
|
|
23
24
|
normalizeEmphasisRanges,
|
|
24
25
|
} from './render-shared.js'
|
|
25
26
|
import {
|
|
@@ -46,9 +47,9 @@ const defaultCustomHighlightOpt = {
|
|
|
46
47
|
scopePrefix: 'hl',
|
|
47
48
|
includeScopeStyles: true,
|
|
48
49
|
shikiScopeMode: 'auto',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
shikiRoleClassifier: null,
|
|
51
|
+
shikiRoleLangResolver: null,
|
|
52
|
+
shikiRoleLangAliases: null,
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const normalizeCustomHighlightProvider = (provider) => {
|
|
@@ -60,7 +61,7 @@ const normalizeCustomHighlightProvider = (provider) => {
|
|
|
60
61
|
const normalizeShikiScopeMode = (mode) => {
|
|
61
62
|
const key = String(mode || '').trim().toLowerCase()
|
|
62
63
|
if (!key) return null
|
|
63
|
-
if (key === '
|
|
64
|
+
if (key === 'role') return 'role'
|
|
64
65
|
if (key === 'semantic') return 'semantic'
|
|
65
66
|
if (key === 'color') return 'color'
|
|
66
67
|
if (key === 'auto') return 'auto'
|
|
@@ -135,7 +136,7 @@ const buildShikiInternalLangAliasMap = (highlighter) => {
|
|
|
135
136
|
} catch (e) {}
|
|
136
137
|
rawMap[rawName] = canonical
|
|
137
138
|
}
|
|
138
|
-
return
|
|
139
|
+
return normalizeShikiRoleLangAliasMap(rawMap)
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
const normalizeCustomHighlightOpt = (opt = {}) => {
|
|
@@ -162,11 +163,11 @@ const normalizeCustomHighlightOpt = (opt = {}) => {
|
|
|
162
163
|
} else {
|
|
163
164
|
next.theme = normalizedTheme.singleTheme || null
|
|
164
165
|
}
|
|
165
|
-
if (typeof next.
|
|
166
|
-
if (typeof next.
|
|
167
|
-
next.
|
|
166
|
+
if (typeof next.shikiRoleClassifier !== 'function') next.shikiRoleClassifier = null
|
|
167
|
+
if (typeof next.shikiRoleLangResolver !== 'function') next.shikiRoleLangResolver = null
|
|
168
|
+
next.shikiRoleLangAliases = normalizeShikiRoleLangAliasMap(next.shikiRoleLangAliases)
|
|
168
169
|
next._shikiInternalLangAliasMap =
|
|
169
|
-
(next.provider === 'shiki' && next.shikiScopeMode === '
|
|
170
|
+
(next.provider === 'shiki' && next.shikiScopeMode === 'role')
|
|
170
171
|
? buildShikiInternalLangAliasMap(next.highlighter)
|
|
171
172
|
: null
|
|
172
173
|
const fallbackOn = Array.isArray(next.fallbackOn) ? next.fallbackOn : fallbackOnDefault
|
|
@@ -272,27 +273,26 @@ const sameScopeStyles = (a, b) => {
|
|
|
272
273
|
return true
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
let
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
276
|
+
const getLogicalLineOffsets = (text) => {
|
|
277
|
+
const str = String(text ?? '')
|
|
278
|
+
const offsets = []
|
|
279
|
+
if (!str) return offsets
|
|
280
|
+
let lineStart = 0
|
|
281
|
+
for (let i = 0; i < str.length; i++) {
|
|
282
|
+
const code = str.charCodeAt(i)
|
|
283
|
+
if (code !== 10 && code !== 13) continue
|
|
284
|
+
offsets.push([lineStart, i])
|
|
285
|
+
if (code === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
|
|
286
|
+
lineStart = i + 1
|
|
286
287
|
}
|
|
287
|
-
|
|
288
|
+
if (lineStart < str.length) offsets.push([lineStart, str.length])
|
|
289
|
+
return offsets
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
const getShikiTokenStyle = (token) => {
|
|
291
293
|
const style = {}
|
|
292
294
|
if (typeof token.color === 'string' && token.color) style.color = token.color
|
|
293
295
|
const fontStyle = Number.isFinite(token.fontStyle) ? token.fontStyle : 0
|
|
294
|
-
if (fontStyle & 1) style.fontStyle = 'italic'
|
|
295
|
-
if (fontStyle & 2) style.fontWeight = '700'
|
|
296
296
|
if (fontStyle & 4) style.textDecoration = 'underline'
|
|
297
297
|
return normalizeScopeStyle(style)
|
|
298
298
|
}
|
|
@@ -459,21 +459,21 @@ const hasShikiOffsets = (tokenLines) => {
|
|
|
459
459
|
return false
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
const getShikiScopeName = (tok, lang, style, opt,
|
|
462
|
+
const getShikiScopeName = (tok, lang, style, opt, preResolvedRoleLang = '') => {
|
|
463
463
|
const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
|
|
464
464
|
const rawScope = getShikiRawScopeName(tok)
|
|
465
|
-
let
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
465
|
+
let customRoleName = null
|
|
466
|
+
const isRoleScopeMode = scopeMode === 'role'
|
|
467
|
+
if (isRoleScopeMode && opt && typeof opt.shikiRoleClassifier === 'function') {
|
|
468
468
|
try {
|
|
469
|
-
const customName = opt.
|
|
470
|
-
if (customName != null && customName !== '')
|
|
469
|
+
const customName = opt.shikiRoleClassifier(rawScope, tok, { lang, style, scopeMode })
|
|
470
|
+
if (customName != null && customName !== '') customRoleName = String(customName)
|
|
471
471
|
} catch (e) {}
|
|
472
472
|
}
|
|
473
|
-
if (
|
|
474
|
-
if (
|
|
475
|
-
const bucket =
|
|
476
|
-
return 'shiki-' + bucket
|
|
473
|
+
if (isRoleScopeMode) {
|
|
474
|
+
if (customRoleName) return 'shiki-role-' + customRoleName
|
|
475
|
+
const bucket = classifyShikiScopeRole(rawScope, tok, lang, opt, preResolvedRoleLang)
|
|
476
|
+
return 'shiki-role-' + bucket
|
|
477
477
|
}
|
|
478
478
|
if (scopeMode === 'semantic') {
|
|
479
479
|
if (rawScope) return 'shiki-' + rawScope
|
|
@@ -515,11 +515,11 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
515
515
|
scopeMode === 'auto' ||
|
|
516
516
|
scopeMode === 'color' ||
|
|
517
517
|
scopeMode === 'semantic' ||
|
|
518
|
-
(scopeMode === '
|
|
518
|
+
(scopeMode === 'role' && !!(opt && typeof opt.shikiRoleClassifier === 'function'))
|
|
519
519
|
const needTokenStyle = includeScopeStyles || needStyleForScopeName
|
|
520
|
-
let
|
|
521
|
-
if (scopeMode === '
|
|
522
|
-
|
|
520
|
+
let preResolvedRoleLang = ''
|
|
521
|
+
if (scopeMode === 'role' && (!opt || typeof opt.shikiRoleLangResolver !== 'function')) {
|
|
522
|
+
preResolvedRoleLang = resolveShikiRoleLangForFence(lang, opt)
|
|
523
523
|
}
|
|
524
524
|
let cursor = 0
|
|
525
525
|
for (let i = 0; i < tokenLines.length; i++) {
|
|
@@ -534,7 +534,7 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
534
534
|
const start = hasOffset ? tok.offset : cursor
|
|
535
535
|
const end = start + content.length
|
|
536
536
|
const style = needTokenStyle ? getShikiTokenStyle(tok) : null
|
|
537
|
-
const scope = getShikiScopeName(tok, lang, style, opt,
|
|
537
|
+
const scope = getShikiScopeName(tok, lang, style, opt, preResolvedRoleLang)
|
|
538
538
|
entries.push({ scope, start, end, style })
|
|
539
539
|
cursor = end
|
|
540
540
|
}
|
|
@@ -551,6 +551,7 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
551
551
|
const entries = []
|
|
552
552
|
for (const range of source.ranges) {
|
|
553
553
|
let scope
|
|
554
|
+
let scopeIndex = null
|
|
554
555
|
let start
|
|
555
556
|
let end
|
|
556
557
|
let style
|
|
@@ -567,10 +568,11 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
567
568
|
} else {
|
|
568
569
|
continue
|
|
569
570
|
}
|
|
570
|
-
if (
|
|
571
|
+
if (Number.isSafeInteger(scope)) scopeIndex = scope
|
|
572
|
+
if (scopeIndex != null && scopes && scopes[scopeIndex] != null) scope = scopes[scopeIndex]
|
|
571
573
|
if (scope == null) continue
|
|
572
574
|
if (!style && scopeStyles) {
|
|
573
|
-
if (
|
|
575
|
+
if (scopeIndex != null && Array.isArray(scopeStyles)) style = scopeStyles[scopeIndex]
|
|
574
576
|
else style = scopeStyles[String(scope)]
|
|
575
577
|
}
|
|
576
578
|
entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
|
|
@@ -609,7 +611,12 @@ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeS
|
|
|
609
611
|
}
|
|
610
612
|
scopeKeyToIndex.set(key, scopeIndex)
|
|
611
613
|
}
|
|
612
|
-
ranges.
|
|
614
|
+
const previousRange = ranges.length ? ranges[ranges.length - 1] : null
|
|
615
|
+
if (previousRange && previousRange[0] === scopeIndex && previousRange[2] === start) {
|
|
616
|
+
previousRange[2] = end
|
|
617
|
+
} else {
|
|
618
|
+
ranges.push([scopeIndex, start, end])
|
|
619
|
+
}
|
|
613
620
|
}
|
|
614
621
|
const payload = {
|
|
615
622
|
v: customHighlightPayloadSchemaVersion,
|
|
@@ -629,8 +636,8 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
629
636
|
const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
|
|
630
637
|
const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
|
|
631
638
|
if (!needsEmphasis && !needsCommentScan) return
|
|
632
|
-
const
|
|
633
|
-
const maxLine =
|
|
639
|
+
const offsets = needsEmphasis ? getLogicalLineOffsets(content) : null
|
|
640
|
+
const maxLine = offsets ? offsets.length : 0
|
|
634
641
|
if (needsEmphasis) {
|
|
635
642
|
const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
|
|
636
643
|
for (const [s, e] of normalized) {
|
|
@@ -640,11 +647,11 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
640
647
|
}
|
|
641
648
|
}
|
|
642
649
|
if (needsCommentScan) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
650
|
+
const ranges = getCommentLineRanges(content, commentMarkValue)
|
|
651
|
+
if (!ranges) return
|
|
652
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
653
|
+
const [start, end] = ranges[i]
|
|
654
|
+
entries.push({ scope: commentLineClass, start, end })
|
|
648
655
|
}
|
|
649
656
|
}
|
|
650
657
|
}
|
|
@@ -657,8 +664,8 @@ const buildShikiTokenOption = (chOpt, targetLang, themeOverride = '') => {
|
|
|
657
664
|
}
|
|
658
665
|
|
|
659
666
|
const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
|
|
660
|
-
const context = { token, md, env, option: chOpt }
|
|
661
667
|
if (chOpt.provider === 'custom') {
|
|
668
|
+
const context = { token, md, env, option: chOpt }
|
|
662
669
|
const getRanges = chOpt._customGetRanges
|
|
663
670
|
if (typeof getRanges !== 'function') throw new Error('customHighlight.getRanges must be a function')
|
|
664
671
|
const result = getRanges(token.content, lang, context)
|
|
@@ -666,6 +673,7 @@ const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
|
|
|
666
673
|
return normalizeCustomProviderRanges(result)
|
|
667
674
|
}
|
|
668
675
|
if (chOpt.provider === 'hljs') {
|
|
676
|
+
const context = { token, md, env, option: chOpt }
|
|
669
677
|
const highlightFn = chOpt._hljsHighlightFn
|
|
670
678
|
if (typeof highlightFn !== 'function') {
|
|
671
679
|
throw new Error('customHighlight.hljsHighlight (or customHighlight.highlight / md.options.highlight) must be a function when provider=hljs')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
escapeHtmlAttr,
|
|
2
3
|
escapeJsonForScript,
|
|
3
4
|
} from '../custom-highlight/payload-utils.js'
|
|
4
5
|
import {
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
renderFenceMarkup,
|
|
29
30
|
} from './render-markup.js'
|
|
30
31
|
|
|
31
|
-
const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold', 'line-notes'])
|
|
32
|
+
const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes'])
|
|
32
33
|
|
|
33
34
|
let fallbackCustomHighlightSeq = 0
|
|
34
35
|
|
|
@@ -49,8 +50,7 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
|
|
|
49
50
|
return preAttrs
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
|
|
53
|
-
const isSamp = opt._sampReg.test(lang)
|
|
53
|
+
const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
|
|
54
54
|
const tag = isSamp ? 'samp' : 'code'
|
|
55
55
|
let content = md.utils.escapeHtml(token.content)
|
|
56
56
|
const lineStrategy = opt.customHighlight.lineFeatureStrategy
|
|
@@ -92,8 +92,9 @@ const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emph
|
|
|
92
92
|
env[customHighlightDataEnvKey][id] = payload
|
|
93
93
|
} else {
|
|
94
94
|
const payloadJson = escapeJsonForScript(payload)
|
|
95
|
-
const scriptId = `pre-highlight-data-${id}`
|
|
96
|
-
|
|
95
|
+
const scriptId = escapeHtmlAttr(`pre-highlight-data-${id}`)
|
|
96
|
+
const escapedId = escapeHtmlAttr(id)
|
|
97
|
+
inlinePayloadScript = `<script type="application/json" id="${scriptId}" ${customHighlightPreAttr}="${escapedId}">${payloadJson}</script>\n`
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -119,6 +120,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
119
120
|
const commentMarkValue = context.commentMarkValue
|
|
120
121
|
const lineNotes = context.lineNotes
|
|
121
122
|
const lineNoteIdPrefix = context.lineNoteIdPrefix
|
|
123
|
+
const isSamp = context.isSamp
|
|
122
124
|
|
|
123
125
|
const apiDecisionBase = {
|
|
124
126
|
renderer: 'api',
|
|
@@ -130,12 +132,13 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
130
132
|
: [],
|
|
131
133
|
}
|
|
132
134
|
try {
|
|
133
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
|
|
135
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
|
|
134
136
|
if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
135
137
|
emitFenceDecision(opt, apiDecisionBase)
|
|
136
138
|
return html
|
|
137
139
|
} catch (err) {
|
|
138
|
-
if (
|
|
140
|
+
if (!shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) throw err
|
|
141
|
+
if (opt.customHighlight.fallback === 'plain') {
|
|
139
142
|
const fallbackDecision = {
|
|
140
143
|
renderer: 'api',
|
|
141
144
|
includePayload: false,
|
|
@@ -144,14 +147,27 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
144
147
|
reason: 'provider-error',
|
|
145
148
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
146
149
|
}
|
|
147
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
|
|
150
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
|
|
148
151
|
if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
149
152
|
emitFenceDecision(opt, fallbackDecision)
|
|
150
153
|
return html
|
|
151
154
|
}
|
|
155
|
+
const fallbackDecision = {
|
|
156
|
+
renderer: 'api',
|
|
157
|
+
includePayload: false,
|
|
158
|
+
fallbackUsed: true,
|
|
159
|
+
fallback: 'markup',
|
|
160
|
+
reason: 'provider-error',
|
|
161
|
+
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
162
|
+
}
|
|
163
|
+
const markupOpt = typeof opt.onFenceDecision === 'function'
|
|
164
|
+
? { ...opt, onFenceDecision: null }
|
|
165
|
+
: opt
|
|
166
|
+
const html = renderFenceMarkup(context, md, markupOpt, slf)
|
|
167
|
+
if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
168
|
+
emitFenceDecision(opt, fallbackDecision)
|
|
169
|
+
return html
|
|
152
170
|
}
|
|
153
|
-
|
|
154
|
-
return renderFenceMarkup(context, md, opt, slf)
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
export {
|
|
@@ -297,6 +297,20 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
297
297
|
break
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
if (sameRefs && prevState.blockCache instanceof Map) {
|
|
301
|
+
for (const [blockId, codeEl] of blockRefs) {
|
|
302
|
+
const cached = prevState.blockCache.get(blockId)
|
|
303
|
+
const textSnapshot = typeof codeEl.textContent === 'string' ? codeEl.textContent : null
|
|
304
|
+
const boundaryRefs = getCodeBoundaryRefs(codeEl)
|
|
305
|
+
if (!cached ||
|
|
306
|
+
cached.textSnapshot !== textSnapshot ||
|
|
307
|
+
cached.firstRef !== boundaryRefs.first ||
|
|
308
|
+
cached.lastRef !== boundaryRefs.last) {
|
|
309
|
+
sameRefs = false
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
300
314
|
if (sameRefs) {
|
|
301
315
|
if (emitDiag) emitDiag({ type: 'runtime-skip', reason: 'unchanged' })
|
|
302
316
|
return { appliedBlocks: 0, appliedRanges: 0, skipped: true, reason: 'unchanged' }
|
|
@@ -308,6 +322,7 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
308
322
|
let insertedScopeStyleMap = null
|
|
309
323
|
if (!incremental) clearAppliedHighlightNames(queryRoot)
|
|
310
324
|
const allScopeRanges = new Map()
|
|
325
|
+
const runtimeScopeNamesByVariant = new Map()
|
|
311
326
|
let appliedBlocks = 0
|
|
312
327
|
let appliedRanges = 0
|
|
313
328
|
let pendingCss = ''
|
|
@@ -342,17 +357,12 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
342
357
|
if (payloadDigestByBlock) {
|
|
343
358
|
blockPayloadDigest = payloadDigestByBlock.get(blockId)
|
|
344
359
|
if (blockPayloadDigest === undefined) {
|
|
345
|
-
|
|
346
|
-
if (prevCached && prevCached.payloadRef === payload && prevCached.variantKey === activePayload.variantKey) {
|
|
347
|
-
blockPayloadDigest = prevCached.payloadDigest || ''
|
|
348
|
-
} else {
|
|
349
|
-
blockPayloadDigest = getCachedPayloadString(payload, payloadStringifyCache) + `|variant=${activePayload.variantKey || ''}`
|
|
350
|
-
}
|
|
360
|
+
blockPayloadDigest = getCachedPayloadString(payload, payloadStringifyCache) + `|variant=${activePayload.variantKey || ''}`
|
|
351
361
|
payloadDigestByBlock.set(blockId, blockPayloadDigest)
|
|
352
362
|
}
|
|
353
363
|
}
|
|
354
|
-
const textSnapshot = typeof codeEl.textContent === 'string' ? codeEl.textContent : null
|
|
355
|
-
const boundaryRefs = getCodeBoundaryRefs(codeEl)
|
|
364
|
+
const textSnapshot = incremental && typeof codeEl.textContent === 'string' ? codeEl.textContent : null
|
|
365
|
+
const boundaryRefs = incremental ? getCodeBoundaryRefs(codeEl) : null
|
|
356
366
|
const cached = prevBlockCache ? prevBlockCache.get(blockId) : null
|
|
357
367
|
if (cached &&
|
|
358
368
|
cached.codeEl === codeEl &&
|
|
@@ -404,6 +414,11 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
404
414
|
const appliedNames = new Set()
|
|
405
415
|
const appliedNameList = []
|
|
406
416
|
const scopeRuntimeNames = new Array(activePayload.scopes.length)
|
|
417
|
+
let runtimeScopeNameCache = runtimeScopeNamesByVariant.get(activePayload.variantKey)
|
|
418
|
+
if (!runtimeScopeNameCache) {
|
|
419
|
+
runtimeScopeNameCache = new Map()
|
|
420
|
+
runtimeScopeNamesByVariant.set(activePayload.variantKey, runtimeScopeNameCache)
|
|
421
|
+
}
|
|
407
422
|
const scopeStyles = Array.isArray(activePayload.scopeStyles) ? activePayload.scopeStyles : null
|
|
408
423
|
const blockScopeRanges = nextBlockCache ? new Map() : null
|
|
409
424
|
const blockScopeMetaParts = nextBlockCache ? new Map() : null
|
|
@@ -448,7 +463,12 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
448
463
|
}
|
|
449
464
|
let runtimeName = scopeRuntimeNames[scopeIdx]
|
|
450
465
|
if (!runtimeName) {
|
|
451
|
-
|
|
466
|
+
const scopeCacheKey = String(scopeName)
|
|
467
|
+
runtimeName = runtimeScopeNameCache.get(scopeCacheKey)
|
|
468
|
+
if (!runtimeName) {
|
|
469
|
+
runtimeName = getRuntimeScopeName(scopeCacheKey, activePayload.variantKey)
|
|
470
|
+
runtimeScopeNameCache.set(scopeCacheKey, runtimeName)
|
|
471
|
+
}
|
|
452
472
|
scopeRuntimeNames[scopeIdx] = runtimeName
|
|
453
473
|
if (scopeStyles) {
|
|
454
474
|
const css = styleToHighlightCss(scopeStyles[scopeIdx])
|
|
@@ -514,7 +534,6 @@ const applyCustomHighlights = (root, options = {}) => {
|
|
|
514
534
|
if (nextBlockCache) {
|
|
515
535
|
nextBlockCache.set(blockId, {
|
|
516
536
|
codeEl,
|
|
517
|
-
payloadRef: payload,
|
|
518
537
|
variantKey: activePayload.variantKey || '',
|
|
519
538
|
payloadDigest: blockPayloadDigest,
|
|
520
539
|
textSnapshot,
|
package/src/fence/render-api.js
CHANGED
|
@@ -40,6 +40,9 @@ const shouldRuntimeFallback = (reason, opt = {}) => {
|
|
|
40
40
|
return shouldApplyApiFallbackForReason(chOpt, reason)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const customHighlightEnvInitInstalledKey = '__rendererFenceCustomHighlightEnvInitInstalled'
|
|
44
|
+
const customHighlightEnvInitOptionKey = '__rendererFenceCustomHighlightEnvInitOption'
|
|
45
|
+
|
|
43
46
|
const mditRendererFenceCustomHighlight = (md, option) => {
|
|
44
47
|
const opt = {
|
|
45
48
|
...createCommonFenceOptionDefaults(md),
|
|
@@ -69,7 +72,7 @@ const mditRendererFenceCustomHighlight = (md, option) => {
|
|
|
69
72
|
opt.customHighlight._hasShikiHighlighter = !!(opt.customHighlight.highlighter && typeof opt.customHighlight.highlighter.codeToTokens === 'function')
|
|
70
73
|
const tokenOptionBase = {}
|
|
71
74
|
if (opt.customHighlight._singleTheme) tokenOptionBase.theme = opt.customHighlight._singleTheme
|
|
72
|
-
if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === '
|
|
75
|
+
if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === 'role') {
|
|
73
76
|
tokenOptionBase.includeExplanation = 'scopeName'
|
|
74
77
|
}
|
|
75
78
|
opt.customHighlight._shikiTokenOptionBase = Object.keys(tokenOptionBase).length ? tokenOptionBase : null
|
|
@@ -78,17 +81,22 @@ const mditRendererFenceCustomHighlight = (md, option) => {
|
|
|
78
81
|
finalizeCommonFenceOption(opt)
|
|
79
82
|
installLineNotesCoreRule(md)
|
|
80
83
|
|
|
81
|
-
md
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
env
|
|
86
|
-
env
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
md[customHighlightEnvInitOptionKey] = opt.customHighlight
|
|
85
|
+
if (!md[customHighlightEnvInitInstalledKey]) {
|
|
86
|
+
md[customHighlightEnvInitInstalledKey] = true
|
|
87
|
+
md.core.ruler.before('block', customHighlightEnvInitRuleName, (state) => {
|
|
88
|
+
const env = state && state.env
|
|
89
|
+
if (!env || typeof env !== 'object') return
|
|
90
|
+
const chOpt = md[customHighlightEnvInitOptionKey]
|
|
91
|
+
if (chOpt && chOpt.transport === 'env') {
|
|
92
|
+
env[customHighlightDataEnvKey] = {}
|
|
93
|
+
env[customHighlightSeqEnvKey] = 0
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
delete env[customHighlightDataEnvKey]
|
|
97
|
+
delete env[customHighlightSeqEnvKey]
|
|
98
|
+
})
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
|
94
102
|
const context = prepareFenceRenderContext(tokens, idx, opt)
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
commentLineClass,
|
|
9
9
|
emitFenceDecision,
|
|
10
10
|
finalizeFenceTimings,
|
|
11
|
+
getCommentLineFlags,
|
|
11
12
|
getLogicalLineCount,
|
|
12
13
|
getNowMs,
|
|
13
14
|
normalizeEmphasisRanges,
|
|
@@ -22,7 +23,8 @@ import {
|
|
|
22
23
|
parsePreCodeWrapper,
|
|
23
24
|
} from '../utils/pre-code-wrapper-parser.js'
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const markupHighlightPreDisabledFeatures = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes', 'samp'])
|
|
27
|
+
const markupPassthroughDisabledFeatures = Object.freeze([...markupHighlightPreDisabledFeatures, 'wrap', 'setPreWrapStyle'])
|
|
26
28
|
|
|
27
29
|
const renderFenceMarkup = (context, md, opt, slf) => {
|
|
28
30
|
const token = context.token
|
|
@@ -40,7 +42,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
40
42
|
const lineNotes = context.lineNotes
|
|
41
43
|
const lineNoteIdPrefix = context.lineNoteIdPrefix
|
|
42
44
|
|
|
43
|
-
const isSamp =
|
|
45
|
+
const isSamp = context.isSamp
|
|
44
46
|
const sourceContent = token.content
|
|
45
47
|
let content = sourceContent
|
|
46
48
|
let commentLines
|
|
@@ -113,41 +115,48 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
113
115
|
|
|
114
116
|
const useHighlightPre = opt.useHighlightPre && hasHighlightPre
|
|
115
117
|
const needLineNumber = !useHighlightPre && opt.setLineNumber && startNumber >= 0
|
|
118
|
+
const wantsEmphasis = !useHighlightPre && opt.setEmphasizeLines && emphasizeLines.length > 0
|
|
119
|
+
const wantsAdvancedLineNumbers = needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)
|
|
120
|
+
const wantsLineNotes = !useHighlightPre && !!(lineNotes && lineNotes.length)
|
|
121
|
+
const hasCommentCandidate = !!(
|
|
122
|
+
!useHighlightPre &&
|
|
123
|
+
commentMarkValue &&
|
|
124
|
+
sourceContent.indexOf(commentMarkValue) !== -1
|
|
125
|
+
)
|
|
126
|
+
const needsMappedLineCounts = wantsAdvancedLineNumbers || wantsLineNotes || hasCommentCandidate
|
|
116
127
|
let sourceLogicalLineCount = -1
|
|
117
128
|
let highlightedLogicalLineCount = -1
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
if (wantsEmphasis || needsMappedLineCounts) highlightedLogicalLineCount = getLogicalLineCount(content)
|
|
130
|
+
if (needsMappedLineCounts) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
|
|
131
|
+
const highlightedHasTrailingBreak = content.endsWith('\n') || content.endsWith('\r')
|
|
132
|
+
const needsLastLineWrapCheck = !highlightedHasTrailingBreak && (needLineNumber || wantsEmphasis || wantsLineNotes)
|
|
133
|
+
if (needsLastLineWrapCheck && highlightedLogicalLineCount < 0) highlightedLogicalLineCount = getLogicalLineCount(content)
|
|
134
|
+
if (needsLastLineWrapCheck && sourceLogicalLineCount < 0) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
|
|
135
|
+
const wrapLastLine = !!(
|
|
136
|
+
needsLastLineWrapCheck &&
|
|
137
|
+
highlightedLogicalLineCount > 0 &&
|
|
138
|
+
highlightedLogicalLineCount === sourceLogicalLineCount
|
|
139
|
+
)
|
|
140
|
+
|
|
122
141
|
let normalizedEmphasis = []
|
|
123
|
-
if (
|
|
124
|
-
ensureLogicalLineCounts()
|
|
142
|
+
if (wantsEmphasis) {
|
|
125
143
|
normalizedEmphasis = normalizeEmphasisRanges(emphasizeLines, highlightedLogicalLineCount)
|
|
126
144
|
}
|
|
127
145
|
let lineNumberPlan = null
|
|
128
|
-
if (
|
|
129
|
-
ensureLogicalLineCounts()
|
|
146
|
+
if (wantsAdvancedLineNumbers) {
|
|
130
147
|
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, sourceLogicalLineCount, highlightedLogicalLineCount)
|
|
131
148
|
}
|
|
132
149
|
let lineNotePlan = null
|
|
133
|
-
if (
|
|
134
|
-
ensureLogicalLineCounts()
|
|
150
|
+
if (wantsLineNotes) {
|
|
135
151
|
lineNotePlan = resolveLineNotesPlan(lineNotes, sourceLogicalLineCount, highlightedLogicalLineCount, lineNoteIdPrefix)
|
|
136
152
|
}
|
|
137
153
|
const needEmphasis = normalizedEmphasis.length > 0
|
|
138
154
|
const needEndSpan = opt.lineEndSpanThreshold > 0
|
|
139
155
|
|
|
140
|
-
if (
|
|
141
|
-
ensureLogicalLineCounts()
|
|
156
|
+
if (hasCommentCandidate) {
|
|
142
157
|
if (highlightedLogicalLineCount === sourceLogicalLineCount) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (rawLines[i].trimStart().startsWith(commentMarkValue)) {
|
|
146
|
-
if (!commentLines) commentLines = []
|
|
147
|
-
commentLines[i] = true
|
|
148
|
-
needComment = true
|
|
149
|
-
}
|
|
150
|
-
}
|
|
158
|
+
commentLines = getCommentLineFlags(sourceContent, commentMarkValue)
|
|
159
|
+
needComment = !!commentLines
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -168,16 +177,17 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
168
177
|
commentLineClass,
|
|
169
178
|
lineNumberPlan,
|
|
170
179
|
lineNotePlan,
|
|
180
|
+
wrapLastLine,
|
|
171
181
|
)
|
|
172
182
|
if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
const tag = isSamp ? 'samp' : 'code'
|
|
185
|
+
const tag = useHighlightPre ? 'code' : (isSamp ? 'samp' : 'code')
|
|
176
186
|
const decision = {
|
|
177
187
|
renderer: 'markup',
|
|
178
188
|
useHighlightPre,
|
|
179
189
|
hasHighlightPre,
|
|
180
|
-
disabledFeatures: useHighlightPre ?
|
|
190
|
+
disabledFeatures: useHighlightPre ? markupHighlightPreDisabledFeatures : [],
|
|
181
191
|
}
|
|
182
192
|
if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
183
193
|
emitFenceDecision(opt, decision)
|