v0.2.0-alpha: DOM capture, all three output slices, lead gates
- Extension DOM mode: auth-context serializer — inlines CSS + images as data URLs, freezes form state, masks [data-dp-mask], strips scripts (R3 client) - API: extractDataUrls rewrites large inline resources into content-addressed assets (R3 server; verified 939KB inline -> 82-byte row + served asset) - Lead-gate steps: config on step, player overlay form blocks advance, submits to leads endpoint (editor toggle + MCP set_lead_gate) - Step-guide export (v2 slice): authed markdown + public HTML at /p/:token/guide - Video export (v3 slice, v0): ffmpeg slideshow of image steps + optional narration mux; DOM rasterization deferred to Chromium service (R4) - Editor: reorder arrows, embed-code copy, guide link, video export - REST reorder endpoint; MCP surface now 13 tools (export_guide, set_lead_gate, render_video) - ffmpeg added to runtime image E2E verified incl. R3 extraction round-trip, lead_gate in public JSON, guide renders, MCP agent exported guide + rendered video.
This commit is contained in:
parent
9b09f14916
commit
a535e15253
18 changed files with 541 additions and 22 deletions
110
extension/dom-content.js
Normal file
110
extension/dom-content.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// DOM-mode capture: on each click, serialize the live document into a
|
||||
// self-contained HTML snapshot (R3 client half — runs inside the user's auth
|
||||
// context so protected images/styles resolve). Scripts stripped; form state
|
||||
// frozen; [data-dp-mask] elements masked.
|
||||
(function () {
|
||||
if (window.__dpDomCaptureInstalled) return;
|
||||
window.__dpDomCaptureInstalled = true;
|
||||
|
||||
var busy = Promise.resolve();
|
||||
|
||||
async function toDataUrl(url) {
|
||||
try {
|
||||
var res = await fetch(url, { credentials: "include" });
|
||||
if (!res.ok) return null;
|
||||
var blob = await res.blob();
|
||||
if (blob.size > 4 * 1024 * 1024) return null; // cap single asset at 4MB
|
||||
return await new Promise(function (resolve) {
|
||||
var r = new FileReader();
|
||||
r.onload = function () { resolve(r.result); };
|
||||
r.onerror = function () { resolve(null); };
|
||||
r.readAsDataURL(blob);
|
||||
});
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
async function collectCss() {
|
||||
var parts = [];
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var sheet = document.styleSheets[i];
|
||||
try {
|
||||
var rules = sheet.cssRules;
|
||||
var buf = [];
|
||||
for (var j = 0; j < rules.length; j++) buf.push(rules[j].cssText);
|
||||
parts.push(buf.join("\n"));
|
||||
} catch (_) {
|
||||
// cross-origin: fetch the stylesheet ourselves
|
||||
if (sheet.href) {
|
||||
try {
|
||||
var res = await fetch(sheet.href, { credentials: "include" });
|
||||
if (res.ok) parts.push(await res.text());
|
||||
} catch (_) { /* skip */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
async function serialize() {
|
||||
var root = document.documentElement.cloneNode(true);
|
||||
|
||||
// strip live/script-ish elements
|
||||
root.querySelectorAll("script,noscript,iframe,link[rel=stylesheet],style,object,embed").forEach(function (el) { el.remove(); });
|
||||
|
||||
// mask sensitive elements
|
||||
root.querySelectorAll("[data-dp-mask]").forEach(function (el) { el.textContent = "•••••"; });
|
||||
|
||||
// freeze form state from the LIVE dom onto the clone (index-aligned)
|
||||
var liveInputs = document.querySelectorAll("input,textarea,select");
|
||||
var cloneInputs = root.querySelectorAll("input,textarea,select");
|
||||
liveInputs.forEach(function (live, i) {
|
||||
var c = cloneInputs[i];
|
||||
if (!c) return;
|
||||
if (live.tagName === "TEXTAREA") c.textContent = live.value;
|
||||
else if (live.tagName === "SELECT") {
|
||||
Array.prototype.forEach.call(c.options, function (o, k) { if (k === live.selectedIndex) o.setAttribute("selected", ""); else o.removeAttribute("selected"); });
|
||||
} else {
|
||||
if (live.type === "checkbox" || live.type === "radio") { if (live.checked) c.setAttribute("checked", ""); else c.removeAttribute("checked"); }
|
||||
else if (live.type === "password") c.setAttribute("value", "•••••");
|
||||
else c.setAttribute("value", live.value);
|
||||
}
|
||||
});
|
||||
|
||||
// inline images from the LIVE dom (auth context), index-aligned
|
||||
var liveImgs = document.querySelectorAll("img");
|
||||
var cloneImgs = root.querySelectorAll("img");
|
||||
for (var i = 0; i < liveImgs.length; i++) {
|
||||
var src = liveImgs[i].currentSrc || liveImgs[i].src;
|
||||
if (!src || src.startsWith("data:")) continue;
|
||||
var d = await toDataUrl(src);
|
||||
if (cloneImgs[i]) {
|
||||
if (d) { cloneImgs[i].setAttribute("src", d); cloneImgs[i].removeAttribute("srcset"); }
|
||||
else cloneImgs[i].setAttribute("src", src); // fall back to absolute URL
|
||||
}
|
||||
}
|
||||
|
||||
var css = await collectCss();
|
||||
var head = root.querySelector("head") || root.insertBefore(document.createElement("head"), root.firstChild);
|
||||
var style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
head.appendChild(style);
|
||||
var base = document.createElement("base");
|
||||
base.setAttribute("href", location.origin);
|
||||
head.insertBefore(base, head.firstChild);
|
||||
|
||||
return "<!doctype html>\n" + root.outerHTML;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
var xPct = (e.clientX / window.innerWidth) * 100;
|
||||
var yPct = (e.clientY / window.innerHeight) * 100;
|
||||
busy = busy.then(async function () {
|
||||
var html = await serialize();
|
||||
chrome.runtime.sendMessage({ type: "domstep", html: html, xPct: xPct, yPct: yPct });
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue