diff --git a/docs/.vuepress/plume.config.ts b/docs/.vuepress/plume.config.ts
index dc5e0acd..6464a896 100644
--- a/docs/.vuepress/plume.config.ts
+++ b/docs/.vuepress/plume.config.ts
@@ -58,4 +58,5 @@ export default defineThemeConfig({
'/article/enx7c9s/': '123456',
},
},
+ autoFrontmatter: { exclude: ['**/*.snippet.*'] },
})
diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts
index 0a5ef85d..888bd636 100644
--- a/docs/.vuepress/theme.ts
+++ b/docs/.vuepress/theme.ts
@@ -5,7 +5,6 @@ import type { Theme } from 'vuepress'
export const theme: Theme = plumeTheme({
hostname: process.env.SITE_HOST || 'https://plume.pengzhanbo.cn',
plugins: {
- frontmatter: { exclude: ['**/*.snippet.*'] },
shiki: { twoslash: true },
diff --git a/theme/src/client/components/VPDoc.vue b/theme/src/client/components/VPDoc.vue
index 542c51d8..6ad59ffa 100644
--- a/theme/src/client/components/VPDoc.vue
+++ b/theme/src/client/components/VPDoc.vue
@@ -6,7 +6,7 @@ import VPDocAside from '@theme/VPDocAside.vue'
import VPDocFooter from '@theme/VPDocFooter.vue'
import VPEncryptPage from '@theme/VPEncryptPage.vue'
import VPDocMeta from '@theme/VPDocMeta.vue'
-import { usePageEncrypt } from '../composables/encrypt.js'
+import { useEncrypt } from '../composables/encrypt.js'
import { useSidebar } from '../composables/sidebar.js'
import { useData } from '../composables/data.js'
@@ -14,7 +14,7 @@ const { page, theme, frontmatter, isDark } = useData()
const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useSidebar()
-const { isPageDecrypted } = usePageEncrypt()
+const { isPageDecrypted } = useEncrypt()
const hasComments = computed(() => {
return page.value.frontmatter.comments !== false
diff --git a/theme/src/client/components/VPEncryptForm.vue b/theme/src/client/components/VPEncryptForm.vue
index a246557b..8a305973 100644
--- a/theme/src/client/components/VPEncryptForm.vue
+++ b/theme/src/client/components/VPEncryptForm.vue
@@ -1,20 +1,28 @@
@@ -12,7 +10,7 @@ const { comparePage } = usePageEncrypt()
-
+
@@ -33,11 +31,15 @@ const { comparePage } = usePageEncrypt()
width: 400px;
padding: 20px;
margin: 40px auto 0;
- background-color: var(--vp-c-bg-alt);
+ border: solid 1px var(--vp-c-divider);
border-radius: 8px;
- box-shadow: var(--vp-shadow-2);
+ box-shadow: var(--vp-shadow-1);
transition: var(--t-color);
- transition-property: box-shadow, background-color;
+ transition-property: box-shadow, border-color;
+ }
+
+ .vp-page-encrypt:hover {
+ box-shadow: var(--vp-shadow-2);
}
}
diff --git a/theme/src/client/composables/encrypt-data.ts b/theme/src/client/composables/encrypt-data.ts
index cb6dc272..a8735a4b 100644
--- a/theme/src/client/composables/encrypt-data.ts
+++ b/theme/src/client/composables/encrypt-data.ts
@@ -1,6 +1,4 @@
-import {
- encrypt as rawEncrypt,
-} from '@internal/encrypt'
+import { encrypt as rawEncrypt } from '@internal/encrypt'
import { ref } from 'vue'
import type { Ref } from 'vue'
@@ -12,16 +10,18 @@ export type EncryptConfig = readonly [
Record, // rules
]
+export interface EncryptDataRule {
+ key: string
+ match: string
+ rules: string[]
+}
+
export interface EncryptData {
global: boolean
separator: string
admins: string[]
matches: string[]
- ruleList: {
- key: string
- match: string
- rules: string[]
- }[]
+ ruleList: EncryptDataRule[]
}
export type EncryptRef = Ref
diff --git a/theme/src/client/composables/encrypt.ts b/theme/src/client/composables/encrypt.ts
index a31a7fb7..71acfa17 100644
--- a/theme/src/client/composables/encrypt.ts
+++ b/theme/src/client/composables/encrypt.ts
@@ -1,9 +1,21 @@
-import { compareSync, genSaltSync } from 'bcrypt-ts/browser'
-import { type Ref, computed } from 'vue'
+import { compare, genSaltSync } from 'bcrypt-ts/browser'
+import type { InjectionKey, Ref } from 'vue'
+import { computed, inject, provide } from 'vue'
import { hasOwn, useSessionStorage } from '@vueuse/core'
import { useRoute } from 'vuepress/client'
import { useData } from './data.js'
-import { useEncryptData } from './encrypt-data.js'
+import { type EncryptDataRule, useEncryptData } from './encrypt-data.js'
+
+export interface Encrypt {
+ hasPageEncrypt: Ref
+ isGlobalDecrypted: Ref
+ isPageDecrypted: Ref
+ hashList: Ref
+}
+
+export const EncryptSymbol: InjectionKey = Symbol(
+ __VUEPRESS_DEV__ ? 'Encrypt' : '',
+)
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => ({
s: [genSaltSync(10), genSaltSync(10)] as const,
@@ -24,23 +36,58 @@ function splitHash(hash: string) {
return hash.slice(left.length, hash.length - right.length)
}
-const cache = new Map()
-function compare(content: string, hash: string, separator = ':') {
+const compareCache = new Map()
+async function compareDecrypt(content: string, hash: string, separator = ':'): Promise {
const key = [content, hash].join(separator)
- if (cache.has(key))
- return cache.get(key)
+ if (compareCache.has(key))
+ return compareCache.get(key)!
- const result = compareSync(content, hash)
- cache.set(key, result)
- return result
+ try {
+ const result = await compare(content, hash)
+ compareCache.set(key, result)
+ return result
+ }
+ catch {
+ compareCache.set(key, false)
+ return false
+ }
}
-export function useGlobalEncrypt(): {
- isGlobalDecrypted: Ref
- compareGlobal: (password: string) => boolean
-} {
+const matchCache = new Map()
+function createMatchRegex(match: string) {
+ if (matchCache.has(match))
+ return matchCache.get(match)!
+
+ const regex = new RegExp(match)
+ matchCache.set(match, regex)
+ return regex
+}
+
+function toMatch(match: string, pagePath: string, filePathRelative: string | null) {
+ const relativePath = filePathRelative || ''
+ if (match[0] === '^') {
+ const regex = createMatchRegex(match)
+ return regex.test(pagePath) || (relativePath && regex.test(relativePath))
+ }
+ if (match.endsWith('.md'))
+ return relativePath && relativePath.endsWith(match)
+
+ return pagePath.startsWith(match) || relativePath.startsWith(match)
+}
+
+export function setupEncrypt() {
+ const { page } = useData()
+ const route = useRoute()
const encrypt = useEncryptData()
+ const hasPageEncrypt = computed(() => {
+ const pagePath = route.path
+ const filePathRelative = page.value.filePathRelative
+ return encrypt.value.ruleList.length
+ ? encrypt.value.matches.some(match => toMatch(match, pagePath, filePathRelative))
+ : false
+ })
+
const isGlobalDecrypted = computed(() => {
if (!encrypt.value.global)
return true
@@ -50,37 +97,14 @@ export function useGlobalEncrypt(): {
return !!hash && encrypt.value.admins.includes(hash)
})
- function compareGlobal(password: string) {
- if (!password)
- return false
-
- for (const admin of encrypt.value.admins) {
- if (compare(password, admin, encrypt.value.separator)) {
- storage.value.g = mergeHash(admin)
- return true
- }
- }
-
- return false
- }
-
- return {
- isGlobalDecrypted,
- compareGlobal,
- }
-}
-
-export function usePageEncrypt() {
- const { page } = useData()
- const route = useRoute()
- const encrypt = useEncryptData()
-
- const hasPageEncrypt = computed(() => encrypt.value.ruleList.length ? encrypt.value.matches.some(toMatch) : false)
-
- const hashList = computed(() => encrypt.value.ruleList.length
- ? encrypt.value.ruleList
- .filter(item => toMatch(item.match))
- : [])
+ const hashList = computed(() => {
+ const pagePath = route.path
+ const filePathRelative = page.value.filePathRelative
+ return encrypt.value.ruleList.length
+ ? encrypt.value.ruleList
+ .filter(item => toMatch(item.match, pagePath, filePathRelative))
+ : []
+ })
const isPageDecrypted = computed(() => {
if (!hasPageEncrypt.value)
@@ -101,60 +125,75 @@ export function usePageEncrypt() {
return false
})
- function toMatch(match: string) {
- const relativePath = page.value.filePathRelative || ''
- if (match[0] === '^') {
- const regex = new RegExp(match)
- return regex.test(route.path) || (relativePath && regex.test(relativePath))
- }
- if (match.endsWith('.md'))
- return relativePath && relativePath.endsWith(match)
+ provide(EncryptSymbol, {
+ hasPageEncrypt,
+ isGlobalDecrypted,
+ isPageDecrypted,
+ hashList,
+ })
+}
- return route.path.startsWith(match) || relativePath.startsWith(match)
- }
+export function useEncrypt(): Encrypt {
+ const result = inject(EncryptSymbol)
- function comparePage(password: string) {
+ if (!result)
+ throw new Error('useEncrypt() is called without setup')
+
+ return result
+}
+
+export function useEncryptCompare() {
+ const encrypt = useEncryptData()
+ const { page } = useData()
+ const route = useRoute()
+ const { hashList } = useEncrypt()
+
+ async function compareGlobal(password: string) {
if (!password)
return false
- let decrypted = false
-
- // check global
for (const admin of encrypt.value.admins) {
- if (compare(password, admin, encrypt.value.separator)) {
- decrypted = true
- storage.value.p = {
- ...storage.value.p,
- __GLOBAL__: mergeHash(admin),
- }
- break
+ if (await compareDecrypt(password, admin, encrypt.value.separator)) {
+ storage.value.g = mergeHash(admin)
+ return true
}
}
- // check page
- if (!decrypted) {
- for (const { match, key, rules } of hashList.value) {
- if (toMatch(match)) {
- for (const rule of rules) {
- if (compare(password, rule, encrypt.value.separator)) {
- decrypted = true
- storage.value.p = {
- ...storage.value.p,
- [key]: mergeHash(rule),
- }
- break
+
+ return false
+ }
+
+ async function comparePage(password: string) {
+ if (!password)
+ return false
+
+ const pagePath = route.path
+ const filePathRelative = page.value.filePathRelative
+
+ let decrypted = false
+
+ for (const { match, key, rules } of hashList.value) {
+ if (toMatch(match, pagePath, filePathRelative)) {
+ for (const rule of rules) {
+ if (await compareDecrypt(password, rule, encrypt.value.separator)) {
+ decrypted = true
+ storage.value.p = {
+ ...storage.value.p,
+ [key]: mergeHash(rule),
}
- }
- if (decrypted)
break
+ }
}
+ if (decrypted)
+ break
}
}
+ if (!decrypted) {
+ decrypted = await compareGlobal(password)
+ }
+
return decrypted
}
- return {
- isPageDecrypted,
- comparePage,
- }
+ return { compareGlobal, comparePage }
}
diff --git a/theme/src/client/config.ts b/theme/src/client/config.ts
index cef6ae7f..c1e45453 100644
--- a/theme/src/client/config.ts
+++ b/theme/src/client/config.ts
@@ -2,7 +2,14 @@ import './styles/index.css'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
-import { enhanceScrollBehavior, setupDarkMode, setupSidebar, setupThemeData, setupWatermark } from './composables/index.js'
+import {
+ enhanceScrollBehavior,
+ setupDarkMode,
+ setupEncrypt,
+ setupSidebar,
+ setupThemeData,
+ setupWatermark,
+} from './composables/index.js'
import { globalComponents } from './globalComponents.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
@@ -16,6 +23,7 @@ export default defineClientConfig({
},
setup() {
setupSidebar()
+ setupEncrypt()
setupWatermark()
},
layouts: { Layout, NotFound },
diff --git a/theme/src/client/layouts/Layout.vue b/theme/src/client/layouts/Layout.vue
index 1f9ef3ec..3a0e2fe4 100644
--- a/theme/src/client/layouts/Layout.vue
+++ b/theme/src/client/layouts/Layout.vue
@@ -11,7 +11,7 @@ import VPFooter from '@theme/VPFooter.vue'
import VPBackToTop from '@theme/VPBackToTop.vue'
import VPEncryptGlobal from '@theme/VPEncryptGlobal.vue'
import { useCloseSidebarOnEscape, useSidebar } from '../composables/sidebar.js'
-import { useGlobalEncrypt, usePageEncrypt } from '../composables/encrypt.js'
+import { useEncrypt } from '../composables/encrypt.js'
import { useData } from '../composables/data.js'
const {
@@ -21,8 +21,7 @@ const {
} = useSidebar()
const { frontmatter } = useData()
-const { isGlobalDecrypted } = useGlobalEncrypt()
-const { isPageDecrypted } = usePageEncrypt()
+const { isGlobalDecrypted, isPageDecrypted } = useEncrypt()
const route = useRoute()
watch(() => route.path, closeSidebar)
@@ -65,7 +64,11 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
-
+
diff --git a/theme/src/client/styles/icons.css b/theme/src/client/styles/icons.css
index 62389174..ab93cb45 100644
--- a/theme/src/client/styles/icons.css
+++ b/theme/src/client/styles/icons.css
@@ -89,6 +89,10 @@
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E %3Cpath fill='currentColor' d='M18 8h-1V7c0-2.757-2.243-5-5-5S7 4.243 7 7v1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2M9 7c0-1.654 1.346-3 3-3s3 1.346 3 3v1H9zm4 8.723V18h-2v-2.277c-.595-.346-1-.984-1-1.723a2 2 0 1 1 4 0c0 .738-.405 1.376-1 1.723' /%3E %3C/svg%3E");
}
+.vpi-loading {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Ccircle cx='18' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='.67' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='.33' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='0' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3C/svg%3E");
+}
+
.vpi-print {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M16 8V5H8v3H6V3h12v5zM4 10h16zm14 2.5q.425 0 .713-.288T19 11.5q0-.425-.288-.712T18 10.5q-.425 0-.712.288T17 11.5q0 .425.288.713T18 12.5M16 19v-4H8v4zm2 2H6v-4H2v-6q0-1.275.875-2.137T5 8h14q1.275 0 2.138.863T22 11v6h-4zm2-6v-4q0-.425-.288-.712T19 10H5q-.425 0-.712.288T4 11v4h2v-2h12v2z' /%3E%3C/svg%3E");
}