webforai 3.0.0 → 4.0.0

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 (92) hide show
  1. package/README.md +25 -5
  2. package/dist/bin.js +1277 -479
  3. package/dist/cjs/detect-client-shell.js +78 -0
  4. package/dist/cjs/extract-meta-refresh.js +71 -0
  5. package/dist/cjs/extractors/lib/sanitize.js +4 -0
  6. package/dist/cjs/html-to-mdast.js +2 -1
  7. package/dist/cjs/index.js +12 -0
  8. package/dist/cjs/link-replacer.js +19 -13
  9. package/dist/cjs/loaders/cf-puppeteer.js +14 -7
  10. package/dist/cjs/loaders/fetch.js +14 -2
  11. package/dist/cjs/loaders/playwright.js +32 -30
  12. package/dist/cjs/loaders/puppeteer.js +14 -7
  13. package/dist/cjs/md-splitter.js +24 -11
  14. package/dist/cjs/mdast-handlers/custom-code-handler.js +18 -7
  15. package/dist/cjs/mdast-handlers/custom-div-handler.js +12 -33
  16. package/dist/cjs/mdast-handlers/definition-list-handler.js +17 -6
  17. package/dist/cjs/mdast-to-markdown.js +2 -5
  18. package/dist/cjs/normalize/index.js +11 -2
  19. package/dist/cjs/platform/client.js +73 -16
  20. package/dist/cjs/platform/index.js +4 -0
  21. package/dist/cjs/platform/types.js +7 -1
  22. package/dist/detect-client-shell.js +53 -0
  23. package/dist/extract-meta-refresh.js +47 -0
  24. package/dist/extractors/lib/sanitize.js +4 -0
  25. package/dist/html-to-mdast.js +2 -1
  26. package/dist/index.js +14 -0
  27. package/dist/link-replacer.js +19 -13
  28. package/dist/loaders/cf-puppeteer.js +15 -8
  29. package/dist/loaders/fetch.js +14 -2
  30. package/dist/loaders/playwright.js +32 -30
  31. package/dist/loaders/puppeteer.js +15 -8
  32. package/dist/md-splitter.js +24 -11
  33. package/dist/mdast-handlers/custom-code-handler.js +17 -7
  34. package/dist/mdast-handlers/custom-div-handler.js +12 -33
  35. package/dist/mdast-handlers/definition-list-handler.js +17 -6
  36. package/dist/mdast-to-markdown.js +2 -5
  37. package/dist/normalize/index.js +12 -2
  38. package/dist/platform/client.js +73 -16
  39. package/dist/platform/index.js +4 -0
  40. package/dist/platform/types.js +5 -1
  41. package/dist/types/cli/bin.js.map +1 -1
  42. package/dist/types/cli/commands/jobs/files.d.ts +28 -0
  43. package/dist/types/cli/commands/jobs/files.js.map +1 -0
  44. package/dist/types/cli/commands/jobs/llms-txt.d.ts +33 -0
  45. package/dist/types/cli/commands/jobs/llms-txt.js.map +1 -0
  46. package/dist/types/cli/commands/jobs/options.d.ts +58 -0
  47. package/dist/types/cli/commands/jobs/options.js.map +1 -0
  48. package/dist/types/cli/commands/jobs/run.d.ts +44 -0
  49. package/dist/types/cli/commands/jobs/run.js.map +1 -0
  50. package/dist/types/cli/commands/skill/content.js.map +1 -1
  51. package/dist/types/cli/commands/webforai/interactive.d.ts +1 -1
  52. package/dist/types/cli/commands/webforai/interactive.js.map +1 -1
  53. package/dist/types/cli/commands/webforai/options.d.ts +9 -2
  54. package/dist/types/cli/commands/webforai/options.js.map +1 -1
  55. package/dist/types/cli/commands/webforai/run.js.map +1 -1
  56. package/dist/types/cli/utils.d.ts +8 -1
  57. package/dist/types/cli/utils.js.map +1 -1
  58. package/dist/types/detect-client-shell.d.ts +24 -0
  59. package/dist/types/detect-client-shell.js.map +1 -0
  60. package/dist/types/extract-meta-refresh.d.ts +29 -0
  61. package/dist/types/extract-meta-refresh.js.map +1 -0
  62. package/dist/types/extractors/lib/sanitize.js.map +1 -1
  63. package/dist/types/html-to-mdast.js.map +1 -1
  64. package/dist/types/index.d.ts +2 -0
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/types/link-replacer.d.ts +3 -1
  67. package/dist/types/link-replacer.js.map +1 -1
  68. package/dist/types/loaders/cf-puppeteer.js.map +1 -1
  69. package/dist/types/loaders/fetch.d.ts +4 -0
  70. package/dist/types/loaders/fetch.js.map +1 -1
  71. package/dist/types/loaders/playwright.js.map +1 -1
  72. package/dist/types/loaders/puppeteer.js.map +1 -1
  73. package/dist/types/md-splitter.d.ts +1 -1
  74. package/dist/types/md-splitter.js.map +1 -1
  75. package/dist/types/mdast-handlers/custom-code-handler.d.ts +2 -0
  76. package/dist/types/mdast-handlers/custom-code-handler.js.map +1 -1
  77. package/dist/types/mdast-handlers/custom-div-handler.js.map +1 -1
  78. package/dist/types/mdast-handlers/definition-list-handler.d.ts +0 -11
  79. package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -1
  80. package/dist/types/mdast-to-markdown.js.map +1 -1
  81. package/dist/types/normalize/index.js.map +1 -1
  82. package/dist/types/platform/client.d.ts +2 -1
  83. package/dist/types/platform/client.js.map +1 -1
  84. package/dist/types/platform/error.d.ts +5 -3
  85. package/dist/types/platform/error.js.map +1 -1
  86. package/dist/types/platform/fetch.d.ts +2 -0
  87. package/dist/types/platform/fetch.js.map +1 -1
  88. package/dist/types/platform/index.d.ts +1 -1
  89. package/dist/types/platform/index.js.map +1 -1
  90. package/dist/types/platform/types.d.ts +35 -6
  91. package/dist/types/platform/types.js.map +1 -1
  92. package/package.json +16 -5
