@diagrammo/dgmo 0.7.0 → 0.7.2

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.
@@ -93,6 +93,7 @@ export interface GanttEra {
93
93
  endDate: string;
94
94
  label: string;
95
95
  color: string | null;
96
+ lineNumber: number;
96
97
  }
97
98
 
98
99
  export interface GanttMarker {
@@ -114,6 +115,9 @@ export interface GanttOptions {
114
115
  dependencies: boolean;
115
116
  sort: 'default' | 'tag';
116
117
  defaultSwimlaneGroup: string | null; // tag group name from `sort: tag:Team`
118
+ /** Line numbers for option/block keywords — maps key to source line */
119
+ optionLineNumbers: Record<string, number>;
120
+ holidaysLineNumber: number | null;
117
121
  }
118
122
 
119
123
  // ── Parsed Result ───────────────────────────────────────────
package/src/org/parser.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  measureIndent,
8
8
  extractColor,
9
9
  parsePipeMetadata,
10
+ MULTIPLE_PIPE_WARNING,
10
11
  CHART_TYPE_RE,
11
12
  TITLE_RE,
12
13
  OPTION_RE,
@@ -280,14 +281,14 @@ export function parseOrg(
280
281
  // Otherwise it's an orphan metadata error
281
282
  if (indent === 0) {
282
283
  // Treat as a node label (e.g., "Dr. Smith: Surgeon" is a valid name)
283
- const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap);
284
+ const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
284
285
  attachNode(node, indent, indentStack, result);
285
286
  } else {
286
287
  pushError(lineNumber, 'Metadata has no parent node');
287
288
  }
288
289
  } else {
289
290
  // It's a node label — possibly with single-line pipe-delimited metadata
290
- const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap);
291
+ const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
291
292
  attachNode(node, indent, indentStack, result);
292
293
  }
293
294
  }
