@diagrammo/dgmo 0.8.19 → 0.8.21
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 +92 -131
- package/dist/editor.cjs +13 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +13 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +13 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +13 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +4524 -1511
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -186
- package/dist/index.d.ts +427 -186
- package/dist/index.js +4526 -1503
- 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-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/language-reference.md +210 -2
- package/package.json +22 -9
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +51 -9
- package/src/boxes-and-lines/parser.ts +16 -4
- package/src/boxes-and-lines/renderer.ts +121 -23
- package/src/boxes-and-lines/types.ts +1 -0
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/completion.ts +26 -0
- package/src/d3.ts +169 -266
- package/src/dgmo-router.ts +103 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/editor/keywords.ts +12 -0
- 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/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +60 -35
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +41 -16
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +305 -59
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +379 -0
- package/src/mindmap/renderer.ts +543 -0
- package/src/mindmap/text-wrap.ts +207 -0
- package/src/mindmap/types.ts +55 -0
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +31 -20
- package/src/sequence/parser.ts +7 -2
- package/src/sequence/renderer.ts +141 -21
- package/src/sharing.ts +2 -0
- package/src/sitemap/layout.ts +35 -12
- 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/export-container.ts +3 -2
- package/src/utils/legend-constants.ts +0 -4
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +2 -2
- package/src/utils/parsing.ts +2 -0
- package/src/utils/time-ticks.ts +213 -0
- 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
- 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
|
@@ -17,7 +17,10 @@ import { looksLikeSitemap, parseSitemap } from './sitemap/parser';
|
|
|
17
17
|
import { parseInfra } from './infra/parser';
|
|
18
18
|
import { parseGantt } from './gantt/parser';
|
|
19
19
|
import { parseBoxesAndLines } from './boxes-and-lines/parser';
|
|
20
|
+
import { parseMindmap } from './mindmap/parser';
|
|
21
|
+
import { parseWireframe } from './wireframe/parser';
|
|
20
22
|
import { parseFirstLine } from './utils/parsing';
|
|
23
|
+
import { makeDgmoError, suggest } from './diagnostics';
|
|
21
24
|
import type { DgmoError } from './diagnostics';
|
|
22
25
|
|
|
23
26
|
// ============================================================
|
|
@@ -148,6 +151,8 @@ const DIAGRAM_TYPES = new Set([
|
|
|
148
151
|
'infra',
|
|
149
152
|
'gantt',
|
|
150
153
|
'boxes-and-lines',
|
|
154
|
+
'mindmap',
|
|
155
|
+
'wireframe',
|
|
151
156
|
]);
|
|
152
157
|
const EXTENDED_CHART_TYPES = new Set([
|
|
153
158
|
'scatter',
|
|
@@ -227,6 +232,19 @@ const PARSE_DISPATCH = new Map<
|
|
|
227
232
|
['infra', (c) => parseInfra(c)],
|
|
228
233
|
['gantt', (c) => parseGantt(c)],
|
|
229
234
|
['boxes-and-lines', (c) => parseBoxesAndLines(c)],
|
|
235
|
+
['mindmap', (c) => parseMindmap(c)],
|
|
236
|
+
['wireframe', (c) => parseWireframe(c)],
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse DGMO content and return diagnostics without rendering.
|
|
241
|
+
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
242
|
+
*/
|
|
243
|
+
/** All known chart type names for colon-pattern detection. */
|
|
244
|
+
const ALL_KNOWN_TYPES = new Set([
|
|
245
|
+
...DATA_CHART_TYPES,
|
|
246
|
+
...VISUALIZATION_TYPES,
|
|
247
|
+
...DIAGRAM_TYPES,
|
|
230
248
|
]);
|
|
231
249
|
|
|
232
250
|
/**
|
|
@@ -237,20 +255,100 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
|
237
255
|
const chartType = parseDgmoChartType(content);
|
|
238
256
|
|
|
239
257
|
if (!chartType) {
|
|
240
|
-
//
|
|
258
|
+
// Check for common mistake: colon in chart type declaration (e.g. "bar: Sales")
|
|
259
|
+
const colonDiag = detectColonChartType(content);
|
|
260
|
+
if (colonDiag) {
|
|
261
|
+
const fallback = parseVisualization(content).diagnostics;
|
|
262
|
+
return { diagnostics: [colonDiag, ...fallback] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// No chart type detected — try visualization parser as fallback
|
|
241
266
|
return { diagnostics: parseVisualization(content).diagnostics };
|
|
242
267
|
}
|
|
243
268
|
|
|
244
269
|
const directParser = PARSE_DISPATCH.get(chartType);
|
|
245
|
-
if (directParser)
|
|
270
|
+
if (directParser) {
|
|
271
|
+
const result = directParser(content);
|
|
272
|
+
return {
|
|
273
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
246
276
|
|
|
247
277
|
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
248
|
-
|
|
278
|
+
const result = parseChart(content);
|
|
279
|
+
return {
|
|
280
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
281
|
+
};
|
|
249
282
|
}
|
|
250
283
|
if (ECHART_TYPES.has(chartType)) {
|
|
251
|
-
|
|
284
|
+
const result = parseExtendedChart(content);
|
|
285
|
+
return {
|
|
286
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
287
|
+
};
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
// Visualization types (slope, wordcloud, arc, timeline, venn, quadrant)
|
|
255
|
-
|
|
291
|
+
const result = parseVisualization(content);
|
|
292
|
+
return {
|
|
293
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================
|
|
298
|
+
// Common-mistake detectors
|
|
299
|
+
// ============================================================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Detects colon-separated chart type declarations like "bar: Sales" or "pie: Data".
|
|
303
|
+
* Returns a diagnostic if the word before the colon is a known or similar chart type.
|
|
304
|
+
*/
|
|
305
|
+
function detectColonChartType(content: string): DgmoError | null {
|
|
306
|
+
const lines = content.split('\n');
|
|
307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
308
|
+
const trimmed = lines[i].trim();
|
|
309
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//'))
|
|
310
|
+
continue;
|
|
311
|
+
|
|
312
|
+
const match = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
313
|
+
if (!match) return null; // First non-empty line doesn't match colon pattern
|
|
314
|
+
|
|
315
|
+
const word = match[1].toLowerCase();
|
|
316
|
+
const rest = match[2].trim();
|
|
317
|
+
|
|
318
|
+
if (ALL_KNOWN_TYPES.has(word)) {
|
|
319
|
+
const example = rest ? `${word} ${rest}` : word;
|
|
320
|
+
return makeDgmoError(
|
|
321
|
+
i + 1,
|
|
322
|
+
`Remove the colon — use '${example}' instead of '${trimmed}'. DGMO chart types don't use colons.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if it's a misspelling of a known type
|
|
327
|
+
const hint = suggest(word, [...ALL_KNOWN_TYPES]);
|
|
328
|
+
if (hint) {
|
|
329
|
+
return makeDgmoError(
|
|
330
|
+
i + 1,
|
|
331
|
+
`Unknown chart type: ${word}. ${hint} Also, DGMO chart types don't use colons.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null; // First line has colon but isn't a chart type — normal data
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Detects when content has only the chart type line with no meaningful data lines.
|
|
342
|
+
*/
|
|
343
|
+
function detectEmptyContent(content: string): DgmoError[] {
|
|
344
|
+
const lines = content.split('\n');
|
|
345
|
+
const nonEmpty = lines.filter(
|
|
346
|
+
(l) => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//')
|
|
347
|
+
);
|
|
348
|
+
if (nonEmpty.length <= 1) {
|
|
349
|
+
return [
|
|
350
|
+
makeDgmoError(1, 'No content after chart type declaration.', 'warning'),
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
return [];
|
|
256
354
|
}
|
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/editor/keywords.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const CHART_TYPES = new Set([
|
|
|
13
13
|
'infra',
|
|
14
14
|
'gantt',
|
|
15
15
|
'boxes-and-lines',
|
|
16
|
+
'wireframe',
|
|
16
17
|
// Data chart types
|
|
17
18
|
'bar',
|
|
18
19
|
'line',
|
|
@@ -170,6 +171,17 @@ export const CONTROL_KEYWORDS = new Set([
|
|
|
170
171
|
'loop',
|
|
171
172
|
'parallel',
|
|
172
173
|
'note',
|
|
174
|
+
// Wireframe elements
|
|
175
|
+
'nav',
|
|
176
|
+
'tabs',
|
|
177
|
+
'table',
|
|
178
|
+
'image',
|
|
179
|
+
'modal',
|
|
180
|
+
'skeleton',
|
|
181
|
+
'alert',
|
|
182
|
+
'progress',
|
|
183
|
+
'chart',
|
|
184
|
+
'mobile',
|
|
173
185
|
]);
|
|
174
186
|
|
|
175
187
|
/** Status keywords — kanban. */
|
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
|
}
|
package/src/gantt/renderer.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { FONT_FAMILY } from '../fonts';
|
|
|
8
8
|
import { getSeriesColors } from '../palettes';
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import { resolveTagColor, resolveActiveTagGroup } from '../utils/tag-groups';
|
|
11
|
-
import { computeTimeTicks } from '../
|
|
11
|
+
import { computeTimeTicks } from '../utils/time-ticks';
|
|
12
12
|
import {
|
|
13
13
|
LEGEND_HEIGHT,
|
|
14
14
|
LEGEND_PILL_PAD,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
TITLE_Y,
|
|
37
37
|
} from '../utils/title-constants';
|
|
38
38
|
import type { PaletteColors } from '../palettes';
|
|
39
|
-
import type { D3ExportDimensions } from '../d3';
|
|
39
|
+
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
40
40
|
import type {
|
|
41
41
|
ResolvedSchedule,
|
|
42
42
|
ResolvedTask,
|
|
@@ -2,6 +2,7 @@ import { resolveColorWithDiagnostic } from '../colors';
|
|
|
2
2
|
import type { DgmoError } from '../diagnostics';
|
|
3
3
|
import type { PaletteColors } from '../palettes';
|
|
4
4
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
5
|
+
import { parseInArrowLabel, matchColorParens } from '../utils/arrows';
|
|
5
6
|
import {
|
|
6
7
|
measureIndent,
|
|
7
8
|
extractColor,
|
|
@@ -87,15 +88,17 @@ function parseNodeRef(text: string, palette?: PaletteColors): NodeRef | null {
|
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Split a line into segments around arrow tokens.
|
|
90
|
-
* Arrows: `->`, `-label->`, `-(color)->`, `-label(color)
|
|
91
|
+
* Arrows: `->`, `-label->`, `-(color)->`, `-label(color)->`, and long-dash
|
|
92
|
+
* variants like `-->`, `--->`, `--foo--->` (TD-9 longest-match: the arrow
|
|
93
|
+
* token is the maximal run of `-+>`).
|
|
91
94
|
*
|
|
92
95
|
* Returns alternating: [nodeText, arrowText, nodeText, arrowText, nodeText, ...]
|
|
93
|
-
* Where arrowText is the full arrow token like `-yes->` or
|
|
96
|
+
* Where arrowText is the synthesized full arrow token like `-yes->` or `->`
|
|
97
|
+
* (with visual dash-run length collapsed to the minimal `-...->` form —
|
|
98
|
+
* edge styling is not yet differentiated by arrow length).
|
|
94
99
|
*/
|
|
95
100
|
function splitArrows(line: string): string[] {
|
|
96
101
|
const segments: string[] = [];
|
|
97
|
-
let lastIndex = 0;
|
|
98
|
-
// Simpler approach: find all `->` positions, then determine if there's a label prefix
|
|
99
102
|
const arrowPositions: {
|
|
100
103
|
start: number;
|
|
101
104
|
end: number;
|
|
@@ -103,60 +106,84 @@ function splitArrows(line: string): string[] {
|
|
|
103
106
|
color?: string;
|
|
104
107
|
}[] = [];
|
|
105
108
|
|
|
106
|
-
// Find all
|
|
109
|
+
// Find all arrow tokens. A token is a maximal run of `-+>` (one-or-more
|
|
110
|
+
// dashes followed by `>`). We scan for `->` and then expand leftward across
|
|
111
|
+
// adjacent dashes to absorb longer forms. `scanFloor` marks the lower
|
|
112
|
+
// bound for the next opening-dash search so an arrow's opening cannot
|
|
113
|
+
// reach back into the territory of a previously consumed arrow.
|
|
107
114
|
let searchFrom = 0;
|
|
115
|
+
let scanFloor = 0;
|
|
108
116
|
while (searchFrom < line.length) {
|
|
109
117
|
const idx = line.indexOf('->', searchFrom);
|
|
110
118
|
if (idx === -1) break;
|
|
111
119
|
|
|
112
|
-
//
|
|
113
|
-
|
|
120
|
+
// TD-9: absorb the full arrow run leftward from idx, but not past the
|
|
121
|
+
// scanFloor (which is right after the previous arrow).
|
|
122
|
+
let runStart = idx;
|
|
123
|
+
while (runStart > scanFloor && line[runStart - 1] === '-') runStart--;
|
|
124
|
+
const arrowEnd = idx + 2; // position after `>`
|
|
125
|
+
|
|
126
|
+
// Look for an opening dash run before the arrow. The opening is the
|
|
127
|
+
// LEFTMOST `-` in the region `[scanFloor, runStart)` that is preceded by
|
|
128
|
+
// whitespace or start-of-line. Any dashes to its right up to the first
|
|
129
|
+
// non-dash character are part of the opening run; content after that is
|
|
130
|
+
// the label; the full arrow token runs from opening through `>`.
|
|
131
|
+
let arrowStart: number;
|
|
114
132
|
let label: string | undefined;
|
|
115
133
|
let color: string | undefined;
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
while (scanBack > 0 && line[scanBack] !== '-') {
|
|
126
|
-
scanBack--;
|
|
135
|
+
let openingStart = -1;
|
|
136
|
+
for (let i = scanFloor; i < runStart; i++) {
|
|
137
|
+
if (line[i] !== '-') continue;
|
|
138
|
+
const prevIsWsOrFloor =
|
|
139
|
+
i === 0 || i === scanFloor || /\s/.test(line[i - 1]);
|
|
140
|
+
if (prevIsWsOrFloor) {
|
|
141
|
+
openingStart = i;
|
|
142
|
+
break;
|
|
127
143
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (labelPart) label = labelPart;
|
|
146
|
-
}
|
|
147
|
-
arrowStart = scanBack;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (openingStart !== -1) {
|
|
147
|
+
// End of opening run: consume consecutive dashes after openingStart.
|
|
148
|
+
let openingEnd = openingStart;
|
|
149
|
+
while (openingEnd < runStart && line[openingEnd] === '-') openingEnd++;
|
|
150
|
+
|
|
151
|
+
// Label content = everything between opening run and the arrow run.
|
|
152
|
+
const arrowContent = line.substring(openingEnd, runStart);
|
|
153
|
+
const colorMatch = arrowContent.match(/\(([^)]+)\)\s*$/);
|
|
154
|
+
if (colorMatch) {
|
|
155
|
+
color = colorMatch[1].trim();
|
|
156
|
+
const labelPart = arrowContent.substring(0, colorMatch.index!).trim();
|
|
157
|
+
if (labelPart) label = labelPart;
|
|
158
|
+
} else {
|
|
159
|
+
const labelPart = arrowContent.trim();
|
|
160
|
+
if (labelPart) label = labelPart;
|
|
148
161
|
}
|
|
162
|
+
arrowStart = openingStart;
|
|
163
|
+
} else {
|
|
164
|
+
// No opening dash run found. All absorbed leftward dashes belong to
|
|
165
|
+
// the arrow token itself (e.g. `A --> B` → arrow is `-->`, no label).
|
|
166
|
+
arrowStart = runStart;
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
arrowPositions.push({ start: arrowStart, end:
|
|
152
|
-
searchFrom =
|
|
169
|
+
arrowPositions.push({ start: arrowStart, end: arrowEnd, label, color });
|
|
170
|
+
searchFrom = arrowEnd;
|
|
171
|
+
scanFloor = arrowEnd;
|
|
153
172
|
}
|
|
154
173
|
|
|
155
174
|
if (arrowPositions.length === 0) {
|
|
156
175
|
return [line];
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
// Build segments
|
|
178
|
+
// Build segments.
|
|
179
|
+
//
|
|
180
|
+
// NOTE: the synthesized arrow token is always the short form (`->`,
|
|
181
|
+
// `-label->`, `-(color)->`). The actual dash run-length (`-->`, `--->`,
|
|
182
|
+
// `---->`) seen in the source is collapsed here. If we ever add
|
|
183
|
+
// dash-length-sensitive edge styling (e.g. Mermaid-style "long arrow"
|
|
184
|
+
// emphasis), thread `arrow.end - arrow.start - label?.length - color?.length`
|
|
185
|
+
// through to ArrowInfo so downstream renderers can honor it.
|
|
186
|
+
let lastIndex = 0;
|
|
160
187
|
for (let i = 0; i < arrowPositions.length; i++) {
|
|
161
188
|
const arrow = arrowPositions[i];
|
|
162
189
|
const beforeText = line.substring(lastIndex, arrow.start).trim();
|
|
@@ -193,22 +220,32 @@ function parseArrowToken(
|
|
|
193
220
|
diagnostics: DgmoError[]
|
|
194
221
|
): ArrowInfo {
|
|
195
222
|
if (token === '->') return {};
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
// TD-11: `-(X)->` is a color if and only if `X` is one of the 11 recognized
|
|
224
|
+
// palette color names. Otherwise the entire `(X)` becomes the label.
|
|
225
|
+
// Delegate the recognition rule to the shared `matchColorParens` helper.
|
|
226
|
+
const bareParen = token.match(/^-(\([A-Za-z]+\))->$/);
|
|
227
|
+
if (bareParen) {
|
|
228
|
+
const colorName = matchColorParens(bareParen[1]);
|
|
229
|
+
if (colorName) {
|
|
230
|
+
return {
|
|
231
|
+
color: resolveColorWithDiagnostic(
|
|
232
|
+
colorName,
|
|
233
|
+
lineNumber,
|
|
234
|
+
diagnostics,
|
|
235
|
+
palette
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Unrecognized color name → whole `(X)` is the label (fall through).
|
|
207
240
|
}
|
|
208
241
|
// -label(color)-> or -label->
|
|
209
242
|
const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
|
|
210
243
|
if (m) {
|
|
211
|
-
const
|
|
244
|
+
const rawLabel = m[1] ?? '';
|
|
245
|
+
// Route label through TD-13/TD-14 validator.
|
|
246
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
247
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
248
|
+
const label = labelResult.label;
|
|
212
249
|
let color = m[2]
|
|
213
250
|
? resolveColorWithDiagnostic(
|
|
214
251
|
m[2].trim(),
|