@diagrammo/dgmo 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/package.json +19 -70
- 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/completion.ts
DELETED
|
@@ -1,2668 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagram symbol extraction API + completion registry.
|
|
3
|
-
*
|
|
4
|
-
* Provides:
|
|
5
|
-
* - DiagramSymbols interface + extractDiagramSymbols() dispatch
|
|
6
|
-
* - COMPLETION_REGISTRY: chart-type → directives map (for editor autocomplete)
|
|
7
|
-
* - CHART_TYPES: array of { name, description } for chart type completion
|
|
8
|
-
* - METADATA_KEY_SET: derived set of all known directive keys
|
|
9
|
-
*
|
|
10
|
-
* Each diagram type registers its own extractor via registerExtractor().
|
|
11
|
-
* All built-in extractors are registered at module init below.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { extractSymbols as extractErSymbols } from './er/parser';
|
|
15
|
-
import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-parser';
|
|
16
|
-
import { extractSymbols as extractInfraSymbols } from './infra/parser';
|
|
17
|
-
import { extractSymbols as extractClassSymbols } from './class/parser';
|
|
18
|
-
import { extractPertSymbols } from './pert/parser';
|
|
19
|
-
import {
|
|
20
|
-
parseFirstLine,
|
|
21
|
-
ALL_CHART_TYPES,
|
|
22
|
-
measureIndent,
|
|
23
|
-
} from './utils/parsing';
|
|
24
|
-
import { RECOGNIZED_COLOR_NAMES } from './colors';
|
|
25
|
-
// Closed enum sets owned by their respective parsers — imported (never
|
|
26
|
-
// hand-copied) so completion can't drift from the grammar (one-oracle rule).
|
|
27
|
-
import { ALL_MARKERS } from './raci/variants';
|
|
28
|
-
import { STATE_KEYWORDS as WIREFRAME_STATE_KEYWORDS } from './wireframe/parser';
|
|
29
|
-
|
|
30
|
-
const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
|
|
31
|
-
RECOGNIZED_COLOR_NAMES
|
|
32
|
-
);
|
|
33
|
-
// Read chart-type descriptions directly from the source-of-truth data
|
|
34
|
-
// module instead of via dgmo-router.ts. dgmo-router imports every
|
|
35
|
-
// parser, and the parsers (Class/ER/Infra/Pert/Flowchart) type-only
|
|
36
|
-
// import DiagramSymbols back from this file — creating a hub of cycles
|
|
37
|
-
// through completion ↔ dgmo-router. Going through chart-types.ts (a
|
|
38
|
-
// leaf module with zero imports) breaks 7 of the 10 known cycles.
|
|
39
|
-
import { chartTypes } from './chart-types';
|
|
40
|
-
|
|
41
|
-
// ============================================================
|
|
42
|
-
// Symbol extraction
|
|
43
|
-
// ============================================================
|
|
44
|
-
|
|
45
|
-
// Types live in ./completion-types so the chart-type parsers can
|
|
46
|
-
// import them without taking a cycle through this file.
|
|
47
|
-
import type { ChartType, DiagramSymbols, ExtractFn } from './completion-types';
|
|
48
|
-
export type { ChartType, DiagramSymbols, ExtractFn };
|
|
49
|
-
|
|
50
|
-
const extractorRegistry = new Map<ChartType, ExtractFn>();
|
|
51
|
-
|
|
52
|
-
export function registerExtractor(kind: ChartType, fn: ExtractFn): void {
|
|
53
|
-
extractorRegistry.set(kind, fn);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract diagram symbols from document text.
|
|
58
|
-
* Returns null if the chart type is unknown or has no registered extractor.
|
|
59
|
-
*/
|
|
60
|
-
export function extractDiagramSymbols(docText: string): DiagramSymbols | null {
|
|
61
|
-
// Parse chartType from first line — bare type name.
|
|
62
|
-
let chartType: string | null = null;
|
|
63
|
-
for (const line of docText.split('\n')) {
|
|
64
|
-
const trimmed = line.trim();
|
|
65
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
66
|
-
const result = parseFirstLine(trimmed);
|
|
67
|
-
if (result) {
|
|
68
|
-
chartType = result.chartType;
|
|
69
|
-
}
|
|
70
|
-
break; // only check the first non-empty, non-comment line
|
|
71
|
-
}
|
|
72
|
-
if (!chartType) return null;
|
|
73
|
-
const fn = extractorRegistry.get(chartType);
|
|
74
|
-
if (!fn) return null;
|
|
75
|
-
const result = fn(docText);
|
|
76
|
-
// Populate `aliases` uniformly for every chart type so downstream
|
|
77
|
-
// editor surfaces don't need per-extractor branches.
|
|
78
|
-
const aliases = extractAliasDeclarations(docText);
|
|
79
|
-
return Object.keys(aliases).length > 0 ? { ...result, aliases } : result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ============================================================
|
|
83
|
-
// Completion registry
|
|
84
|
-
// ============================================================
|
|
85
|
-
|
|
86
|
-
/** Specification for a single directive: description + optional enumerated values. */
|
|
87
|
-
export interface DirectiveValueSpec {
|
|
88
|
-
description: string;
|
|
89
|
-
values?: string[];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Specification for a chart type's directives. */
|
|
93
|
-
export interface DirectiveSpec {
|
|
94
|
-
directives: Record<string, DirectiveValueSpec>;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Global directives applied to every chart type
|
|
98
|
-
const GLOBAL_DIRECTIVES: Record<string, DirectiveValueSpec> = {
|
|
99
|
-
palette: {
|
|
100
|
-
description: 'Color palette name',
|
|
101
|
-
values: [
|
|
102
|
-
'nord',
|
|
103
|
-
'catppuccin',
|
|
104
|
-
'tokyo-night',
|
|
105
|
-
'atlas',
|
|
106
|
-
'blueprint',
|
|
107
|
-
'slate',
|
|
108
|
-
'tidewater',
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
theme: {
|
|
112
|
-
description: 'Color theme',
|
|
113
|
-
values: ['light', 'dark', 'transparent'],
|
|
114
|
-
},
|
|
115
|
-
'no-title': {
|
|
116
|
-
description: 'Hide the diagram title',
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
function withGlobals(
|
|
121
|
-
directives: Record<string, DirectiveValueSpec> = {}
|
|
122
|
-
): DirectiveSpec {
|
|
123
|
-
return { directives: { ...GLOBAL_DIRECTIVES, ...directives } };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Chart-type → directive specifications. Every chart type has at least palette + theme. */
|
|
127
|
-
export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
128
|
-
// ── Data charts ──────────────────────────────────────────
|
|
129
|
-
[
|
|
130
|
-
'bar',
|
|
131
|
-
withGlobals({
|
|
132
|
-
stack: {
|
|
133
|
-
description:
|
|
134
|
-
'Multi-series block header → stacked bars (one bar/category)',
|
|
135
|
-
},
|
|
136
|
-
group: {
|
|
137
|
-
description:
|
|
138
|
-
'Multi-series block header → clustered (side-by-side) bars',
|
|
139
|
-
},
|
|
140
|
-
'x-label': { description: 'X-axis label' },
|
|
141
|
-
'y-label': { description: 'Y-axis label' },
|
|
142
|
-
'orientation-horizontal': { description: 'Switch to horizontal bars' },
|
|
143
|
-
'no-value': { description: 'Hide value labels atop each bar' },
|
|
144
|
-
color: { description: 'Bar color override' },
|
|
145
|
-
}),
|
|
146
|
-
],
|
|
147
|
-
[
|
|
148
|
-
'line',
|
|
149
|
-
withGlobals({
|
|
150
|
-
series: { description: 'Series name(s)' },
|
|
151
|
-
fill: { description: 'Fill under the line (area chart)' },
|
|
152
|
-
'x-label': { description: 'X-axis label' },
|
|
153
|
-
'y-label': { description: 'Y-axis label (left axis)' },
|
|
154
|
-
'y-right-label': {
|
|
155
|
-
description: 'Right y-axis label (dual-axis; group series beneath it)',
|
|
156
|
-
},
|
|
157
|
-
'no-value': { description: 'Hide value labels at each point' },
|
|
158
|
-
}),
|
|
159
|
-
],
|
|
160
|
-
[
|
|
161
|
-
'pie',
|
|
162
|
-
withGlobals({
|
|
163
|
-
hole: {
|
|
164
|
-
description:
|
|
165
|
-
'Doughnut ring (optional ratio 0–0.9); shows the total in center',
|
|
166
|
-
},
|
|
167
|
-
'no-center-total': {
|
|
168
|
-
description: 'Hide the center total on a doughnut/hole pie',
|
|
169
|
-
},
|
|
170
|
-
'no-name': { description: 'Hide name from segment labels' },
|
|
171
|
-
'no-value': { description: 'Hide value from segment labels' },
|
|
172
|
-
'no-percent': { description: 'Hide percent from segment labels' },
|
|
173
|
-
}),
|
|
174
|
-
],
|
|
175
|
-
[
|
|
176
|
-
'polar-area',
|
|
177
|
-
withGlobals({
|
|
178
|
-
'no-name': { description: 'Hide name from segment labels' },
|
|
179
|
-
'no-value': { description: 'Hide value from segment labels' },
|
|
180
|
-
'no-percent': { description: 'Hide percent from segment labels' },
|
|
181
|
-
}),
|
|
182
|
-
],
|
|
183
|
-
[
|
|
184
|
-
'radar',
|
|
185
|
-
withGlobals({
|
|
186
|
-
'no-value': { description: 'Hide value labels at each vertex' },
|
|
187
|
-
}),
|
|
188
|
-
],
|
|
189
|
-
|
|
190
|
-
// ── Extended charts ──────────────────────────────────────
|
|
191
|
-
[
|
|
192
|
-
'scatter',
|
|
193
|
-
withGlobals({
|
|
194
|
-
'no-name': { description: 'Hide point labels' },
|
|
195
|
-
'x-label': { description: 'X-axis label' },
|
|
196
|
-
'y-label': { description: 'Y-axis label' },
|
|
197
|
-
'size-label': { description: 'Size axis label' },
|
|
198
|
-
}),
|
|
199
|
-
],
|
|
200
|
-
[
|
|
201
|
-
'heatmap',
|
|
202
|
-
withGlobals({
|
|
203
|
-
columns: { description: 'Column labels (required)' },
|
|
204
|
-
'no-value': { description: 'Hide cell value text' },
|
|
205
|
-
}),
|
|
206
|
-
],
|
|
207
|
-
['sankey', withGlobals()],
|
|
208
|
-
[
|
|
209
|
-
'funnel',
|
|
210
|
-
withGlobals({
|
|
211
|
-
'no-name': { description: 'Hide left-side name labels' },
|
|
212
|
-
'no-value': { description: 'Hide right-side value labels' },
|
|
213
|
-
}),
|
|
214
|
-
],
|
|
215
|
-
[
|
|
216
|
-
'function',
|
|
217
|
-
withGlobals({
|
|
218
|
-
x: { description: 'X-axis range (start to end)' },
|
|
219
|
-
'x-label': { description: 'X-axis label' },
|
|
220
|
-
'y-label': { description: 'Y-axis label' },
|
|
221
|
-
shade: { description: 'Fill area below curves with translucent color' },
|
|
222
|
-
}),
|
|
223
|
-
],
|
|
224
|
-
|
|
225
|
-
// ── Visualizations ───────────────────────────────────────
|
|
226
|
-
['slope', withGlobals()],
|
|
227
|
-
[
|
|
228
|
-
'wordcloud',
|
|
229
|
-
withGlobals({
|
|
230
|
-
rotate: {
|
|
231
|
-
description: 'Word rotation',
|
|
232
|
-
values: ['none', 'mixed', 'angled'],
|
|
233
|
-
},
|
|
234
|
-
max: { description: 'Maximum word count' },
|
|
235
|
-
size: { description: 'Font size range (min, max)' },
|
|
236
|
-
}),
|
|
237
|
-
],
|
|
238
|
-
[
|
|
239
|
-
'arc',
|
|
240
|
-
withGlobals({
|
|
241
|
-
order: {
|
|
242
|
-
description: 'Node ordering',
|
|
243
|
-
values: ['appearance', 'name', 'group', 'degree'],
|
|
244
|
-
},
|
|
245
|
-
}),
|
|
246
|
-
],
|
|
247
|
-
[
|
|
248
|
-
'timeline',
|
|
249
|
-
withGlobals({
|
|
250
|
-
'active-tag': { description: 'Active tag group name' },
|
|
251
|
-
}),
|
|
252
|
-
],
|
|
253
|
-
[
|
|
254
|
-
'event-line',
|
|
255
|
-
withGlobals({
|
|
256
|
-
'no-scale': { description: 'Space events evenly instead of by date' },
|
|
257
|
-
side: {
|
|
258
|
-
description: 'Card placement: side above | below (default alternate)',
|
|
259
|
-
},
|
|
260
|
-
'no-box': {
|
|
261
|
-
description: 'Card-less label/rule/description style (slides)',
|
|
262
|
-
},
|
|
263
|
-
'no-legend': { description: 'Hide the tag legend' },
|
|
264
|
-
TBD: {
|
|
265
|
-
description:
|
|
266
|
-
'Date prefix for a not-yet-scheduled future event (e.g. "TBD Console Port")',
|
|
267
|
-
},
|
|
268
|
-
'active-tag': { description: 'Active tag group name' },
|
|
269
|
-
}),
|
|
270
|
-
],
|
|
271
|
-
['venn', withGlobals()],
|
|
272
|
-
[
|
|
273
|
-
'quadrant',
|
|
274
|
-
withGlobals({
|
|
275
|
-
'x-label': { description: 'X-axis labels (low, high)' },
|
|
276
|
-
'y-label': { description: 'Y-axis labels (low, high)' },
|
|
277
|
-
}),
|
|
278
|
-
],
|
|
279
|
-
|
|
280
|
-
// ── Diagrams ─────────────────────────────────────────────
|
|
281
|
-
[
|
|
282
|
-
'sequence',
|
|
283
|
-
withGlobals({
|
|
284
|
-
activations: {
|
|
285
|
-
description: 'Show activation bars',
|
|
286
|
-
values: ['on', 'off'],
|
|
287
|
-
},
|
|
288
|
-
'active-tag': { description: 'Active tag group name' },
|
|
289
|
-
}),
|
|
290
|
-
],
|
|
291
|
-
[
|
|
292
|
-
'flowchart',
|
|
293
|
-
// Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill, no-notes
|
|
294
|
-
withGlobals({
|
|
295
|
-
'direction-lr': { description: 'Switch to left-to-right layout' },
|
|
296
|
-
'orientation-vertical': {
|
|
297
|
-
description: 'Use vertical orientation for ranks',
|
|
298
|
-
},
|
|
299
|
-
'no-notes': { description: 'Suppress all node note boxes' },
|
|
300
|
-
}),
|
|
301
|
-
],
|
|
302
|
-
['class', withGlobals({})],
|
|
303
|
-
[
|
|
304
|
-
'er',
|
|
305
|
-
// Spec §9 §8.5: notation (chen/crow), active-tag.
|
|
306
|
-
withGlobals({
|
|
307
|
-
notation: {
|
|
308
|
-
description: 'ER notation style',
|
|
309
|
-
values: ['chen', 'crow'],
|
|
310
|
-
},
|
|
311
|
-
'active-tag': { description: 'Active tag group name' },
|
|
312
|
-
}),
|
|
313
|
-
],
|
|
314
|
-
[
|
|
315
|
-
'org',
|
|
316
|
-
// Spec §7 §6.5: direction-tb, sub-node-label, show-sub-node-count,
|
|
317
|
-
// hide, active-tag. solid-fill via SOLID_FILL_CAPABLE.
|
|
318
|
-
withGlobals({
|
|
319
|
-
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
320
|
-
'sub-node-label': { description: 'Label for sub-nodes' },
|
|
321
|
-
'show-sub-node-count': { description: 'Show sub-node counts' },
|
|
322
|
-
hide: { description: 'Hide tag:value pairs' },
|
|
323
|
-
'active-tag': { description: 'Active tag group name' },
|
|
324
|
-
}),
|
|
325
|
-
],
|
|
326
|
-
[
|
|
327
|
-
'kanban',
|
|
328
|
-
// Spec §11 §10.4: hide, active-tag.
|
|
329
|
-
withGlobals({
|
|
330
|
-
hide: { description: 'Hide tag:value pairs' },
|
|
331
|
-
'active-tag': { description: 'Active tag group name' },
|
|
332
|
-
}),
|
|
333
|
-
],
|
|
334
|
-
// RACI / RASCI / DACI — one chart type (`raci`); the variant (RACI, RASCI,
|
|
335
|
-
// or DACI) is inferred from the markers used (D → DACI, S → RASCI).
|
|
336
|
-
[
|
|
337
|
-
'raci',
|
|
338
|
-
withGlobals({
|
|
339
|
-
roles: {
|
|
340
|
-
description:
|
|
341
|
-
'Declare role column order (inline `roles A, B, C` or indented block with per-role pipe metadata)',
|
|
342
|
-
},
|
|
343
|
-
'active-tag': { description: 'Active tag group name' },
|
|
344
|
-
}),
|
|
345
|
-
],
|
|
346
|
-
[
|
|
347
|
-
'c4',
|
|
348
|
-
// Spec §8 §7.7: direction-tb, active-tag.
|
|
349
|
-
withGlobals({
|
|
350
|
-
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
351
|
-
'active-tag': { description: 'Active tag group name' },
|
|
352
|
-
}),
|
|
353
|
-
],
|
|
354
|
-
[
|
|
355
|
-
'state',
|
|
356
|
-
// Spec §6 §5.6: direction-tb, solid-fill, no-notes.
|
|
357
|
-
withGlobals({
|
|
358
|
-
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
359
|
-
'no-notes': { description: 'Suppress all state note boxes' },
|
|
360
|
-
}),
|
|
361
|
-
],
|
|
362
|
-
[
|
|
363
|
-
'sitemap',
|
|
364
|
-
withGlobals({
|
|
365
|
-
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
366
|
-
'active-tag': { description: 'Active tag group name' },
|
|
367
|
-
}),
|
|
368
|
-
],
|
|
369
|
-
[
|
|
370
|
-
'infra',
|
|
371
|
-
withGlobals({
|
|
372
|
-
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
373
|
-
animate: { description: 'Enable traffic animation' },
|
|
374
|
-
'default-latency-ms': { description: 'Default latency for all nodes' },
|
|
375
|
-
'default-uptime': { description: 'Default uptime for all nodes' },
|
|
376
|
-
'default-rps': { description: 'Default RPS capacity for all nodes' },
|
|
377
|
-
'slo-availability': { description: 'SLO availability target (0-1)' },
|
|
378
|
-
'slo-p90-latency-ms': { description: 'SLO p90 latency target in ms' },
|
|
379
|
-
'slo-warning-margin': { description: 'SLO warning margin percentage' },
|
|
380
|
-
'active-tag': { description: 'Active tag group name' },
|
|
381
|
-
}),
|
|
382
|
-
],
|
|
383
|
-
[
|
|
384
|
-
'pert',
|
|
385
|
-
withGlobals({
|
|
386
|
-
'time-unit': {
|
|
387
|
-
description: 'Time unit for activity durations',
|
|
388
|
-
values: ['min', 'h', 'd', 'bd', 'w', 'm', 'q', 'y'],
|
|
389
|
-
},
|
|
390
|
-
confidence: {
|
|
391
|
-
description: 'Confidence factor for M-only durations',
|
|
392
|
-
values: ['high', 'medium', 'low'],
|
|
393
|
-
},
|
|
394
|
-
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
395
|
-
'node-detail': {
|
|
396
|
-
description: 'Node visual density',
|
|
397
|
-
values: ['compact', 'full'],
|
|
398
|
-
},
|
|
399
|
-
trials: {
|
|
400
|
-
description:
|
|
401
|
-
'Monte Carlo trial count (auto-derived from activity count)',
|
|
402
|
-
},
|
|
403
|
-
seed: { description: 'Monte Carlo PRNG seed (auto-derived from title)' },
|
|
404
|
-
'scrubber-trials': {
|
|
405
|
-
description: 'Fast-MC trials for the duration scrubber (default 300)',
|
|
406
|
-
},
|
|
407
|
-
'start-date': { description: 'Project start date (YYYY-MM-DD or now)' },
|
|
408
|
-
'end-date': { description: 'Project end date (YYYY-MM-DD)' },
|
|
409
|
-
'sprint-length': { description: 'Sprint duration (e.g. 2w)' },
|
|
410
|
-
'sprint-number': { description: 'Starting sprint number' },
|
|
411
|
-
'sprint-start': { description: 'Sprint start date (YYYY-MM-DD)' },
|
|
412
|
-
'active-tag': { description: 'Active tag group name' },
|
|
413
|
-
}),
|
|
414
|
-
],
|
|
415
|
-
[
|
|
416
|
-
'gantt',
|
|
417
|
-
// Spec §13 §12.2 Options.
|
|
418
|
-
withGlobals({
|
|
419
|
-
start: { description: 'Project start date (YYYY-MM-DD)' },
|
|
420
|
-
'today-marker': {
|
|
421
|
-
description: 'Today marker (bare = on, or YYYY-MM-DD date)',
|
|
422
|
-
},
|
|
423
|
-
sort: { description: 'Sort order', values: ['time', 'group', 'tag'] },
|
|
424
|
-
'critical-path': { description: 'Show critical path' },
|
|
425
|
-
'no-dependencies': { description: 'Hide dependency arrows' },
|
|
426
|
-
'sprint-length': { description: 'Sprint duration (e.g. 2w)' },
|
|
427
|
-
'sprint-number': { description: 'Starting sprint number' },
|
|
428
|
-
'sprint-start': { description: 'Sprint start date (YYYY-MM-DD)' },
|
|
429
|
-
'active-tag': { description: 'Active tag group name' },
|
|
430
|
-
// Legacy positive form `dependencies` — kept for back-compat. Use
|
|
431
|
-
// `no-dependencies` to suppress dependency arrows in new code.
|
|
432
|
-
dependencies: { description: 'Show dependencies (legacy form)' },
|
|
433
|
-
}),
|
|
434
|
-
],
|
|
435
|
-
[
|
|
436
|
-
'boxes-and-lines',
|
|
437
|
-
withGlobals({
|
|
438
|
-
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
439
|
-
'active-tag': { description: 'Active tag group name' },
|
|
440
|
-
hide: { description: 'Hide tag:value pairs' },
|
|
441
|
-
heat: {
|
|
442
|
-
description:
|
|
443
|
-
'Label for the value→colour ramp, with an optional trailing [low] [high] color pair (pairs with the `heat:` key)',
|
|
444
|
-
},
|
|
445
|
-
'show-values': { description: 'Print box values as text' },
|
|
446
|
-
}),
|
|
447
|
-
],
|
|
448
|
-
[
|
|
449
|
-
'swimlane',
|
|
450
|
-
withGlobals({
|
|
451
|
-
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
452
|
-
lane: { description: 'Declare a lane (row) with an optional color' },
|
|
453
|
-
'active-tag': { description: 'Active tag group name' },
|
|
454
|
-
}),
|
|
455
|
-
],
|
|
456
|
-
[
|
|
457
|
-
'version-control',
|
|
458
|
-
withGlobals({
|
|
459
|
-
direction: {
|
|
460
|
-
description: 'Layout direction',
|
|
461
|
-
values: ['LR', 'TB', 'BT'],
|
|
462
|
-
},
|
|
463
|
-
merge: { description: 'Merge a branch into the active branch' },
|
|
464
|
-
'cherry-pick': { description: 'Copy a commit onto the active branch' },
|
|
465
|
-
rebase: { description: 'Replay a branch onto another (rebase X onto Y)' },
|
|
466
|
-
reset: { description: 'Move a branch pointer back (reset X to commit)' },
|
|
467
|
-
revert: { description: 'Add an inverse commit undoing a commit' },
|
|
468
|
-
ref: {
|
|
469
|
-
description:
|
|
470
|
-
'Pointer at a commit (ref origin/main at commit) — remotes / HEAD',
|
|
471
|
-
},
|
|
472
|
-
note: { description: 'Numbered step annotation on the current commit' },
|
|
473
|
-
'no-labels': { description: 'Hide commit messages' },
|
|
474
|
-
'no-lanes': { description: 'Hide branch lanes' },
|
|
475
|
-
'no-head': { description: 'Hide the HEAD marker' },
|
|
476
|
-
}),
|
|
477
|
-
],
|
|
478
|
-
[
|
|
479
|
-
'mindmap',
|
|
480
|
-
withGlobals({
|
|
481
|
-
'active-tag': { description: 'Active tag group name' },
|
|
482
|
-
}),
|
|
483
|
-
],
|
|
484
|
-
[
|
|
485
|
-
'wireframe',
|
|
486
|
-
withGlobals({
|
|
487
|
-
mobile: { description: 'Use mobile (narrow vertical) layout' },
|
|
488
|
-
'active-tag': { description: 'Active tag group name' },
|
|
489
|
-
}),
|
|
490
|
-
],
|
|
491
|
-
[
|
|
492
|
-
'tech-radar',
|
|
493
|
-
// Spec §20 documents one directive: `show-blip-legend`. `rings` is a
|
|
494
|
-
// structural block keyword; quadrant/ring/trend/color are pipe metadata
|
|
495
|
-
// that live in PIPE_METADATA.
|
|
496
|
-
withGlobals({
|
|
497
|
-
'show-blip-legend': {
|
|
498
|
-
description: 'Render the four-column blip listing alongside the radar',
|
|
499
|
-
},
|
|
500
|
-
}),
|
|
501
|
-
],
|
|
502
|
-
[
|
|
503
|
-
'cycle',
|
|
504
|
-
withGlobals({
|
|
505
|
-
'direction-counterclockwise': {
|
|
506
|
-
description: 'Reverse cycle direction to counterclockwise',
|
|
507
|
-
},
|
|
508
|
-
'circle-nodes': {
|
|
509
|
-
description: 'Render nodes as circles instead of rectangles',
|
|
510
|
-
},
|
|
511
|
-
}),
|
|
512
|
-
],
|
|
513
|
-
[
|
|
514
|
-
'journey-map',
|
|
515
|
-
// Spec §22 directives: `active-tag`. `persona` is a
|
|
516
|
-
// structural keyword (like `tag` / `roles`), not a directive.
|
|
517
|
-
// `solid-fill` is added via SOLID_FILL_CAPABLE below.
|
|
518
|
-
withGlobals({
|
|
519
|
-
'active-tag': { description: 'Active tag group name' },
|
|
520
|
-
}),
|
|
521
|
-
],
|
|
522
|
-
[
|
|
523
|
-
'pyramid',
|
|
524
|
-
// Spec §23.5 documents `inverted`; `solid-fill` is added via
|
|
525
|
-
// SOLID_FILL_CAPABLE below (working but not yet in spec §23.5).
|
|
526
|
-
// `color`/`description` are layer pipe-metadata, not directives.
|
|
527
|
-
withGlobals({
|
|
528
|
-
inverted: { description: 'Flip apex to the bottom (funnel orientation)' },
|
|
529
|
-
}),
|
|
530
|
-
],
|
|
531
|
-
[
|
|
532
|
-
'ring',
|
|
533
|
-
// Per spec §24.5 the only chart-specific directive is `solid-fill`,
|
|
534
|
-
// applied via SOLID_FILL_CAPABLE below. `color`/`description` are
|
|
535
|
-
// layer pipe-metadata, not directives — they live in PIPE_METADATA.
|
|
536
|
-
withGlobals({}),
|
|
537
|
-
],
|
|
538
|
-
[
|
|
539
|
-
'treemap',
|
|
540
|
-
// Hierarchy sized by a bare trailing number; color modes + opt-outs.
|
|
541
|
-
withGlobals({
|
|
542
|
-
heat: {
|
|
543
|
-
description:
|
|
544
|
-
'Name the color-by-value ramp (`heat <Label> [low] [high]`); pairs with the per-node `heat:` key',
|
|
545
|
-
},
|
|
546
|
-
depth: {
|
|
547
|
-
description:
|
|
548
|
-
'Render N levels; deeper subtrees collapse to a drillable block',
|
|
549
|
-
},
|
|
550
|
-
'no-values': { description: 'Hide value labels' },
|
|
551
|
-
'no-percent': { description: 'Hide percentage labels' },
|
|
552
|
-
'no-headers': { description: 'Hide parent header bars' },
|
|
553
|
-
'no-legend': { description: 'Hide the legend' },
|
|
554
|
-
radial: {
|
|
555
|
-
description:
|
|
556
|
-
'Render as a sunburst / hierarchical pie (concentric rings)',
|
|
557
|
-
},
|
|
558
|
-
}),
|
|
559
|
-
],
|
|
560
|
-
[
|
|
561
|
-
'block',
|
|
562
|
-
// Author-controlled grid; columns are inferred from placement, so the only
|
|
563
|
-
// directives are an explicit `columns` override and `no-legend`.
|
|
564
|
-
withGlobals({
|
|
565
|
-
columns: {
|
|
566
|
-
description:
|
|
567
|
-
'Grid width (columns); inferred from the widest row if omitted',
|
|
568
|
-
},
|
|
569
|
-
'no-legend': { description: 'Hide the tag legend' },
|
|
570
|
-
}),
|
|
571
|
-
],
|
|
572
|
-
[
|
|
573
|
-
'map',
|
|
574
|
-
// Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
|
|
575
|
-
// only switches are bare `no-*` opt-outs, surfaced proactively so a
|
|
576
|
-
// zero-config map still hints at what can be turned off. `poi`/`route` are
|
|
577
|
-
// content keywords, not directives; metadata keys (heat/size/width/label/style)
|
|
578
|
-
// live in the reserved-key registry.
|
|
579
|
-
withGlobals({
|
|
580
|
-
'region-heat': {
|
|
581
|
-
description:
|
|
582
|
-
'Label for the region value→colour ramp, with an optional trailing [low] [high] color pair (pairs with the `heat:` key)',
|
|
583
|
-
},
|
|
584
|
-
'poi-size': {
|
|
585
|
-
description:
|
|
586
|
-
'Label for the POI value→marker-size channel (pairs with the `size:` key)',
|
|
587
|
-
},
|
|
588
|
-
'flow-width': {
|
|
589
|
-
description:
|
|
590
|
-
'Label for the edge/leg value→line-thickness channel (pairs with the `width:` key)',
|
|
591
|
-
},
|
|
592
|
-
locale: {
|
|
593
|
-
description:
|
|
594
|
-
'Default country/state for bare place names, e.g. locale US-GA',
|
|
595
|
-
},
|
|
596
|
-
'active-tag': {
|
|
597
|
-
description: 'Which tag group leads when several are present',
|
|
598
|
-
},
|
|
599
|
-
caption: { description: 'Caption line (data-source attribution)' },
|
|
600
|
-
'no-title': { description: 'Suppress the title banner' },
|
|
601
|
-
'no-legend': { description: 'Suppress the legend' },
|
|
602
|
-
'no-coastline': {
|
|
603
|
-
description: 'Turn off coastal water-lines (on by default)',
|
|
604
|
-
},
|
|
605
|
-
'no-relief': {
|
|
606
|
-
description: 'Turn off mountain-range relief shading (on by default)',
|
|
607
|
-
},
|
|
608
|
-
'no-context-labels': {
|
|
609
|
-
description: 'Turn off orientation labels for water + nearby countries',
|
|
610
|
-
},
|
|
611
|
-
'no-region-labels': {
|
|
612
|
-
description: 'Turn off subdivision name labels (on by default)',
|
|
613
|
-
},
|
|
614
|
-
'no-region-heat-value': {
|
|
615
|
-
description:
|
|
616
|
-
'Turn off the heat value shown under each region (on by default)',
|
|
617
|
-
},
|
|
618
|
-
'no-poi-labels': { description: 'Turn off POI labels (on by default)' },
|
|
619
|
-
'no-colorize': {
|
|
620
|
-
description:
|
|
621
|
-
'Force plain green-land reference dress (regions are auto-coloured by default)',
|
|
622
|
-
},
|
|
623
|
-
'no-cities': {
|
|
624
|
-
description:
|
|
625
|
-
'Turn off the subtle city dots scattered across the basemap (on by default)',
|
|
626
|
-
},
|
|
627
|
-
'no-cluster-pois': {
|
|
628
|
-
description:
|
|
629
|
-
'Always fan out coincident POI markers instead of collapsing them into a count badge',
|
|
630
|
-
},
|
|
631
|
-
}),
|
|
632
|
-
],
|
|
633
|
-
]);
|
|
634
|
-
|
|
635
|
-
// ── Cross-chart-type bare-keyword option: `solid-fill` ──────────
|
|
636
|
-
// Adds the directive to every chart type whose renderer actually responds to
|
|
637
|
-
// it (renders shapes at full intent saturation when set). Chart types where
|
|
638
|
-
// the keyword is a no-op (gantt/infra/tech-radar/event-line deliberately opt
|
|
639
|
-
// out; quadrant/scatter-dot/wordcloud/line/arc/slope have no tinted shape
|
|
640
|
-
// fill) intentionally don't list it — keeps the completion popup honest.
|
|
641
|
-
const SOLID_FILL_CAPABLE = new Set([
|
|
642
|
-
'flowchart',
|
|
643
|
-
'state',
|
|
644
|
-
'sequence',
|
|
645
|
-
'c4',
|
|
646
|
-
'org',
|
|
647
|
-
'kanban',
|
|
648
|
-
'journey-map',
|
|
649
|
-
'mindmap',
|
|
650
|
-
'cycle',
|
|
651
|
-
'pyramid',
|
|
652
|
-
'ring',
|
|
653
|
-
'funnel',
|
|
654
|
-
'class',
|
|
655
|
-
'er',
|
|
656
|
-
'sitemap',
|
|
657
|
-
'boxes-and-lines',
|
|
658
|
-
'wireframe',
|
|
659
|
-
'bar',
|
|
660
|
-
'pie',
|
|
661
|
-
'polar-area',
|
|
662
|
-
'radar',
|
|
663
|
-
'scatter',
|
|
664
|
-
'pert',
|
|
665
|
-
'block',
|
|
666
|
-
'swimlane',
|
|
667
|
-
'treemap',
|
|
668
|
-
'heatmap',
|
|
669
|
-
'venn',
|
|
670
|
-
'timeline',
|
|
671
|
-
]);
|
|
672
|
-
for (const [type, spec] of COMPLETION_REGISTRY) {
|
|
673
|
-
if (SOLID_FILL_CAPABLE.has(type)) {
|
|
674
|
-
spec.directives['solid-fill'] = {
|
|
675
|
-
description:
|
|
676
|
-
'Render shapes with full intent color instead of the default 25% tint',
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ============================================================
|
|
682
|
-
// Chart types array (for chart type completion popup)
|
|
683
|
-
// ============================================================
|
|
684
|
-
|
|
685
|
-
/** All chart types with descriptions, for chart type autocomplete. */
|
|
686
|
-
const CHART_TYPE_DESCRIPTIONS: Record<string, string> = Object.fromEntries(
|
|
687
|
-
chartTypes.map((c) => [c.id, c.description])
|
|
688
|
-
);
|
|
689
|
-
|
|
690
|
-
export const CHART_TYPES: ReadonlyArray<{ name: string; description: string }> =
|
|
691
|
-
[...ALL_CHART_TYPES].map((name) => ({
|
|
692
|
-
name,
|
|
693
|
-
description: CHART_TYPE_DESCRIPTIONS[name] ?? name,
|
|
694
|
-
}));
|
|
695
|
-
|
|
696
|
-
// ============================================================
|
|
697
|
-
// Entity types for `is a` declarations
|
|
698
|
-
// ============================================================
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Entity types for `Name is a <type>` declarations, keyed by chart type.
|
|
702
|
-
* Values are sourced from parser constants (VALID_PARTICIPANT_TYPES,
|
|
703
|
-
* C4_IS_A_RE).
|
|
704
|
-
*/
|
|
705
|
-
export const ENTITY_TYPES = new Map<string, string[]>([
|
|
706
|
-
['sequence', ['actor', 'database', 'queue', 'cache']],
|
|
707
|
-
[
|
|
708
|
-
'c4',
|
|
709
|
-
['person', 'system', 'container', 'component', 'external', 'database'],
|
|
710
|
-
],
|
|
711
|
-
]);
|
|
712
|
-
|
|
713
|
-
// ============================================================
|
|
714
|
-
// Structural keywords for line-leading completion
|
|
715
|
-
// ============================================================
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Chart-type-specific structural keywords offered on an empty/start-of-line in
|
|
719
|
-
* the data zone (block openers like `loop`, section headers like `containers`,
|
|
720
|
-
* the `tag` block declaration, etc.). This is the single source of truth for
|
|
721
|
-
* the editor's structural-keyword popup — every entry MUST be a token the
|
|
722
|
-
* corresponding parser actually recognizes (validated by the
|
|
723
|
-
* completion-conformance suite). Do NOT add removed/diagnostic-only tokens
|
|
724
|
-
* (e.g. cycle's `no-descriptions`) or tokens the parser ignores.
|
|
725
|
-
*
|
|
726
|
-
* Chart types not listed here have no structural keywords (most data charts).
|
|
727
|
-
*/
|
|
728
|
-
export const STRUCTURAL_KEYWORDS = new Map<string, string[]>([
|
|
729
|
-
['sequence', ['if', 'else', 'loop', 'parallel', 'note', 'tag']],
|
|
730
|
-
['gantt', ['era', 'marker', 'holiday', 'workweek', 'parallel', 'tag']],
|
|
731
|
-
['c4', ['containers', 'components', 'deployment', 'tag']],
|
|
732
|
-
['timeline', ['era', 'marker', 'tag']],
|
|
733
|
-
['org', ['tag']],
|
|
734
|
-
['kanban', ['tag']],
|
|
735
|
-
['sitemap', ['tag']],
|
|
736
|
-
['infra', ['tag']],
|
|
737
|
-
['pert', ['tag']],
|
|
738
|
-
['mindmap', ['tag']],
|
|
739
|
-
['treemap', ['tag']],
|
|
740
|
-
['block', ['tag']],
|
|
741
|
-
['boxes-and-lines', ['tag']],
|
|
742
|
-
['swimlane', ['lane', 'tag']],
|
|
743
|
-
[
|
|
744
|
-
'version-control',
|
|
745
|
-
['merge', 'cherry-pick', 'rebase', 'reset', 'revert', 'ref', 'note'],
|
|
746
|
-
],
|
|
747
|
-
['er', ['tag']],
|
|
748
|
-
['cycle', ['direction-counterclockwise', 'circle-nodes']],
|
|
749
|
-
['journey-map', ['persona', 'tag']],
|
|
750
|
-
['raci', ['roles']],
|
|
751
|
-
['tech-radar', ['rings']],
|
|
752
|
-
[
|
|
753
|
-
'wireframe',
|
|
754
|
-
[
|
|
755
|
-
'nav',
|
|
756
|
-
'tabs',
|
|
757
|
-
'table',
|
|
758
|
-
'image',
|
|
759
|
-
'modal',
|
|
760
|
-
'skeleton',
|
|
761
|
-
'alert',
|
|
762
|
-
'progress',
|
|
763
|
-
'chart',
|
|
764
|
-
'mobile',
|
|
765
|
-
'tag',
|
|
766
|
-
],
|
|
767
|
-
],
|
|
768
|
-
['class', ['abstract', 'interface', 'enum', 'extends', 'implements']],
|
|
769
|
-
]);
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Chart types that support `tag` block declarations (and thus the
|
|
773
|
-
* `alias`/`default` sub-keywords inside a tag block). Derived from
|
|
774
|
-
* STRUCTURAL_KEYWORDS so the two can never drift — a chart supports tag blocks
|
|
775
|
-
* iff it offers the `tag` keyword.
|
|
776
|
-
*/
|
|
777
|
-
export const TAG_SUPPORTING_TYPES: ReadonlySet<string> = new Set(
|
|
778
|
-
[...STRUCTURAL_KEYWORDS]
|
|
779
|
-
.filter(([, kws]) => kws.includes('tag'))
|
|
780
|
-
.map(([type]) => type)
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
// ============================================================
|
|
784
|
-
// Reference grammar descriptor (slot 5 — entity reference)
|
|
785
|
-
// ============================================================
|
|
786
|
-
|
|
787
|
-
/** Whether a chart type has a reference position (arrow/operator → a prior
|
|
788
|
-
* declaration) and which operators open it. The single oracle that drives
|
|
789
|
-
* the library extractor audit, the app's entity-trigger map, and the
|
|
790
|
-
* conformance drift-guard — so the three can never disagree. v1 models
|
|
791
|
-
* reference-after-operator only (NOT metadata-target references). */
|
|
792
|
-
export interface ReferenceGrammar {
|
|
793
|
-
hasReferenceGrammar: boolean;
|
|
794
|
-
/** Literal operator tokens that introduce a reference (e.g. `->`, `<->`,
|
|
795
|
-
* `~>`, `+`). The app derives its trigger regex from these (allowing the
|
|
796
|
-
* labeled `-label->` / `~label~>` variants where applicable). */
|
|
797
|
-
referenceOperators: string[];
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Per-type reference grammar. Grounded in each parser's arrow/operator
|
|
802
|
-
* grammar — NOT hand-typed blind. Types with no reference position (org,
|
|
803
|
-
* kanban, mindmap, all data charts, the radial/visualization types) declare
|
|
804
|
-
* `hasReferenceGrammar: false`. `map` references (`route A ~> B`) are owned by
|
|
805
|
-
* the app's bespoke geo-completion path, so it stays `false` here to avoid
|
|
806
|
-
* double-handling.
|
|
807
|
-
*/
|
|
808
|
-
export const REFERENCE_GRAMMAR = new Map<string, ReferenceGrammar>([
|
|
809
|
-
// Diagrams with a genuine reference position
|
|
810
|
-
['sequence', { hasReferenceGrammar: true, referenceOperators: ['->', '~>'] }],
|
|
811
|
-
['flowchart', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
812
|
-
['state', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
813
|
-
['sitemap', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
814
|
-
[
|
|
815
|
-
'c4',
|
|
816
|
-
{
|
|
817
|
-
hasReferenceGrammar: true,
|
|
818
|
-
referenceOperators: ['->', '<->', '~>', '<~>'],
|
|
819
|
-
},
|
|
820
|
-
],
|
|
821
|
-
// ER/class/infra reference grammars are irregular (cardinality / typed
|
|
822
|
-
// relationship operators / indented arrows); the app keeps a special-cased
|
|
823
|
-
// trigger for those, but they're still reference-grammar types here so the
|
|
824
|
-
// drift-guard covers their extractors.
|
|
825
|
-
['er', { hasReferenceGrammar: true, referenceOperators: ['--'] }],
|
|
826
|
-
[
|
|
827
|
-
'class',
|
|
828
|
-
{
|
|
829
|
-
hasReferenceGrammar: true,
|
|
830
|
-
referenceOperators: ['-->', '--|>', '..|>', '*--', 'o--', '..>'],
|
|
831
|
-
},
|
|
832
|
-
],
|
|
833
|
-
['infra', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
834
|
-
['gantt', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
835
|
-
['pert', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
836
|
-
['arc', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
|
|
837
|
-
['sankey', { hasReferenceGrammar: true, referenceOperators: ['->', '--'] }],
|
|
838
|
-
[
|
|
839
|
-
'boxes-and-lines',
|
|
840
|
-
{ hasReferenceGrammar: true, referenceOperators: ['->', '<->'] },
|
|
841
|
-
],
|
|
842
|
-
// Venn references prior sets via the `+` intersection operator (not an arrow).
|
|
843
|
-
['venn', { hasReferenceGrammar: true, referenceOperators: ['+'] }],
|
|
844
|
-
]);
|
|
845
|
-
|
|
846
|
-
// Every other registered chart type has no reference position.
|
|
847
|
-
for (const type of ALL_CHART_TYPES) {
|
|
848
|
-
if (!REFERENCE_GRAMMAR.has(type)) {
|
|
849
|
-
REFERENCE_GRAMMAR.set(type, {
|
|
850
|
-
hasReferenceGrammar: false,
|
|
851
|
-
referenceOperators: [],
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// ============================================================
|
|
857
|
-
// Closed value enums (slot 7) sourced from parser constants
|
|
858
|
-
// ============================================================
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* RACI marker alphabet (slot-7 enum in the role-assignment value position).
|
|
862
|
-
* Consumed by editor completion. The chart type is always `raci`; the variant
|
|
863
|
-
* (RACI / RASCI / DACI) is inferred from the markers used, so completion offers
|
|
864
|
-
* the full union (R / A / S / C / I / D) sourced from `raci/variants.ts`'s
|
|
865
|
-
* `ALL_MARKERS` — the marker set can never drift from the parser.
|
|
866
|
-
*/
|
|
867
|
-
export const RACI_MARKER_ALPHABETS: ReadonlyMap<string, readonly string[]> =
|
|
868
|
-
new Map([['raci', [...ALL_MARKERS]]]);
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Closed set of wireframe element state flags (slot-7 trailing enum, e.g.
|
|
872
|
-
* `(Submit) primary destructive`). Sourced from the wireframe parser's
|
|
873
|
-
* `STATE_KEYWORDS` — exported there and consumed here, never re-typed.
|
|
874
|
-
*/
|
|
875
|
-
export const WIREFRAME_FLAGS: readonly string[] = [...WIREFRAME_STATE_KEYWORDS];
|
|
876
|
-
|
|
877
|
-
// ============================================================
|
|
878
|
-
// Pipe metadata for inline `| key value` on data lines
|
|
879
|
-
// ============================================================
|
|
880
|
-
|
|
881
|
-
/** Specification for a single pipe metadata key. */
|
|
882
|
-
export interface PipeKeySpec {
|
|
883
|
-
description: string;
|
|
884
|
-
values?: string[];
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Pipe metadata keys for inline `| key value` on data lines.
|
|
889
|
-
* Keyed by chart type → { context-name: keys }.
|
|
890
|
-
*
|
|
891
|
-
* Contexts are open-ended. The two universal ones are:
|
|
892
|
-
* - `node` — the default for any non-arrow line
|
|
893
|
-
* - `edge` — lines containing an arrow (`->`, `--`)
|
|
894
|
-
*
|
|
895
|
-
* Charts with richer line types declare additional contexts:
|
|
896
|
-
* - raci: `role`, `phase`, `assignment`
|
|
897
|
-
* - ring / pyramid: `layer`
|
|
898
|
-
* - tech-radar: `quadrant`, `blip`
|
|
899
|
-
* - journey-map: `step`
|
|
900
|
-
*
|
|
901
|
-
* IMPORTANT: NEVER add 'sequence' here. The `|` character in sequence
|
|
902
|
-
* diagrams separates display names from identifiers and tag metadata.
|
|
903
|
-
* Adding sequence would trigger false pipe-metadata completions on every `|`.
|
|
904
|
-
*/
|
|
905
|
-
export type PipeContextMap = Record<string, Record<string, PipeKeySpec>>;
|
|
906
|
-
|
|
907
|
-
export const PIPE_METADATA = new Map<string, PipeContextMap>([
|
|
908
|
-
[
|
|
909
|
-
'infra',
|
|
910
|
-
{
|
|
911
|
-
node: {
|
|
912
|
-
description: { description: 'Node description text' },
|
|
913
|
-
instances: {
|
|
914
|
-
description: 'Instance count or auto-scaling range (N-M)',
|
|
915
|
-
},
|
|
916
|
-
'latency-ms': { description: 'Per-request latency in milliseconds' },
|
|
917
|
-
'max-rps': { description: 'Max requests per second per instance' },
|
|
918
|
-
'cache-hit': { description: 'Cache hit percentage (0-100)' },
|
|
919
|
-
'firewall-block': { description: 'Traffic blocked percentage' },
|
|
920
|
-
'ratelimit-rps': { description: 'Max RPS allowed through' },
|
|
921
|
-
'cb-error-threshold': {
|
|
922
|
-
description: 'Circuit breaker error threshold %',
|
|
923
|
-
},
|
|
924
|
-
'cb-latency-threshold-ms': {
|
|
925
|
-
description: 'Circuit breaker latency threshold',
|
|
926
|
-
},
|
|
927
|
-
uptime: { description: 'Component availability (0-1)' },
|
|
928
|
-
concurrency: { description: 'Concurrent request limit' },
|
|
929
|
-
'duration-ms': { description: 'Processing duration' },
|
|
930
|
-
'cold-start-ms': { description: 'Function cold-start time' },
|
|
931
|
-
buffer: { description: 'Queue/buffer capacity' },
|
|
932
|
-
'drain-rate': { description: 'Queue drain rate' },
|
|
933
|
-
'retention-hours': { description: 'Data retention period' },
|
|
934
|
-
partitions: { description: 'Queue/stream partition count' },
|
|
935
|
-
'slo-availability': { description: 'Node availability target (0-1)' },
|
|
936
|
-
'slo-p90-latency-ms': { description: 'Node p90 latency target' },
|
|
937
|
-
'slo-warning-margin': { description: 'Node SLO warning margin' },
|
|
938
|
-
},
|
|
939
|
-
edge: {
|
|
940
|
-
split: { description: 'Traffic split percentage (e.g., 60%)' },
|
|
941
|
-
fanout: { description: 'Fanout multiplier (integer >= 1)' },
|
|
942
|
-
},
|
|
943
|
-
},
|
|
944
|
-
],
|
|
945
|
-
[
|
|
946
|
-
'c4',
|
|
947
|
-
{
|
|
948
|
-
node: {
|
|
949
|
-
description: { description: 'Element description' },
|
|
950
|
-
tech: { description: 'Technology stack' },
|
|
951
|
-
technology: { description: 'Technology stack (alias for tech)' },
|
|
952
|
-
},
|
|
953
|
-
edge: {},
|
|
954
|
-
},
|
|
955
|
-
],
|
|
956
|
-
[
|
|
957
|
-
'gantt',
|
|
958
|
-
{
|
|
959
|
-
node: {
|
|
960
|
-
duration: { description: 'Task duration (e.g., 30bd, 5d, 1.5w)' },
|
|
961
|
-
start: { description: 'Explicit start date (e.g., 2024-01-15)' },
|
|
962
|
-
progress: { description: 'Task progress (0–100)' },
|
|
963
|
-
offset: { description: 'Task start offset (e.g., 8bd, -3bd)' },
|
|
964
|
-
},
|
|
965
|
-
edge: {
|
|
966
|
-
// Gantt "edge" = dependency arrow (TaskA -> TaskB | offset 2bd)
|
|
967
|
-
offset: { description: 'Dependency offset (e.g., 2bd, -1w)' },
|
|
968
|
-
},
|
|
969
|
-
},
|
|
970
|
-
],
|
|
971
|
-
[
|
|
972
|
-
'boxes-and-lines',
|
|
973
|
-
{
|
|
974
|
-
node: {
|
|
975
|
-
description: { description: 'Node description text' },
|
|
976
|
-
heat: { description: 'Numeric value for the heat (colour) ramp' },
|
|
977
|
-
},
|
|
978
|
-
edge: {},
|
|
979
|
-
},
|
|
980
|
-
],
|
|
981
|
-
[
|
|
982
|
-
'mindmap',
|
|
983
|
-
{
|
|
984
|
-
node: {
|
|
985
|
-
description: { description: 'Node description text' },
|
|
986
|
-
collapsed: { description: 'Collapse node subtree by default' },
|
|
987
|
-
},
|
|
988
|
-
edge: {},
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
[
|
|
992
|
-
// Tech-radar pipe metadata (spec §20). Two contexts:
|
|
993
|
-
// - quadrant: top-level quadrant headers (`Tools | quadrant: top-left, color: blue`)
|
|
994
|
-
// - blip: indented blip lines (` Vite | ring: Adopt, trend: up`)
|
|
995
|
-
'tech-radar',
|
|
996
|
-
{
|
|
997
|
-
quadrant: {
|
|
998
|
-
quadrant: {
|
|
999
|
-
description: 'Quadrant position',
|
|
1000
|
-
values: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
|
1001
|
-
},
|
|
1002
|
-
color: { description: 'Override quadrant color' },
|
|
1003
|
-
},
|
|
1004
|
-
blip: {
|
|
1005
|
-
ring: { description: 'Ring assignment (must match a declared ring)' },
|
|
1006
|
-
trend: {
|
|
1007
|
-
description: 'Blip trend indicator',
|
|
1008
|
-
values: ['new', 'up', 'down', 'stable'],
|
|
1009
|
-
},
|
|
1010
|
-
},
|
|
1011
|
-
},
|
|
1012
|
-
],
|
|
1013
|
-
[
|
|
1014
|
-
'cycle',
|
|
1015
|
-
{
|
|
1016
|
-
node: {
|
|
1017
|
-
color: { description: 'Node fill color (palette name)' },
|
|
1018
|
-
span: { description: 'Relative arc distance to next node' },
|
|
1019
|
-
description: { description: 'Node description text' },
|
|
1020
|
-
},
|
|
1021
|
-
edge: {
|
|
1022
|
-
color: { description: 'Edge stroke color (palette name)' },
|
|
1023
|
-
width: { description: 'Edge stroke width in pixels' },
|
|
1024
|
-
},
|
|
1025
|
-
},
|
|
1026
|
-
],
|
|
1027
|
-
[
|
|
1028
|
-
// RACI pipe metadata (spec §24A). Two contexts:
|
|
1029
|
-
// - role: declarations inside the `roles` block (`Cap | color: red`)
|
|
1030
|
-
// - phase: bracketed phase headers (`[Departure] | color: teal`)
|
|
1031
|
-
'raci',
|
|
1032
|
-
{
|
|
1033
|
-
role: {
|
|
1034
|
-
color: { description: 'Role column tint (palette name)' },
|
|
1035
|
-
},
|
|
1036
|
-
phase: {
|
|
1037
|
-
color: { description: 'Phase bar tint (palette name)' },
|
|
1038
|
-
},
|
|
1039
|
-
},
|
|
1040
|
-
],
|
|
1041
|
-
[
|
|
1042
|
-
// Ring layer pipe metadata (spec §24.4). One context: `layer`.
|
|
1043
|
-
'ring',
|
|
1044
|
-
{
|
|
1045
|
-
layer: {
|
|
1046
|
-
color: { description: 'Ring color (palette name)' },
|
|
1047
|
-
description: { description: 'Layer description (one-liner shorthand)' },
|
|
1048
|
-
},
|
|
1049
|
-
},
|
|
1050
|
-
],
|
|
1051
|
-
[
|
|
1052
|
-
// Pyramid layer pipe metadata (spec §23.4). Identical surface to ring.
|
|
1053
|
-
'pyramid',
|
|
1054
|
-
{
|
|
1055
|
-
layer: {
|
|
1056
|
-
color: { description: 'Layer color (palette name)' },
|
|
1057
|
-
description: { description: 'Layer description (one-liner shorthand)' },
|
|
1058
|
-
},
|
|
1059
|
-
},
|
|
1060
|
-
],
|
|
1061
|
-
[
|
|
1062
|
-
// Journey-map step pipe metadata (spec §22).
|
|
1063
|
-
// Tag aliases (e.g. `ch: Web`) are user-defined via the `tag` block
|
|
1064
|
-
// and resolved dynamically — not part of the static map.
|
|
1065
|
-
'journey-map',
|
|
1066
|
-
{
|
|
1067
|
-
step: {
|
|
1068
|
-
score: { description: 'Step score (1–5 integer; high = good)' },
|
|
1069
|
-
emotion: { description: 'Emotional state at this step' },
|
|
1070
|
-
description: { description: 'Step description text' },
|
|
1071
|
-
pain: { description: 'Pain point at this step' },
|
|
1072
|
-
opportunity: { description: 'Opportunity at this step' },
|
|
1073
|
-
thought: { description: 'User thought at this step' },
|
|
1074
|
-
},
|
|
1075
|
-
},
|
|
1076
|
-
],
|
|
1077
|
-
[
|
|
1078
|
-
'org',
|
|
1079
|
-
{
|
|
1080
|
-
node: {
|
|
1081
|
-
description: { description: 'Person/team description' },
|
|
1082
|
-
role: { description: 'Role or job title' },
|
|
1083
|
-
location: { description: 'Office location' },
|
|
1084
|
-
email: { description: 'Email address' },
|
|
1085
|
-
phone: { description: 'Phone number' },
|
|
1086
|
-
},
|
|
1087
|
-
},
|
|
1088
|
-
],
|
|
1089
|
-
[
|
|
1090
|
-
'er',
|
|
1091
|
-
{
|
|
1092
|
-
node: {
|
|
1093
|
-
description: { description: 'Entity description' },
|
|
1094
|
-
domain: { description: 'Domain grouping' },
|
|
1095
|
-
},
|
|
1096
|
-
},
|
|
1097
|
-
],
|
|
1098
|
-
[
|
|
1099
|
-
'class',
|
|
1100
|
-
{
|
|
1101
|
-
node: {
|
|
1102
|
-
description: { description: 'Class description' },
|
|
1103
|
-
},
|
|
1104
|
-
},
|
|
1105
|
-
],
|
|
1106
|
-
[
|
|
1107
|
-
'kanban',
|
|
1108
|
-
{
|
|
1109
|
-
node: {
|
|
1110
|
-
description: { description: 'Card description' },
|
|
1111
|
-
assignee: { description: 'Assigned person' },
|
|
1112
|
-
due: { description: 'Due date (YYYY-MM-DD)' },
|
|
1113
|
-
},
|
|
1114
|
-
},
|
|
1115
|
-
],
|
|
1116
|
-
[
|
|
1117
|
-
'sitemap',
|
|
1118
|
-
{
|
|
1119
|
-
node: {
|
|
1120
|
-
description: { description: 'Page description' },
|
|
1121
|
-
status: { description: 'Page status' },
|
|
1122
|
-
},
|
|
1123
|
-
},
|
|
1124
|
-
],
|
|
1125
|
-
[
|
|
1126
|
-
'pert',
|
|
1127
|
-
{
|
|
1128
|
-
node: {
|
|
1129
|
-
description: { description: 'Activity description' },
|
|
1130
|
-
confidence: {
|
|
1131
|
-
description: 'Confidence factor',
|
|
1132
|
-
values: ['high', 'medium', 'low'],
|
|
1133
|
-
},
|
|
1134
|
-
collapsed: { description: 'Collapse activity detail' },
|
|
1135
|
-
},
|
|
1136
|
-
},
|
|
1137
|
-
],
|
|
1138
|
-
[
|
|
1139
|
-
'timeline',
|
|
1140
|
-
{
|
|
1141
|
-
node: {
|
|
1142
|
-
description: { description: 'Event description' },
|
|
1143
|
-
duration: { description: 'Event duration (e.g., 30d, 1.5y)' },
|
|
1144
|
-
},
|
|
1145
|
-
},
|
|
1146
|
-
],
|
|
1147
|
-
]);
|
|
1148
|
-
|
|
1149
|
-
// ============================================================
|
|
1150
|
-
// Derived metadata key set
|
|
1151
|
-
// ============================================================
|
|
1152
|
-
|
|
1153
|
-
/** All known directive keys, derived from COMPLETION_REGISTRY. Includes implicit keys. */
|
|
1154
|
-
export const METADATA_KEY_SET: ReadonlySet<string> = new Set([
|
|
1155
|
-
'chart',
|
|
1156
|
-
'title', // implicit directives recognized as metadata
|
|
1157
|
-
...[...COMPLETION_REGISTRY.values()].flatMap((spec) =>
|
|
1158
|
-
Object.keys(spec.directives)
|
|
1159
|
-
),
|
|
1160
|
-
]);
|
|
1161
|
-
|
|
1162
|
-
// ============================================================
|
|
1163
|
-
// Sequence extractor
|
|
1164
|
-
// ============================================================
|
|
1165
|
-
|
|
1166
|
-
// Universal Name Handling: source/target accept multi-word + "quoted" names.
|
|
1167
|
-
// `[^|]+?` captures greedy-to-pipe-or-arrow; the arrow alternation acts as
|
|
1168
|
-
// the boundary. Caller strips quotes via stripQuotes().
|
|
1169
|
-
const SEQ_ARROW_RE =
|
|
1170
|
-
/^(?:"([^"]+)"|([^|"]+?))\s+(->|-.*->|~>|~.*~>)\s+(?:"([^"]+)"|([^|"]+?))(?:\s|\|.*)?$/;
|
|
1171
|
-
const SEQ_IS_A_RE = /^(?:"([^"]+)"|([^|":]+?))\s+is\s+an?\s+/i;
|
|
1172
|
-
const SEQ_SECTION_RE = /^==/;
|
|
1173
|
-
const SEQ_STRUCTURAL_RE = /^(if|else|loop|parallel|end)\b/i;
|
|
1174
|
-
|
|
1175
|
-
function extractSequenceSymbols(docText: string): DiagramSymbols {
|
|
1176
|
-
const lines = docText.split('\n');
|
|
1177
|
-
const entities: string[] = [];
|
|
1178
|
-
let pastFirstLine = false;
|
|
1179
|
-
|
|
1180
|
-
for (const line of lines) {
|
|
1181
|
-
const trimmed = line.trim();
|
|
1182
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1183
|
-
|
|
1184
|
-
// Skip first line (chart type)
|
|
1185
|
-
if (!pastFirstLine) {
|
|
1186
|
-
pastFirstLine = true;
|
|
1187
|
-
continue;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Skip metadata lines
|
|
1191
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1192
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1193
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1194
|
-
|
|
1195
|
-
// Skip sections, structural keywords
|
|
1196
|
-
if (SEQ_SECTION_RE.test(trimmed)) continue;
|
|
1197
|
-
if (SEQ_STRUCTURAL_RE.test(trimmed)) continue;
|
|
1198
|
-
|
|
1199
|
-
// Arrow lines: A -> B, A -label-> B, A ~> B
|
|
1200
|
-
const arrowMatch = trimmed.match(SEQ_ARROW_RE);
|
|
1201
|
-
if (arrowMatch) {
|
|
1202
|
-
const src = (arrowMatch[1] ?? arrowMatch[2] ?? '').trim();
|
|
1203
|
-
const dst = (arrowMatch[4] ?? arrowMatch[5] ?? '').trim();
|
|
1204
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
1205
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Type declarations: A is a person, A is an actor
|
|
1210
|
-
const isAMatch = trimmed.match(SEQ_IS_A_RE);
|
|
1211
|
-
if (isAMatch) {
|
|
1212
|
-
const name = (isAMatch[1] ?? isAMatch[2] ?? '').trim();
|
|
1213
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
return {
|
|
1219
|
-
kind: 'sequence',
|
|
1220
|
-
entities,
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// ============================================================
|
|
1225
|
-
// State extractor
|
|
1226
|
-
// ============================================================
|
|
1227
|
-
|
|
1228
|
-
const STATE_ARROW_RE = /^(\S+)\s+->\s+(\S+)/;
|
|
1229
|
-
|
|
1230
|
-
function extractStateSymbols(docText: string): DiagramSymbols {
|
|
1231
|
-
const lines = docText.split('\n');
|
|
1232
|
-
const entities: string[] = [];
|
|
1233
|
-
let pastFirstLine = false;
|
|
1234
|
-
|
|
1235
|
-
for (const line of lines) {
|
|
1236
|
-
const trimmed = line.trim();
|
|
1237
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1238
|
-
|
|
1239
|
-
if (!pastFirstLine) {
|
|
1240
|
-
pastFirstLine = true;
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Skip metadata lines
|
|
1245
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1246
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1247
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1248
|
-
|
|
1249
|
-
const arrowMatch = trimmed.match(STATE_ARROW_RE);
|
|
1250
|
-
if (arrowMatch) {
|
|
1251
|
-
// Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
|
|
1252
|
-
const src = arrowMatch[1]!.split('|')[0]!.trim();
|
|
1253
|
-
const dst = arrowMatch[2]!.split('|')[0]!.trim();
|
|
1254
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
1255
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
return { kind: 'state', entities };
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// ============================================================
|
|
1263
|
-
// Tag declaration extraction
|
|
1264
|
-
// ============================================================
|
|
1265
|
-
|
|
1266
|
-
// Matches tag declarations in both forms:
|
|
1267
|
-
// - `tag Name alias x` (explicit alias keyword)
|
|
1268
|
-
// - `tag Name x` (shorthand: 1-4 lowercase chars = alias, matching parser's isAliasToken)
|
|
1269
|
-
const TAG_DECL_EXPLICIT_RE = /^tag\s+(\S+)\s+alias\s+(\S+)/i;
|
|
1270
|
-
const TAG_DECL_SHORT_RE = /^tag\s+(\S+)\s+([a-z]{1,4})(?:\s|$)/;
|
|
1271
|
-
|
|
1272
|
-
/**
|
|
1273
|
-
* Extract tag declarations from document text.
|
|
1274
|
-
* Returns a map of alias (or full name) → array of tag values.
|
|
1275
|
-
* Keys preserve original case for display; use case-insensitive lookup.
|
|
1276
|
-
*/
|
|
1277
|
-
export function extractTagDeclarations(docText: string): Map<string, string[]> {
|
|
1278
|
-
const result = new Map<string, string[]>();
|
|
1279
|
-
const lines = docText.split('\n');
|
|
1280
|
-
let currentAlias: string | null = null;
|
|
1281
|
-
let currentValues: string[] = [];
|
|
1282
|
-
|
|
1283
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1284
|
-
// In-bounds by loop guard.
|
|
1285
|
-
const raw = lines[i]!;
|
|
1286
|
-
const trimmed = raw.trim();
|
|
1287
|
-
|
|
1288
|
-
// Check for tag declaration — try explicit `alias` keyword first, then shorthand
|
|
1289
|
-
const tagMatch =
|
|
1290
|
-
trimmed.match(TAG_DECL_EXPLICIT_RE) ?? trimmed.match(TAG_DECL_SHORT_RE);
|
|
1291
|
-
if (tagMatch) {
|
|
1292
|
-
// Save previous tag group
|
|
1293
|
-
if (currentAlias !== null) {
|
|
1294
|
-
result.set(currentAlias, currentValues);
|
|
1295
|
-
}
|
|
1296
|
-
// Both regexes capture groups 1 (and 2 for explicit) on successful match.
|
|
1297
|
-
const name = tagMatch[1]!;
|
|
1298
|
-
const alias = tagMatch[2] ?? name;
|
|
1299
|
-
currentAlias = alias;
|
|
1300
|
-
currentValues = [];
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
// Also match bare `tag Name` (no alias) — fall through with name as key
|
|
1304
|
-
if (/^tag\s+(\S+)\s*$/i.test(trimmed)) {
|
|
1305
|
-
if (currentAlias !== null) {
|
|
1306
|
-
result.set(currentAlias, currentValues);
|
|
1307
|
-
}
|
|
1308
|
-
// Regex captured group 1 by successful re-match (test passed above).
|
|
1309
|
-
currentAlias = trimmed.match(/^tag\s+(\S+)/i)![1]!;
|
|
1310
|
-
currentValues = [];
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// Collect indented tag values
|
|
1315
|
-
if (
|
|
1316
|
-
currentAlias !== null &&
|
|
1317
|
-
raw.length > 0 &&
|
|
1318
|
-
(raw[0] === ' ' || raw[0] === '\t')
|
|
1319
|
-
) {
|
|
1320
|
-
if (trimmed && !trimmed.startsWith('//')) {
|
|
1321
|
-
// Strip trailing-token color (§1.5): `Frontend blue` → `Frontend`.
|
|
1322
|
-
// Whitespace-split; if the last token is a recognized color word,
|
|
1323
|
-
// drop it; otherwise the whole trimmed string is the value.
|
|
1324
|
-
const lastSpaceIdx = trimmed.lastIndexOf(' ');
|
|
1325
|
-
const value =
|
|
1326
|
-
lastSpaceIdx > 0 &&
|
|
1327
|
-
RECOGNIZED_COLOR_SET.has(trimmed.substring(lastSpaceIdx + 1))
|
|
1328
|
-
? trimmed.substring(0, lastSpaceIdx).trim()
|
|
1329
|
-
: trimmed;
|
|
1330
|
-
if (value) currentValues.push(value);
|
|
1331
|
-
}
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// Non-indented non-tag line ends the current tag block
|
|
1336
|
-
if (currentAlias !== null && trimmed) {
|
|
1337
|
-
result.set(currentAlias, currentValues);
|
|
1338
|
-
currentAlias = null;
|
|
1339
|
-
currentValues = [];
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// Save last tag group
|
|
1344
|
-
if (currentAlias !== null) {
|
|
1345
|
-
result.set(currentAlias, currentValues);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
return result;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// ============================================================
|
|
1352
|
-
// Universal alias extractor (`Name as <alias>` postfix)
|
|
1353
|
-
// ============================================================
|
|
1354
|
-
|
|
1355
|
-
// Postfix-alias form on any name-slot line. Caller-agnostic — runs
|
|
1356
|
-
// over the full document so every chart-type extractor can populate
|
|
1357
|
-
// `DiagramSymbols.aliases` consistently.
|
|
1358
|
-
const ALIAS_POSTFIX_DECL_RE =
|
|
1359
|
-
/(?:^|[^|/])\s*(.+?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*(?:\|.*)?$/;
|
|
1360
|
-
|
|
1361
|
-
/**
|
|
1362
|
-
* Scan document text for `Name as <alias>` declarations.
|
|
1363
|
-
*
|
|
1364
|
-
* Returns `Record<alias, canonical-fragment>`. The canonical may
|
|
1365
|
-
* still carry color/type modifiers (the per-parser logic peels
|
|
1366
|
-
* those off at parse time); for autocomplete display purposes the
|
|
1367
|
-
* raw fragment is good enough.
|
|
1368
|
-
*
|
|
1369
|
-
* Pure helper — does NOT enforce strict-ordering, collisions, or
|
|
1370
|
-
* other semantic rules. Those are parser-side checks.
|
|
1371
|
-
*/
|
|
1372
|
-
export function extractAliasDeclarations(
|
|
1373
|
-
docText: string
|
|
1374
|
-
): Record<string, string> {
|
|
1375
|
-
const aliases: Record<string, string> = {};
|
|
1376
|
-
for (const raw of docText.split('\n')) {
|
|
1377
|
-
const trimmed = raw.trim();
|
|
1378
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1379
|
-
const match = trimmed.match(ALIAS_POSTFIX_DECL_RE);
|
|
1380
|
-
if (!match) continue;
|
|
1381
|
-
// Regex captured groups 1 and 2 by successful match.
|
|
1382
|
-
const canonical = match[1]!.trim();
|
|
1383
|
-
const alias = match[2]!;
|
|
1384
|
-
// Skip if canonical itself looks structural (arrow / pipe-only / brackets-only)
|
|
1385
|
-
if (!canonical || canonical === '[' || canonical === ']') continue;
|
|
1386
|
-
if (!(alias in aliases)) {
|
|
1387
|
-
aliases[alias] = canonical;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
return aliases;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
// ============================================================
|
|
1394
|
-
// Sitemap extractor
|
|
1395
|
-
// ============================================================
|
|
1396
|
-
|
|
1397
|
-
const SITEMAP_CONTAINER_RE = /^\[([^\]]+)\]/;
|
|
1398
|
-
const SITEMAP_ARROW_RE = /^-.*->\s*(.+)$/;
|
|
1399
|
-
const SITEMAP_BARE_ARROW_RE = /^->\s*(.+)$/;
|
|
1400
|
-
const SITEMAP_METADATA_RE = /^([^:]+):\s*(.+)$/;
|
|
1401
|
-
|
|
1402
|
-
function extractSitemapSymbols(docText: string): DiagramSymbols {
|
|
1403
|
-
const lines = docText.split('\n');
|
|
1404
|
-
const entities: string[] = [];
|
|
1405
|
-
let pastFirstLine = false;
|
|
1406
|
-
let inTagBlock = false;
|
|
1407
|
-
let lastNodeIndent = -1;
|
|
1408
|
-
|
|
1409
|
-
for (const line of lines) {
|
|
1410
|
-
const trimmed = line.trim();
|
|
1411
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1412
|
-
|
|
1413
|
-
if (!pastFirstLine) {
|
|
1414
|
-
pastFirstLine = true;
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// Skip metadata lines
|
|
1419
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1420
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1421
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1422
|
-
|
|
1423
|
-
// Track tag blocks
|
|
1424
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1425
|
-
inTagBlock = true;
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
const indent = line.search(/\S/);
|
|
1429
|
-
if (inTagBlock) {
|
|
1430
|
-
if (indent > 0) continue;
|
|
1431
|
-
inTagBlock = false;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// Containers: [GroupName]
|
|
1435
|
-
const containerMatch = trimmed.match(SITEMAP_CONTAINER_RE);
|
|
1436
|
-
if (containerMatch) {
|
|
1437
|
-
// Regex captured group 1 by successful match; split('|')[0] always defined.
|
|
1438
|
-
const name = containerMatch[1]!.split('|')[0]!.trim();
|
|
1439
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1440
|
-
lastNodeIndent = indent;
|
|
1441
|
-
continue;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Arrows: -> Target or -label-> Target
|
|
1445
|
-
const bareArrow = trimmed.match(SITEMAP_BARE_ARROW_RE);
|
|
1446
|
-
const labeledArrow = !bareArrow ? trimmed.match(SITEMAP_ARROW_RE) : null;
|
|
1447
|
-
if (bareArrow || labeledArrow) {
|
|
1448
|
-
// split('|')[0] always defined on any string.
|
|
1449
|
-
const target = (bareArrow?.[1] ?? labeledArrow?.[1] ?? '')
|
|
1450
|
-
.split('|')[0]!
|
|
1451
|
-
.trim();
|
|
1452
|
-
if (target && !entities.includes(target)) entities.push(target);
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// Indented metadata under a node (key: value) — skip
|
|
1457
|
-
if (
|
|
1458
|
-
indent > 0 &&
|
|
1459
|
-
lastNodeIndent >= 0 &&
|
|
1460
|
-
indent > lastNodeIndent &&
|
|
1461
|
-
SITEMAP_METADATA_RE.test(trimmed)
|
|
1462
|
-
) {
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Page label (anything else that's not special)
|
|
1467
|
-
// split('|')[0] always defined on any string.
|
|
1468
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
1469
|
-
if (label) {
|
|
1470
|
-
if (!entities.includes(label)) entities.push(label);
|
|
1471
|
-
lastNodeIndent = indent;
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
return { kind: 'sitemap', entities };
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// ============================================================
|
|
1479
|
-
// C4 extractor
|
|
1480
|
-
// ============================================================
|
|
1481
|
-
|
|
1482
|
-
const C4_ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
|
|
1483
|
-
const C4_IS_A_RE =
|
|
1484
|
-
/^(.+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b/i;
|
|
1485
|
-
const C4_ARROW_RE =
|
|
1486
|
-
/^(\S+)\s+(?:->|-.*->|~>|~.*~>|<->|<-.*->|<~>|<~.*~>)\s+(\S+)/;
|
|
1487
|
-
const C4_SECTION_RE = /^(containers|components|deployment)\s*$/i;
|
|
1488
|
-
|
|
1489
|
-
function extractC4Symbols(docText: string): DiagramSymbols {
|
|
1490
|
-
const lines = docText.split('\n');
|
|
1491
|
-
const entities: string[] = [];
|
|
1492
|
-
let pastFirstLine = false;
|
|
1493
|
-
let inTagBlock = false;
|
|
1494
|
-
|
|
1495
|
-
for (const line of lines) {
|
|
1496
|
-
const trimmed = line.trim();
|
|
1497
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1498
|
-
|
|
1499
|
-
if (!pastFirstLine) {
|
|
1500
|
-
pastFirstLine = true;
|
|
1501
|
-
continue;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1505
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1506
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1507
|
-
|
|
1508
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1509
|
-
inTagBlock = true;
|
|
1510
|
-
continue;
|
|
1511
|
-
}
|
|
1512
|
-
const indent = line.search(/\S/);
|
|
1513
|
-
if (inTagBlock) {
|
|
1514
|
-
if (indent > 0) continue;
|
|
1515
|
-
inTagBlock = false;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// Skip section headers
|
|
1519
|
-
if (C4_SECTION_RE.test(trimmed)) continue;
|
|
1520
|
-
|
|
1521
|
-
// Element declaration: person Name, system Name, etc.
|
|
1522
|
-
const elemMatch = trimmed.match(C4_ELEMENT_RE);
|
|
1523
|
-
if (elemMatch) {
|
|
1524
|
-
// Regex captured group 2 by successful match; split('|')[0] always defined.
|
|
1525
|
-
const name = elemMatch[2]!.split('|')[0]!.trim();
|
|
1526
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1527
|
-
continue;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
// Is-a declaration: Name is a person
|
|
1531
|
-
const isAMatch = trimmed.match(C4_IS_A_RE);
|
|
1532
|
-
if (isAMatch) {
|
|
1533
|
-
// Regex captured group 1 by successful match; split('|')[0] always defined.
|
|
1534
|
-
const name = isAMatch[1]!.split('|')[0]!.trim();
|
|
1535
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
// Arrow lines: Source -> Target, Source ~> Target, etc.
|
|
1540
|
-
const arrowMatch = trimmed.match(C4_ARROW_RE);
|
|
1541
|
-
if (arrowMatch) {
|
|
1542
|
-
// Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
|
|
1543
|
-
const src = arrowMatch[1]!.split('|')[0]!.trim();
|
|
1544
|
-
const dst = arrowMatch[2]!.split('|')[0]!.trim();
|
|
1545
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
1546
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
1547
|
-
continue;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
return {
|
|
1552
|
-
kind: 'c4',
|
|
1553
|
-
entities,
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// ============================================================
|
|
1558
|
-
// Gantt extractor
|
|
1559
|
-
// ============================================================
|
|
1560
|
-
|
|
1561
|
-
const GANTT_LEGACY_DURATION_RE =
|
|
1562
|
-
/^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h|s)\??\s+(.+)$/;
|
|
1563
|
-
const GANTT_LEGACY_DATE_RE = /^(\d{4}-\d{2}-\d{2}(?:\s\d{2}:\d{2})?)\s+(.+)$/;
|
|
1564
|
-
const GANTT_GROUP_RE = /^\[(.+?)\]/;
|
|
1565
|
-
const GANTT_STRUCTURAL_RE = /^(era|marker|holiday|workweek|parallel)\b/i;
|
|
1566
|
-
const GANTT_META_KEY_RE = /\b(?:duration|start):\s/;
|
|
1567
|
-
|
|
1568
|
-
function extractGanttSymbols(docText: string): DiagramSymbols {
|
|
1569
|
-
const lines = docText.split('\n');
|
|
1570
|
-
const entities: string[] = [];
|
|
1571
|
-
let pastFirstLine = false;
|
|
1572
|
-
let inTagBlock = false;
|
|
1573
|
-
|
|
1574
|
-
for (const line of lines) {
|
|
1575
|
-
const trimmed = line.trim();
|
|
1576
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1577
|
-
|
|
1578
|
-
if (!pastFirstLine) {
|
|
1579
|
-
pastFirstLine = true;
|
|
1580
|
-
continue;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1584
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1585
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1586
|
-
|
|
1587
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1588
|
-
inTagBlock = true;
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
const indent = line.search(/\S/);
|
|
1592
|
-
if (inTagBlock) {
|
|
1593
|
-
if (indent > 0) continue;
|
|
1594
|
-
inTagBlock = false;
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// Groups: [GroupName]
|
|
1598
|
-
const groupMatch = trimmed.match(GANTT_GROUP_RE);
|
|
1599
|
-
if (groupMatch) {
|
|
1600
|
-
const name = groupMatch[1]!.trim();
|
|
1601
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1602
|
-
continue;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// New syntax: Task Name duration: 5d or Task Name start: 2024-01-15
|
|
1606
|
-
// (checked before structural keyword skip so "Era of Innovation duration: 5d" isn't skipped)
|
|
1607
|
-
if (GANTT_META_KEY_RE.test(trimmed)) {
|
|
1608
|
-
const cutIdx = trimmed.search(
|
|
1609
|
-
/\b(?:duration|start|progress|offset|color|description):\s/
|
|
1610
|
-
);
|
|
1611
|
-
if (cutIdx > 0) {
|
|
1612
|
-
let taskName = trimmed.substring(0, cutIdx).trim();
|
|
1613
|
-
const arrowIdx = taskName.indexOf('->');
|
|
1614
|
-
if (arrowIdx > 0)
|
|
1615
|
-
taskName = taskName
|
|
1616
|
-
.substring(0, arrowIdx)
|
|
1617
|
-
.replace(/-[^>]*$/, '')
|
|
1618
|
-
.trim();
|
|
1619
|
-
if (taskName && !entities.includes(taskName)) entities.push(taskName);
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Skip structural keywords (after new-syntax check so "Era of Innovation duration: 5d" isn't skipped)
|
|
1625
|
-
if (GANTT_STRUCTURAL_RE.test(trimmed)) continue;
|
|
1626
|
-
|
|
1627
|
-
// Legacy: Tasks by duration: 30d Task Name
|
|
1628
|
-
const durMatch = trimmed.match(GANTT_LEGACY_DURATION_RE);
|
|
1629
|
-
if (durMatch) {
|
|
1630
|
-
let taskName = durMatch[3]!.split('|')[0]!.trim();
|
|
1631
|
-
const arrowIdx = taskName.indexOf('->');
|
|
1632
|
-
if (arrowIdx > 0)
|
|
1633
|
-
taskName = taskName
|
|
1634
|
-
.substring(0, arrowIdx)
|
|
1635
|
-
.replace(/-[^>]*$/, '')
|
|
1636
|
-
.trim();
|
|
1637
|
-
if (taskName && !entities.includes(taskName)) entities.push(taskName);
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Legacy: Tasks by date: 2024-01-15 Task Name
|
|
1642
|
-
const dateMatch = trimmed.match(GANTT_LEGACY_DATE_RE);
|
|
1643
|
-
if (dateMatch) {
|
|
1644
|
-
let taskName = dateMatch[2]!.split('|')[0]!.trim();
|
|
1645
|
-
const arrowIdx = taskName.indexOf('->');
|
|
1646
|
-
if (arrowIdx > 0)
|
|
1647
|
-
taskName = taskName
|
|
1648
|
-
.substring(0, arrowIdx)
|
|
1649
|
-
.replace(/-[^>]*$/, '')
|
|
1650
|
-
.trim();
|
|
1651
|
-
if (taskName && !entities.includes(taskName)) entities.push(taskName);
|
|
1652
|
-
continue;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
return { kind: 'gantt', entities };
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// ============================================================
|
|
1660
|
-
// Boxes-and-lines extractor
|
|
1661
|
-
// ============================================================
|
|
1662
|
-
|
|
1663
|
-
const BL_ARROW_RE = /^(\S+)\s+(?:-.*)?(?:->|<->)\s+(\S+)/;
|
|
1664
|
-
|
|
1665
|
-
function extractBoxesAndLinesSymbols(docText: string): DiagramSymbols {
|
|
1666
|
-
const lines = docText.split('\n');
|
|
1667
|
-
const entities: string[] = [];
|
|
1668
|
-
let pastFirstLine = false;
|
|
1669
|
-
let inTagBlock = false;
|
|
1670
|
-
|
|
1671
|
-
for (const line of lines) {
|
|
1672
|
-
const trimmed = line.trim();
|
|
1673
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1674
|
-
|
|
1675
|
-
if (!pastFirstLine) {
|
|
1676
|
-
pastFirstLine = true;
|
|
1677
|
-
continue;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
1681
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1682
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1683
|
-
|
|
1684
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1685
|
-
inTagBlock = true;
|
|
1686
|
-
continue;
|
|
1687
|
-
}
|
|
1688
|
-
const indent = line.search(/\S/);
|
|
1689
|
-
if (inTagBlock) {
|
|
1690
|
-
if (indent > 0) continue;
|
|
1691
|
-
inTagBlock = false;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Skip groups
|
|
1695
|
-
if (/^\[.+?\]/.test(trimmed)) continue;
|
|
1696
|
-
|
|
1697
|
-
// Edge lines
|
|
1698
|
-
const arrowMatch = trimmed.match(BL_ARROW_RE);
|
|
1699
|
-
if (arrowMatch) {
|
|
1700
|
-
// Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
|
|
1701
|
-
const src = arrowMatch[1]!.split('|')[0]!.trim();
|
|
1702
|
-
const dst = arrowMatch[2]!.split('|')[0]!.trim();
|
|
1703
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
1704
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
// Node lines
|
|
1709
|
-
// split('|')[0] and chained split('[')[0] always defined on any string.
|
|
1710
|
-
const label = trimmed.split('|')[0]!.split('[')[0]!.trim();
|
|
1711
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
return { kind: 'boxes-and-lines', entities };
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// ============================================================
|
|
1718
|
-
// Org extractor
|
|
1719
|
-
// ============================================================
|
|
1720
|
-
|
|
1721
|
-
const ORG_GROUP_RE = /^\[(.+?)\]/;
|
|
1722
|
-
|
|
1723
|
-
function extractOrgSymbols(docText: string): DiagramSymbols {
|
|
1724
|
-
const lines = docText.split('\n');
|
|
1725
|
-
const entities: string[] = [];
|
|
1726
|
-
let pastFirstLine = false;
|
|
1727
|
-
let inTagBlock = false;
|
|
1728
|
-
|
|
1729
|
-
for (const line of lines) {
|
|
1730
|
-
const trimmed = line.trim();
|
|
1731
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1732
|
-
|
|
1733
|
-
if (!pastFirstLine) {
|
|
1734
|
-
pastFirstLine = true;
|
|
1735
|
-
continue;
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1739
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1740
|
-
|
|
1741
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1742
|
-
inTagBlock = true;
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
const indent = line.search(/\S/);
|
|
1746
|
-
if (inTagBlock) {
|
|
1747
|
-
if (indent > 0) continue;
|
|
1748
|
-
inTagBlock = false;
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
// Team/group headers: [Team Name]
|
|
1752
|
-
const groupMatch = trimmed.match(ORG_GROUP_RE);
|
|
1753
|
-
if (groupMatch) {
|
|
1754
|
-
const name = groupMatch[1]!.split('|')[0]!.trim();
|
|
1755
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1756
|
-
continue;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// Skip indented metadata lines (key: value)
|
|
1760
|
-
if (indent > 0 && /^[a-z]+\s*:/.test(trimmed)) continue;
|
|
1761
|
-
|
|
1762
|
-
// Person name (indent 0 or direct child)
|
|
1763
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
1764
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
return { kind: 'org', entities };
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// ============================================================
|
|
1771
|
-
// Kanban extractor
|
|
1772
|
-
// ============================================================
|
|
1773
|
-
|
|
1774
|
-
const KANBAN_COLUMN_RE = /^\[(.+?)\]/;
|
|
1775
|
-
|
|
1776
|
-
function extractKanbanSymbols(docText: string): DiagramSymbols {
|
|
1777
|
-
const lines = docText.split('\n');
|
|
1778
|
-
const entities: string[] = [];
|
|
1779
|
-
let pastFirstLine = false;
|
|
1780
|
-
let inTagBlock = false;
|
|
1781
|
-
|
|
1782
|
-
for (const line of lines) {
|
|
1783
|
-
const trimmed = line.trim();
|
|
1784
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1785
|
-
|
|
1786
|
-
if (!pastFirstLine) {
|
|
1787
|
-
pastFirstLine = true;
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1792
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1793
|
-
|
|
1794
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1795
|
-
inTagBlock = true;
|
|
1796
|
-
continue;
|
|
1797
|
-
}
|
|
1798
|
-
const indent = line.search(/\S/);
|
|
1799
|
-
if (inTagBlock) {
|
|
1800
|
-
if (indent > 0) continue;
|
|
1801
|
-
inTagBlock = false;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
// Column headers: [Column Name]
|
|
1805
|
-
const colMatch = trimmed.match(KANBAN_COLUMN_RE);
|
|
1806
|
-
if (colMatch) {
|
|
1807
|
-
const name = colMatch[1]!.split('|')[0]!.trim();
|
|
1808
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Card names (indented under columns)
|
|
1813
|
-
if (indent > 0) {
|
|
1814
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
1815
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
return { kind: 'kanban', entities };
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ============================================================
|
|
1823
|
-
// Mindmap extractor
|
|
1824
|
-
// ============================================================
|
|
1825
|
-
|
|
1826
|
-
function extractMindmapSymbols(docText: string): DiagramSymbols {
|
|
1827
|
-
const lines = docText.split('\n');
|
|
1828
|
-
const entities: string[] = [];
|
|
1829
|
-
let pastFirstLine = false;
|
|
1830
|
-
let inTagBlock = false;
|
|
1831
|
-
|
|
1832
|
-
for (const line of lines) {
|
|
1833
|
-
const trimmed = line.trim();
|
|
1834
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1835
|
-
|
|
1836
|
-
if (!pastFirstLine) {
|
|
1837
|
-
pastFirstLine = true;
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1842
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1843
|
-
|
|
1844
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1845
|
-
inTagBlock = true;
|
|
1846
|
-
continue;
|
|
1847
|
-
}
|
|
1848
|
-
const indent = line.search(/\S/);
|
|
1849
|
-
if (inTagBlock) {
|
|
1850
|
-
if (indent > 0) continue;
|
|
1851
|
-
inTagBlock = false;
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
// Skip indented metadata (description:, collapsed:)
|
|
1855
|
-
if (/^(description|collapsed)\s*:/i.test(trimmed)) continue;
|
|
1856
|
-
|
|
1857
|
-
// Node name (at any indent level)
|
|
1858
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
1859
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
return { kind: 'mindmap', entities };
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// ============================================================
|
|
1866
|
-
// Treemap extractor
|
|
1867
|
-
// ============================================================
|
|
1868
|
-
|
|
1869
|
-
function extractTreemapSymbols(docText: string): DiagramSymbols {
|
|
1870
|
-
const lines = docText.split('\n');
|
|
1871
|
-
const entities: string[] = [];
|
|
1872
|
-
let pastFirstLine = false;
|
|
1873
|
-
let inTagBlock = false;
|
|
1874
|
-
|
|
1875
|
-
for (const line of lines) {
|
|
1876
|
-
const trimmed = line.trim();
|
|
1877
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1878
|
-
|
|
1879
|
-
if (!pastFirstLine) {
|
|
1880
|
-
pastFirstLine = true;
|
|
1881
|
-
continue;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
// Directives and tag blocks are not node entities.
|
|
1885
|
-
if (
|
|
1886
|
-
/^(depth|heat|no-[a-z]+)\s/i.test(trimmed) ||
|
|
1887
|
-
/^(no-[a-z]+|radial)$/i.test(trimmed)
|
|
1888
|
-
)
|
|
1889
|
-
continue;
|
|
1890
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1891
|
-
inTagBlock = true;
|
|
1892
|
-
continue;
|
|
1893
|
-
}
|
|
1894
|
-
const indent = line.search(/\S/);
|
|
1895
|
-
if (inTagBlock) {
|
|
1896
|
-
if (indent > 0) continue;
|
|
1897
|
-
inTagBlock = false;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// Node name: strip same-line metadata + the bare trailing value number.
|
|
1901
|
-
let label = trimmed.split(/\s+\w+:/)[0]!.trim();
|
|
1902
|
-
label = label.replace(/\s+-?\d[\d_,.]*$/, '').trim();
|
|
1903
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
return { kind: 'treemap', entities };
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
/** Block-diagram symbols: every `[Label]` on a grid row (skips tag blocks and
|
|
1910
|
-
* the `columns` / `no-*` directives). */
|
|
1911
|
-
function extractBlockSymbols(docText: string): DiagramSymbols {
|
|
1912
|
-
const lines = docText.split('\n');
|
|
1913
|
-
const entities: string[] = [];
|
|
1914
|
-
let pastFirstLine = false;
|
|
1915
|
-
let inTagBlock = false;
|
|
1916
|
-
|
|
1917
|
-
for (const line of lines) {
|
|
1918
|
-
const trimmed = line.trim();
|
|
1919
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1920
|
-
if (!pastFirstLine) {
|
|
1921
|
-
pastFirstLine = true;
|
|
1922
|
-
continue;
|
|
1923
|
-
}
|
|
1924
|
-
if (/^(columns|no-[a-z]+)\b/i.test(trimmed)) continue;
|
|
1925
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
1926
|
-
inTagBlock = true;
|
|
1927
|
-
continue;
|
|
1928
|
-
}
|
|
1929
|
-
const indent = line.search(/\S/);
|
|
1930
|
-
if (inTagBlock) {
|
|
1931
|
-
if (indent > 0) continue;
|
|
1932
|
-
inTagBlock = false;
|
|
1933
|
-
}
|
|
1934
|
-
const re = /\[([^\]]*)\]/g;
|
|
1935
|
-
let m: RegExpExecArray | null;
|
|
1936
|
-
while ((m = re.exec(trimmed)) !== null) {
|
|
1937
|
-
const label = m[1]!.trim();
|
|
1938
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
return { kind: 'block', entities };
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// ============================================================
|
|
1946
|
-
// Pyramid extractor
|
|
1947
|
-
// ============================================================
|
|
1948
|
-
|
|
1949
|
-
function extractPyramidSymbols(docText: string): DiagramSymbols {
|
|
1950
|
-
const lines = docText.split('\n');
|
|
1951
|
-
const entities: string[] = [];
|
|
1952
|
-
let pastFirstLine = false;
|
|
1953
|
-
|
|
1954
|
-
for (const line of lines) {
|
|
1955
|
-
const trimmed = line.trim();
|
|
1956
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1957
|
-
|
|
1958
|
-
if (!pastFirstLine) {
|
|
1959
|
-
pastFirstLine = true;
|
|
1960
|
-
continue;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1964
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1965
|
-
if (firstToken === 'inverted' || firstToken === 'solid-fill') continue;
|
|
1966
|
-
|
|
1967
|
-
// Skip indented description lines
|
|
1968
|
-
if (line[0] === ' ' || line[0] === '\t') continue;
|
|
1969
|
-
|
|
1970
|
-
// Layer name (strip pipe metadata)
|
|
1971
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
1972
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
return { kind: 'pyramid', entities };
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// ============================================================
|
|
1979
|
-
// Ring extractor
|
|
1980
|
-
// ============================================================
|
|
1981
|
-
|
|
1982
|
-
function extractRingSymbols(docText: string): DiagramSymbols {
|
|
1983
|
-
const lines = docText.split('\n');
|
|
1984
|
-
const entities: string[] = [];
|
|
1985
|
-
let pastFirstLine = false;
|
|
1986
|
-
|
|
1987
|
-
for (const line of lines) {
|
|
1988
|
-
const trimmed = line.trim();
|
|
1989
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
1990
|
-
|
|
1991
|
-
if (!pastFirstLine) {
|
|
1992
|
-
pastFirstLine = true;
|
|
1993
|
-
continue;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
1997
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
1998
|
-
if (firstToken === 'solid-fill') continue;
|
|
1999
|
-
|
|
2000
|
-
// Skip indented description lines
|
|
2001
|
-
if (line[0] === ' ' || line[0] === '\t') continue;
|
|
2002
|
-
|
|
2003
|
-
// Layer name (strip pipe metadata)
|
|
2004
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
2005
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
return { kind: 'ring', entities };
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
// ============================================================
|
|
2012
|
-
// Arc extractor
|
|
2013
|
-
// ============================================================
|
|
2014
|
-
|
|
2015
|
-
const ARC_ARROW_RE = /^(\S+)\s+(?:->|-[^>]*->)\s+(\S+)/;
|
|
2016
|
-
|
|
2017
|
-
function extractArcSymbols(docText: string): DiagramSymbols {
|
|
2018
|
-
const lines = docText.split('\n');
|
|
2019
|
-
const entities: string[] = [];
|
|
2020
|
-
let pastFirstLine = false;
|
|
2021
|
-
|
|
2022
|
-
for (const line of lines) {
|
|
2023
|
-
const trimmed = line.trim();
|
|
2024
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2025
|
-
|
|
2026
|
-
if (!pastFirstLine) {
|
|
2027
|
-
pastFirstLine = true;
|
|
2028
|
-
continue;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2032
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2033
|
-
|
|
2034
|
-
const arrowMatch = trimmed.match(ARC_ARROW_RE);
|
|
2035
|
-
if (arrowMatch) {
|
|
2036
|
-
const src = arrowMatch[1]!.split('|')[0]!.trim();
|
|
2037
|
-
const dst = arrowMatch[2]!.split('|')[0]!.trim();
|
|
2038
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
2039
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
return { kind: 'arc', entities };
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
// ============================================================
|
|
2047
|
-
// Sankey extractor
|
|
2048
|
-
// ============================================================
|
|
2049
|
-
|
|
2050
|
-
// Sankey links accept both directed `->` and undirected `--` (echarts.ts §1.5).
|
|
2051
|
-
const SANKEY_ARROW_RE = /^(.+?)\s+(?:->|--)\s+(.+?)\s+(\d[\d,_.]*)/;
|
|
2052
|
-
|
|
2053
|
-
function extractSankeySymbols(docText: string): DiagramSymbols {
|
|
2054
|
-
const lines = docText.split('\n');
|
|
2055
|
-
const entities: string[] = [];
|
|
2056
|
-
let pastFirstLine = false;
|
|
2057
|
-
|
|
2058
|
-
for (const line of lines) {
|
|
2059
|
-
const trimmed = line.trim();
|
|
2060
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2061
|
-
|
|
2062
|
-
if (!pastFirstLine) {
|
|
2063
|
-
pastFirstLine = true;
|
|
2064
|
-
continue;
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2068
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2069
|
-
|
|
2070
|
-
const arrowMatch = trimmed.match(SANKEY_ARROW_RE);
|
|
2071
|
-
if (arrowMatch) {
|
|
2072
|
-
const src = arrowMatch[1]!.split('|')[0]!.trim();
|
|
2073
|
-
const dst = arrowMatch[2]!.split('|')[0]!.trim();
|
|
2074
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
2075
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
2076
|
-
} else {
|
|
2077
|
-
// Standalone node declaration (just a name, possibly with color)
|
|
2078
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
2079
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
return { kind: 'sankey', entities };
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
// ============================================================
|
|
2087
|
-
// Timeline extractor
|
|
2088
|
-
// ============================================================
|
|
2089
|
-
|
|
2090
|
-
const TIMELINE_ERA_RE = /^era\s+/i;
|
|
2091
|
-
const TIMELINE_MARKER_RE = /^marker\s+/i;
|
|
2092
|
-
|
|
2093
|
-
const TIMELINE_SCHEDULING_RE = /\b(?:start|end|duration)\s*:/;
|
|
2094
|
-
|
|
2095
|
-
function extractTimelineSymbols(docText: string): DiagramSymbols {
|
|
2096
|
-
const lines = docText.split('\n');
|
|
2097
|
-
const entities: string[] = [];
|
|
2098
|
-
let pastFirstLine = false;
|
|
2099
|
-
let inTagBlock = false;
|
|
2100
|
-
|
|
2101
|
-
for (const line of lines) {
|
|
2102
|
-
const trimmed = line.trim();
|
|
2103
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2104
|
-
|
|
2105
|
-
if (!pastFirstLine) {
|
|
2106
|
-
pastFirstLine = true;
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2111
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2112
|
-
if (TIMELINE_ERA_RE.test(trimmed) || TIMELINE_MARKER_RE.test(trimmed))
|
|
2113
|
-
continue;
|
|
2114
|
-
|
|
2115
|
-
if (/^tag\s+/i.test(trimmed)) {
|
|
2116
|
-
inTagBlock = true;
|
|
2117
|
-
continue;
|
|
2118
|
-
}
|
|
2119
|
-
const indent = line.search(/\S/);
|
|
2120
|
-
if (inTagBlock) {
|
|
2121
|
-
if (indent > 0) continue;
|
|
2122
|
-
inTagBlock = false;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
let label: string;
|
|
2126
|
-
if (TIMELINE_SCHEDULING_RE.test(trimmed)) {
|
|
2127
|
-
label = trimmed
|
|
2128
|
-
.replace(/\b(?:start|end|duration|color|description)\s*:.*$/, '')
|
|
2129
|
-
.split('|')[0]!
|
|
2130
|
-
.trim();
|
|
2131
|
-
} else {
|
|
2132
|
-
label = trimmed
|
|
2133
|
-
.replace(
|
|
2134
|
-
/^\d{4}(?:-\d{2}(?:-\d{2})?)?\s*(?:->\s*\d{4}(?:-\d{2}(?:-\d{2})?)?)?\s*/,
|
|
2135
|
-
''
|
|
2136
|
-
)
|
|
2137
|
-
.split('|')[0]!
|
|
2138
|
-
.trim();
|
|
2139
|
-
}
|
|
2140
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
return { kind: 'timeline', entities };
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
// ============================================================
|
|
2147
|
-
// Venn extractor
|
|
2148
|
-
// ============================================================
|
|
2149
|
-
|
|
2150
|
-
function extractVennSymbols(docText: string): DiagramSymbols {
|
|
2151
|
-
const lines = docText.split('\n');
|
|
2152
|
-
const entities: string[] = [];
|
|
2153
|
-
let pastFirstLine = false;
|
|
2154
|
-
|
|
2155
|
-
for (const line of lines) {
|
|
2156
|
-
const trimmed = line.trim();
|
|
2157
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2158
|
-
|
|
2159
|
-
if (!pastFirstLine) {
|
|
2160
|
-
pastFirstLine = true;
|
|
2161
|
-
continue;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2165
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2166
|
-
|
|
2167
|
-
// Skip indented intersection lines
|
|
2168
|
-
if (line[0] === ' ' || line[0] === '\t') continue;
|
|
2169
|
-
|
|
2170
|
-
// Skip intersection rows — those are references to prior sets, not
|
|
2171
|
-
// declarations. Set NAMES + aliases come from the declaration lines below
|
|
2172
|
-
// (the `+`-completion offers those). Match the parser's detection (d3.ts:
|
|
2173
|
-
// any `+` on the line = intersection), not just the spaced form.
|
|
2174
|
-
if (trimmed.includes('+')) continue;
|
|
2175
|
-
|
|
2176
|
-
// Set declaration: `Name [as <alias>] [color]`. Emit both the clean name
|
|
2177
|
-
// and the alias so `Set + ` reference completion can offer either token.
|
|
2178
|
-
const work = trimmed.split('|')[0]!.trim();
|
|
2179
|
-
const asMatch = work.match(/^(.+?)\s+as\s+([A-Za-z][\w-]*)\b/i);
|
|
2180
|
-
if (asMatch) {
|
|
2181
|
-
const name = asMatch[1]!.trim();
|
|
2182
|
-
const alias = asMatch[2]!;
|
|
2183
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
2184
|
-
if (alias && !entities.includes(alias)) entities.push(alias);
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
// No alias — strip a trailing color token if present.
|
|
2188
|
-
const colorMatch = work.match(/^(.+?)\s+(\S+)$/);
|
|
2189
|
-
const label =
|
|
2190
|
-
colorMatch && RECOGNIZED_COLOR_SET.has(colorMatch[2]!.toLowerCase())
|
|
2191
|
-
? colorMatch[1]!.trim()
|
|
2192
|
-
: work;
|
|
2193
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
return { kind: 'venn', entities };
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
// ============================================================
|
|
2200
|
-
// Quadrant extractor
|
|
2201
|
-
// ============================================================
|
|
2202
|
-
|
|
2203
|
-
const QUADRANT_POSITION_RE =
|
|
2204
|
-
/^(top-right|top-left|bottom-right|bottom-left)\s+/i;
|
|
2205
|
-
|
|
2206
|
-
function extractQuadrantSymbols(docText: string): DiagramSymbols {
|
|
2207
|
-
const lines = docText.split('\n');
|
|
2208
|
-
const entities: string[] = [];
|
|
2209
|
-
let pastFirstLine = false;
|
|
2210
|
-
|
|
2211
|
-
for (const line of lines) {
|
|
2212
|
-
const trimmed = line.trim();
|
|
2213
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2214
|
-
|
|
2215
|
-
if (!pastFirstLine) {
|
|
2216
|
-
pastFirstLine = true;
|
|
2217
|
-
continue;
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2221
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2222
|
-
if (QUADRANT_POSITION_RE.test(trimmed)) continue;
|
|
2223
|
-
|
|
2224
|
-
// Point name (may have coordinates: Name x,y)
|
|
2225
|
-
const parts = trimmed.split(/\s+\d/);
|
|
2226
|
-
const label = (parts[0] ?? '').split('|')[0]!.trim();
|
|
2227
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
return { kind: 'quadrant', entities };
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// ============================================================
|
|
2234
|
-
// Slope extractor
|
|
2235
|
-
// ============================================================
|
|
2236
|
-
|
|
2237
|
-
function extractSlopeSymbols(docText: string): DiagramSymbols {
|
|
2238
|
-
const lines = docText.split('\n');
|
|
2239
|
-
const entities: string[] = [];
|
|
2240
|
-
let pastFirstLine = false;
|
|
2241
|
-
|
|
2242
|
-
for (const line of lines) {
|
|
2243
|
-
const trimmed = line.trim();
|
|
2244
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2245
|
-
|
|
2246
|
-
if (!pastFirstLine) {
|
|
2247
|
-
pastFirstLine = true;
|
|
2248
|
-
continue;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2252
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2253
|
-
if (firstToken === 'period') continue;
|
|
2254
|
-
|
|
2255
|
-
// Data row: Label value1 value2 [color]
|
|
2256
|
-
// Extract just the label (everything before first number)
|
|
2257
|
-
const numIdx = trimmed.search(/\s\d/);
|
|
2258
|
-
const label =
|
|
2259
|
-
numIdx > 0
|
|
2260
|
-
? trimmed.slice(0, numIdx).trim()
|
|
2261
|
-
: trimmed.split('|')[0]!.trim();
|
|
2262
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
return { kind: 'slope', entities };
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
// ============================================================
|
|
2269
|
-
// Generic data chart extractor
|
|
2270
|
-
// ============================================================
|
|
2271
|
-
|
|
2272
|
-
const SERIES_RE = /^series\s+(.+)$/i;
|
|
2273
|
-
// "A -> B value" / "A -- B value" flow-link rows (chord/sankey via the shared
|
|
2274
|
-
// extractor). Both endpoints captured; trailing weight optional.
|
|
2275
|
-
const DATA_EDGE_RE = /^(.+?)\s+(?:->|--)\s+(.+?)(?:\s+-?\d[\d,_.]*)?\s*$/;
|
|
2276
|
-
|
|
2277
|
-
function extractDataChartSymbols(docText: string): DiagramSymbols {
|
|
2278
|
-
const lines = docText.split('\n');
|
|
2279
|
-
const entities: string[] = [];
|
|
2280
|
-
let chartType = 'bar';
|
|
2281
|
-
let pastFirstLine = false;
|
|
2282
|
-
|
|
2283
|
-
for (const line of lines) {
|
|
2284
|
-
const trimmed = line.trim();
|
|
2285
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2286
|
-
|
|
2287
|
-
if (!pastFirstLine) {
|
|
2288
|
-
pastFirstLine = true;
|
|
2289
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2290
|
-
if (firstToken) chartType = firstToken;
|
|
2291
|
-
continue;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2295
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2296
|
-
|
|
2297
|
-
// Series declarations: "series Revenue, Expenses"
|
|
2298
|
-
const seriesMatch = trimmed.match(SERIES_RE);
|
|
2299
|
-
if (seriesMatch) {
|
|
2300
|
-
for (const s of seriesMatch[1]!.split(',')) {
|
|
2301
|
-
const name = s.trim().split(/\s+/)[0]!;
|
|
2302
|
-
if (name && !entities.includes(name)) entities.push(name);
|
|
2303
|
-
}
|
|
2304
|
-
continue;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// Edge rows (chord / flow links): "A -> B value" / "A -- B value" — recover
|
|
2308
|
-
// BOTH endpoints. Without this the trailing-number scan below mangles the
|
|
2309
|
-
// whole "A -> B" into a single junk entity.
|
|
2310
|
-
const edgeMatch = trimmed.match(DATA_EDGE_RE);
|
|
2311
|
-
if (edgeMatch) {
|
|
2312
|
-
const src = edgeMatch[1]!.trim();
|
|
2313
|
-
const dst = edgeMatch[2]!.trim();
|
|
2314
|
-
if (src && !entities.includes(src)) entities.push(src);
|
|
2315
|
-
if (dst && !entities.includes(dst)) entities.push(dst);
|
|
2316
|
-
continue;
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
// Label-expression rows (function curves §15.4): "Name: expression". ONLY
|
|
2320
|
-
// for `function` — other data charts have no colon-form and would otherwise
|
|
2321
|
-
// truncate legitimate colon-bearing labels (`12:00 5` → `12`). Column-0
|
|
2322
|
-
// only, so indented `key: value` metadata is never misread as an entity.
|
|
2323
|
-
if (chartType === 'function') {
|
|
2324
|
-
const indent = line.length - line.trimStart().length;
|
|
2325
|
-
const colonIdx = trimmed.indexOf(':');
|
|
2326
|
-
if (indent === 0 && colonIdx > 0) {
|
|
2327
|
-
const beforeColon = trimmed.slice(0, colonIdx).trim();
|
|
2328
|
-
if (beforeColon && !METADATA_KEY_SET.has(beforeColon.toLowerCase())) {
|
|
2329
|
-
if (!entities.includes(beforeColon)) entities.push(beforeColon);
|
|
2330
|
-
continue;
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
// Data rows: "Label value [value...] [color]"
|
|
2336
|
-
const numIdx = trimmed.search(/\s-?\d/);
|
|
2337
|
-
if (numIdx > 0) {
|
|
2338
|
-
const label = trimmed.slice(0, numIdx).trim();
|
|
2339
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
return { kind: chartType, entities };
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
// ============================================================
|
|
2347
|
-
// Wireframe extractor
|
|
2348
|
-
// ============================================================
|
|
2349
|
-
|
|
2350
|
-
// Wireframe element grammar is not a numeric-row variant, so it earns its own
|
|
2351
|
-
// extractor: `[label]` fields, `(button)`, `{a | b}` dropdowns/selects, with
|
|
2352
|
-
// possibly several elements per line. Returns the element labels as entities.
|
|
2353
|
-
const WF_FIELD_RE = /\[([^\]]*)\]/g;
|
|
2354
|
-
const WF_BUTTON_RE = /\(([^)]+)\)/g;
|
|
2355
|
-
const WF_DROPDOWN_RE = /\{([^}]+)\}/g;
|
|
2356
|
-
|
|
2357
|
-
function extractWireframeSymbols(docText: string): DiagramSymbols {
|
|
2358
|
-
const lines = docText.split('\n');
|
|
2359
|
-
const entities: string[] = [];
|
|
2360
|
-
let pastFirstLine = false;
|
|
2361
|
-
|
|
2362
|
-
const push = (s: string): void => {
|
|
2363
|
-
// Strip any legacy trailing `| meta` inside a bracket; the `{a|b}` pipe is
|
|
2364
|
-
// handled separately by the dropdown split below.
|
|
2365
|
-
const t = s.split('|')[0]!.trim();
|
|
2366
|
-
if (t && !entities.includes(t)) entities.push(t);
|
|
2367
|
-
};
|
|
2368
|
-
|
|
2369
|
-
for (const line of lines) {
|
|
2370
|
-
const trimmed = line.trim();
|
|
2371
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2372
|
-
|
|
2373
|
-
if (!pastFirstLine) {
|
|
2374
|
-
pastFirstLine = true;
|
|
2375
|
-
continue;
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2379
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2380
|
-
|
|
2381
|
-
// `[label]` input/group fields.
|
|
2382
|
-
for (const m of trimmed.matchAll(WF_FIELD_RE)) {
|
|
2383
|
-
const inner = m[1]!.trim();
|
|
2384
|
-
if (inner) push(inner);
|
|
2385
|
-
}
|
|
2386
|
-
// `(button)` — skip radio markers `(*)` / `( )`.
|
|
2387
|
-
for (const m of trimmed.matchAll(WF_BUTTON_RE)) {
|
|
2388
|
-
const inner = m[1]!.trim();
|
|
2389
|
-
if (inner === '*' || inner === '') continue;
|
|
2390
|
-
push(inner);
|
|
2391
|
-
}
|
|
2392
|
-
// `{opt1 | opt2}` dropdown/select — each option is an entity (carve-out pipe).
|
|
2393
|
-
for (const m of trimmed.matchAll(WF_DROPDOWN_RE)) {
|
|
2394
|
-
for (const opt of m[1]!.split('|')) {
|
|
2395
|
-
const t = opt.trim();
|
|
2396
|
-
if (t && !entities.includes(t)) entities.push(t);
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
return { kind: 'wireframe', entities };
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
// ============================================================
|
|
2405
|
-
// Register built-in extractors
|
|
2406
|
-
// ============================================================
|
|
2407
|
-
|
|
2408
|
-
registerExtractor('er', extractErSymbols);
|
|
2409
|
-
registerExtractor('flowchart', extractFlowchartSymbols);
|
|
2410
|
-
registerExtractor('infra', extractInfraSymbols);
|
|
2411
|
-
registerExtractor('class', extractClassSymbols);
|
|
2412
|
-
registerExtractor('sequence', extractSequenceSymbols);
|
|
2413
|
-
registerExtractor('state', extractStateSymbols);
|
|
2414
|
-
registerExtractor('sitemap', extractSitemapSymbols);
|
|
2415
|
-
registerExtractor('c4', extractC4Symbols);
|
|
2416
|
-
registerExtractor('gantt', extractGanttSymbols);
|
|
2417
|
-
registerExtractor('pert', extractPertSymbols);
|
|
2418
|
-
registerExtractor('boxes-and-lines', extractBoxesAndLinesSymbols);
|
|
2419
|
-
registerExtractor('tech-radar', extractTechRadarSymbols);
|
|
2420
|
-
registerExtractor('cycle', extractCycleSymbols);
|
|
2421
|
-
registerExtractor('journey-map', extractJourneyMapSymbols);
|
|
2422
|
-
registerExtractor('raci', extractRaciSymbols);
|
|
2423
|
-
registerExtractor('org', extractOrgSymbols);
|
|
2424
|
-
registerExtractor('kanban', extractKanbanSymbols);
|
|
2425
|
-
registerExtractor('mindmap', extractMindmapSymbols);
|
|
2426
|
-
registerExtractor('treemap', extractTreemapSymbols);
|
|
2427
|
-
registerExtractor('block', extractBlockSymbols);
|
|
2428
|
-
registerExtractor('pyramid', extractPyramidSymbols);
|
|
2429
|
-
registerExtractor('ring', extractRingSymbols);
|
|
2430
|
-
registerExtractor('arc', extractArcSymbols);
|
|
2431
|
-
registerExtractor('sankey', extractSankeySymbols);
|
|
2432
|
-
registerExtractor('timeline', extractTimelineSymbols);
|
|
2433
|
-
registerExtractor('venn', extractVennSymbols);
|
|
2434
|
-
registerExtractor('quadrant', extractQuadrantSymbols);
|
|
2435
|
-
registerExtractor('slope', extractSlopeSymbols);
|
|
2436
|
-
registerExtractor('bar', extractDataChartSymbols);
|
|
2437
|
-
registerExtractor('line', extractDataChartSymbols);
|
|
2438
|
-
registerExtractor('pie', extractDataChartSymbols);
|
|
2439
|
-
registerExtractor('polar-area', extractDataChartSymbols);
|
|
2440
|
-
registerExtractor('radar', extractDataChartSymbols);
|
|
2441
|
-
registerExtractor('scatter', extractDataChartSymbols);
|
|
2442
|
-
registerExtractor('heatmap', extractDataChartSymbols);
|
|
2443
|
-
registerExtractor('funnel', extractDataChartSymbols);
|
|
2444
|
-
// `function` (`Name: expr`) and `wordcloud` (`Word weight`) had NO extractor
|
|
2445
|
-
// registered — extractDiagramSymbols returned null for them. The generalized
|
|
2446
|
-
// shared extractor now handles the colon-label form, so register both.
|
|
2447
|
-
registerExtractor('function', extractDataChartSymbols);
|
|
2448
|
-
registerExtractor('wordcloud', extractDataChartSymbols);
|
|
2449
|
-
registerExtractor('wireframe', extractWireframeSymbols);
|
|
2450
|
-
|
|
2451
|
-
function extractTechRadarSymbols(docText: string): DiagramSymbols {
|
|
2452
|
-
const entities: string[] = [];
|
|
2453
|
-
|
|
2454
|
-
// Extract ring names and aliases from the rings block
|
|
2455
|
-
const lines = docText.split('\n');
|
|
2456
|
-
let inRings = false;
|
|
2457
|
-
for (const line of lines) {
|
|
2458
|
-
const trimmed = line.trim();
|
|
2459
|
-
if (trimmed.toLowerCase() === 'rings') {
|
|
2460
|
-
inRings = true;
|
|
2461
|
-
continue;
|
|
2462
|
-
}
|
|
2463
|
-
if (inRings) {
|
|
2464
|
-
if (!trimmed || (line[0] !== ' ' && line[0] !== '\t')) {
|
|
2465
|
-
inRings = false;
|
|
2466
|
-
continue;
|
|
2467
|
-
}
|
|
2468
|
-
// Parse ring name (and alias)
|
|
2469
|
-
const aliasMatch = trimmed.match(/^(.+?)\s+(?:alias|aka)\s+(\S+)\s*$/i);
|
|
2470
|
-
if (aliasMatch) {
|
|
2471
|
-
// Regex captured groups 1 and 2 by successful match.
|
|
2472
|
-
entities.push(aliasMatch[1]!.trim());
|
|
2473
|
-
entities.push(aliasMatch[2]!.trim());
|
|
2474
|
-
} else {
|
|
2475
|
-
entities.push(trimmed);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
return { kind: 'tech-radar', entities };
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
// ============================================================
|
|
2484
|
-
// Cycle extractor
|
|
2485
|
-
// ============================================================
|
|
2486
|
-
|
|
2487
|
-
function extractCycleSymbols(docText: string): DiagramSymbols {
|
|
2488
|
-
const lines = docText.split('\n');
|
|
2489
|
-
const entities: string[] = [];
|
|
2490
|
-
let pastFirstLine = false;
|
|
2491
|
-
|
|
2492
|
-
for (const line of lines) {
|
|
2493
|
-
const trimmed = line.trim();
|
|
2494
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2495
|
-
|
|
2496
|
-
if (!pastFirstLine) {
|
|
2497
|
-
pastFirstLine = true;
|
|
2498
|
-
continue;
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
// Skip directives/metadata
|
|
2502
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
2503
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2504
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2505
|
-
if (
|
|
2506
|
-
firstToken === 'direction-counterclockwise' ||
|
|
2507
|
-
firstToken === 'circle-nodes'
|
|
2508
|
-
)
|
|
2509
|
-
continue;
|
|
2510
|
-
|
|
2511
|
-
// Skip indented lines (descriptions, edges)
|
|
2512
|
-
if (line[0] === ' ' || line[0] === '\t') continue;
|
|
2513
|
-
|
|
2514
|
-
// Node label (strip pipe metadata)
|
|
2515
|
-
// split('|')[0] always defined on any string.
|
|
2516
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
2517
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
return {
|
|
2521
|
-
kind: 'cycle',
|
|
2522
|
-
entities,
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
// ============================================================
|
|
2527
|
-
// RACI / RASCI / DACI extractor
|
|
2528
|
-
// ============================================================
|
|
2529
|
-
//
|
|
2530
|
-
// Extract role names, task names, and phase labels for editor
|
|
2531
|
-
// autocomplete. Mirrors the lightweight per-line scan pattern used
|
|
2532
|
-
// by other extractors (cycle / journey-map) — does NOT rebuild the
|
|
2533
|
-
// full AST.
|
|
2534
|
-
|
|
2535
|
-
const RACI_PHASE_RE = /^\[(.+)\]\s*$/;
|
|
2536
|
-
const RACI_ROLES_DIRECTIVE_RE = /^roles\s+(.+)$/i;
|
|
2537
|
-
const RACI_VARIANT_DIRECTIVE_RE = /^variant\s+(.+)$/i;
|
|
2538
|
-
const RACI_ROLE_ASSIGNMENT_RE = /^([^:]+):\s*(.*)$/;
|
|
2539
|
-
|
|
2540
|
-
function extractRaciSymbols(docText: string): DiagramSymbols {
|
|
2541
|
-
const lines = docText.split('\n');
|
|
2542
|
-
const entities: string[] = [];
|
|
2543
|
-
let chartType = 'raci';
|
|
2544
|
-
let pastFirstLine = false;
|
|
2545
|
-
let underTask = false;
|
|
2546
|
-
|
|
2547
|
-
const push = (s: string): void => {
|
|
2548
|
-
const trimmed = s.trim();
|
|
2549
|
-
if (trimmed && !entities.includes(trimmed)) entities.push(trimmed);
|
|
2550
|
-
};
|
|
2551
|
-
|
|
2552
|
-
for (const line of lines) {
|
|
2553
|
-
const trimmed = line.trim();
|
|
2554
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2555
|
-
|
|
2556
|
-
if (!pastFirstLine) {
|
|
2557
|
-
pastFirstLine = true;
|
|
2558
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
2559
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2560
|
-
if (firstToken === 'raci') {
|
|
2561
|
-
chartType = firstToken;
|
|
2562
|
-
}
|
|
2563
|
-
continue;
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
const indent = measureIndent(line);
|
|
2567
|
-
|
|
2568
|
-
// Header directives
|
|
2569
|
-
if (indent === 0) {
|
|
2570
|
-
const rolesMatch = trimmed.match(RACI_ROLES_DIRECTIVE_RE);
|
|
2571
|
-
if (rolesMatch) {
|
|
2572
|
-
// Regex captured group 1 by successful match.
|
|
2573
|
-
for (const r of rolesMatch[1]!.split(',')) push(r);
|
|
2574
|
-
continue;
|
|
2575
|
-
}
|
|
2576
|
-
if (RACI_VARIANT_DIRECTIVE_RE.test(trimmed)) continue;
|
|
2577
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
2578
|
-
if (METADATA_KEY_SET.has(trimmed.split(/\s+/)[0]!.toLowerCase()))
|
|
2579
|
-
continue;
|
|
2580
|
-
if (
|
|
2581
|
-
trimmed.toLowerCase() === 'draft' ||
|
|
2582
|
-
trimmed.toLowerCase() === 'solid-fill'
|
|
2583
|
-
)
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
// [Phase Label]
|
|
2588
|
-
const phaseMatch = trimmed.match(RACI_PHASE_RE);
|
|
2589
|
-
if (phaseMatch && indent === 0) {
|
|
2590
|
-
// Regex captured group 1 by successful match.
|
|
2591
|
-
push(phaseMatch[1]!);
|
|
2592
|
-
underTask = false;
|
|
2593
|
-
continue;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
// Role assignment (Role: markers) — only valid under a task
|
|
2597
|
-
const roleMatch = trimmed.match(RACI_ROLE_ASSIGNMENT_RE);
|
|
2598
|
-
if (underTask && roleMatch) {
|
|
2599
|
-
// Strip a possible trailing `# annotation`
|
|
2600
|
-
// Regex captured group 1 by successful match.
|
|
2601
|
-
const rolePart = roleMatch[1]!.trim();
|
|
2602
|
-
push(rolePart);
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
// Otherwise: treat the line as a task name. `#` is NOT a comment
|
|
2607
|
-
// character in DGMO (`//` is) — task names are used verbatim.
|
|
2608
|
-
push(trimmed);
|
|
2609
|
-
underTask = true;
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
return {
|
|
2613
|
-
kind: chartType,
|
|
2614
|
-
entities,
|
|
2615
|
-
};
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
function extractJourneyMapSymbols(docText: string): DiagramSymbols {
|
|
2619
|
-
const lines = docText.split('\n');
|
|
2620
|
-
const entities: string[] = [];
|
|
2621
|
-
let pastFirstLine = false;
|
|
2622
|
-
|
|
2623
|
-
for (const line of lines) {
|
|
2624
|
-
const trimmed = line.trim();
|
|
2625
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
2626
|
-
|
|
2627
|
-
if (!pastFirstLine) {
|
|
2628
|
-
pastFirstLine = true;
|
|
2629
|
-
continue;
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// Skip directives/metadata at indent 0
|
|
2633
|
-
// split(/\s+/) on non-empty `trimmed` always yields at least one element.
|
|
2634
|
-
const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
|
|
2635
|
-
if (METADATA_KEY_SET.has(firstToken)) continue;
|
|
2636
|
-
if (firstToken === 'persona' || firstToken === 'tag') continue;
|
|
2637
|
-
|
|
2638
|
-
const isIndented = line[0] === ' ' || line[0] === '\t';
|
|
2639
|
-
|
|
2640
|
-
// Skip deep-indented lines (annotations, descriptions under steps)
|
|
2641
|
-
// but keep singly-indented lines (steps within phases)
|
|
2642
|
-
if (isIndented) {
|
|
2643
|
-
// Annotation/description keywords — skip
|
|
2644
|
-
if (/^(pain|opportunity|thought|description)\s*:/i.test(trimmed))
|
|
2645
|
-
continue;
|
|
2646
|
-
// Tag group entries — skip
|
|
2647
|
-
if (/^\S+\([^)]+\)/.test(trimmed)) continue;
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
// Phase header
|
|
2651
|
-
const phaseMatch = trimmed.match(/^\[(.+?)\]$/);
|
|
2652
|
-
if (phaseMatch) {
|
|
2653
|
-
// Regex captured group 1 by successful match.
|
|
2654
|
-
entities.push(phaseMatch[1]!.trim());
|
|
2655
|
-
continue;
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
// Step label (strip pipe metadata) — works for both indent 0 and indented steps
|
|
2659
|
-
// split('|')[0] always defined on any string.
|
|
2660
|
-
const label = trimmed.split('|')[0]!.trim();
|
|
2661
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
return {
|
|
2665
|
-
kind: 'journey-map',
|
|
2666
|
-
entities,
|
|
2667
|
-
};
|
|
2668
|
-
}
|