@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
|
@@ -18,7 +18,10 @@ import { dirname, join, relative, resolve, sep } 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
|
+
import { importedArchitectureBlockIndex } from "../scripts/markdown-blocks.mjs";
|
|
21
22
|
import { isMarkdownPath, listMarkdownFiles } from "../scripts/markdown-files.mjs";
|
|
23
|
+
import { startArchitectureEditorServer } from "./architecture-editor-server.mjs";
|
|
24
|
+
import { saveArchitectureSource } from "./architecture-source.mjs";
|
|
22
25
|
import { isPathInside } from "./output-paths.mjs";
|
|
23
26
|
import { safeJoin, sendChunkedVendorAsset, sendFile } from "./static-files.mjs";
|
|
24
27
|
|
|
@@ -27,6 +30,7 @@ const EXT_DIR = resolve(RUNTIME_DIR, "..");
|
|
|
27
30
|
const VENDOR_DIR = join(EXT_DIR, "vendor");
|
|
28
31
|
const VENDOR_MANIFEST = join(VENDOR_DIR, "vendor-assets.lock.json");
|
|
29
32
|
const MAX_BODY_BYTES = 4096;
|
|
33
|
+
const MAX_EDIT_BODY_BYTES = 256 * 1024;
|
|
30
34
|
|
|
31
35
|
export function createUrlToken() {
|
|
32
36
|
return randomBytes(24).toString("base64url");
|
|
@@ -87,8 +91,13 @@ function broadcast(session) {
|
|
|
87
91
|
* Returns `{ url, token, port, close, broadcast }`. `session.url` is set to the
|
|
88
92
|
* token-scoped base URL so the shared output runtime can render from it.
|
|
89
93
|
*/
|
|
90
|
-
export async function startPresentationServer(
|
|
94
|
+
export async function startPresentationServer(
|
|
95
|
+
session,
|
|
96
|
+
{ editable = false, onLog, presenter, token = createUrlToken() } = {},
|
|
97
|
+
) {
|
|
91
98
|
const base = `/${token}`;
|
|
99
|
+
const architectureEditors = new Map();
|
|
100
|
+
const editingAvailable = editable === true;
|
|
92
101
|
let port = 0;
|
|
93
102
|
|
|
94
103
|
const server = createServer(async (req, res) => {
|
|
@@ -149,13 +158,21 @@ export async function startPresentationServer(session, { onLog, token = createUr
|
|
|
149
158
|
customThemeCss: session.customThemeCss,
|
|
150
159
|
customThemeMeta: session.customThemeMeta,
|
|
151
160
|
mode: session.mode,
|
|
152
|
-
sourceBacked:
|
|
153
|
-
|
|
161
|
+
sourceBacked: editingAvailable,
|
|
162
|
+
sourceModeAvailable: false,
|
|
163
|
+
sourceMode: editingAvailable ? "live" : "snapshot",
|
|
154
164
|
sourceWatchStatus: session.watchStatus || "inactive",
|
|
155
165
|
sourceWatchError: session.watchError || "",
|
|
156
|
-
presenterRunning:
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
presenterRunning: Boolean(presenter?.isRunning?.()),
|
|
167
|
+
presenterWindowAvailable: Boolean(presenter),
|
|
168
|
+
presenterViewAvailable: Boolean(presenter),
|
|
169
|
+
pdfExportAvailable: false,
|
|
170
|
+
pptxExportAvailable: false,
|
|
171
|
+
markdownImportAvailable: false,
|
|
172
|
+
architectureEditAvailable: editingAvailable,
|
|
173
|
+
architectureEdit: editingAvailable && Boolean(session.architectureEdit),
|
|
174
|
+
architectureDetailedEdit: editingAvailable,
|
|
175
|
+
architectureDetailedEditTarget: editingAvailable ? "window" : "",
|
|
159
176
|
});
|
|
160
177
|
return;
|
|
161
178
|
}
|
|
@@ -371,8 +388,241 @@ export async function startPresentationServer(session, { onLog, token = createUr
|
|
|
371
388
|
return;
|
|
372
389
|
}
|
|
373
390
|
|
|
374
|
-
|
|
375
|
-
|
|
391
|
+
if (editingAvailable && route === "/edit-mode") {
|
|
392
|
+
if (req.method !== "POST") {
|
|
393
|
+
res.setHeader("Allow", "POST");
|
|
394
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!sameOrigin()) {
|
|
398
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let body;
|
|
402
|
+
try {
|
|
403
|
+
body = await readJsonBody(req);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
res.setHeader("Connection", "close");
|
|
406
|
+
json(res, error?.message === "payload_too_large" ? 413 : 400, {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: error?.message || "bad_request",
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (typeof body.enabled !== "boolean") {
|
|
413
|
+
json(res, 400, { ok: false, error: "enabled (boolean) is required" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const changed = Boolean(session.architectureEdit) !== body.enabled;
|
|
417
|
+
session.architectureEdit = body.enabled;
|
|
418
|
+
json(res, 200, {
|
|
419
|
+
ok: true,
|
|
420
|
+
changed,
|
|
421
|
+
architectureEdit: session.architectureEdit,
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (editingAvailable && route === "/edit") {
|
|
427
|
+
if (req.method !== "POST") {
|
|
428
|
+
res.setHeader("Allow", "POST");
|
|
429
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!sameOrigin()) {
|
|
433
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
let body;
|
|
437
|
+
try {
|
|
438
|
+
body = await readJsonBody(req, MAX_EDIT_BODY_BYTES);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
res.setHeader("Connection", "close");
|
|
441
|
+
json(res, error?.message === "payload_too_large" ? 413 : 400, {
|
|
442
|
+
ok: false,
|
|
443
|
+
error: error?.message || "bad_request",
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!session.architectureEdit) {
|
|
448
|
+
json(res, 409, { ok: false, error: "edit_mode_disabled" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (typeof body.source !== "string" || !body.source.trim()) {
|
|
452
|
+
json(res, 400, { ok: false, error: "source (string) is required" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const index = Number.isInteger(body.index) ? body.index : session.index;
|
|
456
|
+
const block = Number.isInteger(body.block) ? body.block : 0;
|
|
457
|
+
const deckVersion = Number.isInteger(body.deckVersion)
|
|
458
|
+
? body.deckVersion
|
|
459
|
+
: session.deckVersion;
|
|
460
|
+
if (index < 0 || index >= session.slides.length) {
|
|
461
|
+
json(res, 400, { ok: false, error: "index_out_of_range" });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (deckVersion !== session.deckVersion) {
|
|
465
|
+
json(res, 409, { ok: false, error: "deck_changed" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const globalBlock = importedArchitectureBlockIndex(session.slides, index, block);
|
|
469
|
+
if (globalBlock === null) {
|
|
470
|
+
json(res, 404, { ok: false, error: "block_not_found" });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const result = await saveArchitectureSource({
|
|
474
|
+
workspaceRoot: session.workspaceRoot,
|
|
475
|
+
sourcePath: session.sourceName,
|
|
476
|
+
sourceFile: session.file,
|
|
477
|
+
blockIndex: globalBlock,
|
|
478
|
+
source: body.source,
|
|
479
|
+
expectedMarkdown: session.sourceMarkdown,
|
|
480
|
+
});
|
|
481
|
+
if (!result.ok) {
|
|
482
|
+
const status =
|
|
483
|
+
result.error === "source_changed"
|
|
484
|
+
? 409
|
|
485
|
+
: result.error === "block_not_found"
|
|
486
|
+
? 404
|
|
487
|
+
: result.error === "source_file_too_large"
|
|
488
|
+
? 413
|
|
489
|
+
: result.error === "source_write_failed"
|
|
490
|
+
? 500
|
|
491
|
+
: 422;
|
|
492
|
+
json(res, status, result);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await session.load({ preserveIndex: true });
|
|
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.",
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
broadcast(session);
|
|
506
|
+
json(res, 200, {
|
|
507
|
+
ok: true,
|
|
508
|
+
version: session.version,
|
|
509
|
+
deckVersion: session.deckVersion,
|
|
510
|
+
index,
|
|
511
|
+
block,
|
|
512
|
+
markdown: session.markdown,
|
|
513
|
+
fileSaved: true,
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (editingAvailable && route === "/architecture-editor/open") {
|
|
519
|
+
if (req.method !== "POST") {
|
|
520
|
+
res.setHeader("Allow", "POST");
|
|
521
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (!sameOrigin()) {
|
|
525
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
let body;
|
|
529
|
+
try {
|
|
530
|
+
body = await readJsonBody(req);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
json(res, error?.message === "payload_too_large" ? 413 : 400, {
|
|
533
|
+
ok: false,
|
|
534
|
+
error: error?.message || "bad_request",
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const slideIndex = Number.isInteger(body.index) ? body.index : session.index;
|
|
539
|
+
const blockIndex = Number.isInteger(body.block) ? body.block : 0;
|
|
540
|
+
const globalBlock = importedArchitectureBlockIndex(
|
|
541
|
+
session.slides,
|
|
542
|
+
slideIndex,
|
|
543
|
+
blockIndex,
|
|
544
|
+
);
|
|
545
|
+
if (globalBlock === null) {
|
|
546
|
+
json(res, 404, { ok: false, error: "block_not_found" });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const key = `${session.file}\0${globalBlock}`;
|
|
550
|
+
let entry = architectureEditors.get(key);
|
|
551
|
+
const refreshExisting = Boolean(entry?.editor);
|
|
552
|
+
if (!entry) {
|
|
553
|
+
entry = { editor: null };
|
|
554
|
+
entry.promise = startArchitectureEditorServer({
|
|
555
|
+
extensionDirectory: EXT_DIR,
|
|
556
|
+
workspaceRoot: session.workspaceRoot,
|
|
557
|
+
sourcePath: session.sourceName,
|
|
558
|
+
blockIndex: globalBlock,
|
|
559
|
+
theme: session.theme,
|
|
560
|
+
logger: onLog,
|
|
561
|
+
onMarkdownSaved: async ({ sourcePath }) => {
|
|
562
|
+
if (resolve(session.workspaceRoot, sourcePath) !== resolve(session.file)) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
await session.load({ preserveIndex: true });
|
|
566
|
+
broadcast(session);
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
architectureEditors.set(key, entry);
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const editor = entry.editor ?? (await entry.promise);
|
|
573
|
+
entry.editor = editor;
|
|
574
|
+
if (refreshExisting && !editor.dirty) {
|
|
575
|
+
await editor.reload(
|
|
576
|
+
{
|
|
577
|
+
sourcePath: session.sourceName,
|
|
578
|
+
blockIndex: globalBlock,
|
|
579
|
+
theme: session.theme,
|
|
580
|
+
},
|
|
581
|
+
{ discard: true },
|
|
582
|
+
);
|
|
583
|
+
} else {
|
|
584
|
+
editor.setTheme(session.theme);
|
|
585
|
+
}
|
|
586
|
+
json(res, 200, { ok: true, url: editor.url });
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (!entry.editor && architectureEditors.get(key) === entry) {
|
|
589
|
+
architectureEditors.delete(key);
|
|
590
|
+
}
|
|
591
|
+
json(res, error?.code === "block_not_found" ? 404 : 409, {
|
|
592
|
+
ok: false,
|
|
593
|
+
error: error?.code || "editor_open_failed",
|
|
594
|
+
message: error?.message || "Architecture Editor could not be opened.",
|
|
595
|
+
});
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (presenter && route === "/present") {
|
|
602
|
+
if (req.method !== "POST" && req.method !== "DELETE") {
|
|
603
|
+
res.setHeader("Allow", "POST, DELETE");
|
|
604
|
+
json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (!sameOrigin()) {
|
|
608
|
+
json(res, 403, { ok: false, error: "origin_not_allowed" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const result = req.method === "POST" ? await presenter.open() : await presenter.close();
|
|
613
|
+
json(res, 200, { ok: true, ...result });
|
|
614
|
+
} catch (error) {
|
|
615
|
+
json(res, 500, {
|
|
616
|
+
ok: false,
|
|
617
|
+
error: error?.code || "presenter_launch_failed",
|
|
618
|
+
message: error?.message || "The audience view could not be updated.",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Routes that are unavailable in this CLI server mode stay explicit so the
|
|
625
|
+
// browser reports an actionable message instead of a bare 404.
|
|
376
626
|
if (
|
|
377
627
|
route === "/present" ||
|
|
378
628
|
route === "/export" ||
|
|
@@ -487,8 +737,14 @@ export async function startPresentationServer(session, { onLog, token = createUr
|
|
|
487
737
|
token,
|
|
488
738
|
port,
|
|
489
739
|
broadcast: () => broadcast(session),
|
|
490
|
-
close: () =>
|
|
491
|
-
|
|
740
|
+
close: async () => {
|
|
741
|
+
await Promise.all(
|
|
742
|
+
[...architectureEditors.values()].map(async (entry) =>
|
|
743
|
+
(entry.editor ?? (await entry.promise.catch(() => null)))?.close().catch(() => {}),
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
architectureEditors.clear();
|
|
747
|
+
await new Promise((done) => {
|
|
492
748
|
for (const client of [...session.clients]) {
|
|
493
749
|
try {
|
|
494
750
|
client.end();
|
|
@@ -499,7 +755,7 @@ export async function startPresentationServer(session, { onLog, token = createUr
|
|
|
499
755
|
session.clients.clear();
|
|
500
756
|
server.close(() => done());
|
|
501
757
|
server.closeAllConnections?.();
|
|
502
|
-
})
|
|
758
|
+
});
|
|
759
|
+
},
|
|
503
760
|
};
|
|
504
761
|
}
|
|
505
|
-
|
package/src/cli.mjs
CHANGED
|
@@ -36,11 +36,12 @@ import {
|
|
|
36
36
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
37
|
|
|
38
38
|
const COMMANDS = [
|
|
39
|
+
["presentation", "Open presenter view and launch the audience view from it."],
|
|
39
40
|
["present", "Serve a deck on loopback and open it in a browser window."],
|
|
40
41
|
["validate", "Check deck structure, Architecture DSL blocks, and themes."],
|
|
41
42
|
["inspect", "Report 1280x720 clipping diagnostics for a deck."],
|
|
42
43
|
["capture", "Write 1280x720 PNG files for selected or clipped slides."],
|
|
43
|
-
["export", "Export the deck as
|
|
44
|
+
["export", "Export the deck as PDF or editable PowerPoint."],
|
|
44
45
|
["guide", "Print the canonical MarkdStage authoring guide."],
|
|
45
46
|
["skill", "Install or check the portable MarkdStage Agent Skills."],
|
|
46
47
|
["help", "Show help for MarkdStage or for one command."],
|
|
@@ -58,6 +59,7 @@ const GLOBAL_OPTIONS = {
|
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
const COMMAND_OPTIONS = {
|
|
62
|
+
presentation: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
|
|
61
63
|
present: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
|
|
62
64
|
validate: {},
|
|
63
65
|
inspect: { slide: { type: "string" }, all: { type: "boolean" }, "fail-on-issues": { type: "boolean" } },
|
|
@@ -101,12 +103,26 @@ function usage(command) {
|
|
|
101
103
|
return lines.join("\n");
|
|
102
104
|
}
|
|
103
105
|
const help = {
|
|
106
|
+
presentation: [
|
|
107
|
+
"Usage: markdstage presentation <file.md> [options]",
|
|
108
|
+
"",
|
|
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.",
|
|
111
|
+
"",
|
|
112
|
+
" --watch Reload on save.",
|
|
113
|
+
" --no-open Serve the presenter view without launching a browser.",
|
|
114
|
+
"",
|
|
115
|
+
"Presentation requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
116
|
+
],
|
|
104
117
|
present: [
|
|
105
118
|
"Usage: markdstage present <file.md> [options]",
|
|
106
119
|
"",
|
|
107
|
-
" --watch Reload
|
|
120
|
+
" --watch Reload on save and enable Architecture editing.",
|
|
108
121
|
" --no-open Serve the deck without launching a browser.",
|
|
109
122
|
"",
|
|
123
|
+
"Without --watch, presentation is read-only. Watch mode starts in normal viewing mode;",
|
|
124
|
+
"use the pencil control to edit Architecture diagrams and open the detailed designer.",
|
|
125
|
+
"",
|
|
110
126
|
"Presenting requires an installed Microsoft Edge, Google Chrome, or Chromium.",
|
|
111
127
|
],
|
|
112
128
|
validate: [
|
|
@@ -130,9 +146,10 @@ function usage(command) {
|
|
|
130
146
|
"Without --pages only the slides reported as clipped are captured.",
|
|
131
147
|
],
|
|
132
148
|
export: [
|
|
133
|
-
"Usage: markdstage export <file.md> [--output slides.pdf]",
|
|
149
|
+
"Usage: markdstage export <file.md> [--output slides.pdf|slides.pptx]",
|
|
134
150
|
"",
|
|
135
|
-
"Produces the same 16:9 PDF as the MarkdStage canvas.",
|
|
151
|
+
"Produces the same 16:9 PDF or hybrid editable PowerPoint as the MarkdStage canvas.",
|
|
152
|
+
"The output extension selects the format; omitting --output keeps PDF as the default.",
|
|
136
153
|
],
|
|
137
154
|
guide: [
|
|
138
155
|
"Usage: markdstage guide [topic] [--json]",
|
|
@@ -259,6 +276,27 @@ export async function run(argv, io = {}) {
|
|
|
259
276
|
if (values.json) json(report);
|
|
260
277
|
return EXIT_OK;
|
|
261
278
|
}
|
|
279
|
+
case "presentation": {
|
|
280
|
+
const file = requireFile(positionals, "presentation");
|
|
281
|
+
const report = await presentCommand(
|
|
282
|
+
{
|
|
283
|
+
...deckOptions(file, values),
|
|
284
|
+
watch: values.watch,
|
|
285
|
+
open: !values["no-open"],
|
|
286
|
+
presenterView: true,
|
|
287
|
+
until: io.until,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
print: values.json ? () => {} : (message) => out(message),
|
|
291
|
+
status: (message, isError) => {
|
|
292
|
+
if (isError) err(message);
|
|
293
|
+
else if (!values.json) out(message);
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
if (values.json) json(report);
|
|
298
|
+
return EXIT_OK;
|
|
299
|
+
}
|
|
262
300
|
case "validate": {
|
|
263
301
|
const file = requireFile(positionals, "validate");
|
|
264
302
|
const report = await validateCommand(deckOptions(file, values));
|
package/src/commands/export.mjs
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
|
-
// markdstage export — produce the same
|
|
1
|
+
// markdstage export — produce the same PDF or editable PowerPoint as the Canvas Extension.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
exportPdf,
|
|
7
|
+
exportPptx,
|
|
8
|
+
pdfNameForSource,
|
|
9
|
+
pptxNameForSource,
|
|
10
|
+
} from "../runtime.mjs";
|
|
4
11
|
import { withDeckServer } from "../deck.mjs";
|
|
5
12
|
|
|
6
|
-
export async function exportCommand(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-
|
|
13
|
+
export async function exportCommand(
|
|
14
|
+
options,
|
|
15
|
+
exporters = { pdf: exportPdf, pptx: exportPptx },
|
|
16
|
+
) {
|
|
17
|
+
return withDeckServer(options, async (session) => {
|
|
18
|
+
const requested = options.output || pdfNameForSource(session.sourceName);
|
|
19
|
+
const extension = extname(requested).toLowerCase();
|
|
20
|
+
if (extension === ".pptx") {
|
|
21
|
+
return exporters.pptx(
|
|
22
|
+
session,
|
|
23
|
+
options.output || pptxNameForSource(session.sourceName),
|
|
24
|
+
options.theme,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return exporters.pdf(session, requested, options.theme);
|
|
28
|
+
});
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
export function formatExportReport(report) {
|
|
17
|
-
|
|
32
|
+
const format = report.format === "pptx" ? "PowerPoint" : "PDF";
|
|
33
|
+
const fallback =
|
|
34
|
+
report.format === "pptx" && report.fallbackCount
|
|
35
|
+
? `, ${report.fallbackCount} fallback item(s)`
|
|
36
|
+
: "";
|
|
37
|
+
return `Exported ${report.total} slide(s) to ${report.path} (${report.bytes} bytes, ${format}, theme ${report.theme}${fallback}).`;
|
|
18
38
|
}
|