@detiq/crawler 1.1.9 → 1.1.10
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 +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
package/dist/color-contrast.js
CHANGED
|
@@ -1,90 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
required: aaaRequired,
|
|
75
|
-
level: 'AAA',
|
|
76
|
-
isLargeText,
|
|
77
|
-
severity: 'warn',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
checked++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return violations;
|
|
85
|
-
}).catch(() => []);
|
|
86
|
-
const passCount = Math.max(0, 100 - violations.length);
|
|
87
|
-
const criticals = violations.filter(v => v.severity === 'fail').length;
|
|
88
|
-
const score = Math.max(0, 100 - criticals * 15 - violations.filter(v => v.severity === 'warn').length * 3);
|
|
89
|
-
return { url, violations, passCount, score, auditedAt };
|
|
1
|
+
async function auditColorContrast(page, url) {
|
|
2
|
+
const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3
|
+
const violations = await page.evaluate(() => {
|
|
4
|
+
function parseRgb(color) {
|
|
5
|
+
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
6
|
+
return m ? [+m[1], +m[2], +m[3], m[4] !== void 0 ? +m[4] : 1] : null;
|
|
7
|
+
}
|
|
8
|
+
function luminance([r, g, b]) {
|
|
9
|
+
const toLinear = (c) => {
|
|
10
|
+
const n = c / 255;
|
|
11
|
+
return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
|
12
|
+
};
|
|
13
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
14
|
+
}
|
|
15
|
+
function contrastRatio(fg, bg) {
|
|
16
|
+
const l1 = luminance(fg);
|
|
17
|
+
const l2 = luminance(bg);
|
|
18
|
+
const lighter = Math.max(l1, l2);
|
|
19
|
+
const darker = Math.min(l1, l2);
|
|
20
|
+
return Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
|
|
21
|
+
}
|
|
22
|
+
const violations2 = [];
|
|
23
|
+
const elements = Array.from(document.querySelectorAll("p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button"));
|
|
24
|
+
let checked = 0;
|
|
25
|
+
for (const el of elements) {
|
|
26
|
+
if (checked >= 100) break;
|
|
27
|
+
const text = (el.textContent ?? "").trim().slice(0, 60);
|
|
28
|
+
if (!text) continue;
|
|
29
|
+
const style = window.getComputedStyle(el);
|
|
30
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
31
|
+
const fg = parseRgb(style.color);
|
|
32
|
+
const bg = parseRgb(style.backgroundColor);
|
|
33
|
+
if (!fg || !bg || bg[3] === 0) continue;
|
|
34
|
+
const fontSize = parseFloat(style.fontSize);
|
|
35
|
+
const fontWeight = style.fontWeight;
|
|
36
|
+
const isLargeText = fontSize >= 18 || fontSize >= 14 && (fontWeight === "bold" || +fontWeight >= 700);
|
|
37
|
+
const ratio = contrastRatio(fg, bg);
|
|
38
|
+
const aaRequired = isLargeText ? 3 : 4.5;
|
|
39
|
+
const aaaRequired = isLargeText ? 4.5 : 7;
|
|
40
|
+
if (ratio < aaRequired) {
|
|
41
|
+
violations2.push({
|
|
42
|
+
selector: el.tagName.toLowerCase(),
|
|
43
|
+
text: text.slice(0, 50),
|
|
44
|
+
foreground: style.color,
|
|
45
|
+
background: style.backgroundColor,
|
|
46
|
+
ratio,
|
|
47
|
+
required: aaRequired,
|
|
48
|
+
level: "AA",
|
|
49
|
+
isLargeText,
|
|
50
|
+
severity: "fail"
|
|
51
|
+
});
|
|
52
|
+
} else if (ratio < aaaRequired) {
|
|
53
|
+
violations2.push({
|
|
54
|
+
selector: el.tagName.toLowerCase(),
|
|
55
|
+
text: text.slice(0, 50),
|
|
56
|
+
foreground: style.color,
|
|
57
|
+
background: style.backgroundColor,
|
|
58
|
+
ratio,
|
|
59
|
+
required: aaaRequired,
|
|
60
|
+
level: "AAA",
|
|
61
|
+
isLargeText,
|
|
62
|
+
severity: "warn"
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
checked++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return violations2;
|
|
69
|
+
}).catch(() => []);
|
|
70
|
+
const passCount = Math.max(0, 100 - violations.length);
|
|
71
|
+
const criticals = violations.filter((v) => v.severity === "fail").length;
|
|
72
|
+
const score = Math.max(0, 100 - criticals * 15 - violations.filter((v) => v.severity === "warn").length * 3);
|
|
73
|
+
return { url, violations, passCount, score, auditedAt };
|
|
90
74
|
}
|
|
75
|
+
export {
|
|
76
|
+
auditColorContrast
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=color-contrast.js.map
|
package/dist/content-chunker.js
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
function estimateTokens(text) {
|
|
2
|
+
return Math.ceil(text.length / 4);
|
|
3
3
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
function chunkContent(text, opts) {
|
|
5
|
+
const size = opts.chunkSize ?? 2e3;
|
|
6
|
+
const charSize = size * 4;
|
|
7
|
+
const overlap = opts.overlap ?? 200;
|
|
8
|
+
const overlapChars = overlap * 4;
|
|
9
|
+
switch (opts.strategy) {
|
|
10
|
+
case "fixed":
|
|
11
|
+
return fixedChunk(text, charSize);
|
|
12
|
+
case "overlap":
|
|
13
|
+
return overlapChunk(text, charSize, overlapChars);
|
|
14
|
+
case "sentence":
|
|
15
|
+
return sentenceChunk(text, charSize);
|
|
16
|
+
case "paragraph":
|
|
17
|
+
return paragraphChunk(text, charSize);
|
|
18
|
+
case "markdown-heading":
|
|
19
|
+
return markdownHeadingChunk(text, charSize);
|
|
20
|
+
case "semantic":
|
|
21
|
+
return semanticChunk(text, charSize);
|
|
22
|
+
case "recursive":
|
|
23
|
+
return recursiveChunk(text, charSize, opts.separators ?? ["\n\n", "\n", ". ", " "]);
|
|
24
|
+
default:
|
|
25
|
+
return fixedChunk(text, charSize);
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
function makeChunk(text, index, startChar, heading) {
|
|
21
|
-
|
|
29
|
+
return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
|
|
22
30
|
}
|
|
23
31
|
function fixedChunk(text, charSize) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < text.length) {
|
|
35
|
+
const slice = text.slice(i, i + charSize);
|
|
36
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
37
|
+
i += charSize;
|
|
38
|
+
}
|
|
39
|
+
return chunks;
|
|
32
40
|
}
|
|
33
41
|
function overlapChunk(text, charSize, overlapChars) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return chunks;
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < text.length) {
|
|
45
|
+
const slice = text.slice(i, i + charSize);
|
|
46
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
47
|
+
i += charSize - overlapChars;
|
|
48
|
+
if (i <= 0) break;
|
|
49
|
+
}
|
|
50
|
+
return chunks;
|
|
44
51
|
}
|
|
45
52
|
function sentenceChunk(text, charSize) {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
|
|
54
|
+
return groupIntoChunks(sentences, charSize);
|
|
48
55
|
}
|
|
49
56
|
function paragraphChunk(text, charSize) {
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const paras = text.split(/\n\n+/).filter((p) => p.trim());
|
|
58
|
+
return groupIntoChunks(paras, charSize);
|
|
52
59
|
}
|
|
53
60
|
function markdownHeadingChunk(text, charSize) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
pos += section.length;
|
|
61
|
+
const chunks = [];
|
|
62
|
+
const headingRe = /^(#{1,3}\s.+)$/m;
|
|
63
|
+
const sections = text.split(/(?=^#{1,3}\s)/m).filter((s) => s.trim());
|
|
64
|
+
let pos = 0;
|
|
65
|
+
for (const section of sections) {
|
|
66
|
+
const headingMatch = section.match(headingRe);
|
|
67
|
+
const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, "").trim() : void 0;
|
|
68
|
+
if (section.length <= charSize) {
|
|
69
|
+
chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
|
|
70
|
+
} else {
|
|
71
|
+
const sub = paragraphChunk(section, charSize);
|
|
72
|
+
for (const s of sub) {
|
|
73
|
+
chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
pos += section.length;
|
|
77
|
+
}
|
|
78
|
+
return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
|
|
74
79
|
}
|
|
75
80
|
function semanticChunk(text, charSize) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (current)
|
|
86
|
-
merged.push(current);
|
|
87
|
-
current = para;
|
|
88
|
-
}
|
|
81
|
+
const paras = text.split(/\n\n+/).filter((p) => p.trim());
|
|
82
|
+
const merged = [];
|
|
83
|
+
let current = "";
|
|
84
|
+
for (const para of paras) {
|
|
85
|
+
if (current.length + para.length + 2 <= charSize) {
|
|
86
|
+
current = current ? current + "\n\n" + para : para;
|
|
87
|
+
} else {
|
|
88
|
+
if (current) merged.push(current);
|
|
89
|
+
current = para;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
}
|
|
92
|
+
if (current) merged.push(current);
|
|
93
|
+
return groupIntoChunks(merged, charSize);
|
|
93
94
|
}
|
|
94
95
|
function recursiveChunk(text, charSize, separators) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
else {
|
|
108
|
-
const sub = recursiveChunk(part, charSize, rest);
|
|
109
|
-
for (const s of sub)
|
|
110
|
-
chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
|
|
111
|
-
}
|
|
112
|
-
pos += part.length + sep.length;
|
|
96
|
+
if (text.length <= charSize) return [makeChunk(text, 0, 0)];
|
|
97
|
+
const [sep, ...rest] = separators;
|
|
98
|
+
if (!sep) return fixedChunk(text, charSize);
|
|
99
|
+
const parts = text.split(sep).filter((p) => p);
|
|
100
|
+
const chunks = [];
|
|
101
|
+
let pos = 0;
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (part.length <= charSize) {
|
|
104
|
+
chunks.push(makeChunk(part, chunks.length, pos));
|
|
105
|
+
} else {
|
|
106
|
+
const sub = recursiveChunk(part, charSize, rest);
|
|
107
|
+
for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
|
|
113
108
|
}
|
|
114
|
-
|
|
109
|
+
pos += part.length + sep.length;
|
|
110
|
+
}
|
|
111
|
+
return chunks;
|
|
115
112
|
}
|
|
116
113
|
function groupIntoChunks(parts, charSize) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
current = current ? current + ' ' + part : part;
|
|
129
|
-
}
|
|
130
|
-
pos += part.length + 1;
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let current = "";
|
|
116
|
+
let startChar = 0;
|
|
117
|
+
let pos = 0;
|
|
118
|
+
for (const part of parts) {
|
|
119
|
+
if (current.length + part.length + 1 > charSize && current) {
|
|
120
|
+
chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
121
|
+
startChar = pos;
|
|
122
|
+
current = part;
|
|
123
|
+
} else {
|
|
124
|
+
current = current ? current + " " + part : part;
|
|
131
125
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
pos += part.length + 1;
|
|
127
|
+
}
|
|
128
|
+
if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
129
|
+
return chunks;
|
|
135
130
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
i++;
|
|
149
|
-
}
|
|
131
|
+
function mergeSmallChunks(chunks, minTokens) {
|
|
132
|
+
const minChars = minTokens * 4;
|
|
133
|
+
const result = [];
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < chunks.length) {
|
|
136
|
+
if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
|
|
137
|
+
const merged = chunks[i].text + "\n\n" + chunks[i + 1].text;
|
|
138
|
+
result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
|
|
139
|
+
i += 2;
|
|
140
|
+
} else {
|
|
141
|
+
result.push({ ...chunks[i], index: result.length });
|
|
142
|
+
i++;
|
|
150
143
|
}
|
|
151
|
-
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
152
146
|
}
|
|
147
|
+
export {
|
|
148
|
+
chunkContent,
|
|
149
|
+
estimateTokens,
|
|
150
|
+
mergeSmallChunks
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=content-chunker.js.map
|
package/dist/content-dedup.js
CHANGED
|
@@ -1,77 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content deduplication — detect near-duplicate pages during crawl.
|
|
3
|
-
*
|
|
4
|
-
* Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),
|
|
5
|
-
* we use 8 structural fingerprints extracted from the markdown to detect pages that
|
|
6
|
-
* share the same layout/template even if the data differs.
|
|
7
|
-
*
|
|
8
|
-
* Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).
|
|
9
|
-
* These waste crawl budget and generate redundant test cases.
|
|
10
|
-
*/
|
|
11
|
-
/** Extract structural fingerprints from markdown — layout-sensitive, data-insensitive */
|
|
12
1
|
function extractFingerprints(markdown) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fps.push('p:' + Math.floor(paraCount / 2));
|
|
36
|
-
// FP7: first heading text (normalized)
|
|
37
|
-
const firstH = headings[0]?.replace(/^#{1,6} /, '').toLowerCase().replace(/[^a-z0-9]/g, '') ?? '';
|
|
38
|
-
fps.push('fh:' + firstH.slice(0, 20));
|
|
39
|
-
// FP8: word count bucket
|
|
40
|
-
const wordCount = markdown.split(/\s+/).length;
|
|
41
|
-
fps.push('wc:' + Math.floor(wordCount / 50));
|
|
42
|
-
return fps;
|
|
2
|
+
const lines = markdown.split("\n").filter((l) => l.trim().length > 0);
|
|
3
|
+
const fps = [];
|
|
4
|
+
const headings = lines.filter((l) => /^#{1,6} /.test(l));
|
|
5
|
+
const hCounts = [0, 0, 0, 0, 0, 0];
|
|
6
|
+
for (const h of headings) {
|
|
7
|
+
const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
|
|
8
|
+
hCounts[lvl - 1]++;
|
|
9
|
+
}
|
|
10
|
+
fps.push("h:" + hCounts.join(","));
|
|
11
|
+
const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
|
|
12
|
+
fps.push("lk:" + Math.floor(linkCount / 5));
|
|
13
|
+
const listCount = lines.filter((l) => /^[\-\*\+] |^\d+\. /.test(l)).length;
|
|
14
|
+
fps.push("li:" + Math.floor(listCount / 3));
|
|
15
|
+
fps.push("code:" + (markdown.includes("```") ? "1" : "0"));
|
|
16
|
+
fps.push("tbl:" + (markdown.includes("|") ? "1" : "0"));
|
|
17
|
+
const paraCount = markdown.split(/\n{2,}/).filter((p) => p.trim().length > 30).length;
|
|
18
|
+
fps.push("p:" + Math.floor(paraCount / 2));
|
|
19
|
+
const firstH = headings[0]?.replace(/^#{1,6} /, "").toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
|
|
20
|
+
fps.push("fh:" + firstH.slice(0, 20));
|
|
21
|
+
const wordCount = markdown.split(/\s+/).length;
|
|
22
|
+
fps.push("wc:" + Math.floor(wordCount / 50));
|
|
23
|
+
return fps;
|
|
43
24
|
}
|
|
44
25
|
function similarity(a, b) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return matches / a.length;
|
|
26
|
+
if (a.length !== b.length) return 0;
|
|
27
|
+
let matches = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
if (a[i] === b[i]) matches++;
|
|
30
|
+
}
|
|
31
|
+
return matches / a.length;
|
|
53
32
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
this.seen.set(url, { fps, url });
|
|
69
|
-
return { isDuplicate: false, similarityScore: 1.0 };
|
|
70
|
-
}
|
|
71
|
-
size() {
|
|
72
|
-
return this.seen.size;
|
|
73
|
-
}
|
|
74
|
-
reset() {
|
|
75
|
-
this.seen.clear();
|
|
33
|
+
class ContentDeduplicator {
|
|
34
|
+
seen = /* @__PURE__ */ new Map();
|
|
35
|
+
threshold;
|
|
36
|
+
constructor(threshold = 0.85) {
|
|
37
|
+
this.threshold = threshold;
|
|
38
|
+
}
|
|
39
|
+
check(url, markdown) {
|
|
40
|
+
const fps = extractFingerprints(markdown);
|
|
41
|
+
for (const [key, entry] of this.seen) {
|
|
42
|
+
const score = similarity(fps, entry.fps);
|
|
43
|
+
if (score >= this.threshold) {
|
|
44
|
+
return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
|
|
45
|
+
}
|
|
76
46
|
}
|
|
47
|
+
this.seen.set(url, { fps, url });
|
|
48
|
+
return { isDuplicate: false, similarityScore: 1 };
|
|
49
|
+
}
|
|
50
|
+
size() {
|
|
51
|
+
return this.seen.size;
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.seen.clear();
|
|
55
|
+
}
|
|
77
56
|
}
|
|
57
|
+
export {
|
|
58
|
+
ContentDeduplicator
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=content-dedup.js.map
|