@diagrammo/dgmo 0.8.21 → 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 (114) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +145 -93
  4. package/dist/editor.cjs +20 -3
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +20 -3
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +15 -2
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +15 -2
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +20843 -14937
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +426 -17
  15. package/dist/index.d.ts +426 -17
  16. package/dist/index.js +20795 -14912
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal.cjs +380 -0
  19. package/dist/internal.cjs.map +1 -0
  20. package/dist/internal.d.cts +179 -0
  21. package/dist/internal.d.ts +179 -0
  22. package/dist/internal.js +337 -0
  23. package/dist/internal.js.map +1 -0
  24. package/docs/guide/chart-cycle.md +156 -0
  25. package/docs/guide/chart-journey-map.md +179 -0
  26. package/docs/guide/chart-pyramid.md +111 -0
  27. package/docs/guide/chart-sitemap.md +18 -1
  28. package/docs/guide/chart-tech-radar.md +219 -0
  29. package/docs/guide/registry.json +6 -0
  30. package/docs/language-reference.md +177 -6
  31. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  32. package/gallery/fixtures/c4-full.dgmo +2 -2
  33. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  34. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  35. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  36. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  37. package/gallery/fixtures/gantt-full.dgmo +2 -2
  38. package/gallery/fixtures/gantt.dgmo +2 -2
  39. package/gallery/fixtures/infra-full.dgmo +2 -2
  40. package/gallery/fixtures/infra.dgmo +1 -1
  41. package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
  42. package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
  43. package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
  44. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  45. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  46. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  47. package/gallery/fixtures/tech-radar.dgmo +36 -0
  48. package/gallery/fixtures/timeline.dgmo +1 -1
  49. package/package.json +11 -1
  50. package/src/boxes-and-lines/layout.ts +309 -33
  51. package/src/boxes-and-lines/parser.ts +86 -10
  52. package/src/boxes-and-lines/renderer.ts +250 -91
  53. package/src/boxes-and-lines/types.ts +1 -1
  54. package/src/c4/layout.ts +8 -8
  55. package/src/c4/parser.ts +35 -2
  56. package/src/c4/renderer.ts +19 -3
  57. package/src/c4/types.ts +1 -0
  58. package/src/chart.ts +14 -7
  59. package/src/cli.ts +5 -35
  60. package/src/completion.ts +233 -41
  61. package/src/cycle/layout.ts +723 -0
  62. package/src/cycle/parser.ts +352 -0
  63. package/src/cycle/renderer.ts +566 -0
  64. package/src/cycle/types.ts +98 -0
  65. package/src/d3.ts +107 -8
  66. package/src/dgmo-router.ts +82 -3
  67. package/src/echarts.ts +8 -5
  68. package/src/editor/dgmo.grammar +5 -1
  69. package/src/editor/dgmo.grammar.js +1 -1
  70. package/src/editor/keywords.ts +17 -0
  71. package/src/gantt/parser.ts +2 -8
  72. package/src/graph/flowchart-parser.ts +15 -21
  73. package/src/graph/state-parser.ts +5 -10
  74. package/src/index.ts +63 -2
  75. package/src/infra/layout.ts +218 -74
  76. package/src/infra/parser.ts +32 -8
  77. package/src/infra/renderer.ts +14 -8
  78. package/src/infra/types.ts +10 -3
  79. package/src/internal.ts +16 -0
  80. package/src/journey-map/layout.ts +386 -0
  81. package/src/journey-map/parser.ts +540 -0
  82. package/src/journey-map/renderer.ts +1521 -0
  83. package/src/journey-map/types.ts +47 -0
  84. package/src/kanban/parser.ts +3 -10
  85. package/src/kanban/renderer.ts +31 -15
  86. package/src/mindmap/parser.ts +12 -18
  87. package/src/mindmap/renderer.ts +14 -13
  88. package/src/mindmap/text-wrap.ts +22 -12
  89. package/src/mindmap/types.ts +2 -2
  90. package/src/org/collapse.ts +81 -0
  91. package/src/org/parser.ts +2 -6
  92. package/src/org/renderer.ts +212 -4
  93. package/src/pyramid/parser.ts +172 -0
  94. package/src/pyramid/renderer.ts +684 -0
  95. package/src/pyramid/types.ts +28 -0
  96. package/src/render.ts +2 -8
  97. package/src/sequence/parser.ts +62 -20
  98. package/src/sequence/renderer.ts +146 -40
  99. package/src/sharing.ts +1 -0
  100. package/src/sitemap/layout.ts +21 -6
  101. package/src/sitemap/parser.ts +26 -17
  102. package/src/sitemap/renderer.ts +34 -0
  103. package/src/sitemap/types.ts +1 -0
  104. package/src/tech-radar/index.ts +14 -0
  105. package/src/tech-radar/interactive.ts +1112 -0
  106. package/src/tech-radar/layout.ts +190 -0
  107. package/src/tech-radar/parser.ts +385 -0
  108. package/src/tech-radar/renderer.ts +1159 -0
  109. package/src/tech-radar/shared.ts +187 -0
  110. package/src/tech-radar/types.ts +81 -0
  111. package/src/utils/description-helpers.ts +33 -0
  112. package/src/utils/legend-layout.ts +3 -1
  113. package/src/utils/parsing.ts +47 -7
  114. package/src/utils/tag-groups.ts +46 -60
