@diagrammo/dgmo 0.47.0 → 0.48.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.js +11 -11
- package/dist/auto.js +16 -16
- package/dist/auto.mjs +41 -12
- package/dist/block.js +4 -4
- package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
- package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
- package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
- package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
- package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
- package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
- package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
- package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
- package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
- package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
- package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
- package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
- package/dist/cli.cjs +15 -15
- package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
- package/dist/element.js +16 -16
- package/dist/element.mjs +41 -12
- package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -4
- package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
- package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
- package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
- package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
- package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
- package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
- package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
- package/package.json +23 -71
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/wireframe/parser.ts
DELETED
|
@@ -1,1114 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Wireframe Diagram Parser
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type { DgmoError } from '../diagnostics';
|
|
6
|
-
import { formatDgmoError, makeDgmoError } from '../diagnostics';
|
|
7
|
-
import type { TagGroup } from '../utils/tag-groups';
|
|
8
|
-
import type { Writable } from '../utils/brand';
|
|
9
|
-
import {
|
|
10
|
-
isTagBlockHeading,
|
|
11
|
-
matchTagBlockHeading,
|
|
12
|
-
validateTagGroupNames,
|
|
13
|
-
stripDefaultModifier,
|
|
14
|
-
finalizeAutoTagColors,
|
|
15
|
-
AUTO_TAG_COLOR_SENTINEL,
|
|
16
|
-
} from '../utils/tag-groups';
|
|
17
|
-
import {
|
|
18
|
-
measureIndent,
|
|
19
|
-
extractColor,
|
|
20
|
-
parseFirstLine,
|
|
21
|
-
OPTION_NOCOLON_RE,
|
|
22
|
-
tryParseSharedOption,
|
|
23
|
-
} from '../utils/parsing';
|
|
24
|
-
import type {
|
|
25
|
-
WireframeElement,
|
|
26
|
-
WireframeElementType,
|
|
27
|
-
WireframeFormFactor,
|
|
28
|
-
ParsedWireframe,
|
|
29
|
-
} from './types';
|
|
30
|
-
|
|
31
|
-
// ============================================================
|
|
32
|
-
// Internal mutable view of WireframeElement for parser construction.
|
|
33
|
-
// Strips readonly from optional array fields that Writable<T> can't reach
|
|
34
|
-
// through `T | undefined` unions.
|
|
35
|
-
// ============================================================
|
|
36
|
-
|
|
37
|
-
type MutableWireframeElement = Omit<
|
|
38
|
-
Writable<WireframeElement>,
|
|
39
|
-
'options' | 'tableHeaders' | 'tableData' | 'inlineElements'
|
|
40
|
-
> & {
|
|
41
|
-
options?: string[];
|
|
42
|
-
tableHeaders?: string[];
|
|
43
|
-
tableData?: string[][];
|
|
44
|
-
inlineElements?: WireframeElement[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ============================================================
|
|
48
|
-
// Constants
|
|
49
|
-
// ============================================================
|
|
50
|
-
|
|
51
|
-
/** Known wireframe option keys (header-phase options before content) */
|
|
52
|
-
const KNOWN_OPTIONS = new Set(['palette', 'theme', 'active-tag']);
|
|
53
|
-
|
|
54
|
-
/** Keywords that only make sense on groups — force group interpretation (EC1).
|
|
55
|
-
* Exported so editor completion can drop them for non-group elements. */
|
|
56
|
-
export const GROUP_ONLY_METADATA = new Set([
|
|
57
|
-
'horizontal',
|
|
58
|
-
'scrollable',
|
|
59
|
-
'collapsed',
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
/** Recognized state keywords for pipe metadata. Exported so editor completion
|
|
63
|
-
* can consume the closed set directly instead of hand-copying it (one-oracle). */
|
|
64
|
-
export const STATE_KEYWORDS = new Set([
|
|
65
|
-
'disabled',
|
|
66
|
-
'active',
|
|
67
|
-
'selected',
|
|
68
|
-
'empty',
|
|
69
|
-
'ghost',
|
|
70
|
-
'destructive',
|
|
71
|
-
'success',
|
|
72
|
-
'warning',
|
|
73
|
-
'info',
|
|
74
|
-
'scrollable',
|
|
75
|
-
'collapsed',
|
|
76
|
-
'toggle',
|
|
77
|
-
'password',
|
|
78
|
-
'textarea',
|
|
79
|
-
'horizontal',
|
|
80
|
-
'primary',
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
/** Element keywords — matched when sole content or followed by recognized params */
|
|
84
|
-
const ELEMENT_KEYWORDS: Record<
|
|
85
|
-
string,
|
|
86
|
-
{ block: boolean; params?: Set<string> }
|
|
87
|
-
> = {
|
|
88
|
-
nav: { block: true },
|
|
89
|
-
tabs: { block: true },
|
|
90
|
-
table: { block: true },
|
|
91
|
-
image: { block: false, params: new Set(['round', 'wide']) },
|
|
92
|
-
modal: { block: true },
|
|
93
|
-
skeleton: { block: true },
|
|
94
|
-
alert: { block: true },
|
|
95
|
-
progress: { block: false },
|
|
96
|
-
chart: { block: false, params: new Set(['line', 'bar', 'pie']) },
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// ============================================================
|
|
100
|
-
// Regexes
|
|
101
|
-
// ============================================================
|
|
102
|
-
|
|
103
|
-
/** Group/input: `[content]` with optional trailing meta (legacy `|` or §1.4 keywords) */
|
|
104
|
-
const BRACKET_RE = /^\[([^\]]*)\](.*)$/;
|
|
105
|
-
|
|
106
|
-
/** Button: `(label)` with optional trailing meta (legacy `|` or §1.4 keywords) */
|
|
107
|
-
const BUTTON_RE = /^\(([^)]+)\)(.*)$/;
|
|
108
|
-
|
|
109
|
-
/** Dropdown: `{opt1 | opt2 | ...}` with optional trailing meta (legacy `|` or §1.4 keywords) */
|
|
110
|
-
const DROPDOWN_RE = /^\{([^}]+)\}(.*)$/;
|
|
111
|
-
|
|
112
|
-
/** Checkbox checked: `<x>` or `< x >` (whitespace-tolerant, EC6) */
|
|
113
|
-
const CHECKBOX_CHECKED_RE = /^<\s*x\s*>$/i;
|
|
114
|
-
|
|
115
|
-
/** Checkbox unchecked: `< >` or `<>` (whitespace-tolerant, EC6) */
|
|
116
|
-
const CHECKBOX_UNCHECKED_RE = /^<\s*>$/;
|
|
117
|
-
|
|
118
|
-
/** Radio selected: `(*) Label` */
|
|
119
|
-
const RADIO_SELECTED_RE = /^\(\*\)\s+(.+)$/;
|
|
120
|
-
|
|
121
|
-
/** Radio unselected: `( ) Label` */
|
|
122
|
-
const RADIO_UNSELECTED_RE = /^\(\s*\)\s+(.+)$/;
|
|
123
|
-
|
|
124
|
-
/** Heading: `# text` or `## text` */
|
|
125
|
-
const HEADING_RE = /^(#{1,2})\s+(.+)$/;
|
|
126
|
-
|
|
127
|
-
/** Divider: `---` (3+ dashes) */
|
|
128
|
-
const DIVIDER_RE = /^-{3,}$/;
|
|
129
|
-
|
|
130
|
-
/** List item: `- text` (anchored to start of segment, F38 fix) */
|
|
131
|
-
const LIST_ITEM_RE = /^-\s+(.+)$/;
|
|
132
|
-
|
|
133
|
-
/** Table skeleton shorthand: `table RxC` */
|
|
134
|
-
const TABLE_SKELETON_RE = /^table\s+(\d+)x(\d+)$/i;
|
|
135
|
-
|
|
136
|
-
// ============================================================
|
|
137
|
-
// Helpers
|
|
138
|
-
// ============================================================
|
|
139
|
-
|
|
140
|
-
/** Generate a deterministic ID from line number and indent. */
|
|
141
|
-
function genId(lineNumber: number, indent: number, suffix = ''): string {
|
|
142
|
-
return `wf-${lineNumber}-${indent}${suffix ? '-' + suffix : ''}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** @deprecated No-op — IDs are now deterministic from line/indent. Kept for test compat. */
|
|
146
|
-
export function resetWireframeIds(): void {
|
|
147
|
-
// no-op
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function makeElement(
|
|
151
|
-
type: WireframeElementType,
|
|
152
|
-
label: string,
|
|
153
|
-
lineNumber: number,
|
|
154
|
-
indent: number
|
|
155
|
-
): MutableWireframeElement {
|
|
156
|
-
return {
|
|
157
|
-
id: genId(lineNumber, indent),
|
|
158
|
-
type,
|
|
159
|
-
label,
|
|
160
|
-
children: [],
|
|
161
|
-
metadata: {},
|
|
162
|
-
states: [],
|
|
163
|
-
annotations: [],
|
|
164
|
-
lineNumber,
|
|
165
|
-
indent,
|
|
166
|
-
isContainer: false,
|
|
167
|
-
orientation: 'vertical',
|
|
168
|
-
isSkeleton: false,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Peel the trailing run of state-keyword tokens from a label
|
|
174
|
-
* (the §1.4 / §19.5 wireframe form). Lowercase-only tokens drawn
|
|
175
|
-
* from the closed `STATE_KEYWORDS` enum are stripped from the
|
|
176
|
-
* right of the label; peeling stops at the first non-enum token.
|
|
177
|
-
*
|
|
178
|
-
* `Submit primary destructive` → label="Submit", states=["primary","destructive"]
|
|
179
|
-
* `Dashboard active` → label="Dashboard", states=["active"]
|
|
180
|
-
* `Active items active` → label="Active items", states=["active"]
|
|
181
|
-
* `Toggle me` → label="Toggle me", states=[] (no trailing enum token)
|
|
182
|
-
*
|
|
183
|
-
* Case-sensitive match (lowercase enum) — authors capitalize their
|
|
184
|
-
* labels to disambiguate (spec §19.5).
|
|
185
|
-
*/
|
|
186
|
-
function peelTrailingFlags(label: string): {
|
|
187
|
-
label: string;
|
|
188
|
-
states: string[];
|
|
189
|
-
} {
|
|
190
|
-
const tokens = label.split(/\s+/).filter(Boolean);
|
|
191
|
-
const states: string[] = [];
|
|
192
|
-
while (tokens.length > 0) {
|
|
193
|
-
const last = tokens[tokens.length - 1]!;
|
|
194
|
-
// Only lowercase tokens count — `Active` (capitalized) stays in
|
|
195
|
-
// the label, `active` is a flag.
|
|
196
|
-
if (last !== last.toLowerCase()) break;
|
|
197
|
-
if (!STATE_KEYWORDS.has(last)) break;
|
|
198
|
-
states.unshift(last);
|
|
199
|
-
tokens.pop();
|
|
200
|
-
}
|
|
201
|
-
return { label: tokens.join(' '), states };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Parse pipe metadata flags/annotations from the text after `|`.
|
|
206
|
-
* Returns states array + annotations array + metadata record.
|
|
207
|
-
*/
|
|
208
|
-
function parseWireframeMetadata(raw: string): {
|
|
209
|
-
states: string[];
|
|
210
|
-
annotations: string[];
|
|
211
|
-
metadata: Record<string, string>;
|
|
212
|
-
} {
|
|
213
|
-
const states: string[] = [];
|
|
214
|
-
const annotations: string[] = [];
|
|
215
|
-
const metadata: Record<string, string> = {};
|
|
216
|
-
|
|
217
|
-
const parts = raw
|
|
218
|
-
.split(',')
|
|
219
|
-
.map((s) => s.trim())
|
|
220
|
-
.filter(Boolean);
|
|
221
|
-
for (const part of parts) {
|
|
222
|
-
const lower = part.toLowerCase();
|
|
223
|
-
if (STATE_KEYWORDS.has(lower)) {
|
|
224
|
-
states.push(lower);
|
|
225
|
-
} else if (part.includes(':')) {
|
|
226
|
-
const [key, ...rest] = part.split(':');
|
|
227
|
-
// In-bounds: split(':') on string containing ':' returns at least 2 elements.
|
|
228
|
-
metadata[key!.trim()] = rest.join(':').trim();
|
|
229
|
-
} else {
|
|
230
|
-
annotations.push(part);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return { states, annotations, metadata };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Apply the trailing meta region of a structural element (the
|
|
238
|
-
* content captured after `]`, `)`, or `}` by BRACKET_RE / BUTTON_RE
|
|
239
|
-
* / DROPDOWN_RE). Accepts both:
|
|
240
|
-
* - legacy: ` | flag1, flag2` (leading `|`, comma-separated)
|
|
241
|
-
* - new (§19.5): ` flag1 flag2` (space-separated keyword list)
|
|
242
|
-
* No-ops when the tail is empty.
|
|
243
|
-
*/
|
|
244
|
-
function applyTrailingMeta(
|
|
245
|
-
el: MutableWireframeElement,
|
|
246
|
-
rawTail: string | undefined
|
|
247
|
-
): void {
|
|
248
|
-
if (!rawTail) return;
|
|
249
|
-
const trimmed = rawTail.trim();
|
|
250
|
-
if (!trimmed) return;
|
|
251
|
-
if (trimmed.startsWith('|')) {
|
|
252
|
-
applyMetadata(el, trimmed.substring(1).trim());
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// §1.4 / §19.5 trailing-keyword form: every token must be a
|
|
256
|
-
// recognized state-keyword (lowercase). Non-enum tokens fall
|
|
257
|
-
// through to applyMetadata so existing semantics (annotations,
|
|
258
|
-
// `key: value`) still work for content that happens to land here.
|
|
259
|
-
const tokens = trimmed.split(/\s+/);
|
|
260
|
-
if (tokens.every((t) => t === t.toLowerCase() && STATE_KEYWORDS.has(t))) {
|
|
261
|
-
applyMetadata(el, tokens.join(', '));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
applyMetadata(el, trimmed);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Apply parsed metadata to an element.
|
|
269
|
-
*/
|
|
270
|
-
function applyMetadata(
|
|
271
|
-
el: MutableWireframeElement,
|
|
272
|
-
metaStr: string | undefined
|
|
273
|
-
): void {
|
|
274
|
-
if (!metaStr) return;
|
|
275
|
-
const { states, annotations, metadata } = parseWireframeMetadata(metaStr);
|
|
276
|
-
el.states.push(...states);
|
|
277
|
-
el.annotations.push(...annotations);
|
|
278
|
-
Object.assign(el.metadata, metadata);
|
|
279
|
-
|
|
280
|
-
// Field variants
|
|
281
|
-
if (el.type === 'textInput') {
|
|
282
|
-
if (states.includes('password')) el.fieldVariant = 'password';
|
|
283
|
-
if (states.includes('textarea')) el.fieldVariant = 'textarea';
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Group-only metadata forces container (EC1)
|
|
287
|
-
for (const s of states) {
|
|
288
|
-
if (GROUP_ONLY_METADATA.has(s)) {
|
|
289
|
-
el.isContainer = true;
|
|
290
|
-
if (s === 'horizontal') el.orientation = 'horizontal';
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Detect if a segment is a keyword element.
|
|
297
|
-
* Returns { keyword, params } or null.
|
|
298
|
-
* F9: keyword matched only when sole content or followed by recognized param.
|
|
299
|
-
*/
|
|
300
|
-
function matchKeyword(segment: string): {
|
|
301
|
-
keyword: string;
|
|
302
|
-
type: WireframeElementType;
|
|
303
|
-
param?: string;
|
|
304
|
-
} | null {
|
|
305
|
-
const words = segment.split(/\s+/);
|
|
306
|
-
// In-bounds: split always returns at least one element.
|
|
307
|
-
const first = words[0]!.toLowerCase();
|
|
308
|
-
const kw = ELEMENT_KEYWORDS[first];
|
|
309
|
-
if (!kw) return null;
|
|
310
|
-
|
|
311
|
-
// Sole content — just the keyword
|
|
312
|
-
if (words.length === 1) {
|
|
313
|
-
return { keyword: first, type: first as WireframeElementType };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const rest = words.slice(1).join(' ');
|
|
317
|
-
|
|
318
|
-
// `modal Title` — modal always takes the rest as title
|
|
319
|
-
if (first === 'modal') {
|
|
320
|
-
return { keyword: first, type: 'modal', param: rest };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// `progress 60` — takes a number
|
|
324
|
-
if (first === 'progress') {
|
|
325
|
-
const num = parseInt(rest, 10);
|
|
326
|
-
if (!isNaN(num)) {
|
|
327
|
-
return { keyword: first, type: 'progress', param: rest };
|
|
328
|
-
}
|
|
329
|
-
return null; // `progress bar` → bare text
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// `table 5x4` — skeleton shorthand
|
|
333
|
-
if (first === 'table') {
|
|
334
|
-
if (TABLE_SKELETON_RE.test(segment)) {
|
|
335
|
-
return { keyword: first, type: 'table', param: rest };
|
|
336
|
-
}
|
|
337
|
-
// `table` followed by content → still a table block
|
|
338
|
-
return { keyword: first, type: 'table' };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Keywords with recognized param sets
|
|
342
|
-
if (kw.params) {
|
|
343
|
-
if (kw.params.has(rest.toLowerCase())) {
|
|
344
|
-
return {
|
|
345
|
-
keyword: first,
|
|
346
|
-
type: first as WireframeElementType,
|
|
347
|
-
param: rest.toLowerCase(),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
// Unrecognized trailing words → bare text (F9)
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Block keywords with unrecognized trailing → bare text
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Parse a single segment (after multi-element line splitting) into an element.
|
|
360
|
-
*/
|
|
361
|
-
function parseSegment(
|
|
362
|
-
segment: string,
|
|
363
|
-
lineNumber: number,
|
|
364
|
-
indent: number,
|
|
365
|
-
diagnostics: DgmoError[]
|
|
366
|
-
): MutableWireframeElement | null {
|
|
367
|
-
const trimmed = segment.trim();
|
|
368
|
-
if (!trimmed) return null;
|
|
369
|
-
|
|
370
|
-
// Heading: `# text` or `## text`
|
|
371
|
-
const headingMatch = trimmed.match(HEADING_RE);
|
|
372
|
-
if (headingMatch) {
|
|
373
|
-
// In-bounds: HEADING_RE has 2 capture groups; matched groups are defined.
|
|
374
|
-
const level = headingMatch[1]!.length as 1 | 2;
|
|
375
|
-
const el = makeElement(
|
|
376
|
-
'heading',
|
|
377
|
-
headingMatch[2]!.trim(),
|
|
378
|
-
lineNumber,
|
|
379
|
-
indent
|
|
380
|
-
);
|
|
381
|
-
el.headingLevel = level;
|
|
382
|
-
return el;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Divider: `---`
|
|
386
|
-
if (DIVIDER_RE.test(trimmed)) {
|
|
387
|
-
return makeElement('divider', '', lineNumber, indent);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Checkbox checked: `<x>` (EC6 — strict pattern)
|
|
391
|
-
if (CHECKBOX_CHECKED_RE.test(trimmed)) {
|
|
392
|
-
const el = makeElement('checkbox', '', lineNumber, indent);
|
|
393
|
-
el.checked = true;
|
|
394
|
-
return el;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Checkbox unchecked: `< >`
|
|
398
|
-
if (CHECKBOX_UNCHECKED_RE.test(trimmed)) {
|
|
399
|
-
const el = makeElement('checkbox', '', lineNumber, indent);
|
|
400
|
-
el.checked = false;
|
|
401
|
-
return el;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Radio selected: `(*) Label`
|
|
405
|
-
const radioSelMatch = trimmed.match(RADIO_SELECTED_RE);
|
|
406
|
-
if (radioSelMatch) {
|
|
407
|
-
const el = makeElement(
|
|
408
|
-
'radio',
|
|
409
|
-
// In-bounds: regex has one capture group; matched group is defined.
|
|
410
|
-
radioSelMatch[1]!.trim(),
|
|
411
|
-
lineNumber,
|
|
412
|
-
indent
|
|
413
|
-
);
|
|
414
|
-
el.selected = true;
|
|
415
|
-
return el;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Radio unselected: `( ) Label`
|
|
419
|
-
const radioUnselMatch = trimmed.match(RADIO_UNSELECTED_RE);
|
|
420
|
-
if (radioUnselMatch) {
|
|
421
|
-
const el = makeElement(
|
|
422
|
-
'radio',
|
|
423
|
-
// In-bounds: regex has one capture group; matched group is defined.
|
|
424
|
-
radioUnselMatch[1]!.trim(),
|
|
425
|
-
lineNumber,
|
|
426
|
-
indent
|
|
427
|
-
);
|
|
428
|
-
el.selected = false;
|
|
429
|
-
return el;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Checkbox with label: `<x> Label` or `< > Label`
|
|
433
|
-
const checkLabelMatch = trimmed.match(/^(<\s*x?\s*>)\s+(.+)$/i);
|
|
434
|
-
if (checkLabelMatch) {
|
|
435
|
-
// In-bounds: regex has 2 capture groups; matched groups are defined.
|
|
436
|
-
const isChecked = /x/i.test(checkLabelMatch[1]!);
|
|
437
|
-
const labelPart = checkLabelMatch[2]!;
|
|
438
|
-
// Legacy `| meta` first, then §1.4 trailing-keyword peel on the
|
|
439
|
-
// remaining label region.
|
|
440
|
-
const pipeSplit = labelPart.split(/\s*\|\s*/);
|
|
441
|
-
const baseLabel = pipeSplit[0]!.trim();
|
|
442
|
-
const peeled = peelTrailingFlags(baseLabel);
|
|
443
|
-
const el = makeElement('checkbox', peeled.label, lineNumber, indent);
|
|
444
|
-
el.checked = isChecked;
|
|
445
|
-
if (peeled.states.length > 0) {
|
|
446
|
-
applyMetadata(el, peeled.states.join(', '));
|
|
447
|
-
}
|
|
448
|
-
if (pipeSplit.length > 1) {
|
|
449
|
-
applyMetadata(el, pipeSplit.slice(1).join(', '));
|
|
450
|
-
}
|
|
451
|
-
return el;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Dropdown: `{opt1 | opt2 | opt3}` (optional trailing metadata after closing brace)
|
|
455
|
-
const dropdownMatch = trimmed.match(DROPDOWN_RE);
|
|
456
|
-
if (dropdownMatch) {
|
|
457
|
-
// In-bounds: DROPDOWN_RE has 2 capture groups; first is required by match.
|
|
458
|
-
const options = dropdownMatch[1]!
|
|
459
|
-
.split('|')
|
|
460
|
-
.map((s) => s.trim())
|
|
461
|
-
.filter(Boolean);
|
|
462
|
-
const el = makeElement('dropdown', options[0] || '', lineNumber, indent);
|
|
463
|
-
el.options = options;
|
|
464
|
-
applyTrailingMeta(el, dropdownMatch[2]);
|
|
465
|
-
return el;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Parens with pipes — likely meant dropdown (AC25) — check before button match
|
|
469
|
-
const parenPipeMatch = trimmed.match(/^\(([^)]*\|[^)]*)\)\s*$/);
|
|
470
|
-
if (parenPipeMatch) {
|
|
471
|
-
diagnostics.push(
|
|
472
|
-
makeDgmoError(
|
|
473
|
-
lineNumber,
|
|
474
|
-
// In-bounds: regex has one capture group; matched group is defined.
|
|
475
|
-
`Did you mean a dropdown? Use braces: {${parenPipeMatch[1]!}} instead of (${parenPipeMatch[1]!})`,
|
|
476
|
-
'warning'
|
|
477
|
-
)
|
|
478
|
-
);
|
|
479
|
-
const options = parenPipeMatch[1]!
|
|
480
|
-
.split('|')
|
|
481
|
-
.map((s) => s.trim())
|
|
482
|
-
.filter(Boolean);
|
|
483
|
-
const el = makeElement('dropdown', options[0] || '', lineNumber, indent);
|
|
484
|
-
el.options = options;
|
|
485
|
-
return el;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Button: `(label)` (must come after radio/checkbox checks)
|
|
489
|
-
const buttonMatch = trimmed.match(BUTTON_RE);
|
|
490
|
-
if (buttonMatch) {
|
|
491
|
-
// In-bounds: BUTTON_RE has 2 capture groups; first is required by match.
|
|
492
|
-
const el = makeElement(
|
|
493
|
-
'button',
|
|
494
|
-
buttonMatch[1]!.trim(),
|
|
495
|
-
lineNumber,
|
|
496
|
-
indent
|
|
497
|
-
);
|
|
498
|
-
applyTrailingMeta(el, buttonMatch[2]);
|
|
499
|
-
return el;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Group/Input: `[content]`
|
|
503
|
-
const bracketMatch = trimmed.match(BRACKET_RE);
|
|
504
|
-
if (bracketMatch) {
|
|
505
|
-
// Will be disambiguated as group vs input later (by children or EC1 metadata)
|
|
506
|
-
// In-bounds: BRACKET_RE has 2 capture groups; first is required by match.
|
|
507
|
-
const el = makeElement(
|
|
508
|
-
'group',
|
|
509
|
-
bracketMatch[1]!.trim(),
|
|
510
|
-
lineNumber,
|
|
511
|
-
indent
|
|
512
|
-
);
|
|
513
|
-
applyTrailingMeta(el, bracketMatch[2]);
|
|
514
|
-
// If no group-forcing metadata applied, default to textInput — will be
|
|
515
|
-
// overridden to 'group' if children are added during indent-stack processing
|
|
516
|
-
if (!el.isContainer) {
|
|
517
|
-
el.type = 'textInput';
|
|
518
|
-
// Apply field variants now that type is set
|
|
519
|
-
if (el.states.includes('password')) el.fieldVariant = 'password';
|
|
520
|
-
if (el.states.includes('textarea')) el.fieldVariant = 'textarea';
|
|
521
|
-
}
|
|
522
|
-
return el;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// List item: `- text` (F38: anchored to segment start)
|
|
526
|
-
const listMatch = trimmed.match(LIST_ITEM_RE);
|
|
527
|
-
if (listMatch) {
|
|
528
|
-
// In-bounds: LIST_ITEM_RE has one capture group; matched group is defined.
|
|
529
|
-
return makeElement('listItem', listMatch[1]!.trim(), lineNumber, indent);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Keyword elements
|
|
533
|
-
const kwMatch = matchKeyword(trimmed);
|
|
534
|
-
if (kwMatch) {
|
|
535
|
-
const el = makeElement(
|
|
536
|
-
kwMatch.type,
|
|
537
|
-
kwMatch.param || '',
|
|
538
|
-
lineNumber,
|
|
539
|
-
indent
|
|
540
|
-
);
|
|
541
|
-
if (kwMatch.type === 'image') {
|
|
542
|
-
el.imageHint = (kwMatch.param as 'round' | 'wide') || 'default';
|
|
543
|
-
} else if (kwMatch.type === 'progress') {
|
|
544
|
-
const val = parseInt(kwMatch.param || '0', 10);
|
|
545
|
-
el.progressValue = Math.max(0, Math.min(100, isNaN(val) ? 0 : val));
|
|
546
|
-
} else if (kwMatch.type === 'chart') {
|
|
547
|
-
el.chartHint = (kwMatch.param as 'line' | 'bar' | 'pie') || 'line';
|
|
548
|
-
} else if (kwMatch.type === 'table') {
|
|
549
|
-
// Check for skeleton shorthand
|
|
550
|
-
const skelMatch = `table ${kwMatch.param || ''}`.match(TABLE_SKELETON_RE);
|
|
551
|
-
if (skelMatch) {
|
|
552
|
-
// In-bounds: TABLE_SKELETON_RE has 2 capture groups; matched groups are defined.
|
|
553
|
-
el.tableRows = parseInt(skelMatch[1]!, 10);
|
|
554
|
-
el.tableCols = parseInt(skelMatch[2]!, 10);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
// Block keywords become containers
|
|
558
|
-
if (ELEMENT_KEYWORDS[kwMatch.keyword]?.block) {
|
|
559
|
-
el.isContainer = true;
|
|
560
|
-
}
|
|
561
|
-
return el;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Unmatched opening brackets (before pipe split, which would interfere)
|
|
565
|
-
if (/^\[[^\]]*$/.test(trimmed)) {
|
|
566
|
-
diagnostics.push(
|
|
567
|
-
makeDgmoError(
|
|
568
|
-
lineNumber,
|
|
569
|
-
`Unmatched '[' — auto-closing bracket`,
|
|
570
|
-
'warning'
|
|
571
|
-
)
|
|
572
|
-
);
|
|
573
|
-
const label = trimmed.substring(1).trim();
|
|
574
|
-
return makeElement('textInput', label, lineNumber, indent);
|
|
575
|
-
}
|
|
576
|
-
if (/^\([^)]*$/.test(trimmed) && !trimmed.match(/^\(\s*\*?\s*\)/)) {
|
|
577
|
-
diagnostics.push(
|
|
578
|
-
makeDgmoError(
|
|
579
|
-
lineNumber,
|
|
580
|
-
`Unmatched '(' — auto-closing bracket`,
|
|
581
|
-
'warning'
|
|
582
|
-
)
|
|
583
|
-
);
|
|
584
|
-
const label = trimmed.substring(1).trim();
|
|
585
|
-
return makeElement('button', label, lineNumber, indent);
|
|
586
|
-
}
|
|
587
|
-
if (/^\{[^}]*$/.test(trimmed)) {
|
|
588
|
-
diagnostics.push(
|
|
589
|
-
makeDgmoError(
|
|
590
|
-
lineNumber,
|
|
591
|
-
`Unmatched '{' — auto-closing bracket`,
|
|
592
|
-
'warning'
|
|
593
|
-
)
|
|
594
|
-
);
|
|
595
|
-
const rawContent = trimmed.substring(1).trim();
|
|
596
|
-
const options = rawContent
|
|
597
|
-
.split('|')
|
|
598
|
-
.map((s) => s.trim())
|
|
599
|
-
.filter(Boolean);
|
|
600
|
-
const el = makeElement('dropdown', options[0] || '', lineNumber, indent);
|
|
601
|
-
el.options = options;
|
|
602
|
-
return el;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Bare text — optional legacy `| meta` form first, otherwise
|
|
606
|
-
// peel the §1.4 / §19.5 trailing-keyword form.
|
|
607
|
-
const pipeParts = trimmed.split(/\s*\|\s*/);
|
|
608
|
-
let textContent: string;
|
|
609
|
-
let states: string[];
|
|
610
|
-
let metaStr: string | undefined;
|
|
611
|
-
if (pipeParts.length > 1) {
|
|
612
|
-
// Legacy `text | meta` form. The pipe diagnostic still fires
|
|
613
|
-
// from the top-of-loop check; we keep extracting for back-compat.
|
|
614
|
-
textContent = pipeParts[0]!.trim();
|
|
615
|
-
metaStr = pipeParts.slice(1).join(', ');
|
|
616
|
-
states = parseWireframeMetadata(metaStr).states;
|
|
617
|
-
} else {
|
|
618
|
-
// §1.4 / §19.5 trailing-keyword peel.
|
|
619
|
-
const peeled = peelTrailingFlags(trimmed);
|
|
620
|
-
textContent = peeled.label;
|
|
621
|
-
states = peeled.states;
|
|
622
|
-
metaStr = states.length > 0 ? states.join(', ') : undefined;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Bare text + semantic state = inline alert (F22)
|
|
626
|
-
const semanticStates = ['warning', 'destructive', 'success', 'info'];
|
|
627
|
-
const hasSemantic = states.some((s) => semanticStates.includes(s));
|
|
628
|
-
if (hasSemantic) {
|
|
629
|
-
const el = makeElement('alert', textContent, lineNumber, indent);
|
|
630
|
-
el.states = states;
|
|
631
|
-
return el;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (metaStr) {
|
|
635
|
-
const el = makeElement('text', textContent, lineNumber, indent);
|
|
636
|
-
applyMetadata(el, metaStr);
|
|
637
|
-
return el;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return makeElement('text', trimmed, lineNumber, indent);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Split a line into segments on 2+ space boundaries (multi-element line rule).
|
|
645
|
-
* Single space is same element.
|
|
646
|
-
*/
|
|
647
|
-
function splitLineSegments(line: string): string[] {
|
|
648
|
-
return line.split(/\s{2,}/).filter(Boolean);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// ============================================================
|
|
652
|
-
// Main Parser
|
|
653
|
-
// ============================================================
|
|
654
|
-
|
|
655
|
-
export function parseWireframe(content: string): ParsedWireframe {
|
|
656
|
-
resetWireframeIds();
|
|
657
|
-
|
|
658
|
-
const diagnostics: DgmoError[] = [];
|
|
659
|
-
const lines = content.split('\n');
|
|
660
|
-
|
|
661
|
-
let title: string | null = null;
|
|
662
|
-
let titleLineNumber: number | null = null;
|
|
663
|
-
let formFactor: WireframeFormFactor = 'desktop';
|
|
664
|
-
const roots: MutableWireframeElement[] = [];
|
|
665
|
-
const modals: MutableWireframeElement[] = [];
|
|
666
|
-
const tagGroups: TagGroup[] = [];
|
|
667
|
-
const options: Record<string, string> = {};
|
|
668
|
-
|
|
669
|
-
// Parsing state
|
|
670
|
-
let phase: 'header' | 'tags' | 'content' = 'header';
|
|
671
|
-
let currentTagGroup: Writable<TagGroup> | null = null;
|
|
672
|
-
|
|
673
|
-
function pushWarning(line: number, msg: string): void {
|
|
674
|
-
diagnostics.push(makeDgmoError(line, msg, 'warning'));
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function makeTagGroup(
|
|
678
|
-
trimmed: string,
|
|
679
|
-
lineNumber: number
|
|
680
|
-
): Writable<TagGroup> | null {
|
|
681
|
-
const match = matchTagBlockHeading(trimmed);
|
|
682
|
-
if (!match) return null;
|
|
683
|
-
return {
|
|
684
|
-
name: match.name,
|
|
685
|
-
...(match.alias !== undefined && { alias: match.alias }),
|
|
686
|
-
entries: [],
|
|
687
|
-
lineNumber,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Indent stack for hierarchy
|
|
692
|
-
const indentStack: { node: MutableWireframeElement; indent: number }[] = [];
|
|
693
|
-
|
|
694
|
-
function findParent(indent: number): MutableWireframeElement | null {
|
|
695
|
-
// Pop nodes at same or deeper indent
|
|
696
|
-
while (
|
|
697
|
-
indentStack.length > 0 &&
|
|
698
|
-
// In-bounds by length-check guard above.
|
|
699
|
-
indentStack[indentStack.length - 1]!.indent >= indent
|
|
700
|
-
) {
|
|
701
|
-
const popped = indentStack.pop()!;
|
|
702
|
-
// When a node had children pushed to it, mark as container
|
|
703
|
-
if (popped.node.children.length > 0 && !popped.node.isContainer) {
|
|
704
|
-
popped.node.isContainer = true;
|
|
705
|
-
// If it was textInput (from bracket detection), upgrade to group
|
|
706
|
-
if (popped.node.type === 'textInput') {
|
|
707
|
-
popped.node.type = 'group';
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
return indentStack.length > 0
|
|
712
|
-
? // In-bounds by length-check.
|
|
713
|
-
indentStack[indentStack.length - 1]!.node
|
|
714
|
-
: null;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function pushElement(el: MutableWireframeElement): void {
|
|
718
|
-
// Modal elements go to separate array
|
|
719
|
-
if (el.type === 'modal') {
|
|
720
|
-
modals.push(el);
|
|
721
|
-
indentStack.push({ node: el, indent: el.indent });
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Find parent based on indent
|
|
726
|
-
findParent(el.indent);
|
|
727
|
-
|
|
728
|
-
if (indentStack.length === 0) {
|
|
729
|
-
roots.push(el);
|
|
730
|
-
} else {
|
|
731
|
-
// In-bounds by length-check above.
|
|
732
|
-
const parent = indentStack[indentStack.length - 1]!.node;
|
|
733
|
-
parent.children.push(el);
|
|
734
|
-
// Propagate skeleton flag
|
|
735
|
-
if (parent.type === 'skeleton' || parent.isSkeleton) {
|
|
736
|
-
el.isSkeleton = true;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Push onto stack if it could be a container
|
|
741
|
-
if (
|
|
742
|
-
el.isContainer ||
|
|
743
|
-
el.type === 'group' ||
|
|
744
|
-
el.type === 'textInput' || // might become group if children follow
|
|
745
|
-
el.type === 'nav' ||
|
|
746
|
-
el.type === 'tabs' ||
|
|
747
|
-
el.type === 'table' ||
|
|
748
|
-
el.type === 'skeleton' ||
|
|
749
|
-
el.type === 'alert'
|
|
750
|
-
) {
|
|
751
|
-
indentStack.push({ node: el, indent: el.indent });
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function pushInlineRow(
|
|
756
|
-
segments: string[],
|
|
757
|
-
lineNumber: number,
|
|
758
|
-
indent: number,
|
|
759
|
-
diags: DgmoError[]
|
|
760
|
-
): void {
|
|
761
|
-
const children: MutableWireframeElement[] = [];
|
|
762
|
-
let lastEl: MutableWireframeElement | null = null;
|
|
763
|
-
for (const seg of segments) {
|
|
764
|
-
// EC5: segment starting with `|` attaches to previous element
|
|
765
|
-
if (seg.startsWith('|') && lastEl) {
|
|
766
|
-
applyMetadata(lastEl, seg.substring(1).trim());
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
const el = parseSegment(seg, lineNumber, indent, diags);
|
|
770
|
-
if (el) {
|
|
771
|
-
children.push(el);
|
|
772
|
-
lastEl = el;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
if (children.length === 0) return;
|
|
776
|
-
if (children.length === 1) {
|
|
777
|
-
// In-bounds by length-check above.
|
|
778
|
-
pushElement(children[0]!);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
// Wrap in a horizontal inline row
|
|
782
|
-
const wrapper = makeElement('group', '', lineNumber, indent);
|
|
783
|
-
wrapper.id = genId(lineNumber, indent, 'row');
|
|
784
|
-
wrapper.isContainer = true;
|
|
785
|
-
wrapper.orientation = 'horizontal';
|
|
786
|
-
wrapper.children = children;
|
|
787
|
-
const wrapperMetadata: Record<string, string> = { _inlineRow: 'true' };
|
|
788
|
-
wrapper.metadata = wrapperMetadata;
|
|
789
|
-
pushElement(wrapper);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
for (let i = 0; i < lines.length; i++) {
|
|
793
|
-
// In-bounds by loop guard.
|
|
794
|
-
const line = lines[i]!;
|
|
795
|
-
const lineNumber = i + 1;
|
|
796
|
-
const trimmed = line.trim();
|
|
797
|
-
|
|
798
|
-
// Skip empty lines and comments
|
|
799
|
-
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
800
|
-
|
|
801
|
-
const indent = measureIndent(line);
|
|
802
|
-
|
|
803
|
-
// ── Header phase ──────────────────────────────────────
|
|
804
|
-
if (phase === 'header') {
|
|
805
|
-
// First line: chart type declaration
|
|
806
|
-
const firstLineResult = parseFirstLine(trimmed);
|
|
807
|
-
if (firstLineResult?.chartType === 'wireframe') {
|
|
808
|
-
title = firstLineResult.title || null;
|
|
809
|
-
titleLineNumber = lineNumber;
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Options: `mobile`, `palette xxx`, `theme xxx`
|
|
814
|
-
if (trimmed === 'mobile') {
|
|
815
|
-
formFactor = 'mobile';
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (tryParseSharedOption(trimmed, options)) {
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
824
|
-
// In-bounds: OPTION_NOCOLON_RE has 2 capture groups; first is required by match.
|
|
825
|
-
if (optMatch && KNOWN_OPTIONS.has(optMatch[1]!.toLowerCase())) {
|
|
826
|
-
options[optMatch[1]!] = optMatch[2] || '';
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Tag group heading
|
|
831
|
-
if (isTagBlockHeading(trimmed)) {
|
|
832
|
-
const tg = makeTagGroup(trimmed, lineNumber);
|
|
833
|
-
if (tg) {
|
|
834
|
-
currentTagGroup = tg;
|
|
835
|
-
tagGroups.push(tg);
|
|
836
|
-
phase = 'tags';
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Not a header line — switch to content phase
|
|
842
|
-
phase = 'content';
|
|
843
|
-
// Fall through to content parsing
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ── Tag group entries ─────────────────────────────────
|
|
847
|
-
if (phase === 'tags') {
|
|
848
|
-
// Tag group heading
|
|
849
|
-
if (isTagBlockHeading(trimmed)) {
|
|
850
|
-
const tg = makeTagGroup(trimmed, lineNumber);
|
|
851
|
-
if (tg) {
|
|
852
|
-
currentTagGroup = tg;
|
|
853
|
-
tagGroups.push(tg);
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Indented tag entry: `Value color` or `Value color default`
|
|
859
|
-
if (indent > 0 && currentTagGroup) {
|
|
860
|
-
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
861
|
-
const { label, color } = extractColor(
|
|
862
|
-
cleanEntry,
|
|
863
|
-
undefined,
|
|
864
|
-
diagnostics,
|
|
865
|
-
lineNumber
|
|
866
|
-
);
|
|
867
|
-
// Bare value (no explicit color) → keep it; finalized below.
|
|
868
|
-
currentTagGroup.entries.push({
|
|
869
|
-
value: label,
|
|
870
|
-
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
871
|
-
lineNumber,
|
|
872
|
-
});
|
|
873
|
-
if (isDefault) {
|
|
874
|
-
currentTagGroup.defaultValue = label;
|
|
875
|
-
} else if (currentTagGroup.entries.length === 1) {
|
|
876
|
-
currentTagGroup.defaultValue = label;
|
|
877
|
-
}
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Non-indented, non-tag-heading → switch to content
|
|
882
|
-
phase = 'content';
|
|
883
|
-
currentTagGroup = null;
|
|
884
|
-
// Fall through
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// ── Content phase ─────────────────────────────────────
|
|
888
|
-
if (phase !== 'content') {
|
|
889
|
-
phase = 'content';
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Options can appear in content phase too (e.g., `mobile` anywhere)
|
|
893
|
-
if (indent === 0 && trimmed === 'mobile') {
|
|
894
|
-
formFactor = 'mobile';
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
if (indent === 0) {
|
|
898
|
-
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
899
|
-
if (
|
|
900
|
-
optMatch &&
|
|
901
|
-
!trimmed.startsWith('#') &&
|
|
902
|
-
!trimmed.startsWith('[') &&
|
|
903
|
-
!trimmed.startsWith('(')
|
|
904
|
-
) {
|
|
905
|
-
// Only treat as option if it looks like one (palette, theme, active-tag, etc.)
|
|
906
|
-
// In-bounds: OPTION_NOCOLON_RE has 2 capture groups; first is required by match.
|
|
907
|
-
const key = optMatch[1]!;
|
|
908
|
-
if (['palette', 'theme', 'active-tag'].includes(key)) {
|
|
909
|
-
options[key] = optMatch[2] || '';
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Tag group entries can appear in content phase too
|
|
916
|
-
if (isTagBlockHeading(trimmed)) {
|
|
917
|
-
const tg = makeTagGroup(trimmed, lineNumber);
|
|
918
|
-
if (tg) {
|
|
919
|
-
currentTagGroup = tg;
|
|
920
|
-
tagGroups.push(tg);
|
|
921
|
-
phase = 'tags';
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Table data rows (comma-separated, indented under a table element)
|
|
927
|
-
if (indentStack.length > 0) {
|
|
928
|
-
// In-bounds by length-check above.
|
|
929
|
-
const topNode = indentStack[indentStack.length - 1]!.node;
|
|
930
|
-
if (topNode.type === 'table' && indent > topNode.indent) {
|
|
931
|
-
// Parse as table row
|
|
932
|
-
const cells = parseTableRow(trimmed);
|
|
933
|
-
if (!topNode.tableHeaders) {
|
|
934
|
-
// First indented row = header row if it's a comma-separated line
|
|
935
|
-
if (trimmed.includes(',')) {
|
|
936
|
-
topNode.tableHeaders = cells;
|
|
937
|
-
} else {
|
|
938
|
-
// Single value — treat as header
|
|
939
|
-
topNode.tableHeaders = cells;
|
|
940
|
-
}
|
|
941
|
-
} else {
|
|
942
|
-
if (!topNode.tableData) topNode.tableData = [];
|
|
943
|
-
topNode.tableData.push(cells);
|
|
944
|
-
}
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Multi-element line splitting
|
|
950
|
-
const segments = splitLineSegments(trimmed);
|
|
951
|
-
|
|
952
|
-
if (segments.length === 1) {
|
|
953
|
-
// Single element
|
|
954
|
-
// In-bounds by length-check above.
|
|
955
|
-
const el = parseSegment(segments[0]!, lineNumber, indent, diagnostics);
|
|
956
|
-
if (el) pushElement(el);
|
|
957
|
-
} else if (segments.length === 2) {
|
|
958
|
-
// Check for orphaned pipe metadata (EC5): second segment starts with `|`
|
|
959
|
-
// In-bounds by length-check above.
|
|
960
|
-
if (segments[1]!.startsWith('|')) {
|
|
961
|
-
const el = parseSegment(segments[0]!, lineNumber, indent, diagnostics);
|
|
962
|
-
if (el) {
|
|
963
|
-
applyMetadata(el, segments[1]!.substring(1).trim());
|
|
964
|
-
pushElement(el);
|
|
965
|
-
}
|
|
966
|
-
} else {
|
|
967
|
-
// Check for label-field pairing (ADR-9):
|
|
968
|
-
// First is bare text AND second contains bracket-mnemonic element
|
|
969
|
-
// In-bounds by length-check above.
|
|
970
|
-
const firstIsBare = isBareText(segments[0]!);
|
|
971
|
-
const secondIsElement = hasBracketMnemonic(segments[1]!);
|
|
972
|
-
|
|
973
|
-
if (firstIsBare && secondIsElement) {
|
|
974
|
-
// Label-for-element pairing
|
|
975
|
-
const fieldEl = parseSegment(
|
|
976
|
-
segments[1]!,
|
|
977
|
-
lineNumber,
|
|
978
|
-
indent,
|
|
979
|
-
diagnostics
|
|
980
|
-
);
|
|
981
|
-
if (fieldEl) {
|
|
982
|
-
const labelEl = makeElement(
|
|
983
|
-
'text',
|
|
984
|
-
segments[0]!.trim(),
|
|
985
|
-
lineNumber,
|
|
986
|
-
indent
|
|
987
|
-
);
|
|
988
|
-
labelEl.id = genId(lineNumber, indent, 'lbl');
|
|
989
|
-
labelEl.labelFor = fieldEl;
|
|
990
|
-
// Wrap in inline container
|
|
991
|
-
const wrapper = makeElement('group', '', lineNumber, indent);
|
|
992
|
-
wrapper.id = genId(lineNumber, indent, 'lf');
|
|
993
|
-
wrapper.isContainer = true;
|
|
994
|
-
wrapper.orientation = 'horizontal';
|
|
995
|
-
wrapper.children.push(labelEl, fieldEl);
|
|
996
|
-
const wrapperMetadata: Record<string, string> = {
|
|
997
|
-
_labelField: 'true',
|
|
998
|
-
};
|
|
999
|
-
wrapper.metadata = wrapperMetadata;
|
|
1000
|
-
pushElement(wrapper);
|
|
1001
|
-
}
|
|
1002
|
-
} else {
|
|
1003
|
-
// Two inline items — wrap in horizontal row
|
|
1004
|
-
pushInlineRow(segments, lineNumber, indent, diagnostics);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
} else {
|
|
1008
|
-
// 3+ segments = inline items, no label pairing — wrap in horizontal row
|
|
1009
|
-
pushInlineRow(segments, lineNumber, indent, diagnostics);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Final cleanup: pop remaining indent stack to finalize container detection
|
|
1014
|
-
while (indentStack.length > 0) {
|
|
1015
|
-
const popped = indentStack.pop()!;
|
|
1016
|
-
if (popped.node.children.length > 0 && !popped.node.isContainer) {
|
|
1017
|
-
popped.node.isContainer = true;
|
|
1018
|
-
if (popped.node.type === 'textInput') {
|
|
1019
|
-
popped.node.type = 'group';
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Assign palette colors to bare (colorless) tag values. Wireframe does not
|
|
1025
|
-
// thread a PaletteColors object into parse, so names resolve to the Nord
|
|
1026
|
-
// defaults — exactly as explicit `Value color` entries already do here.
|
|
1027
|
-
finalizeAutoTagColors(tagGroups as Writable<TagGroup>[]);
|
|
1028
|
-
|
|
1029
|
-
// Validate tag groups
|
|
1030
|
-
validateTagGroupNames(tagGroups, pushWarning, (line, msg) => {
|
|
1031
|
-
diagnostics.push(makeDgmoError(line, msg));
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
const error = diagnostics.find((d) => d.severity === 'error')
|
|
1035
|
-
? formatDgmoError(diagnostics.find((d) => d.severity === 'error')!)
|
|
1036
|
-
: null;
|
|
1037
|
-
|
|
1038
|
-
return {
|
|
1039
|
-
title,
|
|
1040
|
-
titleLineNumber,
|
|
1041
|
-
formFactor,
|
|
1042
|
-
roots,
|
|
1043
|
-
modals,
|
|
1044
|
-
tagGroups,
|
|
1045
|
-
options,
|
|
1046
|
-
diagnostics,
|
|
1047
|
-
error,
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// ============================================================
|
|
1052
|
-
// Table row parsing (bracket-depth tracking, F7)
|
|
1053
|
-
// ============================================================
|
|
1054
|
-
|
|
1055
|
-
function parseTableRow(line: string): string[] {
|
|
1056
|
-
const cells: string[] = [];
|
|
1057
|
-
let current = '';
|
|
1058
|
-
let depth = 0;
|
|
1059
|
-
let escaped = false;
|
|
1060
|
-
|
|
1061
|
-
for (let i = 0; i < line.length; i++) {
|
|
1062
|
-
const ch = line[i];
|
|
1063
|
-
|
|
1064
|
-
if (escaped) {
|
|
1065
|
-
current += ch;
|
|
1066
|
-
escaped = false;
|
|
1067
|
-
continue;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
if (ch === '\\') {
|
|
1071
|
-
escaped = true;
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (ch === '[' || ch === '{' || ch === '(' || ch === '<') depth++;
|
|
1076
|
-
if (ch === ']' || ch === '}' || ch === ')' || ch === '>')
|
|
1077
|
-
depth = Math.max(0, depth - 1);
|
|
1078
|
-
|
|
1079
|
-
if (ch === ',' && depth === 0) {
|
|
1080
|
-
cells.push(current.trim());
|
|
1081
|
-
current = '';
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
current += ch;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (current.trim()) {
|
|
1089
|
-
cells.push(current.trim());
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return cells;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// ============================================================
|
|
1096
|
-
// Helpers for multi-element line disambiguation
|
|
1097
|
-
// ============================================================
|
|
1098
|
-
|
|
1099
|
-
function isBareText(segment: string): boolean {
|
|
1100
|
-
const s = segment.trim();
|
|
1101
|
-
if (!s) return false;
|
|
1102
|
-
// Not bare text if it starts with a bracket mnemonic character
|
|
1103
|
-
if (/^[[\]({<#-]/.test(s)) return false;
|
|
1104
|
-
// Not bare text if it's a keyword
|
|
1105
|
-
if (matchKeyword(s)) return false;
|
|
1106
|
-
return true;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function hasBracketMnemonic(segment: string): boolean {
|
|
1110
|
-
const s = segment.trim();
|
|
1111
|
-
return (
|
|
1112
|
-
/^\[/.test(s) || /^\(/.test(s) || /^\{/.test(s) || /^<\s*x?\s*>/i.test(s)
|
|
1113
|
-
);
|
|
1114
|
-
}
|