@hyperframes/core 0.6.53 → 0.6.55
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/compiler/htmlBundler.js +1 -1
- package/dist/compiler/htmlBundler.js.map +1 -1
- package/dist/compiler/staticGuard.d.ts +1 -1
- package/dist/compiler/staticGuard.d.ts.map +1 -1
- package/dist/compiler/staticGuard.js +2 -2
- package/dist/compiler/staticGuard.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lint/hyperframeLinter.d.ts +1 -1
- package/dist/lint/hyperframeLinter.d.ts.map +1 -1
- package/dist/lint/hyperframeLinter.js +2 -2
- package/dist/lint/hyperframeLinter.js.map +1 -1
- package/dist/lint/rules/gsap.d.ts +3 -2
- package/dist/lint/rules/gsap.d.ts.map +1 -1
- package/dist/lint/rules/gsap.js +61 -124
- package/dist/lint/rules/gsap.js.map +1 -1
- package/dist/lint/types.d.ts +1 -1
- package/dist/lint/types.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +9 -0
- package/dist/parsers/gsapConstants.d.ts.map +1 -0
- package/dist/parsers/gsapConstants.js +47 -0
- package/dist/parsers/gsapConstants.js.map +1 -0
- package/dist/parsers/gsapParser.d.ts +3 -37
- package/dist/parsers/gsapParser.d.ts.map +1 -1
- package/dist/parsers/gsapParser.js +704 -372
- package/dist/parsers/gsapParser.js.map +1 -1
- package/dist/parsers/gsapSerialize.d.ts +56 -0
- package/dist/parsers/gsapSerialize.d.ts.map +1 -0
- package/dist/parsers/gsapSerialize.js +223 -0
- package/dist/parsers/gsapSerialize.js.map +1 -0
- package/dist/parsers/htmlParser.d.ts.map +1 -1
- package/dist/parsers/htmlParser.js +1 -80
- package/dist/parsers/htmlParser.js.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/sourceMutation.js +8 -5
- package/dist/studio-api/helpers/sourceMutation.js.map +1 -1
- package/dist/studio-api/routes/files.d.ts.map +1 -1
- package/dist/studio-api/routes/files.js +140 -0
- package/dist/studio-api/routes/files.js.map +1 -1
- package/package.json +12 -2
|
@@ -1,415 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only GSAP AST parser. Depends on recast / @babel/parser, which compile
|
|
3
|
+
* to CommonJS that calls `require("fs")` — so this module must never be in the
|
|
4
|
+
* static import graph of isomorphic/browser code. It is reachable only via the
|
|
5
|
+
* `@hyperframes/core/gsap-parser` subpath (studio-api mutations + the linter).
|
|
6
|
+
*
|
|
7
|
+
* Recast-free helpers (serialization, keyframe conversion, validation, types)
|
|
8
|
+
* live in `./gsapSerialize` and are re-exported here so this subpath exposes the
|
|
9
|
+
* full surface for tests and server-side consumers.
|
|
10
|
+
*/
|
|
11
|
+
import * as recast from "recast";
|
|
12
|
+
import { parse as babelParse } from "@babel/parser";
|
|
13
|
+
export { serializeGsapAnimations, getAnimationsForElementId, validateCompositionGsap, keyframesToGsapAnimations, gsapAnimationsToKeyframes, SUPPORTED_PROPS, SUPPORTED_EASES, } from "./gsapSerialize";
|
|
1
14
|
const GSAP_METHODS = new Set(["set", "to", "from", "fromTo"]);
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"power2.out",
|
|
22
|
-
"power2.inOut",
|
|
23
|
-
"power3.in",
|
|
24
|
-
"power3.out",
|
|
25
|
-
"power3.inOut",
|
|
26
|
-
"power4.in",
|
|
27
|
-
"power4.out",
|
|
28
|
-
"power4.inOut",
|
|
29
|
-
"back.in",
|
|
30
|
-
"back.out",
|
|
31
|
-
"back.inOut",
|
|
32
|
-
"elastic.in",
|
|
33
|
-
"elastic.out",
|
|
34
|
-
"elastic.inOut",
|
|
35
|
-
"bounce.in",
|
|
36
|
-
"bounce.out",
|
|
37
|
-
"bounce.inOut",
|
|
38
|
-
"expo.in",
|
|
39
|
-
"expo.out",
|
|
40
|
-
"expo.inOut",
|
|
41
|
-
];
|
|
42
|
-
function parseObjectLiteral(str) {
|
|
43
|
-
const result = {};
|
|
44
|
-
const cleaned = str.replace(/^\{|\}$/g, "").trim();
|
|
45
|
-
if (!cleaned)
|
|
46
|
-
return result;
|
|
47
|
-
const propRegex = /(\w+)\s*:\s*("[^"]*"|'[^']*'|[\d.]+|[a-zA-Z_][\w.]*)/g;
|
|
48
|
-
let match;
|
|
49
|
-
while ((match = propRegex.exec(cleaned)) !== null) {
|
|
50
|
-
const key = match[1] ?? "";
|
|
51
|
-
let value = match[2] ?? "";
|
|
52
|
-
if (typeof value === "string") {
|
|
53
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
54
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
55
|
-
value = value.slice(1, -1);
|
|
15
|
+
function parseScript(script) {
|
|
16
|
+
return recast.parse(script, {
|
|
17
|
+
parser: {
|
|
18
|
+
parse(source) {
|
|
19
|
+
return babelParse(source, { sourceType: "script", plugins: [], tokens: true });
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function collectScopeBindings(ast) {
|
|
25
|
+
const bindings = new Map();
|
|
26
|
+
recast.types.visit(ast, {
|
|
27
|
+
visitVariableDeclarator(path) {
|
|
28
|
+
const name = path.node.id?.name;
|
|
29
|
+
const init = path.node.init;
|
|
30
|
+
if (name && init) {
|
|
31
|
+
const val = resolveNode(init, bindings);
|
|
32
|
+
if (val !== undefined)
|
|
33
|
+
bindings.set(name, val);
|
|
56
34
|
}
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
this.traverse(path);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return bindings;
|
|
39
|
+
}
|
|
40
|
+
function resolveNode(node, scope) {
|
|
41
|
+
if (!node)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (node.type === "NumericLiteral" || (node.type === "Literal" && typeof node.value === "number"))
|
|
44
|
+
return node.value;
|
|
45
|
+
if (node.type === "StringLiteral" || (node.type === "Literal" && typeof node.value === "string"))
|
|
46
|
+
return node.value;
|
|
47
|
+
if (node.type === "BooleanLiteral" ||
|
|
48
|
+
(node.type === "Literal" && typeof node.value === "boolean"))
|
|
49
|
+
return node.value;
|
|
50
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument) {
|
|
51
|
+
const val = resolveNode(node.argument, scope);
|
|
52
|
+
return typeof val === "number" ? -val : undefined;
|
|
53
|
+
}
|
|
54
|
+
if (node.type === "BinaryExpression") {
|
|
55
|
+
const left = resolveNode(node.left, scope);
|
|
56
|
+
const right = resolveNode(node.right, scope);
|
|
57
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
58
|
+
switch (node.operator) {
|
|
59
|
+
case "+":
|
|
60
|
+
return left + right;
|
|
61
|
+
case "-":
|
|
62
|
+
return left - right;
|
|
63
|
+
case "*":
|
|
64
|
+
return left * right;
|
|
65
|
+
case "/":
|
|
66
|
+
return right !== 0 ? left / right : undefined;
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
|
-
|
|
69
|
+
if (typeof left === "string" && node.operator === "+")
|
|
70
|
+
return left + String(right ?? "");
|
|
71
|
+
if (typeof right === "string" && node.operator === "+")
|
|
72
|
+
return String(left ?? "") + right;
|
|
62
73
|
}
|
|
63
|
-
|
|
74
|
+
if (node.type === "Identifier" && scope.has(node.name)) {
|
|
75
|
+
return scope.get(node.name);
|
|
76
|
+
}
|
|
77
|
+
if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
|
|
78
|
+
return node.quasis?.[0]?.value?.cooked ?? undefined;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
64
81
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
function extractLiteralValue(node, scope) {
|
|
83
|
+
return resolveNode(node, scope);
|
|
84
|
+
}
|
|
85
|
+
// ── Element-target resolution ───────────────────────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// Real compositions target tweens through element variables resolved from the
|
|
88
|
+
// DOM (`const kicker = root.querySelector(".kicker"); tl.to(kicker, …)`), arrays
|
|
89
|
+
// of them (`tl.to([a, b], …)`), `gsap.utils.toArray(".sel")`, and per-element
|
|
90
|
+
// loop variables (`items.forEach(el => tl.to(el, …))`) — not inline string
|
|
91
|
+
// selectors. To make those tweens editable we resolve each target back to the
|
|
92
|
+
// CSS selector(s) it addresses. Resolution is lexically scoped: the same
|
|
93
|
+
// variable name can mean different elements in different IIFEs.
|
|
94
|
+
const QUERY_METHODS = new Set(["querySelector", "querySelectorAll"]);
|
|
95
|
+
const ITERATION_METHODS = new Set(["forEach", "map"]);
|
|
96
|
+
const SCOPE_NODE_TYPES = new Set([
|
|
97
|
+
"Program",
|
|
98
|
+
"FunctionDeclaration",
|
|
99
|
+
"FunctionExpression",
|
|
100
|
+
"ArrowFunctionExpression",
|
|
101
|
+
]);
|
|
102
|
+
/**
|
|
103
|
+
* If `node` is a DOM lookup call — `x.querySelector(".sel")`,
|
|
104
|
+
* `document.querySelectorAll(".sel")`, `document.getElementById("id")`, or
|
|
105
|
+
* `gsap.utils.toArray(".sel")` — return the CSS selector it resolves to.
|
|
106
|
+
* `getElementById("id")` maps to `#id`. Returns null for anything else.
|
|
107
|
+
*/
|
|
108
|
+
function selectorFromQueryCall(node, scope) {
|
|
109
|
+
if (node?.type !== "CallExpression")
|
|
110
|
+
return null;
|
|
111
|
+
const callee = node.callee;
|
|
112
|
+
if (callee?.type !== "MemberExpression" || callee.property?.type !== "Identifier")
|
|
113
|
+
return null;
|
|
114
|
+
const method = callee.property.name;
|
|
115
|
+
const argValue = resolveNode(node.arguments?.[0], scope);
|
|
116
|
+
if (typeof argValue !== "string" || argValue.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
if (QUERY_METHODS.has(method) || method === "toArray")
|
|
119
|
+
return argValue;
|
|
120
|
+
if (method === "getElementById")
|
|
121
|
+
return `#${argValue}`;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/** The nearest enclosing function/program node — the binding scope of `path`. */
|
|
125
|
+
function enclosingScopeNode(path) {
|
|
126
|
+
let p = path?.parentPath;
|
|
127
|
+
while (p) {
|
|
128
|
+
if (SCOPE_NODE_TYPES.has(p.node?.type))
|
|
129
|
+
return p.node;
|
|
130
|
+
p = p.parentPath;
|
|
75
131
|
}
|
|
76
|
-
return
|
|
132
|
+
return null;
|
|
77
133
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
/** Scope nodes enclosing `path`, innermost first. */
|
|
135
|
+
function scopeChainOf(path) {
|
|
136
|
+
const chain = [];
|
|
137
|
+
let p = path;
|
|
138
|
+
while (p) {
|
|
139
|
+
if (SCOPE_NODE_TYPES.has(p.node?.type))
|
|
140
|
+
chain.push(p.node);
|
|
141
|
+
p = p.parentPath;
|
|
142
|
+
}
|
|
143
|
+
return chain;
|
|
144
|
+
}
|
|
145
|
+
function addBinding(bindings, scopeNode, name, selector) {
|
|
146
|
+
let scoped = bindings.get(scopeNode);
|
|
147
|
+
if (!scoped) {
|
|
148
|
+
scoped = new Map();
|
|
149
|
+
bindings.set(scopeNode, scoped);
|
|
150
|
+
}
|
|
151
|
+
if (!scoped.has(name))
|
|
152
|
+
scoped.set(name, selector);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build a lexically-scoped index of element variables → selector. Two passes:
|
|
156
|
+
* (1) direct DOM-lookup assignments (`const x = root.querySelector(...)`), then
|
|
157
|
+
* (2) iteration callback params (`coll.forEach(el => …)`), whose element type is
|
|
158
|
+
* the collection's selector — resolved against the pass-1 bindings.
|
|
159
|
+
*/
|
|
160
|
+
function collectTargetBindings(ast, scope) {
|
|
161
|
+
const bindings = new Map();
|
|
162
|
+
recast.types.visit(ast, {
|
|
163
|
+
visitVariableDeclarator(path) {
|
|
164
|
+
const name = path.node.id?.name;
|
|
165
|
+
const selector = selectorFromQueryCall(path.node.init, scope);
|
|
166
|
+
if (name && selector !== null)
|
|
167
|
+
addBinding(bindings, enclosingScopeNode(path), name, selector);
|
|
168
|
+
this.traverse(path);
|
|
169
|
+
},
|
|
170
|
+
visitAssignmentExpression(path) {
|
|
171
|
+
const left = path.node.left;
|
|
172
|
+
const selector = selectorFromQueryCall(path.node.right, scope);
|
|
173
|
+
if (left?.type === "Identifier" && selector !== null) {
|
|
174
|
+
addBinding(bindings, enclosingScopeNode(path), left.name, selector);
|
|
175
|
+
}
|
|
176
|
+
this.traverse(path);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
// Pass 2: forEach/map callback params take the collection's selector.
|
|
180
|
+
recast.types.visit(ast, {
|
|
181
|
+
visitCallExpression(path) {
|
|
182
|
+
const node = path.node;
|
|
183
|
+
const callee = node.callee;
|
|
184
|
+
if (callee?.type === "MemberExpression" &&
|
|
185
|
+
callee.property?.type === "Identifier" &&
|
|
186
|
+
ITERATION_METHODS.has(callee.property.name)) {
|
|
187
|
+
const collectionSelector = resolveCollectionSelector(callee.object, path, scope, bindings);
|
|
188
|
+
const fn = node.arguments?.[0];
|
|
189
|
+
const param = fn?.params?.[0];
|
|
190
|
+
if (collectionSelector && param?.type === "Identifier" && isFunctionNode(fn)) {
|
|
191
|
+
addBinding(bindings, fn, param.name, collectionSelector);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.traverse(path);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return bindings;
|
|
198
|
+
}
|
|
199
|
+
function isFunctionNode(node) {
|
|
200
|
+
return (node?.type === "ArrowFunctionExpression" ||
|
|
201
|
+
node?.type === "FunctionExpression" ||
|
|
202
|
+
node?.type === "FunctionDeclaration");
|
|
203
|
+
}
|
|
204
|
+
/** Resolve the selector a `.forEach`/`.map` is iterating over (variable or inline call). */
|
|
205
|
+
function resolveCollectionSelector(node, callPath, scope, bindings) {
|
|
206
|
+
if (node?.type === "Identifier")
|
|
207
|
+
return lookupBinding(node.name, callPath, bindings);
|
|
208
|
+
if (node?.type === "CallExpression")
|
|
209
|
+
return selectorFromQueryCall(node, scope);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/** Resolve a variable name to its selector using the lexical scope chain of `path`. */
|
|
213
|
+
function lookupBinding(name, path, bindings) {
|
|
214
|
+
for (const scopeNode of scopeChainOf(path)) {
|
|
215
|
+
const selector = bindings.get(scopeNode)?.get(name);
|
|
216
|
+
if (selector !== undefined)
|
|
217
|
+
return selector;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolve a tween's first argument to a CSS selector. Handles inline string
|
|
223
|
+
* literals, element variables (lexically scoped), arrays of elements (joined
|
|
224
|
+
* into a CSS group selector), inline DOM lookup / `toArray` calls, and indexed
|
|
225
|
+
* access (`items[i]`). Returns null when the target can't be resolved
|
|
226
|
+
* statically (e.g. an object-target duration anchor `tl.to({ _: 0 }, …)`, or a
|
|
227
|
+
* runtime-computed selector).
|
|
228
|
+
*/
|
|
229
|
+
function resolveTargetSelector(node, path, scope, bindings) {
|
|
230
|
+
if (!node)
|
|
231
|
+
return null;
|
|
232
|
+
if (node.type === "StringLiteral" || node.type === "Literal") {
|
|
233
|
+
return typeof node.value === "string" ? node.value : null;
|
|
234
|
+
}
|
|
235
|
+
if (node.type === "Identifier") {
|
|
236
|
+
return lookupBinding(node.name, path, bindings);
|
|
237
|
+
}
|
|
238
|
+
if (node.type === "CallExpression") {
|
|
239
|
+
return selectorFromQueryCall(node, scope);
|
|
240
|
+
}
|
|
241
|
+
if (node.type === "ArrayExpression") {
|
|
242
|
+
const parts = node.elements
|
|
243
|
+
.map((el) => resolveTargetSelector(el, path, scope, bindings))
|
|
244
|
+
.filter((s) => typeof s === "string" && s.length > 0);
|
|
245
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
246
|
+
}
|
|
247
|
+
if (node.type === "MemberExpression" && node.object?.type === "Identifier") {
|
|
248
|
+
// `items[i]` — the element type is the collection's selector.
|
|
249
|
+
return lookupBinding(node.object.name, path, bindings);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
function objectExpressionToRecord(node, scope) {
|
|
254
|
+
const result = {};
|
|
255
|
+
if (node?.type !== "ObjectExpression")
|
|
256
|
+
return result;
|
|
257
|
+
for (const prop of node.properties ?? []) {
|
|
258
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property")
|
|
259
|
+
continue;
|
|
260
|
+
const key = prop.key?.name ?? prop.key?.value;
|
|
261
|
+
if (!key)
|
|
92
262
|
continue;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (animation) {
|
|
97
|
-
animations.push(animation);
|
|
263
|
+
const resolved = resolveNode(prop.value, scope);
|
|
264
|
+
if (resolved !== undefined) {
|
|
265
|
+
result[key] = resolved;
|
|
98
266
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (lastAnimIdx !== -1) {
|
|
103
|
-
const afterLastAnim = script.slice(lastAnimIdx);
|
|
104
|
-
const endOfCall = afterLastAnim.indexOf(";");
|
|
105
|
-
if (endOfCall !== -1) {
|
|
106
|
-
postamble = script.slice(lastAnimIdx + endOfCall + 1).trim();
|
|
267
|
+
else {
|
|
268
|
+
// Preserve unresolvable values as raw source text so they survive round-trips
|
|
269
|
+
result[key] = `__raw:${recast.print(prop.value).code}`;
|
|
107
270
|
}
|
|
108
271
|
}
|
|
109
|
-
return
|
|
272
|
+
return result;
|
|
110
273
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
274
|
+
// ── Timeline Variable Detection ─────────────────────────────────────────────
|
|
275
|
+
function isGsapTimelineCall(node) {
|
|
276
|
+
return (node?.type === "CallExpression" &&
|
|
277
|
+
node.callee?.type === "MemberExpression" &&
|
|
278
|
+
node.callee.object?.name === "gsap" &&
|
|
279
|
+
node.callee.property?.name === "timeline");
|
|
280
|
+
}
|
|
281
|
+
function findTimelineVar(ast) {
|
|
282
|
+
let timelineVar = null;
|
|
283
|
+
let timelineCount = 0;
|
|
284
|
+
recast.types.visit(ast, {
|
|
285
|
+
visitVariableDeclarator(path) {
|
|
286
|
+
if (isGsapTimelineCall(path.node.init)) {
|
|
287
|
+
timelineCount += 1;
|
|
288
|
+
if (!timelineVar)
|
|
289
|
+
timelineVar = path.node.id?.name ?? null;
|
|
290
|
+
}
|
|
291
|
+
this.traverse(path);
|
|
292
|
+
},
|
|
293
|
+
visitAssignmentExpression(path) {
|
|
294
|
+
if (isGsapTimelineCall(path.node.right)) {
|
|
295
|
+
timelineCount += 1;
|
|
296
|
+
if (!timelineVar) {
|
|
297
|
+
const left = path.node.left;
|
|
298
|
+
if (left?.type === "Identifier")
|
|
299
|
+
timelineVar = left.name;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.traverse(path);
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
return { timelineVar, timelineCount };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* True when the member chain of `callNode.callee` is rooted at the timeline
|
|
309
|
+
* variable — `tl.to(...)` and every link of a chain `tl.to(...).to(...)`.
|
|
310
|
+
*/
|
|
311
|
+
function isTimelineRootedCall(callNode, timelineVar) {
|
|
312
|
+
let obj = callNode.callee?.object;
|
|
313
|
+
while (obj?.type === "CallExpression") {
|
|
314
|
+
obj = obj.callee?.object;
|
|
136
315
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
316
|
+
return obj?.type === "Identifier" && obj.name === timelineVar;
|
|
317
|
+
}
|
|
318
|
+
function findAllTweenCalls(ast, timelineVar, scope, targetBindings) {
|
|
319
|
+
const results = [];
|
|
320
|
+
recast.types.visit(ast, {
|
|
321
|
+
visitCallExpression(path) {
|
|
322
|
+
const node = path.node;
|
|
323
|
+
const callee = node.callee;
|
|
324
|
+
if (callee?.type === "MemberExpression" &&
|
|
325
|
+
callee.property?.type === "Identifier" &&
|
|
326
|
+
isTimelineRootedCall(node, timelineVar)) {
|
|
327
|
+
const method = callee.property.name;
|
|
328
|
+
if (!GSAP_METHODS.has(method)) {
|
|
329
|
+
this.traverse(path);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const args = node.arguments;
|
|
333
|
+
if (args.length < 2) {
|
|
334
|
+
this.traverse(path);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const selectorValue = resolveTargetSelector(args[0], path, scope, targetBindings);
|
|
338
|
+
if (!selectorValue) {
|
|
339
|
+
this.traverse(path);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (method === "fromTo") {
|
|
343
|
+
results.push({
|
|
344
|
+
path,
|
|
345
|
+
node,
|
|
346
|
+
method: "fromTo",
|
|
347
|
+
selector: selectorValue,
|
|
348
|
+
fromArg: args[1],
|
|
349
|
+
varsArg: args[2],
|
|
350
|
+
positionArg: args[3],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
results.push({
|
|
355
|
+
path,
|
|
356
|
+
node,
|
|
357
|
+
method: method,
|
|
358
|
+
selector: selectorValue,
|
|
359
|
+
varsArg: args[1],
|
|
360
|
+
positionArg: args[2],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.traverse(path);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
return results;
|
|
368
|
+
}
|
|
369
|
+
/** Keys that are stored on dedicated GsapAnimation fields (not in properties/extras). */
|
|
370
|
+
const BUILTIN_VAR_KEYS = new Set(["duration", "ease", "delay"]);
|
|
371
|
+
/** Keys that are never preserved (callbacks / advanced patterns). */
|
|
372
|
+
const DROPPED_VAR_KEYS = new Set(["keyframes", "onComplete", "onStart", "onUpdate", "onRepeat"]);
|
|
373
|
+
/** Keys that belong in `extras` — non-editable GSAP config that must survive round-trips. */
|
|
374
|
+
const EXTRAS_KEYS = new Set([
|
|
375
|
+
"stagger",
|
|
376
|
+
"yoyo",
|
|
377
|
+
"repeat",
|
|
378
|
+
"repeatDelay",
|
|
379
|
+
"snap",
|
|
380
|
+
"overwrite",
|
|
381
|
+
"immediateRender",
|
|
382
|
+
]);
|
|
383
|
+
/**
|
|
384
|
+
* Extract raw source text for a property in an ObjectExpression AST node.
|
|
385
|
+
* Returns the printed source of the value node, suitable for verbatim re-emission.
|
|
386
|
+
*/
|
|
387
|
+
function extractRawPropertySource(varsArgNode, key) {
|
|
388
|
+
if (varsArgNode?.type !== "ObjectExpression")
|
|
389
|
+
return undefined;
|
|
390
|
+
for (const prop of varsArgNode.properties ?? []) {
|
|
391
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property")
|
|
392
|
+
continue;
|
|
393
|
+
const propKey = prop.key?.name ?? prop.key?.value;
|
|
394
|
+
if (propKey === key) {
|
|
395
|
+
return recast.print(prop.value).code;
|
|
146
396
|
}
|
|
147
397
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
function tweenCallToAnimation(call, scope) {
|
|
401
|
+
const vars = objectExpressionToRecord(call.varsArg, scope);
|
|
402
|
+
const properties = {};
|
|
403
|
+
const extras = {};
|
|
404
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
405
|
+
if (BUILTIN_VAR_KEYS.has(key))
|
|
406
|
+
continue;
|
|
407
|
+
if (DROPPED_VAR_KEYS.has(key))
|
|
408
|
+
continue;
|
|
409
|
+
if (EXTRAS_KEYS.has(key)) {
|
|
410
|
+
// For extras, prefer the raw AST source so complex objects like
|
|
411
|
+
// `stagger: { each: 0.15, from: "start" }` survive verbatim.
|
|
412
|
+
const rawSource = extractRawPropertySource(call.varsArg, key);
|
|
413
|
+
if (rawSource !== undefined) {
|
|
414
|
+
extras[key] = `__raw:${rawSource}`;
|
|
415
|
+
}
|
|
416
|
+
else if (val !== undefined) {
|
|
417
|
+
extras[key] = val;
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (typeof val === "number" || typeof val === "string") {
|
|
422
|
+
properties[key] = val;
|
|
154
423
|
}
|
|
155
424
|
}
|
|
156
|
-
let
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
425
|
+
let fromProperties;
|
|
426
|
+
if (call.method === "fromTo" && call.fromArg) {
|
|
427
|
+
fromProperties = {};
|
|
428
|
+
const fromVars = objectExpressionToRecord(call.fromArg, scope);
|
|
429
|
+
for (const [key, val] of Object.entries(fromVars)) {
|
|
430
|
+
if (typeof val === "number" || typeof val === "string") {
|
|
431
|
+
fromProperties[key] = val;
|
|
162
432
|
}
|
|
163
433
|
}
|
|
164
434
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
435
|
+
const posVal = call.positionArg ? extractLiteralValue(call.positionArg, scope) : 0;
|
|
436
|
+
const position = typeof posVal === "number" ? posVal : typeof posVal === "string" ? posVal : 0;
|
|
437
|
+
const duration = typeof vars.duration === "number" ? vars.duration : undefined;
|
|
438
|
+
const ease = typeof vars.ease === "string" ? vars.ease : undefined;
|
|
439
|
+
const anim = {
|
|
440
|
+
targetSelector: call.selector,
|
|
441
|
+
method: call.method,
|
|
169
442
|
position,
|
|
170
|
-
properties
|
|
171
|
-
fromProperties
|
|
443
|
+
properties,
|
|
444
|
+
fromProperties,
|
|
172
445
|
duration,
|
|
173
446
|
ease,
|
|
174
447
|
};
|
|
448
|
+
if (Object.keys(extras).length > 0)
|
|
449
|
+
anim.extras = extras;
|
|
450
|
+
return anim;
|
|
451
|
+
}
|
|
452
|
+
// ── Stable ID Generation ───────────────────────────────────────────────────
|
|
453
|
+
function assignStableIds(anims) {
|
|
454
|
+
const counts = new Map();
|
|
455
|
+
return anims.map((anim) => {
|
|
456
|
+
const posKey = typeof anim.position === "number"
|
|
457
|
+
? String(Math.round(anim.position * 1000))
|
|
458
|
+
: String(anim.position);
|
|
459
|
+
const base = `${anim.targetSelector}-${anim.method}-${posKey}`;
|
|
460
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
461
|
+
counts.set(base, count);
|
|
462
|
+
const id = count === 1 ? base : `${base}-${count}`;
|
|
463
|
+
return { ...anim, id };
|
|
464
|
+
});
|
|
175
465
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Parse a script to its recast AST plus the located tween calls. The mutation
|
|
468
|
+
* functions reuse this so they can edit the exact call node in place (recast
|
|
469
|
+
* preserves all surrounding source — interleaved `gsap.set`, element variable
|
|
470
|
+
* declarations, the IIFE wrapper, comments and formatting).
|
|
471
|
+
*/
|
|
472
|
+
function parseGsapAst(script) {
|
|
473
|
+
const ast = parseScript(script);
|
|
474
|
+
const scope = collectScopeBindings(ast);
|
|
475
|
+
const targetBindings = collectTargetBindings(ast, scope);
|
|
476
|
+
const detection = findTimelineVar(ast);
|
|
477
|
+
const timelineVar = detection.timelineVar ?? "tl";
|
|
478
|
+
const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);
|
|
479
|
+
const animations = assignStableIds(calls.map((call) => tweenCallToAnimation(call, scope)));
|
|
480
|
+
const located = animations.map((animation, i) => ({
|
|
481
|
+
id: animation.id,
|
|
482
|
+
call: calls[i],
|
|
483
|
+
animation,
|
|
484
|
+
}));
|
|
485
|
+
return { ast, scope, timelineVar, detection, located };
|
|
486
|
+
}
|
|
487
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
488
|
+
export function parseGsapScript(script) {
|
|
489
|
+
try {
|
|
490
|
+
const { detection, timelineVar, located } = parseGsapAst(script);
|
|
491
|
+
const animations = located.map((l) => l.animation);
|
|
492
|
+
const timelineMatch = script.match(new RegExp(`^[\\s\\S]*?(?:const|let|var)\\s+${timelineVar}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?`));
|
|
493
|
+
const preamble = timelineMatch?.[0] ?? `const ${timelineVar} = gsap.timeline({ paused: true });`;
|
|
494
|
+
const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);
|
|
495
|
+
let postamble = "";
|
|
496
|
+
if (lastCallIdx !== -1) {
|
|
497
|
+
const afterLast = script.slice(lastCallIdx);
|
|
498
|
+
const endOfCall = afterLast.indexOf(";");
|
|
499
|
+
if (endOfCall !== -1) {
|
|
500
|
+
postamble = script.slice(lastCallIdx + endOfCall + 1).trim();
|
|
196
501
|
}
|
|
197
502
|
}
|
|
503
|
+
const result = { animations, timelineVar, preamble, postamble };
|
|
504
|
+
if (detection.timelineCount > 1)
|
|
505
|
+
result.multipleTimelines = true;
|
|
506
|
+
if (detection.timelineCount > 0 && detection.timelineVar === null)
|
|
507
|
+
result.unsupportedTimelinePattern = true;
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return { animations: [], timelineVar: "tl", preamble: "", postamble: "" };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ── In-place AST mutation helpers ───────────────────────────────────────────
|
|
515
|
+
//
|
|
516
|
+
// Edits operate directly on the located call's AST node and reprint via recast,
|
|
517
|
+
// which preserves every untouched statement. This is what lets us edit tweens
|
|
518
|
+
// in real compositions (variable targets, interleaved `gsap.set`, IIFE wrapper)
|
|
519
|
+
// without regenerating — and discarding — the surrounding code.
|
|
520
|
+
/** Render a model value to the JS source it should emit as. Mirrors gsapSerialize. */
|
|
521
|
+
function valueToCode(value) {
|
|
522
|
+
if (typeof value === "string" && value.startsWith("__raw:"))
|
|
523
|
+
return value.slice(6);
|
|
524
|
+
if (typeof value === "string")
|
|
525
|
+
return JSON.stringify(value);
|
|
526
|
+
return String(value);
|
|
527
|
+
}
|
|
528
|
+
function safeKey(key) {
|
|
529
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Parse a value/expression snippet into a standalone AST expression node.
|
|
533
|
+
* Uses an assignment (`__hf__ = <code>`) rather than wrapping in parens so an
|
|
534
|
+
* object literal parses as an expression without recast re-emitting the
|
|
535
|
+
* surrounding parentheses.
|
|
536
|
+
*/
|
|
537
|
+
function parseExpr(code) {
|
|
538
|
+
return parseScript(`__hf__ = ${code};`).program.body[0].expression.right;
|
|
539
|
+
}
|
|
540
|
+
function propKeyName(prop) {
|
|
541
|
+
return prop?.key?.name ?? prop?.key?.value;
|
|
542
|
+
}
|
|
543
|
+
function isObjectProperty(prop) {
|
|
544
|
+
return prop?.type === "ObjectProperty" || prop?.type === "Property";
|
|
545
|
+
}
|
|
546
|
+
/** A key the inspector treats as an editable transform/style property. */
|
|
547
|
+
function isEditablePropertyKey(key) {
|
|
548
|
+
return !BUILTIN_VAR_KEYS.has(key) && !DROPPED_VAR_KEYS.has(key) && !EXTRAS_KEYS.has(key);
|
|
549
|
+
}
|
|
550
|
+
function makeObjectProperty(key, value) {
|
|
551
|
+
const obj = parseExpr(`{ ${safeKey(key)}: ${valueToCode(value)} }`);
|
|
552
|
+
return obj.properties[0];
|
|
553
|
+
}
|
|
554
|
+
/** Set (or insert) a single key on an ObjectExpression, preserving sibling keys. */
|
|
555
|
+
function setVarsKey(varsArg, key, value) {
|
|
556
|
+
if (varsArg?.type !== "ObjectExpression")
|
|
557
|
+
return;
|
|
558
|
+
const existing = varsArg.properties.find((p) => isObjectProperty(p) && propKeyName(p) === key);
|
|
559
|
+
if (existing) {
|
|
560
|
+
existing.value = parseExpr(valueToCode(value));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
varsArg.properties.push(makeObjectProperty(key, value));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Replace the editable-property keys on an ObjectExpression with `newProps`,
|
|
568
|
+
* leaving `duration`, `ease`, `stagger`, callbacks and other non-editable keys
|
|
569
|
+
* untouched.
|
|
570
|
+
*/
|
|
571
|
+
function reconcileEditableProperties(varsArg, newProps) {
|
|
572
|
+
if (varsArg?.type !== "ObjectExpression")
|
|
573
|
+
return;
|
|
574
|
+
// Drop editable props no longer present.
|
|
575
|
+
varsArg.properties = varsArg.properties.filter((p) => {
|
|
576
|
+
if (!isObjectProperty(p))
|
|
577
|
+
return true;
|
|
578
|
+
const key = propKeyName(p);
|
|
579
|
+
if (typeof key !== "string")
|
|
580
|
+
return true;
|
|
581
|
+
if (!isEditablePropertyKey(key))
|
|
582
|
+
return true;
|
|
583
|
+
return key in newProps;
|
|
198
584
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
585
|
+
// Upsert each new prop, preserving the order keys first appeared.
|
|
586
|
+
for (const [key, value] of Object.entries(newProps)) {
|
|
587
|
+
setVarsKey(varsArg, key, value);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function applyUpdatesToCall(call, updates) {
|
|
591
|
+
if (updates.properties)
|
|
592
|
+
reconcileEditableProperties(call.varsArg, updates.properties);
|
|
593
|
+
if (updates.fromProperties && call.method === "fromTo") {
|
|
594
|
+
reconcileEditableProperties(call.fromArg, updates.fromProperties);
|
|
595
|
+
}
|
|
596
|
+
if (updates.duration !== undefined)
|
|
597
|
+
setVarsKey(call.varsArg, "duration", updates.duration);
|
|
598
|
+
if (updates.ease !== undefined)
|
|
599
|
+
setVarsKey(call.varsArg, "ease", updates.ease);
|
|
600
|
+
if (updates.position !== undefined) {
|
|
601
|
+
const posIdx = call.method === "fromTo" ? 3 : 2;
|
|
602
|
+
call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/** Walk up to the enclosing ExpressionStatement path (for prune / insertAfter). */
|
|
606
|
+
function findStatementPath(path) {
|
|
607
|
+
let p = path;
|
|
608
|
+
while (p) {
|
|
609
|
+
if (p.node?.type === "ExpressionStatement")
|
|
610
|
+
return p;
|
|
611
|
+
p = p.parentPath;
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
/** Build the source for a single `tl.method(selector, vars, position)` call. */
|
|
616
|
+
function buildTweenStatementCode(timelineVar, anim) {
|
|
617
|
+
const selector = JSON.stringify(anim.targetSelector);
|
|
618
|
+
const props = { ...anim.properties };
|
|
619
|
+
// `set` is instantaneous — GSAP ignores duration on it, so don't emit one.
|
|
620
|
+
if (anim.method !== "set" && anim.duration !== undefined)
|
|
621
|
+
props.duration = anim.duration;
|
|
622
|
+
if (anim.ease)
|
|
623
|
+
props.ease = anim.ease;
|
|
624
|
+
const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
625
|
+
if (anim.extras) {
|
|
626
|
+
for (const [k, v] of Object.entries(anim.extras)) {
|
|
627
|
+
entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
|
|
231
628
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
629
|
+
}
|
|
630
|
+
const objCode = `{ ${entries.join(", ")} }`;
|
|
631
|
+
const posCode = valueToCode(typeof anim.position === "number" ? anim.position : (anim.position ?? 0));
|
|
632
|
+
if (anim.method === "fromTo") {
|
|
633
|
+
const fromEntries = Object.entries(anim.fromProperties ?? {}).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
|
634
|
+
const fromCode = `{ ${fromEntries.join(", ")} }`;
|
|
635
|
+
return `${timelineVar}.fromTo(${selector}, ${fromCode}, ${objCode}, ${posCode});`;
|
|
636
|
+
}
|
|
637
|
+
return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;
|
|
235
638
|
}
|
|
236
639
|
export function updateAnimationInScript(script, animationId, updates) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
640
|
+
let parsed;
|
|
641
|
+
try {
|
|
642
|
+
parsed = parseGsapAst(script);
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
console.warn("[gsap-parser] updateAnimationInScript parse failed:", e);
|
|
646
|
+
return script;
|
|
647
|
+
}
|
|
648
|
+
const target = parsed.located.find((l) => l.id === animationId);
|
|
649
|
+
if (!target)
|
|
650
|
+
return script;
|
|
651
|
+
applyUpdatesToCall(target.call, updates);
|
|
652
|
+
return recast.print(parsed.ast).code;
|
|
245
653
|
}
|
|
246
654
|
export function addAnimationToScript(script, animation) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parsed.animations.push(newAnim);
|
|
251
|
-
return {
|
|
252
|
-
script: serializeGsapAnimations(parsed.animations, parsed.timelineVar),
|
|
253
|
-
id,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
export function removeAnimationFromScript(script, animationId) {
|
|
257
|
-
const parsed = parseGsapScript(script);
|
|
258
|
-
const filtered = parsed.animations.filter((a) => a.id !== animationId);
|
|
259
|
-
return serializeGsapAnimations(filtered, parsed.timelineVar);
|
|
260
|
-
}
|
|
261
|
-
export function getAnimationsForElement(animations, elementId) {
|
|
262
|
-
const selector = `#${elementId}`;
|
|
263
|
-
return animations.filter((a) => a.targetSelector === selector);
|
|
264
|
-
}
|
|
265
|
-
const FORBIDDEN_GSAP_PATTERNS = [
|
|
266
|
-
{ pattern: /\.call\s*\(/, message: "call() method not allowed" },
|
|
267
|
-
{
|
|
268
|
-
pattern: /\.add\s*\(\s*function/,
|
|
269
|
-
message: "add(function) not allowed",
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
pattern: /\.add\s*\(\s*\(/,
|
|
273
|
-
message: "add() with arrow function not allowed",
|
|
274
|
-
},
|
|
275
|
-
{ pattern: /onComplete\s*:/, message: "onComplete callback not allowed" },
|
|
276
|
-
{ pattern: /onStart\s*:/, message: "onStart callback not allowed" },
|
|
277
|
-
{ pattern: /onUpdate\s*:/, message: "onUpdate callback not allowed" },
|
|
278
|
-
{
|
|
279
|
-
pattern: /onRepeat\s*:/,
|
|
280
|
-
message: "onRepeat callback not allowed",
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
pattern: /onReverseComplete\s*:/,
|
|
284
|
-
message: "onReverseComplete callback not allowed",
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
pattern: /repeat\s*:\s*-1/,
|
|
288
|
-
message: "Infinite repeat (repeat: -1) not allowed",
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
pattern: /Math\.random\s*\(/,
|
|
292
|
-
message: "Random values (Math.random) not allowed",
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
pattern: /Date\.now\s*\(/,
|
|
296
|
-
message: "Date-dependent values (Date.now) not allowed",
|
|
297
|
-
},
|
|
298
|
-
{ pattern: /new\s+Date\s*\(/, message: "Date constructor not allowed" },
|
|
299
|
-
{ pattern: /setTimeout\s*\(/, message: "setTimeout not allowed" },
|
|
300
|
-
{ pattern: /setInterval\s*\(/, message: "setInterval not allowed" },
|
|
301
|
-
{
|
|
302
|
-
pattern: /requestAnimationFrame\s*\(/,
|
|
303
|
-
message: "requestAnimationFrame not allowed",
|
|
304
|
-
},
|
|
305
|
-
];
|
|
306
|
-
export function validateCompositionGsap(script) {
|
|
307
|
-
const errors = [];
|
|
308
|
-
const warnings = [];
|
|
309
|
-
for (const { pattern, message } of FORBIDDEN_GSAP_PATTERNS) {
|
|
310
|
-
if (pattern.test(script)) {
|
|
311
|
-
errors.push(message);
|
|
312
|
-
}
|
|
655
|
+
let parsed;
|
|
656
|
+
try {
|
|
657
|
+
parsed = parseGsapAst(script);
|
|
313
658
|
}
|
|
314
|
-
|
|
315
|
-
|
|
659
|
+
catch (e) {
|
|
660
|
+
console.warn("[gsap-parser] addAnimationToScript parse failed:", e);
|
|
661
|
+
return { script, id: "" };
|
|
316
662
|
}
|
|
317
|
-
|
|
318
|
-
|
|
663
|
+
// Nothing to anchor against and no timeline to target — treat as parse failure.
|
|
664
|
+
if (parsed.located.length === 0 && parsed.detection.timelineVar === null) {
|
|
665
|
+
return { script, id: "" };
|
|
319
666
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
667
|
+
const id = `anim-${Date.now()}`;
|
|
668
|
+
const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);
|
|
669
|
+
const newStatement = parseScript(statementCode).program.body[0];
|
|
670
|
+
const lastCall = parsed.located[parsed.located.length - 1]?.call;
|
|
671
|
+
const anchorPath = lastCall
|
|
672
|
+
? findStatementPath(lastCall.path)
|
|
673
|
+
: findTimelineDeclarationPath(parsed.ast, parsed.timelineVar);
|
|
674
|
+
if (anchorPath) {
|
|
675
|
+
anchorPath.insertAfter(newStatement);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
parsed.ast.program.body.push(newStatement);
|
|
679
|
+
}
|
|
680
|
+
return { script: recast.print(parsed.ast).code, id };
|
|
325
681
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const position = prevKf ? elementStartTime + prevKf.time : absoluteTime;
|
|
338
|
-
const properties = {};
|
|
339
|
-
for (const [key, value] of Object.entries(kf.properties)) {
|
|
340
|
-
if (typeof value !== "number")
|
|
341
|
-
continue;
|
|
342
|
-
if (key === "x")
|
|
343
|
-
properties.x = baseX + value;
|
|
344
|
-
else if (key === "y")
|
|
345
|
-
properties.y = baseY + value;
|
|
346
|
-
else if (key === "scale")
|
|
347
|
-
properties.scale = baseScale * value;
|
|
348
|
-
else
|
|
349
|
-
properties[key] = value;
|
|
350
|
-
}
|
|
351
|
-
animations.push({
|
|
352
|
-
id: `${elementId}-kf-${kf.id}`,
|
|
353
|
-
targetSelector: `#${elementId}`,
|
|
354
|
-
method: isFirst ? "set" : "to",
|
|
355
|
-
position,
|
|
356
|
-
properties,
|
|
357
|
-
duration: isFirst ? undefined : duration,
|
|
358
|
-
ease: kf.ease,
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
return animations;
|
|
362
|
-
}
|
|
363
|
-
export function gsapAnimationsToKeyframes(animations, elementStartTime, options) {
|
|
364
|
-
const validMethods = ["set", "to", "from", "fromTo"];
|
|
365
|
-
const baseX = options?.baseX ?? 0;
|
|
366
|
-
const baseY = options?.baseY ?? 0;
|
|
367
|
-
const baseScale = options?.baseScale ?? 1;
|
|
368
|
-
const clampTimeToZero = options?.clampTimeToZero ?? true;
|
|
369
|
-
const skipBaseSet = options?.skipBaseSet ?? false;
|
|
370
|
-
const baseTimeEpsilon = 0.001;
|
|
371
|
-
const baseValueEpsilon = 0.00001;
|
|
372
|
-
return animations
|
|
373
|
-
.filter((a) => validMethods.includes(a.method))
|
|
374
|
-
.map((a) => {
|
|
375
|
-
const relativeTimeRaw = a.position - elementStartTime;
|
|
376
|
-
const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;
|
|
377
|
-
const properties = {};
|
|
378
|
-
for (const [key, value] of Object.entries(a.properties)) {
|
|
379
|
-
if (SUPPORTED_PROPS.includes(key) && typeof value === "number") {
|
|
380
|
-
if (key === "x") {
|
|
381
|
-
properties.x = value - baseX;
|
|
382
|
-
}
|
|
383
|
-
else if (key === "y") {
|
|
384
|
-
properties.y = value - baseY;
|
|
385
|
-
}
|
|
386
|
-
else if (key === "scale") {
|
|
387
|
-
properties.scale =
|
|
388
|
-
baseScale !== 0 ? value / baseScale : value;
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
properties[key] = value;
|
|
682
|
+
/** Find the statement path of `const <timelineVar> = gsap.timeline(...)`. */
|
|
683
|
+
function findTimelineDeclarationPath(ast, timelineVar) {
|
|
684
|
+
let found = null;
|
|
685
|
+
recast.types.visit(ast, {
|
|
686
|
+
visitVariableDeclaration(path) {
|
|
687
|
+
if (found)
|
|
688
|
+
return false;
|
|
689
|
+
for (const decl of path.node.declarations ?? []) {
|
|
690
|
+
if (decl.id?.name === timelineVar && isGsapTimelineCall(decl.init)) {
|
|
691
|
+
found = path;
|
|
692
|
+
return false;
|
|
392
693
|
}
|
|
393
694
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
695
|
+
this.traverse(path);
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
return found;
|
|
699
|
+
}
|
|
700
|
+
/** Find the call that chains off `targetNode` (i.e. whose callee object IS it). */
|
|
701
|
+
function findChainParentCall(stmtNode, targetNode) {
|
|
702
|
+
let found = null;
|
|
703
|
+
recast.types.visit(stmtNode, {
|
|
704
|
+
visitCallExpression(p) {
|
|
705
|
+
if (found)
|
|
706
|
+
return false;
|
|
707
|
+
if (p.node.callee?.type === "MemberExpression" && p.node.callee.object === targetNode) {
|
|
708
|
+
found = p.node;
|
|
709
|
+
return false;
|
|
405
710
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
711
|
+
this.traverse(p);
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
return found;
|
|
715
|
+
}
|
|
716
|
+
export function removeAnimationFromScript(script, animationId) {
|
|
717
|
+
let parsed;
|
|
718
|
+
try {
|
|
719
|
+
parsed = parseGsapAst(script);
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
console.warn("[gsap-parser] removeAnimationFromScript parse failed:", e);
|
|
723
|
+
return script;
|
|
724
|
+
}
|
|
725
|
+
const target = parsed.located.find((l) => l.id === animationId);
|
|
726
|
+
if (!target)
|
|
727
|
+
return script;
|
|
728
|
+
const node = target.call.node;
|
|
729
|
+
const stmtPath = findStatementPath(target.call.path);
|
|
730
|
+
if (!stmtPath)
|
|
731
|
+
return script;
|
|
732
|
+
const parentCall = findChainParentCall(stmtPath.node, node);
|
|
733
|
+
if (parentCall) {
|
|
734
|
+
// Inner link of a chain — splice it out by re-pointing the next link.
|
|
735
|
+
parentCall.callee.object = node.callee.object;
|
|
736
|
+
}
|
|
737
|
+
else if (node.callee?.object?.type === "CallExpression") {
|
|
738
|
+
// Outermost link of a chain with earlier links — drop just this link.
|
|
739
|
+
stmtPath.node.expression = node.callee.object;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
// Standalone tween — remove the whole statement.
|
|
743
|
+
stmtPath.prune();
|
|
744
|
+
}
|
|
745
|
+
return recast.print(parsed.ast).code;
|
|
414
746
|
}
|
|
415
747
|
//# sourceMappingURL=gsapParser.js.map
|