@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as transformMermaidStatic, t as mermaidClientScript } from "./mermaid2.js";
2
- import { n as transformTabs, t as generateTabsCSS } from "./tabs2.js";
2
+ import { n as resetTabGroupCounter, r as transformTabs, t as generateTabsCSS } from "./tabs2.js";
3
3
  import { n as transformYouTube, t as extractVideoId } from "./youtube2.js";
4
4
  import { i as transformGitHub, n as fetchRepoData, r as prefetchGitHubRepos, t as collectGitHubRepos } from "./github2.js";
5
5
  import { i as transformOgp, n as fetchOgpData, r as prefetchOgpData, t as collectOgpUrls } from "./ogp2.js";
@@ -6764,39 +6764,40 @@ var import_dist = /* @__PURE__ */ __toESM(require_dist(), 1);
6764
6764
  /**
6765
6765
  * Syntax highlighting with Shiki via rehype.
6766
6766
  */
6767
+ const BUILTIN_LANGS = [
6768
+ "javascript",
6769
+ "typescript",
6770
+ "jsx",
6771
+ "tsx",
6772
+ "vue",
6773
+ "svelte",
6774
+ "html",
6775
+ "css",
6776
+ "scss",
6777
+ "json",
6778
+ "yaml",
6779
+ "markdown",
6780
+ "bash",
6781
+ "shell",
6782
+ "rust",
6783
+ "python",
6784
+ "go",
6785
+ "java",
6786
+ "c",
6787
+ "cpp",
6788
+ "sql",
6789
+ "graphql",
6790
+ "diff",
6791
+ "toml"
6792
+ ];
6767
6793
  let highlighterPromise = null;
6768
6794
  /**
6769
6795
  * Get or create the Shiki highlighter.
6770
6796
  */
