chore: 1.0.0 release
This commit is contained in:
+117
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user