@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.mjs CHANGED
@@ -7,18 +7,20 @@ import * as path$2 from "path";
7
7
  import { unified } from "unified";
8
8
  import rehypeParsePlugin from "rehype-parse";
9
9
  import rehypeStringifyPlugin from "rehype-stringify";
10
- import { existsSync, readFileSync } from "node:fs";
10
+ import fs, { createReadStream, existsSync, readFileSync } from "node:fs";
11
11
  import * as path$1 from "node:path";
12
- import path, { dirname, join } from "node:path";
13
- import * as fs$1 from "node:fs/promises";
14
- import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
12
+ import path, { dirname, extname, join, relative, resolve, sep } from "node:path";
13
+ import * as fs$2 from "node:fs/promises";
14
+ import { access, copyFile, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
15
15
  import { Buffer as Buffer$1 } from "node:buffer";
16
16
  import { tmpdir } from "node:os";
17
17
  import { promisify } from "node:util";
18
18
  import { execFile, spawn } from "node:child_process";
19
- import * as fs$2 from "fs/promises";
19
+ import * as fs$3 from "fs/promises";
20
20
  import * as crypto from "crypto";
21
- import * as fs from "fs";
21
+ import { createHash } from "node:crypto";
22
+ import { deflateSync, inflateSync } from "node:zlib";
23
+ import * as fs$1 from "fs";
22
24
  import { glob } from "glob";
23
25
  //#region src/markdown.ts
24
26
  const DEFAULT_MARKDOWN_EXTENSIONS = [
@@ -245,6 +247,15 @@ async function highlightCode(html) {
245
247
  const result = await unified().use(rehypeParse$3, { fragment: true }).use(rehypeNativeHighlight).use(rehypeStringify$3).process(html);
246
248
  return String(result);
247
249
  }
250
+ /**
251
+ * Highlight every code block in a rendered page, preserving original classes
252
+ * and per-line metadata when the native document pass cannot read the markup.
253
+ */
254
+ async function highlightPageHtml(html, mergeHighlightedCodeBlocks) {
255
+ const native = await highlightDocumentNatively(html);
256
+ if (native && native.skipped.length === 0) return native.html;
257
+ return mergeHighlightedCodeBlocks(html, await highlightCode(html));
258
+ }
248
259
  //#endregion
249
260
  //#region src/plugins/mermaid.ts
250
261
  /**
@@ -336,6 +347,69 @@ function warnMissingMmdcOnce() {
336
347
  */
337
348
  const mermaidClientScript = "";
338
349
  //#endregion
350
+ //#region src/plugins/math.ts
351
+ /**
352
+ * Build-time KaTeX rendering for opt-in `$…$` / `$$…$$` math.
353
+ *
354
+ * KaTeX is an optional peer. Sites that never enable `math` do not install it,
355
+ * and the published plugin does not bundle or depend on it.
356
+ */
357
+ const KATEX_ASSET_DIR = "__ox_katex__";
358
+ const MATH_TAG = /<(span|div) class="ox-math ox-math-(inline|block)" data-ox-tex="([^"]*)">[\s\S]*?<\/\1>/g;
359
+ let missingWarned = false;
360
+ /**
361
+ * Replaces rust `ox-math` placeholders with static KaTeX HTML.
362
+ * Leaves the escaped TeX fallback when `katex` is not installed.
363
+ */
364
+ async function renderKatexMath(html) {
365
+ if (!html.includes("data-ox-tex")) return html;
366
+ const katex = loadKatex();
367
+ if (!katex) {
368
+ warnMissingKatexOnce();
369
+ return html;
370
+ }
371
+ return html.replace(MATH_TAG, (_match, tag, kind, encoded) => {
372
+ return `<${tag} class="ox-math ox-math-${kind}">${katex.renderToString(decodeHtmlAttr$2(encoded), {
373
+ displayMode: kind === "block",
374
+ throwOnError: false,
375
+ trust: false,
376
+ output: "htmlAndMathml"
377
+ })}</${tag}>`;
378
+ });
379
+ }
380
+ /** Directory that contains `katex.min.css` and `fonts/`, or `null`. */
381
+ function resolveKatexDist() {
382
+ for (const resolver of createKatexResolvers()) try {
383
+ return join(dirname(resolver.resolve("katex/package.json")), "dist");
384
+ } catch {}
385
+ return null;
386
+ }
387
+ function loadKatex() {
388
+ for (const resolver of createKatexResolvers()) try {
389
+ const loaded = resolver(resolver.resolve("katex"));
390
+ if (typeof loaded.renderToString === "function") return loaded;
391
+ if (loaded.default && typeof loaded.default.renderToString === "function") return loaded.default;
392
+ } catch {}
393
+ return null;
394
+ }
395
+ function createKatexResolvers() {
396
+ const consumerRequire = createRequire(join(process.cwd(), "noop.js"));
397
+ const resolvers = [consumerRequire];
398
+ try {
399
+ resolvers.push(createRequire(consumerRequire.resolve("@ox-content/vite-plugin")));
400
+ } catch {}
401
+ resolvers.push(createRequire(import.meta.url));
402
+ return resolvers;
403
+ }
404
+ function decodeHtmlAttr$2(value) {
405
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
406
+ }
407
+ function warnMissingKatexOnce() {
408
+ if (missingWarned) return;
409
+ missingWarned = true;
410
+ 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.");
411
+ }
412
+ //#endregion
339
413
  //#region src/plugins/pm.ts
340
414
  /**
341
415
  * Package Manager Tabs Plugin
@@ -553,15 +627,15 @@ function assetRecord(src, media) {
553
627
  //#region src/plugins/twitter/render.ts
554
628
  function renderFetchedTweet(permalink, data, assets, options) {
555
629
  const profile = `https://x.com/${encodeURIComponent(data.user.screen_name)}`;
556
- const author = escapeHtml$5(data.user.name);
557
- const handle = escapeHtml$5(data.user.screen_name);
558
- const avatar = assets.avatar ? `<img class="ox-tweet__avatar" src="${escapeAttribute(assets.avatar)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
630
+ const author = escapeHtml$6(data.user.name);
631
+ const handle = escapeHtml$6(data.user.screen_name);
632
+ const avatar = assets.avatar ? `<img class="ox-tweet__avatar" src="${escapeAttribute$2(assets.avatar)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
559
633
  const media = renderMedia(assets);
560
634
  const footer = renderFooter(permalink, data.created_at, options.lang);
561
635
  return [
562
636
  "<figure class=\"ox-tweet ox-tweet--fetched\">",
563
637
  "<header class=\"ox-tweet__header\">",
564
- `<a class="ox-tweet__profile" href="${escapeAttribute(profile)}" target="_blank" rel="noopener noreferrer">`,
638
+ `<a class="ox-tweet__profile" href="${escapeAttribute$2(profile)}" target="_blank" rel="noopener noreferrer">`,
565
639
  avatar,
566
640
  `<span class="ox-tweet__author-name">${author}</span>`,
567
641
  `<span class="ox-tweet__author-handle">@${handle}</span>`,
@@ -584,7 +658,7 @@ function renderTweetText(data) {
584
658
  if (entity.kind === "url") {
585
659
  const href = entity.expanded_url ?? entity.url;
586
660
  const label = entity.display_url ?? href;
587
- output += `<a href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$5(label)}</a>`;
661
+ output += `<a href="${escapeAttribute$2(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
588
662
  }
589
663
  cursor = entityEnd;
590
664
  }
@@ -607,12 +681,12 @@ function renderMedia(assets) {
607
681
  if (assets.media.length === 0) return "";
608
682
  const images = assets.media.map((item) => {
609
683
  const size = [item.width ? ` width="${item.width}"` : "", item.height ? ` height="${item.height}"` : ""].join("");
610
- return `<img class="ox-tweet__media-item" src="${escapeAttribute(item.src)}" alt="${escapeAttribute(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
684
+ return `<img class="ox-tweet__media-item" src="${escapeAttribute$2(item.src)}" alt="${escapeAttribute$2(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
611
685
  }).join("");
612
686
  return `<div class="ox-tweet__media" data-count="${assets.media.length}">${images}</div>`;
613
687
  }
614
688
  function renderFooter(permalink, createdAt, lang) {
615
- 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>`;
689
+ 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>`;
616
690
  const date = new Date(createdAt);
617
691
  if (Number.isNaN(date.valueOf())) return renderFooter(permalink, void 0, lang);
618
692
  const iso = date.toISOString();
@@ -628,15 +702,15 @@ function renderFooter(permalink, createdAt, lang) {
628
702
  timeZone: "UTC"
629
703
  }).format(date);
630
704
  }
631
- 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>`;
705
+ 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>`;
632
706
  }
633
707
  function escapeText(value) {
634
- return escapeHtml$5(value).replaceAll("\n", "<br>");
708
+ return escapeHtml$6(value).replaceAll("\n", "<br>");
635
709
  }
636
- function escapeAttribute(value) {
637
- return escapeHtml$5(value).replaceAll("`", "&#96;");
710
+ function escapeAttribute$2(value) {
711
+ return escapeHtml$6(value).replaceAll("`", "&#96;");
638
712
  }
639
- function escapeHtml$5(value) {
713
+ function escapeHtml$6(value) {
640
714
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
641
715
  }
642
716
  //#endregion
@@ -757,6 +831,11 @@ function sourceKey(source) {
757
831
  function formatLineRange(lines) {
758
832
  return lines.start === lines.end ? `L${lines.start}` : `L${lines.start}-L${lines.end}`;
759
833
  }
834
+ function summarizeCommitMessage(message) {
835
+ const firstLine = message.split(/\r?\n/, 1)[0]?.replace(/\s+/g, " ").trim() ?? "";
836
+ if (firstLine.length <= 120) return firstLine;
837
+ return `${firstLine.slice(0, 119)}…`;
838
+ }
760
839
  function parseGitHubLineRange(value) {
761
840
  if (!value) return void 0;
762
841
  const match = value.trim().match(/^#?L?(\d+)(?:-L?(\d+))?$/i);
@@ -831,6 +910,24 @@ function githubHeaders(options) {
831
910
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
832
911
  return headers;
833
912
  }
913
+ async function fetchSourceCommit(source, options) {
914
+ try {
915
+ const apiUrl = `https://api.github.com/repos/${source.repo}/commits?path=${encodeURIComponent(source.path)}&sha=${encodeURIComponent(source.ref)}&per_page=1`;
916
+ const response = await fetch(apiUrl, { headers: githubHeaders(options) });
917
+ if (!response.ok) return;
918
+ const item = (await response.json())[0];
919
+ const sha = item?.sha;
920
+ const message = item?.commit?.message ? summarizeCommitMessage(item.commit.message) : "";
921
+ if (!sha || !message) return;
922
+ return {
923
+ sha,
924
+ message,
925
+ html_url: item.html_url ?? `https://github.com/${source.repo}/commit/${sha}`
926
+ };
927
+ } catch {
928
+ return;
929
+ }
930
+ }
834
931
  /**
835
932
  * Fetch repository data from GitHub API.
836
933
  */
@@ -869,7 +966,7 @@ async function fetchGitHubSource(source, options) {
869
966
  }
