@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.
- package/dist/http/native.d.ts.map +1 -1
- package/dist/http/native.js +20 -26
- package/dist/http/session-teardown.d.ts +17 -0
- package/dist/http/session-teardown.d.ts.map +1 -0
- package/dist/http/session-teardown.js +30 -0
- package/dist/lib/code-lang.d.ts.map +1 -1
- package/dist/lib/code-lang.js +2 -8
- package/dist/lib/core.d.ts.map +1 -1
- package/dist/lib/core.js +2 -0
- package/dist/lib/dom-prep.d.ts +2 -0
- package/dist/lib/dom-prep.d.ts.map +1 -1
- package/dist/lib/dom-prep.js +150 -139
- package/dist/lib/md-cleanup.d.ts.map +1 -1
- package/dist/lib/md-cleanup.js +53 -20
- package/dist/lib/progress.d.ts.map +1 -1
- package/dist/lib/progress.js +59 -32
- package/dist/lib/sdk-interop.d.ts +8 -0
- package/dist/lib/sdk-interop.d.ts.map +1 -0
- package/dist/lib/sdk-interop.js +73 -0
- package/dist/lib/task-handlers.d.ts +0 -2
- package/dist/lib/task-handlers.d.ts.map +1 -1
- package/dist/lib/task-handlers.js +9 -94
- package/dist/resources/index.js +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -3
- package/dist/tasks/call-contract.d.ts +25 -0
- package/dist/tasks/call-contract.d.ts.map +1 -0
- package/dist/tasks/call-contract.js +58 -0
- package/dist/tasks/cursor-codec.d.ts +5 -0
- package/dist/tasks/cursor-codec.d.ts.map +1 -0
- package/dist/tasks/cursor-codec.js +41 -0
- package/dist/tasks/execution.d.ts +1 -20
- package/dist/tasks/execution.d.ts.map +1 -1
- package/dist/tasks/execution.js +7 -32
- package/dist/tasks/manager.d.ts +1 -5
- package/dist/tasks/manager.d.ts.map +1 -1
- package/dist/tasks/manager.js +16 -144
- package/dist/tasks/owner.d.ts +12 -2
- package/dist/tasks/owner.d.ts.map +1 -1
- package/dist/tasks/owner.js +52 -3
- package/dist/tasks/tool-registry.d.ts +1 -0
- package/dist/tasks/tool-registry.d.ts.map +1 -1
- package/dist/tasks/waiters.d.ts +27 -0
- package/dist/tasks/waiters.d.ts.map +1 -0
- package/dist/tasks/waiters.js +113 -0
- package/dist/tools/fetch-url-progress.d.ts +17 -0
- package/dist/tools/fetch-url-progress.d.ts.map +1 -0
- package/dist/tools/fetch-url-progress.js +78 -0
- package/dist/tools/fetch-url.d.ts +5 -9
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +43 -96
- package/dist/transform/next-flight.d.ts +2 -0
- package/dist/transform/next-flight.d.ts.map +1 -0
- package/dist/transform/next-flight.js +285 -0
- package/dist/transform/title-policy.d.ts +12 -0
- package/dist/transform/title-policy.d.ts.map +1 -0
- package/dist/transform/title-policy.js +85 -0
- package/dist/transform/transform.d.ts.map +1 -1
- package/dist/transform/transform.js +62 -475
- package/package.json +4 -4
- package/dist/tools/index.d.ts +0 -3
- package/dist/tools/index.d.ts.map +0 -1
- 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 {
|
|
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
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
655
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return
|
|
999
|
-
|
|
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(/'|'/g, "'")
|
|
1076
|
-
.replace(/"/g, '"')
|
|
1077
|
-
.replace(/&/g, '&')
|
|
1078
|
-
.replace(/</g, '<')
|
|
1079
|
-
.replace(/>/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 `  `;
|
|
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) {
|