@diagrammo/dgmo 0.3.2 → 0.4.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-sequence/SKILL.md +7 -9
- package/.cursorrules +4 -4
- package/.github/copilot-instructions.md +4 -4
- package/.windsurfrules +4 -4
- package/README.md +11 -14
- package/dist/cli.cjs +150 -150
- package/dist/index.cjs +336 -891
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -7
- package/dist/index.d.ts +3 -7
- package/dist/index.js +335 -891
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +16 -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 +53 -133
- package/src/sequence/renderer.ts +4 -82
- package/src/utils/arrows.ts +37 -17
- package/src/utils/parsing.ts +43 -0
package/src/sequence/parser.ts
CHANGED
|
@@ -53,18 +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
|
-
/** Standalone return — the message itself IS a return (dashed arrow, no call). */
|
|
67
|
-
standaloneReturn?: boolean;
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
/**
|
|
@@ -164,70 +159,13 @@ const GROUP_HEADING_PATTERN = /^##\s+(.+?)(?:\(([^)]+)\))?\s*$/;
|
|
|
164
159
|
// Section divider pattern — "== Label ==", "== Label(color) ==", or "== Label" (trailing == optional)
|
|
165
160
|
const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
166
161
|
|
|
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*(.+)$/;
|
|
162
|
+
// Arrow pattern for sequence inference — detects any arrow form
|
|
163
|
+
const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
|
|
176
164
|
|
|
177
165
|
// Note patterns — "note: text", "note right of API: text", "note left of User"
|
|
178
166
|
const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(\S+))?\s*:\s*(.+)$/i;
|
|
179
167
|
const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+([^\s:]+))?\s*:?\s*$/i;
|
|
180
168
|
|
|
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
169
|
/**
|
|
232
170
|
* Parse a .dgmo file with `chart: sequence` into a structured representation.
|
|
233
171
|
*/
|
|
@@ -365,7 +303,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
365
303
|
// Parse header key: value lines (always top-level)
|
|
366
304
|
// Skip 'note' lines — parsed in the indent-aware section below
|
|
367
305
|
const colonIndex = trimmed.indexOf(':');
|
|
368
|
-
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>')) {
|
|
306
|
+
if (colonIndex > 0 && !trimmed.includes('->') && !trimmed.includes('~>') && !trimmed.includes('<-') && !trimmed.includes('<~')) {
|
|
369
307
|
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
370
308
|
if (key === 'note' || key.startsWith('note ')) {
|
|
371
309
|
// Fall through to indent-aware note parsing below
|
|
@@ -522,7 +460,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
522
460
|
continue;
|
|
523
461
|
}
|
|
524
462
|
|
|
525
|
-
// ---- Labeled arrows: -label->, ~label
|
|
463
|
+
// ---- Labeled arrows: -label->, ~label~> ----
|
|
526
464
|
// Must be checked BEFORE plain arrow patterns to avoid partial matches
|
|
527
465
|
const labeledArrow = parseArrow(trimmed);
|
|
528
466
|
if (labeledArrow && 'error' in labeledArrow) {
|
|
@@ -531,17 +469,15 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
531
469
|
}
|
|
532
470
|
if (labeledArrow) {
|
|
533
471
|
contentStarted = true;
|
|
534
|
-
const { from, to, label, async: isAsync
|
|
472
|
+
const { from, to, label, async: isAsync } = labeledArrow;
|
|
535
473
|
lastMsgFrom = from;
|
|
536
474
|
|
|
537
475
|
const msg: SequenceMessage = {
|
|
538
476
|
from,
|
|
539
477
|
to,
|
|
540
478
|
label,
|
|
541
|
-
returnLabel: undefined,
|
|
542
479
|
lineNumber,
|
|
543
480
|
...(isAsync ? { async: true } : {}),
|
|
544
|
-
...(bidirectional ? { bidirectional: true } : {}),
|
|
545
481
|
};
|
|
546
482
|
result.messages.push(msg);
|
|
547
483
|
currentContainer().push(msg);
|
|
@@ -566,89 +502,73 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
566
502
|
continue;
|
|
567
503
|
}
|
|
568
504
|
|
|
569
|
-
// ----
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
/^(\S+)\s*<->\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
505
|
+
// ---- Error: old colon-postfix syntax (A -> B: msg) ----
|
|
506
|
+
const colonPostfixSync = trimmed.match(
|
|
507
|
+
/^(\S+)\s*->\s*([^\s:]+)\s*:\s*(.+)$/
|
|
573
508
|
);
|
|
574
|
-
const
|
|
575
|
-
/^(\S+)\s
|
|
509
|
+
const colonPostfixAsync = trimmed.match(
|
|
510
|
+
/^(\S+)\s*~>\s*([^\s:]+)\s*:\s*(.+)$/
|
|
576
511
|
);
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const msg: SequenceMessage = {
|
|
587
|
-
from,
|
|
588
|
-
to,
|
|
589
|
-
label: rawLabel,
|
|
512
|
+
const colonPostfix = colonPostfixSync || colonPostfixAsync;
|
|
513
|
+
if (colonPostfix) {
|
|
514
|
+
const a = colonPostfix[1];
|
|
515
|
+
const b = colonPostfix[2];
|
|
516
|
+
const msg = colonPostfix[3].trim();
|
|
517
|
+
const arrowChar = colonPostfixAsync ? '~' : '-';
|
|
518
|
+
const arrowEnd = colonPostfixAsync ? '~>' : '->';
|
|
519
|
+
pushError(
|
|
590
520
|
lineNumber,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
};
|
|
594
|
-
result.messages.push(msg);
|
|
595
|
-
currentContainer().push(msg);
|
|
596
|
-
|
|
597
|
-
if (!result.participants.some((p) => p.id === from)) {
|
|
598
|
-
result.participants.push({
|
|
599
|
-
id: from,
|
|
600
|
-
label: from,
|
|
601
|
-
type: inferParticipantType(from),
|
|
602
|
-
lineNumber,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
if (!result.participants.some((p) => p.id === to)) {
|
|
606
|
-
result.participants.push({
|
|
607
|
-
id: to,
|
|
608
|
-
label: to,
|
|
609
|
-
type: inferParticipantType(to),
|
|
610
|
-
lineNumber,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
521
|
+
`Colon syntax is no longer supported. Use '${a} ${arrowChar}${msg}${arrowEnd} ${b}' instead`
|
|
522
|
+
);
|
|
613
523
|
continue;
|
|
614
524
|
}
|
|
615
525
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
/^(\S+)\s*~>\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
620
|
-
);
|
|
621
|
-
const syncArrowMatch = trimmed.match(
|
|
622
|
-
/^(\S+)\s*->\s*([^\s:]+)\s*(?::\s*(.+))?$/
|
|
526
|
+
// ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
|
|
527
|
+
const bidiPlainMatch = trimmed.match(
|
|
528
|
+
/^(\S+)\s*(?:<->|<~>)\s*(\S+)/
|
|
623
529
|
);
|
|
624
|
-
|
|
625
|
-
|
|
530
|
+
if (bidiPlainMatch) {
|
|
531
|
+
pushError(
|
|
532
|
+
lineNumber,
|
|
533
|
+
"Bidirectional arrows are no longer supported. Use two separate lines: 'A -msg-> B' and 'B -msg-> A'"
|
|
534
|
+
);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ---- Deprecated bare return arrows: A <- B, A <~ B ----
|
|
539
|
+
const bareReturnSync = trimmed.match(/^(\S+)\s+<-\s+(\S+)$/);
|
|
540
|
+
const bareReturnAsync = trimmed.match(/^(\S+)\s+<~\s+(\S+)$/);
|
|
541
|
+
const bareReturn = bareReturnSync || bareReturnAsync;
|
|
542
|
+
if (bareReturn) {
|
|
543
|
+
const to = bareReturn[1];
|
|
544
|
+
const from = bareReturn[2];
|
|
545
|
+
pushError(
|
|
546
|
+
lineNumber,
|
|
547
|
+
`Left-pointing arrows are no longer supported. Write '${from} -> ${to}' instead`
|
|
548
|
+
);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
626
551
|
|
|
627
|
-
|
|
552
|
+
// ---- Bare (unlabeled) call arrows: A -> B, A ~> B ----
|
|
553
|
+
const bareCallSync = trimmed.match(/^(\S+)\s*->\s*(\S+)$/);
|
|
554
|
+
const bareCallAsync = trimmed.match(/^(\S+)\s*~>\s*(\S+)$/);
|
|
555
|
+
const bareCall = bareCallSync || bareCallAsync;
|
|
556
|
+
if (bareCall) {
|
|
628
557
|
contentStarted = true;
|
|
629
|
-
const from =
|
|
630
|
-
const to =
|
|
558
|
+
const from = bareCall[1];
|
|
559
|
+
const to = bareCall[2];
|
|
631
560
|
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
561
|
|
|
639
562
|
const msg: SequenceMessage = {
|
|
640
563
|
from,
|
|
641
564
|
to,
|
|
642
|
-
label,
|
|
643
|
-
returnLabel,
|
|
565
|
+
label: '',
|
|
644
566
|
lineNumber,
|
|
645
|
-
...(
|
|
646
|
-
...(standaloneReturn ? { standaloneReturn: true } : {}),
|
|
567
|
+
...(bareCallAsync ? { async: true } : {}),
|
|
647
568
|
};
|
|
648
569
|
result.messages.push(msg);
|
|
649
570
|
currentContainer().push(msg);
|
|
650
571
|
|
|
651
|
-
// Auto-register participants from message usage with type inference
|
|
652
572
|
if (!result.participants.some((p) => p.id === from)) {
|
|
653
573
|
result.participants.push({
|
|
654
574
|
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,33 +564,11 @@ 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
|
}
|
|
573
571
|
|
|
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
|
-
|
|
596
572
|
// Emit call
|
|
597
573
|
steps.push({
|
|
598
574
|
type: 'call',
|
|
@@ -601,14 +577,8 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
601
577
|
label: msg.label,
|
|
602
578
|
messageIndex: mi,
|
|
603
579
|
...(msg.async ? { async: true } : {}),
|
|
604
|
-
...(msg.bidirectional ? { bidirectional: true } : {}),
|
|
605
580
|
});
|
|
606
581
|
|
|
607
|
-
// Bidirectional messages: no activation bar, no return
|
|
608
|
-
if (msg.bidirectional) {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
582
|
// Async messages: no return arrow, no activation on target
|
|
613
583
|
if (msg.async) {
|
|
614
584
|
continue;
|
|
@@ -620,7 +590,7 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
620
590
|
type: 'return',
|
|
621
591
|
from: msg.to,
|
|
622
592
|
to: msg.from,
|
|
623
|
-
label:
|
|
593
|
+
label: '',
|
|
624
594
|
messageIndex: mi,
|
|
625
595
|
});
|
|
626
596
|
} else {
|
|
@@ -628,7 +598,6 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
628
598
|
stack.push({
|
|
629
599
|
from: msg.from,
|
|
630
600
|
to: msg.to,
|
|
631
|
-
returnLabel: msg.returnLabel,
|
|
632
601
|
messageIndex: mi,
|
|
633
602
|
});
|
|
634
603
|
}
|
|
@@ -641,7 +610,7 @@ export function buildRenderSequence(messages: SequenceMessage[]): RenderStep[] {
|
|
|
641
610
|
type: 'return',
|
|
642
611
|
from: top.to,
|
|
643
612
|
to: top.from,
|
|
644
|
-
label:
|
|
613
|
+
label: '',
|
|
645
614
|
messageIndex: top.messageIndex,
|
|
646
615
|
});
|
|
647
616
|
}
|
|
@@ -1368,42 +1337,6 @@ export function renderSequenceDiagram(
|
|
|
1368
1337
|
.attr('stroke', palette.text)
|
|
1369
1338
|
.attr('stroke-width', 1.2);
|
|
1370
1339
|
|
|
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
1340
|
// Render title
|
|
1408
1341
|
if (title) {
|
|
1409
1342
|
const titleEl = svg
|
|
@@ -1958,12 +1891,7 @@ export function renderSequenceDiagram(
|
|
|
1958
1891
|
const markerRef = step.async
|
|
1959
1892
|
? 'url(#seq-arrowhead-async)'
|
|
1960
1893
|
: 'url(#seq-arrowhead)';
|
|
1961
|
-
|
|
1962
|
-
? step.async
|
|
1963
|
-
? 'url(#seq-arrowhead-async-reverse)'
|
|
1964
|
-
: 'url(#seq-arrowhead-reverse)'
|
|
1965
|
-
: null;
|
|
1966
|
-
const line = svg
|
|
1894
|
+
svg
|
|
1967
1895
|
.append('line')
|
|
1968
1896
|
.attr('x1', x1)
|
|
1969
1897
|
.attr('y1', y)
|
|
@@ -1979,12 +1907,6 @@ export function renderSequenceDiagram(
|
|
|
1979
1907
|
)
|
|
1980
1908
|
.attr('data-msg-index', String(step.messageIndex))
|
|
1981
1909
|
.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
1910
|
|
|
1989
1911
|
if (step.label) {
|
|
1990
1912
|
const midX = (x1 + x2) / 2;
|
package/src/utils/arrows.ts
CHANGED
|
@@ -2,49 +2,70 @@
|
|
|
2
2
|
// Shared Arrow Parsing Utility
|
|
3
3
|
// ============================================================
|
|
4
4
|
//
|
|
5
|
-
// Labeled arrow syntax
|
|
6
|
-
//
|
|
5
|
+
// Labeled arrow syntax (always left-to-right):
|
|
6
|
+
// Sync: `-label->`
|
|
7
|
+
// Async: `~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
|
-
bidirectional: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
const BIDI_SYNC_LABELED_RE = /^(\S+)\s+<-(.+)->\s+(\S+)$/;
|
|
18
|
-
const BIDI_ASYNC_LABELED_RE = /^(\S+)\s+<~(.+)~>\s+(\S+)$/;
|
|
16
|
+
// Forward (call) patterns
|
|
19
17
|
const SYNC_LABELED_RE = /^(\S+)\s+-(.+)->\s+(\S+)$/;
|
|
20
18
|
const ASYNC_LABELED_RE = /^(\S+)\s+~(.+)~>\s+(\S+)$/;
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
// Deprecated patterns — produce errors
|
|
21
|
+
const RETURN_SYNC_LABELED_RE = /^(\S+)\s+<-(.+)-\s+(\S+)$/;
|
|
22
|
+
const RETURN_ASYNC_LABELED_RE = /^(\S+)\s+<~(.+)~\s+(\S+)$/;
|
|
23
|
+
const BIDI_SYNC_RE = /^(\S+)\s+<-(.+)->\s+(\S+)$/;
|
|
24
|
+
const BIDI_ASYNC_RE = /^(\S+)\s+<~(.+)~>\s+(\S+)$/;
|
|
25
|
+
|
|
26
|
+
const ARROW_CHARS = ['->', '~>'];
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Try to parse a labeled arrow from a trimmed line.
|
|
26
30
|
*
|
|
27
31
|
* Returns:
|
|
28
32
|
* - `ParsedArrow` if matched and valid
|
|
29
|
-
* - `{ error: string }` if matched but
|
|
30
|
-
* - `null` if not a labeled arrow (caller should fall through to
|
|
33
|
+
* - `{ error: string }` if matched but invalid (deprecated syntax)
|
|
34
|
+
* - `null` if not a labeled arrow (caller should fall through to bare patterns)
|
|
31
35
|
*/
|
|
32
36
|
export function parseArrow(
|
|
33
37
|
line: string,
|
|
34
38
|
): ParsedArrow | { error: string } | null {
|
|
35
|
-
//
|
|
39
|
+
// Check bidi patterns first — return error
|
|
40
|
+
if (BIDI_SYNC_RE.test(line) || BIDI_ASYNC_RE.test(line)) {
|
|
41
|
+
return {
|
|
42
|
+
error:
|
|
43
|
+
"Bidirectional arrows are no longer supported. Use two separate lines: 'A -msg-> B' and 'B -msg-> A'",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check deprecated return arrow patterns — return error
|
|
48
|
+
if (RETURN_SYNC_LABELED_RE.test(line) || RETURN_ASYNC_LABELED_RE.test(line)) {
|
|
49
|
+
const m =
|
|
50
|
+
line.match(RETURN_SYNC_LABELED_RE) ??
|
|
51
|
+
line.match(RETURN_ASYNC_LABELED_RE);
|
|
52
|
+
const from = m![3];
|
|
53
|
+
const to = m![1];
|
|
54
|
+
const label = m![2].trim();
|
|
55
|
+
return {
|
|
56
|
+
error: `Left-pointing arrows are no longer supported. Write '${from} -${label}-> ${to}' instead`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
const patterns: {
|
|
37
61
|
re: RegExp;
|
|
38
62
|
async: boolean;
|
|
39
|
-
bidirectional: boolean;
|
|
40
63
|
}[] = [
|
|
41
|
-
{ re:
|
|
42
|
-
{ re:
|
|
43
|
-
{ re: SYNC_LABELED_RE, async: false, bidirectional: false },
|
|
44
|
-
{ re: ASYNC_LABELED_RE, async: true, bidirectional: false },
|
|
64
|
+
{ re: SYNC_LABELED_RE, async: false },
|
|
65
|
+
{ re: ASYNC_LABELED_RE, async: true },
|
|
45
66
|
];
|
|
46
67
|
|
|
47
|
-
for (const { re, async: isAsync
|
|
68
|
+
for (const { re, async: isAsync } of patterns) {
|
|
48
69
|
const m = line.match(re);
|
|
49
70
|
if (!m) continue;
|
|
50
71
|
|
|
@@ -67,7 +88,6 @@ export function parseArrow(
|
|
|
67
88
|
to: m[3],
|
|
68
89
|
label,
|
|
69
90
|
async: isAsync,
|
|
70
|
-
bidirectional,
|
|
71
91
|
};
|
|
72
92
|
}
|
|
73
93
|
|
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[],
|