@thallylabs/migrate 0.2.40 → 0.2.42

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
@@ -1370,8 +1370,29 @@ ${trimmed.slice(0, marker).trimEnd()}`;
1370
1370
  function headingIdMarker(id) {
1371
1371
  return `{/* #${id} */}`;
1372
1372
  }
1373
+ var HEADING_ID_WORD = /[\p{L}\p{M}\p{N}_+&/@\u2019\u201c\u201d\u2014\u2013\u2192]/u;
1373
1374
  function mintlifyHeadingSlug(text3) {
1374
- return text3.replace(/\u200b/g, "").normalize("NFC").trim().toLowerCase().replace(/[.\s]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").replace(/[^\p{L}\p{M}\p{N}_\-+&/\u2019\u201c\u201d\u2014\u2013\u2192]/gu, "");
1375
+ const chars = [...text3.replace(/[\u200b*]/g, "").normalize("NFC").trim().toLowerCase()];
1376
+ const isWord = (char) => char !== void 0 && HEADING_ID_WORD.test(char);
1377
+ let id = "";
1378
+ let afterDash = false;
1379
+ let endsWithDash = false;
1380
+ chars.forEach((char, index) => {
1381
+ const before = chars[index - 1];
1382
+ const after = chars[index + 1];
1383
+ endsWithDash = false;
1384
+ if (isWord(char)) {
1385
+ id += char;
1386
+ afterDash = false;
1387
+ } else if (/[\s-]/.test(char) || char === "." && isWord(after) || char === "(" && isWord(before) && (isWord(after) || after === "{") || char === ")" && after !== void 0 && !/\s/.test(after)) {
1388
+ if (id && !afterDash) id += "-";
1389
+ afterDash = true;
1390
+ endsWithDash = true;
1391
+ } else if (char === "{" || char === ":" || char === '"') {
1392
+ afterDash = false;
1393
+ }
1394
+ });
1395
+ return endsWithDash ? id.replace(/-$/, "") : id;
1375
1396
  }
1376
1397
  function thallyHeadingSlug(text3) {
1377
1398
  return text3.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
@@ -1383,7 +1404,7 @@ function typesetQuotes(text3) {
1383
1404
  }).join("");
1384
1405
  }
1385
1406
  function plainHeadingText(source) {
1386
- 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");
1407
+ 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");
1387
1408
  }
1388
1409
  function markMintlifyHeadings(body) {
1389
1410
  let fence;
@@ -2587,7 +2608,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2587
2608
  }
2588
2609
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
2589
2610
  rewritten = replaceOutsideCode(rewritten, (segment) => {
2590
- let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
2611
+ 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>");
2591
2612
  if (runDocusaurus) {
2592
2613
  result = result.replace(/<TabItem\b([^<>]*)>/g, (_match, attributes) => {
2593
2614
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
@@ -2596,7 +2617,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2596
2617
  result = normalizeDocusaurusLinkTags(result).replace(/<(?:DocCardList|TOCInline)\b[^<>]*\/>/g, "");
2597
2618
  }
2598
2619
  if (runMintlify) {
2599
- 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>");
2620
+ result = normalizeMintlifyUpdateLabels(result).replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Callout(\s[^<>]*)?>/g, (tag) => tag.replace(/(\s)type=(?:"[^"]*"|'[^']*'|\{[^{}]*\})/, '$1type="info"')).replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
2600
2621
  result = pinStepsTitleSize(result);
2601
2622
  }
2602
2623
  if (runFern) {
@@ -2615,6 +2636,16 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2615
2636
  }
2616
2637
  return normalizeFlowParagraphContainers(normalized);
2617
2638
  }
2639
+ function externalPageUrl(value) {
2640
+ const url = typeof value === "string" ? value.trim() : "";
2641
+ if (!/^https?:\/\/[^\s]+$/i.test(url)) return void 0;
2642
+ try {
2643
+ new URL(url);
2644
+ return url;
2645
+ } catch {
2646
+ return void 0;
2647
+ }
2648
+ }
2618
2649
  function parseMarkdownPage(input) {
2619
2650
  const parsed = parseFrontmatter(input.raw);
2620
2651
  const fallbackIdentity = {
@@ -2639,6 +2670,7 @@ function parseMarkdownPage(input) {
2639
2670
  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;
2640
2671
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
2641
2672
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
2673
+ const externalUrl = input.platform === "mintlify" ? externalPageUrl(parsed.data.url) : void 0;
2642
2674
  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);
2643
2675
  const page = {
2644
2676
  id: identity.id,
@@ -2654,7 +2686,9 @@ function parseMarkdownPage(input) {
2654
2686
  keywords,
2655
2687
  mode,
2656
2688
  hidden: parsed.data.hidden === true ? true : void 0,
2657
- noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
2689
+ // The path of a page that links elsewhere redirects, so it has no business in a sitemap.
2690
+ noindex: parsed.data.noindex === true || parsed.data.noindex === "true" || externalUrl ? true : void 0,
2691
+ ...externalUrl ? { externalUrl } : {},
2658
2692
  ...Object.keys(meta).length > 0 ? { meta } : {},
2659
2693
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
2660
2694
  ...apiFrontmatter(parsed.data, input.warn),
@@ -4391,12 +4425,17 @@ function addMintlifyHomepageRedirects(config, pages) {
4391
4425
  const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
4392
4426
  if (!homepage) return config;
4393
4427
  const redirects = [...config.redirects ?? []];
4394
- const sources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
4428
+ const routeKey = (route) => (route.split(/[?#]/, 1)[0].replace(/\/$/, "") || "/").toLowerCase();
4429
+ const sources = new Set(redirects.map((redirect) => routeKey(redirect.source)));
4395
4430
  const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).filter((locale) => homepage !== "introduction" || pages.some((page) => page.locale === locale.code && page.navigationId === homepage)).map((locale) => locale.code)];
4431
+ const homeLinksElsewhere = pages.some((page) => page.id === "introduction" && page.externalUrl);
4396
4432
  for (const locale of roots) {
4433
+ if (!locale && homeLinksElsewhere) continue;
4397
4434
  const source = locale ? `/${locale}` : "/";
4398
4435
  if (sources.has(source.toLowerCase())) continue;
4399
- redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
4436
+ const destination = `${locale ? `/${locale}` : ""}/${homepage}`;
4437
+ if (redirects.some((redirect) => routeKey(redirect.source) === destination.toLowerCase() && routeKey(redirect.destination) === source.toLowerCase())) continue;
4438
+ redirects.push({ source, destination, permanent: true });
4400
4439
  }
4401
4440
  return { ...config, redirects };
4402
4441
  }
@@ -4593,7 +4632,7 @@ function mergeApiGroups(existing, incoming) {
4593
4632
  return merged.length > 0 ? merged : void 0;
4594
4633
  }
4595
4634
  var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
4596
- function mintlifyNavigationApiReferences(config) {
4635
+ function mintlifyNavigationApiReferences(config, tabFor) {
4597
4636
  const navigation = objectValue2(config.navigation) ?? config;
4598
4637
  const references = [];
4599
4638
  const defaultLanguage = pickDefaultLanguage(
@@ -4617,7 +4656,7 @@ function mintlifyNavigationApiReferences(config) {
4617
4656
  references.push({
4618
4657
  value,
4619
4658
  kind,
4620
- tabLabel,
4659
+ tabLabel: tabFor?.(node) ?? tabLabel,
4621
4660
  ...sibling,
4622
4661
  ...typeof directory2 === "string" && directory2.trim() ? { directory: directory2.trim() } : {},
4623
4662
  ...operations.length > 0 ? { operations } : {},
@@ -4972,6 +5011,21 @@ function convertNavigationValues(values, context) {
4972
5011
  return page ? [page] : [];
4973
5012
  });
4974
5013
  }
5014
+ var API_TAB_CONTAINER = /* @__PURE__ */ Symbol("apiTabContainer");
5015
+ function takeApiTabSlots(tabs) {
5016
+ const slots = [];
5017
+ const real = [];
5018
+ tabs.forEach((tab, index) => {
5019
+ const container = tab[API_TAB_CONTAINER];
5020
+ if (!container) {
5021
+ real.push(tab);
5022
+ return;
5023
+ }
5024
+ const template = Object.fromEntries(Object.entries(tab).filter(([key]) => key !== "groups"));
5025
+ slots.push({ container, slot: { tab: template, preceding: tabs.slice(0, index).map((earlier) => earlier.tab).reverse() } });
5026
+ });
5027
+ return { tabs: real, slots };
5028
+ }
4975
5029
  function convertContainerToTabs(containerValue, context, fallbackTab, trace, depth = 0) {
4976
5030
  const rawContainer = objectValue2(containerValue);
4977
5031
  if (!rawContainer) return [];
@@ -5048,6 +5102,17 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
5048
5102
  ...convertNavigationValues(values, context),
5049
5103
  ...Array.isArray(container.menu) ? convertMenuItems(container.menu, context) : []
5050
5104
  ];
5105
+ if (children.length === 0 && depth > 0 && context.operationGroups && !context.otherLanguage && !context.operations.gated && !context.operations.hidden && specSource(rawContainer.openapi) && operationGroups(rawContainer).length > 0) {
5106
+ const placeholder2 = {
5107
+ tab: fallbackTab,
5108
+ groups: [],
5109
+ ...containerPresentation(container),
5110
+ ...projectedHref(container.href) ? { href: projectedHref(container.href) } : {},
5111
+ ...container.hidden === true ? { hidden: true } : {},
5112
+ [API_TAB_CONTAINER]: rawContainer
5113
+ };
5114
+ return [placeholder2];
5115
+ }
5051
5116
  return children.length > 0 ? [{
5052
5117
  tab: fallbackTab,
5053
5118
  ...hasRootNodes ? { pages: children } : { groups: children },
@@ -5228,6 +5293,8 @@ function projectedCompatibleConfig(config, warnings) {
5228
5293
  return {
5229
5294
  // Mintlify pages show a group eyebrow above the title and no breadcrumb trail.
5230
5295
  breadcrumbs: false,
5296
+ // Mintlify typesets straight quotes in prose as curly ones.
5297
+ smartQuotes: true,
5231
5298
  // `styling.eyebrows: "breadcrumbs"` shows the whole group path; "section" (the default) shows the nearest group.
5232
5299
  ...objectValue2(config.styling)?.eyebrows === "breadcrumbs" ? { eyebrow: "path" } : {},
5233
5300
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
@@ -5479,6 +5546,9 @@ function projectMintlifyNavigation(config, options = {}) {
5479
5546
  }
5480
5547
  }
5481
5548
  }
5549
+ const taken = takeApiTabSlots(tabs);
5550
+ tabs = taken.tabs;
5551
+ const apiTabSlots = taken.slots;
5482
5552
  if (tabs.length === 0) {
5483
5553
  warnings.push({
5484
5554
  code: "unsupported-config",
@@ -5524,6 +5594,7 @@ function projectMintlifyNavigation(config, options = {}) {
5524
5594
  }) : [];
5525
5595
  if (versions.length > 1) {
5526
5596
  tabs = annotateVersions(tabs);
5597
+ for (const entry of apiTabSlots) [entry.slot.tab] = annotateVersions([entry.slot.tab]);
5527
5598
  for (const [locale, items] of Object.entries(localizedNavigation)) localizedNavigation[locale] = annotateVersions(items);
5528
5599
  }
5529
5600
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
@@ -5547,11 +5618,13 @@ function projectMintlifyNavigation(config, options = {}) {
5547
5618
  }];
5548
5619
  }) : [];
5549
5620
  collectReferenceAccess(navigation, OPEN_NAVIGATION_ACCESS, options.pathPrefix, appearances);
5621
+ const keepsCollectionNamedGroup = [tabs, ...Object.values(localizedNavigation)].some((items) => items.some((item) => item.groups?.some((group) => group.group === (item.displayLabel ?? item.tab))));
5550
5622
  return {
5551
5623
  docsConfig: {
5552
5624
  tabs,
5553
- ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 ? { navigation: {
5625
+ ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 || keepsCollectionNamedGroup ? { navigation: {
5554
5626
  ...projectionTrace.rootContainerKind === "dropdowns" ? { display: "dropdown" } : {},
5627
+ ...keepsCollectionNamedGroup ? { groupHeadings: "all" } : {},
5555
5628
  ...versions.length > 1 ? { versions } : {},
5556
5629
  ...shortcuts.length > 0 ? { shortcuts } : {}
5557
5630
  } } : {},
@@ -5563,6 +5636,7 @@ function projectMintlifyNavigation(config, options = {}) {
5563
5636
  pageReferences: references,
5564
5637
  referenceAccess: appearances,
5565
5638
  gatedSourceRefs,
5639
+ apiTabSlots,
5566
5640
  warnings
5567
5641
  };
5568
5642
  }
@@ -7786,7 +7860,10 @@ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
7786
7860
  "width",
7787
7861
  "height",
7788
7862
  "preload",
7789
- "aria-label"
7863
+ "aria-label",
7864
+ // Publisher asset provenance does not change playback and is never emitted.
7865
+ // Keep this explicit: arbitrary data attributes may drive custom controls.
7866
+ "data-path"
7790
7867
  ]);
7791
7868
  var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
7792
7869
  "src",
@@ -9074,6 +9151,9 @@ function destinationRoute(destination) {
9074
9151
  function pageRoutes(pages) {
9075
9152
  return pages.flatMap((page) => [`/${page.id}`, `/${page.id.replace(/\/index$/, "")}`]);
9076
9153
  }
9154
+ function pageEntryRoutes(page) {
9155
+ return [.../* @__PURE__ */ new Set([...pageRoutes([page]), ...page.id === "introduction" ? ["/"] : []])];
9156
+ }
9077
9157
  function servedRoutes(pages) {
9078
9158
  return new Set(pageRoutes(pages).map((route) => route.toLowerCase()));
9079
9159
  }
@@ -9138,11 +9218,13 @@ function dropConflictingRedirects(config, pages, warnings) {
9138
9218
  const loopingByCase = [];
9139
9219
  const folded = redirectChains();
9140
9220
  const asWritten = redirectChains();
9221
+ const rootRedirected = config.redirects.some((redirect) => literalRoute(redirect.source) === "/");
9141
9222
  const redirects = config.redirects.filter((redirect) => {
9142
9223
  const source = literalRoute(redirect.source);
9143
9224
  if (!source) return true;
9144
9225
  const writtenSource = exactRoute(redirect.source);
9145
- if (served.has(source)) {
9226
+ const retiresRootAlias = source === "/introduction" && !rootRedirected && destinationRoute(redirect.destination) === "/";
9227
+ if (served.has(source) && !retiresRootAlias) {
9146
9228
  const hides = servedAsWritten.has(writtenSource) ? hidden : hiddenByCase;
9147
9229
  hides.push(redirect.source);
9148
9230
  return false;
@@ -12337,12 +12419,19 @@ function mintlifyTopLevelApiReferences(config) {
12337
12419
  function specFileCandidates(files) {
12338
12420
  return files.filter((file) => /\.(?:json|ya?ml)$/i.test(file.relativePath)).map((file) => ({ sourcePath: file.relativePath, filename: specRefBaseName(file.relativePath) }));
12339
12421
  }
12340
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = []) {
12422
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = [], apiTabSlots = []) {
12341
12423
  if (!mintlifyConfig) return [];
12424
+ const slotByContainer = new Map(apiTabSlots.map(({ container, slot }) => [container, slot]));
12425
+ const slotByLabel = new Map(apiTabSlots.map(({ slot }) => [slot.tab.tab, slot]));
12342
12426
  const references = [
12343
12427
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
12344
- ...mintlifyNavigationApiReferences(mintlifyConfig)
12428
+ ...mintlifyNavigationApiReferences(mintlifyConfig, (container) => slotByContainer.get(container)?.tab.tab)
12345
12429
  ];
12430
+ const endpointPagesOf = (reference) => {
12431
+ const directory2 = reference.groups ? endpointDirectory(reference.directory) : void 0;
12432
+ const slot = reference.tabLabel ? slotByLabel.get(reference.tabLabel) : void 0;
12433
+ return directory2 === void 0 ? void 0 : { directory: directory2, ...slot ? { slot } : {} };
12434
+ };
12346
12435
  const known = new Set(references.map((reference) => canonicalSpecRef(reference.value)));
12347
12436
  const specFiles = specFileCandidates(files);
12348
12437
  const namedFiles = /* @__PURE__ */ new Map();
@@ -12383,6 +12472,14 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12383
12472
  if (reference.groups) {
12384
12473
  const merged = mergeApiGroups(shared.target.groups, reference.groups);
12385
12474
  if (merged) shared.target.groups = merged;
12475
+ const own2 = endpointPagesOf(reference);
12476
+ if (shared.directory === void 0) {
12477
+ shared.directory = own2?.directory ?? null;
12478
+ if (own2) shared.target.endpointPages = own2;
12479
+ } else if (shared.directory !== (own2?.directory ?? null)) {
12480
+ shared.directory = null;
12481
+ delete shared.target.endpointPages;
12482
+ }
12386
12483
  } else if (listsNothing(reference)) {
12387
12484
  shared.unlisted = true;
12388
12485
  delete shared.target.groups;
@@ -12401,11 +12498,12 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12401
12498
  continue;
12402
12499
  }
12403
12500
  if (/^https:\/\//i.test(reference.value)) {
12404
- const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, ...groups ? { groups } : {} };
12501
+ const pagesTarget = endpointPagesOf(reference);
12502
+ const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, ...groups ? { groups } : {}, ...groups && pagesTarget ? { endpointPages: pagesTarget } : {} };
12405
12503
  mountDirectory(remote, reference);
12406
12504
  remoteSpecs.push(remote);
12407
12505
  remoteByKey.set(dedupeKey, remote);
12408
- groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
12506
+ groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true, ...groups ? { directory: pagesTarget?.directory ?? null } : {} });
12409
12507
  warnings.push({
12410
12508
  code: "unsupported-config",
12411
12509
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -12426,12 +12524,12 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12426
12524
  });
12427
12525
  continue;
12428
12526
  }
12429
- if (reference.directory) {
12430
- warnings.push({
12431
- code: "unsupported-config",
12432
- message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Links in page bodies and Mintlify endpoint URLs under "${reference.directory}/<tag>/<operation>" are redirected to the matching endpoints where one exists (a separate warning counts any endpoint redirects that were skipped); check any other links to "${reference.directory}/..." manually.`
12433
- });
12434
- }
12527
+ const directoryWarning = reference.directory ? {
12528
+ code: "unsupported-config",
12529
+ message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Links in page bodies and Mintlify endpoint URLs under "${reference.directory}/<tag>/<operation>" are redirected to the matching endpoints where one exists (a separate warning counts any endpoint redirects that were skipped); check any other links to "${reference.directory}/..." manually.`
12530
+ } : void 0;
12531
+ const deferWarning = Boolean(groups && endpointPagesOf(reference));
12532
+ if (directoryWarning && !deferWarning) warnings.push(directoryWarning);
12435
12533
  const content = readSpecFile(match.absolutePath, match.relativePath, warnings);
12436
12534
  if (!content) continue;
12437
12535
  const spec = {
@@ -12444,10 +12542,11 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12444
12542
  ...reference.pageOnly ? { pageOnly: true } : {},
12445
12543
  sourcePath: match.relativePath,
12446
12544
  directory: reference.directory,
12447
- ...groups ? { groups } : {}
12545
+ ...groups ? { groups } : {},
12546
+ ...deferWarning ? { endpointPages: endpointPagesOf(reference), directoryWarning } : {}
12448
12547
  };
12449
12548
  specs.push(spec);
12450
- groupTargets.set(dedupeKey, { target: spec, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
12549
+ groupTargets.set(dedupeKey, { target: spec, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true, ...groups ? { directory: endpointPagesOf(reference)?.directory ?? null } : {} });
12451
12550
  }
12452
12551
  for (const { target: target2, unlisted, withheld } of groupTargets.values()) {
12453
12552
  if (withheld && !unlisted && !target2.groups) target2.navigation = false;
@@ -12503,20 +12602,52 @@ function apiTabSpecIds(tabs) {
12503
12602
  }
12504
12603
  var flagged = (value) => value === true || value === "true";
12505
12604
  var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
12605
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12606
+ function resolveLocalSpecRef(document, ref) {
12607
+ if (ref !== "#" && !ref.startsWith("#/")) return void 0;
12608
+ let node = document;
12609
+ for (const segment of ref.slice(2).split("/").filter(Boolean)) {
12610
+ if (!isRecord2(node) && !Array.isArray(node)) return void 0;
12611
+ let decoded = segment;
12612
+ try {
12613
+ decoded = decodeURIComponent(segment);
12614
+ } catch {
12615
+ }
12616
+ node = node[decoded.replace(/~1/g, "/").replace(/~0/g, "~")];
12617
+ }
12618
+ return node;
12619
+ }
12620
+ function viewSpecPathItem(document, entry) {
12621
+ const chain = [entry];
12622
+ let current = entry;
12623
+ while (typeof current.$ref === "string") {
12624
+ const target2 = resolveLocalSpecRef(document, current.$ref);
12625
+ if (!isRecord2(target2) || chain.includes(target2)) break;
12626
+ chain.push(target2);
12627
+ current = target2;
12628
+ }
12629
+ const item = Object.assign({}, ...[...chain].reverse());
12630
+ delete item.$ref;
12631
+ return { item, withheld: chain.some(isWithheld) };
12632
+ }
12506
12633
  function parseOpenApiOperationsForLinking(content) {
12507
- let document;
12634
+ const document = readSpecDocument(content);
12635
+ return visibleOperations(document, isRecord2(document) ? document.paths : void 0);
12636
+ }
12637
+ function readSpecDocument(content) {
12508
12638
  try {
12509
- document = parseSpecDocument(content.toString("utf8")).document;
12639
+ return parseSpecDocument(content.toString("utf8")).document;
12510
12640
  } catch {
12511
- return [];
12641
+ return void 0;
12512
12642
  }
12513
- const paths = document && typeof document === "object" ? document.paths : null;
12514
- if (!paths || typeof paths !== "object") return [];
12643
+ }
12644
+ function visibleOperations(document, entries) {
12645
+ if (!isRecord2(entries)) return [];
12515
12646
  const operations = [];
12516
- for (const [path, pathItem] of Object.entries(paths)) {
12517
- if (!pathItem || typeof pathItem !== "object") continue;
12518
- const item = pathItem;
12519
- if (isWithheld(item)) continue;
12647
+ for (const [path, pathItem] of Object.entries(entries)) {
12648
+ if (!isRecord2(pathItem)) continue;
12649
+ const { item, withheld } = viewSpecPathItem(document, pathItem);
12650
+ if (withheld) continue;
12520
12651
  for (const method of OPENAPI_HTTP_METHODS) {
12521
12652
  const operation = item[method];
12522
12653
  if (!operation || typeof operation !== "object") continue;
@@ -12528,7 +12659,8 @@ function parseOpenApiOperationsForLinking(content) {
12528
12659
  path,
12529
12660
  tag: tags[0],
12530
12661
  summary: typeof op.summary === "string" ? op.summary : void 0,
12531
- operationId: typeof op.operationId === "string" ? op.operationId : void 0
12662
+ operationId: typeof op.operationId === "string" ? op.operationId : void 0,
12663
+ description: typeof op.description === "string" ? op.description : void 0
12532
12664
  });
12533
12665
  }
12534
12666
  }
@@ -12549,13 +12681,26 @@ function servedOperations(source, parsed) {
12549
12681
  });
12550
12682
  }
12551
12683
  var mountPrefix = (directory2) => directory2.replace(/^\/+|\/+$/g, "").toLowerCase();
12684
+ function mintlifyEndpointUrl(directory2, operation) {
12685
+ const tag = mintlifyUrlSlug(operation.tag ?? "default");
12686
+ const leaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
12687
+ return {
12688
+ tag,
12689
+ leaf,
12690
+ url: `${directory2}/${tag}/${leaf}`.replace(/^\/+|\/+$/g, ""),
12691
+ title: operation.summary?.trim() || operation.operationId?.trim() || `${operation.method.toUpperCase()} ${operation.path}`
12692
+ };
12693
+ }
12552
12694
  function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12553
12695
  const map = /* @__PURE__ */ new Map();
12554
12696
  const prefixLandings = /* @__PURE__ */ new Map();
12555
12697
  const mintlifyPages = /* @__PURE__ */ new Map();
12556
12698
  const duplicatePages = [];
12557
12699
  const parsedByFile = /* @__PURE__ */ new Map();
12558
- const bound = apiTabSpecIds(docsConfig.tabs).flatMap(({ tab, specId }) => sources.filter((entry) => tab.api?.source === specAssetPath(entry.filename) && (!entry.tab || entry.tab === tab.tab)).map((source) => ({ source, specId })));
12700
+ const bound = [
12701
+ ...apiTabSpecIds(docsConfig.tabs).flatMap(({ tab, specId }) => sources.filter((entry) => !entry.endpointPages && tab.api?.source === specAssetPath(entry.filename) && (!entry.tab || entry.tab === tab.tab)).map((source) => ({ source, specId }))),
12702
+ ...sources.filter((entry) => entry.endpointPages).map((source) => ({ source }))
12703
+ ];
12559
12704
  const mounts = new Set([...bound.map(({ source }) => source.prefix ? mountPrefix(source.prefix) : ""), ...otherMounts.map(mountPrefix)].filter(Boolean));
12560
12705
  const underDeeperMount = (route, own2) => {
12561
12706
  const lower = route.toLowerCase();
@@ -12565,9 +12710,12 @@ function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12565
12710
  if (!source.prefix) return;
12566
12711
  const base = source.prefix.replace(/^\/+|\/+$/g, "");
12567
12712
  const prefix = mountPrefix(source.prefix);
12568
- if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
12713
+ if (prefix && specId !== void 0) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
12569
12714
  for (const operation of servedOperations(source, parsedByFile)) {
12570
- const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
12715
+ const endpoint = mintlifyEndpointUrl(base, operation);
12716
+ if (specId === void 0 && (!endpoint.tag || !endpoint.leaf)) continue;
12717
+ const thallyHref = specId === void 0 ? `/${endpoint.url}` : `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
12718
+ if (specId === void 0 && prefix && !prefixLandings.has(prefix)) prefixLandings.set(prefix, thallyHref);
12571
12719
  const leafCandidates = /* @__PURE__ */ new Set();
12572
12720
  if (operation.operationId) {
12573
12721
  const parts = operation.operationId.split(/[_.]/).filter(Boolean);
@@ -12580,17 +12728,17 @@ function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12580
12728
  if (operation.operationId) leafCandidates.add(mintlifyOperationSlugSegment(operation.operationId));
12581
12729
  leafCandidates.add(mintlifyOperationSlugSegment(`${operation.method} ${operation.path}`));
12582
12730
  const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
12583
- const mintlifyTag = mintlifyUrlSlug(operation.tag ?? "default");
12584
- const mintlifyLeaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
12585
- const canonical2 = `${base}/${mintlifyTag}/${mintlifyLeaf}`.replace(/^\/+|\/+$/g, "");
12731
+ const { tag: mintlifyTag, leaf: mintlifyLeaf, url: canonical2 } = endpoint;
12586
12732
  if (mintlifyTag && mintlifyLeaf && !underDeeperMount(canonical2, prefix)) {
12587
- if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
12588
- else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
12733
+ if (specId !== void 0) {
12734
+ if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
12735
+ else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
12736
+ }
12589
12737
  if (!map.has(canonical2.toLowerCase())) map.set(canonical2.toLowerCase(), thallyHref);
12590
12738
  }
12591
12739
  const tagRoot = `${base}/${mintlifyTag}`.replace(/^\/+|\/+$/g, "");
12592
12740
  if (mintlifyTag && !underDeeperMount(tagRoot, prefix)) {
12593
- if (!mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
12741
+ if (specId !== void 0 && !mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
12594
12742
  if (!map.has(tagRoot.toLowerCase())) map.set(tagRoot.toLowerCase(), thallyHref);
12595
12743
  }
12596
12744
  for (const leaf of leafCandidates) {
@@ -12631,6 +12779,150 @@ function linkApiMounts(sources, docsConfig, pages, warnings, reservedMounts = []
12631
12779
  rewriteApiLinksInPages(pages, maps.operationLinks, maps.prefixLandings, warnings, { mounts: reservedMounts, redirects: docsConfig.redirects });
12632
12780
  return mintlifyEndpointRedirects(maps, pages, docsConfig.redirects ?? [], warnings);
12633
12781
  }
12782
+ var DEFAULT_ENDPOINT_DIRECTORY = "api-reference";
12783
+ function endpointDirectory(directory2) {
12784
+ const value = (directory2 ?? DEFAULT_ENDPOINT_DIRECTORY).replace(/^\/+|\/+$/g, "");
12785
+ const plain = (segment) => /^[\p{L}\p{N}_&-][\p{L}\p{N}_&.-]*$/u.test(segment) && Buffer.byteLength(`${segment}.mdx`) <= 255;
12786
+ return value && value.split("/").every(plain) ? value : void 0;
12787
+ }
12788
+ function createEndpointPageLedger(pages) {
12789
+ return { owners: new Map(pages.map((page) => [page.id.toLowerCase(), { id: page.id, operation: null }])), duplicates: [], unnamed: [] };
12790
+ }
12791
+ function reportEndpointPages(ledger, warnings) {
12792
+ const names = (urls) => `${urls.slice(0, 10).join(", ")}${urls.length > 10 ? ", ..." : ""}`;
12793
+ if (ledger.duplicates.length > 0) {
12794
+ warnings.push({
12795
+ code: "unsupported-config",
12796
+ message: `${ledger.duplicates.length} Mintlify endpoint URL${ledger.duplicates.length === 1 ? " is" : "s are"} shared by more than one operation (same tag and summary), so only the first operation has a page at each: ${names(ledger.duplicates.map((url) => `/${url}`))}.`
12797
+ });
12798
+ }
12799
+ if (ledger.unnamed.length > 0) {
12800
+ warnings.push({
12801
+ code: "unsupported-config",
12802
+ message: `${ledger.unnamed.length} listed operation${ledger.unnamed.length === 1 ? " has" : "s have"} no page because ${ledger.unnamed.length === 1 ? "its" : "their"} tag or title leaves no usable page name: ${names(ledger.unnamed)}.`
12803
+ });
12804
+ }
12805
+ }
12806
+ function endpointDescription(description, title) {
12807
+ const text3 = (description ?? "").split(/\r?\n\s*\r?\n/, 1)[0].replace(/<[^>]*>/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*~]|__/g, "").replace(/^\s*#+\s*/, "").replace(/\s+/g, " ").trim();
12808
+ const sentence = /^(.+?[.!?])(?=\s|$)/.exec(text3)?.[1] ?? text3;
12809
+ const limited = sentence.length > 300 ? `${sentence.slice(0, 297).replace(/\s+\S*$/, "")}...` : sentence;
12810
+ return limited || title;
12811
+ }
12812
+ function insertEndpointTab(tabs, tab, preceding) {
12813
+ if (preceding.length === 0) {
12814
+ tabs.unshift(tab);
12815
+ return;
12816
+ }
12817
+ for (const label of preceding) {
12818
+ const index = tabs.findIndex((candidate) => candidate.tab === label);
12819
+ if (index !== -1) {
12820
+ tabs.splice(index + 1, 0, tab);
12821
+ return;
12822
+ }
12823
+ }
12824
+ tabs.push(tab);
12825
+ }
12826
+ function addEndpointPages(options) {
12827
+ const { tabs, pages, ledger, spec } = options;
12828
+ const operations = parseOpenApiOperationsForLinking(spec.content);
12829
+ if (operations.length === 0) return null;
12830
+ const existing = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => !tab.hidden && tab.tab.toLowerCase().includes("api"));
12831
+ if (existing?.api) return null;
12832
+ const byKey = new Map(operations.map((operation) => [`${operation.method} ${operation.path}`, operation]));
12833
+ const listed = [];
12834
+ const pageFor = (reference) => {
12835
+ const [method, ...path] = reference.trim().split(/\s+/);
12836
+ const operation = byKey.get(`${method.toLowerCase()} ${path.join(" ")}`);
12837
+ if (!operation) return void 0;
12838
+ const key = `${operation.method.toUpperCase()} ${operation.path}`;
12839
+ const endpoint = mintlifyEndpointUrl(spec.target.directory, operation);
12840
+ if (!endpoint.tag || !endpoint.leaf || Buffer.byteLength(`${endpoint.tag}.mdx`) > 255 || Buffer.byteLength(`${endpoint.leaf}.mdx`) > 255) {
12841
+ if (!ledger.unnamed.includes(key)) ledger.unnamed.push(key);
12842
+ return void 0;
12843
+ }
12844
+ const identity = `${spec.assetPath} ${key}`;
12845
+ const owner = ledger.owners.get(endpoint.url.toLowerCase());
12846
+ if (owner && owner.operation !== null && owner.operation !== identity) {
12847
+ if (!ledger.duplicates.includes(endpoint.url)) ledger.duplicates.push(endpoint.url);
12848
+ return void 0;
12849
+ }
12850
+ if (!owner) {
12851
+ pages.push({
12852
+ id: endpoint.url,
12853
+ navigationId: endpoint.url,
12854
+ title: endpoint.title,
12855
+ description: endpointDescription(operation.description, endpoint.title),
12856
+ // The summary belongs to metadata; keep full spec prose in the safe
12857
+ // operation Markdown renderer rather than copying it into executable MDX.
12858
+ descriptionPlacement: "operation",
12859
+ keywords: [],
12860
+ body: "",
12861
+ source: "docs.json",
12862
+ openapi: `${spec.assetPath} ${key}`
12863
+ });
12864
+ ledger.owners.set(endpoint.url.toLowerCase(), { id: endpoint.url, operation: identity });
12865
+ }
12866
+ listed.push(key);
12867
+ return owner?.id ?? endpoint.url;
12868
+ };
12869
+ const convert2 = (nodes) => nodes.flatMap((node) => {
12870
+ if (typeof node === "string") {
12871
+ const id = pageFor(node);
12872
+ return id ? [id] : [];
12873
+ }
12874
+ const pagesOf = convert2(node.pages);
12875
+ return pagesOf.length > 0 ? [{ group: node.group, ...node.icon ? { icon: node.icon } : {}, pages: pagesOf }] : [];
12876
+ });
12877
+ const groups = convert2(spec.groups);
12878
+ if (groups.length === 0) return null;
12879
+ if (existing) {
12880
+ if (existing.groups || !existing.pages) existing.groups = [...existing.groups ?? [], ...groups];
12881
+ else existing.pages = [...existing.pages, ...groups];
12882
+ } else {
12883
+ const { slot } = spec.target;
12884
+ const tab = {
12885
+ ...slot?.tab ?? { tab: spec.tabLabel ?? "API Reference", ...spec.icon ? { icon: spec.icon } : {}, ...spec.hidden ? { hidden: true } : {} },
12886
+ groups
12887
+ };
12888
+ if (slot) insertEndpointTab(tabs, tab, slot.preceding);
12889
+ else insertApiTab(tabs, tab, spec.parentTab);
12890
+ }
12891
+ if (!tabs.some((tab) => tab.api?.source === spec.assetPath)) {
12892
+ insertApiTab(tabs, { tab: `OpenAPI: ${spec.source}`, hidden: true, api: { source: spec.assetPath } });
12893
+ }
12894
+ return { operations: listed };
12895
+ }
12896
+ function specLookupOrder(tabs, versions = []) {
12897
+ const collected = (tab) => !tab.hidden || Boolean(tab.version && versions.some((version) => version.label === tab.version));
12898
+ const sources = [...tabs.filter(collected), ...tabs.filter((tab) => !collected(tab))].flatMap((tab) => tab.api ? [tab.api.source] : []);
12899
+ return [...new Set(sources)];
12900
+ }
12901
+ function publishedOperationKeys(content) {
12902
+ const document = readSpecDocument(content);
12903
+ const table = (field) => visibleOperations(document, isRecord2(document) ? document[field] : void 0);
12904
+ return /* @__PURE__ */ new Set([
12905
+ ...table("paths").map((operation) => `${operation.method} ${operation.path}`),
12906
+ ...table("webhooks").map((operation) => `webhook ${operation.path}`)
12907
+ ]);
12908
+ }
12909
+ function qualifyDisplacedBareRefs(options) {
12910
+ const { pages, before, after, content, qualifier } = options;
12911
+ if (before.join("\n") === after.join("\n")) return;
12912
+ const published = /* @__PURE__ */ new Map();
12913
+ const firstPublishing = (order, key) => order.find((source) => {
12914
+ if (!published.has(source)) published.set(source, publishedOperationKeys(content(source) ?? Buffer.alloc(0)));
12915
+ return published.get(source).has(key);
12916
+ });
12917
+ for (const page of pages) {
12918
+ const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
12919
+ if (!ref || ref.specRef) continue;
12920
+ const [method, ...rest] = ref.operation.trim().split(/\s+/);
12921
+ const key = `${method.toLowerCase()} ${rest.join(" ")}`;
12922
+ const reached = firstPublishing(before, key);
12923
+ if (reached !== void 0 && reached !== firstPublishing(after, key)) page.openapi = withSpecRef(ref, qualifier(reached));
12924
+ }
12925
+ }
12634
12926
  function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, answered = {}) {
12635
12927
  if (operationLinks.size === 0 && prefixLandings.size === 0) return;
12636
12928
  const unmatched = /* @__PURE__ */ new Set();
@@ -13878,6 +14170,7 @@ function migrateRepository(options) {
13878
14170
  const referenceOrder = /* @__PURE__ */ new Map();
13879
14171
  let docusaurusSidebars = null;
13880
14172
  let mintlifyConfig = null;
14173
+ let mintlifyApiTabSlots = [];
13881
14174
  const mintlifyNavigationAccess = /* @__PURE__ */ new Map();
13882
14175
  let fernRawConfig = null;
13883
14176
  let fernBasePath = "";
@@ -13922,6 +14215,7 @@ function migrateRepository(options) {
13922
14215
  }
13923
14216
  });
13924
14217
  docsConfig = projected.docsConfig;
14218
+ mintlifyApiTabSlots = projected.apiTabSlots;
13925
14219
  warnings.push(...projected.warnings);
13926
14220
  for (const repo of projected.gatedSourceRefs) gatedSourceRepos.add(repo.toLowerCase());
13927
14221
  for (const appearance of projected.referenceAccess) {
@@ -15029,7 +15323,8 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15029
15323
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
15030
15324
  };
15031
15325
  const nameSpec = createSpecAssetNamer();
15032
- const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)));
15326
+ const generatedEndpointPages = /* @__PURE__ */ new Set();
15327
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)), mintlifyApiTabSlots);
15033
15328
  const navigation = navigationSpecContext(mintlifyConfig, (path) => resolvePageIdentity(path)?.id ?? null);
