@open-slide/core 1.0.6 → 1.2.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-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
- package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
- package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +136 -24
- package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
- package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +5 -1
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +48 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +17 -2
- package/src/app/components/inspector/inspector-panel.tsx +90 -26
- package/src/app/components/inspector/inspector-provider.tsx +136 -1
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +26 -8
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/present/use-idle.ts +6 -4
- package/src/app/components/style-panel/design-provider.tsx +13 -0
- package/src/app/components/style-panel/style-panel.tsx +23 -11
- package/src/app/components/thumbnail-rail.tsx +317 -55
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/routes/home.tsx +34 -12
- package/src/app/routes/presenter.tsx +27 -24
- package/src/app/routes/slide.tsx +238 -51
- package/src/locale/en.ts +35 -4
- package/src/locale/ja.ts +35 -4
- package/src/locale/types.ts +38 -4
- package/src/locale/zh-cn.ts +35 -4
- package/src/locale/zh-tw.ts +35 -4
|
@@ -7,6 +7,7 @@ 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";
|
|
@@ -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");
|
|
@@ -647,8 +648,11 @@ function planReplacePlaceholder(ast, element, assetPath) {
|
|
|
647
648
|
const { identifier, importSplice } = planAssetImport(ast, assetPath);
|
|
648
649
|
const styleParts = [];
|
|
649
650
|
if (width != null) styleParts.push(`width: ${width}`);
|
|
651
|
+
else if (height != null) styleParts.push(`width: '100%'`);
|
|
650
652
|
if (height != null) styleParts.push(`height: ${height}`);
|
|
653
|
+
else if (width != null) styleParts.push(`height: '100%'`);
|
|
651
654
|
styleParts.push(`objectFit: 'cover'`);
|
|
655
|
+
styleParts.push(`objectPosition: '50% 50%'`);
|
|
652
656
|
const replacement = `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`;
|
|
653
657
|
return {
|
|
654
658
|
importSplice,
|
|
@@ -660,7 +664,7 @@ function applyEdit(source, line, column, ops) {
|
|
|
660
664
|
ok: true,
|
|
661
665
|
source
|
|
662
666
|
};
|
|
663
|
-
const ast = parseSource$
|
|
667
|
+
const ast = parseSource$2(source);
|
|
664
668
|
if (!ast) return {
|
|
665
669
|
ok: false,
|
|
666
670
|
status: 422,
|
|
@@ -756,38 +760,38 @@ function commentsPlugin(opts) {
|
|
|
756
760
|
if (method !== "POST") return next();
|
|
757
761
|
try {
|
|
758
762
|
if (url.pathname === "/") {
|
|
759
|
-
const body = await readBody$
|
|
763
|
+
const body = await readBody$3(req);
|
|
760
764
|
const slideId = body.slideId ?? "";
|
|
761
|
-
const file = resolveSlidePath$
|
|
762
|
-
if (!file) return json$
|
|
763
|
-
if (!body.line || body.line < 1) return json$
|
|
764
|
-
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" });
|
|
765
769
|
let source;
|
|
766
770
|
try {
|
|
767
771
|
source = await fs.readFile(file, "utf8");
|
|
768
772
|
} catch {
|
|
769
|
-
return json$
|
|
773
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
770
774
|
}
|
|
771
775
|
const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
|
|
772
|
-
if (!result.ok) return json$
|
|
776
|
+
if (!result.ok) return json$3(res, result.status, { error: result.error });
|
|
773
777
|
const changed = result.source !== source;
|
|
774
778
|
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
775
|
-
return json$
|
|
779
|
+
return json$3(res, 200, {
|
|
776
780
|
ok: true,
|
|
777
781
|
changed
|
|
778
782
|
});
|
|
779
783
|
}
|
|
780
784
|
if (url.pathname === "/batch") {
|
|
781
|
-
const body = await readBody$
|
|
785
|
+
const body = await readBody$3(req);
|
|
782
786
|
const slideId = body.slideId ?? "";
|
|
783
|
-
const file = resolveSlidePath$
|
|
784
|
-
if (!file) return json$
|
|
785
|
-
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" });
|
|
786
790
|
let source;
|
|
787
791
|
try {
|
|
788
792
|
source = await fs.readFile(file, "utf8");
|
|
789
793
|
} catch {
|
|
790
|
-
return json$
|
|
794
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
791
795
|
}
|
|
792
796
|
const original = source;
|
|
793
797
|
const results = [];
|
|
@@ -810,7 +814,7 @@ function commentsPlugin(opts) {
|
|
|
810
814
|
}
|
|
811
815
|
const changed = source !== original;
|
|
812
816
|
if (changed) await fs.writeFile(file, source, "utf8");
|
|
813
|
-
return json$
|
|
817
|
+
return json$3(res, 200, {
|
|
814
818
|
ok: true,
|
|
815
819
|
changed,
|
|
816
820
|
results
|
|
@@ -818,7 +822,7 @@ function commentsPlugin(opts) {
|
|
|
818
822
|
}
|
|
819
823
|
return next();
|
|
820
824
|
} catch (err) {
|
|
821
|
-
json$
|
|
825
|
+
json$3(res, 500, { error: String(err.message ?? err) });
|
|
822
826
|
}
|
|
823
827
|
});
|
|
824
828
|
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
@@ -827,31 +831,31 @@ function commentsPlugin(opts) {
|
|
|
827
831
|
try {
|
|
828
832
|
if (method === "GET" && url.pathname === "/") {
|
|
829
833
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
830
|
-
const file = resolveSlidePath$
|
|
831
|
-
if (!file) return json$
|
|
834
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
835
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
832
836
|
let source;
|
|
833
837
|
try {
|
|
834
838
|
source = await fs.readFile(file, "utf8");
|
|
835
839
|
} catch {
|
|
836
|
-
return json$
|
|
840
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
837
841
|
}
|
|
838
|
-
return json$
|
|
842
|
+
return json$3(res, 200, { comments: parseMarkers(source) });
|
|
839
843
|
}
|
|
840
844
|
if (method === "POST" && url.pathname === "/add") {
|
|
841
|
-
const body = await readBody$
|
|
845
|
+
const body = await readBody$3(req);
|
|
842
846
|
const slideId = body.slideId ?? "";
|
|
843
|
-
const file = resolveSlidePath$
|
|
844
|
-
if (!file) return json$
|
|
845
|
-
if (!body.line || body.line < 1) return json$
|
|
846
|
-
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" });
|
|
847
851
|
let source;
|
|
848
852
|
try {
|
|
849
853
|
source = await fs.readFile(file, "utf8");
|
|
850
854
|
} catch {
|
|
851
|
-
return json$
|
|
855
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
852
856
|
}
|
|
853
857
|
const plan = findInsertion(source, body.line, body.column);
|
|
854
|
-
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.` });
|
|
855
859
|
const id = newId();
|
|
856
860
|
const ts = new Date().toISOString();
|
|
857
861
|
const payload = b64urlEncode(JSON.stringify({
|
|
@@ -862,35 +866,109 @@ function commentsPlugin(opts) {
|
|
|
862
866
|
const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
|
|
863
867
|
await fs.writeFile(file, next$1, "utf8");
|
|
864
868
|
const markerLine = offsetToLine(next$1, plan.offset + 1);
|
|
865
|
-
return json$
|
|
869
|
+
return json$3(res, 200, {
|
|
866
870
|
id,
|
|
867
871
|
line: markerLine
|
|
868
872
|
});
|
|
869
873
|
}
|
|
870
874
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
871
875
|
const id = url.pathname.slice(1);
|
|
872
|
-
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" });
|
|
873
877
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
874
|
-
const file = resolveSlidePath$
|
|
875
|
-
if (!file) return json$
|
|
878
|
+
const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
|
|
879
|
+
if (!file) return json$3(res, 400, { error: "invalid slideId" });
|
|
876
880
|
let source;
|
|
877
881
|
try {
|
|
878
882
|
source = await fs.readFile(file, "utf8");
|
|
879
883
|
} catch {
|
|
880
|
-
return json$
|
|
884
|
+
return json$3(res, 404, { error: "slide not found" });
|
|
881
885
|
}
|
|
882
886
|
const lines = source.split("\n");
|
|
883
887
|
const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
884
888
|
const hit = lines.findIndex((l) => idRe.test(l));
|
|
885
|
-
if (hit === -1) return json$
|
|
889
|
+
if (hit === -1) return json$3(res, 404, { error: "marker not found" });
|
|
886
890
|
lines.splice(hit, 1);
|
|
887
891
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
888
|
-
return json$
|
|
892
|
+
return json$3(res, 200, { ok: true });
|
|
889
893
|
}
|
|
890
894
|
next();
|
|
891
895
|
} catch (err) {
|
|
892
|
-
json$
|
|
896
|
+
json$3(res, 500, { error: String(err.message ?? err) });
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
};
|
|
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;
|
|
893
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 {}
|
|
894
972
|
});
|
|
895
973
|
}
|
|
896
974
|
};
|
|
@@ -898,8 +976,8 @@ function commentsPlugin(opts) {
|
|
|
898
976
|
|
|
899
977
|
//#endregion
|
|
900
978
|
//#region src/vite/design-plugin.ts
|
|
901
|
-
const SLIDE_ID_RE$
|
|
902
|
-
async function readBody$
|
|
979
|
+
const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
|
|
980
|
+
async function readBody$2(req) {
|
|
903
981
|
return await new Promise((resolve, reject) => {
|
|
904
982
|
const chunks = [];
|
|
905
983
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -915,19 +993,19 @@ async function readBody$1(req) {
|
|
|
915
993
|
req.on("error", reject);
|
|
916
994
|
});
|
|
917
995
|
}
|
|
918
|
-
function json$
|
|
996
|
+
function json$2(res, status, body) {
|
|
919
997
|
res.statusCode = status;
|
|
920
998
|
res.setHeader("content-type", "application/json");
|
|
921
999
|
res.end(JSON.stringify(body));
|
|
922
1000
|
}
|
|
923
|
-
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
924
|
-
if (!SLIDE_ID_RE$
|
|
1001
|
+
function resolveSlidePath$1(userCwd, slidesDir, slideId) {
|
|
1002
|
+
if (!SLIDE_ID_RE$2.test(slideId)) return null;
|
|
925
1003
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
926
1004
|
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
927
1005
|
if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
|
|
928
1006
|
return full;
|
|
929
1007
|
}
|
|
930
|
-
function parseSource(source) {
|
|
1008
|
+
function parseSource$1(source) {
|
|
931
1009
|
try {
|
|
932
1010
|
return parse(source, {
|
|
933
1011
|
sourceType: "module",
|
|
@@ -1065,7 +1143,7 @@ function serializeDesign(design) {
|
|
|
1065
1143
|
return serializeValue(design, 0);
|
|
1066
1144
|
}
|
|
1067
1145
|
function parseSlideDesign(source) {
|
|
1068
|
-
const ast = parseSource(source);
|
|
1146
|
+
const ast = parseSource$1(source);
|
|
1069
1147
|
if (!ast) return {
|
|
1070
1148
|
ok: false,
|
|
1071
1149
|
exists: true,
|
|
@@ -1207,7 +1285,7 @@ function applyDesignWrite(source, next) {
|
|
|
1207
1285
|
error: `serialize failed: ${err.message}`
|
|
1208
1286
|
};
|
|
1209
1287
|
}
|
|
1210
|
-
const ast = parseSource(source);
|
|
1288
|
+
const ast = parseSource$1(source);
|
|
1211
1289
|
if (!ast) return {
|
|
1212
1290
|
ok: false,
|
|
1213
1291
|
status: 422,
|
|
@@ -1223,7 +1301,7 @@ function applyDesignWrite(source, next) {
|
|
|
1223
1301
|
};
|
|
1224
1302
|
}
|
|
1225
1303
|
const withImport = ensureDesignSystemImport(source, ast);
|
|
1226
|
-
const ast2 = parseSource(withImport.source);
|
|
1304
|
+
const ast2 = parseSource$1(withImport.source);
|
|
1227
1305
|
if (!ast2) return {
|
|
1228
1306
|
ok: false,
|
|
1229
1307
|
status: 422,
|
|
@@ -1249,51 +1327,51 @@ function designPlugin(opts) {
|
|
|
1249
1327
|
const url = new URL(req.url ?? "/", "http://local");
|
|
1250
1328
|
const method = req.method ?? "GET";
|
|
1251
1329
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
1252
|
-
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
1253
|
-
if (!file) return json$
|
|
1330
|
+
const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
|
|
1331
|
+
if (!file) return json$2(res, 400, { error: "invalid slideId" });
|
|
1254
1332
|
try {
|
|
1255
1333
|
if (method === "GET" && url.pathname === "/") {
|
|
1256
1334
|
let source;
|
|
1257
1335
|
try {
|
|
1258
1336
|
source = await fs.readFile(file, "utf8");
|
|
1259
1337
|
} catch {
|
|
1260
|
-
return json$
|
|
1338
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1261
1339
|
}
|
|
1262
1340
|
const parsed = parseSlideDesign(source);
|
|
1263
|
-
if (parsed.ok) return json$
|
|
1341
|
+
if (parsed.ok) return json$2(res, 200, {
|
|
1264
1342
|
design: parsed.design,
|
|
1265
1343
|
exists: true,
|
|
1266
1344
|
warning: null
|
|
1267
1345
|
});
|
|
1268
|
-
if (parsed.exists === false) return json$
|
|
1346
|
+
if (parsed.exists === false) return json$2(res, 200, {
|
|
1269
1347
|
design: defaultDesign,
|
|
1270
1348
|
exists: false,
|
|
1271
1349
|
warning: null
|
|
1272
1350
|
});
|
|
1273
|
-
return json$
|
|
1351
|
+
return json$2(res, 200, {
|
|
1274
1352
|
design: defaultDesign,
|
|
1275
1353
|
exists: true,
|
|
1276
1354
|
warning: parsed.error
|
|
1277
1355
|
});
|
|
1278
1356
|
}
|
|
1279
1357
|
if (method === "PUT" && url.pathname === "/") {
|
|
1280
|
-
const body = await readBody$
|
|
1358
|
+
const body = await readBody$2(req);
|
|
1281
1359
|
const patch = body.patch;
|
|
1282
|
-
if (!patch || typeof patch !== "object") return json$
|
|
1360
|
+
if (!patch || typeof patch !== "object") return json$2(res, 400, { error: "missing patch object" });
|
|
1283
1361
|
let source;
|
|
1284
1362
|
try {
|
|
1285
1363
|
source = await fs.readFile(file, "utf8");
|
|
1286
1364
|
} catch {
|
|
1287
|
-
return json$
|
|
1365
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1288
1366
|
}
|
|
1289
1367
|
const parsed = parseSlideDesign(source);
|
|
1290
1368
|
const baseDesign = parsed.ok ? parsed.design : defaultDesign;
|
|
1291
|
-
if (!parsed.ok && parsed.exists) return json$
|
|
1369
|
+
if (!parsed.ok && parsed.exists) return json$2(res, 422, { error: parsed.error });
|
|
1292
1370
|
const merged = mergeDesign(baseDesign, patch);
|
|
1293
1371
|
const written = applyDesignWrite(source, merged);
|
|
1294
|
-
if (!written.ok) return json$
|
|
1372
|
+
if (!written.ok) return json$2(res, written.status, { error: written.error });
|
|
1295
1373
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1296
|
-
return json$
|
|
1374
|
+
return json$2(res, 200, {
|
|
1297
1375
|
ok: true,
|
|
1298
1376
|
design: merged,
|
|
1299
1377
|
created: written.created
|
|
@@ -1304,12 +1382,12 @@ function designPlugin(opts) {
|
|
|
1304
1382
|
try {
|
|
1305
1383
|
source = await fs.readFile(file, "utf8");
|
|
1306
1384
|
} catch {
|
|
1307
|
-
return json$
|
|
1385
|
+
return json$2(res, 404, { error: "slide not found" });
|
|
1308
1386
|
}
|
|
1309
1387
|
const written = applyDesignWrite(source, defaultDesign);
|
|
1310
|
-
if (!written.ok) return json$
|
|
1388
|
+
if (!written.ok) return json$2(res, written.status, { error: written.error });
|
|
1311
1389
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
1312
|
-
return json$
|
|
1390
|
+
return json$2(res, 200, {
|
|
1313
1391
|
ok: true,
|
|
1314
1392
|
design: defaultDesign,
|
|
1315
1393
|
created: written.created
|
|
@@ -1317,7 +1395,7 @@ function designPlugin(opts) {
|
|
|
1317
1395
|
}
|
|
1318
1396
|
return next();
|
|
1319
1397
|
} catch (err) {
|
|
1320
|
-
json$
|
|
1398
|
+
json$2(res, 500, { error: String(err.message ?? err) });
|
|
1321
1399
|
}
|
|
1322
1400
|
});
|
|
1323
1401
|
}
|
|
@@ -1327,7 +1405,7 @@ function designPlugin(opts) {
|
|
|
1327
1405
|
//#endregion
|
|
1328
1406
|
//#region src/vite/files-plugin.ts
|
|
1329
1407
|
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
1330
|
-
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
1408
|
+
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
1331
1409
|
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
1332
1410
|
const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
|
|
1333
1411
|
const ASSET_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -1368,7 +1446,7 @@ function validateAssetName(v) {
|
|
|
1368
1446
|
if (dot <= 0 || dot === trimmed.length - 1) return null;
|
|
1369
1447
|
return trimmed;
|
|
1370
1448
|
}
|
|
1371
|
-
async function readBody(req) {
|
|
1449
|
+
async function readBody$1(req) {
|
|
1372
1450
|
return await new Promise((resolve, reject) => {
|
|
1373
1451
|
const chunks = [];
|
|
1374
1452
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -1384,7 +1462,7 @@ async function readBody(req) {
|
|
|
1384
1462
|
req.on("error", reject);
|
|
1385
1463
|
});
|
|
1386
1464
|
}
|
|
1387
|
-
function json(res, status, body) {
|
|
1465
|
+
function json$1(res, status, body) {
|
|
1388
1466
|
res.statusCode = status;
|
|
1389
1467
|
res.setHeader("content-type", "application/json");
|
|
1390
1468
|
res.end(JSON.stringify(body));
|
|
@@ -1428,7 +1506,7 @@ function validateSlideName(v) {
|
|
|
1428
1506
|
return trimmed;
|
|
1429
1507
|
}
|
|
1430
1508
|
async function rmSlideDir(slidesRoot, slideId) {
|
|
1431
|
-
if (!SLIDE_ID_RE.test(slideId)) return false;
|
|
1509
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return false;
|
|
1432
1510
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1433
1511
|
if (!dir.startsWith(slidesRoot + path.sep)) return false;
|
|
1434
1512
|
try {
|
|
@@ -1442,7 +1520,7 @@ async function rmSlideDir(slidesRoot, slideId) {
|
|
|
1442
1520
|
}
|
|
1443
1521
|
}
|
|
1444
1522
|
function resolveAssetsDir(slidesRoot, slideId) {
|
|
1445
|
-
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1523
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
1446
1524
|
const slideDir = path.resolve(slidesRoot, slideId);
|
|
1447
1525
|
if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
|
|
1448
1526
|
const assetsDir = path.resolve(slideDir, "assets");
|
|
@@ -1458,7 +1536,7 @@ function resolveAssetFile(slidesRoot, slideId, filename) {
|
|
|
1458
1536
|
return file;
|
|
1459
1537
|
}
|
|
1460
1538
|
function resolveSlideEntry(slidesRoot, slideId) {
|
|
1461
|
-
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1539
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
1462
1540
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1463
1541
|
if (!dir.startsWith(slidesRoot + path.sep)) return null;
|
|
1464
1542
|
return path.join(dir, "index.tsx");
|
|
@@ -1521,6 +1599,236 @@ function updateMetaTitleInSource(source, title) {
|
|
|
1521
1599
|
const insertion = `export const meta: SlideMeta = { title: ${newLiteral} };\n\n`;
|
|
1522
1600
|
return source.slice(0, exportDefaultIdx) + insertion + source.slice(exportDefaultIdx);
|
|
1523
1601
|
}
|
|
1602
|
+
function findDefaultExportArray(source) {
|
|
1603
|
+
let ast;
|
|
1604
|
+
try {
|
|
1605
|
+
ast = parse(source, {
|
|
1606
|
+
sourceType: "module",
|
|
1607
|
+
plugins: ["typescript", "jsx"],
|
|
1608
|
+
errorRecovery: true
|
|
1609
|
+
});
|
|
1610
|
+
} catch {
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
const body = ast.program?.body ?? [];
|
|
1614
|
+
for (const node of body) {
|
|
1615
|
+
if (node.type !== "ExportDefaultDeclaration") continue;
|
|
1616
|
+
let inner = node.declaration;
|
|
1617
|
+
while (inner && (inner.type === "TSAsExpression" || inner.type === "TSSatisfiesExpression")) inner = inner.expression;
|
|
1618
|
+
if (!inner || inner.type !== "ArrayExpression") return null;
|
|
1619
|
+
const arrayStart = inner.start;
|
|
1620
|
+
const arrayEnd = inner.end;
|
|
1621
|
+
const rawElements = inner.elements ?? [];
|
|
1622
|
+
const elements = [];
|
|
1623
|
+
for (const el of rawElements) {
|
|
1624
|
+
if (!el || typeof el.start !== "number" || typeof el.end !== "number") return null;
|
|
1625
|
+
elements.push({
|
|
1626
|
+
start: el.start,
|
|
1627
|
+
end: el.end
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
elements,
|
|
1632
|
+
arrayStart,
|
|
1633
|
+
arrayEnd
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Rewrite `export default [...]` so its elements appear in the requested order.
|
|
1640
|
+
*
|
|
1641
|
+
* `order[i]` is the original index that should land at new position `i`. The
|
|
1642
|
+
* function preserves each element's exact source slice (including any inline
|
|
1643
|
+
* comments that hug an identifier) and keeps the inter-element separator slots
|
|
1644
|
+
* in their original positions, so a 3-page array `[A, B, C]` reordered to
|
|
1645
|
+
* `[2, 0, 1]` becomes `[C, A, B]` with the same indentation and trailing
|
|
1646
|
+
* commas the author wrote.
|
|
1647
|
+
*
|
|
1648
|
+
* Returns `null` when the file's default export isn't an array literal, or the
|
|
1649
|
+
* order is not a valid permutation of `[0, n-1]`.
|
|
1650
|
+
*/
|
|
1651
|
+
function reorderDefaultExportPagesInSource(source, order) {
|
|
1652
|
+
const found = findDefaultExportArray(source);
|
|
1653
|
+
if (!found) return null;
|
|
1654
|
+
const { elements, arrayStart, arrayEnd } = found;
|
|
1655
|
+
const n = elements.length;
|
|
1656
|
+
if (order.length !== n) return null;
|
|
1657
|
+
const seen = new Set();
|
|
1658
|
+
for (const idx of order) {
|
|
1659
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= n) return null;
|
|
1660
|
+
if (seen.has(idx)) return null;
|
|
1661
|
+
seen.add(idx);
|
|
1662
|
+
}
|
|
1663
|
+
if (n === 0) return source;
|
|
1664
|
+
let identity = true;
|
|
1665
|
+
for (let i = 0; i < n; i++) if (order[i] !== i) {
|
|
1666
|
+
identity = false;
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
if (identity) return source;
|
|
1670
|
+
const prefix = source.slice(arrayStart, elements[0].start);
|
|
1671
|
+
const suffix = source.slice(elements[n - 1].end, arrayEnd);
|
|
1672
|
+
const separators = [];
|
|
1673
|
+
for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
|
|
1674
|
+
const elementText = elements.map((el) => source.slice(el.start, el.end));
|
|
1675
|
+
let rebuilt = prefix + elementText[order[0]];
|
|
1676
|
+
for (let i = 1; i < n; i++) rebuilt += separators[i - 1] + elementText[order[i]];
|
|
1677
|
+
rebuilt += suffix;
|
|
1678
|
+
return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
|
|
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
|
+
}
|
|
1524
1832
|
function validateIcon(v) {
|
|
1525
1833
|
if (!v || typeof v !== "object") return null;
|
|
1526
1834
|
const icon = v;
|
|
@@ -1563,7 +1871,7 @@ function filesPlugin(opts) {
|
|
|
1563
1871
|
const parts = rel.split(path.sep);
|
|
1564
1872
|
if (parts.length < 3 || parts[1] !== "assets") return;
|
|
1565
1873
|
const slideId = parts[0];
|
|
1566
|
-
if (!SLIDE_ID_RE.test(slideId)) return;
|
|
1874
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return;
|
|
1567
1875
|
server.ws.send({
|
|
1568
1876
|
type: "custom",
|
|
1569
1877
|
event: "open-slide:assets-changed",
|
|
@@ -1577,27 +1885,84 @@ function filesPlugin(opts) {
|
|
|
1577
1885
|
const url = new URL(req.url ?? "/", "http://local");
|
|
1578
1886
|
const method = req.method ?? "GET";
|
|
1579
1887
|
try {
|
|
1888
|
+
const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
|
|
1889
|
+
if (reorderMatch && method === "PUT") {
|
|
1890
|
+
const slideId$1 = reorderMatch[1];
|
|
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" });
|
|
1894
|
+
const order = [];
|
|
1895
|
+
for (const v of body.order) {
|
|
1896
|
+
if (!Number.isInteger(v)) return json$1(res, 400, { error: "invalid order" });
|
|
1897
|
+
order.push(v);
|
|
1898
|
+
}
|
|
1899
|
+
const entry = resolveSlideEntry(slidesRoot, slideId$1);
|
|
1900
|
+
if (!entry) return json$1(res, 400, { error: "invalid slideId" });
|
|
1901
|
+
let source;
|
|
1902
|
+
try {
|
|
1903
|
+
source = await fs.readFile(entry, "utf8");
|
|
1904
|
+
} catch {
|
|
1905
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1906
|
+
}
|
|
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, {
|
|
1913
|
+
ok: true,
|
|
1914
|
+
slideId: slideId$1,
|
|
1915
|
+
order
|
|
1916
|
+
});
|
|
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
|
+
}
|
|
1580
1945
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
1581
1946
|
if (!idMatch) return next();
|
|
1582
1947
|
const slideId = idMatch[1];
|
|
1583
|
-
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" });
|
|
1584
1949
|
if (method === "PATCH") {
|
|
1585
|
-
const body = await readBody(req);
|
|
1950
|
+
const body = await readBody$1(req);
|
|
1586
1951
|
const name = validateSlideName(body.name);
|
|
1587
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
1952
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1588
1953
|
const entry = resolveSlideEntry(slidesRoot, slideId);
|
|
1589
|
-
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
1954
|
+
if (!entry) return json$1(res, 400, { error: "invalid slideId" });
|
|
1590
1955
|
let source;
|
|
1591
1956
|
try {
|
|
1592
1957
|
source = await fs.readFile(entry, "utf8");
|
|
1593
1958
|
} catch {
|
|
1594
|
-
return json(res, 404, { error: "slide not found" });
|
|
1959
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
1595
1960
|
}
|
|
1596
1961
|
const updated = updateMetaTitleInSource(source, name);
|
|
1597
|
-
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" });
|
|
1598
1963
|
if (updated !== source) await fs.writeFile(entry, updated, "utf8");
|
|
1599
1964
|
server.ws.send({ type: "full-reload" });
|
|
1600
|
-
return json(res, 200, {
|
|
1965
|
+
return json$1(res, 200, {
|
|
1601
1966
|
ok: true,
|
|
1602
1967
|
slideId,
|
|
1603
1968
|
name
|
|
@@ -1605,15 +1970,15 @@ function filesPlugin(opts) {
|
|
|
1605
1970
|
}
|
|
1606
1971
|
if (method === "DELETE") {
|
|
1607
1972
|
const removed = await rmSlideDir(slidesRoot, slideId);
|
|
1608
|
-
if (!removed) return json(res, 404, { error: "slide not found" });
|
|
1973
|
+
if (!removed) return json$1(res, 404, { error: "slide not found" });
|
|
1609
1974
|
const manifest = await readManifest(manifestPath);
|
|
1610
1975
|
delete manifest.assignments[slideId];
|
|
1611
1976
|
await writeManifest(manifestPath, manifest);
|
|
1612
|
-
return json(res, 200, { ok: true });
|
|
1977
|
+
return json$1(res, 200, { ok: true });
|
|
1613
1978
|
}
|
|
1614
1979
|
return next();
|
|
1615
1980
|
} catch (err) {
|
|
1616
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
1981
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1617
1982
|
}
|
|
1618
1983
|
});
|
|
1619
1984
|
server.middlewares.use("/__assets", async (req, res, next) => {
|
|
@@ -1625,12 +1990,12 @@ function filesPlugin(opts) {
|
|
|
1625
1990
|
if (listMatch && method === "GET") {
|
|
1626
1991
|
const slideId = listMatch[1];
|
|
1627
1992
|
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
1628
|
-
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
1993
|
+
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
1629
1994
|
let entries;
|
|
1630
1995
|
try {
|
|
1631
1996
|
entries = await fs.readdir(assetsDir);
|
|
1632
1997
|
} catch (err) {
|
|
1633
|
-
if (err.code === "ENOENT") return json(res, 200, { assets: [] });
|
|
1998
|
+
if (err.code === "ENOENT") return json$1(res, 200, { assets: [] });
|
|
1634
1999
|
throw err;
|
|
1635
2000
|
}
|
|
1636
2001
|
const assets = [];
|
|
@@ -1647,13 +2012,13 @@ function filesPlugin(opts) {
|
|
|
1647
2012
|
});
|
|
1648
2013
|
}
|
|
1649
2014
|
assets.sort((a, b) => a.name.localeCompare(b.name));
|
|
1650
|
-
return json(res, 200, { assets });
|
|
2015
|
+
return json$1(res, 200, { assets });
|
|
1651
2016
|
}
|
|
1652
2017
|
if (fileMatch) {
|
|
1653
2018
|
const slideId = fileMatch[1];
|
|
1654
2019
|
const filename = decodeURIComponent(fileMatch[2]);
|
|
1655
2020
|
const file = resolveAssetFile(slidesRoot, slideId, filename);
|
|
1656
|
-
if (!file) return json(res, 400, { error: "invalid path" });
|
|
2021
|
+
if (!file) return json$1(res, 400, { error: "invalid path" });
|
|
1657
2022
|
if (method === "GET") try {
|
|
1658
2023
|
const buf = await fs.readFile(file);
|
|
1659
2024
|
res.statusCode = 200;
|
|
@@ -1662,20 +2027,20 @@ function filesPlugin(opts) {
|
|
|
1662
2027
|
res.end(buf);
|
|
1663
2028
|
return;
|
|
1664
2029
|
} catch (err) {
|
|
1665
|
-
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" });
|
|
1666
2031
|
throw err;
|
|
1667
2032
|
}
|
|
1668
2033
|
if (method === "POST") {
|
|
1669
2034
|
const overwrite = url.searchParams.get("overwrite") === "1";
|
|
1670
2035
|
const lenHeader = req.headers["content-length"];
|
|
1671
2036
|
const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
|
|
1672
|
-
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" });
|
|
1673
2038
|
if (!overwrite) try {
|
|
1674
2039
|
await fs.access(file);
|
|
1675
|
-
return json(res, 409, { error: "asset exists" });
|
|
2040
|
+
return json$1(res, 409, { error: "asset exists" });
|
|
1676
2041
|
} catch {}
|
|
1677
2042
|
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
1678
|
-
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
2043
|
+
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
1679
2044
|
await fs.mkdir(assetsDir, { recursive: true });
|
|
1680
2045
|
const chunks = [];
|
|
1681
2046
|
let total = 0;
|
|
@@ -1693,9 +2058,9 @@ function filesPlugin(opts) {
|
|
|
1693
2058
|
req.on("end", () => resolve());
|
|
1694
2059
|
req.on("error", reject);
|
|
1695
2060
|
});
|
|
1696
|
-
if (oversized) return json(res, 413, { error: "file too large" });
|
|
2061
|
+
if (oversized) return json$1(res, 413, { error: "file too large" });
|
|
1697
2062
|
await fs.writeFile(file, Buffer.concat(chunks));
|
|
1698
|
-
return json(res, 200, {
|
|
2063
|
+
return json$1(res, 200, {
|
|
1699
2064
|
ok: true,
|
|
1700
2065
|
name: filename,
|
|
1701
2066
|
size: total,
|
|
@@ -1704,26 +2069,26 @@ function filesPlugin(opts) {
|
|
|
1704
2069
|
});
|
|
1705
2070
|
}
|
|
1706
2071
|
if (method === "PATCH") {
|
|
1707
|
-
const body = await readBody(req);
|
|
2072
|
+
const body = await readBody$1(req);
|
|
1708
2073
|
const target = validateAssetName(body.name);
|
|
1709
|
-
if (!target) return json(res, 400, { error: "invalid name" });
|
|
1710
|
-
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, {
|
|
1711
2076
|
ok: true,
|
|
1712
2077
|
name: filename
|
|
1713
2078
|
});
|
|
1714
2079
|
const dest = resolveAssetFile(slidesRoot, slideId, target);
|
|
1715
|
-
if (!dest) return json(res, 400, { error: "invalid name" });
|
|
2080
|
+
if (!dest) return json$1(res, 400, { error: "invalid name" });
|
|
1716
2081
|
try {
|
|
1717
2082
|
await fs.access(dest);
|
|
1718
|
-
return json(res, 409, { error: "target exists" });
|
|
2083
|
+
return json$1(res, 409, { error: "target exists" });
|
|
1719
2084
|
} catch {}
|
|
1720
2085
|
try {
|
|
1721
2086
|
await fs.rename(file, dest);
|
|
1722
2087
|
} catch (err) {
|
|
1723
|
-
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" });
|
|
1724
2089
|
throw err;
|
|
1725
2090
|
}
|
|
1726
|
-
return json(res, 200, {
|
|
2091
|
+
return json$1(res, 200, {
|
|
1727
2092
|
ok: true,
|
|
1728
2093
|
name: target
|
|
1729
2094
|
});
|
|
@@ -1732,15 +2097,15 @@ function filesPlugin(opts) {
|
|
|
1732
2097
|
try {
|
|
1733
2098
|
await fs.unlink(file);
|
|
1734
2099
|
} catch (err) {
|
|
1735
|
-
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" });
|
|
1736
2101
|
throw err;
|
|
1737
2102
|
}
|
|
1738
|
-
return json(res, 200, { ok: true });
|
|
2103
|
+
return json$1(res, 200, { ok: true });
|
|
1739
2104
|
}
|
|
1740
2105
|
}
|
|
1741
2106
|
return next();
|
|
1742
2107
|
} catch (err) {
|
|
1743
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
2108
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1744
2109
|
}
|
|
1745
2110
|
});
|
|
1746
2111
|
server.middlewares.use("/__svgl", async (req, res, next) => {
|
|
@@ -1759,16 +2124,16 @@ function filesPlugin(opts) {
|
|
|
1759
2124
|
target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
|
|
1760
2125
|
} else if (reqUrl.pathname === "/svg") {
|
|
1761
2126
|
const u = reqUrl.searchParams.get("u");
|
|
1762
|
-
if (!u) return json(res, 400, { error: "missing u" });
|
|
2127
|
+
if (!u) return json$1(res, 400, { error: "missing u" });
|
|
1763
2128
|
let parsed;
|
|
1764
2129
|
try {
|
|
1765
2130
|
parsed = new URL(u);
|
|
1766
2131
|
} catch {
|
|
1767
|
-
return json(res, 400, { error: "invalid u" });
|
|
2132
|
+
return json$1(res, 400, { error: "invalid u" });
|
|
1768
2133
|
}
|
|
1769
|
-
if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
|
|
2134
|
+
if (parsed.protocol !== "https:") return json$1(res, 400, { error: "https only" });
|
|
1770
2135
|
const host = parsed.hostname.toLowerCase();
|
|
1771
|
-
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" });
|
|
1772
2137
|
target = parsed.toString();
|
|
1773
2138
|
} else return next();
|
|
1774
2139
|
const upstream = await fetch(target);
|
|
@@ -1779,7 +2144,7 @@ function filesPlugin(opts) {
|
|
|
1779
2144
|
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
1780
2145
|
res.end(buf);
|
|
1781
2146
|
} catch (err) {
|
|
1782
|
-
json(res, 502, { error: String(err.message ?? err) });
|
|
2147
|
+
json$1(res, 502, { error: String(err.message ?? err) });
|
|
1783
2148
|
}
|
|
1784
2149
|
});
|
|
1785
2150
|
server.middlewares.use("/__folders", async (req, res, next) => {
|
|
@@ -1788,14 +2153,14 @@ function filesPlugin(opts) {
|
|
|
1788
2153
|
try {
|
|
1789
2154
|
if (method === "GET" && url.pathname === "/") {
|
|
1790
2155
|
const manifest = await readManifest(manifestPath);
|
|
1791
|
-
return json(res, 200, manifest);
|
|
2156
|
+
return json$1(res, 200, manifest);
|
|
1792
2157
|
}
|
|
1793
2158
|
if (method === "POST" && url.pathname === "/") {
|
|
1794
|
-
const body = await readBody(req);
|
|
2159
|
+
const body = await readBody$1(req);
|
|
1795
2160
|
const name = validateName(body.name);
|
|
1796
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2161
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1797
2162
|
const icon = validateIcon(body.icon);
|
|
1798
|
-
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2163
|
+
if (!icon) return json$1(res, 400, { error: "invalid icon" });
|
|
1799
2164
|
const manifest = await readManifest(manifestPath);
|
|
1800
2165
|
const folder = {
|
|
1801
2166
|
id: newFolderId(),
|
|
@@ -1804,58 +2169,58 @@ function filesPlugin(opts) {
|
|
|
1804
2169
|
};
|
|
1805
2170
|
manifest.folders.push(folder);
|
|
1806
2171
|
await writeManifest(manifestPath, manifest);
|
|
1807
|
-
return json(res, 200, folder);
|
|
2172
|
+
return json$1(res, 200, folder);
|
|
1808
2173
|
}
|
|
1809
2174
|
if (method === "PUT" && url.pathname === "/assign") {
|
|
1810
|
-
const body = await readBody(req);
|
|
1811
|
-
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" });
|
|
1812
2177
|
const slideId = body.slideId;
|
|
1813
2178
|
let folderId;
|
|
1814
2179
|
if (body.folderId === null) folderId = null;
|
|
1815
2180
|
else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
|
|
1816
|
-
else return json(res, 400, { error: "invalid folderId" });
|
|
2181
|
+
else return json$1(res, 400, { error: "invalid folderId" });
|
|
1817
2182
|
const manifest = await readManifest(manifestPath);
|
|
1818
|
-
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" });
|
|
1819
2184
|
if (folderId === null) delete manifest.assignments[slideId];
|
|
1820
2185
|
else manifest.assignments[slideId] = folderId;
|
|
1821
2186
|
await writeManifest(manifestPath, manifest);
|
|
1822
|
-
return json(res, 200, { ok: true });
|
|
2187
|
+
return json$1(res, 200, { ok: true });
|
|
1823
2188
|
}
|
|
1824
2189
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
1825
2190
|
if (idMatch) {
|
|
1826
2191
|
const id = idMatch[1];
|
|
1827
|
-
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" });
|
|
1828
2193
|
if (method === "PATCH") {
|
|
1829
|
-
const body = await readBody(req);
|
|
2194
|
+
const body = await readBody$1(req);
|
|
1830
2195
|
const manifest = await readManifest(manifestPath);
|
|
1831
2196
|
const folder = manifest.folders.find((f) => f.id === id);
|
|
1832
|
-
if (!folder) return json(res, 404, { error: "folder not found" });
|
|
2197
|
+
if (!folder) return json$1(res, 404, { error: "folder not found" });
|
|
1833
2198
|
if (body.name !== void 0) {
|
|
1834
2199
|
const name = validateName(body.name);
|
|
1835
|
-
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2200
|
+
if (!name) return json$1(res, 400, { error: "invalid name" });
|
|
1836
2201
|
folder.name = name;
|
|
1837
2202
|
}
|
|
1838
2203
|
if (body.icon !== void 0) {
|
|
1839
2204
|
const icon = validateIcon(body.icon);
|
|
1840
|
-
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2205
|
+
if (!icon) return json$1(res, 400, { error: "invalid icon" });
|
|
1841
2206
|
folder.icon = icon;
|
|
1842
2207
|
}
|
|
1843
2208
|
await writeManifest(manifestPath, manifest);
|
|
1844
|
-
return json(res, 200, folder);
|
|
2209
|
+
return json$1(res, 200, folder);
|
|
1845
2210
|
}
|
|
1846
2211
|
if (method === "DELETE") {
|
|
1847
2212
|
const manifest = await readManifest(manifestPath);
|
|
1848
2213
|
const before = manifest.folders.length;
|
|
1849
2214
|
manifest.folders = manifest.folders.filter((f) => f.id !== id);
|
|
1850
|
-
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" });
|
|
1851
2216
|
for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
|
|
1852
2217
|
await writeManifest(manifestPath, manifest);
|
|
1853
|
-
return json(res, 200, { ok: true });
|
|
2218
|
+
return json$1(res, 200, { ok: true });
|
|
1854
2219
|
}
|
|
1855
2220
|
}
|
|
1856
2221
|
next();
|
|
1857
2222
|
} catch (err) {
|
|
1858
|
-
json(res, 500, { error: String(err.message ?? err) });
|
|
2223
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
1859
2224
|
}
|
|
1860
2225
|
});
|
|
1861
2226
|
}
|
|
@@ -1866,11 +2231,11 @@ function filesPlugin(opts) {
|
|
|
1866
2231
|
//#region src/vite/loc-tags-plugin.ts
|
|
1867
2232
|
const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
|
|
1868
2233
|
function isTaggableJsxName(name) {
|
|
1869
|
-
if (!t.isJSXIdentifier(name)) return false;
|
|
2234
|
+
if (!t$1.isJSXIdentifier(name)) return false;
|
|
1870
2235
|
return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
|
|
1871
2236
|
}
|
|
1872
2237
|
function alreadyTagged(opening) {
|
|
1873
|
-
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");
|
|
1874
2239
|
}
|
|
1875
2240
|
function injectLocTags(code) {
|
|
1876
2241
|
let ast;
|
|
@@ -1885,7 +2250,7 @@ function injectLocTags(code) {
|
|
|
1885
2250
|
}
|
|
1886
2251
|
const insertions = [];
|
|
1887
2252
|
walkJsx(ast, (node) => {
|
|
1888
|
-
if (!t.isJSXElement(node) || !node.loc) return;
|
|
2253
|
+
if (!t$1.isJSXElement(node) || !node.loc) return;
|
|
1889
2254
|
const opening = node.openingElement;
|
|
1890
2255
|
const name = opening.name;
|
|
1891
2256
|
if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
|
|
@@ -1920,6 +2285,204 @@ function locTagsPlugin(opts) {
|
|
|
1920
2285
|
};
|
|
1921
2286
|
}
|
|
1922
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
|
+
|
|
1923
2486
|
//#endregion
|
|
1924
2487
|
//#region src/vite/open-slide-plugin.ts
|
|
1925
2488
|
const CONFIG_FILE = "open-slide.config.ts";
|
|
@@ -2121,9 +2684,17 @@ async function createViteConfig(opts) {
|
|
|
2121
2684
|
userCwd,
|
|
2122
2685
|
slidesDir
|
|
2123
2686
|
}),
|
|
2687
|
+
notesPlugin({
|
|
2688
|
+
userCwd,
|
|
2689
|
+
slidesDir
|
|
2690
|
+
}),
|
|
2124
2691
|
filesPlugin({
|
|
2125
2692
|
userCwd,
|
|
2126
2693
|
slidesDir
|
|
2694
|
+
}),
|
|
2695
|
+
currentPlugin({
|
|
2696
|
+
userCwd,
|
|
2697
|
+
slidesDir
|
|
2127
2698
|
})
|
|
2128
2699
|
],
|
|
2129
2700
|
resolve: { alias: { "@": APP_ROOT } },
|