@@ -326,15 +327,16 @@ function parseNodeLabel(
326
327
  lineNumber: number,
327
328
  palette: PaletteColors | undefined,
328
329
  counter: number,
329
- aliasMap: Map<string, string> = new Map()
330
+ aliasMap: Map<string, string> = new Map(),
331
+ warnFn?: (line: number, msg: string) => void,
330
332
  ): OrgNode {
331
- // Check for single-line compact metadata: "Alice Park | role: Senior | location: NY"
333
+ // Check for single-line compact metadata: "Alice Park | role: Senior, location: NY"
332
334
  const segments = trimmed.split('|').map((s) => s.trim());
333
335
 
334
336
  let rawLabel = segments[0];
335
337
  const { label, color } = extractColor(rawLabel, palette);
336
338
 
337
- const metadata = parsePipeMetadata(segments, aliasMap);
339
+ const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
338
340
 
339
341
  return {
340
342
  id: `node-${counter}`,
@@ -6,7 +6,7 @@ import { inferParticipantType } from './participant-inference';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
8
  import { parseArrow } from '../utils/arrows';
9
- import { measureIndent, extractColor, parsePipeMetadata } from '../utils/parsing';
9
+ import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING } from '../utils/parsing';
10
10
  import type { TagGroup } from '../utils/tag-groups';
11
11
  import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
12
12
 
@@ -237,12 +237,13 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
237
237
  const aliasMap = new Map<string, string>();
238
238
 
239
239
  /** Split pipe metadata from a line: "core | k: v" → { core, meta } */
240
- const splitPipe = (text: string): { core: string; meta?: Record<string, string> } => {
240
+ const splitPipe = (text: string, ln?: number): { core: string; meta?: Record<string, string> } => {
241
241
  const idx = text.indexOf('|');
242
242
  if (idx < 0) return { core: text };
243
243
  const core = text.substring(0, idx).trimEnd();
244
244
  const segments = text.substring(idx).split('|');
245
- const meta = parsePipeMetadata(segments, aliasMap);
245
+ const warnFn = ln != null ? () => pushWarning(ln, MULTIPLE_PIPE_WARNING) : undefined;
246
+ const meta = parsePipeMetadata(segments, aliasMap, warnFn);
246
247
  return Object.keys(meta).length > 0 ? { core, meta } : { core };
247
248
  };
248
249
 
@@ -287,7 +288,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
287
288
  if (gpipeIdx >= 0) {
288
289
  const nameAndColor = groupName.substring(0, gpipeIdx).trimEnd();
289
290
  const segments = groupName.substring(gpipeIdx).split('|');
290
- const meta = parsePipeMetadata(segments, aliasMap);
291
+ const meta = parsePipeMetadata(segments, aliasMap, () => pushWarning(lineNumber, MULTIPLE_PIPE_WARNING));
291
292
  if (Object.keys(meta).length > 0) groupMeta = meta;
292
293
  // Re-extract color from name part
293
294
  const colorSuffix = nameAndColor.match(/^(.+?)\(([^)]+)\)$/);
@@ -444,7 +445,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
444
445
  }
445
446
 
446
447
  // Parse "Name is a type [aka Alias]" declarations (always top-level)
447
- const { core: isACore, meta: isAMeta } = splitPipe(trimmed);
448
+ const { core: isACore, meta: isAMeta } = splitPipe(trimmed, lineNumber);
448
449
  const isAMatch = isACore.match(IS_A_PATTERN);
449
450
  if (isAMatch) {
450
451
  contentStarted = true;
@@ -491,7 +492,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
491
492
  }
492
493
 
493
494
  // Parse standalone "Name position N" (no "is a" type)
494
- const { core: posCore, meta: posMeta } = splitPipe(trimmed);
495
+ const { core: posCore, meta: posMeta } = splitPipe(trimmed, lineNumber);
495
496
  const posOnlyMatch = posCore.match(POSITION_ONLY_PATTERN);
496
497
  if (posOnlyMatch) {
497
498
  contentStarted = true;
@@ -523,7 +524,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
523
524
 
524
525
  // Colored participant declaration — "Name(color)" at any level
525
526
  // Color syntax is deprecated — emit warning and register without color
526
- const { core: colorCore, meta: colorMeta } = splitPipe(trimmed);
527
+ const { core: colorCore, meta: colorMeta } = splitPipe(trimmed, lineNumber);
527
528
  const coloredMatch = colorCore.match(COLORED_PARTICIPANT_PATTERN);
528
529
  if (coloredMatch && !ARROW_PATTERN.test(colorCore)) {
529
530
  const id = coloredMatch[1];
@@ -554,7 +555,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
554
555
  // Bare participant name — either inside an active group (indented) or top-level declaration
555
556
  // Supports pipe metadata: " API | c: Gateway" or "Tapin2 | l:Park"
556
557
  {
557
- const { core: bareCore, meta: bareMeta } = splitPipe(trimmed);
558
+ const { core: bareCore, meta: bareMeta } = splitPipe(trimmed, lineNumber);
558
559
  const inGroup = activeGroup && measureIndent(raw) > 0;
559
560
  if (/^\S+$/.test(bareCore) && !ARROW_PATTERN.test(bareCore) && (inGroup || !contentStarted || bareMeta)) {
560
561
  contentStarted = true;
@@ -600,7 +601,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
600
601
  }
601
602
 
602
603
  // Split pipe metadata before arrow parsing (arrows use $ anchor)
603
- const { core: arrowCore, meta: arrowMeta } = splitPipe(trimmed);
604
+ const { core: arrowCore, meta: arrowMeta } = splitPipe(trimmed, lineNumber);
604
605
 
605
606
  // Parse message lines first — arrows take priority over keywords
606
607
  // Reject "async" keyword prefix — use ~> instead
@@ -11,6 +11,7 @@ import {
11
11
  measureIndent,
12
12
  extractColor,
13
13
  parsePipeMetadata,
14
+ MULTIPLE_PIPE_WARNING,
14
15
  CHART_TYPE_RE,
15
16
  TITLE_RE,
16
17
  OPTION_RE,
@@ -360,7 +361,7 @@ export function parseSitemap(
360
361
  } else if (metadataMatch && indentStack.length === 0) {
361
362
  // Could be a node label containing ':'
362
363
  if (indent === 0) {
363
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap);
364
+ const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
364
365
  attachNode(node, indent, indentStack, result);
365
366
  labelToNode.set(node.label.toLowerCase(), node);
366
367
  } else {
@@ -368,7 +369,7 @@ export function parseSitemap(
368
369
  }
369
370
  } else {
370
371
  // Node label — possibly with pipe-delimited metadata
371
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap);
372
+ const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
372
373
  attachNode(node, indent, indentStack, result);
373
374
  labelToNode.set(node.label.toLowerCase(), node);
374
375
  }
@@ -430,11 +431,12 @@ function parseNodeLabel(
430
431
  palette: PaletteColors | undefined,
431
432
  counter: number,
432
433
  aliasMap: Map<string, string> = new Map(),
434
+ warnFn?: (line: number, msg: string) => void,
433
435
  ): SitemapNode {
434
436
  const segments = trimmed.split('|').map((s) => s.trim());
435
437
  const rawLabel = segments[0];
436
438
  const { label, color } = extractColor(rawLabel, palette);
437
- const metadata = parsePipeMetadata(segments, aliasMap);
439
+ const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
438
440
 
439
441
  return {
440
442
  id: `node-${counter}`,
@@ -118,23 +118,34 @@ export function parseSeriesNames(
118
118
  return { series, names, nameColors, newIndex };
119
119
  }
120
120
 
121
- /** Parse pipe-delimited metadata from segments after the first (name) segment. */
121
+ /** Warning message for multiple pipes on a single line. */
122
+ export const MULTIPLE_PIPE_WARNING =
123
+ 'Use a single "|" to start metadata, then separate items with commas.';
124
+
125
+ /**
126
+ * Parse metadata from segments after the first (name) segment.
127
+ * A single `|` separates the label from metadata; items after the pipe are comma-delimited.
128
+ * Multiple pipes are treated as commas for backward compatibility but trigger a warning.
129
+ */
122
130
  export function parsePipeMetadata(
123
131
  segments: string[],
124
132
  aliasMap: Map<string, string> = new Map(),
133
+ warnMultiplePipes?: () => void,
125
134
  ): Record<string, string> {
135
+ if (segments.length > 2 && warnMultiplePipes) {
136
+ warnMultiplePipes();
137
+ }
126
138
  const metadata: Record<string, string> = {};
127
- for (let j = 1; j < segments.length; j++) {
128
- for (const part of segments[j].split(',')) {
129
- const trimmedPart = part.trim();
130
- if (!trimmedPart) continue;
131
- const colonIdx = trimmedPart.indexOf(':');
132
- if (colonIdx > 0) {
133
- const rawKey = trimmedPart.substring(0, colonIdx).trim().toLowerCase();
134
- const key = aliasMap.get(rawKey) ?? rawKey;
135
- const value = trimmedPart.substring(colonIdx + 1).trim();
136
- metadata[key] = value;
137
- }
139
+ const raw = segments.slice(1).join(',');
140
+ for (const part of raw.split(',')) {
141
+ const trimmedPart = part.trim();
142
+ if (!trimmedPart) continue;
143
+ const colonIdx = trimmedPart.indexOf(':');
144
+ if (colonIdx > 0) {
145
+ const rawKey = trimmedPart.substring(0, colonIdx).trim().toLowerCase();
146
+ const key = aliasMap.get(rawKey) ?? rawKey;
147
+ const value = trimmedPart.substring(colonIdx + 1).trim();
148
+ metadata[key] = value;
138
149
  }
139
150
  }
140
151
  return metadata;