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