@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
package/src/er/parser.ts
DELETED
|
@@ -1,750 +0,0 @@
|
|
|
1
|
-
import { resolveColorWithDiagnostic } from '../colors';
|
|
2
|
-
import type { PaletteColors } from '../palettes';
|
|
3
|
-
import {
|
|
4
|
-
formatDgmoError,
|
|
5
|
-
makeDgmoError,
|
|
6
|
-
NAME_DIAGNOSTIC_CODES,
|
|
7
|
-
nameMergedMessage,
|
|
8
|
-
suggest,
|
|
9
|
-
} from '../diagnostics';
|
|
10
|
-
import { ER_REGISTRY, withTagAliases } from '../utils/reserved-key-registry';
|
|
11
|
-
import { validateLabelCharacters } from '../utils/arrows';
|
|
12
|
-
import { normalizeName, displayName } from '../utils/name-normalize';
|
|
13
|
-
import {
|
|
14
|
-
measureIndent,
|
|
15
|
-
extractColor,
|
|
16
|
-
splitNameAndMeta,
|
|
17
|
-
parseFirstLine,
|
|
18
|
-
OPTION_NOCOLON_RE,
|
|
19
|
-
tryParseSharedOption,
|
|
20
|
-
stripQuotes,
|
|
21
|
-
tokenizeQuoteAware,
|
|
22
|
-
warnUnknownMetaKeys,
|
|
23
|
-
} from '../utils/parsing';
|
|
24
|
-
import {
|
|
25
|
-
matchTagBlockHeading,
|
|
26
|
-
validateTagValues,
|
|
27
|
-
validateTagGroupNames,
|
|
28
|
-
stripDefaultModifier,
|
|
29
|
-
finalizeAutoTagColors,
|
|
30
|
-
AUTO_TAG_COLOR_SENTINEL,
|
|
31
|
-
tagAttrKey,
|
|
32
|
-
} from '../utils/tag-groups';
|
|
33
|
-
import type { TagGroup } from '../utils/tag-groups';
|
|
34
|
-
import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
|
|
35
|
-
import type { Writable } from '../utils/brand';
|
|
36
|
-
import type {
|
|
37
|
-
ParsedERDiagram,
|
|
38
|
-
ERTable,
|
|
39
|
-
ERConstraint,
|
|
40
|
-
ERCardinality,
|
|
41
|
-
} from './types';
|
|
42
|
-
|
|
43
|
-
// ============================================================
|
|
44
|
-
// Helpers
|
|
45
|
-
// ============================================================
|
|
46
|
-
|
|
47
|
-
function tableId(name: string): string {
|
|
48
|
-
return normalizeName(name);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ============================================================
|
|
52
|
-
// Regex patterns
|
|
53
|
-
// ============================================================
|
|
54
|
-
|
|
55
|
-
// Table declaration: `name`, `name color` (trailing-token §1.5), or
|
|
56
|
-
// `name | key: value`. Multi-word names allowed; quote `"name with reserved
|
|
57
|
-
// chars"` if the name contains pipe / paren / colon. Captures:
|
|
58
|
-
// 1: quoted-name content (without surrounding quotes), or undefined
|
|
59
|
-
// 2: bare-name (trimmed at call site), or undefined
|
|
60
|
-
// 3: trailing-token color (recognized palette word), or undefined
|
|
61
|
-
// 4: pipe metadata (without leading `|`), or undefined
|
|
62
|
-
const TABLE_DECL_RE =
|
|
63
|
-
/^(?:"([^"]+)"|([a-zA-Z_][^|":(]*?))(?:\s+(red|orange|yellow|green|blue|purple|teal|cyan|gray|black|white))?(?:\s*\|(.+))?$/;
|
|
64
|
-
|
|
65
|
-
// Column: name [type] [constraints...] — space-separated, no colon, no brackets
|
|
66
|
-
// First token is always the name. Second token is the type if it's not a constraint keyword.
|
|
67
|
-
// Remaining tokens are constraint keywords (pk, fk, unique, nullable).
|
|
68
|
-
// Handled programmatically, not with a single regex.
|
|
69
|
-
|
|
70
|
-
// Indented relationship: 1-* target, 1--* target, or 1-label-* target / 1--label--* target
|
|
71
|
-
// Target name accepts multi-word + quoted form per Universal Name Handling.
|
|
72
|
-
const INDENT_REL_RE =
|
|
73
|
-
/^([1*?])-{1,2}(?:(.+?)-{1,2})?([1*?])\s+(?:"([^"]+)"|([a-zA-Z_][^":]*?))\s*$/;
|
|
74
|
-
|
|
75
|
-
// Constraint keywords
|
|
76
|
-
const CONSTRAINT_MAP: Record<string, ERConstraint> = {
|
|
77
|
-
pk: 'pk',
|
|
78
|
-
fk: 'fk',
|
|
79
|
-
unique: 'unique',
|
|
80
|
-
nullable: 'nullable',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Known options (space-separated, no colon)
|
|
84
|
-
const KNOWN_OPTIONS = new Set(['notation', 'active-tag']);
|
|
85
|
-
|
|
86
|
-
// ============================================================
|
|
87
|
-
// Cardinality parsing
|
|
88
|
-
// ============================================================
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Parse a cardinality side token (symbolic only: "1", "*", "?").
|
|
92
|
-
*/
|
|
93
|
-
function parseCardSide(token: string): ERCardinality | null {
|
|
94
|
-
if (token === '1' || token === '*' || token === '?') return token;
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Try to parse a relationship line with symbolic cardinality.
|
|
100
|
-
*
|
|
101
|
-
* Supported form (no colon before label):
|
|
102
|
-
* tableName 1--* tableName label
|
|
103
|
-
* tableName 1-* tableName label
|
|
104
|
-
* tableName ?--1 tableName
|
|
105
|
-
*/
|
|
106
|
-
// Top-level relationship (symbolic): SourceName 1--* TargetName [label]
|
|
107
|
-
// Both names accept quoted or multi-word bare form.
|
|
108
|
-
const REL_SYMBOLIC_RE =
|
|
109
|
-
/^(?:"([^"]+)"|([a-zA-Z_][^":]*?))\s+([1*?])\s*-{1,2}\s*([1*?])\s+(?:"([^"]+)"|([a-zA-Z_][^":]*?))(?:\s+(.+))?$/;
|
|
110
|
-
|
|
111
|
-
/** Detects keyword cardinality forms to emit helpful error */
|
|
112
|
-
const REL_KEYWORD_RE =
|
|
113
|
-
/^(?:"([^"]+)"|([a-zA-Z_][^":]*?))\s+(one|many|zero)[- ]to[- ](one|many|zero)\s+(?:"([^"]+)"|([a-zA-Z_][^":]*?))(?:\s+(.+))?$/i;
|
|
114
|
-
|
|
115
|
-
const KEYWORD_TO_SYMBOL: Record<string, string> = {
|
|
116
|
-
one: '1',
|
|
117
|
-
many: '*',
|
|
118
|
-
zero: '?',
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
function parseRelationship(
|
|
122
|
-
trimmed: string,
|
|
123
|
-
lineNumber: number,
|
|
124
|
-
pushError: (line: number, message: string) => void
|
|
125
|
-
): {
|
|
126
|
-
source: string;
|
|
127
|
-
target: string;
|
|
128
|
-
from: ERCardinality;
|
|
129
|
-
to: ERCardinality;
|
|
130
|
-
label?: string;
|
|
131
|
-
} | null {
|
|
132
|
-
// Symbolic: 1--*, 1-*, ?--1, etc.
|
|
133
|
-
// Captures: [1]=qSrc [2]=bareSrc [3]=fromCard [4]=toCard [5]=qTarget [6]=bareTarget [7]=label
|
|
134
|
-
const sym = trimmed.match(REL_SYMBOLIC_RE);
|
|
135
|
-
if (sym) {
|
|
136
|
-
// Groups 3 and 4 are non-optional in REL_SYMBOLIC_RE — always defined on match.
|
|
137
|
-
const fromCard = parseCardSide(sym[3]!);
|
|
138
|
-
const toCard = parseCardSide(sym[4]!);
|
|
139
|
-
if (fromCard && toCard) {
|
|
140
|
-
const sourceName = (sym[1] ?? sym[2] ?? '').trim();
|
|
141
|
-
const targetName = (sym[5] ?? sym[6] ?? '').trim();
|
|
142
|
-
const label = sym[7]?.trim();
|
|
143
|
-
if (label) {
|
|
144
|
-
validateLabelCharacters(label, lineNumber).forEach((d) =>
|
|
145
|
-
pushError(d.line, d.message)
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
source: tableId(sourceName),
|
|
150
|
-
target: tableId(targetName),
|
|
151
|
-
from: fromCard,
|
|
152
|
-
to: toCard,
|
|
153
|
-
...(label !== undefined && { label }),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Keyword / natural: produce helpful error with symbolic suggestion.
|
|
159
|
-
// Captures: [1]=qSrc [2]=bareSrc [3]=fromKw [4]=toKw [5]=qTarget [6]=bareTarget [7]=label
|
|
160
|
-
const kw = trimmed.match(REL_KEYWORD_RE);
|
|
161
|
-
if (kw) {
|
|
162
|
-
const sourceName = (kw[1] ?? kw[2] ?? '').trim();
|
|
163
|
-
const targetName = (kw[5] ?? kw[6] ?? '').trim();
|
|
164
|
-
// Groups 3 and 4 are non-optional in REL_KEYWORD_RE — always defined on match.
|
|
165
|
-
const fromKw = kw[3]!;
|
|
166
|
-
const toKw = kw[4]!;
|
|
167
|
-
const fromSym = KEYWORD_TO_SYMBOL[fromKw.toLowerCase()] ?? fromKw;
|
|
168
|
-
const toSym = KEYWORD_TO_SYMBOL[toKw.toLowerCase()] ?? toKw;
|
|
169
|
-
pushError(
|
|
170
|
-
lineNumber,
|
|
171
|
-
`Use symbolic cardinality (1--*, ?--1, *--*) instead of "${fromKw}-to-${toKw}". Example: ${sourceName} ${fromSym}--${toSym} ${targetName}`
|
|
172
|
-
);
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ============================================================
|
|
180
|
-
// Column parser (space-separated: name [type] [constraints...])
|
|
181
|
-
// ============================================================
|
|
182
|
-
|
|
183
|
-
function parseColumn(trimmed: string): {
|
|
184
|
-
name: string;
|
|
185
|
-
type?: string;
|
|
186
|
-
constraints: ERConstraint[];
|
|
187
|
-
} | null {
|
|
188
|
-
// Quote-aware tokenizer keeps `"first name"` together as a single token
|
|
189
|
-
// so multi-word column names work via Universal Name Handling quoting.
|
|
190
|
-
const rawTokens = tokenizeQuoteAware(trimmed);
|
|
191
|
-
if (rawTokens.length === 0) return null;
|
|
192
|
-
|
|
193
|
-
// In-bounds by length-0 guard above.
|
|
194
|
-
const firstRaw = rawTokens[0]!;
|
|
195
|
-
const wasQuoted = firstRaw.startsWith('"') || firstRaw.startsWith("'");
|
|
196
|
-
const name = stripQuotes(firstRaw);
|
|
197
|
-
|
|
198
|
-
// Bare names must look like a single word (preserves rejection of lines
|
|
199
|
-
// that aren't column declarations, e.g. relationships). Quoted names are
|
|
200
|
-
// accepted as-is.
|
|
201
|
-
if (!wasQuoted && !/^\w+$/.test(name)) return null;
|
|
202
|
-
|
|
203
|
-
const constraints: ERConstraint[] = [];
|
|
204
|
-
let type: string | undefined;
|
|
205
|
-
|
|
206
|
-
for (let i = 1; i < rawTokens.length; i++) {
|
|
207
|
-
// In-bounds by loop guard.
|
|
208
|
-
const tok = stripQuotes(rawTokens[i]!);
|
|
209
|
-
const lower = tok.toLowerCase();
|
|
210
|
-
const constraint = CONSTRAINT_MAP[lower];
|
|
211
|
-
if (constraint) {
|
|
212
|
-
constraints.push(constraint);
|
|
213
|
-
} else if (type === undefined) {
|
|
214
|
-
// First non-constraint token after name is the type
|
|
215
|
-
type = tok;
|
|
216
|
-
} else {
|
|
217
|
-
// Unknown token after type — not a valid column line
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return { name, ...(type !== undefined && { type }), constraints };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ============================================================
|
|
226
|
-
// Main parser
|
|
227
|
-
// ============================================================
|
|
228
|
-
|
|
229
|
-
export function parseERDiagram(
|
|
230
|
-
content: string,
|
|
231
|
-
palette?: PaletteColors
|
|
232
|
-
): ParsedERDiagram {
|
|
233
|
-
const lines = content.split('\n');
|
|
234
|
-
const options: Record<string, string> = {};
|
|
235
|
-
const result: Writable<ParsedERDiagram> = {
|
|
236
|
-
type: 'er',
|
|
237
|
-
options,
|
|
238
|
-
tables: [],
|
|
239
|
-
relationships: [],
|
|
240
|
-
tagGroups: [],
|
|
241
|
-
diagnostics: [],
|
|
242
|
-
error: null,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const pushError = (line: number, message: string): void => {
|
|
246
|
-
const diag = makeDgmoError(line, message);
|
|
247
|
-
result.diagnostics.push(diag);
|
|
248
|
-
if (!result.error) result.error = formatDgmoError(diag);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const tableMap = new Map<string, Writable<ERTable>>();
|
|
252
|
-
// Sidecar: track each table's metadata as a mutable Record so we can
|
|
253
|
-
// assign into it (table.metadata is `Readonly<Record<...>>` per the spec).
|
|
254
|
-
const tableMetadataMap = new Map<string, Record<string, string>>();
|
|
255
|
-
let currentTable: Writable<ERTable> | null = null;
|
|
256
|
-
const notes: DiagramNote[] = [];
|
|
257
|
-
let contentStarted = false;
|
|
258
|
-
let currentTagGroup: Writable<TagGroup> | null = null;
|
|
259
|
-
// metaAliasMap: tag-group metadata-key aliases (per A1 convention).
|
|
260
|
-
const metaAliasMap = new Map<string, string>();
|
|
261
|
-
// nameAliasMap: TD-18 entity-name aliases (`u` → `users`). Per C8.
|
|
262
|
-
const nameAliasMap = new Map<string, string>();
|
|
263
|
-
function peelAlias(label: string): { label: string; alias?: string } {
|
|
264
|
-
const trimmed = label.trim();
|
|
265
|
-
const m = trimmed.match(/^(.*?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/);
|
|
266
|
-
if (!m) return { label: trimmed };
|
|
267
|
-
// Groups 1 and 2 are non-optional — always defined on match.
|
|
268
|
-
return { label: m[1]!.trim(), alias: m[2]! };
|
|
269
|
-
}
|
|
270
|
-
function resolveAliasName(token: string): string {
|
|
271
|
-
return nameAliasMap.get(token.trim()) ?? token;
|
|
272
|
-
}
|
|
273
|
-
let firstLineParsed = false;
|
|
274
|
-
|
|
275
|
-
function getOrCreateTable(
|
|
276
|
-
name: string,
|
|
277
|
-
lineNumber: number
|
|
278
|
-
): Writable<ERTable> {
|
|
279
|
-
const key = tableId(name);
|
|
280
|
-
const existing = tableMap.get(key);
|
|
281
|
-
if (existing) {
|
|
282
|
-
const incomingDisplay = displayName(name);
|
|
283
|
-
const existingDisplay = displayName(existing.name);
|
|
284
|
-
if (incomingDisplay !== existingDisplay) {
|
|
285
|
-
result.diagnostics.push(
|
|
286
|
-
makeDgmoError(
|
|
287
|
-
lineNumber,
|
|
288
|
-
nameMergedMessage({
|
|
289
|
-
incomingDisplay,
|
|
290
|
-
incomingLine: lineNumber,
|
|
291
|
-
existingDisplay,
|
|
292
|
-
existingLine: existing.lineNumber,
|
|
293
|
-
}),
|
|
294
|
-
'warning',
|
|
295
|
-
NAME_DIAGNOSTIC_CODES.NAME_MERGED
|
|
296
|
-
)
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
return existing;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const metadata: Record<string, string> = {};
|
|
303
|
-
const table: Writable<ERTable> = {
|
|
304
|
-
id: key,
|
|
305
|
-
name,
|
|
306
|
-
columns: [],
|
|
307
|
-
metadata,
|
|
308
|
-
lineNumber,
|
|
309
|
-
};
|
|
310
|
-
tableMap.set(key, table);
|
|
311
|
-
tableMetadataMap.set(key, metadata);
|
|
312
|
-
result.tables.push(table);
|
|
313
|
-
return table;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
for (let i = 0; i < lines.length; i++) {
|
|
317
|
-
// In-bounds by loop guard.
|
|
318
|
-
const raw = lines[i]!;
|
|
319
|
-
const trimmed = raw.trim();
|
|
320
|
-
const lineNumber = i + 1;
|
|
321
|
-
const indent = measureIndent(raw);
|
|
322
|
-
|
|
323
|
-
// Skip empty lines
|
|
324
|
-
if (!trimmed) {
|
|
325
|
-
if (indent === 0) currentTable = null;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Skip comments
|
|
330
|
-
if (trimmed.startsWith('//')) continue;
|
|
331
|
-
|
|
332
|
-
// First line: chart type + optional title
|
|
333
|
-
if (!firstLineParsed && indent === 0) {
|
|
334
|
-
const firstLineResult = parseFirstLine(trimmed);
|
|
335
|
-
if (firstLineResult?.chartType === 'er') {
|
|
336
|
-
firstLineParsed = true;
|
|
337
|
-
if (firstLineResult.title) {
|
|
338
|
-
result.title = firstLineResult.title;
|
|
339
|
-
result.titleLineNumber = lineNumber;
|
|
340
|
-
}
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
// Not an explicit `er` first line — that's OK, treat as implicit
|
|
344
|
-
firstLineParsed = true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Tag group heading — `tag Name`
|
|
348
|
-
if (!contentStarted && indent === 0) {
|
|
349
|
-
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
350
|
-
if (tagBlockMatch) {
|
|
351
|
-
currentTagGroup = {
|
|
352
|
-
name: tagBlockMatch.name,
|
|
353
|
-
...(tagBlockMatch.alias !== undefined && {
|
|
354
|
-
alias: tagBlockMatch.alias,
|
|
355
|
-
}),
|
|
356
|
-
entries: [],
|
|
357
|
-
lineNumber,
|
|
358
|
-
};
|
|
359
|
-
if (tagBlockMatch.alias) {
|
|
360
|
-
metaAliasMap.set(
|
|
361
|
-
normalizeName(tagBlockMatch.alias),
|
|
362
|
-
tagBlockMatch.name.toLowerCase()
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
metaAliasMap.set(
|
|
366
|
-
normalizeName(tagBlockMatch.name),
|
|
367
|
-
tagBlockMatch.name.toLowerCase()
|
|
368
|
-
);
|
|
369
|
-
result.tagGroups.push(currentTagGroup);
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Tag group entries (indented under tag heading)
|
|
375
|
-
if (currentTagGroup && !contentStarted && indent > 0) {
|
|
376
|
-
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
377
|
-
const { label, color } = extractColor(
|
|
378
|
-
cleanEntry,
|
|
379
|
-
palette,
|
|
380
|
-
result.diagnostics,
|
|
381
|
-
lineNumber
|
|
382
|
-
);
|
|
383
|
-
// Bare value (no explicit color) → keep it; finalized below.
|
|
384
|
-
if (isDefault) {
|
|
385
|
-
currentTagGroup.defaultValue = label;
|
|
386
|
-
} else if (currentTagGroup.entries.length === 0) {
|
|
387
|
-
currentTagGroup.defaultValue = label;
|
|
388
|
-
}
|
|
389
|
-
currentTagGroup.entries.push({
|
|
390
|
-
value: label,
|
|
391
|
-
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
392
|
-
lineNumber,
|
|
393
|
-
});
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// End tag group on non-indented line
|
|
398
|
-
if (currentTagGroup && indent === 0) {
|
|
399
|
-
currentTagGroup = null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Options (space-separated, no colon) — before content
|
|
403
|
-
if (!contentStarted && indent === 0) {
|
|
404
|
-
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
405
|
-
if (optMatch) {
|
|
406
|
-
// Groups 1 and 2 are non-optional in OPTION_NOCOLON_RE — always defined on match.
|
|
407
|
-
const key = optMatch[1]!.toLowerCase();
|
|
408
|
-
const value = optMatch[2]!.trim();
|
|
409
|
-
if (KNOWN_OPTIONS.has(key)) {
|
|
410
|
-
options[key] = value.toLowerCase();
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// ER-local bare flag: suppress the semantic PK/FK role colors (on by
|
|
415
|
-
// default). View-state opt-out, mirrors `no-analysis` (§1 view-state).
|
|
416
|
-
if (trimmed.toLowerCase() === 'no-semantic-colors') {
|
|
417
|
-
options['no-semantic-colors'] = 'on';
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
if (tryParseSharedOption(trimmed, options)) {
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Indented lines = columns or relationships of current table
|
|
426
|
-
if (indent > 0 && currentTable) {
|
|
427
|
-
// Try indented relationship first: 1-* target or 1-label-* target
|
|
428
|
-
// ER chart-specific constraint: labels cannot contain `-` because
|
|
429
|
-
// INDENT_REL_RE uses `-{1,2}` as hard delimiters on both sides of the
|
|
430
|
-
// label. So `1-has-*` works but `1-has dashes-*` does not.
|
|
431
|
-
// INDENT_REL_RE captures: [1]=fromCard [2]=label [3]=toCard
|
|
432
|
-
// [4]=qTarget [5]=bareTarget
|
|
433
|
-
const indentRel = trimmed.match(INDENT_REL_RE);
|
|
434
|
-
if (indentRel) {
|
|
435
|
-
// Groups 1 and 3 are non-optional in INDENT_REL_RE — always defined on match.
|
|
436
|
-
const fromCard = parseCardSide(indentRel[1]!);
|
|
437
|
-
const toCard = parseCardSide(indentRel[3]!);
|
|
438
|
-
if (fromCard && toCard) {
|
|
439
|
-
const rawTarget = (indentRel[4] ?? indentRel[5] ?? '').trim();
|
|
440
|
-
// TD-18: resolve alias literal → canonical table name.
|
|
441
|
-
const targetName = resolveAliasName(rawTarget);
|
|
442
|
-
getOrCreateTable(targetName, lineNumber);
|
|
443
|
-
const rawLabel = indentRel[2]?.trim();
|
|
444
|
-
if (rawLabel) {
|
|
445
|
-
result.diagnostics.push(
|
|
446
|
-
...validateLabelCharacters(rawLabel, lineNumber)
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
result.relationships.push({
|
|
450
|
-
source: currentTable.id,
|
|
451
|
-
target: tableId(targetName),
|
|
452
|
-
cardinality: { from: fromCard, to: toCard },
|
|
453
|
-
...(rawLabel && { label: rawLabel }),
|
|
454
|
-
lineNumber,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const colResult = parseColumn(trimmed);
|
|
461
|
-
if (colResult) {
|
|
462
|
-
currentTable.columns.push({
|
|
463
|
-
name: colResult.name,
|
|
464
|
-
...(colResult.type && { type: colResult.type }),
|
|
465
|
-
constraints: colResult.constraints,
|
|
466
|
-
lineNumber,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// At indent 0 — this ends any previous table context
|
|
473
|
-
currentTable = null;
|
|
474
|
-
contentStarted = true;
|
|
475
|
-
|
|
476
|
-
// Note annotation (top-level): `note <Table> [inline body]` + an optional
|
|
477
|
-
// indented body. `note -> X` is excluded so it can still parse as content.
|
|
478
|
-
const noteResult = tryCollectNote(
|
|
479
|
-
lines,
|
|
480
|
-
i,
|
|
481
|
-
indent,
|
|
482
|
-
palette,
|
|
483
|
-
result.diagnostics
|
|
484
|
-
);
|
|
485
|
-
if (noteResult) {
|
|
486
|
-
if (noteResult.note) notes.push(noteResult.note);
|
|
487
|
-
i = noteResult.lastIndex;
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Reject top-level relationships — must be indented under source table
|
|
492
|
-
const rel = parseRelationship(trimmed, lineNumber, pushError);
|
|
493
|
-
if (rel) {
|
|
494
|
-
result.diagnostics.push(
|
|
495
|
-
makeDgmoError(
|
|
496
|
-
lineNumber,
|
|
497
|
-
`Relationship "${rel.source} → ${rel.target}" must be indented under the source table "${rel.source}"`,
|
|
498
|
-
'warning'
|
|
499
|
-
)
|
|
500
|
-
);
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Try table declaration.
|
|
505
|
-
//
|
|
506
|
-
// First, run §1.4 unified metadata cut on the full line — peels off
|
|
507
|
-
// same-line metadata (e.g. `Users red Domain: Billing` → name region
|
|
508
|
-
// `Users red`, color `red`, meta { domain: 'Billing' }). The remaining
|
|
509
|
-
// name region runs through the legacy quoted/bare-name regex.
|
|
510
|
-
const registry = withTagAliases(ER_REGISTRY, new Set(metaAliasMap.keys()));
|
|
511
|
-
const split = splitNameAndMeta(
|
|
512
|
-
trimmed,
|
|
513
|
-
registry,
|
|
514
|
-
metaAliasMap,
|
|
515
|
-
undefined,
|
|
516
|
-
result.diagnostics,
|
|
517
|
-
lineNumber
|
|
518
|
-
);
|
|
519
|
-
warnUnknownMetaKeys(
|
|
520
|
-
split.meta,
|
|
521
|
-
registry,
|
|
522
|
-
(msg) =>
|
|
523
|
-
result.diagnostics.push(makeDgmoError(lineNumber, msg, 'warning')),
|
|
524
|
-
split.name
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
// Re-run the legacy name regex on the cleaned name region (color is
|
|
528
|
-
// already peeled by splitNameAndMeta, so the regex's color slot
|
|
529
|
-
// shouldn't match; we accept either form for back-compat).
|
|
530
|
-
const tableDecl = split.name.match(TABLE_DECL_RE);
|
|
531
|
-
if (tableDecl) {
|
|
532
|
-
const rawName = (tableDecl[1] ?? tableDecl[2] ?? '').trim();
|
|
533
|
-
const peeled = peelAlias(rawName);
|
|
534
|
-
const name = peeled.label;
|
|
535
|
-
if (peeled.alias) nameAliasMap.set(peeled.alias, name);
|
|
536
|
-
// Color may have been captured either by the §1.5 trailing-token
|
|
537
|
-
// peel in splitNameAndMeta OR (if splitNameAndMeta didn't run a
|
|
538
|
-
// cut) by the legacy regex's color group.
|
|
539
|
-
const colorName = split.color ?? tableDecl[3]?.trim();
|
|
540
|
-
const color = colorName
|
|
541
|
-
? resolveColorWithDiagnostic(
|
|
542
|
-
colorName,
|
|
543
|
-
lineNumber,
|
|
544
|
-
result.diagnostics,
|
|
545
|
-
palette
|
|
546
|
-
)
|
|
547
|
-
: undefined;
|
|
548
|
-
|
|
549
|
-
const table = getOrCreateTable(name, lineNumber);
|
|
550
|
-
if (color) table.color = color;
|
|
551
|
-
table.lineNumber = lineNumber;
|
|
552
|
-
|
|
553
|
-
if (Object.keys(split.meta).length > 0) {
|
|
554
|
-
const mutableMeta = tableMetadataMap.get(table.id);
|
|
555
|
-
if (mutableMeta) Object.assign(mutableMeta, split.meta);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
currentTable = table;
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Catch-all: nothing matched this line
|
|
563
|
-
result.diagnostics.push(
|
|
564
|
-
makeDgmoError(
|
|
565
|
-
lineNumber,
|
|
566
|
-
`Unexpected line: '${trimmed}'. A table is a bare name at indent 0 ('users'); columns are indented and SPACE-separated, no colon ('id int pk'); relationships are indented ('1--* orders'). (§9)`,
|
|
567
|
-
'warning'
|
|
568
|
-
)
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Validation
|
|
573
|
-
if (result.tables.length === 0 && !result.error) {
|
|
574
|
-
const diag = makeDgmoError(
|
|
575
|
-
1,
|
|
576
|
-
'No tables found. Add table declarations like "users" or "orders blue".'
|
|
577
|
-
);
|
|
578
|
-
result.diagnostics.push(diag);
|
|
579
|
-
result.error = formatDgmoError(diag);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Assign palette colors to bare (colorless) tag values.
|
|
583
|
-
finalizeAutoTagColors(result.tagGroups as Writable<TagGroup>[], palette);
|
|
584
|
-
|
|
585
|
-
// Validate tag values on tables
|
|
586
|
-
if (result.tagGroups.length > 0) {
|
|
587
|
-
const tagEntities = result.tables.map((t) => ({
|
|
588
|
-
metadata: t.metadata,
|
|
589
|
-
lineNumber: t.lineNumber,
|
|
590
|
-
}));
|
|
591
|
-
validateTagValues(
|
|
592
|
-
tagEntities,
|
|
593
|
-
result.tagGroups,
|
|
594
|
-
(line, msg) =>
|
|
595
|
-
result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
|
|
596
|
-
suggest
|
|
597
|
-
);
|
|
598
|
-
validateTagGroupNames(result.tagGroups, (line, msg) =>
|
|
599
|
-
result.diagnostics.push(makeDgmoError(line, msg, 'warning'))
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
// Inject defaults for tables without explicit tags
|
|
603
|
-
for (const group of result.tagGroups) {
|
|
604
|
-
if (!group.defaultValue) continue;
|
|
605
|
-
const key = tagAttrKey(group.name);
|
|
606
|
-
for (const table of result.tables) {
|
|
607
|
-
if (!table.metadata[key]) {
|
|
608
|
-
const mutableMeta = tableMetadataMap.get(table.id);
|
|
609
|
-
if (mutableMeta) mutableMeta[key] = group.defaultValue;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Resolve note refs against table names (forward refs OK). The id→note
|
|
616
|
-
// binding is recomputed in layout; this pass surfaces diagnostics.
|
|
617
|
-
if (notes.length > 0) {
|
|
618
|
-
result.notes = notes;
|
|
619
|
-
resolveNotes(
|
|
620
|
-
notes,
|
|
621
|
-
result.tables.map((t) => ({ id: t.id, label: t.name })),
|
|
622
|
-
result.diagnostics
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Warn about isolated tables (not in any relationship)
|
|
627
|
-
if (
|
|
628
|
-
result.tables.length >= 2 &&
|
|
629
|
-
result.relationships.length >= 1 &&
|
|
630
|
-
!result.error
|
|
631
|
-
) {
|
|
632
|
-
const connectedIds = new Set<string>();
|
|
633
|
-
for (const rel of result.relationships) {
|
|
634
|
-
// rel.source and rel.target are already-normalized table ids
|
|
635
|
-
// eslint-disable-next-line name-normalize/required-at-insertion
|
|
636
|
-
connectedIds.add(rel.source);
|
|
637
|
-
// eslint-disable-next-line name-normalize/required-at-insertion
|
|
638
|
-
connectedIds.add(rel.target);
|
|
639
|
-
}
|
|
640
|
-
for (const table of result.tables) {
|
|
641
|
-
if (!connectedIds.has(table.id)) {
|
|
642
|
-
result.diagnostics.push(
|
|
643
|
-
makeDgmoError(
|
|
644
|
-
table.lineNumber,
|
|
645
|
-
`Table "${table.name}" is not connected to any other table`,
|
|
646
|
-
'warning'
|
|
647
|
-
)
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return result;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ============================================================
|
|
657
|
-
// Detection helper
|
|
658
|
-
// ============================================================
|
|
659
|
-
|
|
660
|
-
// Column detection for looksLikeERDiagram: space-separated with constraint keywords
|
|
661
|
-
const CONSTRAINT_KEYWORD_RE = /\b(pk|fk)\b/i;
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Detect if content looks like an ER diagram without explicit `er` first line.
|
|
665
|
-
* Looks for indented lines with pk or fk constraint keywords.
|
|
666
|
-
*/
|
|
667
|
-
export function looksLikeERDiagram(content: string): boolean {
|
|
668
|
-
const lines = content.split('\n');
|
|
669
|
-
|
|
670
|
-
let hasConstraint = false;
|
|
671
|
-
let hasTableDecl = false;
|
|
672
|
-
let hasRelationship = false;
|
|
673
|
-
|
|
674
|
-
for (const line of lines) {
|
|
675
|
-
const trimmed = line.trim();
|
|
676
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
677
|
-
|
|
678
|
-
// Skip metadata (both old colon and new space-separated)
|
|
679
|
-
if (/^(er|notation)\s/i.test(trimmed) || /^er$/i.test(trimmed)) continue;
|
|
680
|
-
|
|
681
|
-
const indent = measureIndent(line);
|
|
682
|
-
|
|
683
|
-
if (indent > 0) {
|
|
684
|
-
// Indented line with pk or fk is strong ER signal
|
|
685
|
-
if (CONSTRAINT_KEYWORD_RE.test(trimmed)) {
|
|
686
|
-
hasConstraint = true;
|
|
687
|
-
}
|
|
688
|
-
// Indented relationship is a strong ER signal
|
|
689
|
-
if (INDENT_REL_RE.test(trimmed)) {
|
|
690
|
-
hasRelationship = true;
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
// Check for table-like declaration
|
|
694
|
-
if (TABLE_DECL_RE.test(trimmed)) {
|
|
695
|
-
hasTableDecl = true;
|
|
696
|
-
}
|
|
697
|
-
// Check for relationship patterns
|
|
698
|
-
if (REL_SYMBOLIC_RE.test(trimmed)) {
|
|
699
|
-
hasRelationship = true;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// pk/fk constraint is a strong enough signal
|
|
705
|
-
if (hasConstraint && hasTableDecl) return true;
|
|
706
|
-
|
|
707
|
-
// Relationship with table declarations is sufficient
|
|
708
|
-
if (hasRelationship && hasTableDecl) return true;
|
|
709
|
-
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// ============================================================
|
|
714
|
-
// Symbol extraction (for completion API)
|
|
715
|
-
// ============================================================
|
|
716
|
-
|
|
717
|
-
import type { DiagramSymbols } from '../completion-types';
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Extract table names (entities) and ER keywords from document text.
|
|
721
|
-
* Used by the dgmo completion API for ghost hints and popup completions.
|
|
722
|
-
*/
|
|
723
|
-
export function extractSymbols(docText: string): DiagramSymbols {
|
|
724
|
-
const entities: string[] = [];
|
|
725
|
-
let inMetadata = true;
|
|
726
|
-
for (const rawLine of docText.split('\n')) {
|
|
727
|
-
const line = rawLine.trim();
|
|
728
|
-
if (inMetadata && /^er(\s|$)/i.test(line)) continue;
|
|
729
|
-
// Under §1.5 trailing-token, `Users blue` matches OPTION_NOCOLON_RE
|
|
730
|
-
// (key=Users, value=blue) but is actually a table with a color.
|
|
731
|
-
// Detect tables FIRST so they aren't swallowed by the option fallback.
|
|
732
|
-
if (/^\s/.test(rawLine)) continue; // indented = column definition, not table
|
|
733
|
-
if (line.length === 0) continue;
|
|
734
|
-
const m = TABLE_DECL_RE.exec(line);
|
|
735
|
-
if (m) {
|
|
736
|
-
const name = (m[1] ?? m[2] ?? '').trim();
|
|
737
|
-
if (name) {
|
|
738
|
-
inMetadata = false;
|
|
739
|
-
entities.push(name);
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (inMetadata && OPTION_NOCOLON_RE.test(line)) continue; // option line
|
|
744
|
-
inMetadata = false;
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
kind: 'er',
|
|
748
|
-
entities,
|
|
749
|
-
};
|
|
750
|
-
}
|