15034
15329
  const defaultSources = /* @__PURE__ */ new Set([
15035
15330
  ...allSpecs.filter((spec) => !spec.pageOnly && spec.tabLabel === void 0).map((spec) => spec.sourcePath),
@@ -15072,12 +15367,61 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15072
15367
  spec.pageReferenced = namedByPages.has(spec);
15073
15368
  spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true));
15074
15369
  }
15075
- docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
15370
+ const listingSpecs = resolvedSpecs.filter((spec) => spec.groups && spec.endpointPages);
15371
+ const unbound = docsConfig;
15372
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs.filter((spec) => !listingSpecs.includes(spec)), warnings);
15373
+ const ledger = createEndpointPageLedger(pages);
15374
+ const tabs = docsConfig.tabs.map((tab) => ({ ...tab }));
15375
+ const pagesBefore = pages.length;
15376
+ const endpointSpecs = /* @__PURE__ */ new Set();
15377
+ for (const spec of listingSpecs) {
15378
+ if (!spec.groups || !spec.endpointPages) continue;
15379
+ const added = addEndpointPages({
15380
+ tabs,
15381
+ pages,
15382
+ ledger,
15383
+ spec: {
15384
+ assetPath: specAssetPath(spec.filename),
15385
+ source: spec.sourcePath,
15386
+ content: spec.content,
15387
+ groups: spec.groups,
15388
+ target: spec.endpointPages,
15389
+ tabLabel: spec.tabLabel,
15390
+ parentTab: spec.parentTab,
15391
+ icon: spec.icon,
15392
+ hidden: spec.hidden
15393
+ }
15394
+ });
15395
+ if (!added) continue;
15396
+ spec.endpointOperations = added.operations;
15397
+ endpointSpecs.add(spec);
15398
+ }
15399
+ reportEndpointPages(ledger, warnings);
15400
+ for (const spec of resolvedSpecs) if (spec.directoryWarning && !endpointSpecs.has(spec)) warnings.push(spec.directoryWarning);
15401
+ for (const page of pages.slice(pagesBefore)) generatedEndpointPages.add(page);
15402
+ docsConfig = injectOpenApiSpecs({ ...docsConfig, tabs }, listingSpecs.filter((spec) => !endpointSpecs.has(spec)), warnings);
15403
+ if (remoteApiSpecs.length === 0 && endpointSpecs.size > 0) {
15404
+ const bySource = new Map(resolvedSpecs.map((spec) => [specAssetPath(spec.filename), spec]));
15405
+ qualifyDisplacedBareRefs({
15406
+ pages,
15407
+ before: specLookupOrder(injectOpenApiSpecs(unbound, resolvedSpecs).tabs, docsConfig.navigation?.versions),
15408
+ after: specLookupOrder(docsConfig.tabs, docsConfig.navigation?.versions),
15409
+ content: (source) => bySource.get(source)?.content,
15410
+ qualifier: (source) => bySource.get(source)?.sourcePath ?? source
15411
+ });
15412
+ }
15413
+ const mountOf = (spec) => endpointSpecs.has(spec) ? { prefix: spec.endpointPages?.directory, operations: spec.endpointOperations } : { prefix: spec.directory };
15076
15414
  if (remoteApiSpecs.length > 0) {
15077
- localApiMounts.push(...resolvedSpecs.flatMap((spec) => spec.directory ? [{ filename: spec.filename, directory: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} }] : []));
15415
+ localApiMounts.push(...resolvedSpecs.flatMap((spec) => {
15416
+ const { prefix, operations } = mountOf(spec);
15417
+ return prefix ? [{ filename: spec.filename, directory: prefix, ...spec.tabLabel ? { tab: spec.tabLabel } : {}, ...operations ? { endpointOperations: operations } : {} }] : [];
15418
+ }));
15078
15419
  } else {
15079
15420
  const endpointRedirects = linkApiMounts(
15080
- resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} })),
15421
+ resolvedSpecs.map((spec) => {
15422
+ const { prefix, operations } = mountOf(spec);
15423
+ return { filename: spec.filename, content: spec.content, prefix, ...operations ? { endpointPages: true, operations } : {}, ...spec.tabLabel ? { tab: spec.tabLabel } : {} };
15424
+ }),
15081
15425
  docsConfig,
