@hyperframes/parsers 0.7.51 → 0.7.52

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/hfIds.d.ts CHANGED
@@ -13,19 +13,14 @@ declare const EXCLUDED_TAGS: Set<string>;
13
13
  * never re-exposes the ordering after first persist.
14
14
  */
15
15
  declare function mintHfId(el: Element, assigned: Set<string>): string;
16
+ declare function isCompositionTemplate(el: Element): boolean;
16
17
  /**
17
- * True for a sub-composition authoring template whose content the studio preview
18
- * unwraps into the served body. Two accepted forms:
19
- * A) `<template data-composition-id="X">…` the id on the template itself.
20
- * B) `<template id="X-template"><div data-composition-id="X">…` — the id on the
21
- * wrapped root div (the form `hyperframes add` scaffolds and registry blocks use).
22
- * Only these are treated as transparent containers for hf-id purposes. A plain
23
- * `<template>` (runtime clone-source: list item, particle, etc.) must NOT get
24
- * inner ids — its content is cloned N times into the live DOM, so a persisted
25
- * inner id would be duplicated across every clone. Form B is distinguished from
26
- * a clone-source by the presence of a direct `[data-composition-id]` child.
18
+ * Walk document-order descendants, descending through composition templates
19
+ * while keeping plain templates inert. linkedom's querySelectorAll does not
20
+ * expose template contents, so callers that model the served composition use
21
+ * this traversal instead.
27
22
  */
28
- declare function isCompositionTemplate(el: Element): boolean;
23
+ declare function walkCompositionDescendants(root: Document | Element, visit: (el: Element) => void): void;
29
24
  declare function ensureHfIds(html: string): string;
30
25
 
31
- export { EXCLUDED_TAGS, ensureHfIds, isCompositionTemplate, mintHfId };
26
+ export { EXCLUDED_TAGS, ensureHfIds, isCompositionTemplate, mintHfId, walkCompositionDescendants };
package/dist/hfIds.js CHANGED
@@ -48,21 +48,36 @@ function mintHfId(el, assigned) {
48
48
  assigned.add(id);
49
49
  return id;
50
50
  }
