@transitrix/diagrams 1.8.11 → 1.8.18
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/activities/validate.js +9 -1
- package/dist/blocks/index.d.ts +5 -4
- package/dist/blocks/index.js +3 -2
- package/dist/blocks/layout.d.ts +8 -1
- package/dist/blocks/layout.js +57 -0
- package/dist/blocks/templates/index.d.ts +9 -0
- package/dist/blocks/templates/index.js +11 -0
- package/dist/blocks/templates/raci.d.ts +9 -0
- package/dist/blocks/templates/raci.js +24 -0
- package/dist/blocks/types.d.ts +58 -0
- package/dist/blocks/validate.d.ts +31 -1
- package/dist/blocks/validate.js +127 -0
- package/dist/capability-map/validate.js +41 -0
- package/dist/fgca/parse-canonical.js +8 -0
- package/dist/repo-validate/check-element-hygiene.d.ts +8 -0
- package/dist/repo-validate/check-element-hygiene.js +211 -0
- package/dist/repo-validate/check-strategy-chain.d.ts +8 -0
- package/dist/repo-validate/check-strategy-chain.js +491 -0
- package/dist/repo-validate/index.d.ts +2 -1
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/resolve-model.d.ts +5 -0
- package/dist/repo-validate/resolve-model.js +71 -0
- package/dist/repo-validate/types.d.ts +70 -1
- package/dist/repo-validate/types.js +22 -3
- package/dist/repo-validate/validate-repo.d.ts +14 -3
- package/dist/repo-validate/validate-repo.js +21 -8
- package/dist/webview/render-blocks.d.ts +23 -1
- package/dist/webview/render-blocks.js +100 -2
- package/package.json +1 -1
- package/src/activities/validate.ts +9 -1
- package/src/blocks/index.ts +16 -2
- package/src/blocks/layout.ts +70 -0
- package/src/blocks/templates/index.ts +13 -0
- package/src/blocks/templates/raci.ts +28 -0
- package/src/blocks/types.ts +67 -0
- package/src/blocks/validate.ts +157 -1
- package/src/capability-map/validate.ts +40 -0
- package/src/fgca/parse-canonical.ts +8 -0
- package/src/repo-validate/check-element-hygiene.ts +236 -0
- package/src/repo-validate/check-strategy-chain.ts +528 -0
- package/src/repo-validate/index.ts +2 -1
- package/src/repo-validate/resolve-model.ts +79 -0
- package/src/repo-validate/types.ts +95 -4
- package/src/repo-validate/validate-repo.ts +21 -8
- package/src/webview/render-blocks.ts +159 -2
|
@@ -7,11 +7,30 @@
|
|
|
7
7
|
// runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
|
|
8
8
|
// "Validation Runtime Convergence").
|
|
9
9
|
//
|
|
10
|
-
// The finding shape
|
|
11
|
-
// richer `target` / `category` reporting taxonomy
|
|
12
|
-
// ADR until a real consumer
|
|
10
|
+
// The finding shape was originally frozen minimal — `{ scope, id, message }` —
|
|
11
|
+
// with the richer `target` / `category` reporting taxonomy explicitly DEFERRED
|
|
12
|
+
// per the ADR until a real consumer needed it. This un-freezes one field,
|
|
13
|
+
// `ruleId`: DSM (migrating off its own Go `Validate*` functions) needs a
|
|
14
|
+
// stable code per finding to map onto its existing import-log rule taxonomy
|
|
15
|
+
// (`api02/internal/importer/{goals,activities,fgca}.go`). The richer
|
|
16
|
+
// `target`/`category` taxonomy stays deferred — only `ruleId` is added.
|
|
17
|
+
// `ruleId` is optional: only checks with a stable, documented code set it
|
|
18
|
+
// (`docs/validation.md` § repo-scope rule list); the original structural
|
|
19
|
+
// checks (syntax/uniqueness/atomicity/referential-integrity/policy) stay
|
|
20
|
+
// uncoded rather than being retrofitted with invented codes.
|
|
21
|
+
//
|
|
22
|
+
// A second field, `severity`, is un-frozen for the same reason: DSM's
|
|
23
|
+
// warn-severity strategy-chain rules (GOALS-009/011, ACT-005, FGCA-012..014
|
|
24
|
+
// — see check-strategy-chain.ts) are non-fatal by
|
|
25
|
+
// DSM's own taxonomy ("import anyway, record the warning"); without a
|
|
26
|
+
// severity field, porting them here would silently make them blocking,
|
|
27
|
+
// since every existing repo-scope finding is implicitly blocking. `severity`
|
|
28
|
+
// is optional and defaults to `'error'` when omitted — every finding from
|
|
29
|
+
// before this field existed (syntax/uniqueness/atomicity/referential-
|
|
30
|
+
// integrity/policy, plus the error-tier strategy-chain rules and
|
|
31
|
+
// TSVC-003/INT-002) stays blocking with no code change at the call site.
|
|
13
32
|
|
|
14
|
-
/** A single repo-scope validation finding.
|
|
33
|
+
/** A single repo-scope validation finding. */
|
|
15
34
|
export interface RepoFinding {
|
|
16
35
|
/** Always `'repo'` — the execution axis this finding came from. */
|
|
17
36
|
scope: 'repo';
|
|
@@ -22,6 +41,19 @@ export interface RepoFinding {
|
|
|
22
41
|
id: string;
|
|
23
42
|
/** Human-readable description of the violation. */
|
|
24
43
|
message: string;
|
|
44
|
+
/**
|
|
45
|
+
* Stable rule code (e.g. `GOALS-010`, `ACT-006`, `FGCA-008`), for a consumer
|
|
46
|
+
* that maps findings onto its own rule taxonomy (`docs/validation.md`).
|
|
47
|
+
* Omitted for checks that don't yet have one.
|
|
48
|
+
*/
|
|
49
|
+
ruleId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* `'error'` (blocking) or `'warning'` (advisory). Omitted is equivalent to
|
|
52
|
+
* `'error'` — every check that predates this field never sets it and must
|
|
53
|
+
* stay blocking. Only the warn-severity strategy-chain rules set this to
|
|
54
|
+
* `'warning'` today (`docs/validation.md` § repo-scope rule list).
|
|
55
|
+
*/
|
|
56
|
+
severity?: 'error' | 'warning';
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
/** A parsed canon document fed to the repo validator. IO (the filesystem walk)
|
|
@@ -47,3 +79,62 @@ export interface RepoModelInput {
|
|
|
47
79
|
/** Documents found under `canon/relations/**`. */
|
|
48
80
|
relations: RepoDoc[];
|
|
49
81
|
}
|
|
82
|
+
|
|
83
|
+
/** One resolved canon element, projected from a `RepoDoc` for a non-JS
|
|
84
|
+
* consumer (DSM's Go importer). The top-level fields below are a stable,
|
|
85
|
+
* minimal identity projection — kept for backward compatibility with
|
|
86
|
+
* consumers that only need `id`/`name`/`notation`/`type`/`layer`/`sourceFile`
|
|
87
|
+
* (mirrors DSM's `methodology_element` table). `data` carries the complete
|
|
88
|
+
* parsed element alongside it, so a consumer never needs an engine schema
|
|
89
|
+
* change to read a canon-authored field the engine already parsed (a goal's
|
|
90
|
+
* `level`/`parent`/`description`/`link`/`tags`, an action's scheduling and
|
|
91
|
+
* ownership fields, etc.) — the engine owns each notation's field set
|
|
92
|
+
* (ELEMENT_PRIMITIVES.md), so its output stays notation-shaped rather than
|
|
93
|
+
* growing a bespoke field list per consumer. This is the faithful-projection
|
|
94
|
+
* fallback: a typed per-notation shape may replace `data` later, but the
|
|
95
|
+
* contract must not be curated down to what one consumer currently needs. */
|
|
96
|
+
export interface ResolvedElementRecord {
|
|
97
|
+
/** Canonical id, e.g. `DRIVER-COMP-1`. */
|
|
98
|
+
id: string;
|
|
99
|
+
/** `name` field; `''` when absent (should not happen for an admitted element). */
|
|
100
|
+
name: string;
|
|
101
|
+
/** The element TYPE's short name — the doc's `notation` field, e.g. `driver`, `goal`, `capability`. */
|
|
102
|
+
notation: string;
|
|
103
|
+
/** Subtype from the TYPE's controlled vocabulary (ELEMENT_PRIMITIVES.md §3), e.g. `internal`/`external` for DRIVER. Omitted when the doc has none. */
|
|
104
|
+
type?: string;
|
|
105
|
+
/** `motivation` / `business` / `application` / `technology` / `implementation`. Read from the doc's `layer` field when present, else derived from the `canon/elements/<NN>_<layer>/…` folder. */
|
|
106
|
+
layer?: string;
|
|
107
|
+
/** Source file path, relative to the scanned root. */
|
|
108
|
+
sourceFile: string;
|
|
109
|
+
/** The full parsed element document — every field its author supplied,
|
|
110
|
+
* unfiltered (including the admission/lifecycle envelope). The same
|
|
111
|
+
* `RepoDoc.data` the repo-scope validator already consumes, so this
|
|
112
|
+
* projection and the validation pass never see a different parse. */
|
|
113
|
+
data: Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One resolved canon relation, projected from a `RepoDoc`. Only
|
|
117
|
+
* emitted when both endpoints resolve to a non-empty id — an endpoint that
|
|
118
|
+
* fails to resolve is already surfaced as a referential-integrity finding by
|
|
119
|
+
* `validateRepoModel`; this projection does not duplicate that as a
|
|
120
|
+
* half-resolved record. */
|
|
121
|
+
export interface ResolvedRelationRecord {
|
|
122
|
+
/** Canonical id, e.g. `REL-EMP-PERSON-OPS-1`. `''` when the relation doc has no `id`. */
|
|
123
|
+
id: string;
|
|
124
|
+
/** The relation's `type` field, e.g. `employment`, `realizes`. Omitted when the doc has none. */
|
|
125
|
+
kind?: string;
|
|
126
|
+
/** Resolved `from` (or legacy `source`) endpoint id. */
|
|
127
|
+
source: string;
|
|
128
|
+
/** Resolved `to` (or legacy `target`) endpoint id. */
|
|
129
|
+
target: string;
|
|
130
|
+
/** Source file path, relative to the scanned root. */
|
|
131
|
+
sourceFile: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The resolved element/relation records for a repo-scope run —
|
|
135
|
+
* the shape `transitrix validate --scope=repo --json --include-model` adds
|
|
136
|
+
* alongside the validation findings. */
|
|
137
|
+
export interface ResolvedRepoModel {
|
|
138
|
+
elements: ResolvedElementRecord[];
|
|
139
|
+
relations: ResolvedRelationRecord[];
|
|
140
|
+
}
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
// 6. Policy (phase 6) — element marked Active/Production with no
|
|
24
24
|
// owner.
|
|
25
25
|
//
|
|
26
|
-
// Findings stay `{ scope, id, message }`
|
|
27
|
-
//
|
|
26
|
+
// Findings stay `{ scope, id, message }` plus the two un-frozen optional
|
|
27
|
+
// fields `ruleId` and `severity` — the richer target/category taxonomy stays
|
|
28
|
+
// deferred per the ADR.
|
|
28
29
|
|
|
30
|
+
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
31
|
+
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
29
32
|
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
30
33
|
|
|
31
34
|
const PScope: RepoFinding['scope'] = 'repo';
|
|
@@ -33,15 +36,16 @@ const PScope: RepoFinding['scope'] = 'repo';
|
|
|
33
36
|
/** Statuses that, per lint.py's policy check, require an owner. */
|
|
34
37
|
const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
|
|
35
38
|
|
|
36
|
-
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
39
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
37
40
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/** Read a string `id` from a parsed doc, or `null` if absent/non-string.
|
|
41
44
|
* Mirrors lint.py, which only treats a doc as an element/relation when it has
|
|
42
45
|
* a top-level `id` (sidecars such as versioned-attribute files have no `id`
|
|
43
|
-
* and are ignored).
|
|
44
|
-
|
|
46
|
+
* and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
|
|
47
|
+
* same id/endpoint reading rules to stay in lockstep with this validator. */
|
|
48
|
+
export function docId(doc: RepoDoc): string | null {
|
|
45
49
|
if (!doc.data) return null;
|
|
46
50
|
const id = doc.data['id'];
|
|
47
51
|
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
@@ -49,7 +53,7 @@ function docId(doc: RepoDoc): string | null {
|
|
|
49
53
|
|
|
50
54
|
/** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
|
|
51
55
|
* mapping (lint.py accepts both). Returns the id string or `null`. */
|
|
52
|
-
function endpointId(value: unknown): string | null {
|
|
56
|
+
export function endpointId(value: unknown): string | null {
|
|
53
57
|
if (typeof value === 'string') return value.length > 0 ? value : null;
|
|
54
58
|
if (isRecord(value)) {
|
|
55
59
|
const id = value['id'];
|
|
@@ -345,6 +349,7 @@ function checkLayerSemantics(input: RepoModelInput, findings: RepoFinding[]): vo
|
|
|
345
349
|
findings.push({
|
|
346
350
|
scope: PScope,
|
|
347
351
|
id: svcId,
|
|
352
|
+
ruleId: 'TSVC-003',
|
|
348
353
|
message:
|
|
349
354
|
`TSVC-003: TECHNOLOGY_SERVICE '${svcId}' node '${nodeRef}' ` +
|
|
350
355
|
`must resolve to a NODE element; got notation='${resolved['notation']}'.`,
|
|
@@ -368,6 +373,7 @@ function checkLayerSemantics(input: RepoModelInput, findings: RepoFinding[]): vo
|
|
|
368
373
|
findings.push({
|
|
369
374
|
scope: PScope,
|
|
370
375
|
id: intId,
|
|
376
|
+
ruleId: 'INT-002',
|
|
371
377
|
message:
|
|
372
378
|
`INT-002: INTEGRATION '${intId}' interface_semantics endpoint '${endpointId}' ` +
|
|
373
379
|
`(${field}) must resolve to an APPLICATION element; got notation='${resolved['notation']}'.`,
|
|
@@ -399,8 +405,9 @@ function checkPolicy(input: RepoModelInput, findings: RepoFinding[]): void {
|
|
|
399
405
|
/**
|
|
400
406
|
* Run all repo-scope checks over a loaded canon model and return the findings.
|
|
401
407
|
* Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
|
|
402
|
-
* semantics, policy). Reaches parity with `lint.py` on the
|
|
403
|
-
* (zero findings on a clean tree
|
|
408
|
+
* semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
|
|
409
|
+
* `acme_corp` fixture (zero findings on a clean tree from the original
|
|
410
|
+
* lint.py-ported phases).
|
|
404
411
|
*/
|
|
405
412
|
export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
|
|
406
413
|
const findings: RepoFinding[] = [];
|
|
@@ -415,5 +422,11 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
|
|
|
415
422
|
checkReferentialIntegrity(input, findings);
|
|
416
423
|
checkLayerSemantics(input, findings);
|
|
417
424
|
checkPolicy(input, findings);
|
|
425
|
+
// Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
|
|
426
|
+
// functions (GOALS-010, ACT-006..009, FGCA-008..011).
|
|
427
|
+
checkStrategyChainSemantics(input, findings);
|
|
428
|
+
// Phase 8 — standalone-element envelope rules ported from DSM's Go
|
|
429
|
+
// Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
|
|
430
|
+
checkElementHygiene(input, findings);
|
|
418
431
|
return findings;
|
|
419
432
|
}
|
|
@@ -12,14 +12,25 @@
|
|
|
12
12
|
* the shared theme CSS embedded in a `<style>` element and a simple optional
|
|
13
13
|
* title `<text>`.
|
|
14
14
|
*/
|
|
15
|
-
import { layoutNestedBlocks } from '../blocks/layout.js';
|
|
16
|
-
import type {
|
|
15
|
+
import { layoutNestedBlocks, layoutGrid } from '../blocks/layout.js';
|
|
16
|
+
import type {
|
|
17
|
+
BlocksFile,
|
|
18
|
+
BlocksLayout,
|
|
19
|
+
BlocksLayoutOptions,
|
|
20
|
+
GridFile,
|
|
21
|
+
GridLayout,
|
|
22
|
+
GridLayoutOptions,
|
|
23
|
+
LaidOutBlock,
|
|
24
|
+
} from '../blocks/types.js';
|
|
17
25
|
import { parseNodeSizePreset, resolveBlocksLeafSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
18
26
|
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
19
27
|
import {
|
|
28
|
+
CHAR_W_PRIMARY,
|
|
20
29
|
emitCenteredTextSvg,
|
|
21
30
|
layoutHeaderBlockText,
|
|
22
31
|
layoutLeafBlockText,
|
|
32
|
+
maxCharsForInnerWidth,
|
|
33
|
+
wrapWords,
|
|
23
34
|
} from './entity-text-layout.js';
|
|
24
35
|
import { escXml } from './render-util.js';
|
|
25
36
|
import { ENTITY_NODE_RX } from './notation-style.js';
|
|
@@ -141,3 +152,149 @@ export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions =
|
|
|
141
152
|
|
|
142
153
|
return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
|
|
143
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Matrix subset (08-blocks.md §4a) — first cut: a single-layer rectangular
|
|
158
|
+
* `grid:` document (the RACI-style matrix case) rendered as a plain
|
|
159
|
+
* HTML/CSS-table-like SVG grid. Layers, arbitrary
|
|
160
|
+
* (non-rectangular) cell sets, and nested sub-grids belong to the general
|
|
161
|
+
* layered-grid superset, which is still in design upstream (methodology) and
|
|
162
|
+
* out of scope here — see 08-blocks.md §4a / §8.
|
|
163
|
+
*/
|
|
164
|
+
const GRID_CELL_PAD_X = 8;
|
|
165
|
+
const GRID_LINE_HEIGHT = 15;
|
|
166
|
+
const GRID_EMBED_CSS = `
|
|
167
|
+
.blocks-grid-border { fill: none; stroke: var(--ts-border, #cbd5e1); }`;
|
|
168
|
+
|
|
169
|
+
function centeredGridCellTextSvg(
|
|
170
|
+
lines: string[],
|
|
171
|
+
cls: string,
|
|
172
|
+
cx: number,
|
|
173
|
+
cellTop: number,
|
|
174
|
+
cellHeight: number,
|
|
175
|
+
): string {
|
|
176
|
+
const ls = lines.length > 0 ? lines : [''];
|
|
177
|
+
const firstY = cellTop + cellHeight / 2 - ((ls.length - 1) / 2) * GRID_LINE_HEIGHT;
|
|
178
|
+
const tspans = ls
|
|
179
|
+
.map((ln, i) => `<tspan x="${cx}" y="${firstY + i * GRID_LINE_HEIGHT}">${escXml(ln)}</tspan>`)
|
|
180
|
+
.join('');
|
|
181
|
+
return `<text class="${cls}" text-anchor="middle" dominant-baseline="central">${tspans}</text>`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Emit the grid body (headers, cells, grid lines) at the given origin offset.
|
|
186
|
+
* Shared by the host-neutral wrapper below; VS Code's blocks preview wraps
|
|
187
|
+
* this the same way it wraps {@link renderBlocksLayoutSvg} for the tree form.
|
|
188
|
+
*/
|
|
189
|
+
export function renderGridBody(layout: GridLayout, ox: number, oy: number): string {
|
|
190
|
+
const tw = layout.bounds.width;
|
|
191
|
+
const th = layout.bounds.height;
|
|
192
|
+
const parts: string[] = [];
|
|
193
|
+
|
|
194
|
+
parts.push(`<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${tw}" height="${th}" stroke="none"/>`);
|
|
195
|
+
|
|
196
|
+
// Corner cell (above the row headers, left of the column headers).
|
|
197
|
+
parts.push(
|
|
198
|
+
`<rect class="diagram-node level-1" x="${ox}" y="${oy}" width="${layout.rowHeaderWidth}" height="${layout.headerHeight}" stroke="none"/>`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
for (const col of layout.columns) {
|
|
202
|
+
const x = col.x + ox;
|
|
203
|
+
parts.push(
|
|
204
|
+
`<rect class="diagram-node level-1" x="${x}" y="${oy}" width="${col.width}" height="${layout.headerHeight}" stroke="none"/>`,
|
|
205
|
+
);
|
|
206
|
+
const maxChars = maxCharsForInnerWidth(col.width - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
|
|
207
|
+
const lines = wrapWords(col.name, maxChars, 2);
|
|
208
|
+
parts.push(centeredGridCellTextSvg(lines, 'text-header', x + col.width / 2, oy, layout.headerHeight));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const row of layout.rows) {
|
|
212
|
+
const y = row.y + oy;
|
|
213
|
+
parts.push(
|
|
214
|
+
`<rect class="diagram-node level-2" x="${ox}" y="${y}" width="${layout.rowHeaderWidth}" height="${row.height}" stroke="none"/>`,
|
|
215
|
+
);
|
|
216
|
+
const maxChars = maxCharsForInnerWidth(layout.rowHeaderWidth - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
|
|
217
|
+
const lines = wrapWords(row.name, maxChars, 2);
|
|
218
|
+
parts.push(centeredGridCellTextSvg(lines, 'text-primary', ox + layout.rowHeaderWidth / 2, y, row.height));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const cell of layout.cells) {
|
|
222
|
+
if (cell.value === undefined) continue;
|
|
223
|
+
const cx = cell.x + ox + cell.width / 2;
|
|
224
|
+
const cy = cell.y + oy + cell.height / 2;
|
|
225
|
+
parts.push(
|
|
226
|
+
`<text class="text-secondary" x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central">${escXml(cell.value)}</text>`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const gridX1 = ox;
|
|
231
|
+
const gridX2 = ox + tw;
|
|
232
|
+
const gridY1 = oy;
|
|
233
|
+
const gridY2 = oy + th;
|
|
234
|
+
|
|
235
|
+
const headerBottomY = oy + layout.headerHeight;
|
|
236
|
+
parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${headerBottomY}" x2="${gridX2}" y2="${headerBottomY}"/>`);
|
|
237
|
+
for (const row of layout.rows) {
|
|
238
|
+
const rowBottomY = oy + row.y + row.height;
|
|
239
|
+
parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${rowBottomY}" x2="${gridX2}" y2="${rowBottomY}"/>`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rowHeaderLineX = ox + layout.rowHeaderWidth;
|
|
243
|
+
parts.push(`<line class="diagram-edge" x1="${rowHeaderLineX}" y1="${gridY1}" x2="${rowHeaderLineX}" y2="${gridY2}"/>`);
|
|
244
|
+
for (const col of layout.columns) {
|
|
245
|
+
const colLineX = ox + col.x + col.width;
|
|
246
|
+
parts.push(`<line class="diagram-edge" x1="${colLineX}" y1="${gridY1}" x2="${colLineX}" y2="${gridY2}"/>`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
parts.push(`<rect class="blocks-grid-border" x="${ox}" y="${oy}" width="${tw}" height="${th}"/>`);
|
|
250
|
+
|
|
251
|
+
return parts.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface RenderGridLayoutOptions {
|
|
255
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
256
|
+
topInset?: number;
|
|
257
|
+
/** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
|
|
258
|
+
title?: string;
|
|
259
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
260
|
+
embedCssTheme?: ThemeId;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The single grid (matrix subset) SVG emitter shared by every host. */
|
|
264
|
+
export function renderGridLayoutSvg(layout: GridLayout, options: RenderGridLayoutOptions = {}): string {
|
|
265
|
+
const { topInset = 0, title = '', embedCssTheme } = options;
|
|
266
|
+
|
|
267
|
+
const w = layout.bounds.width + PAD * 2;
|
|
268
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
269
|
+
const ox = PAD;
|
|
270
|
+
const oy = PAD + topInset;
|
|
271
|
+
|
|
272
|
+
const body = renderGridBody(layout, ox, oy);
|
|
273
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}${GRID_EMBED_CSS}</style>` : '';
|
|
274
|
+
|
|
275
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
276
|
+
${title}
|
|
277
|
+
${body}
|
|
278
|
+
</svg>`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface RenderGridOptions {
|
|
282
|
+
title?: string;
|
|
283
|
+
layoutOptions?: GridLayoutOptions;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
|
|
287
|
+
export function renderGridSvg(doc: GridFile, options: RenderGridOptions = {}): string {
|
|
288
|
+
const { title = '', layoutOptions } = options;
|
|
289
|
+
const layout = layoutGrid(doc, layoutOptions);
|
|
290
|
+
|
|
291
|
+
if (layout.columns.length === 0 || layout.rows.length === 0) {
|
|
292
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const titleSvg = title
|
|
296
|
+
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
|
|
297
|
+
: '';
|
|
298
|
+
|
|
299
|
+
return renderGridLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
|
|
300
|
+
}
|