15082
15426
  pages,
15083
15427
  warnings
@@ -15134,7 +15478,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15134
15478
  }
15135
15479
  return sameName ? "the file could not be read as an OpenAPI document" : `no file exists at "${key}"`;
15136
15480
  };
15137
- rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
15481
+ rewriteMintlifyPageSpecRefs(pages.filter((page) => !generatedEndpointPages.has(page)), pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
15138
15482
  if (docsConfig.tabs.some((tab) => tab.api)) docsConfig = { ...docsConfig, api: { ...docsConfig.api, specLink: false } };
15139
15483
  }
15140
15484
  if (platform === "mintlify") {
@@ -15356,6 +15700,24 @@ ${body}`;
15356
15700
  if (docsConfig.redirects) {
15357
15701
  docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), [...pages, ...sourceRefPages], warnings);
15358
15702
  }
15703
+ const redirectSources = new Map((docsConfig.redirects ?? []).map((redirect) => [(redirect.source.replace(/\/+$/, "") || "/").toLowerCase(), redirect.destination]));
15704
+ const linkRedirects = [];
15705
+ for (const page of [...pages, ...sourceRefPages]) {
15706
+ if (!page.externalUrl) continue;
15707
+ for (const source of pageEntryRoutes(page)) {
15708
+ const existing = redirectSources.get(source.toLowerCase());
15709
+ if (existing !== void 0) {
15710
+ warnings.push({
15711
+ code: "unsupported-config",
15712
+ message: `Page "${page.id}" links to ${stripControlCharacters(page.externalUrl)}, but "${source}" already redirects to ${stripControlCharacters(existing)}; that redirect was kept, so "${source}" does not open the link.`
15713
+ });
15714
+ continue;
15715
+ }
15716
+ redirectSources.set(source.toLowerCase(), page.externalUrl);
15717
+ linkRedirects.push({ source, destination: page.externalUrl, permanent: false });
15718
+ }
15719
+ }
15720
+ if (linkRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...linkRedirects] };
15359
15721
  if (pages.length === 0) {
15360
15722
  const reasons = warnings.map((warning) => warning.message);
15361
15723
  throw new Error(reasons.length ? `No importable Markdown or MDX pages were found in the repository. Warnings encountered during migration:
@@ -15439,7 +15801,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
15439
15801
  });
