@thallylabs/migrate 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,15 +9,15 @@ import {
9
9
  openSync,
10
10
  readFileSync as readFileSync6,
11
11
  readSync,
12
- readdirSync as readdirSync3,
13
- realpathSync as realpathSync3,
12
+ readdirSync as readdirSync4,
13
+ realpathSync as realpathSync4,
14
14
  rmSync as rmSync2,
15
15
  statSync
16
16
  } from "fs";
17
17
  import { createRequire } from "module";
18
18
  import { basename as basename3, dirname as dirname4, extname as extname6, isAbsolute as isAbsolute3, posix as posix4, relative as relative6, resolve as resolvePath, sep as sep3 } from "path";
19
19
  import { parse as parseYaml6 } from "yaml";
20
- import * as ts4 from "typescript";
20
+ import * as ts5 from "typescript";
21
21
 
22
22
  // src/inline-extraction.ts
23
23
  import ts from "typescript";
@@ -808,6 +808,7 @@ import { unified } from "unified";
808
808
  import { extname } from "path";
809
809
 
810
810
  // src/path.ts
811
+ import { realpathSync } from "fs";
811
812
  import { isAbsolute, relative, resolve, sep } from "path";
812
813
  var SAFE_SEGMENT = /[^a-z0-9._-]+/gi;
813
814
  function trimTrailingSlashes(value) {
@@ -829,7 +830,8 @@ function slugifySegment(value, preserveCase = false) {
829
830
  } catch {
830
831
  }
831
832
  const normalized = preserveCase ? decoded : decoded.toLowerCase();
832
- return normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
833
+ const slug2 = normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
834
+ return slug2 === "." || slug2 === ".." ? "" : slug2;
833
835
  }
834
836
  function pageIdFromReference(value, preserveCase = false) {
835
837
  const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
@@ -884,6 +886,22 @@ function normalizeAssetPath(value) {
884
886
  if (segments.some((segment) => segment === "." || segment === "..")) return null;
885
887
  return segments.join("/");
886
888
  }
889
+ function withinRealRoot(candidate, repositoryRoot) {
890
+ let real;
891
+ try {
892
+ real = realpathSync(candidate);
893
+ } catch {
894
+ return candidate;
895
+ }
896
+ const realRoot = realpathSync(repositoryRoot);
897
+ if (real !== realRoot && !real.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep)) {
898
+ throw new Error(`Migration path escapes its root: ${candidate}`);
899
+ }
900
+ return candidate;
901
+ }
902
+ function isPathEscapeError(error) {
903
+ return error instanceof Error && /escapes its root|Unsafe migration path/.test(error.message);
904
+ }
887
905
 
888
906
  // src/mintlify-extras.ts
889
907
  function objectValue(value) {
@@ -1048,7 +1066,7 @@ function playgroundDisplay(value, where, warn) {
1048
1066
  warn?.(`${where} "auth" requires reader sign-in, which Thally does not support. Migrated as "simple" (no Try it). Set it to "interactive" to allow everyone.`);
1049
1067
  return "simple";
1050
1068
  }
1051
- warn?.(`${where} ${JSON.stringify(value)} is not one of interactive, simple, none, auth and was dropped.`);
1069
+ warn?.(`${where} is not one of interactive, simple, none, auth and was dropped.`);
1052
1070
  return void 0;
1053
1071
  }
1054
1072
 
@@ -1059,13 +1077,23 @@ function titleFromId(id) {
1059
1077
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1060
1078
  }
1061
1079
  var descriptionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
