@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.
@@ -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 { presentCommand } from "./commands/present.mjs";
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 presenter view and launch the audience view from it."],
40
- ["preview", "Serve a deck on loopback and open it in a browser window."],
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: { output: { type: "string" } },
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: markdstage <command> [options]",
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 presenter view with the current slide, next-slide preview, and speaker notes.",
110
- "Use Start presentation in that view to open the synchronized audience window.",
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 Reload on save.",
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
- " --watch Reload on save and enable Architecture editing.",
121
- " --no-open Serve the deck without launching a browser.",
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
- "Without --watch, preview is read-only. Watch mode starts in normal viewing mode;",
124
- "use the pencil control to edit Architecture diagrams and open the detailed designer.",
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> [--output slides.pdf|slides.pptx]",
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" || command === undefined) {
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
- err(`Unknown option: ${command}\n\n${usage()}`);
234
- return EXIT_USAGE;
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 presentCommand(
268
- { ...deckOptions(file, values), watch: values.watch, open: !values["no-open"], until: io.until },
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 presentCommand(
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));
@@ -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);
@@ -1,181 +1,171 @@
1
- // Shared server for the markdstage preview and present commands.
1
+ // Shared browser application for the top-level, preview, and present commands.
2
2
 
3
- import { mkdtemp, readFile, rm } from "node:fs/promises";
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 createWatcher(session, server, { onStatus }) {
19
- const { createMarkdownWatcher } = await import(
20
- pathToFileURL(sharedPath("scripts", "markdown-watcher.mjs")).href
21
- );
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({
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 closeAudience = async () => {
34
+ const closeAudienceNow = async () => {
62
35
  const process = audienceProcess;
36
+ const profileDir = audienceProfileDir;
63
37
  audienceProcess = null;
64
- if (isProcessRunning(process)) await terminateProcessTree(process);
65
- if (audienceProfileDir) {
66
- await rm(audienceProfileDir, { recursive: true, force: true }).catch(() => {});
67
- audienceProfileDir = "";
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 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,
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
- : null;
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({ ...options, presenter: audience }, async (session, server) => {
106
- if (audience) {
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
- 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.",
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
- io.print(
143
- `MarkdStage is ${options.presenterView ? "presenting" : "previewing"} ${session.sourceName || session.file}`,
144
- );
145
- io.print(` slides: ${session.slides.length}`);
146
- io.print(` theme: ${session.theme}`);
147
- io.print(` workspace: ${resolve(session.workspaceRoot)}`);
148
- io.print(` url: ${browserUrl.href}`);
149
- if (options.watch) {
150
- io.print(" watching: on (live reload and Architecture editing are enabled)");
151
- }
152
- io.print("Press Ctrl+C to stop.");
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
- await new Promise((done) => {
155
- const stop = () => {
156
- process.off("SIGINT", stop);
157
- process.off("SIGTERM", stop);
158
- done();
159
- };
160
- process.once("SIGINT", stop);
161
- process.once("SIGTERM", stop);
162
- if (browserProcess) browserProcess.once("close", stop);
163
- if (options.until) options.until.then(stop, stop);
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
- return {
167
- ok: true,
168
- url: browserUrl.href,
169
- total: session.slides.length,
170
- theme: session.theme,
171
- };
172
- } finally {
173
- watcher?.close();
174
- await closeAudience();
175
- if (isProcessRunning(browserProcess)) await terminateProcessTree(browserProcess);
176
- if (profileDir) await rm(profileDir, { recursive: true, force: true }).catch(() => {});
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
  }