@diagrammo/dgmo 0.15.0 → 0.16.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 (127) hide show
  1. package/README.md +23 -10
  2. package/dist/advanced.cjs +53094 -0
  3. package/dist/advanced.d.cts +4690 -0
  4. package/dist/advanced.d.ts +4690 -0
  5. package/dist/advanced.js +52849 -0
  6. package/dist/auto.cjs +2298 -2069
  7. package/dist/auto.js +132 -109
  8. package/dist/auto.mjs +2294 -2065
  9. package/dist/cli.cjs +175 -152
  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 +2281 -2048
  15. package/dist/index.d.cts +45 -1
  16. package/dist/index.d.ts +45 -1
  17. package/dist/index.js +2276 -2044
  18. package/dist/internal.cjs +2064 -1831
  19. package/dist/internal.d.cts +113 -113
  20. package/dist/internal.d.ts +113 -113
  21. package/dist/internal.js +2059 -1826
  22. package/dist/pert.cjs +325 -0
  23. package/dist/pert.d.cts +542 -0
  24. package/dist/pert.d.ts +542 -0
  25. package/dist/pert.js +294 -0
  26. package/docs/language-reference.md +83 -66
  27. package/gallery/fixtures/area.dgmo +3 -3
  28. package/gallery/fixtures/bar-stacked.dgmo +5 -5
  29. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  30. package/gallery/fixtures/c4-full.dgmo +8 -8
  31. package/gallery/fixtures/class-full.dgmo +2 -2
  32. package/gallery/fixtures/doughnut.dgmo +6 -6
  33. package/gallery/fixtures/flowchart-colors.dgmo +3 -3
  34. package/gallery/fixtures/function.dgmo +3 -3
  35. package/gallery/fixtures/gantt-full.dgmo +9 -9
  36. package/gallery/fixtures/gantt.dgmo +7 -7
  37. package/gallery/fixtures/infra-full.dgmo +6 -6
  38. package/gallery/fixtures/infra.dgmo +2 -2
  39. package/gallery/fixtures/kanban.dgmo +9 -9
  40. package/gallery/fixtures/line.dgmo +2 -2
  41. package/gallery/fixtures/multi-line.dgmo +3 -3
  42. package/gallery/fixtures/org-full.dgmo +6 -6
  43. package/gallery/fixtures/quadrant.dgmo +2 -2
  44. package/gallery/fixtures/sankey.dgmo +9 -9
  45. package/gallery/fixtures/scatter.dgmo +3 -3
  46. package/gallery/fixtures/sequence-tags-protocols.dgmo +8 -8
  47. package/gallery/fixtures/sequence-tags.dgmo +7 -7
  48. package/gallery/fixtures/sitemap-full.dgmo +7 -7
  49. package/gallery/fixtures/slope.dgmo +5 -5
  50. package/gallery/fixtures/spr-eras.dgmo +9 -9
  51. package/gallery/fixtures/timeline.dgmo +3 -3
  52. package/gallery/fixtures/venn.dgmo +3 -3
  53. package/package.json +28 -3
  54. package/src/advanced.ts +730 -0
  55. package/src/auto/index.ts +14 -13
  56. package/src/boxes-and-lines/layout.ts +481 -445
  57. package/src/boxes-and-lines/renderer.ts +5 -1
  58. package/src/c4/parser.ts +8 -8
  59. package/src/c4/renderer.ts +15 -8
  60. package/src/chart-types.ts +0 -5
  61. package/src/chart.ts +18 -9
  62. package/src/class/parser.ts +8 -15
  63. package/src/class/renderer.ts +17 -6
  64. package/src/cli.ts +15 -13
  65. package/src/completion-types.ts +28 -0
  66. package/src/completion.ts +28 -21
  67. package/src/cycle/layout.ts +2 -2
  68. package/src/cycle/parser.ts +14 -0
  69. package/src/cycle/renderer.ts +6 -3
  70. package/src/d3.ts +1537 -1164
  71. package/src/echarts.ts +37 -20
  72. package/src/editor/dgmo.grammar +1 -3
  73. package/src/editor/dgmo.grammar.js +8 -8
  74. package/src/editor/dgmo.grammar.terms.js +11 -12
  75. package/src/editor/highlight-api.ts +0 -1
  76. package/src/editor/highlight.ts +0 -1
  77. package/src/er/parser.ts +19 -20
  78. package/src/er/renderer.ts +20 -8
  79. package/src/gantt/calculator.ts +1 -11
  80. package/src/gantt/parser.ts +17 -17
  81. package/src/gantt/renderer.ts +9 -6
  82. package/src/graph/flowchart-parser.ts +19 -85
  83. package/src/graph/flowchart-renderer.ts +4 -9
  84. package/src/graph/layout.ts +0 -2
  85. package/src/graph/state-parser.ts +17 -62
  86. package/src/graph/state-renderer.ts +4 -9
  87. package/src/index.ts +17 -1
  88. package/src/infra/parser.ts +40 -30
  89. package/src/infra/renderer.ts +9 -6
  90. package/src/internal.ts +9 -721
  91. package/src/journey-map/parser.ts +10 -3
  92. package/src/journey-map/renderer.ts +3 -1
  93. package/src/kanban/parser.ts +12 -8
  94. package/src/kanban/renderer.ts +3 -1
  95. package/src/mindmap/layout.ts +1 -1
  96. package/src/mindmap/parser.ts +3 -3
  97. package/src/mindmap/renderer.ts +2 -1
  98. package/src/org/parser.ts +3 -3
  99. package/src/org/renderer.ts +5 -4
  100. package/src/pert/layout.ts +1 -1
  101. package/src/pert/monte-carlo.ts +2 -2
  102. package/src/pert/parser.ts +10 -10
  103. package/src/pert/renderer.ts +7 -2
  104. package/src/pert/types.ts +1 -1
  105. package/src/pyramid/parser.ts +12 -0
  106. package/src/raci/parser.ts +44 -14
  107. package/src/raci/renderer.ts +3 -2
  108. package/src/raci/types.ts +4 -3
  109. package/src/ring/parser.ts +12 -0
  110. package/src/sequence/parser.ts +15 -9
  111. package/src/sequence/renderer.ts +2 -5
  112. package/src/sitemap/layout.ts +0 -2
  113. package/src/sitemap/parser.ts +12 -38
  114. package/src/sitemap/renderer.ts +13 -13
  115. package/src/sitemap/types.ts +0 -1
  116. package/src/tech-radar/interactive.ts +1 -1
  117. package/src/tech-radar/renderer.ts +6 -4
  118. package/src/tech-radar/types.ts +2 -0
  119. package/src/utils/arrows.ts +3 -28
  120. package/src/utils/legend-d3.ts +12 -6
  121. package/src/utils/legend-layout.ts +1 -1
  122. package/src/utils/legend-types.ts +1 -1
  123. package/src/utils/parsing.ts +64 -35
  124. package/src/utils/tag-groups.ts +109 -30
  125. package/src/wireframe/layout.ts +11 -7
  126. package/src/wireframe/parser.ts +4 -4
  127. package/src/wireframe/renderer.ts +5 -2
