/** * SDK document model — adaptation layer on top of @hyperframes/core. * * F6 decision: SDK builds ON core, no parser duplication. * - ensureHfIds (from core) is the parse entry point: all construction starts here. * - DOMParser is NOT used (browser-only). linkedom is the node-safe primitive. * - ParsedHtml (core) is the Studio timeline view (timed elements only). * HyperFramesElement is the editing view (ALL editable elements, with raw attrs). */ import { parseHTML } from "linkedom"; import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids"; import { parseGsapScriptAcornForWrite } from "@hyperframes/core/gsap-parser-acorn"; import { findRoot, getElementStyles, getGsapScripts, getOwnText, isNewHostBoundary, querySelectorAllDeep, } from "./engine/model.js"; import type { HyperFramesElement, SdkDocument } from "./types.js"; // Tags that carry no editable content and must not enter the element tree. const EXCLUDED_TAGS = new Set([ "script", "style", "template", "meta", "link", "noscript", "base", "head", ]); // Snapshot text is TRIMMED for display (markup indentation produces noisy // whitespace text nodes). The raw text target is shared with setText so shadow // value checks and dispatch serialization use the same DOM target. function snapshotText(el: Element): string | null { const trimmed = getOwnText(el).trim(); return trimmed.length > 0 ? trimmed : null; } // Parsing the GSAP script (acorn AST walk) is the expensive part and depends // only on the script text, so memoize the {tween id, selector} pairs by script. // Selector→hf-id resolution still runs each call — it depends on the live DOM, // which changes on dispatch. Single-entry cache covers the hot path (same comp, // repeated getElements() rebuilds) and stays bounded. let gsapLocatedCacheKey: string | null = null; let gsapLocatedCacheVal: Array<{ id: string; selector: string }> = []; function parseLocatedCached(script: string): Array<{ id: string; selector: string }> { if (gsapLocatedCacheKey === script) return gsapLocatedCacheVal; const parsed = parseGsapScriptAcornForWrite(script); gsapLocatedCacheVal = parsed ? parsed.located.map(({ id, animation }) => ({ id, selector: animation.targetSelector })) : []; gsapLocatedCacheKey = script; return gsapLocatedCacheVal; } /** * Map each element's data-hf-id → the GSAP tween ids targeting it. Tween ids * come from the acorn parser's stable `targetSelector-method-position` scheme — * the SAME id-space the studio-api read path and the SDK GSAP ops use, so these * ids are dispatchable as-is via setGsapTween/removeGsapTween. Best-effort: a * malformed selector or unparseable script yields no entries (animationIds: []). */ function buildAnimationIdMap(document: Document): Map { const map = new Map(); for (const script of getGsapScripts(document)) { for (const { id, selector } of parseLocatedCached(script)) { appendAnimationIdsForSelector(map, document, id, selector); } } return map; } function appendAnimationIdsForSelector( map: Map, document: Document, animationId: string, selector: string, ): void { if (!selector) return; let matches: Element[]; try { matches = querySelectorAllDeep(document, selector); } catch { return; // selector not valid for querySelectorAll — skip } for (const el of matches) { const hfId = el.getAttribute("data-hf-id"); if (!hfId) continue; const list = map.get(hfId); if (list) list.push(animationId); else map.set(hfId, [animationId]); } } /** * Every GSAP tween id `parseLocatedCached` finds in the script, with no DOM * matching at all — the same id space the server-side script ops * (removeAllKeyframesFromScript et al.) resolve against. Unlike * buildAnimationIdMap's per-element map, this never drops an id just because * its selector doesn't currently CSS-match a live element — that gap is what * caused a false animation_not_found divergence in the resolver-shadow * tripwire (a tween on a renamed/duplicate/scoped selector still resolves on * the server, which reads the script directly). */ export function parsedAnimationIds(script: string): Set { return new Set(parseLocatedCached(script).map(({ id }) => id)); } /** * Build the element list for a parent's children, treating a COMPOSITION * template (`