@diagrammo/dgmo 0.8.18 → 0.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli.cjs +89 -130
  2. package/dist/index.cjs +1202 -993
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +216 -114
  5. package/dist/index.d.ts +216 -114
  6. package/dist/index.js +1211 -985
  7. package/dist/index.js.map +1 -1
  8. package/docs/language-reference.md +73 -0
  9. package/package.json +22 -9
  10. package/src/boxes-and-lines/parser.ts +8 -3
  11. package/src/c4/parser.ts +8 -7
  12. package/src/class/parser.ts +6 -0
  13. package/src/cli.ts +1 -9
  14. package/src/d3.ts +16 -234
  15. package/src/dgmo-router.ts +97 -5
  16. package/src/diagnostics.ts +16 -6
  17. package/src/echarts.ts +43 -10
  18. package/src/er/parser.ts +22 -2
  19. package/src/gantt/renderer.ts +153 -91
  20. package/src/graph/flowchart-parser.ts +89 -52
  21. package/src/graph/state-parser.ts +60 -35
  22. package/src/index.ts +23 -18
  23. package/src/infra/parser.ts +9 -2
  24. package/src/kanban/renderer.ts +2 -2
  25. package/src/palettes/color-utils.ts +4 -12
  26. package/src/palettes/index.ts +0 -4
  27. package/src/render.ts +30 -16
  28. package/src/sequence/collapse.ts +169 -0
  29. package/src/sequence/parser.ts +21 -4
  30. package/src/sequence/renderer.ts +198 -52
  31. package/src/sharing.ts +86 -49
  32. package/src/sitemap/renderer.ts +1 -6
  33. package/src/utils/arrows.ts +180 -11
  34. package/src/utils/d3-types.ts +4 -0
  35. package/src/utils/legend-constants.ts +11 -4
  36. package/src/utils/legend-d3.ts +171 -0
  37. package/src/utils/legend-layout.ts +140 -13
  38. package/src/utils/legend-types.ts +45 -0
  39. package/src/utils/time-ticks.ts +213 -0
  40. package/src/branding.ts +0 -67
  41. package/src/dgmo-mermaid.ts +0 -262
  42. package/src/palettes/mermaid-bridge.ts +0 -220
@@ -5,6 +5,24 @@
5
5
  // Labeled arrow syntax (always left-to-right):
6
6
  // Sync: `-label->`
7
7
  // Async: `~label~>`
8
+ //
9
+ // In-arrow label character-set contract (see docs/dgmo-language-spec.md
10
+ // §"In-Arrow Message Labels"):
11
+ // - Allowed: any codepoint except the forbidden substrings and forbidden
12
+ // control characters below.
13
+ // - Forbidden substrings: "->", "~>" (arrow-token lookalikes inside labels).
14
+ // Use the post-colon form for labels that need these symbols:
15
+ // `A -> B: uses -> to chain`
16
+ // - Forbidden characters: C0 control chars U+0000–U+001F EXCEPT U+0009 (tab),
17
+ // and U+007F (DEL).
18
+ // - Whitespace: leading/trailing trimmed; internal runs (incl. tab, NBSP,
19
+ // ZWSP) preserved — never collapsed.
20
+ // - Plain text only: no markdown interpretation. `*`, `_`, backticks,
21
+ // `[`, `]`, `{`, `}` are literal characters.
22
+
23
+ import type { DgmoError } from '../diagnostics';
24
+ import { makeDgmoError } from '../diagnostics';
25
+ import { RECOGNIZED_COLOR_NAMES } from '../colors';
8
26
 
