diff --git a/plugins/plugin-md-power/src/node/features/icons/writer.ts b/plugins/plugin-md-power/src/node/features/icons/writer.ts index b6f156eb..f279b13b 100644 --- a/plugins/plugin-md-power/src/node/features/icons/writer.ts +++ b/plugins/plugin-md-power/src/node/features/icons/writer.ts @@ -1,3 +1,4 @@ +import { constants, promises as fsp } from 'node:fs' import type { App } from 'vuepress/core' import { getIconContentCSS, getIconData } from '@iconify/utils' import { fs, logger } from 'vuepress/utils' @@ -15,6 +16,7 @@ export interface IconCacheItem { const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8) const iconDataCache = new Map() const URL_CONTENT_RE = /(url\([^]+?\))/ +const CSS_PATH = 'internal/md-power/icons.css' function resolveOption(opt?: boolean | IconsOptions): Required { const options = typeof opt === 'object' ? opt : {} @@ -27,8 +29,16 @@ function resolveOption(opt?: boolean | IconsOptions): Required { export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) { const cache = new Map() const isInstalled = isPackageExists('@iconify/json') + const currentPath = app.dir.temp(CSS_PATH) - const write = (content: string) => app.writeTemp('internal/md-power/icons.css', content) + const write = async (content: string) => { + if (!content && app.env.isDev) { + if (existsSync(currentPath) && (await fsp.stat(currentPath)).isFile()) { + return + } + } + await app.writeTemp(CSS_PATH, content) + } let timer: NodeJS.Timeout | null = null const options = resolveOption(opt) @@ -42,10 +52,12 @@ export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) { timer = setTimeout(async () => { let css = defaultContent - for (const [, { content, className }] of cache) - css += `.${className} {\n --svg: ${content};\n}\n` + if (cache.size > 0) { + for (const [, { content, className }] of cache) + css += `.${className} {\n --svg: ${content};\n}\n` - await write(css) + await write(css) + } }, 100) } @@ -132,3 +144,13 @@ async function genIconContent(iconName: string, cb: (content: string) => void) { const match = content.match(URL_CONTENT_RE) return cb(match ? match[1] : '') } + +function existsSync(fp: string) { + try { + fs.accessSync(fp, constants.R_OK) + return true + } + catch { + return false + } +} diff --git a/theme/src/client/components/Blog/VPBlog.vue b/theme/src/client/components/Blog/VPBlog.vue index 2f84ed29..4e4dbc92 100644 --- a/theme/src/client/components/Blog/VPBlog.vue +++ b/theme/src/client/components/Blog/VPBlog.vue @@ -106,7 +106,7 @@ const { theme, page } = useData() @media (min-width: 960px) { .vp-blog { - min-height: calc(100vh - var(--vp-footer-height, 0px)); + min-height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px)); } .blog-container { diff --git a/theme/src/client/components/VPDoc.vue b/theme/src/client/components/VPDoc.vue index 13142d0b..9d8b7470 100644 --- a/theme/src/client/components/VPDoc.vue +++ b/theme/src/client/components/VPDoc.vue @@ -173,6 +173,10 @@ watch( max-width: 784px; } + .vp-doc-container:not(.has-sidebar.has-aside) .content { + max-width: 884px; + } + .vp-doc-container:not(.has-sidebar) .container { max-width: 1104px; } diff --git a/theme/src/client/composables/sidebar.ts b/theme/src/client/composables/sidebar.ts index 08d505cd..a8ec2873 100644 --- a/theme/src/client/composables/sidebar.ts +++ b/theme/src/client/composables/sidebar.ts @@ -373,7 +373,7 @@ export function useSidebarControl(item: ComputedRef): Sideb } return item.value.items - ? containsActiveLink(page.value.filePathRelative || '', item.value.items) + ? containsActiveLink(page.value.path, item.value.items) : false }) diff --git a/theme/src/client/styles/content.css b/theme/src/client/styles/content.css index 8efbcc50..a0588952 100644 --- a/theme/src/client/styles/content.css +++ b/theme/src/client/styles/content.css @@ -98,6 +98,10 @@ } } +.vp-doc > h1:first-of-type { + display: none; +} + .vp-doc img { display: inline-block; } diff --git a/theme/src/node/autoFrontmatter/generator.ts b/theme/src/node/autoFrontmatter/generator.ts index 23a32a30..c59a5801 100644 --- a/theme/src/node/autoFrontmatter/generator.ts +++ b/theme/src/node/autoFrontmatter/generator.ts @@ -93,7 +93,6 @@ export async function watchAutoFrontmatter(app: App, watchers: any[], enable?: ( async function generator(file: AutoFrontmatterMarkdownFile): Promise { if (!generate) return - const { filepath, relativePath } = file const current = generate.rules.find(({ filter }) => filter(relativePath)) @@ -115,7 +114,7 @@ async function generator(file: AutoFrontmatterMarkdownFile): Promise { .replace(/\s+\n/g, '\n') const newContent = yaml ? `${yaml}---\n${content}` : content - fs.writeFileSync(filepath, newContent, 'utf-8') + await fs.promises.writeFile(filepath, newContent, 'utf-8') } catch (e) { console.error(e) diff --git a/theme/src/node/autoFrontmatter/resolveOptions.ts b/theme/src/node/autoFrontmatter/resolveOptions.ts index c0e83d6c..37e1a25d 100644 --- a/theme/src/node/autoFrontmatter/resolveOptions.ts +++ b/theme/src/node/autoFrontmatter/resolveOptions.ts @@ -22,7 +22,7 @@ import { resolveNotesOptions } from '../config/index.js' export function resolveOptions( localeOptions: PlumeThemeLocaleOptions, - frontmatter: AutoFrontmatter, + options: AutoFrontmatter, ): AutoFrontmatter { const pkg = getPackage() const { locales = {}, article: articlePrefix = '/article/' } = localeOptions @@ -46,23 +46,28 @@ export function resolveOptions( }) .filter(Boolean) - const baseFrontmatter: AutoFrontmatterObject = { - author(author: string, { relativePath }, data: any) { + const baseFrontmatter: AutoFrontmatterObject = {} + + if (options.author !== false) { + baseFrontmatter.author = (author: string, { relativePath }, data) => { if (author) return author - if (data.friends) + if (data.friends || data.pageLayout === 'friends') return const profile = resolveOptions(relativePath).profile ?? resolveOptions(relativePath).avatar return profile?.name || pkg.author || '' - }, - createTime(formatTime: string, { createTime }, data: any) { + } + } + + if (options.createTime !== false) { + baseFrontmatter.createTime = (formatTime: string, { createTime }, data) => { if (formatTime) return formatTime - if (data.friends) + if (data.friends || data.pageLayout === 'friends') return return format(new Date(createTime), 'yyyy/MM/dd HH:mm:ss') - }, + } } const notesByLocale = (locale: string) => { @@ -86,8 +91,8 @@ export function resolveOptions( } return { - include: frontmatter?.include ?? ['**/*.md'], - exclude: uniq(['.vuepress/**/*', 'node_modules', ...(frontmatter?.exclude ?? [])]), + include: options?.include ?? ['**/*.md'], + exclude: uniq(['.vuepress/**/*', 'node_modules', ...(options?.exclude ?? [])]), frontmatter: [ localesNotesDirs.length @@ -95,31 +100,39 @@ export function resolveOptions( // note 首页链接 include: localesNotesDirs.map(dir => pathJoin(dir, '/{readme,README,index}.md')), frontmatter: { - title(title: string, { relativePath }) { - if (title) - return title - const note = findNote(relativePath) - if (note?.text) - return note.text - return getCurrentDirname('', relativePath) || '' - }, + ...options.title !== false + ? { + title(title: string, { relativePath }) { + if (title) + return title + const note = findNote(relativePath) + if (note?.text) + return note.text + return getCurrentDirname('', relativePath) || '' + }, + } as AutoFrontmatterObject + : undefined, ...baseFrontmatter, - permalink(permalink: string, { relativePath }, data: any) { - if (permalink) - return permalink - if (data.friends) - return - const locale = resolveLocale(relativePath) + ...options.permalink !== false + ? { + permalink(permalink: string, { relativePath }, data: any) { + if (permalink) + return permalink + if (data.friends) + return + const locale = resolveLocale(relativePath) - const prefix = notesByLocale(locale)?.link || '' - const note = findNote(relativePath) - return pathJoin( - locale, - prefix, - note?.link || getCurrentDirname(note?.dir, relativePath), - '/', - ) - }, + const prefix = notesByLocale(locale)?.link || '' + const note = findNote(relativePath) + return pathJoin( + locale, + prefix, + note?.link || getCurrentDirname(note?.dir, relativePath), + '/', + ) + }, + } as AutoFrontmatterObject + : undefined, }, } : '', @@ -127,45 +140,53 @@ export function resolveOptions( ? { include: localesNotesDirs.map(dir => `${dir}**/**.md`), frontmatter: { - title(title: string, { relativePath }) { - if (title) - return title + ...options.title !== false + ? { + title(title: string, { relativePath }) { + if (title) + return title - const note = findNote(relativePath) - let basename = path.basename(relativePath, '.md') - if (note?.sidebar === 'auto') - basename = basename.replace(/^\d+\./, '') + const note = findNote(relativePath) + let basename = path.basename(relativePath, '.md') + if (note?.sidebar === 'auto') + basename = basename.replace(/^\d+\./, '') - return basename - }, + return basename + }, + } as AutoFrontmatterObject + : undefined, ...baseFrontmatter, - permalink(permalink: string, { relativePath }, data: any) { - if (permalink) - return permalink - if (data.friends) - return - const locale = resolveLocale(relativePath) - const notes = notesByLocale(locale) - const note = findNote(relativePath) - const prefix = notes?.link || '' - const args: string[] = [ - locale, - prefix, - note?.link || '', - ] - const sidebar = note?.sidebar + ...options.permalink !== false + ? { + permalink(permalink: string, { relativePath }, data: any) { + if (permalink) + return permalink + if (data.friends) + return + const locale = resolveLocale(relativePath) + const notes = notesByLocale(locale) + const note = findNote(relativePath) + const prefix = notes?.link || '' + const args: string[] = [ + locale, + prefix, + note?.link || '', + ] + const sidebar = note?.sidebar - if (note && sidebar && sidebar !== 'auto') { - const res = resolveLinkBySidebar(sidebar, pathJoin(notes?.dir || '', note.dir || '')) - const file = ensureLeadingSlash(relativePath) - if (res[file]) - args.push(res[file]) - else - res[path.dirname(file)] && args.push(res[path.dirname(file)]) - } + if (note && sidebar && sidebar !== 'auto') { + const res = resolveLinkBySidebar(sidebar, pathJoin(notes?.dir || '', note.dir || '')) + const file = ensureLeadingSlash(relativePath) + if (res[file]) + args.push(res[file]) + else + res[path.dirname(file)] && args.push(res[path.dirname(file)]) + } - return pathJoin(...args, nanoid(), '/') - }, + return pathJoin(...args, nanoid(), '/') + }, + } as AutoFrontmatterObject + : undefined, }, } : '', @@ -176,21 +197,29 @@ export function resolveOptions( { include: '*', frontmatter: { - title(title: string, { relativePath }) { - if (title) - return title - const basename = path.basename(relativePath || '', '.md') - return basename - }, + ...options.title !== false + ? { + title(title: string, { relativePath }) { + if (title) + return title + const basename = path.basename(relativePath || '', '.md') + return basename + }, + } as AutoFrontmatterObject + : undefined, ...baseFrontmatter, - permalink(permalink: string, { relativePath }) { - if (permalink) - return permalink - const locale = resolveLocale(relativePath) - const prefix = withBase(articlePrefix, locale) + ...options.permalink !== false + ? { + permalink(permalink: string, { relativePath }) { + if (permalink) + return permalink + const locale = resolveLocale(relativePath) + const prefix = withBase(articlePrefix, locale) - return normalizePath(`${prefix}/${nanoid()}/`) - }, + return normalizePath(`${prefix}/${nanoid()}/`) + }, + } as AutoFrontmatterObject + : undefined, }, }, ].filter(Boolean) as AutoFrontmatterArray, diff --git a/theme/src/node/loadConfig/loader.ts b/theme/src/node/loadConfig/loader.ts index 4a31030b..ec1425aa 100644 --- a/theme/src/node/loadConfig/loader.ts +++ b/theme/src/node/loadConfig/loader.ts @@ -27,7 +27,6 @@ export interface Loader { dependencies: string[] load: () => Promise<{ config: ThemeConfig, dependencies: string[] }> loaded: boolean - watcher: FSWatcher | null changeEvents: ChangeEvent[] whenLoaded: ChangeEvent[] defaultConfig: ThemeConfig @@ -47,7 +46,6 @@ export async function initConfigLoader( dependencies: [], load: () => compiler(loader!.configFile), loaded: false, - watcher: null, changeEvents: [], whenLoaded: [], defaultConfig, @@ -64,7 +62,7 @@ export async function initConfigLoader( const { config, dependencies = [] } = await loader.load() loader.loaded = true - addDependencies(dependencies) + loader.dependencies = [...dependencies] updateResolvedConfig(app, config) runChangeEvents() @@ -81,14 +79,14 @@ export function watchConfigFile(app: App, watchers: any[]) { cwd: path.join(path.dirname(loader.configFile), '../'), }) - addDependencies() + addDependencies(watcher) watcher.on('change', async () => { if (loader) { loader.loaded = false const { config, dependencies = [] } = await loader.load() loader.loaded = true - addDependencies(dependencies) + addDependencies(watcher, dependencies) updateResolvedConfig(app, config) runChangeEvents() } @@ -99,8 +97,6 @@ export function watchConfigFile(app: App, watchers: any[]) { runChangeEvents() }) - loader.watcher = watcher - watchers.push(watcher) } @@ -147,7 +143,7 @@ function runChangeEvents() { } } -function addDependencies(dependencies?: string[]) { +function addDependencies(watcher: FSWatcher, dependencies?: string[]) { if (!loader) return @@ -155,9 +151,9 @@ function addDependencies(dependencies?: string[]) { const deps = dependencies .filter(dep => !loader!.dependencies.includes(dep) && dep[0] === '.') loader.dependencies.push(...deps) - deps.length && loader.watcher?.add(deps) + watcher.add(deps) } else { - loader.watcher?.add(loader.dependencies) + watcher.add(loader.dependencies) } } diff --git a/theme/src/node/prepare/prepareSidebar.ts b/theme/src/node/prepare/prepareSidebar.ts index 51e0e99a..5cbe34cf 100644 --- a/theme/src/node/prepare/prepareSidebar.ts +++ b/theme/src/node/prepare/prepareSidebar.ts @@ -138,7 +138,7 @@ function getAutoDirSidebar( current.icon = frontmatter.icon as ThemeIcon } if (parent?.items?.length) { - parent.collapsed = true + parent.collapsed = false } parent = current items = current.items as ResolvedSidebarItem[] diff --git a/theme/src/shared/auto-frontmatter.ts b/theme/src/shared/auto-frontmatter.ts index d8f83b79..924a1987 100644 --- a/theme/src/shared/auto-frontmatter.ts +++ b/theme/src/shared/auto-frontmatter.ts @@ -1,4 +1,5 @@ import type { Stats } from 'node:fs' +import type { PlumeThemePageFrontmatter } from './frontmatter/page.js' export interface AutoFrontmatterMarkdownFile { filepath: string @@ -8,13 +9,13 @@ export interface AutoFrontmatterMarkdownFile { stats: Stats } -export type FrontmatterFn = ( +export type FrontmatterFn = ( value: T, file: AutoFrontmatterMarkdownFile, - data: K + data: PlumeThemePageFrontmatter ) => T | PromiseLike -export type AutoFrontmatterObject = Record> +export type AutoFrontmatterObject = Record> export type AutoFrontmatterArray = { include: string | string[] @@ -23,12 +24,42 @@ export type AutoFrontmatterArray = { export interface AutoFrontmatter { /** - * FilterPattern + * glob 匹配,被匹配的文件将会自动生成 frontmatter + * + * @default ['**\/*.md'] */ include?: string | string[] + /** + * glob 匹配,被匹配的文件将不会自动生成 frontmatter + */ exclude?: string | string[] + /** + * 是否自动生成 permalink + * + * @default true + */ + permalink?: boolean + /** + * 是否自动生成 createTime + * + * 默认读取 文件创建时间,`createTitme` 比 vuepress 默认的 `date` 时间更精准到秒 + */ + createTime?: boolean + /** + * 是否自动生成 author + * + * 默认读取 `profile.name` 或 `package.json` 的 `author` + */ + author?: boolean + /** + * 是否自动生成 title + * + * 默认读取文件名作为标题 + */ + title?: boolean + /** * { * key(value, file, data) {