@diagrammo/dgmo 0.8.19 → 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 +681 -872
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -103
- package/dist/index.d.ts +110 -103
- package/dist/index.js +693 -864
- 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 +2 -2
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/state-parser.ts +60 -35
- package/src/index.ts +13 -16
- 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/parser.ts +7 -2
- package/src/sequence/renderer.ts +12 -3
- 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 +0 -4
- 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/index.ts
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
export { makeDgmoError, formatDgmoError } from './diagnostics';
|
|
6
6
|
export type { DgmoError, DgmoSeverity } from './diagnostics';
|
|
7
7
|
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Arrow helpers (in-arrow label validation)
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
parseInArrowLabel,
|
|
14
|
+
validateLabelCharacters,
|
|
15
|
+
matchColorParens,
|
|
16
|
+
ARROW_DIAGNOSTIC_CODES,
|
|
17
|
+
} from './utils/arrows';
|
|
18
|
+
export type { ParseInArrowLabelResult } from './utils/arrows';
|
|
19
|
+
|
|
8
20
|
// ============================================================
|
|
9
21
|
// Unified API
|
|
10
22
|
// ============================================================
|
|
@@ -38,9 +50,9 @@ export {
|
|
|
38
50
|
orderArcNodes,
|
|
39
51
|
parseTimelineDate,
|
|
40
52
|
addDurationToDate,
|
|
41
|
-
computeTimeTicks,
|
|
42
53
|
formatDateLabel,
|
|
43
54
|
} from './d3';
|
|
55
|
+
export { computeTimeTicks } from './utils/time-ticks';
|
|
44
56
|
export type {
|
|
45
57
|
ParsedVisualization,
|
|
46
58
|
VisualizationType,
|
|
@@ -73,9 +85,6 @@ export {
|
|
|
73
85
|
RULE_COUNT,
|
|
74
86
|
} from './sequence/participant-inference';
|
|
75
87
|
|
|
76
|
-
export { parseQuadrant } from './dgmo-mermaid';
|
|
77
|
-
export type { ParsedQuadrant } from './dgmo-mermaid';
|
|
78
|
-
|
|
79
88
|
export { parseFlowchart, looksLikeFlowchart } from './graph/flowchart-parser';
|
|
80
89
|
|
|
81
90
|
export { parseState, looksLikeState } from './graph/state-parser';
|
|
@@ -378,8 +387,6 @@ export type {
|
|
|
378
387
|
LegendHandle,
|
|
379
388
|
LegendPalette,
|
|
380
389
|
} from './utils/legend-types';
|
|
381
|
-
export { buildMermaidQuadrant } from './dgmo-mermaid';
|
|
382
|
-
|
|
383
390
|
// ============================================================
|
|
384
391
|
// Renderers (produce SVG output)
|
|
385
392
|
// ============================================================
|
|
@@ -437,7 +444,6 @@ export {
|
|
|
437
444
|
hexToHSL,
|
|
438
445
|
hslToHex,
|
|
439
446
|
hexToHSLString,
|
|
440
|
-
mute,
|
|
441
447
|
tint,
|
|
442
448
|
shade,
|
|
443
449
|
getSeriesColors,
|
|
@@ -453,9 +459,6 @@ export {
|
|
|
453
459
|
boldPalette,
|
|
454
460
|
draculaPalette,
|
|
455
461
|
monokaiPalette,
|
|
456
|
-
// Mermaid bridge
|
|
457
|
-
buildMermaidThemeVars,
|
|
458
|
-
buildThemeCSS,
|
|
459
462
|
} from './palettes';
|
|
460
463
|
|
|
461
464
|
export type { PaletteConfig, PaletteColors } from './palettes';
|
|
@@ -500,9 +503,3 @@ export type {
|
|
|
500
503
|
} from './completion';
|
|
501
504
|
|
|
502
505
|
export { parseFirstLine, ALL_CHART_TYPES } from './utils/parsing';
|
|
503
|
-
|
|
504
|
-
// ============================================================
|
|
505
|
-
// Branding
|
|
506
|
-
// ============================================================
|
|
507
|
-
|
|
508
|
-
export { injectBranding } from './branding';
|
package/src/infra/parser.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
10
10
|
import { resolveColorWithDiagnostic } from '../colors';
|
|
11
|
+
import { parseInArrowLabel } from '../utils/arrows';
|
|
11
12
|
import {
|
|
12
13
|
measureIndent,
|
|
13
14
|
parseFirstLine,
|
|
@@ -477,7 +478,10 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
477
478
|
// Async labeled connection: ~label~> Target
|
|
478
479
|
const asyncConnMatch = trimmed.match(ASYNC_CONNECTION_RE);
|
|
479
480
|
if (asyncConnMatch) {
|
|
480
|
-
const
|
|
481
|
+
const rawLabel = asyncConnMatch[1] ?? '';
|
|
482
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
483
|
+
result.diagnostics.push(...labelResult.diagnostics);
|
|
484
|
+
const label = labelResult.label ?? '';
|
|
481
485
|
const targetRaw = asyncConnMatch[2].trim();
|
|
482
486
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
483
487
|
const targetName = pipeMeta.clean || targetRaw;
|
|
@@ -551,7 +555,10 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
551
555
|
// Labeled connection: -label-> Target | split: N%, fanout: 3
|
|
552
556
|
const connMatch = trimmed.match(CONNECTION_RE);
|
|
553
557
|
if (connMatch) {
|
|
554
|
-
const
|
|
558
|
+
const rawLabel = connMatch[1] ?? '';
|
|
559
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
560
|
+
result.diagnostics.push(...labelResult.diagnostics);
|
|
561
|
+
const label = labelResult.label ?? '';
|
|
555
562
|
const targetRaw = connMatch[2].trim();
|
|
556
563
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
557
564
|
const targetName = pipeMeta.clean || targetRaw;
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
// Public options object
|
|
29
29
|
// ============================================================
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
interface KanbanInteractiveOptions {
|
|
32
32
|
onNavigateToLine?: (line: number) => void;
|
|
33
33
|
exportDims?: { width: number; height: number };
|
|
34
34
|
activeTagGroup?: string | null;
|
|
@@ -608,7 +608,7 @@ interface SwimlaneBucket {
|
|
|
608
608
|
cellsByColumn: Record<string, KanbanCard[]>;
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
|
|
611
|
+
function bucketCardsBySwimlane(
|
|
612
612
|
columns: KanbanColumn[],
|
|
613
613
|
swimlaneGroup: KanbanTagGroup
|
|
614
614
|
): SwimlaneBucket[] {
|
|
@@ -84,17 +84,6 @@ export function hexToHSLString(hex: string): string {
|
|
|
84
84
|
// Color Manipulation
|
|
85
85
|
// ============================================================
|
|
86
86
|
|
|
87
|
-
/**
|
|
88
|
-
* Derive a muted (desaturated, darkened) variant of a color.
|
|
89
|
-
* Used by the Mermaid theme generator for dark-mode fills.
|
|
90
|
-
*
|
|
91
|
-
* Algorithm: cap saturation at 35% and lightness at 36%.
|
|
92
|
-
*/
|
|
93
|
-
export function mute(hex: string): string {
|
|
94
|
-
const { h, s, l } = hexToHSL(hex);
|
|
95
|
-
return hslToHex(h, Math.min(s, 35), Math.min(l, 36));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
87
|
/**
|
|
99
88
|
* Blend a color toward white (light mode quadrant fills).
|
|
100
89
|
* amount: 0 = original, 1 = white
|
|
@@ -232,7 +221,10 @@ export function getSeriesColors(palette: PaletteColors): string[] {
|
|
|
232
221
|
* saturation and lightness, guaranteeing every segment gets a unique,
|
|
233
222
|
* perceptually distinct color regardless of segment count.
|
|
234
223
|
*/
|
|
235
|
-
export function getSegmentColors(
|
|
224
|
+
export function getSegmentColors(
|
|
225
|
+
palette: PaletteColors,
|
|
226
|
+
count: number
|
|
227
|
+
): string[] {
|
|
236
228
|
const base = getSeriesColors(palette);
|
|
237
229
|
const unique = [...new Set(base)];
|
|
238
230
|
const hsls = unique.map(hexToHSL);
|
package/src/palettes/index.ts
CHANGED
|
@@ -14,7 +14,6 @@ export {
|
|
|
14
14
|
hexToHSL,
|
|
15
15
|
hslToHex,
|
|
16
16
|
hexToHSLString,
|
|
17
|
-
mute,
|
|
18
17
|
tint,
|
|
19
18
|
shade,
|
|
20
19
|
getSeriesColors,
|
|
@@ -34,6 +33,3 @@ export { tokyoNightPalette } from './tokyo-night';
|
|
|
34
33
|
|
|
35
34
|
export { draculaPalette } from './dracula';
|
|
36
35
|
export { monokaiPalette } from './monokai';
|
|
37
|
-
|
|
38
|
-
// Re-export Mermaid bridge
|
|
39
|
-
export { buildMermaidThemeVars, buildThemeCSS } from './mermaid-bridge';
|
package/src/render.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { renderForExport } from './d3';
|
|
2
2
|
import { renderExtendedChartForExport } from './echarts';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseDgmoChartType,
|
|
5
|
+
getRenderCategory,
|
|
6
|
+
parseDgmo,
|
|
7
|
+
} from './dgmo-router';
|
|
8
|
+
import type { DgmoError } from './diagnostics';
|
|
4
9
|
import { getPalette } from './palettes/registry';
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -45,13 +50,13 @@ async function ensureDom(): Promise<void> {
|
|
|
45
50
|
*
|
|
46
51
|
* @param content - DGMO source text
|
|
47
52
|
* @param options - Optional theme and palette settings
|
|
48
|
-
* @returns SVG string,
|
|
53
|
+
* @returns Object with `svg` (SVG string, empty on error) and `diagnostics` (parse errors/warnings)
|
|
49
54
|
*
|
|
50
55
|
* @example
|
|
51
56
|
* ```ts
|
|
52
57
|
* import { render } from '@diagrammo/dgmo';
|
|
53
58
|
*
|
|
54
|
-
* const svg = await render(`pie Languages
|
|
59
|
+
* const { svg, diagnostics } = await render(`pie Languages
|
|
55
60
|
* TypeScript: 45
|
|
56
61
|
* Python: 30
|
|
57
62
|
* Rust: 25`);
|
|
@@ -62,7 +67,6 @@ export async function render(
|
|
|
62
67
|
options?: {
|
|
63
68
|
theme?: 'light' | 'dark' | 'transparent';
|
|
64
69
|
palette?: string;
|
|
65
|
-
branding?: boolean;
|
|
66
70
|
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
67
71
|
c4System?: string;
|
|
68
72
|
c4Container?: string;
|
|
@@ -70,14 +74,15 @@ export async function render(
|
|
|
70
74
|
/** Legend state for export — controls which tag group is shown in exported SVG. */
|
|
71
75
|
legendState?: { activeGroup?: string; hiddenAttributes?: string[] };
|
|
72
76
|
}
|
|
73
|
-
): Promise<string> {
|
|
77
|
+
): Promise<{ svg: string; diagnostics: DgmoError[] }> {
|
|
74
78
|
const theme = options?.theme ?? 'light';
|
|
75
79
|
const paletteName = options?.palette ?? 'nord';
|
|
76
|
-
const branding = options?.branding ?? false;
|
|
77
80
|
|
|
78
81
|
const paletteColors =
|
|
79
82
|
getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
|
|
80
83
|
|
|
84
|
+
const { diagnostics } = parseDgmo(content);
|
|
85
|
+
|
|
81
86
|
const chartType = parseDgmoChartType(content);
|
|
82
87
|
const category = chartType ? getRenderCategory(chartType) : null;
|
|
83
88
|
|
|
@@ -92,18 +97,27 @@ export async function render(
|
|
|
92
97
|
: undefined;
|
|
93
98
|
|
|
94
99
|
if (category === 'data-chart') {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const svg = await renderExtendedChartForExport(
|
|
101
|
+
content,
|
|
102
|
+
theme,
|
|
103
|
+
paletteColors
|
|
104
|
+
);
|
|
105
|
+
return { svg, diagnostics };
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
// Visualization/diagram and unknown/null types all go through the unified renderer
|
|
101
109
|
await ensureDom();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
const svg = await renderForExport(
|
|
111
|
+
content,
|
|
112
|
+
theme,
|
|
113
|
+
paletteColors,
|
|
114
|
+
legendExportState,
|
|
115
|
+
{
|
|
116
|
+
c4Level: options?.c4Level,
|
|
117
|
+
c4System: options?.c4System,
|
|
118
|
+
c4Container: options?.c4Container,
|
|
119
|
+
tagGroup: options?.tagGroup,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return { svg, diagnostics };
|
|
109
123
|
}
|
package/src/sequence/parser.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { inferParticipantType } from './participant-inference';
|
|
6
6
|
import type { DgmoError } from '../diagnostics';
|
|
7
7
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
8
|
-
import { parseArrow } from '../utils/arrows';
|
|
8
|
+
import { parseArrow, parseInArrowLabel } from '../utils/arrows';
|
|
9
9
|
import {
|
|
10
10
|
measureIndent,
|
|
11
11
|
extractColor,
|
|
@@ -945,9 +945,14 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
945
945
|
}
|
|
946
946
|
if (labeledArrow) {
|
|
947
947
|
contentStarted = true;
|
|
948
|
-
const { from, to, label, async: isAsync } = labeledArrow;
|
|
948
|
+
const { from, to, label: rawLabel, async: isAsync } = labeledArrow;
|
|
949
949
|
lastMsgFrom = from;
|
|
950
950
|
|
|
951
|
+
// TD-13/TD-14: validate in-arrow label characters
|
|
952
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
953
|
+
labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
|
|
954
|
+
const label = labelResult.label ?? rawLabel;
|
|
955
|
+
|
|
951
956
|
const msg: SequenceMessage = {
|
|
952
957
|
from,
|
|
953
958
|
to,
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -2365,7 +2365,9 @@ export function renderSequenceDiagram(
|
|
|
2365
2365
|
if (tagKey && msgTagValue) {
|
|
2366
2366
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2367
2367
|
}
|
|
2368
|
-
|
|
2368
|
+
// TD-1: in-arrow labels render as plain text (no markdown interpretation).
|
|
2369
|
+
// Fixes the `location[]`-style silent character drop.
|
|
2370
|
+
labelEl.text(step.label);
|
|
2369
2371
|
}
|
|
2370
2372
|
} else {
|
|
2371
2373
|
// Normal call arrow — snap to activation box edges
|
|
@@ -2433,7 +2435,9 @@ export function renderSequenceDiagram(
|
|
|
2433
2435
|
if (tagKey && msgTagValue) {
|
|
2434
2436
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2435
2437
|
}
|
|
2436
|
-
|
|
2438
|
+
// TD-1: in-arrow labels render as plain text (no markdown interpretation).
|
|
2439
|
+
// Fixes the `location[]`-style silent character drop.
|
|
2440
|
+
labelEl.text(step.label);
|
|
2437
2441
|
}
|
|
2438
2442
|
}
|
|
2439
2443
|
} else {
|
|
@@ -2505,7 +2509,12 @@ export function renderSequenceDiagram(
|
|
|
2505
2509
|
if (tagKey && msgTagValue) {
|
|
2506
2510
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2507
2511
|
}
|
|
2508
|
-
|
|
2512
|
+
// TD-1: in-arrow labels render as plain text (no markdown
|
|
2513
|
+
// interpretation). Return-arrow labels are currently always empty
|
|
2514
|
+
// (buildRenderSequence sets them to '') but this path is kept in
|
|
2515
|
+
// sync with the call/self-call sites above to prevent a future
|
|
2516
|
+
// change resurrecting the location[] silent-drop bug.
|
|
2517
|
+
labelEl.text(step.label);
|
|
2509
2518
|
}
|
|
2510
2519
|
}
|
|
2511
2520
|
});
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -704,8 +704,6 @@ export async function renderSitemapForExport(
|
|
|
704
704
|
const { parseSitemap } = await import('./parser');
|
|
705
705
|
const { layoutSitemap } = await import('./layout');
|
|
706
706
|
const { getPalette } = await import('../palettes');
|
|
707
|
-
const { injectBranding } = await import('../branding');
|
|
708
|
-
|
|
709
707
|
const isDark = theme === 'dark';
|
|
710
708
|
const effectivePalette =
|
|
711
709
|
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
@@ -756,8 +754,5 @@ export async function renderSitemapForExport(
|
|
|
756
754
|
|
|
757
755
|
const svgHtml = svgEl.outerHTML;
|
|
758
756
|
document.body.removeChild(container);
|
|
759
|
-
|
|
760
|
-
const brandColor =
|
|
761
|
-
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
762
|
-
return injectBranding(svgHtml, brandColor);
|
|
757
|
+
return svgHtml;
|
|
763
758
|
}
|
package/src/utils/arrows.ts
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
// Labeled arrow syntax (always left-to-right):
|
|
6
6
|
// Sync: `-label->`
|
|
7
7
|
// Async: `~label~>`
|
|
8
|
+
//
|
|
9
|
+
// In-arrow label character-set contract (see docs/dgmo-language-spec.md
|
|
10
|
+
// §"In-Arrow Message Labels"):
|
|
11
|
+
// - Allowed: any codepoint except the forbidden substrings and forbidden
|
|
12
|
+
// control characters below.
|
|
13
|
+
// - Forbidden substrings: "->", "~>" (arrow-token lookalikes inside labels).
|
|
14
|
+
// Use the post-colon form for labels that need these symbols:
|
|
15
|
+
// `A -> B: uses -> to chain`
|
|
16
|
+
// - Forbidden characters: C0 control chars U+0000–U+001F EXCEPT U+0009 (tab),
|
|
17
|
+
// and U+007F (DEL).
|
|
18
|
+
// - Whitespace: leading/trailing trimmed; internal runs (incl. tab, NBSP,
|
|
19
|
+
// ZWSP) preserved — never collapsed.
|
|
20
|
+
// - Plain text only: no markdown interpretation. `*`, `_`, backticks,
|
|
21
|
+
// `[`, `]`, `{`, `}` are literal characters.
|
|
22
|
+
|
|
23
|
+
import type { DgmoError } from '../diagnostics';
|
|
24
|
+
import { makeDgmoError } from '../diagnostics';
|
|
25
|
+
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
8
26
|
|
|
9
27
|
interface ParsedArrow {
|
|
10
28
|
from: string;
|
|
@@ -13,6 +31,160 @@ interface ParsedArrow {
|
|
|
13
31
|
async: boolean;
|
|
14
32
|
}
|
|
15
33
|
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Diagnostic codes (TD-16)
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stable diagnostic codes for in-arrow label parsing errors.
|
|
40
|
+
*
|
|
41
|
+
* **Active codes** — emitted by the parser pipeline today:
|
|
42
|
+
* - `ARROW_SUBSTRING_IN_LABEL` (TD-13)
|
|
43
|
+
* - `CONTROL_CHAR_IN_LABEL` (TD-14)
|
|
44
|
+
*
|
|
45
|
+
* **Reserved codes** — declared but NOT currently emitted. These are
|
|
46
|
+
* placeholders for future tightening of the arrow-tokenization rules
|
|
47
|
+
* described in TD-9. Today's chart parsers catch these cases through
|
|
48
|
+
* their own regex machinery with different diagnostics. A follow-up
|
|
49
|
+
* spec that introduces a dedicated tokenizer can start emitting them
|
|
50
|
+
* without changing the public code shape:
|
|
51
|
+
* - `TRAILING_ARROW_TEXT` — extra `->`/`~>` after the primary arrow
|
|
52
|
+
* - `MIXED_ARROW_DELIMITERS` — opening delim type doesn't match arrow
|
|
53
|
+
*
|
|
54
|
+
* See `docs/dgmo-language-spec-decisions.md` → TD-16 for the rationale.
|
|
55
|
+
*/
|
|
56
|
+
export const ARROW_DIAGNOSTIC_CODES = {
|
|
57
|
+
/** Active: label contains `->` or `~>` substring (TD-13). */
|
|
58
|
+
ARROW_SUBSTRING_IN_LABEL: 'E_ARROW_SUBSTRING_IN_LABEL',
|
|
59
|
+
/** Active: label contains a forbidden control character (TD-14). */
|
|
60
|
+
CONTROL_CHAR_IN_LABEL: 'E_CONTROL_CHAR_IN_LABEL',
|
|
61
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
62
|
+
TRAILING_ARROW_TEXT: 'E_TRAILING_ARROW_TEXT',
|
|
63
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
64
|
+
MIXED_ARROW_DELIMITERS: 'E_MIXED_ARROW_DELIMITERS',
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// validateLabelCharacters (TD-13, TD-14)
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate an in-arrow label against the TD-13 and TD-14 character-set
|
|
73
|
+
* contract. Returns diagnostics (possibly empty). Does NOT mutate the label —
|
|
74
|
+
* callers that want a normalized label should trim before calling.
|
|
75
|
+
*
|
|
76
|
+
* TD-13: label must not contain the substrings "->" or "~>".
|
|
77
|
+
* TD-14: label must not contain C0 control chars other than tab, and no DEL.
|
|
78
|
+
*/
|
|
79
|
+
export function validateLabelCharacters(
|
|
80
|
+
label: string,
|
|
81
|
+
lineNumber: number
|
|
82
|
+
): DgmoError[] {
|
|
83
|
+
const out: DgmoError[] = [];
|
|
84
|
+
|
|
85
|
+
// TD-13: forbidden substrings
|
|
86
|
+
if (label.includes('->') || label.includes('~>')) {
|
|
87
|
+
out.push(
|
|
88
|
+
makeDgmoError(
|
|
89
|
+
lineNumber,
|
|
90
|
+
'Arrow symbols (-> or ~>) are not allowed inside a label. ' +
|
|
91
|
+
'Move the label after the arrow: "A -> B: uses -> to chain". ' +
|
|
92
|
+
'See "In-Arrow Message Labels" → Forbidden.',
|
|
93
|
+
'error',
|
|
94
|
+
ARROW_DIAGNOSTIC_CODES.ARROW_SUBSTRING_IN_LABEL
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// TD-14: control chars (iterate codepoints to handle surrogate pairs)
|
|
100
|
+
for (const ch of label) {
|
|
101
|
+
const cp = ch.codePointAt(0)!;
|
|
102
|
+
const isC0 = cp >= 0x00 && cp <= 0x1f && cp !== 0x09; // allow tab
|
|
103
|
+
const isDel = cp === 0x7f;
|
|
104
|
+
if (isC0 || isDel) {
|
|
105
|
+
const hex = cp.toString(16).toUpperCase().padStart(4, '0');
|
|
106
|
+
out.push(
|
|
107
|
+
makeDgmoError(
|
|
108
|
+
lineNumber,
|
|
109
|
+
`Label contains a control character (U+${hex}). ` +
|
|
110
|
+
'Remove it and use plain text.',
|
|
111
|
+
'error',
|
|
112
|
+
ARROW_DIAGNOSTIC_CODES.CONTROL_CHAR_IN_LABEL
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
break; // one diagnostic per label is enough
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// parseInArrowLabel (TD-1, TD-8, TD-10, TD-13, TD-14)
|
|
124
|
+
// ============================================================
|
|
125
|
+
|
|
126
|
+
export interface ParseInArrowLabelResult {
|
|
127
|
+
/** Cleaned label (trimmed; `undefined` if empty after trim per TD-10). */
|
|
128
|
+
label: string | undefined;
|
|
129
|
+
diagnostics: DgmoError[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalize and validate a raw in-arrow label.
|
|
134
|
+
*
|
|
135
|
+
* Behavior:
|
|
136
|
+
* - Trims leading/trailing whitespace (TD-8: internal whitespace preserved).
|
|
137
|
+
* - Empty-after-trim → `{ label: undefined }` (TD-10 normalization).
|
|
138
|
+
* - TD-13: emits `E_ARROW_SUBSTRING_IN_LABEL` if `->` or `~>` is present.
|
|
139
|
+
* - TD-14: emits `E_CONTROL_CHAR_IN_LABEL` for forbidden control chars.
|
|
140
|
+
*
|
|
141
|
+
* This helper is intentionally chart-agnostic: it operates on an already
|
|
142
|
+
* extracted label string, leaving each chart's existing arrow-finding
|
|
143
|
+
* tokenization in place. TD-11 color-parens is handled inside the
|
|
144
|
+
* flowchart and state `parseArrowToken` functions because those are the
|
|
145
|
+
* only charts that interpret `-(color)->` as a colored edge; they use
|
|
146
|
+
* `matchColorParens()` from this module for the shared lookup.
|
|
147
|
+
*/
|
|
148
|
+
export function parseInArrowLabel(
|
|
149
|
+
rawLabel: string,
|
|
150
|
+
lineNumber: number
|
|
151
|
+
): ParseInArrowLabelResult {
|
|
152
|
+
const trimmed = rawLabel.trim();
|
|
153
|
+
|
|
154
|
+
// TD-10: empty/whitespace-only label normalizes to undefined
|
|
155
|
+
if (trimmed.length === 0) {
|
|
156
|
+
return { label: undefined, diagnostics: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// TD-13 / TD-14 validation
|
|
160
|
+
const diagnostics = validateLabelCharacters(trimmed, lineNumber);
|
|
161
|
+
|
|
162
|
+
return { label: trimmed, diagnostics };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// matchColorParens — shared TD-11 helper for flowchart and state
|
|
167
|
+
// ============================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Test whether a string matches the TD-11 color-parens form `(colorName)`
|
|
171
|
+
* where `colorName` is one of the 11 recognized palette color names from
|
|
172
|
+
* `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
|
|
173
|
+
* on a match, or `null` on fall-through (whole string becomes a label).
|
|
174
|
+
*
|
|
175
|
+
* Used by flowchart and state parsers to keep the color-parens recognition
|
|
176
|
+
* rule in one place — do NOT re-implement the regex in chart parsers.
|
|
177
|
+
*/
|
|
178
|
+
export function matchColorParens(content: string): string | null {
|
|
179
|
+
const m = content.match(/^\(([A-Za-z]+)\)$/);
|
|
180
|
+
if (!m) return null;
|
|
181
|
+
const candidate = m[1].toLowerCase();
|
|
182
|
+
if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
16
188
|
// Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
|
|
17
189
|
const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
|
|
18
190
|
const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
|
|
@@ -23,8 +195,6 @@ const RETURN_ASYNC_LABELED_RE = /^(.+?)\s*<~(.+)~\s*(.+)$/;
|
|
|
23
195
|
const BIDI_SYNC_RE = /^(.+?)\s*<-(.+)->\s*(.+)$/;
|
|
24
196
|
const BIDI_ASYNC_RE = /^(.+?)\s*<~(.+)~>\s*(.+)$/;
|
|
25
197
|
|
|
26
|
-
const ARROW_CHARS = ['->', '~>'];
|
|
27
|
-
|
|
28
198
|
/**
|
|
29
199
|
* Try to parse a labeled arrow from a trimmed line.
|
|
30
200
|
*
|
|
@@ -32,6 +202,14 @@ const ARROW_CHARS = ['->', '~>'];
|
|
|
32
202
|
* - `ParsedArrow` if matched and valid
|
|
33
203
|
* - `{ error: string }` if matched but invalid (deprecated syntax)
|
|
34
204
|
* - `null` if not a labeled arrow (caller should fall through to bare patterns)
|
|
205
|
+
*
|
|
206
|
+
* Note: arrow-char-in-label validation (TD-13) is NOT performed here —
|
|
207
|
+
* callers must route the returned `label` through `parseInArrowLabel` or
|
|
208
|
+
* `validateLabelCharacters` to get the unified `E_ARROW_SUBSTRING_IN_LABEL`
|
|
209
|
+
* diagnostic with the correct code. In practice this path is unreachable
|
|
210
|
+
* because arrow regexes are greedy enough to absorb inner `->`/`~>` tokens
|
|
211
|
+
* into the source/destination captures, but the check remains at the
|
|
212
|
+
* validator level for defense in depth.
|
|
35
213
|
*/
|
|
36
214
|
export function parseArrow(
|
|
37
215
|
line: string
|
|
@@ -73,15 +251,6 @@ export function parseArrow(
|
|
|
73
251
|
// Empty label (e.g. `--> B`) — fall through to plain arrow handling
|
|
74
252
|
if (!label) return null;
|
|
75
253
|
|
|
76
|
-
// Validate: no arrow chars inside label
|
|
77
|
-
for (const arrow of ARROW_CHARS) {
|
|
78
|
-
if (label.includes(arrow)) {
|
|
79
|
-
return {
|
|
80
|
-
error: 'Arrow characters (->, ~>) are not allowed inside labels',
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
254
|
return {
|
|
86
255
|
from: m[1],
|
|
87
256
|
to: m[3],
|
|
@@ -16,10 +16,6 @@ export const LEGEND_EYE_SIZE = 14;
|
|
|
16
16
|
export const LEGEND_EYE_GAP = 6;
|
|
17
17
|
export const LEGEND_ICON_W = 20;
|
|
18
18
|
|
|
19
|
-
// ── Spacing constants (centralized legend system) ───────────
|
|
20
|
-
export const LEGEND_TOP_PAD = 12;
|
|
21
|
-
export const LEGEND_TITLE_GAP = 8;
|
|
22
|
-
export const LEGEND_CONTENT_GAP = 12;
|
|
23
19
|
export const LEGEND_MAX_ENTRY_ROWS = 3;
|
|
24
20
|
|
|
25
21
|
// ── Proportional text measurement ────────────────────────────
|