9
27
  interface ParsedArrow {
10
28
  from: string;
@@ -13,6 +31,160 @@ interface ParsedArrow {
13
31
  async: boolean;
14
32
  }
15
33
 
34
+ // ============================================================
35
+ // Diagnostic codes (TD-16)
36
+ // ============================================================
37
+
38
+ /**
39
+ * Stable diagnostic codes for in-arrow label parsing errors.
40
+ *
41
+ * **Active codes** — emitted by the parser pipeline today:
42
+ * - `ARROW_SUBSTRING_IN_LABEL` (TD-13)
43
+ * - `CONTROL_CHAR_IN_LABEL` (TD-14)
44
+ *
45
+ * **Reserved codes** — declared but NOT currently emitted. These are
46
+ * placeholders for future tightening of the arrow-tokenization rules
47
+ * described in TD-9. Today's chart parsers catch these cases through
48
+ * their own regex machinery with different diagnostics. A follow-up
49
+ * spec that introduces a dedicated tokenizer can start emitting them
50
+ * without changing the public code shape:
51
+ * - `TRAILING_ARROW_TEXT` — extra `->`/`~>` after the primary arrow
52
+ * - `MIXED_ARROW_DELIMITERS` — opening delim type doesn't match arrow
53
+ *
54
+ * See `docs/dgmo-language-spec-decisions.md` → TD-16 for the rationale.
55
+ */
56
+ export const ARROW_DIAGNOSTIC_CODES = {
57
+ /** Active: label contains `->` or `~>` substring (TD-13). */
58
+ ARROW_SUBSTRING_IN_LABEL: 'E_ARROW_SUBSTRING_IN_LABEL',
59
+ /** Active: label contains a forbidden control character (TD-14). */
60
+ CONTROL_CHAR_IN_LABEL: 'E_CONTROL_CHAR_IN_LABEL',
61
+ /** Reserved: not currently emitted by any parser. See JSDoc above. */
62
+ TRAILING_ARROW_TEXT: 'E_TRAILING_ARROW_TEXT',
63
+ /** Reserved: not currently emitted by any parser. See JSDoc above. */
64
+ MIXED_ARROW_DELIMITERS: 'E_MIXED_ARROW_DELIMITERS',
65
+ } as const;
66
+
67
+ // ============================================================
68
+ // validateLabelCharacters (TD-13, TD-14)
69
+ // ============================================================
70
+
71
+ /**
72
+ * Validate an in-arrow label against the TD-13 and TD-14 character-set
73
+ * contract. Returns diagnostics (possibly empty). Does NOT mutate the label —
74
+ * callers that want a normalized label should trim before calling.
75
+ *
76
+ * TD-13: label must not contain the substrings "->" or "~>".
77
+ * TD-14: label must not contain C0 control chars other than tab, and no DEL.
78
+ */
79
+ export function validateLabelCharacters(
80
+ label: string,
81
+ lineNumber: number
82
+ ): DgmoError[] {
83
+ const out: DgmoError[] = [];
84
+
85
+ // TD-13: forbidden substrings
86
+ if (label.includes('->') || label.includes('~>')) {
87
+ out.push(
88
+ makeDgmoError(
89
+ lineNumber,
90
+ 'Arrow symbols (-> or ~>) are not allowed inside a label. ' +
91
+ 'Move the label after the arrow: "A -> B: uses -> to chain". ' +
92
+ 'See "In-Arrow Message Labels" → Forbidden.',
93
+ 'error',
94
+ ARROW_DIAGNOSTIC_CODES.ARROW_SUBSTRING_IN_LABEL
95
+ )
96
+ );
97
+ }
98
+
99
+ // TD-14: control chars (iterate codepoints to handle surrogate pairs)
100
+ for (const ch of label) {
101
+ const cp = ch.codePointAt(0)!;
102
+ const isC0 = cp >= 0x00 && cp <= 0x1f && cp !== 0x09; // allow tab
103
+ const isDel = cp === 0x7f;
104
+ if (isC0 || isDel) {
105
+ const hex = cp.toString(16).toUpperCase().padStart(4, '0');
106
+ out.push(
107
+ makeDgmoError(
108
+ lineNumber,
109
+ `Label contains a control character (U+${hex}). ` +
110
+ 'Remove it and use plain text.',
111
+ 'error',
112
+ ARROW_DIAGNOSTIC_CODES.CONTROL_CHAR_IN_LABEL
113
+ )
114
+ );
115
+ break; // one diagnostic per label is enough
116
+ }
117
+ }
118
+
119
+ return out;
120
+ }
121
+
122
+ // ============================================================
123
+ // parseInArrowLabel (TD-1, TD-8, TD-10, TD-13, TD-14)
124
+ // ============================================================
125
+
126
+ export interface ParseInArrowLabelResult {
127
+ /** Cleaned label (trimmed; `undefined` if empty after trim per TD-10). */
128
+ label: string | undefined;
129
+ diagnostics: DgmoError[];
130
+ }
131
+
132
+ /**
133
+ * Normalize and validate a raw in-arrow label.
134
+ *
135
+ * Behavior:
136
+ * - Trims leading/trailing whitespace (TD-8: internal whitespace preserved).
137
+ * - Empty-after-trim → `{ label: undefined }` (TD-10 normalization).
138
+ * - TD-13: emits `E_ARROW_SUBSTRING_IN_LABEL` if `->` or `~>` is present.
139
+ * - TD-14: emits `E_CONTROL_CHAR_IN_LABEL` for forbidden control chars.
140
+ *
141
+ * This helper is intentionally chart-agnostic: it operates on an already
142
+ * extracted label string, leaving each chart's existing arrow-finding
143
+ * tokenization in place. TD-11 color-parens is handled inside the
144
+ * flowchart and state `parseArrowToken` functions because those are the
145
+ * only charts that interpret `-(color)->` as a colored edge; they use
146
+ * `matchColorParens()` from this module for the shared lookup.
147
+ */
148
+ export function parseInArrowLabel(
149
+ rawLabel: string,
150
+ lineNumber: number
151
+ ): ParseInArrowLabelResult {
152
+ const trimmed = rawLabel.trim();
153
+
154
+ // TD-10: empty/whitespace-only label normalizes to undefined
155
+ if (trimmed.length === 0) {
156
+ return { label: undefined, diagnostics: [] };
157
+ }
158
+
159
+ // TD-13 / TD-14 validation
160
+ const diagnostics = validateLabelCharacters(trimmed, lineNumber);
161
+
162
+ return { label: trimmed, diagnostics };
163
+ }
164
+
165
+ // ============================================================
166
+ // matchColorParens — shared TD-11 helper for flowchart and state
167
+ // ============================================================
168
+
169
+ /**
170
+ * Test whether a string matches the TD-11 color-parens form `(colorName)`
171
+ * where `colorName` is one of the 11 recognized palette color names from
172
+ * `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
173
+ * on a match, or `null` on fall-through (whole string becomes a label).
174
+ *
175
+ * Used by flowchart and state parsers to keep the color-parens recognition
176
+ * rule in one place — do NOT re-implement the regex in chart parsers.
177
+ */
178
+ export function matchColorParens(content: string): string | null {
179
+ const m = content.match(/^\(([A-Za-z]+)\)$/);
180
+ if (!m) return null;
181
+ const candidate = m[1].toLowerCase();
182
+ if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
183
+ return candidate;
184
+ }
185
+ return null;
186
+ }
187
+
16
188
  // Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
