@peaceroad/markdown-it-renderer-fence 0.4.1 → 0.6.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
@@ -1,177 +1,554 @@
1
1
  # p7d-markdown-it-renderer-fence
2
2
 
3
- In code blocks, the `<samp>` tag is used with the language keywords `samp`, `shell`, and `console`.
3
+ A `markdown-it` plugin for code block rendering and enhancements.
4
4
 
5
- To add specific styles to code blocks and display line numbers in code blocks, enclose each line in a span element and set CSS.
5
+ Default is `markup` mode. Custom Highlight API mode is available as an advanced/experimental path.
6
6
 
7
- ## Use
7
+ ## Install
8
8
 
9
- It is assumed that you will use highlight.js and markdown-it-attrs together. It will probably work without them though.
9
+ ```bash
10
+ npm i @peaceroad/markdown-it-renderer-fence markdown-it markdown-it-attrs
11
+ ```
10
12
 
11
- ```js
12
- import mdit from 'markdown-it'
13
- import mditAttrs from 'markdown-it-attrs'
14
- import highlightjs from 'highlihgtjs'
15
- import mditRendererFence from '@peaceroad/markdown-it-renderer-fence'
13
+ If you use syntax highlighting, install a highlighter too (for example `highlight.js` or `shiki`).
16
14
 
17
- const md = mdit({
18
- langPrefix: 'language-',
19
- highlight: (str, lang) => {
20
- if (lang && highlightjs.getLanguage(lang)) {
21
- try {
22
- return highlightjs.highlight(str, { language: lang }).value
23
- } catch (__) {}
24
- }
25
- return md.utils.escapeHtml(str)
26
- }
27
- }).use(mditAttrs).use(mditRendererFence)
15
+ ## Entry Points
28
16
 
29
- const htmlCont = md.render('...')
30
- ```
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`).
31
25
 
32
- It is also intended to be used in conjunction with @peaceroad/markdown-it-figure-with-p-caption.
26
+ ## Markup Mode (Default)
33
27
 
34
- ## Code block to samp block
28
+ ### Quick Start
35
29
 
36
- ~~~
37
- [Markdown]
38
- ```samp
39
- $ pwd
40
- /home/User
30
+ Dispatcher entry:
31
+
32
+ ```js
33
+ import MarkdownIt from 'markdown-it'
34
+ import markdownItAttrs from 'markdown-it-attrs'
35
+ 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'
39
+
40
+ const md = MarkdownIt({
41
+ html: true,
42
+ langPrefix: 'language-',
43
+ highlight: (code, lang) => {
44
+ if (lang && hljs.getLanguage(lang)) {
45
+ try {
46
+ return hljs.highlight(code, { language: lang }).value
47
+ } catch (e) {}
48
+ }
49
+ return md.utils.escapeHtml(code)
50
+ },
51
+ })
52
+ .use(markdownItAttrs)
53
+ .use(rendererFence) // defaults to markup; direct markup entry can be leaner
41
54
  ```
42
- [HTML]
43
- <pre><samp>$ pwd
44
- /home/User
45
- </samp></pre>
46
55
 
56
+ ### Markup with Shiki
47
57
 
48
- [Markdown]
49
- ```shell
50
- $ pwd
51
- /home/User
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.
60
+
61
+ ```js
62
+ import MarkdownIt from 'markdown-it'
63
+ import markdownItAttrs from 'markdown-it-attrs'
64
+ import { createHighlighter } from 'shiki'
65
+ 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
+
86
+ const highlighter = await createHighlighter({
87
+ themes: ['github-light'],
88
+ langs, // preload grammars; Shiki does not auto-detect language
89
+ })
90
+
91
+ const md = MarkdownIt({
92
+ html: true,
93
+ langPrefix: 'language-',
94
+ highlight: (code, lang) => {
95
+ const targetLang = lang || 'text'
96
+ try {
97
+ return highlighter.codeToHtml(code, { lang: targetLang, theme: 'github-light' })
98
+ } catch (e) {
99
+ // when lang grammar is not loaded (or invalid), keep output safe
100
+ return md.utils.escapeHtml(code)
101
+ }
102
+ },
103
+ })
104
+ .use(markdownItAttrs)
105
+ .use(rendererFence)
52
106
  ```
53
- [HTML]
54
- <pre><samp class="language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash"><span class="hljs-built_in">pwd</span></span>
55
- /home/User
56
- </samp></pre>
57
107
 
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
58
115
 
59
- [Markdown]
60
- ```console
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/reset controls via `line-number-skip` / `line-number-reset`.
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
61
132
  $ pwd