@@ -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
 
@@ -418,25 +498,21 @@ export function injectDefaultTagMetadata(
418
498
  *
419
499
  * 1. Programmatic override (from render API / CLI flag) — highest priority
420
500
  * 2. Diagram-level `active-tag` option (from parsed source)
421
- * 3. No coloring (null) collapsed-by-default
501
+ * 3. Auto-activate first declared tag group
502
+ * 4. No coloring (null)
422
503
  *
423
504
  * The sentinel value `"none"` (case-insensitive) at any level means
424
- * "suppress tag coloring."
505
+ * "suppress tag coloring." Diagrams with tag groups render colored by
506
+ * default across every render path (CLI, export, share-link, app); use
507
+ * `active-tag none` to opt out.
425
508
  *
426
- * Note: there is no auto-activate-first-group fallback. Coloring is
427
- * opt-in: either the source carries `active-tag <name>` or the caller
428
- * (typically the app on user click) supplies a programmatic override.
429
- * Static exports therefore render the legend as a row of collapsed
430
- * pills with nodes uncolored — consistent with the app's pre-click
431
- * default.
432
- *
433
- * @param _tagGroups Declared tag groups (kept for API stability; unused since auto-activation removed)
509
+ * @param tagGroups Declared tag groups (only `.name` is used)
434
510
  * @param explicitActiveTag Value of `active-tag` option from parsed diagram, if any
435
511
  * @param programmaticOverride Value from render API / CLI; `undefined` = not set,
436
512
  * `null` or `''` = explicitly no coloring
437
513
  */
438
514
  export function resolveActiveTagGroup(
439
- _tagGroups: ReadonlyArray<{ name: string }>,
515
+ tagGroups: ReadonlyArray<{ name: string }>,
440
516
  explicitActiveTag: string | undefined,
441
517
  programmaticOverride?: string | null
442
518
  ): string | null {
@@ -453,7 +529,10 @@ export function resolveActiveTagGroup(
453
529
  return explicitActiveTag;
454
530
  }
455
531
 
456
- // 3. No explicit activation → no coloring (collapsed-by-default)
532
+ // 3. Auto-activate first declared group
533
+ if (tagGroups.length > 0) return tagGroups[0].name;
534
+
535
+ // 4. No tag groups → no coloring
457
536
  return null;
458
537
  }
459
538
 
@@ -333,7 +333,8 @@ function layoutElement(
333
333
 
334
334
  // Container — layout children
335
335
  const isInlineRow =
336
- el.metadata._inlineRow === 'true' || el.metadata._labelField === 'true';
336
+ el.metadata['_inlineRow'] === 'true' ||
337
+ el.metadata['_labelField'] === 'true';
337
338
  const padTop = isInlineRow ? 0 : GROUP_PADDING_TOP;
338
339
  const padBottom = isInlineRow ? 0 : GROUP_PADDING_BOTTOM;
339
340
  const padX = isInlineRow ? 0 : GROUP_PADDING_X;
@@ -402,8 +403,8 @@ function allocateEqualWidths(
402
403
  function getElementHeight(el: WireframeElement): number {
403
404
  if (el.type === 'heading') {
404
405
  return el.headingLevel === 2
405
- ? (ELEMENT_HEIGHTS.subheading ?? 36)
406
- : (ELEMENT_HEIGHTS.heading ?? 48);
406
+ ? (ELEMENT_HEIGHTS['subheading'] ?? 36)
407
+ : (ELEMENT_HEIGHTS['heading'] ?? 48);
407
408
  }
408
409
 
409
410
  if (el.type === 'textInput' && el.fieldVariant === 'textarea') {
@@ -421,11 +422,11 @@ function getElementHeight(el: WireframeElement): number {
421
422
  if (el.type === 'image') {
422
423
  if (el.imageHint === 'round') return 80;
423
424
  if (el.imageHint === 'wide') return 80;
424
- return ELEMENT_HEIGHTS.image ?? 120;
425
+ return ELEMENT_HEIGHTS['image'] ?? 120;
425
426
  }
426
427
 
427
428
  // Label-field wrapper
428
- if (el.metadata._labelField === 'true') {
429
+ if (el.metadata['_labelField'] === 'true') {
429
430
  return 36; // input height
430
431
  }
431
432
 
@@ -434,7 +435,7 @@ function getElementHeight(el: WireframeElement): number {
434
435
 
435
436
  function getSpacingAfter(el: WireframeElement): number {
436
437
  if (el.type === 'heading' && el.headingLevel === 2) {
437
- return SPACING_AFTER.subheading ?? 12;
438
+ return SPACING_AFTER['subheading'] ?? 12;
438
439
  }
439
440
  return SPACING_AFTER[el.type] ?? 8;
440
441
  }
@@ -448,7 +449,10 @@ function computeFieldAlignX(children: WireframeElement[]): number {
448
449
  let labelFieldCount = 0;
449
450
 
450
451
  for (const child of children) {
451
- if (child.metadata._labelField === 'true' && child.children.length >= 2) {
452
+ if (
453
+ child.metadata['_labelField'] === 'true' &&
454
+ child.children.length >= 2
455
+ ) {
452
456
  const labelEl = child.children[0];
453
457
  const labelWidth = labelEl.label.length * CHAR_WIDTH;
454
458
  maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
@@ -650,7 +650,7 @@ export function parseWireframe(content: string): ParsedWireframe {
650
650
  wrapper.isContainer = true;
651
651
  wrapper.orientation = 'horizontal';
652
652
  wrapper.children = children;
653
- wrapper.metadata._inlineRow = 'true';
653
+ wrapper.metadata['_inlineRow'] = 'true';
654
654
  pushElement(wrapper);
655
655
  }
656
656
 
@@ -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;
@@ -852,7 +852,7 @@ export function parseWireframe(content: string): ParsedWireframe {
852
852
  wrapper.isContainer = true;
853
853
  wrapper.orientation = 'horizontal';
854
854
  wrapper.children.push(labelEl, fieldEl);
855
- wrapper.metadata._labelField = 'true';
855
+ wrapper.metadata['_labelField'] = 'true';
856
856
  pushElement(wrapper);
857
857
  }
858
858
  } else {
@@ -268,7 +268,10 @@ function renderGroup(
268
268
  const el = node.element;
269
269
 
270
270
  // Inline rows and label-field wrappers — no group chrome, just render children
271
- if (el.metadata._inlineRow === 'true' || el.metadata._labelField === 'true') {
271
+ if (
272
+ el.metadata['_inlineRow'] === 'true' ||
273
+ el.metadata['_labelField'] === 'true'
274
+ ) {
272
275
  for (const child of node.children) {
273
276
  renderNode(g, child, ctx, depth);
274
277
  }
@@ -637,7 +640,7 @@ function renderText(
637
640
  const el = node.element;
638
641
 
639
642
  // Check if this is a label-field wrapper
640
- if (el.metadata._labelField === 'true' && el.children.length >= 2) {
643
+ if (el.metadata['_labelField'] === 'true' && el.children.length >= 2) {
641
644
  for (const child of node.children) {
642
645
  renderNode(g, child, ctx, 0);
643
646
  }