@ox-content/vite-plugin 3.0.0-alpha.1 → 3.0.0-alpha.2

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
@@ -12,6 +12,7 @@ let rehype_stringify = require("rehype-stringify");
12
12
  rehype_stringify = require_vitepress.__toESM(rehype_stringify, 1);
13
13
  let node_module = require("node:module");
14
14
  let node_fs = require("node:fs");
15
+ node_fs = require_vitepress.__toESM(node_fs, 1);
15
16
  let node_path = require("node:path");
16
17
  node_path = require_vitepress.__toESM(node_path, 1);
17
18
  let node_fs_promises = require("node:fs/promises");
@@ -24,6 +25,8 @@ let fs_promises = require("fs/promises");
24
25
  fs_promises = require_vitepress.__toESM(fs_promises, 1);
25
26
  let crypto = require("crypto");
26
27
  crypto = require_vitepress.__toESM(crypto, 1);
28
+ let node_crypto = require("node:crypto");
29
+ let node_zlib = require("node:zlib");
27
30
  let fs = require("fs");
28
31
  fs = require_vitepress.__toESM(fs, 1);
29
32
  let glob = require("glob");
@@ -252,6 +255,15 @@ async function highlightCode(html) {
252
255
  const result = await (0, unified.unified)().use(rehypeParse$3, { fragment: true }).use(rehypeNativeHighlight).use(rehypeStringify$3).process(html);
253
256
  return String(result);
254
257
  }
258
+ /**
259
+ * Highlight every code block in a rendered page, preserving original classes
260
+ * and per-line metadata when the native document pass cannot read the markup.
261
+ */
262
+ async function highlightPageHtml(html, mergeHighlightedCodeBlocks) {
263
+ const native = await highlightDocumentNatively(html);
264
+ if (native && native.skipped.length === 0) return native.html;
265
+ return mergeHighlightedCodeBlocks(html, await highlightCode(html));
266
+ }
255
267
  //#endregion
256
268
  //#region src/plugins/mermaid.ts
257
269
  /**
@@ -343,6 +355,69 @@ function warnMissingMmdcOnce() {
343
355
  */
344
356
  const mermaidClientScript = "";
345
357
  //#endregion
358
+ //#region src/plugins/math.ts
359
+ /**
360
+ * Build-time KaTeX rendering for opt-in `$…$` / `$$…$$` math.
361
+ *
362
+ * KaTeX is an optional peer. Sites that never enable `math` do not install it,
363
+ * and the published plugin does not bundle or depend on it.
364
+ */
365
+ const KATEX_ASSET_DIR = "__ox_katex__";
366
+ const MATH_TAG = /<(span|div) class="ox-math ox-math-(inline|block)" data-ox-tex="([^"]*)">[\s\S]*?<\/\1>/g;
367
+ let missingWarned = false;
368
+ /**
369
+ * Replaces rust `ox-math` placeholders with static KaTeX HTML.
370
+ * Leaves the escaped TeX fallback when `katex` is not installed.
371
+ */
372
+ async function renderKatexMath(html) {
373
+ if (!html.includes("data-ox-tex")) return html;
374
+ const katex = loadKatex();
375
+ if (!katex) {
376
+ warnMissingKatexOnce();
377
+ return html;
378
+ }
379
+ return html.replace(MATH_TAG, (_match, tag, kind, encoded) => {
380
+ return `<${tag} class="ox-math ox-math-${kind}">${katex.renderToString(decodeHtmlAttr$2(encoded), {
381
+ displayMode: kind === "block",
382
+ throwOnError: false,
383
+ trust: false,
384
+ output: "htmlAndMathml"
385
+ })}</${tag}>`;
386
+ });
387
+ }
388
+ /** Directory that contains `katex.min.css` and `fonts/`, or `null`. */
389
+ function resolveKatexDist() {
390
+ for (const resolver of createKatexResolvers()) try {
391
+ return (0, node_path.join)((0, node_path.dirname)(resolver.resolve("katex/package.json")), "dist");
392
+ } catch {}
393
+ return null;
394
+ }
395
+ function loadKatex() {
396
+ for (const resolver of createKatexResolvers()) try {
397
+ const loaded = resolver(resolver.resolve("katex"));
398
+ if (typeof loaded.renderToString === "function") return loaded;
399
+ if (loaded.default && typeof loaded.default.renderToString === "function") return loaded.default;
400
+ } catch {}
401
+ return null;
402
+ }
403
+ function createKatexResolvers() {
404
+ const consumerRequire = (0, node_module.createRequire)((0, node_path.join)(process.cwd(), "noop.js"));
405
+ const resolvers = [consumerRequire];
406
+ try {
407
+ resolvers.push((0, node_module.createRequire)(consumerRequire.resolve("@ox-content/vite-plugin")));
408
+ } catch {}
409
+ resolvers.push((0, node_module.createRequire)(require("url").pathToFileURL(__filename).href));
410
+ return resolvers;
411
+ }
412
+ function decodeHtmlAttr$2(value) {
413
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
414
+ }
415
+ function warnMissingKatexOnce() {
416
+ if (missingWarned) return;
417
+ missingWarned = true;
418
+ console.warn("[ox-content] math is enabled but `katex` was not found. Install it with `npm i -D katex` to render LaTeX; escaped TeX placeholders are left as-is.");
419
+ }
420
+ //#endregion
346
421
  //#region src/plugins/pm.ts
347
422
  /**
348
423
  * Package Manager Tabs Plugin
@@ -560,15 +635,15 @@ function assetRecord(src, media) {
560
635
  //#region src/plugins/twitter/render.ts
561
636
  function renderFetchedTweet(permalink, data, assets, options) {
562
637
  const profile = `https://x.com/${encodeURIComponent(data.user.screen_name)}`;
563
- const author = escapeHtml$5(data.user.name);
564
- const handle = escapeHtml$5(data.user.screen_name);
565
- const avatar = assets.avatar ? `<img class="ox-tweet__avatar" src="${escapeAttribute(assets.avatar)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
638
+ const author = escapeHtml$6(data.user.name);
639
+ const handle = escapeHtml$6(data.user.screen_name);
640
+ const avatar = assets.avatar ? `<img class="ox-tweet__avatar" src="${escapeAttribute$2(assets.avatar)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
566
641
  const media = renderMedia(assets);
567
642
  const footer = renderFooter(permalink, data.created_at, options.lang);
568
643
  return [
569
644
  "<figure class=\"ox-tweet ox-tweet--fetched\">",
570
645
  "<header class=\"ox-tweet__header\">",
571
- `<a class="ox-tweet__profile" href="${escapeAttribute(profile)}" target="_blank" rel="noopener noreferrer">`,
646
+ `<a class="ox-tweet__profile" href="${escapeAttribute$2(profile)}" target="_blank" rel="noopener noreferrer">`,
572
647
  avatar,
573
648
  `<span class="ox-tweet__author-name">${author}</span>`,
574
649
  `<span class="ox-tweet__author-handle">@${handle}</span>`,
@@ -591,7 +666,7 @@ function renderTweetText(data) {
591
666
  if (entity.kind === "url") {
592
667
  const href = entity.expanded_url ?? entity.url;
593
668
  const label = entity.display_url ?? href;
594
- output += `<a href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$5(label)}</a>`;
669
+ output += `<a href="${escapeAttribute$2(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
595
670
  }
596
671
  cursor = entityEnd;
597
672
  }
@@ -614,12 +689,12 @@ function renderMedia(assets) {
614
689
  if (assets.media.length === 0) return "";
615
690
  const images = assets.media.map((item) => {
616
691
  const size = [item.width ? ` width="${item.width}"` : "", item.height ? ` height="${item.height}"` : ""].join("");
617
- return `<img class="ox-tweet__media-item" src="${escapeAttribute(item.src)}" alt="${escapeAttribute(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
692
+ return `<img class="ox-tweet__media-item" src="${escapeAttribute$2(item.src)}" alt="${escapeAttribute$2(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
618
693
  }).join("");
619
694
  return `<div class="ox-tweet__media" data-count="${assets.media.length}">${images}</div>`;
620
695
  }
621
696
  function renderFooter(permalink, createdAt, lang) {
622
- if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute(permalink)}" target="_blank" rel="noopener noreferrer">View on X</a></footer>`;
697
+ if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$2(permalink)}" target="_blank" rel="noopener noreferrer">View on X</a></footer>`;
623
698
  const date = new Date(createdAt);
624
699
  if (Number.isNaN(date.valueOf())) return renderFooter(permalink, void 0, lang);
625
700
  const iso = date.toISOString();
@@ -635,15 +710,15 @@ function renderFooter(permalink, createdAt, lang) {
635
710
  timeZone: "UTC"
636
711
  }).format(date);
637
712
  }
638
- return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute(permalink)}" target="_blank" rel="noopener noreferrer"><time datetime="${iso}">${escapeHtml$5(label)}</time></a></footer>`;
713
+ return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$2(permalink)}" target="_blank" rel="noopener noreferrer"><time datetime="${iso}">${escapeHtml$6(label)}</time></a></footer>`;
639
714
  }
640
715
  function escapeText(value) {
641
- return escapeHtml$5(value).replaceAll("\n", "<br>");
716
+ return escapeHtml$6(value).replaceAll("\n", "<br>");
642
717
  }
643
- function escapeAttribute(value) {
644
- return escapeHtml$5(value).replaceAll("`", "&#96;");
718
+ function escapeAttribute$2(value) {
719
+ return escapeHtml$6(value).replaceAll("`", "&#96;");
645
720
  }
646
- function escapeHtml$5(value) {
721
+ function escapeHtml$6(value) {
647
722
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
648
723
  }
649
724
  //#endregion
@@ -764,6 +839,11 @@ function sourceKey(source) {
764
839
  function formatLineRange(lines) {
765
840
  return lines.start === lines.end ? `L${lines.start}` : `L${lines.start}-L${lines.end}`;
766
841
  }
842
+ function summarizeCommitMessage(message) {
843
+ const firstLine = message.split(/\r?\n/, 1)[0]?.replace(/\s+/g, " ").trim() ?? "";
844
+ if (firstLine.length <= 120) return firstLine;
845
+ return `${firstLine.slice(0, 119)}…`;
846
+ }
767
847
  function parseGitHubLineRange(value) {
768
848
  if (!value) return void 0;
769
849
  const match = value.trim().match(/^#?L?(\d+)(?:-L?(\d+))?$/i);
@@ -838,6 +918,24 @@ function githubHeaders(options) {
838
918
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
839
919
  return headers;
840
920
  }
921
+ async function fetchSourceCommit(source, options) {
922
+ try {
923
+ const apiUrl = `https://api.github.com/repos/${source.repo}/commits?path=${encodeURIComponent(source.path)}&sha=${encodeURIComponent(source.ref)}&per_page=1`;
924
+ const response = await fetch(apiUrl, { headers: githubHeaders(options) });
925
+ if (!response.ok) return;
926
+ const item = (await response.json())[0];
927
+ const sha = item?.sha;
928
+ const message = item?.commit?.message ? summarizeCommitMessage(item.commit.message) : "";
929
+ if (!sha || !message) return;
930
+ return {
931
+ sha,
932
+ message,
933
+ html_url: item.html_url ?? `https://github.com/${source.repo}/commit/${sha}`
934
+ };
935
+ } catch {
936
+ return;
937
+ }
938
+ }
841
939
  /**
842
940
  * Fetch repository data from GitHub API.
843
941
  */
@@ -876,7 +974,7 @@ async function fetchGitHubSource(source, options) {
876
974
  }
