@diagrammo/dgmo 0.8.2 → 0.8.3
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 +189 -194
- package/dist/index.cjs +450 -596
- 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 +450 -596
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +821 -1060
- package/package.json +1 -1
- package/src/c4/parser.ts +19 -13
- package/src/chart.ts +69 -47
- package/src/class/parser.ts +46 -19
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +11 -16
- package/src/completion.ts +29 -25
- package/src/d3.ts +173 -174
- package/src/dgmo-router.ts +1 -1
- package/src/echarts.ts +42 -22
- package/src/er/parser.ts +9 -17
- package/src/gantt/parser.ts +108 -40
- package/src/graph/flowchart-parser.ts +7 -55
- package/src/graph/state-parser.ts +7 -10
- package/src/infra/parser.ts +6 -126
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +7 -13
- package/src/kanban/parser.ts +4 -7
- package/src/org/parser.ts +5 -8
- package/src/org/resolver.ts +3 -3
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +22 -45
- package/src/sitemap/parser.ts +10 -17
- package/src/utils/parsing.ts +9 -43
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/infra/parser.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Parses `infra [Title]` syntax into a structured InfraModel.
|
|
6
6
|
// Handles: chart metadata, component blocks with indented properties
|
|
7
|
-
// and connections, [Group]
|
|
7
|
+
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
9
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
10
|
-
import { measureIndent,
|
|
10
|
+
import { measureIndent, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
|
|
11
11
|
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
12
12
|
import type {
|
|
13
13
|
ParsedInfra,
|
|
@@ -40,12 +40,6 @@ const ASYNC_SIMPLE_CONNECTION_RE =
|
|
|
40
40
|
// Deprecated xN fanout suffix (e.g. "x5" at end of line)
|
|
41
41
|
const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
|
|
42
42
|
|
|
43
|
-
// "is a" type declaration: NodeName is a <type>
|
|
44
|
-
const IS_A_RE = /^(.+?)\s+is\s+an?\s+(database|cache|queue|service|gateway|storage|function|network)\s*$/i;
|
|
45
|
-
|
|
46
|
-
// Valid node types for "is a" declarations
|
|
47
|
-
const VALID_NODE_TYPES = new Set(['database', 'cache', 'queue', 'service', 'gateway', 'storage', 'function', 'network']);
|
|
48
|
-
|
|
49
43
|
// Group declaration: [Group Name] with optional pipe metadata
|
|
50
44
|
const GROUP_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
|
|
51
45
|
|
|
@@ -217,16 +211,9 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
217
211
|
continue;
|
|
218
212
|
}
|
|
219
213
|
|
|
220
|
-
// direction
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const raw = trimmed.replace(/^(?:direction|orientation)\s+/i, '').trim();
|
|
224
|
-
const dir = normalizeDirection(raw);
|
|
225
|
-
if (dir) {
|
|
226
|
-
result.direction = dir;
|
|
227
|
-
} else {
|
|
228
|
-
warn(lineNumber, `Unknown direction '${raw}'. Expected 'LR', 'TB', 'horizontal', or 'vertical'.`);
|
|
229
|
-
}
|
|
214
|
+
// direction-tb — bare boolean to switch to top-to-bottom (default is LR)
|
|
215
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
216
|
+
result.direction = 'TB';
|
|
230
217
|
continue;
|
|
231
218
|
}
|
|
232
219
|
|
|
@@ -247,23 +234,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
247
234
|
continue;
|
|
248
235
|
}
|
|
249
236
|
|
|
250
|
-
// scenario: Name — no longer supported
|
|
251
|
-
if (/^scenario\s*:/i.test(trimmed)) {
|
|
252
|
-
setError(lineNumber, `'scenario:' syntax is no longer supported`);
|
|
253
|
-
// Skip indented block
|
|
254
|
-
let si = i + 1;
|
|
255
|
-
while (si < lines.length) {
|
|
256
|
-
const sLine = lines[si];
|
|
257
|
-
const sTrimmed = sLine.trim();
|
|
258
|
-
if (!sTrimmed || sTrimmed.startsWith('#')) { si++; continue; }
|
|
259
|
-
const sIndent = sLine.length - sLine.trimStart().length;
|
|
260
|
-
if (sIndent === 0) break;
|
|
261
|
-
si++;
|
|
262
|
-
}
|
|
263
|
-
i = si - 1;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
237
|
// Tag group: `tag Name [alias]` (via shared matchTagBlockHeading)
|
|
268
238
|
const tagMatch = matchTagBlockHeading(trimmed);
|
|
269
239
|
if (tagMatch) {
|
|
@@ -278,23 +248,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
278
248
|
continue;
|
|
279
249
|
}
|
|
280
250
|
|
|
281
|
-
// # GroupName (alternate group notation)
|
|
282
|
-
const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
|
|
283
|
-
if (hashGroupMatch) {
|
|
284
|
-
finishCurrentNode();
|
|
285
|
-
finishCurrentTagGroup();
|
|
286
|
-
const gLabel = hashGroupMatch[1].trim();
|
|
287
|
-
const gId = groupId(gLabel);
|
|
288
|
-
currentGroup = {
|
|
289
|
-
id: gId,
|
|
290
|
-
label: gLabel,
|
|
291
|
-
metadata: undefined,
|
|
292
|
-
lineNumber,
|
|
293
|
-
};
|
|
294
|
-
result.groups.push(currentGroup);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
251
|
// [Group Name] or [Group Name] | t: Engineering
|
|
299
252
|
const groupMatch = trimmed.match(GROUP_RE);
|
|
300
253
|
if (groupMatch) {
|
|
@@ -313,32 +266,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
313
266
|
continue;
|
|
314
267
|
}
|
|
315
268
|
|
|
316
|
-
// "is a" type declaration: NodeName is a <type>
|
|
317
|
-
const isaMatch = trimmed.match(IS_A_RE);
|
|
318
|
-
if (isaMatch) {
|
|
319
|
-
finishCurrentNode();
|
|
320
|
-
finishCurrentTagGroup();
|
|
321
|
-
|
|
322
|
-
const name = isaMatch[1].trim();
|
|
323
|
-
const nType = isaMatch[2].toLowerCase();
|
|
324
|
-
const id = nodeId(name);
|
|
325
|
-
const isEdge = EDGE_NODE_NAMES.has(id.toLowerCase());
|
|
326
|
-
|
|
327
|
-
currentNode = {
|
|
328
|
-
id,
|
|
329
|
-
label: name,
|
|
330
|
-
properties: [],
|
|
331
|
-
groupId: null,
|
|
332
|
-
tags: {},
|
|
333
|
-
isEdge,
|
|
334
|
-
nodeType: nType,
|
|
335
|
-
lineNumber,
|
|
336
|
-
};
|
|
337
|
-
currentGroup = null;
|
|
338
|
-
baseIndent = 0;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
269
|
// Component at top level (no indent)
|
|
343
270
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
344
271
|
if (compMatch) {
|
|
@@ -408,30 +335,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
408
335
|
}
|
|
409
336
|
}
|
|
410
337
|
|
|
411
|
-
// "is a" type declaration inside group
|
|
412
|
-
const isaMatchG = trimmed.match(IS_A_RE);
|
|
413
|
-
if (isaMatchG) {
|
|
414
|
-
finishCurrentTagGroup();
|
|
415
|
-
const name = isaMatchG[1].trim();
|
|
416
|
-
const nType = isaMatchG[2].toLowerCase();
|
|
417
|
-
const id = nodeId(name);
|
|
418
|
-
// Cascade group metadata into node tags (node-level overrides later)
|
|
419
|
-
const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
|
|
420
|
-
|
|
421
|
-
currentNode = {
|
|
422
|
-
id,
|
|
423
|
-
label: name,
|
|
424
|
-
properties: [],
|
|
425
|
-
groupId: currentGroup.id,
|
|
426
|
-
tags,
|
|
427
|
-
isEdge: false,
|
|
428
|
-
nodeType: nType,
|
|
429
|
-
lineNumber,
|
|
430
|
-
};
|
|
431
|
-
baseIndent = indent;
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
338
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
436
339
|
if (compMatch) {
|
|
437
340
|
finishCurrentTagGroup();
|
|
@@ -639,28 +542,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
639
542
|
if (currentGroup && indent > 0) {
|
|
640
543
|
finishCurrentNode();
|
|
641
544
|
|
|
642
|
-
// "is a" type declaration inside group
|
|
643
|
-
const isaMatchG2 = trimmed.match(IS_A_RE);
|
|
644
|
-
if (isaMatchG2) {
|
|
645
|
-
const name = isaMatchG2[1].trim();
|
|
646
|
-
const nType = isaMatchG2[2].toLowerCase();
|
|
647
|
-
const id = nodeId(name);
|
|
648
|
-
const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
|
|
649
|
-
|
|
650
|
-
currentNode = {
|
|
651
|
-
id,
|
|
652
|
-
label: name,
|
|
653
|
-
properties: [],
|
|
654
|
-
groupId: currentGroup.id,
|
|
655
|
-
tags,
|
|
656
|
-
isEdge: false,
|
|
657
|
-
nodeType: nType,
|
|
658
|
-
lineNumber,
|
|
659
|
-
};
|
|
660
|
-
baseIndent = indent;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
545
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
665
546
|
if (compMatch) {
|
|
666
547
|
const name = compMatch[1];
|
|
@@ -780,7 +661,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
780
661
|
// Recognize new-style bare options (`key value`) and old-style (`key: value`)
|
|
781
662
|
const firstLine = parseFirstLine(line);
|
|
782
663
|
if (firstLine) continue; // chart type line
|
|
783
|
-
if (/^(?:direction|
|
|
664
|
+
if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line)) continue;
|
|
784
665
|
if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
|
|
785
666
|
inMetadata = false;
|
|
786
667
|
} else {
|
|
@@ -794,7 +675,6 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
794
675
|
if (/^tag\s*:/i.test(line)) { inTagGroup = true; continue; } // legacy
|
|
795
676
|
inTagGroup = false;
|
|
796
677
|
if (/^\[/.test(line)) continue; // [Group] header
|
|
797
|
-
if (/^#\s/.test(line)) continue; // # Group header
|
|
798
678
|
const m = COMPONENT_RE.exec(line);
|
|
799
679
|
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
800
680
|
} else {
|
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
|
}
|
|
@@ -203,16 +203,16 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
203
203
|
continue;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// hide directive (
|
|
206
|
+
// hide directive (colon syntax): `hide phase:Planning, phase:Review`
|
|
207
207
|
const hideMatch = trimmed.match(/^hide\s+(.+)/i);
|
|
208
208
|
if (hideMatch && !trimmed.match(/^hide\s*\|/)) {
|
|
209
|
-
// Parse comma-separated tag
|
|
209
|
+
// Parse comma-separated tag:value pairs: `phase:Planning, phase:Review`
|
|
210
210
|
const pairs = hideMatch[1].split(',');
|
|
211
211
|
for (const pair of pairs) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
const groupKey =
|
|
215
|
-
const value =
|
|
212
|
+
const colonIdx = pair.indexOf(':');
|
|
213
|
+
if (colonIdx > 0) {
|
|
214
|
+
const groupKey = pair.substring(0, colonIdx).trim().toLowerCase();
|
|
215
|
+
const value = pair.substring(colonIdx + 1).trim().toLowerCase();
|
|
216
216
|
if (groupKey && value) {
|
|
217
217
|
if (!result.initialHiddenTagValues.has(groupKey)) {
|
|
218
218
|
result.initialHiddenTagValues.set(groupKey, new Set());
|
|
@@ -231,7 +231,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
231
231
|
const key = optMatch[1].toLowerCase();
|
|
232
232
|
const value = optMatch[2].trim();
|
|
233
233
|
// Only recognize known option keys (not node content)
|
|
234
|
-
if (key === 'active-tag'
|
|
234
|
+
if (key === 'active-tag') {
|
|
235
235
|
result.options[key] = value;
|
|
236
236
|
continue;
|
|
237
237
|
}
|
|
@@ -247,12 +247,6 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
247
247
|
);
|
|
248
248
|
continue;
|
|
249
249
|
}
|
|
250
|
-
if (tagBlockMatch.deprecated) {
|
|
251
|
-
result.diagnostics.push(
|
|
252
|
-
makeDgmoError(lineNum, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag ${tagBlockMatch.name}' instead`)
|
|
253
|
-
);
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
250
|
currentTagGroup = {
|
|
257
251
|
name: tagBlockMatch.name,
|
|
258
252
|
alias: tagBlockMatch.alias,
|
package/src/kanban/parser.ts
CHANGED
|
@@ -27,10 +27,11 @@ const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
|
|
|
27
27
|
|
|
28
28
|
/** Known kanban options (key-value). */
|
|
29
29
|
const KNOWN_OPTIONS = new Set([
|
|
30
|
-
'
|
|
30
|
+
'hide',
|
|
31
31
|
]);
|
|
32
32
|
/** Known kanban boolean options (bare keyword = on). */
|
|
33
33
|
const KNOWN_BOOLEANS = new Set<string>([
|
|
34
|
+
'no-auto-color',
|
|
34
35
|
]);
|
|
35
36
|
|
|
36
37
|
// ============================================================
|
|
@@ -125,15 +126,11 @@ export function parseKanban(
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// Tag group heading — `tag
|
|
129
|
-
// Must be checked BEFORE OPTION_RE to prevent `tag
|
|
129
|
+
// Tag group heading — `tag Name`
|
|
130
|
+
// Must be checked BEFORE OPTION_RE to prevent `tag Rank` being swallowed as option
|
|
130
131
|
if (!contentStarted) {
|
|
131
132
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
132
133
|
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
134
|
currentTagGroup = {
|
|
138
135
|
name: tagBlockMatch.name,
|
|
139
136
|
alias: tagBlockMatch.alias,
|
package/src/org/parser.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
measureIndent,
|
|
8
8
|
extractColor,
|
|
9
9
|
parsePipeMetadata,
|
|
10
|
-
|
|
10
|
+
MULTIPLE_PIPE_ERROR,
|
|
11
11
|
parseFirstLine,
|
|
12
12
|
OPTION_NOCOLON_RE,
|
|
13
13
|
} from '../utils/parsing';
|
|
@@ -46,18 +46,19 @@ const METADATA_RE = /^([^:]+):\s*(.+)$/;
|
|
|
46
46
|
|
|
47
47
|
/** Known org chart options (key-value). */
|
|
48
48
|
const KNOWN_OPTIONS = new Set([
|
|
49
|
-
'
|
|
49
|
+
'sub-node-label', 'hide', 'show-sub-node-count',
|
|
50
50
|
]);
|
|
51
51
|
/** Known org chart boolean options (bare keyword = on). */
|
|
52
52
|
const KNOWN_BOOLEANS = new Set([
|
|
53
53
|
'show-sub-node-count',
|
|
54
|
+
'direction-tb',
|
|
54
55
|
]);
|
|
55
56
|
|
|
56
57
|
// ============================================================
|
|
57
58
|
// Inference
|
|
58
59
|
// ============================================================
|
|
59
60
|
|
|
60
|
-
/** Returns true if content contains tag group headings (`tag
|
|
61
|
+
/** Returns true if content contains tag group headings (`tag …`), suggesting an org chart. */
|
|
61
62
|
export function looksLikeOrg(content: string): boolean {
|
|
62
63
|
for (const line of content.split('\n')) {
|
|
63
64
|
const trimmed = line.trim();
|
|
@@ -169,10 +170,6 @@ export function parseOrg(
|
|
|
169
170
|
pushError(lineNumber, 'Tag groups must appear before org content');
|
|
170
171
|
continue;
|
|
171
172
|
}
|
|
172
|
-
if (tagBlockMatch.deprecated) {
|
|
173
|
-
pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
173
|
currentTagGroup = {
|
|
177
174
|
name: tagBlockMatch.name,
|
|
178
175
|
alias: tagBlockMatch.alias,
|
|
@@ -335,7 +332,7 @@ function parseNodeLabel(
|
|
|
335
332
|
let rawLabel = segments[0];
|
|
336
333
|
const { label, color } = extractColor(rawLabel, palette);
|
|
337
334
|
|
|
338
|
-
const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber,
|
|
335
|
+
const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined);
|
|
339
336
|
|
|
340
337
|
return {
|
|
341
338
|
id: `node-${counter}`,
|
package/src/org/resolver.ts
CHANGED
|
@@ -36,14 +36,14 @@ export interface ResolveImportsResult {
|
|
|
36
36
|
const MAX_DEPTH = 10;
|
|
37
37
|
const IMPORT_RE = /^(\s+)import:?\s+(.+\.dgmo)\s*$/i;
|
|
38
38
|
const TAGS_RE = /^tags:?\s+(.+\.dgmo)\s*$/i;
|
|
39
|
-
/** Matches new-style first line: `org ...` or
|
|
40
|
-
const HEADER_RE = /^(org|kanban|
|
|
39
|
+
/** Matches new-style first line: `org ...` or `kanban ...` or `title: ...` */
|
|
40
|
+
const HEADER_RE = /^(org|kanban|title\s*:)/i;
|
|
41
41
|
/**
|
|
42
42
|
* Known option keys that can appear in org chart headers (space-separated).
|
|
43
43
|
* Only these are stripped from imported files — avoids eating content like "Alice Chen".
|
|
44
44
|
*/
|
|
45
45
|
const KNOWN_HEADER_OPTIONS = new Set([
|
|
46
|
-
'direction', 'sub-node-label', 'hide', 'show-sub-node-count',
|
|
46
|
+
'direction-tb', 'sub-node-label', 'hide', 'show-sub-node-count',
|
|
47
47
|
'color-off',
|
|
48
48
|
]);
|
|
49
49
|
|
package/src/render.ts
CHANGED
|
@@ -36,8 +36,7 @@ async function ensureDom(): Promise<void> {
|
|
|
36
36
|
* ```ts
|
|
37
37
|
* import { render } from '@diagrammo/dgmo';
|
|
38
38
|
*
|
|
39
|
-
* const svg = await render(`
|
|
40
|
-
* title: Languages
|
|
39
|
+
* const svg = await render(`pie Languages
|
|
41
40
|
* TypeScript: 45
|
|
42
41
|
* Python: 30
|
|
43
42
|
* Rust: 25`);
|
package/src/sequence/parser.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { inferParticipantType } from './participant-inference';
|
|
|
6
6
|
import type { DgmoError } from '../diagnostics';
|
|
7
7
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
8
8
|
import { parseArrow } from '../utils/arrows';
|
|
9
|
-
import { measureIndent, extractColor, parsePipeMetadata,
|
|
9
|
+
import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_ERROR, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
|
|
10
10
|
import type { TagGroup } from '../utils/tag-groups';
|
|
11
11
|
import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
12
12
|
|
|
@@ -185,17 +185,15 @@ const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
|
185
185
|
// Arrow pattern for sequence inference — detects any arrow form
|
|
186
186
|
const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
|
|
187
187
|
|
|
188
|
-
// Note patterns — colon-free syntax
|
|
188
|
+
// Note patterns — colon-free syntax only
|
|
189
189
|
// Single-line: "note text", "note left text", "note right of X text", "note left X text"
|
|
190
190
|
// Multi-line: "note", "note right", "note right of X", "note left X" (body indented below)
|
|
191
|
-
// Also supports legacy colon syntax: "note: text", "note right of X: text"
|
|
192
191
|
//
|
|
193
192
|
// The colon-free positioned form requires participant resolution — the parser
|
|
194
193
|
// already has participant collection infrastructure, so we match the general
|
|
195
194
|
// structure here and resolve participant vs text in the parsing logic.
|
|
196
|
-
const NOTE_SINGLE_COLON = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*:\s*(.+)$/i;
|
|
197
195
|
const NOTE_BARE = /^note\s+(.+)$/i;
|
|
198
|
-
const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s
|
|
196
|
+
const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*$/i;
|
|
199
197
|
|
|
200
198
|
/** Result of parseNoteLine — indicates what the parser should do. */
|
|
201
199
|
type NoteParseResult =
|
|
@@ -208,10 +206,10 @@ type NoteParseResult =
|
|
|
208
206
|
* Parse a note line, resolving participant names from the known participants list.
|
|
209
207
|
*
|
|
210
208
|
* Supports:
|
|
211
|
-
* - `note
|
|
212
|
-
* - `note left of X
|
|
213
|
-
* - `note right
|
|
214
|
-
* - `note right of X
|
|
209
|
+
* - `note text` — default position (right), last msg sender
|
|
210
|
+
* - `note left of X text` / `note left X text`
|
|
211
|
+
* - `note right` — multi-line head
|
|
212
|
+
* - `note right of X` / `note left X` — multi-line head
|
|
215
213
|
* - Quoted participant: `note left "Auth Service" text`
|
|
216
214
|
*/
|
|
217
215
|
function parseNoteLine(
|
|
@@ -222,25 +220,12 @@ function parseNoteLine(
|
|
|
222
220
|
const lower = trimmed.toLowerCase();
|
|
223
221
|
if (!lower.startsWith('note')) return null;
|
|
224
222
|
// Must be exactly "note" or "note " — not "notebook" etc.
|
|
225
|
-
if (trimmed.length > 4 && trimmed[4] !== ' '
|
|
223
|
+
if (trimmed.length > 4 && trimmed[4] !== ' ') return null;
|
|
226
224
|
|
|
227
|
-
// 1. Try
|
|
228
|
-
const colonMatch = trimmed.match(NOTE_SINGLE_COLON);
|
|
229
|
-
if (colonMatch) {
|
|
230
|
-
const position = (colonMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
|
|
231
|
-
let participantId = colonMatch[2] || null;
|
|
232
|
-
if (!participantId) {
|
|
233
|
-
if (!lastMsgFrom) return { kind: 'skip' };
|
|
234
|
-
participantId = lastMsgFrom;
|
|
235
|
-
}
|
|
236
|
-
if (!participants.some((p) => p.id === participantId)) return { kind: 'skip' };
|
|
237
|
-
return { kind: 'single', position, participantId, text: colonMatch[3].trim() };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// 2. Try multi-line head (no text after note): `note`, `note right`, `note right of X`, `note left X`
|
|
225
|
+
// 1. Try multi-line head (no text after note): `note`, `note right`, `note right of X`, `note left X`
|
|
241
226
|
// NOTE: NOTE_MULTI's (.+?) can greedily capture "participant text" as one group.
|
|
242
|
-
// Only trust this match if the captured participant actually exists. Otherwise,
|
|
243
|
-
// through to the bare-note handler which does proper participant-aware splitting.
|
|
227
|
+
// Only trust this match if the captured participant actually exists. Otherwise,
|
|
228
|
+
// fall through to the bare-note handler which does proper participant-aware splitting.
|
|
244
229
|
const multiMatch = trimmed.match(NOTE_MULTI);
|
|
245
230
|
if (multiMatch) {
|
|
246
231
|
const position = (multiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
|
|
@@ -255,7 +240,7 @@ function parseNoteLine(
|
|
|
255
240
|
// Participant not found — fall through to bare-note handler for proper resolution
|
|
256
241
|
}
|
|
257
242
|
|
|
258
|
-
//
|
|
243
|
+
// 2. Bare note: `note text` or `note left [of] X text`
|
|
259
244
|
const bareMatch = trimmed.match(NOTE_BARE);
|
|
260
245
|
if (bareMatch) {
|
|
261
246
|
const rest = bareMatch[1].trim();
|
|
@@ -427,7 +412,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
427
412
|
if (idx < 0) return { core: text };
|
|
428
413
|
const core = text.substring(0, idx).trimEnd();
|
|
429
414
|
const segments = text.substring(idx).split('|');
|
|
430
|
-
const warnFn = ln != null ? () =>
|
|
415
|
+
const warnFn = ln != null ? () => pushError(ln, MULTIPLE_PIPE_ERROR) : undefined;
|
|
431
416
|
const meta = parsePipeMetadata(segments, aliasMap, warnFn);
|
|
432
417
|
return Object.keys(meta).length > 0 ? { core, meta } : { core };
|
|
433
418
|
};
|
|
@@ -475,7 +460,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
475
460
|
const afterBracket = groupMatch[3]?.trim() || '';
|
|
476
461
|
if (afterBracket.startsWith('|')) {
|
|
477
462
|
const segments = afterBracket.split('|');
|
|
478
|
-
const meta = parsePipeMetadata(segments, aliasMap, () =>
|
|
463
|
+
const meta = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
479
464
|
if (Object.keys(meta).length > 0) groupMeta = meta;
|
|
480
465
|
}
|
|
481
466
|
|
|
@@ -531,9 +516,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
531
516
|
}
|
|
532
517
|
|
|
533
518
|
// ---- Tag group handling ----
|
|
534
|
-
// Tag block heading: "tag
|
|
519
|
+
// Tag block heading: "tag Name [alias X]"
|
|
535
520
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
536
|
-
if (tagBlockMatch
|
|
521
|
+
if (tagBlockMatch) {
|
|
537
522
|
if (contentStarted) {
|
|
538
523
|
pushError(lineNumber, 'Tag groups must appear before sequence content');
|
|
539
524
|
continue;
|
|
@@ -608,14 +593,6 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
608
593
|
} else {
|
|
609
594
|
const value = trimmed.substring(colonIndex + 1).trim();
|
|
610
595
|
|
|
611
|
-
if (key === 'chart') {
|
|
612
|
-
hasExplicitChart = true;
|
|
613
|
-
if (value.toLowerCase() !== 'sequence') {
|
|
614
|
-
return fail(lineNumber, `Expected chart type "sequence", got "${value}"`);
|
|
615
|
-
}
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
596
|
// Enforce headers-before-content
|
|
620
597
|
if (contentStarted) {
|
|
621
598
|
pushError(lineNumber, `Options like '${key}: ${value}' must appear before the first message or declaration`);
|
|
@@ -666,8 +643,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
666
643
|
}
|
|
667
644
|
|
|
668
645
|
// Parse "Name is a type [aka Alias]" declarations (always top-level)
|
|
646
|
+
// Skip lines starting with 'note' — handled by note parsing below
|
|
669
647
|
const { core: isACore, meta: isAMeta } = splitPipe(trimmed, lineNumber);
|
|
670
|
-
const isAMatch = isACore.match(IS_A_PATTERN);
|
|
648
|
+
const isAMatch = !/^note(\s|$)/i.test(trimmed) ? isACore.match(IS_A_PATTERN) : null;
|
|
671
649
|
if (isAMatch) {
|
|
672
650
|
contentStarted = true;
|
|
673
651
|
const id = isAMatch[1];
|
|
@@ -1049,12 +1027,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1049
1027
|
continue;
|
|
1050
1028
|
}
|
|
1051
1029
|
|
|
1052
|
-
// ---- Note parsing (
|
|
1030
|
+
// ---- Note parsing (space-separated only) ----
|
|
1053
1031
|
// Strategy:
|
|
1054
|
-
// 1. Try
|
|
1055
|
-
// 2.
|
|
1056
|
-
// 3.
|
|
1057
|
-
// 4. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
|
|
1032
|
+
// 1. Try bare note: `note text` — position defaults, text is everything after `note`
|
|
1033
|
+
// 2. For positioned: `note left [of] X text` — needs participant lookup to split name vs text
|
|
1034
|
+
// 3. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
|
|
1058
1035
|
{
|
|
1059
1036
|
const noteParsed = parseNoteLine(trimmed, result.participants, lastMsgFrom);
|
|
1060
1037
|
if (noteParsed) {
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -11,10 +11,8 @@ import {
|
|
|
11
11
|
measureIndent,
|
|
12
12
|
extractColor,
|
|
13
13
|
parsePipeMetadata,
|
|
14
|
-
normalizeDirection,
|
|
15
14
|
inferArrowColor,
|
|
16
|
-
|
|
17
|
-
CHART_TYPE_RE,
|
|
15
|
+
MULTIPLE_PIPE_ERROR,
|
|
18
16
|
TITLE_RE,
|
|
19
17
|
OPTION_RE,
|
|
20
18
|
parseFirstLine,
|
|
@@ -92,7 +90,7 @@ export function looksLikeSitemap(content: string): boolean {
|
|
|
92
90
|
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
93
91
|
|
|
94
92
|
// Skip header lines
|
|
95
|
-
if (
|
|
93
|
+
if (parseFirstLine(trimmed) || TITLE_RE.test(trimmed)) continue;
|
|
96
94
|
if (isTagBlockHeading(trimmed)) continue;
|
|
97
95
|
|
|
98
96
|
if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
|
|
@@ -124,7 +122,7 @@ export function parseSitemap(
|
|
|
124
122
|
const result: ParsedSitemap = {
|
|
125
123
|
title: null,
|
|
126
124
|
titleLineNumber: null,
|
|
127
|
-
direction: '
|
|
125
|
+
direction: 'LR',
|
|
128
126
|
roots: [],
|
|
129
127
|
edges: [],
|
|
130
128
|
tagGroups: [],
|
|
@@ -226,10 +224,6 @@ export function parseSitemap(
|
|
|
226
224
|
pushError(lineNumber, 'Tag groups must appear before sitemap content');
|
|
227
225
|
continue;
|
|
228
226
|
}
|
|
229
|
-
if (tagBlockMatch.deprecated) {
|
|
230
|
-
pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
227
|
currentTagGroup = {
|
|
234
228
|
name: tagBlockMatch.name,
|
|
235
229
|
alias: tagBlockMatch.alias,
|
|
@@ -247,16 +241,15 @@ export function parseSitemap(
|
|
|
247
241
|
// Skip lines with `|` (pipe metadata) or `->` (arrows) — those are content
|
|
248
242
|
if (!contentStarted && !currentTagGroup && measureIndent(line) === 0
|
|
249
243
|
&& !trimmed.includes('|') && !trimmed.includes('->')) {
|
|
244
|
+
// Bare boolean: direction-tb
|
|
245
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
246
|
+
result.direction = 'TB';
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
250
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
251
251
|
if (optMatch) {
|
|
252
252
|
const key = optMatch[1].trim().toLowerCase();
|
|
253
|
-
if (key === 'direction' || key === 'orientation') {
|
|
254
|
-
const dir = normalizeDirection(optMatch[2]);
|
|
255
|
-
if (dir) {
|
|
256
|
-
result.direction = dir as SitemapDirection;
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
253
|
result.options[key] = optMatch[2].trim();
|
|
261
254
|
continue;
|
|
262
255
|
}
|
|
@@ -440,7 +433,7 @@ function parseNodeLabel(
|
|
|
440
433
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
441
434
|
const rawLabel = segments[0];
|
|
442
435
|
const { label, color } = extractColor(rawLabel, palette);
|
|
443
|
-
const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber,
|
|
436
|
+
const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined);
|
|
444
437
|
|
|
445
438
|
return {
|
|
446
439
|
id: `node-${counter}`,
|