// // extractor.js — Prisme // // Author: Simon-Pierre Boucher // // Deterministic DOM extraction (Distiller step 1, CLAUDE.md §4). // Injected at documentEnd; defines functions only — nothing runs until // the app asks. Never sends raw HTML anywhere: produces a compact block // list with DOM paths so every derived element can be traced back. (function () { "use strict"; var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, IFRAME: 1, SVG: 1, CANVAS: 1, NAV: 1, FOOTER: 1, ASIDE: 1, FORM: 1, BUTTON: 1, SELECT: 1, TEMPLATE: 1 }; var NOISE_RE = /(^|[-_ ])(ad|ads|advert|promo|banner|cookie|consent|gdpr|newsletter|popup|paywall|subscribe|share|social|sidebar|related|comment)s?([-_ ]|$)/i; function isNoise(el) { var probe = (el.className && String(el.className)) + " " + (el.id || ""); return NOISE_RE.test(probe); } function isHidden(el) { return el.getClientRects && el.getClientRects().length === 0; } function linkDensity(el) { var text = el.textContent || ""; if (text.length === 0) return 1; var linked = 0; var anchors = el.getElementsByTagName("a"); for (var i = 0; i < anchors.length; i++) linked += (anchors[i].textContent || "").length; return linked / text.length; } function domPath(el) { var path = []; var node = el; while (node && node !== document.body) { var parent = node.parentElement; if (!parent) break; path.unshift(Array.prototype.indexOf.call(parent.children, node)); node = parent; } return path.join("/"); } function clean(text) { return text.replace(/\s+/g, " ").trim(); } function blockFor(el) { var tag = el.tagName; if (/^H[1-6]$/.test(tag)) return { kind: "heading", level: parseInt(tag[1], 10) }; if (tag === "P") return { kind: "paragraph", level: 0 }; if (tag === "LI") return { kind: "listItem", level: 0 }; if (tag === "BLOCKQUOTE") return { kind: "quote", level: 0 }; if (tag === "PRE") return { kind: "code", level: 0 }; if (tag === "TABLE") return { kind: "table", level: 0 }; if (tag === "FIGCAPTION") return { kind: "caption", level: 0 }; return null; } // Depth-first walk. A captured block's subtree is not descended into, so // nested text is never emitted twice. function walk(el, blocks, maxBlocks) { if (blocks.length >= maxBlocks) return; if (SKIP_TAGS[el.tagName] || isNoise(el) || isHidden(el)) return; var spec = blockFor(el); if (spec) { if (spec.kind === "heading" || linkDensity(el) <= 0.5) { var text = clean(spec.kind === "table" ? (el.innerText || "") : (el.textContent || "")); if (text.length >= (spec.kind === "heading" ? 2 : 25)) { if (text.length > 2000) text = text.slice(0, 2000) + "…"; blocks.push({ kind: spec.kind, level: spec.level, text: text, domPath: domPath(el) }); } } return; } for (var i = 0; i < el.children.length; i++) { walk(el.children[i], blocks, maxBlocks); } } window.__prismeExtract = function (maxBlocks) { var blocks = []; walk(document.body, blocks, maxBlocks || 400); return JSON.stringify({ title: clean(document.title || ""), lang: document.documentElement.lang || null, blocks: blocks }); }; // Current text selection with its source anchor and section context — // everything an excerpt needs (CLAUDE.md §5, "L'extrait", P0). window.__prismeSelection = function () { var sel = document.getSelection(); if (!sel || sel.isCollapsed) return null; var text = sel.toString().replace(/\s+/g, " ").trim(); if (!text) return null; if (text.length > 2000) text = text.slice(0, 2000) + "…"; var node = sel.anchorNode; var el = node ? (node.nodeType === 1 ? node : node.parentElement) : null; var path = el ? domPath(el) : null; // Nearest heading above the selection gives the section context. var heading = null; var probe = el; while (probe && probe !== document.body && !heading) { var sibling = probe; while ((sibling = sibling.previousElementSibling)) { if (/^H[1-6]$/.test(sibling.tagName)) { heading = sibling.textContent; break; } } probe = probe.parentElement; } return JSON.stringify({ text: text, domPath: path, section: heading ? clean(heading) : null }); }; window.__prismeScrollTo = function (path) { var node = document.body; if (path) { var parts = path.split("/"); for (var i = 0; i < parts.length && node; i++) { node = node.children[parseInt(parts[i], 10)]; } } if (node && node.scrollIntoView) { node.scrollIntoView({ behavior: "smooth", block: "start" }); return true; } return false; }; })();