@diagrammo/dgmo 0.3.0 → 0.3.1

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.
@@ -44,6 +44,41 @@ Set via CLI: `dgmo diagram.dgmo --palette catppuccin --theme dark`
44
44
 
45
45
  Text fields support: `*italic*`, `**bold**`, `` `code` ``, `[link text](url)`. Bare URLs are auto-linked.
46
46
 
47
+ ### Multi-line Values
48
+
49
+ Properties that accept comma-separated lists (`series`, `columns`, `rows`, `x-axis`, `y-axis`) also accept an indented multi-line format. Leave the value after the colon empty and list each value on its own indented line:
50
+
51
+ ```
52
+ // Single-line (still works)
53
+ series: Rum, Spices, Silk, Gold
54
+
55
+ // Multi-line equivalent
56
+ series:
57
+ Rum
58
+ Spices
59
+ Silk
60
+ Gold
61
+ ```
62
+
63
+ Multi-line blocks support blank lines and `//` comments within the block. Trailing commas on values are stripped for convenience.
64
+
65
+ ```
66
+ series:
67
+ Rum (red)
68
+ Spices (green)
69
+ // gold last
70
+ Gold (yellow)
71
+ ```
72
+
73
+ Works with `columns:` and `rows:` in heatmaps:
74
+
75
+ ```
76
+ columns:
77
+ January
78
+ February
79
+ March
80
+ ```
81
+
47
82
  ---
48
83
 
49
84
  ## Chart Types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/chart.ts CHANGED