package/src/c4/types.ts CHANGED
@@ -53,6 +53,7 @@ export interface C4Element {
53
53
  type: C4ElementType;
54
54
  shape: C4Shape;
55
55
  metadata: Record<string, string>;
56
+ description?: string[];
56
57
  children: C4Element[];
57
58
  groups: C4Group[];
58
59
  relationships: C4Relationship[];
package/src/chart.ts CHANGED
@@ -64,6 +64,7 @@ import type { PaletteColors } from './palettes';
64
64
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
65
65
  import {
66
66
  extractColor,
67
+ normalizeNumericToken,
67
68
  parseFirstLine,
68
69
  parseSeriesNames,
69
70
  } from './utils/parsing';
@@ -528,7 +529,8 @@ export function parseDataRowValues(
528
529
  // Find how many trailing comma-separated parts are numeric
529
530
  let numericCount = 0;
530
531
  for (let j = commaParts.length - 1; j >= 0; j--) {
531
- const part = commaParts[j].trim();
532
+ const part =
533
+ normalizeNumericToken(commaParts[j].trim()) ?? commaParts[j].trim();
532
534
  if (part && !isNaN(parseFloat(part)) && isFinite(Number(part))) {
533
535
  numericCount++;
534
536
  } else {
@@ -545,7 +547,9 @@ export function parseDataRowValues(
545
547
  // Split firstPart from the right: last space-separated token must be numeric
546
548
  const lastSpaceIdx = firstPart.lastIndexOf(' ');
547
549
  if (lastSpaceIdx >= 0) {
548
- const possibleFirstVal = firstPart.substring(lastSpaceIdx + 1).trim();
550
+ const rawFirstVal = firstPart.substring(lastSpaceIdx + 1).trim();
551
+ const possibleFirstVal =
552
+ normalizeNumericToken(rawFirstVal) ?? rawFirstVal;
549
553
  if (
550
554
  possibleFirstVal &&
551
555
  !isNaN(parseFloat(possibleFirstVal)) &&
@@ -555,7 +559,8 @@ export function parseDataRowValues(
555
559
  if (label) {
556
560
  const values = [parseFloat(possibleFirstVal)];
557
561
  for (const p of extraValueParts) {
558
- values.push(parseFloat(p.trim()));
562
+ const normP = normalizeNumericToken(p.trim()) ?? p.trim();
563
+ values.push(parseFloat(normP));
559
564
  }
560
565
  return { label, values };
561
566
  }
@@ -577,8 +582,9 @@ export function parseDataRowValues(
577
582
  let idx = tokens.length - 1;
578
583
  while (idx >= 1 && values.length < limit) {
579
584
  const tok = tokens[idx];
580
- const num = parseFloat(tok);
581
- if (isNaN(num) || !isFinite(Number(tok))) break;
585
+ const normTok = normalizeNumericToken(tok) ?? tok;
586
+ const num = parseFloat(normTok);
587
+ if (isNaN(num) || !isFinite(Number(normTok))) break;
582
588
  values.unshift(num);
583
589
  idx--;
584
590
  }
@@ -590,8 +596,9 @@ export function parseDataRowValues(
590
596
 
591
597
  // Single-value mode: only the last space-separated token
592
598
  const lastToken = tokens[tokens.length - 1];
593
- const num = parseFloat(lastToken);
594
- if (isNaN(num) || !isFinite(Number(lastToken))) return null;
599
+ const normalizedLast = normalizeNumericToken(lastToken) ?? lastToken;
600
+ const num = parseFloat(normalizedLast);
601
+ if (isNaN(num) || !isFinite(Number(normalizedLast))) return null;
595
602
 
596
603
  const label = tokens.slice(0, -1).join(' ');
597
604
  if (!label) return null;
package/src/cli.ts CHANGED
@@ -6,7 +6,11 @@ import { resolve, join, basename, extname } from 'node:path';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { Resvg } from '@resvg/resvg-js';
8
8
  import { render } from './render';
9
- import { parseDgmo, getAllChartTypes } from './dgmo-router';
9
+ import {
10
+ parseDgmo,
11
+ getAllChartTypes,
12
+ CHART_TYPE_DESCRIPTIONS,
13
+ } from './dgmo-router';
10
14
  import { parseDgmoChartType } from './dgmo-router';
11
15
  import { formatDgmoError } from './diagnostics';
12
16
  import { getPalette, getAvailablePalettes } from './palettes';
@@ -19,40 +23,6 @@ const PALETTES = getAvailablePalettes().map((p) => p.id);
19
23
 
20
24
  const THEMES = ['light', 'dark', 'transparent'] as const;
21
25
 
22
- const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
23
- bar: 'Bar chart — categorical comparisons',
24
- line: 'Line chart — trends over time',
25
- 'multi-line': 'Multi-line chart — multiple series trends',
26
- area: 'Area chart — filled line chart',
27
- pie: 'Pie chart — part-to-whole proportions',
28
- doughnut: 'Doughnut chart — ring-style pie chart',
29
- radar: 'Radar chart — multi-dimensional metrics',
30
- 'polar-area': 'Polar area chart — radial bar chart',
31
- 'bar-stacked': 'Stacked bar chart — multi-series categorical',
32
- scatter: 'Scatter plot — 2D data points or bubble chart',
33
- sankey: 'Sankey diagram — flow/allocation visualization',
34
- chord: 'Chord diagram — circular flow relationships',
35
- function: 'Function plot — mathematical expressions',
36
- heatmap: 'Heatmap — matrix intensity visualization',
37
- funnel: 'Funnel chart — conversion pipeline',
38
- slope: 'Slope chart — change between two periods',
39
- wordcloud: 'Word cloud — term frequency visualization',
40
- arc: 'Arc diagram — network relationships',
41
- timeline: 'Timeline — events, eras, and date ranges',
42
- venn: 'Venn diagram — set overlaps',
43
- quadrant: 'Quadrant chart — 2x2 positioning matrix',
44
- sequence: 'Sequence diagram — message/interaction flows',
45
- flowchart: 'Flowchart — decision trees and process flows',
46
- class: 'Class diagram — UML class hierarchies',
47
- er: 'ER diagram — database schemas and relationships',
48
- org: 'Org chart — hierarchical tree structures',
49
- kanban: 'Kanban board — task/workflow columns',
50
- c4: 'C4 diagram — system architecture (context, container, component, deployment)',
51
- infra: 'Infra chart — infrastructure traffic flow with rps computation',
52
- 'boxes-and-lines':
53
- 'Boxes and lines — general-purpose node-edge diagrams with groups and tags',
54
- };
55
-
56
26
  const CLAUDE_SKILL_CONTENT = `# dgmo — Diagrammo Diagram Assistant
57
27
 
58
28
  You are helping the user author, render, and share diagrams using the \`dgmo\` CLI and \`.dgmo\` file format.
package/src/completion.ts CHANGED
@@ -16,6 +16,7 @@ import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-par
16
16
  import { extractSymbols as extractInfraSymbols } from './infra/parser';
17
17
  import { extractSymbols as extractClassSymbols } from './class/parser';
18
18
  import { parseFirstLine, ALL_CHART_TYPES } from './utils/parsing';
19
+ import { CHART_TYPE_DESCRIPTIONS } from './dgmo-router';
19
20
 
20
21
  // ============================================================
21
22
  // Symbol extraction
@@ -349,53 +350,52 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
349
350
  'active-tag': { description: 'Active tag group name' },
350
351
  }),
351
352
  ],
353
+ [
354
+ 'tech-radar',
355
+ withGlobals({
356
+ rings: { description: 'Ring names block (innermost to outermost)' },
357
+ quadrant: {
358
+ description:
359
+ 'Quadrant position (top-left, top-right, bottom-left, bottom-right)',
360
+ },
361
+ ring: { description: 'Ring assignment for a blip' },
362
+ trend: { description: 'Blip trend (new, up, down, stable)' },
363
+ color: { description: 'Override quadrant color' },
364
+ }),
365
+ ],
366
+ [
367
+ 'cycle',
368
+ withGlobals({
369
+ 'direction-counterclockwise': {
370
+ description: 'Reverse cycle direction to counterclockwise',
371
+ },
372
+ 'hide-descriptions': { description: 'Hide node and edge descriptions' },
373
+ 'circle-nodes': {
374
+ description: 'Render nodes as circles instead of rectangles',
375
+ },
376
+ }),
377
+ ],
378
+ [
379
+ 'journey-map',
380
+ withGlobals({
381
+ 'no-legend': { description: 'Hide the score legend' },
382
+ persona: { description: 'Define the journey persona' },
383
+ }),
384
+ ],
385
+ [
386
+ 'pyramid',
387
+ withGlobals({
388
+ inverted: { description: 'Flip apex to the bottom (funnel orientation)' },
389
+ color: { description: 'Override layer color (pipe metadata)' },
390
+ description: { description: 'Layer description (pipe or indented body)' },
391
+ }),
392
+ ],
352
393
  ]);
353
394
 
354
395
  // ============================================================
355
396
  // Chart types array (for chart type completion popup)
356
397
  // ============================================================
357
398
 
358
- const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
359
- // Data charts
360
- bar: 'Bar chart',
361
- line: 'Line chart',
362
- pie: 'Pie chart',
363
- doughnut: 'Doughnut chart',
364
- area: 'Area chart',
365
- 'polar-area': 'Polar area chart',
366
- radar: 'Radar chart',
367
- 'bar-stacked': 'Stacked bar chart',
368
- // Extended charts
369
- scatter: 'Scatter plot',
370
- heatmap: 'Heatmap',
371
- sankey: 'Sankey flow diagram',
372
- chord: 'Chord diagram',
373
- funnel: 'Funnel chart',
374
- function: 'Mathematical function plot',
375
- // Visualizations
376
- slope: 'Slope chart',
377
- wordcloud: 'Word cloud',
378
- arc: 'Arc diagram',
379
- timeline: 'Timeline',
380
- venn: 'Venn diagram',
381
- quadrant: 'Quadrant chart',
382
- // Diagrams
383
- sequence: 'Sequence diagram',
384
- flowchart: 'Flowchart',
385
- class: 'Class diagram',
386
- er: 'Entity-relationship diagram',
387
- org: 'Organization chart',
388
- kanban: 'Kanban board',
389
- c4: 'C4 architecture diagram',
390
- state: 'State diagram',
391
- sitemap: 'Sitemap diagram',
392
- infra: 'Infrastructure diagram',
393
- gantt: 'Gantt chart',
394
- 'boxes-and-lines': 'Boxes and lines diagram',
395
- mindmap: 'Mindmap diagram',
396
- wireframe: 'UI wireframe diagram',
397
- };
398
-
399
399
  /** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
400
400
  export const CHART_TYPES: ReadonlyArray<{ name: string; description: string }> =
401
401
  [...ALL_CHART_TYPES]
@@ -537,6 +537,38 @@ export const PIPE_METADATA = new Map<
537
537
  edge: {},
538
538
  },
539
539
  ],
540
+ [
541
+ 'tech-radar',
542
+ {
543
+ node: {
544
+ quadrant: {
545
+ description: 'Quadrant position',
546
+ values: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
547
+ },
548
+ ring: { description: 'Ring assignment for blip' },
549
+ trend: {
550
+ description: 'Blip trend indicator',
551
+ values: ['new', 'up', 'down', 'stable'],
552
+ },
553
+ color: { description: 'Override quadrant color' },
554
+ },
555
+ edge: {},
556
+ },
557
+ ],
558
+ [
559
+ 'cycle',
560
+ {
561
+ node: {
562
+ color: { description: 'Node fill color (palette name)' },
563
+ span: { description: 'Relative arc distance to next node' },
564
+ description: { description: 'Node description text' },
565
+ },
566
+ edge: {
567
+ color: { description: 'Edge stroke color (palette name)' },
568
+ width: { description: 'Edge stroke width in pixels' },
569
+ },
570
+ },
571
+ ],
540
572
  ]);
541
573
 
542
574
  // ============================================================
@@ -1035,3 +1067,163 @@ registerExtractor('sitemap', extractSitemapSymbols);
1035
1067
  registerExtractor('c4', extractC4Symbols);
1036
1068
  registerExtractor('gantt', extractGanttSymbols);
1037
1069
  registerExtractor('boxes-and-lines', extractBoxesAndLinesSymbols);
1070
+ registerExtractor('tech-radar', extractTechRadarSymbols);
1071
+ registerExtractor('cycle', extractCycleSymbols);
1072
+ registerExtractor('journey-map', extractJourneyMapSymbols);
1073
+
1074
+ function extractTechRadarSymbols(docText: string): DiagramSymbols {
1075
+ const entities: string[] = [];
1076
+ const keywords: string[] = [
1077
+ 'rings',
1078
+ 'quadrant',
1079
+ 'ring',
1080
+ 'trend',
1081
+ 'new',
1082
+ 'up',
1083
+ 'down',
1084
+ 'stable',
1085
+ 'top-left',
1086
+ 'top-right',
1087
+ 'bottom-left',
1088
+ 'bottom-right',
1089
+ 'alias',
1090
+ 'aka',
1091
+ 'color',
1092
+ ];
1093
+
1094
+ // Extract ring names and aliases from the rings block
1095
+ const lines = docText.split('\n');
1096
+ let inRings = false;
1097
+ for (const line of lines) {
1098
+ const trimmed = line.trim();
1099
+ if (trimmed.toLowerCase() === 'rings') {
1100
+ inRings = true;
1101
+ continue;
1102
+ }
1103
+ if (inRings) {
1104
+ if (!trimmed || (line[0] !== ' ' && line[0] !== '\t')) {
1105
+ inRings = false;
1106
+ continue;
1107
+ }
1108
+ // Parse ring name (and alias)
1109
+ const aliasMatch = trimmed.match(/^(.+?)\s+(?:alias|aka)\s+(\S+)\s*$/i);
1110
+ if (aliasMatch) {
1111
+ entities.push(aliasMatch[1].trim());
1112
+ entities.push(aliasMatch[2].trim());
1113
+ } else {
1114
+ entities.push(trimmed);
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ return { kind: 'tech-radar', entities, keywords };
1120
+ }
1121
+
1122
+ // ============================================================
1123
+ // Cycle extractor
1124
+ // ============================================================
1125
+
1126
+ function extractCycleSymbols(docText: string): DiagramSymbols {
1127
+ const lines = docText.split('\n');
1128
+ const entities: string[] = [];
1129
+ let pastFirstLine = false;
1130
+
1131
+ for (const line of lines) {
1132
+ const trimmed = line.trim();
1133
+ if (!trimmed || trimmed.startsWith('//')) continue;
1134
+
1135
+ if (!pastFirstLine) {
1136
+ pastFirstLine = true;
1137
+ continue;
1138
+ }
1139
+
1140
+ // Skip directives/metadata
1141
+ const firstToken = trimmed.split(/\s+/)[0].toLowerCase();
1142
+ if (METADATA_KEY_SET.has(firstToken)) continue;
1143
+ if (
1144
+ firstToken === 'direction-counterclockwise' ||
1145
+ firstToken === 'circle-nodes' ||
1146
+ firstToken === 'hide-descriptions'
1147
+ )
1148
+ continue;
1149
+
1150
+ // Skip indented lines (descriptions, edges)
1151
+ if (line[0] === ' ' || line[0] === '\t') continue;
1152
+
1153
+ // Node label (strip pipe metadata)
1154
+ const label = trimmed.split('|')[0].trim();
1155
+ if (label && !entities.includes(label)) entities.push(label);
1156
+ }
1157
+
1158
+ return {
1159
+ kind: 'cycle',
1160
+ entities,
1161
+ keywords: [
1162
+ 'direction-counterclockwise',
1163
+ 'hide-descriptions',
1164
+ 'circle-nodes',
1165
+ ],
1166
+ };
1167
+ }
1168
+
1169
+ function extractJourneyMapSymbols(docText: string): DiagramSymbols {
1170
+ const lines = docText.split('\n');
1171
+ const entities: string[] = [];
1172
+ let pastFirstLine = false;
1173
+
1174
+ for (const line of lines) {
1175
+ const trimmed = line.trim();
1176
+ if (!trimmed || trimmed.startsWith('//')) continue;
1177
+
1178
+ if (!pastFirstLine) {
1179
+ pastFirstLine = true;
1180
+ continue;
1181
+ }
1182
+
1183
+ // Skip directives/metadata at indent 0
1184
+ const firstToken = trimmed.split(/\s+/)[0].toLowerCase();
1185
+ if (METADATA_KEY_SET.has(firstToken)) continue;
1186
+ if (
1187
+ firstToken === 'persona' ||
1188
+ firstToken === 'tag' ||
1189
+ firstToken === 'no-legend'
1190
+ )
1191
+ continue;
1192
+
1193
+ const isIndented = line[0] === ' ' || line[0] === '\t';
1194
+
1195
+ // Skip deep-indented lines (annotations, descriptions under steps)
1196
+ // but keep singly-indented lines (steps within phases)
1197
+ if (isIndented) {
1198
+ // Annotation/description keywords — skip
1199
+ if (/^(pain|opportunity|thought|description)\s*:/i.test(trimmed))
1200
+ continue;
1201
+ // Tag group entries — skip
1202
+ if (/^\S+\([^)]+\)/.test(trimmed)) continue;
1203
+ }
1204
+
1205
+ // Phase header
1206
+ const phaseMatch = trimmed.match(/^\[(.+?)\]$/);
1207
+ if (phaseMatch) {
1208
+ entities.push(phaseMatch[1].trim());
1209
+ continue;
1210
+ }
1211
+
1212
+ // Step label (strip pipe metadata) — works for both indent 0 and indented steps
1213
+ const label = trimmed.split('|')[0].trim();
1214
+ if (label && !entities.includes(label)) entities.push(label);
1215
+ }
1216
+
1217
+ return {
1218
+ kind: 'journey-map',
1219
+ entities,
1220
+ keywords: [
1221
+ 'persona',
1222
+ 'no-legend',
1223
+ 'pain',
1224
+ 'opportunity',
1225
+ 'thought',
1226
+ 'description',
1227
+ ],
1228
+ };
1229
+ }