spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
1//2// AtlasExtractor.js3// Zyquo Atlas4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Content-extraction driver injected into an isolated WKContentWorld alongside9// Mozilla Readability.js + Readability-readerable.js. Turns the live page into10// clean, model-ready markdown with metadata and the user's selection, using11// the fallback ladder from docs/AI-BROWSER-RESEARCH.md §2:12// 1. Readability (article) → markdown13// 2. visible structured-text walk (app pages)14// 3. title + url + meta description15// SECURITY: only visible, rendered content is emitted — hidden/off-screen and16// aria-hidden nodes are dropped so injected instructions in invisible text17// never reach the model. Every entry point returns a value (never undefined)18// so the Swift bridge never crashes.19//2021(function () {22 "use strict";23 if (window.__zyquoAtlas) return;2425 const MAX_BYTES_DEFAULT = 600000;2627 function meta(name) {28 const el = document.querySelector(29 'meta[property="' + name + '"],meta[name="' + name + '"]'30 );31 return el ? el.getAttribute("content") : null;32 }3334 function canonicalURL() {35 const l = document.querySelector('link[rel="canonical"]');36 return (l && l.href) || location.href;37 }3839 function faviconURL() {40 const l = document.querySelector('link[rel~="icon"],link[rel="apple-touch-icon"]');41 try {42 return l ? new URL(l.getAttribute("href"), location.href).href43 : new URL("/favicon.ico", location.origin).href;44 } catch (_) { return null; }45 }4647 // Is a node visible to the user (drops the injection surface)?48 function isVisible(el) {49 if (!el || el.nodeType !== 1) return false;50 if (el.getAttribute && el.getAttribute("aria-hidden") === "true") return false;51 const s = window.getComputedStyle(el);52 if (!s || s.display === "none" || s.visibility === "hidden" || parseFloat(s.opacity) === 0) {53 return false;54 }55 const r = el.getBoundingClientRect();56 if (r.width === 0 && r.height === 0) return false;57 return true;58 }5960 // Walk visible DOM emitting lightweight markdown (headings, lists, links,61 // code, paragraphs). Used both to render a Readability article DOM and as the62 // app-page fallback.63 function toMarkdown(root) {64 const out = [];65 const SKIP = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "SVG", "CANVAS", "IFRAME"]);66 function walk(node, listDepth) {67 for (const child of node.childNodes) {68 if (child.nodeType === 3) {69 const t = child.textContent.replace(/\s+/g, " ");70 if (t.trim()) out.push(t);71 continue;72 }73 if (child.nodeType !== 1) continue;74 const tag = child.tagName;75 if (SKIP.has(tag)) continue;76 if (!isVisible(child)) continue;77 if (/^H[1-6]$/.test(tag)) {78 const level = "#".repeat(parseInt(tag[1], 10));79 out.push("\n" + level + " " + child.textContent.trim() + "\n");80 } else if (tag === "P") {81 out.push("\n" + child.textContent.trim() + "\n");82 } else if (tag === "LI") {83 out.push("\n" + " ".repeat(listDepth) + "- " + child.textContent.trim());84 } else if (tag === "PRE") {85 out.push("\n```\n" + child.textContent.replace(/\n+$/, "") + "\n```\n");86 } else if (tag === "A" && child.getAttribute("href")) {87 const txt = child.textContent.trim();88 if (txt) out.push("[" + txt + "](" + child.href + ")");89 } else if (tag === "BR") {90 out.push("\n");91 } else if (tag === "UL" || tag === "OL") {92 walk(child, listDepth + 1);93 out.push("\n");94 } else {95 walk(child, listDepth);96 }97 }98 }99 walk(root, 0);100 return out.join(" ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();101 }102103 // Index of heading offsets in the produced markdown for chunking + citations.104 function headingIndex(markdown) {105 const headings = [];106 const re = /^(#{1,6})\s+(.*)$/gm;107 let m;108 while ((m = re.exec(markdown)) !== null) {109 headings.push({ level: m[1].length, title: m[2].trim(), offset: m.index });110 }111 return headings;112 }113114 function currentSelection() {115 const sel = window.getSelection();116 const text = sel ? String(sel).trim() : "";117 if (!text) return null;118 let context = null;119 try {120 const node = sel.anchorNode && sel.anchorNode.parentElement;121 const block = node && node.closest("p,li,section,article,div");122 if (block) context = block.textContent.replace(/\s+/g, " ").trim().slice(0, 800);123 } catch (_) {}124 return { text: text, context: context };125 }126127 function truncateAtBoundary(s, maxBytes) {128 if (s.length <= maxBytes) return s;129 const cut = s.lastIndexOf("\n", maxBytes);130 return s.slice(0, cut > maxBytes * 0.5 ? cut : maxBytes) + "\n\n…[truncated]";131 }132133 function extract(mode, maxBytes) {134 maxBytes = maxBytes || MAX_BYTES_DEFAULT;135 try {136 let markdown = "";137 let quality = "reader";138 let title = document.title;139 let byline = null;140 let published = null;141 let readerable = false;142143 try {144 readerable = typeof isProbablyReaderable === "function" &&145 isProbablyReaderable(document);146 } catch (_) {}147148 if (mode !== "rawText" && readerable && typeof Readability === "function") {149 try {150 const clone = document.cloneNode(true); // never mutate the live page151 const article = new Readability(clone).parse();152 if (article) {153 title = article.title || title;154 byline = article.byline || null;155 published = article.publishedTime || null;156 const holder = document.createElement("div");157 holder.innerHTML = article.content || "";158 markdown = toMarkdown(holder);159 }160 } catch (_) {}161 }162163 if (!markdown || markdown.length < 400) { // fallback ladder164 markdown = toMarkdown(document.body || document.documentElement);165 quality = readerable ? "reader" : "rawText";166 }167 if (!markdown) { // last resort168 markdown = (document.body ? document.body.innerText : "").trim();169 quality = "rawText";170 }171172 markdown = truncateAtBoundary(markdown, maxBytes);173174 return {175 ok: true,176 context: {177 url: location.href,178 canonical: canonicalURL(),179 title: (title || location.host || "").trim(),180 byline: byline,181 description: meta("description") || meta("og:description"),182 siteName: meta("og:site_name"),183 lang: document.documentElement.lang || null,184 published: published,185 favicon: faviconURL(),186 markdown: markdown,187 quality: quality,188 headings: headingIndex(markdown),189 selection: currentSelection(),190 wordCount: markdown ? markdown.split(/\s+/).length : 0,191 truncated: markdown.indexOf("…[truncated]") !== -1192 }193 };194 } catch (e) {195 return { ok: false, error: String((e && e.stack) || e) };196 }197 }198199 window.__zyquoAtlas = { extract: extract };200201 // Selection observer → native floating toolbar. Debounced; posts the trimmed202 // selection text + its viewport rect, or an empty text on collapse.203 var selTimer = null, lastSel = "";204 function reportSelection() {205 try {206 var sel = window.getSelection();207 var text = sel ? String(sel).trim() : "";208 if (text === lastSel) return;209 lastSel = text;210 var payload = { text: text };211 if (text && sel.rangeCount) {212 var r = sel.getRangeAt(0).getBoundingClientRect();213 payload.x = r.x; payload.y = r.y; payload.w = r.width; payload.h = r.height;214 }215 if (window.webkit && window.webkit.messageHandlers &&216 window.webkit.messageHandlers.atlasSelection) {217 window.webkit.messageHandlers.atlasSelection.postMessage(payload);218 }219 } catch (e) { /* ignore */ }220 }221 document.addEventListener("selectionchange", function () {222 if (selTimer) clearTimeout(selTimer);223 selTimer = setTimeout(reportSelection, 220);224 });225})();226