@detiq/crawler 1.0.6 → 1.0.8
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/crawler.js +680 -38
- package/dist/crawler.js.map +3 -3
- package/dist/extractors/advanced.js +20 -0
- package/dist/extractors/advanced.js.map +2 -2
- package/dist/interactive-probe.js +334 -2
- package/dist/interactive-probe.js.map +3 -3
- package/dist/screen-reader.js +4 -3
- package/dist/screen-reader.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +768 -52
- package/src/extractors/advanced.ts +27 -1
- package/src/interactive-probe.ts +407 -3
- package/src/job-queue.ts +38 -1
- package/src/screen-reader.ts +12 -6
- package/types/extractors/advanced.d.ts +10 -0
- package/types/interactive-probe.d.ts +36 -0
package/dist/crawler.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chromium } from "rebrowser-playwright-core";
|
|
|
3
3
|
import { chromium as stealthChromium } from "playwright-extra";
|
|
4
4
|
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync as existsSyncFs, readFileSync } from "node:fs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
stealthChromium.use(StealthPlugin());
|
|
9
9
|
import { z } from "zod";
|
|
@@ -16,7 +16,7 @@ import { pHashVerdict, mergeAiVisualVerdict } from "./visual-diff-verdict.js";
|
|
|
16
16
|
import { skyvernFallback, skyvernNavigate } from "./skyvern.js";
|
|
17
17
|
import { loginWithStagehand } from "./ai-login.js";
|
|
18
18
|
import { readScreen } from "./screen-reader.js";
|
|
19
|
-
import { probeAllScreens } from "./interactive-probe.js";
|
|
19
|
+
import { probeAllScreens, probeKeyboardNav } from "./interactive-probe.js";
|
|
20
20
|
import { computeElementHash } from "./element-hash.js";
|
|
21
21
|
import { logger } from "./logger.js";
|
|
22
22
|
import { getSsoToken } from "./sso-token.js";
|
|
@@ -43,6 +43,7 @@ import { auditColorContrast } from "./color-contrast.js";
|
|
|
43
43
|
import { detectFeatureFlags } from "./feature-flag-detector.js";
|
|
44
44
|
import { detectLanguage } from "./language-detector.js";
|
|
45
45
|
import { detectForms } from "./form-detector.js";
|
|
46
|
+
import { auditAccessibility } from "./accessibility-auditor.js";
|
|
46
47
|
import { RedirectRegistry } from "./redirect-tracker.js";
|
|
47
48
|
import { DeadLinkTracker } from "./dead-link-detector.js";
|
|
48
49
|
import { screensToSitemapUrls, writeSitemap } from "./sitemap-writer.js";
|
|
@@ -1262,7 +1263,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1262
1263
|
const discovered = [];
|
|
1263
1264
|
const performanceMetrics = [];
|
|
1264
1265
|
const seenDomHashes = new Set(opts.knownDomHashes ?? []);
|
|
1265
|
-
const SKIP_PATH = /\/(terms|privacy|dpa|legal|cookie|changelog|blog|
|
|
1266
|
+
const SKIP_PATH = /\/(terms|privacy|dpa|legal|cookie-policy|changelog|blog|imprint|gdpr|compliance|acceptable-use|security-policy|press|sitemap)([-/]|$)/i;
|
|
1266
1267
|
const segIdx = opts.segmentIndex ?? 1;
|
|
1267
1268
|
const robotsCache = opts.respectRobotsTxt ? new RobotsCache() : null;
|
|
1268
1269
|
let avgNavMs = 0;
|
|
@@ -1678,8 +1679,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1678
1679
|
try {
|
|
1679
1680
|
const _require = createRequire(import.meta.url);
|
|
1680
1681
|
const axePath = _require.resolve("axe-core");
|
|
1681
|
-
const
|
|
1682
|
-
await page.
|
|
1682
|
+
const axeContent = readFileSync(axePath, "utf8");
|
|
1683
|
+
await page.evaluate(axeContent);
|
|
1683
1684
|
const result = await page.evaluate(async () => {
|
|
1684
1685
|
return window.axe.run(document, {
|
|
1685
1686
|
runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "best-practice"] },
|
|
@@ -1696,7 +1697,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1696
1697
|
}));
|
|
1697
1698
|
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
1698
1699
|
return { violations, violationCount: violations.length, criticalCount };
|
|
1699
|
-
} catch {
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
logger.warn({ err: String(err?.message ?? err) }, "[crawler] axe-core a11y audit failed (non-fatal)");
|
|
1700
1702
|
return null;
|
|
1701
1703
|
}
|
|
1702
1704
|
})();
|
|
@@ -1808,12 +1810,13 @@ async function crawlProject(opts, onProgress) {
|
|
|
1808
1810
|
})();
|
|
1809
1811
|
void (async () => {
|
|
1810
1812
|
const screenId = screen.id;
|
|
1811
|
-
const [seoR, contrastR, flagsR, langR, formsR] = await Promise.allSettled([
|
|
1813
|
+
const [seoR, contrastR, flagsR, langR, formsR, a11yManualR] = await Promise.allSettled([
|
|
1812
1814
|
checkSeo(page, url),
|
|
1813
1815
|
auditColorContrast(page, url),
|
|
1814
1816
|
detectFeatureFlags(page, url),
|
|
1815
1817
|
detectLanguage(page, url),
|
|
1816
|
-
detectForms(page)
|
|
1818
|
+
detectForms(page),
|
|
1819
|
+
auditAccessibility(page, url)
|
|
1817
1820
|
]);
|
|
1818
1821
|
const base = { screenId, jobId: opts.jobId };
|
|
1819
1822
|
if (seoR.status === "fulfilled")
|
|
@@ -1831,11 +1834,23 @@ async function crawlProject(opts, onProgress) {
|
|
|
1831
1834
|
if (formsR.status === "fulfilled" && Array.isArray(formsR.value) && formsR.value.length)
|
|
1832
1835
|
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, { ...base, type: "FORMS_DETECTED", payload: { forms: formsR.value } }).catch(() => {
|
|
1833
1836
|
});
|
|
1837
|
+
if (a11yManualR.status === "fulfilled" && a11yManualR.value.violations?.length)
|
|
1838
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, { ...base, type: "A11Y_MANUAL_AUDIT", payload: a11yManualR.value }).catch(() => {
|
|
1839
|
+
});
|
|
1834
1840
|
})();
|
|
1835
1841
|
try {
|
|
1836
|
-
await page.addStyleTag({ content: "*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }" });
|
|
1842
|
+
await page.addStyleTag({ content: "*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; animation-play-state: paused !important; animation-fill-mode: both !important; }" });
|
|
1837
1843
|
} catch {
|
|
1838
1844
|
}
|
|
1845
|
+
await page.evaluate(() => {
|
|
1846
|
+
try {
|
|
1847
|
+
const noop = () => 0;
|
|
1848
|
+
Object.defineProperty(window, "requestAnimationFrame", { value: noop, configurable: true });
|
|
1849
|
+
Object.defineProperty(window, "cancelAnimationFrame", { value: noop, configurable: true });
|
|
1850
|
+
} catch {
|
|
1851
|
+
}
|
|
1852
|
+
}).catch(() => {
|
|
1853
|
+
});
|
|
1839
1854
|
const canonUrl2 = canonicalize(url);
|
|
1840
1855
|
const linkRecipe = await AppBrain.getActionRecipe(
|
|
1841
1856
|
opts.tenantId,
|
|
@@ -1901,9 +1916,10 @@ async function crawlProject(opts, onProgress) {
|
|
|
1901
1916
|
const fallback = err?.domLinks ?? [];
|
|
1902
1917
|
if (err?.isRateLimit) {
|
|
1903
1918
|
aiErrorStreak++;
|
|
1904
|
-
if (aiErrorStreak >=
|
|
1905
|
-
aiSkipCount =
|
|
1906
|
-
|
|
1919
|
+
if (aiErrorStreak >= 5) {
|
|
1920
|
+
aiSkipCount = 3;
|
|
1921
|
+
aiErrorStreak = 0;
|
|
1922
|
+
logger.warn("[crawler] 5 consecutive AI rate limits \u2014 pausing AI link discovery for next 3 screens then retrying");
|
|
1907
1923
|
}
|
|
1908
1924
|
if (!aiRateLimitWarned) {
|
|
1909
1925
|
aiRateLimitWarned = true;
|
|
@@ -1917,6 +1933,18 @@ async function crawlProject(opts, onProgress) {
|
|
|
1917
1933
|
]);
|
|
1918
1934
|
logger.info({ url, screenId: screen.id, version: screen.version, elements: elements.length, links: nextLinks.length, atSample: atElements.slice(0, 3).map((a) => a.label) }, "[crawler] screen captured");
|
|
1919
1935
|
console.log("[crawler] AT:", JSON.stringify(atElements.slice(0, 5)));
|
|
1936
|
+
probeKeyboardNav(page).then((kbResult) => {
|
|
1937
|
+
if (kbResult.focusableCount > 0) {
|
|
1938
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
1939
|
+
screenId: screen.id,
|
|
1940
|
+
jobId: opts.jobId,
|
|
1941
|
+
type: "KEYBOARD_NAV",
|
|
1942
|
+
payload: kbResult
|
|
1943
|
+
}).catch(() => {
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}).catch(() => {
|
|
1947
|
+
});
|
|
1920
1948
|
try {
|
|
1921
1949
|
const domContent = await capturePageHtml(page);
|
|
1922
1950
|
if (!domContent) {
|
|
@@ -1957,15 +1985,16 @@ async function crawlProject(opts, onProgress) {
|
|
|
1957
1985
|
try {
|
|
1958
1986
|
const llmCfg = await getCachedCrawlLLM(opts.tenantId);
|
|
1959
1987
|
const snippet = extracted.markdown.slice(0, 1200);
|
|
1988
|
+
if (snippet.trim().length < 80) return void 0;
|
|
1960
1989
|
const { text, usage } = await askTracked(
|
|
1961
|
-
"You are a QA analyst.
|
|
1990
|
+
"You are a QA analyst. Output ONLY a single complete sentence (no preamble, no quotes) describing what this screen does or shows. Be specific. Stop after the first sentence.",
|
|
1962
1991
|
`Screen: "${name}"
|
|
1963
1992
|
URL: ${url}
|
|
1964
1993
|
|
|
1965
1994
|
Content:
|
|
1966
1995
|
${snippet}`,
|
|
1967
1996
|
llmCfg,
|
|
1968
|
-
|
|
1997
|
+
200
|
|
1969
1998
|
);
|
|
1970
1999
|
if (usage) {
|
|
1971
2000
|
recordLLMCall({
|
|
@@ -1978,7 +2007,9 @@ ${snippet}`,
|
|
|
1978
2007
|
}).catch(() => {
|
|
1979
2008
|
});
|
|
1980
2009
|
}
|
|
1981
|
-
|
|
2010
|
+
const clean = text.trim().replace(/^["']|["']$/g, "");
|
|
2011
|
+
const firstSentence = clean.match(/^[^.!?]+[.!?]/)?.[0] ?? clean;
|
|
2012
|
+
return firstSentence.trim();
|
|
1982
2013
|
} catch {
|
|
1983
2014
|
return void 0;
|
|
1984
2015
|
}
|
|
@@ -2835,7 +2866,7 @@ ${snippet}`,
|
|
|
2835
2866
|
}
|
|
2836
2867
|
}
|
|
2837
2868
|
}
|
|
2838
|
-
const shouldRunInteractiveProbe =
|
|
2869
|
+
const shouldRunInteractiveProbe = true;
|
|
2839
2870
|
if (shouldRunInteractiveProbe) {
|
|
2840
2871
|
await onProgress?.(88, "Running interactive probe (capturing real errors & modals)");
|
|
2841
2872
|
const probeInputs = discovered.filter((d) => d.screenId).map((d) => ({ url: d.url, screenId: d.screenId, elements: d.elementFacts ?? [] }));
|
|
@@ -2974,7 +3005,10 @@ ${snippet}`,
|
|
|
2974
3005
|
}
|
|
2975
3006
|
const ELEMENT_NOISE = /^(cookie|consent|privacy|accept|decline|dismiss|close|copy|©|v\d|switch to (dark|light)|dark mode|light mode|toggle (dark|light|theme)|theme (toggle|switch)|share|tweet|follow|subscribe to newsletter|skip to|back to top|\s*$)/i;
|
|
2976
3007
|
async function extractElements(stagehand, page, llmCfg) {
|
|
2977
|
-
const domElements = await domFallbackElements(page)
|
|
3008
|
+
const domElements = await domFallbackElements(page).catch((err) => {
|
|
3009
|
+
console.warn("[crawler] domFallbackElements failed (page context issue or DOM error):", err?.message ?? err);
|
|
3010
|
+
return [];
|
|
3011
|
+
});
|
|
2978
3012
|
const iframeElements = [];
|
|
2979
3013
|
try {
|
|
2980
3014
|
const iframes = page.frames ? page.frames() : [];
|
|
@@ -3009,9 +3043,32 @@ async function extractElements(stagehand, page, llmCfg) {
|
|
|
3009
3043
|
dynamicElements = [];
|
|
3010
3044
|
}
|
|
3011
3045
|
}
|
|
3012
|
-
if (domElements.length >= 2) return [...domElements, ...iframeElements, ...dynamicElements];
|
|
3013
3046
|
const contentElements = await contentFallbackElements(page);
|
|
3014
|
-
|
|
3047
|
+
const crossSourceDedup = (arr) => {
|
|
3048
|
+
const stripPhrases = (s) => s.replace(/^button to expand ['"]?|['"]?\s*details$/gi, "").replace(/\s+(faq\s*)?button$/i, "").replace(/^button:\s*/i, "").toLowerCase().trim().slice(0, 80);
|
|
3049
|
+
const ROLE_PRIORITY = { "faq-question": 10, "faq-answer": 9, "h1": 8, "h2": 7, "h3": 6, "h4": 5, "heading": 8, button: 3, a: 2 };
|
|
3050
|
+
const normMap = /* @__PURE__ */ new Map();
|
|
3051
|
+
const result = [];
|
|
3052
|
+
for (const el of arr) {
|
|
3053
|
+
const norm = stripPhrases(el.meaning);
|
|
3054
|
+
if (!norm) {
|
|
3055
|
+
result.push(el);
|
|
3056
|
+
continue;
|
|
3057
|
+
}
|
|
3058
|
+
const existingIdx = normMap.get(norm);
|
|
3059
|
+
if (existingIdx === void 0) {
|
|
3060
|
+
normMap.set(norm, result.length);
|
|
3061
|
+
result.push(el);
|
|
3062
|
+
} else {
|
|
3063
|
+
const exPri = ROLE_PRIORITY[result[existingIdx].role] ?? 1;
|
|
3064
|
+
const thisPri = ROLE_PRIORITY[el.role] ?? 1;
|
|
3065
|
+
if (thisPri > exPri) result[existingIdx] = el;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
return result;
|
|
3069
|
+
};
|
|
3070
|
+
if (domElements.length >= 2) return crossSourceDedup([...domElements, ...iframeElements, ...dynamicElements, ...contentElements]);
|
|
3071
|
+
if (contentElements.length > 0) return crossSourceDedup([...domElements, ...contentElements]);
|
|
3015
3072
|
try {
|
|
3016
3073
|
const extracted = await Promise.race([
|
|
3017
3074
|
stagehand.extract(
|
|
@@ -3082,21 +3139,26 @@ async function domFallbackElements(page) {
|
|
|
3082
3139
|
const seen = /* @__PURE__ */ new Set();
|
|
3083
3140
|
const noise = /^(cookie|consent|privacy|accept|decline|dismiss|close|copy|©|v\d|switch to (dark|light)|dark mode|light mode|toggle (dark|light|theme)|theme (toggle|switch)|share|tweet|follow|subscribe to newsletter|skip to|back to top|\s*$)/i;
|
|
3084
3141
|
const querySelectorAllDeep = (root, selector2) => {
|
|
3085
|
-
const results =
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3142
|
+
const results = [];
|
|
3143
|
+
const queue = [root];
|
|
3144
|
+
while (queue.length > 0) {
|
|
3145
|
+
const current = queue.shift();
|
|
3146
|
+
Array.from(current.querySelectorAll(selector2)).forEach((el) => results.push(el));
|
|
3147
|
+
current.querySelectorAll("*").forEach((el) => {
|
|
3148
|
+
if (el.shadowRoot) queue.push(el.shadowRoot);
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3091
3151
|
return results;
|
|
3092
3152
|
};
|
|
3093
|
-
const selector = 'input:not([type=hidden]),select,textarea,button,[role="button"],[role="combobox"],[role="slider"],[role="spinbutton"],[role="switch"],[role="tab"],[role="dialog"],[role="alertdialog"],[role="menu"],[role="menuitem"],[role="listbox"],[role="option"],[role="treeitem"],[role="searchbox"],a[role="button"],[contenteditable="true"]';
|
|
3153
|
+
const selector = 'input:not([type=hidden]),select,textarea,button,[role="button"],[role="checkbox"],[role="radio"],[role="combobox"],[role="slider"],[role="spinbutton"],[role="switch"],[role="tab"],[role="dialog"],[role="alertdialog"],[role="menu"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="listbox"],[role="option"],[role="treeitem"],[role="searchbox"],[role="gridcell"],[role="link"],a[role="button"],[contenteditable="true"]';
|
|
3094
3154
|
querySelectorAllDeep(document, selector).forEach((el) => {
|
|
3095
3155
|
const style = window.getComputedStyle(el);
|
|
3096
3156
|
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0" || style.pointerEvents === "none") return;
|
|
3097
3157
|
if (el.hasAttribute("inert") || el.getAttribute("aria-disabled") === "true") return;
|
|
3098
3158
|
const rect = el.getBoundingClientRect();
|
|
3099
|
-
|
|
3159
|
+
const hasAccessibleLabel = !!(el.getAttribute("aria-label") || el.getAttribute("aria-labelledby"));
|
|
3160
|
+
if (rect.width === 0 && rect.height === 0 && !hasAccessibleLabel) return;
|
|
3161
|
+
if (el.closest('[id*="intercom"],[class*="intercom-"],[id*="crisp"],[class*="crisp-"],[id*="freshchat"],[id*="freshdesk"],[id*="drift"],[class*="drift-"],[id*="hubspot-"],[class*="hs-chat"],[class*="hs-widget"],[id*="tawk"],[class*="tawk-"],[id*="chat-widget"],[class*="chat-widget"],[class*="chatbot"],[id*="zopim"],[id*="ze-snippet"],[class*="cookie-banner"],[class*="cookie-consent"],[class*="gdpr-banner"],[id*="cookie-law"],[class*="cookie-law"],[id*="popup-form"],[class*="popup-form"],[class*="floating-form"],[class*="sticky-cta"],[id*="wm-ipp"],[class*="wm-ipp"]')) return;
|
|
3100
3162
|
const tag = el.tagName.toLowerCase();
|
|
3101
3163
|
const ariaRole = el.getAttribute("role");
|
|
3102
3164
|
const isActionEl = tag === "button" || tag === "a" || ariaRole === "button";
|
|
@@ -3249,7 +3311,7 @@ async function domFallbackElements(page) {
|
|
|
3249
3311
|
const priority = (e) => ["email", "password", "text", "tel", "date", "number", "select", "textarea", "url", "file"].includes(e.role) ? 0 : 1;
|
|
3250
3312
|
out.sort((a, b) => priority(a) - priority(b));
|
|
3251
3313
|
return out;
|
|
3252
|
-
}).catch(() => []);
|
|
3314
|
+
}).then((elems) => elems.map((e) => ({ ...e, isInteractive: true }))).catch(() => []);
|
|
3253
3315
|
}
|
|
3254
3316
|
async function contentFallbackElements(page) {
|
|
3255
3317
|
return page.evaluate(() => {
|
|
@@ -3257,11 +3319,15 @@ async function contentFallbackElements(page) {
|
|
|
3257
3319
|
const seen = /* @__PURE__ */ new Set();
|
|
3258
3320
|
const addUniq = (item) => {
|
|
3259
3321
|
const key = `${item.role}:${item.meaning.toLowerCase().slice(0, 60)}`;
|
|
3260
|
-
if (!seen.has(key) && item.meaning.trim().length >
|
|
3322
|
+
if (!seen.has(key) && item.meaning.trim().length > 1) {
|
|
3261
3323
|
seen.add(key);
|
|
3262
3324
|
out.push(item);
|
|
3263
3325
|
}
|
|
3264
3326
|
};
|
|
3327
|
+
const isVisible = (el) => {
|
|
3328
|
+
const s = window.getComputedStyle(el);
|
|
3329
|
+
return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0" && el.offsetParent !== null;
|
|
3330
|
+
};
|
|
3265
3331
|
const title = document.title?.trim();
|
|
3266
3332
|
if (title) addUniq({ meaning: title, role: "page-title", notes: JSON.stringify({ tag: "title" }) });
|
|
3267
3333
|
const desc = document.querySelector('meta[name="description"]')?.getAttribute("content")?.trim();
|
|
@@ -3270,17 +3336,197 @@ async function contentFallbackElements(page) {
|
|
|
3270
3336
|
if (ogTitle) addUniq({ meaning: ogTitle, role: "og-title", notes: JSON.stringify({ tag: "meta[property=og:title]" }) });
|
|
3271
3337
|
const canonical = document.querySelector('link[rel="canonical"]')?.getAttribute("href")?.trim();
|
|
3272
3338
|
if (canonical) addUniq({ meaning: canonical, role: "canonical-url", notes: JSON.stringify({ tag: "link[rel=canonical]" }) });
|
|
3273
|
-
document.querySelectorAll("h1, h2, h3").forEach((h) => {
|
|
3274
|
-
|
|
3339
|
+
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
3340
|
+
if (!isVisible(h)) return;
|
|
3341
|
+
const text = h.textContent?.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3275
3342
|
if (text) addUniq({ meaning: text, role: h.tagName.toLowerCase(), notes: JSON.stringify({ level: h.tagName }) });
|
|
3276
3343
|
});
|
|
3277
|
-
document.querySelectorAll("
|
|
3278
|
-
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3344
|
+
document.querySelectorAll('details, [data-state="closed"], [data-state="open"]').forEach((el) => {
|
|
3345
|
+
if (!isVisible(el)) return;
|
|
3346
|
+
const summary = el.tagName === "DETAILS" ? el.querySelector("summary") : el.querySelector('[class*="trigger" i], [class*="header" i], [class*="question" i], [class*="title" i]') ?? el;
|
|
3347
|
+
const qText = summary?.textContent?.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
3348
|
+
if (qText && qText.length > 3) {
|
|
3349
|
+
addUniq({ meaning: qText, role: "faq-question", notes: JSON.stringify({ expanded: el.hasAttribute("open") || el.getAttribute("data-state") === "open" }) });
|
|
3350
|
+
}
|
|
3351
|
+
const contentEl = el.tagName === "DETAILS" ? Array.from(el.children).find((c) => c.tagName !== "SUMMARY") : el.querySelector('[class*="content" i], [class*="answer" i], [class*="panel" i], [class*="body" i]:not(body)') ?? el.nextElementSibling;
|
|
3352
|
+
if (contentEl) {
|
|
3353
|
+
const aText = contentEl.textContent?.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3354
|
+
if (aText && aText.length > 5 && aText !== qText) addUniq({ meaning: aText, role: "faq-answer", notes: JSON.stringify({ question: qText }) });
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
document.querySelectorAll('[class*="faq" i]:not(script):not(style):not(nav), [id*="faq" i]:not(script):not(style)').forEach((container) => {
|
|
3358
|
+
if (!isVisible(container)) return;
|
|
3359
|
+
const qEls = container.querySelectorAll('[class*="question" i], [class*="trigger" i], summary, dt, [class*="heading" i]');
|
|
3360
|
+
const aEls = container.querySelectorAll('[class*="answer" i], [class*="content" i]:not(main):not(article), [class*="panel" i], dd');
|
|
3361
|
+
qEls.forEach((q) => {
|
|
3362
|
+
const qText = q.textContent?.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
3363
|
+
if (qText && qText.length > 3) addUniq({ meaning: qText, role: "faq-question", notes: JSON.stringify({ source: "class-faq" }) });
|
|
3364
|
+
});
|
|
3365
|
+
aEls.forEach((a) => {
|
|
3366
|
+
const aText = a.textContent?.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3367
|
+
if (aText && aText.length > 5) addUniq({ meaning: aText, role: "faq-answer", notes: JSON.stringify({ source: "class-faq" }) });
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
document.querySelectorAll('[class*="card" i]:not(nav):not(header):not(footer), [class*="chip" i], [class*="badge" i], [class*="tag" i], [class*="pill" i]').forEach((el) => {
|
|
3371
|
+
if (!isVisible(el)) return;
|
|
3372
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3373
|
+
if (text && text.length > 2 && text.length < 200) addUniq({ meaning: text, role: "content-card" });
|
|
3374
|
+
});
|
|
3375
|
+
const mainContent = document.querySelector('main, [role="main"], article') ?? document.body;
|
|
3376
|
+
let listItemCount = 0;
|
|
3377
|
+
mainContent.querySelectorAll("li").forEach((li) => {
|
|
3378
|
+
if (listItemCount >= 30) return;
|
|
3379
|
+
if (!isVisible(li)) return;
|
|
3380
|
+
const text = li.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3381
|
+
if (text && text.length > 3 && text.length < 200) {
|
|
3382
|
+
addUniq({ meaning: text, role: "list-item" });
|
|
3383
|
+
listItemCount++;
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
mainContent.querySelectorAll('section > p:first-of-type, article > p:first-of-type, [class*="section" i] > p:first-of-type').forEach((p) => {
|
|
3387
|
+
if (!isVisible(p)) return;
|
|
3388
|
+
const text = p.textContent?.replace(/\s+/g, " ").trim().slice(0, 250);
|
|
3389
|
+
if (text && text.length > 10) addUniq({ meaning: text, role: "section-paragraph" });
|
|
3390
|
+
});
|
|
3391
|
+
document.querySelectorAll('[class*="cta" i] p, [class*="call-to-action" i] p, section:not(form) > p').forEach((p) => {
|
|
3392
|
+
if (!isVisible(p)) return;
|
|
3393
|
+
const text = p.textContent?.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
3394
|
+
if (text && text.length > 10 && text.length < 400) addUniq({ meaning: text, role: "cta-section" });
|
|
3395
|
+
});
|
|
3396
|
+
document.querySelectorAll('nav a, [role="navigation"] a, header nav a, footer nav a, [aria-label*="navigation" i] a, [aria-label*="menu" i] a').forEach((a) => {
|
|
3397
|
+
if (!isVisible(a)) return;
|
|
3398
|
+
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
3399
|
+
const href = a.href;
|
|
3400
|
+
if (!label || !href || /^(javascript:|#$)/i.test(href)) return;
|
|
3401
|
+
let landmark = "Site Navigation";
|
|
3402
|
+
let el = a.parentElement;
|
|
3403
|
+
while (el) {
|
|
3404
|
+
const tag = el.tagName?.toLowerCase();
|
|
3405
|
+
const role = el.getAttribute?.("role")?.toLowerCase();
|
|
3406
|
+
if (tag === "header" || role === "banner") {
|
|
3407
|
+
landmark = "Header";
|
|
3408
|
+
break;
|
|
3409
|
+
}
|
|
3410
|
+
if (tag === "footer" || role === "contentinfo") {
|
|
3411
|
+
landmark = "Footer";
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
if (tag === "nav" || role === "navigation") {
|
|
3415
|
+
const directLabel = el.getAttribute("aria-label")?.trim() ?? "";
|
|
3416
|
+
const labelId = el.getAttribute("aria-labelledby");
|
|
3417
|
+
const referencedLabel = labelId ? document.getElementById(labelId)?.textContent?.trim() ?? "" : "";
|
|
3418
|
+
landmark = directLabel || referencedLabel || "Site Navigation";
|
|
3419
|
+
break;
|
|
3420
|
+
}
|
|
3421
|
+
el = el.parentElement;
|
|
3422
|
+
}
|
|
3423
|
+
addUniq({ meaning: label, role: "nav-link", notes: JSON.stringify({ href: href.slice(0, 150), landmark }) });
|
|
3424
|
+
});
|
|
3425
|
+
document.querySelectorAll('[aria-label*="breadcrumb" i] a, [class*="breadcrumb" i] a, nav[aria-label*="breadcrumb" i] a').forEach((a) => {
|
|
3426
|
+
const label = a.textContent?.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
3427
|
+
const href = a.href;
|
|
3428
|
+
if (label) addUniq({ meaning: label, role: "breadcrumb-item", notes: JSON.stringify({ href: href?.slice(0, 150) }) });
|
|
3429
|
+
});
|
|
3430
|
+
let footerLinkCount = 0;
|
|
3431
|
+
const footerEls = /* @__PURE__ */ new Set();
|
|
3432
|
+
document.querySelectorAll('footer, [role="contentinfo"], [class*="footer" i]:not(script):not(style):not(nav), [id*="footer" i]:not(script):not(style)').forEach((f) => footerEls.add(f));
|
|
3433
|
+
const footerLinks = [];
|
|
3434
|
+
footerEls.forEach((f) => f.querySelectorAll("a").forEach((a) => footerLinks.push(a)));
|
|
3435
|
+
const seenFooterHrefs = /* @__PURE__ */ new Set();
|
|
3436
|
+
footerLinks.filter((a) => {
|
|
3437
|
+
const href = a.href ?? "";
|
|
3438
|
+
if (seenFooterHrefs.has(href)) return false;
|
|
3439
|
+
seenFooterHrefs.add(href);
|
|
3440
|
+
return true;
|
|
3441
|
+
}).forEach((a) => {
|
|
3442
|
+
if (footerLinkCount >= 25) return;
|
|
3443
|
+
if (!isVisible(a)) return;
|
|
3444
|
+
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
3445
|
+
const href = a.href ?? "";
|
|
3446
|
+
if (!label) return;
|
|
3447
|
+
if (/^mailto:/i.test(href)) {
|
|
3448
|
+
addUniq({ meaning: label || href, role: "footer-contact", notes: JSON.stringify({ href }) });
|
|
3449
|
+
footerLinkCount++;
|
|
3450
|
+
} else if (/^tel:/i.test(href)) {
|
|
3451
|
+
addUniq({ meaning: label || href, role: "footer-contact", notes: JSON.stringify({ href }) });
|
|
3452
|
+
footerLinkCount++;
|
|
3453
|
+
} else if (label.length > 0) {
|
|
3454
|
+
addUniq({ meaning: label, role: "footer-link", notes: JSON.stringify({ href: href.slice(0, 150) }) });
|
|
3455
|
+
footerLinkCount++;
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
let imgCount = 0;
|
|
3459
|
+
const isImgVisible = (el) => {
|
|
3460
|
+
const s = window.getComputedStyle(el);
|
|
3461
|
+
return s.display !== "none" && s.visibility !== "hidden";
|
|
3462
|
+
};
|
|
3463
|
+
document.querySelectorAll("img").forEach((img) => {
|
|
3464
|
+
if (imgCount >= 20) return;
|
|
3465
|
+
if (!isImgVisible(img)) return;
|
|
3466
|
+
const alt = img.getAttribute("alt")?.trim().slice(0, 80) ?? "";
|
|
3467
|
+
const src = (img.getAttribute("src") || img.getAttribute("data-src") || img.getAttribute("data-lazy-src") || img.getAttribute("data-original") || "").slice(0, 120);
|
|
3468
|
+
const srcsetFirst = img.getAttribute("srcset")?.split(",")[0]?.trim().split(" ")[0]?.slice(0, 120) ?? "";
|
|
3469
|
+
const resolvedSrc = src || srcsetFirst;
|
|
3470
|
+
if (!resolvedSrc && !alt) return;
|
|
3471
|
+
const label = alt || resolvedSrc.split("/").pop()?.split("?")[0]?.slice(0, 60) || "Image";
|
|
3472
|
+
addUniq({ meaning: label, role: alt ? "image" : "image-no-alt", notes: JSON.stringify({ src: resolvedSrc, alt, hasAlt: !!alt }) });
|
|
3473
|
+
imgCount++;
|
|
3474
|
+
});
|
|
3475
|
+
document.querySelectorAll("picture source[srcset]").forEach((src) => {
|
|
3476
|
+
if (imgCount >= 25) return;
|
|
3477
|
+
const srcset = src.getAttribute("srcset")?.split(",")[0]?.trim().split(" ")[0]?.slice(0, 120) ?? "";
|
|
3478
|
+
const media = src.getAttribute("media") ?? "";
|
|
3479
|
+
if (srcset) {
|
|
3480
|
+
addUniq({ meaning: `Responsive image${media ? " (" + media + ")" : ""}: ${srcset.split("/").pop()?.split("?")[0] ?? ""}`, role: "picture-source", notes: JSON.stringify({ srcset: srcset.slice(0, 150), media }) });
|
|
3481
|
+
imgCount++;
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
let bgCount = 0;
|
|
3485
|
+
document.querySelectorAll('[style*="background-image"], [style*="background:"]').forEach((el) => {
|
|
3486
|
+
if (bgCount >= 15) return;
|
|
3487
|
+
if (!isImgVisible(el)) return;
|
|
3488
|
+
const style = el.getAttribute("style") ?? "";
|
|
3489
|
+
const match = style.match(/url\(['"]?([^'")\s]+)['"]?\)/i);
|
|
3490
|
+
if (match) {
|
|
3491
|
+
const url = match[1].slice(0, 120);
|
|
3492
|
+
const role = el.tagName.toLowerCase();
|
|
3493
|
+
const context = (el.getAttribute("aria-label") || el.getAttribute("title") || el.className.split(" ").find((c) => c.length > 3 && !["true", "false"].includes(c)) || "").slice(0, 60);
|
|
3494
|
+
addUniq({ meaning: context || `BG image on <${role}>: ${url.split("/").pop()?.split("?")[0] ?? url}`, role: "bg-image", notes: JSON.stringify({ url, element: role, context }) });
|
|
3495
|
+
bgCount++;
|
|
3496
|
+
}
|
|
3497
|
+
});
|
|
3498
|
+
document.querySelectorAll('[class*="hero" i], [class*="banner" i], [class*="cover" i], [class*="header-bg" i], [class*="card" i]:not(nav), [class*="feature" i], [class*="service" i], [class*="about" i], [class*="team" i], section').forEach((el) => {
|
|
3499
|
+
if (bgCount >= 15) return;
|
|
3500
|
+
if (!isImgVisible(el)) return;
|
|
3501
|
+
try {
|
|
3502
|
+
const computed = window.getComputedStyle(el).backgroundImage;
|
|
3503
|
+
if (computed && computed !== "none" && computed.includes("url(")) {
|
|
3504
|
+
const match = computed.match(/url\(['"]?([^'")\s]+)['"]?\)/i);
|
|
3505
|
+
if (match) {
|
|
3506
|
+
const url = match[1].slice(0, 120);
|
|
3507
|
+
const tag = el.tagName.toLowerCase();
|
|
3508
|
+
const cls = Array.from(el.classList).find((c) => typeof c === "string" && c.length > 3 && !/^(true|false|active|visible)$/.test(c)) ?? "";
|
|
3509
|
+
addUniq({ meaning: `CSS bg image on .${cls || tag}: ${url.split("/").pop()?.split("?")[0] ?? url}`, role: "bg-image", notes: JSON.stringify({ url, cssClass: cls, element: tag }) });
|
|
3510
|
+
bgCount++;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
document.querySelectorAll("[data-bg], [data-background], [data-background-image]").forEach((el) => {
|
|
3517
|
+
if (bgCount >= 15) return;
|
|
3518
|
+
if (!isVisible(el)) return;
|
|
3519
|
+
const url = (el.getAttribute("data-bg") || el.getAttribute("data-background") || el.getAttribute("data-background-image") || "").slice(0, 120);
|
|
3520
|
+
if (url) {
|
|
3521
|
+
addUniq({ meaning: `Lazy BG image: ${url.split("/").pop()?.split("?")[0] ?? url}`, role: "bg-image", notes: JSON.stringify({ url, pattern: "data-bg" }) });
|
|
3522
|
+
bgCount++;
|
|
3523
|
+
}
|
|
3281
3524
|
});
|
|
3282
3525
|
const navTags = /* @__PURE__ */ new Set(["nav", "header", "footer"]);
|
|
3526
|
+
let contentLinkCount = 0;
|
|
3283
3527
|
document.querySelectorAll("a[href]").forEach((a) => {
|
|
3528
|
+
if (contentLinkCount >= 20) return;
|
|
3529
|
+
if (!isVisible(a)) return;
|
|
3284
3530
|
let p = a.parentElement;
|
|
3285
3531
|
while (p) {
|
|
3286
3532
|
if (navTags.has(p.tagName?.toLowerCase())) return;
|
|
@@ -3288,17 +3534,413 @@ async function contentFallbackElements(page) {
|
|
|
3288
3534
|
}
|
|
3289
3535
|
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 80);
|
|
3290
3536
|
const href = a.href;
|
|
3291
|
-
if (label && href && !/^(
|
|
3537
|
+
if (label && href && !/^(#$|javascript:|mailto:|tel:)/i.test(href)) {
|
|
3292
3538
|
const isExternal = !href.includes(location.hostname);
|
|
3293
3539
|
addUniq({ meaning: label, role: isExternal ? "external-link" : "internal-link", notes: JSON.stringify({ href: href.slice(0, 120) }) });
|
|
3540
|
+
contentLinkCount++;
|
|
3294
3541
|
}
|
|
3295
3542
|
});
|
|
3296
3543
|
const hasAnimation = !!document.querySelector('[class*="animate"], [class*="motion"], [class*="transition"], [class*="scroll-"], [data-aos], [data-scroll]');
|
|
3297
3544
|
if (hasAnimation) addUniq({ meaning: "CSS animations / scroll effects detected on page", role: "animation-layer", notes: JSON.stringify({ detected: true }) });
|
|
3298
3545
|
if (document.querySelector("video")) addUniq({ meaning: "Video element present", role: "video", notes: JSON.stringify({ count: document.querySelectorAll("video").length }) });
|
|
3299
3546
|
if (document.querySelector("canvas")) addUniq({ meaning: "Canvas element present", role: "canvas" });
|
|
3300
|
-
|
|
3301
|
-
|
|
3547
|
+
document.querySelectorAll('main section, article section, [role="main"] section').forEach((el) => {
|
|
3548
|
+
if (!isVisible(el)) return;
|
|
3549
|
+
const heading = el.querySelector("h1,h2,h3,h4")?.textContent?.trim() ?? "";
|
|
3550
|
+
const itemCount = el.querySelectorAll('li, [class*="card" i], [class*="chip" i]').length;
|
|
3551
|
+
const paragraphs = el.querySelectorAll("p").length;
|
|
3552
|
+
const meaning = heading || `Section (${itemCount} items)`;
|
|
3553
|
+
if (meaning) addUniq({ meaning, role: "page-section", notes: JSON.stringify({ heading, itemCount, paragraphs, id: el.id || el.getAttribute("data-section") }) });
|
|
3554
|
+
});
|
|
3555
|
+
const ogDesc = document.querySelector('meta[property="og:description"]')?.getAttribute("content")?.trim();
|
|
3556
|
+
if (ogDesc) addUniq({ meaning: ogDesc, role: "og-description", notes: JSON.stringify({ tag: "meta[property=og:description]" }) });
|
|
3557
|
+
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content")?.trim();
|
|
3558
|
+
if (ogImage) addUniq({ meaning: ogImage, role: "og-image", notes: JSON.stringify({ tag: "meta[property=og:image]", src: ogImage }) });
|
|
3559
|
+
const twitterTitle = document.querySelector('meta[name="twitter:title"]')?.getAttribute("content")?.trim();
|
|
3560
|
+
if (twitterTitle) addUniq({ meaning: twitterTitle, role: "twitter-title", notes: JSON.stringify({ tag: "meta[name=twitter:title]" }) });
|
|
3561
|
+
const twitterDesc = document.querySelector('meta[name="twitter:description"]')?.getAttribute("content")?.trim();
|
|
3562
|
+
if (twitterDesc) addUniq({ meaning: twitterDesc, role: "twitter-description", notes: JSON.stringify({ tag: "meta[name=twitter:description]" }) });
|
|
3563
|
+
const jsonFonts = [];
|
|
3564
|
+
document.querySelectorAll('link[rel="preload"][as="font"], link[href*="fonts.googleapis"]').forEach((l) => {
|
|
3565
|
+
const url = l.href?.slice(0, 120);
|
|
3566
|
+
if (url) jsonFonts.push(url);
|
|
3567
|
+
});
|
|
3568
|
+
if (jsonFonts.length) addUniq({ meaning: `Font imports: ${jsonFonts.length} font file(s)`, role: "font-stack", notes: JSON.stringify({ fonts: jsonFonts.slice(0, 5) }) });
|
|
3569
|
+
document.querySelectorAll('script[type="application/ld+json"], script[class*="tsr"]').forEach((s) => {
|
|
3570
|
+
try {
|
|
3571
|
+
const data = JSON.parse(s.textContent ?? "{}");
|
|
3572
|
+
const items = Array.isArray(data) ? data : [data];
|
|
3573
|
+
for (const item of items) {
|
|
3574
|
+
const faqArr = item.mainEntity ?? item.faqs ?? item.questions ?? item.faq ?? [];
|
|
3575
|
+
(Array.isArray(faqArr) ? faqArr : []).forEach((f) => {
|
|
3576
|
+
const q = f.name ?? f.question ?? "";
|
|
3577
|
+
const a = f.acceptedAnswer?.text ?? f.answer ?? "";
|
|
3578
|
+
if (q) addUniq({ meaning: String(q).slice(0, 150), role: "faq-question", notes: JSON.stringify({ source: "json-ld" }) });
|
|
3579
|
+
if (a) addUniq({ meaning: String(a).slice(0, 200), role: "faq-answer", notes: JSON.stringify({ question: String(q).slice(0, 80), source: "json-ld" }) });
|
|
3580
|
+
});
|
|
3581
|
+
const deliverables = item.deliverables ?? item.services ?? item.offerings ?? [];
|
|
3582
|
+
(Array.isArray(deliverables) ? deliverables : []).forEach((d) => {
|
|
3583
|
+
const title2 = d.title ?? d.name ?? "";
|
|
3584
|
+
const desc2 = d.description ?? d.desc ?? "";
|
|
3585
|
+
if (title2) addUniq({ meaning: `${title2}${desc2 ? ": " + desc2.slice(0, 120) : ""}`, role: "deliverable", notes: JSON.stringify({ title: title2, description: String(desc2).slice(0, 200) }) });
|
|
3586
|
+
});
|
|
3587
|
+
const stack = item.stack ?? item.technologies ?? item.tools ?? [];
|
|
3588
|
+
(Array.isArray(stack) ? stack : []).forEach((tool) => {
|
|
3589
|
+
const name = typeof tool === "string" ? tool : tool.name ?? "";
|
|
3590
|
+
if (name) addUniq({ meaning: name, role: "tech-stack-item", notes: JSON.stringify({ source: "json-ld" }) });
|
|
3591
|
+
});
|
|
3592
|
+
const outcomes = item.outcomes ?? item.metrics ?? item.stats ?? [];
|
|
3593
|
+
(Array.isArray(outcomes) ? outcomes : []).forEach((m) => {
|
|
3594
|
+
const metric = m.metric ?? m.value ?? "";
|
|
3595
|
+
const label = m.label ?? m.name ?? "";
|
|
3596
|
+
if (metric && label) addUniq({ meaning: `${metric} \u2014 ${label}`, role: "metric-card", notes: JSON.stringify({ metric, label }) });
|
|
3597
|
+
});
|
|
3598
|
+
const features = item.features ?? item.capabilities ?? [];
|
|
3599
|
+
(Array.isArray(features) ? features : []).forEach((f) => {
|
|
3600
|
+
const name = typeof f === "string" ? f : f.name ?? "";
|
|
3601
|
+
if (name) addUniq({ meaning: name, role: "feature-badge", notes: JSON.stringify({ source: "json-ld" }) });
|
|
3602
|
+
});
|
|
3603
|
+
if (item["@type"] === "HowTo" || item["@type"] === "Recipe") {
|
|
3604
|
+
const steps = item.step ?? item.steps ?? [];
|
|
3605
|
+
(Array.isArray(steps) ? steps : []).forEach((step, idx) => {
|
|
3606
|
+
const text = step.text ?? step.description ?? (typeof step === "string" ? step : "");
|
|
3607
|
+
const name = step.name ?? `Step ${idx + 1}`;
|
|
3608
|
+
if (text) addUniq({ meaning: `${name}: ${String(text).slice(0, 200)}`, role: "howto-step", notes: JSON.stringify({ stepIndex: idx + 1, name }) });
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
if (item["@type"] === "BreadcrumbList") {
|
|
3612
|
+
const crumbs = item.itemListElement ?? [];
|
|
3613
|
+
(Array.isArray(crumbs) ? crumbs : []).forEach((crumb) => {
|
|
3614
|
+
const name = crumb.name ?? crumb.item?.name ?? "";
|
|
3615
|
+
const url = crumb.item?.["@id"] ?? crumb.item ?? "";
|
|
3616
|
+
if (name) addUniq({ meaning: name, role: "breadcrumb-item", notes: JSON.stringify({ position: crumb.position, url }) });
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
if (item["@type"] === "Event") {
|
|
3620
|
+
const name = item.name ?? "";
|
|
3621
|
+
const startDate = item.startDate ?? "";
|
|
3622
|
+
const location2 = item.location?.name ?? item.location?.address?.streetAddress ?? "";
|
|
3623
|
+
if (name) addUniq({ meaning: name, role: "event-name", notes: JSON.stringify({ startDate, location: location2, source: "json-ld" }) });
|
|
3624
|
+
}
|
|
3625
|
+
if (item["@type"] === "JobPosting") {
|
|
3626
|
+
const title2 = item.title ?? "";
|
|
3627
|
+
const desc2 = item.description ?? "";
|
|
3628
|
+
const employer = item.hiringOrganization?.name ?? "";
|
|
3629
|
+
if (title2) addUniq({ meaning: `${title2}${employer ? " at " + employer : ""}`, role: "job-posting", notes: JSON.stringify({ description: String(desc2).slice(0, 200) }) });
|
|
3630
|
+
}
|
|
3631
|
+
if (item["@type"] === "Review") {
|
|
3632
|
+
const body = item.reviewBody ?? "";
|
|
3633
|
+
const rating = item.reviewRating?.ratingValue ?? "";
|
|
3634
|
+
const author = item.author?.name ?? "";
|
|
3635
|
+
if (body) addUniq({ meaning: String(body).slice(0, 200), role: "review-text", notes: JSON.stringify({ rating, author }) });
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
} catch {
|
|
3639
|
+
}
|
|
3640
|
+
});
|
|
3641
|
+
document.querySelectorAll('[role="tabpanel"], [data-state="active"][id*="panel"], [id*="panel"][aria-labelledby]').forEach((el) => {
|
|
3642
|
+
if (!isVisible(el)) return;
|
|
3643
|
+
const heading = el.querySelector("h2, h3, h4")?.textContent?.trim() ?? "";
|
|
3644
|
+
const desc2 = el.querySelector("p")?.textContent?.replace(/\s+/g, " ").trim().slice(0, 200) ?? "";
|
|
3645
|
+
const badges = Array.from(el.querySelectorAll('[class*="badge" i], [class*="tag" i], [class*="pill" i], [class*="chip" i], [class*="rounded-full" i]')).map((b) => b.textContent?.trim()).filter(Boolean).slice(0, 8);
|
|
3646
|
+
const meaning = heading || desc2;
|
|
3647
|
+
if (meaning) addUniq({ meaning: meaning.slice(0, 150), role: "tab-panel", notes: JSON.stringify({ heading, description: desc2, badges, panelId: el.id }) });
|
|
3648
|
+
});
|
|
3649
|
+
document.querySelectorAll("[aria-controls]").forEach((btn) => {
|
|
3650
|
+
if (!isVisible(btn)) return;
|
|
3651
|
+
const panelId = btn.getAttribute("aria-controls");
|
|
3652
|
+
if (!panelId) return;
|
|
3653
|
+
const panel = document.getElementById(panelId);
|
|
3654
|
+
if (!panel) return;
|
|
3655
|
+
const qText = btn.textContent?.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
3656
|
+
const aText = panel.textContent?.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3657
|
+
if (qText && qText.length > 3) addUniq({ meaning: qText, role: "faq-question", notes: JSON.stringify({ panelId, expanded: btn.getAttribute("aria-expanded") }) });
|
|
3658
|
+
if (aText && aText.length > 5 && aText !== qText) addUniq({ meaning: aText, role: "faq-answer", notes: JSON.stringify({ question: qText?.slice(0, 80), source: "aria-controls" }) });
|
|
3659
|
+
});
|
|
3660
|
+
const mainEl = document.querySelector('main, [role="main"], article') ?? document.body;
|
|
3661
|
+
mainEl.querySelectorAll('[class*="mono" i]:not(code):not(pre), [class*="pill" i]:not(nav), [class*="chip" i]:not(nav), [class*="tag" i]:not(nav), [class*="badge" i]:not(nav)').forEach((el) => {
|
|
3662
|
+
if (!isVisible(el)) return;
|
|
3663
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
3664
|
+
if (text && text.length > 1 && text.length < 60 && !text.includes(" ")) {
|
|
3665
|
+
addUniq({ meaning: text, role: "tech-stack-item", notes: JSON.stringify({ source: "dom-pill" }) });
|
|
3666
|
+
} else if (text && text.length > 1 && text.length < 40) {
|
|
3667
|
+
addUniq({ meaning: text, role: "feature-badge", notes: JSON.stringify({ source: "dom-pill" }) });
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
mainEl.querySelectorAll('a[href][class*="card" i], [class*="card" i] a[href], [class*="service" i][href], a[class*="service" i]').forEach((el) => {
|
|
3671
|
+
if (!isVisible(el)) return;
|
|
3672
|
+
const anchor = el.tagName === "A" ? el : el.querySelector("a");
|
|
3673
|
+
if (!anchor) return;
|
|
3674
|
+
const href = anchor.href;
|
|
3675
|
+
if (!href || /^(#|javascript:|mailto:|tel:)/i.test(href)) return;
|
|
3676
|
+
const heading = el.querySelector("h2, h3, h4")?.textContent?.trim() ?? "";
|
|
3677
|
+
const desc2 = el.querySelector("p")?.textContent?.replace(/\s+/g, " ").trim().slice(0, 120) ?? "";
|
|
3678
|
+
const label = anchor.textContent?.replace(/\s+/g, " ").trim().slice(0, 80) ?? "";
|
|
3679
|
+
const meaning = heading || label || "Linked card";
|
|
3680
|
+
if (meaning && meaning !== "Linked card" || heading && desc2) {
|
|
3681
|
+
addUniq({ meaning: `${heading || meaning}${desc2 ? ": " + desc2 : ""}`.slice(0, 180), role: "service-card", notes: JSON.stringify({ href: href.slice(0, 150), heading, description: desc2 }) });
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3684
|
+
mainEl.querySelectorAll('[class*="metric" i], [class*="outcome" i], [class*="stat" i], [class*="kpi" i]').forEach((el) => {
|
|
3685
|
+
if (!isVisible(el)) return;
|
|
3686
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3687
|
+
if (text && text.length > 2 && text.length < 120) addUniq({ meaning: text, role: "metric-card", notes: JSON.stringify({ source: "dom-class" }) });
|
|
3688
|
+
});
|
|
3689
|
+
document.querySelectorAll('.sr-only, [class*="sr-only" i], [class*="visually-hidden" i]').forEach((el) => {
|
|
3690
|
+
const tag = el.tagName?.toLowerCase() ?? "";
|
|
3691
|
+
if (!/^h[1-6]$/.test(tag)) return;
|
|
3692
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3693
|
+
if (text) addUniq({ meaning: text, role: `${tag}-sr-only`, notes: JSON.stringify({ srOnly: true, tag }) });
|
|
3694
|
+
});
|
|
3695
|
+
try {
|
|
3696
|
+
const cssTokens = {};
|
|
3697
|
+
const rootStyle = window.getComputedStyle(document.documentElement);
|
|
3698
|
+
try {
|
|
3699
|
+
for (let si = 0; si < document.styleSheets.length; si++) {
|
|
3700
|
+
try {
|
|
3701
|
+
const sheet = document.styleSheets[si];
|
|
3702
|
+
const rules = sheet.cssRules;
|
|
3703
|
+
for (let ri = 0; ri < rules.length; ri++) {
|
|
3704
|
+
const rule = rules[ri];
|
|
3705
|
+
if (rule.selectorText === ":root" || rule.selectorText === "html" || rule.selectorText === ":root, html") {
|
|
3706
|
+
const style = rule.style;
|
|
3707
|
+
for (let pi = 0; pi < style.length; pi++) {
|
|
3708
|
+
const prop = style[pi];
|
|
3709
|
+
if (prop.startsWith("--") && Object.keys(cssTokens).length < 30) {
|
|
3710
|
+
const val = style.getPropertyValue(prop).trim();
|
|
3711
|
+
if (val) cssTokens[prop] = val.slice(0, 60);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
} catch {
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
const probePrefixes = ["", "--color-", "--bs-", "--brand-", "--clr-", "--theme-", "--md-sys-color-"];
|
|
3722
|
+
const probeNames = ["primary", "secondary", "accent", "background", "foreground", "surface", "text", "muted", "border", "ring", "card", "destructive", "warning", "success"];
|
|
3723
|
+
const probeFonts = ["--font-sans", "--font-mono", "--font-display", "--font-heading", "--font-body", "--font-family-base"];
|
|
3724
|
+
for (const pfx of probePrefixes) {
|
|
3725
|
+
for (const name of probeNames) {
|
|
3726
|
+
const v = `${pfx}${name}`;
|
|
3727
|
+
if (!cssTokens[v]) {
|
|
3728
|
+
const val = rootStyle.getPropertyValue(v).trim();
|
|
3729
|
+
if (val) cssTokens[v] = val.slice(0, 60);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
for (const v of probeFonts) {
|
|
3734
|
+
if (!cssTokens[v]) {
|
|
3735
|
+
const val = rootStyle.getPropertyValue(v).trim();
|
|
3736
|
+
if (val) cssTokens[v] = val.slice(0, 60);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
if (Object.keys(cssTokens).length > 0) {
|
|
3740
|
+
const limited = Object.fromEntries(Object.entries(cssTokens).slice(0, 25));
|
|
3741
|
+
addUniq({ meaning: `Design tokens: ${Object.keys(limited).join(", ")}`, role: "design-tokens", notes: JSON.stringify(limited) });
|
|
3742
|
+
}
|
|
3743
|
+
} catch {
|
|
3744
|
+
}
|
|
3745
|
+
document.querySelectorAll("table").forEach((table) => {
|
|
3746
|
+
if (!isVisible(table)) return;
|
|
3747
|
+
const headers = Array.from(table.querySelectorAll("thead th, thead td, tr:first-child th")).map((th) => th.textContent?.trim()).filter(Boolean).slice(0, 8);
|
|
3748
|
+
const rowCount = table.querySelectorAll("tbody tr").length;
|
|
3749
|
+
if (headers.length > 0) addUniq({ meaning: `Table: ${headers.join(" \xB7 ")}`, role: "data-table", notes: JSON.stringify({ headers, rowCount }) });
|
|
3750
|
+
});
|
|
3751
|
+
document.querySelectorAll("dl").forEach((dl) => {
|
|
3752
|
+
if (!isVisible(dl)) return;
|
|
3753
|
+
const terms = Array.from(dl.querySelectorAll("dt")).map((dt) => dt.textContent?.trim()).filter(Boolean).slice(0, 10);
|
|
3754
|
+
if (terms.length > 0) addUniq({ meaning: `Definition list: ${terms.slice(0, 4).join(", ")}`, role: "definition-list", notes: JSON.stringify({ terms: terms.slice(0, 10) }) });
|
|
3755
|
+
});
|
|
3756
|
+
let quoteCount = 0;
|
|
3757
|
+
document.querySelectorAll('blockquote, [class*="testimonial" i], [class*="review" i]:not(script), [class*="quote" i]:not([class*="icon"]), [class*="pullquote" i]').forEach((el) => {
|
|
3758
|
+
if (quoteCount >= 10) return;
|
|
3759
|
+
if (!isVisible(el)) return;
|
|
3760
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3761
|
+
const author = el.querySelector('[class*="author" i], [class*="name" i], cite')?.textContent?.trim().slice(0, 60) ?? "";
|
|
3762
|
+
if (text && text.length > 20) {
|
|
3763
|
+
addUniq({ meaning: `${text.slice(0, 160)}${author ? " \u2014 " + author : ""}`, role: "testimonial", notes: JSON.stringify({ author }) });
|
|
3764
|
+
quoteCount++;
|
|
3765
|
+
}
|
|
3766
|
+
});
|
|
3767
|
+
document.querySelectorAll('[role="alert"], [role="status"], [role="banner"]:not(header), .alert, .notice, .notification, .warning-banner, .error-banner, .success-banner, [class*="alert-" i]:not(button), [class*="toast" i]:not(button), [class*="snackbar" i]').forEach((el) => {
|
|
3768
|
+
if (!isVisible(el)) return;
|
|
3769
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3770
|
+
if (text && text.length > 5) addUniq({ meaning: text, role: "alert-banner", notes: JSON.stringify({ ariaRole: el.getAttribute("role") }) });
|
|
3771
|
+
});
|
|
3772
|
+
let pricingCount = 0;
|
|
3773
|
+
document.querySelectorAll('[class*="pricing" i], [class*="plan" i]:not(main):not(article), [class*="tier" i]:not(nav), [class*="package" i]:not(script)').forEach((el) => {
|
|
3774
|
+
if (pricingCount >= 8) return;
|
|
3775
|
+
if (!isVisible(el)) return;
|
|
3776
|
+
const name = el.querySelector('h2,h3,h4,[class*="plan-name" i],[class*="tier-name" i]')?.textContent?.trim().slice(0, 60) ?? "";
|
|
3777
|
+
const price = el.querySelector('[class*="price" i],[class*="amount" i]')?.textContent?.trim().slice(0, 40) ?? "";
|
|
3778
|
+
if (name || price) {
|
|
3779
|
+
addUniq({ meaning: `${name || "Plan"}${price ? ": " + price : ""}`, role: "pricing-plan", notes: JSON.stringify({ planName: name, price }) });
|
|
3780
|
+
pricingCount++;
|
|
3781
|
+
}
|
|
3782
|
+
});
|
|
3783
|
+
const paginationEl = document.querySelector('[role="navigation"][aria-label*="paginat" i], [class*="pagination" i], [aria-label*="paginat" i]');
|
|
3784
|
+
if (paginationEl && isVisible(paginationEl)) {
|
|
3785
|
+
const pages = Array.from(paginationEl.querySelectorAll("a, button")).map((p) => p.textContent?.trim()).filter(Boolean).slice(0, 10);
|
|
3786
|
+
if (pages.length > 0) addUniq({ meaning: `Pagination: ${pages.join(", ")}`, role: "pagination", notes: JSON.stringify({ pages }) });
|
|
3787
|
+
}
|
|
3788
|
+
let codeCount = 0;
|
|
3789
|
+
document.querySelectorAll('pre code, [class*="code-block" i], [class*="highlight" i] pre').forEach((el) => {
|
|
3790
|
+
if (codeCount >= 5) return;
|
|
3791
|
+
if (!isVisible(el)) return;
|
|
3792
|
+
const lang = el.getAttribute("class")?.match(/language-(\w+)/)?.[1] ?? "";
|
|
3793
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3794
|
+
if (text && text.length > 10) {
|
|
3795
|
+
addUniq({ meaning: `Code example${lang ? " (" + lang + ")" : ""}: ${text.slice(0, 80)}`, role: "code-block", notes: JSON.stringify({ language: lang }) });
|
|
3796
|
+
codeCount++;
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
let dropdownCount = 0;
|
|
3800
|
+
document.querySelectorAll('[role="listbox"] [role="option"], [role="option"]:not([role="listbox"] *)').forEach((el) => {
|
|
3801
|
+
if (dropdownCount >= 20) return;
|
|
3802
|
+
if (!isVisible(el)) return;
|
|
3803
|
+
const text = el.textContent?.trim().slice(0, 80);
|
|
3804
|
+
if (text) {
|
|
3805
|
+
addUniq({ meaning: text, role: "dropdown-option", notes: JSON.stringify({ selected: el.getAttribute("aria-selected") }) });
|
|
3806
|
+
dropdownCount++;
|
|
3807
|
+
}
|
|
3808
|
+
});
|
|
3809
|
+
let timelineCount = 0;
|
|
3810
|
+
document.querySelectorAll('[class*="timeline" i] li, [class*="timeline" i] [class*="step" i], [class*="process" i] li, [class*="steps" i] li').forEach((el) => {
|
|
3811
|
+
if (timelineCount >= 10) return;
|
|
3812
|
+
if (!isVisible(el)) return;
|
|
3813
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3814
|
+
if (text && text.length > 5) {
|
|
3815
|
+
addUniq({ meaning: text, role: "timeline-step", notes: JSON.stringify({ source: "dom-class" }) });
|
|
3816
|
+
timelineCount++;
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
let teamCount = 0;
|
|
3820
|
+
document.querySelectorAll('[class*="team-member" i], [class*="team-card" i], [class*="person-card" i], [class*="bio" i][class*="card" i]').forEach((el) => {
|
|
3821
|
+
if (teamCount >= 10) return;
|
|
3822
|
+
if (!isVisible(el)) return;
|
|
3823
|
+
const name = el.querySelector('[class*="name" i], h3, h4')?.textContent?.trim().slice(0, 60) ?? "";
|
|
3824
|
+
const role = el.querySelector('[class*="role" i], [class*="title" i], [class*="position" i]')?.textContent?.trim().slice(0, 60) ?? "";
|
|
3825
|
+
if (name) {
|
|
3826
|
+
addUniq({ meaning: `${name}${role ? " \u2014 " + role : ""}`, role: "team-member", notes: JSON.stringify({ name, role }) });
|
|
3827
|
+
teamCount++;
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
document.querySelectorAll("iframe").forEach((iframe) => {
|
|
3831
|
+
const src = iframe.getAttribute("src") || iframe.getAttribute("data-src") || "";
|
|
3832
|
+
const title2 = iframe.getAttribute("title") || iframe.getAttribute("aria-label") || "";
|
|
3833
|
+
if (!src) return;
|
|
3834
|
+
if (/youtube\.com\/embed|youtu\.be\/|youtube-nocookie\.com/i.test(src)) {
|
|
3835
|
+
const videoId = src.match(/\/embed\/([^/?&]+)/)?.[1] ?? "";
|
|
3836
|
+
addUniq({ meaning: title2 || `YouTube video${videoId ? " (ID: " + videoId + ")" : ""}`, role: "video-embed", notes: JSON.stringify({ platform: "youtube", src: src.slice(0, 200), title: title2, videoId }) });
|
|
3837
|
+
} else if (/vimeo\.com\/video\//i.test(src)) {
|
|
3838
|
+
const videoId = src.match(/video\/(\d+)/)?.[1] ?? "";
|
|
3839
|
+
addUniq({ meaning: title2 || `Vimeo video${videoId ? " (ID: " + videoId + ")" : ""}`, role: "video-embed", notes: JSON.stringify({ platform: "vimeo", src: src.slice(0, 200), title: title2, videoId }) });
|
|
3840
|
+
} else if (/wistia\.com|loom\.com|dailymotion\.com|twitch\.tv|brightcove\.com/i.test(src)) {
|
|
3841
|
+
const platform = /wistia/.test(src) ? "wistia" : /loom/.test(src) ? "loom" : /dailymotion/.test(src) ? "dailymotion" : /twitch/.test(src) ? "twitch" : "brightcove";
|
|
3842
|
+
addUniq({ meaning: title2 || `${platform} video`, role: "video-embed", notes: JSON.stringify({ platform, src: src.slice(0, 200), title: title2 }) });
|
|
3843
|
+
} else if (/maps\.google\.com|google\.com\/maps\/embed/i.test(src)) {
|
|
3844
|
+
const q = new URLSearchParams(src.split("?")[1] ?? "").get("q") ?? new URLSearchParams(src.split("?")[1] ?? "").get("query") ?? "";
|
|
3845
|
+
addUniq({ meaning: title2 || `Google Maps${q ? ": " + decodeURIComponent(q).slice(0, 80) : ""}`, role: "map-embed", notes: JSON.stringify({ provider: "google", src: src.slice(0, 200), query: q }) });
|
|
3846
|
+
} else if (/openstreetmap\.org|mapbox\.com|here\.com|maps\.apple\.com/i.test(src)) {
|
|
3847
|
+
const provider = /openstreetmap/.test(src) ? "OpenStreetMap" : /mapbox/.test(src) ? "Mapbox" : /here\.com/.test(src) ? "HERE Maps" : "Apple Maps";
|
|
3848
|
+
addUniq({ meaning: title2 || `${provider} embed`, role: "map-embed", notes: JSON.stringify({ provider, src: src.slice(0, 200), title: title2 }) });
|
|
3849
|
+
}
|
|
3850
|
+
});
|
|
3851
|
+
document.querySelectorAll("svg").forEach((svg) => {
|
|
3852
|
+
if (!isVisible(svg)) return;
|
|
3853
|
+
const titleEl = svg.querySelector("title");
|
|
3854
|
+
const label = svg.getAttribute("aria-label")?.trim() || titleEl?.textContent?.trim() || "";
|
|
3855
|
+
if (!label) return;
|
|
3856
|
+
const rect = svg.getBoundingClientRect();
|
|
3857
|
+
if (rect.width < 48 && rect.height < 48) return;
|
|
3858
|
+
addUniq({ meaning: label.slice(0, 100), role: "svg-image", notes: JSON.stringify({ width: Math.round(rect.width), height: Math.round(rect.height) }) });
|
|
3859
|
+
});
|
|
3860
|
+
let counterCount = 0;
|
|
3861
|
+
document.querySelectorAll('[data-count],[data-target],[data-to],[data-countup],[class*="counter" i]:not(button),[class*="count-up" i],[class*="odometer" i]').forEach((el) => {
|
|
3862
|
+
if (counterCount >= 15) return;
|
|
3863
|
+
if (!isVisible(el)) return;
|
|
3864
|
+
const targetVal = el.getAttribute("data-count") || el.getAttribute("data-target") || el.getAttribute("data-to") || el.getAttribute("data-countup") || "";
|
|
3865
|
+
const currentText = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 40) ?? "";
|
|
3866
|
+
const contextLabel = el.closest('[class*="stat" i],[class*="metric" i],[class*="kpi" i],[class*="counter-item" i]')?.textContent?.replace(/\s+/g, " ").trim().slice(0, 80) ?? "";
|
|
3867
|
+
const value = targetVal || currentText;
|
|
3868
|
+
if (value && value !== "0") {
|
|
3869
|
+
addUniq({ meaning: contextLabel.slice(0, 100) || `Counter: ${value}`, role: "animated-counter", notes: JSON.stringify({ targetValue: targetVal, displayedValue: currentText }) });
|
|
3870
|
+
counterCount++;
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3873
|
+
let logoCount = 0;
|
|
3874
|
+
document.querySelectorAll('[class*="client" i] img,[class*="partner" i] img,[class*="logo-grid" i] img,[class*="clients-" i] img,[class*="brand-logo" i] img,[class*="trust-logo" i] img,[class*="sponsor" i] img').forEach((img) => {
|
|
3875
|
+
if (logoCount >= 20) return;
|
|
3876
|
+
if (!isVisible(img)) return;
|
|
3877
|
+
const alt = img.getAttribute("alt")?.trim().slice(0, 60) ?? "";
|
|
3878
|
+
const src = (img.getAttribute("src") || img.getAttribute("data-src") || "").slice(0, 120);
|
|
3879
|
+
if (!alt && !src) return;
|
|
3880
|
+
addUniq({ meaning: alt || src.split("/").pop()?.split("?")[0]?.slice(0, 50) || "Client logo", role: "client-logo", notes: JSON.stringify({ alt, src, hasAlt: !!alt }) });
|
|
3881
|
+
logoCount++;
|
|
3882
|
+
});
|
|
3883
|
+
let awardCount = 0;
|
|
3884
|
+
document.querySelectorAll('[class*="award" i],[class*="certif" i]:not(script):not(link),[class*="accredit" i],[class*="trust-badge" i],[class*="recognition" i],[class*="badge" i][class*="award" i]').forEach((el) => {
|
|
3885
|
+
if (awardCount >= 10) return;
|
|
3886
|
+
if (!isVisible(el)) return;
|
|
3887
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3888
|
+
const imgEl = el.querySelector("img");
|
|
3889
|
+
const label = text || imgEl?.getAttribute("alt")?.trim() || "";
|
|
3890
|
+
if (label && label.length > 2) {
|
|
3891
|
+
addUniq({ meaning: label, role: "award-badge", notes: JSON.stringify({ hasImage: !!imgEl }) });
|
|
3892
|
+
awardCount++;
|
|
3893
|
+
}
|
|
3894
|
+
});
|
|
3895
|
+
try {
|
|
3896
|
+
const abTools = [];
|
|
3897
|
+
if (window.optimizely || window.Optimizely) abTools.push("Optimizely");
|
|
3898
|
+
if (window.VWO || window._vwo_code) abTools.push("VWO");
|
|
3899
|
+
if (window.__ABTasty || window.ABTasty) abTools.push("AB Tasty");
|
|
3900
|
+
if (document.querySelector('script[src*="optimize.js"]')) abTools.push("Google Optimize");
|
|
3901
|
+
if (window.splitbee) abTools.push("Splitbee");
|
|
3902
|
+
if (window.__GROWTHBOOK__ || document.querySelector('script[src*="growthbook"]')) abTools.push("GrowthBook");
|
|
3903
|
+
if (window.Kameleoon) abTools.push("Kameleoon");
|
|
3904
|
+
if (abTools.length > 0) {
|
|
3905
|
+
const optExp = window.optimizely?.get?.("state")?.getActiveExperimentIds?.() ?? [];
|
|
3906
|
+
addUniq({ meaning: `A/B testing: ${abTools.join(", ")}${optExp.length ? " (" + optExp.length + " active experiments)" : ""}`, role: "ab-test-layer", notes: JSON.stringify({ tools: abTools, activeExperiments: optExp.slice(0, 10) }) });
|
|
3907
|
+
}
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
try {
|
|
3911
|
+
const signals = [];
|
|
3912
|
+
if (window.__geo || window.__country || window.__region) signals.push("window geo globals");
|
|
3913
|
+
if (document.querySelector("[data-geo],[data-country],[data-region],[data-locale]")) signals.push("data-geo DOM attributes");
|
|
3914
|
+
const ipScripts = Array.from(document.querySelectorAll("script[src]")).filter((s) => /ipinfo|maxmind|ipstack|ipgeolocation/i.test(s.getAttribute("src") ?? "")).length;
|
|
3915
|
+
if (ipScripts > 0) signals.push("IP geolocation script");
|
|
3916
|
+
if (window.Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone) signals.push(`timezone: ${window.Intl.DateTimeFormat().resolvedOptions().timeZone}`);
|
|
3917
|
+
if (signals.length > 1) {
|
|
3918
|
+
addUniq({ meaning: `Personalization/geo signals: ${signals.join("; ")}`, role: "personalization-layer", notes: JSON.stringify({ signals }) });
|
|
3919
|
+
}
|
|
3920
|
+
} catch {
|
|
3921
|
+
}
|
|
3922
|
+
let hoverCount = 0;
|
|
3923
|
+
document.querySelectorAll('[class*="group"]:not(nav):not(ul):not(ol),[class*="hover-trigger" i],[data-hover],[class*="has-dropdown" i]').forEach((trigger) => {
|
|
3924
|
+
if (hoverCount >= 8) return;
|
|
3925
|
+
if (!isVisible(trigger)) return;
|
|
3926
|
+
const hiddenChildren = Array.from(trigger.querySelectorAll('[class*="group-hover"],[class*="hover-content" i],[class*="dropdown-menu" i],[class*="submenu" i]')).filter((c) => {
|
|
3927
|
+
try {
|
|
3928
|
+
const s = window.getComputedStyle(c);
|
|
3929
|
+
return s.display === "none" || s.visibility === "hidden" || parseFloat(s.opacity) < 0.1;
|
|
3930
|
+
} catch {
|
|
3931
|
+
return false;
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
for (const child of hiddenChildren.slice(0, 3)) {
|
|
3935
|
+
const text = child.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3936
|
+
if (text && text.length > 3) {
|
|
3937
|
+
addUniq({ meaning: text, role: "hover-content", notes: JSON.stringify({ triggerClass: (trigger.className?.toString() ?? "").slice(0, 60) }) });
|
|
3938
|
+
hoverCount++;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
return out.slice(0, 400);
|
|
3943
|
+
}).then((elems) => elems.map((e) => ({ ...e, isInteractive: false }))).catch(() => []);
|
|
3302
3944
|
}
|
|
3303
3945
|
async function interactAndCollectLinks(page, origin) {
|
|
3304
3946
|
const collected = /* @__PURE__ */ new Set();
|