@markdstage/markdstage 0.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -413,7 +413,7 @@ body.presenter-mode .nav{opacity:0;}
413
413
  body.presenter-mode .nav:hover,body.presenter-mode .nav:focus-within{opacity:1;}
414
414
  body.presenter-mode #navPresent{display:none;}
415
415
  body.presenter-mode #navPresenterView{display:none;}
416
- body.presenter-mode #navExport{display:none;}
416
+ body.presenter-mode #navExport,body.presenter-mode #navExportPptx{display:none;}
417
417
  body.presenter-mode #navImport{display:none;}
418
418
  body.presenter-mode #navSourceMode{display:none;}
419
419
  body.preview-mode .nav,body.preview-mode .overview{display:none!important;}
@@ -566,7 +566,7 @@ body.fixed-output-mode .deck.backcover-slide{background:var(--backcover-bg,var(-
566
566
  body.fixed-output-mode .deck::before{position:absolute;}
567
567
  body.fixed-output-mode .body{overflow:hidden;}
568
568
  body.fixed-output-mode .kicker{font-size:12px;}
569
- body.fixed-output-mode img{max-height:346px;}
569
+ body.fixed-output-mode img:not(.theme-cover-background){max-height:346px;}
570
570
  body.fixed-output-mode pre.mermaid svg{max-height:317px;}
571
571
  body.fixed-output-mode .architecture-svg{max-height:504px;}
572
572
  body.fixed-output-mode footer{gap:.65em;font-size:11px;margin-top:16px;}
@@ -583,6 +583,46 @@ body.capture-mode .nav,body.capture-mode .overview,body.capture-mode .layout-war
583
583
  display:none!important;
584
584
  }
585
585
 
586
+ /* ===== Headless hybrid PowerPoint export =====
587
+ The complete deck is stacked at the same 1280x720 geometry as PDF output.
588
+ Native objects become transparent only after renderer.js has measured them,
589
+ leaving backgrounds, decoration, Mermaid, code, and other fallback artwork. */
590
+ html:has(body.pptx-mode),body.pptx-mode{width:1280px;height:auto;overflow:visible;}
591
+ body.pptx-mode{background:none;}
592
+ body.pptx-mode #stage{display:block;width:1280px;height:auto;}
593
+ body.pptx-mode .deck{display:flex;}
594
+ body.pptx-mode .nav,body.pptx-mode .overview,body.pptx-mode .layout-warning,
595
+ body.pptx-mode .mermaidTooltip{display:none!important;}
596
+ body.pptx-artwork-mode [data-pptx-native="text"],
597
+ body.pptx-artwork-mode [data-pptx-native="text"] *,
598
+ body.pptx-artwork-mode [data-pptx-native="table"],
599
+ body.pptx-artwork-mode [data-pptx-native="table"] *{
600
+ color:transparent!important;
601
+ -webkit-text-fill-color:transparent!important;
602
+ text-decoration-color:transparent!important;
603
+ caret-color:transparent!important;
604
+ }
605
+ body.pptx-artwork-mode li[data-pptx-native="text"]::marker{color:transparent!important;}
606
+ body.pptx-artwork-mode [data-pptx-native="table"]{visibility:hidden!important;}
607
+ body.pptx-artwork-mode img[data-pptx-native="image"]{
608
+ object-fit:none!important;
609
+ object-position:99999px 99999px!important;
610
+ }
611
+ body.pptx-artwork-mode svg [data-pptx-native="shape"],
612
+ body.pptx-artwork-mode svg [data-pptx-native="shape"] *,
613
+ body.pptx-artwork-mode svg [data-pptx-native="text"],
614
+ body.pptx-artwork-mode svg [data-pptx-native="text"] *,
615
+ body.pptx-artwork-mode svg [data-pptx-native="connector"],
616
+ body.pptx-artwork-mode svg [data-pptx-native="connector"] *,
617
+ body.pptx-artwork-mode svg [data-pptx-native="table"]{
618
+ fill:transparent!important;
619
+ stroke:transparent!important;
620
+ color:transparent!important;
621
+ -webkit-text-fill-color:transparent!important;
622
+ }
623
+ body.pptx-artwork-mode svg path[data-pptx-native="connector"]{marker-end:none!important;}
624
+ body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
625
+
586
626
  /* ===== Headless PDF export ===== */
587
627
  @page{size:13.333333in 7.5in;margin:0;}
588
628
  @media print{
@@ -0,0 +1,651 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { join, resolve, sep } from "node:path";
4
+
5
+ import { parseArchitecture } from "../renderer/architecture.mjs";
6
+ import {
7
+ ARCHITECTURE_ASSET_MAX_BYTES,
8
+ importArchitectureAsset,
9
+ listArchitectureAssets,
10
+ } from "../scripts/architecture-assets.mjs";
11
+ import { resolveAssetFile } from "../scripts/asset-paths.mjs";
12
+ import {
13
+ readArchitectureSourceTarget,
14
+ saveArchitectureSource,
15
+ } from "./architecture-source.mjs";
16
+ import { sendFile } from "./static-files.mjs";
17
+
18
+ const MAX_DRAFT_BYTES = 256 * 1024;
19
+ const THEMES = new Set(["dark", "light", "microsoft"]);
20
+
21
+ function normalizeTheme(value) {
22
+ return THEMES.has(value) ? value : "dark";
23
+ }
24
+
25
+ function sendJson(res, status, body) {
26
+ res.statusCode = status;
27
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
28
+ res.setHeader("Cache-Control", "no-store");
29
+ res.end(JSON.stringify(body));
30
+ }
31
+
32
+ function readJsonBody(req, limit = MAX_DRAFT_BYTES) {
33
+ return new Promise((resolveBody, rejectBody) => {
34
+ const chunks = [];
35
+ let size = 0;
36
+ let settled = false;
37
+ const settle = (callback, value) => {
38
+ if (settled) return;
39
+ settled = true;
40
+ callback(value);
41
+ };
42
+ req.on("data", (chunk) => {
43
+ if (settled) return;
44
+ size += chunk.length;
45
+ if (size > limit) {
46
+ settle(rejectBody, new Error("payload_too_large"));
47
+ return;
48
+ }
49
+ chunks.push(chunk);
50
+ });
51
+ req.on("end", () => {
52
+ if (settled) return;
53
+ try {
54
+ settle(resolveBody, JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
55
+ } catch (_) {
56
+ settle(rejectBody, new Error("invalid_json"));
57
+ }
58
+ });
59
+ req.on("error", (error) => settle(rejectBody, error));
60
+ });
61
+ }
62
+
63
+ function readBinaryBody(req, limit = ARCHITECTURE_ASSET_MAX_BYTES) {
64
+ return new Promise((resolveBody, rejectBody) => {
65
+ const declared = Number(req.headers["content-length"]);
66
+ if (Number.isFinite(declared) && declared > limit) {
67
+ rejectBody(new Error("asset_too_large"));
68
+ return;
69
+ }
70
+ const chunks = [];
71
+ let size = 0;
72
+ let settled = false;
73
+ const settle = (callback, value) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ callback(value);
77
+ };
78
+ req.on("data", (chunk) => {
79
+ if (settled) return;
80
+ size += chunk.length;
81
+ if (size > limit) {
82
+ settle(rejectBody, new Error("asset_too_large"));
83
+ return;
84
+ }
85
+ chunks.push(chunk);
86
+ });
87
+ req.on("end", () => {
88
+ if (!settled) settle(resolveBody, Buffer.concat(chunks));
89
+ });
90
+ req.on("error", (error) => settle(rejectBody, error));
91
+ });
92
+ }
93
+
94
+ function assetErrorStatus(error) {
95
+ if (error?.code === "asset_too_large" || error?.message === "asset_too_large") return 413;
96
+ if (
97
+ [
98
+ "asset_content_type_mismatch",
99
+ "asset_signature_mismatch",
100
+ "unsupported_asset_type",
101
+ ].includes(error?.code)
102
+ ) {
103
+ return 415;
104
+ }
105
+ if (
106
+ [
107
+ "asset_root_outside_workspace",
108
+ "invalid_asset_path",
109
+ "asset_source_outside_workspace",
110
+ "asset_outside_workspace",
111
+ ].includes(error?.code)
112
+ ) {
113
+ return 403;
114
+ }
115
+ if (error?.code === "asset_write_failed" || error?.code === "asset_root_unavailable") return 500;
116
+ return 400;
117
+ }
118
+
119
+ function hostAllowed(req, port) {
120
+ const host = req.headers.host;
121
+ return (
122
+ host === `127.0.0.1:${port}` ||
123
+ host === `localhost:${port}` ||
124
+ host === `[::1]:${port}`
125
+ );
126
+ }
127
+
128
+ function editorSaveStatus(result) {
129
+ if (["source_changed", "stale_generation", "stale_revision"].includes(result.error)) return 409;
130
+ if (result.error === "source_file_too_large") return 413;
131
+ if (result.error === "source_write_failed") return 500;
132
+ return 422;
133
+ }
134
+
135
+ export async function startArchitectureEditorServer({
136
+ extensionDirectory,
137
+ workspaceRoot,
138
+ sourcePath,
139
+ blockIndex = 0,
140
+ theme = "dark",
141
+ onMarkdownSaved,
142
+ logger,
143
+ token = randomBytes(24).toString("base64url"),
144
+ } = {}) {
145
+ const inst = {
146
+ workspaceRoot: resolve(workspaceRoot),
147
+ sourcePath: "",
148
+ sourceFile: "",
149
+ sourceMode: 0,
150
+ blockIndex: 0,
151
+ baseMarkdown: "",
152
+ savedSource: "",
153
+ draftSource: "",
154
+ draftRevision: 0,
155
+ generation: 0,
156
+ dirty: false,
157
+ version: 0,
158
+ theme: normalizeTheme(theme),
159
+ clients: new Set(),
160
+ };
161
+ const base = `/${token}`;
162
+ let port = 0;
163
+ let closed = false;
164
+ let operationQueue = Promise.resolve();
165
+
166
+ const log = (message, level = "warning") => {
167
+ if (typeof logger === "function") logger(message, level);
168
+ };
169
+
170
+ const broadcast = () => {
171
+ for (const client of [...inst.clients]) {
172
+ try {
173
+ client.write(`data: ${inst.version}\n\n`);
174
+ } catch (_) {
175
+ inst.clients.delete(client);
176
+ }
177
+ }
178
+ };
179
+
180
+ const serializeOperation = (operation) => {
181
+ const current = operationQueue.catch(() => {}).then(operation);
182
+ operationQueue = current;
183
+ return current;
184
+ };
185
+
186
+ const loadTarget = async (input, { discard = false } = {}) => {
187
+ if (inst.dirty && !discard) {
188
+ const error = new Error(
189
+ "The editor has unsaved changes. Save or explicitly discard them before reloading.",
190
+ );
191
+ error.code = "unsaved_changes";
192
+ throw error;
193
+ }
194
+ const nextSourcePath = String(input?.sourcePath || "").trim();
195
+ const nextBlockIndex = input?.blockIndex ?? 0;
196
+ const target = await readArchitectureSourceTarget(
197
+ inst.workspaceRoot,
198
+ nextSourcePath,
199
+ nextBlockIndex,
200
+ );
201
+ inst.sourcePath = target.relativePath;
202
+ inst.sourceFile = target.path;
203
+ inst.sourceMode = target.mode;
204
+ inst.blockIndex = nextBlockIndex;
205
+ inst.baseMarkdown = target.markdown;
206
+ inst.savedSource = target.source;
207
+ inst.draftSource = target.source;
208
+ inst.draftRevision = 0;
209
+ inst.generation += 1;
210
+ inst.dirty = false;
211
+ inst.theme = normalizeTheme(input?.theme);
212
+ inst.version += 1;
213
+ broadcast();
214
+ };
215
+
216
+ const snapshotSaveRequest = () => ({
217
+ sourceFile: inst.sourceFile,
218
+ sourcePath: inst.sourcePath,
219
+ blockIndex: inst.blockIndex,
220
+ baseMarkdown: inst.baseMarkdown,
221
+ source: inst.draftSource,
222
+ revision: inst.draftRevision,
223
+ generation: inst.generation,
224
+ });
225
+
226
+ const saveInstance = (request) =>
227
+ serializeOperation(async () => {
228
+ if (
229
+ inst.generation !== request.generation ||
230
+ resolve(inst.sourceFile) !== resolve(request.sourceFile)
231
+ ) {
232
+ return {
233
+ ok: false,
234
+ error: "stale_generation",
235
+ message: "The editor target was reloaded. Refresh before saving.",
236
+ };
237
+ }
238
+ const result = await saveArchitectureSource({
239
+ workspaceRoot: inst.workspaceRoot,
240
+ sourcePath: request.sourcePath,
241
+ sourceFile: request.sourceFile,
242
+ blockIndex: request.blockIndex,
243
+ source: request.source,
244
+ expectedMarkdown: request.baseMarkdown,
245
+ });
246
+ if (!result.ok) return result;
247
+
248
+ inst.baseMarkdown = result.markdown;
249
+ inst.savedSource = request.source;
250
+ inst.dirty = inst.draftSource !== inst.savedSource;
251
+ inst.version += 1;
252
+ broadcast();
253
+ const response = {
254
+ ok: true,
255
+ sourcePath: result.sourcePath,
256
+ blockIndex: request.blockIndex,
257
+ generation: request.generation,
258
+ savedRevision: request.revision,
259
+ dirty: inst.dirty,
260
+ version: inst.version,
261
+ };
262
+ if (typeof onMarkdownSaved === "function") {
263
+ try {
264
+ await onMarkdownSaved({
265
+ workspaceRoot: inst.workspaceRoot,
266
+ sourcePath: result.sourcePath,
267
+ markdown: result.markdown,
268
+ });
269
+ } catch (error) {
270
+ log(`architecture-editor: MarkdStage sync failed: ${error?.message || error}`);
271
+ }
272
+ }
273
+ return response;
274
+ });
275
+
276
+ const reloadTarget = (input = {}, options = {}) =>
277
+ serializeOperation(() =>
278
+ loadTarget(
279
+ {
280
+ sourcePath: input.sourcePath ?? inst.sourcePath,
281
+ blockIndex: input.blockIndex ?? inst.blockIndex,
282
+ theme: input.theme ?? inst.theme,
283
+ },
284
+ options,
285
+ ),
286
+ );
287
+
288
+ await loadTarget({ sourcePath, blockIndex, theme });
289
+
290
+ const editorDir = join(extensionDirectory, "architecture-editor");
291
+ const rendererDir = join(extensionDirectory, "renderer");
292
+ const server = createServer(async (req, res) => {
293
+ res.setHeader("X-Content-Type-Options", "nosniff");
294
+ res.setHeader("Referrer-Policy", "no-referrer");
295
+ if (!hostAllowed(req, port)) {
296
+ res.statusCode = 403;
297
+ res.end("Forbidden");
298
+ return;
299
+ }
300
+
301
+ let requestUrl;
302
+ let pathname;
303
+ try {
304
+ requestUrl = new URL(req.url, `http://127.0.0.1:${port}`);
305
+ pathname = decodeURIComponent(requestUrl.pathname);
306
+ } catch (_) {
307
+ res.statusCode = 400;
308
+ res.end("Bad request");
309
+ return;
310
+ }
311
+ if (pathname !== base && !pathname.startsWith(`${base}/`)) {
312
+ res.statusCode = 404;
313
+ res.end("Not found");
314
+ return;
315
+ }
316
+ const route = pathname.slice(base.length) || "/";
317
+ const sameOrigin = () => {
318
+ const origin = req.headers.origin;
319
+ return !origin || origin === `http://127.0.0.1:${port}`;
320
+ };
321
+
322
+ if (route === "/" || route === "/index.html") {
323
+ await sendFile(res, join(editorDir, "index.html"));
324
+ return;
325
+ }
326
+ if (route === "/state") {
327
+ sendJson(res, 200, {
328
+ version: inst.version,
329
+ sourcePath: inst.sourcePath.split(sep).join("/"),
330
+ blockIndex: inst.blockIndex,
331
+ source: inst.draftSource,
332
+ generation: inst.generation,
333
+ draftRevision: inst.draftRevision,
334
+ dirty: inst.dirty,
335
+ theme: inst.theme,
336
+ });
337
+ return;
338
+ }
339
+ if (route === "/events") {
340
+ res.statusCode = 200;
341
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
342
+ res.setHeader("Cache-Control", "no-store");
343
+ res.setHeader("Connection", "keep-alive");
344
+ res.write("retry: 2000\n\n");
345
+ res.write(`data: ${inst.version}\n\n`);
346
+ inst.clients.add(res);
347
+ const heartbeat = setInterval(() => {
348
+ try {
349
+ res.write(": ping\n\n");
350
+ } catch (_) {
351
+ inst.clients.delete(res);
352
+ }
353
+ }, 15_000);
354
+ req.on("close", () => {
355
+ clearInterval(heartbeat);
356
+ inst.clients.delete(res);
357
+ });
358
+ return;
359
+ }
360
+ if (route === "/asset-library") {
361
+ if (req.method !== "GET") {
362
+ res.setHeader("Allow", "GET");
363
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
364
+ return;
365
+ }
366
+ try {
367
+ const assets = await listArchitectureAssets(inst.workspaceRoot, inst.sourcePath);
368
+ sendJson(res, 200, { ok: true, assets });
369
+ } catch (error) {
370
+ sendJson(res, assetErrorStatus(error), {
371
+ ok: false,
372
+ error: error?.code || "asset_list_failed",
373
+ message: error?.message || "Images could not be listed.",
374
+ });
375
+ }
376
+ return;
377
+ }
378
+ if (route === "/asset-upload") {
379
+ if (req.method !== "POST") {
380
+ res.setHeader("Allow", "POST");
381
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
382
+ return;
383
+ }
384
+ if (!sameOrigin()) {
385
+ sendJson(res, 403, { ok: false, error: "origin_not_allowed" });
386
+ return;
387
+ }
388
+ try {
389
+ const content = await readBinaryBody(req);
390
+ const imported = await importArchitectureAsset(inst.workspaceRoot, {
391
+ filename: requestUrl.searchParams.get("name") || "",
392
+ contentType: req.headers["content-type"],
393
+ content,
394
+ });
395
+ sendJson(res, 201, { ok: true, asset: imported });
396
+ } catch (error) {
397
+ sendJson(res, assetErrorStatus(error), {
398
+ ok: false,
399
+ error: error?.code || error?.message || "asset_upload_failed",
400
+ message: error?.message || "The image could not be imported.",
401
+ });
402
+ }
403
+ return;
404
+ }
405
+ if (route === "/draft") {
406
+ if (req.method !== "POST") {
407
+ res.setHeader("Allow", "POST");
408
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
409
+ return;
410
+ }
411
+ if (!sameOrigin()) {
412
+ sendJson(res, 403, { ok: false, error: "origin_not_allowed" });
413
+ return;
414
+ }
415
+ let body;
416
+ try {
417
+ body = await readJsonBody(req);
418
+ } catch (error) {
419
+ sendJson(res, error?.message === "payload_too_large" ? 413 : 400, {
420
+ ok: false,
421
+ error: error?.message || "bad_request",
422
+ });
423
+ return;
424
+ }
425
+ if (
426
+ typeof body.source !== "string" ||
427
+ !Number.isInteger(body.generation) ||
428
+ !Number.isInteger(body.revision) ||
429
+ body.revision < 0
430
+ ) {
431
+ sendJson(res, 400, { ok: false, error: "invalid_draft" });
432
+ return;
433
+ }
434
+ if (body.generation !== inst.generation) {
435
+ sendJson(res, 409, {
436
+ ok: false,
437
+ error: "stale_generation",
438
+ message: "The editor target was reloaded. Refresh before editing.",
439
+ generation: inst.generation,
440
+ revision: inst.draftRevision,
441
+ dirty: inst.dirty,
442
+ version: inst.version,
443
+ });
444
+ return;
445
+ }
446
+ try {
447
+ parseArchitecture(body.source);
448
+ } catch (error) {
449
+ sendJson(res, 422, {
450
+ ok: false,
451
+ error: "invalid_architecture",
452
+ message: error?.message || "Invalid Architecture DSL.",
453
+ });
454
+ return;
455
+ }
456
+ if (body.revision <= inst.draftRevision) {
457
+ sendJson(res, 409, {
458
+ ok: false,
459
+ error: "stale_draft",
460
+ message: "This draft revision is stale. Reload the editor state before editing.",
461
+ revision: inst.draftRevision,
462
+ dirty: inst.dirty,
463
+ version: inst.version,
464
+ });
465
+ return;
466
+ }
467
+ inst.draftRevision = body.revision;
468
+ inst.draftSource = body.source;
469
+ inst.dirty = inst.draftSource !== inst.savedSource;
470
+ inst.version += 1;
471
+ broadcast();
472
+ sendJson(res, 200, {
473
+ ok: true,
474
+ revision: inst.draftRevision,
475
+ dirty: inst.dirty,
476
+ version: inst.version,
477
+ });
478
+ return;
479
+ }
480
+ if (route === "/save") {
481
+ if (req.method !== "POST") {
482
+ res.setHeader("Allow", "POST");
483
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
484
+ return;
485
+ }
486
+ if (!sameOrigin()) {
487
+ sendJson(res, 403, { ok: false, error: "origin_not_allowed" });
488
+ return;
489
+ }
490
+ let body;
491
+ try {
492
+ body = await readJsonBody(req, 4096);
493
+ } catch (error) {
494
+ sendJson(res, error?.message === "payload_too_large" ? 413 : 400, {
495
+ ok: false,
496
+ error: error?.message || "bad_request",
497
+ });
498
+ return;
499
+ }
500
+ if (!Number.isInteger(body.generation) || body.generation !== inst.generation) {
501
+ sendJson(res, 409, {
502
+ ok: false,
503
+ error: "stale_generation",
504
+ message: "The editor target was reloaded. Refresh before saving.",
505
+ });
506
+ return;
507
+ }
508
+ if (!Number.isInteger(body.revision) || body.revision !== inst.draftRevision) {
509
+ sendJson(res, 409, {
510
+ ok: false,
511
+ error: "stale_revision",
512
+ message: "The draft changed before saving. Save again to use the latest revision.",
513
+ });
514
+ return;
515
+ }
516
+ const result = await saveInstance(snapshotSaveRequest());
517
+ sendJson(res, result.ok ? 200 : editorSaveStatus(result), result);
518
+ return;
519
+ }
520
+ if (route === "/reload") {
521
+ if (req.method !== "POST") {
522
+ res.setHeader("Allow", "POST");
523
+ sendJson(res, 405, { ok: false, error: "method_not_allowed" });
524
+ return;
525
+ }
526
+ if (!sameOrigin()) {
527
+ sendJson(res, 403, { ok: false, error: "origin_not_allowed" });
528
+ return;
529
+ }
530
+ let body;
531
+ try {
532
+ body = await readJsonBody(req, 4096);
533
+ } catch (error) {
534
+ sendJson(res, error?.message === "payload_too_large" ? 413 : 400, {
535
+ ok: false,
536
+ error: error?.message || "bad_request",
537
+ });
538
+ return;
539
+ }
540
+ if ("discard" in body && typeof body.discard !== "boolean") {
541
+ sendJson(res, 400, { ok: false, error: "invalid_reload" });
542
+ return;
543
+ }
544
+ try {
545
+ await reloadTarget({}, { discard: body.discard === true });
546
+ sendJson(res, 200, {
547
+ ok: true,
548
+ version: inst.version,
549
+ generation: inst.generation,
550
+ dirty: inst.dirty,
551
+ });
552
+ } catch (error) {
553
+ const status =
554
+ error?.code === "unsaved_changes"
555
+ ? 409
556
+ : error?.code === "source_file_too_large"
557
+ ? 413
558
+ : error?.code === "source_file_not_found" || error?.code === "block_not_found"
559
+ ? 404
560
+ : 422;
561
+ sendJson(res, status, {
562
+ ok: false,
563
+ error: error?.code || "reload_failed",
564
+ message: error?.message || "The source Markdown could not be reloaded.",
565
+ });
566
+ }
567
+ return;
568
+ }
569
+ if (route === "/editor/editor.js") {
570
+ await sendFile(res, join(editorDir, "editor.js"));
571
+ return;
572
+ }
573
+ if (route === "/editor/editor.css") {
574
+ await sendFile(res, join(editorDir, "editor.css"));
575
+ return;
576
+ }
577
+ if (route === "/renderer/slides.css") {
578
+ await sendFile(res, join(rendererDir, "slides.css"));
579
+ return;
580
+ }
581
+ if (
582
+ [
583
+ "/renderer/architecture.mjs",
584
+ "/renderer/architecture-edit.mjs",
585
+ "/renderer/architecture-document.mjs",
586
+ ].includes(route)
587
+ ) {
588
+ await sendFile(res, join(extensionDirectory, route.replace(/^\//, "")));
589
+ return;
590
+ }
591
+ if (route.startsWith("/assets/")) {
592
+ try {
593
+ const asset = await resolveAssetFile(
594
+ inst.workspaceRoot,
595
+ inst.sourcePath,
596
+ route.slice("/assets/".length),
597
+ );
598
+ if (!asset) {
599
+ res.statusCode = 404;
600
+ res.end("Not found");
601
+ return;
602
+ }
603
+ await sendFile(res, asset, { cache: true });
604
+ } catch (error) {
605
+ res.statusCode = assetErrorStatus(error);
606
+ res.end(res.statusCode === 403 ? "Forbidden" : "Not found");
607
+ }
608
+ return;
609
+ }
610
+ res.statusCode = 404;
611
+ res.end("Not found");
612
+ });
613
+
614
+ await new Promise((ready) => server.listen(0, "127.0.0.1", ready));
615
+ const address = server.address();
616
+ port = typeof address === "object" && address ? address.port : 0;
617
+ const url = `http://127.0.0.1:${port}${base}/`;
618
+
619
+ return {
620
+ url,
621
+ get sourcePath() {
622
+ return inst.sourcePath;
623
+ },
624
+ get blockIndex() {
625
+ return inst.blockIndex;
626
+ },
627
+ get dirty() {
628
+ return inst.dirty;
629
+ },
630
+ get version() {
631
+ return inst.version;
632
+ },
633
+ setTheme(nextTheme) {
634
+ inst.theme = normalizeTheme(nextTheme);
635
+ },
636
+ save() {
637
+ return saveInstance(snapshotSaveRequest());
638
+ },
639
+ reload(input = {}, options = {}) {
640
+ return reloadTarget(input, options);
641
+ },
642
+ async close() {
643
+ if (closed) return;
644
+ closed = true;
645
+ for (const client of [...inst.clients]) client.end();
646
+ inst.clients.clear();
647
+ server.closeAllConnections?.();
648
+ await new Promise((done) => server.close(done));
649
+ },
650
+ };
651
+ }