@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.
Files changed (47) hide show
  1. package/dist/compiler/htmlBundler.js +1 -1
  2. package/dist/compiler/htmlBundler.js.map +1 -1
  3. package/dist/compiler/staticGuard.d.ts +1 -1
  4. package/dist/compiler/staticGuard.d.ts.map +1 -1
  5. package/dist/compiler/staticGuard.js +2 -2
  6. package/dist/compiler/staticGuard.js.map +1 -1
  7. package/dist/generated/runtime-inline.js +1 -1
  8. package/dist/generated/runtime-inline.js.map +1 -1
  9. package/dist/hyperframe.manifest.json +1 -1
  10. package/dist/hyperframe.runtime.iife.js +13 -13
  11. package/dist/hyperframe.runtime.mjs +13 -13
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/lint/hyperframeLinter.d.ts +1 -1
  17. package/dist/lint/hyperframeLinter.d.ts.map +1 -1
  18. package/dist/lint/hyperframeLinter.js +2 -2
  19. package/dist/lint/hyperframeLinter.js.map +1 -1
  20. package/dist/lint/rules/gsap.d.ts +3 -2
  21. package/dist/lint/rules/gsap.d.ts.map +1 -1
  22. package/dist/lint/rules/gsap.js +20 -8
  23. package/dist/lint/rules/gsap.js.map +1 -1
  24. package/dist/lint/types.d.ts +1 -1
  25. package/dist/lint/types.d.ts.map +1 -1
  26. package/dist/parsers/gsapConstants.d.ts +9 -0
  27. package/dist/parsers/gsapConstants.d.ts.map +1 -0
  28. package/dist/parsers/gsapConstants.js +47 -0
  29. package/dist/parsers/gsapConstants.js.map +1 -0
  30. package/dist/parsers/gsapParser.d.ts +3 -37
  31. package/dist/parsers/gsapParser.d.ts.map +1 -1
  32. package/dist/parsers/gsapParser.js +315 -368
  33. package/dist/parsers/gsapParser.js.map +1 -1
  34. package/dist/parsers/gsapSerialize.d.ts +51 -0
  35. package/dist/parsers/gsapSerialize.d.ts.map +1 -0
  36. package/dist/parsers/gsapSerialize.js +218 -0
  37. package/dist/parsers/gsapSerialize.js.map +1 -0
  38. package/dist/parsers/htmlParser.d.ts.map +1 -1
  39. package/dist/parsers/htmlParser.js +1 -80
  40. package/dist/parsers/htmlParser.js.map +1 -1
  41. package/dist/studio-api/helpers/sourceMutation.d.ts.map +1 -1
  42. package/dist/studio-api/helpers/sourceMutation.js +8 -5
  43. package/dist/studio-api/helpers/sourceMutation.js.map +1 -1
  44. package/dist/studio-api/routes/files.d.ts.map +1 -1
  45. package/dist/studio-api/routes/files.js +132 -0
  46. package/dist/studio-api/routes/files.js.map +1 -1
  47. 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
- export const SUPPORTED_PROPS = [
3
- "opacity",
4
- "visibility",
5
- "x",
6
- "y",
7
- "scale",
8
- "scaleX",
9
- "scaleY",
10
- "rotation",
11
- "autoAlpha",
12
- "width",
13
- "height",
14
- ];
15
- export const SUPPORTED_EASES = [
16
- "none",
17
- "power1.in",
18
- "power1.out",
19
- "power1.inOut",
20
- "power2.in",
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
- else if (!isNaN(Number(value))) {
58
- value = Number(value);
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
- result[key] = value;
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
- return result;
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
- return -1;
78
+ if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
79
+ return node.quasis?.[0]?.value?.cooked ?? undefined;
80
+ }
81
+ return undefined;
77
82
  }
