chore: tweak

This commit is contained in:
pengzhanbo 2024-07-08 02:45:39 +08:00
parent 0fb444b68f
commit 2c91011697
24 changed files with 537 additions and 302 deletions

View File

@ -21,10 +21,6 @@ export default defineUserConfig({
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules'],
markdown: {
code: false,
},
bundler: viteBundler(),
theme,

View File

@ -1,60 +1,9 @@
import process from 'node:process'
import themePlume from 'vuepress-theme-plume'
import { plumeTheme } from 'vuepress-theme-plume'
import type { Theme } from 'vuepress'
import { enNotes, zhNotes } from './notes.js'
import { enNavbar, zhNavbar } from './navbar.js'
export const theme: Theme = themePlume({
logo: '/plume.png',
export const theme: Theme = plumeTheme({
hostname: process.env.SITE_HOST || 'https://plume.pengzhanbo.cn',
docsRepo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
docsDir: 'docs',
profile: {
avatar: '/plume.png',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
location: 'GuangZhou, China',
organization: 'pengzhanbo',
},
social: [
{ icon: 'github', link: 'https://github.com/pengzhanbo/vuepress-theme-plume' },
{ icon: 'gitlab', link: 'https://pengzhanbo.cn' },
{ icon: 'npm', link: 'https://pengzhanbo.cn' },
{ icon: 'docker', link: 'https://pengzhanbo.cn' },
{ icon: 'stackoverflow', link: 'https://pengzhanbo.cn' },
{ icon: 'juejin', link: 'https://pengzhanbo.cn' },
{ icon: 'discord', link: 'https://pengzhanbo.cn' },
{ icon: 'instagram', link: 'https://pengzhanbo.cn' },
{ icon: 'mastodon', link: 'https://pengzhanbo.cn' },
{ icon: 'slack', link: 'https://pengzhanbo.cn' },
{ icon: 'bilibili', link: 'https://pengzhanbo.cn' },
{ icon: 'linkedin', link: 'https://pengzhanbo.cn' },
{ icon: 'qq', link: 'https://pengzhanbo.cn' },
{ icon: 'twitter', link: 'https://pengzhanbo.cn' },
{ icon: 'x', link: 'https://pengzhanbo.cn' },
{ icon: 'weibo', link: 'https://pengzhanbo.cn' },
{ icon: 'youtube', link: 'https://pengzhanbo.cn' },
{ icon: 'zhihu', link: 'https://pengzhanbo.cn' },
{ icon: 'douban', link: 'https://pengzhanbo.cn' },
{ icon: 'steam', link: 'https://pengzhanbo.cn' },
{ icon: 'xbox', link: 'https://pengzhanbo.cn' },
],
navbarSocialInclude: ['github'],
footer: { copyright: 'Copyright © 2021-present pengzhanbo' },
locales: {
'/': {
notes: zhNotes,
navbar: zhNavbar,
},
'/en/': {
notes: enNotes,
navbar: enNavbar,
},
},
plugins: {
frontmatter: { exclude: ['**/*.snippet.*'] },
@ -108,9 +57,4 @@ export const theme: Theme = themePlume({
},
},
encrypt: {
rules: {
'/article/enx7c9s/': '123456',
},
},
})

18
pnpm-lock.yaml generated
View File

@ -350,15 +350,9 @@ importers:
'@vue/devtools-api':
specifier: 6.6.3
version: 6.6.3
'@vuepress-plume/plugin-auto-frontmatter':
specifier: workspace:*
version: link:../plugins/plugin-auto-frontmatter
'@vuepress-plume/plugin-baidu-tongji':
specifier: workspace:*
version: link:../plugins/plugin-baidu-tongji
'@vuepress-plume/plugin-blog-data':
specifier: workspace:*
version: link:../plugins/plugin-blog-data
'@vuepress-plume/plugin-content-update':
specifier: workspace:*
version: link:../plugins/plugin-content-update
@ -368,9 +362,6 @@ importers:
'@vuepress-plume/plugin-iconify':
specifier: workspace:*
version: link:../plugins/plugin-iconify
'@vuepress-plume/plugin-notes-data':
specifier: workspace:*
version: link:../plugins/plugin-notes-data
'@vuepress-plume/plugin-search':
specifier: workspace:*
version: link:../plugins/plugin-search
@ -431,6 +422,15 @@ importers:
esbuild:
specifier: ~0.21.5
version: 0.21.5
fast-glob:
specifier: ^3.3.2
version: 3.3.2
gray-matter:
specifier: ^4.0.3
version: 4.0.3
json2yaml:
specifier: ^1.1.0
version: 1.1.0
katex:
specifier: ^0.16.10
version: 0.16.10

View File

@ -1,19 +1,19 @@
<script lang="ts" setup>
import VPNavBarMenuGroup from '@theme/Nav/VPNavBarMenuGroup.vue'
import VPNavBarMenuLink from '@theme/Nav/VPNavBarMenuLink.vue'
import { useData } from '../../composables/data.js'
import { useNavbarData } from '../../composables/nav.js'
const { theme } = useData()
const navbar = useNavbarData()
</script>
<template>
<nav
v-if="theme.navbar"
v-if="navbar.length"
aria-labelledby="main-nav-aria-label"
class="vp-navbar-menu"
>
<span id="main-nav-aria-label" class="visually-hidden">Main Navigation</span>
<template v-for="item in theme.navbar" :key="item.text">
<template v-for="item in navbar" :key="item.text">
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
<VPNavBarMenuGroup v-else :item="item" />
</template>

View File

@ -2,17 +2,20 @@
import { computed } from 'vue'
import { resolveRouteFullPath } from 'vuepress/client'
import VPFlyout from '@theme/VPFlyout.vue'
import type { NavItem, NavItemWithChildren } from '../../../shared/index.js'
import type {
ResolvedNavItem,
ResolvedNavItemWithChildren,
} from '../../../shared/resolved/navbar.js'
import { isActive } from '../../utils/index.js'
import { useData } from '../../composables/data.js'
const props = defineProps<{
item: NavItemWithChildren
item: ResolvedNavItemWithChildren
}>()
const { page } = useData()
function isChildActive(navItem: NavItem) {
function isChildActive(navItem: ResolvedNavItem): boolean {
if ('link' in navItem) {
return isActive(
page.value.path,

View File

@ -2,12 +2,12 @@
import { resolveRouteFullPath } from 'vuepress/client'
import VPLink from '@theme/VPLink.vue'
import VPIcon from '@theme/VPIcon.vue'
import type { NavItemWithLink } from '../../../shared/index.js'
import type { ResolvedNavItemWithLink } from '../../../shared/resolved/navbar.js'
import { isActive } from '../../utils/index.js'
import { useData } from '../../composables/data.js'
defineProps<{
item: NavItemWithLink
item: ResolvedNavItemWithLink
}>()
const { page } = useData()
@ -23,7 +23,7 @@ const { page } = useData()
),
}"
:href="item.link"
no-icon
:no-icon="item.noIcon"
:target="item.target"
:rel="item.rel"
tabindex="0"

View File

@ -1,19 +1,17 @@
<script lang="ts" setup>
import VPNavScreenMenuGroup from '@theme/Nav/VPNavScreenMenuGroup.vue'
import VPNavScreenMenuLink from '@theme/Nav/VPNavScreenMenuLink.vue'
import { useData } from '../../composables/data.js'
import { useNavbarData } from '../../composables/nav.js'
const { theme } = useData()
const navbar = useNavbarData()
</script>
<template>
<nav v-if="theme.navbar" class="vp-nav-screen-menu">
<template v-for="item in theme.navbar" :key="item.text">
<nav v-if="navbar.length" class="vp-nav-screen-menu">
<template v-for="item in navbar" :key="item.text">
<VPNavScreenMenuLink
v-if="'link' in item"
:text="item.text"
:link="item.link"
:icon="item.icon"
:item="item"
/>
<VPNavScreenMenuGroup
v-else

View File

@ -3,10 +3,11 @@ import { computed, ref } from 'vue'
import VPIcon from '@theme/VPIcon.vue'
import VPNavScreenMenuGroupLink from '@theme/Nav/VPNavScreenMenuGroupLink.vue'
import VPNavScreenMenuGroupSection from '@theme/Nav/VPNavScreenMenuGroupSection.vue'
import type { ThemeIcon } from '../../../shared/index.js'
const props = defineProps<{
text: string
icon?: string | { svg: string }
icon?: ThemeIcon
items: any[]
}>()
@ -39,11 +40,7 @@ function toggle() {
<div :id="groupId" class="items">
<template v-for="item in items" :key="item.text">
<div v-if="'link' in item" :key="item.text" class="item">
<VPNavScreenMenuGroupLink
:text="item.text"
:link="item.link"
:icon="item.icon"
/>
<VPNavScreenMenuGroupLink :item="item" />
</div>
<div v-else class="group">

View File

@ -2,11 +2,10 @@
import { inject } from 'vue'
import VPLink from '@theme/VPLink.vue'
import VPIcon from '@theme/VPIcon.vue'
import type { ResolvedNavItemWithLink } from '../../../shared/resolved/navbar.js'
defineProps<{
icon?: string | { svg: string }
text: string
link: string
item: ResolvedNavItemWithLink
}>()
const closeScreen = inject('close-screen') as () => void
@ -15,11 +14,14 @@ const closeScreen = inject('close-screen') as () => void
<template>
<VPLink
class="vp-nav-screen-menu-group-link"
:href="link"
:href="item.link"
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
@click="closeScreen"
>
<VPIcon v-if="icon" :name="icon" />
<i v-text="text" />
<VPIcon v-if="item.icon" :name="item.icon" />
<span v-html="item.text" />
</VPLink>
</template>

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import VPIcon from '@theme/VPIcon.vue'
import VPNavScreenMenuGroupLink from '@theme/Nav/VPNavScreenMenuGroupLink.vue'
import type { NavItemWithLink } from '../../../shared/index.js'
import type { NavItemWithLink, ThemeIcon } from '../../../shared/index.js'
defineProps<{
icon?: string | { svg: string }
icon?: ThemeIcon
text?: string
items: NavItemWithLink[]
}>()
@ -19,9 +19,7 @@ defineProps<{
<VPNavScreenMenuGroupLink
v-for="item in items"
:key="item.text"
:text="item.text"
:link="item.link"
:icon="item.icon"
:item="item"
/>
</div>
</template>

View File

@ -2,20 +2,26 @@
import { inject } from 'vue'
import VPLink from '@theme/VPLink.vue'
import VPIcon from '@theme/VPIcon.vue'
import type { ResolvedNavItemWithLink } from '../../../shared/resolved/navbar.js'
defineProps<{
text: string
link: string
icon?: string | { svg: string }
item: ResolvedNavItemWithLink
}>()
const closeScreen = inject('close-screen') as () => void
</script>
<template>
<VPLink class="vp-nav-screen-menu-link" :href="link" @click="closeScreen">
<VPIcon v-if="icon" :name="icon" />
<i v-text="text" />
<VPLink
class="vp-nav-screen-menu-link"
:href="item.link"
:target="item.target"
:rel="item.rel"
:no-icon="item.noIcon"
@click="closeScreen"
>
<VPIcon v-if="item.icon" :name="item.icon" />
<span v-html="item.text" />
</VPLink>
</template>

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import type { NotesSidebarItem } from '@vuepress-plume/plugin-notes-data'
import { computed } from 'vue'
import VPLink from '@theme/VPLink.vue'
import VPIcon from '@theme/VPIcon.vue'
import { useSidebarControl } from '../composables/sidebar.js'
import type { ResolvedSidebarItem } from '../../shared/resolved/sidebar.js'
const props = defineProps<{
item: NotesSidebarItem
item: ResolvedSidebarItem
depth: number
}>()
@ -98,7 +98,7 @@ function onCaretClick() {
<div v-if="item.items && item.items.length" class="items">
<template v-if="depth < 5">
<VPSidebarItem
v-for="i in (item.items as NotesSidebarItem[])"
v-for="i in item.items"
:key="i.text"
:item="i"
:depth="depth + 1"

View File

@ -1,8 +1,8 @@
import { usePageLang } from 'vuepress/client'
import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useBlogPostData } from './blog-data.js'
import { useData } from './data.js'
import { useRouteQuery } from './route-query.js'
@ -23,7 +23,7 @@ export function usePostListControl() {
const postList = computed(() => {
const stickyList = list.value.filter(item =>
typeof item.sticky === 'boolean' ? item.sticky : item.sticky >= 0,
item.sticky === true || typeof item.sticky === 'number',
)
const otherList = list.value.filter(
item => item.sticky === undefined || item.sticky === false,
@ -33,7 +33,7 @@ export function usePostListControl() {
...stickyList.sort((prev, next) => {
if (next.sticky === true && prev.sticky === true)
return 0
return next.sticky > prev.sticky ? 1 : -1
return next.sticky! > prev.sticky! ? 1 : -1
}),
...otherList,
] as PlumeThemeBlogPostItem[]

View File

@ -1,5 +1,4 @@
import type { Ref } from 'vue'
import type { ThemeLocaleDataRef } from '@vuepress/plugin-theme-data/client'
import {
usePageData,
usePageFrontmatter,
@ -19,6 +18,7 @@ import type {
PlumeThemePageFrontmatter,
PlumeThemePostFrontmatter,
} from '../../shared/index.js'
import type { ThemeLocaleDataRef } from './theme-data.js'
import { useThemeLocaleData } from './theme-data.js'
import { useDarkMode } from './dark-mode.js'

View File

@ -3,26 +3,7 @@ import { type Ref, computed } from 'vue'
import { hasOwn, useSessionStorage } from '@vueuse/core'
import { useRoute } from 'vuepress/client'
import { useData } from './data.js'
declare const __PLUME_ENCRYPT_GLOBAL__: boolean
declare const __PLUME_ENCRYPT_SEPARATOR__: string
declare const __PLUME_ENCRYPT_ADMIN__: string
declare const __PLUME_ENCRYPT_KEYS__: string[]
declare const __PLUME_ENCRYPT_RULES__: Record<string, string>
const global = __PLUME_ENCRYPT_GLOBAL__
const separator = __PLUME_ENCRYPT_SEPARATOR__
const admin = __PLUME_ENCRYPT_ADMIN__
const matches = __PLUME_ENCRYPT_KEYS__
const rules = __PLUME_ENCRYPT_RULES__
const admins = admin.split(separator)
const ruleList = Object.keys(rules).map(key => ({
key,
match: matches[key] as string,
rules: rules[key].split(separator),
}))
import { useEncryptData } from './encrypt-data.js'
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => ({
s: [genSaltSync(10), genSaltSync(10)] as const,
@ -44,7 +25,7 @@ function splitHash(hash: string) {
}
const cache = new Map<string, boolean>()
function compare(content: string, hash: string) {
function compare(content: string, hash: string, separator = ':') {
const key = [content, hash].join(separator)
if (cache.has(key))
return cache.get(key)
@ -58,21 +39,23 @@ export function useGlobalEncrypt(): {
isGlobalDecrypted: Ref<boolean>
compareGlobal: (password: string) => boolean
} {
const encrypt = useEncryptData()
const isGlobalDecrypted = computed(() => {
if (!global)
if (!encrypt.value.global)
return true
const hash = splitHash(storage.value.g)
return !!hash && admins.includes(hash)
return !!hash && encrypt.value.admins.includes(hash)
})
function compareGlobal(password: string) {
if (!password)
return false
for (const admin of admins) {
if (compare(password, admin)) {
for (const admin of encrypt.value.admins) {
if (compare(password, admin, encrypt.value.separator)) {
storage.value.g = mergeHash(admin)
return true
}
@ -90,11 +73,12 @@ export function useGlobalEncrypt(): {
export function usePageEncrypt() {
const { page } = useData()
const route = useRoute()
const encrypt = useEncryptData()
const hasPageEncrypt = computed(() => ruleList.length ? matches.some(toMatch) : false)
const hasPageEncrypt = computed(() => encrypt.value.ruleList.length ? encrypt.value.matches.some(toMatch) : false)
const hashList = computed(() => ruleList.length
? ruleList
const hashList = computed(() => encrypt.value.ruleList.length
? encrypt.value.ruleList
.filter(item => toMatch(item.match))
: [])
@ -103,7 +87,7 @@ export function usePageEncrypt() {
return true
const hash = splitHash(storage.value.p.__GLOBAL__ || '')
if (hash && admins.includes(hash))
if (hash && encrypt.value.admins.includes(hash))
return true
for (const { key, rules } of hashList.value) {
@ -136,8 +120,8 @@ export function usePageEncrypt() {
let decrypted = false
// check global
for (const admin of admins) {
if (compare(password, admin)) {
for (const admin of encrypt.value.admins) {
if (compare(password, admin, encrypt.value.separator)) {
decrypted = true
storage.value.p = {
...storage.value.p,
@ -151,7 +135,7 @@ export function usePageEncrypt() {
for (const { match, key, rules } of hashList.value) {
if (toMatch(match)) {
for (const rule of rules) {
if (compare(password, rule)) {
if (compare(password, rule, encrypt.value.separator)) {
decrypted = true
storage.value.p = {
...storage.value.p,

View File

@ -13,12 +13,16 @@ export * from './edit-link.js'
export * from './latest-updated.js'
export * from './contributors.js'
export * from './blog-data.js'
export * from './blog-post-list.js'
export * from './blog-extract.js'
export * from './blog-tags.js'
export * from './blog-archives.js'
export * from './tag-colors.js'
export * from './encrypt-data.js'
export * from './encrypt.js'
export * from './link.js'
export * from './locale.js'
export * from './route-query.js'

View File

@ -1,8 +1,9 @@
import { resolveRoute, useRouteLocale, withBase } from 'vuepress/client'
import { computed } from 'vue'
import { normalizeLink } from '../utils/index.js'
import { useThemeData } from './theme-data.js'
import { useData } from './data.js'
import { getSidebarFirstLink, getSidebarList, normalizePath, useNotesData } from './sidebar.js'
import { getSidebarFirstLink, useSidebarData } from './sidebar.js'
export function useLangs({
removeCurrent = true,
@ -10,7 +11,7 @@ export function useLangs({
const theme = useThemeData()
const { page } = useData()
const routeLocale = useRouteLocale()
const notesData = useNotesData()
const sidebar = useSidebarData()
const currentLang = computed(() => {
const link = routeLocale.value
@ -22,17 +23,15 @@ export function useLangs({
const getPageLink = (locale: string) => {
const pagePath = page.value.path.slice(routeLocale.value.length)
const targetPath = normalizePath(`${locale}${pagePath}`)
const targetPath = normalizeLink(locale, pagePath)
const { notFound, path } = resolveRoute(targetPath)
if (!notFound)
return path
const locales = theme.value.locales || {}
const blog = locales[`/${locale}/`]?.blog
const fallback = locales['/']?.blog ?? theme.value.blog
const blog = theme.value.blog
if (page.value.isBlogPost)
return withBase(blog?.link || normalizePath(`${locale}${fallback?.link || 'blog/'}`))
return withBase(blog?.link || normalizeLink(locale, 'blog/'))
const sidebarList = getSidebarList(targetPath, notesData.value)
const sidebarList = sidebar.value
if (sidebarList.length > 0) {
const link = getSidebarFirstLink(sidebarList)

View File

@ -1,6 +1,43 @@
import type { Ref } from 'vue'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vuepress/client'
import type { NavItem } from '../../shared/index.js'
import type {
ResolvedNavItem,
ResolvedNavItemWithLink,
} from '../../shared/resolved/navbar.js'
import { normalizeLink, resolveNavLink } from '../utils/index.js'
import { useData } from './data.js'
export function useNavbarData(): Ref<ResolvedNavItem[]> {
const { theme } = useData()
return computed(() => resolveNavbar(theme.value.navbar || []))
}
function resolveNavbar(navbar: NavItem[], _prefix = ''): ResolvedNavItem[] {
const resolved: ResolvedNavItem[] = []
navbar.forEach((item) => {
if (typeof item === 'string') {
resolved.push(resolveNavLink(normalizeLink(_prefix, item)))
}
else {
const { items, prefix, ...args } = item
const res = { ...args } as ResolvedNavItem
if ('link' in res) {
res.link = normalizeLink(_prefix, res.link)
}
if (items?.length) {
res.items = resolveNavbar(
items,
normalizeLink(_prefix, prefix),
) as ResolvedNavItemWithLink[]
}
resolved.push(res)
}
})
return resolved
}
export interface UseNavReturn {
isScreenOpen: Ref<boolean>

View File

@ -1,91 +1,261 @@
import { resolveRouteFullPath, useRoute, withBase } from 'vuepress/client'
import type {
NotesData,
NotesSidebarItem,
} from '@vuepress-plume/plugin-notes-data'
import { useNotesData } from '@vuepress-plume/plugin-notes-data/client'
import { resolveRouteFullPath, useRoute, useRouteLocale } from 'vuepress/client'
import {
ensureLeadingSlash,
isArray,
isPlainObject,
isString,
} from '@vuepress/helper/client'
import { useMediaQuery } from '@vueuse/core'
import type { ComputedRef, Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
import { isActive } from '../utils/index.js'
import type { ComputedRef, InjectionKey, Ref } from 'vue'
import {
computed,
inject,
onMounted,
onUnmounted,
provide,
ref,
watch,
watchEffect,
watchPostEffect,
} from 'vue'
import { sidebar as sidebarRaw } from '@internal/sidebar'
import { isActive, normalizeLink, normalizePrefix, resolveNavLink } from '../utils/index.js'
import type { Sidebar, SidebarItem } from '../../shared/index.js'
import type { ResolvedSidebarItem } from '../../shared/resolved/sidebar.js'
import { useData } from './data.js'
export { useNotesData }
export type SidebarData = Record<string, Sidebar>
export function normalizePath(path: string) {
return path.replace(/\/\\+/g, '/').replace(/\/+/g, '/')
}
export type SidebarDataRef = Ref<SidebarData>
export type AutoDirSidebarRef = Ref<SidebarItem[]>
export function getSidebarList(path: string, notesData: NotesData) {
const link = Object.keys(notesData).find(link =>
path.startsWith(normalizePath(link)),
)
const sidebar = link ? notesData[link] : []
const { __auto__, ...items } = sidebarRaw
const groups: NotesSidebarItem[] = []
const sidebarData: SidebarDataRef = ref(items)
const autoDirSidebar: AutoDirSidebarRef = ref(__auto__)
let lastGroupIndex: number = 0
for (const index in sidebar) {
const item = sidebar[index]
if (item.items && item.items.length) {
lastGroupIndex = groups.push(item)
continue
}
if (!groups[lastGroupIndex])
groups.push({ items: [] })
groups[lastGroupIndex]!.items!.push(item)
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateSidebar = (data: SidebarData) => {
const { __auto__, ...items } = data
sidebarData.value = items
autoDirSidebar.value = __auto__ as SidebarItem[]
}
return groups
}
export function getSidebarFirstLink(sidebar: NotesSidebarItem[]) {
for (const item of sidebar) {
if (item.link)
return item.link
if (item.items)
return getSidebarFirstLink(item.items as NotesSidebarItem[])
}
return ''
}
const sidebarSymbol: InjectionKey<Ref<ResolvedSidebarItem[]>> = Symbol(
__VUEPRESS_DEV__ ? 'sidebar' : '',
)
export function useSidebar() {
const route = useRoute()
const notesData = useNotesData()
const { frontmatter, theme } = useData()
export function setupSidebar() {
const { page, frontmatter } = useData()
const is960 = useMediaQuery('(min-width: 960px)')
const isOpen = ref(false)
const sidebarKey = computed(() => {
const link = Object.keys(notesData.value).find(link =>
route.path.startsWith(normalizePath(withBase(link))),
)
return link
})
const sidebar = computed(() => {
const link = typeof frontmatter.value.sidebar === 'string'
? frontmatter.value.sidebar
: route.path
return getSidebarList(link, notesData.value)
})
const routeLocale = useRouteLocale()
const hasSidebar = computed(() => {
return (
frontmatter.value.pageLayout !== 'home'
&& sidebar.value.length > 0
&& frontmatter.value.pageLayout !== 'friends'
&& frontmatter.value.sidebar !== false
&& frontmatter.value.layout !== 'NotFound'
)
})
const sidebarData = computed(() => {
return hasSidebar.value
? getSidebar(typeof frontmatter.value.sidebar === 'string'
? frontmatter.value.sidebar
: page.value.path, routeLocale.value)
: []
})
provide(sidebarSymbol, sidebarData)
}
export function useSidebarData(): Ref<ResolvedSidebarItem[]> {
const sidebarData = inject(sidebarSymbol)
if (!sidebarData) {
throw new Error('useSidebarData() is called without provider.')
}
return sidebarData
}
/**
* Get the `Sidebar` from sidebar option. This method will ensure to get correct
* sidebar config from `MultiSideBarConfig` with various path combinations such
* as matching `guide/` and `/guide/`. If no matching config was found, it will
* return empty array.
*/
export function getSidebar(routePath: string, routeLocal: string): ResolvedSidebarItem[] {
const _sidebar = sidebarData.value[routeLocal]
if (_sidebar === 'auto') {
return resolveSidebarItems(autoDirSidebar.value[routeLocal])
}
else if (isArray(_sidebar)) {
return resolveSidebarItems(_sidebar, routeLocal)
}
else if (isPlainObject(_sidebar)) {
const dir
= Object.keys(_sidebar)
.sort((a, b) => b.split('/').length - a.split('/').length)
.find((dir) => {
// make sure the multi sidebar key starts with slash too
return routePath.startsWith(ensureLeadingSlash(dir))
}) || ''
const sidebar = dir ? _sidebar[dir] : undefined
if (sidebar === 'auto') {
return resolveSidebarItems(
dir ? autoDirSidebar.value[dir] : [],
routeLocal,
)
}
else if (isArray(sidebar)) {
return resolveSidebarItems(sidebar, dir)
}
else if (isPlainObject(sidebar)) {
const prefix = normalizePrefix(dir, sidebar.prefix)
return resolveSidebarItems(
sidebar.items === 'auto'
? autoDirSidebar.value[prefix]
: sidebar.items,
prefix,
)
}
}
return []
}
function resolveSidebarItems(sidebarItems: (string | SidebarItem)[], _prefix = ''): ResolvedSidebarItem[] {
const resolved: ResolvedSidebarItem[] = []
sidebarItems.forEach((item) => {
if (isString(item)) {
resolved.push(resolveNavLink(normalizeLink(_prefix, item)))
}
else {
const { link, items, prefix, dir, ...args } = item
const navLink = { ...args } as ResolvedSidebarItem
if (link) {
navLink.link = normalizeLink(_prefix, link)
const nav = resolveNavLink(navLink.link)
navLink.icon = nav.icon
}
const nextPrefix = normalizePrefix(_prefix, prefix || dir)
if (items === 'auto') {
navLink.items = autoDirSidebar.value[nextPrefix]
}
else {
navLink.items = items?.length
? resolveSidebarItems(items, nextPrefix)
: undefined
}
resolved.push(navLink)
}
})
return resolved
}
/**
* Get or generate sidebar group from the given sidebar items.
*/
export function getSidebarGroups(sidebar: ResolvedSidebarItem[]): ResolvedSidebarItem[] {
const groups: ResolvedSidebarItem[] = []
let lastGroupIndex = 0
for (const index in sidebar) {
const item = sidebar[index]
if (item.items) {
lastGroupIndex = groups.push(item)
continue
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] })
}
groups[lastGroupIndex]!.items!.push(item)
}
return groups
}
/**
* Check if the given sidebar item contains any active link.
*/
export function hasActiveLink(path: string, items: ResolvedSidebarItem | ResolvedSidebarItem[]): boolean {
if (Array.isArray(items)) {
return items.some(item => hasActiveLink(path, item))
}
return isActive(
path,
items.link ? resolveRouteFullPath(items.link) : undefined,
)
? true
: items.items
? hasActiveLink(path, items.items)
: false
}
export interface SidebarControl {
collapsed: Ref<boolean>
collapsible: ComputedRef<boolean>
isLink: ComputedRef<boolean>
isActiveLink: Ref<boolean>
hasActiveLink: ComputedRef<boolean>
hasChildren: ComputedRef<boolean>
toggle: () => void
}
export interface UseSidebarReturn {
isOpen: Ref<boolean>
sidebar: Ref<ResolvedSidebarItem[]>
sidebarKey: Ref<string>
sidebarGroups: Ref<ResolvedSidebarItem[]>
hasSidebar: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
open: () => void
close: () => void
toggle: () => void
}
const containsActiveLink = hasActiveLink
export function useSidebar(): UseSidebarReturn {
const { theme, frontmatter, page } = useData()
const routeLocal = useRouteLocale()
const is960 = useMediaQuery('(min-width: 960px)')
const isOpen = ref(false)
const sidebarKey = computed(() => {
const _sidebar = sidebarData.value[routeLocal.value]
if (!_sidebar || _sidebar === 'auto' || isArray(_sidebar))
return routeLocal.value
return Object.keys(_sidebar)
.sort((a, b) => b.split('/').length - a.split('/').length)
.find((dir) => {
return page.value.path.startsWith(ensureLeadingSlash(dir))
}) || ''
})
const sidebar = useSidebarData()
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false
&& sidebar.value.length > 0
&& frontmatter.value.pageLayout !== 'home'
)
})
const hasAside = computed(() => {
if (frontmatter.value.pageLayout === 'home')
if (frontmatter.value.pageLayout === 'home' || frontmatter.value.home)
return false
if (frontmatter.value.aside != null)
return !!frontmatter.value.aside
@ -107,37 +277,38 @@ export function useSidebar() {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
})
function open() {
const open = (): void => {
isOpen.value = true
}
function close() {
const close = (): void => {
isOpen.value = false
}
function toggle() {
const toggle = (): void => {
isOpen.value ? close() : open()
}
return {
isOpen,
sidebar,
sidebarKey,
sidebarGroups,
hasSidebar,
hasAside,
leftAside,
isSidebarEnabled,
sidebarGroups,
sidebarKey,
open,
close,
toggle,
}
}
export function useCloseSidebarOnEscape(
isOpen: Ref<boolean>,
close: () => void,
) {
/**
* a11y: cache the element that opened the Sidebar (the menu button) then
* focus that button again when Menu is closed with Escape key.
*/
export function useCloseSidebarOnEscape(isOpen: Ref<boolean>, close: () => void): void {
let triggerElement: HTMLButtonElement | undefined
watchEffect(() => {
@ -154,7 +325,7 @@ export function useCloseSidebarOnEscape(
window.removeEventListener('keyup', onEscape)
})
function onEscape(e: KeyboardEvent) {
function onEscape(e: KeyboardEvent): void {
if (e.key === 'Escape' && isOpen.value) {
close()
triggerElement?.focus()
@ -162,14 +333,14 @@ export function useCloseSidebarOnEscape(
}
}
export function useSidebarControl(item: ComputedRef<NotesSidebarItem>) {
export function useSidebarControl(item: ComputedRef<ResolvedSidebarItem>): SidebarControl {
const { page } = useData()
const route = useRoute()
const collapsed = ref(item.value.collapsed ?? false)
const collapsed = ref(false)
const collapsible = computed(() => {
return item.value.collapsed !== null && item.value.collapsed !== undefined
return item.value.collapsed != null
})
const isLink = computed(() => {
@ -177,22 +348,23 @@ export function useSidebarControl(item: ComputedRef<NotesSidebarItem>) {
})
const isActiveLink = ref(false)
const updateIsActiveLink = () => {
isActiveLink.value = isActive(page.value.path, item.value.link ? resolveRouteFullPath(item.value.link) : undefined)
const updateIsActiveLink = (): void => {
isActiveLink.value = isActive(
page.value.path,
item.value.link ? resolveRouteFullPath(item.value.link) : undefined,
)
}
watch([page, item, () => route.hash], updateIsActiveLink)
onMounted(updateIsActiveLink)
const hasActiveLink = computed(() => {
if (isActiveLink.value)
if (isActiveLink.value) {
return true
}
return item.value.items
? containsActiveLink(
page.value.path,
item.value.items as NotesSidebarItem[],
)
? containsActiveLink(page.value.filePathRelative || '', item.value.items)
: false
})
@ -204,13 +376,14 @@ export function useSidebarControl(item: ComputedRef<NotesSidebarItem>) {
collapsed.value = !!(collapsible.value && item.value.collapsed)
})
watchEffect(() => {
watchPostEffect(() => {
;(isActiveLink.value || hasActiveLink.value) && (collapsed.value = false)
})
function toggle() {
if (collapsible.value)
const toggle = (): void => {
if (collapsible.value) {
collapsed.value = !collapsed.value
}
}
return {
@ -224,43 +397,12 @@ export function useSidebarControl(item: ComputedRef<NotesSidebarItem>) {
}
}
export function containsActiveLink(
path: string,
items: NotesSidebarItem | NotesSidebarItem[],
): boolean {
if (Array.isArray(items))
return items.some(item => containsActiveLink(path, item))
return isActive(path, items.link ? resolveRouteFullPath(items.link) : undefined)
? true
: items.items
? containsActiveLink(path, items.items as NotesSidebarItem[])
: false
}
/**
* Get or generate sidebar group from the given sidebar items.
*/
export function getSidebarGroups(
sidebar: NotesSidebarItem[],
): NotesSidebarItem[] {
const groups: NotesSidebarItem[] = []
let lastGroupIndex = 0
for (const index in sidebar) {
const item = sidebar[index]
if (item.items) {
lastGroupIndex = groups.push(item)
continue
}
if (!groups[lastGroupIndex])
groups.push({ items: [] })
groups[lastGroupIndex]!.items!.push(item)
export function getSidebarFirstLink(sidebar: ResolvedSidebarItem[]): string {
for (const item of sidebar) {
if (item.link)
return item.link
if (item.items)
return getSidebarFirstLink(item.items)
}
return groups
return ''
}

View File

@ -10,7 +10,7 @@ const tagColorsRef: TagColorsRef = ref(articleTagColors)
export const useTagColors = (): TagColorsRef => tagColorsRef
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateArticleTagColor = (data: TagColors) => {
__VUE_HMR_RUNTIME__.updateArticleTagColors = (data: TagColors) => {
tagColorsRef.value = data
}
}

View File

@ -1,16 +1,76 @@
import {
useThemeData as _useThemeData,
useThemeLocaleData as _useThemeLocaleData,
} from '@vuepress/plugin-theme-data/client'
import type {
ThemeDataRef,
ThemeLocaleDataRef,
} from '@vuepress/plugin-theme-data/client'
import { themeData as themeDataRaw } from '@internal/themePlumeData'
import { computed, inject, ref } from 'vue'
import type { App, ComputedRef, InjectionKey, Ref } from 'vue'
import { type ClientData, type RouteLocale, clientDataSymbol } from 'vuepress/client'
import type { PlumeThemeData } from '../../shared/index.js'
export function useThemeData(): ThemeDataRef<PlumeThemeData> {
return _useThemeData<PlumeThemeData>()
declare const __VUE_HMR_RUNTIME__: Record<string, any>
export type ThemeDataRef<T extends PlumeThemeData = PlumeThemeData> = Ref<T>
export type ThemeLocaleDataRef<T extends PlumeThemeData = PlumeThemeData> = ComputedRef<T>
export const themeLocaleDataSymbol: InjectionKey<ThemeLocaleDataRef> = Symbol(
__VUEPRESS_DEV__ ? 'themeLocaleData' : '',
)
export const themeData: ThemeDataRef = ref(themeDataRaw)
export function useThemeData<
T extends PlumeThemeData = PlumeThemeData,
>(): ThemeDataRef<T> {
return themeData as ThemeDataRef<T>
}
export function useThemeLocaleData(): ThemeLocaleDataRef<PlumeThemeData> {
return _useThemeLocaleData<PlumeThemeData>()
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateThemeData = (data: PlumeThemeData) => {
themeData.value = data
}
}
export function useThemeLocaleData<
T extends PlumeThemeData = PlumeThemeData,
>(): ThemeLocaleDataRef<T> {
const themeLocaleData = inject(themeLocaleDataSymbol)
if (!themeLocaleData) {
throw new Error('useThemeLocaleData() is called without provider.')
}
return themeLocaleData as ThemeLocaleDataRef<T>
}
/**
* Merge the locales fields to the root fields
* according to the route path
*/
function resolveThemeLocaleData(theme: PlumeThemeData, routeLocale: RouteLocale): PlumeThemeData {
const { locales, ...baseOptions } = theme
return {
...baseOptions,
...locales?.[routeLocale],
}
}
export function setupThemeData(app: App) {
// provide theme data & theme locale data
const themeData = useThemeData()
const clientData: ClientData
= app._context.provides[clientDataSymbol as unknown as symbol]
const themeLocaleData = computed(() =>
resolveThemeLocaleData(themeData.value, clientData.routeLocale.value),
)
app.provide(themeLocaleDataSymbol, themeLocaleData)
Object.defineProperties(app.config.globalProperties, {
$theme: {
get() {
return themeData.value
},
},
$themeLocale: {
get() {
return themeLocaleData.value
},
},
})
}

View File

@ -2,18 +2,20 @@ import './styles/index.css'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { enhanceScrollBehavior, setupDarkMode, setupWatermark } from './composables/index.js'
import { enhanceScrollBehavior, setupDarkMode, setupSidebar, setupThemeData, setupWatermark } from './composables/index.js'
import { globalComponents } from './globalComponents.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
export default defineClientConfig({
enhance({ app, router }) {
setupThemeData(app)
setupDarkMode(app)
enhanceScrollBehavior(router)
globalComponents(app)
},
setup() {
setupSidebar()
setupWatermark()
},
layouts: { Layout, NotFound },

View File

@ -13,3 +13,48 @@ declare module '@internal/articleTagColors' {
articleTagColors,
}
}
declare module '@internal/themePlumeData' {
import type { PlumeThemeData } from '../shared/index.js'
const themeData: PlumeThemeData
export {
themeData,
}
}
declare module '@internal/blogData' {
import type { PlumeThemeBlogPostData } from '../shared/index.js'
const blogPostData: PlumeThemeBlogPostData
export {
blogPostData,
}
}
declare module '@internal/sidebar' {
import type { Sidebar, SidebarItem } from '../shared/index.js'
const sidebar: {
__auto__: SidebarItem[]
[key: string]: Sidebar
}
export {
sidebar,
}
}
declare module '@internal/encrypt' {
const encrypt: readonly [
boolean, // global
string, // separator
string, // admin
string[], // keys
Record<string, string>, // rules
]
export {
encrypt,
}
}

View File

@ -1,5 +1,11 @@
import { resolveRoute } from 'vuepress/client'
import type { NavItemWithLink } from '../../shared/index.js'
import {
ensureEndingSlash,
ensureLeadingSlash,
isLinkAbsolute,
isLinkWithProtocol,
} from '@vuepress/helper/client'
import type { ResolvedNavItemWithLink } from '../../shared/resolved/navbar.js'
/**
* Resolve NavLink props from string
@ -8,9 +14,10 @@ import type { NavItemWithLink } from '../../shared/index.js'
* - Input: '/README.md'
* - Output: { text: 'Home', link: '/' }
*/
export function resolveNavLink(link: string): NavItemWithLink {
export function resolveNavLink(link: string): ResolvedNavItemWithLink {
const { notFound, meta, path } = resolveRoute<{
title?: string
icon?: string
}>(link)
return notFound
@ -18,5 +25,16 @@ export function resolveNavLink(link: string): NavItemWithLink {
: {
text: meta.title || path,
link: path,
icon: meta.icon,
}
}
export function normalizeLink(base = '', link = ''): string {
return isLinkAbsolute(link) || isLinkWithProtocol(link)
? link
: ensureLeadingSlash(`${base}/${link}`.replace(/\/+/g, '/'))
}
export function normalizePrefix(base: string, link = ''): string {
return ensureEndingSlash(normalizeLink(base, link))
}