@diagrammo/dgmo 0.7.3 → 0.8.0
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/AGENTS.md +15 -20
- package/README.md +56 -58
- package/dist/cli.cjs +188 -181
- package/dist/index.cjs +3506 -1057
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -43
- package/dist/index.d.ts +196 -43
- package/dist/index.js +3493 -1057
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +629 -289
- package/package.json +1 -1
- package/src/c4/layout.ts +6 -9
- package/src/c4/parser.ts +189 -83
- package/src/c4/renderer.ts +8 -9
- package/src/chart.ts +296 -83
- package/src/class/parser.ts +54 -37
- package/src/class/renderer.ts +8 -8
- package/src/cli.ts +8 -8
- package/src/colors.ts +4 -1
- package/src/completion.ts +757 -10
- package/src/d3.ts +310 -73
- package/src/dgmo-router.ts +63 -8
- package/src/echarts.ts +726 -231
- package/src/er/parser.ts +94 -76
- package/src/er/renderer.ts +6 -5
- package/src/gantt/parser.ts +144 -69
- package/src/gantt/renderer.ts +50 -14
- package/src/gantt/types.ts +3 -3
- package/src/graph/flowchart-parser.ts +97 -37
- package/src/graph/flowchart-renderer.ts +4 -3
- package/src/graph/state-parser.ts +50 -31
- package/src/graph/state-renderer.ts +4 -3
- package/src/index.ts +14 -5
- package/src/infra/compute.ts +1 -0
- package/src/infra/layout.ts +3 -0
- package/src/infra/parser.ts +291 -92
- package/src/infra/renderer.ts +172 -30
- package/src/infra/types.ts +5 -0
- package/src/initiative-status/layout.ts +1 -1
- package/src/initiative-status/parser.ts +121 -47
- package/src/initiative-status/renderer.ts +42 -23
- package/src/initiative-status/types.ts +10 -2
- package/src/kanban/parser.ts +60 -37
- package/src/kanban/renderer.ts +2 -2
- package/src/kanban/types.ts +1 -0
- package/src/org/layout.ts +9 -9
- package/src/org/parser.ts +39 -40
- package/src/org/renderer.ts +5 -6
- package/src/org/resolver.ts +26 -19
- package/src/render.ts +1 -1
- package/src/sequence/parser.ts +304 -95
- package/src/sequence/renderer.ts +9 -9
- package/src/sitemap/layout.ts +3 -4
- package/src/sitemap/parser.ts +57 -49
- package/src/sitemap/renderer.ts +6 -7
- package/src/utils/arrows.ts +25 -6
- package/src/utils/duration.ts +43 -7
- package/src/utils/legend-constants.ts +26 -0
- package/src/utils/legend-svg.ts +167 -0
- package/src/utils/parsing.ts +247 -7
- package/src/utils/tag-groups.ts +160 -15
- package/src/utils/title-constants.ts +9 -0
package/src/gantt/parser.ts
CHANGED
|
@@ -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 } from '../utils/parsing';
|
|
9
|
+
import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING, parseFirstLine, prescanOptions, GROUP_HASH_RE } from '../utils/parsing';
|
|
10
10
|
import { parseOffset } from '../utils/duration';
|
|
11
11
|
import type { PaletteColors } from '../palettes';
|
|
12
12
|
import { resolveColor } from '../colors';
|
|
@@ -30,44 +30,38 @@ import type {
|
|
|
30
30
|
|
|
31
31
|
// ── Regexes ─────────────────────────────────────────────────
|
|
32
32
|
|
|
33
|
-
/** Duration task: `30d
|
|
34
|
-
const DURATION_RE = /^(\d+(?:\.\d+)?)(
|
|
33
|
+
/** Duration task: `30d Label`, `1.5w Label`, `10bd? Label`, `2h Label`, `90min Label` */
|
|
34
|
+
const DURATION_RE = /^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h)(\?)?\s+(.+)$/;
|
|
35
35
|
|
|
36
|
-
/** Explicit date task: `2024-01-15: Label` */
|
|
37
|
-
const EXPLICIT_DATE_RE = /^(\d{4}-\d{2}-\d{2})
|
|
36
|
+
/** Explicit date task: `2024-01-15 Label` or `2024-01-15 14:30 Label` */
|
|
37
|
+
const EXPLICIT_DATE_RE = /^(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)\s+(.+)$/;
|
|
38
38
|
|
|
39
|
-
/** Timeline migration syntax: `2024-01-15 -> 30d: Label` */
|
|
40
|
-
const TIMELINE_DURATION_RE = /^(\d{4}-\d{2}-\d{2})\s*->\s*(\d+(?:\.\d+)?)(
|
|
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+(.+)$/;
|
|
41
41
|
|
|
42
42
|
/** Group container: `[GroupName]` with optional pipe metadata */
|
|
43
43
|
const GROUP_RE = /^\[(.+?)\]\s*(.*)$/;
|
|
44
44
|
|
|
45
|
-
/** Dependency: `-> TargetName` with optional pipe metadata */
|
|
46
|
-
const DEPENDENCY_RE =
|
|
45
|
+
/** Dependency: `-> TargetName` or `-label-> TargetName` with optional pipe metadata */
|
|
46
|
+
const DEPENDENCY_RE = /^(?:-(.+?))?->\s*(.+)$/;
|
|
47
47
|
|
|
48
48
|
/** Comment line */
|
|
49
49
|
const COMMENT_RE = /^\/\//;
|
|
50
50
|
|
|
51
|
-
/** Era: `era YYYY[-MM[-DD]] -> YYYY[-MM[-DD]]
|
|
52
|
-
const ERA_RE = /^era\s+(\d{4}(?:-\d{2}(?:-\d{2})?)?)\s*->\s*(\d{4}(?:-\d{2}(?:-\d{2})?)?)\s
|
|
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;
|
|
53
53
|
|
|
54
|
-
/** Marker: `marker
|
|
55
|
-
const MARKER_RE = /^marker
|
|
54
|
+
/** Marker: `marker YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
|
|
55
|
+
const MARKER_RE = /^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
|
|
56
56
|
|
|
57
|
-
/** Holiday date: `2024-01-15
|
|
58
|
-
const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2})
|
|
57
|
+
/** Holiday date: `2024-01-15 Label` */
|
|
58
|
+
const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/;
|
|
59
59
|
|
|
60
|
-
/** Holiday range: `2024-12-24 -> 2024-12-31
|
|
61
|
-
const HOLIDAY_RANGE_RE = /^(\d{4}-\d{2}-\d{2})\s*->\s*(\d{4}-\d{2}-\d{2})
|
|
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+(.+)$/;
|
|
62
62
|
|
|
63
|
-
/** Workweek override: `workweek
|
|
64
|
-
const WORKWEEK_RE = /^workweek
|
|
65
|
-
|
|
66
|
-
/** chart: gantt */
|
|
67
|
-
const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
|
|
68
|
-
|
|
69
|
-
/** Option lines */
|
|
70
|
-
const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
63
|
+
/** Workweek override: `workweek sun-thu` */
|
|
64
|
+
const WORKWEEK_RE = /^workweek\s+(.+)$/i;
|
|
71
65
|
|
|
72
66
|
// Valid weekday names
|
|
73
67
|
const WEEKDAY_MAP: Record<string, Weekday> = {
|
|
@@ -101,10 +95,9 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
101
95
|
start: null,
|
|
102
96
|
title: null,
|
|
103
97
|
titleLineNumber: null,
|
|
104
|
-
orientation: 'horizontal',
|
|
105
98
|
todayMarker: 'off',
|
|
106
99
|
criticalPath: false,
|
|
107
|
-
dependencies:
|
|
100
|
+
dependencies: true,
|
|
108
101
|
sort: 'default',
|
|
109
102
|
defaultSwimlaneGroup: null,
|
|
110
103
|
optionLineNumbers: {},
|
|
@@ -193,14 +186,19 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
193
186
|
|
|
194
187
|
// ── Chart type ────────────────────────────────────────
|
|
195
188
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
189
|
+
if (!seenChartType) {
|
|
190
|
+
const firstLineResult = parseFirstLine(line);
|
|
191
|
+
if (firstLineResult) {
|
|
192
|
+
if (firstLineResult.chartType !== 'gantt') {
|
|
193
|
+
return fail(lineNumber, `Expected chart type "gantt", got "${firstLineResult.chartType}"`);
|
|
194
|
+
}
|
|
195
|
+
seenChartType = true;
|
|
196
|
+
if (firstLineResult.title) {
|
|
197
|
+
result.options.title = firstLineResult.title;
|
|
198
|
+
result.options.titleLineNumber = lineNumber;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
201
|
}
|
|
202
|
-
seenChartType = true;
|
|
203
|
-
continue;
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
// ── Holidays block ────────────────────────────────────
|
|
@@ -261,22 +259,18 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
261
259
|
currentTagGroup = null;
|
|
262
260
|
// fall through to process this line normally
|
|
263
261
|
} else {
|
|
264
|
-
// Parse tag entry: `Value(color)` or `Value`
|
|
262
|
+
// Parse tag entry: `Value(color)` or `Value`
|
|
263
|
+
// First entry is the default (no `default` keyword needed)
|
|
265
264
|
if (COMMENT_RE.test(line)) continue;
|
|
266
|
-
|
|
267
|
-
let isDefault = false;
|
|
268
|
-
if (entryLine.endsWith(' default') || entryLine.endsWith('\tdefault')) {
|
|
269
|
-
isDefault = true;
|
|
270
|
-
entryLine = entryLine.replace(/\s+default$/, '').trim();
|
|
271
|
-
}
|
|
272
|
-
const extracted = extractColor(entryLine, palette);
|
|
265
|
+
const extracted = extractColor(line, palette);
|
|
273
266
|
const color = extracted.color || seriesColors[currentTagGroup.entries.length % seriesColors.length] || '#888888';
|
|
267
|
+
const isFirstEntry = currentTagGroup.entries.length === 0;
|
|
274
268
|
currentTagGroup.entries.push({
|
|
275
269
|
value: extracted.label,
|
|
276
270
|
color,
|
|
277
271
|
lineNumber,
|
|
278
272
|
});
|
|
279
|
-
if (
|
|
273
|
+
if (isFirstEntry) {
|
|
280
274
|
currentTagGroup.defaultValue = extracted.label;
|
|
281
275
|
}
|
|
282
276
|
continue;
|
|
@@ -302,7 +296,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
302
296
|
// Dependency under a task
|
|
303
297
|
const depMatch = line.match(DEPENDENCY_RE);
|
|
304
298
|
if (depMatch) {
|
|
305
|
-
const
|
|
299
|
+
const label = depMatch[1]?.trim() || undefined;
|
|
300
|
+
const depParts = depMatch[2].split('|');
|
|
306
301
|
const targetName = depParts[0].trim();
|
|
307
302
|
let offset: Offset | undefined;
|
|
308
303
|
|
|
@@ -310,7 +305,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
310
305
|
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
311
306
|
if (meta.lag || meta.lead) {
|
|
312
307
|
const key = meta.lag ? 'lag' : 'lead';
|
|
313
|
-
softError(lineNumber, `
|
|
308
|
+
softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
314
309
|
}
|
|
315
310
|
if (meta.offset) {
|
|
316
311
|
const raw = meta.offset;
|
|
@@ -327,6 +322,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
327
322
|
|
|
328
323
|
lastTaskNode.dependencies.push({
|
|
329
324
|
targetName,
|
|
325
|
+
label,
|
|
330
326
|
offset,
|
|
331
327
|
lineNumber,
|
|
332
328
|
});
|
|
@@ -349,7 +345,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
349
345
|
|
|
350
346
|
// ── Header options ────────────────────────────────────
|
|
351
347
|
|
|
352
|
-
if (line.toLowerCase() === 'holidays') {
|
|
348
|
+
if (line.toLowerCase() === 'holiday' || line.toLowerCase() === 'holidays') {
|
|
353
349
|
inHolidaysBlock = true;
|
|
354
350
|
holidaysBlockIndent = indent;
|
|
355
351
|
inHeaderBlock = false;
|
|
@@ -357,9 +353,26 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
357
353
|
continue;
|
|
358
354
|
}
|
|
359
355
|
|
|
356
|
+
// Single-line holiday: `holiday 2024-12-25 Christmas`
|
|
357
|
+
const holidayInlineMatch = line.match(/^holiday\s+(\d{4}-\d{2}-\d{2})\s+(.+)$/i);
|
|
358
|
+
if (holidayInlineMatch) {
|
|
359
|
+
result.holidays.dates.push({
|
|
360
|
+
date: holidayInlineMatch[1],
|
|
361
|
+
label: holidayInlineMatch[2].trim(),
|
|
362
|
+
lineNumber,
|
|
363
|
+
});
|
|
364
|
+
result.options.holidaysLineNumber ??= lineNumber;
|
|
365
|
+
inHeaderBlock = false;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
360
369
|
// Tag block heading
|
|
361
370
|
const tagMatch = matchTagBlockHeading(line);
|
|
362
371
|
if (tagMatch) {
|
|
372
|
+
if (tagMatch.deprecated) {
|
|
373
|
+
softError(lineNumber, `'## ${tagMatch.name}' is no longer supported — use 'tag ${tagMatch.name}' instead`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
363
376
|
inTagBlock = true;
|
|
364
377
|
tagBlockIndent = indent;
|
|
365
378
|
inHeaderBlock = false;
|
|
@@ -406,11 +419,49 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
406
419
|
continue;
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
// Options
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
422
|
+
// Options — space-separated: `start 2024-04-01`, `title My Plan`
|
|
423
|
+
// Boolean options: bare keyword = on, `no-X` = off
|
|
424
|
+
const optNoColonMatch = line.match(/^([a-z][a-z0-9-]*)\s+(.+)$/i);
|
|
425
|
+
const bareKeyword = line.match(/^([a-z][a-z0-9-]*)$/i);
|
|
426
|
+
|
|
427
|
+
// Bare boolean keywords
|
|
428
|
+
if (bareKeyword && KNOWN_BOOLEANS.has(bareKeyword[1].toLowerCase())) {
|
|
429
|
+
const key = bareKeyword[1].toLowerCase();
|
|
430
|
+
result.options.optionLineNumbers[key] = lineNumber;
|
|
431
|
+
switch (key) {
|
|
432
|
+
case 'critical-path':
|
|
433
|
+
result.options.criticalPath = true;
|
|
434
|
+
break;
|
|
435
|
+
case 'today-marker':
|
|
436
|
+
result.options.todayMarker = 'on';
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Negated booleans: `no-dependencies`, `no-critical-path`
|
|
443
|
+
if (bareKeyword && bareKeyword[1].toLowerCase().startsWith('no-')) {
|
|
444
|
+
const base = bareKeyword[1].toLowerCase().substring(3);
|
|
445
|
+
if (KNOWN_BOOLEANS.has(base)) {
|
|
446
|
+
result.options.optionLineNumbers[base] = lineNumber;
|
|
447
|
+
switch (base) {
|
|
448
|
+
case 'dependencies':
|
|
449
|
+
result.options.dependencies = false;
|
|
450
|
+
break;
|
|
451
|
+
case 'critical-path':
|
|
452
|
+
result.options.criticalPath = false;
|
|
453
|
+
break;
|
|
454
|
+
case 'today-marker':
|
|
455
|
+
result.options.todayMarker = 'off';
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (optNoColonMatch && isKnownOption(optNoColonMatch[1].toLowerCase())) {
|
|
463
|
+
const key = optNoColonMatch[1].toLowerCase();
|
|
464
|
+
const value = optNoColonMatch[2].trim();
|
|
414
465
|
result.options.optionLineNumbers[key] = lineNumber;
|
|
415
466
|
|
|
416
467
|
switch (key) {
|
|
@@ -422,26 +473,21 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
422
473
|
result.options.titleLineNumber = lineNumber;
|
|
423
474
|
break;
|
|
424
475
|
case 'orientation':
|
|
425
|
-
|
|
426
|
-
result.options.orientation = value;
|
|
427
|
-
} else {
|
|
428
|
-
warn(lineNumber, `Invalid orientation: "${value}". Expected "horizontal" or "vertical".`);
|
|
429
|
-
}
|
|
476
|
+
warn(lineNumber, `'orientation' is not supported for gantt charts`);
|
|
430
477
|
break;
|
|
431
478
|
case 'today-marker':
|
|
432
|
-
if (
|
|
433
|
-
result.options.todayMarker = value;
|
|
434
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
479
|
+
if (/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?$/.test(value)) {
|
|
435
480
|
result.options.todayMarker = value;
|
|
436
481
|
} else {
|
|
437
|
-
warn(lineNumber, `Invalid today-marker value: "${value}". Expected
|
|
482
|
+
warn(lineNumber, `Invalid today-marker value: "${value}". Expected YYYY-MM-DD.`);
|
|
438
483
|
}
|
|
439
484
|
break;
|
|
440
485
|
case 'critical-path':
|
|
441
|
-
result.options.criticalPath =
|
|
486
|
+
result.options.criticalPath = true;
|
|
442
487
|
break;
|
|
443
488
|
case 'dependencies':
|
|
444
|
-
|
|
489
|
+
// Boolean with value — but `dependencies` is now default ON, so only `no-dependencies` turns it off
|
|
490
|
+
result.options.dependencies = true;
|
|
445
491
|
break;
|
|
446
492
|
case 'sort':
|
|
447
493
|
if (value === 'tag' || value.startsWith('tag:')) {
|
|
@@ -460,6 +506,29 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
460
506
|
|
|
461
507
|
inHeaderBlock = false;
|
|
462
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
|
+
|
|
463
532
|
// ── Parallel block ────────────────────────────────────
|
|
464
533
|
|
|
465
534
|
if (line === 'parallel') {
|
|
@@ -583,11 +652,12 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
583
652
|
if (depMatch) {
|
|
584
653
|
// Dependency without a task context is an error
|
|
585
654
|
if (!lastTaskNode) {
|
|
586
|
-
softError(lineNumber, `Dependency "-> ${depMatch[
|
|
655
|
+
softError(lineNumber, `Dependency "-> ${depMatch[2]}" must be indented under a task.`);
|
|
587
656
|
continue;
|
|
588
657
|
}
|
|
589
658
|
// This happens when the dep is at the same indent as the task
|
|
590
|
-
const
|
|
659
|
+
const label = depMatch[1]?.trim() || undefined;
|
|
660
|
+
const depParts = depMatch[2].split('|');
|
|
591
661
|
const targetName = depParts[0].trim();
|
|
592
662
|
let offset: Offset | undefined;
|
|
593
663
|
|
|
@@ -595,7 +665,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
595
665
|
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
596
666
|
if (meta.lag || meta.lead) {
|
|
597
667
|
const key = meta.lag ? 'lag' : 'lead';
|
|
598
|
-
|
|
668
|
+
softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
599
669
|
}
|
|
600
670
|
if (meta.offset) {
|
|
601
671
|
const raw = meta.offset;
|
|
@@ -610,13 +680,13 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
610
680
|
}
|
|
611
681
|
}
|
|
612
682
|
|
|
613
|
-
lastTaskNode.dependencies.push({ targetName, offset, lineNumber });
|
|
683
|
+
lastTaskNode.dependencies.push({ targetName, label, offset, lineNumber });
|
|
614
684
|
continue;
|
|
615
685
|
}
|
|
616
686
|
|
|
617
687
|
// ── Bare label = parse error ──────────────────────────
|
|
618
688
|
|
|
619
|
-
softError(lineNumber, `Expected duration (e.g., "10d
|
|
689
|
+
softError(lineNumber, `Expected duration (e.g., "10d Task"), group brackets (e.g., "[Group]"), or keyword. Got: "${line}"`);
|
|
620
690
|
continue;
|
|
621
691
|
}
|
|
622
692
|
|
|
@@ -631,7 +701,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
631
701
|
|
|
632
702
|
// Validate sort: tag requires tag groups
|
|
633
703
|
if (result.options.sort === 'tag' && result.tagGroups.length === 0) {
|
|
634
|
-
warn(0, 'sort
|
|
704
|
+
warn(0, 'sort tag has no effect — no tag groups defined.');
|
|
635
705
|
result.options.sort = 'default';
|
|
636
706
|
}
|
|
637
707
|
|
|
@@ -677,7 +747,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
677
747
|
// Reject lag/lead — use offset instead
|
|
678
748
|
if (metadata.lag || metadata.lead) {
|
|
679
749
|
const key = metadata.lag ? 'lag' : 'lead';
|
|
680
|
-
softError(ln, `
|
|
750
|
+
softError(ln, `"${key}" is no longer supported — use "offset: ${metadata[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
681
751
|
}
|
|
682
752
|
|
|
683
753
|
// Extract task-level offset from metadata
|
|
@@ -767,6 +837,11 @@ const KNOWN_OPTIONS = new Set([
|
|
|
767
837
|
'critical-path', 'dependencies', 'chart', 'sort',
|
|
768
838
|
]);
|
|
769
839
|
|
|
840
|
+
/** Boolean options that can appear as bare keywords or with `no-` prefix. */
|
|
841
|
+
const KNOWN_BOOLEANS = new Set([
|
|
842
|
+
'critical-path', 'today-marker', 'dependencies',
|
|
843
|
+
]);
|
|
844
|
+
|
|
770
845
|
function isKnownOption(key: string): boolean {
|
|
771
846
|
return KNOWN_OPTIONS.has(key);
|
|
772
847
|
}
|
package/src/gantt/renderer.ts
CHANGED
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
LEGEND_HEIGHT,
|
|
15
15
|
LEGEND_PILL_PAD,
|
|
16
16
|
LEGEND_PILL_FONT_SIZE,
|
|
17
|
-
LEGEND_PILL_FONT_W,
|
|
18
17
|
LEGEND_CAPSULE_PAD,
|
|
19
18
|
LEGEND_DOT_R,
|
|
20
19
|
LEGEND_ENTRY_FONT_SIZE,
|
|
21
|
-
LEGEND_ENTRY_FONT_W,
|
|
22
20
|
LEGEND_ENTRY_DOT_GAP,
|
|
23
21
|
LEGEND_ENTRY_TRAIL,
|
|
24
22
|
LEGEND_GROUP_GAP,
|
|
25
23
|
LEGEND_ICON_W,
|
|
24
|
+
measureLegendText,
|
|
26
25
|
} from '../utils/legend-constants';
|
|
26
|
+
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
|
|
27
27
|
import type { PaletteColors } from '../palettes';
|
|
28
28
|
import type { D3ExportDimensions } from '../d3';
|
|
29
29
|
import type { ResolvedSchedule, ResolvedTask, ResolvedGroup, Weekday } from './types';
|
|
@@ -258,8 +258,8 @@ export function renderGantt(
|
|
|
258
258
|
.select(container)
|
|
259
259
|
.append('svg')
|
|
260
260
|
.attr('viewBox', `0 0 ${containerWidth} ${outerHeight}`)
|
|
261
|
-
.attr('width', containerWidth)
|
|
262
|
-
.attr('
|
|
261
|
+
.attr('width', exportDims ? containerWidth : '100%')
|
|
262
|
+
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
263
263
|
.attr('font-family', FONT_FAMILY)
|
|
264
264
|
.style('overflow', 'visible');
|
|
265
265
|
|
|
@@ -273,10 +273,10 @@ export function renderGantt(
|
|
|
273
273
|
svg
|
|
274
274
|
.append('text')
|
|
275
275
|
.attr('x', containerWidth / 2)
|
|
276
|
-
.attr('y',
|
|
276
|
+
.attr('y', TITLE_Y)
|
|
277
277
|
.attr('text-anchor', 'middle')
|
|
278
|
-
.attr('font-size',
|
|
279
|
-
.attr('font-weight',
|
|
278
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
279
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
280
280
|
.attr('fill', palette.text)
|
|
281
281
|
.text(title);
|
|
282
282
|
}
|
|
@@ -1251,6 +1251,38 @@ function renderDependencyArrows(
|
|
|
1251
1251
|
.attr('points', arrowheadPoints(tx, ty, headSize, angle))
|
|
1252
1252
|
.attr('fill', arrowColor)
|
|
1253
1253
|
.attr('opacity', 0.5);
|
|
1254
|
+
|
|
1255
|
+
// Label at midpoint of the dependency path
|
|
1256
|
+
if (dep.label) {
|
|
1257
|
+
const midX = (sx + tx) / 2;
|
|
1258
|
+
const midY = (sy + ty) / 2;
|
|
1259
|
+
// Background rect for readability
|
|
1260
|
+
const labelEl = g.append('text')
|
|
1261
|
+
.attr('class', 'gantt-dep-label')
|
|
1262
|
+
.attr('data-dep-from', rt.task.id)
|
|
1263
|
+
.attr('data-dep-to', targetTask.task.id)
|
|
1264
|
+
.attr('x', midX)
|
|
1265
|
+
.attr('y', midY - 4)
|
|
1266
|
+
.attr('text-anchor', 'middle')
|
|
1267
|
+
.attr('font-size', 10)
|
|
1268
|
+
.attr('font-family', FONT_FAMILY)
|
|
1269
|
+
.attr('fill', palette.text)
|
|
1270
|
+
.attr('opacity', 0.7)
|
|
1271
|
+
.text(dep.label);
|
|
1272
|
+
// Insert a background rect behind the text for contrast
|
|
1273
|
+
const bbox = (labelEl.node() as SVGTextElement)?.getBBox?.();
|
|
1274
|
+
if (bbox && bbox.width > 0) {
|
|
1275
|
+
g.insert('rect', '.gantt-dep-label:last-of-type')
|
|
1276
|
+
.attr('class', 'gantt-dep-label-bg')
|
|
1277
|
+
.attr('x', bbox.x - 2)
|
|
1278
|
+
.attr('y', bbox.y - 1)
|
|
1279
|
+
.attr('width', bbox.width + 4)
|
|
1280
|
+
.attr('height', bbox.height + 2)
|
|
1281
|
+
.attr('fill', palette.bg)
|
|
1282
|
+
.attr('opacity', 0.8)
|
|
1283
|
+
.attr('rx', 2);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1254
1286
|
}
|
|
1255
1287
|
}
|
|
1256
1288
|
}
|
|
@@ -1407,13 +1439,13 @@ function renderTagLegend(
|
|
|
1407
1439
|
const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1408
1440
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1409
1441
|
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1410
|
-
const pillW = group.name
|
|
1442
|
+
const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
|
|
1411
1443
|
let groupW = pillW;
|
|
1412
1444
|
if (isActive) {
|
|
1413
1445
|
const entries = filteredEntries.get(group.name.toLowerCase()) ?? group.entries;
|
|
1414
1446
|
let entriesW = 0;
|
|
1415
1447
|
for (const entry of entries) {
|
|
1416
|
-
entriesW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value
|
|
1448
|
+
entriesW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1417
1449
|
}
|
|
1418
1450
|
groupW = LEGEND_CAPSULE_PAD * 2 + pillW + 4 + entriesW;
|
|
1419
1451
|
} else if (isSwimlane && !isActive) {
|
|
@@ -1427,7 +1459,7 @@ function renderTagLegend(
|
|
|
1427
1459
|
|
|
1428
1460
|
// Critical Path pill width
|
|
1429
1461
|
const cpLabel = 'Critical Path';
|
|
1430
|
-
const cpPillW = cpLabel
|
|
1462
|
+
const cpPillW = measureLegendText(cpLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1431
1463
|
if (hasCriticalPath) {
|
|
1432
1464
|
if (visibleGroups.length > 0) totalW += LEGEND_GROUP_GAP;
|
|
1433
1465
|
totalW += cpPillW;
|
|
@@ -1449,7 +1481,7 @@ function renderTagLegend(
|
|
|
1449
1481
|
const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1450
1482
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1451
1483
|
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1452
|
-
const pillW = group.name
|
|
1484
|
+
const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
|
|
1453
1485
|
const pillH = isActive ? LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2 : LEGEND_HEIGHT;
|
|
1454
1486
|
const groupW = groupWidths[i];
|
|
1455
1487
|
|
|
@@ -1496,7 +1528,7 @@ function renderTagLegend(
|
|
|
1496
1528
|
}
|
|
1497
1529
|
|
|
1498
1530
|
// Pill text (offset to leave room for icon on right)
|
|
1499
|
-
const textW = group.name
|
|
1531
|
+
const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1500
1532
|
gEl.append('text')
|
|
1501
1533
|
.attr('x', pillXOff + textW / 2)
|
|
1502
1534
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
@@ -1589,7 +1621,7 @@ function renderTagLegend(
|
|
|
1589
1621
|
}
|
|
1590
1622
|
});
|
|
1591
1623
|
|
|
1592
|
-
ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value
|
|
1624
|
+
ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1593
1625
|
}
|
|
1594
1626
|
}
|
|
1595
1627
|
|
|
@@ -2272,7 +2304,11 @@ function diamondPoints(cx: number, cy: number, size: number): string {
|
|
|
2272
2304
|
const MONTH_ABBR = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
2273
2305
|
|
|
2274
2306
|
function formatGanttDate(d: Date): string {
|
|
2275
|
-
|
|
2307
|
+
const base = `${MONTH_ABBR[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
2308
|
+
if (d.getHours() === 0 && d.getMinutes() === 0) return base;
|
|
2309
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
2310
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
2311
|
+
return `${base} ${hh}:${mm}`;
|
|
2276
2312
|
}
|
|
2277
2313
|
|
|
2278
2314
|
function showGanttDateIndicators(
|
package/src/gantt/types.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { TagGroup } from '../utils/tag-groups';
|
|
|
7
7
|
|
|
8
8
|
// ── Duration ────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
/** Calendar units: d (days), w (weeks), m (months), q (quarters), y (years). bd = business days. */
|
|
11
|
-
export type DurationUnit = 'd' | 'bd' | 'w' | 'm' | 'q' | 'y';
|
|
10
|
+
/** Calendar units: d (days), w (weeks), m (months), q (quarters), y (years), h (hours), min (minutes). bd = business days. */
|
|
11
|
+
export type DurationUnit = 'd' | 'bd' | 'w' | 'm' | 'q' | 'y' | 'h' | 'min';
|
|
12
12
|
|
|
13
13
|
export interface Duration {
|
|
14
14
|
amount: number;
|
|
@@ -24,6 +24,7 @@ export interface Offset {
|
|
|
24
24
|
|
|
25
25
|
export interface GanttDependency {
|
|
26
26
|
targetName: string; // raw string from `-> X` or `-> Group.X`
|
|
27
|
+
label?: string; // optional label from `-label-> X` syntax
|
|
27
28
|
offset?: Offset;
|
|
28
29
|
lineNumber: number;
|
|
29
30
|
}
|
|
@@ -109,7 +110,6 @@ export interface GanttOptions {
|
|
|
109
110
|
start: string | null; // YYYY[-MM[-DD]] or null for relative timeline
|
|
110
111
|
title: string | null;
|
|
111
112
|
titleLineNumber: number | null;
|
|
112
|
-
orientation: 'horizontal' | 'vertical';
|
|
113
113
|
todayMarker: 'off' | 'on' | string; // 'on' = current date, string = YYYY-MM-DD
|
|
114
114
|
criticalPath: boolean;
|
|
115
115
|
dependencies: boolean;
|