@markdstage/markdstage 0.1.3 → 2.4.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 +21 -6
- package/package.json +3 -1
- package/shared/README.md +55 -31
- package/shared/architecture-editor/editor.css +155 -0
- package/shared/architecture-editor/editor.js +1775 -0
- package/shared/architecture-editor/index.html +99 -0
- package/shared/markdstage-guide.mjs +2 -2
- package/shared/renderer/architecture.mjs +233 -0
- package/shared/renderer/index.html +53 -12
- package/shared/renderer/renderer.js +1263 -25
- package/shared/renderer/slides.css +81 -6
- package/shared/runtime/architecture-editor-server.mjs +651 -0
- package/shared/runtime/architecture-source.mjs +195 -0
- package/shared/runtime/browser.mjs +71 -4
- package/shared/runtime/deck-session.mjs +3 -1
- package/shared/runtime/output-paths.mjs +18 -1
- package/shared/runtime/output.mjs +267 -4
- package/shared/runtime/pptx-package.mjs +1088 -0
- package/shared/runtime/presentation-server.mjs +268 -12
- package/src/cli.mjs +42 -4
- package/src/commands/export.mjs +31 -11
- package/src/commands/present.mjs +138 -49
- package/src/deck.mjs +2 -0
- package/src/runtime.mjs +8 -2
- package/src/skills.mjs +42 -16
package/src/commands/present.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// markdstage present — serve the deck on loopback and open it in a browser.
|
|
2
2
|
|
|
3
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -19,73 +19,162 @@ async function createWatcher(session, server, { onStatus }) {
|
|
|
19
19
|
const { createMarkdownWatcher } = await import(
|
|
20
20
|
pathToFileURL(sharedPath("scripts", "markdown-watcher.mjs")).href
|
|
21
21
|
);
|
|
22
|
-
|
|
22
|
+
const setWatchState = (status, error = "") => {
|
|
23
|
+
const changed = session.watchStatus !== status || session.watchError !== error;
|
|
24
|
+
session.watchStatus = status;
|
|
25
|
+
session.watchError = error;
|
|
26
|
+
if (changed) server.broadcast();
|
|
27
|
+
};
|
|
28
|
+
const watcher = createMarkdownWatcher({
|
|
23
29
|
path: session.file,
|
|
24
30
|
onChange: async () => {
|
|
25
31
|
try {
|
|
32
|
+
if ((await readFile(session.file, "utf8")) === session.sourceMarkdown) {
|
|
33
|
+
setWatchState("watching");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
26
36
|
// Keep the current slide, and keep the last valid deck when the file is
|
|
27
37
|
// saved in a broken intermediate state.
|
|
28
38
|
await session.load({ preserveIndex: true });
|
|
39
|
+
setWatchState("watching");
|
|
29
40
|
server.broadcast();
|
|
30
41
|
onStatus(`reloaded ${session.sourceName} (${session.slides.length} slides)`);
|
|
31
42
|
} catch (error) {
|
|
43
|
+
setWatchState("error", error?.code || "source_reload_failed");
|
|
32
44
|
onStatus(`reload failed, keeping the last valid deck: ${error?.message || error}`, true);
|
|
33
45
|
}
|
|
34
46
|
},
|
|
35
|
-
onError: (error) =>
|
|
47
|
+
onError: (error) => {
|
|
48
|
+
setWatchState("error", "watch_failed");
|
|
49
|
+
onStatus(`watch error: ${error?.message || error}`, true);
|
|
50
|
+
},
|
|
36
51
|
});
|
|
52
|
+
setWatchState("watching");
|
|
53
|
+
return watcher;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
export async function presentCommand(options, io) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}) : null;
|
|
57
|
+
let audienceProcess = null;
|
|
58
|
+
let audienceProfileDir = "";
|
|
59
|
+
let audienceUrl = "";
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"Presenting requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the deck only.",
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
profileDir = await mkdtemp(join(tmpdir(), "markdstage-presenter-window-"));
|
|
56
|
-
browserProcess = spawn(
|
|
57
|
-
browser,
|
|
58
|
-
buildPresenterBrowserArgs({ profileDir, presenterUrl: server.url }),
|
|
59
|
-
{ windowsHide: false, stdio: "ignore" },
|
|
60
|
-
);
|
|
61
|
-
browserProcess.once("error", (error) => {
|
|
62
|
-
io.status(`browser failed to start: ${error?.message || error}`, true);
|
|
63
|
-
});
|
|
61
|
+
const closeAudience = async () => {
|
|
62
|
+
const process = audienceProcess;
|
|
63
|
+
audienceProcess = null;
|
|
64
|
+
if (isProcessRunning(process)) await terminateProcessTree(process);
|
|
65
|
+
if (audienceProfileDir) {
|
|
66
|
+
await rm(audienceProfileDir, { recursive: true, force: true }).catch(() => {});
|
|
67
|
+
audienceProfileDir = "";
|
|
64
68
|
}
|
|
69
|
+
return { stopped: Boolean(process) };
|
|
70
|
+
};
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const audience = options.presenterView
|
|
73
|
+
? {
|
|
74
|
+
isRunning: () => isProcessRunning(audienceProcess),
|
|
75
|
+
open: async () => {
|
|
76
|
+
if (isProcessRunning(audienceProcess)) return { alreadyRunning: true };
|
|
77
|
+
await closeAudience();
|
|
78
|
+
const browser = findChromiumBrowser();
|
|
79
|
+
if (!browser) {
|
|
80
|
+
throw new MarkdStageError(
|
|
81
|
+
"presenter_browser_not_found",
|
|
82
|
+
"Opening the audience view requires Microsoft Edge, Google Chrome, or Chromium.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
audienceProfileDir = await mkdtemp(join(tmpdir(), "markdstage-audience-window-"));
|
|
86
|
+
audienceProcess = spawn(
|
|
87
|
+
browser,
|
|
88
|
+
buildPresenterBrowserArgs({
|
|
89
|
+
profileDir: audienceProfileDir,
|
|
90
|
+
presenterUrl: audienceUrl,
|
|
91
|
+
}),
|
|
92
|
+
{ windowsHide: false, stdio: "ignore" },
|
|
93
|
+
);
|
|
94
|
+
await new Promise((ready, reject) => {
|
|
95
|
+
audienceProcess.once("spawn", ready);
|
|
96
|
+
audienceProcess.once("error", reject);
|
|
97
|
+
});
|
|
98
|
+
return { alreadyRunning: false };
|
|
99
|
+
},
|
|
100
|
+
close: closeAudience,
|
|
101
|
+
}
|
|
102
|
+
: null;
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
try {
|
|
105
|
+
return await withDeckServer({ ...options, presenter: audience }, async (session, server) => {
|
|
106
|
+
if (audience) {
|
|
107
|
+
const url = new URL(server.url);
|
|
108
|
+
url.searchParams.set("present", "1");
|
|
109
|
+
audienceUrl = url.href;
|
|
110
|
+
}
|
|
111
|
+
const watcher = options.watch
|
|
112
|
+
? await createWatcher(session, server, {
|
|
113
|
+
onStatus: (message, isError) => io.status(message, isError),
|
|
114
|
+
})
|
|
115
|
+
: null;
|
|
85
116
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
let browserProcess = null;
|
|
118
|
+
let profileDir = "";
|
|
119
|
+
const browserUrl = new URL(server.url);
|
|
120
|
+
if (options.presenterView) browserUrl.searchParams.set("presenter", "1");
|
|
121
|
+
try {
|
|
122
|
+
if (options.open) {
|
|
123
|
+
const browser = findChromiumBrowser();
|
|
124
|
+
if (!browser) {
|
|
125
|
+
throw new MarkdStageError(
|
|
126
|
+
"presenter_browser_not_found",
|
|
127
|
+
"Presenting requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the deck only.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
profileDir = await mkdtemp(join(tmpdir(), "markdstage-presenter-window-"));
|
|
131
|
+
browserProcess = spawn(
|
|
132
|
+
browser,
|
|
133
|
+
buildPresenterBrowserArgs({ profileDir, presenterUrl: browserUrl.href }),
|
|
134
|
+
{ windowsHide: false, stdio: "ignore" },
|
|
135
|
+
);
|
|
136
|
+
await new Promise((ready, reject) => {
|
|
137
|
+
browserProcess.once("spawn", ready);
|
|
138
|
+
browserProcess.once("error", reject);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
io.print(`MarkdStage is presenting ${session.sourceName || session.file}`);
|
|
143
|
+
io.print(` slides: ${session.slides.length}`);
|
|
144
|
+
io.print(` theme: ${session.theme}`);
|
|
145
|
+
io.print(` workspace: ${resolve(session.workspaceRoot)}`);
|
|
146
|
+
io.print(` url: ${browserUrl.href}`);
|
|
147
|
+
if (options.watch) {
|
|
148
|
+
io.print(" watching: on (live reload and Architecture editing are enabled)");
|
|
149
|
+
}
|
|
150
|
+
io.print("Press Ctrl+C to stop.");
|
|
151
|
+
|
|
152
|
+
await new Promise((done) => {
|
|
153
|
+
const stop = () => {
|
|
154
|
+
process.off("SIGINT", stop);
|
|
155
|
+
process.off("SIGTERM", stop);
|
|
156
|
+
done();
|
|
157
|
+
};
|
|
158
|
+
process.once("SIGINT", stop);
|
|
159
|
+
process.once("SIGTERM", stop);
|
|
160
|
+
if (browserProcess) browserProcess.once("close", stop);
|
|
161
|
+
if (options.until) options.until.then(stop, stop);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
url: browserUrl.href,
|
|
167
|
+
total: session.slides.length,
|
|
168
|
+
theme: session.theme,
|
|
169
|
+
};
|
|
170
|
+
} finally {
|
|
171
|
+
watcher?.close();
|
|
172
|
+
await closeAudience();
|
|
173
|
+
if (isProcessRunning(browserProcess)) await terminateProcessTree(browserProcess);
|
|
174
|
+
if (profileDir) await rm(profileDir, { recursive: true, force: true }).catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} finally {
|
|
178
|
+
await closeAudience();
|
|
179
|
+
}
|
|
91
180
|
}
|
package/src/deck.mjs
CHANGED
|
@@ -23,6 +23,8 @@ export async function withDeckServer(options, run) {
|
|
|
23
23
|
const server = await startPresentationServer(session, {
|
|
24
24
|
token,
|
|
25
25
|
onLog: options.log,
|
|
26
|
+
editable: options.watch === true,
|
|
27
|
+
presenter: options.presenter,
|
|
26
28
|
});
|
|
27
29
|
try {
|
|
28
30
|
return await run(session, server);
|
package/src/runtime.mjs
CHANGED
|
@@ -64,9 +64,15 @@ export const { MarkdStageError } = errors;
|
|
|
64
64
|
export const { createDeckSession, readDeckSlides, resolveDeckFile, resolveDeckTheme } =
|
|
65
65
|
deckSession;
|
|
66
66
|
export const { createUrlToken, startPresentationServer } = presentationServer;
|
|
67
|
-
export const {
|
|
67
|
+
export const {
|
|
68
|
+
captureSlides,
|
|
69
|
+
exportPdf,
|
|
70
|
+
exportPptx,
|
|
71
|
+
inspectLayout,
|
|
72
|
+
MAX_CAPTURE_SLIDES,
|
|
73
|
+
} = output;
|
|
68
74
|
export const { findChromiumBrowser, terminateProcessTree, isProcessRunning } = browser;
|
|
69
|
-
export const { captureDirectoryName, pdfNameForSource } = outputPaths;
|
|
75
|
+
export const { captureDirectoryName, pdfNameForSource, pptxNameForSource } = outputPaths;
|
|
70
76
|
export const {
|
|
71
77
|
architectureValidationErrors,
|
|
72
78
|
deckValidationFeedback,
|
package/src/skills.mjs
CHANGED
|
@@ -24,11 +24,12 @@ export const SKILL_TARGETS = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const DESCRIPTION =
|
|
27
|
-
"Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user " +
|
|
28
|
-
|
|
29
|
-
'"turn this file into slides", "export the deck to PDF
|
|
30
|
-
"
|
|
31
|
-
"
|
|
27
|
+
"Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user asks to create, " +
|
|
28
|
+
"refine, present, preview, validate, inspect, screenshot, or export a Markdown deck " +
|
|
29
|
+
'("present slides.md", "turn this file into slides", "export the deck to PDF or PowerPoint", ' +
|
|
30
|
+
'"check whether my slides fit"). Provides a deterministic create-review-deliver workflow, ' +
|
|
31
|
+
"browser-based Architecture DSL editing, theme validation, 1280x720 clipping diagnostics, " +
|
|
32
|
+
"targeted PNG capture, and PDF/PowerPoint export.";
|
|
32
33
|
|
|
33
34
|
function frontMatter(fields) {
|
|
34
35
|
const lines = ["---"];
|
|
@@ -66,26 +67,51 @@ what the MarkdStage canvas and MarkdStage Desktop render.
|
|
|
66
67
|
- An installed Microsoft Edge, Google Chrome, or Chromium (never downloaded automatically).
|
|
67
68
|
- The CLI: \`npx @markdstage/markdstage <command>\` or \`npm install --global @markdstage/markdstage\`.
|
|
68
69
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
1.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
## Recommended authoring workflow
|
|
71
|
+
|
|
72
|
+
1. Establish the source material, audience, objective, approximate length, theme,
|
|
73
|
+
required diagrams, and output format.
|
|
74
|
+
2. Read only the relevant guidance. Start with
|
|
75
|
+
\`markdstage guide slide-format\`, then retrieve \`themes\`,
|
|
76
|
+
\`custom-themes\`, or \`architecture-dsl\` when needed.
|
|
77
|
+
3. Create the complete deck as one Markdown source file (see
|
|
78
|
+
\`references/slide-format.md\`).
|
|
79
|
+
4. Validate structure, themes, and Architecture DSL before visual review:
|
|
80
|
+
\`markdstage validate slides.md --json\`.
|
|
81
|
+
5. Use \`markdstage present slides.md --watch\` for live source-backed authoring.
|
|
82
|
+
It reloads on save without losing the current slide and keeps the last valid
|
|
83
|
+
deck while a save is incomplete.
|
|
84
|
+
6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
|
|
85
|
+
\`--slide <n>\` after localized changes and \`--fail-on-issues\` in CI.
|
|
86
|
+
7. Run \`markdstage capture slides.md\` only after inspection. Without
|
|
87
|
+
\`--pages\`, it captures only clipped slides; use \`--pages 2,4\` for pages
|
|
88
|
+
whose balance, spacing, or diagrams need visual judgment.
|
|
89
|
+
8. Revise Markdown and repeat validation plus targeted inspection until the deck
|
|
90
|
+
is valid, unclipped, concise, and visually balanced.
|
|
91
|
+
9. Deliver from the same source with \`markdstage presentation slides.md\`,
|
|
92
|
+
\`markdstage export slides.md --output slides.pdf\`, or
|
|
93
|
+
\`markdstage export slides.md --output slides.pptx\`.
|
|
94
|
+
|
|
95
|
+
The browser in \`present --watch\` starts in viewing mode. The user can activate
|
|
96
|
+
the pencil control to move Architecture elements, then choose **Advanced edit**
|
|
97
|
+
for the detailed designer. Placement changes save immediately, while the
|
|
98
|
+
detailed designer saves only when the user selects **Save**. \`present\` without
|
|
99
|
+
\`--watch\` is read-only.
|
|
76
100
|
|
|
77
101
|
Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
|
|
78
|
-
content or by changing the layout in front matter.
|
|
102
|
+
content or by changing the layout in front matter. Prefer structured validation
|
|
103
|
+
and layout diagnostics over capturing every slide.
|
|
79
104
|
|
|
80
105
|
## Commands
|
|
81
106
|
|
|
82
107
|
| Command | Purpose |
|
|
83
108
|
| --- | --- |
|
|
84
|
-
| \`markdstage
|
|
109
|
+
| \`markdstage presentation <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
|
|
110
|
+
| \`markdstage present <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. |
|
|
85
111
|
| \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
|
|
86
|
-
| \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide. |
|
|
112
|
+
| \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
|
|
87
113
|
| \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |
|
|
88
|
-
| \`markdstage export <file> [--output slides.pdf]\` | Produce
|
|
114
|
+
| \`markdstage export <file> [--output slides.pdf|slides.pptx]\` | Produce a 16:9 PDF or hybrid editable PowerPoint. |
|
|
89
115
|
| \`markdstage guide <topic>\` | Print the canonical MarkdStage authoring guide. |
|
|
90
116
|
|
|
91
117
|
Exit codes: \`0\` success, \`1\` usage error, \`2\` deck or input error, \`3\` no
|