@j0hanz/fetch-url-mcp 1.10.2 → 1.10.4

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.
Files changed (63) hide show
  1. package/dist/http/native.d.ts.map +1 -1
  2. package/dist/http/native.js +20 -26
  3. package/dist/http/session-teardown.d.ts +17 -0
  4. package/dist/http/session-teardown.d.ts.map +1 -0
  5. package/dist/http/session-teardown.js +30 -0
  6. package/dist/lib/code-lang.d.ts.map +1 -1
  7. package/dist/lib/code-lang.js +2 -8
  8. package/dist/lib/core.d.ts.map +1 -1
  9. package/dist/lib/core.js +2 -0
  10. package/dist/lib/dom-prep.d.ts +2 -0
  11. package/dist/lib/dom-prep.d.ts.map +1 -1
  12. package/dist/lib/dom-prep.js +150 -139
  13. package/dist/lib/md-cleanup.d.ts.map +1 -1
  14. package/dist/lib/md-cleanup.js +53 -20
  15. package/dist/lib/progress.d.ts.map +1 -1
  16. package/dist/lib/progress.js +59 -32
  17. package/dist/lib/sdk-interop.d.ts +8 -0
  18. package/dist/lib/sdk-interop.d.ts.map +1 -0
  19. package/dist/lib/sdk-interop.js +73 -0
  20. package/dist/lib/task-handlers.d.ts +0 -2
  21. package/dist/lib/task-handlers.d.ts.map +1 -1
  22. package/dist/lib/task-handlers.js +9 -94
  23. package/dist/resources/index.js +1 -1
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +10 -3
  26. package/dist/tasks/call-contract.d.ts +25 -0
  27. package/dist/tasks/call-contract.d.ts.map +1 -0
  28. package/dist/tasks/call-contract.js +58 -0
  29. package/dist/tasks/cursor-codec.d.ts +5 -0
  30. package/dist/tasks/cursor-codec.d.ts.map +1 -0
  31. package/dist/tasks/cursor-codec.js +41 -0
  32. package/dist/tasks/execution.d.ts +1 -20
  33. package/dist/tasks/execution.d.ts.map +1 -1
  34. package/dist/tasks/execution.js +7 -32
  35. package/dist/tasks/manager.d.ts +1 -5
  36. package/dist/tasks/manager.d.ts.map +1 -1
  37. package/dist/tasks/manager.js +16 -144
  38. package/dist/tasks/owner.d.ts +12 -2
  39. package/dist/tasks/owner.d.ts.map +1 -1
  40. package/dist/tasks/owner.js +52 -3
  41. package/dist/tasks/tool-registry.d.ts +1 -0
  42. package/dist/tasks/tool-registry.d.ts.map +1 -1
  43. package/dist/tasks/waiters.d.ts +27 -0
  44. package/dist/tasks/waiters.d.ts.map +1 -0
  45. package/dist/tasks/waiters.js +113 -0
  46. package/dist/tools/fetch-url-progress.d.ts +17 -0
  47. package/dist/tools/fetch-url-progress.d.ts.map +1 -0
  48. package/dist/tools/fetch-url-progress.js +78 -0
  49. package/dist/tools/fetch-url.d.ts +5 -9
  50. package/dist/tools/fetch-url.d.ts.map +1 -1
  51. package/dist/tools/fetch-url.js +43 -96
  52. package/dist/transform/next-flight.d.ts +2 -0
  53. package/dist/transform/next-flight.d.ts.map +1 -0
  54. package/dist/transform/next-flight.js +285 -0
  55. package/dist/transform/title-policy.d.ts +12 -0
  56. package/dist/transform/title-policy.d.ts.map +1 -0
  57. package/dist/transform/title-policy.js +85 -0
  58. package/dist/transform/transform.d.ts.map +1 -1
  59. package/dist/transform/transform.js +62 -475
  60. package/package.json +4 -4
  61. package/dist/tools/index.d.ts +0 -3
  62. package/dist/tools/index.d.ts.map +0 -1
  63. package/dist/tools/index.js +0 -4
@@ -3,10 +3,10 @@ import diagnosticsChannel from 'node:diagnostics_channel';
3
3
  import { performance } from 'node:perf_hooks';
4
4
  import { isProbablyReaderable, Readability } from '@mozilla/readability';
5
5
  import { parseHTML } from 'linkedom';
6
- import { detectLanguageFromCode, extractLanguageFromClassName, } from '../lib/code-lang.js';
6
+ import { extractLanguageFromClassName } from '../lib/code-lang.js';
7
7
  import { config } from '../lib/core.js';
8
8
  import { getOperationId, getRequestId, logDebug, logError, logInfo, logWarn, redactUrl, } from '../lib/core.js';
9
- import { prepareDocumentForMarkdown, removeNoiseFromHtml, serializeDocumentForMarkdown, } from '../lib/dom-prep.js';
9
+ import { normalizeTabContent, prepareDocumentForMarkdown, removeNoiseFromHtml, serializeDocumentForMarkdown, } from '../lib/dom-prep.js';
10
10
  import { isRawTextContentUrl } from '../lib/http.js';
