@markdstage/markdstage 2.6.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/package.json +1 -1
- package/shared/README.md +41 -14
- package/shared/markdstage-guide.mjs +8 -2
- package/shared/renderer/renderer.js +37 -7
- package/shared/renderer/slides.css +14 -0
- package/shared/runtime/browser.mjs +29 -104
- package/shared/runtime/output.mjs +5 -5
- package/src/cli.mjs +13 -13
- package/src/commands/present.mjs +4 -2
- package/src/skills.mjs +6 -6
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ exported PDF, or hybrid editable PowerPoint deck.
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```console
|
|
19
|
-
npx @markdstage/markdstage presentation slides.md
|
|
20
19
|
npx @markdstage/markdstage present slides.md
|
|
20
|
+
npx @markdstage/markdstage preview slides.md
|
|
21
21
|
npm install --global @markdstage/markdstage
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -31,8 +31,8 @@ npm install --global .\markdstage-markdstage-<version>.tgz
|
|
|
31
31
|
## Commands
|
|
32
32
|
|
|
33
33
|
```console
|
|
34
|
-
markdstage
|
|
35
|
-
markdstage
|
|
34
|
+
markdstage present slides.md
|
|
35
|
+
markdstage preview slides.md --watch
|
|
36
36
|
markdstage validate slides.md --json
|
|
37
37
|
markdstage inspect slides.md --json
|
|
38
38
|
markdstage capture slides.md --pages 2,4
|
|
@@ -45,8 +45,8 @@ markdstage skill install --target claude
|
|
|
45
45
|
|
|
46
46
|
| Command | Description |
|
|
47
47
|
| --- | --- |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
48
|
+
| `present` | Opens the presenter view with the current slide, next-slide preview, speaker notes, and navigation. Select **Start presentation** to open the synchronized audience-facing window; select **End presentation** to close it. `--watch` reloads on save, and `--no-open` serves the presenter URL without launching a browser. |
|
|
49
|
+
| `preview` | Serves the deck on loopback and opens the MarkdStage preview: navigation, presenter view, next-slide preview, speaker notes, overview, custom themes, Mermaid, Architecture DSL, and local assets. `--watch` reloads on save while preserving the current slide, keeps the last valid deck when a save is broken, and enables Architecture editing. Without `--watch`, the source is read-only. `--no-open` serves the deck only. |
|
|
50
50
|
| `validate` | Checks deck structure, Architecture DSL blocks, themes, and theme paths. |
|
|
51
51
|
| `inspect` | Reports the same compact 1280x720 clipping diagnostics as the canvas `inspect_layout` action. `--slide <n>` limits it to one page, `--all` includes slides that fit, `--fail-on-issues` exits with code 5. |
|
|
52
52
|
| `capture` | Writes 1280x720 PNG files. Without `--pages` only the slides reported as clipped are captured. |
|
|
@@ -60,7 +60,7 @@ Global options: `--workspace <dir>`, `--theme <name>`, `--theme-file <path>`,
|
|
|
60
60
|
|
|
61
61
|
## Architecture editing
|
|
62
62
|
|
|
63
|
-
Run `markdstage
|
|
63
|
+
Run `markdstage preview slides.md --watch` for the live authoring workflow. The
|
|
64
64
|
browser starts in viewing mode. Select the pencil control to move Architecture
|
|
65
65
|
elements; those placement changes are saved atomically to the matching
|
|
66
66
|
`architecture` fence. Select **Advanced edit** to add, update, duplicate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdstage/markdstage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "runceel",
|
package/shared/README.md
CHANGED
|
@@ -207,10 +207,31 @@ When the extension detects a presentation-related prompt, it adds a short hint
|
|
|
207
207
|
to use `markdstage_guide` at most once per session. No hint is added after the
|
|
208
208
|
tool has already been called, and session-end cleanup removes the state.
|
|
209
209
|
|
|
210
|
-
###
|
|
210
|
+
### Markdown file syntax
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
A Markdown file represents the complete deck. At the top level, a line containing
|
|
213
|
+
only `---` after a blank line separates slides. A leading `---` block is file
|
|
214
|
+
front matter, and `---` inside a fenced code block is not a separator.
|
|
215
|
+
|
|
216
|
+
Use only the top-level `---` syntax for slide boundaries. Custom markers such as
|
|
217
|
+
`<!-- slide -->` are not slide separators and must not be used.
|
|
218
|
+
|
|
219
|
+
Do not place a top-level HTML comment immediately before a slide separator. End
|
|
220
|
+
the comment, leave a blank line, and then write `---`; without the blank line,
|
|
221
|
+
the separator may not be recognized as a slide boundary.
|
|
222
|
+
|
|
223
|
+
To have MarkdStage read and split a workspace Markdown file, use **More controls >
|
|
224
|
+
Open Markdown**. This file-loading workflow is separate from the Canvas API.
|
|
225
|
+
|
|
226
|
+
### Canvas API `slides` array
|
|
227
|
+
|
|
228
|
+
For canvas `open` and `load_deck`, each element of the `slides` array is exactly
|
|
229
|
+
one slide. The caller must read, split, or generate the deck before invoking the
|
|
230
|
+
Canvas API. Do not pass a complete multi-slide Markdown file as one array element
|
|
231
|
+
and expect its `---` lines to be split.
|
|
232
|
+
|
|
233
|
+
Each array element is one Markdown string. It may start with front matter
|
|
234
|
+
delimited by `---`, followed by GFM-compatible content.
|
|
214
235
|
|
|
215
236
|
| Front matter | Purpose |
|
|
216
237
|
| --- | --- |
|
|
@@ -245,14 +266,6 @@ First explain the **prerequisites**.
|
|
|
245
266
|
-->
|
|
246
267
|
```
|
|
247
268
|
|
|
248
|
-
Write local images as `` and pass the source
|
|
249
|
-
Markdown's workspace-relative path as `sourceName`. Lookup tries adjacent
|
|
250
|
-
`assets/` before workspace-root `assets/`. Architecture `icon` and `image.src`
|
|
251
|
-
use `assets/foo.svg` without a leading slash and follow the same lookup order.
|
|
252
|
-
`sourceName` supplies this resolution base only; it does not read or watch the
|
|
253
|
-
Markdown file.
|
|
254
|
-
Specifically, lookup checks `assets/` beside the Markdown before `assets/` at the workspace root.
|
|
255
|
-
|
|
256
269
|
On a standard slide, the first H1/H2 is fixed in the top title area, so its
|
|
257
270
|
position does not move with body length. Later headings remain in the body.
|
|
258
271
|
Specialized `title`, `section`, and `backcover` layouts retain their own
|
|
@@ -287,6 +300,20 @@ layout: section
|
|
|
287
300
|
## Key GitHub Copilot features
|
|
288
301
|
```
|
|
289
302
|
|
|
303
|
+
### `sourceName` role
|
|
304
|
+
|
|
305
|
+
`sourceName` is workspace-relative metadata used to resolve adjacent themes and
|
|
306
|
+
images and to derive output filenames. It does not read, parse, split, or watch
|
|
307
|
+
Markdown content. Passing `sourceName` does not load a deck; non-empty canvas
|
|
308
|
+
input must still include the complete `slides` array.
|
|
309
|
+
|
|
310
|
+
To have MarkdStage load a Markdown file, use **More controls > Open Markdown**.
|
|
311
|
+
|
|
312
|
+
Write local images as ``. With `sourceName`,
|
|
313
|
+
lookup tries `assets/` beside the Markdown before workspace-root `assets/`.
|
|
314
|
+
Architecture `icon` and `image.src` use `assets/foo.svg` without a leading slash
|
|
315
|
+
and follow the same lookup order.
|
|
316
|
+
|
|
290
317
|
### Choosing a theme
|
|
291
318
|
|
|
292
319
|
Set the deck-wide `theme`; use `dark` when omitted.
|
|
@@ -745,8 +772,8 @@ Architecture diagrams can be moved directly over the rendered result. For decks
|
|
|
745
772
|
imported through **More controls > Open Markdown**, edits write back to the source `architecture` fence and
|
|
746
773
|
survive re-import. Decks supplied directly through open / `load_deck` cannot be
|
|
747
774
|
reversibly mapped to a source file, so they save only to canvas deck state.
|
|
748
|
-
In the standalone CLI, `markdstage
|
|
749
|
-
source-backed editing workflow; `
|
|
775
|
+
In the standalone CLI, `markdstage preview slides.md --watch` enables the same
|
|
776
|
+
source-backed editing workflow; `preview` without `--watch` is read-only.
|
|
750
777
|
|
|
751
778
|
**Placement editing is a stable part of Architecture DSL v1.**
|
|
752
779
|
|
|
@@ -837,7 +864,7 @@ blocks.
|
|
|
837
864
|
For a source-backed deck imported through **More controls > Open Markdown**,
|
|
838
865
|
**Shape editing** opens this editor directly. If the slide contains multiple
|
|
839
866
|
Architecture blocks, select one from the accessible picker first. CLI
|
|
840
|
-
`
|
|
867
|
+
`preview --watch` opens the same editor in a browser window. Agents can open
|
|
841
868
|
the editor canvas directly:
|
|
842
869
|
|
|
843
870
|
```json
|
|
@@ -110,12 +110,18 @@ export async function readGuide(topic = "overview") {
|
|
|
110
110
|
section(readme, "## How it works"),
|
|
111
111
|
"",
|
|
112
112
|
"Users can load workspace Markdown directly with **More controls > Open Markdown** (deterministic splitting without AI; natural-language summarization remains the AI's responsibility). The workspace root is the Git repository root when available, otherwise the folder opened for the current session.",
|
|
113
|
-
"Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage
|
|
113
|
+
"Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage preview slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `preview` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
|
|
114
114
|
"",
|
|
115
115
|
"For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
|
|
116
116
|
].join("\n");
|
|
117
117
|
case "slide-format":
|
|
118
|
-
return
|
|
118
|
+
return [
|
|
119
|
+
section(readme, "### Markdown file syntax"),
|
|
120
|
+
"",
|
|
121
|
+
section(readme, "### Canvas API `slides` array"),
|
|
122
|
+
"",
|
|
123
|
+
section(readme, "### `sourceName` role"),
|
|
124
|
+
].join("\n");
|
|
119
125
|
case "themes":
|
|
120
126
|
return section(readme, "### Choosing a theme");
|
|
121
127
|
case "custom-themes":
|
|
@@ -2148,6 +2148,7 @@ let currentVersion = -1;
|
|
|
2148
2148
|
let knownDeckVersion = -1;
|
|
2149
2149
|
let deckSlides = [];
|
|
2150
2150
|
let deckTitles = [];
|
|
2151
|
+
let deckLayouts = [];
|
|
2151
2152
|
let navIndex = 0;
|
|
2152
2153
|
let navTotal = 0;
|
|
2153
2154
|
let navMode = "deck";
|
|
@@ -2189,6 +2190,11 @@ function deriveTitle(md) {
|
|
|
2189
2190
|
return fallback ? trimTitle(fallback) : "(Untitled)";
|
|
2190
2191
|
}
|
|
2191
2192
|
|
|
2193
|
+
function deriveLayout(md) {
|
|
2194
|
+
const { meta } = splitFrontMatter(typeof md === "string" ? md : "");
|
|
2195
|
+
return typeof meta.layout === "string" ? meta.layout.trim().toLowerCase() : "";
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2192
2198
|
function trimTitle(text) {
|
|
2193
2199
|
const stripped = text
|
|
2194
2200
|
.replace(/[*_`>#~]/g, "")
|
|
@@ -2205,6 +2211,7 @@ async function fetchDeck() {
|
|
|
2205
2211
|
if (Array.isArray(data.slides)) {
|
|
2206
2212
|
deckSlides = data.slides;
|
|
2207
2213
|
deckTitles = deckSlides.map(deriveTitle);
|
|
2214
|
+
deckLayouts = deckSlides.map(deriveLayout);
|
|
2208
2215
|
}
|
|
2209
2216
|
if (typeof data.deckVersion === "number") knownDeckVersion = data.deckVersion;
|
|
2210
2217
|
buildOverview();
|
|
@@ -3032,24 +3039,51 @@ function buildOverview() {
|
|
|
3032
3039
|
const list = document.getElementById("overviewList");
|
|
3033
3040
|
if (!list) return;
|
|
3034
3041
|
list.replaceChildren();
|
|
3042
|
+
let insideSection = false;
|
|
3035
3043
|
deckTitles.forEach((title, i) => {
|
|
3044
|
+
const layout = deckLayouts[i] || "";
|
|
3045
|
+
if (layout === "title" || layout === "backcover") insideSection = false;
|
|
3046
|
+
const sectionChild =
|
|
3047
|
+
insideSection && layout !== "title" && layout !== "section" && layout !== "backcover";
|
|
3048
|
+
|
|
3036
3049
|
const li = document.createElement("li");
|
|
3037
3050
|
li.className = "overview-item";
|
|
3051
|
+
if (layout === "title" || layout === "section") {
|
|
3052
|
+
li.classList.add(`overview-item-${layout}`);
|
|
3053
|
+
}
|
|
3054
|
+
if (sectionChild) li.classList.add("overview-item-section-child");
|
|
3038
3055
|
li.dataset.index = String(i);
|
|
3056
|
+
if (layout) li.dataset.layout = layout;
|
|
3057
|
+
|
|
3039
3058
|
const btn = document.createElement("button");
|
|
3040
3059
|
btn.type = "button";
|
|
3041
3060
|
btn.className = "overview-link";
|
|
3061
|
+
btn.setAttribute(
|
|
3062
|
+
"aria-label",
|
|
3063
|
+
`${i + 1} ${title}${layout === "title" || layout === "section" ? `, ${layout} slide` : ""}`,
|
|
3064
|
+
);
|
|
3065
|
+
|
|
3042
3066
|
const num = document.createElement("span");
|
|
3043
3067
|
num.className = "overview-num";
|
|
3044
3068
|
num.textContent = String(i + 1);
|
|
3045
3069
|
const label = document.createElement("span");
|
|
3046
3070
|
label.className = "overview-label";
|
|
3047
3071
|
label.textContent = title;
|
|
3072
|
+
|
|
3048
3073
|
btn.appendChild(num);
|
|
3074
|
+
if (layout === "title" || layout === "section") {
|
|
3075
|
+
const kind = document.createElement("span");
|
|
3076
|
+
kind.className = "overview-kind";
|
|
3077
|
+
kind.textContent = layout === "title" ? "Title" : "Section";
|
|
3078
|
+
kind.setAttribute("aria-hidden", "true");
|
|
3079
|
+
btn.appendChild(kind);
|
|
3080
|
+
}
|
|
3049
3081
|
btn.appendChild(label);
|
|
3050
3082
|
btn.addEventListener("click", () => goToIndex(i));
|
|
3051
3083
|
li.appendChild(btn);
|
|
3052
3084
|
list.appendChild(li);
|
|
3085
|
+
|
|
3086
|
+
if (layout === "section") insideSection = true;
|
|
3053
3087
|
});
|
|
3054
3088
|
highlightOverview();
|
|
3055
3089
|
}
|
|
@@ -3478,13 +3512,9 @@ function init() {
|
|
|
3478
3512
|
return;
|
|
3479
3513
|
}
|
|
3480
3514
|
if (params.get("print") === "1") {
|
|
3481
|
-
// Print mode
|
|
3482
|
-
//
|
|
3483
|
-
//
|
|
3484
|
-
// This early return is also **the primary fix for the #12 hang**. Only print
|
|
3485
|
-
// mode avoids connectEvents() (an unclosed SSE) and the two-second setInterval,
|
|
3486
|
-
// allowing the page to become idle and --print-to-pdf to complete.
|
|
3487
|
-
// Removing the return makes printing hang forever.
|
|
3515
|
+
// Print mode renders one immutable output snapshot and never reaches editing,
|
|
3516
|
+
// SSE, or polling branches. The output runtime waits for its ready report
|
|
3517
|
+
// before asking Chromium to create the PDF.
|
|
3488
3518
|
initPrint(params).catch(reportPrintBootstrapFailure);
|
|
3489
3519
|
return;
|
|
3490
3520
|
}
|
|
@@ -547,8 +547,22 @@ body.presenter-view-mode .layout-warning{display:none;}
|
|
|
547
547
|
align-items:center;justify-content:center;border-radius:6px;
|
|
548
548
|
font-size:.82em;font-weight:700;font-variant-numeric:tabular-nums;
|
|
549
549
|
color:var(--muted);background:var(--code);border:1px solid var(--border);}
|
|
550
|
+
.overview-kind{flex:0 0 auto;padding:.18em .48em;border-radius:999px;
|
|
551
|
+
color:var(--accent-strong);background:var(--surface);border:1px solid var(--accent);
|
|
552
|
+
font-size:.66em;font-weight:750;letter-spacing:.06em;text-transform:uppercase;}
|
|
550
553
|
.overview-label{flex:1 1 auto;font-size:.96em;line-height:1.35;
|
|
551
554
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
555
|
+
.overview-item-title{margin-bottom:.45em;}
|
|
556
|
+
.overview-item-title .overview-link{background:color-mix(in srgb,var(--accent-soft) 74%,transparent);
|
|
557
|
+
border:1px solid color-mix(in srgb,var(--accent) 48%,var(--border));}
|
|
558
|
+
.overview-item-title .overview-label{color:var(--fg);font-weight:700;}
|
|
559
|
+
.overview-item-section{margin-top:.65em;padding-top:.55em;border-top:1px solid var(--border);}
|
|
560
|
+
.overview-item-section .overview-link{padding-block:.65em;background:var(--accent-soft);
|
|
561
|
+
border-left:4px solid var(--accent);}
|
|
562
|
+
.overview-item-section .overview-num{color:var(--fg);border-color:var(--accent);}
|
|
563
|
+
.overview-item-section .overview-label{color:var(--fg);font-weight:700;}
|
|
564
|
+
.overview-item-section-child .overview-link{width:calc(100% - 1.2em);margin-left:1.2em;
|
|
565
|
+
border-left:1px solid color-mix(in srgb,var(--accent) 42%,var(--border));border-radius:0 8px 8px 0;}
|
|
552
566
|
.overview-item.current .overview-link{background:var(--accent-soft);}
|
|
553
567
|
.overview-item.current .overview-num{color:var(--fg);background:var(--accent);
|
|
554
568
|
border-color:var(--accent);}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// because the Extension is distributed as a folder ZIP.
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { readFile, open, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { readFile, open, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
12
12
|
|
|
@@ -173,84 +173,6 @@ export function delay(milliseconds) {
|
|
|
173
173
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
/**
|
|
177
|
-
* Run a headless browser once with `--print-to-pdf`.
|
|
178
|
-
*
|
|
179
|
-
* ⚠️ **`pageUrl` must include `?print=1&token=...` (#12).**
|
|
180
|
-
*
|
|
181
|
-
* `--print-to-pdf` completes only when the page becomes idle. In renderer `init()`,
|
|
182
|
-
* only print mode returns early. Normal and presenter views keep an unclosed SSE
|
|
183
|
-
* (`new EventSource("./events")`) and a two-second `setInterval` running.
|
|
184
|
-
* Passing a URL without `?print=1` therefore means **the browser never exits**.
|
|
185
|
-
*
|
|
186
|
-
* Observed results, using Chrome arguments byte-for-byte identical to this function:
|
|
187
|
-
*
|
|
188
|
-
* | URL | Result |
|
|
189
|
-
* | -------------------------- | --------------------------------------- |
|
|
190
|
-
* | `/?print=1&token=<valid>` | exit 0 @ 2.4s (valid PDF) |
|
|
191
|
-
* | `/?print=1&token=` (empty) | exit 0 @ 1.9s (blank; renderer reports failure) |
|
|
192
|
-
* | `/` (normal view) | **HANG** (still running after 120 seconds) |
|
|
193
|
-
* | `/?present=1` | **HANG** |
|
|
194
|
-
* | `/nope-404` (no renderer) | exit 0 @ 3.0s |
|
|
195
|
-
*
|
|
196
|
-
* ⚠️ **`--virtual-time-budget` is effectively ignored by `--headless=new`.**
|
|
197
|
-
* The `--virtual-time-budget=12000` argument below does not stop this hang.
|
|
198
|
-
* Adding `--timeout=8000` is also ineffective, as verified empirically.
|
|
199
|
-
* The argument is harmless and remains in place, but **do not treat it as a
|
|
200
|
-
* wall-clock timeout**. Only Node's `PDF_RENDER_TIMEOUT_MS` and
|
|
201
|
-
* `terminateProcessTree` enforce a limit, and failure may take up to 60 seconds.
|
|
202
|
-
*/
|
|
203
|
-
export async function runHeadlessBrowser(browser, args, failureLabel) {
|
|
204
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
205
|
-
const child = spawn(browser, args, {
|
|
206
|
-
detached: process.platform !== "win32",
|
|
207
|
-
windowsHide: true,
|
|
208
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
209
|
-
});
|
|
210
|
-
let diagnostics = "";
|
|
211
|
-
let settled = false;
|
|
212
|
-
let timedOut = false;
|
|
213
|
-
const appendDiagnostics = (chunk) => {
|
|
214
|
-
diagnostics = `${diagnostics}${chunk.toString()}`.slice(-12_000);
|
|
215
|
-
};
|
|
216
|
-
child.stdout.on("data", appendDiagnostics);
|
|
217
|
-
child.stderr.on("data", appendDiagnostics);
|
|
218
|
-
|
|
219
|
-
const settle = (error) => {
|
|
220
|
-
if (settled) return;
|
|
221
|
-
settled = true;
|
|
222
|
-
clearTimeout(timer);
|
|
223
|
-
if (error) rejectPromise(error);
|
|
224
|
-
else resolvePromise();
|
|
225
|
-
};
|
|
226
|
-
const timer = setTimeout(async () => {
|
|
227
|
-
if (settled) return;
|
|
228
|
-
timedOut = true;
|
|
229
|
-
await terminateProcessTree(child);
|
|
230
|
-
settle(new Error(`${failureLabel} timed out after ${PDF_RENDER_TIMEOUT_MS / 1000}s.`));
|
|
231
|
-
}, PDF_RENDER_TIMEOUT_MS);
|
|
232
|
-
|
|
233
|
-
child.once("error", (error) => {
|
|
234
|
-
if (!timedOut) settle(error);
|
|
235
|
-
});
|
|
236
|
-
child.once("exit", (code, signal) => {
|
|
237
|
-
if (timedOut) return;
|
|
238
|
-
if (code === 0) {
|
|
239
|
-
settle();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const detail = diagnostics.trim();
|
|
243
|
-
settle(
|
|
244
|
-
new Error(
|
|
245
|
-
`${failureLabel} failed (${signal ? `signal ${signal}` : `exit ${code}`})${
|
|
246
|
-
detail ? `: ${detail}` : "."
|
|
247
|
-
}`,
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
176
|
function withSandboxFallback(args) {
|
|
255
177
|
if (
|
|
256
178
|
process.platform !== "win32" &&
|
|
@@ -262,35 +184,38 @@ function withSandboxFallback(args) {
|
|
|
262
184
|
return args;
|
|
263
185
|
}
|
|
264
186
|
|
|
265
|
-
export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir) {
|
|
266
|
-
// Enforce the contract at runtime. Otherwise it silently waits 60 seconds
|
|
267
|
-
// before timing out, obscuring the cause; fail immediately with an explanation.
|
|
187
|
+
export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir, job) {
|
|
268
188
|
if (new URL(pageUrl).searchParams.get("print") !== "1") {
|
|
269
189
|
throw new Error(
|
|
270
|
-
`Refusing to
|
|
190
|
+
`Refusing to render PDF from a non-print URL (${pageUrl}).`,
|
|
271
191
|
);
|
|
272
192
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
193
|
+
if (!job || typeof job !== "object") {
|
|
194
|
+
throw new Error("PDF rendering requires an output job.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { cdp, child } = await openCdpOutputPage(browser, pageUrl, profileDir, job);
|
|
198
|
+
try {
|
|
199
|
+
// The renderer reports ready only after Mermaid, images, fonts, and layout
|
|
200
|
+
// have settled. Give Chromium two compositor frames before printing.
|
|
201
|
+
await cdp.send("Runtime.evaluate", {
|
|
202
|
+
expression:
|
|
203
|
+
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
|
|
204
|
+
awaitPromise: true,
|
|
205
|
+
});
|
|
206
|
+
const pdf = await cdp.send("Page.printToPDF", {
|
|
207
|
+
displayHeaderFooter: false,
|
|
208
|
+
printBackground: true,
|
|
209
|
+
preferCSSPageSize: true,
|
|
210
|
+
transferMode: "ReturnAsBase64",
|
|
211
|
+
});
|
|
212
|
+
if (typeof pdf.data !== "string" || pdf.data.length === 0) {
|
|
213
|
+
throw new Error("Chromium DevTools did not return PDF data.");
|
|
214
|
+
}
|
|
215
|
+
await writeFile(outputPath, Buffer.from(pdf.data, "base64"));
|
|
216
|
+
} finally {
|
|
217
|
+
await closeCdpOutputPage(cdp, child);
|
|
218
|
+
}
|
|
294
219
|
}
|
|
295
220
|
|
|
296
221
|
async function waitForDevToolsPort(profileDir, child, diagnostics) {
|
|
@@ -497,14 +497,14 @@ export async function exportPdf(inst, requestedPath, requestedTheme) {
|
|
|
497
497
|
profileDir = await mkdtemp(join(tmpdir(), "markdstage-pdf-"));
|
|
498
498
|
const outputBase = basename(outputPath, extname(outputPath)) || "markdstage";
|
|
499
499
|
temporaryOutputPath = join(outputParent, `.${outputBase}.${token}.tmp.pdf`);
|
|
500
|
-
|
|
500
|
+
const exportJob = createOutputJob(snapshot, "pdf");
|
|
501
|
+
inst.exportJobs.set(token, exportJob);
|
|
501
502
|
|
|
502
503
|
const pageUrl = pageUrlFor(inst, { print: 1, token });
|
|
503
|
-
await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir);
|
|
504
|
-
|
|
505
|
-
if (exportJob?.status !== "ready") {
|
|
504
|
+
await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir, exportJob);
|
|
505
|
+
if (exportJob.status !== "ready") {
|
|
506
506
|
throw new Error(
|
|
507
|
-
exportJob
|
|
507
|
+
exportJob.error || "The print renderer did not finish before PDF generation.",
|
|
508
508
|
);
|
|
509
509
|
}
|
|
510
510
|
const bytes = await verifyPdf(temporaryOutputPath);
|
package/src/cli.mjs
CHANGED
|
@@ -36,8 +36,8 @@ import {
|
|
|
36
36
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
37
|
|
|
38
38
|
const COMMANDS = [
|
|
39
|
-
["
|
|
40
|
-
["
|
|
39
|
+
["present", "Open presenter view and launch the audience view from it."],
|
|
40
|
+
["preview", "Serve a deck on loopback and open it in a browser window."],
|
|
41
41
|
["validate", "Check deck structure, Architecture DSL blocks, and themes."],
|
|
42
42
|
["inspect", "Report 1280x720 clipping diagnostics for a deck."],
|
|
43
43
|
["capture", "Write 1280x720 PNG files for selected or clipped slides."],
|
|
@@ -59,8 +59,8 @@ const GLOBAL_OPTIONS = {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const COMMAND_OPTIONS = {
|
|
62
|
-
presentation: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
|
|
63
62
|
present: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
|
|
63
|
+
preview: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
|
|
64
64
|
validate: {},
|
|
65
65
|
inspect: { slide: { type: "string" }, all: { type: "boolean" }, "fail-on-issues": { type: "boolean" } },
|
|
66
66
|
capture: { pages: { type: "string" }, output: { type: "string" } },
|
|
@@ -103,8 +103,8 @@ function usage(command) {
|
|
|
103
103
|
return lines.join("\n");
|
|
104
104
|
}
|
|
105
105
|
const help = {
|
|
106
|
-
|
|
107
|
-
"Usage: markdstage
|
|
106
|
+
present: [
|
|
107
|
+
"Usage: markdstage present <file.md> [options]",
|
|
108
108
|
"",
|
|
109
109
|
"Opens presenter view with the current slide, next-slide preview, and speaker notes.",
|
|
110
110
|
"Use Start presentation in that view to open the synchronized audience window.",
|
|
@@ -114,16 +114,16 @@ function usage(command) {
|
|
|
114
114
|
"",
|
|
115
115
|
"Presentation requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
116
116
|
],
|
|
117
|
-
|
|
118
|
-
"Usage: markdstage
|
|
117
|
+
preview: [
|
|
118
|
+
"Usage: markdstage preview <file.md> [options]",
|
|
119
119
|
"",
|
|
120
120
|
" --watch Reload on save and enable Architecture editing.",
|
|
121
121
|
" --no-open Serve the deck without launching a browser.",
|
|
122
122
|
"",
|
|
123
|
-
"Without --watch,
|
|
123
|
+
"Without --watch, preview is read-only. Watch mode starts in normal viewing mode;",
|
|
124
124
|
"use the pencil control to edit Architecture diagrams and open the detailed designer.",
|
|
125
125
|
"",
|
|
126
|
-
"
|
|
126
|
+
"Preview requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
127
127
|
],
|
|
128
128
|
validate: [
|
|
129
129
|
"Usage: markdstage validate <file.md> [--json]",
|
|
@@ -262,8 +262,8 @@ export async function run(argv, io = {}) {
|
|
|
262
262
|
|
|
263
263
|
try {
|
|
264
264
|
switch (command) {
|
|
265
|
-
case "
|
|
266
|
-
const file = requireFile(positionals, "
|
|
265
|
+
case "preview": {
|
|
266
|
+
const file = requireFile(positionals, "preview");
|
|
267
267
|
const report = await presentCommand(
|
|
268
268
|
{ ...deckOptions(file, values), watch: values.watch, open: !values["no-open"], until: io.until },
|
|
269
269
|
{
|
|
@@ -277,8 +277,8 @@ export async function run(argv, io = {}) {
|
|
|
277
277
|
if (values.json) json(report);
|
|
278
278
|
return EXIT_OK;
|
|
279
279
|
}
|
|
280
|
-
case "
|
|
281
|
-
const file = requireFile(positionals, "
|
|
280
|
+
case "present": {
|
|
281
|
+
const file = requireFile(positionals, "present");
|
|
282
282
|
const report = await presentCommand(
|
|
283
283
|
{
|
|
284
284
|
...deckOptions(file, values),
|
package/src/commands/present.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Shared server for the markdstage preview and present commands.
|
|
2
2
|
|
|
3
3
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
@@ -139,7 +139,9 @@ export async function presentCommand(options, io) {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
io.print(
|
|
142
|
+
io.print(
|
|
143
|
+
`MarkdStage is ${options.presenterView ? "presenting" : "previewing"} ${session.sourceName || session.file}`,
|
|
144
|
+
);
|
|
143
145
|
io.print(` slides: ${session.slides.length}`);
|
|
144
146
|
io.print(` theme: ${session.theme}`);
|
|
145
147
|
io.print(` workspace: ${resolve(session.workspaceRoot)}`);
|
package/src/skills.mjs
CHANGED
|
@@ -78,7 +78,7 @@ what the MarkdStage canvas and MarkdStage Desktop render.
|
|
|
78
78
|
\`references/slide-format.md\`).
|
|
79
79
|
4. Validate structure, themes, and Architecture DSL before visual review:
|
|
80
80
|
\`markdstage validate slides.md --json\`.
|
|
81
|
-
5. Use \`markdstage
|
|
81
|
+
5. Use \`markdstage preview slides.md --watch\` for live source-backed authoring.
|
|
82
82
|
It reloads on save without losing the current slide and keeps the last valid
|
|
83
83
|
deck while a save is incomplete.
|
|
84
84
|
6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
|
|
@@ -88,14 +88,14 @@ what the MarkdStage canvas and MarkdStage Desktop render.
|
|
|
88
88
|
whose balance, spacing, or diagrams need visual judgment.
|
|
89
89
|
8. Revise Markdown and repeat validation plus targeted inspection until the deck
|
|
90
90
|
is valid, unclipped, concise, and visually balanced.
|
|
91
|
-
9. Deliver from the same source with \`markdstage
|
|
91
|
+
9. Deliver from the same source with \`markdstage present slides.md\`,
|
|
92
92
|
\`markdstage export slides.md --output slides.pdf\`, or
|
|
93
93
|
\`markdstage export slides.md --output slides.pptx\`.
|
|
94
94
|
|
|
95
|
-
The browser in \`
|
|
95
|
+
The browser in \`preview --watch\` starts in viewing mode. The user can activate
|
|
96
96
|
the pencil control to move Architecture elements, then choose **Advanced edit**
|
|
97
97
|
for the detailed designer. Placement changes save immediately, while the
|
|
98
|
-
detailed designer saves only when the user selects **Save**. \`
|
|
98
|
+
detailed designer saves only when the user selects **Save**. \`preview\` without
|
|
99
99
|
\`--watch\` is read-only.
|
|
100
100
|
|
|
101
101
|
Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
|
|
@@ -106,8 +106,8 @@ and layout diagnostics over capturing every slide.
|
|
|
106
106
|
|
|
107
107
|
| Command | Purpose |
|
|
108
108
|
| --- | --- |
|
|
109
|
-
| \`markdstage
|
|
110
|
-
| \`markdstage
|
|
109
|
+
| \`markdstage present <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
|
|
110
|
+
| \`markdstage preview <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save, keeps the current slide, and enables Architecture placement and detailed editing. Without it, the source is read-only. |
|
|
111
111
|
| \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
|
|
112
112
|
| \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
|
|
113
113
|
| \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |
|