@diagrammo/dgmo 0.8.5 → 0.8.7

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 (65) hide show
  1. package/.claude/commands/dgmo.md +34 -27
  2. package/.cursorrules +20 -2
  3. package/.github/copilot-instructions.md +20 -2
  4. package/.windsurfrules +20 -2
  5. package/AGENTS.md +23 -3
  6. package/README.md +0 -1
  7. package/dist/cli.cjs +189 -190
  8. package/dist/editor.cjs +3 -18
  9. package/dist/editor.cjs.map +1 -1
  10. package/dist/editor.js +3 -18
  11. package/dist/editor.js.map +1 -1
  12. package/dist/highlight.cjs +4 -21
  13. package/dist/highlight.cjs.map +1 -1
  14. package/dist/highlight.js +4 -21
  15. package/dist/highlight.js.map +1 -1
  16. package/dist/index.cjs +2791 -2999
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +56 -56
  19. package/dist/index.d.ts +56 -56
  20. package/dist/index.js +2786 -2992
  21. package/dist/index.js.map +1 -1
  22. package/docs/ai-integration.md +1 -1
  23. package/docs/language-reference.md +112 -121
  24. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  25. package/package.json +1 -1
  26. package/src/boxes-and-lines/collapse.ts +78 -0
  27. package/src/boxes-and-lines/layout.ts +319 -0
  28. package/src/boxes-and-lines/parser.ts +697 -0
  29. package/src/boxes-and-lines/renderer.ts +848 -0
  30. package/src/boxes-and-lines/types.ts +40 -0
  31. package/src/c4/parser.ts +10 -5
  32. package/src/c4/renderer.ts +232 -56
  33. package/src/chart.ts +9 -4
  34. package/src/cli.ts +6 -5
  35. package/src/completion.ts +25 -33
  36. package/src/d3.ts +26 -27
  37. package/src/dgmo-router.ts +3 -7
  38. package/src/echarts.ts +38 -2
  39. package/src/editor/keywords.ts +4 -19
  40. package/src/er/parser.ts +10 -4
  41. package/src/gantt/parser.ts +10 -4
  42. package/src/gantt/renderer.ts +3 -5
  43. package/src/index.ts +17 -26
  44. package/src/infra/parser.ts +10 -5
  45. package/src/infra/renderer.ts +2 -2
  46. package/src/kanban/parser.ts +10 -5
  47. package/src/kanban/renderer.ts +43 -18
  48. package/src/org/parser.ts +7 -4
  49. package/src/org/renderer.ts +40 -29
  50. package/src/sequence/parser.ts +11 -5
  51. package/src/sequence/renderer.ts +114 -45
  52. package/src/sitemap/parser.ts +8 -4
  53. package/src/sitemap/renderer.ts +137 -57
  54. package/src/utils/legend-svg.ts +44 -20
  55. package/src/utils/parsing.ts +1 -1
  56. package/src/utils/tag-groups.ts +59 -15
  57. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  58. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  59. package/gallery/fixtures/initiative-status.dgmo +0 -9
  60. package/src/initiative-status/collapse.ts +0 -76
  61. package/src/initiative-status/filter.ts +0 -63
  62. package/src/initiative-status/layout.ts +0 -650
  63. package/src/initiative-status/parser.ts +0 -629
  64. package/src/initiative-status/renderer.ts +0 -1199
  65. package/src/initiative-status/types.ts +0 -57
@@ -7,7 +7,12 @@ import { FONT_FAMILY } from '../fonts';
7
7
  import type { PaletteColors } from '../palettes';
8
8
  import { mix } from '../palettes/color-utils';
9
9
  import { renderInlineText } from '../utils/inline-markdown';
10
- import type { ParsedKanban, KanbanColumn, KanbanCard, KanbanTagGroup } from './types';
10
+ import type {
11
+ ParsedKanban,
12
+ KanbanColumn,
13
+ KanbanCard,
14
+ KanbanTagGroup,
15
+ } from './types';
11
16
  import { parseKanban } from './parser';
12
17
  import { isArchiveColumn } from './mutations';
