@diagrammo/dgmo 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +9 -9
  2. package/dist/advanced.cjs +612 -734
  3. package/dist/advanced.d.cts +42 -36
  4. package/dist/advanced.d.ts +42 -36
  5. package/dist/advanced.js +612 -733
  6. package/dist/auto.cjs +508 -620
  7. package/dist/auto.js +105 -105
  8. package/dist/auto.mjs +508 -620
  9. package/dist/cli.cjs +144 -144
  10. package/dist/editor.cjs +8 -9
  11. package/dist/editor.js +8 -9
  12. package/dist/highlight.cjs +8 -9
  13. package/dist/highlight.js +8 -9
  14. package/dist/index.cjs +497 -608
  15. package/dist/index.js +497 -608
  16. package/dist/internal.cjs +612 -734
  17. package/dist/internal.d.cts +42 -36
  18. package/dist/internal.d.ts +42 -36
  19. package/dist/internal.js +612 -733
  20. package/dist/pert.d.cts +2 -2
  21. package/dist/pert.d.ts +2 -2
  22. package/docs/language-reference.md +97 -84
  23. package/docs/migration-sequence-color-to-tags.md +1 -1
  24. package/gallery/fixtures/area.dgmo +3 -3
  25. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  26. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  27. package/gallery/fixtures/c4-full.dgmo +8 -8
  28. package/gallery/fixtures/class-full.dgmo +2 -2
  29. package/gallery/fixtures/doughnut.dgmo +6 -6
  30. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  31. package/gallery/fixtures/function.dgmo +3 -3
  32. package/gallery/fixtures/gantt-full.dgmo +9 -9
  33. package/gallery/fixtures/gantt.dgmo +7 -7
  34. package/gallery/fixtures/infra-full.dgmo +6 -6
  35. package/gallery/fixtures/infra.dgmo +2 -2
  36. package/gallery/fixtures/kanban.dgmo +9 -9
  37. package/gallery/fixtures/line.dgmo +2 -2
  38. package/gallery/fixtures/multi-line.dgmo +3 -3
  39. package/gallery/fixtures/org-full.dgmo +6 -6
  40. package/gallery/fixtures/quadrant.dgmo +2 -2
  41. package/gallery/fixtures/sankey.dgmo +9 -9
  42. package/gallery/fixtures/scatter.dgmo +3 -3
  43. package/gallery/fixtures/sequence-tags-protocols.dgmo +11 -11
  44. package/gallery/fixtures/sequence-tags.dgmo +10 -10
  45. package/gallery/fixtures/sequence.dgmo +4 -4
  46. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  47. package/gallery/fixtures/slope.dgmo +5 -5
  48. package/gallery/fixtures/spr-eras.dgmo +9 -9
  49. package/gallery/fixtures/timeline.dgmo +3 -3
  50. package/gallery/fixtures/venn.dgmo +3 -3
  51. package/package.json +7 -3
  52. package/src/advanced.ts +0 -1
  53. package/src/auto/index.ts +2 -2
  54. package/src/boxes-and-lines/layout.ts +1 -2
  55. package/src/boxes-and-lines/renderer.ts +5 -1
  56. package/src/c4/parser.ts +2 -2
  57. package/src/c4/renderer.ts +15 -8
  58. package/src/chart.ts +18 -9
  59. package/src/class/parser.ts +8 -7
  60. package/src/class/renderer.ts +17 -6
  61. package/src/cli.ts +8 -8
  62. package/src/completion.ts +14 -17
  63. package/src/cycle/parser.ts +15 -1
  64. package/src/cycle/renderer.ts +6 -3
  65. package/src/d3.ts +88 -49
  66. package/src/diagnostics.ts +20 -0
  67. package/src/echarts.ts +28 -11
  68. package/src/editor/dgmo.grammar +1 -3
  69. package/src/editor/dgmo.grammar.d.ts +1 -1
  70. package/src/editor/dgmo.grammar.js +8 -8
  71. package/src/editor/dgmo.grammar.terms.js +11 -12
  72. package/src/editor/highlight-api.ts +0 -1
  73. package/src/editor/highlight.ts +0 -1
  74. package/src/er/parser.ts +19 -12
  75. package/src/er/renderer.ts +19 -7
  76. package/src/gantt/parser.ts +1 -1
  77. package/src/gantt/renderer.ts +7 -4
  78. package/src/graph/flowchart-parser.ts +18 -84
  79. package/src/graph/flowchart-renderer.ts +6 -8
  80. package/src/graph/layout.ts +0 -2
  81. package/src/graph/state-parser.ts +17 -62
  82. package/src/graph/state-renderer.ts +3 -8
  83. package/src/infra/parser.ts +21 -11
  84. package/src/infra/renderer.ts +8 -6
  85. package/src/journey-map/parser.ts +11 -4
  86. package/src/journey-map/renderer.ts +3 -1
  87. package/src/kanban/parser.ts +11 -7
  88. package/src/kanban/renderer.ts +3 -1
  89. package/src/mindmap/parser.ts +4 -5
  90. package/src/mindmap/renderer.ts +2 -1
  91. package/src/org/parser.ts +3 -3
  92. package/src/org/renderer.ts +4 -3
  93. package/src/pert/analyzer.ts +10 -10
  94. package/src/pert/layout.ts +1 -1
  95. package/src/pert/parser.ts +8 -8
  96. package/src/pert/renderer.ts +7 -2
  97. package/src/pert/types.ts +1 -1
  98. package/src/pyramid/parser.ts +13 -1
  99. package/src/raci/parser.ts +42 -12
  100. package/src/raci/renderer.ts +2 -1
  101. package/src/raci/types.ts +4 -3
  102. package/src/ring/parser.ts +13 -1
  103. package/src/sequence/parser.ts +81 -23
  104. package/src/sequence/participant-inference.ts +18 -181
  105. package/src/sequence/renderer.ts +48 -137
  106. package/src/sitemap/layout.ts +0 -2
  107. package/src/sitemap/parser.ts +12 -38
  108. package/src/sitemap/renderer.ts +13 -13
  109. package/src/sitemap/types.ts +0 -1
  110. package/src/tech-radar/parser.ts +2 -2
  111. package/src/tech-radar/renderer.ts +5 -3
  112. package/src/tech-radar/types.ts +2 -0
  113. package/src/utils/arrows.ts +3 -28
  114. package/src/utils/extract-alias.ts +1 -1
  115. package/src/utils/inline-markdown.ts +1 -1
  116. package/src/utils/legend-d3.ts +12 -6
  117. package/src/utils/legend-layout.ts +1 -1
  118. package/src/utils/legend-types.ts +1 -1
  119. package/src/utils/parsing.ts +64 -35
  120. package/src/utils/tag-groups.ts +98 -18
  121. package/src/utils/time-ticks.ts +1 -1
  122. package/src/wireframe/parser.ts +3 -3
