@hyperframes/core 0.6.98 → 0.6.99

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.
@@ -0,0 +1,980 @@
1
+ // fallow-ignore-file code-duplication
2
+ /**
3
+ * Browser-safe GSAP read path — acorn + acorn-walk.
4
+ *
5
+ * T6b oracle: produces identical ParsedGsap output to gsapParser.ts (recast).
6
+ * Replaces recast as the shared implementation once T6d passes.
7
+ *
8
+ * Write path (T6c) will add magic-string splice once read parity is confirmed.
9
+ * No Node globals, no fs, no require — safe to bundle for browser use.
10
+ */
11
+ import * as acorn from "acorn";
12
+ import * as acornWalk from "acorn-walk";
13
+ import { classifyTweenPropertyGroup } from "./gsapConstants.js";
14
+ const GSAP_METHODS = new Set(["set", "to", "from", "fromTo"]);
15
+ const QUERY_METHODS = new Set(["querySelector", "querySelectorAll"]);
16
+ const ITERATION_METHODS = new Set(["forEach", "map"]);
17
+ const SCOPE_NODE_TYPES = new Set([
18
+ "Program",
19
+ "FunctionDeclaration",
20
+ "FunctionExpression",
21
+ "ArrowFunctionExpression",
22
+ ]);
23
+ // ── Value resolution ─────────────────────────────────────────────────────────
24
+ // fallow-ignore-next-line complexity
25
+ function resolveNode(node, scope) {
26
+ if (!node)
27
+ return undefined;
28
+ if (node.type === "NumericLiteral" || (node.type === "Literal" && typeof node.value === "number"))
29
+ return node.value;
30
+ if (node.type === "StringLiteral" || (node.type === "Literal" && typeof node.value === "string"))
31
+ return node.value;
32
+ if (node.type === "BooleanLiteral" ||
33
+ (node.type === "Literal" && typeof node.value === "boolean"))
34
+ return node.value;
35
+ if (node.type === "UnaryExpression" && node.operator === "-" && node.argument) {
36
+ const val = resolveNode(node.argument, scope);
37
+ return typeof val === "number" ? -val : undefined;
38
+ }
39
+ if (node.type === "BinaryExpression") {
40
+ const left = resolveNode(node.left, scope);
41
+ const right = resolveNode(node.right, scope);
42
+ if (typeof left === "number" && typeof right === "number") {
43
+ switch (node.operator) {
44
+ case "+":
45
+ return left + right;
46
+ case "-":
47
+ return left - right;
48
+ case "*":
49
+ return left * right;
50
+ case "/":
51
+ return right !== 0 ? left / right : undefined;
52
+ }
53
+ }
54
+ if (typeof left === "string" && node.operator === "+")
55
+ return left + String(right ?? "");
56
+ if (typeof right === "string" && node.operator === "+")
57
+ return String(left ?? "") + right;
58
+ }
59
+ if (node.type === "Identifier" && scope.has(node.name)) {
60
+ return scope.get(node.name);
61
+ }
62
+ if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
63
+ return node.quasis?.[0]?.value?.cooked ?? undefined;
64
+ }
65
+ return undefined;
66
+ }
67
+ function extractLiteralValue(node, scope) {
68
+ return resolveNode(node, scope);
69
+ }
70
+ // ── DOM selector resolution ───────────────────────────────────────────────────
71
+ // fallow-ignore-next-line complexity
72
+ function selectorFromQueryCall(node, scope) {
73
+ if (node?.type !== "CallExpression")
74
+ return null;
75
+ const callee = node.callee;
76
+ if (callee?.type !== "MemberExpression" || callee.property?.type !== "Identifier")
77
+ return null;
78
+ const method = callee.property.name;
79
+ const argValue = resolveNode(node.arguments?.[0], scope);
80
+ if (typeof argValue !== "string" || argValue.length === 0)
81
+ return null;
82
+ if (QUERY_METHODS.has(method) || method === "toArray")
83
+ return argValue;
84
+ if (method === "getElementById")
85
+ return `#${argValue}`;
86
+ return null;
87
+ }
88
+ // ── Ancestor-based scope helpers (replaces NodePath walking) ──────────────────
89
+ /**
90
+ * Return the nearest ancestor node whose type is in SCOPE_NODE_TYPES.
91
+ * `ancestors` is the acorn-walk ancestor array (root→current, current is last).
92
+ */
93
+ function enclosingScopeNodeFromAncestors(ancestors) {
94
+ for (let i = ancestors.length - 2; i >= 0; i--) {
95
+ const node = ancestors[i];
96
+ if (node && SCOPE_NODE_TYPES.has(node.type))
97
+ return node;
98
+ }
99
+ return null;
100
+ }
101
+ /** Scope chain innermost-first, derived from the acorn-walk ancestors array. */
102
+ function scopeChainFromAncestors(ancestors) {
103
+ const chain = [];
104
+ for (let i = ancestors.length - 1; i >= 0; i--) {
105
+ const node = ancestors[i];
106
+ if (node && SCOPE_NODE_TYPES.has(node.type))
107
+ chain.push(node);
108
+ }
109
+ return chain;
110
+ }
111
+ // ── Target bindings ───────────────────────────────────────────────────────────
112
+ function addBinding(bindings, scopeNode, name, selector) {
113
+ let scoped = bindings.get(scopeNode);
114
+ if (!scoped) {
115
+ scoped = new Map();
116
+ bindings.set(scopeNode, scoped);
117
+ }
118
+ if (!scoped.has(name))
119
+ scoped.set(name, selector);
120
+ }
121
+ function lookupBindingFromAncestors(name, ancestors, bindings) {
122
+ for (const scopeNode of scopeChainFromAncestors(ancestors)) {
123
+ const selector = bindings.get(scopeNode)?.get(name);
124
+ if (selector !== undefined)
125
+ return selector;
126
+ }
127
+ // Program-scope bindings are stored under null (enclosingScopeNodeFromAncestors
128
+ // returns null when no function wrapper exists — the common case in HF scripts).
129
+ return bindings.get(null)?.get(name) ?? null;
130
+ }
131
+ function isFunctionNode(node) {
132
+ return (node?.type === "ArrowFunctionExpression" ||
133
+ node?.type === "FunctionExpression" ||
134
+ node?.type === "FunctionDeclaration");
135
+ }
136
+ function resolveCollectionSelector(node, ancestors, scope, bindings) {
137
+ if (node?.type === "Identifier")
138
+ return lookupBindingFromAncestors(node.name, ancestors, bindings);
139
+ if (node?.type === "CallExpression")
140
+ return selectorFromQueryCall(node, scope);
141
+ return null;
142
+ }
143
+ function collectScopeBindings(ast) {
144
+ const bindings = new Map();
145
+ acornWalk.simple(ast, {
146
+ VariableDeclarator(node) {
147
+ const name = node.id?.name;
148
+ const init = node.init;
149
+ if (name && init) {
150
+ const val = resolveNode(init, bindings);
151
+ if (val !== undefined)
152
+ bindings.set(name, val);
153
+ }
154
+ },
155
+ });
156
+ return bindings;
157
+ }
158
+ /**
159
+ * Build a lexically-scoped index of element variables → selector.
160
+ * Pass 1: direct DOM-lookup assignments.
161
+ * Pass 2: forEach/map callback params whose collection's selector is known.
162
+ */
163
+ function collectTargetBindings(ast, scope) {
164
+ const bindings = new Map();
165
+ acornWalk.ancestor(ast, {
166
+ VariableDeclarator(node, _, ancestors) {
167
+ const name = node.id?.name;
168
+ const selector = selectorFromQueryCall(node.init, scope);
169
+ if (name && selector !== null) {
170
+ addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), name, selector);
171
+ }
172
+ },
173
+ AssignmentExpression(node, _, ancestors) {
174
+ const left = node.left;
175
+ const selector = selectorFromQueryCall(node.right, scope);
176
+ if (left?.type === "Identifier" && selector !== null) {
177
+ addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), left.name, selector);
178
+ }
179
+ },
180
+ });
181
+ // Pass 2: forEach/map callback params take the collection's selector.
182
+ acornWalk.ancestor(ast, {
183
+ // fallow-ignore-next-line complexity
184
+ CallExpression(node, _, ancestors) {
185
+ const callee = node.callee;
186
+ if (callee?.type === "MemberExpression" &&
187
+ callee.property?.type === "Identifier" &&
188
+ ITERATION_METHODS.has(callee.property.name)) {
189
+ const collectionSelector = resolveCollectionSelector(callee.object, ancestors, scope, bindings);
190
+ const fn = node.arguments?.[0];
191
+ const param = fn?.params?.[0];
192
+ if (collectionSelector && param?.type === "Identifier" && isFunctionNode(fn)) {
193
+ addBinding(bindings, fn, param.name, collectionSelector);
194
+ }
195
+ }
196
+ },
197
+ });
198
+ return bindings;
199
+ }
200
+ // fallow-ignore-next-line complexity
201
+ function resolveTargetSelector(node, ancestors, scope, bindings) {
202
+ if (!node)
203
+ return null;
204
+ if (node.type === "StringLiteral" || node.type === "Literal") {
205
+ return typeof node.value === "string" ? node.value : null;
206
+ }
207
+ if (node.type === "Identifier") {
208
+ return lookupBindingFromAncestors(node.name, ancestors, bindings);
209
+ }
210
+ if (node.type === "CallExpression") {
211
+ return selectorFromQueryCall(node, scope);
212
+ }
213
+ if (node.type === "ArrayExpression") {
214
+ const parts = node.elements
215
+ .map((el) => resolveTargetSelector(el, ancestors, scope, bindings))
216
+ .filter((s) => typeof s === "string" && s.length > 0);
217
+ return parts.length > 0 ? parts.join(", ") : null;
218
+ }
219
+ if (node.type === "MemberExpression" && node.object?.type === "Identifier") {
220
+ return lookupBindingFromAncestors(node.object.name, ancestors, bindings);
221
+ }
222
+ return null;
223
+ }
224
+ // ── ObjectExpression utilities ────────────────────────────────────────────────
225
+ function isObjectProperty(prop) {
226
+ return prop?.type === "ObjectProperty" || prop?.type === "Property";
227
+ }
228
+ function propKeyName(prop) {
229
+ return prop?.key?.name ?? prop?.key?.value;
230
+ }
231
+ function findPropertyNode(varsArgNode, key) {
232
+ if (varsArgNode?.type !== "ObjectExpression")
233
+ return undefined;
234
+ for (const prop of varsArgNode.properties ?? []) {
235
+ if (!isObjectProperty(prop))
236
+ continue;
237
+ if (propKeyName(prop) === key)
238
+ return prop.value;
239
+ }
240
+ return undefined;
241
+ }
242
+ /**
243
+ * Extract raw source text for a property value — the offset-splice primitive.
244
+ * Equivalent to `recast.print(node).code` for unmodified nodes.
245
+ */
246
+ function extractRawPropertySource(varsArgNode, key, source) {
247
+ const node = findPropertyNode(varsArgNode, key);
248
+ return node ? source.slice(node.start, node.end) : undefined;
249
+ }
250
+ // fallow-ignore-next-line complexity
251
+ function objectExpressionToRecord(node, scope, source) {
252
+ const result = {};
253
+ if (node?.type !== "ObjectExpression")
254
+ return result;
255
+ for (const prop of node.properties ?? []) {
256
+ if (!isObjectProperty(prop))
257
+ continue;
258
+ const key = prop.key?.name ?? prop.key?.value;
259
+ if (!key)
260
+ continue;
261
+ const resolved = resolveNode(prop.value, scope);
262
+ if (resolved !== undefined) {
263
+ result[key] = resolved;
264
+ }
265
+ else {
266
+ result[key] = `__raw:${source.slice(prop.value.start, prop.value.end)}`;
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ // ── Timeline detection ────────────────────────────────────────────────────────
272
+ function isGsapTimelineCall(node) {
273
+ return (node?.type === "CallExpression" &&
274
+ node.callee?.type === "MemberExpression" &&
275
+ node.callee.object?.name === "gsap" &&
276
+ node.callee.property?.name === "timeline");
277
+ }
278
+ // fallow-ignore-next-line complexity
279
+ function extractTimelineDefaults(callNode, scope) {
280
+ const arg = callNode.arguments?.[0];
281
+ if (!arg || arg.type !== "ObjectExpression")
282
+ return undefined;
283
+ const defaultsProp = arg.properties?.find((p) => isObjectProperty(p) && propKeyName(p) === "defaults");
284
+ if (!defaultsProp?.value || defaultsProp.value.type !== "ObjectExpression")
285
+ return undefined;
286
+ const result = {};
287
+ for (const prop of defaultsProp.value.properties ?? []) {
288
+ if (!isObjectProperty(prop))
289
+ continue;
290
+ const key = propKeyName(prop);
291
+ const val = resolveNode(prop.value, scope);
292
+ if (key === "ease" && typeof val === "string")
293
+ result.ease = val;
294
+ if (key === "duration" && typeof val === "number")
295
+ result.duration = val;
296
+ }
297
+ return Object.keys(result).length > 0 ? result : undefined;
298
+ }
299
+ function findTimelineVar(ast, scope) {
300
+ let timelineVar = null;
301
+ let timelineCount = 0;
302
+ let defaults;
303
+ const emptyScope = scope ?? new Map();
304
+ acornWalk.simple(ast, {
305
+ VariableDeclarator(node) {
306
+ if (isGsapTimelineCall(node.init)) {
307
+ timelineCount += 1;
308
+ if (!timelineVar) {
309
+ timelineVar = node.id?.name ?? null;
310
+ defaults = extractTimelineDefaults(node.init, emptyScope);
311
+ }
312
+ }
313
+ },
314
+ AssignmentExpression(node) {
315
+ if (isGsapTimelineCall(node.right)) {
316
+ timelineCount += 1;
317
+ if (!timelineVar) {
318
+ const left = node.left;
319
+ if (left?.type === "Identifier")
320
+ timelineVar = left.name;
321
+ defaults = extractTimelineDefaults(node.right, emptyScope);
322
+ }
323
+ }
324
+ },
325
+ });
326
+ return { timelineVar, timelineCount, defaults };
327
+ }
328
+ // ── Tween call collection ─────────────────────────────────────────────────────
329
+ /** Keys stored on dedicated GsapAnimation fields (not in properties/extras). */
330
+ const BUILTIN_VAR_KEYS = new Set(["duration", "ease", "delay"]);
331
+ /** Keys never preserved (callbacks / advanced patterns). */
332
+ const DROPPED_VAR_KEYS = new Set(["onComplete", "onStart", "onUpdate", "onRepeat"]);
333
+ /** Keys that go in `extras` — non-editable GSAP config that must survive round-trips. */
334
+ const EXTRAS_KEYS = new Set([
335
+ "stagger",
336
+ "yoyo",
337
+ "repeat",
338
+ "repeatDelay",
339
+ "snap",
340
+ "overwrite",
341
+ "immediateRender",
342
+ ]);
343
+ /** True when callee chain is rooted at the timeline variable. */
344
+ function isTimelineRootedCall(callNode, timelineVar) {
345
+ let obj = callNode.callee?.object;
346
+ while (obj?.type === "CallExpression") {
347
+ obj = obj.callee?.object;
348
+ }
349
+ return obj?.type === "Identifier" && obj.name === timelineVar;
350
+ }
351
+ /**
352
+ * Pre-order recursive walk for tween collection.
353
+ *
354
+ * acorn-walk is POST-order (visitor fires after children), which reverses
355
+ * chained calls vs recast.types.visit (PRE-order). We need pre-order to
356
+ * match the golden ordering where the outermost chained call appears first.
357
+ */
358
+ function findAllTweenCalls(ast, timelineVar, scope, targetBindings) {
359
+ const results = [];
360
+ // fallow-ignore-next-line complexity
361
+ function visit(node, ancestors) {
362
+ if (!node || typeof node !== "object")
363
+ return;
364
+ const nodeAncestors = [...ancestors, node];
365
+ // Fire BEFORE children (pre-order) so chained outer calls come first.
366
+ if (node.type === "CallExpression") {
367
+ const callee = node.callee;
368
+ if (callee?.type === "MemberExpression" &&
369
+ callee.property?.type === "Identifier" &&
370
+ isTimelineRootedCall(node, timelineVar) &&
371
+ GSAP_METHODS.has(callee.property.name)) {
372
+ const method = callee.property.name;
373
+ const args = node.arguments;
374
+ const selectorValue = args.length >= 1
375
+ ? (resolveTargetSelector(args[0], nodeAncestors, scope, targetBindings) ??
376
+ "__unresolved__")
377
+ : "__unresolved__";
378
+ if (method === "fromTo" && args.length >= 3) {
379
+ results.push({
380
+ node,
381
+ ancestors: nodeAncestors,
382
+ method: "fromTo",
383
+ selector: selectorValue,
384
+ fromArg: args[1],
385
+ varsArg: args[2],
386
+ positionArg: args[3],
387
+ });
388
+ }
389
+ else if (method !== "fromTo" && args.length >= 2) {
390
+ results.push({
391
+ node,
392
+ ancestors: nodeAncestors,
393
+ method: method,
394
+ selector: selectorValue,
395
+ varsArg: args[1],
396
+ positionArg: args[2],
397
+ });
398
+ }
399
+ }
400
+ }
401
+ // Traverse children. Object.keys preserves insertion order, so callee
402
+ // comes before arguments in acorn's CallExpression nodes.
403
+ for (const key of Object.keys(node)) {
404
+ if (key === "type" || key === "start" || key === "end" || key === "loc")
405
+ continue;
406
+ const child = node[key];
407
+ if (Array.isArray(child)) {
408
+ for (const item of child) {
409
+ if (item && typeof item === "object" && item.type)
410
+ visit(item, nodeAncestors);
411
+ }
412
+ }
413
+ else if (child && typeof child === "object" && child.type) {
414
+ visit(child, nodeAncestors);
415
+ }
416
+ }
417
+ }
418
+ visit(ast, []);
419
+ return results;
420
+ }
421
+ // ── Keyframes parsing ─────────────────────────────────────────────────────────
422
+ const PERCENTAGE_KEY_RE = /^(\d+(?:\.\d+)?)%$/;
423
+ function tryResolveStringProp(propValue, scope) {
424
+ const val = resolveNode(propValue, scope);
425
+ return typeof val === "string" ? val : undefined;
426
+ }
427
+ // fallow-ignore-next-line complexity
428
+ function parsePercentageKeyframes(node, scope, source) {
429
+ const keyframes = [];
430
+ let ease;
431
+ let easeEach;
432
+ for (const prop of node.properties ?? []) {
433
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
434
+ continue;
435
+ const key = prop.key?.value ?? prop.key?.name;
436
+ if (typeof key !== "string")
437
+ continue;
438
+ const pctMatch = PERCENTAGE_KEY_RE.exec(key);
439
+ if (pctMatch) {
440
+ const percentage = Number.parseFloat(pctMatch[1] ?? "0");
441
+ const record = objectExpressionToRecord(prop.value, scope, source);
442
+ const properties = {};
443
+ let kfEase;
444
+ for (const [k, v] of Object.entries(record)) {
445
+ if (k === "ease" && typeof v === "string") {
446
+ kfEase = v;
447
+ }
448
+ else if (typeof v === "number" || typeof v === "string") {
449
+ properties[k] = v;
450
+ }
451
+ }
452
+ keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });
453
+ }
454
+ else if (key === "ease") {
455
+ ease = tryResolveStringProp(prop.value, scope) ?? ease;
456
+ }
457
+ else if (key === "easeEach") {
458
+ easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;
459
+ }
460
+ }
461
+ keyframes.sort((a, b) => a.percentage - b.percentage);
462
+ return {
463
+ format: "percentage",
464
+ keyframes,
465
+ ...(ease ? { ease } : {}),
466
+ ...(easeEach ? { easeEach } : {}),
467
+ };
468
+ }
469
+ // fallow-ignore-next-line complexity
470
+ function computeKeyframesTotalDuration(varsNode, scope, source) {
471
+ const kfNode = (varsNode.properties ?? []).find((p) => (p.key?.name ?? p.key?.value) === "keyframes")?.value;
472
+ if (!kfNode || kfNode.type !== "ArrayExpression")
473
+ return undefined;
474
+ let total = 0;
475
+ for (const el of kfNode.elements ?? []) {
476
+ if (!el || el.type !== "ObjectExpression")
477
+ continue;
478
+ const r = objectExpressionToRecord(el, scope, source);
479
+ if (typeof r.duration === "number")
480
+ total += r.duration;
481
+ }
482
+ return total > 0 ? total : undefined;
483
+ }
484
+ // fallow-ignore-next-line complexity
485
+ function parseObjectArrayKeyframes(node, scope, source) {
486
+ const elements = node.elements ?? [];
487
+ const raw = [];
488
+ for (const el of elements) {
489
+ if (!el || el.type !== "ObjectExpression")
490
+ continue;
491
+ const record = objectExpressionToRecord(el, scope, source);
492
+ const properties = {};
493
+ let duration;
494
+ let ease;
495
+ for (const [k, v] of Object.entries(record)) {
496
+ if (k === "duration" && typeof v === "number") {
497
+ duration = v;
498
+ }
499
+ else if (k === "ease" && typeof v === "string") {
500
+ ease = v;
501
+ }
502
+ else if (typeof v === "number" || typeof v === "string") {
503
+ properties[k] = v;
504
+ }
505
+ }
506
+ raw.push({ properties, duration, ease });
507
+ }
508
+ const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);
509
+ const keyframes = [];
510
+ if (totalDuration > 0) {
511
+ let cumulative = 0;
512
+ for (const entry of raw) {
513
+ cumulative += entry.duration ?? 0;
514
+ const percentage = Math.round((cumulative / totalDuration) * 100);
515
+ keyframes.push({
516
+ percentage,
517
+ properties: entry.properties,
518
+ ...(entry.ease ? { ease: entry.ease } : {}),
519
+ });
520
+ }
521
+ }
522
+ else {
523
+ for (let i = 0; i < raw.length; i++) {
524
+ const entry = raw[i];
525
+ if (!entry)
526
+ continue;
527
+ const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;
528
+ keyframes.push({
529
+ percentage,
530
+ properties: entry.properties,
531
+ ...(entry.ease ? { ease: entry.ease } : {}),
532
+ });
533
+ }
534
+ }
535
+ return { format: "object-array", keyframes };
536
+ }
537
+ // fallow-ignore-next-line complexity
538
+ function parseSimpleArrayKeyframes(node, scope) {
539
+ const arrayProps = new Map();
540
+ let ease;
541
+ let easeEach;
542
+ for (const prop of node.properties ?? []) {
543
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
544
+ continue;
545
+ const key = prop.key?.name ?? prop.key?.value;
546
+ if (typeof key !== "string")
547
+ continue;
548
+ if (prop.value?.type === "ArrayExpression") {
549
+ const values = [];
550
+ for (const el of prop.value.elements ?? []) {
551
+ const val = resolveNode(el, scope);
552
+ if (typeof val === "number" || typeof val === "string") {
553
+ values.push(val);
554
+ }
555
+ }
556
+ if (values.length > 0)
557
+ arrayProps.set(key, values);
558
+ }
559
+ else if (key === "ease") {
560
+ ease = tryResolveStringProp(prop.value, scope) ?? ease;
561
+ }
562
+ else if (key === "easeEach") {
563
+ easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;
564
+ }
565
+ }
566
+ const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);
567
+ const keyframes = [];
568
+ for (let i = 0; i < maxLen; i++) {
569
+ const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;
570
+ const properties = {};
571
+ for (const [key, values] of arrayProps) {
572
+ if (i < values.length)
573
+ properties[key] = values[i];
574
+ }
575
+ keyframes.push({ percentage, properties });
576
+ }
577
+ return {
578
+ format: "simple-array",
579
+ keyframes,
580
+ ...(ease ? { ease } : {}),
581
+ ...(easeEach ? { easeEach } : {}),
582
+ };
583
+ }
584
+ // fallow-ignore-next-line complexity
585
+ function parseKeyframesNode(node, scope, source) {
586
+ if (!node)
587
+ return undefined;
588
+ if (node.type === "ArrayExpression") {
589
+ return parseObjectArrayKeyframes(node, scope, source);
590
+ }
591
+ if (node.type !== "ObjectExpression")
592
+ return undefined;
593
+ const props = node.properties ?? [];
594
+ let hasPercentageKey = false;
595
+ let hasArrayValue = false;
596
+ for (const prop of props) {
597
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
598
+ continue;
599
+ const key = prop.key?.value ?? prop.key?.name;
600
+ if (typeof key === "string" && PERCENTAGE_KEY_RE.test(key)) {
601
+ hasPercentageKey = true;
602
+ break;
603
+ }
604
+ if (prop.value?.type === "ArrayExpression") {
605
+ hasArrayValue = true;
606
+ }
607
+ }
608
+ if (hasPercentageKey)
609
+ return parsePercentageKeyframes(node, scope, source);
610
+ if (hasArrayValue)
611
+ return parseSimpleArrayKeyframes(node, scope);
612
+ return undefined;
613
+ }
614
+ // fallow-ignore-next-line complexity
615
+ function parseMotionPathNode(node, scope, source) {
616
+ if (!node)
617
+ return undefined;
618
+ let pathNode;
619
+ let autoRotate = false;
620
+ let curviness = 1;
621
+ let isCubic = false;
622
+ if (node.type === "ObjectExpression") {
623
+ for (const prop of node.properties ?? []) {
624
+ if (!isObjectProperty(prop))
625
+ continue;
626
+ const key = propKeyName(prop);
627
+ if (key === "path")
628
+ pathNode = prop.value;
629
+ else if (key === "autoRotate") {
630
+ const val = resolveNode(prop.value, scope);
631
+ autoRotate = typeof val === "number" ? val : val === true;
632
+ }
633
+ else if (key === "curviness") {
634
+ const val = resolveNode(prop.value, scope);
635
+ if (typeof val === "number")
636
+ curviness = val;
637
+ }
638
+ else if (key === "type") {
639
+ const val = resolveNode(prop.value, scope);
640
+ if (val === "cubic")
641
+ isCubic = true;
642
+ }
643
+ }
644
+ }
645
+ else if (node.type === "ArrayExpression") {
646
+ pathNode = node;
647
+ }
648
+ if (!pathNode || pathNode.type !== "ArrayExpression")
649
+ return undefined;
650
+ const elements = pathNode.elements ?? [];
651
+ const coords = [];
652
+ for (const elem of elements) {
653
+ if (!elem || elem.type !== "ObjectExpression")
654
+ continue;
655
+ const rec = objectExpressionToRecord(elem, scope, source);
656
+ const x = typeof rec.x === "number" ? rec.x : undefined;
657
+ const y = typeof rec.y === "number" ? rec.y : undefined;
658
+ if (x !== undefined && y !== undefined)
659
+ coords.push({ x, y });
660
+ }
661
+ if (coords.length < 2)
662
+ return undefined;
663
+ let waypoints;
664
+ const segments = [];
665
+ if (isCubic && coords.length >= 4) {
666
+ waypoints = [];
667
+ const first = coords[0];
668
+ if (first)
669
+ waypoints.push(first);
670
+ for (let i = 1; i + 2 < coords.length; i += 3) {
671
+ const cp1 = coords[i];
672
+ const cp2 = coords[i + 1];
673
+ const anchor = coords[i + 2];
674
+ if (!cp1 || !cp2 || !anchor)
675
+ continue;
676
+ waypoints.push(anchor);
677
+ segments.push({ curviness, cp1, cp2 });
678
+ }
679
+ }
680
+ else {
681
+ waypoints = coords;
682
+ for (let i = 0; i < waypoints.length - 1; i++) {
683
+ segments.push({ curviness });
684
+ }
685
+ }
686
+ return {
687
+ arcPath: { enabled: true, autoRotate, segments },
688
+ waypoints,
689
+ };
690
+ }
691
+ // ── Animation assembly ────────────────────────────────────────────────────────
692
+ // fallow-ignore-next-line complexity
693
+ function tweenCallToAnimation(call, scope, source) {
694
+ const vars = objectExpressionToRecord(call.varsArg, scope, source);
695
+ const properties = {};
696
+ const extras = {};
697
+ let keyframesData;
698
+ let hasUnresolvedKeyframes = false;
699
+ let motionPathResult;
700
+ for (const [key, val] of Object.entries(vars)) {
701
+ if (BUILTIN_VAR_KEYS.has(key))
702
+ continue;
703
+ if (DROPPED_VAR_KEYS.has(key))
704
+ continue;
705
+ if (key === "keyframes") {
706
+ const kfNode = findPropertyNode(call.varsArg, "keyframes");
707
+ keyframesData = parseKeyframesNode(kfNode, scope, source);
708
+ if (!keyframesData && kfNode)
709
+ hasUnresolvedKeyframes = true;
710
+ continue;
711
+ }
712
+ if (key === "motionPath") {
713
+ const mpNode = findPropertyNode(call.varsArg, "motionPath");
714
+ motionPathResult = parseMotionPathNode(mpNode, scope, source);
715
+ continue;
716
+ }
717
+ if (key === "easeEach")
718
+ continue;
719
+ if (EXTRAS_KEYS.has(key)) {
720
+ const rawSource = extractRawPropertySource(call.varsArg, key, source);
721
+ if (rawSource !== undefined) {
722
+ extras[key] = `__raw:${rawSource}`;
723
+ }
724
+ else if (val !== undefined) {
725
+ extras[key] = val;
726
+ }
727
+ continue;
728
+ }
729
+ if (typeof val === "number" || typeof val === "string") {
730
+ properties[key] = val;
731
+ }
732
+ }
733
+ if (keyframesData && typeof vars.easeEach === "string") {
734
+ keyframesData.easeEach = vars.easeEach;
735
+ }
736
+ if (motionPathResult) {
737
+ const { waypoints } = motionPathResult;
738
+ if (!keyframesData) {
739
+ const kf = waypoints.map((wp, i) => ({
740
+ percentage: waypoints.length > 1 ? Math.round((i / (waypoints.length - 1)) * 100) : 0,
741
+ properties: { x: wp.x, y: wp.y },
742
+ }));
743
+ keyframesData = { format: "percentage", keyframes: kf };
744
+ }
745
+ else {
746
+ const kfs = keyframesData.keyframes;
747
+ if (kfs.length === waypoints.length) {
748
+ for (let i = 0; i < kfs.length; i++) {
749
+ const kf = kfs[i];
750
+ const wp = waypoints[i];
751
+ if (kf && wp) {
752
+ kf.properties.x = wp.x;
753
+ kf.properties.y = wp.y;
754
+ }
755
+ }
756
+ }
757
+ }
758
+ }
759
+ let fromProperties;
760
+ if (call.method === "fromTo" && call.fromArg) {
761
+ fromProperties = {};
762
+ const fromVars = objectExpressionToRecord(call.fromArg, scope, source);
763
+ for (const [key, val] of Object.entries(fromVars)) {
764
+ if (typeof val === "number" || typeof val === "string") {
765
+ fromProperties[key] = val;
766
+ }
767
+ }
768
+ }
769
+ const hasPositionArg = !!call.positionArg;
770
+ const posVal = hasPositionArg ? extractLiteralValue(call.positionArg, scope) : 0;
771
+ const position = typeof posVal === "number" ? posVal : typeof posVal === "string" ? posVal : 0;
772
+ let duration = typeof vars.duration === "number" ? vars.duration : undefined;
773
+ const ease = typeof vars.ease === "string" ? vars.ease : undefined;
774
+ if (duration === undefined && keyframesData) {
775
+ duration = computeKeyframesTotalDuration(call.varsArg, scope, source);
776
+ }
777
+ const anim = {
778
+ targetSelector: call.selector,
779
+ method: call.method,
780
+ position,
781
+ properties,
782
+ fromProperties,
783
+ duration,
784
+ ease,
785
+ };
786
+ if (!hasPositionArg)
787
+ anim.implicitPosition = true;
788
+ let group = classifyTweenPropertyGroup(properties);
789
+ if (!group && keyframesData) {
790
+ const kfProps = {};
791
+ for (const kf of keyframesData.keyframes) {
792
+ for (const k of Object.keys(kf.properties))
793
+ kfProps[k] = true;
794
+ }
795
+ group = classifyTweenPropertyGroup(kfProps);
796
+ }
797
+ if (group)
798
+ anim.propertyGroup = group;
799
+ if (Object.keys(extras).length > 0)
800
+ anim.extras = extras;
801
+ if (keyframesData)
802
+ anim.keyframes = keyframesData;
803
+ if (motionPathResult)
804
+ anim.arcPath = motionPathResult.arcPath;
805
+ if (hasUnresolvedKeyframes)
806
+ anim.hasUnresolvedKeyframes = true;
807
+ if (call.selector === "__unresolved__")
808
+ anim.hasUnresolvedSelector = true;
809
+ return anim;
810
+ }
811
+ // ── Timeline position resolution ─────────────────────────────────────────────
812
+ const GSAP_DEFAULT_DURATION = 0.5;
813
+ // fallow-ignore-next-line complexity
814
+ function resolvePositionString(pos, cursor, prevStart) {
815
+ const trimmed = pos.trim();
816
+ if (trimmed === "")
817
+ return cursor;
818
+ if (trimmed.startsWith("+=")) {
819
+ const n = Number.parseFloat(trimmed.slice(2));
820
+ return Number.isFinite(n) ? cursor + n : null;
821
+ }
822
+ if (trimmed.startsWith("-=")) {
823
+ const n = Number.parseFloat(trimmed.slice(2));
824
+ return Number.isFinite(n) ? cursor - n : null;
825
+ }
826
+ if (trimmed === "<")
827
+ return prevStart;
828
+ if (trimmed === ">")
829
+ return cursor;
830
+ if (trimmed.startsWith("<")) {
831
+ const n = Number.parseFloat(trimmed.slice(1));
832
+ return Number.isFinite(n) ? prevStart + n : null;
833
+ }
834
+ if (trimmed.startsWith(">")) {
835
+ const n = Number.parseFloat(trimmed.slice(1));
836
+ return Number.isFinite(n) ? cursor + n : null;
837
+ }
838
+ const n = Number.parseFloat(trimmed);
839
+ return Number.isFinite(n) ? n : null;
840
+ }
841
+ function applyTimelineDefaults(anims, defaults) {
842
+ if (!defaults)
843
+ return;
844
+ for (const anim of anims) {
845
+ if (anim.method === "set")
846
+ continue;
847
+ if (anim.duration === undefined && defaults.duration !== undefined) {
848
+ anim.duration = defaults.duration;
849
+ }
850
+ if (anim.ease === undefined && defaults.ease !== undefined) {
851
+ anim.ease = defaults.ease;
852
+ }
853
+ }
854
+ }
855
+ function resolveTimelinePositions(anims) {
856
+ let cursor = 0;
857
+ let prevStart = 0;
858
+ for (const anim of anims) {
859
+ const duration = anim.method === "set" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);
860
+ let start;
861
+ if (anim.implicitPosition) {
862
+ start = cursor;
863
+ }
864
+ else if (typeof anim.position === "number") {
865
+ start = anim.position;
866
+ }
867
+ else if (typeof anim.position === "string") {
868
+ start = resolvePositionString(anim.position, cursor, prevStart);
869
+ }
870
+ else {
871
+ start = cursor;
872
+ }
873
+ if (start != null) {
874
+ anim.resolvedStart = Math.max(0, start);
875
+ prevStart = anim.resolvedStart;
876
+ cursor = Math.max(cursor, anim.resolvedStart + duration);
877
+ }
878
+ }
879
+ }
880
+ function sortBySourcePosition(calls) {
881
+ calls.sort((a, b) => {
882
+ const aLoc = a.node.callee?.property?.loc?.start;
883
+ const bLoc = b.node.callee?.property?.loc?.start;
884
+ if (!aLoc || !bLoc)
885
+ return 0;
886
+ return aLoc.line - bLoc.line || aLoc.column - bLoc.column;
887
+ });
888
+ }
889
+ // ── Stable ID generation ──────────────────────────────────────────────────────
890
+ function assignStableIds(anims) {
891
+ const counts = new Map();
892
+ return anims.map((anim) => {
893
+ const posKey = typeof anim.position === "number"
894
+ ? String(Math.round(anim.position * 1000))
895
+ : String(anim.position);
896
+ const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : "";
897
+ const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;
898
+ const count = (counts.get(base) ?? 0) + 1;
899
+ counts.set(base, count);
900
+ const id = count === 1 ? base : `${base}-${count}`;
901
+ return { ...anim, id };
902
+ });
903
+ }
904
+ /**
905
+ * Parse a GSAP script and return internal AST + call nodes for the write path.
906
+ * Consumed by gsapWriterAcorn.ts (magic-string offset-splice).
907
+ */
908
+ export function parseGsapScriptAcornForWrite(script) {
909
+ try {
910
+ const ast = acorn.parse(script, {
911
+ ecmaVersion: "latest",
912
+ sourceType: "script",
913
+ locations: true,
914
+ });
915
+ const scope = collectScopeBindings(ast);
916
+ const targetBindings = collectTargetBindings(ast, scope);
917
+ const detection = findTimelineVar(ast, scope);
918
+ const timelineVar = detection.timelineVar ?? "tl";
919
+ const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);
920
+ sortBySourcePosition(calls);
921
+ const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));
922
+ applyTimelineDefaults(rawAnims, detection.defaults);
923
+ resolveTimelinePositions(rawAnims);
924
+ const animations = assignStableIds(rawAnims);
925
+ const located = calls.map((call, i) => ({
926
+ id: animations[i].id,
927
+ call,
928
+ animation: animations[i],
929
+ }));
930
+ return { ast, timelineVar, hasTimeline: detection.timelineVar !== null, located };
931
+ }
932
+ catch {
933
+ return null;
934
+ }
935
+ }
936
+ // ── Public API ────────────────────────────────────────────────────────────────
937
+ /**
938
+ * Browser-safe equivalent of `parseGsapScript` (gsapParser.ts).
939
+ * Uses acorn + acorn-walk instead of recast + @babel/parser.
940
+ */
941
+ export function parseGsapScriptAcorn(script) {
942
+ try {
943
+ const ast = acorn.parse(script, {
944
+ ecmaVersion: "latest",
945
+ sourceType: "script",
946
+ locations: true,
947
+ });
948
+ const scope = collectScopeBindings(ast);
949
+ const targetBindings = collectTargetBindings(ast, scope);
950
+ const detection = findTimelineVar(ast, scope);
951
+ const timelineVar = detection.timelineVar ?? "tl";
952
+ const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);
953
+ sortBySourcePosition(calls);
954
+ const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));
955
+ applyTimelineDefaults(rawAnims, detection.defaults);
956
+ resolveTimelinePositions(rawAnims);
957
+ const animations = assignStableIds(rawAnims);
958
+ const timelineMatch = script.match(new RegExp(`^[\\s\\S]*?(?:const|let|var)\\s+${timelineVar}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?`));
959
+ const preamble = timelineMatch?.[0] ?? `const ${timelineVar} = gsap.timeline({ paused: true });`;
960
+ const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);
961
+ let postamble = "";
962
+ if (lastCallIdx !== -1) {
963
+ const afterLast = script.slice(lastCallIdx);
964
+ const endOfCall = afterLast.indexOf(";");
965
+ if (endOfCall !== -1) {
966
+ postamble = script.slice(lastCallIdx + endOfCall + 1).trim();
967
+ }
968
+ }
969
+ const result = { animations, timelineVar, preamble, postamble };
970
+ if (detection.timelineCount > 1)
971
+ result.multipleTimelines = true;
972
+ if (detection.timelineCount > 0 && detection.timelineVar === null)
973
+ result.unsupportedTimelinePattern = true;
974
+ return result;
975
+ }
976
+ catch {
977
+ return { animations: [], timelineVar: "tl", preamble: "", postamble: "" };
978
+ }
979
+ }
980
+ //# sourceMappingURL=gsapParserAcorn.js.map