@diagrammo/dgmo 0.2.21 → 0.2.23
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 +119 -113
- package/dist/index.cjs +6317 -2337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -1
- package/dist/index.d.ts +264 -1
- package/dist/index.js +6299 -2337
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/layout.ts +2137 -0
- package/src/c4/parser.ts +809 -0
- package/src/c4/renderer.ts +1916 -0
- package/src/c4/types.ts +86 -0
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +54 -10
- package/src/d3.ts +148 -10
- package/src/dgmo-router.ts +13 -0
- package/src/echarts.ts +7 -8
- package/src/er/renderer.ts +2 -2
- package/src/graph/flowchart-renderer.ts +1 -1
- package/src/index.ts +54 -0
- package/src/initiative-status/layout.ts +217 -0
- package/src/initiative-status/parser.ts +246 -0
- package/src/initiative-status/renderer.ts +834 -0
- package/src/initiative-status/types.ts +43 -0
- package/src/kanban/renderer.ts +23 -3
- package/src/org/layout.ts +64 -26
- package/src/org/renderer.ts +47 -18
- package/src/org/resolver.ts +3 -1
- package/src/render.ts +9 -1
- package/src/sequence/participant-inference.ts +1 -0
- package/src/sequence/renderer.ts +12 -6
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Initiative Status Diagram — Layout (dagre-based)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import dagre from '@dagrejs/dagre';
|
|
6
|
+
import type { ParsedInitiativeStatus, ISEdge, InitiativeStatus } from './types';
|
|
7
|
+
|
|
8
|
+
export interface ISLayoutNode {
|
|
9
|
+
label: string;
|
|
10
|
+
status: import('./types').InitiativeStatus;
|
|
11
|
+
shape: import('../sequence/parser').ParticipantType;
|
|
12
|
+
lineNumber: number;
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ISLayoutEdge {
|
|
20
|
+
source: string;
|
|
21
|
+
target: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
status: import('./types').InitiativeStatus;
|
|
24
|
+
lineNumber: number;
|
|
25
|
+
points: { x: number; y: number }[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ISLayoutGroup {
|
|
29
|
+
label: string;
|
|
30
|
+
status: InitiativeStatus;
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
lineNumber: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ISLayoutResult {
|
|
39
|
+
nodes: ISLayoutNode[];
|
|
40
|
+
edges: ISLayoutEdge[];
|
|
41
|
+
groups: ISLayoutGroup[];
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Roll up child statuses: worst (least-progressed) wins
|
|
47
|
+
// Priority: todo > wip > done > na > null
|
|
48
|
+
const STATUS_PRIORITY: Record<string, number> = { todo: 3, wip: 2, done: 1, na: 0 };
|
|
49
|
+
|
|
50
|
+
function rollUpStatus(members: ISLayoutNode[]): InitiativeStatus {
|
|
51
|
+
let worst: InitiativeStatus = null;
|
|
52
|
+
let worstPri = -1;
|
|
53
|
+
for (const m of members) {
|
|
54
|
+
const pri = m.status ? (STATUS_PRIORITY[m.status] ?? -1) : -1;
|
|
55
|
+
if (pri > worstPri) {
|
|
56
|
+
worstPri = pri;
|
|
57
|
+
worst = m.status;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return worst;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Golden ratio fixed-size nodes — all boxes are identical dimensions
|
|
64
|
+
const PHI = 1.618;
|
|
65
|
+
const NODE_HEIGHT = 60;
|
|
66
|
+
const NODE_WIDTH = Math.round(NODE_HEIGHT * PHI); // ~97
|
|
67
|
+
const GROUP_PADDING = 20;
|
|
68
|
+
|
|
69
|
+
export function layoutInitiativeStatus(parsed: ParsedInitiativeStatus): ISLayoutResult {
|
|
70
|
+
if (parsed.nodes.length === 0) {
|
|
71
|
+
return { nodes: [], edges: [], groups: [], width: 0, height: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasGroups = parsed.groups.length > 0;
|
|
75
|
+
const g = new dagre.graphlib.Graph({ multigraph: true, compound: hasGroups });
|
|
76
|
+
g.setGraph({
|
|
77
|
+
rankdir: 'LR',
|
|
78
|
+
nodesep: 80,
|
|
79
|
+
ranksep: 160,
|
|
80
|
+
edgesep: 40,
|
|
81
|
+
});
|
|
82
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
83
|
+
|
|
84
|
+
// Add group parent nodes
|
|
85
|
+
for (const group of parsed.groups) {
|
|
86
|
+
const groupId = `__group_${group.label}`;
|
|
87
|
+
g.setNode(groupId, { label: group.label, clusterLabelPos: 'top' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add nodes — all same size (golden ratio)
|
|
91
|
+
for (const node of parsed.nodes) {
|
|
92
|
+
g.setNode(node.label, { label: node.label, width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Assign children to group parents
|
|
96
|
+
for (const group of parsed.groups) {
|
|
97
|
+
const groupId = `__group_${group.label}`;
|
|
98
|
+
for (const nodeLabel of group.nodeLabels) {
|
|
99
|
+
if (g.hasNode(nodeLabel)) {
|
|
100
|
+
g.setParent(nodeLabel, groupId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add edges — use multigraph names to allow duplicates between same pair
|
|
106
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
107
|
+
const edge = parsed.edges[i];
|
|
108
|
+
g.setEdge(edge.source, edge.target, { label: edge.label ?? '' }, `e${i}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Compute layout
|
|
112
|
+
dagre.layout(g);
|
|
113
|
+
|
|
114
|
+
// Extract positioned nodes — dagre owns within-rank ordering
|
|
115
|
+
// (crossing minimization). We don't reorder post-layout because
|
|
116
|
+
// that would desync edge waypoints from node positions.
|
|
117
|
+
const layoutNodes: ISLayoutNode[] = parsed.nodes.map((node) => {
|
|
118
|
+
const pos = g.node(node.label);
|
|
119
|
+
return {
|
|
120
|
+
label: node.label,
|
|
121
|
+
status: node.status,
|
|
122
|
+
shape: node.shape,
|
|
123
|
+
lineNumber: node.lineNumber,
|
|
124
|
+
x: pos.x,
|
|
125
|
+
y: pos.y,
|
|
126
|
+
width: pos.width,
|
|
127
|
+
height: pos.height,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Extract edge waypoints
|
|
132
|
+
const layoutEdges: ISLayoutEdge[] = parsed.edges.map((edge, i) => {
|
|
133
|
+
const edgeData = g.edge(edge.source, edge.target, `e${i}`);
|
|
134
|
+
return {
|
|
135
|
+
source: edge.source,
|
|
136
|
+
target: edge.target,
|
|
137
|
+
label: edge.label,
|
|
138
|
+
status: edge.status,
|
|
139
|
+
lineNumber: edge.lineNumber,
|
|
140
|
+
points: edgeData?.points ?? [],
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Compute group bounding boxes from member positions
|
|
145
|
+
const layoutGroups: ISLayoutGroup[] = [];
|
|
146
|
+
if (parsed.groups.length > 0) {
|
|
147
|
+
const nodeMap = new Map(layoutNodes.map((n) => [n.label, n]));
|
|
148
|
+
for (const group of parsed.groups) {
|
|
149
|
+
const members = group.nodeLabels
|
|
150
|
+
.map((label) => nodeMap.get(label))
|
|
151
|
+
.filter((n): n is ISLayoutNode => n !== undefined);
|
|
152
|
+
|
|
153
|
+
if (members.length === 0) continue;
|
|
154
|
+
|
|
155
|
+
let minX = Infinity;
|
|
156
|
+
let minY = Infinity;
|
|
157
|
+
let maxX = -Infinity;
|
|
158
|
+
let maxY = -Infinity;
|
|
159
|
+
|
|
160
|
+
for (const member of members) {
|
|
161
|
+
const left = member.x - member.width / 2;
|
|
162
|
+
const right = member.x + member.width / 2;
|
|
163
|
+
const top = member.y - member.height / 2;
|
|
164
|
+
const bottom = member.y + member.height / 2;
|
|
165
|
+
if (left < minX) minX = left;
|
|
166
|
+
if (right > maxX) maxX = right;
|
|
167
|
+
if (top < minY) minY = top;
|
|
168
|
+
if (bottom > maxY) maxY = bottom;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
layoutGroups.push({
|
|
172
|
+
label: group.label,
|
|
173
|
+
status: rollUpStatus(members),
|
|
174
|
+
x: minX - GROUP_PADDING,
|
|
175
|
+
y: minY - GROUP_PADDING,
|
|
176
|
+
width: maxX - minX + GROUP_PADDING * 2,
|
|
177
|
+
height: maxY - minY + GROUP_PADDING * 2,
|
|
178
|
+
lineNumber: group.lineNumber,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Compute total dimensions
|
|
184
|
+
let totalWidth = 0;
|
|
185
|
+
let totalHeight = 0;
|
|
186
|
+
for (const node of layoutNodes) {
|
|
187
|
+
const right = node.x + node.width / 2;
|
|
188
|
+
const bottom = node.y + node.height / 2;
|
|
189
|
+
if (right > totalWidth) totalWidth = right;
|
|
190
|
+
if (bottom > totalHeight) totalHeight = bottom;
|
|
191
|
+
}
|
|
192
|
+
// Also consider group bounds
|
|
193
|
+
for (const group of layoutGroups) {
|
|
194
|
+
const right = group.x + group.width;
|
|
195
|
+
const bottom = group.y + group.height;
|
|
196
|
+
if (right > totalWidth) totalWidth = right;
|
|
197
|
+
if (bottom > totalHeight) totalHeight = bottom;
|
|
198
|
+
}
|
|
199
|
+
// Also consider edge control points
|
|
200
|
+
for (const edge of layoutEdges) {
|
|
201
|
+
for (const pt of edge.points) {
|
|
202
|
+
if (pt.x > totalWidth) totalWidth = pt.x;
|
|
203
|
+
if (pt.y > totalHeight) totalHeight = pt.y;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Add margin
|
|
207
|
+
totalWidth += 40;
|
|
208
|
+
totalHeight += 40;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
nodes: layoutNodes,
|
|
212
|
+
edges: layoutEdges,
|
|
213
|
+
groups: layoutGroups,
|
|
214
|
+
width: totalWidth,
|
|
215
|
+
height: totalHeight,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Initiative Status Diagram — Parser
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
6
|
+
import type { DgmoError } from '../diagnostics';
|
|
7
|
+
import type {
|
|
8
|
+
ParsedInitiativeStatus,
|
|
9
|
+
ISNode,
|
|
10
|
+
ISEdge,
|
|
11
|
+
ISGroup,
|
|
12
|
+
InitiativeStatus,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { VALID_STATUSES } from './types';
|
|
15
|
+
import { inferParticipantType } from '../sequence/participant-inference';
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// Heuristic — does this content look like an initiative-status diagram?
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if the content looks like an initiative-status diagram.
|
|
23
|
+
* Detects `->` arrows combined with `| done/wip/todo/na` status markers.
|
|
24
|
+
*/
|
|
25
|
+
export function looksLikeInitiativeStatus(content: string): boolean {
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
let hasArrow = false;
|
|
28
|
+
let hasStatus = false;
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
|
|
32
|
+
if (trimmed.match(/^chart\s*:/i)) continue;
|
|
33
|
+
if (trimmed.match(/^title\s*:/i)) continue;
|
|
34
|
+
if (trimmed.includes('->')) hasArrow = true;
|
|
35
|
+
if (/\|\s*(done|wip|todo|na)\s*$/i.test(trimmed)) hasStatus = true;
|
|
36
|
+
if (hasArrow && hasStatus) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Parser
|
|
43
|
+
// ============================================================
|
|
44
|
+
|
|
45
|
+
function parseStatus(raw: string, line: number, diagnostics: DgmoError[]): InitiativeStatus {
|
|
46
|
+
const trimmed = raw.trim().toLowerCase();
|
|
47
|
+
if (!trimmed) return null;
|
|
48
|
+
if (VALID_STATUSES.includes(trimmed)) return trimmed as InitiativeStatus;
|
|
49
|
+
|
|
50
|
+
// Unknown status — emit warning with suggestion
|
|
51
|
+
const hint = suggest(trimmed, VALID_STATUSES);
|
|
52
|
+
const msg = `Unknown status "${raw.trim()}"${hint ? `. ${hint}` : ''}`;
|
|
53
|
+
diagnostics.push(makeDgmoError(line, msg, 'warning'));
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
58
|
+
const result: ParsedInitiativeStatus = {
|
|
59
|
+
type: 'initiative-status',
|
|
60
|
+
title: null,
|
|
61
|
+
titleLineNumber: null,
|
|
62
|
+
nodes: [],
|
|
63
|
+
edges: [],
|
|
64
|
+
groups: [],
|
|
65
|
+
options: {},
|
|
66
|
+
diagnostics: [],
|
|
67
|
+
error: undefined,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const lines = content.split('\n');
|
|
71
|
+
const nodeLabels = new Set<string>();
|
|
72
|
+
let currentGroup: ISGroup | null = null;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const lineNum = i + 1; // 1-based
|
|
76
|
+
const raw = lines[i];
|
|
77
|
+
const trimmed = raw.trim();
|
|
78
|
+
|
|
79
|
+
// Skip blanks and comments
|
|
80
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
|
|
81
|
+
|
|
82
|
+
// chart: header
|
|
83
|
+
const chartMatch = trimmed.match(/^chart\s*:\s*(.+)/i);
|
|
84
|
+
if (chartMatch) {
|
|
85
|
+
const chartType = chartMatch[1].trim().toLowerCase();
|
|
86
|
+
if (chartType !== 'initiative-status') {
|
|
87
|
+
const diag = makeDgmoError(lineNum, `Expected chart type "initiative-status", got "${chartType}"`);
|
|
88
|
+
result.diagnostics.push(diag);
|
|
89
|
+
result.error = formatDgmoError(diag);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// title: header
|
|
96
|
+
const titleMatch = trimmed.match(/^title\s*:\s*(.+)/i);
|
|
97
|
+
if (titleMatch) {
|
|
98
|
+
result.title = titleMatch[1].trim();
|
|
99
|
+
result.titleLineNumber = lineNum;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Group header: [Group Name]
|
|
104
|
+
const groupMatch = trimmed.match(/^\[(.+)\]\s*$/);
|
|
105
|
+
if (groupMatch) {
|
|
106
|
+
// Close previous group
|
|
107
|
+
if (currentGroup) {
|
|
108
|
+
result.groups.push(currentGroup);
|
|
109
|
+
}
|
|
110
|
+
currentGroup = { label: groupMatch[1], nodeLabels: [], lineNumber: lineNum };
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Non-indented line closes the current group
|
|
115
|
+
const isIndented = raw.length > 0 && raw !== trimmed && /^\s/.test(raw);
|
|
116
|
+
if (!isIndented && currentGroup) {
|
|
117
|
+
result.groups.push(currentGroup);
|
|
118
|
+
currentGroup = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Edge: contains `->`
|
|
122
|
+
if (trimmed.includes('->')) {
|
|
123
|
+
const edge = parseEdgeLine(trimmed, lineNum, result.diagnostics);
|
|
124
|
+
if (edge) result.edges.push(edge);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Node: everything else
|
|
129
|
+
const node = parseNodeLine(trimmed, lineNum, result.diagnostics);
|
|
130
|
+
if (node) {
|
|
131
|
+
if (nodeLabels.has(node.label)) {
|
|
132
|
+
result.diagnostics.push(
|
|
133
|
+
makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
nodeLabels.add(node.label);
|
|
137
|
+
}
|
|
138
|
+
result.nodes.push(node);
|
|
139
|
+
// Add to current group if indented
|
|
140
|
+
if (currentGroup && isIndented) {
|
|
141
|
+
currentGroup.nodeLabels.push(node.label);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Close any trailing group
|
|
147
|
+
if (currentGroup) {
|
|
148
|
+
result.groups.push(currentGroup);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate edges reference declared nodes
|
|
152
|
+
for (const edge of result.edges) {
|
|
153
|
+
if (!nodeLabels.has(edge.source)) {
|
|
154
|
+
result.diagnostics.push(
|
|
155
|
+
makeDgmoError(edge.lineNumber, `Edge source "${edge.source}" is not a declared node`, 'warning')
|
|
156
|
+
);
|
|
157
|
+
// Auto-create an implicit node
|
|
158
|
+
if (!result.nodes.some((n) => n.label === edge.source)) {
|
|
159
|
+
result.nodes.push({ label: edge.source, status: null, shape: inferParticipantType(edge.source), lineNumber: edge.lineNumber });
|
|
160
|
+
nodeLabels.add(edge.source);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!nodeLabels.has(edge.target)) {
|
|
164
|
+
result.diagnostics.push(
|
|
165
|
+
makeDgmoError(edge.lineNumber, `Edge target "${edge.target}" is not a declared node`, 'warning')
|
|
166
|
+
);
|
|
167
|
+
if (!result.nodes.some((n) => n.label === edge.target)) {
|
|
168
|
+
result.nodes.push({ label: edge.target, status: null, shape: inferParticipantType(edge.target), lineNumber: edge.lineNumber });
|
|
169
|
+
nodeLabels.add(edge.target);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================
|
|
178
|
+
// Line parsers
|
|
179
|
+
// ============================================================
|
|
180
|
+
|
|
181
|
+
function parseNodeLine(
|
|
182
|
+
trimmed: string,
|
|
183
|
+
lineNum: number,
|
|
184
|
+
diagnostics: DgmoError[]
|
|
185
|
+
): ISNode | null {
|
|
186
|
+
// Format: <label> | <status>
|
|
187
|
+
// or just: <label>
|
|
188
|
+
const pipeIdx = trimmed.lastIndexOf('|');
|
|
189
|
+
if (pipeIdx >= 0) {
|
|
190
|
+
const label = trimmed.slice(0, pipeIdx).trim();
|
|
191
|
+
const statusRaw = trimmed.slice(pipeIdx + 1).trim();
|
|
192
|
+
if (!label) return null;
|
|
193
|
+
const status = parseStatus(statusRaw, lineNum, diagnostics);
|
|
194
|
+
return { label, status, shape: inferParticipantType(label), lineNumber: lineNum };
|
|
195
|
+
}
|
|
196
|
+
return { label: trimmed, status: null, shape: inferParticipantType(trimmed), lineNumber: lineNum };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseEdgeLine(
|
|
200
|
+
trimmed: string,
|
|
201
|
+
lineNum: number,
|
|
202
|
+
diagnostics: DgmoError[]
|
|
203
|
+
): ISEdge | null {
|
|
204
|
+
// Format: <source> -> <target>: <label> | <status>
|
|
205
|
+
// or: <source> -> <target> | <status>
|
|
206
|
+
// or: <source> -> <target>: <label>
|
|
207
|
+
// or: <source> -> <target>
|
|
208
|
+
|
|
209
|
+
const arrowIdx = trimmed.indexOf('->');
|
|
210
|
+
if (arrowIdx < 0) return null;
|
|
211
|
+
|
|
212
|
+
const source = trimmed.slice(0, arrowIdx).trim();
|
|
213
|
+
let rest = trimmed.slice(arrowIdx + 2).trim();
|
|
214
|
+
|
|
215
|
+
if (!source || !rest) {
|
|
216
|
+
diagnostics.push(makeDgmoError(lineNum, 'Edge is missing source or target'));
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Extract status from end (after last |)
|
|
221
|
+
let status: InitiativeStatus = null;
|
|
222
|
+
const lastPipe = rest.lastIndexOf('|');
|
|
223
|
+
if (lastPipe >= 0) {
|
|
224
|
+
const statusRaw = rest.slice(lastPipe + 1).trim();
|
|
225
|
+
status = parseStatus(statusRaw, lineNum, diagnostics);
|
|
226
|
+
rest = rest.slice(0, lastPipe).trim();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Extract target and optional label (target: label)
|
|
230
|
+
let target: string;
|
|
231
|
+
let label: string | undefined;
|
|
232
|
+
const colonIdx = rest.indexOf(':');
|
|
233
|
+
if (colonIdx >= 0) {
|
|
234
|
+
target = rest.slice(0, colonIdx).trim();
|
|
235
|
+
label = rest.slice(colonIdx + 1).trim() || undefined;
|
|
236
|
+
} else {
|
|
237
|
+
target = rest.trim();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!target) {
|
|
241
|
+
diagnostics.push(makeDgmoError(lineNum, 'Edge is missing target'));
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { source, target, label, status, lineNumber: lineNum };
|
|
246
|
+
}
|