@peaceroad/markdown-it-renderer-fence 0.6.1 → 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.
Files changed (34) hide show
  1. package/README.md +183 -430
  2. package/docs/README.md +37 -0
  3. package/docs/code-highlighting-design.md +317 -0
  4. package/docs/custom-highlight-styling-guide.md +204 -0
  5. package/package.json +22 -13
  6. package/src/custom-highlight/option-validator.js +19 -10
  7. package/src/custom-highlight/scope-name.js +23 -0
  8. package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +229 -44
  9. package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +64 -56
  10. package/src/custom-highlight/shiki-theme-role.js +176 -0
  11. package/src/entry/markup-highlight.js +4 -0
  12. package/src/fence/line-notes.js +199 -0
  13. package/src/fence/render-api-provider.js +62 -62
  14. package/src/fence/render-api-renderer.js +26 -11
  15. package/src/fence/render-api-runtime.js +5 -13
  16. package/src/fence/render-api.js +24 -12
  17. package/src/fence/render-markup.js +24 -16
  18. package/src/fence/render-shared.js +232 -20
  19. package/theme/_shared/rf-core.css +164 -0
  20. package/theme/rf-basic/README.md +91 -0
  21. package/theme/rf-basic/api/highlightjs.css +71 -0
  22. package/theme/rf-basic/api/shiki.css +67 -0
  23. package/theme/rf-basic/base.css +3 -0
  24. package/theme/rf-basic/index.css +7 -0
  25. package/theme/rf-basic/index.js +155 -0
  26. package/theme/rf-basic/markup/highlightjs.css +77 -0
  27. package/theme/rf-basic/markup/shiki.css +11 -0
  28. package/theme/rf-monochrome/README.md +71 -0
  29. package/theme/rf-monochrome/base.css +3 -0
  30. package/theme/rf-monochrome/index.css +5 -0
  31. package/theme/rf-monochrome/index.js +72 -0
  32. package/theme/rf-monochrome/markup/highlightjs.css +134 -0
  33. package/theme/rf-monochrome/markup/shiki.css +27 -0
  34. package/THIRD_PARTY_NOTICES.md +0 -56
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # p7d-markdown-it-renderer-fence
2
2
 
3
- A `markdown-it` plugin for code block rendering and enhancements.
3
+ A `markdown-it` fence renderer for safer `<pre><code>` / `<pre><samp>` output, line features, optional syntax highlighting, and advanced Custom Highlight API integration.
4
4
 
5
- Default is `markup` mode. Custom Highlight API mode is available as an advanced/experimental path.
5
+ Default mode is **markup**. Custom Highlight API mode is available for advanced browser-runtime highlighting.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,32 +10,15 @@ Default is `markup` mode. Custom Highlight API mode is available as an advanced/
10
10
  npm i @peaceroad/markdown-it-renderer-fence markdown-it markdown-it-attrs
11
11
  ```
12
12
 
13
- If you use syntax highlighting, install a highlighter too (for example `highlight.js` or `shiki`).
13
+ Install a highlighter separately when you need syntax highlighting, for example `highlight.js` or `shiki`.
14
14
 
15
- ## Entry Points
16
-
17
- - `@peaceroad/markdown-it-renderer-fence`
18
- Dispatcher entry (`highlightRenderer` selects mode).
19
- - `@peaceroad/markdown-it-renderer-fence/markup-highlight`
20
- Markup-focused entry.
21
- - `@peaceroad/markdown-it-renderer-fence/custom-highlight`
22
- API-mode entry + runtime/payload helpers.
23
- - `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime`
24
- Runtime-only entry (`applyCustomHighlights` / `observeCustomHighlights` / `clearCustomHighlights`).
25
-
26
- ## Markup Mode (Default)
27
-
28
- ### Quick Start
29
-
30
- Dispatcher entry:
15
+ ## Quick Start (Markup Mode)
31
16
 
32
17
  ```js
33
18
  import MarkdownIt from 'markdown-it'
34
19
  import markdownItAttrs from 'markdown-it-attrs'
35
20
  import hljs from 'highlight.js'
36
- import rendererFence from '@peaceroad/markdown-it-renderer-fence'
37
- // Markup-only entry (skips dispatcher/API branch):
38
- // import rendererFence from '@peaceroad/markdown-it-renderer-fence/markup-highlight'
21
+ import rendererFence from '@peaceroad/markdown-it-renderer-fence/markup-highlight'
39
22
 
