@diagrammo/dgmo 0.2.28 → 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 +361 -921
- package/dist/index.cjs +184 -107
- 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 +184 -107
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +51 -14
- package/package.json +6 -6
- package/src/chart.ts +12 -6
- package/src/class/layout.ts +17 -12
- package/src/class/parser.ts +20 -42
- package/src/class/renderer.ts +44 -46
- 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
|
|
@@ -441,7 +476,10 @@ Animal
|
|
|
441
476
|
+ speak(): void
|
|
442
477
|
|
|
443
478
|
Dog extends Animal
|
|
479
|
+
+ breed: string
|
|
480
|
+
|
|
444
481
|
Cat extends Animal
|
|
482
|
+
+ indoor: boolean
|
|
445
483
|
```
|
|
446
484
|
|
|
447
485
|
Full example:
|
|
@@ -453,38 +491,37 @@ title: Type Hierarchy
|
|
|
453
491
|
Printable [interface]
|
|
454
492
|
+ print(): void
|
|
455
493
|
|
|
456
|
-
Shape [abstract]
|
|
494
|
+
Shape implements Printable [abstract]
|
|
457
495
|
# x: number
|
|
458
496
|
# y: number
|
|
459
497
|
+ area(): number
|
|
460
|
-
|
|
498
|
+
count: number {static}
|
|
461
499
|
|
|
462
|
-
Circle
|
|
500
|
+
Circle extends Shape
|
|
463
501
|
- radius: number
|
|
464
502
|
+ area(): number
|
|
465
503
|
|
|
466
|
-
Rectangle
|
|
504
|
+
Rectangle extends Shape
|
|
467
505
|
- width: number
|
|
468
506
|
- height: number
|
|
469
507
|
|
|
470
|
-
Circle extends Shape
|
|
471
|
-
Rectangle extends Shape
|
|
472
|
-
Shape implements Printable
|
|
473
508
|
Shape *-- Circle : contains
|
|
474
509
|
```
|
|
475
510
|
|
|
476
511
|
**Class modifiers**: `[abstract]`, `[interface]`, `[enum]`
|
|
477
512
|
|
|
513
|
+
**Inheritance**: `ClassName extends Parent` or `ClassName implements Interface` — declared inline in the class header. Members are indented below.
|
|
514
|
+
|
|
478
515
|
**Member visibility**: `+` public, `#` protected, `-` private. Static: `{static}`.
|
|
479
516
|
|
|
480
|
-
**Relationships** (
|
|
481
|
-
- Inheritance: `A
|
|
482
|
-
- Implementation: `A
|
|
483
|
-
- Composition: `A
|
|
484
|
-
- Aggregation: `A
|
|
485
|
-
- Dependency: `A
|
|
517
|
+
**Relationships** (arrow syntax):
|
|
518
|
+
- Inheritance: `A --|> B`
|
|
519
|
+
- Implementation: `A ..|> B`
|
|
520
|
+
- Composition: `A *-- B`
|
|
521
|
+
- Aggregation: `A o-- B`
|
|
522
|
+
- Dependency: `A ..> B`
|
|
486
523
|
- Association: `A -> B`
|
|
487
|
-
- Optional label: `A
|
|
524
|
+
- Optional label: `A *-- B : description`
|
|
488
525
|
|
|
489
526
|
### er
|
|
490
527
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "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",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"d3-scale": "^4.0.2",
|
|
51
51
|
"d3-selection": "^3.0.0",
|
|
52
52
|
"d3-shape": "^3.2.0",
|
|
53
|
-
"echarts": "^
|
|
53
|
+
"echarts": "^6.0.0",
|
|
54
54
|
"lz-string": "^1.5.0"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"jsdom": "^
|
|
57
|
+
"jsdom": "^28.1.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/d3-array": "^3.2.1",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"@types/d3-scale": "^4.0.8",
|
|
64
64
|
"@types/d3-selection": "^3.0.11",
|
|
65
65
|
"@types/d3-shape": "^3.1.7",
|
|
66
|
-
"@types/dagre": "^0.7.
|
|
67
|
-
"@types/jsdom": "^
|
|
66
|
+
"@types/dagre": "^0.7.54",
|
|
67
|
+
"@types/jsdom": "^28.0.0",
|
|
68
68
|
"jscpd": "^4.0.8",
|
|
69
69
|
"tsup": "^8.5.1",
|
|
70
70
|
"typescript": "^5.7.3",
|
|
71
|
-
"vitest": "^
|
|
71
|
+
"vitest": "^4.0.18"
|
|
72
72
|
}
|
|
73
73
|
}
|
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/class/layout.ts
CHANGED
|
@@ -87,7 +87,7 @@ function computeNodeDimensions(node: ClassNode): {
|
|
|
87
87
|
const headerHeight = HEADER_BASE + (node.modifier ? MODIFIER_BADGE : 0);
|
|
88
88
|
|
|
89
89
|
// Fields compartment
|
|
90
|
-
let fieldsHeight
|
|
90
|
+
let fieldsHeight: number;
|
|
91
91
|
if (isEnum) {
|
|
92
92
|
// Enum values go in fields compartment
|
|
93
93
|
const enumValues = node.members; // all members are enum values
|
|
@@ -96,6 +96,8 @@ function computeNodeDimensions(node: ClassNode): {
|
|
|
96
96
|
COMPARTMENT_PADDING_Y * 2 +
|
|
97
97
|
enumValues.length * MEMBER_LINE_HEIGHT +
|
|
98
98
|
SEPARATOR_HEIGHT;
|
|
99
|
+
} else {
|
|
100
|
+
fieldsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
|
|
99
101
|
}
|
|
100
102
|
} else {
|
|
101
103
|
if (fields.length > 0) {
|
|
@@ -103,24 +105,27 @@ function computeNodeDimensions(node: ClassNode): {
|
|
|
103
105
|
COMPARTMENT_PADDING_Y * 2 +
|
|
104
106
|
fields.length * MEMBER_LINE_HEIGHT +
|
|
105
107
|
SEPARATOR_HEIGHT;
|
|
108
|
+
} else {
|
|
109
|
+
// UML: always show attributes compartment
|
|
110
|
+
fieldsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
// Methods compartment (not for enums)
|
|
110
115
|
let methodsHeight = 0;
|
|
111
|
-
if (!isEnum
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
if (!isEnum) {
|
|
117
|
+
if (methods.length > 0) {
|
|
118
|
+
methodsHeight =
|
|
119
|
+
COMPARTMENT_PADDING_Y * 2 +
|
|
120
|
+
methods.length * MEMBER_LINE_HEIGHT +
|
|
121
|
+
SEPARATOR_HEIGHT;
|
|
122
|
+
} else {
|
|
123
|
+
// UML: always show methods compartment
|
|
124
|
+
methodsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
const height =
|
|
120
|
-
headerHeight +
|
|
121
|
-
fieldsHeight +
|
|
122
|
-
methodsHeight +
|
|
123
|
-
(fieldsHeight === 0 && methodsHeight === 0 ? 4 : 0);
|
|
128
|
+
const height = headerHeight + fieldsHeight + methodsHeight;
|
|
124
129
|
|
|
125
130
|
return { width, height, headerHeight, fieldsHeight, methodsHeight };
|
|
126
131
|
}
|
package/src/class/parser.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
ParsedClassDiagram,
|
|
7
7
|
ClassNode,
|
|
8
8
|
ClassMember,
|
|
9
|
-
ClassRelationship,
|
|
10
9
|
ClassModifier,
|
|
11
10
|
MemberVisibility,
|
|
12
11
|
RelationshipType,
|
|
@@ -24,14 +23,9 @@ function classId(name: string): string {
|
|
|
24
23
|
// Regex patterns
|
|
25
24
|
// ============================================================
|
|
26
25
|
|
|
27
|
-
// Class declaration: ClassName [modifier] (color)
|
|
26
|
+
// Class declaration: ClassName [extends|implements ParentClass] [modifier] (color)
|
|
28
27
|
const CLASS_DECL_RE =
|
|
29
|
-
/^([A-Z][A-Za-z0-9_]*)(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
|
|
30
|
-
|
|
31
|
-
// Relationship — keyword syntax:
|
|
32
|
-
// ClassName extends|implements|contains|has|uses TargetClass : label
|
|
33
|
-
const REL_KEYWORD_RE =
|
|
34
|
-
/^([A-Z][A-Za-z0-9_]*)\s+(extends|implements|contains|has|uses)\s+([A-Z][A-Za-z0-9_]*)(?:\s*:\s*(.+))?$/;
|
|
28
|
+
/^([A-Z][A-Za-z0-9_]*)(?:\s+(extends|implements)\s+([A-Z][A-Za-z0-9_]*))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
|
|
35
29
|
|
|
36
30
|
// Relationship — arrow syntax:
|
|
37
31
|
// ClassName --|> TargetClass : label
|
|
@@ -45,14 +39,6 @@ const STATIC_SUFFIX_RE = /\{static\}\s*$/;
|
|
|
45
39
|
const METHOD_RE = /^(.+?)\(([^)]*)\)(?:\s*:\s*(.+))?$/;
|
|
46
40
|
const FIELD_RE = /^(.+?)\s*:\s*(.+)$/;
|
|
47
41
|
|
|
48
|
-
const KEYWORD_TO_TYPE: Record<string, RelationshipType> = {
|
|
49
|
-
extends: 'extends',
|
|
50
|
-
implements: 'implements',
|
|
51
|
-
contains: 'composes',
|
|
52
|
-
has: 'aggregates',
|
|
53
|
-
uses: 'depends',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
42
|
const ARROW_TO_TYPE: Record<string, RelationshipType> = {
|
|
57
43
|
'--|>': 'extends',
|
|
58
44
|
'..|>': 'implements',
|
|
@@ -257,28 +243,6 @@ export function parseClassDiagram(
|
|
|
257
243
|
currentClass = null;
|
|
258
244
|
contentStarted = true;
|
|
259
245
|
|
|
260
|
-
// Try relationship — keyword syntax
|
|
261
|
-
const relKeyword = trimmed.match(REL_KEYWORD_RE);
|
|
262
|
-
if (relKeyword) {
|
|
263
|
-
const sourceName = relKeyword[1];
|
|
264
|
-
const keyword = relKeyword[2].toLowerCase();
|
|
265
|
-
const targetName = relKeyword[3];
|
|
266
|
-
const label = relKeyword[4]?.trim();
|
|
267
|
-
|
|
268
|
-
// Ensure both classes exist
|
|
269
|
-
getOrCreateClass(sourceName, lineNumber);
|
|
270
|
-
getOrCreateClass(targetName, lineNumber);
|
|
271
|
-
|
|
272
|
-
result.relationships.push({
|
|
273
|
-
source: classId(sourceName),
|
|
274
|
-
target: classId(targetName),
|
|
275
|
-
type: KEYWORD_TO_TYPE[keyword],
|
|
276
|
-
...(label && { label }),
|
|
277
|
-
lineNumber,
|
|
278
|
-
});
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
246
|
// Try relationship — arrow syntax
|
|
283
247
|
const relArrow = trimmed.match(REL_ARROW_RE);
|
|
284
248
|
if (relArrow) {
|
|
@@ -305,8 +269,10 @@ export function parseClassDiagram(
|
|
|
305
269
|
const classDecl = trimmed.match(CLASS_DECL_RE);
|
|
306
270
|
if (classDecl) {
|
|
307
271
|
const name = classDecl[1];
|
|
308
|
-
const
|
|
309
|
-
const
|
|
272
|
+
const relKeyword = classDecl[2] as 'extends' | 'implements' | undefined;
|
|
273
|
+
const parentName = classDecl[3];
|
|
274
|
+
const modifier = classDecl[4] as ClassModifier | undefined;
|
|
275
|
+
const colorName = classDecl[5]?.trim();
|
|
310
276
|
const color = colorName ? resolveColor(colorName, palette) : undefined;
|
|
311
277
|
|
|
312
278
|
const node = getOrCreateClass(name, lineNumber);
|
|
@@ -315,6 +281,17 @@ export function parseClassDiagram(
|
|
|
315
281
|
// Update line number to the declaration line (may have been created by relationship)
|
|
316
282
|
node.lineNumber = lineNumber;
|
|
317
283
|
|
|
284
|
+
// Inline extends/implements creates a relationship
|
|
285
|
+
if (relKeyword && parentName) {
|
|
286
|
+
getOrCreateClass(parentName, lineNumber);
|
|
287
|
+
result.relationships.push({
|
|
288
|
+
source: classId(name),
|
|
289
|
+
target: classId(parentName),
|
|
290
|
+
type: relKeyword as RelationshipType,
|
|
291
|
+
lineNumber,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
318
295
|
currentClass = node;
|
|
319
296
|
continue;
|
|
320
297
|
}
|
|
@@ -376,9 +353,10 @@ export function looksLikeClassDiagram(content: string): boolean {
|
|
|
376
353
|
hasModifier = true;
|
|
377
354
|
hasClassDecl = true;
|
|
378
355
|
}
|
|
379
|
-
// Check for
|
|
380
|
-
if (
|
|
356
|
+
// Check for inline extends/implements in class declaration
|
|
357
|
+
if (/^[A-Z][A-Za-z0-9_]*\s+(extends|implements)\s+[A-Z]/.test(trimmed)) {
|
|
381
358
|
hasRelationship = true;
|
|
359
|
+
hasClassDecl = true;
|
|
382
360
|
}
|
|
383
361
|
// Check for relationship arrows
|
|
384
362
|
if (REL_ARROW_RE.test(trimmed)) {
|
package/src/class/renderer.ts
CHANGED
|
@@ -406,43 +406,42 @@ export function renderClassDiagram(
|
|
|
406
406
|
const methods = node.members.filter((m) => m.isMethod);
|
|
407
407
|
|
|
408
408
|
if (isEnum) {
|
|
409
|
-
// Enum:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
memberY += MEMBER_LINE_HEIGHT;
|
|
431
|
-
}
|
|
409
|
+
// Enum: single values compartment
|
|
410
|
+
// Separator
|
|
411
|
+
nodeG.append('line')
|
|
412
|
+
.attr('x1', -w / 2)
|
|
413
|
+
.attr('y1', yPos)
|
|
414
|
+
.attr('x2', w / 2)
|
|
415
|
+
.attr('y2', yPos)
|
|
416
|
+
.attr('stroke', stroke)
|
|
417
|
+
.attr('stroke-width', 0.5)
|
|
418
|
+
.attr('stroke-opacity', 0.5);
|
|
419
|
+
|
|
420
|
+
let memberY = yPos + COMPARTMENT_PADDING_Y;
|
|
421
|
+
for (const member of node.members) {
|
|
422
|
+
nodeG.append('text')
|
|
423
|
+
.attr('x', -w / 2 + MEMBER_PADDING_X)
|
|
424
|
+
.attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
|
|
425
|
+
.attr('dominant-baseline', 'central')
|
|
426
|
+
.attr('fill', palette.text)
|
|
427
|
+
.attr('font-size', MEMBER_FONT_SIZE)
|
|
428
|
+
.text(member.name);
|
|
429
|
+
memberY += MEMBER_LINE_HEIGHT;
|
|
432
430
|
}
|
|
433
431
|
} else {
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
432
|
+
// UML 3-compartment layout: always show both separators
|
|
433
|
+
|
|
434
|
+
// Fields separator
|
|
435
|
+
nodeG.append('line')
|
|
436
|
+
.attr('x1', -w / 2)
|
|
437
|
+
.attr('y1', yPos)
|
|
438
|
+
.attr('x2', w / 2)
|
|
439
|
+
.attr('y2', yPos)
|
|
440
|
+
.attr('stroke', stroke)
|
|
441
|
+
.attr('stroke-width', 0.5)
|
|
442
|
+
.attr('stroke-opacity', 0.5);
|
|
445
443
|
|
|
444
|
+
if (fields.length > 0) {
|
|
446
445
|
let memberY = yPos + COMPARTMENT_PADDING_Y;
|
|
447
446
|
for (const field of fields) {
|
|
448
447
|
const vis = visibilitySymbol(field.visibility);
|
|
@@ -463,21 +462,20 @@ export function renderClassDiagram(
|
|
|
463
462
|
|
|
464
463
|
memberY += MEMBER_LINE_HEIGHT;
|
|
465
464
|
}
|
|
466
|
-
yPos += node.fieldsHeight;
|
|
467
465
|
}
|
|
466
|
+
yPos += node.fieldsHeight;
|
|
467
|
+
|
|
468
|
+
// Methods separator
|
|
469
|
+
nodeG.append('line')
|
|
470
|
+
.attr('x1', -w / 2)
|
|
471
|
+
.attr('y1', yPos)
|
|
472
|
+
.attr('x2', w / 2)
|
|
473
|
+
.attr('y2', yPos)
|
|
474
|
+
.attr('stroke', stroke)
|
|
475
|
+
.attr('stroke-width', 0.5)
|
|
476
|
+
.attr('stroke-opacity', 0.5);
|
|
468
477
|
|
|
469
|
-
// Methods compartment
|
|
470
478
|
if (methods.length > 0) {
|
|
471
|
-
// Separator
|
|
472
|
-
nodeG.append('line')
|
|
473
|
-
.attr('x1', -w / 2)
|
|
474
|
-
.attr('y1', yPos)
|
|
475
|
-
.attr('x2', w / 2)
|
|
476
|
-
.attr('y2', yPos)
|
|
477
|
-
.attr('stroke', stroke)
|
|
478
|
-
.attr('stroke-width', 0.5)
|
|
479
|
-
.attr('stroke-opacity', 0.5);
|
|
480
|
-
|
|
481
479
|
let memberY = yPos + COMPARTMENT_PADDING_Y;
|
|
482
480
|
for (const method of methods) {
|
|
483
481
|
const vis = visibilitySymbol(method.visibility);
|
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[],
|