@hyperframes/core 0.6.52 → 0.6.54
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/generated/runtime-inline.js +1 -1
- package/dist/generated/runtime-inline.js.map +1 -1
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +13 -13
- package/dist/hyperframe.runtime.mjs +13 -13
- 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 +20 -8
- 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 +315 -368
- package/dist/parsers/gsapParser.js.map +1 -1
- package/dist/parsers/gsapSerialize.d.ts +51 -0
- package/dist/parsers/gsapSerialize.d.ts.map +1 -0
- package/dist/parsers/gsapSerialize.js +218 -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 +132 -0
- package/dist/studio-api/routes/files.js.map +1 -1
- package/package.json +12 -2
|
@@ -1,415 +1,362 @@
|
|
|
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
|
+
import { serializeGsapAnimations, } from "./gsapSerialize";
|
|
14
|
+
export { serializeGsapAnimations, getAnimationsForElement, validateCompositionGsap, keyframesToGsapAnimations, gsapAnimationsToKeyframes, SUPPORTED_PROPS, SUPPORTED_EASES, } from "./gsapSerialize";
|
|
1
15
|
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);
|
|
16
|
+
function parseScript(script) {
|
|
17
|
+
return recast.parse(script, {
|
|
18
|
+
parser: {
|
|
19
|
+
parse(source) {
|
|
20
|
+
return babelParse(source, { sourceType: "script", plugins: [], tokens: true });
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function collectScopeBindings(ast) {
|
|
26
|
+
const bindings = new Map();
|
|
27
|
+
recast.types.visit(ast, {
|
|
28
|
+
visitVariableDeclarator(path) {
|
|
29
|
+
const name = path.node.id?.name;
|
|
30
|
+
const init = path.node.init;
|
|
31
|
+
if (name && init) {
|
|
32
|
+
const val = resolveNode(init, bindings);
|
|
33
|
+
if (val !== undefined)
|
|
34
|
+
bindings.set(name, val);
|
|
56
35
|
}
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
this.traverse(path);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return bindings;
|
|
40
|
+
}
|
|
41
|
+
function resolveNode(node, scope) {
|
|
42
|
+
if (!node)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (node.type === "NumericLiteral" || (node.type === "Literal" && typeof node.value === "number"))
|
|
45
|
+
return node.value;
|
|
46
|
+
if (node.type === "StringLiteral" || (node.type === "Literal" && typeof node.value === "string"))
|
|
47
|
+
return node.value;
|
|
48
|
+
if (node.type === "BooleanLiteral" ||
|
|
49
|
+
(node.type === "Literal" && typeof node.value === "boolean"))
|
|
50
|
+
return node.value;
|
|
51
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument) {
|
|
52
|
+
const val = resolveNode(node.argument, scope);
|
|
53
|
+
return typeof val === "number" ? -val : undefined;
|
|
54
|
+
}
|
|
55
|
+
if (node.type === "BinaryExpression") {
|
|
56
|
+
const left = resolveNode(node.left, scope);
|
|
57
|
+
const right = resolveNode(node.right, scope);
|
|
58
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
59
|
+
switch (node.operator) {
|
|
60
|
+
case "+":
|
|
61
|
+
return left + right;
|
|
62
|
+
case "-":
|
|
63
|
+
return left - right;
|
|
64
|
+
case "*":
|
|
65
|
+
return left * right;
|
|
66
|
+
case "/":
|
|
67
|
+
return right !== 0 ? left / right : undefined;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
if (typeof left === "string" && node.operator === "+")
|
|
71
|
+
return left + String(right ?? "");
|
|
72
|
+
if (typeof right === "string" && node.operator === "+")
|
|
73
|
+
return String(left ?? "") + right;
|
|
62
74
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function findMatchingBrace(str, startIndex) {
|
|
66
|
-
let depth = 0;
|
|
67
|
-
for (let i = startIndex; i < str.length; i++) {
|
|
68
|
-
if (str[i] === "{")
|
|
69
|
-
depth++;
|
|
70
|
-
else if (str[i] === "}") {
|
|
71
|
-
depth--;
|
|
72
|
-
if (depth === 0)
|
|
73
|
-
return i;
|
|
74
|
-
}
|
|
75
|
+
if (node.type === "Identifier" && scope.has(node.name)) {
|
|
76
|
+
return scope.get(node.name);
|
|
75
77
|
}
|
|
76
|
-
|
|
78
|
+
if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
|
|
79
|
+
return node.quasis?.[0]?.value?.cooked ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const methodPattern = new RegExp(`${timelineVar}\\.(set|to|from|fromTo)\\s*\\(([^)]+(?:\\{[^}]*\\}[^)]*)+)\\)`, "g");
|
|
88
|
-
let match;
|
|
89
|
-
while ((match = methodPattern.exec(script)) !== null) {
|
|
90
|
-
const rawMethod = match[1];
|
|
91
|
-
if (!rawMethod || !GSAP_METHODS.has(rawMethod))
|
|
83
|
+
function extractLiteralValue(node, scope) {
|
|
84
|
+
return resolveNode(node, scope);
|
|
85
|
+
}
|
|
86
|
+
function objectExpressionToRecord(node, scope) {
|
|
87
|
+
const result = {};
|
|
88
|
+
if (node?.type !== "ObjectExpression")
|
|
89
|
+
return result;
|
|
90
|
+
for (const prop of node.properties ?? []) {
|
|
91
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property")
|
|
92
92
|
continue;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
const key = prop.key?.name ?? prop.key?.value;
|
|
94
|
+
if (!key)
|
|
95
|
+
continue;
|
|
96
|
+
const resolved = resolveNode(prop.value, scope);
|
|
97
|
+
if (resolved !== undefined) {
|
|
98
|
+
result[key] = resolved;
|
|
98
99
|
}
|
|
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();
|
|
100
|
+
else {
|
|
101
|
+
// Preserve unresolvable values as raw source text so they survive round-trips
|
|
102
|
+
result[key] = `__raw:${recast.print(prop.value).code}`;
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
|
-
return
|
|
105
|
+
return result;
|
|
110
106
|
}
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
107
|
+
// ── Timeline Variable Detection ─────────────────────────────────────────────
|
|
108
|
+
function isGsapTimelineCall(node) {
|
|
109
|
+
return (node?.type === "CallExpression" &&
|
|
110
|
+
node.callee?.type === "MemberExpression" &&
|
|
111
|
+
node.callee.object?.name === "gsap" &&
|
|
112
|
+
node.callee.property?.name === "timeline");
|
|
113
|
+
}
|
|
114
|
+
function findTimelineVar(ast) {
|
|
115
|
+
let timelineVar = null;
|
|
116
|
+
let timelineCount = 0;
|
|
117
|
+
recast.types.visit(ast, {
|
|
118
|
+
visitVariableDeclarator(path) {
|
|
119
|
+
if (isGsapTimelineCall(path.node.init)) {
|
|
120
|
+
timelineCount += 1;
|
|
121
|
+
if (!timelineVar)
|
|
122
|
+
timelineVar = path.node.id?.name ?? null;
|
|
123
|
+
}
|
|
124
|
+
this.traverse(path);
|
|
125
|
+
},
|
|
126
|
+
visitAssignmentExpression(path) {
|
|
127
|
+
if (isGsapTimelineCall(path.node.right)) {
|
|
128
|
+
timelineCount += 1;
|
|
129
|
+
if (!timelineVar) {
|
|
130
|
+
const left = path.node.left;
|
|
131
|
+
if (left?.type === "Identifier")
|
|
132
|
+
timelineVar = left.name;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.traverse(path);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
return { timelineVar, timelineCount };
|
|
139
|
+
}
|
|
140
|
+
function findAllTweenCalls(ast, timelineVar) {
|
|
141
|
+
const results = [];
|
|
142
|
+
recast.types.visit(ast, {
|
|
143
|
+
visitCallExpression(path) {
|
|
144
|
+
const node = path.node;
|
|
145
|
+
const callee = node.callee;
|
|
146
|
+
if (callee?.type === "MemberExpression" &&
|
|
147
|
+
callee.object?.type === "Identifier" &&
|
|
148
|
+
callee.object.name === timelineVar &&
|
|
149
|
+
callee.property?.type === "Identifier") {
|
|
150
|
+
const method = callee.property.name;
|
|
151
|
+
if (!GSAP_METHODS.has(method)) {
|
|
152
|
+
this.traverse(path);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const args = node.arguments;
|
|
156
|
+
if (args.length < 2) {
|
|
157
|
+
this.traverse(path);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const selectorArg = args[0];
|
|
161
|
+
const selectorValue = selectorArg.type === "StringLiteral" || selectorArg.type === "Literal"
|
|
162
|
+
? String(selectorArg.value)
|
|
163
|
+
: null;
|
|
164
|
+
if (!selectorValue) {
|
|
165
|
+
this.traverse(path);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (method === "fromTo") {
|
|
169
|
+
results.push({
|
|
170
|
+
path,
|
|
171
|
+
node,
|
|
172
|
+
method: "fromTo",
|
|
173
|
+
selector: selectorValue,
|
|
174
|
+
fromArg: args[1],
|
|
175
|
+
varsArg: args[2],
|
|
176
|
+
positionArg: args[3],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
results.push({
|
|
181
|
+
path,
|
|
182
|
+
node,
|
|
183
|
+
method: method,
|
|
184
|
+
selector: selectorValue,
|
|
185
|
+
varsArg: args[1],
|
|
186
|
+
positionArg: args[2],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.traverse(path);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
/** Keys that are stored on dedicated GsapAnimation fields (not in properties/extras). */
|
|
196
|
+
const BUILTIN_VAR_KEYS = new Set(["duration", "ease", "delay"]);
|
|
197
|
+
/** Keys that are never preserved (callbacks / advanced patterns). */
|
|
198
|
+
const DROPPED_VAR_KEYS = new Set(["keyframes", "onComplete", "onStart", "onUpdate", "onRepeat"]);
|
|
199
|
+
/** Keys that belong in `extras` — non-editable GSAP config that must survive round-trips. */
|
|
200
|
+
const EXTRAS_KEYS = new Set([
|
|
201
|
+
"stagger",
|
|
202
|
+
"yoyo",
|
|
203
|
+
"repeat",
|
|
204
|
+
"repeatDelay",
|
|
205
|
+
"snap",
|
|
206
|
+
"overwrite",
|
|
207
|
+
"immediateRender",
|
|
208
|
+
]);
|
|
209
|
+
/**
|
|
210
|
+
* Extract raw source text for a property in an ObjectExpression AST node.
|
|
211
|
+
* Returns the printed source of the value node, suitable for verbatim re-emission.
|
|
212
|
+
*/
|
|
213
|
+
function extractRawPropertySource(varsArgNode, key) {
|
|
214
|
+
if (varsArgNode?.type !== "ObjectExpression")
|
|
215
|
+
return undefined;
|
|
216
|
+
for (const prop of varsArgNode.properties ?? []) {
|
|
217
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property")
|
|
218
|
+
continue;
|
|
219
|
+
const propKey = prop.key?.name ?? prop.key?.value;
|
|
220
|
+
if (propKey === key) {
|
|
221
|
+
return recast.print(prop.value).code;
|
|
146
222
|
}
|
|
147
223
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
function tweenCallToAnimation(call, scope) {
|
|
227
|
+
const vars = objectExpressionToRecord(call.varsArg, scope);
|
|
228
|
+
const properties = {};
|
|
229
|
+
const extras = {};
|
|
230
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
231
|
+
if (BUILTIN_VAR_KEYS.has(key))
|
|
232
|
+
continue;
|
|
233
|
+
if (DROPPED_VAR_KEYS.has(key))
|
|
234
|
+
continue;
|
|
235
|
+
if (EXTRAS_KEYS.has(key)) {
|
|
236
|
+
// For extras, prefer the raw AST source so complex objects like
|
|
237
|
+
// `stagger: { each: 0.15, from: "start" }` survive verbatim.
|
|
238
|
+
const rawSource = extractRawPropertySource(call.varsArg, key);
|
|
239
|
+
if (rawSource !== undefined) {
|
|
240
|
+
extras[key] = `__raw:${rawSource}`;
|
|
241
|
+
}
|
|
242
|
+
else if (val !== undefined) {
|
|
243
|
+
extras[key] = val;
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (typeof val === "number" || typeof val === "string") {
|
|
248
|
+
properties[key] = val;
|
|
154
249
|
}
|
|
155
250
|
}
|
|
156
|
-
let
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
251
|
+
let fromProperties;
|
|
252
|
+
if (call.method === "fromTo" && call.fromArg) {
|
|
253
|
+
fromProperties = {};
|
|
254
|
+
const fromVars = objectExpressionToRecord(call.fromArg, scope);
|
|
255
|
+
for (const [key, val] of Object.entries(fromVars)) {
|
|
256
|
+
if (typeof val === "number" || typeof val === "string") {
|
|
257
|
+
fromProperties[key] = val;
|
|
162
258
|
}
|
|
163
259
|
}
|
|
164
260
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
261
|
+
const posVal = call.positionArg ? extractLiteralValue(call.positionArg, scope) : 0;
|
|
262
|
+
const position = typeof posVal === "number" ? posVal : typeof posVal === "string" ? posVal : 0;
|
|
263
|
+
const duration = typeof vars.duration === "number" ? vars.duration : undefined;
|
|
264
|
+
const ease = typeof vars.ease === "string" ? vars.ease : undefined;
|
|
265
|
+
const anim = {
|
|
266
|
+
targetSelector: call.selector,
|
|
267
|
+
method: call.method,
|
|
169
268
|
position,
|
|
170
|
-
properties
|
|
171
|
-
fromProperties
|
|
269
|
+
properties,
|
|
270
|
+
fromProperties,
|
|
172
271
|
duration,
|
|
173
272
|
ease,
|
|
174
273
|
};
|
|
274
|
+
if (Object.keys(extras).length > 0)
|
|
275
|
+
anim.extras = extras;
|
|
276
|
+
return anim;
|
|
175
277
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return ` ${timelineVar}.set(${selector}, ${propsStr}, ${anim.position});`;
|
|
189
|
-
case "to":
|
|
190
|
-
return ` ${timelineVar}.to(${selector}, ${propsStr}, ${anim.position});`;
|
|
191
|
-
case "from":
|
|
192
|
-
return ` ${timelineVar}.from(${selector}, ${propsStr}, ${anim.position});`;
|
|
193
|
-
case "fromTo": {
|
|
194
|
-
const fromStr = serializeObject(anim.fromProperties || {});
|
|
195
|
-
return ` ${timelineVar}.fromTo(${selector}, ${fromStr}, ${propsStr}, ${anim.position});`;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
278
|
+
// ── Stable ID Generation ───────────────────────────────────────────────────
|
|
279
|
+
function assignStableIds(anims) {
|
|
280
|
+
const counts = new Map();
|
|
281
|
+
return anims.map((anim) => {
|
|
282
|
+
const posKey = typeof anim.position === "number"
|
|
283
|
+
? String(Math.round(anim.position * 1000))
|
|
284
|
+
: String(anim.position);
|
|
285
|
+
const base = `${anim.targetSelector}-${anim.method}-${posKey}`;
|
|
286
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
287
|
+
counts.set(base, count);
|
|
288
|
+
const id = count === 1 ? base : `${base}-${count}`;
|
|
289
|
+
return { ...anim, id };
|
|
198
290
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
291
|
+
}
|
|
292
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
293
|
+
export function parseGsapScript(script) {
|
|
294
|
+
try {
|
|
295
|
+
const ast = parseScript(script);
|
|
296
|
+
const scope = collectScopeBindings(ast);
|
|
297
|
+
const detection = findTimelineVar(ast);
|
|
298
|
+
const timelineVar = detection.timelineVar ?? "tl";
|
|
299
|
+
const calls = findAllTweenCalls(ast, timelineVar);
|
|
300
|
+
const animations = assignStableIds(calls.map((call) => tweenCallToAnimation(call, scope)));
|
|
301
|
+
const timelineMatch = script.match(new RegExp(`^[\\s\\S]*?(?:const|let|var)\\s+${timelineVar}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?`));
|
|
302
|
+
const preamble = timelineMatch?.[0] ?? `const ${timelineVar} = gsap.timeline({ paused: true });`;
|
|
303
|
+
const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);
|
|
304
|
+
let postamble = "";
|
|
305
|
+
if (lastCallIdx !== -1) {
|
|
306
|
+
const afterLast = script.slice(lastCallIdx);
|
|
307
|
+
const endOfCall = afterLast.indexOf(";");
|
|
308
|
+
if (endOfCall !== -1) {
|
|
309
|
+
postamble = script.slice(lastCallIdx + endOfCall + 1).trim();
|
|
310
|
+
}
|
|
218
311
|
}
|
|
219
|
-
|
|
220
|
-
|
|
312
|
+
const result = { animations, timelineVar, preamble, postamble };
|
|
313
|
+
if (detection.timelineCount > 1)
|
|
314
|
+
result.multipleTimelines = true;
|
|
315
|
+
if (detection.timelineCount > 0 && detection.timelineVar === null)
|
|
316
|
+
result.unsupportedTimelinePattern = true;
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return { animations: [], timelineVar: "tl", preamble: "", postamble: "" };
|
|
221
321
|
}
|
|
222
|
-
return `
|
|
223
|
-
const ${timelineVar} = gsap.timeline({ paused: true });
|
|
224
|
-
${lines.join("\n")}${mediaSync}
|
|
225
|
-
`;
|
|
226
322
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return `${key}: "${value}"`;
|
|
231
|
-
}
|
|
232
|
-
return `${key}: ${value}`;
|
|
233
|
-
});
|
|
234
|
-
return `{ ${entries.join(", ")} }`;
|
|
323
|
+
/** Returns true when the parse result is a failure fallback (no animations, no preamble). */
|
|
324
|
+
function isParseFailure(parsed) {
|
|
325
|
+
return parsed.animations.length === 0 && !parsed.preamble;
|
|
235
326
|
}
|
|
236
327
|
export function updateAnimationInScript(script, animationId, updates) {
|
|
237
328
|
const parsed = parseGsapScript(script);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
329
|
+
if (isParseFailure(parsed))
|
|
330
|
+
return script;
|
|
331
|
+
const updated = parsed.animations.map((anim) => anim.id === animationId ? { ...anim, ...updates } : anim);
|
|
332
|
+
return serializeGsapAnimations(updated, parsed.timelineVar, {
|
|
333
|
+
preamble: parsed.preamble,
|
|
334
|
+
postamble: parsed.postamble,
|
|
243
335
|
});
|
|
244
|
-
return serializeGsapAnimations(updated, parsed.timelineVar);
|
|
245
336
|
}
|
|
246
337
|
export function addAnimationToScript(script, animation) {
|
|
247
338
|
const parsed = parseGsapScript(script);
|
|
339
|
+
if (isParseFailure(parsed))
|
|
340
|
+
return { script, id: "" };
|
|
248
341
|
const id = `anim-${Date.now()}`;
|
|
249
342
|
const newAnim = { ...animation, id };
|
|
250
|
-
parsed.animations
|
|
343
|
+
const allAnimations = [...parsed.animations, newAnim];
|
|
251
344
|
return {
|
|
252
|
-
script: serializeGsapAnimations(
|
|
345
|
+
script: serializeGsapAnimations(allAnimations, parsed.timelineVar, {
|
|
346
|
+
preamble: parsed.preamble,
|
|
347
|
+
postamble: parsed.postamble,
|
|
348
|
+
}),
|
|
253
349
|
id,
|
|
254
350
|
};
|
|
255
351
|
}
|
|
256
352
|
export function removeAnimationFromScript(script, animationId) {
|
|
257
353
|
const parsed = parseGsapScript(script);
|
|
354
|
+
if (isParseFailure(parsed))
|
|
355
|
+
return script;
|
|
258
356
|
const filtered = parsed.animations.filter((a) => a.id !== animationId);
|
|
259
|
-
return serializeGsapAnimations(filtered, parsed.timelineVar
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (/yoyo\s*:\s*true/.test(script)) {
|
|
315
|
-
warnings.push("yoyo animations may behave unexpectedly when scrubbing");
|
|
316
|
-
}
|
|
317
|
-
if (/stagger\s*:/.test(script)) {
|
|
318
|
-
warnings.push("stagger animations may not serialize correctly");
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
valid: errors.length === 0,
|
|
322
|
-
errors,
|
|
323
|
-
warnings,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
export function keyframesToGsapAnimations(elementId, keyframes, elementStartTime, base) {
|
|
327
|
-
const sorted = [...keyframes].sort((a, b) => a.time - b.time);
|
|
328
|
-
const animations = [];
|
|
329
|
-
const baseX = base?.x ?? 0;
|
|
330
|
-
const baseY = base?.y ?? 0;
|
|
331
|
-
const baseScale = base?.scale ?? 1;
|
|
332
|
-
sorted.forEach((kf, i) => {
|
|
333
|
-
const absoluteTime = elementStartTime + kf.time;
|
|
334
|
-
const isFirst = i === 0;
|
|
335
|
-
const prevKf = i > 0 ? sorted[i - 1] : null;
|
|
336
|
-
const duration = prevKf ? kf.time - prevKf.time : undefined;
|
|
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
|
-
});
|
|
357
|
+
return serializeGsapAnimations(filtered, parsed.timelineVar, {
|
|
358
|
+
preamble: parsed.preamble,
|
|
359
|
+
postamble: parsed.postamble,
|
|
360
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;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (skipBaseSet && a.method === "set" && Math.abs(time) <= baseTimeEpsilon) {
|
|
396
|
-
const propKeys = Object.keys(properties);
|
|
397
|
-
const isOnlyBaseProps = propKeys.every((k) => k === "x" || k === "y" || k === "scale");
|
|
398
|
-
if (isOnlyBaseProps && propKeys.length > 0) {
|
|
399
|
-
const hasNonBaseOffset = (properties.x !== undefined && Math.abs(properties.x) > baseValueEpsilon) ||
|
|
400
|
-
(properties.y !== undefined && Math.abs(properties.y) > baseValueEpsilon) ||
|
|
401
|
-
(properties.scale !== undefined && Math.abs(properties.scale - 1) > baseValueEpsilon);
|
|
402
|
-
if (!hasNonBaseOffset) {
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const kf = { id: a.id, time, properties };
|
|
408
|
-
if (a.ease !== undefined)
|
|
409
|
-
kf.ease = a.ease;
|
|
410
|
-
return kf;
|
|
411
|
-
})
|
|
412
|
-
.filter((kf) => kf !== null)
|
|
413
|
-
.sort((a, b) => a.time - b.time);
|
|
414
361
|
}
|
|
415
362
|
//# sourceMappingURL=gsapParser.js.map
|