@markdstage/markdstage 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +90 -0
  2. package/bin/markdstage.mjs +12 -0
  3. package/package.json +45 -0
  4. package/shared/README.md +1014 -0
  5. package/shared/THIRD-PARTY-NOTICES.md +19 -0
  6. package/shared/deck-state.mjs +105 -0
  7. package/shared/docs/custom-theme-authoring.md +208 -0
  8. package/shared/markdown-deck.mjs +220 -0
  9. package/shared/markdstage-guide.mjs +276 -0
  10. package/shared/presenter-window.mjs +17 -0
  11. package/shared/renderer/architecture-document.mjs +596 -0
  12. package/shared/renderer/architecture-edit.mjs +298 -0
  13. package/shared/renderer/architecture-editor.mjs +449 -0
  14. package/shared/renderer/architecture.mjs +4033 -0
  15. package/shared/renderer/import-path.mjs +11 -0
  16. package/shared/renderer/index.html +106 -0
  17. package/shared/renderer/renderer.js +2082 -0
  18. package/shared/renderer/slides.css +614 -0
  19. package/shared/renderer/speaker-notes.mjs +106 -0
  20. package/shared/renderer/theme.mjs +205 -0
  21. package/shared/runtime/browser.mjs +539 -0
  22. package/shared/runtime/custom-theme.mjs +135 -0
  23. package/shared/runtime/deck-session.mjs +188 -0
  24. package/shared/runtime/errors.mjs +17 -0
  25. package/shared/runtime/output-paths.mjs +159 -0
  26. package/shared/runtime/output.mjs +385 -0
  27. package/shared/runtime/presentation-server.mjs +505 -0
  28. package/shared/runtime/static-files.mjs +70 -0
  29. package/shared/schema/README.md +228 -0
  30. package/shared/schema/architecture-v1.schema.json +664 -0
  31. package/shared/schema/examples/web-app.architecture.json +119 -0
  32. package/shared/schema/theme-metadata-v1.schema.json +75 -0
  33. package/shared/schema/theme-v1.json +84 -0
  34. package/shared/scripts/architecture-assets.mjs +226 -0
  35. package/shared/scripts/asset-paths.mjs +92 -0
  36. package/shared/scripts/atomic-markdown-replace.mjs +46 -0
  37. package/shared/scripts/markdown-blocks.mjs +182 -0
  38. package/shared/scripts/markdown-files.mjs +63 -0
  39. package/shared/scripts/markdown-save-coordinator.mjs +18 -0
  40. package/shared/scripts/markdown-watcher.mjs +80 -0
  41. package/shared/scripts/theme-paths.mjs +108 -0
  42. package/shared/scripts/vendor-assets.mjs +132 -0
  43. package/shared/scripts/workspace-root.mjs +32 -0
  44. package/shared/vendor/highlight.LICENSE +29 -0
  45. package/shared/vendor/highlight.min.js +1244 -0
  46. package/shared/vendor/marked.min.js +6 -0
  47. package/shared/vendor/mermaid.min.js.part-0001 +268 -0
  48. package/shared/vendor/mermaid.min.js.part-0002 +304 -0
  49. package/shared/vendor/mermaid.min.js.part-0003 +324 -0
  50. package/shared/vendor/mermaid.min.js.part-0004 +374 -0
  51. package/shared/vendor/mermaid.min.js.part-0005 +564 -0
  52. package/shared/vendor/mermaid.min.js.part-0006 +1308 -0
  53. package/shared/vendor/mermaid.min.js.part-0007 +269 -0
  54. package/shared/vendor/purify.min.js +3 -0
  55. package/shared/vendor/vendor-assets.lock.json +60 -0
  56. package/src/cli.mjs +347 -0
  57. package/src/commands/capture.mjs +23 -0
  58. package/src/commands/export.mjs +18 -0
  59. package/src/commands/guide.mjs +23 -0
  60. package/src/commands/inspect.mjs +35 -0
  61. package/src/commands/present.mjs +91 -0
  62. package/src/commands/skill.mjs +114 -0
  63. package/src/commands/validate.mjs +79 -0
  64. package/src/deck.mjs +63 -0
  65. package/src/exit.mjs +58 -0
  66. package/src/runtime.mjs +77 -0
  67. package/src/skills.mjs +155 -0
