118 lines
3.8 KiB
JavaScript
118 lines
3.8 KiB
JavaScript
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;
|
||
}
|