@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.
- package/dist/accessibility-auditor.js +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
|
@@ -1,265 +1,282 @@
|
|
|
1
|
-
import { parseHTML } from
|
|
2
|
-
function extractByCssSelector(html, selector, attr) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
153
|
-
for (const item of jsonLd) {
|
|
51
|
+
export function extractTables(html) {
|
|
154
52
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
212
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
2
|
-
function sha256(input) {
|
|
3
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|