@lumencast/compiler 0.2.0 → 0.3.0
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/.tsbuildinfo +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +36 -0
- package/dist/compile.js.map +1 -1
- package/dist/lsml-types.d.ts +19 -7
- package/dist/lsml-types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/compile.ts +361 -325
- package/src/index.ts +21 -21
- package/src/lsml-types.ts +210 -197
package/src/compile.ts
CHANGED
|
@@ -1,325 +1,361 @@
|
|
|
1
|
-
// LSML 1.0 / 1.1 → flat RenderBundle compiler.
|
|
2
|
-
//
|
|
3
|
-
// LSML lets authors write idiomatic primitives with inline `bind: { value: "path" }`,
|
|
4
|
-
// CSS-style `style.fontSize`, `repeat.template`, `animate` directives. The runtime
|
|
5
|
-
// expects a flat shape: per-node `bindings` map, primitive-specific prop names
|
|
6
|
-
// (Solar lineage: `text.size`, `text.colour`), and `repeat` whose template is its
|
|
7
|
-
// only child.
|
|
8
|
-
//
|
|
9
|
-
// This compiler bridges the two formats. It does NOT execute the bundle — it
|
|
10
|
-
// produces a JSON the runtime then renders.
|
|
11
|
-
//
|
|
12
|
-
// Version support :
|
|
13
|
-
// - 1.0 (LSML-1.md) : the original 9-primitive catalog.
|
|
14
|
-
// - 1.1 (LSML-1.md §17 / §5.4 / §4.9) : additive over 1.0 — `instance`
|
|
15
|
-
// primitive, universal props (`visible` / `sizing` / `opacity` /
|
|
16
|
-
// `rotation`) on every primitive, `bindUniversal` field, multi-fill
|
|
17
|
-
// `fills[]` on shapes, stacked `backgrounds[]` on frames, profile
|
|
18
|
-
// declarations, `$schema` field.
|
|
19
|
-
//
|
|
20
|
-
// Unsupported 1.1 features compile to best-effort output (the renderer
|
|
21
|
-
// surfaces `BUNDLE_INCOMPATIBLE` per LSML §15.1 if it can't honour them).
|
|
22
|
-
// Bundles tagged 2.x are rejected — major bumps require explicit support.
|
|
23
|
-
|
|
24
|
-
import type { RenderBundle, RenderNode } from "@lumencast/runtime";
|
|
25
|
-
import type {
|
|
26
|
-
LSMLAnimateDirective,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...(oi.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (node.bind?.
|
|
82
|
-
if (node.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (node.
|
|
90
|
-
if (node.
|
|
91
|
-
if (node.
|
|
92
|
-
if (node.
|
|
93
|
-
if (node.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (node.
|
|
99
|
-
if (node.
|
|
100
|
-
if (node.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
props["
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
props["
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (node.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
props["
|
|
124
|
-
props["
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
props["
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (node.
|
|
136
|
-
if (node.
|
|
137
|
-
if (node.
|
|
138
|
-
if (node.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (node.
|
|
145
|
-
if (node.
|
|
146
|
-
if (node.
|
|
147
|
-
if (node.
|
|
148
|
-
|
|
149
|
-
props["
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
props["
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
props["
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (node.
|
|
165
|
-
if (node.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
if (node.
|
|
178
|
-
if (node.
|
|
179
|
-
if (node.
|
|
180
|
-
if (node.
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
props["
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
if (Object.keys(
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (node.animate.
|
|
205
|
-
if (node.animate.transform?.
|
|
206
|
-
if (node.animate.transform?.
|
|
207
|
-
|
|
208
|
-
transitions["
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (s.
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
switch (
|
|
314
|
-
case "
|
|
315
|
-
return "
|
|
316
|
-
case "
|
|
317
|
-
return "
|
|
318
|
-
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1
|
+
// LSML 1.0 / 1.1 → flat RenderBundle compiler.
|
|
2
|
+
//
|
|
3
|
+
// LSML lets authors write idiomatic primitives with inline `bind: { value: "path" }`,
|
|
4
|
+
// CSS-style `style.fontSize`, `repeat.template`, `animate` directives. The runtime
|
|
5
|
+
// expects a flat shape: per-node `bindings` map, primitive-specific prop names
|
|
6
|
+
// (Solar lineage: `text.size`, `text.colour`), and `repeat` whose template is its
|
|
7
|
+
// only child.
|
|
8
|
+
//
|
|
9
|
+
// This compiler bridges the two formats. It does NOT execute the bundle — it
|
|
10
|
+
// produces a JSON the runtime then renders.
|
|
11
|
+
//
|
|
12
|
+
// Version support :
|
|
13
|
+
// - 1.0 (LSML-1.md) : the original 9-primitive catalog.
|
|
14
|
+
// - 1.1 (LSML-1.md §17 / §5.4 / §4.9) : additive over 1.0 — `instance`
|
|
15
|
+
// primitive, universal props (`visible` / `sizing` / `opacity` /
|
|
16
|
+
// `rotation`) on every primitive, `bindUniversal` field, multi-fill
|
|
17
|
+
// `fills[]` on shapes, stacked `backgrounds[]` on frames, profile
|
|
18
|
+
// declarations, `$schema` field.
|
|
19
|
+
//
|
|
20
|
+
// Unsupported 1.1 features compile to best-effort output (the renderer
|
|
21
|
+
// surfaces `BUNDLE_INCOMPATIBLE` per LSML §15.1 if it can't honour them).
|
|
22
|
+
// Bundles tagged 2.x are rejected — major bumps require explicit support.
|
|
23
|
+
|
|
24
|
+
import type { RenderBundle, RenderNode } from "@lumencast/runtime";
|
|
25
|
+
import type {
|
|
26
|
+
LSMLAnimateDirective,
|
|
27
|
+
LSMLAnimateState,
|
|
28
|
+
LSMLBundle,
|
|
29
|
+
LSMLNode,
|
|
30
|
+
LSMLRepeat,
|
|
31
|
+
LSMLText,
|
|
32
|
+
} from "./lsml-types.js";
|
|
33
|
+
|
|
34
|
+
export interface CompileOptions {
|
|
35
|
+
/** When true, throws on any unrecognized LSML extension. Default false (warn-only). */
|
|
36
|
+
strict?: boolean;
|
|
37
|
+
/** Optional warn collector — receives each warning string. */
|
|
38
|
+
onWarn?: (message: string) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SUPPORTED_VERSIONS = new Set(["1.0", "1.1"] as const);
|
|
42
|
+
|
|
43
|
+
export function compileBundle(lsml: LSMLBundle, options: CompileOptions = {}): RenderBundle {
|
|
44
|
+
if (!SUPPORTED_VERSIONS.has(lsml.lsml as "1.0" | "1.1")) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`compiler: LSML version "${lsml.lsml}" is not supported (supported: ${[...SUPPORTED_VERSIONS].join(", ")})`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
scene_version: lsml.scene_version,
|
|
51
|
+
root: compileNode(lsml.layout, options),
|
|
52
|
+
...(lsml.operator_inputs
|
|
53
|
+
? {
|
|
54
|
+
operator_inputs: lsml.operator_inputs.map((oi) => ({
|
|
55
|
+
path: oi.path,
|
|
56
|
+
label: oi.label,
|
|
57
|
+
type: oi.type as never,
|
|
58
|
+
writable_by: oi.writable_by,
|
|
59
|
+
...(oi.group !== undefined ? { group: oi.group } : {}),
|
|
60
|
+
...(oi.constraints ?? {}),
|
|
61
|
+
})),
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
...(lsml.external_adapters
|
|
65
|
+
? {
|
|
66
|
+
external_adapters: lsml.external_adapters as RenderBundle["external_adapters"],
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compileNode(node: LSMLNode, opts: CompileOptions): RenderNode {
|
|
73
|
+
if (node.kind === "repeat") {
|
|
74
|
+
return compileRepeat(node, opts);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const props: Record<string, unknown> = {};
|
|
78
|
+
const bindings: Record<string, string> = {};
|
|
79
|
+
|
|
80
|
+
// Common: bind.value/src → bindings
|
|
81
|
+
if (node.bind?.value !== undefined) bindings["value"] = node.bind.value;
|
|
82
|
+
if (node.bind?.src !== undefined) bindings["src"] = node.bind.src;
|
|
83
|
+
if (node.bindStyle) {
|
|
84
|
+
for (const [k, v] of Object.entries(node.bindStyle)) bindings[k] = v;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
switch (node.kind) {
|
|
88
|
+
case "stack":
|
|
89
|
+
if (node.direction !== undefined) props["direction"] = node.direction;
|
|
90
|
+
if (node.gap !== undefined) props["gap"] = node.gap;
|
|
91
|
+
if (node.align !== undefined) props["align"] = mapAlign(node.align);
|
|
92
|
+
if (node.justify !== undefined) props["justify"] = mapJustify(node.justify);
|
|
93
|
+
if (node.padding !== undefined) props["padding"] = node.padding;
|
|
94
|
+
if (node.rtl !== undefined) props["rtl"] = node.rtl;
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case "grid":
|
|
98
|
+
if (node.columns !== undefined) props["columns"] = node.columns;
|
|
99
|
+
if (node.rows !== undefined) props["rows"] = node.rows;
|
|
100
|
+
if (node.gap !== undefined) props["gap"] = node.gap;
|
|
101
|
+
if (node.padding !== undefined) props["padding"] = node.padding;
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "frame":
|
|
105
|
+
if (node.size !== undefined) {
|
|
106
|
+
props["width"] = node.size.w;
|
|
107
|
+
props["height"] = node.size.h;
|
|
108
|
+
}
|
|
109
|
+
if (node.position !== undefined) {
|
|
110
|
+
props["x"] = node.position.x;
|
|
111
|
+
props["y"] = node.position.y;
|
|
112
|
+
}
|
|
113
|
+
if (node.background !== undefined) props["background"] = node.background;
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case "text":
|
|
117
|
+
mapTextStyle(node, props);
|
|
118
|
+
if (node.format !== undefined) props["format"] = node.format;
|
|
119
|
+
if (node.maxLines !== undefined) props["maxLines"] = node.maxLines;
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case "image":
|
|
123
|
+
props["alt"] = node.alt;
|
|
124
|
+
props["width"] = node.size.w;
|
|
125
|
+
props["height"] = node.size.h;
|
|
126
|
+
if (node.fit !== undefined) props["fit"] = node.fit;
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "shape":
|
|
130
|
+
props["geometry"] = node.geometry;
|
|
131
|
+
if (node.size !== undefined) {
|
|
132
|
+
props["width"] = node.size.w;
|
|
133
|
+
props["height"] = node.size.h;
|
|
134
|
+
}
|
|
135
|
+
if (node.pathData !== undefined) props["pathData"] = node.pathData;
|
|
136
|
+
if (node.fill !== undefined) props["fill"] = node.fill;
|
|
137
|
+
if (node.stroke !== undefined) props["stroke"] = node.stroke;
|
|
138
|
+
if (node.cornerRadius !== undefined) props["cornerRadius"] = node.cornerRadius;
|
|
139
|
+
if (node.ariaLabel !== undefined) props["ariaLabel"] = node.ariaLabel;
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "media":
|
|
143
|
+
props["kind_hint"] = node.kind_hint;
|
|
144
|
+
if (node.controls !== undefined) props["controls"] = node.controls;
|
|
145
|
+
if (node.autoplay !== undefined) props["autoplay"] = node.autoplay;
|
|
146
|
+
if (node.muted !== undefined) props["muted"] = node.muted;
|
|
147
|
+
if (node.loop !== undefined) props["loop"] = node.loop;
|
|
148
|
+
if (node.size !== undefined) {
|
|
149
|
+
props["width"] = node.size.w;
|
|
150
|
+
props["height"] = node.size.h;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case "instance":
|
|
155
|
+
// 1.1+ — sub-scene mount (LSML §4.9). The runtime resolves
|
|
156
|
+
// `scene_id` + `scene_version` to a separate bundle and renders
|
|
157
|
+
// it inline ; the compiler just forwards the reference.
|
|
158
|
+
props["scene_id"] = node.scene_id;
|
|
159
|
+
props["scene_version"] = node.scene_version;
|
|
160
|
+
if (node.size !== undefined) {
|
|
161
|
+
props["width"] = node.size.w;
|
|
162
|
+
props["height"] = node.size.h;
|
|
163
|
+
}
|
|
164
|
+
if (node.fit !== undefined) props["fit"] = node.fit;
|
|
165
|
+
if (node.params !== undefined) props["params"] = node.params;
|
|
166
|
+
if (node.bindParams) {
|
|
167
|
+
for (const [k, v] of Object.entries(node.bindParams)) {
|
|
168
|
+
bindings[`params.${k}`] = v;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Universal props (LSML §5.4 — 1.1+). Forwarded to the renderer when
|
|
175
|
+
// present on the source node. Defaults are spec-side, not compiler-side
|
|
176
|
+
// (the runtime applies them per primitive).
|
|
177
|
+
if (node.visible !== undefined) props["visible"] = node.visible;
|
|
178
|
+
if (node.opacity !== undefined) props["opacity"] = node.opacity;
|
|
179
|
+
if (node.rotation !== undefined) props["rotation"] = node.rotation;
|
|
180
|
+
if (node.sizing !== undefined) props["sizing"] = node.sizing;
|
|
181
|
+
if (node.position !== undefined && props["x"] === undefined && props["y"] === undefined) {
|
|
182
|
+
// Frame's case above already sets x/y from `position` ; the universal
|
|
183
|
+
// §5.4 prop takes effect on every other primitive.
|
|
184
|
+
props["x"] = node.position.x;
|
|
185
|
+
props["y"] = node.position.y;
|
|
186
|
+
}
|
|
187
|
+
if (node.bindUniversal) {
|
|
188
|
+
for (const [k, v] of Object.entries(node.bindUniversal)) bindings[k] = v;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const children = node.children?.map((c) => compileNode(c, opts));
|
|
192
|
+
|
|
193
|
+
const out: RenderNode = { kind: node.kind };
|
|
194
|
+
if (node.id !== undefined) out.id = node.id;
|
|
195
|
+
if (Object.keys(props).length > 0) out.props = props;
|
|
196
|
+
if (Object.keys(bindings).length > 0) out.bindings = bindings;
|
|
197
|
+
if (children && children.length > 0) out.children = children;
|
|
198
|
+
|
|
199
|
+
// Animate directive → transitions on the listed prop keys.
|
|
200
|
+
if (node.animate) {
|
|
201
|
+
const tx = compileAnimate(node.animate);
|
|
202
|
+
if (tx) {
|
|
203
|
+
const transitions: Record<string, ReturnType<typeof compileAnimate>> = {};
|
|
204
|
+
if (node.animate.opacity !== undefined) transitions["opacity"] = tx;
|
|
205
|
+
if (node.animate.transform?.scale !== undefined) transitions["scale"] = tx;
|
|
206
|
+
if (node.animate.transform?.rotate !== undefined) transitions["rotate"] = tx;
|
|
207
|
+
if (node.animate.transform?.translate !== undefined) {
|
|
208
|
+
transitions["x"] = tx;
|
|
209
|
+
transitions["y"] = tx;
|
|
210
|
+
}
|
|
211
|
+
// Type assertion: RenderNode.transitions matches Transition shape; the
|
|
212
|
+
// cast keeps the compiler self-contained without re-importing the runtime
|
|
213
|
+
// Transition type.
|
|
214
|
+
if (Object.keys(transitions).length > 0) {
|
|
215
|
+
out.transitions = transitions as RenderNode["transitions"];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// LSML 1.1 §6 `animate.from` → flat framer `initial` map. Lowered
|
|
220
|
+
// independently of `transition` : an author may declare a `from`
|
|
221
|
+
// without a `transition` (mount-play with the runtime's default
|
|
222
|
+
// timing). When no `from` is present, `animate_initial` is omitted
|
|
223
|
+
// and the prior no-mount-play behaviour is preserved (rétro-compat).
|
|
224
|
+
if (node.animate.from) {
|
|
225
|
+
const initial = lowerAnimateState(node.animate.from);
|
|
226
|
+
if (Object.keys(initial).length > 0) {
|
|
227
|
+
out.animate_initial = initial;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Lower an `animate.from` (or any LSML animate state) into the flat
|
|
236
|
+
* framer-motion key space the runtime primitives consume: `opacity`,
|
|
237
|
+
* `scale`, `rotate`, `x`, `y`. A scalar `scale` applies uniformly ; a
|
|
238
|
+
* `[sx, sy]` pair is collapsed to `sx` (framer takes a single scale on
|
|
239
|
+
* the motion components used here). `translate: [x, y]` → `x` / `y`. */
|
|
240
|
+
function lowerAnimateState(s: LSMLAnimateState): Record<string, number> {
|
|
241
|
+
const out: Record<string, number> = {};
|
|
242
|
+
if (typeof s.opacity === "number") out["opacity"] = s.opacity;
|
|
243
|
+
const t = s.transform;
|
|
244
|
+
if (t) {
|
|
245
|
+
if (t.scale !== undefined) {
|
|
246
|
+
out["scale"] = Array.isArray(t.scale) ? t.scale[0] : t.scale;
|
|
247
|
+
}
|
|
248
|
+
if (typeof t.rotate === "number") out["rotate"] = t.rotate;
|
|
249
|
+
if (t.translate !== undefined) {
|
|
250
|
+
out["x"] = t.translate[0];
|
|
251
|
+
out["y"] = t.translate[1];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function compileRepeat(node: LSMLRepeat, opts: CompileOptions): RenderNode {
|
|
258
|
+
if (!node.bind?.items) {
|
|
259
|
+
throw new Error(`compiler: repeat node "${node.id ?? "<anon>"}" missing bind.items`);
|
|
260
|
+
}
|
|
261
|
+
const compiledTemplate = compileNode(node.template, opts);
|
|
262
|
+
const out: RenderNode = {
|
|
263
|
+
kind: "repeat",
|
|
264
|
+
bindings: { items: node.bind.items },
|
|
265
|
+
children: [compiledTemplate],
|
|
266
|
+
};
|
|
267
|
+
if (node.id !== undefined) out.id = node.id;
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function mapTextStyle(node: LSMLText, props: Record<string, unknown>): void {
|
|
272
|
+
if (!node.style) return;
|
|
273
|
+
const s = node.style;
|
|
274
|
+
// Solar's Text primitive consumes size/weight/colour (UK), not CSS-style names.
|
|
275
|
+
if (s.fontSize !== undefined) props["size"] = s.fontSize;
|
|
276
|
+
if (s.fontFamily !== undefined) props["font"] = s.fontFamily;
|
|
277
|
+
if (s.fontWeight !== undefined) props["weight"] = s.fontWeight;
|
|
278
|
+
if (s.color !== undefined) props["colour"] = s.color;
|
|
279
|
+
if (s.textAlign !== undefined) props["align"] = mapTextAlign(s.textAlign);
|
|
280
|
+
if (s.lineHeight !== undefined) props["lineHeight"] = s.lineHeight;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function mapAlign(a: NonNullable<Extract<LSMLNode, { kind: "stack" }>["align"]>): string {
|
|
284
|
+
// LSML uses CSS-grid vocabulary; Solar's Stack consumes flexbox vocabulary.
|
|
285
|
+
switch (a) {
|
|
286
|
+
case "start":
|
|
287
|
+
return "flex-start";
|
|
288
|
+
case "center":
|
|
289
|
+
return "center";
|
|
290
|
+
case "end":
|
|
291
|
+
return "flex-end";
|
|
292
|
+
case "stretch":
|
|
293
|
+
return "stretch";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function mapJustify(j: NonNullable<Extract<LSMLNode, { kind: "stack" }>["justify"]>): string {
|
|
298
|
+
switch (j) {
|
|
299
|
+
case "start":
|
|
300
|
+
return "flex-start";
|
|
301
|
+
case "center":
|
|
302
|
+
return "center";
|
|
303
|
+
case "end":
|
|
304
|
+
return "flex-end";
|
|
305
|
+
case "space-between":
|
|
306
|
+
return "space-between";
|
|
307
|
+
case "space-around":
|
|
308
|
+
return "space-around";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function mapTextAlign(a: NonNullable<NonNullable<LSMLText["style"]>["textAlign"]>): string {
|
|
313
|
+
switch (a) {
|
|
314
|
+
case "start":
|
|
315
|
+
return "left";
|
|
316
|
+
case "end":
|
|
317
|
+
return "right";
|
|
318
|
+
default:
|
|
319
|
+
return a;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function compileAnimate(a: LSMLAnimateDirective):
|
|
324
|
+
| {
|
|
325
|
+
kind: "tween";
|
|
326
|
+
duration_ms: number;
|
|
327
|
+
ease?: "linear" | "cubic-in" | "cubic-out" | "cubic-in-out";
|
|
328
|
+
}
|
|
329
|
+
| { kind: "spring"; stiffness?: number; damping?: number }
|
|
330
|
+
| undefined {
|
|
331
|
+
const t = a.transition;
|
|
332
|
+
if (!t) return undefined;
|
|
333
|
+
if (t.easing === "spring") {
|
|
334
|
+
const out: { kind: "spring"; stiffness?: number; damping?: number } = { kind: "spring" };
|
|
335
|
+
if (t.stiffness !== undefined) out.stiffness = t.stiffness;
|
|
336
|
+
if (t.damping !== undefined) out.damping = t.damping;
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
kind: "tween",
|
|
341
|
+
duration_ms: t.duration ?? 200,
|
|
342
|
+
ease: mapEase(t.easing),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function mapEase(
|
|
347
|
+
e: "linear" | "ease-in" | "ease-out" | "ease-in-out" | "spring" | undefined,
|
|
348
|
+
): "linear" | "cubic-in" | "cubic-out" | "cubic-in-out" | undefined {
|
|
349
|
+
switch (e) {
|
|
350
|
+
case "linear":
|
|
351
|
+
return "linear";
|
|
352
|
+
case "ease-in":
|
|
353
|
+
return "cubic-in";
|
|
354
|
+
case "ease-out":
|
|
355
|
+
return "cubic-out";
|
|
356
|
+
case "ease-in-out":
|
|
357
|
+
return "cubic-in-out";
|
|
358
|
+
default:
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|