@@ -98,14 +98,16 @@ const katexDuplicates = (node) => {
98
98
  if (!(classes.includes("katex") || classes.includes("katex-display"))) {
99
99
  return [];
100
100
  }
101
- const hasMathml = node.children.some((child) => (0, import_hast_fast.isElement)(child) && (0, import_hast_fast.classList)(child).includes("katex-mathml"));
101
+ const hasMathml = node.children.some(
102
+ (child) => (0, import_hast_fast.isElement)(child) && (0, import_hast_fast.classList)(child).includes("katex-mathml") && hasMathSource(child)
103
+ );
102
104
  if (!hasMathml) {
103
105
  return [];
104
106
  }
105
107
  return node.children.filter((child) => (0, import_hast_fast.isElement)(child) && (0, import_hast_fast.classList)(child).includes("katex-html"));
106
108
  };
107
109
  const mediawikiDuplicates = (node) => {
108
- if (!(0, import_hast_fast.classList)(node).includes("mwe-math-element")) {
110
+ if (!(0, import_hast_fast.classList)(node).includes("mwe-math-element") || !hasMathSource(node)) {
109
111
  return [];
110
112
  }
111
113
  return node.children.filter(
@@ -116,8 +118,15 @@ const mathjaxDuplicates = (node) => {
116
118
  if (node.tagName !== "mjx-container") {
117
119
  return [];
118
120
  }
121
+ const hasMathml = node.children.some(
122
+ (child) => (0, import_hast_fast.isElement)(child) && child.tagName === "mjx-assistive-mml" && hasMathSource(child)
123
+ );
124
+ if (!hasMathml) {
125
+ return [];
126
+ }
119
127
  return node.children.filter((child) => (0, import_hast_fast.isElement)(child) && child.tagName !== "mjx-assistive-mml");
120
128
  };
129
+ const hasMathSource = (node) => Boolean((0, import_hast_fast.findElement)(node, (element) => element.tagName === "math" && element.children.length > 0));
121
130
  const dedupeRenderedMath = (tree) => {
122
131
  const doomed = /* @__PURE__ */ new Set();
123
132
  (0, import_hast_fast.walk)(tree, (node) => {
@@ -50,14 +50,28 @@ const parseRetryAfter = (body, response) => {
50
50
  const header = Number(response.headers.get("retry-after"));
51
51
  return Number.isFinite(header) && header > 0 ? header : void 0;
52
52
  };
53
- const toApiError = (response, body) => {
53
+ const dashboardHint = (status, dashboardUrl) => {
54
+ if (status === 401) {
55
+ return `create or check API keys at ${dashboardUrl}`;
56
+ }
57
+ if (status === 402) {
58
+ return `manage billing, credits and the spend cap at ${dashboardUrl}`;
59
+ }
60
+ return void 0;
61
+ };
62
+ const toApiError = (response, body, dashboardUrl) => {
54
63
  const envelope = body;
55
64
  const code = typeof envelope?.error?.code === "string" ? envelope.error.code : "invalid_response";
56
- const message = typeof envelope?.error?.message === "string" ? envelope.error.message : `unexpected response (HTTP ${response.status})`;
65
+ let message = typeof envelope?.error?.message === "string" ? envelope.error.message : `unexpected response (HTTP ${response.status})`;
66
+ const hint = code === "invalid_response" ? void 0 : dashboardHint(response.status, dashboardUrl);
67
+ if (hint && !message.includes("/dashboard")) {
68
+ message = `${message} (${hint})`;
69
+ }
57
70
  return new import_error.PlatformApiError(code, message, response.status, parseRetryAfter(body, response));
58
71
  };
59
72
  const createPlatformClient = (options = {}) => {
60
73
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
74
+ const dashboardUrl = `${baseUrl}/dashboard`;
61
75
  const fetchImpl = (0, import_fetch.resolveFetch)(options.fetch);
62
76
  const request = async (path, init) => {
63
77
  const headers = { accept: "application/json" };
@@ -68,7 +82,7 @@ const createPlatformClient = (options = {}) => {
68
82
  if (!options.apiKey) {
69
83
  throw new import_error.PlatformApiError(
70
84
  "missing_api_key",
71
- "This endpoint needs an API key: pass `apiKey` to createPlatformClient (create one on the platform dashboard).",
85
+ `This endpoint needs an API key: pass \`apiKey\` to createPlatformClient (create one at ${dashboardUrl}; 1,000 free credits/month).`,
72
86
  0
73
87
  );
74
88
  }
@@ -77,18 +91,19 @@ const createPlatformClient = (options = {}) => {
77
91
  const response = await fetchImpl(`${baseUrl}${path}`, {
78
92
  method: init.method,
79
93
  headers,
80
- body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
94
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
95
+ ...init.signal ? { signal: init.signal } : {}
81
96
  });
82
97
  const body = await response.json().catch(() => void 0);
83
98
  if (!response.ok) {
84
- throw toApiError(response, body);
99
+ throw toApiError(response, body, dashboardUrl);
85
100
  }
86
101
  if (body === void 0) {
87
102
  throw new import_error.PlatformApiError("invalid_response", "response body is not JSON", response.status);
88
103
  }
89
104
  return body;
90
105
  };
91
- const getJob = (jobId) => request(`/v1/jobs/${encodeURIComponent(jobId)}`, { method: "GET", auth: true });
106
+ const getJob = (jobId, signal) => request(`/v1/jobs/${encodeURIComponent(jobId)}`, { method: "GET", auth: true, signal });
92
107
  const getJobResults = (jobId, opts) => {
93
108
  const query = opts?.cursor ? `?cursor=${encodeURIComponent(opts.cursor)}` : "";
94
109
  return request(`/v1/jobs/${encodeURIComponent(jobId)}/results${query}`, { method: "GET", auth: true });
@@ -104,6 +119,17 @@ const createPlatformClient = (options = {}) => {
104
119
  }
105
120
  return await response.json();
106
121
  };
122
+ const getJobOrBackOff = async (jobId, signal, interval) => {
123
+ try {
124
+ return await getJob(jobId, signal);
125
+ } catch (error) {
126
+ if (!(error instanceof import_error.PlatformApiError && error.status === 429)) {
127
+ throw error;
128
+ }
129
+ await sleep(error.retryAfter !== void 0 ? error.retryAfter * 1e3 : interval, signal);
130
+ return void 0;
131
+ }
132
+ };
107
133
  return {
108
134
  scrape: (opts) => request("/v1/scrape", { method: "POST", body: opts, auth: true }),
109
135
  scrapeAsync: (opts) => request("/v1/scrape", { method: "POST", body: { ...opts, async: true }, auth: true }),
@@ -114,17 +140,48 @@ const createPlatformClient = (options = {}) => {
114
140
  waitForJob: async (jobId, opts = {}) => {
115
141
  const interval = opts.pollIntervalMs ?? 2e3;
116
142
  const timeout = opts.timeoutMs ?? 10 * 60 * 1e3;
117
- const startedAt = Date.now();
118
- for (; ; ) {
119
- const status = await getJob(jobId);
120
- opts.onStatus?.(status);
121
- if (TERMINAL_STATES.has(status.status)) {
122
- return status;
123
- }
124
- if (Date.now() - startedAt + interval > timeout) {
125
- throw new import_error.PlatformApiError("poll_timeout", `job ${jobId} still ${status.status} after ${timeout}ms`, 0);
143
+ if (!Number.isFinite(interval) || interval < 0 || interval > 2147483647) {
144
+ throw new RangeError("pollIntervalMs must be between 0 and 2147483647");
145
+ }
146
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
147
+ throw new RangeError("timeoutMs must be greater than 0 and at most 2147483647");
148
+ }
149
+ opts.signal?.throwIfAborted();
150
+ const controller = new AbortController();
151
+ const { signal } = controller;
152
+ const onAbort = () => controller.abort(opts.signal?.reason);
153
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
154
+ const timer = setTimeout(
155
+ () => controller.abort(new import_error.PlatformApiError("poll_timeout", `job ${jobId} did not finish within ${timeout}ms`, 0)),
156
+ timeout
157
+ );
158
+ let rejectOnAbort = () => {
159
+ };
160
+ const aborted = new Promise((_resolve, reject) => {
161
+ rejectOnAbort = () => reject(signal.reason);
162
+ signal.addEventListener("abort", rejectOnAbort, { once: true });
163
+ });
164
+ const poll = async () => {
165
+ for (; ; ) {
166
+ signal.throwIfAborted();
167
+ const status = await getJobOrBackOff(jobId, signal, interval);
168
+ signal.throwIfAborted();
169
+ if (status === void 0) {
170
+ continue;
171
+ }
172
+ opts.onStatus?.(status);
173
+ if (TERMINAL_STATES.has(status.status)) {
174
+ return status;
175
+ }
176
+ await sleep(interval, signal);
126
177
  }
127
- await sleep(interval, opts.signal);
178
+ };
179
+ try {
180
+ return await Promise.race([poll(), aborted]);
181
+ } finally {
182
+ clearTimeout(timer);
183
+ opts.signal?.removeEventListener("abort", onAbort);
184
+ signal.removeEventListener("abort", rejectOnAbort);
128
185
  }
129
186
  },
130
187
  jobResults: async function* (jobId) {
@@ -18,10 +18,12 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var platform_exports = {};
20
20
  __export(platform_exports, {
21
+ CRAWL_SITEMAP_MODES: () => import_types.CRAWL_SITEMAP_MODES,
21
22
  DEFAULT_BASE_URL: () => import_client.DEFAULT_BASE_URL,
22
23
  ENGINES: () => import_types.ENGINES,
23
24
  PlatformApiError: () => import_error.PlatformApiError,
24
25
  REGIONS: () => import_types.REGIONS,
26
+ REQUESTED_ENGINES: () => import_types.REQUESTED_ENGINES,
25
27
  createPlatformClient: () => import_client.createPlatformClient,
26
28
  isStoredPageStub: () => import_types.isStoredPageStub
27
29
  });
@@ -31,10 +33,12 @@ var import_error = require("./error");
31
33
  var import_types = require("./types");
32
34
  // Annotate the CommonJS export names for ESM import in node:
33
35
  0 && (module.exports = {
36
+ CRAWL_SITEMAP_MODES,
34
37
  DEFAULT_BASE_URL,
35
38
  ENGINES,
36
39
  PlatformApiError,
37
40
  REGIONS,
41
+ REQUESTED_ENGINES,
38
42
  createPlatformClient,
39
43
  isStoredPageStub
40
44
  });
@@ -18,17 +18,23 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var types_exports = {};
20
20
  __export(types_exports, {
21
+ CRAWL_SITEMAP_MODES: () => CRAWL_SITEMAP_MODES,
21
22
  ENGINES: () => ENGINES,
22
23
  REGIONS: () => REGIONS,
24
+ REQUESTED_ENGINES: () => REQUESTED_ENGINES,
23
25
  isStoredPageStub: () => isStoredPageStub
24
26
  });
25
27
  module.exports = __toCommonJS(types_exports);
26
28
  const ENGINES = ["fetch", "browser", "proxy-fetch", "proxy-browser"];
27
- const REGIONS = ["auto", "us", "eu", "uk", "jp", "asia"];
29
+ const REQUESTED_ENGINES = [...ENGINES, "auto"];
30
+ const REGIONS = ["auto", "jp"];
31
+ const CRAWL_SITEMAP_MODES = ["skip", "include", "only"];
28
32
  const isStoredPageStub = (item) => item.status === "ok" && "resultUrl" in item;
29
33
  // Annotate the CommonJS export names for ESM import in node:
30
34
  0 && (module.exports = {
35
+ CRAWL_SITEMAP_MODES,
31
36
  ENGINES,
32
37
  REGIONS,
38
+ REQUESTED_ENGINES,
33
39
  isStoredPageStub
34
40
  });
@@ -0,0 +1,53 @@
1
+ // src/detect-client-shell.ts
2
+ var SHELL_SUSPICION_THRESHOLD = 300;
3
+ var SHELL_EMPTY_THRESHOLD = 50;
4
+ var BODY_PATTERN = /<body[^>]*>([\s\S]*?)<\/body>/i;
5
+ var HEAD_PATTERN = /<head[^>]*>[\s\S]*?<\/head>/gi;
6
+ var NOSCRIPT_PATTERN = /<noscript[^>]*>([\s\S]*?)<\/noscript>/gi;
7
+ var INVISIBLE_ELEMENT_PATTERN = /<(script|style|template|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
8
+ var COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
9
+ var TAG_PATTERN = /<[^>]+>/g;
10
+ var MOUNT_POINT_PATTERN = /<[a-z][^>]*\sid=["'](?:root|app|__next|___gatsby|__nuxt|q-app|svelte|react-root)["']|<app-root[\s>]|\sdata-reactroot[\s>=]/i;
11
+ var CHALLENGE_PATTERN = /cf-chl|challenge-platform|cf_chl_opt|just a moment|attention required|checking your browser|_incapsula_|px-captcha|datadome/i;
12
+ var ENTITIES = {
13
+ "&nbsp;": " ",
14
+ "&amp;": "&",
15
+ "&lt;": "<",
16
+ "&gt;": ">",
17
+ "&quot;": '"',
18
+ "&#39;": "'"
19
+ };
20
+ var visibleTextOf = (markup) => markup.replace(COMMENT_PATTERN, " ").replace(INVISIBLE_ELEMENT_PATTERN, " ").replace(TAG_PATTERN, " ").replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? " ").replace(/\s+/g, " ").trim();
21
+ var noscriptMentionsJavascript = (html) => {
22
+ for (const match of html.matchAll(NOSCRIPT_PATTERN)) {
23
+ if (/javascript/i.test(match[1] ?? "")) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ };
29
+ var detectClientShell = (html) => {
30
+ const body = BODY_PATTERN.exec(html)?.[1] ?? html.replace(HEAD_PATTERN, " ");
31
+ const visibleTextLength = visibleTextOf(body).length;
32
+ if (visibleTextLength >= SHELL_SUSPICION_THRESHOLD) {
33
+ return { isShell: false, visibleTextLength };
34
+ }
35
+ if (CHALLENGE_PATTERN.test(html)) {
36
+ return { isShell: true, reason: "anti-bot-challenge", visibleTextLength };
37
+ }
38
+ if (MOUNT_POINT_PATTERN.test(body)) {
39
+ return { isShell: true, reason: "spa-shell", visibleTextLength };
40
+ }
41
+ if (noscriptMentionsJavascript(html)) {
42
+ return { isShell: true, reason: "noscript-only", visibleTextLength };
43
+ }
44
+ if (visibleTextLength < SHELL_EMPTY_THRESHOLD) {
45
+ return { isShell: true, reason: "empty-body", visibleTextLength };
46
+ }
47
+ return { isShell: false, visibleTextLength };
48
+ };
49
+ export {
50
+ SHELL_EMPTY_THRESHOLD,
51
+ SHELL_SUSPICION_THRESHOLD,
52
+ detectClientShell
53
+ };
@@ -0,0 +1,47 @@
1
+ // src/extract-meta-refresh.ts
2
+ var MAX_META_REFRESH_DELAY_SECONDS = 10;
3
+ var META_TAG_PATTERN = /<meta\b[^>]*>/gi;
4
+ var HTTP_EQUIV_REFRESH_PATTERN = /\bhttp-equiv\s*=\s*("\s*refresh\s*"|'\s*refresh\s*'|refresh\b)/i;
5
+ var CONTENT_ATTR_PATTERN = /\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
6
+ var CONTENT_VALUE_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*[;,]\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+?))\s*$/i;
7
+ var resolveTarget = (raw, baseUrl) => {
8
+ try {
9
+ return baseUrl === void 0 ? new URL(raw) : new URL(raw, baseUrl);
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ };
14
+ var extractMetaRefresh = (html, baseUrl) => {
15
+ for (const [tag] of html.matchAll(META_TAG_PATTERN)) {
16
+ if (!HTTP_EQUIV_REFRESH_PATTERN.test(tag)) {
17
+ continue;
18
+ }
19
+ const contentMatch = CONTENT_ATTR_PATTERN.exec(tag);
20
+ const content = contentMatch?.[1] ?? contentMatch?.[2] ?? contentMatch?.[3];
21
+ if (!content) {
22
+ continue;
23
+ }
24
+ const value = CONTENT_VALUE_PATTERN.exec(content);
25
+ if (!value) {
26
+ continue;
27
+ }
28
+ const delaySeconds = Number.parseFloat(value[1] ?? "");
29
+ const raw = (value[2] ?? value[3] ?? value[4] ?? "").trim();
30
+ if (!raw || delaySeconds > MAX_META_REFRESH_DELAY_SECONDS) {
31
+ continue;
32
+ }
33
+ const target = resolveTarget(raw, baseUrl);
34
+ if (!target || target.protocol !== "http:" && target.protocol !== "https:") {
35
+ continue;
36
+ }
37
+ if (baseUrl !== void 0 && resolveTarget(baseUrl, void 0)?.href === target.href) {
38
+ continue;
39
+ }
40
+ return { url: target.href, delaySeconds };
41
+ }
42
+ return void 0;
43
+ };
44
+ export {
45
+ MAX_META_REFRESH_DELAY_SECONDS,
46
+ extractMetaRefresh
47
+ };
@@ -374,7 +374,11 @@ var countAnchors = (element) => {
374
374
  }
375
375
  return count;
376
376
  };
377
+ var LITERAL_TEXT_TAGS = /* @__PURE__ */ new Set(["code", "kbd", "samp", "var", "pre"]);
377
378
  var isMidArticleWidget = (element) => {
379
+ if (LITERAL_TEXT_TAGS.has(element.tagName)) {
380
+ return false;
381
+ }
378
382
  if (element.tagName === "form" || element.tagName === "fieldset") {
379
383
  return true;
380
384
  }
@@ -12,13 +12,14 @@ import { definitionListHandler } from "./mdast-handlers/definition-list-handler.
12
12
  import { mathHandler } from "./mdast-handlers/math-handler.js";
13
13
  import { supSubHandler } from "./mdast-handlers/sup-sub-handler.js";
14
14
  import { normalizeHast } from "./normalize/index.js";
15
+ import { cloneHast } from "./utils/hast-fast.js";
15
16
  import { getLangFromHast, getLangFromStr, getUrlFromHast } from "./utils/hast-utils.js";
16
17
  var htmlToMdast = (htmlOrHast, options) => {
17
18
  const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
18
19
  const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
19
20
  const isOwned = typeof htmlOrHast === "string" || (options?.owned ?? false);
20
21
  const url = defaultUrl || getUrlFromHast(hast);
21
- const extractedHast = pipeExtractors({ hast, lang, url, owned: isOwned }, extractors);
22
+ const extractedHast = pipeExtractors({ hast: isOwned ? hast : cloneHast(hast), lang, url, owned: true }, extractors);
22
23
  normalizeHast(extractedHast, options?.normalize);
23
24
  const mdast = toMdast(extractedHast, {
24
25
  handlers: {
package/dist/index.js CHANGED
@@ -22,15 +22,29 @@ import {
22
22
  runAdapters,
23
23
  youtubeCaptionTracks
24
24
  } from "./adapters/index.js";
25
+ import {
26
+ detectClientShell,
27
+ SHELL_EMPTY_THRESHOLD,
28
+ SHELL_SUSPICION_THRESHOLD
29
+ } from "./detect-client-shell.js";
30
+ import {
31
+ extractMetaRefresh,
32
+ MAX_META_REFRESH_DELAY_SECONDS
33
+ } from "./extract-meta-refresh.js";
25
34
  import { extractMetadata, toFrontmatter } from "./metadata/index.js";
26
35
  import { normalizeHast } from "./normalize/index.js";
27
36
  export {
28
37
  BUILTIN_ADAPTERS,
38
+ MAX_META_REFRESH_DELAY_SECONDS,
39
+ SHELL_EMPTY_THRESHOLD,
40
+ SHELL_SUSPICION_THRESHOLD,
29
41
  adapterById,
30
42
  autoExtractor,
31
43
  buildPageSignature,
32
44
  createAutoExtractor,
33
45
  defineSelectorAdapter,
46
+ detectClientShell,
47
+ extractMetaRefresh,
34
48
  extractMetadata,
35
49
  htmlToMarkdown,
36
50
  htmlToMarkdownWithMetadata,
@@ -1,17 +1,23 @@
1
1
  // src/link-replacer.ts
2
- var linkReplacer = (markdown, base) => {
3
- const regex = /(!?\[.*?\]\()([^)\s]+)(\))/g;
4
- return markdown.replace(regex, (match, pre, url, post) => {
5
- if (/^(https?:|#)/.test(url)) {
6
- return match;
7
- }
8
- try {
9
- const absoluteUrl = new URL(url, base).href;
10
- return `${pre}${absoluteUrl}${post}`;
11
- } catch {
12
- return match;
13
- }
14
- });
2
+ var absoluteUrl = (url, base) => {
3
+ if (/^(?:[a-z][a-z\d+.-]*:|#)/i.test(url)) {
4
+ return url;
5
+ }
6
+ try {
7
+ return new URL(url, base).href;
8
+ } catch {
9
+ return url;
10
+ }
11
+ };
12
+ var linkReplacer = (node, base) => {
13
+ let result = node;
14
+ if (node.type === "link" || node.type === "image" || node.type === "definition") {
15
+ result = { ...node, url: absoluteUrl(node.url, base) };
16
+ }
17
+ if ("children" in result) {
18
+ return { ...result, children: result.children.map((child) => linkReplacer(child, base)) };
19
+ }
20
+ return result;
15
21
  };
16
22
  export {
17
23
  linkReplacer
@@ -1,14 +1,21 @@
1
1
  // src/loaders/cf-puppeteer.ts
2
- import puppeteer from "@cloudflare/puppeteer";
3
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2
+ import puppeteer, { TimeoutError } from "@cloudflare/puppeteer";
4
3
  var loadHtml = async (url, ctx) => {
5
4
  const browser = await puppeteer.launch(ctx);
6
- const page = await browser.newPage();
7
- await page.goto(url);
8
- const html = await page.content();
9
- await Promise.race([page.waitForNetworkIdle(), sleep(1e4)]);
10
- await page.close();
11
- return html;
5
+ try {
6
+ const page = await browser.newPage();
7
+ await page.goto(url);
8
+ try {
9
+ await page.waitForNetworkIdle({ timeout: 1e4 });
10
+ } catch (error) {
11
+ if (!(error instanceof TimeoutError)) {
12
+ throw error;
13
+ }
14
+ }
15
+ return await page.content();
16
+ } finally {
17
+ await browser.close();
18
+ }
12
19
  };
13
20
  export {
14
21
  loadHtml
@@ -1,8 +1,20 @@
1
1
  // src/loaders/fetch.ts
2
+ import { extractMetaRefresh } from "../extract-meta-refresh.js";
2
3
  var USER_AGENT = "mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/125.0.0.0 safari/537.36";
4
+ var MAX_META_REFRESH_HOPS = 3;
3
5
  var loadHtml = async (url, userAgent = USER_AGENT) => {
4
- const response = await fetch(url, { headers: { "User-Agent": userAgent } });
5
- return response.text();
6
+ let current = url;
7
+ let html = "";
8
+ for (let hop = 0; hop <= MAX_META_REFRESH_HOPS; hop++) {
9
+ const response = await fetch(current, { headers: { "User-Agent": userAgent } });
10
+ html = await response.text();
11
+ const refresh = extractMetaRefresh(html, response.url || current);
12
+ if (!refresh) {
13
+ break;
14
+ }
15
+ current = refresh.url;
16
+ }
17
+ return html;
6
18
  };
7
19
  export {
8
20
  USER_AGENT,
@@ -13,40 +13,42 @@ var SUPER_BYPASS_DEVICE = {
13
13
  var loadHtml = async (url, options) => {
14
14
  const { browser, waitUntil, timeout, superBypassMode } = options ?? {};
15
15
  const _browser = browser ?? await chromium.launch({ headless: true });
16
- const context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
17
- if (superBypassMode) {
18
- await context.addInitScript(() => {
19
- Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
20
- Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
21
- Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
22
- });
23
- }
24
- const page = await context.newPage();
25
- if (superBypassMode) {
26
- await page.route("**/*.js", (route) => {
27
- if (route.request().url().includes("captcha-delivery")) {
28
- return route.abort();
16
+ let context;
17
+ try {
18
+ context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
19
+ if (superBypassMode) {
20
+ await context.addInitScript(() => {
21
+ Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
22
+ Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
23
+ Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
24
+ });
25
+ }
26
+ const page = await context.newPage();
27
+ if (superBypassMode) {
28
+ await page.route("**/*.js", (route) => {
29
+ if (route.request().url().includes("captcha-delivery")) {
30
+ return route.abort();
31
+ }
32
+ return route.continue();
33
+ });
34
+ }
35
+ await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
36
+ await page.evaluate(() => {
37
+ const elements = document.querySelectorAll("*");
38
+ for (const element of elements) {
39
+ const rect = element.getBoundingClientRect();
40
+ element.setAttribute("data-rwidth", rect.width.toString());
41
+ element.setAttribute("data-rheight", rect.height.toString());
29
42
  }
30
- return route.continue();
31
43
  });
32
- }
33
- await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
34
- await page.evaluate(() => {
35
- const elements = document.querySelectorAll("*");
36
- for (const element of elements) {
37
- const rect = element.getBoundingClientRect();
38
- element.setAttribute("data-rwidth", rect.width.toString());
39
- element.setAttribute("data-rheight", rect.height.toString());
44
+ return await page.content();
45
+ } finally {
46
+ if (browser) {
47
+ await context?.close();
48
+ } else {
49
+ await _browser.close();
40
50
  }
41
- });
42
- const html = await page.content();
43
- await page.close();
44
- if (browser) {
45
- await context.close();
46
- } else {
47
- await _browser.close();
48
51
  }
49
- return html;
50
52
  };
51
53
  export {
52
54
  loadHtml
@@ -1,6 +1,5 @@
1
1
  // src/loaders/puppeteer.ts
2
- import puppeteer from "puppeteer";
3
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2
+ import puppeteer, { TimeoutError } from "puppeteer";
4
3
  var loadHtml = async (url, ctx) => {
5
4
  const browser = await puppeteer.launch(
6
5
  ctx || {
@@ -8,12 +7,20 @@ var loadHtml = async (url, ctx) => {
8
7
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
9
8
  }
10
9
  );
11
- const page = await browser.newPage();
12
- await page.goto(url);
13
- const html = await page.content();
14
- await Promise.race([page.waitForNetworkIdle(), sleep(1e4)]);
15
- await browser.close();
16
- return html;
10
+ try {
11
+ const page = await browser.newPage();
12
+ await page.goto(url);
13
+ try {
14
+ await page.waitForNetworkIdle({ timeout: 1e4 });
15
+ } catch (error) {
16
+ if (!(error instanceof TimeoutError)) {
17
+ throw error;
18
+ }
19
+ }
20
+ return await page.content();
21
+ } finally {
22
+ await browser.close();
23
+ }
17
24
  };
18
25
  export {
19
26
  loadHtml
@@ -3,15 +3,13 @@ import { mdastToMarkdown } from "./mdast-to-markdown.js";
3
3
  import { chunk } from "./utils/common.js";
4
4
  import { internalType, unwarpRoot, warpRoot } from "./utils/mdast-utils.js";
5
5
  var PRIORITY_SPLITTERS = ["h1", "h2", "h3", "h4", "h5", "h6", "list", "table", "code"];
6
- var getSplitterGenerator = function* () {
7
- for (const splitter of PRIORITY_SPLITTERS) {
8
- yield splitter;
9
- }
10
- };
11
- var _mdastSplitter = async (contents, checker, splitterGenerator) => {
12
- const splitter = splitterGenerator.next().value;
6
+ var _mdastSplitter = async (contents, checker, priority, signal) => {
7
+ signal?.throwIfAborted();
8
+ const splitter = PRIORITY_SPLITTERS[priority];
13
9
  const markdown = mdastToMarkdown(warpRoot(contents));
14
- if (await checker(markdown) || contents.length === 1) {
10
+ const accepted = await checker(markdown);
11
+ signal?.throwIfAborted();
12
+ if (accepted || contents.length === 1) {
15
13
  return [contents];
16
14
  }
17
15
  const chunked = splitter ? contents.reduce((acc, content) => {
@@ -22,11 +20,26 @@ var _mdastSplitter = async (contents, checker, splitterGenerator) => {
22
20
  acc[acc.length - 1].push(content);
23
21
  return acc;
24
22
  }, []) : chunk(contents, Math.ceil(contents.length / 2));
25
- const splitting = chunked.map((chunk2) => _mdastSplitter(chunk2, checker, splitterGenerator));
23
+ const splitting = chunked.map((chunk2) => _mdastSplitter(chunk2, checker, priority + 1, signal));
26
24
  return Promise.all(splitting).then((chunks) => chunks.flat());
27
25
  };
28
- var mdastSplitter = (mdast, checker, _options) => {
29
- return _mdastSplitter(unwarpRoot(mdast), checker, getSplitterGenerator());
26
+ var mdastSplitter = async (mdast, checker, options) => {
27
+ const signal = options?.signal;
28
+ if (!signal) {
29
+ return _mdastSplitter(unwarpRoot(mdast), checker, 0);
30
+ }
31
+ signal.throwIfAborted();
32
+ let onAbort = () => {
33
+ };
34
+ const aborted = new Promise((_, reject) => {
35
+ onAbort = () => reject(signal.reason);
36
+ signal.addEventListener("abort", onAbort, { once: true });
37
+ });
38
+ try {
39
+ return await Promise.race([_mdastSplitter(unwarpRoot(mdast), checker, 0, signal), aborted]);
40
+ } finally {
41
+ signal.removeEventListener("abort", onAbort);
42
+ }
30
43
  };
31
44
  export {
32
45
  mdastSplitter