@diagrammo/dgmo 0.26.0 → 0.27.0
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/README.md +3 -3
- package/dist/advanced.cjs +4182 -2704
- package/dist/advanced.d.cts +266 -58
- package/dist/advanced.d.ts +266 -58
- package/dist/advanced.js +4182 -2698
- package/dist/auto.cjs +4042 -2581
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4042 -2581
- package/dist/cli.cjs +172 -170
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +4067 -2583
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4067 -2583
- package/dist/internal.cjs +4182 -2704
- package/dist/internal.d.cts +266 -58
- package/dist/internal.d.ts +266 -58
- package/dist/internal.js +4182 -2698
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +1 -1
- package/src/advanced.ts +1 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -0
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion.ts +26 -12
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -0
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +34 -1
- package/src/graph/flowchart-renderer.ts +78 -64
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +78 -64
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +57 -12
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1196 -90
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +34 -68
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
map Smuggler's Run
|
|
2
|
+
|
|
3
|
+
tag Leg as l
|
|
4
|
+
Sail blue
|
|
5
|
+
March green
|
|
6
|
+
Row orange
|
|
7
|
+
|
|
8
|
+
poi 20.05 -72.82 as tor label: Tortuga
|
|
9
|
+
poi 23.13 -82.38 as hav label: Havana
|
|
10
|
+
poi 18.02 -76.79 as cove label: Pirate's Cove
|
|
11
|
+
poi 25.06 -77.35 as nas label: Nassau
|
|
12
|
+
|
|
13
|
+
tor ~> hav l: Sail
|
|
14
|
+
hav ~> cove l: Sail
|
|
15
|
+
cove -> nas l: March
|
|
16
|
+
nas ~> tor l: Row
|
package/package.json
CHANGED
package/src/advanced.ts
CHANGED
|
@@ -585,6 +585,7 @@ export type {
|
|
|
585
585
|
BoundaryTopology,
|
|
586
586
|
RegionName,
|
|
587
587
|
RegionNames,
|
|
588
|
+
AirportData,
|
|
588
589
|
} from './map/data/types';
|
|
589
590
|
|
|
590
591
|
export type { RaciDragSource, RaciInteractionHandlers } from './raci';
|
|
@@ -728,18 +729,12 @@ export {
|
|
|
728
729
|
contrastText,
|
|
729
730
|
// Palette definitions
|
|
730
731
|
nordPalette,
|
|
731
|
-
solarizedPalette,
|
|
732
732
|
catppuccinPalette,
|
|
733
|
-
rosePinePalette,
|
|
734
|
-
gruvboxPalette,
|
|
735
733
|
tokyoNightPalette,
|
|
736
|
-
oneDarkPalette,
|
|
737
734
|
atlasPalette,
|
|
738
735
|
blueprintPalette,
|
|
739
736
|
slatePalette,
|
|
740
737
|
tidewaterPalette,
|
|
741
|
-
draculaPalette,
|
|
742
|
-
monokaiPalette,
|
|
743
738
|
} from './palettes';
|
|
744
739
|
|
|
745
740
|
export type { PaletteConfig, PaletteColors } from './palettes';
|
package/src/auto/index.ts
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
12
12
|
import type { ParsedBoxesAndLines, BLNode, BLGroup } from './types';
|
|
13
|
+
import { measureText, wrapTextToWidth } from '../utils/text-measure';
|
|
14
|
+
import {
|
|
15
|
+
resolveNotes,
|
|
16
|
+
buildPlacedNotes,
|
|
17
|
+
noteCanvasShift,
|
|
18
|
+
type PlacedNote,
|
|
19
|
+
} from '../utils/notes';
|
|
13
20
|
|
|
14
21
|
// ── Constants ──────────────────────────────────────────────
|
|
15
22
|
const MARGIN = 40;
|
|
@@ -31,6 +38,11 @@ const MAX_DESC_LINES = 6;
|
|
|
31
38
|
const MAX_LABEL_LINES = 3;
|
|
32
39
|
const LABEL_LINE_HEIGHT = 1.3;
|
|
33
40
|
const LABEL_PAD = 12;
|
|
41
|
+
// Bottom value-row reserved on a DESCRIBED node under `show-values`: a thin
|
|
42
|
+
// divider + a "Metric: value" footer line (replaces the old corner badge).
|
|
43
|
+
const VALUE_ROW_FONT = 11;
|
|
44
|
+
const VALUE_ROW_H =
|
|
45
|
+
SEPARATOR_GAP + VALUE_ROW_FONT * DESC_LINE_HEIGHT + DESC_PADDING;
|
|
34
46
|
|
|
35
47
|
// ── Result types ───────────────────────────────────────────
|
|
36
48
|
|
|
@@ -40,6 +52,8 @@ export interface BLLayoutNode {
|
|
|
40
52
|
readonly y: number;
|
|
41
53
|
readonly width: number;
|
|
42
54
|
readonly height: number;
|
|
55
|
+
/** A note floated beside this box (never moves the box). */
|
|
56
|
+
readonly note?: PlacedNote;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
export interface BLLayoutEdge {
|
|
@@ -113,15 +127,14 @@ function estimateLabelLines(label: string, nodeWidth = NODE_WIDTH): number {
|
|
|
113
127
|
if (!part) continue;
|
|
114
128
|
words.push(...splitCamelCase(part));
|
|
115
129
|
}
|
|
130
|
+
const maxTextWidth = nodeWidth - 24;
|
|
116
131
|
for (let fontSize = 13; fontSize >= 9; fontSize--) {
|
|
117
|
-
|
|
118
|
-
const maxChars = Math.floor((nodeWidth - 24) / charWidth);
|
|
119
|
-
if (maxChars < 2) continue;
|
|
132
|
+
if (maxTextWidth < measureText('MM', fontSize)) continue;
|
|
120
133
|
let lines = 1;
|
|
121
134
|
let current = '';
|
|
122
135
|
for (const word of words) {
|
|
123
136
|
const test = current ? `${current} ${word}` : word;
|
|
124
|
-
if (test
|
|
137
|
+
if (measureText(test, fontSize) <= maxTextWidth) {
|
|
125
138
|
current = test;
|
|
126
139
|
} else {
|
|
127
140
|
lines++;
|
|
@@ -133,35 +146,31 @@ function estimateLabelLines(label: string, nodeWidth = NODE_WIDTH): number {
|
|
|
133
146
|
return MAX_LABEL_LINES;
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
function computeNodeSize(
|
|
149
|
+
function computeNodeSize(
|
|
150
|
+
node: BLNode,
|
|
151
|
+
reserveValueRow: boolean
|
|
152
|
+
): { width: number; height: number } {
|
|
137
153
|
if (!node.description || node.description.length === 0) {
|
|
138
154
|
return { width: NODE_WIDTH, height: NODE_HEIGHT };
|
|
139
155
|
}
|
|
140
156
|
const w = DESC_NODE_WIDTH;
|
|
141
157
|
const labelLines = estimateLabelLines(node.label, w);
|
|
142
158
|
const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT + LABEL_PAD;
|
|
143
|
-
const
|
|
159
|
+
const maxTextWidth = w - 24;
|
|
144
160
|
let totalRenderedLines = 0;
|
|
145
161
|
for (const line of node.description) {
|
|
146
|
-
if (line
|
|
162
|
+
if (measureText(line, DESC_FONT_SIZE) <= maxTextWidth) {
|
|
147
163
|
totalRenderedLines += 1;
|
|
148
164
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (test.length <= charsPerLine) {
|
|
157
|
-
current = test;
|
|
158
|
-
} else {
|
|
159
|
-
if (current) lineCount++;
|
|
160
|
-
current = fitted;
|
|
165
|
+
// Hard-break long words to match the renderer's slicing behaviour.
|
|
166
|
+
totalRenderedLines += wrapTextToWidth(
|
|
167
|
+
line,
|
|
168
|
+
DESC_FONT_SIZE,
|
|
169
|
+
maxTextWidth,
|
|
170
|
+
{
|
|
171
|
+
hardBreak: true,
|
|
161
172
|
}
|
|
162
|
-
|
|
163
|
-
if (current) lineCount++;
|
|
164
|
-
totalRenderedLines += lineCount;
|
|
173
|
+
).length;
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
totalRenderedLines = Math.min(totalRenderedLines, MAX_DESC_LINES);
|
|
@@ -172,7 +181,8 @@ function computeNodeSize(node: BLNode): { width: number; height: number } {
|
|
|
172
181
|
SEPARATOR_GAP +
|
|
173
182
|
DESC_PADDING +
|
|
174
183
|
descriptionHeight +
|
|
175
|
-
DESC_PADDING
|
|
184
|
+
DESC_PADDING +
|
|
185
|
+
(reserveValueRow ? VALUE_ROW_H : 0);
|
|
176
186
|
return { width: w, height: Math.max(NODE_HEIGHT, totalHeight) };
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -394,7 +404,10 @@ export async function layoutBoxesAndLines(
|
|
|
394
404
|
collapsedChildCounts: Map<string, number>;
|
|
395
405
|
originalGroups: readonly BLGroup[];
|
|
396
406
|
},
|
|
397
|
-
layoutOptions?: {
|
|
407
|
+
layoutOptions?: {
|
|
408
|
+
hideDescriptions?: boolean;
|
|
409
|
+
collapsedNotes?: ReadonlySet<number>;
|
|
410
|
+
}
|
|
398
411
|
): Promise<BLLayoutResult> {
|
|
399
412
|
const hideDescriptions = layoutOptions?.hideDescriptions ?? false;
|
|
400
413
|
const direction = parsed.direction === 'TB' ? 'DOWN' : 'RIGHT';
|
|
@@ -423,7 +436,7 @@ export async function layoutBoxesAndLines(
|
|
|
423
436
|
for (const node of parsed.nodes) {
|
|
424
437
|
const size = hideDescriptions
|
|
425
438
|
? { width: NODE_WIDTH, height: NODE_HEIGHT }
|
|
426
|
-
: computeNodeSize(node);
|
|
439
|
+
: computeNodeSize(node, parsed.showValues === true);
|
|
427
440
|
nodeSizes.set(node.label, size);
|
|
428
441
|
if (!hideDescriptions && node.description && node.description.length > 0) {
|
|
429
442
|
maxDescHeight = Math.max(maxDescHeight, size.height);
|
|
@@ -728,5 +741,112 @@ export async function layoutBoxesAndLines(
|
|
|
728
741
|
bestScore = s;
|
|
729
742
|
}
|
|
730
743
|
}
|
|
731
|
-
|
|
744
|
+
|
|
745
|
+
return attachNotes(best, parsed, layoutOptions?.collapsedNotes);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Float notes beside their boxes on the chosen layout (runs after variant
|
|
750
|
+
* selection — notes don't affect scoring). `no-notes` opts out. A note placed
|
|
751
|
+
* above/left can land off-canvas, so the whole layout is shifted to fit.
|
|
752
|
+
* Un-annotated diagrams are returned unchanged (min coords stay ≥ 0).
|
|
753
|
+
*/
|
|
754
|
+
function attachNotes(
|
|
755
|
+
layout: BLLayoutResult,
|
|
756
|
+
parsed: ParsedBoxesAndLines,
|
|
757
|
+
collapsedNotes?: ReadonlySet<number>
|
|
758
|
+
): BLLayoutResult {
|
|
759
|
+
const notesSuppressed = parsed.options?.['no-notes'] === 'on';
|
|
760
|
+
const noteByNode =
|
|
761
|
+
notesSuppressed || !parsed.notes
|
|
762
|
+
? new Map()
|
|
763
|
+
: resolveNotes(
|
|
764
|
+
parsed.notes,
|
|
765
|
+
parsed.nodes.map((n) => ({ id: n.label, label: n.label }))
|
|
766
|
+
);
|
|
767
|
+
if (noteByNode.size === 0) return layout;
|
|
768
|
+
|
|
769
|
+
const placed = buildPlacedNotes(
|
|
770
|
+
layout.nodes.map((n) => ({
|
|
771
|
+
id: n.label,
|
|
772
|
+
x: n.x,
|
|
773
|
+
y: n.y,
|
|
774
|
+
width: n.width,
|
|
775
|
+
height: n.height,
|
|
776
|
+
})),
|
|
777
|
+
noteByNode,
|
|
778
|
+
parsed.direction === 'TB' ? 'TB' : 'LR',
|
|
779
|
+
collapsedNotes
|
|
780
|
+
);
|
|
781
|
+
const notedNodes: BLLayoutNode[] = layout.nodes.map((n) => {
|
|
782
|
+
const note = placed.get(n.label);
|
|
783
|
+
return note ? { ...n, note } : n;
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Content bbox over nodes (+ their floated notes) and groups — matches the
|
|
787
|
+
// prior max-extent computation plus the notes.
|
|
788
|
+
let bbMinX = Infinity;
|
|
789
|
+
let bbMinY = Infinity;
|
|
790
|
+
let bbMaxX = -Infinity;
|
|
791
|
+
let bbMaxY = -Infinity;
|
|
792
|
+
const extend = (l: number, t: number, r: number, b: number): void => {
|
|
793
|
+
if (l < bbMinX) bbMinX = l;
|
|
794
|
+
if (t < bbMinY) bbMinY = t;
|
|
795
|
+
if (r > bbMaxX) bbMaxX = r;
|
|
796
|
+
if (b > bbMaxY) bbMaxY = b;
|
|
797
|
+
};
|
|
798
|
+
for (const n of notedNodes) {
|
|
799
|
+
extend(
|
|
800
|
+
n.x - n.width / 2,
|
|
801
|
+
n.y - n.height / 2,
|
|
802
|
+
n.x + n.width / 2,
|
|
803
|
+
n.y + n.height / 2
|
|
804
|
+
);
|
|
805
|
+
if (n.note && !n.note.collapsed) {
|
|
806
|
+
extend(
|
|
807
|
+
n.x + n.note.x,
|
|
808
|
+
n.y + n.note.y,
|
|
809
|
+
n.x + n.note.x + n.note.width,
|
|
810
|
+
n.y + n.note.y + n.note.height
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
for (const grp of layout.groups) {
|
|
815
|
+
extend(
|
|
816
|
+
grp.x - grp.width / 2,
|
|
817
|
+
grp.y - grp.height / 2,
|
|
818
|
+
grp.x + grp.width / 2,
|
|
819
|
+
grp.y + grp.height / 2
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (!Number.isFinite(bbMinX)) return { ...layout, nodes: notedNodes };
|
|
823
|
+
|
|
824
|
+
const { shiftX, shiftY } = noteCanvasShift(bbMinX, bbMinY);
|
|
825
|
+
const shifted = shiftX !== 0 || shiftY !== 0;
|
|
826
|
+
const finalNodes = shifted
|
|
827
|
+
? notedNodes.map((n) => ({ ...n, x: n.x + shiftX, y: n.y + shiftY }))
|
|
828
|
+
: notedNodes;
|
|
829
|
+
const finalEdges = shifted
|
|
830
|
+
? layout.edges.map((e) => ({
|
|
831
|
+
...e,
|
|
832
|
+
points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
|
|
833
|
+
...(e.labelX !== undefined && { labelX: e.labelX + shiftX }),
|
|
834
|
+
...(e.labelY !== undefined && { labelY: e.labelY + shiftY }),
|
|
835
|
+
}))
|
|
836
|
+
: layout.edges;
|
|
837
|
+
const finalGroups = shifted
|
|
838
|
+
? layout.groups.map((grp) => ({
|
|
839
|
+
...grp,
|
|
840
|
+
x: grp.x + shiftX,
|
|
841
|
+
y: grp.y + shiftY,
|
|
842
|
+
}))
|
|
843
|
+
: layout.groups;
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
nodes: finalNodes,
|
|
847
|
+
edges: finalEdges,
|
|
848
|
+
groups: finalGroups,
|
|
849
|
+
width: bbMaxX + shiftX + MARGIN,
|
|
850
|
+
height: bbMaxY + shiftY + MARGIN,
|
|
851
|
+
};
|
|
732
852
|
}
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
extractColor,
|
|
27
27
|
parseFirstLine,
|
|
28
28
|
OPTION_NOCOLON_RE,
|
|
29
|
-
|
|
29
|
+
peelRampColors,
|
|
30
30
|
splitNameAndMeta,
|
|
31
31
|
tryParseSharedOption,
|
|
32
32
|
warnUnknownMetaKeys,
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
BOXES_AND_LINES_REGISTRY,
|
|
36
36
|
withTagAliases,
|
|
37
37
|
} from '../utils/reserved-key-registry';
|
|
38
|
+
import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
|
|
39
|
+
import type { PaletteColors } from '../palettes';
|
|
38
40
|
|
|
39
41
|
const MAX_GROUP_DEPTH = 2;
|
|
40
42
|
|
|
@@ -113,8 +115,12 @@ type MutBLGroup = Omit<Writable<BLGroup>, 'metadata' | 'children'> & {
|
|
|
113
115
|
children: string[];
|
|
114
116
|
};
|
|
115
117
|
|
|
116
|
-
export function parseBoxesAndLines(
|
|
118
|
+
export function parseBoxesAndLines(
|
|
119
|
+
content: string,
|
|
120
|
+
palette?: PaletteColors
|
|
121
|
+
): ParsedBoxesAndLines {
|
|
117
122
|
const options: Record<string, string> = {};
|
|
123
|
+
const notes: DiagramNote[] = [];
|
|
118
124
|
const initialHiddenTagValues = new Map<string, Set<string>>();
|
|
119
125
|
const nodes: MutBLNode[] = [];
|
|
120
126
|
const edges: MutBLEdge[] = [];
|
|
@@ -305,11 +311,10 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
|
|
|
305
311
|
const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
|
|
306
312
|
if (metricMatch) {
|
|
307
313
|
// Regex capture group present after successful match.
|
|
308
|
-
const { label,
|
|
309
|
-
metricMatch[1]!.trim()
|
|
310
|
-
);
|
|
314
|
+
const { label, low, high } = peelRampColors(metricMatch[1]!.trim());
|
|
311
315
|
result.boxMetric = label;
|
|
312
|
-
if (
|
|
316
|
+
if (high !== undefined) result.boxMetricColor = high;
|
|
317
|
+
if (low !== undefined) result.boxMetricLowColor = low;
|
|
313
318
|
continue;
|
|
314
319
|
}
|
|
315
320
|
if (/^show-values$/i.test(trimmed)) {
|
|
@@ -336,6 +341,24 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
|
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
// Note annotation (top-level): `note <Box> [inline body]` + an optional
|
|
345
|
+
// indented body. Checked before tag/group/node/edge matching so a note is
|
|
346
|
+
// never mistaken for a box; gated to indent 0. `note -> X` is excluded.
|
|
347
|
+
if (indent === 0) {
|
|
348
|
+
const noteResult = tryCollectNote(
|
|
349
|
+
lines,
|
|
350
|
+
i,
|
|
351
|
+
indent,
|
|
352
|
+
palette,
|
|
353
|
+
result.diagnostics
|
|
354
|
+
);
|
|
355
|
+
if (noteResult) {
|
|
356
|
+
if (noteResult.note) notes.push(noteResult.note);
|
|
357
|
+
i = noteResult.lastIndex;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
339
362
|
// Tag group heading — must be checked BEFORE group/node/edge matching
|
|
340
363
|
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
341
364
|
if (tagBlockMatch && indent === 0) {
|
|
@@ -721,6 +744,17 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
|
|
|
721
744
|
}
|
|
722
745
|
result.edges = validEdges;
|
|
723
746
|
|
|
747
|
+
// Resolve note refs against box labels (forward refs OK). The id→note
|
|
748
|
+
// binding is recomputed in layout; this pass surfaces diagnostics.
|
|
749
|
+
if (notes.length > 0) {
|
|
750
|
+
result.notes = notes;
|
|
751
|
+
resolveNotes(
|
|
752
|
+
notes,
|
|
753
|
+
result.nodes.map((n) => ({ id: n.label, label: n.label })),
|
|
754
|
+
result.diagnostics
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
724
758
|
// Post-parse: inject default tag metadata and validate tag values
|
|
725
759
|
if (result.tagGroups.length > 0) {
|
|
726
760
|
injectDefaultTagMetadata(result.nodes, result.tagGroups);
|
|
@@ -1007,8 +1041,9 @@ function parseEdgeLine(
|
|
|
1007
1041
|
};
|
|
1008
1042
|
}
|
|
1009
1043
|
|
|
1010
|
-
// Check for labeled arrow: `Source -label-> Target`
|
|
1011
|
-
|
|
1044
|
+
// Check for labeled arrow: `Source -label-> Target` (label lazy → split
|
|
1045
|
+
// at the first arrow, consistent with the other parsers).
|
|
1046
|
+
const labeledMatch = trimmed.match(/^(.+?)\s+-(.+?)->\s*(.+)$/);
|
|
1012
1047
|
if (labeledMatch) {
|
|
1013
1048
|
// Regex capture groups present after successful match.
|
|
1014
1049
|
const rawSource = labeledMatch[1]!.trim();
|