15440
15802
  }
15441
15803
  if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
15442
- docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
15804
+ docsConfig = { ...docsConfig, navigation: { display: "dropdown", ...docsConfig.navigation?.groupHeadings ? { groupHeadings: docsConfig.navigation.groupHeadings } : {} } };
15443
15805
  }
15444
15806
  const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) ?? (fernRawConfig["global-theme"] === "nvidia" ? { primary: "#76B900" } : void 0) : isTopLevelDocusaurus ? presentation?.colors ?? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
15445
15807
  const assetPaths = new Set(assets.map((asset) => asset.path));
@@ -15635,7 +15997,7 @@ function renderPage(bundle, page) {
15635
15997
  page.icon ? `icon: ${yamlString(page.icon)}` : null,
15636
15998
  page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
15637
15999
  `description: ${yamlString(page.description)}`,
15638
- page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
16000
+ page.descriptionPlacement === "body" || page.descriptionPlacement === "operation" ? `descriptionPlacement: ${page.descriptionPlacement}` : null,
15639
16001
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
15640
16002
  page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
15641
16003
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
@@ -15760,6 +16122,7 @@ function renderMigrationFiles(bundle, options = {}) {
15760
16122
  // src/url-run.ts
15761
16123
  import { createHash as createHash6 } from "crypto";
15762
16124
  import { load as load8 } from "cheerio";
16125
+ import { parse as parse2, postprocess, preprocess } from "micromark";
15763
16126
  import remarkParse5 from "remark-parse";
15764
16127
  import remarkMdx5 from "remark-mdx";
15765
16128
  import { unified as unified5 } from "unified";
@@ -16838,9 +17201,9 @@ function apiOperationCodeControls(body, specification, operation) {
16838
17201
  }
16839
17202
 
16840
17203
  // src/url-run.ts
16841
- var CONVERTER_VERSION = "url-evidence-v6";
17204
+ var CONVERTER_VERSION = "url-evidence-v7";
16842
17205
  var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
16843
- var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
17206
+ var ACCEPT = "text/html,application/xhtml+xml";
16844
17207
  var MAX_RESPONSE_BYTES = 8e6;
16845
17208
  var MAX_ASSET_BYTES2 = 1e7;
16846
17209
  var MAX_RECORDS = 1e5;
@@ -16870,6 +17233,11 @@ function canonical(value, base) {
16870
17233
  return null;
16871
17234
  }
16872
17235
  }
17236
+ function sameRouteMarkdownRepresentation(alternate, primary) {
17237
+ const target2 = new URL(alternate), source = new URL(primary);
17238
+ const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17239
+ return target2.origin === source.origin && target2.search === source.search && directory2(target2.pathname) === directory2(source.pathname) && /\.mdx?$/i.test(target2.pathname);
17240
+ }
16873
17241
  function classifyMigrationResponse(document) {
16874
17242
  if (document.status && document.status >= 400) return "unsupported";
16875
17243
  if (/\/(?:llms(?:-full)?|robots)\.txt$|\/sitemap[^/]*\.xml$|\/search(?:\.html)?$/i.test(document.finalUrl.pathname) || /\.(?:json|ya?ml)$/i.test(document.finalUrl.pathname) || /(?:xml|json)/i.test(document.contentType)) return "index";
@@ -17120,7 +17488,7 @@ function resolveMigrationRecord(records, record) {
17120
17488
  }
17121
17489
  return record;
17122
17490
  }
17123
- function navigationConfig(checkpoint, records) {
17491
+ function navigationConfig(checkpoint, records, occurrencePages = /* @__PURE__ */ new Map()) {
17124
17492
  const navigation = checkpoint.navigation;
17125
17493
  const shortcuts = [];
17126
17494
  const collectShortcuts = (nodes) => {
@@ -17145,7 +17513,7 @@ function navigationConfig(checkpoint, records) {
17145
17513
  const project2 = (nodes) => nodes.flatMap((node) => {
17146
17514
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
17147
17515
  const target2 = resolveMigrationRecord(records, record);
17148
- const page = target2?.page;
17516
+ const page = occurrencePages.get(node) ?? target2?.page;
17149
17517
  const children = project2(node.children ?? []);
17150
17518
  if (children.length) {
17151
17519
  const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === (page && projection.navigationId(page)) : contains(entry.pages));
@@ -17445,8 +17813,19 @@ async function runUrlMigration(options) {
17445
17813
  if (!record.evidence) continue;
17446
17814
  const final = canonical(record.evidence.finalUrl, sourceUrl);
17447
17815
  if (!final) continue;
17448
- if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
17449
- else if (record.outcome === "excluded") {
17816
+ if (record.outcome === "imported") {
17817
+ enqueueArticleLinks(record.evidence.links, final, record.url);
17818
+ for (const reference of record.evidence.markdownRepresentations ?? []) {
17819
+ const alternate = canonical(reference, final);
17820
+ if (!alternate || !sameRouteMarkdownRepresentation(alternate, final)) continue;
17821
+ enqueue(alternate, final, "declared-markdown-representation");
17822
+ const sibling = records.get(alternate);
17823
+ if (sibling && sibling.representationOf !== record.url) {
17824
+ sibling.representationOf = record.url;
17825
+ changed.push(sibling);
17826
+ }
17827
+ }
17828
+ } else if (record.outcome === "excluded") {
17450
17829
  if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
17451
17830
  for (const link of record.evidence.links) enqueue(link, final, record.url);
17452
17831
  }
@@ -17529,6 +17908,7 @@ async function runUrlMigration(options) {
17529
17908
  let claimedId;
17530
17909
  let claimedFinal;
17531
17910
  let markdownRepresentation;
17911
+ const markdownRepresentations = [];
17532
17912
  let exactKey;
17533
17913
  try {
17534
17914
  delete record.reason;
@@ -17621,13 +18001,13 @@ async function runUrlMigration(options) {
17621
18001
  $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
17622
18002
  const alternate = canonical($(node).attr("href"), final);
17623
18003
  if (!alternate) return;
17624
- const target2 = new URL(alternate), primary = new URL(final);
17625
- const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17626
- if (target2.origin !== primary.origin || target2.search !== primary.search || directory2(target2.pathname) !== directory2(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
18004
+ if (!sameRouteMarkdownRepresentation(alternate, final)) return;
17627
18005
  enqueue(alternate, final, "declared-markdown-representation");
17628
18006
  const sibling = records.get(alternate);
18007
+ if (!sibling) return;
17629
18008
  sibling.representationOf = record.url;
17630
18009
  markdownRepresentation ??= alternate;
18010
+ markdownRepresentations.push(alternate);
17631
18011
  changed.push(sibling);
17632
18012
  });
17633
18013
  }
@@ -17640,7 +18020,8 @@ async function runUrlMigration(options) {
17640
18020
  }
17641
18021
  enqueueNavigation(observedNavigation, final);
17642
18022
  }
17643
- if (imported >= maxPages) {
18023
+ const replacesRepresentation = imported >= maxPages && markdownRepresentations.some((url) => records.get(url)?.page);
18024
+ if (imported >= maxPages && !replacesRepresentation) {
17644
18025
  record.outcome = "excluded";
17645
18026
  record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
17646
18027
  return;
@@ -17659,14 +18040,17 @@ async function runUrlMigration(options) {
17659
18040
  const locale = language.locale;
17660
18041
  const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
17661
18042
  const identityId = locale ? `${locale}/${navigationId}` : id;
17662
- if (usedIds.has(identityId) && usedIds.get(identityId) !== record.url) {
18043
+ const owner = records.get(usedIds.get(identityId) ?? "");
18044
+ const sharesDeclaredRepresentation = owner && (owner.representationOf === record.url || record.representationOf === owner.url);
18045
+ if (owner && owner !== record && !sharesDeclaredRepresentation) {
17663
18046
  record.outcome = "failed";
17664
18047
  record.reason = `Destination collision for ${identityId}.`;
17665
18048
  return;
17666
18049
  }
17667
- usedIds.set(identityId, record.url);
18050
+ if (!owner || owner === record || owner.representationOf === record.url) usedIds.set(identityId, record.url);
17668
18051
  claimedId = identityId;
17669
18052
  const evidence = evidenceOf(document);
18053
+ if (markdownRepresentations.length) evidence.markdownRepresentations = markdownRepresentations;
17670
18054
  const page = convert(document, id);
17671
18055
  if (!page) {
17672
18056
  record.outcome = "failed";
@@ -17877,6 +18261,7 @@ ${difference}`;
17877
18261
  if (!primary || primary === record) continue;
17878
18262
  const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
17879
18263
  if (!winner) continue;
18264
+ if (winner.page) usedIds.set(winner.page.id, winner.url);
17880
18265
  const alternate = winner === primary ? record : primary;
17881
18266
  if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
17882
18267
  if (alternate.outcome === "imported") {
@@ -17898,7 +18283,17 @@ ${difference}`;
17898
18283
  settleArticleIdentities();
17899
18284
  while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
17900
18285
  if (imported >= maxPages) {
17901
- const index = pending.findIndex((record, position) => position >= cursor && isDiscoveryIndex(new URL(record.url), source));
18286
+ const index = pending.findIndex((record, position) => {
18287
+ if (position < cursor || record.outcome !== "pending") return false;
18288
+ const url = new URL(record.url);
18289
+ if (isDiscoveryIndex(url, source)) return true;
18290
+ if (/\.mdx?$/i.test(url.pathname)) return false;
18291
+ return [".md", ".mdx", "/index.md", "/index.mdx"].some((extension) => {
18292
+ const alternate = new URL(url);
18293
+ alternate.pathname = alternate.pathname.replace(/\/index$/i, "") + extension;
18294
+ return records.get(alternate.href)?.outcome === "imported";
18295
+ });
18296
+ });
17902
18297
  if (index < 0) break;
17903
18298
  pending.splice(cursor, 0, pending.splice(index, 1)[0]);
17904
18299
  }
@@ -17922,19 +18317,60 @@ ${difference}`;
17922
18317
  }
17923
18318
  const navigationLabels = /* @__PURE__ */ new Map();
17924
18319
  const navigationIssues = [];
18320
+ const navigationOccurrences = /* @__PURE__ */ new Map();
17925
18321
  const labelPages = (nodes) => {
17926
18322
  for (const node of nodes) {
17927
18323
  const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
17928
18324
  const target2 = resolveMigrationRecord(records, entry);
17929
- if (target2?.page && node.label && !node.children?.length && !entry?.aliasAnchor) {
17930
- const previous = navigationLabels.get(target2.url);
17931
- if (previous && previous !== node.label) navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${previous}, ${node.label}); review the shared sidebar title.` });
17932
- else navigationLabels.set(target2.url, node.label);
18325
+ if (entry && target2?.page && node.label && !node.children?.length && !entry.aliasAnchor) {
18326
+ const occurrences = navigationOccurrences.get(target2) ?? [];
18327
+ occurrences.push({ node, entry });
18328
+ navigationOccurrences.set(target2, occurrences);
17933
18329
  }
17934
18330
  labelPages(node.children ?? []);
17935
18331
  }
17936
18332
  };
17937
18333
  labelPages(checkpoint.navigation);
18334
+ const occurrencePages = /* @__PURE__ */ new Map();
18335
+ const navigationAliases = /* @__PURE__ */ new Map();
18336
+ const occupiedPageIds = new Set(checkpoint.records.flatMap((record) => record.page ? [projection.pageId(record.page)] : []));
18337
+ const navigationRedirects = [];
18338
+ for (const [target2, occurrences] of navigationOccurrences) {
18339
+ const final = target2.evidence && canonical(target2.evidence.finalUrl, sourceUrl);
18340
+ const primary = occurrences.find(({ entry }) => entry.url === final) ?? occurrences.find(({ entry }) => entry === target2) ?? occurrences[0];
18341
+ navigationLabels.set(target2.url, primary.node.label);
18342
+ for (const { node, entry } of occurrences) {
18343
+ if (node.label === primary.node.label) continue;
18344
+ const existing = navigationAliases.get(entry.url);
18345
+ if (existing?.page.navTitle === node.label && existing.target === target2) {
18346
+ occurrencePages.set(node, existing.page);
18347
+ continue;
18348
+ }
18349
+ const sourceRoute = new URL(entry.url);
18350
+ const id = inScope(sourceRoute, source, scopePath) ? idForUrl(sourceRoute, scopePath) : null;
18351
+ const locale = projection.canonicalLocale(target2.page);
18352
+ let navigationId = id && (locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id);
18353
+ const legacyPrefix = locale && `${locale.toLowerCase()}-`;
18354
+ if (navigationId && legacyPrefix && projection.navigationId(target2.page) !== target2.page.navigationId && navigationId.toLowerCase().startsWith(legacyPrefix)) navigationId = navigationId.slice(legacyPrefix.length);
18355
+ const page = navigationId ? {
18356
+ ...target2.page,
18357
+ id: locale ? `${locale}/${navigationId}` : navigationId,
18358
+ navigationId,
18359
+ ...locale ? { locale } : {},
18360
+ navTitle: node.label,
18361
+ source: entry.url,
18362
+ noindex: true
18363
+ } : void 0;
18364
+ if (!page || existing || occupiedPageIds.has(projection.pageId(page))) {
18365
+ navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${primary.node.label}, ${node.label}); their source routes cannot retain distinct sidebar titles safely.` });
18366
+ continue;
18367
+ }
18368
+ occupiedPageIds.add(projection.pageId(page));
18369
+ navigationAliases.set(entry.url, { page, target: target2 });
18370
+ occurrencePages.set(node, page);
18371
+ navigationRedirects.push({ source: projection.route(page), destination: projection.route(target2.page), permanent: true });
18372
+ }
18373
+ }
17938
18374
  const pages = checkpoint.records.flatMap((record) => record.page ? [{ ...record.page, id: projection.pageId(record.page), navigationId: projection.navigationId(record.page), ...projection.canonicalLocale(record.page) ? { locale: projection.canonicalLocale(record.page) } : {}, ...navigationLabels.has(record.url) ? { navTitle: navigationLabels.get(record.url) } : {}, body: rewriteMarkdownDestinations(record.page.body, (value) => {
17939
18375
  let url;
17940
18376
  try {
@@ -17955,6 +18391,10 @@ ${difference}`;
17955
18391
  const hasBrokenSourceAnchor = Boolean(url.hash && destination?.outcome === "imported" && destination.evidence && !destination.evidence.anchors.includes(anchor));
17956
18392
  return `${route}${hasBrokenSourceAnchor ? "" : effectiveHash}`;
17957
18393
  }) }] : []);
18394
+ const projectedPages = new Map(pages.map((page) => [page.id, page]));
18395
+ for (const { page, target: target2 } of navigationAliases.values()) {
18396
+ pages.push({ ...page, id: projection.pageId(page), body: projectedPages.get(projection.pageId(target2.page)).body });
18397
+ }
17958
18398
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
17959
18399
  validation.issues.push(...navigationIssues);
17960
18400
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
@@ -17975,12 +18415,12 @@ ${difference}`;
17975
18415
  platform: options.platform ?? "generic",
17976
18416
  pages,
17977
18417
  assets: [...assets.values()],
17978
- docsConfig: navigationConfig(checkpoint, records) ?? buildNavigationFromPages(pages),
18418
+ docsConfig: navigationConfig(checkpoint, records, occurrencePages) ?? buildNavigationFromPages(pages),
17979
18419
  warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
17980
18420
  urlMigrationValidation: validation,
17981
18421
  stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
17982
18422
  };
17983
- if (projection.redirects.length) bundle.docsConfig.redirects = projection.redirects;
18423
+ if (projection.redirects.length || navigationRedirects.length) bundle.docsConfig.redirects = [...projection.redirects, ...navigationRedirects];
17984
18424
  if (projection.locales.length) {
17985
18425
  const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
17986
18426
  bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
@@ -17998,9 +18438,11 @@ function rewriteMarkdownDestinations(body, rewrite) {
17998
18438
  const original = body.slice(start, end);
17999
18439
  const replacement = rewrite(node.url);
18000
18440
  if (replacement === node.url) return null;
18001
- const index = original.lastIndexOf(node.url);
18002
- if (index < 0) return null;
18003
- return { start: start + index, end: start + index + node.url.length, replacement };
18441
+ const destinations = postprocess(parse2().document().write(preprocess()(original, void 0, true))).filter(([event, token]) => event === "enter" && ["resourceDestinationString", "definitionDestinationString", "autolinkProtocol", "autolinkEmail"].includes(token.type));
18442
+ const destination = destinations.at(-1)?.[1];
18443
+ if (!destination) return null;
18444
+ const escaped = replacement.replace(/[\\&()<>]/g, "\\$&").replace(/[\t\n\r ]/g, (character) => encodeURIComponent(character));
18445
+ return { start: start + destination.start.offset, end: start + destination.end.offset, replacement: escaped };
18004
18446
  }).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
18005
18447
  for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
18006
18448
  if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
@@ -18164,6 +18606,12 @@ function slug(value) {
18164
18606
  function operationTarget(specId, operation) {
18165
18607
  return `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
18166
18608
  }
18609
+ function bindApiTab(tabs, reference, api) {
18610
+ const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
18611
+ if (tab) tab.api = api;
18612
+ else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api }, reference.parentTab);
18613
+ return tab?.tab ?? reference.tabLabel ?? "API Reference";
18614
+ }
18167
18615
  async function hydrateRemoteApiSpecs(bundle, fetcher) {
18168
18616
  if (!bundle.remoteApiSpecs?.length) return bundle;
18169
18617
  const assets = [...bundle.assets];
@@ -18172,7 +18620,8 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18172
18620
  const warnings = [...bundle.warnings];
18173
18621
  const pages = bundle.pages.map((page) => ({ ...page }));
18174
18622
  const authoredLinks = /* @__PURE__ */ new Set();
18175
- const served = servedRoutes(pages);
18623
+ const ledger = createEndpointPageLedger(pages);
18624
+ const pagesBefore = pages.length;
18176
18625
  const endpointUrls = /* @__PURE__ */ new Map();
18177
18626
  for (const page of bundle.pages) {
18178
18627
  for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
@@ -18183,6 +18632,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18183
18632
  }
18184
18633
  }
18185
18634
  const bound = [];
18635
+ const apiTabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
18636
+ for (const mount of bundle.localApiMounts ?? []) {
18637
+ if (mount.endpointOperations) bindApiTab(apiTabs, { tabLabel: mount.tab }, { source: `openapi/${mount.filename}` });
18638
+ }
18186
18639
  for (const reference of bundle.remoteApiSpecs) {
18187
18640
  for (let index = warnings.length - 1; index >= 0; index--) {
18188
18641
  if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
@@ -18202,11 +18655,29 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18202
18655
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
18203
18656
  if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
18204
18657
  }
18205
- const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
18658
+ bindApiTab(apiTabs, reference, { source: `openapi/${filename}` });
18659
+ const endpoint = reference.groups && reference.endpointPages ? addEndpointPages({
18660
+ tabs,
18661
+ pages,
18662
+ ledger,
18663
+ spec: {
18664
+ assetPath: `openapi/${filename}`,
18665
+ source: reference.url,
18666
+ content: body,
18667
+ groups: reference.groups,
18668
+ target: reference.endpointPages,
18669
+ tabLabel: reference.tabLabel,
18670
+ parentTab: reference.parentTab,
18671
+ icon: reference.icon,
18672
+ hidden: reference.hidden
18673
+ }
18674
+ }) : null;
18675
+ if (endpoint) {
18676
+ bound.push({ reference, filename, content: body, operations, tabName: reference.tabLabel ?? "API Reference", endpointOperations: endpoint.operations });
18677
+ continue;
18678
+ }
18206
18679
  const api = { source: `openapi/${filename}`, ...reference.navigation === false ? { navigation: false } : {}, ...reference.groups ? { groups: reference.groups } : {} };
18207
- if (tab) tab.api = api;
18208
- else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api }, reference.parentTab);
18209
- bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
18680
+ bound.push({ reference, filename, content: body, operations, tabName: bindApiTab(tabs, reference, api) });
18210
18681
  } catch (error) {
18211
18682
  warnings.push({
18212
18683
  code: "fetch-failed",
@@ -18215,19 +18686,22 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18215
18686
  });
18216
18687
  }
18217
18688
  }
18689
+ const pageMounts = bound.flatMap(({ reference, endpointOperations }) => endpointOperations && reference.endpointPages ? [reference.endpointPages.directory] : []);
18218
18690
  const directories = [
18219
18691
  ...(bundle.localApiMounts ?? []).map(({ directory: directory2 }) => directory2),
18220
- ...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory: directory2 }) => directory2))
18692
+ ...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory: directory2 }) => directory2)),
18693
+ ...pageMounts
18221
18694
  ];
