@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 +491 -114
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/index.js +33 -503
- package/package.json +28 -4
- package/src/custom-highlight/option-validator.js +152 -0
- package/src/custom-highlight/payload-utils.js +43 -0
- package/src/custom-highlight/runtime-utils.js +83 -0
- package/src/custom-highlight/shiki-keyword-rules.js +407 -0
- package/src/custom-highlight/shiki-keyword.js +417 -0
- package/src/entry/custom-highlight-runtime.js +11 -0
- package/src/entry/custom-highlight.js +25 -0
- package/src/entry/markup-highlight.js +27 -0
- package/src/fence/render-api-constants.js +34 -0
- package/src/fence/render-api-provider.js +810 -0
- package/src/fence/render-api-renderer.js +145 -0
- package/src/fence/render-api-runtime.js +712 -0
- package/src/fence/render-api.js +107 -0
- package/src/fence/render-markup.js +177 -0
- package/src/fence/render-shared.js +590 -0
- package/src/utils/attr-utils.js +120 -0
- package/src/utils/pre-code-wrapper-parser.js +114 -0
package/README.md
CHANGED
|
@@ -1,177 +1,554 @@
|
|
|
1
1
|
# p7d-markdown-it-renderer-fence
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A `markdown-it` plugin for code block rendering and enhancements.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Default is `markup` mode. Custom Highlight API mode is available as an advanced/experimental path.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm i @peaceroad/markdown-it-renderer-fence markdown-it markdown-it-attrs
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
## Markup Mode (Default)
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
### Quick Start
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
md.render('Nyaan')
|
|
145
|
+
const a = 1
|
|
146
|
+
console.log(a)
|
|
81
147
|
```
|
|
82
148
|
~~~
|
|
83
149
|
|
|
84
|
-
|
|
85
|
-
<pre><code class="language-js" data-pre-start="1" style="counter-set:pre-line-number 1;"
|
|
86
|
-
<span class="pre-line"
|
|
87
|
-
<span class="pre-line">
|
|
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
|
-
|
|
157
|
+
Advanced line number control:
|
|
92
158
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
202
|
+
Wrap:
|
|
125
203
|
|
|
126
204
|
~~~md
|
|
127
205
|
```js {wrap}
|
|
128
|
-
const
|
|
206
|
+
const veryLongLine = '...'
|
|
129
207
|
```
|
|
130
208
|
~~~
|
|
131
209
|
|
|
132
|
-
|
|
133
|
-
<pre data-pre-wrap="true" style="white-space: pre-wrap; overflow-wrap: anywhere;"
|
|
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
|
-
|
|
216
|
+
Comment line marker:
|
|
142
217
|
|
|
143
218
|
~~~md
|
|
144
|
-
```samp {comment-
|
|
219
|
+
```samp {comment-mark="#"}
|
|
145
220
|
# comment
|
|
146
221
|
echo 1
|
|
147
222
|
```
|
|
148
223
|
~~~
|
|
149
224
|
|
|
150
|
-
|
|
151
|
-
<pre><samp data-pre-comment-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
Use API mode only if you need runtime range highlighting and can manage runtime/CSS integration in your app.
|
|
164
309
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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).
|