@markdy/core 0.7.28 → 0.8.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.
Files changed (4) hide show
  1. package/README.md +21 -16
  2. package/dist/index.d.ts +190 -188
  3. package/dist/index.js +698 -1213
  4. package/package.json +1 -1
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/parser.ts
646
- var ParseError = class extends Error {
647
- constructor(message, line) {
648
- super(`Line ${line}: ${message}`);
649
- this.line = line;
650
- this.name = "ParseError";
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
- function stripComment(line) {
655
- let depth = 0;
656
- let stringQuote = null;
657
- let lastNonSpace = "";
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 splitByComma(s) {
683
- const parts = [];
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
- function parseValue(s) {
706
- const t = s.trim();
707
- const quoted = parseQuotedLiteral(t);
708
- if (quoted !== null) {
709
- return quoted;
710
- }
711
- if (t.startsWith("(") && t.endsWith(")")) {
712
- const inner = t.slice(1, -1);
713
- return inner.split(",").map((p) => {
714
- const v = p.trim();
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
- function isEscapedChar(s, idx) {
724
- let slashCount = 0;
725
- for (let i = idx - 1; i >= 0 && s[i] === "\\"; i--) {
726
- slashCount++;
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 slashCount % 2 === 1;
404
+ return dedupeEdges(edges);
729
405
  }
730
- function parseQuotedLiteral(token) {
731
- if (token.length < 2) return null;
732
- const quote2 = token[0];
733
- if (quote2 !== '"' && quote2 !== "'" || token[token.length - 1] !== quote2) {
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 (isEscapedChar(token, token.length - 1)) {
737
- return null;
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 unescapeQuotedContent(content, quote2) {
742
- let out = "";
743
- for (let i = 0; i < content.length; i++) {
744
- const ch = content[i];
745
- if (ch !== "\\") {
746
- out += ch;
747
- continue;
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
- var POSITIONAL_KEYS = {
765
- say: ["text"],
766
- throw: ["asset"]
767
- };
768
- function parseActionParams(action, raw) {
769
- const params = {};
770
- const trimmed = raw.trim();
771
- if (!trimmed) return params;
772
- const positionalKeys = POSITIONAL_KEYS[action] ?? [];
773
- let positionalIndex = 0;
774
- for (const token of splitByComma(trimmed)) {
775
- const t = token.trim();
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 params;
445
+ return ranks;
789
446
  }
790
- var MODIFIER_KEYS = /* @__PURE__ */ new Set(["scale", "rotate", "opacity", "size", "z"]);
791
- function parseSpaceModifiers(raw, lineNum, warnings) {
792
- const result = {};
793
- const tokens = raw.trim().split(/\s+/).filter(Boolean);
794
- for (let i = 0; i + 1 < tokens.length; i += 2) {
795
- const key = tokens[i];
796
- const val = Number(tokens[i + 1]);
797
- if (Number.isNaN(val)) continue;
798
- if (MODIFIER_KEYS.has(key)) {
799
- result[key] = val;
800
- } else {
801
- warnings.push({
802
- kind: "unknown-modifier",
803
- message: `unknown modifier "${key}" \u2014 ignored`,
804
- line: lineNum
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 result;
494
+ return nodes;
809
495
  }
810
- function parseWithModifiers(raw, lineNum, warnings) {
811
- const result = {};
812
- for (const token of splitByComma(raw)) {
813
- const t = token.trim();
814
- if (!t) continue;
815
- const eqIdx = t.indexOf("=");
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
- const key = t.slice(0, eqIdx).trim();
825
- const val = Number(t.slice(eqIdx + 1).trim());
826
- if (Number.isNaN(val)) {
827
- warnings.push({
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
- warnings.push({
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 result;
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 parse(source, opts = {}) {
919
- if (!opts._fromPreset) {
920
- const expansion = tryExpandSolePreset(source);
921
- if (expansion) return parse(expansion, { ...opts, _fromPreset: true });
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
- meta: { ...DEFAULTS },
925
- assets: {},
926
- actors: {},
927
- events: [],
928
- defs: {},
929
- seqs: {},
930
- vars: {},
931
- groups: {},
932
- chapters: [],
933
- warnings: [],
934
- imports: []
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 (labelLengthWarningThreshold > 0 && typeof actorInfo.args[0] === "string") {
1166
- const label = actorInfo.args[0];
1167
- if (label.length > labelLengthWarningThreshold) {
1168
- ast.warnings.push({
1169
- kind: "label-overflow",
1170
- message: `actor "${actorInfo.name}" label length ${label.length} exceeds ${labelLengthWarningThreshold}; consider shortening to reduce overlap`,
1171
- line: lineNum
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
- continue;
1176
- }
1177
- if (raw.startsWith("group ")) {
1178
- parseGroupLine(raw, lineNum, ast);
1179
- continue;
1180
- }
1181
- if (raw.startsWith("@")) {
1182
- parseEventLine(raw, lineNum, ast, inChapter, topScope);
1183
- continue;
1184
- }
1185
- throw new ParseError(`Unrecognized statement: ${raw}`, lineNum);
1186
- }
1187
- if (inDef) {
1188
- throw new ParseError(`Unclosed def block "${inDef.name}"`, inDef.startLine);
1189
- }
1190
- if (defNeedsClose) {
1191
- throw new ParseError("Unclosed def block (missing '}')", lines.length);
1192
- }
1193
- if (inSeq) {
1194
- throw new ParseError(`Unclosed seq block "${inSeq.name}"`, inSeq.startLine);
1195
- }
1196
- if (inChapter) {
1197
- throw new ParseError(`Unclosed chapter "${inChapter.name}"`, inChapter.startLine);
1198
- }
1199
- if (ast.meta.duration === void 0) {
1200
- let maxEnd = 0;
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 ast;
593
+ return { cues, beats: beatRanges };
1208
594
  }
1209
- function parseActorCallArgs(argsRaw) {
1210
- if (!argsRaw.trim()) return [];
1211
- return splitByComma(argsRaw).map((a) => {
1212
- const t = a.trim();
1213
- const q = parseQuotedLiteral(t);
1214
- return q ?? t;
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
- function parseActorLine(raw, lineNum, ast) {
1218
- const architectureNode = parseArchitectureNodeLine(raw, lineNum, ast);
1219
- if (architectureNode) return architectureNode;
1220
- const amAnchor = ACTOR_ANCHOR_POS_RE.exec(raw);
1221
- const amNum = amAnchor ? null : ACTOR_NUM_POS_RE.exec(raw);
1222
- if (!amAnchor && !amNum) throw new ParseError(`Invalid actor declaration: ${raw}`, lineNum);
1223
- let name;
1224
- let typeName;
1225
- let argsRaw;
1226
- let x;
1227
- let y;
1228
- let anchor;
1229
- let trailer;
1230
- if (amAnchor) {
1231
- const [, nm, tn, ar, an, tr] = amAnchor;
1232
- name = nm;
1233
- typeName = tn;
1234
- argsRaw = ar;
1235
- anchor = an;
1236
- trailer = tr;
1237
- x = ast.meta.width / 2;
1238
- switch (anchor) {
1239
- case "top":
1240
- y = Math.round(ast.meta.height * CAPTION_TOP_Y_FRAC);
1241
- break;
1242
- case "bottom":
1243
- y = Math.round(ast.meta.height * CAPTION_BOTTOM_Y_FRAC);
1244
- break;
1245
- default:
1246
- y = Math.round(ast.meta.height * CAPTION_CENTER_Y_FRAC);
1247
- break;
1248
- }
1249
- } else {
1250
- const [, nm, tn, ar, xs, ys, tr] = amNum;
1251
- name = nm;
1252
- typeName = tn;
1253
- argsRaw = ar;
1254
- x = Number(xs);
1255
- y = Number(ys);
1256
- trailer = tr;
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
- if (name === "camera") {
1259
- throw new ParseError(
1260
- `"camera" is a reserved actor name; drop this declaration and use camera.pan/zoom/shake directly`,
1261
- lineNum
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
- const rawArgs = parseActorCallArgs(argsRaw);
1265
- let resolvedType;
1266
- let resolvedArgs;
1267
- if (isKnownActorType(typeName)) {
1268
- resolvedType = typeName;
1269
- resolvedArgs = rawArgs;
1270
- } else if (ast.defs[typeName]) {
1271
- const tmpl = ast.defs[typeName];
1272
- const localVars = {};
1273
- for (let pi = 0; pi < tmpl.params.length; pi++) {
1274
- localVars[tmpl.params[pi]] = rawArgs[pi] ?? "";
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
- resolvedType = tmpl.actorType;
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
- if (!anchor && (x < 0 || x > ast.meta.width || y < 0 || y > ast.meta.height)) {
1295
- throw new ParseError(
1296
- `Actor "${name}" position (${x}, ${y}) is outside scene bounds (0\u2013${ast.meta.width}, 0\u2013${ast.meta.height})`,
1297
- lineNum
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
- ast.actors[name] = {
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 isArchitectureNodeLine(raw) {
1311
- return ARCHITECTURE_NODE_RE.test(raw);
745
+ function splitTargets(raw) {
746
+ return raw.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
1312
747
  }
1313
- function parseArchitectureNodeLine(raw, lineNum, ast) {
1314
- const m = ARCHITECTURE_NODE_RE.exec(raw);
1315
- if (!m) return null;
1316
- const [, alias, name, labelRaw, xRaw, yRaw, trailer] = m;
1317
- if (name === "camera") {
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
- const typeName = ARCHITECTURE_ALIAS_TO_TYPE[alias] ?? alias;
1324
- if (!isKnownActorType(typeName)) {
1325
- throw new ParseError(
1326
- `Architecture node "${alias}" requires an actor pack that registers type "${typeName}"`,
1327
- lineNum
1328
- );
1329
- }
1330
- const actorIndex = Object.keys(ast.actors).length;
1331
- const x = xRaw === void 0 ? autoNodeX(actorIndex, ast.meta.width) : Number(xRaw);
1332
- const y = yRaw === void 0 ? autoNodeY(actorIndex, ast.meta.height, ast.meta.width) : Number(yRaw);
1333
- if (x < 0 || x > ast.meta.width || y < 0 || y > ast.meta.height) {
1334
- throw new ParseError(
1335
- `Actor "${name}" position (${x}, ${y}) is outside scene bounds (0\u2013${ast.meta.width}, 0\u2013${ast.meta.height})`,
1336
- lineNum
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
- const quoted = labelRaw ? parseQuotedLiteral(labelRaw.trim()) : null;
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 autoNodeY(index, height, width = DEFAULTS.width) {
1357
- const columns = Math.max(1, Math.floor(width / 220));
1358
- const row = Math.floor(index / columns);
1359
- return Math.min(height - 60, 90 + row * 120);
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
- var GROUP_RE = /^group\s+([A-Za-z_][\w.]*)\s*=\s*(.+)$/;
1362
- function parseGroupLine(raw, lineNum, ast) {
1363
- const m = GROUP_RE.exec(raw);
1364
- if (!m) {
1365
- throw new ParseError(`Invalid group declaration: ${raw}`, lineNum);
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
- const [, name, membersRaw] = m;
1368
- if (ast.actors[name]) {
1369
- throw new ParseError(`Group "${name}" collides with an actor of the same name`, lineNum);
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
- if (ast.groups[name]) {
1372
- throw new ParseError(`Group "${name}" is already defined`, lineNum);
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
- const members = splitByComma(membersRaw).map((part) => part.trim()).filter((part) => part.length > 0);
1375
- if (members.length === 0) {
1376
- throw new ParseError(`Group "${name}" has no members`, lineNum);
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
- for (const member of members) {
1379
- if (!ast.actors[member]) {
1380
- throw new ParseError(`Unknown actor "${member}" in group "${name}"`, lineNum);
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
- const seen = /* @__PURE__ */ new Set();
1384
- for (const member of members) {
1385
- if (seen.has(member)) {
1386
- throw new ParseError(`Actor "${member}" listed twice in group "${name}"`, lineNum);
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
- seen.add(member);
853
+ return { kind: "use", pattern: m[1], args, line: lineNo };
1389
854
  }
1390
- ast.groups[name] = members;
855
+ throw new ParseError(`unknown cue '${head}'`, lineNo);
1391
856
  }
1392
- function takeStagger(paramsRaw, lineNum) {
1393
- if (!paramsRaw.includes("stagger")) return { step: 0, rest: paramsRaw };
1394
- const kept = [];
1395
- let step = 0;
1396
- for (const part of splitByComma(paramsRaw)) {
1397
- const eq = part.indexOf("=");
1398
- if (eq !== -1 && part.slice(0, eq).trim() === "stagger") {
1399
- const rawValue = part.slice(eq + 1).trim();
1400
- const value = Number(rawValue);
1401
- if (Number.isNaN(value)) {
1402
- throw new ParseError(`Invalid stagger value: ${rawValue}`, lineNum);
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
- kept.push(part);
871
+ out.push(cue);
1411
872
  }
1412
- return { step, rest: kept.join(",") };
873
+ return out;
1413
874
  }
1414
- function parseEventLine(raw, lineNum, ast, inChapter, topScope) {
1415
- const scope = inChapter?.scope ?? topScope;
1416
- const recordEventTime = (t) => {
1417
- if (inChapter && t < inChapter.earliestEventTime) {
1418
- inChapter.earliestEventTime = t;
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
- const rel = REL_EVENT_RE.exec(raw);
1422
- const abs = rel ? null : EVENT_RE.exec(raw);
1423
- if (!rel && !abs) {
1424
- throw new ParseError(`Invalid event: ${raw}`, lineNum);
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
- let time;
1427
- let actor;
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
- const mustUnderstand = actionToken.startsWith("!");
1452
- const action = mustUnderstand ? actionToken.slice(1) : actionToken;
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
- const groupMembers = ast.groups[actor];
1477
- if (!groupMembers && !ast.actors[actor]) {
1478
- throw new ParseError(`Unknown actor: "${actor}"`, lineNum);
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
- if (groupMembers) {
1481
- const { step, rest } = takeStagger(paramsRaw, lineNum);
1482
- groupMembers.forEach((member, index) => {
1483
- emitActorEvent(
1484
- member,
1485
- round3(time + index * step),
1486
- action,
1487
- rest,
1488
- mustUnderstand,
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
- emitActorEvent(
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 emitActorEvent(actor, time, action, paramsRaw, mustUnderstand, lineNum, ast, scope, inChapter, recordEventTime) {
1512
- const actorDef = ast.actors[actor];
1513
- if (!actorDef) {
1514
- throw new ParseError(`Unknown actor: "${actor}"`, lineNum);
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
- if (action === "play") {
1517
- const playParts = splitByComma(paramsRaw);
1518
- const seqName = playParts[0]?.trim();
1519
- if (!seqName || !ast.seqs[seqName]) {
1520
- throw new ParseError(`Unknown sequence: "${seqName}"`, lineNum);
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
- const seq = ast.seqs[seqName];
1523
- const playVars = {};
1524
- for (let pi = 1; pi < playParts.length; pi++) {
1525
- const eqIdx = playParts[pi].indexOf("=");
1526
- if (eqIdx !== -1) {
1527
- const k = playParts[pi].slice(0, eqIdx).trim();
1528
- const v = playParts[pi].slice(eqIdx + 1).trim();
1529
- playVars[k] = v;
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
- let posIdx = 0;
1533
- for (let pi = 1; pi < playParts.length; pi++) {
1534
- if (!playParts[pi].includes("=") && posIdx < seq.params.length) {
1535
- playVars[seq.params[posIdx]] = playParts[pi].trim();
1536
- posIdx++;
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
- for (const sev of seq.events) {
1540
- const expandedParams = interpolate(sev.paramsRaw, playVars);
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
- return;
1078
+ throw new ParseError(`unexpected statement`, lineNo);
1558
1079
  }
1559
- validateActionForActor(action, actorDef, mustUnderstand, lineNum, ast.warnings);
1560
- const params = parseActionParams(action, paramsRaw);
1561
- validateMoveTarget(action, params, ast.meta, actor, lineNum);
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
- if (mustUnderstand) {
1590
- throw new ParseError(`Unknown action "${action}" (must-understand form)`, lineNum);
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
- warnings.push({
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 tryExpandSolePreset(source) {
1599
- const lines = source.split(/\r?\n/);
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 round3(n) {
1625
- return Math.round(n * 1e3) / 1e3;
1101
+ function parseAndCompile(source) {
1102
+ const ast = parse(source);
1103
+ const plan = compile(ast);
1104
+ return { ast, plan };
1626
1105
  }
1627
1106
  export {
1628
- BUILTIN_ACTOR_TYPES,
1629
- CAMERA_ACTION_NAMES,
1630
- FIGURE_ONLY_ACTION_NAMES,
1631
- PRESETS,
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
- UNIVERSAL_ACTION_NAMES,
1115
+ THEMES,
1637
1116
  VISUAL_PRIMITIVE_TYPES,
1117
+ canonicalNodeKind,
1118
+ compile,
1119
+ compilePlan,
1120
+ humanizeId,
1121
+ nodeRole,
1638
1122
  parse,
1639
- registerActorPack
1123
+ parseAndCompile,
1124
+ resolveTheme
1640
1125
  };