SPB Git

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%
4.7 KB · 145 lines javascript
Raw Blame History
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