@thallylabs/migrate 0.2.7 → 0.2.9

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
@@ -2,17 +2,21 @@
2
2
  import { compileSync as compileSync2 } from "@mdx-js/mdx";
3
3
  import { spawn } from "child_process";
4
4
  import {
5
+ closeSync,
5
6
  existsSync as existsSync5,
6
7
  lstatSync as lstatSync5,
8
+ openSync,
7
9
  readFileSync as readFileSync5,
8
- readdirSync,
10
+ readSync,
11
+ readdirSync as readdirSync2,
9
12
  realpathSync as realpathSync2,
10
13
  rmSync,
11
14
  statSync
12
15
  } from "fs";
13
16
  import { createRequire } from "module";
14
- import { basename as basename2, dirname as dirname4, extname as extname4, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
17
+ import { basename as basename3, dirname as dirname4, extname as extname5, isAbsolute as isAbsolute2, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
15
18
  import { parse as parseYaml4 } from "yaml";
19
+ import * as ts2 from "typescript";
16
20
 
17
21
  // src/components.ts
18
22
  import { createHash } from "crypto";
@@ -50,6 +54,8 @@ function parseFrontmatter(raw) {
50
54
 
51
55
  // src/mdx.ts
52
56
  import { compileSync } from "@mdx-js/mdx";
57
+ import { nameToEmoji } from "gemoji";
58
+ import remarkGfm from "remark-gfm";
53
59
  import remarkMdx from "remark-mdx";
54
60
  import remarkParse from "remark-parse";
55
61
  import { unified } from "unified";
@@ -125,7 +131,7 @@ function titleFromId(id) {
125
131
  if (value === "introduction") return "Introduction";
126
132
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
127
133
  }
128
- var descriptionParser = unified().use(remarkParse).use(remarkMdx);
134
+ var descriptionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
129
135
  function firstParagraph(content) {
130
136
  function plainText(node) {
131
137
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
@@ -159,19 +165,21 @@ function docusaurusAdmonitionTag(kind) {
159
165
  function normalizeDocusaurusAdmonitions(body) {
160
166
  const lines = body.split("\n");
161
167
  const openAdmonitions = [];
168
+ const headingLines = [];
162
169
  let codeFence = null;
163
- return lines.map((line) => {
164
- const codeMatch = line.match(/^\s*(`{3,}|~{3,})/);
170
+ const output = lines.map((line, index) => {
171
+ const codeMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/);
165
172
  if (codeMatch) {
166
- if (!codeFence) codeFence = codeMatch[1][0];
167
- else if (codeMatch[1][0] === codeFence) codeFence = null;
173
+ if (!codeFence) codeFence = codeMatch[1];
174
+ else if (codeMatch[1][0] === codeFence[0] && codeMatch[1].length >= codeFence.length && /^\s*$/.test(line.slice(codeMatch[0].length))) codeFence = null;
168
175
  return line;
169
176
  }
170
177
  if (codeFence) return line;
178
+ if (/^#{1,6}\s/.test(line)) headingLines.push(index);
171
179
  const opening = line.match(/^\s*(:{3,})(note|tip|info|warning|caution|danger)(?:\[([^\]]+)\]|\s+(.+))?\s*$/i);
172
180
  if (opening) {
173
181
  const tag = docusaurusAdmonitionTag(opening[2].toLowerCase());
174
- openAdmonitions.push({ delimiter: opening[1], tag });
182
+ openAdmonitions.push({ delimiter: opening[1], tag, openedAt: index });
175
183
  const title = (opening[3] ?? opening[4])?.trim();
176
184
  return title ? `<${tag}>
177
185
  **${title}**` : `<${tag}>`;
@@ -183,7 +191,74 @@ function normalizeDocusaurusAdmonitions(body) {
183
191
  return `</${current.tag}>`;
184
192
  }
185
193
  return line;
186
- }).join("\n");
194
+ });
195
+ const closingByLine = /* @__PURE__ */ new Map();
196
+ for (const open of openAdmonitions.reverse()) {
197
+ const at = headingLines.find((line) => line > open.openedAt) ?? output.length;
198
+ closingByLine.set(at, [...closingByLine.get(at) ?? [], `</${open.tag}>`]);
199
+ }
200
+ for (const [at, closing] of [...closingByLine].sort(([left], [right]) => right - left)) output.splice(at, 0, ...closing);
201
+ return output.join("\n");
202
+ }
203
+ function unwrapDocusaurusMdxCodeBlocks(body) {
204
+ const output = [];
205
+ let fence;
206
+ let isMdxCodeBlock = false;
207
+ for (const line of body.split("\n")) {
208
+ const opening = !fence && line.match(/^ {0,3}(`{3,}|~{3,})mdx-code-block\s*$/);
209
+ if (opening) {
210
+ fence = opening[1];
211
+ isMdxCodeBlock = true;
212
+ continue;
213
+ }
214
+ if (fence) {
215
+ const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
216
+ if (closing && closing[1][0] === fence[0] && closing[1].length >= fence.length) {
217
+ if (!isMdxCodeBlock) output.push(line);
218
+ fence = void 0;
219
+ isMdxCodeBlock = false;
220
+ continue;
221
+ }
222
+ if (isMdxCodeBlock && /^\s*(?:import|export)\b/.test(line)) return body;
223
+ output.push(line);
224
+ continue;
225
+ }
226
+ const ordinaryFence = line.match(/^ {0,3}(`{3,}|~{3,})/);
227
+ if (ordinaryFence) {
228
+ fence = ordinaryFence[1];
229
+ isMdxCodeBlock = false;
230
+ }
231
+ output.push(line);
232
+ }
233
+ return isMdxCodeBlock ? body : output.join("\n");
234
+ }
235
+ function normalizeExplicitHeadingIds(body) {
236
+ return replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
237
+ if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
238
+ const trimmed = line.trimEnd();
239
+ if (trimmed.endsWith("}")) {
240
+ const marker = trimmed.lastIndexOf(" {#");
241
+ if (marker >= 0) {
242
+ const id = trimmed.slice(marker + 3, -1);
243
+ if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) {
244
+ return `<a id="${id}"></a>
245
+ ${trimmed.slice(0, marker).trimEnd()}`;
246
+ }
247
+ }
248
+ }
249
+ if (trimmed.endsWith("*/}")) {
250
+ const marker = trimmed.lastIndexOf(" {/*");
251
+ if (marker >= 0) {
252
+ const comment = trimmed.slice(marker + 4, -3).trim();
253
+ const id = comment.startsWith("#") ? comment.slice(1) : "";
254
+ if (/^[A-Za-z][A-Za-z0-9_.:-]*$/.test(id)) {
255
+ return `<a id="${id}"></a>
256
+ ${trimmed.slice(0, marker).trimEnd()}`;
257
+ }
258
+ }
259
+ }
260
+ return line;
261
+ }).join("\n"));
187
262
  }
188
263
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
189
264
  "Tabs",
@@ -192,19 +267,11 @@ var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
192
267
  "DocCardList",
193
268
  "TOCInline"
194
269
  ]);
195
- function isGlobalDocusaurusImport(line) {
196
- const trimmed = line.trim().replace(/;$/, "").trimEnd();
197
- if (!trimmed.startsWith("import ")) return false;
198
- const separator = trimmed.indexOf(" from ", "import ".length);
199
- if (separator < 0) return false;
200
- const component = trimmed.slice("import ".length, separator).trim();
201
- if (!GLOBAL_DOCUSARUS_COMPONENTS.has(component)) return false;
202
- const source = trimmed.slice(separator + " from ".length);
203
- if (source.length < 3) return false;
204
- const quote = source[0];
205
- if (quote !== "'" && quote !== '"' || source.at(-1) !== quote) return false;
206
- const moduleName = source.slice(1, -1);
207
- return moduleName.startsWith("@theme/") || moduleName.startsWith("@docusaurus/");
270
+ function removeGlobalDocusaurusImports(body) {
271
+ return replaceOutsideCode(body, (segment) => segment.replace(
272
+ /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@(?:theme|docusaurus)\/[^'"]+)\2\s*;?/g,
273
+ (statement, component) => GLOBAL_DOCUSARUS_COMPONENTS.has(component) ? "" : statement
274
+ ));
208
275
  }
209
276
  var JS_LITERAL_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined"]);
210
277
  var BARE_IDENTIFIER_PATH = /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*(?:\.[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)*$/u;
@@ -221,6 +288,20 @@ var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
221
288
  "BigInt",
222
289
  "Reflect"
223
290
  ]);
291
+ function leadingPageHeading(body) {
292
+ try {
293
+ const root = descriptionParser.parse(body);
294
+ const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html");
295
+ if (first?.type !== "heading" || first.depth !== 1) return void 0;
296
+ const text = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text).join("");
297
+ const title = text(first).replace(/\s+/g, " ").trim();
298
+ const start = first.position?.start.offset;
299
+ const end = first.position?.end.offset;
300
+ return title && start !== void 0 && end !== void 0 ? { title, start, end } : void 0;
301
+ } catch {
302
+ return void 0;
303
+ }
304
+ }
224
305
  function collectPatternNames(pattern, names) {
225
306
  switch (pattern.type) {
226
307
  case "Identifier":
@@ -418,7 +499,11 @@ function escapeFernLiteralBraces(raw) {
418
499
  const end = node.position?.end.offset;
419
500
  const root2 = value.split(".")[0];
420
501
  const safeBuiltinPath = value.includes(".") && SAFE_BUILTIN_ROOTS.has(root2);
421
- if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
502
+ const isTemplatePlaceholder = /^\{\s*[A-Za-z_$][\w$.-]*\s*\}$/.test(value);
503
+ const isIssueList = /^[A-Za-z][A-Za-z0-9]*-\d+(?:\s*,\s*[A-Za-z][A-Za-z0-9]*-\d+)*$/.test(value);
504
+ if ((isTemplatePlaceholder || isIssueList) && start !== void 0 && end !== void 0) {
505
+ edits.push({ start, end, value: body.slice(start, end).replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
506
+ } else if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
422
507
  edits.push({ start, end, value: `\\{${value}\\}` });
423
508
  }
424
509
  return;
@@ -532,10 +617,10 @@ function maskCode(body) {
532
617
  let codeFence = null;
533
618
  let fenceBuffer = [];
534
619
  for (const line of source.split("\n")) {
535
- const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
620
+ const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/);
536
621
  if (codeFence) {
537
622
  fenceBuffer.push(line);
538
- if (fenceMatch && fenceMatch[1][0] === codeFence) {
623
+ if (fenceMatch && fenceMatch[1][0] === codeFence[0] && fenceMatch[1].length >= codeFence.length && /^\s*$/.test(line.slice(fenceMatch[0].length))) {
539
624
  lines.push(stash(fenceBuffer.join("\n")));
540
625
  codeFence = null;
541
626
  fenceBuffer = [];
@@ -543,7 +628,7 @@ function maskCode(body) {
543
628
  continue;
544
629
  }
545
630
  if (fenceMatch) {
546
- codeFence = fenceMatch[1][0];
631
+ codeFence = fenceMatch[1];
547
632
  fenceBuffer = [line];
548
633
  continue;
549
634
  }
@@ -565,14 +650,104 @@ function normalizeHtmlComments(body) {
565
650
  function normalizeFernCallouts(body) {
566
651
  return replaceOutsideCode(body, (whole) => {
567
652
  const openTags = [];
568
- return whole.replace(/<Callout\s+intent=(?:"([^"]*)"|'([^']*)')([^>]*?)(\/?)>/g, (_match, doubleQuoted, singleQuoted, rest, selfClose) => {
569
- const intent = (doubleQuoted ?? singleQuoted ?? "").toLowerCase();
570
- const tag = intent === "warning" ? "Warning" : intent === "success" || intent === "tip" ? "Tip" : intent === "error" || intent === "danger" ? "Error" : "Note";
571
- if (!selfClose) openTags.push(tag);
572
- return `<${tag}${rest}${selfClose}>`;
573
- }).replace(/<\/Callout>/g, (match) => openTags.length ? `</${openTags.pop()}>` : match);
653
+ const output = [];
654
+ let cursor = 0;
655
+ while (cursor < whole.length) {
656
+ const opening = whole.indexOf("<Callout", cursor);
657
+ const closing = whole.indexOf("</Callout>", cursor);
658
+ const next = opening < 0 ? closing : closing < 0 ? opening : Math.min(opening, closing);
659
+ if (next < 0) break;
660
+ output.push(whole.slice(cursor, next));
661
+ if (next === closing) {
662
+ const tag = openTags.pop();
663
+ output.push(tag ? `</${tag}>` : "</Callout>");
664
+ cursor = next + "</Callout>".length;
665
+ continue;
666
+ }
667
+ const attributeStart = next + "<Callout".length;
668
+ if (!/[\s/>]/.test(whole[attributeStart] ?? "")) {
669
+ output.push("<Callout");
670
+ cursor = attributeStart;
671
+ continue;
672
+ }
673
+ let end = attributeStart;
674
+ let quote = null;
675
+ let expressionDepth = 0;
676
+ for (; end < whole.length; end++) {
677
+ const character = whole[end];
678
+ if (quote) {
679
+ if (character === quote && whole[end - 1] !== "\\") quote = null;
680
+ } else if (character === '"' || character === "'") {
681
+ quote = character;
682
+ } else if (character === "{") {
683
+ expressionDepth++;
684
+ } else if (character === "}" && expressionDepth > 0) {
685
+ expressionDepth--;
686
+ } else if (character === ">" && expressionDepth === 0) {
687
+ break;
688
+ }
689
+ }
690
+ if (end === whole.length) {
691
+ output.push(whole.slice(next));
692
+ cursor = whole.length;
693
+ break;
694
+ }
695
+ const attributes = whole.slice(attributeStart, end);
696
+ const intent = findStaticCalloutIntent(attributes);
697
+ const isSelfClosing = attributes.trimEnd().endsWith("/");
698
+ if (!intent) {
699
+ output.push(whole.slice(next, end + 1));
700
+ if (!isSelfClosing) openTags.push(null);
701
+ } else {
702
+ const value = intent.value.toLowerCase();
703
+ const tag = value === "warning" ? "Warning" : value === "success" || value === "tip" ? "Tip" : value === "error" || value === "danger" ? "Error" : "Note";
704
+ output.push(`<${tag}${attributes.slice(0, intent.start)}${attributes.slice(intent.end)}>`);
705
+ if (!isSelfClosing) openTags.push(tag);
706
+ }
707
+ cursor = end + 1;
708
+ }
709
+ output.push(whole.slice(cursor));
710
+ return output.join("");
574
711
  });
575
712
  }
713
+ function findStaticCalloutIntent(attributes) {
714
+ let cursor = 0;
715
+ while (cursor < attributes.length) {
716
+ const start = cursor;
717
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
718
+ if (cursor === start) {
719
+ cursor++;
720
+ continue;
721
+ }
722
+ const nameStart = cursor;
723
+ while (/[A-Za-z0-9_-]/.test(attributes[cursor] ?? "")) cursor++;
724
+ const name = attributes.slice(nameStart, cursor);
725
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
726
+ if (attributes[cursor] !== "=") continue;
727
+ cursor++;
728
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
729
+ const quote = attributes[cursor];
730
+ if (quote !== '"' && quote !== "'") {
731
+ if (quote === "{") {
732
+ let depth = 0;
733
+ do {
734
+ if (attributes[cursor] === "{") depth++;
735
+ if (attributes[cursor] === "}") depth--;
736
+ cursor++;
737
+ } while (cursor < attributes.length && depth > 0);
738
+ } else {
739
+ while (cursor < attributes.length && !/\s/.test(attributes[cursor])) cursor++;
740
+ }
741
+ continue;
742
+ }
743
+ const valueStart = ++cursor;
744
+ while (cursor < attributes.length && attributes[cursor] !== quote) cursor++;
745
+ const value = attributes.slice(valueStart, cursor);
746
+ if (cursor < attributes.length) cursor++;
747
+ if (name === "intent") return { start, end: cursor, value };
748
+ }
749
+ return null;
750
+ }
576
751
  function unwrapFernCodeBlockTabs(body) {
577
752
  return replaceOutsideCode(body, (whole) => {
578
753
  let groupDepth = 0;
@@ -679,25 +854,31 @@ ${result}`;
679
854
  return result;
680
855
  }
681
856
  function normalizeDocusaurusTabs(body) {
682
- return body.replace(/<Tabs\b([^>]*)>([\s\S]*?)<\/Tabs>/g, (_match, attributes, inner) => {
683
- const labelByValue = /* @__PURE__ */ new Map();
684
- const valuesSource = attributes.match(/\bvalues=\{(\[[\s\S]*?\])\s*\}/)?.[1];
685
- if (valuesSource) {
686
- for (const entry of valuesSource.matchAll(/\{([^{}]*)\}/g)) {
687
- const label = entry[1].match(/\blabel:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
688
- const value = entry[1].match(/\bvalue:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
689
- if (value) labelByValue.set(value, label ?? value);
690
- }
691
- }
692
- const cleanedAttributes = attributes.replace(/\s*defaultValue=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g, "").replace(/\s*values=\{[\s\S]*?\]\s*\}/g, "").replace(/\s*groupId=(?:"[^"]*"|'[^']*')/g, "").replace(/\s*queryString(?:=(?:\{[^}]*\}|"[^"]*"|'[^']*'))?/g, "");
693
- const rewrittenInner = inner.replace(/<TabItem\b([^>]*)>/g, (_itemMatch, itemAttributes) => {
694
- const label = itemAttributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
695
- const value = itemAttributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
696
- const title = label ?? (value && labelByValue.get(value)) ?? value ?? "Tab";
697
- return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
698
- }).replace(/<\/TabItem>/g, "</Tab>");
699
- return `<Tabs${cleanedAttributes}>${rewrittenInner}</Tabs>`;
700
- });
857
+ let result = body;
858
+ for (let depth = 0; depth < 20; depth += 1) {
859
+ const updated = result.replace(/<Tabs\b([^>]*)>((?:(?!<Tabs\b)[\s\S])*?)<\/Tabs>/g, (_match, attributes, inner) => {
860
+ const labelByValue = /* @__PURE__ */ new Map();
861
+ const valuesSource = attributes.match(/\bvalues=\{(\[[\s\S]*?\])\s*\}/)?.[1];
862
+ if (valuesSource) {
863
+ for (const entry of valuesSource.matchAll(/\{([^{}]*)\}/g)) {
864
+ const label = entry[1].match(/\blabel:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
865
+ const value = entry[1].match(/\bvalue:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
866
+ if (value) labelByValue.set(value, label ?? value);
867
+ }
868
+ }
869
+ const cleanedAttributes = attributes.replace(/\s*defaultValue=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g, "").replace(/\s*values=\{[\s\S]*?\]\s*\}/g, "").replace(/\s*values=\{[^{}]*\}/g, "").replace(/\s*groupId=(?:"[^"]*"|'[^']*')/g, "").replace(/\s*queryString(?:=(?:\{[^}]*\}|"[^"]*"|'[^']*'))?/g, "");
870
+ const rewrittenInner = inner.replace(/<TabItem\b([^>]*)>/g, (_itemMatch, itemAttributes) => {
871
+ const label = itemAttributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
872
+ const value = itemAttributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
873
+ const title = label ?? (value && labelByValue.get(value)) ?? value ?? "Tab";
874
+ return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
875
+ }).replace(/<\/TabItem>/g, "</Tab>");
876
+ return `<ThallyTabs${cleanedAttributes}>${rewrittenInner}</ThallyTabs>`;
877
+ });
878
+ if (updated === result) break;
879
+ result = updated;
880
+ }
881
+ return result.replace(/<ThallyTabs\b/g, "<Tabs").replace(/<\/ThallyTabs>/g, "</Tabs>");
701
882
  }
702
883
  var TAB_FENCE_OPEN = /^(`{3,}|~{3,})(\S*)\s+tab\b(.*)$/;
703
884
  function normalizeDocusaurusTabBlocks(body) {
@@ -783,15 +964,72 @@ function convertHtmlStyleAttributes(body) {
783
964
  (_match, tag, before, _quote, styleText, after, selfClose) => `<${tag}${before} style={${cssTextToStyleObjectLiteral(styleText)}}${after}${selfClose ? " /" : ""}>`
784
965
  );
785
966
  }
786
- function normalizeMdx(body, platform) {
967
+ function normalizeMintlifyUpdateLabels(body) {
968
+ return body.replace(/(<Update\b[^>]*\blabel=)\{(<\>[\s\S]*?<\/\>)\}/g, (match, prefix, fragment) => {
969
+ let root;
970
+ try {
971
+ root = descriptionParser.parse(fragment);
972
+ } catch {
973
+ return match;
974
+ }
975
+ const plainText = (node) => {
976
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
977
+ if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" || node.type === "mdxjsEsm") return null;
978
+ const parts = (node.children ?? []).map(plainText);
979
+ return parts.some((part) => part === null) ? null : parts.join("");
980
+ };
981
+ const label = plainText(root)?.replace(/\s+/g, " ").trim();
982
+ return label && !/[{}]/.test(label) ? `${prefix}{${JSON.stringify(label)}}` : match;
983
+ });
984
+ }
985
+ function normalizeMintlifyParameterAnchors(body) {
986
+ const seen = /* @__PURE__ */ new Set();
987
+ const anchorFor = (name) => {
988
+ const id = `param-${name.toLowerCase().replace(/[._]/g, "-")}`;
989
+ if (seen.has(id)) return "";
990
+ seen.add(id);
991
+ return `<a id="${id}"></a>
992
+ `;
993
+ };
994
+ let fence;
995
+ const withHeadings = body.split("\n").map((line) => {
996
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
997
+ if (marker) {
998
+ if (!fence) fence = marker[1];
999
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = void 0;
1000
+ return line;
1001
+ }
1002
+ if (fence) return line;
1003
+ const heading = line.match(/^ {0,3}#{1,6}\s+`([A-Za-z][A-Za-z0-9._-]*)`(?:\s|$)/);
1004
+ return heading ? `${anchorFor(heading[1])}${line}` : line;
1005
+ }).join("\n");
1006
+ return replaceOutsideCode(withHeadings, (segment) => segment.replace(
1007
+ /<(ResponseField|ParamField)\b([^>]*?)\bname=(?:"([A-Za-z][A-Za-z0-9._-]*)"|'([A-Za-z][A-Za-z0-9._-]*)')([^>]*)>/g,
1008
+ (match, _tag, _before, doubleQuoted, singleQuoted) => `${anchorFor(doubleQuoted ?? singleQuoted)}${match}`
1009
+ ));
1010
+ }
1011
+ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
787
1012
  const runFern = platform === "fern";
788
1013
  const runMintlify = platform === "mintlify";
789
1014
  const runDocusaurus = platform === "docusaurus";
790
- let rewritten = normalizeDocusaurusAdmonitions(runFern ? normalizeFernCallouts(body) : body);
1015
+ const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1016
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
791
1017
  if (runDocusaurus) {
792
- const withoutGlobalImports = rewritten.split("\n").filter((line) => !isGlobalDocusaurusImport(line)).join("\n");
793
- rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(withoutGlobalImports));
1018
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1019
+ /:([+\w-]+):/g,
1020
+ (shortcode, name) => nameToEmoji[name] ?? shortcode
1021
+ ));
1022
+ rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1023
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1024
+ /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
1025
+ (statement, binding, _quote, _source, offset) => {
1026
+ const withoutStatement = segment.slice(0, offset) + segment.slice(offset + statement.length);
1027
+ return new RegExp(`\\b${binding}\\s*(?:\\.|\\[|\\})`).test(withoutStatement) ? statement : "";
1028
+ }
1029
+ ));
1030
+ rewritten = rewritten.replace(/^\n{2,}/, "\n");
794
1031
  }
1032
+ if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
795
1033
  rewritten = replaceOutsideCode(rewritten, (segment) => {
796
1034
  let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${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>");
797
1035
  if (runDocusaurus) {
@@ -801,7 +1039,7 @@ function normalizeMdx(body, platform) {
801
1039
  }).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, "");
802
1040
  }
803
1041
  if (runMintlify) {
804
- result = 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>");
1042
+ 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>");
805
1043
  }
806
1044
  if (runFern) {
807
1045
  result = result.replace(/<CodeBlocks(\s[^>]*)?>/g, "<CodeGroup$1>").replace(/<\/CodeBlocks>/g, "</CodeGroup>").replace(/<ParameterField\b/g, "<ParamField").replace(/<\/ParameterField>/g, "</ParamField>").replace(/<Cards(\s[^>]*)?>/g, "<CardGroup$1>").replace(/<\/Cards>/g, "</CardGroup>").replace(/<Success(\s[^>]*)?>/g, "<Tip$1>").replace(/<\/Success>/g, "</Tip>").replace(/<Launch(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Launch>/g, "</Note>").replace(/<Files(\s[^>]*)?>\n?/g, "").replace(/<\/Files>/g, "");
@@ -809,7 +1047,15 @@ function normalizeMdx(body, platform) {
809
1047
  return result;
810
1048
  });
811
1049
  if (runFern) rewritten = unwrapFernCodeBlockTabs(rewritten);
812
- return provideScopedGlobalReferences(rewritten);
1050
+ const normalized = provideScopedGlobalReferences(rewritten);
1051
+ if (runDocusaurus && sourceBody !== body) {
1052
+ try {
1053
+ compileSync(normalized, { outputFormat: "program" });
1054
+ } catch {
1055
+ return normalizeMdx(body, platform, false);
1056
+ }
1057
+ }
1058
+ return normalized;
813
1059
  }
814
1060
  function parseMarkdownPage(input) {
815
1061
  const parsed = parseFrontmatter(input.raw);
@@ -819,9 +1065,13 @@ function parseMarkdownPage(input) {
819
1065
  ...input.locale ? { locale: input.locale } : {}
820
1066
  };
821
1067
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
822
- const body = normalizeMdx(parsed.content, input.platform).trim();
1068
+ let body = normalizeMdx(parsed.content, input.platform).trim();
823
1069
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
824
- const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(identity.navigationId);
1070
+ const heading = leadingPageHeading(body);
1071
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
1072
+ if (heading && heading.title === title) {
1073
+ body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
1074
+ }
825
1075
  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;
826
1076
  const badge = typeof parsed.data.tag === "string" && parsed.data.tag.trim() ? parsed.data.tag.trim() : typeof parsed.data.badge === "string" && parsed.data.badge.trim() ? parsed.data.badge.trim() : void 0;
827
1077
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
@@ -834,6 +1084,7 @@ function parseMarkdownPage(input) {
834
1084
  title,
835
1085
  navTitle,
836
1086
  description,
1087
+ ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
837
1088
  badge,
838
1089
  keywords,
839
1090
  mode,
@@ -919,6 +1170,73 @@ function normalizeAssetPath(value) {
919
1170
  }
920
1171
 
921
1172
  // src/components.ts
1173
+ function normalizeIndentedFences(content) {
1174
+ const lines = content.split("\n");
1175
+ let active;
1176
+ let ordinaryFence;
1177
+ let indentedJsx;
1178
+ let listIndent;
1179
+ return lines.map((line) => {
1180
+ const trimmed = line.trimStart();
1181
+ if (indentedJsx) {
1182
+ const normalized2 = line.startsWith(indentedJsx.indent) ? line.slice(indentedJsx.indent.length) : line;
1183
+ if (normalized2.trimStart().startsWith(`</${indentedJsx.tag}>`)) indentedJsx = void 0;
1184
+ return normalized2;
1185
+ }
1186
+ if (ordinaryFence) {
1187
+ const closing2 = trimmed.match(/^(`{3,}|~{3,})\s*$/)?.[1];
1188
+ if (closing2 && closing2[0] === ordinaryFence[0] && closing2.length >= ordinaryFence.length) ordinaryFence = void 0;
1189
+ return line;
1190
+ }
1191
+ if (!active) {
1192
+ const listItem = line.match(/^([ \t]*)(?:[-*+]|\d+[.)])\s/);
1193
+ const indentation = line.length - trimmed.length;
1194
+ if (listItem) listIndent = listItem[1].length;
1195
+ else if (trimmed && listIndent !== void 0 && indentation <= listIndent) listIndent = void 0;
1196
+ const jsx = line.match(/^([ \t]{2,})<([A-Z][\w.]*)\b[^>]*>\s*$/);
1197
+ if (jsx && listIndent !== void 0 && jsx[1].length > listIndent && !trimmed.endsWith("/>")) {
1198
+ indentedJsx = { tag: jsx[2], indent: jsx[1] };
1199
+ return line.slice(jsx[1].length);
1200
+ }
1201
+ const match = line.match(/^([ \t]{4,})(`{3,}|~{3,})/);
1202
+ if (match) {
1203
+ active = { indent: match[1], marker: match[2] };
1204
+ return line.slice(match[1].length);
1205
+ }
1206
+ const ordinary = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/);
1207
+ if (ordinary) ordinaryFence = ordinary[1];
1208
+ return line;
1209
+ }
1210
+ const normalized = line.startsWith(active.indent) ? line.slice(active.indent.length) : line;
1211
+ const closing = normalized.trimStart().match(/^(`{3,}|~{3,})\s*$/)?.[1];
1212
+ if (closing && closing[0] === active.marker[0] && closing.length >= active.marker.length) active = void 0;
1213
+ return normalized;
1214
+ }).join("\n");
1215
+ }
1216
+ function staticServerHelper(path, imported, local) {
1217
+ if (imported === "default") return null;
1218
+ const parsed = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1219
+ const isLiteral = (node) => {
1220
+ if (ts.isParenthesizedExpression(node)) return isLiteral(node.expression);
1221
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return true;
1222
+ if ([ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return true;
1223
+ if (ts.isPrefixUnaryExpression(node)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(node.operator) && ts.isNumericLiteral(node.operand);
1224
+ if (ts.isArrayLiteralExpression(node)) return node.elements.every((element) => ts.isExpression(element) && isLiteral(element));
1225
+ if (ts.isObjectLiteralExpression(node)) return node.properties.every((property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) || ts.isNumericLiteral(property.name)) && isLiteral(property.initializer));
1226
+ return false;
1227
+ };
1228
+ for (const statement of parsed.statements) {
1229
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
1230
+ for (const declaration of statement.declarationList.declarations) {
1231
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== imported || !declaration.initializer || !ts.isArrowFunction(declaration.initializer)) continue;
1232
+ const arrow = declaration.initializer;
1233
+ if (arrow.parameters.length > 0) continue;
1234
+ const result = ts.isBlock(arrow.body) ? arrow.body.statements.length === 1 && ts.isReturnStatement(arrow.body.statements[0]) ? arrow.body.statements[0].expression : void 0 : arrow.body;
1235
+ if (result && isLiteral(result)) return `export const ${local} = () => (${result.getText(parsed)});`;
1236
+ }
1237
+ }
1238
+ return null;
1239
+ }
922
1240
  var YOUTUBE_VIDEO_ID = /^[\w-]{1,64}$/;
923
1241
  var YOUTUBE_ID_PACKAGES = /* @__PURE__ */ new Set(["react-lite-youtube-embed", "react-youtube", "@justinribeiro/lite-youtube"]);
924
1242
  var YOUTUBE_URL = /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{1,64})/i;
@@ -1474,7 +1792,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1474
1792
  function transform(raw, currentFile) {
1475
1793
  const parsedFrontmatter = parseFrontmatter(raw).content;
1476
1794
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
1477
- const content = normalizeHtmlComments(parsedFrontmatter);
1795
+ const content = normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter));
1478
1796
  let tree;
1479
1797
  try {
1480
1798
  tree = parser.parse(content);
@@ -1488,6 +1806,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1488
1806
  const declarations = [];
1489
1807
  const moduleImports = [];
1490
1808
  const realPageImports = [];
1809
+ const serverPageDeclarations = [];
1491
1810
  const sharedImportEdits = [];
1492
1811
  let hasUnsupportedImports = false;
1493
1812
  function hasExpressionReference(names, options = {}) {
@@ -1553,13 +1872,19 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1553
1872
  const rawSpecifier = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
1554
1873
  const specifier = rawSpecifier.startsWith("@site/") ? `/${rawSpecifier.slice("@site/".length)}` : rawSpecifier;
1555
1874
  if (isScaffoldProvidedImport(specifier)) {
1875
+ if (specifier === "react" && bindings.length > 0 && bindings.every((binding) => REACT_GLOBALS.has(binding.imported)) && !hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
1876
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1877
+ continue;
1878
+ }
1556
1879
  moduleImports.push(statement.getText(ast));
1557
1880
  continue;
1558
1881
  }
1559
1882
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
1560
1883
  if (SHARED_IMPORTS.has(specifier)) {
1561
- moduleImports.push(statement.getText(ast));
1562
- sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1884
+ const isUnusedReactHookImport = specifier === "react" && bindings.length > 0 && bindings.every((binding) => REACT_GLOBALS.has(binding.imported)) && !hasExpressionReference(new Set(bindings.map((binding) => binding.local)));
1885
+ if (!isUnusedReactHookImport) moduleImports.push(statement.getText(ast));
1886
+ if (isUnusedReactHookImport) edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1887
+ else sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1563
1888
  } else if (specifier.startsWith("@theme/") || specifier.startsWith("@docusaurus/")) {
1564
1889
  edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1565
1890
  } else if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
@@ -1595,18 +1920,19 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1595
1920
  hasUnsupportedImports = true;
1596
1921
  continue;
1597
1922
  }
1598
- if (bindings.some((binding) => !/^[A-Z]/.test(binding.local))) {
1599
- warn("MDX imports containing non-component values require manual migration; the import was preserved.", currentFile);
1600
- hasUnsupportedImports = true;
1601
- continue;
1602
- }
1603
1923
  try {
1604
- const path = copyGraph(resolveDependency(specifier, currentFile));
1924
+ const sourcePath = resolveDependency(specifier, currentFile);
1925
+ const path = copyGraph(sourcePath);
1605
1926
  const isSvgUsedAsTag = extname(path).toLowerCase() === ".svg" && bindings.some((binding) => usedAsJsxTagName(binding.local));
1606
1927
  for (const binding of bindings) {
1607
1928
  const registerPath = isSvgUsedAsTag ? wrapSvgAsComponent(path) : path;
1608
1929
  const registerImported = isSvgUsedAsTag ? "default" : binding.imported;
1609
1930
  if (!isSvgUsedAsTag && hasExpressionReference(/* @__PURE__ */ new Set([binding.local]))) {
1931
+ const helper = staticServerHelper(sourcePath, binding.imported, binding.local);
1932
+ if (helper) {
1933
+ serverPageDeclarations.push(helper);
1934
+ continue;
1935
+ }
1610
1936
  const aliasSpecifier = portableSpecifier(`@/${registerPath.replace(/^src\//, "").replace(/\\/g, "/")}`);
1611
1937
  realPageImports.push(`import { ${registerImported} as ${binding.local} } from ${JSON.stringify(aliasSpecifier)};`);
1612
1938
  continue;
@@ -1617,6 +1943,15 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1617
1943
  }
1618
1944
  edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1619
1945
  } catch (error) {
1946
+ if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
1947
+ warnings.push({
1948
+ code: "skipped-file",
1949
+ message: `This page was excluded because local import ${JSON.stringify(rawSpecifier)} could not be copied and is used in an expression: ${error instanceof Error ? error.message : "unsupported dependency"}.`,
1950
+ source: relative2(root, currentFile).replace(/\\/g, "/")
1951
+ });
1952
+ hasUnsupportedImports = true;
1953
+ continue;
1954
+ }
1620
1955
  const failedPackage = error instanceof Error ? error.message.match(/^external package (\S+) requires manual installation and review$/)?.[1] : void 0;
1621
1956
  if (failedPackage && YOUTUBE_ID_PACKAGES.has(failedPackage)) {
1622
1957
  for (const binding of bindings) unsupportedImports.set(binding.local, failedPackage);
@@ -1758,9 +2093,64 @@ ${inlineSource}` });
1758
2093
  }
1759
2094
  edits.push(...declarations.map(({ start, end }) => ({ start, end, value: "" })), ...sharedImportEdits);
1760
2095
  }
2096
+ const pageFunctions = functionDeclaredNames(content);
2097
+ const passiveRoots = [];
2098
+ const wrappedRoots = /* @__PURE__ */ new Set();
2099
+ walk(tree, (node) => {
2100
+ if (!node.name || !aliases.has(node.name) || !functionValuedAttribute(node, pageFunctions) || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
2101
+ if (passiveRoots.some((root2) => node.position.start.offset >= root2.position.start.offset && node.position.end.offset <= root2.position.end.offset)) return;
2102
+ const functionAttributes = (node.attributes ?? []).filter((attribute) => {
2103
+ const value = attribute.value && typeof attribute.value === "object" ? attribute.value.value?.trim() : void 0;
2104
+ return !!value && pageFunctions.has(value);
2105
+ });
2106
+ if (functionAttributes.length === 1 && functionAttributes[0].name && functionAttributes[0].position?.start.offset !== void 0 && functionAttributes[0].position.end.offset !== void 0) {
2107
+ const functionName = functionAttributes[0].value.value.trim();
2108
+ const declaration = declarations.find((entry) => new RegExp(`^export const ${functionName}\\s*=`).test(entry.source));
2109
+ const safeDeclaration = declaration?.source.match(/^export const \w+\s*=\s*\(\s*\{\s*children(?:\s*,\s*\.\.\.props)?\s*\}\s*\)\s*=>\s*\([\s\S]*\)\s*;?$/) && !/\b(?:window|document|process|fetch|use[A-Z]\w*)\b/.test(declaration.source) && [...declaration.source.matchAll(/<([A-Z]\w*)\b/g)].every((match) => match[1] === "CodeBlock") && [...declaration.source.matchAll(/\{([^{}]*)\}/g)].every((match) => ["children", "...props", "children, ...props"].includes(match[1].trim()));
2110
+ const binding = registrations.get(aliases.get(node.name));
2111
+ if (safeDeclaration && binding) {
2112
+ const fallbackDeclaration = declaration.source.replace(/<CodeBlock\b[^>]*>/g, "<pre><code>").replace(/<\/CodeBlock>/g, "</code></pre>");
2113
+ const wrapperPath = `${destinationRoot}/callback-${hash(`${currentFile}:${node.position.start.offset}`)}.jsx`;
2114
+ const imported = binding.imported === "default" ? "default" : binding.imported;
2115
+ const importClause = imported === "default" ? "import Original" : `import { ${imported} as Original }`;
2116
+ const specifier = portableSpecifier(`./${relative2(destinationRoot, binding.path).replace(/\\/g, "/")}`);
2117
+ const wrapperName = register(wrapperPath, "CallbackWrapper");
2118
+ copied.set(wrapperPath, { path: wrapperPath, content: [
2119
+ "'use client';",
2120
+ `${importClause} from ${JSON.stringify(specifier)};`,
2121
+ fallbackDeclaration,
2122
+ `export function CallbackWrapper({ children, ...props }) { return <Original {...props} ${functionAttributes[0].name}={${functionName}}>{children}</Original>; }`
2123
+ ].join("\n") });
2124
+ const start = node.position.start.offset;
2125
+ const end = node.position.end.offset;
2126
+ const source2 = content.slice(start, end);
2127
+ const opening = source2.indexOf(`<${node.name}`);
2128
+ const closing = source2.lastIndexOf(`</${node.name}`);
2129
+ if (opening >= 0) edits.push({ start: start + opening + 1, end: start + opening + 1 + node.name.length, value: wrapperName });
2130
+ if (closing >= 0) edits.push({ start: start + closing + 2, end: start + closing + 2 + node.name.length, value: wrapperName });
2131
+ edits.push({ start: functionAttributes[0].position.start.offset, end: functionAttributes[0].position.end.offset, value: "" });
2132
+ wrappedRoots.add(node);
2133
+ if (declaration.source.includes("<CodeBlock")) warn(`Interactive <${node.name}> was retained with a basic code block in place of Mintlify's CodeBlock.`, currentFile);
2134
+ return;
2135
+ }
2136
+ }
2137
+ const source = content.slice(node.position.start.offset, node.position.end.offset);
2138
+ const openingEnd = source.indexOf(">");
2139
+ const closingStart = source.lastIndexOf(`</${node.name}`);
2140
+ const children = openingEnd >= 0 && closingStart > openingEnd ? source.slice(openingEnd + 1, closingStart) : "";
2141
+ edits.push({
2142
+ start: node.position.start.offset,
2143
+ end: node.position.end.offset,
2144
+ value: `<div data-migration-interactive-fallback="${node.name}">${children}</div>`
2145
+ });
2146
+ passiveRoots.push(node);
2147
+ warn(`Interactive <${node.name}> passes a page-local function into a client component; its child content was retained without the interactive control. Move the callback into a client component to restore it.`, currentFile);
2148
+ });
1761
2149
  walk(tree, (node) => {
1762
2150
  const replacement = node.name ? aliases.get(node.name) : void 0;
1763
2151
  if (!replacement || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
2152
+ if (wrappedRoots.has(node)) return;
2153
+ if (passiveRoots.some((root2) => node.position.start.offset >= root2.position.start.offset && node.position.end.offset <= root2.position.end.offset)) return;
1764
2154
  const start = node.position.start.offset;
1765
2155
  const end = node.position.end.offset;
1766
2156
  if (extracted.some((item) => start >= item.node.position.start.offset && end <= item.node.position.end.offset)) return;
@@ -1780,7 +2170,8 @@ ${inlineSource}` });
1780
2170
  edits.push({ start, end, value });
1781
2171
  });
1782
2172
  const rendered = applyReplacements(content, edits);
1783
- const realImportsBlock = realPageImports.length ? `${[...new Set(realPageImports)].join("\n")}
2173
+ const pageStatements = [.../* @__PURE__ */ new Set([...realPageImports, ...serverPageDeclarations])];
2174
+ const realImportsBlock = pageStatements.length ? `${pageStatements.join("\n")}
1784
2175
 
1785
2176
  ` : "";
1786
2177
  return frontmatter + realImportsBlock + rendered;
@@ -2404,6 +2795,7 @@ function projectMintlifyNavigation(config, options = {}) {
2404
2795
  }) : [];
2405
2796
  let tabs = [];
2406
2797
  let i18n;
2798
+ const localizedNavigation = {};
2407
2799
  const projectionTrace = {};
2408
2800
  if (languages.length > 0) {
2409
2801
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
@@ -2439,8 +2831,11 @@ function projectMintlifyNavigation(config, options = {}) {
2439
2831
  if (language === defaultLanguage) {
2440
2832
  tabs = languageTabs;
2441
2833
  for (const reference of references) defaultPageIds.add(reference.navigationId);
2834
+ } else if (languageTabs.length > 0) {
2835
+ localizedNavigation[locale] = languageTabs;
2442
2836
  }
2443
2837
  }
2838
+ if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
2444
2839
  } else {
2445
2840
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
2446
2841
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
@@ -2598,11 +2993,18 @@ function isDocumentationExtension(filename) {
2598
2993
  return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
2599
2994
  }
2600
2995
  function pruneMissingNavigationPages(config, availableIds) {
2601
- const pruneNodes = (nodes) => nodes.flatMap((node) => {
2602
- if (typeof node === "string") return availableIds.has(node) ? [node] : [];
2603
- const pages = pruneNodes(node.pages);
2604
- return pages.length > 0 ? [{ ...node, pages }] : [];
2605
- });
2996
+ const pruneNodes = (nodes) => {
2997
+ const seenSiblings = /* @__PURE__ */ new Set();
2998
+ return nodes.flatMap((node) => {
2999
+ if (typeof node === "string") {
3000
+ if (!availableIds.has(node) || seenSiblings.has(node)) return [];
3001
+ seenSiblings.add(node);
3002
+ return [node];
3003
+ }
3004
+ const pages = pruneNodes(node.pages);
3005
+ return pages.length > 0 ? [{ ...node, pages }] : [];
3006
+ });
3007
+ };
2606
3008
  const tabs = config.tabs.flatMap((tab) => {
2607
3009
  const hadPages = (tab.pages?.length ?? 0) > 0 || (tab.groups?.length ?? 0) > 0;
2608
3010
  const pages = tab.pages ? pruneNodes(tab.pages) : void 0;
@@ -2695,7 +3097,7 @@ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
2695
3097
  function sourceReferenceKey(value) {
2696
3098
  return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
2697
3099
  }
2698
- function rewriteDocusaurusLinks(body, current, descriptors) {
3100
+ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
2699
3101
  const routes = /* @__PURE__ */ new Map();
2700
3102
  for (const descriptor of descriptors) {
2701
3103
  const sourceKey = sourceReferenceKey(descriptor.sourcePath);
@@ -2711,26 +3113,52 @@ function rewriteDocusaurusLinks(body, current, descriptors) {
2711
3113
  const suffixIndex = target.search(/[?#]/);
2712
3114
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
2713
3115
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
2714
- const candidates = path.startsWith("/") ? [sourceReferenceKey(path), sourceReferenceKey(path).replace(/^docs\//, "")] : [
3116
+ const candidates = path.startsWith("/") ? [
3117
+ sourceReferenceKey(path),
3118
+ sourceReferenceKey(path).replace(/^docs\//, ""),
3119
+ sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
3120
+ ] : [
2715
3121
  sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
2716
3122
  sourceReferenceKey(path)
2717
3123
  ];
2718
3124
  const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
2719
- if (!route) return target;
3125
+ if (!route) {
3126
+ if (path.startsWith("/") && path !== "/" && options.sourceOrigin) {
3127
+ options.onExternalLink?.(target);
3128
+ return new URL(target, options.sourceOrigin).toString();
3129
+ }
3130
+ return target;
3131
+ }
2720
3132
  return `${route === "introduction" ? "/" : `/${route}`}${suffix}`;
2721
3133
  }
2722
3134
  let codeFence = null;
2723
3135
  return body.split("\n").map((line) => {
2724
- const fence = line.match(/^\s*(`{3,}|~{3,})/);
3136
+ const fence = line.match(/^\s{0,3}(`{3,}|~{3,})/);
2725
3137
  if (fence) {
2726
- if (!codeFence) codeFence = fence[1][0];
2727
- else if (fence[1][0] === codeFence) codeFence = null;
3138
+ if (!codeFence) codeFence = fence[1];
3139
+ else if (fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length && /^\s*$/.test(line.slice(fence[0].length))) codeFence = null;
2728
3140
  return line;
2729
3141
  }
2730
3142
  if (codeFence) return line;
2731
- return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`);
3143
+ return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
3144
+ const bracketed = destination.startsWith("<") && destination.endsWith(">");
3145
+ const target = bracketed ? destination.slice(1, -1) : destination;
3146
+ const rewritten = rewriteTarget(target);
3147
+ return `${prefix}${bracketed ? `<${rewritten}>` : rewritten}`;
3148
+ });
2732
3149
  }).join("\n");
2733
3150
  }
3151
+ function readDocusaurusSiteOrigin(repositoryRoot) {
3152
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
3153
+ if (!configPath) return void 0;
3154
+ const address = readBoundedText(configPath).match(/\burl['"]?\s*:\s*(['"])(https?:\/\/[^'"]+)\1/)?.[2];
3155
+ if (!address) return void 0;
3156
+ try {
3157
+ return new URL(address).origin;
3158
+ } catch {
3159
+ return void 0;
3160
+ }
3161
+ }
2734
3162
  function readBoundedText(path) {
2735
3163
  if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
2736
3164
  throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
@@ -2892,6 +3320,14 @@ function configuredSidebarPath(repositoryRoot) {
2892
3320
  const path = resolveWithin(repositoryRoot, filename);
2893
3321
  if (!existsSync3(path)) continue;
2894
3322
  const source = readBoundedText(path);
3323
+ for (const docs of source.matchAll(/\bdocs\s*:\s*/g)) {
3324
+ const object = matchingObjectLiteral(source, (docs.index ?? 0) + docs[0].length);
3325
+ const candidate2 = object && staticStringField(object, "sidebarPath")?.replace(/^\.\//, "");
3326
+ if (candidate2) {
3327
+ const resolved2 = resolveWithin(repositoryRoot, candidate2);
3328
+ if (existsSync3(resolved2) && lstatSync3(resolved2).isFile()) return candidate2;
3329
+ }
3330
+ }
2895
3331
  const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
2896
3332
  if (!match) continue;
2897
3333
  const candidate = match[2].replace(/^\.\//, "");
@@ -2900,9 +3336,12 @@ function configuredSidebarPath(repositoryRoot) {
2900
3336
  }
2901
3337
  return null;
2902
3338
  }
2903
- function readDocusaurusSidebars(repositoryRoot) {
2904
- const configured = configuredSidebarPath(repositoryRoot);
2905
- const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync3(resolveWithin(repositoryRoot, candidate)));
3339
+ function readDocusaurusSidebars(repositoryRoot, versionedSidebarPath) {
3340
+ const configured = versionedSidebarPath ?? configuredSidebarPath(repositoryRoot);
3341
+ const sourcePath = [configured, ...versionedSidebarPath ? [] : SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => {
3342
+ const path = resolveWithin(repositoryRoot, candidate);
3343
+ return existsSync3(path) && lstatSync3(path).isFile();
3344
+ });
2906
3345
  if (!sourcePath) return null;
2907
3346
  const absolutePath = resolveWithin(repositoryRoot, sourcePath);
2908
3347
  const source = readBoundedText(absolutePath);
@@ -3070,15 +3509,156 @@ function readDocusaurusThemeColor(repositoryRoot) {
3070
3509
  if (lightMode) colors.dark = lightMode;
3071
3510
  return Object.keys(colors).length > 0 ? colors : void 0;
3072
3511
  }
3073
- function readDocusaurusBrandAssetPaths(repositoryRoot) {
3512
+ function propertyStart(source, name) {
3513
+ const pattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${name}["']?\\s*:\\s*`, "m");
3514
+ const match = pattern.exec(source);
3515
+ return match ? match.index + match[0].length : -1;
3516
+ }
3517
+ function staticStringBindings(source) {
3518
+ const bindings = /* @__PURE__ */ new Map();
3519
+ for (const match of source.matchAll(/\bconst\s+([A-Z][A-Z0-9_]*)\s*=\s*((?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))/g)) {
3520
+ try {
3521
+ const value = JSON5.parse(match[2]);
3522
+ if (typeof value === "string") bindings.set(match[1], value);
3523
+ } catch {
3524
+ }
3525
+ }
3526
+ return bindings;
3527
+ }
3528
+ function staticArrayField(source, name) {
3529
+ const start = propertyStart(source, name);
3530
+ return start < 0 ? null : matchingArrayLiteral(source, start);
3531
+ }
3532
+ function staticObjectEntries(array) {
3533
+ if (!array) return [];
3534
+ const entries = [];
3535
+ let afterDelimiter = 1;
3536
+ let quote = "";
3537
+ let lineComment = false;
3538
+ let blockComment = false;
3539
+ for (let index = 1; index < array.length - 1; index++) {
3540
+ const char = array[index];
3541
+ const next = array[index + 1];
3542
+ if (lineComment) {
3543
+ if (char === "\n") lineComment = false;
3544
+ continue;
3545
+ }
3546
+ if (blockComment) {
3547
+ if (char === "*" && next === "/") {
3548
+ blockComment = false;
3549
+ index++;
3550
+ }
3551
+ continue;
3552
+ }
3553
+ if (quote) {
3554
+ if (char === "\\") {
3555
+ index++;
3556
+ continue;
3557
+ }
3558
+ if (char === quote) quote = "";
3559
+ continue;
3560
+ }
3561
+ if (char === "/" && next === "/") {
3562
+ lineComment = true;
3563
+ index++;
3564
+ continue;
3565
+ }
3566
+ if (char === "/" && next === "*") {
3567
+ blockComment = true;
3568
+ index++;
3569
+ continue;
3570
+ }
3571
+ if (char === '"' || char === "'" || char === "`") {
3572
+ quote = char;
3573
+ continue;
3574
+ }
3575
+ if (char === ",") {
3576
+ afterDelimiter = index + 1;
3577
+ continue;
3578
+ }
3579
+ if (char !== "{") continue;
3580
+ const literal = matchingObjectLiteral(array, index);
3581
+ if (!literal) continue;
3582
+ const lead = array.slice(afterDelimiter, index).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim();
3583
+ if (!lead) entries.push(literal);
3584
+ index += literal.length - 1;
3585
+ }
3586
+ return entries;
3587
+ }
3588
+ function staticStringField(source, name, bindings) {
3589
+ const start = propertyStart(source, name);
3590
+ if (start < 0) return void 0;
3591
+ const quoted = source.slice(start).match(/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/);
3592
+ if (!quoted) {
3593
+ const binding = source.slice(start).match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
3594
+ return binding ? bindings?.get(binding) : void 0;
3595
+ }
3596
+ try {
3597
+ const parsed = JSON5.parse(quoted[1]);
3598
+ return typeof parsed === "string" ? parsed : void 0;
3599
+ } catch {
3600
+ return void 0;
3601
+ }
3602
+ }
3603
+ function readDocusaurusSiteSettings(repositoryRoot) {
3604
+ const empty = { docsRouteBasePath: "docs", navbarLinks: [], footerLinks: [] };
3074
3605
  const configPath = findDocusaurusConfigPath(repositoryRoot);
3075
- if (!configPath) return [];
3076
- const configSource = readBoundedText(configPath);
3077
- const favicon = configSource.match(/\bfavicon\s*:\s*(['"])([^'"]+)\1/)?.[2];
3078
- const logoBlock = configSource.match(/\blogo\s*:\s*\{([^}]*)\}/)?.[1];
3079
- const logo = logoBlock?.match(/\bsrc\s*:\s*(['"])([^'"]+)\1/)?.[2];
3080
- const logoDark = logoBlock?.match(/\bsrcDark\s*:\s*(['"])([^'"]+)\1/)?.[2];
3081
- return [favicon, logo, logoDark].filter((path) => typeof path === "string");
3606
+ if (!configPath) return empty;
3607
+ const source = readBoundedText(configPath);
3608
+ const bindings = staticStringBindings(source);
3609
+ const presets = staticArrayField(source, "presets");
3610
+ const classicDocs = presets && [...presets.matchAll(/["']?docs["']?\s*:\s*/g)].map((match) => matchingObjectLiteral(presets, (match.index ?? 0) + match[0].length)).find((object) => Boolean(object));
3611
+ const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
3612
+ const themeStart = propertyStart(source, "themeConfig");
3613
+ const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
3614
+ const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
3615
+ const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
3616
+ const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
3617
+ const logo = logoStart < 0 ? null : matchingObjectLiteral(navbar, logoStart);
3618
+ const footerStart = theme ? propertyStart(theme, "footer") : -1;
3619
+ const footer = footerStart < 0 ? null : matchingObjectLiteral(theme, footerStart);
3620
+ const navbarLinks = navbar ? staticObjectEntries(staticArrayField(navbar, "items")).flatMap((item) => {
3621
+ if (propertyStart(item, "items") >= 0 || propertyStart(item, "dropdownItemsAfter") >= 0) return [];
3622
+ const docsPluginId = staticStringField(item, "docsPluginId", bindings);
3623
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3624
+ const docId = staticStringField(item, "docId", bindings);
3625
+ const sidebarId = staticStringField(item, "sidebarId", bindings);
3626
+ const label = staticStringField(item, "label", bindings) ?? staticStringField(item, "aria-label", bindings);
3627
+ return label && (href || docId || sidebarId || docsPluginId) ? [{
3628
+ label,
3629
+ ...href ? { href } : {},
3630
+ ...docId ? { docId } : {},
3631
+ ...sidebarId ? { sidebarId } : {},
3632
+ ...docsPluginId ? { docsPluginId } : {}
3633
+ }] : [];
3634
+ }) : [];
3635
+ const footerLinks = footer ? staticObjectEntries(staticArrayField(footer, "links")).flatMap((column) => {
3636
+ const heading = staticStringField(column, "title", bindings);
3637
+ if (!heading) return [];
3638
+ const items = staticObjectEntries(staticArrayField(column, "items")).flatMap((item) => {
3639
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3640
+ const label = staticStringField(item, "label", bindings);
3641
+ return label && href ? [{ label, href }] : [];
3642
+ });
3643
+ return items.length > 0 ? [{ heading, items }] : [];
3644
+ }) : [];
3645
+ 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;
3646
+ return {
3647
+ name: staticStringField(source, "title"),
3648
+ description: staticStringField(source, "tagline"),
3649
+ docsRouteBasePath,
3650
+ ...logo && staticStringField(logo, "src") ? {
3651
+ logo: {
3652
+ light: staticStringField(logo, "src"),
3653
+ ...staticStringField(logo, "srcDark") ? { dark: staticStringField(logo, "srcDark") } : {},
3654
+ showTitle: Boolean(navbar && staticStringField(navbar, "title"))
3655
+ }
3656
+ } : {},
3657
+ ...staticStringField(source, "favicon") ? { favicon: staticStringField(source, "favicon") } : {},
3658
+ navbarLinks,
3659
+ footerLinks,
3660
+ ...copyright ? { copyright } : {}
3661
+ };
3082
3662
  }
3083
3663
  function autogeneratedItems(dirName, context) {
3084
3664
  const normalizedDir = dirName === "." ? "" : normalizeDocId(dirName);
@@ -3165,19 +3745,19 @@ function projectDocusaurusNavigation(input) {
3165
3745
  ]]];
3166
3746
  const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
3167
3747
  const converted = convertItems(value, context);
3168
- const loosePages = converted.filter((entry) => typeof entry === "string");
3169
- const groups = converted.filter((entry) => typeof entry !== "string");
3170
- if (loosePages.length > 0) groups.unshift({ group: "Overview", pages: loosePages });
3171
- if (groups.length === 0) return [];
3748
+ if (converted.length === 0) return [];
3172
3749
  return [{
3173
3750
  tab: sidebarEntries.length === 1 ? "Documentation" : titleCase2(sidebarId),
3174
- groups
3751
+ // Docusaurus categories are disclosures, including at the sidebar
3752
+ // root. Keeping them as Thally root nodes preserves their collapsed
3753
+ // state and the authored interleaving of documents and categories.
3754
+ pages: converted
3175
3755
  }];
3176
3756
  });
3177
3757
  const unreferenced = input.descriptors.filter((page) => !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
3178
3758
  if (unreferenced.length > 0) {
3179
- if (tabs.length === 0) tabs.push({ tab: "Documentation", groups: [] });
3180
- tabs[0].groups?.push({ group: "Additional", pages: unreferenced });
3759
+ if (tabs.length === 0) tabs.push({ tab: "Documentation", pages: [] });
3760
+ tabs[0].pages?.push({ group: "Additional", pages: unreferenced });
3181
3761
  }
3182
3762
  return {
3183
3763
  docsConfig: { tabs },
@@ -3188,10 +3768,37 @@ function projectDocusaurusNavigation(input) {
3188
3768
  }
3189
3769
 
3190
3770
  // src/fern.ts
3191
- import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4 } from "fs";
3192
- import { dirname as dirname3, relative as relative4 } from "path";
3771
+ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
3772
+ import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3773
+ import { load } from "cheerio";
3193
3774
  import { parse as parseYaml3 } from "yaml";
3194
3775
  var MAX_CONFIG_BYTES2 = 2e6;
3776
+ function announcementMarkdown(message) {
3777
+ const document = load(message);
3778
+ document("script, style, iframe, object, embed, svg, math").remove();
3779
+ const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([\\`*_\[\]{}()!])/g, "\\$1");
3780
+ const links = /* @__PURE__ */ new Map();
3781
+ document("a").each((index, element) => {
3782
+ const marker = `THALLYANNOUNCEMENTLINK${index}TOKEN`;
3783
+ const label = document(element).text().trim();
3784
+ const rawHref = document(element).attr("href")?.trim() ?? "";
3785
+ let href;
3786
+ if (/^https?:\/\//i.test(rawHref)) {
3787
+ try {
3788
+ href = new URL(rawHref).toString();
3789
+ } catch {
3790
+ }
3791
+ } else if (rawHref.startsWith("/") && !rawHref.startsWith("//") && !/[\\<>\s]/.test(rawHref)) {
3792
+ href = rawHref;
3793
+ }
3794
+ const destination = href?.replace(/\(/g, "%28").replace(/\)/g, "%29");
3795
+ links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
3796
+ document(element).replaceWith(marker);
3797
+ });
3798
+ let text = escapeText(document("body").text());
3799
+ for (const [marker, link] of links) text = text.replaceAll(marker, link);
3800
+ return text.trim();
3801
+ }
3195
3802
  function objectValue3(value) {
3196
3803
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3197
3804
  }
@@ -3243,8 +3850,9 @@ function fernLabelSlug(label) {
3243
3850
  }
3244
3851
  function segmentFor(value, label, context) {
3245
3852
  if (value["skip-slug"] === true) return null;
3246
- const explicit = typeof value.slug === "string" ? value.slug.trim() : "";
3247
- if (!explicit) return fernLabelSlug(label) || null;
3853
+ if (typeof value.slug !== "string") return fernLabelSlug(label) || null;
3854
+ const explicit = value.slug.trim();
3855
+ if (!explicit) return null;
3248
3856
  const slugified = fernBasicSlug(explicit);
3249
3857
  if (context && explicit !== slugified && FERN_SEGMENT_ALIAS.test(explicit)) {
3250
3858
  context.segmentAliases.set(slugified, explicit);
@@ -3257,28 +3865,99 @@ function uniqueNavigationId(base, context) {
3257
3865
  while (context.seenNavigationIds.has(`${base}-${index}`)) index++;
3258
3866
  return `${base}-${index}`;
3259
3867
  }
3260
- function registerPageAt(rawPath, base, context) {
3261
- const relativePath = rawPath.trim().replace(/^\.\//, "");
3262
- const sourcePath = context.pathPrefix ? `${context.pathPrefix}/${relativePath}` : relativePath;
3868
+ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
3869
+ let sourcePath;
3870
+ try {
3871
+ const absolute = resolveWithinRoot(
3872
+ context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
3873
+ rawPath.trim(),
3874
+ context.repositoryRoot
3875
+ );
3876
+ sourcePath = relative4(context.fernRoot, absolute).replace(/\\/g, "/");
3877
+ } catch {
3878
+ warnOnce2(context, `fern-unsafe-page-${rawPath}`, `Fern page path "${rawPath}" escapes the repository and was skipped.`);
3879
+ return "";
3880
+ }
3263
3881
  const navigationId = uniqueNavigationId(base || "introduction", context);
3264
3882
  context.seenNavigationIds.add(navigationId);
3265
- context.descriptors.push({ sourcePath, navigationId });
3883
+ const isHidden = hidden || context.hiddenInherited === true;
3884
+ context.descriptors.push({ sourcePath, navigationId, ...navTitle ? { navTitle } : {}, ...isHidden ? { hidden: true } : {} });
3266
3885
  const kept = context.sourcePathToNavigationId.get(sourcePath);
3267
- if (kept) return kept;
3886
+ if (kept) {
3887
+ if (!isHidden) {
3888
+ const owner = context.descriptors.find((descriptor) => descriptor.navigationId === kept);
3889
+ if (owner) owner.hidden = false;
3890
+ }
3891
+ return kept;
3892
+ }
3268
3893
  context.sourcePathToNavigationId.set(sourcePath, navigationId);
3269
3894
  return navigationId;
3270
3895
  }
3896
+ function registerFolder(object, parentSegments, context) {
3897
+ const rawPath = String(object.folder).trim();
3898
+ let folder;
3899
+ try {
3900
+ folder = resolveWithinRoot(
3901
+ context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
3902
+ rawPath,
3903
+ context.repositoryRoot
3904
+ );
3905
+ } catch {
3906
+ warnOnce2(context, `fern-unsafe-folder-${rawPath}`, `Fern folder "${rawPath}" escapes the repository and was skipped.`);
3907
+ return null;
3908
+ }
3909
+ if (!existsSync4(folder) || !lstatSync4(folder).isDirectory()) {
3910
+ warnOnce2(context, `fern-missing-folder-${rawPath}`, `Fern folder "${rawPath}" does not exist and was skipped.`);
3911
+ return null;
3912
+ }
3913
+ const label = typeof object.title === "string" && object.title.trim() ? object.title.trim() : titleCase3(basename2(folder));
3914
+ const segment = typeof object.slug === "string" || object["skip-slug"] === true ? segmentFor(object, label, context) : fernBasicSlug(basename2(folder));
3915
+ const segments = segment ? [...parentSegments, segment] : parentSegments;
3916
+ const visit = (directory, routeSegments, depth) => {
3917
+ if (depth > 12 || context.descriptors.length >= 5e3) return [];
3918
+ const pages2 = [];
3919
+ 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));
3920
+ for (const entry of entries) {
3921
+ const path = resolveWithin(directory, entry.name);
3922
+ if (entry.isDirectory()) {
3923
+ const nested = visit(path, [...routeSegments, fernBasicSlug(entry.name)], depth + 1);
3924
+ if (nested.length) pages2.push({ group: titleCase3(entry.name), pages: nested });
3925
+ } else if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) {
3926
+ const stem = basename2(entry.name, extname4(entry.name));
3927
+ const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
3928
+ const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
3929
+ const source = lstatSync4(path).size <= MAX_CONFIG_BYTES2 ? readFileSync4(path, "utf8") : "";
3930
+ const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
3931
+ const hidden = /^hidden:\s*true\s*$/m.test(frontmatter);
3932
+ const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" }, void 0, hidden);
3933
+ if (registered && !hidden && !context.hiddenInherited) pages2.push(registered);
3934
+ }
3935
+ }
3936
+ return pages2;
3937
+ };
3938
+ const pages = visit(folder, segments, 0);
3939
+ if (!pages.length) return null;
3940
+ addBareRouteRedirect(segments, pages, context);
3941
+ return { group: label, pages };
3942
+ }
3271
3943
  function registerPage(object, parentSegments, context) {
3272
3944
  if (typeof object.path !== "string" || !object.path.trim()) return null;
3273
3945
  const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
3274
3946
  const segment = segmentFor(object, label, context);
3275
3947
  const base = [...parentSegments, segment].filter(Boolean).join("/");
3276
- return registerPageAt(object.path, base, context);
3948
+ return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
3277
3949
  }
3278
3950
  function convertNode(node, parentSegments, context) {
3279
3951
  const object = objectValue3(node);
3280
3952
  if (!object) return null;
3281
- if (typeof object.page === "string") return registerPage(object, parentSegments, context);
3953
+ if (typeof object.page === "string") {
3954
+ const page = registerPage(object, parentSegments, context);
3955
+ return object.hidden === true || context.hiddenInherited ? null : page;
3956
+ }
3957
+ if (typeof object.folder === "string") {
3958
+ const folder = registerFolder(object, parentSegments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
3959
+ return object.hidden === true || context.hiddenInherited ? null : folder;
3960
+ }
3282
3961
  if (typeof object.section === "string") {
3283
3962
  const label = object.section;
3284
3963
  const segment = segmentFor(object, label, context);
@@ -3287,14 +3966,16 @@ function convertNode(node, parentSegments, context) {
3287
3966
  const pages = [];
3288
3967
  const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
3289
3968
  if (hasOwnPath) {
3290
- pages.push(registerPageAt(object.path, segments.join("/"), context));
3969
+ const registered = registerPageAt(object.path, segments.join("/"), context, label, object.hidden === true);
3970
+ if (registered) pages.push(registered);
3291
3971
  }
3292
3972
  for (const child of contents) {
3293
- const converted = convertNode(child, segments, context);
3973
+ const converted = convertNode(child, segments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
3294
3974
  if (converted) pages.push(converted);
3295
3975
  }
3296
3976
  if (pages.length === 0) return null;
3297
3977
  if (!hasOwnPath && segment) addBareRouteRedirect(segments, pages, context);
3978
+ if (object.hidden === true || context.hiddenInherited) return null;
3298
3979
  return {
3299
3980
  group: label,
3300
3981
  ...typeof object.icon === "string" ? { icon: object.icon } : {},
@@ -3319,8 +4000,7 @@ function convertNode(node, parentSegments, context) {
3319
4000
  return null;
3320
4001
  }
3321
4002
  if (typeof object.changelog === "string") {
3322
- warnOnce2(context, "fern-changelog", "Fern changelog sections are not supported and were omitted.");
3323
- return null;
4003
+ return registerFolder({ folder: object.changelog, title: "Changelog", "skip-slug": true }, parentSegments, context);
3324
4004
  }
3325
4005
  return null;
3326
4006
  }
@@ -3364,6 +4044,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
3364
4044
  }
3365
4045
  const versionConfig = objectValue3(readBoundedYaml(versionPath));
3366
4046
  if (!versionConfig) return config;
4047
+ context.pathPrefix = relative4(fernRoot, dirname3(versionPath)).replace(/\\/g, "/");
3367
4048
  return { ...config, navigation: versionConfig.navigation, tabs: versionConfig.tabs ?? config.tabs };
3368
4049
  } catch (error) {
3369
4050
  warnOnce2(
@@ -3392,6 +4073,43 @@ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallba
3392
4073
  for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
3393
4074
  context.apiSections[index].tabLabel ??= label;
3394
4075
  }
4076
+ if (groups.length === 0 && typeof meta.href === "string" && /^https?:\/\//i.test(meta.href)) {
4077
+ return [{ tab: label, href: meta.href }];
4078
+ }
4079
+ if (groups.length === 0 && typeof meta.changelog === "string") {
4080
+ const changelog = registerFolder({ folder: meta.changelog, title: label, "skip-slug": true }, segments, context);
4081
+ if (changelog) {
4082
+ const indexRoute = segments.join("/");
4083
+ const descriptors = new Map(context.descriptors.map((descriptor) => [descriptor.navigationId, descriptor]));
4084
+ const visiblePages = changelog.pages.filter((page) => typeof page === "string").filter((page) => !descriptors.get(page)?.hidden);
4085
+ const index = visiblePages.find((page) => {
4086
+ const descriptor = descriptors.get(page);
4087
+ if (!descriptor) return false;
4088
+ const stem = basename2(descriptor.sourcePath, extname4(descriptor.sourcePath));
4089
+ return page === indexRoute || stem === "overview" || stem === "index";
4090
+ });
4091
+ const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
4092
+ const date = (page) => descriptors.get(page)?.sourcePath.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
4093
+ return date(right).localeCompare(date(left)) || left.localeCompare(right);
4094
+ });
4095
+ for (const entryId of entries) {
4096
+ const descriptor = descriptors.get(entryId);
4097
+ if (descriptor) descriptor.hidden = true;
4098
+ }
4099
+ changelog.pages = [index ?? indexRoute];
4100
+ context.changelogIndexes.push({ route: indexRoute, entries });
4101
+ const latest = index ?? firstLeafId(changelog.pages);
4102
+ const bareRouteIndex = context.bareRouteRedirects.findIndex((redirect) => redirect.source === `/${indexRoute}`);
4103
+ const indexDescriptor = index ? descriptors.get(index) : void 0;
4104
+ const indexSource = indexDescriptor && lstatSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot)).size <= MAX_CONFIG_BYTES2 ? readFileSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot), "utf8") : "";
4105
+ const indexFrontmatter = indexSource.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
4106
+ const indexSlug = indexFrontmatter.match(/^slug:\s*["']?([^\s"']+)["']?\s*$/m)?.[1]?.replace(/^\/+|\/+$/g, "");
4107
+ const hasBarePage = index === indexRoute || indexSlug === indexRoute;
4108
+ if (bareRouteIndex >= 0 && hasBarePage) context.bareRouteRedirects.splice(bareRouteIndex, 1);
4109
+ else if (latest && bareRouteIndex >= 0) context.bareRouteRedirects[bareRouteIndex].destination = `/${latest}`;
4110
+ return [{ tab: label, groups: [changelog] }];
4111
+ }
4112
+ }
3395
4113
  if (groups.length === 0) return [];
3396
4114
  if (segments.length > 0) addBareRouteRedirect(segments, groups, context);
3397
4115
  return [{
@@ -3463,9 +4181,12 @@ function projectFernProducts(rawProducts, fernRoot, context) {
3463
4181
  }
3464
4182
  function projectFernNavigation(input) {
3465
4183
  const context = {
4184
+ fernRoot: input.fernRoot,
4185
+ repositoryRoot: input.repositoryRoot ?? input.fernRoot,
3466
4186
  descriptors: [],
3467
4187
  seenNavigationIds: /* @__PURE__ */ new Set(),
3468
4188
  apiSections: [],
4189
+ changelogIndexes: [],
3469
4190
  warnings: [],
3470
4191
  warningKeys: /* @__PURE__ */ new Set(),
3471
4192
  bareRouteRedirects: [],
@@ -3488,6 +4209,9 @@ function projectFernNavigation(input) {
3488
4209
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
3489
4210
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
3490
4211
  }) : [];
4212
+ const announcement = objectValue3(config.announcement);
4213
+ const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
4214
+ const bannerContent = announcementMarkdown(announcementMessage);
3491
4215
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
3492
4216
  const redirect = objectValue3(value);
3493
4217
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -3509,13 +4233,6 @@ function projectFernNavigation(input) {
3509
4233
  ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
3510
4234
  }];
3511
4235
  }) : [];
3512
- if (config.logo || config.favicon) {
3513
- warnOnce2(
3514
- context,
3515
- "fern-branding",
3516
- "Fern logo and favicon were not migrated; add them to public/brand/ (default-logo-light.svg, default-logo-dark.svg, default-favicon-light.svg, default-favicon-dark.svg)."
3517
- );
3518
- }
3519
4236
  const authoredSources = new Set(redirects.map((redirect) => redirect.source));
3520
4237
  const knownIds = new Set(context.descriptors.map((descriptor) => descriptor.navigationId));
3521
4238
  const keptNavigationId = (descriptor) => context.sourcePathToNavigationId.get(descriptor.sourcePath) ?? descriptor.navigationId;
@@ -3559,12 +4276,14 @@ function projectFernNavigation(input) {
3559
4276
  return {
3560
4277
  docsConfig: {
3561
4278
  tabs,
4279
+ ...bannerContent ? { banner: { content: bannerContent, dismissible: true } } : {},
3562
4280
  ...navbarLinks.length > 0 ? { navbar: { links: navbarLinks } } : {},
3563
4281
  ...allRedirects.length > 0 ? { redirects: allRedirects } : {}
3564
4282
  },
3565
4283
  descriptors: context.descriptors,
3566
4284
  warnings: context.warnings,
3567
- apiSections: disambiguatedApiSections
4285
+ apiSections: disambiguatedApiSections,
4286
+ changelogIndexes: context.changelogIndexes
3568
4287
  };
3569
4288
  }
3570
4289
 
@@ -3618,7 +4337,8 @@ var REPOSITORY_ONLY_DOCUMENTS = /* @__PURE__ */ new Set([
3618
4337
  "security.md"
3619
4338
  ]);
3620
4339
  var SNIPPET_DIRECTORIES = /* @__PURE__ */ new Set(["snippets", "_snippets", "partials", "_partials"]);
3621
- var SNIPPET_IMPORT_PATTERN = /^import\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?(?:\s*\/\/.*)?$/gm;
4340
+ 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;
4341
+ var SNIPPET_VALUE_IMPORT_PATTERN = /\bimport\s*\{([^{}]+)\}\s*from\s+['"]([^'"]+\.mdx?)['"]\s*;?/g;
3622
4342
  var SNIPPET_TAG_PATTERN = /<Snippet\s+file=(?:"([^"]+)"|'([^']+)')\s*(?:\/>|>[\s\S]*?<\/Snippet>)/g;
3623
4343
  var MINTIGNORE_FILENAME = ".mintignore";
3624
4344
  var createIgnoreMatcher = createRequire(import.meta.url)("ignore");
@@ -3664,7 +4384,7 @@ function countMarkdownPages(directory, limit = 2e4) {
3664
4384
  const current = stack.pop();
3665
4385
  let entries;
3666
4386
  try {
3667
- entries = readdirSync(current, { withFileTypes: true });
4387
+ entries = readdirSync2(current, { withFileTypes: true });
3668
4388
  } catch {
3669
4389
  continue;
3670
4390
  }
@@ -3683,7 +4403,7 @@ function pickPreferredDocsRoot(candidates, repositoryDir, platformLabel, warning
3683
4403
  const ranked = [...candidates].sort((left, right) => {
3684
4404
  const byRank = (rank?.(right) ?? 0) - (rank?.(left) ?? 0);
3685
4405
  if (byRank !== 0) return byRank;
3686
- const byName = (basename2(right) === "docs" ? 1 : 0) - (basename2(left) === "docs" ? 1 : 0);
4406
+ const byName = (basename3(right) === "docs" ? 1 : 0) - (basename3(left) === "docs" ? 1 : 0);
3687
4407
  if (byName !== 0) return byName;
3688
4408
  return countMarkdownPages(right) - countMarkdownPages(left);
3689
4409
  });
@@ -3717,7 +4437,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir, warnings) {
3717
4437
  visited++;
3718
4438
  if (hasMintlifyConfig(current.directory)) candidates.push(current.directory);
3719
4439
  if (current.depth >= 4) continue;
3720
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4440
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3721
4441
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3722
4442
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
3723
4443
  }
@@ -3749,7 +4469,7 @@ function findFernProjectRoot(repositoryDir, docsDir) {
3749
4469
  visited++;
3750
4470
  if (hasFernConfig(current.directory)) return current.directory;
3751
4471
  if (current.depth >= 4) continue;
3752
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4472
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3753
4473
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3754
4474
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
3755
4475
  }
@@ -3775,7 +4495,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir, warnings) {
3775
4495
  visited++;
3776
4496
  if (hasDocusaurusConfig(current.directory)) candidates.push(current.directory);
3777
4497
  if (current.depth >= 4) continue;
3778
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4498
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3779
4499
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3780
4500
  queue.push({
3781
4501
  directory: resolveWithin(current.directory, entry.name),
@@ -3796,6 +4516,52 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
3796
4516
  const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
3797
4517
  return trimTrailingSlashes(configured?.replace(/^\.\//, "") ?? "") || "docs";
3798
4518
  }
4519
+ function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
4520
+ const archives = [];
4521
+ const relativeRoot = relative5(repositoryDir, projectRoot).replace(/\\/g, "/");
4522
+ const directoryNames = (directory) => {
4523
+ const path = resolveWithin(projectRoot, directory);
4524
+ if (!existsSync5(path) || !lstatSync5(path).isDirectory()) return [];
4525
+ return readdirSync2(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).sort();
4526
+ };
4527
+ let versions = directoryNames("versioned_docs").filter((name) => /^version-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name));
4528
+ const versionsPath = resolveWithin(projectRoot, "versions.json");
4529
+ if (existsSync5(versionsPath) && lstatSync5(versionsPath).isFile()) {
4530
+ try {
4531
+ if (lstatSync5(versionsPath).size > 1e5) throw new Error("file exceeds the 100 KB limit");
4532
+ const declared = JSON.parse(readFileSync5(versionsPath, "utf8"));
4533
+ if (!Array.isArray(declared) || !declared.every((value) => typeof value === "string")) throw new Error("expected an array of version names");
4534
+ const declaredOrder = declared.map((value) => `version-${value}`);
4535
+ versions = [...declaredOrder.filter((name) => versions.includes(name)), ...versions.filter((name) => !declaredOrder.includes(name))];
4536
+ } catch (error) {
4537
+ warnings.push({ code: "unsupported-config", message: `Docusaurus versions.json could not be read safely: ${error instanceof Error ? error.message : String(error)}` });
4538
+ }
4539
+ }
4540
+ const sidebarPath = (version) => {
4541
+ const candidates = ["json", "js", "cjs", "mjs", "ts"].map((extension) => `versioned_sidebars/${version}-sidebars.${extension}`);
4542
+ return candidates.find((candidate) => {
4543
+ const path = resolveWithin(projectRoot, candidate);
4544
+ return existsSync5(path) && lstatSync5(path).isFile();
4545
+ });
4546
+ };
4547
+ const add = (directory, routePrefix, label, sidebar) => {
4548
+ const docsDir = [relativeRoot, directory].filter(Boolean).join("/");
4549
+ archives.push({ docsDir, routePrefix, label, ...sidebar ? { sidebarPath: sidebar } : {} });
4550
+ };
4551
+ for (const version of versions) add(`versioned_docs/${version}`, version.slice("version-".length), `Version ${version.slice("version-".length)}`, sidebarPath(version));
4552
+ for (const locale of directoryNames("i18n").filter((name) => /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*$/.test(name))) {
4553
+ const localizedRoot = `i18n/${locale}/docusaurus-plugin-content-docs`;
4554
+ const current = resolveWithin(projectRoot, `${localizedRoot}/current`);
4555
+ if (existsSync5(current) && lstatSync5(current).isDirectory()) add(`${localizedRoot}/current`, locale, `${locale} Documentation`);
4556
+ const localizedVersions = directoryNames(localizedRoot).filter((name) => /^version-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name));
4557
+ for (const version of localizedVersions) {
4558
+ const path = resolveWithin(projectRoot, `${localizedRoot}/${version}`);
4559
+ if (existsSync5(path) && lstatSync5(path).isDirectory()) add(`${localizedRoot}/${version}`, `${locale}/${version.slice("version-".length)}`, `${locale} Version ${version.slice("version-".length)}`);
4560
+ }
4561
+ }
4562
+ if (archives.length > 48) warnings.push({ code: "limit-reached", message: `Only the first 48 Docusaurus archived or localized docs roots were imported (${archives.length} found).` });
4563
+ return archives.slice(0, 48);
4564
+ }
3799
4565
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot, warnings) {
3800
4566
  const source = readDocusaurusConfigSource(projectRoot);
3801
4567
  const plugins = [];
@@ -4022,10 +4788,10 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
4022
4788
  }
4023
4789
  function containsMarkdown(directory, depth = 0) {
4024
4790
  if (depth > 4) return false;
4025
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
4791
+ for (const entry of readdirSync2(directory, { withFileTypes: true })) {
4026
4792
  if (isIgnoredDirectory(entry.name) || entry.isSymbolicLink()) continue;
4027
4793
  const path = resolveWithin(directory, entry.name);
4028
- if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) return true;
4794
+ if (entry.isFile() && [".md", ".mdx"].includes(extname5(entry.name).toLowerCase())) return true;
4029
4795
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
4030
4796
  }
4031
4797
  return false;
@@ -4058,7 +4824,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
4058
4824
  visitedRealPaths.add(directoryReal);
4059
4825
  let entries;
4060
4826
  try {
4061
- entries = readdirSync(directory, { withFileTypes: true });
4827
+ entries = readdirSync2(directory, { withFileTypes: true });
4062
4828
  } catch {
4063
4829
  return;
4064
4830
  }
@@ -4138,12 +4904,6 @@ function fernThemeColors(value) {
4138
4904
  if (lightMode) colors.dark = lightMode;
4139
4905
  return Object.keys(colors).length > 0 ? colors : void 0;
4140
4906
  }
4141
- function mintlifyBrandAssetPaths(value) {
4142
- if (typeof value === "string") return [value];
4143
- if (!value || typeof value !== "object") return [];
4144
- const source = value;
4145
- return ["light", "dark"].map((key) => source[key]).filter((entry) => typeof entry === "string");
4146
- }
4147
4907
  function normalizedReferenceKey(value) {
4148
4908
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "").replace(/\/(?:index|readme)$/i, "").replace(/^(?:index|readme)$/i, "") || "introduction";
4149
4909
  }
@@ -4151,7 +4911,25 @@ function exactReferenceKey(value) {
4151
4911
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
4152
4912
  }
4153
4913
  function findOpenApi(files) {
4154
- return files.find((file) => OPENAPI_FILENAMES.has(basename2(file.relativePath).toLowerCase())) ?? null;
4914
+ return files.find((file) => OPENAPI_FILENAMES.has(basename3(file.relativePath).toLowerCase()) && classifyApiSpec(file.absolutePath) === "openapi") ?? null;
4915
+ }
4916
+ function classifyApiSpec(path) {
4917
+ const buffer = Buffer.alloc(64e3);
4918
+ let bytesRead = 0;
4919
+ try {
4920
+ const descriptor = openSync(path, "r");
4921
+ try {
4922
+ bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
4923
+ } finally {
4924
+ closeSync(descriptor);
4925
+ }
4926
+ } catch {
4927
+ return "unknown";
4928
+ }
4929
+ const text = buffer.toString("utf8", 0, bytesRead);
4930
+ const topLevel = text.match(/^(?:openapi|swagger|asyncapi)\s*:/m)?.[0] ?? text.match(/^\s*\{\s*"(?:openapi|swagger|asyncapi)"\s*:/m)?.[0];
4931
+ if (!topLevel) return "unknown";
4932
+ return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
4155
4933
  }
4156
4934
  function mintlifyTopLevelApiReferences(config) {
4157
4935
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
@@ -4200,7 +4978,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4200
4978
  });
4201
4979
  continue;
4202
4980
  }
4203
- specs.push({ filename: basename2(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
4981
+ specs.push({ filename: basename3(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
4204
4982
  }
4205
4983
  return specs;
4206
4984
  }
@@ -4267,6 +5045,12 @@ function findFernConfiguredOpenApi(fernRoot, repositoryDir, apiName, apiNameExpl
4267
5045
  try {
4268
5046
  const absolute = resolveWithinRoot(dir, specPath, repositoryDir);
4269
5047
  if (!existsSync5(absolute) || !lstatSync5(absolute).isFile()) continue;
5048
+ const kind = classifyApiSpec(absolute);
5049
+ if (kind === "asyncapi") {
5050
+ unsupported.push({ kind: "asyncapi", path: specPath });
5051
+ continue;
5052
+ }
5053
+ if (kind !== "openapi") continue;
4270
5054
  return { spec: { absolutePath: absolute, relativePath: relative5(repositoryDir, absolute).replace(/\\/g, "/") }, unsupported };
4271
5055
  } catch {
4272
5056
  warnings.push({
@@ -4304,9 +5088,11 @@ function staticSnippetProperties(attributes) {
4304
5088
  }
4305
5089
  return properties;
4306
5090
  }
4307
- function interpolateSnippet(snippet, attributes) {
5091
+ function interpolateSnippet(snippet, attributes, children) {
4308
5092
  const properties = staticSnippetProperties(attributes);
5093
+ if (children !== void 0) properties.set("children", children.trim());
4309
5094
  if (properties.size === 0) return snippet;
5095
+ const isJsxSnippet = /<[A-Za-z][A-Za-z0-9]*/.test(snippet);
4310
5096
  let fence = "";
4311
5097
  return snippet.split("\n").map((line) => {
4312
5098
  const marker = line.match(/^\s*(`{3,}|~{3,})/)?.[1];
@@ -4317,11 +5103,276 @@ function interpolateSnippet(snippet, attributes) {
4317
5103
  }
4318
5104
  if (fence) return line;
4319
5105
  return line.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (original, name) => {
4320
- return properties.get(name) ?? original;
5106
+ const value = properties.get(name);
5107
+ if (value === void 0) return original;
5108
+ if (name === "children") return value;
5109
+ return isJsxSnippet ? `{${JSON.stringify(value)}}` : value;
5110
+ });
5111
+ }).join("\n");
5112
+ }
5113
+ function snippetComponentBody(source, componentName) {
5114
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
5115
+ for (const statement of parsed.statements) {
5116
+ if (!ts2.isVariableStatement(statement)) continue;
5117
+ for (const declaration of statement.declarationList.declarations) {
5118
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
5119
+ const initializer = declaration.initializer;
5120
+ if (!initializer || !ts2.isArrowFunction(initializer)) continue;
5121
+ const returned = ts2.isBlock(initializer.body) ? initializer.body.statements.find(ts2.isReturnStatement)?.expression : initializer.body;
5122
+ if (!returned) continue;
5123
+ let expression = returned;
5124
+ while (ts2.isParenthesizedExpression(expression)) expression = expression.expression;
5125
+ if (ts2.isJsxElement(expression) || ts2.isJsxSelfClosingElement(expression) || ts2.isJsxFragment(expression)) {
5126
+ return expression.getText(parsed);
5127
+ }
5128
+ }
5129
+ }
5130
+ return source;
5131
+ }
5132
+ function statefulSnippetDeclaration(source, componentName) {
5133
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
5134
+ if (parsed.statements.length !== 1) return null;
5135
+ const statement = parsed.statements[0];
5136
+ if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) || statement.declarationList.declarations.length !== 1) return null;
5137
+ const declaration = statement.declarationList.declarations[0];
5138
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== componentName || !declaration.initializer || !ts2.isArrowFunction(declaration.initializer) || !ts2.isBlock(declaration.initializer.body)) return null;
5139
+ const body = declaration.initializer.body.statements;
5140
+ if (body.length < 2 || !body.some(ts2.isReturnStatement)) return null;
5141
+ return source.trim();
5142
+ }
5143
+ function staticNamedSnippetValues(source) {
5144
+ const values = /* @__PURE__ */ new Map();
5145
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
5146
+ for (const statement of parsed.statements) {
5147
+ if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword)) continue;
5148
+ for (const declaration of statement.declarationList.declarations) {
5149
+ if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) continue;
5150
+ const expression = declaration.initializer;
5151
+ if (ts2.isStringLiteral(expression) || ts2.isNoSubstitutionTemplateLiteral(expression)) {
5152
+ values.set(declaration.name.text, JSON.stringify(expression.text));
5153
+ } else if (ts2.isNumericLiteral(expression)) {
5154
+ values.set(declaration.name.text, expression.text);
5155
+ } else if (expression.kind === ts2.SyntaxKind.TrueKeyword || expression.kind === ts2.SyntaxKind.FalseKeyword) {
5156
+ values.set(declaration.name.text, expression.kind === ts2.SyntaxKind.TrueKeyword ? "true" : "false");
5157
+ }
5158
+ }
5159
+ }
5160
+ return values;
5161
+ }
5162
+ function hoistMdxImports(source) {
5163
+ const imports2 = /* @__PURE__ */ new Set();
5164
+ const withoutImports = replaceOutsideCode(source, (body) => body.replace(
5165
+ /^[ \t]*import\s+(?:\{[^}\n]+\}|[A-Za-z_$][\w$]*)\s+from\s+(['"])[^'"\n]+\1[ \t]*;?/gm,
5166
+ (statement) => {
5167
+ imports2.add(statement.trim().replace(/;?$/, ";"));
5168
+ return "";
5169
+ }
5170
+ ));
5171
+ if (imports2.size === 0) return source;
5172
+ const frontmatter = withoutImports.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
5173
+ const prefix = frontmatter?.[0] ?? "";
5174
+ return `${prefix}${[...imports2].join("\n")}
5175
+
5176
+ ${withoutImports.slice(prefix.length).replace(/^(?:\r?\n)*/, "")}`;
5177
+ }
5178
+ function fernSourceBasePath(config) {
5179
+ const instance = Array.isArray(config.instances) ? config.instances[0] : null;
5180
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) return "";
5181
+ const value = instance;
5182
+ const address = typeof value["custom-domain"] === "string" ? value["custom-domain"] : value.url;
5183
+ if (typeof address !== "string") return "";
5184
+ try {
5185
+ const path = new URL(/^https?:\/\//i.test(address) ? address : `https://${address}`).pathname.replace(/\/+$/, "");
5186
+ return path === "/" ? "" : path;
5187
+ } catch {
5188
+ return "";
5189
+ }
5190
+ }
5191
+ function fernDefaultVersionPath(config) {
5192
+ if (!Array.isArray(config.versions)) return "";
5193
+ const versions = config.versions.filter((entry) => !!entry && typeof entry === "object" && !Array.isArray(entry));
5194
+ const chosen = versions.find((version) => version.default === true) ?? versions[0];
5195
+ return typeof chosen?.slug === "string" && /^[A-Za-z0-9_-]+$/.test(chosen.slug) ? `/${chosen.slug}` : "";
5196
+ }
5197
+ function fernSourceSiteUrl(config) {
5198
+ const instance = Array.isArray(config.instances) ? config.instances[0] : null;
5199
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) return void 0;
5200
+ const value = instance;
5201
+ const address = typeof value["custom-domain"] === "string" ? value["custom-domain"] : value.url;
5202
+ if (typeof address !== "string") return void 0;
5203
+ try {
5204
+ const url = new URL(/^https?:\/\//i.test(address) ? address : `https://${address}`);
5205
+ return `${url.origin}${fernSourceBasePath(config)}${fernDefaultVersionPath(config)}`;
5206
+ } catch {
5207
+ return void 0;
5208
+ }
5209
+ }
5210
+ function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized) {
5211
+ const rewrite = (target) => {
5212
+ const path = target.split(/[?#]/, 1)[0].replace(/^\//, "").replace(/\/$/, "");
5213
+ if (!path || pageIds.has(path) || ASSET_EXTENSIONS.has(extname5(path).toLowerCase())) return target;
5214
+ try {
5215
+ const base = new URL(`${sourceSiteUrl}/`);
5216
+ const sourceLinkUrl = new URL(target.slice(1), base);
5217
+ if (sourceLinkUrl.origin !== base.origin) return target;
5218
+ const sourceLink = sourceLinkUrl.toString();
5219
+ externalized.add(target);
5220
+ return sourceLink;
5221
+ } catch {
5222
+ return target;
5223
+ }
5224
+ };
5225
+ return replaceOutsideCode(body, (text) => text.replace(/(\]\()\/(?!\/)([^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(`/${target}`)}${after}`).replace(/(\b(?:href|to)=(['"]))\/(?!\/)([^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(`/${target}`)}${quote}`));
5226
+ }
5227
+ function stripFernBasePathFromLinks(body, basePath) {
5228
+ if (!basePath) return body;
5229
+ const escaped = basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5230
+ return replaceOutsideCode(body, (text) => text.replace(new RegExp(`(\\]\\()${escaped}(?=\\/|[)#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")).replace(new RegExp(`(\\b(?:href|to)=["'])${escaped}(?=\\/|["'#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")));
5231
+ }
5232
+ function rewriteMintlifyMountedLinks(body, pageIds) {
5233
+ const rewrite = (target) => {
5234
+ const suffixAt = target.search(/[?#]/);
5235
+ const pathname = suffixAt < 0 ? target : target.slice(0, suffixAt);
5236
+ const suffix = suffixAt < 0 ? "" : target.slice(suffixAt);
5237
+ if (!pathname.startsWith("/") || pathname.startsWith("//") || pathname.includes("\\")) return target;
5238
+ const segments = pathname.slice(1).split("/");
5239
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return target;
5240
+ if (pageIds.has(segments.join("/"))) return target;
5241
+ for (let count = 1; count < segments.length && count <= 3; count++) {
5242
+ const candidate = segments.slice(count).join("/");
5243
+ if (pageIds.has(candidate)) return `/${candidate}${suffix}`;
5244
+ }
5245
+ return target;
5246
+ };
5247
+ return replaceOutsideCode(body, (text) => text.replace(/(\]\()(\/[^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(target)}${after}`).replace(/(\b(?:href|to)=(['"]))(\/[^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(target)}${quote}`));
5248
+ }
5249
+ function preserveLinkedAnchors(pages) {
5250
+ const pageById = new Map(pages.map((page) => [page.id, page]));
5251
+ const requested = /* @__PURE__ */ new Map();
5252
+ const crossPageRequests = /* @__PURE__ */ new Set();
5253
+ const addTarget = (target, currentId) => {
5254
+ const hash2 = target.indexOf("#");
5255
+ if (hash2 < 0) return;
5256
+ let fragment;
5257
+ try {
5258
+ fragment = decodeURIComponent(target.slice(hash2 + 1));
5259
+ } catch {
5260
+ return;
5261
+ }
5262
+ if (!/^[\p{L}\p{N}_.:$-]{1,120}$/u.test(fragment)) return;
5263
+ const path = target.slice(0, hash2).split("?")[0];
5264
+ const id = target.startsWith("#") ? currentId : path.startsWith("/") && !path.startsWith("//") ? path.replace(/^\//, "").replace(/\/$/, "") || "introduction" : "";
5265
+ if (!pageById.has(id)) return;
5266
+ if (!requested.has(id)) requested.set(id, /* @__PURE__ */ new Set());
5267
+ requested.get(id).add(fragment);
5268
+ if (id !== currentId) crossPageRequests.add(`${id}#${fragment}`);
5269
+ };
5270
+ for (const page of pages) {
5271
+ replaceOutsideCode(page.body, (text) => {
5272
+ for (const match of text.matchAll(/\]\(([^)\s]+)\)/g)) addTarget(match[1], page.id);
5273
+ for (const match of text.matchAll(/\b(?:href|to)=(['"])([^'"\n]+)\1/g)) addTarget(match[2], page.id);
5274
+ return text;
4321
5275
  });
5276
+ }
5277
+ const comparable = (value) => value.replace(/&(?:lt|gt|amp|quot);/g, " ").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
5278
+ for (const page of pages) {
5279
+ const fragments = requested.get(page.id);
5280
+ if (!fragments?.size) continue;
5281
+ const lines = page.body.split("\n");
5282
+ let fence;
5283
+ const tableCells = [];
5284
+ const headings = lines.map((line, index) => {
5285
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
5286
+ if (marker) {
5287
+ if (!fence) fence = marker[1];
5288
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = void 0;
5289
+ return null;
5290
+ }
5291
+ if (fence) return null;
5292
+ const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
5293
+ if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
5294
+ const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
5295
+ return match ? { index, text: match[1] } : null;
5296
+ }).filter((entry) => entry !== null);
5297
+ const existing = /* @__PURE__ */ new Set();
5298
+ replaceOutsideCode(page.body, (text) => {
5299
+ for (const match of text.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
5300
+ return text;
5301
+ });
5302
+ const candidateIndexes = /* @__PURE__ */ new Map();
5303
+ for (const heading of headings) {
5304
+ const key = comparable(heading.text);
5305
+ candidateIndexes.set(key, [...candidateIndexes.get(key) ?? [], { index: heading.index, kind: "heading" }]);
5306
+ }
5307
+ for (const cell of tableCells) {
5308
+ const key = comparable(cell.text);
5309
+ candidateIndexes.set(key, [...candidateIndexes.get(key) ?? [], { index: cell.index, kind: "table" }]);
5310
+ }
5311
+ for (const heading of headings) existing.add(heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""));
5312
+ const headingAliases = /* @__PURE__ */ new Map();
5313
+ const tableAliases = /* @__PURE__ */ new Map();
5314
+ for (const fragment of fragments) {
5315
+ if (existing.has(fragment)) continue;
5316
+ const matches = candidateIndexes.get(comparable(fragment)) ?? [];
5317
+ let match = matches.length === 1 ? matches[0] : void 0;
5318
+ if (!match && matches.length > 1 && !crossPageRequests.has(`${page.id}#${fragment}`)) {
5319
+ const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5320
+ const linkPattern = new RegExp(`\\]\\(#${escaped}\\)|\\b(?:href|to)=["']#${escaped}["']`);
5321
+ const linkIndexes = lines.flatMap((line, index2) => linkPattern.test(line) ? [index2] : []);
5322
+ const selections = linkIndexes.map((index2) => matches.filter((candidate) => candidate.index < index2).at(-1));
5323
+ if (selections.length && selections.every((candidate) => candidate?.index === selections[0]?.index)) match = selections[0];
5324
+ }
5325
+ if (!match) continue;
5326
+ const { index, kind } = match;
5327
+ const aliases = kind === "heading" ? headingAliases : tableAliases;
5328
+ aliases.set(index, [...aliases.get(index) ?? [], fragment]);
5329
+ existing.add(fragment);
5330
+ }
5331
+ if (headingAliases.size || tableAliases.size) page.body = lines.flatMap((line, index) => [
5332
+ ...(headingAliases.get(index) ?? []).map((fragment) => `<a id="${fragment}"></a>`),
5333
+ tableAliases.has(index) ? line.replace(/^( {0,3}\|\s*)/, (prefix) => `${prefix}${tableAliases.get(index).map((fragment) => `<a id="${fragment}"></a>`).join("")}`) : line
5334
+ ]).join("\n");
5335
+ }
5336
+ }
5337
+ function closeOpenCodeFence(body) {
5338
+ let open = null;
5339
+ for (const line of body.split(/\r?\n/)) {
5340
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5341
+ if (!match) continue;
5342
+ const marker = match[1][0];
5343
+ if (!open) {
5344
+ open = { marker, length: match[1].length };
5345
+ } else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
5346
+ open = null;
5347
+ }
5348
+ }
5349
+ return open ? `${body}
5350
+
5351
+ ${open.marker.repeat(open.length)}` : body;
5352
+ }
5353
+ function addDatedHeadingAliases(body, date) {
5354
+ let open = null;
5355
+ const existingIds = new Set([...body.matchAll(/<a\s+id=["']([^"']+)["']/g)].map((match) => match[1]));
5356
+ return body.split(/\r?\n/).flatMap((line) => {
5357
+ const fence = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5358
+ if (fence) {
5359
+ const marker = fence[1][0];
5360
+ if (!open) open = { marker, length: fence[1].length };
5361
+ else if (marker === open.marker && fence[1].length >= open.length && !fence[2].trim()) open = null;
5362
+ return [line];
5363
+ }
5364
+ if (open) return [line];
5365
+ const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
5366
+ if (!heading) return [line];
5367
+ const slug = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
5368
+ const id = `${date}-${slug}`;
5369
+ if (!slug || existingIds.has(id)) return [line];
5370
+ existingIds.add(id);
5371
+ return [`<a id="${id}" />`, "", line];
4322
5372
  }).join("\n");
4323
5373
  }
4324
5374
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
5375
+ sourcePath = sourcePath.replace(/\\(?=_)/g, "");
4325
5376
  const repositoryRelative = sourcePath.startsWith("@site/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative5(repositoryRoot, resolvePath(dirname4(currentFile), sourcePath)).replace(/\\/g, "/");
4326
5377
  const candidate = resolveWithin(repositoryRoot, repositoryRelative);
4327
5378
  resolveWithin(repositoryRoot, relative5(repositoryRoot, candidate));
@@ -4332,12 +5383,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
4332
5383
  for (const file of files) {
4333
5384
  const segments = file.relativePath.split("/");
4334
5385
  if (!segments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()))) continue;
4335
- const basenameWithoutExtension = basename2(file.relativePath).replace(/\.mdx?$/i, "");
5386
+ const basenameWithoutExtension = basename3(file.relativePath).replace(/\.mdx?$/i, "");
4336
5387
  const conventionalAlias = basenameWithoutExtension.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4337
5388
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
4338
5389
  }
4339
5390
  for (const file of files) {
4340
- if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
5391
+ if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) continue;
4341
5392
  const raw = readFileSync5(file.absolutePath, "utf8");
4342
5393
  for (const match of raw.matchAll(SNIPPET_IMPORT_PATTERN)) {
4343
5394
  try {
@@ -4365,9 +5416,13 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
4365
5416
  } catch {
4366
5417
  return null;
4367
5418
  }
4368
- if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
5419
+ if (!ASSET_EXTENSIONS.has(extname5(decodedPath).toLowerCase())) return null;
4369
5420
  try {
4370
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname4(currentFile), decodedPath);
5421
+ const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
5422
+ const realRoot = realpathSync2(siteRoot);
5423
+ const realCandidate = realpathSync2(candidate);
5424
+ const realRelative = relative5(realRoot, realCandidate);
5425
+ if (realRelative === ".." || realRelative.startsWith(`..${sep2}`) || isAbsolute2(realRelative)) return null;
4371
5426
  const siteRelative = relative5(siteRoot, candidate).replace(/\\/g, "/");
4372
5427
  resolveWithin(siteRoot, siteRelative);
4373
5428
  if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return null;
@@ -4381,15 +5436,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
4381
5436
  }
4382
5437
  }
4383
5438
  function rewriteFernIdRenameLinks(body, renames) {
4384
- let codeFence = null;
4385
- return body.split("\n").map((line) => {
4386
- const fence = line.match(/^\s*(`{3,}|~{3,})/);
4387
- if (fence) {
4388
- if (!codeFence) codeFence = fence[1][0];
4389
- else if (fence[1][0] === codeFence) codeFence = null;
4390
- return line;
4391
- }
4392
- if (codeFence) return line;
5439
+ return replaceOutsideCode(body, (text) => {
4393
5440
  const rewriteTarget = (target) => {
4394
5441
  const suffixIndex = target.search(/[?#]/);
4395
5442
  const path = (suffixIndex >= 0 ? target.slice(0, suffixIndex) : target).replace(/^\/+/, "");
@@ -4397,8 +5444,8 @@ function rewriteFernIdRenameLinks(body, renames) {
4397
5444
  const renamed = renames.get(path);
4398
5445
  return renamed ? `/${renamed}${suffix}` : target;
4399
5446
  };
4400
- return line.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
4401
- }).join("\n");
5447
+ return text.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
5448
+ });
4402
5449
  }
4403
5450
  function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced) {
4404
5451
  return body.replace(/(!?\[[^\]]*\]\()(<[^>]+>|[^)\s]+)([^)]*\))/g, (original, opening, destination, closing) => {
@@ -4428,8 +5475,10 @@ function locallyDeclaredNames(raw) {
4428
5475
  }
4429
5476
  function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map()) {
4430
5477
  if (depth >= 8) return raw;
5478
+ raw = normalizeIndentedFences(raw);
4431
5479
  const snippets = /* @__PURE__ */ new Map();
4432
- const withoutImports = raw.replace(
5480
+ const preservedDeclarations = /* @__PURE__ */ new Map();
5481
+ let withoutImports = replaceOutsideCode(raw, (source) => source.replace(
4433
5482
  SNIPPET_IMPORT_PATTERN,
4434
5483
  (_statement, namedComponent, defaultComponent, sourcePath) => {
4435
5484
  const componentName = namedComponent ?? defaultComponent;
@@ -4445,7 +5494,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4445
5494
  siteRoot,
4446
5495
  globalAliases
4447
5496
  );
4448
- snippets.set(componentName, nested);
5497
+ const declaration = statefulSnippetDeclaration(nested, componentName);
5498
+ if (declaration) preservedDeclarations.set(componentName, declaration);
5499
+ else snippets.set(componentName, snippetComponentBody(nested, componentName));
4449
5500
  return "";
4450
5501
  } catch {
4451
5502
  warnings.push({
@@ -4457,7 +5508,40 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4457
5508
  return "";
4458
5509
  }
4459
5510
  }
4460
- );
5511
+ ));
5512
+ withoutImports = replaceOutsideCode(withoutImports, (source) => source.replace(
5513
+ SNIPPET_VALUE_IMPORT_PATTERN,
5514
+ (statement, names, sourcePath) => {
5515
+ const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
5516
+ const parsed = name.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
5517
+ return parsed ? { exported: parsed[1], local: parsed[2] ?? parsed[1] } : null;
5518
+ });
5519
+ if (bindings.length === 0 || bindings.some((binding) => !binding)) return statement;
5520
+ try {
5521
+ const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
5522
+ if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return statement;
5523
+ const snippetSource = readFileSync5(candidate, "utf8");
5524
+ const values = staticNamedSnippetValues(snippetSource);
5525
+ const declarations = [];
5526
+ const components = [];
5527
+ for (const binding of bindings) {
5528
+ const value = values.get(binding.exported);
5529
+ if (value !== void 0) {
5530
+ declarations.push(`export const ${binding.local} = ${value};`);
5531
+ continue;
5532
+ }
5533
+ if (!/^[A-Z]/.test(binding.exported)) return statement;
5534
+ const body = snippetComponentBody(snippetSource, binding.exported);
5535
+ if (body === snippetSource) return statement;
5536
+ components.push([binding.local, body]);
5537
+ }
5538
+ for (const [name, body] of components) snippets.set(name, body);
5539
+ return declarations.join("\n");
5540
+ } catch {
5541
+ return statement;
5542
+ }
5543
+ }
5544
+ ));
4461
5545
  const localNames = locallyDeclaredNames(withoutImports);
4462
5546
  for (const [componentName, candidate] of globalAliases) {
4463
5547
  if (snippets.has(componentName) || localNames.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
@@ -4470,14 +5554,16 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4470
5554
  siteRoot,
4471
5555
  globalAliases
4472
5556
  );
4473
- snippets.set(componentName, nested);
5557
+ const declaration = statefulSnippetDeclaration(nested, componentName);
5558
+ if (declaration) preservedDeclarations.set(componentName, declaration);
5559
+ else snippets.set(componentName, snippetComponentBody(nested, componentName));
4474
5560
  }
4475
5561
  let result = withoutImports;
4476
5562
  for (const [componentName, snippet] of snippets) {
4477
5563
  result = result.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (_tag, attributes) => {
4478
5564
  return interpolateSnippet(snippet, attributes);
4479
- }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>(?:[\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes) => {
4480
- return interpolateSnippet(snippet, attributes);
5565
+ }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes, children) => {
5566
+ return interpolateSnippet(snippet, attributes, children);
4481
5567
  });
4482
5568
  }
4483
5569
  result = result.replace(SNIPPET_TAG_PATTERN, (_tag, doubleQuoted, singleQuoted) => {
@@ -4503,13 +5589,25 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4503
5589
  return `{/* Missing snippet: ${filePath} */}`;
4504
5590
  }
4505
5591
  });
4506
- return result;
5592
+ if (preservedDeclarations.size > 0) {
5593
+ const frontmatter = result.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
5594
+ const prefix = frontmatter?.[0] ?? "";
5595
+ result = `${prefix}${[...preservedDeclarations.values()].join("\n\n")}
5596
+
5597
+ ${result.slice(prefix.length)}`;
5598
+ }
5599
+ return depth === 0 ? hoistMdxImports(result) : result;
4507
5600
  }
4508
5601
  function injectOpenApiSpecs(config, specs) {
4509
5602
  let tabs = config.tabs.map((tab) => ({ ...tab }));
4510
5603
  for (const spec of specs) {
4511
5604
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
4512
- if (apiTab) apiTab.api = { source: `/${spec.filename}`, navigation: false };
5605
+ if (apiTab) apiTab.api = {
5606
+ source: `/${spec.filename}`,
5607
+ // An API-only tab needs generated endpoint navigation; an authored
5608
+ // page tab keeps its own groups alongside the bound spec.
5609
+ ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
5610
+ };
4513
5611
  else tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
4514
5612
  }
4515
5613
  return { ...config, tabs };
@@ -4547,30 +5645,6 @@ function migrateRepository(options) {
4547
5645
  }
4548
5646
  return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
4549
5647
  })();
4550
- if (platform === "docusaurus" && docusaurusProjectRoot) {
4551
- for (const [directory, label] of [
4552
- ["versioned_docs", "versioned docs"],
4553
- ["versioned_sidebars", "versioned sidebars"]
4554
- ]) {
4555
- const path = resolveWithin(docusaurusProjectRoot, directory);
4556
- if (!existsSync5(path) || !lstatSync5(path).isDirectory()) continue;
4557
- const versions = readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4558
- if (versions.length) warnings.push({
4559
- code: "unsupported-config",
4560
- message: `Docusaurus ${label} were skipped; only current docs were imported. Skipped: ${versions.join(", ")}.`,
4561
- source: directory
4562
- });
4563
- }
4564
- const i18nPath = resolveWithin(docusaurusProjectRoot, "i18n");
4565
- if (existsSync5(i18nPath) && lstatSync5(i18nPath).isDirectory()) {
4566
- const locales = readdirSync(i18nPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4567
- if (locales.length) warnings.push({
4568
- code: "unsupported-config",
4569
- message: `Docusaurus localized content was skipped; only the default locale was imported. Skipped locales: ${locales.join(", ")}.`,
4570
- source: "i18n"
4571
- });
4572
- }
4573
- }
4574
5648
  const componentRoot = mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir;
4575
5649
  const componentMigrator = platform === "mintlify" || platform === "docusaurus" || platform === "fern" ? createComponentMigrator(componentRoot, repositoryDir, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
4576
5650
  let docsConfig = { tabs: [] };
@@ -4580,7 +5654,14 @@ function migrateRepository(options) {
4580
5654
  let docusaurusSidebars = null;
4581
5655
  let mintlifyConfig = null;
4582
5656
  let fernRawConfig = null;
5657
+ let fernBasePath = "";
5658
+ let fernVersionPath = "";
4583
5659
  let fernApiSections = [];
5660
+ let fernChangelogIndexes = [];
5661
+ const fernReferencedPaths = /* @__PURE__ */ new Set();
5662
+ const fernSourceLinkAliases = /* @__PURE__ */ new Map();
5663
+ const fernNavTitles = /* @__PURE__ */ new Map();
5664
+ const fernHiddenIds = /* @__PURE__ */ new Set();
4584
5665
  if (platform === "mintlify") {
4585
5666
  try {
4586
5667
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
@@ -4609,11 +5690,28 @@ function migrateRepository(options) {
4609
5690
  const fernConfig = readFernConfig(fernProjectRoot);
4610
5691
  if (fernConfig) {
4611
5692
  fernRawConfig = fernConfig.config;
4612
- const projected = projectFernNavigation({ config: fernConfig.config, fernRoot: fernProjectRoot });
5693
+ fernBasePath = fernSourceBasePath(fernConfig.config);
5694
+ fernVersionPath = fernDefaultVersionPath(fernConfig.config);
5695
+ const projected = projectFernNavigation({ config: fernConfig.config, fernRoot: fernProjectRoot, repositoryRoot: repositoryDir });
4613
5696
  docsConfig = projected.docsConfig;
4614
5697
  warnings.push(...projected.warnings);
4615
5698
  fernApiSections = projected.apiSections;
5699
+ fernChangelogIndexes = projected.changelogIndexes;
4616
5700
  for (const [index, descriptor] of projected.descriptors.entries()) {
5701
+ fernReferencedPaths.add(descriptor.sourcePath);
5702
+ if (descriptor.navTitle && !fernNavTitles.has(descriptor.navigationId)) {
5703
+ fernNavTitles.set(descriptor.navigationId, descriptor.navTitle);
5704
+ }
5705
+ if (descriptor.hidden) fernHiddenIds.add(descriptor.navigationId);
5706
+ const sourceTail = descriptor.sourcePath.match(/(?:^|\/)pages\/(.+)\.mdx?$/i)?.[1]?.replace(/\/index$/i, "");
5707
+ if (sourceTail) {
5708
+ const routeSegments = descriptor.navigationId.split("/");
5709
+ const sourceSegments = sourceTail.split("/");
5710
+ if (routeSegments.length === sourceSegments.length + 1) {
5711
+ const alias = `${routeSegments[0]}/${sourceTail}`;
5712
+ if (alias !== descriptor.navigationId && !fernSourceLinkAliases.has(alias)) fernSourceLinkAliases.set(alias, descriptor.navigationId);
5713
+ }
5714
+ }
4617
5715
  const key = normalizedReferenceKey(descriptor.sourcePath);
4618
5716
  if (!referenceMap.has(key)) referenceMap.set(key, { navigationId: descriptor.navigationId });
4619
5717
  const exactKey = exactReferenceKey(descriptor.sourcePath);
@@ -4630,7 +5728,7 @@ function migrateRepository(options) {
4630
5728
  }
4631
5729
  if (platform === "docusaurus") {
4632
5730
  try {
4633
- docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot);
5731
+ docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot, options.docusaurusSidebarPath);
4634
5732
  } catch (error) {
4635
5733
  warnings.push({
4636
5734
  code: "unsupported-config",
@@ -4644,6 +5742,19 @@ function migrateRepository(options) {
4644
5742
  }
4645
5743
  const mintignoreMatcher = platform === "mintlify" && mintlifyProjectRoot ? readMintignoreMatcher(mintlifyProjectRoot) : null;
4646
5744
  const files = mintignoreMatcher ? scanFiles(contentRoot, repositoryDir, warnings).filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scanFiles(contentRoot, repositoryDir, warnings);
5745
+ if (platform === "fern" && fernProjectRoot) {
5746
+ const scanned = new Set(files.map((file) => file.absolutePath));
5747
+ for (const sourcePath of fernReferencedPaths) {
5748
+ try {
5749
+ const absolutePath = resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir);
5750
+ if (scanned.has(absolutePath) || !existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) continue;
5751
+ files.push({ absolutePath, relativePath: sourcePath });
5752
+ scanned.add(absolutePath);
5753
+ } catch {
5754
+ warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
5755
+ }
5756
+ }
5757
+ }
4647
5758
  const pages = [];
4648
5759
  const assets = [];
4649
5760
  const referencedAssetPaths = /* @__PURE__ */ new Map();
@@ -4673,16 +5784,16 @@ function migrateRepository(options) {
4673
5784
  continue;
4674
5785
  }
4675
5786
  const sourceSegments = file.relativePath.split("/");
4676
- if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename2(file.relativePath).startsWith("_")) {
5787
+ if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename3(file.relativePath).startsWith("_")) {
4677
5788
  skipped++;
4678
5789
  continue;
4679
5790
  }
4680
- const rootFilename = !file.relativePath.includes("/") ? basename2(file.relativePath).toLowerCase() : "";
5791
+ const rootFilename = !file.relativePath.includes("/") ? basename3(file.relativePath).toLowerCase() : "";
4681
5792
  if (REPOSITORY_ONLY_DOCUMENTS.has(rootFilename) && !exactReferenceMap.has(exactReferenceKey(file.relativePath))) {
4682
5793
  skipped++;
4683
5794
  continue;
4684
5795
  }
4685
- if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
5796
+ if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) {
4686
5797
  skipped++;
4687
5798
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
4688
5799
  continue;
@@ -4793,6 +5904,11 @@ function migrateRepository(options) {
4793
5904
  skipped++;
4794
5905
  continue;
4795
5906
  }
5907
+ if (platform === "fern" && !page.navTitle) {
5908
+ const navTitle = fernNavTitles.get(navigationId);
5909
+ if (navTitle && navTitle !== page.title) page.navTitle = navTitle;
5910
+ }
5911
+ if (platform === "fern" && fernHiddenIds.has(navigationId)) page.hidden = true;
4796
5912
  if (platform === "mintlify" && mintlifyProjectRoot) {
4797
5913
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath) => {
4798
5914
  addAssetReference(assetPath, file.relativePath);
@@ -4802,6 +5918,8 @@ function migrateRepository(options) {
4802
5918
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, fernProjectRoot, (assetPath) => {
4803
5919
  addAssetReference(assetPath, file.relativePath);
4804
5920
  });
5921
+ page.body = stripFernBasePathFromLinks(page.body, fernBasePath);
5922
+ page.body = stripFernBasePathFromLinks(page.body, fernVersionPath);
4805
5923
  }
4806
5924
  if (platform === "mintlify" || platform === "docusaurus" || platform === "fern") {
4807
5925
  page.body = replaceLinkWithAnchor(page.body);
@@ -4882,7 +6000,7 @@ function migrateRepository(options) {
4882
6000
  const assetCandidates = [];
4883
6001
  for (const file of [...files, ...repositoryAssets]) {
4884
6002
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
4885
- if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
6003
+ if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
4886
6004
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
4887
6005
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
4888
6006
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
@@ -4938,10 +6056,19 @@ function migrateRepository(options) {
4938
6056
  const descriptorByNavigationId = new Map(
4939
6057
  docusaurusDescriptors.map((descriptor) => [descriptor.navigationId, descriptor])
4940
6058
  );
6059
+ const sourceOrigin = docusaurusProjectRoot ? readDocusaurusSiteOrigin(docusaurusProjectRoot) : void 0;
6060
+ const externalizedLinks = /* @__PURE__ */ new Set();
4941
6061
  for (const page of pages) {
4942
6062
  const descriptor = descriptorByNavigationId.get(page.navigationId);
4943
- if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors);
6063
+ if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors, {
6064
+ sourceOrigin,
6065
+ onExternalLink: (target) => externalizedLinks.add(target)
6066
+ });
4944
6067
  }
6068
+ if (externalizedLinks.size > 0) warnings.push({
6069
+ code: "unsupported-config",
6070
+ message: `${externalizedLinks.size} link(s) outside the imported Docusaurus docs still point to ${sourceOrigin}; migrate those sections separately if this site must be fully independent.`
6071
+ });
4945
6072
  if (docusaurusProjectRoot && !options.docusaurusSkipRedirects) {
4946
6073
  const redirects = readDocusaurusRedirects(docusaurusProjectRoot, warnings);
4947
6074
  if (redirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...redirects] };
@@ -4972,6 +6099,11 @@ function migrateRepository(options) {
4972
6099
  } : {}
4973
6100
  };
4974
6101
  }
6102
+ if (platform === "fern" && fernSourceLinkAliases.size > 0) {
6103
+ const imported = new Set(pages.map((page) => page.id));
6104
+ const aliases = new Map([...fernSourceLinkAliases].map(([source, target]) => [source, fernIdRenames.get(target) ?? target]).filter(([, target]) => imported.has(target)));
6105
+ for (const page of pages) page.body = rewriteFernIdRenameLinks(page.body, aliases);
6106
+ }
4975
6107
  if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
4976
6108
  if (platform === "fern" && fernProjectRoot) {
4977
6109
  const sections = fernApiSections.length > 0 ? fernApiSections : [{ nameExplicit: false }];
@@ -4981,7 +6113,7 @@ function migrateRepository(options) {
4981
6113
  const resolution = findFernConfiguredOpenApi(fernProjectRoot, repositoryDir, section.name, section.nameExplicit, warnings);
4982
6114
  const spec = resolution.spec ?? (!section.nameExplicit ? findOpenApi(files) : null);
4983
6115
  if (spec) {
4984
- let filename = basename2(spec.relativePath);
6116
+ let filename = basename3(spec.relativePath);
4985
6117
  const existingSource = specFilenameSources.get(filename);
4986
6118
  if (existingSource && existingSource !== spec.absolutePath) {
4987
6119
  const prefix = section.name ? `${section.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")}-` : `${specFilenameSources.size + 1}-`;
@@ -5032,7 +6164,7 @@ function migrateRepository(options) {
5032
6164
  } else {
5033
6165
  const fallback = findOpenApi(files);
5034
6166
  if (fallback) {
5035
- const filename = basename2(fallback.relativePath);
6167
+ const filename = basename3(fallback.relativePath);
5036
6168
  if (!assets.some((asset) => asset.path === filename)) {
5037
6169
  assets.push({ path: filename, content: readFileSync5(fallback.absolutePath) });
5038
6170
  }
@@ -5045,9 +6177,25 @@ function migrateRepository(options) {
5045
6177
  const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
5046
6178
  if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
5047
6179
  docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
6180
+ const reachablePaths = /* @__PURE__ */ new Set([
6181
+ ...pages.map((page) => page.id),
6182
+ ...(docsConfig.redirects ?? []).map((redirect) => redirect.source.replace(/^\//, ""))
6183
+ ]);
6184
+ for (const page of pages) page.body = rewriteMintlifyMountedLinks(page.body, reachablePaths);
5048
6185
  }
5049
6186
  if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
5050
- for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot, warnings)) {
6187
+ const additionalRoots = [
6188
+ ...additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot, warnings).map((plugin) => ({
6189
+ ...plugin,
6190
+ label: plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
6191
+ })),
6192
+ ...docusaurusArchiveRoots(docusaurusProjectRoot, repositoryDir, warnings)
6193
+ ];
6194
+ for (const plugin of additionalRoots) {
6195
+ if (discovered >= MAX_SOURCE_FILES) {
6196
+ warnings.push({ code: "limit-reached", message: `Docusaurus docs import stopped at the ${MAX_SOURCE_FILES}-file discovery budget.` });
6197
+ break;
6198
+ }
5051
6199
  if (plugin.docsDir === configuredDocsDir) continue;
5052
6200
  const pluginBundle = migrateRepository({
5053
6201
  ...options,
@@ -5055,22 +6203,27 @@ function migrateRepository(options) {
5055
6203
  platform: "docusaurus",
5056
6204
  docusaurusRoutePrefix: plugin.routePrefix,
5057
6205
  docusaurusSkipPlugins: true,
5058
- docusaurusSkipSidebar: true,
6206
+ docusaurusSkipSidebar: !plugin.sidebarPath,
6207
+ docusaurusSidebarPath: plugin.sidebarPath,
5059
6208
  docusaurusSkipAssets: true,
5060
6209
  docusaurusSkipRedirects: true
5061
6210
  });
5062
6211
  for (const page of pluginBundle.pages) {
5063
- if (seenPageIds.has(page.id)) continue;
6212
+ if (seenPageIds.has(page.id)) {
6213
+ skipped++;
6214
+ warnings.push({ code: "collision", message: `Docusaurus docs roots both map to ${page.id}; the first page was kept.`, source: page.source });
6215
+ continue;
6216
+ }
5064
6217
  seenPageIds.add(page.id);
5065
6218
  pages.push(page);
5066
6219
  }
5067
- const assetPaths = new Set(assets.map((asset) => asset.path));
6220
+ const assetPaths2 = new Set(assets.map((asset) => asset.path));
5068
6221
  for (const asset of pluginBundle.assets) {
5069
- if (assetPaths.has(asset.path)) continue;
5070
- assetPaths.add(asset.path);
6222
+ if (assetPaths2.has(asset.path)) continue;
6223
+ assetPaths2.add(asset.path);
5071
6224
  assets.push(asset);
5072
6225
  }
5073
- const pluginLabel = plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
6226
+ const pluginLabel = plugin.label;
5074
6227
  docsConfig.tabs.push(...pluginBundle.docsConfig.tabs.map((tab, index) => ({
5075
6228
  ...tab,
5076
6229
  tab: index === 0 ? pluginLabel : `${pluginLabel}: ${tab.tab}`
@@ -5080,21 +6233,234 @@ function migrateRepository(options) {
5080
6233
  skipped += pluginBundle.stats.skipped;
5081
6234
  }
5082
6235
  }
6236
+ if (platform === "docusaurus") {
6237
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6238
+ docsConfig.tabs = docsConfig.tabs.map((tab) => {
6239
+ if (tab.href) return tab;
6240
+ const root = tab.tab.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-");
6241
+ if (!root || !importedRoutes.has(root)) return tab;
6242
+ return {
6243
+ ...tab,
6244
+ href: `/${root}`,
6245
+ ...tab.pages ? { pages: tab.pages.map((node) => {
6246
+ if (typeof node === "string") return node;
6247
+ const index = node.pages.indexOf(root);
6248
+ return index > 0 ? { ...node, pages: [root, ...node.pages.filter((page) => page !== root)] } : node;
6249
+ }) } : {},
6250
+ ...tab.groups ? { groups: tab.groups.map((group) => {
6251
+ const index = group.pages.indexOf(root);
6252
+ return index > 0 ? { ...group, pages: [root, ...group.pages.filter((page) => page !== root)] } : group;
6253
+ }) } : {}
6254
+ };
6255
+ });
6256
+ }
6257
+ if (platform === "fern") {
6258
+ const byId = new Map(pages.map((page) => [page.navigationId, page]));
6259
+ for (const changelog of fernChangelogIndexes) {
6260
+ let index = byId.get(changelog.route);
6261
+ const entries = changelog.entries.map((id) => byId.get(fernIdRenames.get(id) ?? id)).filter((page) => Boolean(page)).slice(0, 10);
6262
+ if (entries.length === 0) continue;
6263
+ if (!index) {
6264
+ index = {
6265
+ id: changelog.route,
6266
+ navigationId: changelog.route,
6267
+ title: "Release Notes",
6268
+ description: "Latest release notes and product updates.",
6269
+ descriptionPlacement: "body",
6270
+ keywords: [],
6271
+ body: "",
6272
+ source: entries[0].source
6273
+ };
6274
+ pages.push(index);
6275
+ byId.set(index.navigationId, index);
6276
+ docsConfig.redirects = docsConfig.redirects?.filter((redirect) => redirect.source !== `/${changelog.route}`);
6277
+ }
6278
+ if (index.body.trim()) continue;
6279
+ index.description ||= "Latest release notes and product updates.";
6280
+ index.body = entries.map((entry) => {
6281
+ const date = entry.source.match(/\d{4}-\d{2}-\d{2}/)?.[0];
6282
+ const escapeText = (value) => value.replace(/([\\{}<>\[\]*_`])/g, "\\$1");
6283
+ const body = /^(?:import|export)\s.+(?:from\s|\{)/m.test(entry.body) ? escapeText(entry.description) : closeOpenCodeFence(date ? addDatedHeadingAliases(entry.body.trim(), date) : entry.body.trim()) || escapeText(entry.description);
6284
+ const dateAnchor = date ? `<a id="${date}T00:00:00.000Z" />
6285
+
6286
+ ` : "";
6287
+ return `${dateAnchor}## ${escapeText(entry.title)}
6288
+
6289
+ ${date ? `**${date}** \xB7 ` : ""}[Read release note](/${entry.id})
6290
+
6291
+ ${body}`;
6292
+ }).join("\n\n---\n\n");
6293
+ }
6294
+ }
5083
6295
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
6296
+ preserveLinkedAnchors(pages);
6297
+ if (platform === "fern" && fernRawConfig) {
6298
+ const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
6299
+ if (sourceSiteUrl) {
6300
+ const externalized = /* @__PURE__ */ new Set();
6301
+ const pageIds = new Set(pages.map((page) => page.id));
6302
+ for (const page of pages) page.body = externalizeMissingFernLinks(page.body, pageIds, sourceSiteUrl, externalized);
6303
+ if (externalized.size > 0) warnings.push({
6304
+ code: "unsupported-config",
6305
+ message: `${externalized.size} unresolved Fern link(s) still point to ${sourceSiteUrl}; import those sections separately for a fully independent site.`
6306
+ });
6307
+ }
6308
+ }
6309
+ if (docsConfig.redirects) {
6310
+ docsConfig = {
6311
+ ...docsConfig,
6312
+ redirects: docsConfig.redirects.filter((redirect) => {
6313
+ const literalRoute = (route) => route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
6314
+ const sourceRoute = literalRoute(redirect.source);
6315
+ if (sourceRoute && sourceRoute === literalRoute(redirect.destination)) {
6316
+ warnings.push({
6317
+ code: "unsupported-config",
6318
+ message: `Self-redirect from ${redirect.source} to ${redirect.destination} was omitted.`
6319
+ });
6320
+ return false;
6321
+ }
6322
+ if (!/[+?()[\]{}]/.test(redirect.source)) return true;
6323
+ warnings.push({
6324
+ code: "unsupported-config",
6325
+ message: `Redirect from ${redirect.source} contains route-pattern syntax Next.js cannot express literally and was omitted.`
6326
+ });
6327
+ return false;
6328
+ })
6329
+ };
6330
+ }
5084
6331
  if (pages.length === 0) {
5085
6332
  const reasons = warnings.map((warning) => warning.message);
5086
6333
  throw new Error(reasons.length ? `No importable Markdown or MDX pages were found in the repository. Warnings encountered during migration:
5087
6334
  ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown or MDX pages were found in the repository.");
5088
6335
  }
5089
6336
  const isTopLevelDocusaurus = platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipRedirects;
5090
- const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
5091
- const brandAssetPaths = mintlifyConfig ? [...mintlifyBrandAssetPaths(mintlifyConfig.logo), ...mintlifyBrandAssetPaths(mintlifyConfig.favicon)] : isTopLevelDocusaurus ? readDocusaurusBrandAssetPaths(docusaurusProjectRoot) : [];
5092
- if (brandAssetPaths.length > 0) {
5093
- warnings.push({
6337
+ const docusaurusSettings = isTopLevelDocusaurus ? readDocusaurusSiteSettings(docusaurusProjectRoot) : void 0;
6338
+ if (docusaurusSettings) {
6339
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6340
+ const sourceOrigin = readDocusaurusSiteOrigin(docusaurusProjectRoot);
6341
+ const externalized = /* @__PURE__ */ new Set();
6342
+ const projectLink = (raw, sourceLiteral = false) => {
6343
+ if (/^https?:\/\//i.test(raw)) return raw;
6344
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw) || raw.includes("\\")) return void 0;
6345
+ const suffixAt = raw.search(/[?#]/);
6346
+ const path = suffixAt < 0 ? raw : raw.slice(0, suffixAt);
6347
+ const suffix = suffixAt < 0 ? "" : raw.slice(suffixAt);
6348
+ const route = path.replace(/^\.?\//, "").replace(/\/+$/, "");
6349
+ if (route === ".." || route.startsWith("../") || route.includes("/../")) return void 0;
6350
+ const docsBase = docusaurusSettings.docsRouteBasePath.replace(/^\/+|\/+$/g, "");
6351
+ const isOutsideDocs = sourceLiteral && docsBase && route !== docsBase && !route.startsWith(`${docsBase}/`);
6352
+ if (isOutsideDocs && sourceOrigin) {
6353
+ externalized.add(raw);
6354
+ return new URL(raw, `${sourceOrigin}/`).toString();
6355
+ }
6356
+ const strippedDocsRoute = docsBase && route.startsWith(`${docsBase}/`) ? route.slice(docsBase.length + 1) : route;
6357
+ const candidates = [route, strippedDocsRoute, ...route === docsBase ? ["introduction"] : []];
6358
+ const imported = candidates.find((candidate) => importedRoutes.has(candidate));
6359
+ if (imported) return `/${imported}${suffix}`;
6360
+ if (sourceOrigin) {
6361
+ externalized.add(raw);
6362
+ return new URL(raw, `${sourceOrigin}/`).toString();
6363
+ }
6364
+ return path.startsWith("/") ? `${path}${suffix}` : `/${route}${suffix}`;
6365
+ };
6366
+ const firstNavigationPage = (nodes) => {
6367
+ for (const node of nodes) {
6368
+ if (typeof node === "string") return node;
6369
+ const nested = firstNavigationPage(node.pages);
6370
+ if (nested) return nested;
6371
+ }
6372
+ return void 0;
6373
+ };
6374
+ const navbarLinks = docusaurusSettings.navbarLinks.flatMap((link) => {
6375
+ const docRoute = link.docId && !link.docsPluginId && docusaurusDescriptors.find((page) => page.docId === link.docId)?.navigationId;
6376
+ const sidebar = link.sidebarId && docsConfig.tabs.find((tab) => tab.tab.toLowerCase() === link.sidebarId.toLowerCase());
6377
+ const sidebarRoute = sidebar && firstNavigationPage(sidebar.pages ?? sidebar.groups ?? []);
6378
+ const pluginRoot = link.docsPluginId && importedRoutes.has(link.docsPluginId) ? link.docsPluginId : void 0;
6379
+ const rawHref = link.href ?? (docRoute ? `/${docRoute}` : void 0) ?? (pluginRoot ? `/${pluginRoot}` : void 0) ?? (sidebarRoute ? `/${sidebarRoute}` : void 0) ?? (link.docsPluginId ? `/${link.docsPluginId}` : void 0);
6380
+ const href = rawHref && projectLink(rawHref, Boolean(link.href));
6381
+ return href ? [{ label: link.label, href }] : [];
6382
+ });
6383
+ const footerLinks = docusaurusSettings.footerLinks?.flatMap((column) => {
6384
+ const items = column.items.flatMap((item) => {
6385
+ const href = projectLink(item.href, true);
6386
+ return href ? [{ label: item.label, href }] : [];
6387
+ });
6388
+ return items.length > 0 ? [{ heading: column.heading, items }] : [];
6389
+ });
6390
+ docsConfig = {
6391
+ ...docsConfig,
6392
+ ...navbarLinks.length > 0 ? { navbar: { ...docsConfig.navbar, links: navbarLinks } } : {},
6393
+ ...footerLinks?.length || docusaurusSettings.copyright ? {
6394
+ footer: {
6395
+ ...docsConfig.footer,
6396
+ ...footerLinks?.length ? { links: footerLinks } : {},
6397
+ ...docusaurusSettings.copyright ? { copyright: docusaurusSettings.copyright } : {}
6398
+ }
6399
+ } : {}
6400
+ };
6401
+ if (externalized.size > 0) warnings.push({
5094
6402
  code: "unsupported-config",
5095
- message: `The site's logo/favicon (${brandAssetPaths.join(", ")}) were copied into public/ but are not wired into the migrated site's branding, which Thally manages from the admin dashboard rather than a static config field; set them there after the site is deployed.`
6403
+ message: `${externalized.size} site navigation/footer link(s) point outside the imported docs and were kept on ${sourceOrigin}.`
5096
6404
  });
5097
6405
  }
6406
+ if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
6407
+ docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
6408
+ }
6409
+ const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
6410
+ const assetPaths = new Set(assets.map((asset) => asset.path));
6411
+ const publicBrandPath = (value) => {
6412
+ if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
6413
+ const normalized = normalizeAssetPath(value);
6414
+ return normalized && assetPaths.has(normalized) ? `/${normalized}` : void 0;
6415
+ };
6416
+ const brandVariants = (value) => {
6417
+ if (typeof value === "string") return { light: value };
6418
+ if (!value || typeof value !== "object") return {};
6419
+ const source = value;
6420
+ const light = source.light ?? source.src;
6421
+ const dark = source.dark ?? source.srcDark;
6422
+ return {
6423
+ ...typeof light === "string" ? { light } : {},
6424
+ ...typeof dark === "string" ? { dark } : {},
6425
+ ...typeof source.showTitle === "boolean" ? { showTitle: source.showTitle } : {}
6426
+ };
6427
+ };
6428
+ const sourceLogo = mintlifyConfig?.logo ?? fernRawConfig?.logo ?? docusaurusSettings?.logo;
6429
+ const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
6430
+ const logo = brandVariants(sourceLogo);
6431
+ const favicon = brandVariants(sourceFavicon);
6432
+ const logoLight = publicBrandPath(logo.light);
6433
+ const logoDark = publicBrandPath(logo.dark);
6434
+ const faviconLight = publicBrandPath(favicon.light);
6435
+ const faviconDark = publicBrandPath(favicon.dark);
6436
+ if (logoLight) docsConfig = {
6437
+ ...docsConfig,
6438
+ navbar: {
6439
+ ...docsConfig.navbar,
6440
+ logo: {
6441
+ light: logoLight,
6442
+ ...logoDark ? { dark: logoDark } : {},
6443
+ showTitle: mintlifyConfig || fernRawConfig ? false : logo.showTitle !== false,
6444
+ ...typeof fernRawConfig?.logo?.["right-text"] === "string" ? { rightText: String(fernRawConfig.logo["right-text"]).trim().slice(0, 40) } : {}
6445
+ }
6446
+ }
6447
+ };
6448
+ if (faviconLight) docsConfig = {
6449
+ ...docsConfig,
6450
+ favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
6451
+ };
6452
+ const missingBrand = [
6453
+ ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
6454
+ ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
6455
+ ];
6456
+ if (missingBrand.length > 0) warnings.push({
6457
+ code: "unsupported-config",
6458
+ message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
6459
+ });
6460
+ if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
6461
+ code: "unsupported-config",
6462
+ message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
6463
+ });
5098
6464
  return {
5099
6465
  sourceUrl: options.sourceUrl,
5100
6466
  sourceKind: "repository",
@@ -5114,6 +6480,12 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
5114
6480
  ...typeof fernRawConfig.title === "string" ? { name: fernRawConfig.title } : {},
5115
6481
  ...themeColors ? { colors: themeColors } : {}
5116
6482
  }
6483
+ } : docusaurusSettings && (docusaurusSettings.name || docusaurusSettings.description || themeColors) ? {
6484
+ site: {
6485
+ ...docusaurusSettings.name ? { name: docusaurusSettings.name } : {},
6486
+ ...docusaurusSettings.description ? { description: docusaurusSettings.description } : {},
6487
+ ...themeColors ? { colors: themeColors } : {}
6488
+ }
5117
6489
  } : themeColors ? {
5118
6490
  site: { colors: themeColors }
5119
6491
  } : {},
@@ -5132,6 +6504,7 @@ function renderPage(bundle, page) {
5132
6504
  `title: ${yamlString(page.title)}`,
5133
6505
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
5134
6506
  `description: ${yamlString(page.description)}`,
6507
+ page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
5135
6508
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
5136
6509
  page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
5137
6510
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
@@ -5233,7 +6606,7 @@ function renderMigrationFiles(bundle, options = {}) {
5233
6606
  }
5234
6607
 
5235
6608
  // src/url.ts
5236
- import { load } from "cheerio";
6609
+ import { load as load2 } from "cheerio";
5237
6610
  import remarkMdx3 from "remark-mdx";
5238
6611
  import remarkParse3 from "remark-parse";
5239
6612
  import TurndownService from "turndown";
@@ -5410,7 +6783,7 @@ function machineIndexScopePath(source, documents) {
5410
6783
  if (!document) continue;
5411
6784
  const references = [...headerLinks(document)];
5412
6785
  if (/html/i.test(document.contentType)) {
5413
- const $ = load(document.body);
6786
+ const $ = load2(document.body);
5414
6787
  references.push(...$("link[href], a[href]").map((_index, element) => $(element).attr("href") ?? "").get().filter((href) => /\/(?:llms(?:-full)?\.txt|sitemap\.xml)(?:[?#]|$)/i.test(href)));
5415
6788
  }
5416
6789
  for (const reference of references) {
@@ -5521,7 +6894,7 @@ function jsonObjectAfterKey(value, key) {
5521
6894
  return null;
5522
6895
  }
5523
6896
  function embeddedMintlifyConfig(body) {
5524
- const $ = load(body);
6897
+ const $ = load2(body);
5525
6898
  const flightPayloads = [];
5526
6899
  $("script").each((_index, element) => {
5527
6900
  const script = $(element).text();
@@ -5619,7 +6992,7 @@ function isSafePortableUrl(value) {
5619
6992
  }
5620
6993
  }
5621
6994
  function decodeHtmlAttribute(value) {
5622
- const document = load(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
6995
+ const document = load2(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
5623
6996
  return document("i").attr("data-thally-value") ?? "";
5624
6997
  }
5625
6998
  function escapeMarkdownText(value, escapeTableSeparator = false) {
@@ -5668,7 +7041,7 @@ function replacePortableHtmlImages(value) {
5668
7041
  if (start < cursor) continue;
5669
7042
  const end = htmlOpeningTagEnd(value, start + match[0].length);
5670
7043
  if (end === null) continue;
5671
- const fragment = load(value.slice(start, end));
7044
+ const fragment = load2(value.slice(start, end));
5672
7045
  const image = fragment("img").first();
5673
7046
  const source = image.attr("src");
5674
7047
  const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
@@ -5693,7 +7066,7 @@ function replacePortableHtmlAnchors(value) {
5693
7066
  const closing = closingMatch.exec(value);
5694
7067
  if (!closing) continue;
5695
7068
  const end = closing.index + closing[0].length;
5696
- const fragment = load(value.slice(start, end));
7069
+ const fragment = load2(value.slice(start, end));
5697
7070
  const anchor = fragment("a").first();
5698
7071
  const href = anchor.attr("href");
5699
7072
  const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
@@ -6074,7 +7447,7 @@ function mergeEmbeddedOpenApi(fragments) {
6074
7447
  return new TextEncoder().encode(stringifyYaml(combined));
6075
7448
  }
6076
7449
  function htmlPage(document, id) {
6077
- const $ = load(document.body);
7450
+ const $ = load2(document.body);
6078
7451
  const links = $('nav a[href], main a[href], article a[href], [role="main"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
6079
7452
  const title = $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
6080
7453
  const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
@@ -6118,7 +7491,7 @@ function htmlPage(document, id) {
6118
7491
  const element = node;
6119
7492
  const className = element.className;
6120
7493
  const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
6121
- const fragment = load(element.outerHTML);
7494
+ const fragment = load2(element.outerHTML);
6122
7495
  const clone = fragment(".theme-admonition").first();
6123
7496
  const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
6124
7497
  const titleText = title2.text().trim();
@@ -6139,7 +7512,7 @@ ${titleText ? `**${titleText}**
6139
7512
  filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
6140
7513
  replacement: (_content, node) => {
6141
7514
  const element = node;
6142
- const fragment = load(element.outerHTML);
7515
+ const fragment = load2(element.outerHTML);
6143
7516
  const container = fragment(".tabs-container").first();
6144
7517
  const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
6145
7518
  const panels = container.children('div[role="tabpanel"]').toArray();
@@ -6162,7 +7535,7 @@ ${tabs.join("\n\n")}
6162
7535
  turndown.addRule("gfm-table", {
6163
7536
  filter: "table",
6164
7537
  replacement: (_content, node) => {
6165
- const fragment = load(node.outerHTML);
7538
+ const fragment = load2(node.outerHTML);
6166
7539
  const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
6167
7540
  fragment(cell).text().replace(/\s+/g, " ").trim(),
6168
7541
  true
@@ -6183,7 +7556,7 @@ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
6183
7556
  filter: (node) => node.nodeName === "PRE",
6184
7557
  replacement: (_content, node) => {
6185
7558
  const element = node;
6186
- const fragment = load(element.outerHTML);
7559
+ const fragment = load2(element.outerHTML);
6187
7560
  const tokenLines = fragment(".token-line").toArray();
6188
7561
  const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
6189
7562
  const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
@@ -6216,7 +7589,7 @@ ${fence}
6216
7589
  };
6217
7590
  }
6218
7591
  function htmlNavigationLinks(document) {
6219
- const $ = load(document.body);
7592
+ const $ = load2(document.body);
6220
7593
  return $('.nav-tabs a[href], nav a[href], aside a[href], [role="navigation"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
6221
7594
  }
6222
7595
  function normalizedLinkLabel(value) {
@@ -6224,7 +7597,7 @@ function normalizedLinkLabel(value) {
6224
7597
  }
6225
7598
  function readDocusaurusSidebar(document) {
6226
7599
  if (!/html/i.test(document.contentType)) return null;
6227
- const $ = load(document.body);
7600
+ const $ = load2(document.body);
6228
7601
  const sidebar = $("ul.theme-doc-sidebar-menu").first();
6229
7602
  if (sidebar.length === 0) return null;
6230
7603
  function readList(list) {
@@ -6260,7 +7633,7 @@ function docusaurusSidebarHrefs(snapshot) {
6260
7633
  return hrefs;
6261
7634
  }
6262
7635
  function deriveDocusaurusScope(source, document, submittedScopePath) {
6263
- const $ = load(document.body);
7636
+ const $ = load2(document.body);
6264
7637
  const sidebar = readDocusaurusSidebar(document);
6265
7638
  const internalPaths = docusaurusSidebarHrefs(sidebar).flatMap((href) => {
6266
7639
  try {
@@ -6390,7 +7763,7 @@ function docusaurusNavigationFromSnapshots(snapshots, pages, scopePath) {
6390
7763
  return { tabs };
6391
7764
  }
6392
7765
  function htmlTopLevelNavigation(document, source, scopePath) {
6393
- const $ = load(document.body);
7766
+ const $ = load2(document.body);
6394
7767
  const collectCandidates = (selector) => {
6395
7768
  const candidates2 = [];
6396
7769
  $(selector).each((_containerIndex, container) => {
@@ -6451,7 +7824,7 @@ function markdownLinks(body) {
6451
7824
  return [...body.matchAll(/\[[^\]]+\]\(([^)\s]+)(?:\s+['"][^'"]*['"])?\)/g)].map((match) => match[1]);
6452
7825
  }
6453
7826
  function sitemapLinks(body) {
6454
- const $ = load(body, { xmlMode: true });
7827
+ const $ = load2(body, { xmlMode: true });
6455
7828
  return $("url > loc, sitemap > loc").map((_index, element) => $(element).text().trim()).get().filter(Boolean);
6456
7829
  }
6457
7830
  function robotsSitemaps(body) {
@@ -6490,7 +7863,7 @@ async function migrateUrl(options) {
6490
7863
  const platform = options.platform ?? detectUrlPlatform(scopedHtmlProbe ?? initial);
6491
7864
  let docusaurusHtmlProbe = platform === "docusaurus" ? scopedHtmlProbe : null;
6492
7865
  if (docusaurusHtmlProbe && !readDocusaurusSidebar(docusaurusHtmlProbe)) {
6493
- const $ = load(docusaurusHtmlProbe.body);
7866
+ const $ = load2(docusaurusHtmlProbe.body);
6494
7867
  const docsHref = $("nav a[href]").toArray().map((element) => ({
6495
7868
  href: $(element).attr("href"),
6496
7869
  label: normalizedLinkLabel($(element).text())