// // AtlasExtractor.js // Zyquo Atlas // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Content-extraction driver injected into an isolated WKContentWorld alongside // Mozilla Readability.js + Readability-readerable.js. Turns the live page into // clean, model-ready markdown with metadata and the user's selection, using // the fallback ladder from docs/AI-BROWSER-RESEARCH.md §2: // 1. Readability (article) → markdown // 2. visible structured-text walk (app pages) // 3. title + url + meta description // SECURITY: only visible, rendered content is emitted — hidden/off-screen and // aria-hidden nodes are dropped so injected instructions in invisible text // never reach the model. Every entry point returns a value (never undefined) // so the Swift bridge never crashes. // (function () { "use strict"; if (window.__zyquoAtlas) return; const MAX_BYTES_DEFAULT = 600000; function meta(name) { const el = document.querySelector( 'meta[property="' + name + '"],meta[name="' + name + '"]' ); return el ? el.getAttribute("content") : null; } function canonicalURL() { const l = document.querySelector('link[rel="canonical"]'); return (l && l.href) || location.href; } function faviconURL() { const l = document.querySelector('link[rel~="icon"],link[rel="apple-touch-icon"]'); try { return l ? new URL(l.getAttribute("href"), location.href).href : new URL("/favicon.ico", location.origin).href; } catch (_) { return null; } } // Is a node visible to the user (drops the injection surface)? function isVisible(el) { if (!el || el.nodeType !== 1) return false; if (el.getAttribute && el.getAttribute("aria-hidden") === "true") return false; const s = window.getComputedStyle(el); if (!s || s.display === "none" || s.visibility === "hidden" || parseFloat(s.opacity) === 0) { return false; } const r = el.getBoundingClientRect(); if (r.width === 0 && r.height === 0) return false; return true; } // Walk visible DOM emitting lightweight markdown (headings, lists, links, // code, paragraphs). Used both to render a Readability article DOM and as the // app-page fallback. function toMarkdown(root) { const out = []; const SKIP = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "SVG", "CANVAS", "IFRAME"]); function walk(node, listDepth) { for (const child of node.childNodes) { if (child.nodeType === 3) { const t = child.textContent.replace(/\s+/g, " "); if (t.trim()) out.push(t); continue; } if (child.nodeType !== 1) continue; const tag = child.tagName; if (SKIP.has(tag)) continue; if (!isVisible(child)) continue; if (/^H[1-6]$/.test(tag)) { const level = "#".repeat(parseInt(tag[1], 10)); out.push("\n" + level + " " + child.textContent.trim() + "\n"); } else if (tag === "P") { out.push("\n" + child.textContent.trim() + "\n"); } else if (tag === "LI") { out.push("\n" + " ".repeat(listDepth) + "- " + child.textContent.trim()); } else if (tag === "PRE") { out.push("\n```\n" + child.textContent.replace(/\n+$/, "") + "\n```\n"); } else if (tag === "A" && child.getAttribute("href")) { const txt = child.textContent.trim(); if (txt) out.push("[" + txt + "](" + child.href + ")"); } else if (tag === "BR") { out.push("\n"); } else if (tag === "UL" || tag === "OL") { walk(child, listDepth + 1); out.push("\n"); } else { walk(child, listDepth); } } } walk(root, 0); return out.join(" ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim(); } // Index of heading offsets in the produced markdown for chunking + citations. function headingIndex(markdown) { const headings = []; const re = /^(#{1,6})\s+(.*)$/gm; let m; while ((m = re.exec(markdown)) !== null) { headings.push({ level: m[1].length, title: m[2].trim(), offset: m.index }); } return headings; } function currentSelection() { const sel = window.getSelection(); const text = sel ? String(sel).trim() : ""; if (!text) return null; let context = null; try { const node = sel.anchorNode && sel.anchorNode.parentElement; const block = node && node.closest("p,li,section,article,div"); if (block) context = block.textContent.replace(/\s+/g, " ").trim().slice(0, 800); } catch (_) {} return { text: text, context: context }; } function truncateAtBoundary(s, maxBytes) { if (s.length <= maxBytes) return s; const cut = s.lastIndexOf("\n", maxBytes); return s.slice(0, cut > maxBytes * 0.5 ? cut : maxBytes) + "\n\n…[truncated]"; } function extract(mode, maxBytes) { maxBytes = maxBytes || MAX_BYTES_DEFAULT; try { let markdown = ""; let quality = "reader"; let title = document.title; let byline = null; let published = null; let readerable = false; try { readerable = typeof isProbablyReaderable === "function" && isProbablyReaderable(document); } catch (_) {} if (mode !== "rawText" && readerable && typeof Readability === "function") { try { const clone = document.cloneNode(true); // never mutate the live page const article = new Readability(clone).parse(); if (article) { title = article.title || title; byline = article.byline || null; published = article.publishedTime || null; const holder = document.createElement("div"); holder.innerHTML = article.content || ""; markdown = toMarkdown(holder); } } catch (_) {} } if (!markdown || markdown.length < 400) { // fallback ladder markdown = toMarkdown(document.body || document.documentElement); quality = readerable ? "reader" : "rawText"; } if (!markdown) { // last resort markdown = (document.body ? document.body.innerText : "").trim(); quality = "rawText"; } markdown = truncateAtBoundary(markdown, maxBytes); return { ok: true, context: { url: location.href, canonical: canonicalURL(), title: (title || location.host || "").trim(), byline: byline, description: meta("description") || meta("og:description"), siteName: meta("og:site_name"), lang: document.documentElement.lang || null, published: published, favicon: faviconURL(), markdown: markdown, quality: quality, headings: headingIndex(markdown), selection: currentSelection(), wordCount: markdown ? markdown.split(/\s+/).length : 0, truncated: markdown.indexOf("…[truncated]") !== -1 } }; } catch (e) { return { ok: false, error: String((e && e.stack) || e) }; } } window.__zyquoAtlas = { extract: extract }; // Selection observer → native floating toolbar. Debounced; posts the trimmed // selection text + its viewport rect, or an empty text on collapse. var selTimer = null, lastSel = ""; function reportSelection() { try { var sel = window.getSelection(); var text = sel ? String(sel).trim() : ""; if (text === lastSel) return; lastSel = text; var payload = { text: text }; if (text && sel.rangeCount) { var r = sel.getRangeAt(0).getBoundingClientRect(); payload.x = r.x; payload.y = r.y; payload.w = r.width; payload.h = r.height; } if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.atlasSelection) { window.webkit.messageHandlers.atlasSelection.postMessage(payload); } } catch (e) { /* ignore */ } } document.addEventListener("selectionchange", function () { if (selTimer) clearTimeout(selTimer); selTimer = setTimeout(reportSelection, 220); }); })();