62
- /home/User
63
133
  ```
64
- [HTML]
65
- <pre><samp class="language-console"><span class="hljs-meta prompt_">$ </span><span class="language-bash"><span class="hljs-built_in">pwd</span></span>
66
- /home/User
67
- </samp></pre>
68
134
  ~~~
69
135
 
136
+ ```html
137
+ <pre><samp class="language-shell">$ pwd
138
+ </samp></pre>
139
+ ```
70
140
 
71
- ## Add span elements to display line number.
72
-
73
- Add `start` or `data-pre-start` attribute by adding attributes used markdown-it-attrs.
74
- Line numbering is enabled only when `start` is a non-negative integer (`0`, `1`, `2`, ...).
141
+ Line numbers:
75
142
 
76
143
  ~~~md
77
144
  ```js {start="1"}
78
- import mdit from 'markdonw-it'
79
- const md = mdit()
80
- md.render('Nyaan')
145
+ const a = 1
146
+ console.log(a)
81
147
  ```
82
148
  ~~~
83
149
 
84
- ~~~html
85
- <pre><code class="language-js" data-pre-start="1" style="counter-set:pre-line-number 1;"><span class="pre-line"><span class="hljs-keyword">import</span> mdit <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;markdonw-it&#x27;</span></span>
86
- <span class="pre-line"><span class="hljs-keyword">const</span> md = <span class="hljs-title function_">mdit</span>()</span>
87
- <span class="pre-line">md.<span class="hljs-title function_">render</span>(<span class="hljs-string">&#x27;Nyaan&#x27;</span>)</span>
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>
88
154
  </code></pre>
89
- ~~~
155
+ ```
90
156
 
91
- CSS example: <https://codepen.io/peaceroad/pen/qBGpYGK>
157
+ Advanced line number control:
92
158
 
93
- If `start` is empty or not a non-negative integer (for example `start=""`, `start="abc"`, `start="1.5"`), the attribute is kept as `data-pre-start` but line-number wrapping/counter style is not enabled.
159
+ ~~~md
160
+ ```txt {start="25" line-number-skip="5" line-number-reset="6:136"}
161
+ line1
162
+ line2
163
+ line3
164
+ line4
165
+ ...
166
+ line6
167
+ ```
168
+ ~~~
94
169
 
95
- ## Add span elements to add background color to the row ranges.
170
+ ```html
171
+ <pre><code class="language-txt" data-pre-start="25" data-pre-line-number-skip="5" data-pre-line-number-reset="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
+ ```
96
180
 
97
- Add `em-lines` or `emphasize-lines` attribute by adding attributes used markdown-it-attrs.
98
- Open-ended ranges are supported: `-5` (1..5), `3-` (3..last), `-` (all).
181
+ Emphasis:
99
182
 
100
183
  ~~~md
101
- ``` {em-lines="2,4-5"}
102
- 1
103
- 2
104
- 3
105
- 4
106
- 5
107
- 6
184
+ ```js {em-lines="2,4-5"}
185
+ line1
186
+ line2
187
+ line3
188
+ line4
189
+ line5
108
190
  ```
109
191
  ~~~
110
192
 
111
- ~~~html
112
- [HTML]
113
- <pre><code data-pre-emphasis="2,4-5">1
114
- <span class="pre-lines-emphasis">2
115
- </span>3
116
- <span class="pre-lines-emphasis">4
117
- 5
118
- </span>6
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>
119
199
  </code></pre>
120
- ~~~
121
-
122
- ## Enable pre-wrap for code blocks
200
+ ```
123
201
 
124
- Add `wrap` or `pre-wrap` attribute (optionally `="true"`) by adding attributes used markdown-it-attrs.
202
+ Wrap:
125
203
 
126
204
  ~~~md
127
205
  ```js {wrap}
128
- const longLine = 'This line is long but wraps.'
206
+ const veryLongLine = '...'
129
207
  ```
130
208
  ~~~
131
209
 
