feat: 扩展本地主题兼容能力

This commit is contained in:
SkyJourney
2026-07-26 00:26:43 +08:00
parent fa07472428
commit ec14f7970a
7 changed files with 769 additions and 130 deletions
+214 -54
View File
@@ -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 });
}