@open-slide/core 1.1.0 → 1.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/dist/{build-DSqSio-T.js → build-_276DMmJ.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
- package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
- package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
- package/dist/en-CDKzoZvf.js +351 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +229 -39
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +166 -326
- package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +9 -1
- package/skills/create-slide/SKILL.md +1 -1
- package/skills/create-theme/SKILL.md +60 -12
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +59 -1
- package/src/app/app.tsx +11 -1
- package/src/app/components/asset-view.tsx +1 -13
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
- package/src/app/components/inspector/inspector-panel.tsx +163 -19
- package/src/app/components/inspector/inspector-provider.tsx +60 -7
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +11 -7
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/sidebar/folder-item.tsx +16 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/components/themes/theme-detail.tsx +300 -0
- package/src/app/components/themes/themes-gallery.tsx +146 -0
- package/src/app/components/thumbnail-rail.tsx +136 -29
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +10 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/routes/home-shell.tsx +173 -0
- package/src/app/routes/home.tsx +108 -204
- package/src/app/routes/slide.tsx +333 -68
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +61 -7
- package/src/locale/ja.ts +62 -7
- package/src/locale/types.ts +62 -5
- package/src/locale/zh-cn.ts +61 -7
- package/src/locale/zh-tw.ts +61 -7
- package/dist/sync-B4eLo2H6.js +0 -3
- /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
- /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultDesign } from "./design-
|
|
1
|
+
import { defaultDesign } from "./design-cpzS8aud.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,11 +7,12 @@ import { existsSync } from "node:fs";
|
|
|
7
7
|
import tailwindcss from "@tailwindcss/vite";
|
|
8
8
|
import react from "@vitejs/plugin-react";
|
|
9
9
|
import { parse } from "@babel/parser";
|
|
10
|
+
import * as t$2 from "@babel/types";
|
|
10
11
|
import * as t$1 from "@babel/types";
|
|
11
12
|
import * as t from "@babel/types";
|
|
12
13
|
import { isJSXElement, isJSXFragment } from "@babel/types";
|
|
13
14
|
import fg from "fast-glob";
|
|
14
|
-
import { loadConfigFromFile } from "vite";
|
|
15
|
+
import { loadConfigFromFile, normalizePath } from "vite";
|
|
15
16
|
|
|
16
17
|
//#region src/vite/babel-walk.ts
|
|
17
18
|
const SKIP_KEYS = new Set([
|
|
@@ -57,7 +58,7 @@ function walkAll(ast, visit) {
|
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region src/vite/comments-plugin.ts
|
|
59
60
|
const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
|
|
60
|
-
const SLIDE_ID_RE$
|
|
61
|
+
const SLIDE_ID_RE$4 = /^[a-z0-9_-]+$/i;
|
|
61
62
|
function b64urlEncode(s) {
|
|
62
63
|
return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
63
64
|
}
|
|
@@ -65,7 +66,7 @@ function b64urlDecode(s) {
|
|
|
65
66
|
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
66
67
|
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
67
68
|
}
|
|
68
|
-
async function readBody$
|
|
69
|
+
async function readBody$3(req) {
|
|
69
70
|
return await new Promise((resolve, reject) => {
|
|
70
71
|
const chunks = [];
|
|
71
72
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -81,13 +82,13 @@ async function readBody$2(req) {
|
|
|
81
82
|
req.on("error", reject);
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
|
-
function json$
|
|
85
|
+
function json$3(res, status, body) {
|
|
85
86
|
res.statusCode = status;
|
|
86
87
|
res.setHeader("content-type", "application/json");
|
|
87
88
|
res.end(JSON.stringify(body));
|
|
88
89
|
}
|
|
89
|
-
function resolveSlidePath$
|
|
90
|
-
if (!SLIDE_ID_RE$
|
|
90
|
+
function resolveSlidePath$2(userCwd, slidesDir, slideId) {
|
|
91
|
+
if (!SLIDE_ID_RE$4.test(slideId)) return null;
|
|
91
92
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
92
93
|
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
93
94
|
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
@@ -135,7 +136,7 @@ function lineIndent(source, lineNumber) {
|
|
|
135
136
|
function findJsxAncestors(ast, line, column) {
|
|
136
137
|
const hits = [];
|
|
137
138
|
walkJsx(ast, (n) => {
|
|
138
|
-
if (!n.loc || !t$
|
|
139
|
+
if (!n.loc || !t$2.isJSXElement(n) && !t$2.isJSXFragment(n)) return;
|
|
139
140
|
const s = n.loc.start;
|
|
140
141
|
const e = n.loc.end;
|
|
141
142
|
const afterStart = line > s.line || line === s.line && column >= s.column;
|
|
@@ -149,7 +150,7 @@ function findJsxAncestors(ast, line, column) {
|
|
|
149
150
|
return hits.map((h) => h.node);
|
|
150
151
|
}
|
|
151
152
|
function planInsertion(source, target) {
|
|
152
|
-
if (t$
|
|
153
|
+
if (t$2.isJSXFragment(target)) {
|
|
153
154
|
const opening = target.openingFragment;
|
|
154
155
|
const startLine = target.loc?.start.line ?? 1;
|
|
155
156
|
return {
|
|
@@ -157,7 +158,7 @@ function planInsertion(source, target) {
|
|
|
157
158
|
indent: `${lineIndent(source, startLine)} `
|
|
158
159
|
};
|
|
159
160
|
}
|
|
160
|
-
if (t$
|
|
161
|
+
if (t$2.isJSXElement(target)) {
|
|
161
162
|
const opening = target.openingElement;
|
|
162
163
|
if (opening.selfClosing) return null;
|
|
163
164
|
const startLine = target.loc?.start.line ?? 1;
|
|
@@ -169,7 +170,7 @@ function planInsertion(source, target) {
|
|
|
169
170
|
return null;
|
|
170
171
|
}
|
|
171
172
|
function findInsertion(source, line, column) {
|
|
172
|
-
const ast = parseSource$
|
|
173
|
+
const ast = parseSource$2(source);
|
|
173
174
|
if (!ast) return null;
|
|
174
175
|
const col = column ?? 0;
|
|
175
176
|
const ancestors = findJsxAncestors(ast, line, col);
|
|
@@ -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$
|
|
188
|
+
function parseSource$2(source) {
|
|
188
189
|
try {
|
|
189
190
|
return parse(source, {
|
|
190
191
|
sourceType: "module",
|
|
@@ -198,13 +199,13 @@ function parseSource$1(source) {
|
|
|
198
199
|
function findInnermostJsxElement(ast, line, column) {
|
|
199
200
|
const exact = findJsxByStart(ast, line, column);
|
|
200
201
|
if (exact) return exact;
|
|
201
|
-
for (const n of findJsxAncestors(ast, line, column)) if (t$
|
|
202
|
+
for (const n of findJsxAncestors(ast, line, column)) if (t$2.isJSXElement(n)) return n;
|
|
202
203
|
return null;
|
|
203
204
|
}
|
|
204
205
|
function findJsxByStart(ast, line, column) {
|
|
205
206
|
let hit = null;
|
|
206
207
|
walkJsx(ast, (n) => {
|
|
207
|
-
if (!t$
|
|
208
|
+
if (!t$2.isJSXElement(n) || !n.loc) return;
|
|
208
209
|
const s = n.loc.start;
|
|
209
210
|
if (s.line === line && s.column === column) {
|
|
210
211
|
hit = n;
|
|
@@ -217,10 +218,10 @@ function jsString$1(s) {
|
|
|
217
218
|
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
218
219
|
}
|
|
219
220
|
function jsxAttrName(attr) {
|
|
220
|
-
return t$
|
|
221
|
+
return t$2.isJSXIdentifier(attr.name) ? attr.name.name : null;
|
|
221
222
|
}
|
|
222
223
|
function findJsxAttr(opening, name) {
|
|
223
|
-
for (const attr of opening.attributes) if (t$
|
|
224
|
+
for (const attr of opening.attributes) if (t$2.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
|
|
224
225
|
return null;
|
|
225
226
|
}
|
|
226
227
|
function buildStyleSplice(source, element, ops) {
|
|
@@ -229,15 +230,15 @@ function buildStyleSplice(source, element, ops) {
|
|
|
229
230
|
const style = new Map();
|
|
230
231
|
if (existing) {
|
|
231
232
|
const value = existing.value;
|
|
232
|
-
if (!value || !t$
|
|
233
|
+
if (!value || !t$2.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
|
|
233
234
|
const expr = value.expression;
|
|
234
|
-
if (!t$
|
|
235
|
+
if (!t$2.isObjectExpression(expr)) return { error: "style is not a literal object" };
|
|
235
236
|
for (const prop of expr.properties) {
|
|
236
|
-
if (!t$
|
|
237
|
+
if (!t$2.isObjectProperty(prop)) return { error: "style contains spread or method" };
|
|
237
238
|
if (prop.computed) return { error: "style has computed key" };
|
|
238
239
|
let keyName = null;
|
|
239
|
-
if (t$
|
|
240
|
-
else if (t$
|
|
240
|
+
if (t$2.isIdentifier(prop.key)) keyName = prop.key.name;
|
|
241
|
+
else if (t$2.isStringLiteral(prop.key)) keyName = prop.key.value;
|
|
241
242
|
if (!keyName) return { error: "style has unsupported key" };
|
|
242
243
|
const v = prop.value;
|
|
243
244
|
if (typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
|
|
@@ -275,7 +276,7 @@ function formatJsxText(value) {
|
|
|
275
276
|
}
|
|
276
277
|
function meaningfulChildren(parent) {
|
|
277
278
|
return parent.children.filter((c) => {
|
|
278
|
-
if (t$
|
|
279
|
+
if (t$2.isJSXText(c)) return c.value.trim() !== "";
|
|
279
280
|
return true;
|
|
280
281
|
});
|
|
281
282
|
}
|
|
@@ -291,7 +292,7 @@ function wrapSplice(parent, text) {
|
|
|
291
292
|
function collectTextCandidates(element, out) {
|
|
292
293
|
const meaningful = meaningfulChildren(element);
|
|
293
294
|
const isSole = meaningful.length === 1;
|
|
294
|
-
for (const child of meaningful) if (t$
|
|
295
|
+
for (const child of meaningful) if (t$2.isJSXText(child)) {
|
|
295
296
|
const current = child.value.trim();
|
|
296
297
|
if (!current) continue;
|
|
297
298
|
out.push({
|
|
@@ -302,9 +303,9 @@ function collectTextCandidates(element, out) {
|
|
|
302
303
|
text: formatJsxText(v)
|
|
303
304
|
}
|
|
304
305
|
});
|
|
305
|
-
} else if (t$
|
|
306
|
+
} else if (t$2.isJSXExpressionContainer(child)) {
|
|
306
307
|
const expr = child.expression;
|
|
307
|
-
if (t$
|
|
308
|
+
if (t$2.isStringLiteral(expr) || t$2.isNumericLiteral(expr)) {
|
|
308
309
|
const current = String(expr.value);
|
|
309
310
|
out.push({
|
|
310
311
|
current,
|
|
@@ -315,14 +316,14 @@ function collectTextCandidates(element, out) {
|
|
|
315
316
|
}
|
|
316
317
|
});
|
|
317
318
|
}
|
|
318
|
-
} else if (t$
|
|
319
|
+
} else if (t$2.isJSXElement(child) || t$2.isJSXFragment(child)) collectTextCandidates(child, out);
|
|
319
320
|
}
|
|
320
321
|
function propPassthroughName(element) {
|
|
321
322
|
const meaningful = meaningfulChildren(element);
|
|
322
323
|
if (meaningful.length !== 1) return null;
|
|
323
324
|
const child = meaningful[0];
|
|
324
|
-
if (!t$
|
|
325
|
-
return t$
|
|
325
|
+
if (!t$2.isJSXExpressionContainer(child)) return null;
|
|
326
|
+
return t$2.isIdentifier(child.expression) ? child.expression.name : null;
|
|
326
327
|
}
|
|
327
328
|
function findEnclosingComponent(ast, target) {
|
|
328
329
|
let best = null;
|
|
@@ -344,17 +345,17 @@ function findEnclosingComponent(ast, target) {
|
|
|
344
345
|
}
|
|
345
346
|
};
|
|
346
347
|
const visitDecl = (decl) => {
|
|
347
|
-
if (t$
|
|
348
|
-
else if (t$
|
|
349
|
-
if (!t$
|
|
350
|
-
if (t$
|
|
348
|
+
if (t$2.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
|
|
349
|
+
else if (t$2.isVariableDeclaration(decl)) for (const d of decl.declarations) {
|
|
350
|
+
if (!t$2.isVariableDeclarator(d) || !t$2.isIdentifier(d.id) || !d.init) continue;
|
|
351
|
+
if (t$2.isArrowFunctionExpression(d.init) || t$2.isFunctionExpression(d.init)) consider(d.id.name, d.init);
|
|
351
352
|
}
|
|
352
353
|
};
|
|
353
354
|
for (const decl of ast.program.body) {
|
|
354
355
|
visitDecl(decl);
|
|
355
|
-
if (t$
|
|
356
|
+
if (t$2.isExportNamedDeclaration(decl) || t$2.isExportDefaultDeclaration(decl)) {
|
|
356
357
|
const inner = decl.declaration;
|
|
357
|
-
if (inner && (t$
|
|
358
|
+
if (inner && (t$2.isStatement(inner) || t$2.isFunctionDeclaration(inner))) visitDecl(inner);
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
return best;
|
|
@@ -362,21 +363,21 @@ function findEnclosingComponent(ast, target) {
|
|
|
362
363
|
function componentDestructuresProp(fn, propName) {
|
|
363
364
|
if (fn.params.length === 0) return false;
|
|
364
365
|
let first = fn.params[0];
|
|
365
|
-
if (t$
|
|
366
|
-
if (!t$
|
|
366
|
+
if (t$2.isAssignmentPattern(first)) first = first.left;
|
|
367
|
+
if (!t$2.isObjectPattern(first)) return false;
|
|
367
368
|
for (const prop of first.properties) {
|
|
368
|
-
if (!t$
|
|
369
|
-
if (t$
|
|
370
|
-
if (t$
|
|
369
|
+
if (!t$2.isObjectProperty(prop)) continue;
|
|
370
|
+
if (t$2.isIdentifier(prop.key) && prop.key.name === propName) return true;
|
|
371
|
+
if (t$2.isStringLiteral(prop.key) && prop.key.value === propName) return true;
|
|
371
372
|
}
|
|
372
373
|
return false;
|
|
373
374
|
}
|
|
374
375
|
function collectCallSiteCandidates(ast, componentName) {
|
|
375
376
|
const out = [];
|
|
376
377
|
walkJsx(ast, (n) => {
|
|
377
|
-
if (!t$
|
|
378
|
+
if (!t$2.isJSXElement(n)) return;
|
|
378
379
|
const elName = n.openingElement.name;
|
|
379
|
-
if (t$
|
|
380
|
+
if (t$2.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
|
|
380
381
|
});
|
|
381
382
|
return out;
|
|
382
383
|
}
|
|
@@ -394,19 +395,19 @@ function spliceRange(node, text) {
|
|
|
394
395
|
function collectPropCallSiteCandidates(ast, componentName, propName) {
|
|
395
396
|
const out = [];
|
|
396
397
|
walkJsx(ast, (n) => {
|
|
397
|
-
if (!t$
|
|
398
|
+
if (!t$2.isJSXElement(n)) return;
|
|
398
399
|
const elName = n.openingElement.name;
|
|
399
|
-
if (!t$
|
|
400
|
+
if (!t$2.isJSXIdentifier(elName) || elName.name !== componentName) return;
|
|
400
401
|
const attr = findJsxAttr(n.openingElement, propName);
|
|
401
402
|
if (!attr?.value) return;
|
|
402
403
|
const v = attr.value;
|
|
403
|
-
if (t$
|
|
404
|
+
if (t$2.isStringLiteral(v)) out.push({
|
|
404
405
|
current: v.value,
|
|
405
406
|
splice: (s) => spliceRange(v, formatJsxAttrValue(s))
|
|
406
407
|
});
|
|
407
|
-
else if (t$
|
|
408
|
+
else if (t$2.isJSXExpressionContainer(v)) {
|
|
408
409
|
const expr = v.expression;
|
|
409
|
-
if (t$
|
|
410
|
+
if (t$2.isStringLiteral(expr) || t$2.isNumericLiteral(expr)) out.push({
|
|
410
411
|
current: String(expr.value),
|
|
411
412
|
splice: (s) => spliceRange(v, formatJsxAttrValue(s))
|
|
412
413
|
});
|
|
@@ -419,17 +420,17 @@ function findEnclosingMapCallback(ast, target) {
|
|
|
419
420
|
const targetStart = target.start ?? 0;
|
|
420
421
|
const targetEnd = target.end ?? 0;
|
|
421
422
|
walkAll(ast, (node) => {
|
|
422
|
-
if (!t$
|
|
423
|
+
if (!t$2.isCallExpression(node)) return;
|
|
423
424
|
const callee = node.callee;
|
|
424
|
-
if (!t$
|
|
425
|
-
if (!t$
|
|
425
|
+
if (!t$2.isMemberExpression(callee) || callee.computed) return;
|
|
426
|
+
if (!t$2.isIdentifier(callee.property)) return;
|
|
426
427
|
if (callee.property.name !== "map" && callee.property.name !== "flatMap") return;
|
|
427
428
|
const fn = node.arguments[0];
|
|
428
|
-
if (!fn || !t$
|
|
429
|
+
if (!fn || !t$2.isArrowFunctionExpression(fn) && !t$2.isFunctionExpression(fn)) return;
|
|
429
430
|
const fnStart = fn.start ?? 0;
|
|
430
431
|
const fnEnd = fn.end ?? 0;
|
|
431
432
|
if (fnStart > targetStart || fnEnd < targetEnd) return;
|
|
432
|
-
if (!t$
|
|
433
|
+
if (!t$2.isExpression(callee.object)) return;
|
|
433
434
|
const size = fnEnd - fnStart;
|
|
434
435
|
if (!best || size < best.size) best = {
|
|
435
436
|
fn,
|
|
@@ -446,26 +447,26 @@ function findEnclosingMapCallback(ast, target) {
|
|
|
446
447
|
}
|
|
447
448
|
function resolveArrayLiteralElements(ast, expr) {
|
|
448
449
|
const dropHoles = (arr) => arr.elements.filter((e) => e != null);
|
|
449
|
-
if (t$
|
|
450
|
-
if (!t$
|
|
450
|
+
if (t$2.isArrayExpression(expr)) return dropHoles(expr);
|
|
451
|
+
if (!t$2.isIdentifier(expr)) return null;
|
|
451
452
|
const name = expr.name;
|
|
452
453
|
const useStart = expr.start ?? 0;
|
|
453
454
|
let init = null;
|
|
454
455
|
walkAll(ast, (node) => {
|
|
455
|
-
if (!t$
|
|
456
|
-
if (!t$
|
|
457
|
-
if (!node.init || !t$
|
|
456
|
+
if (!t$2.isVariableDeclarator(node)) return;
|
|
457
|
+
if (!t$2.isIdentifier(node.id) || node.id.name !== name) return;
|
|
458
|
+
if (!node.init || !t$2.isArrayExpression(node.init)) return;
|
|
458
459
|
if ((node.init.start ?? 0) > useStart) return;
|
|
459
460
|
init = node.init;
|
|
460
461
|
});
|
|
461
462
|
return init ? dropHoles(init) : null;
|
|
462
463
|
}
|
|
463
464
|
function findObjectProperty(obj, name) {
|
|
464
|
-
if (!t$
|
|
465
|
+
if (!t$2.isObjectExpression(obj)) return null;
|
|
465
466
|
for (const prop of obj.properties) {
|
|
466
|
-
if (!t$
|
|
467
|
-
if (t$
|
|
468
|
-
if (t$
|
|
467
|
+
if (!t$2.isObjectProperty(prop) || prop.computed) continue;
|
|
468
|
+
if (t$2.isIdentifier(prop.key) && prop.key.name === name) return prop;
|
|
469
|
+
if (t$2.isStringLiteral(prop.key) && prop.key.value === name) return prop;
|
|
469
470
|
}
|
|
470
471
|
return null;
|
|
471
472
|
}
|
|
@@ -473,22 +474,22 @@ function decodeMapPassthrough(element, callbackParam) {
|
|
|
473
474
|
const meaningful = meaningfulChildren(element);
|
|
474
475
|
if (meaningful.length !== 1) return null;
|
|
475
476
|
const child = meaningful[0];
|
|
476
|
-
if (!t$
|
|
477
|
+
if (!t$2.isJSXExpressionContainer(child)) return null;
|
|
477
478
|
const expr = child.expression;
|
|
478
|
-
if (t$
|
|
479
|
+
if (t$2.isMemberExpression(expr)) {
|
|
479
480
|
if (expr.computed) return null;
|
|
480
|
-
if (!t$
|
|
481
|
-
if (!callbackParam || !t$
|
|
481
|
+
if (!t$2.isIdentifier(expr.object) || !t$2.isIdentifier(expr.property)) return null;
|
|
482
|
+
if (!callbackParam || !t$2.isIdentifier(callbackParam)) return null;
|
|
482
483
|
if (callbackParam.name !== expr.object.name) return null;
|
|
483
484
|
return expr.property.name;
|
|
484
485
|
}
|
|
485
|
-
if (t$
|
|
486
|
+
if (t$2.isIdentifier(expr)) {
|
|
486
487
|
const fieldName = expr.name;
|
|
487
|
-
if (!callbackParam || !t$
|
|
488
|
+
if (!callbackParam || !t$2.isObjectPattern(callbackParam)) return null;
|
|
488
489
|
for (const prop of callbackParam.properties) {
|
|
489
|
-
if (!t$
|
|
490
|
-
if (!t$
|
|
491
|
-
return t$
|
|
490
|
+
if (!t$2.isObjectProperty(prop) || prop.computed) continue;
|
|
491
|
+
if (!t$2.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
|
|
492
|
+
return t$2.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
|
|
492
493
|
}
|
|
493
494
|
}
|
|
494
495
|
return null;
|
|
@@ -505,11 +506,11 @@ function collectArrayMapCandidates(ast, element) {
|
|
|
505
506
|
const prop = findObjectProperty(obj, fieldName);
|
|
506
507
|
if (!prop) continue;
|
|
507
508
|
const v = prop.value;
|
|
508
|
-
if (t$
|
|
509
|
+
if (t$2.isStringLiteral(v)) out.push({
|
|
509
510
|
current: v.value,
|
|
510
511
|
splice: (s) => spliceRange(v, jsString$1(s))
|
|
511
512
|
});
|
|
512
|
-
else if (t$
|
|
513
|
+
else if (t$2.isNumericLiteral(v)) out.push({
|
|
513
514
|
current: String(v.value),
|
|
514
515
|
splice: (s) => spliceRange(v, jsString$1(s))
|
|
515
516
|
});
|
|
@@ -538,9 +539,9 @@ function buildTextSplice(ast, element, value, prevText) {
|
|
|
538
539
|
function findImports$1(ast) {
|
|
539
540
|
const out = [];
|
|
540
541
|
for (const node of ast.program.body) {
|
|
541
|
-
if (!t$
|
|
542
|
+
if (!t$2.isImportDeclaration(node)) continue;
|
|
542
543
|
let def = null;
|
|
543
|
-
for (const spec of node.specifiers) if (t$
|
|
544
|
+
for (const spec of node.specifiers) if (t$2.isImportDefaultSpecifier(spec)) {
|
|
544
545
|
def = spec.local.name;
|
|
545
546
|
break;
|
|
546
547
|
}
|
|
@@ -556,7 +557,7 @@ function collectTopLevelIdentifiers(ast) {
|
|
|
556
557
|
const names = new Set();
|
|
557
558
|
for (const imp of findImports$1(ast)) {
|
|
558
559
|
if (imp.defaultIdent) names.add(imp.defaultIdent);
|
|
559
|
-
for (const spec of imp.node.specifiers) if (!t$
|
|
560
|
+
for (const spec of imp.node.specifiers) if (!t$2.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
|
|
560
561
|
}
|
|
561
562
|
return names;
|
|
562
563
|
}
|
|
@@ -625,21 +626,21 @@ function readJsxStringAttr(opening, name) {
|
|
|
625
626
|
const attr = findJsxAttr(opening, name);
|
|
626
627
|
const v = attr?.value;
|
|
627
628
|
if (!v) return null;
|
|
628
|
-
if (t$
|
|
629
|
-
if (t$
|
|
629
|
+
if (t$2.isStringLiteral(v)) return v.value;
|
|
630
|
+
if (t$2.isJSXExpressionContainer(v) && t$2.isStringLiteral(v.expression)) return v.expression.value;
|
|
630
631
|
return null;
|
|
631
632
|
}
|
|
632
633
|
function readJsxNumberAttr(opening, name) {
|
|
633
634
|
const attr = findJsxAttr(opening, name);
|
|
634
635
|
const v = attr?.value;
|
|
635
|
-
if (!v || !t$
|
|
636
|
-
if (!t$
|
|
636
|
+
if (!v || !t$2.isJSXExpressionContainer(v)) return null;
|
|
637
|
+
if (!t$2.isNumericLiteral(v.expression)) return null;
|
|
637
638
|
const n = v.expression.value;
|
|
638
639
|
return Number.isFinite(n) ? n : null;
|
|
639
640
|
}
|
|
640
641
|
function planReplacePlaceholder(ast, element, assetPath) {
|
|
641
642
|
const opening = element.openingElement;
|
|
642
|
-
if (!t$
|
|
643
|
+
if (!t$2.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
|
|
643
644
|
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
644
645
|
const hint = readJsxStringAttr(opening, "hint") ?? "";
|
|
645
646
|
const width = readJsxNumberAttr(opening, "width");
|
|
@@ -663,7 +664,7 @@ function applyEdit(source, line, column, ops) {
|
|
|
663
664
|
ok: true,
|
|
664
665
|
source
|
|
665
666
|
};
|
|
666
|
-
const ast = parseSource$
|
|
667
|
+
const ast = parseSource$2(source);
|
|
667
668
|
if (!ast) return {
|
|
668
669
|
ok: false,
|
|
669
670
|
status: 422,
|
|
@@ -759,38 +760,38 @@ function commentsPlugin(opts) {
|
|
|
759
760
|
if (method !== "POST") return next();
|
|
760
761
|
try {
|
|
761
762
|
if (url.pathname === "/") {
|
|
762
|
-
const body = await readBody$
|
|
763
|
+
const body = await readBody$3(req);
|
|
763
764
|
const slideId = body.slideId ?? "";
|
|
764
|
-
const file = resolveSlidePath$
|
|
765
|
-
if (!file) return json$
|
|
766
|
-
if (!body.line || body.line < 1) return json$
|
|
767
|
-
if (!Array.isArray(body.ops)) return json$
|
|
765
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
766
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
767
|
+
if (!body.line || body.line < 1) return json$3(res, 400, { error: "invalid line" });
|
|
768
|
+
if (!Array.isArray(body.ops)) return json$3(res, 400, { error: "missing ops" });
|
|
768
769
|
let source;
|
|
769
770
|
try {
|
|
770
771
|
source = await fs.readFile(file, "utf8");
|
|
771
772
|
} catch {
|
|
772
|
-
return json$
|
|
773
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
773
774
|
}
|
|
774
775
|
const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
|
|
775
|
-
if (!result.ok) return json$
|
|
776
|
+
if (!result.ok) return json$3(res, result.status, { error: result.error });
|
|
776
777
|
const changed = result.source !== source;
|
|
777
778
|
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
778
|
-
return json$
|
|
779
|
+
return json$3(res, 200, {
|
|
779
780
|
ok: true,
|
|
780
781
|
changed
|
|
781
782
|
});
|
|
782
783
|
}
|
|
783
784
|
if (url.pathname === "/batch") {
|
|
784
|
-
const body = await readBody$
|
|
785
|
+
const body = await readBody$3(req);
|
|
785
786
|
const slideId = body.slideId ?? "";
|
|
786
|
-
const file = resolveSlidePath$
|
|
787
|
-
if (!file) return json$
|
|
788
|
-
if (!Array.isArray(body.edits)) return json$
|
|
787
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
788
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
789
|
+
if (!Array.isArray(body.edits)) return json$3(res, 400, { error: "missing edits" });
|
|
789
790
|
let source;
|
|
790
791
|
try {
|
|
791
792
|
source = await fs.readFile(file, "utf8");
|
|
792
793
|
} catch {
|
|
793
|
-
return json$
|
|
794
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
794
795
|
}
|
|
795
796
|
const original = source;
|
|
796
797
|
const results = [];
|
|
@@ -813,7 +814,7 @@ function commentsPlugin(opts) {
|
|
|
813
814
|
}
|
|
814
815
|
const changed = source !== original;
|
|
815
816
|
if (changed) await fs.writeFile(file, source, "utf8");
|
|
816
|
-
return json$
|
|
817
|
+
return json$3(res, 200, {
|
|
817
818
|
ok: true,
|
|
818
819
|
changed,
|
|
819
820
|
results
|
|
@@ -821,7 +822,7 @@ function commentsPlugin(opts) {
|
|
|
821
822
|
}
|
|
822
823
|
return next();
|
|
823
824
|
} catch (err) {
|
|
824
|
-
json$
|
|
825
|
+
json$3(res, 500, { error: String(err.message ?? err) });
|
|
825
826
|
}
|
|
826
827
|
});
|
|
827
828
|
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
@@ -830,31 +831,31 @@ function commentsPlugin(opts) {
|
|
|
830
831
|
try {
|
|
831
832
|
if (method === "GET" && url.pathname === "/") {
|
|
832
833
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
833
|
-
const file = resolveSlidePath$
|
|
834
|
-
if (!file) return json$
|
|
834
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
835
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
835
836
|
let source;
|
|
836
837
|
try {
|
|
837
838
|
source = await fs.readFile(file, "utf8");
|
|
838
839
|
} catch {
|
|
839
|
-
return json$
|
|
840
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
840
841
|
}
|
|
841
|
-
return json$
|
|
842
|
+
return json$3(res, 200, { comments: parseMarkers(source) });
|
|
842
843
|
}
|
|
843
844
|
if (method === "POST" && url.pathname === "/add") {
|
|
844
|
-
const body = await readBody$
|
|
845
|
+
const body = await readBody$3(req);
|
|
845
846
|
const slideId = body.slideId ?? "";
|
|
846
|
-
const file = resolveSlidePath$
|
|
847
|
-
if (!file) return json$
|
|
848
|
-
if (!body.line || body.line < 1) return json$
|
|
849
|
-
if (!body.text || typeof body.text !== "string") return json$
|
|
847
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
848
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
849
|
+
if (!body.line || body.line < 1) return json$3(res, 400, { error: "invalid line" });
|
|
850
|
+
if (!body.text || typeof body.text !== "string") return json$3(res, 400, { error: "missing text" });
|
|
850
851
|
let source;
|
|
851
852
|
try {
|
|
852
853
|
source = await fs.readFile(file, "utf8");
|
|
853
854
|
} catch {
|
|
854
|
-
return json$
|
|
855
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
855
856
|
}
|
|
856
857
|
const plan = findInsertion(source, body.line, body.column);
|
|
857
|
-
if (!plan) return json$
|
|
858
|
+
if (!plan) return json$3(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
|
|
858
859
|
const id = newId();
|
|
859
860
|
const ts = new Date().toISOString();
|
|
860
861
|
const payload = b64urlEncode(JSON.stringify({
|
|
@@ -865,44 +866,118 @@ function commentsPlugin(opts) {
|
|
|
865
866
|
const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
|
|
866
867
|
await fs.writeFile(file, next$1, "utf8");
|
|
867
868
|
const markerLine = offsetToLine(next$1, plan.offset + 1);
|
|
868
|
-
return json$
|
|
869
|
+
return json$3(res, 200, {
|
|
869
870
|
id,
|
|
870
871
|
line: markerLine
|
|
871
872
|
});
|
|
872
873
|
}
|
|
873
874
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
874
875
|
const id = url.pathname.slice(1);
|
|
875
|
-
if (!/^c-[a-f0-9]+$/.test(id)) return json$
|
|
876
|
+
if (!/^c-[a-f0-9]+$/.test(id)) return json$3(res, 400, { error: "invalid id" });
|
|
876
877
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
877
|
-
const file = resolveSlidePath$
|
|
878
|
-
if (!file) return json$
|
|
878
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
879
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
879
880
|
let source;
|
|
880
881
|
try {
|
|
881
882
|
source = await fs.readFile(file, "utf8");
|
|
882
883
|
} catch {
|
|
883
|
-
return json$
|
|
884
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
884
885
|
}
|
|
885
886
|
const lines = source.split("\n");
|
|
886
887
|
const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
887
888
|
const hit = lines.findIndex((l) => idRe.test(l));
|
|
888
|
-
if (hit === -1) return json$
|
|
889
|
+
if (hit === -1) return json$3(res, 404, { error: "marker not found" });
|
|
889
890
|
lines.splice(hit, 1);
|
|
890
891
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
891
|
-
return json$
|
|
892
|
+
return json$3(res, 200, { ok: true });
|
|
892
893
|
}
|
|
893
894
|
next();
|
|
894
895
|
} catch (err) {
|
|
895
|
-
json$
|
|
896
|
+
json$3(res, 500, { error: String(err.message ?? err) });
|
|
896
897
|
}
|
|
897
898
|
});
|
|
898
899
|
}
|
|
899
900
|
};
|
|
900
901
|
}
|
|
901
902
|
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/vite/current-plugin.ts
|
|
905
|
+
const SLIDE_ID_RE$3 = /^[a-z0-9_-]+$/i;
|
|
906
|
+
const TEXT_SNIPPET_MAX = 120;
|
|
907
|
+
function parseSelection(raw) {
|
|
908
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
909
|
+
const sel = raw;
|
|
910
|
+
if (typeof sel.line !== "number" || !Number.isFinite(sel.line)) return null;
|
|
911
|
+
if (typeof sel.column !== "number" || !Number.isFinite(sel.column)) return null;
|
|
912
|
+
const tagName = typeof sel.tagName === "string" ? sel.tagName.toLowerCase().slice(0, 32) : "unknown";
|
|
913
|
+
const text = typeof sel.text === "string" ? sel.text.replace(/\s+/g, " ").trim().slice(0, TEXT_SNIPPET_MAX) : "";
|
|
914
|
+
return {
|
|
915
|
+
line: Math.max(1, Math.floor(sel.line)),
|
|
916
|
+
column: Math.max(0, Math.floor(sel.column)),
|
|
917
|
+
tagName,
|
|
918
|
+
text
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function currentPlugin(opts) {
|
|
922
|
+
const userCwd = opts.userCwd;
|
|
923
|
+
const slidesDir = opts.slidesDir ?? "slides";
|
|
924
|
+
const outDir = path.join(userCwd, "node_modules", ".open-slide");
|
|
925
|
+
const outFile = path.join(outDir, "current.json");
|
|
926
|
+
const tmpFile = `${outFile}.tmp`;
|
|
927
|
+
let cached = null;
|
|
928
|
+
return {
|
|
929
|
+
name: "open-slide:current",
|
|
930
|
+
apply: "serve",
|
|
931
|
+
configureServer(server) {
|
|
932
|
+
server.ws.on("open-slide:current", async (raw) => {
|
|
933
|
+
const next = cached ? { ...cached } : {
|
|
934
|
+
slideId: "",
|
|
935
|
+
pageIndex: 0,
|
|
936
|
+
pageNumber: 1,
|
|
937
|
+
totalPages: 1,
|
|
938
|
+
slideTitle: "",
|
|
939
|
+
view: "slides",
|
|
940
|
+
pagePath: "",
|
|
941
|
+
selection: null
|
|
942
|
+
};
|
|
943
|
+
if (typeof raw?.slideId === "string") {
|
|
944
|
+
if (!SLIDE_ID_RE$3.test(raw.slideId)) return;
|
|
945
|
+
const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
|
|
946
|
+
const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
|
|
947
|
+
const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
|
|
948
|
+
const slideTitle = typeof raw.slideTitle === "string" ? raw.slideTitle : raw.slideId;
|
|
949
|
+
const view = raw.view === "assets" ? "assets" : "slides";
|
|
950
|
+
const pagePath = path.join(slidesDir, raw.slideId, "index.tsx").split(path.sep).join("/");
|
|
951
|
+
if (cached?.slideId !== raw.slideId || cached?.pageIndex !== pageIndex) next.selection = null;
|
|
952
|
+
next.slideId = raw.slideId;
|
|
953
|
+
next.pageIndex = pageIndex;
|
|
954
|
+
next.pageNumber = pageIndex + 1;
|
|
955
|
+
next.totalPages = totalPages;
|
|
956
|
+
next.slideTitle = slideTitle;
|
|
957
|
+
next.view = view;
|
|
958
|
+
next.pagePath = pagePath;
|
|
959
|
+
}
|
|
960
|
+
if ("selection" in raw) next.selection = parseSelection(raw.selection);
|
|
961
|
+
if (!next.slideId) return;
|
|
962
|
+
cached = next;
|
|
963
|
+
const body = {
|
|
964
|
+
...next,
|
|
965
|
+
updatedAt: new Date().toISOString()
|
|
966
|
+
};
|
|
967
|
+
try {
|
|
968
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
969
|
+
await fs.writeFile(tmpFile, `${JSON.stringify(body, null, 2)}\n`, "utf8");
|
|
970
|
+
await fs.rename(tmpFile, outFile);
|
|
971
|
+
} catch {}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
902
977
|
//#endregion
|
|
903
978
|
//#region src/vite/design-plugin.ts
|
|
904
|
-
const SLIDE_ID_RE$
|
|
905
|
-
async function readBody$
|
|
979
|
+
const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
|
|
980
|
+
async function readBody$2(req) {
|
|
906
981
|
return await new Promise((resolve, reject) => {
|
|
907
982
|
const chunks = [];
|
|
908
983
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -918,19 +993,19 @@ async function readBody$1(req) {
|
|
|
918
993
|
req.on("error", reject);
|
|
919
994
|
});
|
|
920
995
|
}
|
|
921
|
-
function json$
|
|
996
|
+
function json$2(res, status, body) {
|
|
922
997
|
res.statusCode = status;
|
|
923
998
|
res.setHeader("content-type", "application/json");
|
|
924
999
|
res.end(JSON.stringify(body));
|
|
925
1000
|
}
|
|
926
|
-
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
927
|
-
if (!SLIDE_ID_RE$
|
|
1001
|
+
function resolveSlidePath$1(userCwd, slidesDir, slideId) {
|
|
1002
|
+
if (!SLIDE_ID_RE$2.test(slideId)) return null;
|
|
928
1003
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
929
1004
|
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
930
1005
|
if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
|
|
931
1006
|
return full;
|
|
932
1007
|
}
|
|
933
|
-
function parseSource(source) {
|
|
1008
|
+
function parseSource$1(source) {
|
|
934
1009
|
try {
|
|
935
1010
|
return parse(source, {
|
|
936
1011
|
sourceType: "module",
|
|
@@ -1068,7 +1143,7 @@ function serializeDesign(design) {
|
|
|
1068
1143
|
return serializeValue(design, 0);
|
|
1069
1144
|
}
|
|
1070
1145
|
function parseSlideDesign(source) {
|
|
1071
|
-
const ast = parseSource(source);
|
|
1146
|
+
const ast = parseSource$1(source);
|
|
1072
1147
|
if (!ast) return {
|
|
1073
1148
|
ok: false,
|
|
1074
1149
|
exists: true,
|
|
@@ -1210,7 +1285,7 @@ function applyDesignWrite(source, next) {
|
|
|
1210
1285
|
error: `serialize failed: ${err.message}`
|
|
1211
1286
|
};
|
|
1212
1287
|
}
|
|
1213
|
-
const ast = parseSource(source);
|
|
1288
|
+
const ast = parseSource$1(source);
|
|
1214
1289
|
if (!ast) return {
|
|
1215
1290
|
ok: false,
|
|
1216
1291
|
status: 422,
|
|
@@ -1226,7 +1301,7 @@ function applyDesignWrite(source, next) {
|
|
|
1226
1301
|
};
|
|
1227
1302
|
}
|
|
1228
1303
|
const withImport = ensureDesignSystemImport(source, ast);
|
|
1229
|
-
const ast2 = parseSource(withImport.source);
|
|
1304
|
+
const ast2 = parseSource$1(withImport.source);
|
|
1230
1305
|
if (!ast2) return {
|
|
1231
1306
|
ok: false,
|
|
1232
1307
|
status: 422,
|
|
@@ -1252,51 +1327,51 @@ function designPlugin(opts) {
|
|
|
1252
1327
|
const url = new URL(req.url ?? "/", "http://local");
|
|
1253
1328
|
const method = req.method ?? "GET";
|
|
1254
1329
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
1255
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
1256
|
-
if (!file) return json$
|
|
1330
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
1331
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
1257
1332
|
try {
|
|
1258
1333
|
if (method === "GET" && url.pathname === "/") {
|
|
1259
1334
|
let source;
|
|
1260
1335
|
try {
|
|
1261
1336
|
source = await fs.readFile(file, "utf8");
|
|
1262
1337
|
} catch {
|
|
1263
|
-
return json$
|
|
1338
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1264
1339
|
}
|
|
1265
1340
|
const parsed = parseSlideDesign(source);
|
|
1266
|
-
if (parsed.ok) return json$
|
|
1341
|
+
if (parsed.ok) return json$2(res, 200, {
|
|
1267
1342
|
design: parsed.design,
|
|
1268
1343
|
exists: true,
|
|
1269
1344
|
warning: null
|
|
1270
1345
|
});
|
|
1271
|
-
if (parsed.exists === false) return json$
|
|
1346
|
+
if (parsed.exists === false) return json$2(res, 200, {
|
|
1272
1347
|
design: defaultDesign,
|
|
1273
1348
|
exists: false,
|
|
1274
1349
|
warning: null
|
|
1275
1350
|
});
|
|
1276
|
-
return json$
|
|
1351
|
+
return json$2(res, 200, {
|
|
1277
1352
|
design: defaultDesign,
|
|
1278
1353
|
exists: true,
|
|
1279
1354
|
warning: parsed.error
|
|
1280
1355
|
});
|
|
1281
1356
|
}
|
|
1282
1357
|
if (method === "PUT" && url.pathname === "/") {
|
|
1283
|
-
const body = await readBody$
|
|
1358
|
+
const body = await readBody$2(req);
|
|
1284
1359
|
const patch = body.patch;
|
|
1285
|
-
if (!patch || typeof patch !== "object") return json$
|
|
1360
|
+
if (!patch || typeof patch !== "object") return json$2(res, 400, { error: "missing patch object" });
|
|
1286
1361
|
let source;
|
|
1287
1362
|
try {
|
|
1288
1363
|
source = await fs.readFile(file, "utf8");
|
|
1289
1364
|
} catch {
|
|
1290
|
-
return json$
|
|
1365
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1291
1366
|
}
|
|
1292
1367
|
const parsed = parseSlideDesign(source);
|
|
1293
1368
|
const baseDesign = parsed.ok ? parsed.design : defaultDesign;
|
|
1294
|
-
if (!parsed.ok && parsed.exists) return json$
|
|
1369
|
+
if (!parsed.ok && parsed.exists) return json$2(res, 422, { error: parsed.error });
|
|
1295
1370
|
const merged = mergeDesign(baseDesign, patch);
|
|
1296
1371
|
const written = applyDesignWrite(source, merged);
|
|
1297
|
-
if (!written.ok) return json$
|
|
1372
|
+
if (!written.ok) return json$2(res, written.status, { error: written.error });
|
|
1298
1373
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1299
|
-
return json$
|
|
1374
|
+
return json$2(res, 200, {
|
|
1300
1375
|
ok: true,
|
|
1301
1376
|
design: merged,
|
|
1302
1377
|
created: written.created
|
|
@@ -1307,12 +1382,12 @@ function designPlugin(opts) {
|
|
|
1307
1382
|
try {
|
|
1308
1383
|
source = await fs.readFile(file, "utf8");
|
|
1309
1384
|
} catch {
|
|
1310
|
-
return json$
|
|
1385
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1311
1386
|
}
|
|
1312
1387
|
const written = applyDesignWrite(source, defaultDesign);
|
|
1313
|
-
if (!written.ok) return json$
|
|
1388
|
+
if (!written.ok) return json$2(res, written.status, { error: written.error });
|
|
1314
1389
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1315
|
-
return json$
|
|
1390
|
+
return json$2(res, 200, {
|
|
1316
1391
|
ok: true,
|
|
1317
1392
|
design: defaultDesign,
|
|
1318
1393
|
created: written.created
|
|
@@ -1320,7 +1395,7 @@ function designPlugin(opts) {
|
|
|
1320
1395
|
}
|
|
1321
1396
|
return next();
|
|
1322
1397
|
} catch (err) {
|
|
1323
|
-
json$
|
|
1398
|
+
json$2(res, 500, { error: String(err.message ?? err) });
|
|
1324
1399
|
}
|
|
1325
1400
|
});
|
|
1326
1401
|
}
|
|
@@ -1330,7 +1405,7 @@ function designPlugin(opts) {
|
|
|
1330
1405
|
//#endregion
|
|
1331
1406
|
//#region src/vite/files-plugin.ts
|
|
1332
1407
|
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
1333
|
-
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
1408
|
+
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
1334
1409
|
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
1335
1410
|
const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
|
|
1336
1411
|
const ASSET_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -1371,7 +1446,7 @@ function validateAssetName(v) {
|
|
|
1371
1446
|
if (dot <= 0 || dot === trimmed.length - 1) return null;
|
|
1372
1447
|
return trimmed;
|
|
1373
1448
|
}
|
|
1374
|
-
async function readBody(req) {
|
|
1449
|
+
async function readBody$1(req) {
|
|
1375
1450
|
return await new Promise((resolve, reject) => {
|
|
1376
1451
|
const chunks = [];
|
|
1377
1452
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -1387,7 +1462,7 @@ async function readBody(req) {
|
|
|
1387
1462
|
req.on("error", reject);
|
|
1388
1463
|
});
|
|
1389
1464
|
}
|
|
1390
|
-
function json(res, status, body) {
|
|
1465
|
+
function json$1(res, status, body) {
|
|
1391
1466
|
res.statusCode = status;
|
|
1392
1467
|
res.setHeader("content-type", "application/json");
|
|
1393
1468
|
res.end(JSON.stringify(body));
|
|
@@ -1431,7 +1506,7 @@ function validateSlideName(v) {
|
|
|
1431
1506
|
return trimmed;
|
|
1432
1507
|
}
|
|
1433
1508
|
async function rmSlideDir(slidesRoot, slideId) {
|
|
1434
|
-
if (!SLIDE_ID_RE.test(slideId)) return false;
|
|
1509
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return false;
|
|
1435
1510
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1436
1511
|
if (!dir.startsWith(slidesRoot + path.sep)) return false;
|
|
1437
1512
|
try {
|
|
@@ -1445,7 +1520,7 @@ async function rmSlideDir(slidesRoot, slideId) {
|
|
|
1445
1520
|
}
|
|
1446
1521
|
}
|
|
1447
1522
|
function resolveAssetsDir(slidesRoot, slideId) {
|
|
1448
|
-
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1523
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
1449
1524
|
const slideDir = path.resolve(slidesRoot, slideId);
|
|
1450
1525
|
if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
|
|
1451
1526
|
const assetsDir = path.resolve(slideDir, "assets");
|
|
@@ -1461,7 +1536,7 @@ function resolveAssetFile(slidesRoot, slideId, filename) {
|
|
|
1461
1536
|
return file;
|
|
1462
1537
|
}
|
|
1463
1538
|
function resolveSlideEntry(slidesRoot, slideId) {
|
|
1464
|
-
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1539
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
1465
1540
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1466
1541
|
if (!dir.startsWith(slidesRoot + path.sep)) return null;
|
|
1467
1542
|
return path.join(dir, "index.tsx");
|
|
@@ -1602,6 +1677,158 @@ function reorderDefaultExportPagesInSource(source, order) {
|
|
|
1602
1677
|
rebuilt += suffix;
|
|
1603
1678
|
return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
|
|
1604
1679
|
}
|
|
1680
|
+
function findNotesArray(source) {
|
|
1681
|
+
let ast;
|
|
1682
|
+
try {
|
|
1683
|
+
ast = parse(source, {
|
|
1684
|
+
sourceType: "module",
|
|
1685
|
+
plugins: ["typescript", "jsx"],
|
|
1686
|
+
errorRecovery: true
|
|
1687
|
+
});
|
|
1688
|
+
} catch {
|
|
1689
|
+
return "invalid";
|
|
1690
|
+
}
|
|
1691
|
+
const body = ast.program?.body ?? [];
|
|
1692
|
+
for (const stmt of body) {
|
|
1693
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
1694
|
+
const decl = stmt.declaration;
|
|
1695
|
+
if (!decl || decl.type !== "VariableDeclaration") continue;
|
|
1696
|
+
const declarations = decl.declarations ?? [];
|
|
1697
|
+
for (const d of declarations) {
|
|
1698
|
+
const id = d.id;
|
|
1699
|
+
if (!id || id.type !== "Identifier" || id.name !== "notes") continue;
|
|
1700
|
+
const init = d.init;
|
|
1701
|
+
if (!init || init.type !== "ArrayExpression") return "invalid";
|
|
1702
|
+
const arrayStart = init.start;
|
|
1703
|
+
const arrayEnd = init.end;
|
|
1704
|
+
if (typeof arrayStart !== "number" || typeof arrayEnd !== "number") return "invalid";
|
|
1705
|
+
const rawElements = init.elements ?? [];
|
|
1706
|
+
const elementTexts = [];
|
|
1707
|
+
for (const el of rawElements) {
|
|
1708
|
+
if (el === null) {
|
|
1709
|
+
elementTexts.push("undefined");
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (el.type === "SpreadElement") return "invalid";
|
|
1713
|
+
const start = el.start;
|
|
1714
|
+
const end = el.end;
|
|
1715
|
+
if (typeof start !== "number" || typeof end !== "number") return "invalid";
|
|
1716
|
+
elementTexts.push(source.slice(start, end));
|
|
1717
|
+
}
|
|
1718
|
+
return {
|
|
1719
|
+
arrayStart,
|
|
1720
|
+
arrayEnd,
|
|
1721
|
+
elementTexts
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Reorder `export const notes = [...]` to follow the page-array reorder.
|
|
1729
|
+
*
|
|
1730
|
+
* `order[i]` is the original page index that should land at new position `i`.
|
|
1731
|
+
* The notes array is index-aligned with the pages array but may be shorter
|
|
1732
|
+
* (trailing `undefined` slots are routinely trimmed). Missing elements are
|
|
1733
|
+
* treated as `undefined`, and trailing `undefined` is trimmed again after
|
|
1734
|
+
* reordering to keep the file tidy.
|
|
1735
|
+
*
|
|
1736
|
+
* Returns the rewritten source, the original source if no `notes` export
|
|
1737
|
+
* exists or the reorder is a no-op, or `null` if the `notes` export's shape
|
|
1738
|
+
* is too surprising to touch safely.
|
|
1739
|
+
*/
|
|
1740
|
+
function reorderNotesArrayInSource(source, order) {
|
|
1741
|
+
for (const idx of order) if (!Number.isInteger(idx) || idx < 0) return null;
|
|
1742
|
+
const found = findNotesArray(source);
|
|
1743
|
+
if (found === "invalid") return null;
|
|
1744
|
+
if (found === null) return source;
|
|
1745
|
+
const { arrayStart, arrayEnd, elementTexts } = found;
|
|
1746
|
+
const pick = (i) => i >= 0 && i < elementTexts.length ? elementTexts[i] : "undefined";
|
|
1747
|
+
const reordered = order.map(pick);
|
|
1748
|
+
while (reordered.length > 0 && reordered[reordered.length - 1] === "undefined") reordered.pop();
|
|
1749
|
+
const replacement = reordered.length === 0 ? "[]" : `[\n${reordered.map((s) => ` ${s},`).join("\n")}\n]`;
|
|
1750
|
+
if (replacement === source.slice(arrayStart, arrayEnd)) return source;
|
|
1751
|
+
return source.slice(0, arrayStart) + replacement + source.slice(arrayEnd);
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Remove the element at `index` from `export default [...]`.
|
|
1755
|
+
*
|
|
1756
|
+
* Preserves the source slice of every other element, dropping the separator
|
|
1757
|
+
* immediately following the removed element (or the preceding one when the
|
|
1758
|
+
* removed element is the last). Returns `null` when the default export isn't
|
|
1759
|
+
* an array literal or `index` is out of range.
|
|
1760
|
+
*/
|
|
1761
|
+
function removePageFromDefaultExportInSource(source, index) {
|
|
1762
|
+
const found = findDefaultExportArray(source);
|
|
1763
|
+
if (!found) return null;
|
|
1764
|
+
const { elements, arrayStart, arrayEnd } = found;
|
|
1765
|
+
const n = elements.length;
|
|
1766
|
+
if (!Number.isInteger(index) || index < 0 || index >= n) return null;
|
|
1767
|
+
if (n === 1) return `${source.slice(0, arrayStart)}[]${source.slice(arrayEnd)}`;
|
|
1768
|
+
const prefix = source.slice(arrayStart, elements[0].start);
|
|
1769
|
+
const suffix = source.slice(elements[n - 1].end, arrayEnd);
|
|
1770
|
+
const separators = [];
|
|
1771
|
+
for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
|
|
1772
|
+
const elementText = elements.map((el) => source.slice(el.start, el.end));
|
|
1773
|
+
const keptElements = [];
|
|
1774
|
+
const keptSeparators = [];
|
|
1775
|
+
for (let i = 0; i < n; i++) {
|
|
1776
|
+
if (i === index) continue;
|
|
1777
|
+
keptElements.push(elementText[i]);
|
|
1778
|
+
}
|
|
1779
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1780
|
+
if (index === n - 1 ? i === n - 2 : i === index) continue;
|
|
1781
|
+
keptSeparators.push(separators[i]);
|
|
1782
|
+
}
|
|
1783
|
+
let rebuilt = prefix + keptElements[0];
|
|
1784
|
+
for (let i = 1; i < keptElements.length; i++) rebuilt += keptSeparators[i - 1] + keptElements[i];
|
|
1785
|
+
rebuilt += suffix;
|
|
1786
|
+
return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
|
|
1787
|
+
}
|
|
1788
|
+
function chooseInsertSeparator(prefix, existingSeparators) {
|
|
1789
|
+
const sample = existingSeparators.find((s) => s.includes(","));
|
|
1790
|
+
if (sample) return sample;
|
|
1791
|
+
if (prefix.includes("\n")) {
|
|
1792
|
+
const m = prefix.match(/\n([ \t]*)$/);
|
|
1793
|
+
const indent$1 = m ? m[1] : " ";
|
|
1794
|
+
return `,\n${indent$1}`;
|
|
1795
|
+
}
|
|
1796
|
+
return ", ";
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Duplicate the element at `index` in `export default [...]`, inserting the
|
|
1800
|
+
* copy immediately after the original. Reuses an existing inter-element
|
|
1801
|
+
* separator when one is available so the cloned entry matches the surrounding
|
|
1802
|
+
* indentation. Returns `null` when the default export isn't an array literal
|
|
1803
|
+
* or `index` is out of range.
|
|
1804
|
+
*/
|
|
1805
|
+
function duplicatePageInDefaultExportInSource(source, index) {
|
|
1806
|
+
const found = findDefaultExportArray(source);
|
|
1807
|
+
if (!found) return null;
|
|
1808
|
+
const { elements, arrayStart, arrayEnd } = found;
|
|
1809
|
+
const n = elements.length;
|
|
1810
|
+
if (!Number.isInteger(index) || index < 0 || index >= n) return null;
|
|
1811
|
+
const prefix = source.slice(arrayStart, elements[0].start);
|
|
1812
|
+
const suffix = source.slice(elements[n - 1].end, arrayEnd);
|
|
1813
|
+
const separators = [];
|
|
1814
|
+
for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
|
|
1815
|
+
const elementText = elements.map((el) => source.slice(el.start, el.end));
|
|
1816
|
+
const insertSep = chooseInsertSeparator(prefix, separators);
|
|
1817
|
+
const newElements = [];
|
|
1818
|
+
const newSeparators = [];
|
|
1819
|
+
for (let i = 0; i < n; i++) {
|
|
1820
|
+
newElements.push(elementText[i]);
|
|
1821
|
+
if (i === index) {
|
|
1822
|
+
newElements.push(elementText[i]);
|
|
1823
|
+
newSeparators.push(insertSep);
|
|
1824
|
+
}
|
|
1825
|
+
if (i < n - 1) newSeparators.push(separators[i]);
|
|
1826
|
+
}
|
|
1827
|
+
let rebuilt = prefix + newElements[0];
|
|
1828
|
+
for (let i = 1; i < newElements.length; i++) rebuilt += newSeparators[i - 1] + newElements[i];
|
|
1829
|
+
rebuilt += suffix;
|
|
1830
|
+
return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
|
|
1831
|
+
}
|
|
1605
1832
|
function validateIcon(v) {
|
|
1606
1833
|
if (!v || typeof v !== "object") return null;
|
|
1607
1834
|
const icon = v;
|
|
@@ -1644,7 +1871,7 @@ function filesPlugin(opts) {
|
|
|
1644
1871
|
const parts = rel.split(path.sep);
|
|
1645
1872
|
if (parts.length < 3 || parts[1] !== "assets") return;
|
|
1646
1873
|
const slideId = parts[0];
|
|
1647
|
-
if (!SLIDE_ID_RE.test(slideId)) return;
|
|
1874
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return;
|
|
1648
1875
|
server.ws.send({
|
|
1649
1876
|
type: "custom",
|
|
1650
1877
|
event: "open-slide:assets-changed",
|
|
@@ -1661,52 +1888,81 @@ function filesPlugin(opts) {
|
|
|
1661
1888
|
const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
|
|
1662
1889
|
if (reorderMatch && method === "PUT") {
|
|
1663
1890
|
const slideId$1 = reorderMatch[1];
|
|
1664
|
-
if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
|
|
1665
|
-
const body = await readBody(req);
|
|
1666
|
-
if (!Array.isArray(body.order)) return json(res, 400, { error: "invalid order" });
|
|
1891
|
+
if (!SLIDE_ID_RE$1.test(slideId$1)) return json$1(res, 400, { error: "invalid slideId" });
|
|
1892
|
+
const body = await readBody$1(req);
|
|
1893
|
+
if (!Array.isArray(body.order)) return json$1(res, 400, { error: "invalid order" });
|
|
1667
1894
|
const order = [];
|
|
1668
1895
|
for (const v of body.order) {
|
|
1669
|
-
if (!Number.isInteger(v)) return json(res, 400, { error: "invalid order" });
|
|
1896
|
+
if (!Number.isInteger(v)) return json$1(res, 400, { error: "invalid order" });
|
|
1670
1897
|
order.push(v);
|
|
1671
1898
|
}
|
|
1672
1899
|
const entry = resolveSlideEntry(slidesRoot, slideId$1);
|
|
1673
|
-
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
1900
|
+
if (!entry) return json$1(res, 400, { error: "invalid slideId" });
|
|
1674
1901
|
let source;
|
|
1675
1902
|
try {
|
|
1676
1903
|
source = await fs.readFile(entry, "utf8");
|
|
1677
1904
|
} catch {
|
|
1678
|
-
return json(res, 404, { error: "slide not found" });
|
|
1905
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1679
1906
|
}
|
|
1680
|
-
const
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
return json(res,
|
|
1907
|
+
const reordered = reorderDefaultExportPagesInSource(source, order);
|
|
1908
|
+
if (reordered === null) return json$1(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
|
|
1909
|
+
const withNotes = reorderNotesArrayInSource(reordered, order);
|
|
1910
|
+
if (withNotes === null) return json$1(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
|
|
1911
|
+
if (withNotes !== source) await fs.writeFile(entry, withNotes, "utf8");
|
|
1912
|
+
return json$1(res, 200, {
|
|
1684
1913
|
ok: true,
|
|
1685
1914
|
slideId: slideId$1,
|
|
1686
1915
|
order
|
|
1687
1916
|
});
|
|
1688
1917
|
}
|
|
1918
|
+
const pageOpMatch = url.pathname.match(/^\/([^/]+)\/pages\/(\d+)(?:\/([a-z]+))?$/);
|
|
1919
|
+
if (pageOpMatch) {
|
|
1920
|
+
const slideId$1 = pageOpMatch[1];
|
|
1921
|
+
const pageIndex = Number.parseInt(pageOpMatch[2], 10);
|
|
1922
|
+
const op = pageOpMatch[3];
|
|
1923
|
+
if (!SLIDE_ID_RE$1.test(slideId$1)) return json$1(res, 400, { error: "invalid slideId" });
|
|
1924
|
+
if (!Number.isInteger(pageIndex) || pageIndex < 0) return json$1(res, 400, { error: "invalid page index" });
|
|
1925
|
+
const isDelete = method === "DELETE" && !op;
|
|
1926
|
+
const isDuplicate = method === "POST" && op === "duplicate";
|
|
1927
|
+
if (!isDelete && !isDuplicate) return next();
|
|
1928
|
+
const entry = resolveSlideEntry(slidesRoot, slideId$1);
|
|
1929
|
+
if (!entry) return json$1(res, 400, { error: "invalid slideId" });
|
|
1930
|
+
let source;
|
|
1931
|
+
try {
|
|
1932
|
+
source = await fs.readFile(entry, "utf8");
|
|
1933
|
+
} catch {
|
|
1934
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1935
|
+
}
|
|
1936
|
+
const updated = isDelete ? removePageFromDefaultExportInSource(source, pageIndex) : duplicatePageInDefaultExportInSource(source, pageIndex);
|
|
1937
|
+
if (updated === null) return json$1(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
|
|
1938
|
+
if (updated !== source) await fs.writeFile(entry, updated, "utf8");
|
|
1939
|
+
return json$1(res, 200, {
|
|
1940
|
+
ok: true,
|
|
1941
|
+
slideId: slideId$1,
|
|
1942
|
+
index: pageIndex
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1689
1945
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
1690
1946
|
if (!idMatch) return next();
|
|
1691
1947
|
const slideId = idMatch[1];
|
|
1692
|
-
if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
|
|
1948
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return json$1(res, 400, { error: "invalid slideId" });
|
|
1693
1949
|
if (method === "PATCH") {
|
|
1694
|
-
const body = await readBody(req);
|
|
1950
|
+
const body = await readBody$1(req);
|
|
1695
1951
|
const name = validateSlideName(body.name);
|
|
1696
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
1952
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1697
1953
|
const entry = resolveSlideEntry(slidesRoot, slideId);
|
|
1698
|
-
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
1954
|
+
if (!entry) return json$1(res, 400, { error: "invalid slideId" });
|
|
1699
1955
|
let source;
|
|
1700
1956
|
try {
|
|
1701
1957
|
source = await fs.readFile(entry, "utf8");
|
|
1702
1958
|
} catch {
|
|
1703
|
-
return json(res, 404, { error: "slide not found" });
|
|
1959
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1704
1960
|
}
|
|
1705
1961
|
const updated = updateMetaTitleInSource(source, name);
|
|
1706
|
-
if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
|
|
1962
|
+
if (updated === null) return json$1(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
|
|
1707
1963
|
if (updated !== source) await fs.writeFile(entry, updated, "utf8");
|
|
1708
1964
|
server.ws.send({ type: "full-reload" });
|
|
1709
|
-
return json(res, 200, {
|
|
1965
|
+
return json$1(res, 200, {
|
|
1710
1966
|
ok: true,
|
|
1711
1967
|
slideId,
|
|
1712
1968
|
name
|
|
@@ -1714,15 +1970,15 @@ function filesPlugin(opts) {
|
|
|
1714
1970
|
}
|
|
1715
1971
|
if (method === "DELETE") {
|
|
1716
1972
|
const removed = await rmSlideDir(slidesRoot, slideId);
|
|
1717
|
-
if (!removed) return json(res, 404, { error: "slide not found" });
|
|
1973
|
+
if (!removed) return json$1(res, 404, { error: "slide not found" });
|
|
1718
1974
|
const manifest = await readManifest(manifestPath);
|
|
1719
1975
|
delete manifest.assignments[slideId];
|
|
1720
1976
|
await writeManifest(manifestPath, manifest);
|
|
1721
|
-
return json(res, 200, { ok: true });
|
|
1977
|
+
return json$1(res, 200, { ok: true });
|
|
1722
1978
|
}
|
|
1723
1979
|
return next();
|
|
1724
1980
|
} catch (err) {
|
|
1725
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
1981
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1726
1982
|
}
|
|
1727
1983
|
});
|
|
1728
1984
|
server.middlewares.use("/__assets", async (req, res, next) => {
|
|
@@ -1734,12 +1990,12 @@ function filesPlugin(opts) {
|
|
|
1734
1990
|
if (listMatch && method === "GET") {
|
|
1735
1991
|
const slideId = listMatch[1];
|
|
1736
1992
|
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
1737
|
-
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
1993
|
+
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
1738
1994
|
let entries;
|
|
1739
1995
|
try {
|
|
1740
1996
|
entries = await fs.readdir(assetsDir);
|
|
1741
1997
|
} catch (err) {
|
|
1742
|
-
if (err.code === "ENOENT") return json(res, 200, { assets: [] });
|
|
1998
|
+
if (err.code === "ENOENT") return json$1(res, 200, { assets: [] });
|
|
1743
1999
|
throw err;
|
|
1744
2000
|
}
|
|
1745
2001
|
const assets = [];
|
|
@@ -1756,13 +2012,13 @@ function filesPlugin(opts) {
|
|
|
1756
2012
|
});
|
|
1757
2013
|
}
|
|
1758
2014
|
assets.sort((a, b) => a.name.localeCompare(b.name));
|
|
1759
|
-
return json(res, 200, { assets });
|
|
2015
|
+
return json$1(res, 200, { assets });
|
|
1760
2016
|
}
|
|
1761
2017
|
if (fileMatch) {
|
|
1762
2018
|
const slideId = fileMatch[1];
|
|
1763
2019
|
const filename = decodeURIComponent(fileMatch[2]);
|
|
1764
2020
|
const file = resolveAssetFile(slidesRoot, slideId, filename);
|
|
1765
|
-
if (!file) return json(res, 400, { error: "invalid path" });
|
|
2021
|
+
if (!file) return json$1(res, 400, { error: "invalid path" });
|
|
1766
2022
|
if (method === "GET") try {
|
|
1767
2023
|
const buf = await fs.readFile(file);
|
|
1768
2024
|
res.statusCode = 200;
|
|
@@ -1771,20 +2027,20 @@ function filesPlugin(opts) {
|
|
|
1771
2027
|
res.end(buf);
|
|
1772
2028
|
return;
|
|
1773
2029
|
} catch (err) {
|
|
1774
|
-
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2030
|
+
if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
|
|
1775
2031
|
throw err;
|
|
1776
2032
|
}
|
|
1777
2033
|
if (method === "POST") {
|
|
1778
2034
|
const overwrite = url.searchParams.get("overwrite") === "1";
|
|
1779
2035
|
const lenHeader = req.headers["content-length"];
|
|
1780
2036
|
const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
|
|
1781
|
-
if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
|
|
2037
|
+
if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json$1(res, 413, { error: "file too large" });
|
|
1782
2038
|
if (!overwrite) try {
|
|
1783
2039
|
await fs.access(file);
|
|
1784
|
-
return json(res, 409, { error: "asset exists" });
|
|
2040
|
+
return json$1(res, 409, { error: "asset exists" });
|
|
1785
2041
|
} catch {}
|
|
1786
2042
|
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
1787
|
-
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
2043
|
+
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
1788
2044
|
await fs.mkdir(assetsDir, { recursive: true });
|
|
1789
2045
|
const chunks = [];
|
|
1790
2046
|
let total = 0;
|
|
@@ -1802,9 +2058,9 @@ function filesPlugin(opts) {
|
|
|
1802
2058
|
req.on("end", () => resolve());
|
|
1803
2059
|
req.on("error", reject);
|
|
1804
2060
|
});
|
|
1805
|
-
if (oversized) return json(res, 413, { error: "file too large" });
|
|
2061
|
+
if (oversized) return json$1(res, 413, { error: "file too large" });
|
|
1806
2062
|
await fs.writeFile(file, Buffer.concat(chunks));
|
|
1807
|
-
return json(res, 200, {
|
|
2063
|
+
return json$1(res, 200, {
|
|
1808
2064
|
ok: true,
|
|
1809
2065
|
name: filename,
|
|
1810
2066
|
size: total,
|
|
@@ -1813,26 +2069,26 @@ function filesPlugin(opts) {
|
|
|
1813
2069
|
});
|
|
1814
2070
|
}
|
|
1815
2071
|
if (method === "PATCH") {
|
|
1816
|
-
const body = await readBody(req);
|
|
2072
|
+
const body = await readBody$1(req);
|
|
1817
2073
|
const target = validateAssetName(body.name);
|
|
1818
|
-
if (!target) return json(res, 400, { error: "invalid name" });
|
|
1819
|
-
if (target === filename) return json(res, 200, {
|
|
2074
|
+
if (!target) return json$1(res, 400, { error: "invalid name" });
|
|
2075
|
+
if (target === filename) return json$1(res, 200, {
|
|
1820
2076
|
ok: true,
|
|
1821
2077
|
name: filename
|
|
1822
2078
|
});
|
|
1823
2079
|
const dest = resolveAssetFile(slidesRoot, slideId, target);
|
|
1824
|
-
if (!dest) return json(res, 400, { error: "invalid name" });
|
|
2080
|
+
if (!dest) return json$1(res, 400, { error: "invalid name" });
|
|
1825
2081
|
try {
|
|
1826
2082
|
await fs.access(dest);
|
|
1827
|
-
return json(res, 409, { error: "target exists" });
|
|
2083
|
+
return json$1(res, 409, { error: "target exists" });
|
|
1828
2084
|
} catch {}
|
|
1829
2085
|
try {
|
|
1830
2086
|
await fs.rename(file, dest);
|
|
1831
2087
|
} catch (err) {
|
|
1832
|
-
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2088
|
+
if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
|
|
1833
2089
|
throw err;
|
|
1834
2090
|
}
|
|
1835
|
-
return json(res, 200, {
|
|
2091
|
+
return json$1(res, 200, {
|
|
1836
2092
|
ok: true,
|
|
1837
2093
|
name: target
|
|
1838
2094
|
});
|
|
@@ -1841,15 +2097,15 @@ function filesPlugin(opts) {
|
|
|
1841
2097
|
try {
|
|
1842
2098
|
await fs.unlink(file);
|
|
1843
2099
|
} catch (err) {
|
|
1844
|
-
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2100
|
+
if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
|
|
1845
2101
|
throw err;
|
|
1846
2102
|
}
|
|
1847
|
-
return json(res, 200, { ok: true });
|
|
2103
|
+
return json$1(res, 200, { ok: true });
|
|
1848
2104
|
}
|
|
1849
2105
|
}
|
|
1850
2106
|
return next();
|
|
1851
2107
|
} catch (err) {
|
|
1852
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
2108
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1853
2109
|
}
|
|
1854
2110
|
});
|
|
1855
2111
|
server.middlewares.use("/__svgl", async (req, res, next) => {
|
|
@@ -1868,16 +2124,16 @@ function filesPlugin(opts) {
|
|
|
1868
2124
|
target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
|
|
1869
2125
|
} else if (reqUrl.pathname === "/svg") {
|
|
1870
2126
|
const u = reqUrl.searchParams.get("u");
|
|
1871
|
-
if (!u) return json(res, 400, { error: "missing u" });
|
|
2127
|
+
if (!u) return json$1(res, 400, { error: "missing u" });
|
|
1872
2128
|
let parsed;
|
|
1873
2129
|
try {
|
|
1874
2130
|
parsed = new URL(u);
|
|
1875
2131
|
} catch {
|
|
1876
|
-
return json(res, 400, { error: "invalid u" });
|
|
2132
|
+
return json$1(res, 400, { error: "invalid u" });
|
|
1877
2133
|
}
|
|
1878
|
-
if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
|
|
2134
|
+
if (parsed.protocol !== "https:") return json$1(res, 400, { error: "https only" });
|
|
1879
2135
|
const host = parsed.hostname.toLowerCase();
|
|
1880
|
-
if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
|
|
2136
|
+
if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json$1(res, 400, { error: "host not allowed" });
|
|
1881
2137
|
target = parsed.toString();
|
|
1882
2138
|
} else return next();
|
|
1883
2139
|
const upstream = await fetch(target);
|
|
@@ -1888,7 +2144,7 @@ function filesPlugin(opts) {
|
|
|
1888
2144
|
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
1889
2145
|
res.end(buf);
|
|
1890
2146
|
} catch (err) {
|
|
1891
|
-
json(res, 502, { error: String(err.message ?? err) });
|
|
2147
|
+
json$1(res, 502, { error: String(err.message ?? err) });
|
|
1892
2148
|
}
|
|
1893
2149
|
});
|
|
1894
2150
|
server.middlewares.use("/__folders", async (req, res, next) => {
|
|
@@ -1897,14 +2153,14 @@ function filesPlugin(opts) {
|
|
|
1897
2153
|
try {
|
|
1898
2154
|
if (method === "GET" && url.pathname === "/") {
|
|
1899
2155
|
const manifest = await readManifest(manifestPath);
|
|
1900
|
-
return json(res, 200, manifest);
|
|
2156
|
+
return json$1(res, 200, manifest);
|
|
1901
2157
|
}
|
|
1902
2158
|
if (method === "POST" && url.pathname === "/") {
|
|
1903
|
-
const body = await readBody(req);
|
|
2159
|
+
const body = await readBody$1(req);
|
|
1904
2160
|
const name = validateName(body.name);
|
|
1905
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2161
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1906
2162
|
const icon = validateIcon(body.icon);
|
|
1907
|
-
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2163
|
+
if (!icon) return json$1(res, 400, { error: "invalid icon" });
|
|
1908
2164
|
const manifest = await readManifest(manifestPath);
|
|
1909
2165
|
const folder = {
|
|
1910
2166
|
id: newFolderId(),
|
|
@@ -1913,58 +2169,58 @@ function filesPlugin(opts) {
|
|
|
1913
2169
|
};
|
|
1914
2170
|
manifest.folders.push(folder);
|
|
1915
2171
|
await writeManifest(manifestPath, manifest);
|
|
1916
|
-
return json(res, 200, folder);
|
|
2172
|
+
return json$1(res, 200, folder);
|
|
1917
2173
|
}
|
|
1918
2174
|
if (method === "PUT" && url.pathname === "/assign") {
|
|
1919
|
-
const body = await readBody(req);
|
|
1920
|
-
if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
|
|
2175
|
+
const body = await readBody$1(req);
|
|
2176
|
+
if (typeof body.slideId !== "string" || !SLIDE_ID_RE$1.test(body.slideId)) return json$1(res, 400, { error: "invalid slideId" });
|
|
1921
2177
|
const slideId = body.slideId;
|
|
1922
2178
|
let folderId;
|
|
1923
2179
|
if (body.folderId === null) folderId = null;
|
|
1924
2180
|
else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
|
|
1925
|
-
else return json(res, 400, { error: "invalid folderId" });
|
|
2181
|
+
else return json$1(res, 400, { error: "invalid folderId" });
|
|
1926
2182
|
const manifest = await readManifest(manifestPath);
|
|
1927
|
-
if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
|
|
2183
|
+
if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json$1(res, 404, { error: "folder not found" });
|
|
1928
2184
|
if (folderId === null) delete manifest.assignments[slideId];
|
|
1929
2185
|
else manifest.assignments[slideId] = folderId;
|
|
1930
2186
|
await writeManifest(manifestPath, manifest);
|
|
1931
|
-
return json(res, 200, { ok: true });
|
|
2187
|
+
return json$1(res, 200, { ok: true });
|
|
1932
2188
|
}
|
|
1933
2189
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
1934
2190
|
if (idMatch) {
|
|
1935
2191
|
const id = idMatch[1];
|
|
1936
|
-
if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
|
|
2192
|
+
if (!FOLDER_ID_RE.test(id)) return json$1(res, 400, { error: "invalid id" });
|
|
1937
2193
|
if (method === "PATCH") {
|
|
1938
|
-
const body = await readBody(req);
|
|
2194
|
+
const body = await readBody$1(req);
|
|
1939
2195
|
const manifest = await readManifest(manifestPath);
|
|
1940
2196
|
const folder = manifest.folders.find((f) => f.id === id);
|
|
1941
|
-
if (!folder) return json(res, 404, { error: "folder not found" });
|
|
2197
|
+
if (!folder) return json$1(res, 404, { error: "folder not found" });
|
|
1942
2198
|
if (body.name !== void 0) {
|
|
1943
2199
|
const name = validateName(body.name);
|
|
1944
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2200
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1945
2201
|
folder.name = name;
|
|
1946
2202
|
}
|
|
1947
2203
|
if (body.icon !== void 0) {
|
|
1948
2204
|
const icon = validateIcon(body.icon);
|
|
1949
|
-
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2205
|
+
if (!icon) return json$1(res, 400, { error: "invalid icon" });
|
|
1950
2206
|
folder.icon = icon;
|
|
1951
2207
|
}
|
|
1952
2208
|
await writeManifest(manifestPath, manifest);
|
|
1953
|
-
return json(res, 200, folder);
|
|
2209
|
+
return json$1(res, 200, folder);
|
|
1954
2210
|
}
|
|
1955
2211
|
if (method === "DELETE") {
|
|
1956
2212
|
const manifest = await readManifest(manifestPath);
|
|
1957
2213
|
const before = manifest.folders.length;
|
|
1958
2214
|
manifest.folders = manifest.folders.filter((f) => f.id !== id);
|
|
1959
|
-
if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
|
|
2215
|
+
if (manifest.folders.length === before) return json$1(res, 404, { error: "folder not found" });
|
|
1960
2216
|
for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
|
|
1961
2217
|
await writeManifest(manifestPath, manifest);
|
|
1962
|
-
return json(res, 200, { ok: true });
|
|
2218
|
+
return json$1(res, 200, { ok: true });
|
|
1963
2219
|
}
|
|
1964
2220
|
}
|
|
1965
2221
|
next();
|
|
1966
2222
|
} catch (err) {
|
|
1967
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
2223
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1968
2224
|
}
|
|
1969
2225
|
});
|
|
1970
2226
|
}
|
|
@@ -1975,11 +2231,11 @@ function filesPlugin(opts) {
|
|
|
1975
2231
|
//#region src/vite/loc-tags-plugin.ts
|
|
1976
2232
|
const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
|
|
1977
2233
|
function isTaggableJsxName(name) {
|
|
1978
|
-
if (!t.isJSXIdentifier(name)) return false;
|
|
2234
|
+
if (!t$1.isJSXIdentifier(name)) return false;
|
|
1979
2235
|
return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
|
|
1980
2236
|
}
|
|
1981
2237
|
function alreadyTagged(opening) {
|
|
1982
|
-
return opening.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-slide-loc");
|
|
2238
|
+
return opening.attributes.some((attr) => t$1.isJSXAttribute(attr) && t$1.isJSXIdentifier(attr.name) && attr.name.name === "data-slide-loc");
|
|
1983
2239
|
}
|
|
1984
2240
|
function injectLocTags(code) {
|
|
1985
2241
|
let ast;
|
|
@@ -1994,7 +2250,7 @@ function injectLocTags(code) {
|
|
|
1994
2250
|
}
|
|
1995
2251
|
const insertions = [];
|
|
1996
2252
|
walkJsx(ast, (node) => {
|
|
1997
|
-
if (!t.isJSXElement(node) || !node.loc) return;
|
|
2253
|
+
if (!t$1.isJSXElement(node) || !node.loc) return;
|
|
1998
2254
|
const opening = node.openingElement;
|
|
1999
2255
|
const name = opening.name;
|
|
2000
2256
|
if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
|
|
@@ -2029,6 +2285,204 @@ function locTagsPlugin(opts) {
|
|
|
2029
2285
|
};
|
|
2030
2286
|
}
|
|
2031
2287
|
|
|
2288
|
+
//#endregion
|
|
2289
|
+
//#region src/vite/notes-plugin.ts
|
|
2290
|
+
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
2291
|
+
async function readBody(req) {
|
|
2292
|
+
return await new Promise((resolve, reject) => {
|
|
2293
|
+
const chunks = [];
|
|
2294
|
+
req.on("data", (c) => chunks.push(c));
|
|
2295
|
+
req.on("end", () => {
|
|
2296
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2297
|
+
if (!raw) return resolve({});
|
|
2298
|
+
try {
|
|
2299
|
+
resolve(JSON.parse(raw));
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
reject(e);
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
req.on("error", reject);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
function json(res, status, body) {
|
|
2308
|
+
res.statusCode = status;
|
|
2309
|
+
res.setHeader("content-type", "application/json");
|
|
2310
|
+
res.end(JSON.stringify(body));
|
|
2311
|
+
}
|
|
2312
|
+
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
2313
|
+
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
2314
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
2315
|
+
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
2316
|
+
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
2317
|
+
return full;
|
|
2318
|
+
}
|
|
2319
|
+
function parseSource(source) {
|
|
2320
|
+
try {
|
|
2321
|
+
return parse(source, {
|
|
2322
|
+
sourceType: "module",
|
|
2323
|
+
plugins: ["typescript", "jsx"],
|
|
2324
|
+
errorRecovery: true
|
|
2325
|
+
});
|
|
2326
|
+
} catch {
|
|
2327
|
+
return null;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function findNotesExport(ast) {
|
|
2331
|
+
for (const stmt of ast.program.body) {
|
|
2332
|
+
if (!t.isExportNamedDeclaration(stmt)) continue;
|
|
2333
|
+
const decl = stmt.declaration;
|
|
2334
|
+
if (!decl || !t.isVariableDeclaration(decl)) continue;
|
|
2335
|
+
for (const d of decl.declarations) {
|
|
2336
|
+
if (!t.isVariableDeclarator(d)) continue;
|
|
2337
|
+
if (!t.isIdentifier(d.id) || d.id.name !== "notes") continue;
|
|
2338
|
+
if (!d.init) return { error: "`notes` export has no initializer" };
|
|
2339
|
+
if (!t.isArrayExpression(d.init)) return { error: "`notes` export is not an array literal" };
|
|
2340
|
+
const arr = d.init;
|
|
2341
|
+
if (typeof stmt.start !== "number" || typeof stmt.end !== "number") return { error: "`notes` export missing source range" };
|
|
2342
|
+
if (typeof arr.start !== "number" || typeof arr.end !== "number") return { error: "`notes` array missing source range" };
|
|
2343
|
+
return {
|
|
2344
|
+
declStart: stmt.start,
|
|
2345
|
+
declEnd: stmt.end,
|
|
2346
|
+
arrayStart: arr.start,
|
|
2347
|
+
arrayEnd: arr.end,
|
|
2348
|
+
elements: arr.elements
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return null;
|
|
2353
|
+
}
|
|
2354
|
+
function renderNoteLiteral(text) {
|
|
2355
|
+
if (text === "") return "undefined";
|
|
2356
|
+
const hasNewline = /\n/.test(text);
|
|
2357
|
+
if (hasNewline) {
|
|
2358
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
2359
|
+
return `\`${escaped}\``;
|
|
2360
|
+
}
|
|
2361
|
+
return JSON.stringify(text);
|
|
2362
|
+
}
|
|
2363
|
+
function findInsertionOffset(ast, source) {
|
|
2364
|
+
let lastImportEnd = -1;
|
|
2365
|
+
for (const stmt of ast.program.body) if (t.isImportDeclaration(stmt) && typeof stmt.end === "number") lastImportEnd = Math.max(lastImportEnd, stmt.end);
|
|
2366
|
+
if (lastImportEnd >= 0) return lastImportEnd;
|
|
2367
|
+
return source.length;
|
|
2368
|
+
}
|
|
2369
|
+
function applyNotesEdit(source, index, text) {
|
|
2370
|
+
if (!Number.isInteger(index) || index < 0) return {
|
|
2371
|
+
ok: false,
|
|
2372
|
+
status: 400,
|
|
2373
|
+
error: "invalid index"
|
|
2374
|
+
};
|
|
2375
|
+
const ast = parseSource(source);
|
|
2376
|
+
if (!ast) return {
|
|
2377
|
+
ok: false,
|
|
2378
|
+
status: 422,
|
|
2379
|
+
error: "could not parse source"
|
|
2380
|
+
};
|
|
2381
|
+
const found = findNotesExport(ast);
|
|
2382
|
+
if (found && "error" in found) return {
|
|
2383
|
+
ok: false,
|
|
2384
|
+
status: 422,
|
|
2385
|
+
error: found.error
|
|
2386
|
+
};
|
|
2387
|
+
const literal = renderNoteLiteral(text);
|
|
2388
|
+
if (!found) {
|
|
2389
|
+
if (text === "") return {
|
|
2390
|
+
ok: true,
|
|
2391
|
+
source
|
|
2392
|
+
};
|
|
2393
|
+
const padding = Array.from({ length: index }, () => "undefined");
|
|
2394
|
+
const items = [...padding, literal];
|
|
2395
|
+
const block = [
|
|
2396
|
+
"",
|
|
2397
|
+
"",
|
|
2398
|
+
"export const notes: (string | undefined)[] = [",
|
|
2399
|
+
...items.map((s) => ` ${s},`),
|
|
2400
|
+
"];",
|
|
2401
|
+
""
|
|
2402
|
+
].join("\n");
|
|
2403
|
+
const offset = findInsertionOffset(ast, source);
|
|
2404
|
+
const next$1 = source.slice(0, offset) + block + source.slice(offset);
|
|
2405
|
+
return {
|
|
2406
|
+
ok: true,
|
|
2407
|
+
source: next$1
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
const elementTexts = [];
|
|
2411
|
+
for (const el of found.elements) {
|
|
2412
|
+
if (el === null) {
|
|
2413
|
+
elementTexts.push("undefined");
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (typeof el.start !== "number" || typeof el.end !== "number") return {
|
|
2417
|
+
ok: false,
|
|
2418
|
+
status: 422,
|
|
2419
|
+
error: "`notes` element missing source range"
|
|
2420
|
+
};
|
|
2421
|
+
elementTexts.push(source.slice(el.start, el.end));
|
|
2422
|
+
}
|
|
2423
|
+
while (elementTexts.length <= index) elementTexts.push("undefined");
|
|
2424
|
+
elementTexts[index] = literal;
|
|
2425
|
+
while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
|
|
2426
|
+
const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
|
|
2427
|
+
const next = source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd);
|
|
2428
|
+
return {
|
|
2429
|
+
ok: true,
|
|
2430
|
+
source: next
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function notesPlugin(opts) {
|
|
2434
|
+
const userCwd = opts.userCwd;
|
|
2435
|
+
const slidesDir = opts.slidesDir ?? "slides";
|
|
2436
|
+
const recentWrites = new Map();
|
|
2437
|
+
const RECENT_WRITE_WINDOW_MS = 1500;
|
|
2438
|
+
return {
|
|
2439
|
+
name: "open-slide:notes",
|
|
2440
|
+
apply: "serve",
|
|
2441
|
+
handleHotUpdate(ctx) {
|
|
2442
|
+
const ts = recentWrites.get(ctx.file);
|
|
2443
|
+
if (ts != null && Date.now() - ts < RECENT_WRITE_WINDOW_MS) {
|
|
2444
|
+
recentWrites.delete(ctx.file);
|
|
2445
|
+
return [];
|
|
2446
|
+
}
|
|
2447
|
+
return void 0;
|
|
2448
|
+
},
|
|
2449
|
+
configureServer(server) {
|
|
2450
|
+
server.middlewares.use("/__notes", async (req, res, next) => {
|
|
2451
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
2452
|
+
const method = req.method ?? "GET";
|
|
2453
|
+
if (method !== "PUT" || url.pathname !== "/") return next();
|
|
2454
|
+
try {
|
|
2455
|
+
const body = await readBody(req);
|
|
2456
|
+
const slideId = body.slideId ?? "";
|
|
2457
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
2458
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2459
|
+
if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
|
|
2460
|
+
if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
|
|
2461
|
+
let source;
|
|
2462
|
+
try {
|
|
2463
|
+
source = await fs.readFile(file, "utf8");
|
|
2464
|
+
} catch {
|
|
2465
|
+
return json(res, 404, { error: "slide not found" });
|
|
2466
|
+
}
|
|
2467
|
+
const result = applyNotesEdit(source, body.index, body.text);
|
|
2468
|
+
if (!result.ok) return json(res, result.status, { error: result.error });
|
|
2469
|
+
const changed = result.source !== source;
|
|
2470
|
+
if (changed) {
|
|
2471
|
+
recentWrites.set(file, Date.now());
|
|
2472
|
+
await fs.writeFile(file, result.source, "utf8");
|
|
2473
|
+
}
|
|
2474
|
+
return json(res, 200, {
|
|
2475
|
+
ok: true,
|
|
2476
|
+
changed
|
|
2477
|
+
});
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2032
2486
|
//#endregion
|
|
2033
2487
|
//#region src/vite/open-slide-plugin.ts
|
|
2034
2488
|
const CONFIG_FILE = "open-slide.config.ts";
|
|
@@ -2051,7 +2505,7 @@ async function readFoldersManifest(file) {
|
|
|
2051
2505
|
throw err;
|
|
2052
2506
|
}
|
|
2053
2507
|
}
|
|
2054
|
-
function resolved(id) {
|
|
2508
|
+
function resolved$1(id) {
|
|
2055
2509
|
return `\0${id}`;
|
|
2056
2510
|
}
|
|
2057
2511
|
async function findSlides(userCwd, slidesDir) {
|
|
@@ -2068,19 +2522,59 @@ function toId(absFile, slidesRoot) {
|
|
|
2068
2522
|
const rel = path.relative(slidesRoot, absFile);
|
|
2069
2523
|
return rel.split(path.sep)[0];
|
|
2070
2524
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2525
|
+
const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
|
|
2526
|
+
function extractMetaTheme(src) {
|
|
2527
|
+
const metaStart = src.search(/export\s+const\s+meta\b/);
|
|
2528
|
+
if (metaStart === -1) return null;
|
|
2529
|
+
const eqIdx = src.indexOf("=", metaStart);
|
|
2530
|
+
if (eqIdx === -1) return null;
|
|
2531
|
+
const openBrace = src.indexOf("{", eqIdx);
|
|
2532
|
+
if (openBrace === -1) return null;
|
|
2533
|
+
let depth = 0;
|
|
2534
|
+
let closeBrace = -1;
|
|
2535
|
+
for (let i = openBrace; i < src.length; i++) {
|
|
2536
|
+
const ch = src[i];
|
|
2537
|
+
if (ch === "{") depth++;
|
|
2538
|
+
else if (ch === "}") {
|
|
2539
|
+
depth--;
|
|
2540
|
+
if (depth === 0) {
|
|
2541
|
+
closeBrace = i;
|
|
2542
|
+
break;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (closeBrace === -1) return null;
|
|
2547
|
+
const body = src.slice(openBrace + 1, closeBrace);
|
|
2548
|
+
const m = body.match(META_THEME_RE);
|
|
2549
|
+
return m ? m[1] : null;
|
|
2550
|
+
}
|
|
2551
|
+
async function readSlideTheme(abs) {
|
|
2552
|
+
try {
|
|
2553
|
+
const src = await fs.readFile(abs, "utf8");
|
|
2554
|
+
return extractMetaTheme(src);
|
|
2555
|
+
} catch {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
async function generateSlidesModule(files, slidesRoot, isDev) {
|
|
2560
|
+
const entries = await Promise.all(files.map(async (abs) => {
|
|
2073
2561
|
const id = toId(abs, slidesRoot);
|
|
2074
2562
|
const importPath = isDev ? `/@fs/${abs.replace(/^\/+/, "")}` : abs;
|
|
2563
|
+
const theme = await readSlideTheme(abs);
|
|
2075
2564
|
return {
|
|
2076
2565
|
id,
|
|
2077
|
-
importPath
|
|
2566
|
+
importPath,
|
|
2567
|
+
theme
|
|
2078
2568
|
};
|
|
2079
|
-
});
|
|
2569
|
+
}));
|
|
2080
2570
|
const ids = JSON.stringify(entries.map((e) => e.id).sort());
|
|
2571
|
+
const themesMap = {};
|
|
2572
|
+
for (const e of entries) if (e.theme) themesMap[e.id] = e.theme;
|
|
2573
|
+
const themesJson = JSON.stringify(themesMap);
|
|
2081
2574
|
const cases = entries.map((e) => ` case ${JSON.stringify(e.id)}: return import(${JSON.stringify(e.importPath)});`).join("\n");
|
|
2082
2575
|
return `// virtual:open-slide/slides — generated
|
|
2083
2576
|
export const slideIds = ${ids};
|
|
2577
|
+
export const slideThemes = ${themesJson};
|
|
2084
2578
|
|
|
2085
2579
|
export async function loadSlide(id) {
|
|
2086
2580
|
switch (id) {
|
|
@@ -2103,17 +2597,17 @@ function openSlidePlugin(opts) {
|
|
|
2103
2597
|
return { server: { fs: { allow: [userCwd] } } };
|
|
2104
2598
|
},
|
|
2105
2599
|
resolveId(id) {
|
|
2106
|
-
if (id === SLIDES_VMOD) return resolved(SLIDES_VMOD);
|
|
2107
|
-
if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
|
|
2108
|
-
if (id === FOLDERS_VMOD) return resolved(FOLDERS_VMOD);
|
|
2600
|
+
if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
|
|
2601
|
+
if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
|
|
2602
|
+
if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
|
|
2109
2603
|
return null;
|
|
2110
2604
|
},
|
|
2111
2605
|
async load(id) {
|
|
2112
|
-
if (id === resolved(SLIDES_VMOD)) {
|
|
2606
|
+
if (id === resolved$1(SLIDES_VMOD)) {
|
|
2113
2607
|
const files = await findSlides(userCwd, slidesDir);
|
|
2114
|
-
return generateSlidesModule(files, slidesRoot, isDev);
|
|
2608
|
+
return await generateSlidesModule(files, slidesRoot, isDev);
|
|
2115
2609
|
}
|
|
2116
|
-
if (id === resolved(CONFIG_VMOD)) {
|
|
2610
|
+
if (id === resolved$1(CONFIG_VMOD)) {
|
|
2117
2611
|
const userBuild = config.build ?? {};
|
|
2118
2612
|
const buildResolved = isDev ? {
|
|
2119
2613
|
showSlideBrowser: true,
|
|
@@ -2130,7 +2624,7 @@ function openSlidePlugin(opts) {
|
|
|
2130
2624
|
};
|
|
2131
2625
|
return `export default ${JSON.stringify(resolvedConfig)};\n`;
|
|
2132
2626
|
}
|
|
2133
|
-
if (id === resolved(FOLDERS_VMOD)) {
|
|
2627
|
+
if (id === resolved$1(FOLDERS_VMOD)) {
|
|
2134
2628
|
const manifest = await readFoldersManifest(foldersManifestPath);
|
|
2135
2629
|
return `export default ${JSON.stringify(manifest)};\n`;
|
|
2136
2630
|
}
|
|
@@ -2149,24 +2643,36 @@ function openSlidePlugin(opts) {
|
|
|
2149
2643
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
2150
2644
|
reloadTimer = setTimeout(() => {
|
|
2151
2645
|
reloadTimer = null;
|
|
2152
|
-
const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
|
|
2646
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
|
|
2153
2647
|
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2154
2648
|
server.ws.send({ type: "full-reload" });
|
|
2155
2649
|
}, 150);
|
|
2156
2650
|
};
|
|
2157
|
-
server.watcher.add(
|
|
2651
|
+
if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
|
|
2158
2652
|
server.watcher.on("add", (p) => {
|
|
2159
2653
|
if (isSlideEntry(p)) reload();
|
|
2160
2654
|
});
|
|
2161
2655
|
server.watcher.on("unlink", (p) => {
|
|
2162
2656
|
if (isSlideEntry(p)) reload();
|
|
2163
2657
|
});
|
|
2658
|
+
let slideThemeTimer = null;
|
|
2659
|
+
const invalidateSlidesVmod = () => {
|
|
2660
|
+
if (slideThemeTimer) clearTimeout(slideThemeTimer);
|
|
2661
|
+
slideThemeTimer = setTimeout(() => {
|
|
2662
|
+
slideThemeTimer = null;
|
|
2663
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
|
|
2664
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2665
|
+
}, 100);
|
|
2666
|
+
};
|
|
2667
|
+
server.watcher.on("change", (p) => {
|
|
2668
|
+
if (isSlideEntry(p)) invalidateSlidesVmod();
|
|
2669
|
+
});
|
|
2164
2670
|
let foldersTimer = null;
|
|
2165
2671
|
const invalidateFolders = () => {
|
|
2166
2672
|
if (foldersTimer) clearTimeout(foldersTimer);
|
|
2167
2673
|
foldersTimer = setTimeout(() => {
|
|
2168
2674
|
foldersTimer = null;
|
|
2169
|
-
const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
|
|
2675
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
|
|
2170
2676
|
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2171
2677
|
}, 100);
|
|
2172
2678
|
};
|
|
@@ -2193,6 +2699,144 @@ async function loadUserConfig(userCwd) {
|
|
|
2193
2699
|
return loaded?.config ?? {};
|
|
2194
2700
|
}
|
|
2195
2701
|
|
|
2702
|
+
//#endregion
|
|
2703
|
+
//#region src/vite/themes-plugin.ts
|
|
2704
|
+
const THEMES_VMOD = "virtual:open-slide/themes";
|
|
2705
|
+
function resolved(id) {
|
|
2706
|
+
return `\0${id}`;
|
|
2707
|
+
}
|
|
2708
|
+
const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
2709
|
+
function parseFrontmatter(raw, themeId) {
|
|
2710
|
+
const match = raw.match(FM_RE);
|
|
2711
|
+
const fmText = match ? match[1] : "";
|
|
2712
|
+
const body = match ? match[2] : raw;
|
|
2713
|
+
const data = {};
|
|
2714
|
+
for (const line of fmText.split(/\r?\n/)) {
|
|
2715
|
+
const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
2716
|
+
if (!m) continue;
|
|
2717
|
+
let value = m[2].trim();
|
|
2718
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2719
|
+
data[m[1]] = value;
|
|
2720
|
+
}
|
|
2721
|
+
return {
|
|
2722
|
+
fm: {
|
|
2723
|
+
name: data.name || themeId,
|
|
2724
|
+
description: data.description || ""
|
|
2725
|
+
},
|
|
2726
|
+
body: body.trim()
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
async function findThemes(userCwd, themesDir) {
|
|
2730
|
+
const abs = path.resolve(userCwd, themesDir);
|
|
2731
|
+
if (!existsSync(abs)) return [];
|
|
2732
|
+
const hits = await fg("*.md", {
|
|
2733
|
+
cwd: abs,
|
|
2734
|
+
absolute: true,
|
|
2735
|
+
onlyFiles: true
|
|
2736
|
+
});
|
|
2737
|
+
return hits.sort();
|
|
2738
|
+
}
|
|
2739
|
+
async function readTheme(mdAbs, themesRoot) {
|
|
2740
|
+
const id = path.basename(mdAbs, ".md");
|
|
2741
|
+
const raw = await fs.readFile(mdAbs, "utf8");
|
|
2742
|
+
const { fm, body } = parseFrontmatter(raw, id);
|
|
2743
|
+
const demoCandidates = [
|
|
2744
|
+
`${id}.demo.tsx`,
|
|
2745
|
+
`${id}.demo.jsx`,
|
|
2746
|
+
`${id}.demo.ts`,
|
|
2747
|
+
`${id}.demo.js`
|
|
2748
|
+
];
|
|
2749
|
+
let demoAbs = null;
|
|
2750
|
+
for (const cand of demoCandidates) {
|
|
2751
|
+
const p = path.join(themesRoot, cand);
|
|
2752
|
+
if (existsSync(p)) {
|
|
2753
|
+
demoAbs = p;
|
|
2754
|
+
break;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
return {
|
|
2758
|
+
id,
|
|
2759
|
+
frontmatter: fm,
|
|
2760
|
+
body,
|
|
2761
|
+
demoAbs
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function generateThemesModule(themes, isDev) {
|
|
2765
|
+
const meta = themes.map((t$3) => ({
|
|
2766
|
+
id: t$3.id,
|
|
2767
|
+
name: t$3.frontmatter.name,
|
|
2768
|
+
description: t$3.frontmatter.description,
|
|
2769
|
+
body: t$3.body,
|
|
2770
|
+
hasDemo: t$3.demoAbs !== null
|
|
2771
|
+
}));
|
|
2772
|
+
const cases = themes.flatMap((t$3) => {
|
|
2773
|
+
const abs = t$3.demoAbs;
|
|
2774
|
+
if (!abs) return [];
|
|
2775
|
+
const importPath = isDev ? `/@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
|
|
2776
|
+
return [` case ${JSON.stringify(t$3.id)}: return import(${JSON.stringify(importPath)});`];
|
|
2777
|
+
}).join("\n");
|
|
2778
|
+
return `// virtual:open-slide/themes — generated
|
|
2779
|
+
export const themes = ${JSON.stringify(meta)};
|
|
2780
|
+
|
|
2781
|
+
export async function loadThemeDemo(id) {
|
|
2782
|
+
switch (id) {
|
|
2783
|
+
${cases}
|
|
2784
|
+
default: throw new Error('Theme demo not found: ' + id);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
`;
|
|
2788
|
+
}
|
|
2789
|
+
function themesPlugin(opts) {
|
|
2790
|
+
const { userCwd, config } = opts;
|
|
2791
|
+
const themesDir = config.themesDir ?? "themes";
|
|
2792
|
+
const themesRoot = path.resolve(userCwd, themesDir);
|
|
2793
|
+
let isDev = false;
|
|
2794
|
+
return {
|
|
2795
|
+
name: "open-slide:themes",
|
|
2796
|
+
config(_c, env) {
|
|
2797
|
+
isDev = env.command === "serve";
|
|
2798
|
+
},
|
|
2799
|
+
resolveId(id) {
|
|
2800
|
+
if (id === THEMES_VMOD) return resolved(THEMES_VMOD);
|
|
2801
|
+
return null;
|
|
2802
|
+
},
|
|
2803
|
+
async load(id) {
|
|
2804
|
+
if (id !== resolved(THEMES_VMOD)) return null;
|
|
2805
|
+
const files = await findThemes(userCwd, themesDir);
|
|
2806
|
+
const themes = await Promise.all(files.map((f) => readTheme(f, themesRoot)));
|
|
2807
|
+
return generateThemesModule(themes, isDev);
|
|
2808
|
+
},
|
|
2809
|
+
configureServer(server) {
|
|
2810
|
+
const isThemeFile = (p) => {
|
|
2811
|
+
const rel = path.relative(themesRoot, p);
|
|
2812
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
2813
|
+
if (rel.includes(path.sep)) return false;
|
|
2814
|
+
return /\.(md|demo\.(tsx|jsx|ts|js))$/.test(rel);
|
|
2815
|
+
};
|
|
2816
|
+
let reloadTimer = null;
|
|
2817
|
+
const reload = () => {
|
|
2818
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
2819
|
+
reloadTimer = setTimeout(() => {
|
|
2820
|
+
reloadTimer = null;
|
|
2821
|
+
const mod = server.moduleGraph.getModuleById(resolved(THEMES_VMOD));
|
|
2822
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2823
|
+
server.ws.send({ type: "full-reload" });
|
|
2824
|
+
}, 150);
|
|
2825
|
+
};
|
|
2826
|
+
if (existsSync(themesRoot)) server.watcher.add(themesRoot);
|
|
2827
|
+
server.watcher.on("add", (p) => {
|
|
2828
|
+
if (isThemeFile(p)) reload();
|
|
2829
|
+
});
|
|
2830
|
+
server.watcher.on("unlink", (p) => {
|
|
2831
|
+
if (isThemeFile(p)) reload();
|
|
2832
|
+
});
|
|
2833
|
+
server.watcher.on("change", (p) => {
|
|
2834
|
+
if (isThemeFile(p)) reload();
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2196
2840
|
//#endregion
|
|
2197
2841
|
//#region src/vite/config.ts
|
|
2198
2842
|
function findPackageRoot(fromFile) {
|
|
@@ -2209,7 +2853,9 @@ async function createViteConfig(opts) {
|
|
|
2209
2853
|
const userCwd = path.resolve(opts.userCwd);
|
|
2210
2854
|
const config = opts.config ?? await loadUserConfig(userCwd);
|
|
2211
2855
|
const slidesDir = config.slidesDir ?? "slides";
|
|
2856
|
+
const themesDir = config.themesDir ?? "themes";
|
|
2212
2857
|
const slidesAbs = path.resolve(userCwd, slidesDir);
|
|
2858
|
+
const themesAbs = path.resolve(userCwd, themesDir);
|
|
2213
2859
|
return {
|
|
2214
2860
|
root: APP_ROOT,
|
|
2215
2861
|
configFile: false,
|
|
@@ -2225,14 +2871,26 @@ async function createViteConfig(opts) {
|
|
|
2225
2871
|
userCwd,
|
|
2226
2872
|
config
|
|
2227
2873
|
}),
|
|
2874
|
+
themesPlugin({
|
|
2875
|
+
userCwd,
|
|
2876
|
+
config
|
|
2877
|
+
}),
|
|
2228
2878
|
designPlugin({ userCwd }),
|
|
2229
2879
|
commentsPlugin({
|
|
2230
2880
|
userCwd,
|
|
2231
2881
|
slidesDir
|
|
2232
2882
|
}),
|
|
2883
|
+
notesPlugin({
|
|
2884
|
+
userCwd,
|
|
2885
|
+
slidesDir
|
|
2886
|
+
}),
|
|
2233
2887
|
filesPlugin({
|
|
2234
2888
|
userCwd,
|
|
2235
2889
|
slidesDir
|
|
2890
|
+
}),
|
|
2891
|
+
currentPlugin({
|
|
2892
|
+
userCwd,
|
|
2893
|
+
slidesDir
|
|
2236
2894
|
})
|
|
2237
2895
|
],
|
|
2238
2896
|
resolve: { alias: { "@": APP_ROOT } },
|
|
@@ -2266,7 +2924,8 @@ async function createViteConfig(opts) {
|
|
|
2266
2924
|
fs: { allow: [
|
|
2267
2925
|
APP_ROOT,
|
|
2268
2926
|
userCwd,
|
|
2269
|
-
slidesAbs
|
|
2927
|
+
slidesAbs,
|
|
2928
|
+
themesAbs
|
|
2270
2929
|
] }
|
|
2271
2930
|
},
|
|
2272
2931
|
build: {
|