@hyperframes/parsers 0.8.141 → 0.8.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assetResolution.d.ts +2 -31
- package/dist/assetResolution.js +104 -19
- package/dist/assetResolution.js.map +1 -1
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +51 -15
- package/dist/assets.js.map +1 -1
- package/dist/composition.d.ts +2 -3
- package/dist/composition.js +11 -7
- package/dist/composition.js.map +1 -1
- package/dist/hfIds.js +3 -1
- package/dist/hfIds.js.map +1 -1
- package/dist/index.d.ts +29 -2
- package/dist/index.js +129 -8
- package/dist/index.js.map +1 -1
- package/dist/slideshow.js +14 -2
- package/dist/slideshow.js.map +1 -1
- package/dist/urlPath-C_0f_-Hp.d.ts +11 -0
- package/package.json +3 -2
package/dist/composition.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/fontAliases.ts","../src/utils/urlPath.ts","../src/compositionVariables.ts","../src/variableUsage.ts","../src/compositionContract.ts","../src/canvasScaffoldPatterns.ts","../src/mediaSrcMutation.ts"],"sourcesContent":["// ── Composition data types ───────────────────────────────────────────────────\n// Moved from @hyperframes/core/core.types in the parsers extraction refactor.\n// These are the types produced and consumed by the parser pipeline.\n\nexport interface Asset {\n id: string;\n url: string;\n type: string;\n is_reference?: boolean;\n /** Duration in seconds for video/audio assets */\n duration?: number;\n}\n\n// ── Timeline types ──────────────────────────────────────────────────────────\n\nexport type TimelineElementType = \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\nexport type MediaElementType = \"video\" | \"image\" | \"audio\";\n\nexport const CANVAS_DIMENSIONS = {\n landscape: { width: 1920, height: 1080 },\n portrait: { width: 1080, height: 1920 },\n \"landscape-4k\": { width: 3840, height: 2160 },\n \"portrait-4k\": { width: 2160, height: 3840 },\n square: { width: 1080, height: 1080 },\n \"square-4k\": { width: 2160, height: 2160 },\n} as const;\n\n// Single source of truth: derive the type from the table so adding a preset\n// extends the union automatically. Avoids the prior `as readonly CanvasResolution[]`\n// cast on `VALID_CANVAS_RESOLUTIONS` quietly drifting if the table grew but\n// the union didn't.\nexport type CanvasResolution = keyof typeof CANVAS_DIMENSIONS;\n\n// `Object.keys` ordering matches insertion order in `CANVAS_DIMENSIONS` on\n// every supported JS engine; tests pin the order in `index.test.ts`. Reorder\n// the table above with care.\nexport const VALID_CANVAS_RESOLUTIONS = Object.keys(\n CANVAS_DIMENSIONS,\n) as readonly CanvasResolution[];\n\nconst RESOLUTION_ALIASES: Record<string, CanvasResolution> = {\n \"1080p\": \"landscape\",\n hd: \"landscape\",\n \"1080p-portrait\": \"portrait\",\n \"portrait-1080p\": \"portrait\",\n \"4k\": \"landscape-4k\",\n uhd: \"landscape-4k\",\n \"4k-portrait\": \"portrait-4k\",\n \"1080p-square\": \"square\",\n \"square-1080p\": \"square\",\n \"4k-square\": \"square-4k\",\n};\n\n/**\n * Aliases that name a resolution *tier* (1080p / 4K) without also nailing an\n * orientation. Historically they all normalize to the `landscape` preset\n * (`normalizeResolutionFlag(\"1080p\") === \"landscape\"`), which then rejects\n * portrait / square compositions with a cryptic \"aspect ratio does not match\"\n * error deep inside the render pipeline. Consumers that know the composition's\n * dimensions can consult this set to decide whether to auto-adapt the preset's\n * orientation instead — see `adaptAspectAgnosticResolution` in the compile\n * stage (`@hyperframes/producer`) and `suggestMatchingPreset` here.\n *\n * Orientation-suffixed aliases (`1080p-portrait`, `4k-square`, …) are absent\n * on purpose: the user *did* pick an orientation, and honoring it is important\n * for the \"you asked for landscape but composed portrait\" error to still fire.\n */\nconst ASPECT_AGNOSTIC_RESOLUTION_ALIASES: ReadonlySet<string> = new Set([\n \"1080p\",\n \"hd\",\n \"4k\",\n \"uhd\",\n]);\n\n/**\n * Map a user-facing resolution string (canonical name or alias) to a\n * `CanvasResolution`. Returns undefined for unknown values so callers\n * can produce their own \"invalid\" UX (CLI exit, route validation, etc.).\n */\nexport function normalizeResolutionFlag(input: string | undefined): CanvasResolution | undefined {\n if (!input) return undefined;\n const lowered = input.toLowerCase();\n if ((VALID_CANVAS_RESOLUTIONS as readonly string[]).includes(lowered)) {\n return lowered as CanvasResolution;\n }\n return RESOLUTION_ALIASES[lowered];\n}\n\n/**\n * True when `input` names a resolution *tier* without nailing an orientation\n * (`1080p`, `hd`, `4k`, `uhd`). Case-insensitive.\n *\n * The `--resolution` CLI flag treats these as \"target this size; keep the\n * composition's orientation\" — a portrait 1080×1920 comp with `--resolution\n * 1080p` should land at 1080×1920, not blow up on aspect mismatch. Explicit\n * canonical presets (`landscape`, `portrait`, …) and orientation-suffixed\n * aliases (`1080p-portrait`) stay strict — the user picked an orientation.\n *\n * Consumers pair this signal with the composition's dimensions (in a\n * follow-up pass after HTML parse) to pick the right preset via\n * `suggestMatchingPreset` — see `adaptAspectAgnosticResolution` in the\n * compile stage (`@hyperframes/producer`) for the canonical remap.\n */\nexport function isAspectAgnosticResolutionAlias(input: string | undefined): boolean {\n if (!input) return false;\n return ASPECT_AGNOSTIC_RESOLUTION_ALIASES.has(input.toLowerCase());\n}\n\n/**\n * Public-boundary helper: given a raw `--resolution` / `--output-resolution`\n * flag value, return the pair every distributed render entrypoint needs to\n * forward end-to-end so the compile stage can adapt aspect-agnostic aliases\n * to the composition's orientation:\n *\n * - `outputResolution`: normalized {@link CanvasResolution} (or `undefined`\n * for unknown values — callers own their invalid-input UX).\n * - `outputResolutionAspectAgnostic`: `true` when the raw input was a\n * tier-only alias (`1080p` / `hd` / `4k` / `uhd`). Passes through to\n * `DistributedRenderConfig.outputResolutionAspectAgnostic` so the compile\n * stage remaps `landscape` → `portrait` / `square` when the composition\n * dimensions demand it.\n *\n * Exported to centralize the two-step pattern (`normalizeResolutionFlag` +\n * `isAspectAgnosticResolutionAlias`) that would otherwise be duplicated at\n * every entrypoint that emits a `DistributedRenderConfig` (`hyperframes\n * cloudrun render`, `hyperframes lambda render` / `render-batch`, the local\n * CLI). Divergence between those callers is what shipped the portrait-1080p\n * regression this helper prevents from recurring.\n */\nexport interface ResolvedResolutionFlag {\n outputResolution: CanvasResolution | undefined;\n outputResolutionAspectAgnostic: boolean;\n}\n\nexport function resolveResolutionFlagPair(input: string | undefined): ResolvedResolutionFlag {\n return {\n outputResolution: normalizeResolutionFlag(input),\n outputResolutionAspectAgnostic: isAspectAgnosticResolutionAlias(input),\n };\n}\n\nexport interface TimelineElementBase {\n id: string;\n type: TimelineElementType;\n name: string;\n startTime: number;\n duration: number;\n zIndex: number;\n x?: number;\n y?: number;\n scale?: number;\n opacity?: number;\n}\n\nexport interface TimelineMediaElement extends TimelineElementBase {\n type: MediaElementType;\n src: string;\n mediaStartTime?: number;\n sourceDuration?: number;\n isAroll?: boolean;\n sourceWidth?: number;\n sourceHeight?: number;\n volume?: number; // linear gain; 0 is silent, 1 is 0 dB, values above 1 boost\n hasAudio?: boolean; // For videos - indicates if video has audio track\n}\n\nexport interface WaveformData {\n peaks: number[];\n duration: number;\n sampleRate?: number;\n}\n\nexport interface TimelineTextElement extends TimelineElementBase {\n type: \"text\";\n content: string;\n color?: string;\n fontSize?: number;\n textShadow?: boolean;\n fontFamily?: string;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n}\n\nexport interface TimelineCompositionElement extends TimelineElementBase {\n type: \"composition\";\n src: string;\n compositionId: string;\n scale?: number;\n sourceDuration?: number;\n variableValues?: Record<string, string | number | boolean>;\n sourceWidth?: number;\n sourceHeight?: number;\n}\n\n// Composition Variable Types\nexport type CompositionVariableType =\n | \"string\"\n | \"number\"\n | \"color\"\n | \"boolean\"\n | \"enum\"\n | \"font\"\n | \"image\";\n\n/**\n * Runtime list of every valid `CompositionVariableType`. Use this anywhere\n * a Set/array of valid type strings is needed (lint rules, validators).\n * The `satisfies` guard turns adding a new variant to the union without\n * also adding it here into a compile error.\n */\nexport const COMPOSITION_VARIABLE_TYPES = [\n \"string\",\n \"number\",\n \"color\",\n \"boolean\",\n \"enum\",\n \"font\",\n \"image\",\n] as const satisfies readonly CompositionVariableType[];\n\nexport interface CompositionVariableBase {\n id: string;\n type: CompositionVariableType;\n label: string;\n description?: string;\n}\n\nexport interface StringVariable extends CompositionVariableBase {\n type: \"string\";\n default: string;\n placeholder?: string;\n maxLength?: number;\n}\n\nexport interface NumberVariable extends CompositionVariableBase {\n type: \"number\";\n default: number;\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n}\n\nexport interface ColorVariable extends CompositionVariableBase {\n type: \"color\";\n default: string;\n /** Brand role identifier, e.g. \"color:primary\". */\n brandRole?: string;\n}\n\nexport interface BooleanVariable extends CompositionVariableBase {\n type: \"boolean\";\n default: boolean;\n}\n\nexport interface EnumVariable extends CompositionVariableBase {\n type: \"enum\";\n default: string;\n options: { value: string; label: string }[];\n}\n\n/**\n * Font variable — value is a `{name, source}` object (object-valued; LOCKED §7).\n * `default` is the fallback font-family name string.\n * `source` is the font stylesheet URL (e.g. Google Fonts CSS).\n * `default_name` / `default_source` are the CSS-level fallbacks when the\n * brand font is absent.\n */\nexport interface FontVariable extends CompositionVariableBase {\n type: \"font\";\n /** Fallback font-family name, e.g. \"Inter\". */\n default: string;\n /** Font stylesheet URL (e.g. Google Fonts CSS link). */\n source?: string;\n /** CSS font-family name to use when source is unavailable, e.g. \"sans-serif\". */\n default_name?: string;\n /** Fallback font stylesheet URL (empty string = system font). */\n default_source?: string;\n}\n\n/**\n * Image variable — value is a `{url, …}` object (object-valued; LOCKED §7).\n * `default` is the fallback image URL string.\n * `brandRole` is an optional semantic label, e.g. \"logo:primary\".\n */\nexport interface ImageVariable extends CompositionVariableBase {\n type: \"image\";\n /** Fallback image URL. */\n default: string;\n /** Brand role identifier, e.g. \"logo:primary\". */\n brandRole?: string;\n}\n\nexport type CompositionVariable =\n | StringVariable\n | NumberVariable\n | ColorVariable\n | BooleanVariable\n | EnumVariable\n | FontVariable\n | ImageVariable;\n\nexport interface CompositionSpec {\n id: string;\n duration: number;\n variables: CompositionVariable[];\n}\n\nexport type TimelineElement =\n | TimelineMediaElement\n | TimelineTextElement\n | TimelineCompositionElement;\n\nexport function isTextElement(el: TimelineElement): el is TimelineTextElement {\n return el.type === \"text\";\n}\n\nexport function isMediaElement(el: TimelineElement): el is TimelineMediaElement {\n return el.type === \"video\" || el.type === \"image\" || el.type === \"audio\";\n}\n\nexport function isCompositionElement(el: TimelineElement): el is TimelineCompositionElement {\n return el.type === \"composition\";\n}\n\nexport interface MediaFile {\n id: string;\n name: string;\n type: TimelineElementType;\n src: string;\n file?: File;\n duration?: number;\n compositionId?: string;\n sourceWidth?: number; // Intrinsic width for compositions\n sourceHeight?: number; // Intrinsic height for compositions\n}\n\nexport const TIMELINE_COLORS: Record<TimelineElementType, string> = {\n video: \"#ec4899\",\n image: \"#3b82f6\",\n text: \"#06b6d4\",\n audio: \"#10b981\",\n composition: \"#f97316\",\n};\n\nexport const DEFAULT_DURATIONS: Record<TimelineElementType, number> = {\n video: 5,\n image: 5,\n text: 2,\n audio: 5,\n composition: 5,\n};\n\nexport interface CompositionAPI {\n id: string;\n duration: number;\n seek(time: number): void;\n getTime(): number;\n getDuration(): number;\n}\n\n// ── Player API types (used by runtime) ────────────────────────────────────\n\nexport interface PlayerAPI {\n play(): void;\n pause(): void;\n seek(time: number, options?: { keepPlaying?: boolean }): void;\n getTime(): number;\n getDuration(): number;\n isPlaying(): boolean;\n getMainTimeline(): unknown;\n getElementBounds(elementId: string): void;\n getElementsAtPoint(x: number, y: number): void;\n setElementPosition(elementId: string, x: number, y: number): void;\n previewElementPosition(elementId: string, x: number, y: number): void;\n setElementKeyframes(\n elementId: string,\n keyframes: Array<{\n id: string;\n time: number;\n properties: { x?: number; y?: number };\n }> | null,\n ): void;\n setElementScale(elementId: string, scale: number): void;\n setElementFontSize(elementId: string, fontSize: number): void;\n setElementTextContent(elementId: string, content: string): void;\n setElementTextColor(elementId: string, color: string): void;\n setElementTextShadow(elementId: string, enabled: boolean): void;\n setElementTextFontWeight(elementId: string, weight: number): void;\n setElementTextFontFamily(elementId: string, fontFamily: string): void;\n setElementTextOutline(elementId: string, enabled: boolean, color?: string, width?: number): void;\n setElementTextHighlight(\n elementId: string,\n enabled: boolean,\n color?: string,\n padding?: number,\n radius?: number,\n ): void;\n setElementVolume(elementId: string, volume: number): void;\n setStageZoom(scale: number, focusX: number, focusY: number): void;\n getStageZoom(): { scale: number; focusX: number; focusY: number };\n setStageZoomKeyframes(\n keyframes: Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }> | null,\n ): void;\n getStageZoomKeyframes(): Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }>;\n addElement(data: AddElementData): boolean;\n removeElement(elementId: string): boolean;\n updateElementTiming(elementId: string, start?: number, end?: number): boolean;\n setElementTiming(\n elementId: string,\n startTime: number,\n duration: number,\n mediaStartTime?: number,\n ): void;\n updateElementSrc(elementId: string, src: string): boolean;\n updateElementLayer(elementId: string, zIndex: number): boolean;\n updateElementBasePosition(elementId: string, x?: number, y?: number, scale?: number): boolean;\n markTimelineDirty(): void;\n isTimelineDirty(): boolean;\n rebuildTimeline(): void;\n ensureTimeline(): void;\n enableRenderMode(): void;\n disableRenderMode(): void;\n renderSeek(\n time: number,\n options?: { suppressEvents?: boolean; subFrameDivisions?: number; exact?: boolean },\n ): void;\n getElementVisibility(elementId: string): { visible: boolean; opacity?: number };\n getVisibleElements(): Array<{ id: string; tagName: string; start: number; end: number }>;\n getRenderState(): {\n time: number;\n duration: number;\n isPlaying: boolean;\n renderMode: boolean;\n timelineDirty: boolean;\n };\n}\n\nexport interface AddElementData {\n id: string;\n type: \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\n name?: string;\n src?: string;\n content?: string;\n start: number;\n end: number;\n zIndex?: number;\n x?: number;\n y?: number;\n scale?: number;\n fontSize?: number;\n color?: string;\n textShadow?: boolean;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n compositionId?: string;\n sourceWidth?: number;\n sourceHeight?: number;\n isAroll?: boolean;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n}\n\nexport interface CompositionAsset {\n id: string;\n name: string;\n type: \"composition\";\n src: string;\n duration: number;\n compositionId: string;\n thumbnail?: string;\n}\n\nexport interface Keyframe {\n id: string;\n time: number;\n properties: Partial<KeyframeProperties>;\n ease?: string;\n}\n\nexport interface KeyframeProperties {\n x: number;\n y: number;\n opacity: number;\n scale: number;\n scaleX: number;\n scaleY: number;\n rotation: number;\n width: number;\n height: number;\n}\n\nexport interface ElementKeyframes {\n elementId: string;\n keyframes: Keyframe[];\n}\n\nexport interface StageZoom {\n scale: number;\n focusX: number;\n focusY: number;\n}\n\nexport interface StageZoomKeyframe {\n id: string;\n time: number;\n zoom: StageZoom;\n ease?: string;\n}\n\nexport function getDefaultStageZoom(resolution: CanvasResolution): StageZoom {\n const { width, height } = CANVAS_DIMENSIONS[resolution];\n return {\n scale: 1,\n focusX: width / 2,\n focusY: height / 2,\n };\n}\n","/**\n * Single source of truth for the deterministic font alias map. Both the\n * producer's @font-face injector and the core lint rules import from here,\n * eliminating manual drift between the two.\n *\n * Keys are lowercase font family names. Values are canonical font slugs\n * matching CANONICAL_FONTS keys in core's fonts/deterministicFonts module.\n */\nexport const FONT_ALIAS_MAP = {\n // ── Canonical bundled fonts (self-referencing) ────────────────────────\n inter: \"inter\",\n montserrat: \"montserrat\",\n outfit: \"outfit\",\n nunito: \"nunito\",\n oswald: \"oswald\",\n \"league gothic\": \"league-gothic\",\n \"archivo black\": \"archivo-black\",\n \"space mono\": \"space-mono\",\n \"ibm plex mono\": \"ibm-plex-mono\",\n \"jetbrains mono\": \"jetbrains-mono\",\n \"eb garamond\": \"eb-garamond\",\n \"playfair display\": \"playfair-display\",\n \"source code pro\": \"source-code-pro\",\n \"noto sans jp\": \"noto-sans-jp\",\n roboto: \"roboto\",\n \"open sans\": \"open-sans\",\n lato: \"lato\",\n poppins: \"poppins\",\n\n // ── Common aliases → nearest canonical ────────────────────────────────\n \"helvetica neue\": \"inter\",\n helvetica: \"inter\",\n arial: \"inter\",\n \"helvetica bold\": \"inter\",\n futura: \"montserrat\",\n \"din alternate\": \"montserrat\",\n \"arial black\": \"montserrat\",\n \"bebas neue\": \"league-gothic\",\n \"courier new\": \"jetbrains-mono\",\n courier: \"jetbrains-mono\",\n garamond: \"eb-garamond\",\n \"noto sans japanese\": \"noto-sans-jp\",\n \"segoe ui\": \"roboto\",\n\n // ── macOS sans-serif system fonts → inter ─────────────────────────────\n \"sf pro\": \"inter\",\n \"sf pro display\": \"inter\",\n \"sf pro text\": \"inter\",\n \"sf pro rounded\": \"inter\",\n avenir: \"inter\",\n \"avenir next\": \"inter\",\n \"lucida grande\": \"inter\",\n geneva: \"inter\",\n optima: \"inter\",\n\n // ── Windows sans-serif system fonts → inter ───────────────────────────\n verdana: \"inter\",\n tahoma: \"inter\",\n \"trebuchet ms\": \"inter\",\n calibri: \"inter\",\n candara: \"inter\",\n corbel: \"inter\",\n \"lucida sans\": \"inter\",\n \"lucida sans unicode\": \"inter\",\n\n // ── Linux sans-serif system fonts → inter ─────────────────────────────\n \"noto sans\": \"inter\",\n \"dejavu sans\": \"inter\",\n \"liberation sans\": \"inter\",\n\n // ── Monospace system fonts → jetbrains-mono ───────────────────────────\n \"sf mono\": \"jetbrains-mono\",\n menlo: \"jetbrains-mono\",\n monaco: \"jetbrains-mono\",\n consolas: \"jetbrains-mono\",\n \"lucida console\": \"jetbrains-mono\",\n \"lucida sans typewriter\": \"jetbrains-mono\",\n \"andale mono\": \"jetbrains-mono\",\n \"dejavu sans mono\": \"jetbrains-mono\",\n \"liberation mono\": \"jetbrains-mono\",\n\n // ── Serif system fonts → eb-garamond ──────────────────────────────────\n georgia: \"eb-garamond\",\n palatino: \"eb-garamond\",\n \"palatino linotype\": \"eb-garamond\",\n \"book antiqua\": \"eb-garamond\",\n cambria: \"eb-garamond\",\n times: \"eb-garamond\",\n \"times new roman\": \"eb-garamond\",\n \"dejavu serif\": \"eb-garamond\",\n \"liberation serif\": \"eb-garamond\",\n} satisfies Readonly<Record<string, string>>;\n\nexport const FONT_ALIAS_KEYS: ReadonlySet<string> = new Set(Object.keys(FONT_ALIAS_MAP));\n\n/**\n * Human-readable display names for canonical font slugs. Used by the lint\n * rule to tell authors what their aliased font will render as.\n */\nexport const CANONICAL_FONT_DISPLAY_NAMES: Readonly<Record<string, string>> = {\n inter: \"Inter\",\n montserrat: \"Montserrat\",\n outfit: \"Outfit\",\n nunito: \"Nunito\",\n oswald: \"Oswald\",\n \"league-gothic\": \"League Gothic\",\n \"archivo-black\": \"Archivo Black\",\n \"space-mono\": \"Space Mono\",\n \"ibm-plex-mono\": \"IBM Plex Mono\",\n \"jetbrains-mono\": \"JetBrains Mono\",\n \"eb-garamond\": \"EB Garamond\",\n \"playfair-display\": \"Playfair Display\",\n \"source-code-pro\": \"Source Code Pro\",\n \"noto-sans-jp\": \"Noto Sans JP\",\n roboto: \"Roboto\",\n \"open-sans\": \"Open Sans\",\n lato: \"Lato\",\n poppins: \"Poppins\",\n};\n\n/**\n * Resolve a font alias to its canonical display name, or undefined if the\n * alias is not in the map.\n */\nexport function resolveAliasDisplayName(alias: string): string | undefined {\n const slug = (FONT_ALIAS_MAP as Record<string, string>)[alias.toLowerCase()];\n if (!slug) return undefined;\n return CANONICAL_FONT_DISPLAY_NAMES[slug];\n}\n","export function decodeUrlPathVariants(path: string): string[] {\n const variants = [path];\n try {\n const decoded = decodeURIComponent(path);\n if (decoded !== path) variants.unshift(decoded);\n } catch {\n // Malformed percent sequences may be literal filesystem names.\n }\n\n return variants;\n}\n","/**\n * Browser-safe parser for the `data-composition-variables` schema attribute.\n * Lives outside htmlParser.ts so browser consumers (SDK, Studio, lint) can\n * import it via `@hyperframes/parsers/composition` without pulling the\n * linkedom/Node HTML-parser machinery from the main entry.\n */\n\nimport type { CompositionVariable, CompositionVariableType } from \"./types.js\";\n\n/**\n * Required typeof for each variable type's `default`. For font the default is\n * the font-family name string; for image it is the fallback URL string —\n * extra metadata fields on both are optional and not validated here.\n */\nconst DEFAULT_TYPEOF: Record<CompositionVariableType, \"string\" | \"number\" | \"boolean\"> = {\n string: \"string\",\n number: \"number\",\n color: \"string\",\n boolean: \"boolean\",\n enum: \"string\",\n font: \"string\",\n image: \"string\",\n};\n\n/**\n * Scalar variable values (string/number/boolean) are the ones that flow into\n * CSS custom props and text bindings; font/image values are object-shaped.\n * Shared so the SDK's CSS-compat writes, the runtime bindings, and Studio's\n * display logic can never disagree on what \"scalar\" means.\n */\nexport function isScalarVariableValue(value: unknown): value is string | number | boolean {\n return typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\";\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\nfunction isVariableType(t: unknown): t is CompositionVariableType {\n return typeof t === \"string\" && t in DEFAULT_TYPEOF;\n}\n\n/**\n * True when the value is a structurally valid variable declaration: id, label,\n * a known type, a default matching that type, and options[] for enums. The\n * same predicate parseCompositionVariables filters with — exported so writers\n * (SDK declaration ops, Studio forms) can validate before persisting.\n */\nexport function isCompositionVariable(v: unknown): v is CompositionVariable {\n if (!isRecord(v)) return false;\n if (typeof v.id !== \"string\" || typeof v.label !== \"string\") return false;\n if (!isVariableType(v.type)) return false;\n if (typeof v.default !== DEFAULT_TYPEOF[v.type]) return false;\n if (v.type === \"enum\" && !Array.isArray(v.options)) return false;\n return true;\n}\n\n/**\n * Parse the typed variable declarations from an element's\n * `data-composition-variables` attribute. Malformed entries (wrong shape,\n * unknown type, default not matching the declared type) are dropped; an\n * absent attribute, invalid JSON, or a non-array payload yields `[]`.\n */\nexport function parseCompositionVariables(htmlEl: Element): CompositionVariable[] {\n const variablesAttr = htmlEl.getAttribute(\"data-composition-variables\");\n if (!variablesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(variablesAttr);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter(isCompositionVariable);\n } catch {\n return [];\n }\n}\n\n/**\n * Protocol allowlist for a resolved media URL. Relative URLs (no scheme) resolve\n * against the page origin and are always safe. Absolute URLs are restricted to\n * http(s)/blob and image data: URIs -- defense-in-depth alongside the runtime's\n * VAR_SRC_TAGS element guard, blocking `javascript:`, `data:text/html`, `file:`,\n * etc. even if a future tag slips past it. Control chars are stripped before the\n * scheme test because browsers ignore them when parsing the URL.\n *\n * Lives here rather than in the runtime because the linter has to reach the same\n * verdict on a declared default statically: a value this rejects is dropped at\n * bind time and the element's authored fallback renders instead, so a second copy\n * of the scheme list would let lint and the runtime disagree silently.\n */\nexport function isSafeMediaUrl(url: string): boolean {\n // Browsers ignore ASCII control chars/whitespace when parsing a URL, so strip\n // them before reading the scheme (defeats `java\\tscript:` style bypasses).\n // oxlint-disable-next-line no-control-regex -- control chars are the target here\n const normalized = url.replace(/[\\u0000-\\u0020]/g, \"\");\n const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalized);\n if (!scheme) return true;\n const proto = scheme[1]?.toLowerCase();\n if (!proto) return false;\n if (proto === \"https\" || proto === \"http\" || proto === \"blob\") return true;\n if (proto === \"data\") return /^data:image\\//i.test(normalized);\n return false;\n}\n","/**\n * Browser-safe static scan for composition-variable reads in script text.\n *\n * Compositions read variables by calling the runtime API — `getVariables()`\n * bare (sub-comp scoped shadow) or via `__hyperframes.getVariables()` /\n * `window.__hyperframes.getVariables()` — and there is no DOM-attribute\n * binding to scan, so \"which variables does this composition use\" can only be\n * derived from the scripts. This is a best-effort static analysis: the\n * patterns agents actually write (destructuring, member access, a single\n * alias variable) resolve to ids; anything opaque flips `scanIncomplete`\n * so consumers can present usage as a lower bound instead of a fact.\n *\n * AST nodes are handled untyped (same convention as gsapParserAcorn.ts) —\n * acorn's structural types don't survive acorn-walk's visitor signatures.\n */\n\nimport * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\n\nexport interface VariableUsageScan {\n /** Variable ids statically read by the script, in first-seen order. */\n usedIds: string[];\n /**\n * True when the script accesses variables in a way the scan cannot resolve\n * (computed keys, rest spreads, the values object escaping into a call…) or\n * when the script fails to parse — usedIds is then a lower bound.\n */\n scanIncomplete: boolean;\n}\n\ninterface Sink {\n use(id: string): void;\n incomplete(): void;\n}\n\n// oxlint-disable no-explicit-any -- untyped acorn AST traversal, see header\n\nfunction isGetVariablesCallee(callee: any): boolean {\n if (callee?.type === \"Identifier\") return callee.name === \"getVariables\";\n if (callee?.type === \"MemberExpression\" && !callee.computed) {\n return callee.property?.type === \"Identifier\" && callee.property.name === \"getVariables\";\n }\n return false;\n}\n\n/** Collect ids from an ObjectPattern destructuring of the values object. */\n// Exhaustive AST-node classification — branchy by nature, same as gsapParserAcorn.\n// fallow-ignore-next-line complexity\nfunction collectFromObjectPattern(pattern: any, out: Sink): void {\n for (const prop of pattern.properties ?? []) {\n if (prop?.type === \"RestElement\") {\n out.incomplete();\n continue;\n }\n if (prop?.type !== \"Property\") continue;\n if (prop.computed === true) {\n out.incomplete();\n } else if (prop.key?.type === \"Identifier\") {\n out.use(String(prop.key.name));\n } else if (prop.key?.type === \"Literal\" && typeof prop.key.value === \"string\") {\n out.use(prop.key.value);\n } else {\n out.incomplete();\n }\n }\n}\n\n/** Collect an id from a MemberExpression reading the values object. */\nfunction collectFromMemberAccess(member: any, out: Sink): void {\n if (member.computed !== true && member.property?.type === \"Identifier\") {\n out.use(String(member.property.name));\n } else if (\n member.computed === true &&\n member.property?.type === \"Literal\" &&\n typeof member.property.value === \"string\"\n ) {\n out.use(member.property.value);\n } else {\n out.incomplete();\n }\n}\n\n/**\n * Classify one read of the values object (a getVariables() call result or an\n * alias holding it) by its immediate syntactic context. Returns the alias\n * name when the value is bound to a plain variable (`const vars = …`).\n */\n// fallow-ignore-next-line complexity\nfunction classifyValueRead(parent: any, valueNode: any, out: Sink): string | null {\n if (!parent || parent.type === \"ExpressionStatement\") {\n // Bare statement — value unused, nothing read.\n return null;\n }\n if (parent.type === \"MemberExpression\" && parent.object === valueNode) {\n collectFromMemberAccess(parent, out);\n return null;\n }\n if (parent.type === \"VariableDeclarator\" && parent.init === valueNode) {\n if (parent.id?.type === \"ObjectPattern\") {\n collectFromObjectPattern(parent.id, out);\n return null;\n }\n if (parent.id?.type === \"Identifier\") return String(parent.id.name);\n out.incomplete();\n return null;\n }\n // The values object escapes (argument, return, spread, assignment…) —\n // reads beyond this point are invisible to the scan.\n out.incomplete();\n return null;\n}\n\nexport function scanVariableUsage(scriptText: string): VariableUsageScan {\n const usedIds: string[] = [];\n const seen = new Set<string>();\n let scanIncomplete = false;\n\n const sink: Sink = {\n use(id: string) {\n if (!seen.has(id)) {\n seen.add(id);\n usedIds.push(id);\n }\n },\n incomplete() {\n scanIncomplete = true;\n },\n };\n\n let ast: any;\n try {\n ast = acorn.parse(scriptText, { ecmaVersion: \"latest\", sourceType: \"script\" });\n } catch {\n return { usedIds: [], scanIncomplete: true };\n }\n\n const aliases = new Set<string>();\n\n // Pass 1: classify every getVariables() call by its parent context.\n acornWalk.ancestor(ast, {\n CallExpression(node: any, _: unknown, ancestors: any[]) {\n if (!isGetVariablesCallee(node.callee)) return;\n const parent = ancestors[ancestors.length - 2];\n const alias = classifyValueRead(parent, node, sink);\n if (alias) aliases.add(alias);\n },\n } as any);\n\n // Pass 2: classify every reference to an alias of the values object.\n // Scope-naive by design: an unrelated same-named identifier can only make\n // the scan report extra ids or flip scanIncomplete, never miss a read.\n if (aliases.size > 0) {\n acornWalk.ancestor(ast, {\n // fallow-ignore-next-line complexity\n Identifier(node: any, _: unknown, ancestors: any[]) {\n if (!aliases.has(String(node.name))) return;\n const parent = ancestors[ancestors.length - 2];\n if (!parent) return;\n // Skip the declarator that introduced the alias and property-position\n // identifiers that merely share the name.\n if (parent.type === \"VariableDeclarator\" && parent.id === node) return;\n if (parent.type === \"MemberExpression\" && parent.property === node) return;\n if (parent.type === \"Property\" && parent.key === node && parent.computed !== true) return;\n // Chained aliases (const v2 = vars) are not followed — flag instead\n // of silently missing reads through the second name.\n if (classifyValueRead(parent, node, sink)) sink.incomplete();\n },\n } as any);\n }\n\n return { usedIds, scanIncomplete };\n}\n","/**\n * Browser-safe authored composition contract.\n *\n * Source HTML is authored with data-start + data-duration + data-track-index.\n * data-end is compiler-derived and data-layer is legacy input only. Readers\n * accept legacy documents; writers always emit the canonical representation.\n */\n\nexport const COMPOSITION_CONTRACT_VERSION = 1 as const;\n\nexport const COMPOSITION_ATTRIBUTES = Object.freeze({\n start: \"data-start\",\n duration: \"data-duration\",\n trackIndex: \"data-track-index\",\n derivedEnd: \"data-end\",\n legacyTrack: \"data-layer\",\n} as const);\n\nexport const CANONICAL_AUTHORED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.start,\n COMPOSITION_ATTRIBUTES.duration,\n COMPOSITION_ATTRIBUTES.trackIndex,\n] as const);\n\nexport const DERIVED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n] as const);\n\nexport const LEGACY_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n] as const);\n\nexport type ReferenceExpression =\n | { kind: \"absolute\"; value: number }\n | { kind: \"reference\"; refId: string; offset: number };\n\nexport type ClipTimingDiagnosticCode =\n | \"invalid-start\"\n | \"unresolved-start-reference\"\n | \"invalid-duration\"\n | \"invalid-end\"\n | \"end-before-start\"\n | \"deprecated-end\"\n | \"conflicting-end\"\n | \"invalid-track-index\"\n | \"deprecated-layer\"\n | \"conflicting-layer\";\n\nexport interface ClipTimingDiagnostic {\n code: ClipTimingDiagnosticCode;\n attribute: string;\n value: string | null;\n}\n\nexport interface ClipAttributeReader {\n getAttribute(name: string): string | null;\n}\n\nexport interface ClipAttributeWriter extends ClipAttributeReader {\n setAttribute(name: string, value: string): void;\n removeAttribute(name: string): void;\n}\n\nexport interface ReadClipTimingOptions {\n /** Resolve a reference to the referenced clip's absolute end time. */\n resolveReferenceEnd?: (refId: string) => number | null | undefined;\n /** Start used when data-start is absent. Defaults to zero. */\n defaultStart?: number | null;\n}\n\nexport interface ClipTiming {\n startExpression: ReferenceExpression | null;\n start: number | null;\n duration: number | null;\n end: number | null;\n trackIndex: number;\n durationSource: \"duration\" | \"legacy-end\" | \"missing\" | \"invalid\";\n trackSource: \"track-index\" | \"legacy-layer\" | \"default\" | \"invalid\";\n diagnostics: ClipTimingDiagnostic[];\n}\n\nexport interface ClipTimingUpdate {\n start?: number | string | ReferenceExpression;\n duration?: number | null;\n trackIndex?: number | null;\n}\n\nexport class ClipTimingWriteError extends Error {\n readonly code: \"invalid-start\" | \"invalid-duration\" | \"invalid-track-index\";\n\n constructor(code: ClipTimingWriteError[\"code\"], message: string) {\n super(message);\n this.name = \"ClipTimingWriteError\";\n this.code = code;\n }\n}\n\n/** Parse a value to a finite number, or null if it is absent/invalid. */\nexport function parseNumeric(value: string | null | undefined): number | null {\n if (value == null || value.trim() === \"\") return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nconst REFERENCE_ID_PATTERN = /^[A-Za-z0-9_.:-]+$/;\n\nfunction isAsciiDigitAt(value: string, index: number): boolean {\n const code = value.charCodeAt(index);\n return code >= 48 && code <= 57;\n}\n\nfunction skipDigitsLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && isAsciiDigitAt(value, cursor)) cursor--;\n return cursor;\n}\n\nfunction skipWhitespaceLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && (value[cursor] ?? \"\").trim() === \"\") cursor--;\n return cursor;\n}\n\nfunction findMagnitudeStart(value: string): number | null {\n const last = value.length - 1;\n if (!isAsciiDigitAt(value, last)) return null;\n let cursor = skipDigitsLeft(value, last);\n if (value[cursor] === \".\") cursor = skipDigitsLeft(value, cursor - 1);\n return cursor + 1;\n}\n\nfunction parseReferenceOffset(\n value: string,\n): { refId: string; operator: \"+\" | \"-\"; magnitude: number } | null {\n const magnitudeStart = findMagnitudeStart(value);\n if (magnitudeStart == null) return null;\n const operatorIndex = skipWhitespaceLeft(value, magnitudeStart - 1);\n const operator = value[operatorIndex];\n if (operator !== \"+\" && operator !== \"-\") return null;\n\n const refId = value.slice(0, operatorIndex).trim();\n if (!REFERENCE_ID_PATTERN.test(refId)) return null;\n const magnitude = Number(value.slice(magnitudeStart));\n if (!Number.isFinite(magnitude)) return null;\n return { refId, operator, magnitude };\n}\n\n/**\n * Parse the data-start grammar: absolute seconds, `clip-id`, or\n * `clip-id +/- offset`, where references resolve to the referenced clip's end.\n */\nexport function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {\n const normalized = (raw ?? \"\").trim();\n if (!normalized) return null;\n const absolute = parseNumeric(normalized);\n if (absolute != null) return { kind: \"absolute\", value: absolute };\n if (REFERENCE_ID_PATTERN.test(normalized)) {\n return { kind: \"reference\", refId: normalized, offset: 0 };\n }\n const reference = parseReferenceOffset(normalized);\n if (!reference) return null;\n return {\n kind: \"reference\",\n refId: reference.refId,\n offset: reference.operator === \"-\" ? -reference.magnitude : reference.magnitude,\n };\n}\n\nfunction pushDiagnostic(\n diagnostics: ClipTimingDiagnostic[],\n code: ClipTimingDiagnosticCode,\n attribute: string,\n value: string | null,\n): void {\n diagnostics.push({ code, attribute, value });\n}\n\nfunction resolveStart(\n expression: ReferenceExpression | null,\n rawStart: string | null,\n options: ReadClipTimingOptions,\n diagnostics: ClipTimingDiagnostic[],\n): number | null {\n if (rawStart == null || rawStart.trim() === \"\") {\n return options.defaultStart === undefined ? 0 : options.defaultStart;\n }\n if (!expression) {\n pushDiagnostic(diagnostics, \"invalid-start\", COMPOSITION_ATTRIBUTES.start, rawStart);\n return null;\n }\n if (expression.kind === \"absolute\") return Math.max(0, expression.value);\n const referencedEnd = options.resolveReferenceEnd?.(expression.refId);\n if (referencedEnd == null || !Number.isFinite(referencedEnd)) {\n pushDiagnostic(\n diagnostics,\n \"unresolved-start-reference\",\n COMPOSITION_ATTRIBUTES.start,\n rawStart,\n );\n return null;\n }\n return Math.max(0, referencedEnd + expression.offset);\n}\n\ntype DurationRead = Pick<ClipTiming, \"duration\" | \"end\" | \"durationSource\">;\ntype TrackRead = Pick<ClipTiming, \"trackIndex\" | \"trackSource\">;\n\n// Float pairs like `data-start=\"0.1\" + data-duration=\"0.2\"` add to\n// `0.30000000000000004`, so the compiler-emitted `data-end` string can be a few\n// ulps off the sum a reader computes from the canonical inputs. Any drift below\n// this ceiling is treated as equal for the derived-end reconciliation check —\n// well below one 60fps frame (~16.67 ms) and orders of magnitude above the\n// worst realistic IEEE-754 residual (~2e-16 s).\nconst DERIVED_END_EQUALITY_EPSILON_SECONDS = 1e-9;\n\nfunction derivedEndsAreConsistent(parsedEnd: number, canonicalEnd: number): boolean {\n return Math.abs(parsedEnd - canonicalEnd) <= DERIVED_END_EQUALITY_EPSILON_SECONDS;\n}\n\n// Reconcile a `data-end` attribute paired with canonical `data-duration`.\n//\n// The compiler (`compileTimingAttrs` in @hyperframes/core) writes\n// `data-end = data-start + data-duration` into the bundled HTML so the runtime\n// can key off a single attribute. That derived attribute is legitimate — it is\n// not a legacy authoring shape. Treating it as `deprecated-end` here made the\n// linter fire `deprecated_data_end` on the compiler's own output whenever\n// StaticGuard re-validated a bundled composition, producing a false-positive\n// cluster reported in the field (n=25+ across cli-feedback crons 61-68).\n//\n// We keep `deprecated-end` for the truly legacy shape (see `readLegacyEnd` —\n// `data-end` present without `data-duration`) and for the *conflicting* case\n// where an author left a stale `data-end` behind a canonical duration edit; a\n// stale end must not be silently overridden without a diagnostic.\nfunction diagnoseDerivedEnd(\n rawEnd: string,\n canonicalEnd: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): void {\n const parsedEnd = parseNumeric(rawEnd);\n if (parsedEnd == null) {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (canonicalEnd == null) {\n // Canonical duration exists but resolved end is unknown (e.g. unresolved\n // reference start). Preserve prior behavior — flag as deprecated so authors\n // remove the stale companion attribute.\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (derivedEndsAreConsistent(parsedEnd, canonicalEnd)) {\n // Compiler-derived (or manually consistent) `data-end` alongside\n // `data-duration` — silent. No diagnostic.\n return;\n }\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"conflicting-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n}\n\nfunction readCanonicalDuration(\n rawDuration: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const duration = parseNumeric(rawDuration);\n if (duration == null || duration < 0) {\n pushDiagnostic(diagnostics, \"invalid-duration\", COMPOSITION_ATTRIBUTES.duration, rawDuration);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return {\n duration,\n end: start == null ? null : start + duration,\n durationSource: \"duration\",\n };\n}\n\nfunction readLegacyEnd(\n rawEnd: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n const end = parseNumeric(rawEnd);\n if (end == null) {\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n if (start == null) return { duration: null, end, durationSource: \"legacy-end\" };\n if (end < start) {\n pushDiagnostic(diagnostics, \"end-before-start\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return { duration: end - start, end, durationSource: \"legacy-end\" };\n}\n\nfunction readDuration(\n attributes: ClipAttributeReader,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const rawDuration = attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration);\n const rawEnd = attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n if (rawDuration == null) {\n return rawEnd == null\n ? { duration: null, end: null, durationSource: \"missing\" }\n : readLegacyEnd(rawEnd, start, diagnostics);\n }\n const canonical = readCanonicalDuration(rawDuration, start, diagnostics);\n if (rawEnd != null) diagnoseDerivedEnd(rawEnd, canonical.end, diagnostics);\n return canonical;\n}\n\nfunction readTrackValue(\n rawValue: string,\n attribute: string,\n source: \"track-index\" | \"legacy-layer\",\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const trackIndex = parseNumeric(rawValue);\n if (trackIndex == null || !Number.isInteger(trackIndex)) {\n pushDiagnostic(diagnostics, \"invalid-track-index\", attribute, rawValue);\n return { trackIndex: 0, trackSource: \"invalid\" };\n }\n return { trackIndex, trackSource: source };\n}\n\nfunction readTrack(\n attributes: ClipAttributeReader,\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const rawTrack = attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n const rawLayer = attributes.getAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n if (rawTrack == null && rawLayer == null) return { trackIndex: 0, trackSource: \"default\" };\n if (rawTrack == null && rawLayer != null) {\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n return readTrackValue(\n rawLayer,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n \"legacy-layer\",\n diagnostics,\n );\n }\n const canonical = readTrackValue(\n rawTrack ?? \"\",\n COMPOSITION_ATTRIBUTES.trackIndex,\n \"track-index\",\n diagnostics,\n );\n if (rawLayer == null) return canonical;\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n const parsedLayer = parseNumeric(rawLayer);\n if (parsedLayer != null && parsedLayer !== canonical.trackIndex) {\n pushDiagnostic(diagnostics, \"conflicting-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n }\n return canonical;\n}\n\n/** Read canonical timing, accepting legacy attributes without preferring them. */\nexport function readClipTiming(\n attributes: ClipAttributeReader,\n options: ReadClipTimingOptions = {},\n): ClipTiming {\n const diagnostics: ClipTimingDiagnostic[] = [];\n const rawStart = attributes.getAttribute(COMPOSITION_ATTRIBUTES.start);\n const startExpression = parseStartExpression(rawStart);\n const start = resolveStart(startExpression, rawStart, options, diagnostics);\n const duration = readDuration(attributes, start, diagnostics);\n const track = readTrack(attributes, diagnostics);\n return { startExpression, start, ...duration, ...track, diagnostics };\n}\n\nfunction serializeStartNumber(start: number): string {\n if (!Number.isFinite(start)) {\n throw new ClipTimingWriteError(\"invalid-start\", \"start must be finite\");\n }\n return String(start);\n}\n\nfunction serializeStartReference(start: ReferenceExpression): string {\n if (start.kind === \"absolute\") return serializeStartNumber(start.value);\n if (!start.refId || !Number.isFinite(start.offset)) {\n throw new ClipTimingWriteError(\n \"invalid-start\",\n \"reference start must have an id and finite offset\",\n );\n }\n if (start.offset === 0) return start.refId;\n return `${start.refId} ${start.offset < 0 ? \"-\" : \"+\"} ${Math.abs(start.offset)}`;\n}\n\nfunction serializeStart(start: ClipTimingUpdate[\"start\"]): string {\n if (typeof start === \"number\") return serializeStartNumber(start);\n if (typeof start === \"object\" && start != null) return serializeStartReference(start);\n if (typeof start === \"string\" && parseStartExpression(start)) return start.trim();\n throw new ClipTimingWriteError(\"invalid-start\", `invalid start expression: ${start ?? \"\"}`);\n}\n\nfunction writeDuration(\n attributes: ClipAttributeWriter,\n duration: number | null | undefined,\n current: ClipTiming,\n): void {\n if (duration === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.duration);\n return;\n }\n if (duration === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n current.duration != null\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(current.duration));\n }\n return;\n }\n if (!Number.isFinite(duration) || duration < 0) {\n throw new ClipTimingWriteError(\n \"invalid-duration\",\n \"duration must be a finite, non-negative number\",\n );\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(duration));\n}\n\nfunction writeTrack(\n attributes: ClipAttributeWriter,\n trackIndex: number | null | undefined,\n current: ClipTiming,\n): void {\n if (trackIndex === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n return;\n }\n if (trackIndex === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex) == null &&\n current.trackSource === \"legacy-layer\"\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(current.trackIndex));\n }\n return;\n }\n if (!Number.isFinite(trackIndex) || !Number.isInteger(trackIndex)) {\n throw new ClipTimingWriteError(\"invalid-track-index\", \"trackIndex must be a finite integer\");\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(trackIndex));\n}\n\n/** Write only canonical authored timing and remove derived/legacy source attributes. */\nexport function writeClipTiming(\n attributes: ClipAttributeWriter,\n update: ClipTimingUpdate,\n): ClipTiming {\n const current = readClipTiming(attributes);\n if (update.start !== undefined) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.start, serializeStart(update.start));\n }\n writeDuration(attributes, update.duration, current);\n writeTrack(attributes, update.trackIndex, current);\n // A legacy end paired with an unresolved reference start cannot be converted\n // to data-duration without a reference resolver. On a duration-omitting edit\n // (for example, moving only the track), preserve that sole duration source\n // instead of canonicalizing it into data loss.\n const preserveUnresolvedLegacyEnd =\n update.duration === undefined &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd) != null &&\n current.duration == null;\n if (!preserveUnresolvedLegacyEnd) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n }\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n return readClipTiming(attributes);\n}\n","/**\n * Where a scaffolded project's canvas resolution lives besides the root's own\n * data-width/data-height: the html/body CSS size and the meta viewport content.\n */\n\n/** html/body CSS, width first. Groups 1/3 are the surrounding text, 2/4 the digits. */\nexport const HTML_BODY_CSS_WIDTH_FIRST_RE =\n /(html\\s*,\\s*body\\s*\\{[^}]*?width:\\s*)(\\d+)px([^}]*?height:\\s*)(\\d+)px/i;\n\n/** Same block, height authored first. */\nexport const HTML_BODY_CSS_HEIGHT_FIRST_RE =\n /(html\\s*,\\s*body\\s*\\{[^}]*?height:\\s*)(\\d+)px([^}]*?width:\\s*)(\\d+)px/i;\n\n/** Viewport meta content. Shared by lint (reads groups 2/4) and cli (replaces via 1/3). */\nexport const VIEWPORT_META_SIZE_RE =\n /(<meta[^>]*name=[\"']viewport[\"'][^>]*content=[\"']width=)(\\d+)(,\\s*height=)(\\d+)/i;\n","import * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\n\nexport interface MediaSrcMutation {\n selector: string;\n operation: \"src_assignment\" | \"set_attribute\";\n raw: string;\n}\n\nfunction parseProgram(script: string): any {\n try {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"script\" });\n } catch {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"module\" });\n }\n}\n\nfunction literalString(node: any): string | undefined {\n if (node?.type === \"Literal\" && typeof node.value === \"string\") return node.value;\n if (node?.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked;\n }\n return undefined;\n}\n\nfunction memberName(node: any): string | undefined {\n if (node?.type !== \"MemberExpression\") return undefined;\n if (!node.computed && node.property?.type === \"Identifier\") return node.property.name;\n return literalString(node.property);\n}\n\nfunction lookupSelector(node: any, bindings: ReadonlyMap<string, string>): string | undefined {\n if (node?.type === \"Identifier\") return bindings.get(node.name);\n if (node?.type !== \"CallExpression\" || node.callee?.type !== \"MemberExpression\") {\n return undefined;\n }\n const method = memberName(node.callee);\n if (method !== \"getElementById\" && method !== \"querySelector\") return undefined;\n const value = literalString(node.arguments?.[0]);\n if (!value) return undefined;\n return method === \"getElementById\" ? `#${value}` : value;\n}\n\n/** Find literal source writes whose target is a statically resolvable DOM lookup. */\nexport function extractMediaSrcMutations(script: string): MediaSrcMutation[] {\n try {\n const ast = parseProgram(script);\n const bindings = new Map<string, string>();\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n if (node.id?.type !== \"Identifier\") return;\n const selector = lookupSelector(node.init, bindings);\n if (selector) bindings.set(node.id.name, selector);\n },\n });\n\n const mutations: MediaSrcMutation[] = [];\n acornWalk.simple(ast, {\n AssignmentExpression(node: any) {\n if (node.operator !== \"=\" || memberName(node.left) !== \"src\") return;\n const selector = lookupSelector(node.left.object, bindings);\n if (!selector) return;\n mutations.push({\n selector,\n operation: \"src_assignment\",\n raw: script.slice(node.start, node.end),\n });\n },\n CallExpression(node: any) {\n if (memberName(node.callee) !== \"setAttribute\") return;\n if (literalString(node.arguments?.[0])?.toLowerCase() !== \"src\") return;\n const selector = lookupSelector(node.callee.object, bindings);\n if (!selector) return;\n mutations.push({\n selector,\n operation: \"set_attribute\",\n raw: script.slice(node.start, node.end),\n });\n },\n });\n return mutations;\n } catch {\n return [];\n }\n}\n"],"mappings":"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","names":["acorn","acornWalk"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/fontAliases.ts","../src/utils/urlPath.ts","../src/compositionVariables.ts","../src/variableUsage.ts","../src/compositionContract.ts","../src/canvasScaffoldPatterns.ts","../src/mediaSrcMutation.ts"],"sourcesContent":["// ── Composition data types ───────────────────────────────────────────────────\n// Moved from @hyperframes/core/core.types in the parsers extraction refactor.\n// These are the types produced and consumed by the parser pipeline.\n\nexport interface Asset {\n id: string;\n url: string;\n type: string;\n is_reference?: boolean;\n /** Duration in seconds for video/audio assets */\n duration?: number;\n}\n\n// ── Timeline types ──────────────────────────────────────────────────────────\n\nexport type TimelineElementType = \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\nexport type MediaElementType = \"video\" | \"image\" | \"audio\";\n\nexport const CANVAS_DIMENSIONS = {\n landscape: { width: 1920, height: 1080 },\n portrait: { width: 1080, height: 1920 },\n \"landscape-4k\": { width: 3840, height: 2160 },\n \"portrait-4k\": { width: 2160, height: 3840 },\n square: { width: 1080, height: 1080 },\n \"square-4k\": { width: 2160, height: 2160 },\n} as const;\n\n// Single source of truth: derive the type from the table so adding a preset\n// extends the union automatically. Avoids the prior `as readonly CanvasResolution[]`\n// cast on `VALID_CANVAS_RESOLUTIONS` quietly drifting if the table grew but\n// the union didn't.\nexport type CanvasResolution = keyof typeof CANVAS_DIMENSIONS;\n\n// `Object.keys` ordering matches insertion order in `CANVAS_DIMENSIONS` on\n// every supported JS engine; tests pin the order in `index.test.ts`. Reorder\n// the table above with care.\nexport const VALID_CANVAS_RESOLUTIONS = Object.keys(\n CANVAS_DIMENSIONS,\n) as readonly CanvasResolution[];\n\nconst RESOLUTION_ALIASES: Record<string, CanvasResolution> = {\n \"1080p\": \"landscape\",\n hd: \"landscape\",\n \"1080p-portrait\": \"portrait\",\n \"portrait-1080p\": \"portrait\",\n \"4k\": \"landscape-4k\",\n uhd: \"landscape-4k\",\n \"4k-portrait\": \"portrait-4k\",\n \"1080p-square\": \"square\",\n \"square-1080p\": \"square\",\n \"4k-square\": \"square-4k\",\n};\n\n/**\n * Aliases that name a resolution *tier* (1080p / 4K) without also nailing an\n * orientation. Historically they all normalize to the `landscape` preset\n * (`normalizeResolutionFlag(\"1080p\") === \"landscape\"`), which then rejects\n * portrait / square compositions with a cryptic \"aspect ratio does not match\"\n * error deep inside the render pipeline. Consumers that know the composition's\n * dimensions can consult this set to decide whether to auto-adapt the preset's\n * orientation instead — see `adaptAspectAgnosticResolution` in the compile\n * stage (`@hyperframes/producer`) and `suggestMatchingPreset` here.\n *\n * Orientation-suffixed aliases (`1080p-portrait`, `4k-square`, …) are absent\n * on purpose: the user *did* pick an orientation, and honoring it is important\n * for the \"you asked for landscape but composed portrait\" error to still fire.\n */\nconst ASPECT_AGNOSTIC_RESOLUTION_ALIASES: ReadonlySet<string> = new Set([\n \"1080p\",\n \"hd\",\n \"4k\",\n \"uhd\",\n]);\n\n/**\n * Map a user-facing resolution string (canonical name or alias) to a\n * `CanvasResolution`. Returns undefined for unknown values so callers\n * can produce their own \"invalid\" UX (CLI exit, route validation, etc.).\n */\nexport function normalizeResolutionFlag(input: string | undefined): CanvasResolution | undefined {\n if (!input) return undefined;\n const lowered = input.toLowerCase();\n if ((VALID_CANVAS_RESOLUTIONS as readonly string[]).includes(lowered)) {\n return lowered as CanvasResolution;\n }\n return RESOLUTION_ALIASES[lowered];\n}\n\n/**\n * True when `input` names a resolution *tier* without nailing an orientation\n * (`1080p`, `hd`, `4k`, `uhd`). Case-insensitive.\n *\n * The `--resolution` CLI flag treats these as \"target this size; keep the\n * composition's orientation\" — a portrait 1080×1920 comp with `--resolution\n * 1080p` should land at 1080×1920, not blow up on aspect mismatch. Explicit\n * canonical presets (`landscape`, `portrait`, …) and orientation-suffixed\n * aliases (`1080p-portrait`) stay strict — the user picked an orientation.\n *\n * Consumers pair this signal with the composition's dimensions (in a\n * follow-up pass after HTML parse) to pick the right preset via\n * `suggestMatchingPreset` — see `adaptAspectAgnosticResolution` in the\n * compile stage (`@hyperframes/producer`) for the canonical remap.\n */\nexport function isAspectAgnosticResolutionAlias(input: string | undefined): boolean {\n if (!input) return false;\n return ASPECT_AGNOSTIC_RESOLUTION_ALIASES.has(input.toLowerCase());\n}\n\n/**\n * Public-boundary helper: given a raw `--resolution` / `--output-resolution`\n * flag value, return the pair every distributed render entrypoint needs to\n * forward end-to-end so the compile stage can adapt aspect-agnostic aliases\n * to the composition's orientation:\n *\n * - `outputResolution`: normalized {@link CanvasResolution} (or `undefined`\n * for unknown values — callers own their invalid-input UX).\n * - `outputResolutionAspectAgnostic`: `true` when the raw input was a\n * tier-only alias (`1080p` / `hd` / `4k` / `uhd`). Passes through to\n * `DistributedRenderConfig.outputResolutionAspectAgnostic` so the compile\n * stage remaps `landscape` → `portrait` / `square` when the composition\n * dimensions demand it.\n *\n * Exported to centralize the two-step pattern (`normalizeResolutionFlag` +\n * `isAspectAgnosticResolutionAlias`) that would otherwise be duplicated at\n * every entrypoint that emits a `DistributedRenderConfig` (`hyperframes\n * cloudrun render`, `hyperframes lambda render` / `render-batch`, the local\n * CLI). Divergence between those callers is what shipped the portrait-1080p\n * regression this helper prevents from recurring.\n */\nexport interface ResolvedResolutionFlag {\n outputResolution: CanvasResolution | undefined;\n outputResolutionAspectAgnostic: boolean;\n}\n\nexport function resolveResolutionFlagPair(input: string | undefined): ResolvedResolutionFlag {\n return {\n outputResolution: normalizeResolutionFlag(input),\n outputResolutionAspectAgnostic: isAspectAgnosticResolutionAlias(input),\n };\n}\n\nexport interface TimelineElementBase {\n id: string;\n type: TimelineElementType;\n name: string;\n startTime: number;\n duration: number;\n zIndex: number;\n x?: number;\n y?: number;\n scale?: number;\n opacity?: number;\n}\n\nexport interface TimelineMediaElement extends TimelineElementBase {\n type: MediaElementType;\n src: string;\n mediaStartTime?: number;\n sourceDuration?: number;\n isAroll?: boolean;\n sourceWidth?: number;\n sourceHeight?: number;\n volume?: number; // linear gain; 0 is silent, 1 is 0 dB, values above 1 boost\n hasAudio?: boolean; // For videos - indicates if video has audio track\n}\n\nexport interface WaveformData {\n peaks: number[];\n duration: number;\n sampleRate?: number;\n}\n\nexport interface TimelineTextElement extends TimelineElementBase {\n type: \"text\";\n content: string;\n color?: string;\n fontSize?: number;\n textShadow?: boolean;\n fontFamily?: string;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n}\n\nexport interface TimelineCompositionElement extends TimelineElementBase {\n type: \"composition\";\n src: string;\n compositionId: string;\n scale?: number;\n sourceDuration?: number;\n variableValues?: Record<string, string | number | boolean>;\n sourceWidth?: number;\n sourceHeight?: number;\n}\n\n// Composition Variable Types\nexport type CompositionVariableType =\n | \"string\"\n | \"number\"\n | \"color\"\n | \"boolean\"\n | \"enum\"\n | \"font\"\n | \"image\";\n\n/**\n * Runtime list of every valid `CompositionVariableType`. Use this anywhere\n * a Set/array of valid type strings is needed (lint rules, validators).\n * The `satisfies` guard turns adding a new variant to the union without\n * also adding it here into a compile error.\n */\nexport const COMPOSITION_VARIABLE_TYPES = [\n \"string\",\n \"number\",\n \"color\",\n \"boolean\",\n \"enum\",\n \"font\",\n \"image\",\n] as const satisfies readonly CompositionVariableType[];\n\nexport interface CompositionVariableBase {\n id: string;\n type: CompositionVariableType;\n label: string;\n description?: string;\n}\n\nexport interface StringVariable extends CompositionVariableBase {\n type: \"string\";\n default: string;\n placeholder?: string;\n maxLength?: number;\n}\n\nexport interface NumberVariable extends CompositionVariableBase {\n type: \"number\";\n default: number;\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n}\n\nexport interface ColorVariable extends CompositionVariableBase {\n type: \"color\";\n default: string;\n /** Brand role identifier, e.g. \"color:primary\". */\n brandRole?: string;\n}\n\nexport interface BooleanVariable extends CompositionVariableBase {\n type: \"boolean\";\n default: boolean;\n}\n\nexport interface EnumVariable extends CompositionVariableBase {\n type: \"enum\";\n default: string;\n options: { value: string; label: string }[];\n}\n\n/**\n * Font variable — value is a `{name, source}` object (object-valued; LOCKED §7).\n * `default` is the fallback font-family name string.\n * `source` is the font stylesheet URL (e.g. Google Fonts CSS).\n * `default_name` / `default_source` are the CSS-level fallbacks when the\n * brand font is absent.\n */\nexport interface FontVariable extends CompositionVariableBase {\n type: \"font\";\n /** Fallback font-family name, e.g. \"Inter\". */\n default: string;\n /** Font stylesheet URL (e.g. Google Fonts CSS link). */\n source?: string;\n /** CSS font-family name to use when source is unavailable, e.g. \"sans-serif\". */\n default_name?: string;\n /** Fallback font stylesheet URL (empty string = system font). */\n default_source?: string;\n}\n\n/**\n * Image variable — value is a `{url, …}` object (object-valued; LOCKED §7).\n * `default` is the fallback image URL string.\n * `brandRole` is an optional semantic label, e.g. \"logo:primary\".\n */\nexport interface ImageVariable extends CompositionVariableBase {\n type: \"image\";\n /** Fallback image URL. */\n default: string;\n /** Brand role identifier, e.g. \"logo:primary\". */\n brandRole?: string;\n}\n\nexport type CompositionVariable =\n | StringVariable\n | NumberVariable\n | ColorVariable\n | BooleanVariable\n | EnumVariable\n | FontVariable\n | ImageVariable;\n\nexport interface CompositionSpec {\n id: string;\n duration: number;\n variables: CompositionVariable[];\n}\n\nexport type TimelineElement =\n | TimelineMediaElement\n | TimelineTextElement\n | TimelineCompositionElement;\n\nexport function isTextElement(el: TimelineElement): el is TimelineTextElement {\n return el.type === \"text\";\n}\n\nexport function isMediaElement(el: TimelineElement): el is TimelineMediaElement {\n return el.type === \"video\" || el.type === \"image\" || el.type === \"audio\";\n}\n\nexport function isCompositionElement(el: TimelineElement): el is TimelineCompositionElement {\n return el.type === \"composition\";\n}\n\nexport interface MediaFile {\n id: string;\n name: string;\n type: TimelineElementType;\n src: string;\n file?: File;\n duration?: number;\n compositionId?: string;\n sourceWidth?: number; // Intrinsic width for compositions\n sourceHeight?: number; // Intrinsic height for compositions\n}\n\nexport const TIMELINE_COLORS: Record<TimelineElementType, string> = {\n video: \"#ec4899\",\n image: \"#3b82f6\",\n text: \"#06b6d4\",\n audio: \"#10b981\",\n composition: \"#f97316\",\n};\n\nexport const DEFAULT_DURATIONS: Record<TimelineElementType, number> = {\n video: 5,\n image: 5,\n text: 2,\n audio: 5,\n composition: 5,\n};\n\nexport interface CompositionAPI {\n id: string;\n duration: number;\n seek(time: number): void;\n getTime(): number;\n getDuration(): number;\n}\n\n// ── Player API types (used by runtime) ────────────────────────────────────\n\nexport interface PlayerAPI {\n play(): void;\n pause(): void;\n seek(time: number, options?: { keepPlaying?: boolean }): void;\n getTime(): number;\n getDuration(): number;\n isPlaying(): boolean;\n getMainTimeline(): unknown;\n getElementBounds(elementId: string): void;\n getElementsAtPoint(x: number, y: number): void;\n setElementPosition(elementId: string, x: number, y: number): void;\n previewElementPosition(elementId: string, x: number, y: number): void;\n setElementKeyframes(\n elementId: string,\n keyframes: Array<{\n id: string;\n time: number;\n properties: { x?: number; y?: number };\n }> | null,\n ): void;\n setElementScale(elementId: string, scale: number): void;\n setElementFontSize(elementId: string, fontSize: number): void;\n setElementTextContent(elementId: string, content: string): void;\n setElementTextColor(elementId: string, color: string): void;\n setElementTextShadow(elementId: string, enabled: boolean): void;\n setElementTextFontWeight(elementId: string, weight: number): void;\n setElementTextFontFamily(elementId: string, fontFamily: string): void;\n setElementTextOutline(elementId: string, enabled: boolean, color?: string, width?: number): void;\n setElementTextHighlight(\n elementId: string,\n enabled: boolean,\n color?: string,\n padding?: number,\n radius?: number,\n ): void;\n setElementVolume(elementId: string, volume: number): void;\n setStageZoom(scale: number, focusX: number, focusY: number): void;\n getStageZoom(): { scale: number; focusX: number; focusY: number };\n setStageZoomKeyframes(\n keyframes: Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }> | null,\n ): void;\n getStageZoomKeyframes(): Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }>;\n addElement(data: AddElementData): boolean;\n removeElement(elementId: string): boolean;\n updateElementTiming(elementId: string, start?: number, end?: number): boolean;\n setElementTiming(\n elementId: string,\n startTime: number,\n duration: number,\n mediaStartTime?: number,\n ): void;\n updateElementSrc(elementId: string, src: string): boolean;\n updateElementLayer(elementId: string, zIndex: number): boolean;\n updateElementBasePosition(elementId: string, x?: number, y?: number, scale?: number): boolean;\n markTimelineDirty(): void;\n isTimelineDirty(): boolean;\n rebuildTimeline(): void;\n ensureTimeline(): void;\n enableRenderMode(): void;\n disableRenderMode(): void;\n renderSeek(\n time: number,\n options?: { suppressEvents?: boolean; subFrameDivisions?: number; exact?: boolean },\n ): void;\n getElementVisibility(elementId: string): { visible: boolean; opacity?: number };\n getVisibleElements(): Array<{ id: string; tagName: string; start: number; end: number }>;\n getRenderState(): {\n time: number;\n duration: number;\n isPlaying: boolean;\n renderMode: boolean;\n timelineDirty: boolean;\n };\n}\n\nexport interface AddElementData {\n id: string;\n type: \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\n name?: string;\n src?: string;\n content?: string;\n start: number;\n end: number;\n zIndex?: number;\n x?: number;\n y?: number;\n scale?: number;\n fontSize?: number;\n color?: string;\n textShadow?: boolean;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n compositionId?: string;\n sourceWidth?: number;\n sourceHeight?: number;\n isAroll?: boolean;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n}\n\nexport interface CompositionAsset {\n id: string;\n name: string;\n type: \"composition\";\n src: string;\n duration: number;\n compositionId: string;\n thumbnail?: string;\n}\n\nexport interface Keyframe {\n id: string;\n time: number;\n properties: Partial<KeyframeProperties>;\n ease?: string;\n}\n\nexport interface KeyframeProperties {\n x: number;\n y: number;\n opacity: number;\n scale: number;\n scaleX: number;\n scaleY: number;\n rotation: number;\n width: number;\n height: number;\n}\n\nexport interface ElementKeyframes {\n elementId: string;\n keyframes: Keyframe[];\n}\n\nexport interface StageZoom {\n scale: number;\n focusX: number;\n focusY: number;\n}\n\nexport interface StageZoomKeyframe {\n id: string;\n time: number;\n zoom: StageZoom;\n ease?: string;\n}\n\nexport function getDefaultStageZoom(resolution: CanvasResolution): StageZoom {\n const { width, height } = CANVAS_DIMENSIONS[resolution];\n return {\n scale: 1,\n focusX: width / 2,\n focusY: height / 2,\n };\n}\n","/**\n * Single source of truth for the deterministic font alias map. Both the\n * producer's @font-face injector and the core lint rules import from here,\n * eliminating manual drift between the two.\n *\n * Keys are lowercase font family names. Values are canonical font slugs\n * matching CANONICAL_FONTS keys in core's fonts/deterministicFonts module.\n */\nexport const FONT_ALIAS_MAP = {\n // ── Canonical bundled fonts (self-referencing) ────────────────────────\n inter: \"inter\",\n montserrat: \"montserrat\",\n outfit: \"outfit\",\n nunito: \"nunito\",\n oswald: \"oswald\",\n \"league gothic\": \"league-gothic\",\n \"archivo black\": \"archivo-black\",\n \"space mono\": \"space-mono\",\n \"ibm plex mono\": \"ibm-plex-mono\",\n \"jetbrains mono\": \"jetbrains-mono\",\n \"eb garamond\": \"eb-garamond\",\n \"playfair display\": \"playfair-display\",\n \"source code pro\": \"source-code-pro\",\n \"noto sans jp\": \"noto-sans-jp\",\n roboto: \"roboto\",\n \"open sans\": \"open-sans\",\n lato: \"lato\",\n poppins: \"poppins\",\n\n // ── Common aliases → nearest canonical ────────────────────────────────\n \"helvetica neue\": \"inter\",\n helvetica: \"inter\",\n arial: \"inter\",\n \"helvetica bold\": \"inter\",\n futura: \"montserrat\",\n \"din alternate\": \"montserrat\",\n \"arial black\": \"montserrat\",\n \"bebas neue\": \"league-gothic\",\n \"courier new\": \"jetbrains-mono\",\n courier: \"jetbrains-mono\",\n garamond: \"eb-garamond\",\n \"noto sans japanese\": \"noto-sans-jp\",\n \"segoe ui\": \"roboto\",\n\n // ── macOS sans-serif system fonts → inter ─────────────────────────────\n \"sf pro\": \"inter\",\n \"sf pro display\": \"inter\",\n \"sf pro text\": \"inter\",\n \"sf pro rounded\": \"inter\",\n avenir: \"inter\",\n \"avenir next\": \"inter\",\n \"lucida grande\": \"inter\",\n geneva: \"inter\",\n optima: \"inter\",\n\n // ── Windows sans-serif system fonts → inter ───────────────────────────\n verdana: \"inter\",\n tahoma: \"inter\",\n \"trebuchet ms\": \"inter\",\n calibri: \"inter\",\n candara: \"inter\",\n corbel: \"inter\",\n \"lucida sans\": \"inter\",\n \"lucida sans unicode\": \"inter\",\n\n // ── Linux sans-serif system fonts → inter ─────────────────────────────\n \"noto sans\": \"inter\",\n \"dejavu sans\": \"inter\",\n \"liberation sans\": \"inter\",\n\n // ── Monospace system fonts → jetbrains-mono ───────────────────────────\n \"sf mono\": \"jetbrains-mono\",\n menlo: \"jetbrains-mono\",\n monaco: \"jetbrains-mono\",\n consolas: \"jetbrains-mono\",\n \"lucida console\": \"jetbrains-mono\",\n \"lucida sans typewriter\": \"jetbrains-mono\",\n \"andale mono\": \"jetbrains-mono\",\n \"dejavu sans mono\": \"jetbrains-mono\",\n \"liberation mono\": \"jetbrains-mono\",\n\n // ── Serif system fonts → eb-garamond ──────────────────────────────────\n georgia: \"eb-garamond\",\n palatino: \"eb-garamond\",\n \"palatino linotype\": \"eb-garamond\",\n \"book antiqua\": \"eb-garamond\",\n cambria: \"eb-garamond\",\n times: \"eb-garamond\",\n \"times new roman\": \"eb-garamond\",\n \"dejavu serif\": \"eb-garamond\",\n \"liberation serif\": \"eb-garamond\",\n} satisfies Readonly<Record<string, string>>;\n\nexport const FONT_ALIAS_KEYS: ReadonlySet<string> = new Set(Object.keys(FONT_ALIAS_MAP));\n\n/**\n * Human-readable display names for canonical font slugs. Used by the lint\n * rule to tell authors what their aliased font will render as.\n */\nexport const CANONICAL_FONT_DISPLAY_NAMES: Readonly<Record<string, string>> = {\n inter: \"Inter\",\n montserrat: \"Montserrat\",\n outfit: \"Outfit\",\n nunito: \"Nunito\",\n oswald: \"Oswald\",\n \"league-gothic\": \"League Gothic\",\n \"archivo-black\": \"Archivo Black\",\n \"space-mono\": \"Space Mono\",\n \"ibm-plex-mono\": \"IBM Plex Mono\",\n \"jetbrains-mono\": \"JetBrains Mono\",\n \"eb-garamond\": \"EB Garamond\",\n \"playfair-display\": \"Playfair Display\",\n \"source-code-pro\": \"Source Code Pro\",\n \"noto-sans-jp\": \"Noto Sans JP\",\n roboto: \"Roboto\",\n \"open-sans\": \"Open Sans\",\n lato: \"Lato\",\n poppins: \"Poppins\",\n};\n\n/**\n * Resolve a font alias to its canonical display name, or undefined if the\n * alias is not in the map.\n */\nexport function resolveAliasDisplayName(alias: string): string | undefined {\n const slug = (FONT_ALIAS_MAP as Record<string, string>)[alias.toLowerCase()];\n if (!slug) return undefined;\n return CANONICAL_FONT_DISPLAY_NAMES[slug];\n}\n","export function decodeUrlPathVariants(path: string): string[] {\n const decoded = decodeWellFormedEscapes(path);\n return decoded === path ? [path] : [decoded, path];\n}\n\n// A browser sends src=\"100%.png\" as-is, so a % without two hex digits after it is the file's own.\nexport function decodeWellFormedEscapes(path: string): string {\n return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {\n try {\n return decodeURIComponent(run);\n } catch {\n return run;\n }\n });\n}\n\nexport function splitUrlSuffix(urlValue: string): { basePath: string; suffix: string } {\n const queryIdx = urlValue.indexOf(\"?\");\n const hashIdx = urlValue.indexOf(\"#\");\n if (queryIdx < 0 && hashIdx < 0) return { basePath: urlValue, suffix: \"\" };\n let cutIdx = urlValue.length;\n if (queryIdx >= 0) cutIdx = Math.min(cutIdx, queryIdx);\n if (hashIdx >= 0) cutIdx = Math.min(cutIdx, hashIdx);\n return { basePath: urlValue.slice(0, cutIdx), suffix: urlValue.slice(cutIdx) };\n}\n\nexport function decodedUrlPath(url: string): string {\n return decodeWellFormedEscapes(splitUrlSuffix(url).basePath);\n}\n\nexport function encodeUrlPath(path: string): string {\n return encodeURIComponent(path)\n .replace(/%2F/g, \"/\")\n .replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nexport function decodeCssEscapes(text: string): string {\n return text\n .replaceAll(\"\\0\", \"\\uFFFD\")\n .replace(\n /\\\\(?:([0-9a-f]{1,6})(?:\\r\\n|[ \\t\\r\\n\\f])?|(\\r\\n|[\\n\\r\\f])|([\\s\\S]))/gi,\n (\n _escaped,\n hex: string | undefined,\n lineBreak: string | undefined,\n char: string | undefined,\n ) => {\n if (hex === undefined) return lineBreak === undefined ? char! : \"\";\n const code = Number.parseInt(hex, 16);\n if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return \"\\uFFFD\";\n return String.fromCodePoint(code);\n },\n );\n}\n","/**\n * Browser-safe parser for the `data-composition-variables` schema attribute.\n * Lives outside htmlParser.ts so browser consumers (SDK, Studio, lint) can\n * import it via `@hyperframes/parsers/composition` without pulling the\n * linkedom/Node HTML-parser machinery from the main entry.\n */\n\nimport type { CompositionVariable, CompositionVariableType } from \"./types.js\";\n\n/**\n * Required typeof for each variable type's `default`. For font the default is\n * the font-family name string; for image it is the fallback URL string —\n * extra metadata fields on both are optional and not validated here.\n */\nconst DEFAULT_TYPEOF: Record<CompositionVariableType, \"string\" | \"number\" | \"boolean\"> = {\n string: \"string\",\n number: \"number\",\n color: \"string\",\n boolean: \"boolean\",\n enum: \"string\",\n font: \"string\",\n image: \"string\",\n};\n\n/**\n * Scalar variable values (string/number/boolean) are the ones that flow into\n * CSS custom props and text bindings; font/image values are object-shaped.\n * Shared so the SDK's CSS-compat writes, the runtime bindings, and Studio's\n * display logic can never disagree on what \"scalar\" means.\n */\nexport function isScalarVariableValue(value: unknown): value is string | number | boolean {\n return typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\";\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\nfunction isVariableType(t: unknown): t is CompositionVariableType {\n return typeof t === \"string\" && t in DEFAULT_TYPEOF;\n}\n\n/**\n * True when the value is a structurally valid variable declaration: id, label,\n * a known type, a default matching that type, and options[] for enums. The\n * same predicate parseCompositionVariables filters with — exported so writers\n * (SDK declaration ops, Studio forms) can validate before persisting.\n */\nexport function isCompositionVariable(v: unknown): v is CompositionVariable {\n if (!isRecord(v)) return false;\n if (typeof v.id !== \"string\" || typeof v.label !== \"string\") return false;\n if (!isVariableType(v.type)) return false;\n if (typeof v.default !== DEFAULT_TYPEOF[v.type]) return false;\n if (v.type === \"enum\" && !Array.isArray(v.options)) return false;\n return true;\n}\n\n/**\n * Parse the typed variable declarations from an element's\n * `data-composition-variables` attribute. Malformed entries (wrong shape,\n * unknown type, default not matching the declared type) are dropped; an\n * absent attribute, invalid JSON, or a non-array payload yields `[]`.\n */\nexport function parseCompositionVariables(htmlEl: Element): CompositionVariable[] {\n const variablesAttr = htmlEl.getAttribute(\"data-composition-variables\");\n if (!variablesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(variablesAttr);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter(isCompositionVariable);\n } catch {\n return [];\n }\n}\n\n/**\n * Protocol allowlist for a resolved media URL. Relative URLs (no scheme) resolve\n * against the page origin and are always safe. Absolute URLs are restricted to\n * http(s)/blob and image data: URIs -- defense-in-depth alongside the runtime's\n * VAR_SRC_TAGS element guard, blocking `javascript:`, `data:text/html`, `file:`,\n * etc. even if a future tag slips past it. Control chars are stripped before the\n * scheme test because browsers ignore them when parsing the URL.\n *\n * Lives here rather than in the runtime because the linter has to reach the same\n * verdict on a declared default statically: a value this rejects is dropped at\n * bind time and the element's authored fallback renders instead, so a second copy\n * of the scheme list would let lint and the runtime disagree silently.\n */\nexport function isSafeMediaUrl(url: string): boolean {\n // Browsers ignore ASCII control chars/whitespace when parsing a URL, so strip\n // them before reading the scheme (defeats `java\\tscript:` style bypasses).\n // oxlint-disable-next-line no-control-regex -- control chars are the target here\n const normalized = url.replace(/[\\u0000-\\u0020]/g, \"\");\n const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalized);\n if (!scheme) return true;\n const proto = scheme[1]?.toLowerCase();\n if (!proto) return false;\n if (proto === \"https\" || proto === \"http\" || proto === \"blob\") return true;\n if (proto === \"data\") return /^data:image\\//i.test(normalized);\n return false;\n}\n","/**\n * Browser-safe static scan for composition-variable reads in script text.\n *\n * Compositions read variables by calling the runtime API — `getVariables()`\n * bare (sub-comp scoped shadow) or via `__hyperframes.getVariables()` /\n * `window.__hyperframes.getVariables()` — and there is no DOM-attribute\n * binding to scan, so \"which variables does this composition use\" can only be\n * derived from the scripts. This is a best-effort static analysis: the\n * patterns agents actually write (destructuring, member access, a single\n * alias variable) resolve to ids; anything opaque flips `scanIncomplete`\n * so consumers can present usage as a lower bound instead of a fact.\n *\n * AST nodes are handled untyped (same convention as gsapParserAcorn.ts) —\n * acorn's structural types don't survive acorn-walk's visitor signatures.\n */\n\nimport * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\n\nexport interface VariableUsageScan {\n /** Variable ids statically read by the script, in first-seen order. */\n usedIds: string[];\n /**\n * True when the script accesses variables in a way the scan cannot resolve\n * (computed keys, rest spreads, the values object escaping into a call…) or\n * when the script fails to parse — usedIds is then a lower bound.\n */\n scanIncomplete: boolean;\n}\n\ninterface Sink {\n use(id: string): void;\n incomplete(): void;\n}\n\n// oxlint-disable no-explicit-any -- untyped acorn AST traversal, see header\n\nfunction isGetVariablesCallee(callee: any): boolean {\n if (callee?.type === \"Identifier\") return callee.name === \"getVariables\";\n if (callee?.type === \"MemberExpression\" && !callee.computed) {\n return callee.property?.type === \"Identifier\" && callee.property.name === \"getVariables\";\n }\n return false;\n}\n\n/** Collect ids from an ObjectPattern destructuring of the values object. */\n// Exhaustive AST-node classification — branchy by nature, same as gsapParserAcorn.\n// fallow-ignore-next-line complexity\nfunction collectFromObjectPattern(pattern: any, out: Sink): void {\n for (const prop of pattern.properties ?? []) {\n if (prop?.type === \"RestElement\") {\n out.incomplete();\n continue;\n }\n if (prop?.type !== \"Property\") continue;\n if (prop.computed === true) {\n out.incomplete();\n } else if (prop.key?.type === \"Identifier\") {\n out.use(String(prop.key.name));\n } else if (prop.key?.type === \"Literal\" && typeof prop.key.value === \"string\") {\n out.use(prop.key.value);\n } else {\n out.incomplete();\n }\n }\n}\n\n/** Collect an id from a MemberExpression reading the values object. */\nfunction collectFromMemberAccess(member: any, out: Sink): void {\n if (member.computed !== true && member.property?.type === \"Identifier\") {\n out.use(String(member.property.name));\n } else if (\n member.computed === true &&\n member.property?.type === \"Literal\" &&\n typeof member.property.value === \"string\"\n ) {\n out.use(member.property.value);\n } else {\n out.incomplete();\n }\n}\n\n/**\n * Classify one read of the values object (a getVariables() call result or an\n * alias holding it) by its immediate syntactic context. Returns the alias\n * name when the value is bound to a plain variable (`const vars = …`).\n */\n// fallow-ignore-next-line complexity\nfunction classifyValueRead(parent: any, valueNode: any, out: Sink): string | null {\n if (!parent || parent.type === \"ExpressionStatement\") {\n // Bare statement — value unused, nothing read.\n return null;\n }\n if (parent.type === \"MemberExpression\" && parent.object === valueNode) {\n collectFromMemberAccess(parent, out);\n return null;\n }\n if (parent.type === \"VariableDeclarator\" && parent.init === valueNode) {\n if (parent.id?.type === \"ObjectPattern\") {\n collectFromObjectPattern(parent.id, out);\n return null;\n }\n if (parent.id?.type === \"Identifier\") return String(parent.id.name);\n out.incomplete();\n return null;\n }\n // The values object escapes (argument, return, spread, assignment…) —\n // reads beyond this point are invisible to the scan.\n out.incomplete();\n return null;\n}\n\nexport function scanVariableUsage(scriptText: string): VariableUsageScan {\n const usedIds: string[] = [];\n const seen = new Set<string>();\n let scanIncomplete = false;\n\n const sink: Sink = {\n use(id: string) {\n if (!seen.has(id)) {\n seen.add(id);\n usedIds.push(id);\n }\n },\n incomplete() {\n scanIncomplete = true;\n },\n };\n\n let ast: any;\n try {\n ast = acorn.parse(scriptText, { ecmaVersion: \"latest\", sourceType: \"script\" });\n } catch {\n return { usedIds: [], scanIncomplete: true };\n }\n\n const aliases = new Set<string>();\n\n // Pass 1: classify every getVariables() call by its parent context.\n acornWalk.ancestor(ast, {\n CallExpression(node: any, _: unknown, ancestors: any[]) {\n if (!isGetVariablesCallee(node.callee)) return;\n const parent = ancestors[ancestors.length - 2];\n const alias = classifyValueRead(parent, node, sink);\n if (alias) aliases.add(alias);\n },\n } as any);\n\n // Pass 2: classify every reference to an alias of the values object.\n // Scope-naive by design: an unrelated same-named identifier can only make\n // the scan report extra ids or flip scanIncomplete, never miss a read.\n if (aliases.size > 0) {\n acornWalk.ancestor(ast, {\n // fallow-ignore-next-line complexity\n Identifier(node: any, _: unknown, ancestors: any[]) {\n if (!aliases.has(String(node.name))) return;\n const parent = ancestors[ancestors.length - 2];\n if (!parent) return;\n // Skip the declarator that introduced the alias and property-position\n // identifiers that merely share the name.\n if (parent.type === \"VariableDeclarator\" && parent.id === node) return;\n if (parent.type === \"MemberExpression\" && parent.property === node) return;\n if (parent.type === \"Property\" && parent.key === node && parent.computed !== true) return;\n // Chained aliases (const v2 = vars) are not followed — flag instead\n // of silently missing reads through the second name.\n if (classifyValueRead(parent, node, sink)) sink.incomplete();\n },\n } as any);\n }\n\n return { usedIds, scanIncomplete };\n}\n","/**\n * Browser-safe authored composition contract.\n *\n * Source HTML is authored with data-start + data-duration + data-track-index.\n * data-end is compiler-derived and data-layer is legacy input only. Readers\n * accept legacy documents; writers always emit the canonical representation.\n */\n\nexport const COMPOSITION_CONTRACT_VERSION = 1 as const;\n\nexport const COMPOSITION_ATTRIBUTES = Object.freeze({\n start: \"data-start\",\n duration: \"data-duration\",\n trackIndex: \"data-track-index\",\n derivedEnd: \"data-end\",\n legacyTrack: \"data-layer\",\n} as const);\n\nexport const CANONICAL_AUTHORED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.start,\n COMPOSITION_ATTRIBUTES.duration,\n COMPOSITION_ATTRIBUTES.trackIndex,\n] as const);\n\nexport const DERIVED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n] as const);\n\nexport const LEGACY_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n] as const);\n\nexport type ReferenceExpression =\n | { kind: \"absolute\"; value: number }\n | { kind: \"reference\"; refId: string; offset: number };\n\nexport type ClipTimingDiagnosticCode =\n | \"invalid-start\"\n | \"unresolved-start-reference\"\n | \"invalid-duration\"\n | \"invalid-end\"\n | \"end-before-start\"\n | \"deprecated-end\"\n | \"conflicting-end\"\n | \"invalid-track-index\"\n | \"deprecated-layer\"\n | \"conflicting-layer\";\n\nexport interface ClipTimingDiagnostic {\n code: ClipTimingDiagnosticCode;\n attribute: string;\n value: string | null;\n}\n\nexport interface ClipAttributeReader {\n getAttribute(name: string): string | null;\n}\n\nexport interface ClipAttributeWriter extends ClipAttributeReader {\n setAttribute(name: string, value: string): void;\n removeAttribute(name: string): void;\n}\n\nexport interface ReadClipTimingOptions {\n /** Resolve a reference to the referenced clip's absolute end time. */\n resolveReferenceEnd?: (refId: string) => number | null | undefined;\n /** Start used when data-start is absent. Defaults to zero. */\n defaultStart?: number | null;\n}\n\nexport interface ClipTiming {\n startExpression: ReferenceExpression | null;\n start: number | null;\n duration: number | null;\n end: number | null;\n trackIndex: number;\n durationSource: \"duration\" | \"legacy-end\" | \"missing\" | \"invalid\";\n trackSource: \"track-index\" | \"legacy-layer\" | \"default\" | \"invalid\";\n diagnostics: ClipTimingDiagnostic[];\n}\n\nexport interface ClipTimingUpdate {\n start?: number | string | ReferenceExpression;\n duration?: number | null;\n trackIndex?: number | null;\n}\n\nexport class ClipTimingWriteError extends Error {\n readonly code: \"invalid-start\" | \"invalid-duration\" | \"invalid-track-index\";\n\n constructor(code: ClipTimingWriteError[\"code\"], message: string) {\n super(message);\n this.name = \"ClipTimingWriteError\";\n this.code = code;\n }\n}\n\n/** Parse a value to a finite number, or null if it is absent/invalid. */\nexport function parseNumeric(value: string | null | undefined): number | null {\n if (value == null || value.trim() === \"\") return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nconst REFERENCE_ID_PATTERN = /^[A-Za-z0-9_.:-]+$/;\n\nfunction isAsciiDigitAt(value: string, index: number): boolean {\n const code = value.charCodeAt(index);\n return code >= 48 && code <= 57;\n}\n\nfunction skipDigitsLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && isAsciiDigitAt(value, cursor)) cursor--;\n return cursor;\n}\n\nfunction skipWhitespaceLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && (value[cursor] ?? \"\").trim() === \"\") cursor--;\n return cursor;\n}\n\nfunction findMagnitudeStart(value: string): number | null {\n const last = value.length - 1;\n if (!isAsciiDigitAt(value, last)) return null;\n let cursor = skipDigitsLeft(value, last);\n if (value[cursor] === \".\") cursor = skipDigitsLeft(value, cursor - 1);\n return cursor + 1;\n}\n\nfunction parseReferenceOffset(\n value: string,\n): { refId: string; operator: \"+\" | \"-\"; magnitude: number } | null {\n const magnitudeStart = findMagnitudeStart(value);\n if (magnitudeStart == null) return null;\n const operatorIndex = skipWhitespaceLeft(value, magnitudeStart - 1);\n const operator = value[operatorIndex];\n if (operator !== \"+\" && operator !== \"-\") return null;\n\n const refId = value.slice(0, operatorIndex).trim();\n if (!REFERENCE_ID_PATTERN.test(refId)) return null;\n const magnitude = Number(value.slice(magnitudeStart));\n if (!Number.isFinite(magnitude)) return null;\n return { refId, operator, magnitude };\n}\n\n/**\n * Parse the data-start grammar: absolute seconds, `clip-id`, or\n * `clip-id +/- offset`, where references resolve to the referenced clip's end.\n */\nexport function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {\n const normalized = (raw ?? \"\").trim();\n if (!normalized) return null;\n const absolute = parseNumeric(normalized);\n if (absolute != null) return { kind: \"absolute\", value: absolute };\n if (REFERENCE_ID_PATTERN.test(normalized)) {\n return { kind: \"reference\", refId: normalized, offset: 0 };\n }\n const reference = parseReferenceOffset(normalized);\n if (!reference) return null;\n return {\n kind: \"reference\",\n refId: reference.refId,\n offset: reference.operator === \"-\" ? -reference.magnitude : reference.magnitude,\n };\n}\n\nfunction pushDiagnostic(\n diagnostics: ClipTimingDiagnostic[],\n code: ClipTimingDiagnosticCode,\n attribute: string,\n value: string | null,\n): void {\n diagnostics.push({ code, attribute, value });\n}\n\nfunction resolveStart(\n expression: ReferenceExpression | null,\n rawStart: string | null,\n options: ReadClipTimingOptions,\n diagnostics: ClipTimingDiagnostic[],\n): number | null {\n if (rawStart == null || rawStart.trim() === \"\") {\n return options.defaultStart === undefined ? 0 : options.defaultStart;\n }\n if (!expression) {\n pushDiagnostic(diagnostics, \"invalid-start\", COMPOSITION_ATTRIBUTES.start, rawStart);\n return null;\n }\n if (expression.kind === \"absolute\") return Math.max(0, expression.value);\n const referencedEnd = options.resolveReferenceEnd?.(expression.refId);\n if (referencedEnd == null || !Number.isFinite(referencedEnd)) {\n pushDiagnostic(\n diagnostics,\n \"unresolved-start-reference\",\n COMPOSITION_ATTRIBUTES.start,\n rawStart,\n );\n return null;\n }\n return Math.max(0, referencedEnd + expression.offset);\n}\n\ntype DurationRead = Pick<ClipTiming, \"duration\" | \"end\" | \"durationSource\">;\ntype TrackRead = Pick<ClipTiming, \"trackIndex\" | \"trackSource\">;\n\n// Float pairs like `data-start=\"0.1\" + data-duration=\"0.2\"` add to\n// `0.30000000000000004`, so the compiler-emitted `data-end` string can be a few\n// ulps off the sum a reader computes from the canonical inputs. Any drift below\n// this ceiling is treated as equal for the derived-end reconciliation check —\n// well below one 60fps frame (~16.67 ms) and orders of magnitude above the\n// worst realistic IEEE-754 residual (~2e-16 s).\nconst DERIVED_END_EQUALITY_EPSILON_SECONDS = 1e-9;\n\nfunction derivedEndsAreConsistent(parsedEnd: number, canonicalEnd: number): boolean {\n return Math.abs(parsedEnd - canonicalEnd) <= DERIVED_END_EQUALITY_EPSILON_SECONDS;\n}\n\n// Reconcile a `data-end` attribute paired with canonical `data-duration`.\n//\n// The compiler (`compileTimingAttrs` in @hyperframes/core) writes\n// `data-end = data-start + data-duration` into the bundled HTML so the runtime\n// can key off a single attribute. That derived attribute is legitimate — it is\n// not a legacy authoring shape. Treating it as `deprecated-end` here made the\n// linter fire `deprecated_data_end` on the compiler's own output whenever\n// StaticGuard re-validated a bundled composition, producing a false-positive\n// cluster reported in the field (n=25+ across cli-feedback crons 61-68).\n//\n// We keep `deprecated-end` for the truly legacy shape (see `readLegacyEnd` —\n// `data-end` present without `data-duration`) and for the *conflicting* case\n// where an author left a stale `data-end` behind a canonical duration edit; a\n// stale end must not be silently overridden without a diagnostic.\nfunction diagnoseDerivedEnd(\n rawEnd: string,\n canonicalEnd: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): void {\n const parsedEnd = parseNumeric(rawEnd);\n if (parsedEnd == null) {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (canonicalEnd == null) {\n // Canonical duration exists but resolved end is unknown (e.g. unresolved\n // reference start). Preserve prior behavior — flag as deprecated so authors\n // remove the stale companion attribute.\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (derivedEndsAreConsistent(parsedEnd, canonicalEnd)) {\n // Compiler-derived (or manually consistent) `data-end` alongside\n // `data-duration` — silent. No diagnostic.\n return;\n }\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"conflicting-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n}\n\nfunction readCanonicalDuration(\n rawDuration: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const duration = parseNumeric(rawDuration);\n if (duration == null || duration < 0) {\n pushDiagnostic(diagnostics, \"invalid-duration\", COMPOSITION_ATTRIBUTES.duration, rawDuration);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return {\n duration,\n end: start == null ? null : start + duration,\n durationSource: \"duration\",\n };\n}\n\nfunction readLegacyEnd(\n rawEnd: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n const end = parseNumeric(rawEnd);\n if (end == null) {\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n if (start == null) return { duration: null, end, durationSource: \"legacy-end\" };\n if (end < start) {\n pushDiagnostic(diagnostics, \"end-before-start\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return { duration: end - start, end, durationSource: \"legacy-end\" };\n}\n\nfunction readDuration(\n attributes: ClipAttributeReader,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const rawDuration = attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration);\n const rawEnd = attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n if (rawDuration == null) {\n return rawEnd == null\n ? { duration: null, end: null, durationSource: \"missing\" }\n : readLegacyEnd(rawEnd, start, diagnostics);\n }\n const canonical = readCanonicalDuration(rawDuration, start, diagnostics);\n if (rawEnd != null) diagnoseDerivedEnd(rawEnd, canonical.end, diagnostics);\n return canonical;\n}\n\nfunction readTrackValue(\n rawValue: string,\n attribute: string,\n source: \"track-index\" | \"legacy-layer\",\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const trackIndex = parseNumeric(rawValue);\n if (trackIndex == null || !Number.isInteger(trackIndex)) {\n pushDiagnostic(diagnostics, \"invalid-track-index\", attribute, rawValue);\n return { trackIndex: 0, trackSource: \"invalid\" };\n }\n return { trackIndex, trackSource: source };\n}\n\nfunction readTrack(\n attributes: ClipAttributeReader,\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const rawTrack = attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n const rawLayer = attributes.getAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n if (rawTrack == null && rawLayer == null) return { trackIndex: 0, trackSource: \"default\" };\n if (rawTrack == null && rawLayer != null) {\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n return readTrackValue(\n rawLayer,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n \"legacy-layer\",\n diagnostics,\n );\n }\n const canonical = readTrackValue(\n rawTrack ?? \"\",\n COMPOSITION_ATTRIBUTES.trackIndex,\n \"track-index\",\n diagnostics,\n );\n if (rawLayer == null) return canonical;\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n const parsedLayer = parseNumeric(rawLayer);\n if (parsedLayer != null && parsedLayer !== canonical.trackIndex) {\n pushDiagnostic(diagnostics, \"conflicting-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n }\n return canonical;\n}\n\n/** Read canonical timing, accepting legacy attributes without preferring them. */\nexport function readClipTiming(\n attributes: ClipAttributeReader,\n options: ReadClipTimingOptions = {},\n): ClipTiming {\n const diagnostics: ClipTimingDiagnostic[] = [];\n const rawStart = attributes.getAttribute(COMPOSITION_ATTRIBUTES.start);\n const startExpression = parseStartExpression(rawStart);\n const start = resolveStart(startExpression, rawStart, options, diagnostics);\n const duration = readDuration(attributes, start, diagnostics);\n const track = readTrack(attributes, diagnostics);\n return { startExpression, start, ...duration, ...track, diagnostics };\n}\n\nfunction serializeStartNumber(start: number): string {\n if (!Number.isFinite(start)) {\n throw new ClipTimingWriteError(\"invalid-start\", \"start must be finite\");\n }\n return String(start);\n}\n\nfunction serializeStartReference(start: ReferenceExpression): string {\n if (start.kind === \"absolute\") return serializeStartNumber(start.value);\n if (!start.refId || !Number.isFinite(start.offset)) {\n throw new ClipTimingWriteError(\n \"invalid-start\",\n \"reference start must have an id and finite offset\",\n );\n }\n if (start.offset === 0) return start.refId;\n return `${start.refId} ${start.offset < 0 ? \"-\" : \"+\"} ${Math.abs(start.offset)}`;\n}\n\nfunction serializeStart(start: ClipTimingUpdate[\"start\"]): string {\n if (typeof start === \"number\") return serializeStartNumber(start);\n if (typeof start === \"object\" && start != null) return serializeStartReference(start);\n if (typeof start === \"string\" && parseStartExpression(start)) return start.trim();\n throw new ClipTimingWriteError(\"invalid-start\", `invalid start expression: ${start ?? \"\"}`);\n}\n\nfunction writeDuration(\n attributes: ClipAttributeWriter,\n duration: number | null | undefined,\n current: ClipTiming,\n): void {\n if (duration === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.duration);\n return;\n }\n if (duration === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n current.duration != null\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(current.duration));\n }\n return;\n }\n if (!Number.isFinite(duration) || duration < 0) {\n throw new ClipTimingWriteError(\n \"invalid-duration\",\n \"duration must be a finite, non-negative number\",\n );\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(duration));\n}\n\nfunction writeTrack(\n attributes: ClipAttributeWriter,\n trackIndex: number | null | undefined,\n current: ClipTiming,\n): void {\n if (trackIndex === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n return;\n }\n if (trackIndex === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex) == null &&\n current.trackSource === \"legacy-layer\"\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(current.trackIndex));\n }\n return;\n }\n if (!Number.isFinite(trackIndex) || !Number.isInteger(trackIndex)) {\n throw new ClipTimingWriteError(\"invalid-track-index\", \"trackIndex must be a finite integer\");\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(trackIndex));\n}\n\n/** Write only canonical authored timing and remove derived/legacy source attributes. */\nexport function writeClipTiming(\n attributes: ClipAttributeWriter,\n update: ClipTimingUpdate,\n): ClipTiming {\n const current = readClipTiming(attributes);\n if (update.start !== undefined) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.start, serializeStart(update.start));\n }\n writeDuration(attributes, update.duration, current);\n writeTrack(attributes, update.trackIndex, current);\n // A legacy end paired with an unresolved reference start cannot be converted\n // to data-duration without a reference resolver. On a duration-omitting edit\n // (for example, moving only the track), preserve that sole duration source\n // instead of canonicalizing it into data loss.\n const preserveUnresolvedLegacyEnd =\n update.duration === undefined &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd) != null &&\n current.duration == null;\n if (!preserveUnresolvedLegacyEnd) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n }\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n return readClipTiming(attributes);\n}\n","/**\n * Where a scaffolded project's canvas resolution lives besides the root's own\n * data-width/data-height: the html/body CSS size and the meta viewport content.\n */\n\n/** html/body CSS, width first. Groups 1/3 are the surrounding text, 2/4 the digits. */\nexport const HTML_BODY_CSS_WIDTH_FIRST_RE =\n /(html\\s*,\\s*body\\s*\\{[^}]*?width:\\s*)(\\d+)px([^}]*?height:\\s*)(\\d+)px/i;\n\n/** Same block, height authored first. */\nexport const HTML_BODY_CSS_HEIGHT_FIRST_RE =\n /(html\\s*,\\s*body\\s*\\{[^}]*?height:\\s*)(\\d+)px([^}]*?width:\\s*)(\\d+)px/i;\n\n/** Viewport meta content. Shared by lint (reads groups 2/4) and cli (replaces via 1/3). */\nexport const VIEWPORT_META_SIZE_RE =\n /(<meta[^>]*name=[\"']viewport[\"'][^>]*content=[\"']width=)(\\d+)(,\\s*height=)(\\d+)/i;\n","import * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\n\nexport interface MediaSrcMutation {\n selector: string;\n operation: \"src_assignment\" | \"set_attribute\";\n raw: string;\n}\n\nfunction parseProgram(script: string): any {\n try {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"script\" });\n } catch {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"module\" });\n }\n}\n\nfunction literalString(node: any): string | undefined {\n if (node?.type === \"Literal\" && typeof node.value === \"string\") return node.value;\n if (node?.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked;\n }\n return undefined;\n}\n\nfunction memberName(node: any): string | undefined {\n if (node?.type !== \"MemberExpression\") return undefined;\n if (!node.computed && node.property?.type === \"Identifier\") return node.property.name;\n return literalString(node.property);\n}\n\nfunction lookupSelector(node: any, bindings: ReadonlyMap<string, string>): string | undefined {\n if (node?.type === \"Identifier\") return bindings.get(node.name);\n if (node?.type !== \"CallExpression\" || node.callee?.type !== \"MemberExpression\") {\n return undefined;\n }\n const method = memberName(node.callee);\n if (method !== \"getElementById\" && method !== \"querySelector\") return undefined;\n const value = literalString(node.arguments?.[0]);\n if (!value) return undefined;\n return method === \"getElementById\" ? `#${value}` : value;\n}\n\n/** Find literal source writes whose target is a statically resolvable DOM lookup. */\nexport function extractMediaSrcMutations(script: string): MediaSrcMutation[] {\n try {\n const ast = parseProgram(script);\n const bindings = new Map<string, string>();\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n if (node.id?.type !== \"Identifier\") return;\n const selector = lookupSelector(node.init, bindings);\n if (selector) bindings.set(node.id.name, selector);\n },\n });\n\n const mutations: MediaSrcMutation[] = [];\n acornWalk.simple(ast, {\n AssignmentExpression(node: any) {\n if (node.operator !== \"=\" || memberName(node.left) !== \"src\") return;\n const selector = lookupSelector(node.left.object, bindings);\n if (!selector) return;\n mutations.push({\n selector,\n operation: \"src_assignment\",\n raw: script.slice(node.start, node.end),\n });\n },\n CallExpression(node: any) {\n if (memberName(node.callee) !== \"setAttribute\") return;\n if (literalString(node.arguments?.[0])?.toLowerCase() !== \"src\") return;\n const selector = lookupSelector(node.callee.object, bindings);\n if (!selector) return;\n mutations.push({\n selector,\n operation: \"set_attribute\",\n raw: script.slice(node.start, node.end),\n });\n },\n });\n return mutations;\n } catch {\n return [];\n }\n}\n"],"mappings":"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","names":["acorn","acornWalk"]}
|
package/dist/hfIds.js
CHANGED
|
@@ -96,7 +96,9 @@ function assignHfIds(body) {
|
|
|
96
96
|
|
|
97
97
|
// src/hfIds.ts
|
|
98
98
|
function ensureHfIds(html) {
|
|
99
|
-
const wrapped =
|
|
99
|
+
const wrapped = !/^(?:<!doctype|<html[\s>/])/i.test(
|
|
100
|
+
html.replace(/^(?:\s|<!--(?:>|->|[\s\S]*?-->))*/, "")
|
|
101
|
+
);
|
|
100
102
|
const { document } = wrapped ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`) : parseHTML(html);
|
|
101
103
|
if (!document.body) return html;
|
|
102
104
|
assignHfIds(document.body);
|
package/dist/hfIds.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hfIds.ts","../src/hfIdAssignment.ts"],"sourcesContent":["/**\n * Stable hf- element id minting (R1). Node-safe (linkedom only, not browser DOM).\n *\n * Two surfaces share these helpers:\n * - ensureHfIds(html): node-id surface — mints data-hf-id on every element.\n * - mintHfId(el, assigned): shared by htmlParser for clip ids.\n *\n * Hash is CONTENT ONLY (tag + sorted attrs + own text) — no sibling position,\n * so inserting a non-identical sibling never shifts another element's id.\n */\nimport { parseHTML } from \"linkedom\";\nimport { assignHfIds } from \"./hfIdAssignment.js\";\nexport {\n EXCLUDED_TAGS,\n mintHfId,\n isCompositionTemplate,\n walkCompositionDescendants,\n} from \"./hfIdAssignment.js\";\n\nexport function ensureHfIds(html: string): string {\n // Wrap fragments so every body element participates in stable ID assignment.\n const wrapped = !/<!doctype|<html[\\s>]/i.test(html);\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n if (!document.body) return html;\n assignHfIds(document.body);\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n","// Non-editable / non-visual elements that should never receive a stable id.\nexport const EXCLUDED_TAGS = new Set([\n \"script\",\n \"style\",\n \"template\",\n \"meta\",\n \"link\",\n \"noscript\",\n \"base\",\n]);\n\n// 32-bit FNV-1a. Pure, deterministic, no crypto, no Math.random.\nfunction fnv1a(str: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n// 4 base-36 chars · 36^4 ≈ 1.68M ids per document. Birthday-paradox collision\n// ≈ N²/(2·36^4): well under 1% per document after dup rehash at realistic\n// clip-model sizes (≤ a few hundred elements). The dup-rehash in mintHfId\n// resolves the rare collision; width is deliberately small for readable ids.\nfunction toHfId(hash: number): string {\n const s = (hash >>> 0).toString(36);\n // Use suffix (most-avalanched bits) for better distribution within the 4-char window.\n const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, \"0\");\n return `hf-${four}`;\n}\n\n// Element's own direct text (TEXT_NODE children), not descendants'.\nfunction ownText(el: Element): string {\n let text = \"\";\n el.childNodes.forEach((n) => {\n if (n.nodeType === 3) text += (n as Text).nodeValue ?? \"\";\n });\n return text.trim();\n}\n\nfunction getContractAttribute(el: Element, name: string): string | null {\n return Array.from(el.attributes).find((attr) => attr.name.toLowerCase() === name)?.value ?? null;\n}\n\nfunction contentKey(el: Element): string {\n // HTML parsers normalize foreign-content attribute names differently too.\n // Canonicalize only the hash input; retain actual SVG attribute spelling.\n // Exclude all data-hf-* attrs (ids, studio state) — they must not influence the hash.\n // Use \\x00 / \\x01 separators (invalid in HTML attrs) to prevent ambiguous serialization.\n const attrs = Array.from(el.attributes)\n .filter((a) => !a.name.toLowerCase().startsWith(\"data-hf-\"))\n .map((a) => `${a.name.toLowerCase()}\\x00${a.value}`)\n .sort()\n .join(\"\\x01\");\n return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;\n}\n\n/**\n * Collision tiebreak for byte-identical siblings: document-order dup counter\n * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`\n * get different ids based on which comes first in the DOM. This is unavoidable:\n * unique ids for byte-identical elements require a positional signal.\n *\n * Why this is safe in practice: once `data-hf-id` is on disk (stamped at project open, or by the\n * first save) it is bound to its element, so reordering identical siblings carries it along, and\n * `ensureHfIds` skips pinned elements (`if (getContractAttribute(el, \"data-hf-id\")) continue`).\n * Before that, an outside edit changes what is minted (an element's attributes or own text, or a\n * twin added or removed), so a save carrying an id served before the edit misses or hits the other\n * twin until the watcher reloads the preview.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of tag, sorted attrs, own text). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (served copy vs. the source a save parses) — see preview.test.ts\n// \"bundle returning untagged HTML gets the ids minted from the source file\". Any change that adds\n// positional, session, or random input to the hash breaks that invariant and\n// makes hf- ids diverge between disk and served HTML, silently corrupting\n// drag-to-edit targeting.\nexport function mintHfId(el: Element, assigned: Set<string>): string {\n const key = contentKey(el);\n let id = toHfId(fnv1a(key));\n let dup = 0;\n while (assigned.has(id)) {\n dup += 1;\n // Graceful fallback instead of a hard throw: rehashing only fails to find a\n // free 4-char slot in a pathological document (~1.6M identical elements).\n // Rather than crash the whole parse, widen the id with the dup counter —\n // still deterministic and unique, just longer than the 4-char norm.\n if (dup > 10000) {\n id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;\n break;\n }\n id = toHfId(fnv1a(`${key}#${dup}`));\n }\n assigned.add(id);\n return id;\n}\n\n/**\n * True for a sub-composition authoring template whose content the studio preview\n * unwraps into the served body. Two accepted forms:\n * A) `<template data-composition-id=\"X\">…` — the id on the template itself.\n * B) `<template id=\"X-template\"><div data-composition-id=\"X\">…` — the id on the\n * wrapped root div (the form `hyperframes add` scaffolds and registry blocks use).\n * Only these are treated as transparent containers for hf-id purposes. A plain\n * `<template>` (runtime clone-source: list item, particle, etc.) must NOT get\n * inner ids — its content is cloned N times into the live DOM, so a persisted\n * inner id would be duplicated across every clone. Form B is distinguished from\n * a clone-source by the presence of a direct `[data-composition-id]` child.\n */\nfunction getChildElements(parent: Element): Element[] {\n const directChildren = Array.from(parent.children);\n if (directChildren.length || parent.tagName.toLowerCase() !== \"template\") return directChildren;\n const content = (parent as HTMLTemplateElement).content;\n if (content?.children.length) return Array.from(content.children);\n return directChildren;\n}\n\nexport function isCompositionTemplate(el: Element): boolean {\n if (el.tagName.toLowerCase() !== \"template\") return false;\n if (getContractAttribute(el, \"data-composition-id\") !== null) return true;\n for (const child of getChildElements(el)) {\n if (getContractAttribute(child, \"data-composition-id\") !== null) return true;\n }\n return false;\n}\n\n/**\n * Walk document-order descendants, descending through composition templates\n * while keeping plain templates inert. linkedom's querySelectorAll does not\n * expose template contents, so callers that model the served composition use\n * this traversal instead.\n */\nexport function walkCompositionDescendants(\n root: Document | Element,\n visit: (el: Element) => void,\n): void {\n const rootElement: Element | null =\n root.nodeType === 9 ? (root as Document).documentElement : (root as Element);\n if (!rootElement) return;\n\n const walk = (parent: Element): void => {\n for (const child of getChildElements(parent)) {\n const isTemplate = child.tagName.toLowerCase() === \"template\";\n if (isTemplate && !isCompositionTemplate(child)) continue;\n visit(child);\n walk(child);\n }\n };\n\n walk(rootElement);\n}\n\n// Internal DOM-only assignment: callers retain their parser and document identity.\nexport function assignHfIds(body: Element): void {\n const assigned = new Set<string>();\n walkCompositionDescendants(body, (el) => {\n const existing = getContractAttribute(el, \"data-hf-id\");\n if (existing) assigned.add(existing);\n });\n walkCompositionDescendants(body, (el) => {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) return;\n if (getContractAttribute(el, \"data-hf-id\")) return;\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/hfIds.ts","../src/hfIdAssignment.ts"],"sourcesContent":["/**\n * Stable hf- element id minting (R1). Node-safe (linkedom only, not browser DOM).\n *\n * Two surfaces share these helpers:\n * - ensureHfIds(html): node-id surface — mints data-hf-id on every element.\n * - mintHfId(el, assigned): shared by htmlParser for clip ids.\n *\n * Hash is CONTENT ONLY (tag + sorted attrs + own text) — no sibling position,\n * so inserting a non-identical sibling never shifts another element's id.\n */\nimport { parseHTML } from \"linkedom\";\nimport { assignHfIds } from \"./hfIdAssignment.js\";\nexport {\n EXCLUDED_TAGS,\n mintHfId,\n isCompositionTemplate,\n walkCompositionDescendants,\n} from \"./hfIdAssignment.js\";\n\nexport function ensureHfIds(html: string): string {\n // Wrap fragments so every body element participates in stable ID assignment.\n const wrapped = !/^(?:<!doctype|<html[\\s>/])/i.test(\n html.replace(/^(?:\\s|<!--(?:>|->|[\\s\\S]*?-->))*/, \"\"),\n );\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n if (!document.body) return html;\n assignHfIds(document.body);\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n","// Non-editable / non-visual elements that should never receive a stable id.\nexport const EXCLUDED_TAGS = new Set([\n \"script\",\n \"style\",\n \"template\",\n \"meta\",\n \"link\",\n \"noscript\",\n \"base\",\n]);\n\n// 32-bit FNV-1a. Pure, deterministic, no crypto, no Math.random.\nfunction fnv1a(str: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n// 4 base-36 chars · 36^4 ≈ 1.68M ids per document. Birthday-paradox collision\n// ≈ N²/(2·36^4): well under 1% per document after dup rehash at realistic\n// clip-model sizes (≤ a few hundred elements). The dup-rehash in mintHfId\n// resolves the rare collision; width is deliberately small for readable ids.\nfunction toHfId(hash: number): string {\n const s = (hash >>> 0).toString(36);\n // Use suffix (most-avalanched bits) for better distribution within the 4-char window.\n const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, \"0\");\n return `hf-${four}`;\n}\n\n// Element's own direct text (TEXT_NODE children), not descendants'.\nfunction ownText(el: Element): string {\n let text = \"\";\n el.childNodes.forEach((n) => {\n if (n.nodeType === 3) text += (n as Text).nodeValue ?? \"\";\n });\n return text.trim();\n}\n\nfunction getContractAttribute(el: Element, name: string): string | null {\n return Array.from(el.attributes).find((attr) => attr.name.toLowerCase() === name)?.value ?? null;\n}\n\nfunction contentKey(el: Element): string {\n // HTML parsers normalize foreign-content attribute names differently too.\n // Canonicalize only the hash input; retain actual SVG attribute spelling.\n // Exclude all data-hf-* attrs (ids, studio state) — they must not influence the hash.\n // Use \\x00 / \\x01 separators (invalid in HTML attrs) to prevent ambiguous serialization.\n const attrs = Array.from(el.attributes)\n .filter((a) => !a.name.toLowerCase().startsWith(\"data-hf-\"))\n .map((a) => `${a.name.toLowerCase()}\\x00${a.value}`)\n .sort()\n .join(\"\\x01\");\n return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;\n}\n\n/**\n * Collision tiebreak for byte-identical siblings: document-order dup counter\n * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`\n * get different ids based on which comes first in the DOM. This is unavoidable:\n * unique ids for byte-identical elements require a positional signal.\n *\n * Why this is safe in practice: once `data-hf-id` is on disk (stamped at project open, or by the\n * first save) it is bound to its element, so reordering identical siblings carries it along, and\n * `ensureHfIds` skips pinned elements (`if (getContractAttribute(el, \"data-hf-id\")) continue`).\n * Before that, an outside edit changes what is minted (an element's attributes or own text, or a\n * twin added or removed), so a save carrying an id served before the edit misses or hits the other\n * twin until the watcher reloads the preview.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of tag, sorted attrs, own text). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (served copy vs. the source a save parses) — see preview.test.ts\n// \"bundle returning untagged HTML gets the ids minted from the source file\". Any change that adds\n// positional, session, or random input to the hash breaks that invariant and\n// makes hf- ids diverge between disk and served HTML, silently corrupting\n// drag-to-edit targeting.\nexport function mintHfId(el: Element, assigned: Set<string>): string {\n const key = contentKey(el);\n let id = toHfId(fnv1a(key));\n let dup = 0;\n while (assigned.has(id)) {\n dup += 1;\n // Graceful fallback instead of a hard throw: rehashing only fails to find a\n // free 4-char slot in a pathological document (~1.6M identical elements).\n // Rather than crash the whole parse, widen the id with the dup counter —\n // still deterministic and unique, just longer than the 4-char norm.\n if (dup > 10000) {\n id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;\n break;\n }\n id = toHfId(fnv1a(`${key}#${dup}`));\n }\n assigned.add(id);\n return id;\n}\n\n/**\n * True for a sub-composition authoring template whose content the studio preview\n * unwraps into the served body. Two accepted forms:\n * A) `<template data-composition-id=\"X\">…` — the id on the template itself.\n * B) `<template id=\"X-template\"><div data-composition-id=\"X\">…` — the id on the\n * wrapped root div (the form `hyperframes add` scaffolds and registry blocks use).\n * Only these are treated as transparent containers for hf-id purposes. A plain\n * `<template>` (runtime clone-source: list item, particle, etc.) must NOT get\n * inner ids — its content is cloned N times into the live DOM, so a persisted\n * inner id would be duplicated across every clone. Form B is distinguished from\n * a clone-source by the presence of a direct `[data-composition-id]` child.\n */\nfunction getChildElements(parent: Element): Element[] {\n const directChildren = Array.from(parent.children);\n if (directChildren.length || parent.tagName.toLowerCase() !== \"template\") return directChildren;\n const content = (parent as HTMLTemplateElement).content;\n if (content?.children.length) return Array.from(content.children);\n return directChildren;\n}\n\nexport function isCompositionTemplate(el: Element): boolean {\n if (el.tagName.toLowerCase() !== \"template\") return false;\n if (getContractAttribute(el, \"data-composition-id\") !== null) return true;\n for (const child of getChildElements(el)) {\n if (getContractAttribute(child, \"data-composition-id\") !== null) return true;\n }\n return false;\n}\n\n/**\n * Walk document-order descendants, descending through composition templates\n * while keeping plain templates inert. linkedom's querySelectorAll does not\n * expose template contents, so callers that model the served composition use\n * this traversal instead.\n */\nexport function walkCompositionDescendants(\n root: Document | Element,\n visit: (el: Element) => void,\n): void {\n const rootElement: Element | null =\n root.nodeType === 9 ? (root as Document).documentElement : (root as Element);\n if (!rootElement) return;\n\n const walk = (parent: Element): void => {\n for (const child of getChildElements(parent)) {\n const isTemplate = child.tagName.toLowerCase() === \"template\";\n if (isTemplate && !isCompositionTemplate(child)) continue;\n visit(child);\n walk(child);\n }\n };\n\n walk(rootElement);\n}\n\n// Internal DOM-only assignment: callers retain their parser and document identity.\nexport function assignHfIds(body: Element): void {\n const assigned = new Set<string>();\n walkCompositionDescendants(body, (el) => {\n const existing = getContractAttribute(el, \"data-hf-id\");\n if (existing) assigned.add(existing);\n });\n walkCompositionDescendants(body, (el) => {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) return;\n if (getContractAttribute(el, \"data-hf-id\")) return;\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,10 +8,11 @@ export { parseGsapScriptAcorn as parseGsapScript } from './gsapParserAcorn.js';
|
|
|
8
8
|
export { EXCLUDED_TAGS, ensureHfIds, isCompositionTemplate, mintHfId, walkCompositionDescendants } from './hfIds.js';
|
|
9
9
|
export { ParsableDocumentLike, SubCompositionValidity, SubCompositionValidityReason, checkSubCompositionUsability, resolveSubCompositionContent } from './subCompositionValidity.js';
|
|
10
10
|
export { CANONICAL_AUTHORED_TIMING_ATTRIBUTES, COMPOSITION_ATTRIBUTES, COMPOSITION_CONTRACT_VERSION, ClipAttributeReader, ClipAttributeWriter, ClipTiming, ClipTimingDiagnostic, ClipTimingDiagnosticCode, ClipTimingUpdate, ClipTimingWriteError, DERIVED_TIMING_ATTRIBUTES, LEGACY_TIMING_ATTRIBUTES, ReadClipTimingOptions, ReferenceExpression, parseNumeric, parseStartExpression, readClipTiming, writeClipTiming } from './compositionContract.js';
|
|
11
|
-
export { CANONICAL_FONT_DISPLAY_NAMES, FONT_ALIAS_KEYS, FONT_ALIAS_MAP, HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE, MediaSrcMutation, VIEWPORT_META_SIZE_RE, VariableUsageScan,
|
|
11
|
+
export { CANONICAL_FONT_DISPLAY_NAMES, FONT_ALIAS_KEYS, FONT_ALIAS_MAP, HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE, MediaSrcMutation, VIEWPORT_META_SIZE_RE, VariableUsageScan, extractMediaSrcMutations, parseCompositionVariables, resolveAliasDisplayName, scanVariableUsage } from './composition.js';
|
|
12
12
|
export { StructureNode, TrackKind, TrackKindSource, isSubCompositionHost, isTimedElement, topLevelElements, trackKindOf } from './topLevelElements.js';
|
|
13
13
|
export { AttrReader, DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS, MAX_PLAYBACK_RATE, MIN_PLAYBACK_RATE, MediaDurationResult, MediaDurationSource, MediaTag, PENDING_MEDIA_DURATION_READERS, ResolveMediaDurationInput, clampPlaybackRate, readAuthoredDurationSeconds, readDataDurationSeconds, readMediaOffsetSeconds, readPlaybackRate, resolveMediaDuration, resolveNaturalDurationSeconds } from './mediaDuration.js';
|
|
14
14
|
export { CompositionDurationResult, CompositionDurationSource, ResolveCompositionDurationInput, TIMING_TOLERANCE_SECONDS, resolveCompositionDuration } from './compositionDuration.js';
|
|
15
|
+
export { c as decodeUrlPathVariants, b as decodedUrlPath, e as encodeUrlPath } from './urlPath-C_0f_-Hp.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Thrown by htmlParser functions when the input HTML is empty or does not
|
|
@@ -166,4 +167,30 @@ interface TimingMismatchReport {
|
|
|
166
167
|
*/
|
|
167
168
|
declare function timingMismatches(html: string): TimingMismatchReport;
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
type HtmlAttributeSpan = {
|
|
171
|
+
name: string;
|
|
172
|
+
start: number;
|
|
173
|
+
end: number;
|
|
174
|
+
} & ({
|
|
175
|
+
kind: "boolean";
|
|
176
|
+
} | {
|
|
177
|
+
kind: "value";
|
|
178
|
+
value: string;
|
|
179
|
+
valueStart: number;
|
|
180
|
+
valueEnd: number;
|
|
181
|
+
quote: string;
|
|
182
|
+
});
|
|
183
|
+
interface HtmlOpeningTagSpan {
|
|
184
|
+
name: string;
|
|
185
|
+
start: number;
|
|
186
|
+
end: number;
|
|
187
|
+
bodyEnd: number;
|
|
188
|
+
closed: boolean;
|
|
189
|
+
rawTextEnd: number | null;
|
|
190
|
+
attributes: HtmlAttributeSpan[];
|
|
191
|
+
}
|
|
192
|
+
declare function scanHtmlOpeningTags(html: string): HtmlOpeningTagSpan[];
|
|
193
|
+
declare const HTML_ATTRIBUTE_ENTITIES: Record<string, readonly string[]>;
|
|
194
|
+
declare function decodeAuthoredAttribute(value: string): string;
|
|
195
|
+
|
|
196
|
+
export { CanvasResolution, CompositionHtmlParseError, type CompositionMetadata, CompositionVariable, HTML_ATTRIBUTE_ENTITIES, type HtmlAttributeSpan, type HtmlOpeningTagSpan, Keyframe, type OutputResolutionCompatibility, type OutputResolutionIssueKind, type ParsedHtml, StageZoomKeyframe, TimelineElement, type TimingFinding, type TimingMismatchReport, type UnresolvedTiming, ValidationResult, addElementToHtml, checkOutputResolutionCompatibility, decodeAuthoredAttribute, extractCompositionMetadata, parseHtml, queryByAttr, removeElementFromHtml, removeElementWithGsapCascade, scanHtmlOpeningTags, suggestMatchingPreset, timingMismatches, unrollComputedTimeline, updateElementInHtml, validateCompositionHtml };
|