diff --git a/theme/src/client/composables/blog-data.ts b/theme/src/client/composables/blog-data.ts new file mode 100644 index 00000000..c300d012 --- /dev/null +++ b/theme/src/client/composables/blog-data.ts @@ -0,0 +1,20 @@ +import { + blogPostData as blogPostDataRaw, +} from '@internal/blogData' +import { ref } from 'vue' +import type { Ref } from 'vue' +import type { PlumeThemeBlogPostData } from '../../shared/index.js' + +export type BlogDataRef = Ref + +export const blogPostData: BlogDataRef = ref(blogPostDataRaw) + +export function useBlogPostData(): BlogDataRef { + return blogPostData as BlogDataRef +} + +if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) { + __VUE_HMR_RUNTIME__.updateBlogPostData = (data: PlumeThemeBlogPostData) => { + blogPostData.value = data + } +} diff --git a/theme/src/client/composables/encrypt-data.ts b/theme/src/client/composables/encrypt-data.ts new file mode 100644 index 00000000..cb6dc272 --- /dev/null +++ b/theme/src/client/composables/encrypt-data.ts @@ -0,0 +1,55 @@ +import { + encrypt as rawEncrypt, +} from '@internal/encrypt' +import { ref } from 'vue' +import type { Ref } from 'vue' + +export type EncryptConfig = readonly [ + boolean, // global + string, // separator + string, // admin + string[], // keys + Record, // rules +] + +export interface EncryptData { + global: boolean + separator: string + admins: string[] + matches: string[] + ruleList: { + key: string + match: string + rules: string[] + }[] +} + +export type EncryptRef = Ref + +export const encrypt: EncryptRef = ref(resolveEncryptData(rawEncrypt)) + +export function useEncryptData(): EncryptRef { + return encrypt as EncryptRef +} + +function resolveEncryptData( + [global, separator, admin, matches, rules]: EncryptConfig, +): EncryptData { + return { + global, + separator, + matches, + admins: admin.split(separator), + ruleList: Object.keys(rules).map(key => ({ + key, + match: matches[key] as string, + rules: rules[key].split(separator), + })), + } +} + +if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) { + __VUE_HMR_RUNTIME__.updateEncrypt = (data: EncryptConfig) => { + encrypt.value = resolveEncryptData(data) + } +} diff --git a/theme/src/node/config/index.ts b/theme/src/node/config/index.ts index 4b3f606f..df0a2cb3 100644 --- a/theme/src/node/config/index.ts +++ b/theme/src/node/config/index.ts @@ -9,5 +9,4 @@ export * from './templateBuildRenderer.js' export * from './resolveSearchOptions.js' export * from './resolvePageHead.js' -export * from './resolveEncrypt.js' export * from './resolveNotesOptions.js' diff --git a/theme/src/node/config/resolveAlias.ts b/theme/src/node/config/resolveAlias.ts index e4b66179..14891fc1 100644 --- a/theme/src/node/config/resolveAlias.ts +++ b/theme/src/node/config/resolveAlias.ts @@ -1,5 +1,5 @@ import { fs, path } from 'vuepress/utils' -import { resolve } from '../utils.js' +import { resolve } from '../utils/index.js' export function resolveAlias() { return { diff --git a/theme/src/node/config/resolveEncrypt.ts b/theme/src/node/config/resolveEncrypt.ts deleted file mode 100644 index d2af5472..00000000 --- a/theme/src/node/config/resolveEncrypt.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { genSaltSync, hashSync } from 'bcrypt-ts' -import { isNumber, isString, random, toArray } from '@pengzhanbo/utils' -import type { Page } from 'vuepress/core' -import type { PlumeThemeEncrypt, PlumeThemePageData } from '../../shared/index.js' - -const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value) -const separator = ':' - -export function resolveEncrypt(encrypt?: PlumeThemeEncrypt) { - const salt = () => genSaltSync(random(8, 16)) - - const admin = encrypt?.admin - ? toArray(encrypt.admin) - .filter(isStringLike) - .map(item => hashSync(String(item), salt())) - .join(separator) - : '' - - const rules: Record = {} - const keys = Object.keys(encrypt?.rules ?? {}) - - if (encrypt?.rules) { - Object.keys(encrypt.rules).forEach((key) => { - const index = keys.indexOf(key) - - rules[String(index)] = toArray(encrypt.rules![key]) - .filter(isStringLike) - .map(item => hashSync(String(item), salt())) - .join(separator) - }) - } - - return { - __PLUME_ENCRYPT_GLOBAL__: encrypt?.global ?? false, - __PLUME_ENCRYPT_SEPARATOR__: separator, - __PLUME_ENCRYPT_ADMIN__: admin, - __PLUME_ENCRYPT_KEYS__: keys, - __PLUME_ENCRYPT_RULES__: rules, - } -} - -export function isEncryptPage(page: Page, encrypt?: PlumeThemeEncrypt) { - if (!encrypt) - return false - - const rules = encrypt.rules ?? {} - - return Object.keys(rules).some((match) => { - const relativePath = page.data.filePathRelative || '' - if (match[0] === '^') { - const regex = new RegExp(match) - return regex.test(page.path) || (relativePath && regex.test(relativePath)) - } - if (match.endsWith('.md')) - return relativePath && relativePath.endsWith(match) - - return page.path.startsWith(match) || relativePath.startsWith(match) - }) -} diff --git a/theme/src/node/config/resolveLocaleOptions.ts b/theme/src/node/config/resolveLocaleOptions.ts index efd1e05a..97764b40 100644 --- a/theme/src/node/config/resolveLocaleOptions.ts +++ b/theme/src/node/config/resolveLocaleOptions.ts @@ -2,7 +2,7 @@ import { entries, fromEntries, getLocaleConfig } from '@vuepress/helper' import type { App } from 'vuepress' import { LOCALE_OPTIONS } from '../locales/index.js' import type { PlumeThemeLocaleData, PlumeThemeLocaleOptions } from '../../shared/index.js' -import { THEME_NAME } from '../utils.js' +import { THEME_NAME } from '../utils/index.js' const FALLBACK_OPTIONS: PlumeThemeLocaleData = { appearance: true, @@ -18,6 +18,10 @@ const FALLBACK_OPTIONS: PlumeThemeLocaleData = { editLink: true, contributors: true, + footer: { + message: + 'Power by VuePress & vuepress-theme-plume', + }, } export function resolveLocaleOptions(app: App, { locales, ...options }: PlumeThemeLocaleOptions): PlumeThemeLocaleOptions { diff --git a/theme/src/node/config/resolveNotesOptions.ts b/theme/src/node/config/resolveNotesOptions.ts index 24ed0e22..4ffac133 100644 --- a/theme/src/node/config/resolveNotesOptions.ts +++ b/theme/src/node/config/resolveNotesOptions.ts @@ -1,8 +1,7 @@ -import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data' import { entries } from '@vuepress/helper' import { uniq } from '@pengzhanbo/utils' -import type { PlumeThemeLocaleOptions } from '../..//shared/index.js' -import { withBase } from '../utils.js' +import type { NotesOptions, PlumeThemeLocaleOptions } from '../../shared/index.js' +import { withBase } from '../utils/index.js' export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) { const locales = localeOptions.locales || {} @@ -22,9 +21,9 @@ export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) { return uniq(notesLinks) } -export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesDataOptions[] { +export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesOptions[] { const locales = localeOptions.locales || {} - const notesOptionsList: NotesDataOptions[] = [] + const notesOptionsList: NotesOptions[] = [] for (const [locale, opt] of entries(locales)) { const options = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes if (options) { diff --git a/theme/src/node/config/resolveProvideData.ts b/theme/src/node/config/resolveProvideData.ts index a0bb6b47..b521f836 100644 --- a/theme/src/node/config/resolveProvideData.ts +++ b/theme/src/node/config/resolveProvideData.ts @@ -1,20 +1,15 @@ import type { App } from 'vuepress' import { entries, fromEntries, getRootLangPath, isPlainObject } from '@vuepress/helper' -import type { PlumeThemeEncrypt, PlumeThemePluginOptions } from '../../shared/index.js' +import type { PlumeThemePluginOptions } from '../../shared/index.js' import { PRESET_LOCALES } from '../locales/index.js' -import { resolveEncrypt } from './resolveEncrypt.js' export function resolveProvideData( app: App, plugins: PlumeThemePluginOptions, - encrypt?: PlumeThemeEncrypt, - ): Record { const root = getRootLangPath(app) return { - // 注入 加密配置 - ...resolveEncrypt(encrypt), // 注入水印配置 __PLUME_WM_FP__: isPlainObject(plugins.watermark) ? plugins.watermark.fullPage !== false diff --git a/theme/src/node/config/resolveThemeData.ts b/theme/src/node/config/resolveThemeData.ts index d61cb7ad..7234a64d 100644 --- a/theme/src/node/config/resolveThemeData.ts +++ b/theme/src/node/config/resolveThemeData.ts @@ -2,9 +2,9 @@ import { entries, getRootLangPath } from '@vuepress/helper' import type { App } from 'vuepress' import type { NavItem, PlumeThemeLocaleOptions } from '../../shared/index.js' import { PRESET_LOCALES } from '../locales/index.js' -import { withBase } from '../utils.js' +import { withBase } from '../utils/index.js' -const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'article', 'avatar'] +const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'sidebar', 'article', 'avatar'] // 过滤不需要出现在多语言配置中的字段 const EXCLUDE_LOCALE_LIST = [...EXCLUDE_LIST, 'blog', 'appearance'] diff --git a/theme/src/node/config/resolveThemeOption.ts b/theme/src/node/config/resolveThemeOption.ts index 39157647..54648591 100644 --- a/theme/src/node/config/resolveThemeOption.ts +++ b/theme/src/node/config/resolveThemeOption.ts @@ -1,7 +1,13 @@ import type { PlumeThemeOptions } from '../../shared/index.js' -import { logger } from '../utils.js' +import { logger } from '../utils/index.js' -export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname, ...localeOptions }: PlumeThemeOptions) { +export function resolveThemeOptions({ + themePlugins, + plugins, + hostname, + configFile, + ...localeOptions +}: PlumeThemeOptions) { const pluginOptions = plugins ?? themePlugins ?? {} if (themePlugins) { @@ -11,8 +17,8 @@ export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname, } return { + configFile, pluginOptions, - encrypt, hostname, localeOptions, } diff --git a/theme/src/node/config/templateBuildRenderer.ts b/theme/src/node/config/templateBuildRenderer.ts index a512f14a..ff39797f 100644 --- a/theme/src/node/config/templateBuildRenderer.ts +++ b/theme/src/node/config/templateBuildRenderer.ts @@ -1,5 +1,5 @@ import { type TemplateRendererContext, templateRenderer } from 'vuepress/utils' -import { getThemePackage } from '../utils.js' +import { getThemePackage } from '../utils/index.js' export function templateBuildRenderer(template: string, context: TemplateRendererContext) { const pkg = getThemePackage() diff --git a/theme/src/node/plugins/getPlugins.ts b/theme/src/node/plugins/getPlugins.ts index c5cb7ad8..5edc6cf9 100644 --- a/theme/src/node/plugins/getPlugins.ts +++ b/theme/src/node/plugins/getPlugins.ts @@ -4,12 +4,8 @@ import { docsearchPlugin } from '@vuepress/plugin-docsearch' import { gitPlugin } from '@vuepress/plugin-git' import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' import { nprogressPlugin } from '@vuepress/plugin-nprogress' -import { themeDataPlugin } from '@vuepress/plugin-theme-data' -import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter' import { baiduTongjiPlugin } from '@vuepress-plume/plugin-baidu-tongji' -import { blogDataPlugin } from '@vuepress-plume/plugin-blog-data' import { iconifyPlugin } from '@vuepress-plume/plugin-iconify' -import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data' import { shikiPlugin } from '@vuepress-plume/plugin-shikiji' import { commentPlugin } from '@vuepress/plugin-comment' import { type MarkdownEnhancePluginOptions, mdEnhancePlugin } from 'vuepress-plugin-md-enhance' @@ -21,54 +17,31 @@ import { searchPlugin } from '@vuepress-plume/plugin-search' import { markdownPowerPlugin } from 'vuepress-plugin-md-power' import { watermarkPlugin } from '@vuepress/plugin-watermark' import { fontsPlugin } from '@vuepress-plume/plugin-fonts' -import type { - PlumeThemeEncrypt, - PlumeThemeLocaleOptions, - PlumeThemePluginOptions, -} from '../../shared/index.js' +import type { PlumeThemeLocaleOptions, PlumeThemePluginOptions } from '../../shared/index.js' import { resolveDocsearchOptions, - resolveNotesOptions, resolveSearchOptions, - resolveThemeData, } from '../config/index.js' -import { resolveAutoFrontmatterOptions } from './resolveAutoFrontmatterOptions.js' -import { resolveBlogDataOptions } from './resolveBlogDataOptions.js' import { customContainerPlugins } from './containerPlugins.js' export interface SetupPluginOptions { app: App pluginOptions: PlumeThemePluginOptions - localeOptions: PlumeThemeLocaleOptions - encrypt?: PlumeThemeEncrypt hostname?: string } export function getPlugins({ app, pluginOptions, - localeOptions, - encrypt, hostname, }: SetupPluginOptions): PluginConfig { const isProd = !app.env.isDev const plugins: PluginConfig = [ - themeDataPlugin({ themeData: resolveThemeData(app, localeOptions) }), - - autoFrontmatterPlugin(resolveAutoFrontmatterOptions(pluginOptions, localeOptions)), - - blogDataPlugin(resolveBlogDataOptions(localeOptions, encrypt)), - - notesDataPlugin(resolveNotesOptions(localeOptions)), - iconifyPlugin(), - fontsPlugin(), - contentUpdatePlugin(), - activeHeaderLinksPlugin({ headerLinkSelector: 'a.outline-link', headerAnchorSelector: '.header-anchor', @@ -185,10 +158,7 @@ export function getPlugins({ } if (pluginOptions.seo !== false && hostname && isProd) { - plugins.push(seoPlugin({ - hostname, - author: localeOptions.locales?.['/'].profile?.name || localeOptions.profile?.name || localeOptions.locales?.['/'].avatar?.name || localeOptions.avatar?.name, - })) + plugins.push(seoPlugin({ hostname })) } return plugins diff --git a/theme/src/node/plugins/resolveBlogDataOptions.ts b/theme/src/node/plugins/resolveBlogDataOptions.ts deleted file mode 100644 index 3c9abd7a..00000000 --- a/theme/src/node/plugins/resolveBlogDataOptions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { BlogDataPluginOptions } from '@vuepress-plume/plugin-blog-data' -import { removeLeadingSlash } from '@vuepress/helper' -import { - isEncryptPage, - resolveNotesOptions, -} from '../config/index.js' -import { normalizePath } from '../utils.js' -import type { PlumeThemeEncrypt, PlumeThemeLocaleOptions } from '../..//shared/index.js' - -export function resolveBlogDataOptions( - localeOptions: PlumeThemeLocaleOptions, - encrypt?: PlumeThemeEncrypt, -): BlogDataPluginOptions { - const blog = localeOptions.blog || {} - const notesList = resolveNotesOptions(localeOptions) - const notesDirList = notesList - .map(notes => removeLeadingSlash(normalizePath(`${notes.dir}/**`))) - .filter(Boolean) - - return { - include: blog?.include ?? ['**/*.md'], - exclude: [ - '**/{README,readme,index}.md', - '.vuepress/', - 'node_modules/', - ...(blog.exclude ?? []), - ...notesDirList, - ].filter(Boolean), - sortBy: 'createTime', - excerpt: true, - pageFilter: (page: any) => page.frontmatter.draft !== true, - extendBlogData: (page: any) => { - const tags = page.frontmatter.tags - const data: Record = { - categoryList: page.data.categoryList, - tags, - sticky: page.frontmatter.sticky, - createTime: page.data.frontmatter.createTime, - lang: page.lang, - } - isEncryptPage(page, encrypt) && (data.encrypt = true) - return data - }, - } -} diff --git a/theme/src/node/utils.ts b/theme/src/node/utils.ts deleted file mode 100644 index ddcd341f..00000000 --- a/theme/src/node/utils.ts +++ /dev/null @@ -1,62 +0,0 @@ -import process from 'node:process' -import { createHash } from 'node:crypto' -import { customAlphabet } from 'nanoid' -import { fs, getDirname, path } from 'vuepress/utils' -import { Logger, ensureEndingSlash, ensureLeadingSlash } from '@vuepress/helper' - -export const THEME_NAME = 'vuepress-theme-plume' - -const __dirname = getDirname(import.meta.url) - -export const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args) -export const templates = (url: string) => resolve('../templates', url) - -export const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8) - -export const hash = (content: string) => createHash('md5').update(content).digest('hex') - -export const logger = new Logger(THEME_NAME) - -export function getPackage() { - let pkg = {} as any - try { - const content = fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8') - pkg = JSON.parse(content) - } - catch { } - return pkg -} - -export function getThemePackage() { - let pkg = {} as any - try { - const content = fs.readFileSync(resolve('../package.json'), 'utf-8') - pkg = JSON.parse(content) - } - catch {} - return pkg -} - -const RE_SLASH = /(\\|\/)+/g -export function normalizePath(path: string) { - return path.replace(RE_SLASH, '/') -} - -export function pathJoin(...args: string[]) { - return normalizePath(path.join(...args)) -} - -const RE_START_END_SLASH = /^\/|\/$/g -export function getCurrentDirname(basePath: string | undefined, filepath: string) { - const dirList = normalizePath(basePath || path.dirname(filepath)) - .replace(RE_START_END_SLASH, '') - .split('/') - return dirList.length > 0 ? dirList[dirList.length - 1] : '' -} - -export function withBase(path = '', base = '/'): string { - path = ensureEndingSlash(ensureLeadingSlash(path)) - if (path.startsWith(base)) - return normalizePath(path) - return normalizePath(`${base}${path}`) -}