@diagrammo/dgmo 0.4.2 → 0.4.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/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +197 -154
- package/dist/index.cjs +8647 -3447
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -58
- package/dist/index.d.ts +503 -58
- package/dist/index.js +8379 -3200
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +578 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1553 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sharing.ts +8 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
package/src/graph/layout.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface LayoutGroup {
|
|
|
32
32
|
id: string;
|
|
33
33
|
label: string;
|
|
34
34
|
color?: string;
|
|
35
|
+
lineNumber: number;
|
|
35
36
|
x: number;
|
|
36
37
|
y: number;
|
|
37
38
|
width: number;
|
|
@@ -49,12 +50,14 @@ export interface LayoutResult {
|
|
|
49
50
|
const GROUP_PADDING = 20;
|
|
50
51
|
|
|
51
52
|
function computeNodeWidth(label: string, shape: GraphShape): number {
|
|
53
|
+
if (shape === 'pseudostate') return 24;
|
|
52
54
|
const base = Math.max(120, label.length * 9 + 40);
|
|
53
55
|
if (shape === 'subroutine') return base + 10;
|
|
54
56
|
return base;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
function computeNodeHeight(shape: GraphShape): number {
|
|
60
|
+
if (shape === 'pseudostate') return 24;
|
|
58
61
|
return shape === 'decision' ? 60 : 50;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -157,6 +160,7 @@ export function layoutGraph(graph: ParsedGraph): LayoutResult {
|
|
|
157
160
|
id: group.id,
|
|
158
161
|
label: group.label,
|
|
159
162
|
color: group.color,
|
|
163
|
+
lineNumber: group.lineNumber,
|
|
160
164
|
x: 0,
|
|
161
165
|
y: 0,
|
|
162
166
|
width: 0,
|
|
@@ -185,6 +189,7 @@ export function layoutGraph(graph: ParsedGraph): LayoutResult {
|
|
|
185
189
|
id: group.id,
|
|
186
190
|
label: group.label,
|
|
187
191
|
color: group.color,
|
|
192
|
+
lineNumber: group.lineNumber,
|
|
188
193
|
x: minX - GROUP_PADDING,
|
|
189
194
|
y: minY - GROUP_PADDING,
|
|
190
195
|
width: maxX - minX + GROUP_PADDING * 2,
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { resolveColor } from '../colors';
|
|
2
|
+
import type { PaletteColors } from '../palettes';
|
|
3
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
+
import { measureIndent, extractColor } from '../utils/parsing';
|
|
5
|
+
import type {
|
|
6
|
+
ParsedGraph,
|
|
7
|
+
GraphNode,
|
|
8
|
+
GraphEdge,
|
|
9
|
+
GraphGroup,
|
|
10
|
+
GraphDirection,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// Constants
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
const PSEUDOSTATE_ID = 'pseudostate:[*]';
|
|
18
|
+
const PSEUDOSTATE_LABEL = '[*]';
|
|
19
|
+
|
|
20
|
+
const GROUP_BRACKET_RE = /^\[([^\]]+)\](?:\(([^)]+)\))?\s*$/;
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// Arrow splitter
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Split a line on `->` arrows, returning alternating segments:
|
|
28
|
+
* [nodeText, arrowToken, nodeText, ...]
|
|
29
|
+
*
|
|
30
|
+
* Arrows: `->`, `-label->`, `-(color)->`, `-label(color)->`
|
|
31
|
+
*/
|
|
32
|
+
function splitArrows(line: string): string[] {
|
|
33
|
+
const segments: string[] = [];
|
|
34
|
+
const arrowPositions: { start: number; end: number; label?: string; color?: string }[] = [];
|
|
35
|
+
|
|
36
|
+
let searchFrom = 0;
|
|
37
|
+
while (searchFrom < line.length) {
|
|
38
|
+
const idx = line.indexOf('->', searchFrom);
|
|
39
|
+
if (idx === -1) break;
|
|
40
|
+
|
|
41
|
+
let arrowStart = idx;
|
|
42
|
+
let label: string | undefined;
|
|
43
|
+
let color: string | undefined;
|
|
44
|
+
|
|
45
|
+
if (idx > 0 && line[idx - 1] !== ' ' && line[idx - 1] !== '\t') {
|
|
46
|
+
let scanBack = idx - 1;
|
|
47
|
+
while (scanBack > 0 && line[scanBack] !== '-') {
|
|
48
|
+
scanBack--;
|
|
49
|
+
}
|
|
50
|
+
if (line[scanBack] === '-' && (scanBack === 0 || /\s/.test(line[scanBack - 1]))) {
|
|
51
|
+
let arrowContent = line.substring(scanBack + 1, idx);
|
|
52
|
+
if (arrowContent.endsWith('-')) arrowContent = arrowContent.slice(0, -1);
|
|
53
|
+
const colorMatch = arrowContent.match(/\(([^)]+)\)\s*$/);
|
|
54
|
+
if (colorMatch) {
|
|
55
|
+
color = colorMatch[1].trim();
|
|
56
|
+
const labelPart = arrowContent.substring(0, colorMatch.index!).trim();
|
|
57
|
+
if (labelPart) label = labelPart;
|
|
58
|
+
} else {
|
|
59
|
+
const labelPart = arrowContent.trim();
|
|
60
|
+
if (labelPart) label = labelPart;
|
|
61
|
+
}
|
|
62
|
+
arrowStart = scanBack;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
arrowPositions.push({ start: arrowStart, end: idx + 2, label, color });
|
|
67
|
+
searchFrom = idx + 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arrowPositions.length === 0) return [line];
|
|
71
|
+
|
|
72
|
+
let lastIndex = 0;
|
|
73
|
+
for (let i = 0; i < arrowPositions.length; i++) {
|
|
74
|
+
const arrow = arrowPositions[i];
|
|
75
|
+
const beforeText = line.substring(lastIndex, arrow.start).trim();
|
|
76
|
+
if (beforeText || i === 0) segments.push(beforeText);
|
|
77
|
+
|
|
78
|
+
let arrowToken = '->';
|
|
79
|
+
if (arrow.label && arrow.color) arrowToken = `-${arrow.label}(${arrow.color})->`;
|
|
80
|
+
else if (arrow.label) arrowToken = `-${arrow.label}->`;
|
|
81
|
+
else if (arrow.color) arrowToken = `-(${arrow.color})->`;
|
|
82
|
+
segments.push(arrowToken);
|
|
83
|
+
lastIndex = arrow.end;
|
|
84
|
+
}
|
|
85
|
+
const remaining = line.substring(lastIndex).trim();
|
|
86
|
+
if (remaining) segments.push(remaining);
|
|
87
|
+
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ArrowInfo {
|
|
92
|
+
label?: string;
|
|
93
|
+
color?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseArrowToken(token: string, palette?: PaletteColors): ArrowInfo {
|
|
97
|
+
if (token === '->') return {};
|
|
98
|
+
const colorOnly = token.match(/^-\(([^)]+)\)->$/);
|
|
99
|
+
if (colorOnly) return { color: resolveColor(colorOnly[1].trim(), palette) };
|
|
100
|
+
const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
|
|
101
|
+
if (m) {
|
|
102
|
+
const label = m[1]?.trim() || undefined;
|
|
103
|
+
const color = m[2] ? resolveColor(m[2].trim(), palette) : undefined;
|
|
104
|
+
return { label, color };
|
|
105
|
+
}
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// Node ref parser
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
interface NodeRef {
|
|
114
|
+
id: string;
|
|
115
|
+
label: string;
|
|
116
|
+
shape: 'state' | 'pseudostate';
|
|
117
|
+
color?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseStateNodeRef(text: string, palette?: PaletteColors): NodeRef | null {
|
|
121
|
+
const t = text.trim();
|
|
122
|
+
if (!t) return null;
|
|
123
|
+
|
|
124
|
+
// Pseudostate: [*]
|
|
125
|
+
if (t === '[*]') {
|
|
126
|
+
return { id: PSEUDOSTATE_ID, label: PSEUDOSTATE_LABEL, shape: 'pseudostate' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// State: bare text with optional (color) suffix
|
|
130
|
+
const { label, color } = extractColor(t, palette);
|
|
131
|
+
if (!label) return null;
|
|
132
|
+
return {
|
|
133
|
+
id: `state:${label.toLowerCase().trim()}`,
|
|
134
|
+
label,
|
|
135
|
+
shape: 'state',
|
|
136
|
+
color,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================
|
|
141
|
+
// Main parser
|
|
142
|
+
// ============================================================
|
|
143
|
+
|
|
144
|
+
export function parseState(
|
|
145
|
+
content: string,
|
|
146
|
+
palette?: PaletteColors
|
|
147
|
+
): ParsedGraph {
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
const result: ParsedGraph = {
|
|
150
|
+
type: 'state',
|
|
151
|
+
direction: 'TB',
|
|
152
|
+
nodes: [],
|
|
153
|
+
edges: [],
|
|
154
|
+
options: {},
|
|
155
|
+
diagnostics: [],
|
|
156
|
+
error: null,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const fail = (line: number, message: string): ParsedGraph => {
|
|
160
|
+
const diag = makeDgmoError(line, message);
|
|
161
|
+
result.diagnostics.push(diag);
|
|
162
|
+
result.error = formatDgmoError(diag);
|
|
163
|
+
return result;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const nodeMap = new Map<string, GraphNode>();
|
|
167
|
+
const indentStack: { nodeId: string; indent: number }[] = [];
|
|
168
|
+
let currentGroup: GraphGroup | null = null;
|
|
169
|
+
let groupIndent = -1;
|
|
170
|
+
const groups: GraphGroup[] = [];
|
|
171
|
+
let contentStarted = false;
|
|
172
|
+
|
|
173
|
+
function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
|
|
174
|
+
const existing = nodeMap.get(ref.id);
|
|
175
|
+
if (existing) return existing;
|
|
176
|
+
|
|
177
|
+
const node: GraphNode = {
|
|
178
|
+
id: ref.id,
|
|
179
|
+
label: ref.label,
|
|
180
|
+
shape: ref.shape,
|
|
181
|
+
lineNumber,
|
|
182
|
+
...(ref.color && { color: ref.color }),
|
|
183
|
+
...(currentGroup && { group: currentGroup.id }),
|
|
184
|
+
};
|
|
185
|
+
nodeMap.set(ref.id, node);
|
|
186
|
+
result.nodes.push(node);
|
|
187
|
+
|
|
188
|
+
if (currentGroup && !currentGroup.nodeIds.includes(ref.id)) {
|
|
189
|
+
currentGroup.nodeIds.push(ref.id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return node;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function addEdge(
|
|
196
|
+
sourceId: string,
|
|
197
|
+
targetId: string,
|
|
198
|
+
lineNumber: number,
|
|
199
|
+
label?: string,
|
|
200
|
+
color?: string
|
|
201
|
+
): void {
|
|
202
|
+
result.edges.push({
|
|
203
|
+
source: sourceId,
|
|
204
|
+
target: targetId,
|
|
205
|
+
lineNumber,
|
|
206
|
+
...(label && { label }),
|
|
207
|
+
...(color && { color }),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// === Main loop ===
|
|
212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
213
|
+
const raw = lines[i];
|
|
214
|
+
const trimmed = raw.trim();
|
|
215
|
+
const lineNumber = i + 1;
|
|
216
|
+
const indent = measureIndent(raw);
|
|
217
|
+
|
|
218
|
+
if (!trimmed) continue;
|
|
219
|
+
if (trimmed.startsWith('//')) continue;
|
|
220
|
+
|
|
221
|
+
// Group brackets: [Name] or [Name](color)
|
|
222
|
+
const groupMatch = trimmed.match(GROUP_BRACKET_RE);
|
|
223
|
+
if (groupMatch && groupMatch[1].trim() !== '*') {
|
|
224
|
+
const groupLabel = groupMatch[1].trim();
|
|
225
|
+
const groupColorName = groupMatch[2]?.trim();
|
|
226
|
+
const groupColor = groupColorName
|
|
227
|
+
? resolveColor(groupColorName, palette)
|
|
228
|
+
: undefined;
|
|
229
|
+
|
|
230
|
+
currentGroup = {
|
|
231
|
+
id: `group:${groupLabel.toLowerCase()}`,
|
|
232
|
+
label: groupLabel,
|
|
233
|
+
nodeIds: [],
|
|
234
|
+
lineNumber,
|
|
235
|
+
...(groupColor && { color: groupColor }),
|
|
236
|
+
};
|
|
237
|
+
groupIndent = indent;
|
|
238
|
+
groups.push(currentGroup);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Metadata directives (before content)
|
|
243
|
+
if (!contentStarted && trimmed.includes(':') && !trimmed.includes('->')) {
|
|
244
|
+
const colonIdx = trimmed.indexOf(':');
|
|
245
|
+
const key = trimmed.substring(0, colonIdx).trim().toLowerCase();
|
|
246
|
+
const value = trimmed.substring(colonIdx + 1).trim();
|
|
247
|
+
|
|
248
|
+
if (key === 'chart') {
|
|
249
|
+
if (value.toLowerCase() !== 'state') {
|
|
250
|
+
const allTypes = ['state', 'flowchart', 'sequence', 'class', 'er', 'org', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
|
|
251
|
+
let msg = `Expected chart type "state", got "${value}"`;
|
|
252
|
+
const hint = suggest(value.toLowerCase(), allTypes);
|
|
253
|
+
if (hint) msg += `. ${hint}`;
|
|
254
|
+
return fail(lineNumber, msg);
|
|
255
|
+
}
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (key === 'title') {
|
|
260
|
+
result.title = value;
|
|
261
|
+
result.titleLineNumber = lineNumber;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (key === 'direction') {
|
|
266
|
+
const dir = value.toUpperCase() as GraphDirection;
|
|
267
|
+
if (dir === 'TB' || dir === 'LR') {
|
|
268
|
+
result.direction = dir;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
result.options[key] = value;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Content line — nodes and edges
|
|
278
|
+
contentStarted = true;
|
|
279
|
+
|
|
280
|
+
// Close current group when indent returns to or below the bracket level
|
|
281
|
+
if (currentGroup && indent <= groupIndent) {
|
|
282
|
+
currentGroup = null;
|
|
283
|
+
groupIndent = -1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Pop indent stack entries at same or deeper indent
|
|
287
|
+
while (indentStack.length > 0) {
|
|
288
|
+
const top = indentStack[indentStack.length - 1];
|
|
289
|
+
if (top.indent >= indent) {
|
|
290
|
+
indentStack.pop();
|
|
291
|
+
} else {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const implicitSourceId =
|
|
297
|
+
indentStack.length > 0
|
|
298
|
+
? indentStack[indentStack.length - 1].nodeId
|
|
299
|
+
: null;
|
|
300
|
+
|
|
301
|
+
const segments = splitArrows(trimmed);
|
|
302
|
+
|
|
303
|
+
if (segments.length === 1) {
|
|
304
|
+
// Single state reference, no arrows — this is the canonical definition
|
|
305
|
+
const ref = parseStateNodeRef(segments[0], palette);
|
|
306
|
+
if (ref) {
|
|
307
|
+
const node = getOrCreateNode(ref, lineNumber);
|
|
308
|
+
// Standalone heading is the "definition" — update lineNumber so
|
|
309
|
+
// clicking the node in the preview navigates here, not to the
|
|
310
|
+
// first edge mention.
|
|
311
|
+
node.lineNumber = lineNumber;
|
|
312
|
+
indentStack.push({ nodeId: node.id, indent });
|
|
313
|
+
}
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Process chain: alternating nodeText / arrowToken / nodeText / ...
|
|
318
|
+
let lastNodeId: string | null = null;
|
|
319
|
+
let pendingArrow: ArrowInfo | null = null;
|
|
320
|
+
|
|
321
|
+
for (let j = 0; j < segments.length; j++) {
|
|
322
|
+
const seg = segments[j];
|
|
323
|
+
|
|
324
|
+
if (seg === '->' || /^-.+->$/.test(seg)) {
|
|
325
|
+
pendingArrow = parseArrowToken(seg, palette);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const ref = parseStateNodeRef(seg, palette);
|
|
330
|
+
if (!ref) continue;
|
|
331
|
+
|
|
332
|
+
const node = getOrCreateNode(ref, lineNumber);
|
|
333
|
+
|
|
334
|
+
if (pendingArrow !== null) {
|
|
335
|
+
// Use explicit source if available, else implicit from indent
|
|
336
|
+
const sourceId = lastNodeId ?? implicitSourceId;
|
|
337
|
+
if (sourceId) {
|
|
338
|
+
addEdge(sourceId, node.id, lineNumber, pendingArrow.label, pendingArrow.color);
|
|
339
|
+
}
|
|
340
|
+
pendingArrow = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
lastNodeId = node.id;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (lastNodeId) {
|
|
347
|
+
indentStack.push({ nodeId: lastNodeId, indent });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (groups.length > 0) result.groups = groups;
|
|
352
|
+
|
|
353
|
+
// Validation: no nodes found
|
|
354
|
+
if (result.nodes.length === 0 && !result.error) {
|
|
355
|
+
const diag = makeDgmoError(1, 'No states found. Add state transitions like: Idle -> Active');
|
|
356
|
+
result.diagnostics.push(diag);
|
|
357
|
+
result.error = formatDgmoError(diag);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Warn about orphaned states
|
|
361
|
+
if (result.nodes.length >= 2 && result.edges.length >= 1 && !result.error) {
|
|
362
|
+
const connectedIds = new Set<string>();
|
|
363
|
+
for (const edge of result.edges) {
|
|
364
|
+
connectedIds.add(edge.source);
|
|
365
|
+
connectedIds.add(edge.target);
|
|
366
|
+
}
|
|
367
|
+
for (const node of result.nodes) {
|
|
368
|
+
if (!connectedIds.has(node.id)) {
|
|
369
|
+
result.diagnostics.push(makeDgmoError(node.lineNumber, `State "${node.label}" is not connected to any other state`, 'warning'));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================
|
|
378
|
+
// Detection helper
|
|
379
|
+
// ============================================================
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Detect if content looks like a state diagram (without explicit `chart: state` header).
|
|
383
|
+
* Only matches if `[*]` token is present — too ambiguous to infer from bare names alone.
|
|
384
|
+
*/
|
|
385
|
+
export function looksLikeState(content: string): boolean {
|
|
386
|
+
// Must have [*] token (start/end pseudostate) and -> arrows
|
|
387
|
+
return content.includes('[*]') && content.includes('->');
|
|
388
|
+
}
|