@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.
@@ -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 — "A -> B: message", "A ~> B: message",
168
- // "A -label-> B", "A ~label~> B", "A <-> B", "A <~> B"
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~>, <-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, bidirectional } = labeledArrow;
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
- // ---- Plain bidi arrows: <-> and <~> ----
570
- // Must be checked BEFORE unidirectional plain arrows
571
- const bidiSyncMatch = trimmed.match(
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 bidiAsyncMatch = trimmed.match(
575
- /^(\S+)\s*<~>\s*([^\s:]+)\s*(?::\s*(.+))?$/
509
+ const colonPostfixAsync = trimmed.match(
510
+ /^(\S+)\s*~>\s*([^\s:]+)\s*:\s*(.+)$/
576
511
  );
577
- const bidiMatch = bidiSyncMatch || bidiAsyncMatch;
578
- if (bidiMatch) {
579
- contentStarted = true;
580
- const from = bidiMatch[1];
581
- const to = bidiMatch[2];
582
- lastMsgFrom = from;
583
- const rawLabel = bidiMatch[3]?.trim() || '';
584
- const isBidiAsync = !!bidiAsyncMatch;
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
- bidirectional: true,
592
- ...(isBidiAsync ? { async: true } : {}),
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
- // Match ~> (async arrow) or -> (sync arrow)
617
- let isAsync = false;
618
- const asyncArrowMatch = trimmed.match(
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
- const arrowMatch = asyncArrowMatch || syncArrowMatch;
625
- if (asyncArrowMatch) isAsync = true;
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
- if (arrowMatch) {
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 = arrowMatch[1];
630
- const to = arrowMatch[2];
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
- ...(isAsync ? { async: true } : {}),
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,
@@ -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: top.returnLabel || '',
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: msg.returnLabel || '',
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: top.returnLabel || '',
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
- const markerStartRef = step.bidirectional
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;
@@ -2,49 +2,70 @@
2
2
  // Shared Arrow Parsing Utility
3
3
  // ============================================================
4
4
  //
5
- // Labeled arrow syntax: `-label->`, `~label~>`, `<-label->`, `<~label~>`
6
- // Used by sequence, C4, and init-status parsers.
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
- // Bidi patterns checked FIRST — longer prefix avoids partial match
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
- const ARROW_CHARS = ['->', '~>', '<->', '<~>'];
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 label contains arrow chars
30
- * - `null` if not a labeled arrow (caller should fall through to plain patterns)
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
- // Order: bidi first (longer prefix), then unidirectional
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: BIDI_SYNC_LABELED_RE, async: false, bidirectional: true },
42
- { re: BIDI_ASYNC_LABELED_RE, async: true, bidirectional: true },
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, bidirectional } of patterns) {
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
 
@@ -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[],