chore: 1.0.0 release

This commit is contained in:
binaryify
2026-08-05 16:25:24 +08:00
commit 5b0d45bf1d
100 changed files with 11034 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
export function normalizeRelativePath(value) {
if (typeof value !== "string") {
throw new TypeError("文件路径必须是字符串");
}
let path = value.trim().replaceAll("\\", "/");
try {
path = decodeURIComponent(path);
} catch {
// Keep the original path when it contains a literal percent sign.
}
path = path.replace(/^file:\/\/+/, "").replace(/^\.\//, "");
if (!path || path.includes("\0") || path.startsWith("/") || /^[A-Za-z]:\//.test(path)) {
throw new Error(`不允许的绝对路径:${value}`);
}
const parts = [];
for (const part of path.split("/")) {
if (!part || part === ".") continue;
if (part === "..") throw new Error(`不允许越过项目目录:${value}`);
parts.push(part);
}
if (parts.length === 0) throw new Error(`无效文件路径:${value}`);
return parts.join("/");
}
export function dirname(path) {
const normalized = normalizeRelativePath(path);
const index = normalized.lastIndexOf("/");
return index === -1 ? "" : normalized.slice(0, index);
}
export function basename(path) {
const normalized = normalizeRelativePath(path);
return normalized.slice(normalized.lastIndexOf("/") + 1);
}
export function joinDeckPath(base, path) {
const child = normalizeRelativePath(path);
if (!base) return child;
const normalizedBase = normalizeRelativePath(base);
if (child === normalizedBase || child.startsWith(`${normalizedBase}/`)) return child;
return normalizeRelativePath(`${normalizedBase}/${child}`);
}
function unquoteYamlScalar(value) {
const text = value.trim();
if (text.startsWith('"')) {
const match = text.match(/^"(?:\\.|[^"\\])*"/);
if (match) {
try {
return JSON.parse(match[0]);
} catch {
return match[0].slice(1, -1);
}
}
}
if (text.startsWith("'")) {
const end = text.indexOf("'", 1);
if (end !== -1) return text.slice(1, end).replaceAll("''", "'");
}
return text.replace(/\s+#.*$/, "").trim();
}
export function extractPagePaths(manifestText) {
const trimmed = manifestText.trim();
if (!trimmed) throw new Error("PPTD manifest 是空文件");
if (trimmed.startsWith("{")) {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed.pages)) throw new Error("PPTD manifest 缺少 pages 数组");
return parsed.pages.map((path) => normalizeRelativePath(String(path)));
}
const lines = manifestText.replaceAll("\r\n", "\n").split("\n");
const pageHeader = lines.findIndex((line) => /^(\s*)pages\s*:\s*(?:#.*)?$/.test(line));
if (pageHeader === -1) throw new Error("PPTD manifest 缺少 pages 列表");
const headerIndent = lines[pageHeader].match(/^\s*/)[0].length;
const pages = [];
for (let index = pageHeader + 1; index < lines.length; index += 1) {
const line = lines[index];
if (!line.trim() || line.trimStart().startsWith("#")) continue;
const indent = line.match(/^\s*/)[0].length;
if (indent <= headerIndent && !/^\s*-/.test(line)) break;
const match = line.match(/^\s*-\s*(.+?)\s*$/);
if (!match) break;
const path = unquoteYamlScalar(match[1]);
if (path) pages.push(normalizeRelativePath(path));
}
if (pages.length === 0) throw new Error("PPTD manifest 的 pages 列表为空");
return pages;
}
export function titleFromManifest(manifestText, fallback) {
const trimmed = manifestText.trim();
if (trimmed.startsWith("{")) {
try {
return JSON.parse(trimmed).title || fallback;
} catch {
return fallback;
}
}
const match = manifestText.match(/^title\s*:\s*(.+?)\s*$/m);
return match ? unquoteYamlScalar(match[1]) : fallback;
}
export function assertWritableChangePath(base, requestedPath) {
const path = joinDeckPath(base, requestedPath);
if (!/\.(?:pptd|page)$/i.test(path)) {
throw new Error(`为安全起见,仅允许编辑 .pptd/.page:${requestedPath}`);
}
return path;
}