@diagrammo/dgmo 0.3.2 → 0.4.0
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/README.md +11 -14
- package/dist/cli.cjs +150 -150
- package/dist/index.cjs +341 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +340 -852
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +18 -19
- package/package.json +1 -1
- package/src/chart.ts +8 -39
- package/src/cli.ts +6 -6
- package/src/d3.ts +198 -674
- package/src/dgmo-router.ts +21 -42
- package/src/echarts.ts +80 -220
- package/src/index.ts +1 -0
- package/src/sequence/parser.ts +55 -106
- package/src/sequence/renderer.ts +4 -60
- package/src/utils/arrows.ts +43 -18
- package/src/utils/parsing.ts +43 -0
package/src/sequence/parser.ts
CHANGED
|
@@ -53,16 +53,13 @@ export interface SequenceParticipant {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* A message between two participants.
|
|
56
|
-
* Placeholder for future stories — included in the interface now for completeness.
|
|
57
56
|
*/
|
|
58
57
|
export interface SequenceMessage {
|
|
59
58
|
from: string;
|
|
60
59
|
to: string;
|
|
61
60
|
label: string;
|
|
62
|
-
returnLabel?: string;
|
|
63
61
|
lineNumber: number;
|
|
64
62
|
async?: boolean;
|
|
65
|
-
bidirectional?: boolean;
|
|
66
63
|
/** Standalone return — the message itself IS a return (dashed arrow, no call). */
|
|
67
64
|
standaloneReturn?: boolean;
|
|
68
65
|
}
|
|
@@ -164,70 +161,13 @@ const GROUP_HEADING_PATTERN = /^##\s+(.+?)(?:\(([^)]+)\))?\s*$/;
|
|
|
164
161
|
// Section divider pattern — "== Label ==", "== Label(color) ==", or "== Label" (trailing == optional)
|
|
165
162
|
const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
166
163
|
|
|
167
|
-
// Arrow pattern for sequence inference —
|
|
168
|
-
|
|
169
|
-
const ARROW_PATTERN = /\S+\s*(?:<->|<~>|->|~>|-\S+->|~\S+~>|<-\S+->|<~\S+~>)\s*\S+/;
|
|
170
|
-
|
|
171
|
-
// <- return syntax: "Login <- 200 OK"
|
|
172
|
-
const ARROW_RETURN_PATTERN = /^(.+?)\s*<-\s*(.+)$/;
|
|
173
|
-
|
|
174
|
-
// UML method(args): returnType syntax: "getUser(id): UserObj"
|
|
175
|
-
const UML_RETURN_PATTERN = /^(\w+\([^)]*\))\s*:\s*(.+)$/;
|
|
164
|
+
// Arrow pattern for sequence inference — detects any arrow form
|
|
165
|
+
const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
|
|
176
166
|
|
|
177
167
|
// Note patterns — "note: text", "note right of API: text", "note left of User"
|
|
178
168
|
const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(\S+))?\s*:\s*(.+)$/i;
|
|
179
169
|
const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+([^\s:]+))?\s*:?\s*$/i;
|
|
180
170
|
|
|
181
|
-
/**
|
|
182
|
-
* Extract return label from a message label string.
|
|
183
|
-
* Priority: `<-` syntax first, then UML `method(): return` syntax,
|
|
184
|
-
* then shorthand ` : ` separator (splits on last occurrence).
|
|
185
|
-
*/
|
|
186
|
-
function parseReturnLabel(rawLabel: string): {
|
|
187
|
-
label: string;
|
|
188
|
-
returnLabel?: string;
|
|
189
|
-
standaloneReturn?: boolean;
|
|
190
|
-
} {
|
|
191
|
-
if (!rawLabel) return { label: '' };
|
|
192
|
-
|
|
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)
|
|
203
|
-
const arrowReturn = rawLabel.match(ARROW_RETURN_PATTERN);
|
|
204
|
-
if (arrowReturn) {
|
|
205
|
-
return { label: arrowReturn[1].trim(), returnLabel: arrowReturn[2].trim() };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Check UML method(args): returnType syntax
|
|
209
|
-
const umlReturn = rawLabel.match(UML_RETURN_PATTERN);
|
|
210
|
-
if (umlReturn) {
|
|
211
|
-
return { label: umlReturn[1].trim(), returnLabel: umlReturn[2].trim() };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Shorthand colon return syntax (split on last ":")
|
|
215
|
-
// Skip if the colon is part of a URL scheme (followed by //)
|
|
216
|
-
const lastColon = rawLabel.lastIndexOf(':');
|
|
217
|
-
if (lastColon > 0 && lastColon < rawLabel.length - 1) {
|
|
218
|
-
const afterColon = rawLabel.substring(lastColon + 1);
|
|
219
|
-
if (!afterColon.startsWith('//')) {
|
|
220
|
-
const reqPart = rawLabel.substring(0, lastColon).trim();
|
|
221
|
-
const resPart = afterColon.trim();
|
|
222
|
-
if (reqPart && resPart) {
|
|
223
|
-
return { label: reqPart, returnLabel: resPart };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { label: rawLabel };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
171
|
/**
|
|
232
172
|
* Parse a .dgmo file with `chart: sequence` into a structured representation.
|
|
233
173
|
*/
|
|
@@ -365,7 +305,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
365
305
|
// Parse header key: value lines (always top-level)
|
|
366
306
|
// Skip 'note' lines — parsed in the indent-aware section below
|
|
367
307
|
const colonIndex = trimmed.indexOf(':');
|
|
368
|
-
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>')) {
|
|
308
|
+
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>') && !trimmed.includes('<-') && !trimmed.includes('<~')) {
|
|
369
309
|
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
370
310
|
if (key === 'note' || key.startsWith('note ')) {
|
|
371
311
|
// Fall through to indent-aware note parsing below
|
|
@@ -531,17 +471,16 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
531
471
|
}
|
|
532
472
|
if (labeledArrow) {
|
|
533
473
|
contentStarted = true;
|
|
534
|
-
const { from, to, label, async: isAsync,
|
|
474
|
+
const { from, to, label, async: isAsync, isReturn } = labeledArrow;
|
|
535
475
|
lastMsgFrom = from;
|
|
536
476
|
|
|
537
477
|
const msg: SequenceMessage = {
|
|
538
478
|
from,
|
|
539
479
|
to,
|
|
540
480
|
label,
|
|
541
|
-
returnLabel: undefined,
|
|
542
481
|
lineNumber,
|
|
543
482
|
...(isAsync ? { async: true } : {}),
|
|
544
|
-
...(
|
|
483
|
+
...(isReturn ? { standaloneReturn: true } : {}),
|
|
545
484
|
};
|
|
546
485
|
result.messages.push(msg);
|
|
547
486
|
currentContainer().push(msg);
|
|
@@ -566,30 +505,56 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
566
505
|
continue;
|
|
567
506
|
}
|
|
568
507
|
|
|
569
|
-
// ----
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
/^(\S+)\s*<->\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
508
|
+
// ---- Error: old colon-postfix syntax (A -> B: msg) ----
|
|
509
|
+
const colonPostfixSync = trimmed.match(
|
|
510
|
+
/^(\S+)\s*->\s*([^\s:]+)\s*:\s*(.+)$/
|
|
573
511
|
);
|
|
574
|
-
const
|
|
575
|
-
/^(\S+)\s
|
|
512
|
+
const colonPostfixAsync = trimmed.match(
|
|
513
|
+
/^(\S+)\s*~>\s*([^\s:]+)\s*:\s*(.+)$/
|
|
576
514
|
);
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
515
|
+
const colonPostfix = colonPostfixSync || colonPostfixAsync;
|
|
516
|
+
if (colonPostfix) {
|
|
517
|
+
const a = colonPostfix[1];
|
|
518
|
+
const b = colonPostfix[2];
|
|
519
|
+
const msg = colonPostfix[3].trim();
|
|
520
|
+
const arrowChar = colonPostfixAsync ? '~' : '-';
|
|
521
|
+
const arrowEnd = colonPostfixAsync ? '~>' : '->';
|
|
522
|
+
pushError(
|
|
523
|
+
lineNumber,
|
|
524
|
+
`Colon syntax is no longer supported. Use '${a} ${arrowChar}${msg}${arrowEnd} ${b}' instead`
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
|
|
530
|
+
const bidiPlainMatch = trimmed.match(
|
|
531
|
+
/^(\S+)\s*(?:<->|<~>)\s*(\S+)/
|
|
532
|
+
);
|
|
533
|
+
if (bidiPlainMatch) {
|
|
534
|
+
pushError(
|
|
535
|
+
lineNumber,
|
|
536
|
+
"Bidirectional arrows are no longer supported. Use two separate lines: 'A -msg-> B' and 'B -msg-> A'"
|
|
537
|
+
);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ---- Bare (unlabeled) return arrows: A <- B, A <~ B ----
|
|
542
|
+
const bareReturnSync = trimmed.match(/^(\S+)\s+<-\s+(\S+)$/);
|
|
543
|
+
const bareReturnAsync = trimmed.match(/^(\S+)\s+<~\s+(\S+)$/);
|
|
544
|
+
const bareReturn = bareReturnSync || bareReturnAsync;
|
|
545
|
+
if (bareReturn) {
|
|
579
546
|
contentStarted = true;
|
|
580
|
-
const
|
|
581
|
-
const
|
|
547
|
+
const to = bareReturn[1]; // left side = receiver
|
|
548
|
+
const from = bareReturn[2]; // right side = sender
|
|
582
549
|
lastMsgFrom = from;
|
|
583
|
-
const rawLabel = bidiMatch[3]?.trim() || '';
|
|
584
|
-
const isBidiAsync = !!bidiAsyncMatch;
|
|
585
550
|
|
|
586
551
|
const msg: SequenceMessage = {
|
|
587
552
|
from,
|
|
588
553
|
to,
|
|
589
|
-
label:
|
|
554
|
+
label: '',
|
|
590
555
|
lineNumber,
|
|
591
|
-
|
|
592
|
-
...(
|
|
556
|
+
standaloneReturn: true,
|
|
557
|
+
...(bareReturnAsync ? { async: true } : {}),
|
|
593
558
|
};
|
|
594
559
|
result.messages.push(msg);
|
|
595
560
|
currentContainer().push(msg);
|
|
@@ -613,42 +578,26 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
613
578
|
continue;
|
|
614
579
|
}
|
|
615
580
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
)
|
|
621
|
-
const syncArrowMatch = trimmed.match(
|
|
622
|
-
/^(\S+)\s*->\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
623
|
-
);
|
|
624
|
-
const arrowMatch = asyncArrowMatch || syncArrowMatch;
|
|
625
|
-
if (asyncArrowMatch) isAsync = true;
|
|
626
|
-
|
|
627
|
-
if (arrowMatch) {
|
|
581
|
+
// ---- Bare (unlabeled) call arrows: A -> B, A ~> B ----
|
|
582
|
+
const bareCallSync = trimmed.match(/^(\S+)\s*->\s*(\S+)$/);
|
|
583
|
+
const bareCallAsync = trimmed.match(/^(\S+)\s*~>\s*(\S+)$/);
|
|
584
|
+
const bareCall = bareCallSync || bareCallAsync;
|
|
585
|
+
if (bareCall) {
|
|
628
586
|
contentStarted = true;
|
|
629
|
-
const from =
|
|
630
|
-
const to =
|
|
587
|
+
const from = bareCall[1];
|
|
588
|
+
const to = bareCall[2];
|
|
631
589
|
lastMsgFrom = from;
|
|
632
|
-
const rawLabel = arrowMatch[3]?.trim() || '';
|
|
633
|
-
|
|
634
|
-
// Extract return label — skip for async messages
|
|
635
|
-
const { label, returnLabel, standaloneReturn } = isAsync
|
|
636
|
-
? { label: rawLabel, returnLabel: undefined, standaloneReturn: undefined }
|
|
637
|
-
: parseReturnLabel(rawLabel);
|
|
638
590
|
|
|
639
591
|
const msg: SequenceMessage = {
|
|
640
592
|
from,
|
|
641
593
|
to,
|
|
642
|
-
label,
|
|
643
|
-
returnLabel,
|
|
594
|
+
label: '',
|
|
644
595
|
lineNumber,
|
|
645
|
-
...(
|
|
646
|
-
...(standaloneReturn ? { standaloneReturn: true } : {}),
|
|
596
|
+
...(bareCallAsync ? { async: true } : {}),
|
|
647
597
|
};
|
|
648
598
|
result.messages.push(msg);
|
|
649
599
|
currentContainer().push(msg);
|
|
650
600
|
|
|
651
|
-
// Auto-register participants from message usage with type inference
|
|
652
601
|
if (!result.participants.some((p) => p.id === from)) {
|
|
653
602
|
result.participants.push({
|
|
654
603
|
id: from,
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -538,7 +538,6 @@ export interface RenderStep {
|
|
|
538
538
|
label: string;
|
|
539
539
|
messageIndex: number;
|
|
540
540
|
async?: boolean;
|
|
541
|
-
bidirectional?: boolean;
|
|
542
541
|
}
|
|
543
542
|
|
|
544
543
|
/**
|
|
@@ -551,7 +550,6 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
551
550
|
const stack: {
|
|
552
551
|
from: string;
|
|
553
552
|
to: string;
|
|
554
|
-
returnLabel?: string;
|
|
555
553
|
messageIndex: number;
|
|
556
554
|
}[] = [];
|
|
557
555
|
|
|
@@ -566,7 +564,7 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
566
564
|
type: 'return',
|
|
567
565
|
from: top.to,
|
|
568
566
|
to: top.from,
|
|
569
|
-
label:
|
|
567
|
+
label: '',
|
|
570
568
|
messageIndex: top.messageIndex,
|
|
571
569
|
});
|
|
572
570
|
}
|
|
@@ -601,14 +599,8 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
601
599
|
label: msg.label,
|
|
602
600
|
messageIndex: mi,
|
|
603
601
|
...(msg.async ? { async: true } : {}),
|
|
604
|
-
...(msg.bidirectional ? { bidirectional: true } : {}),
|
|
605
602
|
});
|
|
606
603
|
|
|
607
|
-
// Bidirectional messages: no activation bar, no return
|
|
608
|
-
if (msg.bidirectional) {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
604
|
// Async messages: no return arrow, no activation on target
|
|
613
605
|
if (msg.async) {
|
|
614
606
|
continue;
|
|
@@ -620,7 +612,7 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
620
612
|
type: 'return',
|
|
621
613
|
from: msg.to,
|
|
622
614
|
to: msg.from,
|
|
623
|
-
label:
|
|
615
|
+
label: '',
|
|
624
616
|
messageIndex: mi,
|
|
625
617
|
});
|
|
626
618
|
} else {
|
|
@@ -628,7 +620,6 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
628
620
|
stack.push({
|
|
629
621
|
from: msg.from,
|
|
630
622
|
to: msg.to,
|
|
631
|
-
returnLabel: msg.returnLabel,
|
|
632
623
|
messageIndex: mi,
|
|
633
624
|
});
|
|
634
625
|
}
|
|
@@ -641,7 +632,7 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
641
632
|
type: 'return',
|
|
642
633
|
from: top.to,
|
|
643
634
|
to: top.from,
|
|
644
|
-
label:
|
|
635
|
+
label: '',
|
|
645
636
|
messageIndex: top.messageIndex,
|
|
646
637
|
});
|
|
647
638
|
}
|
|
@@ -1368,42 +1359,6 @@ export function renderSequenceDiagram(
|
|
|
1368
1359
|
.attr('stroke', palette.text)
|
|
1369
1360
|
.attr('stroke-width', 1.2);
|
|
1370
1361
|
|
|
1371
|
-
// Filled reverse arrowhead for bidirectional sync arrows (marker-start)
|
|
1372
|
-
defs
|
|
1373
|
-
.append('marker')
|
|
1374
|
-
.attr('id', 'seq-arrowhead-reverse')
|
|
1375
|
-
.attr('viewBox', `0 0 ${ARROWHEAD_SIZE} ${ARROWHEAD_SIZE}`)
|
|
1376
|
-
.attr('refX', 0)
|
|
1377
|
-
.attr('refY', ARROWHEAD_SIZE / 2)
|
|
1378
|
-
.attr('markerWidth', ARROWHEAD_SIZE)
|
|
1379
|
-
.attr('markerHeight', ARROWHEAD_SIZE)
|
|
1380
|
-
.attr('orient', 'auto')
|
|
1381
|
-
.append('polygon')
|
|
1382
|
-
.attr(
|
|
1383
|
-
'points',
|
|
1384
|
-
`${ARROWHEAD_SIZE},0 0,${ARROWHEAD_SIZE / 2} ${ARROWHEAD_SIZE},${ARROWHEAD_SIZE}`
|
|
1385
|
-
)
|
|
1386
|
-
.attr('fill', palette.text);
|
|
1387
|
-
|
|
1388
|
-
// Open reverse arrowhead for bidirectional async arrows (marker-start)
|
|
1389
|
-
defs
|
|
1390
|
-
.append('marker')
|
|
1391
|
-
.attr('id', 'seq-arrowhead-async-reverse')
|
|
1392
|
-
.attr('viewBox', `0 0 ${ARROWHEAD_SIZE} ${ARROWHEAD_SIZE}`)
|
|
1393
|
-
.attr('refX', 0)
|
|
1394
|
-
.attr('refY', ARROWHEAD_SIZE / 2)
|
|
1395
|
-
.attr('markerWidth', ARROWHEAD_SIZE)
|
|
1396
|
-
.attr('markerHeight', ARROWHEAD_SIZE)
|
|
1397
|
-
.attr('orient', 'auto')
|
|
1398
|
-
.append('polyline')
|
|
1399
|
-
.attr(
|
|
1400
|
-
'points',
|
|
1401
|
-
`${ARROWHEAD_SIZE},0 0,${ARROWHEAD_SIZE / 2} ${ARROWHEAD_SIZE},${ARROWHEAD_SIZE}`
|
|
1402
|
-
)
|
|
1403
|
-
.attr('fill', 'none')
|
|
1404
|
-
.attr('stroke', palette.text)
|
|
1405
|
-
.attr('stroke-width', 1.2);
|
|
1406
|
-
|
|
1407
1362
|
// Render title
|
|
1408
1363
|
if (title) {
|
|
1409
1364
|
const titleEl = svg
|
|
@@ -1958,12 +1913,7 @@ export function renderSequenceDiagram(
|
|
|
1958
1913
|
const markerRef = step.async
|
|
1959
1914
|
? 'url(#seq-arrowhead-async)'
|
|
1960
1915
|
: 'url(#seq-arrowhead)';
|
|
1961
|
-
|
|
1962
|
-
? step.async
|
|
1963
|
-
? 'url(#seq-arrowhead-async-reverse)'
|
|
1964
|
-
: 'url(#seq-arrowhead-reverse)'
|
|
1965
|
-
: null;
|
|
1966
|
-
const line = svg
|
|
1916
|
+
svg
|
|
1967
1917
|
.append('line')
|
|
1968
1918
|
.attr('x1', x1)
|
|
1969
1919
|
.attr('y1', y)
|
|
@@ -1979,12 +1929,6 @@ export function renderSequenceDiagram(
|
|
|
1979
1929
|
)
|
|
1980
1930
|
.attr('data-msg-index', String(step.messageIndex))
|
|
1981
1931
|
.attr('data-step-index', String(i));
|
|
1982
|
-
if (markerStartRef) {
|
|
1983
|
-
line.attr('marker-start', markerStartRef);
|
|
1984
|
-
}
|
|
1985
|
-
if (step.bidirectional && step.async) {
|
|
1986
|
-
line.attr('stroke-dasharray', '6 4');
|
|
1987
|
-
}
|
|
1988
1932
|
|
|
1989
1933
|
if (step.label) {
|
|
1990
1934
|
const midX = (x1 + x2) / 2;
|
package/src/utils/arrows.ts
CHANGED
|
@@ -2,49 +2,63 @@
|
|
|
2
2
|
// Shared Arrow Parsing Utility
|
|
3
3
|
// ============================================================
|
|
4
4
|
//
|
|
5
|
-
// Labeled arrow syntax:
|
|
6
|
-
//
|
|
5
|
+
// Labeled arrow syntax:
|
|
6
|
+
// Forward: `-label->`, `~label~>`
|
|
7
|
+
// Return: `<-label-`, `<~label~`
|
|
7
8
|
|
|
8
9
|
export interface ParsedArrow {
|
|
9
10
|
from: string;
|
|
10
11
|
to: string;
|
|
11
12
|
label: string;
|
|
12
13
|
async: boolean;
|
|
13
|
-
|
|
14
|
+
isReturn: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
-
const BIDI_SYNC_LABELED_RE = /^(\S+)\s+<-(.+)->\s+(\S+)$/;
|
|
18
|
-
const BIDI_ASYNC_LABELED_RE = /^(\S+)\s+<~(.+)~>\s+(\S+)$/;
|
|
17
|
+
// Forward (call) patterns
|
|
19
18
|
const SYNC_LABELED_RE = /^(\S+)\s+-(.+)->\s+(\S+)$/;
|
|
20
19
|
const ASYNC_LABELED_RE = /^(\S+)\s+~(.+)~>\s+(\S+)$/;
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
// Return patterns — A <-msg- B means from=B, to=A
|
|
22
|
+
const RETURN_SYNC_LABELED_RE = /^(\S+)\s+<-(.+)-\s+(\S+)$/;
|
|
23
|
+
const RETURN_ASYNC_LABELED_RE = /^(\S+)\s+<~(.+)~\s+(\S+)$/;
|
|
24
|
+
|
|
25
|
+
// Bidi detection (for error messages only)
|
|
26
|
+
const BIDI_SYNC_RE = /^(\S+)\s+<-(.+)->\s+(\S+)$/;
|
|
27
|
+
const BIDI_ASYNC_RE = /^(\S+)\s+<~(.+)~>\s+(\S+)$/;
|
|
28
|
+
|
|
29
|
+
const ARROW_CHARS = ['->', '~>', '<-', '<~'];
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
32
|
* Try to parse a labeled arrow from a trimmed line.
|
|
26
33
|
*
|
|
27
34
|
* Returns:
|
|
28
35
|
* - `ParsedArrow` if matched and valid
|
|
29
|
-
* - `{ error: string }` if matched but
|
|
30
|
-
* - `null` if not a labeled arrow (caller should fall through to
|
|
36
|
+
* - `{ error: string }` if matched but invalid (bidi, or arrow chars in label)
|
|
37
|
+
* - `null` if not a labeled arrow (caller should fall through to bare patterns)
|
|
31
38
|
*/
|
|
32
39
|
export function parseArrow(
|
|
33
40
|
line: string,
|
|
34
41
|
): ParsedArrow | { error: string } | null {
|
|
35
|
-
//
|
|
42
|
+
// Check bidi patterns first — return error
|
|
43
|
+
if (BIDI_SYNC_RE.test(line) || BIDI_ASYNC_RE.test(line)) {
|
|
44
|
+
return {
|
|
45
|
+
error:
|
|
46
|
+
"Bidirectional arrows are no longer supported. Use two separate lines: 'A -msg-> B' and 'B -msg-> A'",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
const patterns: {
|
|
37
51
|
re: RegExp;
|
|
38
52
|
async: boolean;
|
|
39
|
-
|
|
53
|
+
isReturn: boolean;
|
|
40
54
|
}[] = [
|
|
41
|
-
{ re:
|
|
42
|
-
{ re:
|
|
43
|
-
{ re: SYNC_LABELED_RE, async: false,
|
|
44
|
-
{ re: ASYNC_LABELED_RE, async: true,
|
|
55
|
+
{ re: RETURN_SYNC_LABELED_RE, async: false, isReturn: true },
|
|
56
|
+
{ re: RETURN_ASYNC_LABELED_RE, async: true, isReturn: true },
|
|
57
|
+
{ re: SYNC_LABELED_RE, async: false, isReturn: false },
|
|
58
|
+
{ re: ASYNC_LABELED_RE, async: true, isReturn: false },
|
|
45
59
|
];
|
|
46
60
|
|
|
47
|
-
for (const { re, async: isAsync,
|
|
61
|
+
for (const { re, async: isAsync, isReturn } of patterns) {
|
|
48
62
|
const m = line.match(re);
|
|
49
63
|
if (!m) continue;
|
|
50
64
|
|
|
@@ -57,17 +71,28 @@ export function parseArrow(
|
|
|
57
71
|
for (const arrow of ARROW_CHARS) {
|
|
58
72
|
if (label.includes(arrow)) {
|
|
59
73
|
return {
|
|
60
|
-
error: 'Arrow characters (->,
|
|
74
|
+
error: 'Arrow characters (->, ~>, <-, <~) are not allowed inside labels',
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
|
|
79
|
+
if (isReturn) {
|
|
80
|
+
// Return arrow: A <-msg- B → from=B (source), to=A (destination)
|
|
81
|
+
return {
|
|
82
|
+
from: m[3],
|
|
83
|
+
to: m[1],
|
|
84
|
+
label,
|
|
85
|
+
async: isAsync,
|
|
86
|
+
isReturn: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
return {
|
|
66
91
|
from: m[1],
|
|
67
92
|
to: m[3],
|
|
68
93
|
label,
|
|
69
94
|
async: isAsync,
|
|
70
|
-
|
|
95
|
+
isReturn: false,
|
|
71
96
|
};
|
|
72
97
|
}
|
|
73
98
|
|
package/src/utils/parsing.ts
CHANGED
|
@@ -75,6 +75,49 @@ export function collectIndentedValues(
|
|
|
75
75
|
return { values, newIndex: j - 1 };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Parse series names from a `series:` value or indented block, extracting
|
|
80
|
+
* optional per-name color suffixes. Shared between chart.ts and echarts.ts.
|
|
81
|
+
*
|
|
82
|
+
* Returns the parsed names, optional colors, and the raw series string
|
|
83
|
+
* (for single-series display), plus `newIndex` if indented values were consumed.
|
|
84
|
+
*/
|
|
85
|
+
export function parseSeriesNames(
|
|
86
|
+
value: string,
|
|
87
|
+
lines: string[],
|
|
88
|
+
lineIndex: number,
|
|
89
|
+
palette?: PaletteColors,
|
|
90
|
+
): {
|
|
91
|
+
series: string;
|
|
92
|
+
names: string[];
|
|
93
|
+
nameColors: (string | undefined)[];
|
|
94
|
+
newIndex: number;
|
|
95
|
+
} {
|
|
96
|
+
let rawNames: string[];
|
|
97
|
+
let series: string;
|
|
98
|
+
let newIndex = lineIndex;
|
|
99
|
+
if (value) {
|
|
100
|
+
series = value;
|
|
101
|
+
rawNames = value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
102
|
+
} else {
|
|
103
|
+
const collected = collectIndentedValues(lines, lineIndex);
|
|
104
|
+
newIndex = collected.newIndex;
|
|
105
|
+
rawNames = collected.values;
|
|
106
|
+
series = rawNames.join(', ');
|
|
107
|
+
}
|
|
108
|
+
const names: string[] = [];
|
|
109
|
+
const nameColors: (string | undefined)[] = [];
|
|
110
|
+
for (const raw of rawNames) {
|
|
111
|
+
const extracted = extractColor(raw, palette);
|
|
112
|
+
nameColors.push(extracted.color);
|
|
113
|
+
names.push(extracted.label);
|
|
114
|
+
}
|
|
115
|
+
if (names.length === 1) {
|
|
116
|
+
series = names[0];
|
|
117
|
+
}
|
|
118
|
+
return { series, names, nameColors, newIndex };
|
|
119
|
+
}
|
|
120
|
+
|
|
78
121
|
/** Parse pipe-delimited metadata from segments after the first (name) segment. */
|
|
79
122
|
export function parsePipeMetadata(
|
|
80
123
|
segments: string[],
|