@diagrammo/dgmo 0.8.3 → 0.8.5
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/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +452 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +188 -185
- package/dist/editor.cjs +338 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +307 -0
- package/dist/editor.js.map +1 -0
- package/dist/highlight.cjs +560 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +530 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3467 -1078
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +3466 -1078
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +46 -37
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +9 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +71 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +211 -62
- package/src/completion.ts +378 -183
- package/src/d3.ts +1044 -303
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +222 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/index.ts +96 -31
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/gantt/parser.ts
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
// Gantt Chart Parser
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
-
import { makeDgmoError, formatDgmoError
|
|
5
|
+
import { makeDgmoError, formatDgmoError } from '../diagnostics';
|
|
6
6
|
import type { DgmoError } from '../diagnostics';
|
|
7
|
-
import type { TagGroup
|
|
7
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
8
8
|
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
measureIndent,
|
|
11
|
+
extractColor,
|
|
12
|
+
parsePipeMetadata,
|
|
13
|
+
MULTIPLE_PIPE_ERROR,
|
|
14
|
+
parseFirstLine,
|
|
15
|
+
} from '../utils/parsing';
|
|
10
16
|
import { parseOffset } from '../utils/duration';
|
|
11
17
|
import type { PaletteColors } from '../palettes';
|
|
12
|
-
import { resolveColor } from '../colors';
|
|
13
18
|
import { getSeriesColors } from '../palettes';
|
|
14
19
|
import type {
|
|
15
20
|
ParsedGantt,
|
|
@@ -17,11 +22,6 @@ import type {
|
|
|
17
22
|
GanttTask,
|
|
18
23
|
GanttGroup,
|
|
19
24
|
GanttParallelBlock,
|
|
20
|
-
GanttDependency,
|
|
21
|
-
GanttHolidays,
|
|
22
|
-
GanttEra,
|
|
23
|
-
GanttMarker,
|
|
24
|
-
GanttOptions,
|
|
25
25
|
Duration,
|
|
26
26
|
DurationUnit,
|
|
27
27
|
Offset,
|
|
@@ -37,7 +37,8 @@ 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 =
|
|
40
|
+
const TIMELINE_DURATION_RE =
|
|
41
|
+
/^(\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
42
|
|
|
42
43
|
/** Group container: `[GroupName]` with optional pipe metadata */
|
|
43
44
|
const GROUP_RE = /^\[(.+?)\]\s*(.*)$/;
|
|
@@ -49,30 +50,47 @@ const DEPENDENCY_RE = /^(?:-(.+?))?->\s*(.+)$/;
|
|
|
49
50
|
const COMMENT_RE = /^\/\//;
|
|
50
51
|
|
|
51
52
|
/** Era: `era YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
|
|
52
|
-
const ERA_RE =
|
|
53
|
+
const ERA_RE =
|
|
54
|
+
/^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
55
|
|
|
54
56
|
/** Marker: `marker YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
|
|
55
|
-
const MARKER_RE =
|
|
57
|
+
const MARKER_RE =
|
|
58
|
+
/^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
|
|
56
59
|
|
|
57
60
|
/** Holiday date: `2024-01-15 Label` */
|
|
58
61
|
const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/;
|
|
59
62
|
|
|
60
63
|
/** Holiday range: `2024-12-24 -> 2024-12-31 Label` */
|
|
61
|
-
const HOLIDAY_RANGE_RE =
|
|
64
|
+
const HOLIDAY_RANGE_RE =
|
|
65
|
+
/^(\d{4}-\d{2}-\d{2})\s*(?:->|\u2013>)\s*(\d{4}-\d{2}-\d{2})\s+(.+)$/;
|
|
62
66
|
|
|
63
67
|
/** Workweek override: `workweek sun-thu` */
|
|
64
68
|
const WORKWEEK_RE = /^workweek\s+(.+)$/i;
|
|
65
69
|
|
|
66
70
|
/** Era entry (inside era block, no `era` prefix): `YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
67
|
-
const ERA_ENTRY_RE =
|
|
71
|
+
const ERA_ENTRY_RE =
|
|
72
|
+
/^(\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
73
|
|
|
69
74
|
/** Marker entry (inside marker block, no `marker` prefix): `YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
70
|
-
const MARKER_ENTRY_RE =
|
|
75
|
+
const MARKER_ENTRY_RE =
|
|
76
|
+
/^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
|
|
71
77
|
|
|
72
78
|
// Valid weekday names
|
|
73
79
|
const WEEKDAY_MAP: Record<string, Weekday> = {
|
|
74
|
-
mon: 'mon',
|
|
75
|
-
|
|
80
|
+
mon: 'mon',
|
|
81
|
+
tue: 'tue',
|
|
82
|
+
wed: 'wed',
|
|
83
|
+
thu: 'thu',
|
|
84
|
+
fri: 'fri',
|
|
85
|
+
sat: 'sat',
|
|
86
|
+
sun: 'sun',
|
|
87
|
+
monday: 'mon',
|
|
88
|
+
tuesday: 'tue',
|
|
89
|
+
wednesday: 'wed',
|
|
90
|
+
thursday: 'thu',
|
|
91
|
+
friday: 'fri',
|
|
92
|
+
saturday: 'sat',
|
|
93
|
+
sunday: 'sun',
|
|
76
94
|
};
|
|
77
95
|
|
|
78
96
|
// ── Block Stack ─────────────────────────────────────────────
|
|
@@ -87,13 +105,20 @@ interface BlockEntry {
|
|
|
87
105
|
|
|
88
106
|
// ── Parser ──────────────────────────────────────────────────
|
|
89
107
|
|
|
90
|
-
export function parseGantt(
|
|
108
|
+
export function parseGantt(
|
|
109
|
+
content: string,
|
|
110
|
+
palette?: PaletteColors
|
|
111
|
+
): ParsedGantt {
|
|
91
112
|
const lines = content.split('\n');
|
|
92
113
|
const diagnostics: DgmoError[] = [];
|
|
93
114
|
|
|
94
115
|
const result: ParsedGantt = {
|
|
95
116
|
nodes: [],
|
|
96
|
-
holidays: {
|
|
117
|
+
holidays: {
|
|
118
|
+
dates: [],
|
|
119
|
+
ranges: [],
|
|
120
|
+
workweek: ['mon', 'tue', 'wed', 'thu', 'fri'],
|
|
121
|
+
},
|
|
97
122
|
tagGroups: [],
|
|
98
123
|
eras: [],
|
|
99
124
|
markers: [],
|
|
@@ -156,7 +181,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
156
181
|
// ── State ───────────────────────────────────────────────
|
|
157
182
|
|
|
158
183
|
let seenChartType = false;
|
|
159
|
-
let inHeaderBlock = true; // options must come before content
|
|
160
184
|
let inHolidaysBlock = false;
|
|
161
185
|
let holidaysBlockIndent = 0;
|
|
162
186
|
let inTagBlock = false;
|
|
@@ -206,7 +230,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
206
230
|
const firstLineResult = parseFirstLine(line);
|
|
207
231
|
if (firstLineResult) {
|
|
208
232
|
if (firstLineResult.chartType !== 'gantt') {
|
|
209
|
-
return fail(
|
|
233
|
+
return fail(
|
|
234
|
+
lineNumber,
|
|
235
|
+
`Expected chart type "gantt", got "${firstLineResult.chartType}"`
|
|
236
|
+
);
|
|
210
237
|
}
|
|
211
238
|
seenChartType = true;
|
|
212
239
|
if (firstLineResult.title) {
|
|
@@ -252,7 +279,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
252
279
|
if (days) {
|
|
253
280
|
result.holidays.workweek = days;
|
|
254
281
|
} else {
|
|
255
|
-
warn(
|
|
282
|
+
warn(
|
|
283
|
+
lineNumber,
|
|
284
|
+
`Invalid workweek format: "${workweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`
|
|
285
|
+
);
|
|
256
286
|
}
|
|
257
287
|
continue;
|
|
258
288
|
}
|
|
@@ -330,7 +360,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
330
360
|
// First entry is the default (no `default` keyword needed)
|
|
331
361
|
if (COMMENT_RE.test(line)) continue;
|
|
332
362
|
const extracted = extractColor(line, palette);
|
|
333
|
-
const color =
|
|
363
|
+
const color =
|
|
364
|
+
extracted.color ||
|
|
365
|
+
seriesColors[currentTagGroup.entries.length % seriesColors.length] ||
|
|
366
|
+
'#888888';
|
|
334
367
|
const isFirstEntry = currentTagGroup.entries.length === 0;
|
|
335
368
|
currentTagGroup.entries.push({
|
|
336
369
|
value: extracted.label,
|
|
@@ -369,19 +402,32 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
369
402
|
let offset: Offset | undefined;
|
|
370
403
|
|
|
371
404
|
if (depParts.length > 1) {
|
|
372
|
-
const meta = parsePipeMetadata(
|
|
405
|
+
const meta = parsePipeMetadata(
|
|
406
|
+
['', ...depParts.slice(1)],
|
|
407
|
+
aliasMap,
|
|
408
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
409
|
+
);
|
|
373
410
|
if (meta.lag || meta.lead) {
|
|
374
411
|
const key = meta.lag ? 'lag' : 'lead';
|
|
375
|
-
softError(
|
|
412
|
+
softError(
|
|
413
|
+
lineNumber,
|
|
414
|
+
`"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`
|
|
415
|
+
);
|
|
376
416
|
}
|
|
377
417
|
if (meta.offset) {
|
|
378
418
|
const raw = meta.offset;
|
|
379
419
|
if (raw.trim().startsWith('+')) {
|
|
380
|
-
warn(
|
|
420
|
+
warn(
|
|
421
|
+
lineNumber,
|
|
422
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
423
|
+
);
|
|
381
424
|
} else {
|
|
382
425
|
offset = parseOffset(raw) ?? undefined;
|
|
383
426
|
if (!offset) {
|
|
384
|
-
warn(
|
|
427
|
+
warn(
|
|
428
|
+
lineNumber,
|
|
429
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
430
|
+
);
|
|
385
431
|
}
|
|
386
432
|
}
|
|
387
433
|
}
|
|
@@ -415,13 +461,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
415
461
|
if (line.toLowerCase() === 'holiday' || line.toLowerCase() === 'holidays') {
|
|
416
462
|
inHolidaysBlock = true;
|
|
417
463
|
holidaysBlockIndent = indent;
|
|
418
|
-
inHeaderBlock = false;
|
|
419
464
|
result.options.holidaysLineNumber = lineNumber;
|
|
420
465
|
continue;
|
|
421
466
|
}
|
|
422
467
|
|
|
423
468
|
// Single-line holiday: `holiday 2024-12-25 Christmas`
|
|
424
|
-
const holidayInlineMatch = line.match(
|
|
469
|
+
const holidayInlineMatch = line.match(
|
|
470
|
+
/^holiday\s+(\d{4}-\d{2}-\d{2})\s+(.+)$/i
|
|
471
|
+
);
|
|
425
472
|
if (holidayInlineMatch) {
|
|
426
473
|
result.holidays.dates.push({
|
|
427
474
|
date: holidayInlineMatch[1],
|
|
@@ -429,7 +476,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
429
476
|
lineNumber,
|
|
430
477
|
});
|
|
431
478
|
result.options.holidaysLineNumber ??= lineNumber;
|
|
432
|
-
inHeaderBlock = false;
|
|
433
479
|
continue;
|
|
434
480
|
}
|
|
435
481
|
|
|
@@ -438,7 +484,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
438
484
|
if (tagMatch) {
|
|
439
485
|
inTagBlock = true;
|
|
440
486
|
tagBlockIndent = indent;
|
|
441
|
-
inHeaderBlock = false;
|
|
442
487
|
currentTagGroup = {
|
|
443
488
|
name: tagMatch.name,
|
|
444
489
|
alias: tagMatch.alias,
|
|
@@ -458,9 +503,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
458
503
|
if (days) {
|
|
459
504
|
result.holidays.workweek = days;
|
|
460
505
|
} else {
|
|
461
|
-
warn(
|
|
506
|
+
warn(
|
|
507
|
+
lineNumber,
|
|
508
|
+
`Invalid workweek format: "${topWorkweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`
|
|
509
|
+
);
|
|
462
510
|
}
|
|
463
|
-
inHeaderBlock = false;
|
|
464
511
|
continue;
|
|
465
512
|
}
|
|
466
513
|
|
|
@@ -468,7 +515,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
468
515
|
if (line.toLowerCase() === 'era') {
|
|
469
516
|
inEraBlock = true;
|
|
470
517
|
eraBlockIndent = indent;
|
|
471
|
-
inHeaderBlock = false;
|
|
472
518
|
continue;
|
|
473
519
|
}
|
|
474
520
|
|
|
@@ -484,7 +530,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
484
530
|
color: eraExtracted.color || null,
|
|
485
531
|
lineNumber,
|
|
486
532
|
});
|
|
487
|
-
inHeaderBlock = false;
|
|
488
533
|
continue;
|
|
489
534
|
}
|
|
490
535
|
|
|
@@ -492,7 +537,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
492
537
|
if (line.toLowerCase() === 'marker') {
|
|
493
538
|
inMarkerBlock = true;
|
|
494
539
|
markerBlockIndent = indent;
|
|
495
|
-
inHeaderBlock = false;
|
|
496
540
|
continue;
|
|
497
541
|
}
|
|
498
542
|
|
|
@@ -507,7 +551,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
507
551
|
color: markerExtracted.color || null,
|
|
508
552
|
lineNumber,
|
|
509
553
|
});
|
|
510
|
-
inHeaderBlock = false;
|
|
511
554
|
continue;
|
|
512
555
|
}
|
|
513
556
|
|
|
@@ -568,7 +611,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
568
611
|
if (/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?$/.test(value)) {
|
|
569
612
|
result.options.todayMarker = value;
|
|
570
613
|
} else {
|
|
571
|
-
warn(
|
|
614
|
+
warn(
|
|
615
|
+
lineNumber,
|
|
616
|
+
`Invalid today-marker value: "${value}". Expected YYYY-MM-DD.`
|
|
617
|
+
);
|
|
572
618
|
}
|
|
573
619
|
break;
|
|
574
620
|
case 'critical-path':
|
|
@@ -583,20 +629,20 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
583
629
|
result.options.sort = 'tag';
|
|
584
630
|
const colonIdx = value.indexOf(':');
|
|
585
631
|
if (colonIdx !== -1) {
|
|
586
|
-
result.options.defaultSwimlaneGroup =
|
|
632
|
+
result.options.defaultSwimlaneGroup =
|
|
633
|
+
value.slice(colonIdx + 1).trim() || null;
|
|
587
634
|
}
|
|
588
635
|
} else {
|
|
589
|
-
warn(
|
|
636
|
+
warn(
|
|
637
|
+
lineNumber,
|
|
638
|
+
`Invalid sort value: "${value}". Expected "tag" or "tag:GroupName".`
|
|
639
|
+
);
|
|
590
640
|
}
|
|
591
641
|
break;
|
|
592
642
|
}
|
|
593
643
|
continue;
|
|
594
644
|
}
|
|
595
645
|
|
|
596
|
-
inHeaderBlock = false;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
646
|
// ── Parallel block ────────────────────────────────────
|
|
601
647
|
|
|
602
648
|
if (line === 'parallel') {
|
|
@@ -616,8 +662,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
616
662
|
const groupMatch = line.match(GROUP_RE);
|
|
617
663
|
if (groupMatch) {
|
|
618
664
|
// Validate nesting: group under a task is invalid
|
|
619
|
-
if (
|
|
620
|
-
|
|
665
|
+
if (
|
|
666
|
+
blockStack.length > 0 &&
|
|
667
|
+
blockStack[blockStack.length - 1].containerType === 'task'
|
|
668
|
+
) {
|
|
669
|
+
softError(
|
|
670
|
+
lineNumber,
|
|
671
|
+
`Cannot nest a group inside a task. Groups must be inside other groups or parallel blocks.`
|
|
672
|
+
);
|
|
621
673
|
continue;
|
|
622
674
|
}
|
|
623
675
|
|
|
@@ -633,7 +685,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
633
685
|
// Check if first segment after brackets is pipe metadata
|
|
634
686
|
metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
|
|
635
687
|
} else if (segments.length > 1) {
|
|
636
|
-
metadata = parsePipeMetadata(
|
|
688
|
+
metadata = parsePipeMetadata(
|
|
689
|
+
['', ...segments.slice(1)],
|
|
690
|
+
aliasMap,
|
|
691
|
+
pipeWarn
|
|
692
|
+
);
|
|
637
693
|
}
|
|
638
694
|
|
|
639
695
|
// Extract color from group name if present
|
|
@@ -670,11 +726,21 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
670
726
|
const uncertain = !!timelineDurMatch[4];
|
|
671
727
|
const labelRaw = timelineDurMatch[5];
|
|
672
728
|
|
|
673
|
-
const task = makeTask(
|
|
729
|
+
const task = makeTask(
|
|
730
|
+
labelRaw,
|
|
731
|
+
{ amount, unit },
|
|
732
|
+
uncertain,
|
|
733
|
+
lineNumber,
|
|
734
|
+
startDate
|
|
735
|
+
);
|
|
674
736
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
675
737
|
currentContainer().push(taskNode);
|
|
676
738
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
677
|
-
blockStack.push({
|
|
739
|
+
blockStack.push({
|
|
740
|
+
node: taskNode as unknown as GanttGroup,
|
|
741
|
+
indent,
|
|
742
|
+
containerType: 'task',
|
|
743
|
+
});
|
|
678
744
|
continue;
|
|
679
745
|
}
|
|
680
746
|
|
|
@@ -691,7 +757,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
691
757
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
692
758
|
currentContainer().push(taskNode);
|
|
693
759
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
694
|
-
blockStack.push({
|
|
760
|
+
blockStack.push({
|
|
761
|
+
node: taskNode as unknown as GanttGroup,
|
|
762
|
+
indent,
|
|
763
|
+
containerType: 'task',
|
|
764
|
+
});
|
|
695
765
|
continue;
|
|
696
766
|
}
|
|
697
767
|
|
|
@@ -704,13 +774,17 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
704
774
|
null, // no duration — it's a date anchor / milestone
|
|
705
775
|
false,
|
|
706
776
|
lineNumber,
|
|
707
|
-
explicitDateMatch[1]
|
|
777
|
+
explicitDateMatch[1]
|
|
708
778
|
);
|
|
709
779
|
// Explicit date tasks with no duration are milestones
|
|
710
780
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
711
781
|
currentContainer().push(taskNode);
|
|
712
782
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
713
|
-
blockStack.push({
|
|
783
|
+
blockStack.push({
|
|
784
|
+
node: taskNode as unknown as GanttGroup,
|
|
785
|
+
indent,
|
|
786
|
+
containerType: 'task',
|
|
787
|
+
});
|
|
714
788
|
continue;
|
|
715
789
|
}
|
|
716
790
|
|
|
@@ -720,7 +794,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
720
794
|
if (depMatch) {
|
|
721
795
|
// Dependency without a task context is an error
|
|
722
796
|
if (!lastTaskNode) {
|
|
723
|
-
softError(
|
|
797
|
+
softError(
|
|
798
|
+
lineNumber,
|
|
799
|
+
`Dependency "-> ${depMatch[2]}" must be indented under a task.`
|
|
800
|
+
);
|
|
724
801
|
continue;
|
|
725
802
|
}
|
|
726
803
|
// This happens when the dep is at the same indent as the task
|
|
@@ -730,19 +807,32 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
730
807
|
let offset: Offset | undefined;
|
|
731
808
|
|
|
732
809
|
if (depParts.length > 1) {
|
|
733
|
-
const meta = parsePipeMetadata(
|
|
810
|
+
const meta = parsePipeMetadata(
|
|
811
|
+
['', ...depParts.slice(1)],
|
|
812
|
+
aliasMap,
|
|
813
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
814
|
+
);
|
|
734
815
|
if (meta.lag || meta.lead) {
|
|
735
816
|
const key = meta.lag ? 'lag' : 'lead';
|
|
736
|
-
softError(
|
|
817
|
+
softError(
|
|
818
|
+
lineNumber,
|
|
819
|
+
`"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`
|
|
820
|
+
);
|
|
737
821
|
}
|
|
738
822
|
if (meta.offset) {
|
|
739
823
|
const raw = meta.offset;
|
|
740
824
|
if (raw.trim().startsWith('+')) {
|
|
741
|
-
warn(
|
|
825
|
+
warn(
|
|
826
|
+
lineNumber,
|
|
827
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
828
|
+
);
|
|
742
829
|
} else {
|
|
743
830
|
offset = parseOffset(raw) ?? undefined;
|
|
744
831
|
if (!offset) {
|
|
745
|
-
warn(
|
|
832
|
+
warn(
|
|
833
|
+
lineNumber,
|
|
834
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
835
|
+
);
|
|
746
836
|
}
|
|
747
837
|
}
|
|
748
838
|
}
|
|
@@ -754,7 +844,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
754
844
|
|
|
755
845
|
// ── Bare label = parse error ──────────────────────────
|
|
756
846
|
|
|
757
|
-
softError(
|
|
847
|
+
softError(
|
|
848
|
+
lineNumber,
|
|
849
|
+
`Expected duration (e.g., "10d Task"), group brackets (e.g., "[Group]"), or keyword. Got: "${line}"`
|
|
850
|
+
);
|
|
758
851
|
continue;
|
|
759
852
|
}
|
|
760
853
|
|
|
@@ -782,20 +875,26 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
782
875
|
duration: Duration | null,
|
|
783
876
|
uncertain: boolean,
|
|
784
877
|
ln: number,
|
|
785
|
-
explicitStart?: string
|
|
878
|
+
explicitStart?: string
|
|
786
879
|
): GanttTask {
|
|
787
880
|
const segments = labelRaw.split('|');
|
|
788
881
|
const label = segments[0].trim();
|
|
789
882
|
|
|
790
883
|
// Check for reserved keyword
|
|
791
884
|
if (label.toLowerCase() === 'parallel') {
|
|
792
|
-
softError(
|
|
885
|
+
softError(
|
|
886
|
+
ln,
|
|
887
|
+
`"parallel" is a reserved keyword and cannot be used as a task name.`
|
|
888
|
+
);
|
|
793
889
|
}
|
|
794
890
|
|
|
795
891
|
// Parse pipe metadata
|
|
796
|
-
const metadata =
|
|
797
|
-
|
|
798
|
-
|
|
892
|
+
const metadata =
|
|
893
|
+
segments.length > 1
|
|
894
|
+
? parsePipeMetadata(segments, aliasMap, () =>
|
|
895
|
+
warn(ln, MULTIPLE_PIPE_ERROR)
|
|
896
|
+
)
|
|
897
|
+
: {};
|
|
799
898
|
|
|
800
899
|
// Extract progress from metadata or shorthand
|
|
801
900
|
let progress: number | null = null;
|
|
@@ -815,7 +914,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
815
914
|
// Reject lag/lead — use offset instead
|
|
816
915
|
if (metadata.lag || metadata.lead) {
|
|
817
916
|
const key = metadata.lag ? 'lag' : 'lead';
|
|
818
|
-
softError(
|
|
917
|
+
softError(
|
|
918
|
+
ln,
|
|
919
|
+
`"${key}" is no longer supported — use "offset: ${metadata[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`
|
|
920
|
+
);
|
|
819
921
|
}
|
|
820
922
|
|
|
821
923
|
// Extract task-level offset from metadata
|
|
@@ -823,11 +925,17 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
823
925
|
if (metadata.offset) {
|
|
824
926
|
const raw = metadata.offset;
|
|
825
927
|
if (raw.trim().startsWith('+')) {
|
|
826
|
-
warn(
|
|
928
|
+
warn(
|
|
929
|
+
ln,
|
|
930
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
931
|
+
);
|
|
827
932
|
} else {
|
|
828
933
|
taskOffset = parseOffset(raw) ?? undefined;
|
|
829
934
|
if (!taskOffset) {
|
|
830
|
-
warn(
|
|
935
|
+
warn(
|
|
936
|
+
ln,
|
|
937
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
938
|
+
);
|
|
831
939
|
}
|
|
832
940
|
}
|
|
833
941
|
delete metadata.offset;
|
|
@@ -873,7 +981,15 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
873
981
|
const start = WEEKDAY_MAP[rangeParts[0].trim()];
|
|
874
982
|
const end = WEEKDAY_MAP[rangeParts[1].trim()];
|
|
875
983
|
if (start && end) {
|
|
876
|
-
const allDays: Weekday[] = [
|
|
984
|
+
const allDays: Weekday[] = [
|
|
985
|
+
'mon',
|
|
986
|
+
'tue',
|
|
987
|
+
'wed',
|
|
988
|
+
'thu',
|
|
989
|
+
'fri',
|
|
990
|
+
'sat',
|
|
991
|
+
'sun',
|
|
992
|
+
];
|
|
877
993
|
const startIdx = allDays.indexOf(start);
|
|
878
994
|
const endIdx = allDays.indexOf(end);
|
|
879
995
|
const days: Weekday[] = [];
|
|
@@ -888,7 +1004,10 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
888
1004
|
}
|
|
889
1005
|
|
|
890
1006
|
// Try comma-separated: "mon, tue, wed, thu, fri"
|
|
891
|
-
const parts = s
|
|
1007
|
+
const parts = s
|
|
1008
|
+
.toLowerCase()
|
|
1009
|
+
.split(',')
|
|
1010
|
+
.map((p) => p.trim());
|
|
892
1011
|
const days: Weekday[] = [];
|
|
893
1012
|
for (const part of parts) {
|
|
894
1013
|
const day = WEEKDAY_MAP[part];
|
|
@@ -901,13 +1020,20 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
901
1020
|
// ── Known option keys ─────────────────────────────────────
|
|
902
1021
|
|
|
903
1022
|
const KNOWN_OPTIONS = new Set([
|
|
904
|
-
'start',
|
|
905
|
-
'
|
|
1023
|
+
'start',
|
|
1024
|
+
'title',
|
|
1025
|
+
'today-marker',
|
|
1026
|
+
'critical-path',
|
|
1027
|
+
'dependencies',
|
|
1028
|
+
'chart',
|
|
1029
|
+
'sort',
|
|
906
1030
|
]);
|
|
907
1031
|
|
|
908
1032
|
/** Boolean options that can appear as bare keywords or with `no-` prefix. */
|
|
909
1033
|
const KNOWN_BOOLEANS = new Set([
|
|
910
|
-
'critical-path',
|
|
1034
|
+
'critical-path',
|
|
1035
|
+
'today-marker',
|
|
1036
|
+
'dependencies',
|
|
911
1037
|
]);
|
|
912
1038
|
|
|
913
1039
|
function isKnownOption(key: string): boolean {
|