mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
chore: tweak
This commit is contained in:
parent
0fb444b68f
commit
2c91011697
@ -21,10 +21,6 @@ export default defineUserConfig({
|
||||
|
||||
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules'],
|
||||
|
||||
markdown: {
|
||||
code: false,
|
||||
},
|
||||
|
||||
bundler: viteBundler(),
|
||||
|
||||
theme,
|
||||
|
||||
@ -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
18
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 ''
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
45
theme/src/client/shim.d.ts
vendored
45
theme/src/client/shim.d.ts
vendored
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user