@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.
- package/.claude/skills/dgmo-chart/SKILL.md +6 -0
- package/README.md +5 -0
- package/dist/cli.cjs +136 -136
- package/dist/index.cjs +144 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +144 -60
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +35 -0
- package/package.json +1 -1
- package/src/chart.ts +12 -6
- package/src/d3.ts +23 -6
- package/src/echarts.ts +25 -7
- package/src/sequence/parser.ts +16 -3
- package/src/sequence/renderer.ts +22 -0
- package/src/utils/parsing.ts +31 -0
|
@@ -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
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
.map((s) => s.trim())
|
|
189
|
-
|
|
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*(
|
|
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
|
|
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*(
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
.map((s) => s.trim())
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/sequence/parser.ts
CHANGED
|
@@ -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
|
-
//
|
|
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);
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -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',
|
package/src/utils/parsing.ts
CHANGED
|
@@ -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[],
|