@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/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';
@@ -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 label = asyncConnMatch[1]?.trim() || '';
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 label = connMatch[1]?.trim() || '';
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;
@@ -28,7 +28,7 @@ import type {
28
28
  // Public options object
29
29
  // ============================================================
30
30
 
31
- export interface KanbanInteractiveOptions {
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
- export function bucketCardsBySwimlane(
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(palette: PaletteColors, count: number): string[] {
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);
@@ -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 { parseDgmoChartType, getRenderCategory } from './dgmo-router';
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, or empty string on error
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
- return renderExtendedChartForExport(content, theme, paletteColors, {
96
- branding,
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
- return renderForExport(content, theme, paletteColors, legendExportState, {
103
- branding,
104
- c4Level: options?.c4Level,
105
- c4System: options?.c4System,
106
- c4Container: options?.c4Container,
107
- tagGroup: options?.tagGroup,
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
  }
@@ -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,
@@ -2365,7 +2365,9 @@ export function renderSequenceDiagram(
2365
2365
  if (tagKey && msgTagValue) {
2366
2366
  labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
2367
2367
  }
2368
- renderInlineText(labelEl, step.label, palette);
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
- renderInlineText(labelEl, step.label, palette);
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
- renderInlineText(labelEl, step.label, palette);
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
  });
@@ -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
  }
@@ -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],
@@ -0,0 +1,4 @@
1
+ export interface D3ExportDimensions {
2
+ width?: number;
3
+ height?: number;
4
+ }
@@ -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 ────────────────────────────