@thallylabs/migrate 0.2.37 → 0.2.39

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,6 +1,6 @@
1
1
  // src/url-run.ts
2
2
  import { createHash } from "crypto";
3
- import { load as load3 } from "cheerio";
3
+ import { load as load5 } from "cheerio";
4
4
  import remarkParse3 from "remark-parse";
5
5
  import remarkMdx3 from "remark-mdx";
6
6
  import { unified as unified3 } from "unified";
@@ -531,7 +531,53 @@ function cloneUrlMigrationCheckpoint(checkpoint) {
531
531
  }
532
532
 
533
533
  // src/url-navigation.ts
534
+ import { load as load2 } from "cheerio";
535
+
536
+ // src/url-scope.ts
534
537
  import { load } from "cheerio";
538
+ function inferUrlMigrationScope(capture) {
539
+ const source = capture.finalUrl;
540
+ let end = source.pathname.length;
541
+ while (end > 0 && source.pathname.charCodeAt(end - 1) === 47) end--;
542
+ const parent = end < source.pathname.length ? source.pathname.slice(0, end) || "/" : source.pathname.slice(0, source.pathname.lastIndexOf("/")) || "/";
543
+ if (!/html/i.test(capture.contentType) || Buffer.byteLength(capture.body) > 8e6) return { scopePath: parent, evidence: "source-parent" };
544
+ const $ = load(capture.body);
545
+ const ancestor = (path) => path === "/" || source.pathname === path || source.pathname.startsWith(`${path}/`);
546
+ const observed = (reference) => {
547
+ if (!reference || reference.length > 8192) return void 0;
548
+ try {
549
+ const url = new URL(reference, source);
550
+ if (url.origin !== source.origin || url.username || url.password || url.search || url.hash || /%2f|%5c/i.test(url.pathname)) return void 0;
551
+ return url;
552
+ } catch {
553
+ return void 0;
554
+ }
555
+ };
556
+ const indexes = [];
557
+ $("link[href],a[href]").slice(0, 1e4).each((_, node) => {
558
+ const url = observed($(node).attr("href"));
559
+ if (!url || !/\/(?:llms(?:-full)?\.txt|sitemap(?:[-_][^/]*)?\.xml)$/i.test(url.pathname)) return;
560
+ const path = url.pathname.slice(0, url.pathname.lastIndexOf("/")) || "/";
561
+ if (ancestor(path)) indexes.push(path);
562
+ });
563
+ if (indexes.length) return { scopePath: indexes.sort((a, b) => b.length - a.length)[0], evidence: "discovery-index" };
564
+ const paths = [];
565
+ $('nav a[href],aside a[href],[role="navigation"] a[href]').slice(0, 1e4).each((_, node) => {
566
+ if ($(node).closest("header,footer").length) return;
567
+ const url = observed($(node).attr("href"));
568
+ if (url && !/\.[a-z0-9]+$/i.test(url.pathname)) paths.push(url.pathname);
569
+ });
570
+ if (new Set(paths).size >= 2 && paths.some((path) => path === source.pathname)) {
571
+ const segments = paths.map((path) => path.split("/").filter(Boolean));
572
+ let count = 0;
573
+ while (segments.every((parts) => parts[count] !== void 0 && parts[count] === segments[0][count])) count++;
574
+ const common = "/" + segments[0].slice(0, count).join("/");
575
+ if (ancestor(common)) return { scopePath: common, evidence: "navigation" };
576
+ }
577
+ return { scopePath: parent, evidence: "source-parent" };
578
+ }
579
+
580
+ // src/url-navigation.ts
535
581
  var MAX_CAPTURE_BYTES = 8e6;
536
582
  var MAX_JSON_BYTES = 2e6;
537
583
  var MAX_NODES = 1e4;
@@ -598,8 +644,18 @@ function flightRecords(stream) {
598
644
  }
599
645
  return records;
600
646
  }