132
- ~~~html
133
- <pre data-pre-wrap="true" style="white-space: pre-wrap; overflow-wrap: anywhere;"><code class="language-js">const longLine = 'This line is long but wraps.'
210
+ ```html
211
+ <pre data-pre-wrap="true" style="white-space: pre-wrap; overflow-wrap: anywhere;">
212
+ <code class="language-js">const veryLongLine = '...'
134
213
  </code></pre>
135
- ~~~
136
-
137
- When `setPreWrapStyle` is set to false, only `data-pre-wrap="true"` is added and the inline style is omitted.
138
-
139
- ## Mark comment lines in code blocks
214
+ ```
140
215
 
141
- Add `comment-line` attribute to mark comment lines. Lines that start with the given marker (after leading whitespace) are wrapped with `<span class="pre-comment-line">`.
216
+ Comment line marker:
142
217
 
143
218
  ~~~md
144
- ```samp {comment-line="#"}
219
+ ```samp {comment-mark="#"}
145
220
  # comment
146
221
  echo 1
147
222
  ```
148
223
  ~~~
149
224
 
150
- ~~~html
151
- <pre><samp data-pre-comment-line="#"><span class="pre-comment-line"># comment</span>
225
+ ```html
226
+ <pre><samp data-pre-comment-mark="#" class="language-samp">
227
+ <span class="pre-line-comment"># comment</span>
152
228
  echo 1
153
229
  </samp></pre>
154
- ~~~
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-reset`
239
+ - `em-lines`
240
+ - line-end spacer
241
+ - `comment-mark`
242
+ - `samp` conversion
243
+
244
+ Official line-number CSS contract:
245
+
246
+ ```css
247
+ .pre-line {
248
+ counter-increment: pre-line-number;
249
+ }
250
+
251
+ .pre-line::before {
252
+ content: counter(pre-line-number);
253
+ }
254
+
255
+ .pre-line.pre-line-no-number {
256
+ counter-increment: none;
257
+ }
258
+
259
+ .pre-line.pre-line-no-number::before {
260
+ content: "";
261
+ }
262
+ ```
263
+
264
+ ### Markup Options
265
+
266
+ - `attrsOrder` (default: `['class', 'id', 'data-*', 'style']`): output attribute order (`data-*` wildcard supported).
267
+ - `setHighlight` (default: `true`): call `md.options.highlight` when available.
268
+ - `setLineNumber` (default: `true`): enable line wrapper spans when `start` / `line-number-start` is valid.
269
+ - `setEmphasizeLines` (default: `true`): enable `em-lines` / `emphasize-lines`.
270
+ - `lineEndSpanThreshold` (default: `0`): append line-end spacer span when visual width threshold is met.
271
+ - `setLineEndSpan`: alias of `lineEndSpanThreshold`.
272
+ - `lineEndSpanClass` (default: `'pre-lineend-spacer'`): CSS class of the spacer span.
273
+ - `setPreWrapStyle` (default: `true`): inject inline pre-wrap style for wrap-enabled blocks.
274
+ - `sampLang` (default: `'shell,console'`): additional fence langs rendered as `<samp>`.
275
+ - `langPrefix` (default: `md.options.langPrefix || 'language-'`): class prefix for language class.
276
+ - `useHighlightPre` (default: `false`): passthrough highlighter `<pre><code>` wrappers and skip line-splitting features.
277
+ - `onFenceDecision` (default: `null`): debug hook for per-fence branch decisions.
278
+ - `onFenceDecisionTiming` (default: `false`): include timing fields in `onFenceDecision`.
279
+
280
+ `em-lines` syntax note:
281
+
282
+ - supports single values (`2`) and ranges (`4-6`)
283
+ - supports open-ended forms (`3-`, `-2`)
284
+ - reversed ranges are normalized (`5-3` behaves as `3-5`)
285
+
286
+ Line-number attr syntax note:
287
+
288
+ - `line-number-start` is the long form of `start`; rendered output still uses `data-pre-start`.
289
+ - `line-number-skip` supports single values (`2`), ranges (`4-6`), and open-ended forms (`3-`).
290
+ - `line-number-reset` uses `line:number` pairs (for example `6:136,14:220`).
291
+ - `line-number-skip` / `line-number-reset` are applied only when source and highlighted logical line counts match; otherwise renderer falls back to plain sequential numbering.
292
+
293
+ Migration note (`0.5.0`):
294
+
295
+ - `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
296
+ - use `comment-mark` / `data-pre-comment-mark` / `pre-line-comment`
297
+
298
+ ## Custom Highlight API Mode (Experimental / Advanced)
155
299
 
