@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 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
- createRfBasicShikiCustomHighlightOptions,
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: createRfBasicShikiCustomHighlightOptions({
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 base code blocks, `samp`, line features, highlight.js markup classes, Shiki API scopes, and highlight.js API scopes.
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
- - base code block styling
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 digest/state work unless incremental runtime apply is requested.
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/rf-basic/base.css`, `theme/rf-basic/api/shiki.css`, `theme/rf-basic/api/highlightjs.css`,
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
- customHighlight: {
44
- provider: 'shiki',
45
- shikiScopeMode: 'role',
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.8.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.18.0",
56
+ "@peaceroad/markdown-it-figure-with-p-caption": "^0.19.0",
55
57
  "highlight.js": "^11.11.1",
56
- "markdown-it": "^14.1.1",
57
- "markdown-it-attrs": "^4.3.1",
58
- "shiki": "^4.1.0"
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, '&amp;')
13
- .replace(/"/g, '&quot;')
14
- .replace(/'/g, '&#39;')
15
- .replace(/</g, '&lt;')
16
- .replace(/>/g, '&gt;')
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
- : () => null
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 (shikiPunctuationTokenReg.test(trimmed)) return 'punctuation'
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
- shikiPunctuationTokenReg.test(trimmed)
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 (!shikiSimpleIdentifierReg.test(trimmed) && hasAnyScopePattern(['punctuation.'])) return 'punctuation'
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 (!shikiSimpleIdentifierReg.test(trimmed)) return 'punctuation'
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(tokenContent, langKey)
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 (shikiSimpleIdentifierReg.test(trimmed)) return 'variable-plain'
553
+ if (isIdentifier) return 'variable-plain'
546
554
  }
547
- if (!shikiSimpleIdentifierReg.test(trimmed) && (bucket === 'text' || bucket === 'meta')) {
548
- const tokenBucket = classifyToken(tokenContent, langKey)
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')) return 'comment'
188
- if (s.includes('meta.shebang')) return 'meta-shebang'
189
- if (s.includes('punctuation.definition.tag.begin') || s.includes('punctuation.definition.tag.end') || s.includes('punctuation.separator.key-value.html')) return 'tag-delimiter'
190
- if (s.includes('entity.other.attribute-name') || s.includes('attribute-name')) return 'attribute'
191
- if (s.includes('entity.name.tag')) return 'tag'
192
- if (s.includes('string.unquoted')) return 'string-unquoted'
193
- if (s.includes('regexp') || s.includes('regex') || s.includes('string') || s.includes('punctuation.definition.string')) return 'string'
194
- if (s.includes('constant.other.color')) return 'number'
195
- if (s.includes('constant.numeric') || s.includes('number')) return 'number'
196
- if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none')) return 'literal'
197
- if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace')) return 'namespace'
198
- if (s.includes('storage.type.function.arrow') || s.includes('keyword.operator')) return 'keyword'
199
- if (s.includes('variable.language.this')) return 'variable-this'
200
- if (s.includes('constant.other.php') || s.includes('variable.other.constant') || s.includes('variable.other.enummember')) return 'variable-const'
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')) return 'variable-member'
202
- if (s.includes('variable.parameter') || s.includes('entity.name.variable.parameter')) return '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'))) return 'variable-plain'
204
- if (s.includes('support.function.builtin') || s.includes('support.function.kernel') || s.includes('support.function.construct')) return 'title-function-builtin'
205
- if (s.includes('entity.name.function.macro')) return 'title-function'
206
- if (s.includes('meta.function') && !s.includes('entity.name.function') && !s.includes('support.function')) return 'meta'
207
- if (s.includes('entity.name.function') || s.includes('support.function')) return 'title-function'
208
- if (s.includes('entity.name.class') || s.includes('entity.name.type.class')) return 'title-class'
209
- if (s.includes('storage.type.built-in.primitive')) return 'type-primitive'
210
- if (s.includes('entity.name.type.namespace') || s.includes('entity.name.type') || s.includes('support.class')) return 'type-name'
211
- if (s.includes('support.type')) return 'type'
212
- if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier')) return 'keyword'
213
- if (s.includes('variable')) return 'variable'
214
- if (s.includes('punctuation')) return '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 = (scopeCandidates) => {
223
- if (!Array.isArray(scopeCandidates)) return false
224
- for (let i = 0; i < scopeCandidates.length; i++) {
225
- const s = String(scopeCandidates[i] || '').toLowerCase()
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 lower = trimmed.toLowerCase().replace(/\s+/g, ' ')
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
- if (shikiPunctuationTokenReg.test(trimmed)) return 'punctuation'
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: shikiSimpleIdentifierReg.test(tokenTrim),
340
- isPunctuation: shikiPunctuationTokenReg.test(tokenTrim),
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(candidates)) return 'type'
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 = classifyShikiRoleByToken(tokenContent, langKey)
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 postHelpers = {
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
- inlinePayloadScript = `<script type="application/json" id="${scriptId}" ${customHighlightPreAttr}="${id}">${payloadJson}</script>\n`
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 {