601
- function mintlifyNavigation(body, base, locale) {
602
- const $ = load(body);
647
+ function mintlifyNavigation(body, base, locale, selectedScope) {
648
+ const $ = load2(body);
649
+ const scope = inferUrlMigrationScope({ body, contentType: "text/html", finalUrl: base });
650
+ const mount = scope.evidence === "source-parent" ? selectedScope ?? "/" : scope.scopePath;
651
+ const configTarget = (value) => {
652
+ const reference = text(value);
653
+ if (!reference) return void 0;
654
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(reference)) return target(reference, base);
655
+ const path = reference.replace(/^\/+/, "");
656
+ const prefix = mount.replace(/^\/|\/$/g, "");
657
+ return target(prefix && path !== prefix && !path.startsWith(`${prefix}/`) ? `${prefix}/${path}` : path, new URL("/", base));
658
+ };
603
659
  let stream = "";
604
660
  const records = [];
605
661
  $("script").each((_index, script) => {
@@ -636,7 +692,7 @@ function mintlifyNavigation(body, base, locale) {
636
692
  const walk = (value, depth = 0, activeLocale) => {
637
693
  if (depth > MAX_DEPTH || ++count > MAX_NODES) throw new Error("Source navigation exceeds its safety bound.");
638
694
  if (typeof value === "string") {
639
- const url2 = target(value, new URL("/", base));
695
+ const url2 = configTarget(value);
640
696
  return url2 ? [{ kind: "page", label: value, url: url2 }] : [];
641
697
  }
642
698
  if (Array.isArray(value)) return value.flatMap((child) => walk(child, depth + 1, activeLocale));
@@ -656,7 +712,7 @@ function mintlifyNavigation(body, base, locale) {
656
712
  return;
657
713
  }
658
714
  if (!object(node)) return;
659
- const href = target(node.href, base);
715
+ const href = configTarget(node.href);
660
716
  if (href) {
661
717
  populated++;
662
718
  const url2 = new URL(href);
@@ -674,10 +730,10 @@ function mintlifyNavigation(body, base, locale) {
674
730
  const selected = matching[0]?.item ?? languages.find((item) => text(item.language)?.toLowerCase() === language?.toLowerCase()) ?? languages.find((item) => text(item.language)?.toLowerCase() === "en") ?? languages[0];
675
731
  return selected ? walk({ ...selected, languages: void 0 }, depth + 1, text(selected.language)) : [];
676
732
  }
677
- const label = text(value.tab) ?? text(value.group) ?? text(value.title) ?? text(value.label);
678
- const kind = text(value.tab) ? "tab" : text(value.group) ? "group" : "page";
733
+ const label = text(value.tab) ?? text(value.dropdown) ?? text(value.group) ?? text(value.sidebarTitle) ?? text(value.title) ?? text(value.label);
734
+ const kind = text(value.tab) || text(value.dropdown) ? "tab" : text(value.group) ? "group" : "page";
679
735
  const children = ["tabs", "groups", "pages", "dropdowns", "anchors", "versions"].flatMap((key) => Array.isArray(value[key]) ? walk(value[key], depth + 1, activeLocale) : []);
680
- const url = target(value.href, base);
736
+ const url = configTarget(value.href);
681
737
  if (!label) return children;
682
738
  if (!url && !children.length) return [];
683
739
  return [{
@@ -685,6 +741,7 @@ function mintlifyNavigation(body, base, locale) {
685
741
  label,
686
742
  ...url ? { url } : {},
687
743
  ...children.length ? { children } : {},
744
+ ...text(value.dropdown) ? { presentation: "dropdown" } : {},
688
745
  ...kind === "tab" && activeLocale ? { locale: activeLocale } : {}
689
746
  }];
690
747
  };
@@ -711,18 +768,19 @@ function resolveNavigation(nodes, base) {
711
768
  ...url ? { url } : {},
712
769
  ...children.length ? { children } : {},
713
770
  ...["tab", "group", "page"].includes(node.kind ?? "") ? { kind: node.kind } : {},
771
+ ...node.presentation === "dropdown" ? { presentation: "dropdown" } : {},
714
772
  ...text(node.locale) ? { locale: text(node.locale) } : {}
715
773
  }];
716
774
  });
717
775
  };
718
776
  return walk(nodes);
719
777
  }
720
- function extractSourceNavigation(capture) {
778
+ function extractSourceNavigation(capture, selectedScope) {
721
779
  if (capture.navigation) return resolveNavigation(capture.navigation, capture.finalUrl);
722
780
  if (!/html/i.test(capture.contentType) || Buffer.byteLength(capture.body) > MAX_CAPTURE_BYTES) return [];
723
- const mintlify = mintlifyNavigation(capture.body, capture.finalUrl, capture.locale);
781
+ const mintlify = mintlifyNavigation(capture.body, capture.finalUrl, capture.locale, selectedScope);
724
782
  if (mintlify.length) return mintlify;
725
- const $ = load(capture.body);
783
+ const $ = load2(capture.body);
726
784
  const candidates = ['.theme-doc-sidebar-menu,[aria-label*="sidebar" i]', "aside", '[role="navigation"],nav'];
727
785
  const root = candidates.map((selector) => $(selector).filter((_index, node) => $(node).find("a[href]").length > 0 && $(node).closest("header").length === 0).first()).find((candidate) => candidate.length);
728
786
  if (!root) return [];
@@ -804,7 +862,7 @@ function migrationImageUrl(value, base) {
804
862
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error("Responsive image URL is not a credential-free HTTP(S) URL.");
805
863
  return url.toString();
806
864
  }
807
- function migrationImageCandidates(value, base, resolve3 = true) {
865
+ function migrationImageCandidates(value, base, resolve4 = true) {
808
866
  if (value.length > 64e3) throw new Error("Responsive image candidate list exceeds its safety bound.");
809
867
  const result = [];
810
868
  let index = 0;
@@ -822,7 +880,7 @@ function migrationImageCandidates(value, base, resolve3 = true) {
822
880
  if (descriptor && !/^(?:[1-9]\d*w|(?:\d+(?:\.\d+)?|\.\d+)x)$/.test(descriptor)) throw new Error("Responsive image has an unsupported descriptor.");
823
881
  if (descriptor.endsWith("x") && Number(descriptor.slice(0, -1)) <= 0) throw new Error("Responsive image density must be positive.");
824
882
  const resolved = migrationImageUrl(url, base);
825
- result.push({ url: resolve3 ? resolved : url, descriptor });
883
+ result.push({ url: resolve4 ? resolved : url, descriptor });
826
884
  if (result.length > 128) throw new Error("Responsive image has too many candidates.");
827
885
  if (value[index] === ",") index++;
828
886
  }
@@ -836,8 +894,88 @@ function migrationImageAttribute(value) {
836
894
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\{/g, "&#123;").replace(/\}/g, "&#125;").replace(/\r/g, "&#13;").replace(/\n/g, "&#10;");
837
895
  }
838
896
 
897
+ // src/url-media.ts
898
+ import { load as load3 } from "cheerio";
899
+ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
900
+ "src",
901
+ "title",
902
+ "poster",
903
+ "controls",
904
+ "muted",
905
+ "autoplay",
906
+ "loop",
907
+ "playsinline",
908
+ "class",
909
+ "id",
910
+ "width",
911
+ "height",
912
+ "preload",
913
+ "aria-label"
914
+ ]);
915
+ var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
916
+ "src",
917
+ "title",
918
+ "width",
919
+ "height",
920
+ "allow",
921
+ "allowfullscreen",
922
+ "referrerpolicy",
923
+ "frameborder",
924
+ "loading",
925
+ "class",
926
+ "id",
927
+ "aria-label"
928
+ ]);
929
+ var YOUTUBE_HOSTS = /* @__PURE__ */ new Set([
930
+ "youtube.com",
931
+ "www.youtube.com",
932
+ "youtube-nocookie.com",
933
+ "www.youtube-nocookie.com"
934
+ ]);
935
+ function projectMigrationMedia(html, base) {
936
+ const $ = load3(html, void 0, false);
937
+ const nodes = $.root().contents().filter((_index, node2) => node2.type !== "text" || Boolean(node2.data.trim()));
938
+ if (nodes.length !== 1) return null;
939
+ const element = nodes.first();
940
+ const node = element[0];
941
+ if (node.type !== "tag" || !["video", "iframe"].includes(node.name)) return null;
942
+ if (element.children().length || element.text().trim()) return null;
943
+ const allowed = node.name === "video" ? VIDEO_ATTRIBUTES : YOUTUBE_ATTRIBUTES;
944
+ if (Object.keys(node.attribs).some((name) => !allowed.has(name))) return null;
945
+ const source = element.attr("src");
946
+ if (!source) return null;
947
+ try {
948
+ let src = migrationImageUrl(source, base);
949
+ let component = "Video";
950
+ const props = [];
951
+ if (node.name === "video") {
952
+ if (!/\.(?:mp4|webm|ogg|mov|m4v)(?:[?#]|$)/i.test(src)) component = "video";
953
+ const isMuted = element.attr("muted") !== void 0;
954
+ const hasAutoplay = element.attr("autoplay") !== void 0;
955
+ if (hasAutoplay && !isMuted) return null;
956
+ props.push("controls={true}");
957
+ for (const [attribute, prop] of [["muted", "muted"], ["loop", "loop"], ["playsinline", "playsInline"], ["autoplay", "autoPlay"]]) {
958
+ if (element.attr(attribute) !== void 0) props.push(`${prop}={true}`);
959
+ }
960
+ const poster = element.attr("poster");
961
+ if (poster) props.push(`poster="${migrationImageAttribute(migrationImageUrl(poster, base))}"`);
962
+ } else {
963
+ const url = new URL(src);
964
+ if (url.protocol !== "https:" || url.port || !YOUTUBE_HOSTS.has(url.hostname)) return null;
965
+ const id = url.pathname.match(/^\/embed\/([A-Za-z0-9_-]{11})$/)?.[1];
966
+ if (!id || url.hash || [...url.searchParams.keys()].some((name) => name !== "si")) return null;
967
+ src = `https://www.youtube-nocookie.com/embed/${id}`;
968
+ }
969
+ const title = element.attr("title") ?? element.attr("aria-label");
970
+ if (title !== void 0) props.push(`title="${migrationImageAttribute(title)}"`);
971
+ return `<${component} src="${migrationImageAttribute(src)}"${props.length ? ` ${props.join(" ")}` : ""} />`;
972
+ } catch {
973
+ return null;
974
+ }
975
+ }
976
+
839
977
  // src/url.ts
840
- import { load as load2 } from "cheerio";
978
+ import { load as load4 } from "cheerio";
841
979
  import remarkMdx2 from "remark-mdx";
842
980
  import remarkParse2 from "remark-parse";
843
981
  import TurndownService from "turndown";
@@ -2470,9 +2608,10 @@ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
2470
2608
  "Warning",
2471
2609
  "YouTube",
2472
2610
  "picture",
2473
- "source"
2611
+ "source",
2612
+ "video"
2474
2613
  ]);
2475
- var PORTABLE_URL_PROPS = "href|src|img|primaryHref|secondaryHref";
2614
+ var PORTABLE_URL_PROPS = "href|src|poster|img|primaryHref|secondaryHref";
2476
2615
  var PORTABLE_URL_PROP_NAMES = new Set(PORTABLE_URL_PROPS.split("|"));
2477
2616
  var MINTLIFY_ARTICLE_LABELS = [
2478
2617
  { baseUrl: "Select base URL", permalink: "Navigate to header" },
@@ -2612,7 +2751,7 @@ function isSafePortableUrl(value) {
2612
2751
  }
2613
2752
  }
2614
2753
  function decodeHtmlAttribute(value) {
2615
- const document = load2(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
2754
+ const document = load4(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
2616
2755
  return document("i").attr("data-thally-value") ?? "";
2617
2756
  }
2618
2757
  function escapeMarkdownText(value, escapeTableSeparator = false) {
@@ -2698,7 +2837,7 @@ function replacePortableHtmlImages(value) {
2698
2837
  if (start < cursor) continue;
2699
2838
  const end = htmlOpeningTagEnd(value, start + match[0].length);
2700
2839
  if (end === null) continue;
2701
- const fragment = load2(value.slice(start, end));
2840
+ const fragment = load4(value.slice(start, end));
2702
2841
  const image = fragment("img").first();
2703
2842
  const source = image.attr("src");
2704
2843
  const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
@@ -2723,7 +2862,7 @@ function replacePortableHtmlAnchors(value) {
2723
2862
  const closing = closingMatch.exec(value);
2724
2863
  if (!closing) continue;
2725
2864
  const end = closing.index + closing[0].length;
2726
- const fragment = load2(value.slice(start, end));
2865
+ const fragment = load4(value.slice(start, end));
2727
2866
  const anchor = fragment("a").first();
2728
2867
  const href = anchor.attr("href");
2729
2868
  const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
@@ -2758,6 +2897,10 @@ function sanitizeMarkdownUrls(value) {
2758
2897
  hasUnsafeMdxSyntax = true;
2759
2898
  return;
2760
2899
  }
2900
+ if (node.name === "video" && node.children?.length) {
2901
+ hasUnsafeMdxSyntax = true;
2902
+ return;
2903
+ }
2761
2904
  for (const attribute of node.attributes ?? []) {
2762
2905
  if (attribute.type !== "mdxJsxAttribute" || !attribute.name || /^on[A-Z]/.test(attribute.name)) {
2763
2906
  hasUnsafeMdxSyntax = true;
@@ -2794,6 +2937,22 @@ function sanitizeMarkdownUrls(value) {
2794
2937
  return;
2795
2938
  }
2796
2939
  }
2940
+ if (node.name === "video") {
2941
+ const isBoolean = ["controls", "muted", "autoPlay", "loop", "playsInline"].includes(attribute.name);
2942
+ const isText = ["src", "poster", "title"].includes(attribute.name);
2943
+ if (isBoolean && typeof staticValue !== "boolean" || isText && typeof attribute.value !== "string" || !isBoolean && !isText) {
2944
+ hasUnsafeMdxSyntax = true;
2945
+ return;
2946
+ }
2947
+ if (attribute.name === "src" || attribute.name === "poster") {
2948
+ try {
2949
+ migrationImageUrl(decodeHtmlAttribute(attribute.value), new URL("https://migration.invalid"));
2950
+ } catch {
2951
+ hasUnsafeMdxSyntax = true;
2952
+ return;
2953
+ }
2954
+ }
2955
+ }
2797
2956
  }
2798
2957
  }
2799
2958
  for (const child of node.children ?? []) validateMdxSyntax(child);
@@ -2906,7 +3065,7 @@ function sanitizeRemoteMarkdown(body) {
2906
3065
  const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
2907
3066
  if (urlSafeMarkdown === null) return null;
2908
3067
  const executable = withoutFencedCode(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
2909
- if (/^\s*(?:import|export)\s/m.test(executable)) return null;
3068
+ if (/^[\t ]*(?:import|export)[\t ]/m.test(executable)) return null;
2910
3069
  let hasUnsafeExpression = false;
2911
3070
  const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
2912
3071
  try {
@@ -2964,7 +3123,7 @@ function extractEmbeddedOpenApi(content) {
2964
3123
  const root = embeddedOpenApiParser.parse(content);
2965
3124
  const candidates = (root.children ?? []).flatMap((node2) => {
2966
3125
  if (node2.type !== "code" || !/^(?:yaml|yml|json)$/i.test(node2.lang ?? "")) return [];
2967
- const metadata = node2.meta?.match(/^(?:(\S+\.(?:yaml|yml|json))\s+)?(delete|get|head|options|patch|post|put|trace)\s+(\/\S+)\s*$/i);
3126
+ const metadata = node2.meta?.match(/^(?:((?:\S+\.(?:yaml|yml|json)|[A-Za-z][A-Za-z0-9_-]{0,127}))\s+)?(delete|get|head|options|patch|post|put|trace)\s+(\/\S+)\s*$/i);
2968
3127
  return metadata ? [{ node: node2, method: metadata[2].toUpperCase(), path: metadata[3] }] : [];
2969
3128
  });
2970
3129
  if (candidates.length !== 1) return { content };
@@ -3049,7 +3208,7 @@ function markdownPage(document, id, platform, warn) {
3049
3208
  };
3050
3209
  }
3051
3210
  function migrationHtmlArticle(body) {
3052
- const $ = load2(body);
3211
+ const $ = load4(body);
3053
3212
  const root = ["main article", "article", "main", '[role="main"]', "body"].map((selector) => $(selector).first()).find((candidate) => candidate.length);
3054
3213
  root.find([
3055
3214
  "script",
@@ -3067,7 +3226,10 @@ function migrationHtmlArticle(body) {
3067
3226
  "#chat-assistant-sheet",
3068
3227
  "#chat-assistant-textarea"
3069
3228
  ].join(",")).remove();
3070
- const permalinkSelectors = MINTLIFY_ARTICLE_LABELS.map((labels) => `a[aria-label="${labels.permalink}"]`);
3229
+ root.find('div[data-component-part="code-group-tab-content"]').each((_index, element) => {
3230
+ if ($(element).parents("pre").length === 0 && $(element).find("pre").length === 0) element.name = "pre";
3231
+ });
3232
+ const permalinkSelectors = MINTLIFY_ARTICLE_LABELS.map((labels2) => `a[aria-label="${labels2.permalink}"]`);
3071
3233
  root.find([...permalinkSelectors, "h1 .hash-link", "h2 .hash-link", "h3 .hash-link", "h4 .hash-link", "h5 .hash-link", "h6 .hash-link"].join(",")).each((_index, link) => {
3072
3234
  const parent = $(link).parent();
3073
3235
  $(link).remove();
@@ -3078,7 +3240,7 @@ function migrationHtmlArticle(body) {
3078
3240
  const isChecked = $(element).attr("checked") !== void 0;
3079
3241
  $(element).replaceWith($("<span></span>").attr("data-thally-static-task-checkbox", isChecked ? "checked" : "unchecked").text(isChecked ? "[x] " : "[ ] "));
3080
3242
  });
3081
- root.find(MINTLIFY_ARTICLE_LABELS.map((labels) => `select[aria-label="${labels.baseUrl}"]`).join(",")).each((_index, element) => {
3243
+ root.find(MINTLIFY_ARTICLE_LABELS.map((labels2) => `select[aria-label="${labels2.baseUrl}"]`).join(",")).each((_index, element) => {
3082
3244
  const select = $(element);
3083
3245
  const options = select.children("option").toArray();
3084
3246
  if (!options.length || options.length !== select.children().length) return;
@@ -3117,7 +3279,7 @@ function htmlPage(document, id, options = {}) {
3117
3279
  const title = scopedTitle || $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
3118
3280
  const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
3119
3281
  let documentHeadingAnchor = "";
3120
- const excludedElements = options.omitDocumentHeading ? "script,style,noscript,nav,footer,aside,form,button,svg,iframe,object,embed,link,meta" : "script,style,noscript,nav,header,footer,aside,form,button,svg,iframe,object,embed,link,meta";
3282
+ const excludedElements = options.omitDocumentHeading ? "script,style,noscript,nav,footer,aside,form,button,svg,object,embed,link,meta" : "script,style,noscript,nav,header,footer,aside,form,button,svg,object,embed,link,meta";
3121
3283
  root.find(excludedElements).remove();
3122
3284
  root.find(".theme-doc-version-badge,.theme-doc-breadcrumbs,.theme-doc-toc-mobile,.table-of-contents,.hash-link").remove();
3123
3285
  if (options.omitDocumentHeading) {
@@ -3175,6 +3337,18 @@ function htmlPage(document, id, options = {}) {
3175
3337
  // Emit literal task-list syntax rather than escaping its brackets as copy.
3176
3338
  replacement: (_content, node) => node.getAttribute("data-thally-static-task-checkbox") === "checked" ? "[x] " : "[ ] "
3177
3339
  });
3340
+ turndown.addRule("migration-authored-paragraph", {
3341
+ filter: (node) => node.nodeName === "SPAN" && node.getAttribute("data-as") === "p",
3342
+ // Some publishers render authored paragraphs as spans. Their explicit
3343
+ // semantic hint must retain a block boundary: otherwise following copy
3344
+ // joins a picture's closing tag or an accordion's source anchor and can
3345
+ // change the resulting MDX grammar. Ordinary spans remain inline.
3346
+ replacement: (content) => `
3347
+
3348
+ ${content}
3349
+
3350
+ `
3351
+ });
3178
3352
  const anchor = (element) => {
3179
3353
  const value = element.getAttribute("id");
3180
3354
  return options.preserveAnchors && value ? `<a id="${value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/\{/g, "&#123;")}" />
@@ -3193,6 +3367,18 @@ ${anchor(element)}${heading ? `${"#".repeat(Number(heading[1]))} ${content.trim(
3193
3367
  `;
3194
3368
  }
3195
3369
  });
3370
+ turndown.addRule("migration-static-media", {
3371
+ filter: (node) => node.nodeName === "VIDEO" || node.nodeName === "IFRAME",
3372
+ replacement: (_content, node) => {
3373
+ const element = node;
3374
+ const projected = projectMigrationMedia(element.outerHTML, document.finalUrl);
3375
+ return projected ? `
3376
+
3377
+ ${anchor(element)}${projected}
3378
+
3379
+ ` : "";
3380
+ }
3381
+ });
3196
3382
  if (options.preserveResponsiveImages) {
3197
3383
  const image = (element) => {
3198
3384
  const tag = element.nodeName.toLowerCase();
@@ -3228,7 +3414,7 @@ ${content.trim()}
3228
3414
  const element = node;
3229
3415
  const className = element.className;
3230
3416
  const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
3231
- const fragment = load2(element.outerHTML);
3417
+ const fragment = load4(element.outerHTML);
3232
3418
  const clone = fragment(".theme-admonition").first();
3233
3419
  const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
3234
3420
  const titleText = title2.text().trim();
@@ -3249,15 +3435,15 @@ ${titleText ? `**${escapeMarkdownText(titleText)}**
3249
3435
  filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
3250
3436
  replacement: (_content, node) => {
3251
3437
  const element = node;
3252
- const fragment = load2(element.outerHTML);
3438
+ const fragment = load4(element.outerHTML);
3253
3439
  const container = fragment(".tabs-container").first();
3254
- const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
3440
+ const labels2 = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
3255
3441
  const panels = container.find('[role="tabpanel"]').toArray().filter((panel) => fragment(panel).closest(".tabs-container")[0] === container[0]);
3256
3442
  if (panels.length === 0) return _content;
3257
3443
  const tabs = panels.map((panel, index) => {
3258
3444
  const panelId = fragment(panel).attr("id");
3259
3445
  const tab = panelId ? container.find('[role="tab"]').toArray().find((item) => fragment(item).attr("aria-controls") === panelId) : void 0;
3260
- const title2 = migrationImageAttribute((tab ? fragment(tab).text().trim() : labels[index]) || `Tab ${index + 1}`);
3446
+ const title2 = migrationImageAttribute((tab ? fragment(tab).text().trim() : labels2[index]) || `Tab ${index + 1}`);
3261
3447
  const panelAnchor = options.preserveAnchors && panelId ? `<a id="${migrationImageAttribute(panelId)}" />
3262
3448
 
3263
3449
  ` : "";
@@ -3277,7 +3463,7 @@ ${tabs.join("\n\n")}
3277
3463
  turndown.addRule("gfm-table", {
3278
3464
  filter: "table",
3279
3465
  replacement: (_content, node) => {
3280
- const fragment = load2(node.outerHTML);
3466
+ const fragment = load4(node.outerHTML);
3281
3467
  const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => {
3282
3468
  const wasConvertingTableCell = isConvertingTableCell;
3283
3469
  isConvertingTableCell = true;
@@ -3303,10 +3489,17 @@ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
3303
3489
  filter: (node) => node.nodeName === "PRE",
3304
3490
  replacement: (_content, node) => {
3305
3491
  const element = node;
3306
- const fragment = load2(element.outerHTML);
3492
+ const fragment = load4(element.outerHTML);
3307
3493
  const tokenLines = fragment(".token-line").toArray();
3308
3494
  const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
3309
- const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
3495
+ const language = ["code", "pre"].flatMap((selector) => {
3496
+ const block = fragment(selector).first();
3497
+ return [
3498
+ block.attr("language"),
3499
+ block.attr("data-language"),
3500
+ block.attr("class")?.match(/(?:^|\s)(?:language-|lang-)([A-Za-z0-9_+-]+)(?=\s|$)/)?.[1]
3501
+ ];
3502
+ }).find((value) => value !== void 0 && /^[A-Za-z0-9_+-]{1,64}$/.test(value)) ?? "";
3310
3503
  const longestFence = Math.max(0, ...[...code.matchAll(/`+/g)].map((match) => match[0].length));
3311
3504
  const fence = "`".repeat(Math.max(3, longestFence + 1));
3312
3505
  return `
@@ -3336,8 +3529,127 @@ ${fence}
3336
3529
  };
3337
3530
  }
3338
3531
 
3532
+ // src/url-api-schema-controls.ts
3533
+ var MAX_CAPTURE_BYTES2 = 8e6;
3534
+ var MAX_TAB_GROUPS = 256;
3535
+ var MAX_TABS = 1024;
3536
+ function object2(value) {
3537
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3538
+ }
3539
+ function localReference(document, reference) {
3540
+ if (typeof reference !== "string" || !reference.startsWith("#/")) return void 0;
3541
+ let target2 = document;
3542
+ try {
3543
+ for (const part of reference.slice(2).split("/")) {
3544
+ const key = decodeURIComponent(part).replace(/~1/g, "/").replace(/~0/g, "~");
3545
+ if (!target2 || typeof target2 !== "object" || !Object.hasOwn(target2, key)) return void 0;
3546
+ target2 = target2[key];
3547
+ }
3548
+ } catch {
3549
+ return void 0;
3550
+ }
3551
+ return target2;
3552
+ }
3553
+ function resolve2(document, value) {
3554
+ const seen = /* @__PURE__ */ new Set();
3555
+ for (let depth = 0; depth < 32; depth += 1) {
3556
+ const schema = object2(value);
3557
+ if (!schema || seen.has(schema)) return void 0;
3558
+ seen.add(schema);
3559
+ if (!Object.hasOwn(schema, "$ref")) return schema;
3560
+ value = localReference(document, schema.$ref);
3561
+ }
3562
+ return void 0;
3563
+ }
3564
+ function complete(document) {
3565
+ let visited = 0;
3566
+ const active = /* @__PURE__ */ new Set();
3567
+ const visit = (value, depth) => {
3568
+ if (++visited > 1e5 || depth > 96) return false;
3569
+ if (typeof value === "number" && !Number.isFinite(value)) return false;
3570
+ if (!value || typeof value !== "object") return true;
3571
+ if (active.has(value)) return false;
3572
+ active.add(value);
3573
+ const schema = object2(value);
3574
+ const valid = (!schema || !Object.hasOwn(schema, "$ref") || Boolean(object2(localReference(document, schema.$ref)))) && Object.values(value).every((child) => visit(child, depth + 1));
3575
+ active.delete(value);
3576
+ return valid;
3577
+ };
3578
+ return visit(document, 0);
3579
+ }
3580
+ function labels(document, value) {
3581
+ const schema = resolve2(document, value);
3582
+ if (!Array.isArray(schema?.oneOf) || schema.oneOf.length < 2 || schema.oneOf.length > 64) return void 0;
3583
+ const result = [];
3584
+ for (const [index, branch] of schema.oneOf.entries()) {
3585
+ const target2 = resolve2(document, branch);
3586
+ if (!target2) return void 0;
3587
+ const title = object2(branch)?.title ?? target2.title;
3588
+ result.push(typeof title === "string" && title.trim() ? title.trim().replace(/\s+/g, " ") : `Option ${index + 1}`);
3589
+ }
3590
+ return new Set(result).size === result.length ? result : void 0;
3591
+ }
3592
+ function apiSchemaTabGroups(body, document, operation) {
3593
+ if (Buffer.byteLength(body) > MAX_CAPTURE_BYTES2 || !complete(document)) return [];
3594
+ const path = object2(document.paths)?.[operation.path];
3595
+ const method = operation.method.toLowerCase();
3596
+ if (!/^(?:get|post|put|patch|delete|head|options|trace)$/.test(method)) return [];
3597
+ const selected = object2(object2(path)?.[method]);
3598
+ if (!selected) return [];
3599
+ const { $, root } = migrationHtmlArticle(body);
3600
+ const groups = root.find('[role="tablist"],[data-component-part="tabs-list"]');
3601
+ if (groups.length > MAX_TAB_GROUPS || root.find('[role="tab"]').length > MAX_TABS) return [];
3602
+ const idCounts = /* @__PURE__ */ new Map();
3603
+ root.find("[id]").each((_index, node) => {
3604
+ const id = $(node).attr("id");
3605
+ idCounts.set(id, (idCounts.get(id) ?? 0) + 1);
3606
+ });
3607
+ const proofs = [];
3608
+ groups.each((index, node) => {
3609
+ const group = $(node);
3610
+ const container = group.parent(".tabs.tab-container");
3611
+ const field = group.closest(".object-param-field");
3612
+ const section = field.parent(".api-section");
3613
+ if (!group.is('[role="tablist"][data-component-part="tabs-list"]') || !container.length || !section.length || !section.closest('[id^="api-playground-"][id$="-operation-page"]').length) return;
3614
+ if (field.parents(".object-param-field").length) return;
3615
+ const heading = section.children(".api-section-heading");
3616
+ const title = heading.find(".api-section-heading-title").text().trim().toLowerCase();
3617
+ const media = heading.find("*").toArray().filter((element) => $(element).children().length === 0).map((element) => $(element).text().trim()).filter((text2) => /^[a-z][a-z\d.+-]*\/[a-z\d.+-]+$/i.test(text2));
3618
+ if (heading.length !== 1 || !media || media.length !== 1) return;
3619
+ let schema;
3620
+ let scope;
3621
+ if (title === "body") {
3622
+ const request = resolve2(document, selected.requestBody);
3623
+ schema = object2(object2(request?.content)?.[media[0]])?.schema;
3624
+ scope = `request:${media[0]}`;
3625
+ } else if (title === "response") {
3626
+ const statuses = heading.find('button[aria-haspopup="menu"]').toArray().map((button) => $(button).text().trim()).filter((text2) => /^(?:[1-5]\d{2}|default)$/.test(text2));
3627
+ if (statuses.length !== 1) return;
3628
+ const response = resolve2(document, object2(selected.responses)?.[statuses[0]]);
3629
+ schema = object2(object2(response?.content)?.[media[0]])?.schema;
3630
+ scope = `response:${statuses[0]}:${media[0]}`;
3631
+ } else return;
3632
+ const expected = labels(document, schema);
3633
+ const tabs = group.children('[role="tab"][data-component-part="tab"]');
3634
+ if (!expected || tabs.length !== expected.length || group.children().length !== tabs.length) return;
3635
+ const panels = container.children('[data-component-part="tabs-panels"]').children('[role="tabpanel"][data-component-part="tab-content"]');
3636
+ if (panels.length !== tabs.length) return;
3637
+ const ids = /* @__PURE__ */ new Set();
3638
+ for (const [position, tab] of tabs.toArray().entries()) {
3639
+ const control = $(tab).attr("aria-controls");
3640
+ const panel = panels.eq(position);
3641
+ if ($(tab).text().trim().replace(/\s+/g, " ") !== expected[position] || !control || ids.has(control) || panel.attr("id") !== control || !$(tab).attr("id") || panel.attr("aria-labelledby") !== $(tab).attr("id") || idCounts.get(control) !== 1 || panel.text().trim() || panel.children().length) return;
3642
+ ids.add(control);
3643
+ }
3644
+ proofs.push({ index, scope });
3645
+ });
3646
+ const scopeCounts = /* @__PURE__ */ new Map();
3647
+ for (const proof of proofs) scopeCounts.set(proof.scope, (scopeCounts.get(proof.scope) ?? 0) + 1);
3648
+ return proofs.filter((proof) => scopeCounts.get(proof.scope) === 1).map((proof) => proof.index);
3649
+ }
3650
+
3339
3651
  // src/url-run.ts
3340
- var CONVERTER_VERSION = "url-evidence-v4";
3652
+ var CONVERTER_VERSION = "url-evidence-v5";
3341
3653
  var parser = unified3().use(remarkParse3).use(remarkMdx3);
3342
3654
  var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
3343
3655
  var MAX_RESPONSE_BYTES = 8e6;
@@ -3400,7 +3712,7 @@ function linksOf(document) {
3400
3712
  return [...root.find("a[href]").toArray(), ...$('link[rel="sitemap"][href],link[rel="alternate"][href]').toArray()].map((node) => $(node).attr("href")).filter(Boolean);
3401
3713
  }
3402
3714
  if (/xml/i.test(document.contentType) || /<urlset|<sitemapindex/i.test(document.body)) {
3403
- const $ = load3(document.body, { xmlMode: true });
3715
+ const $ = load5(document.body, { xmlMode: true });
3404
3716
  return $("loc").toArray().map((node) => $(node).text().trim());
3405
3717
  }
3406
3718
  return [
@@ -3443,8 +3755,8 @@ function markdownHeadingAnchors(body) {
3443
3755
  return occurrence ? `${base}-${occurrence}` : base;
3444
3756
  });
3445
3757
  }
3446
- function sourceNavigation(capture) {
3447
- return extractSourceNavigation(capture);
3758
+ function sourceNavigation(capture, selectedScope) {
3759
+ return extractSourceNavigation(capture, selectedScope);
3448
3760
  }
3449
3761
  function idForUrl(url, scope) {
3450
3762
  const relative4 = scope === "/" ? url.pathname.slice(1) : url.pathname.slice(scope.length).replace(/^\//, "");
@@ -3453,7 +3765,7 @@ function idForUrl(url, scope) {
3453
3765
  }
3454
3766
  function articleIdentity(document) {
3455
3767
  if (!/html/i.test(document.contentType)) return document.finalUrl;
3456
- const $ = load3(document.body);
3768
+ const $ = load5(document.body);
3457
3769
  const reference = $('link[rel="canonical"][href]').first().attr("href");
3458
3770
  if (!reference) return document.finalUrl;
3459
3771
  try {
@@ -3509,16 +3821,24 @@ function assetReferences(capture) {
3509
3821
  }
3510
3822
  const { $, root } = migrationHtmlArticle(capture.body);
3511
3823
  return [
3512
- ...root.find("img[src],video[poster],a[download][href]").toArray().map((node) => $(node).attr("src") ?? $(node).attr("poster") ?? $(node).attr("href")),
3824
+ ...root.find("img[src],video[poster],a[download][href]").toArray().map((node) => $(node).is("video") ? $(node).attr("poster") : $(node).is("a") ? $(node).attr("href") : $(node).attr("src")),
3513
3825
  ...root.find("[srcset]").toArray().flatMap((node) => migrationImageCandidates($(node).attr("srcset") ?? "", capture.finalUrl).map((candidate) => candidate.url))
3514
3826
  ].filter(Boolean);
3515
3827
  }
3516
- function unresolvedStaticControls(body, hasApiSchema = false) {
3828
+ function unresolvedStaticControls(body, hasApiSchema = false, coveredSchemaGroups = []) {
3517
3829
  const { $, root } = migrationHtmlArticle(body);
3830
+ root.find('[role="tablist"],[data-component-part="tabs-list"]').each((index, node) => {
3831
+ if (coveredSchemaGroups.includes(index)) $(node).remove();
3832
+ });
3518
3833
  const tabs = root.find('[role="tab"]').toArray();
3519
3834
  for (const tab of tabs) {
3520
3835
  const controls = $(tab).attr("aria-controls");
3521
- const panels = controls ? root.find("[id]").filter((_index, node) => $(node).attr("id") === controls) : root.find('[role="tabpanel"]').eq(tabs.indexOf(tab));
3836
+ const group = $(tab).closest('[role="tablist"]');
3837
+ const container = group.closest(".code-group,.tabs-container,.tabs.tab-container");
3838
+ const peers = group.find('[role="tab"]').toArray().filter((node) => $(node).closest('[role="tablist"]')[0] === group[0]);
3839
+ const localPanels = container.find('[role="tabpanel"]').toArray().filter((node) => $(node).closest(".code-group,.tabs-container,.tabs.tab-container")[0] === container[0]);
3840
+ const owners = container.find('[role="tablist"]').toArray().filter((node) => $(node).closest(".code-group,.tabs-container,.tabs.tab-container")[0] === container[0]);
3841
+ const panels = controls ? root.find("[id]").filter((_index, node) => $(node).attr("id") === controls) : owners.length === 1 && peers.length === localPanels.length ? $(localPanels).eq(peers.indexOf(tab)) : $([]);
3522
3842
  if (panels.length !== 1 || !panels.text().trim() && !panels.find("pre,img,table,svg").length || panels.find('[aria-busy="true"],[role="progressbar"]').length) return true;
3523
3843
  }
3524
3844
  return root.find("details:not([open])").toArray().some((node) => {
@@ -3569,12 +3889,12 @@ function hasCompleteLocalApiSchema(document) {
3569
3889
  if (typeof value === "number" && !Number.isFinite(value)) return false;
3570
3890
  if (!value || typeof value !== "object") return true;
3571
3891
  if (Array.isArray(value)) return value.every((child) => visit(child, depth + 1));
3572
- const object2 = value;
3573
- if (Object.prototype.hasOwnProperty.call(object2, "$ref")) {
3574
- if (typeof object2.$ref !== "string" || !object2.$ref.startsWith("#/")) return false;
3892
+ const object3 = value;
3893
+ if (Object.prototype.hasOwnProperty.call(object3, "$ref")) {
3894
+ if (typeof object3.$ref !== "string" || !object3.$ref.startsWith("#/")) return false;
3575
3895
  let target2 = document;
3576
3896
  try {
3577
- for (const encoded of object2.$ref.slice(2).split("/")) {
3897
+ for (const encoded of object3.$ref.slice(2).split("/")) {
3578
3898
  const key = decodeURIComponent(encoded).replace(/~1/g, "/").replace(/~0/g, "~");
3579
3899
  if (!target2 || typeof target2 !== "object" || !Object.prototype.hasOwnProperty.call(target2, key)) return false;
3580
3900
  target2 = target2[key];
@@ -3583,7 +3903,7 @@ function hasCompleteLocalApiSchema(document) {
3583
3903
  return false;
3584
3904
  }
3585
3905
  }
3586
- return Object.values(object2).every((child) => visit(child, depth + 1));
3906
+ return Object.values(object3).every((child) => visit(child, depth + 1));
3587
3907
  };
3588
3908
  return visit(document);
3589
3909
  }
@@ -3598,6 +3918,25 @@ function resolveMigrationRecord(records, record) {
3598
3918
  }
3599
3919
  function navigationConfig(checkpoint, records) {
3600
3920
  const navigation = checkpoint.navigation;
3921
+ const shortcuts = [];
3922
+ const collectShortcuts = (nodes) => {
3923
+ for (const node of nodes) {
3924
+ if (node.url) {
3925
+ const record = records.get(canonical(node.url, checkpoint.sourceUrl) ?? "");
3926
+ const href = record?.redirectTo ?? node.url;
3927
+ const identity = canonical(href, checkpoint.sourceUrl);
3928
+ const target2 = identity ? new URL(href, checkpoint.sourceUrl).href : null;
3929
+ if (target2 && !inScope(new URL(target2), new URL(checkpoint.sourceUrl), checkpoint.scopePath) && !shortcuts.some((link) => link.label === node.label && link.href === target2)) shortcuts.push({ label: node.label, href: target2 });
3930
+ }
3931
+ collectShortcuts(node.children ?? []);
3932
+ }
3933
+ };
3934
+ collectShortcuts(navigation);
3935
+ const hasDropdown = navigation.some((node) => node.presentation === "dropdown");
3936
+ const externalNavigation = shortcuts.length || hasDropdown ? { navigation: {
3937
+ ...shortcuts.length ? { shortcuts } : {},
3938
+ ...hasDropdown ? { display: "dropdown" } : {}
3939
+ } } : {};
3601
3940
  const projection = localeProjection(checkpoint);
3602
3941
  const project = (nodes) => nodes.flatMap((node) => {
3603
3942
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
@@ -3618,7 +3957,7 @@ function navigationConfig(checkpoint, records) {
3618
3957
  return pages2.length || tab.url ? [{ tab: tab.label, ...pages2.length ? { pages: pages2 } : {}, ...tab.url ? { href: tab.url } : {} }] : [];
3619
3958
  });
3620
3959
  const primary = localizedTabs(projection.defaultLocale);
3621
- return { tabs: primary.length ? primary : localizedTabs(locales[0]), ...locales.length > 1 ? {
3960
+ return { ...externalNavigation, tabs: primary.length ? primary : localizedTabs(locales[0]), ...locales.length > 1 ? {
3622
3961
  i18n: {
3623
3962
  defaultLocale: projection.defaultLocale,
3624
3963
  locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })),
@@ -3627,7 +3966,7 @@ function navigationConfig(checkpoint, records) {
3627
3966
  } : {} };
3628
3967
  }
3629
3968
  const pages = project(navigation);
3630
- return pages.length ? { tabs: [{ tab: "Documentation", pages }] } : null;
3969
+ return pages.length || shortcuts.length ? { ...externalNavigation, tabs: [{ tab: "Documentation", pages }] } : null;
3631
3970
  }
3632
3971
  function canonicalLanguage(value) {
3633
3972
  if (!/^[a-z]{2,3}(?:-[a-z0-9]{2,8}){0,4}$/i.test(value)) return void 0;
@@ -3647,7 +3986,7 @@ function observedLocale(document, id, scopePath) {
3647
3986
  }
3648
3987
  const urlLocale = [id.split("/", 1)[0], ...scopePath.split("/"), document.finalUrl.pathname.split("/").filter(Boolean)[0]].find((segment) => segment && LOCALES.has(segment.toLowerCase()));
3649
3988
  if (urlLocale) candidates.push({ source: "URL", value: canonicalLanguage(urlLocale) });
3650
- const declared = /html/i.test(document.contentType) ? load3(document.body)("html").attr("lang")?.trim() : void 0;
3989
+ const declared = /html/i.test(document.contentType) ? load5(document.body)("html").attr("lang")?.trim() : void 0;
3651
3990
  if (declared) {
3652
3991
  const locale = canonicalLanguage(declared);
3653
3992
  if (locale) candidates.push({ source: "html lang", value: locale });
@@ -3724,13 +4063,15 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3724
4063
  const projection = localeProjection(checkpoint);
3725
4064
  const primaryIds = new Set(checkpoint.records.flatMap((record) => record.page && (!projection.canonicalLocale(record.page) || projection.canonicalLocale(record.page) === projection.defaultLocale) ? [projection.navigationId(record.page)] : []));
3726
4065
  const outputIds = /* @__PURE__ */ new Set();
3727
- const resolve3 = (url) => {
4066
+ const resolve4 = (url) => {
3728
4067
  const record = records.get(canonical(url, checkpoint.sourceUrl) ?? "");
3729
4068
  return resolveMigrationRecord(records, record);
3730
4069
  };
3731
4070
  for (const record of checkpoint.records) {
3732
4071
  counts[record.outcome]++;
3733
4072
  issues.push(...record.issues ?? []);
4073
+ warnings.push(...record.warnings ?? []);
4074
+ if (record.outcome === "alias" && !resolveMigrationRecord(records, record)?.page) issues.push({ code: "missing-page", source: record.url, message: "An alias does not resolve to an imported source page." });
3734
4075
  if (record.outcome === "pending" || record.outcome === "failed") issues.push({ code: "missing-page", source: record.url, message: record.reason ?? `Inventory entry is ${record.outcome}.` });
3735
4076
  if (!record.page || !record.evidence) continue;
3736
4077
  const outputId = projection.pageId(record.page);
@@ -3749,8 +4090,9 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3749
4090
  continue;
3750
4091
  }
3751
4092
  if (!inScope(link, source, checkpoint.scopePath)) continue;
4093
+ if (isDiscoveryIndex(link, source)) continue;
3752
4094
  if (/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js|json|ya?ml)$/i.test(link.pathname)) continue;
3753
- const destination = resolve3(link.toString());
4095
+ const destination = resolve4(link.toString());
3754
4096
  if (!destination || !["imported", "excluded"].includes(destination.outcome)) {
3755
4097
  issues.push({ code: "missing-page", source: record.url, message: `Unresolved internal destination: ${link}` });
3756
4098
  } else if (link.hash && destination.outcome === "imported") {
@@ -3777,7 +4119,9 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3777
4119
  if (node.url) {
3778
4120
  const url = new URL(node.url, source);
3779
4121
  if (inScope(url, source, checkpoint.scopePath)) {
3780
- const page = resolve3(url.toString())?.page;
4122
+ const destination = resolve4(url.toString());
4123
+ if (destination?.outcome === "excluded" && destination.redirectTo) continue;
4124
+ const page = destination?.page;
3781
4125
  if (!page) issues.push({ code: "navigation-missing", source: node.url, message: `Source navigation target ${node.label} was not imported.` });
3782
4126
  else if (locale && locale !== projection.defaultLocale && !outputIds.has(`${locale}/${projection.navigationId(page)}`)) {
3783
4127
  issues.push({ code: "navigation-missing", source: node.url, message: `The ${locale} sidebar target ${node.label} has no authored localized runtime page.` });
@@ -3794,16 +4138,32 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3794
4138
  async function runUrlMigration(options) {
3795
4139
  const source = validateMigrationUrl(options.sourceUrl);
3796
4140
  const sourceUrl = canonical(source.toString(), source.toString());
3797
- const requestedScope = options.scopePath ?? (source.pathname.endsWith("/") ? source.pathname : source.pathname.slice(0, source.pathname.lastIndexOf("/")) || "/");
3798
- let scopeEnd = requestedScope.length;
3799
- while (scopeEnd > 0 && requestedScope.charCodeAt(scopeEnd - 1) === 47) scopeEnd--;
3800
- const scopePath = requestedScope.slice(0, scopeEnd) || "/";
3801
- if (!scopePath.startsWith("/") || scopePath.includes("?") || scopePath.includes("#") || !inScope(source, source, scopePath)) throw new Error("The selected documentation scope must contain the source URL.");
3802
4141
  const maxPages = options.maxPages ?? 1e3;
3803
4142
  if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > 1e4) throw new Error("maxPages must be between 1 and 10000.");
3804
4143
  const concurrency = Math.max(1, Math.min(32, Math.floor(options.concurrency ?? 5)));
3805
4144
  const batchSize = Math.max(1, Math.min(500, Math.floor(options.batchSize ?? 50)));
3806
4145
  if (!Number.isFinite(concurrency) || !Number.isFinite(batchSize) || options.maxCaptures !== void 0 && (!Number.isInteger(options.maxCaptures) || options.maxCaptures < 1) || options.maxTotalBytes !== void 0 && (!Number.isFinite(options.maxTotalBytes) || options.maxTotalBytes < 1)) throw new Error("Migration batch, concurrency and byte limits must be positive finite numbers.");
4146
+ const fetcher = options.fetcher ?? defaultMigrationFetcher;
4147
+ const acquire = options.captureProvider?.capture.bind(options.captureProvider) ?? (async ({ url }) => fetcher(url, { accept: ACCEPT }));
4148
+ let initial;
4149
+ let inferredScope;
4150
+ if (options.scopePath === void 0 && !options.checkpoint && !options.signal?.aborted) {
4151
+ initial = Promise.resolve().then(() => acquire({ url: source, signal: options.signal }));
4152
+ try {
4153
+ const document = await initial;
4154
+ if (document.finalUrl.origin === source.origin) {
4155
+ const inferred = inferUrlMigrationScope(document).scopePath;
4156
+ if (inScope(source, source, inferred)) inferredScope = inferred;
4157
+ }
4158
+ } catch {
4159
+ }
4160
+ }
4161
+ const capture = (request) => initial && canonical(request.url.href, sourceUrl) === sourceUrl ? initial : acquire(request);
4162
+ const requestedScope = options.scopePath ?? options.checkpoint?.scopePath ?? inferredScope ?? (source.pathname.endsWith("/") ? source.pathname : source.pathname.slice(0, source.pathname.lastIndexOf("/")) || "/");
4163
+ let scopeEnd = requestedScope.length;
4164
+ while (scopeEnd > 0 && requestedScope.charCodeAt(scopeEnd - 1) === 47) scopeEnd--;
4165
+ const scopePath = requestedScope.slice(0, scopeEnd) || "/";
4166
+ if (!scopePath.startsWith("/") || scopePath.includes("?") || scopePath.includes("#") || !inScope(source, source, scopePath)) throw new Error("The selected documentation scope must contain the source URL.");
3807
4167
  const captureVersion = options.captureVersion ?? "fetch-v1";
3808
4168
  if (options.checkpoint && options.checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
3809
4169
  const checkpoint = options.checkpoint ? cloneUrlMigrationCheckpoint(options.checkpoint) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
@@ -3822,6 +4182,8 @@ async function runUrlMigration(options) {
3822
4182
  delete record.apiSpecification;
3823
4183
  delete record.version;
3824
4184
  delete record.issues;
4185
+ delete record.warnings;
4186
+ delete record.redirectTo;
3825
4187
  delete record.reason;
3826
4188
  delete record.aliasOf;
3827
4189
  }
@@ -3834,6 +4196,7 @@ async function runUrlMigration(options) {
3834
4196
  const enqueue = (value, base, discoveredBy) => {
3835
4197
  const url = canonical(value, base);
3836
4198
  if (!url || !inScope(new URL(url), source, scopePath) && !isDiscoveryIndex(new URL(url), source)) return;
4199
+ const isAttachment = discoveredBy !== "submitted" && /\.(?:json|ya?ml)$/i.test(new URL(url).pathname);
3837
4200
  const existing = records.get(url);
3838
4201
  if (existing) {
3839
4202
  if (!existing.discoveredBy.includes(discoveredBy)) {
@@ -3843,10 +4206,15 @@ async function runUrlMigration(options) {
3843
4206
  return;
3844
4207
  }
3845
4208
  if (records.size >= MAX_RECORDS) throw new Error("Discovered URL inventory exceeded the 100000 URL safety bound.");
3846
- const record = { url, discoveredBy: [discoveredBy], outcome: "pending" };
4209
+ const record = {
4210
+ url,
4211
+ discoveredBy: [discoveredBy],
4212
+ outcome: isAttachment ? "excluded" : "pending",
4213
+ ...isAttachment ? { reason: "Machine-readable attachment, not an authored article." } : {}
4214
+ };
3847
4215
  records.set(url, record);
3848
4216
  checkpoint.records.push(record);
3849
- pending.push(record);
4217
+ if (!isAttachment) pending.push(record);
3850
4218
  changed.push(record);
3851
4219
  };
3852
4220
  enqueue(sourceUrl, sourceUrl, "submitted");
@@ -3879,8 +4247,6 @@ async function runUrlMigration(options) {
3879
4247
  }
3880
4248
  }
3881
4249
  enqueueNavigation(checkpoint.navigation, sourceUrl);
3882
- const fetcher = options.fetcher ?? defaultMigrationFetcher;
3883
- const capture = options.captureProvider?.capture.bind(options.captureProvider) ?? (async ({ url }) => fetcher(url, { accept: ACCEPT }));
3884
4250
  const assetFetcher = options.assetFetcher ?? (!options.fetcher && !options.captureProvider ? defaultAssetFetcher : void 0);
3885
4251
  const usedIds = new Map([...records.values()].filter((record) => record.page).map((record) => [record.page.id, record.url]));
3886
4252
  const finalUrls = new Map([...records.values()].filter((record) => record.evidence && record.page).map((record) => [canonical(record.evidence.finalUrl, sourceUrl), record.url]));
@@ -3888,6 +4254,10 @@ async function runUrlMigration(options) {
3888
4254
  const assetPayloads = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.sha256, asset])));
3889
4255
  const assetBytes = /* @__PURE__ */ new Map();
3890
4256
  const retainAsset = (asset) => {
4257
+ if (asset.unavailable) {
4258
+ if ([404, 410].includes(asset.unavailable.status) && asset.content.byteLength === 0) return asset;
4259
+ throw new Error("Invalid source asset availability evidence.");
4260
+ }
3891
4261
  if (!acceptedAsset(asset)) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
3892
4262
  const sha256 = hash(asset.content);
3893
4263
  const content = assetBytes.get(sha256) ?? Buffer.from(asset.content);
@@ -3920,6 +4290,11 @@ async function runUrlMigration(options) {
3920
4290
  let markdownRepresentation;
3921
4291
  try {
3922
4292
  delete record.reason;
4293
+ if (/\/llms-full\.txt$/i.test(new URL(record.url).pathname)) {
4294
+ record.outcome = "excluded";
4295
+ record.reason = "Aggregate documentation corpus; individual articles are inventoried separately.";
4296
+ return;
4297
+ }
3923
4298
  delete record.apiSpecification;
3924
4299
  const represented = resolveMigrationRecord(records, record.representationOf ? records.get(record.representationOf) : void 0);
3925
4300
  if (represented?.outcome === "imported") {
@@ -3929,6 +4304,16 @@ async function runUrlMigration(options) {
3929
4304
  }
3930
4305
  const document = await capture({ url: new URL(record.url), signal: options.signal });
3931
4306
  if (options.signal?.aborted) return;
4307
+ if (document.status && document.status >= 300 && document.status < 400 && document.headers?.location) {
4308
+ const destination = canonical(document.headers.location, document.finalUrl.href);
4309
+ if (destination && new URL(destination).origin !== source.origin) {
4310
+ record.outcome = "excluded";
4311
+ record.redirectTo = new URL(document.headers.location, document.finalUrl).href;
4312
+ record.reason = "The source entry is a redirect to an external public resource, retained as a link.";
4313
+ return;
4314
+ }
4315
+ throw new Error("Capture returned an unresolved redirect.");
4316
+ }
3932
4317
  if (Buffer.byteLength(document.body) > MAX_RESPONSE_BYTES) throw new Error("Captured response exceeds the 8 MB safety bound.");
3933
4318
  const identity = articleIdentity(document);
3934
4319
  const final = canonical(identity.toString(), sourceUrl);
@@ -3936,6 +4321,7 @@ async function runUrlMigration(options) {
3936
4321
  if (!inScope(document.finalUrl, source, scopePath) && !(role === "index" && isDiscoveryIndex(document.finalUrl, source))) {
3937
4322
  record.outcome = "excluded";
3938
4323
  record.reason = "Redirected outside the selected origin/path.";
4324
+ if (document.finalUrl.origin === source.origin && (!document.status || document.status >= 200 && document.status < 300)) record.redirectTo = document.finalUrl.href;
3939
4325
  return;
3940
4326
  }
3941
4327
  if (document.status && document.status >= 400) throw new Error(`Source page returned HTTP ${document.status}.`);
@@ -3953,7 +4339,7 @@ async function runUrlMigration(options) {
3953
4339
  return;
3954
4340
  }
3955
4341
  if (/html/i.test(document.contentType)) {
3956
- const $ = load3(document.body);
4342
+ const $ = load5(document.body);
3957
4343
  $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
3958
4344
  const alternate = canonical($(node).attr("href"), final);
3959
4345
  if (!alternate) return;
@@ -3967,7 +4353,7 @@ async function runUrlMigration(options) {
3967
4353
  changed.push(sibling);
3968
4354
  });
3969
4355
  }
3970
- const observedNavigation = sourceNavigation(document);
4356
+ const observedNavigation = sourceNavigation(document, scopePath);
3971
4357
  if (observedNavigation.length && !(checkpoint.navigation.some((node) => node.kind === "tab") && !observedNavigation.some((node) => node.kind === "tab"))) {
3972
4358
  const merged = mergeSourceNavigation(checkpoint.navigation, observedNavigation);
3973
4359
  if (JSON.stringify(merged) !== JSON.stringify(checkpoint.navigation)) {
@@ -4009,6 +4395,7 @@ async function runUrlMigration(options) {
4009
4395
  record.reason = "Content could not be represented as safe inert MDX.";
4010
4396
  return;
4011
4397
  }
4398
+ let coveredSchemaGroups = [];
4012
4399
  const hasSchemaChoice = /html/i.test(document.contentType) && migrationHtmlArticle(document.body).root.find('select[aria-label="Select schema type"],select[aria-label="\u9009\u62E9\u67B6\u6784\u7C7B\u578B"]').length > 0;
4013
4400
  if (/html/i.test(document.contentType) && markdownRepresentation && (unresolvedStaticControls(document.body) || hasSchemaChoice)) {
4014
4401
  const schemaCapture = await capture({ url: new URL(markdownRepresentation), signal: options.signal });
@@ -4024,6 +4411,7 @@ async function runUrlMigration(options) {
4024
4411
  const path = `openapi/migration-${sha256}.json`;
4025
4412
  record.apiSpecification = { path, sha256, document: fragment.document };
4026
4413
  page.openapi = `${path} ${fragment.method} ${fragment.path}`;
4414
+ coveredSchemaGroups = apiSchemaTabGroups(document.body, fragment.document, fragment);
4027
4415
  }
4028
4416
  }
4029
4417
  const states = document.states ?? [];
@@ -4053,17 +4441,24 @@ ${difference}`;
4053
4441
  }
4054
4442
  record.assets = [];
4055
4443
  record.issues = [];
4444
+ record.warnings = [];
4056
4445
  if (/html/i.test(document.contentType)) {
4057
4446
  const { $, root } = migrationHtmlArticle(document.body);
4058
4447
  root.find("nav,footer,aside,.theme-doc-breadcrumbs,.theme-doc-toc-mobile,.table-of-contents,.hash-link").remove();
4059
4448
  root.find('button svg,a.hash-link svg,[aria-hidden="true"] svg,svg[aria-hidden="true"],svg[data-lucide],svg[data-icon]').remove();
4449
+ root.find("video,iframe").each((_index, node) => {
4450
+ if (projectMigrationMedia($.html(node), document.finalUrl) !== null) {
4451
+ record.warnings.push({ code: "source-asset-warning", source: record.url, message: "Embedded video is preserved at its original public media URL; playback depends on the original host." });
4452
+ $(node).remove();
4453
+ }
4454
+ });
4060
4455
  if (record.apiSpecification) {
4061
4456
  root.find('select[aria-label="Select schema type"],select[aria-label="\u9009\u62E9\u67B6\u6784\u7C7B\u578B"]').remove();
4062
4457
  root.find('.option-dropdown svg[data-component-part="icon-svg"]').remove();
4063
4458
  }
4064
4459
  const unsupported = [...new Set(root.find("iframe,object,embed,canvas,svg,form,input,textarea,select,math,video,audio").toArray().map((node) => node.tagName))];
4065
4460
  if (unsupported.length) record.issues.push({ code: "unsupported-component", source: record.url, message: `Rendered content requires explicit review for: ${unsupported.join(", ")}.` });
4066
- if (!states.length && unresolvedStaticControls(document.body, Boolean(record.apiSpecification))) record.issues.push({ code: "unsupported-component", source: record.url, message: "An interactive alternative has no complete captured panel or disclosure content." });
4461
+ if (!states.length && unresolvedStaticControls(document.body, Boolean(record.apiSpecification), coveredSchemaGroups)) record.issues.push({ code: "unsupported-component", source: record.url, message: "An interactive alternative has no complete captured panel or disclosure content." });
4067
4462
  }
4068
4463
  const suppliedAssets = new Map((document.assets ?? []).map((asset) => [canonical(asset.url, final), asset]));
4069
4464
  const assetUrls = [...new Set([...assetReferences(document), ...states.flatMap((state) => assetReferences(state.response))].map((url) => canonical(url, final)).filter((url) => Boolean(url)))];
@@ -4077,6 +4472,11 @@ ${difference}`;
4077
4472
  const supplied = suppliedAssets.get(url);
4078
4473
  const asset = supplied ? retainAsset(supplied) : assetRequest ? await assetRequest : null;
4079
4474
  if (!asset) throw new Error("Capture host did not supply asset bytes or a binary fetch boundary.");
4475
+ if (asset.unavailable && [404, 410].includes(asset.unavailable.status) && asset.content.byteLength === 0) {
4476
+ record.warnings.push({ code: "asset-failed", source: record.url, message: `The original source asset is unavailable (HTTP ${asset.unavailable.status}); its original URL was retained: ${url}` });
4477
+ page.body = rewriteMarkdownDestinations(page.body, (destination) => canonical(destination, final) === url ? url : destination);
4478
+ continue;
4479
+ }
4080
4480
  const extension = acceptedAsset(asset);
4081
4481
  if (!extension) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
4082
4482
  const sha256 = hash(asset.content);
@@ -4118,7 +4518,7 @@ ${difference}`;
4118
4518
  } catch (error) {
4119
4519
  if (options.signal?.aborted) return;
4120
4520
  const wasImported = record.outcome === "imported";
4121
- record.outcome = record.discoveredBy.every((origin) => origin === "discovery-index") ? "excluded" : "failed";
4521
+ record.outcome = isDiscoveryIndex(new URL(record.url), source) || record.discoveredBy.every((origin) => origin === "discovery-index") ? "excluded" : "failed";
4122
4522
  record.reason = error instanceof Error ? error.message : String(error);
4123
4523
  if (record.page) {
4124
4524
  if (wasImported) imported--;
@@ -4143,7 +4543,7 @@ ${difference}`;
4143
4543
  for (const record of records.values()) {
4144
4544
  if (!record.representationOf) continue;
4145
4545
  const primary = resolveMigrationRecord(records, records.get(record.representationOf));
4146
- if (!primary) continue;
4546
+ if (!primary || primary === record) continue;
4147
4547
  const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
4148
4548
  if (!winner) continue;
4149
4549
  const alternate = winner === primary ? record : primary;
@@ -4269,7 +4669,7 @@ function rewriteMarkdownDestinations(body, rewrite) {
4269
4669
  return { start: start + index, end: start + index + node.url.length, replacement };
4270
4670
  }).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
4271
4671
  for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
4272
- if (!attribute.name || !["src", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
4672
+ if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
4273
4673
  let value = attribute.value;
4274
4674
  if (typeof value !== "string") {
4275
4675
  try {
@@ -4300,7 +4700,7 @@ function rewriteMarkdownDestinations(body, rewrite) {
4300
4700
  // src/components.ts
4301
4701
  import { createHash as createHash2 } from "crypto";
4302
4702
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
4303
- import { basename, dirname as dirname2, extname as extname3, posix as posix2, relative as relative3, resolve as resolve2 } from "path";
4703
+ import { basename, dirname as dirname2, extname as extname3, posix as posix2, relative as relative3, resolve as resolve3 } from "path";
4304
4704
  import postcss from "postcss";
4305
4705
  import selectorParser from "postcss-selector-parser";
4306
4706
  import remarkMdx4 from "remark-mdx";
@@ -4522,6 +4922,7 @@ function renderMigrationFiles(bundle, options = {}) {
4522
4922
  }
4523
4923
  export {
4524
4924
  classifyMigrationResponse,
4925
+ inferUrlMigrationScope,
4525
4926
  renderMigrationFiles,
4526
4927
  runUrlMigration,
4527
4928
  validateUrlMigration