@peaceroad/markdown-it-renderer-fence 0.8.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 +11 -6
- package/docs/code-highlighting-design.md +8 -4
- package/docs/custom-highlight-styling-guide.md +12 -6
- package/package.json +7 -5
- package/src/custom-highlight/payload-utils.js +4 -9
- package/src/custom-highlight/shiki-role-rules.js +20 -10
- package/src/custom-highlight/shiki-role.js +50 -50
- package/src/fence/render-api-constants.js +1 -1
- package/src/fence/render-api-provider.js +2 -3
- package/src/fence/render-api-renderer.js +19 -4
- package/src/fence/render-api-runtime.js +29 -10
- package/src/fence/render-markup.js +30 -15
- package/src/fence/render-shared.js +72 -22
- package/src/utils/attr-utils.js +10 -0
- package/theme/_shared/rf-core.css +7 -1
- package/theme/line-notes.css +94 -0
- package/theme/line-number.css +64 -0
- package/theme/rf-basic/README.md +31 -5
- package/theme/rf-basic/api/shiki.css +1 -1
- package/theme/rf-basic/base.css +2 -0
- package/theme/rf-basic/index.js +9 -0
- package/theme/rf-monochrome/README.md +21 -8
- package/theme/rf-monochrome/base.css +2 -0
- package/theme/rf-monochrome/index.js +4 -5
- package/theme/rf-monochrome/markup/highlightjs.css +26 -10
- package/theme/rf-monochrome/markup/shiki.css +12 -1
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ const md = MarkdownIt({
|
|
|
76
76
|
| `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` | browser runtime only (`applyCustomHighlights`, `observeCustomHighlights`, `clearCustomHighlights`) |
|
|
77
77
|
| `@peaceroad/markdown-it-renderer-fence/theme/rf-basic` | color preset Shiki theme object and API provider helpers |
|
|
78
78
|
| `@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css` | complete color preset CSS |
|
|
79
|
+
| `@peaceroad/markdown-it-renderer-fence/theme/line-number.css` | theme-neutral line-number layout CSS |
|
|
80
|
+
| `@peaceroad/markdown-it-renderer-fence/theme/line-notes.css` | theme-neutral line-notes layout CSS |
|
|
79
81
|
| `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome` | one-color Shiki markup theme object |
|
|
80
82
|
| `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome.css` | complete one-color markup preset CSS |
|
|
81
83
|
|
|
@@ -83,6 +85,8 @@ Use `@peaceroad/markdown-it-renderer-fence/theme/rf-basic` for JavaScript helper
|
|
|
83
85
|
|
|
84
86
|
Split preset CSS is also available:
|
|
85
87
|
|
|
88
|
+
- `theme/line-number.css`
|
|
89
|
+
- `theme/line-notes.css`
|
|
86
90
|
- `theme/rf-basic/base.css`
|
|
87
91
|
- `theme/rf-basic/api/highlightjs.css`
|
|
88
92
|
- `theme/rf-basic/api/shiki.css`
|
|
@@ -165,7 +169,7 @@ For emitted HTML contracts and visual samples, see the files listed in [Docs and
|
|
|
165
169
|
| `onFenceDecision` | debug hook for per-fence branch decisions |
|
|
166
170
|
| `onFenceDecisionTiming` | include timing fields in `onFenceDecision` payloads |
|
|
167
171
|
|
|
168
|
-
Line features fail closed when highlighter output line counts do not match source line counts. In `useHighlightPre` passthrough, line-splitting features and `<samp>` conversion are intentionally skipped.
|
|
172
|
+
Line features fail closed when highlighter output line counts do not match source line counts. In `useHighlightPre` passthrough, line-splitting features and `<samp>` conversion are intentionally skipped. Emphasis ranges are normalized before rendering: reversed ranges are corrected, ranges are sorted, and overlapping or adjacent ranges are coalesced.
|
|
169
173
|
|
|
170
174
|
### Custom Highlight API options
|
|
171
175
|
|
|
@@ -198,7 +202,7 @@ import rendererFenceApi, {
|
|
|
198
202
|
renderCustomHighlightPayloadScript,
|
|
199
203
|
} from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
|
|
200
204
|
import {
|
|
201
|
-
|
|
205
|
+
createRfBasicShikiRoleCustomHighlightOptions,
|
|
202
206
|
rfBasicShikiTheme,
|
|
203
207
|
rfBasicThemeName,
|
|
204
208
|
} from '@peaceroad/markdown-it-renderer-fence/theme/rf-basic'
|
|
@@ -211,11 +215,9 @@ const highlighter = await createHighlighter({
|
|
|
211
215
|
const md = MarkdownIt({ html: true })
|
|
212
216
|
.use(markdownItAttrs)
|
|
213
217
|
.use(rendererFenceApi, {
|
|
214
|
-
customHighlight:
|
|
218
|
+
customHighlight: createRfBasicShikiRoleCustomHighlightOptions({
|
|
215
219
|
highlighter,
|
|
216
220
|
theme: rfBasicThemeName,
|
|
217
|
-
shikiScopeMode: 'role',
|
|
218
|
-
includeScopeStyles: false,
|
|
219
221
|
transport: 'env',
|
|
220
222
|
}),
|
|
221
223
|
})
|
|
@@ -251,7 +253,9 @@ The optional preset theme is a package artifact, not an automatically loaded dep
|
|
|
251
253
|
@import "@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css";
|
|
252
254
|
```
|
|
253
255
|
|
|
254
|
-
The color preset covers
|
|
256
|
+
The color preset covers fenced code blocks, `samp`, line-number/line-notes layout, other line features, highlight.js markup classes, Shiki role-mode API scopes, and highlight.js API scopes. It does not style inline Markdown code.
|
|
257
|
+
|
|
258
|
+
Line-number HTML and counter behavior belong to the renderer, while spacing and colors remain theme-controlled. Override `--line-number-width`, `--line-number-gap`, `--line-number-rule-gap`, `--line-number-color`, and `--line-number-rule-color` after importing the structural or preset CSS.
|
|
255
259
|
|
|
256
260
|
For one-color print/monochrome output, use markup mode and import the opt-in monochrome override:
|
|
257
261
|
|
|
@@ -281,6 +285,7 @@ From `@peaceroad/markdown-it-renderer-fence/theme/rf-basic`:
|
|
|
281
285
|
- `rfBasicThemeName`
|
|
282
286
|
- `createRfBasicShikiTheme`
|
|
283
287
|
- `createRfBasicShikiCustomHighlightOptions`
|
|
288
|
+
- `createRfBasicShikiRoleCustomHighlightOptions`
|
|
284
289
|
- `createRfBasicHighlightjsCustomHighlightOptions`
|
|
285
290
|
- `rfBasicSyntaxCssVars`
|
|
286
291
|
- `createRfBasicShikiRoleScopeColorVars`
|
|
@@ -175,6 +175,8 @@ Exports:
|
|
|
175
175
|
|
|
176
176
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic`
|
|
177
177
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css`
|
|
178
|
+
- `@peaceroad/markdown-it-renderer-fence/theme/line-number.css`
|
|
179
|
+
- `@peaceroad/markdown-it-renderer-fence/theme/line-notes.css`
|
|
178
180
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/base.css`
|
|
179
181
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/api/shiki.css`
|
|
180
182
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/api/highlightjs.css`
|
|
@@ -185,8 +187,7 @@ Exports:
|
|
|
185
187
|
|
|
186
188
|
The theme should cover:
|
|
187
189
|
|
|
188
|
-
-
|
|
189
|
-
- inline code styling
|
|
190
|
+
- fenced code block styling (inline Markdown code remains application-owned)
|
|
190
191
|
- line numbers
|
|
191
192
|
- emphasized lines
|
|
192
193
|
- comment marks
|
|
@@ -201,12 +202,14 @@ Design rules:
|
|
|
201
202
|
|
|
202
203
|
- CSS must not be auto-loaded by the renderer.
|
|
203
204
|
- CSS variables should be stable enough for consumers to override.
|
|
205
|
+
- Component theme CSS must not set document-wide `color-scheme`; scope UA color-scheme hints to the code/terminal surfaces that own the palette.
|
|
204
206
|
- Light/dark variants should share the same role names.
|
|
205
207
|
- `samp` should read as terminal output; using an inverted-looking surface is acceptable when contrast remains strong.
|
|
206
208
|
- A one-color print theme should be markup-mode-only when it relies on bold/italic, because Custom Highlight API
|
|
207
209
|
cannot apply `font-weight` or `font-style`.
|
|
208
210
|
- In one-color terminal-like `samp` blocks, token-level bold/italic can make command/output boundaries ambiguous. Prefer suppressing token emphasis unless the terminal renderer has structural prompt/input/output markup.
|
|
209
211
|
- Provider-specific CSS can be split, but base layout and shared variables should stay central.
|
|
212
|
+
- Theme-neutral structural line CSS should have one package-owned source of truth and be imported by complete presets; examples may wrap those public files but must not become the implementation source.
|
|
210
213
|
- An internal shared CSS source under `theme/_shared/` is acceptable when public theme files
|
|
211
214
|
import it and package dry-runs verify it is shipped. Consumers should import public theme
|
|
212
215
|
paths such as `theme/rf-basic/base.css`, not private shared files.
|
|
@@ -242,8 +245,9 @@ Current principles:
|
|
|
242
245
|
- Use cheap source sentinels before scanning tokens for sidecar fences.
|
|
243
246
|
- Run line splitting only when a line feature needs it.
|
|
244
247
|
- Parse advanced line-number ranges lazily.
|
|
245
|
-
- Keep role-mode caches bounded.
|
|
246
|
-
- Avoid payload
|
|
248
|
+
- Keep role-mode caches bounded and cache every resolved base-scope bucket, not only fallback buckets.
|
|
249
|
+
- Avoid payload digests, DOM text snapshots, and boundary-reference work unless incremental runtime apply is requested.
|
|
250
|
+
- Reuse sanitized runtime scope names within one apply pass.
|
|
247
251
|
- Keep markup-only and custom-highlight entries available for load isolation.
|
|
248
252
|
|
|
249
253
|
Benchmark changes with:
|
|
@@ -28,7 +28,8 @@ In API mode, color comes from one of two sources:
|
|
|
28
28
|
The plugin does not auto-load theme CSS files.
|
|
29
29
|
The complete optional preset theme CSS is exported as `@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css`.
|
|
30
30
|
Provider-specific split files are also available:
|
|
31
|
-
`theme/
|
|
31
|
+
`theme/line-number.css`, `theme/line-notes.css`, `theme/rf-basic/base.css`,
|
|
32
|
+
`theme/rf-basic/api/shiki.css`, `theme/rf-basic/api/highlightjs.css`,
|
|
32
33
|
`theme/rf-basic/markup/shiki.css`, and `theme/rf-basic/markup/highlightjs.css`.
|
|
33
34
|
The optional JS theme helper uses CSS variable color values so Shiki can emit payload `scopeStyles` like `var(--rf-syntax-keyword)`.
|
|
34
35
|
Your page still needs CSS that defines those variables, either by loading the preset CSS or by defining compatible variables in your app.
|
|
@@ -40,11 +41,9 @@ Your page still needs CSS that defines those variables, either by loading the pr
|
|
|
40
41
|
Use CSS-managed role buckets:
|
|
41
42
|
|
|
42
43
|
```js
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
includeScopeStyles: false,
|
|
47
|
-
}
|
|
44
|
+
import { createRfBasicShikiRoleCustomHighlightOptions } from '@peaceroad/markdown-it-renderer-fence/theme/rf-basic'
|
|
45
|
+
|
|
46
|
+
customHighlight: createRfBasicShikiRoleCustomHighlightOptions({ highlighter })
|
|
48
47
|
```
|
|
49
48
|
|
|
50
49
|
Why:
|
|
@@ -138,6 +137,8 @@ Notes:
|
|
|
138
137
|
|
|
139
138
|
## 7. Runtime and CSS Constraints
|
|
140
139
|
|
|
140
|
+
Use one canonical runtime root per document, normally `document`. Custom highlight names are shared by scope and the browser's `CSS.highlights` registry is document-global, so applying two disjoint roots independently can replace same-named ranges registered by the other root. A subtree root is suitable when it is the only root managed by this runtime in that document.
|
|
141
|
+
|
|
141
142
|
`::highlight(...)` reliably supports only a subset of properties.
|
|
142
143
|
This plugin emits/uses:
|
|
143
144
|
|
|
@@ -168,6 +169,8 @@ This package ships an optional preset theme. It is not auto-loaded, but it is a
|
|
|
168
169
|
CSS:
|
|
169
170
|
|
|
170
171
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css`
|
|
172
|
+
- `@peaceroad/markdown-it-renderer-fence/theme/line-number.css`
|
|
173
|
+
- `@peaceroad/markdown-it-renderer-fence/theme/line-notes.css`
|
|
171
174
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/base.css`
|
|
172
175
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/api/shiki.css`
|
|
173
176
|
- `@peaceroad/markdown-it-renderer-fence/theme/rf-basic/api/highlightjs.css`
|
|
@@ -186,6 +189,7 @@ The JS helper provides:
|
|
|
186
189
|
|
|
187
190
|
- `rfBasicShikiTheme`: a Shiki theme object that uses `--rf-*` CSS variables.
|
|
188
191
|
- `createRfBasicShikiCustomHighlightOptions(...)`: a small Shiki provider option helper.
|
|
192
|
+
- `createRfBasicShikiRoleCustomHighlightOptions(...)`: the recommended CSS-managed role profile (`role` + no payload scope styles).
|
|
189
193
|
- `createRfBasicHighlightjsCustomHighlightOptions(...)`: a small highlight.js provider option helper.
|
|
190
194
|
- role maps used by repository examples to keep Shiki, highlight.js, and `::highlight(...)` CSS aligned.
|
|
191
195
|
|
|
@@ -197,6 +201,8 @@ syntax colors, so it is useful for print or one-color output. It is not a Custom
|
|
|
197
201
|
|
|
198
202
|
The repository keeps `example/theme.css` only as a tiny local wrapper around the distributed preset CSS.
|
|
199
203
|
|
|
204
|
+
The preset sets `color-scheme` only on themed `pre` surfaces. It does not opt the whole document into light/dark UA colors. Override `--rf-code-color-scheme` or `--rf-samp-color-scheme` when a site uses an explicit scheme that differs from `prefers-color-scheme`.
|
|
205
|
+
|
|
200
206
|
For production:
|
|
201
207
|
|
|
202
208
|
- Markup mode: use this preset, the CSS theme from your highlighter, or your design system.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-renderer-fence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A markdown-it fence renderer with code/samp output, line features, Custom Highlight API support, and optional themes.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"./theme/rf-basic": "./theme/rf-basic/index.js",
|
|
12
12
|
"./theme/rf-basic.css": "./theme/rf-basic/index.css",
|
|
13
13
|
"./theme/rf-basic/*": "./theme/rf-basic/*",
|
|
14
|
+
"./theme/line-number.css": "./theme/line-number.css",
|
|
15
|
+
"./theme/line-notes.css": "./theme/line-notes.css",
|
|
14
16
|
"./theme/rf-monochrome": "./theme/rf-monochrome/index.js",
|
|
15
17
|
"./theme/rf-monochrome.css": "./theme/rf-monochrome/index.css",
|
|
16
18
|
"./theme/rf-monochrome/*": "./theme/rf-monochrome/*"
|
|
@@ -51,10 +53,10 @@
|
|
|
51
53
|
"repository": "https://github.com/peaceroad/p7d-markdown-it-renderer-fence",
|
|
52
54
|
"license": "MIT",
|
|
53
55
|
"devDependencies": {
|
|
54
|
-
"@peaceroad/markdown-it-figure-with-p-caption": "^0.
|
|
56
|
+
"@peaceroad/markdown-it-figure-with-p-caption": "^0.19.0",
|
|
55
57
|
"highlight.js": "^11.11.1",
|
|
56
|
-
"markdown-it": "^14.
|
|
57
|
-
"markdown-it-attrs": "^
|
|
58
|
-
"shiki": "^4.1
|
|
58
|
+
"markdown-it": "^14.3.0",
|
|
59
|
+
"markdown-it-attrs": "^5.0.0",
|
|
60
|
+
"shiki": "^4.3.1"
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtmlAttr,
|
|
3
|
+
} from '../utils/attr-utils.js'
|
|
4
|
+
|
|
1
5
|
const escapeJsonForScript = (data) => {
|
|
2
6
|
return JSON.stringify(data)
|
|
3
7
|
.replace(/</g, '\\u003C')
|
|
@@ -7,15 +11,6 @@ const escapeJsonForScript = (data) => {
|
|
|
7
11
|
.replace(/\u2029/g, '\\u2029')
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
const escapeHtmlAttr = (value) => {
|
|
11
|
-
return String(value)
|
|
12
|
-
.replace(/&/g, '&')
|
|
13
|
-
.replace(/"/g, '"')
|
|
14
|
-
.replace(/'/g, ''')
|
|
15
|
-
.replace(/</g, '<')
|
|
16
|
-
.replace(/>/g, '>')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
const getCustomHighlightPayloadMap = (env, envKey = 'rendererFenceCustomHighlights') => {
|
|
20
15
|
if (!env || typeof env !== 'object') return {}
|
|
21
16
|
const map = env[envKey]
|
|
@@ -408,7 +408,7 @@ const roleV4SortedLangRules = new Map(
|
|
|
408
408
|
)
|
|
409
409
|
|
|
410
410
|
const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
411
|
-
const postHelper = helper && typeof helper === 'object' ? helper : {}
|
|
411
|
+
const postHelper = helper && typeof helper === 'object' ? helper : (ctx || {})
|
|
412
412
|
const hasAnyScopePattern = typeof postHelper.hasAnyScopePattern === 'function'
|
|
413
413
|
? postHelper.hasAnyScopePattern
|
|
414
414
|
: () => false
|
|
@@ -417,7 +417,7 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
417
417
|
: (pattern) => hasAnyScopePattern([pattern])
|
|
418
418
|
const classifyToken = typeof postHelper.classifyToken === 'function'
|
|
419
419
|
? postHelper.classifyToken
|
|
420
|
-
:
|
|
420
|
+
: null
|
|
421
421
|
|
|
422
422
|
const bucket = String(currentBucket || 'text')
|
|
423
423
|
const langKey = String(ctx && ctx.langKey || '')
|
|
@@ -425,6 +425,12 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
425
425
|
const tokenContent = String(ctx && ctx.tokenContent || '')
|
|
426
426
|
const trimmed = String(ctx && ctx.tokenTrim || '')
|
|
427
427
|
const tokenLower = (ctx && typeof ctx.tokenLower === 'string') ? ctx.tokenLower : trimmed.toLowerCase()
|
|
428
|
+
const isIdentifier = (ctx && typeof ctx.isIdentifier === 'boolean')
|
|
429
|
+
? ctx.isIdentifier
|
|
430
|
+
: shikiSimpleIdentifierReg.test(trimmed)
|
|
431
|
+
const isPunctuation = (ctx && typeof ctx.isPunctuation === 'boolean')
|
|
432
|
+
? ctx.isPunctuation
|
|
433
|
+
: shikiPunctuationTokenReg.test(trimmed)
|
|
428
434
|
if (!trimmed) return 'text'
|
|
429
435
|
|
|
430
436
|
if ((langKey === 'bash' || langKey === 'shellscript') && shikiShellOptionTokenReg.test(trimmed)) {
|
|
@@ -487,7 +493,7 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
487
493
|
if (hasAnyScopePattern(['meta.function']) && !hasAnyScopePattern(['entity.name.function', 'support.function'])) return 'meta'
|
|
488
494
|
if (hasAnyScopePattern(['punctuation.accessor'])) return 'text'
|
|
489
495
|
if (!shikiFunctionLikeIdentifierReg.test(trimmed)) {
|
|
490
|
-
if (
|
|
496
|
+
if (isPunctuation) return 'punctuation'
|
|
491
497
|
if (/[.\[\](){}:]/.test(trimmed)) return 'text'
|
|
492
498
|
}
|
|
493
499
|
}
|
|
@@ -505,7 +511,7 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
505
511
|
if (bucket === 'variable') {
|
|
506
512
|
if (
|
|
507
513
|
hasAnyScopePattern(['meta.type.annotation']) &&
|
|
508
|
-
|
|
514
|
+
isPunctuation
|
|
509
515
|
) return 'punctuation'
|
|
510
516
|
if (hasAnyScopePattern(['variable.language.this'])) return 'variable-this'
|
|
511
517
|
if (hasAnyScopePattern(['variable.other.constant', 'constant.other.php', 'variable.other.enummember'])) return 'variable-const'
|
|
@@ -517,7 +523,7 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
517
523
|
if (hasAnyScopePattern(['variable.other.constant', 'constant.other.php', 'variable.other.enummember'])) return 'variable-const'
|
|
518
524
|
if (langKey === 'csharp' && hasAnyScopePattern(['entity.name.variable.local.cs'])) return 'text'
|
|
519
525
|
if (hasAnyScopePattern(['source.sql', 'source.python', 'source.ruby'])) return 'text'
|
|
520
|
-
if (!
|
|
526
|
+
if (!isIdentifier && hasAnyScopePattern(['punctuation.'])) return 'punctuation'
|
|
521
527
|
}
|
|
522
528
|
if (bucket === 'variable-member') {
|
|
523
529
|
if (hasAnyScopePattern(['variable.object.property.ts', 'variable-object-property-ts', 'variable.object.property.js', 'variable-object-property-js'])) return 'variable-property'
|
|
@@ -530,7 +536,7 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
530
536
|
if (hasAnyScopePattern(['variable.other.readwrite.hcl', 'variable.other.normal.shell', 'meta.block.hcl'])) return 'variable-plain'
|
|
531
537
|
if (hasAnyScopePattern(['storage.type.built-in.primitive', 'keyword.operator.type.annotation', 'keyword.operator.type'])) return 'keyword'
|
|
532
538
|
if (hasAnyScopePattern(['entity.name.type', 'support.type'])) return 'type-name'
|
|
533
|
-
if (!
|
|
539
|
+
if (!isIdentifier) return 'punctuation'
|
|
534
540
|
}
|
|
535
541
|
if (bucket === 'variable-property') {
|
|
536
542
|
if (hasAnyScopePattern(['variable.object.property', 'variable.other.property'])) return 'variable-parameter'
|
|
@@ -539,13 +545,17 @@ const applyShikiRoleLegacyPostRules = (currentBucket, ctx, helper) => {
|
|
|
539
545
|
if (bucket === 'keyword' && hasAnyScopePattern(['variable.language.this'])) return 'variable-this'
|
|
540
546
|
if (bucket === 'literal' && langKey === 'sql') return 'text'
|
|
541
547
|
if (bucket === 'text') {
|
|
542
|
-
const tokenBucket = classifyToken
|
|
548
|
+
const tokenBucket = classifyToken
|
|
549
|
+
? classifyToken(tokenContent, langKey)
|
|
550
|
+
: ((ctx && ctx.tokenBucket) || null)
|
|
543
551
|
if (tokenBucket) return tokenBucket
|
|
544
552
|
if (hasAnyScopePattern(['source.sql', 'source.python', 'source.ruby', 'text.html.basic'])) return 'text'
|
|
545
|
-
if (
|
|
553
|
+
if (isIdentifier) return 'variable-plain'
|
|
546
554
|
}
|
|
547
|
-
if (!
|
|
548
|
-
const tokenBucket = classifyToken
|
|
555
|
+
if (!isIdentifier && (bucket === 'text' || bucket === 'meta')) {
|
|
556
|
+
const tokenBucket = classifyToken
|
|
557
|
+
? classifyToken(tokenContent, langKey)
|
|
558
|
+
: ((ctx && ctx.tokenBucket) || null)
|
|
549
559
|
if (tokenBucket) return tokenBucket
|
|
550
560
|
}
|
|
551
561
|
return bucket
|
|
@@ -184,45 +184,44 @@ const classifyShikiScopeRoleSingle = (scope) => {
|
|
|
184
184
|
const cached = shikiScopeRoleCache.get(s)
|
|
185
185
|
if (cached) return cached
|
|
186
186
|
let bucket = 'text'
|
|
187
|
-
if (s.includes('comment'))
|
|
188
|
-
if (s.includes('meta.shebang'))
|
|
189
|
-
if (s.includes('punctuation.definition.tag.begin') || s.includes('punctuation.definition.tag.end') || s.includes('punctuation.separator.key-value.html'))
|
|
190
|
-
if (s.includes('entity.other.attribute-name') || s.includes('attribute-name'))
|
|
191
|
-
if (s.includes('entity.name.tag'))
|
|
192
|
-
if (s.includes('string.unquoted'))
|
|
193
|
-
if (s.includes('regexp') || s.includes('regex') || s.includes('string') || s.includes('punctuation.definition.string'))
|
|
194
|
-
if (s.includes('constant.other.color'))
|
|
195
|
-
if (s.includes('constant.numeric') || s.includes('number'))
|
|
196
|
-
if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none'))
|
|
197
|
-
if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace'))
|
|
198
|
-
if (s.includes('storage.type.function.arrow') || s.includes('keyword.operator'))
|
|
199
|
-
if (s.includes('variable.language.this'))
|
|
200
|
-
if (s.includes('constant.other.php') || s.includes('variable.other.constant') || s.includes('variable.other.enummember'))
|
|
201
|
-
if (s.includes('variable.object.property') || s.includes('variable.other.property') || s.includes('variable.other.member') || s.includes('variable.object.c') || s.includes('variable.ruby'))
|
|
202
|
-
if (s.includes('variable.parameter') || s.includes('entity.name.variable.parameter'))
|
|
203
|
-
if (s.includes('entity.name.variable.local') || s.includes('variable.other.readwrite') || s.includes('variable.other.normal') || s.includes('variable.other.php') || (s.includes('variable.other.') && !s.includes('variable.other.property') && !s.includes('variable.other.member')))
|
|
204
|
-
if (s.includes('support.function.builtin') || s.includes('support.function.kernel') || s.includes('support.function.construct'))
|
|
205
|
-
if (s.includes('entity.name.function.macro'))
|
|
206
|
-
if (s.includes('meta.function') && !s.includes('entity.name.function') && !s.includes('support.function'))
|
|
207
|
-
if (s.includes('entity.name.function') || s.includes('support.function'))
|
|
208
|
-
if (s.includes('entity.name.class') || s.includes('entity.name.type.class'))
|
|
209
|
-
if (s.includes('storage.type.built-in.primitive'))
|
|
210
|
-
if (s.includes('entity.name.type.namespace') || s.includes('entity.name.type') || s.includes('support.class'))
|
|
211
|
-
if (s.includes('support.type'))
|
|
212
|
-
if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier'))
|
|
213
|
-
if (s.includes('variable'))
|
|
214
|
-
if (s.includes('punctuation'))
|
|
215
|
-
if (s.includes('source.')) bucket = 'text'
|
|
187
|
+
if (s.includes('comment')) bucket = 'comment'
|
|
188
|
+
else if (s.includes('meta.shebang')) bucket = 'meta-shebang'
|
|
189
|
+
else if (s.includes('punctuation.definition.tag.begin') || s.includes('punctuation.definition.tag.end') || s.includes('punctuation.separator.key-value.html')) bucket = 'tag-delimiter'
|
|
190
|
+
else if (s.includes('entity.other.attribute-name') || s.includes('attribute-name')) bucket = 'attribute'
|
|
191
|
+
else if (s.includes('entity.name.tag')) bucket = 'tag'
|
|
192
|
+
else if (s.includes('string.unquoted')) bucket = 'string-unquoted'
|
|
193
|
+
else if (s.includes('regexp') || s.includes('regex') || s.includes('string') || s.includes('punctuation.definition.string')) bucket = 'string'
|
|
194
|
+
else if (s.includes('constant.other.color')) bucket = 'number'
|
|
195
|
+
else if (s.includes('constant.numeric') || s.includes('number')) bucket = 'number'
|
|
196
|
+
else if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none')) bucket = 'literal'
|
|
197
|
+
else if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace')) bucket = 'namespace'
|
|
198
|
+
else if (s.includes('storage.type.function.arrow') || s.includes('keyword.operator')) bucket = 'keyword'
|
|
199
|
+
else if (s.includes('variable.language.this')) bucket = 'variable-this'
|
|
200
|
+
else if (s.includes('constant.other.php') || s.includes('variable.other.constant') || s.includes('variable.other.enummember')) bucket = 'variable-const'
|
|
201
|
+
else if (s.includes('variable.object.property') || s.includes('variable.other.property') || s.includes('variable.other.member') || s.includes('variable.object.c') || s.includes('variable.ruby')) bucket = 'variable-member'
|
|
202
|
+
else if (s.includes('variable.parameter') || s.includes('entity.name.variable.parameter')) bucket = 'variable-parameter'
|
|
203
|
+
else if (s.includes('entity.name.variable.local') || s.includes('variable.other.readwrite') || s.includes('variable.other.normal') || s.includes('variable.other.php') || (s.includes('variable.other.') && !s.includes('variable.other.property') && !s.includes('variable.other.member'))) bucket = 'variable-plain'
|
|
204
|
+
else if (s.includes('support.function.builtin') || s.includes('support.function.kernel') || s.includes('support.function.construct')) bucket = 'title-function-builtin'
|
|
205
|
+
else if (s.includes('entity.name.function.macro')) bucket = 'title-function'
|
|
206
|
+
else if (s.includes('meta.function') && !s.includes('entity.name.function') && !s.includes('support.function')) bucket = 'meta'
|
|
207
|
+
else if (s.includes('entity.name.function') || s.includes('support.function')) bucket = 'title-function'
|
|
208
|
+
else if (s.includes('entity.name.class') || s.includes('entity.name.type.class')) bucket = 'title-class'
|
|
209
|
+
else if (s.includes('storage.type.built-in.primitive')) bucket = 'type-primitive'
|
|
210
|
+
else if (s.includes('entity.name.type.namespace') || s.includes('entity.name.type') || s.includes('support.class')) bucket = 'type-name'
|
|
211
|
+
else if (s.includes('support.type')) bucket = 'type'
|
|
212
|
+
else if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier')) bucket = 'keyword'
|
|
213
|
+
else if (s.includes('variable')) bucket = 'variable'
|
|
214
|
+
else if (s.includes('punctuation')) bucket = 'punctuation'
|
|
216
215
|
else if (s.includes('meta')) bucket = 'meta'
|
|
216
|
+
if (shikiScopeRoleCache.size >= 8192) shikiScopeRoleCache.clear()
|
|
217
217
|
shikiScopeRoleCache.set(s, bucket)
|
|
218
|
-
if (shikiScopeRoleCache.size > 8192) shikiScopeRoleCache.clear()
|
|
219
218
|
return bucket
|
|
220
219
|
}
|
|
221
220
|
|
|
222
|
-
const hasJsonYamlPropertyNameScope = (
|
|
223
|
-
if (!Array.isArray(
|
|
224
|
-
for (let i = 0; i <
|
|
225
|
-
const s =
|
|
221
|
+
const hasJsonYamlPropertyNameScope = (lowerScopeCandidates) => {
|
|
222
|
+
if (!Array.isArray(lowerScopeCandidates)) return false
|
|
223
|
+
for (let i = 0; i < lowerScopeCandidates.length; i++) {
|
|
224
|
+
const s = lowerScopeCandidates[i]
|
|
226
225
|
if (!s.includes('property-name') && !s.includes('dictionary-key')) continue
|
|
227
226
|
if (s.includes('.json') || s.includes('-json') || s.includes('.yaml') || s.includes('-yaml')) return true
|
|
228
227
|
}
|
|
@@ -302,18 +301,22 @@ const resolveShikiRoleLang = (lang, scopeCandidates, tok, opt) => {
|
|
|
302
301
|
return ''
|
|
303
302
|
}
|
|
304
303
|
|
|
305
|
-
const classifyShikiRoleByToken = (content, langKey) => {
|
|
304
|
+
const classifyShikiRoleByToken = (content, langKey, trimmedValue, lowerValue, punctuationValue) => {
|
|
306
305
|
const text = String(content || '')
|
|
307
|
-
const trimmed = text.trim()
|
|
306
|
+
const trimmed = typeof trimmedValue === 'string' ? trimmedValue : text.trim()
|
|
308
307
|
if (!trimmed) return 'text'
|
|
309
|
-
const
|
|
308
|
+
const rawLower = typeof lowerValue === 'string' ? lowerValue : trimmed.toLowerCase()
|
|
309
|
+
const lower = rawLower.replace(/\s+/g, ' ')
|
|
310
310
|
if (lower.startsWith('#!')) return 'meta'
|
|
311
311
|
if (langKey !== 'sql' && shikiGlobalLiteralTokenSet.has(lower)) return 'literal'
|
|
312
312
|
if (shikiNumberTokenReg.test(lower)) return 'number'
|
|
313
313
|
const set = shikiLangKeywordTokens[langKey]
|
|
314
314
|
if (set && set.has(lower)) return 'keyword'
|
|
315
315
|
if (langKey === 'css' && lower.startsWith('@')) return 'keyword'
|
|
316
|
-
|
|
316
|
+
const isPunctuation = typeof punctuationValue === 'boolean'
|
|
317
|
+
? punctuationValue
|
|
318
|
+
: shikiPunctuationTokenReg.test(trimmed)
|
|
319
|
+
if (isPunctuation) return 'punctuation'
|
|
317
320
|
return null
|
|
318
321
|
}
|
|
319
322
|
|
|
@@ -326,6 +329,9 @@ const buildShikiRoleContext = (rawScope, tok, lang, opt, preResolvedLangKey = ''
|
|
|
326
329
|
const langKey = preResolvedLangKey || resolveShikiRoleLang(lang, scopeCandidates, token, opt)
|
|
327
330
|
const lowerScopeCandidates = toLowerScopeCandidates(scopeCandidates)
|
|
328
331
|
const hasAnyScopePatternCached = createScopePatternMatcher(lowerScopeCandidates)
|
|
332
|
+
const isIdentifier = shikiSimpleIdentifierReg.test(tokenTrim)
|
|
333
|
+
const isPunctuation = shikiPunctuationTokenReg.test(tokenTrim)
|
|
334
|
+
const tokenBucket = classifyShikiRoleByToken(tokenContent, langKey, tokenTrim, tokenLower, isPunctuation)
|
|
329
335
|
return {
|
|
330
336
|
rawScope,
|
|
331
337
|
token,
|
|
@@ -336,8 +342,9 @@ const buildShikiRoleContext = (rawScope, tok, lang, opt, preResolvedLangKey = ''
|
|
|
336
342
|
tokenContent,
|
|
337
343
|
tokenTrim,
|
|
338
344
|
tokenLower,
|
|
339
|
-
isIdentifier
|
|
340
|
-
isPunctuation
|
|
345
|
+
isIdentifier,
|
|
346
|
+
isPunctuation,
|
|
347
|
+
tokenBucket,
|
|
341
348
|
hasAnyScopePattern: hasAnyScopePatternCached,
|
|
342
349
|
hasScopePattern: (pattern) => hasAnyScopePatternCached([pattern]),
|
|
343
350
|
}
|
|
@@ -347,7 +354,6 @@ const classifyShikiScopeRoleBaseFromContext = (context) => {
|
|
|
347
354
|
if (!context || typeof context !== 'object') return 'text'
|
|
348
355
|
let best = 'text'
|
|
349
356
|
let bestScore = shikiRoleBucketScore.text
|
|
350
|
-
const candidates = Array.isArray(context.scopeCandidates) ? context.scopeCandidates : []
|
|
351
357
|
const lowerScopeCandidates = Array.isArray(context.lowerScopeCandidates) ? context.lowerScopeCandidates : []
|
|
352
358
|
const hasAnyPattern = (context && typeof context.hasAnyScopePattern === 'function')
|
|
353
359
|
? context.hasAnyScopePattern
|
|
@@ -356,12 +362,11 @@ const classifyShikiScopeRoleBaseFromContext = (context) => {
|
|
|
356
362
|
? context.hasScopePattern
|
|
357
363
|
: (pattern) => hasAnyPattern([pattern])
|
|
358
364
|
const langKey = String(context.langKey || '')
|
|
359
|
-
const tokenContent = context.tokenContent
|
|
360
365
|
if (langKey === 'hcl' || langKey === 'terraform') {
|
|
361
366
|
const hclBucket = classifyShikiHclRoleBucket(lowerScopeCandidates, hasAnyPattern)
|
|
362
367
|
if (hclBucket) return hclBucket
|
|
363
368
|
}
|
|
364
|
-
if (hasJsonYamlPropertyNameScope(
|
|
369
|
+
if (hasJsonYamlPropertyNameScope(lowerScopeCandidates)) return 'type'
|
|
365
370
|
if (langKey === 'yaml' && hasScope('entity.name.tag.yaml')) return 'tag'
|
|
366
371
|
for (let i = 0; i < lowerScopeCandidates.length; i++) {
|
|
367
372
|
const bucket = classifyShikiScopeRoleSingle(lowerScopeCandidates[i])
|
|
@@ -378,7 +383,7 @@ const classifyShikiScopeRoleBaseFromContext = (context) => {
|
|
|
378
383
|
bestScore = shikiRoleBucketScore.text + 1
|
|
379
384
|
}
|
|
380
385
|
}
|
|
381
|
-
const tokenBucket =
|
|
386
|
+
const tokenBucket = context.tokenBucket
|
|
382
387
|
if (tokenBucket) {
|
|
383
388
|
const tokenScore = (shikiRoleBucketScore[tokenBucket] || shikiRoleBucketScore.text) + 1
|
|
384
389
|
if (best !== 'string' && best !== 'string-unquoted' && tokenScore > bestScore) best = tokenBucket
|
|
@@ -407,12 +412,7 @@ const classifyShikiScopeRole = (rawScope, tok, lang, opt, preResolvedRoleLang =
|
|
|
407
412
|
tokenLower,
|
|
408
413
|
)
|
|
409
414
|
const baseBucket = classifyShikiScopeRoleBaseFromContext(context)
|
|
410
|
-
const
|
|
411
|
-
classifyToken: classifyShikiRoleByToken,
|
|
412
|
-
hasAnyScopePattern: context.hasAnyScopePattern,
|
|
413
|
-
hasScopePattern: context.hasScopePattern,
|
|
414
|
-
}
|
|
415
|
-
const postBucket = applyShikiRoleLegacyPostRules(baseBucket, context, postHelpers)
|
|
415
|
+
const postBucket = applyShikiRoleLegacyPostRules(baseBucket, context)
|
|
416
416
|
const finalBucket = applyRoleV4Rules(postBucket, context)
|
|
417
417
|
return finalBucket || postBucket || baseBucket
|
|
418
418
|
}
|
|
@@ -11,7 +11,7 @@ const customHighlightDataScriptId = 'pre-highlight-data'
|
|
|
11
11
|
const customHighlightStyleTagId = 'pre-highlight-style'
|
|
12
12
|
const customHighlightEnvInitRuleName = 'renderer_fence_custom_highlight_env_init'
|
|
13
13
|
|
|
14
|
-
const runtimeFallbackReasonSet = new Set(['api-unsupported', 'apply-error'])
|
|
14
|
+
const runtimeFallbackReasonSet = new Set(['api-unsupported', 'range-invalid', 'apply-error'])
|
|
15
15
|
|
|
16
16
|
const customHighlightPayloadSchemaVersion = 1
|
|
17
17
|
const customHighlightPayloadSupportedVersions = [customHighlightPayloadSchemaVersion]
|
|
@@ -293,8 +293,6 @@ const getShikiTokenStyle = (token) => {
|
|
|
293
293
|
const style = {}
|
|
294
294
|
if (typeof token.color === 'string' && token.color) style.color = token.color
|
|
295
295
|
const fontStyle = Number.isFinite(token.fontStyle) ? token.fontStyle : 0
|
|
296
|
-
if (fontStyle & 1) style.fontStyle = 'italic'
|
|
297
|
-
if (fontStyle & 2) style.fontWeight = '700'
|
|
298
296
|
if (fontStyle & 4) style.textDecoration = 'underline'
|
|
299
297
|
return normalizeScopeStyle(style)
|
|
300
298
|
}
|
|
@@ -666,8 +664,8 @@ const buildShikiTokenOption = (chOpt, targetLang, themeOverride = '') => {
|
|
|
666
664
|
}
|
|
667
665
|
|
|
668
666
|
const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
|
|
669
|
-
const context = { token, md, env, option: chOpt }
|
|
670
667
|
if (chOpt.provider === 'custom') {
|
|
668
|
+
const context = { token, md, env, option: chOpt }
|
|
671
669
|
const getRanges = chOpt._customGetRanges
|
|
672
670
|
if (typeof getRanges !== 'function') throw new Error('customHighlight.getRanges must be a function')
|
|
673
671
|
const result = getRanges(token.content, lang, context)
|
|
@@ -675,6 +673,7 @@ const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
|
|
|
675
673
|
return normalizeCustomProviderRanges(result)
|
|
676
674
|
}
|
|
677
675
|
if (chOpt.provider === 'hljs') {
|
|
676
|
+
const context = { token, md, env, option: chOpt }
|
|
678
677
|
const highlightFn = chOpt._hljsHighlightFn
|
|
679
678
|
if (typeof highlightFn !== 'function') {
|
|
680
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 {
|
|
@@ -91,8 +92,9 @@ const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumb
|
|
|
91
92
|
env[customHighlightDataEnvKey][id] = payload
|
|
92
93
|
} else {
|
|
93
94
|
const payloadJson = escapeJsonForScript(payload)
|
|
94
|
-
const scriptId = `pre-highlight-data-${id}`
|
|
95
|
-
|
|
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`
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -150,9 +152,22 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
150
152
|
emitFenceDecision(opt, fallbackDecision)
|
|
151
153
|
return html
|
|
152
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
|
|
153
170
|
}
|
|
154
|
-
|
|
155
|
-
return renderFenceMarkup(context, md, opt, slf)
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
export {
|