@markdstage/markdstage 3.3.0 → 3.8.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 +25 -13
- package/package.json +1 -1
- package/shared/README.md +47 -10
- package/shared/architecture-editor/editor.css +9 -5
- package/shared/architecture-editor/editor.js +440 -75
- package/shared/architecture-editor/index.html +2 -2
- package/shared/docs/custom-theme-authoring.md +61 -4
- package/shared/markdown-deck.mjs +9 -5
- package/shared/renderer/architecture-document.mjs +169 -10
- package/shared/renderer/index.html +24 -1
- package/shared/renderer/mermaid-scene.mjs +6725 -197
- package/shared/renderer/renderer.js +260 -55
- package/shared/renderer/scene-graph.mjs +83 -13
- package/shared/renderer/scene-pptx.mjs +154 -1
- package/shared/renderer/scene-svg.mjs +227 -11
- package/shared/renderer/slide-background.mjs +22 -0
- package/shared/renderer/slides.css +32 -6
- package/shared/renderer/theme.mjs +328 -12
- package/shared/runtime/browser.mjs +75 -5
- package/shared/runtime/deck-session.mjs +35 -9
- package/shared/runtime/output-paths.mjs +7 -0
- package/shared/runtime/output.mjs +4 -2
- package/shared/runtime/pptx-package.mjs +103 -22
- package/shared/runtime/presentation-server.mjs +410 -95
- package/shared/runtime/slide-backgrounds.mjs +44 -0
- package/shared/schema/theme-metadata-v1.schema.json +25 -0
- package/shared/schema/theme-v1.json +3 -3
- package/src/cli.mjs +101 -21
- package/src/commands/export.mjs +2 -0
- package/src/commands/present.mjs +133 -143
- package/src/deck.mjs +4 -0
- package/src/skills.mjs +12 -8
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { parseSlideBackground } from "../renderer/slide-background.mjs";
|
|
3
|
+
import { parseFrontMatter, THEME_ASSET_MAX_BYTES } from "../renderer/theme.mjs";
|
|
4
|
+
import { resolveAssetFile } from "../scripts/asset-paths.mjs";
|
|
5
|
+
import { MarkdStageError } from "./errors.mjs";
|
|
6
|
+
|
|
7
|
+
export async function resolveSlideBackgroundFile(workspaceRoot, sourceName, value) {
|
|
8
|
+
let canonical;
|
|
9
|
+
try {
|
|
10
|
+
canonical = parseSlideBackground(value);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new MarkdStageError("invalid_slide_background", error.message);
|
|
13
|
+
}
|
|
14
|
+
if (!canonical) return null;
|
|
15
|
+
let file;
|
|
16
|
+
try {
|
|
17
|
+
file = await resolveAssetFile(workspaceRoot, sourceName, canonical.slice("/assets/".length));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new MarkdStageError("invalid_slide_background", `Invalid background-image ${canonical}: ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
if (!file) {
|
|
22
|
+
throw new MarkdStageError("slide_background_not_found", `Background image was not found: ${canonical}`);
|
|
23
|
+
}
|
|
24
|
+
const info = await stat(file);
|
|
25
|
+
if (!info.isFile()) {
|
|
26
|
+
throw new MarkdStageError("slide_background_not_found", `Background image is not a file: ${canonical}`);
|
|
27
|
+
}
|
|
28
|
+
if (info.size > THEME_ASSET_MAX_BYTES) {
|
|
29
|
+
throw new MarkdStageError("slide_background_too_large", `Background image must be 2 MiB or smaller: ${canonical}`);
|
|
30
|
+
}
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadSlideBackgrounds(workspaceRoot, sourceName, slides) {
|
|
35
|
+
for (let index = 0; index < slides.length; index += 1) {
|
|
36
|
+
const meta = parseFrontMatter(slides[index]);
|
|
37
|
+
if (!Object.hasOwn(meta, "background-image")) continue;
|
|
38
|
+
try {
|
|
39
|
+
await resolveSlideBackgroundFile(workspaceRoot, sourceName, meta["background-image"]);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new MarkdStageError(error.code || "invalid_slide_background", `Slide ${index + 1}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
"version": {
|
|
13
13
|
"const": 1
|
|
14
14
|
},
|
|
15
|
+
"background": {
|
|
16
|
+
"description": "Common decorative background for default and center layouts only.",
|
|
17
|
+
"$ref": "#/$defs/decorativeImage"
|
|
18
|
+
},
|
|
19
|
+
"layouts": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"default": {
|
|
23
|
+
"$ref": "#/$defs/layoutBackground"
|
|
24
|
+
},
|
|
25
|
+
"center": {
|
|
26
|
+
"$ref": "#/$defs/layoutBackground"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"additionalProperties": false
|
|
30
|
+
},
|
|
15
31
|
"cover": {
|
|
16
32
|
"type": "object",
|
|
17
33
|
"properties": {
|
|
@@ -39,6 +55,15 @@
|
|
|
39
55
|
},
|
|
40
56
|
"additionalProperties": false,
|
|
41
57
|
"$defs": {
|
|
58
|
+
"layoutBackground": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"background": {
|
|
62
|
+
"$ref": "#/$defs/decorativeImage"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"additionalProperties": false
|
|
66
|
+
},
|
|
42
67
|
"assetPath": {
|
|
43
68
|
"type": "string",
|
|
44
69
|
"maxLength": 200,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/runceel/markdstage/schema/theme-v1.json",
|
|
4
4
|
"title": "MarkdStage custom theme v1",
|
|
5
|
-
"description": "Machine-readable catalog for CSS custom properties supported by the MarkdStage canvas custom theme. Optional cover/backcover assets live in a sibling theme.json manifest.",
|
|
5
|
+
"description": "Machine-readable catalog for CSS custom properties supported by the MarkdStage canvas custom theme. Optional common and default/center background images and cover/backcover assets live in a sibling theme.json manifest; background-image front matter overrides images per slide without CSS url().",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"version",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"type": "object",
|
|
17
17
|
"description": "CSS custom property declarations. The runtime accepts any name matching the pattern.",
|
|
18
18
|
"properties": {
|
|
19
|
-
"--bg": { "type": "string", "description": "Standard slide background" },
|
|
19
|
+
"--bg": { "type": "string", "description": "Standard slide background beneath optional decorative images" },
|
|
20
20
|
"--fg": { "type": "string", "description": "Headings and primary text" },
|
|
21
21
|
"--muted": { "type": "string", "description": "Secondary text" },
|
|
22
22
|
"--body": { "type": "string", "description": "Body text" },
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"additionalProperties": false,
|
|
81
81
|
"x-theme-file-format": "CSS custom property declarations, optionally wrapped in one :root block.",
|
|
82
82
|
"x-value-syntax": "Any non-empty CSS value except selectors, @import, url(), javascript:, expression(), and style tags.",
|
|
83
|
-
"x-theme-metadata": "When present, theme.json beside the CSS file must conform to theme-metadata-v1.schema.json."
|
|
83
|
+
"x-theme-metadata": "When present, theme.json beside the CSS file must conform to theme-metadata-v1.schema.json. Decorative { image, alt? } entries in background and layouts.default.background/layouts.center.background use theme-local assets/ paths. Layout images override the common background for default/center only; title retains cover.background. Per-slide background-image overrides every layout and theme. Images are centered cover over existing colors; missing settings, not invalid images, trigger fallback."
|
|
84
84
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
exitCodeFor,
|
|
22
22
|
} from "./exit.mjs";
|
|
23
23
|
import { parsePageList } from "./deck.mjs";
|
|
24
|
-
import {
|
|
24
|
+
import { applicationCommand } from "./commands/present.mjs";
|
|
25
25
|
import { validateCommand, formatValidateReport } from "./commands/validate.mjs";
|
|
26
26
|
import { inspectCommand, formatInspectReport } from "./commands/inspect.mjs";
|
|
27
27
|
import { captureCommand, formatCaptureReport } from "./commands/capture.mjs";
|
|
@@ -36,8 +36,8 @@ import {
|
|
|
36
36
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
37
|
|
|
38
38
|
const COMMANDS = [
|
|
39
|
-
["present", "Open
|
|
40
|
-
["preview", "
|
|
39
|
+
["present", "Open the MarkdStage UI in presenter view."],
|
|
40
|
+
["preview", "Open the MarkdStage UI in slide view."],
|
|
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."],
|
|
@@ -64,7 +64,10 @@ const COMMAND_OPTIONS = {
|
|
|
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" } },
|
|
67
|
-
export: {
|
|
67
|
+
export: {
|
|
68
|
+
output: { type: "string" },
|
|
69
|
+
"mermaid-image-fallback": { type: "boolean" },
|
|
70
|
+
},
|
|
68
71
|
guide: {},
|
|
69
72
|
skill: {
|
|
70
73
|
target: { type: "string" },
|
|
@@ -78,7 +81,14 @@ function usage(command) {
|
|
|
78
81
|
const lines = [
|
|
79
82
|
"MarkdStage — turn Markdown into 16:9 slides.",
|
|
80
83
|
"",
|
|
81
|
-
"Usage:
|
|
84
|
+
"Usage:",
|
|
85
|
+
" markdstage [options]",
|
|
86
|
+
" markdstage <file.md> [options]",
|
|
87
|
+
" markdstage <command> [options]",
|
|
88
|
+
"",
|
|
89
|
+
"Application:",
|
|
90
|
+
" With no file, open an empty UI and choose Markdown from the workspace.",
|
|
91
|
+
" With a Markdown file, open it in live slide view and refresh it on save.",
|
|
82
92
|
"",
|
|
83
93
|
"Commands:",
|
|
84
94
|
];
|
|
@@ -91,6 +101,7 @@ function usage(command) {
|
|
|
91
101
|
" --workspace <dir> Confine every read and write to this directory.",
|
|
92
102
|
" --theme <name> Override the deck theme.",
|
|
93
103
|
" --theme-file <path> Use a custom theme metadata file.",
|
|
104
|
+
" --no-open Serve the UI without launching a browser.",
|
|
94
105
|
" --json Print machine-readable JSON.",
|
|
95
106
|
" -h, --help Show help for a command.",
|
|
96
107
|
" -v, --version Print the CLI version.",
|
|
@@ -106,10 +117,10 @@ function usage(command) {
|
|
|
106
117
|
present: [
|
|
107
118
|
"Usage: markdstage present <file.md> [options]",
|
|
108
119
|
"",
|
|
109
|
-
"Opens
|
|
110
|
-
"
|
|
120
|
+
"Opens the full MarkdStage UI in presenter view.",
|
|
121
|
+
"Open Markdown, automatic refresh, editing, export, and audience controls remain available.",
|
|
111
122
|
"",
|
|
112
|
-
" --watch
|
|
123
|
+
" --watch Start with automatic refresh enabled.",
|
|
113
124
|
" --no-open Serve the presenter view without launching a browser.",
|
|
114
125
|
"",
|
|
115
126
|
"Presentation requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
@@ -117,11 +128,13 @@ function usage(command) {
|
|
|
117
128
|
preview: [
|
|
118
129
|
"Usage: markdstage preview <file.md> [options]",
|
|
119
130
|
"",
|
|
120
|
-
"
|
|
121
|
-
"
|
|
131
|
+
"Opens the full MarkdStage UI in slide view.",
|
|
132
|
+
"",
|
|
133
|
+
" --watch Start with automatic refresh enabled.",
|
|
134
|
+
" --no-open Serve the UI without launching a browser.",
|
|
122
135
|
"",
|
|
123
|
-
"
|
|
124
|
-
"
|
|
136
|
+
"Preview starts on the fixed 16:9 output surface. Use Output preview to switch",
|
|
137
|
+
"to the responsive layout. Architecture editing and export remain available.",
|
|
125
138
|
"",
|
|
126
139
|
"Preview requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
127
140
|
],
|
|
@@ -146,11 +159,14 @@ function usage(command) {
|
|
|
146
159
|
"Without --pages only the slides reported as clipped are captured.",
|
|
147
160
|
],
|
|
148
161
|
export: [
|
|
149
|
-
"Usage: markdstage export <file.md> [
|
|
162
|
+
"Usage: markdstage export <file.md> [options]",
|
|
150
163
|
"",
|
|
151
164
|
"Produces the same 16:9 PDF or hybrid editable PowerPoint as the MarkdStage canvas.",
|
|
152
165
|
"PowerPoint output includes speaker-note Markdown as readable plain text notes.",
|
|
153
166
|
"The output extension selects the format; omitting --output keeps PDF as the default.",
|
|
167
|
+
" --output <path> Write PDF or PowerPoint to this path.",
|
|
168
|
+
" --mermaid-image-fallback Render each Mermaid diagram as one image in PowerPoint.",
|
|
169
|
+
" Applies only with an explicit .pptx output.",
|
|
154
170
|
],
|
|
155
171
|
guide: [
|
|
156
172
|
"Usage: markdstage guide [topic] [--json]",
|
|
@@ -201,6 +217,10 @@ function deckOptions(file, values) {
|
|
|
201
217
|
};
|
|
202
218
|
}
|
|
203
219
|
|
|
220
|
+
function isMarkdownArgument(value) {
|
|
221
|
+
return typeof value === "string" && /\.(?:md|markdown)$/i.test(value);
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
export async function run(argv, io = {}) {
|
|
205
225
|
const out = io.out ?? ((text) => console.log(text));
|
|
206
226
|
const err = io.err ?? ((text) => console.error(text));
|
|
@@ -208,7 +228,7 @@ export async function run(argv, io = {}) {
|
|
|
208
228
|
|
|
209
229
|
const command = argv[0];
|
|
210
230
|
const rest = argv.slice(1);
|
|
211
|
-
if (command === "--help" || command === "-h"
|
|
231
|
+
if (command === "--help" || command === "-h") {
|
|
212
232
|
out(usage());
|
|
213
233
|
return EXIT_OK;
|
|
214
234
|
}
|
|
@@ -229,9 +249,63 @@ export async function run(argv, io = {}) {
|
|
|
229
249
|
out(await packageVersion());
|
|
230
250
|
return EXIT_OK;
|
|
231
251
|
}
|
|
232
|
-
if (command.startsWith("-")) {
|
|
233
|
-
|
|
234
|
-
|
|
252
|
+
if (command === undefined || isMarkdownArgument(command) || command.startsWith("-")) {
|
|
253
|
+
let values;
|
|
254
|
+
let positionals;
|
|
255
|
+
try {
|
|
256
|
+
({ values, positionals } = parseArgs({
|
|
257
|
+
args: argv,
|
|
258
|
+
options: {
|
|
259
|
+
...GLOBAL_OPTIONS,
|
|
260
|
+
"no-open": { type: "boolean" },
|
|
261
|
+
},
|
|
262
|
+
allowPositionals: true,
|
|
263
|
+
}));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
err(`${error.message}\n\n${usage()}`);
|
|
266
|
+
return EXIT_USAGE;
|
|
267
|
+
}
|
|
268
|
+
if (values.help) {
|
|
269
|
+
out(usage());
|
|
270
|
+
return EXIT_OK;
|
|
271
|
+
}
|
|
272
|
+
if (values.version) {
|
|
273
|
+
out(await packageVersion());
|
|
274
|
+
return EXIT_OK;
|
|
275
|
+
}
|
|
276
|
+
if (positionals.length > 1) {
|
|
277
|
+
err(`Application mode accepts at most one Markdown file.\n\n${usage()}`);
|
|
278
|
+
return EXIT_USAGE;
|
|
279
|
+
}
|
|
280
|
+
const file = positionals[0];
|
|
281
|
+
if (file && !isMarkdownArgument(file)) {
|
|
282
|
+
err(`Unknown command: ${file}\n\n${usage()}`);
|
|
283
|
+
return EXIT_USAGE;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const report = await applicationCommand(
|
|
287
|
+
{
|
|
288
|
+
...deckOptions(file, values),
|
|
289
|
+
live: Boolean(file),
|
|
290
|
+
open: io.open ?? !values["no-open"],
|
|
291
|
+
until: io.until,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
print: values.json ? () => {} : (message) => out(message),
|
|
295
|
+
status: (message, isError) => {
|
|
296
|
+
if (isError) err(message);
|
|
297
|
+
else if (!values.json) out(message);
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
if (values.json) json(report);
|
|
302
|
+
return EXIT_OK;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const code = exitCodeFor(error);
|
|
305
|
+
if (values.json) json(errorPayload(error));
|
|
306
|
+
else err(error?.message || String(error));
|
|
307
|
+
return code || EXIT_FAILURE;
|
|
308
|
+
}
|
|
235
309
|
}
|
|
236
310
|
if (!COMMAND_OPTIONS[command]) {
|
|
237
311
|
err(`Unknown command: ${command}\n\n${usage()}`);
|
|
@@ -264,8 +338,13 @@ export async function run(argv, io = {}) {
|
|
|
264
338
|
switch (command) {
|
|
265
339
|
case "preview": {
|
|
266
340
|
const file = requireFile(positionals, "preview");
|
|
267
|
-
const report = await
|
|
268
|
-
{
|
|
341
|
+
const report = await applicationCommand(
|
|
342
|
+
{
|
|
343
|
+
...deckOptions(file, values),
|
|
344
|
+
watch: values.watch,
|
|
345
|
+
open: io.open ?? !values["no-open"],
|
|
346
|
+
until: io.until,
|
|
347
|
+
},
|
|
269
348
|
{
|
|
270
349
|
print: values.json ? () => {} : (message) => out(message),
|
|
271
350
|
status: (message, isError) => {
|
|
@@ -279,11 +358,11 @@ export async function run(argv, io = {}) {
|
|
|
279
358
|
}
|
|
280
359
|
case "present": {
|
|
281
360
|
const file = requireFile(positionals, "present");
|
|
282
|
-
const report = await
|
|
361
|
+
const report = await applicationCommand(
|
|
283
362
|
{
|
|
284
363
|
...deckOptions(file, values),
|
|
285
364
|
watch: values.watch,
|
|
286
|
-
open: !values["no-open"],
|
|
365
|
+
open: io.open ?? !values["no-open"],
|
|
287
366
|
presenterView: true,
|
|
288
367
|
until: io.until,
|
|
289
368
|
},
|
|
@@ -348,6 +427,7 @@ export async function run(argv, io = {}) {
|
|
|
348
427
|
const report = await exportCommand({
|
|
349
428
|
...deckOptions(file, values),
|
|
350
429
|
output: values.output,
|
|
430
|
+
mermaidImageFallback: values["mermaid-image-fallback"],
|
|
351
431
|
});
|
|
352
432
|
if (values.json) json(report);
|
|
353
433
|
else out(formatExportReport(report));
|
package/src/commands/export.mjs
CHANGED
|
@@ -22,6 +22,8 @@ export async function exportCommand(
|
|
|
22
22
|
session,
|
|
23
23
|
options.output || pptxNameForSource(session.sourceName),
|
|
24
24
|
options.theme,
|
|
25
|
+
undefined,
|
|
26
|
+
{ mermaidImageFallback: options.mermaidImageFallback === true },
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
29
|
return exporters.pdf(session, requested, options.theme);
|
package/src/commands/present.mjs
CHANGED
|
@@ -1,181 +1,171 @@
|
|
|
1
|
-
// Shared
|
|
1
|
+
// Shared browser application for the top-level, preview, and present commands.
|
|
2
2
|
|
|
3
|
-
import { mkdtemp,
|
|
3
|
+
import { mkdtemp, 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";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
8
7
|
import {
|
|
9
8
|
MarkdStageError,
|
|
10
9
|
buildPresenterBrowserArgs,
|
|
11
10
|
findChromiumBrowser,
|
|
12
11
|
isProcessRunning,
|
|
13
|
-
sharedPath,
|
|
14
12
|
terminateProcessTree,
|
|
15
13
|
} from "../runtime.mjs";
|
|
16
14
|
import { withDeckServer } from "../deck.mjs";
|
|
17
15
|
|
|
18
|
-
async function
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
session.watchError = error;
|
|
26
|
-
if (changed) server.broadcast();
|
|
27
|
-
};
|
|
28
|
-
const watcher = createMarkdownWatcher({
|
|
29
|
-
path: session.file,
|
|
30
|
-
onChange: async () => {
|
|
31
|
-
try {
|
|
32
|
-
if ((await readFile(session.file, "utf8")) === session.sourceMarkdown) {
|
|
33
|
-
setWatchState("watching");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Keep the current slide, and keep the last valid deck when the file is
|
|
37
|
-
// saved in a broken intermediate state.
|
|
38
|
-
await session.load({ preserveIndex: true });
|
|
39
|
-
setWatchState("watching");
|
|
40
|
-
server.broadcast();
|
|
41
|
-
onStatus(`reloaded ${session.sourceName} (${session.slides.length} slides)`);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
setWatchState("error", error?.code || "source_reload_failed");
|
|
44
|
-
onStatus(`reload failed, keeping the last valid deck: ${error?.message || error}`, true);
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
onError: (error) => {
|
|
48
|
-
setWatchState("error", "watch_failed");
|
|
49
|
-
onStatus(`watch error: ${error?.message || error}`, true);
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
setWatchState("watching");
|
|
53
|
-
return watcher;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function presentCommand(options, io) {
|
|
16
|
+
export async function applicationCommand(options, io, dependencies = {}) {
|
|
17
|
+
const createTempDirectory = dependencies.mkdtemp ?? mkdtemp;
|
|
18
|
+
const findBrowser = dependencies.findChromiumBrowser ?? findChromiumBrowser;
|
|
19
|
+
const processIsRunning = dependencies.isProcessRunning ?? isProcessRunning;
|
|
20
|
+
const remove = dependencies.rm ?? rm;
|
|
21
|
+
const spawnBrowser = dependencies.spawn ?? spawn;
|
|
22
|
+
const terminate = dependencies.terminateProcessTree ?? terminateProcessTree;
|
|
57
23
|
let audienceProcess = null;
|
|
58
24
|
let audienceProfileDir = "";
|
|
59
25
|
let audienceUrl = "";
|
|
26
|
+
let audienceOperation = Promise.resolve();
|
|
27
|
+
|
|
28
|
+
const runAudienceOperation = (operation) => {
|
|
29
|
+
const result = audienceOperation.then(operation, operation);
|
|
30
|
+
audienceOperation = result.catch(() => {});
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
60
33
|
|
|
61
|
-
const
|
|
34
|
+
const closeAudienceNow = async () => {
|
|
62
35
|
const process = audienceProcess;
|
|
36
|
+
const profileDir = audienceProfileDir;
|
|
63
37
|
audienceProcess = null;
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
38
|
+
audienceProfileDir = "";
|
|
39
|
+
if (processIsRunning(process)) await terminate(process);
|
|
40
|
+
if (profileDir) {
|
|
41
|
+
await remove(profileDir, { recursive: true, force: true }).catch(() => {});
|
|
68
42
|
}
|
|
69
43
|
return { stopped: Boolean(process) };
|
|
70
44
|
};
|
|
71
45
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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,
|
|
46
|
+
const closeAudience = () => runAudienceOperation(closeAudienceNow);
|
|
47
|
+
|
|
48
|
+
const audience = {
|
|
49
|
+
isRunning: () => processIsRunning(audienceProcess),
|
|
50
|
+
open: () => runAudienceOperation(async () => {
|
|
51
|
+
if (processIsRunning(audienceProcess)) return { alreadyRunning: true };
|
|
52
|
+
await closeAudienceNow();
|
|
53
|
+
const browser = findBrowser();
|
|
54
|
+
if (!browser) {
|
|
55
|
+
throw new MarkdStageError(
|
|
56
|
+
"presenter_browser_not_found",
|
|
57
|
+
"Opening the audience view requires Microsoft Edge, Google Chrome, or Chromium.",
|
|
58
|
+
);
|
|
101
59
|
}
|
|
102
|
-
|
|
60
|
+
const profileDir = await createTempDirectory(
|
|
61
|
+
join(tmpdir(), "markdstage-audience-window-"),
|
|
62
|
+
);
|
|
63
|
+
const process = spawnBrowser(
|
|
64
|
+
browser,
|
|
65
|
+
buildPresenterBrowserArgs({
|
|
66
|
+
profileDir,
|
|
67
|
+
presenterUrl: audienceUrl,
|
|
68
|
+
}),
|
|
69
|
+
{ windowsHide: false, stdio: "ignore" },
|
|
70
|
+
);
|
|
71
|
+
try {
|
|
72
|
+
await new Promise((ready, reject) => {
|
|
73
|
+
process.once("spawn", ready);
|
|
74
|
+
process.once("error", reject);
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (processIsRunning(process)) await terminate(process);
|
|
78
|
+
await remove(profileDir, { recursive: true, force: true }).catch(() => {});
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
audienceProcess = process;
|
|
82
|
+
audienceProfileDir = profileDir;
|
|
83
|
+
return { alreadyRunning: false };
|
|
84
|
+
}),
|
|
85
|
+
close: closeAudience,
|
|
86
|
+
};
|
|
103
87
|
|
|
104
88
|
try {
|
|
105
|
-
return await withDeckServer(
|
|
106
|
-
|
|
89
|
+
return await withDeckServer(
|
|
90
|
+
{
|
|
91
|
+
...options,
|
|
92
|
+
application: true,
|
|
93
|
+
initialSourceMode: options.live || options.watch ? "live" : "snapshot",
|
|
94
|
+
presenter: audience,
|
|
95
|
+
},
|
|
96
|
+
async (session, server) => {
|
|
107
97
|
const url = new URL(server.url);
|
|
108
98
|
url.searchParams.set("present", "1");
|
|
109
99
|
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;
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
101
|
+
let browserProcess = null;
|
|
102
|
+
let profileDir = "";
|
|
103
|
+
const browserUrl = new URL(server.url);
|
|
104
|
+
if (options.presenterView) browserUrl.searchParams.set("presenter", "1");
|
|
105
|
+
try {
|
|
106
|
+
if (options.open) {
|
|
107
|
+
const browser = findBrowser();
|
|
108
|
+
if (!browser) {
|
|
109
|
+
throw new MarkdStageError(
|
|
110
|
+
"presenter_browser_not_found",
|
|
111
|
+
"Opening MarkdStage requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the UI only.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
profileDir = await createTempDirectory(join(tmpdir(), "markdstage-app-window-"));
|
|
115
|
+
browserProcess = spawnBrowser(
|
|
116
|
+
browser,
|
|
117
|
+
buildPresenterBrowserArgs({ profileDir, presenterUrl: browserUrl.href }),
|
|
118
|
+
{ windowsHide: false, stdio: "ignore" },
|
|
128
119
|
);
|
|
120
|
+
await new Promise((ready, reject) => {
|
|
121
|
+
browserProcess.once("spawn", ready);
|
|
122
|
+
browserProcess.once("error", reject);
|
|
123
|
+
});
|
|
129
124
|
}
|
|
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
125
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
io.print(
|
|
151
|
-
|
|
152
|
-
|
|
126
|
+
const operation = options.presenterView
|
|
127
|
+
? "presenting"
|
|
128
|
+
: session.file
|
|
129
|
+
? "previewing"
|
|
130
|
+
: "ready in";
|
|
131
|
+
io.print(`MarkdStage is ${operation} ${session.sourceName || "the workspace"}`);
|
|
132
|
+
io.print(` slides: ${session.slides.length}`);
|
|
133
|
+
io.print(` theme: ${session.theme}`);
|
|
134
|
+
io.print(` workspace: ${resolve(session.workspaceRoot)}`);
|
|
135
|
+
io.print(` url: ${browserUrl.href}`);
|
|
136
|
+
if ((options.live || options.watch) && session.file) {
|
|
137
|
+
io.print(" watching: on (live reload is enabled)");
|
|
138
|
+
}
|
|
139
|
+
io.print("Press Ctrl+C to stop.");
|
|
153
140
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
141
|
+
await new Promise((done) => {
|
|
142
|
+
const stop = () => {
|
|
143
|
+
process.off("SIGINT", stop);
|
|
144
|
+
process.off("SIGTERM", stop);
|
|
145
|
+
done();
|
|
146
|
+
};
|
|
147
|
+
process.once("SIGINT", stop);
|
|
148
|
+
process.once("SIGTERM", stop);
|
|
149
|
+
if (browserProcess) browserProcess.once("close", stop);
|
|
150
|
+
if (options.until) options.until.then(stop, stop);
|
|
151
|
+
});
|
|
165
152
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
url: browserUrl.href,
|
|
156
|
+
total: session.slides.length,
|
|
157
|
+
theme: session.theme,
|
|
158
|
+
sourceMode: server.sourceMode,
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
await closeAudience();
|
|
162
|
+
if (processIsRunning(browserProcess)) await terminate(browserProcess);
|
|
163
|
+
if (profileDir) {
|
|
164
|
+
await remove(profileDir, { recursive: true, force: true }).catch(() => {});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
);
|
|
179
169
|
} finally {
|
|
180
170
|
await closeAudience();
|
|
181
171
|
}
|