@peaceroad/markdown-it-renderer-fence 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -471
- package/docs/README.md +37 -0
- package/docs/code-highlighting-design.md +317 -0
- package/docs/custom-highlight-styling-guide.md +204 -0
- package/package.json +21 -11
- package/src/custom-highlight/option-validator.js +5 -5
- package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +229 -44
- package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +64 -56
- package/src/custom-highlight/shiki-theme-role.js +176 -0
- package/src/fence/line-notes.js +27 -9
- package/src/fence/render-api-provider.js +59 -50
- package/src/fence/render-api-renderer.js +7 -6
- package/src/fence/render-api.js +20 -12
- package/src/fence/render-markup.js +4 -9
- package/src/fence/render-shared.js +78 -8
- package/theme/_shared/rf-core.css +164 -0
- package/theme/rf-basic/README.md +91 -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 +3 -0
- package/theme/rf-basic/index.css +7 -0
- package/theme/rf-basic/index.js +155 -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 +71 -0
- package/theme/rf-monochrome/base.css +3 -0
- package/theme/rf-monochrome/index.css +5 -0
- package/theme/rf-monochrome/index.js +72 -0
- package/theme/rf-monochrome/markup/highlightjs.css +134 -0
- package/theme/rf-monochrome/markup/shiki.css +27 -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,19 +273,20 @@ 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) => {
|
|
@@ -459,21 +461,21 @@ const hasShikiOffsets = (tokenLines) => {
|
|
|
459
461
|
return false
|
|
460
462
|
}
|
|
461
463
|
|
|
462
|
-
const getShikiScopeName = (tok, lang, style, opt,
|
|
464
|
+
const getShikiScopeName = (tok, lang, style, opt, preResolvedRoleLang = '') => {
|
|
463
465
|
const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
|
|
464
466
|
const rawScope = getShikiRawScopeName(tok)
|
|
465
|
-
let
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
467
|
+
let customRoleName = null
|
|
468
|
+
const isRoleScopeMode = scopeMode === 'role'
|
|
469
|
+
if (isRoleScopeMode && opt && typeof opt.shikiRoleClassifier === 'function') {
|
|
468
470
|
try {
|
|
469
|
-
const customName = opt.
|
|
470
|
-
if (customName != null && customName !== '')
|
|
471
|
+
const customName = opt.shikiRoleClassifier(rawScope, tok, { lang, style, scopeMode })
|
|
472
|
+
if (customName != null && customName !== '') customRoleName = String(customName)
|
|
471
473
|
} catch (e) {}
|
|
472
474
|
}
|
|
473
|
-
if (
|
|
474
|
-
if (
|
|
475
|
-
const bucket =
|
|
476
|
-
return 'shiki-' + bucket
|
|
475
|
+
if (isRoleScopeMode) {
|
|
476
|
+
if (customRoleName) return 'shiki-role-' + customRoleName
|
|
477
|
+
const bucket = classifyShikiScopeRole(rawScope, tok, lang, opt, preResolvedRoleLang)
|
|
478
|
+
return 'shiki-role-' + bucket
|
|
477
479
|
}
|
|
478
480
|
if (scopeMode === 'semantic') {
|
|
479
481
|
if (rawScope) return 'shiki-' + rawScope
|
|
@@ -515,11 +517,11 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
515
517
|
scopeMode === 'auto' ||
|
|
516
518
|
scopeMode === 'color' ||
|
|
517
519
|
scopeMode === 'semantic' ||
|
|
518
|
-
(scopeMode === '
|
|
520
|
+
(scopeMode === 'role' && !!(opt && typeof opt.shikiRoleClassifier === 'function'))
|
|
519
521
|
const needTokenStyle = includeScopeStyles || needStyleForScopeName
|
|
520
|
-
let
|
|
521
|
-
if (scopeMode === '
|
|
522
|
-
|
|
522
|
+
let preResolvedRoleLang = ''
|
|
523
|
+
if (scopeMode === 'role' && (!opt || typeof opt.shikiRoleLangResolver !== 'function')) {
|
|
524
|
+
preResolvedRoleLang = resolveShikiRoleLangForFence(lang, opt)
|
|
523
525
|
}
|
|
524
526
|
let cursor = 0
|
|
525
527
|
for (let i = 0; i < tokenLines.length; i++) {
|
|
@@ -534,7 +536,7 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
534
536
|
const start = hasOffset ? tok.offset : cursor
|
|
535
537
|
const end = start + content.length
|
|
536
538
|
const style = needTokenStyle ? getShikiTokenStyle(tok) : null
|
|
537
|
-
const scope = getShikiScopeName(tok, lang, style, opt,
|
|
539
|
+
const scope = getShikiScopeName(tok, lang, style, opt, preResolvedRoleLang)
|
|
538
540
|
entries.push({ scope, start, end, style })
|
|
539
541
|
cursor = end
|
|
540
542
|
}
|
|
@@ -551,6 +553,7 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
551
553
|
const entries = []
|
|
552
554
|
for (const range of source.ranges) {
|
|
553
555
|
let scope
|
|
556
|
+
let scopeIndex = null
|
|
554
557
|
let start
|
|
555
558
|
let end
|
|
556
559
|
let style
|
|
@@ -567,10 +570,11 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
567
570
|
} else {
|
|
568
571
|
continue
|
|
569
572
|
}
|
|
570
|
-
if (
|
|
573
|
+
if (Number.isSafeInteger(scope)) scopeIndex = scope
|
|
574
|
+
if (scopeIndex != null && scopes && scopes[scopeIndex] != null) scope = scopes[scopeIndex]
|
|
571
575
|
if (scope == null) continue
|
|
572
576
|
if (!style && scopeStyles) {
|
|
573
|
-
if (
|
|
577
|
+
if (scopeIndex != null && Array.isArray(scopeStyles)) style = scopeStyles[scopeIndex]
|
|
574
578
|
else style = scopeStyles[String(scope)]
|
|
575
579
|
}
|
|
576
580
|
entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
|
|
@@ -609,7 +613,12 @@ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeS
|
|
|
609
613
|
}
|
|
610
614
|
scopeKeyToIndex.set(key, scopeIndex)
|
|
611
615
|
}
|
|
612
|
-
ranges.
|
|
616
|
+
const previousRange = ranges.length ? ranges[ranges.length - 1] : null
|
|
617
|
+
if (previousRange && previousRange[0] === scopeIndex && previousRange[2] === start) {
|
|
618
|
+
previousRange[2] = end
|
|
619
|
+
} else {
|
|
620
|
+
ranges.push([scopeIndex, start, end])
|
|
621
|
+
}
|
|
613
622
|
}
|
|
614
623
|
const payload = {
|
|
615
624
|
v: customHighlightPayloadSchemaVersion,
|
|
@@ -629,8 +638,8 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
629
638
|
const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
|
|
630
639
|
const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
|
|
631
640
|
if (!needsEmphasis && !needsCommentScan) return
|
|
632
|
-
const
|
|
633
|
-
const maxLine =
|
|
641
|
+
const offsets = needsEmphasis ? getLogicalLineOffsets(content) : null
|
|
642
|
+
const maxLine = offsets ? offsets.length : 0
|
|
634
643
|
if (needsEmphasis) {
|
|
635
644
|
const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
|
|
636
645
|
for (const [s, e] of normalized) {
|
|
@@ -640,11 +649,11 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
640
649
|
}
|
|
641
650
|
}
|
|
642
651
|
if (needsCommentScan) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
652
|
+
const ranges = getCommentLineRanges(content, commentMarkValue)
|
|
653
|
+
if (!ranges) return
|
|
654
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
655
|
+
const [start, end] = ranges[i]
|
|
656
|
+
entries.push({ scope: commentLineClass, start, end })
|
|
648
657
|
}
|
|
649
658
|
}
|
|
650
659
|
}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
renderFenceMarkup,
|
|
29
29
|
} from './render-markup.js'
|
|
30
30
|
|
|
31
|
-
const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold', 'line-notes'])
|
|
31
|
+
const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes'])
|
|
32
32
|
|
|
33
33
|
let fallbackCustomHighlightSeq = 0
|
|
34
34
|
|
|
@@ -49,8 +49,7 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
|
|
|
49
49
|
return preAttrs
|
|
50
50
|
}
|
|
51
51
|
|
|
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)
|
|
52
|
+
const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
|
|
54
53
|
const tag = isSamp ? 'samp' : 'code'
|
|
55
54
|
let content = md.utils.escapeHtml(token.content)
|
|
56
55
|
const lineStrategy = opt.customHighlight.lineFeatureStrategy
|
|
@@ -119,6 +118,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
119
118
|
const commentMarkValue = context.commentMarkValue
|
|
120
119
|
const lineNotes = context.lineNotes
|
|
121
120
|
const lineNoteIdPrefix = context.lineNoteIdPrefix
|
|
121
|
+
const isSamp = context.isSamp
|
|
122
122
|
|
|
123
123
|
const apiDecisionBase = {
|
|
124
124
|
renderer: 'api',
|
|
@@ -130,12 +130,13 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
130
130
|
: [],
|
|
131
131
|
}
|
|
132
132
|
try {
|
|
133
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
|
|
133
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
|
|
134
134
|
if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
135
135
|
emitFenceDecision(opt, apiDecisionBase)
|
|
136
136
|
return html
|
|
137
137
|
} catch (err) {
|
|
138
|
-
if (
|
|
138
|
+
if (!shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) throw err
|
|
139
|
+
if (opt.customHighlight.fallback === 'plain') {
|
|
139
140
|
const fallbackDecision = {
|
|
140
141
|
renderer: 'api',
|
|
141
142
|
includePayload: false,
|
|
@@ -144,7 +145,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
144
145
|
reason: 'provider-error',
|
|
145
146
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
146
147
|
}
|
|
147
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
|
|
148
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
|
|
148
149
|
if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
149
150
|
emitFenceDecision(opt, fallbackDecision)
|
|
150
151
|
return html
|
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,
|
|
@@ -40,7 +41,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
40
41
|
const lineNotes = context.lineNotes
|
|
41
42
|
const lineNoteIdPrefix = context.lineNoteIdPrefix
|
|
42
43
|
|
|
43
|
-
const isSamp =
|
|
44
|
+
const isSamp = context.isSamp
|
|
44
45
|
const sourceContent = token.content
|
|
45
46
|
let content = sourceContent
|
|
46
47
|
let commentLines
|
|
@@ -140,14 +141,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
140
141
|
if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
|
|
141
142
|
ensureLogicalLineCounts()
|
|
142
143
|
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
|
-
}
|
|
144
|
+
commentLines = getCommentLineFlags(sourceContent, commentMarkValue)
|
|
145
|
+
needComment = !!commentLines
|
|
151
146
|
}
|
|
152
147
|
}
|
|
153
148
|
|
|
@@ -103,7 +103,13 @@ const createCommonFenceOptionDefaults = (md) => {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const finalizeCommonFenceOption = (opt) => {
|
|
106
|
-
|
|
106
|
+
const sampLangs = ['samp']
|
|
107
|
+
const extraSampLangs = String(opt.sampLang ?? '').split(',')
|
|
108
|
+
for (let i = 0; i < extraSampLangs.length; i++) {
|
|
109
|
+
const lang = extraSampLangs[i].trim()
|
|
110
|
+
if (lang) sampLangs.push(lang)
|
|
111
|
+
}
|
|
112
|
+
opt._sampLangSet = new Set(sampLangs)
|
|
107
113
|
opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
|
|
108
114
|
opt._attrOrderRankCache = new Map()
|
|
109
115
|
return opt
|
|
@@ -132,6 +138,8 @@ const parsePositiveLineIndex = (val) => {
|
|
|
132
138
|
return num
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
const isTruthyAttrValue = (val) => val !== 'false' && val !== '0'
|
|
142
|
+
|
|
135
143
|
const escapeHtmlText = (value) => {
|
|
136
144
|
return String(value ?? '')
|
|
137
145
|
.replace(/&/g, '&')
|
|
@@ -154,6 +162,62 @@ const getLogicalLineCount = (text) => {
|
|
|
154
162
|
return count
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
const isLineStartWhitespaceCode = (code) => {
|
|
166
|
+
return code <= 32 || code === 160 || code === 0xFEFF
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const lineStartsWithAfterIndent = (str, start, end, marker) => {
|
|
170
|
+
let pos = start
|
|
171
|
+
while (pos < end && isLineStartWhitespaceCode(str.charCodeAt(pos))) pos++
|
|
172
|
+
return str.startsWith(marker, pos)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const scanLogicalLineBounds = (str, onLine) => {
|
|
176
|
+
let lineStart = 0
|
|
177
|
+
let lineIndex = 0
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i <= str.length; i++) {
|
|
180
|
+
const code = i < str.length ? str.charCodeAt(i) : 10
|
|
181
|
+
if (code !== 10 && code !== 13) continue
|
|
182
|
+
onLine(lineStart, i, lineIndex)
|
|
183
|
+
if (code === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
|
|
184
|
+
lineStart = i + 1
|
|
185
|
+
lineIndex++
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const getCommentLineFlags = (text, commentMarkValue) => {
|
|
190
|
+
const str = String(text ?? '')
|
|
191
|
+
const marker = String(commentMarkValue ?? '')
|
|
192
|
+
if (!str || !marker) return null
|
|
193
|
+
const flags = []
|
|
194
|
+
let hasComment = false
|
|
195
|
+
|
|
196
|
+
scanLogicalLineBounds(str, (lineStart, lineEnd, lineIndex) => {
|
|
197
|
+
if (lineStartsWithAfterIndent(str, lineStart, lineEnd, marker)) {
|
|
198
|
+
flags[lineIndex] = true
|
|
199
|
+
hasComment = true
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
return hasComment ? flags : null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const getCommentLineRanges = (text, commentMarkValue) => {
|
|
207
|
+
const str = String(text ?? '')
|
|
208
|
+
const marker = String(commentMarkValue ?? '')
|
|
209
|
+
if (!str || !marker) return null
|
|
210
|
+
const ranges = []
|
|
211
|
+
|
|
212
|
+
scanLogicalLineBounds(str, (lineStart, lineEnd) => {
|
|
213
|
+
if (lineEnd > lineStart && lineStartsWithAfterIndent(str, lineStart, lineEnd, marker)) {
|
|
214
|
+
ranges.push([lineStart, lineEnd])
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
return ranges.length ? ranges : null
|
|
219
|
+
}
|
|
220
|
+
|
|
157
221
|
const parseLineRangeList = (attrVal, parseValue) => {
|
|
158
222
|
const str = String(attrVal ?? '')
|
|
159
223
|
if (!str) return []
|
|
@@ -179,14 +243,8 @@ const parseLineRangeList = (attrVal, parseValue) => {
|
|
|
179
243
|
return result
|
|
180
244
|
}
|
|
181
245
|
|
|
182
|
-
const parsePositiveLineNumber = (val) => {
|
|
183
|
-
const num = Number(val)
|
|
184
|
-
if (!Number.isFinite(num) || num <= 0) return null
|
|
185
|
-
return num
|
|
186
|
-
}
|
|
187
|
-
|
|
188
246
|
const getEmphasizeLines = (attrVal) => {
|
|
189
|
-
return parseLineRangeList(attrVal,
|
|
247
|
+
return parseLineRangeList(attrVal, parsePositiveLineIndex)
|
|
190
248
|
}
|
|
191
249
|
|
|
192
250
|
const getLineNumberSkipRanges = (attrVal) => {
|
|
@@ -537,6 +595,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
537
595
|
let wrapEnabled = false
|
|
538
596
|
let preWrapValue
|
|
539
597
|
let commentMarkValue
|
|
598
|
+
let tagOverride = ''
|
|
540
599
|
const lineNotes = getTokenLineNotes(token)
|
|
541
600
|
const lineNoteIdPrefix = lineNotes && lineNotes.length ? getLineNoteIdPrefix(token, idx) : ''
|
|
542
601
|
const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
|
|
@@ -592,6 +651,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
592
651
|
}
|
|
593
652
|
break
|
|
594
653
|
case 'data-pre-emphasis':
|
|
654
|
+
if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
|
|
595
655
|
dataPreEmphasisIndex = newAttrs.length
|
|
596
656
|
newAttrs.push(attr)
|
|
597
657
|
break
|
|
@@ -628,6 +688,12 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
628
688
|
sawCommentMark = true
|
|
629
689
|
}
|
|
630
690
|
break
|
|
691
|
+
case 'samp':
|
|
692
|
+
tagOverride = isTruthyAttrValue(val) ? 'samp' : ''
|
|
693
|
+
break
|
|
694
|
+
case 'code':
|
|
695
|
+
tagOverride = isTruthyAttrValue(val) ? 'code' : ''
|
|
696
|
+
break
|
|
631
697
|
case 'line-number-skip':
|
|
632
698
|
case 'pre-line-number-skip':
|
|
633
699
|
lineNumberSkipValue = val
|
|
@@ -695,10 +761,12 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
695
761
|
|
|
696
762
|
if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
|
|
697
763
|
if (lineNotes && lineNotes.length) token.attrSet('data-pre-line-notes', 'true')
|
|
764
|
+
const isSamp = tagOverride ? tagOverride === 'samp' : opt._sampLangSet.has(lang)
|
|
698
765
|
|
|
699
766
|
return {
|
|
700
767
|
token,
|
|
701
768
|
lang,
|
|
769
|
+
isSamp,
|
|
702
770
|
startNumber,
|
|
703
771
|
emphasizeLines,
|
|
704
772
|
lineNumberSkipValue,
|
|
@@ -722,6 +790,8 @@ export {
|
|
|
722
790
|
emitFenceDecision,
|
|
723
791
|
finalizeCommonFenceOption,
|
|
724
792
|
finalizeFenceTimings,
|
|
793
|
+
getCommentLineFlags,
|
|
794
|
+
getCommentLineRanges,
|
|
725
795
|
getLogicalLineCount,
|
|
726
796
|
getNowMs,
|
|
727
797
|
getEmphasizeLines,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*! Renderer Fence shared base | MIT License */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
color-scheme: light dark;
|
|
5
|
+
|
|
6
|
+
--rf-code-bg: oklch(99.4% 0 0);
|
|
7
|
+
--rf-code-fg: oklch(4% 0 0);
|
|
8
|
+
--rf-code-muted: oklch(42% 0 0);
|
|
9
|
+
--rf-code-border: oklch(86% 0 0);
|
|
10
|
+
|
|
11
|
+
--rf-samp-bg: oklch(18% 0 0);
|
|
12
|
+
--rf-samp-fg: oklch(95% 0 0);
|
|
13
|
+
--rf-samp-muted: oklch(76% 0 0);
|
|
14
|
+
--rf-samp-border: oklch(41% 0 0);
|
|
15
|
+
--rf-samp-prompt: oklch(78% 0.13 285);
|
|
16
|
+
|
|
17
|
+
--rf-accent: oklch(50% 0.18 285);
|
|
18
|
+
--rf-accent-soft: oklch(94% 0.035 285);
|
|
19
|
+
--rf-accent-border: oklch(82% 0.08 285);
|
|
20
|
+
|
|
21
|
+
--rf-syntax-keyword: oklch(38% 0.255 25);
|
|
22
|
+
--rf-syntax-string: oklch(34% 0.235 250);
|
|
23
|
+
--rf-syntax-number: oklch(36% 0.245 260);
|
|
24
|
+
--rf-syntax-comment: oklch(39% 0.026 255);
|
|
25
|
+
--rf-syntax-type: oklch(38% 0.235 295);
|
|
26
|
+
--rf-syntax-title: oklch(39% 0.27 305);
|
|
27
|
+
--rf-syntax-variable: oklch(37% 0.205 80);
|
|
28
|
+
--rf-syntax-literal: oklch(36% 0.245 260);
|
|
29
|
+
--rf-syntax-tag: oklch(34% 0.18 150);
|
|
30
|
+
--rf-syntax-attribute: oklch(36% 0.23 260);
|
|
31
|
+
--rf-syntax-punctuation: oklch(12% 0.018 255);
|
|
32
|
+
--rf-syntax-meta: oklch(12% 0.018 255);
|
|
33
|
+
|
|
34
|
+
--rf-samp-syntax-keyword: oklch(78% 0.08 25);
|
|
35
|
+
--rf-samp-syntax-string: oklch(82% 0.055 250);
|
|
36
|
+
--rf-samp-syntax-number: oklch(80% 0.07 255);
|
|
37
|
+
--rf-samp-syntax-comment: oklch(75% 0.01 255);
|
|
38
|
+
--rf-samp-syntax-type: oklch(81% 0.06 295);
|
|
39
|
+
--rf-samp-syntax-title: oklch(82% 0.07 305);
|
|
40
|
+
--rf-samp-syntax-variable: oklch(82% 0.075 90);
|
|
41
|
+
--rf-samp-syntax-literal: oklch(80% 0.07 255);
|
|
42
|
+
--rf-samp-syntax-tag: oklch(81% 0.065 150);
|
|
43
|
+
--rf-samp-syntax-attribute: oklch(80% 0.07 255);
|
|
44
|
+
--rf-samp-syntax-punctuation: oklch(86% 0.012 255);
|
|
45
|
+
|
|
46
|
+
--rf-line-emphasis-bg: color-mix(in oklch, var(--rf-accent-soft) 70%, transparent);
|
|
47
|
+
--rf-line-number-fg: var(--rf-code-muted);
|
|
48
|
+
--rf-line-number-rule: color-mix(in oklch, var(--rf-code-border) 85%, transparent);
|
|
49
|
+
--rf-line-comment-bg: color-mix(in oklch, var(--rf-syntax-comment) 12%, transparent);
|
|
50
|
+
--rf-line-note-bg: oklch(100% 0 0);
|
|
51
|
+
--rf-line-note-border: var(--rf-accent-border);
|
|
52
|
+
--rf-line-note-fg: var(--rf-code-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root {
|
|
57
|
+
--rf-code-bg: oklch(18% 0 0);
|
|
58
|
+
--rf-code-fg: oklch(95% 0 0);
|
|
59
|
+
--rf-code-muted: oklch(76% 0 0);
|
|
60
|
+
--rf-code-border: oklch(38% 0 0);
|
|
61
|
+
|
|
62
|
+
--rf-samp-bg: oklch(99.4% 0 0);
|
|
63
|
+
--rf-samp-fg: oklch(4% 0 0);
|
|
64
|
+
--rf-samp-muted: oklch(42% 0 0);
|
|
65
|
+
--rf-samp-border: oklch(86% 0 0);
|
|
66
|
+
--rf-samp-prompt: oklch(50% 0.18 285);
|
|
67
|
+
|
|
68
|
+
--rf-accent: oklch(78% 0.13 285);
|
|
69
|
+
--rf-accent-soft: oklch(27% 0.05 285);
|
|
70
|
+
--rf-accent-border: oklch(48% 0.11 285);
|
|
71
|
+
|
|
72
|
+
--rf-syntax-keyword: oklch(78% 0.08 25);
|
|
73
|
+
--rf-syntax-string: oklch(82% 0.055 250);
|
|
74
|
+
--rf-syntax-number: oklch(80% 0.07 255);
|
|
75
|
+
--rf-syntax-comment: oklch(75% 0.01 255);
|
|
76
|
+
--rf-syntax-type: oklch(81% 0.06 295);
|
|
77
|
+
--rf-syntax-title: oklch(82% 0.07 305);
|
|
78
|
+
--rf-syntax-variable: oklch(82% 0.075 90);
|
|
79
|
+
--rf-syntax-literal: oklch(80% 0.07 255);
|
|
80
|
+
--rf-syntax-tag: oklch(81% 0.065 150);
|
|
81
|
+
--rf-syntax-attribute: oklch(80% 0.07 255);
|
|
82
|
+
--rf-syntax-punctuation: oklch(86% 0.012 255);
|
|
83
|
+
--rf-syntax-meta: oklch(86% 0.012 255);
|
|
84
|
+
|
|
85
|
+
--rf-samp-syntax-keyword: oklch(38% 0.255 25);
|
|
86
|
+
--rf-samp-syntax-string: oklch(34% 0.235 250);
|
|
87
|
+
--rf-samp-syntax-number: oklch(36% 0.245 260);
|
|
88
|
+
--rf-samp-syntax-comment: oklch(39% 0.026 255);
|
|
89
|
+
--rf-samp-syntax-type: oklch(38% 0.235 295);
|
|
90
|
+
--rf-samp-syntax-title: oklch(39% 0.27 305);
|
|
91
|
+
--rf-samp-syntax-variable: oklch(37% 0.205 80);
|
|
92
|
+
--rf-samp-syntax-literal: oklch(36% 0.245 260);
|
|
93
|
+
--rf-samp-syntax-tag: oklch(34% 0.18 150);
|
|
94
|
+
--rf-samp-syntax-attribute: oklch(36% 0.23 260);
|
|
95
|
+
--rf-samp-syntax-punctuation: oklch(12% 0.018 255);
|
|
96
|
+
|
|
97
|
+
--rf-line-emphasis-bg: color-mix(in oklch, var(--rf-accent-soft) 70%, transparent);
|
|
98
|
+
--rf-line-number-fg: var(--rf-code-muted);
|
|
99
|
+
--rf-line-number-rule: color-mix(in oklch, var(--rf-code-border) 85%, transparent);
|
|
100
|
+
--rf-line-comment-bg: color-mix(in oklch, var(--rf-syntax-comment) 16%, transparent);
|
|
101
|
+
--rf-line-note-bg: oklch(24% 0.01 255);
|
|
102
|
+
--rf-line-note-border: var(--rf-accent-border);
|
|
103
|
+
--rf-line-note-fg: var(--rf-code-muted);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pre {
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 1rem;
|
|
110
|
+
overflow: auto;
|
|
111
|
+
color: var(--rf-code-fg);
|
|
112
|
+
background: var(--rf-code-bg);
|
|
113
|
+
border: 1px solid var(--rf-code-border);
|
|
114
|
+
border-radius: 0.5rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pre code,
|
|
118
|
+
pre samp {
|
|
119
|
+
color: inherit;
|
|
120
|
+
background: transparent;
|
|
121
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.pre-lines-emphasis {
|
|
125
|
+
background: var(--rf-line-emphasis-bg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.pre-line-comment {
|
|
129
|
+
color: var(--rf-syntax-comment);
|
|
130
|
+
background: var(--rf-line-comment-bg);
|
|
131
|
+
padding-block: 0.04em;
|
|
132
|
+
line-height: 1.45;
|
|
133
|
+
-webkit-box-decoration-break: clone;
|
|
134
|
+
box-decoration-break: clone;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pre:has(> samp) {
|
|
138
|
+
--rf-syntax-keyword: var(--rf-samp-syntax-keyword);
|
|
139
|
+
--rf-syntax-string: var(--rf-samp-syntax-string);
|
|
140
|
+
--rf-syntax-number: var(--rf-samp-syntax-number);
|
|
141
|
+
--rf-syntax-comment: var(--rf-samp-syntax-comment);
|
|
142
|
+
--rf-syntax-type: var(--rf-samp-syntax-type);
|
|
143
|
+
--rf-syntax-title: var(--rf-samp-syntax-title);
|
|
144
|
+
--rf-syntax-variable: var(--rf-samp-syntax-variable);
|
|
145
|
+
--rf-syntax-literal: var(--rf-samp-syntax-literal);
|
|
146
|
+
--rf-syntax-tag: var(--rf-samp-syntax-tag);
|
|
147
|
+
--rf-syntax-attribute: var(--rf-samp-syntax-attribute);
|
|
148
|
+
--rf-syntax-punctuation: var(--rf-samp-syntax-punctuation);
|
|
149
|
+
--rf-syntax-meta: var(--rf-samp-syntax-punctuation);
|
|
150
|
+
--line-number-color: var(--rf-samp-muted);
|
|
151
|
+
--line-number-rule-color: var(--rf-samp-border);
|
|
152
|
+
--line-note-color: var(--rf-samp-muted);
|
|
153
|
+
--line-note-label-color: var(--rf-samp-muted);
|
|
154
|
+
--line-note-border-color: var(--rf-samp-border);
|
|
155
|
+
--line-note-background: color-mix(in oklch, var(--rf-samp-bg) 92%, var(--rf-samp-fg));
|
|
156
|
+
color: var(--rf-samp-fg);
|
|
157
|
+
background: var(--rf-samp-bg);
|
|
158
|
+
border-color: var(--rf-samp-border);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pre:has(> samp) .pre-line-comment {
|
|
162
|
+
color: var(--rf-samp-muted);
|
|
163
|
+
background: color-mix(in oklch, var(--rf-samp-muted) 18%, transparent);
|
|
164
|
+
}
|