From c3c0d71d1d5ee6244e6b51abef4bbead713aac03 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 29 Mar 2025 23:00:07 -0400 Subject: [PATCH] first working version --- docs/.vitepress/config/index.ts | 3 +- docs/.vitepress/config/shared.ts | 2 - .../{.vitepress/config/zh.ts => zh/config.ts} | 27 ++++---- src/client/app/data.ts | 44 ++++++++++++- src/client/app/utils.ts | 62 +++++++++++++++++++ .../theme-default/support/translation.ts | 1 + 6 files changed, 121 insertions(+), 18 deletions(-) rename docs/{.vitepress/config/zh.ts => zh/config.ts} (90%) diff --git a/docs/.vitepress/config/index.ts b/docs/.vitepress/config/index.ts index 08a81fb9..1654ec57 100644 --- a/docs/.vitepress/config/index.ts +++ b/docs/.vitepress/config/index.ts @@ -1,7 +1,6 @@ import { defineConfig } from 'vitepress' import { shared } from './shared' import { en } from './en' -import { zh } from './zh' import { pt } from './pt' import { ru } from './ru' import { es } from './es' @@ -12,7 +11,7 @@ export default defineConfig({ ...shared, locales: { root: { label: 'English', ...en }, - zh: { label: '简体中文', ...zh }, + zh: { label: '简体中文' }, pt: { label: 'Português', ...pt }, ru: { label: 'Русский', ...ru }, es: { label: 'Español', ...es }, diff --git a/docs/.vitepress/config/shared.ts b/docs/.vitepress/config/shared.ts index 1f4961d2..86eb104a 100644 --- a/docs/.vitepress/config/shared.ts +++ b/docs/.vitepress/config/shared.ts @@ -9,7 +9,6 @@ import { search as faSearch } from './fa' import { search as koSearch } from './ko' import { search as ptSearch } from './pt' import { search as ruSearch } from './ru' -import { search as zhSearch } from './zh' export const shared = defineConfig({ title: 'VitePress', @@ -99,7 +98,6 @@ export const shared = defineConfig({ apiKey: '52f578a92b88ad6abde815aae2b0ad7c', indexName: 'vitepress', locales: { - ...zhSearch, ...ptSearch, ...ruSearch, ...esSearch, diff --git a/docs/.vitepress/config/zh.ts b/docs/zh/config.ts similarity index 90% rename from docs/.vitepress/config/zh.ts rename to docs/zh/config.ts index e9d5fbcd..4120d126 100644 --- a/docs/.vitepress/config/zh.ts +++ b/docs/zh/config.ts @@ -1,16 +1,14 @@ -import { createRequire } from 'module' -import { defineConfig, type DefaultTheme } from 'vitepress' +import { type DefaultTheme, type UserConfig } from 'vitepress' +import vitepress from 'vitepress/package.json' +import type { DocSearchProps } from '../../types/docsearch' -const require = createRequire(import.meta.url) -const pkg = require('vitepress/package.json') - -export const zh = defineConfig({ +export default { lang: 'zh-Hans', description: '由 Vite 和 Vue 驱动的静态站点生成器', themeConfig: { nav: nav(), - + search: { options: searchOptions() } as DefaultTheme.Config['search'], sidebar: { '/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() }, '/zh/reference/': { base: '/zh/reference/', items: sidebarReference() } @@ -43,6 +41,13 @@ export const zh = defineConfig({ } }, + notFound: { + title: '页面未找到', + quote: '若你不改变航向,始终凝望远方,你终将抵达前行的彼岸。', + linkLabel: '返回首页', + linkText: '返回首页' + }, + langMenuLabel: '多语言', returnToTopLabel: '回到顶部', sidebarMenuLabel: '菜单', @@ -51,7 +56,7 @@ export const zh = defineConfig({ darkModeSwitchTitle: '切换到深色模式', skipToContentLabel: '跳转到内容' } -}) +} as UserConfig function nav(): DefaultTheme.NavItem[] { return [ @@ -66,7 +71,7 @@ function nav(): DefaultTheme.NavItem[] { activeMatch: '/zh/reference/' }, { - text: pkg.version, + text: vitepress.version, items: [ { text: '更新日志', @@ -160,8 +165,8 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { ] } -export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { - zh: { +function searchOptions(): Partial { + return { placeholder: '搜索文档', translations: { button: { diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 16288e87..3a64e948 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -19,6 +19,7 @@ import { type SiteData } from '../shared' import type { Route } from './router' +import { dirname, stackView } from './utils' export const dataSymbol: InjectionKey = Symbol() @@ -69,11 +70,46 @@ if (import.meta.hot) { }) } +// hierarchical config pre-loading +const extraConfig: Record = Object.fromEntries( + Object.entries( + import.meta.glob('/**/config.([cm]?js|ts|json)', { + eager: true + }) + ).map(([path, module]) => [ + dirname(path), + { __module__: path, ...((module as any)?.default ?? module) } + ]) +) + +function getExtraConfigs(path: string): SiteData[] { + if (!path.startsWith('/')) path = `/${path}` + const configs: SiteData[] = [] + const segments = path.split('/').slice(1, -1) + while (segments.length) { + const key = `/${segments.join('/')}/` + if (key in extraConfig) configs.push(extraConfig[key]) + segments.pop() + } + // debug info + const summaryTitle = `Extra Configs for ${path}:` + const summary = configs.map((c, i) => ` ${i + 1}. ${(c as any).__module__}`) + summary.push(` ${summary.length + 1}. .vitepress/config (root)`) + console.info( + [summaryTitle, ''.padEnd(summaryTitle.length, '='), ...summary].join('\n') + ) + return configs +} + // per-app data export function initData(route: Route): VitePressData { - const site = computed(() => - resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath) - ) + const site = computed(() => { + const data = resolveSiteDataByRoute( + siteDataRef.value, + route.data.relativePath + ) + return stackView(...getExtraConfigs(route.data.relativePath), data) + }) const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart const isDark = @@ -124,6 +160,8 @@ export function initData(route: Route): VitePressData { export function useData(): VitePressData { const data = inject(dataSymbol) + ;(window as any).stackView = stackView + ;(window as any).data = data if (!data) { throw new Error('vitepress data not properly injected in app') } diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index 53ef6e51..3190cd0a 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -140,3 +140,65 @@ function tryOffsetSelector(selector: string, padding: number): number { if (bot < 0) return 0 return bot + padding } + +export function dirname(path: string) { + const segments = path.split('/') + segments[segments.length - 1] = '' + return segments.join('/') +} + +const unpackStackView = Symbol('unpackStackView') + +function isStackable(obj: any) { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj) +} +/** + * Creates a deep, merged view of multiple objects without mutating originals. + * Returns a readonly proxy behaving like a merged object of the input objects. + * Layers are merged in descending precedence, i.e. earlier layer is on top. + */ +export function stackView(...layers: T[]): T { + layers = layers.filter((layer) => layer !== undefined) + if (layers.length == 0) return undefined as any as T + if (layers.length == 1 || !isStackable(layers[0])) return layers[0] + layers = layers.filter(isStackable) + if (layers.length == 1) return layers[0] + return new Proxy( + {}, + { + get(target, prop) { + if (prop === unpackStackView) { + return layers + } + return stackView(...layers.map((layer) => (layer as any)?.[prop])) + }, + set(target, prop, value) { + throw new Error('StackView is read-only and cannot be mutated.') + }, + has(target, prop) { + for (const layer of layers) { + if (prop in layer) return true + } + return false + }, + ownKeys(target) { + const keys = new Set() + for (const layer of layers) { + for (const key of Object.keys(layer)) { + keys.add(key) + } + } + return Array.from(keys) + }, + getOwnPropertyDescriptor(target, prop) { + for (const layer of layers) { + if (prop in layer) { + return Object.getOwnPropertyDescriptor(layer, prop) + } + } + } + } + ) as T +} + +stackView.unpack = (obj: any) => obj?.[unpackStackView] diff --git a/src/client/theme-default/support/translation.ts b/src/client/theme-default/support/translation.ts index 12ade37b..4c285cbb 100644 --- a/src/client/theme-default/support/translation.ts +++ b/src/client/theme-default/support/translation.ts @@ -15,6 +15,7 @@ export function createSearchTranslate( const isObject = themeObject && typeof themeObject === 'object' const locales = (isObject && themeObject.locales?.[localeIndex.value]?.translations) || + (isObject && themeObject?.translations) || null const translations = (isObject && themeObject.translations) || null