@diagrammo/dgmo 0.8.5 → 0.8.7
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.md +34 -27
- package/.cursorrules +20 -2
- package/.github/copilot-instructions.md +20 -2
- package/.windsurfrules +20 -2
- package/AGENTS.md +23 -3
- package/README.md +0 -1
- package/dist/cli.cjs +189 -190
- package/dist/editor.cjs +3 -18
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +3 -18
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +4 -21
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +4 -21
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +2791 -2999
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -56
- package/dist/index.d.ts +56 -56
- package/dist/index.js +2786 -2992
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +112 -121
- package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +78 -0
- package/src/boxes-and-lines/layout.ts +319 -0
- package/src/boxes-and-lines/parser.ts +697 -0
- package/src/boxes-and-lines/renderer.ts +848 -0
- package/src/boxes-and-lines/types.ts +40 -0
- package/src/c4/parser.ts +10 -5
- package/src/c4/renderer.ts +232 -56
- package/src/chart.ts +9 -4
- package/src/cli.ts +6 -5
- package/src/completion.ts +25 -33
- package/src/d3.ts +26 -27
- package/src/dgmo-router.ts +3 -7
- package/src/echarts.ts +38 -2
- package/src/editor/keywords.ts +4 -19
- package/src/er/parser.ts +10 -4
- package/src/gantt/parser.ts +10 -4
- package/src/gantt/renderer.ts +3 -5
- package/src/index.ts +17 -26
- package/src/infra/parser.ts +10 -5
- package/src/infra/renderer.ts +2 -2
- package/src/kanban/parser.ts +10 -5
- package/src/kanban/renderer.ts +43 -18
- package/src/org/parser.ts +7 -4
- package/src/org/renderer.ts +40 -29
- package/src/sequence/parser.ts +11 -5
- package/src/sequence/renderer.ts +114 -45
- package/src/sitemap/parser.ts +8 -4
- package/src/sitemap/renderer.ts +137 -57
- package/src/utils/legend-svg.ts +44 -20
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +59 -15
- package/gallery/fixtures/initiative-status-full.dgmo +0 -46
- package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
- package/gallery/fixtures/initiative-status.dgmo +0 -9
- package/src/initiative-status/collapse.ts +0 -76
- package/src/initiative-status/filter.ts +0 -63
- package/src/initiative-status/layout.ts +0 -650
- package/src/initiative-status/parser.ts +0 -629
- package/src/initiative-status/renderer.ts +0 -1199
- package/src/initiative-status/types.ts +0 -57
package/src/utils/tag-groups.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface TagGroup {
|
|
|
16
16
|
name: string;
|
|
17
17
|
alias?: string;
|
|
18
18
|
entries: TagEntry[];
|
|
19
|
-
/**
|
|
19
|
+
/** Default value for nodes without explicit metadata. First entry unless another is marked `default`. */
|
|
20
20
|
defaultValue?: string;
|
|
21
21
|
lineNumber: number;
|
|
22
22
|
}
|
|
@@ -30,6 +30,26 @@ export interface TagBlockMatch {
|
|
|
30
30
|
inlineValues?: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// ── Default Modifier ────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strip trailing `default` keyword from a tag entry string.
|
|
37
|
+
* Returns the cleaned text and whether the keyword was present.
|
|
38
|
+
*
|
|
39
|
+
* Examples:
|
|
40
|
+
* "NA(gray) default" → { text: "NA(gray)", isDefault: true }
|
|
41
|
+
* "Done(green)" → { text: "Done(green)", isDefault: false }
|
|
42
|
+
*/
|
|
43
|
+
export function stripDefaultModifier(text: string): {
|
|
44
|
+
text: string;
|
|
45
|
+
isDefault: boolean;
|
|
46
|
+
} {
|
|
47
|
+
if (/\bdefault\s*$/.test(text)) {
|
|
48
|
+
return { text: text.replace(/\s+default\s*$/, '').trim(), isDefault: true };
|
|
49
|
+
}
|
|
50
|
+
return { text, isDefault: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
// ── Regexes ─────────────────────────────────────────────────
|
|
34
54
|
|
|
35
55
|
/** Canonical syntax: line starting with `tag` keyword (no colon). */
|
|
@@ -89,10 +109,15 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
89
109
|
// BEFORE any value tokens (values have `(color)` suffixes or appear after we see a comma).
|
|
90
110
|
|
|
91
111
|
// First check for explicit `alias` keyword: `tag Name alias X`
|
|
92
|
-
const aliasKeywordIdx = tokens.findIndex(
|
|
112
|
+
const aliasKeywordIdx = tokens.findIndex(
|
|
113
|
+
(t, i) => i > 0 && t.toLowerCase() === 'alias'
|
|
114
|
+
);
|
|
93
115
|
if (aliasKeywordIdx > 0 && aliasKeywordIdx + 1 < tokens.length) {
|
|
94
116
|
// Everything before `alias` is the name, the token after `alias` is the alias
|
|
95
|
-
name = tokens
|
|
117
|
+
name = tokens
|
|
118
|
+
.slice(0, aliasKeywordIdx)
|
|
119
|
+
.map((t) => stripQuotes(t))
|
|
120
|
+
.join(' ');
|
|
96
121
|
alias = tokens[aliasKeywordIdx + 1];
|
|
97
122
|
restStartIdx = aliasKeywordIdx + 2;
|
|
98
123
|
} else {
|
|
@@ -103,7 +128,11 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
103
128
|
|
|
104
129
|
if (tokens.length === 1) {
|
|
105
130
|
// Just `tag Name` — no alias, no values
|
|
106
|
-
} else if (
|
|
131
|
+
} else if (
|
|
132
|
+
tokens.length === 2 &&
|
|
133
|
+
isAliasToken(tokens[1]) &&
|
|
134
|
+
!commaInRemaining
|
|
135
|
+
) {
|
|
107
136
|
// `tag Priority p` — alias only, no values
|
|
108
137
|
alias = tokens[1];
|
|
109
138
|
restStartIdx = 2;
|
|
@@ -130,11 +159,17 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
130
159
|
if (valueStart > 1 && isAliasToken(tokens[valueStart - 1])) {
|
|
131
160
|
alias = tokens[valueStart - 1];
|
|
132
161
|
// Name is everything from token[0] to token[valueStart-2]
|
|
133
|
-
name = tokens
|
|
162
|
+
name = tokens
|
|
163
|
+
.slice(0, valueStart - 1)
|
|
164
|
+
.map((t) => stripQuotes(t))
|
|
165
|
+
.join(' ');
|
|
134
166
|
restStartIdx = valueStart;
|
|
135
167
|
} else {
|
|
136
168
|
// No alias — name is everything before values
|
|
137
|
-
name = tokens
|
|
169
|
+
name = tokens
|
|
170
|
+
.slice(0, valueStart)
|
|
171
|
+
.map((t) => stripQuotes(t))
|
|
172
|
+
.join(' ');
|
|
138
173
|
restStartIdx = valueStart;
|
|
139
174
|
}
|
|
140
175
|
}
|
|
@@ -146,7 +181,10 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
146
181
|
if (restStartIdx < tokens.length) {
|
|
147
182
|
// Rejoin and split by comma for inline values
|
|
148
183
|
const valueStr = tokens.slice(restStartIdx).join(' ');
|
|
149
|
-
inlineValues = valueStr
|
|
184
|
+
inlineValues = valueStr
|
|
185
|
+
.split(',')
|
|
186
|
+
.map((v) => v.trim())
|
|
187
|
+
.filter(Boolean);
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
// Check for trailing color hint on name (without inline values)
|
|
@@ -163,7 +201,8 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
163
201
|
name,
|
|
164
202
|
alias,
|
|
165
203
|
colorHint,
|
|
166
|
-
inlineValues:
|
|
204
|
+
inlineValues:
|
|
205
|
+
inlineValues && inlineValues.length > 0 ? inlineValues : undefined,
|
|
167
206
|
};
|
|
168
207
|
}
|
|
169
208
|
|
|
@@ -204,9 +243,8 @@ export function resolveTagColor(
|
|
|
204
243
|
if (!metaValue) return '#999999';
|
|
205
244
|
|
|
206
245
|
return (
|
|
207
|
-
group.entries.find(
|
|
208
|
-
|
|
209
|
-
)?.color ?? '#999999'
|
|
246
|
+
group.entries.find((e) => e.value.toLowerCase() === metaValue.toLowerCase())
|
|
247
|
+
?.color ?? '#999999'
|
|
210
248
|
);
|
|
211
249
|
}
|
|
212
250
|
|
|
@@ -223,7 +261,10 @@ export function resolveTagColor(
|
|
|
223
261
|
* @param suggestFn Optional did-you-mean suggestion function
|
|
224
262
|
*/
|
|
225
263
|
export function validateTagValues(
|
|
226
|
-
entities: ReadonlyArray<{
|
|
264
|
+
entities: ReadonlyArray<{
|
|
265
|
+
metadata: Record<string, string>;
|
|
266
|
+
lineNumber: number;
|
|
267
|
+
}>,
|
|
227
268
|
tagGroups: ReadonlyArray<TagGroup>,
|
|
228
269
|
pushWarning: (lineNumber: number, message: string) => void,
|
|
229
270
|
suggestFn?: (input: string, candidates: readonly string[]) => string | null
|
|
@@ -244,8 +285,8 @@ export function validateTagValues(
|
|
|
244
285
|
// Suppress warning if the value is a prefix of any valid entry —
|
|
245
286
|
// the user is likely still typing (live parse during editing).
|
|
246
287
|
const valueLower = value.toLowerCase();
|
|
247
|
-
const isPrefix = group.entries.some(
|
|
248
|
-
|
|
288
|
+
const isPrefix = group.entries.some((e) =>
|
|
289
|
+
e.value.toLowerCase().startsWith(valueLower)
|
|
249
290
|
);
|
|
250
291
|
if (!isPrefix) {
|
|
251
292
|
const defined = group.entries.map((e) => e.value);
|
|
@@ -281,7 +322,10 @@ export function injectDefaultTagMetadata(
|
|
|
281
322
|
const defaults: { key: string; value: string }[] = [];
|
|
282
323
|
for (const group of tagGroups) {
|
|
283
324
|
if (group.defaultValue) {
|
|
284
|
-
defaults.push({
|
|
325
|
+
defaults.push({
|
|
326
|
+
key: group.name.toLowerCase(),
|
|
327
|
+
value: group.defaultValue,
|
|
328
|
+
});
|
|
285
329
|
}
|
|
286
330
|
}
|
|
287
331
|
if (defaults.length === 0) return;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
initiative-status Platform Modernization — Full Status
|
|
2
|
-
|
|
3
|
-
tag Phase alias p
|
|
4
|
-
Discovery(green) default
|
|
5
|
-
Build(blue)
|
|
6
|
-
Launch(orange)
|
|
7
|
-
Maintenance(purple)
|
|
8
|
-
|
|
9
|
-
tag Team alias t
|
|
10
|
-
Frontend(blue)
|
|
11
|
-
Backend(green)
|
|
12
|
-
Platform(teal)
|
|
13
|
-
Data(purple)
|
|
14
|
-
|
|
15
|
-
[Identity] | t: Backend
|
|
16
|
-
Auth Service | done, p: Build
|
|
17
|
-
SSO Integration | doing, p: Build
|
|
18
|
-
-> Auth Service | done
|
|
19
|
-
MFA Rollout | blocked, p: Launch
|
|
20
|
-
-> SSO Integration | doing
|
|
21
|
-
|
|
22
|
-
[Payments] | t: Platform
|
|
23
|
-
Payment Gateway | doing, p: Build
|
|
24
|
-
Billing UI | todo, p: Build, t: Frontend
|
|
25
|
-
-> Payment Gateway | doing
|
|
26
|
-
Invoice API | todo, p: Launch
|
|
27
|
-
-> Payment Gateway | doing
|
|
28
|
-
-> Billing UI | todo
|
|
29
|
-
|
|
30
|
-
[Search] | t: Backend
|
|
31
|
-
Search API | done, p: Build
|
|
32
|
-
Search UI | doing, p: Build, t: Frontend
|
|
33
|
-
-> Search API | done
|
|
34
|
-
Search Analytics | na, p: Maintenance, t: Data
|
|
35
|
-
|
|
36
|
-
[Observability] | t: Platform
|
|
37
|
-
Metrics Pipeline | done, p: Discovery
|
|
38
|
-
Alert Rules | done, p: Build
|
|
39
|
-
-> Metrics Pipeline | done
|
|
40
|
-
Dashboard | doing, p: Build, t: Frontend
|
|
41
|
-
-> Metrics Pipeline | done
|
|
42
|
-
-> Alert Rules | done
|
|
43
|
-
|
|
44
|
-
Auth Service -> Payment Gateway: validates | done
|
|
45
|
-
SSO Integration -> Search API: provides identity | done
|
|
46
|
-
Metrics Pipeline -> Search API: monitors | done
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
initiative-status Platform Migration
|
|
2
|
-
|
|
3
|
-
tag Phase alias p
|
|
4
|
-
Discovery default
|
|
5
|
-
Build
|
|
6
|
-
Launch
|
|
7
|
-
|
|
8
|
-
tag Team alias t
|
|
9
|
-
Frontend
|
|
10
|
-
Backend
|
|
11
|
-
Platform
|
|
12
|
-
|
|
13
|
-
[Identity]
|
|
14
|
-
Auth Service | done, p: Discovery, t: Backend
|
|
15
|
-
SSO Integration | doing, p: Build, t: Backend
|
|
16
|
-
-> Auth Service | done
|
|
17
|
-
|
|
18
|
-
[Payments]
|
|
19
|
-
Payment Gateway | doing, p: Build, t: Platform
|
|
20
|
-
Billing UI | todo, p: Build, t: Frontend
|
|
21
|
-
-> Payment Gateway | doing
|
|
22
|
-
|
|
23
|
-
[Search]
|
|
24
|
-
Search API | todo, p: Launch, t: Backend
|
|
25
|
-
Search UI | todo, p: Launch, t: Frontend
|
|
26
|
-
-> Search API
|
|
27
|
-
|
|
28
|
-
Auth Service -> Payment Gateway: validates | done
|
|
29
|
-
SSO Integration -> Search API: provides identity | todo
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { ParsedInitiativeStatus, InitiativeStatus, ISGroup } from './types';
|
|
2
|
-
import { rollUpStatus } from './layout';
|
|
3
|
-
|
|
4
|
-
// ============================================================
|
|
5
|
-
// CollapseResult — returned by collapseInitiativeStatus
|
|
6
|
-
// ============================================================
|
|
7
|
-
|
|
8
|
-
export interface CollapseResult {
|
|
9
|
-
parsed: ParsedInitiativeStatus;
|
|
10
|
-
collapsedGroupStatuses: Map<string, InitiativeStatus>;
|
|
11
|
-
originalGroups: ISGroup[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ============================================================
|
|
15
|
-
// collapseInitiativeStatus — pure transform
|
|
16
|
-
//
|
|
17
|
-
// Returns a new ParsedInitiativeStatus with:
|
|
18
|
-
// - Children of collapsed groups removed from nodes
|
|
19
|
-
// - Edges redirected: source/target pointing to hidden nodes
|
|
20
|
-
// → point to the group label
|
|
21
|
-
// - Internal edges (both endpoints in same collapsed group) dropped
|
|
22
|
-
// - Duplicate edges (same source, target, label) deduplicated
|
|
23
|
-
// (first occurrence kept)
|
|
24
|
-
// - Collapsed groups removed from groups[] (layout handles them
|
|
25
|
-
// as regular nodes)
|
|
26
|
-
// - collapsedGroupStatuses: worst-case status per collapsed group
|
|
27
|
-
// ============================================================
|
|
28
|
-
|
|
29
|
-
export function collapseInitiativeStatus(
|
|
30
|
-
parsed: ParsedInitiativeStatus,
|
|
31
|
-
collapsedGroups: Set<string>
|
|
32
|
-
): CollapseResult {
|
|
33
|
-
const originalGroups = parsed.groups;
|
|
34
|
-
|
|
35
|
-
if (collapsedGroups.size === 0) {
|
|
36
|
-
return { parsed, collapsedGroupStatuses: new Map(), originalGroups };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Build node → collapsed group lookup
|
|
40
|
-
const nodeToGroup = new Map<string, string>();
|
|
41
|
-
const collapsedGroupStatuses = new Map<string, InitiativeStatus>();
|
|
42
|
-
|
|
43
|
-
for (const group of parsed.groups) {
|
|
44
|
-
if (!collapsedGroups.has(group.label)) continue;
|
|
45
|
-
const children = group.nodeLabels
|
|
46
|
-
.map((l) => parsed.nodes.find((n) => n.label === l))
|
|
47
|
-
.filter((n): n is (typeof parsed.nodes)[0] => n !== undefined);
|
|
48
|
-
for (const node of children) nodeToGroup.set(node.label, group.label);
|
|
49
|
-
collapsedGroupStatuses.set(group.label, rollUpStatus(children));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Filter nodes: remove children of collapsed groups
|
|
53
|
-
const nodes = parsed.nodes.filter((n) => !nodeToGroup.has(n.label));
|
|
54
|
-
|
|
55
|
-
// Remap and deduplicate edges
|
|
56
|
-
const edgeKeys = new Set<string>();
|
|
57
|
-
const edges: typeof parsed.edges = [];
|
|
58
|
-
for (const edge of parsed.edges) {
|
|
59
|
-
const src = nodeToGroup.get(edge.source) ?? edge.source;
|
|
60
|
-
const tgt = nodeToGroup.get(edge.target) ?? edge.target;
|
|
61
|
-
if (src === tgt) continue; // internal edge → drop
|
|
62
|
-
const key = `${src}|${tgt}|${edge.label ?? ''}`;
|
|
63
|
-
if (edgeKeys.has(key)) continue; // duplicate → drop
|
|
64
|
-
edgeKeys.add(key);
|
|
65
|
-
edges.push({ ...edge, source: src, target: tgt });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Keep only expanded groups in groups[]
|
|
69
|
-
const groups = parsed.groups.filter((g) => !collapsedGroups.has(g.label));
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
parsed: { ...parsed, nodes, edges, groups },
|
|
73
|
-
collapsedGroupStatuses,
|
|
74
|
-
originalGroups,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Initiative Status — Tag-Based Filter
|
|
3
|
-
//
|
|
4
|
-
// Immutable graph transform: returns a new ParsedInitiativeStatus
|
|
5
|
-
// with hidden-value nodes removed, their edges dropped,
|
|
6
|
-
// and group.nodeLabels cleaned.
|
|
7
|
-
// ============================================================
|
|
8
|
-
|
|
9
|
-
import type { ParsedInitiativeStatus } from './types';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Filter an initiative-status graph by hiding nodes whose tag metadata
|
|
13
|
-
* matches any hidden value. Returns a new (immutable copy) ParsedInitiativeStatus.
|
|
14
|
-
*
|
|
15
|
-
* @param parsed Fully-resolved parsed result (defaults already injected)
|
|
16
|
-
* @param hiddenTagValues Map<groupKey, Set<hiddenValues>> — all keys/values lowercase
|
|
17
|
-
* @returns Filtered copy; original is not mutated
|
|
18
|
-
*/
|
|
19
|
-
export function filterInitiativeStatusByTags(
|
|
20
|
-
parsed: ParsedInitiativeStatus,
|
|
21
|
-
hiddenTagValues: Map<string, Set<string>>
|
|
22
|
-
): ParsedInitiativeStatus {
|
|
23
|
-
// Fast path: no filtering
|
|
24
|
-
if (hiddenTagValues.size === 0) return parsed;
|
|
25
|
-
|
|
26
|
-
// Build set of hidden node labels
|
|
27
|
-
const hiddenNodeLabels = new Set<string>();
|
|
28
|
-
for (const node of parsed.nodes) {
|
|
29
|
-
for (const [groupKey, hiddenValues] of hiddenTagValues) {
|
|
30
|
-
const nodeValue = node.metadata[groupKey];
|
|
31
|
-
if (nodeValue && hiddenValues.has(nodeValue.toLowerCase())) {
|
|
32
|
-
hiddenNodeLabels.add(node.label);
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// No nodes hidden — return input unchanged
|
|
39
|
-
if (hiddenNodeLabels.size === 0) return parsed;
|
|
40
|
-
|
|
41
|
-
// Filter nodes
|
|
42
|
-
const nodes = parsed.nodes.filter((n) => !hiddenNodeLabels.has(n.label));
|
|
43
|
-
|
|
44
|
-
// Filter edges: remove edges where source OR target is hidden
|
|
45
|
-
const edges = parsed.edges.filter(
|
|
46
|
-
(e) => !hiddenNodeLabels.has(e.source) && !hiddenNodeLabels.has(e.target)
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// Clean group nodeLabels; remove empty groups
|
|
50
|
-
const groups = parsed.groups
|
|
51
|
-
.map((g) => ({
|
|
52
|
-
...g,
|
|
53
|
-
nodeLabels: g.nodeLabels.filter((l) => !hiddenNodeLabels.has(l)),
|
|
54
|
-
}))
|
|
55
|
-
.filter((g) => g.nodeLabels.length > 0);
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
...parsed,
|
|
59
|
-
nodes,
|
|
60
|
-
edges,
|
|
61
|
-
groups,
|
|
62
|
-
};
|
|
63
|
-
}
|