@diagrammo/dgmo 0.8.18 → 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 +1202 -993
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -114
- package/dist/index.d.ts +216 -114
- package/dist/index.js +1211 -985
- 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 +153 -91
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/state-parser.ts +60 -35
- package/src/index.ts +23 -18
- 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/collapse.ts +169 -0
- package/src/sequence/parser.ts +21 -4
- package/src/sequence/renderer.ts +198 -52
- package/src/sharing.ts +86 -49
- 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 +11 -4
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +140 -13
- package/src/utils/legend-types.ts +45 -0
- 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/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,
|
|
@@ -20,9 +20,11 @@ import {
|
|
|
20
20
|
LEGEND_ENTRY_TRAIL,
|
|
21
21
|
LEGEND_GROUP_GAP,
|
|
22
22
|
LEGEND_ICON_W,
|
|
23
|
+
LEGEND_GEAR_PILL_W,
|
|
23
24
|
measureLegendText,
|
|
24
25
|
} from '../utils/legend-constants';
|
|
25
26
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
27
|
+
import { controlsGroupCapsuleWidth } from '../utils/legend-layout';
|
|
26
28
|
import type {
|
|
27
29
|
LegendConfig,
|
|
28
30
|
LegendState,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
TITLE_Y,
|
|
35
37
|
} from '../utils/title-constants';
|
|
36
38
|
import type { PaletteColors } from '../palettes';
|
|
37
|
-
import type { D3ExportDimensions } from '../d3';
|
|
39
|
+
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
38
40
|
import type {
|
|
39
41
|
ResolvedSchedule,
|
|
40
42
|
ResolvedTask,
|
|
@@ -233,6 +235,8 @@ export function renderGantt(
|
|
|
233
235
|
options?.currentActiveGroup
|
|
234
236
|
);
|
|
235
237
|
let criticalPathActive = false;
|
|
238
|
+
let dependenciesActive = !!resolved.options.dependencies;
|
|
239
|
+
let controlsExpanded = false;
|
|
236
240
|
|
|
237
241
|
// ── Build row list (structural vs tag mode) ─────────────
|
|
238
242
|
|
|
@@ -264,11 +268,21 @@ export function renderGantt(
|
|
|
264
268
|
|
|
265
269
|
const totalRows = rows.length;
|
|
266
270
|
|
|
271
|
+
// Pre-compute critical path / dependency flags (needed for legend height reservation)
|
|
272
|
+
const hasCriticalPath =
|
|
273
|
+
resolved.options.criticalPath &&
|
|
274
|
+
resolved.tasks.some((t) => t.isCriticalPath);
|
|
275
|
+
const hasDependencies =
|
|
276
|
+
resolved.options.dependencies &&
|
|
277
|
+
resolved.tasks.some((t) => t.task.dependencies.length > 0);
|
|
278
|
+
|
|
267
279
|
// Vertical layout — matches timeline pattern (d3.ts:3649-3655)
|
|
268
280
|
const title = resolved.options.title;
|
|
269
281
|
const titleHeight = title ? 50 : 20;
|
|
270
282
|
const tagLegendReserve =
|
|
271
|
-
resolved.tagGroups.length > 0
|
|
283
|
+
resolved.tagGroups.length > 0 || hasCriticalPath || hasDependencies
|
|
284
|
+
? LEGEND_HEIGHT + 8
|
|
285
|
+
: 0;
|
|
272
286
|
const topDateLabelReserve = 22; // tick (6) + gap (4) + label height (~12)
|
|
273
287
|
const hasOverheadLabels =
|
|
274
288
|
resolved.markers.length > 0 || resolved.eras.length > 0;
|
|
@@ -327,13 +341,9 @@ export function renderGantt(
|
|
|
327
341
|
|
|
328
342
|
// ── Tag legend (interactive) ────────────────────────────
|
|
329
343
|
|
|
330
|
-
const hasCriticalPath =
|
|
331
|
-
resolved.options.criticalPath &&
|
|
332
|
-
resolved.tasks.some((t) => t.isCriticalPath);
|
|
333
|
-
|
|
334
344
|
function drawLegend() {
|
|
335
345
|
svg.selectAll('.gantt-tag-legend-container').remove();
|
|
336
|
-
if (resolved.tagGroups.length > 0 || hasCriticalPath) {
|
|
346
|
+
if (resolved.tagGroups.length > 0 || hasCriticalPath || hasDependencies) {
|
|
337
347
|
const legendY = titleHeight;
|
|
338
348
|
renderTagLegend(
|
|
339
349
|
svg,
|
|
@@ -359,17 +369,41 @@ export function renderGantt(
|
|
|
359
369
|
recolorBars();
|
|
360
370
|
},
|
|
361
371
|
() => {
|
|
362
|
-
|
|
372
|
+
controlsExpanded = !controlsExpanded;
|
|
363
373
|
drawLegend();
|
|
364
374
|
},
|
|
365
375
|
currentSwimlaneGroup,
|
|
366
376
|
onSwimlaneChange,
|
|
367
377
|
viewMode,
|
|
368
|
-
resolved.tasks
|
|
378
|
+
resolved.tasks,
|
|
379
|
+
controlsExpanded,
|
|
380
|
+
hasDependencies,
|
|
381
|
+
dependenciesActive,
|
|
382
|
+
(toggleId, active) => {
|
|
383
|
+
if (toggleId === 'critical-path') {
|
|
384
|
+
criticalPathActive = active;
|
|
385
|
+
} else if (toggleId === 'dependencies') {
|
|
386
|
+
dependenciesActive = active;
|
|
387
|
+
// Show/hide dependency arrows
|
|
388
|
+
g.selectAll<SVGElement, unknown>(
|
|
389
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead, .gantt-dep-label'
|
|
390
|
+
).attr('display', active ? null : 'none');
|
|
391
|
+
}
|
|
392
|
+
drawLegend();
|
|
393
|
+
}
|
|
369
394
|
);
|
|
370
395
|
}
|
|
371
396
|
}
|
|
372
397
|
|
|
398
|
+
function restoreHighlight() {
|
|
399
|
+
if (criticalPathActive) {
|
|
400
|
+
applyCriticalPathHighlight(svg, g);
|
|
401
|
+
} else {
|
|
402
|
+
svg.attr('data-critical-path-active', null);
|
|
403
|
+
resetHighlight(g, svg);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
373
407
|
function recolorBars() {
|
|
374
408
|
g.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
375
409
|
const el = d3Selection.select(this);
|
|
@@ -568,7 +602,7 @@ export function renderGantt(
|
|
|
568
602
|
}
|
|
569
603
|
})
|
|
570
604
|
.on('mouseleave', () => {
|
|
571
|
-
|
|
605
|
+
restoreHighlight();
|
|
572
606
|
hideGanttDateIndicators(g);
|
|
573
607
|
});
|
|
574
608
|
|
|
@@ -979,7 +1013,7 @@ export function renderGantt(
|
|
|
979
1013
|
.text(task.label);
|
|
980
1014
|
})
|
|
981
1015
|
.on('mouseleave', () => {
|
|
982
|
-
|
|
1016
|
+
restoreHighlight();
|
|
983
1017
|
hideGanttDateIndicators(g);
|
|
984
1018
|
g.selectAll('.gantt-milestone-hover-label').remove();
|
|
985
1019
|
});
|
|
@@ -1023,11 +1057,7 @@ export function renderGantt(
|
|
|
1023
1057
|
})
|
|
1024
1058
|
.on('mouseleave', () => {
|
|
1025
1059
|
if (resolved.options.dependencies) {
|
|
1026
|
-
|
|
1027
|
-
applyCriticalPathHighlight(svg, g);
|
|
1028
|
-
} else {
|
|
1029
|
-
resetHighlight(g, svg);
|
|
1030
|
-
}
|
|
1060
|
+
restoreHighlight();
|
|
1031
1061
|
}
|
|
1032
1062
|
resetTaskLabels(svg);
|
|
1033
1063
|
hideGanttDateIndicators(g);
|
|
@@ -1704,6 +1734,7 @@ function applyCriticalPathHighlight(
|
|
|
1704
1734
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1705
1735
|
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined>
|
|
1706
1736
|
) {
|
|
1737
|
+
svg.attr('data-critical-path-active', 'true');
|
|
1707
1738
|
chartG.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
1708
1739
|
const el = d3Selection.select(this);
|
|
1709
1740
|
el.attr(
|
|
@@ -1827,6 +1858,34 @@ function drawSwimlaneIcon(
|
|
|
1827
1858
|
return iconG;
|
|
1828
1859
|
}
|
|
1829
1860
|
|
|
1861
|
+
function buildControlsToggles(
|
|
1862
|
+
hasCriticalPath: boolean,
|
|
1863
|
+
criticalPathActive: boolean,
|
|
1864
|
+
hasDependencies: boolean,
|
|
1865
|
+
dependenciesActive: boolean
|
|
1866
|
+
): import('../utils/legend-types').ControlsGroupToggle[] {
|
|
1867
|
+
const toggles: import('../utils/legend-types').ControlsGroupToggle[] = [];
|
|
1868
|
+
if (hasCriticalPath) {
|
|
1869
|
+
toggles.push({
|
|
1870
|
+
id: 'critical-path',
|
|
1871
|
+
type: 'toggle',
|
|
1872
|
+
label: 'Critical Path',
|
|
1873
|
+
active: criticalPathActive,
|
|
1874
|
+
onToggle: () => {},
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
if (hasDependencies) {
|
|
1878
|
+
toggles.push({
|
|
1879
|
+
id: 'dependencies',
|
|
1880
|
+
type: 'toggle',
|
|
1881
|
+
label: 'Dependencies',
|
|
1882
|
+
active: dependenciesActive,
|
|
1883
|
+
onToggle: () => {},
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
return toggles;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1830
1889
|
function renderTagLegend(
|
|
1831
1890
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1832
1891
|
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
@@ -1841,16 +1900,16 @@ function renderTagLegend(
|
|
|
1841
1900
|
criticalPathActive: boolean,
|
|
1842
1901
|
optionLineNumbers: Record<string, number>,
|
|
1843
1902
|
onToggle?: (groupName: string) => void,
|
|
1844
|
-
|
|
1903
|
+
onToggleControlsExpand?: () => void,
|
|
1845
1904
|
currentSwimlaneGroup?: string | null,
|
|
1846
1905
|
onSwimlaneChange?: (group: string | null) => void,
|
|
1847
1906
|
legendViewMode?: boolean,
|
|
1848
|
-
resolvedTasks?: ResolvedTask[]
|
|
1907
|
+
resolvedTasks?: ResolvedTask[],
|
|
1908
|
+
controlsExpanded = false,
|
|
1909
|
+
hasDependencies = false,
|
|
1910
|
+
dependenciesActive = false,
|
|
1911
|
+
onControlsToggle?: (toggleId: string, active: boolean) => void
|
|
1849
1912
|
): void {
|
|
1850
|
-
const groupBg = isDark
|
|
1851
|
-
? mix(palette.surface, palette.bg, 50)
|
|
1852
|
-
: mix(palette.surface, palette.bg, 30);
|
|
1853
|
-
|
|
1854
1913
|
// Build visible groups: active group expanded + swimlane group as compact pill
|
|
1855
1914
|
let visibleGroups: TagGroup[];
|
|
1856
1915
|
if (activeGroupName) {
|
|
@@ -1934,13 +1993,16 @@ function renderTagLegend(
|
|
|
1934
1993
|
}
|
|
1935
1994
|
totalW += Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
|
|
1936
1995
|
|
|
1937
|
-
//
|
|
1938
|
-
const
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
if (
|
|
1996
|
+
// Controls group width — replaces standalone critical path pill
|
|
1997
|
+
const hasControls = hasCriticalPath || hasDependencies;
|
|
1998
|
+
const controlsToggleLabels: Array<{ label: string }> = [];
|
|
1999
|
+
if (hasCriticalPath) controlsToggleLabels.push({ label: 'Critical Path' });
|
|
2000
|
+
if (hasDependencies) controlsToggleLabels.push({ label: 'Dependencies' });
|
|
2001
|
+
if (hasControls) {
|
|
1942
2002
|
if (visibleGroups.length > 0) totalW += LEGEND_GROUP_GAP;
|
|
1943
|
-
totalW +=
|
|
2003
|
+
totalW += controlsExpanded
|
|
2004
|
+
? controlsGroupCapsuleWidth(controlsToggleLabels)
|
|
2005
|
+
: LEGEND_GEAR_PILL_W;
|
|
1944
2006
|
}
|
|
1945
2007
|
|
|
1946
2008
|
// Center over full container (matching title centering)
|
|
@@ -1952,8 +2014,6 @@ function renderTagLegend(
|
|
|
1952
2014
|
.attr('class', 'gantt-tag-legend-container')
|
|
1953
2015
|
.attr('transform', `translate(${legendX}, ${legendY})`);
|
|
1954
2016
|
|
|
1955
|
-
let cursorX = 0;
|
|
1956
|
-
|
|
1957
2017
|
// Render tag groups via centralized legend system
|
|
1958
2018
|
if (visibleGroups.length > 0) {
|
|
1959
2019
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
@@ -1969,6 +2029,13 @@ function renderTagLegend(
|
|
|
1969
2029
|
};
|
|
1970
2030
|
});
|
|
1971
2031
|
|
|
2032
|
+
const controlsToggles = buildControlsToggles(
|
|
2033
|
+
hasCriticalPath,
|
|
2034
|
+
criticalPathActive,
|
|
2035
|
+
hasDependencies,
|
|
2036
|
+
dependenciesActive
|
|
2037
|
+
);
|
|
2038
|
+
|
|
1972
2039
|
const legendConfig: LegendConfig = {
|
|
1973
2040
|
groups: legendGroups,
|
|
1974
2041
|
position: {
|
|
@@ -1977,16 +2044,30 @@ function renderTagLegend(
|
|
|
1977
2044
|
},
|
|
1978
2045
|
mode: 'fixed' as const,
|
|
1979
2046
|
capsulePillAddonWidth: iconReserve,
|
|
2047
|
+
controlsGroup:
|
|
2048
|
+
controlsToggles.length > 0 ? { toggles: controlsToggles } : undefined,
|
|
2049
|
+
};
|
|
2050
|
+
const legendState: LegendState = {
|
|
2051
|
+
activeGroup: activeGroupName,
|
|
2052
|
+
controlsExpanded,
|
|
1980
2053
|
};
|
|
1981
|
-
const legendState: LegendState = { activeGroup: activeGroupName };
|
|
1982
2054
|
|
|
1983
|
-
|
|
2055
|
+
let tagGroupsW =
|
|
1984
2056
|
visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) +
|
|
1985
2057
|
Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
|
|
2058
|
+
// Add controls group space to the renderLegendD3 container width
|
|
2059
|
+
if (hasControls) {
|
|
2060
|
+
if (visibleGroups.length > 0) tagGroupsW += LEGEND_GROUP_GAP;
|
|
2061
|
+
tagGroupsW += controlsExpanded
|
|
2062
|
+
? controlsGroupCapsuleWidth(controlsToggleLabels)
|
|
2063
|
+
: LEGEND_GEAR_PILL_W;
|
|
2064
|
+
}
|
|
1986
2065
|
const tagGroupG = legendRow.append('g');
|
|
1987
2066
|
|
|
1988
2067
|
const legendCallbacks: LegendCallbacks = {
|
|
1989
2068
|
onGroupToggle: onToggle,
|
|
2069
|
+
onControlsExpand: onToggleControlsExpand,
|
|
2070
|
+
onControlsToggle,
|
|
1990
2071
|
onEntryHover: (groupName, entryValue) => {
|
|
1991
2072
|
const tagKey = groupName.toLowerCase();
|
|
1992
2073
|
if (entryValue) {
|
|
@@ -2092,67 +2173,43 @@ function renderTagLegend(
|
|
|
2092
2173
|
legendCallbacks,
|
|
2093
2174
|
tagGroupsW
|
|
2094
2175
|
);
|
|
2176
|
+
} else if (hasControls) {
|
|
2177
|
+
// No tag groups, but controls group needs rendering
|
|
2178
|
+
const controlsToggles = buildControlsToggles(
|
|
2179
|
+
hasCriticalPath,
|
|
2180
|
+
criticalPathActive,
|
|
2181
|
+
hasDependencies,
|
|
2182
|
+
dependenciesActive
|
|
2183
|
+
);
|
|
2095
2184
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
.append('g')
|
|
2106
|
-
.attr('transform', `translate(${cursorX}, 0)`)
|
|
2107
|
-
.attr('class', 'gantt-legend-critical-path')
|
|
2108
|
-
.style('cursor', 'pointer')
|
|
2109
|
-
.on('click', () => {
|
|
2110
|
-
if (onToggleCriticalPath) onToggleCriticalPath();
|
|
2111
|
-
});
|
|
2112
|
-
if (cpLineNum) cpG.attr('data-line-number', String(cpLineNum));
|
|
2113
|
-
|
|
2114
|
-
cpG
|
|
2115
|
-
.append('rect')
|
|
2116
|
-
.attr('width', cpPillW)
|
|
2117
|
-
.attr('height', LEGEND_HEIGHT)
|
|
2118
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
2119
|
-
.attr('fill', criticalPathActive ? palette.bg : groupBg);
|
|
2120
|
-
|
|
2121
|
-
if (criticalPathActive) {
|
|
2122
|
-
cpG
|
|
2123
|
-
.append('rect')
|
|
2124
|
-
.attr('width', cpPillW)
|
|
2125
|
-
.attr('height', LEGEND_HEIGHT)
|
|
2126
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
2127
|
-
.attr('fill', 'none')
|
|
2128
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
2129
|
-
.attr('stroke-width', 0.75);
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
cpG
|
|
2133
|
-
.append('text')
|
|
2134
|
-
.attr('x', cpPillW / 2)
|
|
2135
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
2136
|
-
.attr('text-anchor', 'middle')
|
|
2137
|
-
.attr('font-size', `${LEGEND_PILL_FONT_SIZE}px`)
|
|
2138
|
-
.attr('font-weight', '500')
|
|
2139
|
-
.attr('fill', criticalPathActive ? palette.text : palette.textMuted)
|
|
2140
|
-
.text(cpLabel);
|
|
2185
|
+
const legendConfig: LegendConfig = {
|
|
2186
|
+
groups: [],
|
|
2187
|
+
position: {
|
|
2188
|
+
placement: 'top-center' as const,
|
|
2189
|
+
titleRelation: 'below-title' as const,
|
|
2190
|
+
},
|
|
2191
|
+
mode: 'fixed' as const,
|
|
2192
|
+
controlsGroup: { toggles: controlsToggles },
|
|
2193
|
+
};
|
|
2141
2194
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2195
|
+
const tagGroupG = legendRow.append('g');
|
|
2196
|
+
renderLegendD3(
|
|
2197
|
+
tagGroupG,
|
|
2198
|
+
legendConfig,
|
|
2199
|
+
{ activeGroup: null, controlsExpanded },
|
|
2200
|
+
palette,
|
|
2201
|
+
isDark,
|
|
2202
|
+
{
|
|
2203
|
+
onControlsExpand: onToggleControlsExpand,
|
|
2204
|
+
onControlsToggle,
|
|
2205
|
+
},
|
|
2206
|
+
totalW
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2146
2209
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
})
|
|
2151
|
-
.on('mouseleave', () => {
|
|
2152
|
-
if (!criticalPathActive) {
|
|
2153
|
-
resetHighlightAll(svg, chartG);
|
|
2154
|
-
}
|
|
2155
|
-
});
|
|
2210
|
+
// Apply persistent critical path highlighting when active
|
|
2211
|
+
if (criticalPathActive) {
|
|
2212
|
+
applyCriticalPathHighlight(svg, chartG);
|
|
2156
2213
|
}
|
|
2157
2214
|
}
|
|
2158
2215
|
|
|
@@ -2990,6 +3047,11 @@ function resetHighlight(
|
|
|
2990
3047
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
2991
3048
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>
|
|
2992
3049
|
): void {
|
|
3050
|
+
// If critical path is actively toggled ON, restore its highlight instead of full reset
|
|
3051
|
+
if (svg.attr('data-critical-path-active') === 'true') {
|
|
3052
|
+
applyCriticalPathHighlight(svg, g);
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
2993
3055
|
g.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone').attr(
|
|
2994
3056
|
'opacity',
|
|
2995
3057
|
1
|
|
@@ -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(),
|