@detiq/crawler 1.1.7 → 1.1.9

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/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.d.ts +10 -0
  22. package/dist/extractors/advanced.js +266 -249
  23. package/dist/extractors/artifact-envelope.js +58 -59
  24. package/dist/failure-analysis-agent.js +33 -37
  25. package/dist/feature-flag-detector.js +66 -51
  26. package/dist/form-detector.js +75 -55
  27. package/dist/frontier.js +101 -76
  28. package/dist/gap-pass.js +89 -68
  29. package/dist/graphql-detector.js +72 -62
  30. package/dist/http-engine.js +330 -282
  31. package/dist/incremental-diff.js +87 -54
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +28 -63
  34. package/dist/index.js.map +2 -2
  35. package/dist/input-fuzzer.js +97 -93
  36. package/dist/interactive-probe.d.ts +36 -0
  37. package/dist/interactive-probe.js +827 -715
  38. package/dist/job-queue.js +2169 -2151
  39. package/dist/js-coverage.js +56 -52
  40. package/dist/js-router-detector.js +117 -86
  41. package/dist/language-detector.js +63 -51
  42. package/dist/link-classifier.js +50 -32
  43. package/dist/live-view.js +108 -91
  44. package/dist/locator-extractor.d.ts +37 -0
  45. package/dist/locator-extractor.js +199 -0
  46. package/dist/locator-extractor.js.map +7 -0
  47. package/dist/logger.js +7 -9
  48. package/dist/markdown-extract.js +61 -48
  49. package/dist/native-crawler.js +946 -780
  50. package/dist/network-observer.js +156 -135
  51. package/dist/network-throttle.js +40 -29
  52. package/dist/openapi-generator.js +92 -77
  53. package/dist/pdf-extractor.js +90 -72
  54. package/dist/performance-metrics.js +102 -80
  55. package/dist/phash.js +144 -125
  56. package/dist/pii-scrubber.js +68 -65
  57. package/dist/playwright-recorder.js +58 -70
  58. package/dist/policies/crawl-safety.js +34 -31
  59. package/dist/profiles/domain-profile.js +125 -130
  60. package/dist/proxy.js +172 -146
  61. package/dist/rate-limiter.js +65 -58
  62. package/dist/redirect-tracker.js +64 -60
  63. package/dist/redis-frontier.js +136 -116
  64. package/dist/redis-queue.js +144 -157
  65. package/dist/response-cache.js +140 -127
  66. package/dist/responsive-detector.js +46 -42
  67. package/dist/retry-policy.js +50 -46
  68. package/dist/robots-txt.js +83 -69
  69. package/dist/screen-reader.js +70 -45
  70. package/dist/screenshot-masker.js +77 -59
  71. package/dist/seo-checker.js +107 -88
  72. package/dist/session-capture.js +212 -192
  73. package/dist/session-export.js +133 -107
  74. package/dist/sitemap-writer.js +41 -38
  75. package/dist/sitemap.js +202 -141
  76. package/dist/skyvern.js +148 -123
  77. package/dist/sqs-queue.js +42 -42
  78. package/dist/sso-token.js +90 -74
  79. package/dist/stealth/human-behavior.js +173 -156
  80. package/dist/telemetry.js +82 -68
  81. package/dist/test-executor.js +335 -309
  82. package/dist/tls-fingerprint.js +88 -77
  83. package/dist/url-pattern-completer.js +90 -71
  84. package/dist/visual-diff-verdict.js +38 -23
  85. package/dist/webhook-notifier.js +69 -71
  86. package/dist/websocket-detector.js +68 -56
  87. package/dist/worker.js +38 -29
  88. package/package.json +1 -1
  89. package/src/index.ts +2 -0
  90. package/src/locator-extractor.ts +226 -0
  91. package/types/index.d.ts +2 -0
  92. package/types/locator-extractor.d.ts +37 -0
