@diagrammo/dgmo 0.8.22 → 0.8.23

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 (53) hide show
  1. package/dist/cli.cjs +111 -109
  2. package/dist/editor.cjs +3 -0
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.js +3 -0
  5. package/dist/editor.js.map +1 -1
  6. package/dist/highlight.cjs +3 -0
  7. package/dist/highlight.cjs.map +1 -1
  8. package/dist/highlight.js +3 -0
  9. package/dist/highlight.js.map +1 -1
  10. package/dist/index.cjs +1010 -215
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +97 -11
  13. package/dist/index.d.ts +97 -11
  14. package/dist/index.js +1001 -213
  15. package/dist/index.js.map +1 -1
  16. package/dist/internal.cjs +380 -0
  17. package/dist/internal.cjs.map +1 -0
  18. package/dist/internal.d.cts +179 -0
  19. package/dist/internal.d.ts +179 -0
  20. package/dist/internal.js +337 -0
  21. package/dist/internal.js.map +1 -0
  22. package/docs/guide/chart-cycle.md +156 -0
  23. package/docs/guide/chart-journey-map.md +179 -0
  24. package/docs/guide/chart-pyramid.md +111 -0
  25. package/docs/guide/registry.json +5 -0
  26. package/docs/language-reference.md +62 -1
  27. package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
  28. package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
  29. package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
  30. package/package.json +11 -1
  31. package/src/cli.ts +5 -35
  32. package/src/completion.ts +9 -44
  33. package/src/cycle/layout.ts +19 -28
  34. package/src/cycle/renderer.ts +59 -32
  35. package/src/cycle/types.ts +21 -0
  36. package/src/d3.ts +21 -1
  37. package/src/dgmo-router.ts +73 -3
  38. package/src/echarts.ts +1 -1
  39. package/src/editor/keywords.ts +3 -0
  40. package/src/index.ts +13 -2
  41. package/src/infra/parser.ts +2 -2
  42. package/src/internal.ts +16 -0
  43. package/src/journey-map/renderer.ts +112 -47
  44. package/src/org/collapse.ts +81 -0
  45. package/src/org/renderer.ts +212 -4
  46. package/src/pyramid/parser.ts +172 -0
  47. package/src/pyramid/renderer.ts +684 -0
  48. package/src/pyramid/types.ts +28 -0
  49. package/src/render.ts +2 -8
  50. package/src/sequence/parser.ts +62 -20
  51. package/src/sequence/renderer.ts +2 -2
  52. package/src/tech-radar/interactive.ts +54 -0
  53. package/src/utils/parsing.ts +1 -0
package/src/render.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  import { renderForExport } from './d3';
2
2
  import { renderExtendedChartForExport } from './echarts';
3
- import {
4
- parseDgmoChartType,
5
- getRenderCategory,
6
- parseDgmo,
7
- } from './dgmo-router';
3
+ import { getRenderCategory, parseDgmo } from './dgmo-router';
8
4
  import type { DgmoError } from './diagnostics';
9
5
  import { getPalette } from './palettes/registry';
10
6
  import type { CompactViewState } from './sharing';
