@markdy/core 0.7.29 → 0.8.1
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/README.md +21 -16
- package/dist/index.d.ts +190 -188
- package/dist/index.js +698 -1213
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,304 +1,3 @@
|
|
|
1
|
-
// src/presets.ts
|
|
2
|
-
function quote(s) {
|
|
3
|
-
const stripped = s.replace(/^"|"$/g, "");
|
|
4
|
-
return `"${stripped.replace(/"/g, '\\"')}"`;
|
|
5
|
-
}
|
|
6
|
-
var PRESETS = {
|
|
7
|
-
meme: (args) => {
|
|
8
|
-
const top = quote(args[0] ?? "when the code works");
|
|
9
|
-
const bottom = quote(args[1] ?? "and you don't know why");
|
|
10
|
-
return `scene width=720 height=720 bg=#111
|
|
11
|
-
|
|
12
|
-
actor top = caption(${top}) at top
|
|
13
|
-
actor hero = figure(#c68642, m, \u{1F60E}) at (360, 430)
|
|
14
|
-
actor bottom = caption(${bottom}) at bottom
|
|
15
|
-
|
|
16
|
-
@0.0: top.fade_in(dur=0.4)
|
|
17
|
-
@0.4: hero.enter(from=bottom, dur=0.6)
|
|
18
|
-
@1.2: hero.face("\u{1F602}")
|
|
19
|
-
@1.8: bottom.fade_in(dur=0.4)
|
|
20
|
-
@3.2: hero.bounce(intensity=20, count=2, dur=0.8)
|
|
21
|
-
`;
|
|
22
|
-
},
|
|
23
|
-
explainer: (args) => {
|
|
24
|
-
const title = quote(args[0] ?? "how it works");
|
|
25
|
-
return `scene width=960 height=540 bg=#0d1117
|
|
26
|
-
|
|
27
|
-
actor title = caption(${title}) at top
|
|
28
|
-
actor hero = figure(#c68642, m, \u{1F60E}) at (480, 360)
|
|
29
|
-
|
|
30
|
-
scene "intro" {
|
|
31
|
-
@+0.0: title.fade_in(dur=0.5)
|
|
32
|
-
@+1.0: hero.enter(from=bottom, dur=0.5)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
scene "body" {
|
|
36
|
-
@+0.0: camera.zoom(to=1.2, dur=0.8)
|
|
37
|
-
@+1.0: hero.wave(side=right, dur=0.5)
|
|
38
|
-
}
|
|
39
|
-
`;
|
|
40
|
-
},
|
|
41
|
-
reaction: (args) => {
|
|
42
|
-
const line = quote(args[0] ?? "wait, what?");
|
|
43
|
-
return `scene width=720 height=720 bg=#fff5f9
|
|
44
|
-
|
|
45
|
-
actor hero = figure(#c68642, m, \u{1F642}) at (360, 380)
|
|
46
|
-
|
|
47
|
-
@0.0: hero.enter(from=left, dur=0.4)
|
|
48
|
-
@0.5: hero.face("\u{1F633}")
|
|
49
|
-
@0.5: hero.say(${line}, dur=1.4)
|
|
50
|
-
@2.0: hero.face("\u{1F602}")
|
|
51
|
-
@2.0: hero.shake(intensity=6, dur=0.5)
|
|
52
|
-
`;
|
|
53
|
-
},
|
|
54
|
-
pov: (args) => {
|
|
55
|
-
const pov = quote(args[0] ?? "POV: you hit ship");
|
|
56
|
-
return `scene width=720 height=960 bg=#0d1117
|
|
57
|
-
|
|
58
|
-
actor label = caption(${pov}) at top
|
|
59
|
-
actor hero = figure(#c68642, m, \u{1F60E}) at (360, 600)
|
|
60
|
-
|
|
61
|
-
@0.0: label.fade_in(dur=0.4)
|
|
62
|
-
@0.4: hero.enter(from=bottom, dur=0.6)
|
|
63
|
-
@1.2: hero.pose(arm_left=70, arm_right=-70, dur=0.3)
|
|
64
|
-
@1.2: hero.face("\u{1F525}")
|
|
65
|
-
`;
|
|
66
|
-
},
|
|
67
|
-
typing: (args) => {
|
|
68
|
-
const text = quote(args[0] ?? "hello world");
|
|
69
|
-
return `scene width=800 height=300 bg=#0f1115
|
|
70
|
-
|
|
71
|
-
actor cursor = text("|") at (120, 150) size 40 opacity 0
|
|
72
|
-
actor line = text(${text}) at (140, 150) size 32 opacity 0
|
|
73
|
-
|
|
74
|
-
@0.0: cursor.fade_in(dur=0.15)
|
|
75
|
-
@+0.1: cursor.fade_out(dur=0.15)
|
|
76
|
-
@+0.0: cursor.fade_in(dur=0.15)
|
|
77
|
-
@+0.1: cursor.fade_out(dur=0.15)
|
|
78
|
-
@+0.0: cursor.fade_in(dur=0.15)
|
|
79
|
-
@+0.0: line.fade_in(dur=0.8)
|
|
80
|
-
@+0.3: cursor.fade_out(dur=0.3)
|
|
81
|
-
`;
|
|
82
|
-
},
|
|
83
|
-
terminal: (args) => {
|
|
84
|
-
const cmd = quote(args[0] ?? "$ npx markdy");
|
|
85
|
-
const output = quote(args[1] ?? "playground ready at http://localhost:4242");
|
|
86
|
-
return `scene width=960 height=420 bg=#0d1117
|
|
87
|
-
|
|
88
|
-
actor prompt = text(${cmd}) at (60, 140) size 24 opacity 0
|
|
89
|
-
actor result = text(${output}) at (60, 200) size 22 opacity 0
|
|
90
|
-
|
|
91
|
-
@0.0: prompt.fade_in(dur=0.4)
|
|
92
|
-
@1.0: result.fade_in(dur=0.4)
|
|
93
|
-
`;
|
|
94
|
-
},
|
|
95
|
-
chat_bubble: (args) => {
|
|
96
|
-
const a = quote(args[0] ?? "how do I animate this?");
|
|
97
|
-
const b = quote(args[1] ?? "ask markdy.");
|
|
98
|
-
return `scene width=800 height=400 bg=#f6f7fb
|
|
99
|
-
|
|
100
|
-
actor alice = figure(#fad4c0, f, \u{1F642}) at (180, 240)
|
|
101
|
-
actor bob = figure(#c68642, m, \u{1F642}) at (620, 240)
|
|
102
|
-
|
|
103
|
-
@0.0: alice.enter(from=left, dur=0.4)
|
|
104
|
-
@0.2: bob.enter(from=right, dur=0.4)
|
|
105
|
-
@0.8: alice.say(${a}, dur=1.8)
|
|
106
|
-
@2.6: bob.face("\u{1F60E}")
|
|
107
|
-
@2.6: bob.say(${b}, dur=1.6)
|
|
108
|
-
`;
|
|
109
|
-
},
|
|
110
|
-
vs: (args) => {
|
|
111
|
-
const left = quote(args[0] ?? "team a");
|
|
112
|
-
const right = quote(args[1] ?? "team b");
|
|
113
|
-
return `scene width=960 height=540 bg=#111
|
|
114
|
-
|
|
115
|
-
actor a = caption(${left}) at top
|
|
116
|
-
actor b = caption(${right}) at bottom
|
|
117
|
-
actor lf = figure(#c68642, m, \u{1F624}) at (260, 320)
|
|
118
|
-
actor rf = figure(#8d5524, m, \u{1F60F}) at (700, 320)
|
|
119
|
-
|
|
120
|
-
@0.0: a.fade_in(dur=0.3)
|
|
121
|
-
@0.0: b.fade_in(dur=0.3)
|
|
122
|
-
@0.3: lf.enter(from=left, dur=0.5)
|
|
123
|
-
@0.3: rf.enter(from=right, dur=0.5)
|
|
124
|
-
@1.2: lf.punch(side=right, dur=0.3)
|
|
125
|
-
@1.3: rf.shake(intensity=8, dur=0.4)
|
|
126
|
-
`;
|
|
127
|
-
},
|
|
128
|
-
tutorial_step: (args) => {
|
|
129
|
-
const n = quote(args[0] ?? "Step 1");
|
|
130
|
-
const body = quote(args[1] ?? "open your editor");
|
|
131
|
-
return `scene width=960 height=420 bg=white
|
|
132
|
-
|
|
133
|
-
actor step = caption(${n}) at top
|
|
134
|
-
actor descr = text(${body}) at (100, 220) size 32 opacity 0
|
|
135
|
-
|
|
136
|
-
@0.0: step.fade_in(dur=0.4)
|
|
137
|
-
@0.5: descr.fade_in(dur=0.4)
|
|
138
|
-
@1.2: descr.move(to=(120, 220), dur=0.5, ease=out)
|
|
139
|
-
`;
|
|
140
|
-
},
|
|
141
|
-
countdown: (args) => {
|
|
142
|
-
const to = quote(args[0] ?? "launch");
|
|
143
|
-
return `scene width=600 height=600 bg=#0d1117
|
|
144
|
-
|
|
145
|
-
actor three = text("3") at (270, 280) size 120 opacity 0
|
|
146
|
-
actor two = text("2") at (270, 280) size 120 opacity 0
|
|
147
|
-
actor one = text("1") at (270, 280) size 120 opacity 0
|
|
148
|
-
actor go = caption(${to}) at center
|
|
149
|
-
|
|
150
|
-
@0.0: three.fade_in(dur=0.2)
|
|
151
|
-
@0.9: three.fade_out(dur=0.2)
|
|
152
|
-
@0.9: two.fade_in(dur=0.2)
|
|
153
|
-
@1.8: two.fade_out(dur=0.2)
|
|
154
|
-
@1.8: one.fade_in(dur=0.2)
|
|
155
|
-
@2.7: one.fade_out(dur=0.2)
|
|
156
|
-
@2.7: go.fade_in(dur=0.4)
|
|
157
|
-
`;
|
|
158
|
-
},
|
|
159
|
-
reveal: (args) => {
|
|
160
|
-
const secret = quote(args[0] ?? "and that's the trick");
|
|
161
|
-
return `scene width=800 height=450 bg=#0d1117
|
|
162
|
-
|
|
163
|
-
actor cover = box() at (350, 175) scale 10 opacity 1
|
|
164
|
-
actor reveal = caption(${secret}) at center
|
|
165
|
-
|
|
166
|
-
@0.4: cover.fade_out(dur=0.5)
|
|
167
|
-
@0.4: reveal.fade_in(dur=0.5)
|
|
168
|
-
`;
|
|
169
|
-
},
|
|
170
|
-
glitch: (_args) => {
|
|
171
|
-
const txt = quote("GLITCH");
|
|
172
|
-
return `scene width=800 height=400 bg=#000
|
|
173
|
-
|
|
174
|
-
actor t = text(${txt}) at (260, 170) size 72
|
|
175
|
-
|
|
176
|
-
@0.0: t.fade_in(dur=0.2)
|
|
177
|
-
@0.4: t.shake(intensity=10, dur=0.3)
|
|
178
|
-
@0.8: t.shake(intensity=6, dur=0.3)
|
|
179
|
-
@1.3: t.fade_out(dur=0.3)
|
|
180
|
-
`;
|
|
181
|
-
},
|
|
182
|
-
zoom_punchline: (args) => {
|
|
183
|
-
const line = quote(args[0] ?? "the punchline");
|
|
184
|
-
return `scene width=800 height=450 bg=#0d1117
|
|
185
|
-
|
|
186
|
-
actor line = caption(${line}) at center
|
|
187
|
-
|
|
188
|
-
@0.0: line.fade_in(dur=0.4)
|
|
189
|
-
@0.6: camera.zoom(to=1.4, dur=0.8, ease=out)
|
|
190
|
-
@1.6: camera.shake(intensity=6, dur=0.4)
|
|
191
|
-
`;
|
|
192
|
-
},
|
|
193
|
-
before_after: (args) => {
|
|
194
|
-
const before = quote(args[0] ?? "before");
|
|
195
|
-
const after = quote(args[1] ?? "after");
|
|
196
|
-
return `scene width=960 height=420 bg=#f6f7fb
|
|
197
|
-
|
|
198
|
-
actor a = caption(${before}) at top
|
|
199
|
-
actor b = caption(${after}) at bottom
|
|
200
|
-
actor dude = figure(#c68642, m, \u{1F635}) at (480, 240)
|
|
201
|
-
|
|
202
|
-
@0.0: a.fade_in(dur=0.3)
|
|
203
|
-
@0.0: dude.enter(from=left, dur=0.5)
|
|
204
|
-
@1.4: dude.face("\u{1F60E}")
|
|
205
|
-
@1.4: b.fade_in(dur=0.3)
|
|
206
|
-
`;
|
|
207
|
-
},
|
|
208
|
-
tier_list: (_args) => {
|
|
209
|
-
return `scene width=960 height=540 bg=#111
|
|
210
|
-
|
|
211
|
-
actor s = text("S") at (60, 90) size 72 opacity 0
|
|
212
|
-
actor a = text("A") at (60, 200) size 72 opacity 0
|
|
213
|
-
actor b = text("B") at (60, 310) size 72 opacity 0
|
|
214
|
-
actor c = text("C") at (60, 420) size 72 opacity 0
|
|
215
|
-
actor title = caption("tier list") at top
|
|
216
|
-
|
|
217
|
-
@0.0: title.fade_in(dur=0.3)
|
|
218
|
-
@0.3: s.fade_in(dur=0.2)
|
|
219
|
-
@0.6: a.fade_in(dur=0.2)
|
|
220
|
-
@0.9: b.fade_in(dur=0.2)
|
|
221
|
-
@1.2: c.fade_in(dur=0.2)
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
var PRESET_NAMES = Object.keys(PRESETS);
|
|
226
|
-
|
|
227
|
-
// src/registry.ts
|
|
228
|
-
var BUILTIN_ACTOR_TYPES = [
|
|
229
|
-
"sprite",
|
|
230
|
-
"text",
|
|
231
|
-
"box",
|
|
232
|
-
"figure",
|
|
233
|
-
"caption"
|
|
234
|
-
];
|
|
235
|
-
var UNIVERSAL_ACTION_NAMES = [
|
|
236
|
-
"enter",
|
|
237
|
-
"exit",
|
|
238
|
-
"move",
|
|
239
|
-
"spring",
|
|
240
|
-
"follow_path",
|
|
241
|
-
"fade_in",
|
|
242
|
-
"fade_out",
|
|
243
|
-
"scale",
|
|
244
|
-
"rotate",
|
|
245
|
-
"shake",
|
|
246
|
-
"pulse",
|
|
247
|
-
"glow",
|
|
248
|
-
"ripple",
|
|
249
|
-
"blur",
|
|
250
|
-
"line_reveal",
|
|
251
|
-
"mask",
|
|
252
|
-
"parallax",
|
|
253
|
-
"say",
|
|
254
|
-
"throw",
|
|
255
|
-
"play"
|
|
256
|
-
];
|
|
257
|
-
var FIGURE_ONLY_ACTION_NAMES = [
|
|
258
|
-
"punch",
|
|
259
|
-
"kick",
|
|
260
|
-
"wave",
|
|
261
|
-
"nod",
|
|
262
|
-
"jump",
|
|
263
|
-
"bounce",
|
|
264
|
-
"face",
|
|
265
|
-
"rotate_part",
|
|
266
|
-
"pose"
|
|
267
|
-
];
|
|
268
|
-
var CAMERA_ACTION_NAMES = ["pan", "zoom", "shake"];
|
|
269
|
-
var UNIVERSAL_ACTIONS = new Set(UNIVERSAL_ACTION_NAMES);
|
|
270
|
-
var FIGURE_ONLY_ACTIONS = new Set(FIGURE_ONLY_ACTION_NAMES);
|
|
271
|
-
var CAMERA_ACTIONS = new Set(CAMERA_ACTION_NAMES);
|
|
272
|
-
var actorTypes = new Set(BUILTIN_ACTOR_TYPES);
|
|
273
|
-
var actorActions = /* @__PURE__ */ new Map();
|
|
274
|
-
function registerActorPack(pack) {
|
|
275
|
-
for (const actor of pack.actors) {
|
|
276
|
-
actorTypes.add(actor);
|
|
277
|
-
if (!actorActions.has(actor)) actorActions.set(actor, /* @__PURE__ */ new Set());
|
|
278
|
-
}
|
|
279
|
-
if (!pack.actions) return;
|
|
280
|
-
for (const [actorType, actions] of Object.entries(pack.actions)) {
|
|
281
|
-
actorTypes.add(actorType);
|
|
282
|
-
const known = actorActions.get(actorType) ?? /* @__PURE__ */ new Set();
|
|
283
|
-
for (const action of actions) known.add(action);
|
|
284
|
-
actorActions.set(actorType, known);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function isKnownActorType(type) {
|
|
288
|
-
return actorTypes.has(type);
|
|
289
|
-
}
|
|
290
|
-
function isFigureOnlyAction(action) {
|
|
291
|
-
return FIGURE_ONLY_ACTIONS.has(action);
|
|
292
|
-
}
|
|
293
|
-
function isCameraAction(action) {
|
|
294
|
-
return CAMERA_ACTIONS.has(action);
|
|
295
|
-
}
|
|
296
|
-
function isKnownAction(actorType, action) {
|
|
297
|
-
if (UNIVERSAL_ACTIONS.has(action)) return true;
|
|
298
|
-
if (actorType === "figure" && FIGURE_ONLY_ACTIONS.has(action)) return true;
|
|
299
|
-
return actorActions.get(actorType)?.has(action) ?? false;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
1
|
// src/system-vocabulary.ts
|
|
303
2
|
var TECHNICAL_NODE_TYPES = [
|
|
304
3
|
"service",
|
|
@@ -642,999 +341,785 @@ var TECHNICAL_NODE_KINDS = {
|
|
|
642
341
|
lock: "distributed"
|
|
643
342
|
};
|
|
644
343
|
|
|
645
|
-
// src/
|
|
646
|
-
var
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
line;
|
|
344
|
+
// src/registry.ts
|
|
345
|
+
var NODE_KINDS = new Set(TECHNICAL_NODE_TYPES);
|
|
346
|
+
var EDGE_OPERATORS = {
|
|
347
|
+
"->": "request",
|
|
348
|
+
"<-": "response",
|
|
349
|
+
"~>": "event",
|
|
350
|
+
"--": "dependency"
|
|
653
351
|
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
for (let i = 0; i < line.length; i++) {
|
|
659
|
-
const ch = line[i];
|
|
660
|
-
if ((ch === '"' || ch === "'") && !isEscapedChar(line, i)) {
|
|
661
|
-
if (stringQuote === ch) stringQuote = null;
|
|
662
|
-
else if (stringQuote === null) stringQuote = ch;
|
|
663
|
-
lastNonSpace = ch;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
if (stringQuote !== null) continue;
|
|
667
|
-
if (ch === "(") {
|
|
668
|
-
depth++;
|
|
669
|
-
lastNonSpace = ch;
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
if (ch === ")") {
|
|
673
|
-
depth--;
|
|
674
|
-
lastNonSpace = ch;
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (ch === "#" && depth === 0 && lastNonSpace !== "=") return line.slice(0, i);
|
|
678
|
-
if (ch !== " " && ch !== " ") lastNonSpace = ch;
|
|
679
|
-
}
|
|
680
|
-
return line;
|
|
352
|
+
var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set(["show", "hide", "glow", "focus", "use"]);
|
|
353
|
+
var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction"]);
|
|
354
|
+
function nodeRole(kind) {
|
|
355
|
+
return TECHNICAL_NODE_KINDS[kind] ?? "compute";
|
|
681
356
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
let depth = 0;
|
|
685
|
-
let stringQuote = null;
|
|
686
|
-
let start = 0;
|
|
687
|
-
for (let i = 0; i < s.length; i++) {
|
|
688
|
-
const ch = s[i];
|
|
689
|
-
if ((ch === '"' || ch === "'") && !isEscapedChar(s, i)) {
|
|
690
|
-
if (stringQuote === ch) stringQuote = null;
|
|
691
|
-
else if (stringQuote === null) stringQuote = ch;
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
694
|
-
if (stringQuote !== null) continue;
|
|
695
|
-
if (ch === "(") depth++;
|
|
696
|
-
else if (ch === ")") depth--;
|
|
697
|
-
else if (ch === "," && depth === 0) {
|
|
698
|
-
parts.push(s.slice(start, i));
|
|
699
|
-
start = i + 1;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
parts.push(s.slice(start));
|
|
703
|
-
return parts;
|
|
357
|
+
function humanizeId(id) {
|
|
358
|
+
return id.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
704
359
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const n2 = Number(v);
|
|
716
|
-
return Number.isNaN(n2) ? v : n2;
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
const n = Number(t);
|
|
720
|
-
if (!Number.isNaN(n) && t !== "") return n;
|
|
721
|
-
return t;
|
|
360
|
+
var NODE_ALIASES = {
|
|
361
|
+
db: "database",
|
|
362
|
+
api: "service",
|
|
363
|
+
gateway: "api_gateway",
|
|
364
|
+
mq: "queue",
|
|
365
|
+
k8s: "cluster",
|
|
366
|
+
lb: "load_balancer"
|
|
367
|
+
};
|
|
368
|
+
function canonicalNodeKind(kind) {
|
|
369
|
+
return NODE_ALIASES[kind] ?? kind;
|
|
722
370
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
371
|
+
|
|
372
|
+
// src/compiler.ts
|
|
373
|
+
var SAFE = 64;
|
|
374
|
+
var TITLE_BAND = 96;
|
|
375
|
+
var NODE_W = 184;
|
|
376
|
+
var NODE_H = 88;
|
|
377
|
+
var DEFAULTS = {
|
|
378
|
+
show: 0.35,
|
|
379
|
+
hide: 0.35,
|
|
380
|
+
flow: 0.55,
|
|
381
|
+
glow: 0.45,
|
|
382
|
+
focus: 0.6,
|
|
383
|
+
beatGap: 0.14,
|
|
384
|
+
cueGap: 0.08,
|
|
385
|
+
stagger: 0.06
|
|
386
|
+
};
|
|
387
|
+
function collectStructuralEdges(ast) {
|
|
388
|
+
const edges = [...ast.edges.map((e) => ({
|
|
389
|
+
id: e.id,
|
|
390
|
+
kind: e.kind,
|
|
391
|
+
from: e.from,
|
|
392
|
+
to: e.to,
|
|
393
|
+
label: e.label
|
|
394
|
+
}))];
|
|
395
|
+
let counter = edges.length;
|
|
396
|
+
for (const beat of ast.beats) {
|
|
397
|
+
for (const cue of beat.cues) {
|
|
398
|
+
collectFlowSegments(cue, (seg) => {
|
|
399
|
+
const id = `flow_${++counter}`;
|
|
400
|
+
edges.push({ id, kind: seg.op, from: seg.from, to: seg.to, label: seg.label });
|
|
401
|
+
});
|
|
402
|
+
}
|
|
727
403
|
}
|
|
728
|
-
return
|
|
404
|
+
return dedupeEdges(edges);
|
|
729
405
|
}
|
|
730
|
-
function
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return null;
|
|
406
|
+
function collectFlowSegments(cue, emit) {
|
|
407
|
+
if (cue.kind === "parallel") {
|
|
408
|
+
for (const child of cue.cues) collectFlowSegments(child, emit);
|
|
409
|
+
return;
|
|
735
410
|
}
|
|
736
|
-
if (
|
|
737
|
-
|
|
411
|
+
if (cue.kind === "flow") {
|
|
412
|
+
for (const seg of cue.segments) emit(seg);
|
|
738
413
|
}
|
|
739
|
-
return unescapeQuotedContent(token.slice(1, -1), quote2);
|
|
740
414
|
}
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (i + 1 >= content.length) {
|
|
750
|
-
out += "\\";
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
const next = content[++i];
|
|
754
|
-
if (next === "n") out += "\n";
|
|
755
|
-
else if (next === "r") out += "\r";
|
|
756
|
-
else if (next === "t") out += " ";
|
|
757
|
-
else if (next === "\\") out += "\\";
|
|
758
|
-
else if (next === quote2) out += quote2;
|
|
759
|
-
else if (next === '"' || next === "'") out += next;
|
|
760
|
-
else out += `\\${next}`;
|
|
415
|
+
function dedupeEdges(edges) {
|
|
416
|
+
const seen = /* @__PURE__ */ new Set();
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const e of edges) {
|
|
419
|
+
const k = `${e.from}|${e.kind}|${e.to}|${e.label ?? ""}`;
|
|
420
|
+
if (seen.has(k)) continue;
|
|
421
|
+
seen.add(k);
|
|
422
|
+
out.push(e);
|
|
761
423
|
}
|
|
762
424
|
return out;
|
|
763
425
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (!t) continue;
|
|
777
|
-
const eqIdx = t.indexOf("=");
|
|
778
|
-
if (eqIdx === -1) {
|
|
779
|
-
const key = positionalKeys[positionalIndex] ?? `_${positionalIndex}`;
|
|
780
|
-
positionalIndex++;
|
|
781
|
-
params[key] = parseValue(t);
|
|
782
|
-
} else {
|
|
783
|
-
const key = t.slice(0, eqIdx).trim();
|
|
784
|
-
const val = t.slice(eqIdx + 1).trim();
|
|
785
|
-
params[key] = parseValue(val);
|
|
426
|
+
function assignRanks(nodeIds, edges, direction) {
|
|
427
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const id of nodeIds) ranks.set(id, 0);
|
|
429
|
+
const forward = edges.filter((e) => e.kind !== "response");
|
|
430
|
+
for (let sweep = 0; sweep < nodeIds.length; sweep++) {
|
|
431
|
+
let changed = false;
|
|
432
|
+
for (const e of forward) {
|
|
433
|
+
const next = (ranks.get(e.from) ?? 0) + 1;
|
|
434
|
+
if (next > (ranks.get(e.to) ?? 0)) {
|
|
435
|
+
ranks.set(e.to, next);
|
|
436
|
+
changed = true;
|
|
437
|
+
}
|
|
786
438
|
}
|
|
439
|
+
if (!changed) break;
|
|
440
|
+
}
|
|
441
|
+
if (direction === "RL" || direction === "BT") {
|
|
442
|
+
const max = Math.max(...ranks.values(), 0);
|
|
443
|
+
for (const [id, r] of ranks) ranks.set(id, max - r);
|
|
787
444
|
}
|
|
788
|
-
return
|
|
445
|
+
return ranks;
|
|
789
446
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
for (
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
447
|
+
function layoutNodes(ast, edges) {
|
|
448
|
+
const nodeIds = Object.keys(ast.nodes);
|
|
449
|
+
const ranks = assignRanks(nodeIds, edges, ast.meta.direction);
|
|
450
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
451
|
+
for (const id of nodeIds) {
|
|
452
|
+
const r = ranks.get(id) ?? 0;
|
|
453
|
+
if (!byRank.has(r)) byRank.set(r, []);
|
|
454
|
+
byRank.get(r).push(id);
|
|
455
|
+
}
|
|
456
|
+
for (const ids of byRank.values()) ids.sort();
|
|
457
|
+
const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
|
|
458
|
+
const contentW = ast.meta.width - SAFE * 2;
|
|
459
|
+
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
460
|
+
const maxRank = Math.max(...byRank.keys(), 0);
|
|
461
|
+
const rankCount = maxRank + 1;
|
|
462
|
+
const nodes = [];
|
|
463
|
+
for (const [rank, ids] of [...byRank.entries()].sort((a, b) => a[0] - b[0])) {
|
|
464
|
+
const rowCount = ids.length;
|
|
465
|
+
ids.forEach((id, idx) => {
|
|
466
|
+
const decl = ast.nodes[id];
|
|
467
|
+
const role = nodeRole(decl.kind);
|
|
468
|
+
let x;
|
|
469
|
+
let y;
|
|
470
|
+
if (isVertical) {
|
|
471
|
+
x = SAFE + contentW / (rowCount + 1) * (idx + 1) - NODE_W / 2;
|
|
472
|
+
y = TITLE_BAND + contentH / Math.max(rankCount, 1) * rank + (contentH / Math.max(rankCount, 1) - NODE_H) / 2;
|
|
473
|
+
} else {
|
|
474
|
+
x = SAFE + contentW / Math.max(rankCount, 1) * rank + (contentW / Math.max(rankCount, 1) - NODE_W) / 2;
|
|
475
|
+
y = TITLE_BAND + contentH / (rowCount + 1) * (idx + 1) - NODE_H / 2;
|
|
476
|
+
}
|
|
477
|
+
x = Math.round(x / 8) * 8;
|
|
478
|
+
y = Math.round(y / 8) * 8;
|
|
479
|
+
const style = decl.style ? ast.styles[decl.style]?.props : void 0;
|
|
480
|
+
nodes.push({
|
|
481
|
+
id,
|
|
482
|
+
kind: decl.kind,
|
|
483
|
+
role,
|
|
484
|
+
label: decl.label,
|
|
485
|
+
x,
|
|
486
|
+
y,
|
|
487
|
+
width: NODE_W,
|
|
488
|
+
height: NODE_H,
|
|
489
|
+
style,
|
|
490
|
+
opacity: 0
|
|
805
491
|
});
|
|
806
|
-
}
|
|
492
|
+
});
|
|
807
493
|
}
|
|
808
|
-
return
|
|
494
|
+
return nodes;
|
|
809
495
|
}
|
|
810
|
-
function
|
|
811
|
-
const
|
|
812
|
-
for (const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (eqIdx === -1) {
|
|
817
|
-
warnings.push({
|
|
818
|
-
kind: "unknown-modifier",
|
|
819
|
-
message: `expected "key=value" in with-clause, got "${t}"`,
|
|
820
|
-
line: lineNum
|
|
821
|
-
});
|
|
496
|
+
function resolveTargets(targets, ast, groups) {
|
|
497
|
+
const out = [];
|
|
498
|
+
for (const t of targets) {
|
|
499
|
+
if (t === "$nodes") {
|
|
500
|
+
out.push(...Object.keys(ast.nodes));
|
|
501
|
+
} else if (t === "$edges") {
|
|
822
502
|
continue;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (
|
|
827
|
-
|
|
828
|
-
kind: "unknown-modifier",
|
|
829
|
-
message: `modifier "${key}" needs a numeric value \u2014 ignored`,
|
|
830
|
-
line: lineNum
|
|
831
|
-
});
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (MODIFIER_KEYS.has(key)) {
|
|
835
|
-
result[key] = val;
|
|
503
|
+
} else if (t.startsWith("$")) {
|
|
504
|
+
const g = t.slice(1);
|
|
505
|
+
if (groups[g]) out.push(...groups[g]);
|
|
506
|
+
} else if (groups[t]) {
|
|
507
|
+
out.push(...groups[t]);
|
|
836
508
|
} else {
|
|
837
|
-
|
|
838
|
-
kind: "unknown-modifier",
|
|
839
|
-
message: `unknown modifier "${key}" \u2014 ignored`,
|
|
840
|
-
line: lineNum
|
|
841
|
-
});
|
|
509
|
+
out.push(t);
|
|
842
510
|
}
|
|
843
511
|
}
|
|
844
|
-
return
|
|
845
|
-
}
|
|
846
|
-
function parseActorTrailer(trailerRaw, lineNum, warnings) {
|
|
847
|
-
const trimmed = trailerRaw.trim();
|
|
848
|
-
if (!trimmed) return {};
|
|
849
|
-
const withMatch = /(^|\s)with(\s|$)/.exec(trimmed);
|
|
850
|
-
if (!withMatch) {
|
|
851
|
-
return parseSpaceModifiers(trimmed, lineNum, warnings);
|
|
852
|
-
}
|
|
853
|
-
const before = trimmed.slice(0, withMatch.index).trim();
|
|
854
|
-
const after = trimmed.slice(withMatch.index + withMatch[0].length).trim();
|
|
855
|
-
const fromSpace = before ? parseSpaceModifiers(before, lineNum, warnings) : {};
|
|
856
|
-
const fromWith = after ? parseWithModifiers(after, lineNum, warnings) : {};
|
|
857
|
-
return { ...fromSpace, ...fromWith };
|
|
858
|
-
}
|
|
859
|
-
var ASSET_RE = /^asset\s+(\w+)\s*=\s*(image|icon)\("([^"]+)"\)$/;
|
|
860
|
-
var ACTOR_NUM_POS_RE = /^actor\s+(\w+)\s*=\s*([\w.]+)\(([^)]*)\)\s+at\s+\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)(.*)$/;
|
|
861
|
-
var ACTOR_ANCHOR_POS_RE = /^actor\s+(\w+)\s*=\s*([\w.]+)\(([^)]*)\)\s+at\s+(top|bottom|center)\b(.*)$/;
|
|
862
|
-
var EVENT_RE = /^@([\d.]+):\s+(\w+)\.(!?\w+)\((.*)\)$/;
|
|
863
|
-
var REL_EVENT_RE = /^@\+([\d.]+):\s+(\w+)\.(!?\w+)\((.*)\)$/;
|
|
864
|
-
var VAR_RE = /^var\s+(\w+)\s*=\s*(.+)$/;
|
|
865
|
-
var DEF_HEADER_RE = /^def\s+(\w+)\(([^)]*)\)\s*\{$/;
|
|
866
|
-
var DEF_BODY_RE = /^\s*([\w]+)\(([^)]*)\)\s*$/;
|
|
867
|
-
var SEQ_HEADER_RE = /^seq\s+(\w+)(?:\(([^)]*)\))?\s*\{$/;
|
|
868
|
-
var SEQ_EVENT_RE = /^@\+([\d.]+):\s+\$\.(!?\w+)\((.*)\)$/;
|
|
869
|
-
var CHAPTER_HEADER_RE = /^scene\s+"([^"]+)"\s*\{$/;
|
|
870
|
-
var IMPORT_RE = /^import\s+"([^"]+)"\s+as\s+(\w+)\s*$/;
|
|
871
|
-
var PRESET_RE = /^preset\s+(\w+)(?:\s*\((.*)\))?\s*$/;
|
|
872
|
-
var ARCHITECTURE_NODE_RE = new RegExp(
|
|
873
|
-
`^(${TECHNICAL_NODE_TYPES.join("|")})\\s+(\\w+)(?:\\s+("[^"]+"|'[^']+'))?(?:\\s+at\\s+\\(\\s*(-?[\\d.]+)\\s*,\\s*(-?[\\d.]+)\\s*\\))?(.*)$`
|
|
874
|
-
);
|
|
875
|
-
var ARCHITECTURE_ALIAS_TO_TYPE = {
|
|
876
|
-
api: "service",
|
|
877
|
-
database: "database",
|
|
878
|
-
db: "db",
|
|
879
|
-
user: "client",
|
|
880
|
-
client: "client",
|
|
881
|
-
service: "service",
|
|
882
|
-
queue: "queue",
|
|
883
|
-
cache: "cache",
|
|
884
|
-
cloud: "cloud",
|
|
885
|
-
region: "region",
|
|
886
|
-
container: "container",
|
|
887
|
-
microservice: "microservice",
|
|
888
|
-
cluster: "cluster"
|
|
889
|
-
};
|
|
890
|
-
function interpolate(s, vars) {
|
|
891
|
-
return s.replace(
|
|
892
|
-
/\\?\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g,
|
|
893
|
-
(_, name) => vars[name] ?? `\${${name}}`
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
var DEFAULTS = {
|
|
897
|
-
width: 800,
|
|
898
|
-
height: 400,
|
|
899
|
-
fps: 30,
|
|
900
|
-
bg: "white"
|
|
901
|
-
};
|
|
902
|
-
var CAPTION_TOP_Y_FRAC = 0.12;
|
|
903
|
-
var CAPTION_BOTTOM_Y_FRAC = 0.88;
|
|
904
|
-
var CAPTION_CENTER_Y_FRAC = 0.5;
|
|
905
|
-
var KNOWN_SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "bg", "duration"]);
|
|
906
|
-
function validateMoveTarget(action, params, meta, actor, line) {
|
|
907
|
-
if (action !== "move") return;
|
|
908
|
-
const to = params.to;
|
|
909
|
-
if (!Array.isArray(to) || to.length < 2) return;
|
|
910
|
-
const [x, y] = to;
|
|
911
|
-
if (x < 0 || x > meta.width || y < 0 || y > meta.height) {
|
|
912
|
-
throw new ParseError(
|
|
913
|
-
`Actor "${actor}" move target (${x}, ${y}) is outside scene bounds (0\u2013${meta.width}, 0\u2013${meta.height})`,
|
|
914
|
-
line
|
|
915
|
-
);
|
|
916
|
-
}
|
|
512
|
+
return [...new Set(out)];
|
|
917
513
|
}
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
514
|
+
function scheduleBeats(ast, edges) {
|
|
515
|
+
const cues = [];
|
|
516
|
+
const beatRanges = [];
|
|
517
|
+
let t = 0;
|
|
518
|
+
let edgeCounter = edges.length;
|
|
519
|
+
const hasIntro = ast.beats.some((b) => b.cues.some((c) => c.kind === "show"));
|
|
520
|
+
if (!hasIntro && Object.keys(ast.nodes).length > 0) {
|
|
521
|
+
cues.push({
|
|
522
|
+
start: 0,
|
|
523
|
+
duration: DEFAULTS.show,
|
|
524
|
+
kind: "show",
|
|
525
|
+
targets: Object.keys(ast.nodes),
|
|
526
|
+
params: { stagger: DEFAULTS.stagger },
|
|
527
|
+
beat: "__intro"
|
|
528
|
+
});
|
|
529
|
+
t += DEFAULTS.show + DEFAULTS.cueGap;
|
|
922
530
|
}
|
|
923
|
-
const ast
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
};
|
|
936
|
-
const actorCountWarningThreshold = opts.actorCountWarningThreshold ?? 10;
|
|
937
|
-
const labelLengthWarningThreshold = opts.labelLengthWarningThreshold ?? 36;
|
|
938
|
-
let actorCountWarned = false;
|
|
939
|
-
let sceneFound = false;
|
|
940
|
-
const lines = source.split(/\r?\n/);
|
|
941
|
-
let inDef = null;
|
|
942
|
-
let defNeedsClose = false;
|
|
943
|
-
let inSeq = null;
|
|
944
|
-
let inChapter = null;
|
|
945
|
-
const topScope = { name: "", prevEnd: 0 };
|
|
946
|
-
for (let i = 0; i < lines.length; i++) {
|
|
947
|
-
const lineNum = i + 1;
|
|
948
|
-
const rawUntouched = lines[i].trim();
|
|
949
|
-
if (!inDef && !defNeedsClose && !inSeq && rawUntouched.startsWith("var ")) {
|
|
950
|
-
const varLine = interpolate(rawUntouched, ast.vars);
|
|
951
|
-
const vm = VAR_RE.exec(varLine);
|
|
952
|
-
if (!vm) {
|
|
953
|
-
throw new ParseError(`Invalid var declaration: ${varLine}`, lineNum);
|
|
954
|
-
}
|
|
955
|
-
const [, name, value] = vm;
|
|
956
|
-
ast.vars[name] = value.trim();
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
let raw = stripComment(lines[i]).trim();
|
|
960
|
-
if (!raw) continue;
|
|
961
|
-
raw = interpolate(raw, ast.vars);
|
|
962
|
-
if (raw === "}") {
|
|
963
|
-
if (defNeedsClose) {
|
|
964
|
-
defNeedsClose = false;
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
if (inDef) {
|
|
968
|
-
throw new ParseError(`Empty def body for "${inDef.name}"`, lineNum);
|
|
969
|
-
}
|
|
970
|
-
if (inSeq) {
|
|
971
|
-
ast.seqs[inSeq.name] = { params: inSeq.params, events: inSeq.events };
|
|
972
|
-
inSeq = null;
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (inChapter) {
|
|
976
|
-
const endTime = inChapter.scope.prevEnd;
|
|
977
|
-
const startTime = inChapter.earliestEventTime === Infinity ? inChapter.openedAt : Math.min(inChapter.openedAt, inChapter.earliestEventTime);
|
|
978
|
-
ast.chapters.push({
|
|
979
|
-
name: inChapter.name,
|
|
980
|
-
startLine: inChapter.startLine,
|
|
981
|
-
startTime,
|
|
982
|
-
endTime
|
|
983
|
-
});
|
|
984
|
-
if (endTime > topScope.prevEnd) topScope.prevEnd = endTime;
|
|
985
|
-
inChapter = null;
|
|
986
|
-
continue;
|
|
987
|
-
}
|
|
988
|
-
throw new ParseError("Unexpected '}'", lineNum);
|
|
989
|
-
}
|
|
990
|
-
if (inDef) {
|
|
991
|
-
const bm = DEF_BODY_RE.exec(raw);
|
|
992
|
-
if (!bm) {
|
|
993
|
-
throw new ParseError(`Invalid def body (expected "type(args)"): ${raw}`, lineNum);
|
|
994
|
-
}
|
|
995
|
-
const [, actorType, bodyArgsRaw] = bm;
|
|
996
|
-
if (!isKnownActorType(actorType)) {
|
|
997
|
-
throw new ParseError(`Unknown actor type in def body: "${actorType}"`, lineNum);
|
|
998
|
-
}
|
|
999
|
-
const bodyArgs = bodyArgsRaw.trim() ? splitByComma(bodyArgsRaw).map((a) => a.trim()) : [];
|
|
1000
|
-
ast.defs[inDef.name] = {
|
|
1001
|
-
params: inDef.params,
|
|
1002
|
-
actorType,
|
|
1003
|
-
bodyArgs
|
|
1004
|
-
};
|
|
1005
|
-
inDef = null;
|
|
1006
|
-
defNeedsClose = true;
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
if (inSeq) {
|
|
1010
|
-
const interpolatedSeq = interpolate(raw, ast.vars);
|
|
1011
|
-
const sm = SEQ_EVENT_RE.exec(interpolatedSeq);
|
|
1012
|
-
if (!sm) {
|
|
1013
|
-
throw new ParseError(`Invalid seq event (expected "@+offset: $.action(params)"): ${raw}`, lineNum);
|
|
1014
|
-
}
|
|
1015
|
-
const [, offsetStr, action, paramsRaw] = sm;
|
|
1016
|
-
inSeq.events.push({ offset: Number(offsetStr), action, paramsRaw });
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
if (raw.startsWith("import ")) {
|
|
1020
|
-
const im = IMPORT_RE.exec(raw);
|
|
1021
|
-
if (!im) {
|
|
1022
|
-
throw new ParseError(`Invalid import declaration: ${raw}`, lineNum);
|
|
1023
|
-
}
|
|
1024
|
-
const [, path, namespace] = im;
|
|
1025
|
-
const decl = { path, namespace, line: lineNum };
|
|
1026
|
-
ast.imports.push(decl);
|
|
1027
|
-
const resolved = opts.imports?.[namespace];
|
|
1028
|
-
if (!resolved) {
|
|
1029
|
-
ast.warnings.push({
|
|
1030
|
-
kind: "import-unresolved",
|
|
1031
|
-
message: `import "${path}" as ${namespace} \u2014 no pre-parsed AST supplied by host`,
|
|
1032
|
-
line: lineNum
|
|
1033
|
-
});
|
|
1034
|
-
} else {
|
|
1035
|
-
for (const [k, v] of Object.entries(resolved.vars)) {
|
|
1036
|
-
ast.vars[`${namespace}.${k}`] = v;
|
|
1037
|
-
}
|
|
1038
|
-
for (const [k, v] of Object.entries(resolved.defs)) {
|
|
1039
|
-
ast.defs[`${namespace}.${k}`] = v;
|
|
1040
|
-
}
|
|
1041
|
-
for (const [k, v] of Object.entries(resolved.seqs)) {
|
|
1042
|
-
ast.seqs[`${namespace}.${k}`] = v;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
continue;
|
|
1046
|
-
}
|
|
1047
|
-
if (raw.startsWith("def ")) {
|
|
1048
|
-
const dm = DEF_HEADER_RE.exec(raw);
|
|
1049
|
-
if (!dm) {
|
|
1050
|
-
throw new ParseError(`Invalid def declaration: ${raw}`, lineNum);
|
|
1051
|
-
}
|
|
1052
|
-
const [, name, paramsRaw] = dm;
|
|
1053
|
-
const params = paramsRaw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1054
|
-
inDef = { name, params, startLine: lineNum };
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (raw.startsWith("seq ")) {
|
|
1058
|
-
const sm = SEQ_HEADER_RE.exec(raw);
|
|
1059
|
-
if (!sm) {
|
|
1060
|
-
throw new ParseError(`Invalid seq declaration: ${raw}`, lineNum);
|
|
1061
|
-
}
|
|
1062
|
-
const [, name, paramsRaw] = sm;
|
|
1063
|
-
const params = paramsRaw ? paramsRaw.split(",").map((p) => p.trim()).filter(Boolean) : [];
|
|
1064
|
-
inSeq = { name, params, events: [], startLine: lineNum };
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (/^scene(\s|$)/.test(raw)) {
|
|
1068
|
-
const chm = CHAPTER_HEADER_RE.exec(raw);
|
|
1069
|
-
if (chm) {
|
|
1070
|
-
if (inChapter) {
|
|
1071
|
-
throw new ParseError(
|
|
1072
|
-
`Nested chapters are not supported; close chapter "${inChapter.name}" first`,
|
|
1073
|
-
lineNum
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
const chapterName = chm[1];
|
|
1077
|
-
inChapter = {
|
|
1078
|
-
name: chapterName,
|
|
1079
|
-
startLine: lineNum,
|
|
1080
|
-
openedAt: topScope.prevEnd,
|
|
1081
|
-
earliestEventTime: Infinity,
|
|
1082
|
-
scope: { name: chapterName, prevEnd: topScope.prevEnd }
|
|
1083
|
-
};
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
if (inChapter) {
|
|
1087
|
-
throw new ParseError(
|
|
1088
|
-
`scene header inside chapter "${inChapter.name}" is not allowed; close the chapter first or use 'scene "title" { ... }' for a nested section`,
|
|
1089
|
-
lineNum
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
if (sceneFound) {
|
|
1093
|
-
throw new ParseError("Duplicate scene declaration", lineNum);
|
|
1094
|
-
}
|
|
1095
|
-
sceneFound = true;
|
|
1096
|
-
for (const [, key, val] of raw.matchAll(/(\w+)=([\S]+)/g)) {
|
|
1097
|
-
switch (key) {
|
|
1098
|
-
case "width":
|
|
1099
|
-
ast.meta.width = Number(val);
|
|
1100
|
-
break;
|
|
1101
|
-
case "height":
|
|
1102
|
-
ast.meta.height = Number(val);
|
|
1103
|
-
break;
|
|
1104
|
-
case "fps":
|
|
1105
|
-
ast.meta.fps = Number(val);
|
|
1106
|
-
break;
|
|
1107
|
-
case "bg":
|
|
1108
|
-
ast.meta.bg = val;
|
|
1109
|
-
break;
|
|
1110
|
-
case "duration":
|
|
1111
|
-
ast.meta.duration = Number(val);
|
|
1112
|
-
break;
|
|
1113
|
-
default:
|
|
1114
|
-
if (KNOWN_SCENE_KEYS.has(key)) break;
|
|
1115
|
-
ast.warnings.push({
|
|
1116
|
-
kind: "unknown-scene-key",
|
|
1117
|
-
message: `unknown scene property "${key}" \u2014 ignored`,
|
|
1118
|
-
line: lineNum
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1124
|
-
if (raw.startsWith("asset")) {
|
|
1125
|
-
const m = ASSET_RE.exec(raw);
|
|
1126
|
-
if (!m) {
|
|
1127
|
-
throw new ParseError(`Invalid asset declaration: ${raw}`, lineNum);
|
|
1128
|
-
}
|
|
1129
|
-
const [, name, type, value] = m;
|
|
1130
|
-
ast.assets[name] = { type, value };
|
|
1131
|
-
continue;
|
|
1132
|
-
}
|
|
1133
|
-
if (raw.startsWith("preset ")) {
|
|
1134
|
-
const pm = PRESET_RE.exec(raw);
|
|
1135
|
-
const maybeName = pm?.[1];
|
|
1136
|
-
if (maybeName && !PRESETS[maybeName]) {
|
|
1137
|
-
const nameList = Object.keys(PRESETS).sort().join(", ");
|
|
1138
|
-
ast.warnings.push({
|
|
1139
|
-
kind: "unknown-preset",
|
|
1140
|
-
message: `unknown preset "${maybeName}" \u2014 available: ${nameList}`,
|
|
1141
|
-
line: lineNum
|
|
1142
|
-
});
|
|
1143
|
-
} else {
|
|
1144
|
-
ast.warnings.push({
|
|
1145
|
-
kind: "preset-mixed",
|
|
1146
|
-
message: "`preset` is a whole-file shorthand; mid-file presets are ignored",
|
|
1147
|
-
line: lineNum
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1152
|
-
if (raw.startsWith("actor ") || isArchitectureNodeLine(raw)) {
|
|
1153
|
-
const actorInfo = parseActorLine(raw, lineNum, ast);
|
|
1154
|
-
if (!actorCountWarned && actorCountWarningThreshold > 0) {
|
|
1155
|
-
const actorCount = Object.keys(ast.actors).length;
|
|
1156
|
-
if (actorCount > actorCountWarningThreshold) {
|
|
1157
|
-
actorCountWarned = true;
|
|
1158
|
-
ast.warnings.push({
|
|
1159
|
-
kind: "actor-count-threshold",
|
|
1160
|
-
message: `scene has ${actorCount} actors (threshold ${actorCountWarningThreshold}); consider splitting into multiple scenes`,
|
|
1161
|
-
line: lineNum
|
|
1162
|
-
});
|
|
531
|
+
for (const beat of ast.beats) {
|
|
532
|
+
const beatStart = t;
|
|
533
|
+
const scheduled = [];
|
|
534
|
+
const processCue = (cue, parallel = false) => {
|
|
535
|
+
if (cue.kind === "parallel") {
|
|
536
|
+
const start = t;
|
|
537
|
+
let maxDur = 0;
|
|
538
|
+
for (const child of cue.cues) {
|
|
539
|
+
const before = t;
|
|
540
|
+
processCue(child, true);
|
|
541
|
+
maxDur = Math.max(maxDur, t - before);
|
|
542
|
+
if (!parallel) t = before;
|
|
1163
543
|
}
|
|
544
|
+
if (!parallel) t = start + maxDur;
|
|
545
|
+
return;
|
|
1164
546
|
}
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
547
|
+
if (cue.kind === "use") return;
|
|
548
|
+
const dur = "dur" in cue && cue.dur !== void 0 ? cue.dur : DEFAULTS[cue.kind] ?? 0.5;
|
|
549
|
+
if (cue.kind === "flow") {
|
|
550
|
+
for (const seg of cue.segments) {
|
|
551
|
+
const edgeId = `flow_${++edgeCounter}`;
|
|
552
|
+
scheduled.push({
|
|
553
|
+
start: t,
|
|
554
|
+
duration: dur / cue.segments.length,
|
|
555
|
+
kind: "flow",
|
|
556
|
+
targets: [seg.from, seg.to],
|
|
557
|
+
edgeId,
|
|
558
|
+
segments: [seg],
|
|
559
|
+
params: {},
|
|
560
|
+
beat: beat.name
|
|
1172
561
|
});
|
|
562
|
+
t += dur / cue.segments.length + DEFAULTS.cueGap / cue.segments.length;
|
|
1173
563
|
}
|
|
564
|
+
return;
|
|
1174
565
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
for (const ev of ast.events) {
|
|
1202
|
-
const dur = typeof ev.params.dur === "number" ? ev.params.dur : 0;
|
|
1203
|
-
maxEnd = Math.max(maxEnd, ev.time + dur);
|
|
1204
|
-
}
|
|
1205
|
-
if (maxEnd > 0) ast.meta.duration = round3(maxEnd);
|
|
566
|
+
if (cue.kind !== "show" && cue.kind !== "hide" && cue.kind !== "glow" && cue.kind !== "focus") return;
|
|
567
|
+
const targets = resolveTargets(cue.targets, ast, Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members])));
|
|
568
|
+
scheduled.push({
|
|
569
|
+
start: t,
|
|
570
|
+
duration: dur,
|
|
571
|
+
kind: cue.kind,
|
|
572
|
+
targets,
|
|
573
|
+
params: {
|
|
574
|
+
stagger: cue.kind === "show" ? cue.stagger ?? DEFAULTS.stagger : void 0,
|
|
575
|
+
color: cue.kind === "glow" ? cue.color : void 0,
|
|
576
|
+
strength: cue.kind === "glow" ? cue.strength : void 0,
|
|
577
|
+
zoom: cue.kind === "focus" ? cue.zoom : void 0
|
|
578
|
+
},
|
|
579
|
+
beat: beat.name
|
|
580
|
+
});
|
|
581
|
+
t += dur + DEFAULTS.cueGap;
|
|
582
|
+
};
|
|
583
|
+
for (const cue of beat.cues) processCue(cue);
|
|
584
|
+
cues.push(...scheduled);
|
|
585
|
+
beatRanges.push({
|
|
586
|
+
name: beat.name,
|
|
587
|
+
label: beat.label,
|
|
588
|
+
start: beatStart,
|
|
589
|
+
end: Math.max(t, beatStart + (beat.dur ?? 0))
|
|
590
|
+
});
|
|
591
|
+
t += DEFAULTS.beatGap;
|
|
1206
592
|
}
|
|
1207
|
-
return
|
|
593
|
+
return { cues, beats: beatRanges };
|
|
1208
594
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
595
|
+
function compilePlan(ast, theme) {
|
|
596
|
+
const structuralEdges = collectStructuralEdges(ast);
|
|
597
|
+
const nodes = layoutNodes(ast, structuralEdges);
|
|
598
|
+
const { cues, beats } = scheduleBeats(ast, structuralEdges);
|
|
599
|
+
const shown = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const cue of cues) {
|
|
601
|
+
if (cue.kind === "show") cue.targets.forEach((id) => shown.add(id));
|
|
602
|
+
}
|
|
603
|
+
if (shown.size === 0) nodes.forEach((n) => {
|
|
604
|
+
n.opacity = 1;
|
|
605
|
+
});
|
|
606
|
+
else nodes.forEach((n) => {
|
|
607
|
+
n.opacity = shown.has(n.id) ? 1 : 0;
|
|
1215
608
|
});
|
|
609
|
+
const duration = ast.meta.duration ?? Math.max(
|
|
610
|
+
...cues.map((c) => c.start + c.duration),
|
|
611
|
+
...beats.map((b) => b.end),
|
|
612
|
+
1
|
|
613
|
+
);
|
|
614
|
+
const title = ast.meta.title ?? "Architecture Diagram";
|
|
615
|
+
return {
|
|
616
|
+
meta: ast.meta,
|
|
617
|
+
theme,
|
|
618
|
+
title,
|
|
619
|
+
nodes,
|
|
620
|
+
edges: structuralEdges,
|
|
621
|
+
cues,
|
|
622
|
+
beats,
|
|
623
|
+
groups: Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members])),
|
|
624
|
+
duration
|
|
625
|
+
};
|
|
1216
626
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
627
|
+
|
|
628
|
+
// src/themes.ts
|
|
629
|
+
var ROLE_COLORS = {
|
|
630
|
+
compute: "#38bdf8",
|
|
631
|
+
code: "#38bdf8",
|
|
632
|
+
client: "#f59e0b",
|
|
633
|
+
data: "#22c55e",
|
|
634
|
+
messaging: "#a78bfa",
|
|
635
|
+
network: "#60a5fa",
|
|
636
|
+
platform: "#c084fc",
|
|
637
|
+
security: "#fb7185",
|
|
638
|
+
delivery: "#facc15",
|
|
639
|
+
observability: "#2dd4bf",
|
|
640
|
+
flow: "#e879f9",
|
|
641
|
+
distributed: "#94a3b8"
|
|
642
|
+
};
|
|
643
|
+
var EDGE_COLORS = {
|
|
644
|
+
request: "#38bdf8",
|
|
645
|
+
response: "#a78bfa",
|
|
646
|
+
event: "#f59e0b",
|
|
647
|
+
dependency: "#64748b"
|
|
648
|
+
};
|
|
649
|
+
var THEMES = {
|
|
650
|
+
midnight: {
|
|
651
|
+
name: "midnight",
|
|
652
|
+
canvas: "#07111f",
|
|
653
|
+
surface: "#0f1b2d",
|
|
654
|
+
surfaceRaised: "#16243a",
|
|
655
|
+
border: "#2b3b52",
|
|
656
|
+
text: "#f4f7fb",
|
|
657
|
+
textMuted: "#9aabc0",
|
|
658
|
+
gridMinor: "rgba(148, 163, 184, 0.13)",
|
|
659
|
+
gridMajor: "rgba(148, 163, 184, 0.2)",
|
|
660
|
+
vignette: "rgba(2, 6, 23, 0.72)",
|
|
661
|
+
accent: "#38bdf8",
|
|
662
|
+
roles: { ...ROLE_COLORS },
|
|
663
|
+
edges: { ...EDGE_COLORS }
|
|
664
|
+
},
|
|
665
|
+
paper: {
|
|
666
|
+
name: "paper",
|
|
667
|
+
canvas: "#f7f9fc",
|
|
668
|
+
surface: "#ffffff",
|
|
669
|
+
surfaceRaised: "#eef3f8",
|
|
670
|
+
border: "#ccd7e5",
|
|
671
|
+
text: "#132033",
|
|
672
|
+
textMuted: "#5d6d82",
|
|
673
|
+
gridMinor: "rgba(15, 23, 42, 0.09)",
|
|
674
|
+
gridMajor: "rgba(15, 23, 42, 0.14)",
|
|
675
|
+
vignette: "rgba(241, 245, 249, 0.74)",
|
|
676
|
+
accent: "#0ea5e9",
|
|
677
|
+
roles: { ...ROLE_COLORS },
|
|
678
|
+
edges: { ...EDGE_COLORS }
|
|
1257
679
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
680
|
+
};
|
|
681
|
+
function resolveTheme(name) {
|
|
682
|
+
return THEMES[name] ?? THEMES.midnight;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/parser.ts
|
|
686
|
+
var ParseError = class extends Error {
|
|
687
|
+
line;
|
|
688
|
+
column;
|
|
689
|
+
constructor(message, line, column) {
|
|
690
|
+
super(`line ${line}: ${message}`);
|
|
691
|
+
this.name = "ParseError";
|
|
692
|
+
this.line = line;
|
|
693
|
+
this.column = column;
|
|
1263
694
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
695
|
+
};
|
|
696
|
+
var LEGACY_PATTERNS = [
|
|
697
|
+
{ re: /^\s*actor\s+/i, message: 'MarkdyScript 0.8 removed `actor` declarations. Use node kinds instead: `service API "Label"`.' },
|
|
698
|
+
{ re: /^\s*@\d/, message: "MarkdyScript 0.8 removed `@time:` events. Use `beat` blocks with flow chains and cues." },
|
|
699
|
+
{ re: /^\s*def\s+/i, message: "MarkdyScript 0.8 removed `def`. Use `pattern name(...):` for reusable flows." },
|
|
700
|
+
{ re: /^\s*seq\s+/i, message: "MarkdyScript 0.8 removed `seq`. Use `pattern` + `use` inside beats." },
|
|
701
|
+
{ re: /^\s*preset\s+/i, message: "MarkdyScript 0.8 removed presets. Use `scene` + `beat` composition." },
|
|
702
|
+
{ re: /^\s*asset\s+/i, message: "MarkdyScript 0.8 is diagram-only; `asset` declarations are not supported." },
|
|
703
|
+
{ re: /figure\s*\(/i, message: "MarkdyScript 0.8 is diagram-only; `figure` actors were removed." }
|
|
704
|
+
];
|
|
705
|
+
var FLOW_OP_RE = /(->|<-|~>|--)/;
|
|
706
|
+
var PROP_RE = /(\w[\w.-]*)=(\S+)/g;
|
|
707
|
+
function stripComment(line) {
|
|
708
|
+
const idx = line.indexOf("//");
|
|
709
|
+
return idx >= 0 ? line.slice(0, idx) : line;
|
|
710
|
+
}
|
|
711
|
+
function parseProps(raw) {
|
|
712
|
+
const props = {};
|
|
713
|
+
for (const match of raw.matchAll(PROP_RE)) {
|
|
714
|
+
const key = match[1];
|
|
715
|
+
let val = match[2];
|
|
716
|
+
if (typeof val === "string") {
|
|
717
|
+
if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
|
|
718
|
+
else if (val === "true") val = true;
|
|
719
|
+
else if (val === "false") val = false;
|
|
720
|
+
else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
|
|
721
|
+
else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
|
|
1275
722
|
}
|
|
1276
|
-
|
|
1277
|
-
resolvedArgs = tmpl.bodyArgs.map((a) => interpolate(a, localVars));
|
|
1278
|
-
} else {
|
|
1279
|
-
throw new ParseError(`Unknown actor type or template: "${typeName}"`, lineNum);
|
|
1280
|
-
}
|
|
1281
|
-
const modifiers = parseActorTrailer(trailer, lineNum, ast.warnings);
|
|
1282
|
-
if (anchor && resolvedType !== "caption") {
|
|
1283
|
-
throw new ParseError(
|
|
1284
|
-
`anchor syntax "at ${anchor}" only applies to caption actors; got ${typeName}`,
|
|
1285
|
-
lineNum
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
if (resolvedType === "caption" && !anchor) {
|
|
1289
|
-
throw new ParseError(
|
|
1290
|
-
`Caption actors require anchor syntax (\`at top | bottom | center\`); got numeric position (${x}, ${y}) for "${name}"`,
|
|
1291
|
-
lineNum
|
|
1292
|
-
);
|
|
723
|
+
props[key] = val;
|
|
1293
724
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
725
|
+
return props;
|
|
726
|
+
}
|
|
727
|
+
function parseStringToken(raw) {
|
|
728
|
+
const trimmed = raw.trimStart();
|
|
729
|
+
if (!trimmed.startsWith('"')) return null;
|
|
730
|
+
let i = 1;
|
|
731
|
+
let value = "";
|
|
732
|
+
while (i < trimmed.length) {
|
|
733
|
+
const ch = trimmed[i];
|
|
734
|
+
if (ch === '"') return { value, rest: trimmed.slice(i + 1).trim() };
|
|
735
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
736
|
+
value += trimmed[i + 1];
|
|
737
|
+
i += 2;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
value += ch;
|
|
741
|
+
i++;
|
|
1299
742
|
}
|
|
1300
|
-
|
|
1301
|
-
type: resolvedType,
|
|
1302
|
-
args: resolvedArgs,
|
|
1303
|
-
x,
|
|
1304
|
-
y,
|
|
1305
|
-
...modifiers,
|
|
1306
|
-
...anchor ? { anchor } : {}
|
|
1307
|
-
};
|
|
1308
|
-
return { name, args: resolvedArgs };
|
|
743
|
+
return null;
|
|
1309
744
|
}
|
|
1310
|
-
function
|
|
1311
|
-
return
|
|
745
|
+
function splitTargets(raw) {
|
|
746
|
+
return raw.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
|
|
1312
747
|
}
|
|
1313
|
-
function
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
throw new ParseError(
|
|
1319
|
-
`"camera" is a reserved actor name; choose a different architecture node name`,
|
|
1320
|
-
lineNum
|
|
1321
|
-
);
|
|
748
|
+
function parseFlowChain(line, lineNo) {
|
|
749
|
+
const segments = [];
|
|
750
|
+
const parts = line.split(FLOW_OP_RE).map((p) => p.trim()).filter(Boolean);
|
|
751
|
+
if (parts.length < 3) {
|
|
752
|
+
throw new ParseError(`expected flow chain like A -> B "label"`, lineNo);
|
|
1322
753
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
);
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
);
|
|
754
|
+
let i = 0;
|
|
755
|
+
let from = splitTargetLabel(parts[i++], lineNo).node;
|
|
756
|
+
while (i < parts.length) {
|
|
757
|
+
const opToken = parts[i++];
|
|
758
|
+
const op = EDGE_OPERATORS[opToken];
|
|
759
|
+
if (!op) throw new ParseError(`unknown flow operator '${opToken}'`, lineNo);
|
|
760
|
+
if (i >= parts.length) throw new ParseError(`expected target after '${opToken}'`, lineNo);
|
|
761
|
+
const { node: to, label } = splitTargetLabel(parts[i++], lineNo);
|
|
762
|
+
if (!to) throw new ParseError(`expected target node after '${opToken}'`, lineNo);
|
|
763
|
+
segments.push({
|
|
764
|
+
from: op === "response" ? to : from,
|
|
765
|
+
op,
|
|
766
|
+
to: op === "response" ? from : to,
|
|
767
|
+
label
|
|
768
|
+
});
|
|
769
|
+
from = op === "response" ? from : to;
|
|
1338
770
|
}
|
|
1339
|
-
|
|
1340
|
-
const label = quoted ?? labelRaw?.trim() ?? name;
|
|
1341
|
-
const modifiers = parseActorTrailer(trailer, lineNum, ast.warnings);
|
|
1342
|
-
ast.actors[name] = {
|
|
1343
|
-
type: typeName,
|
|
1344
|
-
args: [label],
|
|
1345
|
-
x,
|
|
1346
|
-
y,
|
|
1347
|
-
...modifiers
|
|
1348
|
-
};
|
|
1349
|
-
return { name, args: [label] };
|
|
1350
|
-
}
|
|
1351
|
-
function autoNodeX(index, width) {
|
|
1352
|
-
const columns = Math.max(1, Math.floor(width / 220));
|
|
1353
|
-
const gutter = width / (columns + 1);
|
|
1354
|
-
return Math.round(gutter * (index % columns + 1));
|
|
771
|
+
return segments;
|
|
1355
772
|
}
|
|
1356
|
-
function
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
773
|
+
function splitTargetLabel(token, lineNo) {
|
|
774
|
+
const quoteIdx = token.indexOf('"');
|
|
775
|
+
if (quoteIdx < 0) return { node: token.trim() };
|
|
776
|
+
const parsed = parseStringToken(token.slice(quoteIdx));
|
|
777
|
+
if (!parsed) throw new ParseError("unterminated string in flow label", lineNo);
|
|
778
|
+
const before = token.slice(0, quoteIdx).trim();
|
|
779
|
+
const node = before || parsed.rest.trim();
|
|
780
|
+
return { node, label: parsed.value };
|
|
1360
781
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
782
|
+
function parseCueLine(line, lineNo) {
|
|
783
|
+
const trimmed = line.trim();
|
|
784
|
+
const propsMatch = trimmed.match(/\s+(?:dur|stagger|color|strength|zoom|after)=\S+/);
|
|
785
|
+
const props = propsMatch ? parseProps(propsMatch[0]) : {};
|
|
786
|
+
if (trimmed.includes(" & ")) {
|
|
787
|
+
const parts = trimmed.split(/\s+&\s+/);
|
|
788
|
+
return {
|
|
789
|
+
kind: "parallel",
|
|
790
|
+
cues: parts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
|
|
791
|
+
line: lineNo
|
|
792
|
+
};
|
|
1366
793
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
794
|
+
if (FLOW_OP_RE.test(trimmed)) {
|
|
795
|
+
const chainPart = trimmed.split(/\s+(?:dur|stagger|color|strength|zoom|after)=/)[0].trim();
|
|
796
|
+
return {
|
|
797
|
+
kind: "flow",
|
|
798
|
+
segments: parseFlowChain(chainPart, lineNo),
|
|
799
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
800
|
+
line: lineNo
|
|
801
|
+
};
|
|
1370
802
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
803
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
804
|
+
const keyword = head.toLowerCase();
|
|
805
|
+
if (keyword === "show" || keyword === "hide") {
|
|
806
|
+
const targetRaw = rest.join(" ").split(/\s+(?:dur|stagger)=/)[0];
|
|
807
|
+
return {
|
|
808
|
+
kind: keyword,
|
|
809
|
+
targets: splitTargets(targetRaw),
|
|
810
|
+
stagger: typeof props.stagger === "number" ? props.stagger : void 0,
|
|
811
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
812
|
+
line: lineNo
|
|
813
|
+
};
|
|
1373
814
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
815
|
+
if (keyword === "glow") {
|
|
816
|
+
const targetRaw = rest.join(" ").split(/\s+(?:color|strength|dur)=/)[0];
|
|
817
|
+
return {
|
|
818
|
+
kind: "glow",
|
|
819
|
+
targets: splitTargets(targetRaw),
|
|
820
|
+
color: typeof props.color === "string" ? props.color : void 0,
|
|
821
|
+
strength: typeof props.strength === "number" ? props.strength : void 0,
|
|
822
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
823
|
+
line: lineNo
|
|
824
|
+
};
|
|
1377
825
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
826
|
+
if (keyword === "focus") {
|
|
827
|
+
const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
|
|
828
|
+
return {
|
|
829
|
+
kind: "focus",
|
|
830
|
+
targets: splitTargets(targetRaw),
|
|
831
|
+
zoom: typeof props.zoom === "number" ? props.zoom : void 0,
|
|
832
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
833
|
+
line: lineNo
|
|
834
|
+
};
|
|
1382
835
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
836
|
+
if (keyword === "use") {
|
|
837
|
+
const call = rest.join(" ");
|
|
838
|
+
const m = call.match(/^(\w+)\s*\((.*)\)\s*$/);
|
|
839
|
+
if (!m) throw new ParseError(`expected use pattern(args)`, lineNo);
|
|
840
|
+
const args = {};
|
|
841
|
+
if (m[2].trim()) {
|
|
842
|
+
for (const part of m[2].split(",")) {
|
|
843
|
+
const trimmed2 = part.trim();
|
|
844
|
+
if (trimmed2.includes("=")) {
|
|
845
|
+
const [k, v] = trimmed2.split("=").map((s) => s.trim());
|
|
846
|
+
if (!k || !v) throw new ParseError(`invalid use argument '${part}'`, lineNo);
|
|
847
|
+
args[k] = v;
|
|
848
|
+
} else {
|
|
849
|
+
args[`__pos_${Object.keys(args).length}`] = trimmed2;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
1387
852
|
}
|
|
1388
|
-
|
|
853
|
+
return { kind: "use", pattern: m[1], args, line: lineNo };
|
|
1389
854
|
}
|
|
1390
|
-
|
|
855
|
+
throw new ParseError(`unknown cue '${head}'`, lineNo);
|
|
1391
856
|
}
|
|
1392
|
-
function
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
if (value < 0) {
|
|
1405
|
-
throw new ParseError(`stagger must not be negative (got ${rawValue})`, lineNum);
|
|
1406
|
-
}
|
|
1407
|
-
step = value;
|
|
857
|
+
function expandPatternCues(cues, patterns, line) {
|
|
858
|
+
const out = [];
|
|
859
|
+
for (const cue of cues) {
|
|
860
|
+
if (cue.kind === "use") {
|
|
861
|
+
const pat = patterns[cue.pattern];
|
|
862
|
+
if (!pat) throw new ParseError(`unknown pattern '${cue.pattern}'`, line);
|
|
863
|
+
const substituted = pat.body.map((c) => substitutePatternCue(c, pat.params, cue.args));
|
|
864
|
+
out.push(...substituted);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (cue.kind === "parallel") {
|
|
868
|
+
out.push({ ...cue, cues: expandPatternCues(cue.cues, patterns, line) });
|
|
1408
869
|
continue;
|
|
1409
870
|
}
|
|
1410
|
-
|
|
871
|
+
out.push(cue);
|
|
1411
872
|
}
|
|
1412
|
-
return
|
|
873
|
+
return out;
|
|
1413
874
|
}
|
|
1414
|
-
function
|
|
1415
|
-
const
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
875
|
+
function substitutePatternCue(cue, params, args) {
|
|
876
|
+
const positional = Object.keys(args).filter((k) => k.startsWith("__pos_")).sort().map((k) => args[k]);
|
|
877
|
+
const resolvedArgs = { ...args };
|
|
878
|
+
params.forEach((p, i) => {
|
|
879
|
+
if (resolvedArgs[p] === void 0 && positional[i]) resolvedArgs[p] = positional[i];
|
|
880
|
+
});
|
|
881
|
+
delete resolvedArgs.__pos_0;
|
|
882
|
+
delete resolvedArgs.__pos_1;
|
|
883
|
+
delete resolvedArgs.__pos_2;
|
|
884
|
+
delete resolvedArgs.__pos_3;
|
|
885
|
+
const sub = (s) => {
|
|
886
|
+
for (const p of params) {
|
|
887
|
+
if (resolvedArgs[p]) s = s.replaceAll(`$${p}`, resolvedArgs[p]);
|
|
1419
888
|
}
|
|
889
|
+
return s;
|
|
1420
890
|
};
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
891
|
+
if (cue.kind === "flow") {
|
|
892
|
+
return {
|
|
893
|
+
...cue,
|
|
894
|
+
segments: cue.segments.map((seg) => ({
|
|
895
|
+
...seg,
|
|
896
|
+
from: sub(seg.from),
|
|
897
|
+
to: sub(seg.to),
|
|
898
|
+
label: seg.label ? sub(seg.label) : void 0
|
|
899
|
+
}))
|
|
900
|
+
};
|
|
1425
901
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
let actionToken;
|
|
1429
|
-
let paramsRaw;
|
|
1430
|
-
if (rel) {
|
|
1431
|
-
const [, offsetStr, act, action2, pr] = rel;
|
|
1432
|
-
const offset = Number(offsetStr);
|
|
1433
|
-
if (Number.isNaN(offset)) {
|
|
1434
|
-
throw new ParseError(`Invalid @+offset value: ${offsetStr}`, lineNum);
|
|
1435
|
-
}
|
|
1436
|
-
time = round3(scope.prevEnd + offset);
|
|
1437
|
-
actor = act;
|
|
1438
|
-
actionToken = action2;
|
|
1439
|
-
paramsRaw = pr;
|
|
1440
|
-
} else {
|
|
1441
|
-
const [, timeStr, act, action2, pr] = abs;
|
|
1442
|
-
const t = Number(timeStr);
|
|
1443
|
-
if (Number.isNaN(t)) {
|
|
1444
|
-
throw new ParseError(`Invalid time value: ${timeStr}`, lineNum);
|
|
1445
|
-
}
|
|
1446
|
-
time = t;
|
|
1447
|
-
actor = act;
|
|
1448
|
-
actionToken = action2;
|
|
1449
|
-
paramsRaw = pr;
|
|
902
|
+
if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus") {
|
|
903
|
+
return { ...cue, targets: cue.targets.map(sub) };
|
|
1450
904
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
if (actor === "camera") {
|
|
1454
|
-
if (!isCameraAction(action)) {
|
|
1455
|
-
if (mustUnderstand) {
|
|
1456
|
-
throw new ParseError(`Unknown camera action "${action}"`, lineNum);
|
|
1457
|
-
}
|
|
1458
|
-
ast.warnings.push({
|
|
1459
|
-
kind: "unknown-camera-action",
|
|
1460
|
-
message: `unknown camera action "${action}" \u2014 renderer will no-op`,
|
|
1461
|
-
line: lineNum
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
const params = parseActionParams(action, paramsRaw);
|
|
1465
|
-
recordEventTime(time);
|
|
1466
|
-
pushEvent(ast, scope, {
|
|
1467
|
-
time,
|
|
1468
|
-
actor: "camera",
|
|
1469
|
-
action,
|
|
1470
|
-
params,
|
|
1471
|
-
line: lineNum,
|
|
1472
|
-
...inChapter ? { chapter: inChapter.name } : {}
|
|
1473
|
-
});
|
|
1474
|
-
return;
|
|
905
|
+
if (cue.kind === "parallel") {
|
|
906
|
+
return { ...cue, cues: cue.cues.map((c) => substitutePatternCue(c, params, args)) };
|
|
1475
907
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
908
|
+
return cue;
|
|
909
|
+
}
|
|
910
|
+
function readBlocks(lines) {
|
|
911
|
+
const blocks = [];
|
|
912
|
+
for (let i = 0; i < lines.length; i++) {
|
|
913
|
+
const raw = lines[i];
|
|
914
|
+
if (!raw.trim() || raw.trim().startsWith("//")) continue;
|
|
915
|
+
const indent = raw.match(/^\s*/)?.[0].length ?? 0;
|
|
916
|
+
blocks.push({ line: i + 1, indent, text: raw.trim() });
|
|
1479
917
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
lineNum,
|
|
1490
|
-
ast,
|
|
1491
|
-
scope,
|
|
1492
|
-
inChapter,
|
|
1493
|
-
recordEventTime
|
|
1494
|
-
);
|
|
1495
|
-
});
|
|
1496
|
-
return;
|
|
918
|
+
return blocks;
|
|
919
|
+
}
|
|
920
|
+
function readIndentedBody(blocks, startIdx, parentIndent) {
|
|
921
|
+
const body = [];
|
|
922
|
+
let i = startIdx;
|
|
923
|
+
while (i < blocks.length) {
|
|
924
|
+
if (blocks[i].indent <= parentIndent) break;
|
|
925
|
+
body.push(blocks[i]);
|
|
926
|
+
i++;
|
|
1497
927
|
}
|
|
1498
|
-
|
|
1499
|
-
actor,
|
|
1500
|
-
time,
|
|
1501
|
-
action,
|
|
1502
|
-
paramsRaw,
|
|
1503
|
-
mustUnderstand,
|
|
1504
|
-
lineNum,
|
|
1505
|
-
ast,
|
|
1506
|
-
scope,
|
|
1507
|
-
inChapter,
|
|
1508
|
-
recordEventTime
|
|
1509
|
-
);
|
|
928
|
+
return { body, nextIdx: i };
|
|
1510
929
|
}
|
|
1511
|
-
function
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
930
|
+
function parse(source, opts = {}) {
|
|
931
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
932
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
933
|
+
const line = stripComment(lines[i2]);
|
|
934
|
+
for (const legacy of LEGACY_PATTERNS) {
|
|
935
|
+
if (legacy.re.test(line)) {
|
|
936
|
+
throw new ParseError(legacy.message, i2 + 1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
1515
939
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
940
|
+
const diagnostics = [];
|
|
941
|
+
const meta = {
|
|
942
|
+
width: 1280,
|
|
943
|
+
height: 720,
|
|
944
|
+
fps: 60,
|
|
945
|
+
theme: "midnight",
|
|
946
|
+
direction: "LR"
|
|
947
|
+
};
|
|
948
|
+
const styles = {};
|
|
949
|
+
const nodes = {};
|
|
950
|
+
const edges = [];
|
|
951
|
+
const groups = {};
|
|
952
|
+
const patterns = {};
|
|
953
|
+
const beats = [];
|
|
954
|
+
let title = "";
|
|
955
|
+
let edgeCounter = 0;
|
|
956
|
+
const blocks = readBlocks(lines.map(stripComment));
|
|
957
|
+
let i = 0;
|
|
958
|
+
while (i < blocks.length) {
|
|
959
|
+
const block = blocks[i];
|
|
960
|
+
const line = block.text;
|
|
961
|
+
const lineNo = block.line;
|
|
962
|
+
if (line.startsWith("scene")) {
|
|
963
|
+
const rest = line.slice(5).trim();
|
|
964
|
+
let remainder = rest;
|
|
965
|
+
const str = parseStringToken(rest);
|
|
966
|
+
if (str) {
|
|
967
|
+
title = str.value;
|
|
968
|
+
remainder = str.rest;
|
|
969
|
+
}
|
|
970
|
+
const props = parseProps(remainder);
|
|
971
|
+
for (const [k, v] of Object.entries(props)) {
|
|
972
|
+
if (!SCENE_KEYS.has(k)) {
|
|
973
|
+
diagnostics.push({ severity: "warning", message: `unknown scene property '${k}'`, line: lineNo });
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (k === "width" || k === "height" || k === "fps") meta[k] = Number(v);
|
|
977
|
+
else if (k === "duration") meta.duration = Number(v);
|
|
978
|
+
else if (k === "theme") meta.theme = String(v);
|
|
979
|
+
else if (k === "direction") meta.direction = String(v).toUpperCase();
|
|
980
|
+
}
|
|
981
|
+
i++;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (/^layout\s+(LR|RL|TB|BT)\b/i.test(line)) {
|
|
985
|
+
meta.direction = line.split(/\s+/)[1].toUpperCase();
|
|
986
|
+
i++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (line.startsWith("style ")) {
|
|
990
|
+
const m = line.match(/^style\s+(\w+)\s*=\s*(.+)$/);
|
|
991
|
+
if (!m) throw new ParseError(`expected style name = props`, lineNo);
|
|
992
|
+
styles[m[1]] = { name: m[1], props: parseProps(m[2]), line: lineNo };
|
|
993
|
+
i++;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (line.startsWith("pattern ")) {
|
|
997
|
+
const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*:\s*$/);
|
|
998
|
+
if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
|
|
999
|
+
const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
|
|
1000
|
+
const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
|
|
1001
|
+
const cues = body.map((b) => parseCueLine(b.text, b.line));
|
|
1002
|
+
patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
|
|
1003
|
+
i = nextIdx;
|
|
1004
|
+
continue;
|
|
1521
1005
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1006
|
+
if (line.startsWith("group ")) {
|
|
1007
|
+
const m = line.match(/^group\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*(.+)$/);
|
|
1008
|
+
if (!m) throw new ParseError(`expected group name: A B C`, lineNo);
|
|
1009
|
+
groups[m[1]] = {
|
|
1010
|
+
id: m[1],
|
|
1011
|
+
label: m[2],
|
|
1012
|
+
members: splitTargets(m[3]),
|
|
1013
|
+
props: {},
|
|
1014
|
+
line: lineNo
|
|
1015
|
+
};
|
|
1016
|
+
i++;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (line.startsWith("edge ")) {
|
|
1020
|
+
const m = line.match(/^edge\s+(\w+)\s*:\s*(.+)$/);
|
|
1021
|
+
if (!m) throw new ParseError(`expected edge id: A -> B`, lineNo);
|
|
1022
|
+
const segments = parseFlowChain(m[2].split(/\s+\w+=/)[0], lineNo);
|
|
1023
|
+
for (const seg of segments) {
|
|
1024
|
+
edges.push({
|
|
1025
|
+
id: `edge_${++edgeCounter}`,
|
|
1026
|
+
kind: seg.op,
|
|
1027
|
+
from: seg.from,
|
|
1028
|
+
to: seg.to,
|
|
1029
|
+
label: seg.label,
|
|
1030
|
+
props: parseProps(m[2]),
|
|
1031
|
+
line: lineNo
|
|
1032
|
+
});
|
|
1530
1033
|
}
|
|
1034
|
+
i++;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (line.startsWith("beat ")) {
|
|
1038
|
+
const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
|
|
1039
|
+
if (!m) throw new ParseError(`expected beat name:`, lineNo);
|
|
1040
|
+
const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
|
|
1041
|
+
let cues = body.map((b) => parseCueLine(b.text, b.line));
|
|
1042
|
+
cues = expandPatternCues(cues, patterns, lineNo);
|
|
1043
|
+
beats.push({ name: m[1], label: m[2], cues, line: lineNo });
|
|
1044
|
+
i = nextIdx;
|
|
1045
|
+
continue;
|
|
1531
1046
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1047
|
+
const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
1048
|
+
if (nodeMatch) {
|
|
1049
|
+
const rawKind = nodeMatch[1].toLowerCase();
|
|
1050
|
+
const kind = canonicalNodeKind(rawKind);
|
|
1051
|
+
const id = nodeMatch[2];
|
|
1052
|
+
let remainder = nodeMatch[3].trim();
|
|
1053
|
+
if (!NODE_KINDS.has(kind)) {
|
|
1054
|
+
throw new ParseError(`unknown node kind '${rawKind}'`, lineNo);
|
|
1055
|
+
}
|
|
1056
|
+
let label = humanizeId(id);
|
|
1057
|
+
const str = parseStringToken(remainder);
|
|
1058
|
+
if (str) {
|
|
1059
|
+
label = str.value;
|
|
1060
|
+
remainder = str.rest;
|
|
1537
1061
|
}
|
|
1062
|
+
const styleMatch = remainder.match(/\bstyle=(\w+)/);
|
|
1063
|
+
const props = parseProps(remainder);
|
|
1064
|
+
nodes[id] = {
|
|
1065
|
+
kind,
|
|
1066
|
+
id,
|
|
1067
|
+
label,
|
|
1068
|
+
style: styleMatch?.[1],
|
|
1069
|
+
props,
|
|
1070
|
+
line: lineNo
|
|
1071
|
+
};
|
|
1072
|
+
i++;
|
|
1073
|
+
continue;
|
|
1538
1074
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const absTime = round3(time + sev.offset);
|
|
1542
|
-
const sevMustUnderstand = sev.action.startsWith("!");
|
|
1543
|
-
const sevAction = sevMustUnderstand ? sev.action.slice(1) : sev.action;
|
|
1544
|
-
validateActionForActor(sevAction, actorDef, sevMustUnderstand, lineNum, ast.warnings);
|
|
1545
|
-
const params2 = parseActionParams(sevAction, expandedParams);
|
|
1546
|
-
validateMoveTarget(sevAction, params2, ast.meta, actor, lineNum);
|
|
1547
|
-
recordEventTime(absTime);
|
|
1548
|
-
pushEvent(ast, scope, {
|
|
1549
|
-
time: absTime,
|
|
1550
|
-
actor,
|
|
1551
|
-
action: sevAction,
|
|
1552
|
-
params: params2,
|
|
1553
|
-
line: lineNum,
|
|
1554
|
-
...inChapter ? { chapter: inChapter.name } : {}
|
|
1555
|
-
});
|
|
1075
|
+
if (BEAT_CUE_KEYWORDS.has(line.split(/\s+/)[0].toLowerCase()) || FLOW_OP_RE.test(line)) {
|
|
1076
|
+
throw new ParseError(`top-level cues must be inside a beat block`, lineNo);
|
|
1556
1077
|
}
|
|
1557
|
-
|
|
1078
|
+
throw new ParseError(`unexpected statement`, lineNo);
|
|
1558
1079
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
recordEventTime(time);
|
|
1563
|
-
pushEvent(ast, scope, {
|
|
1564
|
-
time,
|
|
1565
|
-
actor,
|
|
1566
|
-
action,
|
|
1567
|
-
params,
|
|
1568
|
-
line: lineNum,
|
|
1569
|
-
...inChapter ? { chapter: inChapter.name } : {}
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
function pushEvent(ast, scope, ev) {
|
|
1573
|
-
ast.events.push(ev);
|
|
1574
|
-
const dur = typeof ev.params.dur === "number" ? ev.params.dur : 0;
|
|
1575
|
-
const endTime = round3(ev.time + dur);
|
|
1576
|
-
if (endTime > scope.prevEnd) scope.prevEnd = endTime;
|
|
1577
|
-
}
|
|
1578
|
-
function validateActionForActor(action, actorDef, mustUnderstand, lineNum, warnings) {
|
|
1579
|
-
if (isKnownAction(actorDef.type, action)) return;
|
|
1580
|
-
if (isFigureOnlyAction(action)) {
|
|
1581
|
-
if (actorDef.type !== "figure") {
|
|
1582
|
-
throw new ParseError(
|
|
1583
|
-
`action "${action}" is figure-only; actor type is "${actorDef.type}"`,
|
|
1584
|
-
lineNum
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
return;
|
|
1080
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
1081
|
+
if (errors.length) {
|
|
1082
|
+
throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
|
|
1588
1083
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1084
|
+
const ast = {
|
|
1085
|
+
meta: { ...meta, title: title || void 0 },
|
|
1086
|
+
styles,
|
|
1087
|
+
nodes,
|
|
1088
|
+
edges,
|
|
1089
|
+
groups,
|
|
1090
|
+
patterns,
|
|
1091
|
+
beats,
|
|
1092
|
+
diagnostics
|
|
1093
|
+
};
|
|
1094
|
+
if (!opts.parseOnly && Object.keys(nodes).length === 0 && beats.length === 0) {
|
|
1591
1095
|
}
|
|
1592
|
-
|
|
1593
|
-
kind: "unknown-action",
|
|
1594
|
-
message: `unknown action "${action}" on ${actorDef.type} actor \u2014 renderer will no-op`,
|
|
1595
|
-
line: lineNum
|
|
1596
|
-
});
|
|
1096
|
+
return ast;
|
|
1597
1097
|
}
|
|
1598
|
-
function
|
|
1599
|
-
|
|
1600
|
-
let presetLine = null;
|
|
1601
|
-
for (const raw of lines) {
|
|
1602
|
-
const stripped = stripComment(raw).trim();
|
|
1603
|
-
if (!stripped) continue;
|
|
1604
|
-
if (stripped.startsWith("preset ")) {
|
|
1605
|
-
if (presetLine) return null;
|
|
1606
|
-
presetLine = stripped;
|
|
1607
|
-
} else {
|
|
1608
|
-
return null;
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
if (!presetLine) return null;
|
|
1612
|
-
const pm = PRESET_RE.exec(presetLine);
|
|
1613
|
-
if (!pm) return null;
|
|
1614
|
-
const [, name, argsRaw] = pm;
|
|
1615
|
-
const fn = PRESETS[name];
|
|
1616
|
-
if (!fn) return null;
|
|
1617
|
-
const args = argsRaw ? splitByComma(argsRaw).map((a) => {
|
|
1618
|
-
const t = a.trim();
|
|
1619
|
-
const q = parseQuotedLiteral(t);
|
|
1620
|
-
return q ?? t;
|
|
1621
|
-
}) : [];
|
|
1622
|
-
return fn(args);
|
|
1098
|
+
function compile(ast) {
|
|
1099
|
+
return compilePlan(ast, resolveTheme(ast.meta.theme));
|
|
1623
1100
|
}
|
|
1624
|
-
function
|
|
1625
|
-
|
|
1101
|
+
function parseAndCompile(source) {
|
|
1102
|
+
const ast = parse(source);
|
|
1103
|
+
const plan = compile(ast);
|
|
1104
|
+
return { ast, plan };
|
|
1626
1105
|
}
|
|
1627
1106
|
export {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
PRESET_NAMES,
|
|
1107
|
+
BEAT_CUE_KEYWORDS,
|
|
1108
|
+
EDGE_OPERATORS,
|
|
1109
|
+
NODE_ALIASES,
|
|
1110
|
+
NODE_KINDS,
|
|
1633
1111
|
ParseError,
|
|
1112
|
+
SCENE_KEYS,
|
|
1634
1113
|
TECHNICAL_NODE_KINDS,
|
|
1635
1114
|
TECHNICAL_NODE_TYPES,
|
|
1636
|
-
|
|
1115
|
+
THEMES,
|
|
1637
1116
|
VISUAL_PRIMITIVE_TYPES,
|
|
1117
|
+
canonicalNodeKind,
|
|
1118
|
+
compile,
|
|
1119
|
+
compilePlan,
|
|
1120
|
+
humanizeId,
|
|
1121
|
+
nodeRole,
|
|
1638
1122
|
parse,
|
|
1639
|
-
|
|
1123
|
+
parseAndCompile,
|
|
1124
|
+
resolveTheme
|
|
1640
1125
|
};
|