@void/md 0.0.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 +152 -0
- package/dist/compile-BRZW8ppH.mjs +333 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +2 -0
- package/dist/plugin-BpyOmjmC.d.mts +56 -0
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.mjs +703 -0
- package/dist/runtime/index.d.mts +6 -0
- package/dist/runtime/index.mjs +8 -0
- package/dist/theme/code.css +188 -0
- package/dist/theme/containers.css +125 -0
- package/dist/theme/content.css +4 -0
- package/dist/theme/index.css +7 -0
- package/dist/theme/prose.css +294 -0
- package/dist/theme/reset.css +107 -0
- package/package.json +60 -0
- package/pages.d.ts +15 -0
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "pathe";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
5
|
+
import { normalizePath } from "vite";
|
|
6
|
+
import matter from "gray-matter";
|
|
7
|
+
import MarkdownExit from "markdown-exit";
|
|
8
|
+
import anchor from "markdown-it-anchor";
|
|
9
|
+
//#region src/codegen/md-types.ts
|
|
10
|
+
/**
|
|
11
|
+
* Generate .void/md.d.ts content.
|
|
12
|
+
*
|
|
13
|
+
* Declares the `@void/md/pages` virtual module so TypeScript can resolve it
|
|
14
|
+
* from anywhere in the project (pages/, routes/, etc.) without @ts-ignore.
|
|
15
|
+
*/
|
|
16
|
+
function generateMdTypes() {
|
|
17
|
+
return [
|
|
18
|
+
"// Auto-generated by @void/md — do not edit",
|
|
19
|
+
"",
|
|
20
|
+
"export interface VoidMdPage {",
|
|
21
|
+
" path: string;",
|
|
22
|
+
" title: string;",
|
|
23
|
+
" frontmatter: Record<string, unknown>;",
|
|
24
|
+
" headings: Array<{ depth: number; slug: string; text: string }>;",
|
|
25
|
+
"}",
|
|
26
|
+
"",
|
|
27
|
+
"declare const pages: Array<VoidMdPage>;",
|
|
28
|
+
"export default pages;",
|
|
29
|
+
""
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/emit-shared.ts
|
|
34
|
+
const ISLAND_TAG_RE$1 = /<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)\/\s*>|<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\3\s*>/g;
|
|
35
|
+
function findIslandTags$1(html, names) {
|
|
36
|
+
const matches = [];
|
|
37
|
+
let match;
|
|
38
|
+
ISLAND_TAG_RE$1.lastIndex = 0;
|
|
39
|
+
while ((match = ISLAND_TAG_RE$1.exec(html)) !== null) {
|
|
40
|
+
const name = match[1] ?? match[3];
|
|
41
|
+
if (names.has(name)) matches.push({
|
|
42
|
+
name,
|
|
43
|
+
start: match.index,
|
|
44
|
+
end: match.index + match[0].length,
|
|
45
|
+
attrs: (match[2] ?? match[4] ?? "").trim()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return matches;
|
|
49
|
+
}
|
|
50
|
+
function parseAttrs$1(attrStr) {
|
|
51
|
+
const result = {};
|
|
52
|
+
const attrRe = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = attrRe.exec(attrStr)) !== null) result[match[1]] = match[2] ?? match[3] ?? match[4] ?? "";
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function toIslandId(imp) {
|
|
58
|
+
return imp.islandId ?? imp.specifier.replace(/\.[^.]+$/, "");
|
|
59
|
+
}
|
|
60
|
+
function buildRenderChunks(html, islandImports) {
|
|
61
|
+
const islandByName = new Map(islandImports.map((item) => [item.name, item]));
|
|
62
|
+
const tags = findIslandTags$1(html, new Set(islandByName.keys()));
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let cursor = 0;
|
|
65
|
+
for (const tag of tags) {
|
|
66
|
+
if (tag.start > cursor) {
|
|
67
|
+
const segment = html.slice(cursor, tag.start);
|
|
68
|
+
if (segment.trim()) chunks.push({
|
|
69
|
+
type: "html",
|
|
70
|
+
html: segment
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const islandImport = islandByName.get(tag.name);
|
|
74
|
+
if (islandImport) chunks.push({
|
|
75
|
+
type: "island",
|
|
76
|
+
islandImport,
|
|
77
|
+
props: parseAttrs$1(tag.attrs)
|
|
78
|
+
});
|
|
79
|
+
cursor = tag.end;
|
|
80
|
+
}
|
|
81
|
+
if (cursor < html.length) {
|
|
82
|
+
const segment = html.slice(cursor);
|
|
83
|
+
if (segment.trim()) chunks.push({
|
|
84
|
+
type: "html",
|
|
85
|
+
html: segment
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return chunks;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/emit-react.ts
|
|
92
|
+
function emitReactComponent(result) {
|
|
93
|
+
return `
|
|
94
|
+
import React from "react";
|
|
95
|
+
import { FrontmatterContext } from "@void/md";
|
|
96
|
+
|
|
97
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
98
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
99
|
+
const __html = ${JSON.stringify(result.html)};
|
|
100
|
+
|
|
101
|
+
export const frontmatter = __frontmatter;
|
|
102
|
+
export const headings = __headings;
|
|
103
|
+
|
|
104
|
+
export default function MarkdownPage() {
|
|
105
|
+
return React.createElement(
|
|
106
|
+
FrontmatterContext.Provider,
|
|
107
|
+
{ value: __frontmatter },
|
|
108
|
+
React.createElement("div", { dangerouslySetInnerHTML: { __html: __html } })
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
function emitReactComponentWithIslands(result, islandImports) {
|
|
114
|
+
const chunks = buildRenderChunks(result.html, islandImports);
|
|
115
|
+
let code = `import React from "react";\n`;
|
|
116
|
+
code += `import { FrontmatterContext } from "@void/md";\n`;
|
|
117
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
118
|
+
const item = islandImports[i];
|
|
119
|
+
code += `import _raw_island_${i} from ${JSON.stringify(item.specifier)};\n`;
|
|
120
|
+
}
|
|
121
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
122
|
+
const item = islandImports[i];
|
|
123
|
+
code += `function ${item.name}(props) {
|
|
124
|
+
return React.createElement(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
"data-island": ${JSON.stringify(toIslandId(item))},
|
|
128
|
+
"data-props": JSON.stringify(props),
|
|
129
|
+
"data-hydrate": ${JSON.stringify(item.strategy)}
|
|
130
|
+
},
|
|
131
|
+
React.createElement(_raw_island_${i}, props)
|
|
132
|
+
);
|
|
133
|
+
}\n`;
|
|
134
|
+
}
|
|
135
|
+
const children = [];
|
|
136
|
+
for (const chunk of chunks) if (chunk.type === "html") children.push(`React.createElement("div", { dangerouslySetInnerHTML: { __html: ${JSON.stringify(chunk.html)} } })`);
|
|
137
|
+
else {
|
|
138
|
+
const propsCode = Object.keys(chunk.props).length > 0 ? JSON.stringify(chunk.props) : "null";
|
|
139
|
+
children.push(`React.createElement(${chunk.islandImport.name}, ${propsCode})`);
|
|
140
|
+
}
|
|
141
|
+
if (children.length === 0) children.push("null");
|
|
142
|
+
code += `
|
|
143
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
144
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
145
|
+
|
|
146
|
+
export const frontmatter = __frontmatter;
|
|
147
|
+
export const headings = __headings;
|
|
148
|
+
|
|
149
|
+
export default function MarkdownPage() {
|
|
150
|
+
return React.createElement(
|
|
151
|
+
FrontmatterContext.Provider,
|
|
152
|
+
{ value: __frontmatter },
|
|
153
|
+
React.createElement(
|
|
154
|
+
React.Fragment,
|
|
155
|
+
null,
|
|
156
|
+
${children.join(",\n ")}
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
return code;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/emit-solid.ts
|
|
165
|
+
function emitSolidComponent(result) {
|
|
166
|
+
return `
|
|
167
|
+
import { FrontmatterContext } from "@void/md";
|
|
168
|
+
|
|
169
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
170
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
171
|
+
const __html = ${JSON.stringify(result.html)};
|
|
172
|
+
|
|
173
|
+
export const frontmatter = __frontmatter;
|
|
174
|
+
export const headings = __headings;
|
|
175
|
+
|
|
176
|
+
export default function MarkdownPage() {
|
|
177
|
+
return (
|
|
178
|
+
<FrontmatterContext.Provider value={__frontmatter}>
|
|
179
|
+
<div innerHTML={__html} />
|
|
180
|
+
</FrontmatterContext.Provider>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
function emitSolidComponentWithIslands(result, islandImports) {
|
|
186
|
+
const chunks = buildRenderChunks(result.html, islandImports);
|
|
187
|
+
const importAliasByName = /* @__PURE__ */ new Map();
|
|
188
|
+
let code = `import { FrontmatterContext } from "@void/md";\n`;
|
|
189
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
190
|
+
const item = islandImports[i];
|
|
191
|
+
const alias = `RawIsland${i}`;
|
|
192
|
+
importAliasByName.set(item.name, alias);
|
|
193
|
+
code += `import ${alias} from ${JSON.stringify(item.specifier)};\n`;
|
|
194
|
+
}
|
|
195
|
+
code += `
|
|
196
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
197
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
198
|
+
|
|
199
|
+
export const frontmatter = __frontmatter;
|
|
200
|
+
export const headings = __headings;
|
|
201
|
+
`;
|
|
202
|
+
const bodyLines = [];
|
|
203
|
+
let index = 0;
|
|
204
|
+
for (const chunk of chunks) {
|
|
205
|
+
if (chunk.type === "html") {
|
|
206
|
+
const htmlName = `__html_chunk_${index++}`;
|
|
207
|
+
code += `\nconst ${htmlName} = ${JSON.stringify(chunk.html)};`;
|
|
208
|
+
bodyLines.push(`<div innerHTML={${htmlName}} />`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const propsName = `__island_props_${index++}`;
|
|
212
|
+
const importAlias = importAliasByName.get(chunk.islandImport.name) ?? chunk.islandImport.name;
|
|
213
|
+
const propsLiteral = Object.keys(chunk.props).length > 0 ? JSON.stringify(chunk.props) : "{}";
|
|
214
|
+
code += `\nconst ${propsName} = ${propsLiteral};`;
|
|
215
|
+
bodyLines.push(`<div data-island=${JSON.stringify(toIslandId(chunk.islandImport))} data-props={JSON.stringify(${propsName})} data-hydrate=${JSON.stringify(chunk.islandImport.strategy)}><${importAlias} {...${propsName}} /></div>`);
|
|
216
|
+
}
|
|
217
|
+
if (bodyLines.length === 0) bodyLines.push("<></>");
|
|
218
|
+
code += `
|
|
219
|
+
|
|
220
|
+
export default function MarkdownPage() {
|
|
221
|
+
return (
|
|
222
|
+
<FrontmatterContext.Provider value={__frontmatter}>
|
|
223
|
+
<>
|
|
224
|
+
${bodyLines.join("\n ")}
|
|
225
|
+
</>
|
|
226
|
+
</FrontmatterContext.Provider>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
return code;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/emit-svelte.ts
|
|
234
|
+
function toSvelteLiteral(value) {
|
|
235
|
+
return JSON.stringify(value).replace(/<\//g, "<\\/");
|
|
236
|
+
}
|
|
237
|
+
function emitSvelteComponent(result) {
|
|
238
|
+
const frontmatter = toSvelteLiteral(result.frontmatter);
|
|
239
|
+
return `<script context="module">
|
|
240
|
+
export const frontmatter = ${frontmatter};
|
|
241
|
+
export const headings = ${toSvelteLiteral(result.headings)};
|
|
242
|
+
<\/script>
|
|
243
|
+
|
|
244
|
+
<script>
|
|
245
|
+
import { setFrontmatter } from "@void/md";
|
|
246
|
+
|
|
247
|
+
const __frontmatter = ${frontmatter};
|
|
248
|
+
setFrontmatter(__frontmatter);
|
|
249
|
+
<\/script>
|
|
250
|
+
|
|
251
|
+
{@html ${toSvelteLiteral(result.html)}}
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
function emitSvelteComponentWithIslands(result, islandImports) {
|
|
255
|
+
const chunks = buildRenderChunks(result.html, islandImports);
|
|
256
|
+
const importAliasByName = /* @__PURE__ */ new Map();
|
|
257
|
+
const frontmatter = toSvelteLiteral(result.frontmatter);
|
|
258
|
+
let source = `<script context="module">
|
|
259
|
+
export const frontmatter = ${frontmatter};
|
|
260
|
+
export const headings = ${toSvelteLiteral(result.headings)};
|
|
261
|
+
<\/script>
|
|
262
|
+
|
|
263
|
+
<script>
|
|
264
|
+
import { setFrontmatter } from "@void/md";
|
|
265
|
+
`;
|
|
266
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
267
|
+
const item = islandImports[i];
|
|
268
|
+
const alias = `RawIsland${i}`;
|
|
269
|
+
importAliasByName.set(item.name, alias);
|
|
270
|
+
source += `import ${alias} from ${JSON.stringify(item.specifier)};
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
source += `
|
|
274
|
+
const __frontmatter = ${frontmatter};
|
|
275
|
+
setFrontmatter(__frontmatter);
|
|
276
|
+
`;
|
|
277
|
+
const body = [];
|
|
278
|
+
let index = 0;
|
|
279
|
+
for (const chunk of chunks) {
|
|
280
|
+
if (chunk.type === "html") {
|
|
281
|
+
body.push(`{@html ${toSvelteLiteral(chunk.html)}}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const propsName = `__island_props_${index++}`;
|
|
285
|
+
const alias = importAliasByName.get(chunk.islandImport.name) ?? chunk.islandImport.name;
|
|
286
|
+
const propsLiteral = Object.keys(chunk.props).length > 0 ? toSvelteLiteral(chunk.props) : "{}";
|
|
287
|
+
source += `const ${propsName} = ${propsLiteral};
|
|
288
|
+
`;
|
|
289
|
+
body.push(`<div data-island="${toIslandId(chunk.islandImport)}" data-props={JSON.stringify(${propsName})} data-hydrate="${chunk.islandImport.strategy}"><${alias} {...${propsName}} /></div>`);
|
|
290
|
+
}
|
|
291
|
+
source += `<\/script>
|
|
292
|
+
|
|
293
|
+
${body.join("\n")}
|
|
294
|
+
`;
|
|
295
|
+
return source;
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/emit-vue.ts
|
|
299
|
+
function emitVueComponent(result) {
|
|
300
|
+
return `
|
|
301
|
+
import { defineComponent, createStaticVNode, provide } from "vue";
|
|
302
|
+
|
|
303
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
304
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
305
|
+
const __html = ${JSON.stringify(result.html)};
|
|
306
|
+
|
|
307
|
+
export const frontmatter = __frontmatter;
|
|
308
|
+
export const headings = __headings;
|
|
309
|
+
|
|
310
|
+
export default defineComponent({
|
|
311
|
+
setup() {
|
|
312
|
+
provide("__void_frontmatter", __frontmatter);
|
|
313
|
+
return () => createStaticVNode(__html, 1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
const ISLAND_TAG_RE = /<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)\/\s*>|<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\3\s*>/g;
|
|
319
|
+
function findIslandTags(html, names) {
|
|
320
|
+
const matches = [];
|
|
321
|
+
let m;
|
|
322
|
+
ISLAND_TAG_RE.lastIndex = 0;
|
|
323
|
+
while ((m = ISLAND_TAG_RE.exec(html)) !== null) {
|
|
324
|
+
const name = m[1] ?? m[3];
|
|
325
|
+
if (names.has(name)) matches.push({
|
|
326
|
+
name,
|
|
327
|
+
start: m.index,
|
|
328
|
+
end: m.index + m[0].length,
|
|
329
|
+
attrs: (m[2] ?? m[4] ?? "").trim(),
|
|
330
|
+
children: m[5] ?? ""
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return matches;
|
|
334
|
+
}
|
|
335
|
+
function parseAttrs(attrStr) {
|
|
336
|
+
const result = {};
|
|
337
|
+
const re = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
338
|
+
let m;
|
|
339
|
+
while ((m = re.exec(attrStr)) !== null) result[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
function emitVueComponentWithIslands(result, islandImports) {
|
|
343
|
+
const nameSet = new Set(islandImports.map((i) => i.name));
|
|
344
|
+
const islandTags = findIslandTags(result.html, nameSet);
|
|
345
|
+
let code = `import { defineComponent, h, createStaticVNode, provide } from "vue";\n`;
|
|
346
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
347
|
+
const imp = islandImports[i];
|
|
348
|
+
code += `import _raw_island_${i} from ${JSON.stringify(imp.specifier)};\n`;
|
|
349
|
+
}
|
|
350
|
+
for (let i = 0; i < islandImports.length; i++) {
|
|
351
|
+
const imp = islandImports[i];
|
|
352
|
+
code += `const ${imp.name} = { inheritAttrs: false, render() {
|
|
353
|
+
const props = this.$attrs;
|
|
354
|
+
return h("div", {
|
|
355
|
+
"data-island": ${JSON.stringify(toIslandId(imp))},
|
|
356
|
+
"data-props": JSON.stringify(props),
|
|
357
|
+
"data-hydrate": ${JSON.stringify(imp.strategy)}
|
|
358
|
+
}, h(_raw_island_${i}, props));
|
|
359
|
+
}};\n`;
|
|
360
|
+
}
|
|
361
|
+
const vnodes = [];
|
|
362
|
+
let cursor = 0;
|
|
363
|
+
for (const tag of islandTags) {
|
|
364
|
+
if (tag.start > cursor) {
|
|
365
|
+
const segment = result.html.slice(cursor, tag.start);
|
|
366
|
+
if (segment.trim()) vnodes.push(`createStaticVNode(${JSON.stringify(segment)}, 1)`);
|
|
367
|
+
}
|
|
368
|
+
const attrs = parseAttrs(tag.attrs);
|
|
369
|
+
const propsStr = Object.keys(attrs).length > 0 ? JSON.stringify(attrs) : "null";
|
|
370
|
+
vnodes.push(`h(${tag.name}, ${propsStr})`);
|
|
371
|
+
cursor = tag.end;
|
|
372
|
+
}
|
|
373
|
+
if (cursor < result.html.length) {
|
|
374
|
+
const segment = result.html.slice(cursor);
|
|
375
|
+
if (segment.trim()) vnodes.push(`createStaticVNode(${JSON.stringify(segment)}, 1)`);
|
|
376
|
+
}
|
|
377
|
+
code += `
|
|
378
|
+
const __frontmatter = ${JSON.stringify(result.frontmatter)};
|
|
379
|
+
const __headings = ${JSON.stringify(result.headings)};
|
|
380
|
+
|
|
381
|
+
export const frontmatter = __frontmatter;
|
|
382
|
+
export const headings = __headings;
|
|
383
|
+
|
|
384
|
+
export default defineComponent({
|
|
385
|
+
setup() {
|
|
386
|
+
provide("__void_frontmatter", __frontmatter);
|
|
387
|
+
return () => [
|
|
388
|
+
${vnodes.join(",\n ")}
|
|
389
|
+
];
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
`;
|
|
393
|
+
return code;
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/extract-script.ts
|
|
397
|
+
const SCRIPT_RE = /^<script\b[^>]*>([\s\S]*?)<\/script>\s*\n?/;
|
|
398
|
+
const ISLAND_IMPORT_RE = /import\s+(\w+)\s+from\s+("[^"]+"|'[^']+')\s+with\s*\{\s*island\s*:\s*("[^"]+"|'[^']+')\s*\}/g;
|
|
399
|
+
const ISLAND_IMPORT_LINE_RE = /^[ \t]*import\s+\w+\s+from\s+(?:"[^"]+"|'[^']+')\s+with\s*\{\s*island\s*:\s*(?:"[^"]+"|'[^']+')\s*\}\s*;?\s*$/gm;
|
|
400
|
+
function extractScript(source) {
|
|
401
|
+
const match = source.match(SCRIPT_RE);
|
|
402
|
+
if (!match) return {
|
|
403
|
+
script: null,
|
|
404
|
+
body: source,
|
|
405
|
+
islandImports: [],
|
|
406
|
+
clientCode: null
|
|
407
|
+
};
|
|
408
|
+
const script = match[1].trim();
|
|
409
|
+
const body = source.slice(match[0].length);
|
|
410
|
+
const islandImports = [];
|
|
411
|
+
let m;
|
|
412
|
+
while ((m = ISLAND_IMPORT_RE.exec(script)) !== null) islandImports.push({
|
|
413
|
+
name: m[1],
|
|
414
|
+
specifier: m[2].slice(1, -1),
|
|
415
|
+
strategy: m[3].slice(1, -1)
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
script,
|
|
419
|
+
body,
|
|
420
|
+
islandImports,
|
|
421
|
+
clientCode: script.replace(ISLAND_IMPORT_LINE_RE, "").trim() || null
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/metadata.ts
|
|
426
|
+
let metadataCompiler = null;
|
|
427
|
+
function getMetadataCompiler() {
|
|
428
|
+
if (metadataCompiler) return metadataCompiler;
|
|
429
|
+
const md = new MarkdownExit({
|
|
430
|
+
html: true,
|
|
431
|
+
linkify: true
|
|
432
|
+
});
|
|
433
|
+
md.use(anchor, { permalink: anchor.permalink.ariaHidden({ placement: "before" }) });
|
|
434
|
+
metadataCompiler = md;
|
|
435
|
+
return md;
|
|
436
|
+
}
|
|
437
|
+
function extractHeadings(tokens) {
|
|
438
|
+
const headings = [];
|
|
439
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
440
|
+
const token = tokens[i];
|
|
441
|
+
if (token.type === "heading_open") {
|
|
442
|
+
const depth = Number(token.tag.slice(1));
|
|
443
|
+
const slug = token.attrGet?.("id") ?? "";
|
|
444
|
+
const text = (tokens[i + 1]?.children?.filter((t) => t.type === "text" || t.type === "code_inline").map((t) => t.content).join("") ?? "").trim();
|
|
445
|
+
headings.push({
|
|
446
|
+
depth,
|
|
447
|
+
slug,
|
|
448
|
+
text
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return headings;
|
|
453
|
+
}
|
|
454
|
+
function extractMetadata(source) {
|
|
455
|
+
const { data: frontmatter, content } = matter(source);
|
|
456
|
+
const headings = extractHeadings(getMetadataCompiler().parse(content, {}));
|
|
457
|
+
return {
|
|
458
|
+
frontmatter,
|
|
459
|
+
headings,
|
|
460
|
+
title: frontmatter.title ?? headings.find((h) => h.depth === 1)?.text ?? ""
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/plugin.ts
|
|
465
|
+
const MD_CLIENT_ENTRY_ID = "virtual:void-md-client";
|
|
466
|
+
const ADAPTER_PLUGIN_NAMES = {
|
|
467
|
+
"void-vue:pages": "vue",
|
|
468
|
+
"void-react:pages": "react",
|
|
469
|
+
"void-svelte:pages": "svelte",
|
|
470
|
+
"void-solid:pages": "solid"
|
|
471
|
+
};
|
|
472
|
+
const SVELTE_VIRTUAL_PREFIX = "/@void-md-svelte/";
|
|
473
|
+
const SOLID_VIRTUAL_PREFIX = "/@void-md-solid/";
|
|
474
|
+
function hasMdFilesWithScript(dir) {
|
|
475
|
+
try {
|
|
476
|
+
for (const entry of readdirSync(dir, {
|
|
477
|
+
withFileTypes: true,
|
|
478
|
+
recursive: true
|
|
479
|
+
})) if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
480
|
+
const content = readFileSync(join(entry.parentPath, entry.name), "utf-8");
|
|
481
|
+
if (/^<script\b/m.test(content)) return true;
|
|
482
|
+
}
|
|
483
|
+
} catch {}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
function hasIslandFiles(dir) {
|
|
487
|
+
try {
|
|
488
|
+
for (const entry of readdirSync(dir, {
|
|
489
|
+
withFileTypes: true,
|
|
490
|
+
recursive: true
|
|
491
|
+
})) if (entry.isFile() && /\.island\./.test(entry.name)) return true;
|
|
492
|
+
} catch {}
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
function voidMarkdown(options) {
|
|
496
|
+
let adapter = null;
|
|
497
|
+
let mdPages = [];
|
|
498
|
+
let root = "";
|
|
499
|
+
const svelteModules = /* @__PURE__ */ new Map();
|
|
500
|
+
const solidModules = /* @__PURE__ */ new Map();
|
|
501
|
+
const clientScripts = /* @__PURE__ */ new Map();
|
|
502
|
+
function createVirtualModuleId(prefix, importerId, source, extension) {
|
|
503
|
+
return `${prefix}${Buffer.from(importerId).toString("base64url")}.${createHash("sha1").update(source).digest("hex").slice(0, 8)}.${extension}`;
|
|
504
|
+
}
|
|
505
|
+
async function scanMdPages() {
|
|
506
|
+
const pagesDir = join(root, "pages");
|
|
507
|
+
if (!existsSync(pagesDir)) return [];
|
|
508
|
+
const mdFiles = await glob("**/*.md", { cwd: pagesDir });
|
|
509
|
+
return (await Promise.all(mdFiles.map(async (file) => {
|
|
510
|
+
const { body, clientCode } = extractScript(readFileSync(join(pagesDir, file), "utf-8"));
|
|
511
|
+
const result = extractMetadata(body);
|
|
512
|
+
const componentId = file.replace(/\.md$/, "").replace(/\/index$/, "").replace(/^index$/, "") || "index";
|
|
513
|
+
if (clientCode) clientScripts.set(componentId, clientCode);
|
|
514
|
+
return {
|
|
515
|
+
path: "/" + file.replace(/\.md$/, "").replace(/\/index$/, "").replace(/^index$/, "") || "/",
|
|
516
|
+
title: result.title,
|
|
517
|
+
frontmatter: result.frontmatter,
|
|
518
|
+
headings: result.headings
|
|
519
|
+
};
|
|
520
|
+
}))).sort((a, b) => a.path.localeCompare(b.path));
|
|
521
|
+
}
|
|
522
|
+
function writeMdTypes() {
|
|
523
|
+
const outDir = join(root, ".void");
|
|
524
|
+
mkdirSync(outDir, { recursive: true });
|
|
525
|
+
writeFileSync(join(outDir, "md.d.ts"), generateMdTypes());
|
|
526
|
+
}
|
|
527
|
+
return [{
|
|
528
|
+
name: "void-md",
|
|
529
|
+
enforce: "pre",
|
|
530
|
+
api: {
|
|
531
|
+
clientEntryId: MD_CLIENT_ENTRY_ID,
|
|
532
|
+
getClientScripts() {
|
|
533
|
+
return clientScripts;
|
|
534
|
+
},
|
|
535
|
+
getClientScriptIds() {
|
|
536
|
+
return new Set(clientScripts.keys());
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
config(config, env) {
|
|
540
|
+
if (env.command === "serve") return;
|
|
541
|
+
const pagesDir = join(config.root || process.cwd(), "pages");
|
|
542
|
+
if (!existsSync(pagesDir)) return;
|
|
543
|
+
if (hasMdFilesWithScript(pagesDir) && hasIslandFiles(pagesDir)) return { environments: { client: { build: { rollupOptions: { input: { "md-client": MD_CLIENT_ENTRY_ID } } } } } };
|
|
544
|
+
},
|
|
545
|
+
configResolved(config) {
|
|
546
|
+
root = config.root;
|
|
547
|
+
for (const plugin of config.plugins) {
|
|
548
|
+
const match = ADAPTER_PLUGIN_NAMES[plugin.name];
|
|
549
|
+
if (match) {
|
|
550
|
+
adapter = match;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!adapter) throw new Error("void-md: No framework adapter detected. Install @void/vue, @void/react, @void/svelte, or @void/solid.");
|
|
555
|
+
},
|
|
556
|
+
async buildStart() {
|
|
557
|
+
svelteModules.clear();
|
|
558
|
+
solidModules.clear();
|
|
559
|
+
clientScripts.clear();
|
|
560
|
+
mdPages = await scanMdPages();
|
|
561
|
+
writeMdTypes();
|
|
562
|
+
},
|
|
563
|
+
configureServer(server) {
|
|
564
|
+
const handleMdChange = async (file) => {
|
|
565
|
+
if (!file.endsWith(".md") || !file.includes("/pages/")) return;
|
|
566
|
+
mdPages = await scanMdPages();
|
|
567
|
+
const mod = server.moduleGraph.getModuleById("\0void-md-pages");
|
|
568
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
569
|
+
for (const [compId] of clientScripts) {
|
|
570
|
+
const scriptMod = server.moduleGraph.getModuleById("\0virtual:void-md-script:" + compId);
|
|
571
|
+
if (scriptMod) server.moduleGraph.invalidateModule(scriptMod);
|
|
572
|
+
}
|
|
573
|
+
const clientMod = server.moduleGraph.getModuleById("\0" + MD_CLIENT_ENTRY_ID);
|
|
574
|
+
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
575
|
+
};
|
|
576
|
+
server.watcher.on("change", handleMdChange);
|
|
577
|
+
server.watcher.on("add", handleMdChange);
|
|
578
|
+
server.watcher.on("unlink", handleMdChange);
|
|
579
|
+
},
|
|
580
|
+
resolveId: {
|
|
581
|
+
filter: { id: /^(@void\/md(\/pages)?|\/@void-md-(svelte|solid)\/|virtual:void-md-(script:|client))/ },
|
|
582
|
+
handler(source) {
|
|
583
|
+
if (source === MD_CLIENT_ENTRY_ID) return "\0" + MD_CLIENT_ENTRY_ID;
|
|
584
|
+
if (source.startsWith("virtual:void-md-script:")) return "\0" + source;
|
|
585
|
+
if (source === "@void/md") return "\0void-md-runtime";
|
|
586
|
+
if (source === "@void/md/pages") return "\0void-md-pages";
|
|
587
|
+
if (source.startsWith(SVELTE_VIRTUAL_PREFIX) || source.startsWith(SOLID_VIRTUAL_PREFIX)) return source;
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
load: {
|
|
591
|
+
filter: { id: /^(\0void-md|\/@void-md-(svelte|solid)\/|\0virtual:void-md-(script:|client))/ },
|
|
592
|
+
handler(id) {
|
|
593
|
+
if (id === "\0" + MD_CLIENT_ENTRY_ID) return generateMdClientEntry(clientScripts);
|
|
594
|
+
if (id.startsWith("\0virtual:void-md-script:")) {
|
|
595
|
+
const componentId = id.slice(24);
|
|
596
|
+
const code = clientScripts.get(componentId);
|
|
597
|
+
if (code) return code;
|
|
598
|
+
throw new Error(`void-md: Missing client script for '${componentId}'.`);
|
|
599
|
+
}
|
|
600
|
+
if (id.startsWith(SVELTE_VIRTUAL_PREFIX)) {
|
|
601
|
+
const source = svelteModules.get(id);
|
|
602
|
+
if (source) return source;
|
|
603
|
+
throw new Error(`void-md: Missing Svelte markdown module '${id}'.`);
|
|
604
|
+
}
|
|
605
|
+
if (id.startsWith(SOLID_VIRTUAL_PREFIX)) {
|
|
606
|
+
const source = solidModules.get(id);
|
|
607
|
+
if (source) return source;
|
|
608
|
+
throw new Error(`void-md: Missing Solid markdown module '${id}'.`);
|
|
609
|
+
}
|
|
610
|
+
if (id === "\0void-md-runtime") {
|
|
611
|
+
if (adapter === "vue") return `
|
|
612
|
+
import { inject } from "vue";
|
|
613
|
+
export function useFrontmatter() { return inject("__void_frontmatter", {}); }
|
|
614
|
+
`;
|
|
615
|
+
if (adapter === "react") return `
|
|
616
|
+
import { createContext, useContext } from "react";
|
|
617
|
+
export const FrontmatterContext = createContext({});
|
|
618
|
+
export function useFrontmatter() { return useContext(FrontmatterContext) ?? {}; }
|
|
619
|
+
`;
|
|
620
|
+
if (adapter === "svelte") return `
|
|
621
|
+
import { getContext, setContext } from "svelte";
|
|
622
|
+
const FRONTMATTER_KEY = "__void_frontmatter";
|
|
623
|
+
export function setFrontmatter(frontmatter) { setContext(FRONTMATTER_KEY, frontmatter); }
|
|
624
|
+
export function useFrontmatter() { return getContext(FRONTMATTER_KEY) ?? {}; }
|
|
625
|
+
`;
|
|
626
|
+
if (adapter === "solid") return `
|
|
627
|
+
import { createContext, useContext } from "solid-js";
|
|
628
|
+
export const FrontmatterContext = createContext({});
|
|
629
|
+
export function useFrontmatter() { return useContext(FrontmatterContext) ?? {}; }
|
|
630
|
+
`;
|
|
631
|
+
throw new Error("void-md: Adapter is not initialized.");
|
|
632
|
+
}
|
|
633
|
+
if (id === "\0void-md-pages") return `export default ${JSON.stringify(mdPages)};`;
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
transform: {
|
|
637
|
+
filter: { id: /\.md$/ },
|
|
638
|
+
async handler(code, _id) {
|
|
639
|
+
const { body, islandImports, clientCode } = extractScript(code);
|
|
640
|
+
const pagesPrefix = join(root, "pages") + "/";
|
|
641
|
+
if (_id.startsWith(pagesPrefix) && clientCode) {
|
|
642
|
+
const componentId = _id.slice(pagesPrefix.length).replace(/\.md$/, "").replace(/\/index$/, "").replace(/^index$/, "") || "index";
|
|
643
|
+
clientScripts.set(componentId, clientCode);
|
|
644
|
+
}
|
|
645
|
+
for (const imp of islandImports) {
|
|
646
|
+
const absPath = resolve(dirname(_id), imp.specifier);
|
|
647
|
+
imp.islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
648
|
+
}
|
|
649
|
+
const { compile } = await import("./compile-BRZW8ppH.mjs");
|
|
650
|
+
const result = await compile(body, { shiki: options?.shiki });
|
|
651
|
+
if (adapter === "vue") {
|
|
652
|
+
if (islandImports.length > 0) return {
|
|
653
|
+
code: emitVueComponentWithIslands(result, islandImports),
|
|
654
|
+
map: null
|
|
655
|
+
};
|
|
656
|
+
return {
|
|
657
|
+
code: emitVueComponent(result),
|
|
658
|
+
map: null
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (adapter === "react") {
|
|
662
|
+
if (islandImports.length > 0) return {
|
|
663
|
+
code: emitReactComponentWithIslands(result, islandImports),
|
|
664
|
+
map: null
|
|
665
|
+
};
|
|
666
|
+
return {
|
|
667
|
+
code: emitReactComponent(result),
|
|
668
|
+
map: null
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (adapter === "svelte") {
|
|
672
|
+
const virtualId = createVirtualModuleId(SVELTE_VIRTUAL_PREFIX, _id, code, "svelte");
|
|
673
|
+
svelteModules.set(virtualId, islandImports.length > 0 ? emitSvelteComponentWithIslands(result, islandImports) : emitSvelteComponent(result));
|
|
674
|
+
return {
|
|
675
|
+
code: `export { default, frontmatter, headings } from ${JSON.stringify(virtualId)};`,
|
|
676
|
+
map: null
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (adapter === "solid") {
|
|
680
|
+
const virtualId = createVirtualModuleId(SOLID_VIRTUAL_PREFIX, _id, code, "tsx");
|
|
681
|
+
solidModules.set(virtualId, islandImports.length > 0 ? emitSolidComponentWithIslands(result, islandImports) : emitSolidComponent(result));
|
|
682
|
+
return {
|
|
683
|
+
code: `export { default, frontmatter, headings } from ${JSON.stringify(virtualId)};`,
|
|
684
|
+
map: null
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
throw new Error(`void-md: Unsupported adapter '${adapter}'.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}];
|
|
691
|
+
}
|
|
692
|
+
function generateMdClientEntry(scripts) {
|
|
693
|
+
if (scripts.size === 0) return "";
|
|
694
|
+
const lines = [];
|
|
695
|
+
lines.push("const mdScripts = {");
|
|
696
|
+
for (const id of scripts.keys()) lines.push(` ${JSON.stringify(id)}: () => import(${JSON.stringify("virtual:void-md-script:" + id)}),`);
|
|
697
|
+
lines.push("};");
|
|
698
|
+
lines.push(`const mdScriptId = document.getElementById("app")?.getAttribute("data-md-script");`);
|
|
699
|
+
lines.push(`if (mdScriptId && mdScripts[mdScriptId]) mdScripts[mdScriptId]();`);
|
|
700
|
+
return lines.join("\n");
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
export { voidMarkdown };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/runtime/index.d.ts
|
|
2
|
+
declare function useFrontmatter(): Record<string, unknown>;
|
|
3
|
+
declare const FrontmatterContext: Record<string, unknown> | null;
|
|
4
|
+
declare function setFrontmatter(_frontmatter: Record<string, unknown>): void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { FrontmatterContext, setFrontmatter, useFrontmatter };
|