// Open Design web clipper page-capture runtime. // // Injected on demand by the service worker via // `chrome.scripting.executeScript({ files: ['capture.js'] })`. It runs in the // page (the extension's isolated content-script world — full DOM + layout // access) and exposes two entry points: // // • `window.__odCapture(opts)` — full-page snapshot (the two artifacts below). // • `window.__odCaptureElement(opts)` — a single picked element captured as a // self-contained `html` snapshot: the page's CSS is kept so the element's // cascade resolves, but the DOM is pruned to just the element (and its // ancestor chain), so opening the saved file shows that one element styled // as it was on the page. One HTML file, easy to share/distribute. // // The full-page pass produces, in ONE go, two artifacts: // // 1. `html` — a self-contained, high-fidelity snapshot of the page: // readable stylesheets inlined, scripts stripped, every URL // absolutized, and a `` injected. Cross-origin image / // background URLs are left absolute in the string; the worker // rewrites them to data URIs afterwards (only the worker can // fetch cross-origin without CORS). // // 2. `figmaIr` — the OD Figma capture IR (a JSON node-tree of FRAME / TEXT / // RECTANGLE nodes with absolute geometry, fills, strokes, // corner radii, shadows and fonts). See `figma-plugin/IR.md`. // Image fills carry a `url` placeholder the worker swaps for a // data URI. // // `resources` is the deduped list of absolute http(s) URLs both outputs depend // on, so the worker fetches each one only once. // // Self-contained, dependency-free, never throws into the caller (the worker // wraps it best-effort). (function () { if (window.__odCapture) return; // idempotent across repeated injections const IR_VERSION = 1; const MAX_NODES = 6000; // safety cap on IR size; logged when hit const MAX_RESOURCES = 300; // --- small utilities ----------------------------------------------------- function absUrl(url, base) { if (!url) return url; try { return new URL(url, base || document.baseURI).href; } catch { return url; } } function isHttp(url) { return typeof url === 'string' && /^https?:\/\//i.test(url); } // Rewrite every `url(...)` inside a CSS string to an absolute URL resolved // against `base` (the stylesheet's own href, so relative asset paths work). function absolutizeCss(cssText, base) { return cssText.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (m, q, ref) => { if (/^(data:|blob:|#)/i.test(ref)) return m; return `url(${q}${absUrl(ref, base)}${q})`; }); } // Collect absolute http(s) url()s out of a CSS string into `sink`. function collectCssUrls(cssText, sink) { const re = /url\(\s*['"]?([^'")]+)['"]?\s*\)/gi; let m; while ((m = re.exec(cssText))) { if (isHttp(m[1])) sink.add(m[1]); } } function absolutizeSrcset(srcset) { return srcset .split(',') .map((part) => { const seg = part.trim(); if (!seg) return ''; const sp = seg.indexOf(' '); const url = sp === -1 ? seg : seg.slice(0, sp); const desc = sp === -1 ? '' : seg.slice(sp); return `${absUrl(url)}${desc}`; }) .filter(Boolean) .join(', '); } // --- self-contained HTML ------------------------------------------------- // Clone , strip the bits a saved snapshot must never carry (live // scripts, our own on-page UI, the page's ). Shared by the full-page // and single-element capture paths. function cloneDocument() { const clone = document.documentElement.cloneNode(true); // Strip scripts (the Library renders snapshots sandboxed; live JS only // mutates the DOM after load and would never run there anyway). clone.querySelectorAll('script, noscript').forEach((n) => n.remove()); // Never serialize our own injected on-page UI (toolbar / toast / pickers). clone.querySelectorAll('[id^="od-clipper-"]').forEach((n) => n.remove()); // Drop any existing ; we inject our own from the live baseURI. clone.querySelectorAll('base').forEach((n) => n.remove()); return clone; } // Inline readable stylesheets in place of their , and absolutize the // page's inline