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