@markdstage/markdstage 3.3.0 → 3.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 +23 -12
- package/package.json +1 -1
- package/shared/README.md +7 -6
- package/shared/renderer/renderer.js +15 -2
- package/shared/runtime/deck-session.mjs +37 -6
- package/shared/runtime/presentation-server.mjs +368 -95
- package/src/cli.mjs +92 -19
- package/src/commands/present.mjs +133 -143
- package/src/deck.mjs +4 -0
- package/src/skills.mjs +12 -8
|
@@ -13,16 +13,22 @@
|
|
|
13
13
|
// overview, custom themes, Mermaid, Architecture DSL, local assets) is identical.
|
|
14
14
|
|
|
15
15
|
import { createServer } from "node:http";
|
|
16
|
-
import { realpath, stat } from "node:fs/promises";
|
|
17
|
-
import { dirname, join,
|
|
16
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { randomBytes } from "node:crypto";
|
|
20
20
|
import { resolveAssetFile } from "../scripts/asset-paths.mjs";
|
|
21
21
|
import { importedArchitectureBlockIndex } from "../scripts/markdown-blocks.mjs";
|
|
22
22
|
import { isMarkdownPath, listMarkdownFiles } from "../scripts/markdown-files.mjs";
|
|
23
|
+
import { createMarkdownWatcher } from "../scripts/markdown-watcher.mjs";
|
|
23
24
|
import { startArchitectureEditorServer } from "./architecture-editor-server.mjs";
|
|
24
25
|
import { saveArchitectureSource } from "./architecture-source.mjs";
|
|
25
|
-
import {
|
|
26
|
+
import { exportPdf, exportPptx } from "./output.mjs";
|
|
27
|
+
import {
|
|
28
|
+
isPathInside,
|
|
29
|
+
pdfNameForSource,
|
|
30
|
+
pptxNameForSource,
|
|
31
|
+
} from "./output-paths.mjs";
|
|
26
32
|
import { safeJoin, sendChunkedVendorAsset, sendFile } from "./static-files.mjs";
|
|
27
33
|
|
|
28
34
|
const RUNTIME_DIR = dirname(fileURLToPath(import.meta.url));
|
|
@@ -93,13 +99,139 @@ function broadcast(session) {
|
|
|
93
99
|
*/
|
|
94
100
|
export async function startPresentationServer(
|
|
95
101
|
session,
|
|
96
|
-
{
|
|
102
|
+
{
|
|
103
|
+
application = false,
|
|
104
|
+
editable = false,
|
|
105
|
+
exporters,
|
|
106
|
+
initialSourceMode = "snapshot",
|
|
107
|
+
onLog,
|
|
108
|
+
presenter,
|
|
109
|
+
token = createUrlToken(),
|
|
110
|
+
watcherFactory = createMarkdownWatcher,
|
|
111
|
+
} = {},
|
|
97
112
|
) {
|
|
98
113
|
const base = `/${token}`;
|
|
99
114
|
const architectureEditors = new Map();
|
|
100
|
-
const
|
|
115
|
+
const applicationMode = application === true;
|
|
116
|
+
const editingAvailable = applicationMode || editable === true;
|
|
117
|
+
const exportPdfImpl = exporters?.pdf ?? exportPdf;
|
|
118
|
+
const exportPptxImpl = exporters?.pptx ?? exportPptx;
|
|
119
|
+
let sourceMode = initialSourceMode === "live" ? "live" : "snapshot";
|
|
120
|
+
let sourceWatchStatus = "inactive";
|
|
121
|
+
let sourceWatchError = "";
|
|
122
|
+
let sourceWatcher = null;
|
|
123
|
+
let sourceWatcherGeneration = 0;
|
|
124
|
+
let sourceOperation = Promise.resolve();
|
|
101
125
|
let port = 0;
|
|
102
126
|
|
|
127
|
+
const runSourceOperation = (operation) => {
|
|
128
|
+
const result = sourceOperation.then(operation, operation);
|
|
129
|
+
sourceOperation = result.catch(() => {});
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const setWatchState = (status, error = "") => {
|
|
134
|
+
const changed = sourceWatchStatus !== status || sourceWatchError !== error;
|
|
135
|
+
sourceWatchStatus = status;
|
|
136
|
+
sourceWatchError = error;
|
|
137
|
+
session.watchStatus = status;
|
|
138
|
+
session.watchError = error;
|
|
139
|
+
if (changed && port) broadcast(session);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const closeSourceWatcher = () => {
|
|
143
|
+
sourceWatcherGeneration += 1;
|
|
144
|
+
sourceWatcher?.close();
|
|
145
|
+
sourceWatcher = null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const reloadSourceNow = async (generation) => {
|
|
149
|
+
if (
|
|
150
|
+
generation !== sourceWatcherGeneration ||
|
|
151
|
+
sourceMode !== "live" ||
|
|
152
|
+
!session.file
|
|
153
|
+
) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const sourceFile = session.file;
|
|
157
|
+
try {
|
|
158
|
+
if ((await readFile(sourceFile, "utf8")) === session.sourceMarkdown) {
|
|
159
|
+
setWatchState("watching");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
generation !== sourceWatcherGeneration ||
|
|
164
|
+
sourceMode !== "live" ||
|
|
165
|
+
session.file !== sourceFile
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await session.load({ preserveIndex: true });
|
|
170
|
+
setWatchState("watching");
|
|
171
|
+
broadcast(session);
|
|
172
|
+
onLog?.(`reloaded ${session.sourceName} (${session.slides.length} slides)`);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (
|
|
175
|
+
generation !== sourceWatcherGeneration ||
|
|
176
|
+
sourceMode !== "live" ||
|
|
177
|
+
session.file !== sourceFile
|
|
178
|
+
) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setWatchState("error", error?.code || "source_reload_failed");
|
|
182
|
+
onLog?.(
|
|
183
|
+
`reload failed, keeping the last valid deck: ${error?.message || error}`,
|
|
184
|
+
"error",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const reloadSource = (generation) =>
|
|
190
|
+
runSourceOperation(() => reloadSourceNow(generation));
|
|
191
|
+
|
|
192
|
+
const bindSourceWatcher = () => {
|
|
193
|
+
closeSourceWatcher();
|
|
194
|
+
if (sourceMode !== "live" || !session.file) {
|
|
195
|
+
setWatchState("inactive");
|
|
196
|
+
return { ok: true };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const generation = sourceWatcherGeneration;
|
|
200
|
+
sourceWatcher = watcherFactory({
|
|
201
|
+
path: session.file,
|
|
202
|
+
onChange: () => reloadSource(generation),
|
|
203
|
+
onError: (error) => {
|
|
204
|
+
setWatchState("error", "watch_failed");
|
|
205
|
+
onLog?.(`watch error: ${error?.message || error}`, "error");
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
setWatchState("watching");
|
|
209
|
+
return { ok: true, generation };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
setWatchState("error", "watch_failed");
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: "watch_failed",
|
|
215
|
+
message: error?.message || "The Markdown watcher could not be started.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const setSourceMode = async (mode, { reload = true } = {}) => {
|
|
221
|
+
sourceMode = mode === "live" ? "live" : "snapshot";
|
|
222
|
+
const result = bindSourceWatcher();
|
|
223
|
+
if (sourceMode === "live" && result.ok && reload) {
|
|
224
|
+
await reloadSourceNow(result.generation);
|
|
225
|
+
}
|
|
226
|
+
return sourceMode === "live"
|
|
227
|
+
? {
|
|
228
|
+
ok: true,
|
|
229
|
+
status: sourceWatchStatus,
|
|
230
|
+
error: sourceWatchError,
|
|
231
|
+
}
|
|
232
|
+
: { ok: true };
|
|
233
|
+
};
|
|
234
|
+
|
|
103
235
|
const server = createServer(async (req, res) => {
|
|
104
236
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
105
237
|
res.setHeader("Referrer-Policy", "no-referrer");
|
|
@@ -158,21 +290,23 @@ export async function startPresentationServer(
|
|
|
158
290
|
customThemeCss: session.customThemeCss,
|
|
159
291
|
customThemeMeta: session.customThemeMeta,
|
|
160
292
|
mode: session.mode,
|
|
161
|
-
sourceBacked:
|
|
162
|
-
sourceModeAvailable:
|
|
163
|
-
sourceMode
|
|
164
|
-
sourceWatchStatus
|
|
165
|
-
sourceWatchError
|
|
293
|
+
sourceBacked: Boolean(session.file),
|
|
294
|
+
sourceModeAvailable: applicationMode && Boolean(session.file),
|
|
295
|
+
sourceMode,
|
|
296
|
+
sourceWatchStatus,
|
|
297
|
+
sourceWatchError,
|
|
166
298
|
presenterRunning: Boolean(presenter?.isRunning?.()),
|
|
167
299
|
presenterWindowAvailable: Boolean(presenter),
|
|
168
300
|
presenterViewAvailable: Boolean(presenter),
|
|
169
|
-
pdfExportAvailable:
|
|
170
|
-
pptxExportAvailable:
|
|
171
|
-
markdownImportAvailable:
|
|
172
|
-
architectureEditAvailable: editingAvailable,
|
|
173
|
-
architectureEdit:
|
|
174
|
-
|
|
175
|
-
|
|
301
|
+
pdfExportAvailable: applicationMode && Boolean(session.file),
|
|
302
|
+
pptxExportAvailable: applicationMode && Boolean(session.file),
|
|
303
|
+
markdownImportAvailable: applicationMode,
|
|
304
|
+
architectureEditAvailable: editingAvailable && Boolean(session.file),
|
|
305
|
+
architectureEdit:
|
|
306
|
+
editingAvailable && Boolean(session.file) && Boolean(session.architectureEdit),
|
|
307
|
+
architectureDetailedEdit: editingAvailable && Boolean(session.file),
|
|
308
|
+
architectureDetailedEditTarget:
|
|
309
|
+
editingAvailable && session.file ? "window" : "",
|
|
176
310
|
});
|
|
177
311
|
return;
|
|
178
312
|
}
|
|
@@ -310,6 +444,10 @@ export async function startPresentationServer(
|
|
|
310
444
|
}
|
|
311
445
|
|
|
312
446
|
if (route === "/markdown-files") {
|
|
447
|
+
if (!applicationMode) {
|
|
448
|
+
json(res, 501, { ok: false, error: "not_supported" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
313
451
|
if (req.method !== "GET") {
|
|
314
452
|
res.setHeader("Allow", "GET");
|
|
315
453
|
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
@@ -327,6 +465,10 @@ export async function startPresentationServer(
|
|
|
327
465
|
|
|
328
466
|
// Loading another workspace Markdown file from the browser 📂 button.
|
|
329
467
|
if (route === "/import") {
|
|
468
|
+
if (!applicationMode) {
|
|
469
|
+
json(res, 501, { ok: false, error: "not_supported" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
330
472
|
if (req.method !== "POST") {
|
|
331
473
|
res.setHeader("Allow", "POST");
|
|
332
474
|
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
@@ -345,6 +487,12 @@ export async function startPresentationServer(
|
|
|
345
487
|
return;
|
|
346
488
|
}
|
|
347
489
|
const requested = typeof body.path === "string" ? body.path.trim() : "";
|
|
490
|
+
const requestedSourceMode =
|
|
491
|
+
body.sourceMode === undefined ? "snapshot" : body.sourceMode;
|
|
492
|
+
if (requestedSourceMode !== "snapshot" && requestedSourceMode !== "live") {
|
|
493
|
+
json(res, 400, { ok: false, error: "invalid_source_mode" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
348
496
|
if (!requested) {
|
|
349
497
|
json(res, 400, { ok: false, error: "path (string) is required" });
|
|
350
498
|
return;
|
|
@@ -366,29 +514,72 @@ export async function startPresentationServer(
|
|
|
366
514
|
json(res, 404, { ok: false, error: "file_not_found" });
|
|
367
515
|
return;
|
|
368
516
|
}
|
|
517
|
+
let sourceResult;
|
|
369
518
|
try {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
519
|
+
sourceResult = await runSourceOperation(async () => {
|
|
520
|
+
await session.openFile(canonical);
|
|
521
|
+
return setSourceMode(requestedSourceMode);
|
|
522
|
+
});
|
|
374
523
|
} catch (error) {
|
|
375
524
|
json(res, 400, { ok: false, error: error?.code || "import_failed" });
|
|
376
525
|
return;
|
|
377
526
|
}
|
|
378
|
-
|
|
379
|
-
|
|
527
|
+
broadcast(session);
|
|
528
|
+
json(res, sourceResult.ok ? 200 : 409, {
|
|
529
|
+
...sourceResult,
|
|
380
530
|
version: session.version,
|
|
381
531
|
index: session.index,
|
|
382
532
|
total: session.slides.length,
|
|
383
533
|
theme: session.theme,
|
|
384
534
|
sourceName: session.sourceName,
|
|
385
|
-
sourceMode
|
|
386
|
-
sourceWatchStatus
|
|
535
|
+
sourceMode,
|
|
536
|
+
sourceWatchStatus,
|
|
387
537
|
});
|
|
388
538
|
return;
|
|
389
539
|
}
|
|
390
540
|
|
|
391
|
-
if (
|
|
541
|
+
if (applicationMode && route === "/source-mode") {
|
|
542
|
+
if (req.method !== "POST") {
|
|
543
|
+
res.setHeader("Allow", "POST");
|
|
544
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (!sameOrigin()) {
|
|
548
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
let body;
|
|
552
|
+
try {
|
|
553
|
+
body = await readJsonBody(req);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
res.setHeader("Connection", "close");
|
|
556
|
+
json(res, error?.message === "payload_too_large" ? 413 : 400, {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: error?.message || "bad_request",
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!session.file) {
|
|
563
|
+
json(res, 409, { ok: false, error: "no_deck" });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (body.mode !== "snapshot" && body.mode !== "live") {
|
|
567
|
+
json(res, 400, { ok: false, error: "invalid_source_mode" });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const previous = sourceMode;
|
|
571
|
+
const result = await runSourceOperation(() => setSourceMode(body.mode));
|
|
572
|
+
json(res, result.ok ? 200 : 409, {
|
|
573
|
+
...result,
|
|
574
|
+
changed: result.ok ? previous !== sourceMode : false,
|
|
575
|
+
sourceMode,
|
|
576
|
+
sourceWatchStatus,
|
|
577
|
+
sourceWatchError,
|
|
578
|
+
});
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (editingAvailable && session.file && route === "/edit-mode") {
|
|
392
583
|
if (req.method !== "POST") {
|
|
393
584
|
res.setHeader("Allow", "POST");
|
|
394
585
|
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
@@ -423,7 +614,7 @@ export async function startPresentationServer(
|
|
|
423
614
|
return;
|
|
424
615
|
}
|
|
425
616
|
|
|
426
|
-
if (editingAvailable && route === "/edit") {
|
|
617
|
+
if (editingAvailable && session.file && route === "/edit") {
|
|
427
618
|
if (req.method !== "POST") {
|
|
428
619
|
res.setHeader("Allow", "POST");
|
|
429
620
|
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
@@ -444,78 +635,97 @@ export async function startPresentationServer(
|
|
|
444
635
|
});
|
|
445
636
|
return;
|
|
446
637
|
}
|
|
447
|
-
if (!session.architectureEdit) {
|
|
448
|
-
json(res, 409, { ok: false, error: "edit_mode_disabled" });
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
638
|
if (typeof body.source !== "string" || !body.source.trim()) {
|
|
452
639
|
json(res, 400, { ok: false, error: "source (string) is required" });
|
|
453
640
|
return;
|
|
454
641
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
} catch (error) {
|
|
498
|
-
json(res, 500, {
|
|
499
|
-
ok: false,
|
|
500
|
-
error: "source_reload_failed",
|
|
501
|
-
message: error?.message || "The saved deck could not be reloaded.",
|
|
642
|
+
const response = await runSourceOperation(async () => {
|
|
643
|
+
if (!session.architectureEdit) {
|
|
644
|
+
return {
|
|
645
|
+
status: 409,
|
|
646
|
+
body: { ok: false, error: "edit_mode_disabled" },
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const index = Number.isInteger(body.index) ? body.index : session.index;
|
|
650
|
+
const block = Number.isInteger(body.block) ? body.block : 0;
|
|
651
|
+
const deckVersion = Number.isInteger(body.deckVersion)
|
|
652
|
+
? body.deckVersion
|
|
653
|
+
: session.deckVersion;
|
|
654
|
+
if (index < 0 || index >= session.slides.length) {
|
|
655
|
+
return {
|
|
656
|
+
status: 400,
|
|
657
|
+
body: { ok: false, error: "index_out_of_range" },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (deckVersion !== session.deckVersion) {
|
|
661
|
+
return {
|
|
662
|
+
status: 409,
|
|
663
|
+
body: { ok: false, error: "deck_changed" },
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const globalBlock = importedArchitectureBlockIndex(
|
|
667
|
+
session.slides,
|
|
668
|
+
index,
|
|
669
|
+
block,
|
|
670
|
+
);
|
|
671
|
+
if (globalBlock === null) {
|
|
672
|
+
return {
|
|
673
|
+
status: 404,
|
|
674
|
+
body: { ok: false, error: "block_not_found" },
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const result = await saveArchitectureSource({
|
|
678
|
+
workspaceRoot: session.workspaceRoot,
|
|
679
|
+
sourcePath: session.sourceName,
|
|
680
|
+
sourceFile: session.file,
|
|
681
|
+
blockIndex: globalBlock,
|
|
682
|
+
source: body.source,
|
|
683
|
+
expectedMarkdown: session.sourceMarkdown,
|
|
502
684
|
});
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
685
|
+
if (!result.ok) {
|
|
686
|
+
const status =
|
|
687
|
+
result.error === "source_changed"
|
|
688
|
+
? 409
|
|
689
|
+
: result.error === "block_not_found"
|
|
690
|
+
? 404
|
|
691
|
+
: result.error === "source_file_too_large"
|
|
692
|
+
? 413
|
|
693
|
+
: result.error === "source_write_failed"
|
|
694
|
+
? 500
|
|
695
|
+
: 422;
|
|
696
|
+
return { status, body: result };
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
await session.load({ preserveIndex: true });
|
|
700
|
+
} catch (error) {
|
|
701
|
+
return {
|
|
702
|
+
status: 500,
|
|
703
|
+
body: {
|
|
704
|
+
ok: false,
|
|
705
|
+
error: "source_reload_failed",
|
|
706
|
+
message: error?.message || "The saved deck could not be reloaded.",
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
status: 200,
|
|
712
|
+
body: {
|
|
713
|
+
ok: true,
|
|
714
|
+
version: session.version,
|
|
715
|
+
deckVersion: session.deckVersion,
|
|
716
|
+
index,
|
|
717
|
+
block,
|
|
718
|
+
markdown: session.markdown,
|
|
719
|
+
fileSaved: true,
|
|
720
|
+
},
|
|
721
|
+
};
|
|
514
722
|
});
|
|
723
|
+
if (response.status === 200) broadcast(session);
|
|
724
|
+
json(res, response.status, response.body);
|
|
515
725
|
return;
|
|
516
726
|
}
|
|
517
727
|
|
|
518
|
-
if (editingAvailable && route === "/architecture-editor/open") {
|
|
728
|
+
if (editingAvailable && session.file && route === "/architecture-editor/open") {
|
|
519
729
|
if (req.method !== "POST") {
|
|
520
730
|
res.setHeader("Allow", "POST");
|
|
521
731
|
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
@@ -559,11 +769,13 @@ export async function startPresentationServer(
|
|
|
559
769
|
theme: session.theme,
|
|
560
770
|
logger: onLog,
|
|
561
771
|
onMarkdownSaved: async ({ sourcePath }) => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
772
|
+
await runSourceOperation(async () => {
|
|
773
|
+
if (resolve(session.workspaceRoot, sourcePath) !== resolve(session.file)) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
await session.load({ preserveIndex: true });
|
|
777
|
+
broadcast(session);
|
|
778
|
+
});
|
|
567
779
|
},
|
|
568
780
|
});
|
|
569
781
|
architectureEditors.set(key, entry);
|
|
@@ -608,6 +820,10 @@ export async function startPresentationServer(
|
|
|
608
820
|
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
609
821
|
return;
|
|
610
822
|
}
|
|
823
|
+
if (!session.slides.length) {
|
|
824
|
+
json(res, 409, { ok: false, error: "no_deck" });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
611
827
|
try {
|
|
612
828
|
const result = req.method === "POST" ? await presenter.open() : await presenter.close();
|
|
613
829
|
json(res, 200, { ok: true, ...result });
|
|
@@ -621,11 +837,59 @@ export async function startPresentationServer(
|
|
|
621
837
|
return;
|
|
622
838
|
}
|
|
623
839
|
|
|
840
|
+
if (applicationMode && (route === "/export" || route === "/export-pptx")) {
|
|
841
|
+
if (req.method !== "POST") {
|
|
842
|
+
res.setHeader("Allow", "POST");
|
|
843
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!sameOrigin()) {
|
|
847
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (!session.slides.length) {
|
|
851
|
+
json(res, 409, { ok: false, error: "no_deck" });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const pptx = route === "/export-pptx";
|
|
855
|
+
try {
|
|
856
|
+
const result = pptx
|
|
857
|
+
? await exportPptxImpl(
|
|
858
|
+
session,
|
|
859
|
+
pptxNameForSource(session.sourceName),
|
|
860
|
+
session.theme,
|
|
861
|
+
)
|
|
862
|
+
: await exportPdfImpl(
|
|
863
|
+
session,
|
|
864
|
+
pdfNameForSource(session.sourceName),
|
|
865
|
+
session.theme,
|
|
866
|
+
);
|
|
867
|
+
json(res, 200, result);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
json(
|
|
870
|
+
res,
|
|
871
|
+
error?.code === "no_deck" || error?.code === "export_in_progress"
|
|
872
|
+
? 409
|
|
873
|
+
: 500,
|
|
874
|
+
{
|
|
875
|
+
ok: false,
|
|
876
|
+
error:
|
|
877
|
+
error?.code || (pptx ? "pptx_export_failed" : "pdf_export_failed"),
|
|
878
|
+
message:
|
|
879
|
+
error?.message ||
|
|
880
|
+
(pptx ? "PowerPoint export failed." : "PDF export failed."),
|
|
881
|
+
},
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
624
887
|
// Routes that are unavailable in this CLI server mode stay explicit so the
|
|
625
888
|
// browser reports an actionable message instead of a bare 404.
|
|
626
889
|
if (
|
|
627
890
|
route === "/present" ||
|
|
628
891
|
route === "/export" ||
|
|
892
|
+
route === "/export-pptx" ||
|
|
629
893
|
route === "/edit" ||
|
|
630
894
|
route === "/edit-mode" ||
|
|
631
895
|
route === "/source-mode" ||
|
|
@@ -730,14 +994,23 @@ export async function startPresentationServer(
|
|
|
730
994
|
port = typeof address === "object" && address ? address.port : 0;
|
|
731
995
|
const url = `http://127.0.0.1:${port}${base}/`;
|
|
732
996
|
session.url = url;
|
|
997
|
+
const initialWatcher = bindSourceWatcher();
|
|
998
|
+
if (sourceMode === "live" && initialWatcher.ok) {
|
|
999
|
+
await runSourceOperation(() => reloadSourceNow(initialWatcher.generation));
|
|
1000
|
+
}
|
|
733
1001
|
|
|
734
1002
|
return {
|
|
735
1003
|
server,
|
|
736
1004
|
url,
|
|
737
1005
|
token,
|
|
738
1006
|
port,
|
|
1007
|
+
get sourceMode() {
|
|
1008
|
+
return sourceMode;
|
|
1009
|
+
},
|
|
739
1010
|
broadcast: () => broadcast(session),
|
|
740
1011
|
close: async () => {
|
|
1012
|
+
closeSourceWatcher();
|
|
1013
|
+
await sourceOperation.catch(() => {});
|
|
741
1014
|
await Promise.all(
|
|
742
1015
|
[...architectureEditors.values()].map(async (entry) =>
|
|
743
1016
|
(entry.editor ?? (await entry.promise.catch(() => null)))?.close().catch(() => {}),
|