@markdy/core 0.8.11 → 0.8.12

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),
513
593
  width: NODE_W,
514
594
  height: NODE_H,
515
- style,
595
+ style: decl.style ? ast.styles[decl.style]?.props : void 0,
516
596
  props: decl.props,
517
- opacity: 0
597
+ opacity: 0,
598
+ shape: nodeShape(decl.kind, dtype),
599
+ focal,
600
+ column: opts.columnLayout ? idx : void 0
518
601
  });
519
602
  });
520
603
  }
521
604
  return nodes;
522
605
  }
523
- function resolveTargets(targets, ast, groups) {
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),
654
+ width: NODE_W,
655
+ height: NODE_H,
656
+ style: decl.style ? ast.styles[decl.style]?.props : void 0,
657
+ props: decl.props,
658
+ opacity: 0,
659
+ shape: "card"
660
+ });
661
+ });
662
+ }
663
+ return nodes;
664
+ }
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) {
@@ -1261,6 +1681,14 @@ function validateReferences(ast) {
1261
1681
  validateFlowEndpoint(edge.line, edge.from);
1262
1682
  validateFlowEndpoint(edge.line, edge.to);
1263
1683
  }
1684
+ for (const ann of ast.annotations) {
1685
+ if (ann.target && !hasNode(ann.target)) {
1686
+ pushWarning(ast.diagnostics, seen, ann.line, `annotation references unknown target '${ann.target}'`);
1687
+ }
1688
+ }
1689
+ if (ast.annotations.length > 2) {
1690
+ pushWarning(ast.diagnostics, seen, ast.annotations[2].line, "more than 2 annotation callouts; editorial diagrams should use \u22642");
1691
+ }
1264
1692
  for (const beat of ast.beats) {
1265
1693
  visitCues(beat.cues, (cue) => {
1266
1694
  if (cue.kind === "flow") {
@@ -1280,6 +1708,61 @@ function validateReferences(ast) {
1280
1708
  });
1281
1709
  }
1282
1710
  }
1711
+ function detectFlowCycles(ast) {
1712
+ if (ast.meta.type === "state") return;
1713
+ const seen = /* @__PURE__ */ new Set();
1714
+ const adj = /* @__PURE__ */ new Map();
1715
+ const ensure = (id) => {
1716
+ if (!adj.has(id)) adj.set(id, []);
1717
+ };
1718
+ for (const id of Object.keys(ast.nodes)) ensure(id);
1719
+ const addEdge = (from, to, line) => {
1720
+ if (from === to) return;
1721
+ if (!ast.nodes[from] || !ast.nodes[to]) return;
1722
+ ensure(from);
1723
+ adj.get(from).push({ to, line });
1724
+ };
1725
+ for (const edge of ast.edges) {
1726
+ if (edge.kind !== "response") addEdge(edge.from, edge.to, edge.line);
1727
+ }
1728
+ for (const beat of ast.beats) {
1729
+ visitCues(beat.cues, (cue) => {
1730
+ if (cue.kind !== "flow") return;
1731
+ for (const segment of cue.segments) {
1732
+ if (segment.op !== "response") addEdge(segment.from, segment.to, cue.line);
1733
+ }
1734
+ });
1735
+ }
1736
+ const WHITE = 0;
1737
+ const GRAY = 1;
1738
+ const BLACK = 2;
1739
+ const color = /* @__PURE__ */ new Map();
1740
+ for (const id of adj.keys()) color.set(id, WHITE);
1741
+ const stack = [];
1742
+ const dfs = (node) => {
1743
+ color.set(node, GRAY);
1744
+ stack.push(node);
1745
+ for (const { to, line } of adj.get(node) ?? []) {
1746
+ if (color.get(to) === GRAY) {
1747
+ const idx = stack.indexOf(to);
1748
+ const cyclePath = [...stack.slice(idx), to].join(" -> ");
1749
+ pushWarning(
1750
+ ast.diagnostics,
1751
+ seen,
1752
+ line,
1753
+ `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)`
1754
+ );
1755
+ } else if (color.get(to) === WHITE) {
1756
+ dfs(to);
1757
+ }
1758
+ }
1759
+ stack.pop();
1760
+ color.set(node, BLACK);
1761
+ };
1762
+ for (const id of adj.keys()) {
1763
+ if (color.get(id) === WHITE) dfs(id);
1764
+ }
1765
+ }
1283
1766
  function parse(source, opts = {}) {
1284
1767
  const lines = source.replace(/\r\n/g, "\n").split("\n");
1285
1768
  const diagnostics = [];
@@ -1296,8 +1779,10 @@ function parse(source, opts = {}) {
1296
1779
  const groups = {};
1297
1780
  const patterns = {};
1298
1781
  const beats = [];
1782
+ const annotations = [];
1299
1783
  let title = "";
1300
1784
  let edgeCounter = 0;
1785
+ let annotationCounter = 0;
1301
1786
  const rawBlocks = readBlocks(lines.map(stripComment));
1302
1787
  const { vars, rest } = extractVars(rawBlocks, diagnostics);
1303
1788
  const blocks = applyVars(rest, vars);
@@ -1334,6 +1819,14 @@ function parse(source, opts = {}) {
1334
1819
  else if (k === "duration") meta.duration = Number(v);
1335
1820
  else if (k === "theme") meta.theme = String(v);
1336
1821
  else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
1822
+ else if (k === "type") {
1823
+ const t = String(v).toLowerCase();
1824
+ if (!DIAGRAM_TYPES.has(t)) {
1825
+ diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
1826
+ } else {
1827
+ meta.type = t;
1828
+ }
1829
+ }
1337
1830
  }
1338
1831
  i++;
1339
1832
  continue;
@@ -1388,6 +1881,23 @@ function parse(source, opts = {}) {
1388
1881
  i = nextIdx;
1389
1882
  continue;
1390
1883
  }
1884
+ if (line.startsWith("annotation ")) {
1885
+ const str = parseStringToken(line.slice("annotation ".length).trim());
1886
+ if (!str) throw new ParseError(`expected annotation "text" with optional target= position=`, lineNo);
1887
+ const props = parseProps(str.rest);
1888
+ const target = typeof props.target === "string" ? props.target : void 0;
1889
+ const position = typeof props.position === "string" ? props.position : void 0;
1890
+ annotations.push({
1891
+ id: `ann_${++annotationCounter}`,
1892
+ text: str.value,
1893
+ target,
1894
+ position,
1895
+ props,
1896
+ line: lineNo
1897
+ });
1898
+ i++;
1899
+ continue;
1900
+ }
1391
1901
  if (line.startsWith("edge ")) {
1392
1902
  const m = line.match(/^edge\s+(\w+)\s*:\s*(.+)$/);
1393
1903
  if (!m) throw new ParseError(`expected edge id: A -> B`, lineNo);
@@ -1455,11 +1965,13 @@ function parse(source, opts = {}) {
1455
1965
  nodes,
1456
1966
  edges,
1457
1967
  groups,
1968
+ annotations,
1458
1969
  patterns,
1459
1970
  beats,
1460
1971
  diagnostics
1461
1972
  };
1462
1973
  validateReferences(ast);
1974
+ detectFlowCycles(ast);
1463
1975
  const errors = ast.diagnostics.filter((d) => d.severity === "error");
1464
1976
  if (errors.length) {
1465
1977
  throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
@@ -1479,6 +1991,7 @@ function parseAndCompile(source) {
1479
1991
  export {
1480
1992
  BEAT_CUE_KEYWORDS,
1481
1993
  CUE_ALIASES,
1994
+ DIAGRAM_TYPES,
1482
1995
  EDGE_OPERATORS,
1483
1996
  NODE_ALIASES,
1484
1997
  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.12",
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",