@markdstage/markdstage 3.2.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 +11 -1
- package/shared/renderer/architecture-scene.mjs +312 -0
- package/shared/renderer/architecture.mjs +41 -18
- 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 +3 -0
- package/shared/runtime/pptx-package.mjs +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdstage/markdstage",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "runceel",
|
package/shared/README.md
CHANGED
|
@@ -108,6 +108,16 @@ The themed slide is displayed and updates automatically
|
|
|
108
108
|
notes are converted from Markdown to readable plain text in the corresponding
|
|
109
109
|
PowerPoint notes pane. AI may call `export_pptx` with another
|
|
110
110
|
workspace-confined `.pptx` path.
|
|
111
|
+
- **Both export buttons show a visible notification outside More controls.**
|
|
112
|
+
Progress remains visible while saving, and both buttons are disabled until
|
|
113
|
+
the operation finishes. Export buttons return to their normal appearance and
|
|
114
|
+
tooltips after success or failure; the notification carries the result.
|
|
115
|
+
Success appears only after the file has been saved,
|
|
116
|
+
with its file name and selectable save location. It dismisses after eight
|
|
117
|
+
seconds, pausing while hovered or keyboard-focused. Errors remain until
|
|
118
|
+
dismissed or another export starts. Success and error notifications also have
|
|
119
|
+
a dismiss button and are announced to screen readers. Notifications never
|
|
120
|
+
appear in exported files or the external presentation window.
|
|
111
121
|
- Use **More controls > Output preview** to letterbox the current slide inside
|
|
112
122
|
the canvas with
|
|
113
123
|
the same fixed 1280×720 typography, spacing, diagram limits, and clipping used
|
|
@@ -1038,7 +1048,7 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
1038
1048
|
| `inspect_layout` | `{ index?: number, includeFits?: boolean }`. Render the registered in-memory PDF snapshot with the fixed 1280×720 output layout; this does not inspect the source file on disk. Omit `index` for one preferred whole-deck inspection. Serialize targeted calls because PDF, layout, and PNG jobs are exclusive. By default, return only clipped pages; `includeFits` includes successful pages. Returns dimensions, issue counts, overflow measurements, nested scroll containers, and a bounded list of element hints. Requires Edge, Chrome, or Chromium. |
|
|
1039
1049
|
| `capture_slides` | `{ indexes?: number[], outputDirectory?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Generate PDF-equivalent 1280×720 PNGs for at most 10 zero-based indexes. When `indexes` is omitted, inspect the deck and capture only clipped pages. Paths stay inside the workspace; results contain paths and layout summaries, not image bytes. Requires Edge, Chrome, or Chromium. |
|
|
1040
1050
|
| `export_pdf` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export one 16:9 page per slide. Relative paths use workspace root; default is `markdstage.pdf`. Theme affects PDF only. Reject paths outside workspace and non-`.pdf` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes }`. Requires Edge, Chrome, or Chromium. |
|
|
1041
|
-
| `export_pptx` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export a hybrid editable 16:9 PowerPoint deck. Supported text, lists, links, tables, fenced code blocks, raster images, and Architecture DSL objects remain native. Code blocks preserve syntax-colored editable runs, whitespace, monospace typography, and block decoration; unsupported effects such as shadows remain reported background fallbacks. Speaker-note Markdown is exported as readable plain text in the corresponding PowerPoint notes pane. Architecture nodes, groups, and connector-label pills are visible AutoShapes with integrated text; icons are foreground pictures. Mermaid and unsupported visuals become reported background fallbacks. Relative paths use workspace root; default is `markdstage.pptx`. Reject paths outside workspace and non-`.pptx` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes, format, fallbackCount, fallbacks }`. Requires Edge, Chrome, or Chromium. |
|
|
1051
|
+
| `export_pptx` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export a hybrid editable 16:9 PowerPoint deck. Supported text, lists, links, tables, fenced code blocks, raster images, Mermaid diagrams, and Architecture DSL objects remain native. Code blocks preserve syntax-colored editable runs, whitespace, monospace typography, and block decoration; unsupported effects such as shadows remain reported background fallbacks. Speaker-note Markdown is exported as readable plain text in the corresponding PowerPoint notes pane. Architecture nodes, groups, and connector-label pills are visible AutoShapes with integrated text; icons are foreground pictures. Mermaid diagrams export as editable shapes, text, and connectors with unsupported SVG details preserved as per-element fallback pictures. Unsupported visuals become reported background fallbacks. Relative paths use workspace root; default is `markdstage.pptx`. Reject paths outside workspace and non-`.pptx` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes, format, fallbackCount, fallbacks }`. Requires Edge, Chrome, or Chromium. |
|
|
1042
1052
|
| `edit_architecture` | `{ enabled: boolean }`. Toggle placement editing. Imported decks also write to the source fence; direct decks write to canvas state. Presenter/print omit UI. Mode is not persisted and `reset` disables it. Returns `{ ok, enabled, version }`. |
|
|
1043
1053
|
| `reset` | No input. Clear deck/slide state, disable editing, and return to the waiting view. |
|
|
1044
1054
|
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createScene,
|
|
3
|
+
normalizeScene,
|
|
4
|
+
validateScene,
|
|
5
|
+
} from "./scene-graph.mjs";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SOURCE_PATH = "architecture";
|
|
8
|
+
|
|
9
|
+
function roundedMetric(value) {
|
|
10
|
+
return Math.round(Math.max(0, Number(value) || 0) * 10) / 10;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function finiteNumberOr(value, fallback) {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function nonEmptyStringOr(value, fallback) {
|
|
18
|
+
return typeof value === "string" && value ? value : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function transformBounds(bounds, options) {
|
|
22
|
+
return {
|
|
23
|
+
x: roundedMetric(options.originX + finiteNumberOr(bounds?.x, 0) * options.scale),
|
|
24
|
+
y: roundedMetric(options.originY + finiteNumberOr(bounds?.y, 0) * options.scale),
|
|
25
|
+
width: roundedMetric(finiteNumberOr(bounds?.width, 0) * options.scale),
|
|
26
|
+
height: roundedMetric(finiteNumberOr(bounds?.height, 0) * options.scale),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function transformPoint(point, options) {
|
|
31
|
+
return {
|
|
32
|
+
x: roundedMetric(options.originX + finiteNumberOr(point?.x, 0) * options.scale),
|
|
33
|
+
y: roundedMetric(options.originY + finiteNumberOr(point?.y, 0) * options.scale),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scaledMetric(value, options) {
|
|
38
|
+
return value === undefined ? undefined : roundedMetric(finiteNumberOr(value, 0) * options.scale);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSceneColor(value) {
|
|
42
|
+
if (value === null || value === undefined) return true;
|
|
43
|
+
if (typeof value !== "string") return false;
|
|
44
|
+
const text = value.trim();
|
|
45
|
+
if (/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text)) return true;
|
|
46
|
+
return /^rgba?\((.*)\)$/i.test(text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeColor(value, path, options, diagnostics) {
|
|
50
|
+
if (value === undefined) return undefined;
|
|
51
|
+
const resolved = options.resolveColor(value, path);
|
|
52
|
+
if (resolved === undefined) return undefined;
|
|
53
|
+
if (resolved === null || resolved === "" || resolved === "none" || resolved === "transparent") return null;
|
|
54
|
+
if (isSceneColor(resolved)) return resolved;
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
path,
|
|
57
|
+
kind: "color",
|
|
58
|
+
reason: `color is not resolved for scene graph: ${String(value)}`,
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function definedEntries(object) {
|
|
64
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mapStyle(object, path, options, diagnostics) {
|
|
68
|
+
const dash = object.dash === undefined ? undefined : options.resolveDash(object.dash, path);
|
|
69
|
+
const sceneDash = (() => {
|
|
70
|
+
if (dash === undefined) return undefined;
|
|
71
|
+
if (dash === "" || dash === "solid" || dash === "dash" || dash === "dashDot" || dash === "dot") return dash;
|
|
72
|
+
if (dash === "dotted") return "dot";
|
|
73
|
+
const values = String(dash)
|
|
74
|
+
.trim()
|
|
75
|
+
.split(/[ ,]+/)
|
|
76
|
+
.map(Number)
|
|
77
|
+
.filter(Number.isFinite);
|
|
78
|
+
if (values.length >= 2 && values[0] <= 2 && values[1] >= values[0] * 2) return "dot";
|
|
79
|
+
if (values.length >= 2) return "dash";
|
|
80
|
+
return "solid";
|
|
81
|
+
})();
|
|
82
|
+
return definedEntries({
|
|
83
|
+
fill: normalizeColor(object.fill, `${path}.fill`, options, diagnostics),
|
|
84
|
+
stroke: normalizeColor(object.stroke, `${path}.stroke`, options, diagnostics),
|
|
85
|
+
strokeWidth: scaledMetric(object.strokeWidth, options),
|
|
86
|
+
dash: sceneDash,
|
|
87
|
+
opacity: object.opacity,
|
|
88
|
+
cornerRadius: scaledMetric(object.cornerRadius, options),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mapText(text, path, options, diagnostics) {
|
|
93
|
+
if (!text?.paragraphs) return text;
|
|
94
|
+
return {
|
|
95
|
+
paragraphs: text.paragraphs.map((paragraph, paragraphIndex) => ({
|
|
96
|
+
...paragraph,
|
|
97
|
+
runs: paragraph.runs.map((run, runIndex) => {
|
|
98
|
+
const runPath = `${path}.paragraphs[${paragraphIndex}].runs[${runIndex}]`;
|
|
99
|
+
return definedEntries({
|
|
100
|
+
...run,
|
|
101
|
+
fontFace: options.fontFace || run.fontFace,
|
|
102
|
+
fontSize: scaledMetric(run.fontSize, options),
|
|
103
|
+
bold: Number(run.fontWeight) >= 600,
|
|
104
|
+
color: normalizeColor(run.color, `${runPath}.color`, options, diagnostics),
|
|
105
|
+
});
|
|
106
|
+
}),
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mapTextLayout(object, options) {
|
|
112
|
+
return definedEntries({
|
|
113
|
+
alignment: object.alignment,
|
|
114
|
+
verticalAlignment: object.verticalAlignment,
|
|
115
|
+
textWrap: object.textWrap,
|
|
116
|
+
textInsets: object.textInsets
|
|
117
|
+
? Object.fromEntries(
|
|
118
|
+
Object.entries(object.textInsets).map(([key, value]) => [key, scaledMetric(value, options)]),
|
|
119
|
+
)
|
|
120
|
+
: undefined,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function applyShapeOpacityToText(text, opacity) {
|
|
125
|
+
if (!text?.paragraphs || !Number.isFinite(opacity)) return text;
|
|
126
|
+
return {
|
|
127
|
+
...text,
|
|
128
|
+
paragraphs: text.paragraphs.map((paragraph) => ({
|
|
129
|
+
...paragraph,
|
|
130
|
+
runs: paragraph.runs.map((run) => ({
|
|
131
|
+
...run,
|
|
132
|
+
opacity: (Number.isFinite(run.opacity) ? run.opacity : 1) * opacity,
|
|
133
|
+
})),
|
|
134
|
+
})),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sourcePathFor(object, fallback) {
|
|
139
|
+
return nonEmptyStringOr(object.architecture?.sourcePath, nonEmptyStringOr(object.sourcePath, fallback));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sourceIdFor(object) {
|
|
143
|
+
return nonEmptyStringOr(object.architecture?.id, nonEmptyStringOr(object.id, ""));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function shapePreset(object) {
|
|
147
|
+
return nonEmptyStringOr(object.shape, "rect");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function arrowEndFor(value) {
|
|
151
|
+
if (value === true) return "triangle";
|
|
152
|
+
if (value === false || value === undefined || value === null) return "none";
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function imageResult(entry, kind, options) {
|
|
157
|
+
const resolved = options.resolveImage(entry, kind);
|
|
158
|
+
if (typeof resolved === "string") return { src: resolved };
|
|
159
|
+
if (resolved && typeof resolved === "object") return resolved;
|
|
160
|
+
return { src: "" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function imageNode(entry, kind, index, options) {
|
|
164
|
+
const image = imageResult(entry, kind, options);
|
|
165
|
+
const bounds = image.bounds || transformBounds(entry, options);
|
|
166
|
+
const architecture = definedEntries({
|
|
167
|
+
...(entry.architecture || {}),
|
|
168
|
+
kind,
|
|
169
|
+
id: entry.architecture?.id || entry.id || "",
|
|
170
|
+
sourcePath: entry.architecture?.sourcePath || entry.sourcePath,
|
|
171
|
+
order: entry.architecture?.order ?? entry.order,
|
|
172
|
+
z: entry.architecture?.z ?? entry.z,
|
|
173
|
+
});
|
|
174
|
+
if (!image.src) {
|
|
175
|
+
return {
|
|
176
|
+
kind: "fallback",
|
|
177
|
+
...(sourceIdFor({ ...entry, architecture }) ? { id: sourceIdFor({ ...entry, architecture }) } : {}),
|
|
178
|
+
sourcePath: sourcePathFor({ ...entry, architecture }, `${kind}s[${index}]`),
|
|
179
|
+
z: finiteNumberOr(image.z, index),
|
|
180
|
+
bounds,
|
|
181
|
+
reason: `${kind}-image-unavailable`,
|
|
182
|
+
meta: { architecture },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
kind: "image",
|
|
187
|
+
...(sourceIdFor({ ...entry, architecture }) ? { id: sourceIdFor({ ...entry, architecture }) } : {}),
|
|
188
|
+
sourcePath: sourcePathFor({ ...entry, architecture }, `${kind}s[${index}]`),
|
|
189
|
+
z: finiteNumberOr(image.z, index),
|
|
190
|
+
bounds,
|
|
191
|
+
src: image.src,
|
|
192
|
+
alt: nonEmptyStringOr(image.alt, kind === "icon-picture" ? `${entry.icon} icon` : `${architecture.id} image`),
|
|
193
|
+
fit: image.fit || "fill",
|
|
194
|
+
opacity: image.opacity ?? 1,
|
|
195
|
+
meta: { architecture },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function objectNode(object, index, options, diagnostics) {
|
|
200
|
+
const path = `objects[${index}]`;
|
|
201
|
+
if (object.type === "connector") {
|
|
202
|
+
return {
|
|
203
|
+
kind: "connector",
|
|
204
|
+
...(sourceIdFor(object) ? { id: sourceIdFor(object) } : {}),
|
|
205
|
+
sourcePath: sourcePathFor(object, path),
|
|
206
|
+
z: index,
|
|
207
|
+
points: (object.points || []).map((point) => transformPoint(point, options)),
|
|
208
|
+
style: mapStyle(object, path, options, diagnostics),
|
|
209
|
+
arrowStart: "none",
|
|
210
|
+
arrowEnd: arrowEndFor(object.arrowEnd),
|
|
211
|
+
meta: definedEntries({
|
|
212
|
+
architecture: object.architecture,
|
|
213
|
+
from: object.from,
|
|
214
|
+
to: object.to,
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (object.type === "image") {
|
|
219
|
+
return imageNode(object, "image-picture", index, options);
|
|
220
|
+
}
|
|
221
|
+
if (object.type === "shape") {
|
|
222
|
+
const text = object.text === undefined
|
|
223
|
+
? undefined
|
|
224
|
+
: applyShapeOpacityToText(mapText(object.text, `${path}.text`, options, diagnostics), object.opacity);
|
|
225
|
+
return {
|
|
226
|
+
kind: "shape",
|
|
227
|
+
...(sourceIdFor(object) ? { id: sourceIdFor(object) } : {}),
|
|
228
|
+
sourcePath: sourcePathFor(object, path),
|
|
229
|
+
z: index,
|
|
230
|
+
bounds: transformBounds(object, options),
|
|
231
|
+
preset: shapePreset(object),
|
|
232
|
+
style: mapStyle(object, path, options, diagnostics),
|
|
233
|
+
...(text !== undefined ? { text } : {}),
|
|
234
|
+
textLayout: mapTextLayout(object, options),
|
|
235
|
+
meta: definedEntries({
|
|
236
|
+
architecture: object.architecture,
|
|
237
|
+
icon: object.icon,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
kind: "fallback",
|
|
243
|
+
sourcePath: sourcePathFor(object, path),
|
|
244
|
+
z: index,
|
|
245
|
+
bounds: transformBounds(object, options),
|
|
246
|
+
reason: `unsupported architecture object type: ${String(object.type)}`,
|
|
247
|
+
meta: definedEntries({ architecture: object.architecture }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function fallbackNode(fallback, index, objectCount, options) {
|
|
252
|
+
return {
|
|
253
|
+
kind: "fallback",
|
|
254
|
+
sourcePath: nonEmptyStringOr(fallback.sourcePath, nonEmptyStringOr(fallback.path, `fallbacks[${index}]`)),
|
|
255
|
+
z: objectCount + index,
|
|
256
|
+
bounds: transformBounds(fallback, options),
|
|
257
|
+
reason: nonEmptyStringOr(fallback.reason, "architecture-rendered-as-artwork"),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeOptions(options = {}) {
|
|
262
|
+
return {
|
|
263
|
+
path: nonEmptyStringOr(options.path, DEFAULT_SOURCE_PATH),
|
|
264
|
+
resolveColor: typeof options.resolveColor === "function" ? options.resolveColor : (value) => value,
|
|
265
|
+
resolveImage: typeof options.resolveImage === "function" ? options.resolveImage : (entry) => entry?.src || "",
|
|
266
|
+
resolveDash: typeof options.resolveDash === "function" ? options.resolveDash : (value) => value,
|
|
267
|
+
scale: finiteNumberOr(options.scale, 1),
|
|
268
|
+
originX: finiteNumberOr(options.originX, 0),
|
|
269
|
+
originY: finiteNumberOr(options.originY, 0),
|
|
270
|
+
fontFace: typeof options.fontFace === "string" && options.fontFace ? options.fontFace : undefined,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function architectureSnapshotToScene(snapshot, options = {}) {
|
|
275
|
+
const normalizedOptions = normalizeOptions(options);
|
|
276
|
+
const diagnostics = [];
|
|
277
|
+
const objects = Array.isArray(snapshot?.objects) ? snapshot.objects : [];
|
|
278
|
+
const icons = Array.isArray(snapshot?.icons) ? snapshot.icons : [];
|
|
279
|
+
const fallbacks = Array.isArray(snapshot?.fallbacks) ? snapshot.fallbacks : [];
|
|
280
|
+
const nodes = objects.flatMap((object, index) => {
|
|
281
|
+
const mapped = [objectNode(object, index, normalizedOptions, diagnostics)];
|
|
282
|
+
if (object.type === "shape" && object.icon) {
|
|
283
|
+
const icon = icons.find((candidate) => candidate.id === object.architecture?.id);
|
|
284
|
+
if (icon) mapped.push(imageNode(icon, "icon-picture", index + 1 / 2, normalizedOptions));
|
|
285
|
+
}
|
|
286
|
+
return mapped;
|
|
287
|
+
});
|
|
288
|
+
const iconObjectIds = new Set(
|
|
289
|
+
objects
|
|
290
|
+
.filter((object) => object.type === "shape" && object.icon)
|
|
291
|
+
.map((object) => object.architecture?.id),
|
|
292
|
+
);
|
|
293
|
+
icons
|
|
294
|
+
.filter((icon) => !iconObjectIds.has(icon.id))
|
|
295
|
+
.forEach((icon, index) => nodes.push(imageNode(icon, "icon-picture", objects.length + index, normalizedOptions)));
|
|
296
|
+
fallbacks.forEach((fallback, index) => nodes.push(fallbackNode(fallback, index, nodes.length, normalizedOptions)));
|
|
297
|
+
|
|
298
|
+
const accessibility = definedEntries({
|
|
299
|
+
title: snapshot?.title,
|
|
300
|
+
description: snapshot?.description,
|
|
301
|
+
});
|
|
302
|
+
const result = normalizeScene(createScene({
|
|
303
|
+
width: finiteNumberOr(snapshot?.canvas?.width, 0) * normalizedOptions.scale,
|
|
304
|
+
height: finiteNumberOr(snapshot?.canvas?.height, 0) * normalizedOptions.scale,
|
|
305
|
+
source: { kind: "architecture", path: normalizedOptions.path },
|
|
306
|
+
...(Object.keys(accessibility).length ? { accessibility } : {}),
|
|
307
|
+
nodes,
|
|
308
|
+
}));
|
|
309
|
+
result.diagnostics.unshift(...diagnostics);
|
|
310
|
+
validateScene(result.scene);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { architectureContract } from "./architecture-contract.mjs";
|
|
2
|
+
import { architectureSnapshotToScene } from "./architecture-scene.mjs";
|
|
3
|
+
import { sceneToSvg, svgPrimitive } from "./scene-svg.mjs";
|
|
2
4
|
import {
|
|
3
5
|
ArchitectureError,
|
|
4
6
|
architectureCompatibilityWarnings,
|
|
@@ -11,7 +13,6 @@ import {
|
|
|
11
13
|
unknownArchitectureField,
|
|
12
14
|
} from "./architecture-diagnostics.mjs";
|
|
13
15
|
|
|
14
|
-
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
15
16
|
const DEFAULT_CANVAS = Object.freeze({ width: 1600, height: 900 });
|
|
16
17
|
const DSL_VERSION = architectureContract.root.properties.version.const;
|
|
17
18
|
const EMPTY_ARCHITECTURE_SOURCE = '{\n "version": 1,\n "elements": []\n}\n';
|
|
@@ -1307,11 +1308,8 @@ export function parseArchitecture(source) {
|
|
|
1307
1308
|
}
|
|
1308
1309
|
|
|
1309
1310
|
function svgElement(documentRef, tag, attributes = {}) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
if (value !== undefined && value !== "") element.setAttribute(name, String(value));
|
|
1313
|
-
}
|
|
1314
|
-
return element;
|
|
1311
|
+
// Produce portable visual detail; scene-svg is the only SVG DOM writer.
|
|
1312
|
+
return svgPrimitive(tag, attributes);
|
|
1315
1313
|
}
|
|
1316
1314
|
|
|
1317
1315
|
function appendSvgTitle(documentRef, parent, text) {
|
|
@@ -3895,13 +3893,13 @@ export function architectureSemanticSnapshot(model) {
|
|
|
3895
3893
|
* export. Coordinates remain in Architecture canvas units here; renderer.js
|
|
3896
3894
|
* maps them through the rendered SVG viewBox into slide pixels.
|
|
3897
3895
|
*/
|
|
3898
|
-
export function architecturePowerPointSnapshot(model, documentRef = globalThis.document) {
|
|
3896
|
+
export function architecturePowerPointSnapshot(model, documentRef = globalThis.document, routingPlan) {
|
|
3899
3897
|
const lookup = new Map(
|
|
3900
3898
|
model.elements
|
|
3901
3899
|
.filter((element) => element.type !== "connector")
|
|
3902
3900
|
.map((element) => [element.id, element]),
|
|
3903
3901
|
);
|
|
3904
|
-
const { routes, diagnostics } = planConnectorRoutes(model, lookup);
|
|
3902
|
+
const { routes, diagnostics } = routingPlan || planConnectorRoutes(model, lookup);
|
|
3905
3903
|
const objects = [];
|
|
3906
3904
|
const fallbacks = [];
|
|
3907
3905
|
const icons = [];
|
|
@@ -4152,8 +4150,8 @@ export function renderArchitectureDiagram(
|
|
|
4152
4150
|
.filter((element) => element.type !== "connector")
|
|
4153
4151
|
.map((element) => [element.id, element]),
|
|
4154
4152
|
);
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4153
|
+
const routingPlan = planConnectorRoutes(model, lookup);
|
|
4154
|
+
const { routes: connectorRoutes, diagnostics: routingDiagnostics } = routingPlan;
|
|
4157
4155
|
model.elements.forEach((element, index) => {
|
|
4158
4156
|
if (element.type === "image") {
|
|
4159
4157
|
const clipPath = svgElement(documentRef, "clipPath", {
|
|
@@ -4195,16 +4193,21 @@ export function renderArchitectureDiagram(
|
|
|
4195
4193
|
// Table for announcing connector endpoints by visible label; traverse elements only once.
|
|
4196
4194
|
const endpointNames = endpointDisplayNames(model.elements);
|
|
4197
4195
|
const frontLabels = [];
|
|
4196
|
+
const visuals = new Map();
|
|
4198
4197
|
|
|
4199
4198
|
model.elements.forEach((element, index) => {
|
|
4200
4199
|
if (element.type === "group") {
|
|
4201
|
-
|
|
4200
|
+
const primitive = renderGroup(documentRef, element);
|
|
4201
|
+
visuals.set(`group:${element.order}`, primitive);
|
|
4202
|
+
svg.appendChild(primitive);
|
|
4202
4203
|
} else if (element.type === "node") {
|
|
4203
|
-
|
|
4204
|
+
const primitive = renderNode(documentRef, element);
|
|
4205
|
+
visuals.set(`node:${element.order}`, primitive);
|
|
4206
|
+
svg.appendChild(primitive);
|
|
4204
4207
|
} else if (element.type === "image") {
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
);
|
|
4208
|
+
const primitive = renderImage(documentRef, element, `architecture-image-clip-${renderId}-${index}`);
|
|
4209
|
+
visuals.set(`image-picture:${element.order}`, primitive);
|
|
4210
|
+
svg.appendChild(primitive);
|
|
4208
4211
|
} else {
|
|
4209
4212
|
const rendered = renderConnector(
|
|
4210
4213
|
documentRef,
|
|
@@ -4214,13 +4217,33 @@ export function renderArchitectureDiagram(
|
|
|
4214
4217
|
endpointNames,
|
|
4215
4218
|
model.canvas,
|
|
4216
4219
|
);
|
|
4220
|
+
visuals.set(`connector:${element.order}`, rendered.group);
|
|
4217
4221
|
svg.appendChild(rendered.group);
|
|
4218
|
-
if (rendered.frontLabel)
|
|
4222
|
+
if (rendered.frontLabel) {
|
|
4223
|
+
visuals.set(`connector-label:${element.order}`, rendered.frontLabel);
|
|
4224
|
+
frontLabels.push(rendered.frontLabel);
|
|
4225
|
+
}
|
|
4219
4226
|
}
|
|
4220
4227
|
});
|
|
4221
4228
|
frontLabels.forEach((label) => svg.appendChild(label));
|
|
4222
|
-
|
|
4223
|
-
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 }));
|
|
4224
4247
|
Object.defineProperty(wrapper, "__presentationPptxSnapshot", {
|
|
4225
4248
|
value: powerPointSnapshot,
|
|
4226
4249
|
enumerable: true,
|
|
@@ -72,9 +72,17 @@
|
|
|
72
72
|
</section>
|
|
73
73
|
</div>
|
|
74
74
|
<span id="presentStatus" class="sr-only" aria-live="polite"></span>
|
|
75
|
-
<span id="exportStatus" class="sr-only" aria-live="polite"></span>
|
|
76
75
|
<span id="sourceStatus" class="sr-only" aria-live="polite"></span>
|
|
77
76
|
</nav>
|
|
77
|
+
<span id="exportStatus" class="export-feedback sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
|
|
78
|
+
<span id="exportErrorStatus" class="export-feedback sr-only" role="alert" aria-atomic="true"></span>
|
|
79
|
+
<section id="exportNotification" class="export-feedback export-notification" aria-label="Export notification" hidden>
|
|
80
|
+
<div class="export-notification-content">
|
|
81
|
+
<p id="exportNotificationMessage" class="export-notification-message"></p>
|
|
82
|
+
<p id="exportNotificationPath" class="export-notification-path" tabindex="0" hidden></p>
|
|
83
|
+
</div>
|
|
84
|
+
<button id="exportNotificationClose" class="nav-btn" type="button" aria-label="Dismiss export notification" hidden>✕</button>
|
|
85
|
+
</section>
|
|
78
86
|
<div id="layoutWarning" class="layout-warning" role="status" aria-live="polite" hidden></div>
|
|
79
87
|
|
|
80
88
|
<section id="presenterView" class="presenter-view" hidden aria-label="Presenter view">
|