feat: 扩展本地主题兼容能力
This commit is contained in:
+214
-54
@@ -1,16 +1,53 @@
|
||||
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
cp,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sourceRoot =
|
||||
process.env.TYPORA_THEME_DIR ??
|
||||
join(process.env.APPDATA ?? "", "Typora", "themes");
|
||||
const sourceCss = join(sourceRoot, "github.css");
|
||||
const sourceAssets = join(sourceRoot, "github");
|
||||
const targetRoot = join(projectRoot, ".local", "themes", "typora-github");
|
||||
const localRoot = join(projectRoot, ".local");
|
||||
const targetRoot = join(localRoot, "themes");
|
||||
const backupRoot = join(localRoot, "theme-backups");
|
||||
const replaceExisting = process.argv.slice(2).includes("--replace");
|
||||
const unknownArguments = process.argv
|
||||
.slice(2)
|
||||
.filter((argument) => argument !== "--replace");
|
||||
|
||||
if (unknownArguments.length > 0) {
|
||||
throw new Error(`不支持的参数:${unknownArguments.join(", ")}`);
|
||||
}
|
||||
|
||||
const typoraThemes = [
|
||||
{
|
||||
key: "github",
|
||||
name: "Typora 默认(GitHub,本地)"
|
||||
},
|
||||
{
|
||||
key: "pixyll",
|
||||
name: "Typora 默认(Pixyll,本地)"
|
||||
},
|
||||
{
|
||||
key: "whitey",
|
||||
name: "Typora 默认(Whitey,本地)"
|
||||
}
|
||||
];
|
||||
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await access(path, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertReadable(path, label) {
|
||||
try {
|
||||
@@ -20,59 +57,182 @@ async function assertReadable(path, label) {
|
||||
}
|
||||
}
|
||||
|
||||
await assertReadable(sourceCss, "Typora GitHub 主题");
|
||||
await assertReadable(sourceAssets, "Typora GitHub 主题资源目录");
|
||||
async function findTyporaInstallRoot() {
|
||||
const candidates = [
|
||||
process.env.TYPORA_INSTALL_DIR,
|
||||
process.env.ProgramFiles
|
||||
? join(process.env.ProgramFiles, "Typora")
|
||||
: undefined,
|
||||
process.env["ProgramFiles(x86)"]
|
||||
? join(process.env["ProgramFiles(x86)"], "Typora")
|
||||
: undefined,
|
||||
process.env.LOCALAPPDATA
|
||||
? join(process.env.LOCALAPPDATA, "Programs", "Typora")
|
||||
: undefined
|
||||
].filter(Boolean);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (
|
||||
await pathExists(join(candidate, "resources", "style", "base.css"))
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await access(targetRoot, constants.F_OK);
|
||||
throw new Error(
|
||||
`目标目录已存在:${targetRoot}\n请先人工确认并移走旧目录,再重新导入。`
|
||||
"未找到 Typora 安装目录。请通过 TYPORA_INSTALL_DIR 指定安装根目录。"
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("目标目录已存在")) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const installRoot = await findTyporaInstallRoot();
|
||||
const sourceThemeRoot =
|
||||
process.env.TYPORA_THEME_DIR ??
|
||||
join(installRoot, "resources", "style", "themes");
|
||||
const sourceBaseCss =
|
||||
process.env.TYPORA_BASE_CSS ??
|
||||
join(installRoot, "resources", "style", "base.css");
|
||||
const packagePath = join(installRoot, "resources", "package.json");
|
||||
const typoraVersion = (await pathExists(packagePath))
|
||||
? JSON.parse(await readFile(packagePath, "utf8")).version ?? "local"
|
||||
: "local";
|
||||
|
||||
await assertReadable(sourceBaseCss, "Typora 基础 CSS");
|
||||
await assertReadable(sourceThemeRoot, "Typora 默认主题目录");
|
||||
|
||||
for (const theme of typoraThemes) {
|
||||
await assertReadable(
|
||||
join(sourceThemeRoot, `${theme.key}.css`),
|
||||
`Typora ${theme.key} 主题`
|
||||
);
|
||||
}
|
||||
|
||||
const existingTargets = [];
|
||||
for (const theme of typoraThemes) {
|
||||
const target = join(targetRoot, `typora-${theme.key}`);
|
||||
if (await pathExists(target)) {
|
||||
existingTargets.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingTargets.length > 0 && !replaceExisting) {
|
||||
throw new Error(
|
||||
[
|
||||
"以下目标目录已存在:",
|
||||
...existingTargets.map((path) => `- ${path}`),
|
||||
"如已确认替换这些本地副本,请使用 --replace。"
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
await mkdir(localRoot, { recursive: true });
|
||||
await mkdir(targetRoot, { recursive: true });
|
||||
await cp(sourceAssets, join(targetRoot, "github"), {
|
||||
recursive: true,
|
||||
errorOnExist: true,
|
||||
force: false
|
||||
});
|
||||
await writeFile(
|
||||
join(targetRoot, "github.css"),
|
||||
await readFile(sourceCss, "utf8"),
|
||||
"utf8"
|
||||
);
|
||||
const stagingRoot = await mkdtemp(join(localRoot, ".typora-import-"));
|
||||
|
||||
const manifest = {
|
||||
manifestVersion: 1,
|
||||
id: "typora-github",
|
||||
name: "Typora 默认(GitHub,本地)",
|
||||
version: "local",
|
||||
description:
|
||||
"从本机 Typora 安装导入,仅供当前设备个人使用,不进入 Git、容器或发布包。",
|
||||
author: "Typora",
|
||||
license: "本地个人使用,未随项目分发",
|
||||
entry: "github.css",
|
||||
domPreset: "typora",
|
||||
defaultFontSize: "16px",
|
||||
supportedFeatures: [
|
||||
"code",
|
||||
"table",
|
||||
"task-list",
|
||||
"footnote",
|
||||
"katex",
|
||||
"mermaid"
|
||||
],
|
||||
bundled: false
|
||||
};
|
||||
try {
|
||||
for (const theme of typoraThemes) {
|
||||
const id = `typora-${theme.key}`;
|
||||
const stagingThemeRoot = join(stagingRoot, id);
|
||||
const sourceCss = join(sourceThemeRoot, `${theme.key}.css`);
|
||||
const sourceAssets = join(sourceThemeRoot, theme.key);
|
||||
|
||||
await writeFile(
|
||||
join(targetRoot, "theme.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
await mkdir(stagingThemeRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(stagingThemeRoot, "typora-base.css"),
|
||||
await readFile(sourceBaseCss, "utf8"),
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(
|
||||
join(stagingThemeRoot, `${theme.key}.css`),
|
||||
await readFile(sourceCss, "utf8"),
|
||||
"utf8"
|
||||
);
|
||||
if (await pathExists(sourceAssets)) {
|
||||
await cp(sourceAssets, join(stagingThemeRoot, theme.key), {
|
||||
recursive: true,
|
||||
errorOnExist: true,
|
||||
force: false
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`已导入本地 Typora 主题:${targetRoot}`);
|
||||
const manifest = {
|
||||
manifestVersion: 1,
|
||||
id,
|
||||
name: theme.name,
|
||||
version: typoraVersion,
|
||||
description:
|
||||
`从本机 Typora ${typoraVersion} 安装导入,仅供当前设备个人使用,` +
|
||||
"不进入 Git、容器或发布包。",
|
||||
author: "Typora",
|
||||
license: "本地个人使用,未随项目分发",
|
||||
base: "typora-base.css",
|
||||
entry: `${theme.key}.css`,
|
||||
domPreset: "typora",
|
||||
defaultFontSize: "16px",
|
||||
supportedFeatures: [
|
||||
"code",
|
||||
"table",
|
||||
"task-list",
|
||||
"footnote",
|
||||
"katex",
|
||||
"mermaid"
|
||||
],
|
||||
bundled: false
|
||||
};
|
||||
|
||||
await writeFile(
|
||||
join(stagingThemeRoot, "theme.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
let currentBackupRoot;
|
||||
if (existingTargets.length > 0) {
|
||||
const backupStamp = new Date()
|
||||
.toISOString()
|
||||
.replaceAll(":", "")
|
||||
.replaceAll(".", "");
|
||||
currentBackupRoot = join(backupRoot, backupStamp);
|
||||
await mkdir(currentBackupRoot, { recursive: true });
|
||||
}
|
||||
|
||||
const installedThemes = [];
|
||||
const backedUpThemes = [];
|
||||
try {
|
||||
for (const theme of typoraThemes) {
|
||||
const id = `typora-${theme.key}`;
|
||||
const target = join(targetRoot, id);
|
||||
if (await pathExists(target)) {
|
||||
const backup = join(currentBackupRoot, id);
|
||||
await rename(target, backup);
|
||||
backedUpThemes.push({ target, backup });
|
||||
}
|
||||
|
||||
await rename(join(stagingRoot, id), target);
|
||||
installedThemes.push(target);
|
||||
}
|
||||
} catch (error) {
|
||||
for (const target of installedThemes.reverse()) {
|
||||
await rm(target, { recursive: true, force: true });
|
||||
}
|
||||
for (const { target, backup } of backedUpThemes.reverse()) {
|
||||
if (await pathExists(backup)) {
|
||||
await rename(backup, target);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Typora 安装目录:${installRoot}`);
|
||||
console.log(`Typora 版本:${typoraVersion}`);
|
||||
for (const theme of typoraThemes) {
|
||||
console.log(
|
||||
`已导入本地主题:${join(targetRoot, `typora-${theme.key}`)}`
|
||||
);
|
||||
}
|
||||
if (currentBackupRoot) {
|
||||
console.log(`旧主题备份:${currentBackupRoot}`);
|
||||
}
|
||||
} finally {
|
||||
await rm(stagingRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user