@open-slide/core 0.0.11 → 0.0.13
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/dist/{build-DHiRlpjn.js → build-DC3FTpWO.js} +2 -1
- package/dist/cli/bin.js +43 -4
- package/dist/{config-LZM903FE.js → config-Cuw0mC5h.js} +592 -63
- package/dist/design-BUML7uvZ.js +35 -0
- package/dist/{dev-B3JzCYn7.js → dev-BuWsdYvn.js} +2 -1
- package/dist/index.d.ts +55 -4
- package/dist/index.js +110 -1
- package/dist/{preview-UikovHEt.js → preview-CIcG-lP3.js} +2 -1
- package/dist/sync-3oqN1WyK.js +139 -0
- package/dist/sync-B4eLo2H6.js +3 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +2 -1
- package/package.json +2 -1
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +81 -0
- package/skills/create-theme/SKILL.md +194 -0
- package/skills/slide-authoring/SKILL.md +288 -0
- package/src/app/{App.tsx → app.tsx} +8 -6
- package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
- package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +121 -0
- package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
- package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
- package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
- package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
- package/src/app/components/inspector/save-bar.tsx +47 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +78 -0
- package/src/app/components/panel/save-card.tsx +139 -0
- package/src/app/components/pdf-progress-toast.tsx +25 -0
- package/src/app/components/player.tsx +341 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +204 -0
- package/src/app/components/present/help-overlay.tsx +56 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +40 -0
- package/src/app/components/present/overview-grid.tsx +184 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +44 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +71 -0
- package/src/app/components/present/use-touch-swipe.ts +63 -0
- package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
- package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
- package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
- package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
- package/src/app/components/style-panel/design-provider.tsx +139 -0
- package/src/app/components/style-panel/style-panel.tsx +326 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +57 -0
- package/src/app/components/thumbnail-rail.tsx +151 -0
- package/src/app/components/ui/button.tsx +51 -19
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +25 -9
- package/src/app/components/ui/dropdown-menu.tsx +29 -12
- package/src/app/components/ui/input.tsx +13 -9
- package/src/app/components/ui/popover.tsx +5 -2
- package/src/app/components/ui/progress.tsx +2 -2
- package/src/app/components/ui/select.tsx +11 -5
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +4 -4
- package/src/app/components/ui/sonner.tsx +11 -1
- package/src/app/components/ui/tabs.tsx +6 -6
- package/src/app/components/ui/textarea.tsx +11 -7
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +6 -6
- package/src/app/components/ui/tooltip.tsx +5 -2
- package/src/app/lib/design.ts +64 -0
- package/src/app/lib/export-html.ts +10 -1
- package/src/app/lib/export-pdf.ts +7 -0
- package/src/app/lib/folders.ts +1 -1
- package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
- package/src/app/lib/sdk.ts +5 -0
- package/src/app/lib/slides.ts +1 -1
- package/src/app/lib/utils.ts +1 -1
- package/src/app/main.tsx +5 -2
- package/src/app/routes/{Home.tsx → home.tsx} +266 -97
- package/src/app/routes/presenter.tsx +400 -0
- package/src/app/routes/slide.tsx +519 -0
- package/src/app/styles.css +338 -67
- package/src/app/components/PdfProgressToast.tsx +0 -23
- package/src/app/components/Player.tsx +0 -100
- package/src/app/components/ThumbnailRail.tsx +0 -68
- package/src/app/components/inspector/SaveBar.tsx +0 -77
- package/src/app/routes/Slide.tsx +0 -478
- /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
- /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
- /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { defaultDesign } from "./design-BUML7uvZ.js";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
4
6
|
import { existsSync } from "node:fs";
|
|
5
7
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
8
|
import react from "@vitejs/plugin-react";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { parse } from "@babel/parser";
|
|
9
10
|
import fg from "fast-glob";
|
|
10
11
|
import { loadConfigFromFile } from "vite";
|
|
@@ -47,7 +48,7 @@ function walkJsx(ast, visit) {
|
|
|
47
48
|
//#endregion
|
|
48
49
|
//#region src/vite/comments-plugin.ts
|
|
49
50
|
const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
|
|
50
|
-
const SLIDE_ID_RE$
|
|
51
|
+
const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
|
|
51
52
|
function b64urlEncode(s) {
|
|
52
53
|
return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
53
54
|
}
|
|
@@ -55,7 +56,7 @@ function b64urlDecode(s) {
|
|
|
55
56
|
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
56
57
|
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
57
58
|
}
|
|
58
|
-
async function readBody$
|
|
59
|
+
async function readBody$2(req) {
|
|
59
60
|
return await new Promise((resolve, reject) => {
|
|
60
61
|
const chunks = [];
|
|
61
62
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -71,13 +72,13 @@ async function readBody$1(req) {
|
|
|
71
72
|
req.on("error", reject);
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
|
-
function json$
|
|
75
|
+
function json$2(res, status, body) {
|
|
75
76
|
res.statusCode = status;
|
|
76
77
|
res.setHeader("content-type", "application/json");
|
|
77
78
|
res.end(JSON.stringify(body));
|
|
78
79
|
}
|
|
79
|
-
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
80
|
-
if (!SLIDE_ID_RE$
|
|
80
|
+
function resolveSlidePath$1(userCwd, slidesDir, slideId) {
|
|
81
|
+
if (!SLIDE_ID_RE$2.test(slideId)) return null;
|
|
81
82
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
82
83
|
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
83
84
|
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
@@ -184,7 +185,7 @@ function offsetToLine(source, offset) {
|
|
|
184
185
|
for (let i = 0; i < offset && i < source.length; i++) if (source[i] === "\n") line++;
|
|
185
186
|
return line;
|
|
186
187
|
}
|
|
187
|
-
function parseSource(source) {
|
|
188
|
+
function parseSource$1(source) {
|
|
188
189
|
try {
|
|
189
190
|
return parse(source, {
|
|
190
191
|
sourceType: "module",
|
|
@@ -196,7 +197,7 @@ function parseSource(source) {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
function findInnermostJsxElement(source, line, column) {
|
|
199
|
-
const ast = parseSource(source);
|
|
200
|
+
const ast = parseSource$1(source);
|
|
200
201
|
if (!ast) return null;
|
|
201
202
|
const exact = findJsxByStart(ast, line, column);
|
|
202
203
|
if (exact) return exact;
|
|
@@ -216,7 +217,7 @@ function findJsxByStart(ast, line, column) {
|
|
|
216
217
|
});
|
|
217
218
|
return hit;
|
|
218
219
|
}
|
|
219
|
-
function jsString(s) {
|
|
220
|
+
function jsString$1(s) {
|
|
220
221
|
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
221
222
|
}
|
|
222
223
|
function findStyleAttr(opening) {
|
|
@@ -251,7 +252,7 @@ function buildStyleSplice(source, element, ops) {
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
for (const op of ops) if (op.value === null) style.delete(op.key);
|
|
254
|
-
else style.set(op.key, jsString(op.value));
|
|
255
|
+
else style.set(op.key, jsString$1(op.value));
|
|
255
256
|
if (style.size === 0) {
|
|
256
257
|
if (!existing) return null;
|
|
257
258
|
let from = existing.start;
|
|
@@ -277,7 +278,7 @@ function buildStyleSplice(source, element, ops) {
|
|
|
277
278
|
};
|
|
278
279
|
}
|
|
279
280
|
function formatJsxText(value) {
|
|
280
|
-
if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString(value)}}`;
|
|
281
|
+
if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString$1(value)}}`;
|
|
281
282
|
return value;
|
|
282
283
|
}
|
|
283
284
|
function buildTextSplice(element, value) {
|
|
@@ -306,13 +307,13 @@ function buildTextSplice(element, value) {
|
|
|
306
307
|
if (expr.type === "StringLiteral" || expr.type === "NumericLiteral") return {
|
|
307
308
|
from: child.start,
|
|
308
309
|
to: child.end,
|
|
309
|
-
text: `{${jsString(value)}}`
|
|
310
|
+
text: `{${jsString$1(value)}}`
|
|
310
311
|
};
|
|
311
312
|
return { error: "element has dynamic expression child" };
|
|
312
313
|
}
|
|
313
314
|
return { error: "element has complex children" };
|
|
314
315
|
}
|
|
315
|
-
function findImports(ast) {
|
|
316
|
+
function findImports$1(ast) {
|
|
316
317
|
const body = ast.program?.body ?? [];
|
|
317
318
|
const out = [];
|
|
318
319
|
for (const node of body) {
|
|
@@ -338,7 +339,7 @@ function findImports(ast) {
|
|
|
338
339
|
}
|
|
339
340
|
function collectTopLevelIdentifiers(ast) {
|
|
340
341
|
const names = new Set();
|
|
341
|
-
for (const imp of findImports(ast)) {
|
|
342
|
+
for (const imp of findImports$1(ast)) {
|
|
342
343
|
if (imp.defaultIdent) names.add(imp.defaultIdent);
|
|
343
344
|
const specs = imp.node.specifiers ?? [];
|
|
344
345
|
for (const spec of specs) if (spec.type !== "ImportDefaultSpecifier") {
|
|
@@ -381,7 +382,7 @@ function planAssetAttr(ast, element, attr, assetPath) {
|
|
|
381
382
|
if (!opening) return { error: "no opening element" };
|
|
382
383
|
if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
|
|
383
384
|
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
384
|
-
const imports = findImports(ast);
|
|
385
|
+
const imports = findImports$1(ast);
|
|
385
386
|
let identifier = null;
|
|
386
387
|
for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
|
|
387
388
|
identifier = imp.defaultIdent;
|
|
@@ -422,6 +423,75 @@ function planAssetAttr(ast, element, attr, assetPath) {
|
|
|
422
423
|
attrSplice
|
|
423
424
|
};
|
|
424
425
|
}
|
|
426
|
+
function readJsxStringAttr(opening, name) {
|
|
427
|
+
const attr = findJsxAttr(opening, name);
|
|
428
|
+
if (!attr) return null;
|
|
429
|
+
const value = attr.value ?? null;
|
|
430
|
+
if (!value) return null;
|
|
431
|
+
if (value.type === "StringLiteral") return value.value;
|
|
432
|
+
if (value.type === "JSXExpressionContainer") {
|
|
433
|
+
const expr = value.expression;
|
|
434
|
+
if (expr.type === "StringLiteral") return expr.value;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
function readJsxNumberAttr(opening, name) {
|
|
439
|
+
const attr = findJsxAttr(opening, name);
|
|
440
|
+
if (!attr) return null;
|
|
441
|
+
const value = attr.value ?? null;
|
|
442
|
+
if (!value || value.type !== "JSXExpressionContainer") return null;
|
|
443
|
+
const expr = value.expression;
|
|
444
|
+
if (expr.type === "NumericLiteral") {
|
|
445
|
+
const n = expr.value;
|
|
446
|
+
return Number.isFinite(n) ? n : null;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function planReplacePlaceholder(ast, element, assetPath) {
|
|
451
|
+
const opening = element.openingElement;
|
|
452
|
+
if (!opening) return { error: "no opening element" };
|
|
453
|
+
const elName = opening.name;
|
|
454
|
+
if (elName?.type !== "JSXIdentifier" || elName.name !== "ImagePlaceholder") return { error: "not a placeholder" };
|
|
455
|
+
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
456
|
+
const hint = readJsxStringAttr(opening, "hint") ?? "";
|
|
457
|
+
const width = readJsxNumberAttr(opening, "width");
|
|
458
|
+
const height = readJsxNumberAttr(opening, "height");
|
|
459
|
+
const imports = findImports$1(ast);
|
|
460
|
+
let identifier = null;
|
|
461
|
+
for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
|
|
462
|
+
identifier = imp.defaultIdent;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
let importSplice = null;
|
|
466
|
+
if (!identifier) {
|
|
467
|
+
const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
|
|
468
|
+
const taken = collectTopLevelIdentifiers(ast);
|
|
469
|
+
identifier = safeAssetIdentifier(filename, taken);
|
|
470
|
+
const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
|
|
471
|
+
const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
|
|
472
|
+
const prefix = imports.length > 0 ? "\n" : "";
|
|
473
|
+
importSplice = {
|
|
474
|
+
from: insertAt,
|
|
475
|
+
to: insertAt,
|
|
476
|
+
text: prefix + importStmt
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const styleParts = [];
|
|
480
|
+
if (width != null) styleParts.push(`width: ${width}`);
|
|
481
|
+
if (height != null) styleParts.push(`height: ${height}`);
|
|
482
|
+
styleParts.push(`objectFit: 'cover'`);
|
|
483
|
+
const styleAttr = ` style={{ ${styleParts.join(", ")} }}`;
|
|
484
|
+
const altAttr = ` alt=${jsString$1(hint)}`;
|
|
485
|
+
const replacement = `<img src={${identifier}}${altAttr}${styleAttr} />`;
|
|
486
|
+
return {
|
|
487
|
+
importSplice,
|
|
488
|
+
elementSplice: {
|
|
489
|
+
from: element.start,
|
|
490
|
+
to: element.end,
|
|
491
|
+
text: replacement
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
425
495
|
function applyEdit(source, line, column, ops) {
|
|
426
496
|
if (ops.length === 0) return {
|
|
427
497
|
ok: true,
|
|
@@ -458,8 +528,9 @@ function applyEdit(source, line, column, ops) {
|
|
|
458
528
|
splices.push(result);
|
|
459
529
|
}
|
|
460
530
|
const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
|
|
461
|
-
|
|
462
|
-
|
|
531
|
+
const placeholderOps = ops.flatMap((op) => op.kind === "replace-placeholder-with-image" ? [op] : []);
|
|
532
|
+
if (assetOps.length > 0 || placeholderOps.length > 0) {
|
|
533
|
+
const ast = parseSource$1(source);
|
|
463
534
|
if (!ast) return {
|
|
464
535
|
ok: false,
|
|
465
536
|
status: 422,
|
|
@@ -476,6 +547,16 @@ function applyEdit(source, line, column, ops) {
|
|
|
476
547
|
splices.push(plan.attrSplice);
|
|
477
548
|
if (plan.importSplice) importSplices.push(plan.importSplice);
|
|
478
549
|
}
|
|
550
|
+
for (const op of placeholderOps) {
|
|
551
|
+
const plan = planReplacePlaceholder(ast, element, op.assetPath);
|
|
552
|
+
if ("error" in plan) return {
|
|
553
|
+
ok: false,
|
|
554
|
+
status: 422,
|
|
555
|
+
error: plan.error
|
|
556
|
+
};
|
|
557
|
+
splices.push(plan.elementSplice);
|
|
558
|
+
if (plan.importSplice) importSplices.push(plan.importSplice);
|
|
559
|
+
}
|
|
479
560
|
if (importSplices.length > 0) {
|
|
480
561
|
const from = importSplices[0].from;
|
|
481
562
|
const to = importSplices[0].to;
|
|
@@ -512,38 +593,38 @@ function commentsPlugin(opts) {
|
|
|
512
593
|
if (method !== "POST") return next();
|
|
513
594
|
try {
|
|
514
595
|
if (url.pathname === "/") {
|
|
515
|
-
const body = await readBody$
|
|
596
|
+
const body = await readBody$2(req);
|
|
516
597
|
const slideId = body.slideId ?? "";
|
|
517
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
518
|
-
if (!file) return json$
|
|
519
|
-
if (!body.line || body.line < 1) return json$
|
|
520
|
-
if (!Array.isArray(body.ops)) return json$
|
|
598
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
599
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
600
|
+
if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
|
|
601
|
+
if (!Array.isArray(body.ops)) return json$2(res, 400, { error: "missing ops" });
|
|
521
602
|
let source;
|
|
522
603
|
try {
|
|
523
604
|
source = await fs.readFile(file, "utf8");
|
|
524
605
|
} catch {
|
|
525
|
-
return json$
|
|
606
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
526
607
|
}
|
|
527
608
|
const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
|
|
528
|
-
if (!result.ok) return json$
|
|
609
|
+
if (!result.ok) return json$2(res, result.status, { error: result.error });
|
|
529
610
|
const changed = result.source !== source;
|
|
530
611
|
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
531
|
-
return json$
|
|
612
|
+
return json$2(res, 200, {
|
|
532
613
|
ok: true,
|
|
533
614
|
changed
|
|
534
615
|
});
|
|
535
616
|
}
|
|
536
617
|
if (url.pathname === "/batch") {
|
|
537
|
-
const body = await readBody$
|
|
618
|
+
const body = await readBody$2(req);
|
|
538
619
|
const slideId = body.slideId ?? "";
|
|
539
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
540
|
-
if (!file) return json$
|
|
541
|
-
if (!Array.isArray(body.edits)) return json$
|
|
620
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
621
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
622
|
+
if (!Array.isArray(body.edits)) return json$2(res, 400, { error: "missing edits" });
|
|
542
623
|
let source;
|
|
543
624
|
try {
|
|
544
625
|
source = await fs.readFile(file, "utf8");
|
|
545
626
|
} catch {
|
|
546
|
-
return json$
|
|
627
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
547
628
|
}
|
|
548
629
|
const original = source;
|
|
549
630
|
const results = [];
|
|
@@ -566,7 +647,7 @@ function commentsPlugin(opts) {
|
|
|
566
647
|
}
|
|
567
648
|
const changed = source !== original;
|
|
568
649
|
if (changed) await fs.writeFile(file, source, "utf8");
|
|
569
|
-
return json$
|
|
650
|
+
return json$2(res, 200, {
|
|
570
651
|
ok: true,
|
|
571
652
|
changed,
|
|
572
653
|
results
|
|
@@ -574,7 +655,7 @@ function commentsPlugin(opts) {
|
|
|
574
655
|
}
|
|
575
656
|
return next();
|
|
576
657
|
} catch (err) {
|
|
577
|
-
json$
|
|
658
|
+
json$2(res, 500, { error: String(err.message ?? err) });
|
|
578
659
|
}
|
|
579
660
|
});
|
|
580
661
|
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
@@ -583,31 +664,31 @@ function commentsPlugin(opts) {
|
|
|
583
664
|
try {
|
|
584
665
|
if (method === "GET" && url.pathname === "/") {
|
|
585
666
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
586
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
587
|
-
if (!file) return json$
|
|
667
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
668
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
588
669
|
let source;
|
|
589
670
|
try {
|
|
590
671
|
source = await fs.readFile(file, "utf8");
|
|
591
672
|
} catch {
|
|
592
|
-
return json$
|
|
673
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
593
674
|
}
|
|
594
|
-
return json$
|
|
675
|
+
return json$2(res, 200, { comments: parseMarkers(source) });
|
|
595
676
|
}
|
|
596
677
|
if (method === "POST" && url.pathname === "/add") {
|
|
597
|
-
const body = await readBody$
|
|
678
|
+
const body = await readBody$2(req);
|
|
598
679
|
const slideId = body.slideId ?? "";
|
|
599
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
600
|
-
if (!file) return json$
|
|
601
|
-
if (!body.line || body.line < 1) return json$
|
|
602
|
-
if (!body.text || typeof body.text !== "string") return json$
|
|
680
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
681
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
682
|
+
if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
|
|
683
|
+
if (!body.text || typeof body.text !== "string") return json$2(res, 400, { error: "missing text" });
|
|
603
684
|
let source;
|
|
604
685
|
try {
|
|
605
686
|
source = await fs.readFile(file, "utf8");
|
|
606
687
|
} catch {
|
|
607
|
-
return json$
|
|
688
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
608
689
|
}
|
|
609
690
|
const plan = findInsertion(source, body.line, body.column);
|
|
610
|
-
if (!plan) return json$
|
|
691
|
+
if (!plan) return json$2(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
|
|
611
692
|
const id = newId();
|
|
612
693
|
const ts = new Date().toISOString();
|
|
613
694
|
const payload = b64urlEncode(JSON.stringify({
|
|
@@ -618,32 +699,460 @@ function commentsPlugin(opts) {
|
|
|
618
699
|
const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
|
|
619
700
|
await fs.writeFile(file, next$1, "utf8");
|
|
620
701
|
const markerLine = offsetToLine(next$1, plan.offset + 1);
|
|
621
|
-
return json$
|
|
702
|
+
return json$2(res, 200, {
|
|
622
703
|
id,
|
|
623
704
|
line: markerLine
|
|
624
705
|
});
|
|
625
706
|
}
|
|
626
707
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
627
708
|
const id = url.pathname.slice(1);
|
|
628
|
-
if (!/^c-[a-f0-9]+$/.test(id)) return json$
|
|
709
|
+
if (!/^c-[a-f0-9]+$/.test(id)) return json$2(res, 400, { error: "invalid id" });
|
|
629
710
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
630
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
631
|
-
if (!file) return json$
|
|
711
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
712
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
632
713
|
let source;
|
|
633
714
|
try {
|
|
634
715
|
source = await fs.readFile(file, "utf8");
|
|
635
716
|
} catch {
|
|
636
|
-
return json$
|
|
717
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
637
718
|
}
|
|
638
719
|
const lines = source.split("\n");
|
|
639
720
|
const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
640
721
|
const hit = lines.findIndex((l) => idRe.test(l));
|
|
641
|
-
if (hit === -1) return json$
|
|
722
|
+
if (hit === -1) return json$2(res, 404, { error: "marker not found" });
|
|
642
723
|
lines.splice(hit, 1);
|
|
643
724
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
644
|
-
return json$
|
|
725
|
+
return json$2(res, 200, { ok: true });
|
|
645
726
|
}
|
|
646
727
|
next();
|
|
728
|
+
} catch (err) {
|
|
729
|
+
json$2(res, 500, { error: String(err.message ?? err) });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/vite/design-plugin.ts
|
|
738
|
+
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
739
|
+
async function readBody$1(req) {
|
|
740
|
+
return await new Promise((resolve, reject) => {
|
|
741
|
+
const chunks = [];
|
|
742
|
+
req.on("data", (c) => chunks.push(c));
|
|
743
|
+
req.on("end", () => {
|
|
744
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
745
|
+
if (!raw) return resolve({});
|
|
746
|
+
try {
|
|
747
|
+
resolve(JSON.parse(raw));
|
|
748
|
+
} catch (e) {
|
|
749
|
+
reject(e);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
req.on("error", reject);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function json$1(res, status, body) {
|
|
756
|
+
res.statusCode = status;
|
|
757
|
+
res.setHeader("content-type", "application/json");
|
|
758
|
+
res.end(JSON.stringify(body));
|
|
759
|
+
}
|
|
760
|
+
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
761
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
762
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
763
|
+
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
764
|
+
if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
|
|
765
|
+
return full;
|
|
766
|
+
}
|
|
767
|
+
function parseSource(source) {
|
|
768
|
+
try {
|
|
769
|
+
return parse(source, {
|
|
770
|
+
sourceType: "module",
|
|
771
|
+
plugins: ["typescript", "jsx"],
|
|
772
|
+
errorRecovery: true
|
|
773
|
+
});
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function findDesignDecl(ast) {
|
|
779
|
+
const body = ast.program?.body ?? [];
|
|
780
|
+
for (const node of body) {
|
|
781
|
+
let varDecl = null;
|
|
782
|
+
if (node.type === "VariableDeclaration") varDecl = node;
|
|
783
|
+
else if (node.type === "ExportNamedDeclaration") {
|
|
784
|
+
const decl = node.declaration;
|
|
785
|
+
if (decl?.type === "VariableDeclaration") varDecl = decl;
|
|
786
|
+
}
|
|
787
|
+
if (!varDecl) continue;
|
|
788
|
+
const declarations = varDecl.declarations ?? [];
|
|
789
|
+
for (const d of declarations) {
|
|
790
|
+
const id = d.id;
|
|
791
|
+
if (!id || id.type !== "Identifier" || id.name !== "design") continue;
|
|
792
|
+
const init = d.init;
|
|
793
|
+
if (!init) return null;
|
|
794
|
+
let inner = init;
|
|
795
|
+
if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
|
|
796
|
+
const expr = inner.expression;
|
|
797
|
+
if (expr) inner = expr;
|
|
798
|
+
}
|
|
799
|
+
if (inner.type !== "ObjectExpression") return null;
|
|
800
|
+
return {
|
|
801
|
+
declStart: node.start,
|
|
802
|
+
declEnd: node.end,
|
|
803
|
+
objectStart: inner.start,
|
|
804
|
+
objectEnd: inner.end
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
function literalToValue(node) {
|
|
811
|
+
switch (node.type) {
|
|
812
|
+
case "StringLiteral": return node.value;
|
|
813
|
+
case "NumericLiteral": return node.value;
|
|
814
|
+
case "BooleanLiteral": return node.value;
|
|
815
|
+
case "NullLiteral": return null;
|
|
816
|
+
case "UnaryExpression": {
|
|
817
|
+
const op = node.operator;
|
|
818
|
+
const arg = node.argument;
|
|
819
|
+
const v = literalToValue(arg);
|
|
820
|
+
if (op === "-" && typeof v === "number") return -v;
|
|
821
|
+
if (op === "+" && typeof v === "number") return v;
|
|
822
|
+
throw new Error(`unsupported unary operator ${op}`);
|
|
823
|
+
}
|
|
824
|
+
case "TemplateLiteral": {
|
|
825
|
+
const quasis = node.quasis;
|
|
826
|
+
const expressions = node.expressions;
|
|
827
|
+
if (expressions.length > 0) throw new Error("template literal has expressions");
|
|
828
|
+
return quasis[0].value.cooked ?? quasis[0].value.raw;
|
|
829
|
+
}
|
|
830
|
+
case "ArrayExpression": {
|
|
831
|
+
const elements = node.elements;
|
|
832
|
+
return elements.map((el) => {
|
|
833
|
+
if (!el) throw new Error("array has hole");
|
|
834
|
+
return literalToValue(el);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
case "ObjectExpression": {
|
|
838
|
+
const properties = node.properties;
|
|
839
|
+
const out = {};
|
|
840
|
+
for (const prop of properties) {
|
|
841
|
+
if (prop.type !== "ObjectProperty") throw new Error("object has spread or method");
|
|
842
|
+
const p = prop;
|
|
843
|
+
if (p.computed) throw new Error("object has computed key");
|
|
844
|
+
let key;
|
|
845
|
+
if (p.key.type === "Identifier" && typeof p.key.name === "string") key = p.key.name;
|
|
846
|
+
else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") key = p.key.value;
|
|
847
|
+
else throw new Error("unsupported object key");
|
|
848
|
+
out[key] = literalToValue(p.value);
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
default: throw new Error(`unsupported node type ${node.type}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function isPlainObject(v) {
|
|
856
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
857
|
+
}
|
|
858
|
+
function mergeDesign(base, patch) {
|
|
859
|
+
const out = JSON.parse(JSON.stringify(base));
|
|
860
|
+
const apply = (target, src) => {
|
|
861
|
+
for (const [k, v] of Object.entries(src)) if (isPlainObject(v) && isPlainObject(target[k])) apply(target[k], v);
|
|
862
|
+
else target[k] = v;
|
|
863
|
+
};
|
|
864
|
+
if (isPlainObject(patch)) apply(out, patch);
|
|
865
|
+
return out;
|
|
866
|
+
}
|
|
867
|
+
function indent(level) {
|
|
868
|
+
return " ".repeat(level);
|
|
869
|
+
}
|
|
870
|
+
function jsString(s) {
|
|
871
|
+
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
872
|
+
}
|
|
873
|
+
function isValidIdentifier(name) {
|
|
874
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
875
|
+
}
|
|
876
|
+
function serializeValue(value, level) {
|
|
877
|
+
if (value === null) return "null";
|
|
878
|
+
if (typeof value === "string") return jsString(value);
|
|
879
|
+
if (typeof value === "number") {
|
|
880
|
+
if (!Number.isFinite(value)) throw new Error("non-finite number");
|
|
881
|
+
return String(value);
|
|
882
|
+
}
|
|
883
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
884
|
+
if (Array.isArray(value)) {
|
|
885
|
+
if (value.length === 0) return "[]";
|
|
886
|
+
const inner = value.map((el) => serializeValue(el, level + 1)).join(", ");
|
|
887
|
+
return `[${inner}]`;
|
|
888
|
+
}
|
|
889
|
+
if (isPlainObject(value)) {
|
|
890
|
+
const entries = Object.entries(value);
|
|
891
|
+
if (entries.length === 0) return "{}";
|
|
892
|
+
const childIndent = indent(level + 1);
|
|
893
|
+
const lines = entries.map(([k, v]) => {
|
|
894
|
+
const key = isValidIdentifier(k) ? k : jsString(k);
|
|
895
|
+
return `${childIndent}${key}: ${serializeValue(v, level + 1)},`;
|
|
896
|
+
});
|
|
897
|
+
return `{\n${lines.join("\n")}\n${indent(level)}}`;
|
|
898
|
+
}
|
|
899
|
+
throw new Error(`unsupported value type ${typeof value}`);
|
|
900
|
+
}
|
|
901
|
+
function serializeDesign(design) {
|
|
902
|
+
return serializeValue(design, 0);
|
|
903
|
+
}
|
|
904
|
+
function parseSlideDesign(source) {
|
|
905
|
+
const ast = parseSource(source);
|
|
906
|
+
if (!ast) return {
|
|
907
|
+
ok: false,
|
|
908
|
+
exists: true,
|
|
909
|
+
error: "could not parse slide source"
|
|
910
|
+
};
|
|
911
|
+
const loc = findDesignDecl(ast);
|
|
912
|
+
if (!loc) return {
|
|
913
|
+
ok: false,
|
|
914
|
+
exists: false
|
|
915
|
+
};
|
|
916
|
+
const objectNode = findDesignObjectNode(ast);
|
|
917
|
+
if (!objectNode) return {
|
|
918
|
+
ok: false,
|
|
919
|
+
exists: true,
|
|
920
|
+
error: "design has unsupported initializer"
|
|
921
|
+
};
|
|
922
|
+
let value;
|
|
923
|
+
try {
|
|
924
|
+
value = literalToValue(objectNode);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
return {
|
|
927
|
+
ok: false,
|
|
928
|
+
exists: true,
|
|
929
|
+
error: err.message
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const merged = mergeDesign(defaultDesign, value);
|
|
933
|
+
return {
|
|
934
|
+
ok: true,
|
|
935
|
+
design: merged,
|
|
936
|
+
loc
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function findDesignObjectNode(ast) {
|
|
940
|
+
const body = ast.program?.body ?? [];
|
|
941
|
+
for (const node of body) {
|
|
942
|
+
let varDecl = null;
|
|
943
|
+
if (node.type === "VariableDeclaration") varDecl = node;
|
|
944
|
+
else if (node.type === "ExportNamedDeclaration") {
|
|
945
|
+
const decl = node.declaration;
|
|
946
|
+
if (decl?.type === "VariableDeclaration") varDecl = decl;
|
|
947
|
+
}
|
|
948
|
+
if (!varDecl) continue;
|
|
949
|
+
const declarations = varDecl.declarations ?? [];
|
|
950
|
+
for (const d of declarations) {
|
|
951
|
+
const id = d.id;
|
|
952
|
+
if (!id || id.type !== "Identifier" || id.name !== "design") continue;
|
|
953
|
+
const init = d.init;
|
|
954
|
+
if (!init) return null;
|
|
955
|
+
let inner = init;
|
|
956
|
+
if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
|
|
957
|
+
const expr = inner.expression;
|
|
958
|
+
if (expr) inner = expr;
|
|
959
|
+
}
|
|
960
|
+
if (inner.type !== "ObjectExpression") return null;
|
|
961
|
+
return inner;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
function findImports(ast) {
|
|
967
|
+
const body = ast.program?.body ?? [];
|
|
968
|
+
const out = [];
|
|
969
|
+
for (const node of body) {
|
|
970
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
971
|
+
const src = node.source?.value;
|
|
972
|
+
if (typeof src !== "string") continue;
|
|
973
|
+
const specs = node.specifiers ?? [];
|
|
974
|
+
out.push({
|
|
975
|
+
node,
|
|
976
|
+
source: src,
|
|
977
|
+
specifiers: specs
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
return out;
|
|
981
|
+
}
|
|
982
|
+
function ensureDesignSystemImport(source, ast) {
|
|
983
|
+
const imports = findImports(ast);
|
|
984
|
+
const coreImport = imports.find((imp) => imp.source === "@open-slide/core");
|
|
985
|
+
if (coreImport) {
|
|
986
|
+
const hasDesignSystem = coreImport.specifiers.some((spec) => {
|
|
987
|
+
if (spec.type !== "ImportSpecifier") return false;
|
|
988
|
+
const imported = spec.imported;
|
|
989
|
+
return imported?.name === "DesignSystem";
|
|
990
|
+
});
|
|
991
|
+
if (hasDesignSystem) return {
|
|
992
|
+
source,
|
|
993
|
+
offsetShift: 0
|
|
994
|
+
};
|
|
995
|
+
const node = coreImport.node;
|
|
996
|
+
const importText = source.slice(node.start, node.end);
|
|
997
|
+
const braceClose = importText.lastIndexOf("}");
|
|
998
|
+
if (braceClose === -1) return {
|
|
999
|
+
source,
|
|
1000
|
+
offsetShift: 0
|
|
1001
|
+
};
|
|
1002
|
+
const absoluteBrace = node.start + braceClose;
|
|
1003
|
+
const insertText = coreImport.specifiers.length > 0 ? ", type DesignSystem" : "type DesignSystem";
|
|
1004
|
+
const next$1 = `${source.slice(0, absoluteBrace)}${insertText}${source.slice(absoluteBrace)}`;
|
|
1005
|
+
return {
|
|
1006
|
+
source: next$1,
|
|
1007
|
+
offsetShift: insertText.length
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
const stmt = `import type { DesignSystem } from '@open-slide/core';\n`;
|
|
1011
|
+
if (imports.length > 0) {
|
|
1012
|
+
const last = imports[imports.length - 1];
|
|
1013
|
+
const insertAt = last.node.end;
|
|
1014
|
+
const trail = source[insertAt] === "\n" ? "" : "\n";
|
|
1015
|
+
const next$1 = `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`;
|
|
1016
|
+
return {
|
|
1017
|
+
source: next$1,
|
|
1018
|
+
offsetShift: 1 + stmt.length - (trail ? 0 : 1)
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const next = `${stmt}\n${source}`;
|
|
1022
|
+
return {
|
|
1023
|
+
source: next,
|
|
1024
|
+
offsetShift: stmt.length + 1
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function findInsertionPoint(source, ast) {
|
|
1028
|
+
const imports = findImports(ast);
|
|
1029
|
+
if (imports.length === 0) return 0;
|
|
1030
|
+
const last = imports[imports.length - 1];
|
|
1031
|
+
let off = last.node.end;
|
|
1032
|
+
while (off < source.length && source[off] !== "\n") off++;
|
|
1033
|
+
if (off < source.length) off++;
|
|
1034
|
+
return off;
|
|
1035
|
+
}
|
|
1036
|
+
function applyDesignWrite(source, next) {
|
|
1037
|
+
let body;
|
|
1038
|
+
try {
|
|
1039
|
+
body = serializeDesign(next);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
return {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
status: 422,
|
|
1044
|
+
error: `serialize failed: ${err.message}`
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const ast = parseSource(source);
|
|
1048
|
+
if (!ast) return {
|
|
1049
|
+
ok: false,
|
|
1050
|
+
status: 422,
|
|
1051
|
+
error: "could not parse slide source"
|
|
1052
|
+
};
|
|
1053
|
+
const loc = findDesignDecl(ast);
|
|
1054
|
+
if (loc) {
|
|
1055
|
+
const out$1 = source.slice(0, loc.objectStart) + body + source.slice(loc.objectEnd);
|
|
1056
|
+
return {
|
|
1057
|
+
ok: true,
|
|
1058
|
+
source: out$1,
|
|
1059
|
+
created: false
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const withImport = ensureDesignSystemImport(source, ast);
|
|
1063
|
+
const ast2 = parseSource(withImport.source);
|
|
1064
|
+
if (!ast2) return {
|
|
1065
|
+
ok: false,
|
|
1066
|
+
status: 422,
|
|
1067
|
+
error: "failed to re-parse after adding import"
|
|
1068
|
+
};
|
|
1069
|
+
const insertAt = findInsertionPoint(withImport.source, ast2);
|
|
1070
|
+
const block = `\nconst design: DesignSystem = ${body};\n`;
|
|
1071
|
+
const out = withImport.source.slice(0, insertAt) + block + withImport.source.slice(insertAt);
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
source: out,
|
|
1075
|
+
created: true
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function designPlugin(opts) {
|
|
1079
|
+
const userCwd = opts.userCwd;
|
|
1080
|
+
const slidesDir = opts.slidesDir ?? "slides";
|
|
1081
|
+
return {
|
|
1082
|
+
name: "open-slide:design",
|
|
1083
|
+
apply: "serve",
|
|
1084
|
+
configureServer(server) {
|
|
1085
|
+
server.middlewares.use("/__design", async (req, res, next) => {
|
|
1086
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
1087
|
+
const method = req.method ?? "GET";
|
|
1088
|
+
const slideId = url.searchParams.get("slideId") ?? "";
|
|
1089
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
1090
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
1091
|
+
try {
|
|
1092
|
+
if (method === "GET" && url.pathname === "/") {
|
|
1093
|
+
let source;
|
|
1094
|
+
try {
|
|
1095
|
+
source = await fs.readFile(file, "utf8");
|
|
1096
|
+
} catch {
|
|
1097
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1098
|
+
}
|
|
1099
|
+
const parsed = parseSlideDesign(source);
|
|
1100
|
+
if (parsed.ok) return json$1(res, 200, {
|
|
1101
|
+
design: parsed.design,
|
|
1102
|
+
exists: true,
|
|
1103
|
+
warning: null
|
|
1104
|
+
});
|
|
1105
|
+
if (parsed.exists === false) return json$1(res, 200, {
|
|
1106
|
+
design: defaultDesign,
|
|
1107
|
+
exists: false,
|
|
1108
|
+
warning: null
|
|
1109
|
+
});
|
|
1110
|
+
return json$1(res, 200, {
|
|
1111
|
+
design: defaultDesign,
|
|
1112
|
+
exists: true,
|
|
1113
|
+
warning: parsed.error
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (method === "PUT" && url.pathname === "/") {
|
|
1117
|
+
const body = await readBody$1(req);
|
|
1118
|
+
const patch = body.patch;
|
|
1119
|
+
if (!patch || typeof patch !== "object") return json$1(res, 400, { error: "missing patch object" });
|
|
1120
|
+
let source;
|
|
1121
|
+
try {
|
|
1122
|
+
source = await fs.readFile(file, "utf8");
|
|
1123
|
+
} catch {
|
|
1124
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1125
|
+
}
|
|
1126
|
+
const parsed = parseSlideDesign(source);
|
|
1127
|
+
const baseDesign = parsed.ok ? parsed.design : defaultDesign;
|
|
1128
|
+
if (!parsed.ok && parsed.exists) return json$1(res, 422, { error: parsed.error });
|
|
1129
|
+
const merged = mergeDesign(baseDesign, patch);
|
|
1130
|
+
const written = applyDesignWrite(source, merged);
|
|
1131
|
+
if (!written.ok) return json$1(res, written.status, { error: written.error });
|
|
1132
|
+
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1133
|
+
return json$1(res, 200, {
|
|
1134
|
+
ok: true,
|
|
1135
|
+
design: merged,
|
|
1136
|
+
created: written.created
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (method === "POST" && url.pathname === "/reset") {
|
|
1140
|
+
let source;
|
|
1141
|
+
try {
|
|
1142
|
+
source = await fs.readFile(file, "utf8");
|
|
1143
|
+
} catch {
|
|
1144
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1145
|
+
}
|
|
1146
|
+
const written = applyDesignWrite(source, defaultDesign);
|
|
1147
|
+
if (!written.ok) return json$1(res, written.status, { error: written.error });
|
|
1148
|
+
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1149
|
+
return json$1(res, 200, {
|
|
1150
|
+
ok: true,
|
|
1151
|
+
design: defaultDesign,
|
|
1152
|
+
created: written.created
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
return next();
|
|
647
1156
|
} catch (err) {
|
|
648
1157
|
json$1(res, 500, { error: String(err.message ?? err) });
|
|
649
1158
|
}
|
|
@@ -838,10 +1347,10 @@ function updateMetaTitleInSource(source, title) {
|
|
|
838
1347
|
return source.slice(0, openBrace + 1) + newBody + source.slice(closeBrace);
|
|
839
1348
|
}
|
|
840
1349
|
const firstIndentMatch = body.match(/\n([ \t]+)\S/);
|
|
841
|
-
const indent = firstIndentMatch ? firstIndentMatch[1] : " ";
|
|
1350
|
+
const indent$1 = firstIndentMatch ? firstIndentMatch[1] : " ";
|
|
842
1351
|
const trimmedBody = body.replace(/^\s*\n?/, "");
|
|
843
1352
|
const needsSeparator = trimmedBody.trim().length > 0;
|
|
844
|
-
const insertion$1 = `\n${indent}title: ${newLiteral}${needsSeparator ? "," : ""}`;
|
|
1353
|
+
const insertion$1 = `\n${indent$1}title: ${newLiteral}${needsSeparator ? "," : ""}`;
|
|
845
1354
|
return source.slice(0, openBrace + 1) + insertion$1 + body + source.slice(closeBrace);
|
|
846
1355
|
}
|
|
847
1356
|
const exportDefaultIdx = source.search(/export\s+default\b/);
|
|
@@ -1192,10 +1701,12 @@ function filesPlugin(opts) {
|
|
|
1192
1701
|
|
|
1193
1702
|
//#endregion
|
|
1194
1703
|
//#region src/vite/loc-tags-plugin.ts
|
|
1195
|
-
|
|
1704
|
+
const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
|
|
1705
|
+
function isTaggableJsxName(name) {
|
|
1196
1706
|
if (!name || typeof name !== "object") return false;
|
|
1197
1707
|
const n = name;
|
|
1198
|
-
|
|
1708
|
+
if (n.type !== "JSXIdentifier" || typeof n.name !== "string") return false;
|
|
1709
|
+
return /^[a-z]/.test(n.name) || FORWARDING_COMPONENTS.has(n.name);
|
|
1199
1710
|
}
|
|
1200
1711
|
function alreadyTagged(opening) {
|
|
1201
1712
|
const attrs = opening.attributes ?? [];
|
|
@@ -1223,7 +1734,7 @@ function injectLocTags(code) {
|
|
|
1223
1734
|
const opening = node.openingElement;
|
|
1224
1735
|
if (!opening) return;
|
|
1225
1736
|
const name = opening.name;
|
|
1226
|
-
if (!
|
|
1737
|
+
if (!isTaggableJsxName(name)) return;
|
|
1227
1738
|
if (alreadyTagged(opening)) return;
|
|
1228
1739
|
const loc = node.loc;
|
|
1229
1740
|
if (!loc) return;
|
|
@@ -1366,21 +1877,38 @@ function openSlidePlugin(opts) {
|
|
|
1366
1877
|
return null;
|
|
1367
1878
|
},
|
|
1368
1879
|
configureServer(server) {
|
|
1880
|
+
const isSlideEntry = (p) => {
|
|
1881
|
+
const rel = path.relative(slidesRoot, p);
|
|
1882
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
1883
|
+
const parts = rel.split(path.sep);
|
|
1884
|
+
if (parts.length !== 2) return false;
|
|
1885
|
+
return /^index\.(tsx|jsx|ts|js)$/.test(parts[1]);
|
|
1886
|
+
};
|
|
1887
|
+
let reloadTimer = null;
|
|
1369
1888
|
const reload = () => {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1889
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1890
|
+
reloadTimer = setTimeout(() => {
|
|
1891
|
+
reloadTimer = null;
|
|
1892
|
+
const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
|
|
1893
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1894
|
+
server.ws.send({ type: "full-reload" });
|
|
1895
|
+
}, 150);
|
|
1373
1896
|
};
|
|
1374
|
-
server.watcher.add(path.join(slidesRoot, "
|
|
1897
|
+
server.watcher.add(path.join(slidesRoot, "*/index.{tsx,jsx,ts,js}"));
|
|
1375
1898
|
server.watcher.on("add", (p) => {
|
|
1376
|
-
if (p
|
|
1899
|
+
if (isSlideEntry(p)) reload();
|
|
1377
1900
|
});
|
|
1378
1901
|
server.watcher.on("unlink", (p) => {
|
|
1379
|
-
if (p
|
|
1902
|
+
if (isSlideEntry(p)) reload();
|
|
1380
1903
|
});
|
|
1904
|
+
let foldersTimer = null;
|
|
1381
1905
|
const invalidateFolders = () => {
|
|
1382
|
-
|
|
1383
|
-
|
|
1906
|
+
if (foldersTimer) clearTimeout(foldersTimer);
|
|
1907
|
+
foldersTimer = setTimeout(() => {
|
|
1908
|
+
foldersTimer = null;
|
|
1909
|
+
const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
|
|
1910
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1911
|
+
}, 100);
|
|
1384
1912
|
};
|
|
1385
1913
|
server.watcher.add(foldersManifestPath);
|
|
1386
1914
|
server.watcher.on("change", (p) => {
|
|
@@ -1436,6 +1964,7 @@ async function createViteConfig(opts) {
|
|
|
1436
1964
|
userCwd,
|
|
1437
1965
|
config
|
|
1438
1966
|
}),
|
|
1967
|
+
designPlugin({ userCwd }),
|
|
1439
1968
|
commentsPlugin({
|
|
1440
1969
|
userCwd,
|
|
1441
1970
|
slidesDir
|