@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.
- package/README.md +21 -6
- package/package.json +3 -1
- package/shared/README.md +29 -12
- 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 +1 -1
- package/shared/renderer/architecture.mjs +233 -0
- package/shared/renderer/index.html +7 -6
- package/shared/renderer/renderer.js +1163 -16
- package/shared/renderer/slides.css +42 -2
- 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 +9 -3
|
@@ -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
|
+
}
|