// 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