diff --git a/plugins/plugin-auto-frontmatter/package.json b/plugins/plugin-auto-frontmatter/package.json index e34d1481..51cacddb 100644 --- a/plugins/plugin-auto-frontmatter/package.json +++ b/plugins/plugin-auto-frontmatter/package.json @@ -2,6 +2,7 @@ "name": "@vuepress-plume/plugin-auto-frontmatter", "type": "module", "version": "1.0.0-rc.72", + "private": true, "description": "The Plugin for VuePress 2 - auto frontmatter", "author": "pengzhanbo ", "license": "MIT", diff --git a/theme/src/node/autoFrontmatter/generator.ts b/theme/src/node/autoFrontmatter/generator.ts new file mode 100644 index 00000000..4de171b4 --- /dev/null +++ b/theme/src/node/autoFrontmatter/generator.ts @@ -0,0 +1,122 @@ +import { fs } from 'vuepress/utils' +import chokidar from 'chokidar' +import { createFilter } from 'create-filter' +import grayMatter from 'gray-matter' +import jsonToYaml from 'json2yaml' +import { isArray, isEmptyObject, promiseParallel, toArray } from '@pengzhanbo/utils' +import type { App } from 'vuepress' +import type { + AutoFrontmatter, + AutoFrontmatterArray, + AutoFrontmatterMarkdownFile, + AutoFrontmatterObject, +} from '../../shared/auto-frontmatter.js' +import type { PlumeThemeLocaleOptions } from '../../shared/index.js' +import { readMarkdown, readMarkdownList } from './readFile.js' +import { resolveOptions } from './resolveOptions.js' + +export interface Generate { + globFilter: (id?: string) => boolean + global: AutoFrontmatterObject + rules: { + include: string | string[] + filter: (id?: string) => boolean + frontmatter: AutoFrontmatterObject + }[] +} + +let generate: Generate | null = null + +export function initAutoFrontmatter( + localeOptions: PlumeThemeLocaleOptions, + autoFrontmatter: AutoFrontmatter = {}, +) { + const { include, exclude, frontmatter = {} } = resolveOptions(localeOptions, autoFrontmatter) + + const globFilter = createFilter(include, exclude, { resolve: false }) + + const userConfig: AutoFrontmatterArray = isArray(frontmatter) + ? frontmatter + : [{ include: '*', frontmatter }] + + const globalConfig: AutoFrontmatterObject + = userConfig.find(({ include }) => include === '*')?.frontmatter || {} + + const rules = userConfig + .filter(({ include }) => include !== '*') + .map(({ include, frontmatter }) => { + return { + include, + filter: createFilter(toArray(include), undefined, { resolve: false }), + frontmatter, + } + }) + + generate = { + globFilter, + global: globalConfig, + rules, + } +} + +export async function generateAFrontmatter(app: App) { + if (!generate) + return + const markdownList = await readMarkdownList(app.dir.source(), generate.globFilter) + await promiseParallel( + markdownList.map(file => () => generator(file)), + 64, + ) +} + +export async function watchAutoFrontmatter(app: App, watchers: any[]) { + if (!generate) + return + + const watcher = chokidar.watch('**/*.md', { + cwd: app.dir.source(), + ignoreInitial: true, + ignored: /(node_modules|\.vuepress)\//, + }) + + watcher.on('add', async (relativePath) => { + if (!generate!.globFilter(relativePath)) + return + const file = await readMarkdown(app.dir.source(), relativePath) + await generator(file) + }) + + watchers.push(watcher) +} + +async function generator(file: AutoFrontmatterMarkdownFile): Promise { + if (!generate) + return + + const { filepath, relativePath } = file + + const current = generate.rules.find(({ filter }) => filter(relativePath)) + const formatter = current?.frontmatter || generate.global + const { data, content } = grayMatter(file.content) + + for (const key in formatter) { + const value = await formatter[key](data[key], file, data) + data[key] = value ?? data[key] + } + + try { + const yaml = isEmptyObject(data) + ? '' + : jsonToYaml + .stringify(data) + .replace(/\n\s{2}/g, '\n') + .replace(/"/g, '') + .replace(/\s+\n/g, '\n') + const newContent = yaml ? `${yaml}---\n${content}` : content + + fs.writeFileSync(filepath, newContent, 'utf-8') + } + catch (e) { + console.error(e) + } +} diff --git a/theme/src/node/autoFrontmatter/index.ts b/theme/src/node/autoFrontmatter/index.ts new file mode 100644 index 00000000..4b3b553d --- /dev/null +++ b/theme/src/node/autoFrontmatter/index.ts @@ -0,0 +1,3 @@ +export * from './generator.js' +export * from './readFile.js' +export * from './resolveOptions.js' diff --git a/theme/src/node/autoFrontmatter/readFile.ts b/theme/src/node/autoFrontmatter/readFile.ts new file mode 100644 index 00000000..7bd38710 --- /dev/null +++ b/theme/src/node/autoFrontmatter/readFile.ts @@ -0,0 +1,38 @@ +import { fs, path } from 'vuepress/utils' +import fg from 'fast-glob' +import type { AutoFrontmatterMarkdownFile } from '../../shared/auto-frontmatter.js' + +export async function readMarkdownList( + sourceDir: string, + filter: (id: string) => boolean, +): Promise { + const files: string[] = await fg(['**/*.md'], { + cwd: sourceDir, + ignore: ['node_modules', '.vuepress'], + }) + + return await Promise.all( + files + .filter(filter) + .map(file => readMarkdown(sourceDir, file)), + ) +} + +export async function readMarkdown( + sourceDir: string, + relativePath: string, +): Promise { + const filepath = path.join(sourceDir, relativePath) + const stats = await fs.promises.stat(filepath) + return { + filepath, + relativePath, + content: await fs.promises.readFile(filepath, 'utf-8'), + createTime: getFileCreateTime(stats), + stats, + } +} + +export function getFileCreateTime(stats: fs.Stats): Date { + return stats.birthtime.getFullYear() !== 1970 ? stats.birthtime : stats.atime +} diff --git a/theme/src/node/plugins/resolveAutoFrontmatterOptions.ts b/theme/src/node/autoFrontmatter/resolveOptions.ts similarity index 87% rename from theme/src/node/plugins/resolveAutoFrontmatterOptions.ts rename to theme/src/node/autoFrontmatter/resolveOptions.ts index 357d32b8..8f9b8a29 100644 --- a/theme/src/node/plugins/resolveAutoFrontmatterOptions.ts +++ b/theme/src/node/autoFrontmatter/resolveOptions.ts @@ -1,17 +1,15 @@ import { path } from 'vuepress/utils' import { removeLeadingSlash, resolveLocalePath } from 'vuepress/shared' import { ensureLeadingSlash } from '@vuepress/helper' -import type { - AutoFrontmatterOptions, - FrontmatterArray, - FrontmatterObject, -} from '@vuepress-plume/plugin-auto-frontmatter' import { format } from 'date-fns' import { uniq } from '@pengzhanbo/utils' -import type { NotesSidebar } from '@vuepress-plume/plugin-notes-data' import type { + AutoFrontmatter, + AutoFrontmatterArray, + AutoFrontmatterObject, PlumeThemeLocaleOptions, PlumeThemePluginOptions, + SidebarItem, } from '../../shared/index.js' import { getCurrentDirname, @@ -20,16 +18,15 @@ import { normalizePath, pathJoin, withBase, -} from '../utils.js' +} from '../utils/index.js' import { resolveNotesOptions } from '../config/index.js' -export function resolveAutoFrontmatterOptions( - pluginOptions: PlumeThemePluginOptions, +export function resolveOptions( localeOptions: PlumeThemeLocaleOptions, -): AutoFrontmatterOptions { + frontmatter: AutoFrontmatter, +): AutoFrontmatter { const pkg = getPackage() const { locales = {}, article: articlePrefix = '/article/' } = localeOptions - const { frontmatter } = pluginOptions const resolveLocale = (relativeFilepath: string) => { const file = ensureLeadingSlash(relativeFilepath) @@ -50,7 +47,7 @@ export function resolveAutoFrontmatterOptions( }) .filter(Boolean) - const baseFrontmatter: FrontmatterObject = { + const baseFrontmatter: AutoFrontmatterObject = { author(author: string, { relativePath }, data: any) { if (author) return author @@ -197,26 +194,33 @@ export function resolveAutoFrontmatterOptions( }, }, }, - ].filter(Boolean) as FrontmatterArray, + ].filter(Boolean) as AutoFrontmatterArray, } } function resolveLinkBySidebar( - sidebar: NotesSidebar, - prefix: string, + sidebar: 'auto' | (string | SidebarItem)[], + _prefix: string, ) { const res: Record = {} + if (sidebar === 'auto') { + return res + } + for (const item of sidebar) { if (typeof item !== 'string') { - const { dir = '', link = '/', items, text = '' } = item - SidebarLink(items, link, text, pathJoin(prefix, dir), res) + const { prefix, dir = '', link = '/', items, text = '' } = item + getSidebarLink(items, link, text, pathJoin(_prefix, prefix || dir), res) } } return res } -function SidebarLink(items: NotesSidebar | undefined, link: string, text: string, dir = '', res: Record = {}) { +function getSidebarLink(items: 'auto' | (string | SidebarItem)[] | undefined, link: string, text: string, dir = '', res: Record = {}) { + if (items === 'auto') + return + if (!items) { res[pathJoin(dir, `${text}.md`)] = link return @@ -237,8 +241,8 @@ function SidebarLink(items: NotesSidebar | undefined, link: string, text: string res[dir] = link } else { - const { dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item - SidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(dir, subDir), res) + const { prefix, dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item + getSidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(prefix || dir, subDir), res) } } }