40
23
  const md = MarkdownIt({
41
24
  html: true,
@@ -50,53 +33,31 @@ const md = MarkdownIt({
50
33
  },
51
34
  })
52
35
  .use(markdownItAttrs)
53
- .use(rendererFence) // defaults to markup; direct markup entry can be leaner
36
+ .use(rendererFence)
54
37
  ```
55
38
 
56
- ### Markup with Shiki
39
+ ### Shiki in Markup Mode
57
40
 
58
- Use the markup-only entry and provide `md.options.highlight` with Shiki.
59
- For production/blog builds, pre-scan markdown and preload only used languages.
41
+ Preload the languages you render, then return Shiki HTML from `md.options.highlight`.
60
42
 
61
43
  ```js
62
44
  import MarkdownIt from 'markdown-it'
63
45
  import markdownItAttrs from 'markdown-it-attrs'
64
46
  import { createHighlighter } from 'shiki'
65
47
  import rendererFence from '@peaceroad/markdown-it-renderer-fence/markup-highlight'
66
- import fs from 'node:fs'
67
- import path from 'node:path'
68
-
69
- const fenceInfoReg = /^```([^\s`{]+)/gm
70
- const collectFenceLangs = (markdown) => {
71
- const out = new Set()
72
- if (!markdown) return out
73
- let m
74
- while ((m = fenceInfoReg.exec(markdown)) !== null) {
75
- const lang = String(m[1] || '').trim().toLowerCase()
76
- if (lang) out.add(lang)
77
- }
78
- return out
79
- }
80
-
81
- // Example: scan markdown source(s) before highlighter creation
82
- const markdown = fs.readFileSync(path.join(process.cwd(), 'article.md'), 'utf8')
83
- const langs = Array.from(collectFenceLangs(markdown))
84
- langs.push('text') // safe fallback
85
48
 
86
49
  const highlighter = await createHighlighter({
87
50
  themes: ['github-light'],
88
- langs, // preload grammars; Shiki does not auto-detect language
51
+ langs: ['javascript', 'typescript', 'json', 'text'],
89
52
  })
90
53
 