@@ -235,7 +235,7 @@ export function computeLegendLayout(
235
235
  containerWidth: number
236
236
  ): LegendLayout {
237
237
  const { groups, controls: configControls, mode } = config;
238
- const isExport = mode === 'inline';
238
+ const isExport = mode === 'export';
239
239
 
240
240
  // Filter groups for export: only active group shown
241
241
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
@@ -37,7 +37,7 @@ export interface LegendPosition {
37
37
  titleRelation: 'below-title' | 'inline-with-title';
38
38
  }
39
39
 
40
- export type LegendMode = 'fixed' | 'inline';
40
+ export type LegendMode = 'preview' | 'export';
41
41
 
42
42
  export type LegendControlExportBehavior = 'include' | 'strip' | 'static';
43
43
 
@@ -4,10 +4,18 @@
4
4
  * pipe-metadata parsing.
5
5
  */
6
6
 
7
- import { resolveColor, resolveColorWithDiagnostic } from '../colors';
7
+ import {
8
+ RECOGNIZED_COLOR_NAMES,
9
+ resolveColor,
10
+ resolveColorWithDiagnostic,
11
+ } from '../colors';
8
12
  import type { DgmoError } from '../diagnostics';
9
13
  import type { PaletteColors } from '../palettes';
10
14
 
15
+ const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
16
+ RECOGNIZED_COLOR_NAMES
17
+ );
18
+
11
19
  // ── All known chart types ────────────────────────────────────
