@markdstage/markdstage 0.1.3 → 2.3.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 +29 -12
- 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 +1 -1
- package/shared/renderer/architecture.mjs +233 -0
- package/shared/renderer/index.html +7 -6
- package/shared/renderer/renderer.js +1163 -16
- package/shared/renderer/slides.css +42 -2
- 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 +9 -3
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
|
@@ -27,8 +27,8 @@ const DESCRIPTION =
|
|
|
27
27
|
"Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user " +
|
|
28
28
|
'asks to present, preview, validate, screenshot, or export a Markdown deck ("present slides.md", ' +
|
|
29
29
|
'"turn this file into slides", "export the deck to PDF", "check whether my slides fit"). ' +
|
|
30
|
-
"Provides deterministic commands for presenting
|
|
31
|
-
"themes, inspecting 1280x720 clipping, capturing PNGs, and exporting PDF.";
|
|
30
|
+
"Provides deterministic commands for presenting and visually editing Architecture DSL in a " +
|
|
31
|
+
"browser, validating themes, inspecting 1280x720 clipping, capturing PNGs, and exporting PDF.";
|
|
32
32
|
|
|
33
33
|
function frontMatter(fields) {
|
|
34
34
|
const lines = ["---"];
|
|
@@ -74,6 +74,12 @@ what the MarkdStage canvas and MarkdStage Desktop render.
|
|
|
74
74
|
4. Capture clipped slides for review: \`markdstage capture slides.md\`.
|
|
75
75
|
5. Present or export: \`markdstage present slides.md --watch\` / \`markdstage export slides.md\`.
|
|
76
76
|
|
|
77
|
+
Use \`present --watch\` for live authoring. It starts in viewing mode; the user can
|
|
78
|
+
activate the pencil control to move Architecture elements, then choose **Advanced
|
|
79
|
+
edit** for the detailed designer. Placement changes save immediately, while the
|
|
80
|
+
detailed designer saves only when the user selects **Save**. \`present\` without
|
|
81
|
+
\`--watch\` is read-only.
|
|
82
|
+
|
|
77
83
|
Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
|
|
78
84
|
content or by changing the layout in front matter.
|
|
79
85
|
|
|
@@ -81,7 +87,7 @@ content or by changing the layout in front matter.
|
|
|
81
87
|
|
|
82
88
|
| Command | Purpose |
|
|
83
89
|
| --- | --- |
|
|
84
|
-
| \`markdstage present <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save
|
|
90
|
+
| \`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
91
|
| \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
|
|
86
92
|
| \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide. |
|
|
87
93
|
| \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |
|