@markdstage/markdstage 2.5.1 → 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 +75 -28
- package/shared/architecture-editor/editor.css +116 -20
- package/shared/architecture-editor/editor.js +577 -42
- package/shared/architecture-editor/index.html +53 -24
- package/shared/markdstage-guide.mjs +8 -2
- package/shared/renderer/architecture.mjs +96 -15
- package/shared/renderer/index.html +11 -0
- package/shared/renderer/renderer.js +179 -11
- package/shared/renderer/slides.css +19 -2
- package/shared/runtime/browser.mjs +29 -104
- package/shared/runtime/output.mjs +5 -5
- package/shared/runtime/pptx-package.mjs +9 -1
- package/shared/schema/architecture-v1.schema.json +11 -1
- package/src/cli.mjs +13 -13
- package/src/commands/present.mjs +4 -2
- package/src/skills.mjs +6 -6
|
@@ -250,12 +250,15 @@ body.mermaid-loading .mermaid{visibility:hidden;}
|
|
|
250
250
|
.architecture-editor [data-architecture-movable="false"]{cursor:not-allowed;outline:none;}
|
|
251
251
|
.architecture-editor [data-architecture-type]:focus-visible>rect,
|
|
252
252
|
.architecture-editor [data-architecture-type]:focus-visible>ellipse,
|
|
253
|
+
.architecture-editor [data-architecture-type]:focus-visible>polygon,
|
|
253
254
|
.architecture-editor .architecture-selected>rect,
|
|
254
|
-
.architecture-editor .architecture-selected>ellipse
|
|
255
|
+
.architecture-editor .architecture-selected>ellipse,
|
|
256
|
+
.architecture-editor .architecture-selected>polygon{
|
|
255
257
|
stroke:var(--accent-strong);stroke-width:7;filter:drop-shadow(0 0 8px var(--accent-line));}
|
|
256
258
|
/* Layout-managed elements cannot move. Show a dashed explanation only when selected. */
|
|
257
259
|
.architecture-editor [data-architecture-movable="false"].architecture-selected>rect,
|
|
258
|
-
.architecture-editor [data-architecture-movable="false"].architecture-selected>ellipse
|
|
260
|
+
.architecture-editor [data-architecture-movable="false"].architecture-selected>ellipse,
|
|
261
|
+
.architecture-editor [data-architecture-movable="false"].architecture-selected>polygon{
|
|
259
262
|
stroke-dasharray:10 8;}
|
|
260
263
|
.architecture-error{display:flex;flex-direction:column;gap:.35em;margin:.5em 0;
|
|
261
264
|
padding:.8em 1em;border:2px solid #d13438;border-radius:8px;
|
|
@@ -544,8 +547,22 @@ body.presenter-view-mode .layout-warning{display:none;}
|
|
|
544
547
|
align-items:center;justify-content:center;border-radius:6px;
|
|
545
548
|
font-size:.82em;font-weight:700;font-variant-numeric:tabular-nums;
|
|
546
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;}
|
|
547
553
|
.overview-label{flex:1 1 auto;font-size:.96em;line-height:1.35;
|
|
548
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;}
|
|
549
566
|
.overview-item.current .overview-link{background:var(--accent-soft);}
|
|
550
567
|
.overview-item.current .overview-num{color:var(--fg);background:var(--accent);
|
|
551
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);
|
|
@@ -569,8 +569,16 @@ function nativeShapeXml(element, path, id, relationships) {
|
|
|
569
569
|
rect: "rect",
|
|
570
570
|
roundedRect: "roundRect",
|
|
571
571
|
ellipse: "ellipse",
|
|
572
|
+
diamond: "diamond",
|
|
573
|
+
triangle: "triangle",
|
|
574
|
+
hexagon: "hexagon",
|
|
575
|
+
parallelogram: "parallelogram",
|
|
572
576
|
}[element.shape];
|
|
573
|
-
if (!preset)
|
|
577
|
+
if (!preset) {
|
|
578
|
+
fail(
|
|
579
|
+
`${path}.shape must be rect, roundedRect, ellipse, diamond, triangle, hexagon, or parallelogram`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
574
582
|
const opacity = optionalUnitInterval(element.opacity, `${path}.opacity`);
|
|
575
583
|
const properties = `${xfrmXml(bounds)}<a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom>${colorXml(
|
|
576
584
|
element.fill,
|
|
@@ -204,7 +204,17 @@
|
|
|
204
204
|
"properties": {
|
|
205
205
|
"type": { "const": "node" },
|
|
206
206
|
"id": { "$ref": "#/$defs/identifier" },
|
|
207
|
-
"shape": {
|
|
207
|
+
"shape": {
|
|
208
|
+
"enum": [
|
|
209
|
+
"rect",
|
|
210
|
+
"rounded-rect",
|
|
211
|
+
"ellipse",
|
|
212
|
+
"diamond",
|
|
213
|
+
"triangle",
|
|
214
|
+
"hexagon",
|
|
215
|
+
"parallelogram"
|
|
216
|
+
]
|
|
217
|
+
},
|
|
208
218
|
"x": { "$ref": "#/$defs/coordinate" },
|
|
209
219
|
"y": { "$ref": "#/$defs/coordinate" },
|
|
210
220
|
"width": { "$ref": "#/$defs/extent" },
|
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. |
|