18222
18695
  const endpointRedirects = linkApiMounts(
18223
18696
  [
18224
- ...(bundle.localApiMounts ?? []).map(({ filename, directory: directory2, tab }) => ({
18697
+ ...(bundle.localApiMounts ?? []).map(({ filename, directory: directory2, tab, endpointOperations }) => ({
18225
18698
  filename,
18226
18699
  content: Buffer.from(assets.find((asset) => asset.path === `openapi/${filename}`)?.content ?? []),
18227
18700
  prefix: directory2,
18701
+ ...endpointOperations ? { endpointPages: true, operations: endpointOperations } : {},
18228
18702
  ...tab ? { tab } : {}
18229
18703
  })),
18230
- ...bound.flatMap(({ reference, filename, content, tabName }) => (reference.directories ?? []).map(({ directory: directory2, operations: listed }) => ({ filename, content, prefix: directory2, tab: tabName, ...listed ? { operations: listed } : {} })))
18704
+ ...bound.flatMap(({ reference, filename, content, tabName, endpointOperations }) => endpointOperations && reference.endpointPages ? [{ filename, content, prefix: reference.endpointPages.directory, tab: tabName, endpointPages: true, operations: endpointOperations }] : (reference.directories ?? []).map(({ directory: directory2, operations: listed }) => ({ filename, content, prefix: directory2, tab: tabName, ...listed ? { operations: listed } : {} })))
18231
18705
  ],
18232
18706
  { ...bundle.docsConfig, tabs, redirects },
18233
18707
  pages,
@@ -18238,6 +18712,18 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18238
18712
  redirects.push(redirect);
18239
18713
  endpointUrls.set(literalRoute(redirect.source) ?? redirect.source, redirect.destination);
18240
18714
  }
18715
+ reportEndpointPages(ledger, warnings);
18716
+ qualifyDisplacedBareRefs({
18717
+ pages,
18718
+ before: specLookupOrder(apiTabs, bundle.docsConfig.navigation?.versions),
18719
+ after: specLookupOrder(tabs, bundle.docsConfig.navigation?.versions),
18720
+ content: (source) => {
18721
+ const asset = assets.find((entry) => entry.path === source);
18722
+ return asset ? Buffer.from(asset.content) : void 0;
18723
+ },
18724
+ qualifier: (source) => source
18725
+ });
18726
+ const served = servedRoutes(pages);
18241
18727
  const existingSources = new Set(redirects.map((redirect) => literalRoute(redirect.source) ?? redirect.source.toLowerCase()));
18242
18728
  const mounts = directories.map(mountPrefix).filter(Boolean);
18243
18729
  const underMount = (source) => mounts.some((prefix) => source.toLowerCase() === `/${prefix}` || source.toLowerCase().startsWith(`/${prefix}/`));
@@ -18262,12 +18748,19 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18262
18748
  const endpoint = endpointUrls.get(literalRoute(redirect.destination) ?? "");
18263
18749
  return endpoint ? { ...redirect, destination: endpoint } : redirect;
18264
18750
  });
18751
+ const boundConfig = { ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} };
18752
+ const newPages = pages.slice(pagesBefore);
18753
+ const withDirectories = bundle.platform === "mintlify" && newPages.length > 0 ? addMintlifyDirectoryRedirects(boundConfig, newPages) : boundConfig;
18754
+ const linkRedirects = new Set(pages.flatMap((page) => page.externalUrl ? pageEntryRoutes(page).map((source) => `${source} ${page.externalUrl}`) : []));
18755
+ const isLinkRedirect = (redirect) => linkRedirects.has(`${redirect.source} ${redirect.destination}`);
18756
+ const links = (withDirectories.redirects ?? []).filter(isLinkRedirect);
18757
+ const unchecked = links.length > 0 ? { ...withDirectories, redirects: withDirectories.redirects.filter((redirect) => !isLinkRedirect(redirect)) } : withDirectories;
18758
+ const checked = dropConflictingRedirects(dropUnroutableRedirects(unchecked, warnings), pages, warnings);
18265
18759
  return {
18266
18760
  ...bundle,
18267
18761
  pages,
18268
18762
  assets,
18269
- // Redirects added above meet the same checks as any other: none may fail the build, hide a page that exists, or loop.
18270
- docsConfig: dropConflictingRedirects(dropUnroutableRedirects({ ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} }, warnings), pages, warnings),
18763
+ docsConfig: links.length > 0 ? { ...checked, redirects: [...checked.redirects ?? [], ...links] } : checked,
18271
18764
  warnings
18272
18765
  };
18273
18766
  }