@thallylabs/migrate 0.2.42 → 0.2.44

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.
@@ -1,10 +1,10 @@
1
1
  // src/url-run.ts
2
- import { createHash as createHash4 } from "crypto";
2
+ import { createHash as createHash5 } from "crypto";
3
3
  import { load as load7 } from "cheerio";
4
4
  import { parse as parse2, postprocess, preprocess } from "micromark";
5
- import remarkParse4 from "remark-parse";
5
+ import remarkParse5 from "remark-parse";
6
6
  import remarkMdx4 from "remark-mdx";
7
- import { unified as unified4 } from "unified";
7
+ import { unified as unified5 } from "unified";
8
8
 
9
9
  // src/navigation.ts
10
10
  import { existsSync, lstatSync, readFileSync, realpathSync as realpathSync2 } from "fs";
@@ -56,6 +56,32 @@ function playgroundDisplay(value, where, warn) {
56
56
  return void 0;
57
57
  }
58
58
 
59
+ // src/redirects.ts
60
+ function trimTrailingSlashes(route) {
61
+ let end = route.length;
62
+ while (end > 0 && route[end - 1] === "/") end--;
63
+ return route.slice(0, end);
64
+ }
65
+ function exactRoute(route) {
66
+ return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? trimTrailingSlashes(route) || "/" : null;
67
+ }
68
+ function literalRoute(route) {
69
+ return exactRoute(route)?.toLowerCase() ?? null;
70
+ }
71
+ function resolveDotSegments(route) {
72
+ const segments = [];
73
+ for (const segment of route.split("/").slice(1)) {
74
+ const dots = segment.replace(/%2e/gi, ".");
75
+ if (dots === "..") segments.pop();
76
+ else if (dots !== ".") segments.push(segment);
77
+ }
78
+ return `/${segments.join("/")}`;
79
+ }
80
+ function destinationRoute(destination) {
81
+ const route = destination.startsWith("//") ? null : exactRoute(destination.split(/[?#]/, 1)[0]);
82
+ return route === null ? null : resolveDotSegments(route);
83
+ }
84
+
59
85
  // src/navigation.ts
60
86
  function titleCase(value) {
61
87
  return value.split(/[-_]/).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -497,15 +523,15 @@ function createMigrationAssetCache() {
497
523
  load(url, acquire) {
498
524
  const existing = get(url);
499
525
  if (existing) return existing;
500
- const request = Promise.resolve().then(acquire).then((asset) => {
526
+ const request2 = Promise.resolve().then(acquire).then((asset) => {
501
527
  pending.delete(url);
502
528
  return remember(url, asset);
503
529
  }, (error) => {
504
530
  pending.delete(url);
505
531
  throw error;
506
532
  });
507
- pending.set(url, request);
508
- return request;
533
+ pending.set(url, request2);
534
+ return request2;
509
535
  },
510
536
  delete(url) {
511
537
  settled.delete(url);
@@ -1270,9 +1296,9 @@ function recoverFlightArticleCode($, root) {
1270
1296
  const projectionBudget = expansionBudget();
1271
1297
  recoverFlightTreeTabs($, root, projectionBudget);
1272
1298
  const renderedHeadings = root.find("h1,h2,h3,h4,h5,h6").toArray().filter((node) => $(node).attr("data-thally-static-disclosure-heading") === void 0);
1273
- const headingText = renderedHeadings.map((node) => labelText($(node).text()));
1299
+ const headingText2 = renderedHeadings.map((node) => labelText($(node).text()));
1274
1300
  const existingCode = root.find("pre").toArray().map((node) => renderedCode($, node));
1275
- if (!headingText.length || !existingCode.length) return;
1301
+ if (!headingText2.length || !existingCode.length) return;
1276
1302
  const prose = root.clone();
1277
1303
  prose.find("pre,button,script,style,nav,footer,aside").remove();
1278
1304
  const renderedProse = labelText(prose.text());
@@ -1285,7 +1311,7 @@ function recoverFlightArticleCode($, root) {
1285
1311
  const anchor = /\s+\[#([A-Za-z\d_.:-]+)\]$/.exec(text2);
1286
1312
  return anchor && $(renderedHeadings[index]).attr("id") === anchor[1] ? text2.slice(0, anchor.index) : text2;
1287
1313
  });
1288
- if (JSON.stringify(headings) !== JSON.stringify(headingText)) continue;
1314
+ if (JSON.stringify(headings) !== JSON.stringify(headingText2)) continue;
1289
1315
  let proseCursor = 0;
1290
1316
  let hasMatchingProse = true;
1291
1317
  for (const paragraph of nodes.filter((node) => node.type === "paragraph" || node.type === "tableCell")) {
@@ -1884,29 +1910,42 @@ function projectMigrationMedia(html, base) {
1884
1910
  }
1885
1911
 
1886
1912
  // src/url-identity.ts
1887
- import { createHash as createHash3 } from "crypto";
1913
+ import { createHash as createHash4 } from "crypto";
1888
1914
  import { load as load6 } from "cheerio";
1889
1915
 
1890
1916
  // src/url.ts
1891
1917
  import { load as load5 } from "cheerio";
1892
1918
  import remarkMdx3 from "remark-mdx";
1893
- import remarkParse3 from "remark-parse";
1919
+ import remarkParse4 from "remark-parse";
1894
1920
  import TurndownService from "turndown";
1895
- import { unified as unified3 } from "unified";
1896
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1921
+ import { unified as unified4 } from "unified";
1922
+ import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
1923
+
1924
+ // src/url-openapi.ts
1925
+ import { createHash as createHash2 } from "crypto";
1926
+
1927
+ // src/remote-spec.ts
1928
+ import { lookup } from "dns/promises";
1929
+ import { request } from "https";
1930
+ import { isIP } from "net";
1931
+ import { createGunzip } from "zlib";
1932
+
1933
+ // src/spec-exclude.ts
1934
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1897
1935
 
1898
1936
  // src/analytics.ts
1899
1937
  var HOSTNAME_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
1900
1938
  var HOSTNAME = new RegExp(`^${HOSTNAME_LABEL}(?:\\.${HOSTNAME_LABEL})*$`);
1901
1939
 
1902
1940
  // src/mdx.ts
1941
+ import { analyze } from "eslint-scope";
1903
1942
  import { posix } from "path";
1904
1943
  import { compileSync } from "@mdx-js/mdx";
1905
1944
  import { nameToEmoji } from "gemoji";
1906
1945
  import remarkGfm2 from "remark-gfm";
1907
1946
  import remarkMdx2 from "remark-mdx";
1908
- import remarkParse2 from "remark-parse";
1909
- import { unified as unified2 } from "unified";
1947
+ import remarkParse3 from "remark-parse";
1948
+ import { unified as unified3 } from "unified";
1910
1949
 
1911
1950
  // src/builtin-components.ts
1912
1951
  var THALLY_BUILTIN_COMPONENTS = /* @__PURE__ */ new Set([
@@ -2343,13 +2382,13 @@ function unresolvedExpressionNames(expression, pageNames) {
2343
2382
  }
2344
2383
 
2345
2384
  // src/frontmatter.ts
2346
- import { parse as parseYaml } from "yaml";
2385
+ import { parse as parseYaml2 } from "yaml";
2347
2386
  function salvageFrontmatterLines(matter) {
2348
2387
  const data = {};
2349
2388
  for (const line of matter.split(/\r?\n/)) {
2350
2389
  if (!/^[A-Za-z_][\w-]*:\s?/.test(line)) continue;
2351
2390
  try {
2352
- const parsedLine = parseYaml(line);
2391
+ const parsedLine = parseYaml2(line);
2353
2392
  if (parsedLine && typeof parsedLine === "object" && !Array.isArray(parsedLine)) {
2354
2393
  Object.assign(data, parsedLine);
2355
2394
  }
@@ -2373,7 +2412,7 @@ function parseFrontmatter(raw) {
2373
2412
  return { content, data: {} };
2374
2413
  }
2375
2414
  try {
2376
- const parsed = parseYaml(matter);
2415
+ const parsed = parseYaml2(matter);
2377
2416
  return {
2378
2417
  content,
2379
2418
  data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
@@ -2387,18 +2426,136 @@ function parseFrontmatter(raw) {
2387
2426
  }
2388
2427
  }
2389
2428
 
2429
+ // src/source-styles.ts
2430
+ import postcss from "postcss";
2431
+ import selectorParser from "postcss-selector-parser";
2432
+ import ts2 from "typescript";
2433
+ var CONTENT_CONTAINER = ".thally-docs-prose";
2434
+ var SOURCE_PAGE_ATTRIBUTE = "data-thally-source-page";
2435
+ var SCOPE = `:where(${CONTENT_CONTAINER}, [${SOURCE_PAGE_ATTRIBUTE}])`;
2436
+
2437
+ // src/fence-meta.ts
2438
+ import remarkParse2 from "remark-parse";
2439
+ import { unified as unified2 } from "unified";
2440
+ function delimitedEnd(meta, start) {
2441
+ const opening = meta[start];
2442
+ if (opening === '"' || opening === "'") {
2443
+ let close = start + 1;
2444
+ while (close < meta.length && meta[close] !== opening) close += meta[close] === "\\" && meta[close + 1] === opening ? 2 : 1;
2445
+ return close < meta.length ? close + 1 : close;
2446
+ }
2447
+ if (opening !== "{") return void 0;
2448
+ let depth = 0;
2449
+ let quote = "";
2450
+ for (let index = start; index < meta.length; index++) {
2451
+ const char = meta[index];
2452
+ if (quote) {
2453
+ if (char === quote) quote = "";
2454
+ } else if (char === '"' || char === "'") quote = char;
2455
+ else if (char === "{") depth++;
2456
+ else if (char === "}" && --depth === 0) return index + 1;
2457
+ }
2458
+ return meta.length;
2459
+ }
2460
+ function tokens(meta) {
2461
+ const result = [];
2462
+ let index = 0;
2463
+ while (index < meta.length) {
2464
+ if (/\s/.test(meta[index])) {
2465
+ index++;
2466
+ continue;
2467
+ }
2468
+ const start = index;
2469
+ const delimited = delimitedEnd(meta, index);
2470
+ if (delimited !== void 0) {
2471
+ result.push({ start, end: delimited, value: meta.slice(start, delimited), quoted: meta[start] !== "{", group: meta[start] === "{" });
2472
+ index = delimited;
2473
+ continue;
2474
+ }
2475
+ const key = /^([A-Za-z][\w-]*)=/.exec(meta.slice(index));
2476
+ if (key) {
2477
+ index += key[0].length;
2478
+ const groupEnd = delimitedEnd(meta, index);
2479
+ if (groupEnd !== void 0) index = groupEnd;
2480
+ else while (index < meta.length && !/\s/.test(meta[index])) index++;
2481
+ result.push({ start, end: index, key: key[1], value: meta.slice(start, index) });
2482
+ continue;
2483
+ }
2484
+ while (index < meta.length && !/\s/.test(meta[index])) {
2485
+ if (index > start && meta[index] === "{" && /^\{[\d,\s-]*\}/.test(meta.slice(index))) break;
2486
+ index++;
2487
+ }
2488
+ result.push({ start, end: index, value: meta.slice(start, index) });
2489
+ }
2490
+ return result;
2491
+ }
2492
+ function normalizeMintlifyCodeFences(body, warn) {
2493
+ const tree = unified2().use(remarkParse2).parse(body);
2494
+ const edits = [];
2495
+ const removed = /* @__PURE__ */ new Set();
2496
+ const unknown = /* @__PURE__ */ new Set();
2497
+ const supported = /* @__PURE__ */ new Set(["title", "filename", "framework", "tag", "icon", "highlight", "focus", "wrap", "lines", "expandable", "nocopy"]);
2498
+ function visit(node) {
2499
+ if (node.type === "code" && node.position?.start.offset !== void 0) {
2500
+ const start = node.position.start.offset;
2501
+ const lineEnd = body.indexOf("\n", start);
2502
+ const end = lineEnd < 0 ? body.length : lineEnd;
2503
+ const line = body.slice(start, end);
2504
+ const opening = /^([`~]{3,}[^\s]*)([ \t]+)(.*?)(\r?)$/.exec(line);
2505
+ if (opening) {
2506
+ const meta = opening[3];
2507
+ const kept = tokens(meta).filter((token) => {
2508
+ if (token.key === "theme" || !token.key && !token.quoted && !token.group && token.value === "twoslash") {
2509
+ removed.add(token.key ?? token.value);
2510
+ return false;
2511
+ }
2512
+ if (token.key && !supported.has(token.key)) unknown.add(token.key);
2513
+ return true;
2514
+ });
2515
+ const normalized = kept.map((token) => meta.slice(token.start, token.end)).join(" ");
2516
+ if (normalized !== meta && kept.length !== tokens(meta).length) edits.push({ start, end, value: `${opening[1]}${normalized ? ` ${normalized}` : ""}${opening[4]}` });
2517
+ }
2518
+ }
2519
+ for (const child of node.children ?? []) visit(child);
2520
+ }
2521
+ visit(tree);
2522
+ if (removed.size) warn?.(`Removed unsupported Mintlify code-fence options: ${[...removed].join(", ")}. Code examples and supported display options were preserved.`);
2523
+ if (unknown.size) warn?.(`Unsupported Mintlify code-fence options are ignored: ${[...unknown].join(", ")}. Review these renderer-specific options.`);
2524
+ return edits.sort((a, b) => b.start - a.start).reduce((text2, edit) => text2.slice(0, edit.start) + edit.value + text2.slice(edit.end), body);
2525
+ }
2526
+
2390
2527
  // src/mdx.ts
2391
2528
  function titleFromId(id) {
2392
2529
  const value = id.split("/").at(-1) ?? id;
2393
2530
  if (value === "introduction") return "Introduction";
2394
2531
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2395
2532
  }
2396
- var descriptionParser = unified2().use(remarkParse2).use(remarkGfm2).use(remarkMdx2);
2397
- function plainText(node) {
2533
+ var descriptionParser = unified3().use(remarkParse3).use(remarkGfm2).use(remarkMdx2);
2534
+ function plainText(node, elements) {
2535
+ if (elements && (node.type === "mdxJsxTextElement" || node.type === "mdxJsxFlowElement")) {
2536
+ elements.found = true;
2537
+ return "";
2538
+ }
2398
2539
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
2399
2540
  if (node.type === "image") return node.alt ?? "";
2400
2541
  if (node.type === "break") return " ";
2401
- return (node.children ?? []).map(plainText).join("");
2542
+ return (node.children ?? []).map((child) => plainText(child, elements)).join("");
2543
+ }
2544
+ function headingText(content, definitions, elements) {
2545
+ const references = [...content.matchAll(/\[([^\]\n]+)\]/g)].map((match) => definitions?.get(match[1].trim().replace(/\s+/g, " ").toLowerCase()) ?? "").join("\n");
2546
+ try {
2547
+ return plainText(descriptionParser.parse(`# ${content}
2548
+
2549
+ ${references}`), elements).trim();
2550
+ } catch {
2551
+ return content;
2552
+ }
2553
+ }
2554
+ function mdxTextWithoutInlineElements(content, definitions) {
2555
+ if (!/<[A-Za-z]/.test(content)) return void 0;
2556
+ const elements = { found: false };
2557
+ const text2 = headingText(content, definitions, elements);
2558
+ return elements.found && /[\p{L}\p{M}\p{N}]/u.test(text2) ? text2 : void 0;
2402
2559
  }
2403
2560
  function firstParagraph(content) {
2404
2561
  function findParagraph(node) {
@@ -2418,6 +2575,23 @@ function firstParagraph(content) {
2418
2575
  return "";
2419
2576
  }
2420
2577
  }
2578
+ var linkParser = unified3().use(remarkParse3);
2579
+ function withoutMarkdownLinks(text2) {
2580
+ const links = [];
2581
+ const collect = (node) => {
2582
+ if (node.type === "link") {
2583
+ const first = node.children?.[0]?.position?.start.offset;
2584
+ const last = node.children?.at(-1)?.position?.end.offset;
2585
+ const start = node.position?.start.offset;
2586
+ const end = node.position?.end.offset;
2587
+ if (start !== void 0 && end !== void 0) links.push({ start, end, textStart: first ?? start, textEnd: last ?? start });
2588
+ return;
2589
+ }
2590
+ for (const child of node.children ?? []) collect(child);
2591
+ };
2592
+ collect(linkParser.parse(text2));
2593
+ return links.reduceRight((result, link) => result.slice(0, link.start) + result.slice(link.textStart, link.textEnd) + result.slice(link.end), text2);
2594
+ }
2421
2595
  function docusaurusAdmonitionTag(kind) {
2422
2596
  if (kind === "danger") return "Error";
2423
2597
  if (kind === "info") return "Info";
@@ -2622,9 +2796,9 @@ ${trimmed.slice(0, marker).trimEnd()}`;
2622
2796
  function headingIdMarker(id) {
2623
2797
  return `{/* #${id} */}`;
2624
2798
  }
2625
- var HEADING_ID_WORD = /[\p{L}\p{M}\p{N}_+&/@\u2019\u201c\u201d\u2014\u2013\u2192]/u;
2799
+ var HEADING_ID_WORD = /[\p{L}\p{M}\p{N}_+&/@%=<>|\u2019\u201c\u201d\u2014\u2013\u2192\u2026]/u;
2626
2800
  function mintlifyHeadingSlug(text2) {
2627
- const chars = [...text2.replace(/[\u200b*]/g, "").normalize("NFC").trim().toLowerCase()];
2801
+ const chars = [...text2.replace(/[\u200b*]/g, "").replace(/(?<=[\p{L}\p{M}\p{N}_])\(s\)(?![\p{L}\p{M}\p{N}_])/gu, "s").normalize("NFC").trim().toLowerCase()];
2628
2802
  const isWord = (char) => char !== void 0 && HEADING_ID_WORD.test(char);
2629
2803
  let id = "";
2630
2804
  let afterDash = false;
@@ -2636,11 +2810,11 @@ function mintlifyHeadingSlug(text2) {
2636
2810
  if (isWord(char)) {
2637
2811
  id += char;
2638
2812
  afterDash = false;
2639
- } else if (/[\s-]/.test(char) || char === "." && isWord(after) || char === "(" && isWord(before) && (isWord(after) || after === "{") || char === ")" && after !== void 0 && !/\s/.test(after)) {
2813
+ } else if (/[\s-]/.test(char) || char === "." && (isWord(after) || after === ",") || char === "(" && isWord(before) && (isWord(after) || after === "{") || char === ")" && after !== void 0 && !/\s/.test(after)) {
2640
2814
  if (id && !afterDash) id += "-";
2641
2815
  afterDash = true;
2642
2816
  endsWithDash = true;
2643
- } else if (char === "{" || char === ":" || char === '"') {
2817
+ } else if (char === "{" || char === ":" || char === '"' || char === ",") {
2644
2818
  afterDash = false;
2645
2819
  }
2646
2820
  });
@@ -2650,17 +2824,77 @@ function thallyHeadingSlug(text2) {
2650
2824
  return text2.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
2651
2825
  }
2652
2826
  function typesetQuotes(text2) {
2827
+ const opensClosed = text2.startsWith('"') && !text2.trimEnd().endsWith('"');
2653
2828
  return text2.split(/(`[^`]*`|<[^>]*>|\]\([^)]*\))/).map((part, index) => {
2654
2829
  if (index % 2) return part;
2655
- return part.replace(/(^|[\s([{\u2014\u2013])"/g, "$1\u201C").replace(/"/g, "\u201D").replace(/(^|[\s([{\u2014\u2013\u201c])'(?=\S)/g, "$1\u2018").replace(/'/g, "\u2019");
2830
+ return part.replace(/(^|[\s([{\u2014\u2013])"/g, (match, before) => opensClosed && index === 0 && before === "" ? match : `${before}\u201C`).replace(/"/g, "\u201D").replace(/(^|[\s([{\u2014\u2013\u201c])'(?=\S)(?!&lt;|<)/g, "$1\u2018").replace(/'/g, "\u2019");
2656
2831
  }).join("");
2657
2832
  }
2833
+ function typesetEllipses(text2) {
2834
+ return text2.split(/(`[^`]*`|<[^>]*>|\]\([^)]*\))/).map((part, index) => index % 2 ? part : part.replace(/\.{3}/g, "\u2026")).join("");
2835
+ }
2658
2836
  function plainHeadingText(source) {
2659
- return source.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(?<!\\)(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
2837
+ const unlinked = !source.includes("`") ? source : source.replace(/(`[^`]*`)|!?\[((?:`[^`]*`|[^\]`])*)\]\([^)]*\)/g, (_match, code, text2 = "") => code ?? text2);
2838
+ return unlinked.split(/(`[^`]*`)/).map((part, index) => index % 2 ? part.slice(1, -1) : part.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/(?<!\\)(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1")).join("");
2839
+ }
2840
+ function staticAttributeString(value) {
2841
+ if (typeof value === "string") return value;
2842
+ const expression = value?.value;
2843
+ if (typeof expression !== "string") return void 0;
2844
+ return expression.match(/^\s*(?:"([^"\\]*)"|'([^'\\]*)'|`((?:[^`\\$]|\$(?!\{))*)`)\s*$/)?.slice(1).find((part) => part !== void 0);
2845
+ }
2846
+ function pageIdClaims(body) {
2847
+ let tree;
2848
+ try {
2849
+ tree = descriptionParser.parse(body);
2850
+ } catch {
2851
+ return { free: () => false };
2852
+ }
2853
+ const owners = /* @__PURE__ */ new Map();
2854
+ const headingLines = /* @__PURE__ */ new Set();
2855
+ let claims = 0;
2856
+ const root = (id) => id.toLowerCase().replace(/-\d+$/, "");
2857
+ const claim = (owner, ...ids) => {
2858
+ for (const id of ids) {
2859
+ const key = root(id);
2860
+ const held = owners.get(key);
2861
+ if (!key) continue;
2862
+ if (!held) owners.set(key, { owner, shared: false });
2863
+ else if (held.owner !== owner) held.shared = true;
2864
+ }
2865
+ };
2866
+ const visit = (node) => {
2867
+ const line = node.position?.start.line ?? 0;
2868
+ if (node.type === "heading" && (node.depth ?? 0) >= 2) {
2869
+ headingLines.add(line);
2870
+ const text2 = typesetQuotes(plainText(node));
2871
+ claim(`heading ${line}`, mintlifyHeadingSlug(text2), thallyHeadingSlug(text2), thallyHeadingSlug(plainText(node, { found: false })));
2872
+ for (const child of node.children ?? []) {
2873
+ const authored = child.type === "mdxTextExpression" ? child.value?.match(/^\s*\/\*\s*#(\S+?)\s*\*\/\s*$/)?.[1] : void 0;
2874
+ if (authored) claim(`claim ${claims++}`, authored);
2875
+ }
2876
+ } else if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
2877
+ for (const { type, name, value } of node.attributes ?? []) {
2878
+ const text2 = type === "mdxJsxAttribute" ? staticAttributeString(value) : void 0;
2879
+ if (text2 === void 0) continue;
2880
+ if (name === "id") claim(`claim ${claims++}`, text2);
2881
+ else if (name === "title") claim(`claim ${claims++}`, mintlifyHeadingSlug(text2), thallyHeadingSlug(text2));
2882
+ }
2883
+ }
2884
+ for (const child of node.children ?? []) visit(child);
2885
+ };
2886
+ visit(tree);
2887
+ return {
2888
+ free: (id, line) => {
2889
+ const held = owners.get(root(id));
2890
+ return headingLines.has(line) && (!held || held.owner === `heading ${line}` && !held.shared);
2891
+ }
2892
+ };
2660
2893
  }
2661
2894
  function markMintlifyHeadings(body) {
2662
2895
  let fence;
2663
- return body.split("\n").map((line) => {
2896
+ let claims;
2897
+ return body.split("\n").map((line, index) => {
2664
2898
  const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
2665
2899
  if (marker) {
2666
2900
  if (!fence) fence = marker[1];
@@ -2674,8 +2908,13 @@ function markMintlifyHeadings(body) {
2674
2908
  const source = typesetQuotes(explicit ? explicit[1] : heading[2]);
2675
2909
  if (explicit) return `${heading[1]}${source}${explicit[2]}`;
2676
2910
  const text2 = plainHeadingText(source);
2677
- const id = mintlifyHeadingSlug(text2);
2678
- return `${heading[1]}${source}${id && id !== thallyHeadingSlug(text2) ? ` ${headingIdMarker(id)}` : ""}`;
2911
+ const id = mintlifyHeadingSlug(plainHeadingText(typesetEllipses(source)));
2912
+ let marked = id !== thallyHeadingSlug(text2);
2913
+ if (id && !marked) {
2914
+ const visible = mdxTextWithoutInlineElements(source);
2915
+ if (visible !== void 0 && id !== thallyHeadingSlug(visible)) marked = (claims ??= pageIdClaims(body)).free(id, index + 1);
2916
+ }
2917
+ return `${heading[1]}${source}${id && marked ? ` ${headingIdMarker(id)}` : ""}`;
2679
2918
  }).join("\n");
2680
2919
  }
2681
2920
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
@@ -2691,6 +2930,7 @@ function removeGlobalDocusaurusImports(body) {
2691
2930
  (statement, component) => GLOBAL_DOCUSARUS_COMPONENTS.has(component) ? "" : statement
2692
2931
  ));
2693
2932
  }
2933
+ var PLAIN_TEXT = new RegExp("^\\p{L}(?:[\\p{L}\\p{N} ,.;'?%$=+-]*[\\p{L}\\p{N}.])?$", "u");
2694
2934
  function leadingPageHeading(body) {
2695
2935
  if (!/^ {0,3}(?:#(?:[ \t]|$)|=+[ \t\r]*$)/m.test(body)) return void 0;
2696
2936
  try {
@@ -2803,6 +3043,27 @@ function normalizeFernFileTrees(body) {
2803
3043
  }
2804
3044
  ));
2805
3045
  }
3046
+ function unwrapHtmlCommentInMdxComment(body) {
3047
+ return replaceOutsideCode(body, (text2) => {
3048
+ const opener = /\{\s*\/\*\s*<!--/g;
3049
+ const closer = /\s*\*\/\s*\}/y;
3050
+ let out = "";
3051
+ let copied = 0;
3052
+ let dash = -2;
3053
+ let star = -2;
3054
+ for (let found = opener.exec(text2); found; found = opener.exec(text2)) {
3055
+ const inner = opener.lastIndex;
3056
+ if (dash !== -1 && dash < inner) dash = text2.indexOf("-->", inner);
3057
+ if (dash === -1) break;
3058
+ if (star !== -1 && star < inner) star = text2.indexOf("*/", inner);
3059
+ closer.lastIndex = dash + 3;
3060
+ if (star !== -1 && star < dash || !closer.test(text2)) continue;
3061
+ out += `${text2.slice(copied, found.index)}{/*${text2.slice(inner, dash)}*/}`;
3062
+ copied = opener.lastIndex = closer.lastIndex;
3063
+ }
3064
+ return out + text2.slice(copied);
3065
+ });
3066
+ }
2806
3067
  function normalizeFernCallouts(body) {
2807
3068
  return replaceOutsideCode(body, (whole) => {
2808
3069
  const openTags = [];
@@ -3367,6 +3628,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
3367
3628
  rewritten = rewritten.replace(/^\n{2,}/, "\n");
3368
3629
  }
3369
3630
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
3631
+ rewritten = unwrapHtmlCommentInMdxComment(rewritten);
3370
3632
  rewritten = replaceOutsideCode(rewritten, (segment) => {
3371
3633
  let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
3372
3634
  if (runDocusaurus) {
@@ -3396,8 +3658,15 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
3396
3658
  }
3397
3659
  return normalizeFlowParagraphContainers(normalized);
3398
3660
  }
3661
+ var SITE_PATH_LINK = /^\/(?:[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*\/?)?(?:\?(?:[A-Za-z0-9._~=&-]|%[0-9A-Fa-f]{2})*)?(?:#[A-Za-z0-9._~-]+)?$/;
3662
+ var MAX_SITE_PATH_LINK_LENGTH = 2e3;
3663
+ function sitePathLink(url) {
3664
+ if (url.length > MAX_SITE_PATH_LINK_LENGTH || url.includes("__ESC_") || !SITE_PATH_LINK.test(url)) return void 0;
3665
+ return url.split(/[?#]/, 1)[0].split("/").some((segment) => segment === "." || segment === "..") ? void 0 : url;
3666
+ }
3399
3667
  function externalPageUrl(value) {
3400
3668
  const url = typeof value === "string" ? value.trim() : "";
3669
+ if (url.startsWith("/")) return sitePathLink(url);
3401
3670
  if (!/^https?:\/\/[^\s]+$/i.test(url)) return void 0;
3402
3671
  try {
3403
3672
  new URL(url);
@@ -3408,16 +3677,46 @@ function externalPageUrl(value) {
3408
3677
  }
3409
3678
  function parseMarkdownPage(input) {
3410
3679
  const parsed = parseFrontmatter(input.raw);
3680
+ if (input.platform === "mintlify") {
3681
+ const supported = /* @__PURE__ */ new Set([
3682
+ "title",
3683
+ "description",
3684
+ "sidebarTitle",
3685
+ "navTitle",
3686
+ "icon",
3687
+ "iconType",
3688
+ "tag",
3689
+ "badge",
3690
+ "keywords",
3691
+ "mode",
3692
+ "hidden",
3693
+ "noindex",
3694
+ "url",
3695
+ "openapi",
3696
+ "api",
3697
+ "authMethod",
3698
+ "playground",
3699
+ // Access settings are enforced by the repository classifier, never lost here.
3700
+ "groups",
3701
+ "public",
3702
+ ...PAGE_META_FIELDS.map(([field]) => field)
3703
+ ]);
3704
+ const dropped = Object.keys(parsed.data).filter((key) => !supported.has(key));
3705
+ if (dropped.length) input.warn?.(`Unsupported Mintlify frontmatter fields were omitted: ${dropped.join(", ")}. Review their behavior before publishing.`);
3706
+ }
3411
3707
  const fallbackIdentity = {
3412
3708
  id: input.id,
3413
3709
  navigationId: input.navigationId ?? input.id,
3414
3710
  ...input.locale ? { locale: input.locale } : {}
3415
3711
  };
3416
3712
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
3417
- const body = removeUndefinedExpressions(injectReactHookImports(normalizeMdx(parsed.content, input.platform)), input.warn).trim();
3713
+ const normalized = normalizeMdx(parsed.content, input.platform);
3714
+ const body = removeUndefinedExpressions(injectReactHookImports(input.platform === "mintlify" ? normalizeMintlifyCodeFences(normalized, input.warn) : normalized), input.warn).trim();
3418
3715
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
3419
3716
  const heading = leadingPageHeading(body);
3420
- const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
3717
+ const authoredTitle = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title;
3718
+ const title = authoredTitle ?? titleFromId(identity.navigationId);
3719
+ const namesOperation = typeof parsed.data.openapi === "string" && Boolean(parsed.data.openapi.trim());
3421
3720
  const sidebarTitle = input.platform === "docusaurus" ? parsed.data.sidebar_label : parsed.data.sidebarTitle;
3422
3721
  const navTitle = typeof sidebarTitle === "string" && sidebarTitle.trim() ? sidebarTitle.trim() : typeof parsed.data.navTitle === "string" && parsed.data.navTitle.trim() ? parsed.data.navTitle.trim() : void 0;
3423
3722
  const icon = typeof parsed.data.icon === "string" && parsed.data.icon.trim() ? parsed.data.icon.trim() : void 0;
@@ -3430,18 +3729,25 @@ function parseMarkdownPage(input) {
3430
3729
  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;
3431
3730
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
3432
3731
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
3732
+ if (input.platform === "mintlify" && typeof parsed.data.url === "string" && parsed.data.url.trim().startsWith("/") && !externalPageUrl(parsed.data.url)) {
3733
+ const shown = parsed.data.url.trim();
3734
+ input.warn?.(`Page "${identity.id}": its "url" frontmatter ${JSON.stringify(shown.length > 80 ? `${shown.slice(0, 80)}...` : shown)} was not turned into a link, so the page was kept with its own content. Only a site path made of letters, digits and . _ ~ - segments, with an optional ?key=value query and #id fragment, can become a link.`);
3735
+ }
3433
3736
  const externalUrl = input.platform === "mintlify" ? externalPageUrl(parsed.data.url) : void 0;
3434
- const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : typeof parsed.data.openapi === "string" && parsed.data.openapi.trim() ? "" : firstParagraph(body);
3737
+ const authoredDescription = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : void 0;
3738
+ const description = authoredDescription ?? (namesOperation ? "" : firstParagraph(body));
3435
3739
  const page = {
3436
3740
  id: identity.id,
3437
3741
  navigationId: identity.navigationId,
3438
3742
  locale: identity.locale,
3439
3743
  title,
3744
+ ...authoredTitle === void 0 && namesOperation ? { titleFromFileName: true } : {},
3440
3745
  navTitle,
3441
3746
  icon,
3442
3747
  iconType,
3443
3748
  description,
3444
- ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
3749
+ // A description taken from the body is the paragraph the body already shows, so it must not also appear under the title.
3750
+ ...input.platform === "docusaurus" || input.platform === "fern" || authoredDescription === void 0 && description !== "" ? { descriptionPlacement: "body" } : {},
3445
3751
  badge,
3446
3752
  keywords,
3447
3753
  mode,
@@ -3456,6 +3762,12 @@ function parseMarkdownPage(input) {
3456
3762
  source: input.source,
3457
3763
  ...parsed.error ? { frontmatterError: parsed.error } : {}
3458
3764
  };
3765
+ const writtenDescription = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
3766
+ const plainDescription2 = withoutMarkdownLinks(writtenDescription);
3767
+ if (plainDescription2 !== writtenDescription) {
3768
+ page.description = plainDescription2;
3769
+ input.warn?.(`Page "${identity.id}": its description had links, which Thally shows as plain text; the link text was kept.`);
3770
+ }
3459
3771
  resolvePageDisplayHeading(page, input.platform);
3460
3772
  return page;
3461
3773
  }
@@ -3487,7 +3799,7 @@ function apiFrontmatter(data, warn) {
3487
3799
  }
3488
3800
 
3489
3801
  // src/url-static-content.ts
3490
- import { createHash as createHash2 } from "crypto";
3802
+ import { createHash as createHash3 } from "crypto";
3491
3803
  import { load as load4 } from "cheerio";
3492
3804
  function projectMigrationSvg(html) {
3493
3805
  const $ = load4(html, { xmlMode: true }, false);
@@ -3496,7 +3808,7 @@ function projectMigrationSvg(html) {
3496
3808
  if (svg.attr("xmlns") === void 0) svg.attr("xmlns", "http://www.w3.org/2000/svg");
3497
3809
  const content = Buffer.from($.xml(svg));
3498
3810
  if (!isPassiveSvg(content)) return null;
3499
- const sha256 = createHash2("sha256").update(content).digest("hex");
3811
+ const sha256 = createHash3("sha256").update(content).digest("hex");
3500
3812
  const labelled = (svg.attr("aria-labelledby") ?? "").split(/\s+/).filter(Boolean).map((id) => svg.find("[id]").filter((_index, node) => $(node).attr("id") === id).text().trim()).filter(Boolean);
3501
3813
  const alternative = svg.attr("aria-label")?.trim() || labelled.join(" ") || svg.children("title").text().trim() || svg.children("desc").text().trim() || svg.find("text").text().trim();
3502
3814
  return { asset: { path: `migration-assets/${sha256}.svg`, content }, alternative };
@@ -3688,8 +4000,8 @@ var MINTLIFY_ARTICLE_LABELS = [
3688
4000
  { baseUrl: "Select base URL", permalink: "Navigate to header" },
3689
4001
  { baseUrl: "\u9009\u62E9\u57FA\u7840URL", permalink: "\u5BFC\u822A\u5230\u6807\u9898" }
3690
4002
  ];
3691
- var remoteMarkdownParser = unified3().use(remarkParse3).use(remarkMdx3);
3692
- var embeddedOpenApiParser = unified3().use(remarkParse3);
4003
+ var remoteMarkdownParser = unified4().use(remarkParse4).use(remarkMdx3);
4004
+ var embeddedOpenApiParser = unified4().use(remarkParse4);
3693
4005
  function validateMigrationUrl(value) {
3694
4006
  let url;
3695
4007
  try {
@@ -3729,10 +4041,10 @@ async function readResponseBody(response) {
3729
4041
  }
3730
4042
  return new TextDecoder().decode(body);
3731
4043
  }
3732
- var defaultMigrationFetcher = async (url, request) => {
4044
+ var defaultMigrationFetcher = async (url, request2) => {
3733
4045
  const response = await fetch(url, {
3734
4046
  headers: {
3735
- Accept: request.accept,
4047
+ Accept: request2.accept,
3736
4048
  "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
3737
4049
  },
3738
4050
  redirect: "follow",
@@ -4206,7 +4518,7 @@ function extractEmbeddedOpenApi(content) {
4206
4518
  const opening = lines[0].match(/^ {0,3}(`{3,}|~{3,})/);
4207
4519
  const closing = lines.at(-1)?.match(/^ {0,3}(`{3,}|~{3,})[\t ]*$/);
4208
4520
  if (!opening || !closing || opening[1][0] !== closing[1][0] || closing[1].length < opening[1].length) return { content };
4209
- const parsed = parseYaml2(node.value ?? "", { maxAliasCount: 50 });
4521
+ const parsed = parseYaml3(node.value ?? "", { maxAliasCount: 50 });
4210
4522
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
4211
4523
  const document = parsed;
4212
4524
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -4667,6 +4979,8 @@ ${fence}
4667
4979
  navigationId: id,
4668
4980
  title,
4669
4981
  description: description || plainDescription(body) || `Documentation imported from ${document.finalUrl.hostname}.`,
4982
+ // A description taken from the page text is already in the body; only one the site published is shown under the title.
4983
+ ...description ? {} : { descriptionPlacement: "body" },
4670
4984
  keywords: [],
4671
4985
  body,
4672
4986
  source: document.finalUrl.toString()
@@ -4736,7 +5050,7 @@ function observedMigrationArticleIdentity(document) {
4736
5050
  canonical2.pathname = canonical2.pathname.replace(/\/+$/, "") || "/";
4737
5051
  const removed = [...source.searchParams].filter(([key, value]) => !canonical2.searchParams.getAll(key).includes(value));
4738
5052
  const sectionAnchor = removed.length === 1 && anchors.includes(removed[0][1]) ? removed[0][1] : void 0;
4739
- return { canonicalUrl: canonical2.href, articleSha256: createHash3("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
5053
+ return { canonicalUrl: canonical2.href, articleSha256: createHash4("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
4740
5054
  }
4741
5055
 
4742
5056
  // src/url-api-schema-controls.ts
@@ -4829,8 +5143,8 @@ function apiSchemaTabGroups(body, document, operation) {
4829
5143
  let schema;
4830
5144
  let scope;
4831
5145
  if (title === "body") {
4832
- const request = resolve2(document, selected.requestBody);
4833
- schema = object2(object2(request?.content)?.[media[0]])?.schema;
5146
+ const request2 = resolve2(document, selected.requestBody);
5147
+ schema = object2(object2(request2?.content)?.[media[0]])?.schema;
4834
5148
  scope = `request:${media[0]}`;
4835
5149
  } else if (title === "response") {
4836
5150
  const statuses = heading.find('button[aria-haspopup="menu"]').toArray().map((button) => $(button).text().trim()).filter((text2) => /^(?:[1-5]\d{2}|default)$/.test(text2));
@@ -4929,8 +5243,8 @@ function apiSchemaDisclosureGroups(body, document, operation) {
4929
5243
  schemas = [object2(object2(response?.content)?.[media[0]])?.schema];
4930
5244
  prefix = "response";
4931
5245
  } else if (media.length === 1 && !statuses.length) {
4932
- const request = resolve2(document, selected.requestBody);
4933
- schemas = [object2(object2(request?.content)?.[media[0]])?.schema];
5246
+ const request2 = resolve2(document, selected.requestBody);
5247
+ schemas = [object2(object2(request2?.content)?.[media[0]])?.schema];
4934
5248
  prefix = "body";
4935
5249
  } else if (!media.length && !statuses.length) {
4936
5250
  const head = $(chain[0]).find(".param-head[id]").filter((_index, element) => $(element).closest(fieldSelector)[0] === chain[0]).first().attr("id") ?? "";
@@ -5063,8 +5377,8 @@ function apiOperationCodeControls(body, specification, operation) {
5063
5377
  const metadata = object2(resolveValue(value));
5064
5378
  const declaration = resolveValue(metadata?.openapi);
5065
5379
  if (typeof declaration !== "string") return false;
5066
- const tokens = declaration.trim().split(/\s+/);
5067
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens.at(-1) === operation.path;
5380
+ const tokens2 = declaration.trim().split(/\s+/);
5381
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens2.at(-1) === operation.path;
5068
5382
  };
5069
5383
  const active = /* @__PURE__ */ new Set();
5070
5384
  const visit = (input, provider, group, depth, owners = []) => {
@@ -5110,9 +5424,9 @@ function apiOperationCodeControls(body, specification, operation) {
5110
5424
  const hint = object2(resolveValue(scope?.pageMetadata)), metadata = object2(resolveValue(props.pageMetadata));
5111
5425
  const declaration = resolveValue(hint?.openapi);
5112
5426
  if (typeof declaration !== "string" || typeof hint?.href !== "string" || !hint.href || hint.href !== metadata?.href) return false;
5113
- const tokens = declaration.trim().split(/\s+/);
5427
+ const tokens2 = declaration.trim().split(/\s+/);
5114
5428
  const template = (path) => path.split("/").map((segment) => /^\{[^{}\/]+\}$/.test(segment) ? "{}" : segment).join("/");
5115
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens.at(-1) ?? "") === template(operation.path);
5429
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens2.at(-1) ?? "") === template(operation.path);
5116
5430
  })()) generatedControllers += 1;
5117
5431
  }
5118
5432
  let local = group;
@@ -5221,14 +5535,14 @@ function apiOperationCodeControls(body, specification, operation) {
5221
5535
 
5222
5536
  // src/url-run.ts
5223
5537
  var CONVERTER_VERSION = "url-evidence-v7";
5224
- var parser2 = unified4().use(remarkParse4).use(remarkMdx4);
5538
+ var parser2 = unified5().use(remarkParse5).use(remarkMdx4);
5225
5539
  var ACCEPT = "text/html,application/xhtml+xml";
5226
5540
  var MAX_RESPONSE_BYTES = 8e6;
5227
5541
  var MAX_ASSET_BYTES = 1e7;
5228
5542
  var MAX_RECORDS = 1e5;
5229
5543
  var LOCALES = /* @__PURE__ */ new Set(["ar", "cs", "da", "de", "el", "en", "es", "fi", "fr", "he", "hi", "hu", "id", "it", "ja", "ko", "ms", "nl", "no", "pl", "pt", "pt-br", "ro", "ru", "sk", "sv", "th", "tr", "uk", "vi", "zh", "zh-cn", "zh-hans", "zh-hant"]);
5230
5544
  function hash(value) {
5231
- return createHash4("sha256").update(value).digest("hex");
5545
+ return createHash5("sha256").update(value).digest("hex");
5232
5546
  }
5233
5547
  function canonicalJson(value) {
5234
5548
  const canonical2 = (value2) => Array.isArray(value2) ? value2.map(canonical2) : value2 !== null && typeof value2 === "object" ? Object.fromEntries(Object.entries(value2).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, field]) => [key, canonical2(field)])) : value2;
@@ -5749,7 +6063,7 @@ async function runUrlMigration(options) {
5749
6063
  } catch {
5750
6064
  }
5751
6065
  }
5752
- const capture = (request) => initial && canonical(request.url.href, sourceUrl) === sourceUrl ? initial : acquire(request);
6066
+ const capture = (request2) => initial && canonical(request2.url.href, sourceUrl) === sourceUrl ? initial : acquire(request2);
5753
6067
  const requestedScope = options.scopePath ?? options.checkpoint?.scopePath ?? inferredScope ?? (source.pathname.endsWith("/") ? source.pathname : source.pathname.slice(0, source.pathname.lastIndexOf("/")) || "/");
5754
6068
  let scopeEnd = requestedScope.length;
5755
6069
  while (scopeEnd > 0 && requestedScope.charCodeAt(scopeEnd - 1) === 47) scopeEnd--;
@@ -6493,16 +6807,16 @@ function rewriteMarkdownDestinations(body, rewrite) {
6493
6807
  }
6494
6808
 
6495
6809
  // src/components.ts
6496
- import { createHash as createHash5 } from "crypto";
6810
+ import { createHash as createHash6 } from "crypto";
6497
6811
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
6498
6812
  import { basename, dirname as dirname2, extname as extname3, posix as posix2, relative as relative3, resolve as resolve3 } from "path";
6499
- import postcss from "postcss";
6500
- import selectorParser from "postcss-selector-parser";
6813
+ import postcss2 from "postcss";
6814
+ import selectorParser2 from "postcss-selector-parser";
6501
6815
  import remarkMdx5 from "remark-mdx";
6502
- import remarkParse5 from "remark-parse";
6503
- import ts2 from "typescript";
6504
- import { unified as unified5 } from "unified";
6505
- var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
6816
+ import remarkParse6 from "remark-parse";
6817
+ import ts3 from "typescript";
6818
+ import { unified as unified6 } from "unified";
6819
+ var parser3 = unified6().use(remarkParse6).use(remarkMdx5);
6506
6820
  var DOCUSAURUS_THEME_SHIMS = {
6507
6821
  "@theme/CodeBlock": {
6508
6822
  filename: "docusaurus-code-block.tsx",
@@ -6542,32 +6856,32 @@ function applyReplacements(source, replacements) {
6542
6856
  );
6543
6857
  }
6544
6858
  function hash2(value) {
6545
- return createHash5("sha256").update(value).digest("hex").slice(0, 12);
6859
+ return createHash6("sha256").update(value).digest("hex").slice(0, 12);
6546
6860
  }
6547
6861
  function sourceFile(source, filename) {
6548
- return ts2.createSourceFile(filename, source, ts2.ScriptTarget.Latest, true, scriptKindFor(filename));
6862
+ return ts3.createSourceFile(filename, source, ts3.ScriptTarget.Latest, true, scriptKindFor(filename));
6549
6863
  }
6550
6864
  function mergeComponentRegistry(existing, incoming) {
6551
6865
  const fingerprint = hash2(incoming);
6552
6866
  const name = `MigratedRegistry${fingerprint}`;
6553
6867
  const registryPath = `./migrated-components-${fingerprint}`;
6554
6868
  const source = sourceFile(existing, "custom-components.tsx");
6555
- const alreadyImported = source.statements.some((statement) => ts2.isImportDeclaration(statement) && ts2.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === registryPath);
6869
+ const alreadyImported = source.statements.some((statement) => ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === registryPath);
6556
6870
  let initializer;
6557
6871
  for (const statement of source.statements) {
6558
- if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword)) continue;
6872
+ if (!ts3.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword)) continue;
6559
6873
  for (const declaration of statement.declarationList.declarations) {
6560
- if (ts2.isIdentifier(declaration.name) && declaration.name.text === "customComponents") initializer = declaration.initializer;
6874
+ if (ts3.isIdentifier(declaration.name) && declaration.name.text === "customComponents") initializer = declaration.initializer;
6561
6875
  }
6562
6876
  }
6563
- while (initializer && (ts2.isAsExpression(initializer) || ts2.isSatisfiesExpression(initializer) || ts2.isParenthesizedExpression(initializer))) initializer = initializer.expression;
6564
- if (!initializer || !ts2.isObjectLiteralExpression(initializer)) {
6877
+ while (initializer && (ts3.isAsExpression(initializer) || ts3.isSatisfiesExpression(initializer) || ts3.isParenthesizedExpression(initializer))) initializer = initializer.expression;
6878
+ if (!initializer || !ts3.isObjectLiteralExpression(initializer)) {
6565
6879
  throw new Error("Cannot safely merge src/mdx/custom-components.tsx: export customComponents as an object literal before importing components. The existing registry was preserved.");
6566
6880
  }
6567
6881
  const content = alreadyImported ? existing : applyReplacements(existing, [
6568
6882
  {
6569
- start: source.statements.filter((statement) => ts2.isExpressionStatement(statement) && ts2.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
6570
- end: source.statements.filter((statement) => ts2.isExpressionStatement(statement) && ts2.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
6883
+ start: source.statements.filter((statement) => ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
6884
+ end: source.statements.filter((statement) => ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
6571
6885
  value: `
6572
6886
  import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
6573
6887
  `
@@ -6619,6 +6933,32 @@ function renderPage(bundle, page) {
6619
6933
  function isIntegrationsObject(value) {
6620
6934
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6621
6935
  }
6936
+ function mergeRedirects(existing = [], incoming = []) {
6937
+ const redirects = [...existing];
6938
+ const key = (source) => literalRoute(source) ?? trimTrailingSlashes(source).toLowerCase();
6939
+ const destinationKey = (destination) => literalRoute(destinationRoute(destination) ?? "");
6940
+ const sources = new Set(existing.map((redirect) => key(redirect.source)));
6941
+ for (const redirect of incoming) {
6942
+ const source = key(redirect.source);
6943
+ if (sources.has(source)) continue;
6944
+ const visited = /* @__PURE__ */ new Set([source]);
6945
+ let destination = destinationKey(redirect.destination);
6946
+ let cycle = false;
6947
+ while (destination) {
6948
+ if (visited.has(destination)) {
6949
+ cycle = true;
6950
+ break;
6951
+ }
6952
+ visited.add(destination);
6953
+ const next = redirects.find((candidate) => literalRoute(candidate.source) === destination);
6954
+ destination = next ? destinationKey(next.destination) : null;
6955
+ }
6956
+ if (cycle) continue;
6957
+ redirects.push(redirect);
6958
+ sources.add(source);
6959
+ }
6960
+ return redirects;
6961
+ }
6622
6962
  function mergeMigrationConfig(existing, incoming) {
6623
6963
  const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
6624
6964
  const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
@@ -6686,6 +7026,10 @@ function mergeMigrationConfig(existing, incoming) {
6686
7026
  ...incoming,
6687
7027
  ...existing,
6688
7028
  tabs,
7029
+ ...existing.redirects || incoming.redirects ? { redirects: mergeRedirects(existing.redirects, incoming.redirects) } : {},
7030
+ // A starter's primary action must not discard a newly imported logo.
7031
+ // Owner-authored fields, including an explicit null logo, remain authoritative.
7032
+ ...incoming.navbar || existing.navbar ? { navbar: { ...incoming.navbar, ...existing.navbar } } : {},
6689
7033
  // Collection presentation belongs to the imported information
6690
7034
  // architecture; starter defaults must not coerce source dropdowns to tabs.
6691
7035
  navigation: incoming.navigation ?? existing.navigation,