@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,298 @@
1
+ // Editing workflow for ```architecture blocks (DOM-independent core).
2
+ //
3
+ // Three key design points:
4
+ //
5
+ // 1. Write editing results back to the source DSL itself.
6
+ // The previous PoC copied {version, overrides:[{id,x,y}]} JSON to the clipboard,
7
+ // but parseArchitecture accepts only $schema / version / canvas / title /
8
+ // description / elements at the top level, and no implementation consumed
9
+ // overrides. It was a dead-end format with nowhere to paste it. This module
10
+ // instead returns the complete DSL with element x / y values updated directly.
11
+ // Saving then requires only replacing the block in the source Markdown.
12
+ //
13
+ // 2. Model coordinates are absolute; DSL coordinates are relative to the parent group.
14
+ // normalizeBox adds the parent's origin, so writing back must subtract the
15
+ // parent group's absolute coordinates. Omitting this breaks nested diagrams.
16
+ //
17
+ // 3. A child of a group with a layout *silently ignores* explicit x / y values.
18
+ // layoutPlacements filters children only by type, with no fixed / flow distinction,
19
+ // and normalizeBox prioritizes placement (placement?.x ?? element.x). About 68%
20
+ // of nodes in repository data meet this condition. Rather than pretending to
21
+ // move them and ignoring the edit, detect it, explain why, and provide
22
+ // releaseLayout so users can explicitly release the layout.
23
+
24
+ import { normalizeArchitectureSource, parseArchitecture } from "./architecture.mjs";
25
+
26
+ /** Standard movement increment in canvas coordinates. */
27
+ export const EDIT_STEP = 10;
28
+ /** Fine movement increment while Shift is held. */
29
+ export const EDIT_FINE_STEP = 1;
30
+ /** Default maximum number of edits stored in history. */
31
+ export const EDIT_HISTORY_LIMIT = 100;
32
+
33
+ // Same range as schema coordinate / extent. Values outside it fail reparsing,
34
+ // so clamp before writing back.
35
+ const COORDINATE_MIN = -4000;
36
+ const COORDINATE_MAX = 4000;
37
+ const EXTENT_MIN = 1;
38
+ const EXTENT_MAX = 4000;
39
+ // Layout calculations produce fractional values. Excessive rounding shifts the
40
+ // diagram when releasing layout, so retain precision to 1/10000 canvas units,
41
+ // below the threshold of visual impact.
42
+ const COORDINATE_PRECISION = 4;
43
+
44
+ function isPlainObject(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+
48
+ function roundCoordinate(value) {
49
+ const rounded = Number(value.toFixed(COORDINATE_PRECISION));
50
+ // Normalize -0 to 0 because JSON renders it as "-0", creating noisy diffs.
51
+ return Object.is(rounded, -0) ? 0 : rounded;
52
+ }
53
+
54
+ function clamp(value, min, max) {
55
+ return Math.min(max, Math.max(min, value));
56
+ }
57
+
58
+ /**
59
+ * Parse a sourcePath such as `elements[0].children[2]` into
60
+ * [{key:"elements",index:0},{key:"children",index:2}].
61
+ * Return null for unexpected forms so the caller aborts the write-back.
62
+ */
63
+ export function parseSourcePath(sourcePath) {
64
+ if (typeof sourcePath !== "string" || sourcePath.length === 0) return null;
65
+ const segments = [];
66
+ const pattern = /([A-Za-z_][A-Za-z0-9_]*)\[(\d+)\]/g;
67
+ let cursor = 0;
68
+ let match = pattern.exec(sourcePath);
69
+ while (match !== null) {
70
+ if (match.index !== cursor) return null;
71
+ segments.push({ key: match[1], index: Number(match[2]) });
72
+ cursor = match.index + match[0].length;
73
+ match = pattern.exec(sourcePath);
74
+ // Consume "." only when another segment follows; otherwise a trailing dot
75
+ // such as "elements[0]." would be accepted.
76
+ if (match !== null && sourcePath[cursor] === ".") cursor += 1;
77
+ }
78
+ if (segments.length === 0 || cursor !== sourcePath.length) return null;
79
+ return segments;
80
+ }
81
+
82
+ /**
83
+ * Get the sourcePath element from raw JSON.
84
+ * parent is the group whose children contains the element, or null for a top-level element.
85
+ */
86
+ export function resolveRawElement(raw, sourcePath) {
87
+ const segments = parseSourcePath(sourcePath);
88
+ if (!segments || !isPlainObject(raw)) return null;
89
+ let owner = raw;
90
+ for (let i = 0; i < segments.length; i += 1) {
91
+ const { key, index } = segments[i];
92
+ const list = owner?.[key];
93
+ if (!Array.isArray(list)) return null;
94
+ const next = list[index];
95
+ if (!isPlainObject(next)) return null;
96
+ if (i === segments.length - 1) {
97
+ return { element: next, parent: owner === raw ? null : owner };
98
+ }
99
+ owner = next;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /** sourcePath of the parent group, or null for a top-level element. */
105
+ export function parentSourcePath(sourcePath) {
106
+ if (typeof sourcePath !== "string") return null;
107
+ const marker = sourcePath.lastIndexOf(".children[");
108
+ return marker === -1 ? null : sourcePath.slice(0, marker);
109
+ }
110
+
111
+ function findById(model, id) {
112
+ return (
113
+ model.elements.find((element) => element.type !== "connector" && element.id === id) ?? null
114
+ );
115
+ }
116
+
117
+ function findBySourcePath(model, sourcePath) {
118
+ return model.elements.find((element) => element.sourcePath === sourcePath) ?? null;
119
+ }
120
+
121
+ /**
122
+ * Return what ultimately determines the element's position.
123
+ * When movable is false, include the layout-managed reason and the group ID to release.
124
+ */
125
+ export function describePlacement(model, id) {
126
+ const element = findById(model, id);
127
+ if (!element) return { found: false, movable: false, reason: "unknown", id };
128
+ const parentPath = parentSourcePath(element.sourcePath);
129
+ const parent = parentPath ? findBySourcePath(model, parentPath) : null;
130
+ const origin = parent ? { x: parent.x, y: parent.y } : { x: 0, y: 0 };
131
+ if (parent?.layout) {
132
+ return {
133
+ found: true,
134
+ movable: false,
135
+ reason: "layout-managed",
136
+ id,
137
+ type: element.type,
138
+ layoutOwner: parent.id,
139
+ layoutType: parent.layout.type,
140
+ origin,
141
+ };
142
+ }
143
+ return {
144
+ found: true,
145
+ movable: true,
146
+ reason: "free",
147
+ id,
148
+ type: element.type,
149
+ layoutOwner: null,
150
+ layoutType: null,
151
+ origin,
152
+ };
153
+ }
154
+
155
+ /** Serialize the edited DSL with stable two-space formatting. */
156
+ export function serializeArchitecture(raw) {
157
+ return `${JSON.stringify(raw, null, 2)}\n`;
158
+ }
159
+
160
+ /**
161
+ * Editing session that keeps source (the complete DSL) synchronized with the model
162
+ * and adds the complete new DSL to history after each change.
163
+ * One edit equals one snapshot, so undo / redo only moves the history index.
164
+ */
165
+ export function createArchitectureEditSession(source, options = {}) {
166
+ const limit = Math.max(1, Math.trunc(options.historyLimit ?? EDIT_HISTORY_LIMIT));
167
+ const entries = [snapshot(source)];
168
+ let cursor = 0;
169
+
170
+ function snapshot(text) {
171
+ const normalized = normalizeArchitectureSource(text);
172
+ return { source: normalized, model: parseArchitecture(normalized) };
173
+ }
174
+
175
+ function current() {
176
+ return entries[cursor];
177
+ }
178
+
179
+ function push(entry) {
180
+ entries.splice(cursor + 1);
181
+ entries.push(entry);
182
+ if (entries.length > limit) entries.shift();
183
+ cursor = entries.length - 1;
184
+ }
185
+
186
+ function commit(raw, info) {
187
+ const text = serializeArchitecture(raw);
188
+ let model;
189
+ try {
190
+ model = parseArchitecture(text);
191
+ } catch (error) {
192
+ // Do not add invalid rewritten DSL to history, preventing broken diagrams from being saved.
193
+ return { ...info, ok: false, reason: "rejected", message: error?.message ?? "" };
194
+ }
195
+ push({ source: text, model });
196
+ return { ...info, ok: true, source: text, model };
197
+ }
198
+
199
+ function move(id, dx, dy) {
200
+ const { source: text, model } = current();
201
+ const placement = describePlacement(model, id);
202
+ if (!placement.found) return { ...placement, ok: false };
203
+ if (!placement.movable) return { ...placement, ok: false };
204
+ const element = findById(model, id);
205
+ const raw = JSON.parse(text);
206
+ const located = resolveRawElement(raw, element.sourcePath);
207
+ if (!located) return { ok: false, reason: "unresolved", id };
208
+ const x = clamp(
209
+ roundCoordinate(element.x - placement.origin.x + dx),
210
+ COORDINATE_MIN,
211
+ COORDINATE_MAX,
212
+ );
213
+ const y = clamp(
214
+ roundCoordinate(element.y - placement.origin.y + dy),
215
+ COORDINATE_MIN,
216
+ COORDINATE_MAX,
217
+ );
218
+ if (x === located.element.x && y === located.element.y) {
219
+ return { ok: false, reason: "unchanged", id, x, y };
220
+ }
221
+ located.element.x = x;
222
+ located.element.y = y;
223
+ return commit(raw, { reason: "moved", id, x, y, type: element.type });
224
+ }
225
+
226
+ /**
227
+ * Remove layout from a group and write the x / y / width / height calculated
228
+ * by that layout to every flowed child.
229
+ *
230
+ * The schema requires all four values for children of a parent without a layout
231
+ * (boxRequired). Writing the calculated values preserves the diagram's appearance.
232
+ */
233
+ function releaseLayout(groupId) {
234
+ const { source: text, model } = current();
235
+ const group = findById(model, groupId);
236
+ if (!group) return { ok: false, reason: "unknown", id: groupId };
237
+ if (group.type !== "group") return { ok: false, reason: "not-a-group", id: groupId };
238
+ if (!group.layout) return { ok: false, reason: "not-layout-managed", id: groupId };
239
+ const raw = JSON.parse(text);
240
+ const located = resolveRawElement(raw, group.sourcePath);
241
+ if (!located) return { ok: false, reason: "unresolved", id: groupId };
242
+ const children = Array.isArray(located.element.children) ? located.element.children : [];
243
+ let released = 0;
244
+ children.forEach((child, index) => {
245
+ if (!isPlainObject(child)) return;
246
+ if (child.type !== "node" && child.type !== "group") return;
247
+ const placed = findBySourcePath(model, `${group.sourcePath}.children[${index}]`);
248
+ if (!placed) return;
249
+ child.x = clamp(roundCoordinate(placed.x - group.x), COORDINATE_MIN, COORDINATE_MAX);
250
+ child.y = clamp(roundCoordinate(placed.y - group.y), COORDINATE_MIN, COORDINATE_MAX);
251
+ child.width = clamp(roundCoordinate(placed.width), EXTENT_MIN, EXTENT_MAX);
252
+ child.height = clamp(roundCoordinate(placed.height), EXTENT_MIN, EXTENT_MAX);
253
+ released += 1;
254
+ });
255
+ delete located.element.layout;
256
+ return commit(raw, {
257
+ reason: "layout-released",
258
+ id: groupId,
259
+ layoutType: group.layout.type,
260
+ released,
261
+ });
262
+ }
263
+
264
+ function undo() {
265
+ if (cursor === 0) return { ok: false, reason: "no-history" };
266
+ cursor -= 1;
267
+ return { ok: true, reason: "undone", source: current().source, model: current().model };
268
+ }
269
+
270
+ function redo() {
271
+ if (cursor >= entries.length - 1) return { ok: false, reason: "no-history" };
272
+ cursor += 1;
273
+ return { ok: true, reason: "redone", source: current().source, model: current().model };
274
+ }
275
+
276
+ return {
277
+ get source() {
278
+ return current().source;
279
+ },
280
+ get model() {
281
+ return current().model;
282
+ },
283
+ get canUndo() {
284
+ return cursor > 0;
285
+ },
286
+ get canRedo() {
287
+ return cursor < entries.length - 1;
288
+ },
289
+ get depth() {
290
+ return entries.length;
291
+ },
292
+ describe: (id) => describePlacement(current().model, id),
293
+ move,
294
+ releaseLayout,
295
+ undo,
296
+ redo,
297
+ };
298
+ }