@diagrammo/dgmo 0.8.2 → 0.8.4
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 +185 -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 +189 -194
- package/dist/editor.cjs +336 -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 +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- 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 +8 -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 +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- 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.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- 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 +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
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,24 +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
|
|
|
70
|
+
/** Era entry (inside era block, no `era` prefix): `YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
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+(.+)$/;
|
|
73
|
+
|
|
74
|
+
/** Marker entry (inside marker block, no `marker` prefix): `YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
75
|
+
const MARKER_ENTRY_RE =
|
|
76
|
+
/^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
|
|
77
|
+
|
|
66
78
|
// Valid weekday names
|
|
67
79
|
const WEEKDAY_MAP: Record<string, Weekday> = {
|
|
68
|
-
mon: 'mon',
|
|
69
|
-
|
|
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',
|
|
70
94
|
};
|
|
71
95
|
|
|
72
96
|
// ── Block Stack ─────────────────────────────────────────────
|
|
@@ -81,13 +105,20 @@ interface BlockEntry {
|
|
|
81
105
|
|
|
82
106
|
// ── Parser ──────────────────────────────────────────────────
|
|
83
107
|
|
|
84
|
-
export function parseGantt(
|
|
108
|
+
export function parseGantt(
|
|
109
|
+
content: string,
|
|
110
|
+
palette?: PaletteColors
|
|
111
|
+
): ParsedGantt {
|
|
85
112
|
const lines = content.split('\n');
|
|
86
113
|
const diagnostics: DgmoError[] = [];
|
|
87
114
|
|
|
88
115
|
const result: ParsedGantt = {
|
|
89
116
|
nodes: [],
|
|
90
|
-
holidays: {
|
|
117
|
+
holidays: {
|
|
118
|
+
dates: [],
|
|
119
|
+
ranges: [],
|
|
120
|
+
workweek: ['mon', 'tue', 'wed', 'thu', 'fri'],
|
|
121
|
+
},
|
|
91
122
|
tagGroups: [],
|
|
92
123
|
eras: [],
|
|
93
124
|
markers: [],
|
|
@@ -150,12 +181,15 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
150
181
|
// ── State ───────────────────────────────────────────────
|
|
151
182
|
|
|
152
183
|
let seenChartType = false;
|
|
153
|
-
let inHeaderBlock = true; // options must come before content
|
|
154
184
|
let inHolidaysBlock = false;
|
|
155
185
|
let holidaysBlockIndent = 0;
|
|
156
186
|
let inTagBlock = false;
|
|
157
187
|
let currentTagGroup: TagGroup | null = null;
|
|
158
188
|
let tagBlockIndent = 0;
|
|
189
|
+
let inEraBlock = false;
|
|
190
|
+
let eraBlockIndent = 0;
|
|
191
|
+
let inMarkerBlock = false;
|
|
192
|
+
let markerBlockIndent = 0;
|
|
159
193
|
let lastTaskNode: (GanttNode & { kind: 'task' }) | null = null;
|
|
160
194
|
let taskIdCounter = 0;
|
|
161
195
|
const seriesColors = palette ? getSeriesColors(palette) : [];
|
|
@@ -170,10 +204,16 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
170
204
|
|
|
171
205
|
// Skip empty lines
|
|
172
206
|
if (!line) {
|
|
173
|
-
// Empty line ends holidays/tag blocks only if at root indent
|
|
207
|
+
// Empty line ends holidays/tag/era/marker blocks only if at root indent
|
|
174
208
|
if (inHolidaysBlock && indent <= holidaysBlockIndent) {
|
|
175
209
|
inHolidaysBlock = false;
|
|
176
210
|
}
|
|
211
|
+
if (inEraBlock && indent <= eraBlockIndent) {
|
|
212
|
+
inEraBlock = false;
|
|
213
|
+
}
|
|
214
|
+
if (inMarkerBlock && indent <= markerBlockIndent) {
|
|
215
|
+
inMarkerBlock = false;
|
|
216
|
+
}
|
|
177
217
|
if (inTagBlock && indent <= tagBlockIndent) {
|
|
178
218
|
inTagBlock = false;
|
|
179
219
|
if (currentTagGroup) {
|
|
@@ -190,7 +230,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
190
230
|
const firstLineResult = parseFirstLine(line);
|
|
191
231
|
if (firstLineResult) {
|
|
192
232
|
if (firstLineResult.chartType !== 'gantt') {
|
|
193
|
-
return fail(
|
|
233
|
+
return fail(
|
|
234
|
+
lineNumber,
|
|
235
|
+
`Expected chart type "gantt", got "${firstLineResult.chartType}"`
|
|
236
|
+
);
|
|
194
237
|
}
|
|
195
238
|
seenChartType = true;
|
|
196
239
|
if (firstLineResult.title) {
|
|
@@ -236,7 +279,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
236
279
|
if (days) {
|
|
237
280
|
result.holidays.workweek = days;
|
|
238
281
|
} else {
|
|
239
|
-
warn(
|
|
282
|
+
warn(
|
|
283
|
+
lineNumber,
|
|
284
|
+
`Invalid workweek format: "${workweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`
|
|
285
|
+
);
|
|
240
286
|
}
|
|
241
287
|
continue;
|
|
242
288
|
}
|
|
@@ -249,6 +295,57 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
249
295
|
}
|
|
250
296
|
}
|
|
251
297
|
|
|
298
|
+
// ── Era block entries ─────────────────────────────────
|
|
299
|
+
|
|
300
|
+
if (inEraBlock) {
|
|
301
|
+
if (indent <= eraBlockIndent) {
|
|
302
|
+
inEraBlock = false;
|
|
303
|
+
// fall through to process this line normally
|
|
304
|
+
} else {
|
|
305
|
+
if (COMMENT_RE.test(line)) continue;
|
|
306
|
+
const eraEntryMatch = line.match(ERA_ENTRY_RE);
|
|
307
|
+
if (eraEntryMatch) {
|
|
308
|
+
const eraLabelRaw = eraEntryMatch[3].trim();
|
|
309
|
+
const eraExtracted = extractColor(eraLabelRaw, palette);
|
|
310
|
+
result.eras.push({
|
|
311
|
+
startDate: eraEntryMatch[1],
|
|
312
|
+
endDate: eraEntryMatch[2],
|
|
313
|
+
label: eraExtracted.label,
|
|
314
|
+
color: eraExtracted.color || null,
|
|
315
|
+
lineNumber,
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
warn(lineNumber, `Unrecognized era entry: "${line}"`);
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Marker block entries ─────────────────────────────
|
|
325
|
+
|
|
326
|
+
if (inMarkerBlock) {
|
|
327
|
+
if (indent <= markerBlockIndent) {
|
|
328
|
+
inMarkerBlock = false;
|
|
329
|
+
// fall through to process this line normally
|
|
330
|
+
} else {
|
|
331
|
+
if (COMMENT_RE.test(line)) continue;
|
|
332
|
+
const markerEntryMatch = line.match(MARKER_ENTRY_RE);
|
|
333
|
+
if (markerEntryMatch) {
|
|
334
|
+
const markerLabelRaw = markerEntryMatch[2].trim();
|
|
335
|
+
const markerExtracted = extractColor(markerLabelRaw, palette);
|
|
336
|
+
result.markers.push({
|
|
337
|
+
date: markerEntryMatch[1],
|
|
338
|
+
label: markerExtracted.label,
|
|
339
|
+
color: markerExtracted.color || null,
|
|
340
|
+
lineNumber,
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
warn(lineNumber, `Unrecognized marker entry: "${line}"`);
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
252
349
|
// ── Tag block entries ─────────────────────────────────
|
|
253
350
|
|
|
254
351
|
if (inTagBlock && currentTagGroup) {
|
|
@@ -263,7 +360,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
263
360
|
// First entry is the default (no `default` keyword needed)
|
|
264
361
|
if (COMMENT_RE.test(line)) continue;
|
|
265
362
|
const extracted = extractColor(line, palette);
|
|
266
|
-
const color =
|
|
363
|
+
const color =
|
|
364
|
+
extracted.color ||
|
|
365
|
+
seriesColors[currentTagGroup.entries.length % seriesColors.length] ||
|
|
366
|
+
'#888888';
|
|
267
367
|
const isFirstEntry = currentTagGroup.entries.length === 0;
|
|
268
368
|
currentTagGroup.entries.push({
|
|
269
369
|
value: extracted.label,
|
|
@@ -302,19 +402,32 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
302
402
|
let offset: Offset | undefined;
|
|
303
403
|
|
|
304
404
|
if (depParts.length > 1) {
|
|
305
|
-
const meta = parsePipeMetadata(
|
|
405
|
+
const meta = parsePipeMetadata(
|
|
406
|
+
['', ...depParts.slice(1)],
|
|
407
|
+
aliasMap,
|
|
408
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
409
|
+
);
|
|
306
410
|
if (meta.lag || meta.lead) {
|
|
307
411
|
const key = meta.lag ? 'lag' : 'lead';
|
|
308
|
-
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
|
+
);
|
|
309
416
|
}
|
|
310
417
|
if (meta.offset) {
|
|
311
418
|
const raw = meta.offset;
|
|
312
419
|
if (raw.trim().startsWith('+')) {
|
|
313
|
-
warn(
|
|
420
|
+
warn(
|
|
421
|
+
lineNumber,
|
|
422
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
423
|
+
);
|
|
314
424
|
} else {
|
|
315
425
|
offset = parseOffset(raw) ?? undefined;
|
|
316
426
|
if (!offset) {
|
|
317
|
-
warn(
|
|
427
|
+
warn(
|
|
428
|
+
lineNumber,
|
|
429
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
430
|
+
);
|
|
318
431
|
}
|
|
319
432
|
}
|
|
320
433
|
}
|
|
@@ -348,13 +461,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
348
461
|
if (line.toLowerCase() === 'holiday' || line.toLowerCase() === 'holidays') {
|
|
349
462
|
inHolidaysBlock = true;
|
|
350
463
|
holidaysBlockIndent = indent;
|
|
351
|
-
inHeaderBlock = false;
|
|
352
464
|
result.options.holidaysLineNumber = lineNumber;
|
|
353
465
|
continue;
|
|
354
466
|
}
|
|
355
467
|
|
|
356
468
|
// Single-line holiday: `holiday 2024-12-25 Christmas`
|
|
357
|
-
const holidayInlineMatch = line.match(
|
|
469
|
+
const holidayInlineMatch = line.match(
|
|
470
|
+
/^holiday\s+(\d{4}-\d{2}-\d{2})\s+(.+)$/i
|
|
471
|
+
);
|
|
358
472
|
if (holidayInlineMatch) {
|
|
359
473
|
result.holidays.dates.push({
|
|
360
474
|
date: holidayInlineMatch[1],
|
|
@@ -362,20 +476,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
362
476
|
lineNumber,
|
|
363
477
|
});
|
|
364
478
|
result.options.holidaysLineNumber ??= lineNumber;
|
|
365
|
-
inHeaderBlock = false;
|
|
366
479
|
continue;
|
|
367
480
|
}
|
|
368
481
|
|
|
369
482
|
// Tag block heading
|
|
370
483
|
const tagMatch = matchTagBlockHeading(line);
|
|
371
484
|
if (tagMatch) {
|
|
372
|
-
if (tagMatch.deprecated) {
|
|
373
|
-
softError(lineNumber, `'## ${tagMatch.name}' is no longer supported — use 'tag ${tagMatch.name}' instead`);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
485
|
inTagBlock = true;
|
|
377
486
|
tagBlockIndent = indent;
|
|
378
|
-
inHeaderBlock = false;
|
|
379
487
|
currentTagGroup = {
|
|
380
488
|
name: tagMatch.name,
|
|
381
489
|
alias: tagMatch.alias,
|
|
@@ -388,7 +496,29 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
388
496
|
continue;
|
|
389
497
|
}
|
|
390
498
|
|
|
391
|
-
//
|
|
499
|
+
// Top-level workweek (outside holiday block)
|
|
500
|
+
const topWorkweekMatch = line.match(WORKWEEK_RE);
|
|
501
|
+
if (topWorkweekMatch) {
|
|
502
|
+
const days = parseWorkweek(topWorkweekMatch[1].trim());
|
|
503
|
+
if (days) {
|
|
504
|
+
result.holidays.workweek = days;
|
|
505
|
+
} else {
|
|
506
|
+
warn(
|
|
507
|
+
lineNumber,
|
|
508
|
+
`Invalid workweek format: "${topWorkweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Era block: bare `era` keyword starts a block
|
|
515
|
+
if (line.toLowerCase() === 'era') {
|
|
516
|
+
inEraBlock = true;
|
|
517
|
+
eraBlockIndent = indent;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Era (inline)
|
|
392
522
|
const eraMatch = line.match(ERA_RE);
|
|
393
523
|
if (eraMatch) {
|
|
394
524
|
const eraLabelRaw = eraMatch[3].trim();
|
|
@@ -400,11 +530,17 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
400
530
|
color: eraExtracted.color || null,
|
|
401
531
|
lineNumber,
|
|
402
532
|
});
|
|
403
|
-
inHeaderBlock = false;
|
|
404
533
|
continue;
|
|
405
534
|
}
|
|
406
535
|
|
|
407
|
-
// Marker
|
|
536
|
+
// Marker block: bare `marker` keyword starts a block
|
|
537
|
+
if (line.toLowerCase() === 'marker') {
|
|
538
|
+
inMarkerBlock = true;
|
|
539
|
+
markerBlockIndent = indent;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Marker (inline)
|
|
408
544
|
const markerMatch = line.match(MARKER_RE);
|
|
409
545
|
if (markerMatch) {
|
|
410
546
|
const markerLabelRaw = markerMatch[2].trim();
|
|
@@ -415,7 +551,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
415
551
|
color: markerExtracted.color || null,
|
|
416
552
|
lineNumber,
|
|
417
553
|
});
|
|
418
|
-
inHeaderBlock = false;
|
|
419
554
|
continue;
|
|
420
555
|
}
|
|
421
556
|
|
|
@@ -472,14 +607,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
472
607
|
result.options.title = value;
|
|
473
608
|
result.options.titleLineNumber = lineNumber;
|
|
474
609
|
break;
|
|
475
|
-
case 'orientation':
|
|
476
|
-
warn(lineNumber, `'orientation' is not supported for gantt charts`);
|
|
477
|
-
break;
|
|
478
610
|
case 'today-marker':
|
|
479
611
|
if (/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?$/.test(value)) {
|
|
480
612
|
result.options.todayMarker = value;
|
|
481
613
|
} else {
|
|
482
|
-
warn(
|
|
614
|
+
warn(
|
|
615
|
+
lineNumber,
|
|
616
|
+
`Invalid today-marker value: "${value}". Expected YYYY-MM-DD.`
|
|
617
|
+
);
|
|
483
618
|
}
|
|
484
619
|
break;
|
|
485
620
|
case 'critical-path':
|
|
@@ -494,41 +629,20 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
494
629
|
result.options.sort = 'tag';
|
|
495
630
|
const colonIdx = value.indexOf(':');
|
|
496
631
|
if (colonIdx !== -1) {
|
|
497
|
-
result.options.defaultSwimlaneGroup =
|
|
632
|
+
result.options.defaultSwimlaneGroup =
|
|
633
|
+
value.slice(colonIdx + 1).trim() || null;
|
|
498
634
|
}
|
|
499
635
|
} else {
|
|
500
|
-
warn(
|
|
636
|
+
warn(
|
|
637
|
+
lineNumber,
|
|
638
|
+
`Invalid sort value: "${value}". Expected "tag" or "tag:GroupName".`
|
|
639
|
+
);
|
|
501
640
|
}
|
|
502
641
|
break;
|
|
503
642
|
}
|
|
504
643
|
continue;
|
|
505
644
|
}
|
|
506
645
|
|
|
507
|
-
inHeaderBlock = false;
|
|
508
|
-
|
|
509
|
-
// ── `# Group` alternate syntax ──────────────────────────
|
|
510
|
-
|
|
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
|
-
|
|
532
646
|
// ── Parallel block ────────────────────────────────────
|
|
533
647
|
|
|
534
648
|
if (line === 'parallel') {
|
|
@@ -548,8 +662,14 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
548
662
|
const groupMatch = line.match(GROUP_RE);
|
|
549
663
|
if (groupMatch) {
|
|
550
664
|
// Validate nesting: group under a task is invalid
|
|
551
|
-
if (
|
|
552
|
-
|
|
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
|
+
);
|
|
553
673
|
continue;
|
|
554
674
|
}
|
|
555
675
|
|
|
@@ -560,12 +680,16 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
560
680
|
let metadata: Record<string, string> = {};
|
|
561
681
|
let color: string | null = null;
|
|
562
682
|
|
|
563
|
-
const pipeWarn = () => warn(lineNumber,
|
|
683
|
+
const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_ERROR);
|
|
564
684
|
if (segments.length > 0 && segments[0].trim()) {
|
|
565
685
|
// Check if first segment after brackets is pipe metadata
|
|
566
686
|
metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
|
|
567
687
|
} else if (segments.length > 1) {
|
|
568
|
-
metadata = parsePipeMetadata(
|
|
688
|
+
metadata = parsePipeMetadata(
|
|
689
|
+
['', ...segments.slice(1)],
|
|
690
|
+
aliasMap,
|
|
691
|
+
pipeWarn
|
|
692
|
+
);
|
|
569
693
|
}
|
|
570
694
|
|
|
571
695
|
// Extract color from group name if present
|
|
@@ -602,11 +726,21 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
602
726
|
const uncertain = !!timelineDurMatch[4];
|
|
603
727
|
const labelRaw = timelineDurMatch[5];
|
|
604
728
|
|
|
605
|
-
const task = makeTask(
|
|
729
|
+
const task = makeTask(
|
|
730
|
+
labelRaw,
|
|
731
|
+
{ amount, unit },
|
|
732
|
+
uncertain,
|
|
733
|
+
lineNumber,
|
|
734
|
+
startDate
|
|
735
|
+
);
|
|
606
736
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
607
737
|
currentContainer().push(taskNode);
|
|
608
738
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
609
|
-
blockStack.push({
|
|
739
|
+
blockStack.push({
|
|
740
|
+
node: taskNode as unknown as GanttGroup,
|
|
741
|
+
indent,
|
|
742
|
+
containerType: 'task',
|
|
743
|
+
});
|
|
610
744
|
continue;
|
|
611
745
|
}
|
|
612
746
|
|
|
@@ -623,7 +757,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
623
757
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
624
758
|
currentContainer().push(taskNode);
|
|
625
759
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
626
|
-
blockStack.push({
|
|
760
|
+
blockStack.push({
|
|
761
|
+
node: taskNode as unknown as GanttGroup,
|
|
762
|
+
indent,
|
|
763
|
+
containerType: 'task',
|
|
764
|
+
});
|
|
627
765
|
continue;
|
|
628
766
|
}
|
|
629
767
|
|
|
@@ -636,13 +774,17 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
636
774
|
null, // no duration — it's a date anchor / milestone
|
|
637
775
|
false,
|
|
638
776
|
lineNumber,
|
|
639
|
-
explicitDateMatch[1]
|
|
777
|
+
explicitDateMatch[1]
|
|
640
778
|
);
|
|
641
779
|
// Explicit date tasks with no duration are milestones
|
|
642
780
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
643
781
|
currentContainer().push(taskNode);
|
|
644
782
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
645
|
-
blockStack.push({
|
|
783
|
+
blockStack.push({
|
|
784
|
+
node: taskNode as unknown as GanttGroup,
|
|
785
|
+
indent,
|
|
786
|
+
containerType: 'task',
|
|
787
|
+
});
|
|
646
788
|
continue;
|
|
647
789
|
}
|
|
648
790
|
|
|
@@ -652,7 +794,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
652
794
|
if (depMatch) {
|
|
653
795
|
// Dependency without a task context is an error
|
|
654
796
|
if (!lastTaskNode) {
|
|
655
|
-
softError(
|
|
797
|
+
softError(
|
|
798
|
+
lineNumber,
|
|
799
|
+
`Dependency "-> ${depMatch[2]}" must be indented under a task.`
|
|
800
|
+
);
|
|
656
801
|
continue;
|
|
657
802
|
}
|
|
658
803
|
// This happens when the dep is at the same indent as the task
|
|
@@ -662,19 +807,32 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
662
807
|
let offset: Offset | undefined;
|
|
663
808
|
|
|
664
809
|
if (depParts.length > 1) {
|
|
665
|
-
const meta = parsePipeMetadata(
|
|
810
|
+
const meta = parsePipeMetadata(
|
|
811
|
+
['', ...depParts.slice(1)],
|
|
812
|
+
aliasMap,
|
|
813
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
814
|
+
);
|
|
666
815
|
if (meta.lag || meta.lead) {
|
|
667
816
|
const key = meta.lag ? 'lag' : 'lead';
|
|
668
|
-
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
|
+
);
|
|
669
821
|
}
|
|
670
822
|
if (meta.offset) {
|
|
671
823
|
const raw = meta.offset;
|
|
672
824
|
if (raw.trim().startsWith('+')) {
|
|
673
|
-
warn(
|
|
825
|
+
warn(
|
|
826
|
+
lineNumber,
|
|
827
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
828
|
+
);
|
|
674
829
|
} else {
|
|
675
830
|
offset = parseOffset(raw) ?? undefined;
|
|
676
831
|
if (!offset) {
|
|
677
|
-
warn(
|
|
832
|
+
warn(
|
|
833
|
+
lineNumber,
|
|
834
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
835
|
+
);
|
|
678
836
|
}
|
|
679
837
|
}
|
|
680
838
|
}
|
|
@@ -686,7 +844,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
686
844
|
|
|
687
845
|
// ── Bare label = parse error ──────────────────────────
|
|
688
846
|
|
|
689
|
-
softError(
|
|
847
|
+
softError(
|
|
848
|
+
lineNumber,
|
|
849
|
+
`Expected duration (e.g., "10d Task"), group brackets (e.g., "[Group]"), or keyword. Got: "${line}"`
|
|
850
|
+
);
|
|
690
851
|
continue;
|
|
691
852
|
}
|
|
692
853
|
|
|
@@ -714,20 +875,26 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
714
875
|
duration: Duration | null,
|
|
715
876
|
uncertain: boolean,
|
|
716
877
|
ln: number,
|
|
717
|
-
explicitStart?: string
|
|
878
|
+
explicitStart?: string
|
|
718
879
|
): GanttTask {
|
|
719
880
|
const segments = labelRaw.split('|');
|
|
720
881
|
const label = segments[0].trim();
|
|
721
882
|
|
|
722
883
|
// Check for reserved keyword
|
|
723
884
|
if (label.toLowerCase() === 'parallel') {
|
|
724
|
-
softError(
|
|
885
|
+
softError(
|
|
886
|
+
ln,
|
|
887
|
+
`"parallel" is a reserved keyword and cannot be used as a task name.`
|
|
888
|
+
);
|
|
725
889
|
}
|
|
726
890
|
|
|
727
891
|
// Parse pipe metadata
|
|
728
|
-
const metadata =
|
|
729
|
-
|
|
730
|
-
|
|
892
|
+
const metadata =
|
|
893
|
+
segments.length > 1
|
|
894
|
+
? parsePipeMetadata(segments, aliasMap, () =>
|
|
895
|
+
warn(ln, MULTIPLE_PIPE_ERROR)
|
|
896
|
+
)
|
|
897
|
+
: {};
|
|
731
898
|
|
|
732
899
|
// Extract progress from metadata or shorthand
|
|
733
900
|
let progress: number | null = null;
|
|
@@ -747,7 +914,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
747
914
|
// Reject lag/lead — use offset instead
|
|
748
915
|
if (metadata.lag || metadata.lead) {
|
|
749
916
|
const key = metadata.lag ? 'lag' : 'lead';
|
|
750
|
-
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
|
+
);
|
|
751
921
|
}
|
|
752
922
|
|
|
753
923
|
// Extract task-level offset from metadata
|
|
@@ -755,11 +925,17 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
755
925
|
if (metadata.offset) {
|
|
756
926
|
const raw = metadata.offset;
|
|
757
927
|
if (raw.trim().startsWith('+')) {
|
|
758
|
-
warn(
|
|
928
|
+
warn(
|
|
929
|
+
ln,
|
|
930
|
+
`Invalid offset: "${raw}". Explicit "+" is not supported — use "${raw.trim().slice(1)}" instead.`
|
|
931
|
+
);
|
|
759
932
|
} else {
|
|
760
933
|
taskOffset = parseOffset(raw) ?? undefined;
|
|
761
934
|
if (!taskOffset) {
|
|
762
|
-
warn(
|
|
935
|
+
warn(
|
|
936
|
+
ln,
|
|
937
|
+
`Invalid offset: "${raw}". Expected format like "3bd", "-5d", or "0bd".`
|
|
938
|
+
);
|
|
763
939
|
}
|
|
764
940
|
}
|
|
765
941
|
delete metadata.offset;
|
|
@@ -805,7 +981,15 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
805
981
|
const start = WEEKDAY_MAP[rangeParts[0].trim()];
|
|
806
982
|
const end = WEEKDAY_MAP[rangeParts[1].trim()];
|
|
807
983
|
if (start && end) {
|
|
808
|
-
const allDays: Weekday[] = [
|
|
984
|
+
const allDays: Weekday[] = [
|
|
985
|
+
'mon',
|
|
986
|
+
'tue',
|
|
987
|
+
'wed',
|
|
988
|
+
'thu',
|
|
989
|
+
'fri',
|
|
990
|
+
'sat',
|
|
991
|
+
'sun',
|
|
992
|
+
];
|
|
809
993
|
const startIdx = allDays.indexOf(start);
|
|
810
994
|
const endIdx = allDays.indexOf(end);
|
|
811
995
|
const days: Weekday[] = [];
|
|
@@ -820,7 +1004,10 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
820
1004
|
}
|
|
821
1005
|
|
|
822
1006
|
// Try comma-separated: "mon, tue, wed, thu, fri"
|
|
823
|
-
const parts = s
|
|
1007
|
+
const parts = s
|
|
1008
|
+
.toLowerCase()
|
|
1009
|
+
.split(',')
|
|
1010
|
+
.map((p) => p.trim());
|
|
824
1011
|
const days: Weekday[] = [];
|
|
825
1012
|
for (const part of parts) {
|
|
826
1013
|
const day = WEEKDAY_MAP[part];
|
|
@@ -833,13 +1020,20 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
833
1020
|
// ── Known option keys ─────────────────────────────────────
|
|
834
1021
|
|
|
835
1022
|
const KNOWN_OPTIONS = new Set([
|
|
836
|
-
'start',
|
|
837
|
-
'
|
|
1023
|
+
'start',
|
|
1024
|
+
'title',
|
|
1025
|
+
'today-marker',
|
|
1026
|
+
'critical-path',
|
|
1027
|
+
'dependencies',
|
|
1028
|
+
'chart',
|
|
1029
|
+
'sort',
|
|
838
1030
|
]);
|
|
839
1031
|
|
|
840
1032
|
/** Boolean options that can appear as bare keywords or with `no-` prefix. */
|
|
841
1033
|
const KNOWN_BOOLEANS = new Set([
|
|
842
|
-
'critical-path',
|
|
1034
|
+
'critical-path',
|
|
1035
|
+
'today-marker',
|
|
1036
|
+
'dependencies',
|
|
843
1037
|
]);
|
|
844
1038
|
|
|
845
1039
|
function isKnownOption(key: string): boolean {
|