156
- Note: this feature relies on line splitting and is disabled when `useHighlightPre` is true and `<pre><code>` is provided by the highlighter.
157
- If a highlighter changes logical line count (for example by injecting extra lines), comment-line marking is skipped to avoid wrong line mapping.
158
- CRLF/LF mixed newlines are supported for logical line-count checks.
300
+ Custom Highlight API mode renders plain code text and emits range payloads for browser-side Custom Highlight API application.
159
301
 
302
+ Important:
160
303
 
161
- ## Options
304
+ - You must run runtime apply in the browser (`applyCustomHighlights` or `observeCustomHighlights`).
305
+ - Without runtime apply, payload exists but browser highlights are not activated.
306
+ - `test/custom-highlight/pre-highlight.js` is a demo helper, not the package runtime API contract.
162
307
 
163
- The following options can be specified when initializing the plugin:
308
+ Use API mode only if you need runtime range highlighting and can manage runtime/CSS integration in your app.
164
309
 
165
- - attrsOrder: default ['class','id','data-*','style'] order of attributes in output; wildcards supported.
166
- - setHighlight: default true — enable calling highlight function (e.g., highlight.js).
167
- - setLineNumber: default true — wrap lines in spans for line numbers.
168
- - setEmphasizeLines: default true — enable emphasis based on emphasize-lines attribute.
169
- - lineEndSpanThreshold: default 0 — character count threshold to append end-of-line span (0 to disable).
170
- - setLineEndSpan: alias for lineEndSpanThreshold.
171
- - lineEndSpanClass: default 'pre-lineend-spacer' — CSS class for end-of-line span.
172
- - setPreWrapStyle: default true — include inline pre-wrap styles on `<pre>` when wrap is enabled.
173
- - sampLang: default 'shell,console' — comma-separated list of languages (in addition to 'samp') that will be rendered using `<samp>`.
174
- - langPrefix: default md.options.langPrefix || 'language-' — prefix for language class on code blocks.
175
- - useHighlightPre: default false — if highlight returns `<pre><code>`, keep that wrapper and skip line-splitting features; attributes are still merged.
310
+ ### Minimal API Example (Shiki provider)
311
+
312
+ ```js
313
+ import MarkdownIt from 'markdown-it'
314
+ import markdownItAttrs from 'markdown-it-attrs'
315
+ import { createHighlighter } from 'shiki'
316
+ import rendererFenceApi, {
317
+ applyCustomHighlights,
318
+ renderCustomHighlightPayloadScript,
319
+ } from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
320
+ // Dispatcher entry alternative:
321
+ // import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence'
322
+
323
+ const highlighter = await createHighlighter({
324
+ themes: ['github-light'],
325
+ langs: ['javascript', 'typescript', 'json'], // preload grammars; Shiki does not auto-detect language
326
+ })
327
+
328
+ const md = MarkdownIt({ html: true })
329
+ .use(markdownItAttrs)
330
+ .use(rendererFenceApi, {
331
+ // highlightRenderer: 'api', // required only with dispatcher entry
332
+ customHighlight: {
333
+ provider: 'shiki',
334
+ highlighter,
335
+ theme: 'github-light',
336
+ transport: 'env',
337
+ },
338
+ })
339
+
340
+ const env = {}
341
+ const html = md.render('```js\nconst x = 1\n```', env)
342
+ const payloadScript = renderCustomHighlightPayloadScript(env) // id="pre-highlight-data"
343
+ ```
344
+
345
+ Browser-side apply:
346
+
347
+ ```js
348
+ applyCustomHighlights(document)
349
+ ```
350
+
351
+ When using `colorScheme: 'auto'`, scheme resolution happens at apply time.
352
+ If OS/browser theme changes after first apply, re-run apply (or use observer watch mode below).
353
+
354
+ If you only need runtime apply on the browser, import runtime-only entry:
355
+
356
+ ```js
357
+ import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
358
+ ```
359
+
360
+ `langs` note:
361
+
362
+ - `createHighlighter({ langs: [...] })` should include the languages you expect to render.
363
+ - Shiki does not auto-detect language from code text.
364
+ - if a target language is not loaded, this plugin falls back to Shiki `text` tokenization for that block (safe, but no syntax color buckets).
365
+
366
+ ### Minimal API Example (highlight.js provider)
367
+
368
+ ```js
369
+ import MarkdownIt from 'markdown-it'
370
+ import markdownItAttrs from 'markdown-it-attrs'
371
+ import hljs from 'highlight.js'
372
+ import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
373
+ // Dispatcher entry alternative:
374
+ // import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence'
375
+
376
+ const md = MarkdownIt({ html: true })
377
+ .use(markdownItAttrs)
378
+ .use(rendererFenceApi, {
379
+ // highlightRenderer: 'api', // required only with dispatcher entry
380
+ customHighlight: {
381
+ provider: 'hljs',
382
+ hljsHighlight: (code, lang) => {
383
+ if (lang && hljs.getLanguage(lang)) {
384
+ return hljs.highlight(code, { language: lang }).value
385
+ }
386
+ return hljs.highlight(code, { language: 'plaintext' }).value
387
+ },
388
+ transport: 'env',
389
+ },
390
+ })
391
+ ```
392
+
393
+ ### API Providers
394
+
395
+ - `customHighlight.provider: 'shiki'`
396
+ Requires `customHighlight.highlighter.codeToTokens(...)` (synchronous).
397
+ - `customHighlight.provider: 'hljs'`
398
+ Uses `customHighlight.hljsHighlight`, fallback to `customHighlight.highlight`, then `md.options.highlight`.
399
+ - `customHighlight.provider: 'custom'`
400
+ Escape hatch. Requires synchronous `customHighlight.getRanges(...)`.
401
+
402
+ ### Shiki Scope Modes
403
+
404
+ - `auto` (default)
405
+ - `color`
406
+ - `semantic`
407
+ - `keyword` (best for stable CSS-managed styling)
408
+
409
+ Migration note (`0.5.0`):
410
+
411
+ - legacy aliases like `json`, `bucket`, `keyword-only` are removed
412
+ - use canonical values: `auto | color | semantic | keyword`
413
+
414
+ Recommended production profile for API mode:
415
+
416
+ ```js
417
+ customHighlight: {
418
+ provider: 'shiki',
419
+ shikiScopeMode: 'keyword',
420
+ includeScopeStyles: false
421
+ }
422
+ ```
423
+
424
+ ### API Multi-Theme (Shiki, `v:1` additive payload)
425
+
426
+ When you need runtime light/dark switching with Shiki color styles, pass object form theme:
427
+
428
+ ```js
429
+ customHighlight: {
430
+ provider: 'shiki',
431
+ highlighter,
432
+ shikiScopeMode: 'color',
433
+ includeScopeStyles: true,
434
+ theme: {
435
+ light: 'github-light',
436
+ dark: 'github-dark',
437
+ default: 'light',
438
+ },
439
+ }
440
+ ```
441
+
442
+ Runtime apply can choose variant:
443
+
444
+ ```js
445
+ applyCustomHighlights(document, { colorScheme: 'auto' }) // 'auto' | 'light' | 'dark'
446
+ ```
447
+
448
+ Auto re-apply on color-scheme change:
449
+
450
+ ```js
451
+ observeCustomHighlights(document, {
452
+ applyOptions: { colorScheme: 'auto', incremental: true },
453
+ watchColorScheme: true,
454
+ })
455
+ ```
456
+
457
+ ### CMS / Copy-Paste Operation
458
+
459
+ If you copy rendered HTML+payload JSON into another CMS/page:
460
+
461
+ - API mode works only when runtime JS is also available on that page.
462
+ - payload JSON alone is not enough.
463
+ - if the target CMS cannot run custom JS, use `markup` mode instead.
464
+ - this plugin does not auto-generate/write runtime JS files during markdown render.
465
+
466
+ Practical patterns:
467
+
468
+ 1. Site/app with bundler
469
+ Bundle `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and run `observeCustomHighlights(...)`.
470
+ 2. Static page with module scripts
471
+ Load runtime entry via your ESM delivery path and call `applyCustomHighlights(...)`.
472
+ 3. HTML-only CMS (no JS injection)
473
+ Use `markup` mode (API mode is not suitable).
474
+
475
+ ### CLI Build Artifact Contract
476
+
477
+ For CLI/static generation, treat API mode output as two artifacts:
478
+
479
+ 1. HTML artifact
480
+ Includes `<pre data-pre-highlight="...">...</pre>` and payload JSON (`env` script or inline-script transport).
481
+ 2. Runtime artifact
482
+ Browser JS that imports `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and runs `applyCustomHighlights(...)` or `observeCustomHighlights(...)`.
483
+
484
+ Typical runtime bridge file:
485
+
486
+ ```js
487
+ import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
488
+
489
+ observeCustomHighlights(document, {
490
+ applyOptions: { colorScheme: 'auto', incremental: true },
491
+ watchColorScheme: true,
492
+ })
493
+ ```
494
+
495
+ Then include that bridge in generated HTML:
496
+
497
+ ```html
498
+ <script type="module" src="/assets/custom-highlight-runtime.js"></script>
499
+ ```
176
500
 
