spb/prisme Public MIT
Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.
Swift 96.7%
JavaScript 3.3%
1//2// extractor.js — Prisme3//4// Author: Simon-Pierre Boucher <contact@spboucher.ai>5//6// Deterministic DOM extraction (Distiller step 1, CLAUDE.md §4).7// Injected at documentEnd; defines functions only — nothing runs until8// the app asks. Never sends raw HTML anywhere: produces a compact block9// list with DOM paths so every derived element can be traced back.1011(function () {12 "use strict";1314 var SKIP_TAGS = {15 SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, IFRAME: 1, SVG: 1, CANVAS: 1,16 NAV: 1, FOOTER: 1, ASIDE: 1, FORM: 1, BUTTON: 1, SELECT: 1, TEMPLATE: 117 };18 var NOISE_RE = /(^|[-_ ])(ad|ads|advert|promo|banner|cookie|consent|gdpr|newsletter|popup|paywall|subscribe|share|social|sidebar|related|comment)s?([-_ ]|$)/i;1920 function isNoise(el) {21 var probe = (el.className && String(el.className)) + " " + (el.id || "");22 return NOISE_RE.test(probe);23 }2425 function isHidden(el) {26 return el.getClientRects && el.getClientRects().length === 0;27 }2829 function linkDensity(el) {30 var text = el.textContent || "";31 if (text.length === 0) return 1;32 var linked = 0;33 var anchors = el.getElementsByTagName("a");34 for (var i = 0; i < anchors.length; i++) linked += (anchors[i].textContent || "").length;35 return linked / text.length;36 }3738 function domPath(el) {39 var path = [];40 var node = el;41 while (node && node !== document.body) {42 var parent = node.parentElement;43 if (!parent) break;44 path.unshift(Array.prototype.indexOf.call(parent.children, node));45 node = parent;46 }47 return path.join("/");48 }4950 function clean(text) {51 return text.replace(/\s+/g, " ").trim();52 }5354 function blockFor(el) {55 var tag = el.tagName;56 if (/^H[1-6]$/.test(tag)) return { kind: "heading", level: parseInt(tag[1], 10) };57 if (tag === "P") return { kind: "paragraph", level: 0 };58 if (tag === "LI") return { kind: "listItem", level: 0 };59 if (tag === "BLOCKQUOTE") return { kind: "quote", level: 0 };60 if (tag === "PRE") return { kind: "code", level: 0 };61 if (tag === "TABLE") return { kind: "table", level: 0 };62 if (tag === "FIGCAPTION") return { kind: "caption", level: 0 };63 return null;64 }6566 // Depth-first walk. A captured block's subtree is not descended into, so67 // nested text is never emitted twice.68 function walk(el, blocks, maxBlocks) {69 if (blocks.length >= maxBlocks) return;70 if (SKIP_TAGS[el.tagName] || isNoise(el) || isHidden(el)) return;7172 var spec = blockFor(el);73 if (spec) {74 if (spec.kind === "heading" || linkDensity(el) <= 0.5) {75 var text = clean(spec.kind === "table" ? (el.innerText || "") : (el.textContent || ""));76 if (text.length >= (spec.kind === "heading" ? 2 : 25)) {77 if (text.length > 2000) text = text.slice(0, 2000) + "…";78 blocks.push({ kind: spec.kind, level: spec.level, text: text, domPath: domPath(el) });79 }80 }81 return;82 }8384 for (var i = 0; i < el.children.length; i++) {85 walk(el.children[i], blocks, maxBlocks);86 }87 }8889 window.__prismeExtract = function (maxBlocks) {90 var blocks = [];91 walk(document.body, blocks, maxBlocks || 400);92 return JSON.stringify({93 title: clean(document.title || ""),94 lang: document.documentElement.lang || null,95 blocks: blocks96 });97 };9899 // Current text selection with its source anchor and section context —100 // everything an excerpt needs (CLAUDE.md §5, "L'extrait", P0).101 window.__prismeSelection = function () {102 var sel = document.getSelection();103 if (!sel || sel.isCollapsed) return null;104 var text = sel.toString().replace(/\s+/g, " ").trim();105 if (!text) return null;106 if (text.length > 2000) text = text.slice(0, 2000) + "…";107108 var node = sel.anchorNode;109 var el = node ? (node.nodeType === 1 ? node : node.parentElement) : null;110 var path = el ? domPath(el) : null;111112 // Nearest heading above the selection gives the section context.113 var heading = null;114 var probe = el;115 while (probe && probe !== document.body && !heading) {116 var sibling = probe;117 while ((sibling = sibling.previousElementSibling)) {118 if (/^H[1-6]$/.test(sibling.tagName)) { heading = sibling.textContent; break; }119 }120 probe = probe.parentElement;121 }122123 return JSON.stringify({124 text: text,125 domPath: path,126 section: heading ? clean(heading) : null127 });128 };129130 window.__prismeScrollTo = function (path) {131 var node = document.body;132 if (path) {133 var parts = path.split("/");134 for (var i = 0; i < parts.length && node; i++) {135 node = node.children[parseInt(parts[i], 10)];136 }137 }138 if (node && node.scrollIntoView) {139 node.scrollIntoView({ behavior: "smooth", block: "start" });140 return true;141 }142 return false;143 };144})();145