@diagrammo/dgmo 0.8.1 → 0.8.3

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/src/echarts.ts CHANGED
@@ -82,6 +82,7 @@ export interface ParsedExtendedChart {
82
82
  ylabelLineNumber?: number;
83
83
  sizelabel?: string;
84
84
  showLabels?: boolean;
85
+ shade?: boolean;
85
86
  categoryColors?: Record<string, string>;
86
87
  categoryLineNumbers?: Record<string, number>;
87
88
  nodeColors?: Record<string, string>;
@@ -126,8 +127,8 @@ const VALID_EXTENDED_TYPES = new Set<ExtendedChartType>([
126
127
 
127
128
  /** Known option keywords for the extended chart parser. */
128
129
  const KNOWN_EXTENDED_OPTIONS = new Set([
129
- 'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel', 'labels',
130
- 'columns', 'rows', 'x',
130
+ 'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel',
131
+ 'no-labels', 'columns', 'rows', 'x',
131
132
  ]);
132
133
 
133
134
  /**
@@ -140,7 +141,7 @@ function parseScatterRow(
140
141
  currentCategory: string,
141
142
  lineNumber: number,
142
143
  ): ParsedScatterPoint | null {
143
- const dataRow = parseDataRowValues(line);
144
+ const dataRow = parseDataRowValues(line, { multiValue: true });
144
145
  if (!dataRow || dataRow.values.length < 2) return null;
145
146
  const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
146
147
  return {
@@ -376,14 +377,11 @@ export function parseExtendedChart(
376
377
  if (firstToken === 'ylabel') { result.ylabel = value; result.ylabelLineNumber = lineNumber; continue; }
377
378
  if (firstToken === 'sizelabel') { result.sizelabel = value; continue; }
378
379
 
379
- if (firstToken === 'labels') {
380
- result.showLabels = value.toLowerCase() === 'on' || value.toLowerCase() === 'true';
381
- continue;
382
- }
383
-
384
380
  if (firstToken === 'columns') {
385
381
  if (value) {
386
- result.columns = value.split(',').map((s) => s.trim());
382
+ result.columns = value.includes(',')
383
+ ? value.split(',').map((s) => s.trim())
384
+ : value.split(/\s+/);
387
385
  } else {
388
386
  const collected = collectIndentedValues(lines, i);
389
387
  i = collected.newIndex;
@@ -394,7 +392,9 @@ export function parseExtendedChart(
394
392
 
395
393
  if (firstToken === 'rows') {
396
394
  if (value) {
397
- result.rows = value.split(',').map((s) => s.trim());
395
+ result.rows = value.includes(',')
396
+ ? value.split(',').map((s) => s.trim())
397
+ : value.split(/\s+/);
398
398
  } else {
399
399
  const collected = collectIndentedValues(lines, i);
400
400
  i = collected.newIndex;
@@ -415,6 +415,10 @@ export function parseExtendedChart(
415
415
  }
416
416
  }
417
417
 
418
+ // Bare boolean options
419
+ if (firstToken === 'no-labels') { result.showLabels = false; continue; }
420
+ if (firstToken === 'shade') { result.shade = true; continue; }
421
+
418
422
  // Bare keyword options (no value)
419
423
  if (firstToken === 'series' && spaceIdx === -1) {
420
424
  const parsed = parseSeriesNames('', lines, i, palette);
@@ -472,9 +476,9 @@ export function parseExtendedChart(
472
476
  }
473
477
  }
474
478
 
475
- // Heatmap data row: "RowLabel val1, val2, val3, ..."
479
+ // Heatmap data row: "RowLabel val1, val2, val3, ..." or "RowLabel val1 val2 val3"
476
480
  if (result.type === 'heatmap') {
477
- const dataRow = parseDataRowValues(trimmed);
481
+ const dataRow = parseDataRowValues(trimmed, { multiValue: true });
478
482
  if (dataRow && dataRow.values.length > 0) {
479
483
  if (!result.heatmapRows) result.heatmapRows = [];
480
484
  result.heatmapRows.push({ label: dataRow.label, values: dataRow.values, lineNumber });
@@ -918,6 +922,12 @@ function buildFunctionOption(
918
922
  itemStyle: {
919
923
  color: fnColor,
920
924
  },
925
+ ...(parsed.shade && {
926
+ areaStyle: {
927
+ color: fnColor,
928
+ opacity: 0.15,
929
+ },
930
+ }),
921
931
  emphasis: EMPHASIS_SELF,
922
932
  };
923
933
  });
@@ -1287,7 +1297,7 @@ function buildScatterOption(
1287
1297
  const hasCategories = points.some((p) => p.category !== undefined);
1288
1298
  const hasSize = points.some((p) => p.size !== undefined);
1289
1299
 
1290
- const showLabels = parsed.showLabels ?? false;
1300
+ const showLabels = parsed.showLabels ?? true;
1291
1301
  const labelFontSize = 11;
1292
1302
 
1293
1303
  // When showLabels is on, we render labels ourselves via graphic — disable ECharts labels
@@ -2165,13 +2175,23 @@ function buildAreaOption(
2165
2175
 
2166
2176
  // ── Segment label formatter ──────────────────────────────────
2167
2177
 
2168
- function segmentLabelFormatter(mode: ParsedChart['labels']): string {
2169
- switch (mode) {
2170
- case 'name': return '{b}';
2171
- case 'value': return '{b} — {c}';
2172
- case 'percent': return '{b} — {d}%';
2173
- default: return '{b} {c} ({d}%)';
2174
- }
2178
+ function segmentLabelFormatter(parsed: ParsedChart): string {
2179
+ const showName = !parsed.noLabelName;
2180
+ const showValue = !parsed.noLabelValue;
2181
+ const showPercent = !parsed.noLabelPercent;
2182
+
2183
+ const parts: string[] = [];
2184
+ if (showName) parts.push('{b}');
2185
+ if (showValue) parts.push('{c}');
2186
+ if (showPercent) parts.push('{d}%');
2187
+
2188
+ if (parts.length === 0) return '{b}'; // fallback: always show name
2189
+ if (parts.length === 1) return parts[0];
2190
+
2191
+ // Name is joined with " — ", value+percent are grouped with parens when all three
2192
+ if (showName && showValue && showPercent) return '{b} — {c} ({d}%)';
2193
+ if (showName) return '{b} — ' + parts.slice(1).join(' ');
2194
+ return parts.join(' ');
2175
2195
  }
2176
2196
 
2177
2197
  // ── Pie / Doughnut ───────────────────────────────────────────
@@ -2210,7 +2230,7 @@ function buildPieOption(
2210
2230
  data,
2211
2231
  label: {
2212
2232
  position: 'outside',
2213
- formatter: segmentLabelFormatter(parsed.labels),
2233
+ formatter: segmentLabelFormatter(parsed),
2214
2234
  color: textColor,
2215
2235
  fontFamily: FONT_FAMILY,
2216
2236
  },
@@ -2329,7 +2349,7 @@ function buildPolarAreaOption(
2329
2349
  data,
2330
2350
  label: {
2331
2351
  position: 'outside',
2332
- formatter: segmentLabelFormatter(parsed.labels),
2352
+ formatter: segmentLabelFormatter(parsed),
2333
2353
  color: textColor,
2334
2354
  fontFamily: FONT_FAMILY,
2335
2355
  },
package/src/er/parser.ts CHANGED
@@ -243,15 +243,10 @@ export function parseERDiagram(
243
243
  firstLineParsed = true;
244
244
  }
245
245
 
246
- // Tag group heading — `tag Name` or deprecated `## Name`
246
+ // Tag group heading — `tag Name`
247
247
  if (!contentStarted && indent === 0) {
248
248
  const tagBlockMatch = matchTagBlockHeading(trimmed);
249
249
  if (tagBlockMatch) {
250
- if (tagBlockMatch.deprecated) {
251
- result.diagnostics.push(makeDgmoError(lineNumber,
252
- `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
253
- continue;
254
- }
255
250
  currentTagGroup = {
256
251
  name: tagBlockMatch.name,
257
252
  alias: tagBlockMatch.alias,
@@ -337,19 +332,16 @@ export function parseERDiagram(
337
332
  currentTable = null;
338
333
  contentStarted = true;
339
334
 
340
- // Try relationship
335
+ // Reject top-level relationships — must be indented under source table
341
336
  const rel = parseRelationship(trimmed, lineNumber, pushError);
342
337
  if (rel) {
343
- getOrCreateTable(rel.source, lineNumber);
344
- getOrCreateTable(rel.target, lineNumber);
345
-
346
- result.relationships.push({
347
- source: tableId(rel.source),
348
- target: tableId(rel.target),
349
- cardinality: { from: rel.from, to: rel.to },
350
- ...(rel.label && { label: rel.label }),
351
- lineNumber,
352
- });
338
+ result.diagnostics.push(
339
+ makeDgmoError(
340
+ lineNumber,
341
+ `Relationship "${rel.source} → ${rel.target}" must be indented under the source table "${rel.source}"`,
342
+ 'warning',
343
+ ),
344
+ );
353
345
  continue;
354
346
  }
355
347
 
@@ -6,7 +6,7 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import type { TagGroup, TagEntry } from '../utils/tag-groups';
8
8
  import { matchTagBlockHeading } from '../utils/tag-groups';
9
- import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING, parseFirstLine, prescanOptions, GROUP_HASH_RE } from '../utils/parsing';
9
+ import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_ERROR, parseFirstLine, prescanOptions } from '../utils/parsing';
10
10
  import { parseOffset } from '../utils/duration';
11
11
  import type { PaletteColors } from '../palettes';
12
12
  import { resolveColor } from '../colors';
@@ -37,7 +37,7 @@ const DURATION_RE = /^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h)(\?)?\s+(.+)$/;
37
37
  const EXPLICIT_DATE_RE = /^(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)\s+(.+)$/;
38
38
 
39
39
  /** Timeline migration syntax: `2024-01-15 -> 30d Label` or `2024-01-15 14:30 -> 2h Label` */
40
- const TIMELINE_DURATION_RE = /^(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)\s*->\s*(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h)(\?)?\s+(.+)$/;
40
+ const TIMELINE_DURATION_RE = /^(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h)(\?)?\s+(.+)$/;
41
41
 
42
42
  /** Group container: `[GroupName]` with optional pipe metadata */
43
43
  const GROUP_RE = /^\[(.+?)\]\s*(.*)$/;
@@ -49,7 +49,7 @@ const DEPENDENCY_RE = /^(?:-(.+?))?->\s*(.+)$/;
49
49
  const COMMENT_RE = /^\/\//;
50
50
 
51
51
  /** Era: `era YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
52
- const ERA_RE = /^era\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s*->\s*(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
52
+ const ERA_RE = /^era\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
53
53
 
54
54
  /** Marker: `marker YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
55
55
  const MARKER_RE = /^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
@@ -58,11 +58,17 @@ const MARKER_RE = /^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.
58
58
  const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/;
59
59
 
60
60
  /** Holiday range: `2024-12-24 -> 2024-12-31 Label` */
61
- const HOLIDAY_RANGE_RE = /^(\d{4}-\d{2}-\d{2})\s*->\s*(\d{4}-\d{2}-\d{2})\s+(.+)$/;
61
+ const HOLIDAY_RANGE_RE = /^(\d{4}-\d{2}-\d{2})\s*(?:->|\u2013>)\s*(\d{4}-\d{2}-\d{2})\s+(.+)$/;
62
62
 
63
63
  /** Workweek override: `workweek sun-thu` */
64
64
  const WORKWEEK_RE = /^workweek\s+(.+)$/i;
65
65
 
66
+ /** Era entry (inside era block, no `era` prefix): `YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label` */
67
+ const ERA_ENTRY_RE = /^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
68
+
69
+ /** Marker entry (inside marker block, no `marker` prefix): `YYYY[-MM[-DD[ HH:MM]]] Label` */
70
+ const MARKER_ENTRY_RE = /^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
71
+
66
72
  // Valid weekday names
67
73
  const WEEKDAY_MAP: Record<string, Weekday> = {
68
74
  mon: 'mon', tue: 'tue', wed: 'wed', thu: 'thu', fri: 'fri', sat: 'sat', sun: 'sun',
@@ -156,6 +162,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
156
162
  let inTagBlock = false;
157
163
  let currentTagGroup: TagGroup | null = null;
158
164
  let tagBlockIndent = 0;
165
+ let inEraBlock = false;
166
+ let eraBlockIndent = 0;
167
+ let inMarkerBlock = false;
168
+ let markerBlockIndent = 0;
159
169
  let lastTaskNode: (GanttNode & { kind: 'task' }) | null = null;
160
170
  let taskIdCounter = 0;
161
171
  const seriesColors = palette ? getSeriesColors(palette) : [];
@@ -170,10 +180,16 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
170
180
 
171
181
  // Skip empty lines
172
182
  if (!line) {
173
- // Empty line ends holidays/tag blocks only if at root indent
183
+ // Empty line ends holidays/tag/era/marker blocks only if at root indent
174
184
  if (inHolidaysBlock && indent <= holidaysBlockIndent) {
175
185
  inHolidaysBlock = false;
176
186
  }
187
+ if (inEraBlock && indent <= eraBlockIndent) {
188
+ inEraBlock = false;
189
+ }
190
+ if (inMarkerBlock && indent <= markerBlockIndent) {
191
+ inMarkerBlock = false;
192
+ }
177
193
  if (inTagBlock && indent <= tagBlockIndent) {
178
194
  inTagBlock = false;
179
195
  if (currentTagGroup) {
@@ -249,6 +265,57 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
249
265
  }
250
266
  }
251
267
 
268
+ // ── Era block entries ─────────────────────────────────
269
+
270
+ if (inEraBlock) {
271
+ if (indent <= eraBlockIndent) {
272
+ inEraBlock = false;
273
+ // fall through to process this line normally
274
+ } else {
275
+ if (COMMENT_RE.test(line)) continue;
276
+ const eraEntryMatch = line.match(ERA_ENTRY_RE);
277
+ if (eraEntryMatch) {
278
+ const eraLabelRaw = eraEntryMatch[3].trim();
279
+ const eraExtracted = extractColor(eraLabelRaw, palette);
280
+ result.eras.push({
281
+ startDate: eraEntryMatch[1],
282
+ endDate: eraEntryMatch[2],
283
+ label: eraExtracted.label,
284
+ color: eraExtracted.color || null,
285
+ lineNumber,
286
+ });
287
+ } else {
288
+ warn(lineNumber, `Unrecognized era entry: "${line}"`);
289
+ }
290
+ continue;
291
+ }
292
+ }
293
+
294
+ // ── Marker block entries ─────────────────────────────
295
+
296
+ if (inMarkerBlock) {
297
+ if (indent <= markerBlockIndent) {
298
+ inMarkerBlock = false;
299
+ // fall through to process this line normally
300
+ } else {
301
+ if (COMMENT_RE.test(line)) continue;
302
+ const markerEntryMatch = line.match(MARKER_ENTRY_RE);
303
+ if (markerEntryMatch) {
304
+ const markerLabelRaw = markerEntryMatch[2].trim();
305
+ const markerExtracted = extractColor(markerLabelRaw, palette);
306
+ result.markers.push({
307
+ date: markerEntryMatch[1],
308
+ label: markerExtracted.label,
309
+ color: markerExtracted.color || null,
310
+ lineNumber,
311
+ });
312
+ } else {
313
+ warn(lineNumber, `Unrecognized marker entry: "${line}"`);
314
+ }
315
+ continue;
316
+ }
317
+ }
318
+
252
319
  // ── Tag block entries ─────────────────────────────────
253
320
 
254
321
  if (inTagBlock && currentTagGroup) {
@@ -302,7 +369,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
302
369
  let offset: Offset | undefined;
303
370
 
304
371
  if (depParts.length > 1) {
305
- const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
372
+ const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR));
306
373
  if (meta.lag || meta.lead) {
307
374
  const key = meta.lag ? 'lag' : 'lead';
308
375
  softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
@@ -369,10 +436,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
369
436
  // Tag block heading
370
437
  const tagMatch = matchTagBlockHeading(line);
371
438
  if (tagMatch) {
372
- if (tagMatch.deprecated) {
373
- softError(lineNumber, `'## ${tagMatch.name}' is no longer supported — use 'tag ${tagMatch.name}' instead`);
374
- continue;
375
- }
376
439
  inTagBlock = true;
377
440
  tagBlockIndent = indent;
378
441
  inHeaderBlock = false;
@@ -388,7 +451,28 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
388
451
  continue;
389
452
  }
390
453
 
391
- // Era
454
+ // Top-level workweek (outside holiday block)
455
+ const topWorkweekMatch = line.match(WORKWEEK_RE);
456
+ if (topWorkweekMatch) {
457
+ const days = parseWorkweek(topWorkweekMatch[1].trim());
458
+ if (days) {
459
+ result.holidays.workweek = days;
460
+ } else {
461
+ warn(lineNumber, `Invalid workweek format: "${topWorkweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`);
462
+ }
463
+ inHeaderBlock = false;
464
+ continue;
465
+ }
466
+
467
+ // Era block: bare `era` keyword starts a block
468
+ if (line.toLowerCase() === 'era') {
469
+ inEraBlock = true;
470
+ eraBlockIndent = indent;
471
+ inHeaderBlock = false;
472
+ continue;
473
+ }
474
+
475
+ // Era (inline)
392
476
  const eraMatch = line.match(ERA_RE);
393
477
  if (eraMatch) {
394
478
  const eraLabelRaw = eraMatch[3].trim();
@@ -404,7 +488,15 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
404
488
  continue;
405
489
  }
406
490
 
407
- // Marker
491
+ // Marker block: bare `marker` keyword starts a block
492
+ if (line.toLowerCase() === 'marker') {
493
+ inMarkerBlock = true;
494
+ markerBlockIndent = indent;
495
+ inHeaderBlock = false;
496
+ continue;
497
+ }
498
+
499
+ // Marker (inline)
408
500
  const markerMatch = line.match(MARKER_RE);
409
501
  if (markerMatch) {
410
502
  const markerLabelRaw = markerMatch[2].trim();
@@ -472,9 +564,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
472
564
  result.options.title = value;
473
565
  result.options.titleLineNumber = lineNumber;
474
566
  break;
475
- case 'orientation':
476
- warn(lineNumber, `'orientation' is not supported for gantt charts`);
477
- break;
478
567
  case 'today-marker':
479
568
  if (/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?$/.test(value)) {
480
569
  result.options.todayMarker = value;
@@ -506,28 +595,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
506
595
 
507
596
  inHeaderBlock = false;
508
597
 
509
- // ── `# Group` alternate syntax ──────────────────────────
510
598
 
511
- const hashGroupMatch = line.match(GROUP_HASH_RE);
512
- if (hashGroupMatch) {
513
- const nameExtracted = extractColor(hashGroupMatch[1], palette);
514
- const group: GanttGroup = {
515
- name: nameExtracted.label,
516
- color: nameExtracted.color || null,
517
- metadata: {},
518
- lineNumber,
519
- children: [],
520
- };
521
- const groupNode: GanttNode = { kind: 'group', ...group };
522
- currentContainer().push(groupNode);
523
- blockStack.push({
524
- node: groupNode as GanttGroup,
525
- indent,
526
- containerType: 'group',
527
- });
528
- lastTaskNode = null;
529
- continue;
530
- }
531
599
 
532
600
  // ── Parallel block ────────────────────────────────────
533
601
 
@@ -560,7 +628,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
560
628
  let metadata: Record<string, string> = {};
561
629
  let color: string | null = null;
562
630
 
563
- const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_WARNING);
631
+ const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_ERROR);
564
632
  if (segments.length > 0 && segments[0].trim()) {
565
633
  // Check if first segment after brackets is pipe metadata
566
634
  metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
@@ -662,7 +730,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
662
730
  let offset: Offset | undefined;
663
731
 
664
732
  if (depParts.length > 1) {
665
- const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
733
+ const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR));
666
734
  if (meta.lag || meta.lead) {
667
735
  const key = meta.lag ? 'lag' : 'lead';
668
736
  softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
@@ -726,7 +794,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
726
794
 
727
795
  // Parse pipe metadata
728
796
  const metadata = segments.length > 1
729
- ? parsePipeMetadata(segments, aliasMap, () => warn(ln, MULTIPLE_PIPE_WARNING))
797
+ ? parsePipeMetadata(segments, aliasMap, () => warn(ln, MULTIPLE_PIPE_ERROR))
730
798
  : {};
731
799
 
732
800
  // Extract progress from metadata or shorthand
@@ -833,7 +901,7 @@ function parseWorkweek(s: string): Weekday[] | null {
833
901
  // ── Known option keys ─────────────────────────────────────
834
902
 
835
903
  const KNOWN_OPTIONS = new Set([
836
- 'start', 'title', 'orientation', 'today-marker',
904
+ 'start', 'title', 'today-marker',
837
905
  'critical-path', 'dependencies', 'chart', 'sort',
838
906
  ]);
839
907
 
@@ -4,19 +4,15 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
4
  import {
5
5
  measureIndent,
6
6
  extractColor,
7
- normalizeDirection,
8
7
  inferArrowColor,
9
8
  parseFirstLine,
10
9
  OPTION_NOCOLON_RE,
11
- GROUP_HASH_RE,
12
- DOUBLE_HASH_RE,
13
10
  ALL_CHART_TYPES,
14
11
  } from '../utils/parsing';
15
12
  import type {
16
13
  ParsedGraph,
17
14
  GraphNode,
18
15
  GraphEdge,
19
- GraphGroup,
20
16
  GraphShape,
21
17
  GraphDirection,
22
18
  } from './types';
@@ -218,7 +214,7 @@ export function parseFlowchart(
218
214
  const lines = content.split('\n');
219
215
  const result: ParsedGraph = {
220
216
  type: 'flowchart',
221
- direction: 'TB',
217
+ direction: 'LR',
222
218
  nodes: [],
223
219
  edges: [],
224
220
  options: {},
@@ -238,11 +234,6 @@ export function parseFlowchart(
238
234
  let contentStarted = false;
239
235
  let firstLineParsed = false;
240
236
 
241
- // Group support
242
- let currentGroup: GraphGroup | null = null;
243
- let groupIndent = -1;
244
- const groups: GraphGroup[] = [];
245
-
246
237
  function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
247
238
  const existing = nodeMap.get(ref.id);
248
239
  if (existing) return existing;
@@ -253,15 +244,10 @@ export function parseFlowchart(
253
244
  shape: ref.shape,
254
245
  lineNumber,
255
246
  ...(ref.color && { color: ref.color }),
256
- ...(currentGroup && { group: currentGroup.id }),
257
247
  };
258
248
  nodeMap.set(ref.id, node);
259
249
  result.nodes.push(node);
260
250
 
261
- if (currentGroup && !currentGroup.nodeIds.includes(ref.id)) {
262
- currentGroup.nodeIds.push(ref.id);
263
- }
264
-
265
251
  return node;
266
252
  }
267
253
 
@@ -418,45 +404,19 @@ export function parseFlowchart(
418
404
  }
419
405
  }
420
406
 
421
- // ## group headings — emit helpful error
422
- if (DOUBLE_HASH_RE.test(trimmed)) {
423
- result.diagnostics.push(
424
- makeDgmoError(lineNumber, 'Use `#` for groups \u2014 nesting is done with indentation.', 'error')
425
- );
426
- continue;
427
- }
428
-
429
- // # GroupName — alternate group notation
430
- const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
431
- if (hashGroupMatch) {
432
- const { label, color } = extractColor(hashGroupMatch[1].trim(), palette);
433
- currentGroup = {
434
- id: `group:${label.toLowerCase()}`,
435
- label,
436
- nodeIds: [],
437
- lineNumber,
438
- ...(color && { color }),
439
- };
440
- groupIndent = indent;
441
- groups.push(currentGroup);
442
- continue;
443
- }
444
-
445
407
  // Options (space-separated, before content)
446
408
  if (!contentStarted) {
409
+ // Bare boolean: direction-tb
410
+ if (/^direction-tb$/i.test(trimmed)) {
411
+ result.direction = 'TB';
412
+ continue;
413
+ }
414
+
447
415
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
448
416
  if (optMatch && !trimmed.includes('->')) {
449
417
  const key = optMatch[1].toLowerCase();
450
418
  const value = optMatch[2].trim();
451
419
 
452
- if (key === 'direction' || key === 'orientation') {
453
- const dir = normalizeDirection(value);
454
- if (dir) {
455
- result.direction = dir;
456
- }
457
- continue;
458
- }
459
-
460
420
  // Boolean: no-color = color off
461
421
  if (key === 'no-color') {
462
422
  result.options['color'] = 'off';
@@ -469,18 +429,10 @@ export function parseFlowchart(
469
429
  }
470
430
  }
471
431
 
472
- // Close current group when indent returns to or below the group level
473
- if (currentGroup && indent <= groupIndent) {
474
- currentGroup = null;
475
- groupIndent = -1;
476
- }
477
-
478
432
  // Content line (nodes and edges)
479
433
  processContentLine(trimmed, lineNumber, indent);
480
434
  }
481
435
 
482
- if (groups.length > 0) result.groups = groups;
483
-
484
436
  // Validation: no nodes found
485
437
  if (result.nodes.length === 0 && !result.error) {
486
438
  const diag = makeDgmoError(1, 'No nodes found. Add flowchart content with shape syntax like [Process] or (Start).');
@@ -4,7 +4,6 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
4
  import {
5
5
  measureIndent,
6
6
  extractColor,
7
- normalizeDirection,
8
7
  parseFirstLine,
9
8
  OPTION_NOCOLON_RE,
10
9
  ALL_CHART_TYPES,
@@ -154,7 +153,7 @@ export function parseState(
154
153
  const lines = content.split('\n');
155
154
  const result: ParsedGraph = {
156
155
  type: 'state',
157
- direction: 'TB',
156
+ direction: 'LR',
158
157
  nodes: [],
159
158
  edges: [],
160
159
  options: {},
@@ -268,19 +267,17 @@ export function parseState(
268
267
 
269
268
  // Options (space-separated, before content)
270
269
  if (!contentStarted) {
270
+ // Bare boolean: direction-tb
271
+ if (/^direction-tb$/i.test(trimmed)) {
272
+ result.direction = 'TB';
273
+ continue;
274
+ }
275
+
271
276
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
272
277
  if (optMatch && !trimmed.includes('->')) {
273
278
  const key = optMatch[1].toLowerCase();
274
279
  const value = optMatch[2].trim();
275
280
 
276
- if (key === 'direction' || key === 'orientation') {
277
- const dir = normalizeDirection(value);
278
- if (dir) {
279
- result.direction = dir;
280
- }
281
- continue;
282
- }
283
-
284
281
  // Boolean: no-color = color off
285
282
  if (key === 'no-color') {
286
283
  result.options['color'] = 'off';