@jarenjs/md 0.34.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 +520 -0
- package/dist/types/ast.d.ts +181 -0
- package/dist/types/bake.d.ts +61 -0
- package/dist/types/compiler.d.ts +141 -0
- package/dist/types/component/index.d.ts +101 -0
- package/dist/types/directives.d.ts +126 -0
- package/dist/types/entities.d.ts +40 -0
- package/dist/types/footnotes.d.ts +83 -0
- package/dist/types/frontmatter.d.ts +67 -0
- package/dist/types/html.d.ts +72 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/loader.d.ts +84 -0
- package/dist/types/mdx.d.ts +45 -0
- package/dist/types/parser.d.ts +116 -0
- package/dist/types/plugins/highlight.d.ts +64 -0
- package/dist/types/plugins/index.d.ts +64 -0
- package/dist/types/plugins/mermaid.d.ts +12 -0
- package/dist/types/scanner.d.ts +240 -0
- package/dist/types/to-html.d.ts +104 -0
- package/dist/types/to-md.d.ts +23 -0
- package/dist/types/to-vnode.d.ts +161 -0
- package/dist/types/utils.d.ts +63 -0
- package/docs/LOADER.md +92 -0
- package/docs/MD-FORMAT.md +502 -0
- package/docs/PLUGINS.md +277 -0
- package/package.json +80 -0
- package/schemas/jaren-md-ast.schema.json +296 -0
- package/src/ast.js +346 -0
- package/src/bake.js +104 -0
- package/src/compiler.js +167 -0
- package/src/component/index.js +191 -0
- package/src/directives.js +371 -0
- package/src/entities.js +107 -0
- package/src/footnotes.js +180 -0
- package/src/frontmatter.js +947 -0
- package/src/html.js +281 -0
- package/src/index.js +76 -0
- package/src/loader.js +0 -0
- package/src/mdx.js +219 -0
- package/src/parser.js +1685 -0
- package/src/plugins/highlight.js +325 -0
- package/src/plugins/index.js +75 -0
- package/src/plugins/mermaid.js +14 -0
- package/src/scanner.js +832 -0
- package/src/to-html.js +425 -0
- package/src/to-md.js +396 -0
- package/src/to-vnode.js +766 -0
- package/src/utils.js +107 -0
- package/styles/md.css +238 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Syntax highlighting for fenced code blocks (PLUGINS.md §6.2).
|
|
4
|
+
*
|
|
5
|
+
* Built-in mode is a zero-dependency single-pass tokenizer driven by
|
|
6
|
+
* compact grammar tables — a keyword set, comment/string delimiters
|
|
7
|
+
* and punctuation classes — compiled to closures once at module load.
|
|
8
|
+
* No regex runs in the token loop. Adapter mode puts shiki/prism/
|
|
9
|
+
* highlight.js behind the same `{ kind, value }` token contract.
|
|
10
|
+
*
|
|
11
|
+
* Token kinds: kw str num com pun id op lit — rendered as
|
|
12
|
+
* `span.tok-{kind}` (plain `id` runs render as bare text).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { definePlugin } from './index.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{ kind: 'kw'|'str'|'num'|'com'|'pun'|'id'|'op'|'lit', value: string }} Token
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A compact grammar table.
|
|
22
|
+
* @typedef {object} MdGrammar
|
|
23
|
+
* @property {string[]} [keywords]
|
|
24
|
+
* @property {string[]} [literals]
|
|
25
|
+
* @property {string[]} [lineComments] comment-to-end-of-line prefixes
|
|
26
|
+
* @property {[string, string]} [blockComment] open/close pair
|
|
27
|
+
* @property {string} [strings] string delimiter characters
|
|
28
|
+
* @property {boolean} [numbers] recognize number literals
|
|
29
|
+
* @property {string} [extraId] extra identifier characters (e.g. `-` for CSS)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const JS_KEYWORDS = [
|
|
33
|
+
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
34
|
+
'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends',
|
|
35
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let',
|
|
36
|
+
'new', 'of', 'return', 'static', 'super', 'switch', 'this', 'throw',
|
|
37
|
+
'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'get', 'set',
|
|
38
|
+
// the TS layer — harmless for plain JS
|
|
39
|
+
'abstract', 'any', 'as', 'asserts', 'declare', 'enum', 'implements',
|
|
40
|
+
'infer', 'interface', 'is', 'keyof', 'namespace', 'never', 'private',
|
|
41
|
+
'protected', 'public', 'readonly', 'satisfies', 'type', 'unknown',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const BASH_KEYWORDS = [
|
|
45
|
+
'if', 'then', 'elif', 'else', 'fi', 'for', 'while', 'until', 'do',
|
|
46
|
+
'done', 'case', 'esac', 'in', 'function', 'select', 'time', 'return',
|
|
47
|
+
'break', 'continue', 'local', 'export', 'readonly', 'declare', 'unset',
|
|
48
|
+
'shift', 'source', 'alias', 'echo', 'exit', 'set', 'cd', 'test',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** @type {Record<string, MdGrammar>} */
|
|
52
|
+
const GRAMMARS = {
|
|
53
|
+
js: {
|
|
54
|
+
keywords: JS_KEYWORDS,
|
|
55
|
+
literals: ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'],
|
|
56
|
+
lineComments: ['//'],
|
|
57
|
+
blockComment: ['/*', '*/'],
|
|
58
|
+
strings: '\'"`',
|
|
59
|
+
numbers: true,
|
|
60
|
+
extraId: '$',
|
|
61
|
+
},
|
|
62
|
+
json: {
|
|
63
|
+
literals: ['true', 'false', 'null'],
|
|
64
|
+
strings: '"',
|
|
65
|
+
numbers: true,
|
|
66
|
+
},
|
|
67
|
+
toml: {
|
|
68
|
+
literals: ['true', 'false', 'null', 'inf', 'nan'],
|
|
69
|
+
lineComments: ['#'],
|
|
70
|
+
strings: '\'"',
|
|
71
|
+
numbers: true,
|
|
72
|
+
extraId: '-',
|
|
73
|
+
},
|
|
74
|
+
html: {
|
|
75
|
+
keywords: ['html', 'head', 'body', 'div', 'span', 'script', 'style',
|
|
76
|
+
'a', 'p', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th', 'input',
|
|
77
|
+
'button', 'form', 'img', 'pre', 'code', 'h1', 'h2', 'h3', 'main',
|
|
78
|
+
'section', 'article', 'header', 'footer', 'nav', 'template'],
|
|
79
|
+
blockComment: ['<!--', '-->'],
|
|
80
|
+
strings: '\'"',
|
|
81
|
+
extraId: '-',
|
|
82
|
+
},
|
|
83
|
+
css: {
|
|
84
|
+
keywords: ['import', 'media', 'supports', 'keyframes', 'font-face',
|
|
85
|
+
'root', 'hover', 'focus', 'active', 'before', 'after', 'not',
|
|
86
|
+
'important'],
|
|
87
|
+
lineComments: [],
|
|
88
|
+
blockComment: ['/*', '*/'],
|
|
89
|
+
strings: '\'"',
|
|
90
|
+
numbers: true,
|
|
91
|
+
extraId: '-#',
|
|
92
|
+
},
|
|
93
|
+
md: {
|
|
94
|
+
lineComments: [],
|
|
95
|
+
strings: '`',
|
|
96
|
+
extraId: '',
|
|
97
|
+
},
|
|
98
|
+
bash: {
|
|
99
|
+
keywords: BASH_KEYWORDS,
|
|
100
|
+
literals: ['true', 'false'],
|
|
101
|
+
lineComments: ['#'],
|
|
102
|
+
strings: '\'"`',
|
|
103
|
+
numbers: true,
|
|
104
|
+
extraId: '-$',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
GRAMMARS.ts = GRAMMARS.js;
|
|
108
|
+
GRAMMARS.jsx = GRAMMARS.js;
|
|
109
|
+
GRAMMARS.tsx = GRAMMARS.js;
|
|
110
|
+
GRAMMARS.mjs = GRAMMARS.js;
|
|
111
|
+
GRAMMARS.cjs = GRAMMARS.js;
|
|
112
|
+
GRAMMARS.javascript = GRAMMARS.js;
|
|
113
|
+
GRAMMARS.typescript = GRAMMARS.js;
|
|
114
|
+
GRAMMARS.josl = GRAMMARS.toml;
|
|
115
|
+
GRAMMARS.ini = GRAMMARS.toml;
|
|
116
|
+
GRAMMARS.yaml = GRAMMARS.toml;
|
|
117
|
+
GRAMMARS.yml = GRAMMARS.toml;
|
|
118
|
+
GRAMMARS.xml = GRAMMARS.html;
|
|
119
|
+
GRAMMARS.markdown = GRAMMARS.md;
|
|
120
|
+
GRAMMARS.sh = GRAMMARS.bash;
|
|
121
|
+
GRAMMARS.shell = GRAMMARS.bash;
|
|
122
|
+
GRAMMARS.console = GRAMMARS.bash;
|
|
123
|
+
|
|
124
|
+
/** The canonical names of the shipped grammars (aliases resolve too). */
|
|
125
|
+
export const GRAMMAR_NAMES = Object.freeze(Object.keys(GRAMMARS));
|
|
126
|
+
|
|
127
|
+
const OP_CHARS = '+-*/%=<>!&|^~?:@';
|
|
128
|
+
const PUN_CHARS = '()[]{},;.';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compile a grammar table into a single-pass tokenizer closure.
|
|
132
|
+
* @param {MdGrammar} grammar
|
|
133
|
+
* @returns {(code: string) => Token[]}
|
|
134
|
+
*/
|
|
135
|
+
function compileGrammar(grammar) {
|
|
136
|
+
const keywords = new Set(grammar.keywords ?? []);
|
|
137
|
+
const literals = new Set(grammar.literals ?? []);
|
|
138
|
+
const lineComments = grammar.lineComments ?? [];
|
|
139
|
+
const blockOpen = grammar.blockComment !== undefined ? grammar.blockComment[0] : null;
|
|
140
|
+
const blockClose = grammar.blockComment !== undefined ? grammar.blockComment[1] : '';
|
|
141
|
+
const strings = grammar.strings ?? '';
|
|
142
|
+
const numbers = grammar.numbers === true;
|
|
143
|
+
// Per-char class lookup for the ASCII range, built once.
|
|
144
|
+
const CLASS = new Uint8Array(128); // 1 id, 2 op, 3 pun, 4 string, 5 digit
|
|
145
|
+
for (let c = 48; c <= 57; c++) CLASS[c] = 5;
|
|
146
|
+
for (let c = 65; c <= 90; c++) CLASS[c] = 1;
|
|
147
|
+
for (let c = 97; c <= 122; c++) CLASS[c] = 1;
|
|
148
|
+
CLASS[95] = 1; // _
|
|
149
|
+
for (const ch of OP_CHARS) CLASS[ch.charCodeAt(0)] = 2;
|
|
150
|
+
for (const ch of PUN_CHARS) CLASS[ch.charCodeAt(0)] = 3;
|
|
151
|
+
for (const ch of strings) CLASS[ch.charCodeAt(0)] = 4;
|
|
152
|
+
for (const ch of grammar.extraId ?? '') CLASS[ch.charCodeAt(0)] = 1;
|
|
153
|
+
|
|
154
|
+
return function tokenize(code) {
|
|
155
|
+
/** @type {Token[]} */
|
|
156
|
+
const tokens = [];
|
|
157
|
+
let i = 0;
|
|
158
|
+
let plain = 0; // start of the pending plain run
|
|
159
|
+
|
|
160
|
+
/** @param {number} end @param {Token['kind']} kind @param {number} to */
|
|
161
|
+
const push = (end, kind, to) => {
|
|
162
|
+
if (end > plain) tokens.push({ kind: 'id', value: code.slice(plain, end) });
|
|
163
|
+
if (to > end) tokens.push({ kind, value: code.slice(end, to) });
|
|
164
|
+
plain = to;
|
|
165
|
+
i = to;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
outer:
|
|
169
|
+
while (i < code.length) {
|
|
170
|
+
const c = code.charCodeAt(i);
|
|
171
|
+
|
|
172
|
+
// Comments (prefix comparison, no regex).
|
|
173
|
+
for (let k = 0; k < lineComments.length; k++) {
|
|
174
|
+
if (c === lineComments[k].charCodeAt(0) && code.startsWith(lineComments[k], i)) {
|
|
175
|
+
let end = code.indexOf('\n', i);
|
|
176
|
+
if (end === -1) end = code.length;
|
|
177
|
+
push(i, 'com', end);
|
|
178
|
+
continue outer;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (blockOpen !== null && c === blockOpen.charCodeAt(0) && code.startsWith(blockOpen, i)) {
|
|
182
|
+
let end = code.indexOf(blockClose, i + blockOpen.length);
|
|
183
|
+
end = end === -1 ? code.length : end + blockClose.length;
|
|
184
|
+
push(i, 'com', end);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const cls = c < 128 ? CLASS[c] : 1;
|
|
189
|
+
|
|
190
|
+
if (cls === 4) {
|
|
191
|
+
// String literal with backslash escapes, to line end at most
|
|
192
|
+
// (unterminated strings stay honest).
|
|
193
|
+
let j = i + 1;
|
|
194
|
+
while (j < code.length) {
|
|
195
|
+
const s = code.charCodeAt(j);
|
|
196
|
+
if (s === 0x5C) { j += 2; continue; }
|
|
197
|
+
if (s === c) { j++; break; }
|
|
198
|
+
if (s === 0x0A && c !== 0x60) break;
|
|
199
|
+
j++;
|
|
200
|
+
}
|
|
201
|
+
push(i, 'str', j);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (numbers && (cls === 5
|
|
206
|
+
|| (c === 0x2E && CLASS[code.charCodeAt(i + 1)] === 5))) {
|
|
207
|
+
let j = i;
|
|
208
|
+
while (j < code.length) {
|
|
209
|
+
const s = code.charCodeAt(j);
|
|
210
|
+
const sc = s < 128 ? CLASS[s] : 0;
|
|
211
|
+
if (sc === 5 || sc === 1 || s === 0x2E || s === 0x5F
|
|
212
|
+
|| ((s === 0x2B || s === 0x2D) && (code.charCodeAt(j - 1) | 32) === 0x65)) {
|
|
213
|
+
j++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
push(i, 'num', j);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (cls === 1) {
|
|
223
|
+
let j = i + 1;
|
|
224
|
+
while (j < code.length) {
|
|
225
|
+
const s = code.charCodeAt(j);
|
|
226
|
+
const sc = s < 128 ? CLASS[s] : 1;
|
|
227
|
+
if (sc === 1 || sc === 5) { j++; continue; }
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const word = code.slice(i, j);
|
|
231
|
+
if (keywords.has(word)) push(i, 'kw', j);
|
|
232
|
+
else if (literals.has(word)) push(i, 'lit', j);
|
|
233
|
+
else i = j; // stays in the plain run
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (cls === 2) { push(i, 'op', i + 1); continue; }
|
|
238
|
+
if (cls === 3) { push(i, 'pun', i + 1); continue; }
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
if (code.length > plain) tokens.push({ kind: 'id', value: code.slice(plain) });
|
|
242
|
+
return tokens;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Compiled tokenizers, one per distinct grammar table. */
|
|
247
|
+
const COMPILED = new Map();
|
|
248
|
+
for (const name of Object.keys(GRAMMARS)) {
|
|
249
|
+
const grammar = GRAMMARS[name];
|
|
250
|
+
if (!COMPILED.has(grammar)) COMPILED.set(grammar, compileGrammar(grammar));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Tokenize `code` with the built-in grammar for `lang`. Returns null
|
|
255
|
+
* when no grammar covers the language.
|
|
256
|
+
* @param {string} code
|
|
257
|
+
* @param {string|null} lang
|
|
258
|
+
* @returns {Token[] | null}
|
|
259
|
+
*/
|
|
260
|
+
export function tokenizeCode(code, lang) {
|
|
261
|
+
if (lang === null) return null;
|
|
262
|
+
const grammar = GRAMMARS[lang];
|
|
263
|
+
if (grammar === undefined) return null;
|
|
264
|
+
return COMPILED.get(grammar)(code);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The syntax-highlighting plugin: takes over rendering of `code`
|
|
269
|
+
* nodes. `adapter(code, lang)` may return tokens (shiki/prism/hljs
|
|
270
|
+
* behind the shared contract) or null to fall back to the built-in
|
|
271
|
+
* grammars, then to plain text.
|
|
272
|
+
*
|
|
273
|
+
* @param {{ grammars?: Record<string, MdGrammar>,
|
|
274
|
+
* adapter?: (code: string, lang: string|null) => (Token[] | null) }} [config]
|
|
275
|
+
* @returns {import('./index.js').MdPlugin}
|
|
276
|
+
*/
|
|
277
|
+
export function highlightPlugin(config = {}) {
|
|
278
|
+
const adapter = config.adapter;
|
|
279
|
+
/** @type {Map<string, (code: string) => Token[]>} */
|
|
280
|
+
const extra = new Map();
|
|
281
|
+
if (config.grammars !== undefined) {
|
|
282
|
+
for (const name of Object.keys(config.grammars)) {
|
|
283
|
+
extra.set(name, compileGrammar(config.grammars[name]));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {string} code
|
|
289
|
+
* @param {string|null} lang
|
|
290
|
+
* @returns {Token[] | null}
|
|
291
|
+
*/
|
|
292
|
+
const tokensFor = (code, lang) => {
|
|
293
|
+
if (adapter !== undefined) {
|
|
294
|
+
const tokens = adapter(code, lang);
|
|
295
|
+
if (tokens !== null && tokens !== undefined) return tokens;
|
|
296
|
+
}
|
|
297
|
+
if (lang !== null) {
|
|
298
|
+
const custom = extra.get(lang);
|
|
299
|
+
if (custom !== undefined) return custom(code);
|
|
300
|
+
}
|
|
301
|
+
return tokenizeCode(code, lang);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return definePlugin({
|
|
305
|
+
name: 'highlight',
|
|
306
|
+
node: 'code',
|
|
307
|
+
render: (node, h) => {
|
|
308
|
+
const lang = node.lang ?? null;
|
|
309
|
+
const props = lang === null ? {} : { class: 'language-' + lang };
|
|
310
|
+
const tokens = tokensFor(node.value, lang);
|
|
311
|
+
if (tokens === null) {
|
|
312
|
+
return h('pre', {}, h('code', props, node.value));
|
|
313
|
+
}
|
|
314
|
+
/** @type {any[]} */
|
|
315
|
+
const children = [];
|
|
316
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
317
|
+
const token = tokens[i];
|
|
318
|
+
children.push(token.kind === 'id'
|
|
319
|
+
? token.value
|
|
320
|
+
: h('span', { class: 'tok-' + token.kind }, token.value));
|
|
321
|
+
}
|
|
322
|
+
return h('pre', {}, h('code', props, ...children));
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Plugin registry helpers and the reference plugins.
|
|
4
|
+
*
|
|
5
|
+
* A plugin is data, validated and frozen at definition time; the
|
|
6
|
+
* parser and vnode emitter bake plugin arrays into dispatch tables at
|
|
7
|
+
* compile time (the normative contract lives in docs/PLUGINS.md).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RE_KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} MdPlugin
|
|
14
|
+
* @property {string} name unique kebab-case identifier
|
|
15
|
+
* @property {string[]} [fences] fenced-code claims by info-string first word
|
|
16
|
+
* @property {any[]} [blocks] block rule descriptors ({ chars, start, continue, close })
|
|
17
|
+
* @property {any[]} [inlines] inline rule descriptors ({ char, scan })
|
|
18
|
+
* @property {string} [node] the AST type this plugin emits/renders
|
|
19
|
+
* @property {(node: any, h: any, ctx: any) => any} [render] pure vnode renderer
|
|
20
|
+
* @property {(node: any, ctx: any) => string} [toHtml] pure HTML-string
|
|
21
|
+
* renderer, for `toHtml`. Independent of `render`: a plugin may serve
|
|
22
|
+
* one emitter, the other, or both (docs/PLUGINS.md §5.1)
|
|
23
|
+
* @property {(el: any, node: any, ctx: any) => any} [hydrate] browser-only upgrade
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate and freeze a plugin spec (docs/PLUGINS.md §1).
|
|
28
|
+
* @param {MdPlugin} spec
|
|
29
|
+
* @returns {MdPlugin}
|
|
30
|
+
*/
|
|
31
|
+
export function definePlugin(spec) {
|
|
32
|
+
if (spec === null || typeof spec !== 'object') {
|
|
33
|
+
throw new TypeError('md plugin: spec must be an object');
|
|
34
|
+
}
|
|
35
|
+
if (typeof spec.name !== 'string' || !RE_KEBAB.test(spec.name)) {
|
|
36
|
+
throw new TypeError(`md plugin: name must be kebab-case, got '${spec.name}'`);
|
|
37
|
+
}
|
|
38
|
+
if (spec.fences !== undefined) {
|
|
39
|
+
if (!Array.isArray(spec.fences) || spec.fences.some((f) => typeof f !== 'string')) {
|
|
40
|
+
throw new TypeError(`md plugin '${spec.name}': fences must be an array of strings`);
|
|
41
|
+
}
|
|
42
|
+
Object.freeze(spec.fences);
|
|
43
|
+
}
|
|
44
|
+
if (spec.blocks !== undefined) {
|
|
45
|
+
for (const rule of spec.blocks) {
|
|
46
|
+
if (typeof rule.chars !== 'string' || rule.chars.length === 0
|
|
47
|
+
|| typeof rule.start !== 'function' || typeof rule.continue !== 'function') {
|
|
48
|
+
throw new TypeError(`md plugin '${spec.name}': block rules need chars, start and continue`);
|
|
49
|
+
}
|
|
50
|
+
Object.freeze(rule);
|
|
51
|
+
}
|
|
52
|
+
Object.freeze(spec.blocks);
|
|
53
|
+
}
|
|
54
|
+
if (spec.inlines !== undefined) {
|
|
55
|
+
for (const rule of spec.inlines) {
|
|
56
|
+
if (typeof rule.char !== 'string' || rule.char.length !== 1
|
|
57
|
+
|| typeof rule.scan !== 'function') {
|
|
58
|
+
throw new TypeError(`md plugin '${spec.name}': inline rules need a single char and scan`);
|
|
59
|
+
}
|
|
60
|
+
Object.freeze(rule);
|
|
61
|
+
}
|
|
62
|
+
Object.freeze(spec.inlines);
|
|
63
|
+
}
|
|
64
|
+
if ((spec.fences !== undefined || spec.node !== undefined)
|
|
65
|
+
&& spec.render !== undefined && typeof spec.render !== 'function') {
|
|
66
|
+
throw new TypeError(`md plugin '${spec.name}': render must be a function`);
|
|
67
|
+
}
|
|
68
|
+
if (spec.toHtml !== undefined && typeof spec.toHtml !== 'function') {
|
|
69
|
+
throw new TypeError(`md plugin '${spec.name}': toHtml must be a function`);
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze(spec);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { highlightPlugin, tokenizeCode, GRAMMAR_NAMES } from './highlight.js';
|
|
75
|
+
export { mermaidPlugin } from './mermaid.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Mermaid diagram support (PLUGINS.md §6.1).
|
|
4
|
+
*
|
|
5
|
+
* The **native** plugin from `@jarenjs/mermaid`: a self-frozen
|
|
6
|
+
* `MdPlugin`-shaped object whose `render` parses the fence source and
|
|
7
|
+
* emits pure-vnode SVG synchronously (SSR-safe, no injected `mermaid`
|
|
8
|
+
* instance, no `innerHTML`). The dependency arrow points md → mermaid
|
|
9
|
+
*, and `@jarenjs/mermaid/plugin` does not import
|
|
10
|
+
* `definePlugin`, so there is no cycle. Consumers who never use it
|
|
11
|
+
* tree-shake it away (`sideEffects:false`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { mermaidPlugin, refreshMermaidFence } from '@jarenjs/mermaid/plugin';
|