@@ -47,6 +47,7 @@ export interface ParsedChart {
47
47
  import { resolveColor } from './colors';
48
48
  import type { PaletteColors } from './palettes';
49
49
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
50
+ import { collectIndentedValues } from './utils/parsing';
50
51
 
51
52
  // ============================================================
52
53
  // Parser
@@ -181,12 +182,17 @@ export function parseChart(
181
182
  }
182
183
 
183
184
  if (key === 'series') {
184
- result.series = value;
185
- // Parse comma-separated series names for multi-series chart types
186
- const rawNames = value
187
- .split(',')
188
- .map((s) => s.trim())
189
- .filter(Boolean);
185
+ // Parse series names — comma-separated on one line, or indented multi-line
186
+ let rawNames: string[];
187
+ if (value) {
188
+ result.series = value;
189
+ rawNames = value.split(',').map((s) => s.trim()).filter(Boolean);
190
+ } else {
191
+ const collected = collectIndentedValues(lines, i);
192
+ i = collected.newIndex;
193
+ rawNames = collected.values;
194
+ result.series = rawNames.join(', ');
195
+ }
190
196
  const names: string[] = [];
191
197
  const nameColors: (string | undefined)[] = [];
192
198
  for (const raw of rawNames) {
package/src/d3.ts CHANGED
@@ -180,6 +180,7 @@ import type { PaletteColors } from './palettes';
180
180
  import { getSeriesColors } from './palettes';
181
181
  import type { DgmoError } from './diagnostics';
182
182
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
183
+ import { collectIndentedValues } from './utils/parsing';
183
184
 
184
185
  // ============================================================
185
186
  // Timeline Date Helper
@@ -517,10 +518,18 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
517
518
 
518
519
  // Quadrant-specific parsing
519
520
  if (result.type === 'quadrant') {
520
- // x-axis: Low, High
521
- const xAxisMatch = line.match(/^x-axis\s*:\s*(.+)/i);
521
+ // x-axis: Low, High — or indented multi-line
522
+ const xAxisMatch = line.match(/^x-axis\s*:\s*(.*)/i);
522
523
  if (xAxisMatch) {
523
- const parts = xAxisMatch[1].split(',').map((s) => s.trim());
524
+ const val = xAxisMatch[1].trim();
525
+ let parts: string[];
526
+ if (val) {
527
+ parts = val.split(',').map((s) => s.trim());
528
+ } else {
529
+ const collected = collectIndentedValues(lines, i);
530
+ i = collected.newIndex;
531
+ parts = collected.values;
532
+ }
524
533
  if (parts.length >= 2) {
525
534
  result.quadrantXAxis = [parts[0], parts[1]];
526
535
  result.quadrantXAxisLineNumber = lineNumber;
@@ -528,10 +537,18 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
528
537
  continue;
529
538
  }
530
539
 
531
- // y-axis: Low, High
532
- const yAxisMatch = line.match(/^y-axis\s*:\s*(.+)/i);
540
+ // y-axis: Low, High — or indented multi-line
541
+ const yAxisMatch = line.match(/^y-axis\s*:\s*(.*)/i);
533
542
  if (yAxisMatch) {
534
- const parts = yAxisMatch[1].split(',').map((s) => s.trim());
543
+ const val = yAxisMatch[1].trim();
544
+ let parts: string[];
545
+ if (val) {
546
+ parts = val.split(',').map((s) => s.trim());
547
+ } else {
548
+ const collected = collectIndentedValues(lines, i);
549
+ i = collected.newIndex;
550
+ parts = collected.values;
551
+ }
535
552
  if (parts.length >= 2) {
536
553
  result.quadrantYAxis = [parts[0], parts[1]];
537
554
  result.quadrantYAxisLineNumber = lineNumber;
package/src/echarts.ts CHANGED
@@ -88,6 +88,7 @@ import { getSeriesColors, getSegmentColors } from './palettes';
88
88
  import { parseChart } from './chart';
89
89
  import type { ParsedChart } from './chart';
90
90
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
91
+ import { collectIndentedValues } from './utils/parsing';
91
92
 
92
93
  // ============================================================
93
94
  // Parser
@@ -193,11 +194,16 @@ export function parseEChart(
193
194
  }
194
195
 
195
196
  if (key === 'series') {
196
- result.series = value;
197
- const rawNames = value
198
- .split(',')
199
- .map((s) => s.trim())
200
- .filter(Boolean);
197
+ let rawNames: string[];
198
+ if (value) {
199
+ result.series = value;
200
+ rawNames = value.split(',').map((s) => s.trim()).filter(Boolean);
201
+ } else {
202
+ const collected = collectIndentedValues(lines, i);
203
+ i = collected.newIndex;
204
+ rawNames = collected.values;
205
+ result.series = rawNames.join(', ');
206
+ }
201
207
  const names: string[] = [];
202
208
  const nameColors: (string | undefined)[] = [];
203
209
  for (const raw of rawNames) {
@@ -241,12 +247,24 @@ export function parseEChart(
241
247
 
242
248
  // Heatmap columns and rows headers
243
249
  if (key === 'columns') {
244
- result.columns = value.split(',').map((s) => s.trim());
250
+ if (value) {
251
+ result.columns = value.split(',').map((s) => s.trim());
252
+ } else {
253
+ const collected = collectIndentedValues(lines, i);
254
+ i = collected.newIndex;
255
+ result.columns = collected.values;
256
+ }
245
257
  continue;
246
258
  }
247
259
 
248
260
  if (key === 'rows') {
249
- result.rows = value.split(',').map((s) => s.trim());
261
+ if (value) {
262
+ result.rows = value.split(',').map((s) => s.trim());
263
+ } else {
264
+ const collected = collectIndentedValues(lines, i);
265
+ i = collected.newIndex;
266
+ result.rows = collected.values;
267
+ }
250
268
  continue;
251
269
  }
252
270
 
@@ -63,6 +63,8 @@ export interface SequenceMessage {
63
63
  lineNumber: number;
64
64
  async?: boolean;
65
65
  bidirectional?: boolean;
66
+ /** Standalone return — the message itself IS a return (dashed arrow, no call). */
67
+ standaloneReturn?: boolean;
66
68
  }
67
69
 
68
70
  /**
@@ -184,10 +186,20 @@ const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+([^\s:]+))?\s*:?\s*$/i;
184
186
  function parseReturnLabel(rawLabel: string): {
185
187
  label: string;
186
188
  returnLabel?: string;
189
+ standaloneReturn?: boolean;
187
190
  } {
188
191
  if (!rawLabel) return { label: '' };
189
192
 
190
- // Check <- syntax first
193
+ // Standalone return: label starts with `<-` (no forward label)
194
+ const standaloneMatch = rawLabel.match(/^<-\s*(.*)$/);
195
+ if (standaloneMatch) {
196
+ return {
197
+ label: standaloneMatch[1].trim(),
198
+ standaloneReturn: true,
199
+ };
200
+ }
201
+
202
+ // Check <- syntax first (separates forward label from return label)
191
203
  const arrowReturn = rawLabel.match(ARROW_RETURN_PATTERN);
192
204
  if (arrowReturn) {
193
205
  return { label: arrowReturn[1].trim(), returnLabel: arrowReturn[2].trim() };
@@ -620,8 +632,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
620
632
  const rawLabel = arrowMatch[3]?.trim() || '';
621
633
 
622
634
  // Extract return label — skip for async messages
623
- const { label, returnLabel } = isAsync
624
- ? { label: rawLabel, returnLabel: undefined }
635
+ const { label, returnLabel, standaloneReturn } = isAsync
636
+ ? { label: rawLabel, returnLabel: undefined, standaloneReturn: undefined }
625
637
  : parseReturnLabel(rawLabel);
626
638
 
627
639
  const msg: SequenceMessage = {
@@ -631,6 +643,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
631
643
  returnLabel,
632
644
  lineNumber,
633
645
  ...(isAsync ? { async: true } : {}),
646
+ ...(standaloneReturn ? { standaloneReturn: true } : {}),
634
647
  };
635
648
  result.messages.push(msg);
636
649
  currentContainer().push(msg);
@@ -571,6 +571,28 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
571
571
  });
572
572
  }
573
573
 
574
+ // Standalone return: emit as a return step directly (no call, no stack).
575
+ // Also pop the matching pending call from the stack so it doesn't
576
+ // generate a duplicate empty return later.
577
+ if (msg.standaloneReturn) {
578
+ // Find and remove the stack entry this return satisfies
579
+ // (the pending call where from→to matches to→from of this return)
580
+ for (let si = stack.length - 1; si >= 0; si--) {
581
+ if (stack[si].from === msg.to && stack[si].to === msg.from) {
582
+ stack.splice(si, 1);
583
+ break;
584
+ }
585
+ }
586
+ steps.push({
587
+ type: 'return',
588
+ from: msg.from,
589
+ to: msg.to,
590
+ label: msg.label,
591
+ messageIndex: mi,
592
+ });
593
+ continue;
594
+ }
595
+
574
596
  // Emit call
575
597
  steps.push({
576
598
  type: 'call',
@@ -44,6 +44,37 @@ export const TITLE_RE = /^title\s*:\s*(.+)/i;
44
44
  /** Matches `option: value` header lines. */
45
45
  export const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
46
46
 
47
+ /**
48
+ * Collect indented continuation lines as individual values.
49
+ * Used when a property like `series:` has an empty value — subsequent
50
+ * indented lines each become one value entry.
51
+ *
52
+ * - Skips blank lines and `//` comment lines within the block
53
+ * - Stops at first non-indented non-empty line (or EOF)
54
+ * - Strips trailing commas from values (user habit tolerance)
55
+ * - Returns `newIndex` so caller does `i = newIndex` and the loop's `i++` lands correctly
56
+ */
57
+ export function collectIndentedValues(
58
+ lines: string[],
59
+ startIndex: number,
60
+ ): { values: string[]; newIndex: number } {
61
+ const values: string[] = [];
62
+ let j = startIndex + 1;
63
+ for (; j < lines.length; j++) {
64
+ const raw = lines[j];
65
+ const trimmed = raw.trim();
66
+ // Skip blank lines within the block
67
+ if (!trimmed) continue;
68
+ // Skip comment lines within the block
69
+ if (trimmed.startsWith('//')) continue;
70
+ // Stop at non-indented lines (first char is not whitespace)
71
+ if (raw[0] !== ' ' && raw[0] !== '\t') break;
72
+ // Strip trailing comma and collect
73
+ values.push(trimmed.replace(/,\s*$/, ''));
74
+ }
75
+ return { values, newIndex: j - 1 };
76
+ }
77
+
47
78
  /** Parse pipe-delimited metadata from segments after the first (name) segment. */
48
79
  export function parsePipeMetadata(
49
80
  segments: string[],