870
967
  try {
871
968
  const apiUrl = `https://api.github.com/repos/${source.repo}/contents/${encodePath(source.path)}?ref=${encodeURIComponent(source.ref)}`;
872
- const response = await fetch(apiUrl, { headers: githubHeaders(options) });
969
+ const [response, commit] = await Promise.all([fetch(apiUrl, { headers: githubHeaders(options) }), fetchSourceCommit(source, options)]);
873
970
  if (!response.ok) {
874
971
  console.warn(`Failed to fetch GitHub source ${source.permalink}: ${response.status}`);
875
972
  return null;
@@ -886,7 +983,8 @@ async function fetchGitHubSource(source, options) {
886
983
  content,
887
984
  size: data.size ?? Buffer$1.byteLength(content),
888
985
  html_url: data.html_url ?? source.permalink,
889
- language: inferLanguage(source.path)
986
+ language: inferLanguage(source.path),
987
+ ...commit ? { commit } : {}
890
988
  };
891
989
  if (options.cache) sourceCache.set(key, {
892
990
  data: sourceData,
@@ -1180,19 +1278,76 @@ function normalizeSourceLines(content) {
1180
1278
  if (lines.length > 1 && lines.at(-1) === "") lines.pop();
1181
1279
  return lines.length > 0 ? lines : [""];
1182
1280
  }
1281
+ function text(value) {
1282
+ return {
1283
+ type: "text",
1284
+ value
1285
+ };
1286
+ }
1287
+ function createSourceLines(lines, start) {
1288
+ return lines.flatMap((line, index) => {
1289
+ const lineNumber = start + index;
1290
+ const span = {
1291
+ type: "element",
1292
+ tagName: "span",
1293
+ properties: {
1294
+ className: ["line"],
1295
+ "data-line": String(lineNumber),
1296
+ "data-line-number": String(lineNumber)
1297
+ },
1298
+ children: [text(line)]
1299
+ };
1300
+ return index === 0 ? [span] : [text("\n"), span];
1301
+ });
1302
+ }
1303
+ function createCommitMeta(commit) {
1304
+ return {
1305
+ type: "element",
1306
+ tagName: "a",
1307
+ properties: {
1308
+ className: ["ox-github-code-commit"],
1309
+ href: commit.html_url,
1310
+ target: "_blank",
1311
+ rel: "noopener noreferrer",
1312
+ title: commit.message
1313
+ },
1314
+ children: [{
1315
+ type: "element",
1316
+ tagName: "span",
1317
+ properties: { className: ["ox-github-code-sha"] },
1318
+ children: [text(commit.sha.slice(0, 7))]
1319
+ }, {
1320
+ type: "element",
1321
+ tagName: "span",
1322
+ properties: { className: ["ox-github-code-commit-message"] },
1323
+ children: [text(commit.message)]
1324
+ }]
1325
+ };
1326
+ }
1183
1327
  function createGitHubSourceCard(source, lines, options) {
1184
1328
  const allLines = normalizeSourceLines(source.content);
1185
1329
  const start = Math.min(lines?.start ?? 1, allLines.length);
1186
1330
  const end = lines ? Math.min(lines.end, allLines.length) : Math.min(allLines.length, options.maxSourceLines);
1187
1331
  const selectedLines = allLines.slice(start - 1, end);
1188
- const lineRange = {
1332
+ const loc = selectedLines.length;
1333
+ const rangeLabel = formatLineRange({
1189
1334
  start,
1190
1335
  end
1191
- };
1192
- const loc = selectedLines.length;
1193
- const rangeLabel = formatLineRange(lineRange);
1194
- const locLabel = !lines && end < allLines.length ? `${rangeLabel} of ${allLines.length} LOC` : `${rangeLabel} - ${loc} LOC`;
1336
+ });
1337
+ const locLabel = !lines && end < allLines.length ? `${rangeLabel} of ${allLines.length} LOC` : `${rangeLabel} · ${loc} LOC`;
1195
1338
  const languageClass = source.language ? [`language-${source.language}`] : [];
1339
+ const heading = [{
1340
+ type: "element",
1341
+ tagName: "a",
1342
+ properties: {
1343
+ className: ["ox-github-code-title"],
1344
+ href: source.permalink,
1345
+ target: "_blank",
1346
+ rel: "noopener noreferrer"
1347
+ },
1348
+ children: [text(`${source.repo}/${source.path}`)]
1349
+ }];
1350
+ if (source.commit) heading.push(createCommitMeta(source.commit));
1196
1351
  return {
1197
1352
  type: "element",
1198
1353
  tagName: "figure",
@@ -1207,65 +1362,34 @@ function createGitHubSourceCard(source, lines, options) {
1207
1362
  properties: { className: ["ox-github-code-header"] },
1208
1363
  children: [{
1209
1364
  type: "element",
1210
- tagName: "a",
1211
- properties: {
1212
- className: ["ox-github-code-title"],
1213
- href: source.permalink,
1214
- target: "_blank",
1215
- rel: "noopener noreferrer"
1216
- },
1217
- children: [{
1218
- type: "text",
1219
- value: `${source.repo}/${source.path}`
1220
- }]
1365
+ tagName: "div",
1366
+ properties: { className: ["ox-github-code-heading"] },
1367
+ children: heading
1221
1368
  }, {
1222
1369
  type: "element",
1223
1370
  tagName: "span",
1224
1371
  properties: { className: ["ox-github-code-loc"] },
1225
- children: [{
1226
- type: "text",
1227
- value: locLabel
1228
- }]
1372
+ children: [text(locLabel)]
1229
1373
  }]
1230
1374
  }, {
1231
1375
  type: "element",
1232
1376
  tagName: "pre",
1233
1377
  properties: {
1234
- className: ["ox-github-code-block", ...languageClass],
1378
+ className: [
1379
+ "ox-github-code-block",
1380
+ "ox-code-block",
1381
+ "line-numbers-mode",
1382
+ ...languageClass
1383
+ ],
1384
+ "data-line-numbers": "true",
1385
+ "data-line-number-start": String(start),
1235
1386
  ...source.language ? { "data-language": source.language } : {}
1236
1387
  },
1237
1388
  children: [{
1238
1389
  type: "element",
1239
1390
  tagName: "code",
1240
1391
  properties: { className: languageClass },
1241
- children: selectedLines.map((line, index) => {
1242
- const lineNumber = start + index;
1243
- return {
1244
- type: "element",
1245
- tagName: "span",
1246
- properties: {
1247
- className: ["line", "ox-github-code-line"],
1248
- "data-line": String(lineNumber)
1249
- },
1250
- children: [{
1251
- type: "element",
1252
- tagName: "span",
1253
- properties: { className: ["ox-github-code-line-number"] },
1254
- children: [{
1255
- type: "text",
1256
- value: String(lineNumber)
1257
- }]
1258
- }, {
1259
- type: "element",
1260
- tagName: "span",
1261
- properties: { className: ["ox-github-code-line-content"] },
1262
- children: [{
1263
- type: "text",
1264
- value: line || " "
1265
- }]
1266
- }]
1267
- };
1268
- })
1392
+ children: createSourceLines(selectedLines, start)
1269
1393
  }]
1270
1394
  }]
1271
1395
  };
@@ -1864,6 +1988,368 @@ function normalizeDiagnostic(diagnostic) {
1864
1988
  };
1865
1989
  }
1866
1990
  //#endregion
1991
+ //#region src/typed-hover-generate.ts
1992
+ async function loadTsgoApi() {
1993
+ try {
1994
+ return await import("./api.mjs");
1995
+ } catch {
1996
+ return;
1997
+ }
1998
+ }
1999
+ async function generateTypedHoverAttachments(fences, tsgoCommand) {
2000
+ if (fences.length === 0) return [];
2001
+ const apiMod = await loadTsgoApi();
2002
+ if (!apiMod) return fences.map((fence) => ({
2003
+ code: fence.code,
2004
+ hovers: []
2005
+ }));
2006
+ const temp = await mkdtemp(join(tmpdir(), "ox-content-typed-hover-"));
2007
+ const api = new apiMod.API({
2008
+ cwd: temp,
2009
+ ...tsgoCommand ? { tsserverPath: tsgoCommand } : {}
2010
+ });
2011
+ try {
2012
+ const files = await Promise.all(fences.map(async (fence, index) => {
2013
+ const extension = fence.language.toLowerCase() === "tsx" ? "tsx" : "ts";
2014
+ const file = join(temp, `snippet-${index}.${extension}`);
2015
+ await writeFile(file, fence.code);
2016
+ return {
2017
+ fence,
2018
+ file
2019
+ };
2020
+ }));
2021
+ const snapshot = api.updateSnapshot({ openFiles: files.map((item) => item.file) });
2022
+ return files.map(({ fence, file }) => {
2023
+ const project = snapshot.getDefaultProjectForFile(file);
2024
+ if (!project) return {
2025
+ code: fence.code,
2026
+ hovers: []
2027
+ };
2028
+ const hovers = [];
2029
+ for (const ident of collectIdentifierRanges(fence.code)) {
2030
+ const type = project.checker.getTypeAtPosition(file, ident.start);
2031
+ if (!type || type.isErrorType?.()) continue;
2032
+ const widened = project.checker.getBaseTypeOfLiteralType(type) ?? type;
2033
+ const text = project.checker.typeToString(widened);
2034
+ if (text) hovers.push({
2035
+ start: ident.start,
2036
+ end: ident.end,
2037
+ type: text
2038
+ });
2039
+ }
2040
+ return {
2041
+ code: fence.code,
2042
+ hovers
2043
+ };
2044
+ });
2045
+ } finally {
2046
+ api.close();
2047
+ await rm(temp, {
2048
+ recursive: true,
2049
+ force: true
2050
+ });
2051
+ }
2052
+ }
2053
+ const IDENTIFIER_KEYWORDS = /* @__PURE__ */ new Set([
2054
+ "abstract",
2055
+ "any",
2056
+ "as",
2057
+ "asserts",
2058
+ "async",
2059
+ "await",
2060
+ "bigint",
2061
+ "boolean",
2062
+ "break",
2063
+ "case",
2064
+ "catch",
2065
+ "class",
2066
+ "const",
2067
+ "continue",
2068
+ "debugger",
2069
+ "declare",
2070
+ "default",
2071
+ "delete",
2072
+ "do",
2073
+ "else",
2074
+ "enum",
2075
+ "export",
2076
+ "extends",
2077
+ "false",
2078
+ "finally",
2079
+ "for",
2080
+ "from",
2081
+ "function",
2082
+ "if",
2083
+ "implements",
2084
+ "import",
2085
+ "in",
2086
+ "infer",
2087
+ "instanceof",
2088
+ "interface",
2089
+ "is",
2090
+ "keyof",
2091
+ "let",
2092
+ "never",
2093
+ "new",
2094
+ "null",
2095
+ "number",
2096
+ "object",
2097
+ "of",
2098
+ "package",
2099
+ "private",
2100
+ "protected",
2101
+ "public",
2102
+ "readonly",
2103
+ "return",
2104
+ "satisfies",
2105
+ "static",
2106
+ "string",
2107
+ "super",
2108
+ "switch",
2109
+ "symbol",
2110
+ "this",
2111
+ "throw",
2112
+ "true",
2113
+ "try",
2114
+ "type",
2115
+ "typeof",
2116
+ "undefined",
2117
+ "unique",
2118
+ "unknown",
2119
+ "using",
2120
+ "var",
2121
+ "void",
2122
+ "while",
2123
+ "with",
2124
+ "yield"
2125
+ ]);
2126
+ function collectIdentifierRanges(code) {
2127
+ const ranges = [];
2128
+ let index = 0;
2129
+ while (index < code.length) {
2130
+ const char = code[index];
2131
+ if (char === "/" && code[index + 1] === "/") {
2132
+ index = code.indexOf("\n", index);
2133
+ if (index === -1) break;
2134
+ continue;
2135
+ }
2136
+ if (char === "/" && code[index + 1] === "*") {
2137
+ const close = code.indexOf("*/", index + 2);
2138
+ index = close === -1 ? code.length : close + 2;
2139
+ continue;
2140
+ }
2141
+ if (char === "\"" || char === "'" || char === "`") {
2142
+ index = skipQuoted(code, index, char);
2143
+ continue;
2144
+ }
2145
+ if (/[A-Za-z_$]/.test(char)) {
2146
+ const start = index;
2147
+ index += 1;
2148
+ while (index < code.length && /[\w$]/.test(code[index])) index += 1;
2149
+ const name = code.slice(start, index);
2150
+ if (!IDENTIFIER_KEYWORDS.has(name)) ranges.push({
2151
+ start,
2152
+ end: index
2153
+ });
2154
+ continue;
2155
+ }
2156
+ index += 1;
2157
+ }
2158
+ return ranges;
2159
+ }
2160
+ function skipQuoted(code, start, quote) {
2161
+ let index = start + 1;
2162
+ while (index < code.length) {
2163
+ if (code[index] === "\\") {
2164
+ index += 2;
2165
+ continue;
2166
+ }
2167
+ if (code[index] === quote) return index + 1;
2168
+ index += 1;
2169
+ }
2170
+ return code.length;
2171
+ }
2172
+ //#endregion
2173
+ //#region src/typed-hover.ts
2174
+ const DEFAULT_LANGUAGES$1 = ["ts", "tsx"];
2175
+ function resolveTypedHoverOptions(options) {
2176
+ if (!options) return {
2177
+ enabled: false,
2178
+ languages: [...DEFAULT_LANGUAGES$1]
2179
+ };
2180
+ if (options === true) return {
2181
+ enabled: true,
2182
+ languages: [...DEFAULT_LANGUAGES$1]
2183
+ };
2184
+ return {
2185
+ enabled: options.enabled ?? true,
2186
+ languages: options.languages ?? [...DEFAULT_LANGUAGES$1],
2187
+ tsgoCommand: options.tsgoCommand
2188
+ };
2189
+ }
2190
+ function hasTypedHoverMeta(meta) {
2191
+ return meta.split(/\s+/).some((token) => token === "twoslash");
2192
+ }
2193
+ function serializeTypedHoverPayload(payload) {
2194
+ return JSON.stringify(payload).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
2195
+ }
2196
+ async function applyTypedHover(source, html, options) {
2197
+ if (!options?.enabled || !source.includes("```")) return html;
2198
+ const languages = new Set(options.languages.map((language) => language.toLowerCase()));
2199
+ const fences = (await extractCodeBlocks(source)).filter((block) => {
2200
+ return languages.has(block.language.toLowerCase()) && hasTypedHoverMeta(block.meta);
2201
+ });
2202
+ if (fences.length === 0) return html;
2203
+ try {
2204
+ return attachTypedHoverPayloads(html, await generateTypedHoverAttachments(fences, options.tsgoCommand));
2205
+ } catch {
2206
+ return html;
2207
+ }
2208
+ }
2209
+ function attachTypedHoverPayloads(html, attachments) {
2210
+ const unused = attachments.filter((item) => item.hovers.length > 0);
2211
+ if (unused.length === 0) return html;
2212
+ let attached = 0;
2213
+ const next = html.replace(/<pre(\b[^>]*)><code(\b[^>]*)>([\s\S]*?)<\/code><\/pre>/g, (full, preAttrs, codeAttrs, inner) => {
2214
+ if (unused.length === 0) return full;
2215
+ if (!isTypeScriptFence(codeAttrs)) return full;
2216
+ const text = decodeHtmlEntities(inner.replace(/<[^>]+>/g, ""));
2217
+ const index = unused.findIndex((item) => normalizeFenceText(item.code) === normalizeFenceText(text));
2218
+ if (index === -1) return full;
2219
+ const item = unused.splice(index, 1)[0];
2220
+ if (!item) return full;
2221
+ attached += 1;
2222
+ const wrapped = wrapHoverRanges(inner, item.hovers);
2223
+ return `${withTypedHoverClass(`<pre${preAttrs}`)}><code${codeAttrs}>${wrapped}</code></pre>\n<script type="application/json" class="ox-typed-hover-data">${serializeTypedHoverPayload({ hovers: item.hovers })}<\/script>`;
2224
+ });
2225
+ if (attached === 0) return html;
2226
+ return `${next}${TYPED_HOVER_STYLE}${TYPED_HOVER_CLIENT}`;
2227
+ }
2228
+ function isTypeScriptFence(codeAttrs) {
2229
+ const match = codeAttrs.match(/class="([^"]*)"/);
2230
+ if (!match?.[1]) return false;
2231
+ return match[1].split(/\s+/).some((token) => {
2232
+ const language = token.replace(/^language-/, "").toLowerCase();
2233
+ return language === "ts" || language === "tsx" || language === "typescript" || language === "typescriptreact";
2234
+ });
2235
+ }
2236
+ function withTypedHoverClass(openPre) {
2237
+ if (/\bclass="/.test(openPre)) return openPre.replace(/\bclass="([^"]*)"/, (_, classes) => {
2238
+ return `class="${classes} ox-typed-hover"`;
2239
+ });
2240
+ return `${openPre} class="ox-typed-hover"`;
2241
+ }
2242
+ function wrapHoverRanges(inner, hovers) {
2243
+ const ranges = [...hovers].sort((a, b) => a.start - b.start || b.end - a.end);
2244
+ let output = "";
2245
+ let htmlIndex = 0;
2246
+ let sourceOffset = 0;
2247
+ let rangeIndex = 0;
2248
+ let openUntil = -1;
2249
+ let openHoverIndex = -1;
2250
+ const startRange = () => {
2251
+ while (rangeIndex < ranges.length && ranges[rangeIndex].start < sourceOffset) rangeIndex += 1;
2252
+ const range = ranges[rangeIndex];
2253
+ if (!range || range.start !== sourceOffset || openUntil !== -1) return;
2254
+ output += `<span class="ox-typed-hover-token" tabindex="0" data-ox-typed-hover="${rangeIndex}">`;
2255
+ openUntil = range.end;
2256
+ openHoverIndex = rangeIndex;
2257
+ rangeIndex += 1;
2258
+ };
2259
+ const endRange = () => {
2260
+ if (openUntil === sourceOffset && openHoverIndex !== -1) {
2261
+ output += "</span>";
2262
+ openUntil = -1;
2263
+ openHoverIndex = -1;
2264
+ }
2265
+ };
2266
+ while (htmlIndex < inner.length) {
2267
+ const char = inner[htmlIndex];
2268
+ if (char === "<") {
2269
+ const close = inner.indexOf(">", htmlIndex);
2270
+ const tag = close === -1 ? inner.slice(htmlIndex) : inner.slice(htmlIndex, close + 1);
2271
+ output += tag;
2272
+ htmlIndex += tag.length;
2273
+ continue;
2274
+ }
2275
+ startRange();
2276
+ if (char === "&") {
2277
+ const semi = inner.indexOf(";", htmlIndex);
2278
+ const entity = semi === -1 ? inner.slice(htmlIndex) : inner.slice(htmlIndex, semi + 1);
2279
+ output += entity;
2280
+ htmlIndex += entity.length;
2281
+ sourceOffset += 1;
2282
+ endRange();
2283
+ continue;
2284
+ }
2285
+ output += char;
2286
+ htmlIndex += 1;
2287
+ sourceOffset += 1;
2288
+ endRange();
2289
+ }
2290
+ if (openHoverIndex !== -1) output += "</span>";
2291
+ return output;
2292
+ }
2293
+ function normalizeFenceText(value) {
2294
+ return decodeHtmlEntities(value).replace(/\r\n/g, "\n").trim();
2295
+ }
2296
+ function decodeHtmlEntities(value) {
2297
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
2298
+ }
2299
+ 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>`;
2300
+ 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>`;
2301
+ //#endregion
2302
+ //#region src/file-tree-options.ts
2303
+ const disabled = {
2304
+ enabled: false,
2305
+ defaultOpen: true,
2306
+ icons: true
2307
+ };
2308
+ function resolveFileTreeOptions(options) {
2309
+ if (!options) return { ...disabled };
2310
+ if (options === true) return enabledDefaults();
2311
+ if (options.enabled === false) return {
2312
+ ...enabledDefaults(),
2313
+ enabled: false,
2314
+ ...resolveIcons(options.icons)
2315
+ };
2316
+ return {
2317
+ enabled: options.enabled ?? true,
2318
+ defaultOpen: options.defaultOpen ?? true,
2319
+ ...resolveIcons(options.icons)
2320
+ };
2321
+ }
2322
+ function toJsFileTreeOptions(options) {
2323
+ if (!options?.enabled) return void 0;
2324
+ return {
2325
+ enabled: true,
2326
+ defaultOpen: options.defaultOpen,
2327
+ icons: options.icons,
2328
+ iconFolder: options.iconFolder,
2329
+ iconFolderOpen: options.iconFolderOpen,
2330
+ iconFile: options.iconFile,
2331
+ iconFiles: options.iconFiles
2332
+ };
2333
+ }
2334
+ function enabledDefaults() {
2335
+ return {
2336
+ enabled: true,
2337
+ defaultOpen: true,
2338
+ icons: true
2339
+ };
2340
+ }
2341
+ function resolveIcons(icons) {
2342
+ if (icons === false) return { icons: false };
2343
+ if (icons === true || icons == null) return { icons: true };
2344
+ return {
2345
+ icons: true,
2346
+ iconFolder: icons.folder,
2347
+ iconFolderOpen: icons.folderOpen,
2348
+ iconFile: icons.file,
2349
+ iconFiles: icons.files
2350
+ };
2351
+ }
2352
+ //#endregion
1867
2353
  //#region src/transform.ts
1868
2354
  /**
1869
2355
  * The NAPI load, cached as the promise rather than as its result.
@@ -1974,7 +2460,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
1974
2460
  } : void 0,
1975
2461
  cards: options.cards?.enabled ? { enabled: true } : void 0,
1976
2462
  steps: options.steps?.enabled ? { enabled: true } : void 0,
1977
- fileTree: options.fileTree?.enabled ? { enabled: true } : void 0,
2463
+ fileTree: toJsFileTreeOptions(options.fileTree),
1978
2464
  sanitize: void 0,
1979
2465
  editThisPage: options.editThisPage?.enabled ? {
1980
2466
  enabled: true,
@@ -1992,26 +2478,27 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
1992
2478
  if (options.mermaid) html = await transformMermaidStatic(html);
1993
2479
  const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
1994
2480
  html = protectedHtml;
1995
- if (options.highlight) {
1996
- const native = await highlightDocumentNatively(html);
1997
- if (native && native.skipped.length === 0) html = native.html;
1998
- else {
1999
- const originalHtml = html;
2000
- const highlightedHtml = await highlightCode(html);
2001
- html = napi.mergeHighlightedCodeBlocks(originalHtml, highlightedHtml);
2002
- }
2003
- }
2481
+ if (options.highlight) html = await highlightPageHtml(html, napi.mergeHighlightedCodeBlocks);
2004
2482
  html = await transformBuiltinEmbeds(html, options.embeds ?? {
2005
2483
  github: {},
2006
2484
  openGraph: {}
2007
2485
  });
2486
+ if (options.highlight && html.includes("ox-github-code-block")) html = await highlightPageHtml(html, napi.mergeHighlightedCodeBlocks);
2008
2487
  html = restoreMermaidSvgs(html, svgs);
2009
2488
  if (options.sanitize?.enabled) html = napi.sanitizeHtml(html, toJsSanitizeOptions(options.sanitize));
2489
+ if (isMathEnabled(options.math)) html = await renderKatexMath(html);
2490
+ const imports = result.imports ?? [];
2491
+ const exports = result.exports ?? [];
2492
+ const components = result.components ?? [];
2493
+ html = await applyTypedHover(source, html, options.typedHover);
2010
2494
  return {
2011
- code: generateModuleCode(html, frontmatter, toc, filePath, options),
2495
+ code: generateModuleCode(html, frontmatter, toc, imports, exports, components, filePath),
2012
2496
  html,
2013
2497
  frontmatter,
2014
- toc
2498
+ toc,
2499
+ imports,
2500
+ exports,
2501
+ components
2015
2502
  };
2016
2503
  }
2017
2504
  async function runCodeBlockTypecheck(source, options) {
@@ -2070,8 +2557,11 @@ function normalizeTocEntry(entry) {
2070
2557
  }
2071
2558
  /**
2072
2559
  * Generates the JavaScript module code.
2560
+ *
2561
+ * MDX metadata is serialized as JSON. User `import` / `export` source is never
2562
+ * emitted as live JavaScript, so transform does not execute module side effects.
2073
2563
  */
2074
- function generateModuleCode(html, frontmatter, toc, filePath, _options) {
2564
+ function generateModuleCode(html, frontmatter, toc, imports, exports, components, filePath) {
2075
2565
  return `
2076
2566
  // Generated by @ox-content/vite-plugin
2077
2567
  // Source: ${filePath}
@@ -2091,6 +2581,21 @@ export const frontmatter = ${JSON.stringify(frontmatter)};
2091
2581
  */
2092
2582
  export const toc = ${JSON.stringify(toc)};
2093
2583
 
2584
+ /**
2585
+ * MDX import statements collected from the AST.
2586
+ */
2587
+ export const imports = ${JSON.stringify(imports)};
2588
+
2589
+ /**
2590
+ * MDX export names collected from the AST.
2591
+ */
2592
+ export const exports = ${JSON.stringify(exports)};
2593
+
2594
+ /**
2595
+ * Unique JSX component names collected from the AST.
2596
+ */
2597
+ export const components = ${JSON.stringify(components)};
2598
+
2094
2599
  /**
2095
2600
  * Default export with all data.
2096
2601
  */
@@ -2098,6 +2603,9 @@ export default {
2098
2603
  html,
2099
2604
  frontmatter,
2100
2605
  toc,
2606
+ imports,
2607
+ exports,
2608
+ components,
2101
2609
  };
2102
2610
 
2103
2611
  // HMR support
@@ -2548,7 +3056,7 @@ function formatChromiumUnavailableDetail(err) {
2548
3056
  /**
2549
3057
  * Escapes HTML special characters.
2550
3058
  */
2551
- function escapeHtml$4(str) {
3059
+ function escapeHtml$5(str) {
2552
3060
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2553
3061
  }
2554
3062
  function normalizeBrandValue(str) {
@@ -2611,12 +3119,12 @@ function getDefaultTemplate() {
2611
3119
  const isBrandCard = normalizeBrandValue(title) === normalizeBrandValue(rawBrand);
2612
3120
  const heroTitle = isBrandCard ? "High-performance Markdown toolkit" : title;
2613
3121
  const heroDescription = isBrandCard ? "Rust-powered docs and high-performance Markdown tooling." : description && description.trim().length > 0 ? description : "Rust-powered docs and Markdown tooling.";
2614
- 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>` : "";
3122
+ 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>` : "";
2615
3123
  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;">
2616
3124
  <div style="position:relative;z-index:1;display:flex;flex-direction:column;height:100%;">
2617
3125
  <div style="display:flex;align-items:flex-start;">${renderWordmarkSvg()}</div>
2618
3126
  <div style="display:flex;flex-direction:column;justify-content:center;gap:24px;max-width:860px;flex:1;padding:22px 0 0;">
2619
- <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>
3127
+ <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>
2620
3128
  ${descriptionHtml}
2621
3129
  </div>
2622
3130
  </div>
@@ -2650,7 +3158,7 @@ function computeCacheKey(templateSource, props, width, height) {
2650
3158
  async function getCached(cacheDir, key) {
2651
3159
  const filePath = path$2.join(cacheDir, `${key}.png`);
2652
3160
  try {
2653
- return await fs$2.readFile(filePath);
3161
+ return await fs$3.readFile(filePath);
2654
3162
  } catch {
2655
3163
  return null;
2656
3164
  }
@@ -2659,9 +3167,9 @@ async function getCached(cacheDir, key) {
2659
3167
  * Writes a PNG buffer to the cache.
2660
3168
  */
2661
3169
  async function writeCache(cacheDir, key, png) {
2662
- await fs$2.mkdir(cacheDir, { recursive: true });
3170
+ await fs$3.mkdir(cacheDir, { recursive: true });
2663
3171
  const filePath = path$2.join(cacheDir, `${key}.png`);
2664
- await fs$2.writeFile(filePath, png);
3172
+ await fs$3.writeFile(filePath, png);
2665
3173
  }
2666
3174
  //#endregion
2667
3175
  //#region \0@oxc-project+runtime@0.143.0/helpers/esm/usingCtx.js
@@ -3147,6 +3655,80 @@ async function renderSinglePage(entry, templateFn, templateSource, options, cach
3147
3655
  }
3148
3656
  }
3149
3657
  //#endregion
3658
+ //#region src/plugins/math-assets.ts
3659
+ /**
3660
+ * Serve and copy KaTeX CSS/fonts only when the optional `katex` package exists.
3661
+ */
3662
+ /**
3663
+ * Copies `katex.min.css` and `fonts/` into the SSG output.
3664
+ * Returns an empty list when KaTeX is not installed.
3665
+ */
3666
+ async function copyKatexAssets(outDir) {
3667
+ const dist = resolveKatexDist();
3668
+ if (!dist) return [];
3669
+ const dest = join(outDir, KATEX_ASSET_DIR);
3670
+ await mkdir(join(dest, "fonts"), { recursive: true });
3671
+ const cssDest = join(dest, "katex.min.css");
3672
+ await copyFile(join(dist, "katex.min.css"), cssDest);
3673
+ await cp(join(dist, "fonts"), join(dest, "fonts"), { recursive: true });
3674
+ return [cssDest];
3675
+ }
3676
+ /** Dev-server middleware that serves `/__ox_katex__/*` from `katex/dist`. */
3677
+ function createKatexAssetsPlugin() {
3678
+ return {
3679
+ name: "ox-content:katex-assets",
3680
+ configureServer(server) {
3681
+ const dist = resolveKatexDist();
3682
+ if (!dist) return;
3683
+ server.middlewares.use((req, res, next) => {
3684
+ const url = req.url ?? "";
3685
+ const marker = `/${KATEX_ASSET_DIR}/`;
3686
+ const index = url.indexOf(marker);
3687
+ if (index === -1) {
3688
+ next();
3689
+ return;
3690
+ }
3691
+ const rel = decodeURIComponent(url.slice(index + marker.length).split("?")[0] ?? "");
3692
+ const file = safeKatexFile(dist, rel);
3693
+ if (!file) {
3694
+ res.statusCode = 404;
3695
+ res.end();
3696
+ return;
3697
+ }
3698
+ stat(file).then((info) => {
3699
+ if (!info.isFile()) {
3700
+ res.statusCode = 404;
3701
+ res.end();
3702
+ return;
3703
+ }
3704
+ res.setHeader("Content-Type", katexContentType(file));
3705
+ createReadStream(file).pipe(res);
3706
+ }).catch(() => {
3707
+ res.statusCode = 404;
3708
+ res.end();
3709
+ });
3710
+ });
3711
+ }
3712
+ };
3713
+ }
3714
+ function safeKatexFile(dist, rel) {
3715
+ if (!rel || rel.includes("\0") || rel.split(/[\\/]/).includes("..")) return null;
3716
+ const full = resolve(dist, rel);
3717
+ const root = resolve(dist) + sep;
3718
+ if (full !== resolve(dist) && !full.startsWith(root)) return null;
3719
+ const inside = relative(dist, full);
3720
+ if (inside.startsWith("..") || inside.includes(`..${sep}`)) return null;
3721
+ return full;
3722
+ }
3723
+ function katexContentType(file) {
3724
+ const ext = extname(file);
3725
+ if (ext === ".css") return "text/css; charset=utf-8";
3726
+ if (ext === ".woff2") return "font/woff2";
3727
+ if (ext === ".woff") return "font/woff";
3728
+ if (ext === ".ttf") return "font/ttf";
3729
+ return "application/octet-stream";
3730
+ }
3731
+ //#endregion
3150
3732
  //#region src/island/parse.ts
3151
3733
  /**
3152
3734
  * Island Parser
@@ -3755,6 +4337,7 @@ function renderPage(page, options) {
3755
4337
  html: page.html,
3756
4338
  toc: page.toc,
3757
4339
  lastUpdated: page.lastUpdated,
4340
+ contributors: page.contributors,
3758
4341
  path: page.path,
3759
4342
  url: page.url,
3760
4343
  frontmatter: page.frontmatter,
@@ -3770,6 +4353,7 @@ function renderPage(page, options) {
3770
4353
  html: p.html,
3771
4354
  toc: p.toc,
3772
4355
  lastUpdated: p.lastUpdated,
4356
+ contributors: p.contributors,
3773
4357
  path: p.path,
3774
4358
  url: p.url,
3775
4359
  frontmatter: p.frontmatter,
@@ -3830,8 +4414,8 @@ function DefaultTheme({ children }) {
3830
4414
  <head>
3831
4415
  <meta charset="UTF-8">
3832
4416
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
3833
- <title>${escapeHtml$3(page.title)} - ${escapeHtml$3(site.name)}</title>
3834
- ${page.description ? `<meta name="description" content="${escapeHtml$3(page.description)}">` : ""}
4417
+ <title>${escapeHtml$4(page.title)} - ${escapeHtml$4(site.name)}</title>
4418
+ ${page.description ? `<meta name="description" content="${escapeHtml$4(page.description)}">` : ""}
3835
4419
  <style>
3836
4420
  :root {
3837
4421
  --octc-color-primary: #4f6fae;
@@ -3855,7 +4439,7 @@ function DefaultTheme({ children }) {
3855
4439
  </head>
3856
4440
  <body>
3857
4441
  <header>
3858
- <h1>${escapeHtml$3(site.name)}</h1>
4442
+ <h1>${escapeHtml$4(site.name)}</h1>
3859
4443
  </header>
3860
4444
  <main>
3861
4445
  ${children.__html}
@@ -3863,7 +4447,7 @@ function DefaultTheme({ children }) {
3863
4447
  </body>
3864
4448
  </html>` };
3865
4449
  }
3866
- function escapeHtml$3(str) {
4450
+ function escapeHtml$4(str) {
3867
4451
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3868
4452
  }
3869
4453
  /**
@@ -3900,7 +4484,7 @@ function createTheme(config) {
3900
4484
  * String bodies follow `ox_content_ssg::generate_site_maps`. The Vite plugin
3901
4485
  * writes those files during SSG without adding a NAPI surface.
3902
4486
  */
3903
- 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";
4487
+ 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";
3904
4488
  /**
3905
4489
  * Resolves `siteMaps` with defaults.
3906
4490
  *
@@ -3927,7 +4511,7 @@ function resolveSiteMapsOptions(value) {
3927
4511
  /** Builds sitemap / robots / llms bodies without writing files. */
3928
4512
  function generateSiteMaps(input) {
3929
4513
  if (!input.options?.enabled) return {};
3930
- if (!hasSiteUrl$1(input.siteUrl)) return { warning: MISSING_SITE_URL$1 };
4514
+ if (!hasSiteUrl$2(input.siteUrl)) return { warning: MISSING_SITE_URL$2 };
3931
4515
  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);
3932
4516
  const result = { sitemapXml: generateSitemapXml(published) };
3933
4517
  if (input.options.robots) result.robotsTxt = generateRobotsTxt(input.sitemapLoc ?? "");
@@ -3954,20 +4538,20 @@ async function writeSiteMapFiles(input) {
3954
4538
  [generated.llmsTxt, "llms.txt"]
3955
4539
  ].filter((entry) => entry[0] != null);
3956
4540
  if (outputs.length === 0) return { files: [] };
3957
- await fs$1.mkdir(input.outDir, { recursive: true });
4541
+ await fs$2.mkdir(input.outDir, { recursive: true });
3958
4542
  const files = [];
3959
4543
  for (const [body, name] of outputs) {
3960
4544
  const outputPath = path$1.join(input.outDir, name);
3961
- await fs$1.writeFile(outputPath, body, "utf8");
4545
+ await fs$2.writeFile(outputPath, body, "utf8");
3962
4546
  files.push(outputPath);
3963
4547
  }
3964
4548
  return { files };
3965
4549
  }
3966
- function hasSiteUrl$1(siteUrl) {
4550
+ function hasSiteUrl$2(siteUrl) {
3967
4551
  return Boolean(siteUrl && siteUrl.trim());
3968
4552
  }
3969
4553
  function absoluteSitemapUrl(siteUrl, base) {
3970
- if (!hasSiteUrl$1(siteUrl)) return "";
4554
+ if (!hasSiteUrl$2(siteUrl)) return "";
3971
4555
  return `${(siteUrl ?? "").trim().replace(/\/+$/, "")}${!base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`}sitemap.xml`;
3972
4556
  }
3973
4557
  function generateSitemapXml(pages) {
@@ -4153,7 +4737,7 @@ function resolvePageRoutes(input) {
4153
4737
  if (!input.permalinks?.enabled) return {
4154
4738
  pages: cascaded.map((page) => ({
4155
4739
  source: page.source,
4156
- urlPath: normalizeUrlPath(page.fileUrl),
4740
+ urlPath: normalizeUrlPath$1(page.fileUrl),
4157
4741
  frontmatter: page.frontmatter
4158
4742
  })),
4159
4743
  errors: []
@@ -4181,7 +4765,7 @@ function resolvePageRoutes(input) {
4181
4765
  errors
4182
4766
  };
4183
4767
  }
4184
- function normalizeUrlPath(value) {
4768
+ function normalizeUrlPath$1(value) {
4185
4769
  const segments = pathSegments(value);
4186
4770
  return segments.length === 0 ? "/" : segments.join("/");
4187
4771
  }
@@ -4215,10 +4799,10 @@ function applyCascade(pages, options) {
4215
4799
  });
4216
4800
  }
4217
4801
  function resolveOne(page) {
4218
- const fileUrl = normalizeUrlPath(page.fileUrl);
4802
+ const fileUrl = normalizeUrlPath$1(page.fileUrl);
4219
4803
  const permalink = readString(page.frontmatter.permalink);
4220
4804
  if (permalink !== void 0) {
4221
- const url = isSafePermalink(permalink) ? normalizeUrlPath(permalink) : void 0;
4805
+ const url = isSafePermalink(permalink) ? normalizeUrlPath$1(permalink) : void 0;
4222
4806
  return url ? { urlPath: url } : {
4223
4807
  urlPath: fileUrl,
4224
4808
  error: `[ox-content] rejected permalink ${JSON.stringify(permalink)} on ${page.source} (path escape); using the file-tree URL`
@@ -4237,7 +4821,7 @@ function resolveOne(page) {
4237
4821
  function rewriteSlug(fileUrl, slug) {
4238
4822
  const trimmed = slug.trim();
4239
4823
  if (trimmed.includes("/") || !isSafePermalink(trimmed)) return;
4240
- const normalized = normalizeUrlPath(trimmed);
4824
+ const normalized = normalizeUrlPath$1(trimmed);
4241
4825
  if (normalized === "/") return;
4242
4826
  if (fileUrl === "/") return normalized;
4243
4827
  const segments = fileUrl.split("/").filter(Boolean);
@@ -4354,8 +4938,8 @@ function applyCollectionRoutes(manifest, permalinks, cascade) {
4354
4938
  }
4355
4939
  /** Updates auto-nav hrefs after permalinks change a page URL. */
4356
4940
  function remapNavGroups(nav, kept, skippedFileUrls) {
4357
- const skipped = new Set(skippedFileUrls.map(normalizeUrlPath));
4358
- const byFile = new Map(kept.map((page) => [normalizeUrlPath(page.fileUrl), page]));
4941
+ const skipped = new Set(skippedFileUrls.map(normalizeUrlPath$1));
4942
+ const byFile = new Map(kept.map((page) => [normalizeUrlPath$1(page.fileUrl), page]));
4359
4943
  return nav.map((group) => ({
4360
4944
  ...group,
4361
4945
  items: remapNavItems(group.items, byFile, skipped)
@@ -4367,7 +4951,7 @@ function routePathsFromUrl(urlPath, srcDir, outDir, base, extension, siteUrl) {
4367
4951
  }
4368
4952
  function remapNavItems(items, byFile, skipped) {
4369
4953
  return items.flatMap((item) => {
4370
- const key = normalizeUrlPath(item.path);
4954
+ const key = normalizeUrlPath$1(item.path);
4371
4955
  if (skipped.has(key)) return [];
4372
4956
  const hit = byFile.get(key);
4373
4957
  const children = item.children ? remapNavItems(item.children, byFile, skipped) : void 0;
@@ -4484,16 +5068,16 @@ function planRedirectFiles(input) {
4484
5068
  async function writeRedirectFiles(input) {
4485
5069
  const plan = planRedirectFiles(input);
4486
5070
  if (plan.files.length === 0 && !plan.netlify && !plan.headers && !plan.json) return { files: [] };
4487
- await fs$1.mkdir(input.outDir, { recursive: true });
5071
+ await fs$2.mkdir(input.outDir, { recursive: true });
4488
5072
  const files = [];
4489
5073
  for (const entry of plan.files) {
4490
5074
  const outputPath = path$1.join(input.outDir, entry.relativePath);
4491
5075
  try {
4492
- await fs$1.access(outputPath);
5076
+ await fs$2.access(outputPath);
4493
5077
  continue;
4494
5078
  } catch {
4495
- await fs$1.mkdir(path$1.dirname(outputPath), { recursive: true });
4496
- await fs$1.writeFile(outputPath, entry.html, "utf8");
5079
+ await fs$2.mkdir(path$1.dirname(outputPath), { recursive: true });
5080
+ await fs$2.writeFile(outputPath, entry.html, "utf8");
4497
5081
  files.push(outputPath);
4498
5082
  }
4499
5083
  }
@@ -4504,14 +5088,14 @@ async function writeRedirectFiles(input) {
4504
5088
  ]) {
4505
5089
  if (!body) continue;
4506
5090
  const outputPath = path$1.join(input.outDir, name);
4507
- await fs$1.writeFile(outputPath, body, "utf8");
5091
+ await fs$2.writeFile(outputPath, body, "utf8");
4508
5092
  files.push(outputPath);
4509
5093
  }
4510
5094
  return { files };
4511
5095
  }
4512
5096
  /** Static HTML redirect body. `dest` is escaped. */
4513
5097
  function generateRedirectHtml(dest) {
4514
- const escaped = escapeHtml$2(dest);
5098
+ const escaped = escapeHtml$3(dest);
4515
5099
  return `\
4516
5100
  <!DOCTYPE html>
4517
5101
  <html lang="en">
@@ -4596,7 +5180,7 @@ function upsert(files, index, occupied, from, to, base) {
4596
5180
  html
4597
5181
  });
4598
5182
  }
4599
- function escapeHtml$2(value) {
5183
+ function escapeHtml$3(value) {
4600
5184
  return value.replace(/[&<>"']/g, (ch) => {
4601
5185
  switch (ch) {
4602
5186
  case "&": return "&amp;";
@@ -4994,7 +5578,7 @@ function createNativeTransformOptions(options) {
4994
5578
  } : void 0,
4995
5579
  cards: options.cards?.enabled ? { enabled: true } : void 0,
4996
5580
  steps: options.steps?.enabled ? { enabled: true } : void 0,
4997
- fileTree: options.fileTree?.enabled ? { enabled: true } : void 0,
5581
+ fileTree: toJsFileTreeOptions(options.fileTree),
4998
5582
  editThisPage: options.editThisPage?.enabled ? {
4999
5583
  enabled: true,
5000
5584
  repoUrl: options.editThisPage.repoUrl,
@@ -5272,8 +5856,8 @@ function weekdayUtc(year, month, day) {
5272
5856
  * String bodies follow `ox_content_ssg::generate_feeds`. The Vite plugin
5273
5857
  * writes those files during SSG without adding a NAPI surface.
5274
5858
  */
5275
- const MISSING_SITE_URL = "[ox-content] feeds is enabled but ssg.siteUrl is not set; RSS, Atom, and JSON feeds were not written";
5276
- const DEFAULT_FORMATS = [
5859
+ const MISSING_SITE_URL$1 = "[ox-content] feeds is enabled but ssg.siteUrl is not set; RSS, Atom, and JSON feeds were not written";
5860
+ const DEFAULT_FORMATS$1 = [
5277
5861
  "rss",
5278
5862
  "atom",
5279
5863
  "json"
@@ -5290,19 +5874,19 @@ const DEFAULT_PATH = "/";
5290
5874
  function resolveFeedsOptions(value) {
5291
5875
  if (!value) return {
5292
5876
  enabled: false,
5293
- formats: [...DEFAULT_FORMATS],
5877
+ formats: [...DEFAULT_FORMATS$1],
5294
5878
  limit: DEFAULT_LIMIT,
5295
5879
  path: DEFAULT_PATH
5296
5880
  };
5297
5881
  if (value === true) return {
5298
5882
  enabled: true,
5299
- formats: [...DEFAULT_FORMATS],
5883
+ formats: [...DEFAULT_FORMATS$1],
5300
5884
  limit: DEFAULT_LIMIT,
5301
5885
  path: DEFAULT_PATH
5302
5886
  };
5303
5887
  return {
5304
5888
  enabled: true,
5305
- formats: normalizeFormats(value.formats),
5889
+ formats: normalizeFormats$1(value.formats),
5306
5890
  collection: value.collection,
5307
5891
  limit: value.limit ?? DEFAULT_LIMIT,
5308
5892
  path: value.path ?? DEFAULT_PATH
@@ -5317,7 +5901,7 @@ function resolveFeedCollectionName(requested, collectionNames) {
5317
5901
  /** Builds RSS / Atom / JSON Feed bodies without writing files. */
5318
5902
  function generateFeeds(input) {
5319
5903
  if (!input.options?.enabled) return {};
5320
- if (!hasSiteUrl(input.siteUrl)) return { warning: MISSING_SITE_URL };
5904
+ if (!hasSiteUrl$1(input.siteUrl)) return { warning: MISSING_SITE_URL$1 };
5321
5905
  const published = publishedItems(input);
5322
5906
  const doc = feedDocument(input);
5323
5907
  const result = {};
@@ -5340,17 +5924,17 @@ async function writeFeedFiles(input) {
5340
5924
  ].filter((entry) => entry[0] != null);
5341
5925
  if (outputs.length === 0) return { files: [] };
5342
5926
  const dest = outputDir(input.outDir, input.options?.path ?? DEFAULT_PATH);
5343
- await fs$1.mkdir(dest, { recursive: true });
5927
+ await fs$2.mkdir(dest, { recursive: true });
5344
5928
  const files = [];
5345
5929
  for (const [body, name] of outputs) {
5346
5930
  const outputPath = path$1.join(dest, name);
5347
- await fs$1.writeFile(outputPath, body, "utf8");
5931
+ await fs$2.writeFile(outputPath, body, "utf8");
5348
5932
  files.push(outputPath);
5349
5933
  }
5350
5934
  return { files };
5351
5935
  }
5352
- function normalizeFormats(formats) {
5353
- if (!formats) return [...DEFAULT_FORMATS];
5936
+ function normalizeFormats$1(formats) {
5937
+ if (!formats) return [...DEFAULT_FORMATS$1];
5354
5938
  const seen = /* @__PURE__ */ new Set();
5355
5939
  const resolved = [];
5356
5940
  for (const format of formats) if ((format === "rss" || format === "atom" || format === "json") && !seen.has(format)) {
@@ -5359,7 +5943,7 @@ function normalizeFormats(formats) {
5359
5943
  }
5360
5944
  return resolved;
5361
5945
  }
5362
- function hasSiteUrl(siteUrl) {
5946
+ function hasSiteUrl$1(siteUrl) {
5363
5947
  return Boolean(siteUrl && siteUrl.trim());
5364
5948
  }
5365
5949
  function homePageUrl(siteUrl, base = "/") {
@@ -5411,7 +5995,7 @@ function normalizeItem(item, input) {
5411
5995
  title: item.title ?? "",
5412
5996
  description: typeof item.description === "string" ? item.description : void 0,
5413
5997
  loc: item.loc || itemLoc(input, item),
5414
- date: parseDate(dateField(item.date ?? item.frontmatter?.date)) ?? parseDate(dateField(item.lastUpdated ?? item.frontmatter?.lastUpdated))
5998
+ date: parseDate(dateField$1(item.date ?? item.frontmatter?.date)) ?? parseDate(dateField$1(item.lastUpdated ?? item.frontmatter?.lastUpdated))
5415
5999
  };
5416
6000
  }
5417
6001
  function itemLoc(input, item) {
@@ -5419,49 +6003,254 @@ function itemLoc(input, item) {
5419
6003
  const urlPath = (item.path ?? "").replace(/^\/+|\/+$/g, "");
5420
6004
  return urlPath ? `${home}${urlPath}/` : home;
5421
6005
  }
5422
- function dateField(value) {
6006
+ function dateField$1(value) {
5423
6007
  if (typeof value === "string" && value.trim()) return value.trim();
5424
6008
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
5425
6009
  if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
5426
6010
  }
5427
6011
  //#endregion
5428
- //#region src/taxonomies-html.ts
6012
+ //#region src/pwa.ts
5429
6013
  /**
5430
- * Escaped taxonomy HTML and confined output paths.
6014
+ * Opt-in web app manifest and conservative service worker.
6015
+ *
6016
+ * The Vite plugin writes those files during SSG without adding a NAPI surface.
6017
+ * Enabling `offline` injects a tiny client script that registers `sw.js`.
5431
6018
  */
5432
- function relatedMarkup(pages) {
5433
- 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>`;
6019
+ const MISSING_SITE_URL = "[ox-content] pwa is enabled but ssg.siteUrl is not set; manifest.webmanifest and sw.js were not written";
6020
+ const DEFAULT_THEME_COLOR = "#000000";
6021
+ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
6022
+ const MANIFEST_NAME = "manifest.webmanifest";
6023
+ const SERVICE_WORKER_NAME = "sw.js";
6024
+ /**
6025
+ * Resolves `pwa` with defaults.
6026
+ *
6027
+ * `false` / omitted stays off. `true` enables the manifest and offline
6028
+ * service worker. An object enables the feature and overrides only the
6029
+ * fields the site set.
6030
+ */
6031
+ function resolvePwaOptions(value) {
6032
+ if (!value) return {
6033
+ enabled: false,
6034
+ offline: true
6035
+ };
6036
+ if (value === true) return {
6037
+ enabled: true,
6038
+ offline: true
6039
+ };
6040
+ return {
6041
+ enabled: true,
6042
+ offline: value.offline ?? true,
6043
+ name: value.name,
6044
+ shortName: value.shortName,
6045
+ themeColor: value.themeColor,
6046
+ backgroundColor: value.backgroundColor,
6047
+ startUrl: value.startUrl
6048
+ };
5434
6049
  }
5435
- function listPageContent(terms, base, urlName) {
5436
- const items = terms.map((term) => listItem(siteHref$2(base, urlName, term.slug), term.label)).join("");
5437
- return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
6050
+ /** Builds manifest / service-worker bodies without writing files. */
6051
+ function generatePwa(input) {
6052
+ if (!input.options?.enabled) return {};
6053
+ if (!hasSiteUrl(input.siteUrl)) return { warning: MISSING_SITE_URL };
6054
+ const base = normalizeBase(input.base);
6055
+ const name = sanitizeManifestText(input.options.name ?? input.siteName ?? "");
6056
+ const shortName = sanitizeManifestText(input.options.shortName ?? name);
6057
+ const startUrl = sanitizeStartUrl(input.options.startUrl, base);
6058
+ const themeColor = sanitizeColor(input.options.themeColor, DEFAULT_THEME_COLOR);
6059
+ const backgroundColor = sanitizeColor(input.options.backgroundColor, DEFAULT_BACKGROUND_COLOR);
6060
+ const result = { manifest: `${escapeJsonScript(JSON.stringify({
6061
+ name,
6062
+ short_name: shortName,
6063
+ start_url: startUrl,
6064
+ scope: base,
6065
+ display: "standalone",
6066
+ background_color: backgroundColor,
6067
+ theme_color: themeColor
6068
+ }, null, 2))}\n` };
6069
+ if (input.options.offline) result.serviceWorker = generateServiceWorker(base);
6070
+ return result;
5438
6071
  }
5439
- function termPageContent(term) {
6072
+ /** Writes enabled PWA files into `outDir`. */
6073
+ async function writePwaFiles(input) {
6074
+ const generated = generatePwa(input);
6075
+ if (generated.warning) return {
6076
+ files: [],
6077
+ warning: generated.warning
6078
+ };
6079
+ const outputs = [[generated.manifest, MANIFEST_NAME], [generated.serviceWorker, SERVICE_WORKER_NAME]].filter((entry) => entry[0] != null);
6080
+ if (outputs.length === 0) return { files: [] };
6081
+ await fs$2.mkdir(input.outDir, { recursive: true });
6082
+ const files = [];
6083
+ for (const [body, name] of outputs) {
6084
+ const outputPath = path$1.join(input.outDir, name);
6085
+ await fs$2.writeFile(outputPath, body, "utf8");
6086
+ files.push(outputPath);
6087
+ }
6088
+ return { files };
6089
+ }
6090
+ /**
6091
+ * Injects `rel=manifest` (and the service-worker register script when offline)
6092
+ * into a themed HTML document. Bare / fragment HTML is left unchanged.
6093
+ */
6094
+ function injectPwaPageTags(html, input) {
6095
+ if (!input.options?.enabled || !isThemedDocument(html)) return html;
6096
+ const base = normalizeBase(input.base);
6097
+ const manifestHref = escapeAttribute$1(`${base}${MANIFEST_NAME}`);
6098
+ const themeColor = sanitizeColor(input.options.themeColor, DEFAULT_THEME_COLOR);
6099
+ let next = insertBeforeTag(html, "</head>", ` ${[`<link rel="manifest" href="${manifestHref}">`, `<meta name="theme-color" content="${escapeAttribute$1(themeColor)}">`].join("\n ")}\n`);
6100
+ if (input.options.offline) {
6101
+ const script = `<script>if("serviceWorker"in navigator)navigator.serviceWorker.register(${JSON.stringify(`${base}${SERVICE_WORKER_NAME}`)})<\/script>`;
6102
+ next = insertBeforeTag(next, "</body>", ` ${script}\n`);
6103
+ }
6104
+ return next;
6105
+ }
6106
+ function generateServiceWorker(base) {
6107
+ return `/* ox-content PWA service worker */
6108
+ const CACHE = "ox-content-pwa-v1";
6109
+ const ASSET_PREFIX = ${JSON.stringify(`${base}assets/`)};
6110
+
6111
+ self.addEventListener("install", (event) => {
6112
+ event.waitUntil(self.skipWaiting());
6113
+ });
6114
+
6115
+ self.addEventListener("activate", (event) => {
6116
+ event.waitUntil(self.clients.claim());
6117
+ });
6118
+
6119
+ self.addEventListener("fetch", (event) => {
6120
+ const request = event.request;
6121
+ if (request.method !== "GET") return;
6122
+ const url = new URL(request.url);
6123
+ if (url.origin !== self.location.origin) return;
6124
+
6125
+ if (isHashedAsset(url.pathname)) {
6126
+ event.respondWith(cacheFirst(request));
6127
+ return;
6128
+ }
6129
+
6130
+ if (isHtmlPage(request)) {
6131
+ event.respondWith(networkFirst(request));
6132
+ }
6133
+ });
6134
+
6135
+ function isHashedAsset(pathname) {
6136
+ if (!pathname.startsWith(ASSET_PREFIX)) return false;
6137
+ return /-[0-9a-f]{8,}\\.[a-z0-9]+$/i.test(pathname);
6138
+ }
6139
+
6140
+ function isHtmlPage(request) {
6141
+ if (request.mode === "navigate") return true;
6142
+ const accept = request.headers.get("accept") || "";
6143
+ return accept.includes("text/html");
6144
+ }
6145
+
6146
+ async function cacheFirst(request) {
6147
+ const cache = await caches.open(CACHE);
6148
+ const cached = await cache.match(request);
6149
+ if (cached) return cached;
6150
+ const response = await fetch(request);
6151
+ if (response.ok) cache.put(request, response.clone());
6152
+ return response;
6153
+ }
6154
+
6155
+ async function networkFirst(request) {
6156
+ const cache = await caches.open(CACHE);
6157
+ try {
6158
+ const response = await fetch(request);
6159
+ if (response.ok) cache.put(request, response.clone());
6160
+ return response;
6161
+ } catch (error) {
6162
+ const cached = await cache.match(request);
6163
+ if (cached) return cached;
6164
+ throw error;
6165
+ }
6166
+ }
6167
+ `;
6168
+ }
6169
+ function hasSiteUrl(siteUrl) {
6170
+ return Boolean(siteUrl && siteUrl.trim());
6171
+ }
6172
+ function normalizeBase(base) {
6173
+ if (!base || base === "/") return "/";
6174
+ return base.endsWith("/") ? base : `${base}/`;
6175
+ }
6176
+ function sanitizeManifestText(value) {
6177
+ return value.split(/\s+/u).filter(Boolean).join(" ");
6178
+ }
6179
+ function sanitizeColor(value, fallback) {
6180
+ if (!value) return fallback;
6181
+ const trimmed = value.trim();
6182
+ if (/^#[0-9A-Fa-f]{3,8}$/.test(trimmed)) return trimmed;
6183
+ if (/^[a-zA-Z][a-zA-Z0-9-]{0,31}$/.test(trimmed)) return trimmed;
6184
+ return fallback;
6185
+ }
6186
+ function sanitizeStartUrl(value, base) {
6187
+ if (!value) return base;
6188
+ const trimmed = value.trim();
6189
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//")) return base;
6190
+ if (/[\n\r\t<>"'`]/.test(trimmed)) return base;
6191
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return base;
6192
+ return trimmed;
6193
+ }
6194
+ function isThemedDocument(html) {
6195
+ return /<\/head>/i.test(html) && /<\/body>/i.test(html);
6196
+ }
6197
+ function insertBeforeTag(html, tag, snippet) {
6198
+ const index = html.toLowerCase().lastIndexOf(tag.toLowerCase());
6199
+ if (index === -1) return html;
6200
+ return `${html.slice(0, index)}${snippet}${html.slice(index)}`;
6201
+ }
6202
+ function escapeJsonScript(value) {
6203
+ return value.replace(/[<>]/g, (ch) => ch === "<" ? "\\u003c" : "\\u003e");
6204
+ }
6205
+ function escapeAttribute$1(value) {
6206
+ return value.replace(/[&<>"']/g, (ch) => {
6207
+ switch (ch) {
6208
+ case "&": return "&amp;";
6209
+ case "<": return "&lt;";
6210
+ case ">": return "&gt;";
6211
+ case "\"": return "&quot;";
6212
+ default: return "&#39;";
6213
+ }
6214
+ });
6215
+ }
6216
+ //#endregion
6217
+ //#region src/taxonomies-html.ts
6218
+ /**
6219
+ * Escaped taxonomy HTML and confined output paths.
6220
+ */
6221
+ function relatedMarkup(pages) {
6222
+ 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>`;
6223
+ }
6224
+ function listPageContent(terms, base, urlName) {
6225
+ const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
6226
+ return `<h1>${escapeHtml$2(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
6227
+ }
6228
+ function termPageContent(term) {
5440
6229
  const items = [...term.pages].sort((left, right) => {
5441
6230
  const titleCmp = left.title.localeCompare(right.title);
5442
6231
  return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
5443
- }).map((page) => listItem(page.routePaths.href, page.title)).join("");
5444
- return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
6232
+ }).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
6233
+ return `<h1>${escapeHtml$2(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
5445
6234
  }
5446
6235
  function displayTaxonomyName(name) {
5447
6236
  return name.charAt(0).toUpperCase() + name.slice(1);
5448
6237
  }
5449
- function siteHref$2(base, ...segments) {
6238
+ function siteHref$3(base, ...segments) {
5450
6239
  const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
5451
6240
  const rest = segments.filter(Boolean).join("/");
5452
6241
  return rest ? `${prefix}${rest}/` : prefix;
5453
6242
  }
5454
- function containedPath(outDir, ...segments) {
6243
+ function containedPath$2(outDir, ...segments) {
5455
6244
  const root = path$1.resolve(outDir);
5456
6245
  const resolved = path$1.resolve(root, ...segments);
5457
6246
  const prefix = root.endsWith(path$1.sep) ? root : `${root}${path$1.sep}`;
5458
6247
  if (resolved === root || !resolved.startsWith(prefix)) return;
5459
6248
  return resolved;
5460
6249
  }
5461
- function listItem(href, label) {
5462
- return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
6250
+ function listItem$1(href, label) {
6251
+ return `<li><a href="${escapeHtml$2(href)}">${escapeHtml$2(label)}</a></li>`;
5463
6252
  }
5464
- function escapeHtml$1(value) {
6253
+ function escapeHtml$2(value) {
5465
6254
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
5466
6255
  }
5467
6256
  //#endregion
@@ -5474,7 +6263,7 @@ function escapeHtml$1(value) {
5474
6263
  */
5475
6264
  const DEFAULT_TAXONOMIES = ["tags", "categories"];
5476
6265
  const DEFAULT_RELATED_LIMIT = 5;
5477
- const HOSTILE_TERM = /^(?:javascript|data):/i;
6266
+ const HOSTILE_TERM$1 = /^(?:javascript|data):/i;
5478
6267
  /**
5479
6268
  * Resolves `taxonomies` with defaults.
5480
6269
  *
@@ -5505,7 +6294,7 @@ function resolveTaxonomiesOptions(value) {
5505
6294
  */
5506
6295
  function termSlug(term) {
5507
6296
  const trimmed = term.trim();
5508
- if (!trimmed || HOSTILE_TERM.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
6297
+ if (!trimmed || HOSTILE_TERM$1.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
5509
6298
  return trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || void 0;
5510
6299
  }
5511
6300
  /** Appends related-page HTML to source pages that share a listed term. */
@@ -5563,8 +6352,8 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5563
6352
  for (const taxonomy of options.taxonomies) {
5564
6353
  const urlName = taxonomy.toLowerCase();
5565
6354
  const terms = collectTerms(listed, taxonomy);
5566
- const listHref = siteHref$2(base, urlName);
5567
- const listOutput = containedPath(outDir, urlName, "index.html");
6355
+ const listHref = siteHref$3(base, urlName);
6356
+ const listOutput = containedPath$2(outDir, urlName, "index.html");
5568
6357
  if (listOutput) pages.push({
5569
6358
  title: displayTaxonomyName(urlName),
5570
6359
  content: listPageContent(terms, base, urlName),
@@ -5573,14 +6362,14 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5573
6362
  href: listHref
5574
6363
  });
5575
6364
  for (const term of terms) {
5576
- const outputPath = containedPath(outDir, urlName, term.slug, "index.html");
6365
+ const outputPath = containedPath$2(outDir, urlName, term.slug, "index.html");
5577
6366
  if (!outputPath) continue;
5578
6367
  pages.push({
5579
6368
  title: term.label,
5580
6369
  content: termPageContent(term),
5581
6370
  outputPath,
5582
6371
  urlPath: `${urlName}/${term.slug}`,
5583
- href: siteHref$2(base, urlName, term.slug)
6372
+ href: siteHref$3(base, urlName, term.slug)
5584
6373
  });
5585
6374
  }
5586
6375
  }
@@ -5588,7 +6377,7 @@ function taxonomyPageSpecs(listed, options, outDir, base) {
5588
6377
  }
5589
6378
  function collectTerms(listed, taxonomy) {
5590
6379
  const buckets = /* @__PURE__ */ new Map();
5591
- for (const page of listed) for (const label of termsFromValue(page.frontmatter[taxonomy])) {
6380
+ for (const page of listed) for (const label of termsFromValue$1(page.frontmatter[taxonomy])) {
5592
6381
  const slug = termSlug(label);
5593
6382
  if (!slug) continue;
5594
6383
  const existing = buckets.get(slug);
@@ -5603,13 +6392,13 @@ function collectTerms(listed, taxonomy) {
5603
6392
  }
5604
6393
  function pageTermKeys(page, taxonomies) {
5605
6394
  const keys = /* @__PURE__ */ new Set();
5606
- for (const taxonomy of taxonomies) for (const label of termsFromValue(page.frontmatter[taxonomy])) {
6395
+ for (const taxonomy of taxonomies) for (const label of termsFromValue$1(page.frontmatter[taxonomy])) {
5607
6396
  const slug = termSlug(label);
5608
6397
  if (slug) keys.add(`${taxonomy.toLowerCase()}\0${slug}`);
5609
6398
  }
5610
6399
  return keys;
5611
6400
  }
5612
- function termsFromValue(value) {
6401
+ function termsFromValue$1(value) {
5613
6402
  if (typeof value === "string") return value.trim() ? [value.trim()] : [];
5614
6403
  if (!Array.isArray(value)) return [];
5615
6404
  return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
@@ -5633,55 +6422,840 @@ function normalizeRelatedLimit(value) {
5633
6422
  if (typeof value === "number" && Number.isFinite(value) && value >= 0) return Math.floor(value);
5634
6423
  return DEFAULT_RELATED_LIMIT;
5635
6424
  }
5636
- function samePage(left, right) {
5637
- if (left.inputPath && right.inputPath) return left.inputPath === right.inputPath;
5638
- return left.routePaths.href === right.routePaths.href;
6425
+ function samePage(left, right) {
6426
+ if (left.inputPath && right.inputPath) return left.inputPath === right.inputPath;
6427
+ return left.routePaths.href === right.routePaths.href;
6428
+ }
6429
+ function sharedCount(left, right) {
6430
+ let count = 0;
6431
+ for (const key of left) if (right.has(key)) count += 1;
6432
+ return count;
6433
+ }
6434
+ //#endregion
6435
+ //#region src/team.ts
6436
+ /**
6437
+ * Resolves `ssg.team` with defaults.
6438
+ *
6439
+ * `false` / omitted stays off. `true` enables an empty member list.
6440
+ * An object enables the feature and keeps the members the site set.
6441
+ */
6442
+ function resolveTeamOptions(value) {
6443
+ if (!value) return {
6444
+ enabled: false,
6445
+ members: []
6446
+ };
6447
+ if (value === true) return {
6448
+ enabled: true,
6449
+ members: []
6450
+ };
6451
+ return {
6452
+ enabled: true,
6453
+ members: normalizeMembers(value.members)
6454
+ };
6455
+ }
6456
+ function normalizeMembers(members) {
6457
+ if (!Array.isArray(members)) return [];
6458
+ return members.flatMap((member) => {
6459
+ if (!member || typeof member.name !== "string") return [];
6460
+ const links = Array.isArray(member.links) ? member.links.flatMap((link) => {
6461
+ if (!link || typeof link.label !== "string" || typeof link.href !== "string") return [];
6462
+ return [{
6463
+ label: link.label,
6464
+ href: link.href
6465
+ }];
6466
+ }) : void 0;
6467
+ return [{
6468
+ name: member.name,
6469
+ role: typeof member.role === "string" ? member.role : void 0,
6470
+ avatar: typeof member.avatar === "string" ? member.avatar : void 0,
6471
+ links
6472
+ }];
6473
+ });
6474
+ }
6475
+ //#endregion
6476
+ //#region src/contributors.ts
6477
+ /**
6478
+ * Opt-in git contributor list helpers.
6479
+ *
6480
+ * Resolution and ignore filtering live here. `git log` is read in Rust
6481
+ * (`getGitContributors`) and names are rendered from `PageData.contributors`.
6482
+ */
6483
+ /**
6484
+ * Resolves `ssg.contributors` with defaults.
6485
+ *
6486
+ * `false` / omitted stays off. `true` enables names only.
6487
+ * An object enables the feature and keeps `ignore` / `avatars`.
6488
+ */
6489
+ function resolveContributorsOption(value) {
6490
+ if (!value) return false;
6491
+ if (value === true) return {
6492
+ ignore: [],
6493
+ avatars: false
6494
+ };
6495
+ return {
6496
+ ignore: Array.isArray(value.ignore) ? value.ignore.filter((entry) => typeof entry === "string") : [],
6497
+ avatars: value.avatars === true
6498
+ };
6499
+ }
6500
+ function filterGitContributors(contributors, ignore) {
6501
+ if (ignore.length === 0) return contributors.filter((contributor) => contributor.name.trim());
6502
+ const needles = new Set(ignore.map((entry) => entry.toLowerCase()));
6503
+ return contributors.filter((contributor) => {
6504
+ const name = contributor.name.trim();
6505
+ if (!name) return false;
6506
+ if (needles.has(name.toLowerCase())) return false;
6507
+ const email = contributor.email?.trim().toLowerCase();
6508
+ return !email || !needles.has(email);
6509
+ });
6510
+ }
6511
+ function gravatarAvatar(email) {
6512
+ return `https://www.gravatar.com/avatar/${createHash("md5").update(email.trim().toLowerCase()).digest("hex")}?d=mp&s=40`;
6513
+ }
6514
+ function applyContributorOptions(raw, option) {
6515
+ return filterGitContributors(raw, option.ignore).map((contributor) => ({
6516
+ name: contributor.name.trim(),
6517
+ avatar: option.avatars && contributor.email?.trim() ? gravatarAvatar(contributor.email) : void 0
6518
+ }));
6519
+ }
6520
+ //#endregion
6521
+ //#region src/blog-options.ts
6522
+ const DEFAULT_PAGE_SIZE = 10;
6523
+ function resolveBlogOptions(value) {
6524
+ if (!value) return {
6525
+ enabled: false,
6526
+ authors: {},
6527
+ pageSize: DEFAULT_PAGE_SIZE
6528
+ };
6529
+ if (value === true) return {
6530
+ enabled: true,
6531
+ authors: {},
6532
+ pageSize: DEFAULT_PAGE_SIZE
6533
+ };
6534
+ return {
6535
+ enabled: true,
6536
+ collection: value.collection,
6537
+ authors: normalizeAuthors(value.authors),
6538
+ pageSize: normalizePageSize(value.pageSize)
6539
+ };
6540
+ }
6541
+ /**
6542
+ * Picks a collection named `blog`, else the only configured collection.
6543
+ *
6544
+ * An explicit name always wins. Several collections and no `blog` name
6545
+ * require `blog.collection`.
6546
+ */
6547
+ function resolveBlogCollectionName(requested, collectionNames) {
6548
+ if (requested) return requested;
6549
+ if (collectionNames.includes("blog")) return "blog";
6550
+ if (collectionNames.length === 1) return collectionNames[0];
6551
+ }
6552
+ function normalizeAuthors(authors) {
6553
+ if (!authors || typeof authors !== "object") return {};
6554
+ const resolved = {};
6555
+ for (const [key, value] of Object.entries(authors)) {
6556
+ if (!value || typeof value.name !== "string") continue;
6557
+ resolved[key] = {
6558
+ name: value.name,
6559
+ bio: typeof value.bio === "string" ? value.bio : void 0,
6560
+ url: typeof value.url === "string" ? value.url : void 0
6561
+ };
6562
+ }
6563
+ return resolved;
6564
+ }
6565
+ function normalizePageSize(value) {
6566
+ if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
6567
+ return DEFAULT_PAGE_SIZE;
6568
+ }
6569
+ //#endregion
6570
+ //#region src/blog-reading.ts
6571
+ /**
6572
+ * Deterministic blog reading-time estimates.
6573
+ */
6574
+ const LATIN_WORDS_PER_MINUTE = 200;
6575
+ const CJK_CHARS_PER_MINUTE = 500;
6576
+ function readingTimeMinutes(markdown) {
6577
+ const body = stripInlineCode(stripFences(stripFrontmatter(markdown)));
6578
+ let latin = 0;
6579
+ let cjk = 0;
6580
+ let latinRun = false;
6581
+ for (const char of body) {
6582
+ const code = char.codePointAt(0) ?? 0;
6583
+ if (isCjkCodePoint(code)) {
6584
+ cjk += 1;
6585
+ latinRun = false;
6586
+ continue;
6587
+ }
6588
+ if (isLatinWordChar(code)) {
6589
+ if (!latinRun) {
6590
+ latin += 1;
6591
+ latinRun = true;
6592
+ }
6593
+ continue;
6594
+ }
6595
+ if (char === "'" || char === "’") continue;
6596
+ latinRun = false;
6597
+ }
6598
+ if (latin === 0 && cjk === 0) return 0;
6599
+ return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
6600
+ }
6601
+ function stripFrontmatter(markdown) {
6602
+ if (!markdown.startsWith("---")) return markdown;
6603
+ const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
6604
+ return match ? markdown.slice(match[0].length) : markdown;
6605
+ }
6606
+ function stripFences(text) {
6607
+ return text.replace(/```[\s\S]*?(?:```|$)/g, " ");
6608
+ }
6609
+ function stripInlineCode(text) {
6610
+ return text.replace(/`[^`\n]*`/g, " ");
6611
+ }
6612
+ function isCjkCodePoint(code) {
6613
+ 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;
6614
+ }
6615
+ function isLatinWordChar(code) {
6616
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
6617
+ }
6618
+ //#endregion
6619
+ //#region src/blog-html.ts
6620
+ /**
6621
+ * Escaped blog index, tag, archive, and post-meta HTML.
6622
+ */
6623
+ /** `https:` or a same-origin path starting with `/` but not `//`. */
6624
+ function isSafeBlogUrl(value) {
6625
+ const trimmed = value.trim();
6626
+ if (trimmed.length === 0 || trimmed.split("").some((ch) => ch === "\n" || ch === "\r" || ch === "\0" || ch === " ")) return false;
6627
+ if (trimmed.startsWith("//")) return false;
6628
+ if (trimmed.startsWith("/")) return true;
6629
+ return trimmed.toLowerCase().startsWith("https:");
6630
+ }
6631
+ function postMetaMarkup(meta) {
6632
+ const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml$1(String(meta.minutes))} min read</p>`];
6633
+ if (meta.authors.length > 0) {
6634
+ const items = meta.authors.map((author) => authorMarkup(author)).join("");
6635
+ parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
6636
+ }
6637
+ if (meta.tags.length > 0) {
6638
+ const items = meta.tags.map((tag) => `<li><a href="${escapeHtml$1(tag.href)}">${escapeHtml$1(tag.label)}</a></li>`).join("");
6639
+ parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
6640
+ }
6641
+ return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
6642
+ }
6643
+ function indexPageContent(items, pager) {
6644
+ const list = items.map((item) => listItem(item)).join("");
6645
+ const links = [];
6646
+ if (pager.newerHref) links.push(`<a href="${escapeHtml$1(pager.newerHref)}" rel="prev">Newer</a>`);
6647
+ if (pager.olderHref) links.push(`<a href="${escapeHtml$1(pager.olderHref)}" rel="next">Older</a>`);
6648
+ return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
6649
+ }
6650
+ function tagPageContent(label, items) {
6651
+ const list = items.map((item) => listItem(item)).join("");
6652
+ return `<h1>${escapeHtml$1(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
6653
+ }
6654
+ function archiveIndexContent(years) {
6655
+ 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>`;
6656
+ }
6657
+ function archiveYearContent(year, months, items) {
6658
+ const monthList = months.map((entry) => `<li><a href="${escapeHtml$1(entry.href)}">${escapeHtml$1(entry.month)}</a></li>`).join("");
6659
+ const posts = items.map((item) => listItem(item)).join("");
6660
+ return `<h1>${escapeHtml$1(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
6661
+ }
6662
+ function archiveMonthContent(label, items) {
6663
+ const list = items.map((item) => listItem(item)).join("");
6664
+ return `<h1>${escapeHtml$1(label)}</h1><ul class="ox-blog">${list}</ul>`;
6665
+ }
6666
+ function siteHref$2(base, ...segments) {
6667
+ const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
6668
+ const rest = segments.filter(Boolean).join("/");
6669
+ return rest ? `${prefix}${rest}/` : prefix;
6670
+ }
6671
+ function containedPath$1(outDir, ...segments) {
6672
+ const root = path$1.resolve(outDir);
6673
+ const resolved = path$1.resolve(root, ...segments);
6674
+ const prefix = root.endsWith(path$1.sep) ? root : `${root}${path$1.sep}`;
6675
+ if (resolved === root || !resolved.startsWith(prefix)) return;
6676
+ return resolved;
6677
+ }
6678
+ function escapeHtml$1(value) {
6679
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
6680
+ }
6681
+ function authorMarkup(author) {
6682
+ const name = escapeHtml$1(author.name);
6683
+ const url = author.url?.trim();
6684
+ 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>`;
6685
+ }
6686
+ function listItem(item) {
6687
+ const time = item.dateLabel ? ` <time datetime="${escapeHtml$1(item.dateLabel)}">${escapeHtml$1(item.dateLabel)}</time>` : "";
6688
+ return `<li><a href="${escapeHtml$1(item.href)}">${escapeHtml$1(item.title)}</a>${time}</li>`;
6689
+ }
6690
+ //#endregion
6691
+ //#region src/blog-posts.ts
6692
+ /**
6693
+ * Blog post selection, tags, authors, and dates.
6694
+ */
6695
+ const HOSTILE_TERM = /^(?:javascript|data):/i;
6696
+ function selectBlogPosts(listed, options, srcDir, collections) {
6697
+ if (isAmbiguousCollection(options, collections)) return;
6698
+ const names = collectionNames(collections);
6699
+ const name = resolveBlogCollectionName(options.collection, names);
6700
+ const sources = name && collections?.enabled ? collections.collections[name]?.source : void 0;
6701
+ return listed.filter((page) => {
6702
+ if (isExcludedPost(page.frontmatter)) return false;
6703
+ if (!sources) return true;
6704
+ return pageMatchesSources(page.inputPath, srcDir, sources);
6705
+ });
6706
+ }
6707
+ function isAmbiguousCollection(options, collections) {
6708
+ if (options.collection) return false;
6709
+ const names = collectionNames(collections);
6710
+ return names.length > 1 && !names.includes("blog");
6711
+ }
6712
+ function collectionNames(collections) {
6713
+ if (!collections?.enabled) return [];
6714
+ return Object.keys(collections.collections);
6715
+ }
6716
+ function pageMatchesSources(inputPath, srcDir, sources) {
6717
+ const relative = path$1.relative(srcDir, inputPath).split(path$1.sep).join("/");
6718
+ return sources.some((source) => matchGlob(relative, source));
6719
+ }
6720
+ function matchGlob(relative, pattern) {
6721
+ const normalized = pattern.replace(/^\/+/, "");
6722
+ let out = "^";
6723
+ for (let i = 0; i < normalized.length; i += 1) {
6724
+ if (normalized.startsWith("**/", i)) {
6725
+ out += "(?:.*/)?";
6726
+ i += 2;
6727
+ continue;
6728
+ }
6729
+ const ch = normalized[i] ?? "";
6730
+ if (ch === "*") {
6731
+ out += "[^/]*";
6732
+ continue;
6733
+ }
6734
+ if (ch === "?") {
6735
+ out += "[^/]";
6736
+ continue;
6737
+ }
6738
+ if (/[.+^${}()|[\]\\]/.test(ch)) {
6739
+ out += `\\${ch}`;
6740
+ continue;
6741
+ }
6742
+ out += ch;
6743
+ }
6744
+ out += "$";
6745
+ return new RegExp(out).test(relative);
6746
+ }
6747
+ function sortPosts(posts) {
6748
+ return [...posts].sort((left, right) => {
6749
+ const dateCmp = (pageUnix(right.frontmatter) ?? Number.NEGATIVE_INFINITY) - (pageUnix(left.frontmatter) ?? Number.NEGATIVE_INFINITY);
6750
+ if (dateCmp !== 0) return dateCmp;
6751
+ return left.routePaths.href < right.routePaths.href ? -1 : left.routePaths.href > right.routePaths.href ? 1 : 0;
6752
+ });
6753
+ }
6754
+ function collectTags(posts) {
6755
+ const buckets = /* @__PURE__ */ new Map();
6756
+ for (const page of posts) for (const label of termsFromValue(page.frontmatter.tags)) {
6757
+ const slug = tagSlug(label);
6758
+ if (!slug) continue;
6759
+ const existing = buckets.get(slug);
6760
+ if (existing) existing.pages.push(page);
6761
+ else buckets.set(slug, {
6762
+ label,
6763
+ slug,
6764
+ pages: [page]
6765
+ });
6766
+ }
6767
+ return [...buckets.values()].sort((left, right) => left.label.localeCompare(right.label));
6768
+ }
6769
+ function datedPosts(posts) {
6770
+ const dated = [];
6771
+ for (const page of posts) {
6772
+ const parsed = pageDate(page.frontmatter);
6773
+ if (!parsed) continue;
6774
+ dated.push({
6775
+ page,
6776
+ year: String(parsed.year).padStart(4, "0"),
6777
+ month: String(parsed.month).padStart(2, "0"),
6778
+ label: `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}`
6779
+ });
6780
+ }
6781
+ return dated;
6782
+ }
6783
+ function uniqueYears(dated) {
6784
+ return [...new Set(dated.map((entry) => entry.year))].sort((left, right) => right.localeCompare(left));
6785
+ }
6786
+ function uniqueMonths(dated) {
6787
+ return [...new Set(dated.map((entry) => entry.month))].sort((left, right) => left.localeCompare(right));
6788
+ }
6789
+ function toListItem(page) {
6790
+ const parsed = pageDate(page.frontmatter);
6791
+ return {
6792
+ title: page.title,
6793
+ href: page.routePaths.href,
6794
+ dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0
6795
+ };
6796
+ }
6797
+ function resolvePostAuthors(frontmatter, map) {
6798
+ const seen = /* @__PURE__ */ new Set();
6799
+ const authors = [];
6800
+ for (const key of authorKeys(frontmatter)) {
6801
+ if (seen.has(key)) continue;
6802
+ seen.add(key);
6803
+ authors.push(map[key] ?? { name: key });
6804
+ }
6805
+ return authors;
6806
+ }
6807
+ function authorKeys(frontmatter) {
6808
+ return [...keysFromValue(frontmatter.author), ...keysFromValue(frontmatter.authors)];
6809
+ }
6810
+ function keysFromValue(value) {
6811
+ if (typeof value === "string") return value.trim() ? [value.trim()] : [];
6812
+ if (!Array.isArray(value)) return [];
6813
+ return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
6814
+ }
6815
+ function postTagLinks(frontmatter, base) {
6816
+ const links = [];
6817
+ const seen = /* @__PURE__ */ new Set();
6818
+ for (const label of termsFromValue(frontmatter.tags)) {
6819
+ const slug = tagSlug(label);
6820
+ if (!slug || seen.has(slug)) continue;
6821
+ seen.add(slug);
6822
+ links.push({
6823
+ label,
6824
+ href: siteHref$2(base, "blog", "tags", slug)
6825
+ });
6826
+ }
6827
+ return links;
6828
+ }
6829
+ function tagSlug(term) {
6830
+ const trimmed = term.trim();
6831
+ if (!trimmed || HOSTILE_TERM.test(trimmed) || trimmed.includes("..") || trimmed.includes("//")) return;
6832
+ return trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || void 0;
6833
+ }
6834
+ function termsFromValue(value) {
6835
+ if (typeof value === "string") return value.trim() ? [value.trim()] : [];
6836
+ if (!Array.isArray(value)) return [];
6837
+ return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
6838
+ }
6839
+ function isExcludedPost(frontmatter) {
6840
+ return frontmatter.draft === true || frontmatter.unlisted === true;
6841
+ }
6842
+ function pageDate(frontmatter) {
6843
+ return parseDate(dateField(frontmatter.date));
6844
+ }
6845
+ function pageUnix(frontmatter) {
6846
+ return pageDate(frontmatter)?.unix;
6847
+ }
6848
+ function dateField(value) {
6849
+ if (typeof value === "string" && value.trim()) return value.trim();
6850
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
6851
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
6852
+ }
6853
+ //#endregion
6854
+ //#region src/blog-pages.ts
6855
+ /**
6856
+ * Generated blog index, tag, and archive pages.
6857
+ */
6858
+ const AMBIGUOUS_COLLECTION = "[ox-content] blog is enabled but multiple collections are configured; set blog.collection";
6859
+ async function injectBlogPostMeta(input) {
6860
+ if (!input.options?.enabled) return;
6861
+ const posts = selectBlogPosts(input.listed, input.options, input.srcDir, input.collections);
6862
+ if (posts === void 0) return;
6863
+ const listedPaths = new Set(posts.map((page) => page.inputPath));
6864
+ for (const page of input.pages) {
6865
+ if (!listedPaths.has(page.inputPath)) continue;
6866
+ const markdown = await readMarkdown(page.inputPath);
6867
+ page.transformedHtml = postMetaMarkup({
6868
+ authors: resolvePostAuthors(page.frontmatter, input.options.authors),
6869
+ minutes: readingTimeMinutes(markdown),
6870
+ tags: postTagLinks(page.frontmatter, input.base)
6871
+ }) + page.transformedHtml;
6872
+ }
6873
+ }
6874
+ /** Maps a generated blog page onto the SSG render shape. */
6875
+ function toBlogProcessResult(page) {
6876
+ return {
6877
+ inputPath: page.outputPath,
6878
+ routePaths: {
6879
+ outputPath: page.outputPath,
6880
+ urlPath: page.urlPath,
6881
+ href: page.href,
6882
+ ogImagePath: "",
6883
+ ogImageUrl: ""
6884
+ },
6885
+ transformedHtml: page.content,
6886
+ title: page.title,
6887
+ frontmatter: {},
6888
+ toc: []
6889
+ };
6890
+ }
6891
+ /** Renders index, tag, and archive pages and appends them to the build. */
6892
+ async function appendBlogPages(input) {
6893
+ if (!input.options?.enabled) return;
6894
+ if (isAmbiguousCollection(input.options, input.collections)) {
6895
+ input.errors.push(AMBIGUOUS_COLLECTION);
6896
+ return;
6897
+ }
6898
+ const posts = selectBlogPosts(input.listedPages, input.options, input.srcDir, input.collections);
6899
+ if (posts === void 0) return;
6900
+ for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
6901
+ input.generatedPages.push({
6902
+ inputPath: spec.outputPath,
6903
+ outputPath: spec.outputPath,
6904
+ html: await input.render(spec)
6905
+ });
6906
+ } catch (err) {
6907
+ const message = err instanceof Error ? err.message : String(err);
6908
+ input.errors.push(`Failed to generate blog page ${spec.href}: ${message}`);
6909
+ }
6910
+ }
6911
+ function blogPageSpecs(posts, options, outDir, base) {
6912
+ const sorted = sortPosts(posts);
6913
+ const pages = [];
6914
+ const pageSize = options.pageSize;
6915
+ const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize) || 1);
6916
+ const totalPages = sorted.length === 0 ? 1 : pageCount;
6917
+ for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
6918
+ const slice = sorted.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
6919
+ const isFirst = pageNumber === 1;
6920
+ const urlPath = isFirst ? "blog" : `blog/page/${pageNumber}`;
6921
+ const outputPath = isFirst ? containedPath$1(outDir, "blog", "index.html") : containedPath$1(outDir, "blog", "page", String(pageNumber), "index.html");
6922
+ if (!outputPath) continue;
6923
+ pages.push({
6924
+ title: isFirst ? "Blog" : `Blog · page ${pageNumber}`,
6925
+ content: indexPageContent(slice.map(toListItem), {
6926
+ newerHref: isFirst ? void 0 : siteHref$2(base, ...pageNumber === 2 ? ["blog"] : [
6927
+ "blog",
6928
+ "page",
6929
+ String(pageNumber - 1)
6930
+ ]),
6931
+ olderHref: pageNumber < totalPages ? siteHref$2(base, "blog", "page", String(pageNumber + 1)) : void 0
6932
+ }),
6933
+ outputPath,
6934
+ urlPath,
6935
+ href: siteHref$2(base, ...urlPath.split("/"))
6936
+ });
6937
+ }
6938
+ const tags = collectTags(sorted);
6939
+ for (const tag of tags) {
6940
+ const outputPath = containedPath$1(outDir, "blog", "tags", tag.slug, "index.html");
6941
+ if (!outputPath) continue;
6942
+ pages.push({
6943
+ title: tag.label,
6944
+ content: tagPageContent(tag.label, tag.pages.map(toListItem)),
6945
+ outputPath,
6946
+ urlPath: `blog/tags/${tag.slug}`,
6947
+ href: siteHref$2(base, "blog", "tags", tag.slug)
6948
+ });
6949
+ }
6950
+ const dated = datedPosts(sorted);
6951
+ if (dated.length > 0) {
6952
+ const years = uniqueYears(dated);
6953
+ const archiveIndex = containedPath$1(outDir, "blog", "archive", "index.html");
6954
+ if (archiveIndex) pages.push({
6955
+ title: "Archive",
6956
+ content: archiveIndexContent(years.map((year) => ({
6957
+ year,
6958
+ href: siteHref$2(base, "blog", "archive", year)
6959
+ }))),
6960
+ outputPath: archiveIndex,
6961
+ urlPath: "blog/archive",
6962
+ href: siteHref$2(base, "blog", "archive")
6963
+ });
6964
+ for (const year of years) {
6965
+ const yearPosts = dated.filter((entry) => entry.year === year);
6966
+ const months = uniqueMonths(yearPosts);
6967
+ const yearPath = containedPath$1(outDir, "blog", "archive", year, "index.html");
6968
+ if (yearPath) pages.push({
6969
+ title: year,
6970
+ content: archiveYearContent(year, months.map((month) => ({
6971
+ month: `${year}-${month}`,
6972
+ href: siteHref$2(base, "blog", "archive", year, month)
6973
+ })), yearPosts.map((entry) => toListItem(entry.page))),
6974
+ outputPath: yearPath,
6975
+ urlPath: `blog/archive/${year}`,
6976
+ href: siteHref$2(base, "blog", "archive", year)
6977
+ });
6978
+ for (const month of months) {
6979
+ const monthPosts = yearPosts.filter((entry) => entry.month === month);
6980
+ const monthPath = containedPath$1(outDir, "blog", "archive", year, month, "index.html");
6981
+ if (!monthPath) continue;
6982
+ pages.push({
6983
+ title: `${year}-${month}`,
6984
+ content: archiveMonthContent(`${year}-${month}`, monthPosts.map((entry) => toListItem(entry.page))),
6985
+ outputPath: monthPath,
6986
+ urlPath: `blog/archive/${year}/${month}`,
6987
+ href: siteHref$2(base, "blog", "archive", year, month)
6988
+ });
6989
+ }
6990
+ }
6991
+ }
6992
+ return pages;
6993
+ }
6994
+ async function readMarkdown(inputPath) {
6995
+ try {
6996
+ return await fs$2.readFile(inputPath, "utf8");
6997
+ } catch {
6998
+ return "";
6999
+ }
7000
+ }
7001
+ //#endregion
7002
+ //#region src/section-index-html.ts
7003
+ /**
7004
+ * Section-index listing HTML and href safety.
7005
+ *
7006
+ * Titles are escaped. `javascript:` / `data:` / `vbscript:` / `file:` hrefs
7007
+ * are dropped. The NAPI helper is preferred when present.
7008
+ */
7009
+ const HOSTILE_SCHEME = /^(?:javascript|data|vbscript|file):/i;
7010
+ /** `https:`-free, same-origin or relative href. `javascript:` is rejected. */
7011
+ function isSafeSectionHref(value) {
7012
+ const trimmed = value.trim();
7013
+ if (!trimmed || /[\n\r\0\t]/.test(trimmed) || trimmed.startsWith("//")) return false;
7014
+ if (trimmed.startsWith("/")) return true;
7015
+ if (trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)) return false;
7016
+ return !HOSTILE_SCHEME.test(trimmed);
7017
+ }
7018
+ /** Escapes text and attribute values in generated listing markup. */
7019
+ function escapeSectionIndexHtml(value) {
7020
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
7021
+ }
7022
+ /** Renders the listing body. Titles are escaped; hostile hrefs are dropped. */
7023
+ function renderSectionIndexHtml(title, items, style) {
7024
+ try {
7025
+ const napi = importNapiModuleSync();
7026
+ if (typeof napi.renderSsgSectionIndex === "function") return napi.renderSsgSectionIndex(title, items.map((item) => ({
7027
+ title: item.title,
7028
+ href: item.href,
7029
+ description: item.description
7030
+ })), style);
7031
+ } catch {}
7032
+ return renderSectionIndexHtmlLocal(title, items, style);
7033
+ }
7034
+ function renderSectionIndexHtmlLocal(title, items, style) {
7035
+ const safe = items.filter((item) => isSafeSectionHref(item.href));
7036
+ const modifier = style === "list" ? "list" : "cards";
7037
+ const listClass = style === "list" ? "ox-section-index__list" : "ox-section-index__cards";
7038
+ const body = safe.map((item) => renderItem(item, style)).join("");
7039
+ return `<nav class="ox-section-index ox-section-index--${modifier}" aria-label="Section pages"><h1>${escapeSectionIndexHtml(title)}</h1><ul class="${listClass}">${body}</ul></nav>`;
7040
+ }
7041
+ function renderItem(item, style) {
7042
+ const href = escapeSectionIndexHtml(item.href.trim());
7043
+ const label = escapeSectionIndexHtml(item.title);
7044
+ if (style === "list") return `<li><a href="${href}">${label}</a></li>`;
7045
+ 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>`;
7046
+ }
7047
+ //#endregion
7048
+ //#region src/section-index-paths.ts
7049
+ /**
7050
+ * Section-index URL, title, and output-path helpers.
7051
+ */
7052
+ function pageTitle(page) {
7053
+ if (page.title.trim()) return page.title;
7054
+ return formatSectionTitle(path$1.basename(page.inputPath ?? page.routePaths.urlPath).replace(/\.[^.]+$/, "") || page.routePaths.urlPath);
7055
+ }
7056
+ function sectionTitle(dir) {
7057
+ if (!dir) return "Home";
7058
+ return formatSectionTitle(dir.slice(dir.lastIndexOf("/") + 1));
7059
+ }
7060
+ function formatSectionTitle(name) {
7061
+ try {
7062
+ return importNapiModuleSync().formatSsgTitle(name);
7063
+ } catch {
7064
+ if (!name) return "Untitled";
7065
+ return name.charAt(0).toUpperCase() + name.slice(1).replace(/[-_]+/g, " ");
7066
+ }
7067
+ }
7068
+ function normalizeUrlPath(urlPath) {
7069
+ if (!urlPath || urlPath === "/") return "";
7070
+ return urlPath.replace(/^\/+|\/+$/g, "");
7071
+ }
7072
+ function parentDir(urlPath) {
7073
+ const normalized = normalizeUrlPath(urlPath);
7074
+ if (!normalized) return null;
7075
+ const index = normalized.lastIndexOf("/");
7076
+ return index === -1 ? "" : normalized.slice(0, index);
7077
+ }
7078
+ function firstChildDir(urlPath, parent) {
7079
+ const normalized = normalizeUrlPath(urlPath);
7080
+ if (!normalized) return;
7081
+ if (!parent) {
7082
+ const slash = normalized.indexOf("/");
7083
+ return slash === -1 ? void 0 : normalized.slice(0, slash);
7084
+ }
7085
+ const prefix = `${parent}/`;
7086
+ if (!normalized.startsWith(prefix) || normalized === parent) return;
7087
+ const rest = normalized.slice(prefix.length);
7088
+ const slash = rest.indexOf("/");
7089
+ return slash === -1 ? void 0 : `${parent}/${rest.slice(0, slash)}`;
7090
+ }
7091
+ function sectionHref(base, dir, extension) {
7092
+ const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
7093
+ const ext = extension.startsWith(".") ? extension : `.${extension}`;
7094
+ return dir ? `${prefix}${dir}/index${ext}` : `${prefix}index${ext}`;
7095
+ }
7096
+ function sectionOutputPath(outDir, dir, extension) {
7097
+ const ext = extension.startsWith(".") ? extension : `.${extension}`;
7098
+ return containedPath(outDir, ...dir ? [...dir.split("/").filter(Boolean), `index${ext}`] : [`index${ext}`]);
7099
+ }
7100
+ function dirFromOutputPath(outputPath, outDir) {
7101
+ return path$1.relative(path$1.resolve(outDir), path$1.resolve(outputPath)).replaceAll(path$1.sep, "/").replace(/\/index\.[^/]+$/u, "").replace(/^index\.[^/]+$/u, "").replace(/^\/+|\/+$/g, "");
7102
+ }
7103
+ function containedPath(outDir, ...segments) {
7104
+ const root = path$1.resolve(outDir);
7105
+ const resolved = path$1.resolve(root, ...segments);
7106
+ const prefix = root.endsWith(path$1.sep) ? root : `${root}${path$1.sep}`;
7107
+ if (resolved !== root && !resolved.startsWith(prefix)) return;
7108
+ if (segments.some((segment) => segment === ".." || segment.includes("\0"))) return;
7109
+ return resolved;
7110
+ }
7111
+ //#endregion
7112
+ //#region src/section-index.ts
7113
+ /**
7114
+ * Opt-in generated section index pages.
7115
+ *
7116
+ * Resolution and directory walking live here. Listing HTML is rendered in
7117
+ * Rust (`ox_content_ssg::render_section_index`) when the NAPI helper is
7118
+ * available; a matching TypeScript renderer covers the same escape / href
7119
+ * rules so the SSG path stays safe either way. The Vite plugin appends
7120
+ * themed HTML during SSG and never overwrites an existing index page.
7121
+ */
7122
+ /**
7123
+ * Resolves `ssg.sectionIndex` with defaults.
7124
+ *
7125
+ * `false` / omitted stays off. `true` enables card listings. An object
7126
+ * enables the feature and overrides only the fields the site set.
7127
+ */
7128
+ function resolveSectionIndexOptions(value) {
7129
+ if (!value) return {
7130
+ enabled: false,
7131
+ style: "cards"
7132
+ };
7133
+ if (value === true) return {
7134
+ enabled: true,
7135
+ style: "cards"
7136
+ };
7137
+ return {
7138
+ enabled: true,
7139
+ style: value.style === "list" ? "list" : "cards"
7140
+ };
7141
+ }
7142
+ /** Maps a generated section index onto the SSG render shape. */
7143
+ function toSectionIndexProcessResult(page) {
7144
+ return {
7145
+ inputPath: page.outputPath,
7146
+ routePaths: {
7147
+ outputPath: page.outputPath,
7148
+ urlPath: page.urlPath,
7149
+ href: page.href,
7150
+ ogImagePath: "",
7151
+ ogImageUrl: ""
7152
+ },
7153
+ transformedHtml: page.content,
7154
+ title: page.title,
7155
+ frontmatter: {},
7156
+ toc: []
7157
+ };
7158
+ }
7159
+ /** Appends generated section indexes for directories that have no real index. */
7160
+ async function appendSectionIndexPages(input) {
7161
+ if (!input.options?.enabled) return;
7162
+ const existingOutputs = new Set(input.generatedPages.map((page) => path$1.normalize(page.outputPath)));
7163
+ for (const spec of sectionIndexSpecs(input.collectedPages, input.listedPages, input.options, input.outDir, input.base, input.extension)) {
7164
+ if (existingOutputs.has(path$1.normalize(spec.outputPath))) continue;
7165
+ try {
7166
+ const html = await input.render(spec);
7167
+ input.generatedPages.push({
7168
+ inputPath: spec.outputPath,
7169
+ outputPath: spec.outputPath,
7170
+ html
7171
+ });
7172
+ existingOutputs.add(path$1.normalize(spec.outputPath));
7173
+ } catch (err) {
7174
+ const message = err instanceof Error ? err.message : String(err);
7175
+ input.errors.push(`Failed to generate section index ${spec.href}: ${message}`);
7176
+ }
7177
+ }
7178
+ }
7179
+ function sectionIndexSpecs(collected, listed, options, outDir, base, extension) {
7180
+ const occupied = /* @__PURE__ */ new Set();
7181
+ for (const page of collected) occupied.add(normalizeUrlPath(page.routePaths.urlPath));
7182
+ for (const page of collected) {
7183
+ const output = page.routePaths.outputPath;
7184
+ if (output) occupied.add(dirFromOutputPath(output, outDir));
7185
+ }
7186
+ const visible = listed.filter((page) => !isHiddenByFlags(page.frontmatter));
7187
+ const childrenByDir = /* @__PURE__ */ new Map();
7188
+ for (const page of visible) {
7189
+ const urlPath = normalizeUrlPath(page.routePaths.urlPath);
7190
+ const parent = parentDir(urlPath);
7191
+ if (parent === null) continue;
7192
+ pushChild(childrenByDir, parent, {
7193
+ title: pageTitle(page),
7194
+ href: page.routePaths.href,
7195
+ description: page.description
7196
+ });
7197
+ let ancestor = parent;
7198
+ while (ancestor !== "") {
7199
+ const grand = parentDir(ancestor);
7200
+ if (grand === null) break;
7201
+ const nested = firstChildDir(urlPath, grand);
7202
+ if (nested) pushUniqueDir(childrenByDir, grand, nested, visible, base, extension);
7203
+ ancestor = grand;
7204
+ }
7205
+ }
7206
+ const pages = [];
7207
+ const dirs = [...childrenByDir.keys()].sort();
7208
+ for (const dir of dirs) {
7209
+ if (occupied.has(dir)) continue;
7210
+ const children = uniqueItems(childrenByDir.get(dir) ?? []).filter((item) => isSafeSectionHref(item.href));
7211
+ if (children.length === 0) continue;
7212
+ children.sort((left, right) => {
7213
+ const titleCmp = left.title.localeCompare(right.title);
7214
+ return titleCmp !== 0 ? titleCmp : left.href.localeCompare(right.href);
7215
+ });
7216
+ const outputPath = sectionOutputPath(outDir, dir, extension);
7217
+ if (!outputPath) continue;
7218
+ const title = sectionTitle(dir);
7219
+ pages.push({
7220
+ title,
7221
+ content: renderSectionIndexHtml(title, children, options.style),
7222
+ outputPath,
7223
+ urlPath: dir || "/",
7224
+ href: sectionHref(base, dir, extension)
7225
+ });
7226
+ }
7227
+ return pages;
7228
+ }
7229
+ function pushChild(map, dir, item) {
7230
+ const list = map.get(dir);
7231
+ if (list) {
7232
+ list.push(item);
7233
+ return;
7234
+ }
7235
+ map.set(dir, [item]);
5639
7236
  }
5640
- function sharedCount(left, right) {
5641
- let count = 0;
5642
- for (const key of left) if (right.has(key)) count += 1;
5643
- return count;
7237
+ function pushUniqueDir(map, parent, childDir, visible, base, extension) {
7238
+ const href = sectionHref(base, childDir, extension);
7239
+ if (map.get(parent)?.some((item) => item.href === href)) return;
7240
+ const indexPage = visible.find((page) => normalizeUrlPath(page.routePaths.urlPath) === childDir);
7241
+ pushChild(map, parent, {
7242
+ title: indexPage ? pageTitle(indexPage) : sectionTitle(childDir),
7243
+ href: indexPage?.routePaths.href ?? href,
7244
+ description: indexPage?.description
7245
+ });
5644
7246
  }
5645
- //#endregion
5646
- //#region src/team.ts
5647
- /**
5648
- * Resolves `ssg.team` with defaults.
5649
- *
5650
- * `false` / omitted stays off. `true` enables an empty member list.
5651
- * An object enables the feature and keeps the members the site set.
5652
- */
5653
- function resolveTeamOptions(value) {
5654
- if (!value) return {
5655
- enabled: false,
5656
- members: []
5657
- };
5658
- if (value === true) return {
5659
- enabled: true,
5660
- members: []
5661
- };
5662
- return {
5663
- enabled: true,
5664
- members: normalizeMembers(value.members)
5665
- };
7247
+ function uniqueItems(items) {
7248
+ const seen = /* @__PURE__ */ new Set();
7249
+ const unique = [];
7250
+ for (const item of items) {
7251
+ if (seen.has(item.href)) continue;
7252
+ seen.add(item.href);
7253
+ unique.push(item);
7254
+ }
7255
+ return unique;
5666
7256
  }
5667
- function normalizeMembers(members) {
5668
- if (!Array.isArray(members)) return [];
5669
- return members.flatMap((member) => {
5670
- if (!member || typeof member.name !== "string") return [];
5671
- const links = Array.isArray(member.links) ? member.links.flatMap((link) => {
5672
- if (!link || typeof link.label !== "string" || typeof link.href !== "string") return [];
5673
- return [{
5674
- label: link.label,
5675
- href: link.href
5676
- }];
5677
- }) : void 0;
5678
- return [{
5679
- name: member.name,
5680
- role: typeof member.role === "string" ? member.role : void 0,
5681
- avatar: typeof member.avatar === "string" ? member.avatar : void 0,
5682
- links
5683
- }];
5684
- });
7257
+ function isHiddenByFlags(frontmatter) {
7258
+ return frontmatter.draft === true || frontmatter.unlisted === true;
5685
7259
  }
5686
7260
  //#endregion
5687
7261
  //#region src/search-provider.ts
@@ -6077,93 +7651,1419 @@ function versionLocation(outputPath, outDir, options) {
6077
7651
  };
6078
7652
  }
6079
7653
  return {
6080
- id: options.current,
6081
- sibling: normalized
7654
+ id: options.current,
7655
+ sibling: normalized
7656
+ };
7657
+ }
7658
+ function outputToHref(outputPath, outDir, base) {
7659
+ return siteHref$1(base, "", relativeUrl(outputPath, outDir));
7660
+ }
7661
+ /** Applies switcher / banner / search rewrite after every version tree is generated. */
7662
+ function decorateVersionedPages(pages, options, outDir, base) {
7663
+ if (!options.enabled) return;
7664
+ const existingHrefs = new Set(pages.map((page) => outputToHref(page.outputPath, outDir, base)));
7665
+ for (const page of pages) {
7666
+ const { id, sibling } = versionLocation(page.outputPath, outDir, options);
7667
+ page.html = applyVersionChrome(page.html, options, id, sibling, base, existingHrefs);
7668
+ }
7669
+ }
7670
+ async function writeSnapshotSearchIndex(input) {
7671
+ const prefix = sanitizePrefix(input.prefix);
7672
+ if (!prefix) return;
7673
+ const destDir = path$1.join(input.outDir, prefix);
7674
+ const prefixBase = searchIndexUrl(input.base, prefix).replace(/search-index\.json$/, "");
7675
+ const json = await buildSearchIndex(input.srcDir, prefixBase, input.extensions, input.publishState, [], input.mdx);
7676
+ await fs$2.mkdir(destDir, { recursive: true });
7677
+ await writeSearchIndex(json, destDir);
7678
+ const dest = path$1.join(destDir, "search-index.json");
7679
+ try {
7680
+ await fs$2.access(dest);
7681
+ } catch {
7682
+ await fs$2.writeFile(dest, json, "utf8");
7683
+ }
7684
+ return dest;
7685
+ }
7686
+ function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
7687
+ if (!options.enabled) return html;
7688
+ const active = options.entries.find((entry) => entry.id === activeId);
7689
+ 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 ?? ""));
7690
+ }
7691
+ function sanitizePrefix(prefix) {
7692
+ const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
7693
+ if (!trimmed) return "";
7694
+ return PREFIX_RE.test(trimmed) && !trimmed.includes("..") ? trimmed : "";
7695
+ }
7696
+ function defaultCurrentEntry() {
7697
+ return {
7698
+ id: DEFAULT_CURRENT_ID,
7699
+ label: "Latest",
7700
+ prefix: "",
7701
+ banner: false
7702
+ };
7703
+ }
7704
+ function normalizeEntries(entries) {
7705
+ if (!entries) return [];
7706
+ const seen = /* @__PURE__ */ new Set();
7707
+ const resolved = [];
7708
+ for (const entry of entries) {
7709
+ if (!entry || typeof entry.id !== "string" || typeof entry.label !== "string") continue;
7710
+ const id = entry.id.trim();
7711
+ const label = entry.label.trim();
7712
+ if (!id || !label || seen.has(id)) continue;
7713
+ const prefix = sanitizePrefix(typeof entry.prefix === "string" ? entry.prefix : "");
7714
+ if (entry.prefix && !prefix) continue;
7715
+ const dir = typeof entry.dir === "string" && entry.dir.trim() ? entry.dir.trim() : void 0;
7716
+ if (dir && (dir.includes("\0") || dir.includes(".."))) continue;
7717
+ seen.add(id);
7718
+ resolved.push({
7719
+ id,
7720
+ label,
7721
+ prefix,
7722
+ dir,
7723
+ banner: normalizeBanner(entry.banner)
7724
+ });
7725
+ }
7726
+ return resolved;
7727
+ }
7728
+ function normalizeBanner(value) {
7729
+ return value === "unreleased" || value === "unmaintained" ? value : false;
7730
+ }
7731
+ function siteHref$1(base, prefix, rest) {
7732
+ const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
7733
+ const parts = [prefix, rest].filter((part) => part && part !== "/");
7734
+ return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
7735
+ }
7736
+ function relativeUrl(outputPath, outDir) {
7737
+ const rel = path$1.posix.normalize(path$1.relative(path$1.resolve(outDir), path$1.resolve(outputPath)).replaceAll(path$1.sep, "/"));
7738
+ if (rel.startsWith("..")) return "";
7739
+ const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
7740
+ return dir === "." ? "" : dir;
7741
+ }
7742
+ //#endregion
7743
+ //#region src/resources-jpeg.ts
7744
+ function encodeJpeg(image, quality = 80) {
7745
+ const yQuant = scaleQuant(LUM_QUANT, quality);
7746
+ const cQuant = scaleQuant(CHR_QUANT, quality);
7747
+ const width = image.width;
7748
+ const height = image.height;
7749
+ const duY = /* @__PURE__ */ new Int32Array(64);
7750
+ const duCb = /* @__PURE__ */ new Int32Array(64);
7751
+ const duCr = /* @__PURE__ */ new Int32Array(64);
7752
+ const bits = new BitWriter();
7753
+ let dcY = 0;
7754
+ let dcCb = 0;
7755
+ let dcCr = 0;
7756
+ for (let y = 0; y < height; y += 8) for (let x = 0; x < width; x += 8) {
7757
+ sampleBlock(image, x, y, duY, duCb, duCr);
7758
+ dcY = encodeBlock(bits, duY, yQuant, dcY, YDC, YAC);
7759
+ dcCb = encodeBlock(bits, duCb, cQuant, dcCb, CDC, CAC);
7760
+ dcCr = encodeBlock(bits, duCr, cQuant, dcCr, CDC, CAC);
7761
+ }
7762
+ bits.flush();
7763
+ return Buffer.concat([
7764
+ jpegHeader(width, height, yQuant, cQuant),
7765
+ bits.toBuffer(),
7766
+ Buffer.from([255, 217])
7767
+ ]);
7768
+ }
7769
+ function sampleBlock(image, left, top, yOut, cbOut, crOut) {
7770
+ for (let j = 0; j < 8; j++) {
7771
+ const y = Math.min(image.height - 1, top + j);
7772
+ for (let i = 0; i < 8; i++) {
7773
+ const x = Math.min(image.width - 1, left + i);
7774
+ const p = (y * image.width + x) * 4;
7775
+ const r = image.data[p] ?? 0;
7776
+ const g = image.data[p + 1] ?? 0;
7777
+ const b = image.data[p + 2] ?? 0;
7778
+ const idx = j * 8 + i;
7779
+ yOut[idx] = (66 * r + 129 * g + 25 * b + 128 >> 8) - 128;
7780
+ cbOut[idx] = -38 * r - 74 * g + 112 * b + 128 >> 8;
7781
+ crOut[idx] = 112 * r - 94 * g - 18 * b + 128 >> 8;
7782
+ }
7783
+ }
7784
+ }
7785
+ function encodeBlock(bits, block, quant, lastDc, dcTable, acTable) {
7786
+ const dct = forwardDct(block);
7787
+ const zz = /* @__PURE__ */ new Int32Array(64);
7788
+ for (let i = 0; i < 64; i++) zz[i] = Math.round(dct[ZIGZAG[i]] / quant[i]);
7789
+ const dc = zz[0] ?? 0;
7790
+ writeCoeff(bits, dc - lastDc, dcTable);
7791
+ let zeroRun = 0;
7792
+ for (let i = 1; i < 64; i++) {
7793
+ const value = zz[i] ?? 0;
7794
+ if (value === 0) {
7795
+ zeroRun++;
7796
+ continue;
7797
+ }
7798
+ while (zeroRun > 15) {
7799
+ writeCode(bits, acTable, 240);
7800
+ zeroRun -= 16;
7801
+ }
7802
+ writeCoeff(bits, value, acTable, zeroRun);
7803
+ zeroRun = 0;
7804
+ }
7805
+ if (zeroRun > 0) writeCode(bits, acTable, 0);
7806
+ return dc;
7807
+ }
7808
+ function writeCoeff(bits, value, table, run = 0) {
7809
+ const category = bitCategory(value);
7810
+ writeCode(bits, table, run << 4 | category);
7811
+ if (category > 0) bits.writeBits(value < 0 ? value + ((1 << category) - 1) : value, category);
7812
+ }
7813
+ function writeCode(bits, table, symbol) {
7814
+ const entry = table.get(symbol);
7815
+ if (!entry) throw new Error("missing Huffman code");
7816
+ bits.writeBits(entry.code, entry.len);
7817
+ }
7818
+ function bitCategory(value) {
7819
+ const abs = Math.abs(value);
7820
+ if (!Number.isFinite(abs) || abs === 0) return 0;
7821
+ return Math.min(11, Math.ceil(Math.log2(abs + 1)));
7822
+ }
7823
+ function forwardDct(block) {
7824
+ const out = /* @__PURE__ */ new Float64Array(64);
7825
+ for (let v = 0; v < 8; v++) for (let u = 0; u < 8; u++) {
7826
+ let sum = 0;
7827
+ 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);
7828
+ const cu = u === 0 ? Math.SQRT1_2 : 1;
7829
+ const cv = v === 0 ? Math.SQRT1_2 : 1;
7830
+ out[v * 8 + u] = .25 * cu * cv * sum;
7831
+ }
7832
+ return out;
7833
+ }
7834
+ function scaleQuant(base, quality) {
7835
+ const q = Math.max(1, Math.min(100, quality));
7836
+ const scale = q < 50 ? Math.floor(5e3 / q) : Math.floor(200 - q * 2);
7837
+ return base.map((value) => Math.max(1, Math.min(255, Math.floor((value * scale + 50) / 100))));
7838
+ }
7839
+ function jpegHeader(width, height, yQuant, cQuant) {
7840
+ const chunks = [
7841
+ Buffer.from([255, 216]),
7842
+ jfifApp0(),
7843
+ dqt(0, yQuant),
7844
+ dqt(1, cQuant),
7845
+ sof(width, height),
7846
+ dht(0, 0, STD_DC_LUM_NCODES, STD_DC_LUM_VALUES),
7847
+ dht(0, 1, STD_DC_CHR_NCODES, STD_DC_CHR_VALUES),
7848
+ dht(1, 0, STD_AC_LUM_NCODES, STD_AC_LUM_VALUES),
7849
+ dht(1, 1, STD_AC_CHR_NCODES, STD_AC_CHR_VALUES),
7850
+ sos()
7851
+ ];
7852
+ return Buffer.concat(chunks);
7853
+ }
7854
+ function jfifApp0() {
7855
+ return Buffer.from([
7856
+ 255,
7857
+ 224,
7858
+ 0,
7859
+ 16,
7860
+ 74,
7861
+ 70,
7862
+ 73,
7863
+ 70,
7864
+ 0,
7865
+ 1,
7866
+ 1,
7867
+ 0,
7868
+ 0,
7869
+ 1,
7870
+ 0,
7871
+ 1,
7872
+ 0,
7873
+ 0
7874
+ ]);
7875
+ }
7876
+ function dqt(id, table) {
7877
+ const out = Buffer.alloc(69);
7878
+ out[0] = 255;
7879
+ out[1] = 219;
7880
+ out.writeUInt16BE(67, 2);
7881
+ out[4] = id;
7882
+ for (let i = 0; i < 64; i++) out[5 + i] = table[i] ?? 1;
7883
+ return out;
7884
+ }
7885
+ function sof(width, height) {
7886
+ const out = Buffer.from([
7887
+ 255,
7888
+ 192,
7889
+ 0,
7890
+ 17,
7891
+ 8,
7892
+ 0,
7893
+ 0,
7894
+ 0,
7895
+ 0,
7896
+ 3,
7897
+ 1,
7898
+ 17,
7899
+ 0,
7900
+ 2,
7901
+ 17,
7902
+ 1,
7903
+ 3,
7904
+ 17,
7905
+ 1
7906
+ ]);
7907
+ out.writeUInt16BE(height, 5);
7908
+ out.writeUInt16BE(width, 7);
7909
+ return out;
7910
+ }
7911
+ function dht(cls, id, ncodes, values) {
7912
+ const out = Buffer.alloc(21 + values.length);
7913
+ out[0] = 255;
7914
+ out[1] = 196;
7915
+ out.writeUInt16BE(19 + values.length, 2);
7916
+ out[4] = cls << 4 | id;
7917
+ Buffer.from(ncodes).copy(out, 5);
7918
+ Buffer.from(values).copy(out, 21);
7919
+ return out;
7920
+ }
7921
+ function sos() {
7922
+ return Buffer.from([
7923
+ 255,
7924
+ 218,
7925
+ 0,
7926
+ 12,
7927
+ 3,
7928
+ 1,
7929
+ 0,
7930
+ 2,
7931
+ 17,
7932
+ 3,
7933
+ 17,
7934
+ 0,
7935
+ 63,
7936
+ 0
7937
+ ]);
7938
+ }
7939
+ var BitWriter = class {
7940
+ bytes = [];
7941
+ bits = 0;
7942
+ length = 0;
7943
+ writeBits(value, count) {
7944
+ for (let i = count - 1; i >= 0; i--) {
7945
+ this.bits = this.bits << 1 | value >> i & 1;
7946
+ this.length++;
7947
+ if (this.length === 8) this.pushByte();
7948
+ }
7949
+ }
7950
+ flush() {
7951
+ if (this.length > 0) {
7952
+ this.bits <<= 8 - this.length;
7953
+ this.pushByte();
7954
+ }
7955
+ }
7956
+ toBuffer() {
7957
+ return Buffer.from(this.bytes);
7958
+ }
7959
+ pushByte() {
7960
+ this.bytes.push(this.bits & 255);
7961
+ if ((this.bits & 255) === 255) this.bytes.push(0);
7962
+ this.bits = 0;
7963
+ this.length = 0;
7964
+ }
7965
+ };
7966
+ function buildHuffman(ncodes, values) {
7967
+ const table = /* @__PURE__ */ new Map();
7968
+ let code = 0;
7969
+ let index = 0;
7970
+ for (let len = 1; len <= 16; len++) {
7971
+ const count = ncodes[len - 1] ?? 0;
7972
+ for (let i = 0; i < count; i++) {
7973
+ table.set(values[index++] ?? 0, {
7974
+ code,
7975
+ len
7976
+ });
7977
+ code++;
7978
+ }
7979
+ code <<= 1;
7980
+ }
7981
+ return table;
7982
+ }
7983
+ const ZIGZAG = [
7984
+ 0,
7985
+ 1,
7986
+ 8,
7987
+ 16,
7988
+ 9,
7989
+ 2,
7990
+ 3,
7991
+ 10,
7992
+ 17,
7993
+ 24,
7994
+ 32,
7995
+ 25,
7996
+ 18,
7997
+ 11,
7998
+ 4,
7999
+ 5,
8000
+ 12,
8001
+ 19,
8002
+ 26,
8003
+ 33,
8004
+ 40,
8005
+ 48,
8006
+ 41,
8007
+ 34,
8008
+ 27,
8009
+ 20,
8010
+ 13,
8011
+ 6,
8012
+ 7,
8013
+ 14,
8014
+ 21,
8015
+ 28,
8016
+ 35,
8017
+ 42,
8018
+ 49,
8019
+ 56,
8020
+ 57,
8021
+ 50,
8022
+ 43,
8023
+ 36,
8024
+ 29,
8025
+ 22,
8026
+ 15,
8027
+ 23,
8028
+ 30,
8029
+ 37,
8030
+ 44,
8031
+ 51,
8032
+ 58,
8033
+ 59,
8034
+ 52,
8035
+ 45,
8036
+ 38,
8037
+ 31,
8038
+ 39,
8039
+ 46,
8040
+ 53,
8041
+ 60,
8042
+ 61,
8043
+ 54,
8044
+ 47,
8045
+ 55,
8046
+ 62,
8047
+ 63
8048
+ ];
8049
+ const LUM_QUANT = [
8050
+ 16,
8051
+ 11,
8052
+ 10,
8053
+ 16,
8054
+ 24,
8055
+ 40,
8056
+ 51,
8057
+ 61,
8058
+ 12,
8059
+ 12,
8060
+ 14,
8061
+ 19,
8062
+ 26,
8063
+ 58,
8064
+ 60,
8065
+ 55,
8066
+ 14,
8067
+ 13,
8068
+ 16,
8069
+ 24,
8070
+ 40,
8071
+ 57,
8072
+ 69,
8073
+ 56,
8074
+ 14,
8075
+ 17,
8076
+ 22,
8077
+ 29,
8078
+ 51,
8079
+ 87,
8080
+ 80,
8081
+ 62,
8082
+ 18,
8083
+ 22,
8084
+ 37,
8085
+ 56,
8086
+ 68,
8087
+ 109,
8088
+ 103,
8089
+ 77,
8090
+ 24,
8091
+ 35,
8092
+ 55,
8093
+ 64,
8094
+ 81,
8095
+ 104,
8096
+ 113,
8097
+ 92,
8098
+ 49,
8099
+ 64,
8100
+ 78,
8101
+ 87,
8102
+ 103,
8103
+ 121,
8104
+ 120,
8105
+ 101,
8106
+ 72,
8107
+ 92,
8108
+ 95,
8109
+ 98,
8110
+ 112,
8111
+ 100,
8112
+ 103,
8113
+ 99
8114
+ ];
8115
+ const CHR_QUANT = [
8116
+ 17,
8117
+ 18,
8118
+ 24,
8119
+ 47,
8120
+ 99,
8121
+ 99,
8122
+ 99,
8123
+ 99,
8124
+ 18,
8125
+ 21,
8126
+ 26,
8127
+ 66,
8128
+ 99,
8129
+ 99,
8130
+ 99,
8131
+ 99,
8132
+ 24,
8133
+ 26,
8134
+ 56,
8135
+ 99,
8136
+ 99,
8137
+ 99,
8138
+ 99,
8139
+ 99,
8140
+ 47,
8141
+ 66,
8142
+ 99,
8143
+ 99,
8144
+ 99,
8145
+ 99,
8146
+ 99,
8147
+ 99,
8148
+ 99,
8149
+ 99,
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
+ ];
8181
+ const STD_DC_LUM_NCODES = [
8182
+ 0,
8183
+ 1,
8184
+ 5,
8185
+ 1,
8186
+ 1,
8187
+ 1,
8188
+ 1,
8189
+ 1,
8190
+ 1,
8191
+ 0,
8192
+ 0,
8193
+ 0,
8194
+ 0,
8195
+ 0,
8196
+ 0,
8197
+ 0
8198
+ ];
8199
+ const STD_DC_LUM_VALUES = [
8200
+ 0,
8201
+ 1,
8202
+ 2,
8203
+ 3,
8204
+ 4,
8205
+ 5,
8206
+ 6,
8207
+ 7,
8208
+ 8,
8209
+ 9,
8210
+ 10,
8211
+ 11
8212
+ ];
8213
+ const STD_DC_CHR_NCODES = [
8214
+ 0,
8215
+ 3,
8216
+ 1,
8217
+ 1,
8218
+ 1,
8219
+ 1,
8220
+ 1,
8221
+ 1,
8222
+ 1,
8223
+ 1,
8224
+ 1,
8225
+ 0,
8226
+ 0,
8227
+ 0,
8228
+ 0,
8229
+ 0
8230
+ ];
8231
+ const STD_DC_CHR_VALUES = [
8232
+ 0,
8233
+ 1,
8234
+ 2,
8235
+ 3,
8236
+ 4,
8237
+ 5,
8238
+ 6,
8239
+ 7,
8240
+ 8,
8241
+ 9,
8242
+ 10,
8243
+ 11
8244
+ ];
8245
+ const STD_AC_LUM_NCODES = [
8246
+ 0,
8247
+ 2,
8248
+ 1,
8249
+ 3,
8250
+ 3,
8251
+ 2,
8252
+ 4,
8253
+ 3,
8254
+ 5,
8255
+ 5,
8256
+ 4,
8257
+ 4,
8258
+ 0,
8259
+ 0,
8260
+ 1,
8261
+ 125
8262
+ ];
8263
+ const STD_AC_LUM_VALUES = [
8264
+ 1,
8265
+ 2,
8266
+ 3,
8267
+ 0,
8268
+ 4,
8269
+ 17,
8270
+ 5,
8271
+ 18,
8272
+ 33,
8273
+ 49,
8274
+ 65,
8275
+ 6,
8276
+ 19,
8277
+ 81,
8278
+ 97,
8279
+ 7,
8280
+ 34,
8281
+ 113,
8282
+ 20,
8283
+ 50,
8284
+ 129,
8285
+ 145,
8286
+ 161,
8287
+ 8,
8288
+ 35,
8289
+ 66,
8290
+ 177,
8291
+ 193,
8292
+ 21,
8293
+ 82,
8294
+ 209,
8295
+ 240,
8296
+ 36,
8297
+ 51,
8298
+ 98,
8299
+ 114,
8300
+ 130,
8301
+ 9,
8302
+ 10,
8303
+ 22,
8304
+ 23,
8305
+ 24,
8306
+ 25,
8307
+ 26,
8308
+ 37,
8309
+ 38,
8310
+ 39,
8311
+ 40,
8312
+ 41,
8313
+ 42,
8314
+ 52,
8315
+ 53,
8316
+ 54,
8317
+ 55,
8318
+ 56,
8319
+ 57,
8320
+ 58,
8321
+ 67,
8322
+ 68,
8323
+ 69,
8324
+ 70,
8325
+ 71,
8326
+ 72,
8327
+ 73,
8328
+ 74,
8329
+ 83,
8330
+ 84,
8331
+ 85,
8332
+ 86,
8333
+ 87,
8334
+ 88,
8335
+ 89,
8336
+ 90,
8337
+ 99,
8338
+ 100,
8339
+ 101,
8340
+ 102,
8341
+ 103,
8342
+ 104,
8343
+ 105,
8344
+ 106,
8345
+ 115,
8346
+ 116,
8347
+ 117,
8348
+ 118,
8349
+ 119,
8350
+ 120,
8351
+ 121,
8352
+ 122,
8353
+ 131,
8354
+ 132,
8355
+ 133,
8356
+ 134,
8357
+ 135,
8358
+ 136,
8359
+ 137,
8360
+ 138,
8361
+ 146,
8362
+ 147,
8363
+ 148,
8364
+ 149,
8365
+ 150,
8366
+ 151,
8367
+ 152,
8368
+ 153,
8369
+ 154,
8370
+ 162,
8371
+ 163,
8372
+ 164,
8373
+ 165,
8374
+ 166,
8375
+ 167,
8376
+ 168,
8377
+ 169,
8378
+ 170,
8379
+ 178,
8380
+ 179,
8381
+ 180,
8382
+ 181,
8383
+ 182,
8384
+ 183,
8385
+ 184,
8386
+ 185,
8387
+ 186,
8388
+ 194,
8389
+ 195,
8390
+ 196,
8391
+ 197,
8392
+ 198,
8393
+ 199,
8394
+ 200,
8395
+ 201,
8396
+ 202,
8397
+ 210,
8398
+ 211,
8399
+ 212,
8400
+ 213,
8401
+ 214,
8402
+ 215,
8403
+ 216,
8404
+ 217,
8405
+ 218,
8406
+ 225,
8407
+ 226,
8408
+ 227,
8409
+ 228,
8410
+ 229,
8411
+ 230,
8412
+ 231,
8413
+ 232,
8414
+ 233,
8415
+ 234,
8416
+ 241,
8417
+ 242,
8418
+ 243,
8419
+ 244,
8420
+ 245,
8421
+ 246,
8422
+ 247,
8423
+ 248,
8424
+ 249,
8425
+ 250
8426
+ ];
8427
+ const STD_AC_CHR_NCODES = [
8428
+ 0,
8429
+ 2,
8430
+ 1,
8431
+ 2,
8432
+ 4,
8433
+ 4,
8434
+ 3,
8435
+ 4,
8436
+ 7,
8437
+ 5,
8438
+ 4,
8439
+ 4,
8440
+ 0,
8441
+ 1,
8442
+ 2,
8443
+ 119
8444
+ ];
8445
+ const STD_AC_CHR_VALUES = [
8446
+ 0,
8447
+ 1,
8448
+ 2,
8449
+ 3,
8450
+ 17,
8451
+ 4,
8452
+ 5,
8453
+ 33,
8454
+ 49,
8455
+ 6,
8456
+ 18,
8457
+ 65,
8458
+ 81,
8459
+ 7,
8460
+ 97,
8461
+ 113,
8462
+ 19,
8463
+ 34,
8464
+ 50,
8465
+ 129,
8466
+ 8,
8467
+ 20,
8468
+ 66,
8469
+ 145,
8470
+ 161,
8471
+ 177,
8472
+ 193,
8473
+ 9,
8474
+ 35,
8475
+ 51,
8476
+ 82,
8477
+ 240,
8478
+ 21,
8479
+ 98,
8480
+ 114,
8481
+ 209,
8482
+ 10,
8483
+ 22,
8484
+ 36,
8485
+ 52,
8486
+ 225,
8487
+ 37,
8488
+ 241,
8489
+ 23,
8490
+ 24,
8491
+ 25,
8492
+ 26,
8493
+ 38,
8494
+ 39,
8495
+ 40,
8496
+ 41,
8497
+ 42,
8498
+ 53,
8499
+ 54,
8500
+ 55,
8501
+ 56,
8502
+ 57,
8503
+ 58,
8504
+ 67,
8505
+ 68,
8506
+ 69,
8507
+ 70,
8508
+ 71,
8509
+ 72,
8510
+ 73,
8511
+ 74,
8512
+ 83,
8513
+ 84,
8514
+ 85,
8515
+ 86,
8516
+ 87,
8517
+ 88,
8518
+ 89,
8519
+ 90,
8520
+ 99,
8521
+ 100,
8522
+ 101,
8523
+ 102,
8524
+ 103,
8525
+ 104,
8526
+ 105,
8527
+ 106,
8528
+ 115,
8529
+ 116,
8530
+ 117,
8531
+ 118,
8532
+ 119,
8533
+ 120,
8534
+ 121,
8535
+ 122,
8536
+ 130,
8537
+ 131,
8538
+ 132,
8539
+ 133,
8540
+ 134,
8541
+ 135,
8542
+ 136,
8543
+ 137,
8544
+ 138,
8545
+ 146,
8546
+ 147,
8547
+ 148,
8548
+ 149,
8549
+ 150,
8550
+ 151,
8551
+ 152,
8552
+ 153,
8553
+ 154,
8554
+ 162,
8555
+ 163,
8556
+ 164,
8557
+ 165,
8558
+ 166,
8559
+ 167,
8560
+ 168,
8561
+ 169,
8562
+ 170,
8563
+ 178,
8564
+ 179,
8565
+ 180,
8566
+ 181,
8567
+ 182,
8568
+ 183,
8569
+ 184,
8570
+ 185,
8571
+ 186,
8572
+ 194,
8573
+ 195,
8574
+ 196,
8575
+ 197,
8576
+ 198,
8577
+ 199,
8578
+ 200,
8579
+ 201,
8580
+ 202,
8581
+ 210,
8582
+ 211,
8583
+ 212,
8584
+ 213,
8585
+ 214,
8586
+ 215,
8587
+ 216,
8588
+ 217,
8589
+ 218,
8590
+ 226,
8591
+ 227,
8592
+ 228,
8593
+ 229,
8594
+ 230,
8595
+ 231,
8596
+ 232,
8597
+ 233,
8598
+ 234,
8599
+ 242,
8600
+ 243,
8601
+ 244,
8602
+ 245,
8603
+ 246,
8604
+ 247,
8605
+ 248,
8606
+ 249,
8607
+ 250
8608
+ ];
8609
+ const YDC = buildHuffman(STD_DC_LUM_NCODES, STD_DC_LUM_VALUES);
8610
+ const CDC = buildHuffman(STD_DC_CHR_NCODES, STD_DC_CHR_VALUES);
8611
+ const YAC = buildHuffman(STD_AC_LUM_NCODES, STD_AC_LUM_VALUES);
8612
+ const CAC = buildHuffman(STD_AC_CHR_NCODES, STD_AC_CHR_VALUES);
8613
+ //#endregion
8614
+ //#region src/resources-image.ts
8615
+ /**
8616
+ * Build-time PNG/JPEG pixel helpers for page resources.
8617
+ *
8618
+ * PNG is decoded and re-encoded for resize/crop. JPEG is encode-only so a
8619
+ * `format=jpeg` transform can change the container after the pixel pass.
8620
+ */
8621
+ const PNG_SIGNATURE = Buffer.from([
8622
+ 137,
8623
+ 80,
8624
+ 78,
8625
+ 71,
8626
+ 13,
8627
+ 10,
8628
+ 26,
8629
+ 10
8630
+ ]);
8631
+ function isPng(buffer) {
8632
+ return buffer.length >= 8 && PNG_SIGNATURE.equals(buffer.subarray(0, 8));
8633
+ }
8634
+ function decodePng(buffer) {
8635
+ if (!isPng(buffer)) throw new Error("not a PNG");
8636
+ let width = 0;
8637
+ let height = 0;
8638
+ let bitDepth = 0;
8639
+ let colorType = 0;
8640
+ const idat = [];
8641
+ let offset = 8;
8642
+ while (offset + 12 <= buffer.length) {
8643
+ const length = buffer.readUInt32BE(offset);
8644
+ const type = buffer.toString("ascii", offset + 4, offset + 8);
8645
+ const start = offset + 8;
8646
+ const end = start + length;
8647
+ if (end + 4 > buffer.length) break;
8648
+ const chunk = buffer.subarray(start, end);
8649
+ if (type === "IHDR") {
8650
+ width = chunk.readUInt32BE(0);
8651
+ height = chunk.readUInt32BE(4);
8652
+ bitDepth = chunk[8] ?? 0;
8653
+ colorType = chunk[9] ?? 0;
8654
+ } else if (type === "IDAT") idat.push(Buffer.from(chunk));
8655
+ else if (type === "IEND") break;
8656
+ offset = end + 4;
8657
+ }
8658
+ if (bitDepth !== 8 || colorType !== 2 && colorType !== 6) throw new Error("unsupported PNG");
8659
+ const channels = colorType === 6 ? 4 : 3;
8660
+ const raw = inflateSync(Buffer.concat(idat));
8661
+ const stride = width * channels;
8662
+ const data = new Uint8Array(width * height * 4);
8663
+ let src = 0;
8664
+ const prior = new Uint8Array(stride);
8665
+ const recon = new Uint8Array(stride);
8666
+ for (let y = 0; y < height; y++) {
8667
+ const filter = raw[src++] ?? 0;
8668
+ for (let x = 0; x < stride; x++) {
8669
+ const sample = raw[src++] ?? 0;
8670
+ const a = x >= channels ? recon[x - channels] : 0;
8671
+ const b = prior[x] ?? 0;
8672
+ const c = x >= channels ? prior[x - channels] : 0;
8673
+ recon[x] = sample + paethPredict(filter, a, b, c) & 255;
8674
+ }
8675
+ for (let x = 0; x < width; x++) {
8676
+ const i = x * channels;
8677
+ const o = (y * width + x) * 4;
8678
+ data[o] = recon[i] ?? 0;
8679
+ data[o + 1] = recon[i + 1] ?? 0;
8680
+ data[o + 2] = recon[i + 2] ?? 0;
8681
+ data[o + 3] = channels === 4 ? recon[i + 3] ?? 255 : 255;
8682
+ }
8683
+ prior.set(recon);
8684
+ }
8685
+ return {
8686
+ width,
8687
+ height,
8688
+ data
8689
+ };
8690
+ }
8691
+ function paethPredict(filter, a, b, c) {
8692
+ switch (filter) {
8693
+ case 0: return 0;
8694
+ case 1: return a;
8695
+ case 2: return b;
8696
+ case 3: return a + b >> 1;
8697
+ case 4: {
8698
+ const p = a + b - c;
8699
+ const pa = Math.abs(p - a);
8700
+ const pb = Math.abs(p - b);
8701
+ const pc = Math.abs(p - c);
8702
+ if (pa <= pb && pa <= pc) return a;
8703
+ if (pb <= pc) return b;
8704
+ return c;
8705
+ }
8706
+ default: throw new Error("unsupported PNG filter");
8707
+ }
8708
+ }
8709
+ function encodePng(image) {
8710
+ const { width, height, data } = image;
8711
+ const raw = Buffer.alloc((width * 4 + 1) * height);
8712
+ let offset = 0;
8713
+ for (let y = 0; y < height; y++) {
8714
+ raw[offset++] = 0;
8715
+ raw.set(data.subarray(y * width * 4, (y + 1) * width * 4), offset);
8716
+ offset += width * 4;
8717
+ }
8718
+ const ihdr = Buffer.alloc(13);
8719
+ ihdr.writeUInt32BE(width, 0);
8720
+ ihdr.writeUInt32BE(height, 4);
8721
+ ihdr[8] = 8;
8722
+ ihdr[9] = 6;
8723
+ return Buffer.concat([
8724
+ PNG_SIGNATURE,
8725
+ pngChunk("IHDR", ihdr),
8726
+ pngChunk("IDAT", deflateSync(raw)),
8727
+ pngChunk("IEND", Buffer.alloc(0))
8728
+ ]);
8729
+ }
8730
+ function pngChunk(type, data) {
8731
+ const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
8732
+ const chunk = Buffer.alloc(12 + data.length);
8733
+ chunk.writeUInt32BE(data.length, 0);
8734
+ body.copy(chunk, 4);
8735
+ chunk.writeUInt32BE(crc32(body), 8 + data.length);
8736
+ return chunk;
8737
+ }
8738
+ function crc32(data) {
8739
+ let crc = 4294967295;
8740
+ for (const byte of data) {
8741
+ crc ^= byte;
8742
+ for (let i = 0; i < 8; i++) crc = crc & 1 ? crc >>> 1 ^ 3988292384 : crc >>> 1;
8743
+ }
8744
+ return (crc ^ 4294967295) >>> 0;
8745
+ }
8746
+ function resizeNearest(image, width, height) {
8747
+ const data = new Uint8Array(width * height * 4);
8748
+ for (let y = 0; y < height; y++) {
8749
+ const sy = Math.min(image.height - 1, Math.floor(y * image.height / height));
8750
+ for (let x = 0; x < width; x++) {
8751
+ const sx = Math.min(image.width - 1, Math.floor(x * image.width / width));
8752
+ data.set(image.data.subarray((sy * image.width + sx) * 4, (sy * image.width + sx) * 4 + 4), (y * width + x) * 4);
8753
+ }
8754
+ }
8755
+ return {
8756
+ width,
8757
+ height,
8758
+ data
8759
+ };
8760
+ }
8761
+ function cropImage(image, x, y, width, height) {
8762
+ const left = Math.max(0, Math.min(image.width, Math.floor(x)));
8763
+ const top = Math.max(0, Math.min(image.height, Math.floor(y)));
8764
+ const cropW = Math.max(1, Math.min(image.width - left, Math.floor(width)));
8765
+ const cropH = Math.max(1, Math.min(image.height - top, Math.floor(height)));
8766
+ const data = new Uint8Array(cropW * cropH * 4);
8767
+ for (let row = 0; row < cropH; row++) {
8768
+ const src = ((top + row) * image.width + left) * 4;
8769
+ data.set(image.data.subarray(src, src + cropW * 4), row * cropW * 4);
8770
+ }
8771
+ return {
8772
+ width: cropW,
8773
+ height: cropH,
8774
+ data
8775
+ };
8776
+ }
8777
+ function coverCrop(image, width, height) {
8778
+ const scale = Math.max(width / image.width, height / image.height);
8779
+ const scaled = resizeNearest(image, Math.max(width, Math.round(image.width * scale)), Math.max(height, Math.round(image.height * scale)));
8780
+ return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
8781
+ }
8782
+ //#endregion
8783
+ //#region src/resources-process.ts
8784
+ /**
8785
+ * Page-resource HTML rewriting and transform writes.
8786
+ */
8787
+ const IMG_TAG = /<img\b[^>]*>/gi;
8788
+ const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
8789
+ async function processPageResources(input) {
8790
+ if (!input.options.enabled) return {
8791
+ html: input.html,
8792
+ files: [],
8793
+ errors: [],
8794
+ fatal: []
8795
+ };
8796
+ const bundleRoot = path$1.dirname(input.inputPath);
8797
+ const outputDir = path$1.dirname(input.outputPath);
8798
+ const files = [];
8799
+ const errors = [];
8800
+ const fatal = [];
8801
+ let html = input.html;
8802
+ const tags = input.html.match(IMG_TAG) ?? [];
8803
+ for (const tag of tags) {
8804
+ const srcMatch = tag.match(SRC_ATTR);
8805
+ const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
8806
+ if (!rawSrc) continue;
8807
+ const src = unescapeHtml(rawSrc);
8808
+ const parsed = parseResourceSrc(src);
8809
+ if (!parsed) continue;
8810
+ const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
8811
+ if (!resolved.ok) {
8812
+ const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
8813
+ errors.push(message);
8814
+ fatal.push(message);
8815
+ continue;
8816
+ }
8817
+ let stat;
8818
+ try {
8819
+ stat = await fs$2.stat(resolved.absolute);
8820
+ } catch {
8821
+ const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
8822
+ errors.push(message);
8823
+ if (input.options.missing === "error") fatal.push(message);
8824
+ continue;
8825
+ }
8826
+ const transformError = validateTransform(parsed.transform, input.options);
8827
+ if (transformError) {
8828
+ const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
8829
+ errors.push(message);
8830
+ fatal.push(message);
8831
+ continue;
8832
+ }
8833
+ const hasTransform = hasPixelOrFormatTransform(parsed.transform);
8834
+ const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : path$1.basename(resolved.absolute);
8835
+ const outputFile = path$1.join(outputDir, outputName);
8836
+ try {
8837
+ if (hasTransform) await writeTransformedResource({
8838
+ sourcePath: resolved.absolute,
8839
+ outputFile,
8840
+ cacheDir: input.cacheDir,
8841
+ mtimeMs: stat.mtimeMs,
8842
+ transform: parsed.transform
8843
+ });
8844
+ else {
8845
+ await fs$2.mkdir(outputDir, { recursive: true });
8846
+ await fs$2.copyFile(resolved.absolute, outputFile);
8847
+ }
8848
+ files.push(outputFile);
8849
+ const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
8850
+ html = html.replace(tag, rewritten);
8851
+ } catch (error) {
8852
+ const detail = error instanceof Error ? error.message : String(error);
8853
+ const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
8854
+ errors.push(message);
8855
+ fatal.push(message);
8856
+ }
8857
+ }
8858
+ return {
8859
+ html,
8860
+ files,
8861
+ errors,
8862
+ fatal
8863
+ };
8864
+ }
8865
+ function resolveBundlePath(pathname, bundleRoot, contentRoot) {
8866
+ if (path$1.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
8867
+ const absolute = path$1.resolve(bundleRoot, pathname);
8868
+ if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
8869
+ return {
8870
+ ok: true,
8871
+ absolute
6082
8872
  };
6083
8873
  }
6084
- function outputToHref(outputPath, outDir, base) {
6085
- return siteHref$1(base, "", relativeUrl(outputPath, outDir));
8874
+ function validateTransform(transform, options) {
8875
+ if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
8876
+ if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
6086
8877
  }
6087
- /** Applies switcher / banner / search rewrite after every version tree is generated. */
6088
- function decorateVersionedPages(pages, options, outDir, base) {
6089
- if (!options.enabled) return;
6090
- const existingHrefs = new Set(pages.map((page) => outputToHref(page.outputPath, outDir, base)));
6091
- for (const page of pages) {
6092
- const { id, sibling } = versionLocation(page.outputPath, outDir, options);
6093
- page.html = applyVersionChrome(page.html, options, id, sibling, base, existingHrefs);
6094
- }
8878
+ function hasPixelOrFormatTransform(transform) {
8879
+ return Boolean(transform.width || transform.height || transform.crop || transform.format);
6095
8880
  }
6096
- async function writeSnapshotSearchIndex(input) {
6097
- const prefix = sanitizePrefix(input.prefix);
6098
- if (!prefix) return;
6099
- const destDir = path$1.join(input.outDir, prefix);
6100
- const prefixBase = searchIndexUrl(input.base, prefix).replace(/search-index\.json$/, "");
6101
- const json = await buildSearchIndex(input.srcDir, prefixBase, input.extensions, input.publishState, [], input.mdx);
6102
- await fs$1.mkdir(destDir, { recursive: true });
6103
- await writeSearchIndex(json, destDir);
6104
- const dest = path$1.join(destDir, "search-index.json");
8881
+ function transformedFileName(pathname, transform, cacheKey) {
8882
+ const stem = path$1.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
8883
+ const ext = outputExtension(pathname, transform.format);
8884
+ return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
8885
+ }
8886
+ function outputExtension(pathname, format) {
8887
+ if (format === "jpeg") return "jpg";
8888
+ if (format) return format;
8889
+ const ext = path$1.extname(pathname).slice(1).toLowerCase();
8890
+ return ext === "jpeg" ? "jpg" : ext || "png";
8891
+ }
8892
+ async function writeTransformedResource(input) {
8893
+ const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
8894
+ const ext = path$1.extname(input.outputFile);
8895
+ const cacheFile = path$1.join(input.cacheDir, `${key}${ext}`);
6105
8896
  try {
6106
- await fs$1.access(dest);
6107
- } catch {
6108
- await fs$1.writeFile(dest, json, "utf8");
8897
+ await fs$2.copyFile(cacheFile, input.outputFile);
8898
+ return;
8899
+ } catch {}
8900
+ const output = transformResourceBuffer(await fs$2.readFile(input.sourcePath), input.sourcePath, input.transform);
8901
+ if (output.length > 8388608) throw new Error("transform produced an oversized file");
8902
+ await fs$2.mkdir(path$1.dirname(input.outputFile), { recursive: true });
8903
+ await fs$2.mkdir(input.cacheDir, { recursive: true });
8904
+ await fs$2.writeFile(cacheFile, output);
8905
+ await fs$2.writeFile(input.outputFile, output);
8906
+ }
8907
+ function transformResourceBuffer(source, sourcePath, transform) {
8908
+ const needsPixels = Boolean(transform.width || transform.height || transform.crop);
8909
+ if (!needsPixels && !transform.format) return source;
8910
+ if (!needsPixels && transform.format) {
8911
+ if (!isPng(source)) {
8912
+ if (transform.format === formatFromPath(sourcePath)) return source;
8913
+ throw new Error(`cannot convert ${path$1.extname(sourcePath) || "source"} to ${transform.format}`);
8914
+ }
8915
+ return encodeFormat(decodePng(source), transform.format);
8916
+ }
8917
+ if (!isPng(source)) throw new Error("resize/crop requires a PNG source");
8918
+ return encodeFormat(applyPixelTransform(decodePng(source), transform), transform.format ?? "png");
8919
+ }
8920
+ function applyPixelTransform(image, transform) {
8921
+ const crop = transform.crop;
8922
+ if (crop && crop !== "center") {
8923
+ const parts = crop.split(",").map((part) => Number(part.trim()));
8924
+ if (parts.length === 4 && parts.every((part) => Number.isFinite(part))) return cropImage(image, parts[0], parts[1], parts[2], parts[3]);
8925
+ throw new Error(`invalid crop ${crop}`);
8926
+ }
8927
+ const width = transform.width;
8928
+ const height = transform.height;
8929
+ if (crop === "center") {
8930
+ if (!width || !height) throw new Error("crop=center requires width and height");
8931
+ return coverCrop(image, width, height);
8932
+ }
8933
+ if (width && height) return resizeNearest(image, width, height);
8934
+ if (width) return resizeNearest(image, width, Math.max(1, Math.round(image.height * width / image.width)));
8935
+ if (height) return resizeNearest(image, Math.max(1, Math.round(image.width * height / image.height)), height);
8936
+ return image;
8937
+ }
8938
+ function encodeFormat(image, format) {
8939
+ if (format === "jpeg") return encodeJpeg(image);
8940
+ if (format === "png") return encodePng(image);
8941
+ if (format === "webp") throw new Error("webp encoding requires a webp source without pixel transforms");
8942
+ throw new Error(`unsupported format ${format}`);
8943
+ }
8944
+ function formatFromPath(filePath) {
8945
+ const ext = path$1.extname(filePath).slice(1).toLowerCase();
8946
+ return ext === "jpg" ? "jpeg" : ext;
8947
+ }
8948
+ function unescapeHtml(value) {
8949
+ return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
8950
+ }
8951
+ function escapeAttribute(value) {
8952
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8953
+ }
8954
+ //#endregion
8955
+ //#region src/resources.ts
8956
+ /**
8957
+ * Opt-in page-bundle resources and build-time image processing.
8958
+ *
8959
+ * A page directory is the bundle root. Sibling images are addressable with
8960
+ * relative URLs. Resize/crop/format query transforms run at build time and
8961
+ * are cached by source mtime plus transform params. Paths that leave the
8962
+ * bundle or `srcDir` are never processed.
8963
+ */
8964
+ const DEFAULT_FORMATS = [
8965
+ "png",
8966
+ "jpeg",
8967
+ "webp"
8968
+ ];
8969
+ const HOSTILE_SRC = /^(?:javascript|data|vbscript):/i;
8970
+ var PageResourceError = class extends Error {
8971
+ issues;
8972
+ constructor(issues) {
8973
+ super(issues.join("\n"));
8974
+ this.name = "PageResourceError";
8975
+ this.issues = issues;
6109
8976
  }
6110
- return dest;
8977
+ };
8978
+ /**
8979
+ * Resolves `resources`. Omitted / `false` stay off. `true` or `{}` enables
8980
+ * defaults. An object enables the feature and overrides only set fields.
8981
+ */
8982
+ function resolveResourcesOptions(value) {
8983
+ if (!value) return {
8984
+ enabled: false,
8985
+ formats: [...DEFAULT_FORMATS],
8986
+ widths: [],
8987
+ missing: "error"
8988
+ };
8989
+ if (value === true) return {
8990
+ enabled: true,
8991
+ formats: [...DEFAULT_FORMATS],
8992
+ widths: [],
8993
+ missing: "error"
8994
+ };
8995
+ return {
8996
+ enabled: true,
8997
+ formats: normalizeFormats(value.formats),
8998
+ widths: normalizeWidths(value.widths),
8999
+ missing: value.missing === "warn" ? "warn" : "error"
9000
+ };
6111
9001
  }
6112
- function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
6113
- if (!options.enabled) return html;
6114
- const active = options.entries.find((entry) => entry.id === activeId);
6115
- 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 ?? ""));
9002
+ /**
9003
+ * Cache key for a source file plus transform. Changing mtime or params
9004
+ * produces a different key so stale derivatives are not reused.
9005
+ */
9006
+ function resourceCacheKey(sourcePath, mtimeMs, transform) {
9007
+ return createHash("sha256").update(sourcePath).update("\0").update(String(mtimeMs)).update("\0").update(JSON.stringify(normalizeTransform(transform))).digest("hex");
6116
9008
  }
6117
- function sanitizePrefix(prefix) {
6118
- const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
6119
- if (!trimmed) return "";
6120
- return PREFIX_RE.test(trimmed) && !trimmed.includes("..") ? trimmed : "";
9009
+ /** True when `candidate` stays inside `root` after resolve. */
9010
+ function isInsideRoot$1(root, candidate) {
9011
+ const resolvedRoot = path$1.resolve(root);
9012
+ const resolved = path$1.resolve(candidate);
9013
+ const relative = path$1.relative(resolvedRoot, resolved);
9014
+ return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
6121
9015
  }
6122
- function defaultCurrentEntry() {
9016
+ function parseResourceSrc(src) {
9017
+ const trimmed = src.trim();
9018
+ if (!trimmed || isRemoteOrAbsolute(trimmed) || HOSTILE_SRC.test(trimmed.replace(/\s+/g, ""))) return;
9019
+ const withoutHash = trimmed.split("#")[0] ?? trimmed;
9020
+ const queryIndex = withoutHash.indexOf("?");
9021
+ const pathname = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
9022
+ const query = queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1);
9023
+ if (!pathname || pathname.includes("\0")) return;
9024
+ const params = new URLSearchParams(query);
6123
9025
  return {
6124
- id: DEFAULT_CURRENT_ID,
6125
- label: "Latest",
6126
- prefix: "",
6127
- banner: false
9026
+ pathname,
9027
+ transform: {
9028
+ width: parsePositiveInt(params.get("width") ?? params.get("w")),
9029
+ height: parsePositiveInt(params.get("height") ?? params.get("h")),
9030
+ crop: params.get("crop")?.trim() || void 0,
9031
+ format: normalizeFormat(params.get("format") ?? void 0)
9032
+ }
6128
9033
  };
6129
9034
  }
6130
- function normalizeEntries(entries) {
6131
- if (!entries) return [];
6132
- const seen = /* @__PURE__ */ new Set();
6133
- const resolved = [];
6134
- for (const entry of entries) {
6135
- if (!entry || typeof entry.id !== "string" || typeof entry.label !== "string") continue;
6136
- const id = entry.id.trim();
6137
- const label = entry.label.trim();
6138
- if (!id || !label || seen.has(id)) continue;
6139
- const prefix = sanitizePrefix(typeof entry.prefix === "string" ? entry.prefix : "");
6140
- if (entry.prefix && !prefix) continue;
6141
- const dir = typeof entry.dir === "string" && entry.dir.trim() ? entry.dir.trim() : void 0;
6142
- if (dir && (dir.includes("\0") || dir.includes(".."))) continue;
6143
- seen.add(id);
6144
- resolved.push({
6145
- id,
6146
- label,
6147
- prefix,
6148
- dir,
6149
- banner: normalizeBanner(entry.banner)
6150
- });
6151
- }
6152
- return resolved;
9035
+ function isRemoteOrAbsolute(src) {
9036
+ const compact = src.replace(/\s+/g, "");
9037
+ return /^[a-z][a-z0-9+.-]*:/i.test(compact) || compact.startsWith("//") || compact.startsWith("/");
6153
9038
  }
6154
- function normalizeBanner(value) {
6155
- return value === "unreleased" || value === "unmaintained" ? value : false;
9039
+ function parsePositiveInt(raw) {
9040
+ if (!raw) return;
9041
+ if (!/^[0-9]+$/.test(raw)) return;
9042
+ const value = Number(raw);
9043
+ return value > 0 ? value : void 0;
6156
9044
  }
6157
- function siteHref$1(base, prefix, rest) {
6158
- const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
6159
- const parts = [prefix, rest].filter((part) => part && part !== "/");
6160
- return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
9045
+ function normalizeFormats(formats) {
9046
+ if (!formats?.length) return [...DEFAULT_FORMATS];
9047
+ const normalized = formats.map((format) => normalizeFormat(format)).filter((format) => Boolean(format));
9048
+ return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_FORMATS];
6161
9049
  }
6162
- function relativeUrl(outputPath, outDir) {
6163
- const rel = path$1.posix.normalize(path$1.relative(path$1.resolve(outDir), path$1.resolve(outputPath)).replaceAll(path$1.sep, "/"));
6164
- if (rel.startsWith("..")) return "";
6165
- const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
6166
- return dir === "." ? "" : dir;
9050
+ function normalizeWidths(widths) {
9051
+ if (!widths?.length) return [];
9052
+ return [...new Set(widths.filter((width) => Number.isInteger(width) && width > 0))];
9053
+ }
9054
+ function normalizeFormat(format) {
9055
+ if (!format) return;
9056
+ const value = format.trim().toLowerCase();
9057
+ if (value === "jpg") return "jpeg";
9058
+ return value || void 0;
9059
+ }
9060
+ function normalizeTransform(transform) {
9061
+ return {
9062
+ width: transform.width,
9063
+ height: transform.height,
9064
+ crop: transform.crop,
9065
+ format: transform.format
9066
+ };
6167
9067
  }
6168
9068
  //#endregion
6169
9069
  //#region src/version-navigation.ts
@@ -6338,14 +9238,18 @@ function resolveSsgOptions(ssg) {
6338
9238
  bare: false,
6339
9239
  generateOgImage: false,
6340
9240
  lastUpdated: false,
9241
+ contributors: resolveContributorsOption(void 0),
6341
9242
  pagination: false,
6342
9243
  breadcrumbs: false,
9244
+ jsonLd: false,
6343
9245
  readerChrome: false,
6344
9246
  localeSwitcher: false,
6345
9247
  a11y: false,
6346
9248
  pageChrome: false,
6347
9249
  notFound: resolveNotFoundOptions(void 0),
6348
- team: resolveTeamOptions(void 0)
9250
+ team: resolveTeamOptions(void 0),
9251
+ blog: resolveBlogOptions(void 0),
9252
+ sectionIndex: resolveSectionIndexOptions(void 0)
6349
9253
  };
6350
9254
  if (ssg === true || ssg === void 0) return {
6351
9255
  enabled: true,
@@ -6354,14 +9258,18 @@ function resolveSsgOptions(ssg) {
6354
9258
  bare: false,
6355
9259
  generateOgImage: false,
6356
9260
  lastUpdated: false,
9261
+ contributors: resolveContributorsOption(void 0),
6357
9262
  pagination: false,
6358
9263
  breadcrumbs: false,
9264
+ jsonLd: false,
6359
9265
  readerChrome: false,
6360
9266
  localeSwitcher: false,
6361
9267
  a11y: false,
6362
9268
  pageChrome: false,
6363
9269
  notFound: resolveNotFoundOptions(void 0),
6364
9270
  team: resolveTeamOptions(void 0),
9271
+ blog: resolveBlogOptions(void 0),
9272
+ sectionIndex: resolveSectionIndexOptions(void 0),
6365
9273
  theme: resolveTheme(void 0)
6366
9274
  };
6367
9275
  return {
@@ -6378,22 +9286,56 @@ function resolveSsgOptions(ssg) {
6378
9286
  ogImage: ssg.ogImage,
6379
9287
  generateOgImage: ssg.generateOgImage ?? false,
6380
9288
  lastUpdated: ssg.lastUpdated ?? false,
9289
+ contributors: resolveContributorsOption(ssg.contributors),
6381
9290
  pagination: resolvePaginationOption(ssg.pagination),
6382
9291
  breadcrumbs: resolvePaginationOption(ssg.breadcrumbs),
9292
+ jsonLd: resolveJsonLdOption(ssg.jsonLd),
6383
9293
  readerChrome: resolveReaderChromeOption(ssg.readerChrome),
6384
9294
  localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
6385
9295
  a11y: resolveA11yOption(ssg.a11y),
6386
9296
  pageChrome: resolvePageChromeOption(ssg.pageChrome),
6387
9297
  notFound: resolveNotFoundOptions(ssg.notFound),
6388
9298
  team: resolveTeamOptions(ssg.team),
9299
+ blog: resolveBlogOptions(ssg.blog),
9300
+ sectionIndex: resolveSectionIndexOptions(ssg.sectionIndex),
6389
9301
  siteUrl: ssg.siteUrl,
6390
9302
  theme: resolveTheme(ssg.theme),
6391
9303
  navigation: ssg.navigation
6392
9304
  };
6393
9305
  }
9306
+ function contributorsForPage(context, inputPath) {
9307
+ const option = context.ssgOptions.contributors;
9308
+ if (!option) return;
9309
+ try {
9310
+ return applyContributorOptions(context.napi?.getGitContributors(inputPath, context.root) ?? [], option);
9311
+ } catch {
9312
+ return [];
9313
+ }
9314
+ }
6394
9315
  function resolvePaginationOption(value) {
6395
9316
  return value === true || typeof value === "object" && value !== null;
6396
9317
  }
9318
+ function resolveJsonLdOption(value) {
9319
+ if (value === true) return { breadcrumbs: true };
9320
+ if (value && typeof value === "object") {
9321
+ const publisher = resolveJsonLdPublisher(value.publisher);
9322
+ return {
9323
+ breadcrumbs: value.breadcrumbs !== false,
9324
+ ...publisher ? { publisher } : {}
9325
+ };
9326
+ }
9327
+ return false;
9328
+ }
9329
+ function resolveJsonLdPublisher(publisher) {
9330
+ if (!publisher || typeof publisher !== "object") return;
9331
+ const name = publisher.name?.trim();
9332
+ const url = publisher.url?.trim();
9333
+ if (!name && !url) return;
9334
+ return {
9335
+ ...name ? { name } : {},
9336
+ ...url ? { url } : {}
9337
+ };
9338
+ }
6397
9339
  function resolveReaderChromeOption(value) {
6398
9340
  if (value === true) return {
6399
9341
  copy: true,
@@ -6522,7 +9464,7 @@ function localeCodesFor(locales) {
6522
9464
  async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
6523
9465
  enabled: false,
6524
9466
  members: []
6525
- }, pageChrome = false, breadcrumbRootHref) {
9467
+ }, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl) {
6526
9468
  const mod = await importNapiModule();
6527
9469
  const tocForRust = pageData.toc.map(toRustTocEntry);
6528
9470
  const navGroupsForRust = convertNavGroupsForRust(navGroups);
@@ -6564,6 +9506,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
6564
9506
  content: pageData.content,
6565
9507
  toc: tocForRust,
6566
9508
  lastUpdated: pageData.lastUpdated,
9509
+ contributors: pageData.contributors,
6567
9510
  path: pageData.path,
6568
9511
  entryPage: entryPageForRust,
6569
9512
  prev: pageData.prev,
@@ -6590,14 +9533,19 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
6590
9533
  localePaths,
6591
9534
  a11y: a11y ? { skipLinkLabel: a11y.skipLinkLabel } : void 0,
6592
9535
  team,
6593
- pageChrome
9536
+ pageChrome,
9537
+ jsonLd: jsonLd ? {
9538
+ breadcrumbs: jsonLd.breadcrumbs,
9539
+ publisher: jsonLd.publisher,
9540
+ siteUrl
9541
+ } : void 0
6594
9542
  });
6595
9543
  }
6596
9544
  async function externalizeSharedPageAssets(pages, outDir, base) {
6597
9545
  const optimized = (await importNapiModule()).externalizeSsgAssets(pages, outDir, base);
6598
9546
  await Promise.all(optimized.assets.map(async (asset) => {
6599
- await fs$2.mkdir(path$2.dirname(asset.outputPath), { recursive: true });
6600
- await fs$2.writeFile(asset.outputPath, asset.content, "utf-8");
9547
+ await fs$3.mkdir(path$2.dirname(asset.outputPath), { recursive: true });
9548
+ await fs$3.writeFile(asset.outputPath, asset.content, "utf-8");
6601
9549
  }));
6602
9550
  return {
6603
9551
  pages: optimized.pages,
@@ -6677,10 +9625,31 @@ async function buildSsg(options, root) {
6677
9625
  errors.push(...collected.errors);
6678
9626
  const { outputPages, listedPages } = applyPublishState(context, collected);
6679
9627
  remapPermalinkNav(context, listedPages);
9628
+ await applyPageResources(context, outputPages, generatedFiles, errors);
6680
9629
  await generateOgImageAssets(context, collected, generatedFiles, errors);
6681
9630
  injectRelatedPages(outputPages, listedPages, context.options.taxonomies);
9631
+ const blog = context.options.blog ?? context.ssgOptions.blog;
9632
+ await injectBlogPostMeta({
9633
+ pages: outputPages,
9634
+ listed: listedPages,
9635
+ options: blog,
9636
+ srcDir: context.srcDir,
9637
+ collections: context.options.collections,
9638
+ base: context.base
9639
+ });
6682
9640
  const generatedPages = await generateHtmlPages(context, outputPages, collected, errors);
6683
9641
  await appendNotFoundPage(generatedPages, context, collected, errors);
9642
+ await appendSectionIndexPages({
9643
+ generatedPages,
9644
+ collectedPages: collected.pageResults,
9645
+ listedPages,
9646
+ options: context.ssgOptions.sectionIndex,
9647
+ outDir: context.outDir,
9648
+ base: context.base,
9649
+ extension: context.ssgOptions.extension,
9650
+ errors,
9651
+ render: (page) => renderSsgPage(context, toSectionIndexProcessResult(page), collected, listedPages)
9652
+ });
6684
9653
  await appendTaxonomyPages({
6685
9654
  generatedPages,
6686
9655
  listedPages,
@@ -6690,8 +9659,20 @@ async function buildSsg(options, root) {
6690
9659
  errors,
6691
9660
  render: (page) => renderSsgPage(context, toTaxonomyProcessResult(page), collected, listedPages)
6692
9661
  });
9662
+ await appendBlogPages({
9663
+ generatedPages,
9664
+ listedPages,
9665
+ options: blog,
9666
+ collections: context.options.collections,
9667
+ srcDir: context.srcDir,
9668
+ outDir: context.outDir,
9669
+ base: context.base,
9670
+ errors,
9671
+ render: (page) => renderSsgPage(context, toBlogProcessResult(page), collected, listedPages)
9672
+ });
6693
9673
  await applyDocumentationVersions(generatedPages, context, errors);
6694
9674
  await writeGeneratedPages(generatedPages, context, generatedFiles, listedPages, outputPages, errors);
9675
+ if (options.math?.enabled) generatedFiles.push(...await copyKatexAssets(outDir));
6695
9676
  return {
6696
9677
  files: generatedFiles,
6697
9678
  errors,
@@ -6701,7 +9682,7 @@ async function buildSsg(options, root) {
6701
9682
  async function cleanOutputDirectory(ssgOptions, outDir) {
6702
9683
  if (!ssgOptions.clean) return;
6703
9684
  try {
6704
- await fs$2.rm(outDir, {
9685
+ await fs$3.rm(outDir, {
6705
9686
  recursive: true,
6706
9687
  force: true
6707
9688
  });
@@ -6720,7 +9701,7 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
6720
9701
  navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
6721
9702
  siteName: await resolveSiteName$1(root, ssgOptions),
6722
9703
  shouldGenerateOgImages: shouldGenerateOgImages(options),
6723
- napi: ssgOptions.lastUpdated ? await importNapiModule() : void 0
9704
+ napi: ssgOptions.lastUpdated || ssgOptions.contributors ? await importNapiModule() : void 0
6724
9705
  };
6725
9706
  }
6726
9707
  /**
@@ -6739,12 +9720,33 @@ async function resolveSiteName$1(root, ssgOptions) {
6739
9720
  if (ssgOptions.siteName) return ssgOptions.siteName;
6740
9721
  try {
6741
9722
  const pkgPath = path$2.join(root, "package.json");
6742
- const pkg = JSON.parse(await fs$2.readFile(pkgPath, "utf-8"));
9723
+ const pkg = JSON.parse(await fs$3.readFile(pkgPath, "utf-8"));
6743
9724
  return pkg.name ? formatTitle(pkg.name) : "Documentation";
6744
9725
  } catch {
6745
9726
  return "Documentation";
6746
9727
  }
6747
9728
  }
9729
+ async function applyPageResources(context, pages, generatedFiles, errors) {
9730
+ const options = context.options.resources;
9731
+ if (!options?.enabled) return;
9732
+ const cacheDir = path$2.join(context.root, ".cache", "ox-content-resources");
9733
+ const fatal = [];
9734
+ for (const page of pages) {
9735
+ const processed = await processPageResources({
9736
+ html: page.transformedHtml,
9737
+ inputPath: page.inputPath,
9738
+ outputPath: page.routePaths.outputPath,
9739
+ srcDir: context.srcDir,
9740
+ options,
9741
+ cacheDir
9742
+ });
9743
+ page.transformedHtml = processed.html;
9744
+ generatedFiles.push(...processed.files);
9745
+ errors.push(...processed.errors);
9746
+ fatal.push(...processed.fatal);
9747
+ }
9748
+ if (fatal.length > 0) throw new PageResourceError(fatal);
9749
+ }
6748
9750
  function applyPermalinkRoutes(context, collected) {
6749
9751
  if (!context.options.permalinks?.enabled && !context.options.cascade?.enabled) return;
6750
9752
  const routed = applySsgPageRoutes({
@@ -6810,7 +9812,7 @@ function applyPublishState(context, collected) {
6810
9812
  };
6811
9813
  }
6812
9814
  async function transformSsgPage(context, inputPath) {
6813
- const result = await transformMarkdown(await fs$2.readFile(inputPath, "utf-8"), inputPath, context.options, {
9815
+ const result = await transformMarkdown(await fs$3.readFile(inputPath, "utf-8"), inputPath, context.options, {
6814
9816
  convertMdLinks: true,
6815
9817
  baseUrl: context.base,
6816
9818
  sourcePath: inputPath
@@ -6824,7 +9826,8 @@ async function transformSsgPage(context, inputPath) {
6824
9826
  transformedHtml,
6825
9827
  title,
6826
9828
  description: frontmatter.description,
6827
- lastUpdated: context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0,
9829
+ lastUpdated: context.ssgOptions.lastUpdated ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
9830
+ contributors: contributorsForPage(context, inputPath),
6828
9831
  frontmatter,
6829
9832
  toc: result.toc
6830
9833
  };
@@ -6979,7 +9982,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
6979
9982
  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 ?? {
6980
9983
  enabled: false,
6981
9984
  members: []
6982
- }, context.ssgOptions.pageChrome, versionNavigation?.root.href);
9985
+ }, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl);
6983
9986
  }
6984
9987
  function rewritePagerOverride(pager, context) {
6985
9988
  return pager?.href ? {
@@ -6995,6 +9998,7 @@ function toThemePageData(pageResult) {
6995
9998
  html: pageResult.transformedHtml,
6996
9999
  toc: pageResult.toc,
6997
10000
  lastUpdated: pageResult.lastUpdated,
10001
+ contributors: pageResult.contributors,
6998
10002
  path: pageResult.inputPath,
6999
10003
  url: pageResult.routePaths.href,
7000
10004
  frontmatter: pageResult.frontmatter,
@@ -7025,6 +10029,7 @@ function createSsgPageData(pageResult) {
7025
10029
  content: pageResult.transformedHtml,
7026
10030
  toc: pageResult.toc,
7027
10031
  lastUpdated: pageResult.lastUpdated,
10032
+ contributors: pageResult.contributors,
7028
10033
  frontmatter,
7029
10034
  path: pageResult.routePaths.urlPath,
7030
10035
  href: pageResult.routePaths.href,
@@ -7041,7 +10046,7 @@ async function appendNotFoundPage(generatedPages, context, collected, errors) {
7041
10046
  const sourcePath = resolveNotFoundSourcePath(context.srcDir, notFound.source);
7042
10047
  const outputPath = resolveNotFoundOutputPath(context.outDir, notFound.output);
7043
10048
  try {
7044
- const pageResult = await transformNotFoundMarkdown(context, sourcePath, await fileExists(sourcePath) ? await fs$2.readFile(sourcePath, "utf8") : FALLBACK_NOT_FOUND_MARKDOWN);
10049
+ const pageResult = await transformNotFoundMarkdown(context, sourcePath, await fileExists(sourcePath) ? await fs$3.readFile(sourcePath, "utf8") : FALLBACK_NOT_FOUND_MARKDOWN);
7045
10050
  pageResult.routePaths = {
7046
10051
  ...pageResult.routePaths,
7047
10052
  outputPath,
@@ -7059,7 +10064,7 @@ async function appendNotFoundPage(generatedPages, context, collected, errors) {
7059
10064
  }
7060
10065
  async function fileExists(filePath) {
7061
10066
  try {
7062
- await fs$2.access(filePath);
10067
+ await fs$3.access(filePath);
7063
10068
  return true;
7064
10069
  } catch {
7065
10070
  return false;
@@ -7124,6 +10129,17 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
7124
10129
  redirects: snapContext.options.redirects?.map
7125
10130
  });
7126
10131
  const snapPages = await generateHtmlPages(snapContext, outputPages, snapCollected, errors);
10132
+ await appendSectionIndexPages({
10133
+ generatedPages: snapPages,
10134
+ collectedPages: snapCollected.pageResults,
10135
+ listedPages,
10136
+ options: snapContext.ssgOptions.sectionIndex,
10137
+ outDir: snapContext.outDir,
10138
+ base: snapContext.base,
10139
+ extension: snapContext.ssgOptions.extension,
10140
+ errors,
10141
+ render: (page) => renderSsgPage(snapContext, toSectionIndexProcessResult(page), snapCollected, listedPages)
10142
+ });
7127
10143
  generatedPages.push(...snapPages);
7128
10144
  if (context.options.search?.enabled) try {
7129
10145
  await writeSnapshotSearchIndex({
@@ -7150,9 +10166,24 @@ function pageAliases(frontmatter) {
7150
10166
  async function writeGeneratedPages(generatedPages, context, generatedFiles, listedPages, outputPages, errors) {
7151
10167
  const optimizedOutput = await externalizeSharedPageAssets(generatedPages, context.outDir, context.base);
7152
10168
  generatedFiles.push(...optimizedOutput.assets);
10169
+ const pwa = await writePwaFiles({
10170
+ outDir: context.outDir,
10171
+ siteUrl: context.ssgOptions.siteUrl,
10172
+ base: context.base,
10173
+ siteName: context.siteName,
10174
+ options: context.options.pwa
10175
+ });
10176
+ generatedFiles.push(...pwa.files);
10177
+ if (pwa.warning) {
10178
+ errors.push(pwa.warning);
10179
+ console.warn(pwa.warning);
10180
+ } else if (!context.ssgOptions.bare && context.options.pwa?.enabled) for (const page of optimizedOutput.pages) page.html = injectPwaPageTags(page.html, {
10181
+ options: context.options.pwa,
10182
+ base: context.base
10183
+ });
7153
10184
  for (const page of optimizedOutput.pages) {
7154
- await fs$2.mkdir(path$2.dirname(page.outputPath), { recursive: true });
7155
- await fs$2.writeFile(page.outputPath, page.html, "utf-8");
10185
+ await fs$3.mkdir(path$2.dirname(page.outputPath), { recursive: true });
10186
+ await fs$3.writeFile(page.outputPath, page.html, "utf-8");
7156
10187
  generatedFiles.push(page.outputPath);
7157
10188
  }
7158
10189
  const siteMaps = await writeSiteMapFiles({
@@ -7277,14 +10308,14 @@ async function resolveMarkdownFile(url, srcDir, extensions) {
7277
10308
  for (const relativePath of directCandidates) {
7278
10309
  const filePath = path$2.join(srcDir, relativePath);
7279
10310
  try {
7280
- await fs$2.access(filePath);
10311
+ await fs$3.access(filePath);
7281
10312
  return filePath;
7282
10313
  } catch {}
7283
10314
  }
7284
10315
  for (const extension of extensions) {
7285
10316
  const indexPath = path$2.join(srcDir, routePath, `index${extension}`);
7286
10317
  try {
7287
- await fs$2.access(indexPath);
10318
+ await fs$3.access(indexPath);
7288
10319
  return indexPath;
7289
10320
  } catch {}
7290
10321
  }
@@ -7328,7 +10359,7 @@ async function resolveSiteName(options, root) {
7328
10359
  if (options.ssg.siteName) return options.ssg.siteName;
7329
10360
  try {
7330
10361
  const pkgPath = path$2.join(root, "package.json");
7331
- const pkg = JSON.parse(await fs$2.readFile(pkgPath, "utf-8"));
10362
+ const pkg = JSON.parse(await fs$3.readFile(pkgPath, "utf-8"));
7332
10363
  if (pkg.name) return formatTitle(pkg.name);
7333
10364
  } catch {}
7334
10365
  return "Documentation";
@@ -7340,7 +10371,7 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
7340
10371
  const srcDir = path$2.resolve(root, options.srcDir);
7341
10372
  resetTabGroupCounter();
7342
10373
  resetIslandCounter();
7343
- const result = await transformMarkdown(await fs$2.readFile(filePath, "utf-8"), filePath, options, {
10374
+ const result = await transformMarkdown(await fs$3.readFile(filePath, "utf-8"), filePath, options, {
7344
10375
  convertMdLinks: true,
7345
10376
  baseUrl: base,
7346
10377
  sourcePath: filePath
@@ -7412,7 +10443,7 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
7412
10443
  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 ?? {
7413
10444
  enabled: false,
7414
10445
  members: []
7415
- }, options.ssg.pageChrome);
10446
+ }, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl);
7416
10447
  html = injectViteHmrClient(html);
7417
10448
  return html;
7418
10449
  }
@@ -7540,7 +10571,7 @@ async function collectPages(options, root) {
7540
10571
  const pages = [];
7541
10572
  const generateOgImage = options.ogImage || options.ssg.generateOgImage;
7542
10573
  for (const file of files.sort()) {
7543
- const content = fs.readFileSync(file, "utf-8");
10574
+ const content = fs$1.readFileSync(file, "utf-8");
7544
10575
  const frontmatter = normalizeVitePressFrontmatter(parseFrontmatter(content));
7545
10576
  if (frontmatter.layout === "entry") continue;
7546
10577
  const title = extractTitle(content, frontmatter);
@@ -7864,7 +10895,7 @@ function createI18nPlugin(resolvedOptions) {
7864
10895
  async buildStart() {
7865
10896
  if (!i18nOptions || !i18nOptions.check) return;
7866
10897
  const dictDir = path$2.resolve(root, i18nOptions.dir);
7867
- if (!fs.existsSync(dictDir)) {
10898
+ if (!fs$1.existsSync(dictDir)) {
7868
10899
  console.warn(`[ox-content:i18n] Dictionary directory not found: ${dictDir}`);
7869
10900
  return;
7870
10901
  }
@@ -7880,7 +10911,7 @@ function createI18nPlugin(resolvedOptions) {
7880
10911
  configureServer(server) {
7881
10912
  if (!i18nOptions) return;
7882
10913
  const dictDir = path$2.resolve(root, i18nOptions.dir);
7883
- if (fs.existsSync(dictDir)) {
10914
+ if (fs$1.existsSync(dictDir)) {
7884
10915
  server.watcher.add(dictDir);
7885
10916
  server.watcher.on("change", (filePath) => {
7886
10917
  if (!filePath.startsWith(dictDir)) return;
@@ -7944,13 +10975,6 @@ function resolveCardOptions(options) {
7944
10975
  return { enabled: options.enabled ?? true };
7945
10976
  }
7946
10977
  //#endregion
7947
- //#region src/file-tree-options.ts
7948
- function resolveFileTreeOptions(options) {
7949
- if (!options) return { enabled: false };
7950
- if (options === true) return { enabled: true };
7951
- return { enabled: options.enabled ?? true };
7952
- }
7953
- //#endregion
7954
10978
  //#region src/include-options.ts
7955
10979
  function resolveIncludeOptions(options) {
7956
10980
  if (!options) return { enabled: false };
@@ -8071,6 +11095,353 @@ async function* renderMarkdownStream(chunks, options = {}) {
8071
11095
  yield renderer.finish();
8072
11096
  }
8073
11097
  //#endregion
11098
+ //#region src/mdx-islands.ts
11099
+ /**
11100
+ * Discover registered MDX islands from the mdast tree or rendered HTML.
11101
+ *
11102
+ * Framework plugins use this instead of a source regex when MDX is on, so
11103
+ * nested JSX, expression attributes, and fragments stay visible. Names that
11104
+ * are not in the global `components` map and are not document-local import
11105
+ * bindings are left as static HTML.
11106
+ */
11107
+ const OX_ISLAND_NAME = /data-ox-island="([^"]+)"/g;
11108
+ /**
11109
+ * Collect named MDX JSX tags from a parsed mdast tree (JSON from NAPI `parse()`).
11110
+ * Fragments (`name: null`) and non-JSX nodes are ignored. Walks nested children
11111
+ * so inner islands are found.
11112
+ */
11113
+ function collectMdxJsxNamesFromAst(ast) {
11114
+ const names = /* @__PURE__ */ new Set();
11115
+ walkMdast(ast, names);
11116
+ return [...names];
11117
+ }
11118
+ /**
11119
+ * Collect `data-ox-island` names from Rust-rendered HTML.
11120
+ * Used when an AST walk is unavailable.
11121
+ */
11122
+ function collectMdxIslandNamesFromHtml(html) {
11123
+ const names = /* @__PURE__ */ new Set();
11124
+ OX_ISLAND_NAME.lastIndex = 0;
11125
+ let match;
11126
+ while ((match = OX_ISLAND_NAME.exec(html)) !== null) {
11127
+ const name = match[1];
11128
+ if (name) names.add(decodeHtmlAttr$1(name));
11129
+ }
11130
+ return [...names];
11131
+ }
11132
+ /** Keep names that exist on the global component map, in first-seen order. */
11133
+ function intersectRegisteredComponentNames(names, components) {
11134
+ return intersectHydratableComponentNames(names, components);
11135
+ }
11136
+ /**
11137
+ * Keep names that are either globally registered or document-local bindings.
11138
+ */
11139
+ function intersectHydratableComponentNames(names, components, localNames) {
11140
+ const local = localNames ? new Set(localNames) : null;
11141
+ const used = [];
11142
+ for (const name of names) if ((local?.has(name) || isRegisteredComponent(name, components)) && !used.includes(name)) used.push(name);
11143
+ return used;
11144
+ }
11145
+ /**
11146
+ * Resolve registered island names for an MDX document.
11147
+ *
11148
+ * Prefers a NAPI `parse()` AST walk. Falls back to rendered `data-ox-island`
11149
+ * names so plugins still hydrate if #659 metadata is not present.
11150
+ */
11151
+ async function discoverRegisteredMdxComponents(input) {
11152
+ return intersectHydratableComponentNames(await tryCollectNamesFromParse(input.source) ?? (input.html !== void 0 ? collectMdxIslandNamesFromHtml(input.html) : []), input.components, input.localNames);
11153
+ }
11154
+ function isRegisteredComponent(name, components) {
11155
+ if (isMapRegistry$1(components)) return components.has(name);
11156
+ if (isPlainObjectRegistry(components)) return Object.prototype.hasOwnProperty.call(components, name);
11157
+ for (const entry of components) if (entry === name) return true;
11158
+ return false;
11159
+ }
11160
+ async function tryCollectNamesFromParse(source) {
11161
+ try {
11162
+ const parsed = (await importNapiModule()).parse(source, {
11163
+ mdx: true,
11164
+ gfm: true
11165
+ });
11166
+ if (!parsed.ast) return null;
11167
+ return collectMdxJsxNamesFromAst(JSON.parse(parsed.ast));
11168
+ } catch {
11169
+ return null;
11170
+ }
11171
+ }
11172
+ function walkMdast(node, names) {
11173
+ if (!node || typeof node !== "object") return;
11174
+ const record = node;
11175
+ if ((record.type === "mdxJsxFlowElement" || record.type === "mdxJsxTextElement") && typeof record.name === "string" && record.name) names.add(record.name);
11176
+ if (Array.isArray(record.children)) for (const child of record.children) walkMdast(child, names);
11177
+ }
11178
+ function isMapRegistry$1(value) {
11179
+ return typeof value === "object" && value !== null && typeof value.has === "function" && typeof value.get === "function";
11180
+ }
11181
+ function isPlainObjectRegistry(value) {
11182
+ return Object.prototype.toString.call(value) === "[object Object]";
11183
+ }
11184
+ function decodeHtmlAttr$1(value) {
11185
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
11186
+ }
11187
+ //#endregion
11188
+ //#region src/document-imports.ts
11189
+ /**
11190
+ * Resolve MDX component imports relative to the document that declared them.
11191
+ *
11192
+ * Only `./` and `../` specifiers become island bindings. Bare, package, and
11193
+ * remote specifiers are reported and ignored. A specifier that leaves the
11194
+ * configured content root is rejected with a diagnostic.
11195
+ */
11196
+ function resolveContentRootPath(input) {
11197
+ if (input.contentRoot) return path.resolve(input.contentRoot);
11198
+ const root = input.root ?? process.cwd();
11199
+ return path.resolve(root, input.srcDir ?? ".");
11200
+ }
11201
+ function stripViteQuery(id) {
11202
+ return id.split("?")[0].split("#")[0];
11203
+ }
11204
+ function resolveDocumentComponentImports(input) {
11205
+ const documentPath = stripViteQuery(input.documentPath);
11206
+ const documentDir = path.dirname(documentPath);
11207
+ const contentRoot = resolveContentRootPath(input);
11208
+ const diagnostics = [];
11209
+ const candidates = [];
11210
+ for (const statement of input.imports) {
11211
+ const specifier = statement.source;
11212
+ if (!isRelativeSpecifier(specifier)) {
11213
+ diagnostics.push({
11214
+ code: "not-relative",
11215
+ message: `Document component import "${specifier}" is not relative and was ignored.`,
11216
+ specifier
11217
+ });
11218
+ continue;
11219
+ }
11220
+ for (const spec of statement.specifiers) {
11221
+ if (spec.kind === "namespace") continue;
11222
+ const resolvedPath = resolveExistingPath(path.resolve(documentDir, specifier));
11223
+ if (!isInsideRoot(resolvedPath, contentRoot)) {
11224
+ diagnostics.push({
11225
+ code: "escapes-root",
11226
+ message: `Document component import "${specifier}" escapes the content root.`,
11227
+ specifier,
11228
+ localName: spec.local
11229
+ });
11230
+ continue;
11231
+ }
11232
+ candidates.push({
11233
+ localName: spec.local,
11234
+ specifier,
11235
+ resolvedPath,
11236
+ importPathRelativeToDocument: toDocumentRelativeImport(documentDir, resolvedPath),
11237
+ imported: spec.imported,
11238
+ kind: spec.kind
11239
+ });
11240
+ }
11241
+ }
11242
+ const counts = /* @__PURE__ */ new Map();
11243
+ for (const binding of candidates) counts.set(binding.localName, (counts.get(binding.localName) ?? 0) + 1);
11244
+ const bindings = [];
11245
+ const reportedDuplicates = /* @__PURE__ */ new Set();
11246
+ for (const binding of candidates) {
11247
+ if ((counts.get(binding.localName) ?? 0) > 1) {
11248
+ if (!reportedDuplicates.has(binding.localName)) {
11249
+ reportedDuplicates.add(binding.localName);
11250
+ diagnostics.push({
11251
+ code: "duplicate-binding",
11252
+ message: `Document component name "${binding.localName}" is imported more than once.`,
11253
+ specifier: binding.specifier,
11254
+ localName: binding.localName
11255
+ });
11256
+ }
11257
+ continue;
11258
+ }
11259
+ bindings.push(binding);
11260
+ }
11261
+ return {
11262
+ bindings,
11263
+ diagnostics
11264
+ };
11265
+ }
11266
+ function isRelativeSpecifier(source) {
11267
+ return source.startsWith("./") || source.startsWith("../");
11268
+ }
11269
+ function resolveExistingPath(filePath) {
11270
+ try {
11271
+ return fs.realpathSync(filePath);
11272
+ } catch {
11273
+ return path.normalize(filePath);
11274
+ }
11275
+ }
11276
+ function isInsideRoot(resolvedPath, root) {
11277
+ const relative = path.relative(resolveExistingPath(root), resolvedPath);
11278
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
11279
+ }
11280
+ function toDocumentRelativeImport(documentDir, resolvedPath) {
11281
+ const relative = path.relative(documentDir, resolvedPath).replace(/\\/g, "/");
11282
+ return relative.startsWith(".") ? relative : `./${relative}`;
11283
+ }
11284
+ //#endregion
11285
+ //#region src/document-islands.ts
11286
+ /**
11287
+ * Combine document-local import resolution with MDX island discovery.
11288
+ */
11289
+ async function discoverDocumentMdxIslands(input) {
11290
+ const resolved = resolveDocumentComponentImports({
11291
+ imports: input.imports,
11292
+ documentPath: input.documentPath,
11293
+ contentRoot: input.contentRoot ?? resolveContentRootPath(input),
11294
+ srcDir: input.srcDir
11295
+ });
11296
+ const localBindings = new Map(resolved.bindings.map((binding) => [binding.localName, binding]));
11297
+ return {
11298
+ usedComponents: await discoverRegisteredMdxComponents({
11299
+ source: input.source,
11300
+ html: input.html,
11301
+ components: input.components,
11302
+ localNames: localBindings.keys()
11303
+ }),
11304
+ localBindings,
11305
+ diagnostics: resolved.diagnostics
11306
+ };
11307
+ }
11308
+ //#endregion
11309
+ //#region src/island-codegen.ts
11310
+ /**
11311
+ * Emit static component imports for framework Markdown modules.
11312
+ *
11313
+ * Document-local bindings win over the global `components` map for that file
11314
+ * only. Two documents that bind the same local name therefore emit different
11315
+ * specifiers and do not share one module id.
11316
+ */
11317
+ function renderIslandComponentImports(usedComponents, input) {
11318
+ const documentDir = path.dirname(stripViteQuery(input.documentPath));
11319
+ const root = input.root || process.cwd();
11320
+ return usedComponents.map((name) => {
11321
+ const local = input.localBindings?.get(name);
11322
+ if (local) return renderLocalImport(local);
11323
+ const componentPath = getGlobalComponentPath(input.globalComponents, name);
11324
+ if (!componentPath) return "";
11325
+ return renderGlobalImport(name, componentPath, documentDir, root);
11326
+ }).filter(Boolean).join("\n");
11327
+ }
11328
+ function renderLocalImport(binding) {
11329
+ const specifier = binding.importPathRelativeToDocument.replace(/\\/g, "/");
11330
+ if (binding.kind === "default") return `import ${binding.localName} from '${specifier}';`;
11331
+ if (binding.imported === binding.localName) return `import { ${binding.imported} } from '${specifier}';`;
11332
+ return `import { ${binding.imported} as ${binding.localName} } from '${specifier}';`;
11333
+ }
11334
+ function renderGlobalImport(name, componentPath, documentDir, root) {
11335
+ const absolutePath = path.resolve(root, componentPath.replace(/^\.\//, ""));
11336
+ const relativePath = path.relative(documentDir, absolutePath).replace(/\\/g, "/");
11337
+ return `import ${name} from '${relativePath.startsWith(".") ? relativePath : `./${relativePath}`}';`;
11338
+ }
11339
+ function getGlobalComponentPath(components, name) {
11340
+ if (isMapRegistry(components)) return components.get(name);
11341
+ return Object.prototype.hasOwnProperty.call(components, name) ? components[name] : void 0;
11342
+ }
11343
+ function isMapRegistry(value) {
11344
+ return typeof value === "object" && value !== null && typeof value.has === "function" && typeof value.get === "function";
11345
+ }
11346
+ //#endregion
11347
+ //#region src/island-ssr.ts
11348
+ const RUST_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
11349
+ "props",
11350
+ "expressions",
11351
+ "spreads"
11352
+ ]);
11353
+ const PAYLOAD_SCRIPT = /^\s*<script type="application\/json">[\s\S]*?<\/script>/;
11354
+ async function applyIslandSsrHtml(html, renderIsland, filePath, names) {
11355
+ const allowed = names ? new Set(names) : null;
11356
+ const islands = findIslandRanges(html);
11357
+ let output = html;
11358
+ for (const island of islands.toReversed()) {
11359
+ if (allowed && !allowed.has(island.name)) continue;
11360
+ const script = output.slice(island.innerStart, island.closeStart).match(PAYLOAD_SCRIPT)?.[0] ?? "";
11361
+ const props = parseIslandProps(island.propsAttr, script);
11362
+ const ssrHtml = await renderIsland(island.name, props, filePath);
11363
+ output = output.slice(0, island.innerStart) + script + ssrHtml + output.slice(island.closeStart);
11364
+ }
11365
+ return output;
11366
+ }
11367
+ function findIslandRanges(html) {
11368
+ const ranges = [];
11369
+ const openRe = /<(div|span)\b([^>]*\bdata-ox-island="([^"]+)"[^>]*)>/gi;
11370
+ let match;
11371
+ while ((match = openRe.exec(html)) !== null) {
11372
+ const tag = match[1];
11373
+ const name = decodeHtmlAttr(match[3] ?? "");
11374
+ if (!tag || !name) continue;
11375
+ const innerStart = match.index + match[0].length;
11376
+ const closeStart = findMatchingClose(html, innerStart, tag);
11377
+ ranges.push({
11378
+ name,
11379
+ innerStart,
11380
+ closeStart,
11381
+ propsAttr: matchAttr(match[2] ?? "", "data-ox-props")
11382
+ });
11383
+ }
11384
+ return ranges;
11385
+ }
11386
+ function findMatchingClose(html, from, tag) {
11387
+ const openNeedle = `<${tag}`;
11388
+ const closeNeedle = `</${tag}>`;
11389
+ let depth = 1;
11390
+ let cursor = from;
11391
+ while (cursor < html.length) {
11392
+ const nextOpen = indexOfTagOpen(html, openNeedle, cursor);
11393
+ const nextClose = html.indexOf(closeNeedle, cursor);
11394
+ if (nextClose === -1) return html.length;
11395
+ if (nextOpen !== -1 && nextOpen < nextClose) {
11396
+ depth += 1;
11397
+ cursor = nextOpen + openNeedle.length;
11398
+ } else {
11399
+ depth -= 1;
11400
+ if (depth === 0) return nextClose;
11401
+ cursor = nextClose + closeNeedle.length;
11402
+ }
11403
+ }
11404
+ return html.length;
11405
+ }
11406
+ function indexOfTagOpen(html, openNeedle, from) {
11407
+ let cursor = from;
11408
+ while (cursor < html.length) {
11409
+ const index = html.indexOf(openNeedle, cursor);
11410
+ if (index === -1) return -1;
11411
+ const next = html[index + openNeedle.length];
11412
+ if (next === " " || next === ">" || next === " " || next === "\n" || next === "/") return index;
11413
+ cursor = index + openNeedle.length;
11414
+ }
11415
+ return -1;
11416
+ }
11417
+ function matchAttr(attrs, name) {
11418
+ const match = new RegExp(`\\b${name}="([^"]*)"`, "i").exec(attrs);
11419
+ return match?.[1] === void 0 ? void 0 : decodeHtmlAttr(match[1]);
11420
+ }
11421
+ function parseIslandProps(propsAttr, script) {
11422
+ const fromAttr = propsAttr ? tryParseJson(propsAttr) : void 0;
11423
+ if (fromAttr) return unwrapIslandProps(fromAttr);
11424
+ const scriptBody = script.match(/<script type="application\/json">([\s\S]*?)<\/script>/i)?.[1];
11425
+ return scriptBody ? unwrapIslandProps(tryParseJson(scriptBody) ?? {}) : {};
11426
+ }
11427
+ function tryParseJson(value) {
11428
+ try {
11429
+ return JSON.parse(value);
11430
+ } catch {
11431
+ return;
11432
+ }
11433
+ }
11434
+ function unwrapIslandProps(parsed) {
11435
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
11436
+ const record = parsed;
11437
+ const keys = Object.keys(record);
11438
+ 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;
11439
+ return record;
11440
+ }
11441
+ function decodeHtmlAttr(value) {
11442
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
11443
+ }
11444
+ //#endregion
8074
11445
  //#region src/framework.ts
8075
11446
  function createFrameworkMarkdownOptions(options) {
8076
11447
  return {
@@ -8087,6 +11458,7 @@ function createFrameworkMarkdownOptions(options) {
8087
11458
  lastUpdated: false,
8088
11459
  pagination: false,
8089
11460
  breadcrumbs: false,
11461
+ jsonLd: false,
8090
11462
  readerChrome: false,
8091
11463
  localeSwitcher: false,
8092
11464
  a11y: false,
@@ -8097,6 +11469,10 @@ function createFrameworkMarkdownOptions(options) {
8097
11469
  robots: true,
8098
11470
  llms: true
8099
11471
  },
11472
+ pwa: {
11473
+ enabled: false,
11474
+ offline: true
11475
+ },
8100
11476
  publishState: {
8101
11477
  enabled: false,
8102
11478
  includeDrafts: false
@@ -8112,6 +11488,7 @@ function createFrameworkMarkdownOptions(options) {
8112
11488
  allowExternal: false
8113
11489
  },
8114
11490
  gfm: options.gfm,
11491
+ mdx: options.mdx,
8115
11492
  frontmatter: options.frontmatter ?? false,
8116
11493
  toc: options.toc,
8117
11494
  tocMaxDepth: options.tocMaxDepth,
@@ -8184,7 +11561,11 @@ function createFrameworkMarkdownOptions(options) {
8184
11561
  includes: { enabled: false },
8185
11562
  cards: { enabled: false },
8186
11563
  steps: { enabled: false },
8187
- fileTree: { enabled: false },
11564
+ fileTree: {
11565
+ enabled: false,
11566
+ defaultOpen: true,
11567
+ icons: true
11568
+ },
8188
11569
  sanitize: { enabled: false },
8189
11570
  editThisPage: {
8190
11571
  enabled: false,
@@ -8282,7 +11663,7 @@ async function collectMarkdownDocsTests(options, cwd) {
8282
11663
  const blocks = [];
8283
11664
  let index = 0;
8284
11665
  for (const [sourcePath, relativePath] of [...files.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
8285
- const extracted = await extractDocsTests(await fs$1.readFile(sourcePath, "utf-8"), {
11666
+ const extracted = await extractDocsTests(await fs$2.readFile(sourcePath, "utf-8"), {
8286
11667
  languages: options.languages,
8287
11668
  requireMeta: options.requireMeta
8288
11669
  });
@@ -8368,18 +11749,18 @@ async function writeDocsTestFiles(options) {
8368
11749
  ...options,
8369
11750
  cwd
8370
11751
  });
8371
- if (clean) await fs$1.rm(generatedDir, {
11752
+ if (clean) await fs$2.rm(generatedDir, {
8372
11753
  recursive: true,
8373
11754
  force: true
8374
11755
  });
8375
- await fs$1.mkdir(generatedDir, { recursive: true });
11756
+ await fs$2.mkdir(generatedDir, { recursive: true });
8376
11757
  return {
8377
11758
  cwd,
8378
11759
  generatedDir,
8379
11760
  blocks,
8380
11761
  files: await Promise.all(blocks.map(async (block) => {
8381
11762
  const filePath = path$1.join(generatedDir, docsTestFileName(block));
8382
- await fs$1.writeFile(filePath, renderDocsTestFile(block, options), "utf-8");
11763
+ await fs$2.writeFile(filePath, renderDocsTestFile(block, options), "utf-8");
8383
11764
  return {
8384
11765
  filePath,
8385
11766
  sourcePath: block.sourcePath,
@@ -8864,7 +12245,7 @@ async function lintMarkdownFile(filePath, options = {}) {
8864
12245
  async function lintMarkdownFiles(options = {}) {
8865
12246
  const resolvedOptions = resolveMarkdownLintFileOptions(options);
8866
12247
  const matchedFiles = await collectMarkdownLintFileEntries(resolvedOptions);
8867
- const results = await lintMatchedMarkdownFiles(matchedFiles, await Promise.all(matchedFiles.map((file) => fs$1.readFile(file.filePath, "utf-8"))), resolvedOptions.lintOptions);
12248
+ const results = await lintMatchedMarkdownFiles(matchedFiles, await Promise.all(matchedFiles.map((file) => fs$2.readFile(file.filePath, "utf-8"))), resolvedOptions.lintOptions);
8868
12249
  const files = matchedFiles.map((file, index) => ({
8869
12250
  ...results[index] ?? createEmptyLintResult(),
8870
12251
  filePath: file.filePath,
@@ -8908,7 +12289,7 @@ async function lintMarkdownFileWithResolvedOptions(filePath, options) {
8908
12289
  skipped: true
8909
12290
  };
8910
12291
  return {
8911
- ...await lintMarkdownAsync(await fs$1.readFile(absoluteFilePath, "utf-8"), {
12292
+ ...await lintMarkdownAsync(await fs$2.readFile(absoluteFilePath, "utf-8"), {
8912
12293
  ...options.lintOptions,
8913
12294
  mdx: resolveMdxForFilePath(absoluteFilePath, options.lintOptions.mdx)
8914
12295
  }),
@@ -9019,6 +12400,7 @@ function oxContent(options = {}) {
9019
12400
  createCollectionsPlugin(resolvedOptions, getRoot),
9020
12401
  createSearchPlugin(resolvedOptions, getRoot)
9021
12402
  ];
12403
+ if (resolvedOptions.math.enabled) plugins.push(createKatexAssetsPlugin());
9022
12404
  if (resolvedOptions.i18n) plugins.push(createI18nPlugin(resolvedOptions));
9023
12405
  if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
9024
12406
  return plugins;
@@ -9166,6 +12548,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
9166
12548
  for (const error of result.errors) console.warn(`[ox-content] ${error}`);
9167
12549
  } catch (err) {
9168
12550
  console.error("[ox-content] SSG build failed:", err);
12551
+ if (err instanceof PageResourceError) throw err;
9169
12552
  }
9170
12553
  }
9171
12554
  };
@@ -9271,9 +12654,12 @@ function resolveOptions(options) {
9271
12654
  permalinks: resolvePermalinksOptions(options.permalinks),
9272
12655
  cascade: resolveCascadeOptions(options.cascade),
9273
12656
  redirects: resolveRedirectsOptions(options.redirects),
12657
+ blog: resolveBlogOptions(options.blog ?? (typeof options.ssg === "object" && options.ssg ? options.ssg.blog : void 0)),
9274
12658
  feeds: resolveFeedsOptions(options.feeds),
12659
+ pwa: resolvePwaOptions(options.pwa),
9275
12660
  taxonomies: resolveTaxonomiesOptions(options.taxonomies),
9276
12661
  versions: resolveVersionsOptions(options.versions),
12662
+ resources: resolveResourcesOptions(options.resources),
9277
12663
  gfm: options.gfm ?? true,
9278
12664
  mdx: options.mdx,
9279
12665
  footnotes: options.footnotes ?? true,
@@ -9299,6 +12685,7 @@ function resolveOptions(options) {
9299
12685
  cjkEmphasis: options.cjkEmphasis ?? false,
9300
12686
  codeBlockLint: resolveCodeBlockLintOptions(options.codeBlockLint),
9301
12687
  codeBlockTypecheck: resolveCodeBlockTypecheckOptions(options.codeBlockTypecheck),
12688
+ typedHover: resolveTypedHoverOptions(options.typedHover),
9302
12689
  docsTests: resolveDocsTestOptions(options.docsTests),
9303
12690
  mermaid: options.mermaid ?? false,
9304
12691
  math: resolveMathOptions(options.math),
@@ -9595,6 +12982,6 @@ function normalizeRuntimeBase(base) {
9595
12982
  return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
9596
12983
  }
9597
12984
  //#endregion
9598
- export { DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, DocsTestRunError, Fragment, IncrementalMarkdownParser, IncrementalMarkdownRenderer, buildCollectionManifest, buildSearchIndex, buildSsg, classifyPublishState, clearRenderContext, collectDocsTests, collectGitHubRepos, collectGitHubSources, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createFrameworkMarkdownOptions, createI18nPlugin, createIncrementalMarkdownParser, createIncrementalMarkdownRenderer, createMarkdownEnvironment, createTheme, defaultTheme, defineCollection, defineCollections, defineTheme, each, escapeSvelteMarkup, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateCollectionsVirtualModule, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, isMarkdownFilePath, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, parsePageChromeFlags, partitionPublishedPages, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, renderAllPages, renderHtmlToFrameworkCode, renderHtmlToReactComponent, renderHtmlToReactCreateElement, renderHtmlToSvelteComponent, renderHtmlToVueComponent, renderHtmlToVueH, renderMarkdownStream, renderPage, renderToString, resolveBadgeOptions, resolveBuiltinEmbedOptions, resolveCardOptions, resolveCascadeOptions, resolveCollectionsOptions, resolveDocsOptions, resolveFeedsOptions, resolveFileTreeOptions, resolveHeaderNavItems, resolveI18nOptions, resolveImageOptions, resolveIncludeOptions, resolveLocaleLabel, resolveMathOptions, resolveNotFoundOptions, resolveOgImageOptions, resolvePageChromeOption, resolvePermalinksOptions, resolvePublishStateOptions, resolveRedirectsOptions, resolveSearchOptions, resolveSiteMapsOptions, resolveSsgOptions, resolveStepsOptions, resolveTaxonomiesOptions, resolveTeamOptions, resolveTheme, resolveVersionsOptions, runDocsTests, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeDocsTestFiles, writeSearchIndex };
12985
+ export { DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, DocsTestRunError, Fragment, IncrementalMarkdownParser, IncrementalMarkdownRenderer, PageResourceError, applyIslandSsrHtml, buildCollectionManifest, buildSearchIndex, buildSsg, classifyPublishState, clearRenderContext, collectDocsTests, collectGitHubRepos, collectGitHubSources, collectMdxIslandNamesFromHtml, collectMdxJsxNamesFromAst, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createFrameworkMarkdownOptions, createI18nPlugin, createIncrementalMarkdownParser, createIncrementalMarkdownRenderer, createMarkdownEnvironment, createTheme, defaultTheme, defineCollection, defineCollections, defineTheme, discoverDocumentMdxIslands, discoverRegisteredMdxComponents, each, escapeSvelteMarkup, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateCollectionsVirtualModule, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, intersectHydratableComponentNames, intersectRegisteredComponentNames, isMarkdownFilePath, isMdxFilePath, isRegisteredComponent, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, parsePageChromeFlags, partitionPublishedPages, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, readingTimeMinutes, renderAllPages, renderHtmlToFrameworkCode, renderHtmlToReactComponent, renderHtmlToReactCreateElement, renderHtmlToSvelteComponent, renderHtmlToVueComponent, renderHtmlToVueH, renderIslandComponentImports, renderMarkdownStream, renderPage, renderToString, resolveBadgeOptions, resolveBlogCollectionName, resolveBlogOptions, resolveBuiltinEmbedOptions, resolveCardOptions, resolveCascadeOptions, resolveCollectionsOptions, resolveContentRootPath, resolveDocsOptions, resolveDocumentComponentImports, resolveFeedsOptions, resolveFileTreeOptions, resolveHeaderNavItems, resolveI18nOptions, resolveImageOptions, resolveIncludeOptions, resolveLocaleLabel, resolveMathOptions, resolveMdxForFilePath, resolveNotFoundOptions, resolveOgImageOptions, resolvePageChromeOption, resolvePermalinksOptions, resolvePublishStateOptions, resolvePwaOptions, resolveRedirectsOptions, resolveResourcesOptions, resolveSearchOptions, resolveSectionIndexOptions, resolveSiteMapsOptions, resolveSsgOptions, resolveStepsOptions, resolveTaxonomiesOptions, resolveTeamOptions, resolveTheme, resolveTypedHoverOptions, resolveVersionsOptions, runDocsTests, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, stripViteQuery, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeDocsTestFiles, writeSearchIndex };
9599
12986
 
9600
12987
  //# sourceMappingURL=index.mjs.map