51
+ function getChildElements(parent) {
52
+ const directChildren = Array.from(parent.children);
53
+ if (directChildren.length || parent.tagName.toLowerCase() !== "template") return directChildren;
54
+ const content = parent.content;
55
+ if (content?.children.length) return Array.from(content.children);
56
+ return directChildren;
57
+ }
51
58
  function isCompositionTemplate(el) {
52
59
  if (el.tagName.toLowerCase() !== "template") return false;
53
60
  if (el.getAttribute("data-composition-id") !== null) return true;
54
- for (const child of Array.from(el.children)) {
61
+ for (const child of getChildElements(el)) {
55
62
  if (child.getAttribute("data-composition-id") !== null) return true;
56
63
  }
57
64
  return false;
58
65
  }
66
+ function walkCompositionDescendants(root, visit) {
67
+ const rootElement = root.nodeType === 9 ? root.documentElement : root;
68
+ if (!rootElement) return;
69
+ const walk = (parent) => {
70
+ for (const child of getChildElements(parent)) {
71
+ const isTemplate = child.tagName.toLowerCase() === "template";
72
+ if (isTemplate && !isCompositionTemplate(child)) continue;
73
+ visit(child);
74
+ walk(child);
75
+ }
76
+ };
77
+ walk(rootElement);
78
+ }
59
79
  function walkElements(root, visit) {
60
- for (const child of Array.from(root.children)) {
61
- const isTemplate = child.tagName.toLowerCase() === "template";
62
- if (isTemplate && !isCompositionTemplate(child)) continue;
63
- visit(child);
64
- walkElements(child, visit);
65
- }
80
+ walkCompositionDescendants(root, visit);
66
81
  }
67
82
  function ensureHfIds(html) {
68
83
  const hasDocumentShell = /<!doctype|<html[\s>]/i.test(html);
@@ -86,6 +101,7 @@ export {
86
101
  EXCLUDED_TAGS,
87
102
  ensureHfIds,
88
103
  isCompositionTemplate,
89
- mintHfId
104
+ mintHfId,
105
+ walkCompositionDescendants
90
106
  };
91
107
  //# sourceMappingURL=hfIds.js.map
package/dist/hfIds.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hfIds.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\";\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 contentKey(el: Element): string {\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.startsWith(\"data-hf-\"))\n .map((a) => `${a.name}\\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 `ensureHfIds` write-back persists\n * `data-hf-id` to source the attribute is physically bound to its element.\n * Reordering identical siblings carries the attribute along → zero\n * order-dependence post-persist. `ensureHfIds` skips pinned elements\n * (`if (el.getAttribute(\"data-hf-id\")) continue`), so normal operation\n * never re-exposes the ordering after first persist.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts\n// \"bundle returning untagged HTML gets same ids as disk\". 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 */\nexport function isCompositionTemplate(el: Element): boolean {\n if (el.tagName.toLowerCase() !== \"template\") return false;\n if (el.getAttribute(\"data-composition-id\") !== null) return true;\n for (const child of Array.from(el.children)) {\n if (child.getAttribute(\"data-composition-id\") !== null) return true;\n }\n return false;\n}\n\n/**\n * Document-order walk of every element under `root`, descending into\n * composition `<template>` subtrees — linkedom's querySelectorAll does not, so\n * template-based sub-comps would otherwise never get inner ids (the preview\n * unwraps the template and stamps the SAME content, so skipping here splits\n * the id space between the served DOM and the raw file). Plain templates are\n * skipped entirely (see isCompositionTemplate).\n */\nfunction walkElements(root: Element, visit: (el: Element) => void): void {\n for (const child of Array.from(root.children)) {\n const isTemplate = child.tagName.toLowerCase() === \"template\";\n if (isTemplate && !isCompositionTemplate(child)) continue;\n visit(child);\n walkElements(child, visit);\n }\n}\n\nexport function ensureHfIds(html: string): string {\n // Mirror parseSourceDocument's fragment-wrapping so bare fragments don't land\n // outside <body> in linkedom, which would cause body.querySelectorAll to return [].\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(html);\n const wrapped = !hasDocumentShell;\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n const body = document.body;\n if (!body) return html;\n\n const assigned = new Set<string>();\n // Seed with already-present ids (pin) so fresh mints never collide with them.\n // Scope to <body> to match the mint walk below — a stray data-hf-id in <head>\n // must not pin an id into the set that a body element would then be bumped off.\n walkElements(body, (el) => {\n const existing = el.getAttribute(\"data-hf-id\");\n if (existing) assigned.add(existing);\n });\n\n walkElements(body, (el) => {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) return;\n if (el.getAttribute(\"data-hf-id\")) return; // pinned\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n });\n\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hfIds.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\";\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 contentKey(el: Element): string {\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.startsWith(\"data-hf-\"))\n .map((a) => `${a.name}\\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 `ensureHfIds` write-back persists\n * `data-hf-id` to source the attribute is physically bound to its element.\n * Reordering identical siblings carries the attribute along → zero\n * order-dependence post-persist. `ensureHfIds` skips pinned elements\n * (`if (el.getAttribute(\"data-hf-id\")) continue`), so normal operation\n * never re-exposes the ordering after first persist.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts\n// \"bundle returning untagged HTML gets same ids as disk\". 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 (el.getAttribute(\"data-composition-id\") !== null) return true;\n for (const child of getChildElements(el)) {\n if (child.getAttribute(\"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/**\n * Document-order walk of every element under `root`, descending into\n * composition `<template>` subtrees — linkedom's querySelectorAll does not, so\n * template-based sub-comps would otherwise never get inner ids (the preview\n * unwraps the template and stamps the SAME content, so skipping here splits\n * the id space between the served DOM and the raw file). Plain templates are\n * skipped entirely (see isCompositionTemplate).\n */\nfunction walkElements(root: Element, visit: (el: Element) => void): void {\n walkCompositionDescendants(root, visit);\n}\n\nexport function ensureHfIds(html: string): string {\n // Mirror parseSourceDocument's fragment-wrapping so bare fragments don't land\n // outside <body> in linkedom, which would cause body.querySelectorAll to return [].\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(html);\n const wrapped = !hasDocumentShell;\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n const body = document.body;\n if (!body) return html;\n\n const assigned = new Set<string>();\n // Seed with already-present ids (pin) so fresh mints never collide with them.\n // Scope to <body> to match the mint walk below — a stray data-hf-id in <head>\n // must not pin an id into the set that a body element would then be bumped off.\n walkElements(body, (el) => {\n const existing = el.getAttribute(\"data-hf-id\");\n if (existing) assigned.add(existing);\n });\n\n walkElements(body, (el) => {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) return;\n if (el.getAttribute(\"data-hf-id\")) return; // pinned\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n });\n\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { isStudioHoldSet } from './gsapParser.js';
5
5
  export { PROPERTY_GROUPS, PropertyGroupName, SUPPORTED_EASES, SUPPORTED_PROPS, classifyPropertyGroup, classifyTweenPropertyGroup } from './gsapConstants.js';
6
6
  export { SPRING_PRESETS, SpringPreset, generateSpringEaseData } from './springEase.js';
7
7
  export { parseGsapScriptAcorn as parseGsapScript } from './gsapParserAcorn.js';
8
- export { EXCLUDED_TAGS, ensureHfIds, isCompositionTemplate, mintHfId } from './hfIds.js';
8
+ export { EXCLUDED_TAGS, ensureHfIds, isCompositionTemplate, mintHfId, walkCompositionDescendants } from './hfIds.js';
9
9
  export { ParsableDocumentLike, SubCompositionValidity, SubCompositionValidityReason, checkSubCompositionUsability } from './subCompositionValidity.js';
10
10
  export { CANONICAL_FONT_DISPLAY_NAMES, FONT_ALIAS_KEYS, FONT_ALIAS_MAP, VariableUsageScan, decodeUrlPathVariants, parseCompositionVariables, resolveAliasDisplayName, scanVariableUsage } from './composition.js';
11
11
 
package/dist/index.js CHANGED
@@ -2026,21 +2026,36 @@ function mintHfId(el, assigned) {
2026
2026
  assigned.add(id);
2027
2027
  return id;
2028
2028
  }
2029
+ function getChildElements(parent) {
2030
+ const directChildren = Array.from(parent.children);
2031
+ if (directChildren.length || parent.tagName.toLowerCase() !== "template") return directChildren;
2032
+ const content = parent.content;
2033
+ if (content?.children.length) return Array.from(content.children);
2034
+ return directChildren;
2035
+ }
2029
2036
  function isCompositionTemplate(el) {
2030
2037
  if (el.tagName.toLowerCase() !== "template") return false;
2031
2038
  if (el.getAttribute("data-composition-id") !== null) return true;
2032
- for (const child of Array.from(el.children)) {
2039
+ for (const child of getChildElements(el)) {
2033
2040
  if (child.getAttribute("data-composition-id") !== null) return true;
2034
2041
  }
2035
2042
  return false;
2036
2043
  }
2044
+ function walkCompositionDescendants(root, visit) {
2045
+ const rootElement = root.nodeType === 9 ? root.documentElement : root;
2046
+ if (!rootElement) return;
2047
+ const walk = (parent) => {
2048
+ for (const child of getChildElements(parent)) {
2049
+ const isTemplate = child.tagName.toLowerCase() === "template";
2050
+ if (isTemplate && !isCompositionTemplate(child)) continue;
2051
+ visit(child);
2052
+ walk(child);
2053
+ }
2054
+ };
2055
+ walk(rootElement);
2056
+ }
2037
2057
  function walkElements(root, visit) {
2038
- for (const child of Array.from(root.children)) {
2039
- const isTemplate = child.tagName.toLowerCase() === "template";
2040
- if (isTemplate && !isCompositionTemplate(child)) continue;
2041
- visit(child);
2042
- walkElements(child, visit);
2043
- }
2058
+ walkCompositionDescendants(root, visit);
2044
2059
  }
2045
2060
  function ensureHfIds(html) {
2046
2061
  const hasDocumentShell = /<!doctype|<html[\s>]/i.test(html);
@@ -2377,7 +2392,7 @@ function parseHtml(html) {
2377
2392
  elements.push(mediaElement);
2378
2393
  }
2379
2394
  });
2380
- const scriptTags = doc.querySelectorAll("script");
2395
+ const scriptTags = findScriptElementsDeep(doc);
2381
2396
  let gsapScript = null;
2382
2397
  for (const script of scriptTags) {
2383
2398
  const src = script.getAttribute("src");
@@ -2622,7 +2637,7 @@ function stripGsapForId(script, elementId) {
2622
2637
  }
2623
2638
  }
2624
2639
  function cascadeRemoveGsapById(doc, elementId) {
2625
- for (const script of Array.from(doc.querySelectorAll("script"))) {
2640
+ for (const script of findScriptElementsDeep(doc)) {
2626
2641
  const text = script.textContent ?? "";
2627
2642
  if (!text.includes("gsap") && !text.includes("ScrollTrigger")) continue;
2628
2643
  const updated = stripGsapForId(text, elementId);
@@ -2696,12 +2711,11 @@ function validateCompositionHtml(html) {
2696
2711
  if (/javascript\s*:/i.test(html)) {
2697
2712
  errors.push("javascript: URLs not allowed");
2698
2713
  }
2699
- const scripts = doc.querySelectorAll("script");
2714
+ const scripts = findScriptElementsDeep(doc);
2700
2715
  if (scripts.length > 2) {
2701
2716
  warnings.push("Multiple script tags detected - only GSAP CDN and main script expected");
2702
2717
  }
2703
- const gsapScript = extractGsapScript(doc);
2704
- if (gsapScript) {
2718
+ for (const gsapScript of extractGsapScripts(doc)) {
2705
2719
  const gsapValidation = validateCompositionGsap(gsapScript);
2706
2720
  errors.push(...gsapValidation.errors);
2707
2721
  warnings.push(...gsapValidation.warnings);
@@ -2712,15 +2726,23 @@ function validateCompositionHtml(html) {
2712
2726
  warnings
2713
2727
  };
2714
2728
  }
2715
- function extractGsapScript(doc) {
2716
- const scripts = doc.querySelectorAll("script");
2729
+ function findScriptElementsDeep(doc) {
2730
+ const scripts = [];
2731
+ walkCompositionDescendants(doc, (el) => {
2732
+ if (el.tagName.toLowerCase() === "script") scripts.push(el);
2733
+ });
2734
+ return scripts;
2735
+ }
2736
+ function extractGsapScripts(doc) {
2737
+ const scripts = findScriptElementsDeep(doc);
2738
+ const gsapScripts = [];
2717
2739
  for (const script of scripts) {
2718
2740
  const content = script.textContent || "";
2719
2741
  if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
2720
- return content;
2742
+ gsapScripts.push(content);
2721
2743
  }
2722
2744
  }
2723
- return null;
2745
+ return gsapScripts;
2724
2746
  }
2725
2747
 
2726
2748
  // src/subCompositionValidity.ts
@@ -3199,6 +3221,7 @@ export {
3199
3221
  unrollComputedTimeline,
3200
3222
  updateElementInHtml,
3201
3223
  validateCompositionGsap,
3202
- validateCompositionHtml
3224
+ validateCompositionHtml,
3225
+ walkCompositionDescendants
3203
3226
  };
3204
3227
  //# sourceMappingURL=index.js.map