@diagrammo/dgmo 0.8.20 → 0.8.22
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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +142 -90
- package/dist/editor.cjs +30 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +30 -4
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +25 -3
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +25 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +21201 -12886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +646 -89
- package/dist/index.d.ts +646 -89
- package/dist/index.js +21178 -12889
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +249 -4
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +360 -42
- package/src/boxes-and-lines/parser.ts +94 -11
- package/src/boxes-and-lines/renderer.ts +371 -114
- package/src/boxes-and-lines/types.ts +2 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/completion.ts +253 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +240 -40
- package/src/dgmo-router.ts +15 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +26 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +5 -10
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +78 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +325 -63
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +373 -0
- package/src/mindmap/renderer.ts +544 -0
- package/src/mindmap/text-wrap.ts +217 -0
- package/src/mindmap/types.ts +55 -0
- package/src/org/parser.ts +2 -6
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +273 -56
- package/src/sharing.ts +3 -0
- package/src/sitemap/layout.ts +56 -18
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1058 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +5 -3
- package/src/utils/parsing.ts +48 -7
- package/src/utils/tag-groups.ts +46 -60
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type * as d3Selection from 'd3-selection';
|
|
2
|
+
import { FONT_FAMILY } from '../fonts';
|
|
3
|
+
import type { PaletteColors } from '../palettes';
|
|
4
|
+
import type { QuadrantPosition, BlipTrend } from './types';
|
|
5
|
+
|
|
6
|
+
/** Default quadrant colors by position when not overridden. */
|
|
7
|
+
export const DEFAULT_QUADRANT_COLORS: Record<QuadrantPosition, string> = {
|
|
8
|
+
'top-left': 'blue',
|
|
9
|
+
'top-right': 'green',
|
|
10
|
+
'bottom-left': 'red',
|
|
11
|
+
'bottom-right': 'orange',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Resolve a quadrant's color from palette, falling back to palette.border. */
|
|
15
|
+
export function resolveQuadrantColor(
|
|
16
|
+
position: QuadrantPosition,
|
|
17
|
+
color: string | null,
|
|
18
|
+
palette: PaletteColors
|
|
19
|
+
): string {
|
|
20
|
+
const name = color ?? DEFAULT_QUADRANT_COLORS[position];
|
|
21
|
+
return (palette.colors as Record<string, string>)[name] ?? palette.border;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a trend indicator (blip circle + optional crescent/ring).
|
|
26
|
+
*
|
|
27
|
+
* @param angleToCenter — angle in radians from the blip toward the radar center.
|
|
28
|
+
* Used to orient the crescent for up/down trends (toward center = up, away = down).
|
|
29
|
+
*/
|
|
30
|
+
export function renderTrendIndicator(
|
|
31
|
+
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
32
|
+
trend: BlipTrend | null,
|
|
33
|
+
color: string,
|
|
34
|
+
cx: number,
|
|
35
|
+
cy: number,
|
|
36
|
+
r: number,
|
|
37
|
+
angleToCenter: number
|
|
38
|
+
): void {
|
|
39
|
+
// Base filled circle (always present)
|
|
40
|
+
g.append('circle')
|
|
41
|
+
.attr('cx', cx)
|
|
42
|
+
.attr('cy', cy)
|
|
43
|
+
.attr('r', r * 0.65)
|
|
44
|
+
.attr('fill', color);
|
|
45
|
+
|
|
46
|
+
if (trend === 'new') {
|
|
47
|
+
// Double circle — outer ring
|
|
48
|
+
g.append('circle')
|
|
49
|
+
.attr('cx', cx)
|
|
50
|
+
.attr('cy', cy)
|
|
51
|
+
.attr('r', r)
|
|
52
|
+
.attr('fill', 'none')
|
|
53
|
+
.attr('stroke', color)
|
|
54
|
+
.attr('stroke-width', 1.5);
|
|
55
|
+
} else if (trend === 'up' || trend === 'down') {
|
|
56
|
+
// Semi-circle crescent (ThoughtWorks style)
|
|
57
|
+
// "up" = crescent on the center-facing side (moving inward)
|
|
58
|
+
// "down" = crescent on the outward-facing side (moving outward)
|
|
59
|
+
const crescentAngle =
|
|
60
|
+
trend === 'up' ? angleToCenter : angleToCenter + Math.PI;
|
|
61
|
+
renderCrescent(g, cx, cy, r, crescentAngle, color);
|
|
62
|
+
}
|
|
63
|
+
// 'stable' or null: plain filled circle only
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draw a crescent (semi-circle arc) on one side of the blip, oriented
|
|
68
|
+
* at `angle` radians (0 = right, π/2 = up in math coords / down in SVG).
|
|
69
|
+
*
|
|
70
|
+
* The crescent is a thick arc segment hugging the outer edge of the blip circle,
|
|
71
|
+
* spanning ~160° centered on `angle`.
|
|
72
|
+
*/
|
|
73
|
+
function renderCrescent(
|
|
74
|
+
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
75
|
+
cx: number,
|
|
76
|
+
cy: number,
|
|
77
|
+
r: number,
|
|
78
|
+
angle: number,
|
|
79
|
+
color: string
|
|
80
|
+
): void {
|
|
81
|
+
const outerR = r;
|
|
82
|
+
const halfSpan = (Math.PI * 4) / 9; // ~80° each side = 160° total arc
|
|
83
|
+
|
|
84
|
+
const startAngle = angle - halfSpan;
|
|
85
|
+
const endAngle = angle + halfSpan;
|
|
86
|
+
|
|
87
|
+
// SVG arc: start and end points on the outer circle (SVG Y-down convention)
|
|
88
|
+
const x1 = cx + outerR * Math.cos(startAngle);
|
|
89
|
+
const y1 = cy + outerR * Math.sin(startAngle);
|
|
90
|
+
const x2 = cx + outerR * Math.cos(endAngle);
|
|
91
|
+
const y2 = cy + outerR * Math.sin(endAngle);
|
|
92
|
+
|
|
93
|
+
// Large arc flag: 0 since we span < 180°; sweep=1 for clockwise in SVG
|
|
94
|
+
const largeArc = halfSpan * 2 > Math.PI ? 1 : 0;
|
|
95
|
+
|
|
96
|
+
g.append('path')
|
|
97
|
+
.attr('d', `M${x1},${y1} A${outerR},${outerR} 0 ${largeArc},1 ${x2},${y2}`)
|
|
98
|
+
.attr('fill', 'none')
|
|
99
|
+
.attr('stroke', color)
|
|
100
|
+
.attr('stroke-width', 2.5)
|
|
101
|
+
.attr('stroke-linecap', 'round');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Trend indicator character for text listings. */
|
|
105
|
+
export function getTrendChar(trend: BlipTrend | null): string {
|
|
106
|
+
switch (trend) {
|
|
107
|
+
case 'new':
|
|
108
|
+
return ' ★';
|
|
109
|
+
case 'up':
|
|
110
|
+
return ' ▲';
|
|
111
|
+
case 'down':
|
|
112
|
+
return ' ▼';
|
|
113
|
+
default:
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// Shared Constants
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
export const DIM_OPACITY = 0.25;
|
|
123
|
+
|
|
124
|
+
export const TREND_ITEMS: { trend: BlipTrend | null; label: string }[] = [
|
|
125
|
+
{ trend: 'new', label: 'New' },
|
|
126
|
+
{ trend: 'up', label: 'Moved in' },
|
|
127
|
+
{ trend: 'down', label: 'Moved out' },
|
|
128
|
+
{ trend: null, label: 'No change' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// ============================================================
|
|
132
|
+
// Tooltip Helpers
|
|
133
|
+
// ============================================================
|
|
134
|
+
|
|
135
|
+
export function createTooltip(
|
|
136
|
+
container: HTMLElement,
|
|
137
|
+
palette: PaletteColors,
|
|
138
|
+
isDark: boolean
|
|
139
|
+
): HTMLDivElement {
|
|
140
|
+
container.style.position = 'relative';
|
|
141
|
+
const existing = container.querySelector<HTMLDivElement>('[data-d3-tooltip]');
|
|
142
|
+
if (existing) {
|
|
143
|
+
existing.style.display = 'none';
|
|
144
|
+
return existing;
|
|
145
|
+
}
|
|
146
|
+
const tip = document.createElement('div');
|
|
147
|
+
tip.setAttribute('data-d3-tooltip', '');
|
|
148
|
+
tip.style.position = 'absolute';
|
|
149
|
+
tip.style.display = 'none';
|
|
150
|
+
tip.style.pointerEvents = 'none';
|
|
151
|
+
tip.style.padding = '6px 10px';
|
|
152
|
+
tip.style.borderRadius = '4px';
|
|
153
|
+
tip.style.fontSize = '12px';
|
|
154
|
+
tip.style.fontFamily = FONT_FAMILY;
|
|
155
|
+
tip.style.lineHeight = '1.4';
|
|
156
|
+
tip.style.zIndex = '10';
|
|
157
|
+
tip.style.whiteSpace = 'nowrap';
|
|
158
|
+
tip.style.background = palette.surface;
|
|
159
|
+
tip.style.color = palette.text;
|
|
160
|
+
tip.style.boxShadow = isDark
|
|
161
|
+
? '0 2px 6px rgba(0,0,0,0.3)'
|
|
162
|
+
: '0 2px 6px rgba(0,0,0,0.12)';
|
|
163
|
+
container.appendChild(tip);
|
|
164
|
+
return tip;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function showTooltip(
|
|
168
|
+
tooltip: HTMLDivElement,
|
|
169
|
+
text: string,
|
|
170
|
+
event: MouseEvent
|
|
171
|
+
): void {
|
|
172
|
+
tooltip.textContent = text;
|
|
173
|
+
tooltip.style.display = 'block';
|
|
174
|
+
const container = tooltip.parentElement!;
|
|
175
|
+
const rect = container.getBoundingClientRect();
|
|
176
|
+
let left = event.clientX - rect.left + 12;
|
|
177
|
+
let top = event.clientY - rect.top - 28;
|
|
178
|
+
const tipW = tooltip.offsetWidth;
|
|
179
|
+
if (left + tipW > rect.width) left = rect.width - tipW - 4;
|
|
180
|
+
if (top < 0) top = event.clientY - rect.top + 16;
|
|
181
|
+
tooltip.style.left = `${left}px`;
|
|
182
|
+
tooltip.style.top = `${top}px`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function hideTooltip(tooltip: HTMLDivElement): void {
|
|
186
|
+
tooltip.style.display = 'none';
|
|
187
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DgmoError } from '../diagnostics';
|
|
2
|
+
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Tech Radar — Parsed Types
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
export type QuadrantPosition =
|
|
8
|
+
| 'top-left'
|
|
9
|
+
| 'top-right'
|
|
10
|
+
| 'bottom-left'
|
|
11
|
+
| 'bottom-right';
|
|
12
|
+
|
|
13
|
+
export type BlipTrend = 'new' | 'up' | 'down' | 'stable';
|
|
14
|
+
|
|
15
|
+
export interface TechRadarRing {
|
|
16
|
+
name: string;
|
|
17
|
+
alias: string | null;
|
|
18
|
+
lineNumber: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TechRadarBlip {
|
|
22
|
+
name: string;
|
|
23
|
+
ring: string;
|
|
24
|
+
trend: BlipTrend | null;
|
|
25
|
+
description: string[];
|
|
26
|
+
lineNumber: number;
|
|
27
|
+
/** Assigned after parsing — global numbering across all quadrants. */
|
|
28
|
+
globalNumber: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TechRadarQuadrant {
|
|
32
|
+
name: string;
|
|
33
|
+
position: QuadrantPosition;
|
|
34
|
+
color: string | null;
|
|
35
|
+
lineNumber: number;
|
|
36
|
+
blips: TechRadarBlip[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParsedTechRadar {
|
|
40
|
+
type: 'tech-radar';
|
|
41
|
+
title: string;
|
|
42
|
+
titleLineNumber: number;
|
|
43
|
+
rings: TechRadarRing[];
|
|
44
|
+
quadrants: TechRadarQuadrant[];
|
|
45
|
+
options: Record<string, string>;
|
|
46
|
+
diagnostics: DgmoError[];
|
|
47
|
+
error: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Tech Radar — Layout Types
|
|
52
|
+
// ============================================================
|
|
53
|
+
|
|
54
|
+
export interface TechRadarLayoutPoint {
|
|
55
|
+
blip: TechRadarBlip;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
quadrantIndex: number;
|
|
59
|
+
ringIndex: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// Tech Radar — Render Options
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
export interface TechRadarRenderOptions {
|
|
67
|
+
/** Whether the blip listing is visible. Default: true for export, false for interactive. */
|
|
68
|
+
showListing?: boolean;
|
|
69
|
+
/** Callback when the listing toggle is clicked. */
|
|
70
|
+
onToggleListing?: (show: boolean) => void;
|
|
71
|
+
/** Whether the controls legend capsule is expanded. */
|
|
72
|
+
controlsExpanded?: boolean;
|
|
73
|
+
/** Callback when the controls gear pill is clicked (expand/collapse). */
|
|
74
|
+
onToggleControlsExpand?: () => void;
|
|
75
|
+
/** Active legend group name (e.g. 'Trends'). */
|
|
76
|
+
activeLegendGroup?: string | null;
|
|
77
|
+
/** Callback when a legend group pill is toggled. */
|
|
78
|
+
onLegendGroupToggle?: (groupName: string) => void;
|
|
79
|
+
/** Active line from the editor cursor — triggers popover/expansion for that blip. */
|
|
80
|
+
activeLine?: number | null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Description Helpers — shared utilities for node descriptions
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Try to strip a leading `description` keyword from a line.
|
|
7
|
+
* Matches: `description text`, `description: text` (colon optional).
|
|
8
|
+
* Does NOT match bare `description` with no trailing text.
|
|
9
|
+
*/
|
|
10
|
+
export function tryStripDescriptionKeyword(line: string): {
|
|
11
|
+
isKeyword: boolean;
|
|
12
|
+
text: string;
|
|
13
|
+
} {
|
|
14
|
+
const match = line.match(/^description\s*:?\s+(.+)$/i);
|
|
15
|
+
if (match) return { isKeyword: true, text: match[1] };
|
|
16
|
+
return { isKeyword: false, text: line };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pre-process a single description line:
|
|
21
|
+
* - `- text` → `• text` (bullet)
|
|
22
|
+
* - `http example.com` → `https://example.com` (bare URL normalization)
|
|
23
|
+
*/
|
|
24
|
+
export function preprocessDescriptionLine(line: string): string {
|
|
25
|
+
// Bullet transform
|
|
26
|
+
if (line.startsWith('- ')) line = '\u2022 ' + line.slice(2);
|
|
27
|
+
// Bare URL normalization
|
|
28
|
+
line = line.replace(
|
|
29
|
+
/\bhttps?\s+([\w][\w.-]+\.[a-z]{2,}(?:\/\S*)?)/gi,
|
|
30
|
+
(_, domain) => `https://${domain}`
|
|
31
|
+
);
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
@@ -7,7 +7,7 @@ import { FONT_FAMILY } from '../fonts';
|
|
|
7
7
|
export function runInExportContainer<T>(
|
|
8
8
|
width: number,
|
|
9
9
|
height: number,
|
|
10
|
-
fn: (container: HTMLDivElement) => T
|
|
10
|
+
fn: (container: HTMLDivElement) => T
|
|
11
11
|
): T {
|
|
12
12
|
const container = document.createElement('div');
|
|
13
13
|
container.style.width = `${width}px`;
|
|
@@ -29,12 +29,13 @@ export function runInExportContainer<T>(
|
|
|
29
29
|
*/
|
|
30
30
|
export function extractExportSvg(
|
|
31
31
|
container: HTMLElement,
|
|
32
|
-
theme: 'light' | 'dark' | 'transparent'
|
|
32
|
+
theme: 'light' | 'dark' | 'transparent'
|
|
33
33
|
): string {
|
|
34
34
|
const svgEl = container.querySelector('svg');
|
|
35
35
|
if (!svgEl) return '';
|
|
36
36
|
if (theme === 'transparent') svgEl.style.background = 'none';
|
|
37
37
|
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
38
38
|
svgEl.style.fontFamily = FONT_FAMILY;
|
|
39
|
+
svgEl.querySelectorAll('[data-export-ignore]').forEach((el) => el.remove());
|
|
39
40
|
return svgEl.outerHTML;
|
|
40
41
|
}
|
package/src/utils/legend-d3.ts
CHANGED
|
@@ -556,7 +556,9 @@ function layoutRows(
|
|
|
556
556
|
|
|
557
557
|
// Commit last row
|
|
558
558
|
if (currentRowItems.length > 0) {
|
|
559
|
-
|
|
559
|
+
if (!alignLeft) {
|
|
560
|
+
centerRowItems(currentRowItems, containerWidth, totalControlsW, gearW);
|
|
561
|
+
}
|
|
560
562
|
rows.push({ y: rowY, items: currentRowItems });
|
|
561
563
|
}
|
|
562
564
|
|
|
@@ -570,8 +572,8 @@ function layoutRows(
|
|
|
570
572
|
const last = groupItemsInRow0[groupItemsInRow0.length - 1];
|
|
571
573
|
controlsGroup.x = last.x + last.width + LEGEND_GROUP_GAP;
|
|
572
574
|
} else {
|
|
573
|
-
// No group items — controls group
|
|
574
|
-
controlsGroup.x =
|
|
575
|
+
// No group items — center the controls group
|
|
576
|
+
controlsGroup.x = (containerWidth - controlsGroup.width) / 2;
|
|
575
577
|
}
|
|
576
578
|
controlsGroup.y = 0;
|
|
577
579
|
}
|
package/src/utils/parsing.ts
CHANGED
|
@@ -47,6 +47,11 @@ export const ALL_CHART_TYPES = new Set([
|
|
|
47
47
|
'infra',
|
|
48
48
|
'gantt',
|
|
49
49
|
'boxes-and-lines',
|
|
50
|
+
'mindmap',
|
|
51
|
+
'wireframe',
|
|
52
|
+
'tech-radar',
|
|
53
|
+
'cycle',
|
|
54
|
+
'journey-map',
|
|
50
55
|
]);
|
|
51
56
|
|
|
52
57
|
/** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */
|
|
@@ -203,15 +208,51 @@ export function prescanOptions(
|
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
/**
|
|
206
|
-
* Normalize a
|
|
207
|
-
* Validates the strict pattern: leftmost group 1-3 digits, then groups of exactly 3.
|
|
211
|
+
* Normalize a numeric token with visual separators (commas or underscores) to a plain number string.
|
|
208
212
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
213
|
+
* Supported formats:
|
|
214
|
+
* - Comma-grouped integers: `1,000` → `'1000'`, `1,234,567` → `'1234567'` (strict 3-digit grouping)
|
|
215
|
+
* - Comma-grouped decimals: `1,234.56` → `'1234.56'`
|
|
216
|
+
* - Underscore-separated integers: `1_000` → `'1000'`, `10_00_000` → `'1000000'` (any grouping)
|
|
217
|
+
* - Underscore-separated decimals: `1_234.56` → `'1234.56'` (no underscores in decimal part)
|
|
218
|
+
* - Negatives: `-1,000` → `'-1000'`, `-1_000` → `'-1000'`
|
|
219
|
+
*
|
|
220
|
+
* Returns `null` if:
|
|
221
|
+
* - Token has no commas or underscores (caller should use raw token as-is)
|
|
222
|
+
* - Token has BOTH commas and underscores (mixed separators rejected)
|
|
223
|
+
* - Token has separators but doesn't match any valid pattern
|
|
211
224
|
*/
|
|
212
|
-
export function
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
export function normalizeNumericToken(token: string): string | null {
|
|
226
|
+
// No separators → null (caller uses raw token)
|
|
227
|
+
if (!token.includes(',') && !token.includes('_')) return null;
|
|
228
|
+
// Mixed separators → rejected
|
|
229
|
+
if (token.includes(',') && token.includes('_')) return null;
|
|
230
|
+
|
|
231
|
+
// Strip optional leading minus sign
|
|
232
|
+
let sign = '';
|
|
233
|
+
let unsigned = token;
|
|
234
|
+
if (unsigned.startsWith('-')) {
|
|
235
|
+
sign = '-';
|
|
236
|
+
unsigned = unsigned.substring(1);
|
|
237
|
+
}
|
|
238
|
+
if (!unsigned) return null;
|
|
239
|
+
|
|
240
|
+
if (unsigned.includes(',')) {
|
|
241
|
+
// Comma-grouped integers: 1,000 or 1,234,567
|
|
242
|
+
if (/^\d{1,3}(,\d{3})+$/.test(unsigned))
|
|
243
|
+
return sign + unsigned.replace(/,/g, '');
|
|
244
|
+
// Comma-grouped decimals: 1,234.56
|
|
245
|
+
if (/^\d{1,3}(,\d{3})+\.\d+$/.test(unsigned))
|
|
246
|
+
return sign + unsigned.replace(/,/g, '');
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Underscore-separated integers: 1_000, 10_00_000
|
|
251
|
+
if (/^\d+(_\d+)+$/.test(unsigned)) return sign + unsigned.replace(/_/g, '');
|
|
252
|
+
// Underscore-separated decimals: 1_234.56 (no underscores in decimal part)
|
|
253
|
+
if (/^\d+(_\d+)*\.\d+$/.test(unsigned) && unsigned.includes('_'))
|
|
254
|
+
return sign + unsigned.replace(/_/g, '');
|
|
255
|
+
return null;
|
|
215
256
|
}
|
|
216
257
|
|
|
217
258
|
/**
|
package/src/utils/tag-groups.ts
CHANGED
|
@@ -108,70 +108,56 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
108
108
|
// Unquoted — collect multi-word name. The alias is the last token that's 1-4 lowercase
|
|
109
109
|
// BEFORE any value tokens (values have `(color)` suffixes or appear after we see a comma).
|
|
110
110
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (tokens.length === 1) {
|
|
130
|
-
// Just `tag Name` — no alias, no values
|
|
131
|
-
} else if (
|
|
132
|
-
tokens.length === 2 &&
|
|
133
|
-
isAliasToken(tokens[1]) &&
|
|
134
|
-
!commaInRemaining
|
|
135
|
-
) {
|
|
136
|
-
// `tag Priority p` — alias only, no values
|
|
111
|
+
// Find where inline values start — look for a token with `(` in it (color suffix)
|
|
112
|
+
// or the presence of a comma in the remaining text
|
|
113
|
+
const remainingText = tokens.slice(1).join(' ');
|
|
114
|
+
const commaInRemaining = remainingText.includes(',');
|
|
115
|
+
|
|
116
|
+
if (tokens.length === 1) {
|
|
117
|
+
// Just `tag Name` — no alias, no values
|
|
118
|
+
} else if (
|
|
119
|
+
tokens.length === 2 &&
|
|
120
|
+
isAliasToken(tokens[1]) &&
|
|
121
|
+
!commaInRemaining
|
|
122
|
+
) {
|
|
123
|
+
// `tag Priority p` — alias only, no values
|
|
124
|
+
alias = tokens[1];
|
|
125
|
+
restStartIdx = 2;
|
|
126
|
+
} else if (tokens.length >= 2) {
|
|
127
|
+
// Check if token[1] is an alias
|
|
128
|
+
if (isAliasToken(tokens[1])) {
|
|
137
129
|
alias = tokens[1];
|
|
138
130
|
restStartIdx = 2;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (let i = 1; i < tokens.length; i++) {
|
|
151
|
-
// A token containing `(` suggests a value with color: `High(red)`
|
|
152
|
-
if (tokens[i].includes('(')) {
|
|
153
|
-
valueStart = i;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
131
|
+
// Multi-word name not applicable when alias is right after first token
|
|
132
|
+
} else {
|
|
133
|
+
// Could be multi-word name: `tag Risk Level lo`
|
|
134
|
+
// Walk tokens to find the alias at the end (before inline values)
|
|
135
|
+
// Find where inline values begin — first token containing `(` or after comma
|
|
136
|
+
let valueStart = tokens.length; // default: no values
|
|
137
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
138
|
+
// A token containing `(` suggests a value with color: `High(red)`
|
|
139
|
+
if (tokens[i].includes('(')) {
|
|
140
|
+
valueStart = i;
|
|
141
|
+
break;
|
|
156
142
|
}
|
|
143
|
+
}
|
|
157
144
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
145
|
+
// Check if the token just before valueStart is an alias
|
|
146
|
+
if (valueStart > 1 && isAliasToken(tokens[valueStart - 1])) {
|
|
147
|
+
alias = tokens[valueStart - 1];
|
|
148
|
+
// Name is everything from token[0] to token[valueStart-2]
|
|
149
|
+
name = tokens
|
|
150
|
+
.slice(0, valueStart - 1)
|
|
151
|
+
.map((t) => stripQuotes(t))
|
|
152
|
+
.join(' ');
|
|
153
|
+
restStartIdx = valueStart;
|
|
154
|
+
} else {
|
|
155
|
+
// No alias — name is everything before values
|
|
156
|
+
name = tokens
|
|
157
|
+
.slice(0, valueStart)
|
|
158
|
+
.map((t) => stripQuotes(t))
|
|
159
|
+
.join(' ');
|
|
160
|
+
restStartIdx = valueStart;
|
|
175
161
|
}
|
|
176
162
|
}
|
|
177
163
|
}
|