@ox-content/vite-plugin 2.88.0 → 2.90.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/dist/index.cjs +1015 -674
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +470 -404
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +470 -404
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1016 -675
- package/dist/index.mjs.map +1 -1
- package/dist/jsx-html.cjs +27 -5
- package/dist/jsx-html.cjs.map +1 -1
- package/dist/jsx-html.d.cts +2 -0
- package/dist/jsx-html.d.cts.map +1 -1
- package/dist/jsx-html.d.mts +2 -0
- package/dist/jsx-html.d.mts.map +1 -1
- package/dist/jsx-html.mjs +27 -5
- package/dist/jsx-html.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,119 @@ function createMarkdownEnvironment(options) {
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
//#endregion
|
|
112
|
+
//#region src/highlight-native.ts
|
|
113
|
+
/**
|
|
114
|
+
* Extract text content from a hast node.
|
|
115
|
+
*/
|
|
116
|
+
function getTextContent(node) {
|
|
117
|
+
let text = "";
|
|
118
|
+
if ("children" in node) {
|
|
119
|
+
for (const child of node.children) if (child.type === "text") text += child.value;
|
|
120
|
+
else if (child.type === "element") text += getTextContent(child);
|
|
121
|
+
}
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
function normalizeClassName(className) {
|
|
125
|
+
if (Array.isArray(className)) return className.filter((value) => typeof value === "string");
|
|
126
|
+
if (typeof className === "string" && className) return className.split(/\s+/).filter(Boolean);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Highlights with the native tree-sitter engine, or `null` when it has no
|
|
131
|
+
* grammar for `lang`.
|
|
132
|
+
*
|
|
133
|
+
* Parsing once and walking the tree is roughly eight times faster than
|
|
134
|
+
* matching TextMate patterns line by line — 10.5 ms against 81.5 ms over the
|
|
135
|
+
* documentation corpus's code blocks — and it emits the same
|
|
136
|
+
* `--octc-shiki-*` markup, so themes are unaffected.
|
|
137
|
+
*/
|
|
138
|
+
function highlightNatively(code, lang) {
|
|
139
|
+
try {
|
|
140
|
+
return require_vitepress.importNapiModuleSync().highlightCodeBlock(code, lang);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Whether the native engine claims `lang`. */
|
|
146
|
+
function nativeSupports(lang) {
|
|
147
|
+
try {
|
|
148
|
+
return require_vitepress.importNapiModuleSync().supportsHighlightLanguage(lang);
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether any block in this tree still needs Shiki.
|
|
155
|
+
*
|
|
156
|
+
* Creating a Shiki highlighter parses two dozen TextMate grammars, about
|
|
157
|
+
* 190 ms, so a document whose languages are all covered natively must not
|
|
158
|
+
* touch it at all.
|
|
159
|
+
*/
|
|
160
|
+
function treeNeedsShiki(tree, nativeThemeApplies) {
|
|
161
|
+
if (!nativeThemeApplies) return true;
|
|
162
|
+
let needed = false;
|
|
163
|
+
const walk = (node) => {
|
|
164
|
+
if (needed || !("children" in node)) return;
|
|
165
|
+
for (const child of node.children) {
|
|
166
|
+
if (child.type !== "element") continue;
|
|
167
|
+
if (child.tagName === "code") {
|
|
168
|
+
const lang = languageOf(child);
|
|
169
|
+
if (lang !== null && !nativeSupports(lang)) {
|
|
170
|
+
needed = true;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
walk(child);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
walk(tree);
|
|
178
|
+
return needed;
|
|
179
|
+
}
|
|
180
|
+
/** The `language-*` class on a `<code>` element, if it carries one. */
|
|
181
|
+
function languageOf(codeElement) {
|
|
182
|
+
const className = normalizeClassName(codeElement.properties?.className).find((value) => value.startsWith("language-"));
|
|
183
|
+
return className ? className.slice(9) : null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Highlights every code block in a rendered document in one native call.
|
|
187
|
+
*
|
|
188
|
+
* Returns the rewritten HTML and the languages it declined, so the caller
|
|
189
|
+
* knows whether Shiki still has to run over the result. Returns `null` when
|
|
190
|
+
* the native module is unavailable.
|
|
191
|
+
*
|
|
192
|
+
* This exists because the plumbing dwarfed the work: walking each page
|
|
193
|
+
* through an HTML parser and serializer to find `<pre>` elements cost 139 ms
|
|
194
|
+
* over the documentation corpus, and re-parsing each highlighted block to
|
|
195
|
+
* splice it back cost another 38 ms, against 14 ms of actual highlighting.
|
|
196
|
+
*
|
|
197
|
+
* It runs off the main thread. The synchronous binding held the event loop
|
|
198
|
+
* for the whole pass, so a build asking for several pages at once still got
|
|
199
|
+
* them one at a time — `Promise.all` over the corpus measured the same as
|
|
200
|
+
* awaiting each page in turn.
|
|
201
|
+
*/
|
|
202
|
+
async function highlightDocumentNatively(html) {
|
|
203
|
+
try {
|
|
204
|
+
return await require_vitepress.importNapiModuleSync().highlightHtmlCodeBlocksAsync(html);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Splices `replacements` back over the blocks the native pass left pending.
|
|
211
|
+
*
|
|
212
|
+
* Entry `i` is the highlighted `<pre>` for pending block `i`, or an empty
|
|
213
|
+
* string to leave that block alone. This keeps a page that needs one exotic
|
|
214
|
+
* grammar — a Vue SFC, a Mermaid diagram — off the HTML round trip, rather
|
|
215
|
+
* than surrendering the whole document for it.
|
|
216
|
+
*/
|
|
217
|
+
function applyPendingHighlights(html, replacements) {
|
|
218
|
+
try {
|
|
219
|
+
return require_vitepress.importNapiModuleSync().applyPendingHighlights(html, replacements);
|
|
220
|
+
} catch {
|
|
221
|
+
return html;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
112
225
|
//#region src/shiki-theme.ts
|
|
113
226
|
/**
|
|
114
227
|
* Name callers pass as `highlightTheme` to render syntax colors as CSS custom
|
|
@@ -233,17 +346,19 @@ function rehypeShikiHighlight(options) {
|
|
|
233
346
|
const { theme, langs } = options;
|
|
234
347
|
return async (tree) => {
|
|
235
348
|
const { themeName } = normalizeThemeInput(theme);
|
|
236
|
-
const
|
|
349
|
+
const nativeThemeApplies = themeName === CSS_VARIABLES_THEME;
|
|
350
|
+
const highlighter = treeNeedsShiki(tree, nativeThemeApplies) ? await getHighlighter(theme, langs) : void 0;
|
|
237
351
|
const highlightBlockCode = (codeElement) => {
|
|
238
352
|
let lang = "text";
|
|
239
353
|
const langClass = normalizeClassName(codeElement.properties?.className).find((value) => value.startsWith("language-"));
|
|
240
354
|
if (langClass) lang = langClass.replace("language-", "");
|
|
241
355
|
const codeText = getTextContent(codeElement);
|
|
242
356
|
try {
|
|
243
|
-
const highlighted = highlighter
|
|
357
|
+
const highlighted = (nativeThemeApplies ? highlightNatively(codeText, lang) : null) ?? highlighter?.codeToHtml(codeText, {
|
|
244
358
|
lang,
|
|
245
359
|
theme: themeName
|
|
246
360
|
});
|
|
361
|
+
if (highlighted === void 0) return null;
|
|
247
362
|
const parsed = (0, unified.unified)().use(rehypeParse$3, { fragment: true }).parse(highlighted);
|
|
248
363
|
if (parsed.children[0]?.type === "element") {
|
|
249
364
|
const highlightedPre = parsed.children[0];
|
|
@@ -262,10 +377,11 @@ function rehypeShikiHighlight(options) {
|
|
|
262
377
|
lang = langClass.replace("language-", "");
|
|
263
378
|
const codeText = getTextContent(codeElement);
|
|
264
379
|
try {
|
|
265
|
-
const highlighted = highlighter
|
|
380
|
+
const highlighted = (nativeThemeApplies ? highlightNatively(codeText, lang) : null) ?? highlighter?.codeToHtml(codeText, {
|
|
266
381
|
lang,
|
|
267
382
|
theme: themeName
|
|
268
383
|
});
|
|
384
|
+
if (highlighted === void 0) return null;
|
|
269
385
|
const parsed = (0, unified.unified)().use(rehypeParse$3, { fragment: true }).parse(highlighted);
|
|
270
386
|
if (parsed.children[0]?.type === "element") {
|
|
271
387
|
const highlightedCode = parsed.children[0].children.find((child) => child.type === "element" && child.tagName === "code");
|
|
@@ -289,7 +405,8 @@ function rehypeShikiHighlight(options) {
|
|
|
289
405
|
const child = node.children[i];
|
|
290
406
|
if (child.type === "element" && child.tagName === "pre") {
|
|
291
407
|
const codeElement = child.children.find((c) => c.type === "element" && c.tagName === "code");
|
|
292
|
-
|
|
408
|
+
const alreadyHighlighted = normalizeClassName(child.properties?.className).includes("shiki");
|
|
409
|
+
if (codeElement && !alreadyHighlighted) {
|
|
293
410
|
const highlightedPre = highlightBlockCode(codeElement);
|
|
294
411
|
if (highlightedPre) node.children[i] = highlightedPre;
|
|
295
412
|
}
|
|
@@ -303,22 +420,6 @@ function rehypeShikiHighlight(options) {
|
|
|
303
420
|
};
|
|
304
421
|
}
|
|
305
422
|
/**
|
|
306
|
-
* Extract text content from a hast node.
|
|
307
|
-
*/
|
|
308
|
-
function getTextContent(node) {
|
|
309
|
-
let text = "";
|
|
310
|
-
if ("children" in node) {
|
|
311
|
-
for (const child of node.children) if (child.type === "text") text += child.value;
|
|
312
|
-
else if (child.type === "element") text += getTextContent(child);
|
|
313
|
-
}
|
|
314
|
-
return text;
|
|
315
|
-
}
|
|
316
|
-
function normalizeClassName(className) {
|
|
317
|
-
if (Array.isArray(className)) return className.filter((value) => typeof value === "string");
|
|
318
|
-
if (typeof className === "string" && className) return className.split(/\s+/).filter(Boolean);
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
423
|
* Apply syntax highlighting to HTML using Shiki.
|
|
323
424
|
*/
|
|
324
425
|
async function highlightCode(html, theme = CSS_VARIABLES_THEME, langs = []) {
|
|
@@ -329,6 +430,83 @@ async function highlightCode(html, theme = CSS_VARIABLES_THEME, langs = []) {
|
|
|
329
430
|
return String(result);
|
|
330
431
|
}
|
|
331
432
|
//#endregion
|
|
433
|
+
//#region src/highlight-pending.ts
|
|
434
|
+
/**
|
|
435
|
+
* Highlighting the blocks the native pass could not claim.
|
|
436
|
+
*
|
|
437
|
+
* These arrive already named — the native pass reports each one's language —
|
|
438
|
+
* which lets this load exactly the grammars a page needs instead of the whole
|
|
439
|
+
* bundled set, and splice each result back without an HTML parser.
|
|
440
|
+
*/
|
|
441
|
+
/**
|
|
442
|
+
* A highlighter that starts with no grammars and gains them on demand.
|
|
443
|
+
*
|
|
444
|
+
* `getHighlighter` loads all two dozen `BUILTIN_LANGS` up front, because the
|
|
445
|
+
* tree walk it serves discovers a page's languages only while rewriting it.
|
|
446
|
+
* The pending list does not have that problem — it names every language it
|
|
447
|
+
* needs — and paying for two dozen TextMate grammars to highlight one Vue
|
|
448
|
+
* block is most of what a page with an exotic block costs.
|
|
449
|
+
*/
|
|
450
|
+
const lazyHighlighterCache = /* @__PURE__ */ new Map();
|
|
451
|
+
const loadedLangs = /* @__PURE__ */ new WeakMap();
|
|
452
|
+
async function getLazyHighlighter(theme, customLangs, wanted) {
|
|
453
|
+
const { themeInput } = normalizeThemeInput(theme);
|
|
454
|
+
const cacheKey = JSON.stringify({
|
|
455
|
+
theme: themeInput,
|
|
456
|
+
langs: customLangs
|
|
457
|
+
});
|
|
458
|
+
let highlighterPromise = lazyHighlighterCache.get(cacheKey);
|
|
459
|
+
if (!highlighterPromise) {
|
|
460
|
+
highlighterPromise = (0, shiki.createHighlighter)({
|
|
461
|
+
themes: [themeInput],
|
|
462
|
+
langs: customLangs
|
|
463
|
+
});
|
|
464
|
+
lazyHighlighterCache.set(cacheKey, highlighterPromise);
|
|
465
|
+
}
|
|
466
|
+
const highlighter = await highlighterPromise;
|
|
467
|
+
let loaded = loadedLangs.get(highlighter);
|
|
468
|
+
if (!loaded) {
|
|
469
|
+
loaded = /* @__PURE__ */ new Set();
|
|
470
|
+
loadedLangs.set(highlighter, loaded);
|
|
471
|
+
}
|
|
472
|
+
const missing = [...new Set(wanted.filter((lang) => !loaded.has(lang) && BUILTIN_LANGS.includes(lang)))];
|
|
473
|
+
if (missing.length > 0) await Promise.all(missing.map(async (lang) => {
|
|
474
|
+
try {
|
|
475
|
+
await highlighter.loadLanguage(lang);
|
|
476
|
+
loaded.add(lang);
|
|
477
|
+
} catch {}
|
|
478
|
+
}));
|
|
479
|
+
return highlighter;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Highlight the blocks the native pass left pending, in order.
|
|
483
|
+
*
|
|
484
|
+
* Entry `i` of the result is the `<pre>` for block `i`, or an empty string
|
|
485
|
+
* when Shiki has no grammar for it either — in which case the block is left
|
|
486
|
+
* exactly as it arrived, the same outcome the tree walk reached by keeping the
|
|
487
|
+
* original element.
|
|
488
|
+
*
|
|
489
|
+
* This is the whole point of the pending list: a page whose only unsupported
|
|
490
|
+
* block is a Mermaid diagram used to be handed to the tree walk in full, which
|
|
491
|
+
* re-highlighted every one of its other blocks and paid for a parse and a
|
|
492
|
+
* serialize of the page to do it.
|
|
493
|
+
*/
|
|
494
|
+
async function highlightPendingBlocks(blocks, theme = CSS_VARIABLES_THEME, langs = []) {
|
|
495
|
+
if (blocks.length === 0) return [];
|
|
496
|
+
const { themeName } = normalizeThemeInput(theme);
|
|
497
|
+
const highlighter = await getLazyHighlighter(theme, langs, blocks.map((block) => block.language));
|
|
498
|
+
return blocks.map((block) => {
|
|
499
|
+
try {
|
|
500
|
+
return highlighter.codeToHtml(block.source, {
|
|
501
|
+
lang: block.language,
|
|
502
|
+
theme: themeName
|
|
503
|
+
});
|
|
504
|
+
} catch {
|
|
505
|
+
return "";
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
332
510
|
//#region src/plugins/mermaid.ts
|
|
333
511
|
/**
|
|
334
512
|
* Mermaid Plugin - Native Rust renderer via NAPI
|
|
@@ -342,21 +520,21 @@ var mermaid_exports = /* @__PURE__ */ require_vitepress.__exportAll({
|
|
|
342
520
|
transformMermaidStatic: () => transformMermaidStatic
|
|
343
521
|
});
|
|
344
522
|
/** Cached NAPI bindings */
|
|
345
|
-
let napiBindings
|
|
346
|
-
let napiLoadAttempted
|
|
523
|
+
let napiBindings = null;
|
|
524
|
+
let napiLoadAttempted = false;
|
|
347
525
|
async function loadNapi() {
|
|
348
|
-
if (napiLoadAttempted
|
|
349
|
-
napiLoadAttempted
|
|
526
|
+
if (napiLoadAttempted) return napiBindings;
|
|
527
|
+
napiLoadAttempted = true;
|
|
350
528
|
try {
|
|
351
529
|
const binding = await require_vitepress.importNapiModule();
|
|
352
530
|
if (typeof binding.transformMermaid !== "function") {
|
|
353
|
-
napiBindings
|
|
531
|
+
napiBindings = null;
|
|
354
532
|
return null;
|
|
355
533
|
}
|
|
356
|
-
napiBindings
|
|
534
|
+
napiBindings = binding;
|
|
357
535
|
return binding;
|
|
358
536
|
} catch {
|
|
359
|
-
napiBindings
|
|
537
|
+
napiBindings = null;
|
|
360
538
|
return null;
|
|
361
539
|
}
|
|
362
540
|
}
|
|
@@ -1947,24 +2125,25 @@ function normalizeDiagnostic(diagnostic) {
|
|
|
1947
2125
|
//#endregion
|
|
1948
2126
|
//#region src/transform.ts
|
|
1949
2127
|
/**
|
|
1950
|
-
*
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
2128
|
+
* The NAPI load, cached as the promise rather than as its result.
|
|
2129
|
+
*
|
|
2130
|
+
* The load yields, and a caller arriving during that yield has to wait for it
|
|
2131
|
+
* rather than read a result that is not there yet. Holding the promise is what
|
|
2132
|
+
* makes every caller wait for the same load; holding an "already attempted"
|
|
2133
|
+
* flag beside an unset result meant the first page to arrive loaded the module
|
|
2134
|
+
* and every page behind it concluded there were no bindings at all.
|
|
2135
|
+
*
|
|
1958
2136
|
* @internal
|
|
1959
2137
|
*/
|
|
1960
|
-
let
|
|
2138
|
+
let napiLoad;
|
|
1961
2139
|
/**
|
|
1962
2140
|
* Lazily loads and caches NAPI bindings.
|
|
1963
2141
|
*
|
|
1964
2142
|
* This function uses lazy loading to defer the import of NAPI bindings
|
|
1965
2143
|
* until they're actually needed. The bindings are loaded only once and
|
|
1966
|
-
* cached for subsequent uses
|
|
1967
|
-
*
|
|
2144
|
+
* cached for subsequent uses, including by callers that ask for them while
|
|
2145
|
+
* that first load is still in flight. If loading fails (e.g., bindings not
|
|
2146
|
+
* built), the failure is cached to avoid repeated load attempts.
|
|
1968
2147
|
*
|
|
1969
2148
|
* ## Performance Considerations
|
|
1970
2149
|
*
|
|
@@ -1995,18 +2174,12 @@ let napiLoadAttempted = false;
|
|
|
1995
2174
|
*
|
|
1996
2175
|
* @internal
|
|
1997
2176
|
*/
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
napiLoadAttempted = true;
|
|
2001
|
-
try {
|
|
2002
|
-
const mod = await require_vitepress.importNapiModule();
|
|
2003
|
-
napiBindings = mod;
|
|
2004
|
-
return mod;
|
|
2005
|
-
} catch (error) {
|
|
2177
|
+
function loadNapiBindings() {
|
|
2178
|
+
napiLoad ??= require_vitepress.importNapiModule().catch((error) => {
|
|
2006
2179
|
if (process.env.DEBUG) console.debug("[ox-content] NAPI bindings load failed:", error);
|
|
2007
|
-
napiBindings = null;
|
|
2008
2180
|
return null;
|
|
2009
|
-
}
|
|
2181
|
+
});
|
|
2182
|
+
return napiLoad;
|
|
2010
2183
|
}
|
|
2011
2184
|
async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
2012
2185
|
const napi = await loadNapiBindings();
|
|
@@ -2061,9 +2234,18 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2061
2234
|
const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
|
|
2062
2235
|
html = protectedHtml;
|
|
2063
2236
|
if (options.highlight) {
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2237
|
+
const native = options.highlightTheme === void 0 || options.highlightTheme === "css-variables" ? await highlightDocumentNatively(html) : null;
|
|
2238
|
+
if (native && native.skipped.length === 0) {
|
|
2239
|
+
html = native.html;
|
|
2240
|
+
if (native.pending.length > 0) {
|
|
2241
|
+
const replacements = await highlightPendingBlocks(native.pending, options.highlightTheme, options.highlightLangs);
|
|
2242
|
+
html = applyPendingHighlights(html, replacements);
|
|
2243
|
+
}
|
|
2244
|
+
} else {
|
|
2245
|
+
const originalHtml = html;
|
|
2246
|
+
const highlightedHtml = await highlightCode(html, options.highlightTheme, options.highlightLangs);
|
|
2247
|
+
html = napi.mergeHighlightedCodeBlocks(originalHtml, highlightedHtml);
|
|
2248
|
+
}
|
|
2067
2249
|
}
|
|
2068
2250
|
html = await transformBuiltinEmbeds(html, options.embeds ?? {
|
|
2069
2251
|
github: {},
|
|
@@ -2177,6 +2359,56 @@ if (import.meta.hot) {
|
|
|
2177
2359
|
}
|
|
2178
2360
|
//#endregion
|
|
2179
2361
|
//#region src/docs.ts
|
|
2362
|
+
/**
|
|
2363
|
+
* Source Documentation Extraction and Generation
|
|
2364
|
+
*
|
|
2365
|
+
* This module provides comprehensive tools for extracting JSDoc/TSDoc comments
|
|
2366
|
+
* from TypeScript/JavaScript source files and automatically generating Markdown
|
|
2367
|
+
* documentation.
|
|
2368
|
+
*
|
|
2369
|
+
* ## Features
|
|
2370
|
+
*
|
|
2371
|
+
* - **Automatic Extraction**: Parses JSDoc comments from functions, classes, interfaces, and types
|
|
2372
|
+
* - **Flexible Filtering**: Include/exclude patterns for selective documentation
|
|
2373
|
+
* - **Markdown Generation**: Converts extracted docs to organized Markdown files
|
|
2374
|
+
* - **Navigation Generation**: Auto-generates sidebar navigation metadata
|
|
2375
|
+
* - **GitHub Links**: Includes clickable links to source code on GitHub
|
|
2376
|
+
*
|
|
2377
|
+
* ## Supported JSDoc Tags
|
|
2378
|
+
*
|
|
2379
|
+
* - `@param {type} name - description` - Function parameter documentation
|
|
2380
|
+
* - `@returns {type} description` - Return value documentation
|
|
2381
|
+
* - `@example` - Code examples (multi-line blocks)
|
|
2382
|
+
* - `@private` - Mark item as private (excluded from docs if private=false)
|
|
2383
|
+
* - `@default value` - Default parameter value
|
|
2384
|
+
* - Custom tags are preserved in the `tags` field
|
|
2385
|
+
*
|
|
2386
|
+
* ## Usage Flow
|
|
2387
|
+
*
|
|
2388
|
+
* 1. Call `extractDocs()` to parse source files
|
|
2389
|
+
* 2. Call `generateMarkdown()` to create Markdown content
|
|
2390
|
+
* 3. Call `writeDocs()` to write files to output directory
|
|
2391
|
+
* 4. Generated nav.ts can be imported for sidebar navigation
|
|
2392
|
+
*
|
|
2393
|
+
* @example
|
|
2394
|
+
* ```typescript
|
|
2395
|
+
* import { extractDocs, generateMarkdown, writeDocs } from './docs';
|
|
2396
|
+
*
|
|
2397
|
+
* const docsOptions = {
|
|
2398
|
+
* enabled: true,
|
|
2399
|
+
* src: ['./src'],
|
|
2400
|
+
* out: './docs/api',
|
|
2401
|
+
* include: ['**\/*.ts'],
|
|
2402
|
+
* exclude: ['**\/*.test.ts'],
|
|
2403
|
+
* groupBy: 'file',
|
|
2404
|
+
* githubUrl: 'https://github.com/user/project',
|
|
2405
|
+
* };
|
|
2406
|
+
*
|
|
2407
|
+
* const extracted = await extractDocs(['./src'], docsOptions);
|
|
2408
|
+
* const markdown = generateMarkdown(extracted, docsOptions);
|
|
2409
|
+
* await writeDocs(markdown, './docs/api', extracted, docsOptions);
|
|
2410
|
+
* ```
|
|
2411
|
+
*/
|
|
2180
2412
|
const DEFAULT_DOCS_INCLUDE = [
|
|
2181
2413
|
"**/*.ts",
|
|
2182
2414
|
"**/*.tsx",
|
|
@@ -2317,7 +2549,7 @@ async function writeDocs(docs, outDir, extractedDocs, options) {
|
|
|
2317
2549
|
napi.writeGeneratedDocs(docs, outDir, extractedDocs ? toRustDocsModules(extractedDocs) : void 0, {
|
|
2318
2550
|
generateNav: options?.generateNav ?? false,
|
|
2319
2551
|
groupBy: options?.groupBy ?? "file",
|
|
2320
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2552
|
+
generatedAt: existingGeneratedAt(outDir) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2321
2553
|
basePath: options?.basePath,
|
|
2322
2554
|
pathStrategy: options?.pathStrategy,
|
|
2323
2555
|
groupOrder: options?.groupOrder,
|
|
@@ -2327,6 +2559,15 @@ async function writeDocs(docs, outDir, extractedDocs, options) {
|
|
|
2327
2559
|
singleEntryRoot: options?.singleEntryRoot
|
|
2328
2560
|
});
|
|
2329
2561
|
}
|
|
2562
|
+
/** Keep `docs.json`'s timestamp stable across regenerations of the same tree. */
|
|
2563
|
+
function existingGeneratedAt(outDir) {
|
|
2564
|
+
try {
|
|
2565
|
+
const parsed = JSON.parse((0, node_fs.readFileSync)(node_path.join(outDir, "docs.json"), "utf8"));
|
|
2566
|
+
return typeof parsed.generatedAt === "string" && parsed.generatedAt.length > 0 ? parsed.generatedAt : void 0;
|
|
2567
|
+
} catch {
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2330
2571
|
function toRustDocsModules(docs) {
|
|
2331
2572
|
return docs.map((doc) => ({
|
|
2332
2573
|
file: doc.file,
|
|
@@ -2767,6 +3008,45 @@ async function resolveTemplate(options, root) {
|
|
|
2767
3008
|
}
|
|
2768
3009
|
}
|
|
2769
3010
|
/**
|
|
3011
|
+
* Matches this package and every subpath it exports.
|
|
3012
|
+
*
|
|
3013
|
+
* A template's natural runtime is whatever renders it, and for the
|
|
3014
|
+
* framework-less kinds that is this package: `renderToString`, `raw`, `when`
|
|
3015
|
+
* and `each` live at its root, and the JSX runtime under `./jsx-runtime`.
|
|
3016
|
+
* Inlining them instead drags the entire plugin — chokidar, fsevents and all
|
|
3017
|
+
* — into the template bundle, which is what made importing it fail outright.
|
|
3018
|
+
*/
|
|
3019
|
+
const OX_CONTENT_PACKAGE = /^@ox-content\/vite-plugin(\/.*)?$/;
|
|
3020
|
+
/**
|
|
3021
|
+
* Whether `id` is a bare specifier, and so resolvable at runtime rather than
|
|
3022
|
+
* something the template bundle has to inline.
|
|
3023
|
+
*
|
|
3024
|
+
* Template bundles are written to `<root>/.cache/og-images/` and imported
|
|
3025
|
+
* from there, so Node resolves anything left external against the project's
|
|
3026
|
+
* own `node_modules`. Relative and absolute imports still bundle, which is
|
|
3027
|
+
* what a template actually needs — its own components travel with it.
|
|
3028
|
+
*/
|
|
3029
|
+
function isBareSpecifier(id) {
|
|
3030
|
+
if (id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return false;
|
|
3031
|
+
return !/^[a-zA-Z]:[\\/]/.test(id);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Rolldown input options for a `.ts` template bundle.
|
|
3035
|
+
*
|
|
3036
|
+
* A `.ts` template is the framework-less kind, so it has no single runtime to
|
|
3037
|
+
* externalize the way the `.vue`, `.svelte` and `.tsx` paths do — anything
|
|
3038
|
+
* from `node_modules` is better resolved at import time than inlined. Nothing
|
|
3039
|
+
* on this path has a compiler plugin, so nothing here needed bundling to be
|
|
3040
|
+
* loadable in the first place.
|
|
3041
|
+
*/
|
|
3042
|
+
function tsTemplateBundleOptions(templatePath) {
|
|
3043
|
+
return {
|
|
3044
|
+
input: templatePath,
|
|
3045
|
+
platform: "node",
|
|
3046
|
+
external: (id) => isBareSpecifier(id)
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
2770
3050
|
* Resolves a plain TypeScript template (existing behavior).
|
|
2771
3051
|
*/
|
|
2772
3052
|
async function resolveTsTemplate(templatePath, options, root) {
|
|
@@ -2775,10 +3055,7 @@ async function resolveTsTemplate(templatePath, options, root) {
|
|
|
2775
3055
|
const cacheDir = path.join(root, ".cache", "og-images");
|
|
2776
3056
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
2777
3057
|
const outfile = path.join(cacheDir, "_template.mjs");
|
|
2778
|
-
const bundle = await rolldown(
|
|
2779
|
-
input: templatePath,
|
|
2780
|
-
platform: "node"
|
|
2781
|
-
});
|
|
3058
|
+
const bundle = await rolldown(tsTemplateBundleOptions(templatePath));
|
|
2782
3059
|
await bundle.write({
|
|
2783
3060
|
file: outfile,
|
|
2784
3061
|
format: "esm"
|
|
@@ -2800,11 +3077,16 @@ async function resolveVueTemplate(templatePath, options, root) {
|
|
|
2800
3077
|
const cacheDir = path.join(root, ".cache", "og-images");
|
|
2801
3078
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
2802
3079
|
const outfile = path.join(cacheDir, "_template_vue.mjs");
|
|
3080
|
+
const plugins = options.vuePlugin === "vizejs" ? await getVizejsPlugin() : [createVueCompilerPlugin()];
|
|
2803
3081
|
const bundle = await rolldown({
|
|
2804
3082
|
input: templatePath,
|
|
2805
3083
|
platform: "node",
|
|
2806
|
-
external: [
|
|
2807
|
-
|
|
3084
|
+
external: [
|
|
3085
|
+
"vue",
|
|
3086
|
+
"vue/server-renderer",
|
|
3087
|
+
OX_CONTENT_PACKAGE
|
|
3088
|
+
],
|
|
3089
|
+
plugins
|
|
2808
3090
|
});
|
|
2809
3091
|
await bundle.write({
|
|
2810
3092
|
file: outfile,
|
|
@@ -2906,7 +3188,8 @@ async function resolveSvelteTemplate(templatePath, root) {
|
|
|
2906
3188
|
"svelte",
|
|
2907
3189
|
"svelte/server",
|
|
2908
3190
|
"svelte/internal",
|
|
2909
|
-
"svelte/internal/server"
|
|
3191
|
+
"svelte/internal/server",
|
|
3192
|
+
OX_CONTENT_PACKAGE
|
|
2910
3193
|
],
|
|
2911
3194
|
plugins: [createSvelteCompilerPlugin()]
|
|
2912
3195
|
});
|
|
@@ -2965,7 +3248,8 @@ async function resolveReactTemplate(templatePath, root) {
|
|
|
2965
3248
|
"react/jsx-runtime",
|
|
2966
3249
|
"react/jsx-dev-runtime",
|
|
2967
3250
|
"react-dom",
|
|
2968
|
-
"react-dom/server"
|
|
3251
|
+
"react-dom/server",
|
|
3252
|
+
OX_CONTENT_PACKAGE
|
|
2969
3253
|
],
|
|
2970
3254
|
transform: { jsx: "react-jsx" }
|
|
2971
3255
|
});
|
|
@@ -3297,311 +3581,676 @@ initIslands((el, props) => {
|
|
|
3297
3581
|
`;
|
|
3298
3582
|
}
|
|
3299
3583
|
//#endregion
|
|
3300
|
-
//#region src/
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3584
|
+
//#region src/page-context.ts
|
|
3585
|
+
var page_context_exports = /* @__PURE__ */ require_vitepress.__exportAll({
|
|
3586
|
+
clearRenderContext: () => clearRenderContext,
|
|
3587
|
+
generateFrontmatterTypes: () => generateFrontmatterTypes,
|
|
3588
|
+
inferType: () => inferType,
|
|
3589
|
+
setRenderContext: () => setRenderContext,
|
|
3590
|
+
useIsActive: () => useIsActive,
|
|
3591
|
+
useNav: () => useNav,
|
|
3592
|
+
usePageProps: () => usePageProps,
|
|
3593
|
+
useRenderContext: () => useRenderContext,
|
|
3594
|
+
useSiteConfig: () => useSiteConfig
|
|
3595
|
+
});
|
|
3311
3596
|
/**
|
|
3312
|
-
*
|
|
3597
|
+
* Sets the current render context.
|
|
3598
|
+
* Called internally during page rendering.
|
|
3599
|
+
* @internal
|
|
3313
3600
|
*/
|
|
3314
|
-
function
|
|
3315
|
-
|
|
3316
|
-
enabled: false,
|
|
3317
|
-
extension: ".html",
|
|
3318
|
-
clean: false,
|
|
3319
|
-
bare: false,
|
|
3320
|
-
generateOgImage: false,
|
|
3321
|
-
lastUpdated: false
|
|
3322
|
-
};
|
|
3323
|
-
if (ssg === true || ssg === void 0) return {
|
|
3324
|
-
enabled: true,
|
|
3325
|
-
extension: ".html",
|
|
3326
|
-
clean: false,
|
|
3327
|
-
bare: false,
|
|
3328
|
-
generateOgImage: false,
|
|
3329
|
-
lastUpdated: false,
|
|
3330
|
-
theme: require_vitepress.resolveTheme(void 0)
|
|
3331
|
-
};
|
|
3332
|
-
return {
|
|
3333
|
-
enabled: ssg.enabled ?? true,
|
|
3334
|
-
extension: ssg.extension ?? ".html",
|
|
3335
|
-
clean: ssg.clean ?? false,
|
|
3336
|
-
bare: ssg.bare ?? false,
|
|
3337
|
-
siteName: ssg.siteName,
|
|
3338
|
-
ogImage: ssg.ogImage,
|
|
3339
|
-
generateOgImage: ssg.generateOgImage ?? false,
|
|
3340
|
-
lastUpdated: ssg.lastUpdated ?? false,
|
|
3341
|
-
siteUrl: ssg.siteUrl,
|
|
3342
|
-
theme: require_vitepress.resolveTheme(ssg.theme),
|
|
3343
|
-
navigation: ssg.navigation
|
|
3344
|
-
};
|
|
3601
|
+
function setRenderContext(ctx) {
|
|
3602
|
+
currentContext = ctx;
|
|
3345
3603
|
}
|
|
3346
3604
|
/**
|
|
3347
|
-
*
|
|
3605
|
+
* Clears the current render context.
|
|
3606
|
+
* Called internally after page rendering.
|
|
3607
|
+
* @internal
|
|
3348
3608
|
*/
|
|
3349
|
-
function
|
|
3350
|
-
|
|
3609
|
+
function clearRenderContext() {
|
|
3610
|
+
currentContext = null;
|
|
3351
3611
|
}
|
|
3352
3612
|
/**
|
|
3353
|
-
*
|
|
3613
|
+
* Gets the current page props.
|
|
3614
|
+
*
|
|
3615
|
+
* @returns The current page props
|
|
3616
|
+
* @throws Error if called outside of a render context
|
|
3617
|
+
*
|
|
3618
|
+
* @example
|
|
3619
|
+
* ```tsx
|
|
3620
|
+
* function PageTitle() {
|
|
3621
|
+
* const page = usePageProps();
|
|
3622
|
+
* return <h1>{page.title}</h1>;
|
|
3623
|
+
* }
|
|
3624
|
+
* ```
|
|
3354
3625
|
*/
|
|
3355
|
-
function
|
|
3356
|
-
|
|
3626
|
+
function usePageProps() {
|
|
3627
|
+
if (!currentContext) throw new Error("[ox-content] usePageProps() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3628
|
+
return currentContext.page;
|
|
3357
3629
|
}
|
|
3358
3630
|
/**
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
const cached = navGroupsForRustCache.get(navGroups);
|
|
3376
|
-
if (cached) return cached;
|
|
3377
|
-
const converted = navGroups.map((group) => ({
|
|
3378
|
-
title: group.title,
|
|
3379
|
-
collapsed: group.collapsed,
|
|
3380
|
-
stickyCollapsed: group.stickyCollapsed,
|
|
3381
|
-
items: group.items.map(toRustNavItem)
|
|
3382
|
-
}));
|
|
3383
|
-
navGroupsForRustCache.set(navGroups, converted);
|
|
3384
|
-
return converted;
|
|
3631
|
+
* Gets the site configuration.
|
|
3632
|
+
*
|
|
3633
|
+
* @returns The site configuration
|
|
3634
|
+
* @throws Error if called outside of a render context
|
|
3635
|
+
*
|
|
3636
|
+
* @example
|
|
3637
|
+
* ```tsx
|
|
3638
|
+
* function SiteHeader() {
|
|
3639
|
+
* const site = useSiteConfig();
|
|
3640
|
+
* return <header>{site.name}</header>;
|
|
3641
|
+
* }
|
|
3642
|
+
* ```
|
|
3643
|
+
*/
|
|
3644
|
+
function useSiteConfig() {
|
|
3645
|
+
if (!currentContext) throw new Error("[ox-content] useSiteConfig() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3646
|
+
return currentContext.site;
|
|
3385
3647
|
}
|
|
3386
3648
|
/**
|
|
3387
|
-
*
|
|
3388
|
-
*
|
|
3389
|
-
*
|
|
3649
|
+
* Gets the full render context.
|
|
3650
|
+
*
|
|
3651
|
+
* @returns The complete render context
|
|
3652
|
+
* @throws Error if called outside of a render context
|
|
3653
|
+
*
|
|
3654
|
+
* @example
|
|
3655
|
+
* ```tsx
|
|
3656
|
+
* function Layout({ children }) {
|
|
3657
|
+
* const ctx = useRenderContext();
|
|
3658
|
+
* return (
|
|
3659
|
+
* <html>
|
|
3660
|
+
* <head><title>{ctx.page.title} - {ctx.site.name}</title></head>
|
|
3661
|
+
* <body>{children}</body>
|
|
3662
|
+
* </html>
|
|
3663
|
+
* );
|
|
3664
|
+
* }
|
|
3665
|
+
* ```
|
|
3390
3666
|
*/
|
|
3391
|
-
function
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
text: entry.text,
|
|
3395
|
-
slug: entry.slug,
|
|
3396
|
-
children: entry.children?.map(toRustTocEntry) ?? []
|
|
3397
|
-
};
|
|
3667
|
+
function useRenderContext() {
|
|
3668
|
+
if (!currentContext) throw new Error("[ox-content] useRenderContext() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3669
|
+
return currentContext;
|
|
3398
3670
|
}
|
|
3399
3671
|
/**
|
|
3400
|
-
*
|
|
3401
|
-
*
|
|
3402
|
-
*
|
|
3672
|
+
* Gets the navigation groups.
|
|
3673
|
+
*
|
|
3674
|
+
* @example
|
|
3675
|
+
* ```tsx
|
|
3676
|
+
* function Sidebar() {
|
|
3677
|
+
* const nav = useNav();
|
|
3678
|
+
* return (
|
|
3679
|
+
* <nav>
|
|
3680
|
+
* {each(nav, (group) => (
|
|
3681
|
+
* <div>
|
|
3682
|
+
* <h3>{group.title}</h3>
|
|
3683
|
+
* <ul>
|
|
3684
|
+
* {each(group.items, (item) => (
|
|
3685
|
+
* <li><a href={item.href}>{item.title}</a></li>
|
|
3686
|
+
* ))}
|
|
3687
|
+
* </ul>
|
|
3688
|
+
* </div>
|
|
3689
|
+
* ))}
|
|
3690
|
+
* </nav>
|
|
3691
|
+
* );
|
|
3692
|
+
* }
|
|
3693
|
+
* ```
|
|
3403
3694
|
*/
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
const cached = rustLocalesCache.get(locales);
|
|
3407
|
-
if (cached) return cached;
|
|
3408
|
-
const converted = locales.map((locale) => ({
|
|
3409
|
-
code: locale.code,
|
|
3410
|
-
name: locale.name,
|
|
3411
|
-
dir: locale.dir ?? "ltr"
|
|
3412
|
-
}));
|
|
3413
|
-
rustLocalesCache.set(locales, converted);
|
|
3414
|
-
return converted;
|
|
3695
|
+
function useNav() {
|
|
3696
|
+
return useSiteConfig().nav;
|
|
3415
3697
|
}
|
|
3416
3698
|
/**
|
|
3417
|
-
*
|
|
3418
|
-
*
|
|
3419
|
-
*
|
|
3699
|
+
* Checks if the given path is the current page.
|
|
3700
|
+
*
|
|
3701
|
+
* @example
|
|
3702
|
+
* ```tsx
|
|
3703
|
+
* function NavLink({ href, children }) {
|
|
3704
|
+
* const isActive = useIsActive(href);
|
|
3705
|
+
* return <a href={href} class={isActive ? 'active' : ''}>{children}</a>;
|
|
3706
|
+
* }
|
|
3707
|
+
* ```
|
|
3420
3708
|
*/
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
if (cached) return cached;
|
|
3425
|
-
const codes = locales.map((locale) => locale.code);
|
|
3426
|
-
localeCodesCache.set(locales, codes);
|
|
3427
|
-
return codes;
|
|
3709
|
+
function useIsActive(path) {
|
|
3710
|
+
const page = usePageProps();
|
|
3711
|
+
return page.path === path || page.url === path;
|
|
3428
3712
|
}
|
|
3429
3713
|
/**
|
|
3430
|
-
*
|
|
3714
|
+
* Infers TypeScript types from frontmatter values.
|
|
3431
3715
|
*/
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
alt: pageData.entryPage.hero.image.alt,
|
|
3451
|
-
width: pageData.entryPage.hero.image.width,
|
|
3452
|
-
height: pageData.entryPage.hero.image.height
|
|
3453
|
-
} : void 0,
|
|
3454
|
-
actions: pageData.entryPage.hero.actions?.map((a) => ({
|
|
3455
|
-
theme: a.theme,
|
|
3456
|
-
text: a.text,
|
|
3457
|
-
link: a.link
|
|
3458
|
-
}))
|
|
3459
|
-
} : void 0,
|
|
3460
|
-
features: pageData.entryPage.features?.map((f) => ({
|
|
3461
|
-
icon: f.icon,
|
|
3462
|
-
title: f.title,
|
|
3463
|
-
details: f.details,
|
|
3464
|
-
link: f.link,
|
|
3465
|
-
linkText: f.linkText
|
|
3466
|
-
}))
|
|
3467
|
-
} : void 0;
|
|
3468
|
-
return mod.generateSsgHtml({
|
|
3469
|
-
title: pageData.title,
|
|
3470
|
-
description: pageData.description,
|
|
3471
|
-
content: pageData.content,
|
|
3472
|
-
toc: tocForRust,
|
|
3473
|
-
lastUpdated: pageData.lastUpdated,
|
|
3474
|
-
path: pageData.path,
|
|
3475
|
-
entryPage: entryPageForRust
|
|
3476
|
-
}, navGroupsForRust, {
|
|
3477
|
-
siteName,
|
|
3478
|
-
base,
|
|
3479
|
-
ogImage,
|
|
3480
|
-
theme: themeForRust,
|
|
3481
|
-
locale,
|
|
3482
|
-
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0
|
|
3483
|
-
});
|
|
3484
|
-
}
|
|
3485
|
-
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
3486
|
-
const optimized = (await require_vitepress.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
3487
|
-
await Promise.all(optimized.assets.map(async (asset) => {
|
|
3488
|
-
await fs_promises.mkdir(path.dirname(asset.outputPath), { recursive: true });
|
|
3489
|
-
await fs_promises.writeFile(asset.outputPath, asset.content, "utf-8");
|
|
3490
|
-
}));
|
|
3491
|
-
return {
|
|
3492
|
-
pages: optimized.pages,
|
|
3493
|
-
assets: optimized.assets.map((asset) => asset.outputPath)
|
|
3494
|
-
};
|
|
3716
|
+
function inferType(value) {
|
|
3717
|
+
if (value === null) return "null";
|
|
3718
|
+
if (value === void 0) return "undefined";
|
|
3719
|
+
if (typeof value === "string") return "string";
|
|
3720
|
+
if (typeof value === "number") return "number";
|
|
3721
|
+
if (typeof value === "boolean") return "boolean";
|
|
3722
|
+
if (Array.isArray(value)) {
|
|
3723
|
+
if (value.length === 0) return "unknown[]";
|
|
3724
|
+
const itemTypes = [...new Set(value.map(inferType))];
|
|
3725
|
+
if (itemTypes.length === 1) return `${itemTypes[0]}[]`;
|
|
3726
|
+
return `(${itemTypes.join(" | ")})[]`;
|
|
3727
|
+
}
|
|
3728
|
+
if (typeof value === "object") {
|
|
3729
|
+
const entries = Object.entries(value);
|
|
3730
|
+
if (entries.length === 0) return "Record<string, unknown>";
|
|
3731
|
+
return `{ ${entries.map(([k, v]) => `${k}: ${inferType(v)}`).join("; ")} }`;
|
|
3732
|
+
}
|
|
3733
|
+
return "unknown";
|
|
3495
3734
|
}
|
|
3496
3735
|
/**
|
|
3497
|
-
*
|
|
3736
|
+
* Generates TypeScript interface from frontmatter samples.
|
|
3498
3737
|
*/
|
|
3499
|
-
function
|
|
3500
|
-
|
|
3738
|
+
function generateFrontmatterTypes(samples, interfaceName = "PageFrontmatter") {
|
|
3739
|
+
const fields = /* @__PURE__ */ new Map();
|
|
3740
|
+
for (const sample of samples) for (const [key, value] of Object.entries(sample)) {
|
|
3741
|
+
const existing = fields.get(key) ?? {
|
|
3742
|
+
types: /* @__PURE__ */ new Set(),
|
|
3743
|
+
count: 0
|
|
3744
|
+
};
|
|
3745
|
+
existing.types.add(inferType(value));
|
|
3746
|
+
existing.count++;
|
|
3747
|
+
fields.set(key, existing);
|
|
3748
|
+
}
|
|
3749
|
+
const lines = [
|
|
3750
|
+
"/**",
|
|
3751
|
+
" * Auto-generated frontmatter type based on your pages.",
|
|
3752
|
+
" * DO NOT EDIT - this file is generated by ox-content.",
|
|
3753
|
+
" */",
|
|
3754
|
+
"",
|
|
3755
|
+
`export interface ${interfaceName} {`
|
|
3756
|
+
];
|
|
3757
|
+
for (const [name, { types, count }] of fields) {
|
|
3758
|
+
const isOptional = count < samples.length;
|
|
3759
|
+
const typeStr = [...types].join(" | ");
|
|
3760
|
+
const optionalMark = isOptional ? "?" : "";
|
|
3761
|
+
lines.push(` ${name}${optionalMark}: ${typeStr};`);
|
|
3762
|
+
}
|
|
3763
|
+
lines.push("}");
|
|
3764
|
+
lines.push("");
|
|
3765
|
+
lines.push(`export type PageProps = import('@ox-content/vite-plugin').PageProps<${interfaceName}>;`);
|
|
3766
|
+
lines.push("");
|
|
3767
|
+
return lines.join("\n");
|
|
3501
3768
|
}
|
|
3769
|
+
var currentContext;
|
|
3770
|
+
var init_page_context = require_vitepress.__esmMin((() => {
|
|
3771
|
+
currentContext = null;
|
|
3772
|
+
}));
|
|
3773
|
+
//#endregion
|
|
3774
|
+
//#region src/theme-renderer.ts
|
|
3502
3775
|
/**
|
|
3503
|
-
*
|
|
3776
|
+
* Theme Renderer for Static HTML Generation
|
|
3777
|
+
*
|
|
3778
|
+
* Renders JSX theme components to static HTML strings.
|
|
3779
|
+
* No client-side JavaScript is included by default.
|
|
3504
3780
|
*/
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
function
|
|
3514
|
-
|
|
3781
|
+
init_page_context();
|
|
3782
|
+
/**
|
|
3783
|
+
* Renders a page using the theme component.
|
|
3784
|
+
*
|
|
3785
|
+
* @param page - Page data to render
|
|
3786
|
+
* @param options - Theme render options
|
|
3787
|
+
* @returns Rendered HTML string
|
|
3788
|
+
*/
|
|
3789
|
+
function renderPage(page, options) {
|
|
3790
|
+
const { theme, siteName, base, nav, pages } = options;
|
|
3791
|
+
setRenderContext({
|
|
3792
|
+
page: {
|
|
3793
|
+
title: page.title,
|
|
3794
|
+
description: page.description,
|
|
3795
|
+
html: page.html,
|
|
3796
|
+
toc: page.toc,
|
|
3797
|
+
lastUpdated: page.lastUpdated,
|
|
3798
|
+
path: page.path,
|
|
3799
|
+
url: page.url,
|
|
3800
|
+
frontmatter: page.frontmatter,
|
|
3801
|
+
layout: page.layout
|
|
3802
|
+
},
|
|
3803
|
+
site: {
|
|
3804
|
+
name: siteName,
|
|
3805
|
+
base,
|
|
3806
|
+
nav,
|
|
3807
|
+
pages: pages.map((p) => ({
|
|
3808
|
+
title: p.title,
|
|
3809
|
+
description: p.description,
|
|
3810
|
+
html: p.html,
|
|
3811
|
+
toc: p.toc,
|
|
3812
|
+
lastUpdated: p.lastUpdated,
|
|
3813
|
+
path: p.path,
|
|
3814
|
+
url: p.url,
|
|
3815
|
+
frontmatter: p.frontmatter,
|
|
3816
|
+
layout: p.layout
|
|
3817
|
+
}))
|
|
3818
|
+
}
|
|
3819
|
+
});
|
|
3820
|
+
try {
|
|
3821
|
+
const result = theme({ children: require_jsx_html.raw(page.html) });
|
|
3822
|
+
const html = require_jsx_html.renderToString(result);
|
|
3823
|
+
if (!html.trimStart().toLowerCase().startsWith("<!doctype")) return `<!DOCTYPE html>\n${html}`;
|
|
3824
|
+
return html;
|
|
3825
|
+
} finally {
|
|
3826
|
+
clearRenderContext();
|
|
3827
|
+
}
|
|
3515
3828
|
}
|
|
3516
3829
|
/**
|
|
3517
|
-
*
|
|
3830
|
+
* Renders all pages and generates type definitions.
|
|
3831
|
+
*
|
|
3832
|
+
* @param pages - All pages to render
|
|
3833
|
+
* @param options - Theme render options
|
|
3834
|
+
* @returns Map of output paths to rendered HTML
|
|
3518
3835
|
*/
|
|
3519
|
-
function
|
|
3520
|
-
|
|
3836
|
+
async function renderAllPages(pages, options) {
|
|
3837
|
+
const results = /* @__PURE__ */ new Map();
|
|
3838
|
+
for (const page of pages) {
|
|
3839
|
+
const html = renderPage(page, {
|
|
3840
|
+
...options,
|
|
3841
|
+
pages
|
|
3842
|
+
});
|
|
3843
|
+
results.set(page.url, html);
|
|
3844
|
+
}
|
|
3845
|
+
if (options.typesOutDir) await generateTypes(pages, options.typesOutDir);
|
|
3846
|
+
return results;
|
|
3521
3847
|
}
|
|
3522
3848
|
/**
|
|
3523
|
-
*
|
|
3849
|
+
* Generates TypeScript type definitions from page frontmatter.
|
|
3850
|
+
*
|
|
3851
|
+
* @param pages - All pages
|
|
3852
|
+
* @param outDir - Output directory for types
|
|
3524
3853
|
*/
|
|
3525
|
-
async function
|
|
3526
|
-
|
|
3854
|
+
async function generateTypes(pages, outDir) {
|
|
3855
|
+
const types = generateFrontmatterTypes(pages.map((p) => p.frontmatter));
|
|
3856
|
+
const typesPath = (0, node_path.join)(outDir, "page-props.d.ts");
|
|
3857
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(typesPath), { recursive: true });
|
|
3858
|
+
await (0, node_fs_promises.writeFile)(typesPath, types, "utf-8");
|
|
3527
3859
|
}
|
|
3528
3860
|
/**
|
|
3529
|
-
*
|
|
3861
|
+
* Default theme component.
|
|
3862
|
+
* A minimal theme that renders page content with basic styling.
|
|
3530
3863
|
*/
|
|
3531
|
-
function
|
|
3532
|
-
|
|
3864
|
+
function DefaultTheme({ children }) {
|
|
3865
|
+
const { usePageProps, useSiteConfig } = (init_page_context(), require_vitepress.__toCommonJS(page_context_exports));
|
|
3866
|
+
const page = usePageProps();
|
|
3867
|
+
const site = useSiteConfig();
|
|
3868
|
+
return { __html: `<!DOCTYPE html>
|
|
3869
|
+
<html lang="en">
|
|
3870
|
+
<head>
|
|
3871
|
+
<meta charset="UTF-8">
|
|
3872
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3873
|
+
<title>${escapeHtml(page.title)} - ${escapeHtml(site.name)}</title>
|
|
3874
|
+
${page.description ? `<meta name="description" content="${escapeHtml(page.description)}">` : ""}
|
|
3875
|
+
<style>
|
|
3876
|
+
:root {
|
|
3877
|
+
--octc-color-primary: #4f6fae;
|
|
3878
|
+
--octc-color-text: #131a30;
|
|
3879
|
+
--octc-color-bg: #ffffff;
|
|
3880
|
+
--octc-color-bg-alt: #f5f7fb;
|
|
3881
|
+
--octc-color-text-muted: #4f607b;
|
|
3882
|
+
--octc-color-border: #d2dbea;
|
|
3883
|
+
}
|
|
3884
|
+
body {
|
|
3885
|
+
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
3886
|
+
line-height: 1.7;
|
|
3887
|
+
color: var(--octc-color-text);
|
|
3888
|
+
background: var(--octc-color-bg);
|
|
3889
|
+
max-width: 800px;
|
|
3890
|
+
margin: 0 auto;
|
|
3891
|
+
padding: 2rem;
|
|
3892
|
+
}
|
|
3893
|
+
a { color: var(--octc-color-primary); }
|
|
3894
|
+
</style>
|
|
3895
|
+
</head>
|
|
3896
|
+
<body>
|
|
3897
|
+
<header>
|
|
3898
|
+
<h1>${escapeHtml(site.name)}</h1>
|
|
3899
|
+
</header>
|
|
3900
|
+
<main>
|
|
3901
|
+
${children.__html}
|
|
3902
|
+
</main>
|
|
3903
|
+
</body>
|
|
3904
|
+
</html>` };
|
|
3905
|
+
}
|
|
3906
|
+
function escapeHtml(str) {
|
|
3907
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3533
3908
|
}
|
|
3534
3909
|
/**
|
|
3535
|
-
*
|
|
3910
|
+
* Creates a theme with layout switching support.
|
|
3911
|
+
*
|
|
3912
|
+
* @example
|
|
3913
|
+
* ```tsx
|
|
3914
|
+
* import { createTheme } from '@ox-content/vite-plugin';
|
|
3915
|
+
* import { DefaultLayout } from './layouts/Default';
|
|
3916
|
+
* import { EntryLayout } from './layouts/Entry';
|
|
3917
|
+
*
|
|
3918
|
+
* export default createTheme({
|
|
3919
|
+
* layouts: {
|
|
3920
|
+
* default: DefaultLayout,
|
|
3921
|
+
* entry: EntryLayout,
|
|
3922
|
+
* },
|
|
3923
|
+
* });
|
|
3924
|
+
* ```
|
|
3536
3925
|
*/
|
|
3537
|
-
function
|
|
3538
|
-
|
|
3926
|
+
function createTheme(config) {
|
|
3927
|
+
const { layouts, defaultLayout = "default" } = config;
|
|
3928
|
+
return function ThemeWithLayouts({ children }) {
|
|
3929
|
+
const layoutName = usePageProps().layout ?? defaultLayout;
|
|
3930
|
+
const Layout = layouts[layoutName] ?? layouts[defaultLayout];
|
|
3931
|
+
if (!Layout) throw new Error(`[ox-content] Layout "${layoutName}" not found. Available layouts: ${Object.keys(layouts).join(", ")}`);
|
|
3932
|
+
return Layout({ children });
|
|
3933
|
+
};
|
|
3539
3934
|
}
|
|
3935
|
+
//#endregion
|
|
3936
|
+
//#region src/ssg.ts
|
|
3540
3937
|
/**
|
|
3541
|
-
*
|
|
3938
|
+
* SSG (Static Site Generation) module for ox-content
|
|
3542
3939
|
*/
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3940
|
+
/**
|
|
3941
|
+
* Deprecated compatibility export for consumers that imported the former
|
|
3942
|
+
* TypeScript SSG template. HTML generation is Rust-backed now.
|
|
3943
|
+
*
|
|
3944
|
+
* @deprecated Use `generateHtmlPage`/`buildSsg` instead.
|
|
3945
|
+
*/
|
|
3946
|
+
const DEFAULT_HTML_TEMPLATE = "<!-- ox-content default HTML template is Rust-backed -->";
|
|
3947
|
+
/**
|
|
3948
|
+
* Resolves SSG options with defaults.
|
|
3949
|
+
*/
|
|
3950
|
+
function resolveSsgOptions(ssg) {
|
|
3951
|
+
if (ssg === false) return {
|
|
3952
|
+
enabled: false,
|
|
3953
|
+
extension: ".html",
|
|
3954
|
+
clean: false,
|
|
3955
|
+
bare: false,
|
|
3956
|
+
generateOgImage: false,
|
|
3957
|
+
lastUpdated: false
|
|
3548
3958
|
};
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
errors.push(...collected.errors);
|
|
3558
|
-
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
3559
|
-
await writeGeneratedPages(await generateHtmlPages(context, collected.pageResults, collected, errors), context, generatedFiles);
|
|
3560
|
-
return {
|
|
3561
|
-
files: generatedFiles,
|
|
3562
|
-
errors
|
|
3959
|
+
if (ssg === true || ssg === void 0) return {
|
|
3960
|
+
enabled: true,
|
|
3961
|
+
extension: ".html",
|
|
3962
|
+
clean: false,
|
|
3963
|
+
bare: false,
|
|
3964
|
+
generateOgImage: false,
|
|
3965
|
+
lastUpdated: false,
|
|
3966
|
+
theme: require_vitepress.resolveTheme(void 0)
|
|
3563
3967
|
};
|
|
3564
|
-
}
|
|
3565
|
-
async function cleanOutputDirectory(ssgOptions, outDir) {
|
|
3566
|
-
if (!ssgOptions.clean) return;
|
|
3567
|
-
try {
|
|
3568
|
-
await fs_promises.rm(outDir, {
|
|
3569
|
-
recursive: true,
|
|
3570
|
-
force: true
|
|
3571
|
-
});
|
|
3572
|
-
} catch {}
|
|
3573
|
-
}
|
|
3574
|
-
async function createBuildSsgContext(options, root, srcDir, outDir, markdownFiles) {
|
|
3575
|
-
const ssgOptions = options.ssg;
|
|
3576
|
-
const base = options.base.endsWith("/") ? options.base : options.base + "/";
|
|
3577
3968
|
return {
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3969
|
+
enabled: ssg.enabled ?? true,
|
|
3970
|
+
extension: ssg.extension ?? ".html",
|
|
3971
|
+
clean: ssg.clean ?? false,
|
|
3972
|
+
bare: ssg.bare ?? false,
|
|
3973
|
+
render: ssg.render,
|
|
3974
|
+
lang: ssg.lang,
|
|
3975
|
+
head: ssg.head,
|
|
3976
|
+
bodyStart: ssg.bodyStart,
|
|
3977
|
+
bodyEnd: ssg.bodyEnd,
|
|
3978
|
+
siteName: ssg.siteName,
|
|
3979
|
+
ogImage: ssg.ogImage,
|
|
3980
|
+
generateOgImage: ssg.generateOgImage ?? false,
|
|
3981
|
+
lastUpdated: ssg.lastUpdated ?? false,
|
|
3982
|
+
siteUrl: ssg.siteUrl,
|
|
3983
|
+
theme: require_vitepress.resolveTheme(ssg.theme),
|
|
3984
|
+
navigation: ssg.navigation
|
|
3588
3985
|
};
|
|
3589
3986
|
}
|
|
3590
3987
|
/**
|
|
3591
|
-
*
|
|
3988
|
+
* Extracts title from content or frontmatter.
|
|
3989
|
+
*/
|
|
3990
|
+
function extractTitle$1(content, frontmatter) {
|
|
3991
|
+
return require_vitepress.importNapiModuleSync().extractSsgTitle(content, typeof frontmatter.title === "string" ? frontmatter.title : void 0);
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Generates a bare HTML page carrying head metadata and injected markup.
|
|
3592
3995
|
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
*
|
|
3597
|
-
*
|
|
3996
|
+
* Bare mode leaves the shell to the consumer, but the metadata here is
|
|
3997
|
+
* already computed for the themed page and cannot be recovered afterwards —
|
|
3998
|
+
* the generated OG image in particular was only discoverable by guessing at
|
|
3999
|
+
* the output directory. A page with none of it set renders exactly what bare
|
|
4000
|
+
* mode emitted before, which keeps the no-JS size baseline honest.
|
|
3598
4001
|
*/
|
|
3599
|
-
function
|
|
3600
|
-
return
|
|
4002
|
+
function generateBarePage(page) {
|
|
4003
|
+
return require_vitepress.importNapiModuleSync().generateSsgBarePage(page);
|
|
3601
4004
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
4005
|
+
/**
|
|
4006
|
+
* Per-build cache for the Rust-facing nav conversion. `navGroups` is the same
|
|
4007
|
+
* `context.navItems` reference for every page in a build, so the deep recursive
|
|
4008
|
+
* copy below only needs to run once per build instead of once per page.
|
|
4009
|
+
*/
|
|
4010
|
+
const navGroupsForRustCache = /* @__PURE__ */ new WeakMap();
|
|
4011
|
+
function toRustNavItem(item) {
|
|
4012
|
+
return {
|
|
4013
|
+
title: item.title,
|
|
4014
|
+
path: item.path,
|
|
4015
|
+
href: item.href,
|
|
4016
|
+
children: item.children?.map(toRustNavItem),
|
|
4017
|
+
collapsed: item.collapsed,
|
|
4018
|
+
stickyCollapsed: item.stickyCollapsed
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
function convertNavGroupsForRust(navGroups) {
|
|
4022
|
+
const cached = navGroupsForRustCache.get(navGroups);
|
|
4023
|
+
if (cached) return cached;
|
|
4024
|
+
const converted = navGroups.map((group) => ({
|
|
4025
|
+
title: group.title,
|
|
4026
|
+
collapsed: group.collapsed,
|
|
4027
|
+
stickyCollapsed: group.stickyCollapsed,
|
|
4028
|
+
items: group.items.map(toRustNavItem)
|
|
4029
|
+
}));
|
|
4030
|
+
navGroupsForRustCache.set(navGroups, converted);
|
|
4031
|
+
return converted;
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* Converts a `TocEntry` tree into the plain shape the Rust binding expects.
|
|
4035
|
+
* Hoisted to module scope so it isn't reallocated for every page; the
|
|
4036
|
+
* per-page `.map` over `pageData.toc` still runs since the TOC is page-specific.
|
|
4037
|
+
*/
|
|
4038
|
+
function toRustTocEntry(entry) {
|
|
4039
|
+
return {
|
|
4040
|
+
depth: entry.depth,
|
|
4041
|
+
text: entry.text,
|
|
4042
|
+
slug: entry.slug,
|
|
4043
|
+
children: entry.children?.map(toRustTocEntry) ?? []
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* Per-build cache for the Rust-facing locale list. `i18n.locales` is the same
|
|
4048
|
+
* reference for every page in a build, so this mapping (and the `?? "ltr"`
|
|
4049
|
+
* default) only runs once per build instead of once per page.
|
|
4050
|
+
*/
|
|
4051
|
+
const rustLocalesCache = /* @__PURE__ */ new WeakMap();
|
|
4052
|
+
function toRustLocales(locales) {
|
|
4053
|
+
const cached = rustLocalesCache.get(locales);
|
|
4054
|
+
if (cached) return cached;
|
|
4055
|
+
const converted = locales.map((locale) => ({
|
|
4056
|
+
code: locale.code,
|
|
4057
|
+
name: locale.name,
|
|
4058
|
+
dir: locale.dir ?? "ltr"
|
|
4059
|
+
}));
|
|
4060
|
+
rustLocalesCache.set(locales, converted);
|
|
4061
|
+
return converted;
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Per-build cache for the locale-code list passed to `getSsgPageLocale`. The
|
|
4065
|
+
* `i18n.locales` reference is stable across a build, so the `.map` to codes
|
|
4066
|
+
* runs once instead of once per page.
|
|
4067
|
+
*/
|
|
4068
|
+
const localeCodesCache = /* @__PURE__ */ new WeakMap();
|
|
4069
|
+
function localeCodesFor(locales) {
|
|
4070
|
+
const cached = localeCodesCache.get(locales);
|
|
4071
|
+
if (cached) return cached;
|
|
4072
|
+
const codes = locales.map((locale) => locale.code);
|
|
4073
|
+
localeCodesCache.set(locales, codes);
|
|
4074
|
+
return codes;
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Generates HTML page with navigation using Rust NAPI bindings.
|
|
4078
|
+
*/
|
|
4079
|
+
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales) {
|
|
4080
|
+
const mod = await require_vitepress.importNapiModule();
|
|
4081
|
+
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
4082
|
+
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
4083
|
+
const themeForRust = theme ? require_vitepress.themeToNapi(theme) : void 0;
|
|
4084
|
+
const entryPageForRust = pageData.entryPage ? {
|
|
4085
|
+
hero: pageData.entryPage.hero ? {
|
|
4086
|
+
name: pageData.entryPage.hero.name,
|
|
4087
|
+
text: pageData.entryPage.hero.text,
|
|
4088
|
+
tagline: pageData.entryPage.hero.tagline,
|
|
4089
|
+
notice: pageData.entryPage.hero.notice ? {
|
|
4090
|
+
title: pageData.entryPage.hero.notice.title,
|
|
4091
|
+
body: pageData.entryPage.hero.notice.body
|
|
4092
|
+
} : void 0,
|
|
4093
|
+
image: pageData.entryPage.hero.image ? {
|
|
4094
|
+
src: pageData.entryPage.hero.image.src,
|
|
4095
|
+
lightSrc: pageData.entryPage.hero.image.lightSrc,
|
|
4096
|
+
darkSrc: pageData.entryPage.hero.image.darkSrc,
|
|
4097
|
+
alt: pageData.entryPage.hero.image.alt,
|
|
4098
|
+
width: pageData.entryPage.hero.image.width,
|
|
4099
|
+
height: pageData.entryPage.hero.image.height
|
|
4100
|
+
} : void 0,
|
|
4101
|
+
actions: pageData.entryPage.hero.actions?.map((a) => ({
|
|
4102
|
+
theme: a.theme,
|
|
4103
|
+
text: a.text,
|
|
4104
|
+
link: a.link
|
|
4105
|
+
}))
|
|
4106
|
+
} : void 0,
|
|
4107
|
+
features: pageData.entryPage.features?.map((f) => ({
|
|
4108
|
+
icon: f.icon,
|
|
4109
|
+
title: f.title,
|
|
4110
|
+
details: f.details,
|
|
4111
|
+
link: f.link,
|
|
4112
|
+
linkText: f.linkText
|
|
4113
|
+
}))
|
|
4114
|
+
} : void 0;
|
|
4115
|
+
return mod.generateSsgHtml({
|
|
4116
|
+
title: pageData.title,
|
|
4117
|
+
description: pageData.description,
|
|
4118
|
+
content: pageData.content,
|
|
4119
|
+
toc: tocForRust,
|
|
4120
|
+
lastUpdated: pageData.lastUpdated,
|
|
4121
|
+
path: pageData.path,
|
|
4122
|
+
entryPage: entryPageForRust
|
|
4123
|
+
}, navGroupsForRust, {
|
|
4124
|
+
siteName,
|
|
4125
|
+
base,
|
|
4126
|
+
ogImage,
|
|
4127
|
+
theme: themeForRust,
|
|
4128
|
+
locale,
|
|
4129
|
+
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
4133
|
+
const optimized = (await require_vitepress.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
4134
|
+
await Promise.all(optimized.assets.map(async (asset) => {
|
|
4135
|
+
await fs_promises.mkdir(path.dirname(asset.outputPath), { recursive: true });
|
|
4136
|
+
await fs_promises.writeFile(asset.outputPath, asset.content, "utf-8");
|
|
4137
|
+
}));
|
|
4138
|
+
return {
|
|
4139
|
+
pages: optimized.pages,
|
|
4140
|
+
assets: optimized.assets.map((asset) => asset.outputPath)
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Converts a markdown file path to a relative URL path.
|
|
4145
|
+
*/
|
|
4146
|
+
function getUrlPath$1(inputPath, srcDir) {
|
|
4147
|
+
return require_vitepress.importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* Resolves manual navigation config to the format used by the built-in SSG renderer.
|
|
4151
|
+
*/
|
|
4152
|
+
function resolveNavigationGroups(navigation, base, extension) {
|
|
4153
|
+
if (!navigation) return;
|
|
4154
|
+
return require_vitepress.importNapiModuleSync().resolveSsgNavigationGroups(navigation, base, extension);
|
|
4155
|
+
}
|
|
4156
|
+
function getPageLocale(urlPath, i18n) {
|
|
4157
|
+
if (!i18n) return void 0;
|
|
4158
|
+
return require_vitepress.importNapiModuleSync().getSsgPageLocale(urlPath, i18n.defaultLocale, localeCodesFor(i18n.locales)) ?? void 0;
|
|
4159
|
+
}
|
|
4160
|
+
function getRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl) {
|
|
4161
|
+
return require_vitepress.importNapiModuleSync().resolveSsgRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl);
|
|
4162
|
+
}
|
|
4163
|
+
/**
|
|
4164
|
+
* Formats a file/dir name as a title.
|
|
4165
|
+
*/
|
|
4166
|
+
function formatTitle(name) {
|
|
4167
|
+
return require_vitepress.importNapiModuleSync().formatSsgTitle(name);
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Collects all markdown files from the source directory.
|
|
4171
|
+
*/
|
|
4172
|
+
async function collectMarkdownFiles(srcDir, extensions = DEFAULT_MARKDOWN_EXTENSIONS) {
|
|
4173
|
+
return require_vitepress.importNapiModuleSync().collectSsgMarkdownFiles(srcDir, [...extensions]);
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Builds navigation items from markdown files, grouped by directory.
|
|
4177
|
+
*/
|
|
4178
|
+
function buildNavItems(markdownFiles, srcDir, base, extension) {
|
|
4179
|
+
return require_vitepress.importNapiModuleSync().buildSsgNavItems(markdownFiles, srcDir, base, extension);
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Builds navigation items from an explicit theme sidebar tree.
|
|
4183
|
+
*/
|
|
4184
|
+
function buildThemeNavItems(sidebar, base, extension) {
|
|
4185
|
+
return require_vitepress.importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* Builds all markdown files to static HTML.
|
|
4189
|
+
*/
|
|
4190
|
+
async function buildSsg(options, root) {
|
|
4191
|
+
const ssgOptions = options.ssg;
|
|
4192
|
+
if (!ssgOptions.enabled) return {
|
|
4193
|
+
files: [],
|
|
4194
|
+
errors: [],
|
|
4195
|
+
ogImages: {}
|
|
4196
|
+
};
|
|
4197
|
+
const srcDir = path.resolve(root, options.srcDir);
|
|
4198
|
+
const outDir = path.resolve(root, options.outDir);
|
|
4199
|
+
const generatedFiles = [];
|
|
4200
|
+
const errors = [];
|
|
4201
|
+
await cleanOutputDirectory(ssgOptions, outDir);
|
|
4202
|
+
const markdownFiles = await collectMarkdownFiles(srcDir, options.extensions);
|
|
4203
|
+
const context = await createBuildSsgContext(options, root, srcDir, outDir, markdownFiles);
|
|
4204
|
+
const collected = await collectPageResults(context, markdownFiles);
|
|
4205
|
+
errors.push(...collected.errors);
|
|
4206
|
+
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
4207
|
+
await writeGeneratedPages(await generateHtmlPages(context, collected.pageResults, collected, errors), context, generatedFiles);
|
|
4208
|
+
return {
|
|
4209
|
+
files: generatedFiles,
|
|
4210
|
+
errors,
|
|
4211
|
+
ogImages: Object.fromEntries(collected.ogImageUrlMap)
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
async function cleanOutputDirectory(ssgOptions, outDir) {
|
|
4215
|
+
if (!ssgOptions.clean) return;
|
|
4216
|
+
try {
|
|
4217
|
+
await fs_promises.rm(outDir, {
|
|
4218
|
+
recursive: true,
|
|
4219
|
+
force: true
|
|
4220
|
+
});
|
|
4221
|
+
} catch {}
|
|
4222
|
+
}
|
|
4223
|
+
async function createBuildSsgContext(options, root, srcDir, outDir, markdownFiles) {
|
|
4224
|
+
const ssgOptions = options.ssg;
|
|
4225
|
+
const base = options.base.endsWith("/") ? options.base : options.base + "/";
|
|
4226
|
+
return {
|
|
4227
|
+
options,
|
|
4228
|
+
ssgOptions,
|
|
4229
|
+
root,
|
|
4230
|
+
srcDir,
|
|
4231
|
+
outDir,
|
|
4232
|
+
base,
|
|
4233
|
+
navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
|
|
4234
|
+
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
4235
|
+
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
4236
|
+
napi: ssgOptions.lastUpdated ? await require_vitepress.importNapiModule() : void 0
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* Whether this build emits one Open Graph image per page.
|
|
4241
|
+
*
|
|
4242
|
+
* `ssg.bare` deliberately does not turn this off. Bare mode only drops the
|
|
4243
|
+
* generated page shell, and bringing your own shell is exactly the case where
|
|
4244
|
+
* per-page OG images are still wanted — the images are written to the output
|
|
4245
|
+
* tree and the consumer injects the `<meta>` tags itself. Nothing in the bare
|
|
4246
|
+
* HTML references them, because bare output has no `<head>` to put them in.
|
|
4247
|
+
*/
|
|
4248
|
+
function shouldGenerateOgImages(options) {
|
|
4249
|
+
return options.ogImage || options.ssg.generateOgImage;
|
|
4250
|
+
}
|
|
4251
|
+
async function resolveSiteName$1(root, ssgOptions) {
|
|
4252
|
+
if (ssgOptions.siteName) return ssgOptions.siteName;
|
|
4253
|
+
try {
|
|
3605
4254
|
const pkgPath = path.join(root, "package.json");
|
|
3606
4255
|
const pkg = JSON.parse(await fs_promises.readFile(pkgPath, "utf-8"));
|
|
3607
4256
|
return pkg.name ? formatTitle(pkg.name) : "Documentation";
|
|
@@ -3721,7 +4370,7 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
3721
4370
|
generatedPages.push({
|
|
3722
4371
|
inputPath: pageResult.inputPath,
|
|
3723
4372
|
outputPath: pageResult.routePaths.outputPath,
|
|
3724
|
-
html: await renderSsgPage(context, pageResult, collected
|
|
4373
|
+
html: await renderSsgPage(context, pageResult, collected, pageResults)
|
|
3725
4374
|
});
|
|
3726
4375
|
} catch (err) {
|
|
3727
4376
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -3729,16 +4378,61 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
3729
4378
|
}
|
|
3730
4379
|
return generatedPages;
|
|
3731
4380
|
}
|
|
3732
|
-
async function renderSsgPage(context, pageResult,
|
|
3733
|
-
|
|
3734
|
-
const pageData = createSsgPageData(pageResult);
|
|
4381
|
+
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
4382
|
+
const { ogImageUrlMap } = collected;
|
|
3735
4383
|
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
4384
|
+
if (context.ssgOptions.render) return renderPage(toThemePageData(pageResult), {
|
|
4385
|
+
theme: context.ssgOptions.render,
|
|
4386
|
+
siteName: context.siteName,
|
|
4387
|
+
base: context.base,
|
|
4388
|
+
nav: context.navItems,
|
|
4389
|
+
pages: allPageResults.map(toThemePageData)
|
|
4390
|
+
});
|
|
4391
|
+
if (context.ssgOptions.bare) return generateBarePage({
|
|
4392
|
+
title: pageResult.title,
|
|
4393
|
+
content: pageResult.transformedHtml,
|
|
4394
|
+
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
4395
|
+
description: pageResult.description,
|
|
4396
|
+
canonicalUrl: canonicalPageUrl(context, pageResult.routePaths.urlPath),
|
|
4397
|
+
siteName: context.ssgOptions.siteName,
|
|
4398
|
+
ogImage: pageOgImage,
|
|
4399
|
+
head: context.ssgOptions.head,
|
|
4400
|
+
bodyStart: context.ssgOptions.bodyStart,
|
|
4401
|
+
bodyEnd: context.ssgOptions.bodyEnd
|
|
4402
|
+
});
|
|
4403
|
+
const pageData = createSsgPageData(pageResult);
|
|
3736
4404
|
return generateHtmlPage(pageData, context.navItems, context.siteName, context.base, pageOgImage, context.ssgOptions.theme, getPageLocale(pageData.path, context.options.i18n), context.options.i18n ? context.options.i18n.locales : void 0);
|
|
3737
4405
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4406
|
+
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
4407
|
+
function toThemePageData(pageResult) {
|
|
4408
|
+
return {
|
|
4409
|
+
title: pageResult.title,
|
|
4410
|
+
description: pageResult.description,
|
|
4411
|
+
html: pageResult.transformedHtml,
|
|
4412
|
+
toc: pageResult.toc,
|
|
4413
|
+
lastUpdated: pageResult.lastUpdated,
|
|
4414
|
+
path: pageResult.inputPath,
|
|
4415
|
+
url: pageResult.routePaths.href,
|
|
4416
|
+
frontmatter: pageResult.frontmatter,
|
|
4417
|
+
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Absolute URL of a page, or `undefined` when `ssg.siteUrl` is not set.
|
|
4422
|
+
*
|
|
4423
|
+
* Built the same way `get_og_image_url` builds the image URL next to it, so
|
|
4424
|
+
* the canonical link and `og:image` always agree about where the page lives.
|
|
4425
|
+
*/
|
|
4426
|
+
function canonicalPageUrl(context, urlPath) {
|
|
4427
|
+
const siteUrl = context.ssgOptions.siteUrl?.replace(/\/+$/, "");
|
|
4428
|
+
if (!siteUrl) return;
|
|
4429
|
+
if (urlPath === "/" || urlPath === "") return `${siteUrl}${context.base}`;
|
|
4430
|
+
return `${siteUrl}${context.base}${urlPath}/`;
|
|
4431
|
+
}
|
|
4432
|
+
function createSsgPageData(pageResult) {
|
|
4433
|
+
const { frontmatter } = pageResult;
|
|
4434
|
+
const entryPage = frontmatter.layout === "entry" ? {
|
|
4435
|
+
hero: frontmatter.hero,
|
|
3742
4436
|
features: frontmatter.features
|
|
3743
4437
|
} : void 0;
|
|
3744
4438
|
return {
|
|
@@ -5753,359 +6447,6 @@ function createEmptyLintResult() {
|
|
|
5753
6447
|
};
|
|
5754
6448
|
}
|
|
5755
6449
|
//#endregion
|
|
5756
|
-
//#region src/page-context.ts
|
|
5757
|
-
var page_context_exports = /* @__PURE__ */ require_vitepress.__exportAll({
|
|
5758
|
-
clearRenderContext: () => clearRenderContext,
|
|
5759
|
-
generateFrontmatterTypes: () => generateFrontmatterTypes,
|
|
5760
|
-
inferType: () => inferType,
|
|
5761
|
-
setRenderContext: () => setRenderContext,
|
|
5762
|
-
useIsActive: () => useIsActive,
|
|
5763
|
-
useNav: () => useNav,
|
|
5764
|
-
usePageProps: () => usePageProps,
|
|
5765
|
-
useRenderContext: () => useRenderContext,
|
|
5766
|
-
useSiteConfig: () => useSiteConfig
|
|
5767
|
-
});
|
|
5768
|
-
/**
|
|
5769
|
-
* Sets the current render context.
|
|
5770
|
-
* Called internally during page rendering.
|
|
5771
|
-
* @internal
|
|
5772
|
-
*/
|
|
5773
|
-
function setRenderContext(ctx) {
|
|
5774
|
-
currentContext = ctx;
|
|
5775
|
-
}
|
|
5776
|
-
/**
|
|
5777
|
-
* Clears the current render context.
|
|
5778
|
-
* Called internally after page rendering.
|
|
5779
|
-
* @internal
|
|
5780
|
-
*/
|
|
5781
|
-
function clearRenderContext() {
|
|
5782
|
-
currentContext = null;
|
|
5783
|
-
}
|
|
5784
|
-
/**
|
|
5785
|
-
* Gets the current page props.
|
|
5786
|
-
*
|
|
5787
|
-
* @returns The current page props
|
|
5788
|
-
* @throws Error if called outside of a render context
|
|
5789
|
-
*
|
|
5790
|
-
* @example
|
|
5791
|
-
* ```tsx
|
|
5792
|
-
* function PageTitle() {
|
|
5793
|
-
* const page = usePageProps();
|
|
5794
|
-
* return <h1>{page.title}</h1>;
|
|
5795
|
-
* }
|
|
5796
|
-
* ```
|
|
5797
|
-
*/
|
|
5798
|
-
function usePageProps() {
|
|
5799
|
-
if (!currentContext) throw new Error("[ox-content] usePageProps() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5800
|
-
return currentContext.page;
|
|
5801
|
-
}
|
|
5802
|
-
/**
|
|
5803
|
-
* Gets the site configuration.
|
|
5804
|
-
*
|
|
5805
|
-
* @returns The site configuration
|
|
5806
|
-
* @throws Error if called outside of a render context
|
|
5807
|
-
*
|
|
5808
|
-
* @example
|
|
5809
|
-
* ```tsx
|
|
5810
|
-
* function SiteHeader() {
|
|
5811
|
-
* const site = useSiteConfig();
|
|
5812
|
-
* return <header>{site.name}</header>;
|
|
5813
|
-
* }
|
|
5814
|
-
* ```
|
|
5815
|
-
*/
|
|
5816
|
-
function useSiteConfig() {
|
|
5817
|
-
if (!currentContext) throw new Error("[ox-content] useSiteConfig() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5818
|
-
return currentContext.site;
|
|
5819
|
-
}
|
|
5820
|
-
/**
|
|
5821
|
-
* Gets the full render context.
|
|
5822
|
-
*
|
|
5823
|
-
* @returns The complete render context
|
|
5824
|
-
* @throws Error if called outside of a render context
|
|
5825
|
-
*
|
|
5826
|
-
* @example
|
|
5827
|
-
* ```tsx
|
|
5828
|
-
* function Layout({ children }) {
|
|
5829
|
-
* const ctx = useRenderContext();
|
|
5830
|
-
* return (
|
|
5831
|
-
* <html>
|
|
5832
|
-
* <head><title>{ctx.page.title} - {ctx.site.name}</title></head>
|
|
5833
|
-
* <body>{children}</body>
|
|
5834
|
-
* </html>
|
|
5835
|
-
* );
|
|
5836
|
-
* }
|
|
5837
|
-
* ```
|
|
5838
|
-
*/
|
|
5839
|
-
function useRenderContext() {
|
|
5840
|
-
if (!currentContext) throw new Error("[ox-content] useRenderContext() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5841
|
-
return currentContext;
|
|
5842
|
-
}
|
|
5843
|
-
/**
|
|
5844
|
-
* Gets the navigation groups.
|
|
5845
|
-
*
|
|
5846
|
-
* @example
|
|
5847
|
-
* ```tsx
|
|
5848
|
-
* function Sidebar() {
|
|
5849
|
-
* const nav = useNav();
|
|
5850
|
-
* return (
|
|
5851
|
-
* <nav>
|
|
5852
|
-
* {each(nav, (group) => (
|
|
5853
|
-
* <div>
|
|
5854
|
-
* <h3>{group.title}</h3>
|
|
5855
|
-
* <ul>
|
|
5856
|
-
* {each(group.items, (item) => (
|
|
5857
|
-
* <li><a href={item.href}>{item.title}</a></li>
|
|
5858
|
-
* ))}
|
|
5859
|
-
* </ul>
|
|
5860
|
-
* </div>
|
|
5861
|
-
* ))}
|
|
5862
|
-
* </nav>
|
|
5863
|
-
* );
|
|
5864
|
-
* }
|
|
5865
|
-
* ```
|
|
5866
|
-
*/
|
|
5867
|
-
function useNav() {
|
|
5868
|
-
return useSiteConfig().nav;
|
|
5869
|
-
}
|
|
5870
|
-
/**
|
|
5871
|
-
* Checks if the given path is the current page.
|
|
5872
|
-
*
|
|
5873
|
-
* @example
|
|
5874
|
-
* ```tsx
|
|
5875
|
-
* function NavLink({ href, children }) {
|
|
5876
|
-
* const isActive = useIsActive(href);
|
|
5877
|
-
* return <a href={href} class={isActive ? 'active' : ''}>{children}</a>;
|
|
5878
|
-
* }
|
|
5879
|
-
* ```
|
|
5880
|
-
*/
|
|
5881
|
-
function useIsActive(path) {
|
|
5882
|
-
const page = usePageProps();
|
|
5883
|
-
return page.path === path || page.url === path;
|
|
5884
|
-
}
|
|
5885
|
-
/**
|
|
5886
|
-
* Infers TypeScript types from frontmatter values.
|
|
5887
|
-
*/
|
|
5888
|
-
function inferType(value) {
|
|
5889
|
-
if (value === null) return "null";
|
|
5890
|
-
if (value === void 0) return "undefined";
|
|
5891
|
-
if (typeof value === "string") return "string";
|
|
5892
|
-
if (typeof value === "number") return "number";
|
|
5893
|
-
if (typeof value === "boolean") return "boolean";
|
|
5894
|
-
if (Array.isArray(value)) {
|
|
5895
|
-
if (value.length === 0) return "unknown[]";
|
|
5896
|
-
const itemTypes = [...new Set(value.map(inferType))];
|
|
5897
|
-
if (itemTypes.length === 1) return `${itemTypes[0]}[]`;
|
|
5898
|
-
return `(${itemTypes.join(" | ")})[]`;
|
|
5899
|
-
}
|
|
5900
|
-
if (typeof value === "object") {
|
|
5901
|
-
const entries = Object.entries(value);
|
|
5902
|
-
if (entries.length === 0) return "Record<string, unknown>";
|
|
5903
|
-
return `{ ${entries.map(([k, v]) => `${k}: ${inferType(v)}`).join("; ")} }`;
|
|
5904
|
-
}
|
|
5905
|
-
return "unknown";
|
|
5906
|
-
}
|
|
5907
|
-
/**
|
|
5908
|
-
* Generates TypeScript interface from frontmatter samples.
|
|
5909
|
-
*/
|
|
5910
|
-
function generateFrontmatterTypes(samples, interfaceName = "PageFrontmatter") {
|
|
5911
|
-
const fields = /* @__PURE__ */ new Map();
|
|
5912
|
-
for (const sample of samples) for (const [key, value] of Object.entries(sample)) {
|
|
5913
|
-
const existing = fields.get(key) ?? {
|
|
5914
|
-
types: /* @__PURE__ */ new Set(),
|
|
5915
|
-
count: 0
|
|
5916
|
-
};
|
|
5917
|
-
existing.types.add(inferType(value));
|
|
5918
|
-
existing.count++;
|
|
5919
|
-
fields.set(key, existing);
|
|
5920
|
-
}
|
|
5921
|
-
const lines = [
|
|
5922
|
-
"/**",
|
|
5923
|
-
" * Auto-generated frontmatter type based on your pages.",
|
|
5924
|
-
" * DO NOT EDIT - this file is generated by ox-content.",
|
|
5925
|
-
" */",
|
|
5926
|
-
"",
|
|
5927
|
-
`export interface ${interfaceName} {`
|
|
5928
|
-
];
|
|
5929
|
-
for (const [name, { types, count }] of fields) {
|
|
5930
|
-
const isOptional = count < samples.length;
|
|
5931
|
-
const typeStr = [...types].join(" | ");
|
|
5932
|
-
const optionalMark = isOptional ? "?" : "";
|
|
5933
|
-
lines.push(` ${name}${optionalMark}: ${typeStr};`);
|
|
5934
|
-
}
|
|
5935
|
-
lines.push("}");
|
|
5936
|
-
lines.push("");
|
|
5937
|
-
lines.push(`export type PageProps = import('@ox-content/vite-plugin').PageProps<${interfaceName}>;`);
|
|
5938
|
-
lines.push("");
|
|
5939
|
-
return lines.join("\n");
|
|
5940
|
-
}
|
|
5941
|
-
var currentContext;
|
|
5942
|
-
var init_page_context = require_vitepress.__esmMin((() => {
|
|
5943
|
-
currentContext = null;
|
|
5944
|
-
}));
|
|
5945
|
-
//#endregion
|
|
5946
|
-
//#region src/theme-renderer.ts
|
|
5947
|
-
/**
|
|
5948
|
-
* Theme Renderer for Static HTML Generation
|
|
5949
|
-
*
|
|
5950
|
-
* Renders JSX theme components to static HTML strings.
|
|
5951
|
-
* No client-side JavaScript is included by default.
|
|
5952
|
-
*/
|
|
5953
|
-
init_page_context();
|
|
5954
|
-
/**
|
|
5955
|
-
* Renders a page using the theme component.
|
|
5956
|
-
*
|
|
5957
|
-
* @param page - Page data to render
|
|
5958
|
-
* @param options - Theme render options
|
|
5959
|
-
* @returns Rendered HTML string
|
|
5960
|
-
*/
|
|
5961
|
-
function renderPage(page, options) {
|
|
5962
|
-
const { theme, siteName, base, nav, pages } = options;
|
|
5963
|
-
setRenderContext({
|
|
5964
|
-
page: {
|
|
5965
|
-
title: page.title,
|
|
5966
|
-
description: page.description,
|
|
5967
|
-
html: page.html,
|
|
5968
|
-
toc: page.toc,
|
|
5969
|
-
lastUpdated: page.lastUpdated,
|
|
5970
|
-
path: page.path,
|
|
5971
|
-
url: page.url,
|
|
5972
|
-
frontmatter: page.frontmatter,
|
|
5973
|
-
layout: page.layout
|
|
5974
|
-
},
|
|
5975
|
-
site: {
|
|
5976
|
-
name: siteName,
|
|
5977
|
-
base,
|
|
5978
|
-
nav,
|
|
5979
|
-
pages: pages.map((p) => ({
|
|
5980
|
-
title: p.title,
|
|
5981
|
-
description: p.description,
|
|
5982
|
-
html: p.html,
|
|
5983
|
-
toc: p.toc,
|
|
5984
|
-
lastUpdated: p.lastUpdated,
|
|
5985
|
-
path: p.path,
|
|
5986
|
-
url: p.url,
|
|
5987
|
-
frontmatter: p.frontmatter,
|
|
5988
|
-
layout: p.layout
|
|
5989
|
-
}))
|
|
5990
|
-
}
|
|
5991
|
-
});
|
|
5992
|
-
try {
|
|
5993
|
-
const result = theme({ children: require_jsx_html.raw(page.html) });
|
|
5994
|
-
const html = require_jsx_html.renderToString(result);
|
|
5995
|
-
if (!html.trimStart().toLowerCase().startsWith("<!doctype")) return `<!DOCTYPE html>\n${html}`;
|
|
5996
|
-
return html;
|
|
5997
|
-
} finally {
|
|
5998
|
-
clearRenderContext();
|
|
5999
|
-
}
|
|
6000
|
-
}
|
|
6001
|
-
/**
|
|
6002
|
-
* Renders all pages and generates type definitions.
|
|
6003
|
-
*
|
|
6004
|
-
* @param pages - All pages to render
|
|
6005
|
-
* @param options - Theme render options
|
|
6006
|
-
* @returns Map of output paths to rendered HTML
|
|
6007
|
-
*/
|
|
6008
|
-
async function renderAllPages(pages, options) {
|
|
6009
|
-
const results = /* @__PURE__ */ new Map();
|
|
6010
|
-
for (const page of pages) {
|
|
6011
|
-
const html = renderPage(page, {
|
|
6012
|
-
...options,
|
|
6013
|
-
pages
|
|
6014
|
-
});
|
|
6015
|
-
results.set(page.url, html);
|
|
6016
|
-
}
|
|
6017
|
-
if (options.typesOutDir) await generateTypes(pages, options.typesOutDir);
|
|
6018
|
-
return results;
|
|
6019
|
-
}
|
|
6020
|
-
/**
|
|
6021
|
-
* Generates TypeScript type definitions from page frontmatter.
|
|
6022
|
-
*
|
|
6023
|
-
* @param pages - All pages
|
|
6024
|
-
* @param outDir - Output directory for types
|
|
6025
|
-
*/
|
|
6026
|
-
async function generateTypes(pages, outDir) {
|
|
6027
|
-
const types = generateFrontmatterTypes(pages.map((p) => p.frontmatter));
|
|
6028
|
-
const typesPath = (0, node_path.join)(outDir, "page-props.d.ts");
|
|
6029
|
-
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(typesPath), { recursive: true });
|
|
6030
|
-
await (0, node_fs_promises.writeFile)(typesPath, types, "utf-8");
|
|
6031
|
-
}
|
|
6032
|
-
/**
|
|
6033
|
-
* Default theme component.
|
|
6034
|
-
* A minimal theme that renders page content with basic styling.
|
|
6035
|
-
*/
|
|
6036
|
-
function DefaultTheme({ children }) {
|
|
6037
|
-
const { usePageProps, useSiteConfig } = (init_page_context(), require_vitepress.__toCommonJS(page_context_exports));
|
|
6038
|
-
const page = usePageProps();
|
|
6039
|
-
const site = useSiteConfig();
|
|
6040
|
-
return { __html: `<!DOCTYPE html>
|
|
6041
|
-
<html lang="en">
|
|
6042
|
-
<head>
|
|
6043
|
-
<meta charset="UTF-8">
|
|
6044
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6045
|
-
<title>${escapeHtml(page.title)} - ${escapeHtml(site.name)}</title>
|
|
6046
|
-
${page.description ? `<meta name="description" content="${escapeHtml(page.description)}">` : ""}
|
|
6047
|
-
<style>
|
|
6048
|
-
:root {
|
|
6049
|
-
--octc-color-primary: #4f6fae;
|
|
6050
|
-
--octc-color-text: #131a30;
|
|
6051
|
-
--octc-color-bg: #ffffff;
|
|
6052
|
-
--octc-color-bg-alt: #f5f7fb;
|
|
6053
|
-
--octc-color-text-muted: #4f607b;
|
|
6054
|
-
--octc-color-border: #d2dbea;
|
|
6055
|
-
}
|
|
6056
|
-
body {
|
|
6057
|
-
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
6058
|
-
line-height: 1.7;
|
|
6059
|
-
color: var(--octc-color-text);
|
|
6060
|
-
background: var(--octc-color-bg);
|
|
6061
|
-
max-width: 800px;
|
|
6062
|
-
margin: 0 auto;
|
|
6063
|
-
padding: 2rem;
|
|
6064
|
-
}
|
|
6065
|
-
a { color: var(--octc-color-primary); }
|
|
6066
|
-
</style>
|
|
6067
|
-
</head>
|
|
6068
|
-
<body>
|
|
6069
|
-
<header>
|
|
6070
|
-
<h1>${escapeHtml(site.name)}</h1>
|
|
6071
|
-
</header>
|
|
6072
|
-
<main>
|
|
6073
|
-
${children.__html}
|
|
6074
|
-
</main>
|
|
6075
|
-
</body>
|
|
6076
|
-
</html>` };
|
|
6077
|
-
}
|
|
6078
|
-
function escapeHtml(str) {
|
|
6079
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6080
|
-
}
|
|
6081
|
-
/**
|
|
6082
|
-
* Creates a theme with layout switching support.
|
|
6083
|
-
*
|
|
6084
|
-
* @example
|
|
6085
|
-
* ```tsx
|
|
6086
|
-
* import { createTheme } from '@ox-content/vite-plugin';
|
|
6087
|
-
* import { DefaultLayout } from './layouts/Default';
|
|
6088
|
-
* import { EntryLayout } from './layouts/Entry';
|
|
6089
|
-
*
|
|
6090
|
-
* export default createTheme({
|
|
6091
|
-
* layouts: {
|
|
6092
|
-
* default: DefaultLayout,
|
|
6093
|
-
* entry: EntryLayout,
|
|
6094
|
-
* },
|
|
6095
|
-
* });
|
|
6096
|
-
* ```
|
|
6097
|
-
*/
|
|
6098
|
-
function createTheme(config) {
|
|
6099
|
-
const { layouts, defaultLayout = "default" } = config;
|
|
6100
|
-
return function ThemeWithLayouts({ children }) {
|
|
6101
|
-
const { usePageProps } = (init_page_context(), require_vitepress.__toCommonJS(page_context_exports));
|
|
6102
|
-
const layoutName = usePageProps().layout ?? defaultLayout;
|
|
6103
|
-
const Layout = layouts[layoutName] ?? layouts[defaultLayout];
|
|
6104
|
-
if (!Layout) throw new Error(`[ox-content] Layout "${layoutName}" not found. Available layouts: ${Object.keys(layouts).join(", ")}`);
|
|
6105
|
-
return Layout({ children });
|
|
6106
|
-
};
|
|
6107
|
-
}
|
|
6108
|
-
//#endregion
|
|
6109
6450
|
//#region src/index.ts
|
|
6110
6451
|
/**
|
|
6111
6452
|
* Vite Plugin for Ox Content
|