@diagrammo/dgmo 0.2.26 → 0.2.28
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 +107 -0
- package/.claude/skills/dgmo-flowchart/SKILL.md +61 -0
- package/.claude/skills/dgmo-generate/SKILL.md +58 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +83 -0
- package/.cursorrules +117 -0
- package/.github/copilot-instructions.md +117 -0
- package/.windsurfrules +117 -0
- package/README.md +10 -3
- package/dist/cli.cjs +116 -108
- package/dist/index.cjs +563 -356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -24
- package/dist/index.d.ts +39 -24
- package/dist/index.js +560 -355
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +125 -0
- package/docs/language-reference.md +784 -0
- package/package.json +10 -3
- package/src/c4/parser.ts +90 -74
- package/src/c4/renderer.ts +13 -12
- package/src/c4/types.ts +6 -4
- package/src/chart.ts +3 -2
- package/src/class/parser.ts +2 -10
- package/src/class/types.ts +1 -1
- package/src/cli.ts +135 -19
- package/src/d3.ts +1 -1
- package/src/dgmo-mermaid.ts +1 -1
- package/src/dgmo-router.ts +1 -1
- package/src/echarts.ts +33 -13
- package/src/er/parser.ts +34 -43
- package/src/er/types.ts +1 -1
- package/src/graph/flowchart-parser.ts +2 -25
- package/src/graph/types.ts +1 -1
- package/src/index.ts +5 -0
- package/src/initiative-status/parser.ts +57 -11
- package/src/initiative-status/types.ts +1 -1
- package/src/kanban/parser.ts +32 -53
- package/src/kanban/renderer.ts +9 -8
- package/src/kanban/types.ts +6 -14
- package/src/org/parser.ts +47 -87
- package/src/org/resolver.ts +11 -12
- package/src/sequence/parser.ts +97 -15
- package/src/sequence/renderer.ts +62 -69
- package/src/utils/arrows.ts +75 -0
- package/src/utils/inline-markdown.ts +75 -0
- package/src/utils/parsing.ts +67 -0
- package/src/utils/tag-groups.ts +76 -0
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { execSync } from 'node:child_process';
|
|
|
3
3
|
import { resolve, basename, extname } from 'node:path';
|
|
4
4
|
import { Resvg } from '@resvg/resvg-js';
|
|
5
5
|
import { render } from './render';
|
|
6
|
-
import { parseDgmo } from './dgmo-router';
|
|
6
|
+
import { parseDgmo, DGMO_CHART_TYPE_MAP } from './dgmo-router';
|
|
7
7
|
import { parseDgmoChartType } from './dgmo-router';
|
|
8
8
|
import { formatDgmoError } from './diagnostics';
|
|
9
9
|
import { getPalette } from './palettes/registry';
|
|
@@ -24,6 +24,38 @@ const PALETTES = [
|
|
|
24
24
|
|
|
25
25
|
const THEMES = ['light', 'dark', 'transparent'] as const;
|
|
26
26
|
|
|
27
|
+
const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
28
|
+
bar: 'Bar chart — categorical comparisons',
|
|
29
|
+
line: 'Line chart — trends over time',
|
|
30
|
+
'multi-line': 'Multi-line chart — multiple series trends',
|
|
31
|
+
area: 'Area chart — filled line chart',
|
|
32
|
+
pie: 'Pie chart — part-to-whole proportions',
|
|
33
|
+
doughnut: 'Doughnut chart — ring-style pie chart',
|
|
34
|
+
radar: 'Radar chart — multi-dimensional metrics',
|
|
35
|
+
'polar-area': 'Polar area chart — radial bar chart',
|
|
36
|
+
'bar-stacked': 'Stacked bar chart — multi-series categorical',
|
|
37
|
+
scatter: 'Scatter plot — 2D data points or bubble chart',
|
|
38
|
+
sankey: 'Sankey diagram — flow/allocation visualization',
|
|
39
|
+
chord: 'Chord diagram — circular flow relationships',
|
|
40
|
+
function: 'Function plot — mathematical expressions',
|
|
41
|
+
heatmap: 'Heatmap — matrix intensity visualization',
|
|
42
|
+
funnel: 'Funnel chart — conversion pipeline',
|
|
43
|
+
slope: 'Slope chart — change between two periods',
|
|
44
|
+
wordcloud: 'Word cloud — term frequency visualization',
|
|
45
|
+
arc: 'Arc diagram — network relationships',
|
|
46
|
+
timeline: 'Timeline — events, eras, and date ranges',
|
|
47
|
+
venn: 'Venn diagram — set overlaps',
|
|
48
|
+
quadrant: 'Quadrant chart — 2x2 positioning matrix',
|
|
49
|
+
sequence: 'Sequence diagram — message/interaction flows',
|
|
50
|
+
flowchart: 'Flowchart — decision trees and process flows',
|
|
51
|
+
class: 'Class diagram — UML class hierarchies',
|
|
52
|
+
er: 'ER diagram — database schemas and relationships',
|
|
53
|
+
org: 'Org chart — hierarchical tree structures',
|
|
54
|
+
kanban: 'Kanban board — task/workflow columns',
|
|
55
|
+
c4: 'C4 diagram — system architecture (context, container, component, deployment)',
|
|
56
|
+
'initiative-status': 'Initiative status — project roadmap with dependency tracking',
|
|
57
|
+
};
|
|
58
|
+
|
|
27
59
|
function printHelp(): void {
|
|
28
60
|
console.log(`Usage: dgmo <input> [options]
|
|
29
61
|
cat input.dgmo | dgmo [options]
|
|
@@ -42,6 +74,8 @@ Options:
|
|
|
42
74
|
--c4-container <name> Container to drill into (with --c4-level components)
|
|
43
75
|
--no-branding Omit diagrammo.app branding from exports
|
|
44
76
|
--copy Copy URL to clipboard (only with -o url)
|
|
77
|
+
--json Output structured JSON to stdout
|
|
78
|
+
--chart-types List all supported chart types
|
|
45
79
|
--help Show this help
|
|
46
80
|
--version Show version`);
|
|
47
81
|
}
|
|
@@ -62,6 +96,8 @@ function parseArgs(argv: string[]): {
|
|
|
62
96
|
version: boolean;
|
|
63
97
|
noBranding: boolean;
|
|
64
98
|
copy: boolean;
|
|
99
|
+
json: boolean;
|
|
100
|
+
chartTypes: boolean;
|
|
65
101
|
c4Level: 'context' | 'containers' | 'components' | 'deployment';
|
|
66
102
|
c4System: string | undefined;
|
|
67
103
|
c4Container: string | undefined;
|
|
@@ -75,6 +111,8 @@ function parseArgs(argv: string[]): {
|
|
|
75
111
|
version: false,
|
|
76
112
|
noBranding: false,
|
|
77
113
|
copy: false,
|
|
114
|
+
json: false,
|
|
115
|
+
chartTypes: false,
|
|
78
116
|
c4Level: 'context' as 'context' | 'containers' | 'components' | 'deployment',
|
|
79
117
|
c4System: undefined as string | undefined,
|
|
80
118
|
c4Container: undefined as string | undefined,
|
|
@@ -134,6 +172,12 @@ function parseArgs(argv: string[]): {
|
|
|
134
172
|
} else if (arg === '--no-branding') {
|
|
135
173
|
result.noBranding = true;
|
|
136
174
|
i++;
|
|
175
|
+
} else if (arg === '--json') {
|
|
176
|
+
result.json = true;
|
|
177
|
+
i++;
|
|
178
|
+
} else if (arg === '--chart-types') {
|
|
179
|
+
result.chartTypes = true;
|
|
180
|
+
i++;
|
|
137
181
|
} else if (arg === '--copy') {
|
|
138
182
|
result.copy = true;
|
|
139
183
|
i++;
|
|
@@ -220,6 +264,23 @@ async function main(): Promise<void> {
|
|
|
220
264
|
return;
|
|
221
265
|
}
|
|
222
266
|
|
|
267
|
+
if (opts.chartTypes) {
|
|
268
|
+
const types = Object.keys(DGMO_CHART_TYPE_MAP);
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
const chartTypes = types.map((id) => ({
|
|
271
|
+
id,
|
|
272
|
+
description: CHART_TYPE_DESCRIPTIONS[id] ?? id,
|
|
273
|
+
}));
|
|
274
|
+
process.stdout.write(JSON.stringify({ chartTypes }, null, 2) + '\n');
|
|
275
|
+
} else {
|
|
276
|
+
for (const id of types) {
|
|
277
|
+
const desc = CHART_TYPE_DESCRIPTIONS[id];
|
|
278
|
+
console.log(desc ? `${id} — ${desc.split(' — ')[1]}` : id);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
223
284
|
// Determine input source
|
|
224
285
|
let content: string;
|
|
225
286
|
let inputBasename: string | undefined;
|
|
@@ -249,6 +310,11 @@ async function main(): Promise<void> {
|
|
|
249
310
|
noInput();
|
|
250
311
|
}
|
|
251
312
|
|
|
313
|
+
// Strip any ANSI escape codes that may have leaked into input
|
|
314
|
+
// (e.g. from shell aliases like cat=bat with --color always)
|
|
315
|
+
// eslint-disable-next-line no-control-regex
|
|
316
|
+
content = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
317
|
+
|
|
252
318
|
// Resolve org chart imports (tags: and import: directives)
|
|
253
319
|
if (opts.input && parseDgmoChartType(content) === 'org') {
|
|
254
320
|
const inputPath = resolve(opts.input);
|
|
@@ -272,14 +338,30 @@ async function main(): Promise<void> {
|
|
|
272
338
|
process.exit(1);
|
|
273
339
|
}
|
|
274
340
|
|
|
341
|
+
const chartType = parseDgmoChartType(content);
|
|
342
|
+
|
|
343
|
+
// Helper for JSON error output
|
|
344
|
+
function exitWithJsonError(error: string, line?: number): never {
|
|
345
|
+
if (opts.json) {
|
|
346
|
+
process.stdout.write(JSON.stringify({
|
|
347
|
+
success: false,
|
|
348
|
+
error,
|
|
349
|
+
...(line != null ? { line } : {}),
|
|
350
|
+
...(chartType ? { chartType } : {}),
|
|
351
|
+
}, null, 2) + '\n');
|
|
352
|
+
} else {
|
|
353
|
+
console.error(error);
|
|
354
|
+
}
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
275
358
|
// URL output — encode DSL directly, no rendering needed
|
|
276
359
|
if (format === 'url') {
|
|
277
360
|
const result = encodeDiagramUrl(content);
|
|
278
361
|
if (result.error) {
|
|
279
|
-
|
|
362
|
+
exitWithJsonError(
|
|
280
363
|
`Error: Diagram too large for URL sharing (${result.compressedSize} bytes, limit ${result.limit} bytes)`
|
|
281
364
|
);
|
|
282
|
-
process.exit(1);
|
|
283
365
|
}
|
|
284
366
|
|
|
285
367
|
if (opts.copy) {
|
|
@@ -298,7 +380,15 @@ async function main(): Promise<void> {
|
|
|
298
380
|
}
|
|
299
381
|
}
|
|
300
382
|
|
|
301
|
-
|
|
383
|
+
if (opts.json) {
|
|
384
|
+
process.stdout.write(JSON.stringify({
|
|
385
|
+
success: true,
|
|
386
|
+
url: result.url,
|
|
387
|
+
...(chartType ? { chartType } : {}),
|
|
388
|
+
}, null, 2) + '\n');
|
|
389
|
+
} else {
|
|
390
|
+
process.stdout.write(result.url + '\n');
|
|
391
|
+
}
|
|
302
392
|
return;
|
|
303
393
|
}
|
|
304
394
|
|
|
@@ -308,10 +398,9 @@ async function main(): Promise<void> {
|
|
|
308
398
|
// which are unavailable in Node.js — check before attempting render.
|
|
309
399
|
const wordcloudRe = /^\s*chart\s*:\s*wordcloud\b/im;
|
|
310
400
|
if (wordcloudRe.test(content)) {
|
|
311
|
-
|
|
401
|
+
exitWithJsonError(
|
|
312
402
|
'Error: Word clouds are not supported in the CLI (requires Canvas). Use the desktop app or browser instead.'
|
|
313
403
|
);
|
|
314
|
-
process.exit(1);
|
|
315
404
|
}
|
|
316
405
|
|
|
317
406
|
// Parse first to collect diagnostics
|
|
@@ -320,28 +409,36 @@ async function main(): Promise<void> {
|
|
|
320
409
|
const warnings = diagnostics.filter((d) => d.severity === 'warning');
|
|
321
410
|
|
|
322
411
|
// Print warnings even if rendering succeeds
|
|
323
|
-
|
|
324
|
-
|
|
412
|
+
if (!opts.json) {
|
|
413
|
+
for (const w of warnings) {
|
|
414
|
+
console.error(`\u26A0 ${formatDgmoError(w)}`);
|
|
415
|
+
}
|
|
325
416
|
}
|
|
326
417
|
|
|
327
|
-
// Print errors
|
|
328
|
-
|
|
329
|
-
|
|
418
|
+
// Print errors and exit
|
|
419
|
+
if (errors.length > 0) {
|
|
420
|
+
if (opts.json) {
|
|
421
|
+
const firstError = errors[0];
|
|
422
|
+
exitWithJsonError(
|
|
423
|
+
formatDgmoError(firstError),
|
|
424
|
+
firstError.line,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
for (const e of errors) {
|
|
428
|
+
console.error(`\u2716 ${formatDgmoError(e)}`);
|
|
429
|
+
}
|
|
330
430
|
}
|
|
331
431
|
|
|
332
432
|
// Validate C4 options
|
|
333
433
|
if (opts.c4Level === 'containers' && !opts.c4System) {
|
|
334
|
-
|
|
335
|
-
process.exit(1);
|
|
434
|
+
exitWithJsonError('Error: --c4-system is required when --c4-level is containers');
|
|
336
435
|
}
|
|
337
436
|
if (opts.c4Level === 'components') {
|
|
338
437
|
if (!opts.c4System) {
|
|
339
|
-
|
|
340
|
-
process.exit(1);
|
|
438
|
+
exitWithJsonError('Error: --c4-system is required when --c4-level is components');
|
|
341
439
|
}
|
|
342
440
|
if (!opts.c4Container) {
|
|
343
|
-
|
|
344
|
-
process.exit(1);
|
|
441
|
+
exitWithJsonError('Error: --c4-container is required when --c4-level is components');
|
|
345
442
|
}
|
|
346
443
|
}
|
|
347
444
|
|
|
@@ -356,7 +453,7 @@ async function main(): Promise<void> {
|
|
|
356
453
|
|
|
357
454
|
if (!svg) {
|
|
358
455
|
if (errors.length === 0) {
|
|
359
|
-
|
|
456
|
+
exitWithJsonError(
|
|
360
457
|
'Error: Failed to render diagram. The input may be empty, invalid, or use an unsupported chart type.'
|
|
361
458
|
);
|
|
362
459
|
}
|
|
@@ -366,7 +463,26 @@ async function main(): Promise<void> {
|
|
|
366
463
|
// Determine output destination
|
|
367
464
|
const pngBg = opts.theme === 'transparent' ? undefined : paletteColors.bg;
|
|
368
465
|
|
|
369
|
-
if (opts.
|
|
466
|
+
if (opts.json) {
|
|
467
|
+
// JSON mode: write file as normal but output JSON result to stdout
|
|
468
|
+
let outputPath: string | undefined;
|
|
469
|
+
if (opts.output) {
|
|
470
|
+
outputPath = resolve(opts.output);
|
|
471
|
+
if (format === 'svg') {
|
|
472
|
+
writeFileSync(outputPath, svg, 'utf-8');
|
|
473
|
+
} else {
|
|
474
|
+
writeFileSync(outputPath, svgToPng(svg, pngBg));
|
|
475
|
+
}
|
|
476
|
+
} else if (inputBasename) {
|
|
477
|
+
outputPath = resolve(`${inputBasename}.png`);
|
|
478
|
+
writeFileSync(outputPath, svgToPng(svg, pngBg));
|
|
479
|
+
}
|
|
480
|
+
process.stdout.write(JSON.stringify({
|
|
481
|
+
success: true,
|
|
482
|
+
...(outputPath ? { output: outputPath } : {}),
|
|
483
|
+
...(chartType ? { chartType } : {}),
|
|
484
|
+
}, null, 2) + '\n');
|
|
485
|
+
} else if (opts.output) {
|
|
370
486
|
// Explicit output path
|
|
371
487
|
const outputPath = resolve(opts.output);
|
|
372
488
|
if (format === 'svg') {
|
package/src/d3.ts
CHANGED
package/src/dgmo-mermaid.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function parseQuadrant(content: string): ParsedQuadrant {
|
|
|
82
82
|
const lineNumber = i + 1; // 1-indexed for editor
|
|
83
83
|
|
|
84
84
|
// Skip empty lines and comments
|
|
85
|
-
if (!line || line.startsWith('
|
|
85
|
+
if (!line || line.startsWith('//')) continue;
|
|
86
86
|
|
|
87
87
|
// Skip the chart: directive (already consumed by router)
|
|
88
88
|
if (/^chart\s*:/i.test(line)) continue;
|
package/src/dgmo-router.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function parseDgmoChartType(content: string): string | null {
|
|
|
81
81
|
for (const line of lines) {
|
|
82
82
|
const trimmed = line.trim();
|
|
83
83
|
// Skip empty lines and comments
|
|
84
|
-
if (!trimmed || trimmed.startsWith('
|
|
84
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
85
85
|
continue;
|
|
86
86
|
const match = trimmed.match(/^chart\s*:\s*(.+)/i);
|
|
87
87
|
if (match) return match[1].trim().toLowerCase();
|
package/src/echarts.ts
CHANGED
|
@@ -75,7 +75,7 @@ export interface ParsedEChart {
|
|
|
75
75
|
showLabels?: boolean;
|
|
76
76
|
categoryColors?: Record<string, string>;
|
|
77
77
|
diagnostics: DgmoError[];
|
|
78
|
-
error
|
|
78
|
+
error: string | null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ============================================================
|
|
@@ -116,6 +116,7 @@ export function parseEChart(
|
|
|
116
116
|
type: 'scatter',
|
|
117
117
|
data: [],
|
|
118
118
|
diagnostics: [],
|
|
119
|
+
error: null,
|
|
119
120
|
};
|
|
120
121
|
|
|
121
122
|
// Track current category for grouped scatter charts
|
|
@@ -144,7 +145,7 @@ export function parseEChart(
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// Skip comments
|
|
147
|
-
if (trimmed.startsWith('
|
|
148
|
+
if (trimmed.startsWith('//')) continue;
|
|
148
149
|
|
|
149
150
|
// Check for category header: [Category Name]
|
|
150
151
|
const categoryMatch = trimmed.match(/^\[(.+)\]$/);
|
|
@@ -964,7 +965,7 @@ function buildScatterOption(
|
|
|
964
965
|
},
|
|
965
966
|
}),
|
|
966
967
|
grid: {
|
|
967
|
-
left: parsed.ylabel ? '
|
|
968
|
+
left: parsed.ylabel ? '12%' : '3%',
|
|
968
969
|
right: '4%',
|
|
969
970
|
bottom: hasCategories ? '15%' : parsed.xlabel ? '10%' : '3%',
|
|
970
971
|
top: parsed.title ? '15%' : '5%',
|
|
@@ -1289,18 +1290,29 @@ function makeGridAxis(
|
|
|
1289
1290
|
splitLineColor: string,
|
|
1290
1291
|
gridOpacity: number,
|
|
1291
1292
|
label?: string,
|
|
1292
|
-
data?: string[]
|
|
1293
|
+
data?: string[],
|
|
1294
|
+
nameGapOverride?: number
|
|
1293
1295
|
): Record<string, unknown> {
|
|
1296
|
+
const defaultGap = type === 'value' ? 75 : 40;
|
|
1294
1297
|
return {
|
|
1295
1298
|
type,
|
|
1296
1299
|
...(data && { data }),
|
|
1297
1300
|
axisLine: { lineStyle: { color: axisLineColor } },
|
|
1298
|
-
axisLabel: {
|
|
1301
|
+
axisLabel: {
|
|
1302
|
+
color: textColor,
|
|
1303
|
+
fontSize: type === 'category' && data ? (data.length > 10 ? 11 : data.length > 5 ? 12 : 16) : 16,
|
|
1304
|
+
fontFamily: FONT_FAMILY,
|
|
1305
|
+
...(type === 'category' && {
|
|
1306
|
+
interval: 0,
|
|
1307
|
+
formatter: (value: string) =>
|
|
1308
|
+
value.replace(/([a-z])([A-Z])/g, '$1\n$2').replace(/ /g, '\n'),
|
|
1309
|
+
}),
|
|
1310
|
+
},
|
|
1299
1311
|
splitLine: { lineStyle: { color: splitLineColor, opacity: gridOpacity } },
|
|
1300
1312
|
...(label && {
|
|
1301
1313
|
name: label,
|
|
1302
1314
|
nameLocation: 'middle',
|
|
1303
|
-
nameGap:
|
|
1315
|
+
nameGap: nameGapOverride ?? defaultGap,
|
|
1304
1316
|
nameTextStyle: { color: textColor, fontSize: 18, fontFamily: FONT_FAMILY },
|
|
1305
1317
|
}),
|
|
1306
1318
|
};
|
|
@@ -1385,7 +1397,12 @@ function buildBarOption(
|
|
|
1385
1397
|
itemStyle: { color: d.color ?? colors[i % colors.length] },
|
|
1386
1398
|
}));
|
|
1387
1399
|
|
|
1388
|
-
|
|
1400
|
+
// When category labels are on the y-axis (horizontal bars), they can be wide —
|
|
1401
|
+
// compute a nameGap that clears the longest label so the ylabel doesn't overlap.
|
|
1402
|
+
const hCatGap = isHorizontal && yLabel
|
|
1403
|
+
? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
|
|
1404
|
+
: undefined;
|
|
1405
|
+
const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap);
|
|
1389
1406
|
const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel);
|
|
1390
1407
|
|
|
1391
1408
|
// xAxis is always the bottom axis, yAxis is always the left axis in ECharts
|
|
@@ -1400,7 +1417,7 @@ function buildBarOption(
|
|
|
1400
1417
|
axisPointer: { type: 'shadow' },
|
|
1401
1418
|
},
|
|
1402
1419
|
grid: {
|
|
1403
|
-
left: yLabel ? '
|
|
1420
|
+
left: yLabel ? '12%' : '3%',
|
|
1404
1421
|
right: '4%',
|
|
1405
1422
|
bottom: xLabel ? '10%' : '3%',
|
|
1406
1423
|
top: parsed.title ? '15%' : '5%',
|
|
@@ -1448,7 +1465,7 @@ function buildLineOption(
|
|
|
1448
1465
|
axisPointer: { type: 'line' },
|
|
1449
1466
|
},
|
|
1450
1467
|
grid: {
|
|
1451
|
-
left: yLabel ? '
|
|
1468
|
+
left: yLabel ? '12%' : '3%',
|
|
1452
1469
|
right: '4%',
|
|
1453
1470
|
bottom: xLabel ? '10%' : '3%',
|
|
1454
1471
|
top: parsed.title ? '15%' : '5%',
|
|
@@ -1524,7 +1541,7 @@ function buildMultiLineOption(
|
|
|
1524
1541
|
textStyle: { color: textColor },
|
|
1525
1542
|
},
|
|
1526
1543
|
grid: {
|
|
1527
|
-
left: yLabel ? '
|
|
1544
|
+
left: yLabel ? '12%' : '3%',
|
|
1528
1545
|
right: '4%',
|
|
1529
1546
|
bottom: '15%',
|
|
1530
1547
|
top: parsed.title ? '15%' : '5%',
|
|
@@ -1563,7 +1580,7 @@ function buildAreaOption(
|
|
|
1563
1580
|
axisPointer: { type: 'line' },
|
|
1564
1581
|
},
|
|
1565
1582
|
grid: {
|
|
1566
|
-
left: yLabel ? '
|
|
1583
|
+
left: yLabel ? '12%' : '3%',
|
|
1567
1584
|
right: '4%',
|
|
1568
1585
|
bottom: xLabel ? '10%' : '3%',
|
|
1569
1586
|
top: parsed.title ? '15%' : '5%',
|
|
@@ -1807,7 +1824,10 @@ function buildBarStackedOption(
|
|
|
1807
1824
|
};
|
|
1808
1825
|
});
|
|
1809
1826
|
|
|
1810
|
-
const
|
|
1827
|
+
const hCatGap = isHorizontal && yLabel
|
|
1828
|
+
? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
|
|
1829
|
+
: undefined;
|
|
1830
|
+
const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap);
|
|
1811
1831
|
const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel);
|
|
1812
1832
|
|
|
1813
1833
|
return {
|
|
@@ -1825,7 +1845,7 @@ function buildBarStackedOption(
|
|
|
1825
1845
|
textStyle: { color: textColor },
|
|
1826
1846
|
},
|
|
1827
1847
|
grid: {
|
|
1828
|
-
left: yLabel ? '
|
|
1848
|
+
left: yLabel ? '12%' : '3%',
|
|
1829
1849
|
right: '4%',
|
|
1830
1850
|
bottom: '15%',
|
|
1831
1851
|
top: parsed.title ? '15%' : '5%',
|
package/src/er/parser.ts
CHANGED
|
@@ -1,6 +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 } from '../utils/parsing';
|
|
4
5
|
import type {
|
|
5
6
|
ParsedERDiagram,
|
|
6
7
|
ERTable,
|
|
@@ -13,16 +14,6 @@ import type {
|
|
|
13
14
|
// Helpers
|
|
14
15
|
// ============================================================
|
|
15
16
|
|
|
16
|
-
function measureIndent(line: string): number {
|
|
17
|
-
let indent = 0;
|
|
18
|
-
for (const ch of line) {
|
|
19
|
-
if (ch === ' ') indent++;
|
|
20
|
-
else if (ch === '\t') indent += 4;
|
|
21
|
-
else break;
|
|
22
|
-
}
|
|
23
|
-
return indent;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
function tableId(name: string): string {
|
|
27
18
|
return name.toLowerCase().trim();
|
|
28
19
|
}
|
|
@@ -50,42 +41,39 @@ const CONSTRAINT_MAP: Record<string, ERConstraint> = {
|
|
|
50
41
|
// Cardinality parsing
|
|
51
42
|
// ============================================================
|
|
52
43
|
|
|
53
|
-
// Cardinality keyword map
|
|
54
|
-
const CARD_WORD: Record<string, ERCardinality> = {
|
|
55
|
-
one: '1',
|
|
56
|
-
many: '*',
|
|
57
|
-
'1': '1',
|
|
58
|
-
'*': '*',
|
|
59
|
-
'?': '?',
|
|
60
|
-
zero: '?',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
44
|
/**
|
|
64
|
-
* Parse a cardinality side token (
|
|
45
|
+
* Parse a cardinality side token (symbolic only: "1", "*", "?").
|
|
65
46
|
*/
|
|
66
47
|
function parseCardSide(token: string): ERCardinality | null {
|
|
67
|
-
|
|
48
|
+
if (token === '1' || token === '*' || token === '?') return token;
|
|
49
|
+
return null;
|
|
68
50
|
}
|
|
69
51
|
|
|
70
52
|
/**
|
|
71
|
-
* Try to parse a relationship line with cardinality.
|
|
53
|
+
* Try to parse a relationship line with symbolic cardinality.
|
|
72
54
|
*
|
|
73
|
-
* Supported
|
|
55
|
+
* Supported form:
|
|
74
56
|
* tableName 1--* tableName : label
|
|
75
57
|
* tableName 1-* tableName : label
|
|
76
|
-
* tableName one-to-many tableName : label
|
|
77
|
-
* tableName one to many tableName : label
|
|
78
|
-
* tableName 1 to many tableName : label
|
|
79
58
|
* tableName ?--1 tableName : label
|
|
80
59
|
*/
|
|
81
60
|
const REL_SYMBOLIC_RE =
|
|
82
61
|
/^([a-zA-Z_]\w*)\s+([1*?])\s*-{1,2}\s*([1*?])\s+([a-zA-Z_]\w*)(?:\s*:\s*(.+))?$/;
|
|
83
62
|
|
|
63
|
+
/** Detects keyword cardinality forms to emit helpful error */
|
|
84
64
|
const REL_KEYWORD_RE =
|
|
85
|
-
/^([a-zA-Z_]\w*)\s+(one|many|zero
|
|
65
|
+
/^([a-zA-Z_]\w*)\s+(one|many|zero)[- ]to[- ](one|many|zero)\s+([a-zA-Z_]\w*)(?:\s*:\s*(.+))?$/i;
|
|
66
|
+
|
|
67
|
+
const KEYWORD_TO_SYMBOL: Record<string, string> = {
|
|
68
|
+
one: '1',
|
|
69
|
+
many: '*',
|
|
70
|
+
zero: '?',
|
|
71
|
+
};
|
|
86
72
|
|
|
87
73
|
function parseRelationship(
|
|
88
|
-
trimmed: string
|
|
74
|
+
trimmed: string,
|
|
75
|
+
lineNumber: number,
|
|
76
|
+
pushError: (line: number, message: string) => void,
|
|
89
77
|
): {
|
|
90
78
|
source: string;
|
|
91
79
|
target: string;
|
|
@@ -109,20 +97,16 @@ function parseRelationship(
|
|
|
109
97
|
}
|
|
110
98
|
}
|
|
111
99
|
|
|
112
|
-
// Keyword / natural:
|
|
100
|
+
// Keyword / natural: produce helpful error with symbolic suggestion
|
|
113
101
|
const kw = trimmed.match(REL_KEYWORD_RE);
|
|
114
102
|
if (kw) {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
to: toCard,
|
|
123
|
-
label: kw[5]?.trim(),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
103
|
+
const fromSym = KEYWORD_TO_SYMBOL[kw[2].toLowerCase()] ?? kw[2];
|
|
104
|
+
const toSym = KEYWORD_TO_SYMBOL[kw[3].toLowerCase()] ?? kw[3];
|
|
105
|
+
pushError(
|
|
106
|
+
lineNumber,
|
|
107
|
+
`Use symbolic cardinality (1--*, ?--1, *--*) instead of "${kw[2]}-to-${kw[3]}". Example: ${kw[1]} ${fromSym}--${toSym} ${kw[4]}`,
|
|
108
|
+
);
|
|
109
|
+
return null;
|
|
126
110
|
}
|
|
127
111
|
|
|
128
112
|
return null;
|
|
@@ -157,6 +141,7 @@ export function parseERDiagram(
|
|
|
157
141
|
tables: [],
|
|
158
142
|
relationships: [],
|
|
159
143
|
diagnostics: [],
|
|
144
|
+
error: null,
|
|
160
145
|
};
|
|
161
146
|
|
|
162
147
|
const fail = (line: number, message: string): ParsedERDiagram => {
|
|
@@ -166,6 +151,12 @@ export function parseERDiagram(
|
|
|
166
151
|
return result;
|
|
167
152
|
};
|
|
168
153
|
|
|
154
|
+
const pushError = (line: number, message: string): void => {
|
|
155
|
+
const diag = makeDgmoError(line, message);
|
|
156
|
+
result.diagnostics.push(diag);
|
|
157
|
+
if (!result.error) result.error = formatDgmoError(diag);
|
|
158
|
+
};
|
|
159
|
+
|
|
169
160
|
const tableMap = new Map<string, ERTable>();
|
|
170
161
|
let currentTable: ERTable | null = null;
|
|
171
162
|
let contentStarted = false;
|
|
@@ -257,7 +248,7 @@ export function parseERDiagram(
|
|
|
257
248
|
contentStarted = true;
|
|
258
249
|
|
|
259
250
|
// Try relationship
|
|
260
|
-
const rel = parseRelationship(trimmed);
|
|
251
|
+
const rel = parseRelationship(trimmed, lineNumber, pushError);
|
|
261
252
|
if (rel) {
|
|
262
253
|
getOrCreateTable(rel.source, lineNumber);
|
|
263
254
|
getOrCreateTable(rel.target, lineNumber);
|
|
@@ -347,7 +338,7 @@ export function looksLikeERDiagram(content: string): boolean {
|
|
|
347
338
|
hasTableDecl = true;
|
|
348
339
|
}
|
|
349
340
|
// Check for relationship patterns
|
|
350
|
-
if (REL_SYMBOLIC_RE.test(trimmed)
|
|
341
|
+
if (REL_SYMBOLIC_RE.test(trimmed)) {
|
|
351
342
|
hasRelationship = true;
|
|
352
343
|
}
|
|
353
344
|
}
|
package/src/er/types.ts
CHANGED
|
@@ -1,6 +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 } from '../utils/parsing';
|
|
4
5
|
import type {
|
|
5
6
|
ParsedGraph,
|
|
6
7
|
GraphNode,
|
|
@@ -14,16 +15,6 @@ import type {
|
|
|
14
15
|
// Helpers
|
|
15
16
|
// ============================================================
|
|
16
17
|
|
|
17
|
-
function measureIndent(line: string): number {
|
|
18
|
-
let indent = 0;
|
|
19
|
-
for (const ch of line) {
|
|
20
|
-
if (ch === ' ') indent++;
|
|
21
|
-
else if (ch === '\t') indent += 4;
|
|
22
|
-
else break;
|
|
23
|
-
}
|
|
24
|
-
return indent;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
18
|
function nodeId(shape: GraphShape, label: string): string {
|
|
28
19
|
return `${shape}:${label.toLowerCase().trim()}`;
|
|
29
20
|
}
|
|
@@ -35,21 +26,6 @@ interface NodeRef {
|
|
|
35
26
|
color?: string;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
|
|
39
|
-
|
|
40
|
-
function extractColor(
|
|
41
|
-
label: string,
|
|
42
|
-
palette?: PaletteColors
|
|
43
|
-
): { label: string; color?: string } {
|
|
44
|
-
const m = label.match(COLOR_SUFFIX_RE);
|
|
45
|
-
if (!m) return { label };
|
|
46
|
-
const colorName = m[1].trim();
|
|
47
|
-
return {
|
|
48
|
-
label: label.substring(0, m.index!).trim(),
|
|
49
|
-
color: resolveColor(colorName, palette),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
/**
|
|
54
30
|
* Try to parse a node reference from a text fragment.
|
|
55
31
|
* Order matters: subroutine & document before process.
|
|
@@ -239,6 +215,7 @@ export function parseFlowchart(
|
|
|
239
215
|
edges: [],
|
|
240
216
|
options: {},
|
|
241
217
|
diagnostics: [],
|
|
218
|
+
error: null,
|
|
242
219
|
};
|
|
243
220
|
|
|
244
221
|
const fail = (line: number, message: string): ParsedGraph => {
|
package/src/graph/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -126,6 +126,11 @@ export type {
|
|
|
126
126
|
GraphDirection,
|
|
127
127
|
} from './graph/types';
|
|
128
128
|
|
|
129
|
+
export type { TagGroup, TagEntry } from './utils/tag-groups';
|
|
130
|
+
|
|
131
|
+
export { parseInlineMarkdown, truncateBareUrl } from './utils/inline-markdown';
|
|
132
|
+
export type { InlineSpan } from './utils/inline-markdown';
|
|
133
|
+
|
|
129
134
|
export { parseOrg } from './org/parser';
|
|
130
135
|
export type {
|
|
131
136
|
ParsedOrg,
|