877
975
  try {
878
976
  const apiUrl = `https://api.github.com/repos/${source.repo}/contents/${encodePath(source.path)}?ref=${encodeURIComponent(source.ref)}`;
879
- const response = await fetch(apiUrl, { headers: githubHeaders(options) });
977
+ const [response, commit] = await Promise.all([fetch(apiUrl, { headers: githubHeaders(options) }), fetchSourceCommit(source, options)]);
880
978
  if (!response.ok) {
881
979
  console.warn(`Failed to fetch GitHub source ${source.permalink}: ${response.status}`);
882
980
  return null;
@@ -893,7 +991,8 @@ async function fetchGitHubSource(source, options) {
893
991
  content,
894
992
  size: data.size ?? node_buffer.Buffer.byteLength(content),
895
993
  html_url: data.html_url ?? source.permalink,
896
- language: inferLanguage(source.path)
994
+ language: inferLanguage(source.path),
995
+ ...commit ? { commit } : {}
897
996
  };
898
997
  if (options.cache) sourceCache.set(key, {
899
998
  data: sourceData,
@@ -1187,19 +1286,76 @@ function normalizeSourceLines(content) {
1187
1286
  if (lines.length > 1 && lines.at(-1) === "") lines.pop();
1188
1287
  return lines.length > 0 ? lines : [""];
1189
1288
  }
1289
+ function text(value) {
1290
+ return {
1291
+ type: "text",
1292
+ value
1293
+ };
1294
+ }
1295
+ function createSourceLines(lines, start) {
1296
+ return lines.flatMap((line, index) => {
1297
+ const lineNumber = start + index;
1298
+ const span = {
1299
+ type: "element",
1300
+ tagName: "span",
1301
+ properties: {
1302
+ className: ["line"],
1303
+ "data-line": String(lineNumber),
1304
+ "data-line-number": String(lineNumber)
1305
+ },
1306
+ children: [text(line)]
1307
+ };
1308
+ return index === 0 ? [span] : [text("\n"), span];
1309
+ });
1310
+ }
1311
+ function createCommitMeta(commit) {
1312
+ return {
1313
+ type: "element",
1314
+ tagName: "a",
1315
+ properties: {
1316
+ className: ["ox-github-code-commit"],
1317
+ href: commit.html_url,
1318
+ target: "_blank",
1319
+ rel: "noopener noreferrer",
1320
+ title: commit.message
1321
+ },
1322
+ children: [{
1323
+ type: "element",
1324
+ tagName: "span",
1325
+ properties: { className: ["ox-github-code-sha"] },
1326
+ children: [text(commit.sha.slice(0, 7))]
1327
+ }, {
1328
+ type: "element",
1329
+ tagName: "span",
1330
+ properties: { className: ["ox-github-code-commit-message"] },
1331
+ children: [text(commit.message)]
1332
+ }]
1333
+ };
1334
+ }
1190
1335
  function createGitHubSourceCard(source, lines, options) {
1191
1336
  const allLines = normalizeSourceLines(source.content);
1192
1337
  const start = Math.min(lines?.start ?? 1, allLines.length);
1193
1338
  const end = lines ? Math.min(lines.end, allLines.length) : Math.min(allLines.length, options.maxSourceLines);
1194
1339
  const selectedLines = allLines.slice(start - 1, end);
1195
- const lineRange = {
1340
+ const loc = selectedLines.length;
1341
+ const rangeLabel = formatLineRange({
1196
1342
  start,
1197
1343
  end
1198
- };
1199
- const loc = selectedLines.length;
1200
- const rangeLabel = formatLineRange(lineRange);
1201
- const locLabel = !lines && end < allLines.length ? `${rangeLabel} of ${allLines.length} LOC` : `${rangeLabel} - ${loc} LOC`;
1344
+ });
1345
+ const locLabel = !lines && end < allLines.length ? `${rangeLabel} of ${allLines.length} LOC` : `${rangeLabel} · ${loc} LOC`;
1202
1346
  const languageClass = source.language ? [`language-${source.language}`] : [];
1347
+ const heading = [{
1348
+ type: "element",
1349
+ tagName: "a",
1350
+ properties: {
1351
+ className: ["ox-github-code-title"],
1352
+ href: source.permalink,
1353
+ target: "_blank",
1354
+ rel: "noopener noreferrer"
1355
+ },
1356
+ children: [text(`${source.repo}/${source.path}`)]
1357
+ }];
1358
+ if (source.commit) heading.push(createCommitMeta(source.commit));
1203
1359
  return {
1204
1360
  type: "element",
1205
1361
  tagName: "figure",
@@ -1214,65 +1370,34 @@ function createGitHubSourceCard(source, lines, options) {
1214
1370
  properties: { className: ["ox-github-code-header"] },
1215
1371
  children: [{
1216
1372
  type: "element",
1217
- tagName: "a",
1218
- properties: {
1219
- className: ["ox-github-code-title"],
1220
- href: source.permalink,
1221
- target: "_blank",
1222
- rel: "noopener noreferrer"
1223
- },
1224
- children: [{
1225
- type: "text",
1226
- value: `${source.repo}/${source.path}`
1227
- }]
1373
+ tagName: "div",
1374
+ properties: { className: ["ox-github-code-heading"] },
1375
+ children: heading
1228
1376
  }, {
1229
1377
  type: "element",
1230
1378
  tagName: "span",
1231
1379
  properties: { className: ["ox-github-code-loc"] },
1232
- children: [{
1233
- type: "text",
1234
- value: locLabel
1235
- }]
1380
+ children: [text(locLabel)]
1236
1381
  }]
1237
1382
  }, {
1238
1383
  type: "element",
1239
1384
  tagName: "pre",
1240
1385
  properties: {
1241
- className: ["ox-github-code-block", ...languageClass],
1386
+ className: [
1387
+ "ox-github-code-block",
1388
+ "ox-code-block",
1389
+ "line-numbers-mode",
1390
+ ...languageClass
1391
+ ],
1392
+ "data-line-numbers": "true",
1393
+ "data-line-number-start": String(start),
1242
1394
  ...source.language ? { "data-language": source.language } : {}
1243
1395
  },
1244
1396
  children: [{
1245
1397
  type: "element",
1246
1398
  tagName: "code",
1247
1399
  properties: { className: languageClass },
1248
- children: selectedLines.map((line, index) => {
1249
- const lineNumber = start + index;
1250
- return {
1251
- type: "element",
1252
- tagName: "span",
1253
- properties: {
1254
- className: ["line", "ox-github-code-line"],
1255
- "data-line": String(lineNumber)
1256
- },
1257
- children: [{
1258
- type: "element",
1259
- tagName: "span",
1260
- properties: { className: ["ox-github-code-line-number"] },
1261
- children: [{
1262
- type: "text",
1263
- value: String(lineNumber)
1264
- }]
1265
- }, {
1266
- type: "element",
1267
- tagName: "span",
1268
- properties: { className: ["ox-github-code-line-content"] },
1269
- children: [{
1270
- type: "text",
1271
- value: line || " "
1272
- }]
1273
- }]
1274
- };
1275
- })
1400
+ children: createSourceLines(selectedLines, start)
1276
1401
  }]
1277
1402
  }]
1278
1403
  };
@@ -1871,6 +1996,368 @@ function normalizeDiagnostic(diagnostic) {
1871
1996
  };
1872
1997
  }
1873
1998
  //#endregion
1999
+ //#region src/typed-hover-generate.ts
2000
+ async function loadTsgoApi() {
2001
+ try {
2002
+ return await Promise.resolve().then(() => require("./api.cjs"));
2003
+ } catch {
2004
+ return;
2005
+ }
2006
+ }
2007
+ async function generateTypedHoverAttachments(fences, tsgoCommand) {
2008
+ if (fences.length === 0) return [];
2009
+ const apiMod = await loadTsgoApi();
2010
+ if (!apiMod) return fences.map((fence) => ({
2011
+ code: fence.code,
2012
+ hovers: []
2013
+ }));
2014
+ const temp = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "ox-content-typed-hover-"));
2015
+ const api = new apiMod.API({
2016
+ cwd: temp,
2017
+ ...tsgoCommand ? { tsserverPath: tsgoCommand } : {}
2018
+ });
2019
+ try {
2020
+ const files = await Promise.all(fences.map(async (fence, index) => {
2021
+ const extension = fence.language.toLowerCase() === "tsx" ? "tsx" : "ts";
2022
+ const file = (0, node_path.join)(temp, `snippet-${index}.${extension}`);
2023
+ await (0, node_fs_promises.writeFile)(file, fence.code);
2024
+ return {
2025
+ fence,
2026
+ file
2027
+ };
2028
+ }));
2029
+ const snapshot = api.updateSnapshot({ openFiles: files.map((item) => item.file) });
2030
+ return files.map(({ fence, file }) => {
2031
+ const project = snapshot.getDefaultProjectForFile(file);
2032
+ if (!project) return {
2033
+ code: fence.code,
2034
+ hovers: []
2035
+ };
2036
+ const hovers = [];
2037
+ for (const ident of collectIdentifierRanges(fence.code)) {
2038
+ const type = project.checker.getTypeAtPosition(file, ident.start);
2039
+ if (!type || type.isErrorType?.()) continue;
2040
+ const widened = project.checker.getBaseTypeOfLiteralType(type) ?? type;
2041
+ const text = project.checker.typeToString(widened);
2042
+ if (text) hovers.push({
2043
+ start: ident.start,
2044
+ end: ident.end,
2045
+ type: text
2046
+ });
2047
+ }
2048
+ return {
2049
+ code: fence.code,
2050
+ hovers
2051
+ };
2052
+ });
2053
+ } finally {
2054
+ api.close();
2055
+ await (0, node_fs_promises.rm)(temp, {
2056
+ recursive: true,
2057
+ force: true
2058
+ });
2059
+ }
2060
+ }
2061
+ const IDENTIFIER_KEYWORDS = /* @__PURE__ */ new Set([
2062
+ "abstract",
2063
+ "any",
2064
+ "as",
2065
+ "asserts",
2066
+ "async",
2067
+ "await",
2068
+ "bigint",
2069
+ "boolean",
2070
+ "break",
2071
+ "case",
2072
+ "catch",
2073
+ "class",
2074
+ "const",
2075
+ "continue",
2076
+ "debugger",
2077
+ "declare",
2078
+ "default",
2079
+ "delete",
2080
+ "do",
2081
+ "else",
2082
+ "enum",
2083
+ "export",
2084
+ "extends",
2085
+ "false",
2086
+ "finally",
2087
+ "for",
2088
+ "from",
2089
+ "function",
2090
+ "if",
2091
+ "implements",
2092
+ "import",
2093
+ "in",
2094
+ "infer",
2095
+ "instanceof",
2096
+ "interface",
2097
+ "is",
2098
+ "keyof",
2099
+ "let",
2100
+ "never",
2101
+ "new",
2102
+ "null",
2103
+ "number",
2104
+ "object",
2105
+ "of",
2106
+ "package",
2107
+ "private",
2108
+ "protected",
2109
+ "public",
2110
+ "readonly",
2111
+ "return",
2112
+ "satisfies",
2113
+ "static",
2114
+ "string",
2115
+ "super",
2116
+ "switch",
2117
+ "symbol",
2118
+ "this",
2119
+ "throw",
2120
+ "true",
2121
+ "try",
2122
+ "type",
2123
+ "typeof",
2124
+ "undefined",
2125
+ "unique",
2126
+ "unknown",
2127
+ "using",
2128
+ "var",
2129
+ "void",
2130
+ "while",
2131
+ "with",
2132
+ "yield"
2133
+ ]);
2134
+ function collectIdentifierRanges(code) {
2135
+ const ranges = [];
2136
+ let index = 0;
2137
+ while (index < code.length) {
2138
+ const char = code[index];
2139
+ if (char === "/" && code[index + 1] === "/") {
2140
+ index = code.indexOf("\n", index);
2141
+ if (index === -1) break;
2142
+ continue;
2143
+ }
2144
+ if (char === "/" && code[index + 1] === "*") {
2145
+ const close = code.indexOf("*/", index + 2);
2146
+ index = close === -1 ? code.length : close + 2;
2147
+ continue;
2148
+ }
2149
+ if (char === "\"" || char === "'" || char === "`") {
2150
+ index = skipQuoted(code, index, char);
2151
+ continue;
2152
+ }
2153
+ if (/[A-Za-z_$]/.test(char)) {
2154
+ const start = index;
2155
+ index += 1;
2156
+ while (index < code.length && /[\w$]/.test(code[index])) index += 1;
2157
+ const name = code.slice(start, index);
2158
+ if (!IDENTIFIER_KEYWORDS.has(name)) ranges.push({
2159
+ start,
2160
+ end: index
2161
+ });
2162
+ continue;
2163
+ }
2164
+ index += 1;
2165
+ }
2166
+ return ranges;
2167
+ }
2168
+ function skipQuoted(code, start, quote) {
2169
+ let index = start + 1;
2170
+ while (index < code.length) {
2171
+ if (code[index] === "\\") {
2172
+ index += 2;
2173
+ continue;
2174
+ }
2175
+ if (code[index] === quote) return index + 1;
2176
+ index += 1;
2177
+ }
2178
+ return code.length;
2179
+ }
2180
+ //#endregion
2181
+ //#region src/typed-hover.ts
2182
+ const DEFAULT_LANGUAGES$1 = ["ts", "tsx"];
2183
+ function resolveTypedHoverOptions(options) {
2184
+ if (!options) return {
2185
+ enabled: false,
2186
+ languages: [...DEFAULT_LANGUAGES$1]
2187
+ };
2188
+ if (options === true) return {
2189
+ enabled: true,
2190
+ languages: [...DEFAULT_LANGUAGES$1]
2191
+ };
2192
+ return {
2193
+ enabled: options.enabled ?? true,
2194
+ languages: options.languages ?? [...DEFAULT_LANGUAGES$1],
2195
+ tsgoCommand: options.tsgoCommand
2196
+ };
2197
+ }
2198
+ function hasTypedHoverMeta(meta) {
2199
+ return meta.split(/\s+/).some((token) => token === "twoslash");
2200
+ }
2201
+ function serializeTypedHoverPayload(payload) {
2202
+ return JSON.stringify(payload).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
2203
+ }
2204
+ async function applyTypedHover(source, html, options) {
2205
+ if (!options?.enabled || !source.includes("```")) return html;
2206
+ const languages = new Set(options.languages.map((language) => language.toLowerCase()));
2207
+ const fences = (await extractCodeBlocks(source)).filter((block) => {
2208
+ return languages.has(block.language.toLowerCase()) && hasTypedHoverMeta(block.meta);
2209
+ });
2210
+ if (fences.length === 0) return html;
2211
+ try {
2212
+ return attachTypedHoverPayloads(html, await generateTypedHoverAttachments(fences, options.tsgoCommand));
2213
+ } catch {
2214
+ return html;
2215
+ }
2216
+ }
2217
+ function attachTypedHoverPayloads(html, attachments) {
2218
+ const unused = attachments.filter((item) => item.hovers.length > 0);
2219
+ if (unused.length === 0) return html;
2220
+ let attached = 0;
2221
+ const next = html.replace(/<pre(\b[^>]*)><code(\b[^>]*)>([\s\S]*?)<\/code><\/pre>/g, (full, preAttrs, codeAttrs, inner) => {
2222
+ if (unused.length === 0) return full;
2223
+ if (!isTypeScriptFence(codeAttrs)) return full;
2224
+ const text = decodeHtmlEntities(inner.replace(/<[^>]+>/g, ""));
2225
+ const index = unused.findIndex((item) => normalizeFenceText(item.code) === normalizeFenceText(text));
2226
+ if (index === -1) return full;
2227
+ const item = unused.splice(index, 1)[0];
2228
+ if (!item) return full;
2229
+ attached += 1;
2230
+ const wrapped = wrapHoverRanges(inner, item.hovers);
2231
+ return `${withTypedHoverClass(`<pre${preAttrs}`)}><code${codeAttrs}>${wrapped}</code></pre>\n<script type="application/json" class="ox-typed-hover-data">${serializeTypedHoverPayload({ hovers: item.hovers })}<\/script>`;
2232
+ });
2233
+ if (attached === 0) return html;
2234
+ return `${next}${TYPED_HOVER_STYLE}${TYPED_HOVER_CLIENT}`;
2235
+ }
2236
+ function isTypeScriptFence(codeAttrs) {
2237
+ const match = codeAttrs.match(/class="([^"]*)"/);
2238
+ if (!match?.[1]) return false;
2239
+ return match[1].split(/\s+/).some((token) => {
2240
+ const language = token.replace(/^language-/, "").toLowerCase();
2241
+ return language === "ts" || language === "tsx" || language === "typescript" || language === "typescriptreact";
2242
+ });
2243
+ }
2244
+ function withTypedHoverClass(openPre) {
2245
+ if (/\bclass="/.test(openPre)) return openPre.replace(/\bclass="([^"]*)"/, (_, classes) => {
2246
+ return `class="${classes} ox-typed-hover"`;
2247
+ });
2248
+ return `${openPre} class="ox-typed-hover"`;
2249
+ }
2250
+ function wrapHoverRanges(inner, hovers) {
2251
+ const ranges = [...hovers].sort((a, b) => a.start - b.start || b.end - a.end);
2252
+ let output = "";
2253
+ let htmlIndex = 0;
2254
+ let sourceOffset = 0;
2255
+ let rangeIndex = 0;
2256
+ let openUntil = -1;
2257
+ let openHoverIndex = -1;
2258
+ const startRange = () => {
2259
+ while (rangeIndex < ranges.length && ranges[rangeIndex].start < sourceOffset) rangeIndex += 1;
2260
+ const range = ranges[rangeIndex];
2261
+ if (!range || range.start !== sourceOffset || openUntil !== -1) return;
2262
+ output += `<span class="ox-typed-hover-token" tabindex="0" data-ox-typed-hover="${rangeIndex}">`;
2263
+ openUntil = range.end;
2264
+ openHoverIndex = rangeIndex;
2265
+ rangeIndex += 1;
2266
+ };
2267
+ const endRange = () => {
2268
+ if (openUntil === sourceOffset && openHoverIndex !== -1) {
2269
+ output += "</span>";
2270
+ openUntil = -1;
2271
+ openHoverIndex = -1;
2272
+ }
2273
+ };
2274
+ while (htmlIndex < inner.length) {
2275
+ const char = inner[htmlIndex];
2276
+ if (char === "<") {
2277
+ const close = inner.indexOf(">", htmlIndex);
2278
+ const tag = close === -1 ? inner.slice(htmlIndex) : inner.slice(htmlIndex, close + 1);
2279
+ output += tag;
2280
+ htmlIndex += tag.length;
2281
+ continue;
2282
+ }
2283
+ startRange();
2284
+ if (char === "&") {
2285
+ const semi = inner.indexOf(";", htmlIndex);
2286
+ const entity = semi === -1 ? inner.slice(htmlIndex) : inner.slice(htmlIndex, semi + 1);
2287
+ output += entity;
2288
+ htmlIndex += entity.length;
2289
+ sourceOffset += 1;
2290
+ endRange();
2291
+ continue;
2292
+ }
2293
+ output += char;
2294
+ htmlIndex += 1;
2295
+ sourceOffset += 1;
2296
+ endRange();
2297
+ }
2298
+ if (openHoverIndex !== -1) output += "</span>";
2299
+ return output;
2300
+ }
2301
+ function normalizeFenceText(value) {
2302
+ return decodeHtmlEntities(value).replace(/\r\n/g, "\n").trim();
2303
+ }
2304
+ function decodeHtmlEntities(value) {
2305
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
2306
+ }
2307
+ const TYPED_HOVER_STYLE = `<style data-ox-typed-hover-style>.ox-typed-hover-token{cursor:help;text-decoration:underline dotted}.ox-typed-hover-overlay{position:fixed;z-index:50;max-width:36rem;padding:.35rem .55rem;border:1px solid #444;border-radius:4px;background:#1e1e1e;color:#d4d4d4;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;pointer-events:none}</style>`;
2308
+ const TYPED_HOVER_CLIENT = `<script data-ox-typed-hover-runtime>(function(){if(window.__oxTypedHover)return;window.__oxTypedHover=1;var tip=document.createElement("div");tip.className="ox-typed-hover-overlay";tip.setAttribute("role","tooltip");tip.hidden=true;document.body.appendChild(tip);function payload(token){var pre=token.closest(".ox-typed-hover");var data=pre&&pre.nextElementSibling;if(!data||!data.classList.contains("ox-typed-hover-data"))return null;try{return JSON.parse(data.textContent||"")}catch(e){return null}}function show(token){var data=payload(token);var item=data&&data.hovers[Number(token.getAttribute("data-ox-typed-hover"))];if(!item)return;tip.textContent=item.type;tip.hidden=false;var box=token.getBoundingClientRect();tip.style.left=Math.max(8,box.left)+"px";tip.style.top=Math.max(8,box.top-tip.offsetHeight-8)+"px"}function hide(){tip.hidden=true}document.addEventListener("mouseover",function(e){var t=e.target.closest(".ox-typed-hover-token");if(t)show(t)});document.addEventListener("mouseout",function(e){var t=e.target.closest(".ox-typed-hover-token");if(t&&!t.contains(e.relatedTarget))hide()});document.addEventListener("focusin",function(e){var t=e.target.closest(".ox-typed-hover-token");if(t)show(t)});document.addEventListener("focusout",function(e){if(!e.relatedTarget||!e.relatedTarget.closest(".ox-typed-hover-token"))hide()});document.addEventListener("keydown",function(e){if(e.key==="Escape")hide()})})();<\/script>`;
2309
+ //#endregion
2310
+ //#region src/file-tree-options.ts
2311
+ const disabled = {
2312
+ enabled: false,
2313
+ defaultOpen: true,
2314
+ icons: true
2315
+ };
2316
+ function resolveFileTreeOptions(options) {
2317
+ if (!options) return { ...disabled };
2318
+ if (options === true) return enabledDefaults();
2319
+ if (options.enabled === false) return {
2320
+ ...enabledDefaults(),
2321
+ enabled: false,
2322
+ ...resolveIcons(options.icons)
2323
+ };
2324
+ return {
2325
+ enabled: options.enabled ?? true,
2326
+ defaultOpen: options.defaultOpen ?? true,
2327
+ ...resolveIcons(options.icons)
2328
+ };
2329
+ }
2330
+ function toJsFileTreeOptions(options) {
2331
+ if (!options?.enabled) return void 0;
2332
+ return {
2333
+ enabled: true,
2334
+ defaultOpen: options.defaultOpen,
2335
+ icons: options.icons,
2336
+ iconFolder: options.iconFolder,
2337
+ iconFolderOpen: options.iconFolderOpen,
2338
+ iconFile: options.iconFile,
2339
+ iconFiles: options.iconFiles
2340
+ };
2341
+ }
2342
+ function enabledDefaults() {
2343
+ return {
2344
+ enabled: true,
2345
+ defaultOpen: true,
2346
+ icons: true
2347
+ };
2348
+ }
2349
+ function resolveIcons(icons) {
2350
+ if (icons === false) return { icons: false };
2351
+ if (icons === true || icons == null) return { icons: true };
2352
+ return {
2353
+ icons: true,
2354
+ iconFolder: icons.folder,
2355
+ iconFolderOpen: icons.folderOpen,
2356
+ iconFile: icons.file,
2357
+ iconFiles: icons.files
2358
+ };
2359
+ }
2360
+ //#endregion
1874
2361
  //#region src/transform.ts
1875
2362
  /**
1876
2363
  * The NAPI load, cached as the promise rather than as its result.
@@ -1981,7 +2468,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
1981
2468
  } : void 0,
1982
2469
  cards: options.cards?.enabled ? { enabled: true } : void 0,
1983
2470
  steps: options.steps?.enabled ? { enabled: true } : void 0,
1984
- fileTree: options.fileTree?.enabled ? { enabled: true } : void 0,
2471
+ fileTree: toJsFileTreeOptions(options.fileTree),
1985
2472
  sanitize: void 0,
1986
2473
  editThisPage: options.editThisPage?.enabled ? {
1987
2474
  enabled: true,
@@ -1999,26 +2486,27 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
1999
2486
  if (options.mermaid) html = await transformMermaidStatic(html);
2000
2487
  const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
2001
2488
  html = protectedHtml;
2002
- if (options.highlight) {
2003
- const native = await highlightDocumentNatively(html);
2004
- if (native && native.skipped.length === 0) html = native.html;
2005
- else {
2006
- const originalHtml = html;
2007
- const highlightedHtml = await highlightCode(html);
2008
- html = napi.mergeHighlightedCodeBlocks(originalHtml, highlightedHtml);
2009
- }
2010
- }
2489
+ if (options.highlight) html = await highlightPageHtml(html, napi.mergeHighlightedCodeBlocks);
2011
2490
  html = await transformBuiltinEmbeds(html, options.embeds ?? {
2012
2491
  github: {},
2013
2492
  openGraph: {}
2014
2493
  });
2494
+ if (options.highlight && html.includes("ox-github-code-block")) html = await highlightPageHtml(html, napi.mergeHighlightedCodeBlocks);
2015
2495
  html = restoreMermaidSvgs(html, svgs);
2016
2496
  if (options.sanitize?.enabled) html = napi.sanitizeHtml(html, toJsSanitizeOptions(options.sanitize));
2497
+ if (isMathEnabled(options.math)) html = await renderKatexMath(html);
2498
+ const imports = result.imports ?? [];
2499
+ const exports = result.exports ?? [];
2500
+ const components = result.components ?? [];
2501
+ html = await applyTypedHover(source, html, options.typedHover);
2017
2502
  return {
2018
- code: generateModuleCode(html, frontmatter, toc, filePath, options),
2503
+ code: generateModuleCode(html, frontmatter, toc, imports, exports, components, filePath),
2019
2504
  html,
2020
2505
  frontmatter,
2021
- toc
2506
+ toc,
2507
+ imports,
2508
+ exports,
2509
+ components
2022
2510
  };
2023
2511
  }
2024
2512
  async function runCodeBlockTypecheck(source, options) {
@@ -2077,8 +2565,11 @@ function normalizeTocEntry(entry) {
2077
2565
  }
2078
2566
  /**
2079
2567
  * Generates the JavaScript module code.
2568
+ *
2569
+ * MDX metadata is serialized as JSON. User `import` / `export` source is never
2570
+ * emitted as live JavaScript, so transform does not execute module side effects.
2080
2571
  */
2081
- function generateModuleCode(html, frontmatter, toc, filePath, _options) {
2572
+ function generateModuleCode(html, frontmatter, toc, imports, exports, components, filePath) {
2082
2573
  return `
2083
2574
  // Generated by @ox-content/vite-plugin
2084
2575
  // Source: ${filePath}
@@ -2098,6 +2589,21 @@ export const frontmatter = ${JSON.stringify(frontmatter)};
2098
2589
  */
2099
2590
  export const toc = ${JSON.stringify(toc)};
2100
2591
 
2592
+ /**
2593
+ * MDX import statements collected from the AST.
2594
+ */
2595
+ export const imports = ${JSON.stringify(imports)};
2596
+
2597
+ /**
2598
+ * MDX export names collected from the AST.
2599
+ */
2600
+ export const exports = ${JSON.stringify(exports)};
2601
+
2602
+ /**
2603
+ * Unique JSX component names collected from the AST.
2604
+ */
2605
+ export const components = ${JSON.stringify(components)};
2606
+
2101
2607
  /**
2102
2608
  * Default export with all data.
2103
2609
  */
@@ -2105,6 +2611,9 @@ export default {
2105
2611
  html,
2106
2612
  frontmatter,
2107
2613
  toc,
2614
+ imports,
2615
+ exports,
2616
+ components,
2108
2617
  };
2109
2618
 
2110
2619
  // HMR support
@@ -2555,7 +3064,7 @@ function formatChromiumUnavailableDetail(err) {
2555
3064
  /**
2556
3065
  * Escapes HTML special characters.
2557
3066
  */
2558
- function escapeHtml$4(str) {
3067
+ function escapeHtml$5(str) {
2559
3068
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2560
3069
  }
2561
3070
  function normalizeBrandValue(str) {
@@ -2618,12 +3127,12 @@ function getDefaultTemplate() {
2618
3127
  const isBrandCard = normalizeBrandValue(title) === normalizeBrandValue(rawBrand);
2619
3128
  const heroTitle = isBrandCard ? "High-performance Markdown toolkit" : title;
2620
3129
  const heroDescription = isBrandCard ? "Rust-powered docs and high-performance Markdown tooling." : description && description.trim().length > 0 ? description : "Rust-powered docs and Markdown tooling.";
2621
- const descriptionHtml = heroDescription.trim().length > 0 ? `<p style="max-width:760px;font-size:28px;color:#93a4c3;line-height:1.45;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$4(heroDescription)}</p>` : "";
3130
+ const descriptionHtml = heroDescription.trim().length > 0 ? `<p style="max-width:760px;font-size:28px;color:#93a4c3;line-height:1.45;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$5(heroDescription)}</p>` : "";
2622
3131
  return `<div style="width:100%;height:100%;position:relative;overflow:hidden;box-sizing:border-box;padding:56px 64px 52px;background:#0b1220;font-family:'IBM Plex Sans','Avenir Next','Segoe UI',system-ui,sans-serif;color:#eff6ff;border:1px solid #223252;border-top:4px solid #4f6fae;">
2623
3132
  <div style="position:relative;z-index:1;display:flex;flex-direction:column;height:100%;">
2624
3133
  <div style="display:flex;align-items:flex-start;">${renderWordmarkSvg()}</div>
2625
3134
  <div style="display:flex;flex-direction:column;justify-content:center;gap:24px;max-width:860px;flex:1;padding:22px 0 0;">
2626
- <h1 style="font-size:78px;font-weight:700;color:#eff6ff;line-height:1.02;letter-spacing:-0.055em;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$4(heroTitle)}</h1>
3135
+ <h1 style="font-size:78px;font-weight:700;color:#eff6ff;line-height:1.02;letter-spacing:-0.055em;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$5(heroTitle)}</h1>
2627
3136
  ${descriptionHtml}
2628
3137
  </div>
2629
3138
  </div>
@@ -3154,6 +3663,80 @@ async function renderSinglePage(entry, templateFn, templateSource, options, cach
3154
3663
  }
3155
3664
  }
3156
3665
  //#endregion
3666
+ //#region src/plugins/math-assets.ts
3667
+ /**
3668
+ * Serve and copy KaTeX CSS/fonts only when the optional `katex` package exists.
3669
+ */
3670
+ /**
3671
+ * Copies `katex.min.css` and `fonts/` into the SSG output.
3672
+ * Returns an empty list when KaTeX is not installed.
3673
+ */
3674
+ async function copyKatexAssets(outDir) {
3675
+ const dist = resolveKatexDist();
3676
+ if (!dist) return [];
3677
+ const dest = (0, node_path.join)(outDir, KATEX_ASSET_DIR);
3678
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dest, "fonts"), { recursive: true });
3679
+ const cssDest = (0, node_path.join)(dest, "katex.min.css");
3680
+ await (0, node_fs_promises.copyFile)((0, node_path.join)(dist, "katex.min.css"), cssDest);
3681
+ await (0, node_fs_promises.cp)((0, node_path.join)(dist, "fonts"), (0, node_path.join)(dest, "fonts"), { recursive: true });
3682
+ return [cssDest];
3683
+ }
3684
+ /** Dev-server middleware that serves `/__ox_katex__/*` from `katex/dist`. */
3685
+ function createKatexAssetsPlugin() {
3686
+ return {
3687
+ name: "ox-content:katex-assets",
3688
+ configureServer(server) {
3689
+ const dist = resolveKatexDist();
3690
+ if (!dist) return;
3691
+ server.middlewares.use((req, res, next) => {
3692
+ const url = req.url ?? "";
3693
+ const marker = `/${KATEX_ASSET_DIR}/`;
3694
+ const index = url.indexOf(marker);
3695
+ if (index === -1) {
3696
+ next();
3697
+ return;
3698
+ }
3699
+ const rel = decodeURIComponent(url.slice(index + marker.length).split("?")[0] ?? "");
3700
+ const file = safeKatexFile(dist, rel);
3701
+ if (!file) {
3702
+ res.statusCode = 404;
3703
+ res.end();
3704
+ return;
3705
+ }
3706
+ (0, node_fs_promises.stat)(file).then((info) => {
3707
+ if (!info.isFile()) {
3708
+ res.statusCode = 404;
3709
+ res.end();
3710
+ return;
3711
+ }
3712
+ res.setHeader("Content-Type", katexContentType(file));
3713
+ (0, node_fs.createReadStream)(file).pipe(res);
3714
+ }).catch(() => {
3715
+ res.statusCode = 404;
3716
+ res.end();
3717
+ });
3718
+ });
3719
+ }
3720
+ };
3721
+ }
3722
+ function safeKatexFile(dist, rel) {
3723
+ if (!rel || rel.includes("\0") || rel.split(/[\\/]/).includes("..")) return null;
3724
+ const full = (0, node_path.resolve)(dist, rel);
3725
+ const root = (0, node_path.resolve)(dist) + node_path.sep;
3726
+ if (full !== (0, node_path.resolve)(dist) && !full.startsWith(root)) return null;
3727
+ const inside = (0, node_path.relative)(dist, full);
3728
+ if (inside.startsWith("..") || inside.includes(`..${node_path.sep}`)) return null;
3729
+ return full;
3730
+ }
3731
+ function katexContentType(file) {
3732
+ const ext = (0, node_path.extname)(file);
3733
+ if (ext === ".css") return "text/css; charset=utf-8";
3734
+ if (ext === ".woff2") return "font/woff2";
3735
+ if (ext === ".woff") return "font/woff";
3736
+ if (ext === ".ttf") return "font/ttf";
3737
+ return "application/octet-stream";
3738
+ }
3739
+ //#endregion
3157
3740
  //#region src/island/parse.ts
3158
3741
  /**
3159
3742
  * Island Parser
@@ -3762,6 +4345,7 @@ function renderPage(page, options) {
3762
4345
  html: page.html,
3763
4346
  toc: page.toc,
3764
4347
  lastUpdated: page.lastUpdated,
4348
+ contributors: page.contributors,
3765
4349
  path: page.path,
3766
4350
  url: page.url,
3767
4351
  frontmatter: page.frontmatter,
@@ -3777,6 +4361,7 @@ function renderPage(page, options) {
3777
4361
  html: p.html,
3778
4362
  toc: p.toc,
3779
4363
  lastUpdated: p.lastUpdated,
4364
+ contributors: p.contributors,
3780
4365
  path: p.path,
3781
4366
  url: p.url,
3782
4367
  frontmatter: p.frontmatter,
@@ -3837,8 +4422,8 @@ function DefaultTheme({ children }) {
3837
4422
  <head>
3838
4423
  <meta charset="UTF-8">
3839
4424
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
3840
- <title>${escapeHtml$3(page.title)} - ${escapeHtml$3(site.name)}</title>
3841
- ${page.description ? `<meta name="description" content="${escapeHtml$3(page.description)}">` : ""}
4425
+ <title>${escapeHtml$4(page.title)} - ${escapeHtml$4(site.name)}</title>
4426
+ ${page.description ? `<meta name="description" content="${escapeHtml$4(page.description)}">` : ""}
3842
4427
  <style>
3843
4428
  :root {
3844
4429
  --octc-color-primary: #4f6fae;
@@ -3862,7 +4447,7 @@ function DefaultTheme({ children }) {
3862
4447
  </head>
3863
4448
  <body>
3864
4449
  <header>
3865
- <h1>${escapeHtml$3(site.name)}</h1>
4450
+ <h1>${escapeHtml$4(site.name)}</h1>
3866
4451
  </header>
3867
4452
  <main>
3868
4453
  ${children.__html}
@@ -3870,7 +4455,7 @@ function DefaultTheme({ children }) {
3870
4455
  </body>
3871
4456
  </html>` };
3872
4457
  }
3873
- function escapeHtml$3(str) {
4458
+ function escapeHtml$4(str) {
3874
4459
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3875
4460
  }
3876
4461
  /**
@@ -3907,7 +4492,7 @@ function createTheme(config) {
3907
4492
  * String bodies follow `ox_content_ssg::generate_site_maps`. The Vite plugin
3908
4493
  * writes those files during SSG without adding a NAPI surface.
3909
4494
  */
3910
- const MISSING_SITE_URL$1 = "[ox-content] siteMaps is enabled but ssg.siteUrl is not set; sitemap.xml, robots.txt, and llms.txt were not written";
4495
+ const MISSING_SITE_URL$2 = "[ox-content] siteMaps is enabled but ssg.siteUrl is not set; sitemap.xml, robots.txt, and llms.txt were not written";
3911
4496
  /**
3912
4497
  * Resolves `siteMaps` with defaults.
3913
4498
  *
@@ -3934,7 +4519,7 @@ function resolveSiteMapsOptions(value) {
3934
4519
  /** Builds sitemap / robots / llms bodies without writing files. */
3935
4520
  function generateSiteMaps(input) {
3936
4521
  if (!input.options?.enabled) return {};
3937
- if (!hasSiteUrl$1(input.siteUrl)) return { warning: MISSING_SITE_URL$1 };
4522
+ if (!hasSiteUrl$2(input.siteUrl)) return { warning: MISSING_SITE_URL$2 };
3938
4523
  const published = input.pages.filter((page) => !page.draft && !page.unlisted && page.loc.length > 0).slice().sort((left, right) => left.loc < right.loc ? -1 : left.loc > right.loc ? 1 : 0);
3939
4524
  const result = { sitemapXml: generateSitemapXml(published) };
3940
4525
  if (input.options.robots) result.robotsTxt = generateRobotsTxt(input.sitemapLoc ?? "");
@@ -3970,11 +4555,11 @@ async function writeSiteMapFiles(input) {
3970
4555
  }
3971
4556
  return { files };
3972
4557
  }
3973
- function hasSiteUrl$1(siteUrl) {
4558
+ function hasSiteUrl$2(siteUrl) {
3974
4559
  return Boolean(siteUrl && siteUrl.trim());
3975
4560
  }
3976
4561
  function absoluteSitemapUrl(siteUrl, base) {
3977
- if (!hasSiteUrl$1(siteUrl)) return "";
4562
+ if (!hasSiteUrl$2(siteUrl)) return "";
3978
4563
  return `${(siteUrl ?? "").trim().replace(/\/+$/, "")}${!base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`}sitemap.xml`;
3979
4564
  }
3980
4565
  function generateSitemapXml(pages) {
@@ -4160,7 +4745,7 @@ function resolvePageRoutes(input) {
4160
4745
  if (!input.permalinks?.enabled) return {
4161
4746
  pages: cascaded.map((page) => ({
4162
4747
  source: page.source,
4163
- urlPath: normalizeUrlPath(page.fileUrl),
4748
+ urlPath: normalizeUrlPath$1(page.fileUrl),
4164
4749
  frontmatter: page.frontmatter
4165
4750
  })),
4166
4751
  errors: []
@@ -4188,7 +4773,7 @@ function resolvePageRoutes(input) {
4188
4773
  errors
4189
4774
  };
4190
4775
  }
4191
- function normalizeUrlPath(value) {
4776
+ function normalizeUrlPath$1(value) {
4192
4777
  const segments = pathSegments(value);
4193
4778
  return segments.length === 0 ? "/" : segments.join("/");
4194
4779
  }
@@ -4222,10 +4807,10 @@ function applyCascade(pages, options) {
4222
4807
  });
4223
4808
  }
4224
4809
  function resolveOne(page) {
4225
- const fileUrl = normalizeUrlPath(page.fileUrl);
4810
+ const fileUrl = normalizeUrlPath$1(page.fileUrl);
4226
4811
  const permalink = readString(page.frontmatter.permalink);
4227
4812
  if (permalink !== void 0) {
4228
- const url = isSafePermalink(permalink) ? normalizeUrlPath(permalink) : void 0;
4813
+ const url = isSafePermalink(permalink) ? normalizeUrlPath$1(permalink) : void 0;
4229
4814
  return url ? { urlPath: url } : {
4230
4815
  urlPath: fileUrl,
4231
4816
  error: `[ox-content] rejected permalink ${JSON.stringify(permalink)} on ${page.source} (path escape); using the file-tree URL`
@@ -4244,7 +4829,7 @@ function resolveOne(page) {
4244
4829
  function rewriteSlug(fileUrl, slug) {
4245
4830
  const trimmed = slug.trim();
4246
4831
  if (trimmed.includes("/") || !isSafePermalink(trimmed)) return;
4247
- const normalized = normalizeUrlPath(trimmed);
4832
+ const normalized = normalizeUrlPath$1(trimmed);
4248
4833
  if (normalized === "/") return;
4249
4834
  if (fileUrl === "/") return normalized;
4250
4835
  const segments = fileUrl.split("/").filter(Boolean);
@@ -4361,8 +4946,8 @@ function applyCollectionRoutes(manifest, permalinks, cascade) {
4361
4946
  }
4362
4947
  /** Updates auto-nav hrefs after permalinks change a page URL. */
4363
4948
  function remapNavGroups(nav, kept, skippedFileUrls) {
4364
- const skipped = new Set(skippedFileUrls.map(normalizeUrlPath));
4365
- const byFile = new Map(kept.map((page) => [normalizeUrlPath(page.fileUrl), page]));
4949
+ const skipped = new Set(skippedFileUrls.map(normalizeUrlPath$1));
4950
+ const byFile = new Map(kept.map((page) => [normalizeUrlPath$1(page.fileUrl), page]));
4366
4951
  return nav.map((group) => ({
4367
4952
  ...group,
4368
4953
  items: remapNavItems(group.items, byFile, skipped)
@@ -4374,7 +4959,7 @@ function routePathsFromUrl(urlPath, srcDir, outDir, base, extension, siteUrl) {
4374
4959
  }
4375
4960
  function remapNavItems(items, byFile, skipped) {
4376
4961
  return items.flatMap((item) => {
4377
- const key = normalizeUrlPath(item.path);
4962
+ const key = normalizeUrlPath$1(item.path);
4378
4963
  if (skipped.has(key)) return [];
4379
4964
  const hit = byFile.get(key);
4380
4965
  const children = item.children ? remapNavItems(item.children, byFile, skipped) : void 0;
@@ -4518,7 +5103,7 @@ async function writeRedirectFiles(input) {
4518
5103
  }
4519
5104
  /** Static HTML redirect body. `dest` is escaped. */
4520
5105
  function generateRedirectHtml(dest) {
4521
- const escaped = escapeHtml$2(dest);
5106
+ const escaped = escapeHtml$3(dest);
4522
5107
  return `\
4523
5108
  <!DOCTYPE html>
4524
5109
  <html lang="en">
@@ -4603,7 +5188,7 @@ function upsert(files, index, occupied, from, to, base) {
4603
5188
  html
4604
5189
  });
4605
5190
  }
4606
- function escapeHtml$2(value) {
5191
+ function escapeHtml$3(value) {
4607
5192
  return value.replace(/[&<>"']/g, (ch) => {
4608
5193
  switch (ch) {
4609
5194
  case "&": return "&amp;";
@@ -5001,7 +5586,7 @@ function createNativeTransformOptions(options) {
5001
5586
  } : void 0,
5002
5587
  cards: options.cards?.enabled ? { enabled: true } : void 0,
5003
5588
  steps: options.steps?.enabled ? { enabled: true } : void 0,
5004
- fileTree: options.fileTree?.enabled ? { enabled: true } : void 0,
5589
+ fileTree: toJsFileTreeOptions(options.fileTree),
5005
5590
  editThisPage: options.editThisPage?.enabled ? {
5006
5591
  enabled: true,
5007
5592
  repoUrl: options.editThisPage.repoUrl,
@@ -5279,8 +5864,8 @@ function weekdayUtc(year, month, day) {
5279
5864
  * String bodies follow `ox_content_ssg::generate_feeds`. The Vite plugin
5280
5865
  * writes those files during SSG without adding a NAPI surface.
5281
5866
  */
5282
- const MISSING_SITE_URL = "[ox-content] feeds is enabled but ssg.siteUrl is not set; RSS, Atom, and JSON feeds were not written";
5283
- const DEFAULT_FORMATS = [
5867
+ const MISSING_SITE_URL$1 = "[ox-content] feeds is enabled but ssg.siteUrl is not set; RSS, Atom, and JSON feeds were not written";
5868
+ const DEFAULT_FORMATS$1 = [
5284
5869
  "rss",
5285
5870
  "atom",
5286
5871
  "json"
@@ -5297,19 +5882,19 @@ const DEFAULT_PATH = "/";
5297
5882
  function resolveFeedsOptions(value) {
5298
5883
  if (!value) return {
5299
5884
  enabled: false,
5300
- formats: [...DEFAULT_FORMATS],
5885
+ formats: [...DEFAULT_FORMATS$1],
5301
5886
  limit: DEFAULT_LIMIT,
5302
5887
  path: DEFAULT_PATH
5303
5888
  };
5304
5889
  if (value === true) return {
5305
5890
  enabled: true,
5306
- formats: [...DEFAULT_FORMATS],
5891
+ formats: [...DEFAULT_FORMATS$1],
5307
5892
  limit: DEFAULT_LIMIT,
5308
5893
  path: DEFAULT_PATH
5309
5894
  };
5310
5895
  return {
5311
5896
  enabled: true,
5312
- formats: normalizeFormats(value.formats),
5897
+ formats: normalizeFormats$1(value.formats),
5313
5898
  collection: value.collection,
5314
5899
  limit: value.limit ?? DEFAULT_LIMIT,
5315
5900
  path: value.path ?? DEFAULT_PATH
@@ -5324,7 +5909,7 @@ function resolveFeedCollectionName(requested, collectionNames) {
5324
5909
  /** Builds RSS / Atom / JSON Feed bodies without writing files. */
5325
5910
  function generateFeeds(input) {
5326
5911
  if (!input.options?.enabled) return {};
5327
- if (!hasSiteUrl(input.siteUrl)) return { warning: MISSING_SITE_URL };
5912
+ if (!hasSiteUrl$1(input.siteUrl)) return { warning: MISSING_SITE_URL$1 };
5328
5913
  const published = publishedItems(input);
5329
5914
  const doc = feedDocument(input);
5330
5915
  const result = {};
@@ -5356,8 +5941,8 @@ async function writeFeedFiles(input) {
5356
5941
  }
5357
5942
  return { files };
5358
5943
  }
5359
- function normalizeFormats(formats) {
5360
- if (!formats) return [...DEFAULT_FORMATS];
5944
+ function normalizeFormats$1(formats) {
5945
+ if (!formats) return [...DEFAULT_FORMATS$1];
5361
5946
  const seen = /* @__PURE__ */ new Set();
5362
5947
  const resolved = [];
5363
5948
  for (const format of formats) if ((format === "rss" || format === "atom" || format === "json") && !seen.has(format)) {
@@ -5366,7 +5951,7 @@ function normalizeFormats(formats) {
5366
5951
  }
5367
5952
  return resolved;
5368
5953
  }
5369
- function hasSiteUrl(siteUrl) {
5954
+ function hasSiteUrl$1(siteUrl) {
5370
5955
  return Boolean(siteUrl && siteUrl.trim());
5371
5956
  }
5372
5957
  function homePageUrl(siteUrl, base = "/") {
@@ -5418,7 +6003,7 @@ function normalizeItem(item, input) {
5418
6003
  title: item.title ?? "",
5419
6004
  description: typeof item.description === "string" ? item.description : void 0,
5420
6005
  loc: item.loc || itemLoc(input, item),
5421
- date: parseDate(dateField(item.date ?? item.frontmatter?.date)) ?? parseDate(dateField(item.lastUpdated ?? item.frontmatter?.lastUpdated))
6006
+ date: parseDate(dateField$1(item.date ?? item.frontmatter?.date)) ?? parseDate(dateField$1(item.lastUpdated ?? item.frontmatter?.lastUpdated))
5422
6007
  };
5423
6008
  }
5424
6009
  function itemLoc(input, item) {
@@ -5426,49 +6011,254 @@ function itemLoc(input, item) {
5426
6011
  const urlPath = (item.path ?? "").replace(/^\/+|\/+$/g, "");
5427
6012
  return urlPath ? `${home}${urlPath}/` : home;
5428
6013
  }
5429
- function dateField(value) {
6014
+ function dateField$1(value) {
5430
6015
  if (typeof value === "string" && value.trim()) return value.trim();
5431
6016
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
5432
6017
  if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
5433
6018
  }
5434
6019
  //#endregion
5435
- //#region src/taxonomies-html.ts
6020
+ //#region src/pwa.ts
5436
6021
  /**
5437
- * Escaped taxonomy HTML and confined output paths.
6022
+ * Opt-in web app manifest and conservative service worker.
6023
+ *
6024
+ * The Vite plugin writes those files during SSG without adding a NAPI surface.
6025
+ * Enabling `offline` injects a tiny client script that registers `sw.js`.
5438
6026
  */
5439
- function relatedMarkup(pages) {
5440
- return `<nav class="ox-related" aria-label="Related pages"><h2>Related pages</h2><ul>${pages.map((page) => listItem(page.routePaths.href, page.title)).join("")}</ul></nav>`;
6027
+ const MISSING_SITE_URL = "[ox-content] pwa is enabled but ssg.siteUrl is not set; manifest.webmanifest and sw.js were not written";
6028
+ const DEFAULT_THEME_COLOR = "#000000";
6029
+ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
6030
+ const MANIFEST_NAME = "manifest.webmanifest";
6031
+ const SERVICE_WORKER_NAME = "sw.js";
6032
+ /**
6033
+ * Resolves `pwa` with defaults.
6034
+ *
6035
+ * `false` / omitted stays off. `true` enables the manifest and offline
6036
+ * service worker. An object enables the feature and overrides only the
6037
+ * fields the site set.
6038
+ */
6039
+ function resolvePwaOptions(value) {
6040
+ if (!value) return {
6041
+ enabled: false,
6042
+ offline: true
6043
+ };
6044
+ if (value === true) return {
6045
+ enabled: true,
6046
+ offline: true
6047
+ };
6048
+ return {
6049
+ enabled: true,
6050
+ offline: value.offline ?? true,
6051
+ name: value.name,
6052
+ shortName: value.shortName,
6053
+ themeColor: value.themeColor,
6054
+ backgroundColor: value.backgroundColor,
6055
+ startUrl: value.startUrl
6056
+ };
5441
6057
  }
5442
- function listPageContent(terms, base, urlName) {
5443
- const items = terms.map((term) => listItem(siteHref$2(base, urlName, term.slug), term.label)).join("");
5444
- return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
6058
+ /** Builds manifest / service-worker bodies without writing files. */
6059
+ function generatePwa(input) {
6060
+ if (!input.options?.enabled) return {};
6061
+ if (!hasSiteUrl(input.siteUrl)) return { warning: MISSING_SITE_URL };
6062
+ const base = normalizeBase(input.base);
6063
+ const name = sanitizeManifestText(input.options.name ?? input.siteName ?? "");
6064
+ const shortName = sanitizeManifestText(input.options.shortName ?? name);
6065
+ const startUrl = sanitizeStartUrl(input.options.startUrl, base);
6066
+ const themeColor = sanitizeColor(input.options.themeColor, DEFAULT_THEME_COLOR);
6067
+ const backgroundColor = sanitizeColor(input.options.backgroundColor, DEFAULT_BACKGROUND_COLOR);
6068
+ const result = { manifest: `${escapeJsonScript(JSON.stringify({
6069
+ name,
6070
+ short_name: shortName,
6071
+ start_url: startUrl,
6072
+ scope: base,
6073
+ display: "standalone",
6074
+ background_color: backgroundColor,
6075
+ theme_color: themeColor
6076
+ }, null, 2))}\n` };
6077
+ if (input.options.offline) result.serviceWorker = generateServiceWorker(base);
6078
+ return result;
5445
6079
  }
5446
- function termPageContent(term) {
6080
+ /** Writes enabled PWA files into `outDir`. */
6081
+ async function writePwaFiles(input) {
6082
+ const generated = generatePwa(input);
6083
+ if (generated.warning) return {
6084
+ files: [],
6085
+ warning: generated.warning
6086
+ };
6087
+ const outputs = [[generated.manifest, MANIFEST_NAME], [generated.serviceWorker, SERVICE_WORKER_NAME]].filter((entry) => entry[0] != null);
6088
+ if (outputs.length === 0) return { files: [] };
6089
+ await node_fs_promises.mkdir(input.outDir, { recursive: true });
6090
+ const files = [];
6091
+ for (const [body, name] of outputs) {
6092
+ const outputPath = node_path.join(input.outDir, name);
6093
+ await node_fs_promises.writeFile(outputPath, body, "utf8");
6094
+ files.push(outputPath);
6095
+ }
6096
+ return { files };
6097
+ }
6098
+ /**
6099
+ * Injects `rel=manifest` (and the service-worker register script when offline)
6100
+ * into a themed HTML document. Bare / fragment HTML is left unchanged.
6101
+ */
6102
+ function injectPwaPageTags(html, input) {
6103
+ if (!input.options?.enabled || !isThemedDocument(html)) return html;
6104
+ const base = normalizeBase(input.base);
6105
+ const manifestHref = escapeAttribute$1(`${base}${MANIFEST_NAME}`);
6106
+ const themeColor = sanitizeColor(input.options.themeColor, DEFAULT_THEME_COLOR);
6107
+ let next = insertBeforeTag(html, "</head>", ` ${[`<link rel="manifest" href="${manifestHref}">`, `<meta name="theme-color" content="${escapeAttribute$1(themeColor)}">`].join("\n ")}\n`);
6108
+ if (input.options.offline) {
6109
+ const script = `<script>if("serviceWorker"in navigator)navigator.serviceWorker.register(${JSON.stringify(`${base}${SERVICE_WORKER_NAME}`)})<\/script>`;
6110
+ next = insertBeforeTag(next, "</body>", ` ${script}\n`);
6111
+ }
6112
+ return next;
6113
+ }
6114
+ function generateServiceWorker(base) {
6115
+ return `/* ox-content PWA service worker */
6116
+ const CACHE = "ox-content-pwa-v1";
6117
+ const ASSET_PREFIX = ${JSON.stringify(`${base}assets/`)};
6118
+
6119
+ self.addEventListener("install", (event) => {
6120
+ event.waitUntil(self.skipWaiting());
6121
+ });
6122
+
6123
+ self.addEventListener("activate", (event) => {
6124
+ event.waitUntil(self.clients.claim());
6125
+ });
6126
+
6127
+ self.addEventListener("fetch", (event) => {
6128
+ const request = event.request;
6129
+ if (request.method !== "GET") return;
6130
+ const url = new URL(request.url);
6131
+ if (url.origin !== self.location.origin) return;
6132
+
6133
+ if (isHashedAsset(url.pathname)) {
6134
+ event.respondWith(cacheFirst(request));
6135
+ return;
6136
+ }
6137
+
6138
+ if (isHtmlPage(request)) {
6139
+ event.respondWith(networkFirst(request));
6140
+ }
6141
+ });
6142
+
6143
+ function isHashedAsset(pathname) {
6144
+ if (!pathname.startsWith(ASSET_PREFIX)) return false;
6145
+ return /-[0-9a-f]{8,}\\.[a-z0-9]+$/i.test(pathname);
6146
+ }
6147
+
6148
+ function isHtmlPage(request) {
6149
+ if (request.mode === "navigate") return true;
6150
+ const accept = request.headers.get("accept") || "";
6151
+ return accept.includes("text/html");
6152
+ }
6153
+
6154
+ async function cacheFirst(request) {
6155
+ const cache = await caches.open(CACHE);
6156
+ const cached = await cache.match(request);
6157
+ if (cached) return cached;
6158
+ const response = await fetch(request);
6159
+ if (response.ok) cache.put(request, response.clone());
6160
+ return response;
6161
+ }
6162
+
6163
+ async function networkFirst(request) {
6164
+ const cache = await caches.open(CACHE);
6165
+ try {
6166
+ const response = await fetch(request);
6167
+ if (response.ok) cache.put(request, response.clone());
6168
+ return response;
6169
+ } catch (error) {
6170
+ const cached = await cache.match(request);
6171
+ if (cached) return cached;
6172
+ throw error;
6173
+ }
6174
+ }
6175
+ `;
6176
+ }
6177
+ function hasSiteUrl(siteUrl) {
6178
+ return Boolean(siteUrl && siteUrl.trim());
6179
+ }
6180
+ function normalizeBase(base) {
6181
+ if (!base || base === "/") return "/";
6182
+ return base.endsWith("/") ? base : `${base}/`;
6183
+ }
6184
+ function sanitizeManifestText(value) {
6185
+ return value.split(/\s+/u).filter(Boolean).join(" ");
6186
+ }
6187
+ function sanitizeColor(value, fallback) {
6188
+ if (!value) return fallback;
6189
+ const trimmed = value.trim();
6190
+ if (/^#[0-9A-Fa-f]{3,8}$/.test(trimmed)) return trimmed;
6191
+ if (/^[a-zA-Z][a-zA-Z0-9-]{0,31}$/.test(trimmed)) return trimmed;
6192
+ return fallback;
6193
+ }
6194
+ function sanitizeStartUrl(value, base) {
6195
+ if (!value) return base;
6196
+ const trimmed = value.trim();
6197
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//")) return base;
6198
+ if (/[\n\r\t<>"'`]/.test(trimmed)) return base;
6199
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return base;
6200
+ return trimmed;
6201
+ }
6202
+ function isThemedDocument(html) {
6203
+ return /<\/head>/i.test(html) && /<\/body>/i.test(html);
6204
+ }
6205
+ function insertBeforeTag(html, tag, snippet) {
6206
+ const index = html.toLowerCase().lastIndexOf(tag.toLowerCase());
6207
+ if (index === -1) return html;
6208
+ return `${html.slice(0, index)}${snippet}${html.slice(index)}`;
6209
+ }
6210
+ function escapeJsonScript(value) {
6211
+ return value.replace(/[<>]/g, (ch) => ch === "<" ? "\\u003c" : "\\u003e");
6212
+ }
6213
+ function escapeAttribute$1(value) {
6214
+ return value.replace(/[&<>"']/g, (ch) => {
6215
+ switch (ch) {
6216
+ case "&": return "&amp;";
6217
+ case "<": return "&lt;";
6218
+ case ">": return "&gt;";
6219
+ case "\"": return "&quot;";
6220
+ default: return "&#39;";
6221
+ }
6222
+ });
6223
+ }
6224
+ //#endregion
6225
+ //#region src/taxonomies-html.ts
6226
+ /**
6227
+ * Escaped taxonomy HTML and confined output paths.
6228
+ */
6229
+ function relatedMarkup(pages) {
6230
+ return `<nav class="ox-related" aria-label="Related pages"><h2>Related pages</h2><ul>${pages.map((page) => listItem$1(page.routePaths.href, page.title)).join("")}</ul></nav>`;
6231
+ }
6232
+ function listPageContent(terms, base, urlName) {
6233
+ const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
6234
+ return `<h1>${escapeHtml$2(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
6235
+ }
6236
+ function termPageContent(term) {
5447
6237
  const items = [...term.pages].sort((left, right) => {
5448
6238
  const titleCmp = left.title.localeCompare(right.title);
5449
6239
  return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
5450
- }).map((page) => listItem(page.routePaths.href, page.title)).join("");
5451
- return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
6240
+ }).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
6241
+ return `<h1>${escapeHtml$2(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
5452
6242
  }
5453
6243
  function displayTaxonomyName(name) {
5454
6244
  return name.charAt(0).toUpperCase() + name.slice(1);
5455
6245
  }
5456
- function siteHref$2(base, ...segments) {
6246
+ function siteHref$3(base, ...segments) {
5457
6247
  const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
5458
6248
  const rest = segments.filter(Boolean).join("/");
5459
6249
  return rest ? `${prefix}${rest}/` : prefix;
5460
6250
  }
5461
- function containedPath(outDir, ...segments) {
6251
+ function containedPath$2(outDir, ...segments) {
5462
6252
  const root = node_path.resolve(outDir);
5463
6253
  const resolved = node_path.resolve(root, ...segments);
5464
6254
  const prefix = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
5465
6255
  if (resolved === root || !resolved.startsWith(prefix)) return;
5466
6256
  return resolved;
5467
6257
  }
5468
- function listItem(href, label) {
5469
- return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
6258
+ function listItem$1(href, label) {
6259
+ return `<li><a href="${escapeHtml$2(href)}">${escapeHtml$2(label)}</a></li>`;
5470
6260
  }
5471
- function escapeHtml$1(value) {
6261
+ function escapeHtml$2(value) {
5472
6262
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
5473
6263
  }
5474
6264
  //#endregion
@@ -5481,7 +6271,7 @@ function escapeHtml$1(value) {
5481
6271
  */
5482
6272
  const DEFAULT_TAXONOMIES = ["tags", "categories"];
5483
6273
  const DEFAULT_RELATED_LIMIT = 5;
5484
- const HOSTILE_TERM = /^(?:javascript|data):/i;
6274
+ const HOSTILE_TERM$1 = /^(?:javascript|data):/i;
5485
6275
  /**
5486
6276
  * Resolves `taxonomies` with defaults.
5487
6277
  *
@@ -5512,7 +6302,7 @@ function resolveTaxonomiesOptions(value) {
5512
6302
  */
5513
6303
  function termSlug(term) {
5514
6304
  const trimmed = term.trim();
5515
- if (!trimmed || HOSTILE_TERM.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
6305
+ if (!trimmed || HOSTILE_TERM$1.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
5516
6306
  return trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || void 0;
5517
6307
  }
5518
6308
  /** Appends related-page HTML to source pages that share a listed term. */
@@ -5570,8 +6360,8 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5570
6360
  for (const taxonomy of options.taxonomies) {
5571
6361
  const urlName = taxonomy.toLowerCase();
5572
6362
  const terms = collectTerms(listed, taxonomy);
5573
- const listHref = siteHref$2(base, urlName);
5574
- const listOutput = containedPath(outDir, urlName, "index.html");
6363
+ const listHref = siteHref$3(base, urlName);
6364
+ const listOutput = containedPath$2(outDir, urlName, "index.html");
5575
6365
  if (listOutput) pages.push({
5576
6366
  title: displayTaxonomyName(urlName),
5577
6367
  content: listPageContent(terms, base, urlName),
@@ -5580,14 +6370,14 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5580
6370
  href: listHref
5581
6371
  });
5582
6372
  for (const term of terms) {
5583
- const outputPath = containedPath(outDir, urlName, term.slug, "index.html");
6373
+ const outputPath = containedPath$2(outDir, urlName, term.slug, "index.html");
5584
6374
  if (!outputPath) continue;
5585
6375
  pages.push({
5586
6376
  title: term.label,
5587
6377
  content: termPageContent(term),
5588
6378
  outputPath,
5589
6379
  urlPath: `${urlName}/${term.slug}`,
5590
- href: siteHref$2(base, urlName, term.slug)
6380
+ href: siteHref$3(base, urlName, term.slug)
5591
6381
  });
5592
6382
  }
5593
6383
  }
@@ -5595,7 +6385,7 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5595
6385
  }
5596
6386
  function collectTerms(listed, taxonomy) {
5597
6387
  const buckets = /* @__PURE__ */ new Map();
5598
- for (const page of listed) for (const label of termsFromValue(page.frontmatter[taxonomy])) {
6388
+ for (const page of listed) for (const label of termsFromValue$1(page.frontmatter[taxonomy])) {
5599
6389
  const slug = termSlug(label);
5600
6390
  if (!slug) continue;
5601
6391
  const existing = buckets.get(slug);
@@ -5610,13 +6400,13 @@ function collectTerms(listed, taxonomy) {
5610
6400
  }
5611
6401
  function pageTermKeys(page, taxonomies) {
5612
6402
  const keys = /* @__PURE__ */ new Set();
5613
- for (const taxonomy of taxonomies) for (const label of termsFromValue(page.frontmatter[taxonomy])) {
6403
+ for (const taxonomy of taxonomies) for (const label of termsFromValue$1(page.frontmatter[taxonomy])) {
5614
6404
  const slug = termSlug(label);
5615
6405
  if (slug) keys.add(`${taxonomy.toLowerCase()}\0${slug}`);
5616
6406
  }
5617
6407
  return keys;
5618
6408
  }
5619
- function termsFromValue(value) {
6409
+ function termsFromValue$1(value) {
5620
6410
  if (typeof value === "string") return value.trim() ? [value.trim()] : [];
5621
6411
  if (!Array.isArray(value)) return [];
5622
6412
  return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
@@ -5671,25 +6461,810 @@ function resolveTeamOptions(value) {
5671
6461
  members: normalizeMembers(value.members)
5672
6462
  };
5673
6463
  }
5674
- function normalizeMembers(members) {
5675
- if (!Array.isArray(members)) return [];
5676
- return members.flatMap((member) => {
5677
- if (!member || typeof member.name !== "string") return [];
5678
- const links = Array.isArray(member.links) ? member.links.flatMap((link) => {
5679
- if (!link || typeof link.label !== "string" || typeof link.href !== "string") return [];
5680
- return [{
5681
- label: link.label,
5682
- href: link.href
5683
- }];
5684
- }) : void 0;
5685
- return [{
5686
- name: member.name,
5687
- role: typeof member.role === "string" ? member.role : void 0,
5688
- avatar: typeof member.avatar === "string" ? member.avatar : void 0,
5689
- links
5690
- }];
6464
+ function normalizeMembers(members) {
6465
+ if (!Array.isArray(members)) return [];
6466
+ return members.flatMap((member) => {
6467
+ if (!member || typeof member.name !== "string") return [];
6468
+ const links = Array.isArray(member.links) ? member.links.flatMap((link) => {
6469
+ if (!link || typeof link.label !== "string" || typeof link.href !== "string") return [];
6470
+ return [{
6471
+ label: link.label,
6472
+ href: link.href
6473
+ }];
6474
+ }) : void 0;
6475
+ return [{
6476
+ name: member.name,
6477
+ role: typeof member.role === "string" ? member.role : void 0,
6478
+ avatar: typeof member.avatar === "string" ? member.avatar : void 0,
6479
+ links
6480
+ }];
6481
+ });
6482
+ }
6483
+ //#endregion
6484
+ //#region src/contributors.ts
6485
+ /**
6486
+ * Opt-in git contributor list helpers.
6487
+ *
6488
+ * Resolution and ignore filtering live here. `git log` is read in Rust
6489
+ * (`getGitContributors`) and names are rendered from `PageData.contributors`.
6490
+ */
6491
+ /**
6492
+ * Resolves `ssg.contributors` with defaults.
6493
+ *
6494
+ * `false` / omitted stays off. `true` enables names only.
6495
+ * An object enables the feature and keeps `ignore` / `avatars`.
6496
+ */
6497
+ function resolveContributorsOption(value) {
6498
+ if (!value) return false;
6499
+ if (value === true) return {
6500
+ ignore: [],
6501
+ avatars: false
6502
+ };
6503
+ return {
6504
+ ignore: Array.isArray(value.ignore) ? value.ignore.filter((entry) => typeof entry === "string") : [],
6505
+ avatars: value.avatars === true
6506
+ };
6507
+ }
6508
+ function filterGitContributors(contributors, ignore) {
6509
+ if (ignore.length === 0) return contributors.filter((contributor) => contributor.name.trim());
6510
+ const needles = new Set(ignore.map((entry) => entry.toLowerCase()));
6511
+ return contributors.filter((contributor) => {
6512
+ const name = contributor.name.trim();
6513
+ if (!name) return false;
6514
+ if (needles.has(name.toLowerCase())) return false;
6515
+ const email = contributor.email?.trim().toLowerCase();
6516
+ return !email || !needles.has(email);
6517
+ });
6518
+ }
6519
+ function gravatarAvatar(email) {
6520
+ return `https://www.gravatar.com/avatar/${(0, node_crypto.createHash)("md5").update(email.trim().toLowerCase()).digest("hex")}?d=mp&s=40`;
6521
+ }
6522
+ function applyContributorOptions(raw, option) {
6523
+ return filterGitContributors(raw, option.ignore).map((contributor) => ({
6524
+ name: contributor.name.trim(),
6525
+ avatar: option.avatars && contributor.email?.trim() ? gravatarAvatar(contributor.email) : void 0
6526
+ }));
6527
+ }
6528
+ //#endregion
6529
+ //#region src/blog-options.ts
6530
+ const DEFAULT_PAGE_SIZE = 10;
6531
+ function resolveBlogOptions(value) {
6532
+ if (!value) return {
6533
+ enabled: false,
6534
+ authors: {},
6535
+ pageSize: DEFAULT_PAGE_SIZE
6536
+ };
6537
+ if (value === true) return {
6538
+ enabled: true,
6539
+ authors: {},
6540
+ pageSize: DEFAULT_PAGE_SIZE
6541
+ };
6542
+ return {
6543
+ enabled: true,
6544
+ collection: value.collection,
6545
+ authors: normalizeAuthors(value.authors),
6546
+ pageSize: normalizePageSize(value.pageSize)
6547
+ };
6548
+ }
6549
+ /**
6550
+ * Picks a collection named `blog`, else the only configured collection.
6551
+ *
6552
+ * An explicit name always wins. Several collections and no `blog` name
6553
+ * require `blog.collection`.
6554
+ */
6555
+ function resolveBlogCollectionName(requested, collectionNames) {
6556
+ if (requested) return requested;
6557
+ if (collectionNames.includes("blog")) return "blog";
6558
+ if (collectionNames.length === 1) return collectionNames[0];
6559
+ }
6560
+ function normalizeAuthors(authors) {
6561
+ if (!authors || typeof authors !== "object") return {};
6562
+ const resolved = {};
6563
+ for (const [key, value] of Object.entries(authors)) {
6564
+ if (!value || typeof value.name !== "string") continue;
6565
+ resolved[key] = {
6566
+ name: value.name,
6567
+ bio: typeof value.bio === "string" ? value.bio : void 0,
6568
+ url: typeof value.url === "string" ? value.url : void 0
6569
+ };
6570
+ }
6571
+ return resolved;
6572
+ }
6573
+ function normalizePageSize(value) {
6574
+ if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
6575
+ return DEFAULT_PAGE_SIZE;
6576
+ }
6577
+ //#endregion
6578
+ //#region src/blog-reading.ts
6579
+ /**
6580
+ * Deterministic blog reading-time estimates.
6581
+ */
6582
+ const LATIN_WORDS_PER_MINUTE = 200;
6583
+ const CJK_CHARS_PER_MINUTE = 500;
6584
+ function readingTimeMinutes(markdown) {
6585
+ const body = stripInlineCode(stripFences(stripFrontmatter(markdown)));
6586
+ let latin = 0;
6587
+ let cjk = 0;
6588
+ let latinRun = false;
6589
+ for (const char of body) {
6590
+ const code = char.codePointAt(0) ?? 0;
6591
+ if (isCjkCodePoint(code)) {
6592
+ cjk += 1;
6593
+ latinRun = false;
6594
+ continue;
6595
+ }
6596
+ if (isLatinWordChar(code)) {
6597
+ if (!latinRun) {
6598
+ latin += 1;
6599
+ latinRun = true;
6600
+ }
6601
+ continue;
6602
+ }
6603
+ if (char === "'" || char === "’") continue;
6604
+ latinRun = false;
6605
+ }
6606
+ if (latin === 0 && cjk === 0) return 0;
6607
+ return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
6608
+ }
6609
+ function stripFrontmatter(markdown) {
6610
+ if (!markdown.startsWith("---")) return markdown;
6611
+ const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
6612
+ return match ? markdown.slice(match[0].length) : markdown;
6613
+ }
6614
+ function stripFences(text) {
6615
+ return text.replace(/```[\s\S]*?(?:```|$)/g, " ");
6616
+ }
6617
+ function stripInlineCode(text) {
6618
+ return text.replace(/`[^`\n]*`/g, " ");
6619
+ }
6620
+ function isCjkCodePoint(code) {
6621
+ return code >= 12352 && code <= 12543 || code >= 12784 && code <= 12799 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 44032 && code <= 55215 || code >= 4352 && code <= 4607;
6622
+ }
6623
+ function isLatinWordChar(code) {
6624
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
6625
+ }
6626
+ //#endregion
6627
+ //#region src/blog-html.ts
6628
+ /**
6629
+ * Escaped blog index, tag, archive, and post-meta HTML.
6630
+ */
6631
+ /** `https:` or a same-origin path starting with `/` but not `//`. */
6632
+ function isSafeBlogUrl(value) {
6633
+ const trimmed = value.trim();
6634
+ if (trimmed.length === 0 || trimmed.split("").some((ch) => ch === "\n" || ch === "\r" || ch === "\0" || ch === " ")) return false;
6635
+ if (trimmed.startsWith("//")) return false;
6636
+ if (trimmed.startsWith("/")) return true;
6637
+ return trimmed.toLowerCase().startsWith("https:");
6638
+ }
6639
+ function postMetaMarkup(meta) {
6640
+ const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml$1(String(meta.minutes))} min read</p>`];
6641
+ if (meta.authors.length > 0) {
6642
+ const items = meta.authors.map((author) => authorMarkup(author)).join("");
6643
+ parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
6644
+ }
6645
+ if (meta.tags.length > 0) {
6646
+ const items = meta.tags.map((tag) => `<li><a href="${escapeHtml$1(tag.href)}">${escapeHtml$1(tag.label)}</a></li>`).join("");
6647
+ parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
6648
+ }
6649
+ return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
6650
+ }
6651
+ function indexPageContent(items, pager) {
6652
+ const list = items.map((item) => listItem(item)).join("");
6653
+ const links = [];
6654
+ if (pager.newerHref) links.push(`<a href="${escapeHtml$1(pager.newerHref)}" rel="prev">Newer</a>`);
6655
+ if (pager.olderHref) links.push(`<a href="${escapeHtml$1(pager.olderHref)}" rel="next">Older</a>`);
6656
+ return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
6657
+ }
6658
+ function tagPageContent(label, items) {
6659
+ const list = items.map((item) => listItem(item)).join("");
6660
+ return `<h1>${escapeHtml$1(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
6661
+ }
6662
+ function archiveIndexContent(years) {
6663
+ return `<h1>Archive</h1><ul class="ox-blog-archive">${years.map((entry) => `<li><a href="${escapeHtml$1(entry.href)}">${escapeHtml$1(entry.year)}</a></li>`).join("")}</ul>`;
6664
+ }
6665
+ function archiveYearContent(year, months, items) {
6666
+ const monthList = months.map((entry) => `<li><a href="${escapeHtml$1(entry.href)}">${escapeHtml$1(entry.month)}</a></li>`).join("");
6667
+ const posts = items.map((item) => listItem(item)).join("");
6668
+ return `<h1>${escapeHtml$1(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
6669
+ }
6670
+ function archiveMonthContent(label, items) {
6671
+ const list = items.map((item) => listItem(item)).join("");
6672
+ return `<h1>${escapeHtml$1(label)}</h1><ul class="ox-blog">${list}</ul>`;
6673
+ }
6674
+ function siteHref$2(base, ...segments) {
6675
+ const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
6676
+ const rest = segments.filter(Boolean).join("/");
6677
+ return rest ? `${prefix}${rest}/` : prefix;
6678
+ }
6679
+ function containedPath$1(outDir, ...segments) {
6680
+ const root = node_path.resolve(outDir);
6681
+ const resolved = node_path.resolve(root, ...segments);
6682
+ const prefix = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
6683
+ if (resolved === root || !resolved.startsWith(prefix)) return;
6684
+ return resolved;
6685
+ }
6686
+ function escapeHtml$1(value) {
6687
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
6688
+ }
6689
+ function authorMarkup(author) {
6690
+ const name = escapeHtml$1(author.name);
6691
+ const url = author.url?.trim();
6692
+ return `<li>${url && isSafeBlogUrl(url) ? `<a class="ox-blog-meta__name" href="${escapeHtml$1(url)}">${name}</a>` : `<span class="ox-blog-meta__name">${name}</span>`}${author.bio && author.bio.length > 0 ? `<p class="ox-blog-meta__bio">${escapeHtml$1(author.bio)}</p>` : ""}</li>`;
6693
+ }
6694
+ function listItem(item) {
6695
+ const time = item.dateLabel ? ` <time datetime="${escapeHtml$1(item.dateLabel)}">${escapeHtml$1(item.dateLabel)}</time>` : "";
6696
+ return `<li><a href="${escapeHtml$1(item.href)}">${escapeHtml$1(item.title)}</a>${time}</li>`;
6697
+ }
6698
+ //#endregion
6699
+ //#region src/blog-posts.ts
6700
+ /**
6701
+ * Blog post selection, tags, authors, and dates.
6702
+ */
6703
+ const HOSTILE_TERM = /^(?:javascript|data):/i;
6704
+ function selectBlogPosts(listed, options, srcDir, collections) {
6705
+ if (isAmbiguousCollection(options, collections)) return;
6706
+ const names = collectionNames(collections);
6707
+ const name = resolveBlogCollectionName(options.collection, names);
6708
+ const sources = name && collections?.enabled ? collections.collections[name]?.source : void 0;
6709
+ return listed.filter((page) => {
6710
+ if (isExcludedPost(page.frontmatter)) return false;
6711
+ if (!sources) return true;
6712
+ return pageMatchesSources(page.inputPath, srcDir, sources);
6713
+ });
6714
+ }
6715
+ function isAmbiguousCollection(options, collections) {
6716
+ if (options.collection) return false;
6717
+ const names = collectionNames(collections);
6718
+ return names.length > 1 && !names.includes("blog");
6719
+ }
6720
+ function collectionNames(collections) {
6721
+ if (!collections?.enabled) return [];
6722
+ return Object.keys(collections.collections);
6723
+ }
6724
+ function pageMatchesSources(inputPath, srcDir, sources) {
6725
+ const relative = node_path.relative(srcDir, inputPath).split(node_path.sep).join("/");
6726
+ return sources.some((source) => matchGlob(relative, source));
6727
+ }
6728
+ function matchGlob(relative, pattern) {
6729
+ const normalized = pattern.replace(/^\/+/, "");
6730
+ let out = "^";
6731
+ for (let i = 0; i < normalized.length; i += 1) {
6732
+ if (normalized.startsWith("**/", i)) {
6733
+ out += "(?:.*/)?";
6734
+ i += 2;
6735
+ continue;
6736
+ }
6737
+ const ch = normalized[i] ?? "";
6738
+ if (ch === "*") {
6739
+ out += "[^/]*";
6740
+ continue;
6741
+ }
6742
+ if (ch === "?") {
6743
+ out += "[^/]";
6744
+ continue;
6745
+ }
6746
+ if (/[.+^${}()|[\]\\]/.test(ch)) {
6747
+ out += `\\${ch}`;
6748
+ continue;
6749
+ }
6750
+ out += ch;
6751
+ }
6752
+ out += "$";
6753
+ return new RegExp(out).test(relative);
6754
+ }
6755
+ function sortPosts(posts) {
6756
+ return [...posts].sort((left, right) => {
6757
+ const dateCmp = (pageUnix(right.frontmatter) ?? Number.NEGATIVE_INFINITY) - (pageUnix(left.frontmatter) ?? Number.NEGATIVE_INFINITY);
6758
+ if (dateCmp !== 0) return dateCmp;
6759
+ return left.routePaths.href < right.routePaths.href ? -1 : left.routePaths.href > right.routePaths.href ? 1 : 0;
6760
+ });
6761
+ }
6762
+ function collectTags(posts) {
6763
+ const buckets = /* @__PURE__ */ new Map();
6764
+ for (const page of posts) for (const label of termsFromValue(page.frontmatter.tags)) {
6765
+ const slug = tagSlug(label);
6766
+ if (!slug) continue;
6767
+ const existing = buckets.get(slug);
6768
+ if (existing) existing.pages.push(page);
6769
+ else buckets.set(slug, {
6770
+ label,
6771
+ slug,
6772
+ pages: [page]
6773
+ });
6774
+ }
6775
+ return [...buckets.values()].sort((left, right) => left.label.localeCompare(right.label));
6776
+ }
6777
+ function datedPosts(posts) {
6778
+ const dated = [];
6779
+ for (const page of posts) {
6780
+ const parsed = pageDate(page.frontmatter);
6781
+ if (!parsed) continue;
6782
+ dated.push({
6783
+ page,
6784
+ year: String(parsed.year).padStart(4, "0"),
6785
+ month: String(parsed.month).padStart(2, "0"),
6786
+ label: `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}`
6787
+ });
6788
+ }
6789
+ return dated;
6790
+ }
6791
+ function uniqueYears(dated) {
6792
+ return [...new Set(dated.map((entry) => entry.year))].sort((left, right) => right.localeCompare(left));
6793
+ }
6794
+ function uniqueMonths(dated) {
6795
+ return [...new Set(dated.map((entry) => entry.month))].sort((left, right) => left.localeCompare(right));
6796
+ }
6797
+ function toListItem(page) {
6798
+ const parsed = pageDate(page.frontmatter);
6799
+ return {
6800
+ title: page.title,
6801
+ href: page.routePaths.href,
6802
+ dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0
6803
+ };
6804
+ }
6805
+ function resolvePostAuthors(frontmatter, map) {
6806
+ const seen = /* @__PURE__ */ new Set();
6807
+ const authors = [];
6808
+ for (const key of authorKeys(frontmatter)) {
6809
+ if (seen.has(key)) continue;
6810
+ seen.add(key);
6811
+ authors.push(map[key] ?? { name: key });
6812
+ }
6813
+ return authors;
6814
+ }
6815
+ function authorKeys(frontmatter) {
6816
+ return [...keysFromValue(frontmatter.author), ...keysFromValue(frontmatter.authors)];
6817
+ }
6818
+ function keysFromValue(value) {
6819
+ if (typeof value === "string") return value.trim() ? [value.trim()] : [];
6820
+ if (!Array.isArray(value)) return [];
6821
+ return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
6822
+ }
6823
+ function postTagLinks(frontmatter, base) {
6824
+ const links = [];
6825
+ const seen = /* @__PURE__ */ new Set();
6826
+ for (const label of termsFromValue(frontmatter.tags)) {
6827
+ const slug = tagSlug(label);
6828
+ if (!slug || seen.has(slug)) continue;
6829
+ seen.add(slug);
6830
+ links.push({
6831
+ label,
6832
+ href: siteHref$2(base, "blog", "tags", slug)
6833
+ });
6834
+ }
6835
+ return links;
6836
+ }
6837
+ function tagSlug(term) {
6838
+ const trimmed = term.trim();
6839
+ if (!trimmed || HOSTILE_TERM.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
6840
+ return trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || void 0;
6841
+ }
6842
+ function termsFromValue(value) {
6843
+ if (typeof value === "string") return value.trim() ? [value.trim()] : [];
6844
+ if (!Array.isArray(value)) return [];
6845
+ return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
6846
+ }
6847
+ function isExcludedPost(frontmatter) {
6848
+ return frontmatter.draft === true || frontmatter.unlisted === true;
6849
+ }
6850
+ function pageDate(frontmatter) {
6851
+ return parseDate(dateField(frontmatter.date));
6852
+ }
6853
+ function pageUnix(frontmatter) {
6854
+ return pageDate(frontmatter)?.unix;
6855
+ }
6856
+ function dateField(value) {
6857
+ if (typeof value === "string" && value.trim()) return value.trim();
6858
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
6859
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
6860
+ }
6861
+ //#endregion
6862
+ //#region src/blog-pages.ts
6863
+ /**
6864
+ * Generated blog index, tag, and archive pages.
6865
+ */
6866
+ const AMBIGUOUS_COLLECTION = "[ox-content] blog is enabled but multiple collections are configured; set blog.collection";
6867
+ async function injectBlogPostMeta(input) {
6868
+ if (!input.options?.enabled) return;
6869
+ const posts = selectBlogPosts(input.listed, input.options, input.srcDir, input.collections);
6870
+ if (posts === void 0) return;
6871
+ const listedPaths = new Set(posts.map((page) => page.inputPath));
6872
+ for (const page of input.pages) {
6873
+ if (!listedPaths.has(page.inputPath)) continue;
6874
+ const markdown = await readMarkdown(page.inputPath);
6875
+ page.transformedHtml = postMetaMarkup({
6876
+ authors: resolvePostAuthors(page.frontmatter, input.options.authors),
6877
+ minutes: readingTimeMinutes(markdown),
6878
+ tags: postTagLinks(page.frontmatter, input.base)
6879
+ }) + page.transformedHtml;
6880
+ }
6881
+ }
6882
+ /** Maps a generated blog page onto the SSG render shape. */
6883
+ function toBlogProcessResult(page) {
6884
+ return {
6885
+ inputPath: page.outputPath,
6886
+ routePaths: {
6887
+ outputPath: page.outputPath,
6888
+ urlPath: page.urlPath,
6889
+ href: page.href,
6890
+ ogImagePath: "",
6891
+ ogImageUrl: ""
6892
+ },
6893
+ transformedHtml: page.content,
6894
+ title: page.title,
6895
+ frontmatter: {},
6896
+ toc: []
6897
+ };
6898
+ }
6899
+ /** Renders index, tag, and archive pages and appends them to the build. */
6900
+ async function appendBlogPages(input) {
6901
+ if (!input.options?.enabled) return;
6902
+ if (isAmbiguousCollection(input.options, input.collections)) {
6903
+ input.errors.push(AMBIGUOUS_COLLECTION);
6904
+ return;
6905
+ }
6906
+ const posts = selectBlogPosts(input.listedPages, input.options, input.srcDir, input.collections);
6907
+ if (posts === void 0) return;
6908
+ for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
6909
+ input.generatedPages.push({
6910
+ inputPath: spec.outputPath,
6911
+ outputPath: spec.outputPath,
6912
+ html: await input.render(spec)
6913
+ });
6914
+ } catch (err) {
6915
+ const message = err instanceof Error ? err.message : String(err);
6916
+ input.errors.push(`Failed to generate blog page ${spec.href}: ${message}`);
6917
+ }
6918
+ }
6919
+ function blogPageSpecs(posts, options, outDir, base) {
6920
+ const sorted = sortPosts(posts);
6921
+ const pages = [];
6922
+ const pageSize = options.pageSize;
6923
+ const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize) || 1);
6924
+ const totalPages = sorted.length === 0 ? 1 : pageCount;
6925
+ for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
6926
+ const slice = sorted.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
6927
+ const isFirst = pageNumber === 1;
6928
+ const urlPath = isFirst ? "blog" : `blog/page/${pageNumber}`;
6929
+ const outputPath = isFirst ? containedPath$1(outDir, "blog", "index.html") : containedPath$1(outDir, "blog", "page", String(pageNumber), "index.html");
6930
+ if (!outputPath) continue;
6931
+ pages.push({
6932
+ title: isFirst ? "Blog" : `Blog · page ${pageNumber}`,
6933
+ content: indexPageContent(slice.map(toListItem), {
6934
+ newerHref: isFirst ? void 0 : siteHref$2(base, ...pageNumber === 2 ? ["blog"] : [
6935
+ "blog",
6936
+ "page",
6937
+ String(pageNumber - 1)
6938
+ ]),
6939
+ olderHref: pageNumber < totalPages ? siteHref$2(base, "blog", "page", String(pageNumber + 1)) : void 0
6940
+ }),
6941
+ outputPath,
6942
+ urlPath,
6943
+ href: siteHref$2(base, ...urlPath.split("/"))
6944
+ });
6945
+ }
6946
+ const tags = collectTags(sorted);
6947
+ for (const tag of tags) {
6948
+ const outputPath = containedPath$1(outDir, "blog", "tags", tag.slug, "index.html");
6949
+ if (!outputPath) continue;
6950
+ pages.push({
6951
+ title: tag.label,
6952
+ content: tagPageContent(tag.label, tag.pages.map(toListItem)),
6953
+ outputPath,
6954
+ urlPath: `blog/tags/${tag.slug}`,
6955
+ href: siteHref$2(base, "blog", "tags", tag.slug)
6956
+ });
6957
+ }
6958
+ const dated = datedPosts(sorted);
6959
+ if (dated.length > 0) {
6960
+ const years = uniqueYears(dated);
6961
+ const archiveIndex = containedPath$1(outDir, "blog", "archive", "index.html");
6962
+ if (archiveIndex) pages.push({
6963
+ title: "Archive",
6964
+ content: archiveIndexContent(years.map((year) => ({
6965
+ year,
6966
+ href: siteHref$2(base, "blog", "archive", year)
6967
+ }))),
6968
+ outputPath: archiveIndex,
6969
+ urlPath: "blog/archive",
6970
+ href: siteHref$2(base, "blog", "archive")
6971
+ });
6972
+ for (const year of years) {
6973
+ const yearPosts = dated.filter((entry) => entry.year === year);
6974
+ const months = uniqueMonths(yearPosts);
6975
+ const yearPath = containedPath$1(outDir, "blog", "archive", year, "index.html");
6976
+ if (yearPath) pages.push({
6977
+ title: year,
6978
+ content: archiveYearContent(year, months.map((month) => ({
6979
+ month: `${year}-${month}`,
6980
+ href: siteHref$2(base, "blog", "archive", year, month)
6981
+ })), yearPosts.map((entry) => toListItem(entry.page))),
6982
+ outputPath: yearPath,
6983
+ urlPath: `blog/archive/${year}`,
6984
+ href: siteHref$2(base, "blog", "archive", year)
6985
+ });
6986
+ for (const month of months) {
6987
+ const monthPosts = yearPosts.filter((entry) => entry.month === month);
6988
+ const monthPath = containedPath$1(outDir, "blog", "archive", year, month, "index.html");
6989
+ if (!monthPath) continue;
6990
+ pages.push({
6991
+ title: `${year}-${month}`,
6992
+ content: archiveMonthContent(`${year}-${month}`, monthPosts.map((entry) => toListItem(entry.page))),
6993
+ outputPath: monthPath,
6994
+ urlPath: `blog/archive/${year}/${month}`,
6995
+ href: siteHref$2(base, "blog", "archive", year, month)
6996
+ });
6997
+ }
6998
+ }
6999
+ }
7000
+ return pages;
7001
+ }
7002
+ async function readMarkdown(inputPath) {
7003
+ try {
7004
+ return await node_fs_promises.readFile(inputPath, "utf8");
7005
+ } catch {
7006
+ return "";
7007
+ }
7008
+ }
7009
+ //#endregion
7010
+ //#region src/section-index-html.ts
7011
+ /**
7012
+ * Section-index listing HTML and href safety.
7013
+ *
7014
+ * Titles are escaped. `javascript:` / `data:` / `vbscript:` / `file:` hrefs
7015
+ * are dropped. The NAPI helper is preferred when present.
7016
+ */
7017
+ const HOSTILE_SCHEME = /^(?:javascript|data|vbscript|file):/i;
7018
+ /** `https:`-free, same-origin or relative href. `javascript:` is rejected. */
7019
+ function isSafeSectionHref(value) {
7020
+ const trimmed = value.trim();
7021
+ if (!trimmed || /[\n\r\0\t]/.test(trimmed) || trimmed.startsWith("//")) return false;
7022
+ if (trimmed.startsWith("/")) return true;
7023
+ if (trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)) return false;
7024
+ return !HOSTILE_SCHEME.test(trimmed);
7025
+ }
7026
+ /** Escapes text and attribute values in generated listing markup. */
7027
+ function escapeSectionIndexHtml(value) {
7028
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
7029
+ }
7030
+ /** Renders the listing body. Titles are escaped; hostile hrefs are dropped. */
7031
+ function renderSectionIndexHtml(title, items, style) {
7032
+ try {
7033
+ const napi = require_vitepress.importNapiModuleSync();
7034
+ if (typeof napi.renderSsgSectionIndex === "function") return napi.renderSsgSectionIndex(title, items.map((item) => ({
7035
+ title: item.title,
7036
+ href: item.href,
7037
+ description: item.description
7038
+ })), style);
7039
+ } catch {}
7040
+ return renderSectionIndexHtmlLocal(title, items, style);
7041
+ }
7042
+ function renderSectionIndexHtmlLocal(title, items, style) {
7043
+ const safe = items.filter((item) => isSafeSectionHref(item.href));
7044
+ const modifier = style === "list" ? "list" : "cards";
7045
+ const listClass = style === "list" ? "ox-section-index__list" : "ox-section-index__cards";
7046
+ const body = safe.map((item) => renderItem(item, style)).join("");
7047
+ return `<nav class="ox-section-index ox-section-index--${modifier}" aria-label="Section pages"><h1>${escapeSectionIndexHtml(title)}</h1><ul class="${listClass}">${body}</ul></nav>`;
7048
+ }
7049
+ function renderItem(item, style) {
7050
+ const href = escapeSectionIndexHtml(item.href.trim());
7051
+ const label = escapeSectionIndexHtml(item.title);
7052
+ if (style === "list") return `<li><a href="${href}">${label}</a></li>`;
7053
+ return `<li class="ox-section-index__card"><a href="${href}"><span class="ox-section-index__title">${label}</span>${typeof item.description === "string" && item.description.trim() ? `<span class="ox-section-index__desc">${escapeSectionIndexHtml(item.description)}</span>` : ""}</a></li>`;
7054
+ }
7055
+ //#endregion
7056
+ //#region src/section-index-paths.ts
7057
+ /**
7058
+ * Section-index URL, title, and output-path helpers.
7059
+ */
7060
+ function pageTitle(page) {
7061
+ if (page.title.trim()) return page.title;
7062
+ return formatSectionTitle(node_path.basename(page.inputPath ?? page.routePaths.urlPath).replace(/\.[^.]+$/, "") || page.routePaths.urlPath);
7063
+ }
7064
+ function sectionTitle(dir) {
7065
+ if (!dir) return "Home";
7066
+ return formatSectionTitle(dir.slice(dir.lastIndexOf("/") + 1));
7067
+ }
7068
+ function formatSectionTitle(name) {
7069
+ try {
7070
+ return require_vitepress.importNapiModuleSync().formatSsgTitle(name);
7071
+ } catch {
7072
+ if (!name) return "Untitled";
7073
+ return name.charAt(0).toUpperCase() + name.slice(1).replace(/[-_]+/g, " ");
7074
+ }
7075
+ }
7076
+ function normalizeUrlPath(urlPath) {
7077
+ if (!urlPath || urlPath === "/") return "";
7078
+ return urlPath.replace(/^\/+|\/+$/g, "");
7079
+ }
7080
+ function parentDir(urlPath) {
7081
+ const normalized = normalizeUrlPath(urlPath);
7082
+ if (!normalized) return null;
7083
+ const index = normalized.lastIndexOf("/");
7084
+ return index === -1 ? "" : normalized.slice(0, index);
7085
+ }
7086
+ function firstChildDir(urlPath, parent) {
7087
+ const normalized = normalizeUrlPath(urlPath);
7088
+ if (!normalized) return;
7089
+ if (!parent) {
7090
+ const slash = normalized.indexOf("/");
7091
+ return slash === -1 ? void 0 : normalized.slice(0, slash);
7092
+ }
7093
+ const prefix = `${parent}/`;
7094
+ if (!normalized.startsWith(prefix) || normalized === parent) return;
7095
+ const rest = normalized.slice(prefix.length);
7096
+ const slash = rest.indexOf("/");
7097
+ return slash === -1 ? void 0 : `${parent}/${rest.slice(0, slash)}`;
7098
+ }
7099
+ function sectionHref(base, dir, extension) {
7100
+ const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
7101
+ const ext = extension.startsWith(".") ? extension : `.${extension}`;
7102
+ return dir ? `${prefix}${dir}/index${ext}` : `${prefix}index${ext}`;
7103
+ }
7104
+ function sectionOutputPath(outDir, dir, extension) {
7105
+ const ext = extension.startsWith(".") ? extension : `.${extension}`;
7106
+ return containedPath(outDir, ...dir ? [...dir.split("/").filter(Boolean), `index${ext}`] : [`index${ext}`]);
7107
+ }
7108
+ function dirFromOutputPath(outputPath, outDir) {
7109
+ return node_path.relative(node_path.resolve(outDir), node_path.resolve(outputPath)).replaceAll(node_path.sep, "/").replace(/\/index\.[^/]+$/u, "").replace(/^index\.[^/]+$/u, "").replace(/^\/+|\/+$/g, "");
7110
+ }
7111
+ function containedPath(outDir, ...segments) {
7112
+ const root = node_path.resolve(outDir);
7113
+ const resolved = node_path.resolve(root, ...segments);
7114
+ const prefix = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
7115
+ if (resolved !== root && !resolved.startsWith(prefix)) return;
7116
+ if (segments.some((segment) => segment === ".." || segment.includes("\0"))) return;
7117
+ return resolved;
7118
+ }
7119
+ //#endregion
7120
+ //#region src/section-index.ts
7121
+ /**
7122
+ * Opt-in generated section index pages.
7123
+ *
7124
+ * Resolution and directory walking live here. Listing HTML is rendered in
7125
+ * Rust (`ox_content_ssg::render_section_index`) when the NAPI helper is
7126
+ * available; a matching TypeScript renderer covers the same escape / href
7127
+ * rules so the SSG path stays safe either way. The Vite plugin appends
7128
+ * themed HTML during SSG and never overwrites an existing index page.
7129
+ */
7130
+ /**
7131
+ * Resolves `ssg.sectionIndex` with defaults.
7132
+ *
7133
+ * `false` / omitted stays off. `true` enables card listings. An object
7134
+ * enables the feature and overrides only the fields the site set.
7135
+ */
7136
+ function resolveSectionIndexOptions(value) {
7137
+ if (!value) return {
7138
+ enabled: false,
7139
+ style: "cards"
7140
+ };
7141
+ if (value === true) return {
7142
+ enabled: true,
7143
+ style: "cards"
7144
+ };
7145
+ return {
7146
+ enabled: true,
7147
+ style: value.style === "list" ? "list" : "cards"
7148
+ };
7149
+ }
7150
+ /** Maps a generated section index onto the SSG render shape. */
7151
+ function toSectionIndexProcessResult(page) {
7152
+ return {
7153
+ inputPath: page.outputPath,
7154
+ routePaths: {
7155
+ outputPath: page.outputPath,
7156
+ urlPath: page.urlPath,
7157
+ href: page.href,
7158
+ ogImagePath: "",
7159
+ ogImageUrl: ""
7160
+ },
7161
+ transformedHtml: page.content,
7162
+ title: page.title,
7163
+ frontmatter: {},
7164
+ toc: []
7165
+ };
7166
+ }
7167
+ /** Appends generated section indexes for directories that have no real index. */
7168
+ async function appendSectionIndexPages(input) {
7169
+ if (!input.options?.enabled) return;
7170
+ const existingOutputs = new Set(input.generatedPages.map((page) => node_path.normalize(page.outputPath)));
7171
+ for (const spec of sectionIndexSpecs(input.collectedPages, input.listedPages, input.options, input.outDir, input.base, input.extension)) {
7172
+ if (existingOutputs.has(node_path.normalize(spec.outputPath))) continue;
7173
+ try {
7174
+ const html = await input.render(spec);
7175
+ input.generatedPages.push({
7176
+ inputPath: spec.outputPath,
7177
+ outputPath: spec.outputPath,
7178
+ html
7179
+ });
7180
+ existingOutputs.add(node_path.normalize(spec.outputPath));
7181
+ } catch (err) {
7182
+ const message = err instanceof Error ? err.message : String(err);
7183
+ input.errors.push(`Failed to generate section index ${spec.href}: ${message}`);
7184
+ }
7185
+ }
7186
+ }
7187
+ function sectionIndexSpecs(collected, listed, options, outDir, base, extension) {
7188
+ const occupied = /* @__PURE__ */ new Set();
7189
+ for (const page of collected) occupied.add(normalizeUrlPath(page.routePaths.urlPath));
7190
+ for (const page of collected) {
7191
+ const output = page.routePaths.outputPath;
7192
+ if (output) occupied.add(dirFromOutputPath(output, outDir));
7193
+ }
7194
+ const visible = listed.filter((page) => !isHiddenByFlags(page.frontmatter));
7195
+ const childrenByDir = /* @__PURE__ */ new Map();
7196
+ for (const page of visible) {
7197
+ const urlPath = normalizeUrlPath(page.routePaths.urlPath);
7198
+ const parent = parentDir(urlPath);
7199
+ if (parent === null) continue;
7200
+ pushChild(childrenByDir, parent, {
7201
+ title: pageTitle(page),
7202
+ href: page.routePaths.href,
7203
+ description: page.description
7204
+ });
7205
+ let ancestor = parent;
7206
+ while (ancestor !== "") {
7207
+ const grand = parentDir(ancestor);
7208
+ if (grand === null) break;
7209
+ const nested = firstChildDir(urlPath, grand);
7210
+ if (nested) pushUniqueDir(childrenByDir, grand, nested, visible, base, extension);
7211
+ ancestor = grand;
7212
+ }
7213
+ }
7214
+ const pages = [];
7215
+ const dirs = [...childrenByDir.keys()].sort();
7216
+ for (const dir of dirs) {
7217
+ if (occupied.has(dir)) continue;
7218
+ const children = uniqueItems(childrenByDir.get(dir) ?? []).filter((item) => isSafeSectionHref(item.href));
7219
+ if (children.length === 0) continue;
7220
+ children.sort((left, right) => {
7221
+ const titleCmp = left.title.localeCompare(right.title);
7222
+ return titleCmp !== 0 ? titleCmp : left.href.localeCompare(right.href);
7223
+ });
7224
+ const outputPath = sectionOutputPath(outDir, dir, extension);
7225
+ if (!outputPath) continue;
7226
+ const title = sectionTitle(dir);
7227
+ pages.push({
7228
+ title,
7229
+ content: renderSectionIndexHtml(title, children, options.style),
7230
+ outputPath,
7231
+ urlPath: dir || "/",
7232
+ href: sectionHref(base, dir, extension)
7233
+ });
7234
+ }
7235
+ return pages;
7236
+ }
7237
+ function pushChild(map, dir, item) {
7238
+ const list = map.get(dir);
7239
+ if (list) {
7240
+ list.push(item);
7241
+ return;
7242
+ }
7243
+ map.set(dir, [item]);
7244
+ }
7245
+ function pushUniqueDir(map, parent, childDir, visible, base, extension) {
7246
+ const href = sectionHref(base, childDir, extension);
7247
+ if (map.get(parent)?.some((item) => item.href === href)) return;
7248
+ const indexPage = visible.find((page) => normalizeUrlPath(page.routePaths.urlPath) === childDir);
7249
+ pushChild(map, parent, {
7250
+ title: indexPage ? pageTitle(indexPage) : sectionTitle(childDir),
7251
+ href: indexPage?.routePaths.href ?? href,
7252
+ description: indexPage?.description
5691
7253
  });
5692
7254
  }
7255
+ function uniqueItems(items) {
7256
+ const seen = /* @__PURE__ */ new Set();
7257
+ const unique = [];
7258
+ for (const item of items) {
7259
+ if (seen.has(item.href)) continue;
7260
+ seen.add(item.href);
7261
+ unique.push(item);
7262
+ }
7263
+ return unique;
7264
+ }
7265
+ function isHiddenByFlags(frontmatter) {
7266
+ return frontmatter.draft === true || frontmatter.unlisted === true;
7267
+ }
5693
7268
  //#endregion
5694
7269
  //#region src/search-provider.ts
5695
7270
  const FORBIDDEN_KEY_NAMES = /* @__PURE__ */ new Set([
@@ -6084,93 +7659,1419 @@ function versionLocation(outputPath, outDir, options) {
6084
7659
  };
6085
7660
  }
6086
7661
  return {
6087
- id: options.current,
6088
- sibling: normalized
7662
+ id: options.current,
7663
+ sibling: normalized
7664
+ };
7665
+ }
7666
+ function outputToHref(outputPath, outDir, base) {
7667
+ return siteHref$1(base, "", relativeUrl(outputPath, outDir));
7668
+ }
7669
+ /** Applies switcher / banner / search rewrite after every version tree is generated. */
7670
+ function decorateVersionedPages(pages, options, outDir, base) {
7671
+ if (!options.enabled) return;
7672
+ const existingHrefs = new Set(pages.map((page) => outputToHref(page.outputPath, outDir, base)));
7673
+ for (const page of pages) {
7674
+ const { id, sibling } = versionLocation(page.outputPath, outDir, options);
7675
+ page.html = applyVersionChrome(page.html, options, id, sibling, base, existingHrefs);
7676
+ }
7677
+ }
7678
+ async function writeSnapshotSearchIndex(input) {
7679
+ const prefix = sanitizePrefix(input.prefix);
7680
+ if (!prefix) return;
7681
+ const destDir = node_path.join(input.outDir, prefix);
7682
+ const prefixBase = searchIndexUrl(input.base, prefix).replace(/search-index\.json$/, "");
7683
+ const json = await buildSearchIndex(input.srcDir, prefixBase, input.extensions, input.publishState, [], input.mdx);
7684
+ await node_fs_promises.mkdir(destDir, { recursive: true });
7685
+ await writeSearchIndex(json, destDir);
7686
+ const dest = node_path.join(destDir, "search-index.json");
7687
+ try {
7688
+ await node_fs_promises.access(dest);
7689
+ } catch {
7690
+ await node_fs_promises.writeFile(dest, json, "utf8");
7691
+ }
7692
+ return dest;
7693
+ }
7694
+ function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
7695
+ if (!options.enabled) return html;
7696
+ const active = options.entries.find((entry) => entry.id === activeId);
7697
+ return injectVersionChrome(html, options.switcher ? versionSwitcherMarkup(versionLinks(options, activeId, siblingPath, base, existingHrefs), options.badge) : "", versionBannerMarkup(active?.banner), searchIndexUrl(base, currentVersionPrefix(options)), searchIndexUrl(base, active?.prefix ?? ""));
7698
+ }
7699
+ function sanitizePrefix(prefix) {
7700
+ const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
7701
+ if (!trimmed) return "";
7702
+ return PREFIX_RE.test(trimmed) && !trimmed.includes("..") ? trimmed : "";
7703
+ }
7704
+ function defaultCurrentEntry() {
7705
+ return {
7706
+ id: DEFAULT_CURRENT_ID,
7707
+ label: "Latest",
7708
+ prefix: "",
7709
+ banner: false
7710
+ };
7711
+ }
7712
+ function normalizeEntries(entries) {
7713
+ if (!entries) return [];
7714
+ const seen = /* @__PURE__ */ new Set();
7715
+ const resolved = [];
7716
+ for (const entry of entries) {
7717
+ if (!entry || typeof entry.id !== "string" || typeof entry.label !== "string") continue;
7718
+ const id = entry.id.trim();
7719
+ const label = entry.label.trim();
7720
+ if (!id || !label || seen.has(id)) continue;
7721
+ const prefix = sanitizePrefix(typeof entry.prefix === "string" ? entry.prefix : "");
7722
+ if (entry.prefix && !prefix) continue;
7723
+ const dir = typeof entry.dir === "string" && entry.dir.trim() ? entry.dir.trim() : void 0;
7724
+ if (dir && (dir.includes("\0") || dir.includes(".."))) continue;
7725
+ seen.add(id);
7726
+ resolved.push({
7727
+ id,
7728
+ label,
7729
+ prefix,
7730
+ dir,
7731
+ banner: normalizeBanner(entry.banner)
7732
+ });
7733
+ }
7734
+ return resolved;
7735
+ }
7736
+ function normalizeBanner(value) {
7737
+ return value === "unreleased" || value === "unmaintained" ? value : false;
7738
+ }
7739
+ function siteHref$1(base, prefix, rest) {
7740
+ const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
7741
+ const parts = [prefix, rest].filter((part) => part && part !== "/");
7742
+ return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
7743
+ }
7744
+ function relativeUrl(outputPath, outDir) {
7745
+ const rel = node_path.posix.normalize(node_path.relative(node_path.resolve(outDir), node_path.resolve(outputPath)).replaceAll(node_path.sep, "/"));
7746
+ if (rel.startsWith("..")) return "";
7747
+ const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
7748
+ return dir === "." ? "" : dir;
7749
+ }
7750
+ //#endregion
7751
+ //#region src/resources-jpeg.ts
7752
+ function encodeJpeg(image, quality = 80) {
7753
+ const yQuant = scaleQuant(LUM_QUANT, quality);
7754
+ const cQuant = scaleQuant(CHR_QUANT, quality);
7755
+ const width = image.width;
7756
+ const height = image.height;
7757
+ const duY = /* @__PURE__ */ new Int32Array(64);
7758
+ const duCb = /* @__PURE__ */ new Int32Array(64);
7759
+ const duCr = /* @__PURE__ */ new Int32Array(64);
7760
+ const bits = new BitWriter();
7761
+ let dcY = 0;
7762
+ let dcCb = 0;
7763
+ let dcCr = 0;
7764
+ for (let y = 0; y < height; y += 8) for (let x = 0; x < width; x += 8) {
7765
+ sampleBlock(image, x, y, duY, duCb, duCr);
7766
+ dcY = encodeBlock(bits, duY, yQuant, dcY, YDC, YAC);
7767
+ dcCb = encodeBlock(bits, duCb, cQuant, dcCb, CDC, CAC);
7768
+ dcCr = encodeBlock(bits, duCr, cQuant, dcCr, CDC, CAC);
7769
+ }
7770
+ bits.flush();
7771
+ return Buffer.concat([
7772
+ jpegHeader(width, height, yQuant, cQuant),
7773
+ bits.toBuffer(),
7774
+ Buffer.from([255, 217])
7775
+ ]);
7776
+ }
7777
+ function sampleBlock(image, left, top, yOut, cbOut, crOut) {
7778
+ for (let j = 0; j < 8; j++) {
7779
+ const y = Math.min(image.height - 1, top + j);
7780
+ for (let i = 0; i < 8; i++) {
7781
+ const x = Math.min(image.width - 1, left + i);
7782
+ const p = (y * image.width + x) * 4;
7783
+ const r = image.data[p] ?? 0;
7784
+ const g = image.data[p + 1] ?? 0;
7785
+ const b = image.data[p + 2] ?? 0;
7786
+ const idx = j * 8 + i;
7787
+ yOut[idx] = (66 * r + 129 * g + 25 * b + 128 >> 8) - 128;
7788
+ cbOut[idx] = -38 * r - 74 * g + 112 * b + 128 >> 8;
7789
+ crOut[idx] = 112 * r - 94 * g - 18 * b + 128 >> 8;
7790
+ }
7791
+ }
7792
+ }
7793
+ function encodeBlock(bits, block, quant, lastDc, dcTable, acTable) {
7794
+ const dct = forwardDct(block);
7795
+ const zz = /* @__PURE__ */ new Int32Array(64);
7796
+ for (let i = 0; i < 64; i++) zz[i] = Math.round(dct[ZIGZAG[i]] / quant[i]);
7797
+ const dc = zz[0] ?? 0;
7798
+ writeCoeff(bits, dc - lastDc, dcTable);
7799
+ let zeroRun = 0;
7800
+ for (let i = 1; i < 64; i++) {
7801
+ const value = zz[i] ?? 0;
7802
+ if (value === 0) {
7803
+ zeroRun++;
7804
+ continue;
7805
+ }
7806
+ while (zeroRun > 15) {
7807
+ writeCode(bits, acTable, 240);
7808
+ zeroRun -= 16;
7809
+ }
7810
+ writeCoeff(bits, value, acTable, zeroRun);
7811
+ zeroRun = 0;
7812
+ }
7813
+ if (zeroRun > 0) writeCode(bits, acTable, 0);
7814
+ return dc;
7815
+ }
7816
+ function writeCoeff(bits, value, table, run = 0) {
7817
+ const category = bitCategory(value);
7818
+ writeCode(bits, table, run << 4 | category);
7819
+ if (category > 0) bits.writeBits(value < 0 ? value + ((1 << category) - 1) : value, category);
7820
+ }
7821
+ function writeCode(bits, table, symbol) {
7822
+ const entry = table.get(symbol);
7823
+ if (!entry) throw new Error("missing Huffman code");
7824
+ bits.writeBits(entry.code, entry.len);
7825
+ }
7826
+ function bitCategory(value) {
7827
+ const abs = Math.abs(value);
7828
+ if (!Number.isFinite(abs) || abs === 0) return 0;
7829
+ return Math.min(11, Math.ceil(Math.log2(abs + 1)));
7830
+ }
7831
+ function forwardDct(block) {
7832
+ const out = /* @__PURE__ */ new Float64Array(64);
7833
+ for (let v = 0; v < 8; v++) for (let u = 0; u < 8; u++) {
7834
+ let sum = 0;
7835
+ for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) sum += (block[y * 8 + x] ?? 0) * Math.cos((2 * x + 1) * u * Math.PI / 16) * Math.cos((2 * y + 1) * v * Math.PI / 16);
7836
+ const cu = u === 0 ? Math.SQRT1_2 : 1;
7837
+ const cv = v === 0 ? Math.SQRT1_2 : 1;
7838
+ out[v * 8 + u] = .25 * cu * cv * sum;
7839
+ }
7840
+ return out;
7841
+ }
7842
+ function scaleQuant(base, quality) {
7843
+ const q = Math.max(1, Math.min(100, quality));
7844
+ const scale = q < 50 ? Math.floor(5e3 / q) : Math.floor(200 - q * 2);
7845
+ return base.map((value) => Math.max(1, Math.min(255, Math.floor((value * scale + 50) / 100))));
7846
+ }
7847
+ function jpegHeader(width, height, yQuant, cQuant) {
7848
+ const chunks = [
7849
+ Buffer.from([255, 216]),
7850
+ jfifApp0(),
7851
+ dqt(0, yQuant),
7852
+ dqt(1, cQuant),
7853
+ sof(width, height),
7854
+ dht(0, 0, STD_DC_LUM_NCODES, STD_DC_LUM_VALUES),
7855
+ dht(0, 1, STD_DC_CHR_NCODES, STD_DC_CHR_VALUES),
7856
+ dht(1, 0, STD_AC_LUM_NCODES, STD_AC_LUM_VALUES),
7857
+ dht(1, 1, STD_AC_CHR_NCODES, STD_AC_CHR_VALUES),
7858
+ sos()
7859
+ ];
7860
+ return Buffer.concat(chunks);
7861
+ }
7862
+ function jfifApp0() {
7863
+ return Buffer.from([
7864
+ 255,
7865
+ 224,
7866
+ 0,
7867
+ 16,
7868
+ 74,
7869
+ 70,
7870
+ 73,
7871
+ 70,
7872
+ 0,
7873
+ 1,
7874
+ 1,
7875
+ 0,
7876
+ 0,
7877
+ 1,
7878
+ 0,
7879
+ 1,
7880
+ 0,
7881
+ 0
7882
+ ]);
7883
+ }
7884
+ function dqt(id, table) {
7885
+ const out = Buffer.alloc(69);
7886
+ out[0] = 255;
7887
+ out[1] = 219;
7888
+ out.writeUInt16BE(67, 2);
7889
+ out[4] = id;
7890
+ for (let i = 0; i < 64; i++) out[5 + i] = table[i] ?? 1;
7891
+ return out;
7892
+ }
7893
+ function sof(width, height) {
7894
+ const out = Buffer.from([
7895
+ 255,
7896
+ 192,
7897
+ 0,
7898
+ 17,
7899
+ 8,
7900
+ 0,
7901
+ 0,
7902
+ 0,
7903
+ 0,
7904
+ 3,
7905
+ 1,
7906
+ 17,
7907
+ 0,
7908
+ 2,
7909
+ 17,
7910
+ 1,
7911
+ 3,
7912
+ 17,
7913
+ 1
7914
+ ]);
7915
+ out.writeUInt16BE(height, 5);
7916
+ out.writeUInt16BE(width, 7);
7917
+ return out;
7918
+ }
7919
+ function dht(cls, id, ncodes, values) {
7920
+ const out = Buffer.alloc(21 + values.length);
7921
+ out[0] = 255;
7922
+ out[1] = 196;
7923
+ out.writeUInt16BE(19 + values.length, 2);
7924
+ out[4] = cls << 4 | id;
7925
+ Buffer.from(ncodes).copy(out, 5);
7926
+ Buffer.from(values).copy(out, 21);
7927
+ return out;
7928
+ }
7929
+ function sos() {
7930
+ return Buffer.from([
7931
+ 255,
7932
+ 218,
7933
+ 0,
7934
+ 12,
7935
+ 3,
7936
+ 1,
7937
+ 0,
7938
+ 2,
7939
+ 17,
7940
+ 3,
7941
+ 17,
7942
+ 0,
7943
+ 63,
7944
+ 0
7945
+ ]);
7946
+ }
7947
+ var BitWriter = class {
7948
+ bytes = [];
7949
+ bits = 0;
7950
+ length = 0;
7951
+ writeBits(value, count) {
7952
+ for (let i = count - 1; i >= 0; i--) {
7953
+ this.bits = this.bits << 1 | value >> i & 1;
7954
+ this.length++;
7955
+ if (this.length === 8) this.pushByte();
7956
+ }
7957
+ }
7958
+ flush() {
7959
+ if (this.length > 0) {
7960
+ this.bits <<= 8 - this.length;
7961
+ this.pushByte();
7962
+ }
7963
+ }
7964
+ toBuffer() {
7965
+ return Buffer.from(this.bytes);
7966
+ }
7967
+ pushByte() {
7968
+ this.bytes.push(this.bits & 255);
7969
+ if ((this.bits & 255) === 255) this.bytes.push(0);
7970
+ this.bits = 0;
7971
+ this.length = 0;
7972
+ }
7973
+ };
7974
+ function buildHuffman(ncodes, values) {
7975
+ const table = /* @__PURE__ */ new Map();
7976
+ let code = 0;
7977
+ let index = 0;
7978
+ for (let len = 1; len <= 16; len++) {
7979
+ const count = ncodes[len - 1] ?? 0;
7980
+ for (let i = 0; i < count; i++) {
7981
+ table.set(values[index++] ?? 0, {
7982
+ code,
7983
+ len
7984
+ });
7985
+ code++;
7986
+ }
7987
+ code <<= 1;
7988
+ }
7989
+ return table;
7990
+ }
7991
+ const ZIGZAG = [
7992
+ 0,
7993
+ 1,
7994
+ 8,
7995
+ 16,
7996
+ 9,
7997
+ 2,
7998
+ 3,
7999
+ 10,
8000
+ 17,
8001
+ 24,
8002
+ 32,
8003
+ 25,
8004
+ 18,
8005
+ 11,
8006
+ 4,
8007
+ 5,
8008
+ 12,
8009
+ 19,
8010
+ 26,
8011
+ 33,
8012
+ 40,
8013
+ 48,
8014
+ 41,
8015
+ 34,
8016
+ 27,
8017
+ 20,
8018
+ 13,
8019
+ 6,
8020
+ 7,
8021
+ 14,
8022
+ 21,
8023
+ 28,
8024
+ 35,
8025
+ 42,
8026
+ 49,
8027
+ 56,
8028
+ 57,
8029
+ 50,
8030
+ 43,
8031
+ 36,
8032
+ 29,
8033
+ 22,
8034
+ 15,
8035
+ 23,
8036
+ 30,
8037
+ 37,
8038
+ 44,
8039
+ 51,
8040
+ 58,
8041
+ 59,
8042
+ 52,
8043
+ 45,
8044
+ 38,
8045
+ 31,
8046
+ 39,
8047
+ 46,
8048
+ 53,
8049
+ 60,
8050
+ 61,
8051
+ 54,
8052
+ 47,
8053
+ 55,
8054
+ 62,
8055
+ 63
8056
+ ];
8057
+ const LUM_QUANT = [
8058
+ 16,
8059
+ 11,
8060
+ 10,
8061
+ 16,
8062
+ 24,
8063
+ 40,
8064
+ 51,
8065
+ 61,
8066
+ 12,
8067
+ 12,
8068
+ 14,
8069
+ 19,
8070
+ 26,
8071
+ 58,
8072
+ 60,
8073
+ 55,
8074
+ 14,
8075
+ 13,
8076
+ 16,
8077
+ 24,
8078
+ 40,
8079
+ 57,
8080
+ 69,
8081
+ 56,
8082
+ 14,
8083
+ 17,
8084
+ 22,
8085
+ 29,
8086
+ 51,
8087
+ 87,
8088
+ 80,
8089
+ 62,
8090
+ 18,
8091
+ 22,
8092
+ 37,
8093
+ 56,
8094
+ 68,
8095
+ 109,
8096
+ 103,
8097
+ 77,
8098
+ 24,
8099
+ 35,
8100
+ 55,
8101
+ 64,
8102
+ 81,
8103
+ 104,
8104
+ 113,
8105
+ 92,
8106
+ 49,
8107
+ 64,
8108
+ 78,
8109
+ 87,
8110
+ 103,
8111
+ 121,
8112
+ 120,
8113
+ 101,
8114
+ 72,
8115
+ 92,
8116
+ 95,
8117
+ 98,
8118
+ 112,
8119
+ 100,
8120
+ 103,
8121
+ 99
8122
+ ];
8123
+ const CHR_QUANT = [
8124
+ 17,
8125
+ 18,
8126
+ 24,
8127
+ 47,
8128
+ 99,
8129
+ 99,
8130
+ 99,
8131
+ 99,
8132
+ 18,
8133
+ 21,
8134
+ 26,
8135
+ 66,
8136
+ 99,
8137
+ 99,
8138
+ 99,
8139
+ 99,
8140
+ 24,
8141
+ 26,
8142
+ 56,
8143
+ 99,
8144
+ 99,
8145
+ 99,
8146
+ 99,
8147
+ 99,
8148
+ 47,
8149
+ 66,
8150
+ 99,
8151
+ 99,
8152
+ 99,
8153
+ 99,
8154
+ 99,
8155
+ 99,
8156
+ 99,
8157
+ 99,
8158
+ 99,
8159
+ 99,
8160
+ 99,
8161
+ 99,
8162
+ 99,
8163
+ 99,
8164
+ 99,
8165
+ 99,
8166
+ 99,
8167
+ 99,
8168
+ 99,
8169
+ 99,
8170
+ 99,
8171
+ 99,
8172
+ 99,
8173
+ 99,
8174
+ 99,
8175
+ 99,
8176
+ 99,
8177
+ 99,
8178
+ 99,
8179
+ 99,
8180
+ 99,
8181
+ 99,
8182
+ 99,
8183
+ 99,
8184
+ 99,
8185
+ 99,
8186
+ 99,
8187
+ 99
8188
+ ];
8189
+ const STD_DC_LUM_NCODES = [
8190
+ 0,
8191
+ 1,
8192
+ 5,
8193
+ 1,
8194
+ 1,
8195
+ 1,
8196
+ 1,
8197
+ 1,
8198
+ 1,
8199
+ 0,
8200
+ 0,
8201
+ 0,
8202
+ 0,
8203
+ 0,
8204
+ 0,
8205
+ 0
8206
+ ];
8207
+ const STD_DC_LUM_VALUES = [
8208
+ 0,
8209
+ 1,
8210
+ 2,
8211
+ 3,
8212
+ 4,
8213
+ 5,
8214
+ 6,
8215
+ 7,
8216
+ 8,
8217
+ 9,
8218
+ 10,
8219
+ 11
8220
+ ];
8221
+ const STD_DC_CHR_NCODES = [
8222
+ 0,
8223
+ 3,
8224
+ 1,
8225
+ 1,
8226
+ 1,
8227
+ 1,
8228
+ 1,
8229
+ 1,
8230
+ 1,
8231
+ 1,
8232
+ 1,
8233
+ 0,
8234
+ 0,
8235
+ 0,
8236
+ 0,
8237
+ 0
8238
+ ];
8239
+ const STD_DC_CHR_VALUES = [
8240
+ 0,
8241
+ 1,
8242
+ 2,
8243
+ 3,
8244
+ 4,
8245
+ 5,
8246
+ 6,
8247
+ 7,
8248
+ 8,
8249
+ 9,
8250
+ 10,
8251
+ 11
8252
+ ];
8253
+ const STD_AC_LUM_NCODES = [
8254
+ 0,
8255
+ 2,
8256
+ 1,
8257
+ 3,
8258
+ 3,
8259
+ 2,
8260
+ 4,
8261
+ 3,
8262
+ 5,
8263
+ 5,
8264
+ 4,
8265
+ 4,
8266
+ 0,
8267
+ 0,
8268
+ 1,
8269
+ 125
8270
+ ];
8271
+ const STD_AC_LUM_VALUES = [
8272
+ 1,
8273
+ 2,
8274
+ 3,
8275
+ 0,
8276
+ 4,
8277
+ 17,
8278
+ 5,
8279
+ 18,
8280
+ 33,
8281
+ 49,
8282
+ 65,
8283
+ 6,
8284
+ 19,
8285
+ 81,
8286
+ 97,
8287
+ 7,
8288
+ 34,
8289
+ 113,
8290
+ 20,
8291
+ 50,
8292
+ 129,
8293
+ 145,
8294
+ 161,
8295
+ 8,
8296
+ 35,
8297
+ 66,
8298
+ 177,
8299
+ 193,
8300
+ 21,
8301
+ 82,
8302
+ 209,
8303
+ 240,
8304
+ 36,
8305
+ 51,
8306
+ 98,
8307
+ 114,
8308
+ 130,
8309
+ 9,
8310
+ 10,
8311
+ 22,
8312
+ 23,
8313
+ 24,
8314
+ 25,
8315
+ 26,
8316
+ 37,
8317
+ 38,
8318
+ 39,
8319
+ 40,
8320
+ 41,
8321
+ 42,
8322
+ 52,
8323
+ 53,
8324
+ 54,
8325
+ 55,
8326
+ 56,
8327
+ 57,
8328
+ 58,
8329
+ 67,
8330
+ 68,
8331
+ 69,
8332
+ 70,
8333
+ 71,
8334
+ 72,
8335
+ 73,
8336
+ 74,
8337
+ 83,
8338
+ 84,
8339
+ 85,
8340
+ 86,
8341
+ 87,
8342
+ 88,
8343
+ 89,
8344
+ 90,
8345
+ 99,
8346
+ 100,
8347
+ 101,
8348
+ 102,
8349
+ 103,
8350
+ 104,
8351
+ 105,
8352
+ 106,
8353
+ 115,
8354
+ 116,
8355
+ 117,
8356
+ 118,
8357
+ 119,
8358
+ 120,
8359
+ 121,
8360
+ 122,
8361
+ 131,
8362
+ 132,
8363
+ 133,
8364
+ 134,
8365
+ 135,
8366
+ 136,
8367
+ 137,
8368
+ 138,
8369
+ 146,
8370
+ 147,
8371
+ 148,
8372
+ 149,
8373
+ 150,
8374
+ 151,
8375
+ 152,
8376
+ 153,
8377
+ 154,
8378
+ 162,
8379
+ 163,
8380
+ 164,
8381
+ 165,
8382
+ 166,
8383
+ 167,
8384
+ 168,
8385
+ 169,
8386
+ 170,
8387
+ 178,
8388
+ 179,
8389
+ 180,
8390
+ 181,
8391
+ 182,
8392
+ 183,
8393
+ 184,
8394
+ 185,
8395
+ 186,
8396
+ 194,
8397
+ 195,
8398
+ 196,
8399
+ 197,
8400
+ 198,
8401
+ 199,
8402
+ 200,
8403
+ 201,
8404
+ 202,
8405
+ 210,
8406
+ 211,
8407
+ 212,
8408
+ 213,
8409
+ 214,
8410
+ 215,
8411
+ 216,
8412
+ 217,
8413
+ 218,
8414
+ 225,
8415
+ 226,
8416
+ 227,
8417
+ 228,
8418
+ 229,
8419
+ 230,
8420
+ 231,
8421
+ 232,
8422
+ 233,
8423
+ 234,
8424
+ 241,
8425
+ 242,
8426
+ 243,
8427
+ 244,
8428
+ 245,
8429
+ 246,
8430
+ 247,
8431
+ 248,
8432
+ 249,
8433
+ 250
8434
+ ];
8435
+ const STD_AC_CHR_NCODES = [
8436
+ 0,
8437
+ 2,
8438
+ 1,
8439
+ 2,
8440
+ 4,
8441
+ 4,
8442
+ 3,
8443
+ 4,
8444
+ 7,
8445
+ 5,
8446
+ 4,
8447
+ 4,
8448
+ 0,
8449
+ 1,
8450
+ 2,
8451
+ 119
8452
+ ];
8453
+ const STD_AC_CHR_VALUES = [
8454
+ 0,
8455
+ 1,
8456
+ 2,
8457
+ 3,
8458
+ 17,
8459
+ 4,
8460
+ 5,
8461
+ 33,
8462
+ 49,
8463
+ 6,
8464
+ 18,
8465
+ 65,
8466
+ 81,
8467
+ 7,
8468
+ 97,
8469
+ 113,
8470
+ 19,
8471
+ 34,
8472
+ 50,
8473
+ 129,
8474
+ 8,
8475
+ 20,
8476
+ 66,
8477
+ 145,
8478
+ 161,
8479
+ 177,
8480
+ 193,
8481
+ 9,
8482
+ 35,
8483
+ 51,
8484
+ 82,
8485
+ 240,
8486
+ 21,
8487
+ 98,
8488
+ 114,
8489
+ 209,
8490
+ 10,
8491
+ 22,
8492
+ 36,
8493
+ 52,
8494
+ 225,
8495
+ 37,
8496
+ 241,
8497
+ 23,
8498
+ 24,
8499
+ 25,
8500
+ 26,
8501
+ 38,
8502
+ 39,
8503
+ 40,
8504
+ 41,
8505
+ 42,
8506
+ 53,
8507
+ 54,
8508
+ 55,
8509
+ 56,
8510
+ 57,
8511
+ 58,
8512
+ 67,
8513
+ 68,
8514
+ 69,
8515
+ 70,
8516
+ 71,
8517
+ 72,
8518
+ 73,
8519
+ 74,
8520
+ 83,
8521
+ 84,
8522
+ 85,
8523
+ 86,
8524
+ 87,
8525
+ 88,
8526
+ 89,
8527
+ 90,
8528
+ 99,
8529
+ 100,
8530
+ 101,
8531
+ 102,
8532
+ 103,
8533
+ 104,
8534
+ 105,
8535
+ 106,
8536
+ 115,
8537
+ 116,
8538
+ 117,
8539
+ 118,
8540
+ 119,
8541
+ 120,
8542
+ 121,
8543
+ 122,
8544
+ 130,
8545
+ 131,
8546
+ 132,
8547
+ 133,
8548
+ 134,
8549
+ 135,
8550
+ 136,
8551
+ 137,
8552
+ 138,
8553
+ 146,
8554
+ 147,
8555
+ 148,
8556
+ 149,
8557
+ 150,
8558
+ 151,
8559
+ 152,
8560
+ 153,
8561
+ 154,
8562
+ 162,
8563
+ 163,
8564
+ 164,
8565
+ 165,
8566
+ 166,
8567
+ 167,
8568
+ 168,
8569
+ 169,
8570
+ 170,
8571
+ 178,
8572
+ 179,
8573
+ 180,
8574
+ 181,
8575
+ 182,
8576
+ 183,
8577
+ 184,
8578
+ 185,
8579
+ 186,
8580
+ 194,
8581
+ 195,
8582
+ 196,
8583
+ 197,
8584
+ 198,
8585
+ 199,
8586
+ 200,
8587
+ 201,
8588
+ 202,
8589
+ 210,
8590
+ 211,
8591
+ 212,
8592
+ 213,
8593
+ 214,
8594
+ 215,
8595
+ 216,
8596
+ 217,
8597
+ 218,
8598
+ 226,
8599
+ 227,
8600
+ 228,
8601
+ 229,
8602
+ 230,
8603
+ 231,
8604
+ 232,
8605
+ 233,
8606
+ 234,
8607
+ 242,
8608
+ 243,
8609
+ 244,
8610
+ 245,
8611
+ 246,
8612
+ 247,
8613
+ 248,
8614
+ 249,
8615
+ 250
8616
+ ];
8617
+ const YDC = buildHuffman(STD_DC_LUM_NCODES, STD_DC_LUM_VALUES);
8618
+ const CDC = buildHuffman(STD_DC_CHR_NCODES, STD_DC_CHR_VALUES);
8619
+ const YAC = buildHuffman(STD_AC_LUM_NCODES, STD_AC_LUM_VALUES);
8620
+ const CAC = buildHuffman(STD_AC_CHR_NCODES, STD_AC_CHR_VALUES);
8621
+ //#endregion
8622
+ //#region src/resources-image.ts
8623
+ /**
8624
+ * Build-time PNG/JPEG pixel helpers for page resources.
8625
+ *
8626
+ * PNG is decoded and re-encoded for resize/crop. JPEG is encode-only so a
8627
+ * `format=jpeg` transform can change the container after the pixel pass.
8628
+ */
8629
+ const PNG_SIGNATURE = Buffer.from([
8630
+ 137,
8631
+ 80,
8632
+ 78,
8633
+ 71,
8634
+ 13,
8635
+ 10,
8636
+ 26,
8637
+ 10
8638
+ ]);
8639
+ function isPng(buffer) {
8640
+ return buffer.length >= 8 && PNG_SIGNATURE.equals(buffer.subarray(0, 8));
8641
+ }
8642
+ function decodePng(buffer) {
8643
+ if (!isPng(buffer)) throw new Error("not a PNG");
8644
+ let width = 0;
8645
+ let height = 0;
8646
+ let bitDepth = 0;
8647
+ let colorType = 0;
8648
+ const idat = [];
8649
+ let offset = 8;
8650
+ while (offset + 12 <= buffer.length) {
8651
+ const length = buffer.readUInt32BE(offset);
8652
+ const type = buffer.toString("ascii", offset + 4, offset + 8);
8653
+ const start = offset + 8;
8654
+ const end = start + length;
8655
+ if (end + 4 > buffer.length) break;
8656
+ const chunk = buffer.subarray(start, end);
8657
+ if (type === "IHDR") {
8658
+ width = chunk.readUInt32BE(0);
8659
+ height = chunk.readUInt32BE(4);
8660
+ bitDepth = chunk[8] ?? 0;
8661
+ colorType = chunk[9] ?? 0;
8662
+ } else if (type === "IDAT") idat.push(Buffer.from(chunk));
8663
+ else if (type === "IEND") break;
8664
+ offset = end + 4;
8665
+ }
8666
+ if (bitDepth !== 8 || colorType !== 2 && colorType !== 6) throw new Error("unsupported PNG");
8667
+ const channels = colorType === 6 ? 4 : 3;
8668
+ const raw = (0, node_zlib.inflateSync)(Buffer.concat(idat));
8669
+ const stride = width * channels;
8670
+ const data = new Uint8Array(width * height * 4);
8671
+ let src = 0;
8672
+ const prior = new Uint8Array(stride);
8673
+ const recon = new Uint8Array(stride);
8674
+ for (let y = 0; y < height; y++) {
8675
+ const filter = raw[src++] ?? 0;
8676
+ for (let x = 0; x < stride; x++) {
8677
+ const sample = raw[src++] ?? 0;
8678
+ const a = x >= channels ? recon[x - channels] : 0;
8679
+ const b = prior[x] ?? 0;
8680
+ const c = x >= channels ? prior[x - channels] : 0;
8681
+ recon[x] = sample + paethPredict(filter, a, b, c) & 255;
8682
+ }
8683
+ for (let x = 0; x < width; x++) {
8684
+ const i = x * channels;
8685
+ const o = (y * width + x) * 4;
8686
+ data[o] = recon[i] ?? 0;
8687
+ data[o + 1] = recon[i + 1] ?? 0;
8688
+ data[o + 2] = recon[i + 2] ?? 0;
8689
+ data[o + 3] = channels === 4 ? recon[i + 3] ?? 255 : 255;
8690
+ }
8691
+ prior.set(recon);
8692
+ }
8693
+ return {
8694
+ width,
8695
+ height,
8696
+ data
8697
+ };
8698
+ }
8699
+ function paethPredict(filter, a, b, c) {
8700
+ switch (filter) {
8701
+ case 0: return 0;
8702
+ case 1: return a;
8703
+ case 2: return b;
8704
+ case 3: return a + b >> 1;
8705
+ case 4: {
8706
+ const p = a + b - c;
8707
+ const pa = Math.abs(p - a);
8708
+ const pb = Math.abs(p - b);
8709
+ const pc = Math.abs(p - c);
8710
+ if (pa <= pb && pa <= pc) return a;
8711
+ if (pb <= pc) return b;
8712
+ return c;
8713
+ }
8714
+ default: throw new Error("unsupported PNG filter");
8715
+ }
8716
+ }
8717
+ function encodePng(image) {
8718
+ const { width, height, data } = image;
8719
+ const raw = Buffer.alloc((width * 4 + 1) * height);
8720
+ let offset = 0;
8721
+ for (let y = 0; y < height; y++) {
8722
+ raw[offset++] = 0;
8723
+ raw.set(data.subarray(y * width * 4, (y + 1) * width * 4), offset);
8724
+ offset += width * 4;
8725
+ }
8726
+ const ihdr = Buffer.alloc(13);
8727
+ ihdr.writeUInt32BE(width, 0);
8728
+ ihdr.writeUInt32BE(height, 4);
8729
+ ihdr[8] = 8;
8730
+ ihdr[9] = 6;
8731
+ return Buffer.concat([
8732
+ PNG_SIGNATURE,
8733
+ pngChunk("IHDR", ihdr),
8734
+ pngChunk("IDAT", (0, node_zlib.deflateSync)(raw)),
8735
+ pngChunk("IEND", Buffer.alloc(0))
8736
+ ]);
8737
+ }
8738
+ function pngChunk(type, data) {
8739
+ const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
8740
+ const chunk = Buffer.alloc(12 + data.length);
8741
+ chunk.writeUInt32BE(data.length, 0);
8742
+ body.copy(chunk, 4);
8743
+ chunk.writeUInt32BE(crc32(body), 8 + data.length);
8744
+ return chunk;
8745
+ }
8746
+ function crc32(data) {
8747
+ let crc = 4294967295;
8748
+ for (const byte of data) {
8749
+ crc ^= byte;
8750
+ for (let i = 0; i < 8; i++) crc = crc & 1 ? crc >>> 1 ^ 3988292384 : crc >>> 1;
8751
+ }
8752
+ return (crc ^ 4294967295) >>> 0;
8753
+ }
8754
+ function resizeNearest(image, width, height) {
8755
+ const data = new Uint8Array(width * height * 4);
8756
+ for (let y = 0; y < height; y++) {
8757
+ const sy = Math.min(image.height - 1, Math.floor(y * image.height / height));
8758
+ for (let x = 0; x < width; x++) {
8759
+ const sx = Math.min(image.width - 1, Math.floor(x * image.width / width));
8760
+ data.set(image.data.subarray((sy * image.width + sx) * 4, (sy * image.width + sx) * 4 + 4), (y * width + x) * 4);
8761
+ }
8762
+ }
8763
+ return {
8764
+ width,
8765
+ height,
8766
+ data
8767
+ };
8768
+ }
8769
+ function cropImage(image, x, y, width, height) {
8770
+ const left = Math.max(0, Math.min(image.width, Math.floor(x)));
8771
+ const top = Math.max(0, Math.min(image.height, Math.floor(y)));
8772
+ const cropW = Math.max(1, Math.min(image.width - left, Math.floor(width)));
8773
+ const cropH = Math.max(1, Math.min(image.height - top, Math.floor(height)));
8774
+ const data = new Uint8Array(cropW * cropH * 4);
8775
+ for (let row = 0; row < cropH; row++) {
8776
+ const src = ((top + row) * image.width + left) * 4;
8777
+ data.set(image.data.subarray(src, src + cropW * 4), row * cropW * 4);
8778
+ }
8779
+ return {
8780
+ width: cropW,
8781
+ height: cropH,
8782
+ data
8783
+ };
8784
+ }
8785
+ function coverCrop(image, width, height) {
8786
+ const scale = Math.max(width / image.width, height / image.height);
8787
+ const scaled = resizeNearest(image, Math.max(width, Math.round(image.width * scale)), Math.max(height, Math.round(image.height * scale)));
8788
+ return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
8789
+ }
8790
+ //#endregion
8791
+ //#region src/resources-process.ts
8792
+ /**
8793
+ * Page-resource HTML rewriting and transform writes.
8794
+ */
8795
+ const IMG_TAG = /<img\b[^>]*>/gi;
8796
+ const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
8797
+ async function processPageResources(input) {
8798
+ if (!input.options.enabled) return {
8799
+ html: input.html,
8800
+ files: [],
8801
+ errors: [],
8802
+ fatal: []
8803
+ };
8804
+ const bundleRoot = node_path.dirname(input.inputPath);
8805
+ const outputDir = node_path.dirname(input.outputPath);
8806
+ const files = [];
8807
+ const errors = [];
8808
+ const fatal = [];
8809
+ let html = input.html;
8810
+ const tags = input.html.match(IMG_TAG) ?? [];
8811
+ for (const tag of tags) {
8812
+ const srcMatch = tag.match(SRC_ATTR);
8813
+ const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
8814
+ if (!rawSrc) continue;
8815
+ const src = unescapeHtml(rawSrc);
8816
+ const parsed = parseResourceSrc(src);
8817
+ if (!parsed) continue;
8818
+ const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
8819
+ if (!resolved.ok) {
8820
+ const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
8821
+ errors.push(message);
8822
+ fatal.push(message);
8823
+ continue;
8824
+ }
8825
+ let stat;
8826
+ try {
8827
+ stat = await node_fs_promises.stat(resolved.absolute);
8828
+ } catch {
8829
+ const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
8830
+ errors.push(message);
8831
+ if (input.options.missing === "error") fatal.push(message);
8832
+ continue;
8833
+ }
8834
+ const transformError = validateTransform(parsed.transform, input.options);
8835
+ if (transformError) {
8836
+ const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
8837
+ errors.push(message);
8838
+ fatal.push(message);
8839
+ continue;
8840
+ }
8841
+ const hasTransform = hasPixelOrFormatTransform(parsed.transform);
8842
+ const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : node_path.basename(resolved.absolute);
8843
+ const outputFile = node_path.join(outputDir, outputName);
8844
+ try {
8845
+ if (hasTransform) await writeTransformedResource({
8846
+ sourcePath: resolved.absolute,
8847
+ outputFile,
8848
+ cacheDir: input.cacheDir,
8849
+ mtimeMs: stat.mtimeMs,
8850
+ transform: parsed.transform
8851
+ });
8852
+ else {
8853
+ await node_fs_promises.mkdir(outputDir, { recursive: true });
8854
+ await node_fs_promises.copyFile(resolved.absolute, outputFile);
8855
+ }
8856
+ files.push(outputFile);
8857
+ const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
8858
+ html = html.replace(tag, rewritten);
8859
+ } catch (error) {
8860
+ const detail = error instanceof Error ? error.message : String(error);
8861
+ const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
8862
+ errors.push(message);
8863
+ fatal.push(message);
8864
+ }
8865
+ }
8866
+ return {
8867
+ html,
8868
+ files,
8869
+ errors,
8870
+ fatal
8871
+ };
8872
+ }
8873
+ function resolveBundlePath(pathname, bundleRoot, contentRoot) {
8874
+ if (node_path.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
8875
+ const absolute = node_path.resolve(bundleRoot, pathname);
8876
+ if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
8877
+ return {
8878
+ ok: true,
8879
+ absolute
6089
8880
  };
6090
8881
  }
6091
- function outputToHref(outputPath, outDir, base) {
6092
- return siteHref$1(base, "", relativeUrl(outputPath, outDir));
8882
+ function validateTransform(transform, options) {
8883
+ if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
8884
+ if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
6093
8885
  }
6094
- /** Applies switcher / banner / search rewrite after every version tree is generated. */
6095
- function decorateVersionedPages(pages, options, outDir, base) {
6096
- if (!options.enabled) return;
6097
- const existingHrefs = new Set(pages.map((page) => outputToHref(page.outputPath, outDir, base)));
6098
- for (const page of pages) {
6099
- const { id, sibling } = versionLocation(page.outputPath, outDir, options);
6100
- page.html = applyVersionChrome(page.html, options, id, sibling, base, existingHrefs);
6101
- }
8886
+ function hasPixelOrFormatTransform(transform) {
8887
+ return Boolean(transform.width || transform.height || transform.crop || transform.format);
6102
8888
  }
6103
- async function writeSnapshotSearchIndex(input) {
6104
- const prefix = sanitizePrefix(input.prefix);
6105
- if (!prefix) return;
6106
- const destDir = node_path.join(input.outDir, prefix);
6107
- const prefixBase = searchIndexUrl(input.base, prefix).replace(/search-index\.json$/, "");
6108
- const json = await buildSearchIndex(input.srcDir, prefixBase, input.extensions, input.publishState, [], input.mdx);
6109
- await node_fs_promises.mkdir(destDir, { recursive: true });
6110
- await writeSearchIndex(json, destDir);
6111
- const dest = node_path.join(destDir, "search-index.json");
8889
+ function transformedFileName(pathname, transform, cacheKey) {
8890
+ const stem = node_path.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
8891
+ const ext = outputExtension(pathname, transform.format);
8892
+ return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
8893
+ }
8894
+ function outputExtension(pathname, format) {
8895
+ if (format === "jpeg") return "jpg";
8896
+ if (format) return format;
8897
+ const ext = node_path.extname(pathname).slice(1).toLowerCase();
8898
+ return ext === "jpeg" ? "jpg" : ext || "png";
8899
+ }
8900
+ async function writeTransformedResource(input) {
8901
+ const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
8902
+ const ext = node_path.extname(input.outputFile);
8903
+ const cacheFile = node_path.join(input.cacheDir, `${key}${ext}`);
6112
8904
  try {
6113
- await node_fs_promises.access(dest);
6114
- } catch {
6115
- await node_fs_promises.writeFile(dest, json, "utf8");
8905
+ await node_fs_promises.copyFile(cacheFile, input.outputFile);
8906
+ return;
8907
+ } catch {}
8908
+ const output = transformResourceBuffer(await node_fs_promises.readFile(input.sourcePath), input.sourcePath, input.transform);
8909
+ if (output.length > 8388608) throw new Error("transform produced an oversized file");
8910
+ await node_fs_promises.mkdir(node_path.dirname(input.outputFile), { recursive: true });
8911
+ await node_fs_promises.mkdir(input.cacheDir, { recursive: true });
8912
+ await node_fs_promises.writeFile(cacheFile, output);
8913
+ await node_fs_promises.writeFile(input.outputFile, output);
8914
+ }
8915
+ function transformResourceBuffer(source, sourcePath, transform) {
8916
+ const needsPixels = Boolean(transform.width || transform.height || transform.crop);
8917
+ if (!needsPixels && !transform.format) return source;
8918
+ if (!needsPixels && transform.format) {
8919
+ if (!isPng(source)) {
8920
+ if (transform.format === formatFromPath(sourcePath)) return source;
8921
+ throw new Error(`cannot convert ${node_path.extname(sourcePath) || "source"} to ${transform.format}`);
8922
+ }
8923
+ return encodeFormat(decodePng(source), transform.format);
8924
+ }
8925
+ if (!isPng(source)) throw new Error("resize/crop requires a PNG source");
8926
+ return encodeFormat(applyPixelTransform(decodePng(source), transform), transform.format ?? "png");
8927
+ }
8928
+ function applyPixelTransform(image, transform) {
8929
+ const crop = transform.crop;
8930
+ if (crop && crop !== "center") {
8931
+ const parts = crop.split(",").map((part) => Number(part.trim()));
8932
+ if (parts.length === 4 && parts.every((part) => Number.isFinite(part))) return cropImage(image, parts[0], parts[1], parts[2], parts[3]);
8933
+ throw new Error(`invalid crop ${crop}`);
8934
+ }
8935
+ const width = transform.width;
8936
+ const height = transform.height;
8937
+ if (crop === "center") {
8938
+ if (!width || !height) throw new Error("crop=center requires width and height");
8939
+ return coverCrop(image, width, height);
8940
+ }
8941
+ if (width && height) return resizeNearest(image, width, height);
8942
+ if (width) return resizeNearest(image, width, Math.max(1, Math.round(image.height * width / image.width)));
8943
+ if (height) return resizeNearest(image, Math.max(1, Math.round(image.width * height / image.height)), height);
8944
+ return image;
8945
+ }
8946
+ function encodeFormat(image, format) {
8947
+ if (format === "jpeg") return encodeJpeg(image);
8948
+ if (format === "png") return encodePng(image);
8949
+ if (format === "webp") throw new Error("webp encoding requires a webp source without pixel transforms");
8950
+ throw new Error(`unsupported format ${format}`);
8951
+ }
8952
+ function formatFromPath(filePath) {
8953
+ const ext = node_path.extname(filePath).slice(1).toLowerCase();
8954
+ return ext === "jpg" ? "jpeg" : ext;
8955
+ }
8956
+ function unescapeHtml(value) {
8957
+ return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
8958
+ }
8959
+ function escapeAttribute(value) {
8960
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8961
+ }
8962
+ //#endregion
8963
+ //#region src/resources.ts
8964
+ /**
8965
+ * Opt-in page-bundle resources and build-time image processing.
8966
+ *
8967
+ * A page directory is the bundle root. Sibling images are addressable with
8968
+ * relative URLs. Resize/crop/format query transforms run at build time and
8969
+ * are cached by source mtime plus transform params. Paths that leave the
8970
+ * bundle or `srcDir` are never processed.
8971
+ */
8972
+ const DEFAULT_FORMATS = [
8973
+ "png",
8974
+ "jpeg",
8975
+ "webp"
8976
+ ];
8977
+ const HOSTILE_SRC = /^(?:javascript|data|vbscript):/i;
8978
+ var PageResourceError = class extends Error {
8979
+ issues;
8980
+ constructor(issues) {
8981
+ super(issues.join("\n"));
8982
+ this.name = "PageResourceError";
8983
+ this.issues = issues;
6116
8984
  }
6117
- return dest;
8985
+ };
8986
+ /**
8987
+ * Resolves `resources`. Omitted / `false` stay off. `true` or `{}` enables
8988
+ * defaults. An object enables the feature and overrides only set fields.
8989
+ */
8990
+ function resolveResourcesOptions(value) {
8991
+ if (!value) return {
8992
+ enabled: false,
8993
+ formats: [...DEFAULT_FORMATS],
8994
+ widths: [],
8995
+ missing: "error"
8996
+ };
8997
+ if (value === true) return {
8998
+ enabled: true,
8999
+ formats: [...DEFAULT_FORMATS],
9000
+ widths: [],
9001
+ missing: "error"
9002
+ };
9003
+ return {
9004
+ enabled: true,
9005
+ formats: normalizeFormats(value.formats),
9006
+ widths: normalizeWidths(value.widths),
9007
+ missing: value.missing === "warn" ? "warn" : "error"
9008
+ };
6118
9009
  }
6119
- function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
6120
- if (!options.enabled) return html;
6121
- const active = options.entries.find((entry) => entry.id === activeId);
6122
- return injectVersionChrome(html, options.switcher ? versionSwitcherMarkup(versionLinks(options, activeId, siblingPath, base, existingHrefs), options.badge) : "", versionBannerMarkup(active?.banner), searchIndexUrl(base, currentVersionPrefix(options)), searchIndexUrl(base, active?.prefix ?? ""));
9010
+ /**
9011
+ * Cache key for a source file plus transform. Changing mtime or params
9012
+ * produces a different key so stale derivatives are not reused.
9013
+ */
9014
+ function resourceCacheKey(sourcePath, mtimeMs, transform) {
9015
+ return (0, node_crypto.createHash)("sha256").update(sourcePath).update("\0").update(String(mtimeMs)).update("\0").update(JSON.stringify(normalizeTransform(transform))).digest("hex");
6123
9016
  }
6124
- function sanitizePrefix(prefix) {
6125
- const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
6126
- if (!trimmed) return "";
6127
- return PREFIX_RE.test(trimmed) && !trimmed.includes("..") ? trimmed : "";
9017
+ /** True when `candidate` stays inside `root` after resolve. */
9018
+ function isInsideRoot$1(root, candidate) {
9019
+ const resolvedRoot = node_path.resolve(root);
9020
+ const resolved = node_path.resolve(candidate);
9021
+ const relative = node_path.relative(resolvedRoot, resolved);
9022
+ return relative === "" || !relative.startsWith("..") && !node_path.isAbsolute(relative);
6128
9023
  }
6129
- function defaultCurrentEntry() {
9024
+ function parseResourceSrc(src) {
9025
+ const trimmed = src.trim();
9026
+ if (!trimmed || isRemoteOrAbsolute(trimmed) || HOSTILE_SRC.test(trimmed.replace(/\s+/g, ""))) return;
9027
+ const withoutHash = trimmed.split("#")[0] ?? trimmed;
9028
+ const queryIndex = withoutHash.indexOf("?");
9029
+ const pathname = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
9030
+ const query = queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1);
9031
+ if (!pathname || pathname.includes("\0")) return;
9032
+ const params = new URLSearchParams(query);
6130
9033
  return {
6131
- id: DEFAULT_CURRENT_ID,
6132
- label: "Latest",
6133
- prefix: "",
6134
- banner: false
9034
+ pathname,
9035
+ transform: {
9036
+ width: parsePositiveInt(params.get("width") ?? params.get("w")),
9037
+ height: parsePositiveInt(params.get("height") ?? params.get("h")),
9038
+ crop: params.get("crop")?.trim() || void 0,
9039
+ format: normalizeFormat(params.get("format") ?? void 0)
9040
+ }
6135
9041
  };
6136
9042
  }
6137
- function normalizeEntries(entries) {
6138
- if (!entries) return [];
6139
- const seen = /* @__PURE__ */ new Set();
6140
- const resolved = [];
6141
- for (const entry of entries) {
6142
- if (!entry || typeof entry.id !== "string" || typeof entry.label !== "string") continue;
6143
- const id = entry.id.trim();
6144
- const label = entry.label.trim();
6145
- if (!id || !label || seen.has(id)) continue;
6146
- const prefix = sanitizePrefix(typeof entry.prefix === "string" ? entry.prefix : "");
6147
- if (entry.prefix && !prefix) continue;
6148
- const dir = typeof entry.dir === "string" && entry.dir.trim() ? entry.dir.trim() : void 0;
6149
- if (dir && (dir.includes("\0") || dir.includes(".."))) continue;
6150
- seen.add(id);
6151
- resolved.push({
6152
- id,
6153
- label,
6154
- prefix,
6155
- dir,
6156
- banner: normalizeBanner(entry.banner)
6157
- });
6158
- }
6159
- return resolved;
9043
+ function isRemoteOrAbsolute(src) {
9044
+ const compact = src.replace(/\s+/g, "");
9045
+ return /^[a-z][a-z0-9+.-]*:/i.test(compact) || compact.startsWith("//") || compact.startsWith("/");
6160
9046
  }
6161
- function normalizeBanner(value) {
6162
- return value === "unreleased" || value === "unmaintained" ? value : false;
9047
+ function parsePositiveInt(raw) {
9048
+ if (!raw) return;
9049
+ if (!/^[0-9]+$/.test(raw)) return;
9050
+ const value = Number(raw);
9051
+ return value > 0 ? value : void 0;
6163
9052
  }
6164
- function siteHref$1(base, prefix, rest) {
6165
- const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
6166
- const parts = [prefix, rest].filter((part) => part && part !== "/");
6167
- return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
9053
+ function normalizeFormats(formats) {
9054
+ if (!formats?.length) return [...DEFAULT_FORMATS];
9055
+ const normalized = formats.map((format) => normalizeFormat(format)).filter((format) => Boolean(format));
9056
+ return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_FORMATS];
6168
9057
  }
6169
- function relativeUrl(outputPath, outDir) {
6170
- const rel = node_path.posix.normalize(node_path.relative(node_path.resolve(outDir), node_path.resolve(outputPath)).replaceAll(node_path.sep, "/"));
6171
- if (rel.startsWith("..")) return "";
6172
- const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
6173
- return dir === "." ? "" : dir;
9058
+ function normalizeWidths(widths) {
9059
+ if (!widths?.length) return [];
9060
+ return [...new Set(widths.filter((width) => Number.isInteger(width) && width > 0))];
9061
+ }
9062
+ function normalizeFormat(format) {
9063
+ if (!format) return;
9064
+ const value = format.trim().toLowerCase();
9065
+ if (value === "jpg") return "jpeg";
9066
+ return value || void 0;
9067
+ }
9068
+ function normalizeTransform(transform) {
9069
+ return {
9070
+ width: transform.width,
9071
+ height: transform.height,
9072
+ crop: transform.crop,
9073
+ format: transform.format
9074
+ };
6174
9075
  }
6175
9076
  //#endregion
6176
9077
  //#region src/version-navigation.ts
@@ -6345,14 +9246,18 @@ function resolveSsgOptions(ssg) {
6345
9246
  bare: false,
6346
9247
  generateOgImage: false,
6347
9248
  lastUpdated: false,
9249
+ contributors: resolveContributorsOption(void 0),
6348
9250
  pagination: false,
6349
9251
  breadcrumbs: false,
9252
+ jsonLd: false,
6350
9253
  readerChrome: false,
6351
9254
  localeSwitcher: false,
6352
9255
  a11y: false,
6353
9256
  pageChrome: false,
6354
9257
  notFound: resolveNotFoundOptions(void 0),
6355
- team: resolveTeamOptions(void 0)
9258
+ team: resolveTeamOptions(void 0),
9259
+ blog: resolveBlogOptions(void 0),
9260
+ sectionIndex: resolveSectionIndexOptions(void 0)
6356
9261
  };
6357
9262
  if (ssg === true || ssg === void 0) return {
6358
9263
  enabled: true,
@@ -6361,14 +9266,18 @@ function resolveSsgOptions(ssg) {
6361
9266
  bare: false,
6362
9267
  generateOgImage: false,
6363
9268
  lastUpdated: false,
9269
+ contributors: resolveContributorsOption(void 0),
6364
9270
  pagination: false,
6365
9271
  breadcrumbs: false,
9272
+ jsonLd: false,
6366
9273
  readerChrome: false,
6367
9274
  localeSwitcher: false,
6368
9275
  a11y: false,
6369
9276
  pageChrome: false,
6370
9277
  notFound: resolveNotFoundOptions(void 0),
6371
9278
  team: resolveTeamOptions(void 0),
9279
+ blog: resolveBlogOptions(void 0),
9280
+ sectionIndex: resolveSectionIndexOptions(void 0),
6372
9281
  theme: require_vitepress.resolveTheme(void 0)
6373
9282
  };
6374
9283
  return {
@@ -6385,22 +9294,56 @@ function resolveSsgOptions(ssg) {
6385
9294
  ogImage: ssg.ogImage,
6386
9295
  generateOgImage: ssg.generateOgImage ?? false,
6387
9296
  lastUpdated: ssg.lastUpdated ?? false,
9297
+ contributors: resolveContributorsOption(ssg.contributors),
6388
9298
  pagination: resolvePaginationOption(ssg.pagination),
6389
9299
  breadcrumbs: resolvePaginationOption(ssg.breadcrumbs),
9300
+ jsonLd: resolveJsonLdOption(ssg.jsonLd),
6390
9301
  readerChrome: resolveReaderChromeOption(ssg.readerChrome),
6391
9302
  localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
6392
9303
  a11y: resolveA11yOption(ssg.a11y),
6393
9304
  pageChrome: require_vitepress.resolvePageChromeOption(ssg.pageChrome),
6394
9305
  notFound: resolveNotFoundOptions(ssg.notFound),
6395
9306
  team: resolveTeamOptions(ssg.team),
9307
+ blog: resolveBlogOptions(ssg.blog),
9308
+ sectionIndex: resolveSectionIndexOptions(ssg.sectionIndex),
6396
9309
  siteUrl: ssg.siteUrl,
6397
9310
  theme: require_vitepress.resolveTheme(ssg.theme),
6398
9311
  navigation: ssg.navigation
6399
9312
  };
6400
9313
  }
9314
+ function contributorsForPage(context, inputPath) {
9315
+ const option = context.ssgOptions.contributors;
9316
+ if (!option) return;
9317
+ try {
9318
+ return applyContributorOptions(context.napi?.getGitContributors(inputPath, context.root) ?? [], option);
9319
+ } catch {
9320
+ return [];
9321
+ }
9322
+ }
6401
9323
  function resolvePaginationOption(value) {
6402
9324
  return value === true || typeof value === "object" && value !== null;
6403
9325
  }
9326
+ function resolveJsonLdOption(value) {
9327
+ if (value === true) return { breadcrumbs: true };
9328
+ if (value && typeof value === "object") {
9329
+ const publisher = resolveJsonLdPublisher(value.publisher);
9330
+ return {
9331
+ breadcrumbs: value.breadcrumbs !== false,
9332
+ ...publisher ? { publisher } : {}
9333
+ };
9334
+ }
9335
+ return false;
9336
+ }
9337
+ function resolveJsonLdPublisher(publisher) {
9338
+ if (!publisher || typeof publisher !== "object") return;
9339
+ const name = publisher.name?.trim();
9340
+ const url = publisher.url?.trim();
9341
+ if (!name && !url) return;
9342
+ return {
9343
+ ...name ? { name } : {},
9344
+ ...url ? { url } : {}
9345
+ };
9346
+ }
6404
9347
  function resolveReaderChromeOption(value) {
6405
9348
  if (value === true) return {
6406
9349
  copy: true,
@@ -6529,7 +9472,7 @@ function localeCodesFor(locales) {
6529
9472
  async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
6530
9473
  enabled: false,
6531
9474
  members: []
6532
- }, pageChrome = false, breadcrumbRootHref) {
9475
+ }, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl) {
6533
9476
  const mod = await require_vitepress.importNapiModule();
6534
9477
  const tocForRust = pageData.toc.map(toRustTocEntry);
6535
9478
  const navGroupsForRust = convertNavGroupsForRust(navGroups);
@@ -6571,6 +9514,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
6571
9514
  content: pageData.content,
6572
9515
  toc: tocForRust,
6573
9516
  lastUpdated: pageData.lastUpdated,
9517
+ contributors: pageData.contributors,
6574
9518
  path: pageData.path,
6575
9519
  entryPage: entryPageForRust,
6576
9520
  prev: pageData.prev,
@@ -6597,7 +9541,12 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
6597
9541
  localePaths,
6598
9542
  a11y: a11y ? { skipLinkLabel: a11y.skipLinkLabel } : void 0,
6599
9543
  team,
6600
- pageChrome
9544
+ pageChrome,
9545
+ jsonLd: jsonLd ? {
9546
+ breadcrumbs: jsonLd.breadcrumbs,
9547
+ publisher: jsonLd.publisher,
9548
+ siteUrl
9549
+ } : void 0
6601
9550
  });
6602
9551
  }
6603
9552
  async function externalizeSharedPageAssets(pages, outDir, base) {
@@ -6684,10 +9633,31 @@ async function buildSsg(options, root) {
6684
9633
  errors.push(...collected.errors);
6685
9634
  const { outputPages, listedPages } = applyPublishState(context, collected);
6686
9635
  remapPermalinkNav(context, listedPages);
9636
+ await applyPageResources(context, outputPages, generatedFiles, errors);
6687
9637
  await generateOgImageAssets(context, collected, generatedFiles, errors);
6688
9638
  injectRelatedPages(outputPages, listedPages, context.options.taxonomies);
9639
+ const blog = context.options.blog ?? context.ssgOptions.blog;
9640
+ await injectBlogPostMeta({
9641
+ pages: outputPages,
9642
+ listed: listedPages,
9643
+ options: blog,
9644
+ srcDir: context.srcDir,
9645
+ collections: context.options.collections,
9646
+ base: context.base
9647
+ });
6689
9648
  const generatedPages = await generateHtmlPages(context, outputPages, collected, errors);
6690
9649
  await appendNotFoundPage(generatedPages, context, collected, errors);
9650
+ await appendSectionIndexPages({
9651
+ generatedPages,
9652
+ collectedPages: collected.pageResults,
9653
+ listedPages,
9654
+ options: context.ssgOptions.sectionIndex,
9655
+ outDir: context.outDir,
9656
+ base: context.base,
9657
+ extension: context.ssgOptions.extension,
9658
+ errors,
9659
+ render: (page) => renderSsgPage(context, toSectionIndexProcessResult(page), collected, listedPages)
9660
+ });
6691
9661
  await appendTaxonomyPages({
6692
9662
  generatedPages,
6693
9663
  listedPages,
@@ -6697,8 +9667,20 @@ async function buildSsg(options, root) {
6697
9667
  errors,
6698
9668
  render: (page) => renderSsgPage(context, toTaxonomyProcessResult(page), collected, listedPages)
6699
9669
  });
9670
+ await appendBlogPages({
9671
+ generatedPages,
9672
+ listedPages,
9673
+ options: blog,
9674
+ collections: context.options.collections,
9675
+ srcDir: context.srcDir,
9676
+ outDir: context.outDir,
9677
+ base: context.base,
9678
+ errors,
9679
+ render: (page) => renderSsgPage(context, toBlogProcessResult(page), collected, listedPages)
9680
+ });
6700
9681
  await applyDocumentationVersions(generatedPages, context, errors);
6701
9682
  await writeGeneratedPages(generatedPages, context, generatedFiles, listedPages, outputPages, errors);
9683
+ if (options.math?.enabled) generatedFiles.push(...await copyKatexAssets(outDir));
6702
9684
  return {
6703
9685
  files: generatedFiles,
6704
9686
  errors,
@@ -6727,7 +9709,7 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
6727
9709
  navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
6728
9710
  siteName: await resolveSiteName$1(root, ssgOptions),
6729
9711
  shouldGenerateOgImages: shouldGenerateOgImages(options),
6730
- napi: ssgOptions.lastUpdated ? await require_vitepress.importNapiModule() : void 0
9712
+ napi: ssgOptions.lastUpdated || ssgOptions.contributors ? await require_vitepress.importNapiModule() : void 0
6731
9713
  };
6732
9714
  }
6733
9715
  /**
@@ -6752,6 +9734,27 @@ async function resolveSiteName$1(root, ssgOptions) {
6752
9734
  return "Documentation";
6753
9735
  }
6754
9736
  }
9737
+ async function applyPageResources(context, pages, generatedFiles, errors) {
9738
+ const options = context.options.resources;
9739
+ if (!options?.enabled) return;
9740
+ const cacheDir = path.join(context.root, ".cache", "ox-content-resources");
9741
+ const fatal = [];
9742
+ for (const page of pages) {
9743
+ const processed = await processPageResources({
9744
+ html: page.transformedHtml,
9745
+ inputPath: page.inputPath,
9746
+ outputPath: page.routePaths.outputPath,
9747
+ srcDir: context.srcDir,
9748
+ options,
9749
+ cacheDir
9750
+ });
9751
+ page.transformedHtml = processed.html;
9752
+ generatedFiles.push(...processed.files);
9753
+ errors.push(...processed.errors);
9754
+ fatal.push(...processed.fatal);
9755
+ }
9756
+ if (fatal.length > 0) throw new PageResourceError(fatal);
9757
+ }
6755
9758
  function applyPermalinkRoutes(context, collected) {
6756
9759
  if (!context.options.permalinks?.enabled && !context.options.cascade?.enabled) return;
6757
9760
  const routed = applySsgPageRoutes({
@@ -6831,7 +9834,8 @@ async function transformSsgPage(context, inputPath) {
6831
9834
  transformedHtml,
6832
9835
  title,
6833
9836
  description: frontmatter.description,
6834
- lastUpdated: context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0,
9837
+ lastUpdated: context.ssgOptions.lastUpdated ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
9838
+ contributors: contributorsForPage(context, inputPath),
6835
9839
  frontmatter,
6836
9840
  toc: result.toc
6837
9841
  };
@@ -6986,7 +9990,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
6986
9990
  return generateHtmlPage(pageData, navItems, context.siteName, context.base, pageOgImage, theme, locale, i18n ? i18n.locales : void 0, context.ssgOptions.pagination, context.ssgOptions.readerChrome, context.ssgOptions.breadcrumbs, context.ssgOptions.localeSwitcher, localePaths, context.ssgOptions.a11y, context.ssgOptions.team ?? {
6987
9991
  enabled: false,
6988
9992
  members: []
6989
- }, context.ssgOptions.pageChrome, versionNavigation?.root.href);
9993
+ }, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl);
6990
9994
  }
6991
9995
  function rewritePagerOverride(pager, context) {
6992
9996
  return pager?.href ? {
@@ -7002,6 +10006,7 @@ function toThemePageData(pageResult) {
7002
10006
  html: pageResult.transformedHtml,
7003
10007
  toc: pageResult.toc,
7004
10008
  lastUpdated: pageResult.lastUpdated,
10009
+ contributors: pageResult.contributors,
7005
10010
  path: pageResult.inputPath,
7006
10011
  url: pageResult.routePaths.href,
7007
10012
  frontmatter: pageResult.frontmatter,
@@ -7032,6 +10037,7 @@ function createSsgPageData(pageResult) {
7032
10037
  content: pageResult.transformedHtml,
7033
10038
  toc: pageResult.toc,
7034
10039
  lastUpdated: pageResult.lastUpdated,
10040
+ contributors: pageResult.contributors,
7035
10041
  frontmatter,
7036
10042
  path: pageResult.routePaths.urlPath,
7037
10043
  href: pageResult.routePaths.href,
@@ -7131,6 +10137,17 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
7131
10137
  redirects: snapContext.options.redirects?.map
7132
10138
  });
7133
10139
  const snapPages = await generateHtmlPages(snapContext, outputPages, snapCollected, errors);
10140
+ await appendSectionIndexPages({
10141
+ generatedPages: snapPages,
10142
+ collectedPages: snapCollected.pageResults,
10143
+ listedPages,
10144
+ options: snapContext.ssgOptions.sectionIndex,
10145
+ outDir: snapContext.outDir,
10146
+ base: snapContext.base,
10147
+ extension: snapContext.ssgOptions.extension,
10148
+ errors,
10149
+ render: (page) => renderSsgPage(snapContext, toSectionIndexProcessResult(page), snapCollected, listedPages)
10150
+ });
7134
10151
  generatedPages.push(...snapPages);
7135
10152
  if (context.options.search?.enabled) try {
7136
10153
  await writeSnapshotSearchIndex({
@@ -7157,6 +10174,21 @@ function pageAliases(frontmatter) {
7157
10174
  async function writeGeneratedPages(generatedPages, context, generatedFiles, listedPages, outputPages, errors) {
7158
10175
  const optimizedOutput = await externalizeSharedPageAssets(generatedPages, context.outDir, context.base);
7159
10176
  generatedFiles.push(...optimizedOutput.assets);
10177
+ const pwa = await writePwaFiles({
10178
+ outDir: context.outDir,
10179
+ siteUrl: context.ssgOptions.siteUrl,
10180
+ base: context.base,
10181
+ siteName: context.siteName,
10182
+ options: context.options.pwa
10183
+ });
10184
+ generatedFiles.push(...pwa.files);
10185
+ if (pwa.warning) {
10186
+ errors.push(pwa.warning);
10187
+ console.warn(pwa.warning);
10188
+ } else if (!context.ssgOptions.bare && context.options.pwa?.enabled) for (const page of optimizedOutput.pages) page.html = injectPwaPageTags(page.html, {
10189
+ options: context.options.pwa,
10190
+ base: context.base
10191
+ });
7160
10192
  for (const page of optimizedOutput.pages) {
7161
10193
  await fs_promises.mkdir(path.dirname(page.outputPath), { recursive: true });
7162
10194
  await fs_promises.writeFile(page.outputPath, page.html, "utf-8");
@@ -7419,7 +10451,7 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
7419
10451
  let html = await generateHtmlPage(pageData, localizedNav, siteName, base, options.ssg.ogImage, theme, locale, i18n ? i18n.locales : void 0, options.ssg.pagination, options.ssg.readerChrome, options.ssg.breadcrumbs, options.ssg.localeSwitcher, localePaths, options.ssg.a11y, options.ssg.team ?? {
7420
10452
  enabled: false,
7421
10453
  members: []
7422
- }, options.ssg.pageChrome);
10454
+ }, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl);
7423
10455
  html = injectViteHmrClient(html);
7424
10456
  return html;
7425
10457
  }
@@ -7951,13 +10983,6 @@ function resolveCardOptions(options) {
7951
10983
  return { enabled: options.enabled ?? true };
7952
10984
  }
7953
10985
  //#endregion
7954
- //#region src/file-tree-options.ts
7955
- function resolveFileTreeOptions(options) {
7956
- if (!options) return { enabled: false };
7957
- if (options === true) return { enabled: true };
7958
- return { enabled: options.enabled ?? true };
7959
- }
7960
- //#endregion
7961
10986
  //#region src/include-options.ts
7962
10987
  function resolveIncludeOptions(options) {
7963
10988
  if (!options) return { enabled: false };
@@ -8078,6 +11103,353 @@ async function* renderMarkdownStream(chunks, options = {}) {
8078
11103
  yield renderer.finish();
8079
11104
  }
8080
11105
  //#endregion
11106
+ //#region src/mdx-islands.ts
11107
+ /**
11108
+ * Discover registered MDX islands from the mdast tree or rendered HTML.
11109
+ *
11110
+ * Framework plugins use this instead of a source regex when MDX is on, so
11111
+ * nested JSX, expression attributes, and fragments stay visible. Names that
11112
+ * are not in the global `components` map and are not document-local import
11113
+ * bindings are left as static HTML.
11114
+ */
11115
+ const OX_ISLAND_NAME = /data-ox-island="([^"]+)"/g;
11116
+ /**
11117
+ * Collect named MDX JSX tags from a parsed mdast tree (JSON from NAPI `parse()`).
11118
+ * Fragments (`name: null`) and non-JSX nodes are ignored. Walks nested children
11119
+ * so inner islands are found.
11120
+ */
11121
+ function collectMdxJsxNamesFromAst(ast) {
11122
+ const names = /* @__PURE__ */ new Set();
11123
+ walkMdast(ast, names);
11124
+ return [...names];
11125
+ }
11126
+ /**
11127
+ * Collect `data-ox-island` names from Rust-rendered HTML.
11128
+ * Used when an AST walk is unavailable.
11129
+ */
11130
+ function collectMdxIslandNamesFromHtml(html) {
11131
+ const names = /* @__PURE__ */ new Set();
11132
+ OX_ISLAND_NAME.lastIndex = 0;
11133
+ let match;
11134
+ while ((match = OX_ISLAND_NAME.exec(html)) !== null) {
11135
+ const name = match[1];
11136
+ if (name) names.add(decodeHtmlAttr$1(name));
11137
+ }
11138
+ return [...names];
11139
+ }
11140
+ /** Keep names that exist on the global component map, in first-seen order. */
11141
+ function intersectRegisteredComponentNames(names, components) {
11142
+ return intersectHydratableComponentNames(names, components);
11143
+ }
11144
+ /**
11145
+ * Keep names that are either globally registered or document-local bindings.
11146
+ */
11147
+ function intersectHydratableComponentNames(names, components, localNames) {
11148
+ const local = localNames ? new Set(localNames) : null;
11149
+ const used = [];
11150
+ for (const name of names) if ((local?.has(name) || isRegisteredComponent(name, components)) && !used.includes(name)) used.push(name);
11151
+ return used;
11152
+ }
11153
+ /**
11154
+ * Resolve registered island names for an MDX document.
11155
+ *
11156
+ * Prefers a NAPI `parse()` AST walk. Falls back to rendered `data-ox-island`
11157
+ * names so plugins still hydrate if #659 metadata is not present.
11158
+ */
11159
+ async function discoverRegisteredMdxComponents(input) {
11160
+ return intersectHydratableComponentNames(await tryCollectNamesFromParse(input.source) ?? (input.html !== void 0 ? collectMdxIslandNamesFromHtml(input.html) : []), input.components, input.localNames);
11161
+ }
11162
+ function isRegisteredComponent(name, components) {
11163
+ if (isMapRegistry$1(components)) return components.has(name);
11164
+ if (isPlainObjectRegistry(components)) return Object.prototype.hasOwnProperty.call(components, name);
11165
+ for (const entry of components) if (entry === name) return true;
11166
+ return false;
11167
+ }
11168
+ async function tryCollectNamesFromParse(source) {
11169
+ try {
11170
+ const parsed = (await require_vitepress.importNapiModule()).parse(source, {
11171
+ mdx: true,
11172
+ gfm: true
11173
+ });
11174
+ if (!parsed.ast) return null;
11175
+ return collectMdxJsxNamesFromAst(JSON.parse(parsed.ast));
11176
+ } catch {
11177
+ return null;
11178
+ }
11179
+ }
11180
+ function walkMdast(node, names) {
11181
+ if (!node || typeof node !== "object") return;
11182
+ const record = node;
11183
+ if ((record.type === "mdxJsxFlowElement" || record.type === "mdxJsxTextElement") && typeof record.name === "string" && record.name) names.add(record.name);
11184
+ if (Array.isArray(record.children)) for (const child of record.children) walkMdast(child, names);
11185
+ }
11186
+ function isMapRegistry$1(value) {
11187
+ return typeof value === "object" && value !== null && typeof value.has === "function" && typeof value.get === "function";
11188
+ }
11189
+ function isPlainObjectRegistry(value) {
11190
+ return Object.prototype.toString.call(value) === "[object Object]";
11191
+ }
11192
+ function decodeHtmlAttr$1(value) {
11193
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
11194
+ }
11195
+ //#endregion
11196
+ //#region src/document-imports.ts
11197
+ /**
11198
+ * Resolve MDX component imports relative to the document that declared them.
11199
+ *
11200
+ * Only `./` and `../` specifiers become island bindings. Bare, package, and
11201
+ * remote specifiers are reported and ignored. A specifier that leaves the
11202
+ * configured content root is rejected with a diagnostic.
11203
+ */
11204
+ function resolveContentRootPath(input) {
11205
+ if (input.contentRoot) return node_path.default.resolve(input.contentRoot);
11206
+ const root = input.root ?? process.cwd();
11207
+ return node_path.default.resolve(root, input.srcDir ?? ".");
11208
+ }
11209
+ function stripViteQuery(id) {
11210
+ return id.split("?")[0].split("#")[0];
11211
+ }
11212
+ function resolveDocumentComponentImports(input) {
11213
+ const documentPath = stripViteQuery(input.documentPath);
11214
+ const documentDir = node_path.default.dirname(documentPath);
11215
+ const contentRoot = resolveContentRootPath(input);
11216
+ const diagnostics = [];
11217
+ const candidates = [];
11218
+ for (const statement of input.imports) {
11219
+ const specifier = statement.source;
11220
+ if (!isRelativeSpecifier(specifier)) {
11221
+ diagnostics.push({
11222
+ code: "not-relative",
11223
+ message: `Document component import "${specifier}" is not relative and was ignored.`,
11224
+ specifier
11225
+ });
11226
+ continue;
11227
+ }
11228
+ for (const spec of statement.specifiers) {
11229
+ if (spec.kind === "namespace") continue;
11230
+ const resolvedPath = resolveExistingPath(node_path.default.resolve(documentDir, specifier));
11231
+ if (!isInsideRoot(resolvedPath, contentRoot)) {
11232
+ diagnostics.push({
11233
+ code: "escapes-root",
11234
+ message: `Document component import "${specifier}" escapes the content root.`,
11235
+ specifier,
11236
+ localName: spec.local
11237
+ });
11238
+ continue;
11239
+ }
11240
+ candidates.push({
11241
+ localName: spec.local,
11242
+ specifier,
11243
+ resolvedPath,
11244
+ importPathRelativeToDocument: toDocumentRelativeImport(documentDir, resolvedPath),
11245
+ imported: spec.imported,
11246
+ kind: spec.kind
11247
+ });
11248
+ }
11249
+ }
11250
+ const counts = /* @__PURE__ */ new Map();
11251
+ for (const binding of candidates) counts.set(binding.localName, (counts.get(binding.localName) ?? 0) + 1);
11252
+ const bindings = [];
11253
+ const reportedDuplicates = /* @__PURE__ */ new Set();
11254
+ for (const binding of candidates) {
11255
+ if ((counts.get(binding.localName) ?? 0) > 1) {
11256
+ if (!reportedDuplicates.has(binding.localName)) {
11257
+ reportedDuplicates.add(binding.localName);
11258
+ diagnostics.push({
11259
+ code: "duplicate-binding",
11260
+ message: `Document component name "${binding.localName}" is imported more than once.`,
11261
+ specifier: binding.specifier,
11262
+ localName: binding.localName
11263
+ });
11264
+ }
11265
+ continue;
11266
+ }
11267
+ bindings.push(binding);
11268
+ }
11269
+ return {
11270
+ bindings,
11271
+ diagnostics
11272
+ };
11273
+ }
11274
+ function isRelativeSpecifier(source) {
11275
+ return source.startsWith("./") || source.startsWith("../");
11276
+ }
11277
+ function resolveExistingPath(filePath) {
11278
+ try {
11279
+ return node_fs.default.realpathSync(filePath);
11280
+ } catch {
11281
+ return node_path.default.normalize(filePath);
11282
+ }
11283
+ }
11284
+ function isInsideRoot(resolvedPath, root) {
11285
+ const relative = node_path.default.relative(resolveExistingPath(root), resolvedPath);
11286
+ return relative === "" || !relative.startsWith(`..${node_path.default.sep}`) && relative !== ".." && !node_path.default.isAbsolute(relative);
11287
+ }
11288
+ function toDocumentRelativeImport(documentDir, resolvedPath) {
11289
+ const relative = node_path.default.relative(documentDir, resolvedPath).replace(/\\/g, "/");
11290
+ return relative.startsWith(".") ? relative : `./${relative}`;
11291
+ }
11292
+ //#endregion
11293
+ //#region src/document-islands.ts
11294
+ /**
11295
+ * Combine document-local import resolution with MDX island discovery.
11296
+ */
11297
+ async function discoverDocumentMdxIslands(input) {
11298
+ const resolved = resolveDocumentComponentImports({
11299
+ imports: input.imports,
11300
+ documentPath: input.documentPath,
11301
+ contentRoot: input.contentRoot ?? resolveContentRootPath(input),
11302
+ srcDir: input.srcDir
11303
+ });
11304
+ const localBindings = new Map(resolved.bindings.map((binding) => [binding.localName, binding]));
11305
+ return {
11306
+ usedComponents: await discoverRegisteredMdxComponents({
11307
+ source: input.source,
11308
+ html: input.html,
11309
+ components: input.components,
11310
+ localNames: localBindings.keys()
11311
+ }),
11312
+ localBindings,
11313
+ diagnostics: resolved.diagnostics
11314
+ };
11315
+ }
11316
+ //#endregion
11317
+ //#region src/island-codegen.ts
11318
+ /**
11319
+ * Emit static component imports for framework Markdown modules.
11320
+ *
11321
+ * Document-local bindings win over the global `components` map for that file
11322
+ * only. Two documents that bind the same local name therefore emit different
11323
+ * specifiers and do not share one module id.
11324
+ */
11325
+ function renderIslandComponentImports(usedComponents, input) {
11326
+ const documentDir = node_path.default.dirname(stripViteQuery(input.documentPath));
11327
+ const root = input.root || process.cwd();
11328
+ return usedComponents.map((name) => {
11329
+ const local = input.localBindings?.get(name);
11330
+ if (local) return renderLocalImport(local);
11331
+ const componentPath = getGlobalComponentPath(input.globalComponents, name);
11332
+ if (!componentPath) return "";
11333
+ return renderGlobalImport(name, componentPath, documentDir, root);
11334
+ }).filter(Boolean).join("\n");
11335
+ }
11336
+ function renderLocalImport(binding) {
11337
+ const specifier = binding.importPathRelativeToDocument.replace(/\\/g, "/");
11338
+ if (binding.kind === "default") return `import ${binding.localName} from '${specifier}';`;
11339
+ if (binding.imported === binding.localName) return `import { ${binding.imported} } from '${specifier}';`;
11340
+ return `import { ${binding.imported} as ${binding.localName} } from '${specifier}';`;
11341
+ }
11342
+ function renderGlobalImport(name, componentPath, documentDir, root) {
11343
+ const absolutePath = node_path.default.resolve(root, componentPath.replace(/^\.\//, ""));
11344
+ const relativePath = node_path.default.relative(documentDir, absolutePath).replace(/\\/g, "/");
11345
+ return `import ${name} from '${relativePath.startsWith(".") ? relativePath : `./${relativePath}`}';`;
11346
+ }
11347
+ function getGlobalComponentPath(components, name) {
11348
+ if (isMapRegistry(components)) return components.get(name);
11349
+ return Object.prototype.hasOwnProperty.call(components, name) ? components[name] : void 0;
11350
+ }
11351
+ function isMapRegistry(value) {
11352
+ return typeof value === "object" && value !== null && typeof value.has === "function" && typeof value.get === "function";
11353
+ }
11354
+ //#endregion
11355
+ //#region src/island-ssr.ts
11356
+ const RUST_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
11357
+ "props",
11358
+ "expressions",
11359
+ "spreads"
11360
+ ]);
11361
+ const PAYLOAD_SCRIPT = /^\s*<script type="application\/json">[\s\S]*?<\/script>/;
11362
+ async function applyIslandSsrHtml(html, renderIsland, filePath, names) {
11363
+ const allowed = names ? new Set(names) : null;
11364
+ const islands = findIslandRanges(html);
11365
+ let output = html;
11366
+ for (const island of islands.toReversed()) {
11367
+ if (allowed && !allowed.has(island.name)) continue;
11368
+ const script = output.slice(island.innerStart, island.closeStart).match(PAYLOAD_SCRIPT)?.[0] ?? "";
11369
+ const props = parseIslandProps(island.propsAttr, script);
11370
+ const ssrHtml = await renderIsland(island.name, props, filePath);
11371
+ output = output.slice(0, island.innerStart) + script + ssrHtml + output.slice(island.closeStart);
11372
+ }
11373
+ return output;
11374
+ }
11375
+ function findIslandRanges(html) {
11376
+ const ranges = [];
11377
+ const openRe = /<(div|span)\b([^>]*\bdata-ox-island="([^"]+)"[^>]*)>/gi;
11378
+ let match;
11379
+ while ((match = openRe.exec(html)) !== null) {
11380
+ const tag = match[1];
11381
+ const name = decodeHtmlAttr(match[3] ?? "");
11382
+ if (!tag || !name) continue;
11383
+ const innerStart = match.index + match[0].length;
11384
+ const closeStart = findMatchingClose(html, innerStart, tag);
11385
+ ranges.push({
11386
+ name,
11387
+ innerStart,
11388
+ closeStart,
11389
+ propsAttr: matchAttr(match[2] ?? "", "data-ox-props")
11390
+ });
11391
+ }
11392
+ return ranges;
11393
+ }
11394
+ function findMatchingClose(html, from, tag) {
11395
+ const openNeedle = `<${tag}`;
11396
+ const closeNeedle = `</${tag}>`;
11397
+ let depth = 1;
11398
+ let cursor = from;
11399
+ while (cursor < html.length) {
11400
+ const nextOpen = indexOfTagOpen(html, openNeedle, cursor);
11401
+ const nextClose = html.indexOf(closeNeedle, cursor);
11402
+ if (nextClose === -1) return html.length;
11403
+ if (nextOpen !== -1 && nextOpen < nextClose) {
11404
+ depth += 1;
11405
+ cursor = nextOpen + openNeedle.length;
11406
+ } else {
11407
+ depth -= 1;
11408
+ if (depth === 0) return nextClose;
11409
+ cursor = nextClose + closeNeedle.length;
11410
+ }
11411
+ }
11412
+ return html.length;
11413
+ }
11414
+ function indexOfTagOpen(html, openNeedle, from) {
11415
+ let cursor = from;
11416
+ while (cursor < html.length) {
11417
+ const index = html.indexOf(openNeedle, cursor);
11418
+ if (index === -1) return -1;
11419
+ const next = html[index + openNeedle.length];
11420
+ if (next === " " || next === ">" || next === " " || next === "\n" || next === "/") return index;
11421
+ cursor = index + openNeedle.length;
11422
+ }
11423
+ return -1;
11424
+ }
11425
+ function matchAttr(attrs, name) {
11426
+ const match = new RegExp(`\\b${name}="([^"]*)"`, "i").exec(attrs);
11427
+ return match?.[1] === void 0 ? void 0 : decodeHtmlAttr(match[1]);
11428
+ }
11429
+ function parseIslandProps(propsAttr, script) {
11430
+ const fromAttr = propsAttr ? tryParseJson(propsAttr) : void 0;
11431
+ if (fromAttr) return unwrapIslandProps(fromAttr);
11432
+ const scriptBody = script.match(/<script type="application\/json">([\s\S]*?)<\/script>/i)?.[1];
11433
+ return scriptBody ? unwrapIslandProps(tryParseJson(scriptBody) ?? {}) : {};
11434
+ }
11435
+ function tryParseJson(value) {
11436
+ try {
11437
+ return JSON.parse(value);
11438
+ } catch {
11439
+ return;
11440
+ }
11441
+ }
11442
+ function unwrapIslandProps(parsed) {
11443
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
11444
+ const record = parsed;
11445
+ const keys = Object.keys(record);
11446
+ if (keys.length > 0 && keys.every((key) => RUST_PAYLOAD_KEYS.has(key)) && record.props && typeof record.props === "object" && !Array.isArray(record.props)) return record.props;
11447
+ return record;
11448
+ }
11449
+ function decodeHtmlAttr(value) {
11450
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
11451
+ }
11452
+ //#endregion
8081
11453
  //#region src/framework.ts
8082
11454
  function createFrameworkMarkdownOptions(options) {
8083
11455
  return {
@@ -8094,6 +11466,7 @@ function createFrameworkMarkdownOptions(options) {
8094
11466
  lastUpdated: false,
8095
11467
  pagination: false,
8096
11468
  breadcrumbs: false,
11469
+ jsonLd: false,
8097
11470
  readerChrome: false,
8098
11471
  localeSwitcher: false,
8099
11472
  a11y: false,
@@ -8104,6 +11477,10 @@ function createFrameworkMarkdownOptions(options) {
8104
11477
  robots: true,
8105
11478
  llms: true
8106
11479
  },
11480
+ pwa: {
11481
+ enabled: false,
11482
+ offline: true
11483
+ },
8107
11484
  publishState: {
8108
11485
  enabled: false,
8109
11486
  includeDrafts: false
@@ -8119,6 +11496,7 @@ function createFrameworkMarkdownOptions(options) {
8119
11496
  allowExternal: false
8120
11497
  },
8121
11498
  gfm: options.gfm,
11499
+ mdx: options.mdx,
8122
11500
  frontmatter: options.frontmatter ?? false,
8123
11501
  toc: options.toc,
8124
11502
  tocMaxDepth: options.tocMaxDepth,
@@ -8191,7 +11569,11 @@ function createFrameworkMarkdownOptions(options) {
8191
11569
  includes: { enabled: false },
8192
11570
  cards: { enabled: false },
8193
11571
  steps: { enabled: false },
8194
- fileTree: { enabled: false },
11572
+ fileTree: {
11573
+ enabled: false,
11574
+ defaultOpen: true,
11575
+ icons: true
11576
+ },
8195
11577
  sanitize: { enabled: false },
8196
11578
  editThisPage: {
8197
11579
  enabled: false,
@@ -9026,6 +12408,7 @@ function oxContent(options = {}) {
9026
12408
  createCollectionsPlugin(resolvedOptions, getRoot),
9027
12409
  createSearchPlugin(resolvedOptions, getRoot)
9028
12410
  ];
12411
+ if (resolvedOptions.math.enabled) plugins.push(createKatexAssetsPlugin());
9029
12412
  if (resolvedOptions.i18n) plugins.push(createI18nPlugin(resolvedOptions));
9030
12413
  if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
9031
12414
  return plugins;
@@ -9173,6 +12556,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
9173
12556
  for (const error of result.errors) console.warn(`[ox-content] ${error}`);
9174
12557
  } catch (err) {
9175
12558
  console.error("[ox-content] SSG build failed:", err);
12559
+ if (err instanceof PageResourceError) throw err;
9176
12560
  }
9177
12561
  }
9178
12562
  };
@@ -9278,9 +12662,12 @@ function resolveOptions(options) {
9278
12662
  permalinks: resolvePermalinksOptions(options.permalinks),
9279
12663
  cascade: resolveCascadeOptions(options.cascade),
9280
12664
  redirects: resolveRedirectsOptions(options.redirects),
12665
+ blog: resolveBlogOptions(options.blog ?? (typeof options.ssg === "object" && options.ssg ? options.ssg.blog : void 0)),
9281
12666
  feeds: resolveFeedsOptions(options.feeds),
12667
+ pwa: resolvePwaOptions(options.pwa),
9282
12668
  taxonomies: resolveTaxonomiesOptions(options.taxonomies),
9283
12669
  versions: resolveVersionsOptions(options.versions),
12670
+ resources: resolveResourcesOptions(options.resources),
9284
12671
  gfm: options.gfm ?? true,
9285
12672
  mdx: options.mdx,
9286
12673
  footnotes: options.footnotes ?? true,
@@ -9306,6 +12693,7 @@ function resolveOptions(options) {
9306
12693
  cjkEmphasis: options.cjkEmphasis ?? false,
9307
12694
  codeBlockLint: resolveCodeBlockLintOptions(options.codeBlockLint),
9308
12695
  codeBlockTypecheck: resolveCodeBlockTypecheckOptions(options.codeBlockTypecheck),
12696
+ typedHover: resolveTypedHoverOptions(options.typedHover),
9309
12697
  docsTests: resolveDocsTestOptions(options.docsTests),
9310
12698
  mermaid: options.mermaid ?? false,
9311
12699
  math: resolveMathOptions(options.math),
@@ -9553,9 +12941,9 @@ function resolveCodeAnnotationsOptions(options) {
9553
12941
  /**
9554
12942
  * Generates virtual module content.
9555
12943
  */
9556
- function generateVirtualModule(path$3, options) {
9557
- if (path$3 === "config") return `export default ${JSON.stringify(options)};`;
9558
- if (path$3 === "runtime") {
12944
+ function generateVirtualModule(path$5, options) {
12945
+ if (path$5 === "config") return `export default ${JSON.stringify(options)};`;
12946
+ if (path$5 === "runtime") {
9559
12947
  const base = normalizeRuntimeBase(options.base);
9560
12948
  return `
9561
12949
  export const base = ${JSON.stringify(base)};
@@ -9609,6 +12997,8 @@ exports.DocsTestRunError = DocsTestRunError;
9609
12997
  exports.Fragment = require_jsx_html.Fragment;
9610
12998
  exports.IncrementalMarkdownParser = IncrementalMarkdownParser;
9611
12999
  exports.IncrementalMarkdownRenderer = IncrementalMarkdownRenderer;
13000
+ exports.PageResourceError = PageResourceError;
13001
+ exports.applyIslandSsrHtml = applyIslandSsrHtml;
9612
13002
  exports.buildCollectionManifest = buildCollectionManifest;
9613
13003
  exports.buildSearchIndex = buildSearchIndex;
9614
13004
  exports.buildSsg = buildSsg;
@@ -9617,6 +13007,8 @@ exports.clearRenderContext = clearRenderContext;
9617
13007
  exports.collectDocsTests = collectDocsTests;
9618
13008
  exports.collectGitHubRepos = collectGitHubRepos;
9619
13009
  exports.collectGitHubSources = collectGitHubSources;
13010
+ exports.collectMdxIslandNamesFromHtml = collectMdxIslandNamesFromHtml;
13011
+ exports.collectMdxJsxNamesFromAst = collectMdxJsxNamesFromAst;
9620
13012
  exports.collectOgpUrls = collectOgpUrls;
9621
13013
  exports.convertVitePressNav = require_vitepress.convertVitePressNav;
9622
13014
  exports.convertVitePressSidebar = require_vitepress.convertVitePressSidebar;
@@ -9630,6 +13022,8 @@ exports.defaultTheme = require_vitepress.defaultTheme;
9630
13022
  exports.defineCollection = defineCollection;
9631
13023
  exports.defineCollections = defineCollections;
9632
13024
  exports.defineTheme = require_vitepress.defineTheme;
13025
+ exports.discoverDocumentMdxIslands = discoverDocumentMdxIslands;
13026
+ exports.discoverRegisteredMdxComponents = discoverRegisteredMdxComponents;
9633
13027
  exports.each = require_jsx_html.each;
9634
13028
  exports.escapeSvelteMarkup = escapeSvelteMarkup;
9635
13029
  exports.extractCodeBlocks = extractCodeBlocks;
@@ -9652,7 +13046,11 @@ exports.generateVirtualModule = generateVirtualModule;
9652
13046
  exports.generateVitePressMigrationConfig = require_vitepress.generateVitePressMigrationConfig;
9653
13047
  exports.hasIslands = hasIslands;
9654
13048
  exports.inferType = inferType;
13049
+ exports.intersectHydratableComponentNames = intersectHydratableComponentNames;
13050
+ exports.intersectRegisteredComponentNames = intersectRegisteredComponentNames;
9655
13051
  exports.isMarkdownFilePath = isMarkdownFilePath;
13052
+ exports.isMdxFilePath = isMdxFilePath;
13053
+ exports.isRegisteredComponent = isRegisteredComponent;
9656
13054
  exports.jsx = require_jsx_html.jsx;
9657
13055
  exports.jsxs = require_jsx_html.jsxs;
9658
13056
  exports.lintCodeBlocks = lintCodeBlocks;
@@ -9673,6 +13071,7 @@ exports.prefetchGitHubRepos = prefetchGitHubRepos;
9673
13071
  exports.prefetchGitHubSources = prefetchGitHubSources;
9674
13072
  exports.prefetchOgpData = prefetchOgpData;
9675
13073
  exports.raw = require_jsx_html.raw;
13074
+ exports.readingTimeMinutes = readingTimeMinutes;
9676
13075
  exports.renderAllPages = renderAllPages;
9677
13076
  exports.renderHtmlToFrameworkCode = renderHtmlToFrameworkCode;
9678
13077
  exports.renderHtmlToReactComponent = renderHtmlToReactComponent;
@@ -9680,15 +13079,20 @@ exports.renderHtmlToReactCreateElement = renderHtmlToReactCreateElement;
9680
13079
  exports.renderHtmlToSvelteComponent = renderHtmlToSvelteComponent;
9681
13080
  exports.renderHtmlToVueComponent = renderHtmlToVueComponent;
9682
13081
  exports.renderHtmlToVueH = renderHtmlToVueH;
13082
+ exports.renderIslandComponentImports = renderIslandComponentImports;
9683
13083
  exports.renderMarkdownStream = renderMarkdownStream;
9684
13084
  exports.renderPage = renderPage;
9685
13085
  exports.renderToString = require_jsx_html.renderToString;
9686
13086
  exports.resolveBadgeOptions = resolveBadgeOptions;
13087
+ exports.resolveBlogCollectionName = resolveBlogCollectionName;
13088
+ exports.resolveBlogOptions = resolveBlogOptions;
9687
13089
  exports.resolveBuiltinEmbedOptions = resolveBuiltinEmbedOptions;
9688
13090
  exports.resolveCardOptions = resolveCardOptions;
9689
13091
  exports.resolveCascadeOptions = resolveCascadeOptions;
9690
13092
  exports.resolveCollectionsOptions = resolveCollectionsOptions;
13093
+ exports.resolveContentRootPath = resolveContentRootPath;
9691
13094
  exports.resolveDocsOptions = resolveDocsOptions;
13095
+ exports.resolveDocumentComponentImports = resolveDocumentComponentImports;
9692
13096
  exports.resolveFeedsOptions = resolveFeedsOptions;
9693
13097
  exports.resolveFileTreeOptions = resolveFileTreeOptions;
9694
13098
  exports.resolveHeaderNavItems = require_vitepress.resolveHeaderNavItems;
@@ -9697,24 +13101,30 @@ exports.resolveImageOptions = resolveImageOptions;
9697
13101
  exports.resolveIncludeOptions = resolveIncludeOptions;
9698
13102
  exports.resolveLocaleLabel = require_vitepress.resolveLocaleLabel;
9699
13103
  exports.resolveMathOptions = resolveMathOptions;
13104
+ exports.resolveMdxForFilePath = resolveMdxForFilePath;
9700
13105
  exports.resolveNotFoundOptions = resolveNotFoundOptions;
9701
13106
  exports.resolveOgImageOptions = resolveOgImageOptions;
9702
13107
  exports.resolvePageChromeOption = require_vitepress.resolvePageChromeOption;
9703
13108
  exports.resolvePermalinksOptions = resolvePermalinksOptions;
9704
13109
  exports.resolvePublishStateOptions = resolvePublishStateOptions;
13110
+ exports.resolvePwaOptions = resolvePwaOptions;
9705
13111
  exports.resolveRedirectsOptions = resolveRedirectsOptions;
13112
+ exports.resolveResourcesOptions = resolveResourcesOptions;
9706
13113
  exports.resolveSearchOptions = resolveSearchOptions;
13114
+ exports.resolveSectionIndexOptions = resolveSectionIndexOptions;
9707
13115
  exports.resolveSiteMapsOptions = resolveSiteMapsOptions;
9708
13116
  exports.resolveSsgOptions = resolveSsgOptions;
9709
13117
  exports.resolveStepsOptions = resolveStepsOptions;
9710
13118
  exports.resolveTaxonomiesOptions = resolveTaxonomiesOptions;
9711
13119
  exports.resolveTeamOptions = resolveTeamOptions;
9712
13120
  exports.resolveTheme = require_vitepress.resolveTheme;
13121
+ exports.resolveTypedHoverOptions = resolveTypedHoverOptions;
9713
13122
  exports.resolveVersionsOptions = resolveVersionsOptions;
9714
13123
  exports.runDocsTests = runDocsTests;
9715
13124
  exports.setRenderContext = setRenderContext;
9716
13125
  exports.shouldLintMarkdownFile = shouldLintMarkdownFile;
9717
13126
  exports.stripMarkdownExtension = stripMarkdownExtension;
13127
+ exports.stripViteQuery = stripViteQuery;
9718
13128
  exports.transformAllPlugins = transformAllPlugins;
9719
13129
  exports.transformGitHub = transformGitHub;
9720
13130
  exports.transformIslands = transformIslands;