@diagrammo/dgmo 0.47.0 → 0.48.1
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/advanced.js +11 -11
- package/dist/auto.js +16 -16
- package/dist/auto.mjs +41 -12
- package/dist/block.js +4 -4
- package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
- package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
- package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
- package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
- package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
- package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
- package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
- package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
- package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
- package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
- package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
- package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
- package/dist/cli.cjs +15 -15
- package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
- package/dist/element.js +16 -16
- package/dist/element.mjs +41 -12
- package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -4
- package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
- package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
- package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
- package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
- package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
- package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
- package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
- package/package.json +23 -71
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
|
@@ -1,1181 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Boxes and Lines Diagram — Parser
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import { makeDgmoError, suggest } from '../diagnostics';
|
|
6
|
-
import type { DgmoError } from '../diagnostics';
|
|
7
|
-
import { parseInArrowLabel } from '../utils/arrows';
|
|
8
|
-
import { normalizeName } from '../utils/name-normalize';
|
|
9
|
-
import type { ParsedBoxesAndLines, BLNode, BLEdge, BLGroup } from './types';
|
|
10
|
-
import {
|
|
11
|
-
matchTagBlockHeading,
|
|
12
|
-
injectDefaultTagMetadata,
|
|
13
|
-
validateTagValues,
|
|
14
|
-
validateTagGroupNames,
|
|
15
|
-
stripDefaultModifier,
|
|
16
|
-
finalizeAutoTagColors,
|
|
17
|
-
AUTO_TAG_COLOR_SENTINEL,
|
|
18
|
-
tagAttrKey,
|
|
19
|
-
} from '../utils/tag-groups';
|
|
20
|
-
import type { TagGroup } from '../utils/tag-groups';
|
|
21
|
-
import type { Writable } from '../utils/brand';
|
|
22
|
-
import {
|
|
23
|
-
extractColor,
|
|
24
|
-
parseFirstLine,
|
|
25
|
-
OPTION_NOCOLON_RE,
|
|
26
|
-
peelRampColors,
|
|
27
|
-
splitNameAndMeta,
|
|
28
|
-
tryParseSharedOption,
|
|
29
|
-
warnUnknownMetaKeys,
|
|
30
|
-
} from '../utils/parsing';
|
|
31
|
-
import {
|
|
32
|
-
BOXES_AND_LINES_REGISTRY,
|
|
33
|
-
withTagAliases,
|
|
34
|
-
} from '../utils/reserved-key-registry';
|
|
35
|
-
import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
|
|
36
|
-
import type { PaletteColors } from '../palettes';
|
|
37
|
-
|
|
38
|
-
const MAX_GROUP_DEPTH = 2;
|
|
39
|
-
|
|
40
|
-
/** Boxes-and-lines requires explicit first line — no heuristic detection. */
|
|
41
|
-
export function looksLikeBoxesAndLines(_content: string): boolean {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Measure leading whitespace (tabs = 4 spaces) */
|
|
46
|
-
function measureIndent(line: string): number {
|
|
47
|
-
let count = 0;
|
|
48
|
-
for (const ch of line) {
|
|
49
|
-
if (ch === ' ') count++;
|
|
50
|
-
else if (ch === '\t') count += 4;
|
|
51
|
-
else break;
|
|
52
|
-
}
|
|
53
|
-
return count;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parse the trailing meta segment of a structural line — accepts both
|
|
58
|
-
* the legacy `| key: value, …` and the new §1.4 same-line `key: value, …`
|
|
59
|
-
* forms (a leading `|` is tolerated and stripped). Extracts
|
|
60
|
-
* `description` separately so callers can hang it on the node body.
|
|
61
|
-
*/
|
|
62
|
-
function parseTailMeta(
|
|
63
|
-
rawTail: string,
|
|
64
|
-
metaAliasMap: Map<string, string>
|
|
65
|
-
): { metadata: Record<string, string>; description?: string[] } {
|
|
66
|
-
const segment = rawTail.trim();
|
|
67
|
-
if (!segment) return { metadata: {} };
|
|
68
|
-
|
|
69
|
-
const metadata: Record<string, string> = {};
|
|
70
|
-
let description: string[] | undefined;
|
|
71
|
-
|
|
72
|
-
for (const item of segment.split(',')) {
|
|
73
|
-
const trimmed = item.trim();
|
|
74
|
-
if (!trimmed) continue;
|
|
75
|
-
|
|
76
|
-
const colonIdx = trimmed.indexOf(':');
|
|
77
|
-
if (colonIdx >= 0) {
|
|
78
|
-
const rawKey = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
79
|
-
const value = trimmed.slice(colonIdx + 1).trim();
|
|
80
|
-
if (rawKey === 'description') {
|
|
81
|
-
description = [value];
|
|
82
|
-
} else {
|
|
83
|
-
const resolvedKey = metaAliasMap.get(rawKey) ?? rawKey;
|
|
84
|
-
metadata[resolvedKey] = value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Bare words are ignored (no status system)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { metadata, ...(description !== undefined && { description }) };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Convert group label to internal ID */
|
|
94
|
-
function groupId(label: string): string {
|
|
95
|
-
return `__group_${label}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Local mutable shapes — element-level metadata/description need to be
|
|
99
|
-
// mutated during parse (cascading group metadata, description collection).
|
|
100
|
-
// These widen the readonly fields to writable; assignment back into the
|
|
101
|
-
// readonly-typed ParsedBoxesAndLines is covariant on Record → Readonly<Record>.
|
|
102
|
-
type MutBLNode = Omit<Writable<BLNode>, 'metadata' | 'description'> & {
|
|
103
|
-
metadata: Record<string, string>;
|
|
104
|
-
description?: string[];
|
|
105
|
-
};
|
|
106
|
-
type MutBLEdge = Omit<Writable<BLEdge>, 'metadata'> & {
|
|
107
|
-
metadata: Record<string, string>;
|
|
108
|
-
};
|
|
109
|
-
type MutBLGroup = Omit<Writable<BLGroup>, 'metadata' | 'children'> & {
|
|
110
|
-
metadata: Record<string, string>;
|
|
111
|
-
children: string[];
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export function parseBoxesAndLines(
|
|
115
|
-
content: string,
|
|
116
|
-
palette?: PaletteColors
|
|
117
|
-
): ParsedBoxesAndLines {
|
|
118
|
-
const options: Record<string, string> = {};
|
|
119
|
-
const notes: DiagramNote[] = [];
|
|
120
|
-
const initialHiddenTagValues = new Map<string, Set<string>>();
|
|
121
|
-
const nodes: MutBLNode[] = [];
|
|
122
|
-
const edges: MutBLEdge[] = [];
|
|
123
|
-
const groups: MutBLGroup[] = [];
|
|
124
|
-
// Trailing `layout` block (Canvas Editor spike): node-id → absolute {x,y}.
|
|
125
|
-
const nodePositions = new Map<string, { x: number; y: number }>();
|
|
126
|
-
const result: Writable<ParsedBoxesAndLines> = {
|
|
127
|
-
type: 'boxes-and-lines',
|
|
128
|
-
title: null,
|
|
129
|
-
titleLineNumber: null,
|
|
130
|
-
nodes,
|
|
131
|
-
edges,
|
|
132
|
-
groups,
|
|
133
|
-
tagGroups: [],
|
|
134
|
-
options,
|
|
135
|
-
initialHiddenTagValues,
|
|
136
|
-
direction: 'LR',
|
|
137
|
-
diagnostics: [],
|
|
138
|
-
error: null,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const lines = content.split('\n');
|
|
142
|
-
const nodeLabels = new Set<string>();
|
|
143
|
-
const groupLabels = new Set<string>();
|
|
144
|
-
let lastNodeLabel: string | null = null;
|
|
145
|
-
let lastSourceIsGroup = false;
|
|
146
|
-
let lastNodeIndent = 0;
|
|
147
|
-
|
|
148
|
-
// Description collection state
|
|
149
|
-
let descState: {
|
|
150
|
-
nodeLabel: string;
|
|
151
|
-
indent: number;
|
|
152
|
-
lines: string[];
|
|
153
|
-
edgeSeen: boolean;
|
|
154
|
-
} | null = null;
|
|
155
|
-
|
|
156
|
-
function flushDescription() {
|
|
157
|
-
if (descState && descState.lines.length > 0) {
|
|
158
|
-
const node = nodes.find((n) => n.label === descState!.nodeLabel);
|
|
159
|
-
if (node) {
|
|
160
|
-
const existing = node.description ?? [];
|
|
161
|
-
node.description = [...existing, ...descState!.lines];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
descState = null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Group stack for nesting
|
|
168
|
-
interface GroupState {
|
|
169
|
-
group: MutBLGroup;
|
|
170
|
-
indent: number;
|
|
171
|
-
depth: number;
|
|
172
|
-
}
|
|
173
|
-
const groupStack: GroupState[] = [];
|
|
174
|
-
|
|
175
|
-
// Tag block state
|
|
176
|
-
let contentStarted = false;
|
|
177
|
-
// `layout` coordinate-block state (Canvas Editor spike). Unlike tag blocks,
|
|
178
|
-
// this is a TRAILING appendix — it may appear after diagram content.
|
|
179
|
-
let inLayoutBlock = false;
|
|
180
|
-
const LAYOUT_ENTRY_RE =
|
|
181
|
-
/^(.+?):\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/;
|
|
182
|
-
let currentTagGroup: Writable<TagGroup> | null = null;
|
|
183
|
-
// metaAliasMap: tag-group metadata-key aliases (per A1).
|
|
184
|
-
const metaAliasMap = new Map<string, string>();
|
|
185
|
-
// nameAliasMap: TD-18 entity-name aliases (`a` → `<canonical id>`). Per C8.
|
|
186
|
-
const nameAliasMap = new Map<string, string>();
|
|
187
|
-
function peelAlias(label: string): { label: string; alias?: string } {
|
|
188
|
-
const trimmed = label.trim();
|
|
189
|
-
const m = trimmed.match(/^(.*?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/);
|
|
190
|
-
if (!m) return { label: trimmed };
|
|
191
|
-
// Regex capture groups present after successful match.
|
|
192
|
-
return { label: m[1]!.trim(), alias: m[2]! };
|
|
193
|
-
}
|
|
194
|
-
const pushWarning = (lineNumber: number, message: string) => {
|
|
195
|
-
result.diagnostics.push(makeDgmoError(lineNumber, message, 'warning'));
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/** Get the innermost active group, if any */
|
|
199
|
-
function currentGroupState(): GroupState | null {
|
|
200
|
-
// In-bounds by length guard.
|
|
201
|
-
return groupStack.length > 0 ? groupStack[groupStack.length - 1]! : null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Close groups that are at or deeper than a given indent level */
|
|
205
|
-
function closeGroupsToIndent(indent: number) {
|
|
206
|
-
while (
|
|
207
|
-
groupStack.length > 0 &&
|
|
208
|
-
// In-bounds by length guard.
|
|
209
|
-
groupStack[groupStack.length - 1]!.indent >= indent
|
|
210
|
-
) {
|
|
211
|
-
const gs = groupStack.pop()!;
|
|
212
|
-
result.groups.push(gs.group);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Ensure a node exists (implicit creation) */
|
|
217
|
-
function ensureNode(label: string, lineNum: number) {
|
|
218
|
-
const key = normalizeName(label);
|
|
219
|
-
if (!nodeLabels.has(key)) {
|
|
220
|
-
result.nodes.push({
|
|
221
|
-
label,
|
|
222
|
-
lineNumber: lineNum,
|
|
223
|
-
metadata: {},
|
|
224
|
-
});
|
|
225
|
-
nodeLabels.add(key);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (let i = 0; i < lines.length; i++) {
|
|
230
|
-
const lineNum = i + 1;
|
|
231
|
-
// In-bounds by loop guard.
|
|
232
|
-
const raw = lines[i]!;
|
|
233
|
-
const trimmed = raw.trim();
|
|
234
|
-
const indent = measureIndent(raw);
|
|
235
|
-
|
|
236
|
-
// Skip blanks and comments
|
|
237
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
238
|
-
|
|
239
|
-
// First line: `boxes-and-lines [Title]`
|
|
240
|
-
const firstLineResult = parseFirstLine(trimmed);
|
|
241
|
-
if (firstLineResult && !contentStarted && i < 5) {
|
|
242
|
-
if (firstLineResult.chartType !== 'boxes-and-lines') {
|
|
243
|
-
const diag = makeDgmoError(
|
|
244
|
-
lineNum,
|
|
245
|
-
`Expected chart type "boxes-and-lines", got "${firstLineResult.chartType}"`
|
|
246
|
-
);
|
|
247
|
-
result.diagnostics.push(diag);
|
|
248
|
-
result.error = diag.message;
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
|
-
if (firstLineResult.title) {
|
|
252
|
-
result.title = firstLineResult.title;
|
|
253
|
-
result.titleLineNumber = lineNum;
|
|
254
|
-
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Directives (non-indented, before or during content)
|
|
259
|
-
if (indent === 0) {
|
|
260
|
-
// direction TB / direction LR
|
|
261
|
-
const dirMatch = trimmed.match(/^direction\s+(TB|LR)$/i);
|
|
262
|
-
if (dirMatch) {
|
|
263
|
-
// Regex capture group present after successful match.
|
|
264
|
-
result.direction = dirMatch[1]!.toUpperCase() as 'LR' | 'TB';
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// hide directive: `hide team:Backend, team:Frontend`
|
|
269
|
-
const hideMatch = trimmed.match(/^hide\s+(.+)/i);
|
|
270
|
-
if (hideMatch && !trimmed.match(/^hide\s*\|/)) {
|
|
271
|
-
// Regex capture group present after successful match.
|
|
272
|
-
const pairs = hideMatch[1]!.split(',');
|
|
273
|
-
for (const pair of pairs) {
|
|
274
|
-
const colonIdx = pair.indexOf(':');
|
|
275
|
-
if (colonIdx > 0) {
|
|
276
|
-
const groupKey = pair.substring(0, colonIdx).trim().toLowerCase();
|
|
277
|
-
const value = pair
|
|
278
|
-
.substring(colonIdx + 1)
|
|
279
|
-
.trim()
|
|
280
|
-
.toLowerCase();
|
|
281
|
-
if (groupKey && value) {
|
|
282
|
-
if (!initialHiddenTagValues.has(groupKey)) {
|
|
283
|
-
initialHiddenTagValues.set(groupKey, new Set());
|
|
284
|
-
}
|
|
285
|
-
initialHiddenTagValues.get(groupKey)!.add(value);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// heat / show-values directives — pre-content only (like
|
|
293
|
-
// active-tag). Explicit regex branches: a bare flag and a
|
|
294
|
-
// `key value` form won't both match the active-tag OPTION codepath.
|
|
295
|
-
if (!contentStarted) {
|
|
296
|
-
const metricMatch = trimmed.match(/^heat\s+(.+)$/i);
|
|
297
|
-
if (metricMatch) {
|
|
298
|
-
// Regex capture group present after successful match.
|
|
299
|
-
const { label, low, high } = peelRampColors(metricMatch[1]!.trim());
|
|
300
|
-
result.boxMetric = label;
|
|
301
|
-
if (high !== undefined) result.boxMetricColor = high;
|
|
302
|
-
if (low !== undefined) result.boxMetricLowColor = low;
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (/^show-values$/i.test(trimmed)) {
|
|
306
|
-
result.showValues = true;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// active-tag directive
|
|
312
|
-
if (!contentStarted) {
|
|
313
|
-
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
314
|
-
if (optMatch) {
|
|
315
|
-
// Regex capture groups present after successful match.
|
|
316
|
-
const key = optMatch[1]!.toLowerCase();
|
|
317
|
-
const value = optMatch[2]!.trim();
|
|
318
|
-
if (key === 'active-tag') {
|
|
319
|
-
options[key] = value;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (tryParseSharedOption(trimmed, options)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Note annotation (top-level): `note <Box> [inline body]` + an optional
|
|
330
|
-
// indented body. Checked before tag/group/node/edge matching so a note is
|
|
331
|
-
// never mistaken for a box; gated to indent 0. `note -> X` is excluded.
|
|
332
|
-
if (indent === 0) {
|
|
333
|
-
const noteResult = tryCollectNote(
|
|
334
|
-
lines,
|
|
335
|
-
i,
|
|
336
|
-
indent,
|
|
337
|
-
palette,
|
|
338
|
-
result.diagnostics
|
|
339
|
-
);
|
|
340
|
-
if (noteResult) {
|
|
341
|
-
if (noteResult.note) notes.push(noteResult.note);
|
|
342
|
-
i = noteResult.lastIndex;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Tag group heading — must be checked BEFORE group/node/edge matching
|
|
348
|
-
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
349
|
-
if (tagBlockMatch && indent === 0) {
|
|
350
|
-
if (contentStarted) {
|
|
351
|
-
result.diagnostics.push(
|
|
352
|
-
makeDgmoError(
|
|
353
|
-
lineNum,
|
|
354
|
-
'Tag groups must appear before diagram content',
|
|
355
|
-
'error'
|
|
356
|
-
)
|
|
357
|
-
);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
const newTagGroup: Writable<TagGroup> = {
|
|
361
|
-
name: tagBlockMatch.name,
|
|
362
|
-
...(tagBlockMatch.alias !== undefined && {
|
|
363
|
-
alias: tagBlockMatch.alias,
|
|
364
|
-
}),
|
|
365
|
-
entries: [],
|
|
366
|
-
lineNumber: lineNum,
|
|
367
|
-
};
|
|
368
|
-
currentTagGroup = newTagGroup;
|
|
369
|
-
if (tagBlockMatch.alias) {
|
|
370
|
-
metaAliasMap.set(
|
|
371
|
-
normalizeName(tagBlockMatch.alias),
|
|
372
|
-
tagAttrKey(tagBlockMatch.name)
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
metaAliasMap.set(
|
|
376
|
-
normalizeName(tagBlockMatch.name),
|
|
377
|
-
tagAttrKey(tagBlockMatch.name)
|
|
378
|
-
);
|
|
379
|
-
if (tagBlockMatch.inlineValues) {
|
|
380
|
-
for (const rawVal of tagBlockMatch.inlineValues) {
|
|
381
|
-
const { text: cleanVal, isDefault } = stripDefaultModifier(rawVal);
|
|
382
|
-
const { label, color } = extractColor(
|
|
383
|
-
cleanVal,
|
|
384
|
-
palette,
|
|
385
|
-
result.diagnostics,
|
|
386
|
-
lineNum
|
|
387
|
-
);
|
|
388
|
-
newTagGroup.entries.push({
|
|
389
|
-
value: label,
|
|
390
|
-
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
391
|
-
lineNumber: lineNum,
|
|
392
|
-
});
|
|
393
|
-
if (isDefault) newTagGroup.defaultValue = label;
|
|
394
|
-
}
|
|
395
|
-
if (!newTagGroup.defaultValue && newTagGroup.entries.length > 0) {
|
|
396
|
-
// In-bounds by length guard.
|
|
397
|
-
newTagGroup.defaultValue = newTagGroup.entries[0]!.value;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
result.tagGroups.push(newTagGroup);
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Tag group entries (indented under tag heading)
|
|
405
|
-
if (currentTagGroup && !contentStarted && indent > 0) {
|
|
406
|
-
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
407
|
-
const { label, color } = extractColor(
|
|
408
|
-
cleanEntry,
|
|
409
|
-
palette,
|
|
410
|
-
result.diagnostics,
|
|
411
|
-
lineNum
|
|
412
|
-
);
|
|
413
|
-
currentTagGroup.entries.push({
|
|
414
|
-
value: label,
|
|
415
|
-
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
416
|
-
lineNumber: lineNum,
|
|
417
|
-
});
|
|
418
|
-
if (isDefault) {
|
|
419
|
-
currentTagGroup.defaultValue = label;
|
|
420
|
-
} else if (currentTagGroup.entries.length === 1) {
|
|
421
|
-
currentTagGroup.defaultValue = label;
|
|
422
|
-
}
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Non-indented line closes tag group
|
|
427
|
-
if (currentTagGroup && indent === 0) {
|
|
428
|
-
currentTagGroup = null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// `layout` coordinate block (Canvas Editor spike). A bare `layout` heading
|
|
432
|
-
// at indent 0 opens the block; indented `<node-id>: <x>, <y>` entries map a
|
|
433
|
-
// node to an absolute position. Any non-indented line closes it. Quarantined
|
|
434
|
-
// before group/node/edge matching so the entries don't parse as nodes.
|
|
435
|
-
if (!inLayoutBlock && indent === 0 && trimmed === 'layout') {
|
|
436
|
-
// Disambiguate from a node legitimately NAMED `layout`: only treat this as
|
|
437
|
-
// the coordinate appendix when the next non-blank line is an indented
|
|
438
|
-
// `<id>: <x>, <y>` entry. Otherwise fall through and parse `layout` as a
|
|
439
|
-
// normal node (no silent data loss).
|
|
440
|
-
let isBlock = false;
|
|
441
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
442
|
-
const peek = lines[j]!;
|
|
443
|
-
if (!peek.trim()) continue;
|
|
444
|
-
isBlock = measureIndent(peek) > 0 && LAYOUT_ENTRY_RE.test(peek.trim());
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
if (isBlock) {
|
|
448
|
-
flushDescription();
|
|
449
|
-
closeGroupsToIndent(0);
|
|
450
|
-
inLayoutBlock = true;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (inLayoutBlock) {
|
|
455
|
-
if (indent > 0) {
|
|
456
|
-
const lm = trimmed.match(LAYOUT_ENTRY_RE);
|
|
457
|
-
if (lm) {
|
|
458
|
-
nodePositions.set(lm[1]!.trim(), {
|
|
459
|
-
x: Number(lm[2]),
|
|
460
|
-
y: Number(lm[3]),
|
|
461
|
-
});
|
|
462
|
-
} else {
|
|
463
|
-
result.diagnostics.push(
|
|
464
|
-
makeDgmoError(
|
|
465
|
-
lineNum,
|
|
466
|
-
`Invalid layout entry "${trimmed}" — expected "<node-id>: <x>, <y>"`,
|
|
467
|
-
'warning'
|
|
468
|
-
)
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
// indent 0 → block ends; fall through to process this line normally.
|
|
474
|
-
inLayoutBlock = false;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Description collection: indented non-edge lines under a node
|
|
478
|
-
if (descState !== null) {
|
|
479
|
-
if (indent > descState.indent) {
|
|
480
|
-
// Check if this is an edge line
|
|
481
|
-
if (trimmed.includes('->') || trimmed.includes('<->')) {
|
|
482
|
-
descState.edgeSeen = true;
|
|
483
|
-
// Fall through to normal edge processing
|
|
484
|
-
} else if (descState.edgeSeen) {
|
|
485
|
-
// Text after edges — emit warning
|
|
486
|
-
result.diagnostics.push(
|
|
487
|
-
makeDgmoError(
|
|
488
|
-
lineNum,
|
|
489
|
-
`Move description lines above edges for '${descState.nodeLabel}' — descriptions must come before -> lines`,
|
|
490
|
-
'warning'
|
|
491
|
-
)
|
|
492
|
-
);
|
|
493
|
-
continue;
|
|
494
|
-
} else if (
|
|
495
|
-
/^-\s*\w/.test(trimmed) &&
|
|
496
|
-
!trimmed.startsWith('- ') &&
|
|
497
|
-
!trimmed.includes('->') &&
|
|
498
|
-
!trimmed.includes('<->')
|
|
499
|
-
) {
|
|
500
|
-
// Looks like a malformed edge (e.g. "-Target" but not "- list item")
|
|
501
|
-
result.diagnostics.push(
|
|
502
|
-
makeDgmoError(
|
|
503
|
-
lineNum,
|
|
504
|
-
`Looks like an incomplete edge — did you mean "-> ${trimmed.slice(1).trim()}"?`,
|
|
505
|
-
'warning'
|
|
506
|
-
)
|
|
507
|
-
);
|
|
508
|
-
descState.lines.push(trimmed);
|
|
509
|
-
continue;
|
|
510
|
-
} else {
|
|
511
|
-
// Collect as description
|
|
512
|
-
descState.lines.push(trimmed);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
} else {
|
|
516
|
-
// Indent decreased — flush description
|
|
517
|
-
flushDescription();
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Close groups that are no longer scoped by indent
|
|
522
|
-
if (indent === 0) {
|
|
523
|
-
closeGroupsToIndent(0);
|
|
524
|
-
} else if (groupStack.length > 0) {
|
|
525
|
-
// Close groups deeper than current indent
|
|
526
|
-
closeGroupsToIndent(indent);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Group-to-group edge: [Group A] -> [Group B] or [Group A] <-> [Group B]
|
|
530
|
-
const groupEdgeMatch = trimmed.match(
|
|
531
|
-
/^\[(.+?)\]\s*(<->|->)\s*\[(.+?)\](.*)$/
|
|
532
|
-
);
|
|
533
|
-
if (groupEdgeMatch) {
|
|
534
|
-
contentStarted = true;
|
|
535
|
-
currentTagGroup = null;
|
|
536
|
-
const sourceLabel = groupEdgeMatch[1];
|
|
537
|
-
const arrow = groupEdgeMatch[2];
|
|
538
|
-
const targetLabel = groupEdgeMatch[3];
|
|
539
|
-
const rawTail = groupEdgeMatch[4] ?? '';
|
|
540
|
-
|
|
541
|
-
const edgeMeta = parseTailMeta(rawTail, metaAliasMap).metadata;
|
|
542
|
-
|
|
543
|
-
result.edges.push({
|
|
544
|
-
// Regex capture groups present after successful match.
|
|
545
|
-
source: groupId(sourceLabel!),
|
|
546
|
-
target: groupId(targetLabel!),
|
|
547
|
-
bidirectional: arrow === '<->',
|
|
548
|
-
lineNumber: lineNum,
|
|
549
|
-
metadata: edgeMeta,
|
|
550
|
-
});
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Labeled group-to-group edge: [Group A] -label-> [Group B]
|
|
555
|
-
const labeledGroupEdgeMatch = trimmed.match(
|
|
556
|
-
/^\[(.+?)\]\s*(?:<-(.+)->|-(.+)->)\s*\[(.+?)\](.*)$/
|
|
557
|
-
);
|
|
558
|
-
if (labeledGroupEdgeMatch) {
|
|
559
|
-
contentStarted = true;
|
|
560
|
-
currentTagGroup = null;
|
|
561
|
-
const sourceLabel = labeledGroupEdgeMatch[1];
|
|
562
|
-
const biLabel = labeledGroupEdgeMatch[2];
|
|
563
|
-
const uniLabel = labeledGroupEdgeMatch[3];
|
|
564
|
-
const targetLabel = labeledGroupEdgeMatch[4];
|
|
565
|
-
const rawTail = labeledGroupEdgeMatch[5] ?? '';
|
|
566
|
-
|
|
567
|
-
const edgeMeta = parseTailMeta(rawTail, metaAliasMap).metadata;
|
|
568
|
-
|
|
569
|
-
const labeledEdgeLabel = (biLabel ?? uniLabel)?.trim();
|
|
570
|
-
result.edges.push({
|
|
571
|
-
// Regex capture groups present after successful match.
|
|
572
|
-
source: groupId(sourceLabel!),
|
|
573
|
-
target: groupId(targetLabel!),
|
|
574
|
-
...(labeledEdgeLabel !== undefined && { label: labeledEdgeLabel }),
|
|
575
|
-
bidirectional: !!biLabel,
|
|
576
|
-
lineNumber: lineNum,
|
|
577
|
-
metadata: edgeMeta,
|
|
578
|
-
});
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Group header: [Group Name] or [Group Name] key: value
|
|
583
|
-
const groupMatch = trimmed.match(/^\[(.+?)\](.*)$/);
|
|
584
|
-
if (groupMatch && !trimmed.includes('->') && !trimmed.includes('<->')) {
|
|
585
|
-
contentStarted = true;
|
|
586
|
-
currentTagGroup = null;
|
|
587
|
-
flushDescription();
|
|
588
|
-
// TD-18: peel optional `as <alias>` from the group label.
|
|
589
|
-
// Regex capture group present after successful match.
|
|
590
|
-
const groupPeeled = peelAlias(groupMatch[1]!);
|
|
591
|
-
const label = groupPeeled.label;
|
|
592
|
-
if (groupPeeled.alias)
|
|
593
|
-
nameAliasMap.set(groupPeeled.alias, groupId(label));
|
|
594
|
-
|
|
595
|
-
// Check nesting depth
|
|
596
|
-
const currentDepth = groupStack.length + 1;
|
|
597
|
-
if (currentDepth > MAX_GROUP_DEPTH) {
|
|
598
|
-
result.diagnostics.push(
|
|
599
|
-
makeDgmoError(
|
|
600
|
-
lineNum,
|
|
601
|
-
`Group nesting exceeds maximum depth of ${MAX_GROUP_DEPTH}`,
|
|
602
|
-
'warning'
|
|
603
|
-
)
|
|
604
|
-
);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const groupMeta = parseTailMeta(
|
|
609
|
-
groupMatch[2] ?? '',
|
|
610
|
-
metaAliasMap
|
|
611
|
-
).metadata;
|
|
612
|
-
|
|
613
|
-
const parentGs = currentGroupState();
|
|
614
|
-
const group: MutBLGroup = {
|
|
615
|
-
label,
|
|
616
|
-
children: [],
|
|
617
|
-
lineNumber: lineNum,
|
|
618
|
-
metadata: groupMeta,
|
|
619
|
-
...(parentGs ? { parentGroup: parentGs.group.label } : {}),
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
// Add nested group as child of parent group
|
|
623
|
-
if (parentGs && indent > parentGs.indent) {
|
|
624
|
-
parentGs.group.children.push(label);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
groupLabels.add(normalizeName(label));
|
|
628
|
-
groupStack.push({ group, indent, depth: currentDepth });
|
|
629
|
-
lastNodeLabel = label;
|
|
630
|
-
lastSourceIsGroup = true;
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Edge detection: contains `->` or `<->`
|
|
635
|
-
if (trimmed.includes('->') || trimmed.includes('<->')) {
|
|
636
|
-
contentStarted = true;
|
|
637
|
-
currentTagGroup = null;
|
|
638
|
-
let edgeText = trimmed;
|
|
639
|
-
|
|
640
|
-
// Indented shorthand: `-> Target` or `-label-> Target`
|
|
641
|
-
if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
|
|
642
|
-
// If the edge is at group-child indent level, use the containing group
|
|
643
|
-
// UNLESS lastNodeLabel is a plain node (not a group) — then the edge
|
|
644
|
-
// is indented under that node and should source from it.
|
|
645
|
-
const gs = currentGroupState();
|
|
646
|
-
const inGroup = gs && indent > gs.indent;
|
|
647
|
-
// Edge is deeper than the last node → indented under that node, use it
|
|
648
|
-
const indentedUnderNode =
|
|
649
|
-
lastNodeLabel && !lastSourceIsGroup && indent > lastNodeIndent;
|
|
650
|
-
if (inGroup && !indentedUnderNode) {
|
|
651
|
-
const sourcePrefix = `[${gs.group.label}]`;
|
|
652
|
-
edgeText = `${sourcePrefix} ${trimmed}`;
|
|
653
|
-
} else if (lastNodeLabel) {
|
|
654
|
-
const sourcePrefix = lastSourceIsGroup
|
|
655
|
-
? `[${lastNodeLabel}]`
|
|
656
|
-
: lastNodeLabel;
|
|
657
|
-
edgeText = `${sourcePrefix} ${trimmed}`;
|
|
658
|
-
} else {
|
|
659
|
-
result.diagnostics.push(
|
|
660
|
-
makeDgmoError(
|
|
661
|
-
lineNum,
|
|
662
|
-
'Indented edge has no preceding node to use as source',
|
|
663
|
-
'warning'
|
|
664
|
-
)
|
|
665
|
-
);
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const edge = parseEdgeLine(
|
|
671
|
-
edgeText,
|
|
672
|
-
lineNum,
|
|
673
|
-
metaAliasMap,
|
|
674
|
-
result.diagnostics,
|
|
675
|
-
nameAliasMap
|
|
676
|
-
);
|
|
677
|
-
if (edge) {
|
|
678
|
-
result.edges.push(edge);
|
|
679
|
-
// Add to current group if indented
|
|
680
|
-
// (edges don't become group children, but their nodes might)
|
|
681
|
-
}
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Node: everything else
|
|
686
|
-
contentStarted = true;
|
|
687
|
-
currentTagGroup = null;
|
|
688
|
-
flushDescription(); // Flush any pending description from previous node
|
|
689
|
-
const node = parseNodeLine(
|
|
690
|
-
trimmed,
|
|
691
|
-
lineNum,
|
|
692
|
-
metaAliasMap,
|
|
693
|
-
result.diagnostics,
|
|
694
|
-
nameAliasMap
|
|
695
|
-
);
|
|
696
|
-
if (!node) {
|
|
697
|
-
result.diagnostics.push(
|
|
698
|
-
makeDgmoError(lineNum, `Unexpected line: '${trimmed}'.`, 'warning')
|
|
699
|
-
);
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
lastNodeLabel = node.label;
|
|
703
|
-
lastSourceIsGroup = false;
|
|
704
|
-
lastNodeIndent = indent;
|
|
705
|
-
|
|
706
|
-
const gs = currentGroupState();
|
|
707
|
-
const isGroupChild = gs && indent > gs.indent;
|
|
708
|
-
|
|
709
|
-
const key = normalizeName(node.label);
|
|
710
|
-
if (nodeLabels.has(key)) {
|
|
711
|
-
// Already declared — if inside a group, just add as child (no duplicate)
|
|
712
|
-
if (isGroupChild) {
|
|
713
|
-
gs.group.children.push(node.label);
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
result.diagnostics.push(
|
|
717
|
-
makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
|
|
718
|
-
);
|
|
719
|
-
} else {
|
|
720
|
-
nodeLabels.add(key);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Cascade group metadata into node (group provides defaults, node overrides)
|
|
724
|
-
if (isGroupChild) {
|
|
725
|
-
for (const [key, val] of Object.entries(gs.group.metadata)) {
|
|
726
|
-
if (!(key in node.metadata)) {
|
|
727
|
-
node.metadata[key] = val;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
gs.group.children.push(node.label);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
result.nodes.push(node);
|
|
734
|
-
descState = { nodeLabel: node.label, indent, lines: [], edgeSeen: false };
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Flush any remaining description
|
|
738
|
-
flushDescription();
|
|
739
|
-
|
|
740
|
-
// Close any remaining groups
|
|
741
|
-
while (groupStack.length > 0) {
|
|
742
|
-
const gs = groupStack.pop()!;
|
|
743
|
-
result.groups.push(gs.group);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Validate group references and implicitly create node endpoints
|
|
747
|
-
const validEdges: BLEdge[] = [];
|
|
748
|
-
for (const edge of result.edges) {
|
|
749
|
-
let valid = true;
|
|
750
|
-
|
|
751
|
-
// Check group references exist
|
|
752
|
-
const allGroups = Array.from(groupLabels);
|
|
753
|
-
if (edge.source.startsWith('__group_')) {
|
|
754
|
-
const label = edge.source.slice('__group_'.length);
|
|
755
|
-
const found = groupLabels.has(normalizeName(label));
|
|
756
|
-
if (!found) {
|
|
757
|
-
let msg = `Group '[${label}]' not found`;
|
|
758
|
-
const hint = suggest(normalizeName(label), allGroups);
|
|
759
|
-
if (hint) msg += `. ${hint}`;
|
|
760
|
-
result.diagnostics.push(makeDgmoError(edge.lineNumber, msg));
|
|
761
|
-
valid = false;
|
|
762
|
-
}
|
|
763
|
-
} else {
|
|
764
|
-
ensureNode(edge.source, edge.lineNumber);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (edge.target.startsWith('__group_')) {
|
|
768
|
-
const label = edge.target.slice('__group_'.length);
|
|
769
|
-
const found = groupLabels.has(normalizeName(label));
|
|
770
|
-
if (!found) {
|
|
771
|
-
let msg = `Group '[${label}]' not found`;
|
|
772
|
-
const hint = suggest(normalizeName(label), allGroups);
|
|
773
|
-
if (hint) msg += `. ${hint}`;
|
|
774
|
-
result.diagnostics.push(makeDgmoError(edge.lineNumber, msg));
|
|
775
|
-
valid = false;
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
ensureNode(edge.target, edge.lineNumber);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (valid) {
|
|
782
|
-
validEdges.push(edge);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
result.edges = validEdges;
|
|
786
|
-
|
|
787
|
-
// Resolve note refs against box labels (forward refs OK). The id→note
|
|
788
|
-
// binding is recomputed in layout; this pass surfaces diagnostics.
|
|
789
|
-
if (notes.length > 0) {
|
|
790
|
-
result.notes = notes;
|
|
791
|
-
resolveNotes(
|
|
792
|
-
notes,
|
|
793
|
-
result.nodes.map((n) => ({ id: n.label, label: n.label })),
|
|
794
|
-
result.diagnostics
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Assign palette colors to bare (colorless) tag values. Boxes-and-lines
|
|
799
|
-
// resolves explicit tag colors against the Nord defaults (no palette is
|
|
800
|
-
// passed to extractColor above), so auto colors match for consistency.
|
|
801
|
-
finalizeAutoTagColors(result.tagGroups as Writable<TagGroup>[]);
|
|
802
|
-
|
|
803
|
-
// Attach parsed `layout` positions. Validate coverage: unknown ids warn; a
|
|
804
|
-
// PARTIAL block (some nodes unpositioned) is honored by neither pin nor seed —
|
|
805
|
-
// the layout engine ignores it and auto-lays-out (Decision 3, AC12), so emit a
|
|
806
|
-
// single diagnostic naming the gap.
|
|
807
|
-
if (nodePositions.size > 0) {
|
|
808
|
-
const nodeLabelSet = new Set(result.nodes.map((n) => n.label));
|
|
809
|
-
for (const id of nodePositions.keys()) {
|
|
810
|
-
if (!nodeLabelSet.has(id)) {
|
|
811
|
-
pushWarning(0, `layout entry for unknown node "${id}" (ignored)`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
const unpositioned = result.nodes
|
|
815
|
-
.filter((n) => !nodePositions.has(n.label))
|
|
816
|
-
.map((n) => n.label);
|
|
817
|
-
if (unpositioned.length > 0) {
|
|
818
|
-
pushWarning(
|
|
819
|
-
0,
|
|
820
|
-
`layout block is partial — ${unpositioned.length} node(s) without coordinates ` +
|
|
821
|
-
`(${unpositioned.slice(0, 5).join(', ')}${unpositioned.length > 5 ? '…' : ''}); ` +
|
|
822
|
-
`ignoring the block and auto-laying-out`
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
result.nodePositions = nodePositions;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Post-parse: inject default tag metadata and validate tag values
|
|
829
|
-
if (result.tagGroups.length > 0) {
|
|
830
|
-
injectDefaultTagMetadata(result.nodes, result.tagGroups);
|
|
831
|
-
validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
|
|
832
|
-
validateTagGroupNames(result.tagGroups, pushWarning, (line, msg) => {
|
|
833
|
-
const diag = makeDgmoError(line, msg);
|
|
834
|
-
result.diagnostics.push(diag);
|
|
835
|
-
if (!result.error) result.error = diag.message;
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return result;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ============================================================
|
|
843
|
-
// Line parsers
|
|
844
|
-
// ============================================================
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Parse a node line. Supports both the new §1.4 same-line form
|
|
848
|
-
* (`Label key: value, key2: value2`) and the legacy pipe form
|
|
849
|
-
* (`Label | key: value, ...`); the top-of-loop pipe diagnostic
|
|
850
|
-
* already fires for the latter.
|
|
851
|
-
*/
|
|
852
|
-
function parseNodeLine(
|
|
853
|
-
trimmed: string,
|
|
854
|
-
lineNum: number,
|
|
855
|
-
metaAliasMap: Map<string, string>,
|
|
856
|
-
diagnostics: DgmoError[],
|
|
857
|
-
nameAliasMap?: Map<string, string>
|
|
858
|
-
): MutBLNode | null {
|
|
859
|
-
const registry = withTagAliases(
|
|
860
|
-
BOXES_AND_LINES_REGISTRY,
|
|
861
|
-
new Set(metaAliasMap.keys())
|
|
862
|
-
);
|
|
863
|
-
const split = splitNameAndMeta(
|
|
864
|
-
trimmed,
|
|
865
|
-
registry,
|
|
866
|
-
metaAliasMap,
|
|
867
|
-
undefined,
|
|
868
|
-
diagnostics,
|
|
869
|
-
lineNum
|
|
870
|
-
);
|
|
871
|
-
warnUnknownMetaKeys(
|
|
872
|
-
split.meta,
|
|
873
|
-
registry,
|
|
874
|
-
(msg) => diagnostics.push(makeDgmoError(lineNum, msg, 'warning')),
|
|
875
|
-
split.name
|
|
876
|
-
);
|
|
877
|
-
|
|
878
|
-
let label = split.name;
|
|
879
|
-
const metadata: Record<string, string> = { ...split.meta };
|
|
880
|
-
let description: string[] | undefined;
|
|
881
|
-
// §1.5 trailing-token color sits in the label slot for nodes
|
|
882
|
-
// that don't store color separately — re-attach so the literal
|
|
883
|
-
// label text matches author intent.
|
|
884
|
-
if (split.color !== undefined) {
|
|
885
|
-
label = `${label} ${split.color}`;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Promote a `description` key out of metadata for the new path.
|
|
889
|
-
if (metadata['description'] !== undefined) {
|
|
890
|
-
description = [metadata['description']];
|
|
891
|
-
delete metadata['description'];
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Lift `heat: X` out of metadata into a typed numeric field (mirror of the
|
|
895
|
-
// map parser's region `heat:`). Validate finite-numeric; delete from metadata
|
|
896
|
-
// so it never becomes a `data-tag-value` attribute.
|
|
897
|
-
let value: number | undefined;
|
|
898
|
-
if (metadata['heat'] !== undefined) {
|
|
899
|
-
const raw = metadata['heat'];
|
|
900
|
-
const num = Number(raw);
|
|
901
|
-
if (Number.isFinite(num)) {
|
|
902
|
-
value = num;
|
|
903
|
-
} else {
|
|
904
|
-
diagnostics.push(
|
|
905
|
-
makeDgmoError(lineNum, `heat must be a number (got "${raw}")`, 'error')
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
|
-
delete metadata['heat'];
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// TD-18 alias is now peeled by splitNameAndMeta — re-register if set.
|
|
912
|
-
if (split.alias) {
|
|
913
|
-
nameAliasMap?.set(normalizeName(split.alias), label);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (!label) return null;
|
|
917
|
-
|
|
918
|
-
return {
|
|
919
|
-
label,
|
|
920
|
-
lineNumber: lineNum,
|
|
921
|
-
metadata,
|
|
922
|
-
...(description !== undefined && { description }),
|
|
923
|
-
...(value !== undefined && { value }),
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Split the right-hand side of an edge (`Target k: v`) into a clean
|
|
929
|
-
* target name + parsed metadata. The metadata cut is at the first
|
|
930
|
-
* reserved key (via splitNameAndMeta + BOXES_AND_LINES_REGISTRY).
|
|
931
|
-
*
|
|
932
|
-
* `[Group]` bracket targets are preserved verbatim — the bracket
|
|
933
|
-
* literal is the target name, not a structural sigil to peel.
|
|
934
|
-
*/
|
|
935
|
-
function splitTargetAndMeta(
|
|
936
|
-
rest: string,
|
|
937
|
-
metaAliasMap: Map<string, string>
|
|
938
|
-
): { target: string; metadata: Record<string, string> } {
|
|
939
|
-
// §1.4 same-line: cut at the first reserved-key colon. If the
|
|
940
|
-
// target is wrapped in `[brackets]` (group endpoint), keep the
|
|
941
|
-
// bracketed form intact.
|
|
942
|
-
const registry = withTagAliases(
|
|
943
|
-
BOXES_AND_LINES_REGISTRY,
|
|
944
|
-
new Set(metaAliasMap.keys())
|
|
945
|
-
);
|
|
946
|
-
const split = splitNameAndMeta(rest, registry, metaAliasMap);
|
|
947
|
-
let target = split.name;
|
|
948
|
-
if (split.color !== undefined) {
|
|
949
|
-
// Colors as trailing tokens stay on the target literal for
|
|
950
|
-
// identifier-style endpoints; reattach.
|
|
951
|
-
target = `${target} ${split.color}`;
|
|
952
|
-
}
|
|
953
|
-
return { target, metadata: split.meta };
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Convert `[Group Name]` to `__group_Group Name`, resolve TD-18 alias
|
|
958
|
-
* literals to their canonical name, otherwise return as-is.
|
|
959
|
-
*/
|
|
960
|
-
function resolveEndpoint(
|
|
961
|
-
name: string,
|
|
962
|
-
nameAliasMap?: Map<string, string>
|
|
963
|
-
): string {
|
|
964
|
-
const m = name.match(/^\[(.+)\]$/);
|
|
965
|
-
// Regex capture group present after successful match.
|
|
966
|
-
if (m) return groupId(m[1]!.trim());
|
|
967
|
-
if (nameAliasMap) {
|
|
968
|
-
const aliased = nameAliasMap.get(name.trim());
|
|
969
|
-
if (aliased !== undefined) return aliased;
|
|
970
|
-
}
|
|
971
|
-
return name;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Parse an edge line. Supports:
|
|
976
|
-
* - `Source -> Target`
|
|
977
|
-
* - `Source -> Target key: value`
|
|
978
|
-
* - `Source -label-> Target`
|
|
979
|
-
* - `Source <-> Target`
|
|
980
|
-
* - `Source <-label-> Target`
|
|
981
|
-
* - `Source -label-> Target key: value`
|
|
982
|
-
*
|
|
983
|
-
* `[Group Name]` in source or target position is resolved to `__group_Group Name`.
|
|
984
|
-
*/
|
|
985
|
-
function parseEdgeLine(
|
|
986
|
-
trimmed: string,
|
|
987
|
-
lineNum: number,
|
|
988
|
-
metaAliasMap: Map<string, string>,
|
|
989
|
-
diagnostics: DgmoError[],
|
|
990
|
-
nameAliasMap?: Map<string, string>
|
|
991
|
-
): BLEdge | null {
|
|
992
|
-
const edgeRegistry = withTagAliases(
|
|
993
|
-
BOXES_AND_LINES_REGISTRY,
|
|
994
|
-
new Set(metaAliasMap.keys())
|
|
995
|
-
);
|
|
996
|
-
// Check for bidirectional labeled: `Source <-label-> Target`
|
|
997
|
-
const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
|
|
998
|
-
if (biLabeledMatch) {
|
|
999
|
-
// Regex capture groups present after successful match.
|
|
1000
|
-
const rawSource = biLabeledMatch[1]!.trim();
|
|
1001
|
-
const source = resolveEndpoint(rawSource, nameAliasMap);
|
|
1002
|
-
const labelResult = parseInArrowLabel(biLabeledMatch[2]!, lineNum);
|
|
1003
|
-
diagnostics.push(...labelResult.diagnostics);
|
|
1004
|
-
const label = labelResult.label;
|
|
1005
|
-
let rest = biLabeledMatch[3]!.trim();
|
|
1006
|
-
|
|
1007
|
-
const { target: biTarget, metadata } = splitTargetAndMeta(
|
|
1008
|
-
rest,
|
|
1009
|
-
metaAliasMap
|
|
1010
|
-
);
|
|
1011
|
-
warnUnknownMetaKeys(metadata, edgeRegistry, (msg) =>
|
|
1012
|
-
diagnostics.push(makeDgmoError(lineNum, msg, 'warning'))
|
|
1013
|
-
);
|
|
1014
|
-
rest = biTarget;
|
|
1015
|
-
|
|
1016
|
-
if (!source || !rest) {
|
|
1017
|
-
diagnostics.push(
|
|
1018
|
-
makeDgmoError(lineNum, 'Edge is missing source or target')
|
|
1019
|
-
);
|
|
1020
|
-
return null;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (rawSource.endsWith(':') || rest.endsWith(':')) {
|
|
1024
|
-
diagnostics.push(
|
|
1025
|
-
makeDgmoError(
|
|
1026
|
-
lineNum,
|
|
1027
|
-
`Trailing colon is not valid — write '${rawSource.replace(/:$/, '')} <-> ${rest.replace(/:$/, '')}' instead`
|
|
1028
|
-
)
|
|
1029
|
-
);
|
|
1030
|
-
return null;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return {
|
|
1034
|
-
source,
|
|
1035
|
-
target: resolveEndpoint(rest, nameAliasMap),
|
|
1036
|
-
...(label !== undefined && { label }),
|
|
1037
|
-
bidirectional: true,
|
|
1038
|
-
lineNumber: lineNum,
|
|
1039
|
-
metadata,
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Check for bidirectional plain: `Source <-> Target`
|
|
1044
|
-
const biIdx = trimmed.indexOf('<->');
|
|
1045
|
-
if (biIdx >= 0) {
|
|
1046
|
-
const rawSource = trimmed.slice(0, biIdx).trim();
|
|
1047
|
-
const source = resolveEndpoint(rawSource, nameAliasMap);
|
|
1048
|
-
let rest = trimmed.slice(biIdx + 3).trim();
|
|
1049
|
-
|
|
1050
|
-
const { target: plainTarget, metadata } = splitTargetAndMeta(
|
|
1051
|
-
rest,
|
|
1052
|
-
metaAliasMap
|
|
1053
|
-
);
|
|
1054
|
-
warnUnknownMetaKeys(metadata, edgeRegistry, (msg) =>
|
|
1055
|
-
diagnostics.push(makeDgmoError(lineNum, msg, 'warning'))
|
|
1056
|
-
);
|
|
1057
|
-
rest = plainTarget;
|
|
1058
|
-
|
|
1059
|
-
if (!source || !rest) {
|
|
1060
|
-
diagnostics.push(
|
|
1061
|
-
makeDgmoError(lineNum, 'Edge is missing source or target')
|
|
1062
|
-
);
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
if (rawSource.endsWith(':') || rest.endsWith(':')) {
|
|
1067
|
-
diagnostics.push(
|
|
1068
|
-
makeDgmoError(
|
|
1069
|
-
lineNum,
|
|
1070
|
-
`Trailing colon is not valid — write '${rawSource.replace(/:$/, '')} <-> ${rest.replace(/:$/, '')}' instead`
|
|
1071
|
-
)
|
|
1072
|
-
);
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
return {
|
|
1077
|
-
source,
|
|
1078
|
-
target: resolveEndpoint(rest, nameAliasMap),
|
|
1079
|
-
bidirectional: true,
|
|
1080
|
-
lineNumber: lineNum,
|
|
1081
|
-
metadata,
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Check for labeled arrow: `Source -label-> Target` (label lazy → split
|
|
1086
|
-
// at the first arrow, consistent with the other parsers).
|
|
1087
|
-
const labeledMatch = trimmed.match(/^(.+?)\s+-(.+?)->\s*(.+)$/);
|
|
1088
|
-
if (labeledMatch) {
|
|
1089
|
-
// Regex capture groups present after successful match.
|
|
1090
|
-
const rawSource = labeledMatch[1]!.trim();
|
|
1091
|
-
const source = resolveEndpoint(rawSource, nameAliasMap);
|
|
1092
|
-
const labelResult = parseInArrowLabel(labeledMatch[2]!, lineNum);
|
|
1093
|
-
diagnostics.push(...labelResult.diagnostics);
|
|
1094
|
-
const label = labelResult.label;
|
|
1095
|
-
let rest = labeledMatch[3]!.trim();
|
|
1096
|
-
|
|
1097
|
-
if (label) {
|
|
1098
|
-
const { target: labeledTarget, metadata } = splitTargetAndMeta(
|
|
1099
|
-
rest,
|
|
1100
|
-
metaAliasMap
|
|
1101
|
-
);
|
|
1102
|
-
warnUnknownMetaKeys(metadata, edgeRegistry, (msg) =>
|
|
1103
|
-
diagnostics.push(makeDgmoError(lineNum, msg, 'warning'))
|
|
1104
|
-
);
|
|
1105
|
-
rest = labeledTarget;
|
|
1106
|
-
|
|
1107
|
-
if (!source || !rest) {
|
|
1108
|
-
diagnostics.push(
|
|
1109
|
-
makeDgmoError(lineNum, 'Edge is missing source or target')
|
|
1110
|
-
);
|
|
1111
|
-
return null;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (rawSource.endsWith(':') || rest.endsWith(':')) {
|
|
1115
|
-
diagnostics.push(
|
|
1116
|
-
makeDgmoError(
|
|
1117
|
-
lineNum,
|
|
1118
|
-
`Trailing colon is not valid — write '${rawSource.replace(/:$/, '')} -> ${rest.replace(/:$/, '')}' instead`
|
|
1119
|
-
)
|
|
1120
|
-
);
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
return {
|
|
1125
|
-
source,
|
|
1126
|
-
target: resolveEndpoint(rest, nameAliasMap),
|
|
1127
|
-
label,
|
|
1128
|
-
bidirectional: false,
|
|
1129
|
-
lineNumber: lineNum,
|
|
1130
|
-
metadata,
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// Plain arrow: `Source -> Target`
|
|
1136
|
-
const arrowIdx = trimmed.indexOf('->');
|
|
1137
|
-
if (arrowIdx < 0) return null;
|
|
1138
|
-
|
|
1139
|
-
const rawSource = trimmed.slice(0, arrowIdx).trim();
|
|
1140
|
-
const source = resolveEndpoint(rawSource, nameAliasMap);
|
|
1141
|
-
let rest = trimmed.slice(arrowIdx + 2).trim();
|
|
1142
|
-
|
|
1143
|
-
if (!source || !rest) {
|
|
1144
|
-
diagnostics.push(
|
|
1145
|
-
makeDgmoError(lineNum, 'Edge is missing source or target')
|
|
1146
|
-
);
|
|
1147
|
-
return null;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const { target: plainTarget, metadata } = splitTargetAndMeta(
|
|
1151
|
-
rest,
|
|
1152
|
-
metaAliasMap
|
|
1153
|
-
);
|
|
1154
|
-
warnUnknownMetaKeys(metadata, edgeRegistry, (msg) =>
|
|
1155
|
-
diagnostics.push(makeDgmoError(lineNum, msg, 'warning'))
|
|
1156
|
-
);
|
|
1157
|
-
rest = plainTarget;
|
|
1158
|
-
|
|
1159
|
-
if (!rest) {
|
|
1160
|
-
diagnostics.push(makeDgmoError(lineNum, 'Edge is missing target'));
|
|
1161
|
-
return null;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
if (rawSource.endsWith(':') || rest.endsWith(':')) {
|
|
1165
|
-
diagnostics.push(
|
|
1166
|
-
makeDgmoError(
|
|
1167
|
-
lineNum,
|
|
1168
|
-
`Trailing colon is not valid — write '${rawSource.replace(/:$/, '')} -> ${rest.replace(/:$/, '')}' instead`
|
|
1169
|
-
)
|
|
1170
|
-
);
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
return {
|
|
1175
|
-
source,
|
|
1176
|
-
target: resolveEndpoint(rest, nameAliasMap),
|
|
1177
|
-
bidirectional: false,
|
|
1178
|
-
lineNumber: lineNum,
|
|
1179
|
-
metadata,
|
|
1180
|
-
};
|
|
1181
|
-
}
|