@@ -1,265 +1,282 @@
1
- import { parseHTML } from "linkedom";
2
- function extractByCssSelector(html, selector, attr) {
3
- try {
4
- const { document } = parseHTML(html);
5
- const nodes = Array.from(document.querySelectorAll(selector));
6
- const matches = nodes.map((n) => {
7
- if (attr) return n.getAttribute(attr) ?? "";
8
- return n.textContent?.trim() ?? "";
9
- }).filter(Boolean);
10
- return { matches, count: matches.length, method: "css-selector" };
11
- } catch (e) {
12
- return { matches: [], count: 0, method: "css-selector-error" };
13
- }
14
- }
15
- function extractByXPath(html, xpath) {
16
- try {
17
- const { document, XPathResult } = parseHTML(html);
18
- const result = document.evaluate(xpath, document, null, XPathResult?.ANY_TYPE ?? 0, null);
19
- const matches = [];
20
- let node = result.iterateNext?.();
21
- while (node) {
22
- const text = node.textContent?.trim() ?? node.nodeValue?.trim() ?? "";
23
- if (text) matches.push(text);
24
- node = result.iterateNext?.();
1
+ import { parseHTML } from 'linkedom';
2
+ export function extractByCssSelector(html, selector, attr) {
3
+ try {
4
+ const { document } = parseHTML(html);
5
+ const nodes = Array.from(document.querySelectorAll(selector));
6
+ const matches = nodes.map((n) => {
7
+ if (attr)
8
+ return n.getAttribute(attr) ?? '';
9
+ return n.textContent?.trim() ?? '';
10
+ }).filter(Boolean);
11
+ return { matches, count: matches.length, method: 'css-selector' };
25
12
  }
26
- return { matches, count: matches.length, method: "xpath" };
27
- } catch (e) {
28
- return { matches: [], count: 0, method: "xpath-error" };
29
- }
30
- }
31
- function extractByRegex(text, pattern, flags) {
32
- try {
33
- const re = new RegExp(pattern, (flags ?? "g").includes("g") ? flags : (flags ?? "") + "g");
34
- const matches = [];
35
- let m;
36
- while ((m = re.exec(text)) !== null) {
37
- matches.push(m[1] ?? m[0]);
38
- if (!re.global) break;
13
+ catch (e) {
14
+ return { matches: [], count: 0, method: 'css-selector-error' };
39
15
  }
40
- return { matches, count: matches.length, method: "regex" };
41
- } catch {
42
- return { matches: [], count: 0, method: "regex-error" };
43
- }
44
16
  }
45
- function extractTables(html) {
46
- try {
47
- const { document } = parseHTML(html);
48
- const tables = Array.from(document.querySelectorAll("table"));
49
- return tables.map((table) => {
50
- const headers = [];
51
- const rows = [];
52
- const ths = [...table.querySelectorAll("thead th, thead td, tr:first-child th")];
53
- if (ths.length > 0) {
54
- ths.forEach((th, i) => headers.push(th.textContent?.trim() || `col${i + 1}`));
55
- }
56
- const trs = [...table.querySelectorAll("tbody tr, tr")];
57
- for (const tr of trs) {
58
- const cells = [...tr.querySelectorAll("td, th")];
59
- if (cells.length === 0) continue;
60
- if (headers.length === 0) {
61
- cells.forEach((_, i) => headers.push(`col${i + 1}`));
17
+ export function extractByXPath(html, xpath) {
18
+ try {
19
+ const { document, XPathResult } = parseHTML(html);
20
+ const result = document.evaluate(xpath, document, null, XPathResult?.ANY_TYPE ?? 0, null);
21
+ const matches = [];
22
+ let node = result.iterateNext?.();
23
+ while (node) {
24
+ const text = node.textContent?.trim() ?? node.nodeValue?.trim() ?? '';
25
+ if (text)
26
+ matches.push(text);
27
+ node = result.iterateNext?.();
62
28
  }
63
- const row = {};
64
- cells.forEach((cell, i) => {
65
- const key = headers[i] || `col${i + 1}`;
66
- row[key] = cell.textContent?.trim() ?? "";
67
- });
68
- if (Object.values(row).some((v) => v)) rows.push(row);
69
- }
70
- return rows;
71
- }).filter((t) => t.length > 0);
72
- } catch {
73
- return [];
74
- }
75
- }
76
- function extractMetadata(html, url) {
77
- const meta = {
78
- title: null,
79
- description: null,
80
- keywords: null,
81
- ogTitle: null,
82
- ogDescription: null,
83
- ogImage: null,
84
- ogUrl: null,
85
- ogType: null,
86
- ogSiteName: null,
87
- ogLocale: null,
88
- twitterCard: null,
89
- twitterTitle: null,
90
- twitterDescription: null,
91
- twitterImage: null,
92
- twitterSite: null,
93
- canonical: null,
94
- robots: null,
95
- hreflang: [],
96
- themeColor: null,
97
- favicon: null,
98
- jsonLd: [],
99
- schemaTypes: [],
100
- lang: null
101
- };
102
- try {
103
- const { document } = parseHTML(html);
104
- meta.title = document.title ?? document.querySelector("title")?.textContent?.trim() ?? null;
105
- meta.lang = document.documentElement?.getAttribute("lang") ?? null;
106
- const metas = [...document.querySelectorAll("meta")];
107
- for (const m of metas) {
108
- const name = m.getAttribute("name")?.toLowerCase() ?? "";
109
- const prop = m.getAttribute("property")?.toLowerCase() ?? "";
110
- const content = m.getAttribute("content") ?? "";
111
- if (name === "description") meta.description = content;
112
- if (name === "keywords") meta.keywords = content;
113
- if (name === "robots") meta.robots = content;
114
- if (name === "twitter:card") meta.twitterCard = content;
115
- if (name === "twitter:title") meta.twitterTitle = content;
116
- if (name === "twitter:description") meta.twitterDescription = content;
117
- if (name === "twitter:image") meta.twitterImage = content;
118
- if (name === "twitter:site") meta.twitterSite = content;
119
- if (name === "theme-color") meta.themeColor = content;
120
- if (prop === "og:title") meta.ogTitle = content;
121
- if (prop === "og:description") meta.ogDescription = content;
122
- if (prop === "og:image") meta.ogImage = content;
123
- if (prop === "og:url") meta.ogUrl = content;
124
- if (prop === "og:type") meta.ogType = content;
125
- if (prop === "og:site_name") meta.ogSiteName = content;
126
- if (prop === "og:locale") meta.ogLocale = content;
29
+ return { matches, count: matches.length, method: 'xpath' };
127
30
  }
128
- const canonical = document.querySelector('link[rel="canonical"]');
129
- if (canonical) meta.canonical = canonical.getAttribute("href") ?? null;
130
- const faviconEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
131
- if (faviconEl) meta.favicon = faviconEl.getAttribute("href") ?? null;
132
- const hreflangEls = [...document.querySelectorAll('link[rel="alternate"][hreflang]')];
133
- for (const el of hreflangEls) {
134
- const lang = el.getAttribute("hreflang") ?? "";
135
- const href = el.getAttribute("href") ?? "";
136
- if (lang && href) meta.hreflang.push({ lang, href });
31
+ catch (e) {
32
+ return { matches: [], count: 0, method: 'xpath-error' };
137
33
  }
138
- const jsonLdScripts = [...document.querySelectorAll('script[type="application/ld+json"]')];
139
- for (const s of jsonLdScripts) {
140
- try {
141
- const parsed = JSON.parse(s.textContent ?? "{}");
142
- meta.jsonLd.push(parsed);
143
- const types = Array.isArray(parsed) ? parsed.map((p) => p["@type"]).flat() : [parsed["@type"]];
144
- meta.schemaTypes.push(...types.filter(Boolean));
145
- } catch {
146
- }
34
+ }
35
+ export function extractByRegex(text, pattern, flags) {
36
+ try {
37
+ const re = new RegExp(pattern, (flags ?? 'g').includes('g') ? flags : (flags ?? '') + 'g');
38
+ const matches = [];
39
+ let m;
40
+ while ((m = re.exec(text)) !== null) {
41
+ matches.push(m[1] ?? m[0]);
42
+ if (!re.global)
43
+ break;
44
+ }
45
+ return { matches, count: matches.length, method: 'regex' };
46
+ }
47
+ catch {
48
+ return { matches: [], count: 0, method: 'regex-error' };
147
49
  }
148
- } catch {
149
- }
150
- return meta;
151
50
  }
152
- function extractStructuredProduct(jsonLd) {
153
- for (const item of jsonLd) {
51
+ export function extractTables(html) {
154
52
  try {
155
- const type = item["@type"];
156
- const isProduct = Array.isArray(type) ? type.some((t) => typeof t === "string" && t.toLowerCase() === "product") : typeof type === "string" && type.toLowerCase() === "product";
157
- if (!isProduct) continue;
158
- const i = item;
159
- const offers = Array.isArray(i.offers) ? i.offers[0] : i.offers;
160
- const rawPrice = offers?.price;
161
- const price = rawPrice != null ? parseFloat(rawPrice) : NaN;
162
- const rawRating = i.aggregateRating?.ratingValue;
163
- const rating = rawRating != null ? parseFloat(rawRating) : NaN;
164
- const rawReviewCount = i.aggregateRating?.reviewCount;
165
- const reviewCount = rawReviewCount != null ? parseInt(rawReviewCount, 10) : NaN;
166
- const rawAvailability = offers?.availability;
167
- const availability = rawAvailability ? rawAvailability.replace(/^https?:\/\/schema\.org\//, "") : null;
168
- const rawImage = i.image;
169
- const image = Array.isArray(rawImage) ? rawImage[0] ?? null : rawImage ?? null;
170
- const brand = i.brand?.name ?? i.brand ?? null;
171
- return {
172
- name: i.name ?? null,
173
- description: i.description ?? null,
174
- brand: typeof brand === "string" ? brand : null,
175
- sku: i.sku ?? null,
176
- price: isNaN(price) ? null : price,
177
- priceCurrency: offers?.priceCurrency ?? null,
178
- availability,
179
- rating: isNaN(rating) ? null : rating,
180
- reviewCount: isNaN(reviewCount) ? null : reviewCount,
181
- image,
182
- url: i.url ?? null
183
- };
184
- } catch {
53
+ const { document } = parseHTML(html);
54
+ const tables = Array.from(document.querySelectorAll('table'));
55
+ return tables.map((table) => {
56
+ const headers = [];
57
+ const rows = [];
58
+ const ths = [...table.querySelectorAll('thead th, thead td, tr:first-child th')];
59
+ if (ths.length > 0) {
60
+ ths.forEach((th, i) => headers.push(th.textContent?.trim() || `col${i + 1}`));
61
+ }
62
+ const trs = [...table.querySelectorAll('tbody tr, tr')];
63
+ for (const tr of trs) {
64
+ const cells = [...tr.querySelectorAll('td, th')];
65
+ if (cells.length === 0)
66
+ continue;
67
+ if (headers.length === 0) {
68
+ cells.forEach((_, i) => headers.push(`col${i + 1}`));
69
+ }
70
+ const row = {};
71
+ cells.forEach((cell, i) => {
72
+ const key = headers[i] || `col${i + 1}`;
73
+ row[key] = cell.textContent?.trim() ?? '';
74
+ });
75
+ if (Object.values(row).some(v => v))
76
+ rows.push(row);
77
+ }
78
+ return rows;
79
+ }).filter(t => t.length > 0);
80
+ }
81
+ catch {
82
+ return [];
185
83
  }
186
- }
187
- return null;
188
84
  }
189
- function extractImages(html, baseUrl) {
190
- try {
191
- const { document } = parseHTML(html);
192
- const base = new URL(baseUrl);
193
- return Array.from(document.querySelectorAll("img")).map((img) => {
194
- const src = img.getAttribute("src") ?? img.getAttribute("data-src") ?? "";
195
- if (!src) return null;
196
- try {
197
- return {
198
- src: new URL(src, base).href,
199
- alt: img.getAttribute("alt") ?? "",
200
- width: img.getAttribute("width") ? parseInt(img.getAttribute("width")) : void 0,
201
- height: img.getAttribute("height") ? parseInt(img.getAttribute("height")) : void 0
202
- };
203
- } catch {
204
- return null;
205
- }
206
- }).filter((x) => x !== null);
207
- } catch {
208
- return [];
209
- }
85
+ export function extractMetadata(html, url) {
86
+ const meta = {
87
+ title: null, description: null, keywords: null,
88
+ ogTitle: null, ogDescription: null, ogImage: null, ogUrl: null, ogType: null,
89
+ ogSiteName: null, ogLocale: null,
90
+ twitterCard: null, twitterTitle: null, twitterDescription: null,
91
+ twitterImage: null, twitterSite: null,
92
+ canonical: null, robots: null,
93
+ hreflang: [], themeColor: null, favicon: null,
94
+ jsonLd: [], schemaTypes: [], lang: null,
95
+ };
96
+ try {
97
+ const { document } = parseHTML(html);
98
+ meta.title = document.title ?? document.querySelector('title')?.textContent?.trim() ?? null;
99
+ meta.lang = document.documentElement?.getAttribute('lang') ?? null;
100
+ const metas = [...document.querySelectorAll('meta')];
101
+ for (const m of metas) {
102
+ const name = m.getAttribute('name')?.toLowerCase() ?? '';
103
+ const prop = m.getAttribute('property')?.toLowerCase() ?? '';
104
+ const content = m.getAttribute('content') ?? '';
105
+ if (name === 'description')
106
+ meta.description = content;
107
+ if (name === 'keywords')
108
+ meta.keywords = content;
109
+ if (name === 'robots')
110
+ meta.robots = content;
111
+ if (name === 'twitter:card')
112
+ meta.twitterCard = content;
113
+ if (name === 'twitter:title')
114
+ meta.twitterTitle = content;
115
+ if (name === 'twitter:description')
116
+ meta.twitterDescription = content;
117
+ if (name === 'twitter:image')
118
+ meta.twitterImage = content;
119
+ if (name === 'twitter:site')
120
+ meta.twitterSite = content;
121
+ if (name === 'theme-color')
122
+ meta.themeColor = content;
123
+ if (prop === 'og:title')
124
+ meta.ogTitle = content;
125
+ if (prop === 'og:description')
126
+ meta.ogDescription = content;
127
+ if (prop === 'og:image')
128
+ meta.ogImage = content;
129
+ if (prop === 'og:url')
130
+ meta.ogUrl = content;
131
+ if (prop === 'og:type')
132
+ meta.ogType = content;
133
+ if (prop === 'og:site_name')
134
+ meta.ogSiteName = content;
135
+ if (prop === 'og:locale')
136
+ meta.ogLocale = content;
137
+ }
138
+ const canonical = document.querySelector('link[rel="canonical"]');
139
+ if (canonical)
140
+ meta.canonical = canonical.getAttribute('href') ?? null;
141
+ const faviconEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
142
+ if (faviconEl)
143
+ meta.favicon = faviconEl.getAttribute('href') ?? null;
144
+ const hreflangEls = [...document.querySelectorAll('link[rel="alternate"][hreflang]')];
145
+ for (const el of hreflangEls) {
146
+ const lang = el.getAttribute('hreflang') ?? '';
147
+ const href = el.getAttribute('href') ?? '';
148
+ if (lang && href)
149
+ meta.hreflang.push({ lang, href });
150
+ }
151
+ const jsonLdScripts = [...document.querySelectorAll('script[type="application/ld+json"]')];
152
+ for (const s of jsonLdScripts) {
153
+ try {
154
+ const parsed = JSON.parse(s.textContent ?? '{}');
155
+ meta.jsonLd.push(parsed);
156
+ const types = Array.isArray(parsed) ? parsed.map((p) => p['@type']).flat() : [parsed['@type']];
157
+ meta.schemaTypes.push(...types.filter(Boolean));
158
+ }
159
+ catch { /* skip invalid JSON-LD */ }
160
+ }
161
+ }
162
+ catch { /* non-fatal */ }
163
+ return meta;
210
164
  }
211
- function extractMediaFiles(html, baseUrl) {
212
- const IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|ico|avif)(\?.*)?$/i;
213
- const VIDEO_EXT = /\.(mp4|webm|ogv|mov|avi)(\?.*)?$/i;
214
- const AUDIO_EXT = /\.(mp3|ogg|wav|flac|aac)(\?.*)?$/i;
215
- const DOC_EXT = /\.(pdf|docx?|xlsx?|pptx?|csv|zip)(\?.*)?$/i;
216
- function classify(url, tag) {
217
- if (tag === "img" || IMAGE_EXT.test(url)) return "image";
218
- if (tag === "video" || VIDEO_EXT.test(url)) return "video";
219
- if (tag === "audio" || AUDIO_EXT.test(url)) return "audio";
220
- if (DOC_EXT.test(url)) return "document";
221
- return "document";
222
- }
223
- try {
224
- const { document } = parseHTML(html);
225
- const base = new URL(baseUrl);
226
- const results = [];
227
- const seen = /* @__PURE__ */ new Set();
228
- const selectors = ["img[src],video[src],audio[src],source[src]", "src"];
229
- const attrPairs = [
230
- ["img", "src"],
231
- ["video", "src"],
232
- ["audio", "src"],
233
- ["source", "src"],
234
- ["a", "href"]
235
- ];
236
- for (const [sel, attr] of attrPairs) {
237
- for (const el of [...document.querySelectorAll(sel)]) {
238
- const raw = el.getAttribute(attr) ?? "";
239
- if (!raw) continue;
165
+ export function extractStructuredProduct(jsonLd) {
166
+ for (const item of jsonLd) {
240
167
  try {
241
- const resolved = new URL(raw, base).href;
242
- if (seen.has(resolved)) continue;
243
- if (sel === "a" && !DOC_EXT.test(resolved) && !IMAGE_EXT.test(resolved) && !VIDEO_EXT.test(resolved) && !AUDIO_EXT.test(resolved)) continue;
244
- seen.add(resolved);
245
- results.push({ url: resolved, type: classify(resolved, sel), tag: sel });
246
- } catch {
168
+ const type = item['@type'];
169
+ const isProduct = Array.isArray(type)
170
+ ? type.some((t) => typeof t === 'string' && t.toLowerCase() === 'product')
171
+ : typeof type === 'string' && type.toLowerCase() === 'product';
172
+ if (!isProduct)
173
+ continue;
174
+ const i = item;
175
+ const offers = Array.isArray(i.offers) ? i.offers[0] : i.offers;
176
+ const rawPrice = offers?.price;
177
+ const price = rawPrice != null ? parseFloat(rawPrice) : NaN;
178
+ const rawRating = i.aggregateRating?.ratingValue;
179
+ const rating = rawRating != null ? parseFloat(rawRating) : NaN;
180
+ const rawReviewCount = i.aggregateRating?.reviewCount;
181
+ const reviewCount = rawReviewCount != null ? parseInt(rawReviewCount, 10) : NaN;
182
+ const rawAvailability = offers?.availability;
183
+ const availability = rawAvailability
184
+ ? rawAvailability.replace(/^https?:\/\/schema\.org\//, '')
185
+ : null;
186
+ const rawImage = i.image;
187
+ const image = Array.isArray(rawImage) ? (rawImage[0] ?? null) : (rawImage ?? null);
188
+ const brand = i.brand?.name ?? i.brand ?? null;
189
+ return {
190
+ name: i.name ?? null,
191
+ description: i.description ?? null,
192
+ brand: typeof brand === 'string' ? brand : null,
193
+ sku: i.sku ?? null,
194
+ price: isNaN(price) ? null : price,
195
+ priceCurrency: offers?.priceCurrency ?? null,
196
+ availability,
197
+ rating: isNaN(rating) ? null : rating,
198
+ reviewCount: isNaN(reviewCount) ? null : reviewCount,
199
+ image,
200
+ url: i.url ?? null,
201
+ };
202
+ }
203
+ catch { /* skip malformed item */ }
204
+ }
205
+ return null;
206
+ }
207
+ export function extractImages(html, baseUrl) {
208
+ try {
209
+ const { document } = parseHTML(html);
210
+ const base = new URL(baseUrl);
211
+ return Array.from(document.querySelectorAll('img'))
212
+ .map((img) => {
213
+ const src = img.getAttribute('src') ?? img.getAttribute('data-src') ?? '';
214
+ if (!src)
215
+ return null;
216
+ try {
217
+ return {
218
+ src: new URL(src, base).href,
219
+ alt: (img.getAttribute('alt') ?? ''),
220
+ width: img.getAttribute('width') ? parseInt(img.getAttribute('width')) : undefined,
221
+ height: img.getAttribute('height') ? parseInt(img.getAttribute('height')) : undefined,
222
+ };
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ })
228
+ .filter((x) => x !== null);
229
+ }
230
+ catch {
231
+ return [];
232
+ }
233
+ }
234
+ export function extractMediaFiles(html, baseUrl) {
235
+ const IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|ico|avif)(\?.*)?$/i;
236
+ const VIDEO_EXT = /\.(mp4|webm|ogv|mov|avi)(\?.*)?$/i;
237
+ const AUDIO_EXT = /\.(mp3|ogg|wav|flac|aac)(\?.*)?$/i;
238
+ const DOC_EXT = /\.(pdf|docx?|xlsx?|pptx?|csv|zip)(\?.*)?$/i;
239
+ function classify(url, tag) {
240
+ if (tag === 'img' || IMAGE_EXT.test(url))
241
+ return 'image';
242
+ if (tag === 'video' || VIDEO_EXT.test(url))
243
+ return 'video';
244
+ if (tag === 'audio' || AUDIO_EXT.test(url))
245
+ return 'audio';
246
+ if (DOC_EXT.test(url))
247
+ return 'document';
248
+ return 'document';
249
+ }
250
+ try {
251
+ const { document } = parseHTML(html);
252
+ const base = new URL(baseUrl);
253
+ const results = [];
254
+ const seen = new Set();
255
+ const selectors = ['img[src],video[src],audio[src],source[src]', 'src'];
256
+ const attrPairs = [
257
+ ['img', 'src'], ['video', 'src'], ['audio', 'src'], ['source', 'src'],
258
+ ['a', 'href'],
259
+ ];
260
+ for (const [sel, attr] of attrPairs) {
261
+ for (const el of [...document.querySelectorAll(sel)]) {
262
+ const raw = el.getAttribute(attr) ?? '';
263
+ if (!raw)
264
+ continue;
265
+ try {
266
+ const resolved = new URL(raw, base).href;
267
+ if (seen.has(resolved))
268
+ continue;
269
+ if (sel === 'a' && !DOC_EXT.test(resolved) && !IMAGE_EXT.test(resolved) && !VIDEO_EXT.test(resolved) && !AUDIO_EXT.test(resolved))
270
+ continue;
271
+ seen.add(resolved);
272
+ results.push({ url: resolved, type: classify(resolved, sel), tag: sel });
273
+ }
274
+ catch { /* skip */ }
275
+ }
247
276
  }
248
- }
277
+ return results;
278
+ }
279
+ catch {
280
+ return [];
249
281
  }
250
- return results;
251
- } catch {
252
- return [];
253
- }
254
282
  }
255
- export {
256
- extractByCssSelector,
257
- extractByRegex,
258
- extractByXPath,
259
- extractImages,
260
- extractMediaFiles,
261
- extractMetadata,
262
- extractStructuredProduct,
263
- extractTables
264
- };
265
- //# sourceMappingURL=advanced.js.map
@@ -1,63 +1,62 @@
1
- import crypto from "node:crypto";
2
- function sha256(input) {
3
- return crypto.createHash("sha256").update(input).digest("hex");
1
+ import crypto from 'node:crypto';
2
+ export function sha256(input) {
3
+ return crypto.createHash('sha256').update(input).digest('hex');
4
4
  }
5
- function buildFitMarkdown(markdown, keywords = []) {
6
- const lines = markdown.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
7
- const lowerKeywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
8
- const filtered = lines.filter((line) => {
9
- const plain = line.replace(/[#*_`>\-[\]()]/g, "").trim();
10
- if (plain.length < 3) return false;
11
- if (/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|newsletter)$/i.test(plain)) return false;
12
- if (lowerKeywords.length === 0) return true;
13
- const lower = plain.toLowerCase();
14
- return lowerKeywords.some((kw) => lower.includes(kw)) || /^#{1,4}\s/.test(line);
15
- });
16
- const candidate = filtered.join("\n");
17
- return candidate.length >= 200 ? candidate : markdown;
5
+ export function buildFitMarkdown(markdown, keywords = []) {
6
+ const lines = markdown
7
+ .split('\n')
8
+ .map((line) => line.trimEnd())
9
+ .filter((line) => line.trim().length > 0);
10
+ const lowerKeywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
11
+ const filtered = lines.filter((line) => {
12
+ const plain = line.replace(/[#*_`>\-[\]()]/g, '').trim();
13
+ if (plain.length < 3)
14
+ return false;
15
+ if (/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|newsletter)$/i.test(plain))
16
+ return false;
17
+ if (lowerKeywords.length === 0)
18
+ return true;
19
+ const lower = plain.toLowerCase();
20
+ return lowerKeywords.some((kw) => lower.includes(kw)) || /^#{1,4}\s/.test(line);
21
+ });
22
+ const candidate = filtered.join('\n');
23
+ return candidate.length >= 200 ? candidate : markdown;
18
24
  }
19
- function summarizeStructuredElements(elements) {
20
- const forms = elements.filter((e) => ["email", "password", "text", "tel", "date", "number", "select", "textarea"].includes(e.role));
21
- const actions = elements.filter((e) => e.role === "button" || e.role === "submit" || e.role === "link" || e.role === "a");
22
- return {
23
- elementCount: elements.length,
24
- formFieldCount: forms.length,
25
- actionCount: actions.length,
26
- fields: forms.slice(0, 50).map((e) => ({ label: e.label, role: e.role, expectedData: e.expectedData })),
27
- actions: actions.slice(0, 50).map((e) => ({ label: e.label, role: e.role }))
28
- };
25
+ export function summarizeStructuredElements(elements) {
26
+ const forms = elements.filter((e) => ['email', 'password', 'text', 'tel', 'date', 'number', 'select', 'textarea'].includes(e.role));
27
+ const actions = elements.filter((e) => e.role === 'button' || e.role === 'submit' || e.role === 'link' || e.role === 'a');
28
+ return {
29
+ elementCount: elements.length,
30
+ formFieldCount: forms.length,
31
+ actionCount: actions.length,
32
+ fields: forms.slice(0, 50).map((e) => ({ label: e.label, role: e.role, expectedData: e.expectedData })),
33
+ actions: actions.slice(0, 50).map((e) => ({ label: e.label, role: e.role })),
34
+ };
29
35
  }
30
- function buildCrawlArtifactEnvelope(input) {
31
- const accessibilityTree = input.accessibilityElements ? JSON.stringify(input.accessibilityElements) : void 0;
32
- return {
33
- tenantId: input.tenantId,
34
- projectId: input.projectId,
35
- jobId: input.jobId,
36
- url: input.url,
37
- canonicalUrl: input.canonicalUrl,
38
- screenId: input.screenId,
39
- status: "ok",
40
- outputs: {
41
- screenshot: input.screenshotPath,
42
- html: input.domPath,
43
- markdown: input.markdown,
44
- fitMarkdown: input.fitMarkdown,
45
- accessibilityTree,
46
- structuredJson: input.structuredJson
47
- },
48
- fingerprints: {
49
- textHash: input.markdown ? sha256(input.markdown) : void 0,
50
- domHash: input.domContent ? sha256(input.domContent) : void 0,
51
- accessibilityHash: accessibilityTree ? sha256(accessibilityTree) : void 0,
52
- urlCanonicalHash: sha256(input.canonicalUrl)
53
- },
54
- evidence: input.evidence ?? []
55
- };
36
+ export function buildCrawlArtifactEnvelope(input) {
37
+ const accessibilityTree = input.accessibilityElements ? JSON.stringify(input.accessibilityElements) : undefined;
38
+ return {
39
+ tenantId: input.tenantId,
40
+ projectId: input.projectId,
41
+ jobId: input.jobId,
42
+ url: input.url,
43
+ canonicalUrl: input.canonicalUrl,
44
+ screenId: input.screenId,
45
+ status: 'ok',
46
+ outputs: {
47
+ screenshot: input.screenshotPath,
48
+ html: input.domPath,
49
+ markdown: input.markdown,
50
+ fitMarkdown: input.fitMarkdown,
51
+ accessibilityTree,
52
+ structuredJson: input.structuredJson,
53
+ },
54
+ fingerprints: {
55
+ textHash: input.markdown ? sha256(input.markdown) : undefined,
56
+ domHash: input.domContent ? sha256(input.domContent) : undefined,
57
+ accessibilityHash: accessibilityTree ? sha256(accessibilityTree) : undefined,
58
+ urlCanonicalHash: sha256(input.canonicalUrl),
59
+ },
60
+ evidence: input.evidence ?? [],
61
+ };
56
62
  }
57
- export {
58
- buildCrawlArtifactEnvelope,
59
- buildFitMarkdown,
60
- sha256,
61
- summarizeStructuredElements
62
- };
63
- //# sourceMappingURL=artifact-envelope.js.map