@ox-content/vite-plugin 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6725,39 +6725,40 @@ var import_dist = /* @__PURE__ */ require_chunk.__toESM(require_dist(), 1);
6725
6725
  /**
6726
6726
  * Syntax highlighting with Shiki via rehype.
6727
6727
  */
6728
+ const BUILTIN_LANGS = [
6729
+ "javascript",
6730
+ "typescript",
6731
+ "jsx",
6732
+ "tsx",
6733
+ "vue",
6734
+ "svelte",
6735
+ "html",
6736
+ "css",
6737
+ "scss",
6738
+ "json",
6739
+ "yaml",
6740
+ "markdown",
6741
+ "bash",
6742
+ "shell",
6743
+ "rust",
6744
+ "python",
6745
+ "go",
6746
+ "java",
6747
+ "c",
6748
+ "cpp",
6749
+ "sql",
6750
+ "graphql",
6751
+ "diff",
6752
+ "toml"
6753
+ ];
6728
6754
  let highlighterPromise = null;
6729
6755
  /**
6730
6756
  * Get or create the Shiki highlighter.
6731
6757
  */
6732
- async function getHighlighter(theme) {
6758
+ async function getHighlighter(theme, customLangs = []) {
6733
6759
  if (!highlighterPromise) highlighterPromise = (0, shiki.createHighlighter)({
6734
6760
  themes: [theme],
6735
- langs: [
6736
- "javascript",
6737
- "typescript",
6738
- "jsx",
6739
- "tsx",
6740
- "vue",
6741
- "svelte",
6742
- "html",
6743
- "css",
6744
- "scss",
6745
- "json",
6746
- "yaml",
6747
- "markdown",
6748
- "bash",
6749
- "shell",
6750
- "rust",
6751
- "python",
6752
- "go",
6753
- "java",
6754
- "c",
6755
- "cpp",
6756
- "sql",
6757
- "graphql",
6758
- "diff",
6759
- "toml"
6760
- ]
6761
+ langs: [...BUILTIN_LANGS, ...customLangs]
6761
6762
  });
6762
6763
  return highlighterPromise;
6763
6764
  }
@@ -6765,9 +6766,9 @@ async function getHighlighter(theme) {
6765
6766
  * Rehype plugin for syntax highlighting with Shiki.
6766
6767
  */
6767
6768
  function rehypeShikiHighlight(options) {
6768
- const { theme } = options;
6769
+ const { theme, langs } = options;
6769
6770
  return async (tree) => {
6770
- const highlighter = await getHighlighter(theme);
6771
+ const highlighter = await getHighlighter(theme, langs);
6771
6772
  const visit = async (node) => {
6772
6773
  if ("children" in node) for (let i = 0; i < node.children.length; i++) {
6773
6774
  const child = node.children[i];
@@ -6810,8 +6811,11 @@ function getTextContent(node) {
6810
6811
  /**
6811
6812
  * Apply syntax highlighting to HTML using Shiki.
6812
6813
  */
6813
- async function highlightCode(html, theme = "github-dark") {
6814
- const result = await (0, unified.unified)().use(rehype_parse.default, { fragment: true }).use(rehypeShikiHighlight, { theme }).use(rehype_stringify.default).process(html);
6814
+ async function highlightCode(html, theme = "github-dark", langs = []) {
6815
+ const result = await (0, unified.unified)().use(rehype_parse.default, { fragment: true }).use(rehypeShikiHighlight, {
6816
+ theme,
6817
+ langs
6818
+ }).use(rehype_stringify.default).process(html);
6815
6819
  return String(result);
6816
6820
  }
6817
6821
 
@@ -6989,7 +6993,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
6989
6993
  if (options.mermaid) html = await require_mermaid.transformMermaidStatic(html);
6990
6994
  const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
6991
6995
  html = protectedHtml;
6992
- if (options.highlight) html = await highlightCode(html, options.highlightTheme);
6996
+ if (options.highlight) html = await highlightCode(html, options.highlightTheme, options.highlightLangs);
6993
6997
  html = restoreMermaidSvgs(html, svgs);
6994
6998
  return {
6995
6999
  code: generateModuleCode(html, frontmatter, toc, filePath, options),
@@ -8340,7 +8344,7 @@ function createVueCompilerPlugin() {
8340
8344
  filename: id,
8341
8345
  id
8342
8346
  });
8343
- if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.join(", ")}`);
8347
+ if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.map(String).join(", ")}`);
8344
8348
  scriptCode = `${templateResult.code}\nexport default { render }`;
8345
8349
  }
8346
8350
  const isTs = !!(descriptor.scriptSetup?.lang === "ts" || descriptor.script?.lang === "ts");
@@ -8678,6 +8682,12 @@ function getComponentName(el) {
8678
8682
  }
8679
8683
  let islandCounter = 0;
8680
8684
  /**
8685
+ * Reset island counter (for testing).
8686
+ */
8687
+ function resetIslandCounter() {
8688
+ islandCounter = 0;
8689
+ }
8690
+ /**
8681
8691
  * Rehype plugin to transform Island components.
8682
8692
  */
8683
8693
  function rehypeIslands(collectedIslands) {
@@ -8771,7 +8781,7 @@ function generateHydrationScript(components) {
8771
8781
  if (components.length === 0) return "";
8772
8782
  return `
8773
8783
  import { initIslands } from '@ox-content/islands';
8774
- ${components.map((name, i) => `import ${name} from './${name}';`).join("\n")}
8784
+ ${components.map((name) => `import ${name} from './${name}';`).join("\n")}
8775
8785
 
8776
8786
  const components = {
8777
8787
  ${components.join(",\n ")}
@@ -10406,6 +10416,207 @@ export default { search, searchOptions, loadIndex };
10406
10416
  `;
10407
10417
  }
10408
10418
 
10419
+ //#endregion
10420
+ //#region src/dev-server.ts
10421
+ /**
10422
+ * Dev server middleware for ox-content SSG.
10423
+ *
10424
+ * Serves fully-rendered HTML pages (with navigation, theme, etc.)
10425
+ * during `vite dev`, matching the SSG build output.
10426
+ */
10427
+ /** File extensions to skip in the middleware. */
10428
+ const SKIP_EXTENSIONS = new Set([
10429
+ ".js",
10430
+ ".ts",
10431
+ ".css",
10432
+ ".scss",
10433
+ ".less",
10434
+ ".svg",
10435
+ ".png",
10436
+ ".jpg",
10437
+ ".jpeg",
10438
+ ".gif",
10439
+ ".webp",
10440
+ ".ico",
10441
+ ".woff",
10442
+ ".woff2",
10443
+ ".ttf",
10444
+ ".eot",
10445
+ ".json",
10446
+ ".map",
10447
+ ".mp4",
10448
+ ".webm",
10449
+ ".mp3",
10450
+ ".pdf"
10451
+ ]);
10452
+ /** Vite internal URL prefixes to skip. */
10453
+ const VITE_INTERNAL_PREFIXES = [
10454
+ "/@vite/",
10455
+ "/@fs/",
10456
+ "/@id/",
10457
+ "/__"
10458
+ ];
10459
+ /**
10460
+ * Check if a request URL should be skipped by the dev server middleware.
10461
+ */
10462
+ function shouldSkip(url) {
10463
+ for (const prefix of VITE_INTERNAL_PREFIXES) if (url.startsWith(prefix)) return true;
10464
+ if (url.includes("/node_modules/")) return true;
10465
+ const extMatch = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
10466
+ if (extMatch) {
10467
+ const ext = "." + extMatch[1].toLowerCase();
10468
+ if (SKIP_EXTENSIONS.has(ext)) return true;
10469
+ }
10470
+ return false;
10471
+ }
10472
+ /**
10473
+ * Resolve a request URL to a markdown file path.
10474
+ * Returns null if no matching file exists.
10475
+ */
10476
+ async function resolveMarkdownFile(url, srcDir) {
10477
+ let pathname = url.split("?")[0].split("#")[0];
10478
+ if (pathname.endsWith("/index.html")) pathname = pathname.slice(0, -11) || "/";
10479
+ if (pathname !== "/" && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
10480
+ let relativePath;
10481
+ if (pathname === "/") relativePath = "index.md";
10482
+ else relativePath = pathname.slice(1) + ".md";
10483
+ const filePath = path$1.join(srcDir, relativePath);
10484
+ try {
10485
+ await fs_promises.access(filePath);
10486
+ return filePath;
10487
+ } catch {
10488
+ const indexPath = path$1.join(srcDir, pathname === "/" ? "" : pathname.slice(1), "index.md");
10489
+ try {
10490
+ await fs_promises.access(indexPath);
10491
+ return indexPath;
10492
+ } catch {
10493
+ return null;
10494
+ }
10495
+ }
10496
+ }
10497
+ /**
10498
+ * Inject Vite HMR client script into the HTML.
10499
+ */
10500
+ function injectViteHmrClient(html) {
10501
+ return html.replace("</head>", "<script type=\"module\" src=\"/@vite/client\"><\/script>\n<script type=\"module\">\nif (import.meta.hot) {\n import.meta.hot.on('ox-content:update', () => {\n location.reload();\n });\n}\n<\/script>\n</head>");
10502
+ }
10503
+ /**
10504
+ * Create a dev server cache instance.
10505
+ */
10506
+ function createDevServerCache() {
10507
+ return {
10508
+ navGroups: null,
10509
+ pages: /* @__PURE__ */ new Map(),
10510
+ siteName: null
10511
+ };
10512
+ }
10513
+ /**
10514
+ * Invalidate navigation cache (called on file add/unlink).
10515
+ */
10516
+ function invalidateNavCache(cache) {
10517
+ cache.navGroups = null;
10518
+ cache.pages.clear();
10519
+ }
10520
+ /**
10521
+ * Invalidate page cache for a specific file (called on file change).
10522
+ */
10523
+ function invalidatePageCache(cache, filePath) {
10524
+ cache.pages.delete(filePath);
10525
+ }
10526
+ /**
10527
+ * Resolve site name from options or package.json.
10528
+ */
10529
+ async function resolveSiteName(options, root) {
10530
+ if (options.ssg.siteName) return options.ssg.siteName;
10531
+ try {
10532
+ const pkgPath = path$1.join(root, "package.json");
10533
+ const pkg = JSON.parse(await fs_promises.readFile(pkgPath, "utf-8"));
10534
+ if (pkg.name) return formatTitle(pkg.name);
10535
+ } catch {}
10536
+ return "Documentation";
10537
+ }
10538
+ /**
10539
+ * Render a single markdown page to full HTML.
10540
+ */
10541
+ async function renderPage$1(filePath, options, navGroups, siteName, base, root) {
10542
+ const srcDir = path$1.resolve(root, options.srcDir);
10543
+ require_tabs.resetTabGroupCounter();
10544
+ resetIslandCounter();
10545
+ const result = await transformMarkdown(await fs_promises.readFile(filePath, "utf-8"), filePath, options, {
10546
+ convertMdLinks: true,
10547
+ baseUrl: base,
10548
+ sourcePath: filePath
10549
+ });
10550
+ let transformedHtml = result.html;
10551
+ const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(transformedHtml);
10552
+ transformedHtml = protectedHtml;
10553
+ transformedHtml = await transformAllPlugins(transformedHtml, {
10554
+ tabs: true,
10555
+ youtube: true,
10556
+ github: true,
10557
+ ogp: true,
10558
+ mermaid: true,
10559
+ githubToken: process.env.GITHUB_TOKEN
10560
+ });
10561
+ if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
10562
+ transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
10563
+ const title = extractTitle$1(transformedHtml, result.frontmatter);
10564
+ const description = result.frontmatter.description;
10565
+ let entryPage;
10566
+ if (result.frontmatter.layout === "entry") entryPage = {
10567
+ hero: result.frontmatter.hero,
10568
+ features: result.frontmatter.features
10569
+ };
10570
+ let html = await generateHtmlPage({
10571
+ title,
10572
+ description,
10573
+ content: transformedHtml,
10574
+ toc: result.toc,
10575
+ frontmatter: result.frontmatter,
10576
+ path: getUrlPath$1(filePath, srcDir),
10577
+ href: getUrlPath$1(filePath, srcDir) || "/",
10578
+ entryPage
10579
+ }, navGroups, siteName, base, options.ssg.ogImage, options.ssg.theme);
10580
+ html = injectViteHmrClient(html);
10581
+ return html;
10582
+ }
10583
+ /**
10584
+ * Create the dev server middleware for SSG page serving.
10585
+ */
10586
+ function createDevServerMiddleware(options, root, cache) {
10587
+ const srcDir = path$1.resolve(root, options.srcDir);
10588
+ const base = options.base.endsWith("/") ? options.base : options.base + "/";
10589
+ return async (req, res, next) => {
10590
+ const url = req.url;
10591
+ if (!url) return next();
10592
+ let routeUrl = url;
10593
+ if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
10594
+ if (shouldSkip(routeUrl)) return next();
10595
+ const filePath = await resolveMarkdownFile(routeUrl, srcDir);
10596
+ if (!filePath) return next();
10597
+ try {
10598
+ const cached = cache.pages.get(filePath);
10599
+ if (cached) {
10600
+ res.setHeader("Content-Type", "text/html");
10601
+ res.setHeader("Cache-Control", "no-cache");
10602
+ res.end(cached);
10603
+ return;
10604
+ }
10605
+ if (!cache.siteName) cache.siteName = await resolveSiteName(options, root);
10606
+ if (!cache.navGroups) cache.navGroups = buildNavItems(await collectMarkdownFiles$1(srcDir), srcDir, base, ".html");
10607
+ const html = await renderPage$1(filePath, options, cache.navGroups, cache.siteName, base, root);
10608
+ cache.pages.set(filePath, html);
10609
+ res.setHeader("Content-Type", "text/html");
10610
+ res.setHeader("Cache-Control", "no-cache");
10611
+ res.end(html);
10612
+ } catch (err) {
10613
+ const message = err instanceof Error ? err.message : String(err);
10614
+ console.error(`[ox-content:dev] Failed to render ${filePath}:`, message);
10615
+ next();
10616
+ }
10617
+ };
10618
+ }
10619
+
10409
10620
  //#endregion
10410
10621
  //#region src/og-viewer.ts
10411
10622
  /**
@@ -10731,7 +10942,7 @@ function createOgViewerPlugin(options) {
10731
10942
  res.end(html);
10732
10943
  } catch (err) {
10733
10944
  res.statusCode = 500;
10734
- res.end(`OG Viewer error: ${err}`);
10945
+ res.end(`OG Viewer error: ${err instanceof Error ? err.message : String(err)}`);
10735
10946
  }
10736
10947
  return;
10737
10948
  }
@@ -11405,8 +11616,24 @@ function oxContent(options = {}) {
11405
11616
  });
11406
11617
  }
11407
11618
  };
11619
+ const ssgDevCache = createDevServerCache();
11408
11620
  const ssgPlugin = {
11409
11621
  name: "ox-content:ssg",
11622
+ configureServer(devServer) {
11623
+ if (!resolvedOptions.ssg.enabled) return;
11624
+ const root = config?.root || process.cwd();
11625
+ const srcDir = path$1.resolve(root, resolvedOptions.srcDir);
11626
+ devServer.middlewares.use(createDevServerMiddleware(resolvedOptions, root, ssgDevCache));
11627
+ devServer.watcher.on("add", (file) => {
11628
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidateNavCache(ssgDevCache);
11629
+ });
11630
+ devServer.watcher.on("unlink", (file) => {
11631
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidateNavCache(ssgDevCache);
11632
+ });
11633
+ devServer.watcher.on("change", (file) => {
11634
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidatePageCache(ssgDevCache, file);
11635
+ });
11636
+ },
11410
11637
  async closeBundle() {
11411
11638
  if (!resolvedOptions.ssg.enabled) return;
11412
11639
  const root = config?.root || process.cwd();
@@ -11482,6 +11709,7 @@ function resolveOptions(options) {
11482
11709
  strikethrough: options.strikethrough ?? true,
11483
11710
  highlight: options.highlight ?? false,
11484
11711
  highlightTheme: options.highlightTheme ?? "github-dark",
11712
+ highlightLangs: options.highlightLangs ?? [],
11485
11713
  mermaid: options.mermaid ?? false,
11486
11714
  frontmatter: options.frontmatter ?? true,
11487
11715
  toc: options.toc ?? true,