177
- When `useHighlightPre` is true and the highlight output contains `<pre><code>`, line-splitting features are disabled (setLineNumber, setEmphasizeLines, lineEndSpanThreshold, comment-line). In this mode `<samp>` conversion is not possible, so avoid `useHighlightPre` when you need samp output.
501
+ ### API Options
502
+
503
+ - `highlightRenderer` (`'markup' | 'api' | 'custom-highlight-api'`, default: `'markup'`): dispatcher mode selector (`/custom-highlight` entry does not need this).
504
+ - `customHighlight.provider` (default: `'shiki'`): range source (`'shiki' | 'hljs' | 'custom'`).
505
+ - `customHighlight.getRanges`: required when `provider: 'custom'`; must return synchronous ranges.
506
+ - `customHighlight.highlighter`: Shiki highlighter object with synchronous `codeToTokens`.
507
+ - `customHighlight.hljsHighlight`: highlight.js-style function used when `provider: 'hljs'`.
508
+ - `customHighlight.highlight`: fallback highlight function for `hljs` provider.
509
+ - `customHighlight.defaultLang`: default language when fence language is empty.
510
+ - `customHighlight.theme`: Shiki theme name string, or object `{ light, dark, default? }` for dual-theme payload.
511
+ - `customHighlight.shikiScopeMode` (default: `'auto'`): scope naming mode (`auto | color | semantic | keyword`).
512
+ - `customHighlight.shikiKeywordClassifier`: custom classifier hook for keyword mode.
513
+ - `customHighlight.shikiKeywordLangResolver`: custom language resolver hook for keyword mode.
514
+ - `customHighlight.shikiKeywordLangAliases`: language alias map for keyword resolver.
515
+ - `customHighlight.includeScopeStyles` (default: `true`): include payload `scopeStyles` when available.
516
+ - `customHighlight.fallback` (`'plain' | 'markup'`, default: `'plain'`): server-side fallback renderer on provider errors.
517
+ - `customHighlight.fallbackOn`: restrict fallback trigger reasons.
518
+ - `customHighlight.transport` (`'env' | 'inline-script'`, default: `'env'`): payload transport target.
519
+ - `customHighlight.idPrefix` (default: `'hl-'`): generated per-block payload id prefix.
520
+ - `customHighlight.scopePrefix` (default: `'hl'`): scope name prefix used for highlight registration.
521
+ - `customHighlight.lineFeatureStrategy` (`'hybrid' | 'disable'`, default: `'hybrid'`): keep or disable line-span features in API rendering.
522
+
523
+ ### API Helper Exports
524
+
525
+ - `applyCustomHighlights`
526
+ - `observeCustomHighlights`
527
+ - `clearCustomHighlights`
528
+ - `shouldRuntimeFallback`
529
+ - `getCustomHighlightPayloadMap`
530
+ - `renderCustomHighlightPayloadScript`
531
+ - `renderCustomHighlightScopeStyleTag`
532
+ - `customHighlightPayloadSchemaVersion`
533
+ - `customHighlightPayloadSupportedVersions`
534
+
535
+ ## Docs and Examples
536
+
537
+ - API styling guide: `docs/custom-highlight-styling-guide.md`
538
+ - default preset CSS sample: `docs/default-highlight-theme.css`
539
+ - provider matrix page: `example/custom-highlight-provider-matrix.html`
540
+
541
+ ## Tests and Benchmarks
542
+
543
+ - all tests: `npm test`
544
+ - provider contract: `npm run test:provider:contract`
545
+ - keyword coverage: `npm run test:provider:keyword`
546
+ - keyword holdout parity: `npm run test:provider:keyword:holdout`
547
+ - performance baseline: `npm run test:performance`
548
+ - runtime apply benchmark: `npm run test:performance:runtime`
549
+
550
+ ## License and Notices
551
+
552
+ - Project license: MIT (`LICENSE`)
553
+ - Third-party notices: `THIRD_PARTY_NOTICES.md`
554
+ - `docs/default-highlight-theme.css` contains mappings derived from highlight.js theme families (BSD-3-Clause; see notices).