@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/dgmo-router.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { parseInfra } from './infra/parser';
|
|
|
18
18
|
import { parseGantt } from './gantt/parser';
|
|
19
19
|
import { parseBoxesAndLines } from './boxes-and-lines/parser';
|
|
20
20
|
import { parseFirstLine } from './utils/parsing';
|
|
21
|
+
import { makeDgmoError, suggest } from './diagnostics';
|
|
21
22
|
import type { DgmoError } from './diagnostics';
|
|
22
23
|
|
|
23
24
|
// ============================================================
|
|
@@ -229,6 +230,17 @@ const PARSE_DISPATCH = new Map<
|
|
|
229
230
|
['boxes-and-lines', (c) => parseBoxesAndLines(c)],
|
|
230
231
|
]);
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Parse DGMO content and return diagnostics without rendering.
|
|
235
|
+
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
236
|
+
*/
|
|
237
|
+
/** All known chart type names for colon-pattern detection. */
|
|
238
|
+
const ALL_KNOWN_TYPES = new Set([
|
|
239
|
+
...DATA_CHART_TYPES,
|
|
240
|
+
...VISUALIZATION_TYPES,
|
|
241
|
+
...DIAGRAM_TYPES,
|
|
242
|
+
]);
|
|
243
|
+
|
|
232
244
|
/**
|
|
233
245
|
* Parse DGMO content and return diagnostics without rendering.
|
|
234
246
|
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
@@ -237,20 +249,100 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
|
237
249
|
const chartType = parseDgmoChartType(content);
|
|
238
250
|
|
|
239
251
|
if (!chartType) {
|
|
240
|
-
//
|
|
252
|
+
// Check for common mistake: colon in chart type declaration (e.g. "bar: Sales")
|
|
253
|
+
const colonDiag = detectColonChartType(content);
|
|
254
|
+
if (colonDiag) {
|
|
255
|
+
const fallback = parseVisualization(content).diagnostics;
|
|
256
|
+
return { diagnostics: [colonDiag, ...fallback] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// No chart type detected — try visualization parser as fallback
|
|
241
260
|
return { diagnostics: parseVisualization(content).diagnostics };
|
|
242
261
|
}
|
|
243
262
|
|
|
244
263
|
const directParser = PARSE_DISPATCH.get(chartType);
|
|
245
|
-
if (directParser)
|
|
264
|
+
if (directParser) {
|
|
265
|
+
const result = directParser(content);
|
|
266
|
+
return {
|
|
267
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
246
270
|
|
|
247
271
|
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
248
|
-
|
|
272
|
+
const result = parseChart(content);
|
|
273
|
+
return {
|
|
274
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
275
|
+
};
|
|
249
276
|
}
|
|
250
277
|
if (ECHART_TYPES.has(chartType)) {
|
|
251
|
-
|
|
278
|
+
const result = parseExtendedChart(content);
|
|
279
|
+
return {
|
|
280
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
281
|
+
};
|
|
252
282
|
}
|
|
253
283
|
|
|
254
284
|
// Visualization types (slope, wordcloud, arc, timeline, venn, quadrant)
|
|
255
|
-
|
|
285
|
+
const result = parseVisualization(content);
|
|
286
|
+
return {
|
|
287
|
+
diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================
|
|
292
|
+
// Common-mistake detectors
|
|
293
|
+
// ============================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detects colon-separated chart type declarations like "bar: Sales" or "pie: Data".
|
|
297
|
+
* Returns a diagnostic if the word before the colon is a known or similar chart type.
|
|
298
|
+
*/
|
|
299
|
+
function detectColonChartType(content: string): DgmoError | null {
|
|
300
|
+
const lines = content.split('\n');
|
|
301
|
+
for (let i = 0; i < lines.length; i++) {
|
|
302
|
+
const trimmed = lines[i].trim();
|
|
303
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//'))
|
|
304
|
+
continue;
|
|
305
|
+
|
|
306
|
+
const match = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
307
|
+
if (!match) return null; // First non-empty line doesn't match colon pattern
|
|
308
|
+
|
|
309
|
+
const word = match[1].toLowerCase();
|
|
310
|
+
const rest = match[2].trim();
|
|
311
|
+
|
|
312
|
+
if (ALL_KNOWN_TYPES.has(word)) {
|
|
313
|
+
const example = rest ? `${word} ${rest}` : word;
|
|
314
|
+
return makeDgmoError(
|
|
315
|
+
i + 1,
|
|
316
|
+
`Remove the colon — use '${example}' instead of '${trimmed}'. DGMO chart types don't use colons.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if it's a misspelling of a known type
|
|
321
|
+
const hint = suggest(word, [...ALL_KNOWN_TYPES]);
|
|
322
|
+
if (hint) {
|
|
323
|
+
return makeDgmoError(
|
|
324
|
+
i + 1,
|
|
325
|
+
`Unknown chart type: ${word}. ${hint} Also, DGMO chart types don't use colons.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return null; // First line has colon but isn't a chart type — normal data
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Detects when content has only the chart type line with no meaningful data lines.
|
|
336
|
+
*/
|
|
337
|
+
function detectEmptyContent(content: string): DgmoError[] {
|
|
338
|
+
const lines = content.split('\n');
|
|
339
|
+
const nonEmpty = lines.filter(
|
|
340
|
+
(l) => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//')
|
|
341
|
+
);
|
|
342
|
+
if (nonEmpty.length <= 1) {
|
|
343
|
+
return [
|
|
344
|
+
makeDgmoError(1, 'No content after chart type declaration.', 'warning'),
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
return [];
|
|
256
348
|
}
|
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/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(),
|
|
@@ -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,
|
|
@@ -31,6 +32,8 @@ const GROUP_BRACKET_RE = /^\[([^\]]+)\](?:\(([^)]+)\))?\s*$/;
|
|
|
31
32
|
* Arrows: `->`, `-label->`, `-(color)->`, `-label(color)->`
|
|
32
33
|
*/
|
|
33
34
|
function splitArrows(line: string): string[] {
|
|
35
|
+
// Mirrors flowchart-parser.ts splitArrows. TD-9 longest-match: arrow token
|
|
36
|
+
// is the maximal run of `-+>`. See that file for the full algorithm rationale.
|
|
34
37
|
const segments: string[] = [];
|
|
35
38
|
const arrowPositions: {
|
|
36
39
|
start: number;
|
|
@@ -40,41 +43,52 @@ function splitArrows(line: string): string[] {
|
|
|
40
43
|
}[] = [];
|
|
41
44
|
|
|
42
45
|
let searchFrom = 0;
|
|
46
|
+
let scanFloor = 0;
|
|
43
47
|
while (searchFrom < line.length) {
|
|
44
48
|
const idx = line.indexOf('->', searchFrom);
|
|
45
49
|
if (idx === -1) break;
|
|
46
50
|
|
|
47
|
-
let
|
|
51
|
+
let runStart = idx;
|
|
52
|
+
while (runStart > scanFloor && line[runStart - 1] === '-') runStart--;
|
|
53
|
+
const arrowEnd = idx + 2;
|
|
54
|
+
|
|
55
|
+
let arrowStart: number;
|
|
48
56
|
let label: string | undefined;
|
|
49
57
|
let color: string | undefined;
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
let openingStart = -1;
|
|
60
|
+
for (let i = scanFloor; i < runStart; i++) {
|
|
61
|
+
if (line[i] !== '-') continue;
|
|
62
|
+
const prevIsWsOrFloor =
|
|
63
|
+
i === 0 || i === scanFloor || /\s/.test(line[i - 1]);
|
|
64
|
+
if (prevIsWsOrFloor) {
|
|
65
|
+
openingStart = i;
|
|
66
|
+
break;
|
|
55
67
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
arrowStart = scanBack;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (openingStart !== -1) {
|
|
71
|
+
let openingEnd = openingStart;
|
|
72
|
+
while (openingEnd < runStart && line[openingEnd] === '-') openingEnd++;
|
|
73
|
+
|
|
74
|
+
const arrowContent = line.substring(openingEnd, runStart);
|
|
75
|
+
const colorMatch = arrowContent.match(/\(([^)]+)\)\s*$/);
|
|
76
|
+
if (colorMatch) {
|
|
77
|
+
color = colorMatch[1].trim();
|
|
78
|
+
const labelPart = arrowContent.substring(0, colorMatch.index!).trim();
|
|
79
|
+
if (labelPart) label = labelPart;
|
|
80
|
+
} else {
|
|
81
|
+
const labelPart = arrowContent.trim();
|
|
82
|
+
if (labelPart) label = labelPart;
|
|
73
83
|
}
|
|
84
|
+
arrowStart = openingStart;
|
|
85
|
+
} else {
|
|
86
|
+
arrowStart = runStart;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
arrowPositions.push({ start: arrowStart, end:
|
|
77
|
-
searchFrom =
|
|
89
|
+
arrowPositions.push({ start: arrowStart, end: arrowEnd, label, color });
|
|
90
|
+
searchFrom = arrowEnd;
|
|
91
|
+
scanFloor = arrowEnd;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
if (arrowPositions.length === 0) return [line];
|
|
@@ -111,19 +125,30 @@ function parseArrowToken(
|
|
|
111
125
|
diagnostics: DgmoError[]
|
|
112
126
|
): ArrowInfo {
|
|
113
127
|
if (token === '->') return {};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
// TD-11: `-(X)->` is a color if and only if X is a recognized palette
|
|
129
|
+
// color; otherwise the whole `(X)` becomes the label. Delegate recognition
|
|
130
|
+
// to the shared `matchColorParens` helper.
|
|
131
|
+
const bareParen = token.match(/^-(\([A-Za-z]+\))->$/);
|
|
132
|
+
if (bareParen) {
|
|
133
|
+
const colorName = matchColorParens(bareParen[1]);
|
|
134
|
+
if (colorName) {
|
|
135
|
+
return {
|
|
136
|
+
color: resolveColorWithDiagnostic(
|
|
137
|
+
colorName,
|
|
138
|
+
lineNumber,
|
|
139
|
+
diagnostics,
|
|
140
|
+
palette
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// fall through — whole `(X)` becomes label
|
|
145
|
+
}
|
|
124
146
|
const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
|
|
125
147
|
if (m) {
|
|
126
|
-
const
|
|
148
|
+
const rawLabel = m[1] ?? '';
|
|
149
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
150
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
151
|
+
const label = labelResult.label;
|
|
127
152
|
const color = m[2]
|
|
128
153
|
? resolveColorWithDiagnostic(
|
|
129
154
|
m[2].trim(),
|