@diagrammo/dgmo 0.8.1 → 0.8.3
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 +189 -194
- package/dist/index.cjs +450 -596
- 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 +450 -596
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +821 -1060
- package/package.json +1 -1
- package/src/c4/parser.ts +19 -13
- package/src/chart.ts +69 -47
- package/src/class/parser.ts +46 -19
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +11 -16
- package/src/completion.ts +29 -25
- package/src/d3.ts +173 -174
- package/src/dgmo-router.ts +1 -1
- package/src/echarts.ts +42 -22
- package/src/er/parser.ts +9 -17
- package/src/gantt/parser.ts +108 -40
- package/src/graph/flowchart-parser.ts +7 -55
- package/src/graph/state-parser.ts +7 -10
- package/src/infra/parser.ts +6 -126
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +7 -13
- package/src/kanban/parser.ts +4 -7
- package/src/org/parser.ts +5 -8
- package/src/org/resolver.ts +3 -3
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +22 -45
- package/src/sitemap/parser.ts +10 -17
- package/src/utils/parsing.ts +9 -43
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/echarts.ts
CHANGED
|
@@ -82,6 +82,7 @@ export interface ParsedExtendedChart {
|
|
|
82
82
|
ylabelLineNumber?: number;
|
|
83
83
|
sizelabel?: string;
|
|
84
84
|
showLabels?: boolean;
|
|
85
|
+
shade?: boolean;
|
|
85
86
|
categoryColors?: Record<string, string>;
|
|
86
87
|
categoryLineNumbers?: Record<string, number>;
|
|
87
88
|
nodeColors?: Record<string, string>;
|
|
@@ -126,8 +127,8 @@ const VALID_EXTENDED_TYPES = new Set<ExtendedChartType>([
|
|
|
126
127
|
|
|
127
128
|
/** Known option keywords for the extended chart parser. */
|
|
128
129
|
const KNOWN_EXTENDED_OPTIONS = new Set([
|
|
129
|
-
'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel',
|
|
130
|
-
'columns', 'rows', 'x',
|
|
130
|
+
'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel',
|
|
131
|
+
'no-labels', 'columns', 'rows', 'x',
|
|
131
132
|
]);
|
|
132
133
|
|
|
133
134
|
/**
|
|
@@ -140,7 +141,7 @@ function parseScatterRow(
|
|
|
140
141
|
currentCategory: string,
|
|
141
142
|
lineNumber: number,
|
|
142
143
|
): ParsedScatterPoint | null {
|
|
143
|
-
const dataRow = parseDataRowValues(line);
|
|
144
|
+
const dataRow = parseDataRowValues(line, { multiValue: true });
|
|
144
145
|
if (!dataRow || dataRow.values.length < 2) return null;
|
|
145
146
|
const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
|
|
146
147
|
return {
|
|
@@ -376,14 +377,11 @@ export function parseExtendedChart(
|
|
|
376
377
|
if (firstToken === 'ylabel') { result.ylabel = value; result.ylabelLineNumber = lineNumber; continue; }
|
|
377
378
|
if (firstToken === 'sizelabel') { result.sizelabel = value; continue; }
|
|
378
379
|
|
|
379
|
-
if (firstToken === 'labels') {
|
|
380
|
-
result.showLabels = value.toLowerCase() === 'on' || value.toLowerCase() === 'true';
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
380
|
if (firstToken === 'columns') {
|
|
385
381
|
if (value) {
|
|
386
|
-
result.columns = value.
|
|
382
|
+
result.columns = value.includes(',')
|
|
383
|
+
? value.split(',').map((s) => s.trim())
|
|
384
|
+
: value.split(/\s+/);
|
|
387
385
|
} else {
|
|
388
386
|
const collected = collectIndentedValues(lines, i);
|
|
389
387
|
i = collected.newIndex;
|
|
@@ -394,7 +392,9 @@ export function parseExtendedChart(
|
|
|
394
392
|
|
|
395
393
|
if (firstToken === 'rows') {
|
|
396
394
|
if (value) {
|
|
397
|
-
result.rows = value.
|
|
395
|
+
result.rows = value.includes(',')
|
|
396
|
+
? value.split(',').map((s) => s.trim())
|
|
397
|
+
: value.split(/\s+/);
|
|
398
398
|
} else {
|
|
399
399
|
const collected = collectIndentedValues(lines, i);
|
|
400
400
|
i = collected.newIndex;
|
|
@@ -415,6 +415,10 @@ export function parseExtendedChart(
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
// Bare boolean options
|
|
419
|
+
if (firstToken === 'no-labels') { result.showLabels = false; continue; }
|
|
420
|
+
if (firstToken === 'shade') { result.shade = true; continue; }
|
|
421
|
+
|
|
418
422
|
// Bare keyword options (no value)
|
|
419
423
|
if (firstToken === 'series' && spaceIdx === -1) {
|
|
420
424
|
const parsed = parseSeriesNames('', lines, i, palette);
|
|
@@ -472,9 +476,9 @@ export function parseExtendedChart(
|
|
|
472
476
|
}
|
|
473
477
|
}
|
|
474
478
|
|
|
475
|
-
// Heatmap data row: "RowLabel val1, val2, val3, ..."
|
|
479
|
+
// Heatmap data row: "RowLabel val1, val2, val3, ..." or "RowLabel val1 val2 val3"
|
|
476
480
|
if (result.type === 'heatmap') {
|
|
477
|
-
const dataRow = parseDataRowValues(trimmed);
|
|
481
|
+
const dataRow = parseDataRowValues(trimmed, { multiValue: true });
|
|
478
482
|
if (dataRow && dataRow.values.length > 0) {
|
|
479
483
|
if (!result.heatmapRows) result.heatmapRows = [];
|
|
480
484
|
result.heatmapRows.push({ label: dataRow.label, values: dataRow.values, lineNumber });
|
|
@@ -918,6 +922,12 @@ function buildFunctionOption(
|
|
|
918
922
|
itemStyle: {
|
|
919
923
|
color: fnColor,
|
|
920
924
|
},
|
|
925
|
+
...(parsed.shade && {
|
|
926
|
+
areaStyle: {
|
|
927
|
+
color: fnColor,
|
|
928
|
+
opacity: 0.15,
|
|
929
|
+
},
|
|
930
|
+
}),
|
|
921
931
|
emphasis: EMPHASIS_SELF,
|
|
922
932
|
};
|
|
923
933
|
});
|
|
@@ -1287,7 +1297,7 @@ function buildScatterOption(
|
|
|
1287
1297
|
const hasCategories = points.some((p) => p.category !== undefined);
|
|
1288
1298
|
const hasSize = points.some((p) => p.size !== undefined);
|
|
1289
1299
|
|
|
1290
|
-
const showLabels = parsed.showLabels ??
|
|
1300
|
+
const showLabels = parsed.showLabels ?? true;
|
|
1291
1301
|
const labelFontSize = 11;
|
|
1292
1302
|
|
|
1293
1303
|
// When showLabels is on, we render labels ourselves via graphic — disable ECharts labels
|
|
@@ -2165,13 +2175,23 @@ function buildAreaOption(
|
|
|
2165
2175
|
|
|
2166
2176
|
// ── Segment label formatter ──────────────────────────────────
|
|
2167
2177
|
|
|
2168
|
-
function segmentLabelFormatter(
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
}
|
|
2178
|
+
function segmentLabelFormatter(parsed: ParsedChart): string {
|
|
2179
|
+
const showName = !parsed.noLabelName;
|
|
2180
|
+
const showValue = !parsed.noLabelValue;
|
|
2181
|
+
const showPercent = !parsed.noLabelPercent;
|
|
2182
|
+
|
|
2183
|
+
const parts: string[] = [];
|
|
2184
|
+
if (showName) parts.push('{b}');
|
|
2185
|
+
if (showValue) parts.push('{c}');
|
|
2186
|
+
if (showPercent) parts.push('{d}%');
|
|
2187
|
+
|
|
2188
|
+
if (parts.length === 0) return '{b}'; // fallback: always show name
|
|
2189
|
+
if (parts.length === 1) return parts[0];
|
|
2190
|
+
|
|
2191
|
+
// Name is joined with " — ", value+percent are grouped with parens when all three
|
|
2192
|
+
if (showName && showValue && showPercent) return '{b} — {c} ({d}%)';
|
|
2193
|
+
if (showName) return '{b} — ' + parts.slice(1).join(' ');
|
|
2194
|
+
return parts.join(' ');
|
|
2175
2195
|
}
|
|
2176
2196
|
|
|
2177
2197
|
// ── Pie / Doughnut ───────────────────────────────────────────
|
|
@@ -2210,7 +2230,7 @@ function buildPieOption(
|
|
|
2210
2230
|
data,
|
|
2211
2231
|
label: {
|
|
2212
2232
|
position: 'outside',
|
|
2213
|
-
formatter: segmentLabelFormatter(parsed
|
|
2233
|
+
formatter: segmentLabelFormatter(parsed),
|
|
2214
2234
|
color: textColor,
|
|
2215
2235
|
fontFamily: FONT_FAMILY,
|
|
2216
2236
|
},
|
|
@@ -2329,7 +2349,7 @@ function buildPolarAreaOption(
|
|
|
2329
2349
|
data,
|
|
2330
2350
|
label: {
|
|
2331
2351
|
position: 'outside',
|
|
2332
|
-
formatter: segmentLabelFormatter(parsed
|
|
2352
|
+
formatter: segmentLabelFormatter(parsed),
|
|
2333
2353
|
color: textColor,
|
|
2334
2354
|
fontFamily: FONT_FAMILY,
|
|
2335
2355
|
},
|
package/src/er/parser.ts
CHANGED
|
@@ -243,15 +243,10 @@ export function parseERDiagram(
|
|
|
243
243
|
firstLineParsed = true;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
// Tag group heading — `tag Name`
|
|
246
|
+
// Tag group heading — `tag Name`
|
|
247
247
|
if (!contentStarted && indent === 0) {
|
|
248
248
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
249
249
|
if (tagBlockMatch) {
|
|
250
|
-
if (tagBlockMatch.deprecated) {
|
|
251
|
-
result.diagnostics.push(makeDgmoError(lineNumber,
|
|
252
|
-
`'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
250
|
currentTagGroup = {
|
|
256
251
|
name: tagBlockMatch.name,
|
|
257
252
|
alias: tagBlockMatch.alias,
|
|
@@ -337,19 +332,16 @@ export function parseERDiagram(
|
|
|
337
332
|
currentTable = null;
|
|
338
333
|
contentStarted = true;
|
|
339
334
|
|
|
340
|
-
//
|
|
335
|
+
// Reject top-level relationships — must be indented under source table
|
|
341
336
|
const rel = parseRelationship(trimmed, lineNumber, pushError);
|
|
342
337
|
if (rel) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
...(rel.label && { label: rel.label }),
|
|
351
|
-
lineNumber,
|
|
352
|
-
});
|
|
338
|
+
result.diagnostics.push(
|
|
339
|
+
makeDgmoError(
|
|
340
|
+
lineNumber,
|
|
341
|
+
`Relationship "${rel.source} → ${rel.target}" must be indented under the source table "${rel.source}"`,
|
|
342
|
+
'warning',
|
|
343
|
+
),
|
|
344
|
+
);
|
|
353
345
|
continue;
|
|
354
346
|
}
|
|
355
347
|
|
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,
|
|
9
|
+
import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_ERROR, parseFirstLine, prescanOptions } from '../utils/parsing';
|
|
10
10
|
import { parseOffset } from '../utils/duration';
|
|
11
11
|
import type { PaletteColors } from '../palettes';
|
|
12
12
|
import { resolveColor } from '../colors';
|
|
@@ -37,7 +37,7 @@ 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 = /^(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)\s
|
|
40
|
+
const TIMELINE_DURATION_RE = /^(\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
41
|
|
|
42
42
|
/** Group container: `[GroupName]` with optional pipe metadata */
|
|
43
43
|
const GROUP_RE = /^\[(.+?)\]\s*(.*)$/;
|
|
@@ -49,7 +49,7 @@ const DEPENDENCY_RE = /^(?:-(.+?))?->\s*(.+)$/;
|
|
|
49
49
|
const COMMENT_RE = /^\/\//;
|
|
50
50
|
|
|
51
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
|
|
52
|
+
const ERA_RE = /^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
53
|
|
|
54
54
|
/** Marker: `marker YYYY[-MM[-DD[ HH:MM]]] Label (color?)` */
|
|
55
55
|
const MARKER_RE = /^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/i;
|
|
@@ -58,11 +58,17 @@ const MARKER_RE = /^marker\s+(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.
|
|
|
58
58
|
const HOLIDAY_DATE_RE = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/;
|
|
59
59
|
|
|
60
60
|
/** Holiday range: `2024-12-24 -> 2024-12-31 Label` */
|
|
61
|
-
const HOLIDAY_RANGE_RE = /^(\d{4}-\d{2}-\d{2})\s
|
|
61
|
+
const HOLIDAY_RANGE_RE = /^(\d{4}-\d{2}-\d{2})\s*(?:->|\u2013>)\s*(\d{4}-\d{2}-\d{2})\s+(.+)$/;
|
|
62
62
|
|
|
63
63
|
/** Workweek override: `workweek sun-thu` */
|
|
64
64
|
const WORKWEEK_RE = /^workweek\s+(.+)$/i;
|
|
65
65
|
|
|
66
|
+
/** Era entry (inside era block, no `era` prefix): `YYYY[-MM[-DD[ HH:MM]]] -> YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
67
|
+
const ERA_ENTRY_RE = /^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
|
|
68
|
+
|
|
69
|
+
/** Marker entry (inside marker block, no `marker` prefix): `YYYY[-MM[-DD[ HH:MM]]] Label` */
|
|
70
|
+
const MARKER_ENTRY_RE = /^(\d{4}(?:-\d{2}(?:-\d{2}(?: \d{2}:\d{2})?)?)?)\s+(.+)$/;
|
|
71
|
+
|
|
66
72
|
// Valid weekday names
|
|
67
73
|
const WEEKDAY_MAP: Record<string, Weekday> = {
|
|
68
74
|
mon: 'mon', tue: 'tue', wed: 'wed', thu: 'thu', fri: 'fri', sat: 'sat', sun: 'sun',
|
|
@@ -156,6 +162,10 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
156
162
|
let inTagBlock = false;
|
|
157
163
|
let currentTagGroup: TagGroup | null = null;
|
|
158
164
|
let tagBlockIndent = 0;
|
|
165
|
+
let inEraBlock = false;
|
|
166
|
+
let eraBlockIndent = 0;
|
|
167
|
+
let inMarkerBlock = false;
|
|
168
|
+
let markerBlockIndent = 0;
|
|
159
169
|
let lastTaskNode: (GanttNode & { kind: 'task' }) | null = null;
|
|
160
170
|
let taskIdCounter = 0;
|
|
161
171
|
const seriesColors = palette ? getSeriesColors(palette) : [];
|
|
@@ -170,10 +180,16 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
170
180
|
|
|
171
181
|
// Skip empty lines
|
|
172
182
|
if (!line) {
|
|
173
|
-
// Empty line ends holidays/tag blocks only if at root indent
|
|
183
|
+
// Empty line ends holidays/tag/era/marker blocks only if at root indent
|
|
174
184
|
if (inHolidaysBlock && indent <= holidaysBlockIndent) {
|
|
175
185
|
inHolidaysBlock = false;
|
|
176
186
|
}
|
|
187
|
+
if (inEraBlock && indent <= eraBlockIndent) {
|
|
188
|
+
inEraBlock = false;
|
|
189
|
+
}
|
|
190
|
+
if (inMarkerBlock && indent <= markerBlockIndent) {
|
|
191
|
+
inMarkerBlock = false;
|
|
192
|
+
}
|
|
177
193
|
if (inTagBlock && indent <= tagBlockIndent) {
|
|
178
194
|
inTagBlock = false;
|
|
179
195
|
if (currentTagGroup) {
|
|
@@ -249,6 +265,57 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
267
|
|
|
268
|
+
// ── Era block entries ─────────────────────────────────
|
|
269
|
+
|
|
270
|
+
if (inEraBlock) {
|
|
271
|
+
if (indent <= eraBlockIndent) {
|
|
272
|
+
inEraBlock = false;
|
|
273
|
+
// fall through to process this line normally
|
|
274
|
+
} else {
|
|
275
|
+
if (COMMENT_RE.test(line)) continue;
|
|
276
|
+
const eraEntryMatch = line.match(ERA_ENTRY_RE);
|
|
277
|
+
if (eraEntryMatch) {
|
|
278
|
+
const eraLabelRaw = eraEntryMatch[3].trim();
|
|
279
|
+
const eraExtracted = extractColor(eraLabelRaw, palette);
|
|
280
|
+
result.eras.push({
|
|
281
|
+
startDate: eraEntryMatch[1],
|
|
282
|
+
endDate: eraEntryMatch[2],
|
|
283
|
+
label: eraExtracted.label,
|
|
284
|
+
color: eraExtracted.color || null,
|
|
285
|
+
lineNumber,
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
warn(lineNumber, `Unrecognized era entry: "${line}"`);
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Marker block entries ─────────────────────────────
|
|
295
|
+
|
|
296
|
+
if (inMarkerBlock) {
|
|
297
|
+
if (indent <= markerBlockIndent) {
|
|
298
|
+
inMarkerBlock = false;
|
|
299
|
+
// fall through to process this line normally
|
|
300
|
+
} else {
|
|
301
|
+
if (COMMENT_RE.test(line)) continue;
|
|
302
|
+
const markerEntryMatch = line.match(MARKER_ENTRY_RE);
|
|
303
|
+
if (markerEntryMatch) {
|
|
304
|
+
const markerLabelRaw = markerEntryMatch[2].trim();
|
|
305
|
+
const markerExtracted = extractColor(markerLabelRaw, palette);
|
|
306
|
+
result.markers.push({
|
|
307
|
+
date: markerEntryMatch[1],
|
|
308
|
+
label: markerExtracted.label,
|
|
309
|
+
color: markerExtracted.color || null,
|
|
310
|
+
lineNumber,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
warn(lineNumber, `Unrecognized marker entry: "${line}"`);
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
252
319
|
// ── Tag block entries ─────────────────────────────────
|
|
253
320
|
|
|
254
321
|
if (inTagBlock && currentTagGroup) {
|
|
@@ -302,7 +369,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
302
369
|
let offset: Offset | undefined;
|
|
303
370
|
|
|
304
371
|
if (depParts.length > 1) {
|
|
305
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber,
|
|
372
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
306
373
|
if (meta.lag || meta.lead) {
|
|
307
374
|
const key = meta.lag ? 'lag' : 'lead';
|
|
308
375
|
softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
@@ -369,10 +436,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
369
436
|
// Tag block heading
|
|
370
437
|
const tagMatch = matchTagBlockHeading(line);
|
|
371
438
|
if (tagMatch) {
|
|
372
|
-
if (tagMatch.deprecated) {
|
|
373
|
-
softError(lineNumber, `'## ${tagMatch.name}' is no longer supported — use 'tag ${tagMatch.name}' instead`);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
439
|
inTagBlock = true;
|
|
377
440
|
tagBlockIndent = indent;
|
|
378
441
|
inHeaderBlock = false;
|
|
@@ -388,7 +451,28 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
388
451
|
continue;
|
|
389
452
|
}
|
|
390
453
|
|
|
391
|
-
//
|
|
454
|
+
// Top-level workweek (outside holiday block)
|
|
455
|
+
const topWorkweekMatch = line.match(WORKWEEK_RE);
|
|
456
|
+
if (topWorkweekMatch) {
|
|
457
|
+
const days = parseWorkweek(topWorkweekMatch[1].trim());
|
|
458
|
+
if (days) {
|
|
459
|
+
result.holidays.workweek = days;
|
|
460
|
+
} else {
|
|
461
|
+
warn(lineNumber, `Invalid workweek format: "${topWorkweekMatch[1]}". Use day range like "sun-thu" or comma-separated days.`);
|
|
462
|
+
}
|
|
463
|
+
inHeaderBlock = false;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Era block: bare `era` keyword starts a block
|
|
468
|
+
if (line.toLowerCase() === 'era') {
|
|
469
|
+
inEraBlock = true;
|
|
470
|
+
eraBlockIndent = indent;
|
|
471
|
+
inHeaderBlock = false;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Era (inline)
|
|
392
476
|
const eraMatch = line.match(ERA_RE);
|
|
393
477
|
if (eraMatch) {
|
|
394
478
|
const eraLabelRaw = eraMatch[3].trim();
|
|
@@ -404,7 +488,15 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
404
488
|
continue;
|
|
405
489
|
}
|
|
406
490
|
|
|
407
|
-
// Marker
|
|
491
|
+
// Marker block: bare `marker` keyword starts a block
|
|
492
|
+
if (line.toLowerCase() === 'marker') {
|
|
493
|
+
inMarkerBlock = true;
|
|
494
|
+
markerBlockIndent = indent;
|
|
495
|
+
inHeaderBlock = false;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Marker (inline)
|
|
408
500
|
const markerMatch = line.match(MARKER_RE);
|
|
409
501
|
if (markerMatch) {
|
|
410
502
|
const markerLabelRaw = markerMatch[2].trim();
|
|
@@ -472,9 +564,6 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
472
564
|
result.options.title = value;
|
|
473
565
|
result.options.titleLineNumber = lineNumber;
|
|
474
566
|
break;
|
|
475
|
-
case 'orientation':
|
|
476
|
-
warn(lineNumber, `'orientation' is not supported for gantt charts`);
|
|
477
|
-
break;
|
|
478
567
|
case 'today-marker':
|
|
479
568
|
if (/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?$/.test(value)) {
|
|
480
569
|
result.options.todayMarker = value;
|
|
@@ -506,28 +595,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
506
595
|
|
|
507
596
|
inHeaderBlock = false;
|
|
508
597
|
|
|
509
|
-
// ── `# Group` alternate syntax ──────────────────────────
|
|
510
598
|
|
|
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
599
|
|
|
532
600
|
// ── Parallel block ────────────────────────────────────
|
|
533
601
|
|
|
@@ -560,7 +628,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
560
628
|
let metadata: Record<string, string> = {};
|
|
561
629
|
let color: string | null = null;
|
|
562
630
|
|
|
563
|
-
const pipeWarn = () => warn(lineNumber,
|
|
631
|
+
const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_ERROR);
|
|
564
632
|
if (segments.length > 0 && segments[0].trim()) {
|
|
565
633
|
// Check if first segment after brackets is pipe metadata
|
|
566
634
|
metadata = parsePipeMetadata(['', ...segments], aliasMap, pipeWarn);
|
|
@@ -662,7 +730,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
662
730
|
let offset: Offset | undefined;
|
|
663
731
|
|
|
664
732
|
if (depParts.length > 1) {
|
|
665
|
-
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber,
|
|
733
|
+
const meta = parsePipeMetadata(['', ...depParts.slice(1)], aliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
666
734
|
if (meta.lag || meta.lead) {
|
|
667
735
|
const key = meta.lag ? 'lag' : 'lead';
|
|
668
736
|
softError(lineNumber, `"${key}" is no longer supported — use "offset: ${meta[key]}" instead.${key === 'lead' ? ' Negate the value for lead behavior: "offset: -...".' : ''}`);
|
|
@@ -726,7 +794,7 @@ export function parseGantt(content: string, palette?: PaletteColors): ParsedGant
|
|
|
726
794
|
|
|
727
795
|
// Parse pipe metadata
|
|
728
796
|
const metadata = segments.length > 1
|
|
729
|
-
? parsePipeMetadata(segments, aliasMap, () => warn(ln,
|
|
797
|
+
? parsePipeMetadata(segments, aliasMap, () => warn(ln, MULTIPLE_PIPE_ERROR))
|
|
730
798
|
: {};
|
|
731
799
|
|
|
732
800
|
// Extract progress from metadata or shorthand
|
|
@@ -833,7 +901,7 @@ function parseWorkweek(s: string): Weekday[] | null {
|
|
|
833
901
|
// ── Known option keys ─────────────────────────────────────
|
|
834
902
|
|
|
835
903
|
const KNOWN_OPTIONS = new Set([
|
|
836
|
-
'start', 'title', '
|
|
904
|
+
'start', 'title', 'today-marker',
|
|
837
905
|
'critical-path', 'dependencies', 'chart', 'sort',
|
|
838
906
|
]);
|
|
839
907
|
|
|
@@ -4,19 +4,15 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
|
4
4
|
import {
|
|
5
5
|
measureIndent,
|
|
6
6
|
extractColor,
|
|
7
|
-
normalizeDirection,
|
|
8
7
|
inferArrowColor,
|
|
9
8
|
parseFirstLine,
|
|
10
9
|
OPTION_NOCOLON_RE,
|
|
11
|
-
GROUP_HASH_RE,
|
|
12
|
-
DOUBLE_HASH_RE,
|
|
13
10
|
ALL_CHART_TYPES,
|
|
14
11
|
} from '../utils/parsing';
|
|
15
12
|
import type {
|
|
16
13
|
ParsedGraph,
|
|
17
14
|
GraphNode,
|
|
18
15
|
GraphEdge,
|
|
19
|
-
GraphGroup,
|
|
20
16
|
GraphShape,
|
|
21
17
|
GraphDirection,
|
|
22
18
|
} from './types';
|
|
@@ -218,7 +214,7 @@ export function parseFlowchart(
|
|
|
218
214
|
const lines = content.split('\n');
|
|
219
215
|
const result: ParsedGraph = {
|
|
220
216
|
type: 'flowchart',
|
|
221
|
-
direction: '
|
|
217
|
+
direction: 'LR',
|
|
222
218
|
nodes: [],
|
|
223
219
|
edges: [],
|
|
224
220
|
options: {},
|
|
@@ -238,11 +234,6 @@ export function parseFlowchart(
|
|
|
238
234
|
let contentStarted = false;
|
|
239
235
|
let firstLineParsed = false;
|
|
240
236
|
|
|
241
|
-
// Group support
|
|
242
|
-
let currentGroup: GraphGroup | null = null;
|
|
243
|
-
let groupIndent = -1;
|
|
244
|
-
const groups: GraphGroup[] = [];
|
|
245
|
-
|
|
246
237
|
function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
|
|
247
238
|
const existing = nodeMap.get(ref.id);
|
|
248
239
|
if (existing) return existing;
|
|
@@ -253,15 +244,10 @@ export function parseFlowchart(
|
|
|
253
244
|
shape: ref.shape,
|
|
254
245
|
lineNumber,
|
|
255
246
|
...(ref.color && { color: ref.color }),
|
|
256
|
-
...(currentGroup && { group: currentGroup.id }),
|
|
257
247
|
};
|
|
258
248
|
nodeMap.set(ref.id, node);
|
|
259
249
|
result.nodes.push(node);
|
|
260
250
|
|
|
261
|
-
if (currentGroup && !currentGroup.nodeIds.includes(ref.id)) {
|
|
262
|
-
currentGroup.nodeIds.push(ref.id);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
251
|
return node;
|
|
266
252
|
}
|
|
267
253
|
|
|
@@ -418,45 +404,19 @@ export function parseFlowchart(
|
|
|
418
404
|
}
|
|
419
405
|
}
|
|
420
406
|
|
|
421
|
-
// ## group headings — emit helpful error
|
|
422
|
-
if (DOUBLE_HASH_RE.test(trimmed)) {
|
|
423
|
-
result.diagnostics.push(
|
|
424
|
-
makeDgmoError(lineNumber, 'Use `#` for groups \u2014 nesting is done with indentation.', 'error')
|
|
425
|
-
);
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// # GroupName — alternate group notation
|
|
430
|
-
const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
|
|
431
|
-
if (hashGroupMatch) {
|
|
432
|
-
const { label, color } = extractColor(hashGroupMatch[1].trim(), palette);
|
|
433
|
-
currentGroup = {
|
|
434
|
-
id: `group:${label.toLowerCase()}`,
|
|
435
|
-
label,
|
|
436
|
-
nodeIds: [],
|
|
437
|
-
lineNumber,
|
|
438
|
-
...(color && { color }),
|
|
439
|
-
};
|
|
440
|
-
groupIndent = indent;
|
|
441
|
-
groups.push(currentGroup);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
407
|
// Options (space-separated, before content)
|
|
446
408
|
if (!contentStarted) {
|
|
409
|
+
// Bare boolean: direction-tb
|
|
410
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
411
|
+
result.direction = 'TB';
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
447
415
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
448
416
|
if (optMatch && !trimmed.includes('->')) {
|
|
449
417
|
const key = optMatch[1].toLowerCase();
|
|
450
418
|
const value = optMatch[2].trim();
|
|
451
419
|
|
|
452
|
-
if (key === 'direction' || key === 'orientation') {
|
|
453
|
-
const dir = normalizeDirection(value);
|
|
454
|
-
if (dir) {
|
|
455
|
-
result.direction = dir;
|
|
456
|
-
}
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
420
|
// Boolean: no-color = color off
|
|
461
421
|
if (key === 'no-color') {
|
|
462
422
|
result.options['color'] = 'off';
|
|
@@ -469,18 +429,10 @@ export function parseFlowchart(
|
|
|
469
429
|
}
|
|
470
430
|
}
|
|
471
431
|
|
|
472
|
-
// Close current group when indent returns to or below the group level
|
|
473
|
-
if (currentGroup && indent <= groupIndent) {
|
|
474
|
-
currentGroup = null;
|
|
475
|
-
groupIndent = -1;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
432
|
// Content line (nodes and edges)
|
|
479
433
|
processContentLine(trimmed, lineNumber, indent);
|
|
480
434
|
}
|
|
481
435
|
|
|
482
|
-
if (groups.length > 0) result.groups = groups;
|
|
483
|
-
|
|
484
436
|
// Validation: no nodes found
|
|
485
437
|
if (result.nodes.length === 0 && !result.error) {
|
|
486
438
|
const diag = makeDgmoError(1, 'No nodes found. Add flowchart content with shape syntax like [Process] or (Start).');
|
|
@@ -4,7 +4,6 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
|
4
4
|
import {
|
|
5
5
|
measureIndent,
|
|
6
6
|
extractColor,
|
|
7
|
-
normalizeDirection,
|
|
8
7
|
parseFirstLine,
|
|
9
8
|
OPTION_NOCOLON_RE,
|
|
10
9
|
ALL_CHART_TYPES,
|
|
@@ -154,7 +153,7 @@ export function parseState(
|
|
|
154
153
|
const lines = content.split('\n');
|
|
155
154
|
const result: ParsedGraph = {
|
|
156
155
|
type: 'state',
|
|
157
|
-
direction: '
|
|
156
|
+
direction: 'LR',
|
|
158
157
|
nodes: [],
|
|
159
158
|
edges: [],
|
|
160
159
|
options: {},
|
|
@@ -268,19 +267,17 @@ export function parseState(
|
|
|
268
267
|
|
|
269
268
|
// Options (space-separated, before content)
|
|
270
269
|
if (!contentStarted) {
|
|
270
|
+
// Bare boolean: direction-tb
|
|
271
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
272
|
+
result.direction = 'TB';
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
272
277
|
if (optMatch && !trimmed.includes('->')) {
|
|
273
278
|
const key = optMatch[1].toLowerCase();
|
|
274
279
|
const value = optMatch[2].trim();
|
|
275
280
|
|
|
276
|
-
if (key === 'direction' || key === 'orientation') {
|
|
277
|
-
const dir = normalizeDirection(value);
|
|
278
|
-
if (dir) {
|
|
279
|
-
result.direction = dir;
|
|
280
|
-
}
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
281
|
// Boolean: no-color = color off
|
|
285
282
|
if (key === 'no-color') {
|
|
286
283
|
result.options['color'] = 'off';
|