@open-slide/core 1.2.0 → 1.4.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-6BeQ3cxb.js → build-1Rqivz0d.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-AxZ5OE1u.js → config-XZJnC_fu.js} +735 -64
- package/dist/{config-CtT8K4VF.d.ts → config-s0YUbmUe.d.ts} +3 -1
- package/dist/{dev-C9eLmUEq.js → dev-0W8gYiSa.js} +2 -2
- package/dist/en-7GU-DHbJ.js +361 -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 +136 -342
- package/dist/{preview-Cunm-f4i.js → preview-DT9hJvzM.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-CRHIeoNq.d.ts → types-QCpkHkiS.d.ts} +42 -2
- 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/slide-authoring/SKILL.md +21 -2
- package/src/app/app.tsx +13 -1
- package/src/app/components/asset-view.tsx +37 -22
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/inspect-overlay.tsx +49 -3
- package/src/app/components/inspector/inspector-panel.tsx +370 -30
- package/src/app/components/inspector/inspector-provider.tsx +390 -49
- package/src/app/components/player.tsx +25 -5
- package/src/app/components/present/control-bar.tsx +12 -0
- package/src/app/components/sidebar/folder-item.tsx +27 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +20 -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 +17 -5
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/export-pdf.ts +6 -0
- package/src/app/lib/inspector/use-editor.ts +9 -1
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +17 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +194 -0
- package/src/app/routes/home.tsx +89 -207
- package/src/app/routes/presenter.tsx +2 -20
- package/src/app/routes/slide.tsx +217 -54
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +49 -7
- package/src/locale/ja.ts +50 -7
- package/src/locale/types.ts +44 -2
- package/src/locale/zh-cn.ts +49 -8
- package/src/locale/zh-tw.ts +49 -8
- 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";
|
|
@@ -12,7 +12,7 @@ import * as t$1 from "@babel/types";
|
|
|
12
12
|
import * as t from "@babel/types";
|
|
13
13
|
import { isJSXElement, isJSXFragment } from "@babel/types";
|
|
14
14
|
import fg from "fast-glob";
|
|
15
|
-
import { loadConfigFromFile } from "vite";
|
|
15
|
+
import { loadConfigFromFile, normalizePath } from "vite";
|
|
16
16
|
|
|
17
17
|
//#region src/vite/babel-walk.ts
|
|
18
18
|
const SKIP_KEYS = new Set([
|
|
@@ -187,11 +187,12 @@ function offsetToLine(source, offset) {
|
|
|
187
187
|
}
|
|
188
188
|
function parseSource$2(source) {
|
|
189
189
|
try {
|
|
190
|
-
|
|
190
|
+
const ast = parse(source, {
|
|
191
191
|
sourceType: "module",
|
|
192
192
|
plugins: ["typescript", "jsx"],
|
|
193
193
|
errorRecovery: true
|
|
194
194
|
});
|
|
195
|
+
return ast.errors && ast.errors.length > 0 ? null : ast;
|
|
195
196
|
} catch {
|
|
196
197
|
return null;
|
|
197
198
|
}
|
|
@@ -202,6 +203,51 @@ function findInnermostJsxElement(ast, line, column) {
|
|
|
202
203
|
for (const n of findJsxAncestors(ast, line, column)) if (t$2.isJSXElement(n)) return n;
|
|
203
204
|
return null;
|
|
204
205
|
}
|
|
206
|
+
function findUniqueElementByText(ast, prevText) {
|
|
207
|
+
const hits = [];
|
|
208
|
+
walkJsx(ast, (n) => {
|
|
209
|
+
if (!t$2.isJSXElement(n)) return;
|
|
210
|
+
const parts = [];
|
|
211
|
+
collectTextRangeParts(n, parts);
|
|
212
|
+
if (textRangeContent(parts) !== prevText) return;
|
|
213
|
+
hits.push({
|
|
214
|
+
node: n,
|
|
215
|
+
size: (n.end ?? 0) - (n.start ?? 0)
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
if (hits.length === 0) return null;
|
|
219
|
+
hits.sort((a, b) => a.size - b.size);
|
|
220
|
+
const best = hits[0];
|
|
221
|
+
const bestStart = best.node.start ?? 0;
|
|
222
|
+
const bestEnd = best.node.end ?? 0;
|
|
223
|
+
const hasSiblingMatch = hits.slice(1).some(({ node }) => (node.start ?? 0) > bestStart || (node.end ?? 0) < bestEnd);
|
|
224
|
+
return hasSiblingMatch ? null : best.node;
|
|
225
|
+
}
|
|
226
|
+
function fallbackTextForOps(ops) {
|
|
227
|
+
for (const op of ops) if ((op.kind === "set-style" || op.kind === "set-text" || op.kind === "set-text-range-style") && op.prevText !== void 0) return op.prevText;
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
function hasOnlyTextOps(ops) {
|
|
231
|
+
return ops.length > 0 && ops.every((op) => op.kind === "set-text");
|
|
232
|
+
}
|
|
233
|
+
function elementTextMatches(element, prevText) {
|
|
234
|
+
const parts = [];
|
|
235
|
+
collectTextRangeParts(element, parts);
|
|
236
|
+
return textRangeContent(parts) === prevText;
|
|
237
|
+
}
|
|
238
|
+
function elementHasTextCandidate(ast, element, prevText) {
|
|
239
|
+
const norm = prevText.trim();
|
|
240
|
+
return collectElementTextCandidates(ast, element).some((candidate) => candidate.current === norm);
|
|
241
|
+
}
|
|
242
|
+
function findElementForEdit(ast, line, column, ops) {
|
|
243
|
+
const element = findInnermostJsxElement(ast, line, column);
|
|
244
|
+
const prevText = fallbackTextForOps(ops);
|
|
245
|
+
if (prevText === null) return element;
|
|
246
|
+
if (hasOnlyTextOps(ops) && element && (elementTextMatches(element, prevText) || elementHasTextCandidate(ast, element, prevText))) return element;
|
|
247
|
+
const textMatch = findUniqueElementByText(ast, prevText);
|
|
248
|
+
if (element && elementTextMatches(element, prevText)) return textMatch ?? element;
|
|
249
|
+
return textMatch ?? element;
|
|
250
|
+
}
|
|
205
251
|
function findJsxByStart(ast, line, column) {
|
|
206
252
|
let hit = null;
|
|
207
253
|
walkJsx(ast, (n) => {
|
|
@@ -227,27 +273,60 @@ function findJsxAttr(opening, name) {
|
|
|
227
273
|
function buildStyleSplice(source, element, ops) {
|
|
228
274
|
const opening = element.openingElement;
|
|
229
275
|
const existing = findJsxAttr(opening, "style");
|
|
230
|
-
const
|
|
276
|
+
const entries = [];
|
|
277
|
+
let hasRawEntry = false;
|
|
231
278
|
if (existing) {
|
|
232
279
|
const value = existing.value;
|
|
233
280
|
if (!value || !t$2.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
|
|
234
281
|
const expr = value.expression;
|
|
235
|
-
if (!t$2.isObjectExpression(expr))
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
282
|
+
if (!t$2.isObjectExpression(expr)) {
|
|
283
|
+
if (typeof expr.start !== "number" || typeof expr.end !== "number") return { error: "style value missing source range" };
|
|
284
|
+
entries.push({
|
|
285
|
+
kind: "raw",
|
|
286
|
+
text: `...(${source.slice(expr.start, expr.end)})`
|
|
287
|
+
});
|
|
288
|
+
hasRawEntry = true;
|
|
289
|
+
} else for (const prop of expr.properties) if (t$2.isObjectProperty(prop) && !prop.computed) {
|
|
239
290
|
let keyName = null;
|
|
240
291
|
if (t$2.isIdentifier(prop.key)) keyName = prop.key.name;
|
|
241
292
|
else if (t$2.isStringLiteral(prop.key)) keyName = prop.key.value;
|
|
242
293
|
if (!keyName) return { error: "style has unsupported key" };
|
|
243
294
|
const v = prop.value;
|
|
244
|
-
if (typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
|
|
245
|
-
|
|
295
|
+
if (typeof prop.key.start !== "number" || typeof prop.key.end !== "number" || typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
|
|
296
|
+
entries.push({
|
|
297
|
+
kind: "prop",
|
|
298
|
+
key: keyName,
|
|
299
|
+
keyText: source.slice(prop.key.start, prop.key.end),
|
|
300
|
+
valueText: source.slice(v.start, v.end)
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
if (typeof prop.start !== "number" || typeof prop.end !== "number") return { error: "style value missing source range" };
|
|
304
|
+
entries.push({
|
|
305
|
+
kind: "raw",
|
|
306
|
+
text: source.slice(prop.start, prop.end)
|
|
307
|
+
});
|
|
308
|
+
hasRawEntry = true;
|
|
246
309
|
}
|
|
247
310
|
}
|
|
248
|
-
for (const op of ops)
|
|
249
|
-
|
|
250
|
-
|
|
311
|
+
for (const op of ops) {
|
|
312
|
+
const matching = entries.filter((entry) => entry.kind === "prop" && entry.key === op.key);
|
|
313
|
+
if (op.value === null) {
|
|
314
|
+
for (const entry of matching) entries.splice(entries.indexOf(entry), 1);
|
|
315
|
+
if (hasRawEntry) entries.push({
|
|
316
|
+
kind: "prop",
|
|
317
|
+
key: op.key,
|
|
318
|
+
keyText: op.key,
|
|
319
|
+
valueText: "undefined"
|
|
320
|
+
});
|
|
321
|
+
} else if (matching.length > 0) matching[matching.length - 1].valueText = jsString$1(op.value);
|
|
322
|
+
else entries.push({
|
|
323
|
+
kind: "prop",
|
|
324
|
+
key: op.key,
|
|
325
|
+
keyText: op.key,
|
|
326
|
+
valueText: jsString$1(op.value)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (entries.length === 0) {
|
|
251
330
|
if (!existing) return null;
|
|
252
331
|
let from = existing.start ?? 0;
|
|
253
332
|
if (from > 0 && source[from - 1] === " ") from -= 1;
|
|
@@ -257,16 +336,29 @@ function buildStyleSplice(source, element, ops) {
|
|
|
257
336
|
text: ""
|
|
258
337
|
};
|
|
259
338
|
}
|
|
260
|
-
const propsText =
|
|
339
|
+
const propsText = entries.map((entry) => entry.kind === "prop" ? `${entry.keyText}: ${entry.valueText}` : entry.text).join(", ");
|
|
261
340
|
const newAttr = `style={{ ${propsText} }}`;
|
|
262
|
-
if (existing)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
341
|
+
if (existing) {
|
|
342
|
+
const lastAttr$1 = opening.attributes[opening.attributes.length - 1];
|
|
343
|
+
if (lastAttr$1 && lastAttr$1 !== existing && typeof lastAttr$1.end === "number") {
|
|
344
|
+
const attrsAfterStyle = source.slice(existing.end ?? 0, lastAttr$1.end).replace(/^[ \t]+/, "");
|
|
345
|
+
return {
|
|
346
|
+
from: existing.start ?? 0,
|
|
347
|
+
to: lastAttr$1.end,
|
|
348
|
+
text: `${attrsAfterStyle} ${newAttr}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
from: existing.start ?? 0,
|
|
353
|
+
to: existing.end ?? 0,
|
|
354
|
+
text: newAttr
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const lastAttr = opening.attributes[opening.attributes.length - 1];
|
|
358
|
+
const at = lastAttr?.end ?? opening.name.end ?? 0;
|
|
267
359
|
return {
|
|
268
|
-
from:
|
|
269
|
-
to:
|
|
360
|
+
from: at,
|
|
361
|
+
to: at,
|
|
270
362
|
text: ` ${newAttr}`
|
|
271
363
|
};
|
|
272
364
|
}
|
|
@@ -280,6 +372,10 @@ function meaningfulChildren(parent) {
|
|
|
280
372
|
return true;
|
|
281
373
|
});
|
|
282
374
|
}
|
|
375
|
+
function isOnlyMeaningfulChild(parent, child) {
|
|
376
|
+
const meaningful = meaningfulChildren(parent);
|
|
377
|
+
return meaningful.length === 1 && meaningful[0] === child;
|
|
378
|
+
}
|
|
283
379
|
function wrapSplice(parent, text) {
|
|
284
380
|
const first = parent.children[0];
|
|
285
381
|
const last = parent.children[parent.children.length - 1];
|
|
@@ -289,6 +385,60 @@ function wrapSplice(parent, text) {
|
|
|
289
385
|
text
|
|
290
386
|
};
|
|
291
387
|
}
|
|
388
|
+
function splitLinesWithOffsets(value) {
|
|
389
|
+
const lines = [];
|
|
390
|
+
let start = 0;
|
|
391
|
+
for (let i = 0; i < value.length; i++) {
|
|
392
|
+
const ch = value[i];
|
|
393
|
+
if (ch !== "\n" && ch !== "\r") continue;
|
|
394
|
+
lines.push({
|
|
395
|
+
text: value.slice(start, i),
|
|
396
|
+
start
|
|
397
|
+
});
|
|
398
|
+
if (ch === "\r" && value[i + 1] === "\n") i += 1;
|
|
399
|
+
start = i + 1;
|
|
400
|
+
}
|
|
401
|
+
lines.push({
|
|
402
|
+
text: value.slice(start),
|
|
403
|
+
start
|
|
404
|
+
});
|
|
405
|
+
return lines;
|
|
406
|
+
}
|
|
407
|
+
function cleanJsxTextWithOffsets(value) {
|
|
408
|
+
const lines = splitLinesWithOffsets(value);
|
|
409
|
+
let lastNonEmptyLine = 0;
|
|
410
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].text.trim()) lastNonEmptyLine = i;
|
|
411
|
+
let text = "";
|
|
412
|
+
const offsets = [];
|
|
413
|
+
for (let i = 0; i < lines.length; i++) {
|
|
414
|
+
const chars = Array.from(lines[i].text, (ch, j) => ({
|
|
415
|
+
ch: ch === " " ? " " : ch,
|
|
416
|
+
offset: lines[i].start + j
|
|
417
|
+
}));
|
|
418
|
+
let from = 0;
|
|
419
|
+
let to = chars.length;
|
|
420
|
+
if (i !== 0) while (from < to && chars[from].ch === " ") from += 1;
|
|
421
|
+
if (i !== lines.length - 1) while (to > from && chars[to - 1].ch === " ") to -= 1;
|
|
422
|
+
if (from >= to) continue;
|
|
423
|
+
for (const item of chars.slice(from, to)) {
|
|
424
|
+
text += item.ch;
|
|
425
|
+
offsets.push(item.offset);
|
|
426
|
+
}
|
|
427
|
+
if (i !== lastNonEmptyLine) {
|
|
428
|
+
text += " ";
|
|
429
|
+
offsets.push(null);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
text,
|
|
434
|
+
offsets
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function isJsxBrElement(node) {
|
|
438
|
+
if (!t$2.isJSXElement(node)) return false;
|
|
439
|
+
const name = node.openingElement.name;
|
|
440
|
+
return t$2.isJSXIdentifier(name) && name.name.toLowerCase() === "br";
|
|
441
|
+
}
|
|
292
442
|
function collectTextCandidates(element, out) {
|
|
293
443
|
const meaningful = meaningfulChildren(element);
|
|
294
444
|
const isSole = meaningful.length === 1;
|
|
@@ -318,6 +468,226 @@ function collectTextCandidates(element, out) {
|
|
|
318
468
|
}
|
|
319
469
|
} else if (t$2.isJSXElement(child) || t$2.isJSXFragment(child)) collectTextCandidates(child, out);
|
|
320
470
|
}
|
|
471
|
+
function collectTextRangeParts(element, out) {
|
|
472
|
+
const parts = [];
|
|
473
|
+
collectTextRangePartsRaw(element, parts);
|
|
474
|
+
out.push(...normalizeTextRangeParts(parts));
|
|
475
|
+
}
|
|
476
|
+
function collectTextRangePartsRaw(element, out) {
|
|
477
|
+
for (const child of element.children) if (t$2.isJSXText(child)) {
|
|
478
|
+
const { text: current, offsets } = cleanJsxTextWithOffsets(child.value);
|
|
479
|
+
if (current) out.push({
|
|
480
|
+
node: child,
|
|
481
|
+
parent: element,
|
|
482
|
+
current,
|
|
483
|
+
raw: child.value,
|
|
484
|
+
text: formatJsxText,
|
|
485
|
+
offsets
|
|
486
|
+
});
|
|
487
|
+
} else if (t$2.isJSXExpressionContainer(child)) {
|
|
488
|
+
const expression = child.expression;
|
|
489
|
+
if (t$2.isStringLiteral(expression) || t$2.isNumericLiteral(expression)) {
|
|
490
|
+
const raw = String(expression.value);
|
|
491
|
+
const current = raw;
|
|
492
|
+
if (current) out.push({
|
|
493
|
+
node: child,
|
|
494
|
+
parent: element,
|
|
495
|
+
current,
|
|
496
|
+
raw,
|
|
497
|
+
text: (value) => `{${jsString$1(value)}}`,
|
|
498
|
+
offsets: Array.from({ length: current.length }, (_, i) => i)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
} else if (isJsxBrElement(child)) out.push({
|
|
502
|
+
node: child,
|
|
503
|
+
current: "\n"
|
|
504
|
+
});
|
|
505
|
+
else if (t$2.isJSXElement(child) || t$2.isJSXFragment(child)) collectTextRangePartsRaw(child, out);
|
|
506
|
+
}
|
|
507
|
+
function normalizeTextRangeParts(parts) {
|
|
508
|
+
return parts.flatMap((part, index) => {
|
|
509
|
+
if (!("raw" in part)) return [part];
|
|
510
|
+
let start = 0;
|
|
511
|
+
let end = part.current.length;
|
|
512
|
+
if (parts[index - 1]?.current === "\n") while (start < end && /\s/.test(part.current[start] ?? "")) start++;
|
|
513
|
+
if (parts[index + 1]?.current === "\n") while (end > start && /\s/.test(part.current[end - 1] ?? "")) end--;
|
|
514
|
+
if (start === 0 && end === part.current.length) return [part];
|
|
515
|
+
if (start >= end) return [];
|
|
516
|
+
return [{
|
|
517
|
+
...part,
|
|
518
|
+
current: part.current.slice(start, end),
|
|
519
|
+
offsets: part.offsets.slice(start, end)
|
|
520
|
+
}];
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function resetValueForRangeStyle(key) {
|
|
524
|
+
if (key === "fontWeight") return "400";
|
|
525
|
+
if (key === "fontStyle") return "normal";
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function styleSpanForText(text, key, value) {
|
|
529
|
+
const styleValue = value ?? resetValueForRangeStyle(key);
|
|
530
|
+
if (styleValue === null) return formatJsxText(text);
|
|
531
|
+
return `<span style={{ ${key}: ${jsString$1(styleValue)} }}>${formatJsxText(text)}</span>`;
|
|
532
|
+
}
|
|
533
|
+
function textRangeContent(parts) {
|
|
534
|
+
return parts.map((part) => part.current).join("");
|
|
535
|
+
}
|
|
536
|
+
function compactText(value) {
|
|
537
|
+
return value.replace(/\s+/g, "");
|
|
538
|
+
}
|
|
539
|
+
function textMatchesExpected(current, expected) {
|
|
540
|
+
return current === expected || compactText(current) === compactText(expected);
|
|
541
|
+
}
|
|
542
|
+
function formatRichText(value, formatText = formatJsxText) {
|
|
543
|
+
return value.split("\n").map((part) => formatText(part)).join("<br />");
|
|
544
|
+
}
|
|
545
|
+
function formatOptionalText(value, formatText = formatJsxText) {
|
|
546
|
+
return value ? formatText(value) : "";
|
|
547
|
+
}
|
|
548
|
+
function textDiff(prevText, nextText) {
|
|
549
|
+
let start = 0;
|
|
550
|
+
while (start < prevText.length && start < nextText.length && prevText[start] === nextText[start]) start += 1;
|
|
551
|
+
let prevEnd = prevText.length;
|
|
552
|
+
let nextEnd = nextText.length;
|
|
553
|
+
while (prevEnd > start && nextEnd > start && prevText[prevEnd - 1] === nextText[nextEnd - 1]) {
|
|
554
|
+
prevEnd -= 1;
|
|
555
|
+
nextEnd -= 1;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
start,
|
|
559
|
+
end: prevEnd,
|
|
560
|
+
value: nextText.slice(start, nextEnd)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function textLeafSplice(part, value) {
|
|
564
|
+
const rawRange = textLeafRawRange(part, 0, part.current.length);
|
|
565
|
+
if (!rawRange) return spliceRange(part.node, part.text(value));
|
|
566
|
+
const { rawStart, rawEnd } = rawRange;
|
|
567
|
+
return {
|
|
568
|
+
from: part.node.start ?? 0,
|
|
569
|
+
to: part.node.end ?? 0,
|
|
570
|
+
text: `${part.raw.slice(0, rawStart)}${formatRichText(value, part.text)}${part.raw.slice(rawEnd)}`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function textLeafRawRange(part, start, end) {
|
|
574
|
+
if (start >= end) return null;
|
|
575
|
+
let first = null;
|
|
576
|
+
let last = null;
|
|
577
|
+
for (let i = start; i < end; i++) {
|
|
578
|
+
const offset = part.offsets[i];
|
|
579
|
+
if (offset === void 0) return null;
|
|
580
|
+
if (offset === null) continue;
|
|
581
|
+
first ??= offset;
|
|
582
|
+
last = offset;
|
|
583
|
+
}
|
|
584
|
+
if (first === null || last === null) return null;
|
|
585
|
+
return {
|
|
586
|
+
rawStart: first,
|
|
587
|
+
rawEnd: last + 1
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function buildTextRangeReplaceSplices(parts, start, end, value) {
|
|
591
|
+
const splices = [];
|
|
592
|
+
let offset = 0;
|
|
593
|
+
let inserted = false;
|
|
594
|
+
for (const part of parts) {
|
|
595
|
+
const partStart = offset;
|
|
596
|
+
const partEnd = partStart + part.current.length;
|
|
597
|
+
offset = partEnd;
|
|
598
|
+
const overlaps = start < partEnd && end > partStart;
|
|
599
|
+
const insertsHere = start === end && !inserted && start >= partStart && start <= partEnd;
|
|
600
|
+
if (!overlaps && !insertsHere) continue;
|
|
601
|
+
if ("raw" in part) {
|
|
602
|
+
const localStart = Math.max(start, partStart) - partStart;
|
|
603
|
+
const localEnd = overlaps ? Math.min(end, partEnd) - partStart : localStart;
|
|
604
|
+
const nextText = `${part.current.slice(0, localStart)}${inserted ? "" : value}${part.current.slice(localEnd)}`;
|
|
605
|
+
splices.push(textLeafSplice(part, nextText));
|
|
606
|
+
} else if (overlaps) splices.push(spliceRange(part.node, inserted ? "" : formatRichText(value)));
|
|
607
|
+
else if (insertsHere) {
|
|
608
|
+
const at = start === partStart ? part.node.start ?? 0 : part.node.end ?? 0;
|
|
609
|
+
splices.push({
|
|
610
|
+
from: at,
|
|
611
|
+
to: at,
|
|
612
|
+
text: formatRichText(value)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
inserted = true;
|
|
616
|
+
}
|
|
617
|
+
if (!inserted && start === end && start === offset) {
|
|
618
|
+
const last = parts[parts.length - 1];
|
|
619
|
+
if (!last) return { error: "element has no editable text" };
|
|
620
|
+
if ("raw" in last) splices.push(textLeafSplice(last, `${last.current}${value}`));
|
|
621
|
+
else splices.push({
|
|
622
|
+
from: last.node.end ?? 0,
|
|
623
|
+
to: last.node.end ?? 0,
|
|
624
|
+
text: formatRichText(value)
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return splices;
|
|
628
|
+
}
|
|
629
|
+
function buildTextContentSplices(element, value, prevText) {
|
|
630
|
+
const parts = [];
|
|
631
|
+
collectTextRangeParts(element, parts);
|
|
632
|
+
const current = textRangeContent(parts);
|
|
633
|
+
if (!textMatchesExpected(current, prevText)) return { error: "no text candidate matches the current value" };
|
|
634
|
+
const diff = textDiff(current, value);
|
|
635
|
+
if (diff.start === diff.end && diff.value === "") return [];
|
|
636
|
+
return buildTextRangeReplaceSplices(parts, diff.start, diff.end, diff.value);
|
|
637
|
+
}
|
|
638
|
+
function buildTextRangeStyleSplices(ast, source, element, start, end, op, prevText) {
|
|
639
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) return { error: "invalid text range" };
|
|
640
|
+
const parts = [];
|
|
641
|
+
collectTextRangeParts(element, parts);
|
|
642
|
+
const current = prevText ?? textRangeContent(parts);
|
|
643
|
+
if (!current) return { error: "element has no editable text" };
|
|
644
|
+
if (end > current.length) return { error: "text range is out of bounds" };
|
|
645
|
+
const renderedText = textRangeContent(parts);
|
|
646
|
+
if (prevText !== void 0 && renderedText !== prevText) {
|
|
647
|
+
if (elementTextCandidateMatches(ast, element, prevText)) {
|
|
648
|
+
const result = buildStyleSplice(source, element, [op]);
|
|
649
|
+
if (result && "error" in result) return result;
|
|
650
|
+
return result ? [result] : [];
|
|
651
|
+
}
|
|
652
|
+
return { error: "no text candidate matches the current value" };
|
|
653
|
+
}
|
|
654
|
+
const splices = [];
|
|
655
|
+
let leafStart = 0;
|
|
656
|
+
for (const leaf of parts) {
|
|
657
|
+
const leafEnd = leafStart + leaf.current.length;
|
|
658
|
+
if (!("raw" in leaf)) {
|
|
659
|
+
leafStart = leafEnd;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const selectedStart = Math.max(start, leafStart);
|
|
663
|
+
const selectedEnd = Math.min(end, leafEnd);
|
|
664
|
+
if (selectedStart >= selectedEnd) {
|
|
665
|
+
leafStart = leafEnd;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (selectedStart === leafStart && selectedEnd === leafEnd && t$2.isJSXElement(leaf.parent) && leaf.parent !== element && isOnlyMeaningfulChild(leaf.parent, leaf.node)) {
|
|
669
|
+
const result = buildStyleSplice(source, leaf.parent, [op]);
|
|
670
|
+
if (result && "error" in result) return result;
|
|
671
|
+
if (result) splices.push(result);
|
|
672
|
+
leafStart = leafEnd;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const localStart = selectedStart - leafStart;
|
|
676
|
+
const localEnd = selectedEnd - leafStart;
|
|
677
|
+
const rawRange = textLeafRawRange(leaf, localStart, localEnd);
|
|
678
|
+
if (!rawRange) return { error: "text range source mismatch" };
|
|
679
|
+
const raw = leaf.raw;
|
|
680
|
+
const { rawStart, rawEnd } = rawRange;
|
|
681
|
+
const before = raw.slice(0, rawStart);
|
|
682
|
+
const selected = leaf.current.slice(localStart, localEnd);
|
|
683
|
+
const after = raw.slice(rawEnd);
|
|
684
|
+
const beforeText = t$2.isJSXText(leaf.node) ? before : formatOptionalText(before, leaf.text);
|
|
685
|
+
const afterText = t$2.isJSXText(leaf.node) ? after : formatOptionalText(after, leaf.text);
|
|
686
|
+
splices.push(spliceRange(leaf.node, `${beforeText}${styleSpanForText(selected, op.key, op.value)}${afterText}`));
|
|
687
|
+
leafStart = leafEnd;
|
|
688
|
+
}
|
|
689
|
+
return splices.length > 0 ? splices : null;
|
|
690
|
+
}
|
|
321
691
|
function propPassthroughName(element) {
|
|
322
692
|
const meaningful = meaningfulChildren(element);
|
|
323
693
|
if (meaningful.length !== 1) return null;
|
|
@@ -517,7 +887,7 @@ function collectArrayMapCandidates(ast, element) {
|
|
|
517
887
|
}
|
|
518
888
|
return out;
|
|
519
889
|
}
|
|
520
|
-
function
|
|
890
|
+
function collectElementTextCandidates(ast, element) {
|
|
521
891
|
const candidates = [];
|
|
522
892
|
collectTextCandidates(element, candidates);
|
|
523
893
|
if (candidates.length === 0) {
|
|
@@ -527,6 +897,14 @@ function buildTextSplice(ast, element, value, prevText) {
|
|
|
527
897
|
else if (passthrough && enclosing && componentDestructuresProp(enclosing.fn, passthrough)) candidates.push(...collectPropCallSiteCandidates(ast, enclosing.name, passthrough));
|
|
528
898
|
}
|
|
529
899
|
if (candidates.length === 0) candidates.push(...collectArrayMapCandidates(ast, element));
|
|
900
|
+
return candidates;
|
|
901
|
+
}
|
|
902
|
+
function elementTextCandidateMatches(ast, element, prevText) {
|
|
903
|
+
const norm = prevText.trim();
|
|
904
|
+
return collectElementTextCandidates(ast, element).some((candidate) => candidate.current === norm);
|
|
905
|
+
}
|
|
906
|
+
function buildTextSplice(ast, element, value, prevText) {
|
|
907
|
+
const candidates = collectElementTextCandidates(ast, element);
|
|
530
908
|
if (candidates.length === 0) return { error: "element has no editable text" };
|
|
531
909
|
if (candidates.length === 1) return candidates[0].splice(value);
|
|
532
910
|
if (prevText === void 0) return { error: "element has multiple text candidates; missing prevText" };
|
|
@@ -603,7 +981,7 @@ function planAssetImport(ast, assetPath) {
|
|
|
603
981
|
}
|
|
604
982
|
function planAssetAttr(ast, element, attr, assetPath) {
|
|
605
983
|
if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
|
|
606
|
-
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
984
|
+
if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return { error: "asset path must start with ./assets/ or @assets/" };
|
|
607
985
|
const { identifier, importSplice } = planAssetImport(ast, assetPath);
|
|
608
986
|
const opening = element.openingElement;
|
|
609
987
|
const newAttr = `${attr}={${identifier}}`;
|
|
@@ -641,7 +1019,7 @@ function readJsxNumberAttr(opening, name) {
|
|
|
641
1019
|
function planReplacePlaceholder(ast, element, assetPath) {
|
|
642
1020
|
const opening = element.openingElement;
|
|
643
1021
|
if (!t$2.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
|
|
644
|
-
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
1022
|
+
if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return { error: "asset path must start with ./assets/ or @assets/" };
|
|
645
1023
|
const hint = readJsxStringAttr(opening, "hint") ?? "";
|
|
646
1024
|
const width = readJsxNumberAttr(opening, "width");
|
|
647
1025
|
const height = readJsxNumberAttr(opening, "height");
|
|
@@ -670,7 +1048,7 @@ function applyEdit(source, line, column, ops) {
|
|
|
670
1048
|
status: 422,
|
|
671
1049
|
error: "could not parse source"
|
|
672
1050
|
};
|
|
673
|
-
const element =
|
|
1051
|
+
const element = findElementForEdit(ast, line, column, ops);
|
|
674
1052
|
if (!element) return {
|
|
675
1053
|
ok: false,
|
|
676
1054
|
status: 422,
|
|
@@ -691,14 +1069,42 @@ function applyEdit(source, line, column, ops) {
|
|
|
691
1069
|
if (result) splices.push(result);
|
|
692
1070
|
}
|
|
693
1071
|
for (const op of ops) {
|
|
694
|
-
if (op.kind !== "set-text") continue;
|
|
695
|
-
const result =
|
|
696
|
-
|
|
1072
|
+
if (op.kind !== "set-text-range-style") continue;
|
|
1073
|
+
const result = buildTextRangeStyleSplices(ast, source, element, op.start, op.end, {
|
|
1074
|
+
key: op.key,
|
|
1075
|
+
value: op.value
|
|
1076
|
+
}, op.prevText);
|
|
1077
|
+
if (result && "error" in result) return {
|
|
697
1078
|
ok: false,
|
|
698
1079
|
status: 422,
|
|
699
1080
|
error: result.error
|
|
700
1081
|
};
|
|
701
|
-
splices.push(result);
|
|
1082
|
+
if (result) splices.push(...result);
|
|
1083
|
+
}
|
|
1084
|
+
for (const op of ops) {
|
|
1085
|
+
if (op.kind !== "set-text") continue;
|
|
1086
|
+
if (op.prevText !== void 0 && (op.value.includes("\n") || op.prevText.includes("\n"))) {
|
|
1087
|
+
const richResult = buildTextContentSplices(element, op.value, op.prevText);
|
|
1088
|
+
if (!("error" in richResult)) {
|
|
1089
|
+
splices.push(...richResult);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const result = buildTextSplice(ast, element, op.value, op.prevText);
|
|
1094
|
+
if ("error" in result) {
|
|
1095
|
+
if (op.prevText === void 0) return {
|
|
1096
|
+
ok: false,
|
|
1097
|
+
status: 422,
|
|
1098
|
+
error: result.error
|
|
1099
|
+
};
|
|
1100
|
+
const richResult = buildTextContentSplices(element, op.value, op.prevText);
|
|
1101
|
+
if ("error" in richResult) return {
|
|
1102
|
+
ok: false,
|
|
1103
|
+
status: 422,
|
|
1104
|
+
error: result.error
|
|
1105
|
+
};
|
|
1106
|
+
splices.push(...richResult);
|
|
1107
|
+
} else splices.push(result);
|
|
702
1108
|
}
|
|
703
1109
|
const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
|
|
704
1110
|
const placeholderOps = ops.flatMap((op) => op.kind === "replace-placeholder-with-image" ? [op] : []);
|
|
@@ -742,6 +1148,11 @@ function applyEdit(source, line, column, ops) {
|
|
|
742
1148
|
splices.sort((a, b) => b.from - a.from);
|
|
743
1149
|
let next = source;
|
|
744
1150
|
for (const sp of splices) next = next.slice(0, sp.from) + sp.text + next.slice(sp.to);
|
|
1151
|
+
if (!parseSource$2(next)) return {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
status: 422,
|
|
1154
|
+
error: "edit would produce invalid source"
|
|
1155
|
+
};
|
|
745
1156
|
return {
|
|
746
1157
|
ok: true,
|
|
747
1158
|
source: next
|
|
@@ -1406,6 +1817,7 @@ function designPlugin(opts) {
|
|
|
1406
1817
|
//#region src/vite/files-plugin.ts
|
|
1407
1818
|
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
1408
1819
|
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
1820
|
+
const GLOBAL_SCOPE = "@global";
|
|
1409
1821
|
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
1410
1822
|
const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
|
|
1411
1823
|
const ASSET_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -1535,6 +1947,19 @@ function resolveAssetFile(slidesRoot, slideId, filename) {
|
|
|
1535
1947
|
if (!file.startsWith(assetsDir + path.sep)) return null;
|
|
1536
1948
|
return file;
|
|
1537
1949
|
}
|
|
1950
|
+
function resolveScopedAssetsDir(slidesRoot, globalAssetsRoot, scope) {
|
|
1951
|
+
if (scope === GLOBAL_SCOPE) return globalAssetsRoot;
|
|
1952
|
+
return resolveAssetsDir(slidesRoot, scope);
|
|
1953
|
+
}
|
|
1954
|
+
function resolveScopedAssetFile(slidesRoot, globalAssetsRoot, scope, filename) {
|
|
1955
|
+
if (scope === GLOBAL_SCOPE) {
|
|
1956
|
+
if (!validateAssetName(filename)) return null;
|
|
1957
|
+
const file = path.resolve(globalAssetsRoot, filename);
|
|
1958
|
+
if (!file.startsWith(globalAssetsRoot + path.sep)) return null;
|
|
1959
|
+
return file;
|
|
1960
|
+
}
|
|
1961
|
+
return resolveAssetFile(slidesRoot, scope, filename);
|
|
1962
|
+
}
|
|
1538
1963
|
function resolveSlideEntry(slidesRoot, slideId) {
|
|
1539
1964
|
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
1540
1965
|
const dir = path.resolve(slidesRoot, slideId);
|
|
@@ -1852,7 +2277,9 @@ function validateIcon(v) {
|
|
|
1852
2277
|
function filesPlugin(opts) {
|
|
1853
2278
|
const userCwd = opts.userCwd;
|
|
1854
2279
|
const slidesDir = opts.slidesDir ?? "slides";
|
|
2280
|
+
const assetsDir = opts.assetsDir ?? "assets";
|
|
1855
2281
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
2282
|
+
const globalAssetsRoot = path.resolve(userCwd, assetsDir);
|
|
1856
2283
|
const manifestPath = path.join(slidesRoot, ".folders.json");
|
|
1857
2284
|
return {
|
|
1858
2285
|
name: "open-slide:files",
|
|
@@ -1865,7 +2292,16 @@ function filesPlugin(opts) {
|
|
|
1865
2292
|
event: "open-slide:files-changed"
|
|
1866
2293
|
});
|
|
1867
2294
|
});
|
|
2295
|
+
server.watcher.add(globalAssetsRoot);
|
|
1868
2296
|
const onAssetChange = (p) => {
|
|
2297
|
+
if (p.startsWith(globalAssetsRoot + path.sep) || p === globalAssetsRoot) {
|
|
2298
|
+
server.ws.send({
|
|
2299
|
+
type: "custom",
|
|
2300
|
+
event: "open-slide:assets-changed",
|
|
2301
|
+
data: { slideId: GLOBAL_SCOPE }
|
|
2302
|
+
});
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
1869
2305
|
if (!p.startsWith(slidesRoot + path.sep)) return;
|
|
1870
2306
|
const rel = p.slice(slidesRoot.length + 1);
|
|
1871
2307
|
const parts = rel.split(path.sep);
|
|
@@ -1989,11 +2425,11 @@ function filesPlugin(opts) {
|
|
|
1989
2425
|
const fileMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)$/);
|
|
1990
2426
|
if (listMatch && method === "GET") {
|
|
1991
2427
|
const slideId = listMatch[1];
|
|
1992
|
-
const assetsDir =
|
|
1993
|
-
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
2428
|
+
const assetsDir$1 = resolveScopedAssetsDir(slidesRoot, globalAssetsRoot, slideId);
|
|
2429
|
+
if (!assetsDir$1) return json$1(res, 400, { error: "invalid slideId" });
|
|
1994
2430
|
let entries;
|
|
1995
2431
|
try {
|
|
1996
|
-
entries = await fs.readdir(assetsDir);
|
|
2432
|
+
entries = await fs.readdir(assetsDir$1);
|
|
1997
2433
|
} catch (err) {
|
|
1998
2434
|
if (err.code === "ENOENT") return json$1(res, 200, { assets: [] });
|
|
1999
2435
|
throw err;
|
|
@@ -2001,7 +2437,7 @@ function filesPlugin(opts) {
|
|
|
2001
2437
|
const assets = [];
|
|
2002
2438
|
for (const name of entries) {
|
|
2003
2439
|
if (!validateAssetName(name)) continue;
|
|
2004
|
-
const stat = await fs.stat(path.join(assetsDir, name));
|
|
2440
|
+
const stat = await fs.stat(path.join(assetsDir$1, name));
|
|
2005
2441
|
if (!stat.isFile()) continue;
|
|
2006
2442
|
assets.push({
|
|
2007
2443
|
name,
|
|
@@ -2017,7 +2453,7 @@ function filesPlugin(opts) {
|
|
|
2017
2453
|
if (fileMatch) {
|
|
2018
2454
|
const slideId = fileMatch[1];
|
|
2019
2455
|
const filename = decodeURIComponent(fileMatch[2]);
|
|
2020
|
-
const file =
|
|
2456
|
+
const file = resolveScopedAssetFile(slidesRoot, globalAssetsRoot, slideId, filename);
|
|
2021
2457
|
if (!file) return json$1(res, 400, { error: "invalid path" });
|
|
2022
2458
|
if (method === "GET") try {
|
|
2023
2459
|
const buf = await fs.readFile(file);
|
|
@@ -2039,9 +2475,9 @@ function filesPlugin(opts) {
|
|
|
2039
2475
|
await fs.access(file);
|
|
2040
2476
|
return json$1(res, 409, { error: "asset exists" });
|
|
2041
2477
|
} catch {}
|
|
2042
|
-
const assetsDir =
|
|
2043
|
-
if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
|
|
2044
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
2478
|
+
const assetsDir$1 = resolveScopedAssetsDir(slidesRoot, globalAssetsRoot, slideId);
|
|
2479
|
+
if (!assetsDir$1) return json$1(res, 400, { error: "invalid slideId" });
|
|
2480
|
+
await fs.mkdir(assetsDir$1, { recursive: true });
|
|
2045
2481
|
const chunks = [];
|
|
2046
2482
|
let total = 0;
|
|
2047
2483
|
let oversized = false;
|
|
@@ -2076,7 +2512,7 @@ function filesPlugin(opts) {
|
|
|
2076
2512
|
ok: true,
|
|
2077
2513
|
name: filename
|
|
2078
2514
|
});
|
|
2079
|
-
const dest =
|
|
2515
|
+
const dest = resolveScopedAssetFile(slidesRoot, globalAssetsRoot, slideId, target);
|
|
2080
2516
|
if (!dest) return json$1(res, 400, { error: "invalid name" });
|
|
2081
2517
|
try {
|
|
2082
2518
|
await fs.access(dest);
|
|
@@ -2505,7 +2941,7 @@ async function readFoldersManifest(file) {
|
|
|
2505
2941
|
throw err;
|
|
2506
2942
|
}
|
|
2507
2943
|
}
|
|
2508
|
-
function resolved(id) {
|
|
2944
|
+
function resolved$1(id) {
|
|
2509
2945
|
return `\0${id}`;
|
|
2510
2946
|
}
|
|
2511
2947
|
async function findSlides(userCwd, slidesDir) {
|
|
@@ -2522,19 +2958,76 @@ function toId(absFile, slidesRoot) {
|
|
|
2522
2958
|
const rel = path.relative(slidesRoot, absFile);
|
|
2523
2959
|
return rel.split(path.sep)[0];
|
|
2524
2960
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2961
|
+
const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
|
|
2962
|
+
function extractMetaTheme(src) {
|
|
2963
|
+
const metaStart = src.search(/export\s+const\s+meta\b/);
|
|
2964
|
+
if (metaStart === -1) return null;
|
|
2965
|
+
const eqIdx = src.indexOf("=", metaStart);
|
|
2966
|
+
if (eqIdx === -1) return null;
|
|
2967
|
+
const openBrace = src.indexOf("{", eqIdx);
|
|
2968
|
+
if (openBrace === -1) return null;
|
|
2969
|
+
let depth = 0;
|
|
2970
|
+
let closeBrace = -1;
|
|
2971
|
+
for (let i = openBrace; i < src.length; i++) {
|
|
2972
|
+
const ch = src[i];
|
|
2973
|
+
if (ch === "{") depth++;
|
|
2974
|
+
else if (ch === "}") {
|
|
2975
|
+
depth--;
|
|
2976
|
+
if (depth === 0) {
|
|
2977
|
+
closeBrace = i;
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
if (closeBrace === -1) return null;
|
|
2983
|
+
const body = src.slice(openBrace + 1, closeBrace);
|
|
2984
|
+
const m = body.match(META_THEME_RE);
|
|
2985
|
+
return m ? m[1] : null;
|
|
2986
|
+
}
|
|
2987
|
+
async function readSlideTheme(abs) {
|
|
2988
|
+
try {
|
|
2989
|
+
const src = await fs.readFile(abs, "utf8");
|
|
2990
|
+
return extractMetaTheme(src);
|
|
2991
|
+
} catch {
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
async function generateSlidesModule(files, slidesRoot, isDev) {
|
|
2996
|
+
const entries = await Promise.all(files.map(async (abs) => {
|
|
2527
2997
|
const id = toId(abs, slidesRoot);
|
|
2528
2998
|
const importPath = isDev ? `/@fs/${abs.replace(/^\/+/, "")}` : abs;
|
|
2999
|
+
const theme = await readSlideTheme(abs);
|
|
2529
3000
|
return {
|
|
2530
3001
|
id,
|
|
2531
|
-
importPath
|
|
3002
|
+
importPath,
|
|
3003
|
+
theme
|
|
2532
3004
|
};
|
|
2533
|
-
});
|
|
3005
|
+
}));
|
|
2534
3006
|
const ids = JSON.stringify(entries.map((e) => e.id).sort());
|
|
2535
|
-
const
|
|
3007
|
+
const themesMap = {};
|
|
3008
|
+
for (const e of entries) if (e.theme) themesMap[e.id] = e.theme;
|
|
3009
|
+
const themesJson = JSON.stringify(themesMap);
|
|
3010
|
+
const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
|
|
3011
|
+
const devRuntime = isDev ? `
|
|
3012
|
+
const slideImportTokens = ${importTokens};
|
|
3013
|
+
if (import.meta.hot) {
|
|
3014
|
+
import.meta.hot.on('open-slide:slide-changed', (data) => {
|
|
3015
|
+
const ids = Array.isArray(data?.slideIds) ? data.slideIds : data?.slideId ? [data.slideId] : [];
|
|
3016
|
+
const token = Date.now();
|
|
3017
|
+
for (const id of ids) {
|
|
3018
|
+
if (Object.prototype.hasOwnProperty.call(slideImportTokens, id)) slideImportTokens[id] = token;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
` : "";
|
|
3023
|
+
const cases = entries.map((e) => {
|
|
3024
|
+
const importExpr = isDev ? `import(/* @vite-ignore */ ${JSON.stringify(`${e.importPath}?t=`)} + slideImportTokens[${JSON.stringify(e.id)}])` : `import(${JSON.stringify(e.importPath)})`;
|
|
3025
|
+
return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
|
|
3026
|
+
}).join("\n");
|
|
2536
3027
|
return `// virtual:open-slide/slides — generated
|
|
2537
3028
|
export const slideIds = ${ids};
|
|
3029
|
+
export const slideThemes = ${themesJson};
|
|
3030
|
+
${devRuntime}
|
|
2538
3031
|
|
|
2539
3032
|
export async function loadSlide(id) {
|
|
2540
3033
|
switch (id) {
|
|
@@ -2550,6 +3043,32 @@ function openSlidePlugin(opts) {
|
|
|
2550
3043
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
2551
3044
|
const foldersManifestPath = path.join(slidesRoot, ".folders.json");
|
|
2552
3045
|
let isDev = false;
|
|
3046
|
+
const slideIdForEntry = (p) => {
|
|
3047
|
+
const rel = path.relative(slidesRoot, p);
|
|
3048
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
3049
|
+
const parts = rel.split(path.sep);
|
|
3050
|
+
if (parts.length !== 2) return null;
|
|
3051
|
+
if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
|
|
3052
|
+
return parts[0];
|
|
3053
|
+
};
|
|
3054
|
+
let slideChangeTimer = null;
|
|
3055
|
+
const pendingSlideChanges = new Set();
|
|
3056
|
+
const queueSlideChanged = (server, id) => {
|
|
3057
|
+
pendingSlideChanges.add(id);
|
|
3058
|
+
if (slideChangeTimer) clearTimeout(slideChangeTimer);
|
|
3059
|
+
slideChangeTimer = setTimeout(() => {
|
|
3060
|
+
slideChangeTimer = null;
|
|
3061
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
|
|
3062
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
3063
|
+
const slideIds = Array.from(pendingSlideChanges);
|
|
3064
|
+
pendingSlideChanges.clear();
|
|
3065
|
+
server.ws.send({
|
|
3066
|
+
type: "custom",
|
|
3067
|
+
event: "open-slide:slide-changed",
|
|
3068
|
+
data: { slideIds }
|
|
3069
|
+
});
|
|
3070
|
+
}, 100);
|
|
3071
|
+
};
|
|
2553
3072
|
return {
|
|
2554
3073
|
name: "open-slide",
|
|
2555
3074
|
config(_c, env) {
|
|
@@ -2557,17 +3076,17 @@ function openSlidePlugin(opts) {
|
|
|
2557
3076
|
return { server: { fs: { allow: [userCwd] } } };
|
|
2558
3077
|
},
|
|
2559
3078
|
resolveId(id) {
|
|
2560
|
-
if (id === SLIDES_VMOD) return resolved(SLIDES_VMOD);
|
|
2561
|
-
if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
|
|
2562
|
-
if (id === FOLDERS_VMOD) return resolved(FOLDERS_VMOD);
|
|
3079
|
+
if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
|
|
3080
|
+
if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
|
|
3081
|
+
if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
|
|
2563
3082
|
return null;
|
|
2564
3083
|
},
|
|
2565
3084
|
async load(id) {
|
|
2566
|
-
if (id === resolved(SLIDES_VMOD)) {
|
|
3085
|
+
if (id === resolved$1(SLIDES_VMOD)) {
|
|
2567
3086
|
const files = await findSlides(userCwd, slidesDir);
|
|
2568
|
-
return generateSlidesModule(files, slidesRoot, isDev);
|
|
3087
|
+
return await generateSlidesModule(files, slidesRoot, isDev);
|
|
2569
3088
|
}
|
|
2570
|
-
if (id === resolved(CONFIG_VMOD)) {
|
|
3089
|
+
if (id === resolved$1(CONFIG_VMOD)) {
|
|
2571
3090
|
const userBuild = config.build ?? {};
|
|
2572
3091
|
const buildResolved = isDev ? {
|
|
2573
3092
|
showSlideBrowser: true,
|
|
@@ -2584,31 +3103,31 @@ function openSlidePlugin(opts) {
|
|
|
2584
3103
|
};
|
|
2585
3104
|
return `export default ${JSON.stringify(resolvedConfig)};\n`;
|
|
2586
3105
|
}
|
|
2587
|
-
if (id === resolved(FOLDERS_VMOD)) {
|
|
3106
|
+
if (id === resolved$1(FOLDERS_VMOD)) {
|
|
2588
3107
|
const manifest = await readFoldersManifest(foldersManifestPath);
|
|
2589
3108
|
return `export default ${JSON.stringify(manifest)};\n`;
|
|
2590
3109
|
}
|
|
2591
3110
|
return null;
|
|
2592
3111
|
},
|
|
3112
|
+
handleHotUpdate(ctx) {
|
|
3113
|
+
const slideId = slideIdForEntry(ctx.file);
|
|
3114
|
+
if (!slideId) return;
|
|
3115
|
+
queueSlideChanged(ctx.server, slideId);
|
|
3116
|
+
return [];
|
|
3117
|
+
},
|
|
2593
3118
|
configureServer(server) {
|
|
2594
|
-
const isSlideEntry = (p) =>
|
|
2595
|
-
const rel = path.relative(slidesRoot, p);
|
|
2596
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
2597
|
-
const parts = rel.split(path.sep);
|
|
2598
|
-
if (parts.length !== 2) return false;
|
|
2599
|
-
return /^index\.(tsx|jsx|ts|js)$/.test(parts[1]);
|
|
2600
|
-
};
|
|
3119
|
+
const isSlideEntry = (p) => slideIdForEntry(p) !== null;
|
|
2601
3120
|
let reloadTimer = null;
|
|
2602
3121
|
const reload = () => {
|
|
2603
3122
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
2604
3123
|
reloadTimer = setTimeout(() => {
|
|
2605
3124
|
reloadTimer = null;
|
|
2606
|
-
const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
|
|
3125
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
|
|
2607
3126
|
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2608
3127
|
server.ws.send({ type: "full-reload" });
|
|
2609
3128
|
}, 150);
|
|
2610
3129
|
};
|
|
2611
|
-
server.watcher.add(
|
|
3130
|
+
if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
|
|
2612
3131
|
server.watcher.on("add", (p) => {
|
|
2613
3132
|
if (isSlideEntry(p)) reload();
|
|
2614
3133
|
});
|
|
@@ -2620,7 +3139,7 @@ function openSlidePlugin(opts) {
|
|
|
2620
3139
|
if (foldersTimer) clearTimeout(foldersTimer);
|
|
2621
3140
|
foldersTimer = setTimeout(() => {
|
|
2622
3141
|
foldersTimer = null;
|
|
2623
|
-
const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
|
|
3142
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
|
|
2624
3143
|
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2625
3144
|
}, 100);
|
|
2626
3145
|
};
|
|
@@ -2647,6 +3166,144 @@ async function loadUserConfig(userCwd) {
|
|
|
2647
3166
|
return loaded?.config ?? {};
|
|
2648
3167
|
}
|
|
2649
3168
|
|
|
3169
|
+
//#endregion
|
|
3170
|
+
//#region src/vite/themes-plugin.ts
|
|
3171
|
+
const THEMES_VMOD = "virtual:open-slide/themes";
|
|
3172
|
+
function resolved(id) {
|
|
3173
|
+
return `\0${id}`;
|
|
3174
|
+
}
|
|
3175
|
+
const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
3176
|
+
function parseFrontmatter(raw, themeId) {
|
|
3177
|
+
const match = raw.match(FM_RE);
|
|
3178
|
+
const fmText = match ? match[1] : "";
|
|
3179
|
+
const body = match ? match[2] : raw;
|
|
3180
|
+
const data = {};
|
|
3181
|
+
for (const line of fmText.split(/\r?\n/)) {
|
|
3182
|
+
const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
3183
|
+
if (!m) continue;
|
|
3184
|
+
let value = m[2].trim();
|
|
3185
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
3186
|
+
data[m[1]] = value;
|
|
3187
|
+
}
|
|
3188
|
+
return {
|
|
3189
|
+
fm: {
|
|
3190
|
+
name: data.name || themeId,
|
|
3191
|
+
description: data.description || ""
|
|
3192
|
+
},
|
|
3193
|
+
body: body.trim()
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
async function findThemes(userCwd, themesDir) {
|
|
3197
|
+
const abs = path.resolve(userCwd, themesDir);
|
|
3198
|
+
if (!existsSync(abs)) return [];
|
|
3199
|
+
const hits = await fg("*.md", {
|
|
3200
|
+
cwd: abs,
|
|
3201
|
+
absolute: true,
|
|
3202
|
+
onlyFiles: true
|
|
3203
|
+
});
|
|
3204
|
+
return hits.sort();
|
|
3205
|
+
}
|
|
3206
|
+
async function readTheme(mdAbs, themesRoot) {
|
|
3207
|
+
const id = path.basename(mdAbs, ".md");
|
|
3208
|
+
const raw = await fs.readFile(mdAbs, "utf8");
|
|
3209
|
+
const { fm, body } = parseFrontmatter(raw, id);
|
|
3210
|
+
const demoCandidates = [
|
|
3211
|
+
`${id}.demo.tsx`,
|
|
3212
|
+
`${id}.demo.jsx`,
|
|
3213
|
+
`${id}.demo.ts`,
|
|
3214
|
+
`${id}.demo.js`
|
|
3215
|
+
];
|
|
3216
|
+
let demoAbs = null;
|
|
3217
|
+
for (const cand of demoCandidates) {
|
|
3218
|
+
const p = path.join(themesRoot, cand);
|
|
3219
|
+
if (existsSync(p)) {
|
|
3220
|
+
demoAbs = p;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return {
|
|
3225
|
+
id,
|
|
3226
|
+
frontmatter: fm,
|
|
3227
|
+
body,
|
|
3228
|
+
demoAbs
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
function generateThemesModule(themes, isDev) {
|
|
3232
|
+
const meta = themes.map((t$3) => ({
|
|
3233
|
+
id: t$3.id,
|
|
3234
|
+
name: t$3.frontmatter.name,
|
|
3235
|
+
description: t$3.frontmatter.description,
|
|
3236
|
+
body: t$3.body,
|
|
3237
|
+
hasDemo: t$3.demoAbs !== null
|
|
3238
|
+
}));
|
|
3239
|
+
const cases = themes.flatMap((t$3) => {
|
|
3240
|
+
const abs = t$3.demoAbs;
|
|
3241
|
+
if (!abs) return [];
|
|
3242
|
+
const importPath = isDev ? `/@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
|
|
3243
|
+
return [` case ${JSON.stringify(t$3.id)}: return import(${JSON.stringify(importPath)});`];
|
|
3244
|
+
}).join("\n");
|
|
3245
|
+
return `// virtual:open-slide/themes — generated
|
|
3246
|
+
export const themes = ${JSON.stringify(meta)};
|
|
3247
|
+
|
|
3248
|
+
export async function loadThemeDemo(id) {
|
|
3249
|
+
switch (id) {
|
|
3250
|
+
${cases}
|
|
3251
|
+
default: throw new Error('Theme demo not found: ' + id);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
`;
|
|
3255
|
+
}
|
|
3256
|
+
function themesPlugin(opts) {
|
|
3257
|
+
const { userCwd, config } = opts;
|
|
3258
|
+
const themesDir = config.themesDir ?? "themes";
|
|
3259
|
+
const themesRoot = path.resolve(userCwd, themesDir);
|
|
3260
|
+
let isDev = false;
|
|
3261
|
+
return {
|
|
3262
|
+
name: "open-slide:themes",
|
|
3263
|
+
config(_c, env) {
|
|
3264
|
+
isDev = env.command === "serve";
|
|
3265
|
+
},
|
|
3266
|
+
resolveId(id) {
|
|
3267
|
+
if (id === THEMES_VMOD) return resolved(THEMES_VMOD);
|
|
3268
|
+
return null;
|
|
3269
|
+
},
|
|
3270
|
+
async load(id) {
|
|
3271
|
+
if (id !== resolved(THEMES_VMOD)) return null;
|
|
3272
|
+
const files = await findThemes(userCwd, themesDir);
|
|
3273
|
+
const themes = await Promise.all(files.map((f) => readTheme(f, themesRoot)));
|
|
3274
|
+
return generateThemesModule(themes, isDev);
|
|
3275
|
+
},
|
|
3276
|
+
configureServer(server) {
|
|
3277
|
+
const isThemeFile = (p) => {
|
|
3278
|
+
const rel = path.relative(themesRoot, p);
|
|
3279
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
3280
|
+
if (rel.includes(path.sep)) return false;
|
|
3281
|
+
return /\.(md|demo\.(tsx|jsx|ts|js))$/.test(rel);
|
|
3282
|
+
};
|
|
3283
|
+
let reloadTimer = null;
|
|
3284
|
+
const reload = () => {
|
|
3285
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
3286
|
+
reloadTimer = setTimeout(() => {
|
|
3287
|
+
reloadTimer = null;
|
|
3288
|
+
const mod = server.moduleGraph.getModuleById(resolved(THEMES_VMOD));
|
|
3289
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
3290
|
+
server.ws.send({ type: "full-reload" });
|
|
3291
|
+
}, 150);
|
|
3292
|
+
};
|
|
3293
|
+
if (existsSync(themesRoot)) server.watcher.add(themesRoot);
|
|
3294
|
+
server.watcher.on("add", (p) => {
|
|
3295
|
+
if (isThemeFile(p)) reload();
|
|
3296
|
+
});
|
|
3297
|
+
server.watcher.on("unlink", (p) => {
|
|
3298
|
+
if (isThemeFile(p)) reload();
|
|
3299
|
+
});
|
|
3300
|
+
server.watcher.on("change", (p) => {
|
|
3301
|
+
if (isThemeFile(p)) reload();
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
|
|
2650
3307
|
//#endregion
|
|
2651
3308
|
//#region src/vite/config.ts
|
|
2652
3309
|
function findPackageRoot(fromFile) {
|
|
@@ -2663,7 +3320,11 @@ async function createViteConfig(opts) {
|
|
|
2663
3320
|
const userCwd = path.resolve(opts.userCwd);
|
|
2664
3321
|
const config = opts.config ?? await loadUserConfig(userCwd);
|
|
2665
3322
|
const slidesDir = config.slidesDir ?? "slides";
|
|
3323
|
+
const themesDir = config.themesDir ?? "themes";
|
|
3324
|
+
const assetsDir = config.assetsDir ?? "assets";
|
|
2666
3325
|
const slidesAbs = path.resolve(userCwd, slidesDir);
|
|
3326
|
+
const themesAbs = path.resolve(userCwd, themesDir);
|
|
3327
|
+
const assetsAbs = path.resolve(userCwd, assetsDir);
|
|
2667
3328
|
return {
|
|
2668
3329
|
root: APP_ROOT,
|
|
2669
3330
|
configFile: false,
|
|
@@ -2679,6 +3340,10 @@ async function createViteConfig(opts) {
|
|
|
2679
3340
|
userCwd,
|
|
2680
3341
|
config
|
|
2681
3342
|
}),
|
|
3343
|
+
themesPlugin({
|
|
3344
|
+
userCwd,
|
|
3345
|
+
config
|
|
3346
|
+
}),
|
|
2682
3347
|
designPlugin({ userCwd }),
|
|
2683
3348
|
commentsPlugin({
|
|
2684
3349
|
userCwd,
|
|
@@ -2690,14 +3355,18 @@ async function createViteConfig(opts) {
|
|
|
2690
3355
|
}),
|
|
2691
3356
|
filesPlugin({
|
|
2692
3357
|
userCwd,
|
|
2693
|
-
slidesDir
|
|
3358
|
+
slidesDir,
|
|
3359
|
+
assetsDir
|
|
2694
3360
|
}),
|
|
2695
3361
|
currentPlugin({
|
|
2696
3362
|
userCwd,
|
|
2697
3363
|
slidesDir
|
|
2698
3364
|
})
|
|
2699
3365
|
],
|
|
2700
|
-
resolve: { alias: {
|
|
3366
|
+
resolve: { alias: {
|
|
3367
|
+
"@": APP_ROOT,
|
|
3368
|
+
"@assets": assetsAbs
|
|
3369
|
+
} },
|
|
2701
3370
|
optimizeDeps: {
|
|
2702
3371
|
entries: [path.join(APP_ROOT, "main.tsx")],
|
|
2703
3372
|
include: [
|
|
@@ -2728,7 +3397,9 @@ async function createViteConfig(opts) {
|
|
|
2728
3397
|
fs: { allow: [
|
|
2729
3398
|
APP_ROOT,
|
|
2730
3399
|
userCwd,
|
|
2731
|
-
slidesAbs
|
|
3400
|
+
slidesAbs,
|
|
3401
|
+
themesAbs,
|
|
3402
|
+
assetsAbs
|
|
2732
3403
|
] }
|
|
2733
3404
|
},
|
|
2734
3405
|
build: {
|