@@ -84,9 +80,7 @@ export async function render(
84
80
  const paletteColors =
85
81
  getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
86
82
 
87
- const { diagnostics } = parseDgmo(content);
88
-
89
- const chartType = parseDgmoChartType(content);
83
+ const { diagnostics, chartType } = parseDgmo(content);
90
84
  const category = chartType ? getRenderCategory(chartType) : null;
91
85
 
92
86
  // Build viewState from legendState (backwards compat) or use provided viewState
@@ -236,6 +236,8 @@ type NoteParseResult =
236
236
  function parseNoteLine(
237
237
  trimmed: string,
238
238
  participants: SequenceParticipant[],
239
+ participantIds: Set<string>,
240
+ sortedParticipantsCache: SequenceParticipant[],
239
241
  lastMsgFrom: string | null
240
242
  ): NoteParseResult {
241
243
  const lower = trimmed.toLowerCase();
@@ -256,7 +258,7 @@ function parseNoteLine(
256
258
  if (!lastMsgFrom) return { kind: 'skip' };
257
259
  participantId = lastMsgFrom;
258
260
  }
259
- if (participants.some((p) => p.id === participantId)) {
261
+ if (participantIds.has(participantId)) {
260
262
  return { kind: 'multi-head', position, participantId };
261
263
  }
262
264
  // Participant not found — fall through to bare-note handler for proper resolution
@@ -284,13 +286,17 @@ function parseNoteLine(
284
286
  if (!afterPos) {
285
287
  // Just `note left` or `note right` — multi-line head
286
288
  if (!lastMsgFrom) return { kind: 'skip' };
287
- if (!participants.some((p) => p.id === lastMsgFrom))
288
- return { kind: 'skip' };
289
+ if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
289
290
  return { kind: 'multi-head', position, participantId: lastMsgFrom };
290
291
  }
291
292
 
292
293
  // Try to match a known participant at the start of afterPos
293
- const resolved = resolveParticipantAndText(afterPos, participants);
294
+ const resolved = resolveParticipantAndText(
295
+ afterPos,
296
+ participants,
297
+ participantIds,
298
+ sortedParticipantsCache
299
+ );
294
300
  if (resolved) {
295
301
  if (resolved.text) {
296
302
  return {
@@ -316,8 +322,7 @@ function parseNoteLine(
316
322
 
317
323
  // Without `of`, treat remaining text as note content on the last-msg sender
318
324
  if (!lastMsgFrom) return { kind: 'skip' };
319
- if (!participants.some((p) => p.id === lastMsgFrom))
320
- return { kind: 'skip' };
325
+ if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
321
326
  return {
322
327
  kind: 'single',
323
328
  position,
@@ -328,8 +333,7 @@ function parseNoteLine(
328
333
 
329
334
  // Plain `note text` — default position, last msg sender
330
335
  if (!lastMsgFrom) return { kind: 'skip' };
331
- if (!participants.some((p) => p.id === lastMsgFrom))
332
- return { kind: 'skip' };
336
+ if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
333
337
  return {
334
338
  kind: 'single',
335
339
  position: 'right',
@@ -348,7 +352,9 @@ function parseNoteLine(
348
352
  */
349
353
  function resolveParticipantAndText(
350
354
  input: string,
351
- participants: SequenceParticipant[]
355
+ participants: SequenceParticipant[],
356
+ participantIds: Set<string>,
357
+ sortedParticipantsCache: SequenceParticipant[]
352
358
  ): { participantId: string; text: string } | null {
353
359
  // Handle quoted participant: `"Auth Service" text`
354
360
  if (input.startsWith('"') || input.startsWith("'")) {
@@ -356,7 +362,7 @@ function resolveParticipantAndText(
356
362
  const endQuote = input.indexOf(quote, 1);
357
363
  if (endQuote > 0) {
358
364
  const name = input.substring(1, endQuote);
359
- if (participants.some((p) => p.id === name)) {
365
+ if (participantIds.has(name)) {
360
366
  const text = input.substring(endQuote + 1).trim();
361
367
  return { participantId: name, text };
362
368
  }
@@ -364,8 +370,8 @@ function resolveParticipantAndText(
364
370
  return null;
365
371
  }
366
372
 
367
- // Sort participants by name length (longest first) for greedy matching
368
- const sorted = [...participants].sort((a, b) => b.id.length - a.id.length);
373
+ // Use pre-sorted participants (longest first) for greedy matching
374
+ const sorted = sortedParticipantsCache;
369
375
  for (const p of sorted) {
370
376
  if (input.startsWith(p.id)) {
371
377
  const remaining = input.substring(p.id.length);
@@ -443,6 +449,25 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
443
449
  // Group parsing state — tracks the active [Group] heading
444
450
  let activeGroup: SequenceGroup | null = null;
445
451
 
452
+ // Fast lookup set for participant existence checks (mirrors result.participants)
453
+ const participantIds = new Set<string>();
454
+
455
+ // Cache sorted participants (longest ID first) for greedy name matching in notes.
456
+ // Invalidated whenever a new participant is added.
457
+ let sortedParticipantsCache: SequenceParticipant[] = [];
458
+ let sortedCacheDirty = true;
459
+
460
+ /** Get sorted participants, rebuilding cache only when dirty. */
461
+ const getSortedParticipants = (): SequenceParticipant[] => {
462
+ if (sortedCacheDirty) {
463
+ sortedParticipantsCache = [...result.participants].sort(
464
+ (a, b) => b.id.length - a.id.length
465
+ );
466
+ sortedCacheDirty = false;
467
+ }
468
+ return sortedParticipantsCache;
469
+ };
470
+
446
471
  // Track participant → group name for duplicate membership detection
447
472
  const participantGroupMap = new Map<string, string>();
448
473
 
@@ -774,7 +799,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
774
799
  const position = posMatch ? parseInt(posMatch[1], 10) : undefined;
775
800
 
776
801
  // Avoid duplicate participant declarations
777
- if (!result.participants.some((p) => p.id === id)) {
802
+ if (!participantIds.has(id)) {
803
+ participantIds.add(id);
804
+ sortedCacheDirty = true;
778
805
  result.participants.push({
779
806
  id,
780
807
  label: alias || id,
@@ -808,7 +835,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
808
835
  const id = posOnlyMatch[1];
809
836
  const position = parseInt(posOnlyMatch[2], 10);
810
837
 
811
- if (!result.participants.some((p) => p.id === id)) {
838
+ if (!participantIds.has(id)) {
839
+ participantIds.add(id);
840
+ sortedCacheDirty = true;
812
841
  result.participants.push({
813
842
  id,
814
843
  label: id,
@@ -846,7 +875,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
846
875
  `'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`
847
876
  );
848
877
  contentStarted = true;
849
- if (!result.participants.some((p) => p.id === id)) {
878
+ if (!participantIds.has(id)) {
879
+ participantIds.add(id);
880
+ sortedCacheDirty = true;
850
881
  result.participants.push({
851
882
  id,
852
883
  label: id,
@@ -882,7 +913,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
882
913
  ) {
883
914
  contentStarted = true;
884
915
  const id = bareCore;
885
- if (!result.participants.some((p) => p.id === id)) {
916
+ if (!participantIds.has(id)) {
917
+ participantIds.add(id);
886
918
  result.participants.push({
887
919
  id,
888
920
  label: id,
@@ -965,7 +997,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
965
997
  currentContainer().push(msg);
966
998
 
967
999
  // Auto-register participants
968
- if (!result.participants.some((p) => p.id === from)) {
1000
+ if (!participantIds.has(from)) {
1001
+ participantIds.add(from);
1002
+ sortedCacheDirty = true;
969
1003
  result.participants.push({
970
1004
  id: from,
971
1005
  label: from,
@@ -973,7 +1007,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
973
1007
  lineNumber,
974
1008
  });
975
1009
  }
976
- if (!result.participants.some((p) => p.id === to)) {
1010
+ if (!participantIds.has(to)) {
1011
+ participantIds.add(to);
1012
+ sortedCacheDirty = true;
977
1013
  result.participants.push({
978
1014
  id: to,
979
1015
  label: to,
@@ -1050,7 +1086,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1050
1086
  result.messages.push(msg);
1051
1087
  currentContainer().push(msg);
1052
1088
 
1053
- if (!result.participants.some((p) => p.id === from)) {
1089
+ if (!participantIds.has(from)) {
1090
+ participantIds.add(from);
1091
+ sortedCacheDirty = true;
1054
1092
  result.participants.push({
1055
1093
  id: from,
1056
1094
  label: from,
@@ -1058,7 +1096,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1058
1096
  lineNumber,
1059
1097
  });
1060
1098
  }
1061
- if (!result.participants.some((p) => p.id === to)) {
1099
+ if (!participantIds.has(to)) {
1100
+ participantIds.add(to);
1101
+ sortedCacheDirty = true;
1062
1102
  result.participants.push({
1063
1103
  id: to,
1064
1104
  label: to,
@@ -1182,6 +1222,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1182
1222
  const noteParsed = parseNoteLine(
1183
1223
  trimmed,
1184
1224
  result.participants,
1225
+ participantIds,
1226
+ getSortedParticipants(),
1185
1227
  lastMsgFrom
1186
1228
  );
1187
1229
  if (noteParsed) {
@@ -2127,7 +2127,7 @@ export function renderSequenceDiagram(
2127
2127
  firstBranchStep = Math.min(firstBranchStep, first);
2128
2128
  }
2129
2129
  if (firstBranchStep < Infinity) {
2130
- const dividerY = stepY(firstBranchStep) - stepSpacing / 2;
2130
+ const dividerY = stepY(firstBranchStep) - BLOCK_HEADER_SPACE;
2131
2131
  deferredLines.push({
2132
2132
  x1: frameX,
2133
2133
  y1: dividerY,
@@ -2156,7 +2156,7 @@ export function renderSequenceDiagram(
2156
2156
  firstElseStep = Math.min(firstElseStep, first);
2157
2157
  }
2158
2158
  if (firstElseStep < Infinity) {
2159
- const dividerY = stepY(firstElseStep) - stepSpacing / 2;
2159
+ const dividerY = stepY(firstElseStep) - BLOCK_HEADER_SPACE;
2160
2160
  deferredLines.push({
2161
2161
  x1: frameX,
2162
2162
  y1: dividerY,
@@ -229,12 +229,46 @@ function renderQuarterCircle(
229
229
  const fillColor =
230
230
  ri % 2 === 0 ? palette.bg : mix(palette.bg, palette.border, 0.15);
231
231
 
232
+ const ringName = parsed.rings[ri].name;
233
+
234
+ // Background ring arc
232
235
  svg
233
236
  .append('path')
234
237
  .attr('d', arcGen(innerR, outerR))
235
238
  .attr('fill', fillColor)
236
239
  .attr('stroke', mutedColor)
237
240
  .attr('stroke-width', 0.5);
241
+
242
+ // Transparent hover overlay for ring interaction
243
+ svg
244
+ .append('path')
245
+ .attr('d', arcGen(innerR, outerR))
246
+ .attr('fill', 'transparent')
247
+ .attr('data-ring-arc', ringName)
248
+ .style('cursor', 'pointer')
249
+ .on('mouseenter', () => {
250
+ // Tint the hovered ring arc
251
+ d3Selection
252
+ .select(rootContainer)
253
+ .selectAll<SVGPathElement, unknown>('[data-ring-arc]')
254
+ .each(function () {
255
+ const el = d3Selection.select(this);
256
+ const isMatch = this.getAttribute('data-ring-arc') === ringName;
257
+ el.attr('fill', isMatch ? qColor : 'transparent').attr(
258
+ 'opacity',
259
+ isMatch ? 0.15 : 1
260
+ );
261
+ });
262
+ dimExceptRing(rootContainer, ringName);
263
+ })
264
+ .on('mouseleave', () => {
265
+ d3Selection
266
+ .select(rootContainer)
267
+ .selectAll<SVGPathElement, unknown>('[data-ring-arc]')
268
+ .attr('fill', 'transparent')
269
+ .attr('opacity', 1);
270
+ clearDim(rootContainer);
271
+ });
238
272
  }
239
273
 
240
274
  // Ring labels removed — the side panel ring headers serve this purpose
@@ -346,10 +380,28 @@ function dimExcept(root: HTMLElement, lineNum: string): void {
346
380
  });
347
381
  }
348
382
 
383
+ function dimExceptRing(root: HTMLElement, ringName: string): void {
384
+ // Dim blips not in the hovered ring (SVG + HTML)
385
+ root.querySelectorAll<HTMLElement>('[data-line-number]').forEach((el) => {
386
+ el.style.opacity =
387
+ el.getAttribute('data-ring') === ringName ? '1' : String(DIM_OPACITY);
388
+ });
389
+ // Dim ring groups not matching
390
+ root.querySelectorAll<HTMLElement>('[data-ring-group]').forEach((el) => {
391
+ el.style.opacity =
392
+ el.getAttribute('data-ring-group') === ringName
393
+ ? '1'
394
+ : String(DIM_OPACITY);
395
+ });
396
+ }
397
+
349
398
  function clearDim(root: HTMLElement): void {
350
399
  root.querySelectorAll<HTMLElement>('[data-line-number]').forEach((el) => {
351
400
  el.style.opacity = '1';
352
401
  });
402
+ root.querySelectorAll<HTMLElement>('[data-ring-group]').forEach((el) => {
403
+ el.style.opacity = '1';
404
+ });
353
405
  }
354
406
 
355
407
  // ============================================================
@@ -383,11 +435,13 @@ function renderHtmlPanel(
383
435
 
384
436
  // Ring group container
385
437
  const ringGroup = document.createElement('div');
438
+ ringGroup.setAttribute('data-ring-group', ringName);
386
439
  ringGroup.style.cssText = `
387
440
  background: ${palette.surface};
388
441
  border-radius: 8px;
389
442
  padding: 10px;
390
443
  margin-bottom: 12px;
444
+ transition: opacity 0.15s;
391
445
  `;
392
446
 
393
447
  // Ring header inside the group
@@ -52,6 +52,7 @@ export const ALL_CHART_TYPES = new Set([
52
52
  'tech-radar',
53
53
  'cycle',
54
54
  'journey-map',
55
+ 'pyramid',
55
56
  ]);
56
57
 
57
58
  /** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */