@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.
Files changed (82) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
@@ -1,90 +1,78 @@
1
- /**
2
- * WCAG color contrast checker.
3
- *
4
- * Evaluates text contrast ratios against backgrounds for all visible text elements.
5
- * Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)
6
- * or AAA (7:1 for normal, 4.5:1 for large text) requirements.
7
- *
8
- * Uses computedStyle to get actual rendered colors — accounts for CSS variables
9
- * and inheritance.
10
- */
11
- export async function auditColorContrast(page, url) {
12
- const auditedAt = new Date().toISOString();
13
- const violations = await page.evaluate(() => {
14
- function parseRgb(color) {
15
- const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
16
- return m ? [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1] : null;
17
- }
18
- function luminance([r, g, b]) {
19
- const toLinear = (c) => {
20
- const n = c / 255;
21
- return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
22
- };
23
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
24
- }
25
- function contrastRatio(fg, bg) {
26
- const l1 = luminance(fg);
27
- const l2 = luminance(bg);
28
- const lighter = Math.max(l1, l2);
29
- const darker = Math.min(l1, l2);
30
- return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
31
- }
32
- const violations = [];
33
- const elements = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button'));
34
- let checked = 0;
35
- for (const el of elements) {
36
- if (checked >= 100)
37
- break; // limit to avoid perf hit
38
- const text = (el.textContent ?? '').trim().slice(0, 60);
39
- if (!text)
40
- continue;
41
- const style = window.getComputedStyle(el);
42
- if (style.display === 'none' || style.visibility === 'hidden')
43
- continue;
44
- const fg = parseRgb(style.color);
45
- const bg = parseRgb(style.backgroundColor);
46
- if (!fg || !bg || bg[3] === 0)
47
- continue; // transparent bg — can't check
48
- const fontSize = parseFloat(style.fontSize);
49
- const fontWeight = style.fontWeight;
50
- const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || +fontWeight >= 700));
51
- const ratio = contrastRatio(fg, bg);
52
- const aaRequired = isLargeText ? 3.0 : 4.5;
53
- const aaaRequired = isLargeText ? 4.5 : 7.0;
54
- if (ratio < aaRequired) {
55
- violations.push({
56
- selector: el.tagName.toLowerCase(),
57
- text: text.slice(0, 50),
58
- foreground: style.color,
59
- background: style.backgroundColor,
60
- ratio,
61
- required: aaRequired,
62
- level: 'AA',
63
- isLargeText,
64
- severity: 'fail',
65
- });
66
- }
67
- else if (ratio < aaaRequired) {
68
- violations.push({
69
- selector: el.tagName.toLowerCase(),
70
- text: text.slice(0, 50),
71
- foreground: style.color,
72
- background: style.backgroundColor,
73
- ratio,
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
@@ -1,152 +1,152 @@
1
- export function estimateTokens(text) {
2
- return Math.ceil(text.length / 4);
1
+ function estimateTokens(text) {
2
+ return Math.ceil(text.length / 4);
3
3
  }
4
- export function chunkContent(text, opts) {
5
- const size = opts.chunkSize ?? 2000;
6
- const charSize = size * 4;
7
- const overlap = opts.overlap ?? 200;
8
- const overlapChars = overlap * 4;
9
- switch (opts.strategy) {
10
- case 'fixed': return fixedChunk(text, charSize);
11
- case 'overlap': return overlapChunk(text, charSize, overlapChars);
12
- case 'sentence': return sentenceChunk(text, charSize);
13
- case 'paragraph': return paragraphChunk(text, charSize);
14
- case 'markdown-heading': return markdownHeadingChunk(text, charSize);
15
- case 'semantic': return semanticChunk(text, charSize);
16
- case 'recursive': return recursiveChunk(text, charSize, opts.separators ?? ['\n\n', '\n', '. ', ' ']);
17
- default: return fixedChunk(text, charSize);
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
- return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
29
+ return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
22
30
  }
23
31
  function fixedChunk(text, charSize) {
24
- const chunks = [];
25
- let i = 0;
26
- while (i < text.length) {
27
- const slice = text.slice(i, i + charSize);
28
- chunks.push(makeChunk(slice, chunks.length, i));
29
- i += charSize;
30
- }
31
- return chunks;
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
- const chunks = [];
35
- let i = 0;
36
- while (i < text.length) {
37
- const slice = text.slice(i, i + charSize);
38
- chunks.push(makeChunk(slice, chunks.length, i));
39
- i += charSize - overlapChars;
40
- if (i <= 0)
41
- break;
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
- const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
47
- return groupIntoChunks(sentences, charSize);
53
+ const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
54
+ return groupIntoChunks(sentences, charSize);
48
55
  }
49
56
  function paragraphChunk(text, charSize) {
50
- const paras = text.split(/\n\n+/).filter(p => p.trim());
51
- return groupIntoChunks(paras, charSize);
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
- const chunks = [];
55
- const headingRe = /^(#{1,3}\s.+)$/m;
56
- const sections = text.split(/(?=^#{1,3}\s)/m).filter(s => s.trim());
57
- let pos = 0;
58
- for (const section of sections) {
59
- const headingMatch = section.match(headingRe);
60
- const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, '').trim() : undefined;
61
- if (section.length <= charSize) {
62
- chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
63
- }
64
- else {
65
- // Section too large — sub-chunk by paragraph
66
- const sub = paragraphChunk(section, charSize);
67
- for (const s of sub) {
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
- return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
76
+ pos += section.length;
77
+ }
78
+ return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
74
79
  }
75
80
  function semanticChunk(text, charSize) {
76
- // Like paragraph but merges short paragraphs with the next
77
- const paras = text.split(/\n\n+/).filter(p => p.trim());
78
- const merged = [];
79
- let current = '';
80
- for (const para of paras) {
81
- if (current.length + para.length + 2 <= charSize) {
82
- current = current ? current + '\n\n' + para : para;
83
- }
84
- else {
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
- if (current)
91
- merged.push(current);
92
- return groupIntoChunks(merged, charSize);
91
+ }
92
+ if (current) merged.push(current);
93
+ return groupIntoChunks(merged, charSize);
93
94
  }
94
95
  function recursiveChunk(text, charSize, separators) {
95
- if (text.length <= charSize)
96
- return [makeChunk(text, 0, 0)];
97
- const [sep, ...rest] = separators;
98
- if (!sep)
99
- return fixedChunk(text, charSize);
100
- const parts = text.split(sep).filter(p => p);
101
- const chunks = [];
102
- let pos = 0;
103
- for (const part of parts) {
104
- if (part.length <= charSize) {
105
- chunks.push(makeChunk(part, chunks.length, pos));
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
- return chunks;
109
+ pos += part.length + sep.length;
110
+ }
111
+ return chunks;
115
112
  }
116
113
  function groupIntoChunks(parts, charSize) {
117
- const chunks = [];
118
- let current = '';
119
- let startChar = 0;
120
- let pos = 0;
121
- for (const part of parts) {
122
- if (current.length + part.length + 1 > charSize && current) {
123
- chunks.push(makeChunk(current.trim(), chunks.length, startChar));
124
- startChar = pos;
125
- current = part;
126
- }
127
- else {
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
- if (current.trim())
133
- chunks.push(makeChunk(current.trim(), chunks.length, startChar));
134
- return chunks;
126
+ pos += part.length + 1;
127
+ }
128
+ if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
129
+ return chunks;
135
130
  }
136
- export function mergeSmallChunks(chunks, minTokens) {
137
- const minChars = minTokens * 4;
138
- const result = [];
139
- let i = 0;
140
- while (i < chunks.length) {
141
- if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
142
- const merged = chunks[i].text + '\n\n' + chunks[i + 1].text;
143
- result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
144
- i += 2;
145
- }
146
- else {
147
- result.push({ ...chunks[i], index: result.length });
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
- return result;
144
+ }
145
+ return result;
152
146
  }
147
+ export {
148
+ chunkContent,
149
+ estimateTokens,
150
+ mergeSmallChunks
151
+ };
152
+ //# sourceMappingURL=content-chunker.js.map
@@ -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
- const lines = markdown.split('\n').filter(l => l.trim().length > 0);
14
- const fps = [];
15
- // FP1: heading structure (H1-H6 count distribution)
16
- const headings = lines.filter(l => /^#{1,6} /.test(l));
17
- const hCounts = [0, 0, 0, 0, 0, 0];
18
- for (const h of headings) {
19
- const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
20
- hCounts[lvl - 1]++;
21
- }
22
- fps.push('h:' + hCounts.join(','));
23
- // FP2: link count (grouped into buckets)
24
- const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
25
- fps.push('lk:' + Math.floor(linkCount / 5));
26
- // FP3: list item count (grouped)
27
- const listCount = lines.filter(l => /^[\-\*\+] |^\d+\. /.test(l)).length;
28
- fps.push('li:' + Math.floor(listCount / 3));
29
- // FP4: code block presence
30
- fps.push('code:' + (markdown.includes('```') ? '1' : '0'));
31
- // FP5: table presence
32
- fps.push('tbl:' + (markdown.includes('|') ? '1' : '0'));
33
- // FP6: approximate paragraph count
34
- const paraCount = markdown.split(/\n{2,}/).filter(p => p.trim().length > 30).length;
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
- if (a.length !== b.length)
46
- return 0;
47
- let matches = 0;
48
- for (let i = 0; i < a.length; i++) {
49
- if (a[i] === b[i])
50
- matches++;
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
- export class ContentDeduplicator {
55
- seen = new Map();
56
- threshold;
57
- constructor(threshold = 0.85) {
58
- this.threshold = threshold;
59
- }
60
- check(url, markdown) {
61
- const fps = extractFingerprints(markdown);
62
- for (const [key, entry] of this.seen) {
63
- const score = similarity(fps, entry.fps);
64
- if (score >= this.threshold) {
65
- return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
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