@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.
Files changed (42) hide show
  1. package/dist/cli.cjs +89 -130
  2. package/dist/index.cjs +1202 -993
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +216 -114
  5. package/dist/index.d.ts +216 -114
  6. package/dist/index.js +1211 -985
  7. package/dist/index.js.map +1 -1
  8. package/docs/language-reference.md +73 -0
  9. package/package.json +22 -9
  10. package/src/boxes-and-lines/parser.ts +8 -3
  11. package/src/c4/parser.ts +8 -7
  12. package/src/class/parser.ts +6 -0
  13. package/src/cli.ts +1 -9
  14. package/src/d3.ts +16 -234
  15. package/src/dgmo-router.ts +97 -5
  16. package/src/diagnostics.ts +16 -6
  17. package/src/echarts.ts +43 -10
  18. package/src/er/parser.ts +22 -2
  19. package/src/gantt/renderer.ts +153 -91
  20. package/src/graph/flowchart-parser.ts +89 -52
  21. package/src/graph/state-parser.ts +60 -35
  22. package/src/index.ts +23 -18
  23. package/src/infra/parser.ts +9 -2
  24. package/src/kanban/renderer.ts +2 -2
  25. package/src/palettes/color-utils.ts +4 -12
  26. package/src/palettes/index.ts +0 -4
  27. package/src/render.ts +30 -16
  28. package/src/sequence/collapse.ts +169 -0
  29. package/src/sequence/parser.ts +21 -4
  30. package/src/sequence/renderer.ts +198 -52
  31. package/src/sharing.ts +86 -49
  32. package/src/sitemap/renderer.ts +1 -6
  33. package/src/utils/arrows.ts +180 -11
  34. package/src/utils/d3-types.ts +4 -0
  35. package/src/utils/legend-constants.ts +11 -4
  36. package/src/utils/legend-d3.ts +171 -0
  37. package/src/utils/legend-layout.ts +140 -13
  38. package/src/utils/legend-types.ts +45 -0
  39. package/src/utils/time-ticks.ts +213 -0
  40. package/src/branding.ts +0 -67
  41. package/src/dgmo-mermaid.ts +0 -262
  42. package/src/palettes/mermaid-bridge.ts +0 -220
@@ -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
- // No chart type detected try visualization parser as fallback (it handles missing chart: line)
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) return { diagnostics: directParser(content).diagnostics };
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
- return { diagnostics: parseChart(content).diagnostics };
272
+ const result = parseChart(content);
273
+ return {
274
+ diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
275
+ };
249
276
  }
250
277
  if (ECHART_TYPES.has(chartType)) {
251
- return { diagnostics: parseExtendedChart(content).diagnostics };
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
- return { diagnostics: parseVisualization(content).diagnostics };
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
  }
@@ -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 { line, message, severity };
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(input: string, candidates: readonly string[]): string | null {
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: sym[5]?.trim(),
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
- ...(indentRel[2]?.trim() && { label: indentRel[2].trim() }),
355
+ ...(rawLabel && { label: rawLabel }),
336
356
  lineNumber,
337
357
  });
338
358
  }