@diagrammo/dgmo 0.8.1 → 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/package.json
CHANGED
package/src/c4/parser.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
measureIndent,
|
|
12
12
|
extractColor,
|
|
13
13
|
parsePipeMetadata,
|
|
14
|
-
|
|
14
|
+
MULTIPLE_PIPE_ERROR,
|
|
15
15
|
parseFirstLine,
|
|
16
16
|
OPTION_NOCOLON_RE,
|
|
17
17
|
} from '../utils/parsing';
|
|
@@ -56,8 +56,8 @@ const SECTION_HEADER_RE = /^(containers|components|deployment)\s*$/i;
|
|
|
56
56
|
/** Matches `container X` references inside deployment nodes */
|
|
57
57
|
const CONTAINER_REF_RE = /^container\s+(.+)$/i;
|
|
58
58
|
|
|
59
|
-
/** Matches indented metadata: `key value` (
|
|
60
|
-
const METADATA_RE = /^([a-z][a-z0-9-]*)
|
|
59
|
+
/** Matches indented metadata: `key: value` (colon-separated) */
|
|
60
|
+
const METADATA_RE = /^([a-z][a-z0-9-]*):\s+(.+)$/i;
|
|
61
61
|
|
|
62
62
|
// ============================================================
|
|
63
63
|
// Helpers
|
|
@@ -83,7 +83,11 @@ const VALID_SHAPES = new Set<string>([
|
|
|
83
83
|
/** Known top-level option keys for C4 diagrams. */
|
|
84
84
|
const KNOWN_C4_OPTIONS = new Set<string>([
|
|
85
85
|
'layout',
|
|
86
|
-
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/** Known C4 boolean options (bare keyword = on). */
|
|
89
|
+
const KNOWN_C4_BOOLEANS = new Set<string>([
|
|
90
|
+
'direction-tb',
|
|
87
91
|
]);
|
|
88
92
|
|
|
89
93
|
const ALL_CHART_TYPES = [
|
|
@@ -286,10 +290,6 @@ export function parseC4(
|
|
|
286
290
|
pushError(lineNumber, 'Tag groups must appear before content');
|
|
287
291
|
continue;
|
|
288
292
|
}
|
|
289
|
-
if (tagBlockMatch.deprecated) {
|
|
290
|
-
pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
293
|
currentTagGroup = {
|
|
294
294
|
name: tagBlockMatch.name,
|
|
295
295
|
alias: tagBlockMatch.alias,
|
|
@@ -303,8 +303,14 @@ export function parseC4(
|
|
|
303
303
|
continue;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
// Generic header options (space-separated: `key value`)
|
|
306
|
+
// Generic header options (space-separated: `key value` or bare boolean)
|
|
307
307
|
if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
|
|
308
|
+
// Bare boolean options
|
|
309
|
+
if (KNOWN_C4_BOOLEANS.has(trimmed.toLowerCase())) {
|
|
310
|
+
result.options[trimmed.toLowerCase()] = 'on';
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
308
314
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
309
315
|
if (optMatch) {
|
|
310
316
|
const key = optMatch[1].trim().toLowerCase();
|
|
@@ -382,7 +388,7 @@ export function parseC4(
|
|
|
382
388
|
// Otherwise it's a deployment node (possibly with pipe metadata)
|
|
383
389
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
384
390
|
const nodeName = segments[0];
|
|
385
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber,
|
|
391
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
386
392
|
const shape = inferC4Shape(nodeName, metadata.tech ?? metadata.technology);
|
|
387
393
|
|
|
388
394
|
const dNode: C4DeploymentNode = {
|
|
@@ -641,7 +647,7 @@ export function parseC4(
|
|
|
641
647
|
namePart = namePart.substring(0, nameIsAMatch.index!).trim();
|
|
642
648
|
}
|
|
643
649
|
|
|
644
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber,
|
|
650
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
645
651
|
|
|
646
652
|
const shape =
|
|
647
653
|
explicitShape ??
|
|
@@ -705,7 +711,7 @@ export function parseC4(
|
|
|
705
711
|
`'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`,
|
|
706
712
|
);
|
|
707
713
|
|
|
708
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber,
|
|
714
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
|
|
709
715
|
|
|
710
716
|
// Determine shape: explicit > inference
|
|
711
717
|
const shape =
|
|
@@ -747,7 +753,7 @@ export function parseC4(
|
|
|
747
753
|
if (parentEntry) {
|
|
748
754
|
const rawKey = metadataMatch[1].trim().toLowerCase();
|
|
749
755
|
|
|
750
|
-
// Special case: `import file.dgmo`
|
|
756
|
+
// Special case: `import: file.dgmo`
|
|
751
757
|
if (rawKey === 'import') {
|
|
752
758
|
parentEntry.element.importPath = metadataMatch[2].trim();
|
|
753
759
|
continue;
|
package/src/chart.ts
CHANGED
|
@@ -46,7 +46,9 @@ export interface ParsedChart {
|
|
|
46
46
|
orientation?: 'horizontal' | 'vertical';
|
|
47
47
|
color?: string;
|
|
48
48
|
label?: string;
|
|
49
|
-
|
|
49
|
+
noLabelName?: boolean;
|
|
50
|
+
noLabelValue?: boolean;
|
|
51
|
+
noLabelPercent?: boolean;
|
|
50
52
|
data: ChartDataPoint[];
|
|
51
53
|
eras?: ChartEra[];
|
|
52
54
|
diagnostics: DgmoError[];
|
|
@@ -60,7 +62,7 @@ export interface ParsedChart {
|
|
|
60
62
|
import { resolveColor } from './colors';
|
|
61
63
|
import type { PaletteColors } from './palettes';
|
|
62
64
|
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
63
|
-
import { extractColor,
|
|
65
|
+
import { extractColor, normalizeGroupedNumber, parseFirstLine, parseSeriesNames } from './utils/parsing';
|
|
64
66
|
|
|
65
67
|
// ============================================================
|
|
66
68
|
// Parser
|
|
@@ -83,8 +85,14 @@ const TYPE_ALIASES: Record<string, ChartType> = {
|
|
|
83
85
|
|
|
84
86
|
/** Known option keywords for the simple chart parser. */
|
|
85
87
|
const KNOWN_OPTIONS = new Set([
|
|
86
|
-
'chart', 'title', 'series', 'xlabel', 'ylabel', 'label',
|
|
87
|
-
'
|
|
88
|
+
'chart', 'title', 'series', 'xlabel', 'ylabel', 'label',
|
|
89
|
+
'no-label-name', 'no-label-value', 'no-label-percent',
|
|
90
|
+
'color',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/** Known boolean options for the simple chart parser. */
|
|
94
|
+
const KNOWN_BOOLEANS = new Set([
|
|
95
|
+
'orientation-horizontal',
|
|
88
96
|
]);
|
|
89
97
|
|
|
90
98
|
/**
|
|
@@ -193,6 +201,14 @@ export function parseChart(
|
|
|
193
201
|
const spaceIdx = trimmed.indexOf(' ');
|
|
194
202
|
const firstToken = (spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed).toLowerCase();
|
|
195
203
|
|
|
204
|
+
// Bare boolean options (e.g. orientation-horizontal)
|
|
205
|
+
if (KNOWN_BOOLEANS.has(firstToken) && spaceIdx < 0) {
|
|
206
|
+
if (firstToken === 'orientation-horizontal') {
|
|
207
|
+
result.orientation = 'horizontal';
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
// Known option with a value
|
|
197
213
|
if (KNOWN_OPTIONS.has(firstToken) && spaceIdx >= 0) {
|
|
198
214
|
const value = trimmed.substring(spaceIdx + 1).trim();
|
|
@@ -234,29 +250,6 @@ export function parseChart(
|
|
|
234
250
|
continue;
|
|
235
251
|
}
|
|
236
252
|
|
|
237
|
-
if (firstToken === 'labels') {
|
|
238
|
-
const v = value.toLowerCase();
|
|
239
|
-
if (v === 'name' || v === 'value' || v === 'percent' || v === 'full') {
|
|
240
|
-
result.labels = v;
|
|
241
|
-
}
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (firstToken === 'orientation' || firstToken === 'direction') {
|
|
246
|
-
// Only bar and bar-stacked support orientation (axis swapping)
|
|
247
|
-
if (result.type === 'bar' || result.type === 'bar-stacked') {
|
|
248
|
-
const vLower = value.toLowerCase();
|
|
249
|
-
if (vLower === 'horizontal' || vLower === 'vertical') {
|
|
250
|
-
result.orientation = vLower;
|
|
251
|
-
} else {
|
|
252
|
-
const dir = normalizeDirection(value);
|
|
253
|
-
if (dir === 'LR') result.orientation = 'horizontal';
|
|
254
|
-
else if (dir === 'TB') result.orientation = 'vertical';
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
253
|
if (firstToken === 'color') {
|
|
261
254
|
result.color = resolveColor(value.trim(), palette) ?? undefined;
|
|
262
255
|
continue;
|
|
@@ -276,6 +269,11 @@ export function parseChart(
|
|
|
276
269
|
}
|
|
277
270
|
}
|
|
278
271
|
|
|
272
|
+
// Bare boolean options: no-label-name, no-label-value, no-label-percent
|
|
273
|
+
if (firstToken === 'no-label-name') { result.noLabelName = true; continue; }
|
|
274
|
+
if (firstToken === 'no-label-value') { result.noLabelValue = true; continue; }
|
|
275
|
+
if (firstToken === 'no-label-percent') { result.noLabelPercent = true; continue; }
|
|
276
|
+
|
|
279
277
|
// Bare "series" keyword with no value — collect indented names
|
|
280
278
|
if (firstToken === 'series' && spaceIdx === -1) {
|
|
281
279
|
const parsed = parseSeriesNames('', lines, i, palette);
|
|
@@ -292,8 +290,10 @@ export function parseChart(
|
|
|
292
290
|
|
|
293
291
|
// Data row: parse from the right — rightmost numeric token(s) = value(s), everything left = label
|
|
294
292
|
// Supports comma-separated multi-values: "Jan 100, 200, 300"
|
|
293
|
+
// Supports space-separated multi-values when series are defined: "Jan 100 200 300"
|
|
295
294
|
// Supports comma-grouped numbers: "Revenue 1,200, 1,500" → [1200, 1500]
|
|
296
|
-
const
|
|
295
|
+
const multiValue = (result.seriesNames?.length ?? 0) >= 2;
|
|
296
|
+
const dataValues = parseDataRowValues(trimmed, { multiValue });
|
|
297
297
|
if (dataValues) {
|
|
298
298
|
const { label: rawLabel, color: pointColor } = extractColor(dataValues.label, palette);
|
|
299
299
|
const [first, ...rest] = dataValues.values;
|
|
@@ -361,7 +361,7 @@ export function parseChart(
|
|
|
361
361
|
for (const dp of result.data) {
|
|
362
362
|
const actualCount = 1 + (dp.extraValues?.length ?? 0);
|
|
363
363
|
if (actualCount !== expectedCount) {
|
|
364
|
-
warn(dp.lineNumber, `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount}
|
|
364
|
+
warn(dp.lineNumber, `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`);
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
// Filter out mismatched data points so renderers get clean data
|
|
@@ -380,20 +380,22 @@ export function parseChart(
|
|
|
380
380
|
|
|
381
381
|
/**
|
|
382
382
|
* Parse a data row line: everything before the last numeric token(s) is the label,
|
|
383
|
-
* numeric tokens at the end are the values. Supports comma-separated multi-values
|
|
384
|
-
* and comma-grouped numbers (e.g., "1,087").
|
|
383
|
+
* numeric tokens at the end are the values. Supports comma-separated multi-values,
|
|
384
|
+
* space-separated multi-values, and comma-grouped numbers (e.g., "1,087").
|
|
385
385
|
*
|
|
386
386
|
* Examples:
|
|
387
|
-
* "Jan 120"
|
|
388
|
-
* "North America 250"
|
|
389
|
-
* "
|
|
390
|
-
* "Q1 10
|
|
391
|
-
* "Revenue 1,200"
|
|
387
|
+
* "Jan 120" → { label: "Jan", values: [120] }
|
|
388
|
+
* "North America 250" → { label: "North America", values: [250] }
|
|
389
|
+
* "Q1 10, 20, 30" → { label: "Q1", values: [10, 20, 30] }
|
|
390
|
+
* "Q1 10 20 30" → { label: "Q1", values: [10, 20, 30] }
|
|
391
|
+
* "Revenue 1,200" → { label: "Revenue", values: [1200] }
|
|
392
|
+
* "Revenue 3,984,078.65"→ { label: "Revenue", values: [3984078.65] }
|
|
392
393
|
*
|
|
393
394
|
* Returns null if the line has no numeric value at the end.
|
|
394
395
|
*/
|
|
395
396
|
export function parseDataRowValues(
|
|
396
397
|
line: string,
|
|
398
|
+
options?: { multiValue?: boolean },
|
|
397
399
|
): { label: string; values: number[] } | null {
|
|
398
400
|
// First, normalize comma-grouped numbers: replace patterns like "1,087" with "1087"
|
|
399
401
|
// We need to be careful: commas also separate multi-values.
|
|
@@ -407,11 +409,12 @@ export function parseDataRowValues(
|
|
|
407
409
|
const normalized: string[] = [];
|
|
408
410
|
for (let i = 0; i < segments.length; i++) {
|
|
409
411
|
const seg = segments[i].trim();
|
|
410
|
-
// Check if this segment is a continuation of a grouped number
|
|
411
|
-
// A continuation
|
|
412
|
+
// Check if this segment is a continuation of a grouped number.
|
|
413
|
+
// A continuation starts with exactly 3 digits (possibly followed by a decimal like ".65")
|
|
414
|
+
// and follows a segment ending in digits.
|
|
412
415
|
// Grouped numbers have NO space around the comma (e.g., "1,087"), so skip if
|
|
413
416
|
// the raw segment has leading whitespace (e.g., ", 350" is a value separator).
|
|
414
|
-
if (i > 0 && /^\d{3}
|
|
417
|
+
if (i > 0 && /^\d{3}(\.\d+)?$/.test(seg) && !/^\s/.test(segments[i])) {
|
|
415
418
|
const prevSeg = normalized[normalized.length - 1].trimEnd();
|
|
416
419
|
// Check if previous segment ends with a number (1-3 digits at the end of the last token)
|
|
417
420
|
if (/\d{1,3}$/.test(prevSeg)) {
|
|
@@ -476,16 +479,35 @@ export function parseDataRowValues(
|
|
|
476
479
|
}
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
// No commas or comma parsing didn't work — split by spaces from right
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
482
|
+
// No commas or comma parsing didn't work — split by spaces from right.
|
|
483
|
+
// When multiValue is enabled, walk backward collecting consecutive numeric tokens.
|
|
484
|
+
// Otherwise (default), take only the last token — preserving labels that contain
|
|
485
|
+
// numbers (e.g., "Region 5 300" → label "Region 5", value 300).
|
|
486
|
+
const tokens = rebuilt.split(/\s+/);
|
|
487
|
+
if (tokens.length < 2) return null;
|
|
488
|
+
|
|
489
|
+
if (options?.multiValue) {
|
|
490
|
+
const values: number[] = [];
|
|
491
|
+
let idx = tokens.length - 1;
|
|
492
|
+
while (idx >= 1) {
|
|
493
|
+
const tok = tokens[idx];
|
|
494
|
+
const num = parseFloat(tok);
|
|
495
|
+
if (isNaN(num) || !isFinite(Number(tok))) break;
|
|
496
|
+
values.unshift(num);
|
|
497
|
+
idx--;
|
|
498
|
+
}
|
|
499
|
+
if (values.length === 0) return null;
|
|
500
|
+
const label = tokens.slice(0, idx + 1).join(' ');
|
|
501
|
+
if (!label) return null;
|
|
502
|
+
return { label, values };
|
|
503
|
+
}
|
|
483
504
|
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
505
|
+
// Single-value mode: only the last space-separated token
|
|
506
|
+
const lastToken = tokens[tokens.length - 1];
|
|
507
|
+
const num = parseFloat(lastToken);
|
|
508
|
+
if (isNaN(num) || !isFinite(Number(lastToken))) return null;
|
|
487
509
|
|
|
488
|
-
const label =
|
|
510
|
+
const label = tokens.slice(0, -1).join(' ');
|
|
489
511
|
if (!label) return null;
|
|
490
512
|
|
|
491
513
|
return { label, values: [num] };
|
package/src/class/parser.ts
CHANGED
|
@@ -30,10 +30,14 @@ function classId(name: string): string {
|
|
|
30
30
|
const CLASS_DECL_RE =
|
|
31
31
|
/^(?:(abstract|interface|enum)\s+)?([A-Z][A-Za-z0-9_]*)(?:\s+(extends|implements)\s+([A-Z][A-Za-z0-9_]*))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
|
|
32
32
|
|
|
33
|
-
// Relationship — arrow syntax:
|
|
34
|
-
//
|
|
35
|
-
//
|
|
33
|
+
// Relationship — arrow syntax (indented under source class):
|
|
34
|
+
// --|> TargetClass label (space-separated)
|
|
35
|
+
// --|> TargetClass : label (colon-separated, kept for transition)
|
|
36
36
|
// Arrows: --|> ..|> *-- o-- ..> ->
|
|
37
|
+
const INDENT_REL_ARROW_RE =
|
|
38
|
+
/^(--\|>|\.\.\|>|\*--|o--|\.\.\>|->)\s*([A-Z][A-Za-z0-9_]*)(?:\s+:?\s*(.+))?$/;
|
|
39
|
+
|
|
40
|
+
// Legacy top-level relationship regex (used only for detection/rejection)
|
|
37
41
|
const REL_ARROW_RE =
|
|
38
42
|
/^([A-Z][A-Za-z0-9_]*)\s*(--\|>|\.\.\|>|\*--|o--|\.\.\>|->)\s*([A-Z][A-Za-z0-9_]*)(?:\s+:?\s*(.+))?$/;
|
|
39
43
|
|
|
@@ -211,9 +215,14 @@ export function parseClassDiagram(
|
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
// Space-separated options before content (new syntax): `color
|
|
218
|
+
// Space-separated options before content (new syntax): `no-auto-color`
|
|
215
219
|
// Only match lines starting with a lowercase token (options), not uppercase (class names)
|
|
216
220
|
if (!contentStarted && indent === 0 && /^[a-z]/.test(trimmed)) {
|
|
221
|
+
// Bare boolean option (single keyword, no value)
|
|
222
|
+
if (trimmed.toLowerCase() === 'no-auto-color') {
|
|
223
|
+
result.options['no-auto-color'] = 'on';
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
217
226
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
218
227
|
if (optMatch) {
|
|
219
228
|
const key = optMatch[1].toLowerCase();
|
|
@@ -226,8 +235,27 @@ export function parseClassDiagram(
|
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
|
|
229
|
-
// Indented lines = members of current class
|
|
238
|
+
// Indented lines = relationships or members of current class
|
|
230
239
|
if (indent > 0 && currentClass) {
|
|
240
|
+
// Try indented relationship arrow: --|> TargetClass [label]
|
|
241
|
+
const indentRel = trimmed.match(INDENT_REL_ARROW_RE);
|
|
242
|
+
if (indentRel) {
|
|
243
|
+
const arrow = indentRel[1];
|
|
244
|
+
const targetName = indentRel[2];
|
|
245
|
+
const label = indentRel[3]?.trim();
|
|
246
|
+
|
|
247
|
+
getOrCreateClass(targetName, lineNumber);
|
|
248
|
+
|
|
249
|
+
result.relationships.push({
|
|
250
|
+
source: currentClass.id,
|
|
251
|
+
target: classId(targetName),
|
|
252
|
+
type: ARROW_TO_TYPE[arrow],
|
|
253
|
+
...(label && { label }),
|
|
254
|
+
lineNumber,
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
const member = parseMember(
|
|
232
260
|
trimmed,
|
|
233
261
|
lineNumber,
|
|
@@ -243,25 +271,19 @@ export function parseClassDiagram(
|
|
|
243
271
|
currentClass = null;
|
|
244
272
|
contentStarted = true;
|
|
245
273
|
|
|
246
|
-
//
|
|
274
|
+
// Reject top-level relationship arrows — must be indented under source class
|
|
247
275
|
const relArrow = trimmed.match(REL_ARROW_RE);
|
|
248
276
|
if (relArrow) {
|
|
249
277
|
const sourceName = relArrow[1];
|
|
250
278
|
const arrow = relArrow[2];
|
|
251
279
|
const targetName = relArrow[3];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
source: classId(sourceName),
|
|
260
|
-
target: classId(targetName),
|
|
261
|
-
type: ARROW_TO_TYPE[arrow],
|
|
262
|
-
...(label && { label }),
|
|
263
|
-
lineNumber,
|
|
264
|
-
});
|
|
280
|
+
result.diagnostics.push(
|
|
281
|
+
makeDgmoError(
|
|
282
|
+
lineNumber,
|
|
283
|
+
`Relationship "${sourceName} ${arrow} ${targetName}" must be indented under the source class "${sourceName}"`,
|
|
284
|
+
'warning',
|
|
285
|
+
),
|
|
286
|
+
);
|
|
265
287
|
continue;
|
|
266
288
|
}
|
|
267
289
|
|
|
@@ -380,6 +402,10 @@ export function looksLikeClassDiagram(content: string): boolean {
|
|
|
380
402
|
if (/^[+\-#]?\s*\w+.*[:(]/.test(trimmed)) {
|
|
381
403
|
hasIndentedMember = true;
|
|
382
404
|
}
|
|
405
|
+
// Indented relationship arrows
|
|
406
|
+
if (INDENT_REL_ARROW_RE.test(trimmed)) {
|
|
407
|
+
hasRelationship = true;
|
|
408
|
+
}
|
|
383
409
|
}
|
|
384
410
|
}
|
|
385
411
|
|
|
@@ -410,6 +436,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
410
436
|
const line = rawLine.trim();
|
|
411
437
|
// Skip old-style colon metadata and new-style first line / space-separated options
|
|
412
438
|
if (inMetadata && (/^[a-z-]+\s*:/i.test(line) || /^class(\s|$)/i.test(line))) continue;
|
|
439
|
+
if (inMetadata && line.toLowerCase() === 'no-auto-color') continue;
|
|
413
440
|
if (inMetadata && /^[a-z]/.test(line) && OPTION_NOCOLON_RE.test(line)) {
|
|
414
441
|
const key = line.match(OPTION_NOCOLON_RE)![1].toLowerCase();
|
|
415
442
|
if (key !== 'abstract' && key !== 'interface' && key !== 'enum') continue;
|
package/src/class/renderer.ts
CHANGED
|
@@ -82,7 +82,7 @@ const CLASS_TYPE_MAP: Record<string, ClassLegendEntry> = {
|
|
|
82
82
|
const CLASS_TYPE_ORDER = ['class', 'abstract', 'interface', 'enum'];
|
|
83
83
|
|
|
84
84
|
function collectClassTypes(parsed: ParsedClassDiagram): ClassLegendEntry[] {
|
|
85
|
-
if (parsed.options?.color
|
|
85
|
+
if (parsed.options?.['no-auto-color']) return [];
|
|
86
86
|
|
|
87
87
|
const present = new Set<string>();
|
|
88
88
|
for (const c of parsed.classes) {
|
|
@@ -500,7 +500,7 @@ export function renderClassDiagram(
|
|
|
500
500
|
|
|
501
501
|
const w = node.width;
|
|
502
502
|
const h = node.height;
|
|
503
|
-
const colorOff = parsed.options?.color
|
|
503
|
+
const colorOff = !!parsed.options?.['no-auto-color'];
|
|
504
504
|
// When legend is collapsed, use neutral color for nodes without explicit color
|
|
505
505
|
const neutralize = hasLegend && !isLegendExpanded && !node.color;
|
|
506
506
|
const effectiveColor = neutralize ? palette.primary : node.color;
|
package/src/cli.ts
CHANGED
|
@@ -199,7 +199,7 @@ Key options:
|
|
|
199
199
|
### Common to all diagrams
|
|
200
200
|
|
|
201
201
|
\`\`\`
|
|
202
|
-
|
|
202
|
+
sequence // explicit type (optional — auto-detected)
|
|
203
203
|
title: My Diagram
|
|
204
204
|
palette: catppuccin // override palette
|
|
205
205
|
|
|
@@ -372,8 +372,7 @@ When the \`dgmo\` MCP server is configured, use these tools directly:
|
|
|
372
372
|
|
|
373
373
|
### Sequence diagram
|
|
374
374
|
\`\`\`
|
|
375
|
-
|
|
376
|
-
title: Auth Flow
|
|
375
|
+
sequence Auth Flow
|
|
377
376
|
|
|
378
377
|
User -Login-> API
|
|
379
378
|
API -Find user-> DB
|
|
@@ -386,8 +385,7 @@ DB -user-> API
|
|
|
386
385
|
|
|
387
386
|
### Flowchart
|
|
388
387
|
\`\`\`
|
|
389
|
-
|
|
390
|
-
title: Process
|
|
388
|
+
flowchart Process
|
|
391
389
|
|
|
392
390
|
(Start) -> <Valid?>
|
|
393
391
|
-yes-> [Process] -> (Done)
|
|
@@ -396,8 +394,7 @@ title: Process
|
|
|
396
394
|
|
|
397
395
|
### Bar chart
|
|
398
396
|
\`\`\`
|
|
399
|
-
|
|
400
|
-
title: Revenue
|
|
397
|
+
bar Revenue
|
|
401
398
|
series: USD
|
|
402
399
|
|
|
403
400
|
North: 850
|
|
@@ -407,8 +404,7 @@ East: 1100
|
|
|
407
404
|
|
|
408
405
|
### ER diagram
|
|
409
406
|
\`\`\`
|
|
410
|
-
|
|
411
|
-
title: Schema
|
|
407
|
+
er Schema
|
|
412
408
|
|
|
413
409
|
users
|
|
414
410
|
id: int [pk]
|
|
@@ -423,7 +419,7 @@ users 1--* posts : writes
|
|
|
423
419
|
|
|
424
420
|
### Org chart
|
|
425
421
|
\`\`\`
|
|
426
|
-
|
|
422
|
+
org
|
|
427
423
|
|
|
428
424
|
CEO
|
|
429
425
|
VP Engineering
|
|
@@ -434,8 +430,7 @@ CEO
|
|
|
434
430
|
|
|
435
431
|
### Infra chart
|
|
436
432
|
\`\`\`
|
|
437
|
-
|
|
438
|
-
direction: LR
|
|
433
|
+
infra
|
|
439
434
|
|
|
440
435
|
edge
|
|
441
436
|
rps: 10000
|
|
@@ -461,7 +456,7 @@ bar, line, multi-line, area, pie, doughnut, radar, polar-area, bar-stacked, scat
|
|
|
461
456
|
|
|
462
457
|
## Common patterns
|
|
463
458
|
|
|
464
|
-
-
|
|
459
|
+
- First line: chart type keyword (e.g. \`sequence\`, \`flowchart\`, \`bar\`) — auto-detected if unambiguous
|
|
465
460
|
- \`title: text\` — diagram title
|
|
466
461
|
- \`// comment\` — only \`//\` comments (not \`#\`)
|
|
467
462
|
- \`(colorname)\` — inline colors: \`Label(red): 100\`
|
|
@@ -480,7 +475,7 @@ dgmo file.dgmo --json # structured JSON output
|
|
|
480
475
|
- Don't use \`#\` for comments — use \`//\`
|
|
481
476
|
- Don't use \`end\` to close sequence blocks — indentation closes them
|
|
482
477
|
- Don't use hex colors in section headers — use named colors
|
|
483
|
-
-
|
|
478
|
+
- Start the file with the chart type keyword when content is ambiguous
|
|
484
479
|
- Sequence arrows: \`->\` (sync), \`~>\` (async) — always left-to-right
|
|
485
480
|
|
|
486
481
|
Full reference: call \`get_language_reference\` MCP tool or visit diagrammo.app/docs
|
|
@@ -686,8 +681,8 @@ function noInput(): never {
|
|
|
686
681
|
writeFileSync(
|
|
687
682
|
samplePath,
|
|
688
683
|
[
|
|
689
|
-
'
|
|
690
|
-
'activations
|
|
684
|
+
'sequence',
|
|
685
|
+
'activations off',
|
|
691
686
|
'',
|
|
692
687
|
'Client -POST /login-> API',
|
|
693
688
|
' API -validate credentials-> Auth',
|