Composed-tree shadow DOM capture, sized invisible spacers, track re-anchoring, block fill-width slides, javascript: href sanitization
- Walker serializes the composed (flattened) tree: open shadow roots walk as host children with <slot> replaced by its assigned light nodes (or fallback), so custom-element content (product cards, web components) is captured instead of arriving childless - In-flow visibility:hidden nodes with a nonzero border box survive the prune as sized invisible placeholders - ghost spacers carry load-bearing geometry - When pruning drops leading in-flow children of a horizontally-translated track, the baked translateX is re-anchored by their aggregate margin-box width (settled carousel tracks return to origin instead of pushing real content off-screen); animation-owned transforms unaffected - Circular-shrink slide guard counts block-level fill children as width-deriving regardless of wAuto (block auto-width IS fill), pinning library-sized slide widths uniformly - javascript: hrefs emit as "#" and the link gate normalizes both sides 382 tests pass (18 new), typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6e4921884c
commit
762855664c
@@ -48,6 +48,14 @@ export type RawNode = {
|
||||
// libraries, FontFaceObserver): absolutely positioned, parked far off-screen, and non-painting.
|
||||
// Never user-visible; excluded from emission so it doesn't ship as page markup.
|
||||
probe?: boolean;
|
||||
// This element hosts an OPEN shadow root: its serialized children are the composed (flattened)
|
||||
// shadow tree — the shadowRoot's children with each <slot> replaced by its assigned light-DOM
|
||||
// nodes — NOT the host's light-DOM children. Custom elements (`<product-info>`) render all their
|
||||
// swatches/title/price inside this shadow tree, so a childNodes-only walk captured them empty.
|
||||
shadowHost?: boolean;
|
||||
// This node was serialized from inside a shadow tree (a shadowRoot descendant, or the shadow
|
||||
// subtree of a slotted light node). Tagged so generation can emit it as ordinary light DOM.
|
||||
inShadow?: boolean;
|
||||
sizing?: RawSizing;
|
||||
before?: RawStyle;
|
||||
after?: RawStyle;
|
||||
@@ -316,7 +324,7 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
|| bbox.x >= OFFSCREEN_PROBE_PX + vpW || bbox.y >= OFFSCREEN_PROBE_PX + pageH;
|
||||
};
|
||||
|
||||
const serializeElement = (el: Element): RawNode | null => {
|
||||
const serializeElement = (el: Element, inShadow = false): RawNode | null => {
|
||||
if (nodeCount >= MAX_NODES) { truncated = true; return null; }
|
||||
const tag = el.tagName.toLowerCase();
|
||||
nodeCount++;
|
||||
@@ -445,6 +453,7 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
bbox,
|
||||
visible: isVisible(el, cs, bbox),
|
||||
...(isProbe(cs, bbox) ? { probe: true } : {}),
|
||||
...(inShadow ? { inShadow: true } : {}),
|
||||
...(sizing ? { sizing } : {}),
|
||||
children: [],
|
||||
};
|
||||
@@ -488,8 +497,28 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
// code blocks with highlighted spans separated by newlines).
|
||||
const preserveWs = /^(pre|pre-wrap|break-spaces)/.test(cs.whiteSpace || "");
|
||||
|
||||
// Composed (flattened) child list — the tree the user actually sees, matching what
|
||||
// getComputedStyle/getBoundingClientRect already report for every node:
|
||||
// - An open shadow HOST renders its shadow tree, not its light children: iterate the
|
||||
// shadowRoot's children and tag them (and the host) so generation emits them as light DOM.
|
||||
// - A <slot> is a placeholder for the light-DOM nodes distributed into it: replace it with its
|
||||
// assignedNodes (or its own fallback children when nothing is assigned). Assigned nodes are
|
||||
// the author's real light content, so they are NOT tagged inShadow — only genuine shadow
|
||||
// descendants are. Walking the shadow tree (never the host's raw light children) means a
|
||||
// slotted child is serialized once, at the slot position, with no double-emission.
|
||||
const sr = (el as HTMLElement).shadowRoot; // open roots only; closed roots are null here
|
||||
let childInShadow = inShadow;
|
||||
let childNodes: Node[];
|
||||
if (sr) {
|
||||
node.shadowHost = true;
|
||||
childInShadow = true;
|
||||
childNodes = Array.from(sr.childNodes);
|
||||
} else {
|
||||
childNodes = Array.from(el.childNodes);
|
||||
}
|
||||
|
||||
// Recurse children (elements + text nodes), preserving order.
|
||||
for (const child of Array.from(el.childNodes)) {
|
||||
for (const child of childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
const t = child.textContent || "";
|
||||
if (preserveWs && t.length > 0) {
|
||||
@@ -509,8 +538,28 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
}
|
||||
if (child.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
const childEl = child as Element;
|
||||
if (SKIP_TAGS.has(childEl.tagName.toLowerCase())) continue;
|
||||
const sn = serializeElement(childEl);
|
||||
const childTag = childEl.tagName.toLowerCase();
|
||||
if (SKIP_TAGS.has(childTag)) continue;
|
||||
// A slot nested deeper inside a shadow tree: expand its assigned light nodes in place. The
|
||||
// assigned nodes are light DOM (author content), so they drop back to inShadow=false.
|
||||
if (childTag === "slot" && childInShadow) {
|
||||
const assigned = (childEl as HTMLSlotElement).assignedNodes({ flatten: true });
|
||||
const slotKids = assigned.length ? assigned : Array.from(childEl.childNodes);
|
||||
for (const sk of slotKids) {
|
||||
if (sk.nodeType === Node.TEXT_NODE) {
|
||||
const t = sk.textContent || "";
|
||||
if (t.trim().length > 0) node.children.push({ text: t });
|
||||
continue;
|
||||
}
|
||||
if (sk.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
const skEl = sk as Element;
|
||||
if (SKIP_TAGS.has(skEl.tagName.toLowerCase())) continue;
|
||||
const skn = serializeElement(skEl, assigned.length ? false : true);
|
||||
if (skn) node.children.push(skn);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const sn = serializeElement(childEl, childInShadow);
|
||||
if (sn) node.children.push(sn);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user