@diagrammo/dgmo 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +180 -178
- package/dist/index.cjs +5296 -2209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -16
- package/dist/index.d.ts +236 -16
- package/dist/index.js +12423 -9343
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/renderer.ts +6 -6
- package/src/class/renderer.ts +183 -7
- package/src/cli.ts +3 -11
- package/src/colors.ts +3 -3
- package/src/d3.ts +128 -23
- package/src/dgmo-router.ts +3 -1
- package/src/er/renderer.ts +11 -5
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/index.ts +27 -0
- package/src/infra/renderer.ts +48 -12
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +293 -10
- package/src/initiative-status/types.ts +6 -0
- package/src/org/layout.ts +22 -55
- package/src/org/renderer.ts +4 -8
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/renderer.ts +5 -4
- package/src/sharing.ts +8 -0
- package/src/sitemap/renderer.ts +4 -4
- package/src/utils/duration.ts +212 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -13,6 +13,9 @@ import type {
|
|
|
13
13
|
} from './types';
|
|
14
14
|
import { VALID_STATUSES } from './types';
|
|
15
15
|
import { inferParticipantType } from '../sequence/participant-inference';
|
|
16
|
+
import { matchTagBlockHeading, injectDefaultTagMetadata, validateTagValues } from '../utils/tag-groups';
|
|
17
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
18
|
+
import { extractColor } from '../utils/parsing';
|
|
16
19
|
|
|
17
20
|
// ============================================================
|
|
18
21
|
// Heuristic — does this content look like an initiative-status diagram?
|
|
@@ -42,6 +45,62 @@ export function looksLikeInitiativeStatus(content: string): boolean {
|
|
|
42
45
|
return hasIndentedArrow;
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// ============================================================
|
|
49
|
+
// Metadata parser — splits comma-delimited segment into status + tags
|
|
50
|
+
// ============================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse the metadata segment after a `|` pipe into a status keyword
|
|
54
|
+
* and key:value tag pairs. Does NOT use parsePipeMetadata() from
|
|
55
|
+
* parsing.ts — that utility drops bare words (no colon), making it
|
|
56
|
+
* incompatible with status keyword extraction.
|
|
57
|
+
*
|
|
58
|
+
* @param segment The raw text after `|` — e.g. `"wip, p: Build, t: Backend"`
|
|
59
|
+
* @param aliasMap Maps lowercase aliases to lowercase group names
|
|
60
|
+
* @param lineNum Line number for diagnostic reporting
|
|
61
|
+
* @param diagnostics Array to push warnings into
|
|
62
|
+
*/
|
|
63
|
+
export function parseNodeMetadata(
|
|
64
|
+
segment: string,
|
|
65
|
+
aliasMap: Map<string, string>,
|
|
66
|
+
lineNum?: number,
|
|
67
|
+
diagnostics?: DgmoError[]
|
|
68
|
+
): { status: InitiativeStatus; metadata: Record<string, string>; hadStatusWord: boolean } {
|
|
69
|
+
const metadata: Record<string, string> = {};
|
|
70
|
+
let status: InitiativeStatus = null;
|
|
71
|
+
let hadStatusWord = false;
|
|
72
|
+
|
|
73
|
+
const items = segment.split(',');
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
const trimmed = item.trim();
|
|
76
|
+
if (!trimmed) continue;
|
|
77
|
+
|
|
78
|
+
const colonIdx = trimmed.indexOf(':');
|
|
79
|
+
if (colonIdx >= 0) {
|
|
80
|
+
// key: value pair
|
|
81
|
+
const rawKey = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
82
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
83
|
+
// Resolve alias to group name
|
|
84
|
+
const resolvedKey = aliasMap.get(rawKey) ?? rawKey;
|
|
85
|
+
metadata[resolvedKey] = value;
|
|
86
|
+
} else {
|
|
87
|
+
// Bare word — check if it's a status keyword
|
|
88
|
+
hadStatusWord = true;
|
|
89
|
+
const lower = trimmed.toLowerCase();
|
|
90
|
+
if (VALID_STATUSES.includes(lower)) {
|
|
91
|
+
status = lower as InitiativeStatus;
|
|
92
|
+
} else if (lineNum !== undefined && diagnostics) {
|
|
93
|
+
// Unknown bare word — likely a status typo, emit warning
|
|
94
|
+
const hint = suggest(lower, VALID_STATUSES);
|
|
95
|
+
const msg = `Unknown status "${trimmed}"${hint ? `. ${hint}` : ''}`;
|
|
96
|
+
diagnostics.push(makeDgmoError(lineNum, msg, 'warning'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { status, metadata, hadStatusWord };
|
|
102
|
+
}
|
|
103
|
+
|
|
45
104
|
// ============================================================
|
|
46
105
|
// Parser
|
|
47
106
|
// ============================================================
|
|
@@ -58,6 +117,17 @@ function parseStatus(raw: string, line: number, diagnostics: DgmoError[]): Initi
|
|
|
58
117
|
return null;
|
|
59
118
|
}
|
|
60
119
|
|
|
120
|
+
/** Measure leading whitespace (tabs = 4 spaces) */
|
|
121
|
+
function measureIndent(line: string): number {
|
|
122
|
+
let count = 0;
|
|
123
|
+
for (const ch of line) {
|
|
124
|
+
if (ch === ' ') count++;
|
|
125
|
+
else if (ch === '\t') count += 4;
|
|
126
|
+
else break;
|
|
127
|
+
}
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
130
|
+
|
|
61
131
|
export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
62
132
|
const result: ParsedInitiativeStatus = {
|
|
63
133
|
type: 'initiative-status',
|
|
@@ -66,7 +136,9 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
66
136
|
nodes: [],
|
|
67
137
|
edges: [],
|
|
68
138
|
groups: [],
|
|
139
|
+
tagGroups: [],
|
|
69
140
|
options: {},
|
|
141
|
+
initialHiddenTagValues: new Map(),
|
|
70
142
|
diagnostics: [],
|
|
71
143
|
error: null,
|
|
72
144
|
};
|
|
@@ -76,6 +148,15 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
76
148
|
let currentGroup: ISGroup | null = null;
|
|
77
149
|
let lastNodeLabel: string | null = null;
|
|
78
150
|
|
|
151
|
+
// Tag block state
|
|
152
|
+
let contentStarted = false;
|
|
153
|
+
let currentTagGroup: TagGroup | null = null;
|
|
154
|
+
const aliasMap = new Map<string, string>(); // lowercase alias → lowercase group name
|
|
155
|
+
|
|
156
|
+
const pushWarning = (lineNumber: number, message: string) => {
|
|
157
|
+
result.diagnostics.push(makeDgmoError(lineNumber, message, 'warning'));
|
|
158
|
+
};
|
|
159
|
+
|
|
79
160
|
for (let i = 0; i < lines.length; i++) {
|
|
80
161
|
const lineNum = i + 1; // 1-based
|
|
81
162
|
const raw = lines[i];
|
|
@@ -105,9 +186,79 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
105
186
|
continue;
|
|
106
187
|
}
|
|
107
188
|
|
|
189
|
+
// hide: directive — parse before tag blocks and content
|
|
190
|
+
const hideMatch = trimmed.match(/^hide\s*:\s*(.+)/i);
|
|
191
|
+
if (hideMatch) {
|
|
192
|
+
const pairs = hideMatch[1].split(',');
|
|
193
|
+
for (const pair of pairs) {
|
|
194
|
+
const colonIdx = pair.indexOf(':');
|
|
195
|
+
if (colonIdx >= 0) {
|
|
196
|
+
const groupKey = pair.slice(0, colonIdx).trim().toLowerCase();
|
|
197
|
+
const value = pair.slice(colonIdx + 1).trim().toLowerCase();
|
|
198
|
+
if (groupKey && value) {
|
|
199
|
+
if (!result.initialHiddenTagValues.has(groupKey)) {
|
|
200
|
+
result.initialHiddenTagValues.set(groupKey, new Set());
|
|
201
|
+
}
|
|
202
|
+
result.initialHiddenTagValues.get(groupKey)!.add(value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Tag group heading — must be checked BEFORE group/node/edge matching
|
|
210
|
+
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
211
|
+
if (tagBlockMatch) {
|
|
212
|
+
if (contentStarted) {
|
|
213
|
+
result.diagnostics.push(
|
|
214
|
+
makeDgmoError(lineNum, 'Tag groups must appear before diagram content', 'error')
|
|
215
|
+
);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (tagBlockMatch.deprecated) {
|
|
219
|
+
pushWarning(lineNum, `'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
220
|
+
}
|
|
221
|
+
currentTagGroup = {
|
|
222
|
+
name: tagBlockMatch.name,
|
|
223
|
+
alias: tagBlockMatch.alias,
|
|
224
|
+
entries: [],
|
|
225
|
+
lineNumber: lineNum,
|
|
226
|
+
};
|
|
227
|
+
if (tagBlockMatch.alias) {
|
|
228
|
+
aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
|
|
229
|
+
}
|
|
230
|
+
result.tagGroups.push(currentTagGroup);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Tag group entries (indented Value(color) [default] under tag heading)
|
|
235
|
+
if (currentTagGroup && !contentStarted) {
|
|
236
|
+
const indent = measureIndent(raw);
|
|
237
|
+
if (indent > 0) {
|
|
238
|
+
const isDefault = /\bdefault\s*$/i.test(trimmed);
|
|
239
|
+
const entryText = isDefault
|
|
240
|
+
? trimmed.replace(/\s+default\s*$/i, '').trim()
|
|
241
|
+
: trimmed;
|
|
242
|
+
const { label, color } = extractColor(entryText);
|
|
243
|
+
if (isDefault) {
|
|
244
|
+
currentTagGroup.defaultValue = label;
|
|
245
|
+
}
|
|
246
|
+
currentTagGroup.entries.push({
|
|
247
|
+
value: label,
|
|
248
|
+
color: color ?? '',
|
|
249
|
+
lineNumber: lineNum,
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Non-indented line after tag group — close and fall through
|
|
254
|
+
currentTagGroup = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
108
257
|
// Group header: [Group Name]
|
|
109
258
|
const groupMatch = trimmed.match(/^\[(.+)\]\s*$/);
|
|
110
259
|
if (groupMatch) {
|
|
260
|
+
contentStarted = true;
|
|
261
|
+
currentTagGroup = null;
|
|
111
262
|
// Close previous group
|
|
112
263
|
if (currentGroup) {
|
|
113
264
|
result.groups.push(currentGroup);
|
|
@@ -125,6 +276,8 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
125
276
|
|
|
126
277
|
// Edge: contains `->` or labeled form `-label->`
|
|
127
278
|
if (trimmed.includes('->')) {
|
|
279
|
+
contentStarted = true;
|
|
280
|
+
currentTagGroup = null;
|
|
128
281
|
let edgeText = trimmed;
|
|
129
282
|
// Indented `-> Target` or `-label-> Target` shorthand
|
|
130
283
|
if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
|
|
@@ -136,13 +289,15 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
136
289
|
}
|
|
137
290
|
edgeText = `${lastNodeLabel} ${trimmed}`;
|
|
138
291
|
}
|
|
139
|
-
const edge = parseEdgeLine(edgeText, lineNum, result.diagnostics);
|
|
292
|
+
const edge = parseEdgeLine(edgeText, lineNum, aliasMap, result.diagnostics);
|
|
140
293
|
if (edge) result.edges.push(edge);
|
|
141
294
|
continue;
|
|
142
295
|
}
|
|
143
296
|
|
|
144
297
|
// Node: everything else
|
|
145
|
-
|
|
298
|
+
contentStarted = true;
|
|
299
|
+
currentTagGroup = null;
|
|
300
|
+
const node = parseNodeLine(trimmed, lineNum, aliasMap, result.diagnostics);
|
|
146
301
|
if (node) {
|
|
147
302
|
lastNodeLabel = node.label;
|
|
148
303
|
if (nodeLabels.has(node.label)) {
|
|
@@ -173,7 +328,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
173
328
|
);
|
|
174
329
|
// Auto-create an implicit node
|
|
175
330
|
if (!result.nodes.some((n) => n.label === edge.source)) {
|
|
176
|
-
result.nodes.push({ label: edge.source, status: 'na', shape: inferParticipantType(edge.source), lineNumber: edge.lineNumber });
|
|
331
|
+
result.nodes.push({ label: edge.source, status: 'na', shape: inferParticipantType(edge.source), lineNumber: edge.lineNumber, metadata: {} });
|
|
177
332
|
nodeLabels.add(edge.source);
|
|
178
333
|
}
|
|
179
334
|
}
|
|
@@ -182,12 +337,18 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
182
337
|
makeDgmoError(edge.lineNumber, `Edge target "${edge.target}" is not a declared node`, 'warning')
|
|
183
338
|
);
|
|
184
339
|
if (!result.nodes.some((n) => n.label === edge.target)) {
|
|
185
|
-
result.nodes.push({ label: edge.target, status: 'na', shape: inferParticipantType(edge.target), lineNumber: edge.lineNumber });
|
|
340
|
+
result.nodes.push({ label: edge.target, status: 'na', shape: inferParticipantType(edge.target), lineNumber: edge.lineNumber, metadata: {} });
|
|
186
341
|
nodeLabels.add(edge.target);
|
|
187
342
|
}
|
|
188
343
|
}
|
|
189
344
|
}
|
|
190
345
|
|
|
346
|
+
// Post-parse: inject default tag metadata and validate tag values
|
|
347
|
+
if (result.tagGroups.length > 0) {
|
|
348
|
+
injectDefaultTagMetadata(result.nodes, result.tagGroups);
|
|
349
|
+
validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
|
|
350
|
+
}
|
|
351
|
+
|
|
191
352
|
return result;
|
|
192
353
|
}
|
|
193
354
|
|
|
@@ -198,27 +359,36 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
198
359
|
function parseNodeLine(
|
|
199
360
|
trimmed: string,
|
|
200
361
|
lineNum: number,
|
|
362
|
+
aliasMap: Map<string, string>,
|
|
201
363
|
diagnostics: DgmoError[]
|
|
202
364
|
): ISNode | null {
|
|
203
|
-
// Format: <label> | <status
|
|
365
|
+
// Format: <label> | <status>, <key: value>, ...
|
|
204
366
|
// or just: <label>
|
|
205
|
-
const pipeIdx = trimmed.
|
|
367
|
+
const pipeIdx = trimmed.indexOf('|');
|
|
206
368
|
if (pipeIdx >= 0) {
|
|
207
369
|
const label = trimmed.slice(0, pipeIdx).trim();
|
|
208
|
-
const
|
|
370
|
+
const metaSegment = trimmed.slice(pipeIdx + 1).trim();
|
|
209
371
|
if (!label) return null;
|
|
210
|
-
const status =
|
|
211
|
-
return {
|
|
372
|
+
const { status, metadata, hadStatusWord } = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
|
|
373
|
+
return {
|
|
374
|
+
label,
|
|
375
|
+
// Unknown status bare word → keep null; no bare word at all → default 'na'
|
|
376
|
+
status: hadStatusWord ? status : (status ?? 'na'),
|
|
377
|
+
shape: inferParticipantType(label),
|
|
378
|
+
lineNumber: lineNum,
|
|
379
|
+
metadata,
|
|
380
|
+
};
|
|
212
381
|
}
|
|
213
|
-
return { label: trimmed, status: 'na', shape: inferParticipantType(trimmed), lineNumber: lineNum };
|
|
382
|
+
return { label: trimmed, status: 'na', shape: inferParticipantType(trimmed), lineNumber: lineNum, metadata: {} };
|
|
214
383
|
}
|
|
215
384
|
|
|
216
385
|
function parseEdgeLine(
|
|
217
386
|
trimmed: string,
|
|
218
387
|
lineNum: number,
|
|
388
|
+
aliasMap: Map<string, string>,
|
|
219
389
|
diagnostics: DgmoError[]
|
|
220
390
|
): ISEdge | null {
|
|
221
|
-
// Format: <source> -> <target>: <label> | <status
|
|
391
|
+
// Format: <source> -> <target>: <label> | <status>, <key: value>, ...
|
|
222
392
|
// or: <source> -> <target> | <status>
|
|
223
393
|
// or: <source> -> <target>: <label>
|
|
224
394
|
// or: <source> -> <target>
|
|
@@ -232,13 +402,15 @@ function parseEdgeLine(
|
|
|
232
402
|
let targetRest = labeledMatch[3].trim();
|
|
233
403
|
|
|
234
404
|
if (label) {
|
|
235
|
-
// Extract status from end (after last |)
|
|
236
405
|
let status: InitiativeStatus = 'na';
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
406
|
+
let metadata: Record<string, string> = {};
|
|
407
|
+
const pipeIdx = targetRest.indexOf('|');
|
|
408
|
+
if (pipeIdx >= 0) {
|
|
409
|
+
const metaSegment = targetRest.slice(pipeIdx + 1).trim();
|
|
410
|
+
const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
|
|
411
|
+
status = parsed.hadStatusWord ? (parsed.status ?? null) : (parsed.status ?? 'na');
|
|
412
|
+
metadata = parsed.metadata;
|
|
413
|
+
targetRest = targetRest.slice(0, pipeIdx).trim();
|
|
242
414
|
}
|
|
243
415
|
|
|
244
416
|
const target = targetRest.trim();
|
|
@@ -247,7 +419,7 @@ function parseEdgeLine(
|
|
|
247
419
|
return null;
|
|
248
420
|
}
|
|
249
421
|
|
|
250
|
-
return { source, target, label, status, lineNumber: lineNum };
|
|
422
|
+
return { source, target, label, status, lineNumber: lineNum, metadata };
|
|
251
423
|
}
|
|
252
424
|
// Empty label — fall through to plain arrow parsing
|
|
253
425
|
}
|
|
@@ -263,13 +435,16 @@ function parseEdgeLine(
|
|
|
263
435
|
return null;
|
|
264
436
|
}
|
|
265
437
|
|
|
266
|
-
// Extract
|
|
438
|
+
// Extract metadata from end (after |)
|
|
267
439
|
let status: InitiativeStatus = 'na';
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
440
|
+
let metadata: Record<string, string> = {};
|
|
441
|
+
const pipeIdx = rest.indexOf('|');
|
|
442
|
+
if (pipeIdx >= 0) {
|
|
443
|
+
const metaSegment = rest.slice(pipeIdx + 1).trim();
|
|
444
|
+
const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
|
|
445
|
+
status = parsed.hadStatusWord ? (parsed.status ?? null) : (parsed.status ?? 'na');
|
|
446
|
+
metadata = parsed.metadata;
|
|
447
|
+
rest = rest.slice(0, pipeIdx).trim();
|
|
273
448
|
}
|
|
274
449
|
|
|
275
450
|
// Extract target and optional label (target: label)
|
|
@@ -288,5 +463,5 @@ function parseEdgeLine(
|
|
|
288
463
|
return null;
|
|
289
464
|
}
|
|
290
465
|
|
|
291
|
-
return { source, target, label, status, lineNumber: lineNum };
|
|
466
|
+
return { source, target, label, status, lineNumber: lineNum, metadata };
|
|
292
467
|
}
|