78
- export function parseGsapScript(script) {
79
- const animations = [];
80
- let idCounter = 0;
81
- const timelineMatch = script.match(/(?:const|let|var)\s+(\w+)\s*=\s*gsap\.timeline/);
82
- const timelineVar = timelineMatch ? (timelineMatch[1] ?? "tl") : "tl";
83
- const preambleMatch = script.match(new RegExp(`^[\\s\\S]*?(?:const|let|var)\\s+${timelineVar}\\s*=\\s*gsap\\.timeline\\s*\\([^)]*\\)\\s*;?`));
84
- const preamble = preambleMatch
85
- ? preambleMatch[0]
86
- : `const ${timelineVar} = gsap.timeline({ paused: true });`;
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 method = rawMethod;
94
- const argsStr = match[2] ?? "";
95
- const animation = parseGsapCall(method, argsStr, ++idCounter);
96
- if (animation) {
97
- animations.push(animation);
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
- const lastAnimIdx = script.lastIndexOf(`${timelineVar}.`);
101
- let postamble = "";
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 { animations, timelineVar, preamble, postamble };
105
+ return result;
110
106
  }
111
- function parseGsapCall(method, argsStr, idNum) {
112
- const selectorMatch = argsStr.match(/^\s*["']([^"']+)["']\s*,/);
113
- if (!selectorMatch)
114
- return null;
115
- const targetSelector = selectorMatch[1] ?? "";
116
- const afterSelector = argsStr.slice(selectorMatch[0].length);
117
- let properties = {};
118
- let fromProperties;
119
- let position = 0;
120
- if (method === "fromTo") {
121
- const firstBrace = afterSelector.indexOf("{");
122
- const firstEnd = findMatchingBrace(afterSelector, firstBrace);
123
- if (firstBrace === -1 || firstEnd === -1)
124
- return null;
125
- fromProperties = parseObjectLiteral(afterSelector.slice(firstBrace, firstEnd + 1));
126
- const secondPart = afterSelector.slice(firstEnd + 1);
127
- const secondBrace = secondPart.indexOf("{");
128
- const secondEnd = findMatchingBrace(secondPart, secondBrace);
129
- if (secondBrace === -1 || secondEnd === -1)
130
- return null;
131
- properties = parseObjectLiteral(secondPart.slice(secondBrace, secondEnd + 1));
132
- const afterProps = secondPart.slice(secondEnd + 1);
133
- const posMatch = afterProps.match(/,\s*([\d.]+)/);
134
- if (posMatch)
135
- position = parseFloat(posMatch[1] ?? "");
136
- }
137
- else {
138
- const braceStart = afterSelector.indexOf("{");
139
- const braceEnd = findMatchingBrace(afterSelector, braceStart);
140
- if (braceStart !== -1 && braceEnd !== -1) {
141
- properties = parseObjectLiteral(afterSelector.slice(braceStart, braceEnd + 1));
142
- const afterProps = afterSelector.slice(braceEnd + 1);
143
- const posMatch = afterProps.match(/,\s*([\d.]+)/);
144
- if (posMatch)
145
- position = parseFloat(posMatch[1] ?? "");
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
- const duration = typeof properties.duration === "number" ? properties.duration : undefined;
149
- const ease = typeof properties.ease === "string" ? properties.ease : undefined;
150
- const filteredProps = {};
151
- for (const [key, value] of Object.entries(properties)) {
152
- if (SUPPORTED_PROPS.includes(key)) {
153
- filteredProps[key] = value;
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 filteredFromProps;
157
- if (fromProperties) {
158
- filteredFromProps = {};
159
- for (const [key, value] of Object.entries(fromProperties)) {
160
- if (SUPPORTED_PROPS.includes(key)) {
161
- filteredFromProps[key] = value;
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
- return {
166
- id: `anim-${idNum}`,
167
- targetSelector,
168
- method,
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: filteredProps,
171
- fromProperties: filteredFromProps,
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
- export function serializeGsapAnimations(animations, timelineVar = "tl", options) {
177
- const sorted = [...animations].sort((a, b) => a.position - b.position);
178
- const lines = sorted.map((anim) => {
179
- const selector = `"${anim.targetSelector}"`;
180
- const props = { ...anim.properties };
181
- if (anim.duration !== undefined)
182
- props.duration = anim.duration;
183
- if (anim.ease)
184
- props.ease = anim.ease;
185
- const propsStr = serializeObject(props);
186
- switch (anim.method) {
187
- case "set":
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
- let mediaSync = "";
200
- if (options?.includeMediaSync) {
201
- mediaSync = `
202
- // Sync media playback
203
- ${timelineVar}.eventCallback("onUpdate", function() {
204
- const time = ${timelineVar}.time();
205
- document.querySelectorAll("video[data-start], audio[data-start]").forEach(function(media) {
206
- const start = parseFloat(media.dataset.start);
207
- const end = parseFloat(media.dataset.end) || Infinity;
208
- const mediaTime = time - start;
209
- if (time >= start && time < end) {
210
- if (Math.abs(media.currentTime - mediaTime) > 0.1) {
211
- media.currentTime = mediaTime;
212
- }
213
- if (media.paused && !${timelineVar}.paused()) {
214
- media.play().catch(function() {});
215
- }
216
- } else if (!media.paused) {
217
- media.pause();
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
- function serializeObject(obj) {
228
- const entries = Object.entries(obj).map(([key, value]) => {
229
- if (typeof value === "string") {
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
- const updated = parsed.animations.map((anim) => {
239
- if (anim.id === animationId) {
240
- return { ...anim, ...updates };
241
- }
242
- return anim;
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.push(newAnim);
343
+ const allAnimations = [...parsed.animations, newAnim];
251
344
  return {
252
- script: serializeGsapAnimations(parsed.animations, parsed.timelineVar),
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
- 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
- }
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