@hyperframes/parsers 0.8.50 → 0.8.51

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/index.js CHANGED
@@ -494,7 +494,16 @@ import * as acorn from "acorn";
494
494
  import * as acornWalk from "acorn-walk";
495
495
 
496
496
  // src/gsapInline.ts
497
- var SKIP_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range", "__hfProvenance", "__hfOrder"]);
497
+ var SKIP_KEYS = /* @__PURE__ */ new Set([
498
+ "type",
499
+ "start",
500
+ "end",
501
+ "loc",
502
+ "range",
503
+ "__hfProvenance",
504
+ "__hfOrder",
505
+ "__hfSiteStart"
506
+ ]);
498
507
  var FUNCTION_TYPES = /* @__PURE__ */ new Set([
499
508
  "ArrowFunctionExpression",
500
509
  "FunctionExpression",
@@ -597,6 +606,9 @@ function isTimelineRooted(call, timelineVar) {
597
606
  if (timelineRootName(call) !== timelineVar) return false;
598
607
  return call.callee?.property?.type === "Identifier" && GSAP_METHODS.has(call.callee.property.name);
599
608
  }
609
+ function isAddLabelCall(call, timelineVar) {
610
+ return timelineRootName(call) === timelineVar && call.callee?.property?.name === "addLabel";
611
+ }
600
612
  function containsTimelineCall(node, timelineVar) {
601
613
  let found = false;
602
614
  walkNodes(node, (n) => {
@@ -803,12 +815,17 @@ function bodyStatements(node) {
803
815
  }
804
816
  function tagTimelineCalls(stmts, prov, ctx) {
805
817
  for (const stmt of stmts) {
818
+ const calls = [];
806
819
  walkNodes(stmt, (n) => {
807
- if (n.type === "CallExpression" && isTimelineRooted(n, ctx.timelineVar)) {
808
- tagProvenance(n, { ...prov });
809
- n.__hfOrder = ctx.order.n++;
810
- }
820
+ if (n.type === "CallExpression" && (isTimelineRooted(n, ctx.timelineVar) || isAddLabelCall(n, ctx.timelineVar)))
821
+ calls.push(n);
811
822
  });
823
+ calls.sort((a, b) => a.callee.property.start - b.callee.property.start);
824
+ for (const n of calls) {
825
+ tagProvenance(n, { ...prov });
826
+ n.__hfOrder = ctx.order.n++;
827
+ n.__hfSiteStart = ctx.rootStart;
828
+ }
812
829
  }
813
830
  }
814
831
  function expandBody(bodyStmts, bindings, prov, ctx) {
@@ -995,10 +1012,14 @@ function inlineComputedTimelines(ast, timelineVar, resolve) {
995
1012
  resolve,
996
1013
  depth: 0,
997
1014
  site: { n: 0 },
998
- order: { n: 0 }
1015
+ order: { n: 0 },
1016
+ rootStart: 0
999
1017
  };
1000
1018
  const body = (ast.body ?? []).filter((stmt) => !isHelperDecl(stmt, helpers));
1001
- ast.body = expandStatements(body, ctx);
1019
+ ast.body = body.flatMap((stmt) => {
1020
+ ctx.rootStart = stmt.start;
1021
+ return expandStatements([stmt], ctx);
1022
+ });
1002
1023
  }
1003
1024
 
1004
1025
  // src/gsapParserAcorn.ts
@@ -1416,6 +1437,14 @@ function objectExpressionToRecord(node, scope, source) {
1416
1437
  function isGsapTimelineCall(node) {
1417
1438
  return node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.name === "gsap" && node.callee.property?.name === "timeline";
1418
1439
  }
1440
+ function hasUnknownDuration(varsNode, scope) {
1441
+ if (!varsNode) return false;
1442
+ if (varsNode.type !== "ObjectExpression") return true;
1443
+ const props = varsNode.properties ?? [];
1444
+ if (props.some((p) => p.type === "SpreadElement")) return true;
1445
+ const prop = props.find((p) => isObjectProperty(p) && propKeyName(p) === "duration");
1446
+ return prop !== void 0 && typeof resolveNode(prop.value, scope) !== "number";
1447
+ }
1419
1448
  function staticMemberKey(node) {
1420
1449
  if (!node || node.type !== "MemberExpression") return null;
1421
1450
  if (node.computed) {
@@ -1457,7 +1486,10 @@ function extractTimelineDefaults(callNode, scope) {
1457
1486
  const key = propKeyName(prop);
1458
1487
  const val = resolveNode(prop.value, scope);
1459
1488
  if (key === "ease" && typeof val === "string") result.ease = val;
1460
- if (key === "duration" && typeof val === "number") result.duration = val;
1489
+ if (key === "duration") {
1490
+ if (typeof val === "number") result.duration = val;
1491
+ else result.durationUnresolved = true;
1492
+ }
1461
1493
  }
1462
1494
  return Object.keys(result).length > 0 ? result : void 0;
1463
1495
  }
@@ -1832,6 +1864,7 @@ function tweenCallToAnimation(call, scope, source, identifierBindings) {
1832
1864
  if (duration === void 0 && keyframesData) {
1833
1865
  duration = computeKeyframesTotalDuration(call.varsArg, scope, source);
1834
1866
  }
1867
+ const durationUnresolved = call.method !== "set" && duration === void 0 && hasUnknownDuration(call.varsArg, scope);
1835
1868
  let selector = call.selector;
1836
1869
  let targetIdentity;
1837
1870
  if (selector === "__unresolved__") {
@@ -1879,6 +1912,7 @@ function tweenCallToAnimation(call, scope, source, identifierBindings) {
1879
1912
  if (keyframesData) anim.keyframes = keyframesData;
1880
1913
  if (motionPathResult) anim.arcPath = motionPathResult.arcPath;
1881
1914
  if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;
1915
+ if (durationUnresolved) anim.durationUnresolved = true;
1882
1916
  if (selector === "__unresolved__") anim.hasUnresolvedSelector = true;
1883
1917
  if (provenance) anim.provenance = provenance;
1884
1918
  return anim;
@@ -2002,8 +2036,9 @@ function applyTimelineDefaults(anims, defaults) {
2002
2036
  if (!defaults) return;
2003
2037
  for (const anim of anims) {
2004
2038
  if (anim.method === "set") continue;
2005
- if (anim.duration === void 0 && defaults.duration !== void 0) {
2006
- anim.duration = defaults.duration;
2039
+ if (anim.duration === void 0 && !anim.durationUnresolved) {
2040
+ if (defaults.duration !== void 0) anim.duration = defaults.duration;
2041
+ else if (defaults.durationUnresolved) anim.durationUnresolved = true;
2007
2042
  }
2008
2043
  if (anim.ease === void 0 && defaults.ease !== void 0) {
2009
2044
  anim.ease = defaults.ease;
@@ -2036,12 +2071,14 @@ function resolveAnimStart(anim, cursor, prevStart, labels) {
2036
2071
  function resolveTimelinePositions(anims, labelDefs = []) {
2037
2072
  let cursor = 0;
2038
2073
  let prevStart = 0;
2074
+ let cursorKnown = true;
2039
2075
  const labels = /* @__PURE__ */ new Map();
2040
2076
  let labelIdx = 0;
2041
2077
  const sortedLabels = [...labelDefs].sort((a, b) => a.order - b.order);
2042
2078
  const defineLabel = (def) => {
2043
2079
  let value;
2044
2080
  if (typeof def.position === "number") value = def.position;
2081
+ else if (!cursorKnown) return;
2045
2082
  else if (typeof def.position === "string") {
2046
2083
  value = resolveLabelPosition(def.position, labels, cursor) ?? cursor;
2047
2084
  } else value = cursor;
@@ -2057,7 +2094,9 @@ function resolveTimelinePositions(anims, labelDefs = []) {
2057
2094
  return;
2058
2095
  }
2059
2096
  const duration = anim.method === "set" ? 0 : anim.duration ?? GSAP_DEFAULT_DURATION;
2060
- const start = resolveAnimStart(anim, cursor, prevStart, labels);
2097
+ const readsCursor = anim.implicitPosition || typeof anim.position !== "number";
2098
+ const start = cursorKnown || !readsCursor ? resolveAnimStart(anim, cursor, prevStart, labels) : null;
2099
+ if (start == null || anim.durationUnresolved) cursorKnown = false;
2061
2100
  if (start != null) {
2062
2101
  anim.resolvedStart = Math.max(0, start);
2063
2102
  prevStart = anim.resolvedStart;
@@ -2067,7 +2106,15 @@ function resolveTimelinePositions(anims, labelDefs = []) {
2067
2106
  while (labelIdx < sortedLabels.length) defineLabel(sortedLabels[labelIdx++]);
2068
2107
  }
2069
2108
  function collectAddLabelDefs(ast, ref, scope, sortedCalls) {
2070
- const callLocs = sortedCalls.map((c) => c.node.callee?.property?.loc?.start);
2109
+ const callKeys = sortedCalls.map((c) => ({
2110
+ site: callSiteStart(c),
2111
+ order: c.node.__hfOrder
2112
+ }));
2113
+ const comesAfter = (site, order, labelSite, labelOrder) => {
2114
+ if (site === void 0) return false;
2115
+ if (site !== labelSite) return site > labelSite;
2116
+ return (order ?? -Infinity) > (labelOrder ?? -Infinity);
2117
+ };
2071
2118
  const defs = [];
2072
2119
  acornWalk.simple(ast, {
2073
2120
  // fallow-ignore-next-line complexity
@@ -2081,12 +2128,11 @@ function collectAddLabelDefs(ast, ref, scope, sortedCalls) {
2081
2128
  if (!name) return;
2082
2129
  const posVal = resolveNode(node.arguments?.[1], scope);
2083
2130
  const position = typeof posVal === "number" || typeof posVal === "string" ? posVal : void 0;
2084
- const labelLoc = callee.property?.loc?.start;
2131
+ const labelStart = node.__hfSiteStart ?? callee.property?.start;
2132
+ const labelOrder = node.__hfOrder;
2085
2133
  let order = sortedCalls.length;
2086
- if (labelLoc) {
2087
- order = callLocs.findIndex(
2088
- (l) => l && (l.line > labelLoc.line || l.line === labelLoc.line && l.column > labelLoc.column)
2089
- );
2134
+ if (labelStart !== void 0) {
2135
+ order = callKeys.findIndex((k) => comesAfter(k.site, k.order, labelStart, labelOrder));
2090
2136
  if (order === -1) order = sortedCalls.length;
2091
2137
  }
2092
2138
  defs.push({ name, position, order });
@@ -2100,13 +2146,17 @@ function compareByLoc(a, b) {
2100
2146
  if (!aLoc || !bLoc) return 0;
2101
2147
  return aLoc.line - bLoc.line || aLoc.column - bLoc.column;
2102
2148
  }
2149
+ function callSiteStart(call) {
2150
+ return call.node.__hfSiteStart ?? call.node.callee?.property?.start;
2151
+ }
2103
2152
  function compareCallOrder(a, b) {
2153
+ const aSite = callSiteStart(a);
2154
+ const bSite = callSiteStart(b);
2155
+ if (aSite !== void 0 && bSite !== void 0 && aSite !== bSite) return aSite - bSite;
2104
2156
  const ao = a.node.__hfOrder;
2105
2157
  const bo = b.node.__hfOrder;
2106
- if (ao === void 0 && bo === void 0) return compareByLoc(a, b);
2107
- if (ao === void 0) return -1;
2108
- if (bo === void 0) return 1;
2109
- return ao - bo;
2158
+ if (ao !== void 0 && bo !== void 0) return ao - bo;
2159
+ return compareByLoc(a, b);
2110
2160
  }
2111
2161
  function sortBySourcePosition(calls) {
2112
2162
  calls.sort(compareCallOrder);
@@ -3428,6 +3478,7 @@ function checkOutputResolutionCompatibility(input) {
3428
3478
 
3429
3479
  // src/gsapUnroll.ts
3430
3480
  import * as acorn2 from "acorn";
3481
+ import * as acornWalk3 from "acorn-walk";
3431
3482
  import MagicString2 from "magic-string";
3432
3483
  function propEntries(props) {
3433
3484
  return Object.entries(props).map(([k, v]) => `${safeJsKey(k)}: ${serializeValue(v)}`);
@@ -3486,37 +3537,140 @@ function enclosingTopLevel(statements, start, end) {
3486
3537
  }
3487
3538
  return null;
3488
3539
  }
3489
- function isHelperDeclNamed(stmt, names) {
3490
- if (stmt.type === "FunctionDeclaration") return names.has(stmt.id?.name);
3491
- if (stmt.type === "VariableDeclaration") {
3492
- return (stmt.declarations ?? []).some((d) => names.has(d.id?.name));
3540
+ function declaresFunction(stmt) {
3541
+ if (stmt.type === "FunctionDeclaration") return true;
3542
+ return stmt.type === "VariableDeclaration" && (stmt.declarations ?? []).some((d) => isFunctionNode(d.init));
3543
+ }
3544
+ function declaredNames(stmt) {
3545
+ if (stmt.type === "FunctionDeclaration") return stmt.id?.name ? [stmt.id.name] : [];
3546
+ if (stmt.type !== "VariableDeclaration") return [];
3547
+ return (stmt.declarations ?? []).flatMap((d) => d.id?.name ? [d.id.name] : []);
3548
+ }
3549
+ function mentionsIdentifier(text, name) {
3550
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3551
+ return new RegExp(`(?<![\\p{ID_Continue}$])${escaped}(?![\\p{ID_Continue}$])`, "u").test(text);
3552
+ }
3553
+ function unreferencedHelperDecls(statements, script, unrolled, helperNames) {
3554
+ let live = statements.filter((stmt) => !unrolled.has(stmt));
3555
+ const dead = [];
3556
+ for (let changed = true; changed; ) {
3557
+ changed = false;
3558
+ for (const decl of live) {
3559
+ const names = declaredNames(decl);
3560
+ if (names.length === 0 || !names.every((n) => helperNames.has(n))) continue;
3561
+ const others = live.filter((s) => s !== decl).map((s) => script.slice(s.start, s.end));
3562
+ if (names.some((n) => others.some((text) => mentionsIdentifier(text, n)))) continue;
3563
+ dead.push(decl);
3564
+ live = live.filter((s) => s !== decl);
3565
+ changed = true;
3566
+ }
3493
3567
  }
3494
- return false;
3568
+ return dead;
3495
3569
  }
3496
- function unrollComputedTimeline(script) {
3497
- const parsed = parseGsapScriptAcorn(script);
3498
- const computed = parsed.animations.filter((a) => isComputed(a) && a.provenance?.sourceRange);
3499
- if (computed.length === 0) return script;
3500
- const statements = topLevelStatements(script);
3570
+ var EFFECT_NODES = /* @__PURE__ */ new Set([
3571
+ "CallExpression",
3572
+ "NewExpression",
3573
+ "UpdateExpression",
3574
+ "AssignmentExpression"
3575
+ ]);
3576
+ function containsEffect(node) {
3577
+ let hit = false;
3578
+ acornWalk3.full(node, (n) => {
3579
+ if (EFFECT_NODES.has(n.type)) hit = true;
3580
+ });
3581
+ return hit;
3582
+ }
3583
+ function isPureTweenChain(expr, timelineVar) {
3584
+ let node = expr;
3585
+ while (node?.type === "CallExpression") {
3586
+ if (!isTimelineRooted(node, timelineVar) || node.arguments.some(containsEffect)) return false;
3587
+ node = node.callee?.object;
3588
+ }
3589
+ return node?.type === "Identifier";
3590
+ }
3591
+ var blockStatements = (block) => block?.type === "BlockStatement" ? block.body : block ? [block] : [];
3592
+ function forEachBody(call) {
3593
+ const callback = call.callee?.property?.name === "forEach" ? call.arguments[0] : null;
3594
+ return callback?.body ? blockStatements(callback.body) : null;
3595
+ }
3596
+ function calledBody(call, helpers) {
3597
+ if (call?.type !== "CallExpression") return null;
3598
+ if (call.callee?.type !== "Identifier") return forEachBody(call);
3599
+ return helpers.get(call.callee.name) ?? null;
3600
+ }
3601
+ function bodyOf(stmt, helpers) {
3602
+ if (stmt.type === "ExpressionStatement") return calledBody(stmt.expression, helpers);
3603
+ return stmt.body ? blockStatements(stmt.body) : null;
3604
+ }
3605
+ function onlyAddsTweens(stmts, ctx, seen = /* @__PURE__ */ new Set()) {
3606
+ if (seen.has(stmts)) return false;
3607
+ seen.add(stmts);
3608
+ return stmts.every((stmt) => {
3609
+ if (stmt.type === "VariableDeclaration") return !containsEffect(stmt);
3610
+ if (stmt.type === "ExpressionStatement" && isPureTweenChain(stmt.expression, ctx.timelineVar)) {
3611
+ return true;
3612
+ }
3613
+ const nested = bodyOf(stmt, ctx.helpers);
3614
+ return nested !== null && onlyAddsTweens(nested, ctx, seen);
3615
+ });
3616
+ }
3617
+ function declaredFunctions(stmt) {
3618
+ if (stmt.type === "FunctionDeclaration") {
3619
+ return stmt.id?.name ? [[stmt.id.name, blockStatements(stmt.body)]] : [];
3620
+ }
3621
+ if (stmt.type !== "VariableDeclaration") return [];
3622
+ return stmt.declarations.filter((d) => d.id?.name && isFunctionNode(d.init)).map((d) => [d.id.name, blockStatements(d.init.body)]);
3623
+ }
3624
+ var collectHelperBodies = (statements) => new Map(statements.flatMap(declaredFunctions));
3625
+ function dropStatementsUnsafeToUnroll(byStatement, ctx) {
3626
+ for (const [stmt, anims] of byStatement) {
3627
+ const unknownTiming = anims.some(
3628
+ (a) => a.durationUnresolved || a.resolvedStart === void 0 || a.hasUnresolvedSelector
3629
+ );
3630
+ const bodyStmts = unknownTiming ? null : bodyOf(stmt, ctx.helpers);
3631
+ if (bodyStmts === null || !onlyAddsTweens(bodyStmts, ctx)) byStatement.delete(stmt);
3632
+ }
3633
+ }
3634
+ function groupByTopLevelStatement(computed, statements) {
3501
3635
  const byStatement = /* @__PURE__ */ new Map();
3502
3636
  const helperNames = /* @__PURE__ */ new Set();
3503
3637
  for (const anim of computed) {
3504
- if (anim.provenance?.fn) helperNames.add(anim.provenance.fn);
3505
3638
  const [s, e] = anim.provenance.sourceRange;
3506
3639
  const stmt = enclosingTopLevel(statements, s, e);
3640
+ if (stmt && declaresFunction(stmt)) return null;
3507
3641
  if (!stmt) continue;
3642
+ if (anim.provenance?.fn) helperNames.add(anim.provenance.fn);
3508
3643
  const list = byStatement.get(stmt) ?? [];
3509
3644
  list.push(anim);
3510
3645
  byStatement.set(stmt, list);
3511
3646
  }
3647
+ return { byStatement, helperNames };
3648
+ }
3649
+ function unrollComputedTimeline(script) {
3650
+ const parsed = parseGsapScriptAcorn(script);
3651
+ const computed = parsed.animations.filter((a) => isComputed(a) && a.provenance?.sourceRange);
3652
+ if (computed.length === 0) return script;
3653
+ const statements = topLevelStatements(script);
3654
+ const grouped = groupByTopLevelStatement(computed, statements);
3655
+ if (!grouped) return script;
3656
+ const { byStatement, helperNames } = grouped;
3657
+ dropStatementsUnsafeToUnroll(byStatement, {
3658
+ timelineVar: parsed.timelineVar,
3659
+ helpers: collectHelperBodies(statements)
3660
+ });
3512
3661
  if (byStatement.size === 0) return script;
3513
3662
  const ms = new MagicString2(script);
3514
3663
  for (const [stmt, anims] of byStatement) {
3515
3664
  const literals = anims.map((a) => serializeTweenStatement(parsed.timelineVar, a)).join("\n");
3516
3665
  ms.overwrite(stmt.start, stmt.end, literals);
3517
3666
  }
3518
- for (const stmt of statements) {
3519
- if (isHelperDeclNamed(stmt, helperNames)) ms.remove(stmt.start, stmt.end);
3667
+ for (const decl of unreferencedHelperDecls(
3668
+ statements,
3669
+ script,
3670
+ new Set(byStatement.keys()),
3671
+ helperNames
3672
+ )) {
3673
+ ms.remove(decl.start, decl.end);
3520
3674
  }
3521
3675
  return ms.toString();
3522
3676
  }
@@ -3560,6 +3714,201 @@ function trackKindOf(node) {
3560
3714
  return { kind: "graphics", source: "tag" };
3561
3715
  }
3562
3716
 
3717
+ // src/timingMismatches.ts
3718
+ import { parseHTML as parseHTML2 } from "linkedom";
3719
+ var EPSILON = 0.05;
3720
+ var NON_RENDERED = /* @__PURE__ */ new Set(["script", "style", "template", "noscript", "link", "meta"]);
3721
+ var toNode = (el) => ({
3722
+ tag: el.tagName,
3723
+ attrs: Object.fromEntries(Array.from(el.attributes, (a) => [a.name, a.value])),
3724
+ children: Array.from(el.children, toNode),
3725
+ el
3726
+ });
3727
+ var numeric = (value) => {
3728
+ if (value == null || value.trim() === "") return null;
3729
+ const n = Number(value);
3730
+ return Number.isFinite(n) ? n : null;
3731
+ };
3732
+ function queryAll(root, selector) {
3733
+ try {
3734
+ return Array.from(root.querySelectorAll(selector));
3735
+ } catch {
3736
+ return [];
3737
+ }
3738
+ }
3739
+ var unresolvedReason = (anim) => {
3740
+ if (anim.durationUnresolved) return "duration";
3741
+ if (anim.resolvedStart === void 0) return "position";
3742
+ return anim.extras?.stagger === void 0 ? null : "stagger";
3743
+ };
3744
+ function rowMembership(root) {
3745
+ const rows = topLevelElements(toNode(root));
3746
+ const rowEls = new Set(rows.map((r) => r.el));
3747
+ const insideRow = /* @__PURE__ */ new Set();
3748
+ for (const row of rows) {
3749
+ insideRow.add(row.el);
3750
+ for (const d of Array.from(row.el.querySelectorAll("*"))) insideRow.add(d);
3751
+ }
3752
+ return { rowEls, insideRow };
3753
+ }
3754
+ var attrNumber = (el, name) => numeric(el.getAttribute(name));
3755
+ function windowFinding(el, selector, anim, start, end) {
3756
+ const dataStart = attrNumber(el, "data-start");
3757
+ const duration = attrNumber(el, "data-duration");
3758
+ if (dataStart === null) return null;
3759
+ const dataEnd = duration === null ? null : dataStart + duration;
3760
+ const beforeStart = anim.method !== "set" && start < dataStart - EPSILON;
3761
+ const afterEnd = dataEnd !== null && start > dataEnd + EPSILON;
3762
+ if (!beforeStart && !afterEnd) return null;
3763
+ const elementId = el.getAttribute("id");
3764
+ return {
3765
+ kind: "tween-outside-window",
3766
+ selector,
3767
+ elementId,
3768
+ dataStart,
3769
+ dataEnd,
3770
+ tweenStart: start,
3771
+ tweenEnd: end
3772
+ };
3773
+ }
3774
+ var tweenEnd = (anim) => anim.resolvedStart + (anim.method === "set" ? 0 : anim.duration ?? 0.5);
3775
+ function checkTarget(el, selector, anim, layout, report) {
3776
+ if (!layout.insideRow.has(el)) {
3777
+ if (!NON_RENDERED.has(el.tagName.toLowerCase())) {
3778
+ report.findings.push({
3779
+ kind: "animated-without-timing",
3780
+ selector,
3781
+ elementId: el.getAttribute("id")
3782
+ });
3783
+ }
3784
+ return;
3785
+ }
3786
+ if (!layout.rowEls.has(el) || isSubCompositionHost(toNode(el))) return;
3787
+ if (el.hasAttribute("data-start") && attrNumber(el, "data-start") === null) {
3788
+ report.unresolved.push({ selector, reason: "start-attribute" });
3789
+ return;
3790
+ }
3791
+ const found = windowFinding(el, selector, anim, anim.resolvedStart, tweenEnd(anim));
3792
+ if (found) report.findings.push(found);
3793
+ }
3794
+ function checkTween(anim, root, layout, seenUntimed, report) {
3795
+ const selector = anim.targetSelector;
3796
+ const targets = queryAll(root, selector).filter((el) => el !== root);
3797
+ const reason = unresolvedReason(anim);
3798
+ if (reason) {
3799
+ if (targets.length > 0) report.unresolved.push({ selector, reason });
3800
+ return null;
3801
+ }
3802
+ for (const el of targets) {
3803
+ const loose = !layout.insideRow.has(el);
3804
+ if (loose && seenUntimed.has(el)) continue;
3805
+ if (loose) seenUntimed.add(el);
3806
+ checkTarget(el, selector, anim, layout, report);
3807
+ }
3808
+ return tweenEnd(anim);
3809
+ }
3810
+ function timingMismatches(html) {
3811
+ const report = { findings: [], unresolved: [] };
3812
+ const { document } = parseHTML2(html);
3813
+ const root = document.querySelector("[data-composition-id]");
3814
+ if (!root) return report;
3815
+ const layout = rowMembership(root);
3816
+ const seenUntimed = /* @__PURE__ */ new Set();
3817
+ const scripts = Array.from(document.querySelectorAll("script:not([src])"));
3818
+ const anims = scripts.flatMap((s) => parseGsapScriptAcorn(s.textContent ?? "").animations);
3819
+ const ends = anims.filter((anim) => !anim.global && !anim.hasUnresolvedSelector).map((anim) => checkTween(anim, root, layout, seenUntimed, report));
3820
+ const timelineEnd = Math.max(0, ...ends.filter((e) => e !== null));
3821
+ const rootDuration = attrNumber(root, "data-duration");
3822
+ if (rootDuration !== null && timelineEnd > rootDuration + EPSILON) {
3823
+ report.findings.push({ kind: "timeline-exceeds-root-duration", rootDuration, timelineEnd });
3824
+ }
3825
+ return report;
3826
+ }
3827
+
3828
+ // src/mediaDuration.ts
3829
+ var nonNegative = (raw) => {
3830
+ const value = parseNumeric(raw);
3831
+ return value !== null && value >= 0 ? value : null;
3832
+ };
3833
+ function readMediaOffsetSeconds(getAttr) {
3834
+ return nonNegative(getAttr("data-playback-start")) ?? nonNegative(getAttr("data-media-start")) ?? 0;
3835
+ }
3836
+ var MIN_PLAYBACK_RATE = 0.1;
3837
+ var MAX_PLAYBACK_RATE = 10;
3838
+ function clampPlaybackRate(raw) {
3839
+ return Number.isFinite(raw) && raw > 0 ? Math.max(MIN_PLAYBACK_RATE, Math.min(MAX_PLAYBACK_RATE, raw)) : 1;
3840
+ }
3841
+ function readPlaybackRate(getAttr, fallback = 1) {
3842
+ const authored = Number.parseFloat(getAttr("data-playback-rate") ?? "");
3843
+ return clampPlaybackRate(Number.isFinite(authored) && authored > 0 ? authored : fallback);
3844
+ }
3845
+ function readDataDurationSeconds(getAttr) {
3846
+ const duration = parseNumeric(getAttr("data-duration"));
3847
+ return duration !== null && duration > 0 ? duration : null;
3848
+ }
3849
+ function readAuthoredDurationSeconds(getAttr, startSeconds) {
3850
+ const duration = readDataDurationSeconds(getAttr);
3851
+ if (duration !== null) return duration;
3852
+ const end = parseNumeric(getAttr("data-end"));
3853
+ return end !== null ? end - startSeconds : null;
3854
+ }
3855
+ var DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS = 3;
3856
+ function resolveNaturalDurationSeconds(sourceDurationSeconds, mediaStartSeconds, playbackRate) {
3857
+ if (!Number.isFinite(sourceDurationSeconds)) return null;
3858
+ const remaining = Math.max(0, sourceDurationSeconds - mediaStartSeconds);
3859
+ return remaining / clampPlaybackRate(playbackRate);
3860
+ }
3861
+ function resolveMediaDuration(input) {
3862
+ const { tag, authoredDurationSeconds, sourceDurationSeconds, mediaStartSeconds, playbackRate } = input;
3863
+ if (authoredDurationSeconds !== null && authoredDurationSeconds > 0) {
3864
+ return { seconds: authoredDurationSeconds, source: "authored" };
3865
+ }
3866
+ if (tag === "img") {
3867
+ return { seconds: DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS, source: "default" };
3868
+ }
3869
+ if (sourceDurationSeconds === null) {
3870
+ return { seconds: null, source: "pending", reason: "source duration not yet probed" };
3871
+ }
3872
+ const seconds = resolveNaturalDurationSeconds(
3873
+ sourceDurationSeconds,
3874
+ mediaStartSeconds,
3875
+ playbackRate
3876
+ );
3877
+ if (seconds === null) {
3878
+ return { seconds: null, source: "pending", reason: "source reported a non-finite duration" };
3879
+ }
3880
+ return { seconds, source: "media" };
3881
+ }
3882
+ var PENDING_MEDIA_DURATION_READERS = [
3883
+ "core/src/runtime/init.ts (visibility end, clock, seek, WebAudio scheduling)",
3884
+ "core/src/compiler/timingCompiler.ts compileTag",
3885
+ "core/src/compiler/htmlCompiler.ts compileHtml",
3886
+ "producer/src/services/htmlCompiler.ts resolveMediaDuration",
3887
+ "engine/src/services/videoFrameExtractor.ts (extraction window)",
3888
+ "engine/src/services/audioMixer.ts",
3889
+ "parsers/src/htmlParser.ts (defaults to 5s, reads only data-media-start)",
3890
+ "studio (timelineDOM.ts, timelineElementHelpers.ts, useTimelineSyncCallbacks.ts)"
3891
+ ];
3892
+
3893
+ // src/compositionDuration.ts
3894
+ var isPositiveFinite = (value) => value !== null && Number.isFinite(value) && value > 0;
3895
+ function resolveCompositionDuration(input) {
3896
+ const pendingClips = input.clipEndsSeconds.filter((end) => end === null).length;
3897
+ if (isPositiveFinite(input.authoredDurationSeconds)) {
3898
+ return { seconds: input.authoredDurationSeconds, source: "authored", pendingClips };
3899
+ }
3900
+ const known = input.clipEndsSeconds.filter(isPositiveFinite);
3901
+ if (known.length > 0) {
3902
+ return { seconds: Math.max(...known), source: "derived", pendingClips };
3903
+ }
3904
+ return {
3905
+ seconds: null,
3906
+ source: "unresolved",
3907
+ pendingClips,
3908
+ reason: pendingClips > 0 ? "every clip's length is still pending" : "no authored duration and no clip with a length"
3909
+ };
3910
+ }
3911
+
3563
3912
  // src/utils/urlPath.ts
3564
3913
  function decodeUrlPathVariants(path) {
3565
3914
  const variants = [path];
@@ -3573,7 +3922,7 @@ function decodeUrlPathVariants(path) {
3573
3922
 
3574
3923
  // src/variableUsage.ts
3575
3924
  import * as acorn3 from "acorn";
3576
- import * as acornWalk3 from "acorn-walk";
3925
+ import * as acornWalk4 from "acorn-walk";
3577
3926
  function isGetVariablesCallee(callee) {
3578
3927
  if (callee?.type === "Identifier") return callee.name === "getVariables";
3579
3928
  if (callee?.type === "MemberExpression" && !callee.computed) {
@@ -3650,7 +3999,7 @@ function scanVariableUsage(scriptText) {
3650
3999
  return { usedIds: [], scanIncomplete: true };
3651
4000
  }
3652
4001
  const aliases = /* @__PURE__ */ new Set();
3653
- acornWalk3.ancestor(ast, {
4002
+ acornWalk4.ancestor(ast, {
3654
4003
  CallExpression(node, _, ancestors) {
3655
4004
  if (!isGetVariablesCallee(node.callee)) return;
3656
4005
  const parent = ancestors[ancestors.length - 2];
@@ -3659,7 +4008,7 @@ function scanVariableUsage(scriptText) {
3659
4008
  }
3660
4009
  });
3661
4010
  if (aliases.size > 0) {
3662
- acornWalk3.ancestor(ast, {
4011
+ acornWalk4.ancestor(ast, {
3663
4012
  // fallow-ignore-next-line complexity
3664
4013
  Identifier(node, _, ancestors) {
3665
4014
  if (!aliases.has(String(node.name))) return;
@@ -3677,7 +4026,7 @@ function scanVariableUsage(scriptText) {
3677
4026
 
3678
4027
  // src/mediaSrcMutation.ts
3679
4028
  import * as acorn4 from "acorn";
3680
- import * as acornWalk4 from "acorn-walk";
4029
+ import * as acornWalk5 from "acorn-walk";
3681
4030
  function parseProgram2(script) {
3682
4031
  try {
3683
4032
  return acorn4.parse(script, { ecmaVersion: "latest", sourceType: "script" });
@@ -3712,7 +4061,7 @@ function extractMediaSrcMutations(script) {
3712
4061
  try {
3713
4062
  const ast = parseProgram2(script);
3714
4063
  const bindings = /* @__PURE__ */ new Map();
3715
- acornWalk4.simple(ast, {
4064
+ acornWalk5.simple(ast, {
3716
4065
  VariableDeclarator(node) {
3717
4066
  if (node.id?.type !== "Identifier") return;
3718
4067
  const selector = lookupSelector(node.init, bindings);
@@ -3720,7 +4069,7 @@ function extractMediaSrcMutations(script) {
3720
4069
  }
3721
4070
  });
3722
4071
  const mutations = [];
3723
- acornWalk4.simple(ast, {
4072
+ acornWalk5.simple(ast, {
3724
4073
  AssignmentExpression(node) {
3725
4074
  if (node.operator !== "=" || memberName(node.left) !== "src") return;
3726
4075
  const selector = lookupSelector(node.left.object, bindings);
@@ -3864,6 +4213,7 @@ export {
3864
4213
  ClipTimingWriteError,
3865
4214
  CompositionHtmlParseError,
3866
4215
  DEFAULT_DURATIONS,
4216
+ DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS,
3867
4217
  DERIVED_TIMING_ATTRIBUTES,
3868
4218
  EXCLUDED_TAGS,
3869
4219
  FONT_ALIAS_KEYS,
@@ -3871,6 +4221,9 @@ export {
3871
4221
  HTML_BODY_CSS_HEIGHT_FIRST_RE,
3872
4222
  HTML_BODY_CSS_WIDTH_FIRST_RE,
3873
4223
  LEGACY_TIMING_ATTRIBUTES,
4224
+ MAX_PLAYBACK_RATE,
4225
+ MIN_PLAYBACK_RATE,
4226
+ PENDING_MEDIA_DURATION_READERS,
3874
4227
  PROPERTY_GROUPS,
3875
4228
  SPRING_PRESETS,
3876
4229
  SUPPORTED_EASES,
@@ -3881,6 +4234,7 @@ export {
3881
4234
  addElementToHtml,
3882
4235
  checkOutputResolutionCompatibility,
3883
4236
  checkSubCompositionUsability,
4237
+ clampPlaybackRate,
3884
4238
  classifyPropertyGroup,
3885
4239
  classifyTweenPropertyGroup,
3886
4240
  decodeUrlPathVariants,
@@ -3909,14 +4263,22 @@ export {
3909
4263
  parseNumeric,
3910
4264
  parseStartExpression,
3911
4265
  queryByAttr,
4266
+ readAuthoredDurationSeconds,
3912
4267
  readClipTiming,
4268
+ readDataDurationSeconds,
4269
+ readMediaOffsetSeconds,
4270
+ readPlaybackRate,
3913
4271
  removeElementFromHtml,
3914
4272
  removeElementWithGsapCascade,
3915
4273
  resolveAliasDisplayName,
4274
+ resolveCompositionDuration,
4275
+ resolveMediaDuration,
4276
+ resolveNaturalDurationSeconds,
3916
4277
  resolveResolutionFlagPair,
3917
4278
  scanVariableUsage,
3918
4279
  serializeGsapAnimations,
3919
4280
  suggestMatchingPreset,
4281
+ timingMismatches,
3920
4282
  topLevelElements,
3921
4283
  trackKindOf,
3922
4284
  unrollComputedTimeline,