@diagrammo/dgmo 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +189 -194
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/infra/types.ts
CHANGED
|
@@ -62,7 +62,6 @@ export interface InfraNode {
|
|
|
62
62
|
groupId: string | null;
|
|
63
63
|
tags: Record<string, string>; // tagGroup -> tagValue
|
|
64
64
|
isEdge: boolean; // true for the `edge` entry-point component
|
|
65
|
-
nodeType?: string; // database, cache, queue, service, gateway, storage, function, network
|
|
66
65
|
description?: string;
|
|
67
66
|
lineNumber: number;
|
|
68
67
|
}
|
|
@@ -13,9 +13,17 @@ import type {
|
|
|
13
13
|
} from './types';
|
|
14
14
|
import { VALID_STATUSES, STATUS_ALIASES } from './types';
|
|
15
15
|
import { inferParticipantType } from '../sequence/participant-inference';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
matchTagBlockHeading,
|
|
18
|
+
injectDefaultTagMetadata,
|
|
19
|
+
validateTagValues,
|
|
20
|
+
} from '../utils/tag-groups';
|
|
17
21
|
import type { TagGroup } from '../utils/tag-groups';
|
|
18
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
extractColor,
|
|
24
|
+
parseFirstLine,
|
|
25
|
+
OPTION_NOCOLON_RE,
|
|
26
|
+
} from '../utils/parsing';
|
|
19
27
|
|
|
20
28
|
// ============================================================
|
|
21
29
|
// Heuristic — does this content look like an initiative-status diagram?
|
|
@@ -38,10 +46,14 @@ export function looksLikeInitiativeStatus(content: string): boolean {
|
|
|
38
46
|
// Skip new-style first line (bare chart type name)
|
|
39
47
|
if (parseFirstLine(trimmed)) continue;
|
|
40
48
|
if (trimmed.includes('->')) hasArrow = true;
|
|
41
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
/\|\s*(done|doing|wip|blocked|paused|waiting|todo|na)\s*$/i.test(trimmed)
|
|
51
|
+
)
|
|
52
|
+
hasStatus = true;
|
|
42
53
|
// Indented arrow is a strong signal — only initiative-status uses this
|
|
43
54
|
const isIndented = line.length > 0 && line !== trimmed && /^\s/.test(line);
|
|
44
|
-
if (isIndented && (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)))
|
|
55
|
+
if (isIndented && (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)))
|
|
56
|
+
hasIndentedArrow = true;
|
|
45
57
|
if (hasArrow && hasStatus) return true;
|
|
46
58
|
}
|
|
47
59
|
return hasIndentedArrow;
|
|
@@ -67,7 +79,11 @@ export function parseNodeMetadata(
|
|
|
67
79
|
aliasMap: Map<string, string>,
|
|
68
80
|
lineNum?: number,
|
|
69
81
|
diagnostics?: DgmoError[]
|
|
70
|
-
): {
|
|
82
|
+
): {
|
|
83
|
+
status: InitiativeStatus;
|
|
84
|
+
metadata: Record<string, string>;
|
|
85
|
+
hadStatusWord: boolean;
|
|
86
|
+
} {
|
|
71
87
|
const metadata: Record<string, string> = {};
|
|
72
88
|
let status: InitiativeStatus = null;
|
|
73
89
|
let hadStatusWord = false;
|
|
@@ -125,7 +141,12 @@ export function parseNodeMetadata(
|
|
|
125
141
|
// Parser
|
|
126
142
|
// ============================================================
|
|
127
143
|
|
|
128
|
-
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
145
|
+
function _parseStatus(
|
|
146
|
+
raw: string,
|
|
147
|
+
line: number,
|
|
148
|
+
diagnostics: DgmoError[]
|
|
149
|
+
): InitiativeStatus {
|
|
129
150
|
const trimmed = raw.trim().toLowerCase();
|
|
130
151
|
if (!trimmed) return 'na';
|
|
131
152
|
const canonical = STATUS_ALIASES[trimmed] ?? trimmed;
|
|
@@ -191,7 +212,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
191
212
|
const firstLineResult = parseFirstLine(trimmed);
|
|
192
213
|
if (firstLineResult && !contentStarted) {
|
|
193
214
|
if (firstLineResult.chartType !== 'initiative-status') {
|
|
194
|
-
const diag = makeDgmoError(
|
|
215
|
+
const diag = makeDgmoError(
|
|
216
|
+
lineNum,
|
|
217
|
+
`Expected chart type "initiative-status", got "${firstLineResult.chartType}"`
|
|
218
|
+
);
|
|
195
219
|
result.diagnostics.push(diag);
|
|
196
220
|
result.error = formatDgmoError(diag);
|
|
197
221
|
return result;
|
|
@@ -203,16 +227,19 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
203
227
|
continue;
|
|
204
228
|
}
|
|
205
229
|
|
|
206
|
-
// hide directive (
|
|
230
|
+
// hide directive (colon syntax): `hide phase:Planning, phase:Review`
|
|
207
231
|
const hideMatch = trimmed.match(/^hide\s+(.+)/i);
|
|
208
232
|
if (hideMatch && !trimmed.match(/^hide\s*\|/)) {
|
|
209
|
-
// Parse comma-separated tag
|
|
233
|
+
// Parse comma-separated tag:value pairs: `phase:Planning, phase:Review`
|
|
210
234
|
const pairs = hideMatch[1].split(',');
|
|
211
235
|
for (const pair of pairs) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
const groupKey =
|
|
215
|
-
const value =
|
|
236
|
+
const colonIdx = pair.indexOf(':');
|
|
237
|
+
if (colonIdx > 0) {
|
|
238
|
+
const groupKey = pair.substring(0, colonIdx).trim().toLowerCase();
|
|
239
|
+
const value = pair
|
|
240
|
+
.substring(colonIdx + 1)
|
|
241
|
+
.trim()
|
|
242
|
+
.toLowerCase();
|
|
216
243
|
if (groupKey && value) {
|
|
217
244
|
if (!result.initialHiddenTagValues.has(groupKey)) {
|
|
218
245
|
result.initialHiddenTagValues.set(groupKey, new Set());
|
|
@@ -231,7 +258,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
231
258
|
const key = optMatch[1].toLowerCase();
|
|
232
259
|
const value = optMatch[2].trim();
|
|
233
260
|
// Only recognize known option keys (not node content)
|
|
234
|
-
if (key === 'active-tag'
|
|
261
|
+
if (key === 'active-tag') {
|
|
235
262
|
result.options[key] = value;
|
|
236
263
|
continue;
|
|
237
264
|
}
|
|
@@ -243,13 +270,11 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
243
270
|
if (tagBlockMatch) {
|
|
244
271
|
if (contentStarted) {
|
|
245
272
|
result.diagnostics.push(
|
|
246
|
-
makeDgmoError(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
result.diagnostics.push(
|
|
252
|
-
makeDgmoError(lineNum, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag ${tagBlockMatch.name}' instead`)
|
|
273
|
+
makeDgmoError(
|
|
274
|
+
lineNum,
|
|
275
|
+
'Tag groups must appear before diagram content',
|
|
276
|
+
'error'
|
|
277
|
+
)
|
|
253
278
|
);
|
|
254
279
|
continue;
|
|
255
280
|
}
|
|
@@ -260,7 +285,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
260
285
|
lineNumber: lineNum,
|
|
261
286
|
};
|
|
262
287
|
if (tagBlockMatch.alias) {
|
|
263
|
-
aliasMap.set(
|
|
288
|
+
aliasMap.set(
|
|
289
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
290
|
+
tagBlockMatch.name.toLowerCase()
|
|
291
|
+
);
|
|
264
292
|
}
|
|
265
293
|
// Handle inline values from single-line tag declaration
|
|
266
294
|
if (tagBlockMatch.inlineValues) {
|
|
@@ -298,7 +326,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
298
326
|
continue;
|
|
299
327
|
}
|
|
300
328
|
// Non-indented line after tag group — close and fall through
|
|
301
|
-
currentTagGroup = null;
|
|
329
|
+
currentTagGroup = null; // eslint-disable-line no-useless-assignment
|
|
302
330
|
}
|
|
303
331
|
|
|
304
332
|
// Group header: [Group Name] or [Group Name] | metadata
|
|
@@ -349,13 +377,22 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
349
377
|
if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
|
|
350
378
|
if (!lastNodeLabel) {
|
|
351
379
|
result.diagnostics.push(
|
|
352
|
-
makeDgmoError(
|
|
380
|
+
makeDgmoError(
|
|
381
|
+
lineNum,
|
|
382
|
+
'Indented edge has no preceding node to use as source',
|
|
383
|
+
'warning'
|
|
384
|
+
)
|
|
353
385
|
);
|
|
354
386
|
continue;
|
|
355
387
|
}
|
|
356
388
|
edgeText = `${lastNodeLabel} ${trimmed}`;
|
|
357
389
|
}
|
|
358
|
-
const edge = parseEdgeLine(
|
|
390
|
+
const edge = parseEdgeLine(
|
|
391
|
+
edgeText,
|
|
392
|
+
lineNum,
|
|
393
|
+
aliasMap,
|
|
394
|
+
result.diagnostics
|
|
395
|
+
);
|
|
359
396
|
if (edge) result.edges.push(edge);
|
|
360
397
|
continue;
|
|
361
398
|
}
|
|
@@ -364,28 +401,32 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
364
401
|
contentStarted = true;
|
|
365
402
|
currentTagGroup = null;
|
|
366
403
|
const node = parseNodeLine(trimmed, lineNum, aliasMap, result.diagnostics);
|
|
367
|
-
if (node) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
404
|
+
if (!node) {
|
|
405
|
+
result.diagnostics.push(
|
|
406
|
+
makeDgmoError(lineNum, `Unexpected line: '${trimmed}'.`, 'warning')
|
|
407
|
+
);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
lastNodeLabel = node.label;
|
|
411
|
+
if (nodeLabels.has(node.label)) {
|
|
412
|
+
result.diagnostics.push(
|
|
413
|
+
makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
|
|
414
|
+
);
|
|
415
|
+
} else {
|
|
416
|
+
nodeLabels.add(node.label);
|
|
417
|
+
}
|
|
418
|
+
// Cascade group metadata into node (group provides defaults, node overrides)
|
|
419
|
+
if (currentGroup && isIndented && currentGroup.metadata) {
|
|
420
|
+
for (const [key, val] of Object.entries(currentGroup.metadata)) {
|
|
421
|
+
if (!(key in node.metadata)) {
|
|
422
|
+
node.metadata[key] = val;
|
|
382
423
|
}
|
|
383
424
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
425
|
+
}
|
|
426
|
+
result.nodes.push(node);
|
|
427
|
+
// Add to current group if indented
|
|
428
|
+
if (currentGroup && isIndented) {
|
|
429
|
+
currentGroup.nodeLabels.push(node.label);
|
|
389
430
|
}
|
|
390
431
|
}
|
|
391
432
|
|
|
@@ -398,20 +439,40 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
398
439
|
for (const edge of result.edges) {
|
|
399
440
|
if (!nodeLabels.has(edge.source)) {
|
|
400
441
|
result.diagnostics.push(
|
|
401
|
-
makeDgmoError(
|
|
442
|
+
makeDgmoError(
|
|
443
|
+
edge.lineNumber,
|
|
444
|
+
`Edge source "${edge.source}" is not a declared node`,
|
|
445
|
+
'warning'
|
|
446
|
+
)
|
|
402
447
|
);
|
|
403
448
|
// Auto-create an implicit node
|
|
404
449
|
if (!result.nodes.some((n) => n.label === edge.source)) {
|
|
405
|
-
result.nodes.push({
|
|
450
|
+
result.nodes.push({
|
|
451
|
+
label: edge.source,
|
|
452
|
+
status: 'na',
|
|
453
|
+
shape: inferParticipantType(edge.source),
|
|
454
|
+
lineNumber: edge.lineNumber,
|
|
455
|
+
metadata: {},
|
|
456
|
+
});
|
|
406
457
|
nodeLabels.add(edge.source);
|
|
407
458
|
}
|
|
408
459
|
}
|
|
409
460
|
if (!nodeLabels.has(edge.target)) {
|
|
410
461
|
result.diagnostics.push(
|
|
411
|
-
makeDgmoError(
|
|
462
|
+
makeDgmoError(
|
|
463
|
+
edge.lineNumber,
|
|
464
|
+
`Edge target "${edge.target}" is not a declared node`,
|
|
465
|
+
'warning'
|
|
466
|
+
)
|
|
412
467
|
);
|
|
413
468
|
if (!result.nodes.some((n) => n.label === edge.target)) {
|
|
414
|
-
result.nodes.push({
|
|
469
|
+
result.nodes.push({
|
|
470
|
+
label: edge.target,
|
|
471
|
+
status: 'na',
|
|
472
|
+
shape: inferParticipantType(edge.target),
|
|
473
|
+
lineNumber: edge.lineNumber,
|
|
474
|
+
metadata: {},
|
|
475
|
+
});
|
|
415
476
|
nodeLabels.add(edge.target);
|
|
416
477
|
}
|
|
417
478
|
}
|
|
@@ -443,7 +504,12 @@ function parseNodeLine(
|
|
|
443
504
|
const label = trimmed.slice(0, pipeIdx).trim();
|
|
444
505
|
const metaSegment = trimmed.slice(pipeIdx + 1).trim();
|
|
445
506
|
if (!label) return null;
|
|
446
|
-
const { status, metadata, hadStatusWord } = parseNodeMetadata(
|
|
507
|
+
const { status, metadata, hadStatusWord } = parseNodeMetadata(
|
|
508
|
+
metaSegment,
|
|
509
|
+
aliasMap,
|
|
510
|
+
lineNum,
|
|
511
|
+
diagnostics
|
|
512
|
+
);
|
|
447
513
|
return {
|
|
448
514
|
label,
|
|
449
515
|
// Unknown status bare word → keep null; no bare word at all → default 'na'
|
|
@@ -453,7 +519,13 @@ function parseNodeLine(
|
|
|
453
519
|
metadata,
|
|
454
520
|
};
|
|
455
521
|
}
|
|
456
|
-
return {
|
|
522
|
+
return {
|
|
523
|
+
label: trimmed,
|
|
524
|
+
status: 'na',
|
|
525
|
+
shape: inferParticipantType(trimmed),
|
|
526
|
+
lineNumber: lineNum,
|
|
527
|
+
metadata: {},
|
|
528
|
+
};
|
|
457
529
|
}
|
|
458
530
|
|
|
459
531
|
function parseEdgeLine(
|
|
@@ -481,8 +553,15 @@ function parseEdgeLine(
|
|
|
481
553
|
const pipeIdx = targetRest.indexOf('|');
|
|
482
554
|
if (pipeIdx >= 0) {
|
|
483
555
|
const metaSegment = targetRest.slice(pipeIdx + 1).trim();
|
|
484
|
-
const parsed = parseNodeMetadata(
|
|
485
|
-
|
|
556
|
+
const parsed = parseNodeMetadata(
|
|
557
|
+
metaSegment,
|
|
558
|
+
aliasMap,
|
|
559
|
+
lineNum,
|
|
560
|
+
diagnostics
|
|
561
|
+
);
|
|
562
|
+
status = parsed.hadStatusWord
|
|
563
|
+
? (parsed.status ?? null)
|
|
564
|
+
: (parsed.status ?? 'na');
|
|
486
565
|
metadata = parsed.metadata;
|
|
487
566
|
targetRest = targetRest.slice(0, pipeIdx).trim();
|
|
488
567
|
}
|
|
@@ -505,7 +584,9 @@ function parseEdgeLine(
|
|
|
505
584
|
let rest = trimmed.slice(arrowIdx + 2).trim();
|
|
506
585
|
|
|
507
586
|
if (!source || !rest) {
|
|
508
|
-
diagnostics.push(
|
|
587
|
+
diagnostics.push(
|
|
588
|
+
makeDgmoError(lineNum, 'Edge is missing source or target')
|
|
589
|
+
);
|
|
509
590
|
return null;
|
|
510
591
|
}
|
|
511
592
|
|
|
@@ -515,8 +596,15 @@ function parseEdgeLine(
|
|
|
515
596
|
const pipeIdx = rest.indexOf('|');
|
|
516
597
|
if (pipeIdx >= 0) {
|
|
517
598
|
const metaSegment = rest.slice(pipeIdx + 1).trim();
|
|
518
|
-
const parsed = parseNodeMetadata(
|
|
519
|
-
|
|
599
|
+
const parsed = parseNodeMetadata(
|
|
600
|
+
metaSegment,
|
|
601
|
+
aliasMap,
|
|
602
|
+
lineNum,
|
|
603
|
+
diagnostics
|
|
604
|
+
);
|
|
605
|
+
status = parsed.hadStatusWord
|
|
606
|
+
? (parsed.status ?? null)
|
|
607
|
+
: (parsed.status ?? 'na');
|
|
520
608
|
metadata = parsed.metadata;
|
|
521
609
|
rest = rest.slice(0, pipeIdx).trim();
|
|
522
610
|
}
|
package/src/kanban/parser.ts
CHANGED
|
@@ -26,12 +26,9 @@ const COLUMN_RE = /^\[(.+?)\](?:\s*\(([^)]+)\))?\s*(?:\|\s*(.+))?$/;
|
|
|
26
26
|
const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
|
|
27
27
|
|
|
28
28
|
/** Known kanban options (key-value). */
|
|
29
|
-
const KNOWN_OPTIONS = new Set([
|
|
30
|
-
'color-off', 'hide',
|
|
31
|
-
]);
|
|
29
|
+
const KNOWN_OPTIONS = new Set(['hide']);
|
|
32
30
|
/** Known kanban boolean options (bare keyword = on). */
|
|
33
|
-
const KNOWN_BOOLEANS = new Set<string>([
|
|
34
|
-
]);
|
|
31
|
+
const KNOWN_BOOLEANS = new Set<string>(['no-auto-color']);
|
|
35
32
|
|
|
36
33
|
// ============================================================
|
|
37
34
|
// Parser
|
|
@@ -125,15 +122,11 @@ export function parseKanban(
|
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
// Tag group heading — `tag
|
|
129
|
-
// Must be checked BEFORE OPTION_RE to prevent `tag
|
|
125
|
+
// Tag group heading — `tag Name`
|
|
126
|
+
// Must be checked BEFORE OPTION_RE to prevent `tag Rank` being swallowed as option
|
|
130
127
|
if (!contentStarted) {
|
|
131
128
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
132
129
|
if (tagBlockMatch) {
|
|
133
|
-
if (tagBlockMatch.deprecated) {
|
|
134
|
-
result.diagnostics.push(makeDgmoError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
130
|
currentTagGroup = {
|
|
138
131
|
name: tagBlockMatch.name,
|
|
139
132
|
alias: tagBlockMatch.alias,
|
|
@@ -141,7 +134,10 @@ export function parseKanban(
|
|
|
141
134
|
lineNumber,
|
|
142
135
|
};
|
|
143
136
|
if (tagBlockMatch.alias) {
|
|
144
|
-
aliasMap.set(
|
|
137
|
+
aliasMap.set(
|
|
138
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
139
|
+
tagBlockMatch.name.toLowerCase()
|
|
140
|
+
);
|
|
145
141
|
}
|
|
146
142
|
result.tagGroups.push(currentTagGroup);
|
|
147
143
|
continue;
|
|
@@ -160,7 +156,10 @@ export function parseKanban(
|
|
|
160
156
|
}
|
|
161
157
|
}
|
|
162
158
|
// Bare boolean option (single keyword, no value)
|
|
163
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
KNOWN_BOOLEANS.has(trimmed.toLowerCase()) &&
|
|
161
|
+
!COLUMN_RE.test(trimmed)
|
|
162
|
+
) {
|
|
164
163
|
result.options[trimmed.toLowerCase()] = 'on';
|
|
165
164
|
continue;
|
|
166
165
|
}
|
|
@@ -202,7 +201,12 @@ export function parseKanban(
|
|
|
202
201
|
if (LEGACY_COLUMN_RE.test(trimmed)) {
|
|
203
202
|
const legacyMatch = trimmed.match(LEGACY_COLUMN_RE)!;
|
|
204
203
|
const name = legacyMatch[1].replace(/\s*\(.*\)\s*$/, '').trim();
|
|
205
|
-
result.diagnostics.push(
|
|
204
|
+
result.diagnostics.push(
|
|
205
|
+
makeDgmoError(
|
|
206
|
+
lineNumber,
|
|
207
|
+
`'== ${name} ==' is no longer supported. Use '[${name}]' instead`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
206
210
|
continue;
|
|
207
211
|
}
|
|
208
212
|
|
|
@@ -227,7 +231,7 @@ export function parseKanban(
|
|
|
227
231
|
columnCounter++;
|
|
228
232
|
const colName = columnMatch[1].trim();
|
|
229
233
|
const colColor = columnMatch[2]
|
|
230
|
-
? resolveColor(columnMatch[2].trim(), palette) ?? undefined
|
|
234
|
+
? (resolveColor(columnMatch[2].trim(), palette) ?? undefined)
|
|
231
235
|
: undefined;
|
|
232
236
|
|
|
233
237
|
// Parse pipe metadata (e.g., "| wip: 3, t: Sprint1")
|
|
@@ -236,7 +240,10 @@ export function parseKanban(
|
|
|
236
240
|
const pipeStr = columnMatch[3];
|
|
237
241
|
if (pipeStr) {
|
|
238
242
|
const pipeSegments = ['', pipeStr];
|
|
239
|
-
Object.assign(
|
|
243
|
+
Object.assign(
|
|
244
|
+
columnMetadata,
|
|
245
|
+
parsePipeMetadata(pipeSegments, aliasMap)
|
|
246
|
+
);
|
|
240
247
|
// Extract wip from metadata
|
|
241
248
|
if (columnMetadata.wip) {
|
|
242
249
|
const wipVal = parseInt(columnMetadata.wip, 10);
|
|
@@ -303,8 +310,8 @@ export function parseKanban(
|
|
|
303
310
|
continue;
|
|
304
311
|
}
|
|
305
312
|
|
|
306
|
-
// Un-indented non-column line in content phase —
|
|
307
|
-
|
|
313
|
+
// Un-indented non-column line in content phase — stray text
|
|
314
|
+
warn(lineNumber, `Unexpected line: '${trimmed}'.`);
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
// Finalize last card's endLineNumber
|
|
@@ -332,7 +339,8 @@ export function parseKanban(
|
|
|
332
339
|
for (const col of result.columns) {
|
|
333
340
|
for (const card of col.cards) {
|
|
334
341
|
for (const [tagKey, tagValue] of Object.entries(card.tags)) {
|
|
335
|
-
const groupKey =
|
|
342
|
+
const groupKey =
|
|
343
|
+
aliasMap.get(tagKey.toLowerCase()) ?? tagKey.toLowerCase();
|
|
336
344
|
const validValues = tagValueSets.get(groupKey);
|
|
337
345
|
if (validValues && !validValues.has(tagValue.toLowerCase())) {
|
|
338
346
|
const entries = result.tagGroups
|