11
11
  import { cleanupMarkdownArtifacts, processFencedContent, } from '../lib/md-cleanup.js';
12
12
  import { addSourceToMarkdown, buildMetadataFooter, extractTitleFromRawMarkdown, isRawTextContent, } from '../lib/md-metadata.js';
@@ -15,6 +15,8 @@ import { FetchError, getErrorMessage, toError } from '../lib/utils.js';
15
15
  import { isObject } from '../lib/utils.js';
16
16
  import { translateHtmlFragmentToMarkdown } from './html-translators.js';
17
17
  import { extractMetadata, extractMetadataFromHead, mergeMetadata, normalizeDocumentTitle, } from './metadata.js';
18
+ import { supplementMarkdownFromNextFlight } from './next-flight.js';
19
+ import { isGithubRepositoryRootUrl, maybePrependSyntheticTitle, maybeStripGithubPrimaryHeading, normalizeSyntheticTitleToken, shouldPreferPrimaryHeadingTitle, } from './title-policy.js';
18
20
  import { getOrCreateWorkerPool, getWorkerPoolStats, shutdownWorkerPool, } from './worker-pool.js';
19
21
  function decodeInput(input, encoding) {
20
22
  if (typeof input === 'string')
@@ -40,13 +42,6 @@ function asError(value) {
40
42
  function isWhitespaceChar(code) {
41
43
  return code === 9 || code === 10 || code === 12 || code === 13 || code === 32;
42
44
  }
43
- function containsWhitespace(value) {
44
- for (let i = 0; i < value.length; i += 1) {
45
- if (isWhitespaceChar(value.charCodeAt(i)))
46
- return true;
47
- }
48
- return false;
49
- }
50
45
  function buildTransformSignal(signal) {
51
46
  const { timeoutMs } = config.transform;
52
47
  if (timeoutMs <= 0)
@@ -302,6 +297,10 @@ function preserveCodeLanguageAttributes(doc) {
302
297
  el.setAttribute('data-language', lang);
303
298
  }
304
299
  }
300
+ // Pre-Readability cleanup on a cloned document.
301
+ // Must strip tabs/breadcrumbs before Readability mangles role attributes.
302
+ // The original document is NOT yet prepared (prepareDocumentForMarkdown
303
+ // runs later in buildContentSource), so this clone starts from raw HTML.
305
304
  function extractArticle(document, url, signal) {
306
305
  if (!isReadabilityCompatible(document)) {
307
306
  logWarn('Document not compatible with Readability');
@@ -333,6 +332,7 @@ function extractArticle(document, url, signal) {
333
332
  : doc;
334
333
  preserveAlertElements(readabilityDoc);
335
334
  preserveCodeLanguageAttributes(readabilityDoc);
335
+ normalizeTabContent(readabilityDoc);
336
336
  for (const el of readabilityDoc.querySelectorAll('[class*="breadcrumb"],[class*="pagination"]')) {
337
337
  el.remove();
338
338
  }
@@ -453,8 +453,12 @@ export function extractContent(html, url, options = {
453
453
  }
454
454
  function resolveRelativeHref(href, baseUrl, origin) {
455
455
  const trimmedHref = href.trim();
456
- if (!trimmedHref || containsWhitespace(trimmedHref))
456
+ if (!trimmedHref)
457
457
  return href;
458
+ for (let i = 0; i < trimmedHref.length; i += 1) {
459
+ if (isWhitespaceChar(trimmedHref.charCodeAt(i)))
460
+ return href;
461
+ }
458
462
  if (isAbsoluteOrSpecialUrl(trimmedHref))
459
463
  return trimmedHref;
460
464
  try {
@@ -628,31 +632,30 @@ function tryTransformRawContent(params) {
628
632
  }
629
633
  const MIN_CONTENT_RATIO = 0.15;
630
634
  const MIN_HTML_LENGTH_FOR_GATE = 100;
631
- const MIN_HEADING_RETENTION_RATIO = 0.3;
632
- const MIN_CODE_BLOCK_RETENTION_RATIO = 0.15;
633
- const MIN_TABLE_RETENTION_RATIO = 0.5;
634
- const MIN_IMAGE_RETENTION_RATIO = 0.2;
635
- const MIN_INTERACTIVE_RETENTION_RATIO = 0.1;
636
- const MIN_INTERACTIVE_ELEMENTS_FOR_GATE = 6;
637
- const MIN_IMAGE_ELEMENTS_FOR_GATE = 4;
635
+ const RETENTION_RULES = [
636
+ { selector: 'h1,h2,h3,h4,h5,h6', minOriginal: 1, ratio: 0.3 },
637
+ { selector: 'pre', minOriginal: 1, ratio: 0.15 },
638
+ { selector: 'table', minOriginal: 1, ratio: 0.5 },
639
+ { selector: 'img', minOriginal: 4, ratio: 0.2 },
640
+ {
641
+ selector: 'button,[role="tab"],[role="tabpanel"],[aria-controls]',
642
+ minOriginal: 6,
643
+ ratio: 0.1,
644
+ },
645
+ ];
638
646
  const MIN_HEADINGS_FOR_EMPTY_SECTION_GATE = 5;
639
647
  const MAX_EMPTY_SECTION_RATIO = 0.05;
640
648
  const MIN_LINE_LENGTH_FOR_TRUNCATION_CHECK = 20;
641
649
  const MAX_TRUNCATED_LINE_RATIO = 0.95;
642
- function needsDocumentWrapper(html) {
643
- const trimmed = html.trim().toLowerCase();
644
- return (!trimmed.startsWith('<!doctype') &&
645
- !trimmed.startsWith('<html') &&
646
- !trimmed.startsWith('<body'));
647
- }
648
- function wrapHtmlFragment(html) {
649
- return `<!DOCTYPE html><html><body>${html}</body></html>`;
650
- }
651
650
  function resolveHtmlDocument(htmlOrDocument) {
652
651
  if (typeof htmlOrDocument !== 'string')
653
652
  return htmlOrDocument;
654
- const htmlToParse = needsDocumentWrapper(htmlOrDocument)
655
- ? wrapHtmlFragment(htmlOrDocument)
653
+ const trimmed = htmlOrDocument.trim().toLowerCase();
654
+ const needsWrapper = !trimmed.startsWith('<!doctype') &&
655
+ !trimmed.startsWith('<html') &&
656
+ !trimmed.startsWith('<body');
657
+ const htmlToParse = needsWrapper
658
+ ? `<!DOCTYPE html><html><body>${htmlOrDocument}</body></html>`
656
659
  : htmlOrDocument;
657
660
  try {
658
661
  return parseHTML(htmlToParse).document;
@@ -662,14 +665,6 @@ function resolveHtmlDocument(htmlOrDocument) {
662
665
  return parseHTML('<!DOCTYPE html><html><body></body></html>').document;
663
666
  }
664
667
  }
665
- function stripNonVisibleNodes(root) {
666
- for (const el of root.querySelectorAll('script,style,noscript')) {
667
- el.remove();
668
- }
669
- }
670
- function resolveNodeText(node) {
671
- return node.textContent ?? '';
672
- }
673
668
  function getTextContentSkippingHidden(node, parts) {
674
669
  const { nodeType } = node;
675
670
  if (nodeType === 3) {
@@ -700,8 +695,10 @@ function getTextContentSkippingHidden(node, parts) {
700
695
  function getVisibleTextLength(htmlOrDocument) {
701
696
  if (typeof htmlOrDocument === 'string') {
702
697
  const doc = resolveHtmlDocument(htmlOrDocument);
703
- stripNonVisibleNodes(doc.body);
704
- return resolveNodeText(doc.body).replace(/\s+/g, ' ').trim().length;
698
+ for (const el of doc.body.querySelectorAll('script,style,noscript')) {
699
+ el.remove();
700
+ }
701
+ return (doc.body.textContent || '').replace(/\s+/g, ' ').trim().length;
705
702
  }
706
703
  const parts = [];
707
704
  getTextContentSkippingHidden(htmlOrDocument.body, parts);
@@ -765,7 +762,6 @@ function hasTruncatedSentences(text) {
765
762
  return incompleteFound / linesFound > MAX_TRUNCATED_LINE_RATIO;
766
763
  }
767
764
  const MIN_CONTENT_ROOT_LENGTH = 100;
768
- const HEADING_SCAN_LIMIT = 12;
769
765
  const BINARY_SAMPLE_SIZE = 2000;
770
766
  export function determineContentExtractionSource(article) {
771
767
  return article !== null;
@@ -817,22 +813,6 @@ const PRIMARY_HEADING_ROOT_SELECTORS = [
817
813
  '.entry-content',
818
814
  '[itemprop="text"]',
819
815
  ];
820
- function normalizeSyntheticTitleToken(value) {
821
- return (value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
822
- }
823
- function shouldPreferPrimaryHeadingTitle(primaryHeading, title) {
824
- const primary = normalizeSyntheticTitleToken(primaryHeading);
825
- if (!primary)
826
- return false;
827
- const normalizedTitle = normalizeSyntheticTitleToken(title);
828
- if (!normalizedTitle)
829
- return true;
830
- if (normalizedTitle === primary)
831
- return true;
832
- return normalizedTitle
833
- .split(/\s*(?:[-|:•·]|–|—)\s*/u)
834
- .some((part) => part === primary);
835
- }
836
816
  function findContentRoot(document) {
837
817
  for (const selector of CONTENT_ROOT_SELECTORS) {
838
818
  const element = document.querySelector(selector);
@@ -911,93 +891,36 @@ function countEmptyHeadingSections(root) {
911
891
  }
912
892
  return emptyCount;
913
893
  }
914
- function isGithubRepositoryRootUrl(url) {
915
- let parsed;
916
- try {
917
- parsed = new URL(url);
918
- }
919
- catch {
920
- return false;
921
- }
922
- const hostname = parsed.hostname.toLowerCase();
923
- if (hostname !== 'github.com' && hostname !== 'www.github.com') {
924
- return false;
925
- }
926
- return parsed.pathname.split('/').filter(Boolean).length === 2;
927
- }
928
894
  const TransformHeuristics = {
929
895
  findContentRoot,
930
896
  findPrimaryHeading,
931
897
  isGithubRepositoryRootUrl,
932
898
  };
933
- const ARTICLE_INTERACTIVE_SELECTOR = 'button,[role="tab"],[role="tabpanel"],[aria-controls]';
934
- function buildArticleDocument(article) {
935
- return parseHTML(`<!DOCTYPE html><html><body>${article.content}</body></html>`).document;
936
- }
937
- function hasSufficientArticleContentRatio(article, document) {
938
- const originalLength = getVisibleTextLength(document);
939
- if (originalLength < MIN_HTML_LENGTH_FOR_GATE)
940
- return true;
941
- return article.textContent.length / originalLength >= MIN_CONTENT_RATIO;
942
- }
943
- function retainsEnoughHeadings(articleDoc, document) {
944
- const originalHeadings = countMatchingElements(document, 'h1,h2,h3,h4,h5,h6');
945
- if (originalHeadings === 0)
946
- return true;
947
- const articleHeadings = countMatchingElements(articleDoc, 'h1,h2,h3,h4,h5,h6');
948
- return articleHeadings / originalHeadings >= MIN_HEADING_RETENTION_RATIO;
949
- }
950
- function retainsEnoughCodeBlocks(articleDoc, document) {
951
- const originalCodeBlocks = countMatchingElements(document, 'pre');
952
- if (originalCodeBlocks === 0)
953
- return true;
954
- const articleCodeBlocks = countMatchingElements(articleDoc, 'pre');
955
- return (articleCodeBlocks / originalCodeBlocks >= MIN_CODE_BLOCK_RETENTION_RATIO);
956
- }
957
- function retainsEnoughTables(articleDoc, document) {
958
- const originalTables = countMatchingElements(document, 'table');
959
- if (originalTables === 0)
960
- return true;
961
- const articleTables = countMatchingElements(articleDoc, 'table');
962
- return articleTables / originalTables >= MIN_TABLE_RETENTION_RATIO;
963
- }
964
- function retainsEnoughImages(articleDoc, document) {
965
- const originalImages = countMatchingElements(document, 'img');
966
- if (originalImages < MIN_IMAGE_ELEMENTS_FOR_GATE)
967
- return true;
968
- const articleImages = countMatchingElements(articleDoc, 'img');
969
- return articleImages / originalImages >= MIN_IMAGE_RETENTION_RATIO;
970
- }
971
- function retainsEnoughInteractiveElements(articleDoc, document) {
972
- const originalInteractive = countMatchingElements(document, ARTICLE_INTERACTIVE_SELECTOR);
973
- if (originalInteractive < MIN_INTERACTIVE_ELEMENTS_FOR_GATE)
974
- return true;
975
- const articleInteractive = countMatchingElements(articleDoc, ARTICLE_INTERACTIVE_SELECTOR);
976
- return (articleInteractive / originalInteractive >= MIN_INTERACTIVE_RETENTION_RATIO);
977
- }
978
- function hasAcceptableEmptySectionRatio(articleDoc) {
979
- const articleHeadings = countMatchingElements(articleDoc, 'h1,h2,h3,h4,h5,h6');
980
- if (articleHeadings < MIN_HEADINGS_FOR_EMPTY_SECTION_GATE)
981
- return true;
982
- const emptySectionRatio = countEmptyHeadingSections(articleDoc) / articleHeadings;
983
- return emptySectionRatio <= MAX_EMPTY_SECTION_RATIO;
984
- }
985
899
  function shouldUseArticleContent(article, document) {
986
- if (!hasSufficientArticleContentRatio(article, document))
987
- return false;
988
- const articleDoc = buildArticleDocument(article);
989
- if (!retainsEnoughHeadings(articleDoc, document))
990
- return false;
991
- if (!retainsEnoughCodeBlocks(articleDoc, document))
992
- return false;
993
- if (!retainsEnoughTables(articleDoc, document))
994
- return false;
995
- if (!retainsEnoughImages(articleDoc, document))
996
- return false;
997
- if (!retainsEnoughInteractiveElements(articleDoc, document))
998
- return false;
999
- if (!hasAcceptableEmptySectionRatio(articleDoc))
900
+ // Content ratio gate
901
+ const originalLength = getVisibleTextLength(document);
902
+ if (originalLength >= MIN_HTML_LENGTH_FOR_GATE) {
903
+ if (article.textContent.length / originalLength < MIN_CONTENT_RATIO)
904
+ return false;
905
+ }
906
+ const articleDoc = parseHTML(`<!DOCTYPE html><html><body>${article.content}</body></html>`).document;
907
+ // Retention checks
908
+ const passesRetention = RETENTION_RULES.every(({ selector, minOriginal, ratio }) => {
909
+ const original = countMatchingElements(document, selector);
910
+ if (original < minOriginal)
911
+ return true;
912
+ return countMatchingElements(articleDoc, selector) / original >= ratio;
913
+ });
914
+ if (!passesRetention)
1000
915
  return false;
916
+ // Empty section ratio
917
+ const articleHeadings = countMatchingElements(articleDoc, 'h1,h2,h3,h4,h5,h6');
918
+ if (articleHeadings >= MIN_HEADINGS_FOR_EMPTY_SECTION_GATE) {
919
+ if (countEmptyHeadingSections(articleDoc) / articleHeadings >
920
+ MAX_EMPTY_SECTION_RATIO) {
921
+ return false;
922
+ }
923
+ }
1001
924
  return !hasTruncatedSentences(article.textContent);
1002
925
  }
1003
926
  function buildContentSource(params) {
@@ -1008,6 +931,7 @@ function buildContentSource(params) {
1008
931
  ? TransformHeuristics.findPrimaryHeading(document)
1009
932
  : undefined;
1010
933
  if (preparedDocument) {
934
+ // Post-extraction cleanup on the original document (non-article path)
1011
935
  prepareDocumentForMarkdown(preparedDocument, url, signal);
1012
936
  primaryHeading =
1013
937
  TransformHeuristics.findPrimaryHeading(preparedDocument) ??
@@ -1023,6 +947,8 @@ function buildContentSource(params) {
1023
947
  };
1024
948
  if (useArticleContent && article) {
1025
949
  const { document: articleDoc } = parseHTML(`<!DOCTYPE html><html><body>${article.content}</body></html>`);
950
+ // Runs on Readability output — tab/noise ops are no-ops here,
951
+ // but table normalization and URL resolution still apply.
1026
952
  prepareDocumentForMarkdown(articleDoc, url, signal);
1027
953
  const articleTitle = article.title !== undefined
1028
954
  ? normalizeDocumentTitle(article.title, url)
@@ -1061,290 +987,6 @@ function buildContentSource(params) {
1061
987
  title: extractedMeta.title,
1062
988
  };
1063
989
  }
1064
- const NEXT_FLIGHT_PAYLOAD_RE = /self\.__next_f\.push\(\[1,"((?:\\.|[^"\\])*)"\]\)<\/script>/gs;
1065
- const TEMPLATE_ASSIGNMENT_RE = /([A-Za-z_$][\w$]*)=`([\s\S]*?)`;/g;
1066
- const OBJECT_ASSIGNMENT_RE = /([A-Za-z_$][\w$]*)=\{([^{}]+)\}/g;
1067
- const FLIGHT_INSTALL_RE = /commands:\{cli:"([^"]+)",npm:"([^"]+)",yarn:"([^"]+)",pnpm:"([^"]+)",bun:"([^"]+)"\}/;
1068
- const FLIGHT_IMPORT_RE = /commands:\{main:'([^']+)',individual:'([^']+)'\}/;
1069
- const FLIGHT_DEMO_RE = /title:"([^"]+)",files:([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)/g;
1070
- const FLIGHT_API_RE = /children:"([^"]+)"\}\),`\\n`,\(0,e\.jsx\)\(o,\{data:\[([\s\S]*?)\]\}\)/g;
1071
- const FLIGHT_API_ROW_RE = /attribute:"([^"]+)",type:"([^"]+)",description:"([^"]*)",default:"([^"]*)"/g;
1072
- const FLIGHT_MERMAID_SECTION_RE = /_jsx\(Heading,\{\s*level:"[1-6]",\s*id:"[^"]+",\s*children:"((?:\\.|[^"\\])*)"\s*\}\)(?:(?!_jsx\(Heading,\{)[\s\S]){0,12000}?_jsx\(Mermaid,\{\s*chart:"((?:\\.|[^"\\])*)"\s*\}\)/g;
1073
- function decodeHtmlEntities(value) {
1074
- return value
1075
- .replace(/&#39;|&#x27;/g, "'")
1076
- .replace(/&quot;/g, '"')
1077
- .replace(/&amp;/g, '&')
1078
- .replace(/&lt;/g, '<')
1079
- .replace(/&gt;/g, '>');
1080
- }
1081
- function decodeFlightStringValue(value) {
1082
- try {
1083
- return JSON.parse(`"${value}"`);
1084
- }
1085
- catch {
1086
- return decodeHtmlEntities(value);
1087
- }
1088
- }
1089
- function decodeNextFlightPayloads(html) {
1090
- const payloads = [];
1091
- for (const match of html.matchAll(NEXT_FLIGHT_PAYLOAD_RE)) {
1092
- const rawPayload = match[1];
1093
- if (!rawPayload)
1094
- continue;
1095
- try {
1096
- payloads.push(JSON.parse(`"${rawPayload}"`));
1097
- }
1098
- catch {
1099
- // Ignore malformed payload fragments and continue with the rest.
1100
- }
1101
- }
1102
- return payloads;
1103
- }
1104
- function parseFlightObjectRefs(text) {
1105
- const templateMap = new Map();
1106
- const aliasMap = new Map();
1107
- const objectMaps = new Map();
1108
- for (const match of text.matchAll(TEMPLATE_ASSIGNMENT_RE)) {
1109
- const name = match[1];
1110
- const code = match[2];
1111
- if (name && code)
1112
- templateMap.set(name, decodeHtmlEntities(code));
1113
- }
1114
- for (const match of text.matchAll(OBJECT_ASSIGNMENT_RE)) {
1115
- const objectName = match[1];
1116
- const body = match[2]?.trim() ?? '';
1117
- if (!objectName || !body)
1118
- continue;
1119
- const spreadMatch = /^\.\.\.([A-Za-z_$][\w$]*)$/.exec(body);
1120
- if (spreadMatch?.[1]) {
1121
- aliasMap.set(objectName, spreadMatch[1]);
1122
- continue;
1123
- }
1124
- const entries = new Map();
1125
- for (const part of body.split(',')) {
1126
- const entryMatch = /(?:"([^"]+)"|([A-Za-z_$][\w$]*)):([A-Za-z_$][\w$]*)$/.exec(part.trim());
1127
- const key = entryMatch?.[1] ?? entryMatch?.[2];
1128
- const value = entryMatch?.[3];
1129
- if (key && value)
1130
- entries.set(key, value);
1131
- }
1132
- if (entries.size > 0)
1133
- objectMaps.set(objectName, entries);
1134
- }
1135
- return { templateMap, aliasMap, objectMaps };
1136
- }
1137
- function resolveFlightCodeRef(name, refs, seen = new Set()) {
1138
- if (!name || seen.has(name))
1139
- return undefined;
1140
- seen.add(name);
1141
- const direct = refs.templateMap.get(name);
1142
- if (direct)
1143
- return direct;
1144
- const alias = refs.aliasMap.get(name);
1145
- if (alias)
1146
- return resolveFlightCodeRef(alias, refs, seen);
1147
- const objectMap = refs.objectMaps.get(name);
1148
- if (!objectMap)
1149
- return undefined;
1150
- for (const ref of objectMap.values()) {
1151
- const resolved = resolveFlightCodeRef(ref, refs, seen);
1152
- if (resolved)
1153
- return resolved;
1154
- }
1155
- return undefined;
1156
- }
1157
- function escapeMarkdownTableCell(value) {
1158
- const normalized = decodeHtmlEntities(value).replace(/\s+/g, ' ').trim();
1159
- return (normalized || '-').replace(/\|/g, '\\|');
1160
- }
1161
- function buildMarkdownTable(rows) {
1162
- if (rows.length === 0)
1163
- return '';
1164
- const lines = [
1165
- '| Prop | Type | Description | Default |',
1166
- '| ---- | ---- | ----------- | ------- |',
1167
- ];
1168
- for (const row of rows) {
1169
- lines.push(`| ${escapeMarkdownTableCell(row.attribute)} | ${escapeMarkdownTableCell(row.type)} | ${escapeMarkdownTableCell(row.description)} | ${escapeMarkdownTableCell(row.defaultValue)} |`);
1170
- }
1171
- return lines.join('\n');
1172
- }
1173
- function buildCodeBlock(code) {
1174
- const trimmed = code.trim();
1175
- if (!trimmed)
1176
- return '';
1177
- const language = detectLanguageFromCode(trimmed) ?? 'tsx';
1178
- return `\`\`\`${language}\n${trimmed}\n\`\`\``;
1179
- }
1180
- function buildMermaidBlock(chart) {
1181
- const normalized = decodeFlightStringValue(chart).trim();
1182
- if (!normalized)
1183
- return '';
1184
- return `\`\`\`mermaid\n${normalized}\n\`\`\``;
1185
- }
1186
- function normalizeSupplementHeadingText(value) {
1187
- return value
1188
- .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
1189
- .replace(/\s+/g, ' ')
1190
- .trim()
1191
- .toLowerCase();
1192
- }
1193
- function getMarkdownHeadingInfo(line) {
1194
- const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line.trim());
1195
- if (!match)
1196
- return null;
1197
- return {
1198
- level: match[1]?.length ?? 0,
1199
- title: normalizeSupplementHeadingText(match[2] ?? ''),
1200
- };
1201
- }
1202
- function findMarkdownSection(lines, title) {
1203
- const target = normalizeSupplementHeadingText(title);
1204
- for (let i = 0; i < lines.length; i += 1) {
1205
- const heading = getMarkdownHeadingInfo(lines[i] ?? '');
1206
- if (heading?.title !== target)
1207
- continue;
1208
- let end = lines.length;
1209
- for (let j = i + 1; j < lines.length; j += 1) {
1210
- const nextLine = lines[j];
1211
- const nextHeading = nextLine !== undefined ? getMarkdownHeadingInfo(nextLine) : null;
1212
- if (nextHeading && nextHeading.level <= heading.level) {
1213
- end = j;
1214
- break;
1215
- }
1216
- }
1217
- return { start: i, end };
1218
- }
1219
- return null;
1220
- }
1221
- function getSectionBody(lines, section) {
1222
- return lines
1223
- .slice(section.start + 1, section.end)
1224
- .join('\n')
1225
- .trim();
1226
- }
1227
- function replaceMarkdownSection(lines, title, body) {
1228
- const section = findMarkdownSection(lines, title);
1229
- if (!section)
1230
- return false;
1231
- const replacement = body.trim().length > 0 ? ['', ...body.trim().split('\n'), ''] : [''];
1232
- lines.splice(section.start + 1, section.end - section.start - 1, ...replacement);
1233
- return true;
1234
- }
1235
- function appendMarkdownSection(lines, title, body) {
1236
- const section = findMarkdownSection(lines, title);
1237
- if (!section)
1238
- return false;
1239
- const bodyText = getSectionBody(lines, section);
1240
- if (bodyText.includes('```'))
1241
- return false;
1242
- const nextBody = bodyText ? `${bodyText}\n\n${body.trim()}` : body.trim();
1243
- return replaceMarkdownSection(lines, title, nextBody);
1244
- }
1245
- function extractNextFlightSupplement(originalHtml) {
1246
- const payloads = decodeNextFlightPayloads(originalHtml);
1247
- if (payloads.length === 0)
1248
- return null;
1249
- const text = payloads.join('\n');
1250
- const refs = parseFlightObjectRefs(text);
1251
- const installMatch = FLIGHT_INSTALL_RE.exec(text);
1252
- const importMatch = FLIGHT_IMPORT_RE.exec(text);
1253
- const apiTables = new Map();
1254
- for (const match of text.matchAll(FLIGHT_API_RE)) {
1255
- const title = match[1];
1256
- const rawRows = match[2] ?? '';
1257
- if (!title)
1258
- continue;
1259
- const rows = [];
1260
- for (const rowMatch of rawRows.matchAll(FLIGHT_API_ROW_RE)) {
1261
- const attribute = rowMatch[1];
1262
- const type = rowMatch[2];
1263
- const description = rowMatch[3];
1264
- const defaultValue = rowMatch[4];
1265
- if (!attribute ||
1266
- !type ||
1267
- description === undefined ||
1268
- defaultValue === undefined) {
1269
- continue;
1270
- }
1271
- rows.push({ attribute, type, description, defaultValue });
1272
- }
1273
- const table = buildMarkdownTable(rows);
1274
- if (table)
1275
- apiTables.set(title, table);
1276
- }
1277
- const mermaidDiagrams = new Map();
1278
- for (const match of text.matchAll(FLIGHT_MERMAID_SECTION_RE)) {
1279
- const title = match[1] ? decodeFlightStringValue(match[1]).trim() : '';
1280
- const chart = match[2] ? buildMermaidBlock(match[2]) : '';
1281
- if (title && chart)
1282
- mermaidDiagrams.set(title, chart);
1283
- }
1284
- const demoCodeBlocks = new Map();
1285
- for (const match of text.matchAll(FLIGHT_DEMO_RE)) {
1286
- const title = match[1];
1287
- const objectName = match[2];
1288
- const key = match[3];
1289
- const ref = objectName
1290
- ? refs.objectMaps.get(objectName)?.get(key ?? '')
1291
- : undefined;
1292
- const code = resolveFlightCodeRef(ref, refs);
1293
- const codeBlock = code ? buildCodeBlock(code) : '';
1294
- if (title && codeBlock)
1295
- demoCodeBlocks.set(title, codeBlock);
1296
- }
1297
- return {
1298
- ...(installMatch ? { installationCommands: installMatch.slice(1) } : {}),
1299
- ...(importMatch ? { importCommands: importMatch.slice(1) } : {}),
1300
- apiTables,
1301
- demoCodeBlocks,
1302
- mermaidDiagrams,
1303
- };
1304
- }
1305
- function supplementMarkdownFromNextFlight(markdown, originalHtml) {
1306
- const supplement = extractNextFlightSupplement(originalHtml);
1307
- if (!supplement)
1308
- return markdown;
1309
- const lines = markdown.split('\n');
1310
- if (supplement.installationCommands?.length) {
1311
- const installationSection = findMarkdownSection(lines, 'Installation');
1312
- if (installationSection) {
1313
- const installBody = getSectionBody(lines, installationSection);
1314
- if (!/(npm|pnpm|yarn|bun|npx)\s+(install|add)/.test(installBody)) {
1315
- appendMarkdownSection(lines, 'Installation', buildCodeBlock(supplement.installationCommands.join('\n')));
1316
- }
1317
- }
1318
- }
1319
- if (supplement.importCommands?.length) {
1320
- const importSection = findMarkdownSection(lines, 'Import');
1321
- if (importSection) {
1322
- const importBody = getSectionBody(lines, importSection);
1323
- if (!/import\s+\{/.test(importBody)) {
1324
- appendMarkdownSection(lines, 'Import', buildCodeBlock(supplement.importCommands.join('\n\n')));
1325
- }
1326
- }
1327
- }
1328
- for (const [title, table] of supplement.apiTables) {
1329
- replaceMarkdownSection(lines, title, table);
1330
- }
1331
- for (const [title, mermaidBlock] of supplement.mermaidDiagrams) {
1332
- const section = findMarkdownSection(lines, title);
1333
- if (!section)
1334
- continue;
1335
- const sectionBody = getSectionBody(lines, section);
1336
- if (sectionBody.includes('```mermaid'))
1337
- continue;
1338
- const nextBody = sectionBody
1339
- ? `${sectionBody}\n\n${mermaidBlock}`
1340
- : mermaidBlock;
1341
- replaceMarkdownSection(lines, title, nextBody);
1342
- }
1343
- for (const [title, codeBlock] of supplement.demoCodeBlocks) {
1344
- appendMarkdownSection(lines, title, codeBlock);
1345
- }
1346
- return lines.join('\n');
1347
- }
1348
990
  function resolveContentSource(params) {
1349
991
  const { article, metadata: extractedMeta, document, truncated, } = extractContentContext(params.html, params.url, {
1350
992
  extractArticle: true,
@@ -1366,33 +1008,6 @@ function resolveContentSource(params) {
1366
1008
  ...(params.signal ? { signal: params.signal } : {}),
1367
1009
  });
1368
1010
  }
1369
- function shouldStripGithubPrimaryHeading(context, url) {
1370
- return (context.primaryHeading !== undefined &&
1371
- TransformHeuristics.isGithubRepositoryRootUrl(url));
1372
- }
1373
- function maybeStripGithubPrimaryHeading(markdown, context, url) {
1374
- if (!shouldStripGithubPrimaryHeading(context, url))
1375
- return markdown;
1376
- return stripLeadingHeading(markdown, context.primaryHeading ?? '');
1377
- }
1378
- function buildSyntheticTitlePrefix(url, favicon, suppressFavicon) {
1379
- if (!favicon || suppressFavicon)
1380
- return ' ';
1381
- let alt = '';
1382
- try {
1383
- alt = new URL(url).hostname;
1384
- }
1385
- catch {
1386
- /* skip */
1387
- }
1388
- return ` ![${alt}](${favicon}) `;
1389
- }
1390
- function maybePrependSyntheticTitle(markdown, context, url) {
1391
- if (!context.title || /^(#{1,6})\s/.test(markdown.trimStart())) {
1392
- return markdown;
1393
- }
1394
- return `#${buildSyntheticTitlePrefix(url, context.favicon, context.suppressSyntheticFavicon)}${context.title}\n\n${markdown}`;
1395
- }
1396
1011
  function buildMarkdownFromContext(context, url, signal) {
1397
1012
  let content = stageTracker.run(url, 'transform:markdown', () => htmlToMarkdown(context.sourceHtml, context.metadata, {
1398
1013
  url,
@@ -1400,7 +1015,7 @@ function buildMarkdownFromContext(context, url, signal) {
1400
1015
  ...(context.document ? { document: context.document } : {}),
1401
1016
  ...(context.skipNoiseRemoval ? { skipNoiseRemoval: true } : {}),
1402
1017
  }));
1403
- content = maybeStripGithubPrimaryHeading(content, context, url);
1018
+ content = maybeStripGithubPrimaryHeading(content, context.primaryHeading, url);
1404
1019
  content = maybePrependSyntheticTitle(content, context, url);
1405
1020
  content = supplementMarkdownFromNextFlight(content, context.originalHtml);
1406
1021
  content = cleanupMarkdownArtifacts(content, signal ? { signal, url } : { url });
@@ -1411,34 +1026,6 @@ function buildMarkdownFromContext(context, url, signal) {
1411
1026
  metadata: context.extractedMetadata,
1412
1027
  };
1413
1028
  }
1414
- function normalizeHeadingText(value) {
1415
- return value.replace(/\s+/g, ' ').trim().toLowerCase();
1416
- }
1417
- function stripLeadingHeading(markdown, headingText) {
1418
- if (!markdown)
1419
- return markdown;
1420
- const lines = markdown.split('\n');
1421
- const target = normalizeHeadingText(headingText);
1422
- let nonEmptySeen = 0;
1423
- for (let i = 0; i < lines.length && nonEmptySeen < HEADING_SCAN_LIMIT; i += 1) {
1424
- const trimmed = lines[i]?.trim() ?? '';
1425
- if (!trimmed)
1426
- continue;
1427
- nonEmptySeen += 1;
1428
- const match = /^(#{1,6})\s+(.+?)\s*$/.exec(trimmed);
1429
- if (!match)
1430
- continue;
1431
- const current = normalizeHeadingText(match[2] ?? '');
1432
- if (current !== target)
1433
- return markdown;
1434
- lines.splice(i, 1);
1435
- if ((lines[i] ?? '').trim() === '') {
1436
- lines.splice(i, 1);
1437
- }
1438
- return lines.join('\n');
1439
- }
1440
- return markdown;
1441
- }
1442
1029
  const REPLACEMENT_CHAR = '\ufffd';
1443
1030
  const BINARY_INDICATOR_THRESHOLD = 0.1;
1444
1031
  function hasBinaryIndicators(content) {