6771
- async function getHighlighter(theme) {
6797
+ async function getHighlighter(theme, customLangs = []) {
6772
6798
  if (!highlighterPromise) highlighterPromise = createHighlighter({
6773
6799
  themes: [theme],
6774
- langs: [
6775
- "javascript",
6776
- "typescript",
6777
- "jsx",
6778
- "tsx",
6779
- "vue",
6780
- "svelte",
6781
- "html",
6782
- "css",
6783
- "scss",
6784
- "json",
6785
- "yaml",
6786
- "markdown",
6787
- "bash",
6788
- "shell",
6789
- "rust",
6790
- "python",
6791
- "go",
6792
- "java",
6793
- "c",
6794
- "cpp",
6795
- "sql",
6796
- "graphql",
6797
- "diff",
6798
- "toml"
6799
- ]
6800
+ langs: [...BUILTIN_LANGS, ...customLangs]
6800
6801
  });
6801
6802
  return highlighterPromise;
6802
6803
  }
@@ -6804,9 +6805,9 @@ async function getHighlighter(theme) {
6804
6805
  * Rehype plugin for syntax highlighting with Shiki.
6805
6806
  */
6806
6807
  function rehypeShikiHighlight(options) {
6807
- const { theme } = options;
6808
+ const { theme, langs } = options;
6808
6809
  return async (tree) => {
6809
- const highlighter = await getHighlighter(theme);
6810
+ const highlighter = await getHighlighter(theme, langs);
6810
6811
  const visit = async (node) => {
6811
6812
  if ("children" in node) for (let i = 0; i < node.children.length; i++) {
6812
6813
  const child = node.children[i];
@@ -6849,8 +6850,11 @@ function getTextContent(node) {
6849
6850
  /**
6850
6851
  * Apply syntax highlighting to HTML using Shiki.
6851
6852
  */
6852
- async function highlightCode(html, theme = "github-dark") {
6853
- const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeShikiHighlight, { theme }).use(rehypeStringify).process(html);
6853
+ async function highlightCode(html, theme = "github-dark", langs = []) {
6854
+ const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeShikiHighlight, {
6855
+ theme,
6856
+ langs
6857
+ }).use(rehypeStringify).process(html);
6854
6858
  return String(result);
6855
6859
  }
6856
6860
 
@@ -7028,7 +7032,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
7028
7032
  if (options.mermaid) html = await transformMermaidStatic(html);
7029
7033
  const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
7030
7034
  html = protectedHtml;
7031
- if (options.highlight) html = await highlightCode(html, options.highlightTheme);
7035
+ if (options.highlight) html = await highlightCode(html, options.highlightTheme, options.highlightLangs);
7032
7036
  html = restoreMermaidSvgs(html, svgs);
7033
7037
  return {
7034
7038
  code: generateModuleCode(html, frontmatter, toc, filePath, options),
@@ -8379,7 +8383,7 @@ function createVueCompilerPlugin() {
8379
8383
  filename: id,
8380
8384
  id
8381
8385
  });
8382
- if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.join(", ")}`);
8386
+ if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.map(String).join(", ")}`);
8383
8387
  scriptCode = `${templateResult.code}\nexport default { render }`;
8384
8388
  }
8385
8389
  const isTs = !!(descriptor.scriptSetup?.lang === "ts" || descriptor.script?.lang === "ts");
@@ -8717,6 +8721,12 @@ function getComponentName(el) {
8717
8721
  }
8718
8722
  let islandCounter = 0;
8719
8723
  /**
8724
+ * Reset island counter (for testing).
8725
+ */
8726
+ function resetIslandCounter() {
8727
+ islandCounter = 0;
8728
+ }
8729
+ /**
8720
8730
  * Rehype plugin to transform Island components.
8721
8731
  */
8722
8732
  function rehypeIslands(collectedIslands) {
@@ -8810,7 +8820,7 @@ function generateHydrationScript(components) {
8810
8820
  if (components.length === 0) return "";
8811
8821
  return `
8812
8822
  import { initIslands } from '@ox-content/islands';
8813
- ${components.map((name, i) => `import ${name} from './${name}';`).join("\n")}
8823
+ ${components.map((name) => `import ${name} from './${name}';`).join("\n")}
8814
8824
 
8815
8825
  const components = {
8816
8826
  ${components.join(",\n ")}
@@ -10445,6 +10455,207 @@ export default { search, searchOptions, loadIndex };
10445
10455
  `;
10446
10456
  }
10447
10457
 
10458
+ //#endregion
10459
+ //#region src/dev-server.ts
10460
+ /**
10461
+ * Dev server middleware for ox-content SSG.
10462
+ *
10463
+ * Serves fully-rendered HTML pages (with navigation, theme, etc.)
10464
+ * during `vite dev`, matching the SSG build output.
10465
+ */
10466
+ /** File extensions to skip in the middleware. */
10467
+ const SKIP_EXTENSIONS = new Set([
10468
+ ".js",
10469
+ ".ts",
10470
+ ".css",
10471
+ ".scss",
10472
+ ".less",
10473
+ ".svg",
10474
+ ".png",
10475
+ ".jpg",
10476
+ ".jpeg",
10477
+ ".gif",
10478
+ ".webp",
10479
+ ".ico",
10480
+ ".woff",
10481
+ ".woff2",
10482
+ ".ttf",
10483
+ ".eot",
10484
+ ".json",
10485
+ ".map",
10486
+ ".mp4",
10487
+ ".webm",
10488
+ ".mp3",
10489
+ ".pdf"
10490
+ ]);
10491
+ /** Vite internal URL prefixes to skip. */
10492
+ const VITE_INTERNAL_PREFIXES = [
10493
+ "/@vite/",
10494
+ "/@fs/",
10495
+ "/@id/",
10496
+ "/__"
10497
+ ];
10498
+ /**
10499
+ * Check if a request URL should be skipped by the dev server middleware.
10500
+ */
10501
+ function shouldSkip(url) {
10502
+ for (const prefix of VITE_INTERNAL_PREFIXES) if (url.startsWith(prefix)) return true;
10503
+ if (url.includes("/node_modules/")) return true;
10504
+ const extMatch = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
10505
+ if (extMatch) {
10506
+ const ext = "." + extMatch[1].toLowerCase();
10507
+ if (SKIP_EXTENSIONS.has(ext)) return true;
10508
+ }
10509
+ return false;
10510
+ }
10511
+ /**
10512
+ * Resolve a request URL to a markdown file path.
10513
+ * Returns null if no matching file exists.
10514
+ */
10515
+ async function resolveMarkdownFile(url, srcDir) {
10516
+ let pathname = url.split("?")[0].split("#")[0];
10517
+ if (pathname.endsWith("/index.html")) pathname = pathname.slice(0, -11) || "/";
10518
+ if (pathname !== "/" && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
10519
+ let relativePath;
10520
+ if (pathname === "/") relativePath = "index.md";
10521
+ else relativePath = pathname.slice(1) + ".md";
10522
+ const filePath = path.join(srcDir, relativePath);
10523
+ try {
10524
+ await fs.access(filePath);
10525
+ return filePath;
10526
+ } catch {
10527
+ const indexPath = path.join(srcDir, pathname === "/" ? "" : pathname.slice(1), "index.md");
10528
+ try {
10529
+ await fs.access(indexPath);
10530
+ return indexPath;
10531
+ } catch {
10532
+ return null;
10533
+ }
10534
+ }
10535
+ }
10536
+ /**
10537
+ * Inject Vite HMR client script into the HTML.
10538
+ */
10539
+ function injectViteHmrClient(html) {
10540
+ 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>");
10541
+ }
10542
+ /**
10543
+ * Create a dev server cache instance.
10544
+ */
10545
+ function createDevServerCache() {
10546
+ return {
10547
+ navGroups: null,
10548
+ pages: /* @__PURE__ */ new Map(),
10549
+ siteName: null
10550
+ };
10551
+ }
10552
+ /**
10553
+ * Invalidate navigation cache (called on file add/unlink).
10554
+ */
10555
+ function invalidateNavCache(cache) {
10556
+ cache.navGroups = null;
10557
+ cache.pages.clear();
10558
+ }
10559
+ /**
10560
+ * Invalidate page cache for a specific file (called on file change).
10561
+ */
10562
+ function invalidatePageCache(cache, filePath) {
10563
+ cache.pages.delete(filePath);
10564
+ }
10565
+ /**
10566
+ * Resolve site name from options or package.json.
10567
+ */
10568
+ async function resolveSiteName(options, root) {
10569
+ if (options.ssg.siteName) return options.ssg.siteName;
10570
+ try {
10571
+ const pkgPath = path.join(root, "package.json");
10572
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
10573
+ if (pkg.name) return formatTitle(pkg.name);
10574
+ } catch {}
10575
+ return "Documentation";
10576
+ }
10577
+ /**
10578
+ * Render a single markdown page to full HTML.
10579
+ */
10580
+ async function renderPage$1(filePath, options, navGroups, siteName, base, root) {
10581
+ const srcDir = path.resolve(root, options.srcDir);
10582
+ resetTabGroupCounter();
10583
+ resetIslandCounter();
10584
+ const result = await transformMarkdown(await fs.readFile(filePath, "utf-8"), filePath, options, {
10585
+ convertMdLinks: true,
10586
+ baseUrl: base,
10587
+ sourcePath: filePath
10588
+ });
10589
+ let transformedHtml = result.html;
10590
+ const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(transformedHtml);
10591
+ transformedHtml = protectedHtml;
10592
+ transformedHtml = await transformAllPlugins(transformedHtml, {
10593
+ tabs: true,
10594
+ youtube: true,
10595
+ github: true,
10596
+ ogp: true,
10597
+ mermaid: true,
10598
+ githubToken: process.env.GITHUB_TOKEN
10599
+ });
10600
+ if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
10601
+ transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
10602
+ const title = extractTitle$1(transformedHtml, result.frontmatter);
10603
+ const description = result.frontmatter.description;
10604
+ let entryPage;
10605
+ if (result.frontmatter.layout === "entry") entryPage = {
10606
+ hero: result.frontmatter.hero,
10607
+ features: result.frontmatter.features
10608
+ };
10609
+ let html = await generateHtmlPage({
10610
+ title,
10611
+ description,
10612
+ content: transformedHtml,
10613
+ toc: result.toc,
10614
+ frontmatter: result.frontmatter,
10615
+ path: getUrlPath$1(filePath, srcDir),
10616
+ href: getUrlPath$1(filePath, srcDir) || "/",
10617
+ entryPage
10618
+ }, navGroups, siteName, base, options.ssg.ogImage, options.ssg.theme);
10619
+ html = injectViteHmrClient(html);
10620
+ return html;
10621
+ }
10622
+ /**
10623
+ * Create the dev server middleware for SSG page serving.
10624
+ */
10625
+ function createDevServerMiddleware(options, root, cache) {
10626
+ const srcDir = path.resolve(root, options.srcDir);
10627
+ const base = options.base.endsWith("/") ? options.base : options.base + "/";
10628
+ return async (req, res, next) => {
10629
+ const url = req.url;
10630
+ if (!url) return next();
10631
+ let routeUrl = url;
10632
+ if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
10633
+ if (shouldSkip(routeUrl)) return next();
10634
+ const filePath = await resolveMarkdownFile(routeUrl, srcDir);
10635
+ if (!filePath) return next();
10636
+ try {
10637
+ const cached = cache.pages.get(filePath);
10638
+ if (cached) {
10639
+ res.setHeader("Content-Type", "text/html");
10640
+ res.setHeader("Cache-Control", "no-cache");
10641
+ res.end(cached);
10642
+ return;
10643
+ }
10644
+ if (!cache.siteName) cache.siteName = await resolveSiteName(options, root);
10645
+ if (!cache.navGroups) cache.navGroups = buildNavItems(await collectMarkdownFiles$1(srcDir), srcDir, base, ".html");
10646
+ const html = await renderPage$1(filePath, options, cache.navGroups, cache.siteName, base, root);
10647
+ cache.pages.set(filePath, html);
10648
+ res.setHeader("Content-Type", "text/html");
10649
+ res.setHeader("Cache-Control", "no-cache");
10650
+ res.end(html);
10651
+ } catch (err) {
10652
+ const message = err instanceof Error ? err.message : String(err);
10653
+ console.error(`[ox-content:dev] Failed to render ${filePath}:`, message);
10654
+ next();
10655
+ }
10656
+ };
10657
+ }
10658
+
10448
10659
  //#endregion
10449
10660
  //#region src/og-viewer.ts
10450
10661
  /**
@@ -10770,7 +10981,7 @@ function createOgViewerPlugin(options) {
10770
10981
  res.end(html);
10771
10982
  } catch (err) {
10772
10983
  res.statusCode = 500;
10773
- res.end(`OG Viewer error: ${err}`);
10984
+ res.end(`OG Viewer error: ${err instanceof Error ? err.message : String(err)}`);
10774
10985
  }
10775
10986
  return;
10776
10987
  }
@@ -11444,8 +11655,24 @@ function oxContent(options = {}) {
11444
11655
  });
11445
11656
  }
11446
11657
  };
11658
+ const ssgDevCache = createDevServerCache();
11447
11659
  const ssgPlugin = {
11448
11660
  name: "ox-content:ssg",
11661
+ configureServer(devServer) {
11662
+ if (!resolvedOptions.ssg.enabled) return;
11663
+ const root = config?.root || process.cwd();
11664
+ const srcDir = path.resolve(root, resolvedOptions.srcDir);
11665
+ devServer.middlewares.use(createDevServerMiddleware(resolvedOptions, root, ssgDevCache));
11666
+ devServer.watcher.on("add", (file) => {
11667
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidateNavCache(ssgDevCache);
11668
+ });
11669
+ devServer.watcher.on("unlink", (file) => {
11670
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidateNavCache(ssgDevCache);
11671
+ });
11672
+ devServer.watcher.on("change", (file) => {
11673
+ if (file.startsWith(srcDir) && file.endsWith(".md")) invalidatePageCache(ssgDevCache, file);
11674
+ });
11675
+ },
11449
11676
  async closeBundle() {
11450
11677
  if (!resolvedOptions.ssg.enabled) return;
11451
11678
  const root = config?.root || process.cwd();
@@ -11521,6 +11748,7 @@ function resolveOptions(options) {
11521
11748
  strikethrough: options.strikethrough ?? true,
11522
11749
  highlight: options.highlight ?? false,
11523
11750
  highlightTheme: options.highlightTheme ?? "github-dark",
11751
+ highlightLangs: options.highlightLangs ?? [],
11524
11752
  mermaid: options.mermaid ?? false,
11525
11753
  frontmatter: options.frontmatter ?? true,
11526
11754
  toc: options.toc ?? true,