@markdstage/markdstage 3.1.0 → 3.3.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/package.json +1 -1
- package/shared/README.md +61 -9
- package/shared/architecture-editor/editor.js +1 -1
- package/shared/architecture-reference.mjs +150 -0
- package/shared/architecture-validation.mjs +309 -0
- package/shared/markdstage-guide.mjs +62 -142
- package/shared/renderer/architecture-contract.mjs +13707 -0
- package/shared/renderer/architecture-diagnostics.mjs +426 -0
- package/shared/renderer/architecture-scene.mjs +312 -0
- package/shared/renderer/architecture.mjs +221 -217
- package/shared/renderer/index.html +9 -1
- package/shared/renderer/mermaid-scene.mjs +1248 -0
- package/shared/renderer/renderer.js +429 -232
- package/shared/renderer/scene-graph.mjs +739 -0
- package/shared/renderer/scene-pptx.mjs +265 -0
- package/shared/renderer/scene-svg.mjs +321 -0
- package/shared/renderer/slides.css +23 -0
- package/shared/renderer/theme.mjs +54 -0
- package/shared/runtime/architecture-editor-server.mjs +5 -0
- package/shared/runtime/architecture-source.mjs +9 -1
- package/shared/runtime/pptx-package.mjs +4 -3
- package/shared/schema/README.md +60 -5
- package/shared/schema/architecture-contract.mjs +355 -0
- package/shared/scripts/generate-architecture-contract.mjs +41 -0
- package/src/commands/validate.mjs +39 -3
- package/src/runtime.mjs +4 -0
- package/src/skills.mjs +6 -2
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { architectureContract } from "./architecture-contract.mjs";
|
|
2
|
+
import { architectureSnapshotToScene } from "./architecture-scene.mjs";
|
|
3
|
+
import { sceneToSvg, svgPrimitive } from "./scene-svg.mjs";
|
|
4
|
+
import {
|
|
5
|
+
ArchitectureError,
|
|
6
|
+
architectureCompatibilityWarnings,
|
|
7
|
+
architectureDiagnostic,
|
|
8
|
+
architectureFailureReport,
|
|
9
|
+
checkArchitectureReferences,
|
|
10
|
+
claimArchitectureId,
|
|
11
|
+
diagnosticLimit,
|
|
12
|
+
throwArchitectureDiagnostic,
|
|
13
|
+
unknownArchitectureField,
|
|
14
|
+
} from "./architecture-diagnostics.mjs";
|
|
15
|
+
|
|
2
16
|
const DEFAULT_CANVAS = Object.freeze({ width: 1600, height: 900 });
|
|
3
|
-
const DSL_VERSION =
|
|
17
|
+
const DSL_VERSION = architectureContract.root.properties.version.const;
|
|
4
18
|
const EMPTY_ARCHITECTURE_SOURCE = '{\n "version": 1,\n "elements": []\n}\n';
|
|
5
19
|
const MAX_SOURCE_LENGTH = 64 * 1024;
|
|
6
|
-
const MAX_ELEMENTS =
|
|
20
|
+
const MAX_ELEMENTS = architectureContract.root.properties.elements.maxItems;
|
|
7
21
|
const MAX_CONNECTORS = 100;
|
|
8
22
|
const MAX_TOTAL_TEXT = 20_000;
|
|
9
23
|
const MAX_DEPTH = 4;
|
|
10
|
-
const MAX_POINTS =
|
|
24
|
+
const MAX_POINTS = architectureContract.elements.connector.properties.points.maxItems;
|
|
11
25
|
const CONNECTOR_ENDPOINT_GAP = 14;
|
|
12
26
|
const MIN_ORTHOGONAL_ENDPOINT_SPAN = 8;
|
|
13
27
|
const CONNECTOR_LANE_SPACING = 52;
|
|
@@ -90,28 +104,20 @@ const ROUTE_FALLBACK_REMEDIES = Object.freeze({
|
|
|
90
104
|
"no-clean-candidate": "every candidate route is blocked by another element",
|
|
91
105
|
});
|
|
92
106
|
|
|
93
|
-
const ID_PATTERN =
|
|
94
|
-
const SHAPES = new Set(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"parallelogram",
|
|
102
|
-
]);
|
|
103
|
-
const ROUTINGS = new Set(["straight", "orthogonal", "polyline"]);
|
|
104
|
-
const PORTS = new Set(["auto", "top", "right", "bottom", "left"]);
|
|
105
|
-
const LABEL_LAYERS = new Set(["front", "behind"]);
|
|
106
|
-
const LAYOUTS = new Set(["row", "column", "grid", "layered"]);
|
|
107
|
-
const LAYOUT_DIRECTIONS = new Set(["down", "right"]);
|
|
108
|
-
const IMAGE_FITS = new Set(["contain", "cover", "stretch"]);
|
|
107
|
+
const ID_PATTERN = new RegExp(architectureContract.definitions.identifier.pattern);
|
|
108
|
+
const SHAPES = new Set(architectureContract.elements.node.properties.shape.enum);
|
|
109
|
+
const ROUTINGS = new Set(architectureContract.elements.connector.properties.routing.enum);
|
|
110
|
+
const PORTS = new Set(architectureContract.elements.connector.properties.fromPort.enum);
|
|
111
|
+
const LABEL_LAYERS = new Set(architectureContract.elements.connector.properties.labelLayer.enum);
|
|
112
|
+
const LAYOUTS = new Set(architectureContract.definitions.layoutObject.properties.type.enum);
|
|
113
|
+
const LAYOUT_DIRECTIONS = new Set(architectureContract.definitions.layoutObject.properties.direction.enum);
|
|
114
|
+
const IMAGE_FITS = new Set(architectureContract.elements.image.properties.fit.enum);
|
|
109
115
|
// Recursion limit while layered layout reads the connection graph. This runs
|
|
110
116
|
// before MAX_DEPTH validation, so impose an independent cutoff for unvalidated input.
|
|
111
117
|
const MAX_GRAPH_SCAN_DEPTH = 16;
|
|
112
118
|
// Fixed number of barycenter sweeps for deterministic output.
|
|
113
119
|
const LAYERED_ORDERING_SWEEPS = 4;
|
|
114
|
-
//
|
|
120
|
+
// Rendering glyphs for the built-in icon names in the schema-derived contract.
|
|
115
121
|
//
|
|
116
122
|
// Naming: lowercase kebab-case (`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`) nouns for
|
|
117
123
|
// general concepts, not product or vendor names. Names are public DSL vocabulary,
|
|
@@ -177,7 +183,7 @@ const ICON_SHAPES = Object.freeze({
|
|
|
177
183
|
{ tag: "path", attributes: { d: "m8.5 12 2.5 2.5 4.5-4.5" } },
|
|
178
184
|
],
|
|
179
185
|
});
|
|
180
|
-
const ICONS = new Set(
|
|
186
|
+
const ICONS = new Set(architectureContract.definitions.iconName.enum);
|
|
181
187
|
// User-provided icons must be repository files under `assets/`. Rendering uses
|
|
182
188
|
// <image href="/assets/...">, served through safeJoin by the `/assets/*` route
|
|
183
189
|
// in extension.mjs or the test harness.
|
|
@@ -196,7 +202,7 @@ const ASSET_PATH_PATTERN = new RegExp(
|
|
|
196
202
|
`^assets/(?:${ASSET_PATH_SEGMENT}/)*${ASSET_PATH_SEGMENT}\\.(?:${ASSET_EXTENSION_PATTERN})$`,
|
|
197
203
|
);
|
|
198
204
|
const ICON_ASSET_PATTERN = ASSET_PATH_PATTERN;
|
|
199
|
-
const MAX_ASSET_REFERENCE =
|
|
205
|
+
const MAX_ASSET_REFERENCE = architectureContract.definitions.assetPath.maxLength;
|
|
200
206
|
const MAX_ICON_REFERENCE = MAX_ASSET_REFERENCE;
|
|
201
207
|
const THEME_TOKENS = Object.freeze({
|
|
202
208
|
accent: "var(--accent)",
|
|
@@ -222,97 +228,18 @@ function localAssetUrl(documentRef, path) {
|
|
|
222
228
|
} catch (_) {}
|
|
223
229
|
return `/${normalized}`;
|
|
224
230
|
}
|
|
225
|
-
const STYLE_KEYS = new Set(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"dash",
|
|
233
|
-
"cornerRadius",
|
|
234
|
-
]);
|
|
235
|
-
const LAYOUT_KEYS = new Set([
|
|
236
|
-
"type",
|
|
237
|
-
"gap",
|
|
238
|
-
"rowGap",
|
|
239
|
-
"columnGap",
|
|
240
|
-
"padding",
|
|
241
|
-
"columns",
|
|
242
|
-
"direction",
|
|
243
|
-
]);
|
|
244
|
-
const ELEMENT_KEYS = Object.freeze({
|
|
245
|
-
node: new Set([
|
|
246
|
-
"type",
|
|
247
|
-
"id",
|
|
248
|
-
"shape",
|
|
249
|
-
"x",
|
|
250
|
-
"y",
|
|
251
|
-
"width",
|
|
252
|
-
"height",
|
|
253
|
-
"text",
|
|
254
|
-
"icon",
|
|
255
|
-
"ariaLabel",
|
|
256
|
-
"z",
|
|
257
|
-
"style",
|
|
258
|
-
]),
|
|
259
|
-
group: new Set([
|
|
260
|
-
"type",
|
|
261
|
-
"id",
|
|
262
|
-
"x",
|
|
263
|
-
"y",
|
|
264
|
-
"width",
|
|
265
|
-
"height",
|
|
266
|
-
"title",
|
|
267
|
-
"ariaLabel",
|
|
268
|
-
"layout",
|
|
269
|
-
"z",
|
|
270
|
-
"style",
|
|
271
|
-
"children",
|
|
272
|
-
]),
|
|
273
|
-
image: new Set([
|
|
274
|
-
"type",
|
|
275
|
-
"id",
|
|
276
|
-
"src",
|
|
277
|
-
"fit",
|
|
278
|
-
"x",
|
|
279
|
-
"y",
|
|
280
|
-
"width",
|
|
281
|
-
"height",
|
|
282
|
-
"ariaLabel",
|
|
283
|
-
"z",
|
|
284
|
-
"style",
|
|
285
|
-
]),
|
|
286
|
-
connector: new Set([
|
|
287
|
-
"type",
|
|
288
|
-
"from",
|
|
289
|
-
"to",
|
|
290
|
-
"fromPort",
|
|
291
|
-
"toPort",
|
|
292
|
-
"label",
|
|
293
|
-
"labelLayer",
|
|
294
|
-
"ariaLabel",
|
|
295
|
-
"routing",
|
|
296
|
-
"points",
|
|
297
|
-
"arrow",
|
|
298
|
-
"lane",
|
|
299
|
-
"z",
|
|
300
|
-
"style",
|
|
301
|
-
]),
|
|
302
|
-
});
|
|
231
|
+
const STYLE_KEYS = new Set(Object.keys(architectureContract.definitions.style.properties));
|
|
232
|
+
const LAYOUT_KEYS = new Set(Object.keys(architectureContract.definitions.layoutObject.properties));
|
|
233
|
+
const ELEMENT_KEYS = Object.freeze(Object.fromEntries(
|
|
234
|
+
Object.entries(architectureContract.elements).map(([type, definition]) =>
|
|
235
|
+
[type, new Set(Object.keys(definition.properties))],
|
|
236
|
+
),
|
|
237
|
+
));
|
|
303
238
|
|
|
304
239
|
let renderSequence = 0;
|
|
305
240
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
super(message);
|
|
309
|
-
this.name = "ArchitectureError";
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function fail(path, message, remedy) {
|
|
314
|
-
const guidance = remedy ? `; ${remedy}` : "";
|
|
315
|
-
throw new ArchitectureError(`${path}: ${message}${guidance}`);
|
|
241
|
+
function fail(path, message, remedy, details) {
|
|
242
|
+
throwArchitectureDiagnostic(architectureDiagnostic(path, message, remedy, details));
|
|
316
243
|
}
|
|
317
244
|
|
|
318
245
|
function describeValue(value) {
|
|
@@ -331,37 +258,42 @@ function isObject(value) {
|
|
|
331
258
|
}
|
|
332
259
|
|
|
333
260
|
function expectObject(value, path, remedy = "use a JSON object such as { }") {
|
|
334
|
-
if (!isObject(value)) fail(path, "must be an object", remedy);
|
|
261
|
+
if (!isObject(value)) fail(path, "must be an object", remedy, { code: "invalid_type" });
|
|
335
262
|
return value;
|
|
336
263
|
}
|
|
337
264
|
|
|
338
265
|
function rejectUnknownKeys(value, allowed, path) {
|
|
339
266
|
for (const key of Object.keys(value)) {
|
|
340
267
|
if (!allowed.has(key)) {
|
|
341
|
-
|
|
342
|
-
`${path}.${key}`,
|
|
343
|
-
"is not supported",
|
|
344
|
-
`remove it or use one of: ${[...allowed].join(", ")}`,
|
|
345
|
-
);
|
|
268
|
+
throwArchitectureDiagnostic(unknownArchitectureField(value, key, allowed, path, value.type));
|
|
346
269
|
}
|
|
347
270
|
}
|
|
348
271
|
}
|
|
349
272
|
|
|
350
|
-
function numberIn(value, path, min, max, fallback, remedy) {
|
|
273
|
+
function numberIn(value, path, min, max, fallback, remedy, details) {
|
|
351
274
|
const candidate = value === undefined ? fallback : value;
|
|
352
275
|
if (typeof candidate !== "number" || !Number.isFinite(candidate)) {
|
|
353
276
|
fail(
|
|
354
277
|
path,
|
|
355
278
|
"must be a finite number",
|
|
356
279
|
remedy ?? `replace ${describeValue(candidate)} with a number between ${min} and ${max}`,
|
|
280
|
+
{ code: candidate === undefined ? "missing_required" : "invalid_type" },
|
|
357
281
|
);
|
|
358
282
|
}
|
|
359
283
|
if (candidate < min || candidate > max) {
|
|
360
|
-
|
|
284
|
+
const intrinsicExtentError = details?.category === "layout" &&
|
|
285
|
+
(candidate < architectureContract.definitions.extent.minimum ||
|
|
286
|
+
candidate > architectureContract.definitions.extent.maximum);
|
|
287
|
+
fail(path, `must be between ${min} and ${max}`, remedy ?? "adjust the value into that range",
|
|
288
|
+
{ code: "out_of_range", ...(intrinsicExtentError ? {} : details) });
|
|
361
289
|
}
|
|
362
290
|
return candidate;
|
|
363
291
|
}
|
|
364
292
|
|
|
293
|
+
function contractNumber(value, path, definition, fallback) {
|
|
294
|
+
return numberIn(value, path, definition.minimum, definition.maximum, fallback);
|
|
295
|
+
}
|
|
296
|
+
|
|
365
297
|
function textValue(value, path, fallback = "", maxLength = 500, remedy) {
|
|
366
298
|
const candidate = value === undefined ? fallback : value;
|
|
367
299
|
if (typeof candidate !== "string") {
|
|
@@ -369,6 +301,7 @@ function textValue(value, path, fallback = "", maxLength = 500, remedy) {
|
|
|
369
301
|
path,
|
|
370
302
|
"must be a string",
|
|
371
303
|
remedy ?? `replace ${describeValue(candidate)} with text in double quotes`,
|
|
304
|
+
{ code: "invalid_type" },
|
|
372
305
|
);
|
|
373
306
|
}
|
|
374
307
|
if (candidate.length > maxLength) {
|
|
@@ -376,6 +309,7 @@ function textValue(value, path, fallback = "", maxLength = 500, remedy) {
|
|
|
376
309
|
path,
|
|
377
310
|
`must be at most ${maxLength} characters`,
|
|
378
311
|
remedy ?? `shorten it by ${candidate.length - maxLength} characters`,
|
|
312
|
+
{ code: "text_limit" },
|
|
379
313
|
);
|
|
380
314
|
}
|
|
381
315
|
return candidate;
|
|
@@ -387,6 +321,7 @@ function idValue(value, path) {
|
|
|
387
321
|
path,
|
|
388
322
|
"must start with a letter and contain only letters, numbers, '.', '_' or '-'",
|
|
389
323
|
`replace ${describeValue(value)} with an id such as 'api-gateway' (64 characters or fewer)`,
|
|
324
|
+
{ code: value === undefined ? "missing_required" : "invalid_identifier" },
|
|
390
325
|
);
|
|
391
326
|
}
|
|
392
327
|
return value;
|
|
@@ -399,6 +334,7 @@ function enumValue(value, path, values, fallback) {
|
|
|
399
334
|
path,
|
|
400
335
|
`must be one of: ${[...values].join(", ")}`,
|
|
401
336
|
`replace ${describeValue(candidate)} with one of them`,
|
|
337
|
+
{ code: value === undefined && fallback === undefined ? "missing_required" : "invalid_value" },
|
|
402
338
|
);
|
|
403
339
|
}
|
|
404
340
|
return candidate;
|
|
@@ -429,6 +365,7 @@ function assetPathValue(value, path) {
|
|
|
429
365
|
`replace ${describeValue(candidate)} with a repository asset such as 'assets/images/diagram.png' (${ASSET_EXTENSIONS.map(
|
|
430
366
|
(extension) => `.${extension}`,
|
|
431
367
|
).join(", ")} only; '..', 'data:' URIs and external URLs are rejected)`,
|
|
368
|
+
{ code: value === undefined ? "missing_required" : "invalid_value" },
|
|
432
369
|
);
|
|
433
370
|
}
|
|
434
371
|
|
|
@@ -453,9 +390,10 @@ function colorValue(value, path, fallback) {
|
|
|
453
390
|
}
|
|
454
391
|
|
|
455
392
|
function normalizeStyle(value, path, defaults) {
|
|
393
|
+
const fields = architectureContract.definitions.style.properties;
|
|
456
394
|
const style = value === undefined ? {} : expectObject(value, path);
|
|
457
395
|
rejectUnknownKeys(style, STYLE_KEYS, path);
|
|
458
|
-
const dash = textValue(style.dash, `${path}.dash`, defaults.dash || "",
|
|
396
|
+
const dash = textValue(style.dash, `${path}.dash`, defaults.dash || "", fields.dash.maxLength);
|
|
459
397
|
if (dash && !/^\d+(?:\.\d+)?(?:[ ,]+\d+(?:\.\d+)?)*$/.test(dash)) {
|
|
460
398
|
fail(
|
|
461
399
|
`${path}.dash`,
|
|
@@ -467,15 +405,14 @@ function normalizeStyle(value, path, defaults) {
|
|
|
467
405
|
fill: colorValue(style.fill, `${path}.fill`, defaults.fill),
|
|
468
406
|
stroke: colorValue(style.stroke, `${path}.stroke`, defaults.stroke),
|
|
469
407
|
textColor: colorValue(style.textColor, `${path}.textColor`, defaults.textColor),
|
|
470
|
-
strokeWidth:
|
|
471
|
-
fontSize:
|
|
472
|
-
opacity:
|
|
408
|
+
strokeWidth: contractNumber(style.strokeWidth, `${path}.strokeWidth`, fields.strokeWidth, defaults.strokeWidth),
|
|
409
|
+
fontSize: contractNumber(style.fontSize, `${path}.fontSize`, fields.fontSize, defaults.fontSize),
|
|
410
|
+
opacity: contractNumber(style.opacity, `${path}.opacity`, fields.opacity, defaults.opacity ?? 1),
|
|
473
411
|
dash,
|
|
474
|
-
cornerRadius:
|
|
412
|
+
cornerRadius: contractNumber(
|
|
475
413
|
style.cornerRadius,
|
|
476
414
|
`${path}.cornerRadius`,
|
|
477
|
-
|
|
478
|
-
200,
|
|
415
|
+
fields.cornerRadius,
|
|
479
416
|
defaults.cornerRadius ?? 24,
|
|
480
417
|
),
|
|
481
418
|
};
|
|
@@ -483,20 +420,21 @@ function normalizeStyle(value, path, defaults) {
|
|
|
483
420
|
|
|
484
421
|
function normalizePoint(value, path, origin) {
|
|
485
422
|
const point = expectObject(value, path);
|
|
486
|
-
rejectUnknownKeys(point, new Set(
|
|
423
|
+
rejectUnknownKeys(point, new Set(Object.keys(architectureContract.definitions.point.properties)), path);
|
|
487
424
|
return {
|
|
488
|
-
x: origin.x +
|
|
489
|
-
y: origin.y +
|
|
425
|
+
x: origin.x + contractNumber(point.x, `${path}.x`, architectureContract.definitions.coordinate),
|
|
426
|
+
y: origin.y + contractNumber(point.y, `${path}.y`, architectureContract.definitions.coordinate),
|
|
490
427
|
};
|
|
491
428
|
}
|
|
492
429
|
|
|
493
430
|
function parseCanvas(value) {
|
|
494
431
|
if (value === undefined) return { ...DEFAULT_CANVAS };
|
|
432
|
+
const fields = architectureContract.definitions.canvas.properties;
|
|
495
433
|
const canvas = expectObject(value, "canvas");
|
|
496
|
-
rejectUnknownKeys(canvas, new Set(
|
|
434
|
+
rejectUnknownKeys(canvas, new Set(Object.keys(architectureContract.definitions.canvas.properties)), "canvas");
|
|
497
435
|
return {
|
|
498
|
-
width:
|
|
499
|
-
height:
|
|
436
|
+
width: contractNumber(canvas.width, "canvas.width", fields.width, DEFAULT_CANVAS.width),
|
|
437
|
+
height: contractNumber(canvas.height, "canvas.height", fields.height, DEFAULT_CANVAS.height),
|
|
500
438
|
};
|
|
501
439
|
}
|
|
502
440
|
|
|
@@ -514,23 +452,25 @@ function parseLayout(value, path) {
|
|
|
514
452
|
};
|
|
515
453
|
}
|
|
516
454
|
const layout = expectObject(value, path);
|
|
455
|
+
const fields = architectureContract.definitions.layoutObject.properties;
|
|
517
456
|
rejectUnknownKeys(layout, LAYOUT_KEYS, path);
|
|
518
|
-
const gap =
|
|
457
|
+
const gap = contractNumber(layout.gap, `${path}.gap`, fields.gap, 36);
|
|
519
458
|
const type = enumValue(layout.type, `${path}.type`, LAYOUTS);
|
|
520
459
|
if (layout.direction !== undefined && type !== "layered") {
|
|
521
460
|
fail(
|
|
522
461
|
`${path}.direction`,
|
|
523
462
|
"is only valid with layered layout",
|
|
524
463
|
'set "type": "layered" or remove the direction',
|
|
464
|
+
{ code: "invalid_condition" },
|
|
525
465
|
);
|
|
526
466
|
}
|
|
527
467
|
return {
|
|
528
468
|
type,
|
|
529
469
|
gap,
|
|
530
|
-
rowGap:
|
|
531
|
-
columnGap:
|
|
532
|
-
padding:
|
|
533
|
-
columns: Math.trunc(
|
|
470
|
+
rowGap: contractNumber(layout.rowGap, `${path}.rowGap`, fields.rowGap, gap),
|
|
471
|
+
columnGap: contractNumber(layout.columnGap, `${path}.columnGap`, fields.columnGap, gap),
|
|
472
|
+
padding: contractNumber(layout.padding, `${path}.padding`, fields.padding, 54),
|
|
473
|
+
columns: Math.trunc(contractNumber(layout.columns, `${path}.columns`, fields.columns, 3)),
|
|
534
474
|
direction: enumValue(
|
|
535
475
|
layout.direction,
|
|
536
476
|
`${path}.direction`,
|
|
@@ -704,6 +644,7 @@ function layoutPlacements(children, group, layout, path, graphEdges = []) {
|
|
|
704
644
|
`${path}.layout`,
|
|
705
645
|
"padding and title leave no space for children",
|
|
706
646
|
"reduce layout padding or increase the group width and height",
|
|
647
|
+
{ code: "layout_fit", category: "layout" },
|
|
707
648
|
);
|
|
708
649
|
}
|
|
709
650
|
const count = flowItems.length;
|
|
@@ -740,6 +681,7 @@ function layoutPlacements(children, group, layout, path, graphEdges = []) {
|
|
|
740
681
|
`${path}.layout`,
|
|
741
682
|
"children do not fit",
|
|
742
683
|
"reduce layout gap/padding, enlarge the group, or move some children out",
|
|
684
|
+
{ code: "layout_fit", category: "layout" },
|
|
743
685
|
);
|
|
744
686
|
}
|
|
745
687
|
tracks.forEach((track, trackIndex) => {
|
|
@@ -767,18 +709,20 @@ function layoutPlacements(children, group, layout, path, graphEdges = []) {
|
|
|
767
709
|
const width = numberIn(
|
|
768
710
|
child.width,
|
|
769
711
|
`${path}.children[${index}].width`,
|
|
770
|
-
|
|
712
|
+
architectureContract.definitions.extent.minimum,
|
|
771
713
|
cellWidth,
|
|
772
714
|
defaultWidth,
|
|
773
715
|
"the parent layout limits each cell; reduce the value, enlarge the group, or drop width to use the automatic size",
|
|
716
|
+
{ code: "layout_extent", category: "layout" },
|
|
774
717
|
);
|
|
775
718
|
const height = numberIn(
|
|
776
719
|
child.height,
|
|
777
720
|
`${path}.children[${index}].height`,
|
|
778
|
-
|
|
721
|
+
architectureContract.definitions.extent.minimum,
|
|
779
722
|
cellHeight,
|
|
780
723
|
defaultHeight,
|
|
781
724
|
"the parent layout limits each cell; reduce the value, enlarge the group, or drop height to use the automatic size",
|
|
725
|
+
{ code: "layout_extent", category: "layout" },
|
|
782
726
|
);
|
|
783
727
|
const cellX = vertical
|
|
784
728
|
? inner.x + offset + positionInTrack * (cellWidth + layout.columnGap)
|
|
@@ -814,12 +758,12 @@ function normalizeBox(element, origin, path, placement) {
|
|
|
814
758
|
return {
|
|
815
759
|
x:
|
|
816
760
|
origin.x +
|
|
817
|
-
(placement?.x ??
|
|
761
|
+
(placement?.x ?? contractNumber(element.x, `${path}.x`, architectureContract.definitions.coordinate)),
|
|
818
762
|
y:
|
|
819
763
|
origin.y +
|
|
820
|
-
(placement?.y ??
|
|
821
|
-
width: placement?.width ??
|
|
822
|
-
height: placement?.height ??
|
|
764
|
+
(placement?.y ?? contractNumber(element.y, `${path}.y`, architectureContract.definitions.coordinate)),
|
|
765
|
+
width: placement?.width ?? contractNumber(element.width, `${path}.width`, architectureContract.definitions.extent),
|
|
766
|
+
height: placement?.height ?? contractNumber(element.height, `${path}.height`, architectureContract.definitions.extent),
|
|
823
767
|
};
|
|
824
768
|
}
|
|
825
769
|
|
|
@@ -833,12 +777,16 @@ function flattenElements(
|
|
|
833
777
|
placements = new Map(),
|
|
834
778
|
graphEdges = [],
|
|
835
779
|
) {
|
|
836
|
-
if (!Array.isArray(rawElements))
|
|
780
|
+
if (!Array.isArray(rawElements)) {
|
|
781
|
+
fail(path, "must be an array", "use a JSON array such as [ ]",
|
|
782
|
+
{ code: rawElements === undefined ? "missing_required" : "invalid_type" });
|
|
783
|
+
}
|
|
837
784
|
if (depth > MAX_DEPTH) {
|
|
838
785
|
fail(
|
|
839
786
|
path,
|
|
840
787
|
`nesting must not exceed ${MAX_DEPTH} levels`,
|
|
841
788
|
"flatten the structure or split the diagram across slides",
|
|
789
|
+
{ code: "nesting_limit" },
|
|
842
790
|
);
|
|
843
791
|
}
|
|
844
792
|
|
|
@@ -848,24 +796,29 @@ function flattenElements(
|
|
|
848
796
|
"elements",
|
|
849
797
|
`must contain at most ${MAX_ELEMENTS} items`,
|
|
850
798
|
"split the diagram across multiple slides",
|
|
799
|
+
{ code: "element_limit" },
|
|
851
800
|
);
|
|
852
801
|
}
|
|
853
802
|
const elementPath = `${path}[${localIndex}]`;
|
|
854
803
|
const element = expectObject(raw, elementPath);
|
|
855
804
|
const type = textValue(element.type, `${elementPath}.type`, "", 20);
|
|
856
805
|
if (!Object.prototype.hasOwnProperty.call(ELEMENT_KEYS, type)) {
|
|
806
|
+
const supported = Object.keys(ELEMENT_KEYS);
|
|
807
|
+
const choices = `${supported.slice(0, -1).join(", ")}, or ${supported.at(-1)}`;
|
|
857
808
|
fail(
|
|
858
809
|
`${elementPath}.type`,
|
|
859
|
-
|
|
810
|
+
`must be ${choices}`,
|
|
860
811
|
element.type === undefined
|
|
861
|
-
?
|
|
862
|
-
: `replace ${describeValue(element.type)} with
|
|
812
|
+
? `add a "type" of ${choices}`
|
|
813
|
+
: `replace ${describeValue(element.type)} with ${choices}`,
|
|
814
|
+
{ code: element.type === undefined ? "missing_required" : "invalid_value" },
|
|
863
815
|
);
|
|
864
816
|
}
|
|
865
817
|
rejectUnknownKeys(element, ELEMENT_KEYS[type], elementPath);
|
|
818
|
+
const fields = architectureContract.elements[type].properties;
|
|
866
819
|
const order = output.length;
|
|
867
820
|
const defaultZ = type === "group" ? -50 : type === "connector" ? -10 : 0;
|
|
868
|
-
const z =
|
|
821
|
+
const z = contractNumber(element.z, `${elementPath}.z`, fields.z, defaultZ);
|
|
869
822
|
|
|
870
823
|
if (type === "connector") {
|
|
871
824
|
const routing = enumValue(
|
|
@@ -880,6 +833,7 @@ function flattenElements(
|
|
|
880
833
|
`${elementPath}.points`,
|
|
881
834
|
`must be an array with at most ${MAX_POINTS} points`,
|
|
882
835
|
"remove extra waypoints or split the connector into several connectors",
|
|
836
|
+
{ code: Array.isArray(points) ? "item_limit" : "invalid_type" },
|
|
883
837
|
);
|
|
884
838
|
}
|
|
885
839
|
if (routing !== "polyline" && points.length) {
|
|
@@ -887,6 +841,7 @@ function flattenElements(
|
|
|
887
841
|
`${elementPath}.points`,
|
|
888
842
|
"is only valid with polyline routing",
|
|
889
843
|
'set "routing": "polyline" or remove the points',
|
|
844
|
+
{ code: "invalid_condition" },
|
|
890
845
|
);
|
|
891
846
|
}
|
|
892
847
|
if (element.arrow !== undefined && typeof element.arrow !== "boolean") {
|
|
@@ -894,6 +849,7 @@ function flattenElements(
|
|
|
894
849
|
`${elementPath}.arrow`,
|
|
895
850
|
"must be a boolean",
|
|
896
851
|
`replace ${describeValue(element.arrow)} with true or false`,
|
|
852
|
+
{ code: "invalid_type" },
|
|
897
853
|
);
|
|
898
854
|
}
|
|
899
855
|
output.push({
|
|
@@ -902,14 +858,14 @@ function flattenElements(
|
|
|
902
858
|
to: idValue(element.to, `${elementPath}.to`),
|
|
903
859
|
fromPort: enumValue(element.fromPort, `${elementPath}.fromPort`, PORTS, "auto"),
|
|
904
860
|
toPort: enumValue(element.toPort, `${elementPath}.toPort`, PORTS, "auto"),
|
|
905
|
-
label: textValue(element.label, `${elementPath}.label`, "",
|
|
861
|
+
label: textValue(element.label, `${elementPath}.label`, "", fields.label.maxLength),
|
|
906
862
|
labelLayer: enumValue(
|
|
907
863
|
element.labelLayer,
|
|
908
864
|
`${elementPath}.labelLayer`,
|
|
909
865
|
LABEL_LAYERS,
|
|
910
866
|
"front",
|
|
911
867
|
),
|
|
912
|
-
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "",
|
|
868
|
+
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "", fields.ariaLabel.maxLength),
|
|
913
869
|
routing,
|
|
914
870
|
points: points.map((point, index) =>
|
|
915
871
|
normalizePoint(point, `${elementPath}.points[${index}]`, origin),
|
|
@@ -918,7 +874,7 @@ function flattenElements(
|
|
|
918
874
|
lane:
|
|
919
875
|
element.lane === undefined
|
|
920
876
|
? null
|
|
921
|
-
: Math.trunc(
|
|
877
|
+
: Math.trunc(contractNumber(element.lane, `${elementPath}.lane`, fields.lane)),
|
|
922
878
|
z,
|
|
923
879
|
order,
|
|
924
880
|
sourcePath: elementPath,
|
|
@@ -936,14 +892,7 @@ function flattenElements(
|
|
|
936
892
|
}
|
|
937
893
|
|
|
938
894
|
const id = idValue(element.id, `${elementPath}.id`);
|
|
939
|
-
|
|
940
|
-
fail(
|
|
941
|
-
`${elementPath}.id`,
|
|
942
|
-
`duplicates '${id}'`,
|
|
943
|
-
"give every node, group, and image a unique id across the whole diagram",
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
ids.add(id);
|
|
895
|
+
claimArchitectureId(ids, id, `${elementPath}.id`);
|
|
947
896
|
const box = normalizeBox(element, origin, elementPath, placements.get(localIndex));
|
|
948
897
|
|
|
949
898
|
if (type === "node") {
|
|
@@ -959,9 +908,9 @@ function flattenElements(
|
|
|
959
908
|
id,
|
|
960
909
|
shape,
|
|
961
910
|
...box,
|
|
962
|
-
text: textValue(element.text, `${elementPath}.text`, "",
|
|
911
|
+
text: textValue(element.text, `${elementPath}.text`, "", fields.text.maxLength),
|
|
963
912
|
icon,
|
|
964
|
-
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "",
|
|
913
|
+
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "", fields.ariaLabel.maxLength),
|
|
965
914
|
z,
|
|
966
915
|
order,
|
|
967
916
|
sourcePath: elementPath,
|
|
@@ -985,7 +934,7 @@ function flattenElements(
|
|
|
985
934
|
...box,
|
|
986
935
|
src: assetPathValue(element.src, `${elementPath}.src`),
|
|
987
936
|
fit: enumValue(element.fit, `${elementPath}.fit`, IMAGE_FITS, "contain"),
|
|
988
|
-
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "",
|
|
937
|
+
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "", fields.ariaLabel.maxLength),
|
|
989
938
|
z,
|
|
990
939
|
order,
|
|
991
940
|
sourcePath: elementPath,
|
|
@@ -1002,6 +951,11 @@ function flattenElements(
|
|
|
1002
951
|
return;
|
|
1003
952
|
}
|
|
1004
953
|
|
|
954
|
+
if (type !== "group") {
|
|
955
|
+
fail(`${elementPath}.type`, "has no runtime implementation",
|
|
956
|
+
"use a supported element or update the runtime together with its contract",
|
|
957
|
+
{ code: "unsupported_element" });
|
|
958
|
+
}
|
|
1005
959
|
const groupStyle = normalizeStyle(element.style, `${elementPath}.style`, {
|
|
1006
960
|
fill: "accentSoft",
|
|
1007
961
|
stroke: "accentLine",
|
|
@@ -1016,8 +970,8 @@ function flattenElements(
|
|
|
1016
970
|
type,
|
|
1017
971
|
id,
|
|
1018
972
|
...box,
|
|
1019
|
-
title: textValue(element.title, `${elementPath}.title`, "",
|
|
1020
|
-
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "",
|
|
973
|
+
title: textValue(element.title, `${elementPath}.title`, "", fields.title.maxLength),
|
|
974
|
+
ariaLabel: textValue(element.ariaLabel, `${elementPath}.ariaLabel`, "", fields.ariaLabel.maxLength),
|
|
1021
975
|
layout: parseLayout(element.layout, `${elementPath}.layout`),
|
|
1022
976
|
z,
|
|
1023
977
|
order,
|
|
@@ -1026,6 +980,10 @@ function flattenElements(
|
|
|
1026
980
|
};
|
|
1027
981
|
output.push(group);
|
|
1028
982
|
const children = element.children === undefined ? [] : element.children;
|
|
983
|
+
if (!Array.isArray(children)) {
|
|
984
|
+
fail(`${elementPath}.children`, "must be an array", "use a JSON array such as [ ]",
|
|
985
|
+
{ code: "invalid_type" });
|
|
986
|
+
}
|
|
1029
987
|
const childPlacements = layoutPlacements(
|
|
1030
988
|
children,
|
|
1031
989
|
group,
|
|
@@ -1096,6 +1054,7 @@ function assignConnectorLanes(elements) {
|
|
|
1096
1054
|
`${connector.sourcePath}.lane`,
|
|
1097
1055
|
`duplicates explicit lane ${connector.lane}`,
|
|
1098
1056
|
"give overlapping connectors different lane values, or omit lane to assign them automatically",
|
|
1057
|
+
{ code: "duplicate_lane", category: "layout" },
|
|
1099
1058
|
);
|
|
1100
1059
|
}
|
|
1101
1060
|
});
|
|
@@ -1135,6 +1094,7 @@ function assignConnectorLanes(elements) {
|
|
|
1135
1094
|
record.connector.sourcePath,
|
|
1136
1095
|
"has no available connector lane",
|
|
1137
1096
|
"reduce the number of connectors between the same elements, or set explicit lane values",
|
|
1097
|
+
{ code: "lane_unavailable", category: "layout" },
|
|
1138
1098
|
);
|
|
1139
1099
|
}
|
|
1140
1100
|
record.connector.lane = selected;
|
|
@@ -1208,29 +1168,35 @@ export function normalizeArchitectureSource(source) {
|
|
|
1208
1168
|
return typeof source === "string" && source.trim() === "" ? EMPTY_ARCHITECTURE_SOURCE : source;
|
|
1209
1169
|
}
|
|
1210
1170
|
|
|
1211
|
-
|
|
1171
|
+
function readArchitectureSource(source) {
|
|
1212
1172
|
if (typeof source !== "string") {
|
|
1213
|
-
fail("diagram", "must be JSON text", "pass the fenced block contents as a string"
|
|
1173
|
+
fail("diagram", "must be JSON text", "pass the fenced block contents as a string",
|
|
1174
|
+
{ code: "invalid_type", category: "json" });
|
|
1214
1175
|
}
|
|
1215
1176
|
if (source.length > MAX_SOURCE_LENGTH) {
|
|
1216
1177
|
fail(
|
|
1217
1178
|
"diagram",
|
|
1218
1179
|
`must be at most ${MAX_SOURCE_LENGTH} characters`,
|
|
1219
1180
|
"split the diagram across multiple slides",
|
|
1181
|
+
{ code: "source_limit", category: "json" },
|
|
1220
1182
|
);
|
|
1221
1183
|
}
|
|
1222
1184
|
const normalizedSource = normalizeArchitectureSource(source);
|
|
1223
|
-
let raw;
|
|
1224
1185
|
try {
|
|
1225
|
-
|
|
1186
|
+
return JSON.parse(normalizedSource);
|
|
1226
1187
|
} catch (error) {
|
|
1188
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1227
1189
|
const detail = error?.message ? ` (${error.message})` : "";
|
|
1228
1190
|
fail(
|
|
1229
1191
|
"diagram",
|
|
1230
1192
|
`contains invalid JSON${detail}`,
|
|
1231
1193
|
"check for trailing commas, unquoted keys, or missing braces",
|
|
1194
|
+
{ code: "invalid_json", category: "json" },
|
|
1232
1195
|
);
|
|
1233
1196
|
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function parseArchitectureModel(raw, stages) {
|
|
1234
1200
|
const root = expectObject(
|
|
1235
1201
|
raw,
|
|
1236
1202
|
"diagram",
|
|
@@ -1238,7 +1204,7 @@ export function parseArchitecture(source) {
|
|
|
1238
1204
|
);
|
|
1239
1205
|
rejectUnknownKeys(
|
|
1240
1206
|
root,
|
|
1241
|
-
new Set(
|
|
1207
|
+
new Set(Object.keys(architectureContract.root.properties)),
|
|
1242
1208
|
"diagram",
|
|
1243
1209
|
);
|
|
1244
1210
|
const version = numberIn(
|
|
@@ -1255,12 +1221,12 @@ export function parseArchitecture(source) {
|
|
|
1255
1221
|
const model = {
|
|
1256
1222
|
version,
|
|
1257
1223
|
canvas: parseCanvas(root.canvas),
|
|
1258
|
-
title: textValue(root.title, "title", "Architecture diagram",
|
|
1224
|
+
title: textValue(root.title, "title", "Architecture diagram", architectureContract.root.properties.title.maxLength),
|
|
1259
1225
|
description: textValue(
|
|
1260
1226
|
root.description,
|
|
1261
1227
|
"description",
|
|
1262
1228
|
"Architecture diagram rendered from a constrained JSON DSL.",
|
|
1263
|
-
|
|
1229
|
+
architectureContract.root.properties.description.maxLength,
|
|
1264
1230
|
),
|
|
1265
1231
|
elements: [],
|
|
1266
1232
|
};
|
|
@@ -1275,40 +1241,14 @@ export function parseArchitecture(source) {
|
|
|
1275
1241
|
new Map(),
|
|
1276
1242
|
collectGraphEdges(root.elements),
|
|
1277
1243
|
);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
);
|
|
1281
|
-
let connectorCount = 0;
|
|
1282
|
-
model.elements.forEach((element) => {
|
|
1283
|
-
if (element.type !== "connector") return;
|
|
1284
|
-
connectorCount += 1;
|
|
1285
|
-
if (!connectable.has(element.from)) {
|
|
1286
|
-
fail(
|
|
1287
|
-
`${element.sourcePath}.from`,
|
|
1288
|
-
`references unknown element '${element.from}'`,
|
|
1289
|
-
`add a node or group with id '${element.from}', or point the connector at an existing id`,
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
if (!connectable.has(element.to)) {
|
|
1293
|
-
fail(
|
|
1294
|
-
`${element.sourcePath}.to`,
|
|
1295
|
-
`references unknown element '${element.to}'`,
|
|
1296
|
-
`add a node or group with id '${element.to}', or point the connector at an existing id`,
|
|
1297
|
-
);
|
|
1298
|
-
}
|
|
1299
|
-
if (element.from === element.to) {
|
|
1300
|
-
fail(
|
|
1301
|
-
element.sourcePath,
|
|
1302
|
-
"self-referencing connectors are not supported",
|
|
1303
|
-
"point the connector at a different element",
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
});
|
|
1244
|
+
stages.structure = "passed";
|
|
1245
|
+
const connectorCount = checkArchitectureReferences(model.elements, undefined, { checkIds: false });
|
|
1307
1246
|
if (connectorCount > MAX_CONNECTORS) {
|
|
1308
1247
|
fail(
|
|
1309
1248
|
"elements",
|
|
1310
1249
|
`must contain at most ${MAX_CONNECTORS} connectors`,
|
|
1311
1250
|
"split the diagram across multiple slides",
|
|
1251
|
+
{ code: "connector_limit", category: "semantic" },
|
|
1312
1252
|
);
|
|
1313
1253
|
}
|
|
1314
1254
|
if (totalTextLength(model) > MAX_TOTAL_TEXT) {
|
|
@@ -1316,21 +1256,60 @@ export function parseArchitecture(source) {
|
|
|
1316
1256
|
"diagram",
|
|
1317
1257
|
`text content must be at most ${MAX_TOTAL_TEXT} characters`,
|
|
1318
1258
|
"shorten node text, group titles, labels, and descriptions",
|
|
1259
|
+
{ code: "total_text_limit", category: "semantic" },
|
|
1319
1260
|
);
|
|
1320
1261
|
}
|
|
1262
|
+
stages.semantic = "passed";
|
|
1321
1263
|
assignConnectorLanes(model.elements);
|
|
1264
|
+
stages.layout = "passed";
|
|
1322
1265
|
model.elements = model.elements
|
|
1323
1266
|
.slice()
|
|
1324
1267
|
.sort((left, right) => left.z - right.z || left.order - right.order);
|
|
1325
1268
|
return model;
|
|
1326
1269
|
}
|
|
1327
1270
|
|
|
1328
|
-
function
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1271
|
+
export function validateArchitecture(source, { maxDiagnostics } = {}) {
|
|
1272
|
+
const limit = diagnosticLimit(maxDiagnostics);
|
|
1273
|
+
const stages = { json: "skipped", structure: "skipped", semantic: "skipped", layout: "skipped" };
|
|
1274
|
+
let raw;
|
|
1275
|
+
let model;
|
|
1276
|
+
try {
|
|
1277
|
+
raw = readArchitectureSource(source);
|
|
1278
|
+
stages.json = "passed";
|
|
1279
|
+
model = parseArchitectureModel(raw, stages);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (!(error instanceof ArchitectureError) || !error.diagnostic) throw error;
|
|
1282
|
+
return architectureFailureReport(raw, error.diagnostic, {
|
|
1283
|
+
maxDiagnostics: limit,
|
|
1284
|
+
maxElements: MAX_ELEMENTS,
|
|
1285
|
+
maxDepth: MAX_DEPTH,
|
|
1286
|
+
stages,
|
|
1287
|
+
});
|
|
1332
1288
|
}
|
|
1333
|
-
|
|
1289
|
+
const warnings = architectureCompatibilityWarnings(raw);
|
|
1290
|
+
return {
|
|
1291
|
+
valid: true,
|
|
1292
|
+
complete: true,
|
|
1293
|
+
truncated: false,
|
|
1294
|
+
truncationReasons: [],
|
|
1295
|
+
stages,
|
|
1296
|
+
diagnostics: warnings.diagnostics,
|
|
1297
|
+
model,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export function parseArchitecture(source) {
|
|
1302
|
+
const validation = validateArchitecture(source);
|
|
1303
|
+
if (validation.valid) return validation.model;
|
|
1304
|
+
const diagnostic = validation.diagnostics[0];
|
|
1305
|
+
const error = new ArchitectureError(diagnostic.message, diagnostic);
|
|
1306
|
+
error.validation = validation;
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function svgElement(documentRef, tag, attributes = {}) {
|
|
1311
|
+
// Produce portable visual detail; scene-svg is the only SVG DOM writer.
|
|
1312
|
+
return svgPrimitive(tag, attributes);
|
|
1334
1313
|
}
|
|
1335
1314
|
|
|
1336
1315
|
function appendSvgTitle(documentRef, parent, text) {
|
|
@@ -3914,13 +3893,13 @@ export function architectureSemanticSnapshot(model) {
|
|
|
3914
3893
|
* export. Coordinates remain in Architecture canvas units here; renderer.js
|
|
3915
3894
|
* maps them through the rendered SVG viewBox into slide pixels.
|
|
3916
3895
|
*/
|
|
3917
|
-
export function architecturePowerPointSnapshot(model, documentRef = globalThis.document) {
|
|
3896
|
+
export function architecturePowerPointSnapshot(model, documentRef = globalThis.document, routingPlan) {
|
|
3918
3897
|
const lookup = new Map(
|
|
3919
3898
|
model.elements
|
|
3920
3899
|
.filter((element) => element.type !== "connector")
|
|
3921
3900
|
.map((element) => [element.id, element]),
|
|
3922
3901
|
);
|
|
3923
|
-
const { routes, diagnostics } = planConnectorRoutes(model, lookup);
|
|
3902
|
+
const { routes, diagnostics } = routingPlan || planConnectorRoutes(model, lookup);
|
|
3924
3903
|
const objects = [];
|
|
3925
3904
|
const fallbacks = [];
|
|
3926
3905
|
const icons = [];
|
|
@@ -4171,8 +4150,8 @@ export function renderArchitectureDiagram(
|
|
|
4171
4150
|
.filter((element) => element.type !== "connector")
|
|
4172
4151
|
.map((element) => [element.id, element]),
|
|
4173
4152
|
);
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4153
|
+
const routingPlan = planConnectorRoutes(model, lookup);
|
|
4154
|
+
const { routes: connectorRoutes, diagnostics: routingDiagnostics } = routingPlan;
|
|
4176
4155
|
model.elements.forEach((element, index) => {
|
|
4177
4156
|
if (element.type === "image") {
|
|
4178
4157
|
const clipPath = svgElement(documentRef, "clipPath", {
|
|
@@ -4214,16 +4193,21 @@ export function renderArchitectureDiagram(
|
|
|
4214
4193
|
// Table for announcing connector endpoints by visible label; traverse elements only once.
|
|
4215
4194
|
const endpointNames = endpointDisplayNames(model.elements);
|
|
4216
4195
|
const frontLabels = [];
|
|
4196
|
+
const visuals = new Map();
|
|
4217
4197
|
|
|
4218
4198
|
model.elements.forEach((element, index) => {
|
|
4219
4199
|
if (element.type === "group") {
|
|
4220
|
-
|
|
4200
|
+
const primitive = renderGroup(documentRef, element);
|
|
4201
|
+
visuals.set(`group:${element.order}`, primitive);
|
|
4202
|
+
svg.appendChild(primitive);
|
|
4221
4203
|
} else if (element.type === "node") {
|
|
4222
|
-
|
|
4204
|
+
const primitive = renderNode(documentRef, element);
|
|
4205
|
+
visuals.set(`node:${element.order}`, primitive);
|
|
4206
|
+
svg.appendChild(primitive);
|
|
4223
4207
|
} else if (element.type === "image") {
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
);
|
|
4208
|
+
const primitive = renderImage(documentRef, element, `architecture-image-clip-${renderId}-${index}`);
|
|
4209
|
+
visuals.set(`image-picture:${element.order}`, primitive);
|
|
4210
|
+
svg.appendChild(primitive);
|
|
4227
4211
|
} else {
|
|
4228
4212
|
const rendered = renderConnector(
|
|
4229
4213
|
documentRef,
|
|
@@ -4233,13 +4217,33 @@ export function renderArchitectureDiagram(
|
|
|
4233
4217
|
endpointNames,
|
|
4234
4218
|
model.canvas,
|
|
4235
4219
|
);
|
|
4220
|
+
visuals.set(`connector:${element.order}`, rendered.group);
|
|
4236
4221
|
svg.appendChild(rendered.group);
|
|
4237
|
-
if (rendered.frontLabel)
|
|
4222
|
+
if (rendered.frontLabel) {
|
|
4223
|
+
visuals.set(`connector-label:${element.order}`, rendered.frontLabel);
|
|
4224
|
+
frontLabels.push(rendered.frontLabel);
|
|
4225
|
+
}
|
|
4238
4226
|
}
|
|
4239
4227
|
});
|
|
4240
4228
|
frontLabels.forEach((label) => svg.appendChild(label));
|
|
4241
|
-
|
|
4242
|
-
const
|
|
4229
|
+
const powerPointSnapshot = architecturePowerPointSnapshot(model, documentRef, routingPlan);
|
|
4230
|
+
const { scene } = architectureSnapshotToScene(powerPointSnapshot);
|
|
4231
|
+
const slots = new Map();
|
|
4232
|
+
scene.nodes.forEach((node, index) => {
|
|
4233
|
+
const source = node.meta?.architecture;
|
|
4234
|
+
const visual = visuals.get(`${source?.kind}:${source?.order}`);
|
|
4235
|
+
if (visual) {
|
|
4236
|
+
node.meta.svg = visual;
|
|
4237
|
+
slots.set(visual, index);
|
|
4238
|
+
} else {
|
|
4239
|
+
// Icons and back-layer labels are painted within their owning node/connector
|
|
4240
|
+
// so opacity, accessible names and editor hit targets remain a single subtree.
|
|
4241
|
+
node.meta = { ...node.meta, svgOwner: source?.order ?? index };
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
svg.children = svg.children.map((child) => slots.has(child) ? { sceneNode: slots.get(child) } : child);
|
|
4245
|
+
scene.meta = { svgRoot: svg };
|
|
4246
|
+
wrapper.appendChild(sceneToSvg(scene, { document: documentRef, template: svg }));
|
|
4243
4247
|
Object.defineProperty(wrapper, "__presentationPptxSnapshot", {
|
|
4244
4248
|
value: powerPointSnapshot,
|
|
4245
4249
|
enumerable: true,
|