chore: 调整优化结构

This commit is contained in:
pengzhanbo 2024-05-26 13:11:21 +08:00
parent e358224217
commit 48c6113f77
26 changed files with 580 additions and 498 deletions

View File

@ -0,0 +1,6 @@
---
title: 测试文件
author: Plume Theme
createTime: 2024/05/26 02:11:27
permalink: /plugins/9kekfblc/
---

View File

@ -1,7 +1,7 @@
export interface NotesDataOptions {
/**
*
* @default '/notes'
* @default '/notes/'
*/
dir: string
/**

View File

@ -1,39 +1,45 @@
import { inject, onMounted, ref } from 'vue'
import { useDark } from '@vueuse/core'
import { inject, ref } from 'vue'
import type { App, InjectionKey, Ref } from 'vue'
import { useThemeData } from './themeData.js'
export type DarkModeRef = Ref<boolean>
type DarkModeRef = Ref<boolean>
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
__VUEPRESS_DEV__ ? 'darkMode' : '',
)
/**
* Inject dark mode global computed
*/
export function useDarkMode(): DarkModeRef {
const isDark = inject(darkModeSymbol)
if (isDark === undefined)
throw new Error('useDarkMode() is called without provider.')
export function setupDarkMode(app: App): void {
const themeLocale = useThemeData()
return isDark
}
const appearance = themeLocale.value.appearance
const isDark
= appearance === 'force-dark'
? ref(true)
: appearance
? useDark({
storageKey: 'vuepress-theme-appearance',
disableTransition: false,
initialValue: () =>
typeof appearance === 'string' ? appearance : 'auto',
...(typeof appearance === 'object' ? appearance : {}),
})
: ref(false)
/**
* Create dark mode ref and provide as global computed in setup
*/
export function setupDarkMode(): void {
const isDark = useDarkMode()
onMounted(() => {
if (document.documentElement.classList.contains('dark'))
isDark.value = true
})
}
export function injectDarkMode(app: App): void {
const isDark = ref<boolean>(false)
app.provide(darkModeSymbol, isDark)
Object.defineProperty(app.config.globalProperties, '$isDark', {
get: () => isDark,
})
}
/**
* Inject dark mode global computed
*/
export function useDarkMode(): DarkModeRef {
const isDarkMode = inject(darkModeSymbol)
if (!isDarkMode)
throw new Error('useDarkMode() is called without provider.')
return isDarkMode
}

View File

@ -0,0 +1,39 @@
import type { Ref } from 'vue'
import type { ThemeLocaleDataRef } from '@vuepress/plugin-theme-data/client'
import {
usePageData,
usePageFrontmatter,
useSiteLocaleData,
} from 'vuepress/client'
import type {
PageDataRef,
PageFrontmatterRef,
SiteLocaleDataRef,
} from 'vuepress/client'
import type {
PlumeThemeLocaleData,
PlumeThemePageData,
PlumeThemePageFrontmatter,
} from '../../shared/index.js'
import { useThemeLocaleData } from './themeData.js'
import { hashRef } from './hash.js'
import { useDarkMode } from './darkMode.js'
export interface Data {
theme: ThemeLocaleDataRef<PlumeThemeLocaleData>
page: PageDataRef<PlumeThemePageData>
frontmatter: PageFrontmatterRef<PlumeThemePageFrontmatter>
hash: Ref<string>
site: SiteLocaleDataRef
isDark: Ref<boolean>
}
export function useData(): Data {
const theme = useThemeLocaleData()
const page = usePageData<PlumeThemePageData>()
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
const site = useSiteLocaleData()
const isDark = useDarkMode()
return { theme, page, frontmatter, hash: hashRef, site, isDark }
}

View File

@ -1,8 +1,8 @@
import { compareSync, genSaltSync } from 'bcrypt-ts/browser'
import { type Ref, computed } from 'vue'
import { hasOwn, useSessionStorage } from '@vueuse/core'
import { usePageData, useRoute } from 'vuepress/client'
import type { PlumeThemePageData } from '../../shared/index.js'
import { useRoute } from 'vuepress/client'
import { useData } from './data.js'
declare const __PLUME_ENCRYPT_GLOBAL__: boolean
declare const __PLUME_ENCRYPT_SEPARATOR__: string
@ -88,7 +88,7 @@ export function useGlobalEncrypt(): {
}
export function usePageEncrypt() {
const page = usePageData<PlumeThemePageData>()
const { page } = useData()
const route = useRoute()
const hasPageEncrypt = computed(() => ruleList.length ? matches.some(toMatch) : false)

View File

@ -9,3 +9,4 @@ export * from './blog.js'
export * from './locale.js'
export * from './useRouteQuery.js'
export * from './watermark.js'
export * from './data.js'

View File

@ -10,7 +10,6 @@ import type { ComputedRef, Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
import type { PlumeThemePageData } from '../../shared/index.js'
import { isActive } from '../utils/index.js'
import { useThemeLocaleData } from './themeData.js'
import { hashRef } from './hash.js'
export { useNotesData }
@ -58,7 +57,6 @@ export function getSidebarFirstLink(sidebar: NotesSidebarItem[]) {
export function useSidebar() {
const route = useRoute()
const notesData = useNotesData()
const theme = useThemeLocaleData()
const frontmatter = usePageFrontmatter()
const page = usePageData<PlumeThemePageData>()
@ -74,7 +72,7 @@ export function useSidebar() {
})
const sidebar = computed(() => {
return theme.value.notes ? getSidebarList(route.path, notesData.value) : []
return getSidebarList(route.path, notesData.value)
})
const hasSidebar = computed(() => {
return (

View File

@ -4,16 +4,14 @@ import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { h } from 'vue'
import Badge from './components/global/Badge.vue'
import ExternalLinkIcon from './components/global/ExternalLinkIcon.vue'
import { injectDarkMode, setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
import { setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
import HomeBox from './components/Home/HomeBox.vue'
export default defineClientConfig({
enhance({ app, router }) {
injectDarkMode(app)
setupDarkMode(app)
// global component
app.component('Badge', Badge)
@ -52,7 +50,6 @@ export default defineClientConfig({
}
},
setup() {
setupDarkMode()
setupWatermark()
},
layouts: {

View File

@ -1,190 +0,0 @@
import { path } from 'vuepress/utils'
import type { App } from 'vuepress/core'
import { resolveLocalePath } from 'vuepress/shared'
import type {
AutoFrontmatterOptions,
FrontmatterArray,
FrontmatterObject,
} from '@vuepress-plume/plugin-auto-frontmatter'
import { format } from 'date-fns'
import { uniq } from '@pengzhanbo/utils'
import type {
PlumeThemeLocaleOptions,
PlumeThemePluginOptions,
} from '../shared/index.js'
import { getCurrentDirname, getPackage, nanoid, pathJoin } from './utils.js'
import { resolveLinkBySidebar, resolveNotesList } from './resolveNotesList.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
export default function autoFrontmatter(
app: App,
options: PlumeThemePluginOptions,
localeOptions: PlumeThemeLocaleOptions,
): AutoFrontmatterOptions {
const sourceDir = app.dir.source()
const pkg = getPackage()
const { locales = {}, article: articlePrefix = '/article/' } = localeOptions
const { frontmatter } = options
const avatar = resolveLocaleOptions(localeOptions, 'avatar')
const notesList = resolveNotesList(localeOptions)
const localesNotesDirs = notesList
.map(({ notes, dir }) => {
const _dir = dir?.replace(/^\//, '')
return notes.map(note => pathJoin(_dir, note.dir || ''))
})
.flat()
.filter(Boolean)
const baseFrontmatter: FrontmatterObject = {
author(author: string, _, data: any) {
if (author)
return author
if (data.friends)
return
return avatar?.name || pkg.author || ''
},
createTime(formatTime: string, { createTime }, data: any) {
if (formatTime)
return formatTime
if (data.friends)
return
return format(new Date(createTime), 'yyyy/MM/dd HH:mm:ss')
},
}
const resolveLocale = (filepath: string) => {
const file = pathJoin('/', path.relative(sourceDir, filepath))
return resolveLocalePath(localeOptions.locales!, file)
}
const notesByLocale = (locale: string) => {
const notes = resolveLocaleOptions(localeOptions, 'notes', locale)
if (notes === false)
return undefined
return notes
}
const findNote = (filepath: string) => {
const file = pathJoin('/', path.relative(sourceDir, filepath))
const locale = resolveLocalePath(locales, file)
const notes = notesByLocale(locale)
if (!notes)
return undefined
const notesList = notes?.notes || []
const notesDir = notes?.dir || ''
return notesList.find(note =>
file.startsWith(path.join(locale, notesDir, note.dir)),
)
}
return {
include: frontmatter?.include ?? ['**/*.md'],
exclude: uniq(['.vuepress/**/*', 'node_modules', ...(frontmatter?.exclude ?? [])]),
frontmatter: [
localesNotesDirs.length
? {
// note 首页链接
include: localesNotesDirs.map(dir => pathJoin(dir, '/{readme,README,index}.md')),
frontmatter: {
title(title: string, { filepath }) {
if (title)
return title
const note = findNote(filepath)
if (note?.text)
return note.text
return getCurrentDirname(note?.dir, filepath) || ''
},
...baseFrontmatter,
permalink(permalink: string, { filepath }, data: any) {
if (permalink)
return permalink
if (data.friends)
return
const locale = resolveLocale(filepath)
const notes = notesByLocale(locale)
const note = findNote(filepath)
return pathJoin(
locale,
notes?.link || '',
note?.link || getCurrentDirname(note?.dir, filepath),
'/',
)
},
},
}
: '',
localesNotesDirs.length
? {
include: localesNotesDirs.map(dir => pathJoin(dir, '**/**.md')),
frontmatter: {
title(title: string, { filepath }) {
if (title)
return title
const note = findNote(filepath)
let basename = path.basename(filepath, '.md')
if (note?.sidebar === 'auto')
basename = basename.replace(/^\d+\./, '')
return basename
},
...baseFrontmatter,
permalink(permalink: string, { filepath }, data: any) {
if (permalink)
return permalink
if (data.friends)
return
const locale = resolveLocale(filepath)
const notes = notesByLocale(locale)
const note = findNote(filepath)
const args: string[] = [
locale,
notes?.link || '',
note?.link || getCurrentDirname(note?.dir, filepath),
]
const sidebar = note?.sidebar
if (sidebar && sidebar !== 'auto') {
const res = resolveLinkBySidebar(sidebar, pathJoin(notes?.dir || '', note?.dir || ''))
const file = pathJoin('/', path.relative(sourceDir, filepath))
res[file] && args.push(res[file])
}
return pathJoin(...args, nanoid(), '/')
},
},
}
: '',
{
include: '**/{readme,README,index}.md',
frontmatter: {},
},
{
include: '*',
frontmatter: {
title(title: string, { filepath }) {
if (title)
return title
const basename = path.basename(filepath, '.md')
return basename
},
...baseFrontmatter,
permalink(permalink: string, { filepath }) {
if (permalink)
return permalink
const locale = resolveLocale(filepath)
const prefix = resolveLocaleOptions(localeOptions, 'article', locale, false)
const args: string[] = []
prefix
? args.push(prefix)
: args.push(locale, articlePrefix)
return pathJoin(...args, nanoid(), '/')
},
},
},
].filter(Boolean) as FrontmatterArray,
}
}

View File

@ -3,3 +3,4 @@ export * from './resolveThemeData.js'
export * from './resolveSearchOptions.js'
export * from './resolvePageHead.js'
export * from './resolveEncrypt.js'
export * from './resolveNotesOptions.js'

View File

@ -2,12 +2,14 @@ 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'
const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
appearance: true,
blog: { link: '/blog/', pagination: { perPage: 20 }, tags: true, archives: true, tagsLink: '/blog/tags/', archivesLink: '/blog/archives/' },
article: '/article/',
notes: { link: '/', dir: 'notes', notes: [] },
notes: { link: '/', dir: '/notes/', notes: [] },
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],
// page meta
@ -22,7 +24,7 @@ export function resolveLocaleOptions(app: App, { locales, ...options }: PlumeThe
...options,
locales: getLocaleConfig({
app,
name: 'vuepress-theme-plume',
name: THEME_NAME,
default: LOCALE_OPTIONS,
config: fromEntries(
entries<PlumeThemeLocaleOptions>({

View File

@ -0,0 +1,37 @@
import type { NotesDataOptions, NotesSidebar } 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'
export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) {
const locales = localeOptions.locales || {}
const notesLinks: string[] = []
for (const [locale, opt] of entries(locales)) {
const config = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes
if (config && config.notes?.length) {
const prefix = config.link || ''
notesLinks.push(
...config.notes.map(
note => withBase(`${prefix}/${note.link || ''}`, locale),
),
)
}
}
return uniq(notesLinks)
}
export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesDataOptions[] {
const locales = localeOptions.locales || {}
const notesOptionsList: NotesDataOptions[] = []
for (const [locale, opt] of entries(locales)) {
const options = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes
if (options) {
options.dir = withBase(options.dir, locale)
notesOptionsList.push(options)
}
}
return notesOptionsList
}

View File

@ -1,10 +1,12 @@
import { ensureEndingSlash, ensureLeadingSlash, entries, getRootLangPath } from '@vuepress/helper'
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 { normalizePath } from '../utils.js'
import { withBase } from '../utils.js'
const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'article']
// 过滤不需要出现在多语言配置中的字段
const EXCLUDE_LOCALE_LIST = [...EXCLUDE_LIST, 'blog', 'appearance']
export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): PlumeThemeLocaleOptions {
const themeData: PlumeThemeLocaleOptions = { locales: {} }
@ -18,13 +20,15 @@ export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): Pl
entries(options.locales || {}).forEach(([locale, opt]) => {
themeData.locales![locale] = {}
entries(opt).forEach(([key, value]) => {
if (!EXCLUDE_LIST.includes(key))
if (!EXCLUDE_LOCALE_LIST.includes(key))
themeData.locales![locale][key] = value
})
})
const blog = options.blog || {}
const blogLink = blog.link || '/blog/'
entries(options.locales || {}).forEach(([locale, opt]) => {
// 注入预设 导航栏。
// 注入预设 导航栏
// home | blog | tags | archives
if (opt.navbar !== false && opt.navbar?.length === 0) {
// fallback navbar option
@ -33,20 +37,19 @@ export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): Pl
text: PRESET_LOCALES[localePath].home,
link: locale,
}]
if (opt.blog) {
navbar.push({
text: PRESET_LOCALES[localePath].blog,
link: withBase(opt.blog.link ?? '/blog/', locale),
})
opt.blog.tags && navbar.push({
text: PRESET_LOCALES[locale].tag,
link: withBase('/tags/', locale),
})
opt.blog.archives && navbar.push({
text: PRESET_LOCALES[locale].archive,
link: withBase('/archives/', locale),
})
}
navbar.push({
text: PRESET_LOCALES[localePath].blog,
link: withBase(blogLink, locale),
})
blog.tags !== false && navbar.push({
text: PRESET_LOCALES[locale].tag,
link: withBase(blog.tagsLink || `${blogLink}/tags/`, locale),
})
blog.archives !== false && navbar.push({
text: PRESET_LOCALES[locale].archive,
link: withBase(blog.archivesLink || `${blogLink}/archives/`, locale),
})
themeData.locales![locale].navbar = navbar
}
else {
@ -56,10 +59,3 @@ export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): Pl
return themeData
}
function withBase(path: string, base = '/'): string {
path = ensureEndingSlash(ensureLeadingSlash(path))
if (path.startsWith(base))
return path
return normalizePath(`${base}${path}`)
}

View File

@ -37,7 +37,7 @@ export const zhLocale: PlumeThemeLocaleData = {
footer: {
message:
'<a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a> 提供支持',
'Power by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
}

View File

@ -1,7 +1,7 @@
import { markdownContainerPlugin as containerPlugin } from '@vuepress/plugin-markdown-container'
import type { Plugin } from 'vuepress/core'
export const customContainers: Plugin[] = [
export const customContainerPlugins: Plugin[] = [
/**
* :::demo-wrapper img no-padding title="xxx" height="100px"
* :::

View File

@ -24,80 +24,43 @@ import type {
PlumeThemeEncrypt,
PlumeThemeLocaleOptions,
PlumeThemePluginOptions,
} from '../shared/index.js'
import autoFrontmatter from './autoFrontmatter.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
import { pathJoin } from './utils.js'
import { resolveNotesList } from './resolveNotesList.js'
import { customContainers } from './container.js'
import { BLOG_TAGS_COLORS_PRESET, generateBlogTagsColors } from './blogTags.js'
import { isEncryptPage } from './config/resolveEncrypt.js'
import { resolveDocsearchOptions, resolveSearchOptions, resolveThemeData } from './config/index.js'
} 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
options: PlumeThemePluginOptions
pluginOptions: PlumeThemePluginOptions
localeOptions: PlumeThemeLocaleOptions
encrypt?: PlumeThemeEncrypt
hostname?: string
}
export function setupPlugins({
export function getPlugins({
app,
options,
pluginOptions,
localeOptions,
encrypt,
hostname,
}: SetupPluginOptions): PluginConfig {
const isProd = !app.env.isDev
const notesList = resolveNotesList(localeOptions)
const notesDirList = notesList
.map(notes => notes.dir && pathJoin(notes.dir, '**').replace(/^\//, ''))
.filter(Boolean)
const blog = resolveLocaleOptions(localeOptions, 'blog')
const plugins: PluginConfig = [
themeDataPlugin({ themeData: resolveThemeData(app, localeOptions) }),
autoFrontmatterPlugin(autoFrontmatter(app, options, localeOptions)),
autoFrontmatterPlugin(resolveAutoFrontmatterOptions(pluginOptions, localeOptions)),
blogDataPlugin({
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.article !== undefined
? !!page.frontmatter.article
: true,
extraBlogData(extra) {
extra.tagsColorsPreset = BLOG_TAGS_COLORS_PRESET
extra.tagsColors = {}
},
extendBlogData: (page: any, extra) => {
const tags = page.frontmatter.tags
generateBlogTagsColors(extra.tagsColors, tags)
const data: Record<string, any> = {
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
},
}),
blogDataPlugin(resolveBlogDataOptions(localeOptions, encrypt)),
notesDataPlugin(notesList),
notesDataPlugin(resolveNotesOptions(localeOptions)),
iconifyPlugin(),
@ -110,10 +73,10 @@ export function setupPlugins({
offset: 20,
}),
...customContainers,
...customContainerPlugins,
]
if (options.readingTime !== false) {
if (pluginOptions.readingTime !== false) {
plugins.push(readingTimePlugin({
locales: {
'/zh/': {
@ -122,22 +85,22 @@ export function setupPlugins({
time: '约$time分钟',
},
},
...options.readingTime,
...pluginOptions.readingTime,
}))
}
if (options.nprogress !== false)
if (pluginOptions.nprogress !== false)
plugins.push(nprogressPlugin())
if (options.git !== false) {
if (pluginOptions.git ?? isProd) {
plugins.push(gitPlugin({
createdTime: false,
updatedTime: resolveLocaleOptions(localeOptions, 'lastUpdated') !== false,
contributors: resolveLocaleOptions(localeOptions, 'contributors') !== false,
updatedTime: true,
contributors: true,
}))
}
if (options.mediumZoom !== false) {
if (pluginOptions.mediumZoom !== false) {
plugins.push(mediumZoomPlugin({
selector: '.plume-content > img, .plume-content :not(a) > img',
zoomOptions: { background: 'var(--vp-c-bg)' },
@ -145,18 +108,18 @@ export function setupPlugins({
}))
}
if (options.docsearch) {
if (options.docsearch.appId && options.docsearch.apiKey)
plugins.push(docsearchPlugin(resolveDocsearchOptions(app, options.docsearch)))
if (pluginOptions.docsearch) {
if (pluginOptions.docsearch.appId && pluginOptions.docsearch.apiKey)
plugins.push(docsearchPlugin(resolveDocsearchOptions(app, pluginOptions.docsearch)))
else
console.error('docsearch plugin: appId and apiKey are both required')
}
else if (options.search !== false) {
plugins.push(searchPlugin(resolveSearchOptions(app, options.search)))
else if (pluginOptions.search !== false) {
plugins.push(searchPlugin(resolveSearchOptions(app, pluginOptions.search)))
}
const shikiOption = options.shiki
const shikiOption = pluginOptions.shiki
let shikiTheme: any = { light: 'vitesse-light', dark: 'vitesse-dark' }
if (shikiOption !== false) {
shikiTheme = shikiOption?.theme ?? shikiTheme
@ -166,7 +129,7 @@ export function setupPlugins({
}))
}
if (options.markdownEnhance !== false) {
if (pluginOptions.markdownEnhance !== false) {
plugins.push(mdEnhancePlugin(
Object.assign(
{
@ -183,42 +146,42 @@ export function setupPlugins({
footnote: true,
katex: true,
} as MarkdownEnhancePluginOptions,
options.markdownEnhance || {},
pluginOptions.markdownEnhance || {},
),
))
}
if (options.markdownPower !== false) {
if (pluginOptions.markdownPower !== false) {
plugins.push(markdownPowerPlugin({
caniuse: options.caniuse,
...options.markdownPower || {},
repl: options.markdownPower?.repl
? { theme: shikiTheme, ...options.markdownPower?.repl }
: options.markdownPower?.repl,
caniuse: pluginOptions.caniuse,
...pluginOptions.markdownPower || {},
repl: pluginOptions.markdownPower?.repl
? { theme: shikiTheme, ...pluginOptions.markdownPower?.repl }
: pluginOptions.markdownPower?.repl,
}))
}
if (options.watermark) {
if (pluginOptions.watermark) {
plugins.push(watermarkPlugin({
delay: 300,
enabled: true,
...typeof options.watermark === 'object' ? options.watermark : {},
...typeof pluginOptions.watermark === 'object' ? pluginOptions.watermark : {},
}))
}
if (options.comment)
plugins.push(commentPlugin(options.comment))
if (pluginOptions.comment)
plugins.push(commentPlugin(pluginOptions.comment))
if (options.baiduTongji !== false && options.baiduTongji?.key && isProd)
plugins.push(baiduTongjiPlugin(options.baiduTongji))
if (pluginOptions.baiduTongji !== false && pluginOptions.baiduTongji?.key && isProd)
plugins.push(baiduTongjiPlugin(pluginOptions.baiduTongji))
if (options.sitemap !== false && hostname && isProd)
if (pluginOptions.sitemap !== false && hostname && isProd)
plugins.push(sitemapPlugin({ hostname }))
if (options.seo !== false && hostname && isProd) {
if (pluginOptions.seo !== false && hostname && isProd) {
plugins.push(seoPlugin({
hostname,
author: resolveLocaleOptions(localeOptions, 'avatar')?.name,
author: localeOptions.locales?.['/'].avatar?.name || localeOptions.avatar?.name,
}))
}

View File

@ -0,0 +1 @@
export * from './getPlugins.js'

View File

@ -0,0 +1,240 @@
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 {
PlumeThemeLocaleOptions,
PlumeThemePluginOptions,
} from '../../shared/index.js'
import {
getCurrentDirname,
getPackage,
nanoid,
normalizePath,
pathJoin,
withBase,
} from '../utils.js'
import { resolveNotesOptions } from '../config/index.js'
export function resolveAutoFrontmatterOptions(
pluginOptions: PlumeThemePluginOptions,
localeOptions: PlumeThemeLocaleOptions,
): AutoFrontmatterOptions {
const pkg = getPackage()
const { locales = {}, article: articlePrefix = '/article/' } = localeOptions
const { frontmatter } = pluginOptions
const resolveLocale = (relativeFilepath: string) => {
const file = ensureLeadingSlash(relativeFilepath)
return resolveLocalePath(localeOptions.locales!, file)
}
const resolveOptions = (relativeFilepath: string) => {
const locale = resolveLocale(relativeFilepath)
return locales[locale] || localeOptions
}
const notesList = resolveNotesOptions(localeOptions)
const localesNotesDirs = notesList
.flatMap(({ notes, dir }) => {
dir = removeLeadingSlash(dir || '')
return notes.map(note => normalizePath(`${dir}/${note.dir || ''}/`))
})
.filter(Boolean)
const baseFrontmatter: FrontmatterObject = {
author(author: string, { relativePath }, data: any) {
if (author)
return author
if (data.friends)
return
const avatar = resolveOptions(relativePath).avatar
return avatar?.name || pkg.author || ''
},
createTime(formatTime: string, { createTime }, data: any) {
if (formatTime)
return formatTime
if (data.friends)
return
return format(new Date(createTime), 'yyyy/MM/dd HH:mm:ss')
},
}
const notesByLocale = (locale: string) => {
const notes = localeOptions.locales?.[locale]?.notes
if (notes === false)
return undefined
return notes
}
const findNote = (relativeFilepath: string) => {
const locale = resolveLocale(relativeFilepath)
const filepath = ensureLeadingSlash(relativeFilepath)
const notes = notesByLocale(locale)
if (!notes)
return undefined
const notesList = notes?.notes || []
const notesDir = notes?.dir || ''
return notesList.find(note =>
filepath.startsWith(normalizePath(`${notesDir}/${note.dir}`)),
)
}
return {
include: frontmatter?.include ?? ['**/*.md'],
exclude: uniq(['.vuepress/**/*', 'node_modules', ...(frontmatter?.exclude ?? [])]),
frontmatter: [
localesNotesDirs.length
? {
// 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(note?.dir, relativePath) || ''
},
...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)
return pathJoin(
locale,
notes?.link || '',
note?.link || getCurrentDirname(note?.dir, relativePath),
'/',
)
},
},
}
: '',
localesNotesDirs.length
? {
include: localesNotesDirs.map(dir => `${dir}**/**.md`),
frontmatter: {
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+\./, '')
return basename
},
...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
if (note && sidebar && sidebar !== 'auto') {
const res = resolveLinkBySidebar(sidebar, pathJoin(prefix, note.dir || ''))
const file = ensureLeadingSlash(relativePath)
res[file] && args.push(res[file])
}
return pathJoin(...args, nanoid(), '/')
},
},
}
: '',
{
include: '**/{readme,README,index}.md',
frontmatter: {},
},
{
include: '*',
frontmatter: {
title(title: string, { relativePath }) {
if (title)
return title
const basename = path.basename(relativePath || '', '.md')
return basename
},
...baseFrontmatter,
permalink(permalink: string, { relativePath }) {
if (permalink)
return permalink
const locale = resolveLocale(relativePath)
const prefix = withBase(articlePrefix, locale)
return normalizePath(`${prefix}/${nanoid()}/`)
},
},
},
].filter(Boolean) as FrontmatterArray,
}
}
function resolveLinkBySidebar(
sidebar: NotesSidebar,
prefix: string,
) {
const res: Record<string, string> = {}
for (const item of sidebar) {
if (typeof item !== 'string') {
const { dir = '', link = '/', items, text = '' } = item
SidebarLink(items, link, text, pathJoin(prefix, dir), res)
}
}
return res
}
function SidebarLink(items: NotesSidebar | undefined, link: string, text: string, dir = '', res: Record<string, string> = {}) {
if (!items) {
res[pathJoin(dir, `${text}.md`)] = link
return
}
for (const item of items) {
if (typeof item === 'string') {
if (!link)
continue
if (item) {
res[pathJoin(dir, `${item}.md`)] = link
}
else {
res[pathJoin(dir, 'README.md')] = link
res[pathJoin(dir, 'index.md')] = link
res[pathJoin(dir, 'readme.md')] = link
}
}
else {
const { dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item
SidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(dir, subDir), res)
}
}
}

View File

@ -0,0 +1,56 @@
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'
import {
BLOG_TAGS_COLORS_PRESET,
generateBlogTagsColors,
} from './blogTags.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.article !== undefined
? !!page.frontmatter.article
: true,
extraBlogData(extra) {
extra.tagsColorsPreset = BLOG_TAGS_COLORS_PRESET
extra.tagsColors = {}
},
extendBlogData: (page: any, extra) => {
const tags = page.frontmatter.tags
generateBlogTagsColors(extra.tagsColors, tags)
const data: Record<string, any> = {
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
},
}
}

View File

@ -1,36 +0,0 @@
import { isEmptyObject } from '@pengzhanbo/utils'
import type { App } from 'vuepress/core'
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
import { normalizePath } from './utils.js'
export function resolveLocaleOptions<
T extends PlumeThemeLocaleOptions = PlumeThemeLocaleOptions,
K extends Exclude<keyof T, 'locales'> = Exclude<keyof T, 'locales'>,
>(options: T, key: K, locale = '', fallback = true): T[K] | undefined {
const locales = options.locales
if (!locales)
return options[key]
locale = !locale || locale === '/' ? '/' : normalizePath(`/${locale}/`)
const localeOptions = locales[locale]
const fallbackLocaleOptions = locales['/']
if (!localeOptions)
return fallback ? options[key] : undefined
const _key = key as keyof typeof localeOptions
const fallbackData = (fallbackLocaleOptions[_key] ?? options[key]) as T[K]
const value = localeOptions[_key] as T[K]
return value ?? (fallback ? fallbackData : undefined)
}
export function resolvedAppLocales(app: App): NonNullable<App['siteData']['locales']> {
if (app.siteData.locales && !isEmptyObject(app.siteData.locales))
return app.siteData.locales
const defaultLang = app.siteData.lang || 'en-US'
return { '/': { lang: defaultLang } }
}

View File

@ -1,63 +0,0 @@
import type { NotesDataOptions, NotesSidebar } from '@vuepress-plume/plugin-notes-data'
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
import { normalizePath, pathJoin } from './utils.js'
export function resolveNotesList(options: PlumeThemeLocaleOptions) {
const locales = options.locales || {}
const notesList: NotesDataOptions[] = []
for (const locale of Object.keys(locales)) {
const notes = resolveLocaleOptions(options, 'notes', locale, false)
if (notes) {
const dir = normalizePath(`/${notes.dir}`)
if (!dir.startsWith(locale))
notes.dir = pathJoin(locale, notes.dir).replace(/^\//, '')
notesList.push(notes)
}
}
return notesList
}
export function resolveLinkBySidebar(
sidebar: NotesSidebar,
prefix: string,
) {
const res: Record<string, string> = {}
for (const item of sidebar) {
if (typeof item !== 'string') {
const { dir = '', link = '/', items, text = '' } = item
SidebarLink(items, link, text, pathJoin(prefix, dir), res)
}
}
return res
}
function SidebarLink(items: NotesSidebar | undefined, link: string, text: string, dir = '', res: Record<string, string> = {}) {
if (!items) {
res[pathJoin(dir, `${text}.md`)] = link
return
}
for (const item of items) {
if (typeof item === 'string') {
if (!link)
continue
if (item) {
res[pathJoin(dir, `${item}.md`)] = link
}
else {
res[pathJoin(dir, 'README.md')] = link
res[pathJoin(dir, 'index.md')] = link
res[pathJoin(dir, 'readme.md')] = link
}
}
else {
const { dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item
SidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(dir, subDir), res)
}
}
}

View File

@ -1,4 +1,8 @@
import { path } from 'vuepress/utils'
import {
ensureLeadingSlash,
getRootLang,
getRootLangPath,
} from '@vuepress/helper'
import type { App, Page } from 'vuepress/core'
import { createPage } from 'vuepress/core'
import type {
@ -6,53 +10,62 @@ import type {
PlumeThemeLocaleOptions,
PlumeThemePageData,
} from '../shared/index.js'
import { pathJoin } from './utils.js'
import { resolveLocaleOptions, resolvedAppLocales } from './resolveLocaleOptions.js'
import { withBase } from './utils.js'
import { PRESET_LOCALES } from './locales/index.js'
import { resolveNotesLinkList } from './config/index.js'
export async function setupPage(
app: App,
localeOption: PlumeThemeLocaleOptions,
) {
const locales = resolvedAppLocales(app)
const defaultBlog = resolveLocaleOptions(localeOption, 'blog')
for (const [, locale] of Object.keys(locales).entries()) {
const blog = resolveLocaleOptions(localeOption, 'blog', locale, false)
const lang = locales[locale].lang || app.siteData.lang
const link = blog?.link
? blog.link
: pathJoin('/', locale, defaultBlog?.link || '/blog/')
const blogPage = await createPage(app, {
path: link,
frontmatter: { lang, type: 'blog' },
})
app.pages.push(blogPage)
const pageList: Promise<Page>[] = []
const locales = localeOption.locales || {}
const rootPath = getRootLangPath(app)
const rootLang = getRootLang(app)
if (blog?.tags !== false || defaultBlog?.tags !== false) {
const tagsPage = await createPage(app, {
path: pathJoin(link, 'tags/'),
frontmatter: { lang, type: 'blog-tags' },
})
app.pages.push(tagsPage)
}
const blog = localeOption.blog || {}
const link = blog.link || '/blog/'
if (blog?.archives !== false || defaultBlog?.archives !== false) {
const archivesPage = await createPage(app, {
path: pathJoin(link, 'archives/'),
frontmatter: { lang, type: 'blog-archives' },
})
app.pages.push(archivesPage)
}
const getTitle = (locale: string, key: string) => {
const opt = PRESET_LOCALES[locale] || PRESET_LOCALES[rootPath] || {}
return opt[key] || ''
}
for (const localePath of Object.keys(locales)) {
const lang = app.siteData.locales?.[localePath]?.lang || rootLang
const locale = localePath === '/' ? rootPath : localePath
// 添加 博客页面
pageList.push(createPage(app, {
path: withBase(link, localePath),
frontmatter: { lang, type: 'blog', title: getTitle(locale, 'blog') },
}))
// 添加 标签页
blog.tags !== false && pageList.push(createPage(app, {
path: withBase(blog.tagsLink || `${link}/tags/`, localePath),
frontmatter: { lang, type: 'blog-tags', title: getTitle(locale, 'tag') },
}))
// 添加归档页
blog.archives !== false && pageList.push(createPage(app, {
path: withBase(blog.archivesLink || `${link}/archives/`, localePath),
frontmatter: { lang, type: 'blog-archives', title: getTitle(locale, 'archive') },
}))
}
app.pages.push(...await Promise.all(pageList))
}
export function extendsPageData(
app: App,
page: Page<PlumeThemePageData>,
localeOptions: PlumeThemeLocaleOptions,
) {
page.data.filePathRelative = page.filePathRelative
page.routeMeta.title = page.title
if (page.frontmatter.icon)
page.routeMeta.icon = page.frontmatter.icon
if (page.frontmatter.friends) {
page.frontmatter.article = false
page.frontmatter.type = 'friends'
@ -66,7 +79,7 @@ export function extendsPageData(
page.data.type = page.frontmatter.type as any
}
autoCategory(app, page, localeOptions)
autoCategory(page, localeOptions)
pageContentRendered(page)
}
@ -75,7 +88,6 @@ const cache: Record<string, number> = {}
const RE_CATEGORY = /^(\d+)?(?:\.?)([^]+)$/
export function autoCategory(
app: App,
page: Page<PlumeThemePageData>,
options: PlumeThemeLocaleOptions,
) {
@ -83,24 +95,15 @@ export function autoCategory(
if (page.frontmatter.type || !pagePath)
return
const locales = Object.keys(resolvedAppLocales(app))
const notesLinks: string[] = []
for (const [, locale] of locales.entries()) {
const config = options.locales?.[locale]?.notes
if (config && config.notes) {
notesLinks.push(
...config.notes.map(
note => path.join(locale, config.link || '', note.link).replace(/\\+/g, '/'),
),
)
}
}
const notesLinks = resolveNotesLinkList(options)
if (notesLinks.some(link => page.path.startsWith(link)))
return
const RE_LOCALE = new RegExp(
`^(${locales.filter(l => l !== '/').join('|')})`,
`^(${Object.keys(options.locales || {}).filter(l => l !== '/').join('|')})`,
)
const categoryList: PageCategoryData[] = `/${pagePath}`
const categoryList: PageCategoryData[] = ensureLeadingSlash(pagePath)
.replace(RE_LOCALE, '')
.replace(/^\//, '')
.split('/')

View File

@ -1,14 +1,12 @@
import type { Page, Theme } from 'vuepress/core'
import { logger, templateRenderer } from 'vuepress/utils'
import { templateRenderer } from 'vuepress/utils'
import { isPlainObject } from '@vuepress/helper'
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
import { setupPlugins } from './plugins.js'
import { getPlugins } from './plugins/index.js'
import { extendsPageData, setupPage } from './setupPages.js'
import { getThemePackage, resolve, templates } from './utils.js'
import { THEME_NAME, getThemePackage, logger, resolve, templates } from './utils.js'
import { resolveEncrypt, resolveLocaleOptions, resolvePageHead } from './config/index.js'
const THEME_NAME = 'vuepress-theme-plume'
export function plumeTheme({
themePlugins,
plugins,
@ -16,10 +14,11 @@ export function plumeTheme({
hostname,
...localeOptions
}: PlumeThemeOptions = {}): Theme {
const pluginsOptions = plugins ?? themePlugins ?? {}
const pluginOptions = plugins ?? themePlugins ?? {}
const pkg = getThemePackage()
const watermarkFullPage = isPlainObject(pluginsOptions.watermark)
? pluginsOptions.watermark.fullPage !== false
const watermarkFullPage = isPlainObject(pluginOptions.watermark)
? pluginOptions.watermark.fullPage !== false
: true
if (themePlugins) {
@ -42,12 +41,12 @@ export function plumeTheme({
clientConfigFile: resolve('client/config.js'),
plugins: setupPlugins({ app, options: pluginsOptions, localeOptions, encrypt, hostname }),
plugins: getPlugins({ app, pluginOptions, localeOptions, encrypt, hostname }),
onInitialized: app => setupPage(app, localeOptions),
onInitialized: async app => await setupPage(app, localeOptions),
extendsPage: (page) => {
extendsPageData(app, page as Page<PlumeThemePageData>, localeOptions)
extendsPageData(page as Page<PlumeThemePageData>, localeOptions)
resolvePageHead(page, localeOptions)
},

View File

@ -1,6 +1,9 @@
import process from 'node:process'
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)
@ -9,6 +12,8 @@ export const templates = (url: string) => resolve('../templates', url)
export const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8)
export const logger = new Logger(THEME_NAME)
export function getPackage() {
let pkg = {} as any
try {
@ -30,8 +35,8 @@ export function getThemePackage() {
}
const RE_SLASH = /(\\|\/)+/g
export function normalizePath(dir: string) {
return dir.replace(RE_SLASH, '/')
export function normalizePath(path: string) {
return path.replace(RE_SLASH, '/')
}
export function pathJoin(...args: string[]) {
@ -45,3 +50,10 @@ export function getCurrentDirname(basePath: string | undefined, filepath: string
.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}`)
}

View File

@ -63,9 +63,23 @@ export interface PlumeThemeBlog {
* @default true
*/
tags?: boolean
/**
*
*
* @default '/blog/tags/'
*/
tagsLink?: string
/**
*
* @default true
*/
archives?: boolean
/**
*
*
* @default '/blog/archives/'
*/
archivesLink?: string
}