@diagrammo/dgmo 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +180 -180
- package/dist/index.cjs +415 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +415 -144
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/parser.ts +3 -2
- package/src/d3.ts +7 -9
- package/src/er/parser.ts +5 -3
- package/src/gantt/calculator.ts +51 -11
- package/src/gantt/parser.ts +31 -20
- package/src/gantt/renderer.ts +524 -132
- package/src/gantt/types.ts +4 -0
- package/src/org/parser.ts +7 -5
- package/src/sequence/parser.ts +10 -9
- package/src/sitemap/parser.ts +5 -3
- package/src/utils/parsing.ts +23 -12
package/package.json
CHANGED
package/src/c4/parser.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
measureIndent,
|
|
12
12
|
extractColor,
|
|
13
13
|
parsePipeMetadata,
|
|
14
|
+
MULTIPLE_PIPE_WARNING,
|
|
14
15
|
CHART_TYPE_RE,
|
|
15
16
|
TITLE_RE,
|
|
16
17
|
OPTION_RE,
|
|
@@ -411,7 +412,7 @@ export function parseC4(
|
|
|
411
412
|
// Otherwise it's a deployment node (possibly with pipe metadata)
|
|
412
413
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
413
414
|
const nodeName = segments[0];
|
|
414
|
-
const metadata = parsePipeMetadata(segments, aliasMap);
|
|
415
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
|
|
415
416
|
const shape = inferC4Shape(nodeName, metadata.tech ?? metadata.technology);
|
|
416
417
|
|
|
417
418
|
const dNode: C4DeploymentNode = {
|
|
@@ -598,7 +599,7 @@ export function parseC4(
|
|
|
598
599
|
namePart = namePart.substring(0, isAMatch.index!).trim();
|
|
599
600
|
}
|
|
600
601
|
|
|
601
|
-
const metadata = parsePipeMetadata(segments, aliasMap);
|
|
602
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
|
|
602
603
|
|
|
603
604
|
// Determine shape: explicit > inference
|
|
604
605
|
const shape =
|
package/src/d3.ts
CHANGED
|
@@ -181,7 +181,7 @@ import { getSeriesColors } from './palettes';
|
|
|
181
181
|
import { mix } from './palettes/color-utils';
|
|
182
182
|
import type { DgmoError } from './diagnostics';
|
|
183
183
|
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
184
|
-
import { collectIndentedValues, extractColor, parsePipeMetadata } from './utils/parsing';
|
|
184
|
+
import { collectIndentedValues, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING } from './utils/parsing';
|
|
185
185
|
import { matchTagBlockHeading, validateTagValues, resolveTagColor } from './utils/tag-groups';
|
|
186
186
|
import type { TagGroup } from './utils/tag-groups';
|
|
187
187
|
import {
|
|
@@ -545,9 +545,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
545
545
|
continue;
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
// Timeline marker lines: marker YYYY
|
|
548
|
+
// Timeline marker lines: marker: YYYY Label (color)
|
|
549
549
|
const markerMatch = line.match(
|
|
550
|
-
/^marker
|
|
550
|
+
/^marker:\s+(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
551
551
|
);
|
|
552
552
|
if (markerMatch) {
|
|
553
553
|
const colorAnnotation = markerMatch[3]?.trim() || null;
|
|
@@ -579,7 +579,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
579
579
|
const endDate = addDurationToDate(startDate, amount, unit);
|
|
580
580
|
const segments = durationMatch[5].split('|');
|
|
581
581
|
const metadata = segments.length > 1
|
|
582
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap)
|
|
582
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
|
|
583
583
|
: {};
|
|
584
584
|
result.timelineEvents.push({
|
|
585
585
|
date: startDate,
|
|
@@ -600,7 +600,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
600
600
|
if (rangeMatch) {
|
|
601
601
|
const segments = rangeMatch[4].split('|');
|
|
602
602
|
const metadata = segments.length > 1
|
|
603
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap)
|
|
603
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
|
|
604
604
|
: {};
|
|
605
605
|
result.timelineEvents.push({
|
|
606
606
|
date: rangeMatch[1],
|
|
@@ -621,7 +621,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
621
621
|
if (pointMatch) {
|
|
622
622
|
const segments = pointMatch[2].split('|');
|
|
623
623
|
const metadata = segments.length > 1
|
|
624
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap)
|
|
624
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
|
|
625
625
|
: {};
|
|
626
626
|
result.timelineEvents.push({
|
|
627
627
|
date: pointMatch[1],
|
|
@@ -6095,10 +6095,8 @@ export async function renderForExport(
|
|
|
6095
6095
|
|
|
6096
6096
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6097
6097
|
const ganttParsed = parseGantt(content, effectivePalette);
|
|
6098
|
-
if (ganttParsed.error) return '';
|
|
6099
|
-
|
|
6100
6098
|
const resolved = calculateSchedule(ganttParsed);
|
|
6101
|
-
if (resolved.
|
|
6099
|
+
if (resolved.tasks.length === 0) return '';
|
|
6102
6100
|
|
|
6103
6101
|
const EXPORT_W = 1200;
|
|
6104
6102
|
const EXPORT_H = 800;
|
package/src/er/parser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
-
import { measureIndent, extractColor, parsePipeMetadata } from '../utils/parsing';
|
|
4
|
+
import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING } from '../utils/parsing';
|
|
5
5
|
import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
6
6
|
import type { TagGroup } from '../utils/tag-groups';
|
|
7
7
|
import type {
|
|
@@ -350,8 +350,10 @@ export function parseERDiagram(
|
|
|
350
350
|
// Parse pipe metadata: TableName(color) | key: value, key2: value2
|
|
351
351
|
const pipeStr = tableDecl[3]?.trim();
|
|
352
352
|
if (pipeStr) {
|
|
353
|
-
//
|
|
354
|
-
const
|
|
353
|
+
// Split on additional pipes (treated as commas) and warn if found
|
|
354
|
+
const pipeSegments = pipeStr.split('|');
|
|
355
|
+
const meta = parsePipeMetadata(['', ...pipeSegments], aliasMap,
|
|
356
|
+
() => result.diagnostics.push(makeDgmoError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning')));
|
|
355
357
|
Object.assign(table.metadata, meta);
|
|
356
358
|
}
|
|
357
359
|
|
package/src/gantt/calculator.ts
CHANGED
|
@@ -119,11 +119,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
119
119
|
for (const dep of task.dependencies) {
|
|
120
120
|
const resolved = resolveTaskName(dep.targetName, allTasks);
|
|
121
121
|
if (isResolverError(resolved)) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} else {
|
|
125
|
-
return fail(dep.lineNumber, `\`-> ${dep.targetName}\` — ${resolved.message}`);
|
|
126
|
-
}
|
|
122
|
+
warn(dep.lineNumber, `\`-> ${dep.targetName}\` — ${resolved.message}`);
|
|
123
|
+
continue;
|
|
127
124
|
}
|
|
128
125
|
|
|
129
126
|
// The dependency means: target starts after source
|
|
@@ -146,15 +143,34 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
146
143
|
|
|
147
144
|
// ── Topological sort with cycle detection ───────────────
|
|
148
145
|
|
|
149
|
-
|
|
146
|
+
let sortedIds = topologicalSort(taskMap);
|
|
150
147
|
if (!sortedIds) {
|
|
151
|
-
// Find cycle
|
|
148
|
+
// Find cycle, warn, and break it by removing one explicit dep edge
|
|
152
149
|
const cycle = findCycle(taskMap);
|
|
153
150
|
const cycleStr = cycle.map(id => taskMap.get(id)!.task.label).join(' → ');
|
|
154
|
-
|
|
151
|
+
warn(
|
|
155
152
|
taskMap.get(cycle[0])!.task.lineNumber,
|
|
156
|
-
`Circular dependency detected: ${cycleStr}
|
|
153
|
+
`Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped.`,
|
|
157
154
|
);
|
|
155
|
+
|
|
156
|
+
// Remove the last edge in the cycle to break it
|
|
157
|
+
// (prefer removing explicit -> deps over implicit sequential ones)
|
|
158
|
+
breakCycle(cycle, taskMap, depOffsetMap);
|
|
159
|
+
|
|
160
|
+
// Retry — if still cyclic after breaking, keep breaking until resolved
|
|
161
|
+
sortedIds = topologicalSort(taskMap);
|
|
162
|
+
let safety = 10;
|
|
163
|
+
while (!sortedIds && safety-- > 0) {
|
|
164
|
+
const nextCycle = findCycle(taskMap);
|
|
165
|
+
if (nextCycle.length === 0) break;
|
|
166
|
+
breakCycle(nextCycle, taskMap, depOffsetMap);
|
|
167
|
+
sortedIds = topologicalSort(taskMap);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!sortedIds) {
|
|
171
|
+
// Truly unresolvable — fall back to task insertion order
|
|
172
|
+
sortedIds = [...taskMap.keys()];
|
|
173
|
+
}
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
// ── Forward pass: resolve dates ─────────────────────────
|
|
@@ -521,6 +537,30 @@ function findCycle(taskMap: Map<string, TaskNode>): string[] {
|
|
|
521
537
|
}
|
|
522
538
|
}
|
|
523
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Break a cycle by removing the last edge. The cycle array is [A, B, ..., A]
|
|
542
|
+
* (starts and ends with the same node). We remove A's predecessor edge to
|
|
543
|
+
* the penultimate node, which breaks the cycle.
|
|
544
|
+
*/
|
|
545
|
+
function breakCycle(
|
|
546
|
+
cycle: string[],
|
|
547
|
+
taskMap: Map<string, TaskNode>,
|
|
548
|
+
depOffsetMap: Map<string, Offset>,
|
|
549
|
+
): void {
|
|
550
|
+
if (cycle.length < 3) return; // need at least [A, B, A]
|
|
551
|
+
// Remove the edge from second-to-last → first (i.e. the edge that closes the cycle)
|
|
552
|
+
const fromId = cycle[cycle.length - 2];
|
|
553
|
+
const toId = cycle[0];
|
|
554
|
+
const toNode = taskMap.get(toId);
|
|
555
|
+
if (toNode) {
|
|
556
|
+
const idx = toNode.predecessors.indexOf(fromId);
|
|
557
|
+
if (idx !== -1) {
|
|
558
|
+
toNode.predecessors.splice(idx, 1);
|
|
559
|
+
depOffsetMap.delete(`${fromId}->${toId}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
524
564
|
// ── Critical path ───────────────────────────────────────────
|
|
525
565
|
|
|
526
566
|
function computeCriticalPath(
|
|
@@ -641,10 +681,10 @@ function buildResolvedGroups(
|
|
|
641
681
|
if (!resolved?.startDate || !resolved?.endDate) continue;
|
|
642
682
|
if (resolved.startDate.getTime() < minStart) minStart = resolved.startDate.getTime();
|
|
643
683
|
if (resolved.endDate.getTime() > maxEnd) maxEnd = resolved.endDate.getTime();
|
|
684
|
+
const dur = resolved.endDate.getTime() - resolved.startDate.getTime();
|
|
685
|
+
totalDuration += dur;
|
|
644
686
|
if (task.progress !== null) {
|
|
645
|
-
const dur = resolved.endDate.getTime() - resolved.startDate.getTime();
|
|
646
687
|
totalProgress += task.progress * dur;
|
|
647
|
-
totalDuration += dur;
|
|
648
688
|
hasProgress = true;
|
|
649
689
|
}
|
|
650
690
|
}
|
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 } from '../utils/parsing';
|
|
9
|
+
import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING } from '../utils/parsing';
|
|
10
10
|
import { parseOffset } from '../utils/duration';
|
|
11
11
|
import type { PaletteColors } from '../palettes';
|
|
12
12
|
import { resolveColor } from '../colors';
|
|
@@ -51,8 +51,8 @@ const COMMENT_RE = /^\/\//;
|
|
|
51
51
|
/** Era: `era YYYY[-MM[-DD]] -> YYYY[-MM[-DD]]: Label (color?)` */
|
|
52
52
|
const ERA_RE = /^era\s+(\d{4}(?:-\d{2}(?:-\d{2})?)?)\s*->\s*(\d{4}(?:-\d{2}(?:-\d{2})?)?)\s*:\s*(.+)$/i;
|
|
53
53
|
|
|
54
|
-
/** Marker: `marker YYYY[-MM[-DD]]
|
|
55
|
-
const MARKER_RE = /^marker
|
|
54
|
+
/** Marker: `marker: YYYY[-MM[-DD]] Label (color?)` */
|
|
55
|
+
const MARKER_RE = /^marker:\s+(\d{4}(?:-\d{2}(?:-\d{2})?)?)\s+(.+)$/i;
|
|
56
56
|
|
|
57
57
|
/** Holiday date: `2024-01-15: Label` */
|
|
58
58
|
const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2}):\s*(.+)$/;
|
|
@@ -107,6 +107,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
107
107
|
dependencies: false,
|
|
108
108
|
sort: 'default',
|
|
109
109
|
defaultSwimlaneGroup: null,
|
|
110
|
+
optionLineNumbers: {},
|
|
111
|
+
holidaysLineNumber: null,
|
|
110
112
|
},
|
|
111
113
|
diagnostics,
|
|
112
114
|
error: null,
|
|
@@ -123,6 +125,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
123
125
|
diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
124
126
|
};
|
|
125
127
|
|
|
128
|
+
/** Red squiggly but parsing continues — line is wrong, rest of chart is fine */
|
|
129
|
+
const softError = (line: number, message: string): void => {
|
|
130
|
+
diagnostics.push(makeDgmoError(line, message, 'error'));
|
|
131
|
+
};
|
|
132
|
+
|
|
126
133
|
// ── Alias map for pipe metadata ─────────────────────────
|
|
127
134
|
|
|
128
135
|
const aliasMap = new Map<string, string>();
|
|
@@ -300,10 +307,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
300
307
|
let offset: Offset | undefined;
|
|
301
308
|
|
|
302
309
|
if (depParts.length > 1) {
|
|
303
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap);
|
|
310
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
304
311
|
if (meta.lag || meta.lead) {
|
|
305
312
|
const key = meta.lag ? 'lag' : 'lead';
|
|
306
|
-
|
|
313
|
+
softError(lineNumber, `Unknown keyword "${key}". Use "offset: ${meta[key]}" instead.`);
|
|
307
314
|
}
|
|
308
315
|
if (meta.offset) {
|
|
309
316
|
const raw = meta.offset;
|
|
@@ -346,6 +353,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
346
353
|
inHolidaysBlock = true;
|
|
347
354
|
holidaysBlockIndent = indent;
|
|
348
355
|
inHeaderBlock = false;
|
|
356
|
+
result.options.holidaysLineNumber = lineNumber;
|
|
349
357
|
continue;
|
|
350
358
|
}
|
|
351
359
|
|
|
@@ -377,6 +385,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
377
385
|
endDate: eraMatch[2],
|
|
378
386
|
label: eraExtracted.label,
|
|
379
387
|
color: eraExtracted.color || null,
|
|
388
|
+
lineNumber,
|
|
380
389
|
});
|
|
381
390
|
inHeaderBlock = false;
|
|
382
391
|
continue;
|
|
@@ -402,6 +411,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
402
411
|
if (optMatch && isKnownOption(optMatch[1].toLowerCase())) {
|
|
403
412
|
const key = optMatch[1].toLowerCase();
|
|
404
413
|
const value = optMatch[2].trim();
|
|
414
|
+
result.options.optionLineNumbers[key] = lineNumber;
|
|
405
415
|
|
|
406
416
|
switch (key) {
|
|
407
417
|
case 'start':
|
|
@@ -470,7 +480,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
470
480
|
if (groupMatch) {
|
|
471
481
|
// Validate nesting: group under a task is invalid
|
|
472
482
|
if (blockStack.length > 0 && blockStack[blockStack.length - 1].containerType === 'task') {
|
|
473
|
-
|
|
483
|
+
softError(lineNumber, `Cannot nest a group inside a task. Groups must be inside other groups or parallel blocks.`);
|
|
484
|
+
continue;
|
|
474
485
|
}
|
|
475
486
|
|
|
476
487
|
const afterBrackets = groupMatch[2].trim();
|
|
@@ -480,11 +491,12 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
480
491
|
let metadata: Record<string, string> = {};
|
|
481
492
|
let color: string | null = null;
|
|
482
493
|
|
|
494
|
+
const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_WARNING);
|
|
483
495
|
if (segments.length > 0 && segments[0].trim()) {
|
|
484
496
|
// Check if first segment after brackets is pipe metadata
|
|
485
|
-
metadata = parsePipeMetadata(['', ...segments], aliasMap);
|
|
497
|
+
metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
|
|
486
498
|
} else if (segments.length > 1) {
|
|
487
|
-
metadata = parsePipeMetadata(['', ...segments.slice(1)], aliasMap);
|
|
499
|
+
metadata = parsePipeMetadata(['', ...segments.slice(1)], aliasMap, pipeWarn);
|
|
488
500
|
}
|
|
489
501
|
|
|
490
502
|
// Extract color from group name if present
|
|
@@ -522,7 +534,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
522
534
|
const labelRaw = timelineDurMatch[5];
|
|
523
535
|
|
|
524
536
|
const task = makeTask(labelRaw, { amount, unit }, uncertain, lineNumber, startDate);
|
|
525
|
-
if (result.error) return result;
|
|
526
537
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
527
538
|
currentContainer().push(taskNode);
|
|
528
539
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
@@ -540,7 +551,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
540
551
|
const labelRaw = durMatch[4];
|
|
541
552
|
|
|
542
553
|
const task = makeTask(labelRaw, { amount, unit }, uncertain, lineNumber);
|
|
543
|
-
if (result.error) return result;
|
|
544
554
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
545
555
|
currentContainer().push(taskNode);
|
|
546
556
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
@@ -559,7 +569,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
559
569
|
lineNumber,
|
|
560
570
|
explicitDateMatch[1],
|
|
561
571
|
);
|
|
562
|
-
if (result.error) return result;
|
|
563
572
|
// Explicit date tasks with no duration are milestones
|
|
564
573
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
565
574
|
currentContainer().push(taskNode);
|
|
@@ -574,7 +583,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
574
583
|
if (depMatch) {
|
|
575
584
|
// Dependency without a task context is an error
|
|
576
585
|
if (!lastTaskNode) {
|
|
577
|
-
|
|
586
|
+
softError(lineNumber, `Dependency "-> ${depMatch[1]}" must be indented under a task.`);
|
|
587
|
+
continue;
|
|
578
588
|
}
|
|
579
589
|
// This happens when the dep is at the same indent as the task
|
|
580
590
|
const depParts = depMatch[1].split('|');
|
|
@@ -582,7 +592,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
582
592
|
let offset: Offset | undefined;
|
|
583
593
|
|
|
584
594
|
if (depParts.length > 1) {
|
|
585
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap);
|
|
595
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
586
596
|
if (meta.lag || meta.lead) {
|
|
587
597
|
const key = meta.lag ? 'lag' : 'lead';
|
|
588
598
|
warn(lineNumber, `"${key}" is deprecated — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
@@ -606,7 +616,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
606
616
|
|
|
607
617
|
// ── Bare label = parse error ──────────────────────────
|
|
608
618
|
|
|
609
|
-
|
|
619
|
+
softError(lineNumber, `Expected duration (e.g., "10d: Task"), group brackets (e.g., "[Group]"), or keyword. Got: "${line}"`);
|
|
620
|
+
continue;
|
|
610
621
|
}
|
|
611
622
|
|
|
612
623
|
// ── Finalize ────────────────────────────────────────────
|
|
@@ -640,12 +651,12 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
640
651
|
|
|
641
652
|
// Check for reserved keyword
|
|
642
653
|
if (label.toLowerCase() === 'parallel') {
|
|
643
|
-
|
|
654
|
+
softError(ln, `"parallel" is a reserved keyword and cannot be used as a task name.`);
|
|
644
655
|
}
|
|
645
656
|
|
|
646
657
|
// Parse pipe metadata
|
|
647
658
|
const metadata = segments.length > 1
|
|
648
|
-
? parsePipeMetadata(segments, aliasMap)
|
|
659
|
+
? parsePipeMetadata(segments, aliasMap, () => warn(ln, MULTIPLE_PIPE_WARNING))
|
|
649
660
|
: {};
|
|
650
661
|
|
|
651
662
|
// Extract progress from metadata or shorthand
|
|
@@ -654,9 +665,9 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
654
665
|
progress = parseFloat(metadata.progress);
|
|
655
666
|
delete metadata.progress;
|
|
656
667
|
}
|
|
657
|
-
// Check for progress shorthand: `| 80%`
|
|
658
|
-
for (
|
|
659
|
-
const seg =
|
|
668
|
+
// Check for progress shorthand: `| 80%` or `| t:X, 80%`
|
|
669
|
+
for (const part of segments.slice(1).join(',').split(',')) {
|
|
670
|
+
const seg = part.trim();
|
|
660
671
|
const progressMatch = seg.match(/^(\d+)%$/);
|
|
661
672
|
if (progressMatch) {
|
|
662
673
|
progress = parseInt(progressMatch[1], 10);
|
|
@@ -666,7 +677,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
666
677
|
// Reject lag/lead — use offset instead
|
|
667
678
|
if (metadata.lag || metadata.lead) {
|
|
668
679
|
const key = metadata.lag ? 'lag' : 'lead';
|
|
669
|
-
|
|
680
|
+
softError(ln, `Unknown keyword "${key}". Use "offset: ${metadata[key]}" instead.`);
|
|
670
681
|
}
|
|
671
682
|
|
|
672
683
|
// Extract task-level offset from metadata
|