91
54
  const md = MarkdownIt({
92
55
  html: true,
93
56
  langPrefix: 'language-',
94
57
  highlight: (code, lang) => {
95
- const targetLang = lang || 'text'
96
58
  try {
97
- return highlighter.codeToHtml(code, { lang: targetLang, theme: 'github-light' })
59
+ return highlighter.codeToHtml(code, { lang: lang || 'text', theme: 'github-light' })
98
60
  } catch (e) {
99
- // when lang grammar is not loaded (or invalid), keep output safe
100
61
  return md.utils.escapeHtml(code)
101
62
  }
102
63
  },
@@ -105,227 +66,128 @@ const md = MarkdownIt({
105
66
  .use(rendererFence)
106
67
  ```
107
68
 
108
- Note:
109
-
110
- - For small projects, a fixed list like `['javascript', 'typescript', 'json', 'text']` is also fine.
111
- - If Shiki grammar for a language is not loaded, `codeToHtml` can fail.
112
- - In markup mode, handle this in your `highlight` callback (for example fallback to escaped plain text as above).
113
-
114
- ### Main Features
115
-
116
- - `samp` rendering for `samp`, `shell`, `console` languages.
117
- - line number wrapping via `start` (`line-number-start` long form) / `data-pre-start`.
118
- - line number skip/set controls via `line-number-skip` / `line-number-set`.
119
- - emphasized lines via `em-lines` / `emphasize-lines`.
120
- - optional line-end spacer via `lineEndSpanThreshold`.
121
- - optional pre-wrap support via `wrap` / `pre-wrap`.
122
- - comment line markers via `comment-mark`.
123
-
124
- ### Fence Attribute Examples
125
-
126
- Simplified HTML below focuses on renderer-fence structure (not full highlighter token spans).
127
-
128
- `samp` conversion:
129
-
130
- ~~~md
131
- ```shell
132
- $ pwd
133
- ```
134
- ~~~
135
-
136
- ```html
137
- <pre><samp class="language-shell">$ pwd
138
- </samp></pre>
139
- ```
69
+ ## Entry Points
140
70
 
141
- Line numbers:
71
+ | Import | Purpose |
72
+ | --- | --- |
73
+ | `@peaceroad/markdown-it-renderer-fence` | compatibility dispatcher; `highlightRenderer` selects mode |
74
+ | `@peaceroad/markdown-it-renderer-fence/markup-highlight` | markup-only renderer path |
75
+ | `@peaceroad/markdown-it-renderer-fence/custom-highlight` | Custom Highlight API renderer + payload/runtime helpers |
76
+ | `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` | browser runtime only (`applyCustomHighlights`, `observeCustomHighlights`, `clearCustomHighlights`) |
77
+ | `@peaceroad/markdown-it-renderer-fence/theme/rf-basic` | color preset Shiki theme object and API provider helpers |
78
+ | `@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css` | complete color preset CSS |
79
+ | `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome` | one-color Shiki markup theme object |
80
+ | `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome.css` | complete one-color markup preset CSS |
81
+
82
+ Use `@peaceroad/markdown-it-renderer-fence/theme/rf-basic` for JavaScript helpers. CSS assets are exposed as CSS subpaths.
83
+
84
+ Split preset CSS is also available:
85
+
86
+ - `theme/rf-basic/base.css`
87
+ - `theme/rf-basic/api/highlightjs.css`
88
+ - `theme/rf-basic/api/shiki.css`
89
+ - `theme/rf-basic/markup/shiki.css`
90
+ - `theme/rf-basic/markup/highlightjs.css`
91
+ - `theme/rf-monochrome.css`
92
+ - `theme/rf-monochrome/markup/highlightjs.css`
93
+ - `theme/rf-monochrome/markup/shiki.css`
94
+
95
+ The plugin does not auto-load CSS or browser runtime JavaScript.
96
+
97
+ ## Features
98
+
99
+ - markup mode with highlighter-owned HTML (`md.options.highlight`)
100
+ - `<samp>` rendering for `samp`, `shell`, and `console` fence languages
101
+ - per-fence `{samp}` / `{code}` tag overrides
102
+ - line numbers via `start` / `line-number-start`
103
+ - advanced line-number controls via `line-number-skip` / `line-number-set`
104
+ - emphasized lines via `em-lines` / `emphasize-lines`
105
+ - optional long-line spacer via `lineEndSpanThreshold`
106
+ - optional pre-wrap via `{wrap}` / `{pre-wrap}`
107
+ - comment markers via `comment-mark`
108
+ - sidecar line notes via an immediate `line-notes` fence (`notes` alias)
109
+ - advanced Custom Highlight API mode for span-free code text plus browser-applied ranges
110
+ - optional first-party theme CSS and Shiki/provider helper exports
111
+
112
+ ## Fence Attribute Examples
142
113
 
143
114
  ~~~md
144
- ```js {start="1"}
115
+ ```js {start="1" em-lines="2"}
145
116
  const a = 1
146
117
  console.log(a)
147
118
  ```
148
119
  ~~~
149
120
 
150
- ```html
151
- <pre><code class="language-js" data-pre-start="1" style="counter-set:pre-line-number 1;">
152
- <span class="pre-line">const a = 1</span>
153
- <span class="pre-line">console.log(a)</span>
154
- </code></pre>
155
- ```
156
-
157
- Advanced line number control:
158
-
159
121
  ~~~md
160
- ```txt {start="25" line-number-skip="5" line-number-set="6:136"}
161
- line1
162
- line2
163
- line3
164
- line4
165
- ...
166
- line6
122
+ ```bash {samp comment-mark="#"}
123
+ # setup
124
+ export NODE_ENV=test
167
125
  ```
168
126
  ~~~
169
127
 
170
- ```html
171
- <pre><code class="language-txt" data-pre-start="25" data-pre-line-number-skip="5" data-pre-line-number-set="6:136" style="counter-set:pre-line-number 25;">
172
- <span class="pre-line">line1</span>
173
- <span class="pre-line">line2</span>
174
- <span class="pre-line">line3</span>
175
- <span class="pre-line">line4</span>
176
- <span class="pre-line pre-line-no-number">...</span>
177
- <span class="pre-line" style="counter-set:pre-line-number 136;">line6</span>
178
- </code></pre>
179
- ```
180
-
181
- Emphasis:
182
-
183
128
  ~~~md
184
- ```js {em-lines="2,4-5"}
185
- line1
186
- line2
187
- line3
188
- line4
189
- line5
129
+ ```shell {code}
130
+ # render as <code> even though shell is normally sampLang
190
131
  ```
191
132
  ~~~
192
133
 
193
- ```html
194
- <pre><code class="language-js" data-pre-emphasis="2,4-5">line1
195
- <span class="pre-lines-emphasis">line2</span>
196
- line3
197
- <span class="pre-lines-emphasis">line4
198
- line5</span>
199
- </code></pre>
200
- ```
201
-
202
- Wrap:
203
-
204
134
  ~~~md
205
- ```js {wrap}
206
- const veryLongLine = '...'
207
- ```
208
- ~~~
209
-
210
- ```html
211
- <pre data-pre-wrap="true" style="white-space: pre-wrap; overflow-wrap: anywhere;">
212
- <code class="language-js">const veryLongLine = '...'
213
- </code></pre>
135
+ ```js {start="5"}
136
+ const a = 1
137
+ console.log(a)
214
138
  ```
215
-
216
- Comment line marker:
217
-
218
- ~~~md
219
- ```samp {comment-mark="#"}
220
- # comment
221
- echo 1
139
+ ```line-notes
140
+ 1: setup {width="7em"}
141
+ 2: result {width="10em"}
222
142
  ```
223
143
  ~~~
224
144
 
225
- ```html
226
- <pre><samp data-pre-comment-mark="#" class="language-samp">
227
- <span class="pre-line-comment"># comment</span>
228
- echo 1
229
- </samp></pre>
230
- ```
231
-
232
- ### Markup Notes
233
-
234
- - `useHighlightPre: true` keeps highlighter-provided `<pre><code>` when present.
235
- - In that passthrough path, line-splitting features are intentionally disabled:
236
- - line numbers
237
- - `line-number-skip`
238
- - `line-number-set`
239
- - `em-lines`
240
- - line-end spacer
241
- - `comment-mark`
242
- - `samp` conversion
243
-
244
- Reference line-number CSS contract:
245
-
246
- Minimum counter contract:
247
-
248
- ```css
249
- pre :is(code, samp)[data-pre-start] .pre-line::before {
250
- content: counter(pre-line-number);
251
- }
252
-
253
- pre :is(code, samp)[data-pre-start] .pre-line::after {
254
- content: "";
255
- counter-increment: pre-line-number;
256
- }
257
-
258
- pre :is(code, samp)[data-pre-start] .pre-line.pre-line-no-number::before {
259
- content: "";
260
- }
261
-
262
- pre :is(code, samp)[data-pre-start] .pre-line.pre-line-no-number::after {
263
- counter-increment: none;
264
- }
265
- ```
266
-
267
- Recommended display CSS:
268
-
269
- - See [`example/line-number.css`](./example/line-number.css) for the full reference stylesheet.
270
- - See [`example/line-number-sample.html`](./example/line-number-sample.html) for Markdown / Preview / HTML side-by-side examples.
271
-
272
- Note:
273
-
274
- - renderer-fence emits `counter-set` as the displayed line number value itself
275
- - the reference CSS increments the counter in `span.pre-line::after`, so `start="1"` maps directly to `counter-set:pre-line-number 1;`
276
- - `line-number-set="6:136"` similarly emits `counter-set:pre-line-number 136;` on that line wrapper
277
- - the minimum contract above is the required part; `example/line-number.css` adds gutter width, divider line, and whitespace handling
278
- - if a consumer uses a different counter strategy, its CSS must be aligned with this HTML contract
279
-
280
- ### Markup Options
281
-
282
- - `attrsOrder` (default: `['class', 'id', 'data-*', 'style']`): output attribute order (`data-*` wildcard supported).
283
- - `setHighlight` (default: `true`): call `md.options.highlight` when available.
284
- - `setLineNumber` (default: `true`): enable line wrapper spans when `start` / `line-number-start` is valid.
285
- - `setEmphasizeLines` (default: `true`): enable `em-lines` / `emphasize-lines`.
286
- - `lineEndSpanThreshold` (default: `0`): append line-end spacer span when visual width threshold is met.
287
- - `setLineEndSpan`: alias of `lineEndSpanThreshold`.
288
- - `lineEndSpanClass` (default: `'pre-lineend-spacer'`): CSS class of the spacer span.
289
- - `setPreWrapStyle` (default: `true`): inject inline pre-wrap style for wrap-enabled blocks.
290
- - `sampLang` (default: `'shell,console'`): additional fence langs rendered as `<samp>`.
291
- - `langPrefix` (default: `md.options.langPrefix || 'language-'`): class prefix for language class.
292
- - `useHighlightPre` (default: `false`): passthrough highlighter `<pre><code>` wrappers and skip line-splitting features.
293
- - `onFenceDecision` (default: `null`): debug hook for per-fence branch decisions.
294
- - `onFenceDecisionTiming` (default: `false`): include timing fields in `onFenceDecision`.
295
-
296
- `em-lines` syntax note:
297
-
298
- - supports single values (`2`) and ranges (`4-6`)
299
- - supports open-ended forms (`3-`, `-2`)
300
- - reversed ranges are normalized (`5-3` behaves as `3-5`)
301
-
302
- Line-number attr syntax note:
303
-
304
- - `line-number-start` is the long form of `start`; rendered output still uses `data-pre-start`.
305
- - `line-number-skip` supports single values (`2`), ranges (`4-6`), and open-ended forms (`3-`).
306
- - `line-number-set` uses `line:number` pairs (for example `6:136,14:220`).
307
- - `line-number-skip` and `line-number-set` must not target the same source line. If they overlap, the skipped line stays blank and the conflicting `line-number-set` entry is ignored.
308
- - `line-number-skip` / `line-number-set` are applied only when source and highlighted logical line counts match; otherwise renderer falls back to plain sequential numbering.
309
-
310
- Migration note (`0.5.0`):
311
-
312
- - `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
313
- - use `comment-mark` / `data-pre-comment-mark` / `pre-line-comment`
314
- - `line-number-reset` was removed; use `line-number-set`
315
-
316
- ## Custom Highlight API Mode (Experimental / Advanced)
317
-
318
- Custom Highlight API mode renders plain code text and emits range payloads for browser-side Custom Highlight API application.
319
-
320
- Important:
321
-
322
- - You must run runtime apply in the browser (`applyCustomHighlights` or `observeCustomHighlights`).
323
- - Without runtime apply, payload exists but browser highlights are not activated.
324
- - `test/custom-highlight/pre-highlight.js` is a demo helper, not the package runtime API contract.
325
-
326
- Use API mode only if you need runtime range highlighting and can manage runtime/CSS integration in your app.
327
-
328
- ### Minimal API Example (Shiki provider)
145
+ For emitted HTML contracts and visual samples, see the files listed in [Docs and Examples](#docs-and-examples).
146
+
147
+ ## Main Options
148
+
149
+ ### Markup / shared options
150
+
151
+ | Option | Summary |
152
+ | --- | --- |
153
+ | `attrsOrder` | output attribute order; supports `data-*` wildcard |
154
+ | `setHighlight` | call `md.options.highlight` when available |
155
+ | `setLineNumber` | enable line wrapping when `start` / `line-number-start` is valid |
156
+ | `setEmphasizeLines` | enable `em-lines` / `emphasize-lines` |
157
+ | `lineEndSpanThreshold` | append line-end spacer spans for visually long lines |
158
+ | `setLineEndSpan` | alias of `lineEndSpanThreshold` |
159
+ | `lineEndSpanClass` | CSS class for line-end spacer spans |
160
+ | `setPreWrapStyle` | inject inline pre-wrap style for wrap-enabled blocks |
161
+ | `sampLang` | comma-separated languages rendered as `<samp>`; default: `shell,console` |
162
+ | `{samp}` / `{code}` | per-fence tag override attrs |
163
+ | `langPrefix` | language class prefix; defaults to `md.options.langPrefix || 'language-'` |
164
+ | `useHighlightPre` | preserve highlighter-owned `<pre><code>` wrappers; disables line-splitting features |
165
+ | `onFenceDecision` | debug hook for per-fence branch decisions |
166
+ | `onFenceDecisionTiming` | include timing fields in `onFenceDecision` payloads |
167
+
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.
169
+
170
+ ### Custom Highlight API options
171
+
172
+ | Option | Summary |
173
+ | --- | --- |
174
+ | `highlightRenderer` | dispatcher mode: `markup`, `api`, or `custom-highlight-api` |
175
+ | `customHighlight.provider` | `shiki`, `hljs`, or `custom` |
176
+ | `customHighlight.highlighter` | Shiki highlighter with synchronous `codeToTokens` |
177
+ | `customHighlight.hljsHighlight` | highlight.js-style function for the `hljs` provider |
178
+ | `customHighlight.getRanges` | synchronous custom range provider |
179
+ | `customHighlight.theme` | Shiki theme name or `{ light, dark, default? }` |
180
+ | `customHighlight.shikiScopeMode` | `auto`, `color`, `semantic`, or `role` |
181
+ | `customHighlight.includeScopeStyles` | include payload `scopeStyles` when available |
182
+ | `customHighlight.transport` | `env` or `inline-script` |
183
+ | `customHighlight.fallback` | provider-error fallback: `plain` or `markup` |
184
+ | `customHighlight.lineFeatureStrategy` | `hybrid` or `disable` |
185
+
186
+ For detailed API-mode styling, runtime, role-mode, and payload guidance, see [Custom Highlight Styling Guide](docs/custom-highlight-styling-guide.md) and [Code Highlighting Design](docs/code-highlighting-design.md).
187
+
188
+ ## Custom Highlight API Mode
189
+
190
+ API mode renders escaped code text and emits payload ranges. It does **not** activate browser highlights by itself; call the runtime in the browser.
329
191
 
330
192
  ```js
331
193
  import MarkdownIt from 'markdown-it'
@@ -335,210 +197,73 @@ import rendererFenceApi, {
335
197
  applyCustomHighlights,
336
198
  renderCustomHighlightPayloadScript,
337
199
  } from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
338
- // Dispatcher entry alternative:
339
- // import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence'
200
+ import {
201
+ createRfBasicShikiCustomHighlightOptions,
202
+ rfBasicShikiTheme,
203
+ rfBasicThemeName,
204
+ } from '@peaceroad/markdown-it-renderer-fence/theme/rf-basic'
340
205
 
341
206
  const highlighter = await createHighlighter({
342
- themes: ['github-light'],
343
- langs: ['javascript', 'typescript', 'json'], // preload grammars; Shiki does not auto-detect language
207
+ themes: [rfBasicShikiTheme],
208
+ langs: ['javascript', 'typescript', 'json'],
344
209
  })
345
210
 
346
211
  const md = MarkdownIt({ html: true })
347
212
  .use(markdownItAttrs)
348
213
  .use(rendererFenceApi, {
349
- // highlightRenderer: 'api', // required only with dispatcher entry
350
- customHighlight: {
351
- provider: 'shiki',
214
+ customHighlight: createRfBasicShikiCustomHighlightOptions({
352
215
  highlighter,
353
- theme: 'github-light',
216
+ theme: rfBasicThemeName,
217
+ shikiScopeMode: 'role',
218
+ includeScopeStyles: false,
354
219
  transport: 'env',
355
- },
220
+ }),
356
221
  })
357
222
 
358
223
  const env = {}
359
- const html = md.render('```js\nconst x = 1\n```', env)
360
- const payloadScript = renderCustomHighlightPayloadScript(env) // id="pre-highlight-data"
361
- ```
362
-
363
- Browser-side apply:
224
+ const html = md.render('```js
225
+ const x = 1
226
+ ```', env)
227
+ const payloadScript = renderCustomHighlightPayloadScript(env)
364
228
 
365
- ```js
229
+ // Browser side:
366
230
  applyCustomHighlights(document)
367
231
  ```
368
232
 
369
- When using `colorScheme: 'auto'`, scheme resolution happens at apply time.
370
- If OS/browser theme changes after first apply, re-run apply (or use observer watch mode below).
371
-
372
- If you only need runtime apply on the browser, import runtime-only entry:
233
+ For lazy apply and color-scheme watching:
373
234
 
374
235
  ```js
375
236
  import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
376
- ```
377
-
378
- `langs` note:
379
-
380
- - `createHighlighter({ langs: [...] })` should include the languages you expect to render.
381
- - Shiki does not auto-detect language from code text.
382
- - if a target language is not loaded, this plugin falls back to Shiki `text` tokenization for that block (safe, but no syntax color buckets).
383
-
384
- ### Minimal API Example (highlight.js provider)
385
-
386
- ```js
387
- import MarkdownIt from 'markdown-it'
388
- import markdownItAttrs from 'markdown-it-attrs'
389
- import hljs from 'highlight.js'
390
- import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
391
- // Dispatcher entry alternative:
392
- // import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence'
393
-
394
- const md = MarkdownIt({ html: true })
395
- .use(markdownItAttrs)
396
- .use(rendererFenceApi, {
397
- // highlightRenderer: 'api', // required only with dispatcher entry
398
- customHighlight: {
399
- provider: 'hljs',
400
- hljsHighlight: (code, lang) => {
401
- if (lang && hljs.getLanguage(lang)) {
402
- return hljs.highlight(code, { language: lang }).value
403
- }
404
- return hljs.highlight(code, { language: 'plaintext' }).value
405
- },
406
- transport: 'env',
407
- },
408
- })
409
- ```
410
-
411
- ### API Providers
412
-
413
- - `customHighlight.provider: 'shiki'`
414
- Requires `customHighlight.highlighter.codeToTokens(...)` (synchronous).
415
- - `customHighlight.provider: 'hljs'`
416
- Uses `customHighlight.hljsHighlight`, fallback to `customHighlight.highlight`, then `md.options.highlight`.
417
- - `customHighlight.provider: 'custom'`
418
- Escape hatch. Requires synchronous `customHighlight.getRanges(...)`.
419
-
420
- ### Shiki Scope Modes
421
-
422
- - `auto` (default)
423
- - `color`
424
- - `semantic`
425
- - `keyword` (best for stable CSS-managed styling)
426
-
427
- Migration note (`0.5.0`):
428
-
429
- - legacy aliases like `json`, `bucket`, `keyword-only` are removed
430
- - use canonical values: `auto | color | semantic | keyword`
431
-
432
- Recommended production profile for API mode:
433
-
434
- ```js
435
- customHighlight: {
436
- provider: 'shiki',
437
- shikiScopeMode: 'keyword',
438
- includeScopeStyles: false
439
- }
440
- ```
441
-
442
- ### API Multi-Theme (Shiki, `v:1` additive payload)
443
-
444
- When you need runtime light/dark switching with Shiki color styles, pass object form theme:
445
237
 
446
- ```js
447
- customHighlight: {
448
- provider: 'shiki',
449
- highlighter,
450
- shikiScopeMode: 'color',
451
- includeScopeStyles: true,
452
- theme: {
453
- light: 'github-light',
454
- dark: 'github-dark',
455
- default: 'light',
456
- },
457
- }
458
- ```
459
-
460
- Runtime apply can choose variant:
461
-
462
- ```js
463
- applyCustomHighlights(document, { colorScheme: 'auto' }) // 'auto' | 'light' | 'dark'
464
- ```
465
-
466
- Auto re-apply on color-scheme change:
467
-
468
- ```js
469
238
  observeCustomHighlights(document, {
470
239
  applyOptions: { colorScheme: 'auto', incremental: true },
471
240
  watchColorScheme: true,
472
241
  })
473
242
  ```
474
243
 
475
- ### CMS / Copy-Paste Operation
476
-
477
- If you copy rendered HTML+payload JSON into another CMS/page:
478
-
479
- - API mode works only when runtime JS is also available on that page.
480
- - payload JSON alone is not enough.
481
- - if the target CMS cannot run custom JS, use `markup` mode instead.
482
- - this plugin does not auto-generate/write runtime JS files during markdown render.
244
+ Use markup mode instead when the target page cannot run runtime JavaScript.
483
245
 
484
- Practical patterns:
246
+ ## Theme
485
247
 
486
- 1. Site/app with bundler
487
- Bundle `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and run `observeCustomHighlights(...)`.
488
- 2. Static page with module scripts
489
- Load runtime entry via your ESM delivery path and call `applyCustomHighlights(...)`.
490
- 3. HTML-only CMS (no JS injection)
491
- Use `markup` mode (API mode is not suitable).
248
+ The optional preset theme is a package artifact, not an automatically loaded dependency.
492
249
 
493
- ### CLI Build Artifact Contract
494
-
495
- For CLI/static generation, treat API mode output as two artifacts:
250
+ ```css
251
+ @import "@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css";
252
+ ```
496
253
 
497
- 1. HTML artifact
498
- Includes `<pre data-pre-highlight="...">...</pre>` and payload JSON (`env` script or inline-script transport).
499
- 2. Runtime artifact
500
- Browser JS that imports `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and runs `applyCustomHighlights(...)` or `observeCustomHighlights(...)`.
254
+ The color preset covers base code blocks, `samp`, line features, highlight.js markup classes, Shiki API scopes, and highlight.js API scopes.
501
255
 
502
- Typical runtime bridge file:
256
+ For one-color print/monochrome output, use markup mode and import the opt-in monochrome override:
503
257
 
504
- ```js
505
- import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
506
-
507
- observeCustomHighlights(document, {
508
- applyOptions: { colorScheme: 'auto', incremental: true },
509
- watchColorScheme: true,
510
- })
258
+ ```css
259
+ @import "@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome.css";
511
260
  ```
512
261
 
513
- Then include that bridge in generated HTML:
262
+ For theme design details, see [`theme/rf-basic/README.md`](theme/rf-basic/README.md) and [`theme/rf-monochrome/README.md`](theme/rf-monochrome/README.md).
514
263
 
515
- ```html
516
- <script type="module" src="/assets/custom-highlight-runtime.js"></script>
517
- ```
264
+ ## Helper Exports
518
265
 
519
- ### API Options
520
-
521
- - `highlightRenderer` (`'markup' | 'api' | 'custom-highlight-api'`, default: `'markup'`): dispatcher mode selector (`/custom-highlight` entry does not need this).
522
- - `customHighlight.provider` (default: `'shiki'`): range source (`'shiki' | 'hljs' | 'custom'`).
523
- - `customHighlight.getRanges`: required when `provider: 'custom'`; must return synchronous ranges.
524
- - `customHighlight.highlighter`: Shiki highlighter object with synchronous `codeToTokens`.
525
- - `customHighlight.hljsHighlight`: highlight.js-style function used when `provider: 'hljs'`.
526
- - `customHighlight.highlight`: fallback highlight function for `hljs` provider.
527
- - `customHighlight.defaultLang`: default language when fence language is empty.
528
- - `customHighlight.theme`: Shiki theme name string, or object `{ light, dark, default? }` for dual-theme payload.
529
- - `customHighlight.shikiScopeMode` (default: `'auto'`): scope naming mode (`auto | color | semantic | keyword`).
530
- - `customHighlight.shikiKeywordClassifier`: custom classifier hook for keyword mode.
531
- - `customHighlight.shikiKeywordLangResolver`: custom language resolver hook for keyword mode.
532
- - `customHighlight.shikiKeywordLangAliases`: language alias map for keyword resolver.
533
- - `customHighlight.includeScopeStyles` (default: `true`): include payload `scopeStyles` when available.
534
- - `customHighlight.fallback` (`'plain' | 'markup'`, default: `'plain'`): server-side fallback renderer on provider errors.
535
- - `customHighlight.fallbackOn`: restrict fallback trigger reasons.
536
- - `customHighlight.transport` (`'env' | 'inline-script'`, default: `'env'`): payload transport target.
537
- - `customHighlight.idPrefix` (default: `'hl-'`): generated per-block payload id prefix.
538
- - `customHighlight.scopePrefix` (default: `'hl'`): scope name prefix used for highlight registration.
539
- - `customHighlight.lineFeatureStrategy` (`'hybrid' | 'disable'`, default: `'hybrid'`): keep or disable line-span features in API rendering.
540
-
541
- ### API Helper Exports
266
+ From `@peaceroad/markdown-it-renderer-fence/custom-highlight`:
542
267
 
543
268
  - `applyCustomHighlights`
544
269
  - `observeCustomHighlights`
@@ -550,23 +275,51 @@ Then include that bridge in generated HTML:
550
275
  - `customHighlightPayloadSchemaVersion`
551
276
  - `customHighlightPayloadSupportedVersions`
552
277
 
278
+ From `@peaceroad/markdown-it-renderer-fence/theme/rf-basic`:
279
+
280
+ - `rfBasicShikiTheme`
281
+ - `rfBasicThemeName`
282
+ - `createRfBasicShikiTheme`
283
+ - `createRfBasicShikiCustomHighlightOptions`
284
+ - `createRfBasicHighlightjsCustomHighlightOptions`
285
+ - `rfBasicSyntaxCssVars`
286
+ - `createRfBasicShikiRoleScopeColorVars`
287
+
288
+ From `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome`:
289
+
290
+ - `rfMonochromeShikiTheme`
291
+ - `rfMonochromeShikiThemeName`
292
+ - `rfMonochromeThemeName`
293
+ - `createRfMonochromeShikiTheme`
294
+
553
295
  ## Docs and Examples
554
296
 
555
- - API styling guide: `docs/custom-highlight-styling-guide.md`
556
- - default preset CSS sample: `docs/default-highlight-theme.css`
557
- - provider matrix page: `example/custom-highlight-provider-matrix.html`
297
+ - [Docs overview](docs/README.md)
298
+ - [Code Highlighting Design](docs/code-highlighting-design.md)
299
+ - [Custom Highlight Styling Guide](docs/custom-highlight-styling-guide.md)
300
+ - [rf-basic Theme README](theme/rf-basic/README.md)
301
+ - [rf-monochrome Theme README](theme/rf-monochrome/README.md)
302
+ - [Example README](example/README.md)
303
+ - `example/custom-highlight-provider-matrix.html`
304
+ - `example/monochrome-highlight-compare.html`
305
+ - `example/line-number-sample.html`
306
+ - `example/line-notes-sample.html`
307
+ - `example/samp-sample.html`
308
+
309
+ Examples are demonstration and verification assets. Do not import runtime or theme helpers from `example/`; use the public package entry points above.
558
310
 
559
311
  ## Tests and Benchmarks
560
312
 
561
- - all tests: `npm test`
562
- - provider contract: `npm run test:provider:contract`
563
- - keyword coverage: `npm run test:provider:keyword`
564
- - keyword holdout parity: `npm run test:provider:keyword:holdout`
565
- - performance baseline: `npm run test:performance`
566
- - runtime apply benchmark: `npm run test:performance:runtime`
313
+ ```bash
314
+ npm test
315
+ npm run test:provider:contract
316
+ npm run test:provider:role
317
+ npm run test:provider:role:holdout
318
+ npm run test:provider:matrix
319
+ npm run test:performance
320
+ npm run test:performance:runtime
321
+ ```
567
322
 
568
- ## License and Notices
323
+ ## License
569
324
 
570
- - Project license: MIT (`LICENSE`)
571
- - Third-party notices: `THIRD_PARTY_NOTICES.md`
572
- - `docs/default-highlight-theme.css` contains mappings derived from highlight.js theme families (BSD-3-Clause; see notices).
325
+ MIT (`LICENSE`)