@ox-content/vite-plugin 2.88.0 → 2.89.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 +1286 -1186
- 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 +1286 -1186
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -2760,6 +2760,45 @@ async function resolveTemplate(options, root) {
|
|
|
2760
2760
|
}
|
|
2761
2761
|
}
|
|
2762
2762
|
/**
|
|
2763
|
+
* Matches this package and every subpath it exports.
|
|
2764
|
+
*
|
|
2765
|
+
* A template's natural runtime is whatever renders it, and for the
|
|
2766
|
+
* framework-less kinds that is this package: `renderToString`, `raw`, `when`
|
|
2767
|
+
* and `each` live at its root, and the JSX runtime under `./jsx-runtime`.
|
|
2768
|
+
* Inlining them instead drags the entire plugin — chokidar, fsevents and all
|
|
2769
|
+
* — into the template bundle, which is what made importing it fail outright.
|
|
2770
|
+
*/
|
|
2771
|
+
const OX_CONTENT_PACKAGE = /^@ox-content\/vite-plugin(\/.*)?$/;
|
|
2772
|
+
/**
|
|
2773
|
+
* Whether `id` is a bare specifier, and so resolvable at runtime rather than
|
|
2774
|
+
* something the template bundle has to inline.
|
|
2775
|
+
*
|
|
2776
|
+
* Template bundles are written to `<root>/.cache/og-images/` and imported
|
|
2777
|
+
* from there, so Node resolves anything left external against the project's
|
|
2778
|
+
* own `node_modules`. Relative and absolute imports still bundle, which is
|
|
2779
|
+
* what a template actually needs — its own components travel with it.
|
|
2780
|
+
*/
|
|
2781
|
+
function isBareSpecifier(id) {
|
|
2782
|
+
if (id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return false;
|
|
2783
|
+
return !/^[a-zA-Z]:[\\/]/.test(id);
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Rolldown input options for a `.ts` template bundle.
|
|
2787
|
+
*
|
|
2788
|
+
* A `.ts` template is the framework-less kind, so it has no single runtime to
|
|
2789
|
+
* externalize the way the `.vue`, `.svelte` and `.tsx` paths do — anything
|
|
2790
|
+
* from `node_modules` is better resolved at import time than inlined. Nothing
|
|
2791
|
+
* on this path has a compiler plugin, so nothing here needed bundling to be
|
|
2792
|
+
* loadable in the first place.
|
|
2793
|
+
*/
|
|
2794
|
+
function tsTemplateBundleOptions(templatePath) {
|
|
2795
|
+
return {
|
|
2796
|
+
input: templatePath,
|
|
2797
|
+
platform: "node",
|
|
2798
|
+
external: (id) => isBareSpecifier(id)
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2763
2802
|
* Resolves a plain TypeScript template (existing behavior).
|
|
2764
2803
|
*/
|
|
2765
2804
|
async function resolveTsTemplate(templatePath, options, root) {
|
|
@@ -2768,10 +2807,7 @@ async function resolveTsTemplate(templatePath, options, root) {
|
|
|
2768
2807
|
const cacheDir = path$2.join(root, ".cache", "og-images");
|
|
2769
2808
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
2770
2809
|
const outfile = path$2.join(cacheDir, "_template.mjs");
|
|
2771
|
-
const bundle = await rolldown(
|
|
2772
|
-
input: templatePath,
|
|
2773
|
-
platform: "node"
|
|
2774
|
-
});
|
|
2810
|
+
const bundle = await rolldown(tsTemplateBundleOptions(templatePath));
|
|
2775
2811
|
await bundle.write({
|
|
2776
2812
|
file: outfile,
|
|
2777
2813
|
format: "esm"
|
|
@@ -2793,11 +2829,16 @@ async function resolveVueTemplate(templatePath, options, root) {
|
|
|
2793
2829
|
const cacheDir = path$2.join(root, ".cache", "og-images");
|
|
2794
2830
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
2795
2831
|
const outfile = path$2.join(cacheDir, "_template_vue.mjs");
|
|
2832
|
+
const plugins = options.vuePlugin === "vizejs" ? await getVizejsPlugin() : [createVueCompilerPlugin()];
|
|
2796
2833
|
const bundle = await rolldown({
|
|
2797
2834
|
input: templatePath,
|
|
2798
2835
|
platform: "node",
|
|
2799
|
-
external: [
|
|
2800
|
-
|
|
2836
|
+
external: [
|
|
2837
|
+
"vue",
|
|
2838
|
+
"vue/server-renderer",
|
|
2839
|
+
OX_CONTENT_PACKAGE
|
|
2840
|
+
],
|
|
2841
|
+
plugins
|
|
2801
2842
|
});
|
|
2802
2843
|
await bundle.write({
|
|
2803
2844
|
file: outfile,
|
|
@@ -2899,7 +2940,8 @@ async function resolveSvelteTemplate(templatePath, root) {
|
|
|
2899
2940
|
"svelte",
|
|
2900
2941
|
"svelte/server",
|
|
2901
2942
|
"svelte/internal",
|
|
2902
|
-
"svelte/internal/server"
|
|
2943
|
+
"svelte/internal/server",
|
|
2944
|
+
OX_CONTENT_PACKAGE
|
|
2903
2945
|
],
|
|
2904
2946
|
plugins: [createSvelteCompilerPlugin()]
|
|
2905
2947
|
});
|
|
@@ -2958,7 +3000,8 @@ async function resolveReactTemplate(templatePath, root) {
|
|
|
2958
3000
|
"react/jsx-runtime",
|
|
2959
3001
|
"react/jsx-dev-runtime",
|
|
2960
3002
|
"react-dom",
|
|
2961
|
-
"react-dom/server"
|
|
3003
|
+
"react-dom/server",
|
|
3004
|
+
OX_CONTENT_PACKAGE
|
|
2962
3005
|
],
|
|
2963
3006
|
transform: { jsx: "react-jsx" }
|
|
2964
3007
|
});
|
|
@@ -3290,674 +3333,724 @@ initIslands((el, props) => {
|
|
|
3290
3333
|
`;
|
|
3291
3334
|
}
|
|
3292
3335
|
//#endregion
|
|
3293
|
-
//#region src/
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3336
|
+
//#region src/page-context.ts
|
|
3337
|
+
var page_context_exports = /* @__PURE__ */ __exportAll({
|
|
3338
|
+
clearRenderContext: () => clearRenderContext,
|
|
3339
|
+
generateFrontmatterTypes: () => generateFrontmatterTypes,
|
|
3340
|
+
inferType: () => inferType,
|
|
3341
|
+
setRenderContext: () => setRenderContext,
|
|
3342
|
+
useIsActive: () => useIsActive,
|
|
3343
|
+
useNav: () => useNav,
|
|
3344
|
+
usePageProps: () => usePageProps,
|
|
3345
|
+
useRenderContext: () => useRenderContext,
|
|
3346
|
+
useSiteConfig: () => useSiteConfig
|
|
3347
|
+
});
|
|
3297
3348
|
/**
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
3300
|
-
*
|
|
3301
|
-
* @deprecated Use `generateHtmlPage`/`buildSsg` instead.
|
|
3349
|
+
* Sets the current render context.
|
|
3350
|
+
* Called internally during page rendering.
|
|
3351
|
+
* @internal
|
|
3302
3352
|
*/
|
|
3303
|
-
|
|
3353
|
+
function setRenderContext(ctx) {
|
|
3354
|
+
currentContext = ctx;
|
|
3355
|
+
}
|
|
3304
3356
|
/**
|
|
3305
|
-
*
|
|
3357
|
+
* Clears the current render context.
|
|
3358
|
+
* Called internally after page rendering.
|
|
3359
|
+
* @internal
|
|
3306
3360
|
*/
|
|
3307
|
-
function
|
|
3308
|
-
|
|
3309
|
-
enabled: false,
|
|
3310
|
-
extension: ".html",
|
|
3311
|
-
clean: false,
|
|
3312
|
-
bare: false,
|
|
3313
|
-
generateOgImage: false,
|
|
3314
|
-
lastUpdated: false
|
|
3315
|
-
};
|
|
3316
|
-
if (ssg === true || ssg === void 0) return {
|
|
3317
|
-
enabled: true,
|
|
3318
|
-
extension: ".html",
|
|
3319
|
-
clean: false,
|
|
3320
|
-
bare: false,
|
|
3321
|
-
generateOgImage: false,
|
|
3322
|
-
lastUpdated: false,
|
|
3323
|
-
theme: resolveTheme(void 0)
|
|
3324
|
-
};
|
|
3325
|
-
return {
|
|
3326
|
-
enabled: ssg.enabled ?? true,
|
|
3327
|
-
extension: ssg.extension ?? ".html",
|
|
3328
|
-
clean: ssg.clean ?? false,
|
|
3329
|
-
bare: ssg.bare ?? false,
|
|
3330
|
-
siteName: ssg.siteName,
|
|
3331
|
-
ogImage: ssg.ogImage,
|
|
3332
|
-
generateOgImage: ssg.generateOgImage ?? false,
|
|
3333
|
-
lastUpdated: ssg.lastUpdated ?? false,
|
|
3334
|
-
siteUrl: ssg.siteUrl,
|
|
3335
|
-
theme: resolveTheme(ssg.theme),
|
|
3336
|
-
navigation: ssg.navigation
|
|
3337
|
-
};
|
|
3361
|
+
function clearRenderContext() {
|
|
3362
|
+
currentContext = null;
|
|
3338
3363
|
}
|
|
3339
3364
|
/**
|
|
3340
|
-
*
|
|
3365
|
+
* Gets the current page props.
|
|
3366
|
+
*
|
|
3367
|
+
* @returns The current page props
|
|
3368
|
+
* @throws Error if called outside of a render context
|
|
3369
|
+
*
|
|
3370
|
+
* @example
|
|
3371
|
+
* ```tsx
|
|
3372
|
+
* function PageTitle() {
|
|
3373
|
+
* const page = usePageProps();
|
|
3374
|
+
* return <h1>{page.title}</h1>;
|
|
3375
|
+
* }
|
|
3376
|
+
* ```
|
|
3341
3377
|
*/
|
|
3342
|
-
function
|
|
3343
|
-
|
|
3378
|
+
function usePageProps() {
|
|
3379
|
+
if (!currentContext) throw new Error("[ox-content] usePageProps() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3380
|
+
return currentContext.page;
|
|
3344
3381
|
}
|
|
3345
3382
|
/**
|
|
3346
|
-
*
|
|
3383
|
+
* Gets the site configuration.
|
|
3384
|
+
*
|
|
3385
|
+
* @returns The site configuration
|
|
3386
|
+
* @throws Error if called outside of a render context
|
|
3387
|
+
*
|
|
3388
|
+
* @example
|
|
3389
|
+
* ```tsx
|
|
3390
|
+
* function SiteHeader() {
|
|
3391
|
+
* const site = useSiteConfig();
|
|
3392
|
+
* return <header>{site.name}</header>;
|
|
3393
|
+
* }
|
|
3394
|
+
* ```
|
|
3347
3395
|
*/
|
|
3348
|
-
function
|
|
3349
|
-
|
|
3396
|
+
function useSiteConfig() {
|
|
3397
|
+
if (!currentContext) throw new Error("[ox-content] useSiteConfig() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3398
|
+
return currentContext.site;
|
|
3350
3399
|
}
|
|
3351
3400
|
/**
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
*
|
|
3401
|
+
* Gets the full render context.
|
|
3402
|
+
*
|
|
3403
|
+
* @returns The complete render context
|
|
3404
|
+
* @throws Error if called outside of a render context
|
|
3405
|
+
*
|
|
3406
|
+
* @example
|
|
3407
|
+
* ```tsx
|
|
3408
|
+
* function Layout({ children }) {
|
|
3409
|
+
* const ctx = useRenderContext();
|
|
3410
|
+
* return (
|
|
3411
|
+
* <html>
|
|
3412
|
+
* <head><title>{ctx.page.title} - {ctx.site.name}</title></head>
|
|
3413
|
+
* <body>{children}</body>
|
|
3414
|
+
* </html>
|
|
3415
|
+
* );
|
|
3416
|
+
* }
|
|
3417
|
+
* ```
|
|
3355
3418
|
*/
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
return
|
|
3359
|
-
title: item.title,
|
|
3360
|
-
path: item.path,
|
|
3361
|
-
href: item.href,
|
|
3362
|
-
children: item.children?.map(toRustNavItem),
|
|
3363
|
-
collapsed: item.collapsed,
|
|
3364
|
-
stickyCollapsed: item.stickyCollapsed
|
|
3365
|
-
};
|
|
3366
|
-
}
|
|
3367
|
-
function convertNavGroupsForRust(navGroups) {
|
|
3368
|
-
const cached = navGroupsForRustCache.get(navGroups);
|
|
3369
|
-
if (cached) return cached;
|
|
3370
|
-
const converted = navGroups.map((group) => ({
|
|
3371
|
-
title: group.title,
|
|
3372
|
-
collapsed: group.collapsed,
|
|
3373
|
-
stickyCollapsed: group.stickyCollapsed,
|
|
3374
|
-
items: group.items.map(toRustNavItem)
|
|
3375
|
-
}));
|
|
3376
|
-
navGroupsForRustCache.set(navGroups, converted);
|
|
3377
|
-
return converted;
|
|
3419
|
+
function useRenderContext() {
|
|
3420
|
+
if (!currentContext) throw new Error("[ox-content] useRenderContext() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
3421
|
+
return currentContext;
|
|
3378
3422
|
}
|
|
3379
3423
|
/**
|
|
3380
|
-
*
|
|
3381
|
-
*
|
|
3382
|
-
*
|
|
3424
|
+
* Gets the navigation groups.
|
|
3425
|
+
*
|
|
3426
|
+
* @example
|
|
3427
|
+
* ```tsx
|
|
3428
|
+
* function Sidebar() {
|
|
3429
|
+
* const nav = useNav();
|
|
3430
|
+
* return (
|
|
3431
|
+
* <nav>
|
|
3432
|
+
* {each(nav, (group) => (
|
|
3433
|
+
* <div>
|
|
3434
|
+
* <h3>{group.title}</h3>
|
|
3435
|
+
* <ul>
|
|
3436
|
+
* {each(group.items, (item) => (
|
|
3437
|
+
* <li><a href={item.href}>{item.title}</a></li>
|
|
3438
|
+
* ))}
|
|
3439
|
+
* </ul>
|
|
3440
|
+
* </div>
|
|
3441
|
+
* ))}
|
|
3442
|
+
* </nav>
|
|
3443
|
+
* );
|
|
3444
|
+
* }
|
|
3445
|
+
* ```
|
|
3383
3446
|
*/
|
|
3384
|
-
function
|
|
3385
|
-
return
|
|
3386
|
-
depth: entry.depth,
|
|
3387
|
-
text: entry.text,
|
|
3388
|
-
slug: entry.slug,
|
|
3389
|
-
children: entry.children?.map(toRustTocEntry) ?? []
|
|
3390
|
-
};
|
|
3447
|
+
function useNav() {
|
|
3448
|
+
return useSiteConfig().nav;
|
|
3391
3449
|
}
|
|
3392
3450
|
/**
|
|
3393
|
-
*
|
|
3394
|
-
*
|
|
3395
|
-
*
|
|
3451
|
+
* Checks if the given path is the current page.
|
|
3452
|
+
*
|
|
3453
|
+
* @example
|
|
3454
|
+
* ```tsx
|
|
3455
|
+
* function NavLink({ href, children }) {
|
|
3456
|
+
* const isActive = useIsActive(href);
|
|
3457
|
+
* return <a href={href} class={isActive ? 'active' : ''}>{children}</a>;
|
|
3458
|
+
* }
|
|
3459
|
+
* ```
|
|
3396
3460
|
*/
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
if (cached) return cached;
|
|
3401
|
-
const converted = locales.map((locale) => ({
|
|
3402
|
-
code: locale.code,
|
|
3403
|
-
name: locale.name,
|
|
3404
|
-
dir: locale.dir ?? "ltr"
|
|
3405
|
-
}));
|
|
3406
|
-
rustLocalesCache.set(locales, converted);
|
|
3407
|
-
return converted;
|
|
3461
|
+
function useIsActive(path) {
|
|
3462
|
+
const page = usePageProps();
|
|
3463
|
+
return page.path === path || page.url === path;
|
|
3408
3464
|
}
|
|
3409
3465
|
/**
|
|
3410
|
-
*
|
|
3411
|
-
* `i18n.locales` reference is stable across a build, so the `.map` to codes
|
|
3412
|
-
* runs once instead of once per page.
|
|
3466
|
+
* Infers TypeScript types from frontmatter values.
|
|
3413
3467
|
*/
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
if (
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3468
|
+
function inferType(value) {
|
|
3469
|
+
if (value === null) return "null";
|
|
3470
|
+
if (value === void 0) return "undefined";
|
|
3471
|
+
if (typeof value === "string") return "string";
|
|
3472
|
+
if (typeof value === "number") return "number";
|
|
3473
|
+
if (typeof value === "boolean") return "boolean";
|
|
3474
|
+
if (Array.isArray(value)) {
|
|
3475
|
+
if (value.length === 0) return "unknown[]";
|
|
3476
|
+
const itemTypes = [...new Set(value.map(inferType))];
|
|
3477
|
+
if (itemTypes.length === 1) return `${itemTypes[0]}[]`;
|
|
3478
|
+
return `(${itemTypes.join(" | ")})[]`;
|
|
3479
|
+
}
|
|
3480
|
+
if (typeof value === "object") {
|
|
3481
|
+
const entries = Object.entries(value);
|
|
3482
|
+
if (entries.length === 0) return "Record<string, unknown>";
|
|
3483
|
+
return `{ ${entries.map(([k, v]) => `${k}: ${inferType(v)}`).join("; ")} }`;
|
|
3484
|
+
}
|
|
3485
|
+
return "unknown";
|
|
3421
3486
|
}
|
|
3422
3487
|
/**
|
|
3423
|
-
* Generates
|
|
3488
|
+
* Generates TypeScript interface from frontmatter samples.
|
|
3424
3489
|
*/
|
|
3425
|
-
|
|
3426
|
-
const
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
title: f.title,
|
|
3456
|
-
details: f.details,
|
|
3457
|
-
link: f.link,
|
|
3458
|
-
linkText: f.linkText
|
|
3459
|
-
}))
|
|
3460
|
-
} : void 0;
|
|
3461
|
-
return mod.generateSsgHtml({
|
|
3462
|
-
title: pageData.title,
|
|
3463
|
-
description: pageData.description,
|
|
3464
|
-
content: pageData.content,
|
|
3465
|
-
toc: tocForRust,
|
|
3466
|
-
lastUpdated: pageData.lastUpdated,
|
|
3467
|
-
path: pageData.path,
|
|
3468
|
-
entryPage: entryPageForRust
|
|
3469
|
-
}, navGroupsForRust, {
|
|
3470
|
-
siteName,
|
|
3471
|
-
base,
|
|
3472
|
-
ogImage,
|
|
3473
|
-
theme: themeForRust,
|
|
3474
|
-
locale,
|
|
3475
|
-
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0
|
|
3476
|
-
});
|
|
3477
|
-
}
|
|
3478
|
-
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
3479
|
-
const optimized = (await importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
3480
|
-
await Promise.all(optimized.assets.map(async (asset) => {
|
|
3481
|
-
await fs$2.mkdir(path$2.dirname(asset.outputPath), { recursive: true });
|
|
3482
|
-
await fs$2.writeFile(asset.outputPath, asset.content, "utf-8");
|
|
3483
|
-
}));
|
|
3484
|
-
return {
|
|
3485
|
-
pages: optimized.pages,
|
|
3486
|
-
assets: optimized.assets.map((asset) => asset.outputPath)
|
|
3487
|
-
};
|
|
3490
|
+
function generateFrontmatterTypes(samples, interfaceName = "PageFrontmatter") {
|
|
3491
|
+
const fields = /* @__PURE__ */ new Map();
|
|
3492
|
+
for (const sample of samples) for (const [key, value] of Object.entries(sample)) {
|
|
3493
|
+
const existing = fields.get(key) ?? {
|
|
3494
|
+
types: /* @__PURE__ */ new Set(),
|
|
3495
|
+
count: 0
|
|
3496
|
+
};
|
|
3497
|
+
existing.types.add(inferType(value));
|
|
3498
|
+
existing.count++;
|
|
3499
|
+
fields.set(key, existing);
|
|
3500
|
+
}
|
|
3501
|
+
const lines = [
|
|
3502
|
+
"/**",
|
|
3503
|
+
" * Auto-generated frontmatter type based on your pages.",
|
|
3504
|
+
" * DO NOT EDIT - this file is generated by ox-content.",
|
|
3505
|
+
" */",
|
|
3506
|
+
"",
|
|
3507
|
+
`export interface ${interfaceName} {`
|
|
3508
|
+
];
|
|
3509
|
+
for (const [name, { types, count }] of fields) {
|
|
3510
|
+
const isOptional = count < samples.length;
|
|
3511
|
+
const typeStr = [...types].join(" | ");
|
|
3512
|
+
const optionalMark = isOptional ? "?" : "";
|
|
3513
|
+
lines.push(` ${name}${optionalMark}: ${typeStr};`);
|
|
3514
|
+
}
|
|
3515
|
+
lines.push("}");
|
|
3516
|
+
lines.push("");
|
|
3517
|
+
lines.push(`export type PageProps = import('@ox-content/vite-plugin').PageProps<${interfaceName}>;`);
|
|
3518
|
+
lines.push("");
|
|
3519
|
+
return lines.join("\n");
|
|
3488
3520
|
}
|
|
3521
|
+
var currentContext;
|
|
3522
|
+
var init_page_context = __esmMin((() => {
|
|
3523
|
+
currentContext = null;
|
|
3524
|
+
}));
|
|
3525
|
+
//#endregion
|
|
3526
|
+
//#region src/theme-renderer.ts
|
|
3489
3527
|
/**
|
|
3490
|
-
*
|
|
3528
|
+
* Theme Renderer for Static HTML Generation
|
|
3529
|
+
*
|
|
3530
|
+
* Renders JSX theme components to static HTML strings.
|
|
3531
|
+
* No client-side JavaScript is included by default.
|
|
3491
3532
|
*/
|
|
3492
|
-
|
|
3493
|
-
return importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
|
|
3494
|
-
}
|
|
3533
|
+
init_page_context();
|
|
3495
3534
|
/**
|
|
3496
|
-
*
|
|
3535
|
+
* Renders a page using the theme component.
|
|
3536
|
+
*
|
|
3537
|
+
* @param page - Page data to render
|
|
3538
|
+
* @param options - Theme render options
|
|
3539
|
+
* @returns Rendered HTML string
|
|
3497
3540
|
*/
|
|
3498
|
-
function
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3541
|
+
function renderPage(page, options) {
|
|
3542
|
+
const { theme, siteName, base, nav, pages } = options;
|
|
3543
|
+
setRenderContext({
|
|
3544
|
+
page: {
|
|
3545
|
+
title: page.title,
|
|
3546
|
+
description: page.description,
|
|
3547
|
+
html: page.html,
|
|
3548
|
+
toc: page.toc,
|
|
3549
|
+
lastUpdated: page.lastUpdated,
|
|
3550
|
+
path: page.path,
|
|
3551
|
+
url: page.url,
|
|
3552
|
+
frontmatter: page.frontmatter,
|
|
3553
|
+
layout: page.layout
|
|
3554
|
+
},
|
|
3555
|
+
site: {
|
|
3556
|
+
name: siteName,
|
|
3557
|
+
base,
|
|
3558
|
+
nav,
|
|
3559
|
+
pages: pages.map((p) => ({
|
|
3560
|
+
title: p.title,
|
|
3561
|
+
description: p.description,
|
|
3562
|
+
html: p.html,
|
|
3563
|
+
toc: p.toc,
|
|
3564
|
+
lastUpdated: p.lastUpdated,
|
|
3565
|
+
path: p.path,
|
|
3566
|
+
url: p.url,
|
|
3567
|
+
frontmatter: p.frontmatter,
|
|
3568
|
+
layout: p.layout
|
|
3569
|
+
}))
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
try {
|
|
3573
|
+
const result = theme({ children: raw(page.html) });
|
|
3574
|
+
const html = renderToString(result);
|
|
3575
|
+
if (!html.trimStart().toLowerCase().startsWith("<!doctype")) return `<!DOCTYPE html>\n${html}`;
|
|
3576
|
+
return html;
|
|
3577
|
+
} finally {
|
|
3578
|
+
clearRenderContext();
|
|
3579
|
+
}
|
|
3508
3580
|
}
|
|
3509
3581
|
/**
|
|
3510
|
-
*
|
|
3582
|
+
* Renders all pages and generates type definitions.
|
|
3583
|
+
*
|
|
3584
|
+
* @param pages - All pages to render
|
|
3585
|
+
* @param options - Theme render options
|
|
3586
|
+
* @returns Map of output paths to rendered HTML
|
|
3511
3587
|
*/
|
|
3512
|
-
function
|
|
3513
|
-
|
|
3588
|
+
async function renderAllPages(pages, options) {
|
|
3589
|
+
const results = /* @__PURE__ */ new Map();
|
|
3590
|
+
for (const page of pages) {
|
|
3591
|
+
const html = renderPage(page, {
|
|
3592
|
+
...options,
|
|
3593
|
+
pages
|
|
3594
|
+
});
|
|
3595
|
+
results.set(page.url, html);
|
|
3596
|
+
}
|
|
3597
|
+
if (options.typesOutDir) await generateTypes(pages, options.typesOutDir);
|
|
3598
|
+
return results;
|
|
3514
3599
|
}
|
|
3515
3600
|
/**
|
|
3516
|
-
*
|
|
3601
|
+
* Generates TypeScript type definitions from page frontmatter.
|
|
3602
|
+
*
|
|
3603
|
+
* @param pages - All pages
|
|
3604
|
+
* @param outDir - Output directory for types
|
|
3517
3605
|
*/
|
|
3518
|
-
async function
|
|
3519
|
-
|
|
3606
|
+
async function generateTypes(pages, outDir) {
|
|
3607
|
+
const types = generateFrontmatterTypes(pages.map((p) => p.frontmatter));
|
|
3608
|
+
const typesPath = join(outDir, "page-props.d.ts");
|
|
3609
|
+
await mkdir(dirname(typesPath), { recursive: true });
|
|
3610
|
+
await writeFile(typesPath, types, "utf-8");
|
|
3520
3611
|
}
|
|
3521
3612
|
/**
|
|
3522
|
-
*
|
|
3613
|
+
* Default theme component.
|
|
3614
|
+
* A minimal theme that renders page content with basic styling.
|
|
3523
3615
|
*/
|
|
3524
|
-
function
|
|
3525
|
-
|
|
3616
|
+
function DefaultTheme({ children }) {
|
|
3617
|
+
const { usePageProps, useSiteConfig } = (init_page_context(), __toCommonJS(page_context_exports));
|
|
3618
|
+
const page = usePageProps();
|
|
3619
|
+
const site = useSiteConfig();
|
|
3620
|
+
return { __html: `<!DOCTYPE html>
|
|
3621
|
+
<html lang="en">
|
|
3622
|
+
<head>
|
|
3623
|
+
<meta charset="UTF-8">
|
|
3624
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3625
|
+
<title>${escapeHtml(page.title)} - ${escapeHtml(site.name)}</title>
|
|
3626
|
+
${page.description ? `<meta name="description" content="${escapeHtml(page.description)}">` : ""}
|
|
3627
|
+
<style>
|
|
3628
|
+
:root {
|
|
3629
|
+
--octc-color-primary: #4f6fae;
|
|
3630
|
+
--octc-color-text: #131a30;
|
|
3631
|
+
--octc-color-bg: #ffffff;
|
|
3632
|
+
--octc-color-bg-alt: #f5f7fb;
|
|
3633
|
+
--octc-color-text-muted: #4f607b;
|
|
3634
|
+
--octc-color-border: #d2dbea;
|
|
3635
|
+
}
|
|
3636
|
+
body {
|
|
3637
|
+
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
3638
|
+
line-height: 1.7;
|
|
3639
|
+
color: var(--octc-color-text);
|
|
3640
|
+
background: var(--octc-color-bg);
|
|
3641
|
+
max-width: 800px;
|
|
3642
|
+
margin: 0 auto;
|
|
3643
|
+
padding: 2rem;
|
|
3644
|
+
}
|
|
3645
|
+
a { color: var(--octc-color-primary); }
|
|
3646
|
+
</style>
|
|
3647
|
+
</head>
|
|
3648
|
+
<body>
|
|
3649
|
+
<header>
|
|
3650
|
+
<h1>${escapeHtml(site.name)}</h1>
|
|
3651
|
+
</header>
|
|
3652
|
+
<main>
|
|
3653
|
+
${children.__html}
|
|
3654
|
+
</main>
|
|
3655
|
+
</body>
|
|
3656
|
+
</html>` };
|
|
3526
3657
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
*/
|
|
3530
|
-
function buildThemeNavItems(sidebar, base, extension) {
|
|
3531
|
-
return importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
|
|
3658
|
+
function escapeHtml(str) {
|
|
3659
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3532
3660
|
}
|
|
3533
3661
|
/**
|
|
3534
|
-
*
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3662
|
+
* Creates a theme with layout switching support.
|
|
3663
|
+
*
|
|
3664
|
+
* @example
|
|
3665
|
+
* ```tsx
|
|
3666
|
+
* import { createTheme } from '@ox-content/vite-plugin';
|
|
3667
|
+
* import { DefaultLayout } from './layouts/Default';
|
|
3668
|
+
* import { EntryLayout } from './layouts/Entry';
|
|
3669
|
+
*
|
|
3670
|
+
* export default createTheme({
|
|
3671
|
+
* layouts: {
|
|
3672
|
+
* default: DefaultLayout,
|
|
3673
|
+
* entry: EntryLayout,
|
|
3674
|
+
* },
|
|
3675
|
+
* });
|
|
3676
|
+
* ```
|
|
3677
|
+
*/
|
|
3678
|
+
function createTheme(config) {
|
|
3679
|
+
const { layouts, defaultLayout = "default" } = config;
|
|
3680
|
+
return function ThemeWithLayouts({ children }) {
|
|
3681
|
+
const layoutName = usePageProps().layout ?? defaultLayout;
|
|
3682
|
+
const Layout = layouts[layoutName] ?? layouts[defaultLayout];
|
|
3683
|
+
if (!Layout) throw new Error(`[ox-content] Layout "${layoutName}" not found. Available layouts: ${Object.keys(layouts).join(", ")}`);
|
|
3684
|
+
return Layout({ children });
|
|
3556
3685
|
};
|
|
3557
3686
|
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3687
|
+
//#endregion
|
|
3688
|
+
//#region src/ssg.ts
|
|
3689
|
+
/**
|
|
3690
|
+
* SSG (Static Site Generation) module for ox-content
|
|
3691
|
+
*/
|
|
3692
|
+
/**
|
|
3693
|
+
* Deprecated compatibility export for consumers that imported the former
|
|
3694
|
+
* TypeScript SSG template. HTML generation is Rust-backed now.
|
|
3695
|
+
*
|
|
3696
|
+
* @deprecated Use `generateHtmlPage`/`buildSsg` instead.
|
|
3697
|
+
*/
|
|
3698
|
+
const DEFAULT_HTML_TEMPLATE = "<!-- ox-content default HTML template is Rust-backed -->";
|
|
3699
|
+
/**
|
|
3700
|
+
* Resolves SSG options with defaults.
|
|
3701
|
+
*/
|
|
3702
|
+
function resolveSsgOptions(ssg) {
|
|
3703
|
+
if (ssg === false) return {
|
|
3704
|
+
enabled: false,
|
|
3705
|
+
extension: ".html",
|
|
3706
|
+
clean: false,
|
|
3707
|
+
bare: false,
|
|
3708
|
+
generateOgImage: false,
|
|
3709
|
+
lastUpdated: false
|
|
3710
|
+
};
|
|
3711
|
+
if (ssg === true || ssg === void 0) return {
|
|
3712
|
+
enabled: true,
|
|
3713
|
+
extension: ".html",
|
|
3714
|
+
clean: false,
|
|
3715
|
+
bare: false,
|
|
3716
|
+
generateOgImage: false,
|
|
3717
|
+
lastUpdated: false,
|
|
3718
|
+
theme: resolveTheme(void 0)
|
|
3719
|
+
};
|
|
3570
3720
|
return {
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3721
|
+
enabled: ssg.enabled ?? true,
|
|
3722
|
+
extension: ssg.extension ?? ".html",
|
|
3723
|
+
clean: ssg.clean ?? false,
|
|
3724
|
+
bare: ssg.bare ?? false,
|
|
3725
|
+
render: ssg.render,
|
|
3726
|
+
lang: ssg.lang,
|
|
3727
|
+
head: ssg.head,
|
|
3728
|
+
bodyStart: ssg.bodyStart,
|
|
3729
|
+
bodyEnd: ssg.bodyEnd,
|
|
3730
|
+
siteName: ssg.siteName,
|
|
3731
|
+
ogImage: ssg.ogImage,
|
|
3732
|
+
generateOgImage: ssg.generateOgImage ?? false,
|
|
3733
|
+
lastUpdated: ssg.lastUpdated ?? false,
|
|
3734
|
+
siteUrl: ssg.siteUrl,
|
|
3735
|
+
theme: resolveTheme(ssg.theme),
|
|
3736
|
+
navigation: ssg.navigation
|
|
3581
3737
|
};
|
|
3582
3738
|
}
|
|
3583
3739
|
/**
|
|
3584
|
-
*
|
|
3585
|
-
*
|
|
3586
|
-
* `ssg.bare` deliberately does not turn this off. Bare mode only drops the
|
|
3587
|
-
* generated page shell, and bringing your own shell is exactly the case where
|
|
3588
|
-
* per-page OG images are still wanted — the images are written to the output
|
|
3589
|
-
* tree and the consumer injects the `<meta>` tags itself. Nothing in the bare
|
|
3590
|
-
* HTML references them, because bare output has no `<head>` to put them in.
|
|
3740
|
+
* Extracts title from content or frontmatter.
|
|
3591
3741
|
*/
|
|
3592
|
-
function
|
|
3593
|
-
return
|
|
3742
|
+
function extractTitle$1(content, frontmatter) {
|
|
3743
|
+
return importNapiModuleSync().extractSsgTitle(content, typeof frontmatter.title === "string" ? frontmatter.title : void 0);
|
|
3594
3744
|
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3745
|
+
/**
|
|
3746
|
+
* Generates a bare HTML page carrying head metadata and injected markup.
|
|
3747
|
+
*
|
|
3748
|
+
* Bare mode leaves the shell to the consumer, but the metadata here is
|
|
3749
|
+
* already computed for the themed page and cannot be recovered afterwards —
|
|
3750
|
+
* the generated OG image in particular was only discoverable by guessing at
|
|
3751
|
+
* the output directory. A page with none of it set renders exactly what bare
|
|
3752
|
+
* mode emitted before, which keeps the no-JS size baseline honest.
|
|
3753
|
+
*/
|
|
3754
|
+
function generateBarePage(page) {
|
|
3755
|
+
return importNapiModuleSync().generateSsgBarePage(page);
|
|
3604
3756
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3757
|
+
/**
|
|
3758
|
+
* Per-build cache for the Rust-facing nav conversion. `navGroups` is the same
|
|
3759
|
+
* `context.navItems` reference for every page in a build, so the deep recursive
|
|
3760
|
+
* copy below only needs to run once per build instead of once per page.
|
|
3761
|
+
*/
|
|
3762
|
+
const navGroupsForRustCache = /* @__PURE__ */ new WeakMap();
|
|
3763
|
+
function toRustNavItem(item) {
|
|
3764
|
+
return {
|
|
3765
|
+
title: item.title,
|
|
3766
|
+
path: item.path,
|
|
3767
|
+
href: item.href,
|
|
3768
|
+
children: item.children?.map(toRustNavItem),
|
|
3769
|
+
collapsed: item.collapsed,
|
|
3770
|
+
stickyCollapsed: item.stickyCollapsed
|
|
3612
3771
|
};
|
|
3613
|
-
for (const inputPath of markdownFiles) try {
|
|
3614
|
-
const pageResult = await transformSsgPage(context, inputPath);
|
|
3615
|
-
collected.pageResults.push(pageResult);
|
|
3616
|
-
collectOgImageEntry(context, pageResult, collected);
|
|
3617
|
-
} catch (err) {
|
|
3618
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3619
|
-
collected.errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
|
|
3620
|
-
}
|
|
3621
|
-
return collected;
|
|
3622
3772
|
}
|
|
3623
|
-
|
|
3624
|
-
const
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3773
|
+
function convertNavGroupsForRust(navGroups) {
|
|
3774
|
+
const cached = navGroupsForRustCache.get(navGroups);
|
|
3775
|
+
if (cached) return cached;
|
|
3776
|
+
const converted = navGroups.map((group) => ({
|
|
3777
|
+
title: group.title,
|
|
3778
|
+
collapsed: group.collapsed,
|
|
3779
|
+
stickyCollapsed: group.stickyCollapsed,
|
|
3780
|
+
items: group.items.map(toRustNavItem)
|
|
3781
|
+
}));
|
|
3782
|
+
navGroupsForRustCache.set(navGroups, converted);
|
|
3783
|
+
return converted;
|
|
3784
|
+
}
|
|
3785
|
+
/**
|
|
3786
|
+
* Converts a `TocEntry` tree into the plain shape the Rust binding expects.
|
|
3787
|
+
* Hoisted to module scope so it isn't reallocated for every page; the
|
|
3788
|
+
* per-page `.map` over `pageData.toc` still runs since the TOC is page-specific.
|
|
3789
|
+
*/
|
|
3790
|
+
function toRustTocEntry(entry) {
|
|
3632
3791
|
return {
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
description: frontmatter.description,
|
|
3638
|
-
lastUpdated: context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0,
|
|
3639
|
-
frontmatter,
|
|
3640
|
-
toc: result.toc
|
|
3792
|
+
depth: entry.depth,
|
|
3793
|
+
text: entry.text,
|
|
3794
|
+
slug: entry.slug,
|
|
3795
|
+
children: entry.children?.map(toRustTocEntry) ?? []
|
|
3641
3796
|
};
|
|
3642
3797
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
3660
|
-
return restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
3798
|
+
/**
|
|
3799
|
+
* Per-build cache for the Rust-facing locale list. `i18n.locales` is the same
|
|
3800
|
+
* reference for every page in a build, so this mapping (and the `?? "ltr"`
|
|
3801
|
+
* default) only runs once per build instead of once per page.
|
|
3802
|
+
*/
|
|
3803
|
+
const rustLocalesCache = /* @__PURE__ */ new WeakMap();
|
|
3804
|
+
function toRustLocales(locales) {
|
|
3805
|
+
const cached = rustLocalesCache.get(locales);
|
|
3806
|
+
if (cached) return cached;
|
|
3807
|
+
const converted = locales.map((locale) => ({
|
|
3808
|
+
code: locale.code,
|
|
3809
|
+
name: locale.name,
|
|
3810
|
+
dir: locale.dir ?? "ltr"
|
|
3811
|
+
}));
|
|
3812
|
+
rustLocalesCache.set(locales, converted);
|
|
3813
|
+
return converted;
|
|
3661
3814
|
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
collected.ogImageInputPaths.push(pageResult.inputPath);
|
|
3675
|
-
collected.ogImageUrlMap.set(pageResult.inputPath, pageResult.routePaths.ogImageUrl);
|
|
3815
|
+
/**
|
|
3816
|
+
* Per-build cache for the locale-code list passed to `getSsgPageLocale`. The
|
|
3817
|
+
* `i18n.locales` reference is stable across a build, so the `.map` to codes
|
|
3818
|
+
* runs once instead of once per page.
|
|
3819
|
+
*/
|
|
3820
|
+
const localeCodesCache = /* @__PURE__ */ new WeakMap();
|
|
3821
|
+
function localeCodesFor(locales) {
|
|
3822
|
+
const cached = localeCodesCache.get(locales);
|
|
3823
|
+
if (cached) return cached;
|
|
3824
|
+
const codes = locales.map((locale) => locale.code);
|
|
3825
|
+
localeCodesCache.set(locales, codes);
|
|
3826
|
+
return codes;
|
|
3676
3827
|
}
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
inputPath: pageResult.inputPath,
|
|
3716
|
-
outputPath: pageResult.routePaths.outputPath,
|
|
3717
|
-
html: await renderSsgPage(context, pageResult, collected.ogImageUrlMap)
|
|
3718
|
-
});
|
|
3719
|
-
} catch (err) {
|
|
3720
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3721
|
-
errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
|
|
3722
|
-
}
|
|
3723
|
-
return generatedPages;
|
|
3724
|
-
}
|
|
3725
|
-
async function renderSsgPage(context, pageResult, ogImageUrlMap) {
|
|
3726
|
-
if (context.ssgOptions.bare) return generateBareHtmlPage(pageResult.transformedHtml, pageResult.title);
|
|
3727
|
-
const pageData = createSsgPageData(pageResult);
|
|
3728
|
-
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
3729
|
-
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);
|
|
3730
|
-
}
|
|
3731
|
-
function createSsgPageData(pageResult) {
|
|
3732
|
-
const { frontmatter } = pageResult;
|
|
3733
|
-
const entryPage = frontmatter.layout === "entry" ? {
|
|
3734
|
-
hero: frontmatter.hero,
|
|
3735
|
-
features: frontmatter.features
|
|
3828
|
+
/**
|
|
3829
|
+
* Generates HTML page with navigation using Rust NAPI bindings.
|
|
3830
|
+
*/
|
|
3831
|
+
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales) {
|
|
3832
|
+
const mod = await importNapiModule();
|
|
3833
|
+
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
3834
|
+
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
3835
|
+
const themeForRust = theme ? themeToNapi(theme) : void 0;
|
|
3836
|
+
const entryPageForRust = pageData.entryPage ? {
|
|
3837
|
+
hero: pageData.entryPage.hero ? {
|
|
3838
|
+
name: pageData.entryPage.hero.name,
|
|
3839
|
+
text: pageData.entryPage.hero.text,
|
|
3840
|
+
tagline: pageData.entryPage.hero.tagline,
|
|
3841
|
+
notice: pageData.entryPage.hero.notice ? {
|
|
3842
|
+
title: pageData.entryPage.hero.notice.title,
|
|
3843
|
+
body: pageData.entryPage.hero.notice.body
|
|
3844
|
+
} : void 0,
|
|
3845
|
+
image: pageData.entryPage.hero.image ? {
|
|
3846
|
+
src: pageData.entryPage.hero.image.src,
|
|
3847
|
+
lightSrc: pageData.entryPage.hero.image.lightSrc,
|
|
3848
|
+
darkSrc: pageData.entryPage.hero.image.darkSrc,
|
|
3849
|
+
alt: pageData.entryPage.hero.image.alt,
|
|
3850
|
+
width: pageData.entryPage.hero.image.width,
|
|
3851
|
+
height: pageData.entryPage.hero.image.height
|
|
3852
|
+
} : void 0,
|
|
3853
|
+
actions: pageData.entryPage.hero.actions?.map((a) => ({
|
|
3854
|
+
theme: a.theme,
|
|
3855
|
+
text: a.text,
|
|
3856
|
+
link: a.link
|
|
3857
|
+
}))
|
|
3858
|
+
} : void 0,
|
|
3859
|
+
features: pageData.entryPage.features?.map((f) => ({
|
|
3860
|
+
icon: f.icon,
|
|
3861
|
+
title: f.title,
|
|
3862
|
+
details: f.details,
|
|
3863
|
+
link: f.link,
|
|
3864
|
+
linkText: f.linkText
|
|
3865
|
+
}))
|
|
3736
3866
|
} : void 0;
|
|
3867
|
+
return mod.generateSsgHtml({
|
|
3868
|
+
title: pageData.title,
|
|
3869
|
+
description: pageData.description,
|
|
3870
|
+
content: pageData.content,
|
|
3871
|
+
toc: tocForRust,
|
|
3872
|
+
lastUpdated: pageData.lastUpdated,
|
|
3873
|
+
path: pageData.path,
|
|
3874
|
+
entryPage: entryPageForRust
|
|
3875
|
+
}, navGroupsForRust, {
|
|
3876
|
+
siteName,
|
|
3877
|
+
base,
|
|
3878
|
+
ogImage,
|
|
3879
|
+
theme: themeForRust,
|
|
3880
|
+
locale,
|
|
3881
|
+
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
3885
|
+
const optimized = (await importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
3886
|
+
await Promise.all(optimized.assets.map(async (asset) => {
|
|
3887
|
+
await fs$2.mkdir(path$2.dirname(asset.outputPath), { recursive: true });
|
|
3888
|
+
await fs$2.writeFile(asset.outputPath, asset.content, "utf-8");
|
|
3889
|
+
}));
|
|
3737
3890
|
return {
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
content: pageResult.transformedHtml,
|
|
3741
|
-
toc: pageResult.toc,
|
|
3742
|
-
lastUpdated: pageResult.lastUpdated,
|
|
3743
|
-
frontmatter,
|
|
3744
|
-
path: pageResult.routePaths.urlPath,
|
|
3745
|
-
href: pageResult.routePaths.href,
|
|
3746
|
-
entryPage
|
|
3891
|
+
pages: optimized.pages,
|
|
3892
|
+
assets: optimized.assets.map((asset) => asset.outputPath)
|
|
3747
3893
|
};
|
|
3748
3894
|
}
|
|
3749
|
-
async function writeGeneratedPages(generatedPages, context, generatedFiles) {
|
|
3750
|
-
const optimizedOutput = await externalizeSharedPageAssets(generatedPages, context.outDir, context.base);
|
|
3751
|
-
generatedFiles.push(...optimizedOutput.assets);
|
|
3752
|
-
for (const page of optimizedOutput.pages) {
|
|
3753
|
-
await fs$2.mkdir(path$2.dirname(page.outputPath), { recursive: true });
|
|
3754
|
-
await fs$2.writeFile(page.outputPath, page.html, "utf-8");
|
|
3755
|
-
generatedFiles.push(page.outputPath);
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
//#endregion
|
|
3759
|
-
//#region src/search.ts
|
|
3760
3895
|
/**
|
|
3761
|
-
*
|
|
3762
|
-
*
|
|
3763
|
-
* Generates search index at build time and provides client-side search.
|
|
3896
|
+
* Converts a markdown file path to a relative URL path.
|
|
3764
3897
|
*/
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
if (!oxContent$1) try {
|
|
3768
|
-
oxContent$1 = await importNapiModule();
|
|
3769
|
-
} catch {
|
|
3770
|
-
console.warn("[ox-content] Native bindings not available, search disabled");
|
|
3771
|
-
return null;
|
|
3772
|
-
}
|
|
3773
|
-
return oxContent$1;
|
|
3898
|
+
function getUrlPath$1(inputPath, srcDir) {
|
|
3899
|
+
return importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
|
|
3774
3900
|
}
|
|
3775
3901
|
/**
|
|
3776
|
-
* Resolves
|
|
3902
|
+
* Resolves manual navigation config to the format used by the built-in SSG renderer.
|
|
3777
3903
|
*/
|
|
3778
|
-
function
|
|
3779
|
-
if (
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
return
|
|
3788
|
-
enabled: opts.enabled ?? true,
|
|
3789
|
-
limit: opts.limit ?? 10,
|
|
3790
|
-
prefix: opts.prefix ?? true,
|
|
3791
|
-
placeholder: opts.placeholder ?? "Search documentation...",
|
|
3792
|
-
hotkey: opts.hotkey ?? "/"
|
|
3793
|
-
};
|
|
3904
|
+
function resolveNavigationGroups(navigation, base, extension) {
|
|
3905
|
+
if (!navigation) return;
|
|
3906
|
+
return importNapiModuleSync().resolveSsgNavigationGroups(navigation, base, extension);
|
|
3907
|
+
}
|
|
3908
|
+
function getPageLocale(urlPath, i18n) {
|
|
3909
|
+
if (!i18n) return void 0;
|
|
3910
|
+
return importNapiModuleSync().getSsgPageLocale(urlPath, i18n.defaultLocale, localeCodesFor(i18n.locales)) ?? void 0;
|
|
3911
|
+
}
|
|
3912
|
+
function getRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl) {
|
|
3913
|
+
return importNapiModuleSync().resolveSsgRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl);
|
|
3794
3914
|
}
|
|
3795
3915
|
/**
|
|
3796
|
-
*
|
|
3916
|
+
* Formats a file/dir name as a title.
|
|
3797
3917
|
*/
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
if (!napi) return JSON.stringify({
|
|
3801
|
-
documents: [],
|
|
3802
|
-
index: {},
|
|
3803
|
-
df: {},
|
|
3804
|
-
avg_dl: 0,
|
|
3805
|
-
doc_count: 0
|
|
3806
|
-
});
|
|
3807
|
-
return napi.buildSearchIndexFromDirectory(srcDir, base, [...extensions]);
|
|
3918
|
+
function formatTitle(name) {
|
|
3919
|
+
return importNapiModuleSync().formatSsgTitle(name);
|
|
3808
3920
|
}
|
|
3809
3921
|
/**
|
|
3810
|
-
*
|
|
3922
|
+
* Collects all markdown files from the source directory.
|
|
3811
3923
|
*/
|
|
3812
|
-
async function
|
|
3813
|
-
|
|
3814
|
-
if (!napi) return;
|
|
3815
|
-
napi.writeSearchIndex(indexJson, outDir);
|
|
3924
|
+
async function collectMarkdownFiles(srcDir, extensions = DEFAULT_MARKDOWN_EXTENSIONS) {
|
|
3925
|
+
return importNapiModuleSync().collectSsgMarkdownFiles(srcDir, [...extensions]);
|
|
3816
3926
|
}
|
|
3817
3927
|
/**
|
|
3818
|
-
*
|
|
3819
|
-
* This is injected into the bundle as a virtual module.
|
|
3928
|
+
* Builds navigation items from markdown files, grouped by directory.
|
|
3820
3929
|
*/
|
|
3821
|
-
function
|
|
3822
|
-
return importNapiModuleSync().
|
|
3930
|
+
function buildNavItems(markdownFiles, srcDir, base, extension) {
|
|
3931
|
+
return importNapiModuleSync().buildSsgNavItems(markdownFiles, srcDir, base, extension);
|
|
3823
3932
|
}
|
|
3824
|
-
//#endregion
|
|
3825
|
-
//#region src/dev-server.ts
|
|
3826
3933
|
/**
|
|
3827
|
-
*
|
|
3828
|
-
*
|
|
3829
|
-
* Serves fully-rendered HTML pages (with navigation, theme, etc.)
|
|
3830
|
-
* during `vite dev`, matching the SSG build output.
|
|
3934
|
+
* Builds navigation items from an explicit theme sidebar tree.
|
|
3831
3935
|
*/
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
".ts",
|
|
3836
|
-
".css",
|
|
3837
|
-
".scss",
|
|
3838
|
-
".less",
|
|
3839
|
-
".svg",
|
|
3840
|
-
".png",
|
|
3841
|
-
".jpg",
|
|
3842
|
-
".jpeg",
|
|
3843
|
-
".gif",
|
|
3844
|
-
".webp",
|
|
3845
|
-
".ico",
|
|
3846
|
-
".woff",
|
|
3847
|
-
".woff2",
|
|
3848
|
-
".ttf",
|
|
3849
|
-
".eot",
|
|
3850
|
-
".json",
|
|
3851
|
-
".map",
|
|
3852
|
-
".mp4",
|
|
3853
|
-
".webm",
|
|
3854
|
-
".mp3",
|
|
3855
|
-
".pdf"
|
|
3856
|
-
]);
|
|
3857
|
-
/** Vite internal URL prefixes to skip. */
|
|
3858
|
-
const VITE_INTERNAL_PREFIXES = [
|
|
3859
|
-
"/@vite/",
|
|
3860
|
-
"/@fs/",
|
|
3861
|
-
"/@id/",
|
|
3862
|
-
"/__"
|
|
3863
|
-
];
|
|
3936
|
+
function buildThemeNavItems(sidebar, base, extension) {
|
|
3937
|
+
return importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
|
|
3938
|
+
}
|
|
3864
3939
|
/**
|
|
3865
|
-
*
|
|
3940
|
+
* Builds all markdown files to static HTML.
|
|
3866
3941
|
*/
|
|
3867
|
-
function
|
|
3868
|
-
|
|
3869
|
-
if (
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
await fs$2.access(filePath);
|
|
3891
|
-
return filePath;
|
|
3892
|
-
} catch {}
|
|
3893
|
-
}
|
|
3894
|
-
for (const extension of extensions) {
|
|
3895
|
-
const indexPath = path$2.join(srcDir, routePath, `index${extension}`);
|
|
3896
|
-
try {
|
|
3897
|
-
await fs$2.access(indexPath);
|
|
3898
|
-
return indexPath;
|
|
3899
|
-
} catch {}
|
|
3900
|
-
}
|
|
3901
|
-
return null;
|
|
3942
|
+
async function buildSsg(options, root) {
|
|
3943
|
+
const ssgOptions = options.ssg;
|
|
3944
|
+
if (!ssgOptions.enabled) return {
|
|
3945
|
+
files: [],
|
|
3946
|
+
errors: [],
|
|
3947
|
+
ogImages: {}
|
|
3948
|
+
};
|
|
3949
|
+
const srcDir = path$2.resolve(root, options.srcDir);
|
|
3950
|
+
const outDir = path$2.resolve(root, options.outDir);
|
|
3951
|
+
const generatedFiles = [];
|
|
3952
|
+
const errors = [];
|
|
3953
|
+
await cleanOutputDirectory(ssgOptions, outDir);
|
|
3954
|
+
const markdownFiles = await collectMarkdownFiles(srcDir, options.extensions);
|
|
3955
|
+
const context = await createBuildSsgContext(options, root, srcDir, outDir, markdownFiles);
|
|
3956
|
+
const collected = await collectPageResults(context, markdownFiles);
|
|
3957
|
+
errors.push(...collected.errors);
|
|
3958
|
+
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
3959
|
+
await writeGeneratedPages(await generateHtmlPages(context, collected.pageResults, collected, errors), context, generatedFiles);
|
|
3960
|
+
return {
|
|
3961
|
+
files: generatedFiles,
|
|
3962
|
+
errors,
|
|
3963
|
+
ogImages: Object.fromEntries(collected.ogImageUrlMap)
|
|
3964
|
+
};
|
|
3902
3965
|
}
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3966
|
+
async function cleanOutputDirectory(ssgOptions, outDir) {
|
|
3967
|
+
if (!ssgOptions.clean) return;
|
|
3968
|
+
try {
|
|
3969
|
+
await fs$2.rm(outDir, {
|
|
3970
|
+
recursive: true,
|
|
3971
|
+
force: true
|
|
3972
|
+
});
|
|
3973
|
+
} catch {}
|
|
3908
3974
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
function createDevServerCache() {
|
|
3975
|
+
async function createBuildSsgContext(options, root, srcDir, outDir, markdownFiles) {
|
|
3976
|
+
const ssgOptions = options.ssg;
|
|
3977
|
+
const base = options.base.endsWith("/") ? options.base : options.base + "/";
|
|
3913
3978
|
return {
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3979
|
+
options,
|
|
3980
|
+
ssgOptions,
|
|
3981
|
+
root,
|
|
3982
|
+
srcDir,
|
|
3983
|
+
outDir,
|
|
3984
|
+
base,
|
|
3985
|
+
navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
|
|
3986
|
+
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
3987
|
+
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
3988
|
+
napi: ssgOptions.lastUpdated ? await importNapiModule() : void 0
|
|
3917
3989
|
};
|
|
3918
3990
|
}
|
|
3919
3991
|
/**
|
|
3920
|
-
*
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
* Invalidate page cache for a specific file (called on file change).
|
|
3992
|
+
* Whether this build emits one Open Graph image per page.
|
|
3993
|
+
*
|
|
3994
|
+
* `ssg.bare` deliberately does not turn this off. Bare mode only drops the
|
|
3995
|
+
* generated page shell, and bringing your own shell is exactly the case where
|
|
3996
|
+
* per-page OG images are still wanted — the images are written to the output
|
|
3997
|
+
* tree and the consumer injects the `<meta>` tags itself. Nothing in the bare
|
|
3998
|
+
* HTML references them, because bare output has no `<head>` to put them in.
|
|
3928
3999
|
*/
|
|
3929
|
-
function
|
|
3930
|
-
|
|
4000
|
+
function shouldGenerateOgImages(options) {
|
|
4001
|
+
return options.ogImage || options.ssg.generateOgImage;
|
|
3931
4002
|
}
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
*/
|
|
3935
|
-
async function resolveSiteName(options, root) {
|
|
3936
|
-
if (options.ssg.siteName) return options.ssg.siteName;
|
|
4003
|
+
async function resolveSiteName$1(root, ssgOptions) {
|
|
4004
|
+
if (ssgOptions.siteName) return ssgOptions.siteName;
|
|
3937
4005
|
try {
|
|
3938
4006
|
const pkgPath = path$2.join(root, "package.json");
|
|
3939
4007
|
const pkg = JSON.parse(await fs$2.readFile(pkgPath, "utf-8"));
|
|
3940
|
-
|
|
3941
|
-
} catch {
|
|
3942
|
-
|
|
4008
|
+
return pkg.name ? formatTitle(pkg.name) : "Documentation";
|
|
4009
|
+
} catch {
|
|
4010
|
+
return "Documentation";
|
|
4011
|
+
}
|
|
3943
4012
|
}
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4013
|
+
async function collectPageResults(context, markdownFiles) {
|
|
4014
|
+
const collected = {
|
|
4015
|
+
pageResults: [],
|
|
4016
|
+
ogImageEntries: [],
|
|
4017
|
+
ogImageInputPaths: [],
|
|
4018
|
+
ogImageUrlMap: /* @__PURE__ */ new Map(),
|
|
4019
|
+
errors: []
|
|
4020
|
+
};
|
|
4021
|
+
for (const inputPath of markdownFiles) try {
|
|
4022
|
+
const pageResult = await transformSsgPage(context, inputPath);
|
|
4023
|
+
collected.pageResults.push(pageResult);
|
|
4024
|
+
collectOgImageEntry(context, pageResult, collected);
|
|
4025
|
+
} catch (err) {
|
|
4026
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4027
|
+
collected.errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
|
|
4028
|
+
}
|
|
4029
|
+
return collected;
|
|
4030
|
+
}
|
|
4031
|
+
async function transformSsgPage(context, inputPath) {
|
|
4032
|
+
const result = await transformMarkdown(await fs$2.readFile(inputPath, "utf-8"), inputPath, context.options, {
|
|
3952
4033
|
convertMdLinks: true,
|
|
3953
|
-
baseUrl: base,
|
|
3954
|
-
sourcePath:
|
|
4034
|
+
baseUrl: context.base,
|
|
4035
|
+
sourcePath: inputPath
|
|
3955
4036
|
});
|
|
3956
4037
|
const frontmatter = normalizeVitePressFrontmatter(result.frontmatter);
|
|
3957
|
-
|
|
3958
|
-
const
|
|
3959
|
-
|
|
3960
|
-
|
|
4038
|
+
const transformedHtml = await transformSsgHtml(result.html, context.options);
|
|
4039
|
+
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
4040
|
+
return {
|
|
4041
|
+
inputPath,
|
|
4042
|
+
routePaths: getRoutePaths(inputPath, context.srcDir, context.outDir, context.base, context.ssgOptions.extension, context.ssgOptions.siteUrl),
|
|
4043
|
+
transformedHtml,
|
|
4044
|
+
title,
|
|
4045
|
+
description: frontmatter.description,
|
|
4046
|
+
lastUpdated: context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0,
|
|
4047
|
+
frontmatter,
|
|
4048
|
+
toc: result.toc
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
async function transformSsgHtml(html, options) {
|
|
4052
|
+
const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(html);
|
|
4053
|
+
let transformedHtml = await transformAllPlugins(protectedHtml, {
|
|
3961
4054
|
tabs: true,
|
|
3962
4055
|
youtube: true,
|
|
3963
4056
|
github: options.embeds.github,
|
|
@@ -3972,83 +4065,443 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root)
|
|
|
3972
4065
|
githubToken: process.env.GITHUB_TOKEN
|
|
3973
4066
|
});
|
|
3974
4067
|
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
3975
|
-
|
|
3976
|
-
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
3977
|
-
const description = frontmatter.description;
|
|
3978
|
-
let entryPage;
|
|
3979
|
-
if (frontmatter.layout === "entry") entryPage = {
|
|
3980
|
-
hero: frontmatter.hero,
|
|
3981
|
-
features: frontmatter.features
|
|
3982
|
-
};
|
|
3983
|
-
let html = await generateHtmlPage({
|
|
3984
|
-
title,
|
|
3985
|
-
description,
|
|
3986
|
-
content: transformedHtml,
|
|
3987
|
-
toc: result.toc,
|
|
3988
|
-
frontmatter,
|
|
3989
|
-
path: getUrlPath$1(filePath, srcDir),
|
|
3990
|
-
href: getUrlPath$1(filePath, srcDir) || "/",
|
|
3991
|
-
entryPage
|
|
3992
|
-
}, navGroups, siteName, base, options.ssg.ogImage, options.ssg.theme);
|
|
3993
|
-
html = injectViteHmrClient(html);
|
|
3994
|
-
return html;
|
|
4068
|
+
return restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
3995
4069
|
}
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
try {
|
|
4011
|
-
const cached = cache.pages.get(filePath);
|
|
4012
|
-
if (cached) {
|
|
4013
|
-
res.setHeader("Content-Type", "text/html");
|
|
4014
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
4015
|
-
res.end(cached);
|
|
4016
|
-
return;
|
|
4017
|
-
}
|
|
4018
|
-
if (!cache.siteName) cache.siteName = await resolveSiteName(options, root);
|
|
4019
|
-
if (!cache.navGroups) {
|
|
4020
|
-
const markdownFiles = await collectMarkdownFiles(srcDir, options.extensions);
|
|
4021
|
-
cache.navGroups = resolveNavigationGroups(options.ssg.navigation, base, options.ssg.extension) ?? (options.ssg.theme?.sidebar.length ? buildThemeNavItems(options.ssg.theme.sidebar, base, options.ssg.extension) : buildNavItems(markdownFiles, srcDir, base, options.ssg.extension));
|
|
4022
|
-
}
|
|
4023
|
-
const html = await renderPage$1(filePath, options, cache.navGroups, cache.siteName, base, root);
|
|
4024
|
-
cache.pages.set(filePath, html);
|
|
4025
|
-
res.setHeader("Content-Type", "text/html");
|
|
4026
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
4027
|
-
res.end(html);
|
|
4028
|
-
} catch (err) {
|
|
4029
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4030
|
-
console.error(`[ox-content:dev] Failed to render ${filePath}:`, message);
|
|
4031
|
-
next();
|
|
4032
|
-
}
|
|
4033
|
-
};
|
|
4070
|
+
function collectOgImageEntry(context, pageResult, collected) {
|
|
4071
|
+
if (!context.shouldGenerateOgImages) return;
|
|
4072
|
+
const { layout: _layout, ...frontmatterRest } = pageResult.frontmatter;
|
|
4073
|
+
collected.ogImageEntries.push({
|
|
4074
|
+
props: {
|
|
4075
|
+
...frontmatterRest,
|
|
4076
|
+
title: pageResult.title,
|
|
4077
|
+
description: pageResult.description,
|
|
4078
|
+
siteName: context.siteName
|
|
4079
|
+
},
|
|
4080
|
+
outputPath: pageResult.routePaths.ogImagePath
|
|
4081
|
+
});
|
|
4082
|
+
collected.ogImageInputPaths.push(pageResult.inputPath);
|
|
4083
|
+
collected.ogImageUrlMap.set(pageResult.inputPath, pageResult.routePaths.ogImageUrl);
|
|
4034
4084
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
for (const
|
|
4050
|
-
|
|
4051
|
-
|
|
4085
|
+
async function generateOgImageAssets(context, collected, generatedFiles, errors) {
|
|
4086
|
+
if (!context.shouldGenerateOgImages || collected.ogImageEntries.length === 0) return;
|
|
4087
|
+
try {
|
|
4088
|
+
const ogResults = await generateOgImages(collected.ogImageEntries, context.options.ogImageOptions, context.root);
|
|
4089
|
+
if (clearMissingBrowserOgImages(ogResults, collected)) return;
|
|
4090
|
+
reportOgImageResults(ogResults, collected, generatedFiles, errors);
|
|
4091
|
+
} catch (err) {
|
|
4092
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4093
|
+
console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
|
|
4094
|
+
collected.ogImageUrlMap.clear();
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
function clearMissingBrowserOgImages(ogResults, collected) {
|
|
4098
|
+
if (!(ogResults.length > 0 && ogResults.every((result) => result.error === "Chromium not available"))) return false;
|
|
4099
|
+
for (const inputPath of collected.ogImageInputPaths) collected.ogImageUrlMap.delete(inputPath);
|
|
4100
|
+
return true;
|
|
4101
|
+
}
|
|
4102
|
+
function reportOgImageResults(ogResults, collected, generatedFiles, errors) {
|
|
4103
|
+
let ogSuccessCount = 0;
|
|
4104
|
+
for (let i = 0; i < ogResults.length; i++) {
|
|
4105
|
+
const result = ogResults[i];
|
|
4106
|
+
if (result.error) {
|
|
4107
|
+
errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
|
|
4108
|
+
collected.ogImageUrlMap.delete(collected.ogImageInputPaths[i]);
|
|
4109
|
+
} else {
|
|
4110
|
+
generatedFiles.push(result.outputPath);
|
|
4111
|
+
ogSuccessCount++;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
if (ogSuccessCount > 0) {
|
|
4115
|
+
const cachedCount = ogResults.filter((result) => result.cached && !result.error).length;
|
|
4116
|
+
console.log(`[ox-content:og-image] Generated ${ogSuccessCount} OG images` + (cachedCount > 0 ? ` (${cachedCount} from cache)` : ""));
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
4120
|
+
const generatedPages = [];
|
|
4121
|
+
for (const pageResult of pageResults) try {
|
|
4122
|
+
generatedPages.push({
|
|
4123
|
+
inputPath: pageResult.inputPath,
|
|
4124
|
+
outputPath: pageResult.routePaths.outputPath,
|
|
4125
|
+
html: await renderSsgPage(context, pageResult, collected, pageResults)
|
|
4126
|
+
});
|
|
4127
|
+
} catch (err) {
|
|
4128
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4129
|
+
errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
|
|
4130
|
+
}
|
|
4131
|
+
return generatedPages;
|
|
4132
|
+
}
|
|
4133
|
+
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
4134
|
+
const { ogImageUrlMap } = collected;
|
|
4135
|
+
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
4136
|
+
if (context.ssgOptions.render) return renderPage(toThemePageData(pageResult), {
|
|
4137
|
+
theme: context.ssgOptions.render,
|
|
4138
|
+
siteName: context.siteName,
|
|
4139
|
+
base: context.base,
|
|
4140
|
+
nav: context.navItems,
|
|
4141
|
+
pages: allPageResults.map(toThemePageData)
|
|
4142
|
+
});
|
|
4143
|
+
if (context.ssgOptions.bare) return generateBarePage({
|
|
4144
|
+
title: pageResult.title,
|
|
4145
|
+
content: pageResult.transformedHtml,
|
|
4146
|
+
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
4147
|
+
description: pageResult.description,
|
|
4148
|
+
canonicalUrl: canonicalPageUrl(context, pageResult.routePaths.urlPath),
|
|
4149
|
+
siteName: context.ssgOptions.siteName,
|
|
4150
|
+
ogImage: pageOgImage,
|
|
4151
|
+
head: context.ssgOptions.head,
|
|
4152
|
+
bodyStart: context.ssgOptions.bodyStart,
|
|
4153
|
+
bodyEnd: context.ssgOptions.bodyEnd
|
|
4154
|
+
});
|
|
4155
|
+
const pageData = createSsgPageData(pageResult);
|
|
4156
|
+
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);
|
|
4157
|
+
}
|
|
4158
|
+
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
4159
|
+
function toThemePageData(pageResult) {
|
|
4160
|
+
return {
|
|
4161
|
+
title: pageResult.title,
|
|
4162
|
+
description: pageResult.description,
|
|
4163
|
+
html: pageResult.transformedHtml,
|
|
4164
|
+
toc: pageResult.toc,
|
|
4165
|
+
lastUpdated: pageResult.lastUpdated,
|
|
4166
|
+
path: pageResult.inputPath,
|
|
4167
|
+
url: pageResult.routePaths.href,
|
|
4168
|
+
frontmatter: pageResult.frontmatter,
|
|
4169
|
+
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Absolute URL of a page, or `undefined` when `ssg.siteUrl` is not set.
|
|
4174
|
+
*
|
|
4175
|
+
* Built the same way `get_og_image_url` builds the image URL next to it, so
|
|
4176
|
+
* the canonical link and `og:image` always agree about where the page lives.
|
|
4177
|
+
*/
|
|
4178
|
+
function canonicalPageUrl(context, urlPath) {
|
|
4179
|
+
const siteUrl = context.ssgOptions.siteUrl?.replace(/\/+$/, "");
|
|
4180
|
+
if (!siteUrl) return;
|
|
4181
|
+
if (urlPath === "/" || urlPath === "") return `${siteUrl}${context.base}`;
|
|
4182
|
+
return `${siteUrl}${context.base}${urlPath}/`;
|
|
4183
|
+
}
|
|
4184
|
+
function createSsgPageData(pageResult) {
|
|
4185
|
+
const { frontmatter } = pageResult;
|
|
4186
|
+
const entryPage = frontmatter.layout === "entry" ? {
|
|
4187
|
+
hero: frontmatter.hero,
|
|
4188
|
+
features: frontmatter.features
|
|
4189
|
+
} : void 0;
|
|
4190
|
+
return {
|
|
4191
|
+
title: pageResult.title,
|
|
4192
|
+
description: pageResult.description,
|
|
4193
|
+
content: pageResult.transformedHtml,
|
|
4194
|
+
toc: pageResult.toc,
|
|
4195
|
+
lastUpdated: pageResult.lastUpdated,
|
|
4196
|
+
frontmatter,
|
|
4197
|
+
path: pageResult.routePaths.urlPath,
|
|
4198
|
+
href: pageResult.routePaths.href,
|
|
4199
|
+
entryPage
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
async function writeGeneratedPages(generatedPages, context, generatedFiles) {
|
|
4203
|
+
const optimizedOutput = await externalizeSharedPageAssets(generatedPages, context.outDir, context.base);
|
|
4204
|
+
generatedFiles.push(...optimizedOutput.assets);
|
|
4205
|
+
for (const page of optimizedOutput.pages) {
|
|
4206
|
+
await fs$2.mkdir(path$2.dirname(page.outputPath), { recursive: true });
|
|
4207
|
+
await fs$2.writeFile(page.outputPath, page.html, "utf-8");
|
|
4208
|
+
generatedFiles.push(page.outputPath);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
//#endregion
|
|
4212
|
+
//#region src/search.ts
|
|
4213
|
+
/**
|
|
4214
|
+
* Full-text search functionality for Ox Content.
|
|
4215
|
+
*
|
|
4216
|
+
* Generates search index at build time and provides client-side search.
|
|
4217
|
+
*/
|
|
4218
|
+
let oxContent$1 = null;
|
|
4219
|
+
async function getOxContent() {
|
|
4220
|
+
if (!oxContent$1) try {
|
|
4221
|
+
oxContent$1 = await importNapiModule();
|
|
4222
|
+
} catch {
|
|
4223
|
+
console.warn("[ox-content] Native bindings not available, search disabled");
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
return oxContent$1;
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Resolves search options with defaults.
|
|
4230
|
+
*/
|
|
4231
|
+
function resolveSearchOptions(options) {
|
|
4232
|
+
if (options === false) return {
|
|
4233
|
+
enabled: false,
|
|
4234
|
+
limit: 10,
|
|
4235
|
+
prefix: true,
|
|
4236
|
+
placeholder: "Search documentation...",
|
|
4237
|
+
hotkey: "/"
|
|
4238
|
+
};
|
|
4239
|
+
const opts = typeof options === "object" ? options : {};
|
|
4240
|
+
return {
|
|
4241
|
+
enabled: opts.enabled ?? true,
|
|
4242
|
+
limit: opts.limit ?? 10,
|
|
4243
|
+
prefix: opts.prefix ?? true,
|
|
4244
|
+
placeholder: opts.placeholder ?? "Search documentation...",
|
|
4245
|
+
hotkey: opts.hotkey ?? "/"
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Builds the search index from Markdown files.
|
|
4250
|
+
*/
|
|
4251
|
+
async function buildSearchIndex(srcDir, base, extensions = DEFAULT_MARKDOWN_EXTENSIONS) {
|
|
4252
|
+
const napi = await getOxContent();
|
|
4253
|
+
if (!napi) return JSON.stringify({
|
|
4254
|
+
documents: [],
|
|
4255
|
+
index: {},
|
|
4256
|
+
df: {},
|
|
4257
|
+
avg_dl: 0,
|
|
4258
|
+
doc_count: 0
|
|
4259
|
+
});
|
|
4260
|
+
return napi.buildSearchIndexFromDirectory(srcDir, base, [...extensions]);
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Writes the search index to a file.
|
|
4264
|
+
*/
|
|
4265
|
+
async function writeSearchIndex(indexJson, outDir) {
|
|
4266
|
+
const napi = await getOxContent();
|
|
4267
|
+
if (!napi) return;
|
|
4268
|
+
napi.writeSearchIndex(indexJson, outDir);
|
|
4269
|
+
}
|
|
4270
|
+
/**
|
|
4271
|
+
* Client-side search module code.
|
|
4272
|
+
* This is injected into the bundle as a virtual module.
|
|
4273
|
+
*/
|
|
4274
|
+
function generateSearchModule(options, indexPath) {
|
|
4275
|
+
return importNapiModuleSync().generateSearchModuleFromOptions(options, indexPath);
|
|
4276
|
+
}
|
|
4277
|
+
//#endregion
|
|
4278
|
+
//#region src/dev-server.ts
|
|
4279
|
+
/**
|
|
4280
|
+
* Dev server middleware for ox-content SSG.
|
|
4281
|
+
*
|
|
4282
|
+
* Serves fully-rendered HTML pages (with navigation, theme, etc.)
|
|
4283
|
+
* during `vite dev`, matching the SSG build output.
|
|
4284
|
+
*/
|
|
4285
|
+
/** File extensions to skip in the middleware. */
|
|
4286
|
+
const SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4287
|
+
".js",
|
|
4288
|
+
".ts",
|
|
4289
|
+
".css",
|
|
4290
|
+
".scss",
|
|
4291
|
+
".less",
|
|
4292
|
+
".svg",
|
|
4293
|
+
".png",
|
|
4294
|
+
".jpg",
|
|
4295
|
+
".jpeg",
|
|
4296
|
+
".gif",
|
|
4297
|
+
".webp",
|
|
4298
|
+
".ico",
|
|
4299
|
+
".woff",
|
|
4300
|
+
".woff2",
|
|
4301
|
+
".ttf",
|
|
4302
|
+
".eot",
|
|
4303
|
+
".json",
|
|
4304
|
+
".map",
|
|
4305
|
+
".mp4",
|
|
4306
|
+
".webm",
|
|
4307
|
+
".mp3",
|
|
4308
|
+
".pdf"
|
|
4309
|
+
]);
|
|
4310
|
+
/** Vite internal URL prefixes to skip. */
|
|
4311
|
+
const VITE_INTERNAL_PREFIXES = [
|
|
4312
|
+
"/@vite/",
|
|
4313
|
+
"/@fs/",
|
|
4314
|
+
"/@id/",
|
|
4315
|
+
"/__"
|
|
4316
|
+
];
|
|
4317
|
+
/**
|
|
4318
|
+
* Check if a request URL should be skipped by the dev server middleware.
|
|
4319
|
+
*/
|
|
4320
|
+
function shouldSkip(url) {
|
|
4321
|
+
for (const prefix of VITE_INTERNAL_PREFIXES) if (url.startsWith(prefix)) return true;
|
|
4322
|
+
if (url.includes("/node_modules/")) return true;
|
|
4323
|
+
const extMatch = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
|
|
4324
|
+
if (extMatch) {
|
|
4325
|
+
const ext = "." + extMatch[1].toLowerCase();
|
|
4326
|
+
if (SKIP_EXTENSIONS.has(ext)) return true;
|
|
4327
|
+
}
|
|
4328
|
+
return false;
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Resolve a request URL to a markdown file path.
|
|
4332
|
+
* Returns null if no matching file exists.
|
|
4333
|
+
*/
|
|
4334
|
+
async function resolveMarkdownFile(url, srcDir, extensions) {
|
|
4335
|
+
let pathname = url.split("?")[0].split("#")[0];
|
|
4336
|
+
if (pathname.endsWith("/index.html")) pathname = pathname.slice(0, -11) || "/";
|
|
4337
|
+
if (pathname !== "/" && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
4338
|
+
const routePath = pathname === "/" ? "" : pathname.slice(1);
|
|
4339
|
+
const directCandidates = pathname === "/" ? extensions.map((extension) => `index${extension}`) : isMarkdownFilePath(routePath, extensions) ? [routePath] : extensions.map((extension) => `${routePath}${extension}`);
|
|
4340
|
+
for (const relativePath of directCandidates) {
|
|
4341
|
+
const filePath = path$2.join(srcDir, relativePath);
|
|
4342
|
+
try {
|
|
4343
|
+
await fs$2.access(filePath);
|
|
4344
|
+
return filePath;
|
|
4345
|
+
} catch {}
|
|
4346
|
+
}
|
|
4347
|
+
for (const extension of extensions) {
|
|
4348
|
+
const indexPath = path$2.join(srcDir, routePath, `index${extension}`);
|
|
4349
|
+
try {
|
|
4350
|
+
await fs$2.access(indexPath);
|
|
4351
|
+
return indexPath;
|
|
4352
|
+
} catch {}
|
|
4353
|
+
}
|
|
4354
|
+
return null;
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Inject Vite HMR client script into the HTML.
|
|
4358
|
+
*/
|
|
4359
|
+
function injectViteHmrClient(html) {
|
|
4360
|
+
return html.replace("</head>", "<script type=\"module\" src=\"/@vite/client\"><\/script>\n<script type=\"module\">\nif (import.meta.hot) {\n const reexecuteBodyScripts = () => {\n const scripts = Array.from(document.body.querySelectorAll('script'));\n for (const script of scripts) {\n const nextScript = document.createElement('script');\n for (const attr of script.attributes) {\n nextScript.setAttribute(attr.name, attr.value);\n }\n nextScript.textContent = script.textContent;\n script.replaceWith(nextScript);\n }\n };\n\n const applyHotUpdate = async () => {\n const nextUrl = new URL(window.location.href);\n nextUrl.searchParams.set('__ox_hmr', String(Date.now()));\n\n const scrollX = window.scrollX;\n const scrollY = window.scrollY;\n const theme = document.documentElement.getAttribute('data-theme');\n\n const response = await fetch(nextUrl.toString(), {\n cache: 'no-store',\n headers: {\n 'x-ox-content-hmr': '1',\n },\n });\n\n if (!response.ok) {\n throw new Error('Failed to fetch updated page');\n }\n\n const nextHtml = await response.text();\n const nextDocument = new DOMParser().parseFromString(nextHtml, 'text/html');\n\n if (!nextDocument.body) {\n throw new Error('Updated page is missing a body');\n }\n\n document.title = nextDocument.title;\n document.body.innerHTML = nextDocument.body.innerHTML;\n reexecuteBodyScripts();\n\n if (theme) {\n document.documentElement.setAttribute('data-theme', theme);\n }\n\n window.scrollTo({ left: scrollX, top: scrollY });\n };\n\n let pendingUpdate = Promise.resolve();\n\n import.meta.hot.on('ox-content:update', () => {\n pendingUpdate = pendingUpdate\n .then(() => applyHotUpdate())\n .catch((error) => {\n console.warn('[ox-content] HMR patch failed, falling back to reload.', error);\n location.reload();\n });\n });\n}\n<\/script>\n</head>");
|
|
4361
|
+
}
|
|
4362
|
+
/**
|
|
4363
|
+
* Create a dev server cache instance.
|
|
4364
|
+
*/
|
|
4365
|
+
function createDevServerCache() {
|
|
4366
|
+
return {
|
|
4367
|
+
navGroups: null,
|
|
4368
|
+
pages: /* @__PURE__ */ new Map(),
|
|
4369
|
+
siteName: null
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Invalidate navigation cache (called on file add/unlink).
|
|
4374
|
+
*/
|
|
4375
|
+
function invalidateNavCache(cache) {
|
|
4376
|
+
cache.navGroups = null;
|
|
4377
|
+
cache.pages.clear();
|
|
4378
|
+
}
|
|
4379
|
+
/**
|
|
4380
|
+
* Invalidate page cache for a specific file (called on file change).
|
|
4381
|
+
*/
|
|
4382
|
+
function invalidatePageCache(cache, filePath) {
|
|
4383
|
+
cache.pages.delete(filePath);
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Resolve site name from options or package.json.
|
|
4387
|
+
*/
|
|
4388
|
+
async function resolveSiteName(options, root) {
|
|
4389
|
+
if (options.ssg.siteName) return options.ssg.siteName;
|
|
4390
|
+
try {
|
|
4391
|
+
const pkgPath = path$2.join(root, "package.json");
|
|
4392
|
+
const pkg = JSON.parse(await fs$2.readFile(pkgPath, "utf-8"));
|
|
4393
|
+
if (pkg.name) return formatTitle(pkg.name);
|
|
4394
|
+
} catch {}
|
|
4395
|
+
return "Documentation";
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Render a single markdown page to full HTML.
|
|
4399
|
+
*/
|
|
4400
|
+
async function renderPage$1(filePath, options, navGroups, siteName, base, root) {
|
|
4401
|
+
const srcDir = path$2.resolve(root, options.srcDir);
|
|
4402
|
+
resetTabGroupCounter();
|
|
4403
|
+
resetIslandCounter();
|
|
4404
|
+
const result = await transformMarkdown(await fs$2.readFile(filePath, "utf-8"), filePath, options, {
|
|
4405
|
+
convertMdLinks: true,
|
|
4406
|
+
baseUrl: base,
|
|
4407
|
+
sourcePath: filePath
|
|
4408
|
+
});
|
|
4409
|
+
const frontmatter = normalizeVitePressFrontmatter(result.frontmatter);
|
|
4410
|
+
let transformedHtml = result.html;
|
|
4411
|
+
const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(transformedHtml);
|
|
4412
|
+
transformedHtml = protectedHtml;
|
|
4413
|
+
transformedHtml = await transformAllPlugins(transformedHtml, {
|
|
4414
|
+
tabs: true,
|
|
4415
|
+
youtube: true,
|
|
4416
|
+
github: options.embeds.github,
|
|
4417
|
+
openGraph: options.embeds.openGraph,
|
|
4418
|
+
pm: options.embeds.pm,
|
|
4419
|
+
spotify: options.embeds.spotify,
|
|
4420
|
+
stackBlitz: options.embeds.stackBlitz,
|
|
4421
|
+
twitter: options.embeds.twitter,
|
|
4422
|
+
bluesky: options.embeds.bluesky,
|
|
4423
|
+
webContainer: options.embeds.webContainer,
|
|
4424
|
+
mermaid: true,
|
|
4425
|
+
githubToken: process.env.GITHUB_TOKEN
|
|
4426
|
+
});
|
|
4427
|
+
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
4428
|
+
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
4429
|
+
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
4430
|
+
const description = frontmatter.description;
|
|
4431
|
+
let entryPage;
|
|
4432
|
+
if (frontmatter.layout === "entry") entryPage = {
|
|
4433
|
+
hero: frontmatter.hero,
|
|
4434
|
+
features: frontmatter.features
|
|
4435
|
+
};
|
|
4436
|
+
let html = await generateHtmlPage({
|
|
4437
|
+
title,
|
|
4438
|
+
description,
|
|
4439
|
+
content: transformedHtml,
|
|
4440
|
+
toc: result.toc,
|
|
4441
|
+
frontmatter,
|
|
4442
|
+
path: getUrlPath$1(filePath, srcDir),
|
|
4443
|
+
href: getUrlPath$1(filePath, srcDir) || "/",
|
|
4444
|
+
entryPage
|
|
4445
|
+
}, navGroups, siteName, base, options.ssg.ogImage, options.ssg.theme);
|
|
4446
|
+
html = injectViteHmrClient(html);
|
|
4447
|
+
return html;
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Create the dev server middleware for SSG page serving.
|
|
4451
|
+
*/
|
|
4452
|
+
function createDevServerMiddleware(options, root, cache) {
|
|
4453
|
+
const srcDir = path$2.resolve(root, options.srcDir);
|
|
4454
|
+
const base = options.base.endsWith("/") ? options.base : options.base + "/";
|
|
4455
|
+
return async (req, res, next) => {
|
|
4456
|
+
const url = req.url;
|
|
4457
|
+
if (!url) return next();
|
|
4458
|
+
let routeUrl = url;
|
|
4459
|
+
if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
|
|
4460
|
+
if (shouldSkip(routeUrl)) return next();
|
|
4461
|
+
const filePath = await resolveMarkdownFile(routeUrl, srcDir, options.extensions);
|
|
4462
|
+
if (!filePath) return next();
|
|
4463
|
+
try {
|
|
4464
|
+
const cached = cache.pages.get(filePath);
|
|
4465
|
+
if (cached) {
|
|
4466
|
+
res.setHeader("Content-Type", "text/html");
|
|
4467
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
4468
|
+
res.end(cached);
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
if (!cache.siteName) cache.siteName = await resolveSiteName(options, root);
|
|
4472
|
+
if (!cache.navGroups) {
|
|
4473
|
+
const markdownFiles = await collectMarkdownFiles(srcDir, options.extensions);
|
|
4474
|
+
cache.navGroups = resolveNavigationGroups(options.ssg.navigation, base, options.ssg.extension) ?? (options.ssg.theme?.sidebar.length ? buildThemeNavItems(options.ssg.theme.sidebar, base, options.ssg.extension) : buildNavItems(markdownFiles, srcDir, base, options.ssg.extension));
|
|
4475
|
+
}
|
|
4476
|
+
const html = await renderPage$1(filePath, options, cache.navGroups, cache.siteName, base, root);
|
|
4477
|
+
cache.pages.set(filePath, html);
|
|
4478
|
+
res.setHeader("Content-Type", "text/html");
|
|
4479
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
4480
|
+
res.end(html);
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4483
|
+
console.error(`[ox-content:dev] Failed to render ${filePath}:`, message);
|
|
4484
|
+
next();
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
//#endregion
|
|
4489
|
+
//#region src/og-viewer.ts
|
|
4490
|
+
/**
|
|
4491
|
+
* OG Viewer - Dev tool for previewing Open Graph metadata
|
|
4492
|
+
*
|
|
4493
|
+
* Accessible at /__og-viewer during development.
|
|
4494
|
+
* Shows all pages with their OG metadata, validation warnings,
|
|
4495
|
+
* and social card previews.
|
|
4496
|
+
*/
|
|
4497
|
+
function parseFrontmatter(content) {
|
|
4498
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4499
|
+
if (!match) return {};
|
|
4500
|
+
const yaml = match[1];
|
|
4501
|
+
const result = {};
|
|
4502
|
+
for (const line of yaml.split("\n")) {
|
|
4503
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
4504
|
+
if (!kv) continue;
|
|
4052
4505
|
const [, key, rawValue] = kv;
|
|
4053
4506
|
let value = rawValue.trim();
|
|
4054
4507
|
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
@@ -5540,562 +5993,209 @@ async function runStandardSpellcheckDocuments(maskedDocuments, options) {
|
|
|
5540
5993
|
} catch (error) {
|
|
5541
5994
|
const imports = standard.imports.join(", ");
|
|
5542
5995
|
const message = imports.length > 0 ? `[ox-content] Failed to load standard dictionaries from ${imports}. Verify the imports and install the referenced CSpell packages.` : "[ox-content] Failed to load the configured standard dictionaries.";
|
|
5543
|
-
throw new Error(message, { cause: error });
|
|
5544
|
-
}
|
|
5545
|
-
}
|
|
5546
|
-
function createStandardSpellcheckSettings(options, locale) {
|
|
5547
|
-
return {
|
|
5548
|
-
import: options.dictionary.standard ? options.dictionary.standard.imports : [],
|
|
5549
|
-
ignoreWords: options.dictionary.ignoredWords,
|
|
5550
|
-
language: locale,
|
|
5551
|
-
version: "0.2",
|
|
5552
|
-
words: [...options.dictionary.words ?? [], ...Object.values(options.dictionary.byLanguage ?? {}).flat()]
|
|
5553
|
-
};
|
|
5554
|
-
}
|
|
5555
|
-
async function loadCspellLib() {
|
|
5556
|
-
cspellLibPromise ??= import("cspell-lib");
|
|
5557
|
-
return cspellLibPromise;
|
|
5558
|
-
}
|
|
5559
|
-
function mapStandardIssueToDiagnostic(issue, languages, newlineOffsets) {
|
|
5560
|
-
const line = getLineNumberAtOffset(newlineOffsets, issue.line.offset);
|
|
5561
|
-
const column = issue.offset - issue.line.offset + 1;
|
|
5562
|
-
return {
|
|
5563
|
-
column,
|
|
5564
|
-
endColumn: column + (issue.length ?? issue.text.length),
|
|
5565
|
-
endLine: line,
|
|
5566
|
-
language: inferStandardIssueLanguage(issue.text, languages),
|
|
5567
|
-
line,
|
|
5568
|
-
message: `Unknown word "${issue.text}".`,
|
|
5569
|
-
ruleId: "spellcheck",
|
|
5570
|
-
severity: "warning",
|
|
5571
|
-
suggestions: issue.suggestions?.slice(0, 3)
|
|
5572
|
-
};
|
|
5573
|
-
}
|
|
5574
|
-
function getLineNumberAtOffset(newlineOffsets, offset) {
|
|
5575
|
-
let lo = 0;
|
|
5576
|
-
let hi = newlineOffsets.length;
|
|
5577
|
-
while (lo < hi) {
|
|
5578
|
-
const mid = lo + hi >>> 1;
|
|
5579
|
-
if (newlineOffsets[mid] < offset) lo = mid + 1;
|
|
5580
|
-
else hi = mid;
|
|
5581
|
-
}
|
|
5582
|
-
return lo + 1;
|
|
5583
|
-
}
|
|
5584
|
-
function inferStandardIssueLanguage(word, languages) {
|
|
5585
|
-
if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(word) && languages.includes("ja")) return "ja";
|
|
5586
|
-
if (/[\p{Script=Han}]/u.test(word)) {
|
|
5587
|
-
if (languages.includes("zh") && !languages.includes("ja")) return "zh";
|
|
5588
|
-
if (languages.includes("ja") && !languages.includes("zh")) return "ja";
|
|
5589
|
-
}
|
|
5590
|
-
if (/[\p{Script=Latin}]/u.test(word)) {
|
|
5591
|
-
const latinLanguages = languages.filter((language) => language !== "ja" && language !== "zh");
|
|
5592
|
-
if (latinLanguages.length === 1) return latinLanguages[0];
|
|
5593
|
-
return inferLatinLanguageFromCharacters(word, latinLanguages);
|
|
5594
|
-
}
|
|
5595
|
-
}
|
|
5596
|
-
function inferLatinLanguageFromCharacters(word, languages) {
|
|
5597
|
-
if (languages.includes("pl") && /[ąćęłńóśźż]/iu.test(word)) return "pl";
|
|
5598
|
-
if (languages.includes("de") && /[äöüß]/iu.test(word)) return "de";
|
|
5599
|
-
if (languages.includes("fr") && /[àâæçéèêëîïôœùûüÿ]/iu.test(word)) return "fr";
|
|
5600
|
-
}
|
|
5601
|
-
function summarizeDiagnostics(diagnostics) {
|
|
5602
|
-
let errorCount = 0;
|
|
5603
|
-
let warningCount = 0;
|
|
5604
|
-
let infoCount = 0;
|
|
5605
|
-
for (const diagnostic of diagnostics) if (diagnostic.severity === "error") errorCount += 1;
|
|
5606
|
-
else if (diagnostic.severity === "warning") warningCount += 1;
|
|
5607
|
-
else infoCount += 1;
|
|
5608
|
-
return {
|
|
5609
|
-
diagnostics,
|
|
5610
|
-
errorCount,
|
|
5611
|
-
infoCount,
|
|
5612
|
-
warningCount
|
|
5613
|
-
};
|
|
5614
|
-
}
|
|
5615
|
-
function createEmptyLintResult$1() {
|
|
5616
|
-
return summarizeDiagnostics([]);
|
|
5617
|
-
}
|
|
5618
|
-
function sortDiagnostics(diagnostics) {
|
|
5619
|
-
return [...diagnostics].sort((left, right) => {
|
|
5620
|
-
if (left.line !== right.line) return left.line - right.line;
|
|
5621
|
-
if (left.column !== right.column) return left.column - right.column;
|
|
5622
|
-
return left.ruleId.localeCompare(right.ruleId);
|
|
5623
|
-
});
|
|
5624
|
-
}
|
|
5625
|
-
//#endregion
|
|
5626
|
-
//#region src/lint-files.ts
|
|
5627
|
-
const DEFAULT_LINT_FILE_INCLUDE = [
|
|
5628
|
-
"**/*.md",
|
|
5629
|
-
"**/*.markdown",
|
|
5630
|
-
"**/*.mdx"
|
|
5631
|
-
];
|
|
5632
|
-
const DEFAULT_LINT_FILE_EXCLUDE = [
|
|
5633
|
-
"**/node_modules/**",
|
|
5634
|
-
"**/.git/**",
|
|
5635
|
-
"**/dist/**"
|
|
5636
|
-
];
|
|
5637
|
-
/**
|
|
5638
|
-
* Returns true if the file path is included by the configured glob filters.
|
|
5639
|
-
*/
|
|
5640
|
-
function shouldLintMarkdownFile(filePath, options = {}) {
|
|
5641
|
-
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
5642
|
-
return shouldLintAbsoluteFile(path$1.resolve(resolvedOptions.cwd, filePath), resolvedOptions);
|
|
5643
|
-
}
|
|
5644
|
-
/**
|
|
5645
|
-
* Lints a single Markdown file using project-style include/exclude settings.
|
|
5646
|
-
*
|
|
5647
|
-
* If the file is filtered out by `include` / `exclude`, the returned result is
|
|
5648
|
-
* marked as `skipped` and contains no diagnostics.
|
|
5649
|
-
*/
|
|
5650
|
-
async function lintMarkdownFile(filePath, options = {}) {
|
|
5651
|
-
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
5652
|
-
return lintMarkdownFileWithResolvedOptions(path$1.resolve(resolvedOptions.cwd, filePath), resolvedOptions);
|
|
5653
|
-
}
|
|
5654
|
-
/**
|
|
5655
|
-
* Lints all Markdown files matched by the configured include/exclude patterns.
|
|
5656
|
-
*/
|
|
5657
|
-
async function lintMarkdownFiles(options = {}) {
|
|
5658
|
-
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
5659
|
-
const matchedFiles = await collectMarkdownLintFileEntries(resolvedOptions);
|
|
5660
|
-
const results = await lintMarkdownDocumentsAsync(await Promise.all(matchedFiles.map((file) => fs.readFile(file.filePath, "utf-8"))), resolvedOptions.lintOptions);
|
|
5661
|
-
const files = matchedFiles.map((file, index) => ({
|
|
5662
|
-
...results[index] ?? createEmptyLintResult(),
|
|
5663
|
-
filePath: file.filePath,
|
|
5664
|
-
relativePath: file.relativePath,
|
|
5665
|
-
skipped: false
|
|
5666
|
-
}));
|
|
5667
|
-
const diagnostics = files.flatMap((fileResult) => fileResult.diagnostics.map((diagnostic) => ({
|
|
5668
|
-
...diagnostic,
|
|
5669
|
-
filePath: fileResult.filePath,
|
|
5670
|
-
relativePath: fileResult.relativePath
|
|
5671
|
-
})));
|
|
5672
|
-
return {
|
|
5673
|
-
checkedFileCount: files.length,
|
|
5674
|
-
diagnostics,
|
|
5675
|
-
errorCount: files.reduce((count, fileResult) => count + fileResult.errorCount, 0),
|
|
5676
|
-
files,
|
|
5677
|
-
infoCount: files.reduce((count, fileResult) => count + fileResult.infoCount, 0),
|
|
5678
|
-
warningCount: files.reduce((count, fileResult) => count + fileResult.warningCount, 0)
|
|
5679
|
-
};
|
|
5680
|
-
}
|
|
5681
|
-
function resolveMarkdownLintFileOptions(options) {
|
|
5682
|
-
return {
|
|
5683
|
-
cwd: path$1.resolve(options.cwd ?? process.cwd()),
|
|
5684
|
-
exclude: [.../* @__PURE__ */ new Set([...options.exclude ?? DEFAULT_LINT_FILE_EXCLUDE, ...options.ignore ?? []])],
|
|
5685
|
-
include: [...new Set(options.include ?? DEFAULT_LINT_FILE_INCLUDE)],
|
|
5686
|
-
lintOptions: {
|
|
5687
|
-
dictionary: options.dictionary,
|
|
5688
|
-
languages: options.languages,
|
|
5689
|
-
rules: options.rules
|
|
5690
|
-
}
|
|
5691
|
-
};
|
|
5692
|
-
}
|
|
5693
|
-
async function lintMarkdownFileWithResolvedOptions(filePath, options) {
|
|
5694
|
-
const absoluteFilePath = path$1.resolve(filePath);
|
|
5695
|
-
const relativePath = normalizePath(path$1.relative(options.cwd, absoluteFilePath));
|
|
5696
|
-
if (!shouldLintAbsoluteFile(absoluteFilePath, options)) return {
|
|
5697
|
-
...createEmptyLintResult(),
|
|
5698
|
-
filePath: absoluteFilePath,
|
|
5699
|
-
relativePath,
|
|
5700
|
-
skipped: true
|
|
5701
|
-
};
|
|
5702
|
-
return {
|
|
5703
|
-
...await lintMarkdownAsync(await fs.readFile(absoluteFilePath, "utf-8"), options.lintOptions),
|
|
5704
|
-
filePath: absoluteFilePath,
|
|
5705
|
-
relativePath,
|
|
5706
|
-
skipped: false
|
|
5707
|
-
};
|
|
5708
|
-
}
|
|
5709
|
-
async function collectMarkdownLintFileEntries(options) {
|
|
5710
|
-
const files = /* @__PURE__ */ new Map();
|
|
5711
|
-
for (const pattern of options.include) {
|
|
5712
|
-
const matches = await glob(pattern, {
|
|
5713
|
-
absolute: true,
|
|
5714
|
-
cwd: options.cwd,
|
|
5715
|
-
ignore: options.exclude,
|
|
5716
|
-
nodir: true
|
|
5717
|
-
});
|
|
5718
|
-
for (const filePath of matches) {
|
|
5719
|
-
const absoluteFilePath = path$1.resolve(filePath);
|
|
5720
|
-
if (shouldLintAbsoluteFile(absoluteFilePath, options)) files.set(absoluteFilePath, {
|
|
5721
|
-
filePath: absoluteFilePath,
|
|
5722
|
-
relativePath: normalizePath(path$1.relative(options.cwd, absoluteFilePath))
|
|
5723
|
-
});
|
|
5724
|
-
}
|
|
5725
|
-
}
|
|
5726
|
-
return [...files.values()].sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
5727
|
-
}
|
|
5728
|
-
function shouldLintAbsoluteFile(filePath, options) {
|
|
5729
|
-
const absolutePath = normalizePath(path$1.resolve(filePath));
|
|
5730
|
-
const relativePath = normalizePath(path$1.relative(options.cwd, absolutePath));
|
|
5731
|
-
const matches = (patterns) => patterns.some((pattern) => {
|
|
5732
|
-
const normalizedPattern = normalizePath(pattern);
|
|
5733
|
-
return path$1.matchesGlob(relativePath, normalizedPattern) || path$1.matchesGlob(absolutePath, normalizedPattern);
|
|
5734
|
-
});
|
|
5735
|
-
return matches(options.include) && !matches(options.exclude);
|
|
5736
|
-
}
|
|
5737
|
-
function normalizePath(value) {
|
|
5738
|
-
return value.split(path$1.sep).join("/");
|
|
5996
|
+
throw new Error(message, { cause: error });
|
|
5997
|
+
}
|
|
5739
5998
|
}
|
|
5740
|
-
function
|
|
5999
|
+
function createStandardSpellcheckSettings(options, locale) {
|
|
5741
6000
|
return {
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
6001
|
+
import: options.dictionary.standard ? options.dictionary.standard.imports : [],
|
|
6002
|
+
ignoreWords: options.dictionary.ignoredWords,
|
|
6003
|
+
language: locale,
|
|
6004
|
+
version: "0.2",
|
|
6005
|
+
words: [...options.dictionary.words ?? [], ...Object.values(options.dictionary.byLanguage ?? {}).flat()]
|
|
5746
6006
|
};
|
|
5747
6007
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
clearRenderContext: () => clearRenderContext,
|
|
5752
|
-
generateFrontmatterTypes: () => generateFrontmatterTypes,
|
|
5753
|
-
inferType: () => inferType,
|
|
5754
|
-
setRenderContext: () => setRenderContext,
|
|
5755
|
-
useIsActive: () => useIsActive,
|
|
5756
|
-
useNav: () => useNav,
|
|
5757
|
-
usePageProps: () => usePageProps,
|
|
5758
|
-
useRenderContext: () => useRenderContext,
|
|
5759
|
-
useSiteConfig: () => useSiteConfig
|
|
5760
|
-
});
|
|
5761
|
-
/**
|
|
5762
|
-
* Sets the current render context.
|
|
5763
|
-
* Called internally during page rendering.
|
|
5764
|
-
* @internal
|
|
5765
|
-
*/
|
|
5766
|
-
function setRenderContext(ctx) {
|
|
5767
|
-
currentContext = ctx;
|
|
5768
|
-
}
|
|
5769
|
-
/**
|
|
5770
|
-
* Clears the current render context.
|
|
5771
|
-
* Called internally after page rendering.
|
|
5772
|
-
* @internal
|
|
5773
|
-
*/
|
|
5774
|
-
function clearRenderContext() {
|
|
5775
|
-
currentContext = null;
|
|
5776
|
-
}
|
|
5777
|
-
/**
|
|
5778
|
-
* Gets the current page props.
|
|
5779
|
-
*
|
|
5780
|
-
* @returns The current page props
|
|
5781
|
-
* @throws Error if called outside of a render context
|
|
5782
|
-
*
|
|
5783
|
-
* @example
|
|
5784
|
-
* ```tsx
|
|
5785
|
-
* function PageTitle() {
|
|
5786
|
-
* const page = usePageProps();
|
|
5787
|
-
* return <h1>{page.title}</h1>;
|
|
5788
|
-
* }
|
|
5789
|
-
* ```
|
|
5790
|
-
*/
|
|
5791
|
-
function usePageProps() {
|
|
5792
|
-
if (!currentContext) throw new Error("[ox-content] usePageProps() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5793
|
-
return currentContext.page;
|
|
5794
|
-
}
|
|
5795
|
-
/**
|
|
5796
|
-
* Gets the site configuration.
|
|
5797
|
-
*
|
|
5798
|
-
* @returns The site configuration
|
|
5799
|
-
* @throws Error if called outside of a render context
|
|
5800
|
-
*
|
|
5801
|
-
* @example
|
|
5802
|
-
* ```tsx
|
|
5803
|
-
* function SiteHeader() {
|
|
5804
|
-
* const site = useSiteConfig();
|
|
5805
|
-
* return <header>{site.name}</header>;
|
|
5806
|
-
* }
|
|
5807
|
-
* ```
|
|
5808
|
-
*/
|
|
5809
|
-
function useSiteConfig() {
|
|
5810
|
-
if (!currentContext) throw new Error("[ox-content] useSiteConfig() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5811
|
-
return currentContext.site;
|
|
5812
|
-
}
|
|
5813
|
-
/**
|
|
5814
|
-
* Gets the full render context.
|
|
5815
|
-
*
|
|
5816
|
-
* @returns The complete render context
|
|
5817
|
-
* @throws Error if called outside of a render context
|
|
5818
|
-
*
|
|
5819
|
-
* @example
|
|
5820
|
-
* ```tsx
|
|
5821
|
-
* function Layout({ children }) {
|
|
5822
|
-
* const ctx = useRenderContext();
|
|
5823
|
-
* return (
|
|
5824
|
-
* <html>
|
|
5825
|
-
* <head><title>{ctx.page.title} - {ctx.site.name}</title></head>
|
|
5826
|
-
* <body>{children}</body>
|
|
5827
|
-
* </html>
|
|
5828
|
-
* );
|
|
5829
|
-
* }
|
|
5830
|
-
* ```
|
|
5831
|
-
*/
|
|
5832
|
-
function useRenderContext() {
|
|
5833
|
-
if (!currentContext) throw new Error("[ox-content] useRenderContext() must be called during page rendering. Make sure you are using it inside a theme component.");
|
|
5834
|
-
return currentContext;
|
|
5835
|
-
}
|
|
5836
|
-
/**
|
|
5837
|
-
* Gets the navigation groups.
|
|
5838
|
-
*
|
|
5839
|
-
* @example
|
|
5840
|
-
* ```tsx
|
|
5841
|
-
* function Sidebar() {
|
|
5842
|
-
* const nav = useNav();
|
|
5843
|
-
* return (
|
|
5844
|
-
* <nav>
|
|
5845
|
-
* {each(nav, (group) => (
|
|
5846
|
-
* <div>
|
|
5847
|
-
* <h3>{group.title}</h3>
|
|
5848
|
-
* <ul>
|
|
5849
|
-
* {each(group.items, (item) => (
|
|
5850
|
-
* <li><a href={item.href}>{item.title}</a></li>
|
|
5851
|
-
* ))}
|
|
5852
|
-
* </ul>
|
|
5853
|
-
* </div>
|
|
5854
|
-
* ))}
|
|
5855
|
-
* </nav>
|
|
5856
|
-
* );
|
|
5857
|
-
* }
|
|
5858
|
-
* ```
|
|
5859
|
-
*/
|
|
5860
|
-
function useNav() {
|
|
5861
|
-
return useSiteConfig().nav;
|
|
6008
|
+
async function loadCspellLib() {
|
|
6009
|
+
cspellLibPromise ??= import("cspell-lib");
|
|
6010
|
+
return cspellLibPromise;
|
|
5862
6011
|
}
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
6012
|
+
function mapStandardIssueToDiagnostic(issue, languages, newlineOffsets) {
|
|
6013
|
+
const line = getLineNumberAtOffset(newlineOffsets, issue.line.offset);
|
|
6014
|
+
const column = issue.offset - issue.line.offset + 1;
|
|
6015
|
+
return {
|
|
6016
|
+
column,
|
|
6017
|
+
endColumn: column + (issue.length ?? issue.text.length),
|
|
6018
|
+
endLine: line,
|
|
6019
|
+
language: inferStandardIssueLanguage(issue.text, languages),
|
|
6020
|
+
line,
|
|
6021
|
+
message: `Unknown word "${issue.text}".`,
|
|
6022
|
+
ruleId: "spellcheck",
|
|
6023
|
+
severity: "warning",
|
|
6024
|
+
suggestions: issue.suggestions?.slice(0, 3)
|
|
6025
|
+
};
|
|
5877
6026
|
}
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
if (typeof value === "number") return "number";
|
|
5886
|
-
if (typeof value === "boolean") return "boolean";
|
|
5887
|
-
if (Array.isArray(value)) {
|
|
5888
|
-
if (value.length === 0) return "unknown[]";
|
|
5889
|
-
const itemTypes = [...new Set(value.map(inferType))];
|
|
5890
|
-
if (itemTypes.length === 1) return `${itemTypes[0]}[]`;
|
|
5891
|
-
return `(${itemTypes.join(" | ")})[]`;
|
|
5892
|
-
}
|
|
5893
|
-
if (typeof value === "object") {
|
|
5894
|
-
const entries = Object.entries(value);
|
|
5895
|
-
if (entries.length === 0) return "Record<string, unknown>";
|
|
5896
|
-
return `{ ${entries.map(([k, v]) => `${k}: ${inferType(v)}`).join("; ")} }`;
|
|
6027
|
+
function getLineNumberAtOffset(newlineOffsets, offset) {
|
|
6028
|
+
let lo = 0;
|
|
6029
|
+
let hi = newlineOffsets.length;
|
|
6030
|
+
while (lo < hi) {
|
|
6031
|
+
const mid = lo + hi >>> 1;
|
|
6032
|
+
if (newlineOffsets[mid] < offset) lo = mid + 1;
|
|
6033
|
+
else hi = mid;
|
|
5897
6034
|
}
|
|
5898
|
-
return
|
|
6035
|
+
return lo + 1;
|
|
5899
6036
|
}
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
for (const sample of samples) for (const [key, value] of Object.entries(sample)) {
|
|
5906
|
-
const existing = fields.get(key) ?? {
|
|
5907
|
-
types: /* @__PURE__ */ new Set(),
|
|
5908
|
-
count: 0
|
|
5909
|
-
};
|
|
5910
|
-
existing.types.add(inferType(value));
|
|
5911
|
-
existing.count++;
|
|
5912
|
-
fields.set(key, existing);
|
|
6037
|
+
function inferStandardIssueLanguage(word, languages) {
|
|
6038
|
+
if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(word) && languages.includes("ja")) return "ja";
|
|
6039
|
+
if (/[\p{Script=Han}]/u.test(word)) {
|
|
6040
|
+
if (languages.includes("zh") && !languages.includes("ja")) return "zh";
|
|
6041
|
+
if (languages.includes("ja") && !languages.includes("zh")) return "ja";
|
|
5913
6042
|
}
|
|
5914
|
-
|
|
5915
|
-
"
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
" */",
|
|
5919
|
-
"",
|
|
5920
|
-
`export interface ${interfaceName} {`
|
|
5921
|
-
];
|
|
5922
|
-
for (const [name, { types, count }] of fields) {
|
|
5923
|
-
const isOptional = count < samples.length;
|
|
5924
|
-
const typeStr = [...types].join(" | ");
|
|
5925
|
-
const optionalMark = isOptional ? "?" : "";
|
|
5926
|
-
lines.push(` ${name}${optionalMark}: ${typeStr};`);
|
|
6043
|
+
if (/[\p{Script=Latin}]/u.test(word)) {
|
|
6044
|
+
const latinLanguages = languages.filter((language) => language !== "ja" && language !== "zh");
|
|
6045
|
+
if (latinLanguages.length === 1) return latinLanguages[0];
|
|
6046
|
+
return inferLatinLanguageFromCharacters(word, latinLanguages);
|
|
5927
6047
|
}
|
|
5928
|
-
lines.push("}");
|
|
5929
|
-
lines.push("");
|
|
5930
|
-
lines.push(`export type PageProps = import('@ox-content/vite-plugin').PageProps<${interfaceName}>;`);
|
|
5931
|
-
lines.push("");
|
|
5932
|
-
return lines.join("\n");
|
|
5933
6048
|
}
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
6049
|
+
function inferLatinLanguageFromCharacters(word, languages) {
|
|
6050
|
+
if (languages.includes("pl") && /[ąćęłńóśźż]/iu.test(word)) return "pl";
|
|
6051
|
+
if (languages.includes("de") && /[äöüß]/iu.test(word)) return "de";
|
|
6052
|
+
if (languages.includes("fr") && /[àâæçéèêëîïôœùûüÿ]/iu.test(word)) return "fr";
|
|
6053
|
+
}
|
|
6054
|
+
function summarizeDiagnostics(diagnostics) {
|
|
6055
|
+
let errorCount = 0;
|
|
6056
|
+
let warningCount = 0;
|
|
6057
|
+
let infoCount = 0;
|
|
6058
|
+
for (const diagnostic of diagnostics) if (diagnostic.severity === "error") errorCount += 1;
|
|
6059
|
+
else if (diagnostic.severity === "warning") warningCount += 1;
|
|
6060
|
+
else infoCount += 1;
|
|
6061
|
+
return {
|
|
6062
|
+
diagnostics,
|
|
6063
|
+
errorCount,
|
|
6064
|
+
infoCount,
|
|
6065
|
+
warningCount
|
|
6066
|
+
};
|
|
6067
|
+
}
|
|
6068
|
+
function createEmptyLintResult$1() {
|
|
6069
|
+
return summarizeDiagnostics([]);
|
|
6070
|
+
}
|
|
6071
|
+
function sortDiagnostics(diagnostics) {
|
|
6072
|
+
return [...diagnostics].sort((left, right) => {
|
|
6073
|
+
if (left.line !== right.line) return left.line - right.line;
|
|
6074
|
+
if (left.column !== right.column) return left.column - right.column;
|
|
6075
|
+
return left.ruleId.localeCompare(right.ruleId);
|
|
6076
|
+
});
|
|
6077
|
+
}
|
|
5938
6078
|
//#endregion
|
|
5939
|
-
//#region src/
|
|
6079
|
+
//#region src/lint-files.ts
|
|
6080
|
+
const DEFAULT_LINT_FILE_INCLUDE = [
|
|
6081
|
+
"**/*.md",
|
|
6082
|
+
"**/*.markdown",
|
|
6083
|
+
"**/*.mdx"
|
|
6084
|
+
];
|
|
6085
|
+
const DEFAULT_LINT_FILE_EXCLUDE = [
|
|
6086
|
+
"**/node_modules/**",
|
|
6087
|
+
"**/.git/**",
|
|
6088
|
+
"**/dist/**"
|
|
6089
|
+
];
|
|
5940
6090
|
/**
|
|
5941
|
-
*
|
|
5942
|
-
*
|
|
5943
|
-
* Renders JSX theme components to static HTML strings.
|
|
5944
|
-
* No client-side JavaScript is included by default.
|
|
6091
|
+
* Returns true if the file path is included by the configured glob filters.
|
|
5945
6092
|
*/
|
|
5946
|
-
|
|
6093
|
+
function shouldLintMarkdownFile(filePath, options = {}) {
|
|
6094
|
+
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
6095
|
+
return shouldLintAbsoluteFile(path$1.resolve(resolvedOptions.cwd, filePath), resolvedOptions);
|
|
6096
|
+
}
|
|
5947
6097
|
/**
|
|
5948
|
-
*
|
|
6098
|
+
* Lints a single Markdown file using project-style include/exclude settings.
|
|
5949
6099
|
*
|
|
5950
|
-
*
|
|
5951
|
-
*
|
|
5952
|
-
* @returns Rendered HTML string
|
|
6100
|
+
* If the file is filtered out by `include` / `exclude`, the returned result is
|
|
6101
|
+
* marked as `skipped` and contains no diagnostics.
|
|
5953
6102
|
*/
|
|
5954
|
-
function
|
|
5955
|
-
const
|
|
5956
|
-
|
|
5957
|
-
page: {
|
|
5958
|
-
title: page.title,
|
|
5959
|
-
description: page.description,
|
|
5960
|
-
html: page.html,
|
|
5961
|
-
toc: page.toc,
|
|
5962
|
-
lastUpdated: page.lastUpdated,
|
|
5963
|
-
path: page.path,
|
|
5964
|
-
url: page.url,
|
|
5965
|
-
frontmatter: page.frontmatter,
|
|
5966
|
-
layout: page.layout
|
|
5967
|
-
},
|
|
5968
|
-
site: {
|
|
5969
|
-
name: siteName,
|
|
5970
|
-
base,
|
|
5971
|
-
nav,
|
|
5972
|
-
pages: pages.map((p) => ({
|
|
5973
|
-
title: p.title,
|
|
5974
|
-
description: p.description,
|
|
5975
|
-
html: p.html,
|
|
5976
|
-
toc: p.toc,
|
|
5977
|
-
lastUpdated: p.lastUpdated,
|
|
5978
|
-
path: p.path,
|
|
5979
|
-
url: p.url,
|
|
5980
|
-
frontmatter: p.frontmatter,
|
|
5981
|
-
layout: p.layout
|
|
5982
|
-
}))
|
|
5983
|
-
}
|
|
5984
|
-
});
|
|
5985
|
-
try {
|
|
5986
|
-
const result = theme({ children: raw(page.html) });
|
|
5987
|
-
const html = renderToString(result);
|
|
5988
|
-
if (!html.trimStart().toLowerCase().startsWith("<!doctype")) return `<!DOCTYPE html>\n${html}`;
|
|
5989
|
-
return html;
|
|
5990
|
-
} finally {
|
|
5991
|
-
clearRenderContext();
|
|
5992
|
-
}
|
|
6103
|
+
async function lintMarkdownFile(filePath, options = {}) {
|
|
6104
|
+
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
6105
|
+
return lintMarkdownFileWithResolvedOptions(path$1.resolve(resolvedOptions.cwd, filePath), resolvedOptions);
|
|
5993
6106
|
}
|
|
5994
6107
|
/**
|
|
5995
|
-
*
|
|
5996
|
-
*
|
|
5997
|
-
* @param pages - All pages to render
|
|
5998
|
-
* @param options - Theme render options
|
|
5999
|
-
* @returns Map of output paths to rendered HTML
|
|
6108
|
+
* Lints all Markdown files matched by the configured include/exclude patterns.
|
|
6000
6109
|
*/
|
|
6001
|
-
async function
|
|
6002
|
-
const
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6110
|
+
async function lintMarkdownFiles(options = {}) {
|
|
6111
|
+
const resolvedOptions = resolveMarkdownLintFileOptions(options);
|
|
6112
|
+
const matchedFiles = await collectMarkdownLintFileEntries(resolvedOptions);
|
|
6113
|
+
const results = await lintMarkdownDocumentsAsync(await Promise.all(matchedFiles.map((file) => fs.readFile(file.filePath, "utf-8"))), resolvedOptions.lintOptions);
|
|
6114
|
+
const files = matchedFiles.map((file, index) => ({
|
|
6115
|
+
...results[index] ?? createEmptyLintResult(),
|
|
6116
|
+
filePath: file.filePath,
|
|
6117
|
+
relativePath: file.relativePath,
|
|
6118
|
+
skipped: false
|
|
6119
|
+
}));
|
|
6120
|
+
const diagnostics = files.flatMap((fileResult) => fileResult.diagnostics.map((diagnostic) => ({
|
|
6121
|
+
...diagnostic,
|
|
6122
|
+
filePath: fileResult.filePath,
|
|
6123
|
+
relativePath: fileResult.relativePath
|
|
6124
|
+
})));
|
|
6125
|
+
return {
|
|
6126
|
+
checkedFileCount: files.length,
|
|
6127
|
+
diagnostics,
|
|
6128
|
+
errorCount: files.reduce((count, fileResult) => count + fileResult.errorCount, 0),
|
|
6129
|
+
files,
|
|
6130
|
+
infoCount: files.reduce((count, fileResult) => count + fileResult.infoCount, 0),
|
|
6131
|
+
warningCount: files.reduce((count, fileResult) => count + fileResult.warningCount, 0)
|
|
6132
|
+
};
|
|
6133
|
+
}
|
|
6134
|
+
function resolveMarkdownLintFileOptions(options) {
|
|
6135
|
+
return {
|
|
6136
|
+
cwd: path$1.resolve(options.cwd ?? process.cwd()),
|
|
6137
|
+
exclude: [.../* @__PURE__ */ new Set([...options.exclude ?? DEFAULT_LINT_FILE_EXCLUDE, ...options.ignore ?? []])],
|
|
6138
|
+
include: [...new Set(options.include ?? DEFAULT_LINT_FILE_INCLUDE)],
|
|
6139
|
+
lintOptions: {
|
|
6140
|
+
dictionary: options.dictionary,
|
|
6141
|
+
languages: options.languages,
|
|
6142
|
+
rules: options.rules
|
|
6143
|
+
}
|
|
6144
|
+
};
|
|
6145
|
+
}
|
|
6146
|
+
async function lintMarkdownFileWithResolvedOptions(filePath, options) {
|
|
6147
|
+
const absoluteFilePath = path$1.resolve(filePath);
|
|
6148
|
+
const relativePath = normalizePath(path$1.relative(options.cwd, absoluteFilePath));
|
|
6149
|
+
if (!shouldLintAbsoluteFile(absoluteFilePath, options)) return {
|
|
6150
|
+
...createEmptyLintResult(),
|
|
6151
|
+
filePath: absoluteFilePath,
|
|
6152
|
+
relativePath,
|
|
6153
|
+
skipped: true
|
|
6154
|
+
};
|
|
6155
|
+
return {
|
|
6156
|
+
...await lintMarkdownAsync(await fs.readFile(absoluteFilePath, "utf-8"), options.lintOptions),
|
|
6157
|
+
filePath: absoluteFilePath,
|
|
6158
|
+
relativePath,
|
|
6159
|
+
skipped: false
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
async function collectMarkdownLintFileEntries(options) {
|
|
6163
|
+
const files = /* @__PURE__ */ new Map();
|
|
6164
|
+
for (const pattern of options.include) {
|
|
6165
|
+
const matches = await glob(pattern, {
|
|
6166
|
+
absolute: true,
|
|
6167
|
+
cwd: options.cwd,
|
|
6168
|
+
ignore: options.exclude,
|
|
6169
|
+
nodir: true
|
|
6007
6170
|
});
|
|
6008
|
-
|
|
6171
|
+
for (const filePath of matches) {
|
|
6172
|
+
const absoluteFilePath = path$1.resolve(filePath);
|
|
6173
|
+
if (shouldLintAbsoluteFile(absoluteFilePath, options)) files.set(absoluteFilePath, {
|
|
6174
|
+
filePath: absoluteFilePath,
|
|
6175
|
+
relativePath: normalizePath(path$1.relative(options.cwd, absoluteFilePath))
|
|
6176
|
+
});
|
|
6177
|
+
}
|
|
6009
6178
|
}
|
|
6010
|
-
|
|
6011
|
-
return results;
|
|
6012
|
-
}
|
|
6013
|
-
/**
|
|
6014
|
-
* Generates TypeScript type definitions from page frontmatter.
|
|
6015
|
-
*
|
|
6016
|
-
* @param pages - All pages
|
|
6017
|
-
* @param outDir - Output directory for types
|
|
6018
|
-
*/
|
|
6019
|
-
async function generateTypes(pages, outDir) {
|
|
6020
|
-
const types = generateFrontmatterTypes(pages.map((p) => p.frontmatter));
|
|
6021
|
-
const typesPath = join(outDir, "page-props.d.ts");
|
|
6022
|
-
await mkdir(dirname(typesPath), { recursive: true });
|
|
6023
|
-
await writeFile(typesPath, types, "utf-8");
|
|
6179
|
+
return [...files.values()].sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
6024
6180
|
}
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
return { __html: `<!DOCTYPE html>
|
|
6034
|
-
<html lang="en">
|
|
6035
|
-
<head>
|
|
6036
|
-
<meta charset="UTF-8">
|
|
6037
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6038
|
-
<title>${escapeHtml(page.title)} - ${escapeHtml(site.name)}</title>
|
|
6039
|
-
${page.description ? `<meta name="description" content="${escapeHtml(page.description)}">` : ""}
|
|
6040
|
-
<style>
|
|
6041
|
-
:root {
|
|
6042
|
-
--octc-color-primary: #4f6fae;
|
|
6043
|
-
--octc-color-text: #131a30;
|
|
6044
|
-
--octc-color-bg: #ffffff;
|
|
6045
|
-
--octc-color-bg-alt: #f5f7fb;
|
|
6046
|
-
--octc-color-text-muted: #4f607b;
|
|
6047
|
-
--octc-color-border: #d2dbea;
|
|
6048
|
-
}
|
|
6049
|
-
body {
|
|
6050
|
-
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
6051
|
-
line-height: 1.7;
|
|
6052
|
-
color: var(--octc-color-text);
|
|
6053
|
-
background: var(--octc-color-bg);
|
|
6054
|
-
max-width: 800px;
|
|
6055
|
-
margin: 0 auto;
|
|
6056
|
-
padding: 2rem;
|
|
6057
|
-
}
|
|
6058
|
-
a { color: var(--octc-color-primary); }
|
|
6059
|
-
</style>
|
|
6060
|
-
</head>
|
|
6061
|
-
<body>
|
|
6062
|
-
<header>
|
|
6063
|
-
<h1>${escapeHtml(site.name)}</h1>
|
|
6064
|
-
</header>
|
|
6065
|
-
<main>
|
|
6066
|
-
${children.__html}
|
|
6067
|
-
</main>
|
|
6068
|
-
</body>
|
|
6069
|
-
</html>` };
|
|
6181
|
+
function shouldLintAbsoluteFile(filePath, options) {
|
|
6182
|
+
const absolutePath = normalizePath(path$1.resolve(filePath));
|
|
6183
|
+
const relativePath = normalizePath(path$1.relative(options.cwd, absolutePath));
|
|
6184
|
+
const matches = (patterns) => patterns.some((pattern) => {
|
|
6185
|
+
const normalizedPattern = normalizePath(pattern);
|
|
6186
|
+
return path$1.matchesGlob(relativePath, normalizedPattern) || path$1.matchesGlob(absolutePath, normalizedPattern);
|
|
6187
|
+
});
|
|
6188
|
+
return matches(options.include) && !matches(options.exclude);
|
|
6070
6189
|
}
|
|
6071
|
-
function
|
|
6072
|
-
return
|
|
6190
|
+
function normalizePath(value) {
|
|
6191
|
+
return value.split(path$1.sep).join("/");
|
|
6073
6192
|
}
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
* import { DefaultLayout } from './layouts/Default';
|
|
6081
|
-
* import { EntryLayout } from './layouts/Entry';
|
|
6082
|
-
*
|
|
6083
|
-
* export default createTheme({
|
|
6084
|
-
* layouts: {
|
|
6085
|
-
* default: DefaultLayout,
|
|
6086
|
-
* entry: EntryLayout,
|
|
6087
|
-
* },
|
|
6088
|
-
* });
|
|
6089
|
-
* ```
|
|
6090
|
-
*/
|
|
6091
|
-
function createTheme(config) {
|
|
6092
|
-
const { layouts, defaultLayout = "default" } = config;
|
|
6093
|
-
return function ThemeWithLayouts({ children }) {
|
|
6094
|
-
const { usePageProps } = (init_page_context(), __toCommonJS(page_context_exports));
|
|
6095
|
-
const layoutName = usePageProps().layout ?? defaultLayout;
|
|
6096
|
-
const Layout = layouts[layoutName] ?? layouts[defaultLayout];
|
|
6097
|
-
if (!Layout) throw new Error(`[ox-content] Layout "${layoutName}" not found. Available layouts: ${Object.keys(layouts).join(", ")}`);
|
|
6098
|
-
return Layout({ children });
|
|
6193
|
+
function createEmptyLintResult() {
|
|
6194
|
+
return {
|
|
6195
|
+
diagnostics: [],
|
|
6196
|
+
errorCount: 0,
|
|
6197
|
+
infoCount: 0,
|
|
6198
|
+
warningCount: 0
|
|
6099
6199
|
};
|
|
6100
6200
|
}
|
|
6101
6201
|
//#endregion
|