@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 +4 -3
- package/dist/index.d.ts +93 -1
- package/dist/index.js +604 -31
- package/package.json +1 -1
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(
|
|
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([
|
|
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
|
-
|
|
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 =
|
|
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({
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
1155
|
-
|
|
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(
|
|
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 } =
|
|
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(
|
|
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