12
20
  /** Complete set of recognized chart type identifiers. */
13
21
  export const ALL_CHART_TYPES = new Set([
@@ -87,27 +95,47 @@ export function measureIndent(line: string): number {
87
95
  return indent;
88
96
  }
89
97
 
90
- /** Matches a trailing `(colorName)` suffix on a label. */
91
- export const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
92
-
93
- /** Extract an optional trailing color suffix from a label, resolving via palette. */
98
+ /**
99
+ * Trailing-token color rule (see docs/dgmo-language-spec.md §1.5).
100
+ *
101
+ * Caller contract: `label` must be a pre-split LABEL REGION the parser is
102
+ * responsible for stripping structural terminators (`as <alias>`, `| pipe
103
+ * metadata`, numeric values, date ranges, brackets, arrow constructs) BEFORE
104
+ * invoking this function. The color rule operates only on what remains.
105
+ *
106
+ * Algorithm: split the label on whitespace; if the final token is exactly one
107
+ * of `RECOGNIZED_COLOR_NAMES` (case-sensitive, lowercase only), peel it off
108
+ * as color and return the rest as the label. Otherwise the entire input
109
+ * stays as the label, no color.
110
+ *
111
+ * Case-sensitivity is deliberate: it provides the escape hatch (`Red`,
112
+ * `Yellow`, `Green` stay as labels — useful for traffic-light tag groups).
113
+ *
114
+ * Returns `{ label, color? }` where `color` is the palette-resolved hex string
115
+ * (or undefined if no color word matched).
116
+ */
94
117
  export function extractColor(
95
118
  label: string,
96
119
  palette?: PaletteColors,
97
120
  diagnostics?: DgmoError[],
98
121
  line?: number
99
122
  ): { label: string; color?: string } {
100
- const m = label.match(COLOR_SUFFIX_RE);
101
- if (!m) return { label };
102
- const colorName = m[1].trim();
123
+ const lastSpaceIdx = Math.max(
124
+ label.lastIndexOf(' '),
125
+ label.lastIndexOf('\t')
126
+ );
127
+ if (lastSpaceIdx < 0) return { label };
128
+ const trailing = label.substring(lastSpaceIdx + 1);
129
+ // Case-sensitive lowercase match against the closed 11-name palette.
130
+ if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
103
131
  let color: string | undefined;
104
132
  if (diagnostics && line !== undefined) {
105
- color = resolveColorWithDiagnostic(colorName, line, diagnostics, palette);
133
+ color = resolveColorWithDiagnostic(trailing, line, diagnostics, palette);
106
134
  } else {
107
- color = resolveColor(colorName, palette) ?? undefined;
135
+ color = resolveColor(trailing, palette) ?? undefined;
108
136
  }
109
137
  return {
110
- label: label.substring(0, m.index!).trim(),
138
+ label: label.substring(0, lastSpaceIdx).trimEnd(),
111
139
  color,
112
140
  };
113
141
  }
@@ -457,31 +485,32 @@ export function parseSeriesNames(
457
485
  }
458
486
 
459
487
  /**
460
- * Infer arrow color from label text.
461
- * Returns a named palette color or undefined if no inference applies.
462
- * Case-insensitive, exact match only (not prefix/substring).
488
+ * Peel a trailing recognized color name from a label region, returning the
489
+ * raw color name (not a resolved hex). Used by chart types that pair the
490
+ * universal trailing-token shortcut with their own pipe-metadata `color: …`
491
+ * long form (cycle, pyramid, ring, raci, boxes-and-lines).
492
+ *
493
+ * Caller contract: `label` must already have pipe metadata stripped — this
494
+ * function operates only on the label region.
495
+ *
496
+ * Returns `{ label, colorName? }`. If the trailing token is not a recognized
497
+ * lowercase color, returns the original label and `colorName: undefined`.
463
498
  */
464
- export function inferArrowColor(label: string): string | undefined {
465
- const lower = label.toLowerCase();
466
- // Green: positive/affirmative
467
- if (
468
- lower === 'yes' ||
469
- lower === 'success' ||
470
- lower === 'ok' ||
471
- lower === 'true'
472
- )
473
- return 'green';
474
- // Red: negative/failure
475
- if (
476
- lower === 'no' ||
477
- lower === 'fail' ||
478
- lower === 'error' ||
479
- lower === 'false'
480
- )
481
- return 'red';
482
- // Orange: uncertain/warning
483
- if (lower === 'maybe' || lower === 'warning') return 'orange';
484
- return undefined;
499
+ export function peelTrailingColorName(label: string): {
500
+ label: string;
501
+ colorName?: string;
502
+ } {
503
+ const lastSpaceIdx = Math.max(
504
+ label.lastIndexOf(' '),
505
+ label.lastIndexOf('\t')
506
+ );
507
+ if (lastSpaceIdx < 0) return { label };
508
+ const trailing = label.substring(lastSpaceIdx + 1);
509
+ if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
510
+ return {
511
+ label: label.substring(0, lastSpaceIdx).trimEnd(),
512
+ colorName: trailing,
513
+ };
485
514
  }
486
515
 
487
516
  /** Error message for multiple pipes on a single line. */
@@ -9,8 +9,9 @@ import {
9
9
  tagShorthandRemovedMessage,
10
10
  type DgmoError,
11
11
  } from '../diagnostics';
12
+ import { RECOGNIZED_COLOR_NAMES } from '../colors';
12
13
 
13
- /** A single entry inside a tag group: `Value(color)` */
14
+ /** A single entry inside a tag group: `Value color` */
14
15
  export interface TagEntry {
15
16
  value: string;
16
17
  color: string;
@@ -32,7 +33,7 @@ interface TagBlockMatch {
32
33
  name: string;
33
34
  alias: string | undefined;
34
35
  colorHint: string | undefined;
35
- /** Inline tag values parsed from single-line form (e.g., `tag Priority as p High(red), Low(blue)`) */
36
+ /** Inline tag values parsed from single-line form (e.g., `tag Priority as p High red, Low blue`) */
36
37
  inlineValues?: string[];
37
38
  /**
38
39
  * If the heading used the legacy `tag Name <alias>` (bare shorthand)
@@ -51,8 +52,8 @@ interface TagBlockMatch {
51
52
  * Returns the cleaned text and whether the keyword was present.
52
53
  *
53
54
  * Examples:
54
- * "NA(gray) default" → { text: "NA(gray)", isDefault: true }
55
- * "Done(green)" → { text: "Done(green)", isDefault: false }
55
+ * "NA gray default" → { text: "NA gray", isDefault: true }
56
+ * "Done green" → { text: "Done green", isDefault: false }
56
57
  */
57
58
  export function stripDefaultModifier(text: string): {
58
59
  text: string;
@@ -90,7 +91,7 @@ export function isTagBlockHeading(trimmed: string): boolean {
90
91
  /**
91
92
  * Parse a tag declaration line: `tag Name [as <alias>] [Values...]`
92
93
  *
93
- * Canonical form (post-TD-18): `tag Priority as p High(red), Low(blue)`.
94
+ * Canonical form: `tag Priority as p High red, Low blue` (universal §1.5).
94
95
  *
95
96
  * Legacy forms still parse for graceful degradation but set
96
97
  * `legacyForm` on the result so the caller can emit
@@ -117,12 +118,26 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
117
118
  let restStartIdx = 1;
118
119
 
119
120
  // Locate any keyword separator (`as` or legacy `alias`) that appears
120
- // BEFORE the first inline-value token. Inline values are tokens that
121
- // contain `(` (color suffix) or follow a comma.
121
+ // BEFORE the first inline-value token. Inline values are recognized by
122
+ // a comma in the line: scan tokens for one. Under §1.5 trailing-token
123
+ // syntax there's no `(color)` marker anymore — a comma anywhere after
124
+ // the name span signals that inline values follow.
122
125
  let valueStart = tokens.length;
123
126
  for (let i = 1; i < tokens.length; i++) {
124
- if (tokens[i].includes('(')) {
127
+ if (tokens[i].includes(',')) {
128
+ // valueStart is the FIRST token of the first inline value, which is
129
+ // the token immediately following the alias / keyword span. Walk
130
+ // back to the start of the value span by finding the most recent
131
+ // word boundary — but for the simple heuristic here, the inline
132
+ // value list starts at the previous non-keyword token.
125
133
  valueStart = i;
134
+ // The token containing the comma might be `High` (in `High red,`)
135
+ // or `red,` (in `High red,` if tokenized differently). Treat the
136
+ // value span as starting at the token BEFORE the first comma
137
+ // unless that token is the alias / keyword.
138
+ // Simpler: use this index as a coarse upper bound. The keyword
139
+ // search below uses [1, valueStart) — anything past `as`/`alias`
140
+ // belongs to the value span.
126
141
  break;
127
142
  }
128
143
  }
@@ -170,16 +185,77 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
170
185
  } else {
171
186
  // No `as`/`alias` keyword — try legacy bare-shorthand. The trailing
172
187
  // token of the name span (just before inline values) is the alias
173
- // candidate; if it passes the universal alias regex, accept it and
174
- // mark legacyForm='bare-shorthand'.
175
- if (tokens[0][0] === '"' || tokens[0][0] === "'") {
188
+ // candidate; if it passes the universal alias regex AND is NOT a
189
+ // recognized palette color (§1.5 escape hatch), accept it.
190
+ //
191
+ // When inline values are present (valueStart < tokens.length), the
192
+ // tokens immediately before the first value-segment-with-color form
193
+ // the (name + alias) prefix. The first value contains at least the
194
+ // value name + trailing color, so we walk back to find where it
195
+ // starts: skip the trailing color token, then 1+ name tokens.
196
+ const isColorWord = (s: string): boolean =>
197
+ (RECOGNIZED_COLOR_NAMES as readonly string[]).includes(s);
198
+
199
+ if (valueStart < tokens.length) {
200
+ // Inline values are present (we found a comma at valueStart).
201
+ // The first value's last token is at index commaIdx; strip the
202
+ // comma to inspect. Walk back to determine the value name length.
203
+ const commaTokenIdx = valueStart;
204
+ // Find where the first value starts: the value contains at least
205
+ // 1 word + optional trailing color. Walk back from commaTokenIdx
206
+ // while the previous tokens look like value-name words (i.e. not
207
+ // a recognized alias-shaped lowercase short token that is followed
208
+ // by a value start).
209
+ // Simpler heuristic: pre-comma trailing color is the last token if
210
+ // it's a recognized color (after stripping comma). The value's
211
+ // name is the token immediately before that. So the value spans
212
+ // (firstValueStart..=commaTokenIdx). The "name + alias" prefix
213
+ // is [0, firstValueStart).
214
+ const lastBeforeComma = tokens[commaTokenIdx].replace(/,$/, '');
215
+ // value = `<name word(s)> <color>` if trailing token is a recognized
216
+ // palette word; otherwise value = `<name word(s)>` (no color).
217
+ const firstValueStart = isColorWord(lastBeforeComma)
218
+ ? commaTokenIdx - 1
219
+ : commaTokenIdx;
220
+ // Now firstValueStart points at the first token of value #1.
221
+ // [0, firstValueStart) is the `<name + optional alias>` prefix.
222
+ const prefixEnd = firstValueStart;
223
+ const aliasCandidate = prefixEnd > 1 ? tokens[prefixEnd - 1] : undefined;
224
+ if (
225
+ aliasCandidate &&
226
+ isAliasToken(aliasCandidate) &&
227
+ !isColorWord(aliasCandidate)
228
+ ) {
229
+ alias = aliasCandidate;
230
+ legacyForm = 'bare-shorthand';
231
+ name = tokens
232
+ .slice(0, prefixEnd - 1)
233
+ .map((t) => stripQuotes(t))
234
+ .join(' ');
235
+ restStartIdx = prefixEnd;
236
+ } else {
237
+ name = tokens
238
+ .slice(0, prefixEnd)
239
+ .map((t) => stripQuotes(t))
240
+ .join(' ');
241
+ restStartIdx = prefixEnd;
242
+ }
243
+ } else if (tokens[0][0] === '"' || tokens[0][0] === "'") {
176
244
  // Quoted name. Check the next token for legacy bare alias.
177
- if (tokens.length > 1 && isAliasToken(tokens[1]) && valueStart > 1) {
245
+ if (
246
+ tokens.length > 1 &&
247
+ isAliasToken(tokens[1]) &&
248
+ !isColorWord(tokens[1])
249
+ ) {
178
250
  alias = tokens[1];
179
251
  legacyForm = 'bare-shorthand';
180
252
  restStartIdx = 2;
181
253
  }
182
- } else if (valueStart > 1 && isAliasToken(tokens[valueStart - 1])) {
254
+ } else if (
255
+ valueStart > 1 &&
256
+ isAliasToken(tokens[valueStart - 1]) &&
257
+ !isColorWord(tokens[valueStart - 1])
258
+ ) {
183
259
  // Bare shorthand at the end of the name span.
184
260
  alias = tokens[valueStart - 1];
185
261
  legacyForm = 'bare-shorthand';
@@ -207,12 +283,16 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
207
283
  .filter(Boolean);
208
284
  }
209
285
 
210
- // Trailing `(color)` on the name itself (no inline values).
286
+ // Trailing recognized-color token on the name itself (no inline values).
287
+ // Per §1.5 universal trailing-token: case-sensitive lowercase match.
211
288
  if (!inlineValues || inlineValues.length === 0) {
212
- const colorMatch = name.match(/\(([^)]+)\)\s*$/);
213
- if (colorMatch) {
214
- colorHint = colorMatch[1];
215
- name = name.substring(0, colorMatch.index!).trim();
289
+ const lastSpaceIdx = name.lastIndexOf(' ');
290
+ if (lastSpaceIdx > 0) {
291
+ const trailing = name.substring(lastSpaceIdx + 1);
292
+ if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(trailing)) {
293
+ colorHint = trailing;
294
+ name = name.substring(0, lastSpaceIdx).trimEnd();
295
+ }
216
296
  }
217
297
  }
218
298
 
@@ -2,7 +2,7 @@
2
2
  // Time axis tick computation — shared by d3.ts and gantt/renderer.ts
3
3
  // ============================================================
4
4
 
5
- import * as d3Scale from 'd3-scale';
5
+ import type * as d3Scale from 'd3-scale';
6
6
 
7
7
  export const MONTH_ABBR = [
8
8
  'Jan',
@@ -668,7 +668,7 @@ export function parseWireframe(content: string): ParsedWireframe {
668
668
  if (phase === 'header') {
669
669
  // First line: chart type declaration
670
670
  const firstLineResult = parseFirstLine(trimmed);
671
- if (firstLineResult && firstLineResult.chartType === 'wireframe') {
671
+ if (firstLineResult?.chartType === 'wireframe') {
672
672
  title = firstLineResult.title || null;
673
673
  titleLineNumber = lineNumber;
674
674
  continue;
@@ -718,7 +718,7 @@ export function parseWireframe(content: string): ParsedWireframe {
718
718
  }
719
719
  }
720
720
 
721
- // Indented tag entry: `Value(color)` or `Value(color) default`
721
+ // Indented tag entry: `Value color` or `Value color default`
722
722
  if (indent > 0 && currentTagGroup) {
723
723
  const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
724
724
  const { label, color } = extractColor(cleanEntry);
@@ -736,7 +736,7 @@ export function parseWireframe(content: string): ParsedWireframe {
736
736
  } else {
737
737
  pushWarning(
738
738
  lineNumber,
739
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
739
+ `Expected 'Value color' in tag group '${currentTagGroup.name}'`
740
740
  );
741
741
  }
742
742
  continue;