@diagrammo/dgmo 0.7.3 → 0.8.0
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/AGENTS.md +15 -20
- package/README.md +56 -58
- package/dist/cli.cjs +188 -181
- package/dist/index.cjs +3506 -1057
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -43
- package/dist/index.d.ts +196 -43
- package/dist/index.js +3493 -1057
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +629 -289
- package/package.json +1 -1
- package/src/c4/layout.ts +6 -9
- package/src/c4/parser.ts +189 -83
- package/src/c4/renderer.ts +8 -9
- package/src/chart.ts +296 -83
- package/src/class/parser.ts +54 -37
- package/src/class/renderer.ts +8 -8
- package/src/cli.ts +8 -8
- package/src/colors.ts +4 -1
- package/src/completion.ts +757 -10
- package/src/d3.ts +310 -73
- package/src/dgmo-router.ts +63 -8
- package/src/echarts.ts +726 -231
- package/src/er/parser.ts +94 -76
- package/src/er/renderer.ts +6 -5
- package/src/gantt/parser.ts +144 -69
- package/src/gantt/renderer.ts +50 -14
- package/src/gantt/types.ts +3 -3
- package/src/graph/flowchart-parser.ts +97 -37
- package/src/graph/flowchart-renderer.ts +4 -3
- package/src/graph/state-parser.ts +50 -31
- package/src/graph/state-renderer.ts +4 -3
- package/src/index.ts +14 -5
- package/src/infra/compute.ts +1 -0
- package/src/infra/layout.ts +3 -0
- package/src/infra/parser.ts +291 -92
- package/src/infra/renderer.ts +172 -30
- package/src/infra/types.ts +5 -0
- package/src/initiative-status/layout.ts +1 -1
- package/src/initiative-status/parser.ts +121 -47
- package/src/initiative-status/renderer.ts +42 -23
- package/src/initiative-status/types.ts +10 -2
- package/src/kanban/parser.ts +60 -37
- package/src/kanban/renderer.ts +2 -2
- package/src/kanban/types.ts +1 -0
- package/src/org/layout.ts +9 -9
- package/src/org/parser.ts +39 -40
- package/src/org/renderer.ts +5 -6
- package/src/org/resolver.ts +26 -19
- package/src/render.ts +1 -1
- package/src/sequence/parser.ts +304 -95
- package/src/sequence/renderer.ts +9 -9
- package/src/sitemap/layout.ts +3 -4
- package/src/sitemap/parser.ts +57 -49
- package/src/sitemap/renderer.ts +6 -7
- package/src/utils/arrows.ts +25 -6
- package/src/utils/duration.ts +43 -7
- package/src/utils/legend-constants.ts +26 -0
- package/src/utils/legend-svg.ts +167 -0
- package/src/utils/parsing.ts +247 -7
- package/src/utils/tag-groups.ts +160 -15
- package/src/utils/title-constants.ts +9 -0
package/src/er/parser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
-
import { measureIndent, extractColor, parsePipeMetadata,
|
|
4
|
+
import { measureIndent, extractColor, parsePipeMetadata, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
|
|
5
5
|
import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
6
6
|
import type { TagGroup } from '../utils/tag-groups';
|
|
7
7
|
import type {
|
|
@@ -27,8 +27,10 @@ function tableId(name: string): string {
|
|
|
27
27
|
// Allows lowercase, uppercase, underscores, digits — must start with letter or underscore
|
|
28
28
|
const TABLE_DECL_RE = /^([a-zA-Z_]\w*)(?:\s*\(([^)]+)\))?(?:\s*\|(.+))?$/;
|
|
29
29
|
|
|
30
|
-
// Column: name
|
|
31
|
-
|
|
30
|
+
// Column: name [type] [constraints...] — space-separated, no colon, no brackets
|
|
31
|
+
// First token is always the name. Second token is the type if it's not a constraint keyword.
|
|
32
|
+
// Remaining tokens are constraint keywords (pk, fk, unique, nullable).
|
|
33
|
+
// Handled programmatically, not with a single regex.
|
|
32
34
|
|
|
33
35
|
// Indented relationship: 1-* target or 1-label-* target
|
|
34
36
|
const INDENT_REL_RE = /^([1*?])-(?:(.+)-)?([1*?])\s+([a-zA-Z_]\w*)\s*$/;
|
|
@@ -41,6 +43,9 @@ const CONSTRAINT_MAP: Record<string, ERConstraint> = {
|
|
|
41
43
|
nullable: 'nullable',
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
// Known options (space-separated, no colon)
|
|
47
|
+
const KNOWN_OPTIONS = new Set(['notation']);
|
|
48
|
+
|
|
44
49
|
// ============================================================
|
|
45
50
|
// Cardinality parsing
|
|
46
51
|
// ============================================================
|
|
@@ -56,17 +61,17 @@ function parseCardSide(token: string): ERCardinality | null {
|
|
|
56
61
|
/**
|
|
57
62
|
* Try to parse a relationship line with symbolic cardinality.
|
|
58
63
|
*
|
|
59
|
-
* Supported form:
|
|
60
|
-
* tableName 1--* tableName
|
|
61
|
-
* tableName 1-* tableName
|
|
62
|
-
* tableName ?--1 tableName
|
|
64
|
+
* Supported form (no colon before label):
|
|
65
|
+
* tableName 1--* tableName label
|
|
66
|
+
* tableName 1-* tableName label
|
|
67
|
+
* tableName ?--1 tableName
|
|
63
68
|
*/
|
|
64
69
|
const REL_SYMBOLIC_RE =
|
|
65
|
-
/^([a-zA-Z_]\w*)\s+([1*?])\s*-{1,2}\s*([1*?])\s+([a-zA-Z_]\w*)(?:\s
|
|
70
|
+
/^([a-zA-Z_]\w*)\s+([1*?])\s*-{1,2}\s*([1*?])\s+([a-zA-Z_]\w*)(?:\s+(.+))?$/;
|
|
66
71
|
|
|
67
72
|
/** Detects keyword cardinality forms to emit helpful error */
|
|
68
73
|
const REL_KEYWORD_RE =
|
|
69
|
-
/^([a-zA-Z_]\w*)\s+(one|many|zero)[- ]to[- ](one|many|zero)\s+([a-zA-Z_]\w*)(?:\s
|
|
74
|
+
/^([a-zA-Z_]\w*)\s+(one|many|zero)[- ]to[- ](one|many|zero)\s+([a-zA-Z_]\w*)(?:\s+(.+))?$/i;
|
|
70
75
|
|
|
71
76
|
const KEYWORD_TO_SYMBOL: Record<string, string> = {
|
|
72
77
|
one: '1',
|
|
@@ -117,17 +122,39 @@ function parseRelationship(
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
// ============================================================
|
|
120
|
-
//
|
|
125
|
+
// Column parser (space-separated: name [type] [constraints...])
|
|
121
126
|
// ============================================================
|
|
122
127
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
function parseColumn(trimmed: string): {
|
|
129
|
+
name: string;
|
|
130
|
+
type?: string;
|
|
131
|
+
constraints: ERConstraint[];
|
|
132
|
+
} | null {
|
|
133
|
+
const tokens = trimmed.split(/\s+/);
|
|
134
|
+
if (tokens.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
// First token must look like a column name (word chars)
|
|
137
|
+
const name = tokens[0];
|
|
138
|
+
if (!/^\w+$/.test(name)) return null;
|
|
139
|
+
|
|
140
|
+
const constraints: ERConstraint[] = [];
|
|
141
|
+
let type: string | undefined;
|
|
142
|
+
|
|
143
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
144
|
+
const lower = tokens[i].toLowerCase();
|
|
145
|
+
const constraint = CONSTRAINT_MAP[lower];
|
|
146
|
+
if (constraint) {
|
|
147
|
+
constraints.push(constraint);
|
|
148
|
+
} else if (type === undefined) {
|
|
149
|
+
// First non-constraint token after name is the type
|
|
150
|
+
type = tokens[i];
|
|
151
|
+
} else {
|
|
152
|
+
// Unknown token after type — not a valid column line
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
129
155
|
}
|
|
130
|
-
|
|
156
|
+
|
|
157
|
+
return { name, type, constraints };
|
|
131
158
|
}
|
|
132
159
|
|
|
133
160
|
// ============================================================
|
|
@@ -167,6 +194,7 @@ export function parseERDiagram(
|
|
|
167
194
|
let contentStarted = false;
|
|
168
195
|
let currentTagGroup: TagGroup | null = null;
|
|
169
196
|
const aliasMap = new Map<string, string>();
|
|
197
|
+
let firstLineParsed = false;
|
|
170
198
|
|
|
171
199
|
function getOrCreateTable(name: string, lineNumber: number): ERTable {
|
|
172
200
|
const id = tableId(name);
|
|
@@ -200,13 +228,29 @@ export function parseERDiagram(
|
|
|
200
228
|
// Skip comments
|
|
201
229
|
if (trimmed.startsWith('//')) continue;
|
|
202
230
|
|
|
203
|
-
//
|
|
231
|
+
// First line: chart type + optional title
|
|
232
|
+
if (!firstLineParsed && indent === 0) {
|
|
233
|
+
const firstLineResult = parseFirstLine(trimmed);
|
|
234
|
+
if (firstLineResult && firstLineResult.chartType === 'er') {
|
|
235
|
+
firstLineParsed = true;
|
|
236
|
+
if (firstLineResult.title) {
|
|
237
|
+
result.title = firstLineResult.title;
|
|
238
|
+
result.titleLineNumber = lineNumber;
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Not an explicit `er` first line — that's OK, treat as implicit
|
|
243
|
+
firstLineParsed = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Tag group heading — `tag Name` or deprecated `## Name`
|
|
204
247
|
if (!contentStarted && indent === 0) {
|
|
205
248
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
206
249
|
if (tagBlockMatch) {
|
|
207
250
|
if (tagBlockMatch.deprecated) {
|
|
208
251
|
result.diagnostics.push(makeDgmoError(lineNumber,
|
|
209
|
-
`'## ${tagBlockMatch.name}' is
|
|
252
|
+
`'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
|
|
253
|
+
continue;
|
|
210
254
|
}
|
|
211
255
|
currentTagGroup = {
|
|
212
256
|
name: tagBlockMatch.name,
|
|
@@ -222,19 +266,16 @@ export function parseERDiagram(
|
|
|
222
266
|
}
|
|
223
267
|
}
|
|
224
268
|
|
|
225
|
-
// Tag group entries (indented under tag
|
|
269
|
+
// Tag group entries (indented under tag heading)
|
|
226
270
|
if (currentTagGroup && !contentStarted && indent > 0) {
|
|
227
|
-
const
|
|
228
|
-
const entryText = isDefault
|
|
229
|
-
? trimmed.replace(/\s+default\s*$/, '').trim()
|
|
230
|
-
: trimmed;
|
|
231
|
-
const { label, color } = extractColor(entryText, palette);
|
|
271
|
+
const { label, color } = extractColor(trimmed, palette);
|
|
232
272
|
if (!color) {
|
|
233
273
|
result.diagnostics.push(makeDgmoError(lineNumber,
|
|
234
274
|
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'`, 'warning'));
|
|
235
275
|
continue;
|
|
236
276
|
}
|
|
237
|
-
|
|
277
|
+
// First entry becomes the default
|
|
278
|
+
if (currentTagGroup.entries.length === 0) {
|
|
238
279
|
currentTagGroup.defaultValue = label;
|
|
239
280
|
}
|
|
240
281
|
currentTagGroup.entries.push({ value: label, color, lineNumber });
|
|
@@ -246,36 +287,17 @@ export function parseERDiagram(
|
|
|
246
287
|
currentTagGroup = null;
|
|
247
288
|
}
|
|
248
289
|
|
|
249
|
-
//
|
|
250
|
-
if (!contentStarted && indent === 0
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
let msg = `Expected chart type "er", got "${value}"`;
|
|
259
|
-
const hint = suggest(value.toLowerCase(), allTypes);
|
|
260
|
-
if (hint) msg += `. ${hint}`;
|
|
261
|
-
return fail(lineNumber, msg);
|
|
290
|
+
// Options (space-separated, no colon) — before content
|
|
291
|
+
if (!contentStarted && indent === 0) {
|
|
292
|
+
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
293
|
+
if (optMatch) {
|
|
294
|
+
const key = optMatch[1].toLowerCase();
|
|
295
|
+
const value = optMatch[2].trim();
|
|
296
|
+
if (KNOWN_OPTIONS.has(key)) {
|
|
297
|
+
result.options[key] = value.toLowerCase();
|
|
298
|
+
continue;
|
|
262
299
|
}
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (key === 'title') {
|
|
267
|
-
result.title = value;
|
|
268
|
-
result.titleLineNumber = lineNumber;
|
|
269
|
-
continue;
|
|
270
300
|
}
|
|
271
|
-
|
|
272
|
-
if (key === 'notation') {
|
|
273
|
-
result.options.notation = value.toLowerCase();
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Unknown single-word keys are metadata — skip
|
|
278
|
-
if (!/\s/.test(key)) continue;
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
// Indented lines = columns or relationships of current table
|
|
@@ -299,17 +321,12 @@ export function parseERDiagram(
|
|
|
299
321
|
continue;
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
const colName = colMatch[1];
|
|
305
|
-
const colType = colMatch[2]?.trim();
|
|
306
|
-
const constraintRaw = colMatch[3];
|
|
307
|
-
const constraints = constraintRaw ? parseConstraints(constraintRaw) : [];
|
|
308
|
-
|
|
324
|
+
const colResult = parseColumn(trimmed);
|
|
325
|
+
if (colResult) {
|
|
309
326
|
currentTable.columns.push({
|
|
310
|
-
name:
|
|
311
|
-
...(
|
|
312
|
-
constraints,
|
|
327
|
+
name: colResult.name,
|
|
328
|
+
...(colResult.type && { type: colResult.type }),
|
|
329
|
+
constraints: colResult.constraints,
|
|
313
330
|
lineNumber,
|
|
314
331
|
});
|
|
315
332
|
}
|
|
@@ -350,10 +367,8 @@ export function parseERDiagram(
|
|
|
350
367
|
// Parse pipe metadata: TableName(color) | key: value, key2: value2
|
|
351
368
|
const pipeStr = tableDecl[3]?.trim();
|
|
352
369
|
if (pipeStr) {
|
|
353
|
-
// Split on additional pipes (treated as commas) and warn if found
|
|
354
370
|
const pipeSegments = pipeStr.split('|');
|
|
355
|
-
const meta = parsePipeMetadata(['', ...pipeSegments], aliasMap
|
|
356
|
-
() => result.diagnostics.push(makeDgmoError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning')));
|
|
371
|
+
const meta = parsePipeMetadata(['', ...pipeSegments], aliasMap);
|
|
357
372
|
Object.assign(table.metadata, meta);
|
|
358
373
|
}
|
|
359
374
|
|
|
@@ -415,9 +430,12 @@ export function parseERDiagram(
|
|
|
415
430
|
// Detection helper
|
|
416
431
|
// ============================================================
|
|
417
432
|
|
|
433
|
+
// Column detection for looksLikeERDiagram: space-separated with constraint keywords
|
|
434
|
+
const CONSTRAINT_KEYWORD_RE = /\b(pk|fk)\b/i;
|
|
435
|
+
|
|
418
436
|
/**
|
|
419
|
-
* Detect if content looks like an ER diagram without explicit `
|
|
420
|
-
* Looks for indented lines with
|
|
437
|
+
* Detect if content looks like an ER diagram without explicit `er` first line.
|
|
438
|
+
* Looks for indented lines with pk or fk constraint keywords.
|
|
421
439
|
*/
|
|
422
440
|
export function looksLikeERDiagram(content: string): boolean {
|
|
423
441
|
const lines = content.split('\n');
|
|
@@ -430,14 +448,14 @@ export function looksLikeERDiagram(content: string): boolean {
|
|
|
430
448
|
const trimmed = line.trim();
|
|
431
449
|
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
432
450
|
|
|
433
|
-
// Skip metadata
|
|
434
|
-
if (/^(
|
|
451
|
+
// Skip metadata (both old colon and new space-separated)
|
|
452
|
+
if (/^(er|notation)\s/i.test(trimmed) || /^er$/i.test(trimmed)) continue;
|
|
435
453
|
|
|
436
454
|
const indent = measureIndent(line);
|
|
437
455
|
|
|
438
456
|
if (indent > 0) {
|
|
439
|
-
// Indented line with
|
|
440
|
-
if (
|
|
457
|
+
// Indented line with pk or fk is strong ER signal
|
|
458
|
+
if (CONSTRAINT_KEYWORD_RE.test(trimmed)) {
|
|
441
459
|
hasConstraint = true;
|
|
442
460
|
}
|
|
443
461
|
// Indented relationship is a strong ER signal
|
|
@@ -456,7 +474,7 @@ export function looksLikeERDiagram(content: string): boolean {
|
|
|
456
474
|
}
|
|
457
475
|
}
|
|
458
476
|
|
|
459
|
-
//
|
|
477
|
+
// pk/fk constraint is a strong enough signal
|
|
460
478
|
if (hasConstraint && hasTableDecl) return true;
|
|
461
479
|
|
|
462
480
|
// Relationship with table declarations is sufficient
|
|
@@ -480,8 +498,8 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
480
498
|
let inMetadata = true;
|
|
481
499
|
for (const rawLine of docText.split('\n')) {
|
|
482
500
|
const line = rawLine.trim();
|
|
483
|
-
if (inMetadata && /^
|
|
484
|
-
if (inMetadata &&
|
|
501
|
+
if (inMetadata && /^er(\s|$)/i.test(line)) continue;
|
|
502
|
+
if (inMetadata && OPTION_NOCOLON_RE.test(line)) continue; // option line
|
|
485
503
|
inMetadata = false;
|
|
486
504
|
if (line.length === 0) continue;
|
|
487
505
|
if (/^\s/.test(rawLine)) continue; // indented = column definition, not table
|
package/src/er/renderer.ts
CHANGED
|
@@ -13,14 +13,15 @@ import {
|
|
|
13
13
|
LEGEND_HEIGHT,
|
|
14
14
|
LEGEND_PILL_PAD,
|
|
15
15
|
LEGEND_PILL_FONT_SIZE,
|
|
16
|
-
LEGEND_PILL_FONT_W,
|
|
17
16
|
LEGEND_CAPSULE_PAD,
|
|
18
17
|
LEGEND_DOT_R,
|
|
19
18
|
LEGEND_ENTRY_FONT_SIZE,
|
|
20
19
|
LEGEND_ENTRY_DOT_GAP,
|
|
21
20
|
LEGEND_ENTRY_TRAIL,
|
|
22
21
|
LEGEND_GROUP_GAP,
|
|
22
|
+
measureLegendText,
|
|
23
23
|
} from '../utils/legend-constants';
|
|
24
|
+
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
|
|
24
25
|
import type { ParsedERDiagram, ERConstraint } from './types';
|
|
25
26
|
import type { ERLayoutResult } from './layout';
|
|
26
27
|
import { parseERDiagram } from './parser';
|
|
@@ -286,11 +287,11 @@ export function renderERDiagram(
|
|
|
286
287
|
.append('text')
|
|
287
288
|
.attr('class', 'chart-title')
|
|
288
289
|
.attr('x', viewW / 2)
|
|
289
|
-
.attr('y',
|
|
290
|
+
.attr('y', TITLE_Y)
|
|
290
291
|
.attr('text-anchor', 'middle')
|
|
291
292
|
.attr('fill', palette.text)
|
|
292
|
-
.attr('font-size',
|
|
293
|
-
.attr('font-weight',
|
|
293
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
294
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
294
295
|
.style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
|
|
295
296
|
.text(parsed.title);
|
|
296
297
|
|
|
@@ -626,7 +627,7 @@ export function renderERDiagram(
|
|
|
626
627
|
: mix(palette.surface, palette.bg, 30);
|
|
627
628
|
|
|
628
629
|
const groupName = 'Role';
|
|
629
|
-
const pillWidth = groupName
|
|
630
|
+
const pillWidth = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
630
631
|
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
631
632
|
|
|
632
633
|
let totalWidth: number;
|