17
189
  const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
18
190
  const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
@@ -23,8 +195,6 @@ const RETURN_ASYNC_LABELED_RE = /^(.+?)\s*<~(.+)~\s*(.+)$/;
23
195
  const BIDI_SYNC_RE = /^(.+?)\s*<-(.+)->\s*(.+)$/;
24
196
  const BIDI_ASYNC_RE = /^(.+?)\s*<~(.+)~>\s*(.+)$/;
25
197
 
26
- const ARROW_CHARS = ['->', '~>'];
27
-
28
198
  /**
29
199
  * Try to parse a labeled arrow from a trimmed line.
30
200
  *
@@ -32,6 +202,14 @@ const ARROW_CHARS = ['->', '~>'];
32
202
  * - `ParsedArrow` if matched and valid
33
203
  * - `{ error: string }` if matched but invalid (deprecated syntax)
34
204
  * - `null` if not a labeled arrow (caller should fall through to bare patterns)
205
+ *
206
+ * Note: arrow-char-in-label validation (TD-13) is NOT performed here —
207
+ * callers must route the returned `label` through `parseInArrowLabel` or
208
+ * `validateLabelCharacters` to get the unified `E_ARROW_SUBSTRING_IN_LABEL`
209
+ * diagnostic with the correct code. In practice this path is unreachable
210
+ * because arrow regexes are greedy enough to absorb inner `->`/`~>` tokens
211
+ * into the source/destination captures, but the check remains at the
212
+ * validator level for defense in depth.
35
213
  */
