@peaceroad/markdown-it-renderer-fence 0.7.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 +179 -470
- package/docs/README.md +37 -0
- package/docs/code-highlighting-design.md +321 -0
- package/docs/custom-highlight-styling-guide.md +210 -0
- package/package.json +25 -13
- package/src/custom-highlight/option-validator.js +5 -5
- package/src/custom-highlight/payload-utils.js +4 -9
- package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +249 -54
- package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +107 -99
- package/src/custom-highlight/shiki-theme-role.js +176 -0
- package/src/fence/line-notes.js +27 -9
- package/src/fence/render-api-constants.js +1 -1
- package/src/fence/render-api-provider.js +61 -53
- package/src/fence/render-api-renderer.js +26 -10
- package/src/fence/render-api-runtime.js +29 -10
- package/src/fence/render-api.js +20 -12
- package/src/fence/render-markup.js +34 -24
- package/src/fence/render-shared.js +150 -30
- package/src/utils/attr-utils.js +10 -0
- package/theme/_shared/rf-core.css +170 -0
- package/theme/line-notes.css +94 -0
- package/theme/line-number.css +64 -0
- package/theme/rf-basic/README.md +117 -0
- package/theme/rf-basic/api/highlightjs.css +71 -0
- package/theme/rf-basic/api/shiki.css +67 -0
- package/theme/rf-basic/base.css +5 -0
- package/theme/rf-basic/index.css +7 -0
- package/theme/rf-basic/index.js +164 -0
- package/theme/rf-basic/markup/highlightjs.css +77 -0
- package/theme/rf-basic/markup/shiki.css +11 -0
- package/theme/rf-monochrome/README.md +84 -0
- package/theme/rf-monochrome/base.css +5 -0
- package/theme/rf-monochrome/index.css +5 -0
- package/theme/rf-monochrome/index.js +71 -0
- package/theme/rf-monochrome/markup/highlightjs.css +150 -0
- package/theme/rf-monochrome/markup/shiki.css +38 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# p7d-markdown-it-renderer-fence
|
|
2
2
|
|
|
3
|
-
A `markdown-it`
|
|
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
|
|
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
|
-
|
|
13
|
+
Install a highlighter separately when you need syntax highlighting, for example `highlight.js` or `shiki`.
|
|
14
14
|
|
|
15
|
-
##
|
|
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)
|
|
36
|
+
.use(rendererFence)
|
|
54
37
|
```
|
|
55
38
|
|
|
56
|
-
### Markup
|
|
39
|
+
### Shiki in Markup Mode
|
|
57
40
|
|
|
58
|
-
|
|
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,
|
|
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:
|
|
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,133 +66,75 @@ const md = MarkdownIt({
|
|
|
105
66
|
.use(rendererFence)
|
|
106
67
|
```
|
|
107
68
|
|
|
108
|
-
|
|
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
|
-
- sidecar line notes via immediate `line-notes` fence (`notes` alias).
|
|
124
|
-
|
|
125
|
-
### Fence Attribute Examples
|
|
126
|
-
|
|
127
|
-
Simplified HTML below focuses on renderer-fence structure (not full highlighter token spans).
|
|
128
|
-
|
|
129
|
-
`samp` conversion:
|
|
130
|
-
|
|
131
|
-
~~~md
|
|
132
|
-
```shell
|
|
133
|
-
$ pwd
|
|
134
|
-
```
|
|
135
|
-
~~~
|
|
136
|
-
|
|
137
|
-
```html
|
|
138
|
-
<pre><samp class="language-shell">$ pwd
|
|
139
|
-
</samp></pre>
|
|
140
|
-
```
|
|
69
|
+
## Entry Points
|
|
141
70
|
|
|
142
|
-
|
|
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/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 |
|
|
81
|
+
| `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome` | one-color Shiki markup theme object |
|
|
82
|
+
| `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome.css` | complete one-color markup preset CSS |
|
|
83
|
+
|
|
84
|
+
Use `@peaceroad/markdown-it-renderer-fence/theme/rf-basic` for JavaScript helpers. CSS assets are exposed as CSS subpaths.
|
|
85
|
+
|
|
86
|
+
Split preset CSS is also available:
|
|
87
|
+
|
|
88
|
+
- `theme/line-number.css`
|
|
89
|
+
- `theme/line-notes.css`
|
|
90
|
+
- `theme/rf-basic/base.css`
|
|
91
|
+
- `theme/rf-basic/api/highlightjs.css`
|
|
92
|
+
- `theme/rf-basic/api/shiki.css`
|
|
93
|
+
- `theme/rf-basic/markup/shiki.css`
|
|
94
|
+
- `theme/rf-basic/markup/highlightjs.css`
|
|
95
|
+
- `theme/rf-monochrome.css`
|
|
96
|
+
- `theme/rf-monochrome/markup/highlightjs.css`
|
|
97
|
+
- `theme/rf-monochrome/markup/shiki.css`
|
|
98
|
+
|
|
99
|
+
The plugin does not auto-load CSS or browser runtime JavaScript.
|
|
100
|
+
|
|
101
|
+
## Features
|
|
102
|
+
|
|
103
|
+
- markup mode with highlighter-owned HTML (`md.options.highlight`)
|
|
104
|
+
- `<samp>` rendering for `samp`, `shell`, and `console` fence languages
|
|
105
|
+
- per-fence `{samp}` / `{code}` tag overrides
|
|
106
|
+
- line numbers via `start` / `line-number-start`
|
|
107
|
+
- advanced line-number controls via `line-number-skip` / `line-number-set`
|
|
108
|
+
- emphasized lines via `em-lines` / `emphasize-lines`
|
|
109
|
+
- optional long-line spacer via `lineEndSpanThreshold`
|
|
110
|
+
- optional pre-wrap via `{wrap}` / `{pre-wrap}`
|
|
111
|
+
- comment markers via `comment-mark`
|
|
112
|
+
- sidecar line notes via an immediate `line-notes` fence (`notes` alias)
|
|
113
|
+
- advanced Custom Highlight API mode for span-free code text plus browser-applied ranges
|
|
114
|
+
- optional first-party theme CSS and Shiki/provider helper exports
|
|
115
|
+
|
|
116
|
+
## Fence Attribute Examples
|
|
143
117
|
|
|
144
118
|
~~~md
|
|
145
|
-
```js {start="1"}
|
|
119
|
+
```js {start="1" em-lines="2"}
|
|
146
120
|
const a = 1
|
|
147
121
|
console.log(a)
|
|
148
122
|
```
|
|
149
123
|
~~~
|
|
150
124
|
|
|
151
|
-
```html
|
|
152
|
-
<pre><code class="language-js" data-pre-start="1" style="counter-set:pre-line-number 1;">
|
|
153
|
-
<span class="pre-line">const a = 1</span>
|
|
154
|
-
<span class="pre-line">console.log(a)</span>
|
|
155
|
-
</code></pre>
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Advanced line number control:
|
|
159
|
-
|
|
160
|
-
~~~md
|
|
161
|
-
```txt {start="25" line-number-skip="5" line-number-set="6:136"}
|
|
162
|
-
line1
|
|
163
|
-
line2
|
|
164
|
-
line3
|
|
165
|
-
line4
|
|
166
|
-
...
|
|
167
|
-
line6
|
|
168
|
-
```
|
|
169
|
-
~~~
|
|
170
|
-
|
|
171
|
-
```html
|
|
172
|
-
<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;">
|
|
173
|
-
<span class="pre-line">line1</span>
|
|
174
|
-
<span class="pre-line">line2</span>
|
|
175
|
-
<span class="pre-line">line3</span>
|
|
176
|
-
<span class="pre-line">line4</span>
|
|
177
|
-
<span class="pre-line pre-line-no-number">...</span>
|
|
178
|
-
<span class="pre-line" style="counter-set:pre-line-number 136;">line6</span>
|
|
179
|
-
</code></pre>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Emphasis:
|
|
183
|
-
|
|
184
|
-
~~~md
|
|
185
|
-
```js {em-lines="2,4-5"}
|
|
186
|
-
line1
|
|
187
|
-
line2
|
|
188
|
-
line3
|
|
189
|
-
line4
|
|
190
|
-
line5
|
|
191
|
-
```
|
|
192
|
-
~~~
|
|
193
|
-
|
|
194
|
-
```html
|
|
195
|
-
<pre><code class="language-js" data-pre-emphasis="2,4-5">line1
|
|
196
|
-
<span class="pre-lines-emphasis">line2</span>
|
|
197
|
-
line3
|
|
198
|
-
<span class="pre-lines-emphasis">line4
|
|
199
|
-
line5</span>
|
|
200
|
-
</code></pre>
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Wrap:
|
|
204
|
-
|
|
205
125
|
~~~md
|
|
206
|
-
```
|
|
207
|
-
|
|
126
|
+
```bash {samp comment-mark="#"}
|
|
127
|
+
# setup
|
|
128
|
+
export NODE_ENV=test
|
|
208
129
|
```
|
|
209
130
|
~~~
|
|
210
131
|
|
|
211
|
-
```html
|
|
212
|
-
<pre data-pre-wrap="true" style="white-space: pre-wrap; overflow-wrap: anywhere;">
|
|
213
|
-
<code class="language-js">const veryLongLine = '...'
|
|
214
|
-
</code></pre>
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Comment line marker:
|
|
218
|
-
|
|
219
132
|
~~~md
|
|
220
|
-
```
|
|
221
|
-
#
|
|
222
|
-
echo 1
|
|
133
|
+
```shell {code}
|
|
134
|
+
# render as <code> even though shell is normally sampLang
|
|
223
135
|
```
|
|
224
136
|
~~~
|
|
225
137
|
|
|
226
|
-
```html
|
|
227
|
-
<pre><samp data-pre-comment-mark="#" class="language-samp">
|
|
228
|
-
<span class="pre-line-comment"># comment</span>
|
|
229
|
-
echo 1
|
|
230
|
-
</samp></pre>
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Line notes:
|
|
234
|
-
|
|
235
138
|
~~~md
|
|
236
139
|
```js {start="5"}
|
|
237
140
|
const a = 1
|
|
@@ -239,135 +142,56 @@ console.log(a)
|
|
|
239
142
|
```
|
|
240
143
|
```line-notes
|
|
241
144
|
1: setup {width="7em"}
|
|
242
|
-
2
|
|
145
|
+
2: result {width="10em"}
|
|
243
146
|
```
|
|
244
147
|
~~~
|
|
245
148
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
- See [`example/line-notes.css`](./example/line-notes.css) for the companion line-note stylesheet.
|
|
293
|
-
- See [`example/line-notes-sample.html`](./example/line-notes-sample.html) for sidecar line-note examples.
|
|
294
|
-
|
|
295
|
-
Note:
|
|
296
|
-
|
|
297
|
-
- renderer-fence emits `counter-set` as the displayed line number value itself
|
|
298
|
-
- the reference CSS increments the counter in `span.pre-line::after`, so `start="1"` maps directly to `counter-set:pre-line-number 1;`
|
|
299
|
-
- `line-number-set="6:136"` similarly emits `counter-set:pre-line-number 136;` on that line wrapper
|
|
300
|
-
- the minimum contract above is the required part; `example/line-number.css` adds gutter width, divider line, and whitespace handling
|
|
301
|
-
- if a consumer uses a different counter strategy, its CSS must be aligned with this HTML contract
|
|
302
|
-
|
|
303
|
-
### Markup Options
|
|
304
|
-
|
|
305
|
-
- `attrsOrder` (default: `['class', 'id', 'data-*', 'style']`): output attribute order (`data-*` wildcard supported).
|
|
306
|
-
- `setHighlight` (default: `true`): call `md.options.highlight` when available.
|
|
307
|
-
- `setLineNumber` (default: `true`): enable line wrapper spans when `start` / `line-number-start` is valid.
|
|
308
|
-
- `setEmphasizeLines` (default: `true`): enable `em-lines` / `emphasize-lines`.
|
|
309
|
-
- `lineEndSpanThreshold` (default: `0`): append line-end spacer span when visual width threshold is met.
|
|
310
|
-
- `setLineEndSpan`: alias of `lineEndSpanThreshold`.
|
|
311
|
-
- `lineEndSpanClass` (default: `'pre-lineend-spacer'`): CSS class of the spacer span.
|
|
312
|
-
- `setPreWrapStyle` (default: `true`): inject inline pre-wrap style for wrap-enabled blocks.
|
|
313
|
-
- `sampLang` (default: `'shell,console'`): additional fence langs rendered as `<samp>`.
|
|
314
|
-
- `langPrefix` (default: `md.options.langPrefix || 'language-'`): class prefix for language class.
|
|
315
|
-
- `useHighlightPre` (default: `false`): passthrough highlighter `<pre><code>` wrappers and skip line-splitting features.
|
|
316
|
-
- `onFenceDecision` (default: `null`): debug hook for per-fence branch decisions.
|
|
317
|
-
- `onFenceDecisionTiming` (default: `false`): include timing fields in `onFenceDecision`.
|
|
318
|
-
|
|
319
|
-
`em-lines` syntax note:
|
|
320
|
-
|
|
321
|
-
- supports single values (`2`) and ranges (`4-6`)
|
|
322
|
-
- supports open-ended forms (`3-`, `-2`)
|
|
323
|
-
- reversed ranges are normalized (`5-3` behaves as `3-5`)
|
|
324
|
-
|
|
325
|
-
Line-number attr syntax note:
|
|
326
|
-
|
|
327
|
-
- `line-number-start` is the long form of `start`; rendered output still uses `data-pre-start`.
|
|
328
|
-
- `line-number-skip` supports single values (`2`), ranges (`4-6`), and open-ended forms (`3-`).
|
|
329
|
-
- `line-number-set` uses `line:number` pairs (for example `6:136,14:220`).
|
|
330
|
-
- `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.
|
|
331
|
-
- `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.
|
|
332
|
-
|
|
333
|
-
Line-note syntax note:
|
|
334
|
-
|
|
335
|
-
- `line-notes` is the canonical sidecar fence name; `notes` is a short alias.
|
|
336
|
-
- Sidecar notes are folded only when they immediately follow a code/samp fence.
|
|
337
|
-
- Immediate sidecar folding is fail-closed: if any non-empty line is malformed, a note entry is incomplete, or the same note start line appears twice, the `line-notes` fence stays as a normal literal fence block.
|
|
338
|
-
- Syntax supports `N: text`, `N-M: text`, `N:text`, and `N-M:text`.
|
|
339
|
-
- Per-note attrs can be appended in markdown-it-attrs style on the same line for single-line notes, for example `5: cache lookup key {width="8rem"}`.
|
|
340
|
-
- For multiline notes, prefer a trailing attrs-only continuation line, for example ` {width="11rem"}`.
|
|
341
|
-
- Continuation lines start with indentation (` ` or tab) and append multiline note text to the previous note.
|
|
342
|
-
- Output keeps note text outside `<code>/<samp>` in a sibling note layer for safer copy semantics.
|
|
343
|
-
- Line-note blocks use the external wrapper contract `pre-wrapper-line-notes`.
|
|
344
|
-
- `data-pre-line-notes-layout="anchor"` is added only for simple non-overlapping note layouts. The current check is intentionally lightweight: it uses the note start line plus the note text's explicit logical line count, so a two-line note starting at line `3` prevents another anchored note from starting at line `4`.
|
|
345
|
-
- This anchor-safety check does not measure CSS wrapping. If a note wraps because of narrow CSS width, consumer CSS should still be prepared to fall back to a below-block list.
|
|
346
|
-
- When anchor positioning is unavailable or the layout is not marked safe, consumer CSS should render notes as a below-block list. The reference stylesheet uses `data-pre-line-note-label` so the fallback still shows which source line each note belongs to.
|
|
347
|
-
- In anchor layouts, the rendered line HTML carries note metadata on `.pre-line`, and the anchor itself is attached to `.pre-line-content` so notes can start from the rendered code end.
|
|
348
|
-
- Accessibility contract: the rendered `.pre-line-content` carries `aria-describedby`, and each note item renders with a stable `id` plus `role="note"`, so assistive tech can associate the note text with its anchor line.
|
|
349
|
-
- When a sidecar fence is folded, the previous fence token `map` is extended to cover the absorbed note-fence lines so editor scroll/jump integrations can still treat the merged output as one source span.
|
|
350
|
-
- `line-notes` are applied only when source and highlighted logical line counts match; otherwise note markup is skipped.
|
|
351
|
-
|
|
352
|
-
Migration note (`0.5.0`):
|
|
353
|
-
|
|
354
|
-
- `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
|
|
355
|
-
- use `comment-mark` / `data-pre-comment-mark` / `pre-line-comment`
|
|
356
|
-
- `line-number-reset` was removed; use `line-number-set`
|
|
357
|
-
|
|
358
|
-
## Custom Highlight API Mode (Experimental / Advanced)
|
|
359
|
-
|
|
360
|
-
Custom Highlight API mode renders plain code text and emits range payloads for browser-side Custom Highlight API application.
|
|
361
|
-
|
|
362
|
-
Important:
|
|
363
|
-
|
|
364
|
-
- You must run runtime apply in the browser (`applyCustomHighlights` or `observeCustomHighlights`).
|
|
365
|
-
- Without runtime apply, payload exists but browser highlights are not activated.
|
|
366
|
-
- `test/custom-highlight/pre-highlight.js` is a demo helper, not the package runtime API contract.
|
|
367
|
-
|
|
368
|
-
Use API mode only if you need runtime range highlighting and can manage runtime/CSS integration in your app.
|
|
369
|
-
|
|
370
|
-
### Minimal API Example (Shiki provider)
|
|
149
|
+
For emitted HTML contracts and visual samples, see the files listed in [Docs and Examples](#docs-and-examples).
|
|
150
|
+
|
|
151
|
+
## Main Options
|
|
152
|
+
|
|
153
|
+
### Markup / shared options
|
|
154
|
+
|
|
155
|
+
| Option | Summary |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `attrsOrder` | output attribute order; supports `data-*` wildcard |
|
|
158
|
+
| `setHighlight` | call `md.options.highlight` when available |
|
|
159
|
+
| `setLineNumber` | enable line wrapping when `start` / `line-number-start` is valid |
|
|
160
|
+
| `setEmphasizeLines` | enable `em-lines` / `emphasize-lines` |
|
|
161
|
+
| `lineEndSpanThreshold` | append line-end spacer spans for visually long lines |
|
|
162
|
+
| `setLineEndSpan` | alias of `lineEndSpanThreshold` |
|
|
163
|
+
| `lineEndSpanClass` | CSS class for line-end spacer spans |
|
|
164
|
+
| `setPreWrapStyle` | inject inline pre-wrap style for wrap-enabled blocks |
|
|
165
|
+
| `sampLang` | comma-separated languages rendered as `<samp>`; default: `shell,console` |
|
|
166
|
+
| `{samp}` / `{code}` | per-fence tag override attrs |
|
|
167
|
+
| `langPrefix` | language class prefix; defaults to `md.options.langPrefix || 'language-'` |
|
|
168
|
+
| `useHighlightPre` | preserve highlighter-owned `<pre><code>` wrappers; disables line-splitting features |
|
|
169
|
+
| `onFenceDecision` | debug hook for per-fence branch decisions |
|
|
170
|
+
| `onFenceDecisionTiming` | include timing fields in `onFenceDecision` payloads |
|
|
171
|
+
|
|
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.
|
|
173
|
+
|
|
174
|
+
### Custom Highlight API options
|
|
175
|
+
|
|
176
|
+
| Option | Summary |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `highlightRenderer` | dispatcher mode: `markup`, `api`, or `custom-highlight-api` |
|
|
179
|
+
| `customHighlight.provider` | `shiki`, `hljs`, or `custom` |
|
|
180
|
+
| `customHighlight.highlighter` | Shiki highlighter with synchronous `codeToTokens` |
|
|
181
|
+
| `customHighlight.hljsHighlight` | highlight.js-style function for the `hljs` provider |
|
|
182
|
+
| `customHighlight.getRanges` | synchronous custom range provider |
|
|
183
|
+
| `customHighlight.theme` | Shiki theme name or `{ light, dark, default? }` |
|
|
184
|
+
| `customHighlight.shikiScopeMode` | `auto`, `color`, `semantic`, or `role` |
|
|
185
|
+
| `customHighlight.includeScopeStyles` | include payload `scopeStyles` when available |
|
|
186
|
+
| `customHighlight.transport` | `env` or `inline-script` |
|
|
187
|
+
| `customHighlight.fallback` | provider-error fallback: `plain` or `markup` |
|
|
188
|
+
| `customHighlight.lineFeatureStrategy` | `hybrid` or `disable` |
|
|
189
|
+
|
|
190
|
+
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).
|
|
191
|
+
|
|
192
|
+
## Custom Highlight API Mode
|
|
193
|
+
|
|
194
|
+
API mode renders escaped code text and emits payload ranges. It does **not** activate browser highlights by itself; call the runtime in the browser.
|
|
371
195
|
|
|
372
196
|
```js
|
|
373
197
|
import MarkdownIt from 'markdown-it'
|
|
@@ -377,211 +201,73 @@ import rendererFenceApi, {
|
|
|
377
201
|
applyCustomHighlights,
|
|
378
202
|
renderCustomHighlightPayloadScript,
|
|
379
203
|
} from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
|
|
380
|
-
|
|
381
|
-
|
|
204
|
+
import {
|
|
205
|
+
createRfBasicShikiRoleCustomHighlightOptions,
|
|
206
|
+
rfBasicShikiTheme,
|
|
207
|
+
rfBasicThemeName,
|
|
208
|
+
} from '@peaceroad/markdown-it-renderer-fence/theme/rf-basic'
|
|
382
209
|
|
|
383
210
|
const highlighter = await createHighlighter({
|
|
384
|
-
themes: [
|
|
385
|
-
langs: ['javascript', 'typescript', 'json'],
|
|
211
|
+
themes: [rfBasicShikiTheme],
|
|
212
|
+
langs: ['javascript', 'typescript', 'json'],
|
|
386
213
|
})
|
|
387
214
|
|
|
388
215
|
const md = MarkdownIt({ html: true })
|
|
389
216
|
.use(markdownItAttrs)
|
|
390
217
|
.use(rendererFenceApi, {
|
|
391
|
-
|
|
392
|
-
customHighlight: {
|
|
393
|
-
provider: 'shiki',
|
|
218
|
+
customHighlight: createRfBasicShikiRoleCustomHighlightOptions({
|
|
394
219
|
highlighter,
|
|
395
|
-
theme:
|
|
220
|
+
theme: rfBasicThemeName,
|
|
396
221
|
transport: 'env',
|
|
397
|
-
},
|
|
222
|
+
}),
|
|
398
223
|
})
|
|
399
224
|
|
|
400
225
|
const env = {}
|
|
401
|
-
const html = md.render('```js
|
|
402
|
-
const
|
|
403
|
-
```
|
|
226
|
+
const html = md.render('```js
|
|
227
|
+
const x = 1
|
|
228
|
+
```', env)
|
|
229
|
+
const payloadScript = renderCustomHighlightPayloadScript(env)
|
|
404
230
|
|
|
405
|
-
Browser
|
|
406
|
-
|
|
407
|
-
```js
|
|
231
|
+
// Browser side:
|
|
408
232
|
applyCustomHighlights(document)
|
|
409
233
|
```
|
|
410
234
|
|
|
411
|
-
|
|
412
|
-
If OS/browser theme changes after first apply, re-run apply (or use observer watch mode below).
|
|
413
|
-
|
|
414
|
-
If you only need runtime apply on the browser, import runtime-only entry:
|
|
235
|
+
For lazy apply and color-scheme watching:
|
|
415
236
|
|
|
416
237
|
```js
|
|
417
238
|
import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
`langs` note:
|
|
421
|
-
|
|
422
|
-
- `createHighlighter({ langs: [...] })` should include the languages you expect to render.
|
|
423
|
-
- Shiki does not auto-detect language from code text.
|
|
424
|
-
- if a target language is not loaded, this plugin falls back to Shiki `text` tokenization for that block (safe, but no syntax color buckets).
|
|
425
|
-
|
|
426
|
-
### Minimal API Example (highlight.js provider)
|
|
427
|
-
|
|
428
|
-
```js
|
|
429
|
-
import MarkdownIt from 'markdown-it'
|
|
430
|
-
import markdownItAttrs from 'markdown-it-attrs'
|
|
431
|
-
import hljs from 'highlight.js'
|
|
432
|
-
import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence/custom-highlight'
|
|
433
|
-
// Dispatcher entry alternative:
|
|
434
|
-
// import rendererFenceApi from '@peaceroad/markdown-it-renderer-fence'
|
|
435
|
-
|
|
436
|
-
const md = MarkdownIt({ html: true })
|
|
437
|
-
.use(markdownItAttrs)
|
|
438
|
-
.use(rendererFenceApi, {
|
|
439
|
-
// highlightRenderer: 'api', // required only with dispatcher entry
|
|
440
|
-
customHighlight: {
|
|
441
|
-
provider: 'hljs',
|
|
442
|
-
hljsHighlight: (code, lang) => {
|
|
443
|
-
if (lang && hljs.getLanguage(lang)) {
|
|
444
|
-
return hljs.highlight(code, { language: lang }).value
|
|
445
|
-
}
|
|
446
|
-
return hljs.highlight(code, { language: 'plaintext' }).value
|
|
447
|
-
},
|
|
448
|
-
transport: 'env',
|
|
449
|
-
},
|
|
450
|
-
})
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### API Providers
|
|
454
|
-
|
|
455
|
-
- `customHighlight.provider: 'shiki'`
|
|
456
|
-
Requires `customHighlight.highlighter.codeToTokens(...)` (synchronous).
|
|
457
|
-
- `customHighlight.provider: 'hljs'`
|
|
458
|
-
Uses `customHighlight.hljsHighlight`, fallback to `customHighlight.highlight`, then `md.options.highlight`.
|
|
459
|
-
- `customHighlight.provider: 'custom'`
|
|
460
|
-
Escape hatch. Requires synchronous `customHighlight.getRanges(...)`.
|
|
461
|
-
|
|
462
|
-
### Shiki Scope Modes
|
|
463
|
-
|
|
464
|
-
- `auto` (default)
|
|
465
|
-
- `color`
|
|
466
|
-
- `semantic`
|
|
467
|
-
- `keyword` (best for stable CSS-managed styling)
|
|
468
|
-
|
|
469
|
-
Migration note (`0.5.0`):
|
|
470
|
-
|
|
471
|
-
- legacy aliases like `json`, `bucket`, `keyword-only` are removed
|
|
472
|
-
- use canonical values: `auto | color | semantic | keyword`
|
|
473
|
-
|
|
474
|
-
Recommended production profile for API mode:
|
|
475
239
|
|
|
476
|
-
```js
|
|
477
|
-
customHighlight: {
|
|
478
|
-
provider: 'shiki',
|
|
479
|
-
shikiScopeMode: 'keyword',
|
|
480
|
-
includeScopeStyles: false
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### API Multi-Theme (Shiki, `v:1` additive payload)
|
|
485
|
-
|
|
486
|
-
When you need runtime light/dark switching with Shiki color styles, pass object form theme:
|
|
487
|
-
|
|
488
|
-
```js
|
|
489
|
-
customHighlight: {
|
|
490
|
-
provider: 'shiki',
|
|
491
|
-
highlighter,
|
|
492
|
-
shikiScopeMode: 'color',
|
|
493
|
-
includeScopeStyles: true,
|
|
494
|
-
theme: {
|
|
495
|
-
light: 'github-light',
|
|
496
|
-
dark: 'github-dark',
|
|
497
|
-
default: 'light',
|
|
498
|
-
},
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
Runtime apply can choose variant:
|
|
503
|
-
|
|
504
|
-
```js
|
|
505
|
-
applyCustomHighlights(document, { colorScheme: 'auto' }) // 'auto' | 'light' | 'dark'
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Auto re-apply on color-scheme change:
|
|
509
|
-
|
|
510
|
-
```js
|
|
511
240
|
observeCustomHighlights(document, {
|
|
512
241
|
applyOptions: { colorScheme: 'auto', incremental: true },
|
|
513
242
|
watchColorScheme: true,
|
|
514
243
|
})
|
|
515
244
|
```
|
|
516
245
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
If you copy rendered HTML+payload JSON into another CMS/page:
|
|
520
|
-
|
|
521
|
-
- API mode works only when runtime JS is also available on that page.
|
|
522
|
-
- payload JSON alone is not enough.
|
|
523
|
-
- if the target CMS cannot run custom JS, use `markup` mode instead.
|
|
524
|
-
- this plugin does not auto-generate/write runtime JS files during markdown render.
|
|
246
|
+
Use markup mode instead when the target page cannot run runtime JavaScript.
|
|
525
247
|
|
|
526
|
-
|
|
248
|
+
## Theme
|
|
527
249
|
|
|
528
|
-
|
|
529
|
-
Bundle `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and run `observeCustomHighlights(...)`.
|
|
530
|
-
2. Static page with module scripts
|
|
531
|
-
Load runtime entry via your ESM delivery path and call `applyCustomHighlights(...)`.
|
|
532
|
-
3. HTML-only CMS (no JS injection)
|
|
533
|
-
Use `markup` mode (API mode is not suitable).
|
|
250
|
+
The optional preset theme is a package artifact, not an automatically loaded dependency.
|
|
534
251
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
252
|
+
```css
|
|
253
|
+
@import "@peaceroad/markdown-it-renderer-fence/theme/rf-basic.css";
|
|
254
|
+
```
|
|
538
255
|
|
|
539
|
-
|
|
540
|
-
Includes `<pre data-pre-highlight="...">...</pre>` and payload JSON (`env` script or inline-script transport).
|
|
541
|
-
2. Runtime artifact
|
|
542
|
-
Browser JS that imports `@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime` and runs `applyCustomHighlights(...)` or `observeCustomHighlights(...)`.
|
|
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.
|
|
543
257
|
|
|
544
|
-
|
|
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.
|
|
545
259
|
|
|
546
|
-
|
|
547
|
-
import { observeCustomHighlights } from '@peaceroad/markdown-it-renderer-fence/custom-highlight-runtime'
|
|
260
|
+
For one-color print/monochrome output, use markup mode and import the opt-in monochrome override:
|
|
548
261
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
watchColorScheme: true,
|
|
552
|
-
})
|
|
262
|
+
```css
|
|
263
|
+
@import "@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome.css";
|
|
553
264
|
```
|
|
554
265
|
|
|
555
|
-
|
|
266
|
+
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).
|
|
556
267
|
|
|
557
|
-
|
|
558
|
-
<script type="module" src="/assets/custom-highlight-runtime.js"></script>
|
|
559
|
-
```
|
|
268
|
+
## Helper Exports
|
|
560
269
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
- `highlightRenderer` (`'markup' | 'api' | 'custom-highlight-api'`, default: `'markup'`): dispatcher mode selector (`/custom-highlight` entry does not need this).
|
|
564
|
-
- `customHighlight.provider` (default: `'shiki'`): range source (`'shiki' | 'hljs' | 'custom'`).
|
|
565
|
-
- `customHighlight.getRanges`: required when `provider: 'custom'`; must return synchronous ranges.
|
|
566
|
-
- `customHighlight.highlighter`: Shiki highlighter object with synchronous `codeToTokens`.
|
|
567
|
-
- `customHighlight.hljsHighlight`: highlight.js-style function used when `provider: 'hljs'`.
|
|
568
|
-
- `customHighlight.highlight`: fallback highlight function for `hljs` provider.
|
|
569
|
-
- `customHighlight.defaultLang`: default language when fence language is empty.
|
|
570
|
-
- `customHighlight.theme`: Shiki theme name string, or object `{ light, dark, default? }` for dual-theme payload.
|
|
571
|
-
- `customHighlight.shikiScopeMode` (default: `'auto'`): scope naming mode (`auto | color | semantic | keyword`).
|
|
572
|
-
- `customHighlight.shikiKeywordClassifier`: custom classifier hook for keyword mode.
|
|
573
|
-
- `customHighlight.shikiKeywordLangResolver`: custom language resolver hook for keyword mode.
|
|
574
|
-
- `customHighlight.shikiKeywordLangAliases`: language alias map for keyword resolver.
|
|
575
|
-
- `customHighlight.includeScopeStyles` (default: `true`): include payload `scopeStyles` when available.
|
|
576
|
-
- `customHighlight.fallback` (`'plain' | 'markup'`, default: `'plain'`): server-side fallback renderer on provider errors.
|
|
577
|
-
- `customHighlight.fallbackOn`: restrict fallback trigger reasons.
|
|
578
|
-
- `customHighlight.transport` (`'env' | 'inline-script'`, default: `'env'`): payload transport target.
|
|
579
|
-
- `customHighlight.idPrefix` (default: `'hl-'`): generated per-block payload id prefix.
|
|
580
|
-
- `customHighlight.scopePrefix` (default: `'hl'`): scope name prefix used for highlight registration.
|
|
581
|
-
- `customHighlight.lineFeatureStrategy` (`'hybrid' | 'disable'`, default: `'hybrid'`): keep or disable line-span features in API rendering.
|
|
582
|
-
- In `NODE_ENV=development`, invalid known enum-like values and unknown `customHighlight` keys emit warn-once diagnostics while keeping normalized fallback behavior.
|
|
583
|
-
|
|
584
|
-
### API Helper Exports
|
|
270
|
+
From `@peaceroad/markdown-it-renderer-fence/custom-highlight`:
|
|
585
271
|
|
|
586
272
|
- `applyCustomHighlights`
|
|
587
273
|
- `observeCustomHighlights`
|
|
@@ -593,29 +279,52 @@ Then include that bridge in generated HTML:
|
|
|
593
279
|
- `customHighlightPayloadSchemaVersion`
|
|
594
280
|
- `customHighlightPayloadSupportedVersions`
|
|
595
281
|
|
|
596
|
-
|
|
282
|
+
From `@peaceroad/markdown-it-renderer-fence/theme/rf-basic`:
|
|
283
|
+
|
|
284
|
+
- `rfBasicShikiTheme`
|
|
285
|
+
- `rfBasicThemeName`
|
|
286
|
+
- `createRfBasicShikiTheme`
|
|
287
|
+
- `createRfBasicShikiCustomHighlightOptions`
|
|
288
|
+
- `createRfBasicShikiRoleCustomHighlightOptions`
|
|
289
|
+
- `createRfBasicHighlightjsCustomHighlightOptions`
|
|
290
|
+
- `rfBasicSyntaxCssVars`
|
|
291
|
+
- `createRfBasicShikiRoleScopeColorVars`
|
|
597
292
|
|
|
598
|
-
-
|
|
599
|
-
|
|
600
|
-
-
|
|
293
|
+
From `@peaceroad/markdown-it-renderer-fence/theme/rf-monochrome`:
|
|
294
|
+
|
|
295
|
+
- `rfMonochromeShikiTheme`
|
|
296
|
+
- `rfMonochromeShikiThemeName`
|
|
297
|
+
- `rfMonochromeThemeName`
|
|
298
|
+
- `createRfMonochromeShikiTheme`
|
|
601
299
|
|
|
602
300
|
## Docs and Examples
|
|
603
301
|
|
|
604
|
-
-
|
|
605
|
-
-
|
|
606
|
-
-
|
|
607
|
-
-
|
|
608
|
-
-
|
|
302
|
+
- [Docs overview](docs/README.md)
|
|
303
|
+
- [Code Highlighting Design](docs/code-highlighting-design.md)
|
|
304
|
+
- [Custom Highlight Styling Guide](docs/custom-highlight-styling-guide.md)
|
|
305
|
+
- [rf-basic Theme README](theme/rf-basic/README.md)
|
|
306
|
+
- [rf-monochrome Theme README](theme/rf-monochrome/README.md)
|
|
307
|
+
- [Example README](example/README.md)
|
|
308
|
+
- `example/custom-highlight-provider-matrix.html`
|
|
309
|
+
- `example/monochrome-highlight-compare.html`
|
|
310
|
+
- `example/line-number-sample.html`
|
|
311
|
+
- `example/line-notes-sample.html`
|
|
312
|
+
- `example/samp-sample.html`
|
|
313
|
+
|
|
314
|
+
Examples are demonstration and verification assets. Do not import runtime or theme helpers from `example/`; use the public package entry points above.
|
|
609
315
|
|
|
610
316
|
## Tests and Benchmarks
|
|
611
317
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
318
|
+
```bash
|
|
319
|
+
npm test
|
|
320
|
+
npm run test:provider:contract
|
|
321
|
+
npm run test:provider:role
|
|
322
|
+
npm run test:provider:role:holdout
|
|
323
|
+
npm run test:provider:matrix
|
|
324
|
+
npm run test:performance
|
|
325
|
+
npm run test:performance:runtime
|
|
326
|
+
```
|
|
618
327
|
|
|
619
328
|
## License
|
|
620
329
|
|
|
621
|
-
|
|
330
|
+
MIT (`LICENSE`)
|