import type { ImageCandidate, PageCandidate, PageDisclosureCandidate, TextCandidate } from '../shared/types'; import { discoverHeuristics } from './heuristics'; const IMAGE_ID_ATTRIBUTE = 'data-slopdetect-image-id'; const MAX_TEXT_BLOCKS = 8; const MIN_TEXT_CHARACTERS = 300; function isVisible(element: Element): boolean { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; } function makeImageCandidate(element: Element, url: string, alt = ''): ImageCandidate | undefined { if (!url || !isVisible(element)) return undefined; const rect = element.getBoundingClientRect(); if (rect.width < 16 || rect.height < 16) return undefined; const existingId = element.getAttribute(IMAGE_ID_ATTRIBUTE); const id = existingId || crypto.randomUUID(); element.setAttribute(IMAGE_ID_ATTRIBUTE, id); return { id, url: new URL(url, document.baseURI).href, alt: alt.trim().slice(0, 200), width: Math.round(rect.width), height: Math.round(rect.height) }; } function discoverImages(): ImageCandidate[] { const candidates: ImageCandidate[] = []; const seenUrls = new Set(); for (const image of document.querySelectorAll('img')) { const candidate = makeImageCandidate(image, image.currentSrc || image.src, image.alt); if (candidate && !seenUrls.has(candidate.url)) { candidates.push(candidate); seenUrls.add(candidate.url); } } for (const image of document.querySelectorAll('svg image')) { const href = image.getAttribute('href') || image.getAttribute('xlink:href') || ''; const candidate = makeImageCandidate(image, href, image.getAttribute('aria-label') || 'SVG image'); if (candidate && !seenUrls.has(candidate.url)) { candidates.push(candidate); seenUrls.add(candidate.url); } } const elements = [...document.querySelectorAll('body *')].slice(0, 2000); for (const element of elements) { const background = window.getComputedStyle(element).backgroundImage; const match = /^url\(["']?(.*?)["']?\)$/.exec(background); if (!match?.[1]) continue; const candidate = makeImageCandidate(element, match[1], element.getAttribute('aria-label') || 'Background image'); if (candidate && !seenUrls.has(candidate.url)) { candidates.push(candidate); seenUrls.add(candidate.url); } } return candidates; } export function estimateTokens(text: string): number { return text.trim() ? Math.ceil(text.trim().split(/\s+/).length * 1.3) : 0; } function discoverText(): TextCandidate[] { const selectors = 'article p, main p, [role="main"] p, article li, main li, blockquote'; const values = [...document.querySelectorAll(selectors)] .filter(isVisible) .map((element) => element.innerText.trim()) .filter((text) => text.length >= MIN_TEXT_CHARACTERS); const unique = [...new Set(values)].sort((left, right) => right.length - left.length).slice(0, MAX_TEXT_BLOCKS); if (unique.length === 0) { const root = document.querySelector('article, main, [role="main"]') || document.body; const combined = root?.innerText.trim().slice(0, 24_000) || ''; if (combined.length >= MIN_TEXT_CHARACTERS) unique.push(combined); } return unique.map((text) => ({ id: crypto.randomUUID(), text, tokenEstimate: estimateTokens(text) })); } function pushDisclosure( output: PageDisclosureCandidate[], source: string, value: string | null | undefined ): void { const trimmed = value?.trim(); if (trimmed) output.push({ source, value: trimmed.slice(0, 2000) }); } function discoverDisclosures(): PageDisclosureCandidate[] { const candidates: PageDisclosureCandidate[] = []; const interestingMeta = /ai|artificial|synthetic|provenance|credential|generator|content-origin/i; for (const meta of document.querySelectorAll('meta[name], meta[property]')) { const name = meta.name || meta.getAttribute('property') || ''; if (interestingMeta.test(name)) { pushDisclosure(candidates, `meta:${name}`, meta.content); } } for (const link of document.querySelectorAll('link[rel]')) { if (/credential|provenance|c2pa|manifest/i.test(link.rel)) { pushDisclosure(candidates, `link:${link.rel}`, link.href); } } for (const script of document.querySelectorAll('script[type="application/ld+json"]')) { if (/"(?:aiGenerated|isAIGenerated|contentOrigin|digitalSourceType)"\s*:\s*(?:true|"[^"]*(?:trainedAlgorithmicMedia|synthetic|AI)[^"]*")/i.test(script.textContent || '')) { pushDisclosure(candidates, 'structured-data', script.textContent); } } const labelSelectors = 'figcaption, small, [class*="disclosure" i], [class*="provenance" i], [class*="credit" i], [aria-label*="AI-generated" i]'; for (const element of document.querySelectorAll(labelSelectors)) { const value = (element.innerText || element.getAttribute('aria-label') || '').trim(); if (value.length <= 300 && /\b(?:AI[- ]generated|generated by AI|artificially generated|created with AI|synthetic media)\b/i.test(value)) { pushDisclosure(candidates, 'visible-label', value); } } for (const element of document.querySelectorAll('[data-ai-generated], [data-content-origin]')) { pushDisclosure( candidates, element.hasAttribute('data-ai-generated') ? 'data-ai-generated' : 'data-content-origin', element.getAttribute('data-ai-generated') || element.getAttribute('data-content-origin') ); } return candidates; } export function discoverPage(): PageCandidate { return { url: location.href, title: document.title, images: discoverImages(), texts: discoverText(), disclosures: discoverDisclosures(), heuristics: discoverHeuristics() }; } export { IMAGE_ID_ATTRIBUTE };