@markdy/core 0.8.11 → 0.8.13

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 CHANGED
@@ -6,7 +6,8 @@ The parser and AST types for [MarkdyScript](../../docs/SYNTAX.md) — a diagram-
6
6
 
7
7
  - **Zero runtime dependencies** — pure TypeScript, no DOM or platform APIs
8
8
  - **Single-pass parser** — line-by-line state machine with strict `ParseError` diagnostics
9
- - **Diagram-native grammar** — scene metadata, architecture nodes, groups, styles, beats, flow chains, and reusable patterns
9
+ - **Diagram-native grammar** — scene metadata, architecture nodes, groups, styles, beats, flow chains, annotations, visual primitives, and reusable patterns
10
+ - **Focused modes** — `architecture`, `flowchart`, `tree`, `state`, `sequence`, and `constellation` compile into deterministic render-plan metadata
10
11
  - **Isomorphic** — runs in Node.js, Deno, Bun, edge runtimes, and the browser
11
12
 
12
13
  ## Installation
@@ -66,12 +67,12 @@ try {
66
67
  | `parseAndCompile` | `(source) => { ast, plan }` | Parse and compile in one call |
67
68
  | `ParseError` | class | Error with `.line` number for diagnostics |
68
69
  | `DiagramAST` | type | Parsed scene: meta, nodes, edges, groups, patterns, beats |
69
- | `RenderPlan` | type | Positioned nodes, routed edges, timed cues, beat ranges |
70
+ | `RenderPlan` | type | Positioned nodes, routed edges, group zones, sequence messages, timed cues, beat ranges |
70
71
  | `SceneMeta` | type | Scene configuration (width, height, fps, theme, direction) |
71
72
  | `NodeDecl` | type | Node declaration (kind, id, label, style) |
72
73
  | `EdgeDecl` | type | Edge declaration (kind, from, to, label) |
73
74
  | `BeatDecl` | type | Named beat with cues |
74
- | `THEMES` / `resolveTheme` | tokens | Semantic theme palettes (`paper`, `midnight`, `blueprint`, `graphite`) |
75
+ | `THEMES` / `resolveTheme` | tokens | Semantic theme palettes (`paper`, `editorial`, `nebula`, `midnight`, `blueprint`, `graphite`) |
75
76
 
76
77
  ## Documentation
77
78
 
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  */
5
5
  type LayoutDirection = "LR" | "RL" | "TB" | "BT";
6
6
  type EdgeKind = "request" | "response" | "event" | "dependency";
7
+ type DiagramType = "architecture" | "flowchart" | "tree" | "state" | "sequence" | "constellation";
8
+ type NodeShape = "card" | "rounded" | "diamond" | "circle" | "pill" | "terminal";
7
9
  type SceneMeta = {
8
10
  title?: string;
9
11
  width: number;
@@ -12,6 +14,8 @@ type SceneMeta = {
12
14
  theme: string;
13
15
  direction: LayoutDirection;
14
16
  duration?: number;
17
+ /** Opt-in diagram mode; defaults to architecture. */
18
+ type?: DiagramType;
15
19
  };
16
20
  type NodeDecl = {
17
21
  kind: string;
@@ -37,6 +41,14 @@ type GroupDecl = {
37
41
  props: Record<string, unknown>;
38
42
  line: number;
39
43
  };
44
+ type AnnotationDecl = {
45
+ id: string;
46
+ text: string;
47
+ target?: string;
48
+ position?: string;
49
+ props: Record<string, unknown>;
50
+ line: number;
51
+ };
40
52
  type StyleDecl = {
41
53
  name: string;
42
54
  props: Record<string, unknown>;
@@ -118,6 +130,7 @@ type DiagramAST = {
118
130
  nodes: Record<string, NodeDecl>;
119
131
  edges: EdgeDecl[];
120
132
  groups: Record<string, GroupDecl>;
133
+ annotations: AnnotationDecl[];
121
134
  patterns: Record<string, PatternDecl>;
122
135
  beats: BeatDecl[];
123
136
  diagnostics: Diagnostic[];
@@ -134,6 +147,17 @@ type ThemeTokens = {
134
147
  gridMajor: string;
135
148
  vignette: string;
136
149
  accent: string;
150
+ /** HTTP / external link accent (editorial skin). */
151
+ link?: string;
152
+ /** Semantic editorial aliases for canvas/text/muted/border roles. */
153
+ paper?: string;
154
+ ink?: string;
155
+ muted?: string;
156
+ rule?: string;
157
+ /** Tertiary caption color. */
158
+ soft?: string;
159
+ /** Focal node fill tint. */
160
+ accentTint?: string;
137
161
  /** Node card fill (falls back to surface derivations when omitted). */
138
162
  nodeSurface?: string;
139
163
  nodeSurfaceRaised?: string;
@@ -143,8 +167,23 @@ type ThemeTokens = {
143
167
  shadow?: string;
144
168
  /** Edge label pill fill. */
145
169
  labelPlate?: string;
170
+ /** Editorial: flat cards without drop shadows. */
171
+ flatCards?: boolean;
146
172
  roles: Record<string, string>;
147
173
  edges: Record<EdgeKind, string>;
174
+ fonts?: {
175
+ title?: string;
176
+ nodeName?: string;
177
+ mono?: string;
178
+ };
179
+ radiusMd?: number;
180
+ spacing?: {
181
+ xs: number;
182
+ sm: number;
183
+ md: number;
184
+ lg: number;
185
+ xl: number;
186
+ };
148
187
  };
149
188
  type PositionedNode = {
150
189
  id: string;
@@ -158,6 +197,10 @@ type PositionedNode = {
158
197
  style?: Record<string, unknown>;
159
198
  props?: Record<string, unknown>;
160
199
  opacity: number;
200
+ shape?: NodeShape;
201
+ focal?: boolean;
202
+ /** Sequence mode column index. */
203
+ column?: number;
161
204
  };
162
205
  type RoutedEdge = {
163
206
  id: string;
@@ -165,6 +208,48 @@ type RoutedEdge = {
165
208
  from: string;
166
209
  to: string;
167
210
  label?: string;
211
+ /** Declared via top-level `edge` (not only flow cues). */
212
+ structural?: boolean;
213
+ selfLoop?: boolean;
214
+ };
215
+ type GroupBoundary = {
216
+ id: string;
217
+ label?: string;
218
+ x: number;
219
+ y: number;
220
+ width: number;
221
+ height: number;
222
+ memberIds: string[];
223
+ props?: Record<string, unknown>;
224
+ };
225
+ type SequenceMessage = {
226
+ id: string;
227
+ from: string;
228
+ to: string;
229
+ kind: EdgeKind;
230
+ label?: string;
231
+ y: number;
232
+ start: number;
233
+ duration: number;
234
+ beat: string;
235
+ };
236
+ type SequenceActivation = {
237
+ id: string;
238
+ participant: string;
239
+ y: number;
240
+ height: number;
241
+ start: number;
242
+ duration: number;
243
+ };
244
+ type TreeBus = {
245
+ id: string;
246
+ parentId: string;
247
+ childIds: string[];
248
+ parentX: number;
249
+ parentY: number;
250
+ branchY: number;
251
+ childXs: number[];
252
+ childY: number;
168
253
  };
169
254
  type TimedCue = {
170
255
  start: number;
@@ -186,11 +271,17 @@ type RenderPlan = {
186
271
  meta: SceneMeta;
187
272
  theme: ThemeTokens;
188
273
  title: string;
274
+ diagramType: DiagramType;
189
275
  nodes: PositionedNode[];
190
276
  edges: RoutedEdge[];
277
+ groupBoundaries: GroupBoundary[];
278
+ annotations: AnnotationDecl[];
191
279
  cues: TimedCue[];
192
280
  beats: BeatRange[];
193
281
  groups: Record<string, string[]>;
282
+ treeBuses: TreeBus[];
283
+ sequenceMessages: SequenceMessage[];
284
+ sequenceActivations: SequenceActivation[];
194
285
  duration: number;
195
286
  };
196
287
 
@@ -221,6 +312,7 @@ declare const THEMES: Record<string, ThemeTokens>;
221
312
  declare function resolveTheme(name: string): ThemeTokens;
222
313
 
223
314
  declare const NODE_KINDS: Set<string>;
315
+ declare const DIAGRAM_TYPES: Set<string>;
224
316
  declare const EDGE_OPERATORS: Record<string, "request" | "response" | "event" | "dependency">;
225
317
  /** Natural-language cue synonyms that AIs reach for, mapped to real cues. */
226
318
  declare const CUE_ALIASES: Record<string, string>;
@@ -236,4 +328,4 @@ declare const TECHNICAL_NODE_TYPES: readonly ["service", "api", "microservice",
236
328
  declare const VISUAL_PRIMITIVE_TYPES: readonly ["panel", "surface", "terminal", "metric", "stat", "grid", "matrix", "lane", "track", "marker", "dot", "token_strip", "chips", "glyph_card", "glyph"];
237
329
  declare const TECHNICAL_NODE_KINDS: Record<(typeof TECHNICAL_NODE_TYPES)[number], string>;
238
330
 
239
- export { BEAT_CUE_KEYWORDS, type BeatDecl, type BeatRange, CUE_ALIASES, type Cue, type Diagnostic, type DiagramAST, EDGE_OPERATORS, type EdgeDecl, type EdgeKind, type FlowSegment, type GroupDecl, type LayoutDirection, NODE_ALIASES, NODE_KINDS, type NodeDecl, ParseError, type ParseOptions, type ParseResult, type PatternDecl, type PositionedNode, type RenderPlan, type RoutedEdge, SCENE_KEYS, type SceneMeta, type StyleDecl, TECHNICAL_NODE_KINDS, TECHNICAL_NODE_TYPES, THEMES, type ThemeTokens, type TimedCue, VISUAL_PRIMITIVE_TYPES, canonicalNodeKind, compile, compilePlan, humanizeId, nodeRole, parse, parseAndCompile, resolveTheme };
331
+ export { type AnnotationDecl, BEAT_CUE_KEYWORDS, type BeatDecl, type BeatRange, CUE_ALIASES, type Cue, DIAGRAM_TYPES, type Diagnostic, type DiagramAST, type DiagramType, EDGE_OPERATORS, type EdgeDecl, type EdgeKind, type FlowSegment, type GroupBoundary, type GroupDecl, type LayoutDirection, NODE_ALIASES, NODE_KINDS, type NodeDecl, type NodeShape, ParseError, type ParseOptions, type ParseResult, type PatternDecl, type PositionedNode, type RenderPlan, type RoutedEdge, SCENE_KEYS, type SceneMeta, type SequenceActivation, type SequenceMessage, type StyleDecl, TECHNICAL_NODE_KINDS, TECHNICAL_NODE_TYPES, THEMES, type ThemeTokens, type TimedCue, type TreeBus, VISUAL_PRIMITIVE_TYPES, canonicalNodeKind, compile, compilePlan, humanizeId, nodeRole, parse, parseAndCompile, resolveTheme };
package/dist/index.js CHANGED
@@ -178,6 +178,23 @@ var VISUAL_PRIMITIVE_TYPES = [
178
178
  "glyph_card",
179
179
  "glyph"
180
180
  ];
181
+ var VISUAL_PRIMITIVE_KINDS = {
182
+ panel: "flow",
183
+ surface: "flow",
184
+ terminal: "flow",
185
+ metric: "observability",
186
+ stat: "observability",
187
+ grid: "flow",
188
+ matrix: "flow",
189
+ lane: "flow",
190
+ track: "flow",
191
+ marker: "flow",
192
+ dot: "flow",
193
+ token_strip: "flow",
194
+ chips: "flow",
195
+ glyph_card: "flow",
196
+ glyph: "flow"
197
+ };
181
198
  var TECHNICAL_NODE_KINDS = {
182
199
  service: "compute",
183
200
  api: "compute",
@@ -342,7 +359,18 @@ var TECHNICAL_NODE_KINDS = {
342
359
  };
343
360
 
344
361
  // src/registry.ts
345
- var NODE_KINDS = new Set(TECHNICAL_NODE_TYPES);
362
+ var NODE_KINDS = /* @__PURE__ */ new Set([
363
+ ...TECHNICAL_NODE_TYPES,
364
+ ...VISUAL_PRIMITIVE_TYPES
365
+ ]);
366
+ var DIAGRAM_TYPES = /* @__PURE__ */ new Set([
367
+ "architecture",
368
+ "flowchart",
369
+ "tree",
370
+ "state",
371
+ "sequence",
372
+ "constellation"
373
+ ]);
346
374
  var EDGE_OPERATORS = {
347
375
  "->": "request",
348
376
  "<-": "response",
@@ -364,9 +392,19 @@ var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set([
364
392
  "use",
365
393
  ...Object.keys(CUE_ALIASES)
366
394
  ]);
367
- var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction", "layout"]);
395
+ var SCENE_KEYS = /* @__PURE__ */ new Set([
396
+ "width",
397
+ "height",
398
+ "fps",
399
+ "theme",
400
+ "duration",
401
+ "direction",
402
+ "layout",
403
+ "type"
404
+ ]);
368
405
  function nodeRole(kind) {
369
- return TECHNICAL_NODE_KINDS[kind] ?? "compute";
406
+ const canonical = canonicalNodeKind(kind);
407
+ return TECHNICAL_NODE_KINDS[canonical] ?? VISUAL_PRIMITIVE_KINDS[canonical] ?? "compute";
370
408
  }
371
409
  function humanizeId(id) {
372
410
  const acronyms = /* @__PURE__ */ new Set(["api", "cdn", "db", "dns", "http", "https", "id", "jwt", "oidc", "sdk", "tls", "ui", "url"]);
@@ -388,7 +426,14 @@ var NODE_ALIASES = {
388
426
  gateway: "api_gateway",
389
427
  mq: "queue",
390
428
  k8s: "cluster",
391
- lb: "load_balancer"
429
+ lb: "load_balancer",
430
+ panel: "surface",
431
+ metric: "stat",
432
+ grid: "matrix",
433
+ lane: "track",
434
+ marker: "dot",
435
+ chips: "token_strip",
436
+ glyph: "glyph_card"
392
437
  };
393
438
  function canonicalNodeKind(kind) {
394
439
  return NODE_ALIASES[kind] ?? kind;
@@ -399,6 +444,7 @@ var SAFE = 44;
399
444
  var TITLE_BAND = 76;
400
445
  var NODE_W = 168;
401
446
  var NODE_H = 72;
447
+ var GROUP_PAD = 24;
402
448
  var DEFAULTS = {
403
449
  show: 0.35,
404
450
  hide: 0.35,
@@ -410,20 +456,47 @@ var DEFAULTS = {
410
456
  cueGap: 0.08,
411
457
  stagger: 0.06
412
458
  };
459
+ function diagramType(ast) {
460
+ return ast.meta.type ?? "architecture";
461
+ }
462
+ function nodeShape(kind, dtype) {
463
+ if (kind === "terminal") return "terminal";
464
+ if (kind === "dot" || kind === "marker") return "circle";
465
+ if (kind === "token_strip" || kind === "chips") return "pill";
466
+ if (kind === "surface" || kind === "stat" || kind === "matrix" || kind === "track" || kind === "glyph_card") return "rounded";
467
+ if (dtype === "constellation") return "rounded";
468
+ if (dtype === "flowchart") {
469
+ if (kind === "start" || kind === "end") return "pill";
470
+ if (kind === "decision" || kind === "condition") return "diamond";
471
+ }
472
+ if (dtype === "state" && kind === "state") return "rounded";
473
+ if (kind === "user" || kind === "client") return "rounded";
474
+ return "card";
475
+ }
413
476
  function collectStructuralEdges(ast) {
414
- const edges = [...ast.edges.map((e) => ({
477
+ const edges = ast.edges.map((e) => ({
415
478
  id: e.id,
416
479
  kind: e.kind,
417
480
  from: e.from,
418
481
  to: e.to,
419
- label: e.label
420
- }))];
482
+ label: e.label,
483
+ structural: true,
484
+ selfLoop: e.from === e.to
485
+ }));
421
486
  let counter = edges.length;
422
487
  for (const beat of ast.beats) {
423
488
  for (const cue of beat.cues) {
424
489
  collectFlowSegments(cue, (seg) => {
425
490
  const id = `flow_${++counter}`;
426
- edges.push({ id, kind: seg.op, from: seg.from, to: seg.to, label: seg.label });
491
+ edges.push({
492
+ id,
493
+ kind: seg.op,
494
+ from: seg.from,
495
+ to: seg.to,
496
+ label: seg.label,
497
+ structural: false,
498
+ selfLoop: seg.from === seg.to
499
+ });
427
500
  });
428
501
  }
429
502
  }
@@ -442,7 +515,7 @@ function dedupeEdges(edges) {
442
515
  const seen = /* @__PURE__ */ new Set();
443
516
  const out = [];
444
517
  for (const e of edges) {
445
- const k = `${e.from}|${e.kind}|${e.to}|${e.label ?? ""}`;
518
+ const k = `${e.from}|${e.kind}|${e.to}|${e.label ?? ""}|${e.structural ? "s" : "f"}`;
446
519
  if (seen.has(k)) continue;
447
520
  seen.add(k);
448
521
  out.push(e);
@@ -452,7 +525,7 @@ function dedupeEdges(edges) {
452
525
  function assignRanks(nodeIds, edges, direction) {
453
526
  const ranks = /* @__PURE__ */ new Map();
454
527
  for (const id of nodeIds) ranks.set(id, 0);
455
- const forward = edges.filter((e) => e.kind !== "response");
528
+ const forward = edges.filter((e) => e.kind !== "response" && !e.selfLoop);
456
529
  for (let sweep = 0; sweep < nodeIds.length; sweep++) {
457
530
  let changed = false;
458
531
  for (const e of forward) {
@@ -470,9 +543,14 @@ function assignRanks(nodeIds, edges, direction) {
470
543
  }
471
544
  return ranks;
472
545
  }
473
- function layoutNodes(ast, edges) {
546
+ function snapGrid(n) {
547
+ return Math.round(n / 8) * 8;
548
+ }
549
+ function layoutRanked(ast, edges, opts) {
474
550
  const nodeIds = Object.keys(ast.nodes);
475
- const ranks = assignRanks(nodeIds, edges, ast.meta.direction);
551
+ const dtype = diagramType(ast);
552
+ const direction = opts.forceVertical ? "TB" : ast.meta.direction;
553
+ const ranks = assignRanks(nodeIds, edges, direction);
476
554
  const byRank = /* @__PURE__ */ new Map();
477
555
  for (const id of nodeIds) {
478
556
  const r = ranks.get(id) ?? 0;
@@ -480,7 +558,7 @@ function layoutNodes(ast, edges) {
480
558
  byRank.get(r).push(id);
481
559
  }
482
560
  for (const ids of byRank.values()) ids.sort();
483
- const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
561
+ const isVertical = direction === "TB" || direction === "BT";
484
562
  const contentW = ast.meta.width - SAFE * 2;
485
563
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
486
564
  const maxRank = Math.max(...byRank.keys(), 0);
@@ -488,45 +566,251 @@ function layoutNodes(ast, edges) {
488
566
  const nodes = [];
489
567
  for (const [rank, ids] of [...byRank.entries()].sort((a, b) => a[0] - b[0])) {
490
568
  const rowCount = ids.length;
491
- ids.forEach((id, idx) => {
569
+ const orderedIds = opts.columnLayout ? nodeIds.filter((id) => ids.includes(id)) : [...ids].sort();
570
+ orderedIds.forEach((id, idx) => {
492
571
  const decl = ast.nodes[id];
493
572
  const role = nodeRole(decl.kind);
494
573
  let x;
495
574
  let y;
496
- if (isVertical) {
575
+ if (opts.columnLayout) {
576
+ x = SAFE + contentW / (rowCount + 1) * (idx + 1) - NODE_W / 2;
577
+ y = TITLE_BAND + 48;
578
+ } else if (isVertical) {
497
579
  x = SAFE + contentW / (rowCount + 1) * (idx + 1) - NODE_W / 2;
498
580
  y = TITLE_BAND + contentH / Math.max(rankCount, 1) * rank + (contentH / Math.max(rankCount, 1) - NODE_H) / 2;
499
581
  } else {
500
582
  x = SAFE + contentW / Math.max(rankCount, 1) * rank + (contentW / Math.max(rankCount, 1) - NODE_W) / 2;
501
583
  y = TITLE_BAND + contentH / (rowCount + 1) * (idx + 1) - NODE_H / 2;
502
584
  }
503
- x = Math.round(x / 8) * 8;
504
- y = Math.round(y / 8) * 8;
505
- const style = decl.style ? ast.styles[decl.style]?.props : void 0;
585
+ const focal = decl.props.focal === true || decl.props.accent === true;
506
586
  nodes.push({
507
587
  id,
508
588
  kind: decl.kind,
509
589
  role,
510
590
  label: decl.label,
511
- x,
512
- y,
591
+ x: snapGrid(x),
592
+ y: snapGrid(y),
593
+ width: NODE_W,
594
+ height: NODE_H,
595
+ style: decl.style ? ast.styles[decl.style]?.props : void 0,
596
+ props: decl.props,
597
+ opacity: 0,
598
+ shape: nodeShape(decl.kind, dtype),
599
+ focal,
600
+ column: opts.columnLayout ? idx : void 0
601
+ });
602
+ });
603
+ }
604
+ return nodes;
605
+ }
606
+ function layoutTree(ast, edges) {
607
+ const nodeIds = Object.keys(ast.nodes);
608
+ const children = /* @__PURE__ */ new Map();
609
+ const parent = /* @__PURE__ */ new Map();
610
+ for (const id of nodeIds) children.set(id, []);
611
+ for (const e of edges) {
612
+ if (e.kind === "response" || e.selfLoop) continue;
613
+ if (!ast.nodes[e.from] || !ast.nodes[e.to]) continue;
614
+ if (parent.has(e.to)) continue;
615
+ parent.set(e.to, e.from);
616
+ children.get(e.from).push(e.to);
617
+ }
618
+ const root = nodeIds.find((id) => !parent.has(id)) ?? nodeIds[0];
619
+ const depth = /* @__PURE__ */ new Map();
620
+ const queue = [root];
621
+ depth.set(root, 0);
622
+ while (queue.length) {
623
+ const id = queue.shift();
624
+ for (const child of children.get(id) ?? []) {
625
+ if (depth.has(child)) continue;
626
+ depth.set(child, (depth.get(id) ?? 0) + 1);
627
+ queue.push(child);
628
+ }
629
+ }
630
+ for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
631
+ const byDepth = /* @__PURE__ */ new Map();
632
+ for (const id of nodeIds) {
633
+ const d = depth.get(id) ?? 0;
634
+ if (!byDepth.has(d)) byDepth.set(d, []);
635
+ byDepth.get(d).push(id);
636
+ }
637
+ for (const ids of byDepth.values()) ids.sort();
638
+ const contentW = ast.meta.width - SAFE * 2;
639
+ const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
640
+ const maxDepth = Math.max(...byDepth.keys(), 0);
641
+ const nodes = [];
642
+ for (const [d, ids] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
643
+ ids.forEach((id, idx) => {
644
+ const decl = ast.nodes[id];
645
+ const x = SAFE + contentW / (ids.length + 1) * (idx + 1) - NODE_W / 2;
646
+ const y = TITLE_BAND + contentH / Math.max(maxDepth + 1, 1) * d + 24;
647
+ nodes.push({
648
+ id,
649
+ kind: decl.kind,
650
+ role: nodeRole(decl.kind),
651
+ label: decl.label,
652
+ x: snapGrid(x),
653
+ y: snapGrid(y),
513
654
  width: NODE_W,
514
655
  height: NODE_H,
515
- style,
656
+ style: decl.style ? ast.styles[decl.style]?.props : void 0,
516
657
  props: decl.props,
517
- opacity: 0
658
+ opacity: 0,
659
+ shape: "card"
518
660
  });
519
661
  });
520
662
  }
521
663
  return nodes;
522
664
  }
523
- function resolveTargets(targets, ast, groups) {
665
+ function cycleSafeEdges(nodeIds, edges) {
666
+ const outgoing = /* @__PURE__ */ new Map();
667
+ const incoming = /* @__PURE__ */ new Set();
668
+ for (const id of nodeIds) outgoing.set(id, []);
669
+ for (const edge of edges) {
670
+ if (edge.kind === "response" || edge.selfLoop) continue;
671
+ if (!outgoing.has(edge.from) || !outgoing.has(edge.to)) continue;
672
+ outgoing.get(edge.from).push(edge);
673
+ incoming.add(edge.to);
674
+ }
675
+ const visited = /* @__PURE__ */ new Set();
676
+ const safe = [];
677
+ const visit = (id) => {
678
+ if (visited.has(id)) return;
679
+ visited.add(id);
680
+ for (const edge of outgoing.get(id) ?? []) {
681
+ if (visited.has(edge.to)) continue;
682
+ safe.push(edge);
683
+ visit(edge.to);
684
+ }
685
+ };
686
+ for (const id of nodeIds) {
687
+ if (!incoming.has(id)) visit(id);
688
+ }
689
+ for (const id of nodeIds) visit(id);
690
+ return safe;
691
+ }
692
+ function layoutConstellation(ast) {
693
+ const nodeIds = Object.keys(ast.nodes);
694
+ if (nodeIds.length === 0) return [];
695
+ const incoming = /* @__PURE__ */ new Set();
696
+ for (const edge of ast.edges) {
697
+ if (edge.kind !== "response" && edge.from !== edge.to && ast.nodes[edge.to]) incoming.add(edge.to);
698
+ }
699
+ const focalId = nodeIds.find((id) => ast.nodes[id].props.focal === true || ast.nodes[id].props.accent === true) ?? nodeIds.find((id) => !incoming.has(id)) ?? nodeIds[0];
700
+ const contentW = ast.meta.width - SAFE * 2;
701
+ const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
702
+ const centerX = SAFE + contentW / 2 - NODE_W / 2;
703
+ const centerY = TITLE_BAND + contentH / 2 - NODE_H / 2;
704
+ const orbitIds = nodeIds.filter((id) => id !== focalId);
705
+ const radiusX = Math.max(140, contentW / 2 - NODE_W / 2 - SAFE);
706
+ const radiusY = Math.max(120, contentH / 2 - NODE_H / 2 - SAFE);
707
+ const nodes = [];
708
+ for (const id of nodeIds) {
709
+ const decl = ast.nodes[id];
710
+ const isFocal = id === focalId;
711
+ const index = orbitIds.indexOf(id);
712
+ const angle = orbitIds.length > 0 ? -Math.PI / 2 + index * Math.PI * 2 / orbitIds.length : 0;
713
+ const x = isFocal ? centerX : SAFE + contentW / 2 + Math.cos(angle) * radiusX - NODE_W / 2;
714
+ const y = isFocal ? centerY : TITLE_BAND + contentH / 2 + Math.sin(angle) * radiusY - NODE_H / 2;
715
+ nodes.push({
716
+ id,
717
+ kind: decl.kind,
718
+ role: nodeRole(decl.kind),
719
+ label: decl.label,
720
+ x: snapGrid(x),
721
+ y: snapGrid(y),
722
+ width: NODE_W,
723
+ height: NODE_H,
724
+ style: decl.style ? ast.styles[decl.style]?.props : void 0,
725
+ props: decl.props,
726
+ opacity: 0,
727
+ shape: nodeShape(decl.kind, "constellation"),
728
+ focal: isFocal || decl.props.focal === true || decl.props.accent === true
729
+ });
730
+ }
731
+ return nodes;
732
+ }
733
+ function layoutNodes(ast, edges) {
734
+ const dtype = diagramType(ast);
735
+ switch (dtype) {
736
+ case "flowchart":
737
+ return layoutRanked(ast, edges, { forceVertical: true });
738
+ case "tree":
739
+ return layoutTree(ast, edges.some((edge) => edge.structural) ? edges.filter((edge) => edge.structural) : edges);
740
+ case "sequence":
741
+ return layoutRanked(ast, [], { columnLayout: true });
742
+ case "constellation":
743
+ return layoutConstellation(ast);
744
+ case "state":
745
+ return layoutRanked(ast, cycleSafeEdges(Object.keys(ast.nodes), edges), { forceVertical: false });
746
+ default:
747
+ return layoutRanked(ast, edges, { forceVertical: false });
748
+ }
749
+ }
750
+ function computeGroupBoundaries(ast, nodes) {
751
+ const byId = new Map(nodes.map((n) => [n.id, n]));
752
+ const boundaries = [];
753
+ for (const group of Object.values(ast.groups)) {
754
+ const members = group.members.filter((id) => byId.has(id));
755
+ if (members.length === 0) continue;
756
+ const rects = members.map((id) => byId.get(id));
757
+ const minX = Math.min(...rects.map((n) => n.x)) - GROUP_PAD;
758
+ const minY = Math.min(...rects.map((n) => n.y)) - GROUP_PAD - 12;
759
+ const maxX = Math.max(...rects.map((n) => n.x + n.width)) + GROUP_PAD;
760
+ const maxY = Math.max(...rects.map((n) => n.y + n.height)) + GROUP_PAD;
761
+ boundaries.push({
762
+ id: group.id,
763
+ label: group.label,
764
+ x: snapGrid(minX),
765
+ y: snapGrid(minY),
766
+ width: snapGrid(maxX - minX),
767
+ height: snapGrid(maxY - minY),
768
+ memberIds: members,
769
+ props: group.props
770
+ });
771
+ }
772
+ return boundaries;
773
+ }
774
+ function computeTreeBuses(ast, nodes, edges) {
775
+ if (diagramType(ast) !== "tree") return [];
776
+ const byId = new Map(nodes.map((node) => [node.id, node]));
777
+ const children = /* @__PURE__ */ new Map();
778
+ const parent = /* @__PURE__ */ new Set();
779
+ for (const edge of edges) {
780
+ if (edge.kind === "response" || edge.selfLoop) continue;
781
+ if (!byId.has(edge.from) || !byId.has(edge.to) || parent.has(edge.to)) continue;
782
+ if (!children.has(edge.from)) children.set(edge.from, []);
783
+ children.get(edge.from).push(edge.to);
784
+ parent.add(edge.to);
785
+ }
786
+ const buses = [];
787
+ for (const [parentId, childIds] of children) {
788
+ const parentNode = byId.get(parentId);
789
+ const childNodes = childIds.map((id) => byId.get(id)).filter(Boolean);
790
+ if (!parentNode || childNodes.length === 0) continue;
791
+ const parentX = parentNode.x + parentNode.width / 2;
792
+ const parentY = parentNode.y + parentNode.height;
793
+ const childY = Math.min(...childNodes.map((node) => node.y));
794
+ buses.push({
795
+ id: `tree_bus_${parentId}`,
796
+ parentId,
797
+ childIds,
798
+ parentX: snapGrid(parentX),
799
+ parentY: snapGrid(parentY),
800
+ branchY: snapGrid((parentY + childY) / 2),
801
+ childXs: childNodes.map((node) => snapGrid(node.x + node.width / 2)),
802
+ childY: snapGrid(childY)
803
+ });
804
+ }
805
+ return buses;
806
+ }
807
+ function resolveTargets(targets, ast, groups, edgeIds) {
524
808
  const out = [];
525
809
  for (const t of targets) {
526
810
  if (t === "$nodes") {
527
811
  out.push(...Object.keys(ast.nodes));
528
812
  } else if (t === "$edges") {
529
- continue;
813
+ out.push(...edgeIds);
530
814
  } else if (t.startsWith("$")) {
531
815
  const g = t.slice(1);
532
816
  if (groups[g]) out.push(...groups[g]);
@@ -543,6 +827,8 @@ function scheduleBeats(ast, edges) {
543
827
  const beatRanges = [];
544
828
  let t = 0;
545
829
  let edgeCounter = edges.length;
830
+ const edgeIds = edges.map((e) => e.id);
831
+ const groupMap = Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members]));
546
832
  const hasIntro = ast.beats.some((b) => b.cues.some((c) => c.kind === "show"));
547
833
  if (!hasIntro && Object.keys(ast.nodes).length > 0) {
548
834
  cues.push({
@@ -575,7 +861,10 @@ function scheduleBeats(ast, edges) {
575
861
  const dur = "dur" in cue && cue.dur !== void 0 ? cue.dur : DEFAULTS[cue.kind] ?? 0.5;
576
862
  if (cue.kind === "flow") {
577
863
  for (const seg of cue.segments) {
578
- const edgeId = `flow_${++edgeCounter}`;
864
+ const existing = edges.find(
865
+ (e) => !e.structural && e.from === seg.from && e.to === seg.to && e.kind === seg.op && e.label === seg.label
866
+ );
867
+ const edgeId = existing?.id ?? `flow_${++edgeCounter}`;
579
868
  scheduled.push({
580
869
  start: t,
581
870
  duration: dur / cue.segments.length,
@@ -591,7 +880,7 @@ function scheduleBeats(ast, edges) {
591
880
  return;
592
881
  }
593
882
  if (cue.kind !== "show" && cue.kind !== "hide" && cue.kind !== "glow" && cue.kind !== "focus" && cue.kind !== "frame") return;
594
- const targets = resolveTargets(cue.targets, ast, Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members])));
883
+ const targets = resolveTargets(cue.targets, ast, groupMap, edgeIds);
595
884
  scheduled.push({
596
885
  start: t,
597
886
  duration: dur,
@@ -619,10 +908,49 @@ function scheduleBeats(ast, edges) {
619
908
  }
620
909
  return { cues, beats: beatRanges };
621
910
  }
911
+ function buildSequencePlan(ast, cues) {
912
+ if (diagramType(ast) !== "sequence") return { messages: [], activations: [] };
913
+ const flowCues = cues.filter((cue) => cue.kind === "flow" && cue.segments?.[0]);
914
+ const firstY = TITLE_BAND + NODE_H + 64;
915
+ const lastY = ast.meta.height - SAFE - 40;
916
+ const step = flowCues.length > 1 ? Math.max(36, Math.min(64, (lastY - firstY) / (flowCues.length - 1))) : 0;
917
+ const messages = [];
918
+ const activations = [];
919
+ flowCues.forEach((cue, index) => {
920
+ const segment = cue.segments[0];
921
+ if (!ast.nodes[segment.from] || !ast.nodes[segment.to]) return;
922
+ const message = {
923
+ id: `sequence_${index + 1}`,
924
+ from: segment.from,
925
+ to: segment.to,
926
+ kind: segment.op,
927
+ label: segment.label,
928
+ y: snapGrid(firstY + step * index),
929
+ start: cue.start,
930
+ duration: cue.duration,
931
+ beat: cue.beat
932
+ };
933
+ messages.push(message);
934
+ for (const participant of /* @__PURE__ */ new Set([message.from, message.to])) {
935
+ activations.push({
936
+ id: `${message.id}_${participant}`,
937
+ participant,
938
+ y: message.y - 18,
939
+ height: 36,
940
+ start: message.start,
941
+ duration: message.duration
942
+ });
943
+ }
944
+ });
945
+ return { messages, activations };
946
+ }
622
947
  function compilePlan(ast, theme) {
623
948
  const structuralEdges = collectStructuralEdges(ast);
624
949
  const nodes = layoutNodes(ast, structuralEdges);
950
+ const groupBoundaries = computeGroupBoundaries(ast, nodes);
951
+ const treeBuses = computeTreeBuses(ast, nodes, structuralEdges.filter((edge) => edge.structural));
625
952
  const { cues, beats } = scheduleBeats(ast, structuralEdges);
953
+ const sequence = buildSequencePlan(ast, cues);
626
954
  const shown = /* @__PURE__ */ new Set();
627
955
  for (const cue of cues) {
628
956
  if (cue.kind === "show") cue.targets.forEach((id) => shown.add(id));
@@ -643,11 +971,17 @@ function compilePlan(ast, theme) {
643
971
  meta: ast.meta,
644
972
  theme,
645
973
  title,
974
+ diagramType: diagramType(ast),
646
975
  nodes,
647
976
  edges: structuralEdges,
977
+ groupBoundaries,
978
+ annotations: ast.annotations.slice(0, 2),
648
979
  cues,
649
980
  beats,
650
981
  groups: Object.fromEntries(Object.entries(ast.groups).map(([k, g]) => [k, g.members])),
982
+ treeBuses,
983
+ sequenceMessages: sequence.messages,
984
+ sequenceActivations: sequence.activations,
651
985
  duration
652
986
  };
653
987
  }
@@ -753,6 +1087,92 @@ var THEMES = {
753
1087
  labelPlate: "#191c22",
754
1088
  roles: { ...ROLE_COLORS },
755
1089
  edges: { ...EDGE_COLORS }
1090
+ },
1091
+ editorial: {
1092
+ name: "editorial",
1093
+ canvas: "#fafafa",
1094
+ surface: "#ffffff",
1095
+ surfaceRaised: "#f8fafc",
1096
+ border: "#e2e8f0",
1097
+ text: "#0f172a",
1098
+ textMuted: "#64748b",
1099
+ paper: "#fafafa",
1100
+ ink: "#0f172a",
1101
+ muted: "#6b7280",
1102
+ rule: "#e2e8f0",
1103
+ soft: "#94a3b8",
1104
+ link: "#047857",
1105
+ gridMinor: "rgba(15, 23, 42, 0.06)",
1106
+ gridMajor: "rgba(15, 23, 42, 0.10)",
1107
+ vignette: "rgba(226, 232, 240, 0.5)",
1108
+ accent: "#047857",
1109
+ accentTint: "rgba(4, 120, 87, 0.08)",
1110
+ nodeSurface: "#ffffff",
1111
+ nodeSurfaceRaised: "#f8fafc",
1112
+ hairline: "#e2e8f0",
1113
+ shadow: "rgba(15, 23, 42, 0.08)",
1114
+ labelPlate: "#fafafa",
1115
+ flatCards: true,
1116
+ roles: { ...ROLE_COLORS, compute: "#0f172a", client: "#64748b", data: "#64748b" },
1117
+ edges: {
1118
+ request: "#334155",
1119
+ response: "#64748b",
1120
+ event: "#047857",
1121
+ dependency: "#94a3b8"
1122
+ },
1123
+ fonts: {
1124
+ title: "Georgia, Times New Roman, serif",
1125
+ nodeName: "ui-sans-serif, system-ui, sans-serif",
1126
+ mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
1127
+ },
1128
+ radiusMd: 6,
1129
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
1130
+ },
1131
+ nebula: {
1132
+ name: "nebula",
1133
+ canvas: "#090b1a",
1134
+ surface: "#111735",
1135
+ surfaceRaised: "#1a2250",
1136
+ border: "rgba(167, 139, 250, 0.28)",
1137
+ text: "#eef2ff",
1138
+ textMuted: "#a5b4fc",
1139
+ paper: "#090b1a",
1140
+ ink: "#eef2ff",
1141
+ muted: "#818cf8",
1142
+ rule: "rgba(129, 140, 248, 0.32)",
1143
+ soft: "#67e8f9",
1144
+ link: "#67e8f9",
1145
+ gridMinor: "rgba(129, 140, 248, 0.08)",
1146
+ gridMajor: "rgba(103, 232, 249, 0.16)",
1147
+ vignette: "rgba(3, 5, 18, 0.86)",
1148
+ accent: "#c4b5fd",
1149
+ accentTint: "rgba(196, 181, 253, 0.14)",
1150
+ nodeSurface: "#111735",
1151
+ nodeSurfaceRaised: "#202a5b",
1152
+ hairline: "rgba(196, 181, 253, 0.32)",
1153
+ shadow: "rgba(3, 5, 18, 0.72)",
1154
+ labelPlate: "#12183b",
1155
+ roles: {
1156
+ ...ROLE_COLORS,
1157
+ compute: "#67e8f9",
1158
+ client: "#f9a8d4",
1159
+ data: "#a7f3d0",
1160
+ network: "#93c5fd",
1161
+ flow: "#c4b5fd"
1162
+ },
1163
+ edges: {
1164
+ request: "#67e8f9",
1165
+ response: "#c4b5fd",
1166
+ event: "#f9a8d4",
1167
+ dependency: "#818cf8"
1168
+ },
1169
+ fonts: {
1170
+ title: "Georgia, Times New Roman, serif",
1171
+ nodeName: "ui-sans-serif, system-ui, sans-serif",
1172
+ mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
1173
+ },
1174
+ radiusMd: 12,
1175
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
756
1176
  }
757
1177
  };
758
1178
  function resolveTheme(name) {
@@ -1151,8 +1571,45 @@ function readIndentedBody(blocks, startIdx, parentIndent) {
1151
1571
  }
1152
1572
  return { body, nextIdx: i };
1153
1573
  }
1154
- function readBody(blocks, startIdx, parentIndent, braceDelimited) {
1155
- if (!braceDelimited) return readIndentedBody(blocks, startIdx, parentIndent);
1574
+ var TOP_LEVEL_KEYWORDS_RE = /^(scene|layout|pattern|group|annotation|edge|beat|var|style)\b/;
1575
+ function isTopLevelStatement(line) {
1576
+ if (line === "}") return true;
1577
+ if (TOP_LEVEL_KEYWORDS_RE.test(line)) return true;
1578
+ const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
1579
+ if (!nodeMatch) return false;
1580
+ const kind = canonicalNodeKind(nodeMatch[1].toLowerCase());
1581
+ return NODE_KINDS.has(kind);
1582
+ }
1583
+ function readColonBody(blocks, startIdx, parentIndent, diagnostics, context, headerLine) {
1584
+ const indented = readIndentedBody(blocks, startIdx, parentIndent);
1585
+ if (indented.body.length > 0 || startIdx >= blocks.length) {
1586
+ return indented;
1587
+ }
1588
+ if (isTopLevelStatement(blocks[startIdx].text)) {
1589
+ return indented;
1590
+ }
1591
+ const body = [];
1592
+ let i = startIdx;
1593
+ while (i < blocks.length) {
1594
+ const block = blocks[i];
1595
+ if (block.indent < parentIndent) break;
1596
+ if (block.indent === parentIndent && isTopLevelStatement(block.text)) break;
1597
+ body.push(block);
1598
+ i++;
1599
+ }
1600
+ if (body.length > 0) {
1601
+ diagnostics.push({
1602
+ severity: "warning",
1603
+ message: `${context} body had no indentation; parsed until the next top-level statement (hosts like MDX/JSX often strip indent from template literals)`,
1604
+ line: headerLine
1605
+ });
1606
+ }
1607
+ return { body, nextIdx: i };
1608
+ }
1609
+ function readBody(blocks, startIdx, parentIndent, braceDelimited, diagnostics = [], context = "block", headerLine = 1) {
1610
+ if (!braceDelimited) {
1611
+ return readColonBody(blocks, startIdx, parentIndent, diagnostics, context, headerLine);
1612
+ }
1156
1613
  const body = [];
1157
1614
  let i = startIdx;
1158
1615
  while (i < blocks.length) {
@@ -1261,6 +1718,14 @@ function validateReferences(ast) {
1261
1718
  validateFlowEndpoint(edge.line, edge.from);
1262
1719
  validateFlowEndpoint(edge.line, edge.to);
1263
1720
  }
1721
+ for (const ann of ast.annotations) {
1722
+ if (ann.target && !hasNode(ann.target)) {
1723
+ pushWarning(ast.diagnostics, seen, ann.line, `annotation references unknown target '${ann.target}'`);
1724
+ }
1725
+ }
1726
+ if (ast.annotations.length > 2) {
1727
+ pushWarning(ast.diagnostics, seen, ast.annotations[2].line, "more than 2 annotation callouts; editorial diagrams should use \u22642");
1728
+ }
1264
1729
  for (const beat of ast.beats) {
1265
1730
  visitCues(beat.cues, (cue) => {
1266
1731
  if (cue.kind === "flow") {
@@ -1280,6 +1745,61 @@ function validateReferences(ast) {
1280
1745
  });
1281
1746
  }
1282
1747
  }
1748
+ function detectFlowCycles(ast) {
1749
+ if (ast.meta.type === "state") return;
1750
+ const seen = /* @__PURE__ */ new Set();
1751
+ const adj = /* @__PURE__ */ new Map();
1752
+ const ensure = (id) => {
1753
+ if (!adj.has(id)) adj.set(id, []);
1754
+ };
1755
+ for (const id of Object.keys(ast.nodes)) ensure(id);
1756
+ const addEdge = (from, to, line) => {
1757
+ if (from === to) return;
1758
+ if (!ast.nodes[from] || !ast.nodes[to]) return;
1759
+ ensure(from);
1760
+ adj.get(from).push({ to, line });
1761
+ };
1762
+ for (const edge of ast.edges) {
1763
+ if (edge.kind !== "response") addEdge(edge.from, edge.to, edge.line);
1764
+ }
1765
+ for (const beat of ast.beats) {
1766
+ visitCues(beat.cues, (cue) => {
1767
+ if (cue.kind !== "flow") return;
1768
+ for (const segment of cue.segments) {
1769
+ if (segment.op !== "response") addEdge(segment.from, segment.to, cue.line);
1770
+ }
1771
+ });
1772
+ }
1773
+ const WHITE = 0;
1774
+ const GRAY = 1;
1775
+ const BLACK = 2;
1776
+ const color = /* @__PURE__ */ new Map();
1777
+ for (const id of adj.keys()) color.set(id, WHITE);
1778
+ const stack = [];
1779
+ const dfs = (node) => {
1780
+ color.set(node, GRAY);
1781
+ stack.push(node);
1782
+ for (const { to, line } of adj.get(node) ?? []) {
1783
+ if (color.get(to) === GRAY) {
1784
+ const idx = stack.indexOf(to);
1785
+ const cyclePath = [...stack.slice(idx), to].join(" -> ");
1786
+ pushWarning(
1787
+ ast.diagnostics,
1788
+ seen,
1789
+ line,
1790
+ `flow cycle detected: ${cyclePath} \u2014 if '${to}' is receiving a reply/return value here, use '<-' for this edge instead of '->'/'~>'/'--' (an unmarked cycle can crush the ranked layout and overlap nodes)`
1791
+ );
1792
+ } else if (color.get(to) === WHITE) {
1793
+ dfs(to);
1794
+ }
1795
+ }
1796
+ stack.pop();
1797
+ color.set(node, BLACK);
1798
+ };
1799
+ for (const id of adj.keys()) {
1800
+ if (color.get(id) === WHITE) dfs(id);
1801
+ }
1802
+ }
1283
1803
  function parse(source, opts = {}) {
1284
1804
  const lines = source.replace(/\r\n/g, "\n").split("\n");
1285
1805
  const diagnostics = [];
@@ -1296,8 +1816,10 @@ function parse(source, opts = {}) {
1296
1816
  const groups = {};
1297
1817
  const patterns = {};
1298
1818
  const beats = [];
1819
+ const annotations = [];
1299
1820
  let title = "";
1300
1821
  let edgeCounter = 0;
1822
+ let annotationCounter = 0;
1301
1823
  const rawBlocks = readBlocks(lines.map(stripComment));
1302
1824
  const { vars, rest } = extractVars(rawBlocks, diagnostics);
1303
1825
  const blocks = applyVars(rest, vars);
@@ -1334,6 +1856,14 @@ function parse(source, opts = {}) {
1334
1856
  else if (k === "duration") meta.duration = Number(v);
1335
1857
  else if (k === "theme") meta.theme = String(v);
1336
1858
  else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
1859
+ else if (k === "type") {
1860
+ const t = String(v).toLowerCase();
1861
+ if (!DIAGRAM_TYPES.has(t)) {
1862
+ diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
1863
+ } else {
1864
+ meta.type = t;
1865
+ }
1866
+ }
1337
1867
  }
1338
1868
  i++;
1339
1869
  continue;
@@ -1354,7 +1884,15 @@ function parse(source, opts = {}) {
1354
1884
  const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*(?::|\{)\s*$/);
1355
1885
  if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
1356
1886
  const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
1357
- const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1887
+ const { body, nextIdx } = readBody(
1888
+ blocks,
1889
+ i + 1,
1890
+ block.indent,
1891
+ line.endsWith("{"),
1892
+ diagnostics,
1893
+ `pattern '${m[1]}'`,
1894
+ lineNo
1895
+ );
1358
1896
  const cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1359
1897
  patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
1360
1898
  i = nextIdx;
@@ -1375,7 +1913,14 @@ function parse(source, opts = {}) {
1375
1913
  }
1376
1914
  const header = line.match(/^group\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
1377
1915
  if (!header) throw new ParseError(`expected group name: A B C`, lineNo);
1378
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1916
+ const { body, nextIdx } = readColonBody(
1917
+ blocks,
1918
+ i + 1,
1919
+ block.indent,
1920
+ diagnostics,
1921
+ `group '${header[1]}'`,
1922
+ lineNo
1923
+ );
1379
1924
  const members = body.flatMap((b) => splitTargets(b.text));
1380
1925
  if (members.length === 0) throw new ParseError(`group '${header[1]}' has no members`, lineNo);
1381
1926
  groups[header[1]] = {
@@ -1388,6 +1933,23 @@ function parse(source, opts = {}) {
1388
1933
  i = nextIdx;
1389
1934
  continue;
1390
1935
  }
1936
+ if (line.startsWith("annotation ")) {
1937
+ const str = parseStringToken(line.slice("annotation ".length).trim());
1938
+ if (!str) throw new ParseError(`expected annotation "text" with optional target= position=`, lineNo);
1939
+ const props = parseProps(str.rest);
1940
+ const target = typeof props.target === "string" ? props.target : void 0;
1941
+ const position = typeof props.position === "string" ? props.position : void 0;
1942
+ annotations.push({
1943
+ id: `ann_${++annotationCounter}`,
1944
+ text: str.value,
1945
+ target,
1946
+ position,
1947
+ props,
1948
+ line: lineNo
1949
+ });
1950
+ i++;
1951
+ continue;
1952
+ }
1391
1953
  if (line.startsWith("edge ")) {
1392
1954
  const m = line.match(/^edge\s+(\w+)\s*:\s*(.+)$/);
1393
1955
  if (!m) throw new ParseError(`expected edge id: A -> B`, lineNo);
@@ -1409,7 +1971,15 @@ function parse(source, opts = {}) {
1409
1971
  if (line.startsWith("beat ")) {
1410
1972
  const m = line.match(/^beat\s+([\w.-]+)(?:\s+"([^"]*)")?\s*(?::|\{)\s*$/);
1411
1973
  if (!m) throw new ParseError(`expected beat name:`, lineNo);
1412
- const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1974
+ const { body, nextIdx } = readBody(
1975
+ blocks,
1976
+ i + 1,
1977
+ block.indent,
1978
+ line.endsWith("{"),
1979
+ diagnostics,
1980
+ `beat '${m[1]}'`,
1981
+ lineNo
1982
+ );
1413
1983
  let cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1414
1984
  cues = expandPatternCues(cues, patterns, lineNo);
1415
1985
  beats.push({ name: m[1], label: m[2], cues, line: lineNo });
@@ -1455,11 +2025,13 @@ function parse(source, opts = {}) {
1455
2025
  nodes,
1456
2026
  edges,
1457
2027
  groups,
2028
+ annotations,
1458
2029
  patterns,
1459
2030
  beats,
1460
2031
  diagnostics
1461
2032
  };
1462
2033
  validateReferences(ast);
2034
+ detectFlowCycles(ast);
1463
2035
  const errors = ast.diagnostics.filter((d) => d.severity === "error");
1464
2036
  if (errors.length) {
1465
2037
  throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
@@ -1479,6 +2051,7 @@ function parseAndCompile(source) {
1479
2051
  export {
1480
2052
  BEAT_CUE_KEYWORDS,
1481
2053
  CUE_ALIASES,
2054
+ DIAGRAM_TYPES,
1482
2055
  EDGE_OPERATORS,
1483
2056
  NODE_ALIASES,
1484
2057
  NODE_KINDS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
4
4
  "description": "Diagram-native MarkdyScript parser and compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",