@diagrammo/dgmo 0.8.18 → 0.8.20
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 +89 -130
- package/dist/index.cjs +1202 -993
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -114
- package/dist/index.d.ts +216 -114
- package/dist/index.js +1211 -985
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +73 -0
- package/package.json +22 -9
- package/src/boxes-and-lines/parser.ts +8 -3
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/d3.ts +16 -234
- package/src/dgmo-router.ts +97 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/er/parser.ts +22 -2
- package/src/gantt/renderer.ts +153 -91
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/state-parser.ts +60 -35
- package/src/index.ts +23 -18
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +2 -2
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +30 -16
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +21 -4
- package/src/sequence/renderer.ts +198 -52
- package/src/sharing.ts +86 -49
- package/src/sitemap/renderer.ts +1 -6
- package/src/utils/arrows.ts +180 -11
- package/src/utils/d3-types.ts +4 -0
- package/src/utils/legend-constants.ts +11 -4
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +140 -13
- package/src/utils/legend-types.ts +45 -0
- package/src/utils/time-ticks.ts +213 -0
- package/src/branding.ts +0 -67
- package/src/dgmo-mermaid.ts +0 -262
- package/src/palettes/mermaid-bridge.ts +0 -220
package/src/dgmo-router.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { parseInfra } from './infra/parser';
|
|
|
18
18
|
import { parseGantt } from './gantt/parser';
|
|
19
19
|
import { parseBoxesAndLines } from './boxes-and-lines/parser';
|
|
20
20
|
import { parseFirstLine } from './utils/parsing';
|
|
21
|
+
import { makeDgmoError, suggest } from './diagnostics';
|
|
21
22
|
import type { DgmoError } from './diagnostics';
|
|
22
23
|
|
|
23
24
|
// ============================================================
|
|
@@ -229,6 +230,17 @@ const PARSE_DISPATCH = new Map<
|
|
|
229
230
|
['boxes-and-lines', (c) => parseBoxesAndLines(c)],
|
|
230
231
|
]);
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Parse DGMO content and return diagnostics without rendering.
|
|
235
|
+
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
236
|
+
*/
|
|
237
|
+
/** All known chart type names for colon-pattern detection. */
|
|
238
|
+
const ALL_KNOWN_TYPES = new Set([
|
|
239
|
+
...DATA_CHART_TYPES,
|
|
240
|
+
...VISUALIZATION_TYPES,
|
|
241
|
+
...DIAGRAM_TYPES,
|
|
242
|
+
]);
|
|
243
|
+
|
|
232
244
|
/**
|
|
233
245
|
* Parse DGMO content and return diagnostics without rendering.
|
|
234
246
|
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
@@ -237,20 +249,100 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
|
237
249
|
const chartType = parseDgmoChartType(content);
|
|
238
250
|
|
|
239
251
|
if (!chartType) {
|
|
240
|
-
//
|
|
252
|
+
// Check for common mistake: colon in chart type declaration (e.g. "bar: Sales")
|
|
253
|
+
const colonDiag = detectColonChartType(content);
|
|
254
|
+
if (colonDiag) {
|
|
255
|
+
const fallback = parseVisualization(content).diagnostics;
|
|
256
|
+
return { diagnostics: [colonDiag, ...fallback] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// No chart type detected — try visualization parser as fallback
|
|
241
260
|
return { diagnostics: parseVisualization(content).diagnostics };
|
|
242
261
|
}
|
|
243
262
|
|
|
244
263
|
const directParser = PARSE_DISPATCH.get(chartType);
|
|
245
|
-
if (directParser)
|
|
264
|
+
if (directParser) {
|
|
265
|
+
const result = directParser(content);
|
|
266
|
+
return {
|
|
267
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
246
270
|
|
|
247
271
|
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
248
|
-
|
|
272
|
+
const result = parseChart(content);
|
|
273
|
+
return {
|
|
274
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
275
|
+
};
|
|
249
276
|
}
|
|
250
277
|
if (ECHART_TYPES.has(chartType)) {
|
|
251
|
-
|
|
278
|
+
const result = parseExtendedChart(content);
|
|
279
|
+
return {
|
|
280
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
281
|
+
};
|
|
252
282
|
}
|
|
253
283
|
|
|
254
284
|
// Visualization types (slope, wordcloud, arc, timeline, venn, quadrant)
|
|
255
|
-
|
|
285
|
+
const result = parseVisualization(content);
|
|
286
|
+
return {
|
|
287
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================
|
|
292
|
+
// Common-mistake detectors
|
|
293
|
+
// ============================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detects colon-separated chart type declarations like "bar: Sales" or "pie: Data".
|
|
297
|
+
* Returns a diagnostic if the word before the colon is a known or similar chart type.
|
|
298
|
+
*/
|
|
299
|
+
function detectColonChartType(content: string): DgmoError | null {
|
|
300
|
+
const lines = content.split('\n');
|
|
301
|
+
for (let i = 0; i < lines.length; i++) {
|
|
302
|
+
const trimmed = lines[i].trim();
|
|
303
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//'))
|
|
304
|
+
continue;
|
|
305
|
+
|
|
306
|
+
const match = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
307
|
+
if (!match) return null; // First non-empty line doesn't match colon pattern
|
|
308
|
+
|
|
309
|
+
const word = match[1].toLowerCase();
|
|
310
|
+
const rest = match[2].trim();
|
|
311
|
+
|
|
312
|
+
if (ALL_KNOWN_TYPES.has(word)) {
|
|
313
|
+
const example = rest ? `${word} ${rest}` : word;
|
|
314
|
+
return makeDgmoError(
|
|
315
|
+
i + 1,
|
|
316
|
+
`Remove the colon — use '${example}' instead of '${trimmed}'. DGMO chart types don't use colons.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if it's a misspelling of a known type
|
|
321
|
+
const hint = suggest(word, [...ALL_KNOWN_TYPES]);
|
|
322
|
+
if (hint) {
|
|
323
|
+
return makeDgmoError(
|
|
324
|
+
i + 1,
|
|
325
|
+
`Unknown chart type: ${word}. ${hint} Also, DGMO chart types don't use colons.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return null; // First line has colon but isn't a chart type — normal data
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Detects when content has only the chart type line with no meaningful data lines.
|
|
336
|
+
*/
|
|
337
|
+
function detectEmptyContent(content: string): DgmoError[] {
|
|
338
|
+
const lines = content.split('\n');
|
|
339
|
+
const nonEmpty = lines.filter(
|
|
340
|
+
(l) => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//')
|
|
341
|
+
);
|
|
342
|
+
if (nonEmpty.length <= 1) {
|
|
343
|
+
return [
|
|
344
|
+
makeDgmoError(1, 'No content after chart type declaration.', 'warning'),
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
return [];
|
|
256
348
|
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -9,14 +9,23 @@ export interface DgmoError {
|
|
|
9
9
|
column?: number; // optional 1-based column
|
|
10
10
|
message: string; // without "Line N:" prefix
|
|
11
11
|
severity: DgmoSeverity;
|
|
12
|
+
/**
|
|
13
|
+
* Optional stable diagnostic code (e.g. 'E_ARROW_SUBSTRING_IN_LABEL').
|
|
14
|
+
* Additive; pre-existing diagnostics omit this field and existing
|
|
15
|
+
* substring-on-`.message` assertions keep working unchanged.
|
|
16
|
+
*/
|
|
17
|
+
code?: string;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export function makeDgmoError(
|
|
15
21
|
line: number,
|
|
16
22
|
message: string,
|
|
17
|
-
severity: DgmoSeverity = 'error'
|
|
23
|
+
severity: DgmoSeverity = 'error',
|
|
24
|
+
code?: string
|
|
18
25
|
): DgmoError {
|
|
19
|
-
return
|
|
26
|
+
return code !== undefined
|
|
27
|
+
? { line, message, severity, code }
|
|
28
|
+
: { line, message, severity };
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
export function formatDgmoError(err: DgmoError): string {
|
|
@@ -43,9 +52,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
43
52
|
for (let j = 1; j <= n; j++) {
|
|
44
53
|
const tmp = dp[j];
|
|
45
54
|
dp[j] =
|
|
46
|
-
a[i - 1] === b[j - 1]
|
|
47
|
-
? prev
|
|
48
|
-
: 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
55
|
+
a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
49
56
|
prev = tmp;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -57,7 +64,10 @@ function levenshtein(a: string, b: string): number {
|
|
|
57
64
|
* Returns null if no good match is found.
|
|
58
65
|
* Threshold: distance ≤ max(2, floor(input.length / 3))
|
|
59
66
|
*/
|
|
60
|
-
export function suggest(
|
|
67
|
+
export function suggest(
|
|
68
|
+
input: string,
|
|
69
|
+
candidates: readonly string[]
|
|
70
|
+
): string | null {
|
|
61
71
|
if (!input || candidates.length === 0) return null;
|
|
62
72
|
const lower = input.toLowerCase();
|
|
63
73
|
const threshold = Math.max(2, Math.floor(lower.length / 3));
|
package/src/echarts.ts
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
|
-
import * as echarts from 'echarts';
|
|
1
|
+
import * as echarts from 'echarts/core';
|
|
2
2
|
import type { EChartsOption } from 'echarts';
|
|
3
|
+
import {
|
|
4
|
+
BarChart,
|
|
5
|
+
LineChart,
|
|
6
|
+
PieChart,
|
|
7
|
+
ScatterChart,
|
|
8
|
+
RadarChart,
|
|
9
|
+
SankeyChart,
|
|
10
|
+
GraphChart,
|
|
11
|
+
HeatmapChart,
|
|
12
|
+
FunnelChart,
|
|
13
|
+
} from 'echarts/charts';
|
|
14
|
+
import {
|
|
15
|
+
GridComponent,
|
|
16
|
+
TitleComponent,
|
|
17
|
+
TooltipComponent,
|
|
18
|
+
LegendComponent,
|
|
19
|
+
RadarComponent,
|
|
20
|
+
VisualMapComponent,
|
|
21
|
+
GraphicComponent,
|
|
22
|
+
} from 'echarts/components';
|
|
23
|
+
import { SVGRenderer } from 'echarts/renderers';
|
|
24
|
+
|
|
25
|
+
echarts.use([
|
|
26
|
+
BarChart,
|
|
27
|
+
LineChart,
|
|
28
|
+
PieChart,
|
|
29
|
+
ScatterChart,
|
|
30
|
+
RadarChart,
|
|
31
|
+
SankeyChart,
|
|
32
|
+
GraphChart,
|
|
33
|
+
HeatmapChart,
|
|
34
|
+
FunnelChart,
|
|
35
|
+
GridComponent,
|
|
36
|
+
TitleComponent,
|
|
37
|
+
TooltipComponent,
|
|
38
|
+
LegendComponent,
|
|
39
|
+
RadarComponent,
|
|
40
|
+
VisualMapComponent,
|
|
41
|
+
GraphicComponent,
|
|
42
|
+
SVGRenderer,
|
|
43
|
+
]);
|
|
3
44
|
import { FONT_FAMILY } from './fonts';
|
|
4
|
-
import { injectBranding } from './branding';
|
|
5
45
|
import { renderLegendSvg } from './utils/legend-svg';
|
|
6
46
|
import type { LegendGroupData } from './utils/legend-svg';
|
|
7
47
|
import {
|
|
@@ -2863,8 +2903,7 @@ const STANDARD_CHART_TYPES = new Set([
|
|
|
2863
2903
|
export async function renderExtendedChartForExport(
|
|
2864
2904
|
content: string,
|
|
2865
2905
|
theme: 'light' | 'dark' | 'transparent',
|
|
2866
|
-
palette?: PaletteColors
|
|
2867
|
-
options?: { branding?: boolean }
|
|
2906
|
+
palette?: PaletteColors
|
|
2868
2907
|
): Promise<string> {
|
|
2869
2908
|
const isDark = theme === 'dark';
|
|
2870
2909
|
|
|
@@ -2965,12 +3004,6 @@ export async function renderExtendedChartForExport(
|
|
|
2965
3004
|
);
|
|
2966
3005
|
}
|
|
2967
3006
|
|
|
2968
|
-
if (options?.branding !== false) {
|
|
2969
|
-
const brandColor =
|
|
2970
|
-
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
2971
|
-
result = injectBranding(result, brandColor);
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
3007
|
return result;
|
|
2975
3008
|
} finally {
|
|
2976
3009
|
chart.dispose();
|
package/src/er/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveColorWithDiagnostic } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
+
import { validateLabelCharacters } from '../utils/arrows';
|
|
4
5
|
import {
|
|
5
6
|
measureIndent,
|
|
6
7
|
extractColor,
|
|
@@ -108,12 +109,22 @@ function parseRelationship(
|
|
|
108
109
|
const fromCard = parseCardSide(sym[2]);
|
|
109
110
|
const toCard = parseCardSide(sym[3]);
|
|
110
111
|
if (fromCard && toCard) {
|
|
112
|
+
const label = sym[5]?.trim();
|
|
113
|
+
// F17: run label through validator for defense in depth. The parent
|
|
114
|
+
// loop currently discards top-level relationships as warnings, so
|
|
115
|
+
// the label never reaches the AST — but if that changes, this keeps
|
|
116
|
+
// character-set validation in sync with the indented path.
|
|
117
|
+
if (label) {
|
|
118
|
+
validateLabelCharacters(label, lineNumber).forEach((d) =>
|
|
119
|
+
pushError(d.line, d.message)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
111
122
|
return {
|
|
112
123
|
source: sym[1],
|
|
113
124
|
target: sym[4],
|
|
114
125
|
from: fromCard,
|
|
115
126
|
to: toCard,
|
|
116
|
-
label
|
|
127
|
+
label,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
}
|
|
@@ -321,6 +332,9 @@ export function parseERDiagram(
|
|
|
321
332
|
// Indented lines = columns or relationships of current table
|
|
322
333
|
if (indent > 0 && currentTable) {
|
|
323
334
|
// Try indented relationship first: 1-* target or 1-label-* target
|
|
335
|
+
// ER chart-specific constraint: labels cannot contain `-` because
|
|
336
|
+
// INDENT_REL_RE uses `-{1,2}` as hard delimiters on both sides of the
|
|
337
|
+
// label. So `1-has-*` works but `1-has dashes-*` does not.
|
|
324
338
|
const indentRel = trimmed.match(INDENT_REL_RE);
|
|
325
339
|
if (indentRel) {
|
|
326
340
|
const fromCard = parseCardSide(indentRel[1]);
|
|
@@ -328,11 +342,17 @@ export function parseERDiagram(
|
|
|
328
342
|
if (fromCard && toCard) {
|
|
329
343
|
const targetName = indentRel[4];
|
|
330
344
|
getOrCreateTable(targetName, lineNumber);
|
|
345
|
+
const rawLabel = indentRel[2]?.trim();
|
|
346
|
+
if (rawLabel) {
|
|
347
|
+
result.diagnostics.push(
|
|
348
|
+
...validateLabelCharacters(rawLabel, lineNumber)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
331
351
|
result.relationships.push({
|
|
332
352
|
source: currentTable.id,
|
|
333
353
|
target: tableId(targetName),
|
|
334
354
|
cardinality: { from: fromCard, to: toCard },
|
|
335
|
-
...(
|
|
355
|
+
...(rawLabel && { label: rawLabel }),
|
|
336
356
|
lineNumber,
|
|
337
357
|
});
|
|
338
358
|
}
|