13
18
  import {
@@ -141,13 +146,18 @@ function computeLayout(
141
146
  const metaCount = tagMeta.length + card.details.length;
142
147
  const metaHeight =
143
148
  metaCount > 0
144
- ? CARD_SEPARATOR_GAP + 1 + CARD_PADDING_Y + metaCount * CARD_META_LINE_HEIGHT
149
+ ? CARD_SEPARATOR_GAP +
150
+ 1 +
151
+ CARD_PADDING_Y +
152
+ metaCount * CARD_META_LINE_HEIGHT
145
153
  : 0;
146
154
  const cardHeight = CARD_HEADER_HEIGHT + CARD_PADDING_Y + metaHeight;
147
155
 
148
156
  // Account for meta label widths
149
157
  for (const m of tagMeta) {
150
- const metaW = (m.label.length + 2 + m.value.length) * CARD_META_FONT_SIZE * 0.6 + CARD_PADDING_X * 2;
158
+ const metaW =
159
+ (m.label.length + 2 + m.value.length) * CARD_META_FONT_SIZE * 0.6 +
160
+ CARD_PADDING_X * 2;
151
161
  maxCardTextWidth = Math.max(maxCardTextWidth, metaW);
152
162
  }
153
163
 
@@ -162,7 +172,10 @@ function computeLayout(
162
172
  cardY += cardHeight + CARD_GAP;
163
173
  }
164
174
 
165
- const colWidth = Math.max(COLUMN_MIN_WIDTH, maxCardTextWidth + COLUMN_PADDING * 2);
175
+ const colWidth = Math.max(
176
+ COLUMN_MIN_WIDTH,
177
+ maxCardTextWidth + COLUMN_PADDING * 2
178
+ );
166
179
 
167
180
  // Set card widths
168
181
  for (const cl of cardLayouts) {
@@ -269,7 +282,11 @@ export function renderKanban(
269
282
  if (isActive) {
270
283
  capsuleContentWidth += 4; // gap after pill
271
284
  for (const entry of group.entries) {
272
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
285
+ capsuleContentWidth +=
286
+ LEGEND_DOT_R * 2 +
287
+ 4 +
288
+ entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 +
289
+ 8;
273
290
  }
274
291
  }
275
292
  const capsuleWidth = capsuleContentWidth + capsulePad * 2;
@@ -293,10 +310,10 @@ export function renderKanban(
293
310
  legendContainer
294
311
  .append('rect')
295
312
  .attr('x', pillX)
296
- .attr('y', legendY + (isActive ? capsulePad : 0))
313
+ .attr('y', legendY + capsulePad)
297
314
  .attr('width', pillWidth)
298
- .attr('height', LEGEND_HEIGHT - (isActive ? capsulePad * 2 : 0))
299
- .attr('rx', (LEGEND_HEIGHT - (isActive ? capsulePad * 2 : 0)) / 2)
315
+ .attr('height', LEGEND_HEIGHT - capsulePad * 2)
316
+ .attr('rx', (LEGEND_HEIGHT - capsulePad * 2) / 2)
300
317
  .attr('fill', pillBg)
301
318
  .attr('class', 'kanban-legend-group')
302
319
  .attr('data-legend-group', group.name.toLowerCase());
@@ -345,12 +362,16 @@ export function renderKanban(
345
362
  entryG
346
363
  .append('text')
347
364
  .attr('x', entryTextX)
348
- .attr('y', legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
365
+ .attr(
366
+ 'y',
367
+ legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
368
+ )
349
369
  .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
350
370
  .attr('fill', palette.textMuted)
351
371
  .text(entry.value);
352
372
 
353
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
373
+ entryX =
374
+ entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
354
375
  }
355
376
  legendX += capsuleWidth + 12;
356
377
  } else {
@@ -423,10 +444,7 @@ export function renderKanban(
423
444
  .attr('x', colLayout.x + COLUMN_PADDING + nameWidth + 8)
424
445
  .attr(
425
446
  'y',
426
- colLayout.y +
427
- COLUMN_HEADER_HEIGHT / 2 +
428
- WIP_FONT_SIZE / 2 -
429
- 1
447
+ colLayout.y + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
430
448
  )
431
449
  .attr('font-size', WIP_FONT_SIZE)
432
450
  .attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
@@ -437,7 +455,11 @@ export function renderKanban(
437
455
  // Cards
438
456
  for (const cardLayout of colLayout.cardLayouts) {
439
457
  const card = cardLayout.card;
440
- const resolvedColor = resolveCardTagColor(card, parsed.tagGroups, activeTagGroup ?? null);
458
+ const resolvedColor = resolveCardTagColor(
459
+ card,
460
+ parsed.tagGroups,
461
+ activeTagGroup ?? null
462
+ );
441
463
  const tagMeta = resolveCardTagMeta(card, parsed.tagGroups);
442
464
  const hasMeta = tagMeta.length > 0 || card.details.length > 0;
443
465
 
@@ -481,7 +503,8 @@ export function renderKanban(
481
503
  .attr('stroke-width', CARD_STROKE_WIDTH);
482
504
 
483
505
  // Card title (inline markdown)
484
- const titleEl = cg.append('text')
506
+ const titleEl = cg
507
+ .append('text')
485
508
  .attr('x', cx + CARD_PADDING_X)
486
509
  .attr('y', cy + CARD_PADDING_Y + CARD_TITLE_FONT_SIZE)
487
510
  .attr('font-size', CARD_TITLE_FONT_SIZE)
@@ -513,7 +536,8 @@ export function renderKanban(
513
536
  .attr('fill', palette.textMuted)
514
537
  .text(`${meta.label}: `);
515
538
 
516
- const labelWidth = (meta.label.length + 2) * CARD_META_FONT_SIZE * 0.6;
539
+ const labelWidth =
540
+ (meta.label.length + 2) * CARD_META_FONT_SIZE * 0.6;
517
541
  cg.append('text')
518
542
  .attr('x', cx + CARD_PADDING_X + labelWidth)
519
543
  .attr('y', metaY)
@@ -526,7 +550,8 @@ export function renderKanban(
526
550
 
527
551
  // Detail lines (inline markdown)
528
552
  for (const detail of card.details) {
529
- const detailEl = cg.append('text')
553
+ const detailEl = cg
554
+ .append('text')
530
555
  .attr('x', cx + CARD_PADDING_X)
531
556
  .attr('y', metaY)
532
557
  .attr('font-size', CARD_META_FONT_SIZE)
package/src/org/parser.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  isTagBlockHeading,
7
7
  matchTagBlockHeading,
8
8
  validateTagValues,
9
+ stripDefaultModifier,
9
10
  } from '../utils/tag-groups';
10
11
  import {
11
12
  measureIndent,
@@ -220,11 +221,12 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
220
221
  }
221
222
 
222
223
  // Tag group entries (indented Value(color) under tag heading)
223
- // First entry is implicitly the default.
224
+ // First entry is the default unless another is marked `default`
224
225
  if (currentTagGroup && !contentStarted) {
225
226
  const indent = measureIndent(line);
226
227
  if (indent > 0) {
227
- const { label, color } = extractColor(trimmed, palette);
228
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
229
+ const { label, color } = extractColor(cleanEntry, palette);
228
230
  if (!color) {
229
231
  pushError(
230
232
  lineNumber,
@@ -232,8 +234,9 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
232
234
  );
233
235
  continue;
234
236
  }
235
- // First entry is the default
236
- if (currentTagGroup.entries.length === 0) {
237
+ if (isDefault) {
238
+ currentTagGroup.defaultValue = label;
239
+ } else if (currentTagGroup.entries.length === 0) {
237
240
  currentTagGroup.defaultValue = label;
238
241
  }
239
242
  currentTagGroup.entries.push({
@@ -4,7 +4,10 @@
4
4
 
5
5
  import * as d3Selection from 'd3-selection';
6
6
  import { FONT_FAMILY } from '../fonts';
7
- import { runInExportContainer, extractExportSvg } from '../utils/export-container';
7
+ import {
8
+ runInExportContainer,
9
+ extractExportSvg,
10
+ } from '../utils/export-container';
8
11
  import type { PaletteColors } from '../palettes';
9
12
  import { mix } from '../palettes/color-utils';
10
13
  import type { ParsedOrg } from './parser';
@@ -170,7 +173,9 @@ export function renderOrg(
170
173
  if (parsed.title) {
171
174
  const titleParent = fixedTitle ? svg : mainG;
172
175
  const titleX = fixedTitle ? width / 2 : diagramW / 2;
173
- const titleY = fixedTitle ? DIAGRAM_PADDING + TITLE_FONT_SIZE : TITLE_FONT_SIZE;
176
+ const titleY = fixedTitle
177
+ ? DIAGRAM_PADDING + TITLE_FONT_SIZE
178
+ : TITLE_FONT_SIZE;
174
179
  const titleEl = titleParent
175
180
  .append('text')
176
181
  .attr('x', titleX)
@@ -235,10 +240,7 @@ export function renderOrg(
235
240
  cG.attr('data-node-toggle', c.nodeId)
236
241
  .attr('tabindex', '0')
237
242
  .attr('role', 'button')
238
- .attr(
239
- 'aria-expanded',
240
- String(!c.hiddenCount)
241
- )
243
+ .attr('aria-expanded', String(!c.hiddenCount))
242
244
  .attr('aria-label', c.label);
243
245
  }
244
246
 
@@ -266,7 +268,10 @@ export function renderOrg(
266
268
  // Container label (bold, at top)
267
269
  cG.append('text')
268
270
  .attr('x', c.width / 2)
269
- .attr('y', CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2)
271
+ .attr(
272
+ 'y',
273
+ CONTAINER_HEADER_HEIGHT / 2 + CONTAINER_LABEL_FONT_SIZE / 2 - 2
274
+ )
270
275
  .attr('text-anchor', 'middle')
271
276
  .attr('fill', palette.text)
272
277
  .attr('font-size', CONTAINER_LABEL_FONT_SIZE)
@@ -277,7 +282,9 @@ export function renderOrg(
277
282
  const metaEntries = Object.entries(c.metadata);
278
283
  if (metaEntries.length > 0) {
279
284
  // Compute max key width so values align vertically
280
- const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
285
+ const metaDisplayKeys = metaEntries.map(
286
+ ([k]) => displayNames.get(k) ?? k
287
+ );
281
288
  const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
282
289
  const valueX = 10 + (maxKeyLen + 2) * (CONTAINER_META_FONT_SIZE * 0.6);
283
290
 
@@ -306,9 +313,11 @@ export function renderOrg(
306
313
  // Collapsed accent bar (interactive only), clipped to card shape
307
314
  if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
308
315
  const clipId = `clip-${c.nodeId}`;
309
- cG.append('clipPath').attr('id', clipId)
316
+ cG.append('clipPath')
317
+ .attr('id', clipId)
310
318
  .append('rect')
311
- .attr('width', c.width).attr('height', c.height)
319
+ .attr('width', c.width)
320
+ .attr('height', c.height)
312
321
  .attr('rx', CONTAINER_RADIUS);
313
322
  cG.append('rect')
314
323
  .attr('x', COLLAPSE_BAR_INSET)
@@ -319,7 +328,6 @@ export function renderOrg(
319
328
  .attr('clip-path', `url(#${clipId})`)
320
329
  .attr('class', 'org-collapse-bar');
321
330
  }
322
-
323
331
  }
324
332
 
325
333
  // Render edges
@@ -350,10 +358,7 @@ export function renderOrg(
350
358
 
351
359
  const nodeG = contentG
352
360
  .append('g')
353
- .attr(
354
- 'transform',
355
- `translate(${node.x - node.width / 2}, ${node.y})`
356
- )
361
+ .attr('transform', `translate(${node.x - node.width / 2}, ${node.y})`)
357
362
  .attr('class', 'org-node')
358
363
  .attr('data-line-number', String(node.lineNumber)) as GSelection;
359
364
 
@@ -428,7 +433,9 @@ export function renderOrg(
428
433
  .attr('stroke-width', 1);
429
434
 
430
435
  // Metadata rows — compute max key width so values align vertically
431
- const metaDisplayKeys = metaEntries.map(([k]) => displayNames.get(k) ?? k);
436
+ const metaDisplayKeys = metaEntries.map(
437
+ ([k]) => displayNames.get(k) ?? k
438
+ );
432
439
  const maxKeyLen = Math.max(...metaDisplayKeys.map((k) => k.length));
433
440
  const valueX = 10 + (maxKeyLen + 2) * (META_FONT_SIZE * 0.6);
434
441
 
@@ -461,9 +468,12 @@ export function renderOrg(
461
468
  // Collapsed accent bar (interactive only), clipped to card shape
462
469
  if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
463
470
  const clipId = `clip-${node.id}`;
464
- nodeG.append('clipPath').attr('id', clipId)
471
+ nodeG
472
+ .append('clipPath')
473
+ .attr('id', clipId)
465
474
  .append('rect')
466
- .attr('width', node.width).attr('height', node.height)
475
+ .attr('width', node.width)
476
+ .attr('height', node.height)
467
477
  .attr('rx', CARD_RADIUS);
468
478
  nodeG
469
479
  .append('rect')
@@ -475,7 +485,6 @@ export function renderOrg(
475
485
  .attr('clip-path', `url(#${clipId})`)
476
486
  .attr('class', 'org-collapse-bar');
477
487
  }
478
-
479
488
  }
480
489
 
481
490
  // Render legend — kanban-style pills.
@@ -494,7 +503,7 @@ export function renderOrg(
494
503
  let fixedPositions: Map<string, number> | undefined;
495
504
  if (fixedLegend && visibleGroups.length > 0) {
496
505
  fixedPositions = new Map();
497
- const effectiveW = (g: typeof visibleGroups[0]) =>
506
+ const effectiveW = (g: (typeof visibleGroups)[0]) =>
498
507
  activeTagGroup != null ? g.width : g.minifiedWidth;
499
508
  const totalW =
500
509
  visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
@@ -511,10 +520,7 @@ export function renderOrg(
511
520
  ? svg
512
521
  .append('g')
513
522
  .attr('class', 'org-legend-fixed')
514
- .attr(
515
- 'transform',
516
- `translate(0, ${DIAGRAM_PADDING + titleReserve})`
517
- )
523
+ .attr('transform', `translate(0, ${DIAGRAM_PADDING + titleReserve})`)
518
524
  : contentG;
519
525
  const legendParent = legendParentBase;
520
526
  if (fixedLegend && activeTagGroup) {
@@ -556,8 +562,8 @@ export function renderOrg(
556
562
  }
557
563
 
558
564
  const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
559
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
560
- const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
565
+ const pillYOff = LEGEND_CAPSULE_PAD;
566
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
561
567
 
562
568
  // Pill background
563
569
  gEl
@@ -610,7 +616,8 @@ export function renderOrg(
610
616
  .attr('opacity', isHidden ? 0.4 : 0.7);
611
617
 
612
618
  // Transparent hit area for easier clicking
613
- eyeG.append('rect')
619
+ eyeG
620
+ .append('rect')
614
621
  .attr('x', eyeX - hitPad)
615
622
  .attr('y', eyeY - hitPad)
616
623
  .attr('width', LEGEND_EYE_SIZE + hitPad * 2)
@@ -618,7 +625,8 @@ export function renderOrg(
618
625
  .attr('fill', 'transparent')
619
626
  .attr('pointer-events', 'all');
620
627
 
621
- eyeG.append('path')
628
+ eyeG
629
+ .append('path')
622
630
  .attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
623
631
  .attr('transform', `translate(${eyeX}, ${eyeY})`)
624
632
  .attr('fill', 'none')
@@ -655,7 +663,10 @@ export function renderOrg(
655
663
  .attr('fill', palette.textMuted)
656
664
  .text(entryLabel);
657
665
 
658
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
666
+ entryX =
667
+ textX +
668
+ measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) +
669
+ LEGEND_ENTRY_TRAIL;
659
670
  }
660
671
  }
661
672
  }
@@ -15,7 +15,11 @@ import {
15
15
  OPTION_NOCOLON_RE,
16
16
  } from '../utils/parsing';
17
17
  import type { TagGroup } from '../utils/tag-groups';
18
- import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
18
+ import {
19
+ matchTagBlockHeading,
20
+ validateTagValues,
21
+ stripDefaultModifier,
22
+ } from '../utils/tag-groups';
19
23
 
20
24
  /** Known sequence-diagram options that take a value (space-separated). */
21
25
  const KNOWN_SEQ_OPTIONS = new Set(['active-tag']);
@@ -594,9 +598,10 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
594
598
  }
595
599
 
596
600
  // Tag group entries (indented Value(color) under tag heading)
597
- // First entry is automatically the default (no `default` keyword needed)
601
+ // First entry is the default unless another is marked `default`
598
602
  if (currentTagGroup && !contentStarted && measureIndent(raw) > 0) {
599
- const { label, color } = extractColor(trimmed);
603
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
604
+ const { label, color } = extractColor(cleanEntry);
600
605
  if (!color) {
601
606
  pushError(
602
607
  lineNumber,
@@ -604,8 +609,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
604
609
  );
605
610
  continue;
606
611
  }
607
- // First entry is the default
608
- if (currentTagGroup.entries.length === 0) {
612
+ if (isDefault) {
613
+ currentTagGroup.defaultValue = label;
614
+ } else if (currentTagGroup.entries.length === 0) {
609
615
  currentTagGroup.defaultValue = label;
610
616
  }
611
617
  currentTagGroup.entries.push({ value: label, color, lineNumber });