36
214
  export function parseArrow(
37
215
  line: string
@@ -73,15 +251,6 @@ export function parseArrow(
73
251
  // Empty label (e.g. `--> B`) — fall through to plain arrow handling
74
252
  if (!label) return null;
75
253
 
76
- // Validate: no arrow chars inside label
77
- for (const arrow of ARROW_CHARS) {
78
- if (label.includes(arrow)) {
79
- return {
80
- error: 'Arrow characters (->, ~>) are not allowed inside labels',
81
- };
82
- }
83
- }
84
-
85
254
  return {
86
255
  from: m[1],
87
256
  to: m[3],
@@ -0,0 +1,4 @@
1
+ export interface D3ExportDimensions {
2
+ width?: number;
3
+ height?: number;
4
+ }
@@ -16,10 +16,6 @@ export const LEGEND_EYE_SIZE = 14;
16
16
  export const LEGEND_EYE_GAP = 6;
17
17
  export const LEGEND_ICON_W = 20;
18
18
 
19
- // ── Spacing constants (centralized legend system) ───────────
20
- export const LEGEND_TOP_PAD = 12;
21
- export const LEGEND_TITLE_GAP = 8;
22
- export const LEGEND_CONTENT_GAP = 12;
23
19
  export const LEGEND_MAX_ENTRY_ROWS = 3;
24
20
 
25
21
  // ── Proportional text measurement ────────────────────────────
@@ -54,3 +50,14 @@ export const EYE_OPEN_PATH =
54
50
  'M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z';
55
51
  export const EYE_CLOSED_PATH =
56
52
  'M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1';
53
+
54
+ // ── Controls group constants ────────────────────────────────
55
+ // Gear/cog icon (14×14 viewBox) — 6 flat teeth with center hole
56
+ // Computed from polar coordinates: outerR=5.5, innerR=3.5, holeR=2, center=(7,7)
57
+ // Uses evenodd fill-rule for the center hole
58
+ export const CONTROLS_ICON_PATH =
59
+ 'M5.6 1.7L8.4 1.7L7.9 3.6L9.5 4.5L10.9 3.1L12.3 5.6L10.4 6.1L10.4 7.9L12.3 8.4L10.9 10.9L9.5 9.5L7.9 10.4L8.4 12.3L5.6 12.3L6.1 10.4L4.5 9.5L3.1 10.9L1.7 8.4L3.6 7.9L3.6 6.1L1.7 5.6L3.1 3.1L4.5 4.5L6.1 3.6Z' +
60
+ 'M5 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z';
61
+ export const LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
62
+ export const LEGEND_TOGGLE_OFF_OPACITY = 0.4;
63
+ export const LEGEND_GEAR_PILL_W = 14 + LEGEND_PILL_PAD; // gear icon (14) + padding
@@ -9,6 +9,9 @@ import {
9
9
  LEGEND_DOT_R,
10
10
  LEGEND_ENTRY_FONT_SIZE,
11
11
  LEGEND_ENTRY_DOT_GAP,
12
+ LEGEND_TOGGLE_DOT_R,
13
+ LEGEND_TOGGLE_OFF_OPACITY,
14
+ CONTROLS_ICON_PATH,
12
15
  measureLegendText,
13
16
  } from './legend-constants';
14
17
  import { computeLegendLayout } from './legend-layout';
@@ -24,6 +27,7 @@ import type {
24
27
  LegendPillLayout,
25
28
  LegendCapsuleLayout,
26
29
  LegendControlLayout,
30
+ ControlsGroupLayout,
27
31
  D3Sel,
28
32
  } from './legend-types';
29
33
 
@@ -83,6 +87,19 @@ export function renderLegendD3(
83
87
  renderPill(legendG, pill, palette, groupBg, callbacks);
84
88
  }
85
89
 
90
+ // Render controls group (gear pill / capsule)
91
+ if (currentLayout.controlsGroup) {
92
+ renderControlsGroup(
93
+ legendG,
94
+ currentLayout.controlsGroup,
95
+ palette,
96
+ groupBg,
97
+ pillBorder,
98
+ callbacks,
99
+ config
100
+ );
101
+ }
102
+
86
103
  // Render controls
87
104
  for (const ctrl of currentLayout.controls) {
88
105
  renderControl(
@@ -398,3 +415,157 @@ function renderControl(
398
415
  g.on('click', () => onClick());
399
416
  }
400
417
  }
418
+
419
+ // ── Controls group (gear pill / capsule) ───────────────────
420
+
421
+ function renderControlsGroup(
422
+ parent: D3Sel,
423
+ layout: ControlsGroupLayout,
424
+ palette: LegendPalette,
425
+ groupBg: string,
426
+ pillBorder: string,
427
+ callbacks?: LegendCallbacks,
428
+ config?: LegendConfig
429
+ ): void {
430
+ const g = parent
431
+ .append('g')
432
+ .attr('transform', `translate(${layout.x},${layout.y})`)
433
+ .attr('data-legend-controls', layout.expanded ? 'expanded' : 'collapsed')
434
+ .attr('data-export-ignore', 'true')
435
+ .style('cursor', 'pointer');
436
+
437
+ if (!layout.expanded) {
438
+ // Collapsed: gear pill
439
+ g.append('rect')
440
+ .attr('width', layout.width)
441
+ .attr('height', layout.height)
442
+ .attr('rx', layout.height / 2)
443
+ .attr('fill', groupBg);
444
+
445
+ // Gear icon centered
446
+ const iconSize = 14;
447
+ const iconX = (layout.width - iconSize) / 2;
448
+ const iconY = (layout.height - iconSize) / 2;
449
+ g.append('path')
450
+ .attr('d', CONTROLS_ICON_PATH)
451
+ .attr('transform', `translate(${iconX},${iconY})`)
452
+ .attr('fill', palette.textMuted)
453
+ .attr('fill-rule', 'evenodd')
454
+ .attr('pointer-events', 'none');
455
+
456
+ if (callbacks?.onControlsExpand) {
457
+ const cb = callbacks.onControlsExpand;
458
+ g.on('click', () => cb());
459
+ }
460
+ } else {
461
+ // Expanded: capsule with gear pill + toggle entries
462
+ const pill = layout.pill;
463
+
464
+ // Outer capsule background
465
+ g.append('rect')
466
+ .attr('width', layout.width)
467
+ .attr('height', layout.height)
468
+ .attr('rx', LEGEND_HEIGHT / 2)
469
+ .attr('fill', groupBg);
470
+
471
+ // Inner gear pill
472
+ const pillG = g
473
+ .append('g')
474
+ .attr('class', 'controls-gear-pill')
475
+ .style('cursor', 'pointer');
476
+
477
+ pillG
478
+ .append('rect')
479
+ .attr('x', pill.x)
480
+ .attr('y', pill.y)
481
+ .attr('width', pill.width)
482
+ .attr('height', pill.height)
483
+ .attr('rx', pill.height / 2)
484
+ .attr('fill', palette.bg);
485
+
486
+ pillG
487
+ .append('rect')
488
+ .attr('x', pill.x)
489
+ .attr('y', pill.y)
490
+ .attr('width', pill.width)
491
+ .attr('height', pill.height)
492
+ .attr('rx', pill.height / 2)
493
+ .attr('fill', 'none')
494
+ .attr('stroke', pillBorder)
495
+ .attr('stroke-width', 0.75);
496
+
497
+ // Gear icon inside pill
498
+ const iconSize = 14;
499
+ const iconX = pill.x + (pill.width - iconSize) / 2;
500
+ const iconY = pill.y + (pill.height - iconSize) / 2;
501
+ pillG
502
+ .append('path')
503
+ .attr('d', CONTROLS_ICON_PATH)
504
+ .attr('transform', `translate(${iconX},${iconY})`)
505
+ .attr('fill', palette.text)
506
+ .attr('fill-rule', 'evenodd')
507
+ .attr('pointer-events', 'none');
508
+
509
+ // Click on gear pill collapses
510
+ if (callbacks?.onControlsExpand) {
511
+ const cb = callbacks.onControlsExpand;
512
+ pillG.on('click', (event: Event) => {
513
+ event.stopPropagation();
514
+ cb();
515
+ });
516
+ }
517
+
518
+ // Toggle entries
519
+ const toggles = config?.controlsGroup?.toggles ?? [];
520
+ for (const tl of layout.toggles) {
521
+ const toggle = toggles.find((t) => t.id === tl.id);
522
+ const entryG = g
523
+ .append('g')
524
+ .attr('data-controls-toggle', tl.id)
525
+ .style('cursor', 'pointer');
526
+
527
+ if (tl.active) {
528
+ // Filled dot
529
+ entryG
530
+ .append('circle')
531
+ .attr('cx', tl.dotCx)
532
+ .attr('cy', tl.dotCy)
533
+ .attr('r', LEGEND_TOGGLE_DOT_R)
534
+ .attr('fill', palette.primary ?? palette.text);
535
+ } else {
536
+ // Hollow dot
537
+ entryG
538
+ .append('circle')
539
+ .attr('cx', tl.dotCx)
540
+ .attr('cy', tl.dotCy)
541
+ .attr('r', LEGEND_TOGGLE_DOT_R)
542
+ .attr('fill', 'none')
543
+ .attr('stroke', palette.textMuted)
544
+ .attr('stroke-width', 1);
545
+ }
546
+
547
+ // Label
548
+ entryG
549
+ .append('text')
550
+ .attr('x', tl.textX)
551
+ .attr('y', tl.textY)
552
+ .attr('dominant-baseline', 'central')
553
+ .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
554
+ .attr('fill', palette.textMuted)
555
+ .attr('opacity', tl.active ? 1 : LEGEND_TOGGLE_OFF_OPACITY)
556
+ .attr('font-family', FONT_FAMILY)
557
+ .text(tl.label);
558
+
559
+ // Click on toggle entry
560
+ if (callbacks?.onControlsToggle && toggle) {
561
+ const cb = callbacks.onControlsToggle;
562
+ const id = tl.id;
563
+ const newActive = !tl.active;
564
+ entryG.on('click', (event: Event) => {
565
+ event.stopPropagation();
566
+ cb(id, newActive);
567
+ });
568
+ }
569
+ }
570
+ }
571
+ }