@thallylabs/migrate 0.2.29 → 0.2.31

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 load2 } from "cheerio";
3
+ import { load as load3 } from "cheerio";
4
4
  import remarkParse3 from "remark-parse";
5
5
  import remarkMdx3 from "remark-mdx";
6
6
  import { unified as unified3 } from "unified";
@@ -390,20 +390,20 @@ function utf8SvgText(content) {
390
390
  if (body[0] === 254 && body[1] === 255 || body[0] === 255 && body[1] === 254 || body[0] === 0 && body[1] === 0 || body.includes(0)) {
391
391
  return null;
392
392
  }
393
- const text = body.toString("utf8");
394
- if (text.includes("\uFFFD")) return null;
395
- const declaration = /^<\?xml\b[^>]*\?>/iu.exec(text);
393
+ const text2 = body.toString("utf8");
394
+ if (text2.includes("\uFFFD")) return null;
395
+ const declaration = /^<\?xml\b[^>]*\?>/iu.exec(text2);
396
396
  if (declaration) {
397
397
  const encoding = /\bencoding\s*=\s*["']([^"']*)["']/iu.exec(declaration[0]);
398
398
  if (encoding && !/^utf-?8$/iu.test(encoding[1].trim())) return null;
399
- } else if (/<\?xml\b/iu.test(text)) {
399
+ } else if (/<\?xml\b/iu.test(text2)) {
400
400
  return null;
401
401
  }
402
- return text;
402
+ return text2;
403
403
  }