@@ -0,0 +1,596 @@
1
+ import {
2
+ ID_PATTERN,
3
+ normalizeArchitectureSource,
4
+ parseArchitecture,
5
+ } from "./architecture.mjs";
6
+ import {
7
+ createArchitectureEditSession,
8
+ describePlacement,
9
+ resolveRawElement,
10
+ serializeArchitecture,
11
+ } from "./architecture-edit.mjs";
12
+
13
+ const HISTORY_LIMIT = 200;
14
+ const COORDINATE_MIN = -4000;
15
+ const COORDINATE_MAX = 4000;
16
+ const EXTENT_MIN = 1;
17
+ const EXTENT_MAX = 4000;
18
+
19
+ function clone(value) {
20
+ return JSON.parse(JSON.stringify(value));
21
+ }
22
+
23
+ function clamp(value, min, max) {
24
+ return Math.min(max, Math.max(min, value));
25
+ }
26
+
27
+ function round(value) {
28
+ const result = Number(Number(value).toFixed(4));
29
+ return Object.is(result, -0) ? 0 : result;
30
+ }
31
+
32
+ function snapshot(source) {
33
+ const normalized = normalizeArchitectureSource(source);
34
+ return {
35
+ source: normalized,
36
+ raw: JSON.parse(normalized),
37
+ model: parseArchitecture(normalized),
38
+ };
39
+ }
40
+
41
+ function rawEntries(raw) {
42
+ const entries = [];
43
+ const walk = (items, prefix, parent) => {
44
+ if (!Array.isArray(items)) return;
45
+ items.forEach((element, index) => {
46
+ if (!element || typeof element !== "object" || Array.isArray(element)) return;
47
+ const sourcePath = `${prefix}[${index}]`;
48
+ entries.push({
49
+ element,
50
+ parent,
51
+ items,
52
+ index,
53
+ sourcePath,
54
+ ref: element.id || sourcePath,
55
+ });
56
+ if (element.type === "group") {
57
+ walk(element.children, `${sourcePath}.children`, element);
58
+ }
59
+ });
60
+ };
61
+ walk(raw.elements, "elements", null);
62
+ return entries;
63
+ }
64
+
65
+ function rawEntry(raw, ref) {
66
+ return (
67
+ rawEntries(raw).find(
68
+ (entry) => entry.sourcePath === ref || (entry.element.id && entry.element.id === ref),
69
+ ) ?? null
70
+ );
71
+ }
72
+
73
+ function modelElement(model, ref) {
74
+ return (
75
+ model.elements.find(
76
+ (element) => element.sourcePath === ref || (element.id && element.id === ref),
77
+ ) ?? null
78
+ );
79
+ }
80
+
81
+ function collectIds(element, ids) {
82
+ if (!element || typeof element !== "object") return;
83
+ if (typeof element.id === "string") ids.add(element.id);
84
+ if (Array.isArray(element.children)) {
85
+ for (const child of element.children) collectIds(child, ids);
86
+ }
87
+ }
88
+
89
+ function removeReferencingConnectors(items, ids) {
90
+ if (!Array.isArray(items)) return;
91
+ for (let index = items.length - 1; index >= 0; index -= 1) {
92
+ const element = items[index];
93
+ if (element?.type === "connector" && (ids.has(element.from) || ids.has(element.to))) {
94
+ items.splice(index, 1);
95
+ continue;
96
+ }
97
+ if (element?.type === "group") removeReferencingConnectors(element.children, ids);
98
+ }
99
+ }
100
+
101
+ function nextId(raw, base) {
102
+ const normalized = String(base || "element")
103
+ .replace(/^[^A-Za-z]+/, "")
104
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
105
+ .slice(0, 54) || "element";
106
+ const ids = new Set(rawEntries(raw).map((entry) => entry.element.id).filter(Boolean));
107
+ if (!ids.has(normalized) && ID_PATTERN.test(normalized)) return normalized;
108
+ for (let index = 2; index < 10_000; index += 1) {
109
+ const candidate = `${normalized}-${index}`.slice(0, 64);
110
+ if (!ids.has(candidate) && ID_PATTERN.test(candidate)) return candidate;
111
+ }
112
+ throw new Error("Could not generate a unique element id.");
113
+ }
114
+
115
+ function setNested(target, path, value) {
116
+ const parts = String(path).split(".").filter(Boolean);
117
+ if (!parts.length) return;
118
+ let owner = target;
119
+ for (let index = 0; index < parts.length - 1; index += 1) {
120
+ const key = parts[index];
121
+ if (!owner[key] || typeof owner[key] !== "object" || Array.isArray(owner[key])) {
122
+ owner[key] = {};
123
+ }
124
+ owner = owner[key];
125
+ }
126
+ const key = parts.at(-1);
127
+ if (value === undefined || value === null || value === "") delete owner[key];
128
+ else owner[key] = value;
129
+
130
+ for (let index = parts.length - 1; index > 0; index -= 1) {
131
+ let candidate = target;
132
+ for (let offset = 0; offset < index; offset += 1) candidate = candidate[parts[offset]];
133
+ if (
134
+ candidate &&
135
+ typeof candidate === "object" &&
136
+ !Array.isArray(candidate) &&
137
+ Object.keys(candidate).length === 0
138
+ ) {
139
+ let parent = target;
140
+ for (let offset = 0; offset < index - 1; offset += 1) parent = parent[parts[offset]];
141
+ delete parent[parts[index - 1]];
142
+ }
143
+ }
144
+ }
145
+
146
+ function remapCloneIds(raw, element) {
147
+ const copy = clone(element);
148
+ const mapping = new Map();
149
+ const reserved = new Set(rawEntries(raw).map((entry) => entry.element.id).filter(Boolean));
150
+ const reserveId = (base) => {
151
+ const normalized = String(base || "element")
152
+ .replace(/^[^A-Za-z]+/, "")
153
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
154
+ .slice(0, 54) || "element";
155
+ for (let index = 1; index < 10_000; index += 1) {
156
+ const candidate = `${normalized}${index === 1 ? "" : `-${index}`}`.slice(0, 64);
157
+ if (!reserved.has(candidate) && ID_PATTERN.test(candidate)) {
158
+ reserved.add(candidate);
159
+ return candidate;
160
+ }
161
+ }
162
+ throw new Error("Could not generate a unique element id.");
163
+ };
164
+ const reserve = (item) => {
165
+ if (!item || typeof item !== "object") return;
166
+ if (typeof item.id === "string") {
167
+ const replacement = reserveId(`${item.id}-copy`);
168
+ mapping.set(item.id, replacement);
169
+ item.id = replacement;
170
+ }
171
+ if (Array.isArray(item.children)) item.children.forEach(reserve);
172
+ };
173
+ reserve(copy);
174
+ const updateConnectors = (item) => {
175
+ if (!item || typeof item !== "object") return;
176
+ if (item.type === "connector") {
177
+ if (mapping.has(item.from)) item.from = mapping.get(item.from);
178
+ if (mapping.has(item.to)) item.to = mapping.get(item.to);
179
+ }
180
+ if (Array.isArray(item.children)) item.children.forEach(updateConnectors);
181
+ };
182
+ updateConnectors(copy);
183
+ return copy;
184
+ }
185
+
186
+ export function createArchitectureDocument(source, options = {}) {
187
+ const historyLimit = Math.max(1, Math.trunc(options.historyLimit ?? HISTORY_LIMIT));
188
+ const history = [snapshot(source)];
189
+ let cursor = 0;
190
+
191
+ function current() {
192
+ return history[cursor];
193
+ }
194
+
195
+ function result(reason, details = {}) {
196
+ return {
197
+ ok: true,
198
+ reason,
199
+ ...details,
200
+ source: current().source,
201
+ model: current().model,
202
+ };
203
+ }
204
+
205
+ function reject(reason, details = {}) {
206
+ return { ok: false, reason, ...details };
207
+ }
208
+
209
+ function commit(raw, reason, details = {}) {
210
+ const sourceText = serializeArchitecture(raw);
211
+ let next;
212
+ try {
213
+ next = snapshot(sourceText);
214
+ } catch (error) {
215
+ return reject("rejected", { message: error?.message || "Invalid Architecture DSL." });
216
+ }
217
+ history.splice(cursor + 1);
218
+ history.push(next);
219
+ while (history.length > historyLimit) history.shift();
220
+ cursor = history.length - 1;
221
+ return result(reason, details);
222
+ }
223
+
224
+ function mutate(reason, mutator) {
225
+ const raw = clone(current().raw);
226
+ let details;
227
+ try {
228
+ details = mutator(raw) ?? {};
229
+ } catch (error) {
230
+ return reject("rejected", { message: error?.message || "The edit could not be applied." });
231
+ }
232
+ if (details?.ok === false) return details;
233
+ return commit(raw, reason, details);
234
+ }
235
+
236
+ function describe(ref) {
237
+ const element = modelElement(current().model, ref);
238
+ if (!element) return { found: false, movable: false, reason: "unknown", ref };
239
+ if (element.type === "connector") {
240
+ return { found: true, movable: false, reason: "connector", ref: element.sourcePath };
241
+ }
242
+ return describePlacement(current().model, element.id);
243
+ }
244
+
245
+ function setRoot(path, value) {
246
+ const allowed = new Set(["title", "description", "canvas.width", "canvas.height"]);
247
+ if (!allowed.has(path)) return reject("unsupported-property", { path });
248
+ return mutate("root-updated", (raw) => {
249
+ setNested(raw, path, value);
250
+ return { path, value };
251
+ });
252
+ }
253
+
254
+ function setElement(ref, path, value) {
255
+ if (path === "type" || path === "children" || path.startsWith("children.")) {
256
+ return reject("unsupported-property", { path });
257
+ }
258
+ if (path === "id") return renameElement(ref, value);
259
+ if (["x", "y", "width", "height"].includes(path)) {
260
+ const element = modelElement(current().model, ref);
261
+ if (element && element.type !== "connector") {
262
+ const placement = describePlacement(current().model, element.id);
263
+ if (!placement.movable) return { ...placement, ok: false };
264
+ }
265
+ }
266
+ return mutate("element-updated", (raw) => {
267
+ const entry = rawEntry(raw, ref);
268
+ if (!entry) return reject("unknown", { ref });
269
+ setNested(entry.element, path, value);
270
+ if (entry.element.type === "connector" && path === "routing" && value !== "polyline") {
271
+ delete entry.element.points;
272
+ }
273
+ return { ref: entry.element.id || entry.sourcePath, path, value };
274
+ });
275
+ }
276
+
277
+ function renameElement(ref, id) {
278
+ const next = String(id || "").trim();
279
+ if (!ID_PATTERN.test(next)) return reject("invalid-id", { id: next });
280
+ return mutate("element-renamed", (raw) => {
281
+ const entry = rawEntry(raw, ref);
282
+ if (!entry || entry.element.type === "connector") return reject("unknown", { ref });
283
+ if (
284
+ rawEntries(raw).some(
285
+ (candidate) => candidate !== entry && candidate.element.id === next,
286
+ )
287
+ ) {
288
+ return reject("duplicate-id", { id: next });
289
+ }
290
+ const previous = entry.element.id;
291
+ entry.element.id = next;
292
+ for (const candidate of rawEntries(raw)) {
293
+ if (candidate.element.type !== "connector") continue;
294
+ if (candidate.element.from === previous) candidate.element.from = next;
295
+ if (candidate.element.to === previous) candidate.element.to = next;
296
+ }
297
+ return { ref: next, previous, id: next };
298
+ });
299
+ }
300
+
301
+ function move(ref, dx, dy) {
302
+ const element = modelElement(current().model, ref);
303
+ if (!element || element.type === "connector") return reject("unknown", { ref });
304
+ const legacy = createArchitectureEditSession(current().source);
305
+ const moved = legacy.move(element.id, Number(dx), Number(dy));
306
+ if (!moved.ok) return moved;
307
+ return commit(JSON.parse(legacy.source), "moved", {
308
+ ref: element.id,
309
+ x: moved.x,
310
+ y: moved.y,
311
+ });
312
+ }
313
+
314
+ function resize(ref, box) {
315
+ const element = modelElement(current().model, ref);
316
+ if (!element || element.type === "connector") return reject("unknown", { ref });
317
+ const placement = describePlacement(current().model, element.id);
318
+ if (!placement.movable) return { ...placement, ok: false };
319
+ return mutate("resized", (raw) => {
320
+ const located = resolveRawElement(raw, element.sourcePath);
321
+ if (!located) return reject("unknown", { ref });
322
+ const x = clamp(round(Number(box.x) - placement.origin.x), COORDINATE_MIN, COORDINATE_MAX);
323
+ const y = clamp(round(Number(box.y) - placement.origin.y), COORDINATE_MIN, COORDINATE_MAX);
324
+ const width = clamp(round(Number(box.width)), EXTENT_MIN, EXTENT_MAX);
325
+ const height = clamp(round(Number(box.height)), EXTENT_MIN, EXTENT_MAX);
326
+ located.element.x = x;
327
+ located.element.y = y;
328
+ located.element.width = width;
329
+ located.element.height = height;
330
+ return { ref: element.id, x, y, width, height };
331
+ });
332
+ }
333
+
334
+ function targetItems(raw, parentId) {
335
+ if (!parentId) return raw.elements;
336
+ const parent = rawEntry(raw, parentId);
337
+ if (!parent || parent.element.type !== "group") return null;
338
+ if (!Array.isArray(parent.element.children)) parent.element.children = [];
339
+ return parent.element.children;
340
+ }
341
+
342
+ function defaultBox(items) {
343
+ const count = items.filter(
344
+ (item) => item?.type === "node" || item?.type === "group" || item?.type === "image",
345
+ ).length;
346
+ return { x: 80 + count * 30, y: 80 + count * 30, width: 260, height: 140 };
347
+ }
348
+
349
+ function requestedBox(items, options, width, height) {
350
+ const box = { ...defaultBox(items), width, height };
351
+ if (Number.isFinite(Number(options.x))) {
352
+ box.x = clamp(round(Number(options.x)), COORDINATE_MIN, COORDINATE_MAX);
353
+ }
354
+ if (Number.isFinite(Number(options.y))) {
355
+ box.y = clamp(round(Number(options.y)), COORDINATE_MIN, COORDINATE_MAX);
356
+ }
357
+ return box;
358
+ }
359
+
360
+ function addNode(options = {}) {
361
+ return mutate("node-added", (raw) => {
362
+ const items = targetItems(raw, options.parentId);
363
+ if (!items) return reject("invalid-parent", { parentId: options.parentId });
364
+ const parent = options.parentId ? rawEntry(raw, options.parentId)?.element : null;
365
+ const id = nextId(raw, options.id || "node");
366
+ const node = {
367
+ type: "node",
368
+ id,
369
+ shape: options.shape || "rounded-rect",
370
+ text: options.text || "Node",
371
+ };
372
+ if (!parent?.layout) Object.assign(node, requestedBox(items, options, 260, 140));
373
+ items.push(node);
374
+ return { ref: id, id };
375
+ });
376
+ }
377
+
378
+ function addGroup(options = {}) {
379
+ return mutate("group-added", (raw) => {
380
+ const items = targetItems(raw, options.parentId);
381
+ if (!items) return reject("invalid-parent", { parentId: options.parentId });
382
+ const parent = options.parentId ? rawEntry(raw, options.parentId)?.element : null;
383
+ const id = nextId(raw, options.id || "group");
384
+ const group = {
385
+ type: "group",
386
+ id,
387
+ title: options.title || "Group",
388
+ children: [],
389
+ };
390
+ if (!parent?.layout) Object.assign(group, requestedBox(items, options, 520, 320));
391
+ items.push(group);
392
+ return { ref: id, id };
393
+ });
394
+ }
395
+
396
+ function addImage(options = {}) {
397
+ return mutate("image-added", (raw) => {
398
+ const items = targetItems(raw, options.parentId);
399
+ if (!items) return reject("invalid-parent", { parentId: options.parentId });
400
+ const parent = options.parentId ? rawEntry(raw, options.parentId)?.element : null;
401
+ const src = String(options.src || "").trim();
402
+ const filename = src.split("/").at(-1) || "Image";
403
+ const id = nextId(raw, options.id || filename.replace(/\.[^.]+$/, "") || "image");
404
+ const image = {
405
+ type: "image",
406
+ id,
407
+ src,
408
+ fit: options.fit || "contain",
409
+ ariaLabel: options.ariaLabel || filename,
410
+ };
411
+ if (!parent?.layout) Object.assign(image, requestedBox(items, options, 340, 220));
412
+ items.push(image);
413
+ return { ref: id, id };
414
+ });
415
+ }
416
+
417
+ function addConnector(options = {}) {
418
+ return mutate("connector-added", (raw) => {
419
+ const endpointIds = new Set(
420
+ rawEntries(raw)
421
+ .filter((entry) => entry.element.type !== "connector")
422
+ .map((entry) => entry.element.id),
423
+ );
424
+ if (!endpointIds.has(options.from) || !endpointIds.has(options.to)) {
425
+ return reject("invalid-endpoint", { from: options.from, to: options.to });
426
+ }
427
+ const items = targetItems(raw, options.parentId);
428
+ if (!items) return reject("invalid-parent", { parentId: options.parentId });
429
+ const connector = {
430
+ type: "connector",
431
+ from: options.from,
432
+ to: options.to,
433
+ routing: options.routing || "orthogonal",
434
+ arrow: options.arrow !== false,
435
+ };
436
+ if (options.label) connector.label = options.label;
437
+ if (options.labelLayer) connector.labelLayer = options.labelLayer;
438
+ items.push(connector);
439
+ return { from: connector.from, to: connector.to };
440
+ });
441
+ }
442
+
443
+ function remove(ref) {
444
+ return mutate("element-deleted", (raw) => {
445
+ const entry = rawEntry(raw, ref);
446
+ if (!entry) return reject("unknown", { ref });
447
+ const ids = new Set();
448
+ collectIds(entry.element, ids);
449
+ entry.items.splice(entry.index, 1);
450
+ if (ids.size) removeReferencingConnectors(raw.elements, ids);
451
+ return { ref, removedIds: [...ids] };
452
+ });
453
+ }
454
+
455
+ function duplicate(ref) {
456
+ return mutate("element-duplicated", (raw) => {
457
+ const entry = rawEntry(raw, ref);
458
+ if (!entry) return reject("unknown", { ref });
459
+ const copy =
460
+ entry.element.type === "connector"
461
+ ? clone(entry.element)
462
+ : remapCloneIds(raw, entry.element);
463
+ if (typeof copy.x === "number") copy.x = clamp(copy.x + 24, COORDINATE_MIN, COORDINATE_MAX);
464
+ if (typeof copy.y === "number") copy.y = clamp(copy.y + 24, COORDINATE_MIN, COORDINATE_MAX);
465
+ entry.items.splice(entry.index + 1, 0, copy);
466
+ const copyPath = entry.sourcePath.replace(/\[\d+\]$/, `[${entry.index + 1}]`);
467
+ return { ref: copy.id || copyPath, id: copy.id };
468
+ });
469
+ }
470
+
471
+ function reorder(ref, delta) {
472
+ return mutate("element-reordered", (raw) => {
473
+ const entry = rawEntry(raw, ref);
474
+ if (!entry) return reject("unknown", { ref });
475
+ const target = clamp(entry.index + Math.trunc(delta), 0, entry.items.length - 1);
476
+ if (target === entry.index) return reject("unchanged", { ref });
477
+ const [element] = entry.items.splice(entry.index, 1);
478
+ entry.items.splice(target, 0, element);
479
+ return { ref, index: target };
480
+ });
481
+ }
482
+
483
+ function reparent(ref, parentId) {
484
+ const element = modelElement(current().model, ref);
485
+ if (!element || element.type === "connector") return reject("unknown", { ref });
486
+ return mutate("element-reparented", (raw) => {
487
+ const entry = rawEntry(raw, ref);
488
+ const target = parentId ? rawEntry(raw, parentId) : null;
489
+ if (!entry) return reject("unknown", { ref });
490
+ if (parentId && (!target || target.element.type !== "group")) {
491
+ return reject("invalid-parent", { parentId });
492
+ }
493
+ const descendants = new Set();
494
+ collectIds(entry.element, descendants);
495
+ if (parentId && descendants.has(parentId)) return reject("cyclic-parent", { parentId });
496
+ const targetList = targetItems(raw, parentId);
497
+ const placement = describePlacement(current().model, element.id);
498
+ entry.items.splice(entry.index, 1);
499
+ if (!target?.element.layout) {
500
+ const parentModel = target ? modelElement(current().model, parentId) : null;
501
+ entry.element.x = round(element.x - (parentModel?.x || 0));
502
+ entry.element.y = round(element.y - (parentModel?.y || 0));
503
+ entry.element.width = round(element.width);
504
+ entry.element.height = round(element.height);
505
+ } else if (placement.movable) {
506
+ delete entry.element.x;
507
+ delete entry.element.y;
508
+ }
509
+ targetList.push(entry.element);
510
+ return { ref: entry.element.id, parentId: parentId || null };
511
+ });
512
+ }
513
+
514
+ function releaseLayout(ref) {
515
+ const element = modelElement(current().model, ref);
516
+ const groupId =
517
+ element?.type === "group" && element.layout
518
+ ? element.id
519
+ : element?.id
520
+ ? describePlacement(current().model, element.id).layoutOwner
521
+ : null;
522
+ if (!groupId) return reject("not-layout-managed", { ref });
523
+ const legacy = createArchitectureEditSession(current().source);
524
+ const released = legacy.releaseLayout(groupId);
525
+ if (!released.ok) return released;
526
+ return commit(JSON.parse(legacy.source), "layout-released", released);
527
+ }
528
+
529
+ function setGroupLayout(ref, layout) {
530
+ return mutate("group-layout-updated", (raw) => {
531
+ const entry = rawEntry(raw, ref);
532
+ if (!entry || entry.element.type !== "group") {
533
+ return reject("not-group", { ref });
534
+ }
535
+ const enabling = !entry.element.layout;
536
+ entry.element.layout = clone(layout);
537
+ for (const child of entry.element.children || []) {
538
+ if (child.type === "connector") continue;
539
+ delete child.x;
540
+ delete child.y;
541
+ if (enabling) {
542
+ delete child.width;
543
+ delete child.height;
544
+ }
545
+ }
546
+ return { ref: entry.element.id, layout: clone(layout) };
547
+ });
548
+ }
549
+
550
+ return {
551
+ get source() {
552
+ return current().source;
553
+ },
554
+ get model() {
555
+ return current().model;
556
+ },
557
+ get raw() {
558
+ return clone(current().raw);
559
+ },
560
+ get canUndo() {
561
+ return cursor > 0;
562
+ },
563
+ get canRedo() {
564
+ return cursor < history.length - 1;
565
+ },
566
+ get depth() {
567
+ return history.length;
568
+ },
569
+ describe,
570
+ setRoot,
571
+ setElement,
572
+ renameElement,
573
+ move,
574
+ resize,
575
+ addNode,
576
+ addGroup,
577
+ addImage,
578
+ addConnector,
579
+ remove,
580
+ duplicate,
581
+ reorder,
582
+ reparent,
583
+ releaseLayout,
584
+ setGroupLayout,
585
+ undo() {
586
+ if (cursor === 0) return reject("no-history");
587
+ cursor -= 1;
588
+ return result("undone");
589
+ },
590
+ redo() {
591
+ if (cursor >= history.length - 1) return reject("no-history");
592
+ cursor += 1;
593
+ return result("redone");
594
+ },
595
+ };
596
+ }