@open-slide/core 1.8.0 → 1.9.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-CCZDC8eF.js → build-ZM7IfDO-.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-C7sZtiY2.js → config-BAZeaz2P.js} +248 -232
- package/dist/{config-D1bANimZ.d.ts → config-D_5nlXFU.d.ts} +6 -1
- package/dist/{dev-kLS_4CAI.js → dev-BQkNTG_t.js} +1 -1
- package/dist/format-CYOb2cAQ.js +1573 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +38 -4
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +1 -1135
- package/dist/{preview-DUkOjOx8.js → preview-D8hUtbRA.js} +1 -1
- package/dist/{types-Bvk1pM70.d.ts → types-AalTbxMj.d.ts} +17 -0
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +2 -1
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +51 -0
- package/src/app/components/sidebar/sidebar.tsx +8 -1
- package/src/app/lib/design-presets.ts +1 -1
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/use-locale.ts +4 -16
- package/src/app/routes/slide.tsx +68 -0
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +19 -0
- package/src/locale/ja.ts +20 -0
- package/src/locale/types.ts +19 -0
- package/src/locale/zh-cn.ts +18 -0
- package/src/locale/zh-tw.ts +18 -0
- package/dist/en-hyGpmL1O.js +0 -375
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import tailwindcss from "@tailwindcss/vite";
|
|
8
8
|
import react from "@vitejs/plugin-react";
|
|
9
9
|
import * as t$4 from "@babel/types";
|
|
@@ -1244,7 +1244,7 @@ function applyRevertAsset(source, assetPath) {
|
|
|
1244
1244
|
|
|
1245
1245
|
//#endregion
|
|
1246
1246
|
//#region src/editing/slide-ops.ts
|
|
1247
|
-
const SLIDE_ID_RE
|
|
1247
|
+
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
1248
1248
|
function validateSlideName(v) {
|
|
1249
1249
|
if (typeof v !== "string") return null;
|
|
1250
1250
|
const trimmed = v.trim();
|
|
@@ -1308,7 +1308,7 @@ function readMetaTitleInSource(source) {
|
|
|
1308
1308
|
return { kind: "missing" };
|
|
1309
1309
|
}
|
|
1310
1310
|
async function rmSlideDir(slidesRoot, slideId) {
|
|
1311
|
-
if (!SLIDE_ID_RE
|
|
1311
|
+
if (!SLIDE_ID_RE.test(slideId)) return false;
|
|
1312
1312
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1313
1313
|
if (!dir.startsWith(slidesRoot + path.sep)) return false;
|
|
1314
1314
|
try {
|
|
@@ -1322,7 +1322,7 @@ async function rmSlideDir(slidesRoot, slideId) {
|
|
|
1322
1322
|
}
|
|
1323
1323
|
}
|
|
1324
1324
|
async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
|
|
1325
|
-
if (!SLIDE_ID_RE
|
|
1325
|
+
if (!SLIDE_ID_RE.test(slideId)) return {
|
|
1326
1326
|
ok: false,
|
|
1327
1327
|
status: 400,
|
|
1328
1328
|
error: "invalid slideId"
|
|
@@ -1345,7 +1345,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
|
|
|
1345
1345
|
}
|
|
1346
1346
|
let newId;
|
|
1347
1347
|
if (desiredId !== void 0) {
|
|
1348
|
-
if (!SLIDE_ID_RE
|
|
1348
|
+
if (!SLIDE_ID_RE.test(desiredId)) return {
|
|
1349
1349
|
ok: false,
|
|
1350
1350
|
status: 400,
|
|
1351
1351
|
error: "invalid newId"
|
|
@@ -1433,7 +1433,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
|
|
|
1433
1433
|
}
|
|
1434
1434
|
}
|
|
1435
1435
|
function resolveSlideEntry(slidesRoot, slideId) {
|
|
1436
|
-
if (!SLIDE_ID_RE
|
|
1436
|
+
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1437
1437
|
const dir = path.resolve(slidesRoot, slideId);
|
|
1438
1438
|
if (!dir.startsWith(slidesRoot + path.sep)) return null;
|
|
1439
1439
|
return path.join(dir, "index.tsx");
|
|
@@ -1770,7 +1770,7 @@ function validateAssetName(v) {
|
|
|
1770
1770
|
return trimmed;
|
|
1771
1771
|
}
|
|
1772
1772
|
function resolveAssetsDir(slidesRoot, slideId) {
|
|
1773
|
-
if (!SLIDE_ID_RE
|
|
1773
|
+
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1774
1774
|
const slideDir = path.resolve(slidesRoot, slideId);
|
|
1775
1775
|
if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
|
|
1776
1776
|
const assetsDir = path.resolve(slideDir, "assets");
|
|
@@ -1883,10 +1883,11 @@ function makeContext(opts) {
|
|
|
1883
1883
|
slidesDir,
|
|
1884
1884
|
slidesRoot,
|
|
1885
1885
|
globalAssetsRoot,
|
|
1886
|
-
manifestPath
|
|
1886
|
+
manifestPath,
|
|
1887
|
+
coreVersion: opts.coreVersion
|
|
1887
1888
|
};
|
|
1888
1889
|
}
|
|
1889
|
-
async function readBody
|
|
1890
|
+
async function readBody(req) {
|
|
1890
1891
|
return await new Promise((resolve, reject) => {
|
|
1891
1892
|
const chunks = [];
|
|
1892
1893
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -1902,17 +1903,21 @@ async function readBody$2(req) {
|
|
|
1902
1903
|
req.on("error", reject);
|
|
1903
1904
|
});
|
|
1904
1905
|
}
|
|
1905
|
-
function json
|
|
1906
|
+
function json(res, status, body) {
|
|
1906
1907
|
res.statusCode = status;
|
|
1907
1908
|
res.setHeader("content-type", "application/json");
|
|
1908
1909
|
res.end(JSON.stringify(body));
|
|
1909
1910
|
}
|
|
1910
|
-
function
|
|
1911
|
-
if (!SLIDE_ID_RE
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1911
|
+
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
1912
|
+
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
1913
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
1914
|
+
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
1915
|
+
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
1914
1916
|
return full;
|
|
1915
1917
|
}
|
|
1918
|
+
function resolveSlideEntryPath(ctx, slideId) {
|
|
1919
|
+
return resolveSlidePath(ctx.userCwd, ctx.slidesDir, slideId);
|
|
1920
|
+
}
|
|
1916
1921
|
|
|
1917
1922
|
//#endregion
|
|
1918
1923
|
//#region src/vite/routes/assets.ts
|
|
@@ -1927,18 +1932,18 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1927
1932
|
if (usagesMatch && method === "GET") {
|
|
1928
1933
|
const scope = usagesMatch[1];
|
|
1929
1934
|
const filename = decodeURIComponent(usagesMatch[2]);
|
|
1930
|
-
if (!validateAssetName(filename)) return json
|
|
1935
|
+
if (!validateAssetName(filename)) return json(res, 400, { error: "invalid path" });
|
|
1931
1936
|
const isGlobal = scope === GLOBAL_SCOPE;
|
|
1932
1937
|
const assetPath = isGlobal ? `@assets/${filename}` : `./assets/${filename}`;
|
|
1933
1938
|
let slideIds;
|
|
1934
1939
|
if (isGlobal) try {
|
|
1935
1940
|
const entries = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
|
|
1936
|
-
slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE
|
|
1941
|
+
slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
|
|
1937
1942
|
} catch {
|
|
1938
1943
|
slideIds = [];
|
|
1939
1944
|
}
|
|
1940
1945
|
else {
|
|
1941
|
-
if (!SLIDE_ID_RE
|
|
1946
|
+
if (!SLIDE_ID_RE.test(scope)) return json(res, 400, { error: "invalid slideId" });
|
|
1942
1947
|
slideIds = [scope];
|
|
1943
1948
|
}
|
|
1944
1949
|
const usages = [];
|
|
@@ -1961,7 +1966,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1961
1966
|
totalCount += count;
|
|
1962
1967
|
}
|
|
1963
1968
|
}
|
|
1964
|
-
return json
|
|
1969
|
+
return json(res, 200, {
|
|
1965
1970
|
usages,
|
|
1966
1971
|
totalCount
|
|
1967
1972
|
});
|
|
@@ -1969,12 +1974,12 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1969
1974
|
if (listMatch && method === "GET") {
|
|
1970
1975
|
const slideId = listMatch[1];
|
|
1971
1976
|
const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
|
|
1972
|
-
if (!scopedDir) return json
|
|
1977
|
+
if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
|
|
1973
1978
|
let entries;
|
|
1974
1979
|
try {
|
|
1975
1980
|
entries = await fs.readdir(scopedDir);
|
|
1976
1981
|
} catch (err) {
|
|
1977
|
-
if (err.code === "ENOENT") return json
|
|
1982
|
+
if (err.code === "ENOENT") return json(res, 200, { assets: [] });
|
|
1978
1983
|
throw err;
|
|
1979
1984
|
}
|
|
1980
1985
|
const assets = [];
|
|
@@ -1997,11 +2002,11 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1997
2002
|
let scanIds;
|
|
1998
2003
|
if (isGlobal) try {
|
|
1999
2004
|
const dirs = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
|
|
2000
|
-
scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE
|
|
2005
|
+
scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
|
|
2001
2006
|
} catch {
|
|
2002
2007
|
scanIds = [];
|
|
2003
2008
|
}
|
|
2004
|
-
else scanIds = SLIDE_ID_RE
|
|
2009
|
+
else scanIds = SLIDE_ID_RE.test(slideId) ? [slideId] : [];
|
|
2005
2010
|
const paths = assets.map((a) => isGlobal ? `@assets/${a.name}` : `./assets/${a.name}`);
|
|
2006
2011
|
const pathToAsset = new Map(paths.map((p, i) => [p, assets[i]]));
|
|
2007
2012
|
for (const sid of scanIds) {
|
|
@@ -2019,13 +2024,13 @@ function registerAssetRoutes(server, ctx) {
|
|
|
2019
2024
|
}
|
|
2020
2025
|
}
|
|
2021
2026
|
}
|
|
2022
|
-
return json
|
|
2027
|
+
return json(res, 200, { assets });
|
|
2023
2028
|
}
|
|
2024
2029
|
if (fileMatch) {
|
|
2025
2030
|
const slideId = fileMatch[1];
|
|
2026
2031
|
const filename = decodeURIComponent(fileMatch[2]);
|
|
2027
2032
|
const file = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, filename);
|
|
2028
|
-
if (!file) return json
|
|
2033
|
+
if (!file) return json(res, 400, { error: "invalid path" });
|
|
2029
2034
|
if (method === "GET") try {
|
|
2030
2035
|
const buf = await fs.readFile(file);
|
|
2031
2036
|
res.statusCode = 200;
|
|
@@ -2034,22 +2039,22 @@ function registerAssetRoutes(server, ctx) {
|
|
|
2034
2039
|
res.end(buf);
|
|
2035
2040
|
return;
|
|
2036
2041
|
} catch (err) {
|
|
2037
|
-
if (err.code === "ENOENT") return json
|
|
2042
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2038
2043
|
throw err;
|
|
2039
2044
|
}
|
|
2040
2045
|
if (method === "POST") {
|
|
2041
2046
|
const requestCheck = validateMutationRequest(req);
|
|
2042
|
-
if (!requestCheck.ok) return json
|
|
2047
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2043
2048
|
const overwrite = url.searchParams.get("overwrite") === "1";
|
|
2044
2049
|
const lenHeader = req.headers["content-length"];
|
|
2045
2050
|
const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
|
|
2046
|
-
if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json
|
|
2051
|
+
if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
|
|
2047
2052
|
if (!overwrite) try {
|
|
2048
2053
|
await fs.access(file);
|
|
2049
|
-
return json
|
|
2054
|
+
return json(res, 409, { error: "asset exists" });
|
|
2050
2055
|
} catch {}
|
|
2051
2056
|
const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
|
|
2052
|
-
if (!scopedDir) return json
|
|
2057
|
+
if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
|
|
2053
2058
|
await fs.mkdir(scopedDir, { recursive: true });
|
|
2054
2059
|
const chunks = [];
|
|
2055
2060
|
let total = 0;
|
|
@@ -2067,9 +2072,9 @@ function registerAssetRoutes(server, ctx) {
|
|
|
2067
2072
|
req.on("end", () => resolve());
|
|
2068
2073
|
req.on("error", reject);
|
|
2069
2074
|
});
|
|
2070
|
-
if (oversized) return json
|
|
2075
|
+
if (oversized) return json(res, 413, { error: "file too large" });
|
|
2071
2076
|
await fs.writeFile(file, Buffer.concat(chunks));
|
|
2072
|
-
return json
|
|
2077
|
+
return json(res, 200, {
|
|
2073
2078
|
ok: true,
|
|
2074
2079
|
name: filename,
|
|
2075
2080
|
size: total,
|
|
@@ -2079,46 +2084,46 @@ function registerAssetRoutes(server, ctx) {
|
|
|
2079
2084
|
}
|
|
2080
2085
|
if (method === "PATCH") {
|
|
2081
2086
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2082
|
-
if (!requestCheck.ok) return json
|
|
2083
|
-
const body = await readBody
|
|
2087
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2088
|
+
const body = await readBody(req);
|
|
2084
2089
|
const target = validateAssetName(body.name);
|
|
2085
|
-
if (!target) return json
|
|
2086
|
-
if (target === filename) return json
|
|
2090
|
+
if (!target) return json(res, 400, { error: "invalid name" });
|
|
2091
|
+
if (target === filename) return json(res, 200, {
|
|
2087
2092
|
ok: true,
|
|
2088
2093
|
name: filename
|
|
2089
2094
|
});
|
|
2090
2095
|
const dest = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, target);
|
|
2091
|
-
if (!dest) return json
|
|
2096
|
+
if (!dest) return json(res, 400, { error: "invalid name" });
|
|
2092
2097
|
try {
|
|
2093
2098
|
await fs.access(dest);
|
|
2094
|
-
return json
|
|
2099
|
+
return json(res, 409, { error: "target exists" });
|
|
2095
2100
|
} catch {}
|
|
2096
2101
|
try {
|
|
2097
2102
|
await fs.rename(file, dest);
|
|
2098
2103
|
} catch (err) {
|
|
2099
|
-
if (err.code === "ENOENT") return json
|
|
2104
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2100
2105
|
throw err;
|
|
2101
2106
|
}
|
|
2102
|
-
return json
|
|
2107
|
+
return json(res, 200, {
|
|
2103
2108
|
ok: true,
|
|
2104
2109
|
name: target
|
|
2105
2110
|
});
|
|
2106
2111
|
}
|
|
2107
2112
|
if (method === "DELETE") {
|
|
2108
2113
|
const requestCheck = validateMutationRequest(req);
|
|
2109
|
-
if (!requestCheck.ok) return json
|
|
2114
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2110
2115
|
try {
|
|
2111
2116
|
await fs.unlink(file);
|
|
2112
2117
|
} catch (err) {
|
|
2113
|
-
if (err.code === "ENOENT") return json
|
|
2118
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
2114
2119
|
throw err;
|
|
2115
2120
|
}
|
|
2116
|
-
return json
|
|
2121
|
+
return json(res, 200, { ok: true });
|
|
2117
2122
|
}
|
|
2118
2123
|
}
|
|
2119
2124
|
return next();
|
|
2120
2125
|
} catch (err) {
|
|
2121
|
-
json
|
|
2126
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2122
2127
|
}
|
|
2123
2128
|
});
|
|
2124
2129
|
}
|
|
@@ -2238,32 +2243,32 @@ function registerCommentRoutes(server, ctx) {
|
|
|
2238
2243
|
if (method === "GET" && url.pathname === "/") {
|
|
2239
2244
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
2240
2245
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2241
|
-
if (!file) return json
|
|
2246
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2242
2247
|
let source;
|
|
2243
2248
|
try {
|
|
2244
2249
|
source = await fs.readFile(file, "utf8");
|
|
2245
2250
|
} catch {
|
|
2246
|
-
return json
|
|
2251
|
+
return json(res, 404, { error: "slide not found" });
|
|
2247
2252
|
}
|
|
2248
|
-
return json
|
|
2253
|
+
return json(res, 200, { comments: parseMarkers(source) });
|
|
2249
2254
|
}
|
|
2250
2255
|
if (method === "POST" && url.pathname === "/add") {
|
|
2251
2256
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2252
|
-
if (!requestCheck.ok) return json
|
|
2253
|
-
const body = await readBody
|
|
2257
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2258
|
+
const body = await readBody(req);
|
|
2254
2259
|
const slideId = body.slideId ?? "";
|
|
2255
2260
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2256
|
-
if (!file) return json
|
|
2257
|
-
if (!body.line || body.line < 1) return json
|
|
2258
|
-
if (!body.text || typeof body.text !== "string") return json
|
|
2261
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2262
|
+
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
2263
|
+
if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
|
|
2259
2264
|
let source;
|
|
2260
2265
|
try {
|
|
2261
2266
|
source = await fs.readFile(file, "utf8");
|
|
2262
2267
|
} catch {
|
|
2263
|
-
return json
|
|
2268
|
+
return json(res, 404, { error: "slide not found" });
|
|
2264
2269
|
}
|
|
2265
2270
|
const plan = findInsertion(source, body.line, body.column);
|
|
2266
|
-
if (!plan) return json
|
|
2271
|
+
if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
|
|
2267
2272
|
const id = newCommentId();
|
|
2268
2273
|
const ts = new Date().toISOString();
|
|
2269
2274
|
const payload = b64urlEncode(JSON.stringify({
|
|
@@ -2274,36 +2279,36 @@ function registerCommentRoutes(server, ctx) {
|
|
|
2274
2279
|
const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
|
|
2275
2280
|
await fs.writeFile(file, next$1, "utf8");
|
|
2276
2281
|
const markerLine = offsetToLine(next$1, plan.offset + 1);
|
|
2277
|
-
return json
|
|
2282
|
+
return json(res, 200, {
|
|
2278
2283
|
id,
|
|
2279
2284
|
line: markerLine
|
|
2280
2285
|
});
|
|
2281
2286
|
}
|
|
2282
2287
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
2283
2288
|
const requestCheck = validateMutationRequest(req);
|
|
2284
|
-
if (!requestCheck.ok) return json
|
|
2289
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2285
2290
|
const id = url.pathname.slice(1);
|
|
2286
|
-
if (!/^c-[a-f0-9]+$/.test(id)) return json
|
|
2291
|
+
if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
|
|
2287
2292
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
2288
2293
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2289
|
-
if (!file) return json
|
|
2294
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2290
2295
|
let source;
|
|
2291
2296
|
try {
|
|
2292
2297
|
source = await fs.readFile(file, "utf8");
|
|
2293
2298
|
} catch {
|
|
2294
|
-
return json
|
|
2299
|
+
return json(res, 404, { error: "slide not found" });
|
|
2295
2300
|
}
|
|
2296
2301
|
const lines = source.split("\n");
|
|
2297
2302
|
const idRe = markerDeleteRegex(id);
|
|
2298
2303
|
const hit = lines.findIndex((l) => idRe.test(l));
|
|
2299
|
-
if (hit === -1) return json
|
|
2304
|
+
if (hit === -1) return json(res, 404, { error: "marker not found" });
|
|
2300
2305
|
lines.splice(hit, 1);
|
|
2301
2306
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
2302
|
-
return json
|
|
2307
|
+
return json(res, 200, { ok: true });
|
|
2303
2308
|
}
|
|
2304
2309
|
next();
|
|
2305
2310
|
} catch (err) {
|
|
2306
|
-
json
|
|
2311
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2307
2312
|
}
|
|
2308
2313
|
});
|
|
2309
2314
|
}
|
|
@@ -2316,64 +2321,64 @@ function registerEditRoutes(server, ctx) {
|
|
|
2316
2321
|
const method = req.method ?? "GET";
|
|
2317
2322
|
if (method !== "POST") return next();
|
|
2318
2323
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2319
|
-
if (!requestCheck.ok) return json
|
|
2324
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2320
2325
|
try {
|
|
2321
2326
|
if (url.pathname === "/") {
|
|
2322
|
-
const body = await readBody
|
|
2327
|
+
const body = await readBody(req);
|
|
2323
2328
|
const slideId = body.slideId ?? "";
|
|
2324
2329
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2325
|
-
if (!file) return json
|
|
2326
|
-
if (!body.line || body.line < 1) return json
|
|
2327
|
-
if (!Array.isArray(body.ops)) return json
|
|
2330
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2331
|
+
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
2332
|
+
if (!Array.isArray(body.ops)) return json(res, 400, { error: "missing ops" });
|
|
2328
2333
|
let source;
|
|
2329
2334
|
try {
|
|
2330
2335
|
source = await fs.readFile(file, "utf8");
|
|
2331
2336
|
} catch {
|
|
2332
|
-
return json
|
|
2337
|
+
return json(res, 404, { error: "slide not found" });
|
|
2333
2338
|
}
|
|
2334
2339
|
const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
|
|
2335
|
-
if (!result.ok) return json
|
|
2340
|
+
if (!result.ok) return json(res, result.status, { error: result.error });
|
|
2336
2341
|
const changed = result.source !== source;
|
|
2337
2342
|
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
2338
|
-
return json
|
|
2343
|
+
return json(res, 200, {
|
|
2339
2344
|
ok: true,
|
|
2340
2345
|
changed
|
|
2341
2346
|
});
|
|
2342
2347
|
}
|
|
2343
2348
|
if (url.pathname === "/revert-asset") {
|
|
2344
|
-
const body = await readBody
|
|
2349
|
+
const body = await readBody(req);
|
|
2345
2350
|
const slideId = body.slideId ?? "";
|
|
2346
2351
|
const assetPath = body.assetPath;
|
|
2347
2352
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2348
|
-
if (!file) return json
|
|
2349
|
-
if (typeof assetPath !== "string" || !assetPath) return json
|
|
2350
|
-
if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json
|
|
2353
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2354
|
+
if (typeof assetPath !== "string" || !assetPath) return json(res, 400, { error: "missing assetPath" });
|
|
2355
|
+
if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
|
|
2351
2356
|
let source;
|
|
2352
2357
|
try {
|
|
2353
2358
|
source = await fs.readFile(file, "utf8");
|
|
2354
2359
|
} catch {
|
|
2355
|
-
return json
|
|
2360
|
+
return json(res, 404, { error: "slide not found" });
|
|
2356
2361
|
}
|
|
2357
2362
|
const result = applyRevertAsset(source, assetPath);
|
|
2358
|
-
if (!result.ok) return json
|
|
2363
|
+
if (!result.ok) return json(res, result.status, { error: result.error });
|
|
2359
2364
|
const changed = result.source !== source;
|
|
2360
2365
|
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
2361
|
-
return json
|
|
2366
|
+
return json(res, 200, {
|
|
2362
2367
|
ok: true,
|
|
2363
2368
|
changed
|
|
2364
2369
|
});
|
|
2365
2370
|
}
|
|
2366
2371
|
if (url.pathname === "/batch") {
|
|
2367
|
-
const body = await readBody
|
|
2372
|
+
const body = await readBody(req);
|
|
2368
2373
|
const slideId = body.slideId ?? "";
|
|
2369
2374
|
const file = resolveSlideEntryPath(ctx, slideId);
|
|
2370
|
-
if (!file) return json
|
|
2371
|
-
if (!Array.isArray(body.edits)) return json
|
|
2375
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
2376
|
+
if (!Array.isArray(body.edits)) return json(res, 400, { error: "missing edits" });
|
|
2372
2377
|
let source;
|
|
2373
2378
|
try {
|
|
2374
2379
|
source = await fs.readFile(file, "utf8");
|
|
2375
2380
|
} catch {
|
|
2376
|
-
return json
|
|
2381
|
+
return json(res, 404, { error: "slide not found" });
|
|
2377
2382
|
}
|
|
2378
2383
|
const original = source;
|
|
2379
2384
|
const results = [];
|
|
@@ -2396,7 +2401,7 @@ function registerEditRoutes(server, ctx) {
|
|
|
2396
2401
|
}
|
|
2397
2402
|
const changed = source !== original;
|
|
2398
2403
|
if (changed) await fs.writeFile(file, source, "utf8");
|
|
2399
|
-
return json
|
|
2404
|
+
return json(res, 200, {
|
|
2400
2405
|
ok: true,
|
|
2401
2406
|
changed,
|
|
2402
2407
|
results
|
|
@@ -2404,7 +2409,7 @@ function registerEditRoutes(server, ctx) {
|
|
|
2404
2409
|
}
|
|
2405
2410
|
return next();
|
|
2406
2411
|
} catch (err) {
|
|
2407
|
-
json
|
|
2412
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2408
2413
|
}
|
|
2409
2414
|
});
|
|
2410
2415
|
}
|
|
@@ -2488,16 +2493,16 @@ function registerFolderRoutes(server, ctx) {
|
|
|
2488
2493
|
try {
|
|
2489
2494
|
if (method === "GET" && url.pathname === "/") {
|
|
2490
2495
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2491
|
-
return json
|
|
2496
|
+
return json(res, 200, manifest);
|
|
2492
2497
|
}
|
|
2493
2498
|
if (method === "POST" && url.pathname === "/") {
|
|
2494
2499
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2495
|
-
if (!requestCheck.ok) return json
|
|
2496
|
-
const body = await readBody
|
|
2500
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2501
|
+
const body = await readBody(req);
|
|
2497
2502
|
const name = validateName(body.name);
|
|
2498
|
-
if (!name) return json
|
|
2503
|
+
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2499
2504
|
const icon = validateIcon(body.icon);
|
|
2500
|
-
if (!icon) return json
|
|
2505
|
+
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2501
2506
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2502
2507
|
const folder = {
|
|
2503
2508
|
id: newFolderId(),
|
|
@@ -2506,76 +2511,76 @@ function registerFolderRoutes(server, ctx) {
|
|
|
2506
2511
|
};
|
|
2507
2512
|
manifest.folders.push(folder);
|
|
2508
2513
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2509
|
-
return json
|
|
2514
|
+
return json(res, 200, folder);
|
|
2510
2515
|
}
|
|
2511
2516
|
if (method === "PUT" && url.pathname === "/assign") {
|
|
2512
2517
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2513
|
-
if (!requestCheck.ok) return json
|
|
2514
|
-
const body = await readBody
|
|
2515
|
-
if (typeof body.slideId !== "string" || !SLIDE_ID_RE
|
|
2518
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2519
|
+
const body = await readBody(req);
|
|
2520
|
+
if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
|
|
2516
2521
|
const slideId = body.slideId;
|
|
2517
2522
|
let folderId;
|
|
2518
2523
|
if (body.folderId === null) folderId = null;
|
|
2519
2524
|
else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
|
|
2520
|
-
else return json
|
|
2525
|
+
else return json(res, 400, { error: "invalid folderId" });
|
|
2521
2526
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2522
|
-
if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json
|
|
2527
|
+
if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
|
|
2523
2528
|
if (folderId === null) delete manifest.assignments[slideId];
|
|
2524
2529
|
else manifest.assignments[slideId] = folderId;
|
|
2525
2530
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2526
|
-
return json
|
|
2531
|
+
return json(res, 200, { ok: true });
|
|
2527
2532
|
}
|
|
2528
2533
|
if (method === "PUT" && url.pathname === "/reorder") {
|
|
2529
2534
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2530
|
-
if (!requestCheck.ok) return json
|
|
2531
|
-
const body = await readBody
|
|
2535
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2536
|
+
const body = await readBody(req);
|
|
2532
2537
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2533
2538
|
const ids = validateReorder(body.ids, manifest.folders);
|
|
2534
|
-
if (!ids) return json
|
|
2539
|
+
if (!ids) return json(res, 400, { error: "invalid ids" });
|
|
2535
2540
|
const byId = new Map(manifest.folders.map((f) => [f.id, f]));
|
|
2536
2541
|
manifest.folders = ids.map((id) => byId.get(id));
|
|
2537
2542
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2538
|
-
return json
|
|
2543
|
+
return json(res, 200, { ok: true });
|
|
2539
2544
|
}
|
|
2540
2545
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
2541
2546
|
if (idMatch) {
|
|
2542
2547
|
const id = idMatch[1];
|
|
2543
|
-
if (!FOLDER_ID_RE.test(id)) return json
|
|
2548
|
+
if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
|
|
2544
2549
|
if (method === "PATCH") {
|
|
2545
2550
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2546
|
-
if (!requestCheck.ok) return json
|
|
2547
|
-
const body = await readBody
|
|
2551
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2552
|
+
const body = await readBody(req);
|
|
2548
2553
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2549
2554
|
const folder = manifest.folders.find((f) => f.id === id);
|
|
2550
|
-
if (!folder) return json
|
|
2555
|
+
if (!folder) return json(res, 404, { error: "folder not found" });
|
|
2551
2556
|
if (body.name !== void 0) {
|
|
2552
2557
|
const name = validateName(body.name);
|
|
2553
|
-
if (!name) return json
|
|
2558
|
+
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2554
2559
|
folder.name = name;
|
|
2555
2560
|
}
|
|
2556
2561
|
if (body.icon !== void 0) {
|
|
2557
2562
|
const icon = validateIcon(body.icon);
|
|
2558
|
-
if (!icon) return json
|
|
2563
|
+
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
2559
2564
|
folder.icon = icon;
|
|
2560
2565
|
}
|
|
2561
2566
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2562
|
-
return json
|
|
2567
|
+
return json(res, 200, folder);
|
|
2563
2568
|
}
|
|
2564
2569
|
if (method === "DELETE") {
|
|
2565
2570
|
const requestCheck = validateMutationRequest(req);
|
|
2566
|
-
if (!requestCheck.ok) return json
|
|
2571
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2567
2572
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2568
2573
|
const before = manifest.folders.length;
|
|
2569
2574
|
manifest.folders = manifest.folders.filter((f) => f.id !== id);
|
|
2570
|
-
if (manifest.folders.length === before) return json
|
|
2575
|
+
if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
|
|
2571
2576
|
for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
|
|
2572
2577
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2573
|
-
return json
|
|
2578
|
+
return json(res, 200, { ok: true });
|
|
2574
2579
|
}
|
|
2575
2580
|
}
|
|
2576
2581
|
next();
|
|
2577
2582
|
} catch (err) {
|
|
2578
|
-
json
|
|
2583
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2579
2584
|
}
|
|
2580
2585
|
});
|
|
2581
2586
|
}
|
|
@@ -2590,30 +2595,30 @@ function registerSlideRoutes(server, ctx) {
|
|
|
2590
2595
|
const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
|
|
2591
2596
|
if (reorderMatch && method === "PUT") {
|
|
2592
2597
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2593
|
-
if (!requestCheck.ok) return json
|
|
2598
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2594
2599
|
const slideId$1 = reorderMatch[1];
|
|
2595
|
-
if (!SLIDE_ID_RE
|
|
2596
|
-
const body = await readBody
|
|
2597
|
-
if (!Array.isArray(body.order)) return json
|
|
2600
|
+
if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
|
|
2601
|
+
const body = await readBody(req);
|
|
2602
|
+
if (!Array.isArray(body.order)) return json(res, 400, { error: "invalid order" });
|
|
2598
2603
|
const order = [];
|
|
2599
2604
|
for (const v of body.order) {
|
|
2600
|
-
if (!Number.isInteger(v)) return json
|
|
2605
|
+
if (!Number.isInteger(v)) return json(res, 400, { error: "invalid order" });
|
|
2601
2606
|
order.push(v);
|
|
2602
2607
|
}
|
|
2603
2608
|
const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
|
|
2604
|
-
if (!entry) return json
|
|
2609
|
+
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
2605
2610
|
let source;
|
|
2606
2611
|
try {
|
|
2607
2612
|
source = await fs.readFile(entry, "utf8");
|
|
2608
2613
|
} catch {
|
|
2609
|
-
return json
|
|
2614
|
+
return json(res, 404, { error: "slide not found" });
|
|
2610
2615
|
}
|
|
2611
2616
|
const reordered = reorderDefaultExportPagesInSource(source, order);
|
|
2612
|
-
if (reordered === null) return json
|
|
2617
|
+
if (reordered === null) return json(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
|
|
2613
2618
|
const withNotes = reorderNotesArrayInSource(reordered, order);
|
|
2614
|
-
if (withNotes === null) return json
|
|
2619
|
+
if (withNotes === null) return json(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
|
|
2615
2620
|
if (withNotes !== source) await fs.writeFile(entry, withNotes, "utf8");
|
|
2616
|
-
return json
|
|
2621
|
+
return json(res, 200, {
|
|
2617
2622
|
ok: true,
|
|
2618
2623
|
slideId: slideId$1,
|
|
2619
2624
|
order
|
|
@@ -2624,25 +2629,25 @@ function registerSlideRoutes(server, ctx) {
|
|
|
2624
2629
|
const slideId$1 = pageOpMatch[1];
|
|
2625
2630
|
const pageIndex = Number.parseInt(pageOpMatch[2], 10);
|
|
2626
2631
|
const op = pageOpMatch[3];
|
|
2627
|
-
if (!SLIDE_ID_RE
|
|
2628
|
-
if (!Number.isInteger(pageIndex) || pageIndex < 0) return json
|
|
2632
|
+
if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
|
|
2633
|
+
if (!Number.isInteger(pageIndex) || pageIndex < 0) return json(res, 400, { error: "invalid page index" });
|
|
2629
2634
|
const isDelete = method === "DELETE" && !op;
|
|
2630
2635
|
const isDuplicate = method === "POST" && op === "duplicate";
|
|
2631
2636
|
if (!isDelete && !isDuplicate) return next();
|
|
2632
2637
|
const requestCheck = validateMutationRequest(req);
|
|
2633
|
-
if (!requestCheck.ok) return json
|
|
2638
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2634
2639
|
const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
|
|
2635
|
-
if (!entry) return json
|
|
2640
|
+
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
2636
2641
|
let source;
|
|
2637
2642
|
try {
|
|
2638
2643
|
source = await fs.readFile(entry, "utf8");
|
|
2639
2644
|
} catch {
|
|
2640
|
-
return json
|
|
2645
|
+
return json(res, 404, { error: "slide not found" });
|
|
2641
2646
|
}
|
|
2642
2647
|
const updated = isDelete ? removePageFromDefaultExportInSource(source, pageIndex) : duplicatePageInDefaultExportInSource(source, pageIndex);
|
|
2643
|
-
if (updated === null) return json
|
|
2648
|
+
if (updated === null) return json(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" });
|
|
2644
2649
|
if (updated !== source) await fs.writeFile(entry, updated, "utf8");
|
|
2645
|
-
return json
|
|
2650
|
+
return json(res, 200, {
|
|
2646
2651
|
ok: true,
|
|
2647
2652
|
slideId: slideId$1,
|
|
2648
2653
|
index: pageIndex
|
|
@@ -2651,20 +2656,20 @@ function registerSlideRoutes(server, ctx) {
|
|
|
2651
2656
|
const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
|
|
2652
2657
|
if (duplicateMatch && method === "POST") {
|
|
2653
2658
|
const requestCheck = validateMutationRequest(req);
|
|
2654
|
-
if (!requestCheck.ok) return json
|
|
2659
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2655
2660
|
const slideId$1 = duplicateMatch[1];
|
|
2656
|
-
if (!SLIDE_ID_RE
|
|
2657
|
-
const body = await readBody
|
|
2658
|
-
if (body.newId !== void 0 && typeof body.newId !== "string") return json
|
|
2661
|
+
if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
|
|
2662
|
+
const body = await readBody(req);
|
|
2663
|
+
if (body.newId !== void 0 && typeof body.newId !== "string") return json(res, 400, { error: "invalid newId" });
|
|
2659
2664
|
const duplicated = await duplicateSlideDir(ctx.slidesRoot, slideId$1, body.newId);
|
|
2660
|
-
if (!duplicated.ok) return json
|
|
2665
|
+
if (!duplicated.ok) return json(res, duplicated.status, { error: duplicated.error });
|
|
2661
2666
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2662
2667
|
const folderId = manifest.assignments[slideId$1];
|
|
2663
2668
|
if (folderId) {
|
|
2664
2669
|
manifest.assignments[duplicated.slideId] = folderId;
|
|
2665
2670
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2666
2671
|
}
|
|
2667
|
-
return json
|
|
2672
|
+
return json(res, 200, {
|
|
2668
2673
|
ok: true,
|
|
2669
2674
|
slideId: duplicated.slideId
|
|
2670
2675
|
});
|
|
@@ -2672,26 +2677,26 @@ function registerSlideRoutes(server, ctx) {
|
|
|
2672
2677
|
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
2673
2678
|
if (!idMatch) return next();
|
|
2674
2679
|
const slideId = idMatch[1];
|
|
2675
|
-
if (!SLIDE_ID_RE
|
|
2680
|
+
if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
|
|
2676
2681
|
if (method === "PATCH") {
|
|
2677
2682
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2678
|
-
if (!requestCheck.ok) return json
|
|
2679
|
-
const body = await readBody
|
|
2683
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2684
|
+
const body = await readBody(req);
|
|
2680
2685
|
const name = validateSlideName(body.name);
|
|
2681
|
-
if (!name) return json
|
|
2686
|
+
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2682
2687
|
const entry = resolveSlideEntry(ctx.slidesRoot, slideId);
|
|
2683
|
-
if (!entry) return json
|
|
2688
|
+
if (!entry) return json(res, 400, { error: "invalid slideId" });
|
|
2684
2689
|
let source;
|
|
2685
2690
|
try {
|
|
2686
2691
|
source = await fs.readFile(entry, "utf8");
|
|
2687
2692
|
} catch {
|
|
2688
|
-
return json
|
|
2693
|
+
return json(res, 404, { error: "slide not found" });
|
|
2689
2694
|
}
|
|
2690
2695
|
const updated = updateMetaTitleInSource(source, name);
|
|
2691
|
-
if (updated === null) return json
|
|
2696
|
+
if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
|
|
2692
2697
|
if (updated !== source) await fs.writeFile(entry, updated, "utf8");
|
|
2693
2698
|
server.ws.send({ type: "full-reload" });
|
|
2694
|
-
return json
|
|
2699
|
+
return json(res, 200, {
|
|
2695
2700
|
ok: true,
|
|
2696
2701
|
slideId,
|
|
2697
2702
|
name
|
|
@@ -2699,17 +2704,17 @@ function registerSlideRoutes(server, ctx) {
|
|
|
2699
2704
|
}
|
|
2700
2705
|
if (method === "DELETE") {
|
|
2701
2706
|
const requestCheck = validateMutationRequest(req);
|
|
2702
|
-
if (!requestCheck.ok) return json
|
|
2707
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2703
2708
|
const removed = await rmSlideDir(ctx.slidesRoot, slideId);
|
|
2704
|
-
if (!removed) return json
|
|
2709
|
+
if (!removed) return json(res, 404, { error: "slide not found" });
|
|
2705
2710
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2706
2711
|
delete manifest.assignments[slideId];
|
|
2707
2712
|
await writeManifest(ctx.manifestPath, manifest);
|
|
2708
|
-
return json
|
|
2713
|
+
return json(res, 200, { ok: true });
|
|
2709
2714
|
}
|
|
2710
2715
|
return next();
|
|
2711
2716
|
} catch (err) {
|
|
2712
|
-
json
|
|
2717
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
2713
2718
|
}
|
|
2714
2719
|
});
|
|
2715
2720
|
}
|
|
@@ -2733,16 +2738,16 @@ function registerSvglRoutes(server) {
|
|
|
2733
2738
|
target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
|
|
2734
2739
|
} else if (reqUrl.pathname === "/svg") {
|
|
2735
2740
|
const u = reqUrl.searchParams.get("u");
|
|
2736
|
-
if (!u) return json
|
|
2741
|
+
if (!u) return json(res, 400, { error: "missing u" });
|
|
2737
2742
|
let parsed;
|
|
2738
2743
|
try {
|
|
2739
2744
|
parsed = new URL(u);
|
|
2740
2745
|
} catch {
|
|
2741
|
-
return json
|
|
2746
|
+
return json(res, 400, { error: "invalid u" });
|
|
2742
2747
|
}
|
|
2743
|
-
if (parsed.protocol !== "https:") return json
|
|
2748
|
+
if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
|
|
2744
2749
|
const host = parsed.hostname.toLowerCase();
|
|
2745
|
-
if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json
|
|
2750
|
+
if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
|
|
2746
2751
|
target = parsed.toString();
|
|
2747
2752
|
} else return next();
|
|
2748
2753
|
const upstream = await fetch(target);
|
|
@@ -2753,11 +2758,68 @@ function registerSvglRoutes(server) {
|
|
|
2753
2758
|
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
2754
2759
|
res.end(buf);
|
|
2755
2760
|
} catch (err) {
|
|
2756
|
-
json
|
|
2761
|
+
json(res, 502, { error: String(err.message ?? err) });
|
|
2757
2762
|
}
|
|
2758
2763
|
});
|
|
2759
2764
|
}
|
|
2760
2765
|
|
|
2766
|
+
//#endregion
|
|
2767
|
+
//#region src/vite/routes/update.ts
|
|
2768
|
+
const PKG = "@open-slide/core";
|
|
2769
|
+
const CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
2770
|
+
let cache = null;
|
|
2771
|
+
function parseSemver(v) {
|
|
2772
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
2773
|
+
if (!m) return null;
|
|
2774
|
+
return [
|
|
2775
|
+
Number(m[1]),
|
|
2776
|
+
Number(m[2]),
|
|
2777
|
+
Number(m[3])
|
|
2778
|
+
];
|
|
2779
|
+
}
|
|
2780
|
+
function isOutdated(current, latest) {
|
|
2781
|
+
const a = parseSemver(current);
|
|
2782
|
+
const b = parseSemver(latest);
|
|
2783
|
+
if (!a || !b) return false;
|
|
2784
|
+
for (let i = 0; i < 3; i++) {
|
|
2785
|
+
if (b[i] > a[i]) return true;
|
|
2786
|
+
if (b[i] < a[i]) return false;
|
|
2787
|
+
}
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
async function fetchLatest(now) {
|
|
2791
|
+
if (cache && now - cache.at < CACHE_TTL_MS) return cache.latest;
|
|
2792
|
+
try {
|
|
2793
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
2794
|
+
signal: AbortSignal.timeout(3e3),
|
|
2795
|
+
headers: { accept: "application/json" }
|
|
2796
|
+
});
|
|
2797
|
+
if (!res.ok) throw new Error(`registry ${res.status}`);
|
|
2798
|
+
const body = await res.json();
|
|
2799
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
2800
|
+
cache = {
|
|
2801
|
+
at: now,
|
|
2802
|
+
latest
|
|
2803
|
+
};
|
|
2804
|
+
return latest;
|
|
2805
|
+
} catch {
|
|
2806
|
+
return cache?.latest ?? null;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
function registerUpdateRoutes(server, current) {
|
|
2810
|
+
server.middlewares.use("/__update-check", async (req, res, next) => {
|
|
2811
|
+
if ((req.method ?? "GET") !== "GET") return next();
|
|
2812
|
+
const latest = await fetchLatest(Date.now());
|
|
2813
|
+
const result = {
|
|
2814
|
+
current,
|
|
2815
|
+
latest,
|
|
2816
|
+
outdated: latest ? isOutdated(current, latest) : false
|
|
2817
|
+
};
|
|
2818
|
+
res.setHeader("cache-control", "no-store");
|
|
2819
|
+
json(res, 200, result);
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2761
2823
|
//#endregion
|
|
2762
2824
|
//#region src/vite/routes/watchers.ts
|
|
2763
2825
|
function registerWatchers(server, ctx) {
|
|
@@ -2783,7 +2845,7 @@ function registerWatchers(server, ctx) {
|
|
|
2783
2845
|
const parts = rel.split(path.sep);
|
|
2784
2846
|
if (parts.length < 3 || parts[1] !== "assets") return;
|
|
2785
2847
|
const slideId = parts[0];
|
|
2786
|
-
if (!SLIDE_ID_RE
|
|
2848
|
+
if (!SLIDE_ID_RE.test(slideId)) return;
|
|
2787
2849
|
server.ws.send({
|
|
2788
2850
|
type: "custom",
|
|
2789
2851
|
event: "open-slide:assets-changed",
|
|
@@ -2810,13 +2872,13 @@ function apiPlugin(opts) {
|
|
|
2810
2872
|
registerAssetRoutes(server, ctx);
|
|
2811
2873
|
registerSvglRoutes(server);
|
|
2812
2874
|
registerFolderRoutes(server, ctx);
|
|
2875
|
+
registerUpdateRoutes(server, ctx.coreVersion);
|
|
2813
2876
|
}
|
|
2814
2877
|
};
|
|
2815
2878
|
}
|
|
2816
2879
|
|
|
2817
2880
|
//#endregion
|
|
2818
2881
|
//#region src/vite/current-plugin.ts
|
|
2819
|
-
const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
|
|
2820
2882
|
const TEXT_SNIPPET_MAX = 120;
|
|
2821
2883
|
function parseSelection(raw) {
|
|
2822
2884
|
if (raw == null || typeof raw !== "object") return null;
|
|
@@ -2855,7 +2917,7 @@ function currentPlugin(opts) {
|
|
|
2855
2917
|
selection: null
|
|
2856
2918
|
};
|
|
2857
2919
|
if (typeof raw?.slideId === "string") {
|
|
2858
|
-
if (!SLIDE_ID_RE
|
|
2920
|
+
if (!SLIDE_ID_RE.test(raw.slideId)) return;
|
|
2859
2921
|
const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
|
|
2860
2922
|
const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
|
|
2861
2923
|
const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
|
|
@@ -2890,35 +2952,6 @@ function currentPlugin(opts) {
|
|
|
2890
2952
|
|
|
2891
2953
|
//#endregion
|
|
2892
2954
|
//#region src/vite/design-plugin.ts
|
|
2893
|
-
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
2894
|
-
async function readBody$1(req) {
|
|
2895
|
-
return await new Promise((resolve, reject) => {
|
|
2896
|
-
const chunks = [];
|
|
2897
|
-
req.on("data", (c) => chunks.push(c));
|
|
2898
|
-
req.on("end", () => {
|
|
2899
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2900
|
-
if (!raw) return resolve({});
|
|
2901
|
-
try {
|
|
2902
|
-
resolve(JSON.parse(raw));
|
|
2903
|
-
} catch (e) {
|
|
2904
|
-
reject(e);
|
|
2905
|
-
}
|
|
2906
|
-
});
|
|
2907
|
-
req.on("error", reject);
|
|
2908
|
-
});
|
|
2909
|
-
}
|
|
2910
|
-
function json$1(res, status, body) {
|
|
2911
|
-
res.statusCode = status;
|
|
2912
|
-
res.setHeader("content-type", "application/json");
|
|
2913
|
-
res.end(JSON.stringify(body));
|
|
2914
|
-
}
|
|
2915
|
-
function resolveSlidePath$1(userCwd, slidesDir, slideId) {
|
|
2916
|
-
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
2917
|
-
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
2918
|
-
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
2919
|
-
if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
|
|
2920
|
-
return full;
|
|
2921
|
-
}
|
|
2922
2955
|
function parseSource$1(source) {
|
|
2923
2956
|
try {
|
|
2924
2957
|
return parse(source, {
|
|
@@ -3241,28 +3274,28 @@ function designPlugin(opts) {
|
|
|
3241
3274
|
const url = new URL(req.url ?? "/", "http://local");
|
|
3242
3275
|
const method = req.method ?? "GET";
|
|
3243
3276
|
const slideId = url.searchParams.get("slideId") ?? "";
|
|
3244
|
-
const file = resolveSlidePath
|
|
3245
|
-
if (!file) return json
|
|
3277
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
3278
|
+
if (!file) return json(res, 400, { error: "invalid slideId" });
|
|
3246
3279
|
try {
|
|
3247
3280
|
if (method === "GET" && url.pathname === "/") {
|
|
3248
3281
|
let source;
|
|
3249
3282
|
try {
|
|
3250
3283
|
source = await fs.readFile(file, "utf8");
|
|
3251
3284
|
} catch {
|
|
3252
|
-
return json
|
|
3285
|
+
return json(res, 404, { error: "slide not found" });
|
|
3253
3286
|
}
|
|
3254
3287
|
const parsed = parseSlideDesign(source);
|
|
3255
|
-
if (parsed.ok) return json
|
|
3288
|
+
if (parsed.ok) return json(res, 200, {
|
|
3256
3289
|
design: parsed.design,
|
|
3257
3290
|
exists: true,
|
|
3258
3291
|
warning: null
|
|
3259
3292
|
});
|
|
3260
|
-
if (parsed.exists === false) return json
|
|
3293
|
+
if (parsed.exists === false) return json(res, 200, {
|
|
3261
3294
|
design: defaultDesign,
|
|
3262
3295
|
exists: false,
|
|
3263
3296
|
warning: null
|
|
3264
3297
|
});
|
|
3265
|
-
return json
|
|
3298
|
+
return json(res, 200, {
|
|
3266
3299
|
design: defaultDesign,
|
|
3267
3300
|
exists: true,
|
|
3268
3301
|
warning: parsed.error
|
|
@@ -3270,24 +3303,24 @@ function designPlugin(opts) {
|
|
|
3270
3303
|
}
|
|
3271
3304
|
if (method === "PUT" && url.pathname === "/") {
|
|
3272
3305
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
3273
|
-
if (!requestCheck.ok) return json
|
|
3274
|
-
const body = await readBody
|
|
3306
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
3307
|
+
const body = await readBody(req);
|
|
3275
3308
|
const patch = body.patch;
|
|
3276
|
-
if (!patch || typeof patch !== "object") return json
|
|
3309
|
+
if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
|
|
3277
3310
|
let source;
|
|
3278
3311
|
try {
|
|
3279
3312
|
source = await fs.readFile(file, "utf8");
|
|
3280
3313
|
} catch {
|
|
3281
|
-
return json
|
|
3314
|
+
return json(res, 404, { error: "slide not found" });
|
|
3282
3315
|
}
|
|
3283
3316
|
const parsed = parseSlideDesign(source);
|
|
3284
3317
|
const baseDesign = parsed.ok ? parsed.design : defaultDesign;
|
|
3285
|
-
if (!parsed.ok && parsed.exists) return json
|
|
3318
|
+
if (!parsed.ok && parsed.exists) return json(res, 422, { error: parsed.error });
|
|
3286
3319
|
const merged = mergeDesign(baseDesign, patch);
|
|
3287
3320
|
const written = applyDesignWrite(source, merged);
|
|
3288
|
-
if (!written.ok) return json
|
|
3321
|
+
if (!written.ok) return json(res, written.status, { error: written.error });
|
|
3289
3322
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
3290
|
-
return json
|
|
3323
|
+
return json(res, 200, {
|
|
3291
3324
|
ok: true,
|
|
3292
3325
|
design: merged,
|
|
3293
3326
|
created: written.created
|
|
@@ -3295,17 +3328,17 @@ function designPlugin(opts) {
|
|
|
3295
3328
|
}
|
|
3296
3329
|
if (method === "POST" && url.pathname === "/reset") {
|
|
3297
3330
|
const requestCheck = validateMutationRequest(req);
|
|
3298
|
-
if (!requestCheck.ok) return json
|
|
3331
|
+
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
3299
3332
|
let source;
|
|
3300
3333
|
try {
|
|
3301
3334
|
source = await fs.readFile(file, "utf8");
|
|
3302
3335
|
} catch {
|
|
3303
|
-
return json
|
|
3336
|
+
return json(res, 404, { error: "slide not found" });
|
|
3304
3337
|
}
|
|
3305
3338
|
const written = applyDesignWrite(source, defaultDesign);
|
|
3306
|
-
if (!written.ok) return json
|
|
3339
|
+
if (!written.ok) return json(res, written.status, { error: written.error });
|
|
3307
3340
|
if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
|
|
3308
|
-
return json
|
|
3341
|
+
return json(res, 200, {
|
|
3309
3342
|
ok: true,
|
|
3310
3343
|
design: defaultDesign,
|
|
3311
3344
|
created: written.created
|
|
@@ -3313,7 +3346,7 @@ function designPlugin(opts) {
|
|
|
3313
3346
|
}
|
|
3314
3347
|
return next();
|
|
3315
3348
|
} catch (err) {
|
|
3316
|
-
json
|
|
3349
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
3317
3350
|
}
|
|
3318
3351
|
});
|
|
3319
3352
|
}
|
|
@@ -3386,35 +3419,6 @@ function locTagsPlugin(opts) {
|
|
|
3386
3419
|
|
|
3387
3420
|
//#endregion
|
|
3388
3421
|
//#region src/vite/notes-plugin.ts
|
|
3389
|
-
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
3390
|
-
async function readBody(req) {
|
|
3391
|
-
return await new Promise((resolve, reject) => {
|
|
3392
|
-
const chunks = [];
|
|
3393
|
-
req.on("data", (c) => chunks.push(c));
|
|
3394
|
-
req.on("end", () => {
|
|
3395
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
3396
|
-
if (!raw) return resolve({});
|
|
3397
|
-
try {
|
|
3398
|
-
resolve(JSON.parse(raw));
|
|
3399
|
-
} catch (e) {
|
|
3400
|
-
reject(e);
|
|
3401
|
-
}
|
|
3402
|
-
});
|
|
3403
|
-
req.on("error", reject);
|
|
3404
|
-
});
|
|
3405
|
-
}
|
|
3406
|
-
function json(res, status, body) {
|
|
3407
|
-
res.statusCode = status;
|
|
3408
|
-
res.setHeader("content-type", "application/json");
|
|
3409
|
-
res.end(JSON.stringify(body));
|
|
3410
|
-
}
|
|
3411
|
-
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
3412
|
-
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
3413
|
-
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
3414
|
-
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
3415
|
-
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
3416
|
-
return full;
|
|
3417
|
-
}
|
|
3418
3422
|
function parseSource(source) {
|
|
3419
3423
|
try {
|
|
3420
3424
|
return parse(source, {
|
|
@@ -3727,7 +3731,7 @@ ${cases}
|
|
|
3727
3731
|
`;
|
|
3728
3732
|
}
|
|
3729
3733
|
function openSlidePlugin(opts) {
|
|
3730
|
-
const { userCwd, config } = opts;
|
|
3734
|
+
const { userCwd, config, coreVersion } = opts;
|
|
3731
3735
|
const slidesDir = config.slidesDir ?? "slides";
|
|
3732
3736
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
3733
3737
|
const foldersManifestPath = path.join(slidesRoot, ".folders.json");
|
|
@@ -3788,7 +3792,8 @@ function openSlidePlugin(opts) {
|
|
|
3788
3792
|
};
|
|
3789
3793
|
const resolvedConfig = {
|
|
3790
3794
|
...config,
|
|
3791
|
-
build: buildResolved
|
|
3795
|
+
build: buildResolved,
|
|
3796
|
+
version: coreVersion
|
|
3792
3797
|
};
|
|
3793
3798
|
return `export default ${JSON.stringify(resolvedConfig)};\n`;
|
|
3794
3799
|
}
|
|
@@ -4005,6 +4010,15 @@ function findPackageRoot(fromFile) {
|
|
|
4005
4010
|
}
|
|
4006
4011
|
const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
4007
4012
|
const APP_ROOT = path.join(PKG_ROOT, "src", "app");
|
|
4013
|
+
function readCoreVersion() {
|
|
4014
|
+
try {
|
|
4015
|
+
const raw = readFileSync(path.join(PKG_ROOT, "package.json"), "utf8");
|
|
4016
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
4017
|
+
} catch {
|
|
4018
|
+
return "0.0.0";
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
const CORE_VERSION = readCoreVersion();
|
|
4008
4022
|
async function createViteConfig(opts) {
|
|
4009
4023
|
const userCwd = path.resolve(opts.userCwd);
|
|
4010
4024
|
const config = opts.config ?? await loadUserConfig(userCwd);
|
|
@@ -4027,7 +4041,8 @@ async function createViteConfig(opts) {
|
|
|
4027
4041
|
tailwindcss(),
|
|
4028
4042
|
openSlidePlugin({
|
|
4029
4043
|
userCwd,
|
|
4030
|
-
config
|
|
4044
|
+
config,
|
|
4045
|
+
coreVersion: CORE_VERSION
|
|
4031
4046
|
}),
|
|
4032
4047
|
themesPlugin({
|
|
4033
4048
|
userCwd,
|
|
@@ -4037,7 +4052,8 @@ async function createViteConfig(opts) {
|
|
|
4037
4052
|
apiPlugin({
|
|
4038
4053
|
userCwd,
|
|
4039
4054
|
slidesDir,
|
|
4040
|
-
assetsDir
|
|
4055
|
+
assetsDir,
|
|
4056
|
+
coreVersion: CORE_VERSION
|
|
4041
4057
|
}),
|
|
4042
4058
|
notesPlugin({
|
|
4043
4059
|
userCwd,
|