@diagrammo/dgmo 0.7.0 → 0.7.1
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 +178 -178
- package/dist/index.cjs +218 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +218 -87
- 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 +26 -20
- package/src/gantt/renderer.ts +177 -51
- 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*(.+)$/;
|
|
@@ -123,6 +123,11 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
123
123
|
diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
/** Red squiggly but parsing continues — line is wrong, rest of chart is fine */
|
|
127
|
+
const softError = (line: number, message: string): void => {
|
|
128
|
+
diagnostics.push(makeDgmoError(line, message, 'error'));
|
|
129
|
+
};
|
|
130
|
+
|
|
126
131
|
// ── Alias map for pipe metadata ─────────────────────────
|
|
127
132
|
|
|
128
133
|
const aliasMap = new Map<string, string>();
|
|
@@ -300,10 +305,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
300
305
|
let offset: Offset | undefined;
|
|
301
306
|
|
|
302
307
|
if (depParts.length > 1) {
|
|
303
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap);
|
|
308
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
304
309
|
if (meta.lag || meta.lead) {
|
|
305
310
|
const key = meta.lag ? 'lag' : 'lead';
|
|
306
|
-
|
|
311
|
+
softError(lineNumber, `Unknown keyword "${key}". Use "offset: ${meta[key]}" instead.`);
|
|
307
312
|
}
|
|
308
313
|
if (meta.offset) {
|
|
309
314
|
const raw = meta.offset;
|
|
@@ -470,7 +475,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
470
475
|
if (groupMatch) {
|
|
471
476
|
// Validate nesting: group under a task is invalid
|
|
472
477
|
if (blockStack.length > 0 && blockStack[blockStack.length - 1].containerType === 'task') {
|
|
473
|
-
|
|
478
|
+
softError(lineNumber, `Cannot nest a group inside a task. Groups must be inside other groups or parallel blocks.`);
|
|
479
|
+
continue;
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
const afterBrackets = groupMatch[2].trim();
|
|
@@ -480,11 +486,12 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
480
486
|
let metadata: Record<string, string> = {};
|
|
481
487
|
let color: string | null = null;
|
|
482
488
|
|
|
489
|
+
const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_WARNING);
|
|
483
490
|
if (segments.length > 0 && segments[0].trim()) {
|
|
484
491
|
// Check if first segment after brackets is pipe metadata
|
|
485
|
-
metadata = parsePipeMetadata(['', ...segments], aliasMap);
|
|
492
|
+
metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
|
|
486
493
|
} else if (segments.length > 1) {
|
|
487
|
-
metadata = parsePipeMetadata(['', ...segments.slice(1)], aliasMap);
|
|
494
|
+
metadata = parsePipeMetadata(['', ...segments.slice(1)], aliasMap, pipeWarn);
|
|
488
495
|
}
|
|
489
496
|
|
|
490
497
|
// Extract color from group name if present
|
|
@@ -522,7 +529,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
522
529
|
const labelRaw = timelineDurMatch[5];
|
|
523
530
|
|
|
524
531
|
const task = makeTask(labelRaw, { amount, unit }, uncertain, lineNumber, startDate);
|
|
525
|
-
if (result.error) return result;
|
|
526
532
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
527
533
|
currentContainer().push(taskNode);
|
|
528
534
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
@@ -540,7 +546,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
540
546
|
const labelRaw = durMatch[4];
|
|
541
547
|
|
|
542
548
|
const task = makeTask(labelRaw, { amount, unit }, uncertain, lineNumber);
|
|
543
|
-
if (result.error) return result;
|
|
544
549
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
545
550
|
currentContainer().push(taskNode);
|
|
546
551
|
lastTaskNode = taskNode as GanttNode & { kind: 'task' };
|
|
@@ -559,7 +564,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
559
564
|
lineNumber,
|
|
560
565
|
explicitDateMatch[1],
|
|
561
566
|
);
|
|
562
|
-
if (result.error) return result;
|
|
563
567
|
// Explicit date tasks with no duration are milestones
|
|
564
568
|
const taskNode: GanttNode = { kind: 'task', ...task };
|
|
565
569
|
currentContainer().push(taskNode);
|
|
@@ -574,7 +578,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
574
578
|
if (depMatch) {
|
|
575
579
|
// Dependency without a task context is an error
|
|
576
580
|
if (!lastTaskNode) {
|
|
577
|
-
|
|
581
|
+
softError(lineNumber, `Dependency "-> ${depMatch[1]}" must be indented under a task.`);
|
|
582
|
+
continue;
|
|
578
583
|
}
|
|
579
584
|
// This happens when the dep is at the same indent as the task
|
|
580
585
|
const depParts = depMatch[1].split('|');
|
|
@@ -582,7 +587,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
582
587
|
let offset: Offset | undefined;
|
|
583
588
|
|
|
584
589
|
if (depParts.length > 1) {
|
|
585
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap);
|
|
590
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING));
|
|
586
591
|
if (meta.lag || meta.lead) {
|
|
587
592
|
const key = meta.lag ? 'lag' : 'lead';
|
|
588
593
|
warn(lineNumber, `"${key}" is deprecated — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
@@ -606,7 +611,8 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
606
611
|
|
|
607
612
|
// ── Bare label = parse error ──────────────────────────
|
|
608
613
|
|
|
609
|
-
|
|
614
|
+
softError(lineNumber, `Expected duration (e.g., "10d: Task"), group brackets (e.g., "[Group]"), or keyword. Got: "${line}"`);
|
|
615
|
+
continue;
|
|
610
616
|
}
|
|
611
617
|
|
|
612
618
|
// ── Finalize ────────────────────────────────────────────
|
|
@@ -640,12 +646,12 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
640
646
|
|
|
641
647
|
// Check for reserved keyword
|
|
642
648
|
if (label.toLowerCase() === 'parallel') {
|
|
643
|
-
|
|
649
|
+
softError(ln, `"parallel" is a reserved keyword and cannot be used as a task name.`);
|
|
644
650
|
}
|
|
645
651
|
|
|
646
652
|
// Parse pipe metadata
|
|
647
653
|
const metadata = segments.length > 1
|
|
648
|
-
? parsePipeMetadata(segments, aliasMap)
|
|
654
|
+
? parsePipeMetadata(segments, aliasMap, () => warn(ln, MULTIPLE_PIPE_WARNING))
|
|
649
655
|
: {};
|
|
650
656
|
|
|
651
657
|
// Extract progress from metadata or shorthand
|
|
@@ -654,9 +660,9 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
654
660
|
progress = parseFloat(metadata.progress);
|
|
655
661
|
delete metadata.progress;
|
|
656
662
|
}
|
|
657
|
-
// Check for progress shorthand: `| 80%`
|
|
658
|
-
for (
|
|
659
|
-
const seg =
|
|
663
|
+
// Check for progress shorthand: `| 80%` or `| t:X, 80%`
|
|
664
|
+
for (const part of segments.slice(1).join(',').split(',')) {
|
|
665
|
+
const seg = part.trim();
|
|
660
666
|
const progressMatch = seg.match(/^(\d+)%$/);
|
|
661
667
|
if (progressMatch) {
|
|
662
668
|
progress = parseInt(progressMatch[1], 10);
|
|
@@ -666,7 +672,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
666
672
|
// Reject lag/lead — use offset instead
|
|
667
673
|
if (metadata.lag || metadata.lead) {
|
|
668
674
|
const key = metadata.lag ? 'lag' : 'lead';
|
|
669
|
-
|
|
675
|
+
softError(ln, `Unknown keyword "${key}". Use "offset: ${metadata[key]}" instead.`);
|
|
670
676
|
}
|
|
671
677
|
|
|
672
678
|
// Extract task-level offset from metadata
|