diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs new file mode 100644 index 0000000..a794bab --- /dev/null +++ b/.vitepress/config.mjs @@ -0,0 +1,29 @@ +import { defineConfig } from 'vitepress' +import { generateNavAndSidebar } from './navSidebar.mjs' + +const { nav, sidebar } = generateNavAndSidebar(process.cwd()) + +export default defineConfig({ + lang: 'zh-CN', + title: 'CookLikeHOC', + description: '像老乡鸡那样做饭', + lastUpdated: true, + cleanUrls: true, + base: '/CookLikeHOC/', + ignoreDeadLinks: true, + srcExclude: ['**/README.md'], + themeConfig: { + logo: '/logo.png', + nav: [ + { text: '首页', link: '/' }, + ...nav, + { text: 'GitHub', link: 'https://github.com/Gar-b-age/CookLikeHOC' }, + ], + sidebar, + search: { provider: 'local' }, + outline: [2, 3], + docFooter: { prev: '上一页', next: '下一页' }, + lastUpdatedText: '上次更新', + }, + vite: { server: { host: true } }, +}) diff --git a/.vitepress/config.ts b/.vitepress/config.ts new file mode 100644 index 0000000..b4b9e68 --- /dev/null +++ b/.vitepress/config.ts @@ -0,0 +1,33 @@ +import { defineConfig } from 'vitepress' +import { generateNavAndSidebar } from './navSidebar' + +const { nav, sidebar } = generateNavAndSidebar(process.cwd()) + +export default defineConfig({ + lang: 'zh-CN', + title: 'CookLikeHOC', + description: '像老乡鸡那样做饭', + lastUpdated: true, + cleanUrls: true, + themeConfig: { + logo: '/logo.png', + nav: [ + { text: '首页', link: '/' }, + ...nav, + { text: 'GitHub', link: 'https://github.com/Gar-b-age/CookLikeHOC' }, + ], + sidebar, + search: { + provider: 'local' + }, + outline: [2, 3], + docFooter: { + prev: '上一页', + next: '下一页', + }, + lastUpdatedText: '上次更新', + }, + vite: { + server: { host: true }, + }, +}) diff --git a/.vitepress/navSidebar.mjs b/.vitepress/navSidebar.mjs new file mode 100644 index 0000000..c3d665c --- /dev/null +++ b/.vitepress/navSidebar.mjs @@ -0,0 +1,64 @@ +import fs from 'node:fs' +import path from 'node:path' + +const DOC_EXT = ['.md'] +const EXCLUDED_DIRS = new Set(['.git', '.github', '.vitepress', 'node_modules', 'public', 'docs', 'images', 'docker_support']) + +function isDirectory(p) { + return fs.existsSync(p) && fs.statSync(p).isDirectory() +} + +function isMarkdown(p) { + return fs.existsSync(p) && fs.statSync(p).isFile() && DOC_EXT.includes(path.extname(p)) +} + +function titleFromName(name) { + return name.replace(/\.md$/i, '') +} + +function sortByPinyinOrName(a, b) { + return a.localeCompare(b, 'zh-Hans-CN-u-co-pinyin') +} + +export function generateNavAndSidebar(rootDir) { + const entries = fs.readdirSync(rootDir) + const sections = entries + .filter((e) => isDirectory(path.join(rootDir, e))) + .filter((e) => !EXCLUDED_DIRS.has(e) && !e.startsWith('.')) + .sort(sortByPinyinOrName) + + const nav = [] + const sidebar = {} + + for (const dir of sections) { + const abs = path.join(rootDir, dir) + const readme = ['README.md', 'readme.md', 'index.md'].find((n) => fs.existsSync(path.join(abs, n))) + const files = fs + .readdirSync(abs) + .filter((f) => isMarkdown(path.join(abs, f))) + .sort(sortByPinyinOrName) + + const items = files.map((f) => ({ + text: titleFromName(f), + link: `/${encodeURI(dir)}/${encodeURI(f)}`, + })) + + if (items.length > 0) { + sidebar[`/${dir}/`] = [ + { + text: dir, + items, + }, + ] + if (readme) { + nav.push({ text: dir, link: `/${encodeURI(dir)}/${encodeURI(readme)}` }) + } else { + nav.push({ text: dir, link: items[0].link }) + } + } else { + nav.push({ text: dir, link: `/${encodeURI(dir)}/` }) + } + } + + return { nav, sidebar } +} diff --git a/.vitepress/navSidebar.ts b/.vitepress/navSidebar.ts new file mode 100644 index 0000000..8b2a2a4 --- /dev/null +++ b/.vitepress/navSidebar.ts @@ -0,0 +1,101 @@ +import fs from 'node:fs' +import path from 'node:path' +import type { DefaultTheme } from 'vitepress' + +export type SidebarItem = DefaultTheme.SidebarItem +export type NavItem = DefaultTheme.NavItem +export type Sidebar = DefaultTheme.Sidebar + +const DOC_EXT = ['.md'] +const EXCLUDED_DIRS = new Set([ + '.git', + '.github', + '.vitepress', + 'node_modules', + 'images', + 'docker_support', + 'public', + 'docs', + 'images', + 'docker_support', +]) + +function isDirectory(p: string) { + return fs.existsSync(p) && fs.statSync(p).isDirectory() +} + +function isMarkdown(p: string) { + return fs.existsSync(p) && fs.statSync(p).isFile() && DOC_EXT.includes(path.extname(p)) +} + +function titleFromName(name: string) { + // strip extension & use as-is (Chinese names kept) + return name.replace(/\.md$/i, '') +} + +function sortByPinyinOrName(a: string, b: string) { + return a.localeCompare(b, 'zh-Hans-CN-u-co-pinyin') +} + +export function generateNavAndSidebar(rootDir: string) { + const entries = fs.readdirSync(rootDir) + const sections = entries + .filter((e) => isDirectory(path.join(rootDir, e))) + .filter((e) => !EXCLUDED_DIRS.has(e) && !e.startsWith('.')) + sections.sort(sortByPinyinOrName) + + const nav: NavItem[] = [] + const sidebar: Sidebar = {} + + // This is the item type we generate from files. It has a required text and link. + // It is compatible with both NavItem and SidebarItem. + type LinkItem = { text: string; link: string } + + for (const dir of sections) { + const abs = path.join(rootDir, dir) + const files = fs + .readdirSync(abs) + .filter((f) => isMarkdown(path.join(abs, f))) + .sort(sortByPinyinOrName) + + const items: LinkItem[] = files.map((f) => ({ + text: titleFromName(f), + link: `/${encodeURI(dir)}/${encodeURI(f)}`, + })) + + if (items.length > 0) { + const readme = ['README.md', 'readme.md', 'index.md'].find((n) => + fs.existsSync(path.join(abs, n)), + ) + const readmeURI = readme ? `/${encodeURI(dir)}/${encodeURI(readme)}` : undefined + + let sectionLink: string + let sectionItems: LinkItem[] + + if (readmeURI) { + sectionLink = readmeURI + sectionItems = items.filter((i) => i.link !== readmeURI) + } else { + sectionLink = items[0].link! + sectionItems = items.slice(1) + } + + sidebar[`/${dir}/`] = [ + { + text: dir, + link: sectionLink, + items: sectionItems, + }, + ] + + nav.push({ + text: dir, + link: sectionLink, + }) + } else { + nav.push({ text: dir, link: `/${encodeURI(dir)}/` }) + } + } + + return { nav, sidebar } +} diff --git a/.vitepress/scripts/generate-indexes.mjs b/.vitepress/scripts/generate-indexes.mjs new file mode 100644 index 0000000..cb0e76a --- /dev/null +++ b/.vitepress/scripts/generate-indexes.mjs @@ -0,0 +1,66 @@ +import fs from 'node:fs' +import path from 'node:path' + +const ROOT = process.cwd() +const EXCLUDED_DIRS = new Set(['.git', '.github', '.vitepress', 'node_modules', 'public', 'docs', 'images', 'docker_support']) + +function isDirectory(p) { + return fs.existsSync(p) && fs.statSync(p).isDirectory() +} + +function isMarkdown(p) { + return fs.existsSync(p) && fs.statSync(p).isFile() && path.extname(p).toLowerCase() === '.md' +} + +function sortByPinyinOrName(a, b) { + return a.localeCompare(b, 'zh-Hans-CN-u-co-pinyin') +} + +function titleFromName(name) { + return name.replace(/\.md$/i, '') +} + +function buildIndexContent(dirName, files) { + const header = `# ${dirName}\n\n\n\n` + if (files.length === 0) return header + '(暂无条目)\n' + const list = files + .sort(sortByPinyinOrName) + .map((f) => `- [${titleFromName(f)}](${encodeURI('./' + f)})`) + .join('\n') + return header + list + '\n' +} + +function shouldOverwriteExisting(readmePath) { + if (!fs.existsSync(readmePath)) return true + const content = fs.readFileSync(readmePath, 'utf8') + // 仅覆盖带有标记的自动生成文件,避免覆盖人工维护的索引 + return content.includes('AUTO-GENERATED: index') +} + +function main() { + const entries = fs.readdirSync(ROOT) + const dirs = entries + .filter((e) => isDirectory(path.join(ROOT, e))) + .filter((e) => !EXCLUDED_DIRS.has(e) && !e.startsWith('.')) + .sort(sortByPinyinOrName) + + let changed = 0 + for (const dir of dirs) { + const abs = path.join(ROOT, dir) + const files = fs + .readdirSync(abs) + .filter((f) => isMarkdown(path.join(abs, f))) + .filter((f) => f.toLowerCase() !== 'readme.md' && f.toLowerCase() !== 'index.md') + .sort(sortByPinyinOrName) + + const readmePath = path.join(abs, 'README.md') + if (!shouldOverwriteExisting(readmePath)) continue + + const content = buildIndexContent(dir, files) + fs.writeFileSync(readmePath, content, 'utf8') + changed++ + } + console.log(`[generate-indexes] updated ${changed} index file(s).`) +} + +main() diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 0000000..48d1b25 --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +import DefaultTheme from 'vitepress/theme' +import './style.css' + +export default { + extends: DefaultTheme, +} diff --git a/.vitepress/theme/style.css b/.vitepress/theme/style.css new file mode 100644 index 0000000..4b05c44 --- /dev/null +++ b/.vitepress/theme/style.css @@ -0,0 +1,7 @@ +:root{ + --vp-font-family-base: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; +} + +.VPDoc .VPDocAsideOutline { + max-height: calc(100vh - 8rem); +} diff --git a/index.md b/index.md new file mode 100644 index 0000000..26243a3 --- /dev/null +++ b/index.md @@ -0,0 +1,19 @@ +--- +layout: home +hero: + name: CookLikeHOC + text: 像老乡鸡那样做饭 + tagline: 文字来自《老乡鸡菜品溯源报告》,并做归纳、编辑与整理 + actions: + - theme: brand + text: 开始浏览 + link: /炒菜/README + - theme: alt + text: GitHub + link: https://github.com/Gar-b-age/CookLikeHOC +features: + - title: 新更新 + details: 已添加2026年发布的《老乡鸡菜品溯源报告 2.0》中新出现的菜品。 + - title: 开始做菜吗 + details: 好 +---