1062
- function firstParagraph(content) {
1063
- function plainText(node) {
1064
- if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
1065
- if (node.type === "image") return node.alt ?? "";
1066
- if (node.type === "break") return " ";
1067
- return (node.children ?? []).map(plainText).join("");
1080
+ function plainText(node) {
1081
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
1082
+ if (node.type === "image") return node.alt ?? "";
1083
+ if (node.type === "break") return " ";
1084
+ return (node.children ?? []).map(plainText).join("");
1085
+ }
1086
+ function mdxVisibleText(content, definitions) {
1087
+ const references = [...content.matchAll(/\[([^\]\n]+)\]/g)].map((match) => definitions?.get(match[1].trim().replace(/\s+/g, " ").toLowerCase()) ?? "").join("\n");
1088
+ try {
1089
+ return plainText(descriptionParser.parse(`# ${content}
1090
+
1091
+ ${references}`)).trim();
1092
+ } catch {
1093
+ return content;
1068
1094
  }
1095
+ }
1096
+ function firstParagraph(content) {
1069
1097
  function findParagraph(node) {
1070
1098
  if (node.type === "paragraph") {
1071
1099
  const value = plainText(node).replace(/\s+/g, " ").trim();
@@ -1089,6 +1117,97 @@ function docusaurusAdmonitionTag(kind) {
1089
1117
  if (kind === "caution" || kind === "warning") return "Warning";
1090
1118
  return "Note";
1091
1119
  }
1120
+ function scanJsxOpeningTag(source, start, name) {
1121
+ const opener = `<${name}`;
1122
+ let quote = null;
1123
+ let depth = 0;
1124
+ let braceQuote = null;
1125
+ let lineComment = false;
1126
+ let blockComment = false;
1127
+ const attributesStart = start + opener.length;
1128
+ for (let index = attributesStart; index < source.length; index++) {
1129
+ const char = source[index];
1130
+ if (char === "<" && !quote && !braceQuote && depth === 0 && !lineComment && !blockComment && source.startsWith(opener, index) && !/[\w$.-]/.test(source[index + opener.length] ?? "")) {
1131
+ return { attributes: source.slice(attributesStart, index), end: null, next: index };
1132
+ }
1133
+ if (lineComment) {
1134
+ if (char === "\n" || char === "\r") lineComment = false;
1135
+ } else if (blockComment) {
1136
+ if (char === "*" && source[index + 1] === "/") {
1137
+ blockComment = false;
1138
+ index++;
1139
+ }
1140
+ } else if (braceQuote) {
1141
+ if (char === "\\") index++;
1142
+ else if (char === braceQuote) braceQuote = null;
1143
+ } else if (depth > 0) {
1144
+ if (char === "/" && source[index + 1] === "/") {
1145
+ lineComment = true;
1146
+ index++;
1147
+ } else if (char === "/" && source[index + 1] === "*") {
1148
+ blockComment = true;
1149
+ index++;
1150
+ } else if (char === '"' || char === "'" || char === "`") braceQuote = char;
1151
+ else if (char === "{") depth++;
1152
+ else if (char === "}") depth--;
1153
+ } else if (quote) {
1154
+ if (char === quote) quote = null;
1155
+ } else if (char === '"' || char === "'") quote = char;
1156
+ else if (char === "{") depth++;
1157
+ else if (char === ">") return { attributes: source.slice(attributesStart, index), end: index, next: index + 1 };
1158
+ }
1159
+ return { attributes: source.slice(attributesStart), end: null, next: source.length };
1160
+ }
1161
+ function normalizeDocusaurusAdmonitionTags(segment) {
1162
+ const open = [];
1163
+ const marker = /<\/Admonition>|<Admonition(?![\w$.-])/g;
1164
+ let output = "";
1165
+ let cursor = 0;
1166
+ for (let match = marker.exec(segment); match; match = marker.exec(segment)) {
1167
+ if (match[0] === "</Admonition>") {
1168
+ output += `${segment.slice(cursor, match.index)}
1169
+ </${open.pop() ?? "Note"}>`;
1170
+ cursor = marker.lastIndex;
1171
+ continue;
1172
+ }
1173
+ const tag = scanJsxOpeningTag(segment, match.index, "Admonition");
1174
+ marker.lastIndex = tag.next;
1175
+ if (tag.end === null) continue;
1176
+ const type = tag.attributes.match(/\btype=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean)?.toLowerCase() ?? "note";
1177
+ const title = tag.attributes.match(/\btitle=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean)?.trim();
1178
+ const callout = docusaurusAdmonitionTag(type);
1179
+ open.push(callout);
1180
+ output += `${segment.slice(cursor, match.index)}${title ? `<${callout}>
1181
+ **${title}**
1182
+ ` : `<${callout}>
1183
+ `}`;
1184
+ cursor = tag.end + 1;
1185
+ marker.lastIndex = cursor;
1186
+ }
1187
+ return output + segment.slice(cursor);
1188
+ }
1189
+ function normalizeDocusaurusLinkTags(segment) {
1190
+ const marker = /<Link(?![\w$.-])/g;
1191
+ let output = "";
1192
+ let cursor = 0;
1193
+ for (let match = marker.exec(segment); match; match = marker.exec(segment)) {
1194
+ const tag = scanJsxOpeningTag(segment, match.index, "Link");
1195
+ marker.lastIndex = tag.next;
1196
+ if (tag.end === null) continue;
1197
+ output += `${segment.slice(cursor, match.index)}<a${tag.attributes.replace(/(^|\s)to=/, "$1href=")}>`;
1198
+ cursor = tag.end + 1;
1199
+ marker.lastIndex = cursor;
1200
+ }
1201
+ return (output + segment.slice(cursor)).replace(/<\/Link>/g, "</a>");
1202
+ }
1203
+ function normalizeDocusaurusThemedImages(segment) {
1204
+ return segment.replace(/<ThemedImage\b([^<]*?)\/>/g, (original, attributes) => {
1205
+ const light = attributes.match(/\blight\s*:\s*(?:useBaseUrl\(\s*)?(?:"([^"]+)"|'([^']+)')/)?.slice(1).find(Boolean);
1206
+ if (!light) return original;
1207
+ const alt = attributes.match(/\balt=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find((value) => value !== void 0);
1208
+ return `<img src="${light.replace(/"/g, "&quot;")}"${alt === void 0 ? "" : ` alt="${alt.replace(/"/g, "&quot;")}"`} />`;
1209
+ });
1210
+ }
1092
1211
  function normalizeDocusaurusAdmonitions(body) {
1093
1212
  const lines = body.split("\n");
1094
1213
  const openAdmonitions = [];
@@ -1260,10 +1379,51 @@ var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
1260
1379
  "BigInt",
1261
1380
  "Reflect"
1262
1381
  ]);
1382
+ function bindSnippetProps(body, properties) {
1383
+ if (!body.includes("props") || properties.size === 0) return body;
1384
+ let tree;
1385
+ try {
1386
+ tree = descriptionParser.parse(body);
1387
+ } catch {
1388
+ return body;
1389
+ }
1390
+ const edits = [];
1391
+ const inspectExpression = (value) => {
1392
+ if (!value || typeof value !== "object") return;
1393
+ const node = value;
1394
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration") {
1395
+ const names = /* @__PURE__ */ new Set();
1396
+ node.params.forEach((param) => collectPatternNames(param, names));
1397
+ if (names.has("props")) return;
1398
+ }
1399
+ if (node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "props") {
1400
+ const key = !node.computed && node.property.type === "Identifier" ? node.property.name : node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : void 0;
1401
+ const replacement = key === void 0 ? void 0 : properties.get(key);
1402
+ if (replacement !== void 0 && body.slice(node.start, node.end).startsWith("props")) {
1403
+ edits.push({ start: node.start, end: node.end, value: JSON.stringify(replacement) });
1404
+ return;
1405
+ }
1406
+ }
1407
+ for (const child of Object.values(value)) {
1408
+ if (Array.isArray(child)) child.forEach(inspectExpression);
1409
+ else if (child && typeof child === "object") inspectExpression(child);
1410
+ }
1411
+ };
1412
+ const visit = (value) => {
1413
+ if (!value || typeof value !== "object") return;
1414
+ const node = value;
1415
+ if (node.data?.estree) inspectExpression(node.data.estree);
1416
+ for (const attribute of node.attributes ?? []) visit(attribute.value);
1417
+ for (const child of node.children ?? []) visit(child);
1418
+ };
1419
+ visit(tree);
1420
+ return edits.sort((a, b) => b.start - a.start).reduce((text2, edit) => text2.slice(0, edit.start) + edit.value + text2.slice(edit.end), body);
1421
+ }
1263
1422
  function leadingPageHeading(body) {
1423
+ if (!/^ {0,3}(?:#(?:[ \t]|$)|=+[ \t\r]*$)/m.test(body)) return void 0;
1264
1424
  try {
1265
1425
  const root = descriptionParser.parse(body);
1266
- const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html");
1426
+ const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html" && !(node.type === "mdxFlowExpression" && node.data?.estree?.body.length === 0));
1267
1427
  if (first?.type !== "heading" || first.depth !== 1) return void 0;
1268
1428
  const text2 = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text2).join("");
1269
1429
  const title = text2(first).replace(/\s+/g, " ").trim();
@@ -1274,6 +1434,12 @@ function leadingPageHeading(body) {
1274
1434
  return void 0;
1275
1435
  }
1276
1436
  }
1437
+ function resolvePageDisplayHeading(page, platform) {
1438
+ const heading = leadingPageHeading(page.body);
1439
+ if (!heading || heading.title !== page.title && platform !== "docusaurus") return;
1440
+ if (heading.title !== page.title) page.headingTitle = heading.title;
1441
+ page.body = `${page.body.slice(0, heading.start)}${page.body.slice(heading.end)}`.trim();
1442
+ }
1277
1443
  function collectPatternNames(pattern, names) {
1278
1444
  switch (pattern.type) {
1279
1445
  case "Identifier":
@@ -2045,13 +2211,13 @@ function normalizeMintlifyUpdateLabels(body) {
2045
2211
  } catch {
2046
2212
  return match;
2047
2213
  }
2048
- const plainText = (node) => {
2214
+ const plainText2 = (node) => {
2049
2215
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
2050
2216
  if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" || node.type === "mdxjsEsm") return null;
2051
- const parts = (node.children ?? []).map(plainText);
2217
+ const parts = (node.children ?? []).map(plainText2);
2052
2218
  return parts.some((part) => part === null) ? null : parts.join("");
2053
2219
  };
2054
- const label = plainText(root)?.replace(/\s+/g, " ").trim();
2220
+ const label = plainText2(root)?.replace(/\s+/g, " ").trim();
2055
2221
  return label && !/[{}]/.test(label) ? `${prefix}{${JSON.stringify(label)}}` : match;
2056
2222
  });
2057
2223
  }
@@ -2334,11 +2500,12 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2334
2500
  const runMintlify = platform === "mintlify";
2335
2501
  const runDocusaurus = platform === "docusaurus";
2336
2502
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
2337
- let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(
2503
+ let rewritten = normalizeExplicitHeadingIds(
2338
2504
  runFern ? normalizeFernFileTrees(normalizeFernCallouts(normalizeNestedCodeFences(sourceBody))) : normalizeNestedCodeFences(sourceBody),
2339
2505
  void 0,
2340
2506
  { headingMarkers: runMintlify }
2341
- ));
2507
+ );
2508
+ if (runDocusaurus) rewritten = normalizeDocusaurusAdmonitions(rewritten);
2342
2509
  if (runMintlify) rewritten = markMintlifyHeadings(rewritten);
2343
2510
  if (runDocusaurus) {
2344
2511
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
@@ -2352,6 +2519,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2352
2519
  return `${attribute}="/${path.replace(/^\.\//, "").replace(/^\/+/, "")}"`;
2353
2520
  }
2354
2521
  ));
2522
+ rewritten = replaceOutsideCode(rewritten, (segment) => normalizeDocusaurusThemedImages(normalizeDocusaurusAdmonitionTags(segment)));
2355
2523
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
2356
2524
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
2357
2525
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -2366,10 +2534,11 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2366
2534
  rewritten = replaceOutsideCode(rewritten, (segment) => {
2367
2535
  let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
2368
2536
  if (runDocusaurus) {
2369
- result = result.replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
2537
+ result = result.replace(/<TabItem\b([^<>]*)>/g, (_match, attributes) => {
2370
2538
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
2371
2539
  return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
2372
- }).replace(/<\/TabItem>/g, "</Tab>").replace(/<Link\b([^>]*)\bto=(?:"([^"]*)"|'([^']*)')([^>]*)>/g, (_match, before, doubleQuoted, singleQuoted, after) => `<a${before}href="${doubleQuoted ?? singleQuoted}"${after}>`).replace(/<\/Link>/g, "</a>").replace(/<(?:DocCardList|TOCInline)\b[^>]*\/>/g, "");
2540
+ }).replace(/<\/TabItem>/g, "</Tab>");
2541
+ result = normalizeDocusaurusLinkTags(result).replace(/<(?:DocCardList|TOCInline)\b[^<>]*\/>/g, "");
2373
2542
  }
2374
2543
  if (runMintlify) {
2375
2544
  result = normalizeMintlifyUpdateLabels(result).replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
@@ -2399,16 +2568,14 @@ function parseMarkdownPage(input) {
2399
2568
  ...input.locale ? { locale: input.locale } : {}
2400
2569
  };
2401
2570
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
2402
- let body = removeUndefinedExpressions(injectReactHookImports(normalizeMdx(parsed.content, input.platform)), input.warn).trim();
2571
+ const body = removeUndefinedExpressions(injectReactHookImports(normalizeMdx(parsed.content, input.platform)), input.warn).trim();
2403
2572
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
2404
2573
  const heading = leadingPageHeading(body);
2405
2574
  const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
2406
- if (heading && heading.title === title) {
2407
- body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
2408
- }
2409
- const navTitle = typeof parsed.data.sidebarTitle === "string" && parsed.data.sidebarTitle.trim() ? parsed.data.sidebarTitle.trim() : typeof parsed.data.navTitle === "string" && parsed.data.navTitle.trim() ? parsed.data.navTitle.trim() : void 0;
2575
+ const sidebarTitle = input.platform === "docusaurus" ? parsed.data.sidebar_label : parsed.data.sidebarTitle;
2576
+ const navTitle = typeof sidebarTitle === "string" && sidebarTitle.trim() ? sidebarTitle.trim() : typeof parsed.data.navTitle === "string" && parsed.data.navTitle.trim() ? parsed.data.navTitle.trim() : void 0;
2410
2577
  const icon = typeof parsed.data.icon === "string" && parsed.data.icon.trim() ? parsed.data.icon.trim() : void 0;
2411
- const iconType = icon && ["regular", "solid", "outline", "brands"].includes(String(parsed.data.iconType)) ? parsed.data.iconType : void 0;
2578
+ const iconType = icon && typeof parsed.data.iconType === "string" && ["regular", "solid", "outline", "brands"].includes(parsed.data.iconType) ? parsed.data.iconType : void 0;
2412
2579
  const meta = {};
2413
2580
  for (const [source, field] of PAGE_META_FIELDS) {
2414
2581
  const value = parsed.data[source];
@@ -2418,7 +2585,7 @@ function parseMarkdownPage(input) {
2418
2585
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
2419
2586
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
2420
2587
  const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : typeof parsed.data.openapi === "string" && parsed.data.openapi.trim() ? "" : firstParagraph(body);
2421
- return {
2588
+ const page = {
2422
2589
  id: identity.id,
2423
2590
  navigationId: identity.navigationId,
2424
2591
  locale: identity.locale,
@@ -2440,6 +2607,8 @@ function parseMarkdownPage(input) {
2440
2607
  source: input.source,
2441
2608
  ...parsed.error ? { frontmatterError: parsed.error } : {}
2442
2609
  };
2610
+ resolvePageDisplayHeading(page, input.platform);
2611
+ return page;
2443
2612
  }
2444
2613
  var PAGE_META_FIELDS = [
2445
2614
  ["og:title", "ogTitle"],
@@ -2463,7 +2632,7 @@ function apiFrontmatter(data, warn) {
2463
2632
  if (data.authMethod !== void 0 && data.authMethod !== null) {
2464
2633
  const method = typeof data.authMethod === "string" ? data.authMethod.trim().toLowerCase() : "";
2465
2634
  if (AUTH_METHODS.has(method)) result.authMethod = method;
2466
- else warn?.(`The page's "authMethod" frontmatter ${JSON.stringify(data.authMethod)} is not one of bearer, basic, key, none and was dropped.`);
2635
+ else warn?.(`The page's "authMethod" frontmatter is not one of bearer, basic, key, none and was dropped.`);
2467
2636
  }
2468
2637
  return result;
2469
2638
  }
@@ -2499,6 +2668,10 @@ function normalizeIndentedFences(content) {
2499
2668
  }
2500
2669
  const match = line.match(/^([ \t]{4,})(`{3,}|~{3,})/);
2501
2670
  if (match) {
2671
+ if (listIndent !== void 0 && match[1].length > listIndent) {
2672
+ ordinaryFence = match[2];
2673
+ return line;
2674
+ }
2502
2675
  active = { indent: match[1], marker: match[2] };
2503
2676
  return line.slice(match[1].length);
2504
2677
  }
@@ -3065,7 +3238,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3065
3238
  function expandCandidates(candidate) {
3066
3239
  const candidates = [candidate];
3067
3240
  if (!extname2(candidate)) {
3068
- candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json"].map((extension) => candidate + extension));
3241
+ candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json", ".d.ts"].map((extension) => candidate + extension));
3069
3242
  candidates.push(...["index.tsx", "index.jsx", "index.ts", "index.js"].map((name) => resolve2(candidate, name)));
3070
3243
  } else if (extname2(candidate) === ".js") {
3071
3244
  candidates.push(candidate.slice(0, -3) + ".ts", candidate.slice(0, -3) + ".tsx");
@@ -3130,7 +3303,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3130
3303
  }
3131
3304
  const text2 = content.toString("utf8");
3132
3305
  const syntax = ts2.transpileModule(text2, {
3133
- fileName: path,
3306
+ fileName: path.replace(/\.d\.ts$/, ".ts"),
3134
3307
  reportDiagnostics: true,
3135
3308
  compilerOptions: { jsx: ts2.JsxEmit.Preserve, target: ts2.ScriptTarget.ESNext, module: ts2.ModuleKind.ESNext }
3136
3309
  });
@@ -3976,13 +4149,92 @@ function projectAuthoredStyles(css, contents) {
3976
4149
  }
3977
4150
 
3978
4151
  // src/docusaurus.ts
3979
- import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
3980
- import { extname as extname4, posix as posix3 } from "path";
3981
- import JSON5 from "json5";
4152
+ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
4153
+ import { extname as extname5, posix as posix3 } from "path";
4154
+ import postcss3 from "postcss";
4155
+
4156
+ // src/css-colors.ts
4157
+ var channel = (value) => Number(value);
4158
+ function cssColorToHex(raw) {
4159
+ const value = raw.trim().replace(/\s*!important$/i, "");
4160
+ if (value.length > 64) return void 0;
4161
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value)?.[1];
4162
+ if (hex) return `#${(hex.length === 3 ? [...hex].map((digit) => digit + digit).join("") : hex).toLowerCase()}`;
4163
+ const rgb = /^(?:rgba?\(\s*)?(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})\s*(?:[,/]\s*[\d.]+%?\s*)?\)?$/i.exec(value);
4164
+ if (!rgb) return void 0;
4165
+ const channels = [rgb[1], rgb[2], rgb[3]].map(channel);
4166
+ if (channels.some((entry) => entry > 255)) return void 0;
4167
+ if (value.includes("(") !== value.includes(")")) return void 0;
4168
+ return `#${channels.map((entry) => entry.toString(16).padStart(2, "0")).join("")}`;
4169
+ }
4170
+ function stripComments(css) {
4171
+ let out = "";
4172
+ let index = 0;
4173
+ while (index < css.length) {
4174
+ const open = css.indexOf("/*", index);
4175
+ if (open < 0) break;
4176
+ out += css.slice(index, open);
4177
+ const close = css.indexOf("*/", open + 2);
4178
+ if (close < 0) return out;
4179
+ index = close + 2;
4180
+ }
4181
+ return out + css.slice(index);
4182
+ }
4183
+ function declarations(body, into) {
4184
+ for (const declaration of body.split(";")) {
4185
+ const colon = declaration.indexOf(":");
4186
+ if (colon < 0) continue;
4187
+ const name = declaration.slice(0, colon).trim().toLowerCase();
4188
+ if (name !== "--primary" && name !== "--primary-light" && name !== "--primary-dark") continue;
4189
+ const hex = cssColorToHex(declaration.slice(colon + 1));
4190
+ if (hex) into[name.slice(2)] = hex;
4191
+ }
4192
+ }
4193
+ function cssBrandVars(css) {
4194
+ const source = stripComments(css);
4195
+ const root = {};
4196
+ const dark = {};
4197
+ let index = 0;
4198
+ while (index < source.length) {
4199
+ const open = source.indexOf("{", index);
4200
+ if (open < 0) break;
4201
+ let prelude = (source.slice(index, open).split("}").pop() ?? "").trim();
4202
+ while (prelude.startsWith("@") && prelude.includes(";")) prelude = prelude.slice(prelude.indexOf(";") + 1).trim();
4203
+ let depth = 1;
4204
+ let close = open + 1;
4205
+ for (; close < source.length && depth > 0; close++) {
4206
+ if (source[close] === "{") depth++;
4207
+ else if (source[close] === "}") depth--;
4208
+ }
4209
+ index = close;
4210
+ if (prelude.startsWith("@")) continue;
4211
+ for (const selector of prelude.split(",")) {
4212
+ const name = selector.trim().toLowerCase();
4213
+ const target = name === ":root" || name === "html" ? root : name === ".dark" || name === "html.dark" || name === ":root.dark" ? dark : void 0;
4214
+ if (target) declarations(source.slice(open + 1, close - 1), target);
4215
+ }
4216
+ }
4217
+ return { root, dark };
4218
+ }
4219
+ function brandColorsFromVars({ root, dark }) {
4220
+ const result = {};
4221
+ const lightAccent = root.primary;
4222
+ const lightButton = root["primary-dark"] ?? root.primary;
4223
+ if (lightAccent || lightButton) result.light = { ...lightAccent ? { accent: lightAccent } : {}, ...lightButton ? { primary: lightButton } : {} };
4224
+ const darkAccent = dark["primary-light"] ?? dark.primary;
4225
+ const darkButton = dark["primary-dark"] ?? dark["primary-light"] ?? dark.primary;
4226
+ if (darkAccent || darkButton) result.dark = { ...darkAccent ? { accent: darkAccent } : {}, ...darkButton ? { primary: darkButton } : {} };
4227
+ return result.light || result.dark ? result : void 0;
4228
+ }
4229
+
4230
+ // src/fern.ts
4231
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync } from "fs";
4232
+ import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
4233
+ import { load } from "cheerio";
3982
4234
  import { parse as parseYaml2 } from "yaml";
3983
4235
 
3984
4236
  // src/navigation.ts
3985
- import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
4237
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
3986
4238
  import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
3987
4239
  var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
3988
4240
  function addMintlifyDirectoryRedirects(config, pages) {
@@ -4136,8 +4388,8 @@ function readMintlifyJson(path, repositoryRoot) {
4136
4388
  if (!existsSync2(path) || !lstatSync2(path).isFile()) {
4137
4389
  throw new Error(`Mintlify config reference is not a regular file: ${relative3(repositoryRoot, path)}`);
4138
4390
  }
4139
- const realPath = realpathSync(path);
4140
- const realRoot = realpathSync(repositoryRoot);
4391
+ const realPath = realpathSync2(path);
4392
+ const realRoot = realpathSync2(repositoryRoot);
4141
4393
  resolveWithin(realRoot, relative3(realRoot, realPath));
4142
4394
  if (lstatSync2(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
4143
4395
  throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative3(repositoryRoot, path)}`);
@@ -5221,1433 +5473,1695 @@ function pruneMissingNavigationPages(config, availableIds) {
5221
5473
  return { ...config, tabs };
5222
5474
  }
5223
5475
 
5224
- // src/docusaurus.ts
5476
+ // src/fern.ts
5225
5477
  var MAX_CONFIG_BYTES = 2e7;
5226
- var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
5227
- var SIDEBAR_FILENAMES = [
5228
- "sidebars.json",
5229
- "sidebars.js",
5230
- "sidebars.cjs",
5231
- "sidebars.mjs",
5232
- "sidebars.ts"
5233
- ];
5478
+ function announcementMarkdown(message) {
5479
+ const document = load(message);
5480
+ document("script, style, iframe, object, embed, svg, math").remove();
5481
+ const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([\\`*_\[\]{}()!])/g, "\\$1");
5482
+ const links = /* @__PURE__ */ new Map();
5483
+ document("a").each((index, element) => {
5484
+ const marker = `THALLYANNOUNCEMENTLINK${index}TOKEN`;
5485
+ const label = document(element).text().trim();
5486
+ const rawHref = document(element).attr("href")?.trim() ?? "";
5487
+ let href;
5488
+ if (/^https?:\/\//i.test(rawHref)) {
5489
+ try {
5490
+ href = new URL(rawHref).toString();
5491
+ } catch {
5492
+ }
5493
+ } else if (rawHref.startsWith("/") && !rawHref.startsWith("//") && !/[\\<>\s]/.test(rawHref)) {
5494
+ href = rawHref;
5495
+ }
5496
+ const destination = href?.replace(/\(/g, "%28").replace(/\)/g, "%29");
5497
+ links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
5498
+ document(element).replaceWith(marker);
5499
+ });
5500
+ let text2 = escapeText(document("body").text());
5501
+ for (const [marker, link] of links) text2 = text2.replaceAll(marker, link);
5502
+ return text2.trim();
5503
+ }
5234
5504
  function objectValue3(value) {
5235
5505
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
5236
5506
  }
5237
- function stripNumberPrefix(value) {
5238
- return value.replace(/^\d+[-_]+/, "");
5239
- }
5240
- function normalizeDocId(value) {
5241
- return value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").split("/").filter(Boolean).map(stripNumberPrefix).join("/");
5507
+ function titleCase2(value) {
5508
+ return value.split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5242
5509
  }
5243
- function docusaurusSlugifySegment(value) {
5244
- let decoded = value;
5245
- try {
5246
- decoded = decodeURIComponent(value);
5247
- } catch {
5510
+ function readBoundedYaml(path) {
5511
+ if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
5512
+ throw new Error("The Fern config is larger than 20 MB and could not be imported.");
5248
5513
  }
5249
- return decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
5514
+ return parseYaml2(readFileSync3(path, "utf8"));
5250
5515
  }
5251
- function docusaurusRouteId(value) {
5252
- const segments = normalizeDocId(value).split("/").map(docusaurusSlugifySegment).filter(Boolean);
5253
- return segments.join("/") || "introduction";
5516
+ function readFernConfig(fernRoot) {
5517
+ const sourcePath = "docs.yml";
5518
+ const path = resolveWithin(fernRoot, sourcePath);
5519
+ if (!existsSync3(path) || !lstatSync3(path).isFile()) return null;
5520
+ const config = objectValue3(readBoundedYaml(path));
5521
+ if (!config) throw new Error("fern/docs.yml must contain an object.");
5522
+ return { config, sourcePath };
5254
5523
  }
5255
- function defaultDocusaurusRouteId(value) {
5256
- const segments = normalizeDocId(value).split("/").filter(Boolean);
5257
- if (/^(?:index|readme)$/i.test(segments.at(-1) ?? "")) segments.pop();
5258
- const normalized = segments.map(docusaurusSlugifySegment).filter(Boolean);
5259
- return normalized.join("/") || "introduction";
5524
+ function warnOnce2(context, key, message) {
5525
+ if (context.warningKeys.has(key)) return;
5526
+ context.warningKeys.add(key);
5527
+ context.warnings.push({ code: "unsupported-config", message });
5260
5528
  }
5261
- function storageIdForRoute(routeId) {
5262
- return /(?:^|\/)(?:index|readme)$/i.test(routeId) ? `${routeId}/index` : routeId;
5529
+ function firstLeafId(nodes) {
5530
+ for (const node of nodes) {
5531
+ if (typeof node === "string") return node;
5532
+ const nested = firstLeafId(node.pages);
5533
+ if (nested) return nested;
5534
+ }
5535
+ return void 0;
5263
5536
  }
5264
- function titleCase2(value) {
5265
- return stripNumberPrefix(value).replace(/sidebar$/i, "").split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5537
+ function addBareRouteRedirect(segments, pages, context) {
5538
+ if (segments.length === 0) return;
5539
+ const destination = firstLeafId(pages);
5540
+ const source = segments.join("/");
5541
+ if (destination && destination !== source) {
5542
+ context.bareRouteRedirects.push({ source: `/${source}`, destination: `/${destination}` });
5543
+ }
5266
5544
  }
5267
- function routeFromSlug(slug2, fallback, sourceDirectory) {
5268
- if (slug2 === "/") return "introduction";
5269
- if (slug2.startsWith("/")) return docusaurusRouteId(slug2);
5270
- const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix3.dirname(fallback));
5271
- const resolved = posix3.normalize(posix3.join(fallbackDirectory, slug2));
5272
- if (resolved === ".." || resolved.startsWith("../")) return null;
5273
- return docusaurusRouteId(resolved);
5545
+ var FERN_SEGMENT_ALIAS = /^[\w.-]+$/;
5546
+ function fernBasicSlug(value) {
5547
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
5274
5548
  }
5275
- function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
5276
- const sourceDocId = normalizeDocId(sourcePath);
5277
- const sourceDirectory = posix3.dirname(sourceDocId);
5278
- const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
5279
- const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix3.join(sourceDirectory, configuredId)) : sourceDocId;
5280
- const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
5281
- const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
5282
- const navigationId = configuredSlug ? routeFromSlug(
5283
- configuredSlug,
5284
- defaultNavigationId,
5285
- sourceDirectory === "." ? "" : sourceDirectory
5286
- ) ?? defaultNavigationId : defaultNavigationId;
5287
- const position = typeof frontmatter.sidebar_position === "number" && Number.isFinite(frontmatter.sidebar_position) ? frontmatter.sidebar_position : void 0;
5288
- return {
5289
- identity: { id: storageIdForRoute(navigationId), navigationId },
5290
- descriptor: {
5291
- sourcePath: sourcePath.replace(/\\/g, "/"),
5292
- docId,
5293
- navigationId,
5294
- ...position === void 0 ? {} : { sidebarPosition: position }
5295
- }
5296
- };
5297
- }
5298
- function sourceReferenceKey(value) {
5299
- return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
5549
+ function fernLabelSlug(label) {
5550
+ const decamelized = label.replace(/[()]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2");
5551
+ return fernBasicSlug(decamelized);
5300
5552
  }
5301
- function sourceHeadings(body) {
5302
- const headings = [];
5303
- const lines = body.split("\n");
5304
- let fence = null;
5305
- for (let index = 0; index < lines.length; index++) {
5306
- const line = lines[index];
5307
- const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
5308
- if (marker) {
5309
- if (!fence) fence = marker[1];
5310
- else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = null;
5311
- continue;
5312
- }
5313
- if (fence) continue;
5314
- const heading = line.match(/^ {0,3}(#{2,6})\s+(.+?)\s*#*\s*$/);
5315
- if (!heading) continue;
5316
- const preceding = lines[index - 1]?.match(/^<a id="([\p{L}\p{N}_.:$-]{1,120})"><\/a>$/u);
5317
- const plainText = !/[<>{}\[\]`*_]/.test(heading[2]);
5318
- const generatedId = plainText ? heading[2].normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "") : null;
5319
- headings.push({ index, level: heading[1].length, id: preceding?.[1] ?? generatedId });
5553
+ function segmentFor(value, label, context) {
5554
+ if (value["skip-slug"] === true) return null;
5555
+ if (typeof value.slug !== "string") return fernLabelSlug(label) || null;
5556
+ const explicit = value.slug.trim();
5557
+ if (!explicit) return null;
5558
+ const slugified = fernBasicSlug(explicit);
5559
+ if (context && explicit !== slugified && FERN_SEGMENT_ALIAS.test(explicit)) {
5560
+ context.segmentAliases.set(slugified, explicit);
5320
5561
  }
5321
- return headings;
5562
+ return slugified || null;
5322
5563
  }
5323
- function addDocusaurusTranslatedHeadingAliases(pages) {
5324
- const byRoute = new Map(pages.map((page) => [page.navigationId, page]));
5325
- for (const page of pages) {
5326
- const locale = page.source.match(/(?:^|[/#])i18n\/([A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*)\/docusaurus-plugin-content-docs\//)?.[1];
5327
- if (!locale) continue;
5328
- const originalRoute = page.navigationId === locale ? "introduction" : page.navigationId.startsWith(`${locale}/`) ? page.navigationId.slice(locale.length + 1) : null;
5329
- if (!originalRoute) continue;
5330
- const original = byRoute.get(originalRoute);
5331
- if (!original) continue;
5332
- const originalHeadings = sourceHeadings(original.body);
5333
- const translatedHeadings = sourceHeadings(page.body);
5334
- if (!originalHeadings.length || originalHeadings.length !== translatedHeadings.length || originalHeadings.some((heading, index) => heading.level !== translatedHeadings[index].level)) continue;
5335
- const occupied = new Set(translatedHeadings.map((heading) => heading.id).filter((id) => !!id));
5336
- for (const match of page.body.matchAll(/<a\s+id="([^"]+)"/g)) occupied.add(match[1]);
5337
- const originalIdCounts = /* @__PURE__ */ new Map();
5338
- for (const heading of originalHeadings) {
5339
- if (heading.id) originalIdCounts.set(heading.id, (originalIdCounts.get(heading.id) ?? 0) + 1);
5340
- }
5341
- const aliases = /* @__PURE__ */ new Map();
5342
- for (let index = 0; index < originalHeadings.length; index++) {
5343
- const id = originalHeadings[index].id;
5344
- if (!id || occupied.has(id) || originalIdCounts.get(id) !== 1) continue;
5345
- aliases.set(translatedHeadings[index].index, id);
5346
- occupied.add(id);
5564
+ function uniqueNavigationId(base, context) {
5565
+ if (!context.seenNavigationIds.has(base)) return base;
5566
+ let index = 2;
5567
+ while (context.seenNavigationIds.has(`${base}-${index}`)) index++;
5568
+ return `${base}-${index}`;
5569
+ }
5570
+ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
5571
+ let sourcePath;
5572
+ try {
5573
+ const absolute = withinRealRoot(resolveWithinRoot(
5574
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
5575
+ rawPath.trim(),
5576
+ context.repositoryRoot
5577
+ ), context.repositoryRoot);
5578
+ sourcePath = relative4(context.fernRoot, absolute).replace(/\\/g, "/");
5579
+ } catch {
5580
+ warnOnce2(context, `fern-unsafe-page-${rawPath}`, `Skipped the Fern page "${rawPath}" because it points outside the repository (symlink or ..).`);
5581
+ return "";
5582
+ }
5583
+ const navigationId = uniqueNavigationId(base || "introduction", context);
5584
+ context.seenNavigationIds.add(navigationId);
5585
+ const isHidden = hidden || context.hiddenInherited === true;
5586
+ context.descriptors.push({ sourcePath, navigationId, ...navTitle ? { navTitle } : {}, ...isHidden ? { hidden: true } : {} });
5587
+ const kept = context.sourcePathToNavigationId.get(sourcePath);
5588
+ if (kept) {
5589
+ if (!isHidden) {
5590
+ const owner = context.descriptors.find((descriptor) => descriptor.navigationId === kept);
5591
+ if (owner) owner.hidden = false;
5347
5592
  }
5348
- if (aliases.size) page.body = page.body.split("\n").flatMap((line, index) => {
5349
- const alias = aliases.get(index);
5350
- return alias ? [`<a id="${alias}"></a>`, line] : [line];
5351
- }).join("\n");
5593
+ return kept;
5352
5594
  }
5595
+ context.sourcePathToNavigationId.set(sourcePath, navigationId);
5596
+ return navigationId;
5353
5597
  }
5354
- function expandDocusaurusDocCardLists(pages, config, sourceRoutes) {
5355
- const pageByRoute = new Map(pages.map((page) => [page.navigationId, page]));
5356
- const containsRoute = (items, route) => items.some((item) => typeof item === "string" ? item === route : containsRoute(item.pages, route));
5357
- const findGroup = (items, route) => {
5358
- for (const item of items) {
5359
- if (typeof item === "string" || !containsRoute(item.pages, route)) continue;
5360
- return findGroup(item.pages, route) ?? item;
5598
+ function registerFolder(object, parentSegments, context) {
5599
+ const rawPath = String(object.folder).trim();
5600
+ let folder;
5601
+ try {
5602
+ folder = withinRealRoot(resolveWithinRoot(
5603
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
5604
+ rawPath,
5605
+ context.repositoryRoot
5606
+ ), context.repositoryRoot);
5607
+ } catch {
5608
+ warnOnce2(context, `fern-unsafe-folder-${rawPath}`, `Skipped the Fern folder "${rawPath}" because it points outside the repository (symlink or ..). None of its pages were imported.`);
5609
+ return null;
5610
+ }
5611
+ if (!existsSync3(folder) || !lstatSync3(folder).isDirectory()) {
5612
+ warnOnce2(context, `fern-missing-folder-${rawPath}`, `Fern folder "${rawPath}" does not exist and was skipped.`);
5613
+ return null;
5614
+ }
5615
+ const label = typeof object.title === "string" && object.title.trim() ? object.title.trim() : titleCase2(basename2(folder));
5616
+ const segment = typeof object.slug === "string" || object["skip-slug"] === true ? segmentFor(object, label, context) : fernBasicSlug(basename2(folder));
5617
+ const segments = segment ? [...parentSegments, segment] : parentSegments;
5618
+ const visit = (directory, routeSegments, depth) => {
5619
+ if (depth > 12 || context.descriptors.length >= 5e3) {
5620
+ context.navigationWalk.incomplete = true;
5621
+ warnOnce2(context, "fern-folder-limit", "Fern folder navigation exceeded its depth or page budget; remaining access rules could not be classified.");
5622
+ return [];
5361
5623
  }
5362
- return void 0;
5363
- };
5364
- const firstRoute = (items) => {
5365
- for (const item of items) {
5366
- if (typeof item === "string") return item;
5367
- const candidate = firstRoute(item.pages);
5368
- if (candidate) return candidate;
5624
+ const pages2 = [];
5625
+ const entries = readdirSync(directory, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && !entry.isSymbolicLink()).sort((left, right) => Number(left.name !== "index.mdx" && left.name !== "index.md") - Number(right.name !== "index.mdx" && right.name !== "index.md") || left.name.localeCompare(right.name));
5626
+ for (const entry of entries) {
5627
+ const path = resolveWithin(directory, entry.name);
5628
+ if (entry.isDirectory()) {
5629
+ const nested = visit(path, [...routeSegments, fernBasicSlug(entry.name)], depth + 1);
5630
+ if (nested.length) pages2.push({ group: titleCase2(entry.name), pages: nested });
5631
+ } else if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) {
5632
+ const stem = basename2(entry.name, extname4(entry.name));
5633
+ const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
5634
+ const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
5635
+ const source = lstatSync3(path).size <= MAX_CONFIG_BYTES ? readFileSync3(path, "utf8") : "";
5636
+ const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
5637
+ const hidden = /^hidden:\s*true\s*$/m.test(frontmatter);
5638
+ const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" }, void 0, hidden);
5639
+ if (registered && !hidden && !context.hiddenInherited) pages2.push(registered);
5640
+ }
5369
5641
  }
5370
- return void 0;
5642
+ return pages2;
5371
5643
  };
5372
- for (const page of pages) {
5373
- if (!sourceRoutes.has(page.navigationId)) continue;
5374
- const group = config.tabs.map((tab) => findGroup([
5375
- ...tab.pages ?? [],
5376
- ...tab.groups ?? []
5377
- ], page.navigationId)).find(Boolean);
5378
- if (!group) {
5379
- page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, "");
5380
- continue;
5644
+ const pages = visit(folder, segments, 0);
5645
+ if (!pages.length) return null;
5646
+ addBareRouteRedirect(segments, pages, context);
5647
+ return { group: label, pages };
5648
+ }
5649
+ function registerPage(object, parentSegments, context) {
5650
+ if (typeof object.path !== "string" || !object.path.trim()) return null;
5651
+ const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
5652
+ const segment = segmentFor(object, label, context);
5653
+ const base = [...parentSegments, segment].filter(Boolean).join("/");
5654
+ return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
5655
+ }
5656
+ function isRestrictedNode(object) {
5657
+ const viewers = object.viewers;
5658
+ if (Array.isArray(viewers) ? viewers.length > 0 : typeof viewers === "string" ? viewers.trim() !== "" : viewers !== void 0 && viewers !== null && viewers !== false) return true;
5659
+ const authed = object.authed;
5660
+ return authed !== void 0 && authed !== null && authed !== false && !(typeof authed === "string" && ["", "false"].includes(authed.trim().toLowerCase()));
5661
+ }
5662
+ function restrictionMark(context) {
5663
+ return { descriptors: context.descriptors.length, apis: context.apiSections.length };
5664
+ }
5665
+ function markRestricted(context, mark) {
5666
+ for (const descriptor of context.descriptors.slice(mark.descriptors)) descriptor.restricted = true;
5667
+ context.restrictedApiSections.push(...context.apiSections.splice(mark.apis));
5668
+ }
5669
+ function convertNode(node, parentSegments, context) {
5670
+ const object = objectValue3(node);
5671
+ if (!object) return null;
5672
+ const walk2 = context.navigationWalk;
5673
+ const reason = walk2.active.has(object) ? "cyclic section" : walk2.active.size >= 64 ? "section depth exceeds 64" : walk2.visits >= 5e3 ? "node budget exceeds 5000" : void 0;
5674
+ if (reason) {
5675
+ walk2.incomplete = true;
5676
+ warnOnce2(context, `navigation-${reason}`, `Fern navigation was skipped: ${reason}.`);
5677
+ return null;
5678
+ }
5679
+ walk2.visits++;
5680
+ walk2.active.add(object);
5681
+ try {
5682
+ if (isRestrictedNode(object)) {
5683
+ const before = restrictionMark(context);
5684
+ convertNodeContents({ ...object, viewers: void 0, authed: void 0 }, parentSegments, context);
5685
+ markRestricted(context, before);
5686
+ return null;
5381
5687
  }
5382
- const cards = group.pages.flatMap((item) => {
5383
- if (typeof item === "string") {
5384
- if (item === page.navigationId) return [];
5385
- const target = pageByRoute.get(item);
5386
- return [{ title: target?.title ?? item.split("/").at(-1) ?? item, route: item }];
5387
- }
5388
- const slug2 = slugifySegment(item.group);
5389
- const routes = [...item.pages].flatMap((child) => typeof child === "string" ? [child] : []);
5390
- const route = routes.find((candidate) => candidate === slug2 || candidate.endsWith(`/${slug2}`)) ?? firstRoute(item.pages);
5391
- return route ? [{ title: item.group, route }] : [];
5392
- });
5393
- const markup = cards.length ? `<CardGroup>
5394
- ${cards.map(({ title, route }) => ` <Card title={${JSON.stringify(title)}} href={${JSON.stringify(`/${route === "introduction" ? "" : route}`)}} />`).join("\n")}
5395
- </CardGroup>` : "";
5396
- page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, markup);
5397
- if (markup && !page.body.includes(markup)) page.body = [page.body.trim(), markup].filter(Boolean).join("\n\n");
5688
+ return convertNodeContents(object, parentSegments, context);
5689
+ } finally {
5690
+ walk2.active.delete(object);
5398
5691
  }
5399
5692
  }
5400
- function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
5401
- const routes = /* @__PURE__ */ new Map();
5402
- for (const descriptor of descriptors) {
5403
- const sourceKey = sourceReferenceKey(descriptor.sourcePath);
5404
- routes.set(sourceKey, descriptor.navigationId);
5405
- routes.set(sourceReferenceKey(descriptor.docId), descriptor.navigationId);
5406
- routes.set(sourceReferenceKey(descriptor.navigationId), descriptor.navigationId);
5407
- const sourceRoute = pageIdFromReference(sourceKey);
5408
- if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
5693
+ function convertNodeContents(object, parentSegments, context) {
5694
+ if (typeof object.page === "string") {
5695
+ const page = registerPage(object, parentSegments, context);
5696
+ return object.hidden === true || context.hiddenInherited ? null : page;
5409
5697
  }
5410
- const currentDirectory = posix3.dirname(sourceReferenceKey(current.sourcePath));
5411
- function copiedAssetTarget(target) {
5412
- if (!target.startsWith("/") || target.startsWith("//") || target.includes("\\")) return null;
5413
- const suffixIndex = target.search(/[?#]/);
5414
- const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
5415
- const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
5416
- if (path.startsWith("/docs/") && options.assetPaths?.has(path.slice("/docs/".length))) {
5417
- return `/${path.slice("/docs/".length)}${suffix}`;
5418
- }
5419
- return options.assetPaths?.has(path.slice(1)) ? target : null;
5698
+ if (typeof object.folder === "string") {
5699
+ const folder = registerFolder(object, parentSegments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
5700
+ return object.hidden === true || context.hiddenInherited ? null : folder;
5420
5701
  }
5421
- function rewriteTarget(target) {
5422
- if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
5423
- if (target.startsWith("#")) return target;
5424
- const suffixIndex = target.search(/[?#]/);
5425
- const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
5426
- const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
5427
- const copiedAsset = copiedAssetTarget(target);
5428
- if (copiedAsset) return copiedAsset;
5429
- const candidates = path.startsWith("/") ? [
5430
- sourceReferenceKey(path),
5431
- sourceReferenceKey(path).replace(/^docs\//, ""),
5432
- sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
5433
- ] : [
5434
- sourceReferenceKey(posix3.normalize(posix3.join(currentDirectory, path))),
5435
- sourceReferenceKey(path)
5436
- ];
5437
- const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
5438
- if (!route) {
5439
- if (path.startsWith("/") && path !== "/" && options.sourceOrigin) {
5440
- options.onExternalLink?.(target);
5441
- return new URL(target, options.sourceOrigin).toString();
5442
- }
5443
- return target;
5702
+ if (typeof object.section === "string") {
5703
+ const label = object.section;
5704
+ const segment = segmentFor(object, label, context);
5705
+ const segments = segment ? [...parentSegments, segment] : parentSegments;
5706
+ const contents = Array.isArray(object.contents) ? object.contents : [];
5707
+ const pages = [];
5708
+ const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
5709
+ if (hasOwnPath) {
5710
+ const registered = registerPageAt(object.path, segments.join("/"), context, label, object.hidden === true);
5711
+ if (registered) pages.push(registered);
5444
5712
  }
5445
- return `${route === "introduction" ? "/" : `/${route}`}${suffix}`;
5446
- }
5447
- let codeFence = null;
5448
- return body.split("\n").map((line) => {
5449
- const fence = line.match(/^\s{0,3}(`{3,}|~{3,})/);
5450
- if (fence) {
5451
- if (!codeFence) codeFence = fence[1];
5452
- else if (fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length && /^\s*$/.test(line.slice(fence[0].length))) codeFence = null;
5453
- return line;
5454
- }
5455
- if (codeFence) return line;
5456
- return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/(\bsrc=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${copiedAssetTarget(target) ?? target}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
5457
- const bracketed = destination.startsWith("<") && destination.endsWith(">");
5458
- const target = bracketed ? destination.slice(1, -1) : destination;
5459
- const rewritten = rewriteTarget(target);
5460
- return `${prefix}${bracketed ? `<${rewritten}>` : rewritten}`;
5461
- });
5462
- }).join("\n");
5463
- }
5464
- function readDocusaurusSiteOrigin(repositoryRoot) {
5465
- const configPath = findDocusaurusConfigPath(repositoryRoot);
5466
- if (!configPath) return void 0;
5467
- const address = readBoundedText(configPath).match(/\burl['"]?\s*:\s*(['"])(https?:\/\/[^'"]+)\1/)?.[2];
5468
- if (!address) return void 0;
5469
- try {
5470
- return new URL(address).origin;
5471
- } catch {
5472
- return void 0;
5473
- }
5474
- }
5475
- function readBoundedText(path) {
5476
- if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
5477
- throw new Error("The Docusaurus sidebar config is larger than 20 MB and could not be imported.");
5478
- }
5479
- return readFileSync3(path, "utf8");
5480
- }
5481
- function matchingObjectLiteral(source, start) {
5482
- let depth = 0;
5483
- let quote = null;
5484
- let isEscaped = false;
5485
- let lineComment = false;
5486
- let blockComment = false;
5487
- let objectStart = -1;
5488
- for (let index = start; index < source.length; index++) {
5489
- const char = source[index];
5490
- const next = source[index + 1];
5491
- if (lineComment) {
5492
- if (char === "\n") lineComment = false;
5493
- continue;
5494
- }
5495
- if (blockComment) {
5496
- if (char === "*" && next === "/") {
5497
- blockComment = false;
5498
- index++;
5499
- }
5500
- continue;
5501
- }
5502
- if (quote) {
5503
- if (isEscaped) isEscaped = false;
5504
- else if (char === "\\") isEscaped = true;
5505
- else if (char === quote) quote = null;
5506
- continue;
5507
- }
5508
- if (char === "/" && next === "/") {
5509
- lineComment = true;
5510
- index++;
5511
- continue;
5512
- }
5513
- if (char === "/" && next === "*") {
5514
- blockComment = true;
5515
- index++;
5516
- continue;
5517
- }
5518
- if (char === '"' || char === "'" || char === "`") {
5519
- quote = char;
5520
- continue;
5521
- }
5522
- if (char === "{") {
5523
- if (objectStart < 0) objectStart = index;
5524
- depth++;
5525
- } else if (char === "}" && objectStart >= 0) {
5526
- depth--;
5527
- if (depth === 0) return source.slice(objectStart, index + 1);
5528
- } else if (objectStart < 0 && !/\s/.test(char)) {
5529
- return null;
5713
+ for (const child of contents) {
5714
+ const converted = convertNode(child, segments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
5715
+ if (converted) pages.push(converted);
5530
5716
  }
5717
+ if (pages.length === 0) return null;
5718
+ if (!hasOwnPath && segment) addBareRouteRedirect(segments, pages, context);
5719
+ if (object.hidden === true || context.hiddenInherited) return null;
5720
+ return {
5721
+ group: label,
5722
+ ...["open-by-default", "closed-by-default", true, false].includes(object.collapsed) ? { defaultOpen: object.collapsed === "open-by-default" || object.collapsed === false } : {},
5723
+ ...typeof object.icon === "string" ? { icon: object.icon } : {},
5724
+ ...object.hidden === true ? { hidden: true } : {},
5725
+ pages
5726
+ };
5531
5727
  }
5532
- return null;
5533
- }
5534
- function parseStaticSidebarModule(source) {
5535
- const bindings = /* @__PURE__ */ new Map();
5536
- const normalizedSource = replaceExternalFbContent(source);
5537
- const parseFailures = [];
5538
- const assignmentPatterns = [
5539
- /\bmodule\.exports\s*=\s*/g,
5540
- /\bexport\s+default\s*/g,
5541
- /\b(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:\s*:\s*[^=;]+)?\s*=\s*/g
5542
- ];
5543
- const candidates = assignmentPatterns.flatMap((pattern) => [...normalizedSource.matchAll(pattern)]).sort((left, right) => (left.index ?? 0) - (right.index ?? 0));
5544
- for (const candidate of candidates) {
5545
- const literal = matchingObjectLiteral(normalizedSource, (candidate.index ?? 0) + candidate[0].length);
5546
- if (!literal) continue;
5547
- try {
5548
- const substituted = [...bindings].reduce(
5549
- (value, [name, binding]) => value.replace(
5550
- new RegExp(`(:\\s*)${name}\\b`, "g"),
5551
- (_match, prefix) => `${prefix}${JSON.stringify(binding)}`
5552
- ),
5553
- literal
5554
- );
5555
- const parsed = JSON5.parse(substituted);
5556
- const object = objectValue3(parsed);
5557
- if (!object) continue;
5558
- const bindingName = candidate[0].match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/)?.[1];
5559
- if (bindingName) {
5560
- bindings.set(bindingName, object);
5561
- continue;
5728
+ if (typeof object.link === "string") {
5729
+ const label = object.link.trim();
5730
+ const rawHref = typeof object.href === "string" ? object.href.trim() : "";
5731
+ let href;
5732
+ if (/^https?:\/\//i.test(rawHref) && !/[\\\u0000-\u0020\u007f]/.test(rawHref)) {
5733
+ try {
5734
+ const url = new URL(rawHref);
5735
+ if (["http:", "https:"].includes(url.protocol) && !url.username && !url.password) href = url.toString();
5736
+ } catch {
5562
5737
  }
5563
- return object;
5564
- } catch (error) {
5565
- const message = error instanceof Error ? error.message : String(error);
5566
- const line = Number(message.match(/at (\d+):/)?.[1] ?? 0);
5567
- const context = line > 0 ? literal.split("\n").slice(Math.max(0, line - 2), line + 1).join(" ") : "";
5568
- parseFailures.push(`${message}${context ? ` near ${context}` : ""}`);
5569
- }
5570
- }
5571
- const exportedBinding = normalizedSource.match(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\b/)?.[1] ?? normalizedSource.match(/\bmodule\.exports\s*=\s*([A-Za-z_$][\w$]*)\b/)?.[1];
5572
- if (exportedBinding && bindings.has(exportedBinding)) return bindings.get(exportedBinding);
5573
- throw new Error(`Sidebar config is executable or outside the supported data-only syntax.${parseFailures[0] ? ` ${parseFailures[0]}` : ""}`);
5574
- }
5575
- function replaceExternalFbContent(source) {
5576
- const marker = "...fbContent(";
5577
- let result = source;
5578
- let searchFrom = 0;
5579
- while (true) {
5580
- const start = result.indexOf(marker, searchFrom);
5581
- if (start < 0) return result.replace(/,\s*,/g, ",");
5582
- const objectStart = result.indexOf("{", start + marker.length);
5583
- if (objectStart < 0) return result;
5584
- const objectLiteral = matchingObjectLiteral(result, objectStart);
5585
- if (!objectLiteral) {
5586
- searchFrom = start + marker.length;
5587
- continue;
5588
5738
  }
5589
- const objectEnd = objectStart + objectLiteral.length;
5590
- const close = result.indexOf(")", objectEnd);
5591
- if (close < 0) return result;
5592
- const externalMatch = /\bexternal\s*:\s*/g.exec(objectLiteral);
5593
- let replacement = "";
5594
- if (externalMatch) {
5595
- const arrayStart = objectLiteral.indexOf("[", externalMatch.index + externalMatch[0].length);
5596
- if (arrayStart >= 0) {
5597
- const array = matchingArrayLiteral(objectLiteral, arrayStart);
5598
- if (array) replacement = array.slice(1, -1);
5739
+ if (label && href && object.hidden !== true && !context.hiddenInherited) {
5740
+ if (!context.externalLinks.some((link) => link.label === label && link.href === href)) {
5741
+ context.externalLinks.push({ label, href });
5599
5742
  }
5743
+ } else if (!href && object.hidden !== true && !context.hiddenInherited) {
5744
+ warnOnce2(context, `fern-link-${rawHref}`, `Fern navigation link "${label}" has an unsafe or invalid external URL and was skipped.`);
5600
5745
  }
5601
- let replaceEnd = close + 1;
5602
- if (!replacement) {
5603
- const trailingComma = result.slice(replaceEnd).match(/^\s*,/);
5604
- if (trailingComma) replaceEnd += trailingComma[0].length;
5605
- }
5606
- result = `${result.slice(0, start)}${replacement}${result.slice(replaceEnd)}`;
5607
- searchFrom = start + replacement.length;
5746
+ return null;
5608
5747
  }
5609
- }
5610
- function matchingArrayLiteral(source, start) {
5611
- let depth = 0;
5612
- let quote = "";
5613
- let isEscaped = false;
5614
- for (let index = start; index < source.length; index++) {
5615
- const character = source[index];
5616
- if (quote) {
5617
- if (isEscaped) isEscaped = false;
5618
- else if (character === "\\") isEscaped = true;
5619
- else if (character === quote) quote = "";
5620
- continue;
5621
- }
5622
- if (character === '"' || character === "'" || character === "`") {
5623
- quote = character;
5624
- continue;
5625
- }
5626
- if (character === "[") depth++;
5627
- if (character === "]" && --depth === 0) return source.slice(start, index + 1);
5748
+ if (typeof object.api === "string") {
5749
+ const ownSegment = segmentFor(object, object.api, context);
5750
+ context.apiSections.push({
5751
+ name: typeof object["api-name"] === "string" ? object["api-name"] : object.api,
5752
+ nameExplicit: typeof object["api-name"] === "string",
5753
+ routeSegments: ownSegment ? [...parentSegments, ownSegment] : parentSegments
5754
+ });
5755
+ return null;
5628
5756
  }
5629
- return null;
5630
- }
5631
- function configuredSidebarPath(repositoryRoot) {
5632
- for (const filename of ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"]) {
5633
- const path = resolveWithin(repositoryRoot, filename);
5634
- if (!existsSync3(path)) continue;
5635
- const source = readBoundedText(path);
5636
- for (const docs of source.matchAll(/\bdocs\s*:\s*/g)) {
5637
- const object = matchingObjectLiteral(source, (docs.index ?? 0) + docs[0].length);
5638
- const candidate2 = object && staticStringField(object, "sidebarPath")?.replace(/^\.\//, "");
5639
- if (candidate2) {
5640
- const resolved2 = resolveWithin(repositoryRoot, candidate2);
5641
- if (existsSync3(resolved2) && lstatSync3(resolved2).isFile()) return candidate2;
5642
- }
5643
- }
5644
- const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
5645
- if (!match) continue;
5646
- const candidate = match[2].replace(/^\.\//, "");
5647
- const resolved = resolveWithin(repositoryRoot, candidate);
5648
- if (existsSync3(resolved) && lstatSync3(resolved).isFile()) return candidate;
5757
+ if (typeof object.changelog === "string") {
5758
+ return registerFolder({ folder: object.changelog, title: "Changelog", "skip-slug": true }, parentSegments, context);
5649
5759
  }
5650
5760
  return null;
5651
5761
  }
5652
- function readDocusaurusSidebars(repositoryRoot, versionedSidebarPath) {
5653
- const configured = versionedSidebarPath ?? configuredSidebarPath(repositoryRoot);
5654
- const sourcePath = [configured, ...versionedSidebarPath ? [] : SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => {
5655
- const path = resolveWithin(repositoryRoot, candidate);
5656
- return existsSync3(path) && lstatSync3(path).isFile();
5762
+ function convertNodes(nodes, parentSegments, context) {
5763
+ return nodes.flatMap((node) => {
5764
+ const converted = convertNode(node, parentSegments, context);
5765
+ return converted ? [converted] : [];
5657
5766
  });
5658
- if (!sourcePath) return null;
5659
- const absolutePath = resolveWithin(repositoryRoot, sourcePath);
5660
- const source = readBoundedText(absolutePath);
5661
- const parsed = extname4(sourcePath).toLowerCase() === ".json" ? objectValue3(JSON5.parse(source)) : parseStaticSidebarModule(source);
5662
- if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
5663
- return { config: parsed, sourcePath };
5664
5767
  }
5665
- function namedExportObjectLiteralText(source, name) {
5666
- const normalizedSource = replaceExternalFbContent(source);
5667
- const match = new RegExp(`\\bexport\\s+(?:const|let|var)\\s+${name}\\b(?:\\s*:\\s*[^=;]+)?\\s*=\\s*`).exec(normalizedSource);
5668
- if (!match) return null;
5669
- return matchingObjectLiteral(normalizedSource, match.index + match[0].length);
5768
+ function groupsFromConverted(converted) {
5769
+ const loosePages = converted.filter((entry) => typeof entry === "string");
5770
+ const groups = converted.filter((entry) => typeof entry !== "string");
5771
+ return loosePages.length > 0 ? [{ group: "Overview", pages: loosePages }, ...groups] : groups;
5670
5772
  }
5671
- function redirectEntriesFromObjectLiteralText(objectLiteral) {
5672
- const match = /\bredirects\s*:\s*/.exec(objectLiteral);
5673
- if (!match) return [];
5674
- const arrayStart = objectLiteral.indexOf("[", match.index + match[0].length);
5675
- if (arrayStart < 0) return [];
5676
- const arrayLiteral = matchingArrayLiteral(objectLiteral, arrayStart);
5677
- if (!arrayLiteral) return [];
5678
- let parsed;
5679
- try {
5680
- parsed = JSON5.parse(arrayLiteral);
5681
- } catch {
5682
- return [];
5773
+ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
5774
+ if (Array.isArray(config.navigation)) return config;
5775
+ if (Array.isArray(config.products)) {
5776
+ warnOnce2(context, "fern-products", "Nested Fern multi-product docs are not supported; only this docs.yml was imported.");
5777
+ return config;
5683
5778
  }
5684
- if (!Array.isArray(parsed)) return [];
5685
- const entries = [];
5686
- for (const raw of parsed) {
5687
- const entry = objectValue3(raw);
5688
- if (!entry || typeof entry.to !== "string") continue;
5689
- const froms = Array.isArray(entry.from) ? entry.from : [entry.from];
5690
- for (const from of froms) {
5691
- if (typeof from !== "string") continue;
5692
- if (!isRedirectPathSafe(from, entry.to)) continue;
5693
- entries.push({ source: from, destination: entry.to });
5694
- }
5779
+ if (!Array.isArray(config.versions)) return config;
5780
+ const versions = config.versions.flatMap((entry) => {
5781
+ const version = objectValue3(entry);
5782
+ return version ? [version] : [];
5783
+ });
5784
+ const chosen = versions.find((version) => version.default === true) ?? versions[0];
5785
+ if (chosen && isRestrictedNode(chosen)) context.versionRestricted = true;
5786
+ const skipped = versions.filter((version) => version !== chosen).map((version) => typeof version.version === "string" ? version.version : void 0).filter((name) => Boolean(name));
5787
+ if (skipped.length > 0) {
5788
+ warnOnce2(
5789
+ context,
5790
+ "fern-versions",
5791
+ `Only the default Fern version was imported; other versions were skipped: ${skipped.join(", ")}.`
5792
+ );
5695
5793
  }
5696
- return entries;
5697
- }
5698
- function readDocusaurusRedirects(repositoryRoot, warnings) {
5699
- const configPath = findDocusaurusConfigPath(repositoryRoot);
5700
- if (!configPath) return [];
5701
- const configSource = readBoundedText(configPath);
5702
- const pluginMatch = /['"]@docusaurus\/plugin-client-redirects['"]\s*,\s*/.exec(configSource);
5703
- if (!pluginMatch) return [];
5704
- const afterPlugin = configSource.slice(pluginMatch.index + pluginMatch[0].length);
5705
- const warnAboutCreateRedirects = (source) => {
5706
- if (!/\bcreateRedirects\s*[:(]/.test(source)) return;
5707
- warnings.push({
5708
- code: "unsupported-config",
5709
- message: "The redirects plugin's createRedirects function computes redirects dynamically and cannot be evaluated during migration; add its redirects manually."
5710
- });
5711
- };
5712
- warnAboutCreateRedirects(configSource);
5713
- const inlineLiteral = matchingObjectLiteral(configSource, pluginMatch.index + pluginMatch[0].length);
5714
- if (inlineLiteral) return redirectEntriesFromObjectLiteralText(inlineLiteral);
5715
- const identifier = afterPlugin.match(/^([A-Za-z_$][\w$]*)/)?.[1];
5716
- if (!identifier) return [];
5717
- const importMatch = new RegExp(`import\\s*\\{[^}]*\\b${identifier}\\b[^}]*\\}\\s*from\\s*(['"])([^'"]+)\\1`).exec(configSource) ?? new RegExp(`import\\s+${identifier}\\s+from\\s*(['"])([^'"]+)\\1`).exec(configSource);
5718
- const modulePath = importMatch?.[2];
5719
- if (!modulePath || !modulePath.startsWith(".")) return [];
5720
- let resolvedPath;
5794
+ if (!chosen || typeof chosen.path !== "string") return config;
5721
5795
  try {
5722
- resolvedPath = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"].map((extension) => resolveWithin(repositoryRoot, `${modulePath.replace(/^\.\//, "")}${extension}`)).find((candidate) => existsSync3(candidate) && lstatSync3(candidate).isFile());
5723
- } catch {
5724
- return [];
5725
- }
5726
- if (!resolvedPath) return [];
5727
- const moduleSource = readBoundedText(resolvedPath);
5728
- warnAboutCreateRedirects(moduleSource);
5729
- const literal = namedExportObjectLiteralText(moduleSource, identifier);
5730
- return literal ? redirectEntriesFromObjectLiteralText(literal) : [];
5731
- }
5732
- function readCategoryMetadata(contentRoot, directory) {
5733
- for (const filename of CATEGORY_FILENAMES) {
5734
- const path = resolveWithin(contentRoot, posix3.join(directory, filename));
5735
- if (!existsSync3(path) || !lstatSync3(path).isFile()) continue;
5736
- try {
5737
- const raw = readBoundedText(path);
5738
- const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml2(raw);
5739
- const object = objectValue3(parsed);
5740
- if (!object) return {};
5741
- return {
5742
- ...typeof object.label === "string" ? { label: object.label } : {},
5743
- ...typeof object.position === "number" ? { position: object.position } : {},
5744
- ..."link" in object ? { link: object.link } : {}
5745
- };
5746
- } catch {
5747
- return {};
5796
+ const versionPath = resolveWithinRoot(configDir, chosen.path, context.repositoryRoot);
5797
+ if (!existsSync3(versionPath) || !lstatSync3(versionPath).isFile()) {
5798
+ warnOnce2(context, "fern-version-not-file", `Fern version file "${chosen.path}" is not a regular file and was skipped.`);
5799
+ return config;
5748
5800
  }
5801
+ withinRealRoot(versionPath, context.repositoryRoot);
5802
+ const versionConfig = objectValue3(readBoundedYaml(versionPath));
5803
+ if (!versionConfig) return config;
5804
+ context.pathPrefix = relative4(fernRoot, dirname3(versionPath)).replace(/\\/g, "/");
5805
+ return { ...config, navigation: versionConfig.navigation, tabs: versionConfig.tabs ?? config.tabs };
5806
+ } catch (error) {
5807
+ warnOnce2(
5808
+ context,
5809
+ "fern-version-read-failed",
5810
+ isPathEscapeError(error) ? `Skipped the Fern version file "${chosen.path}" because it points outside the repository (symlink or ..).` : `Fern version file "${chosen.path}" could not be read (${error instanceof Error ? error.message : String(error)}) and was skipped.`
5811
+ );
5812
+ return config;
5749
5813
  }
5750
- return {};
5751
- }
5752
- function descriptorSort(left, right) {
5753
- const leftPosition = left.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
5754
- const rightPosition = right.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
5755
- if (leftPosition !== rightPosition) return leftPosition - rightPosition;
5756
- return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
5757
5814
  }
5758
- function registerDoc(docId, context, sourceDirectory) {
5759
- const key = normalizeDocId(docId);
5760
- const scopedKey = sourceDirectory && !docId.startsWith("/") ? normalizeDocId(posix3.join(sourceDirectory, key)) : void 0;
5761
- const descriptor = (scopedKey ? context.descriptorByDocId.get(scopedKey) : void 0) ?? context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
5762
- if (!descriptor) {
5763
- context.warnings.push({
5764
- code: "missing-page",
5765
- message: "A Docusaurus sidebar document did not resolve to an imported page.",
5766
- source: docId
5767
- });
5768
- return null;
5815
+ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallbackTabLabel, context) {
5816
+ const before = restrictionMark(context);
5817
+ let tabs;
5818
+ let restricted;
5819
+ try {
5820
+ tabs = buildUnrestrictedTabs(rawConfig, configDir, fernRoot, routePrefix, fallbackTabLabel, context);
5821
+ restricted = context.versionRestricted === true;
5822
+ } finally {
5823
+ context.versionRestricted = false;
5769
5824
  }
5770
- context.referencedNavigationIds.add(descriptor.navigationId);
5771
- return descriptor.navigationId;
5825
+ if (!restricted) return tabs;
5826
+ markRestricted(context, before);
5827
+ return [];
5772
5828
  }
5773
- function generatedIndexPage(label, linkValue, context, sourceDirectory) {
5774
- const link = objectValue3(linkValue);
5775
- if (!link) return null;
5776
- if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context, sourceDirectory);
5777
- if (link.type !== "generated-index") return null;
5778
- const fallback = `category/${slugifySegment(label)}`;
5779
- const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
5780
- const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix3.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
5781
- if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
5782
- const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
5783
- context.generatedPages.push({
5784
- id: storageIdForRoute(navigationId),
5785
- navigationId,
5786
- title: typeof link.title === "string" ? link.title : label,
5787
- description,
5788
- keywords: Array.isArray(link.keywords) ? link.keywords.filter((value) => typeof value === "string") : [],
5789
- body: description,
5790
- source: `${context.sourceUrl}#${context.sidebarSource}`
5829
+ function buildUnrestrictedTabs(rawConfig, configDir, fernRoot, routePrefix, fallbackTabLabel, context) {
5830
+ const config = resolveVersionedNavigation(rawConfig, configDir, fernRoot, context);
5831
+ const tabsMeta = objectValue3(config.tabs) ?? {};
5832
+ const navigation = config.navigation;
5833
+ const isTabbedNavigation = Array.isArray(navigation) && navigation.length > 0 && navigation.every((entry) => typeof objectValue3(entry)?.tab === "string");
5834
+ if (isTabbedNavigation) {
5835
+ return navigation.flatMap((entry) => {
5836
+ const id = String(entry.tab);
5837
+ const meta = objectValue3(tabsMeta[id]) ?? {};
5838
+ const label = typeof meta["display-name"] === "string" ? meta["display-name"] : titleCase2(id);
5839
+ const tabSegment = segmentFor(meta, label, context);
5840
+ const segments = [...routePrefix, ...tabSegment ? [tabSegment] : []];
5841
+ const layout = Array.isArray(entry.layout) ? entry.layout : [];
5842
+ const sectionsBefore = context.apiSections.length;
5843
+ const descriptorsBefore = restrictionMark(context);
5844
+ const groups = groupsFromConverted(convertNodes(layout, segments, context));
5845
+ if (isRestrictedNode(entry) || isRestrictedNode(meta)) {
5846
+ markRestricted(context, descriptorsBefore);
5847
+ return [];
5848
+ }
5849
+ for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
5850
+ context.apiSections[index].tabLabel ??= label;
5851
+ }
5852
+ if (groups.length === 0 && typeof meta.href === "string" && /^https?:\/\//i.test(meta.href)) {
5853
+ return [{ tab: label, href: meta.href }];
5854
+ }
5855
+ if (groups.length === 0 && typeof meta.changelog === "string") {
5856
+ const changelog = registerFolder({ folder: meta.changelog, title: label, "skip-slug": true }, segments, context);
5857
+ if (changelog) {
5858
+ const indexRoute = segments.join("/");
5859
+ const descriptors = new Map(context.descriptors.map((descriptor) => [descriptor.navigationId, descriptor]));
5860
+ const visiblePages = changelog.pages.filter((page) => typeof page === "string").filter((page) => !descriptors.get(page)?.hidden);
5861
+ const index = visiblePages.find((page) => {
5862
+ const descriptor = descriptors.get(page);
5863
+ if (!descriptor) return false;
5864
+ const stem = basename2(descriptor.sourcePath, extname4(descriptor.sourcePath));
5865
+ return page === indexRoute || stem === "overview" || stem === "index";
5866
+ });
5867
+ const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
5868
+ const date = (page) => descriptors.get(page)?.sourcePath.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
5869
+ return date(right).localeCompare(date(left)) || left.localeCompare(right);
5870
+ });
5871
+ for (const entryId of entries) {
5872
+ const descriptor = descriptors.get(entryId);
5873
+ if (descriptor) descriptor.hidden = true;
5874
+ }
5875
+ changelog.pages = [index ?? indexRoute];
5876
+ context.changelogIndexes.push({ route: indexRoute, entries });
5877
+ const latest = index ?? firstLeafId(changelog.pages);
5878
+ const bareRouteIndex = context.bareRouteRedirects.findIndex((redirect) => redirect.source === `/${indexRoute}`);
5879
+ const indexDescriptor = index ? descriptors.get(index) : void 0;
5880
+ const indexSource = indexDescriptor && lstatSync3(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot)).size <= MAX_CONFIG_BYTES ? readFileSync3(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot), "utf8") : "";
5881
+ const indexFrontmatter = indexSource.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
5882
+ const indexSlug = indexFrontmatter.match(/^slug:\s*["']?([^\s"']+)["']?\s*$/m)?.[1]?.replace(/^\/+|\/+$/g, "");
5883
+ const hasBarePage = index === indexRoute || indexSlug === indexRoute;
5884
+ if (bareRouteIndex >= 0 && hasBarePage) context.bareRouteRedirects.splice(bareRouteIndex, 1);
5885
+ else if (latest && bareRouteIndex >= 0) context.bareRouteRedirects[bareRouteIndex].destination = `/${latest}`;
5886
+ return [{ tab: label, groups: [changelog] }];
5887
+ }
5888
+ }
5889
+ if (groups.length === 0) return [];
5890
+ if (segments.length > 0) addBareRouteRedirect(segments, groups, context);
5891
+ return [{
5892
+ tab: label,
5893
+ ...typeof meta.icon === "string" ? { icon: meta.icon } : {},
5894
+ groups
5895
+ }];
5791
5896
  });
5792
- context.generatedIds.add(navigationId);
5793
5897
  }
5794
- context.referencedNavigationIds.add(navigationId);
5795
- return navigationId;
5796
- }
5797
- var DOCUSAURUS_HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
5798
- function findDocusaurusConfigPath(repositoryRoot) {
5799
- return ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].map((filename) => resolveWithin(repositoryRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync3(candidate).isFile()) ?? null;
5800
- }
5801
- function readDocusaurusThemeColor(repositoryRoot) {
5802
- const configPath = findDocusaurusConfigPath(repositoryRoot);
5803
- if (!configPath) return void 0;
5804
- const configSource = readBoundedText(configPath);
5805
- const cssRelativePath = configSource.match(/\bcustomCss\s*:\s*(?:\[\s*)?(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/)?.[2];
5806
- if (!cssRelativePath) return void 0;
5807
- let cssPath;
5808
- try {
5809
- cssPath = resolveWithin(repositoryRoot, cssRelativePath.replace(/^\.\//, ""));
5810
- } catch {
5811
- return void 0;
5898
+ if (Array.isArray(navigation)) {
5899
+ const sectionsBefore = context.apiSections.length;
5900
+ const groups = groupsFromConverted(convertNodes(navigation, routePrefix, context));
5901
+ for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
5902
+ context.apiSections[index].tabLabel ??= fallbackTabLabel;
5903
+ }
5904
+ if (groups.length > 0) {
5905
+ if (routePrefix.length > 0) addBareRouteRedirect(routePrefix, groups, context);
5906
+ return [{ tab: fallbackTabLabel, groups }];
5907
+ }
5812
5908
  }
5813
- if (!existsSync3(cssPath) || !lstatSync3(cssPath).isFile()) return void 0;
5814
- const css = readBoundedText(cssPath);
5815
- const pick = (block) => {
5816
- const value = block?.match(/--ifm-color-primary\s*:\s*([^;]+);/)?.[1]?.trim();
5817
- return value && DOCUSAURUS_HEX_COLOR.test(value) ? value : void 0;
5818
- };
5819
- const lightMode = pick(css.match(/:root\s*\{([^}]*)\}/)?.[1]);
5820
- const darkMode = pick(css.match(/\[data-theme=(['"])dark\1\]\s*\{([^}]*)\}/)?.[2]);
5821
- const colors = {};
5822
- if (darkMode) colors.light = darkMode;
5823
- if (lightMode) colors.dark = lightMode;
5824
- return Object.keys(colors).length > 0 ? colors : void 0;
5825
- }
5826
- function propertyStart(source, name) {
5827
- const pattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${name}["']?\\s*:\\s*`, "m");
5828
- const match = pattern.exec(source);
5829
- return match ? match.index + match[0].length : -1;
5909
+ return [];
5830
5910
  }
5831
- function staticStringBindings(source) {
5832
- const bindings = /* @__PURE__ */ new Map();
5833
- for (const match of source.matchAll(/\bconst\s+([A-Z][A-Z0-9_]*)\s*=\s*((?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))/g)) {
5911
+ function projectFernProducts(rawProducts, fernRoot, context) {
5912
+ return rawProducts.flatMap((entry) => {
5913
+ const product = objectValue3(entry);
5914
+ const rawPath = typeof product?.path === "string" ? product.path.trim() : "";
5915
+ if (!product || !rawPath) return [];
5916
+ const label = typeof product["display-name"] === "string" && product["display-name"].trim() ? product["display-name"].trim() : "Product";
5917
+ let productPath;
5834
5918
  try {
5835
- const value = JSON5.parse(match[2]);
5836
- if (typeof value === "string") bindings.set(match[1], value);
5919
+ productPath = withinRealRoot(resolveWithin(fernRoot, rawPath), context.repositoryRoot);
5837
5920
  } catch {
5921
+ warnOnce2(context, `fern-product-unsafe-${rawPath}`, `Skipped the Fern product "${label}" because "${rawPath}" points outside the repository (symlink or ..).`);
5922
+ return [];
5838
5923
  }
5839
- }
5840
- return bindings;
5841
- }
5842
- function staticArrayField(source, name) {
5843
- const start = propertyStart(source, name);
5844
- return start < 0 ? null : matchingArrayLiteral(source, start);
5845
- }
5846
- function staticObjectEntries(array) {
5847
- if (!array) return [];
5848
- const entries = [];
5849
- let afterDelimiter = 1;
5850
- let quote = "";
5851
- let lineComment = false;
5852
- let blockComment = false;
5853
- for (let index = 1; index < array.length - 1; index++) {
5854
- const char = array[index];
5855
- const next = array[index + 1];
5856
- if (lineComment) {
5857
- if (char === "\n") lineComment = false;
5858
- continue;
5924
+ if (!existsSync3(productPath) || !lstatSync3(productPath).isFile()) {
5925
+ warnOnce2(context, `fern-product-missing-${rawPath}`, `Fern product "${label}" path "${rawPath}" does not exist and was skipped.`);
5926
+ return [];
5859
5927
  }
5860
- if (blockComment) {
5861
- if (char === "*" && next === "/") {
5862
- blockComment = false;
5863
- index++;
5864
- }
5865
- continue;
5928
+ let productConfig;
5929
+ try {
5930
+ productConfig = objectValue3(readBoundedYaml(productPath));
5931
+ } catch (error) {
5932
+ warnOnce2(
5933
+ context,
5934
+ `fern-product-read-failed-${rawPath}`,
5935
+ `Fern product "${label}" could not be read (${error instanceof Error ? error.message : String(error)}) and was skipped.`
5936
+ );
5937
+ return [];
5866
5938
  }
5867
- if (quote) {
5868
- if (char === "\\") {
5869
- index++;
5870
- continue;
5871
- }
5872
- if (char === quote) quote = "";
5873
- continue;
5939
+ if (!productConfig) return [];
5940
+ const productDir = dirname3(productPath);
5941
+ const routeSegment = segmentFor(product, label, context);
5942
+ const priorPrefix = context.pathPrefix;
5943
+ context.pathPrefix = relative4(fernRoot, productDir).replace(/\\/g, "/");
5944
+ const before = restrictionMark(context);
5945
+ try {
5946
+ const tabs = buildTabsFromConfig(
5947
+ productConfig,
5948
+ productDir,
5949
+ fernRoot,
5950
+ routeSegment ? [routeSegment] : [],
5951
+ label,
5952
+ context
5953
+ );
5954
+ if (!isRestrictedNode(product)) return tabs;
5955
+ markRestricted(context, before);
5956
+ return [];
5957
+ } finally {
5958
+ context.pathPrefix = priorPrefix;
5874
5959
  }
5875
- if (char === "/" && next === "/") {
5876
- lineComment = true;
5877
- index++;
5878
- continue;
5879
- }
5880
- if (char === "/" && next === "*") {
5881
- blockComment = true;
5882
- index++;
5883
- continue;
5884
- }
5885
- if (char === '"' || char === "'" || char === "`") {
5886
- quote = char;
5887
- continue;
5888
- }
5889
- if (char === ",") {
5890
- afterDelimiter = index + 1;
5891
- continue;
5892
- }
5893
- if (char !== "{") continue;
5894
- const literal = matchingObjectLiteral(array, index);
5895
- if (!literal) continue;
5896
- const lead = array.slice(afterDelimiter, index).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim();
5897
- if (!lead) entries.push(literal);
5898
- index += literal.length - 1;
5899
- }
5900
- return entries;
5960
+ });
5901
5961
  }
5902
- function staticStringField(source, name, bindings) {
5903
- const start = propertyStart(source, name);
5904
- if (start < 0) return void 0;
5905
- const quoted = source.slice(start).match(/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/);
5906
- if (!quoted) {
5907
- const binding = source.slice(start).match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
5908
- return binding ? bindings?.get(binding) : void 0;
5962
+ function projectFernNavigation(input) {
5963
+ const context = {
5964
+ navigationWalk: { active: /* @__PURE__ */ new Set(), visits: 0, incomplete: false },
5965
+ fernRoot: input.fernRoot,
5966
+ repositoryRoot: input.repositoryRoot ?? input.fernRoot,
5967
+ descriptors: [],
5968
+ seenNavigationIds: /* @__PURE__ */ new Set(),
5969
+ apiSections: [],
5970
+ restrictedApiSections: [],
5971
+ changelogIndexes: [],
5972
+ warnings: [],
5973
+ warningKeys: /* @__PURE__ */ new Set(),
5974
+ externalLinks: [],
5975
+ bareRouteRedirects: [],
5976
+ segmentAliases: /* @__PURE__ */ new Map(),
5977
+ pathPrefix: "",
5978
+ sourcePathToNavigationId: /* @__PURE__ */ new Map()
5979
+ };
5980
+ const config = input.config;
5981
+ const hasProducts = Array.isArray(config.products) && config.products.length > 0 && !Array.isArray(config.navigation);
5982
+ const tabs = hasProducts ? projectFernProducts(config.products, input.fernRoot, context) : buildTabsFromConfig(config, input.fernRoot, input.fernRoot, [], "Documentation", context);
5983
+ if (tabs.length === 0) {
5984
+ context.warnings.push({
5985
+ code: "unsupported-config",
5986
+ message: "Fern navigation could not be projected; generated navigation will be used."
5987
+ });
5909
5988
  }
5910
- try {
5911
- const parsed = JSON5.parse(quoted[1]);
5912
- return typeof parsed === "string" ? parsed : void 0;
5913
- } catch {
5914
- return void 0;
5989
+ const navbarLinks = Array.isArray(config["navbar-links"]) ? config["navbar-links"].flatMap((entry) => {
5990
+ const link = objectValue3(entry);
5991
+ const href = typeof link?.url === "string" ? link.url : typeof link?.href === "string" ? link.href : typeof link?.value === "string" ? link.value : void 0;
5992
+ const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
5993
+ return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
5994
+ }) : [];
5995
+ for (const link of context.externalLinks) {
5996
+ if (!navbarLinks.some((existing) => existing.label === link.label && existing.href === link.href)) {
5997
+ navbarLinks.push(link);
5998
+ }
5915
5999
  }
5916
- }
5917
- function readDocusaurusSiteSettings(repositoryRoot) {
5918
- const empty2 = { docsRouteBasePath: "docs", defaultColorMode: "light", navbarLinks: [], footerLinks: [] };
5919
- const configPath = findDocusaurusConfigPath(repositoryRoot);
5920
- if (!configPath) return empty2;
5921
- const source = readBoundedText(configPath);
5922
- const bindings = staticStringBindings(source);
5923
- const presets = staticArrayField(source, "presets");
5924
- const classicDocs = presets && [...presets.matchAll(/["']?docs["']?\s*:\s*/g)].map((match) => matchingObjectLiteral(presets, (match.index ?? 0) + match[0].length)).find((object) => Boolean(object));
5925
- const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
5926
- const themeStart = propertyStart(source, "themeConfig");
5927
- const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
5928
- const colorModeStart = theme ? propertyStart(theme, "colorMode") : -1;
5929
- const colorMode = colorModeStart < 0 ? null : matchingObjectLiteral(theme, colorModeStart);
5930
- const configuredColorMode = colorMode ? staticStringField(colorMode, "defaultMode") : void 0;
5931
- const defaultColorMode = configuredColorMode === "dark" ? "dark" : "light";
5932
- const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
5933
- const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
5934
- const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
5935
- const logo = logoStart < 0 ? null : matchingObjectLiteral(navbar, logoStart);
5936
- const footerStart = theme ? propertyStart(theme, "footer") : -1;
5937
- const footer = footerStart < 0 ? null : matchingObjectLiteral(theme, footerStart);
5938
- const navbarLinks = navbar ? staticObjectEntries(staticArrayField(navbar, "items")).flatMap((item) => {
5939
- if (propertyStart(item, "items") >= 0 || propertyStart(item, "dropdownItemsAfter") >= 0) return [];
5940
- const docsPluginId = staticStringField(item, "docsPluginId", bindings);
5941
- const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
5942
- const docId = staticStringField(item, "docId", bindings);
5943
- const sidebarId = staticStringField(item, "sidebarId", bindings);
5944
- const label = staticStringField(item, "label", bindings) ?? staticStringField(item, "aria-label", bindings);
5945
- return label && (href || docId || sidebarId || docsPluginId) ? [{
5946
- label,
5947
- ...href ? { href } : {},
5948
- ...docId ? { docId } : {},
5949
- ...sidebarId ? { sidebarId } : {},
5950
- ...docsPluginId ? { docsPluginId } : {}
5951
- }] : [];
6000
+ const announcement = objectValue3(config.announcement);
6001
+ const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
6002
+ const bannerContent = announcementMarkdown(announcementMessage);
6003
+ const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
6004
+ const redirect = objectValue3(value);
6005
+ if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
6006
+ const rawSource = redirect.source.trim();
6007
+ const rawDestination = redirect.destination.trim();
6008
+ if (!isRedirectPathSafe(rawSource, rawDestination)) return [];
6009
+ const translated = translateRedirectWildcards(rawSource, rawDestination);
6010
+ if (!translated) {
6011
+ warnOnce2(
6012
+ context,
6013
+ `fern-redirect-wildcard-${rawSource}`,
6014
+ `Redirect from ${rawSource} uses a wildcard Next.js cannot express and was dropped.`
6015
+ );
6016
+ return [];
6017
+ }
6018
+ return [{
6019
+ source: translated.source,
6020
+ destination: translated.destination,
6021
+ ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
6022
+ }];
5952
6023
  }) : [];
5953
- const footerLinks = footer ? staticObjectEntries(staticArrayField(footer, "links")).flatMap((column) => {
5954
- const heading = staticStringField(column, "title", bindings);
5955
- if (!heading) return [];
5956
- const items = staticObjectEntries(staticArrayField(column, "items")).flatMap((item) => {
5957
- const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
5958
- const label = staticStringField(item, "label", bindings);
5959
- return label && href ? [{ label, href }] : [];
6024
+ const authoredSources = new Set(redirects.map((redirect) => redirect.source));
6025
+ const knownIds = new Set(context.descriptors.map((descriptor) => descriptor.navigationId));
6026
+ const keptNavigationId = (descriptor) => context.sourcePathToNavigationId.get(descriptor.sourcePath) ?? descriptor.navigationId;
6027
+ const segmentAliasRedirects = context.segmentAliases.size > 0 ? context.descriptors.flatMap((descriptor) => {
6028
+ const segments = descriptor.navigationId.split("/");
6029
+ let changed = false;
6030
+ const aliased = segments.map((segment) => {
6031
+ const literal = context.segmentAliases.get(segment);
6032
+ if (!literal) return segment;
6033
+ changed = true;
6034
+ return literal;
5960
6035
  });
5961
- return items.length > 0 ? [{ heading, items }] : [];
6036
+ if (!changed) return [];
6037
+ const source = `/${aliased.join("/")}`;
6038
+ const destination = `/${keptNavigationId(descriptor)}`;
6039
+ if (source === destination || knownIds.has(aliased.join("/"))) return [];
6040
+ return [{ source, destination }];
5962
6041
  }) : [];
5963
- const copyright = footer && propertyStart(footer, "copyright") >= 0 ? staticStringField(footer, "copyright") ?? footer.slice(propertyStart(footer, "copyright")).match(/^`([^`]+)`/)?.[1]?.replace(/\$\{new Date\(\)\.getFullYear\(\)\}/g, "{year}") : void 0;
5964
- return {
5965
- name: staticStringField(source, "title"),
5966
- description: staticStringField(source, "tagline"),
5967
- ...navbar && staticStringField(navbar, "title") ? { navbarTitle: staticStringField(navbar, "title") } : {},
5968
- defaultColorMode,
5969
- docsRouteBasePath,
5970
- ...logo && staticStringField(logo, "src") ? {
5971
- logo: {
5972
- light: staticStringField(logo, "src"),
5973
- ...staticStringField(logo, "srcDark") ? { dark: staticStringField(logo, "srcDark") } : {},
5974
- showTitle: Boolean(navbar && staticStringField(navbar, "title"))
5975
- }
5976
- } : {},
5977
- ...staticStringField(source, "favicon") ? { favicon: staticStringField(source, "favicon") } : {},
5978
- navbarLinks,
5979
- footerLinks,
5980
- ...copyright ? { copyright } : {}
5981
- };
5982
- }
5983
- function autogeneratedItems(dirName, context) {
5984
- const normalizedDir = dirName === "." ? "" : normalizeDocId(dirName);
5985
- const inDirectory = context.descriptors.filter((descriptor) => {
5986
- const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
5987
- return !normalizedDir || source === normalizedDir || source.startsWith(`${normalizedDir}/`);
6042
+ const duplicateNavRedirects = context.descriptors.flatMap((descriptor) => {
6043
+ const kept = keptNavigationId(descriptor);
6044
+ if (kept === descriptor.navigationId) return [];
6045
+ return [{ source: `/${descriptor.navigationId}`, destination: `/${kept}` }];
5988
6046
  });
5989
- const directPages = [];
5990
- const childDirectories = /* @__PURE__ */ new Set();
5991
- for (const descriptor of inDirectory) {
5992
- const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
5993
- const relativePath = normalizedDir ? posix3.relative(normalizedDir, source) : source;
5994
- const segments = relativePath.split("/").filter(Boolean);
5995
- if (segments.length <= 1) directPages.push(descriptor);
5996
- else childDirectories.add(segments[0]);
5997
- }
5998
- const pages = directPages.map((descriptor) => ({
5999
- // An index document is the category landing page in Docusaurus. Thally
6000
- // renders it as a child link, so put it before the category's other docs.
6001
- position: descriptor.sidebarPosition ?? (/(?:^|\/)(?:index|readme)\.mdx?$/i.test(descriptor.sourcePath) ? -1 : Number.MAX_SAFE_INTEGER),
6002
- sortKey: descriptor.sourcePath,
6003
- item: descriptor.navigationId
6004
- }));
6005
- const groups = [...childDirectories].map((segment) => {
6006
- const directory = normalizedDir ? posix3.join(normalizedDir, segment) : segment;
6007
- const metadata = readCategoryMetadata(context.contentRoot, directory);
6008
- const pages2 = autogeneratedItems(directory, context);
6009
- if (pages2.length === 1 && typeof pages2[0] === "string" && !metadata.label && metadata.position === void 0 && !metadata.link) {
6010
- const onlyPage = context.descriptors.find((descriptor) => descriptor.navigationId === pages2[0]);
6011
- if (onlyPage && /(?:^|\/)(?:index|readme)\.mdx?$/i.test(onlyPage.sourcePath)) {
6012
- return {
6013
- position: onlyPage.sidebarPosition ?? Number.MAX_SAFE_INTEGER,
6014
- sortKey: segment,
6015
- item: pages2[0]
6016
- };
6017
- }
6047
+ const allRedirects = [
6048
+ ...redirects,
6049
+ ...context.bareRouteRedirects.filter((redirect) => !authoredSources.has(redirect.source)),
6050
+ ...segmentAliasRedirects.filter((redirect) => !authoredSources.has(redirect.source)),
6051
+ ...duplicateNavRedirects.filter((redirect) => !authoredSources.has(redirect.source))
6052
+ ];
6053
+ const claimedTabLabels = /* @__PURE__ */ new Set();
6054
+ const disambiguatedApiSections = context.apiSections.map((section) => {
6055
+ if (!section.tabLabel || !claimedTabLabels.has(section.tabLabel)) {
6056
+ if (section.tabLabel) claimedTabLabels.add(section.tabLabel);
6057
+ return section;
6018
6058
  }
6019
- const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context, directory);
6020
- if (landing && !pages2.includes(landing)) pages2.unshift(landing);
6021
- return {
6022
- position: metadata.position ?? Number.MAX_SAFE_INTEGER,
6023
- sortKey: segment,
6024
- item: {
6025
- group: metadata.label ?? titleCase2(segment),
6026
- pages: pages2
6059
+ const distinguishingSegment = section.routeSegments.at(-1);
6060
+ let tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase2(distinguishingSegment)}` : section.tabLabel;
6061
+ if (!tabLabel || tabLabel === `${section.tabLabel}: ${section.tabLabel}` || claimedTabLabels.has(tabLabel)) {
6062
+ let ordinal = 2;
6063
+ let candidate = `${section.tabLabel} (${ordinal})`;
6064
+ while (claimedTabLabels.has(candidate)) {
6065
+ ordinal += 1;
6066
+ candidate = `${section.tabLabel} (${ordinal})`;
6027
6067
  }
6028
- };
6029
- });
6030
- return [...pages, ...groups].sort((left, right) => left.position - right.position || left.sortKey.localeCompare(right.sortKey, void 0, { numeric: true })).map(({ item }) => {
6031
- if (typeof item === "string") context.referencedNavigationIds.add(item);
6032
- return item;
6068
+ tabLabel = candidate;
6069
+ }
6070
+ claimedTabLabels.add(tabLabel);
6071
+ return { ...section, tabLabel };
6033
6072
  });
6034
- }
6035
- function convertItems(value, context) {
6036
- if (typeof value === "string") {
6037
- const page = registerDoc(value, context);
6038
- return page ? [page] : [];
6039
- }
6040
- if (Array.isArray(value)) return value.flatMap((entry) => convertItems(entry, context));
6041
- const object = objectValue3(value);
6042
- if (!object) return [];
6043
- if (object.type === "doc" || object.type === "ref") {
6044
- const page = typeof object.id === "string" ? registerDoc(object.id, context) : null;
6045
- return page ? [page] : [];
6046
- }
6047
- if (object.type === "autogenerated") {
6048
- return autogeneratedItems(typeof object.dirName === "string" ? object.dirName : ".", context);
6049
- }
6050
- if (object.type === "category") {
6051
- const label = typeof object.label === "string" ? object.label : "Documentation";
6052
- const pages = convertItems(object.items, context);
6053
- const landing = generatedIndexPage(label, object.link, context);
6054
- if (landing && !pages.includes(landing)) pages.unshift(landing);
6055
- return pages.length > 0 ? [{ group: label, pages }] : [];
6056
- }
6057
- if (object.type === "link" || object.type === "html") return [];
6058
- return Object.entries(object).flatMap(([label, items]) => {
6059
- const pages = convertItems(items, context);
6060
- return pages.length > 0 ? [{ group: label, pages }] : [];
6061
- });
6062
- }
6063
- function projectDocusaurusNavigation(input) {
6064
- const warnings = [];
6065
- const descriptorByDocId = new Map(input.descriptors.map((page) => [page.docId, page]));
6066
- const context = {
6067
- contentRoot: input.contentRoot,
6068
- descriptors: input.descriptors,
6069
- descriptorByDocId,
6070
- generatedPages: [],
6071
- generatedIds: /* @__PURE__ */ new Set(),
6072
- referencedNavigationIds: /* @__PURE__ */ new Set(),
6073
- sourceUrl: input.sourceUrl,
6074
- routePrefix: input.routePrefix ?? "",
6075
- sidebarSource: input.sidebars?.sourcePath ?? "autogenerated sidebar",
6076
- warnings
6077
- };
6078
- const sidebarEntries = input.sidebars ? Object.entries(input.sidebars.config) : [["docs", [
6079
- { type: "autogenerated", dirName: "." }
6080
- ]]];
6081
- const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
6082
- const converted = convertItems(value, context);
6083
- if (converted.length === 0) return [];
6084
- return [{
6085
- tab: sidebarEntries.length === 1 ? "Documentation" : titleCase2(sidebarId),
6086
- // Docusaurus categories are disclosures, including at the sidebar
6087
- // root. Keeping them as Thally root nodes preserves their collapsed
6088
- // state and the authored interleaving of documents and categories.
6089
- pages: converted
6090
- }];
6091
- });
6092
- const unreferenced = input.descriptors.filter((page) => !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
6093
- if (unreferenced.length > 0) {
6094
- if (tabs.length === 0) tabs.push({ tab: "Documentation", pages: [] });
6095
- tabs[0].pages?.push({ group: "Additional", pages: unreferenced });
6096
- }
6097
- return {
6098
- docsConfig: { tabs },
6099
- generatedPages: context.generatedPages,
6100
- referencedNavigationIds: context.referencedNavigationIds,
6101
- warnings
6102
- };
6073
+ return {
6074
+ navigationIncomplete: context.navigationWalk.incomplete,
6075
+ docsConfig: {
6076
+ tabs,
6077
+ ...bannerContent ? { banner: { content: bannerContent, dismissible: true } } : {},
6078
+ ...navbarLinks.length > 0 ? { navbar: { links: navbarLinks } } : {},
6079
+ ...allRedirects.length > 0 ? { redirects: allRedirects } : {}
6080
+ },
6081
+ descriptors: context.descriptors,
6082
+ warnings: context.warnings,
6083
+ apiSections: disambiguatedApiSections,
6084
+ restrictedApiSections: context.restrictedApiSections,
6085
+ changelogIndexes: context.changelogIndexes
6086
+ };
6103
6087
  }
6104
6088
 
6105
- // src/fern.ts
6106
- import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
6107
- import { basename as basename2, dirname as dirname3, extname as extname5, relative as relative4 } from "path";
6108
- import { load } from "cheerio";
6089
+ // src/docusaurus.ts
6090
+ import JSON5 from "json5";
6091
+ import ts4 from "typescript";
6109
6092
  import { parse as parseYaml3 } from "yaml";
6110
6093
  var MAX_CONFIG_BYTES2 = 2e7;
6111
- function announcementMarkdown(message) {
6112
- const document = load(message);
6113
- document("script, style, iframe, object, embed, svg, math").remove();
6114
- const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([\\`*_\[\]{}()!])/g, "\\$1");
6115
- const links = /* @__PURE__ */ new Map();
6116
- document("a").each((index, element) => {
6117
- const marker = `THALLYANNOUNCEMENTLINK${index}TOKEN`;
6118
- const label = document(element).text().trim();
6119
- const rawHref = document(element).attr("href")?.trim() ?? "";
6120
- let href;
6121
- if (/^https?:\/\//i.test(rawHref)) {
6122
- try {
6123
- href = new URL(rawHref).toString();
6124
- } catch {
6125
- }
6126
- } else if (rawHref.startsWith("/") && !rawHref.startsWith("//") && !/[\\<>\s]/.test(rawHref)) {
6127
- href = rawHref;
6128
- }
6129
- const destination = href?.replace(/\(/g, "%28").replace(/\)/g, "%29");
6130
- links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
6131
- document(element).replaceWith(marker);
6132
- });
6133
- let text2 = escapeText(document("body").text());
6134
- for (const [marker, link] of links) text2 = text2.replaceAll(marker, link);
6135
- return text2.trim();
6094
+ var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
6095
+ var SIDEBAR_FILENAMES = [
6096
+ "sidebars.json",
6097
+ "sidebars.js",
6098
+ "sidebars.cjs",
6099
+ "sidebars.mjs",
6100
+ "sidebars.ts"
6101
+ ];
6102
+ function escapeRegExp(value) {
6103
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6136
6104
  }
6137
6105
  function objectValue4(value) {
6138
6106
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
6139
6107
  }
6140
- function titleCase3(value) {
6141
- return value.split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
6108
+ function stripNumberPrefix(value) {
6109
+ return value.replace(/^\d+[-_]+/, "");
6142
6110
  }
6143
- function readBoundedYaml(path) {
6144
- if (lstatSync4(path).size > MAX_CONFIG_BYTES2) {
6145
- throw new Error("The Fern config is larger than 20 MB and could not be imported.");
6111
+ function normalizeDocId(value) {
6112
+ return value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").split("/").filter(Boolean).map(stripNumberPrefix).join("/");
6113
+ }
6114
+ function docusaurusSlugifySegment(value) {
6115
+ let decoded = value;
6116
+ try {
6117
+ decoded = decodeURIComponent(value);
6118
+ } catch {
6146
6119
  }
6147
- return parseYaml3(readFileSync4(path, "utf8"));
6120
+ const slug2 = decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
6121
+ return slug2 === "." || slug2 === ".." ? "" : slug2;
6148
6122
  }
6149
- function readFernConfig(fernRoot) {
6150
- const sourcePath = "docs.yml";
6151
- const path = resolveWithin(fernRoot, sourcePath);
6152
- if (!existsSync4(path) || !lstatSync4(path).isFile()) return null;
6153
- const config = objectValue4(readBoundedYaml(path));
6154
- if (!config) throw new Error("fern/docs.yml must contain an object.");
6155
- return { config, sourcePath };
6123
+ function docusaurusRouteId(value) {
6124
+ const segments = normalizeDocId(value).split("/").map(docusaurusSlugifySegment).filter(Boolean);
6125
+ return segments.join("/") || "introduction";
6156
6126
  }
6157
- function warnOnce2(context, key, message) {
6158
- if (context.warningKeys.has(key)) return;
6159
- context.warningKeys.add(key);
6160
- context.warnings.push({ code: "unsupported-config", message });
6127
+ function defaultDocusaurusRouteId(value) {
6128
+ const segments = normalizeDocId(value).split("/").filter(Boolean);
6129
+ if (/^(?:index|readme)$/i.test(segments.at(-1) ?? "")) segments.pop();
6130
+ const normalized = segments.map(docusaurusSlugifySegment).filter(Boolean);
6131
+ return normalized.join("/") || "introduction";
6161
6132
  }
6162
- function firstLeafId(nodes) {
6163
- for (const node of nodes) {
6164
- if (typeof node === "string") return node;
6165
- const nested = firstLeafId(node.pages);
6166
- if (nested) return nested;
6167
- }
6168
- return void 0;
6133
+ function storageIdForRoute(routeId) {
6134
+ return /(?:^|\/)(?:index|readme)$/i.test(routeId) ? `${routeId}/index` : routeId;
6169
6135
  }
6170
- function addBareRouteRedirect(segments, pages, context) {
6171
- if (segments.length === 0) return;
6172
- const destination = firstLeafId(pages);
6173
- const source = segments.join("/");
6174
- if (destination && destination !== source) {
6175
- context.bareRouteRedirects.push({ source: `/${source}`, destination: `/${destination}` });
6176
- }
6136
+ function titleCase3(value) {
6137
+ return stripNumberPrefix(value).replace(/sidebar$/i, "").split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
6177
6138
  }
6178
- var FERN_SEGMENT_ALIAS = /^[\w.-]+$/;
6179
- function fernBasicSlug(value) {
6180
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
6139
+ function routeFromSlug(slug2, fallback, sourceDirectory) {
6140
+ if (slug2 === "/") return "introduction";
6141
+ if (slug2.startsWith("/")) return docusaurusRouteId(slug2);
6142
+ const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix3.dirname(fallback));
6143
+ const resolved = posix3.normalize(posix3.join(fallbackDirectory, slug2));
6144
+ if (resolved === ".." || resolved.startsWith("../")) return null;
6145
+ return docusaurusRouteId(resolved);
6181
6146
  }
6182
- function fernLabelSlug(label) {
6183
- const decamelized = label.replace(/[()]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2");
6184
- return fernBasicSlug(decamelized);
6147
+ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
6148
+ const sourceDocId = normalizeDocId(sourcePath);
6149
+ const sourceDirectory = posix3.dirname(sourceDocId);
6150
+ const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
6151
+ const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix3.join(sourceDirectory, configuredId)) : sourceDocId;
6152
+ const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
6153
+ const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
6154
+ const navigationId = configuredSlug ? routeFromSlug(
6155
+ configuredSlug,
6156
+ defaultNavigationId,
6157
+ sourceDirectory === "." ? "" : sourceDirectory
6158
+ ) ?? defaultNavigationId : defaultNavigationId;
6159
+ const position = typeof frontmatter.sidebar_position === "number" && Number.isFinite(frontmatter.sidebar_position) ? frontmatter.sidebar_position : void 0;
6160
+ return {
6161
+ identity: { id: storageIdForRoute(navigationId), navigationId },
6162
+ descriptor: {
6163
+ sourcePath: sourcePath.replace(/\\/g, "/"),
6164
+ docId,
6165
+ navigationId,
6166
+ ...position === void 0 ? {} : { sidebarPosition: position },
6167
+ ...isUnlisted(frontmatter.unlisted) ? { unlisted: true } : {}
6168
+ }
6169
+ };
6185
6170
  }
6186
- function segmentFor(value, label, context) {
6187
- if (value["skip-slug"] === true) return null;
6188
- if (typeof value.slug !== "string") return fernLabelSlug(label) || null;
6189
- const explicit = value.slug.trim();
6190
- if (!explicit) return null;
6191
- const slugified = fernBasicSlug(explicit);
6192
- if (context && explicit !== slugified && FERN_SEGMENT_ALIAS.test(explicit)) {
6193
- context.segmentAliases.set(slugified, explicit);
6194
- }
6195
- return slugified || null;
6171
+ function isUnlisted(value) {
6172
+ return value !== void 0 && value !== null && value !== false && !(typeof value === "string" && ["", "false"].includes(value.trim().toLowerCase()));
6196
6173
  }
6197
- function uniqueNavigationId(base, context) {
6198
- if (!context.seenNavigationIds.has(base)) return base;
6199
- let index = 2;
6200
- while (context.seenNavigationIds.has(`${base}-${index}`)) index++;
6201
- return `${base}-${index}`;
6174
+ function sourceReferenceKey(value) {
6175
+ return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
6202
6176
  }
6203
- function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
6204
- let sourcePath;
6205
- try {
6206
- const absolute = resolveWithinRoot(
6207
- context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
6208
- rawPath.trim(),
6209
- context.repositoryRoot
6210
- );
6211
- sourcePath = relative4(context.fernRoot, absolute).replace(/\\/g, "/");
6212
- } catch {
6213
- warnOnce2(context, `fern-unsafe-page-${rawPath}`, `Fern page path "${rawPath}" escapes the repository and was skipped.`);
6214
- return "";
6215
- }
6216
- const navigationId = uniqueNavigationId(base || "introduction", context);
6217
- context.seenNavigationIds.add(navigationId);
6218
- const isHidden = hidden || context.hiddenInherited === true;
6219
- context.descriptors.push({ sourcePath, navigationId, ...navTitle ? { navTitle } : {}, ...isHidden ? { hidden: true } : {} });
6220
- const kept = context.sourcePathToNavigationId.get(sourcePath);
6221
- if (kept) {
6222
- if (!isHidden) {
6223
- const owner = context.descriptors.find((descriptor) => descriptor.navigationId === kept);
6224
- if (owner) owner.hidden = false;
6177
+ function sourceHeadings(body) {
6178
+ const headings = [];
6179
+ const lines = body.split("\n");
6180
+ let fence = null;
6181
+ for (let index = 0; index < lines.length; index++) {
6182
+ const line = lines[index];
6183
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
6184
+ if (marker) {
6185
+ if (!fence) fence = marker[1];
6186
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = null;
6187
+ continue;
6225
6188
  }
6226
- return kept;
6189
+ if (fence) continue;
6190
+ const heading = line.match(/^ {0,3}(#{2,6})\s+(.+?)\s*#*\s*$/);
6191
+ if (!heading) continue;
6192
+ const preceding = lines[index - 1]?.match(/^<a id="([\p{L}\p{N}_.:$-]{1,120})"><\/a>$/u);
6193
+ const plainText2 = !/[<>{}\[\]`*_]/.test(heading[2]);
6194
+ const generatedId = plainText2 ? heading[2].normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "") : null;
6195
+ headings.push({ index, level: heading[1].length, id: preceding?.[1] ?? generatedId });
6227
6196
  }
6228
- context.sourcePathToNavigationId.set(sourcePath, navigationId);
6229
- return navigationId;
6197
+ return headings;
6230
6198
  }
6231
- function registerFolder(object, parentSegments, context) {
6232
- const rawPath = String(object.folder).trim();
6233
- let folder;
6234
- try {
6235
- folder = resolveWithinRoot(
6236
- context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
6237
- rawPath,
6238
- context.repositoryRoot
6239
- );
6240
- } catch {
6241
- warnOnce2(context, `fern-unsafe-folder-${rawPath}`, `Fern folder "${rawPath}" escapes the repository and was skipped.`);
6242
- return null;
6243
- }
6244
- if (!existsSync4(folder) || !lstatSync4(folder).isDirectory()) {
6245
- warnOnce2(context, `fern-missing-folder-${rawPath}`, `Fern folder "${rawPath}" does not exist and was skipped.`);
6246
- return null;
6247
- }
6248
- const label = typeof object.title === "string" && object.title.trim() ? object.title.trim() : titleCase3(basename2(folder));
6249
- const segment = typeof object.slug === "string" || object["skip-slug"] === true ? segmentFor(object, label, context) : fernBasicSlug(basename2(folder));
6250
- const segments = segment ? [...parentSegments, segment] : parentSegments;
6251
- const visit = (directory, routeSegments, depth) => {
6252
- if (depth > 12 || context.descriptors.length >= 5e3) return [];
6253
- const pages2 = [];
6254
- const entries = readdirSync(directory, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && !entry.isSymbolicLink()).sort((left, right) => Number(left.name !== "index.mdx" && left.name !== "index.md") - Number(right.name !== "index.mdx" && right.name !== "index.md") || left.name.localeCompare(right.name));
6255
- for (const entry of entries) {
6256
- const path = resolveWithin(directory, entry.name);
6257
- if (entry.isDirectory()) {
6258
- const nested = visit(path, [...routeSegments, fernBasicSlug(entry.name)], depth + 1);
6259
- if (nested.length) pages2.push({ group: titleCase3(entry.name), pages: nested });
6260
- } else if (entry.isFile() && [".md", ".mdx"].includes(extname5(entry.name).toLowerCase())) {
6261
- const stem = basename2(entry.name, extname5(entry.name));
6262
- const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
6263
- const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
6264
- const source = lstatSync4(path).size <= MAX_CONFIG_BYTES2 ? readFileSync4(path, "utf8") : "";
6265
- const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
6266
- const hidden = /^hidden:\s*true\s*$/m.test(frontmatter);
6267
- const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" }, void 0, hidden);
6268
- if (registered && !hidden && !context.hiddenInherited) pages2.push(registered);
6269
- }
6199
+ function addDocusaurusTranslatedHeadingAliases(pages) {
6200
+ const byRoute = new Map(pages.map((page) => [page.navigationId, page]));
6201
+ for (const page of pages) {
6202
+ const locale = page.source.match(/(?:^|[/#])i18n\/([A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*)\/docusaurus-plugin-content-docs\//)?.[1];
6203
+ if (!locale) continue;
6204
+ const originalRoute = page.navigationId === locale ? "introduction" : page.navigationId.startsWith(`${locale}/`) ? page.navigationId.slice(locale.length + 1) : null;
6205
+ if (!originalRoute) continue;
6206
+ const original = byRoute.get(originalRoute);
6207
+ if (!original) continue;
6208
+ const originalHeadings = sourceHeadings(original.body);
6209
+ const translatedHeadings = sourceHeadings(page.body);
6210
+ if (!originalHeadings.length || originalHeadings.length !== translatedHeadings.length || originalHeadings.some((heading, index) => heading.level !== translatedHeadings[index].level)) continue;
6211
+ const occupied = new Set(translatedHeadings.map((heading) => heading.id).filter((id) => !!id));
6212
+ for (const match of page.body.matchAll(/<a\s+id="([^"]+)"/g)) occupied.add(match[1]);
6213
+ const originalIdCounts = /* @__PURE__ */ new Map();
6214
+ for (const heading of originalHeadings) {
6215
+ if (heading.id) originalIdCounts.set(heading.id, (originalIdCounts.get(heading.id) ?? 0) + 1);
6270
6216
  }
6271
- return pages2;
6272
- };
6273
- const pages = visit(folder, segments, 0);
6274
- if (!pages.length) return null;
6275
- addBareRouteRedirect(segments, pages, context);
6276
- return { group: label, pages };
6277
- }
6278
- function registerPage(object, parentSegments, context) {
6279
- if (typeof object.path !== "string" || !object.path.trim()) return null;
6280
- const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
6281
- const segment = segmentFor(object, label, context);
6282
- const base = [...parentSegments, segment].filter(Boolean).join("/");
6283
- return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
6284
- }
6285
- function convertNode(node, parentSegments, context) {
6286
- const object = objectValue4(node);
6287
- if (!object) return null;
6288
- if (typeof object.page === "string") {
6289
- const page = registerPage(object, parentSegments, context);
6290
- return object.hidden === true || context.hiddenInherited ? null : page;
6291
- }
6292
- if (typeof object.folder === "string") {
6293
- const folder = registerFolder(object, parentSegments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
6294
- return object.hidden === true || context.hiddenInherited ? null : folder;
6217
+ const aliases = /* @__PURE__ */ new Map();
6218
+ for (let index = 0; index < originalHeadings.length; index++) {
6219
+ const id = originalHeadings[index].id;
6220
+ if (!id || occupied.has(id) || originalIdCounts.get(id) !== 1) continue;
6221
+ aliases.set(translatedHeadings[index].index, id);
6222
+ occupied.add(id);
6223
+ }
6224
+ if (aliases.size) page.body = page.body.split("\n").flatMap((line, index) => {
6225
+ const alias = aliases.get(index);
6226
+ return alias ? [`<a id="${alias}"></a>`, line] : [line];
6227
+ }).join("\n");
6295
6228
  }
6296
- if (typeof object.section === "string") {
6297
- const label = object.section;
6298
- const segment = segmentFor(object, label, context);
6299
- const segments = segment ? [...parentSegments, segment] : parentSegments;
6300
- const contents = Array.isArray(object.contents) ? object.contents : [];
6301
- const pages = [];
6302
- const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
6303
- if (hasOwnPath) {
6304
- const registered = registerPageAt(object.path, segments.join("/"), context, label, object.hidden === true);
6305
- if (registered) pages.push(registered);
6229
+ }
6230
+ function expandDocusaurusDocCardLists(pages, config, sourceRoutes) {
6231
+ const pageByRoute = new Map(pages.map((page) => [page.navigationId, page]));
6232
+ const containsRoute = (items, route) => items.some((item) => typeof item === "string" ? item === route : containsRoute(item.pages, route));
6233
+ const findGroup = (items, route) => {
6234
+ for (const item of items) {
6235
+ if (typeof item === "string" || !containsRoute(item.pages, route)) continue;
6236
+ return findGroup(item.pages, route) ?? item;
6306
6237
  }
6307
- for (const child of contents) {
6308
- const converted = convertNode(child, segments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
6309
- if (converted) pages.push(converted);
6238
+ return void 0;
6239
+ };
6240
+ const firstRoute = (items) => {
6241
+ for (const item of items) {
6242
+ if (typeof item === "string") return item;
6243
+ const candidate = firstRoute(item.pages);
6244
+ if (candidate) return candidate;
6310
6245
  }
6311
- if (pages.length === 0) return null;
6312
- if (!hasOwnPath && segment) addBareRouteRedirect(segments, pages, context);
6313
- if (object.hidden === true || context.hiddenInherited) return null;
6314
- return {
6315
- group: label,
6316
- ...typeof object.icon === "string" ? { icon: object.icon } : {},
6317
- ...object.hidden === true ? { hidden: true } : {},
6318
- pages
6319
- };
6320
- }
6321
- if (typeof object.link === "string") {
6322
- const label = object.link.trim();
6323
- const rawHref = typeof object.href === "string" ? object.href.trim() : "";
6324
- let href;
6325
- if (/^https?:\/\//i.test(rawHref) && !/[\\\u0000-\u0020\u007f]/.test(rawHref)) {
6326
- try {
6327
- const url = new URL(rawHref);
6328
- if (["http:", "https:"].includes(url.protocol) && !url.username && !url.password) href = url.toString();
6329
- } catch {
6246
+ return void 0;
6247
+ };
6248
+ for (const page of pages) {
6249
+ if (!sourceRoutes.has(page.navigationId)) continue;
6250
+ const group = config.tabs.map((tab) => findGroup([
6251
+ ...tab.pages ?? [],
6252
+ ...tab.groups ?? []
6253
+ ], page.navigationId)).find(Boolean);
6254
+ if (!group) {
6255
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, "");
6256
+ continue;
6257
+ }
6258
+ const cards = group.pages.flatMap((item) => {
6259
+ if (typeof item === "string") {
6260
+ if (item === page.navigationId) return [];
6261
+ const target = pageByRoute.get(item);
6262
+ return [{ title: target?.title ?? item.split("/").at(-1) ?? item, route: item }];
6330
6263
  }
6264
+ const slug2 = slugifySegment(item.group);
6265
+ const routes = [...item.pages].flatMap((child) => typeof child === "string" ? [child] : []);
6266
+ const route = routes.find((candidate) => candidate === slug2 || candidate.endsWith(`/${slug2}`)) ?? firstRoute(item.pages);
6267
+ return route ? [{ title: item.group, route }] : [];
6268
+ });
6269
+ const markup = cards.length ? `<CardGroup>
6270
+ ${cards.map(({ title, route }) => ` <Card title={${JSON.stringify(title)}} href={${JSON.stringify(`/${route === "introduction" ? "" : route}`)}} />`).join("\n")}
6271
+ </CardGroup>` : "";
6272
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, markup);
6273
+ if (markup && !page.body.includes(markup)) page.body = [page.body.trim(), markup].filter(Boolean).join("\n\n");
6274
+ }
6275
+ }
6276
+ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
6277
+ const routes = /* @__PURE__ */ new Map();
6278
+ for (const descriptor of descriptors) {
6279
+ const sourceKey = sourceReferenceKey(descriptor.sourcePath);
6280
+ routes.set(sourceKey, descriptor.navigationId);
6281
+ routes.set(sourceReferenceKey(descriptor.docId), descriptor.navigationId);
6282
+ routes.set(sourceReferenceKey(descriptor.navigationId), descriptor.navigationId);
6283
+ const sourceRoute = pageIdFromReference(sourceKey);
6284
+ if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
6285
+ }
6286
+ const currentDirectory = posix3.dirname(sourceReferenceKey(current.sourcePath));
6287
+ function copiedAssetTarget(target) {
6288
+ if (!target.startsWith("/") || target.startsWith("//") || target.includes("\\")) return null;
6289
+ const suffixIndex = target.search(/[?#]/);
6290
+ const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
6291
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
6292
+ if (path.startsWith("/docs/") && options.assetPaths?.has(path.slice("/docs/".length))) {
6293
+ return `/${path.slice("/docs/".length)}${suffix}`;
6331
6294
  }
6332
- if (label && href && object.hidden !== true && !context.hiddenInherited) {
6333
- if (!context.externalLinks.some((link) => link.label === label && link.href === href)) {
6334
- context.externalLinks.push({ label, href });
6295
+ return options.assetPaths?.has(path.slice(1)) ? target : null;
6296
+ }
6297
+ function rewriteTarget(target) {
6298
+ if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
6299
+ if (target.startsWith("#")) return target;
6300
+ const suffixIndex = target.search(/[?#]/);
6301
+ const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
6302
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
6303
+ const copiedAsset = copiedAssetTarget(target);
6304
+ if (copiedAsset) return copiedAsset;
6305
+ const candidates = path.startsWith("/") ? [
6306
+ sourceReferenceKey(path),
6307
+ sourceReferenceKey(path).replace(/^docs\//, ""),
6308
+ sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
6309
+ ] : [
6310
+ sourceReferenceKey(posix3.normalize(posix3.join(currentDirectory, path))),
6311
+ sourceReferenceKey(path)
6312
+ ];
6313
+ const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
6314
+ if (!route) {
6315
+ if (path.startsWith("/") && path !== "/" && options.sourceOrigin) {
6316
+ options.onExternalLink?.(target);
6317
+ return new URL(target, options.sourceOrigin).toString();
6335
6318
  }
6336
- } else if (!href && object.hidden !== true && !context.hiddenInherited) {
6337
- warnOnce2(context, `fern-link-${rawHref}`, `Fern navigation link "${label}" has an unsafe or invalid external URL and was skipped.`);
6319
+ return target;
6338
6320
  }
6339
- return null;
6321
+ return `${route === "introduction" ? "/" : `/${route}`}${suffix}`;
6340
6322
  }
6341
- if (typeof object.api === "string") {
6342
- const ownSegment = segmentFor(object, object.api, context);
6343
- context.apiSections.push({
6344
- name: typeof object["api-name"] === "string" ? object["api-name"] : object.api,
6345
- nameExplicit: typeof object["api-name"] === "string",
6346
- routeSegments: ownSegment ? [...parentSegments, ownSegment] : parentSegments
6323
+ let codeFence = null;
6324
+ return body.split("\n").map((line) => {
6325
+ const fence = line.match(/^\s{0,3}(`{3,}|~{3,})/);
6326
+ if (fence) {
6327
+ if (!codeFence) codeFence = fence[1];
6328
+ else if (fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length && /^\s*$/.test(line.slice(fence[0].length))) codeFence = null;
6329
+ return line;
6330
+ }
6331
+ if (codeFence) return line;
6332
+ return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/(\bsrc=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${copiedAssetTarget(target) ?? target}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
6333
+ const bracketed = destination.startsWith("<") && destination.endsWith(">");
6334
+ const target = bracketed ? destination.slice(1, -1) : destination;
6335
+ const rewritten = rewriteTarget(target);
6336
+ return `${prefix}${bracketed ? `<${rewritten}>` : rewritten}`;
6347
6337
  });
6348
- return null;
6338
+ }).join("\n");
6339
+ }
6340
+ function readDocusaurusSiteOrigin(repositoryRoot) {
6341
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
6342
+ if (!configPath) return void 0;
6343
+ const address = readBoundedText(configPath).match(/\burl['"]?\s*:\s*(['"])(https?:\/\/[^'"]+)\1/)?.[2];
6344
+ if (!address) return void 0;
6345
+ try {
6346
+ return new URL(address).origin;
6347
+ } catch {
6348
+ return void 0;
6349
6349
  }
6350
- if (typeof object.changelog === "string") {
6351
- return registerFolder({ folder: object.changelog, title: "Changelog", "skip-slug": true }, parentSegments, context);
6350
+ }
6351
+ function readBoundedText(path) {
6352
+ if (lstatSync4(path).size > MAX_CONFIG_BYTES2) {
6353
+ throw new Error("The Docusaurus sidebar config is larger than 20 MB and could not be imported.");
6352
6354
  }
6353
- return null;
6355
+ return readFileSync4(path, "utf8");
6354
6356
  }
6355
- function convertNodes(nodes, parentSegments, context) {
6356
- return nodes.flatMap((node) => {
6357
- const converted = convertNode(node, parentSegments, context);
6358
- return converted ? [converted] : [];
6359
- });
6357
+ function braceTable(source) {
6358
+ const table = /* @__PURE__ */ new Map();
6359
+ try {
6360
+ const tree = ts4.createSourceFile("config.ts", source, ts4.ScriptTarget.Latest, false, ts4.ScriptKind.TS);
6361
+ const pending = [tree];
6362
+ while (pending.length > 0) {
6363
+ const node = pending.pop();
6364
+ if (ts4.isObjectLiteralExpression(node) || ts4.isBlock(node)) {
6365
+ const start = node.getStart(tree);
6366
+ const end = node.end - 1;
6367
+ if (source[start] === "{" && source[end] === "}") table.set(start, end);
6368
+ }
6369
+ ts4.forEachChild(node, (child) => {
6370
+ pending.push(child);
6371
+ });
6372
+ }
6373
+ } catch {
6374
+ }
6375
+ return table;
6360
6376
  }
6361
- function groupsFromConverted(converted) {
6362
- const loosePages = converted.filter((entry) => typeof entry === "string");
6363
- const groups = converted.filter((entry) => typeof entry !== "string");
6364
- return loosePages.length > 0 ? [{ group: "Overview", pages: loosePages }, ...groups] : groups;
6377
+ var lastBraceSource;
6378
+ var lastBraceTable = /* @__PURE__ */ new Map();
6379
+ function matchingObjectLiteral(source, start) {
6380
+ let index = start;
6381
+ while (index < source.length) {
6382
+ if (/\s/.test(source[index])) index++;
6383
+ else if (source.startsWith("//", index)) {
6384
+ const end2 = source.indexOf("\n", index);
6385
+ index = end2 < 0 ? source.length : end2 + 1;
6386
+ } else if (source.startsWith("/*", index)) {
6387
+ const end2 = source.indexOf("*/", index + 2);
6388
+ index = end2 < 0 ? source.length : end2 + 2;
6389
+ } else break;
6390
+ }
6391
+ if (source[index] !== "{") return null;
6392
+ if (lastBraceSource !== source) {
6393
+ lastBraceSource = source;
6394
+ lastBraceTable = braceTable(source);
6395
+ }
6396
+ const end = lastBraceTable.get(index);
6397
+ return end === void 0 ? null : source.slice(index, end + 1);
6365
6398
  }
6366
- function resolveVersionedNavigation(config, configDir, fernRoot, context) {
6367
- if (Array.isArray(config.navigation)) return config;
6368
- if (Array.isArray(config.products)) {
6369
- warnOnce2(context, "fern-products", "Nested Fern multi-product docs are not supported; only this docs.yml was imported.");
6370
- return config;
6371
- }
6372
- if (!Array.isArray(config.versions)) return config;
6373
- const versions = config.versions.flatMap((entry) => {
6374
- const version = objectValue4(entry);
6375
- return version ? [version] : [];
6376
- });
6377
- const chosen = versions.find((version) => version.default === true) ?? versions[0];
6378
- const skipped = versions.filter((version) => version !== chosen).map((version) => typeof version.version === "string" ? version.version : void 0).filter((name) => Boolean(name));
6379
- if (skipped.length > 0) {
6380
- warnOnce2(
6381
- context,
6382
- "fern-versions",
6383
- `Only the default Fern version was imported; other versions were skipped: ${skipped.join(", ")}.`
6384
- );
6385
- }
6386
- if (!chosen || typeof chosen.path !== "string") return config;
6387
- try {
6388
- const versionPath = resolveWithinRoot(configDir, chosen.path, context.repositoryRoot);
6389
- if (!existsSync4(versionPath) || !lstatSync4(versionPath).isFile()) {
6390
- warnOnce2(context, "fern-version-not-file", `Fern version file "${chosen.path}" is not a regular file and was skipped.`);
6391
- return config;
6399
+ function parseStaticSidebarModule(source) {
6400
+ const bindings = /* @__PURE__ */ new Map();
6401
+ const normalizedSource = replaceExternalFbContent(source);
6402
+ const parseFailures = [];
6403
+ const assignmentPatterns = [
6404
+ /\bmodule\.exports\s*=\s*/g,
6405
+ /\bexport\s+default\s*/g,
6406
+ /\b(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:\s*:\s*(?:(?!\b(?:const|let|var)\b)[^=;])+)?\s*=\s*/g
6407
+ ];
6408
+ const candidates = assignmentPatterns.flatMap((pattern) => [...normalizedSource.matchAll(pattern)]).sort((left, right) => (left.index ?? 0) - (right.index ?? 0));
6409
+ for (const candidate of candidates) {
6410
+ const literal = matchingObjectLiteral(normalizedSource, (candidate.index ?? 0) + candidate[0].length);
6411
+ if (!literal) continue;
6412
+ try {
6413
+ const substituted = [...bindings].reduce(
6414
+ (value, [name, binding]) => value.replace(
6415
+ new RegExp(`(:\\s*)${escapeRegExp(name)}(?![\\w$])`, "g"),
6416
+ (_match, prefix) => `${prefix}${JSON.stringify(binding)}`
6417
+ ),
6418
+ literal
6419
+ );
6420
+ const parsed = JSON5.parse(substituted);
6421
+ const object = objectValue4(parsed);
6422
+ if (!object) continue;
6423
+ const bindingName = candidate[0].match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/)?.[1];
6424
+ if (bindingName) {
6425
+ bindings.set(bindingName, object);
6426
+ continue;
6427
+ }
6428
+ return object;
6429
+ } catch (error) {
6430
+ const message = error instanceof Error ? error.message : String(error);
6431
+ const line = Number(message.match(/at (\d+):/)?.[1] ?? 0);
6432
+ const context = line > 0 ? literal.split("\n").slice(Math.max(0, line - 2), line + 1).join(" ") : "";
6433
+ parseFailures.push(`${message}${context ? ` near ${context}` : ""}`);
6392
6434
  }
6393
- const versionConfig = objectValue4(readBoundedYaml(versionPath));
6394
- if (!versionConfig) return config;
6395
- context.pathPrefix = relative4(fernRoot, dirname3(versionPath)).replace(/\\/g, "/");
6396
- return { ...config, navigation: versionConfig.navigation, tabs: versionConfig.tabs ?? config.tabs };
6397
- } catch (error) {
6398
- warnOnce2(
6399
- context,
6400
- "fern-version-read-failed",
6401
- `Fern version file "${chosen.path}" could not be read (${error instanceof Error ? error.message : String(error)}) and was skipped.`
6402
- );
6403
- return config;
6404
6435
  }
6436
+ const exportedBinding = normalizedSource.match(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\b/)?.[1] ?? normalizedSource.match(/\bmodule\.exports\s*=\s*([A-Za-z_$][\w$]*)\b/)?.[1];
6437
+ if (exportedBinding && bindings.has(exportedBinding)) return bindings.get(exportedBinding);
6438
+ throw new Error(`Sidebar config is executable or outside the supported data-only syntax.${parseFailures[0] ? ` ${parseFailures[0]}` : ""}`);
6405
6439
  }
6406
- function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallbackTabLabel, context) {
6407
- const config = resolveVersionedNavigation(rawConfig, configDir, fernRoot, context);
6408
- const tabsMeta = objectValue4(config.tabs) ?? {};
6409
- const navigation = config.navigation;
6410
- const isTabbedNavigation = Array.isArray(navigation) && navigation.length > 0 && navigation.every((entry) => typeof objectValue4(entry)?.tab === "string");
6411
- if (isTabbedNavigation) {
6412
- return navigation.flatMap((entry) => {
6413
- const id = String(entry.tab);
6414
- const meta = objectValue4(tabsMeta[id]) ?? {};
6415
- const label = typeof meta["display-name"] === "string" ? meta["display-name"] : titleCase3(id);
6416
- const tabSegment = segmentFor(meta, label, context);
6417
- const segments = [...routePrefix, ...tabSegment ? [tabSegment] : []];
6418
- const layout = Array.isArray(entry.layout) ? entry.layout : [];
6419
- const sectionsBefore = context.apiSections.length;
6420
- const groups = groupsFromConverted(convertNodes(layout, segments, context));
6421
- for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
6422
- context.apiSections[index].tabLabel ??= label;
6423
- }
6424
- if (groups.length === 0 && typeof meta.href === "string" && /^https?:\/\//i.test(meta.href)) {
6425
- return [{ tab: label, href: meta.href }];
6426
- }
6427
- if (groups.length === 0 && typeof meta.changelog === "string") {
6428
- const changelog = registerFolder({ folder: meta.changelog, title: label, "skip-slug": true }, segments, context);
6429
- if (changelog) {
6430
- const indexRoute = segments.join("/");
6431
- const descriptors = new Map(context.descriptors.map((descriptor) => [descriptor.navigationId, descriptor]));
6432
- const visiblePages = changelog.pages.filter((page) => typeof page === "string").filter((page) => !descriptors.get(page)?.hidden);
6433
- const index = visiblePages.find((page) => {
6434
- const descriptor = descriptors.get(page);
6435
- if (!descriptor) return false;
6436
- const stem = basename2(descriptor.sourcePath, extname5(descriptor.sourcePath));
6437
- return page === indexRoute || stem === "overview" || stem === "index";
6438
- });
6439
- const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
6440
- const date = (page) => descriptors.get(page)?.sourcePath.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
6441
- return date(right).localeCompare(date(left)) || left.localeCompare(right);
6442
- });
6443
- for (const entryId of entries) {
6444
- const descriptor = descriptors.get(entryId);
6445
- if (descriptor) descriptor.hidden = true;
6446
- }
6447
- changelog.pages = [index ?? indexRoute];
6448
- context.changelogIndexes.push({ route: indexRoute, entries });
6449
- const latest = index ?? firstLeafId(changelog.pages);
6450
- const bareRouteIndex = context.bareRouteRedirects.findIndex((redirect) => redirect.source === `/${indexRoute}`);
6451
- const indexDescriptor = index ? descriptors.get(index) : void 0;
6452
- const indexSource = indexDescriptor && lstatSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot)).size <= MAX_CONFIG_BYTES2 ? readFileSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot), "utf8") : "";
6453
- const indexFrontmatter = indexSource.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
6454
- const indexSlug = indexFrontmatter.match(/^slug:\s*["']?([^\s"']+)["']?\s*$/m)?.[1]?.replace(/^\/+|\/+$/g, "");
6455
- const hasBarePage = index === indexRoute || indexSlug === indexRoute;
6456
- if (bareRouteIndex >= 0 && hasBarePage) context.bareRouteRedirects.splice(bareRouteIndex, 1);
6457
- else if (latest && bareRouteIndex >= 0) context.bareRouteRedirects[bareRouteIndex].destination = `/${latest}`;
6458
- return [{ tab: label, groups: [changelog] }];
6459
- }
6440
+ function replaceExternalFbContent(source) {
6441
+ const marker = "...fbContent(";
6442
+ let output = "";
6443
+ let cursor = 0;
6444
+ let searchFrom = 0;
6445
+ while (true) {
6446
+ const start = source.indexOf(marker, searchFrom);
6447
+ if (start < 0) return `${output}${source.slice(cursor)}`.replace(/,\s*,/g, ",");
6448
+ const objectStart = source.indexOf("{", start + marker.length);
6449
+ if (objectStart < 0) return `${output}${source.slice(cursor)}`;
6450
+ const objectLiteral = matchingObjectLiteral(source, objectStart);
6451
+ if (!objectLiteral) {
6452
+ searchFrom = start + marker.length;
6453
+ continue;
6454
+ }
6455
+ const close = source.indexOf(")", objectStart + objectLiteral.length);
6456
+ if (close < 0) return `${output}${source.slice(cursor)}`;
6457
+ const externalMatch = /\bexternal\s*:\s*/g.exec(objectLiteral);
6458
+ let replacement = "";
6459
+ if (externalMatch) {
6460
+ const arrayStart = objectLiteral.indexOf("[", externalMatch.index + externalMatch[0].length);
6461
+ if (arrayStart >= 0) {
6462
+ const array = matchingArrayLiteral(objectLiteral, arrayStart);
6463
+ if (array) replacement = array.slice(1, -1);
6460
6464
  }
6461
- if (groups.length === 0) return [];
6462
- if (segments.length > 0) addBareRouteRedirect(segments, groups, context);
6463
- return [{
6464
- tab: label,
6465
- ...typeof meta.icon === "string" ? { icon: meta.icon } : {},
6466
- groups
6467
- }];
6468
- });
6469
- }
6470
- if (Array.isArray(navigation)) {
6471
- const sectionsBefore = context.apiSections.length;
6472
- const groups = groupsFromConverted(convertNodes(navigation, routePrefix, context));
6473
- for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
6474
- context.apiSections[index].tabLabel ??= fallbackTabLabel;
6475
6465
  }
6476
- if (groups.length > 0) {
6477
- if (routePrefix.length > 0) addBareRouteRedirect(routePrefix, groups, context);
6478
- return [{ tab: fallbackTabLabel, groups }];
6466
+ let replaceEnd = close + 1;
6467
+ if (!replacement) {
6468
+ const trailingComma = /\s*,/y;
6469
+ trailingComma.lastIndex = replaceEnd;
6470
+ const found = trailingComma.exec(source);
6471
+ if (found) replaceEnd += found[0].length;
6479
6472
  }
6473
+ output += `${source.slice(cursor, start)}${replacement}`;
6474
+ cursor = replaceEnd;
6475
+ searchFrom = replaceEnd;
6480
6476
  }
6481
- return [];
6482
6477
  }
6483
- function projectFernProducts(rawProducts, fernRoot, context) {
6484
- return rawProducts.flatMap((entry) => {
6485
- const product = objectValue4(entry);
6486
- const rawPath = typeof product?.path === "string" ? product.path.trim() : "";
6487
- if (!product || !rawPath) return [];
6488
- const label = typeof product["display-name"] === "string" && product["display-name"].trim() ? product["display-name"].trim() : "Product";
6489
- let productPath;
6490
- try {
6491
- productPath = resolveWithin(fernRoot, rawPath);
6492
- } catch {
6493
- warnOnce2(context, `fern-product-unsafe-${rawPath}`, `Fern product "${label}" path "${rawPath}" is unsafe and was skipped.`);
6494
- return [];
6478
+ function matchingArrayLiteral(source, start) {
6479
+ let depth = 0;
6480
+ let quote = "";
6481
+ let isEscaped = false;
6482
+ for (let index = start; index < source.length; index++) {
6483
+ const character = source[index];
6484
+ if (quote) {
6485
+ if (isEscaped) isEscaped = false;
6486
+ else if (character === "\\") isEscaped = true;
6487
+ else if (character === quote) quote = "";
6488
+ continue;
6495
6489
  }
6496
- if (!existsSync4(productPath) || !lstatSync4(productPath).isFile()) {
6497
- warnOnce2(context, `fern-product-missing-${rawPath}`, `Fern product "${label}" path "${rawPath}" does not exist and was skipped.`);
6498
- return [];
6490
+ if (source.startsWith("//", index)) {
6491
+ const end = source.indexOf("\n", index + 2);
6492
+ if (end < 0) return null;
6493
+ index = end;
6494
+ continue;
6499
6495
  }
6500
- let productConfig;
6501
- try {
6502
- productConfig = objectValue4(readBoundedYaml(productPath));
6503
- } catch (error) {
6504
- warnOnce2(
6505
- context,
6506
- `fern-product-read-failed-${rawPath}`,
6507
- `Fern product "${label}" could not be read (${error instanceof Error ? error.message : String(error)}) and was skipped.`
6508
- );
6509
- return [];
6496
+ if (source.startsWith("/*", index)) {
6497
+ const end = source.indexOf("*/", index + 2);
6498
+ if (end < 0) return null;
6499
+ index = end + 1;
6500
+ continue;
6510
6501
  }
6511
- if (!productConfig) return [];
6512
- const productDir = dirname3(productPath);
6513
- const routeSegment = segmentFor(product, label, context);
6514
- const priorPrefix = context.pathPrefix;
6515
- context.pathPrefix = relative4(fernRoot, productDir).replace(/\\/g, "/");
6516
- try {
6517
- return buildTabsFromConfig(
6518
- productConfig,
6519
- productDir,
6520
- fernRoot,
6521
- routeSegment ? [routeSegment] : [],
6522
- label,
6523
- context
6524
- );
6525
- } finally {
6526
- context.pathPrefix = priorPrefix;
6502
+ if (character === '"' || character === "'" || character === "`") {
6503
+ quote = character;
6504
+ continue;
6527
6505
  }
6528
- });
6529
- }
6530
- function projectFernNavigation(input) {
6531
- const context = {
6532
- fernRoot: input.fernRoot,
6533
- repositoryRoot: input.repositoryRoot ?? input.fernRoot,
6534
- descriptors: [],
6535
- seenNavigationIds: /* @__PURE__ */ new Set(),
6536
- apiSections: [],
6537
- changelogIndexes: [],
6538
- warnings: [],
6539
- warningKeys: /* @__PURE__ */ new Set(),
6540
- externalLinks: [],
6541
- bareRouteRedirects: [],
6542
- segmentAliases: /* @__PURE__ */ new Map(),
6543
- pathPrefix: "",
6544
- sourcePathToNavigationId: /* @__PURE__ */ new Map()
6545
- };
6546
- const config = input.config;
6547
- const hasProducts = Array.isArray(config.products) && config.products.length > 0 && !Array.isArray(config.navigation);
6548
- const tabs = hasProducts ? projectFernProducts(config.products, input.fernRoot, context) : buildTabsFromConfig(config, input.fernRoot, input.fernRoot, [], "Documentation", context);
6549
- if (tabs.length === 0) {
6550
- context.warnings.push({
6551
- code: "unsupported-config",
6552
- message: "Fern navigation could not be projected; generated navigation will be used."
6553
- });
6506
+ if (character === "[") depth++;
6507
+ if (character === "]" && --depth === 0) return source.slice(start, index + 1);
6554
6508
  }
6555
- const navbarLinks = Array.isArray(config["navbar-links"]) ? config["navbar-links"].flatMap((entry) => {
6556
- const link = objectValue4(entry);
6557
- const href = typeof link?.url === "string" ? link.url : typeof link?.href === "string" ? link.href : typeof link?.value === "string" ? link.value : void 0;
6558
- const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
6559
- return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
6560
- }) : [];
6561
- for (const link of context.externalLinks) {
6562
- if (!navbarLinks.some((existing) => existing.label === link.label && existing.href === link.href)) {
6563
- navbarLinks.push(link);
6509
+ return null;
6510
+ }
6511
+ function configuredSidebarPath(repositoryRoot) {
6512
+ for (const filename of ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"]) {
6513
+ const path = resolveWithin(repositoryRoot, filename);
6514
+ if (!existsSync4(path)) continue;
6515
+ const source = readBoundedText(path);
6516
+ for (const docs of source.matchAll(/\bdocs\s*:\s*/g)) {
6517
+ const object = matchingObjectLiteral(source, (docs.index ?? 0) + docs[0].length);
6518
+ const candidate2 = object && staticStringField(object, "sidebarPath")?.replace(/^\.\//, "");
6519
+ if (candidate2) {
6520
+ const resolved2 = withinRealRoot(resolveWithin(repositoryRoot, candidate2), repositoryRoot);
6521
+ if (existsSync4(resolved2) && lstatSync4(resolved2).isFile()) return candidate2;
6522
+ }
6564
6523
  }
6524
+ const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
6525
+ if (!match) continue;
6526
+ const candidate = match[2].replace(/^\.\//, "");
6527
+ const resolved = withinRealRoot(resolveWithin(repositoryRoot, candidate), repositoryRoot);
6528
+ if (existsSync4(resolved) && lstatSync4(resolved).isFile()) return candidate;
6565
6529
  }
6566
- const announcement = objectValue4(config.announcement);
6567
- const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
6568
- const bannerContent = announcementMarkdown(announcementMessage);
6569
- const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
6570
- const redirect = objectValue4(value);
6571
- if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
6572
- const rawSource = redirect.source.trim();
6573
- const rawDestination = redirect.destination.trim();
6574
- if (!isRedirectPathSafe(rawSource, rawDestination)) return [];
6575
- const translated = translateRedirectWildcards(rawSource, rawDestination);
6576
- if (!translated) {
6577
- warnOnce2(
6578
- context,
6579
- `fern-redirect-wildcard-${rawSource}`,
6580
- `Redirect from ${rawSource} uses a wildcard Next.js cannot express and was dropped.`
6581
- );
6582
- return [];
6530
+ return null;
6531
+ }
6532
+ function readDocusaurusSidebars(repositoryRoot, versionedSidebarPath) {
6533
+ const configured = versionedSidebarPath ?? configuredSidebarPath(repositoryRoot);
6534
+ const sourcePath = [configured, ...versionedSidebarPath ? [] : SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => {
6535
+ const path = withinRealRoot(resolveWithin(repositoryRoot, candidate), repositoryRoot);
6536
+ return existsSync4(path) && lstatSync4(path).isFile();
6537
+ });
6538
+ if (!sourcePath) return null;
6539
+ const absolutePath = withinRealRoot(resolveWithin(repositoryRoot, sourcePath), repositoryRoot);
6540
+ const source = readBoundedText(absolutePath);
6541
+ const parsed = extname5(sourcePath).toLowerCase() === ".json" ? objectValue4(JSON5.parse(source)) : parseStaticSidebarModule(source);
6542
+ if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
6543
+ return { config: parsed, sourcePath };
6544
+ }
6545
+ function namedExportObjectLiteralText(source, name) {
6546
+ const normalizedSource = replaceExternalFbContent(source);
6547
+ const match = new RegExp(`\\bexport\\s+(?:const|let|var)\\s+${escapeRegExp(name)}(?![\\w$])(?:\\s*:\\s*(?:(?!\\b(?:const|let|var)\\b)[^=;])+)?\\s*=\\s*`).exec(normalizedSource);
6548
+ if (!match) return null;
6549
+ return matchingObjectLiteral(normalizedSource, match.index + match[0].length);
6550
+ }
6551
+ function redirectEntriesFromObjectLiteralText(objectLiteral, warnings) {
6552
+ const match = /\bredirects\s*:\s*/.exec(objectLiteral);
6553
+ if (!match) return [];
6554
+ const arrayStart = objectLiteral.indexOf("[", match.index + match[0].length);
6555
+ if (arrayStart < 0) return [];
6556
+ const arrayLiteral = matchingArrayLiteral(objectLiteral, arrayStart);
6557
+ if (!arrayLiteral) return [];
6558
+ let parsed;
6559
+ try {
6560
+ parsed = JSON5.parse(arrayLiteral);
6561
+ } catch {
6562
+ return [];
6563
+ }
6564
+ if (!Array.isArray(parsed)) return [];
6565
+ const entries = [];
6566
+ for (const raw of parsed) {
6567
+ const entry = objectValue4(raw);
6568
+ if (!entry || typeof entry.to !== "string") continue;
6569
+ const froms = Array.isArray(entry.from) ? entry.from : [entry.from];
6570
+ for (const from of froms) {
6571
+ if (typeof from !== "string") continue;
6572
+ if (!isRedirectPathSafe(from, entry.to)) continue;
6573
+ const translated = translateRedirectWildcards(from, entry.to);
6574
+ if (!translated) {
6575
+ warnings.push({ code: "unsupported-config", message: `Redirect from ${from} uses a wildcard Next.js cannot express and was dropped.` });
6576
+ continue;
6577
+ }
6578
+ entries.push(translated);
6583
6579
  }
6584
- return [{
6585
- source: translated.source,
6586
- destination: translated.destination,
6587
- ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
6588
- }];
6580
+ }
6581
+ return entries;
6582
+ }
6583
+ function readDocusaurusRedirects(repositoryRoot, warnings) {
6584
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
6585
+ if (!configPath) return [];
6586
+ const configSource = readBoundedText(configPath);
6587
+ const pluginMatch = /['"]@docusaurus\/plugin-client-redirects['"]\s*,\s*/.exec(configSource);
6588
+ if (!pluginMatch) return [];
6589
+ const afterPlugin = configSource.slice(pluginMatch.index + pluginMatch[0].length);
6590
+ const warnAboutCreateRedirects = (source) => {
6591
+ if (!/\bcreateRedirects\s*[:(]/.test(source)) return;
6592
+ warnings.push({
6593
+ code: "unsupported-config",
6594
+ message: "The redirects plugin's createRedirects function computes redirects dynamically and cannot be evaluated during migration; add its redirects manually."
6595
+ });
6596
+ };
6597
+ warnAboutCreateRedirects(configSource);
6598
+ const inlineLiteral = matchingObjectLiteral(configSource, pluginMatch.index + pluginMatch[0].length);
6599
+ if (inlineLiteral) return redirectEntriesFromObjectLiteralText(inlineLiteral, warnings);
6600
+ const identifier = afterPlugin.match(/^([A-Za-z_$][\w$]*)/)?.[1];
6601
+ if (!identifier) return [];
6602
+ const importMatch = new RegExp(`import\\s*\\{[^}]*(?<![\\w$])${escapeRegExp(identifier)}(?![\\w$])[^}]*\\}\\s*from\\s*(['"])([^'"]+)\\1`).exec(configSource) ?? new RegExp(`import\\s+${escapeRegExp(identifier)}\\s+from\\s*(['"])([^'"]+)\\1`).exec(configSource);
6603
+ const modulePath = importMatch?.[2];
6604
+ if (!modulePath || !modulePath.startsWith(".")) return [];
6605
+ let resolvedPath;
6606
+ try {
6607
+ resolvedPath = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"].map((extension) => withinRealRoot(resolveWithin(repositoryRoot, `${modulePath.replace(/^\.\//, "")}${extension}`), repositoryRoot)).find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile());
6608
+ } catch {
6609
+ return [];
6610
+ }
6611
+ if (!resolvedPath) return [];
6612
+ const moduleSource = readBoundedText(resolvedPath);
6613
+ warnAboutCreateRedirects(moduleSource);
6614
+ const literal = namedExportObjectLiteralText(moduleSource, identifier);
6615
+ return literal ? redirectEntriesFromObjectLiteralText(literal, warnings) : [];
6616
+ }
6617
+ function readCategoryMetadata(contentRoot, directory) {
6618
+ for (const filename of CATEGORY_FILENAMES) {
6619
+ const path = resolveWithin(contentRoot, posix3.join(directory, filename));
6620
+ try {
6621
+ withinRealRoot(path, contentRoot);
6622
+ } catch {
6623
+ continue;
6624
+ }
6625
+ if (!existsSync4(path) || !lstatSync4(path).isFile()) continue;
6626
+ try {
6627
+ const raw = readBoundedText(path);
6628
+ const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml3(raw);
6629
+ const object = objectValue4(parsed);
6630
+ if (!object) return {};
6631
+ return {
6632
+ ...typeof object.label === "string" ? { label: object.label } : {},
6633
+ ...typeof object.position === "number" ? { position: object.position } : {},
6634
+ ..."link" in object ? { link: object.link } : {}
6635
+ };
6636
+ } catch {
6637
+ return {};
6638
+ }
6639
+ }
6640
+ return {};
6641
+ }
6642
+ function descriptorSort(left, right) {
6643
+ const leftPosition = left.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
6644
+ const rightPosition = right.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
6645
+ if (leftPosition !== rightPosition) return leftPosition - rightPosition;
6646
+ return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
6647
+ }
6648
+ function registerDoc(docId, context, sourceDirectory) {
6649
+ const key = normalizeDocId(docId);
6650
+ const scopedKey = sourceDirectory && !docId.startsWith("/") ? normalizeDocId(posix3.join(sourceDirectory, key)) : void 0;
6651
+ const descriptor = (scopedKey ? context.descriptorByDocId.get(scopedKey) : void 0) ?? context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
6652
+ if (!descriptor) {
6653
+ context.warnings.push({
6654
+ code: "missing-page",
6655
+ message: "A Docusaurus sidebar document did not resolve to an imported page.",
6656
+ source: docId
6657
+ });
6658
+ return null;
6659
+ }
6660
+ context.referencedNavigationIds.add(descriptor.navigationId);
6661
+ return descriptor.navigationId;
6662
+ }
6663
+ function generatedIndexPage(label, linkValue, context, sourceDirectory) {
6664
+ const link = objectValue4(linkValue);
6665
+ if (!link) return null;
6666
+ if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context, sourceDirectory);
6667
+ if (link.type !== "generated-index") return null;
6668
+ const fallback = `category/${slugifySegment(label)}`;
6669
+ const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
6670
+ const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix3.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
6671
+ if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
6672
+ const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
6673
+ context.generatedPages.push({
6674
+ id: storageIdForRoute(navigationId),
6675
+ navigationId,
6676
+ title: typeof link.title === "string" ? link.title : label,
6677
+ description,
6678
+ keywords: Array.isArray(link.keywords) ? link.keywords.filter((value) => typeof value === "string") : [],
6679
+ body: description,
6680
+ source: `${context.sourceUrl}#${context.sidebarSource}`
6681
+ });
6682
+ context.generatedIds.add(navigationId);
6683
+ }
6684
+ context.referencedNavigationIds.add(navigationId);
6685
+ return navigationId;
6686
+ }
6687
+ var DOCUSAURUS_HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
6688
+ function findDocusaurusConfigPath(repositoryRoot) {
6689
+ return ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].map((filename) => resolveWithin(repositoryRoot, filename)).find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile()) ?? null;
6690
+ }
6691
+ function readDocusaurusThemeColor(repositoryRoot) {
6692
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
6693
+ if (!configPath) return void 0;
6694
+ const configSource = readBoundedText(configPath);
6695
+ const cssRelativePath = configSource.match(/\bcustomCss\s*:\s*(?:\[\s*)?(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/)?.[2];
6696
+ if (!cssRelativePath) return void 0;
6697
+ let cssPath;
6698
+ try {
6699
+ cssPath = withinRealRoot(resolveWithin(repositoryRoot, cssRelativePath.replace(/^\.\//, "")), repositoryRoot);
6700
+ } catch {
6701
+ return void 0;
6702
+ }
6703
+ if (!existsSync4(cssPath) || !lstatSync4(cssPath).isFile()) return void 0;
6704
+ const css = readBoundedText(cssPath);
6705
+ const pick = (block) => {
6706
+ const value = block?.match(/--ifm-color-primary\s*:\s*([^;]+);/)?.[1]?.trim();
6707
+ return value && DOCUSAURUS_HEX_COLOR.test(value) ? value : void 0;
6708
+ };
6709
+ const lightMode = pick(css.match(/:root\s*\{([^}]*)\}/)?.[1]);
6710
+ const darkMode = pick(css.match(/\[data-theme=(['"])dark\1\]\s*\{([^}]*)\}/)?.[2]);
6711
+ const colors = {};
6712
+ if (darkMode) colors.light = darkMode;
6713
+ if (lightMode) colors.dark = lightMode;
6714
+ return Object.keys(colors).length > 0 ? colors : void 0;
6715
+ }
6716
+ function propertyStart(source, name) {
6717
+ const pattern = new RegExp(`["']?${name}["']?\\s*:\\s*`, "g");
6718
+ for (let match = pattern.exec(source); match; match = pattern.exec(source)) {
6719
+ let before = match.index - 1;
6720
+ while (before >= 0 && (source[before] === " " || source[before] === " ")) before--;
6721
+ if (before < 0 || "\r\n,{".includes(source[before])) return match.index + match[0].length;
6722
+ }
6723
+ return -1;
6724
+ }
6725
+ function staticStringBindings(source) {
6726
+ const bindings = /* @__PURE__ */ new Map();
6727
+ for (const match of source.matchAll(/\bconst\s+([A-Z][A-Z0-9_]*)\s*=\s*((?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))/g)) {
6728
+ try {
6729
+ const value = JSON5.parse(match[2]);
6730
+ if (typeof value === "string") bindings.set(match[1], value);
6731
+ } catch {
6732
+ }
6733
+ }
6734
+ return bindings;
6735
+ }
6736
+ function staticArrayField(source, name) {
6737
+ const start = propertyStart(source, name);
6738
+ return start < 0 ? null : matchingArrayLiteral(source, start);
6739
+ }
6740
+ function staticObjectEntries(array) {
6741
+ if (!array) return [];
6742
+ const entries = [];
6743
+ let afterDelimiter = 1;
6744
+ let quote = "";
6745
+ let lineComment = false;
6746
+ let blockComment = false;
6747
+ for (let index = 1; index < array.length - 1; index++) {
6748
+ const char = array[index];
6749
+ const next = array[index + 1];
6750
+ if (lineComment) {
6751
+ if (char === "\n") lineComment = false;
6752
+ continue;
6753
+ }
6754
+ if (blockComment) {
6755
+ if (char === "*" && next === "/") {
6756
+ blockComment = false;
6757
+ index++;
6758
+ }
6759
+ continue;
6760
+ }
6761
+ if (quote) {
6762
+ if (char === "\\") {
6763
+ index++;
6764
+ continue;
6765
+ }
6766
+ if (char === quote) quote = "";
6767
+ continue;
6768
+ }
6769
+ if (char === "/" && next === "/") {
6770
+ lineComment = true;
6771
+ index++;
6772
+ continue;
6773
+ }
6774
+ if (char === "/" && next === "*") {
6775
+ blockComment = true;
6776
+ index++;
6777
+ continue;
6778
+ }
6779
+ if (char === '"' || char === "'" || char === "`") {
6780
+ quote = char;
6781
+ continue;
6782
+ }
6783
+ if (char === ",") {
6784
+ afterDelimiter = index + 1;
6785
+ continue;
6786
+ }
6787
+ if (char !== "{") continue;
6788
+ const literal = matchingObjectLiteral(array, index);
6789
+ if (!literal) continue;
6790
+ const lead = array.slice(afterDelimiter, index).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim();
6791
+ if (!lead) entries.push(literal);
6792
+ index += literal.length - 1;
6793
+ }
6794
+ return entries;
6795
+ }
6796
+ function staticStringField(source, name, bindings) {
6797
+ const start = propertyStart(source, name);
6798
+ if (start < 0) return void 0;
6799
+ const quoted = source.slice(start).match(/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/);
6800
+ if (!quoted) {
6801
+ const binding = source.slice(start).match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
6802
+ return binding ? bindings?.get(binding) : void 0;
6803
+ }
6804
+ try {
6805
+ const parsed = JSON5.parse(quoted[1]);
6806
+ return typeof parsed === "string" ? parsed : void 0;
6807
+ } catch {
6808
+ return void 0;
6809
+ }
6810
+ }
6811
+ function readDocusaurusSiteSettings(repositoryRoot) {
6812
+ const empty2 = { docsRouteBasePath: "docs", defaultColorMode: "light", navbarLinks: [], footerLinks: [] };
6813
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
6814
+ if (!configPath) return empty2;
6815
+ const source = readBoundedText(configPath);
6816
+ const bindings = staticStringBindings(source);
6817
+ const presets = staticArrayField(source, "presets");
6818
+ const classicDocs = presets && [...presets.matchAll(/["']?docs["']?\s*:\s*/g)].map((match) => matchingObjectLiteral(presets, (match.index ?? 0) + match[0].length)).find((object) => Boolean(object));
6819
+ const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
6820
+ const themeStart = propertyStart(source, "themeConfig");
6821
+ const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
6822
+ const colorModeStart = theme ? propertyStart(theme, "colorMode") : -1;
6823
+ const colorMode = colorModeStart < 0 ? null : matchingObjectLiteral(theme, colorModeStart);
6824
+ const configuredColorMode = colorMode ? staticStringField(colorMode, "defaultMode") : void 0;
6825
+ const defaultColorMode = configuredColorMode === "dark" ? "dark" : "light";
6826
+ const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
6827
+ const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
6828
+ const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
6829
+ const logo = logoStart < 0 ? null : matchingObjectLiteral(navbar, logoStart);
6830
+ const footerStart = theme ? propertyStart(theme, "footer") : -1;
6831
+ const footer = footerStart < 0 ? null : matchingObjectLiteral(theme, footerStart);
6832
+ const navbarLinks = navbar ? staticObjectEntries(staticArrayField(navbar, "items")).flatMap((item) => {
6833
+ if (propertyStart(item, "items") >= 0 || propertyStart(item, "dropdownItemsAfter") >= 0) return [];
6834
+ const docsPluginId = staticStringField(item, "docsPluginId", bindings);
6835
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
6836
+ const docId = staticStringField(item, "docId", bindings);
6837
+ const sidebarId = staticStringField(item, "sidebarId", bindings);
6838
+ const label = staticStringField(item, "label", bindings) ?? staticStringField(item, "aria-label", bindings);
6839
+ return label && (href || docId || sidebarId || docsPluginId) ? [{
6840
+ label,
6841
+ ...href ? { href } : {},
6842
+ ...docId ? { docId } : {},
6843
+ ...sidebarId ? { sidebarId } : {},
6844
+ ...docsPluginId ? { docsPluginId } : {}
6845
+ }] : [];
6589
6846
  }) : [];
6590
- const authoredSources = new Set(redirects.map((redirect) => redirect.source));
6591
- const knownIds = new Set(context.descriptors.map((descriptor) => descriptor.navigationId));
6592
- const keptNavigationId = (descriptor) => context.sourcePathToNavigationId.get(descriptor.sourcePath) ?? descriptor.navigationId;
6593
- const segmentAliasRedirects = context.segmentAliases.size > 0 ? context.descriptors.flatMap((descriptor) => {
6594
- const segments = descriptor.navigationId.split("/");
6595
- let changed = false;
6596
- const aliased = segments.map((segment) => {
6597
- const literal = context.segmentAliases.get(segment);
6598
- if (!literal) return segment;
6599
- changed = true;
6600
- return literal;
6847
+ const footerLinks = footer ? staticObjectEntries(staticArrayField(footer, "links")).flatMap((column) => {
6848
+ const heading = staticStringField(column, "title", bindings);
6849
+ if (!heading) return [];
6850
+ const items = staticObjectEntries(staticArrayField(column, "items")).flatMap((item) => {
6851
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
6852
+ const label = staticStringField(item, "label", bindings);
6853
+ return label && href ? [{ label, href }] : [];
6601
6854
  });
6602
- if (!changed) return [];
6603
- const source = `/${aliased.join("/")}`;
6604
- const destination = `/${keptNavigationId(descriptor)}`;
6605
- if (source === destination || knownIds.has(aliased.join("/"))) return [];
6606
- return [{ source, destination }];
6855
+ return items.length > 0 ? [{ heading, items }] : [];
6607
6856
  }) : [];
6608
- const duplicateNavRedirects = context.descriptors.flatMap((descriptor) => {
6609
- const kept = keptNavigationId(descriptor);
6610
- if (kept === descriptor.navigationId) return [];
6611
- return [{ source: `/${descriptor.navigationId}`, destination: `/${kept}` }];
6857
+ const copyright = footer && propertyStart(footer, "copyright") >= 0 ? staticStringField(footer, "copyright") ?? footer.slice(propertyStart(footer, "copyright")).match(/^`([^`]+)`/)?.[1]?.replace(/\$\{new Date\(\)\.getFullYear\(\)\}/g, "{year}") : void 0;
6858
+ const announcementStart = theme ? propertyStart(theme, "announcementBar") : -1;
6859
+ const announcement = announcementStart < 0 ? null : matchingObjectLiteral(theme, announcementStart);
6860
+ const announcementContent = announcement && staticStringField(announcement, "content") || announcement?.slice(propertyStart(announcement, "content")).match(/^`([^`]+)`/)?.[1];
6861
+ const bannerContent = announcementContent ? announcementMarkdown(announcementContent) : "";
6862
+ return {
6863
+ ...bannerContent ? { banner: { content: bannerContent, dismissible: !/isCloseable\s*:\s*false/.test(announcement), ...staticStringField(announcement, "id") ? { id: staticStringField(announcement, "id") } : {} } } : {},
6864
+ name: staticStringField(source, "title"),
6865
+ description: staticStringField(source, "tagline"),
6866
+ ...navbar && staticStringField(navbar, "title") ? { navbarTitle: staticStringField(navbar, "title") } : {},
6867
+ defaultColorMode,
6868
+ docsRouteBasePath,
6869
+ ...logo && staticStringField(logo, "src") ? {
6870
+ logo: {
6871
+ light: staticStringField(logo, "src"),
6872
+ ...staticStringField(logo, "srcDark") ? { dark: staticStringField(logo, "srcDark") } : {},
6873
+ showTitle: Boolean(navbar && staticStringField(navbar, "title"))
6874
+ }
6875
+ } : {},
6876
+ ...staticStringField(source, "favicon") ? { favicon: staticStringField(source, "favicon") } : {},
6877
+ navbarLinks,
6878
+ footerLinks,
6879
+ ...copyright ? { copyright } : {}
6880
+ };
6881
+ }
6882
+ function autogeneratedItems(dirName, context) {
6883
+ const normalizedDir = dirName === "." ? "" : dirName.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
6884
+ const inDirectory = context.descriptors.filter((descriptor) => {
6885
+ const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
6886
+ return !normalizedDir || source === normalizedDir || source.startsWith(`${normalizedDir}/`);
6887
+ });
6888
+ const directPages = [];
6889
+ const childDirectories = /* @__PURE__ */ new Set();
6890
+ for (const descriptor of inDirectory) {
6891
+ const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
6892
+ const relativePath = normalizedDir ? posix3.relative(normalizedDir, source) : source;
6893
+ const segments = relativePath.split("/").filter(Boolean);
6894
+ if (segments.length <= 1) directPages.push(descriptor);
6895
+ else childDirectories.add(segments[0]);
6896
+ }
6897
+ const pages = directPages.map((descriptor) => ({
6898
+ // An index document is the category landing page in Docusaurus. Thally
6899
+ // renders it as a child link, so put it before the category's other docs.
6900
+ position: descriptor.sidebarPosition ?? (/(?:^|\/)(?:index|readme)\.mdx?$/i.test(descriptor.sourcePath) ? -1 : Number.MAX_SAFE_INTEGER),
6901
+ // Siblings sort by file or directory name, like the category entries below.
6902
+ sortKey: posix3.basename(descriptor.sourcePath),
6903
+ item: descriptor.navigationId
6904
+ }));
6905
+ const groups = [...childDirectories].map((segment) => {
6906
+ const directory = normalizedDir ? posix3.join(normalizedDir, segment) : segment;
6907
+ const metadata = readCategoryMetadata(context.contentRoot, directory);
6908
+ const pages2 = autogeneratedItems(directory, context);
6909
+ if (pages2.length === 1 && typeof pages2[0] === "string" && !metadata.label && metadata.position === void 0 && !metadata.link) {
6910
+ const onlyPage = context.descriptors.find((descriptor) => descriptor.navigationId === pages2[0]);
6911
+ if (onlyPage && /(?:^|\/)(?:index|readme)\.mdx?$/i.test(onlyPage.sourcePath)) {
6912
+ return {
6913
+ position: onlyPage.sidebarPosition ?? Number.MAX_SAFE_INTEGER,
6914
+ sortKey: segment,
6915
+ item: pages2[0]
6916
+ };
6917
+ }
6918
+ }
6919
+ const landing = generatedIndexPage(metadata.label ?? titleCase3(segment), metadata.link, context, directory);
6920
+ if (landing && !pages2.includes(landing)) pages2.unshift(landing);
6921
+ return {
6922
+ position: metadata.position ?? Number.MAX_SAFE_INTEGER,
6923
+ sortKey: segment,
6924
+ item: {
6925
+ group: metadata.label ?? titleCase3(segment),
6926
+ pages: pages2
6927
+ }
6928
+ };
6929
+ });
6930
+ return [...pages, ...groups].sort((left, right) => left.position - right.position || left.sortKey.localeCompare(right.sortKey, void 0, { numeric: true })).map(({ item }) => {
6931
+ if (typeof item === "string") context.referencedNavigationIds.add(item);
6932
+ return item;
6933
+ });
6934
+ }
6935
+ function convertItems(value, context) {
6936
+ if (typeof value === "string") {
6937
+ const page = registerDoc(value, context);
6938
+ return page ? [page] : [];
6939
+ }
6940
+ if (Array.isArray(value)) return value.flatMap((entry) => convertItems(entry, context));
6941
+ const object = objectValue4(value);
6942
+ if (!object) return [];
6943
+ if (object.type === "doc" || object.type === "ref") {
6944
+ const page = typeof object.id === "string" ? registerDoc(object.id, context) : null;
6945
+ return page ? [page] : [];
6946
+ }
6947
+ if (object.type === "autogenerated") {
6948
+ return autogeneratedItems(typeof object.dirName === "string" ? object.dirName : ".", context);
6949
+ }
6950
+ if (object.type === "category") {
6951
+ const label = typeof object.label === "string" ? object.label : "Documentation";
6952
+ const pages = convertItems(object.items, context);
6953
+ const landing = generatedIndexPage(label, object.link, context);
6954
+ if (landing && !pages.includes(landing)) pages.unshift(landing);
6955
+ return pages.length > 0 ? [{ group: label, pages }] : [];
6956
+ }
6957
+ if (object.type === "link" || object.type === "html") return [];
6958
+ return Object.entries(object).flatMap(([label, items]) => {
6959
+ const pages = convertItems(items, context);
6960
+ return pages.length > 0 ? [{ group: label, pages }] : [];
6961
+ });
6962
+ }
6963
+ function projectDocusaurusNavigation(input) {
6964
+ const warnings = [];
6965
+ const descriptorByDocId = new Map(input.descriptors.map((page) => [page.docId, page]));
6966
+ const context = {
6967
+ contentRoot: input.contentRoot,
6968
+ descriptors: input.descriptors,
6969
+ descriptorByDocId,
6970
+ generatedPages: [],
6971
+ generatedIds: /* @__PURE__ */ new Set(),
6972
+ referencedNavigationIds: /* @__PURE__ */ new Set(),
6973
+ sourceUrl: input.sourceUrl,
6974
+ routePrefix: input.routePrefix ?? "",
6975
+ sidebarSource: input.sidebars?.sourcePath ?? "autogenerated sidebar",
6976
+ warnings
6977
+ };
6978
+ const sidebarEntries = input.sidebars ? Object.entries(input.sidebars.config) : [["docs", [
6979
+ { type: "autogenerated", dirName: "." }
6980
+ ]]];
6981
+ const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
6982
+ const converted = convertItems(value, context);
6983
+ if (converted.length === 0) return [];
6984
+ return [{
6985
+ tab: sidebarEntries.length === 1 ? "Documentation" : titleCase3(sidebarId),
6986
+ // Docusaurus categories are disclosures, including at the sidebar
6987
+ // root. Keeping them as Thally root nodes preserves their collapsed
6988
+ // state and the authored interleaving of documents and categories.
6989
+ pages: converted
6990
+ }];
6612
6991
  });
6613
- const allRedirects = [
6614
- ...redirects,
6615
- ...context.bareRouteRedirects.filter((redirect) => !authoredSources.has(redirect.source)),
6616
- ...segmentAliasRedirects.filter((redirect) => !authoredSources.has(redirect.source)),
6617
- ...duplicateNavRedirects.filter((redirect) => !authoredSources.has(redirect.source))
6618
- ];
6619
- const claimedTabLabels = /* @__PURE__ */ new Set();
6620
- const disambiguatedApiSections = context.apiSections.map((section) => {
6621
- if (!section.tabLabel || !claimedTabLabels.has(section.tabLabel)) {
6622
- if (section.tabLabel) claimedTabLabels.add(section.tabLabel);
6623
- return section;
6624
- }
6625
- const distinguishingSegment = section.routeSegments.at(-1);
6626
- let tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase3(distinguishingSegment)}` : section.tabLabel;
6627
- if (!tabLabel || tabLabel === `${section.tabLabel}: ${section.tabLabel}` || claimedTabLabels.has(tabLabel)) {
6628
- let ordinal = 2;
6629
- let candidate = `${section.tabLabel} (${ordinal})`;
6630
- while (claimedTabLabels.has(candidate)) {
6631
- ordinal += 1;
6632
- candidate = `${section.tabLabel} (${ordinal})`;
6633
- }
6634
- tabLabel = candidate;
6635
- }
6636
- claimedTabLabels.add(tabLabel);
6637
- return { ...section, tabLabel };
6992
+ const unlisted = new Set(input.descriptors.filter((page) => page.unlisted).map((page) => page.navigationId));
6993
+ const withoutUnlisted = (items) => items.flatMap((item) => {
6994
+ if (typeof item === "string") return unlisted.has(item) ? [] : [item];
6995
+ const pages = withoutUnlisted(item.pages);
6996
+ return pages.length > 0 ? [{ ...item, pages }] : [];
6638
6997
  });
6998
+ if (unlisted.size > 0) {
6999
+ for (const tab of tabs) tab.pages = withoutUnlisted(tab.pages);
7000
+ }
7001
+ const unreferenced = input.descriptors.filter((page) => !unlisted.has(page.navigationId) && !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
7002
+ if (unreferenced.length > 0) {
7003
+ if (tabs.length === 0) tabs.push({ tab: "Documentation", pages: [] });
7004
+ tabs[0].pages?.push({ group: "Additional", pages: unreferenced });
7005
+ }
6639
7006
  return {
6640
- docsConfig: {
6641
- tabs,
6642
- ...bannerContent ? { banner: { content: bannerContent, dismissible: true } } : {},
6643
- ...navbarLinks.length > 0 ? { navbar: { links: navbarLinks } } : {},
6644
- ...allRedirects.length > 0 ? { redirects: allRedirects } : {}
6645
- },
6646
- descriptors: context.descriptors,
6647
- warnings: context.warnings,
6648
- apiSections: disambiguatedApiSections,
6649
- changelogIndexes: context.changelogIndexes
7007
+ docsConfig: { tabs },
7008
+ generatedPages: context.generatedPages,
7009
+ referencedNavigationIds: context.referencedNavigationIds,
7010
+ warnings
7011
+ };
7012
+ }
7013
+ function readDocusaurusPresentation(repositoryRoot) {
7014
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
7015
+ if (!configPath) return void 0;
7016
+ const config = readBoundedText(configPath);
7017
+ const relative7 = config.match(/\bcustomCss\s*:\s*(?:\[\s*)?(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/)?.[2];
7018
+ if (!relative7) return void 0;
7019
+ let css;
7020
+ try {
7021
+ const path = withinRealRoot(resolveWithin(repositoryRoot, relative7), repositoryRoot);
7022
+ if (!lstatSync4(path).isFile()) return void 0;
7023
+ css = readBoundedText(path);
7024
+ if (/\btheme\(/.test(css) || /\.s[ac]ss$/.test(relative7)) {
7025
+ const candidateDirectory = resolveWithin(repositoryRoot, "build/assets/css");
7026
+ if (existsSync4(candidateDirectory)) {
7027
+ const directory = withinRealRoot(candidateDirectory, repositoryRoot);
7028
+ const compiled = readdirSync2(directory).filter((name) => /^styles[.\w-]*\.css$/.test(name)).sort().slice(0, 8);
7029
+ let totalBytes = 0;
7030
+ css = compiled.map((name) => {
7031
+ const path2 = withinRealRoot(resolveWithin(directory, name), repositoryRoot);
7032
+ const stat = lstatSync4(path2);
7033
+ if (!stat.isFile()) return "";
7034
+ totalBytes += stat.size;
7035
+ if (totalBytes > MAX_CONFIG_BYTES2) throw new Error("Compiled theme exceeds the config size limit.");
7036
+ return readBoundedText(path2);
7037
+ }).join("\n") || css;
7038
+ }
7039
+ }
7040
+ } catch {
7041
+ return void 0;
7042
+ }
7043
+ const modes = { light: /* @__PURE__ */ new Map(), dark: /* @__PURE__ */ new Map() };
7044
+ const banners = { light: /* @__PURE__ */ new Map(), dark: /* @__PURE__ */ new Map() };
7045
+ try {
7046
+ postcss3.parse(css).walkRules((rule) => {
7047
+ if (rule.parent?.type !== "root") return;
7048
+ for (const selector of rule.selector.split(",")) {
7049
+ const key = selector.trim();
7050
+ const target = key === ":root" || key === "html" || /^(?:html)?\[data-theme=['"]?light['"]?\]$/.test(key) ? modes.light : /^(?:html)?\[data-theme=['"]?dark['"]?\]$/.test(key) || key === ".dark" ? modes.dark : void 0;
7051
+ if (target) rule.walkDecls((decl) => {
7052
+ if (decl.prop.startsWith("--ifm-")) target.set(decl.prop, decl.value);
7053
+ });
7054
+ const banner = key === ".theme-announcement-bar" ? banners.light : /^(?:html)?\[data-theme=['"]?dark['"]?\] \.theme-announcement-bar$/.test(key) ? banners.dark : void 0;
7055
+ if (banner) rule.walkDecls((decl) => {
7056
+ banner.set(decl.prop, decl.value);
7057
+ });
7058
+ }
7059
+ });
7060
+ } catch {
7061
+ return void 0;
7062
+ }
7063
+ const value = (name, mode, active = /* @__PURE__ */ new Set()) => {
7064
+ if (active.has(name) || active.size >= 16) return void 0;
7065
+ active.add(name);
7066
+ const raw = modes[mode].get(name) ?? modes.light.get(name);
7067
+ const reference = raw && /^var\((--[\w-]+)\)$/.exec(raw.trim())?.[1];
7068
+ return reference ? value(reference, mode, active) : raw;
7069
+ };
7070
+ const color = (name, mode) => {
7071
+ const raw = value(name, mode);
7072
+ return raw ? cssColorToHex(raw) : void 0;
7073
+ };
7074
+ const mapped = { "--docs-bg": "--ifm-background-color", "--docs-ink": "--ifm-font-color-base", "--docs-ink-2": "--ifm-font-color-base", "--docs-accent": "--ifm-link-color" };
7075
+ const declarations2 = (mode) => Object.entries(mapped).flatMap(([target, source]) => {
7076
+ const hex = color(source, mode);
7077
+ return hex ? [`${target}:${hex}`] : [];
7078
+ }).join(";");
7079
+ const heading = value("--ifm-heading-font-family", "light")?.trim();
7080
+ const headingFont = heading && /^[\w -]{1,80}$/.test(heading) ? heading : void 0;
7081
+ const dimension = (name, fallback) => {
7082
+ const raw = value(name, "light");
7083
+ return raw && /^\d+(?:\.\d+)?(?:px|rem)$/.test(raw) ? raw : fallback;
6650
7084
  };
7085
+ const bodyFont = value("--ifm-font-family-base", "light");
7086
+ const safeBodyFont = bodyFont && /^[\w\s,'"._-]{1,250}$/.test(bodyFont) ? bodyFont : "ui-sans-serif,system-ui,sans-serif";
7087
+ const bannerCss = (mode) => {
7088
+ const styles = [];
7089
+ for (const [property, raw] of banners[mode]) {
7090
+ const reference = /^var\((--[\w-]+)\)$/.exec(raw)?.[1];
7091
+ const resolved = reference ? value(reference, mode) : raw;
7092
+ if (!resolved) continue;
7093
+ const hex = ["background-color", "color"].includes(property) ? cssColorToHex(resolved) : void 0;
7094
+ if (hex) styles.push(`${property}:${hex}`);
7095
+ else if (property === "font-family" && /^[\w\s,'"._-]{1,250}$/.test(resolved) || ["font-size", "padding"].includes(property) && /^(?:\d*\.?\d+(?:px|rem)?\s*){1,4}$/.test(resolved) || property === "font-weight" && /^(?:[1-9]00|bold|normal)$/.test(resolved)) styles.push(`${property}:${resolved}`);
7096
+ }
7097
+ return styles.join(";");
7098
+ };
7099
+ let tokens = "";
7100
+ const prismStart = propertyStart(config, "prism");
7101
+ const prism = prismStart < 0 ? null : matchingObjectLiteral(config, prismStart);
7102
+ const themeStart = prism ? propertyStart(prism, "theme") : -1;
7103
+ const themeExpression = themeStart < 0 ? "" : prism.slice(themeStart);
7104
+ const identifier = /^([\w$]+)/.exec(themeExpression)?.[1];
7105
+ const binding = identifier && config.match(new RegExp(`(?:const|let|var)\\s+${identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*(require\\([^\\n]+)`))?.[1];
7106
+ const themePath = (themeExpression || "").match(/^require\(\s*['"]([^'"]+)['"]/)?.[1] ?? binding?.match(/^require\(\s*['"]([^'"]+)['"]/)?.[1];
7107
+ if (themePath) try {
7108
+ const base = resolveWithin(repositoryRoot, themePath);
7109
+ const path = [base, `${base}.js`, `${base}/index.js`, `${base}/index.cjs.js`].find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile());
7110
+ if (path) {
7111
+ const source = readBoundedText(withinRealRoot(path, repositoryRoot));
7112
+ const start = source.search(/(?:module\.exports|(?:const|let|var)\s+\w+)\s*=/);
7113
+ const literal = start < 0 ? null : matchingObjectLiteral(source, source.indexOf("=", start) + 1);
7114
+ const theme = literal ? JSON5.parse(literal) : null;
7115
+ const tokenNames = { keyword: "keyword", string: "string", comment: "comment", function: "function", property: "property", constant: "constant", punctuation: "punctuation", parameter: "parameter" };
7116
+ const colors = /* @__PURE__ */ new Map();
7117
+ for (const [target, raw] of [["--shiki-color-text", theme?.plain?.color], ["--docs-code-bg", theme?.plain?.backgroundColor]]) {
7118
+ const hex = typeof raw === "string" ? cssColorToHex(raw) : void 0;
7119
+ if (hex && target) colors.set(target, hex);
7120
+ }
7121
+ for (const style of theme?.styles ?? []) for (const type of Array.isArray(style.types) ? style.types : []) {
7122
+ const hex = typeof style.style?.color === "string" ? cssColorToHex(style.style.color) : void 0;
7123
+ if (tokenNames[type] && hex) colors.set(`--shiki-token-${tokenNames[type]}`, hex);
7124
+ }
7125
+ tokens = [...colors].map(([name, hex]) => `${name}:${hex}`).join(";");
7126
+ }
7127
+ } catch {
7128
+ }
7129
+ const lightAccent = color("--ifm-color-primary", "light");
7130
+ const darkAccent = color("--ifm-color-primary", "dark");
7131
+ return { headingFont, colors: lightAccent || darkAccent ? { dark: lightAccent, light: darkAccent } : void 0, unresolved: /\btheme\(/.test(css), css: `
7132
+ :root{${declarations2("light")}}
7133
+ :root.dark{${declarations2("dark")}}
7134
+ body{font-family:${safeBodyFont}}
7135
+ .thally-docs-root,.thally-docs-sidebar{background:var(--docs-bg);color:var(--docs-ink)}
7136
+ .thally-docs-topbar{background:var(--docs-bg);backdrop-filter:none}
7137
+ .thally-docs-topbar-inner{height:${dimension("--ifm-navbar-height", "60px")};max-width:none;padding-inline:24px}
7138
+ .thally-docs-collection-row{display:none}
7139
+ .thally-docs-actions{flex:1}.thally-docs-navlinks{order:-1;margin-left:auto;margin-right:12px}
7140
+ .thally-docs-topbar-link svg{display:none}
7141
+ .thally-docs-search,.thally-docs-search>button:first-of-type{width:180px}
7142
+ .thally-docs-search>button:first-of-type{border:1px solid var(--docs-line);border-radius:24px;background:var(--docs-bg)}
7143
+ .thally-ink-banner{${bannerCss("light")}}
7144
+ .dark .thally-ink-banner{${bannerCss("dark")}}
7145
+ .thally-ink-banner-content{color:inherit;font-size:inherit;font-weight:inherit}
7146
+ .thally-ink-banner-content a{color:var(--docs-accent)}
7147
+ .thally-ink-banner-dot{display:none}
7148
+ .thally-docs-eyebrow{display:none}
7149
+ .thally-docs-header h1{font-size:${dimension("--ifm-h1-font-size", "2.5rem")};line-height:1.25;color:var(--docs-ink)}
7150
+ .thally-docs-prose{line-height:1.5;color:var(--docs-ink)}
7151
+ .thally-docs-prose p{margin-block:0 1rem}
7152
+ .thally-docs-prose a{color:var(--docs-accent)}
7153
+ .thally-docs-code{${tokens};background:var(--docs-code-bg)}
7154
+ @media(min-width:1024px){
7155
+ .thally-docs-shell{max-width:none;padding-inline:0}
7156
+ .thally-docs-sidebar,.thally-docs-sidebar>div{width:300px}
7157
+ .thally-docs-sidebar{border-right:1px solid var(--docs-line)}
7158
+ .thally-docs-main{padding-top:50px}
7159
+ .thally-docs-main>div{padding-inline:26px}
7160
+ .thally-docs-article nav[aria-label=Breadcrumb]{display:none}
7161
+ .thally-docs-sidebar nav{padding-inline:16px}
7162
+ .thally-docs-sidebar a[aria-current=page]{background:transparent!important;text-shadow:none}
7163
+ }
7164
+ ` };
6651
7165
  }
6652
7166
 
6653
7167
  // src/openapi-ref.ts
@@ -6733,7 +7247,7 @@ function sharedSpecMessage(path, excluded = []) {
6733
7247
  }
6734
7248
 
6735
7249
  // src/source-refs.ts
6736
- import { lstatSync as lstatSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, realpathSync as realpathSync2, rmSync, unlinkSync } from "fs";
7250
+ import { lstatSync as lstatSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, rmSync, unlinkSync } from "fs";
6737
7251
  import { isAbsolute as isAbsolute2, join, relative as relative5, resolve as resolve3, sep as sep2 } from "path";
6738
7252
 
6739
7253
  // src/url.ts
@@ -8586,7 +9100,7 @@ function sanitizeAndMeasure(root) {
8586
9100
  const stack = [root];
8587
9101
  while (stack.length > 0) {
8588
9102
  const directory = stack.pop();
8589
- for (const entry of readdirSync2(directory, { withFileTypes: true })) {
9103
+ for (const entry of readdirSync3(directory, { withFileTypes: true })) {
8590
9104
  const path = join(directory, entry.name);
8591
9105
  if (entry.isSymbolicLink()) {
8592
9106
  unlinkSync(path);
@@ -8648,8 +9162,8 @@ function empty(mapping, message) {
8648
9162
  function importSourceRef(mapping, repositoryDir) {
8649
9163
  const docsRoot = findSourceRefDocsRoot(repositoryDir);
8650
9164
  if (!docsRoot) return empty(mapping, "no Mintlify docs.json was found in docs/ or at the repository root.");
8651
- const realRoot = realpathSync2(docsRoot);
8652
- const fromClone = relative5(realpathSync2(repositoryDir), realRoot);
9165
+ const realRoot = realpathSync3(docsRoot);
9166
+ const fromClone = relative5(realpathSync3(repositoryDir), realRoot);
8653
9167
  if (fromClone === ".." || fromClone.startsWith(`..${sep2}`) || isAbsolute2(fromClone)) return empty(mapping, "its docs directory resolves outside the repository.");
8654
9168
  const measured = sanitizeAndMeasure(realRoot);
8655
9169
  if (measured.error) return empty(mapping, `${measured.error}, over the per-repository limit. Nothing was imported.`);
@@ -8695,80 +9209,6 @@ function sourceRefMountCollides(siteRoot, mountPath) {
8695
9209
  });
8696
9210
  }
8697
9211
 
8698
- // src/css-colors.ts
8699
- var channel = (value) => Number(value);
8700
- function cssColorToHex(raw) {
8701
- const value = raw.trim().replace(/\s*!important$/i, "");
8702
- if (value.length > 64) return void 0;
8703
- const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value)?.[1];
8704
- if (hex) return `#${(hex.length === 3 ? [...hex].map((digit) => digit + digit).join("") : hex).toLowerCase()}`;
8705
- const rgb = /^(?:rgba?\(\s*)?(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})\s*(?:[,/]\s*[\d.]+%?\s*)?\)?$/i.exec(value);
8706
- if (!rgb) return void 0;
8707
- const channels = [rgb[1], rgb[2], rgb[3]].map(channel);
8708
- if (channels.some((entry) => entry > 255)) return void 0;
8709
- if (value.includes("(") !== value.includes(")")) return void 0;
8710
- return `#${channels.map((entry) => entry.toString(16).padStart(2, "0")).join("")}`;
8711
- }
8712
- function stripComments(css) {
8713
- let out = "";
8714
- let index = 0;
8715
- while (index < css.length) {
8716
- const open = css.indexOf("/*", index);
8717
- if (open < 0) break;
8718
- out += css.slice(index, open);
8719
- const close = css.indexOf("*/", open + 2);
8720
- if (close < 0) return out;
8721
- index = close + 2;
8722
- }
8723
- return out + css.slice(index);
8724
- }
8725
- function declarations(body, into) {
8726
- for (const declaration of body.split(";")) {
8727
- const colon = declaration.indexOf(":");
8728
- if (colon < 0) continue;
8729
- const name = declaration.slice(0, colon).trim().toLowerCase();
8730
- if (name !== "--primary" && name !== "--primary-light" && name !== "--primary-dark") continue;
8731
- const hex = cssColorToHex(declaration.slice(colon + 1));
8732
- if (hex) into[name.slice(2)] = hex;
8733
- }
8734
- }
8735
- function cssBrandVars(css) {
8736
- const source = stripComments(css);
8737
- const root = {};
8738
- const dark = {};
8739
- let index = 0;
8740
- while (index < source.length) {
8741
- const open = source.indexOf("{", index);
8742
- if (open < 0) break;
8743
- let prelude = (source.slice(index, open).split("}").pop() ?? "").trim();
8744
- while (prelude.startsWith("@") && prelude.includes(";")) prelude = prelude.slice(prelude.indexOf(";") + 1).trim();
8745
- let depth = 1;
8746
- let close = open + 1;
8747
- for (; close < source.length && depth > 0; close++) {
8748
- if (source[close] === "{") depth++;
8749
- else if (source[close] === "}") depth--;
8750
- }
8751
- index = close;
8752
- if (prelude.startsWith("@")) continue;
8753
- for (const selector of prelude.split(",")) {
8754
- const name = selector.trim().toLowerCase();
8755
- const target = name === ":root" || name === "html" ? root : name === ".dark" || name === "html.dark" || name === ":root.dark" ? dark : void 0;
8756
- if (target) declarations(source.slice(open + 1, close - 1), target);
8757
- }
8758
- }
8759
- return { root, dark };
8760
- }
8761
- function brandColorsFromVars({ root, dark }) {
8762
- const result = {};
8763
- const lightAccent = root.primary;
8764
- const lightButton = root["primary-dark"] ?? root.primary;
8765
- if (lightAccent || lightButton) result.light = { ...lightAccent ? { accent: lightAccent } : {}, ...lightButton ? { primary: lightButton } : {} };
8766
- const darkAccent = dark["primary-light"] ?? dark.primary;
8767
- const darkButton = dark["primary-dark"] ?? dark["primary-light"] ?? dark.primary;
8768
- if (darkAccent || darkButton) result.dark = { ...darkAccent ? { accent: darkAccent } : {}, ...darkButton ? { primary: darkButton } : {} };
8769
- return result.light || result.dark ? result : void 0;
8770
- }
8771
-
8772
9212
  // src/repository.ts
8773
9213
  var MAX_SOURCE_FILES = 5e3;
8774
9214
  var MAX_PAGE_BYTES = 2e6;
@@ -8833,8 +9273,16 @@ var REPOSITORY_ONLY_DOCUMENTS = /* @__PURE__ */ new Set([
8833
9273
  ]);
8834
9274
  var SNIPPET_DIRECTORIES = /* @__PURE__ */ new Set(["snippets", "_snippets", "partials", "_partials"]);
8835
9275
  var SNIPPET_IMPORT_PATTERN = /\bimport\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?/g;
9276
+ var DOCUSAURUS_GENERATED_EXPORTS = Object.assign(/* @__PURE__ */ Object.create(null), {
9277
+ toc: "[]",
9278
+ frontMatter: "{}",
9279
+ metadata: "{}",
9280
+ assets: "{}",
9281
+ contentTitle: "undefined"
9282
+ });
8836
9283
  var SNIPPET_VALUE_IMPORT_PATTERN = /\bimport\s*\{([^{}]+)\}\s*from\s+['"]([^'"]+\.mdx?)['"]\s*;?/g;
8837
9284
  var SNIPPET_TAG_PATTERN = /<Snippet\s+file=(?:"([^"]+)"|'([^']+)')\s*(?:\/>|>[\s\S]*?<\/Snippet>)/g;
9285
+ var FERN_MARKDOWN_TAG_PATTERN = /<Markdown\s+src=(?:"([^"]+)"|'([^']+)')\s*(?:\/>|>[\s\S]*?<\/Markdown>)/g;
8838
9286
  var MINTIGNORE_FILENAME = ".mintignore";
8839
9287
  var createIgnoreMatcher = createRequire(import.meta.url)("ignore");
8840
9288
  function isIgnoredDirectory(name) {
@@ -8879,7 +9327,7 @@ function countMarkdownPages(directory, limit = 2e4) {
8879
9327
  const current = stack.pop();
8880
9328
  let entries;
8881
9329
  try {
8882
- entries = readdirSync3(current, { withFileTypes: true });
9330
+ entries = readdirSync4(current, { withFileTypes: true });
8883
9331
  } catch {
8884
9332
  continue;
8885
9333
  }
@@ -8932,7 +9380,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir, warnings) {
8932
9380
  visited++;
8933
9381
  if (hasMintlifyConfig(current.directory)) candidates.push(current.directory);
8934
9382
  if (current.depth >= 4) continue;
8935
- for (const entry of readdirSync3(current.directory, { withFileTypes: true })) {
9383
+ for (const entry of readdirSync4(current.directory, { withFileTypes: true })) {
8936
9384
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
8937
9385
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
8938
9386
  }
@@ -8964,7 +9412,7 @@ function findFernProjectRoot(repositoryDir, docsDir) {
8964
9412
  visited++;
8965
9413
  if (hasFernConfig(current.directory)) return current.directory;
8966
9414
  if (current.depth >= 4) continue;
8967
- for (const entry of readdirSync3(current.directory, { withFileTypes: true })) {
9415
+ for (const entry of readdirSync4(current.directory, { withFileTypes: true })) {
8968
9416
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
8969
9417
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
8970
9418
  }
@@ -8990,7 +9438,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir, warnings) {
8990
9438
  visited++;
8991
9439
  if (hasDocusaurusConfig(current.directory)) candidates.push(current.directory);
8992
9440
  if (current.depth >= 4) continue;
8993
- for (const entry of readdirSync3(current.directory, { withFileTypes: true })) {
9441
+ for (const entry of readdirSync4(current.directory, { withFileTypes: true })) {
8994
9442
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
8995
9443
  queue.push({
8996
9444
  directory: resolveWithin(current.directory, entry.name),
@@ -9100,7 +9548,7 @@ function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
9100
9548
  const directoryNames = (directory) => {
9101
9549
  const path = resolveWithin(projectRoot, directory);
9102
9550
  if (!existsSync5(path) || !lstatSync6(path).isDirectory()) return [];
9103
- return readdirSync3(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).sort();
9551
+ return readdirSync4(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).sort();
9104
9552
  };
9105
9553
  let versions = directoryNames("versioned_docs").filter((name) => /^version-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name));
9106
9554
  const versionsPath = resolveWithin(projectRoot, "versions.json");
@@ -9415,7 +9863,7 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
9415
9863
  }
9416
9864
  function containsMarkdown(directory, depth = 0) {
9417
9865
  if (depth > 4) return false;
9418
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
9866
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
9419
9867
  if (isIgnoredDirectory(entry.name) || entry.isSymbolicLink()) continue;
9420
9868
  const path = resolveWithin(directory, entry.name);
9421
9869
  if (entry.isFile() && [".md", ".mdx"].includes(extname6(entry.name).toLowerCase())) return true;
@@ -9436,7 +9884,7 @@ function scanFiles(root, confinementRoot = root, warnings, rank, budget = MAX_SO
9436
9884
  const walkCap = rank ? budget * RANKED_WALK_FACTOR : budget;
9437
9885
  let confinementReal;
9438
9886
  try {
9439
- confinementReal = realpathSync3(confinementRoot);
9887
+ confinementReal = realpathSync4(confinementRoot);
9440
9888
  } catch {
9441
9889
  confinementReal = confinementRoot;
9442
9890
  }
@@ -9445,7 +9893,7 @@ function scanFiles(root, confinementRoot = root, warnings, rank, budget = MAX_SO
9445
9893
  if (files.length >= walkCap) return;
9446
9894
  let directoryReal;
9447
9895
  try {
9448
- directoryReal = realpathSync3(directory);
9896
+ directoryReal = realpathSync4(directory);
9449
9897
  } catch {
9450
9898
  return;
9451
9899
  }
@@ -9453,7 +9901,7 @@ function scanFiles(root, confinementRoot = root, warnings, rank, budget = MAX_SO
9453
9901
  visitedRealPaths.add(directoryReal);
9454
9902
  let entries;
9455
9903
  try {
9456
- entries = readdirSync3(directory, { withFileTypes: true });
9904
+ entries = readdirSync4(directory, { withFileTypes: true });
9457
9905
  } catch {
9458
9906
  return;
9459
9907
  }
@@ -9474,7 +9922,7 @@ function scanFiles(root, confinementRoot = root, warnings, rank, budget = MAX_SO
9474
9922
  if (entry.isSymbolicLink()) {
9475
9923
  let real;
9476
9924
  try {
9477
- real = realpathSync3(path);
9925
+ real = realpathSync4(path);
9478
9926
  } catch {
9479
9927
  continue;
9480
9928
  }
@@ -9601,7 +10049,7 @@ var PATH_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\`*!?#&]+/;
9601
10049
  function publicAssetKey(repoRelative) {
9602
10050
  return repoRelative.split("/", 1)[0].toLowerCase() === "public" ? repoRelative.slice("public/".length) : repoRelative;
9603
10051
  }
9604
- function addPathReferences(text2, baseDir, exact, loose) {
10052
+ function addPathReferences(text2, baseDir, exact, loose, docusaurus) {
9605
10053
  for (const word of text2.split(PATH_SEPARATORS)) {
9606
10054
  let token = word.replace(/\.+$/, "");
9607
10055
  if (!/\.[a-z0-9]{2,5}$/i.test(token) || token.startsWith("//")) continue;
@@ -9615,14 +10063,40 @@ function addPathReferences(text2, baseDir, exact, loose) {
9615
10063
  loose.add(token.toLowerCase());
9616
10064
  continue;
9617
10065
  }
10066
+ if (docusaurus && !token.startsWith("/")) {
10067
+ const fromSite = normalizeAssetPath(posix4.normalize(token.startsWith("@site/") ? token.slice("@site/".length) : posix4.join(docusaurus.siteBase, token)));
10068
+ const docsFolder = docusaurus.docsPrefix ? `${docusaurus.docsPrefix}/` : "";
10069
+ const siteKey = fromSite?.startsWith("static/") ? fromSite.slice("static/".length) : fromSite && docsFolder && fromSite.startsWith(docsFolder) ? fromSite.slice(docsFolder.length) : void 0;
10070
+ if (siteKey) {
10071
+ exact.add(siteKey);
10072
+ loose.add(siteKey.toLowerCase());
10073
+ }
10074
+ if (token.startsWith("@site/")) continue;
10075
+ }
9618
10076
  if (!token.startsWith("/") && baseDir === void 0) continue;
9619
10077
  const resolved = normalizeAssetPath(posix4.normalize(token.startsWith("/") ? token.slice(1) : posix4.join(baseDir ?? "", token)));
9620
10078
  if (!resolved) continue;
9621
- const key = publicAssetKey(resolved);
10079
+ const key = docusaurus && resolved.startsWith("static/") ? resolved.slice("static/".length) : publicAssetKey(resolved);
9622
10080
  exact.add(key);
9623
10081
  loose.add(key.toLowerCase());
9624
10082
  }
9625
10083
  }
10084
+ function addFernConfigPathReferences(config, exact, loose) {
10085
+ const pending = [{ value: config, depth: 0 }];
10086
+ const seen = /* @__PURE__ */ new Set();
10087
+ let visits = 0;
10088
+ while (pending.length > 0 && visits < 5e3) {
10089
+ const { value, depth } = pending.pop();
10090
+ visits++;
10091
+ if (typeof value === "string") {
10092
+ addPathReferences(value, "", exact, loose);
10093
+ } else if (value && typeof value === "object" && !seen.has(value) && depth < 64) {
10094
+ seen.add(value);
10095
+ if (!Array.isArray(value) && fernViewersReason(value)) continue;
10096
+ for (const child of Object.values(value)) pending.push({ value: child, depth: depth + 1 });
10097
+ }
10098
+ }
10099
+ }
9626
10100
  function looselyNamed(assetPath, loose) {
9627
10101
  const lower = assetPath.toLowerCase();
9628
10102
  const name = lower.split("/").pop() ?? lower;
@@ -9650,6 +10124,18 @@ function classifyApiSpec(path) {
9650
10124
  return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
9651
10125
  }
9652
10126
  var SPEC_DIRECTORY = "openapi";
10127
+ var MAX_SPEC_BYTES = 5e7;
10128
+ function readSpecFile(absolutePath, relativePath, warnings) {
10129
+ if (lstatSync6(absolutePath).size > MAX_SPEC_BYTES) {
10130
+ warnings.push({
10131
+ code: "skipped-file",
10132
+ message: `The OpenAPI spec "${relativePath}" is larger than ${MAX_SPEC_BYTES / 1e6} MB and was not migrated; split it or reduce its size.`,
10133
+ source: relativePath
10134
+ });
10135
+ return void 0;
10136
+ }
10137
+ return readFileSync6(absolutePath);
10138
+ }
9653
10139
  function specAssetPath(filename) {
9654
10140
  return `${SPEC_DIRECTORY}/${filename}`;
9655
10141
  }
@@ -9725,9 +10211,11 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
9725
10211
  message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Links in page bodies and Mintlify endpoint URLs under "${reference.directory}/<tag>/<operation>" are redirected to the matching endpoints where one exists (a separate warning counts any endpoint redirects that were skipped); check any other links to "${reference.directory}/..." manually.`
9726
10212
  });
9727
10213
  }
10214
+ const content = readSpecFile(match.absolutePath, match.relativePath, warnings);
10215
+ if (!content) continue;
9728
10216
  specs.push({
9729
10217
  filename: basename3(match.relativePath),
9730
- content: readFileSync6(match.absolutePath),
10218
+ content,
9731
10219
  tabLabel: reference.tabLabel,
9732
10220
  parentTab: reference.parentTab,
9733
10221
  icon: reference.icon,
@@ -9976,7 +10464,7 @@ function findFernConfiguredOpenApi(fernRoot, repositoryDir, apiName, apiNameExpl
9976
10464
  for (const specPath of fernGeneratorsOpenApiPaths(config)) {
9977
10465
  if (/^(?:https?:)?\/\//i.test(specPath)) continue;
9978
10466
  try {
9979
- const absolute = resolveWithinRoot(dir, specPath, repositoryDir);
10467
+ const absolute = withinRealRoot(resolveWithinRoot(dir, specPath, repositoryDir), repositoryDir);
9980
10468
  if (!existsSync5(absolute) || !lstatSync6(absolute).isFile()) continue;
9981
10469
  const kind = classifyApiSpec(absolute);
9982
10470
  if (kind === "asyncapi") {
@@ -9988,7 +10476,7 @@ function findFernConfiguredOpenApi(fernRoot, repositoryDir, apiName, apiNameExpl
9988
10476
  } catch {
9989
10477
  warnings.push({
9990
10478
  code: "unsupported-config",
9991
- message: `The OpenAPI spec path "${specPath}" in ${relative6(repositoryDir, generatorsPath).replace(/\\/g, "/")} is outside the repository and was skipped.`
10479
+ message: `Skipped the OpenAPI spec "${specPath}" listed in ${relative6(repositoryDir, generatorsPath).replace(/\\/g, "/")} because it points outside the repository (symlink or ..).`
9992
10480
  });
9993
10481
  }
9994
10482
  }
@@ -10027,7 +10515,7 @@ function ownRestrictionReason(access) {
10027
10515
  }
10028
10516
  function canonicalPath(path) {
10029
10517
  try {
10030
- return realpathSync3.native(path);
10518
+ return realpathSync4.native(path);
10031
10519
  } catch {
10032
10520
  return path;
10033
10521
  }
@@ -10042,14 +10530,86 @@ function pageHeadAccess(raw) {
10042
10530
  function describeRestriction(access) {
10043
10531
  return access.groups?.length ? `restricted to groups ${access.groups.join(", ")}` : "`public: false`";
10044
10532
  }
10533
+ function pageHeadGateReason(raw, platform) {
10534
+ const parsed = parseFrontmatter(raw);
10535
+ const gateKeys = platform === "docusaurus" ? /^draft\s*:/m : /^(viewers|authed)\s*:/m;
10536
+ const unreadable = parsed.error && gateKeys.test(raw.slice(0, raw.length - parsed.content.length)) ? "frontmatter could not be parsed and declares an access or draft setting" : void 0;
10537
+ const openapi = typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0;
10538
+ const reason = platform === "docusaurus" ? docusaurusDraftReason(parsed.data) : fernViewersReason(parsed.data);
10539
+ return { reason: reason ?? unreadable, openapi };
10540
+ }
10541
+ var DOCUSAURUS_DRAFT_REASON = "frontmatter `draft` marks it as a draft Docusaurus does not publish";
10542
+ function docusaurusDraftReason(data) {
10543
+ const draft = data.draft;
10544
+ if (draft === void 0 || draft === false || typeof draft === "string" && draft.trim().toLowerCase() === "false") return void 0;
10545
+ return DOCUSAURUS_DRAFT_REASON;
10546
+ }
10547
+ function withoutFencedCode3(raw) {
10548
+ let fence = null;
10549
+ return raw.split("\n").map((line) => {
10550
+ if (fence) {
10551
+ const closing = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
10552
+ if (closing && closing[1][0] === fence.char && closing[1].length >= fence.length) fence = null;
10553
+ return "";
10554
+ }
10555
+ const opening = /^ {0,3}(`{3,}|~{3,})([^\r]*)$/.exec(line);
10556
+ if (opening && !(opening[1][0] === "`" && opening[2].includes("`"))) {
10557
+ fence = { char: opening[1][0], length: opening[1].length };
10558
+ return "";
10559
+ }
10560
+ return line;
10561
+ }).join("\n");
10562
+ }
10563
+ function fernIfRolesReason(raw) {
10564
+ const text2 = withoutFencedCode3(raw).replace(/(`{1,3})[^`\n]*\1/g, "");
10565
+ const marker = /<If(?![\w$.-])/g;
10566
+ for (let match = marker.exec(text2); match; match = marker.exec(text2)) {
10567
+ const tag = scanJsxOpeningTag(text2, match.index, "If");
10568
+ marker.lastIndex = tag.next;
10569
+ if (tag.attributes.includes("...")) {
10570
+ return "it contains `<If>` spread props whose access rules cannot be resolved safely";
10571
+ }
10572
+ if (tag.attributes.includes("{") && /[/`]/.test(tag.attributes)) {
10573
+ return "it contains `<If>` expression props whose access rules cannot be resolved safely";
10574
+ }
10575
+ if (/(?:^|[\s{}"'])(?:roles|viewers)\s*=/.test(tag.attributes)) {
10576
+ return "it contains `<If roles>` content that Fern shows only to some roles";
10577
+ }
10578
+ }
10579
+ return void 0;
10580
+ }
10581
+ function fernViewersReason(data) {
10582
+ const viewers = data.viewers;
10583
+ const hasViewers = Array.isArray(viewers) ? viewers.length > 0 : typeof viewers === "string" ? viewers.trim() !== "" : viewers !== void 0 && viewers !== null && viewers !== false;
10584
+ if (hasViewers) return "frontmatter `viewers` restricts it to signed-in roles";
10585
+ const authed = data.authed;
10586
+ if (authed !== void 0 && authed !== null && authed !== false && !(typeof authed === "string" && ["", "false"].includes(authed.trim().toLowerCase()))) {
10587
+ return "frontmatter `authed` requires sign-in";
10588
+ }
10589
+ return void 0;
10590
+ }
10045
10591
  function withoutFrontmatter(value) {
10046
10592
  return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
10047
10593
  }
10048
10594
  function staticSnippetProperties(attributes) {
10049
10595
  const properties = /* @__PURE__ */ new Map();
10050
- const matcher = /\b([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/g;
10051
- for (const match of attributes.matchAll(matcher)) {
10052
- properties.set(match[1], match[2] ?? match[3] ?? match[4] ?? match[5] ?? "");
10596
+ let tree;
10597
+ try {
10598
+ tree = ts5.createSourceFile("snippet.tsx", `const snippet = <Snippet ${attributes.replace(/\/\s*$/, "")} />`, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10599
+ } catch {
10600
+ return properties;
10601
+ }
10602
+ const statement = tree.statements[0];
10603
+ if (!statement || !ts5.isVariableStatement(statement)) return properties;
10604
+ const element = statement.declarationList.declarations[0]?.initializer;
10605
+ if (!element || !ts5.isJsxSelfClosingElement(element)) return properties;
10606
+ for (const attribute of element.attributes.properties) {
10607
+ if (!ts5.isJsxAttribute(attribute) || !ts5.isIdentifier(attribute.name)) continue;
10608
+ const value = attribute.initializer;
10609
+ if (value && ts5.isStringLiteral(value)) properties.set(attribute.name.text, value.text);
10610
+ else if (value && ts5.isJsxExpression(value) && value.expression && ts5.isStringLiteralLike(value.expression)) {
10611
+ properties.set(attribute.name.text, value.expression.text);
10612
+ }
10053
10613
  }
10054
10614
  return properties;
10055
10615
  }
@@ -10057,6 +10617,7 @@ function interpolateSnippet(snippet, attributes, children) {
10057
10617
  const properties = staticSnippetProperties(attributes);
10058
10618
  if (children !== void 0) properties.set("children", children.trim());
10059
10619
  if (properties.size === 0) return snippet;
10620
+ snippet = bindSnippetProps(snippet, properties);
10060
10621
  const isJsxSnippet = /<[A-Za-z][A-Za-z0-9]*/.test(snippet);
10061
10622
  let fence = "";
10062
10623
  return snippet.split("\n").map((line) => {
@@ -10083,18 +10644,18 @@ function isolateFences(snippet, source, start, end) {
10083
10644
  return `${fence.test(lines[0]) && /\S/.test(before) ? "\n" : ""}${snippet}${fence.test(lines[lines.length - 1]) && /\S/.test(after) ? "\n" : ""}`;
10084
10645
  }
10085
10646
  function snippetComponentBody(source, componentName) {
10086
- const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
10647
+ const parsed = ts5.createSourceFile("snippet.tsx", source, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10087
10648
  for (const statement of parsed.statements) {
10088
- if (!ts4.isVariableStatement(statement)) continue;
10649
+ if (!ts5.isVariableStatement(statement)) continue;
10089
10650
  for (const declaration of statement.declarationList.declarations) {
10090
- if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
10651
+ if (!ts5.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
10091
10652
  const initializer = declaration.initializer;
10092
- if (!initializer || !ts4.isArrowFunction(initializer)) continue;
10093
- const returned = ts4.isBlock(initializer.body) ? initializer.body.statements.find(ts4.isReturnStatement)?.expression : initializer.body;
10653
+ if (!initializer || !ts5.isArrowFunction(initializer)) continue;
10654
+ const returned = ts5.isBlock(initializer.body) ? initializer.body.statements.find(ts5.isReturnStatement)?.expression : initializer.body;
10094
10655
  if (!returned) continue;
10095
10656
  let expression = returned;
10096
- while (ts4.isParenthesizedExpression(expression)) expression = expression.expression;
10097
- if (ts4.isJsxElement(expression) || ts4.isJsxSelfClosingElement(expression) || ts4.isJsxFragment(expression)) {
10657
+ while (ts5.isParenthesizedExpression(expression)) expression = expression.expression;
10658
+ if (ts5.isJsxElement(expression) || ts5.isJsxSelfClosingElement(expression) || ts5.isJsxFragment(expression)) {
10098
10659
  return expression.getText(parsed);
10099
10660
  }
10100
10661
  }
@@ -10102,65 +10663,65 @@ function snippetComponentBody(source, componentName) {
10102
10663
  return source;
10103
10664
  }
10104
10665
  function statefulSnippetDeclaration(source, componentName) {
10105
- const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
10106
- const isEsm = (statement2) => ts4.isImportDeclaration(statement2) || (ts4.isVariableStatement(statement2) || ts4.isFunctionDeclaration(statement2) || ts4.isClassDeclaration(statement2)) && !!ts4.getModifiers(statement2)?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword);
10666
+ const parsed = ts5.createSourceFile("snippet.tsx", source, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10667
+ const isEsm = (statement2) => ts5.isImportDeclaration(statement2) || (ts5.isVariableStatement(statement2) || ts5.isFunctionDeclaration(statement2) || ts5.isClassDeclaration(statement2)) && !!ts5.getModifiers(statement2)?.some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword);
10107
10668
  if (parsed.statements.length === 0 || !parsed.statements.every(isEsm)) return null;
10108
- const statement = parsed.statements.find((candidate) => ts4.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts4.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
10109
- if (!statement || !ts4.isVariableStatement(statement)) return null;
10669
+ const statement = parsed.statements.find((candidate) => ts5.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts5.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
10670
+ if (!statement || !ts5.isVariableStatement(statement)) return null;
10110
10671
  const declaration = statement.declarationList.declarations[0];
10111
- if (!declaration.initializer || !ts4.isArrowFunction(declaration.initializer) || !ts4.isBlock(declaration.initializer.body)) return null;
10672
+ if (!declaration.initializer || !ts5.isArrowFunction(declaration.initializer) || !ts5.isBlock(declaration.initializer.body)) return null;
10112
10673
  const body = declaration.initializer.body.statements;
10113
- if (body.length < 2 || !body.some(ts4.isReturnStatement)) return null;
10674
+ if (body.length < 2 || !body.some(ts5.isReturnStatement)) return null;
10114
10675
  return source.trim();
10115
10676
  }
10116
10677
  function isStaticLiteral(expression) {
10117
- if (ts4.isStringLiteral(expression) || ts4.isNoSubstitutionTemplateLiteral(expression) || ts4.isNumericLiteral(expression)) return true;
10118
- if (expression.kind === ts4.SyntaxKind.TrueKeyword || expression.kind === ts4.SyntaxKind.FalseKeyword || expression.kind === ts4.SyntaxKind.NullKeyword) return true;
10119
- if (ts4.isArrayLiteralExpression(expression)) return expression.elements.every(isStaticLiteral);
10120
- if (ts4.isObjectLiteralExpression(expression)) {
10121
- return expression.properties.every((property) => ts4.isPropertyAssignment(property) && (ts4.isIdentifier(property.name) || ts4.isStringLiteral(property.name)) && isStaticLiteral(property.initializer));
10678
+ if (ts5.isStringLiteral(expression) || ts5.isNoSubstitutionTemplateLiteral(expression) || ts5.isNumericLiteral(expression)) return true;
10679
+ if (expression.kind === ts5.SyntaxKind.TrueKeyword || expression.kind === ts5.SyntaxKind.FalseKeyword || expression.kind === ts5.SyntaxKind.NullKeyword) return true;
10680
+ if (ts5.isArrayLiteralExpression(expression)) return expression.elements.every(isStaticLiteral);
10681
+ if (ts5.isObjectLiteralExpression(expression)) {
10682
+ return expression.properties.every((property) => ts5.isPropertyAssignment(property) && (ts5.isIdentifier(property.name) || ts5.isStringLiteral(property.name)) && isStaticLiteral(property.initializer));
10122
10683
  }
10123
10684
  return false;
10124
10685
  }
10125
10686
  function snippetExportedNames(source) {
10126
10687
  const names = /* @__PURE__ */ new Set();
10127
10688
  const addBinding = (name) => {
10128
- if (ts4.isIdentifier(name)) names.add(name.text);
10129
- else for (const element of name.elements) if (!ts4.isOmittedExpression(element)) addBinding(element.name);
10689
+ if (ts5.isIdentifier(name)) names.add(name.text);
10690
+ else for (const element of name.elements) if (!ts5.isOmittedExpression(element)) addBinding(element.name);
10130
10691
  };
10131
- const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
10692
+ const parsed = ts5.createSourceFile("snippet.tsx", source, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10132
10693
  for (const statement of parsed.statements) {
10133
- if (ts4.isExportDeclaration(statement)) {
10134
- if (statement.exportClause && ts4.isNamedExports(statement.exportClause)) {
10694
+ if (ts5.isExportDeclaration(statement)) {
10695
+ if (statement.exportClause && ts5.isNamedExports(statement.exportClause)) {
10135
10696
  for (const element of statement.exportClause.elements) names.add(element.name.text);
10136
10697
  }
10137
10698
  continue;
10138
10699
  }
10139
- const modifiers = ts4.getModifiers(statement);
10140
- if (!modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword)) continue;
10141
- if (modifiers.some((modifier) => modifier.kind === ts4.SyntaxKind.DefaultKeyword)) continue;
10142
- if (ts4.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) addBinding(declaration.name);
10143
- else if ((ts4.isFunctionDeclaration(statement) || ts4.isClassDeclaration(statement)) && statement.name) names.add(statement.name.text);
10700
+ const modifiers = ts5.getModifiers(statement);
10701
+ if (!modifiers?.some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword)) continue;
10702
+ if (modifiers.some((modifier) => modifier.kind === ts5.SyntaxKind.DefaultKeyword)) continue;
10703
+ if (ts5.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) addBinding(declaration.name);
10704
+ else if ((ts5.isFunctionDeclaration(statement) || ts5.isClassDeclaration(statement)) && statement.name) names.add(statement.name.text);
10144
10705
  }
10145
10706
  return names;
10146
10707
  }
10147
10708
  function staticNamedSnippetValues(source) {
10148
10709
  const values = /* @__PURE__ */ new Map();
10149
- const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
10710
+ const parsed = ts5.createSourceFile("snippet.tsx", source, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10150
10711
  for (const statement of parsed.statements) {
10151
- if (!ts4.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword)) continue;
10712
+ if (!ts5.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword)) continue;
10152
10713
  for (const declaration of statement.declarationList.declarations) {
10153
- if (!ts4.isIdentifier(declaration.name) || !declaration.initializer) continue;
10714
+ if (!ts5.isIdentifier(declaration.name) || !declaration.initializer) continue;
10154
10715
  const expression = declaration.initializer;
10155
- if (ts4.isStringLiteral(expression) || ts4.isNoSubstitutionTemplateLiteral(expression)) {
10716
+ if (ts5.isStringLiteral(expression) || ts5.isNoSubstitutionTemplateLiteral(expression)) {
10156
10717
  values.set(declaration.name.text, JSON.stringify(expression.text));
10157
- } else if (ts4.isNumericLiteral(expression)) {
10718
+ } else if (ts5.isNumericLiteral(expression)) {
10158
10719
  values.set(declaration.name.text, expression.text);
10159
- } else if (expression.kind === ts4.SyntaxKind.TrueKeyword || expression.kind === ts4.SyntaxKind.FalseKeyword) {
10160
- values.set(declaration.name.text, expression.kind === ts4.SyntaxKind.TrueKeyword ? "true" : "false");
10161
- } else if (/^[a-z_$]/.test(declaration.name.text) && (ts4.isArrowFunction(expression) || ts4.isFunctionExpression(expression)) && isSelfContainedFunction(expression)) {
10720
+ } else if (expression.kind === ts5.SyntaxKind.TrueKeyword || expression.kind === ts5.SyntaxKind.FalseKeyword) {
10721
+ values.set(declaration.name.text, expression.kind === ts5.SyntaxKind.TrueKeyword ? "true" : "false");
10722
+ } else if (/^[a-z_$]/.test(declaration.name.text) && (ts5.isArrowFunction(expression) || ts5.isFunctionExpression(expression)) && isSelfContainedFunction(expression)) {
10162
10723
  values.set(declaration.name.text, expression.getText(parsed));
10163
- } else if ((ts4.isObjectLiteralExpression(expression) || ts4.isArrayLiteralExpression(expression)) && isStaticLiteral(expression)) {
10724
+ } else if ((ts5.isObjectLiteralExpression(expression) || ts5.isArrayLiteralExpression(expression)) && isStaticLiteral(expression)) {
10164
10725
  values.set(declaration.name.text, expression.getText(parsed));
10165
10726
  }
10166
10727
  }
@@ -10286,6 +10847,14 @@ function preserveLinkedAnchors(pages) {
10286
10847
  for (const page of pages) {
10287
10848
  const fragments = requested.get(page.id);
10288
10849
  if (!fragments?.size) continue;
10850
+ const definitions = /* @__PURE__ */ new Map();
10851
+ replaceOutsideCodeAndComments(page.body, (text2) => {
10852
+ for (const match of text2.matchAll(/^ {0,3}\[([^\]\n]+)\]:[^\n]*$/gm)) {
10853
+ const label = match[1].trim().replace(/\s+/g, " ").toLowerCase();
10854
+ if (!definitions.has(label)) definitions.set(label, match[0]);
10855
+ }
10856
+ return text2;
10857
+ });
10289
10858
  const lines = page.body.split("\n");
10290
10859
  let fence;
10291
10860
  const tableCells = [];
@@ -10300,7 +10869,7 @@ function preserveLinkedAnchors(pages) {
10300
10869
  const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
10301
10870
  if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
10302
10871
  const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
10303
- return match ? { index, text: match[1].replace(/\s*\{\/\*\s*#\S+?\s*\*\/\}$/, "") } : null;
10872
+ return match ? { index, text: mdxVisibleText(match[1].replace(/\s*\{\/\*\s*#\S+?\s*\*\/\}$/, ""), definitions) } : null;
10304
10873
  }).filter((entry) => entry !== null);
10305
10874
  const existing = /* @__PURE__ */ new Set();
10306
10875
  replaceOutsideCode(page.body, (text2) => {
@@ -10403,18 +10972,66 @@ function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
10403
10972
  resolveWithin(repositoryRoot, relative6(repositoryRoot, candidate));
10404
10973
  return withinRealRoot(candidate, repositoryRoot);
10405
10974
  }
10406
- function withinRealRoot(candidate, repositoryRoot) {
10407
- let real;
10408
- try {
10409
- real = realpathSync3(candidate);
10410
- } catch {
10411
- return candidate;
10412
- }
10413
- const realRoot = realpathSync3(repositoryRoot);
10414
- if (real !== realRoot && !real.startsWith(realRoot.endsWith(sep3) ? realRoot : realRoot + sep3)) {
10415
- throw new Error(`Migration path escapes its root: ${candidate}`);
10975
+ function docusaurusGlobalImports(siteRoot, repositoryRoot, warnings) {
10976
+ const registrations = /* @__PURE__ */ new Map();
10977
+ for (const filename of ["MDXComponents.js", "MDXComponents.jsx", "MDXComponents.ts", "MDXComponents.tsx", "MDXComponents/index.js", "MDXComponents/index.tsx"]) {
10978
+ try {
10979
+ const path = withinRealRoot(resolveWithin(siteRoot, `src/theme/${filename}`), repositoryRoot);
10980
+ if (!existsSync5(path) || !lstatSync6(path).isFile()) continue;
10981
+ if (lstatSync6(path).size > MAX_PAGE_BYTES) throw new Error("registry exceeds the 2 MB limit");
10982
+ const tree = ts5.createSourceFile(path, readFileSync6(path, "utf8"), ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
10983
+ const bindings = /* @__PURE__ */ new Map();
10984
+ for (const statement of tree.statements) {
10985
+ if (!ts5.isImportDeclaration(statement) || !ts5.isStringLiteral(statement.moduleSpecifier) || statement.importClause?.isTypeOnly) continue;
10986
+ let source = statement.moduleSpecifier.text;
10987
+ if (source.startsWith(".")) source = `@site/${posix4.normalize(posix4.join("src/theme", posix4.dirname(filename), source))}`;
10988
+ if (!source.startsWith("@site/") && !source.startsWith("/")) continue;
10989
+ const clause = statement.importClause;
10990
+ if (clause?.name) bindings.set(clause.name.text, { source, imported: "default" });
10991
+ if (clause?.namedBindings && ts5.isNamedImports(clause.namedBindings)) {
10992
+ for (const binding of clause.namedBindings.elements) {
10993
+ if (!binding.isTypeOnly) bindings.set(binding.name.text, { source, imported: binding.propertyName?.text ?? binding.name.text });
10994
+ }
10995
+ }
10996
+ }
10997
+ for (const statement of tree.statements) {
10998
+ if (!ts5.isExportAssignment(statement) || statement.isExportEquals || !ts5.isObjectLiteralExpression(statement.expression)) continue;
10999
+ for (const property of statement.expression.properties) {
11000
+ if (!ts5.isShorthandPropertyAssignment(property) && !ts5.isPropertyAssignment(property)) continue;
11001
+ const name = property.name.getText(tree).replace(/^['"]|['"]$/g, "");
11002
+ const local = ts5.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
11003
+ if (!/^[A-Z][\w$]*$/.test(name) || !ts5.isIdentifier(local)) continue;
11004
+ const binding = bindings.get(local.text);
11005
+ if (!binding) continue;
11006
+ if (isThallyBuiltinComponent(name)) {
11007
+ warnings.push({ code: "unsupported-config", message: `Global ${name} override uses the native widget mapping; review the source override for custom styling.`, source: `src/theme/${filename}` });
11008
+ continue;
11009
+ }
11010
+ registrations.set(name, binding.imported === "default" ? `import ${name} from ${JSON.stringify(binding.source)};` : `import { ${binding.imported} as ${name} } from ${JSON.stringify(binding.source)};`);
11011
+ }
11012
+ }
11013
+ return registrations;
11014
+ } catch (error) {
11015
+ warnings.push({ code: "unsupported-config", message: `Docusaurus MDX registry was not imported: ${error instanceof Error ? error.message : "unreadable file"}.`, source: `src/theme/${filename}` });
11016
+ }
10416
11017
  }
10417
- return candidate;
11018
+ return registrations;
11019
+ }
11020
+ function injectDocusaurusGlobalImports(raw, imports2) {
11021
+ if (imports2.size === 0) return raw;
11022
+ const parsed = parseFrontmatter(raw);
11023
+ const declared = pageScopeNames([parsed.content]);
11024
+ const used = /* @__PURE__ */ new Set();
11025
+ replaceOutsideCodeAndComments(parsed.content, (segment) => {
11026
+ for (const match of segment.matchAll(/<([A-Z][\w$]*)(?=[\s/>])/g)) {
11027
+ if (imports2.has(match[1]) && !declared.has(match[1])) used.add(match[1]);
11028
+ }
11029
+ return segment;
11030
+ });
11031
+ const prefix = raw.slice(0, raw.length - parsed.content.length);
11032
+ return used.size === 0 ? raw : `${prefix}${[...used].map((name) => imports2.get(name)).join("\n")}
11033
+
11034
+ ${parsed.content}`;
10418
11035
  }
10419
11036
  function globalSnippetAliases(files, repositoryRoot, siteRoot) {
10420
11037
  const aliases = /* @__PURE__ */ new Map();
@@ -10464,8 +11081,8 @@ function snippetExportIndex(files) {
10464
11081
  continue;
10465
11082
  }
10466
11083
  if (extension !== ".mdx") {
10467
- const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
10468
- const hasDefault = parsed.statements.some((statement) => ts4.isExportAssignment(statement) && !statement.isExportEquals || ts4.getModifiers(statement)?.some((modifier) => modifier.kind === ts4.SyntaxKind.DefaultKeyword));
11084
+ const parsed = ts5.createSourceFile("snippet.tsx", source, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TSX);
11085
+ const hasDefault = parsed.statements.some((statement) => ts5.isExportAssignment(statement) && !statement.isExportEquals || ts5.getModifiers(statement)?.some((modifier) => modifier.kind === ts5.SyntaxKind.DefaultKeyword));
10469
11086
  if (hasDefault) add(basename3(file.relativePath).replace(/\.[^.]+$/, ""), file.absolutePath);
10470
11087
  }
10471
11088
  }
@@ -10524,8 +11141,8 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced, onRootF
10524
11141
  usedRootFallback = true;
10525
11142
  }
10526
11143
  }
10527
- const realRoot = realpathSync3(siteRoot);
10528
- const realCandidate = realpathSync3(candidate);
11144
+ const realRoot = realpathSync4(siteRoot);
11145
+ const realCandidate = realpathSync4(candidate);
10529
11146
  const realRelative = relative6(realRoot, realCandidate);
10530
11147
  if (realRelative === ".." || realRelative.startsWith(`..${sep3}`) || isAbsolute3(realRelative)) return null;
10531
11148
  const siteRelative = relative6(siteRoot, candidate).replace(/\\/g, "/");
@@ -10533,7 +11150,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced, onRootF
10533
11150
  if (!existsSync5(candidate) || !lstatSync6(candidate).isFile()) return null;
10534
11151
  const normalized = normalizeAssetPath(siteRelative);
10535
11152
  if (!normalized) return null;
10536
- const onDiskRelative = relative6(realRoot, realpathSync3.native(candidate)).replace(/\\/g, "/");
11153
+ const onDiskRelative = relative6(realRoot, realpathSync4.native(candidate)).replace(/\\/g, "/");
10537
11154
  const onDisk = onDiskRelative.toLowerCase() === siteRelative.toLowerCase() ? normalizeAssetPath(onDiskRelative) : null;
10538
11155
  onReferenced?.(normalized, onDisk ?? void 0);
10539
11156
  if (usedRootFallback) onRootFallback?.(decodedPath, normalized);
@@ -10594,7 +11211,13 @@ function locallyDeclaredNames(raw) {
10594
11211
  }
10595
11212
  return names;
10596
11213
  }
10597
- function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map(), gate) {
11214
+ function exportBlock(declarations2, statement, sourcePath) {
11215
+ const code = declarations2.join("\n");
11216
+ return code && statement.slice(statement.lastIndexOf(sourcePath)).includes("\n") ? `${code}
11217
+
11218
+ ` : code;
11219
+ }
11220
+ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map(), gate, platform) {
10598
11221
  if (depth >= 8) return raw;
10599
11222
  raw = normalizeIndentedFences(raw);
10600
11223
  const snippets = /* @__PURE__ */ new Map();
@@ -10608,6 +11231,10 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10608
11231
  warnings.push({ code: "gated-page", message: `${label} is access-restricted and was NOT inlined; it was left as a comment.`, source });
10609
11232
  return `{/* Access-restricted content withheld: ${shown} */}`;
10610
11233
  }
11234
+ if (verdict.kind === "draft") {
11235
+ warnings.push({ code: "gated-page", message: `${label} is a draft page that Docusaurus does not publish, so it was not inlined; it was left as a comment.`, source });
11236
+ return `{/* Draft content not inlined: ${shown} */}`;
11237
+ }
10611
11238
  if (verdict.kind === "snippet-gated") {
10612
11239
  warnings.push({
10613
11240
  code: "gated-page",
@@ -10623,6 +11250,59 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10623
11250
  });
10624
11251
  return `{/* ${verdict.kind === "oversized" ? "Oversized" : "Unreadable"} content not inlined: ${shown} */}`;
10625
11252
  };
11253
+ if (platform === "fern") raw = replaceOutsideCodeAndComments(raw, (segment) => {
11254
+ const marker = /<Code(?![\w$.-])/g;
11255
+ let output = "";
11256
+ let cursor = 0;
11257
+ for (let match = marker.exec(segment); match; match = marker.exec(segment)) {
11258
+ const opening = scanJsxOpeningTag(segment, match.index, "Code");
11259
+ marker.lastIndex = opening.next;
11260
+ if (opening.end === null) continue;
11261
+ if (segment[opening.end - 1] !== "/") continue;
11262
+ const properties = staticSnippetProperties(opening.attributes);
11263
+ const attribute = (name) => properties.get(name);
11264
+ const filePath = attribute("src");
11265
+ if (!filePath) continue;
11266
+ const end = opening.end + 1;
11267
+ let replacement;
11268
+ try {
11269
+ const candidate = resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot);
11270
+ if (!existsSync5(candidate) || !lstatSync6(candidate).isFile()) throw new Error("file not found");
11271
+ if (lstatSync6(candidate).size > MAX_PAGE_BYTES) throw new Error("file exceeds the 2 MB inline limit");
11272
+ const blocked = blockInline(candidate, filePath, `Code source ${filePath}`);
11273
+ if (blocked) replacement = blocked;
11274
+ else {
11275
+ let code = readFileSync6(candidate, "utf8").replace(/\r\n/g, "\n");
11276
+ const lines = attribute("lines");
11277
+ if (lines !== void 0) {
11278
+ const range = /^(\d+)(?:-(\d+))?$/.exec(lines);
11279
+ const first = Number(range?.[1]);
11280
+ const last = Number(range?.[2] ?? range?.[1]);
11281
+ if (!Number.isSafeInteger(first) || !Number.isSafeInteger(last) || first < 1 || last < first) throw new Error("invalid line range");
11282
+ code = code.split("\n").slice(first - 1, last).join("\n");
11283
+ }
11284
+ const language = attribute("language") ?? "text";
11285
+ if (!/^[\w+-]+$/.test(language)) throw new Error("invalid language");
11286
+ const title = attribute("title")?.replace(/[\r\n`]/g, " ");
11287
+ let fenceLength = 3;
11288
+ for (const run of code.matchAll(/`+/g)) fenceLength = Math.max(fenceLength, run[0].length + 1);
11289
+ const fence = "`".repeat(fenceLength);
11290
+ const before = segment.slice(segment.lastIndexOf("\n", match.index - 1) + 1, match.index);
11291
+ const indent = /^[ \t]*$/.test(before) ? before : "";
11292
+ replacement = isolateFences(`${fence}${language}${title ? ` ${title}` : ""}
11293
+ ${code.replace(/\n$/, "")}
11294
+ ${fence}`.split("\n").join(`
11295
+ ${indent}`), segment, match.index, end);
11296
+ }
11297
+ } catch (error) {
11298
+ warnings.push({ code: "skipped-file", message: `Code source ${filePath} was not inlined: ${error instanceof Error ? error.message : "unreadable file"}.`, source });
11299
+ replacement = mdxComment(` Code source not inlined: ${filePath} `);
11300
+ }
11301
+ output += segment.slice(cursor, match.index) + replacement;
11302
+ cursor = end;
11303
+ }
11304
+ return output + segment.slice(cursor);
11305
+ });
10626
11306
  let withoutImports = replaceOutsideCodeAndMdxComments(raw, (source2) => source2.replace(
10627
11307
  SNIPPET_IMPORT_PATTERN,
10628
11308
  (_statement, namedComponent, defaultComponent, sourcePath) => {
@@ -10644,7 +11324,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10644
11324
  depth + 1,
10645
11325
  siteRoot,
10646
11326
  globalAliases,
10647
- gate
11327
+ gate,
11328
+ platform
10648
11329
  );
10649
11330
  const declaration = statefulSnippetDeclaration(nested, componentName);
10650
11331
  if (declaration) preservedDeclarations.set(componentName, declaration);
@@ -10692,7 +11373,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10692
11373
  if (/^[A-Z]/.test(binding.exported)) snippets.set(binding.local, blocked);
10693
11374
  else declarations3.push(`export const ${binding.local} = undefined;`);
10694
11375
  }
10695
- return declarations3.join("\n");
11376
+ return exportBlock(declarations3, statement, sourcePath);
10696
11377
  }
10697
11378
  const snippetSource = readFileSync6(candidate, "utf8");
10698
11379
  const values = staticNamedSnippetValues(snippetSource);
@@ -10707,6 +11388,11 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10707
11388
  }
10708
11389
  exportedNames ??= snippetExportedNames(snippetSource);
10709
11390
  const exported = exportedNames.has(binding.exported);
11391
+ const generated = exported || platform !== "docusaurus" ? void 0 : DOCUSAURUS_GENERATED_EXPORTS[binding.exported];
11392
+ if (generated) {
11393
+ declarations2.push(`export const ${binding.local} = ${generated};`);
11394
+ continue;
11395
+ }
10710
11396
  if (!exported && !(/^[A-Z]/.test(binding.exported) && snippetComponentBody(snippetSource, binding.exported) !== snippetSource)) {
10711
11397
  warnings.push({
10712
11398
  code: "unsupported-config",
@@ -10730,7 +11416,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10730
11416
  components.push([binding.local, body]);
10731
11417
  }
10732
11418
  for (const [name, body] of components) snippets.set(name, body);
10733
- return declarations2.join("\n");
11419
+ return exportBlock(declarations2, statement, sourcePath);
10734
11420
  } catch (error) {
10735
11421
  if (error instanceof Error && error.message.includes("escapes its root")) {
10736
11422
  warnings.push({
@@ -10764,7 +11450,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10764
11450
  depth + 1,
10765
11451
  siteRoot,
10766
11452
  globalAliases,
10767
- gate
11453
+ gate,
11454
+ platform
10768
11455
  );
10769
11456
  const declaration = statefulSnippetDeclaration(nested, componentName);
10770
11457
  if (declaration) preservedDeclarations.set(componentName, declaration);
@@ -10778,13 +11465,17 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10778
11465
  return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes, children)), source2, offset, offset + tag.length);
10779
11466
  });
10780
11467
  }
10781
- return segment.replace(SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
11468
+ const tagLabel = (filePath) => platform === "fern" ? `Included file ${filePath}` : `Snippet file="${filePath}"`;
11469
+ return segment.replace(platform === "fern" ? FERN_MARKDOWN_TAG_PATTERN : SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
10782
11470
  const filePath = doubleQuoted ?? singleQuoted;
10783
11471
  try {
10784
- const candidate = [
11472
+ const candidate = (platform === "fern" ? [
11473
+ // Fern's absolute include paths are rooted at fern/, never snippets/.
11474
+ () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
11475
+ ] : [
10785
11476
  () => withinRealRoot(resolveWithin(siteRoot, `snippets/${filePath}`), repositoryRoot),
10786
11477
  () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
10787
- ].map((resolveCandidate) => {
11478
+ ]).map((resolveCandidate) => {
10788
11479
  try {
10789
11480
  return resolveCandidate();
10790
11481
  } catch {
@@ -10792,7 +11483,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10792
11483
  }
10793
11484
  }).find((path) => path !== void 0 && existsSync5(path) && lstatSync6(path).isFile());
10794
11485
  if (!candidate) throw new Error("file not found");
10795
- const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
11486
+ const blocked = blockInline(candidate, filePath, tagLabel(filePath));
10796
11487
  if (blocked) return blocked;
10797
11488
  return isolateFences(closeOpenCodeFence(inlineMdxSnippets(
10798
11489
  withoutFrontmatter(readFileSync6(candidate, "utf8")),
@@ -10802,12 +11493,13 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
10802
11493
  depth + 1,
10803
11494
  siteRoot,
10804
11495
  globalAliases,
10805
- gate
11496
+ gate,
11497
+ platform
10806
11498
  )), source2, offset, offset + tag.length);
10807
11499
  } catch {
10808
11500
  warnings.push({
10809
11501
  code: "missing-page",
10810
- message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
11502
+ message: platform === "fern" ? `${tagLabel(filePath)} could not be found, so it was left out of the page.` : `${tagLabel(filePath)} could not be resolved and was left as a comment.`,
10811
11503
  source: relative6(repositoryRoot, currentFile).replace(/\\/g, "/")
10812
11504
  });
10813
11505
  return mdxComment(` Missing snippet: ${filePath} `);
@@ -10890,6 +11582,7 @@ function migrateRepository(options) {
10890
11582
  })();
10891
11583
  const componentRoot = mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir;
10892
11584
  const componentMigrator = platform === "mintlify" || platform === "docusaurus" || platform === "fern" ? createComponentMigrator(componentRoot, repositoryDir, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot, options.componentNamespace)) : void 0;
11585
+ const globalDocusaurusImports = platform === "docusaurus" ? docusaurusGlobalImports(componentRoot, repositoryDir, warnings) : /* @__PURE__ */ new Map();
10893
11586
  let docsConfig = { tabs: [] };
10894
11587
  let defaultVersionPrefixes = /* @__PURE__ */ new Set();
10895
11588
  let allVersionPrefixes = /* @__PURE__ */ new Set();
@@ -10903,12 +11596,15 @@ function migrateRepository(options) {
10903
11596
  let fernBasePath = "";
10904
11597
  let fernVersionPath = "";
10905
11598
  let fernApiSections = [];
11599
+ let fernRestrictedApiSections = [];
11600
+ let fernNavigationIncomplete = false;
10906
11601
  const fernExternalSourcePaths = /* @__PURE__ */ new Set();
10907
11602
  let fernChangelogIndexes = [];
10908
11603
  const fernReferencedPaths = /* @__PURE__ */ new Set();
10909
11604
  const fernSourceLinkAliases = /* @__PURE__ */ new Map();
10910
11605
  const fernNavTitles = /* @__PURE__ */ new Map();
10911
11606
  const fernHiddenIds = /* @__PURE__ */ new Set();
11607
+ const fernRestrictedPaths = /* @__PURE__ */ new Set();
10912
11608
  const activeSourceRefs = [];
10913
11609
  const usedSourceRefs = /* @__PURE__ */ new Set();
10914
11610
  const gatedSourceRepos = /* @__PURE__ */ new Set();
@@ -10971,6 +11667,8 @@ function migrateRepository(options) {
10971
11667
  docsConfig = projected.docsConfig;
10972
11668
  warnings.push(...projected.warnings);
10973
11669
  fernApiSections = projected.apiSections;
11670
+ fernRestrictedApiSections = projected.restrictedApiSections;
11671
+ fernNavigationIncomplete = projected.navigationIncomplete;
10974
11672
  fernChangelogIndexes = projected.changelogIndexes;
10975
11673
  for (const [index, descriptor] of projected.descriptors.entries()) {
10976
11674
  fernReferencedPaths.add(descriptor.sourcePath);
@@ -10978,6 +11676,7 @@ function migrateRepository(options) {
10978
11676
  fernNavTitles.set(descriptor.navigationId, descriptor.navTitle);
10979
11677
  }
10980
11678
  if (descriptor.hidden) fernHiddenIds.add(descriptor.navigationId);
11679
+ if (descriptor.restricted) fernRestrictedPaths.add(canonicalPath(resolvePath(fernProjectRoot, descriptor.sourcePath)));
10981
11680
  const sourceTail = descriptor.sourcePath.match(/(?:^|\/)pages\/(.+)\.mdx?$/i)?.[1]?.replace(/\/index$/i, "");
10982
11681
  if (sourceTail) {
10983
11682
  const routeSegments = descriptor.navigationId.split("/");
@@ -11002,13 +11701,17 @@ function migrateRepository(options) {
11002
11701
  });
11003
11702
  }
11004
11703
  }
11704
+ if (fernNavigationIncomplete) {
11705
+ throw new Error(`Fern navigation could not be fully classified, so no pages, assets or API specs were migrated. Fix cyclic references or reduce the navigation's nesting or node count before retrying.
11706
+ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
11707
+ }
11005
11708
  if (platform === "docusaurus") {
11006
11709
  try {
11007
11710
  docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot, options.docusaurusSidebarPath);
11008
11711
  } catch (error) {
11009
11712
  warnings.push({
11010
11713
  code: "unsupported-config",
11011
- message: `Docusaurus sidebar could not be read safely: ${error instanceof Error ? error.message : String(error)} Generated navigation will be used.`
11714
+ message: isPathEscapeError(error) ? "Skipped the sidebar file because it points outside the repository (symlink or ..). Navigation was generated from the docs folder instead." : `Docusaurus sidebar could not be read safely: ${error instanceof Error ? error.message : String(error)} Generated navigation will be used.`
11012
11715
  });
11013
11716
  }
11014
11717
  }
@@ -11040,7 +11743,7 @@ function migrateRepository(options) {
11040
11743
  for (const sourcePath of fernExternalSourcePaths) {
11041
11744
  if (discovered2.has(sourcePath)) continue;
11042
11745
  try {
11043
- const absolutePath = resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir);
11746
+ const absolutePath = withinRealRoot(resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir), repositoryDir);
11044
11747
  if (existsSync5(absolutePath) && lstatSync6(absolutePath).isFile()) {
11045
11748
  files.push({ absolutePath, relativePath: sourcePath });
11046
11749
  discovered2.add(sourcePath);
@@ -11088,10 +11791,11 @@ function migrateRepository(options) {
11088
11791
  const size = lstatSync6(file.absolutePath).size;
11089
11792
  const raw = readFrontmatterHead(file.absolutePath) ?? (size > MAX_PAGE_BYTES ? void 0 : readFileSync6(file.absolutePath, "utf8"));
11090
11793
  if (raw === void 0) throw new Error("frontmatter is not terminated within the bounded read");
11091
- head = pageHeadAccess(raw);
11794
+ head = platform === "mintlify" ? pageHeadAccess(raw) : pageHeadGateReason(raw, platform);
11092
11795
  } catch {
11093
- head = { access: { problem: "frontmatter could not be read" } };
11796
+ head = platform === "mintlify" ? { access: { problem: "frontmatter could not be read" } } : { reason: "frontmatter could not be read" };
11094
11797
  }
11798
+ if (!("access" in head)) return head.reason ? { reason: head.reason, quarantineReason: head.reason, openapi: head.openapi } : { openapi: head.openapi };
11095
11799
  const verdict = resolvePageAccess(head.access, navigationAccessFor(file.relativePath));
11096
11800
  if (verdict.problem) return { reason: verdict.problem, quarantineReason: verdict.problem, openapi: head.openapi };
11097
11801
  const access = verdict.access ?? {};
@@ -11144,9 +11848,38 @@ function migrateRepository(options) {
11144
11848
  withheldDocFiles.push(file);
11145
11849
  }
11146
11850
  }
11147
- const pagesNotClassified = droppedPageCount > 0 || platform === "mintlify" && scanTruncated;
11851
+ const fernBodyReason = (file) => {
11852
+ if (platform !== "fern") return void 0;
11853
+ try {
11854
+ return lstatSync6(file.absolutePath).size > MAX_PAGE_BYTES ? void 0 : fernIfRolesReason(readFileSync6(file.absolutePath, "utf8"));
11855
+ } catch {
11856
+ return "it could not be read to check for role-restricted content";
11857
+ }
11858
+ };
11859
+ const gateReasonCache = /* @__PURE__ */ new Map();
11860
+ const nonMintlifyGateReason = (file) => {
11861
+ if (!isDocFile(file)) return void 0;
11862
+ const key = canonicalPath(file.absolutePath);
11863
+ let reason = gateReasonCache.get(key);
11864
+ if (reason === void 0) {
11865
+ reason = platform === "docusaurus" && file.relativePath.split("/").some((segment) => segment.startsWith("_")) ? null : (fernRestrictedPaths.has(key) ? "docs.yml `viewers` restricts it to signed-in roles" : classifyPageGate(file).reason ?? fernBodyReason(file)) ?? null;
11866
+ gateReasonCache.set(key, reason);
11867
+ }
11868
+ return reason ?? void 0;
11869
+ };
11870
+ if (platform === "docusaurus" || platform === "fern") {
11871
+ for (const file of /* @__PURE__ */ new Set([...scannedFiles, ...files])) {
11872
+ const reason = nonMintlifyGateReason(file);
11873
+ if (!reason) continue;
11874
+ gateByPath.set(file.absolutePath, { reason, quarantineReason: reason });
11875
+ withheldPaths.add(file.absolutePath);
11876
+ }
11877
+ }
11878
+ const pagesNotClassified = droppedPageCount > 0 || scanTruncated || files.some((file) => isDocFile(file) && lstatSync6(file.absolutePath).size > MAX_PAGE_BYTES);
11148
11879
  const hasWithheldContent = withheldPaths.size > 0 || withheldDocFiles.length > 0;
11149
- const trackPublishedRefs = platform === "mintlify" && (hasWithheldContent || pagesNotClassified);
11880
+ const trackPublishedRefs = hasWithheldContent || pagesNotClassified;
11881
+ const docusaurusDocsPrefix = platform === "docusaurus" ? relative6(docusaurusProjectRoot ?? repositoryDir, contentRoot).replace(/\\/g, "/") : "";
11882
+ const docusaurusPaths = (siteBase) => platform === "docusaurus" ? { siteBase: posix4.normalize(siteBase).replace(/^\.$/, ""), docsPrefix: docusaurusDocsPrefix } : void 0;
11150
11883
  const withheldCanonical = new Set([...withheldPaths].map(canonicalPath));
11151
11884
  const verdictCache = /* @__PURE__ */ new Map();
11152
11885
  const classifyInlineCandidate = (candidate, key) => {
@@ -11177,7 +11910,11 @@ function migrateRepository(options) {
11177
11910
  verdictCache.set(key, verdict);
11178
11911
  }
11179
11912
  return verdict ?? void 0;
11180
- } : void 0;
11913
+ } : (candidate) => {
11914
+ const reason = nonMintlifyGateReason({ absolutePath: candidate, relativePath: relative6(contentRoot, candidate).replace(/\\/g, "/") });
11915
+ if (reason) return { kind: reason === DOCUSAURUS_DRAFT_REASON ? "draft" : "gated", reason };
11916
+ return lstatSync6(candidate).size > MAX_PAGE_BYTES ? { kind: "oversized", reason: "over 2 MB" } : void 0;
11917
+ };
11181
11918
  const fernIdRenames = /* @__PURE__ */ new Map();
11182
11919
  let skipped = 0;
11183
11920
  let discovered = files.length;
@@ -11239,7 +11976,7 @@ function migrateRepository(options) {
11239
11976
  continue;
11240
11977
  }
11241
11978
  const sourceSegments = file.relativePath.split("/");
11242
- if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename3(file.relativePath).startsWith("_")) {
11979
+ if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && sourceSegments.some((segment) => segment.startsWith("_"))) {
11243
11980
  skipped++;
11244
11981
  continue;
11245
11982
  }
@@ -11280,7 +12017,7 @@ function migrateRepository(options) {
11280
12017
  }
11281
12018
  const key = normalizedReferenceKey(file.relativePath);
11282
12019
  let pageGate;
11283
- if (platform === "mintlify") {
12020
+ {
11284
12021
  pageGate = gateByPath.get(file.absolutePath);
11285
12022
  if (pageGate?.access?.isPublic === true) sawPublicTrue = true;
11286
12023
  const gateReason = pageGate?.quarantineReason;
@@ -11310,7 +12047,7 @@ function migrateRepository(options) {
11310
12047
  }
11311
12048
  warnings.push({
11312
12049
  code: "gated-page",
11313
- message: `Access-restricted on the source site, but its access rules could not be migrated safely (${gateReason}), so it was NOT published. Fix the rules in the original, saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}, and move it into src/content/; until then links from other pages to it will break.`,
12050
+ message: gateReason === DOCUSAURUS_DRAFT_REASON ? `This is a draft page that Docusaurus does not publish, so it was NOT published. The original is saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}; links from other pages to it will break.` : platform === "mintlify" ? `Access-restricted on the source site, but its access rules could not be migrated safely (${gateReason}), so it was NOT published. Fix the rules in the original, saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}, and move it into src/content/; until then links from other pages to it will break.` : `Access-restricted on the source site (${gateReason}), so it was NOT published. The original is saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}; links from other pages to it will break.`,
11314
12051
  source: file.relativePath
11315
12052
  });
11316
12053
  continue;
@@ -11348,14 +12085,15 @@ function migrateRepository(options) {
11348
12085
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
11349
12086
  const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
11350
12087
  let raw = inlineMdxSnippets(
11351
- mintlifyProjectRoot ? importUndeclaredSnippetComponents(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, mintlifyProjectRoot, snippetExports, snippetAliases, inlineGate, warnings) : readFileSync6(file.absolutePath, "utf8"),
12088
+ mintlifyProjectRoot ? importUndeclaredSnippetComponents(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, mintlifyProjectRoot, snippetExports, snippetAliases, inlineGate, warnings) : injectDocusaurusGlobalImports(readFileSync6(file.absolutePath, "utf8"), globalDocusaurusImports),
11352
12089
  file.absolutePath,
11353
12090
  repositoryDir,
11354
12091
  warnings,
11355
12092
  0,
11356
12093
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir,
11357
12094
  snippetAliases,
11358
- inlineGate
12095
+ inlineGate,
12096
+ platform
11359
12097
  );
11360
12098
  const publishedText = trackPublishedRefs && !isRestrictedPage ? raw : void 0;
11361
12099
  const pageAssetReferences = [];
@@ -11476,6 +12214,7 @@ function migrateRepository(options) {
11476
12214
  });
11477
12215
  });
11478
12216
  }
12217
+ if (platform === "docusaurus") resolvePageDisplayHeading(page, platform);
11479
12218
  let mdxError = invalidMdxReason(page.body);
11480
12219
  if (mdxError) {
11481
12220
  const repaired = escapeUnmatchedClosingTags(page.body);
@@ -11543,7 +12282,7 @@ function migrateRepository(options) {
11543
12282
  pages.push(page);
11544
12283
  if (isRestrictedPage) restrictedPages.add(page);
11545
12284
  for (const assetPath of pageAssetReferences) addAssetReference(assetPath, file.relativePath);
11546
- if (publishedText !== void 0) addPathReferences(publishedText, posix4.dirname(file.relativePath).replace(/^\.$/, ""), publishedExact, publishedLoose);
12285
+ if (publishedText !== void 0) addPathReferences(publishedText, posix4.dirname(file.relativePath).replace(/^\.$/, ""), publishedExact, publishedLoose, docusaurusPaths(posix4.join(docusaurusDocsPrefix, posix4.dirname(file.relativePath))));
11547
12286
  if (hasDocusaurusDocCardList) {
11548
12287
  docusaurusDocCardRoutes.add(page.navigationId);
11549
12288
  }
@@ -11561,6 +12300,7 @@ function migrateRepository(options) {
11561
12300
  }
11562
12301
  }
11563
12302
  if (docusaurusDescriptor) {
12303
+ if (docusaurusDescriptor.unlisted) page.hidden = true;
11564
12304
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
11565
12305
  }
11566
12306
  }
@@ -11579,8 +12319,8 @@ function migrateRepository(options) {
11579
12319
  try {
11580
12320
  const absolutePath = source ? resolveWithinRoot(fernProjectRoot, source.replace(/^\/+/, ""), repositoryDir) : resolveWithin(repositoryDir, candidate);
11581
12321
  if (!existsSync5(absolutePath) || !lstatSync6(absolutePath).isFile()) return;
11582
- const realRoot = realpathSync3(repositoryDir);
11583
- const realPath = realpathSync3(absolutePath);
12322
+ const realRoot = realpathSync4(repositoryDir);
12323
+ const realPath = realpathSync4(absolutePath);
11584
12324
  const confined = relative6(realRoot, realPath);
11585
12325
  if (confined === ".." || confined.startsWith(`..${sep3}`) || isAbsolute3(confined)) return;
11586
12326
  const withinFern = relative6(fernProjectRoot, absolutePath);
@@ -11617,6 +12357,7 @@ function migrateRepository(options) {
11617
12357
  }));
11618
12358
  }) : [];
11619
12359
  const assetCandidates = [];
12360
+ const seenAssetSources = /* @__PURE__ */ new Map();
11620
12361
  const mintlifyFonts = platform === "mintlify" ? mintlifyFontSources(mintlifyConfig) : [];
11621
12362
  const fontRelativePaths = new Set(mintlifyFonts.flatMap((font) => font.path ? [font.path] : []));
11622
12363
  const importedComponentSources = new Set((componentMigrator?.files() ?? []).flatMap((file) => {
@@ -11663,6 +12404,11 @@ function migrateRepository(options) {
11663
12404
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
11664
12405
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
11665
12406
  if (!assetPath) continue;
12407
+ const sourceKey = canonicalPath(file.absolutePath);
12408
+ const destinations = seenAssetSources.get(sourceKey) ?? /* @__PURE__ */ new Set();
12409
+ if (destinations.has(assetPath)) continue;
12410
+ destinations.add(assetPath);
12411
+ seenAssetSources.set(sourceKey, destinations);
11666
12412
  if (siteKind) {
11667
12413
  siteAssetPaths.set(assetPath, siteKind);
11668
12414
  if (siteKind === "font") fontAssetByRelative.set(file.relativePath, assetPath);
@@ -11682,8 +12428,9 @@ function migrateRepository(options) {
11682
12428
  const destinationCounts = /* @__PURE__ */ new Map();
11683
12429
  for (const { assetPath } of assetCandidates) destinationCounts.set(assetPath, (destinationCounts.get(assetPath) ?? 0) + 1);
11684
12430
  if (trackPublishedRefs) {
11685
- const publishedConfig = JSON.stringify(mintlifyConfig ?? {}, (_key, value) => value && typeof value === "object" && !Array.isArray(value) && navigationGateReason(value) ? void 0 : value);
11686
- addPathReferences(publishedConfig, "", publishedExact, publishedLoose);
12431
+ const publishedConfig = platform === "mintlify" ? JSON.stringify(mintlifyConfig ?? {}, (_key, value) => value && typeof value === "object" && !Array.isArray(value) && navigationGateReason(value) ? void 0 : value) : JSON.stringify([docsConfig, docusaurusProjectRoot ? readDocusaurusSiteSettings(docusaurusProjectRoot) : null]);
12432
+ addPathReferences(publishedConfig, "", publishedExact, publishedLoose, docusaurusPaths(""));
12433
+ if (fernRawConfig) addFernConfigPathReferences(fernRawConfig, publishedExact, publishedLoose);
11687
12434
  }
11688
12435
  for (const file of componentMigrator?.files() ?? []) {
11689
12436
  if (!trackPublishedRefs || typeof file.content !== "string") continue;
@@ -11692,7 +12439,7 @@ function migrateRepository(options) {
11692
12439
  }
11693
12440
  const failClosedAssets = trackPublishedRefs;
11694
12441
  const isPublicReachable = (assetPath) => referencedAssetPaths.has(assetPath) || publishedExact.has(assetPath);
11695
- const isQuarantinedAsset = (assetPath) => (withheldAssetPaths.has(assetPath) || failClosedAssets && !siteAssetPaths.has(assetPath)) && platform === "mintlify" && !(isPublicReachable(assetPath) && !(failClosedAssets && (destinationCounts.get(assetPath) ?? 0) > 1));
12442
+ const isQuarantinedAsset = (assetPath) => (withheldAssetPaths.has(assetPath) || failClosedAssets && !siteAssetPaths.has(assetPath)) && !(isPublicReachable(assetPath) && !(failClosedAssets && (destinationCounts.get(assetPath) ?? 0) > 1));
11696
12443
  let withheldAssetCount = 0;
11697
12444
  let unreferencedAssetCount = 0;
11698
12445
  let totalAssetBytes = 0;
@@ -11913,7 +12660,7 @@ function migrateRepository(options) {
11913
12660
  }
11914
12661
  if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
11915
12662
  if (platform === "fern" && fernProjectRoot) {
11916
- const sections = fernApiSections.length > 0 ? fernApiSections : [{ nameExplicit: false }];
12663
+ const sections = fernApiSections.length > 0 ? fernApiSections : fernRestrictedApiSections.length > 0 ? [] : [{ nameExplicit: false }];
11917
12664
  const resolvedSpecs = [];
11918
12665
  const specFilenameSources = /* @__PURE__ */ new Map();
11919
12666
  for (const section of sections) {
@@ -11927,7 +12674,8 @@ function migrateRepository(options) {
11927
12674
  filename = `${prefix}${filename}`;
11928
12675
  }
11929
12676
  specFilenameSources.set(filename, spec.absolutePath);
11930
- const specContent = readFileSync6(spec.absolutePath);
12677
+ const specContent = readSpecFile(spec.absolutePath, spec.relativePath, warnings);
12678
+ if (!specContent) continue;
11931
12679
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
11932
12680
  assets.push(specAsset(filename, specContent));
11933
12681
  }
@@ -11959,6 +12707,19 @@ function migrateRepository(options) {
11959
12707
  message: checked.length ? `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API. Checked: ${checked.join(", ")}. Point generators.yml at a spec file that exists.` : `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API, because its api-name is not a valid folder name. Add the spec manually.`
11960
12708
  });
11961
12709
  }
12710
+ const publicSpecFiles = new Set(specFilenameSources.values());
12711
+ for (const section of fernRestrictedApiSections) {
12712
+ const spec = findFernConfiguredOpenApi(fernProjectRoot, repositoryDir, section.name, section.nameExplicit, warnings).spec ?? (!section.nameExplicit ? findOpenApi(files) : null);
12713
+ if (!spec || publicSpecFiles.has(spec.absolutePath)) continue;
12714
+ const content = readSpecFile(spec.absolutePath, spec.relativePath, warnings);
12715
+ if (!content) continue;
12716
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(basename3(spec.relativePath))}`, content });
12717
+ warnings.push({
12718
+ code: "gated-page",
12719
+ message: `The "${section.name ?? "API"}" API reference is limited to signed-in roles in Fern, so it was NOT published. Its spec was saved at ${QUARANTINE_DIRECTORY}/assets/${specAssetPath(basename3(spec.relativePath))}.`,
12720
+ source: spec.relativePath
12721
+ });
12722
+ }
11962
12723
  if (resolvedSpecs.length > 0) {
11963
12724
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
11964
12725
  const { operationLinks, prefixLandings } = apiOperationLinkMap(
@@ -12030,14 +12791,15 @@ function migrateRepository(options) {
12030
12791
  if (endpointRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...endpointRedirects] };
12031
12792
  } else {
12032
12793
  const fallback = findOpenApi(files);
12033
- if (fallback) {
12794
+ const fallbackContent = fallback ? readSpecFile(fallback.absolutePath, fallback.relativePath, warnings) : void 0;
12795
+ if (fallback && fallbackContent) {
12034
12796
  const filename = basename3(fallback.relativePath);
12035
12797
  const matches = (ref) => specRefMatches(ref, fallback.relativePath, filename, true);
12036
12798
  if (withheldSpecRefs.some(matches) && !publishedSpecRefs.some(matches)) {
12037
- quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: readFileSync6(fallback.absolutePath) });
12799
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: fallbackContent });
12038
12800
  withheldAssetCount++;
12039
12801
  } else {
12040
- const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: readFileSync6(fallback.absolutePath) }, withheldSpecRefs, keptSpecRefs);
12802
+ const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: fallbackContent }, withheldSpecRefs, keptSpecRefs);
12041
12803
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
12042
12804
  assets.push(specAsset(filename, pruned.content));
12043
12805
  }
@@ -12312,7 +13074,14 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
12312
13074
  }
12313
13075
  const isTopLevelDocusaurus = platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipRedirects;
12314
13076
  const docusaurusSettings = isTopLevelDocusaurus ? readDocusaurusSiteSettings(docusaurusProjectRoot) : void 0;
13077
+ const presentation = docusaurusSettings ? readDocusaurusPresentation(docusaurusProjectRoot) : void 0;
12315
13078
  if (docusaurusSettings) {
13079
+ if (presentation) {
13080
+ assets.push({ path: "_thally/source-theme.css", content: Buffer.from(presentation.css) });
13081
+ docsConfig = { ...docsConfig, stylesheets: [...docsConfig.stylesheets ?? [], "/_thally/source-theme.css"], ...presentation.headingFont ? { fonts: { ...docsConfig.fonts, heading: { family: presentation.headingFont, weight: ["400", "500", "600", "700"] } } } : {} };
13082
+ if (presentation.unresolved) warnings.push({ code: "unsupported-config", message: "Docusaurus Sass/Tailwind theme values could not be resolved statically; provide compiled build/assets/css/styles.*.css to retain those colors." });
13083
+ }
13084
+ if (docusaurusSettings.banner) docsConfig = { ...docsConfig, banner: docusaurusSettings.banner };
12316
13085
  const importedRoutes = new Set(pages.map((page) => page.navigationId));
12317
13086
  const sourceOrigin = readDocusaurusSiteOrigin(docusaurusProjectRoot);
12318
13087
  const externalized = /* @__PURE__ */ new Set();
@@ -12383,7 +13152,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
12383
13152
  if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
12384
13153
  docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
12385
13154
  }
12386
- const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
13155
+ const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) ?? (fernRawConfig["global-theme"] === "nvidia" ? { primary: "#76B900" } : void 0) : isTopLevelDocusaurus ? presentation?.colors ?? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
12387
13156
  const assetPaths = new Set(assets.map((asset) => asset.path));
12388
13157
  const publicBrandPath = (value) => {
12389
13158
  if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
@@ -12490,7 +13259,12 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
12490
13259
  message: (sawPublicTrue ? "Some pages set `public: true`, which means the source site used Mintlify authentication and every page WITHOUT it was private. " : "") + "Access control set in the Mintlify dashboard is not visible in the repository. Check the source site's dashboard access settings before publishing: until docs.json `auth` is configured, Thally publishes every imported page that has no `groups` or `public: false` to everyone. Confirm nothing here was meant to stay private before deploying."
12491
13260
  });
12492
13261
  }
12493
- if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
13262
+ if (typeof fernRawConfig?.["global-theme"] === "string") warnings.push({
13263
+ code: "unsupported-config",
13264
+ message: `Fern global theme ${JSON.stringify(fernRawConfig["global-theme"])} could not be fetched during local migration; local overrides and Thally default styling tokens were applied.`,
13265
+ source: "global-theme"
13266
+ });
13267
+ if (fernRawConfig?.logo && !fernRawConfig["global-theme"] && !logoLight && !logo.light) warnings.push({
12494
13268
  code: "unsupported-config",
12495
13269
  message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
12496
13270
  });
@@ -12541,6 +13315,7 @@ function renderPage(bundle, page) {
12541
13315
  return [
12542
13316
  "---",
12543
13317
  `title: ${yamlString(page.title)}`,
13318
+ page.headingTitle ? `headingTitle: ${yamlString(page.headingTitle)}` : null,
12544
13319
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
12545
13320
  page.icon ? `icon: ${yamlString(page.icon)}` : null,
12546
13321
  page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
@@ -12673,7 +13448,7 @@ import { lookup } from "dns/promises";
12673
13448
  import { request } from "https";
12674
13449
  import { isIP } from "net";
12675
13450
  import { parse as parseYaml7 } from "yaml";
12676
- var MAX_SPEC_BYTES = 25e6;
13451
+ var MAX_SPEC_BYTES2 = 25e6;
12677
13452
  var MAX_REDIRECTS = 3;
12678
13453
  function isPublicAddress(address, family) {
12679
13454
  if (family === 4) {
@@ -12724,7 +13499,7 @@ async function requestPinnedSpec(url) {
12724
13499
  return;
12725
13500
  }
12726
13501
  const statedBytes = Number(response.headers["content-length"] ?? 0);
12727
- if (statedBytes > MAX_SPEC_BYTES) {
13502
+ if (statedBytes > MAX_SPEC_BYTES2) {
12728
13503
  response.destroy();
12729
13504
  reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
12730
13505
  return;
@@ -12733,7 +13508,7 @@ async function requestPinnedSpec(url) {
12733
13508
  let bytes = 0;
12734
13509
  response.on("data", (chunk) => {
12735
13510
  bytes += chunk.length;
12736
- if (bytes > MAX_SPEC_BYTES) {
13511
+ if (bytes > MAX_SPEC_BYTES2) {
12737
13512
  response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
12738
13513
  return;
12739
13514
  }
@@ -12752,7 +13527,7 @@ async function downloadSpec(value, fetcher) {
12752
13527
  const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
12753
13528
  validatedSpecUrl(response.finalUrl.toString());
12754
13529
  const body = Buffer.from(response.body);
12755
- if (body.length > MAX_SPEC_BYTES) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
13530
+ if (body.length > MAX_SPEC_BYTES2) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
12756
13531
  return body;
12757
13532
  }
12758
13533
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {