@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.
- package/README.md +90 -0
- package/bin/markdstage.mjs +12 -0
- package/package.json +45 -0
- package/shared/README.md +1014 -0
- package/shared/THIRD-PARTY-NOTICES.md +19 -0
- package/shared/deck-state.mjs +105 -0
- package/shared/docs/custom-theme-authoring.md +208 -0
- package/shared/markdown-deck.mjs +220 -0
- package/shared/markdstage-guide.mjs +276 -0
- package/shared/presenter-window.mjs +17 -0
- package/shared/renderer/architecture-document.mjs +596 -0
- package/shared/renderer/architecture-edit.mjs +298 -0
- package/shared/renderer/architecture-editor.mjs +449 -0
- package/shared/renderer/architecture.mjs +4033 -0
- package/shared/renderer/import-path.mjs +11 -0
- package/shared/renderer/index.html +106 -0
- package/shared/renderer/renderer.js +2082 -0
- package/shared/renderer/slides.css +614 -0
- package/shared/renderer/speaker-notes.mjs +106 -0
- package/shared/renderer/theme.mjs +205 -0
- package/shared/runtime/browser.mjs +539 -0
- package/shared/runtime/custom-theme.mjs +135 -0
- package/shared/runtime/deck-session.mjs +188 -0
- package/shared/runtime/errors.mjs +17 -0
- package/shared/runtime/output-paths.mjs +159 -0
- package/shared/runtime/output.mjs +385 -0
- package/shared/runtime/presentation-server.mjs +505 -0
- package/shared/runtime/static-files.mjs +70 -0
- package/shared/schema/README.md +228 -0
- package/shared/schema/architecture-v1.schema.json +664 -0
- package/shared/schema/examples/web-app.architecture.json +119 -0
- package/shared/schema/theme-metadata-v1.schema.json +75 -0
- package/shared/schema/theme-v1.json +84 -0
- package/shared/scripts/architecture-assets.mjs +226 -0
- package/shared/scripts/asset-paths.mjs +92 -0
- package/shared/scripts/atomic-markdown-replace.mjs +46 -0
- package/shared/scripts/markdown-blocks.mjs +182 -0
- package/shared/scripts/markdown-files.mjs +63 -0
- package/shared/scripts/markdown-save-coordinator.mjs +18 -0
- package/shared/scripts/markdown-watcher.mjs +80 -0
- package/shared/scripts/theme-paths.mjs +108 -0
- package/shared/scripts/vendor-assets.mjs +132 -0
- package/shared/scripts/workspace-root.mjs +32 -0
- package/shared/vendor/highlight.LICENSE +29 -0
- package/shared/vendor/highlight.min.js +1244 -0
- package/shared/vendor/marked.min.js +6 -0
- package/shared/vendor/mermaid.min.js.part-0001 +268 -0
- package/shared/vendor/mermaid.min.js.part-0002 +304 -0
- package/shared/vendor/mermaid.min.js.part-0003 +324 -0
- package/shared/vendor/mermaid.min.js.part-0004 +374 -0
- package/shared/vendor/mermaid.min.js.part-0005 +564 -0
- package/shared/vendor/mermaid.min.js.part-0006 +1308 -0
- package/shared/vendor/mermaid.min.js.part-0007 +269 -0
- package/shared/vendor/purify.min.js +3 -0
- package/shared/vendor/vendor-assets.lock.json +60 -0
- package/src/cli.mjs +347 -0
- package/src/commands/capture.mjs +23 -0
- package/src/commands/export.mjs +18 -0
- package/src/commands/guide.mjs +23 -0
- package/src/commands/inspect.mjs +35 -0
- package/src/commands/present.mjs +91 -0
- package/src/commands/skill.mjs +114 -0
- package/src/commands/validate.mjs +79 -0
- package/src/deck.mjs +63 -0
- package/src/exit.mjs +58 -0
- package/src/runtime.mjs +77 -0
- package/src/skills.mjs +155 -0
package/shared/README.md
ADDED
|
@@ -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 `` 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`.
|