2026-05-08 00:57:12 +00:00
|
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
|
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
|
|
|
|
|
|
|
|
const rootDir = resolve(import.meta.dirname, "..");
|
2026-05-09 03:16:30 +00:00
|
|
|
|
const iconDir = resolve(rootDir, "icons");
|
2026-05-08 06:24:30 +00:00
|
|
|
|
const themePath = resolve(iconDir, "icon-theme.json");
|
|
|
|
|
|
const fontPath = resolve(iconDir, "icons.woff");
|
2026-05-08 00:57:12 +00:00
|
|
|
|
const outputPath = resolve(rootDir, "src/scss/components/_file-icons.generated.scss");
|
|
|
|
|
|
|
|
|
|
|
|
function readJson(path) {
|
|
|
|
|
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cssAttribute(value) {
|
|
|
|
|
|
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cssContent(value) {
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
function iconByClass(theme, fontClass) {
|
|
|
|
|
|
const glyph = theme.glyphs?.find((item) => item.font_class === fontClass);
|
2026-05-08 00:57:12 +00:00
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
if (!glyph?.unicode) {
|
|
|
|
|
|
throw new Error(`找不到图标 glyph:${fontClass}`);
|
2026-05-08 00:57:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
return {
|
|
|
|
|
|
fontCharacter: `\\${glyph.unicode}`,
|
|
|
|
|
|
fontColor: glyph.color ?? colorForIcon(fontClass),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function colorForIcon(fontClass) {
|
|
|
|
|
|
const accentColors = {
|
|
|
|
|
|
accent1: "#ff6188",
|
|
|
|
|
|
accent2: "#fc9867",
|
|
|
|
|
|
accent3: "#ffd866",
|
|
|
|
|
|
accent4: "#a9dc76",
|
|
|
|
|
|
accent5: "#78dce8",
|
|
|
|
|
|
accent6: "#ab9df2",
|
|
|
|
|
|
};
|
|
|
|
|
|
const specialColors = {
|
|
|
|
|
|
agent: "#ab9df2",
|
|
|
|
|
|
api: "#78dce8",
|
|
|
|
|
|
bug: "#ff6188",
|
|
|
|
|
|
changelog: "#fc9867",
|
|
|
|
|
|
cursor: "#66d9ef",
|
|
|
|
|
|
prompts: "#e6db74",
|
|
|
|
|
|
readme: "#a9dc76",
|
|
|
|
|
|
todo: "#ffd866",
|
|
|
|
|
|
};
|
|
|
|
|
|
const accent = fontClass.match(/accent[1-6]$/)?.[0];
|
|
|
|
|
|
|
|
|
|
|
|
return specialColors[fontClass] ?? accentColors[accent] ?? "#ffd866";
|
2026-05-08 00:57:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeFileNameSelector(fileName) {
|
|
|
|
|
|
const escaped = cssAttribute(fileName);
|
|
|
|
|
|
return [
|
|
|
|
|
|
`.nav-file-title[data-path="${escaped}"]::before`,
|
|
|
|
|
|
`.nav-file-title[data-path$="/${escaped}"]::before`,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function iconRule(selectors, definition) {
|
|
|
|
|
|
return `${selectors.join(",\n")} {\n --monokai-file-icon-color: ${definition.fontColor};\n content: "${cssContent(definition.fontCharacter)}";\n}\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sortedEntries(record) {
|
|
|
|
|
|
return Object.entries(record ?? {}).sort(([left], [right]) =>
|
|
|
|
|
|
left.localeCompare(right, "en", { sensitivity: "variant" }),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!existsSync(themePath) || !existsSync(fontPath)) {
|
|
|
|
|
|
console.error(`找不到图标主题资产:${iconDir}`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const theme = readJson(themePath);
|
|
|
|
|
|
const fontBase64 = readFileSync(fontPath).toString("base64");
|
2026-05-15 09:13:25 +00:00
|
|
|
|
const defaultFileIcon = iconByClass(theme, "default-accent3");
|
2026-05-08 00:57:12 +00:00
|
|
|
|
const obsidianFileExtensions = {
|
2026-05-15 09:13:25 +00:00
|
|
|
|
"7z": "archive-accent3",
|
|
|
|
|
|
astro: "astro-accent6",
|
|
|
|
|
|
bash: "shell-accent4",
|
|
|
|
|
|
bmp: "image-accent3",
|
|
|
|
|
|
c: "c-accent1",
|
|
|
|
|
|
canvas: "json-accent6",
|
|
|
|
|
|
cjs: "js-accent3",
|
|
|
|
|
|
cpp: "cpp-accent5",
|
|
|
|
|
|
cs: "csharp-accent6",
|
|
|
|
|
|
css: "css-accent2",
|
|
|
|
|
|
csv: "csv-accent3",
|
|
|
|
|
|
dart: "dart-accent5",
|
|
|
|
|
|
dockerfile: "docker-accent5",
|
|
|
|
|
|
env: "env-accent3",
|
|
|
|
|
|
gif: "image-accent3",
|
|
|
|
|
|
go: "go-accent5",
|
|
|
|
|
|
gql: "graphql-accent1",
|
|
|
|
|
|
graphql: "graphql-accent1",
|
|
|
|
|
|
h: "c-accent1",
|
|
|
|
|
|
hpp: "cpp-accent5",
|
|
|
|
|
|
html: "markup-accent2",
|
|
|
|
|
|
jpeg: "image-accent3",
|
|
|
|
|
|
jpg: "image-accent3",
|
|
|
|
|
|
js: "js-accent3",
|
|
|
|
|
|
json: "json-accent6",
|
|
|
|
|
|
jsx: "react-accent5",
|
|
|
|
|
|
kt: "kotlin-accent6",
|
|
|
|
|
|
less: "less-accent6",
|
|
|
|
|
|
lua: "lua-accent5",
|
|
|
|
|
|
md: "markdown-accent4",
|
|
|
|
|
|
mdx: "markdownx-accent4",
|
|
|
|
|
|
mjs: "js-accent3",
|
|
|
|
|
|
png: "image-accent3",
|
|
|
|
|
|
py: "python-accent4",
|
|
|
|
|
|
rb: "ruby-accent1",
|
|
|
|
|
|
rs: "rust-accent2",
|
|
|
|
|
|
sass: "sass-accent1",
|
|
|
|
|
|
scss: "sass-accent1",
|
|
|
|
|
|
sh: "shell-accent4",
|
|
|
|
|
|
sql: "sql-accent6",
|
|
|
|
|
|
svg: "svg-accent2",
|
|
|
|
|
|
svelte: "svelte-accent1",
|
|
|
|
|
|
swift: "swift-accent1",
|
|
|
|
|
|
tex: "tex-accent2",
|
|
|
|
|
|
toml: "toml-accent6",
|
|
|
|
|
|
ts: "typescript-accent5",
|
|
|
|
|
|
tsx: "react-accent5",
|
|
|
|
|
|
vue: "vue-accent4",
|
|
|
|
|
|
yaml: "yaml-accent3",
|
|
|
|
|
|
yml: "yaml-accent3",
|
|
|
|
|
|
zip: "archive-accent3",
|
2026-05-08 00:57:12 +00:00
|
|
|
|
};
|
|
|
|
|
|
const obsidianFileNames = {
|
2026-05-15 09:13:25 +00:00
|
|
|
|
"AGENTS.md": "agent",
|
|
|
|
|
|
"agents.md": "agent",
|
|
|
|
|
|
"API.md": "api",
|
|
|
|
|
|
"api.md": "api",
|
|
|
|
|
|
"CHANGELOG.md": "changelog",
|
|
|
|
|
|
"changelog.md": "changelog",
|
|
|
|
|
|
"CLAUDE.md": "agent",
|
|
|
|
|
|
"claude.md": "agent",
|
|
|
|
|
|
"CURSOR.md": "cursor",
|
|
|
|
|
|
"cursor.md": "cursor",
|
|
|
|
|
|
"PROMPTS.md": "prompts",
|
|
|
|
|
|
"prompts.md": "prompts",
|
|
|
|
|
|
"package.json": "npm-accent1",
|
|
|
|
|
|
"README.md": "readme",
|
|
|
|
|
|
"readme.md": "readme",
|
|
|
|
|
|
"TODO.md": "todo",
|
|
|
|
|
|
"todo.md": "todo",
|
2026-05-08 00:57:12 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
"/* stylelint-disable */",
|
|
|
|
|
|
"/*",
|
|
|
|
|
|
" * 此文件由 `npm run generate:icons` 自动生成。",
|
2026-05-09 03:16:30 +00:00
|
|
|
|
" * 请修改 `scripts/generate-icon-theme.js` 或 `icons/` 源资产,不要手写编辑本文件。",
|
2026-05-08 00:57:12 +00:00
|
|
|
|
" */",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"@font-face {",
|
|
|
|
|
|
' font-family: "monokai-pro-icons";',
|
|
|
|
|
|
` src: url("data:font/woff;base64,${fontBase64}") format("woff");`,
|
|
|
|
|
|
" font-display: block;",
|
|
|
|
|
|
" font-style: normal;",
|
|
|
|
|
|
" font-weight: 400;",
|
|
|
|
|
|
"}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
".nav-file-title::before {",
|
2026-06-22 09:16:12 +00:00
|
|
|
|
" display: inline-flex;",
|
|
|
|
|
|
" align-items: center;",
|
|
|
|
|
|
" justify-content: center;",
|
|
|
|
|
|
" width: var(--monokai-file-tree-slot-size);",
|
|
|
|
|
|
" min-width: var(--monokai-file-tree-slot-size);",
|
|
|
|
|
|
" height: var(--monokai-file-tree-glyph-size);",
|
|
|
|
|
|
" margin-inline-end: calc(-1 * var(--monokai-file-tree-slot-size));",
|
|
|
|
|
|
" transform: translateX(calc(-1 * (var(--monokai-file-tree-slot-size) + var(--monokai-file-tree-icon-gap))));",
|
2026-05-15 09:41:28 +00:00
|
|
|
|
` --monokai-file-icon-color: ${defaultFileIcon.fontColor};`,
|
2026-05-08 00:57:12 +00:00
|
|
|
|
' color: var(--monokai-file-icon-color, var(--icon-color));',
|
2026-05-15 09:41:28 +00:00
|
|
|
|
` content: "${cssContent(defaultFileIcon.fontCharacter)}";`,
|
2026-05-08 00:57:12 +00:00
|
|
|
|
' font-family: "monokai-pro-icons";',
|
2026-06-22 09:16:12 +00:00
|
|
|
|
" font-size: var(--monokai-file-tree-glyph-size);",
|
2026-05-08 00:57:12 +00:00
|
|
|
|
" font-style: normal;",
|
|
|
|
|
|
" font-variant: normal;",
|
|
|
|
|
|
" font-weight: 400;",
|
|
|
|
|
|
" line-height: 1;",
|
2026-06-22 09:16:12 +00:00
|
|
|
|
" pointer-events: none;",
|
2026-05-08 00:57:12 +00:00
|
|
|
|
" speak: never;",
|
|
|
|
|
|
" text-align: center;",
|
|
|
|
|
|
" text-rendering: auto;",
|
|
|
|
|
|
"}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
for (const [extension, iconId] of sortedEntries(obsidianFileExtensions)) {
|
|
|
|
|
|
const definition = iconByClass(theme, iconId);
|
2026-05-08 00:57:12 +00:00
|
|
|
|
const escapedExtension = cssAttribute(extension);
|
|
|
|
|
|
lines.push(iconRule([`.nav-file-title[data-path$=".${escapedExtension}"]::before`], definition));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
for (const [fileName, iconId] of sortedEntries(obsidianFileNames)) {
|
|
|
|
|
|
const definition = iconByClass(theme, iconId);
|
2026-05-08 00:57:12 +00:00
|
|
|
|
lines.push(iconRule(normalizeFileNameSelector(fileName), definition));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 09:13:25 +00:00
|
|
|
|
lines.push(iconRule([
|
|
|
|
|
|
'.nav-file-title[data-path^="Bug_"][data-path$=".md"]::before',
|
|
|
|
|
|
'.nav-file-title[data-path*="/Bug_"][data-path$=".md"]::before',
|
|
|
|
|
|
], iconByClass(theme, "bug")));
|
|
|
|
|
|
|
2026-05-08 00:57:12 +00:00
|
|
|
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
|
|
|
|
writeFileSync(outputPath, `${lines.join("\n")}\n`);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`已生成文件树图标样式:${outputPath}`);
|