404
404
  function isPassiveSvg(content) {
405
- const text = utf8SvgText(Buffer.from(content.buffer, content.byteOffset, content.byteLength));
406
- if (text === null) return false;
405
+ const text2 = utf8SvgText(Buffer.from(content.buffer, content.byteOffset, content.byteLength));
406
+ if (text2 === null) return false;
407
407
  let isPassive = true;
408
408
  let depth = 0;
409
409
  let sawRoot = false;
@@ -455,7 +455,7 @@ function isPassiveSvg(content) {
455
455
  { xmlMode: true, decodeEntities: true, recognizeSelfClosing: true }
456
456
  );
457
457
  try {
458
- parser3.write(text);
458
+ parser3.write(text2);
459
459
  parser3.end();
460
460
  } catch {
461
461
  return false;
@@ -530,6 +530,274 @@ function cloneUrlMigrationCheckpoint(checkpoint) {
530
530
  return clone(checkpoint);
531
531
  }
532
532
 
533
+ // src/url-navigation.ts
534
+ import { load } from "cheerio";
535
+ var MAX_CAPTURE_BYTES = 8e6;
536
+ var MAX_JSON_BYTES = 2e6;
537
+ var MAX_NODES = 1e4;
538
+ var MAX_DEPTH = 32;
539
+ var MAX_STRING_LENGTH = 8192;
540
+ function object(value) {
541
+ return value !== null && typeof value === "object" && !Array.isArray(value);
542
+ }
543
+ function text(value) {
544
+ return typeof value === "string" && value.length <= MAX_STRING_LENGTH && value.trim() ? value.trim() : void 0;
545
+ }
546
+ function target(value, base) {
547
+ const reference = text(value);
548
+ if (!reference) return void 0;
549
+ try {
550
+ const url = new URL(reference, base);
551
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password ? url.toString() : void 0;
552
+ } catch {
553
+ return void 0;
554
+ }
555
+ }
556
+ function readJson(value) {
557
+ if (Buffer.byteLength(value) > MAX_JSON_BYTES) return void 0;
558
+ let depth = 0;
559
+ let quoted = false;
560
+ let escaped = false;
561
+ for (const char of value) {
562
+ if (quoted) {
563
+ if (escaped) escaped = false;
564
+ else if (char === "\\") escaped = true;
565
+ else if (char === '"') quoted = false;
566
+ } else if (char === '"') quoted = true;
567
+ else if (char === "[" || char === "{") {
568
+ if (++depth > 96) return void 0;
569
+ } else if (char === "]" || char === "}") depth--;
570
+ }
571
+ try {
572
+ return JSON.parse(value);
573
+ } catch {
574
+ return void 0;
575
+ }
576
+ }
577
+ function flightRecords(stream) {
578
+ const bytes = Buffer.from(stream);
579
+ const records = [];
580
+ let cursor = 0;
581
+ while (cursor < bytes.length) {
582
+ const colon = bytes.indexOf(58, cursor);
583
+ if (colon < 0 || colon - cursor > 16 || !/^[a-f\d]*$/i.test(bytes.toString("utf8", cursor, colon))) break;
584
+ cursor = colon + 1;
585
+ if (bytes[cursor] === 84) {
586
+ const comma = bytes.indexOf(44, cursor + 1);
587
+ const length = comma < 0 ? "" : bytes.toString("utf8", cursor + 1, comma);
588
+ if (!/^[a-f\d]{1,8}$/i.test(length)) break;
589
+ const end2 = comma + 1 + Number.parseInt(length, 16);
590
+ if (end2 > bytes.length) break;
591
+ cursor = end2;
592
+ continue;
593
+ }
594
+ const newline = bytes.indexOf(10, cursor);
595
+ const end = newline < 0 ? bytes.length : newline;
596
+ if (bytes[cursor] === 91 || bytes[cursor] === 123) records.push(readJson(bytes.toString("utf8", cursor, end)));
597
+ cursor = end + 1;
598
+ }
599
+ return records;
600
+ }
601
+ function mintlifyNavigation(body, base, locale) {
602
+ const $ = load(body);
603
+ let stream = "";
604
+ const records = [];
605
+ $("script").each((_index, script) => {
606
+ const source = $(script).text();
607
+ const call = /^\s*self\.__next_f\.push\(\s*(\[[\s\S]*\])\s*\)\s*;?\s*$/.exec(source);
608
+ if (call) {
609
+ const value = readJson(call[1]);
610
+ if (Array.isArray(value) && value[0] === 1 && typeof value[1] === "string") stream += value[1];
611
+ } else if ($(script).attr("type") === "application/json") {
612
+ records.push(readJson(source));
613
+ }
614
+ });
615
+ records.push(...flightRecords(stream));
616
+ const candidates = [];
617
+ let visited = 0;
618
+ const visit = (value, depth = 0) => {
619
+ if (++visited > MAX_NODES * 5 || depth > 96) return;
620
+ if (Array.isArray(value)) {
621
+ for (const child of value) visit(child, depth + 1);
622
+ } else if (object(value)) {
623
+ for (const key of ["scopedNav", "docsNavWithMetadata", "anonymizedNav"]) {
624
+ if (object(value[key])) candidates.push({ value: value[key], rank: key === "scopedNav" ? 3 : 2 });
625
+ }
626
+ if (object(value.docsConfig) && object(value.docsConfig.navigation)) {
627
+ candidates.push({ value: value.docsConfig.navigation, rank: 1 });
628
+ }
629
+ for (const child of Object.values(value)) visit(child, depth + 1);
630
+ }
631
+ };
632
+ records.forEach((record) => visit(record));
633
+ const language = locale ?? text($("html").attr("lang"));
634
+ for (const candidate of candidates.sort((a, b) => b.rank - a.rank)) {
635
+ let count = 0;
636
+ const walk = (value, depth = 0, activeLocale) => {
637
+ if (depth > MAX_DEPTH || ++count > MAX_NODES) throw new Error("Source navigation exceeds its safety bound.");
638
+ if (typeof value === "string") {
639
+ const url2 = target(value, new URL("/", base));
640
+ return url2 ? [{ kind: "page", label: value, url: url2 }] : [];
641
+ }
642
+ if (Array.isArray(value)) return value.flatMap((child) => walk(child, depth + 1, activeLocale));
643
+ if (!object(value)) return [];
644
+ if (Array.isArray(value.languages)) {
645
+ const languages = value.languages.filter(object);
646
+ const current = new URL(base);
647
+ current.hash = "";
648
+ const routeScore = (item) => {
649
+ let visited2 = 0;
650
+ let matches = false;
651
+ let populated = 0;
652
+ const scan = (node, depth2 = 0) => {
653
+ if (++visited2 > MAX_NODES || depth2 > MAX_DEPTH) return;
654
+ if (Array.isArray(node)) {
655
+ for (const child of node) scan(child, depth2 + 1);
656
+ return;
657
+ }
658
+ if (!object(node)) return;
659
+ const href = target(node.href, base);
660
+ if (href) {
661
+ populated++;
662
+ const url2 = new URL(href);
663
+ url2.hash = "";
664
+ if (url2.toString() === current.toString()) matches = true;
665
+ }
666
+ for (const key of ["tabs", "groups", "pages", "dropdowns", "anchors", "versions"]) {
667
+ if (Array.isArray(node[key])) scan(node[key], depth2 + 1);
668
+ }
669
+ };
670
+ scan(item);
671
+ return matches ? populated : 0;
672
+ };
673
+ const matching = languages.map((item) => ({ item, score: routeScore(item) })).filter((candidate2) => candidate2.score > 0).sort((a, b) => b.score - a.score);
674
+ 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
+ return selected ? walk({ ...selected, languages: void 0 }, depth + 1, text(selected.language)) : [];
676
+ }
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";
679
+ 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);
681
+ if (!label) return children;
682
+ if (!url && !children.length) return [];
683
+ return [{
684
+ kind,
685
+ label,
686
+ ...url ? { url } : {},
687
+ ...children.length ? { children } : {},
688
+ ...kind === "tab" && activeLocale ? { locale: activeLocale } : {}
689
+ }];
690
+ };
691
+ try {
692
+ const nodes = walk(candidate.value);
693
+ if (nodes.length) return nodes;
694
+ } catch {
695
+ }
696
+ }
697
+ return [];
698
+ }
699
+ function resolveNavigation(nodes, base) {
700
+ let count = 0;
701
+ const walk = (items, depth = 0) => {
702
+ if (depth > MAX_DEPTH || !Array.isArray(items)) throw new Error("Invalid source navigation.");
703
+ return items.flatMap((node) => {
704
+ if (++count > MAX_NODES || !object(node)) throw new Error("Source navigation exceeds its safety bound.");
705
+ const label = text(node.label);
706
+ if (!label) return [];
707
+ const url = target(node.url, base);
708
+ const children = node.children ? walk(node.children, depth + 1) : [];
709
+ return [{
710
+ label,
711
+ ...url ? { url } : {},
712
+ ...children.length ? { children } : {},
713
+ ...["tab", "group", "page"].includes(node.kind ?? "") ? { kind: node.kind } : {},
714
+ ...text(node.locale) ? { locale: text(node.locale) } : {}
715
+ }];
716
+ });
717
+ };
718
+ return walk(nodes);
719
+ }
720
+ function extractSourceNavigation(capture) {
721
+ if (capture.navigation) return resolveNavigation(capture.navigation, capture.finalUrl);
722
+ if (!/html/i.test(capture.contentType) || Buffer.byteLength(capture.body) > MAX_CAPTURE_BYTES) return [];
723
+ const mintlify = mintlifyNavigation(capture.body, capture.finalUrl, capture.locale);
724
+ if (mintlify.length) return mintlify;
725
+ const $ = load(capture.body);
726
+ const candidates = ['.theme-doc-sidebar-menu,[aria-label*="sidebar" i]', "aside", '[role="navigation"],nav'];
727
+ 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
+ if (!root) return [];
729
+ let count = 0;
730
+ const walk = (element, excluded, depth = 0) => {
731
+ if (depth > MAX_DEPTH) throw new Error("Source navigation exceeds its safety bound.");
732
+ const result = [];
733
+ $(element).children().each((_index, node) => {
734
+ if (++count > MAX_NODES) throw new Error("Source navigation exceeds its safety bound.");
735
+ if (node === excluded) return;
736
+ const item = $(node);
737
+ if (item.is("a[href]")) {
738
+ const label = item.text().replace(/\s+/g, " ").trim();
739
+ const url = target(item.attr("href"), capture.finalUrl);
740
+ if (label && url) result.push({ label, url });
741
+ } else if (item.is("li,details") || item.attr("data-migration-group") !== void 0 || item.children("h2,h3,h4,h5,h6").length > 0) {
742
+ const directLink = item.children("a[href]").first();
743
+ const direct = directLink.length ? directLink : item.children("div").children("a[href]").first();
744
+ const label = direct.text().trim() || item.children("summary,button,span,h2,h3,h4,h5,h6").first().text().trim();
745
+ const children = walk(node, direct[0], depth + 1);
746
+ const url = target(direct.attr("href"), capture.finalUrl);
747
+ if (label && (url || children.length)) result.push({ label, ...url ? { url } : {}, ...children.length ? { children } : {} });
748
+ else result.push(...children);
749
+ } else result.push(...walk(node, excluded, depth + 1));
750
+ });
751
+ return result;
752
+ };
753
+ try {
754
+ return walk(root);
755
+ } catch {
756
+ return [];
757
+ }
758
+ }
759
+ function placeholder(node) {
760
+ const children = node.children ?? [];
761
+ return node.kind === "tab" && children.length === 1 && children[0].kind === "page" && children[0].label === node.label && !children[0].children?.length;
762
+ }
763
+ function mergeSourceNavigation(existing, incoming) {
764
+ let count = 0;
765
+ const merge = (left, right, depth = 0) => {
766
+ if (depth > MAX_DEPTH || (count += left.length + right.length) > MAX_NODES * 4) throw new Error("Source navigation exceeds its safety bound.");
767
+ const result = left.map((node) => ({ ...node }));
768
+ const identity = (node) => JSON.stringify([
769
+ node.kind,
770
+ node.label,
771
+ node.locale,
772
+ node.kind === "tab" || node.kind === "group" ? void 0 : node.url
773
+ ]);
774
+ const positions = /* @__PURE__ */ new Map();
775
+ result.forEach((node, index) => {
776
+ const key = identity(node);
777
+ const siblings = positions.get(key) ?? [];
778
+ siblings.push(index);
779
+ positions.set(key, siblings);
780
+ });
781
+ const offsets = /* @__PURE__ */ new Map();
782
+ for (const next of right) {
783
+ const key = identity(next);
784
+ const offset = offsets.get(key) ?? 0;
785
+ const index = positions.get(key)?.[offset];
786
+ offsets.set(key, offset + 1);
787
+ if (index === void 0) {
788
+ result.push({ ...next });
789
+ continue;
790
+ }
791
+ const previous = result[index];
792
+ if (placeholder(previous) && !placeholder(next)) result[index] = { ...next };
793
+ else if (!placeholder(previous) && placeholder(next)) continue;
794
+ else result[index] = { ...previous, ...next, ...previous.children || next.children ? { children: merge(previous.children ?? [], next.children ?? [], depth + 1) } : {} };
795
+ }
796
+ return result;
797
+ };
798
+ return merge(existing, incoming);
799
+ }
800
+
533
801
  // src/url-images.ts
534
802
  function migrationImageUrl(value, base) {
535
803
  const url = new URL(value, base);
@@ -569,7 +837,7 @@ function migrationImageAttribute(value) {
569
837
  }
570
838
 
571
839
  // src/url.ts
572
- import { load } from "cheerio";
840
+ import { load as load2 } from "cheerio";
573
841
  import remarkMdx2 from "remark-mdx";
574
842
  import remarkParse2 from "remark-parse";
575
843
  import TurndownService from "turndown";
@@ -845,9 +1113,9 @@ function collectRefs(root) {
845
1113
  return;
846
1114
  }
847
1115
  if (ts.isAwaitExpression(node) && !inAsync) awaitOutsideAsync = true;
848
- const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment ? node.left : (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) ? node.operand : void 0;
849
- if (target) {
850
- let base = target;
1116
+ const target2 = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment ? node.left : (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) ? node.operand : void 0;
1117
+ if (target2) {
1118
+ let base = target2;
851
1119
  while (ts.isPropertyAccessExpression(base) || ts.isElementAccessExpression(base)) base = base.expression;
852
1120
  if (ts.isIdentifier(base) && !bound(base.text, scopes)) assigned.push(base.text);
853
1121
  }
@@ -968,8 +1236,8 @@ function pageScopeNames(esmSources) {
968
1236
  for (const statement of parse(source).statements) {
969
1237
  for (const local of importLocals(statement.getText())) names.add(local);
970
1238
  if (ts.isImportDeclaration(statement)) continue;
971
- const target = ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ? void 0 : statement;
972
- if (target) for (const name of declaredNames(target).names) names.add(name);
1239
+ const target2 = ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ? void 0 : statement;
1240
+ if (target2) for (const name of declaredNames(target2).names) names.add(name);
973
1241
  }
974
1242
  }
975
1243
  return names;
@@ -1265,14 +1533,14 @@ ${trimmed.slice(0, marker).trimEnd()}`;
1265
1533
  function headingIdMarker(id) {
1266
1534
  return `{/* #${id} */}`;
1267
1535
  }
1268
- function mintlifyHeadingSlug(text) {
1269
- return text.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, "");
1536
+ function mintlifyHeadingSlug(text2) {
1537
+ return text2.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, "");
1270
1538
  }
1271
- function thallyHeadingSlug(text) {
1272
- return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
1539
+ function thallyHeadingSlug(text2) {
1540
+ return text2.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
1273
1541
  }
1274
- function typesetQuotes(text) {
1275
- return text.split(/(`[^`]*`|<[^>]*>|\]\([^)]*\))/).map((part, index) => {
1542
+ function typesetQuotes(text2) {
1543
+ return text2.split(/(`[^`]*`|<[^>]*>|\]\([^)]*\))/).map((part, index) => {
1276
1544
  if (index % 2) return part;
1277
1545
  return part.replace(/(^|[\s([{\u2014\u2013])"/g, "$1\u201C").replace(/"/g, "\u201D").replace(/(^|[\s([{\u2014\u2013\u201c])'(?=\S)/g, "$1\u2018").replace(/'/g, "\u2019");
1278
1546
  }).join("");
@@ -1295,9 +1563,9 @@ function markMintlifyHeadings(body) {
1295
1563
  const explicit = heading[2].match(/^(.*?)([ \t]*\{\/\*[ \t]*#\S+[ \t]*\*\/\})$/);
1296
1564
  const source = typesetQuotes(explicit ? explicit[1] : heading[2]);
1297
1565
  if (explicit) return `${heading[1]}${source}${explicit[2]}`;
1298
- const text = plainHeadingText(source);
1299
- const id = mintlifyHeadingSlug(text);
1300
- return `${heading[1]}${source}${id && id !== thallyHeadingSlug(text) ? ` ${headingIdMarker(id)}` : ""}`;
1566
+ const text2 = plainHeadingText(source);
1567
+ const id = mintlifyHeadingSlug(text2);
1568
+ return `${heading[1]}${source}${id && id !== thallyHeadingSlug(text2) ? ` ${headingIdMarker(id)}` : ""}`;
1301
1569
  }).join("\n");
1302
1570
  }
1303
1571
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
@@ -1319,8 +1587,8 @@ function leadingPageHeading(body) {
1319
1587
  const root = descriptionParser.parse(body);
1320
1588
  const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html" && !(node.type === "mdxFlowExpression" && node.data?.estree?.body.length === 0));
1321
1589
  if (first?.type !== "heading" || first.depth !== 1) return void 0;
1322
- const text = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text).join("");
1323
- const title = text(first).replace(/\s+/g, " ").trim();
1590
+ const text2 = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text2).join("");
1591
+ const title = text2(first).replace(/\s+/g, " ").trim();
1324
1592
  const start = first.position?.start.offset;
1325
1593
  const end = first.position?.end.offset;
1326
1594
  return title && start !== void 0 && end !== void 0 ? { title, start, end } : void 0;
@@ -1349,8 +1617,8 @@ function splitFrontmatterBlock(raw) {
1349
1617
  function maskCode(body) {
1350
1618
  const source = body.replace(/\u0000/g, "");
1351
1619
  const blocks = [];
1352
- const stash = (text) => {
1353
- blocks.push(text);
1620
+ const stash = (text2) => {
1621
+ blocks.push(text2);
1354
1622
  return `\0${blocks.length - 1}\0`;
1355
1623
  };
1356
1624
  const lines = [];
@@ -1377,7 +1645,7 @@ function maskCode(body) {
1377
1645
  if (fenceBuffer.length) lines.push(stash(fenceBuffer.join("\n")));
1378
1646
  return {
1379
1647
  masked: lines.join("\n"),
1380
- unmask: (text) => text.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)]),
1648
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)]),
1381
1649
  openFence: codeFence
1382
1650
  };
1383
1651
  }
@@ -1385,8 +1653,8 @@ function replaceOutsideCode(body, transform) {
1385
1653
  const { masked, unmask } = maskCode(body);
1386
1654
  return unmask(transform(masked));
1387
1655
  }
1388
- function mdxComment(text) {
1389
- return `{/*${text.replace(/\*\//g, "* /")}*/}`;
1656
+ function mdxComment(text2) {
1657
+ return `{/*${text2.replace(/\*\//g, "* /")}*/}`;
1390
1658
  }
1391
1659
  function normalizeFernFileTrees(body) {
1392
1660
  return replaceOutsideCode(body, (whole) => whole.replace(
@@ -1793,12 +2061,12 @@ function maskCodeRegions(body) {
1793
2061
  }).join("\n");
1794
2062
  }
1795
2063
  var UNTERMINATED = "unterminated";
1796
- function readTagEnd(text, from) {
1797
- if (from < text.length && !/[\s/>]/.test(text[from])) return null;
2064
+ function readTagEnd(text2, from) {
2065
+ if (from < text2.length && !/[\s/>]/.test(text2[from])) return null;
1798
2066
  let depth = 0;
1799
2067
  let quote = "";
1800
- for (let i = from; i < text.length; i++) {
1801
- const char = text[i];
2068
+ for (let i = from; i < text2.length; i++) {
2069
+ const char = text2[i];
1802
2070
  if (quote) {
1803
2071
  if (char === "\\" && depth > 0) i++;
1804
2072
  else if (char === quote) quote = "";
@@ -1811,8 +2079,8 @@ function readTagEnd(text, from) {
1811
2079
  } else if (depth === 0) {
1812
2080
  if (char === "<") return null;
1813
2081
  if (char === ">") {
1814
- const selfClosing = text[i - 1] === "/" && i > from;
1815
- return { end: i + 1, selfClosing, attributes: text.slice(from, selfClosing ? i - 1 : i) };
2082
+ const selfClosing = text2[i - 1] === "/" && i > from;
2083
+ return { end: i + 1, selfClosing, attributes: text2.slice(from, selfClosing ? i - 1 : i) };
1816
2084
  }
1817
2085
  }
1818
2086
  }
@@ -1925,7 +2193,7 @@ function removeUndefinedExpressions(body, warn) {
1925
2193
  visit(tree);
1926
2194
  if (edits.length === 0) return body;
1927
2195
  warn?.(`Expressions that use ${[...removed].map((name) => `"${name}"`).join(", ")} were removed so the page builds: ${removed.size === 1 ? "the name is" : "the names are"} not defined on this page. Define ${removed.size === 1 ? "the name" : "the names"} or escape the braces as \\{ \\} to show the text.`);
1928
- return edits.sort((a, b) => b.start - a.start).reduce((text, edit) => text.slice(0, edit.start) + text.slice(edit.end), body);
2196
+ return edits.sort((a, b) => b.start - a.start).reduce((text2, edit) => text2.slice(0, edit.start) + text2.slice(edit.end), body);
1929
2197
  }
1930
2198
  function normalizeFlowParagraphContainers(body) {
1931
2199
  if (!body.includes("<p")) return body;
@@ -1951,7 +2219,7 @@ function normalizeFlowParagraphContainers(body) {
1951
2219
  for (const child of node.children ?? []) visit(child);
1952
2220
  }
1953
2221
  visit(tree);
1954
- return edits.sort((a, b) => b.offset - a.offset).reduce((text, edit) => text.slice(0, edit.offset) + edit.value + text.slice(edit.offset + 1), body);
2222
+ return edits.sort((a, b) => b.offset - a.offset).reduce((text2, edit) => text2.slice(0, edit.offset) + edit.value + text2.slice(edit.offset + 1), body);
1955
2223
  }
1956
2224
  function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1957
2225
  const runFern = platform === "fern";
@@ -2168,7 +2436,12 @@ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
2168
2436
  ]);
2169
2437
  var PORTABLE_URL_PROPS = "href|src|img|primaryHref|secondaryHref";
2170
2438
  var PORTABLE_URL_PROP_NAMES = new Set(PORTABLE_URL_PROPS.split("|"));
2439
+ var MINTLIFY_ARTICLE_LABELS = [
2440
+ { baseUrl: "Select base URL", permalink: "Navigate to header" },
2441
+ { baseUrl: "\u9009\u62E9\u57FA\u7840URL", permalink: "\u5BFC\u822A\u5230\u6807\u9898" }
2442
+ ];
2171
2443
  var remoteMarkdownParser = unified2().use(remarkParse2).use(remarkMdx2);
2444
+ var embeddedOpenApiParser = unified2().use(remarkParse2);
2172
2445
  function validateMigrationUrl(value) {
2173
2446
  let url;
2174
2447
  try {
@@ -2301,7 +2574,7 @@ function isSafePortableUrl(value) {
2301
2574
  }
2302
2575
  }
2303
2576
  function decodeHtmlAttribute(value) {
2304
- const document = load(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
2577
+ const document = load2(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
2305
2578
  return document("i").attr("data-thally-value") ?? "";
2306
2579
  }
2307
2580
  function escapeMarkdownText(value, escapeTableSeparator = false) {
@@ -2318,6 +2591,43 @@ function escapeMarkdownText(value, escapeTableSeparator = false) {
2318
2591
  }
2319
2592
  return escaped;
2320
2593
  }
2594
+ function portableMarkdownTableCell(value) {
2595
+ let output = "";
2596
+ let hasOddBackslashes = false;
2597
+ for (let index = 0; index < value.length; index += 1) {
2598
+ const character = value[index];
2599
+ if (character === "\\") {
2600
+ output += character;
2601
+ hasOddBackslashes = !hasOddBackslashes;
2602
+ continue;
2603
+ }
2604
+ if (character === "|" && !hasOddBackslashes) output += "\\";
2605
+ if (character === "\r" || character === "\n") {
2606
+ output += " ";
2607
+ if (character === "\r" && value[index + 1] === "\n") index += 1;
2608
+ } else output += character;
2609
+ hasOddBackslashes = false;
2610
+ }
2611
+ return output.trim();
2612
+ }
2613
+ function needsLiteralTableCode(value) {
2614
+ let hasOddBackslashes = false;
2615
+ for (const character of value) {
2616
+ if (character === "\\") hasOddBackslashes = !hasOddBackslashes;
2617
+ else {
2618
+ if (character === "|" && hasOddBackslashes) return true;
2619
+ hasOddBackslashes = false;
2620
+ }
2621
+ }
2622
+ return false;
2623
+ }
2624
+ function literalMdxCodeText(value) {
2625
+ let output = "";
2626
+ for (const character of value) {
2627
+ output += /[A-Za-z0-9 ]/.test(character) ? character : `&#${character.codePointAt(0)};`;
2628
+ }
2629
+ return output;
2630
+ }
2321
2631
  function markdownLinkDestination(value) {
2322
2632
  try {
2323
2633
  return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/&/g, "&amp;");
@@ -2350,7 +2660,7 @@ function replacePortableHtmlImages(value) {
2350
2660
  if (start < cursor) continue;
2351
2661
  const end = htmlOpeningTagEnd(value, start + match[0].length);
2352
2662
  if (end === null) continue;
2353
- const fragment = load(value.slice(start, end));
2663
+ const fragment = load2(value.slice(start, end));
2354
2664
  const image = fragment("img").first();
2355
2665
  const source = image.attr("src");
2356
2666
  const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
@@ -2375,7 +2685,7 @@ function replacePortableHtmlAnchors(value) {
2375
2685
  const closing = closingMatch.exec(value);
2376
2686
  if (!closing) continue;
2377
2687
  const end = closing.index + closing[0].length;
2378
- const fragment = load(value.slice(start, end));
2688
+ const fragment = load2(value.slice(start, end));
2379
2689
  const anchor = fragment("a").first();
2380
2690
  const href = anchor.attr("href");
2381
2691
  const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
@@ -2405,6 +2715,11 @@ function sanitizeMarkdownUrls(value) {
2405
2715
  return;
2406
2716
  }
2407
2717
  if (node.type === "mdxJsxTextElement" || node.type === "mdxJsxFlowElement") {
2718
+ const isLiteralCode = node.type === "mdxJsxTextElement" && node.name === "code" && !node.attributes?.length && (node.children ?? []).every((child) => child.type === "text");
2719
+ if (!isLiteralCode && (!node.name || !PORTABLE_MDX_COMPONENTS.has(node.name))) {
2720
+ hasUnsafeMdxSyntax = true;
2721
+ return;
2722
+ }
2408
2723
  for (const attribute of node.attributes ?? []) {
2409
2724
  if (attribute.type !== "mdxJsxAttribute" || !attribute.name || /^on[A-Z]/.test(attribute.name)) {
2410
2725
  hasUnsafeMdxSyntax = true;
@@ -2565,10 +2880,6 @@ function sanitizeRemoteMarkdown(body) {
2565
2880
  }
2566
2881
  }).replace(/\\[{}]/g, "");
2567
2882
  if (hasUnsafeExpression || /[{}]/.test(withoutStaticProps) || /\bon[A-Z][A-Za-z]*\s*=/g.test(withoutStaticProps) || hasUnsafePortableUrlProp(executable)) return null;
2568
- for (const match of withoutStaticProps.matchAll(/<\/?([A-Za-z][A-Za-z0-9.]*)(?:\s|>|\/)/g)) {
2569
- const component = match[1].split(".", 1)[0];
2570
- if (!PORTABLE_MDX_COMPONENTS.has(component)) return null;
2571
- }
2572
2883
  return urlSafeMarkdown;
2573
2884
  }
2574
2885
  function remoteMarkdownMetadata(body) {
@@ -2610,22 +2921,45 @@ function stripMintlifyDocumentationIndex(body) {
2610
2921
  return lines.slice(index).join("\n");
2611
2922
  }
2612
2923
  function extractEmbeddedOpenApi(content) {
2613
- const fencePattern = /^\s{0,3}(`{3,}|~{3,})(?:yaml|yml|json)\s+(\S+\.(?:yaml|yml|json))\s+(delete|get|head|options|patch|post|put|trace)\s+(\/\S+)\s*\r?\n([\s\S]*?)^\s{0,3}\1\s*$/im;
2614
- const match = fencePattern.exec(content);
2615
- if (!match) return { content };
2924
+ if (Buffer.byteLength(content, "utf8") > MAX_LOCAL_RESPONSE_BYTES) return { content };
2616
2925
  try {
2617
- const parsed = parseYaml2(match[5], { maxAliasCount: 50 });
2926
+ const root = embeddedOpenApiParser.parse(content);
2927
+ const candidates = (root.children ?? []).flatMap((node2) => {
2928
+ if (node2.type !== "code" || !/^(?:yaml|yml|json)$/i.test(node2.lang ?? "")) return [];
2929
+ const metadata = node2.meta?.match(/^(?:(\S+\.(?:yaml|yml|json))\s+)?(delete|get|head|options|patch|post|put|trace)\s+(\/\S+)\s*$/i);
2930
+ return metadata ? [{ node: node2, method: metadata[2].toUpperCase(), path: metadata[3] }] : [];
2931
+ });
2932
+ if (candidates.length !== 1) return { content };
2933
+ const { node, method, path } = candidates[0];
2934
+ const start = node.position?.start.offset;
2935
+ const end = node.position?.end.offset;
2936
+ if (start === void 0 || end === void 0) return { content };
2937
+ const lines = content.slice(start, end).split(/\r?\n/);
2938
+ const opening = lines[0].match(/^ {0,3}(`{3,}|~{3,})/);
2939
+ const closing = lines.at(-1)?.match(/^ {0,3}(`{3,}|~{3,})[\t ]*$/);
2940
+ if (!opening || !closing || opening[1][0] !== closing[1][0] || closing[1].length < opening[1].length) return { content };
2941
+ const parsed = parseYaml2(node.value ?? "", { maxAliasCount: 50 });
2618
2942
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
2619
2943
  const document = parsed;
2620
2944
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
2621
- const method = match[3].toUpperCase();
2622
- const path = match[4];
2623
2945
  const paths = document.paths;
2624
2946
  if (!paths || typeof paths !== "object" || Array.isArray(paths)) return { content };
2947
+ if (!Object.prototype.hasOwnProperty.call(paths, path)) return { content };
2625
2948
  const pathItem = paths[path];
2626
- if (!pathItem || typeof pathItem !== "object" || Array.isArray(pathItem) || !(match[3].toLowerCase() in pathItem)) return { content };
2627
- const before = content.slice(0, match.index).replace(/(?:^|\n)##\s+OpenAPI\s*\n\s*$/i, "\n");
2628
- const after = content.slice(match.index + match[0].length);
2949
+ if (!pathItem || typeof pathItem !== "object" || Array.isArray(pathItem) || !Object.prototype.hasOwnProperty.call(pathItem, method.toLowerCase())) return { content };
2950
+ const operation = pathItem[method.toLowerCase()];
2951
+ if (!operation || typeof operation !== "object" || Array.isArray(operation)) return { content };
2952
+ let values = 0;
2953
+ let textLength = 0;
2954
+ const serialized = JSON.stringify(document, (_key, value) => {
2955
+ values += 1;
2956
+ if (typeof value === "string") textLength += value.length;
2957
+ if (values > 1e5 || textLength > MAX_LOCAL_RESPONSE_BYTES || typeof value === "number" && !Number.isFinite(value)) throw new Error("OpenAPI document exceeds portable data limits.");
2958
+ return value;
2959
+ });
2960
+ if (Buffer.byteLength(serialized, "utf8") > MAX_LOCAL_RESPONSE_BYTES) return { content };
2961
+ const before = content.slice(0, start).replace(/(?:^|\n)##\s+OpenAPI\s*\n\s*$/i, "\n");
2962
+ const after = content.slice(end);
2629
2963
  return {
2630
2964
  content: `${before}${after}`.trim(),
2631
2965
  openapi: `${method} ${path}`,
@@ -2676,14 +3010,74 @@ function markdownPage(document, id, platform, warn) {
2676
3010
  openApiFragment: embeddedOpenApi.fragment
2677
3011
  };
2678
3012
  }
3013
+ function migrationHtmlArticle(body) {
3014
+ const $ = load2(body);
3015
+ const root = ["main article", "article", "main", '[role="main"]', "body"].map((selector) => $(selector).first()).find((candidate) => candidate.length);
3016
+ root.find([
3017
+ "script",
3018
+ "style",
3019
+ "noscript",
3020
+ "nav",
3021
+ "footer",
3022
+ "aside",
3023
+ ".theme-doc-version-badge",
3024
+ ".theme-doc-breadcrumbs",
3025
+ ".theme-doc-toc-mobile",
3026
+ ".table-of-contents",
3027
+ ".chat-assistant-floating-input",
3028
+ ".chat-assistant-sheet",
3029
+ "#chat-assistant-sheet",
3030
+ "#chat-assistant-textarea"
3031
+ ].join(",")).remove();
3032
+ const permalinkSelectors = MINTLIFY_ARTICLE_LABELS.map((labels) => `a[aria-label="${labels.permalink}"]`);
3033
+ 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) => {
3034
+ const parent = $(link).parent();
3035
+ $(link).remove();
3036
+ if (parent.is("div") && !parent.text().trim() && parent.children().length === 0) parent.remove();
3037
+ });
3038
+ root.find('svg.lucide-chevrons-up-down,summary[data-component-part="expandable-button"] svg[data-component-part="icon-svg"]').remove();
3039
+ root.find('li.task-list-item > input[type="checkbox"][disabled]').each((_index, element) => {
3040
+ const isChecked = $(element).attr("checked") !== void 0;
3041
+ $(element).replaceWith($("<span></span>").attr("data-thally-static-task-checkbox", isChecked ? "checked" : "unchecked").text(isChecked ? "[x] " : "[ ] "));
3042
+ });
3043
+ root.find(MINTLIFY_ARTICLE_LABELS.map((labels) => `select[aria-label="${labels.baseUrl}"]`).join(",")).each((_index, element) => {
3044
+ const select = $(element);
3045
+ const options = select.children("option").toArray();
3046
+ if (!options.length || options.length !== select.children().length) return;
3047
+ const urls = [];
3048
+ for (const option of options) {
3049
+ if ($(option).children().length) return;
3050
+ const label = $(option).text().trim();
3051
+ if (!/^https:\/\//i.test(label) || /[\s\u0000-\u001f\u007f]/.test(label)) return;
3052
+ try {
3053
+ const url = new URL(label);
3054
+ if (url.protocol !== "https:" || url.username || url.password) return;
3055
+ urls.push({ label, href: url.href, isSelected: $(option).attr("selected") !== void 0 });
3056
+ } catch {
3057
+ return;
3058
+ }
3059
+ }
3060
+ const projection = $("<div data-thally-static-base-urls></div>");
3061
+ const sourceId = select.attr("id");
3062
+ if (sourceId) projection.attr("id", sourceId);
3063
+ projection.append($("<p></p>").text(select.attr("aria-label")));
3064
+ const list = $("<ul></ul>");
3065
+ for (const url of urls) {
3066
+ const item = $("<li></li>").append($("<a></a>").attr("href", url.href).text(url.label));
3067
+ if (url.isSelected) item.append(" (selected)");
3068
+ list.append(item);
3069
+ }
3070
+ projection.append(list);
3071
+ select.replaceWith(projection);
3072
+ });
3073
+ return { $, root };
3074
+ }
2679
3075
  function htmlPage(document, id, options = {}) {
2680
- const $ = load(document.body);
2681
- const links = $('nav a[href], main a[href], article a[href], [role="main"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
2682
- const scopedTitle = options.omitDocumentHeading ? $('main article, article, main, [role="main"]').first().find("h1").first().text().trim() : "";
3076
+ const { $, root } = migrationHtmlArticle(document.body);
3077
+ const links = root.find("a[href]").map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
3078
+ const scopedTitle = options.omitDocumentHeading ? root.find("h1").first().text().trim() : "";
2683
3079
  const title = scopedTitle || $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
2684
3080
  const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
2685
- const content = $('main article, article, main, [role="main"]').first();
2686
- const root = content.length ? content : $("body");
2687
3081
  let documentHeadingAnchor = "";
2688
3082
  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";
2689
3083
  root.find(excludedElements).remove();
@@ -2697,6 +3091,9 @@ function htmlPage(document, id, options = {}) {
2697
3091
  `;
2698
3092
  heading.remove();
2699
3093
  }
3094
+ root.find("h1").each((_index, element) => {
3095
+ element.name = "h2";
3096
+ });
2700
3097
  }
2701
3098
  root.find("a[href]").each((_index, element) => {
2702
3099
  const href = $(element).attr("href");
@@ -2726,8 +3123,20 @@ function htmlPage(document, id, options = {}) {
2726
3123
  $(element).removeAttr("src");
2727
3124
  }
2728
3125
  });
2729
- const turndown = new TurndownService({ codeBlockStyle: "fenced", emDelimiter: "_", headingStyle: "atx" });
3126
+ const turndown = new TurndownService({ codeBlockStyle: "fenced", emDelimiter: "_", headingStyle: "atx", preformattedCode: true });
3127
+ let isConvertingTableCell = false;
2730
3128
  turndown.remove(["script", "style", "noscript"]);
3129
+ turndown.addRule("migration-literal-table-code", {
3130
+ filter: (node) => isConvertingTableCell && node.nodeName === "CODE" && node.parentNode?.nodeName !== "PRE" && needsLiteralTableCode(node.textContent ?? ""),
3131
+ // A strict, attribute-free text leaf preserves the payload that GFM's
3132
+ // escaped-pipe code-span grammar cannot represent without changing it.
3133
+ replacement: (_content, node) => `<code>${literalMdxCodeText(node.textContent ?? "")}</code>`
3134
+ });
3135
+ turndown.addRule("migration-static-task-checkbox", {
3136
+ filter: (node) => node.nodeName === "SPAN" && ["checked", "unchecked"].includes(node.getAttribute("data-thally-static-task-checkbox") ?? ""),
3137
+ // Emit literal task-list syntax rather than escaping its brackets as copy.
3138
+ replacement: (_content, node) => node.getAttribute("data-thally-static-task-checkbox") === "checked" ? "[x] " : "[ ] "
3139
+ });
2731
3140
  const anchor = (element) => {
2732
3141
  const value = element.getAttribute("id");
2733
3142
  return options.preserveAnchors && value ? `<a id="${value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/\{/g, "&#123;")}" />
@@ -2736,12 +3145,12 @@ function htmlPage(document, id, options = {}) {
2736
3145
  };
2737
3146
  if (options.preserveAnchors) turndown.addRule("migration-source-anchor", {
2738
3147
  filter: (node) => node.hasAttribute("id") && node.nodeName !== "PRE",
2739
- replacement: (content2, node) => {
3148
+ replacement: (content, node) => {
2740
3149
  const element = node;
2741
3150
  const heading = element.nodeName.match(/^H([1-6])$/);
2742
3151
  return `
2743
3152
 
2744
- ${anchor(element)}${heading ? `${"#".repeat(Number(heading[1]))} ` : ""}${content2}
3153
+ ${anchor(element)}${heading ? `${"#".repeat(Number(heading[1]))} ${content.trim()}` : content}
2745
3154
 
2746
3155
  `;
2747
3156
  }
@@ -2766,10 +3175,10 @@ ${anchor(element)}${heading ? `${"#".repeat(Number(heading[1]))} ` : ""}${conten
2766
3175
  });
2767
3176
  turndown.addRule("migration-responsive-picture", {
2768
3177
  filter: "picture",
2769
- replacement: (content2) => `
3178
+ replacement: (content) => `
2770
3179
 
2771
3180
  <picture>
2772
- ${content2.trim()}
3181
+ ${content.trim()}
2773
3182
  </picture>
2774
3183
 
2775
3184
  `
@@ -2781,7 +3190,7 @@ ${content2.trim()}
2781
3190
  const element = node;
2782
3191
  const className = element.className;
2783
3192
  const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
2784
- const fragment = load(element.outerHTML);
3193
+ const fragment = load2(element.outerHTML);
2785
3194
  const clone = fragment(".theme-admonition").first();
2786
3195
  const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
2787
3196
  const titleText = title2.text().trim();
@@ -2802,7 +3211,7 @@ ${titleText ? `**${escapeMarkdownText(titleText)}**
2802
3211
  filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
2803
3212
  replacement: (_content, node) => {
2804
3213
  const element = node;
2805
- const fragment = load(element.outerHTML);
3214
+ const fragment = load2(element.outerHTML);
2806
3215
  const container = fragment(".tabs-container").first();
2807
3216
  const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
2808
3217
  const panels = container.find('[role="tabpanel"]').toArray().filter((panel) => fragment(panel).closest(".tabs-container")[0] === container[0]);
@@ -2830,11 +3239,16 @@ ${tabs.join("\n\n")}
2830
3239
  turndown.addRule("gfm-table", {
2831
3240
  filter: "table",
2832
3241
  replacement: (_content, node) => {
2833
- const fragment = load(node.outerHTML);
2834
- const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
2835
- fragment(cell).text().replace(/\s+/g, " ").trim(),
2836
- true
2837
- ))).filter((row) => row.length > 0);
3242
+ const fragment = load2(node.outerHTML);
3243
+ const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => {
3244
+ const wasConvertingTableCell = isConvertingTableCell;
3245
+ isConvertingTableCell = true;
3246
+ try {
3247
+ return portableMarkdownTableCell(turndown.turndown(fragment(cell).html() ?? ""));
3248
+ } finally {
3249
+ isConvertingTableCell = wasConvertingTableCell;
3250
+ }
3251
+ })).filter((row) => row.length > 0);
2838
3252
  if (rows.length === 0) return "";
2839
3253
  const width = Math.max(...rows.map((row) => row.length));
2840
3254
  const normalized = rows.map((row) => [...row, ...Array(width - row.length).fill("")]);
@@ -2851,7 +3265,7 @@ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
2851
3265
  filter: (node) => node.nodeName === "PRE",
2852
3266
  replacement: (_content, node) => {
2853
3267
  const element = node;
2854
- const fragment = load(element.outerHTML);
3268
+ const fragment = load2(element.outerHTML);
2855
3269
  const tokenLines = fragment(".token-line").toArray();
2856
3270
  const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
2857
3271
  const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
@@ -2867,7 +3281,7 @@ ${fence}
2867
3281
  }
2868
3282
  });
2869
3283
  const escape = turndown.escape.bind(turndown);
2870
- turndown.escape = (text) => escape(text).replace(/[<{}]/g, (match) => `\\${match}`);
3284
+ turndown.escape = (text2) => isConvertingTableCell ? escape(text2.replace(/[<>{}]/g, (character) => ({ "<": "&lt;", ">": "&gt;", "{": "&#123;", "}": "&#125;" })[character])) : escape(text2).replace(/[<{}]/g, (match) => `\\${match}`);
2871
3285
  const body = `${documentHeadingAnchor}${turndown.turndown(root.html() ?? "").trim()}`;
2872
3286
  if (!title || body.length < 40 && $("article").length === 0) return { page: null, links };
2873
3287
  return {
@@ -2885,7 +3299,7 @@ ${fence}
2885
3299
  }
2886
3300
 
2887
3301
  // src/url-run.ts
2888
- var CONVERTER_VERSION = "url-evidence-v2";
3302
+ var CONVERTER_VERSION = "url-evidence-v3";
2889
3303
  var parser = unified3().use(remarkParse3).use(remarkMdx3);
2890
3304
  var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
2891
3305
  var MAX_RESPONSE_BYTES = 8e6;
@@ -2895,9 +3309,12 @@ var LOCALES = /* @__PURE__ */ new Set(["ar", "cs", "da", "de", "el", "en", "es",
2895
3309
  function hash(value) {
2896
3310
  return createHash("sha256").update(value).digest("hex");
2897
3311
  }
3312
+ function canonicalJson(value) {
3313
+ const canonical2 = (value2) => Array.isArray(value2) ? value2.map(canonical2) : value2 !== null && typeof value2 === "object" ? Object.fromEntries(Object.entries(value2).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, field]) => [key, canonical2(field)])) : value2;
3314
+ return JSON.stringify(canonical2(value));
3315
+ }
2898
3316
  function pageHash(page) {
2899
- const canonical2 = (value) => Array.isArray(value) ? value.map(canonical2) : value !== null && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, field]) => [key, canonical2(field)])) : value;
2900
- return hash(JSON.stringify(canonical2(page)));
3317
+ return hash(canonicalJson(page));
2901
3318
  }
2902
3319
  function inScope(url, source, scope) {
2903
3320
  return url.origin === source.origin && !url.username && !url.password && (scope === "/" || url.pathname === scope || url.pathname.startsWith(`${scope}/`));
@@ -2916,7 +3333,7 @@ function canonical(value, base) {
2916
3333
  }
2917
3334
  function classifyMigrationResponse(document) {
2918
3335
  if (document.status && document.status >= 400) return "unsupported";
2919
- if (/\/(?:llms(?:-full)?|robots)\.txt$|\/sitemap[^/]*\.xml$|\/search(?:\.html)?$/i.test(document.finalUrl.pathname) || /(?:xml|json)/i.test(document.contentType)) return "index";
3336
+ 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";
2920
3337
  if (/^(?:image|audio|video)\//i.test(document.contentType) || /\.(?:pdf|zip|png|jpe?g|webp|gif|svg|ico|mp4|webm)$/i.test(document.finalUrl.pathname)) return "asset";
2921
3338
  return /(?:html|markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname) ? "page" : "unsupported";
2922
3339
  }
@@ -2938,14 +3355,14 @@ function alternativeBlocks(body, baseline) {
2938
3355
  return blocks(body).filter((block) => !originals.has(block)).join("\n\n");
2939
3356
  }
2940
3357
  function linksOf(document) {
3358
+ if (/json|ya?ml/i.test(document.contentType) || /\.(?:json|ya?ml)$/i.test(document.finalUrl.pathname)) return [];
2941
3359
  if (/html/i.test(document.contentType)) {
2942
- const $ = load2(document.body);
2943
- const root = $('main article,article,main,[role="main"]').first();
3360
+ const { $, root } = migrationHtmlArticle(document.body);
2944
3361
  root.find("nav,footer,aside").remove();
2945
3362
  return [...root.find("a[href]").toArray(), ...$('link[rel="sitemap"][href],link[rel="alternate"][href]').toArray()].map((node) => $(node).attr("href")).filter(Boolean);
2946
3363
  }
2947
3364
  if (/xml/i.test(document.contentType) || /<urlset|<sitemapindex/i.test(document.body)) {
2948
- const $ = load2(document.body, { xmlMode: true });
3365
+ const $ = load3(document.body, { xmlMode: true });
2949
3366
  return $("loc").toArray().map((node) => $(node).text().trim());
2950
3367
  }
2951
3368
  return [
@@ -2954,8 +3371,7 @@ function linksOf(document) {
2954
3371
  ];
2955
3372
  }
2956
3373
  function htmlEvidence(document) {
2957
- const $ = load2(document.body);
2958
- const root = $('main article,article,main,[role="main"]').first();
3374
+ const { $, root } = migrationHtmlArticle(document.body);
2959
3375
  const article = root.length ? root : $("body");
2960
3376
  article.find("nav,footer,aside,form,script,style").remove();
2961
3377
  return {
@@ -2982,55 +3398,35 @@ function evidenceOf(capture) {
2982
3398
  function markdownHeadingAnchors(body) {
2983
3399
  const seen = /* @__PURE__ */ new Map();
2984
3400
  return markdownNodes(body).filter((node) => node.type === "heading").map((node) => {
2985
- const text = (value) => value.value ?? (value.children ?? []).map(text).join("");
2986
- const base = text(node).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
3401
+ const text2 = (value) => value.value ?? (value.children ?? []).map(text2).join("");
3402
+ const base = text2(node).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
2987
3403
  const occurrence = seen.get(base) ?? 0;
2988
3404
  seen.set(base, occurrence + 1);
2989
3405
  return occurrence ? `${base}-${occurrence}` : base;
2990
3406
  });
2991
3407
  }
2992
3408
  function sourceNavigation(capture) {
2993
- if (capture.navigation) {
2994
- const resolve3 = (nodes, depth = 0) => {
2995
- if (depth > 32 || nodes.length > 1e4) throw new Error("Source navigation exceeds its depth or width safety bound.");
2996
- return nodes.map((node) => ({ label: node.label, ...node.url ? { url: new URL(node.url, capture.finalUrl).toString() } : {}, ...node.children ? { children: resolve3(node.children, depth + 1) } : {} }));
2997
- };
2998
- return resolve3(capture.navigation);
2999
- }
3000
- if (!/html/i.test(capture.contentType)) return [];
3001
- const $ = load2(capture.body);
3002
- const candidates = ['.theme-doc-sidebar-menu,[aria-label*="sidebar" i]', "aside", '[role="navigation"],nav'];
3003
- 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) ?? $("aside").first();
3004
- if (!root.length) return [];
3005
- const walk = (element, excluded) => {
3006
- const result = [];
3007
- $(element).children().each((_index, node) => {
3008
- if (node === excluded) return;
3009
- const item = $(node);
3010
- if (item.is("a[href]")) {
3011
- result.push({ label: item.text().replace(/\s+/g, " ").trim(), url: new URL(item.attr("href"), capture.finalUrl).toString() });
3012
- } else if (item.is("li,details") || item.attr("data-migration-group") !== void 0 || item.children("h2,h3,h4,h5,h6").length > 0) {
3013
- const directLink = item.children("a[href]").first();
3014
- const direct = directLink.length ? directLink : item.children("div").children("a[href]").first();
3015
- const label = direct.text().trim() || item.children("summary,button,span,h2,h3,h4,h5,h6").first().text().trim();
3016
- const children = walk(node, direct[0]);
3017
- if (label) result.push({ label, ...direct.length ? { url: new URL(direct.attr("href"), capture.finalUrl).toString() } : {}, ...children.length ? { children } : {} });
3018
- else result.push(...children);
3019
- } else result.push(...walk(node, excluded));
3020
- });
3021
- return result;
3022
- };
3023
- try {
3024
- return walk(root);
3025
- } catch {
3026
- return [];
3027
- }
3409
+ return extractSourceNavigation(capture);
3028
3410
  }
3029
3411
  function idForUrl(url, scope) {
3030
3412
  const relative4 = scope === "/" ? url.pathname.slice(1) : url.pathname.slice(scope.length).replace(/^\//, "");
3031
3413
  const id = pageIdFromReference(relative4 || "introduction", true);
3032
3414
  return id && `${id}${url.search ? `--${hash(url.search).slice(0, 12)}` : ""}`;
3033
3415
  }
3416
+ function articleIdentity(document) {
3417
+ if (!/html/i.test(document.contentType)) return document.finalUrl;
3418
+ const $ = load3(document.body);
3419
+ const reference = $('link[rel="canonical"][href]').first().attr("href");
3420
+ if (!reference) return document.finalUrl;
3421
+ try {
3422
+ const target2 = new URL(reference, document.finalUrl);
3423
+ const source = document.finalUrl;
3424
+ const directory = (path) => path.replace(/\/index\/?$/i, "").replace(/\/$/, "");
3425
+ return target2.origin === source.origin && !target2.username && !target2.password && target2.search === source.search && directory(target2.pathname) === directory(source.pathname) ? target2 : source;
3426
+ } catch {
3427
+ return document.finalUrl;
3428
+ }
3429
+ }
3034
3430
  function convert(document, id) {
3035
3431
  if (/html/i.test(document.contentType)) {
3036
3432
  const technical = htmlEvidence(document);
@@ -3048,9 +3444,20 @@ function convert(document, id) {
3048
3444
  const info = original.split("\n", 1)[0].replace(/^`+|^~+/, "");
3049
3445
  const code = technical.code[index];
3050
3446
  const fence = "`".repeat(Math.max(3, ...[...code.matchAll(/`+/g)].map((match) => match[0].length + 1)));
3447
+ const lineStart = page.body.lastIndexOf("\n", start - 1) + 1;
3448
+ const prefix = page.body.slice(lineStart, start);
3449
+ let hasContainerPrefix = true;
3450
+ for (const character of prefix) {
3451
+ if (character !== " " && character !== " " && character !== ">") {
3452
+ hasContainerPrefix = false;
3453
+ break;
3454
+ }
3455
+ }
3456
+ const indent = hasContainerPrefix ? prefix : "";
3457
+ const payload = code.split("\n").map((line) => `${indent}${line}`).join("\n");
3051
3458
  page.body = `${page.body.slice(0, start)}${fence}${info}
3052
- ${code}
3053
- ${fence}${page.body.slice(end)}`;
3459
+ ${payload}
3460
+ ${indent}${fence}${page.body.slice(end)}`;
3054
3461
  }
3055
3462
  }
3056
3463
  return page;
@@ -3062,16 +3469,14 @@ function assetReferences(capture) {
3062
3469
  if (!/html/i.test(capture.contentType)) {
3063
3470
  return markdownNodes(capture.body).filter((node) => node.type === "image").flatMap((node) => node.url ? [node.url] : []);
3064
3471
  }
3065
- const $ = load2(capture.body);
3066
- const root = $('main article,article,main,[role="main"]').first();
3472
+ const { $, root } = migrationHtmlArticle(capture.body);
3067
3473
  return [
3068
3474
  ...root.find("img[src],video[poster],a[download][href]").toArray().map((node) => $(node).attr("src") ?? $(node).attr("poster") ?? $(node).attr("href")),
3069
3475
  ...root.find("[srcset]").toArray().flatMap((node) => migrationImageCandidates($(node).attr("srcset") ?? "", capture.finalUrl).map((candidate) => candidate.url))
3070
3476
  ].filter(Boolean);
3071
3477
  }
3072
- function unresolvedStaticControls(body) {
3073
- const $ = load2(body);
3074
- const root = $('main article,article,main,[role="main"]').first();
3478
+ function unresolvedStaticControls(body, hasApiSchema = false) {
3479
+ const { $, root } = migrationHtmlArticle(body);
3075
3480
  const tabs = root.find('[role="tab"]').toArray();
3076
3481
  for (const tab of tabs) {
3077
3482
  const controls = $(tab).attr("aria-controls");
@@ -3079,6 +3484,7 @@ function unresolvedStaticControls(body) {
3079
3484
  if (panels.length !== 1 || !panels.text().trim() && !panels.find("pre,img,table,svg").length || panels.find('[aria-busy="true"],[role="progressbar"]').length) return true;
3080
3485
  }
3081
3486
  return root.find("details:not([open])").toArray().some((node) => {
3487
+ if (hasApiSchema && $(node).is(".expandable") && $(node).children('summary[data-component-part="expandable-button"]').length) return false;
3082
3488
  const clone = $(node).clone();
3083
3489
  clone.children("summary").remove();
3084
3490
  return !clone.text().trim() && !clone.find("pre,img,table,svg").length;
@@ -3116,13 +3522,47 @@ function acceptedAsset(asset) {
3116
3522
  }
3117
3523
  function validRecord(record) {
3118
3524
  if (record.outcome !== "imported") return true;
3119
- return Boolean(record.page && record.pageSha256 === pageHash(record.page) && record.evidence && pageIdFromReference(record.page.id, true) === record.page.id && pageIdFromReference(record.page.navigationId, true) === record.page.navigationId && sanitizeRemoteMarkdown(record.page.body.replace(/^<a id="[^"<]*" \/>$/gm, "")) !== null && /^[a-f0-9]{64}$/.test(record.evidence.sha256) && record.assets?.every((asset) => /^migration-assets\/[a-f0-9]{64}\.[a-z0-9]+$/.test(asset.path) && hash(Buffer.from(asset.base64, "base64")) === asset.sha256));
3525
+ return Boolean(record.page && record.pageSha256 === pageHash(record.page) && record.evidence && pageIdFromReference(record.page.id, true) === record.page.id && pageIdFromReference(record.page.navigationId, true) === record.page.navigationId && sanitizeRemoteMarkdown(record.page.body.replace(/^<a id="[^"<]*" \/>$/gm, "")) !== null && /^[a-f0-9]{64}$/.test(record.evidence.sha256) && (!record.apiSpecification || hasCompleteLocalApiSchema(record.apiSpecification.document) && record.apiSpecification.path === `openapi/migration-${record.apiSpecification.sha256}.json` && hash(canonicalJson(record.apiSpecification.document)) === record.apiSpecification.sha256) && record.assets?.every((asset) => /^migration-assets\/[a-f0-9]{64}\.[a-z0-9]+$/.test(asset.path) && hash(Buffer.from(asset.base64, "base64")) === asset.sha256));
3526
+ }
3527
+ function hasCompleteLocalApiSchema(document) {
3528
+ let visited = 0;
3529
+ const visit = (value, depth = 0) => {
3530
+ if (++visited > 1e5 || depth > 96) return false;
3531
+ if (typeof value === "number" && !Number.isFinite(value)) return false;
3532
+ if (!value || typeof value !== "object") return true;
3533
+ if (Array.isArray(value)) return value.every((child) => visit(child, depth + 1));
3534
+ const object2 = value;
3535
+ if (Object.prototype.hasOwnProperty.call(object2, "$ref")) {
3536
+ if (typeof object2.$ref !== "string" || !object2.$ref.startsWith("#/")) return false;
3537
+ let target2 = document;
3538
+ try {
3539
+ for (const encoded of object2.$ref.slice(2).split("/")) {
3540
+ const key = decodeURIComponent(encoded).replace(/~1/g, "/").replace(/~0/g, "~");
3541
+ if (!target2 || typeof target2 !== "object" || !Object.prototype.hasOwnProperty.call(target2, key)) return false;
3542
+ target2 = target2[key];
3543
+ }
3544
+ } catch {
3545
+ return false;
3546
+ }
3547
+ }
3548
+ return Object.values(object2).every((child) => visit(child, depth + 1));
3549
+ };
3550
+ return visit(document);
3551
+ }
3552
+ function resolveMigrationRecord(records, record) {
3553
+ const seen = /* @__PURE__ */ new Set();
3554
+ while (record?.outcome === "alias") {
3555
+ if (seen.has(record.url) || seen.size >= 32) return void 0;
3556
+ seen.add(record.url);
3557
+ record = records.get(record.aliasOf ?? "");
3558
+ }
3559
+ return record;
3120
3560
  }
3121
3561
  function navigationConfig(navigation, records) {
3122
3562
  const project = (nodes) => nodes.flatMap((node) => {
3123
3563
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
3124
- const target = record?.outcome === "alias" ? records.get(record.aliasOf ?? "") : record;
3125
- const page = target?.page;
3564
+ const target2 = resolveMigrationRecord(records, record);
3565
+ const page = target2?.page;
3126
3566
  const children = project(node.children ?? []);
3127
3567
  if (children.length) {
3128
3568
  const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === page?.navigationId : contains(entry.pages));
@@ -3130,6 +3570,23 @@ function navigationConfig(navigation, records) {
3130
3570
  }
3131
3571
  return page ? [page.navigationId] : [];
3132
3572
  });
3573
+ const tabs = navigation.filter((node) => node.kind === "tab");
3574
+ if (tabs.length) {
3575
+ const projection = localeProjection({ records: [...records.values()] });
3576
+ const locales = [...new Set(tabs.map((tab) => tab.locale ?? projection.defaultLocale))];
3577
+ const localizedTabs = (locale) => tabs.filter((tab) => (tab.locale ?? projection.defaultLocale) === locale).flatMap((tab) => {
3578
+ const pages2 = project(tab.children ?? []);
3579
+ return pages2.length || tab.url ? [{ tab: tab.label, ...pages2.length ? { pages: pages2 } : {}, ...tab.url ? { href: tab.url } : {} }] : [];
3580
+ });
3581
+ const primary = localizedTabs(projection.defaultLocale);
3582
+ return { tabs: primary.length ? primary : localizedTabs(locales[0]), ...locales.length > 1 ? {
3583
+ i18n: {
3584
+ defaultLocale: projection.defaultLocale,
3585
+ locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })),
3586
+ navigation: Object.fromEntries(locales.map((locale) => [locale, localizedTabs(locale)]))
3587
+ }
3588
+ } : {} };
3589
+ }
3133
3590
  const pages = project(navigation);
3134
3591
  return pages.length ? { tabs: [{ tab: "Documentation", pages }] } : null;
3135
3592
  }
@@ -3151,7 +3608,7 @@ function observedLocale(document, id, scopePath) {
3151
3608
  }
3152
3609
  const urlLocale = [id.split("/", 1)[0], ...scopePath.split("/"), document.finalUrl.pathname.split("/").filter(Boolean)[0]].find((segment) => segment && LOCALES.has(segment.toLowerCase()));
3153
3610
  if (urlLocale) candidates.push({ source: "URL", value: canonicalLanguage(urlLocale) });
3154
- const declared = /html/i.test(document.contentType) ? load2(document.body)("html").attr("lang")?.trim() : void 0;
3611
+ const declared = /html/i.test(document.contentType) ? load3(document.body)("html").attr("lang")?.trim() : void 0;
3155
3612
  if (declared) {
3156
3613
  const locale = canonicalLanguage(declared);
3157
3614
  if (locale) candidates.push({ source: "html lang", value: locale });
@@ -3186,6 +3643,7 @@ function localeProjection(checkpoint) {
3186
3643
  }
3187
3644
  function validateUrlMigration(checkpoint, cancelled = false) {
3188
3645
  const issues = [];
3646
+ const warnings = [];
3189
3647
  const counts = { pending: 0, imported: 0, excluded: 0, failed: 0, alias: 0 };
3190
3648
  const records = new Map(checkpoint.records.map((record) => [record.url, record]));
3191
3649
  const source = new URL(checkpoint.sourceUrl);
@@ -3194,7 +3652,7 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3194
3652
  const outputIds = /* @__PURE__ */ new Set();
3195
3653
  const resolve3 = (url) => {
3196
3654
  const record = records.get(canonical(url, checkpoint.sourceUrl) ?? "");
3197
- return record?.outcome === "alias" ? records.get(record.aliasOf ?? "") : record;
3655
+ return resolveMigrationRecord(records, record);
3198
3656
  };
3199
3657
  for (const record of checkpoint.records) {
3200
3658
  counts[record.outcome]++;
@@ -3227,7 +3685,15 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3227
3685
  anchor = decodeURIComponent(anchor);
3228
3686
  } catch {
3229
3687
  }
3230
- if (!destination.evidence?.anchors.includes(anchor)) issues.push({ code: "missing-anchor", source: record.url, message: `Missing source anchor: ${link}` });
3688
+ if (!destination.evidence?.anchors.includes(anchor)) {
3689
+ warnings.push({ code: "source-link-warning", source: record.url, message: `The original source link refers to an absent source anchor: ${link}` });
3690
+ } else if (destination.page) {
3691
+ const nodes = markdownNodes(destination.page.body);
3692
+ const explicit = nodes.flatMap((node) => node.name === "a" ? (node.attributes ?? []).flatMap((attribute) => attribute.name === "id" && typeof attribute.value === "string" ? [attribute.value] : []) : []);
3693
+ if (!explicit.includes(anchor) && !markdownHeadingAnchors(destination.page.body).includes(anchor)) {
3694
+ issues.push({ code: "missing-anchor", source: record.url, message: `Conversion lost a source anchor: ${link}` });
3695
+ }
3696
+ }
3231
3697
  }
3232
3698
  }
3233
3699
  }
@@ -3242,7 +3708,7 @@ function validateUrlMigration(checkpoint, cancelled = false) {
3242
3708
  };
3243
3709
  visit(checkpoint.navigation);
3244
3710
  if (!counts.imported) issues.push({ code: "missing-page", source: checkpoint.sourceUrl, message: "No readable article pages were imported." });
3245
- return { status: cancelled ? "cancelled" : issues.length ? "partial" : "passed", issues, counts };
3711
+ return { status: cancelled ? "cancelled" : issues.length ? "partial" : "passed", issues, counts, ...warnings.length ? { warnings } : {} };
3246
3712
  }
3247
3713
  async function runUrlMigration(options) {
3248
3714
  const source = validateMigrationUrl(options.sourceUrl);
@@ -3272,6 +3738,11 @@ async function runUrlMigration(options) {
3272
3738
  delete record.pageSha256;
3273
3739
  delete record.evidence;
3274
3740
  delete record.assets;
3741
+ delete record.apiSpecification;
3742
+ delete record.version;
3743
+ delete record.issues;
3744
+ delete record.reason;
3745
+ delete record.aliasOf;
3275
3746
  }
3276
3747
  if (record.page && pageIdFromReference(record.page.id, true) !== record.page.id) throw new Error("Checkpoint contains an unsafe page path.");
3277
3748
  records.set(record.url, record);
@@ -3300,6 +3771,33 @@ async function runUrlMigration(options) {
3300
3771
  enqueue(sourceUrl, sourceUrl, "submitted");
3301
3772
  for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`${scopePath === "/" ? "" : scopePath}/${path}`, sourceUrl, "discovery-index");
3302
3773
  if (scopePath !== "/") for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`/${path}`, sourceUrl, "discovery-index");
3774
+ const enqueueArticleLinks = (links, base, discoveredBy) => {
3775
+ for (const link of links) {
3776
+ try {
3777
+ const url = new URL(link, base);
3778
+ if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, base, discoveredBy);
3779
+ } catch {
3780
+ }
3781
+ }
3782
+ };
3783
+ const enqueueNavigation = (nodes, base) => {
3784
+ for (const node of nodes) {
3785
+ if (node.url) enqueue(node.url, base, "source-navigation");
3786
+ enqueueNavigation(node.children ?? [], base);
3787
+ }
3788
+ };
3789
+ for (const record of [...records.values()]) {
3790
+ if (!record.evidence) continue;
3791
+ const final = canonical(record.evidence.finalUrl, sourceUrl);
3792
+ if (!final) continue;
3793
+ if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
3794
+ else if (record.outcome === "excluded") {
3795
+ if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
3796
+ for (const link of record.evidence.links) enqueue(link, final, record.url);
3797
+ }
3798
+ }
3799
+ }
3800
+ enqueueNavigation(checkpoint.navigation, sourceUrl);
3303
3801
  const fetcher = options.fetcher ?? defaultMigrationFetcher;
3304
3802
  const capture = options.captureProvider?.capture.bind(options.captureProvider) ?? (async ({ url }) => fetcher(url, { accept: ACCEPT }));
3305
3803
  const assetFetcher = options.assetFetcher ?? (!options.fetcher && !options.captureProvider ? defaultAssetFetcher : void 0);
@@ -3335,13 +3833,24 @@ async function runUrlMigration(options) {
3335
3833
  navigationChanged = false;
3336
3834
  };
3337
3835
  const process = async (record) => {
3338
- if (options.signal?.aborted) return;
3836
+ if (options.signal?.aborted || record.outcome !== "pending") return;
3837
+ let claimedId;
3838
+ let claimedFinal;
3839
+ let markdownRepresentation;
3339
3840
  try {
3340
3841
  delete record.reason;
3842
+ delete record.apiSpecification;
3843
+ const represented = resolveMigrationRecord(records, record.representationOf ? records.get(record.representationOf) : void 0);
3844
+ if (represented?.outcome === "imported") {
3845
+ record.outcome = "alias";
3846
+ record.aliasOf = represented.url;
3847
+ return;
3848
+ }
3341
3849
  const document = await capture({ url: new URL(record.url), signal: options.signal });
3342
3850
  if (options.signal?.aborted) return;
3343
3851
  if (Buffer.byteLength(document.body) > MAX_RESPONSE_BYTES) throw new Error("Captured response exceeds the 8 MB safety bound.");
3344
- const final = canonical(document.finalUrl.toString(), sourceUrl);
3852
+ const identity = articleIdentity(document);
3853
+ const final = canonical(identity.toString(), sourceUrl);
3345
3854
  const role = classifyMigrationResponse(document);
3346
3855
  if (!inScope(document.finalUrl, source, scopePath) && !(role === "index" && isDiscoveryIndex(document.finalUrl, source))) {
3347
3856
  record.outcome = "excluded";
@@ -3362,6 +3871,30 @@ async function runUrlMigration(options) {
3362
3871
  for (const link of links) enqueue(link, final, record.url);
3363
3872
  return;
3364
3873
  }
3874
+ if (/html/i.test(document.contentType)) {
3875
+ const $ = load3(document.body);
3876
+ $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
3877
+ const alternate = canonical($(node).attr("href"), final);
3878
+ if (!alternate) return;
3879
+ const target2 = new URL(alternate), primary = new URL(final);
3880
+ const directory = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
3881
+ if (target2.origin !== primary.origin || target2.search !== primary.search || directory(target2.pathname) !== directory(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
3882
+ enqueue(alternate, final, "declared-markdown-representation");
3883
+ const sibling = records.get(alternate);
3884
+ sibling.representationOf = record.url;
3885
+ markdownRepresentation ??= alternate;
3886
+ changed.push(sibling);
3887
+ });
3888
+ }
3889
+ const observedNavigation = sourceNavigation(document);
3890
+ if (observedNavigation.length && !(checkpoint.navigation.some((node) => node.kind === "tab") && !observedNavigation.some((node) => node.kind === "tab"))) {
3891
+ const merged = mergeSourceNavigation(checkpoint.navigation, observedNavigation);
3892
+ if (JSON.stringify(merged) !== JSON.stringify(checkpoint.navigation)) {
3893
+ checkpoint.navigation = merged;
3894
+ navigationChanged = true;
3895
+ }
3896
+ enqueueNavigation(observedNavigation, final);
3897
+ }
3365
3898
  if (imported >= maxPages) {
3366
3899
  record.outcome = "excluded";
3367
3900
  record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
@@ -3374,14 +3907,20 @@ async function runUrlMigration(options) {
3374
3907
  return;
3375
3908
  }
3376
3909
  finalUrls.set(final, record.url);
3377
- const id = idForUrl(document.finalUrl, scopePath);
3910
+ claimedFinal = final;
3911
+ const id = idForUrl(identity, scopePath);
3378
3912
  if (!id) throw new Error("Source URL has no safe destination path.");
3379
- if (usedIds.has(id) && usedIds.get(id) !== record.url) {
3913
+ const language = observedLocale(document, id, scopePath);
3914
+ const locale = language.locale;
3915
+ const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
3916
+ const identityId = locale ? `${locale}/${navigationId}` : id;
3917
+ if (usedIds.has(identityId) && usedIds.get(identityId) !== record.url) {
3380
3918
  record.outcome = "failed";
3381
- record.reason = `Destination collision for ${id}.`;
3919
+ record.reason = `Destination collision for ${identityId}.`;
3382
3920
  return;
3383
3921
  }
3384
- usedIds.set(id, record.url);
3922
+ usedIds.set(identityId, record.url);
3923
+ claimedId = identityId;
3385
3924
  const evidence = evidenceOf(document);
3386
3925
  const page = convert(document, id);
3387
3926
  if (!page) {
@@ -3389,6 +3928,23 @@ async function runUrlMigration(options) {
3389
3928
  record.reason = "Content could not be represented as safe inert MDX.";
3390
3929
  return;
3391
3930
  }
3931
+ 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;
3932
+ if (/html/i.test(document.contentType) && markdownRepresentation && (unresolvedStaticControls(document.body) || hasSchemaChoice)) {
3933
+ const schemaCapture = await capture({ url: new URL(markdownRepresentation), signal: options.signal });
3934
+ if (options.signal?.aborted) return;
3935
+ if (schemaCapture.status && schemaCapture.status >= 400 || !/text\/(?:markdown|plain)/i.test(schemaCapture.contentType)) throw new Error("API schema representation is not a successful Markdown response.");
3936
+ totalBytes += Buffer.byteLength(schemaCapture.body);
3937
+ if (Buffer.byteLength(schemaCapture.body) > MAX_RESPONSE_BYTES || totalBytes > (options.maxTotalBytes ?? 5e8)) throw new Error("API schema capture exceeds its content byte budget.");
3938
+ const expected = new URL(markdownRepresentation);
3939
+ if (schemaCapture.finalUrl.origin !== expected.origin || schemaCapture.finalUrl.pathname !== expected.pathname || schemaCapture.finalUrl.search !== expected.search) throw new Error("API schema representation escaped its declared identity.");
3940
+ const fragment = extractEmbeddedOpenApi(schemaCapture.body).fragment;
3941
+ if (fragment && hasCompleteLocalApiSchema(fragment.document)) {
3942
+ const sha256 = hash(canonicalJson(fragment.document));
3943
+ const path = `openapi/migration-${sha256}.json`;
3944
+ record.apiSpecification = { path, sha256, document: fragment.document };
3945
+ page.openapi = `${path} ${fragment.method} ${fragment.path}`;
3946
+ }
3947
+ }
3392
3948
  const states = document.states ?? [];
3393
3949
  if (states.length > 64) throw new Error("Capture contains too many alternative states.");
3394
3950
  for (const state of states) {
@@ -3417,14 +3973,16 @@ ${difference}`;
3417
3973
  record.assets = [];
3418
3974
  record.issues = [];
3419
3975
  if (/html/i.test(document.contentType)) {
3420
- const $ = load2(document.body);
3421
- const root = $('main article,article,main,[role="main"]').first();
3976
+ const { $, root } = migrationHtmlArticle(document.body);
3422
3977
  root.find("nav,footer,aside,.theme-doc-breadcrumbs,.theme-doc-toc-mobile,.table-of-contents,.hash-link").remove();
3423
3978
  root.find('button svg,a.hash-link svg,[aria-hidden="true"] svg,svg[aria-hidden="true"],svg[data-lucide],svg[data-icon]').remove();
3424
- if (root.find("h1").length > 1) record.issues.push({ code: "unsupported-component", source: record.url, message: "The article contains multiple level-one headings; review their hierarchy." });
3979
+ if (record.apiSpecification) {
3980
+ root.find('select[aria-label="Select schema type"],select[aria-label="\u9009\u62E9\u67B6\u6784\u7C7B\u578B"]').remove();
3981
+ root.find('.option-dropdown svg[data-component-part="icon-svg"]').remove();
3982
+ }
3425
3983
  const unsupported = [...new Set(root.find("iframe,object,embed,canvas,svg,form,input,textarea,select,math,video,audio").toArray().map((node) => node.tagName))];
3426
3984
  if (unsupported.length) record.issues.push({ code: "unsupported-component", source: record.url, message: `Rendered content requires explicit review for: ${unsupported.join(", ")}.` });
3427
- if (!states.length && unresolvedStaticControls(document.body)) record.issues.push({ code: "unsupported-component", source: record.url, message: "An interactive alternative has no complete captured panel or disclosure content." });
3985
+ 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." });
3428
3986
  }
3429
3987
  const suppliedAssets = new Map((document.assets ?? []).map((asset) => [canonical(asset.url, final), asset]));
3430
3988
  const assetUrls = [...new Set([...assetReferences(document), ...states.flatMap((state) => assetReferences(state.response))].map((url) => canonical(url, final)).filter((url) => Boolean(url)))];
@@ -3464,42 +4022,18 @@ ${difference}`;
3464
4022
  if (sanitizeRemoteMarkdown(page.body.replace(/^<a id="[^"<]*" \/>$/gm, "")) === null) throw new Error("Converted content did not pass inert MDX validation.");
3465
4023
  record.evidence = evidence;
3466
4024
  record.page = page;
3467
- const language = observedLocale(document, page.id, scopePath);
3468
4025
  record.issues.push(...language.issues);
3469
4026
  const canonicalLocale = language.locale;
3470
4027
  if (canonicalLocale) {
3471
4028
  page.locale = canonicalLocale;
3472
- const segments = page.id.split("/");
3473
- const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === canonicalLocale.toLowerCase());
3474
- page.navigationId = segments.filter((_segment, index) => index !== localeIndex).join("/") || "introduction";
4029
+ page.navigationId = navigationId;
3475
4030
  page.id = `${canonicalLocale}/${page.navigationId}`;
3476
- if (usedIds.has(page.id) && usedIds.get(page.id) !== record.url) throw new Error(`Destination collision for locale page ${page.id}.`);
3477
- usedIds.delete(id);
3478
- usedIds.set(page.id, record.url);
3479
4031
  }
3480
4032
  record.version = document.version ?? [...page.id.split("/"), ...document.finalUrl.pathname.split("/")].find((segment) => /^v\d+(?:\.\d+)*$/i.test(segment));
3481
4033
  record.pageSha256 = pageHash(page);
3482
4034
  record.outcome = "imported";
3483
4035
  imported++;
3484
- const navigation = checkpoint.navigation.length ? [] : sourceNavigation(document);
3485
- if (!checkpoint.navigation.length && navigation.length) {
3486
- checkpoint.navigation = navigation;
3487
- navigationChanged = true;
3488
- }
3489
- for (const link of evidence.links) {
3490
- try {
3491
- const url = new URL(link, final);
3492
- if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, final, record.url);
3493
- } catch {
3494
- }
3495
- }
3496
- const visit = (nodes) => {
3497
- for (const node of nodes) {
3498
- if (node.url) enqueue(node.url, final, "source-navigation");
3499
- visit(node.children ?? []);
3500
- }
3501
- };
3502
- visit(navigation);
4036
+ enqueueArticleLinks(evidence.links, final, record.url);
3503
4037
  } catch (error) {
3504
4038
  if (options.signal?.aborted) return;
3505
4039
  const wasImported = record.outcome === "imported";
@@ -3512,13 +4046,43 @@ ${difference}`;
3512
4046
  delete record.page;
3513
4047
  delete record.pageSha256;
3514
4048
  delete record.assets;
4049
+ delete record.apiSpecification;
3515
4050
  } finally {
4051
+ if (record.outcome !== "imported") {
4052
+ if (claimedId && usedIds.get(claimedId) === record.url) usedIds.delete(claimedId);
4053
+ if (claimedFinal && finalUrls.get(claimedFinal) === record.url) finalUrls.delete(claimedFinal);
4054
+ }
3516
4055
  if (record.outcome !== "pending") {
3517
4056
  if (options.onRecord) await options.onRecord(record);
3518
4057
  changed.push(record);
3519
4058
  }
3520
4059
  }
3521
4060
  };
4061
+ const settleRepresentations = () => {
4062
+ for (const record of records.values()) {
4063
+ if (!record.representationOf) continue;
4064
+ const primary = resolveMigrationRecord(records, records.get(record.representationOf));
4065
+ if (!primary) continue;
4066
+ const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
4067
+ if (!winner) continue;
4068
+ const alternate = winner === primary ? record : primary;
4069
+ if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
4070
+ if (alternate.outcome === "imported") {
4071
+ imported--;
4072
+ if (alternate.page && usedIds.get(alternate.page.id) === alternate.url) usedIds.delete(alternate.page.id);
4073
+ }
4074
+ alternate.outcome = "alias";
4075
+ alternate.aliasOf = winner.url;
4076
+ delete alternate.reason;
4077
+ delete alternate.page;
4078
+ delete alternate.pageSha256;
4079
+ delete alternate.assets;
4080
+ delete alternate.issues;
4081
+ delete alternate.apiSpecification;
4082
+ changed.push(alternate);
4083
+ }
4084
+ };
4085
+ settleRepresentations();
3522
4086
  while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
3523
4087
  if (imported >= maxPages) {
3524
4088
  const index = pending.findIndex((record, position) => position >= cursor && isDiscoveryIndex(new URL(record.url), source));
@@ -3528,17 +4092,18 @@ ${difference}`;
3528
4092
  const batch = pending.slice(cursor, cursor + Math.min(concurrency, imported >= maxPages ? 1 : maxPages - imported, (options.maxCaptures ?? Infinity) - cursor));
3529
4093
  cursor += batch.length;
3530
4094
  await Promise.all(batch.map(process));
4095
+ settleRepresentations();
3531
4096
  if (changed.length >= batchSize) await flush();
3532
4097
  }
3533
4098
  await flush();
3534
4099
  const projection = localeProjection(checkpoint);
3535
4100
  const routeMap = /* @__PURE__ */ new Map();
3536
4101
  for (const record of checkpoint.records) {
3537
- const target = record.outcome === "alias" ? records.get(record.aliasOf ?? "") : record;
3538
- if (target?.page) {
3539
- const route = projection.route(target.page);
4102
+ const target2 = resolveMigrationRecord(records, record);
4103
+ if (target2?.page) {
4104
+ const route = projection.route(target2.page);
3540
4105
  routeMap.set(record.url, route);
3541
- if (target.evidence) routeMap.set(canonical(target.evidence.finalUrl, sourceUrl), route);
4106
+ if (target2.evidence) routeMap.set(canonical(target2.evidence.finalUrl, sourceUrl), route);
3542
4107
  }
3543
4108
  }
3544
4109
  const navigationLabels = /* @__PURE__ */ new Map();
@@ -3546,11 +4111,11 @@ ${difference}`;
3546
4111
  const labelPages = (nodes) => {
3547
4112
  for (const node of nodes) {
3548
4113
  const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
3549
- const target = entry?.outcome === "alias" ? records.get(entry.aliasOf ?? "") : entry;
3550
- if (target?.page && node.label && !node.children?.length) {
3551
- const previous = navigationLabels.get(target.url);
3552
- if (previous && previous !== node.label) navigationIssues.push({ code: "unsupported-component", source: target.url, message: `This page has multiple source navigation labels (${previous}, ${node.label}); review the shared sidebar title.` });
3553
- else navigationLabels.set(target.url, node.label);
4114
+ const target2 = resolveMigrationRecord(records, entry);
4115
+ if (target2?.page && node.label && !node.children?.length) {
4116
+ const previous = navigationLabels.get(target2.url);
4117
+ 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.` });
4118
+ else navigationLabels.set(target2.url, node.label);
3554
4119
  }
3555
4120
  labelPages(node.children ?? []);
3556
4121
  }
@@ -3576,6 +4141,10 @@ ${difference}`;
3576
4141
  assets.set(asset.path, { path: asset.path, content: Buffer.from(content.buffer, content.byteOffset, content.byteLength) });
3577
4142
  }
3578
4143
  }
4144
+ for (const record of checkpoint.records) if (record.page && record.apiSpecification) {
4145
+ const spec = record.apiSpecification;
4146
+ assets.set(spec.path, { path: spec.path, projectRelative: true, content: Buffer.from(canonicalJson(spec.document)) });
4147
+ }
3579
4148
  const bundle = {
3580
4149
  sourceUrl,
3581
4150
  sourceKind: "url",
@@ -3583,13 +4152,16 @@ ${difference}`;
3583
4152
  pages,
3584
4153
  assets: [...assets.values()],
3585
4154
  docsConfig: navigationConfig(checkpoint.navigation, records) ?? buildNavigationFromPages(pages),
3586
- warnings: validation.issues.map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
4155
+ warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
3587
4156
  urlMigrationValidation: validation,
3588
4157
  stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
3589
4158
  };
3590
4159
  if (projection.locales.length) {
3591
4160
  const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
3592
- bundle.docsConfig.i18n = { defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
4161
+ bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
4162
+ }
4163
+ for (const spec of assets.values()) if (spec.projectRelative && spec.path.startsWith("openapi/")) {
4164
+ bundle.docsConfig.tabs.push({ tab: `Migration API ${spec.path.slice(-69, -5)}`, hidden: true, api: { source: spec.path, navigation: false } });
3593
4165
  }
3594
4166
  const stopReason = options.signal?.aborted ? "cancelled" : budgetExhausted ? "byte-limit" : cursor < pending.length && cursor >= (options.maxCaptures ?? Infinity) ? "capture-limit" : imported >= maxPages && cursor < pending.length ? "page-limit" : cursor < pending.length ? "capture-limit" : "complete";
3595
4167
  return { bundle, checkpoint, validation, stopReason };
@@ -3679,7 +4251,7 @@ var DOCUSAURUS_THEME_SHIMS = {
3679
4251
  };
3680
4252
  function applyReplacements(source, replacements) {
3681
4253
  return replacements.sort((a, b) => b.start - a.start).reduce(
3682
- (text, edit) => text.slice(0, edit.start) + edit.value + text.slice(edit.end),
4254
+ (text2, edit) => text2.slice(0, edit.start) + edit.value + text2.slice(edit.end),
3683
4255
  source
3684
4256
  );
3685
4257
  }
@@ -3808,18 +4380,18 @@ function mergeMigrationConfig(existing, incoming) {
3808
4380
  names.add(tab.tab.toLowerCase());
3809
4381
  continue;
3810
4382
  }
3811
- const target = tabs.find((item) => item.tab.toLowerCase() === tab.tab.toLowerCase());
3812
- if (!target) continue;
4383
+ const target2 = tabs.find((item) => item.tab.toLowerCase() === tab.tab.toLowerCase());
4384
+ if (!target2) continue;
3813
4385
  const incomingRootNodes = [
3814
4386
  ...uniqueTab.pages ?? [],
3815
4387
  ...uniqueTab.groups ?? []
3816
4388
  ];
3817
4389
  if (uniqueTab.pages?.length) {
3818
- target.pages = [...target.pages ?? target.groups ?? [], ...incomingRootNodes];
3819
- delete target.groups;
4390
+ target2.pages = [...target2.pages ?? target2.groups ?? [], ...incomingRootNodes];
4391
+ delete target2.groups;
3820
4392
  } else if (uniqueTab.groups?.length) {
3821
- if (target.pages) target.pages = [...target.pages, ...uniqueTab.groups];
3822
- else target.groups = [...target.groups ?? [], ...uniqueTab.groups];
4393
+ if (target2.pages) target2.pages = [...target2.pages, ...uniqueTab.groups];
4394
+ else target2.groups = [...target2.groups ?? [], ...uniqueTab.groups];
3823
4395
  }
3824
4396
  }
3825
4397
  const changelog = existing.tabs.find((tab) => tab.tab.toLowerCase() === "changelog") ?? incoming.tabs.find((tab) => tab.tab.toLowerCase() === "changelog");