@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,1014 @@
1
+ # MarkdStage canvas Extension
2
+
3
+ **Markdown, ready for the stage.**
4
+
5
+ MarkdStage renders Markdown slide fragments with themes in a **native Copilot
6
+ canvas**. The `markdstage` skill uses this extension to run presentations.
7
+
8
+ ## How it works
9
+
10
+ ```text
11
+ Agent
12
+ | open_canvas("MarkdStage", { input: { slides: [...] } })
13
+ | opens and registers the complete deck at startup
14
+ | invoke_canvas_action("load_deck", { slides: [...] })
15
+ | replaces the deck during a presentation
16
+ v
17
+ extension.mjs (Node / @github/copilot-sdk)
18
+ | starts one loopback HTTP server per instance
19
+ | applies input.slides before open returns its URL, avoiding a placeholder
20
+ | stores the complete deck and current index and exposes the current slide at /state
21
+ | accepts canvas navigation through POST /navigate
22
+ | monitors Surface Pen Win+F20 / Win+F18 shortcuts on Windows
23
+ | publishes updates through /events (SSE)
24
+ v
25
+ Canvas iframe (renderer/)
26
+ | renders Markdown with marked and sanitizes HTML with DOMPurify
27
+ | highlights language-tagged code fences with highlight.js
28
+ | converts ```mermaid blocks with mermaid.run
29
+ | converts validated ```architecture JSON DSL into a safe SVG DOM
30
+ | provides ◀ ▶, ✎, 16:9 PDF preview, margin clicks, arrow keys, and the ☰ slide list
31
+ | opens a synchronized external window with ⛶
32
+ v
33
+ The themed slide is displayed and updates automatically
34
+ ```
35
+
36
+ - **Register every slide at startup** in the `slides` field of `open_canvas`
37
+ `input`. The open handler applies the deck before returning the URL, so the
38
+ first slide appears immediately without a "deck not loaded" placeholder.
39
+ Use `load_deck` only to replace content or theme during the presentation. Omit
40
+ open input when only refocusing an existing instance; every non-empty open
41
+ input must include `slides`. `sourceName` is resolution/output metadata only:
42
+ it never reads or watches the named Markdown file.
43
+ Users navigate through ◀ ▶, left-click/right-click on empty slide margins,
44
+ arrow keys, or ☰. The canvas posts navigation to `POST /navigate`, and every
45
+ client stays synchronized. Left-click advances; right-click goes back.
46
+ Content, links, images, navigation controls, and the slide list retain their
47
+ normal interactions and context menus. No external server or manually
48
+ started `localhost` port is required.
49
+ - On Windows, **one press of the Surface Pen tail button advances, and a long
50
+ press goes back**. A small PowerShell keyboard-hook helper receives
51
+ `Win+F20` and `Win+F18` and forwards them to the existing navigation path.
52
+ `Win+F19` double press, pen connection, removal, or docking never launches
53
+ the external presenter. The implementation does not depend on the packaged
54
+ app identity required by the official `PenButtonListener`.
55
+ - Themes are **dark (default), light, microsoft, and custom**. Set a deck theme
56
+ through open input or `load_deck`. The renderer sets `<html data-theme>` and
57
+ `slides.css` supplies the palette. `microsoft` is built in. `custom` loads a
58
+ custom-property-only CSS file from `themeFile` or front matter `theme-file`.
59
+ Precedence is **explicit canvas theme > Markdown front matter > dark**.
60
+ Theme-file lookup tries the source Markdown folder before the repository
61
+ root, allowing a deck-local file to override a shared file with the same
62
+ path. Files outside the workspace and arbitrary selectors are rejected.
63
+ A sibling `theme.json` may define cover background, cover/back-cover logos,
64
+ and copyright. **Every theme automatically receives a final
65
+ `layout: backcover` slide** unless one already exists. Logo and copyright
66
+ appear only when supplied by metadata or front matter.
67
+ - See [`docs/custom-theme-authoring.md`](docs/custom-theme-authoring.md) for
68
+ custom themes. AI can retrieve the same guidance through `markdstage_guide`
69
+ topics `custom-themes` and `theme-schema`. `schema/theme-v1.json` describes
70
+ standard custom properties, and `schema/theme-metadata-v1.schema.json`
71
+ describes `theme.json`.
72
+ - Content size has four levels: **auto (default), normal, large, and xlarge**.
73
+ `auto` measures standard slides without code, tables, images, or Mermaid and
74
+ enlarges only when ample space remains.
75
+ - Put **speaker notes** in top-level HTML comments on each slide. Presenter view
76
+ renders notes as Markdown and follows navigation. Notes are absent from
77
+ regular slides, the external presenter, and PDF output.
78
+ - The **canvas renderer** owns navigation controls, ✎ editing, the slide list,
79
+ and current position. ✎ toggles the same placement mode as
80
+ `edit_architecture`. For Markdown loaded with 📂, **Advanced editing** opens
81
+ the dedicated `architecture-editor` canvas. The agent only opens the deck and
82
+ does not run an `ask_user` loop. Margin clicks are installed only in normal
83
+ canvas and presenter modes, never print mode. `goto_slide` remains available
84
+ for an explicit page request from chat.
85
+ - **PDF Export is available from the printer icon.** When `sourceName` is passed
86
+ to open / `load_deck`, the printer saves `<source-name>.pdf` in the workspace.
87
+ It does not load or watch that file. AI may call `export_pdf` with another
88
+ `outputPath`. Hidden print mode renders every page, then headless Edge/Chrome
89
+ produces a 16:9 PDF with backgrounds, images, highlighted code, and Mermaid.
90
+ - Use the **16:9 control** to letterbox the current slide inside the canvas with
91
+ the same fixed 1280×720 typography, spacing, diagram limits, and clipping used
92
+ by PDF output. This preview is local to the canvas and does not change deck
93
+ state. A visible and accessible warning identifies content that would be
94
+ clipped.
95
+ - AI should call **`inspect_layout` before exporting a non-scrolling deck**. It
96
+ renders the currently registered in-memory output snapshot in headless
97
+ Chromium; it does not read or validate the source file named by `sourceName`.
98
+ The snapshot includes a temporary `show_slide` replacement. Prefer one
99
+ whole-deck inspection, or serialize targeted inspections because PDF, layout,
100
+ and PNG output jobs are intentionally exclusive. The result contains compact
101
+ JSON for clipped pages, including vertical/horizontal overflow and bounded
102
+ element hints. Call `capture_slides` only when visual inspection is needed;
103
+ PNGs are fixed 1280×720 files and the action returns paths instead of inline
104
+ image data.
105
+ - Open the **external presenter** with ⛶ or `open_presenter`. Edge / Chrome /
106
+ Chromium starts in a dedicated temporary profile as a movable, resizable
107
+ 1280×720 app-mode window. It shares `/state`, `/navigate`, and SSE with the
108
+ canvas, so keyboard and Surface Pen position remain synchronized. Move it to
109
+ the target monitor and use standard browser/OS full-screen controls (`F11`
110
+ on Windows). Close it with `Alt+F4` or `close_presenter`. Closing the canvas
111
+ also stops it.
112
+ - Local image lookup for `/assets/...` tries `assets/` beside the source
113
+ Markdown, then workspace-root `assets/`. `sourceName` determines the source
114
+ folder. This lets deck-local images override workspace-wide images.
115
+ - In other words, lookup checks `assets/` beside the Markdown before
116
+ `assets/` at the workspace root, using `sourceName` as the resolution base.
117
+ - Add language names such as `csharp`, `json`, or `diff` to code fences for
118
+ highlight.js syntax highlighting.
119
+
120
+ ## Markdown import (📂)
121
+
122
+ Users can load Markdown directly from the canvas without AI. Press 📂 or `I` to
123
+ open a filtered list of workspace `*.md` / `*.markdown` files. Selecting one
124
+ loads it, splits it into slides, and replaces the deck. Import also works from
125
+ the initial waiting view. The control is not shown in presenter or print mode.
126
+ The workspace root is the Git repository root when available, otherwise the
127
+ folder opened for the current session.
128
+
129
+ Import offers two update modes:
130
+
131
+ - **Keep imported snapshot (default):** retain the deck exactly as imported and
132
+ ignore later saves.
133
+ - **Update automatically on save:** watch the Markdown and reload deck and
134
+ front-matter theme while preserving the current page when possible.
135
+
136
+ For a source-backed deck, the toolbar update button can switch modes at any
137
+ time. Switching to live mode loads the latest file immediately. Switching to
138
+ snapshot preserves the current display. If the watched file becomes empty,
139
+ deleted, or unreadable, MarkdStage retains the last valid deck and marks the
140
+ button as an error. The next valid save recovers automatically.
141
+
142
+ Splitting follows **Slidev / Marp syntax** mechanically. Summarizing prose into
143
+ slides remains the AI's responsibility.
144
+
145
+ - A `---` line immediately after a blank line separates slides. Setext heading
146
+ underlines and separators inside code fences do not split.
147
+ - **Initial front matter is deck configuration** inherited by all slides
148
+ (`theme`, `theme-file`, `deck`, `kicker`, `size`, `logo`, `copyright`, and
149
+ related keys). It is also slide one's own front matter, so
150
+ `layout: title` affects slide one only.
151
+ - **Each page may have front matter.** When the block after the separator
152
+ contains only `key: value` entries and is closed by `---`, it is treated as
153
+ that page's front matter; the separator line also opens the block.
154
+ - Precedence is **page front matter > deck front matter > generated values**.
155
+ - `page` and `total` are generated only when absent and are not generated for
156
+ `title`, `section`, or `backcover` layouts. Explicit values are preserved.
157
+
158
+ ```markdown
159
+ ---
160
+ title: Sample
161
+ theme: microsoft
162
+ layout: title
163
+ ---
164
+
165
+ # Cover
166
+
167
+ ---
168
+ kicker: Getting started
169
+ layout: section
170
+ ---
171
+
172
+ ## Slide two
173
+ ```
174
+
175
+ The imported filename is retained as `sourceName`, enabling source-based PDF
176
+ naming, adjacent `assets/`, and Markdown-relative `theme-file` lookup with
177
+ workspace-root fallback. Files outside the workspace cannot be selected; no
178
+ OS file dialog is used.
179
+
180
+ ## AI authoring guide
181
+
182
+ The extension provides `markdstage_guide`. Before authoring Markdown for the
183
+ MarkdStage canvas, AI should request the required `overview`, `slide-format`,
184
+ `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or
185
+ `architecture-schema` topic. Runtime guidance is generated from this README and
186
+ `schema/architecture-v1.schema.json`, so user-scoped extension installs expose
187
+ the same contract.
188
+
189
+ When the extension detects a presentation-related prompt, it adds a short hint
190
+ to use `markdstage_guide` at most once per session. No hint is added after the
191
+ tool has already been called, and session-end cleanup removes the state.
192
+
193
+ ### Slide fragment format
194
+
195
+ Each element in open / `load_deck` `slides` is one Markdown string. It may start
196
+ with front matter delimited by `---`, followed by GFM-compatible content.
197
+
198
+ | Front matter | Purpose |
199
+ | --- | --- |
200
+ | `deck` / `kicker` | Footer deck name / label above the heading |
201
+ | `page` / `total` | One-based current page / total page count |
202
+ | `title` | Browser-tab title |
203
+ | `layout` | `title` for a cover, `section` for a section divider, `backcover` for the back cover, or `center` to vertically center heading and body together. Standard slides omit it and align heading and body to the top. |
204
+ | `size` | `auto` (default), `normal`, `large`, or `xlarge` |
205
+ | `theme` | Per-slide override; normally use the deck theme |
206
+ | `theme-file` | CSS for `custom`, resolved beside the Markdown before workspace root |
207
+ | `logo` / `copyright` | Override `backcover` metadata |
208
+
209
+ Make the first slide a `layout: title` cover. The extension appends a final
210
+ `layout: backcover`. Content may contain headings, lists, tables, images, code,
211
+ `mermaid`, and `architecture`.
212
+
213
+ Put speaker notes in top-level HTML comments, as in Slidev / Marp. Comments may
214
+ contain Markdown, and multiple comments are displayed with a blank line between
215
+ them. Comments inside code fences and `<!-- slide-size: ... -->` directives are
216
+ not notes.
217
+
218
+ ```markdown
219
+ ## Demo
220
+
221
+ - Only this content appears on the slide
222
+
223
+ <!--
224
+ First explain the **prerequisites**.
225
+
226
+ 1. Demonstrate the operation
227
+ 2. Take questions
228
+ -->
229
+ ```
230
+
231
+ Write local images as `![Alternative text](/assets/foo.png)` and pass the source
232
+ Markdown's workspace-relative path as `sourceName`. Lookup tries adjacent
233
+ `assets/` before workspace-root `assets/`. Architecture `icon` and `image.src`
234
+ use `assets/foo.svg` without a leading slash and follow the same lookup order.
235
+ `sourceName` supplies this resolution base only; it does not read or watch the
236
+ Markdown file.
237
+ Specifically, lookup checks `assets/` beside the Markdown before `assets/` at the workspace root.
238
+
239
+ On a standard slide, the first H1/H2 is fixed in the top title area, so its
240
+ position does not move with body length. Later headings remain in the body.
241
+ Specialized `title`, `section`, and `backcover` layouts retain their own
242
+ positioning.
243
+
244
+ The standard body begins **top-aligned** below the title. Use `layout: center`
245
+ only when a short slide should center heading and body together. The footer
246
+ remains fixed to the bottom.
247
+
248
+ ```markdown
249
+ ---
250
+ layout: center
251
+ deck: Presentation
252
+ page: 3
253
+ total: 8
254
+ ---
255
+
256
+ ## Center only this slide vertically
257
+
258
+ - The heading and body are centered as one block
259
+ ```
260
+
261
+ For an intermediate chapter divider, use `layout: section`, normally with one
262
+ H1/H2. Add `kicker` or footer data only when needed. The background follows the
263
+ theme and contains no image, logo, or icon.
264
+
265
+ ```markdown
266
+ ---
267
+ layout: section
268
+ ---
269
+
270
+ ## Key GitHub Copilot features
271
+ ```
272
+
273
+ ### Choosing a theme
274
+
275
+ Set the deck-wide `theme`; use `dark` when omitted.
276
+
277
+ For `custom`, resolve `theme-file` first from the same folder as the source Markdown
278
+ and then from the workspace root. A shared `themes/brand/theme.css` may therefore be
279
+ overridden by `<Markdown folder>/themes/brand/theme.css`. AI must pass the
280
+ workspace-relative source path as `sourceName`.
281
+
282
+ | Theme | Selection guidance |
283
+ | --- | --- |
284
+ | `dark` | Dark, black-based, cool, or striking |
285
+ | `light` | Bright, white-based, clean, and neutral |
286
+ | `microsoft` | Microsoft, Fluent, Office, or the Microsoft four-color style |
287
+ | `custom` | Reproduce brand colors or an organizational template with CSS custom properties |
288
+
289
+ ## Architecture DSL v1
290
+
291
+ An `architecture` code fence renders a position-stable JSON DSL as SVG. Canvas
292
+ dimensions use logical coordinates and a `viewBox`, so canvas, external
293
+ presenter, and PDF preserve the same proportions.
294
+
295
+ An empty fence is valid shorthand for a diagram with zero elements. The
296
+ dedicated Architecture Editor replaces it with canonical JSON based on
297
+ `{ "version": 1, "elements": [] }`. Non-empty invalid JSON remains an error.
298
+ Its changes affect source Markdown only when explicitly saved.
299
+
300
+ ````markdown
301
+ ```architecture
302
+ ```
303
+ ````
304
+
305
+ When writing explicit JSON, `elements` is required by the JSON Schema.
306
+
307
+ > **Architecture DSL v1 is stable.** The [placement editor](#placement-editing)
308
+ > is part of v1, not an experimental feature. A document accepted as v1 will
309
+ > continue to be accepted as v1, and diagram meaning—element placement and
310
+ > connections—is preserved. Compatibility guarantees and migration policy are
311
+ > documented in [`schema/README.md`](./schema/README.md).
312
+ >
313
+ > The guarantee excludes pixel-identical rendering, which can change with font
314
+ > metrics, theme tokens, or routing improvements, and excludes exact diagnostic
315
+ > wording. Continue to use Mermaid for complex automatic layout.
316
+
317
+ ````markdown
318
+ ```architecture
319
+ {
320
+ "version": 1,
321
+ "title": "Web application architecture",
322
+ "description": "A client, application tier, and database.",
323
+ "canvas": { "width": 1600, "height": 900 },
324
+ "elements": [
325
+ {
326
+ "type": "group", "id": "cloud", "x": 480, "y": 90,
327
+ "width": 1040, "height": 700, "title": "Cloud",
328
+ "layout": { "type": "row", "gap": 60, "padding": 70 },
329
+ "children": [
330
+ {
331
+ "type": "node", "id": "api", "shape": "rounded-rect",
332
+ "text": "API", "icon": "api",
333
+ "style": { "fill": "surface", "stroke": "accent" }
334
+ },
335
+ {
336
+ "type": "node", "id": "db", "shape": "ellipse",
337
+ "text": "Database", "icon": "database"
338
+ }
339
+ ]
340
+ },
341
+ {
342
+ "type": "node", "id": "client", "shape": "rect",
343
+ "x": 80, "y": 330, "width": 280, "height": 150, "text": "Client"
344
+ },
345
+ {
346
+ "type": "connector", "from": "client", "to": "api",
347
+ "fromPort": "right", "toPort": "left",
348
+ "routing": "orthogonal", "label": "HTTPS", "arrow": true
349
+ },
350
+ {
351
+ "type": "connector", "from": "api", "to": "db",
352
+ "routing": "polyline", "points": [{ "x": 500, "y": 400 }],
353
+ "label": "SQL", "arrow": true, "z": -10
354
+ }
355
+ ]
356
+ }
357
+ ```
358
+ ````
359
+
360
+ - A `node` has a unique `id`, multiline `text`, and shape `rect`,
361
+ `rounded-rect`, or `ellipse`.
362
+ - `icon` is a built-in name or a path under an adjacent or workspace-root
363
+ `assets/` folder. See [Icons](#icons).
364
+ - An `image` places an `assets/` image as an independent element. `fit` is
365
+ `contain` (default), `cover`, or `stretch`; use `ariaLabel` for its accessible
366
+ name. Images participate in group layout, z-order, connector endpoints, and
367
+ orthogonal-routing obstacles like nodes.
368
+ - Group-child coordinates are relative to the group's top left. Group border
369
+ and title render before children. Without `layout`, explicit coordinates are
370
+ used. `row`, `column`, and `grid` calculate positions and omitted dimensions
371
+ from group interior size plus `gap` or `rowGap` / `columnGap`, `padding`, and
372
+ `columns`. `layered` uses dependency direction. These are deterministic
373
+ presentation helpers, not general graph auto-layout.
374
+ - A `connector` attaches to `from` / `to` boundaries and supports `straight`,
375
+ `orthogonal`, and `polyline`, arrows, and labels. Ports are `auto`, `top`,
376
+ `right`, `bottom`, or `left`. Parallel edges receive stable lanes, and
377
+ branching exits separate. Orthogonal routing resolves short edges first and
378
+ favors corridors that reduce overlap and crossings with nodes, images, and
379
+ already routed connectors. A default 14-logical-pixel gap separates line
380
+ endpoints from boxes. For complex diagrams, specify `lane` or polyline
381
+ `points`. Long labels shrink based on Unicode display width and are visually
382
+ omitted when they cannot fit, while the full value remains in `aria-label`.
383
+ A label that would hide its own line is moved perpendicular to it.
384
+ `labelLayer: "front"` (default) draws labels in front of boxes; `"behind"`
385
+ leaves them in connector z-order.
386
+ - Elements render from smaller `z` (`-100` to `100`) to larger, preserving
387
+ declaration order for equal values. Defaults are group `-50`, connector
388
+ `-10`, and node/image `0`, giving container → line → box order. Connector `z`
389
+ applies to its line and `"behind"` label; `"front"` labels use a final layer.
390
+ - `style` supports `fill`, `stroke`, `textColor`, `strokeWidth`, `fontSize`,
391
+ `opacity`, `dash`, and `cornerRadius`. Prefer theme tokens `accent`,
392
+ `accentStrong`, `accentSoft`, `accentLine`, `surface`, `fg`, `muted`, `body`,
393
+ `border`, and `bg`. Literal colors are limited to hex, white, black, and
394
+ transparent.
395
+ - Invalid JSON, out-of-range numbers, duplicate IDs, unknown references, and
396
+ unsupported elements, styles, or colors render an inline diagram error while
397
+ preserving other slide content. DSL values never generate HTML, script, or
398
+ event attributes. Generated asset URLs stay on same-origin `/assets/...`.
399
+ - `version` is currently `1` and defaults to v1. Limits include 64 KiB source,
400
+ 200 total elements, 100 connectors, four nesting levels, 12 polyline
401
+ intermediate points, 20,000 total text characters, and 200-character icon/src
402
+ references.
403
+ - Diagnostics include a JSON path and remediation after `;`, for example:
404
+ `elements[0].icon: must be a built-in icon name (cloud, database, ...) or a
405
+ path under assets/; replace 'rocket' with a built-in name, or with a
406
+ repository asset such as 'assets/icons/logo.svg' (...)`.
407
+ - A draft 2020-12 JSON Schema is provided at
408
+ [`schema/architecture-v1.schema.json`](./schema/architecture-v1.schema.json).
409
+ A relative `$schema` in a standalone `.architecture.json` enables editor
410
+ completion and validation; the same JSON can be pasted into a fence. The
411
+ parser accepts and ignores root `$schema`. The schema validates structure;
412
+ the parser still validates references, ID uniqueness, flattened limits, and
413
+ layout fit. See [`schema/README.md`](./schema/README.md).
414
+ - The SVG root has `<title>`, `<desc>`, and `aria-labelledby`. Group, node,
415
+ image, and connector elements receive meaningful roles, `aria-label`, and SVG
416
+ `<title>`. Use root `description` and element `ariaLabel` when needed.
417
+
418
+ ### Accessibility
419
+
420
+ Automated checks in `test/a11y/` use axe-core and inspect Chromium's
421
+ accessibility tree through CDP.
422
+
423
+ **Exposed content**
424
+
425
+ - The diagram root exposes `<title>` and `<desc>` through `aria-labelledby`.
426
+ `description` should summarize the diagram for readers who cannot see it.
427
+ - Groups, nodes, and connectors each have `aria-label`. A connector defaults to
428
+ `<visible from label> to <visible to label>: <label>`. Endpoints use visible
429
+ node `text` / group `title`, falling back to ID only when no visible label
430
+ exists. Explicit `ariaLabel` overrides the complete name.
431
+ - Visible diagram text uses `aria-hidden="true"` to prevent duplicate reading
432
+ as both accessible name and child text. This does not affect rendering.
433
+
434
+ **Reading order**
435
+
436
+ - Reading order is **DOM order = z-order rendering order**, not declaration
437
+ order.
438
+ - With default z values, groups are read first, then connectors, then nodes.
439
+ - Declaration order is exposed as `data-architecture-order` for tooling but
440
+ does not control assistive-technology order.
441
+ - **Use `z` for visual stacking, not reading or traversal order.**
442
+ - The implementation does not use `aria-flowto` or `aria-owns` because support
443
+ varies across Windows WebView2, macOS WKWebView, and Linux WebKitGTK.
444
+
445
+ **Keyboard**
446
+
447
+ - In regular, presenter, and print modes, each diagram is exactly one tab stop.
448
+ Individual diagram elements are not tab stops.
449
+ - Element-level traversal belongs to edit mode, where nodes and groups become
450
+ tab stops and arrow keys move the selected element. The root tab stop is then
451
+ removed.
452
+ - Edit-mode tab order still follows DOM/render order.
453
+
454
+ **Edit-mode announcements**
455
+
456
+ - The toolbar contains two `role="status"` / `aria-live="polite"` regions:
457
+ one for operation results and one for save results.
458
+ - **Do not combine them.** An operation may replace the operation message, but
459
+ a save failure means an edit may be lost and must remain until a later save
460
+ succeeds. A combined region would let the next operation erase that warning.
461
+ - Both regions may update nearly simultaneously; some assistive technologies
462
+ might overlap announcements. Preserve the independent failure lifetime when
463
+ improving this behavior.
464
+
465
+ **Known limits**
466
+
467
+ - Real screen-reader speech has not been validated with NVDA, JAWS, VoiceOver,
468
+ or Narrator. Evidence comes from Chromium's accessibility tree. Do not change
469
+ role choices, such as `role="group"` for connectors, without device testing.
470
+ - Nested groups render visually but are flattened in the accessibility tree.
471
+ Membership is not conveyed automatically; include it in `ariaLabel` when
472
+ necessary.
473
+ - Automated WCAG 2.1 A/AA and diagram best-practice checks do not prove that a
474
+ result is accessible.
475
+
476
+ **Rendering cost**
477
+
478
+ - A `MAX_ELEMENTS` (200-element) diagram takes approximately 10–12 ms from
479
+ parse through layout, routing, and SVG generation on the development machine.
480
+ `test/perf/` locks absolute time and scaling below 24× cost for 8× elements.
481
+
482
+ ### Browser support
483
+
484
+ | Feature | Supported environment |
485
+ | --- | --- |
486
+ | Canvas slide rendering and placement editing | Tauri WebView: Windows WebView2 / Chromium, macOS WKWebView / WebKit, Linux WebKitGTK |
487
+ | External presenter | Edge / Chrome / Chromium only, because it starts with `--app` |
488
+ | PDF export | Edge / Chrome / Chromium only, because it uses headless `--print-to-pdf` |
489
+ | Automated tests | Playwright Chromium; visual baselines are separate for Linux and Windows |
490
+
491
+ Standalone Firefox and Safari are untested. Presenter and PDF workflows directly
492
+ depend on Chromium launch options.
493
+
494
+ ### Icons
495
+
496
+ `node.icon` accepts a **built-in icon name** or an **image path under
497
+ Markdown-adjacent or workspace-root `assets/`**.
498
+
499
+ #### Built-in icons
500
+
501
+ Built-ins read no external asset. They use bundled 24×24 SVG primitives.
502
+
503
+ | Name | Intended concept |
504
+ | --- | --- |
505
+ | `cloud` | Cloud or managed service |
506
+ | `database` | Relational database or persistent store |
507
+ | `api` | API, endpoint, or contract |
508
+ | `user` | User, person, or actor |
509
+ | `server` | Server, host, or worker |
510
+ | `analytics` | Analytics, metrics, or dashboard |
511
+ | `browser` | Browser or web front end |
512
+ | `mobile` | Mobile application or device |
513
+ | `network` | Network, connection, or distributed system |
514
+ | `queue` | Queue, messaging, or asynchronous processing |
515
+ | `shield` | Authentication, authorization, security, or guardrail |
516
+
517
+ - Names use lowercase kebab-case
518
+ (`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`) and generic concepts rather than product
519
+ or vendor names. Unit tests enforce this rule.
520
+ - Built-ins are unfilled line drawings whose stroke uses node `textColor`
521
+ (theme token `fg` by default), so all themes recolor them automatically.
522
+ Tests enforce at least 3:1 contrast against each theme background under WCAG
523
+ 2.1 SC 1.4.11.
524
+ - Existing names and drawings are public v1 vocabulary. Additions are
525
+ compatible; renaming or redrawing is breaking.
526
+
527
+ #### Using an `assets/` icon
528
+
529
+ Place an image under `assets/` beside the source Markdown or at workspace root,
530
+ and use an `assets/`-relative path. Adjacent assets take precedence. The
531
+ extension serves them on same-origin `/assets/...`, so canvas, presenter, and
532
+ PDF resolve them consistently.
533
+
534
+ ````markdown
535
+ ```architecture
536
+ {
537
+ "elements": [
538
+ {
539
+ "type": "node", "id": "brand",
540
+ "x": 80, "y": 80, "width": 260, "height": 140,
541
+ "text": "Our service", "icon": "assets/sample.svg"
542
+ }
543
+ ]
544
+ }
545
+ ```
546
+ ````
547
+
548
+ Allowed extensions are `.svg`, `.png`, `.webp`, `.jpg`, and `.jpeg`,
549
+ case-insensitively.
550
+
551
+ Accepted paths:
552
+
553
+ - Start with `assets/`; no leading slash.
554
+ - Each segment starts with an alphanumeric character and then uses only
555
+ alphanumeric characters, `_`, `-`, or `.`.
556
+ - Subfolders such as `assets/icons/brand/logo.svg` are allowed.
557
+ - Total length is at most 200 characters.
558
+
559
+ Rejected references:
560
+
561
+ ```text
562
+ https://example.com/logo.svg external URLs are prohibited
563
+ //example.com/logo.svg protocol-relative URLs are prohibited
564
+ data:image/svg+xml;base64,... data URIs are prohibited
565
+ assets/../secret.svg .. is prohibited
566
+ /assets/logo.svg absolute paths are prohibited
567
+ images/logo.svg paths outside assets/ are prohibited
568
+ assets/logo.gif unsupported extensions are prohibited
569
+ assets\logo.svg backslashes are prohibited
570
+ assets/logo.svg?v=2 query strings are prohibited
571
+ ```
572
+
573
+ The parser and JSON Schema enforce the same path shape. Rejection renders an
574
+ inline diagnostic with remediation.
575
+
576
+ #### User-provided icon limits
577
+
578
+ - User assets **do not follow theme colors**. They render as `<image>`, so the
579
+ extension cannot replace internal colors, including in SVG. All themes show
580
+ the same asset.
581
+ - The asset author must ensure readability on dark and light/microsoft
582
+ backgrounds. Use colors with sufficient contrast, give the icon an opaque
583
+ background, or present with one fixed theme.
584
+ - Secure static browser mode is expected to disable script in SVG, but only
585
+ trusted assets should be stored.
586
+ - The parser does not inspect the filesystem. A missing file leaves the icon
587
+ region empty rather than producing a DSL error; verify spelling.
588
+ - Icons are `aria-hidden="true"`, and asset paths are not part of accessible
589
+ names. Put meaning in `text` or `ariaLabel`.
590
+
591
+ #### Licensing and attribution
592
+
593
+ Users are responsible for licenses of images and icons committed to `assets/`.
594
+
595
+ - Commit only assets whose licenses permit redistribution.
596
+ - For assets requiring attribution, such as CC BY or some Apache-2.0-derived
597
+ icon sets, provide credit on a slide or in `assets/README.md`, including
598
+ source, author, license, and URL.
599
+ - Follow owner brand guidelines for third-party trademarks and logos. MarkdStage
600
+ only fits them into a 24×24 box with
601
+ `preserveAspectRatio="xMidYMid meet"` and does not recolor or modify them.
602
+ - The 11 built-in icons were created for this repository, follow its license,
603
+ and require no attribution.
604
+
605
+ ### Standalone image
606
+
607
+ Use `type: "image"` when an image is an independent diagram element rather
608
+ than node decoration. `src` shares the safety rules and formats of `node.icon`.
609
+
610
+ ````markdown
611
+ ```architecture
612
+ {
613
+ "elements": [
614
+ {
615
+ "type": "image", "id": "system-map",
616
+ "src": "assets/system-map.svg",
617
+ "fit": "contain", "ariaLabel": "Complete system diagram",
618
+ "x": 80, "y": 80, "width": 720, "height": 420
619
+ },
620
+ {
621
+ "type": "node", "id": "details",
622
+ "text": "Details", "x": 980, "y": 230, "width": 260, "height": 120
623
+ },
624
+ {
625
+ "type": "connector", "from": "system-map", "to": "details",
626
+ "routing": "orthogonal", "arrow": true
627
+ }
628
+ ]
629
+ }
630
+ ```
631
+ ````
632
+
633
+ | `fit` | Behavior |
634
+ | --- | --- |
635
+ | `contain` | Preserve aspect ratio and show the complete image (default) |
636
+ | `cover` | Preserve aspect ratio and crop overflow to fill the region |
637
+ | `stretch` | Change aspect ratio to fill the region |
638
+
639
+ Without `ariaLabel`, the accessible name is the asset filename, then `id`.
640
+ Images are clipped and participate in row / column / grid / layered layout,
641
+ z-order, connector endpoints, and orthogonal-routing obstacles. Themes do not
642
+ recolor images.
643
+
644
+ ### Automatic connector routing
645
+
646
+ `routing: "orthogonal"` routes without manual polyline points. `labelLayer`
647
+ offers only two stacking choices:
648
+
649
+ | `labelLayer` | Display |
650
+ | --- | --- |
651
+ | `front` (default) | Draw the label after boxes, in front of nodes and images |
652
+ | `behind` | Keep the label in connector z-order, where boxes may cover it |
653
+
654
+ 1. **Candidate enumeration and cost minimization:** evaluate straight, L-shaped,
655
+ and related candidates. In descending penalty order, cost includes node
656
+ intersection, labels covering nodes, intersection/overlap with routes,
657
+ label overlap, turns, and length. Label occupancy uses the same pill size as
658
+ rendering.
659
+ 2. **Escalation to grid search:** when the selected route hits a node or
660
+ conflicts with another route, try Dijkstra search over a sparse coordinate
661
+ grid. Adopt it only when cost strictly improves.
662
+ 3. **Global rip-up and reroute:** sequential placement can favor the first edge,
663
+ so reroute each edge over several complete passes. Replace only when total
664
+ cost decreases, and reject replacements that increase crossings unless they
665
+ fix an obscured node or label.
666
+
667
+ The same input always yields the same route. Connectors are stably sorted by
668
+ distance and declaration order; routing uses no randomness.
669
+
670
+ #### Routing-budget fallback contract
671
+
672
+ Grid search stops at 120 coordinates per axis, 10,000 grid points, or 20,000
673
+ expansions. When a budget is reached, rendering continues with the best
674
+ available route rather than changing a previously renderable diagram into an
675
+ error.
676
+
677
+ Degradation is not silent. Only a route crossing a node or a label covering an
678
+ unrelated node produces:
679
+
680
+ | Notification | Content |
681
+ | --- | --- |
682
+ | Warning banner below the diagram | Amber `role="status"` / `aria-live="polite"` block listing connector and reason |
683
+ | `data-architecture-routing="degraded"` | Wrapper attribute available to automated tests |
684
+ | `console.warn` | Development notification; never `console.error` |
685
+
686
+ Crossings alone do not notify because they may be unavoidable. Explicit
687
+ `straight` and `polyline` routes do not produce automatic-routing diagnostics.
688
+ Resolve degradation by moving nodes or supplying `polyline` points.
689
+
690
+ #### Preventing a label from hiding its own line
691
+
692
+ Labels normally sit at the route midpoint. Because pills have a minimum width
693
+ of 70 logical pixels, nearby nodes can let a pill cover its entire line and
694
+ arrow. For example, a row layout `gap: 60` leaves only 32 visible pixels after
695
+ the default 14-pixel gap at both ends.
696
+
697
+ When midpoint placement would leave less than 50 logical pixels visible, the
698
+ pill moves **perpendicular to its own segment**:
699
+
700
+ - Above a horizontal segment and to the right of a vertical segment.
701
+ - Eight logical pixels between the pill edge and line.
702
+ - Direction-independent: `a → b` and `b → a` choose the same side.
703
+ - Routing costs use the displaced position.
704
+ - Applies equally to `straight`, `orthogonal`, and `polyline`.
705
+
706
+ This is a v1 rendering contract. Exact label coordinates are not guaranteed,
707
+ but a label will not completely hide its own line and arrow. Increase node
708
+ distance or layout `gap` to return the label to its midpoint.
709
+
710
+ ### Layered layout
711
+
712
+ `layout: { "type": "layered" }` arranges group children along connector
713
+ direction. `direction` is `down` (default) or `right`. `up` and `left` are
714
+ rejected; reverse connector `from` / `to` instead. `direction` is rejected for
715
+ non-layered layouts.
716
+
717
+ Cycles terminate safely by ignoring back edges encountered later in declaration
718
+ order while assigning layers. Use this mode when dependency relationships
719
+ should determine placement without writing every coordinate.
720
+
721
+ ### Placement editing
722
+
723
+ Architecture diagrams can be moved directly over the rendered result. For decks
724
+ imported through 📂, edits write back to the source `architecture` fence and
725
+ survive re-import. Decks supplied directly through open / `load_deck` cannot be
726
+ reversibly mapped to a source file, so they save only to canvas deck state.
727
+
728
+ **Placement editing is a stable part of Architecture DSL v1.**
729
+
730
+ - Write-back changes only the original `architecture` fence. Prose, front
731
+ matter, and line endings outside it remain unchanged.
732
+ - If the fence changed externally after import, saving refuses the conflict
733
+ rather than overwriting the source. Re-import before editing again.
734
+ - Save success or failure is always visible.
735
+ - `?present=1` and `?print=1` never create edit UI.
736
+ - Edit mode is server state and is not persisted with the deck.
737
+
738
+ These intentional v1 tradeoffs follow the compatibility policy in
739
+ [`schema/README.md`](./schema/README.md).
740
+
741
+ Enter mode with:
742
+
743
+ | Method | Purpose |
744
+ | --- | --- |
745
+ | Canvas action `edit_architecture` with `{ "enabled": true }` | Normal agent-controlled operation |
746
+ | Renderer URL `?architectureEdit=1` | Local debugging |
747
+
748
+ `reset` disables editing. The query parameter also updates server state through
749
+ `POST /edit-mode`; server state is the sole source of truth, preventing polling
750
+ from disabling a client-only mode or `POST /edit` returning an unexpected 409.
751
+
752
+ | Operation | Mouse | Keyboard |
753
+ | --- | --- | --- |
754
+ | Select | Click a node | Tab / Shift+Tab |
755
+ | Move | Drag | Arrow keys (10 logical px) |
756
+ | Fine adjustment | — | Shift+Arrow (1 px) |
757
+ | Detach layout | Toolbar **Detach layout** | `L` |
758
+ | Undo | Toolbar **Undo** | Ctrl+Z |
759
+ | Redo | Toolbar **Redo** | Ctrl+Shift+Z / Ctrl+Y |
760
+ | Clear selection | Click outside the diagram | Escape |
761
+ | Full editing | Toolbar **Advanced editing** | — |
762
+
763
+ Every move redraws the complete diagram and reroutes connectors. A live status
764
+ region announces results.
765
+
766
+ #### Dedicated Architecture Editor
767
+
768
+ `architecture-editor` is a separate canvas for comprehensive editing of an
769
+ **existing** Markdown `architecture` block. It does not insert new blocks.
770
+
771
+ - Add, delete, duplicate, reorder, and reparent nodes, groups, images, and
772
+ connectors.
773
+ - Drag, resize, snap to grid, zoom, and pan. Scroll horizontally/vertically
774
+ when the diagram exceeds the workspace, or pan with middle-drag / Space-drag.
775
+ - Context menus on elements, empty space, and tree items provide appropriate
776
+ add, connect, duplicate, delete, front/back order, Undo/Redo, and Save
777
+ commands. Group **Layout** menus offer `none`, `row`, `column`, `grid`, and
778
+ `layered`, with the current value checked. Children do not offer an action to
779
+ detach the parent layout. Items added from empty canvas space are centered at
780
+ the context-menu point.
781
+ - Open the same menu from a focused diagram/tree item with `Shift+F10` or the
782
+ Context Menu key. Navigate with arrows, open submenus with `ArrowRight`,
783
+ return with `ArrowLeft`, execute with Enter/Space, and close with Escape.
784
+ - A typed inspector edits geometry, layout, style, icon, ports, routing,
785
+ polyline points, canvas metadata, and other editable v1 fields.
786
+ - Add a standalone **Image** from the toolbar or context menu. Node inspector
787
+ **Select image from assets/** uses the same picker. It searches
788
+ Markdown-adjacent and workspace-root `assets/` in that order. Import SVG,
789
+ PNG, WebP, JPG, or JPEG up to 10 MB into workspace-root `assets/`; collisions
790
+ are numbered as `name-2.ext`.
791
+ - The picker selects/imports only; it does not rename, move, or delete shared
792
+ assets. Removing or undoing an image does not delete its file.
793
+ - Changes remain in an in-memory draft until explicit **Save** or `save`.
794
+ - External file changes produce a conflict. Call `reload` with
795
+ `{ "discard": true }` to discard the draft explicitly.
796
+ - Saving reloads any MarkdStage canvas showing the same Markdown while
797
+ preserving page and theme.
798
+
799
+ Only source-backed decks imported through 📂 enable **Advanced editing**.
800
+ Agents can open the editor directly:
801
+
802
+ ```json
803
+ {
804
+ "canvasId": "architecture-editor",
805
+ "instanceId": "architecture-editor-main",
806
+ "input": {
807
+ "sourcePath": "slides.md",
808
+ "blockIndex": 0,
809
+ "theme": "dark"
810
+ }
811
+ }
812
+ ```
813
+
814
+ `sourcePath` is a workspace-relative `.md` / `.markdown` path. `blockIndex` is
815
+ zero-based across all Architecture blocks. Paths outside the workspace,
816
+ symlinks, oversized files, missing blocks, and invalid DSL fail closed.
817
+
818
+ #### Save results are always visible
819
+
820
+ Placement mode writes each operation immediately. Imported decks target source
821
+ Markdown; other decks target canvas state. The toolbar always reports success
822
+ or failure through `data-architecture-save-state` values `saving`, `saved`, and
823
+ `failed`.
824
+
825
+ Because the visual diagram already moved, a hidden save failure could cause a
826
+ user to lose an edit they believed was stored. A failure therefore remains
827
+ prominent until a later save succeeds. Messages distinguish 409 after edit mode
828
+ was disabled, 404 after deck replacement, and network failure; the result is
829
+ never console-only.
830
+
831
+ #### Layout-managed nodes do not move
832
+
833
+ For a child of a group with `layout`, the layout engine recalculates position
834
+ and silently ignores explicit `x` / `y`. Approximately 68% of nodes in this
835
+ repository's real data are layout-managed.
836
+
837
+ Placement mode therefore refuses to move such a node and announces which group
838
+ controls it. Press `L` to detach layout. This removes group `layout` and writes
839
+ calculated `x`, `y`, `width`, and `height` to all children without changing
840
+ appearance. Children can then move freely. Undo restores the operation, but the
841
+ serialized conversion is otherwise irreversible; restoring layout after save
842
+ requires authoring it again.
843
+
844
+ #### Presenter and print never include edit UI
845
+
846
+ For `?present=1` and `?print=1`, edit UI is not constructed in the DOM. When
847
+ mode is disabled, the server also rejects `POST /edit` with
848
+ `409 edit_mode_disabled`. `npm run test:editing` locks both behaviors.
849
+
850
+ #### Known editing tradeoff
851
+
852
+ Write-back formats fence JSON with `JSON.stringify(..., null, 2)`, normalizing
853
+ indentation and wrapping inside the fence without changing values. Prose, front
854
+ matter, and CRLF/LF line endings outside the fence remain byte-for-byte
855
+ unchanged; saving a CRLF file does not convert the complete file to LF.
856
+
857
+ Use Mermaid for automatic layout, sequence diagrams, and class diagrams. Use
858
+ Architecture DSL for presentation-specific coordinates, dimensions, containers,
859
+ and overlap. Built-in icons use only inline SVG path data. Connector crossing
860
+ minimization is heuristic and does not guarantee the global optimum. Automatic
861
+ node placement is limited to `layered`; no force-directed graph layout exists.
862
+
863
+ ## Content size
864
+
865
+ Source Markdown can specify a page size with a leading comment:
866
+
867
+ ```markdown
868
+ <!-- slide-size: large -->
869
+
870
+ ## Emphasized slide
871
+ ```
872
+
873
+ Direct slide fragments use front matter:
874
+
875
+ ```markdown
876
+ ---
877
+ size: xlarge
878
+ ---
879
+ ## Emphasized slide
880
+ ```
881
+
882
+ Accepted values are `auto`, `normal`, `large`, and `xlarge`. Front matter wins
883
+ over the comment. `title`, `section`, and `backcover` layouts retain their
884
+ special layout and are never auto-enlarged.
885
+
886
+ ## Surface Pen controls
887
+
888
+ 1. Pair Surface Pen with Windows over Bluetooth.
889
+ 2. Open the MarkdStage canvas.
890
+ 3. Use the tail button:
891
+
892
+ | Gesture | Windows shortcut | Action |
893
+ | --- | --- | --- |
894
+ | Single press | `Win+F20` | Next slide |
895
+ | Long press | `Win+F18` | Previous slide |
896
+
897
+ `Win+F19` double press and pen connection/removal/docking do not launch
898
+ MarkdStage. Start the presenter through ⛶ or explicit `open_presenter`.
899
+
900
+ The Windows setting that lets apps override shortcut-button behavior may remain
901
+ enabled. This implementation listens directly for Windows pen shortcuts rather
902
+ than using `PenButtonListener`. The hook suppresses `Win+F20` and `Win+F18`, so
903
+ Windows Ink defaults do not run simultaneously. `Win+F19` remains with Windows.
904
+ Canvas buttons and keyboard continue to work when pen input is unavailable.
905
+
906
+ ## Actions
907
+
908
+ Start with `open_canvas` (`canvasId: "MarkdStage"`) and complete-deck input:
909
+ `{ slides: string[], index?: number, theme?: "dark" | "light" | "microsoft" |
910
+ "custom", sourceName?: string }`. Pass the source Markdown filename in
911
+ `sourceName`; the printer saves `<source-name-without-extension>.pdf`. This is
912
+ metadata for resolution and output naming only and never reads or watches the
913
+ file. The open handler applies the deck before returning its URL. Omit input
914
+ when only refocusing an existing canvas. Any non-empty input without `slides`
915
+ fails with `invalid_input`; pass the complete `slides` array or call
916
+ `load_deck` to replace the registered snapshot.
917
+
918
+ | Action | Input and behavior |
919
+ | --- | --- |
920
+ | `load_deck` | `{ slides: string[], index?: number, theme?: "dark" | "light" | "microsoft" | "custom", sourceName?: string }`. Replace/reload the deck for mid-presentation content or theme changes. `index` defaults to `0`; theme defaults to `dark`. `sourceName` is metadata only and never reads or watches Markdown. Appends one back cover without duplication. Returns `{ ok, version, index, total, theme, validationFeedback? }`. Missing front matter or Architecture errors do not prevent display; remediation is returned in `validationFeedback` and logged for open. |
921
+ | `goto_slide` | `{ index: number }`. Select a clamped zero-based index. Intended for explicit chat requests, not normal navigation. Returns `{ ok, changed, version, index, total }`. |
922
+ | `show_slide` | `{ markdown: string }`. Temporarily replace the current slide. Supports front matter keys `deck`, `kicker`, `page`, `total`, `title`, `layout`, `size`, and `theme`. Omitted theme inherits the deck theme. The override is included in output snapshots until navigation or deck replacement resumes the registered deck. |
923
+ | `get_architecture_errors` | `{ index?: number }`. Validate the complete deck or one zero-based slide, including temporary content. Returns `{ ok, scope, index?, page?, total, errorCount, errors }`; errors contain `{ slideIndex, page, blockIndex, architecture, code, message }`. No deck and out-of-range indexes are errors. |
924
+ | `open_presenter` | No input. Start one synchronized movable/resizable 1280×720 Chromium app-mode window. Use `F11` on Windows for full screen. Returns `{ ok, started, alreadyRunning, browser?, pid? }`. |
925
+ | `close_presenter` | No input. Stop presenter and remove its temporary profile. Returns `{ ok, stopped }`. |
926
+ | `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. |
927
+ | `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. |
928
+ | `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. |
929
+ | `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 }`. |
930
+ | `reset` | No input. Clear deck/slide state, disable editing, and return to the waiting view. |
931
+
932
+ `architecture-editor` is a separate canvas. Its open input is
933
+ `{ sourcePath: string, blockIndex: number, theme?: "dark" | "light" |
934
+ "microsoft" }`. Its actions are `save` (no input) and `reload`
935
+ (`{ discard?: boolean }`).
936
+
937
+ ### Internal HTTP endpoints for the renderer
938
+
939
+ | Endpoint | Purpose |
940
+ | --- | --- |
941
+ | `GET /state` | Return current `markdown`, `index`, `total`, `theme`, `mode`, `architectureEdit`, source/watch state, `version`, and `deckVersion`. |
942
+ | `GET /deck` | Return all `slides` and `deckVersion`; the ☰ list fetches only when the version changes. |
943
+ | `GET /export-data` | Return the token-bound deck snapshot to print, inspection, or capture mode. |
944
+ | `POST /export-status` | Let print/capture mode report rendering status and fixed-layout diagnostics to PDF export, `inspect_layout`, or `capture_slides`. |
945
+ | `POST /navigate` | Accept absolute `{ index }` or relative `{ delta }`, update position, and notify all clients through SSE. |
946
+ | `POST /present` | Start the presenter from ⛶; accepts same-origin POST only. |
947
+ | `POST /export` | Start source-named PDF export from the printer icon; accepts same-origin POST only. |
948
+ | `POST /edit` | Write an edited fence with `{ index, block, source }`; returns `409 edit_mode_disabled` when editing is off. |
949
+ | `POST /edit-mode` | Set `{ enabled }`, including for `?architectureEdit=1`; same-origin POST only. |
950
+ | `POST /architecture-editor/open` | Convert source-backed slide/block indexes to a file-wide block index and open Architecture Editor. |
951
+ | `GET /events` | SSE nudge for low-latency `version` changes. |
952
+ | `GET /markdown-files` | Return bounded workspace-relative `*.md` / `*.markdown` paths for 📂, excluding `.git`, `node_modules`, and dot-prefixed entries. |
953
+ | `POST /import` | Load and split `{ path, sourceMode?: "snapshot" | "live" }`; reject paths outside workspace, wrong extensions, and oversized files. Same-origin POST only. |
954
+ | `POST /source-mode` | Switch a source-backed deck with `{ mode: "snapshot" | "live" }`; entering live mode loads the latest source. |
955
+
956
+ ## File layout
957
+
958
+ ```text
959
+ .github/extensions/markdstage/
960
+ extension.mjs # Canvas declaration, loopback server, and actions
961
+ architecture-canvas.mjs # Architecture Editor state, validation, save, conflicts
962
+ architecture-editor/
963
+ index.html # Full diagram-editor canvas shell
964
+ editor.css # Workspace, tree, and inspector styles
965
+ editor.js # Diagram commands, draft, and explicit-save UI
966
+ copilot-extension.json # Manifest for Gist sharing
967
+ markdown-deck.mjs # Raw Markdown splitting for 📂 import
968
+ scripts/
969
+ markdown-blocks.mjs # Scan and replace architecture fences
970
+ markdown-files.mjs # Scan workspace Markdown
971
+ markdown-watcher.mjs # Watch and debounce source-backed Markdown
972
+ windows/
973
+ pen-button-listener.ps1 # Relay Surface Pen Win+F20 / Win+F18 to Node
974
+ renderer/
975
+ index.html # Iframe shell, toolbar, slide/import overlays
976
+ slides.css # Built-in dark/light/microsoft themes and navigation UI
977
+ renderer.js # Front matter, marked, Mermaid, Architecture, SSE, controls
978
+ architecture.mjs # Validate JSON DSL and create safe SVG DOM
979
+ architecture-edit.mjs # DOM-independent move/detach/Undo/Redo/serialization
980
+ architecture-editor.mjs # Placement UI and Advanced editing entry point
981
+ architecture-document.mjs # Full-editor command/session API
982
+ schema/
983
+ architecture-v1.schema.json # Architecture DSL v1 JSON Schema (draft 2020-12)
984
+ README.md # Schema use, versioning, and migration policy
985
+ examples/ # Samples with relative $schema references
986
+ vendor/
987
+ marked.min.js # Markdown renderer
988
+ purify.min.js # DOMPurify HTML sanitizer
989
+ highlight.min.js # Code syntax highlighting
990
+ highlight.LICENSE # highlight.js MIT license
991
+ mermaid.min.js.part-* # Split Mermaid bundle for the 1 MB file limit
992
+ ```
993
+
994
+ ## Third-party licenses
995
+
996
+ `vendor/` contains the following open-source software under their respective
997
+ licenses. See [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md) for copyright
998
+ and license notices.
999
+
1000
+ - **marked** — MIT License © 2011-2024 Christopher Jeffrey and contributors —
1001
+ https://github.com/markedjs/marked
1002
+ - **DOMPurify** — Apache-2.0 / MPL-2.0 © Cure53 and contributors —
1003
+ https://github.com/cure53/DOMPurify
1004
+ - **highlight.js** — MIT License © 2006 Ivan Sagalaev —
1005
+ https://github.com/highlightjs/highlight.js
1006
+ - **Mermaid** — MIT License © 2014-2024 Knut Sveidqvist and contributors —
1007
+ https://github.com/mermaid-js/mermaid
1008
+
1009
+ `mermaid.min.js` is approximately 3 MB and is split into
1010
+ `mermaid.min.js.part-*` to satisfy installer single-file limits. The extension
1011
+ reassembles the parts in order when serving HTTP, so Gist and repository
1012
+ installs retain Mermaid support. Source/chunk SHA-256 values are tracked in
1013
+ `vendor/vendor-assets.lock.json`; regenerate and verify them with
1014
+ `scripts/vendor-assets.mjs`.