UNPKG

vuepress-theme-plume

Version:

A Blog&Document Theme for VuePress 2.0

1,508 lines (1,475 loc) 58.1 kB
import { hasOwn, tryOnScopeDispose, useDark, useEventListener, useLocalStorage, useMediaQuery, useSessionStorage, useThrottleFn, watchDebounced } from "@vueuse/core"; import { computed, customRef, inject, nextTick, onMounted, onUnmounted, onUpdated, provide, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue"; import { decodeData, ensureLeadingSlash, isArray, isLinkAbsolute, isLinkExternal, isLinkWithProtocol, isPlainObject, isString, removeLeadingSlash } from "@vuepress/helper/client"; import { clientDataSymbol, onContentUpdated, resolveRoute, resolveRouteFullPath, usePageData, usePageFrontmatter, usePageLang, useRoute, useRouteLocale, useRouter, useSiteLocaleData } from "vuepress/client"; import { inBrowser, isActive, normalizeLink, normalizePrefix, resolveEditLink, resolveNavLink, toArray } from "../utils/index.js"; import { collections } from "@internal/collectionsData"; import { ensureEndingSlash, isPlainObject as isPlainObject$1, isString as isString$1, removeEndingSlash, removeLeadingSlash as removeLeadingSlash$1 } from "vuepress/shared"; import { themeData as themeData$1 } from "@internal/themePlumeData"; import { compare, genSaltSync } from "bcrypt-ts/browser"; import { encrypt as encrypt$1 } from "@internal/encrypt"; import { sidebar } from "@internal/sidebar"; import { useContributors as useContributors$1 } from "@vuepress/plugin-git/client"; import { icons } from "@internal/iconify"; import { postsData as postsData$1 } from "@internal/postsData"; import { articleTagColors } from "@internal/articleTagColors"; import { defineWatermarkConfig } from "@vuepress/plugin-watermark/client"; //#region src/client/composables/collections.ts const collectionsRef = ref(collections); const collectionItemRef = ref(); const forceCollection = ref(); if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateCollections = (data) => { collectionsRef.value = data; }; const useCollections = () => collectionsRef; const useCollection = () => collectionItemRef; function forceUpdateCollection(dir) { forceCollection.value = dir; } function setupCollection() { const routeLocale = useRouteLocale(); const { page } = useData(); const startWith = (link) => link ? page.value.path.startsWith(normalizeLink(routeLocale.value, removeLeadingSlash$1(link))) : false; watchEffect(() => { collectionItemRef.value = collectionsRef.value[routeLocale.value]?.find((item) => { if (forceCollection.value) { if (forceCollection.value === true) return item.type === "post"; return item.dir === forceCollection.value; } if (page.value.filePathRelative) return page.value.filePathRelative?.startsWith(normalizeLink(routeLocale.value, item.dir).slice(1)); else { const { link, linkPrefix, dir, tagsLink, categoriesLink, archivesLink } = item; return startWith(link) || startWith(linkPrefix) || startWith(dir) || startWith(tagsLink) || startWith(categoriesLink) || startWith(archivesLink); } }); }); } //#endregion //#region src/client/composables/theme-data.ts const themeLocaleDataSymbol = Symbol(__VUEPRESS_DEV__ ? "themeLocaleData" : ""); const themeData = ref(themeData$1); function useThemeData() { return themeData; } if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateThemeData = (data) => { themeData.value = data; }; function useThemeLocaleData() { const themeLocaleData = inject(themeLocaleDataSymbol); if (!themeLocaleData) throw new Error("useThemeLocaleData() is called without provider."); return themeLocaleData; } /** * Merge the locales fields to the root fields * according to the route path */ function resolveThemeLocaleData(theme, routeLocale) { const { locales, ...baseOptions } = theme; return { ...baseOptions, ...locales?.[routeLocale] }; } function setupThemeData(app) { const themeData$2 = useThemeData(); const clientData = app._context.provides[clientDataSymbol]; const themeLocaleData = computed(() => resolveThemeLocaleData(themeData$2.value, clientData.routeLocale.value)); app.provide(themeLocaleDataSymbol, themeLocaleData); Object.defineProperties(app.config.globalProperties, { $theme: { get() { return themeData$2.value; } }, $themeLocale: { get() { return themeLocaleData.value; } } }); } //#endregion //#region src/client/composables/dark-mode.ts const darkModeSymbol = Symbol(__VUEPRESS_DEV__ ? "darkMode" : ""); function enableTransitions() { if (typeof document === "undefined") return false; return "startViewTransition" in document && window.matchMedia("(prefers-reduced-motion: no-preference)").matches; } function setupDarkMode(app) { const theme = useThemeData(); const transition = theme.value.transition; const disableTransition = enableTransitions() || (typeof transition === "object" ? transition.appearance === false : transition === false); const appearance = theme.value.appearance; const isDark = appearance === "force-dark" ? ref(true) : appearance ? useDark({ storageKey: "vuepress-theme-appearance", attribute: "data-theme", valueLight: "light", valueDark: "dark", disableTransition, initialValue: () => typeof appearance === "string" ? appearance : "auto", ...typeof appearance === "object" ? appearance : {} }) : ref(false); app.provide(darkModeSymbol, isDark); if (__VUEPRESS_DEV__ && appearance === "force-dark" && typeof document !== "undefined") document.documentElement.dataset.theme = "dark"; Object.defineProperty(app.config.globalProperties, "$isDark", { get: () => isDark }); useEventListener("beforeprint", () => { if (isDark.value) document.documentElement.dataset.theme = "light"; }); useEventListener("afterprint", () => { if (isDark.value) document.documentElement.dataset.theme = "dark"; }); } /** * Inject dark mode global computed */ function useDarkMode() { const isDarkMode = inject(darkModeSymbol); if (!isDarkMode) throw new Error("useDarkMode() is called without provider."); return isDarkMode; } //#endregion //#region src/client/composables/data.ts function useData() { const theme = useThemeLocaleData(); const page = usePageData(); const frontmatter = usePageFrontmatter(); const site = useSiteLocaleData(); const isDark = useDarkMode(); return { theme, page, frontmatter, lang: usePageLang(), site, isDark, collection: useCollection() }; } //#endregion //#region src/client/composables/encrypt-data.ts const encrypt = ref(resolveEncryptData(encrypt$1)); function useEncryptData() { return encrypt; } function resolveEncryptData([global, separator, admin, matches, rules]) { const keys = matches.map((match) => decodeData(match)); return { global, separator, matches: keys, admins: admin.split(separator), ruleList: Object.keys(rules).map((key) => ({ key, match: keys[key], rules: rules[key].split(separator) })) }; } if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateEncrypt = (data) => { encrypt.value = resolveEncryptData(data); }; //#endregion //#region src/client/composables/encrypt.ts const EncryptSymbol = Symbol(__VUEPRESS_DEV__ ? "Encrypt" : ""); const storage = useSessionStorage("2a0a3d6afb2fdf1f", () => { if (__VUEPRESS_SSR__) return { s: ["", ""], g: "", p: {} }; return { s: [genSaltSync(10), genSaltSync(10)], g: "", p: {} }; }); function mergeHash(hash) { const [left, right] = storage.value.s; return left + hash + right; } function splitHash(hash) { const [left, right] = storage.value.s; if (!hash.startsWith(left) || !hash.endsWith(right)) return ""; return hash.slice(left.length, hash.length - right.length); } const compareCache = /* @__PURE__ */ new Map(); async function compareDecrypt(content, hash, separator = ":") { const key = [content, hash].join(separator); if (compareCache.has(key)) return compareCache.get(key); try { const result = await compare(content, hash); compareCache.set(key, result); return result; } catch { compareCache.set(key, false); return false; } } const matchCache = /* @__PURE__ */ new Map(); function createMatchRegex(match) { if (matchCache.has(match)) return matchCache.get(match); const regex = new RegExp(match); matchCache.set(match, regex); return regex; } function toMatch(match, pagePath, filePathRelative) { const relativePath = filePathRelative || ""; if (match[0] === "^") { const regex = createMatchRegex(match); return regex.test(pagePath) || regex.test(relativePath); } if (match.endsWith(".md")) return relativePath && relativePath.endsWith(match); return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash$1(match)); } function setupEncrypt() { const { page } = useData(); const route = useRoute(); const encrypt$2 = useEncryptData(); const hasPageEncrypt = computed(() => { const pagePath = route.path; const filePathRelative = page.value.filePathRelative; if (page.value._e) return true; return encrypt$2.value.ruleList.length ? encrypt$2.value.matches.some((match) => toMatch(match, pagePath, filePathRelative)) : false; }); const isGlobalDecrypted = computed(() => { if (!encrypt$2.value.global) return true; const hash = splitHash(storage.value.g); return !!hash && encrypt$2.value.admins.includes(hash); }); const hashList = computed(() => { const pagePath = route.path; const filePathRelative = page.value.filePathRelative; const passwords = typeof page.value._e === "string" ? page.value._e.split(":") : []; return [passwords.length ? { key: pagePath.replace(/\//g, "").replace(/\.html$/, ""), match: pagePath, rules: passwords } : void 0, ...encrypt$2.value.ruleList.length ? encrypt$2.value.ruleList.filter((item) => toMatch(item.match, pagePath, filePathRelative)) : []].filter(Boolean); }); provide(EncryptSymbol, { hasPageEncrypt, isGlobalDecrypted, isPageDecrypted: computed(() => { if (!hasPageEncrypt.value) return true; const hash = splitHash(storage.value.g || ""); if (hash && encrypt$2.value.admins.includes(hash)) return true; for (const { key, rules } of hashList.value) if (hasOwn(storage.value.p, key)) { const hash$1 = splitHash(storage.value.p[key]); if (hash$1 && rules.includes(hash$1)) return true; } return false; }), hashList }); } function useEncrypt() { const result = inject(EncryptSymbol); if (!result) throw new Error("useEncrypt() is called without setup"); return result; } function useEncryptCompare() { const encrypt$2 = useEncryptData(); const { page } = useData(); const route = useRoute(); const { hashList } = useEncrypt(); async function compareGlobal(password) { if (!password) return false; for (const admin of encrypt$2.value.admins) if (await compareDecrypt(password, admin, encrypt$2.value.separator)) { storage.value.g = mergeHash(admin); return true; } return false; } async function comparePage(password) { 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$2.value.separator)) { decrypted = true; storage.value.p = { ...storage.value.p, [key]: mergeHash(rule) }; break; } if (decrypted) break; } if (!decrypted) decrypted = await compareGlobal(password); return decrypted; } return { compareGlobal, comparePage }; } //#endregion //#region src/client/composables/sidebar-data.ts const { __auto__, __home__, ...items } = sidebar; const sidebarData = ref(items); const autoDirSidebar = ref(__auto__); const autoHomeData = ref(__home__); if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateSidebar = (data) => { const { __auto__: __auto__$1, __home__: __home__$1, ...items$1 } = data; sidebarData.value = items$1; autoDirSidebar.value = __auto__$1; autoHomeData.value = __home__$1; }; const sidebarSymbol = Symbol(__VUEPRESS_DEV__ ? "sidebar" : ""); function setupSidebar() { const { page, frontmatter } = useData(); const routeLocale = useRouteLocale(); const hasSidebar = computed(() => { return frontmatter.value.pageLayout !== "home" && frontmatter.value.pageLayout !== "friends" && frontmatter.value.sidebar !== false && frontmatter.value.layout !== "NotFound"; }); provide(sidebarSymbol, computed(() => { return hasSidebar.value ? getSidebar(typeof frontmatter.value.sidebar === "string" ? frontmatter.value.sidebar : page.value.path, routeLocale.value) : []; })); } function useSidebarData() { const sidebarData$1 = inject(sidebarSymbol); if (!sidebarData$1) throw new Error("useSidebarData() is called without provider."); return sidebarData$1; } /** * 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. */ function getSidebar(routePath, routeLocal) { 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)) { routePath = decodeURIComponent(routePath); const dir = Object.keys(_sidebar).sort((a, b) => b.split("/").length - a.split("/").length).find((dir$1) => { return routePath.startsWith(`${routeLocal}${removeLeadingSlash(dir$1)}`); }) || ""; const sidebar$1 = dir ? _sidebar[dir] : void 0; if (sidebar$1 === "auto") return resolveSidebarItems(dir ? autoDirSidebar.value[dir] : [], routeLocal); else if (isArray(sidebar$1)) return resolveSidebarItems(sidebar$1, dir); else if (isPlainObject(sidebar$1)) { const prefix = normalizePrefix(routeLocal, sidebar$1.prefix); return resolveSidebarItems(sidebar$1.items === "auto" ? autoDirSidebar.value[prefix] : sidebar$1.items, prefix); } } return []; } function resolveSidebarItems(sidebarItems, _prefix = "") { const resolved = []; sidebarItems.forEach((item) => { if (isString(item)) resolved.push(resolveNavLink(normalizeLink(_prefix, item))); else { const { link, items: items$1, prefix, dir, ...args } = item; const navLink = { ...args }; if (link) { navLink.link = link.startsWith("---") ? link : normalizeLink(_prefix, link); const nav = resolveNavLink(navLink.link); navLink.icon = nav.icon || navLink.icon; navLink.badge = nav.badge || navLink.badge; } const nextPrefix = normalizePrefix(_prefix, prefix || dir); if (items$1 === "auto") { navLink.items = resolveSidebarItems(autoDirSidebar.value[nextPrefix], nextPrefix); if (!navLink.link && autoHomeData.value[nextPrefix]) { navLink.link = normalizeLink(autoHomeData.value[nextPrefix]); const nav = resolveNavLink(navLink.link); navLink.icon = nav.icon || navLink.icon; navLink.badge = nav.badge || navLink.badge; } } else navLink.items = items$1?.length ? resolveSidebarItems(items$1, nextPrefix) : void 0; resolved.push(navLink); } }); return resolved; } /** * Get or generate sidebar group from the given sidebar items. */ function getSidebarGroups(sidebar$1) { const groups = []; let lastGroupIndex = 0; for (const index in sidebar$1) { const item = sidebar$1[index]; if (item.items) { lastGroupIndex = groups.push(item); continue; } if (!groups[lastGroupIndex]) groups.push({ items: [] }); groups[lastGroupIndex].items.push(item); } return groups; } function getSidebarFirstLink(sidebar$1) { for (const item of sidebar$1) { if (item.link) return item.link; if (item.items) return getSidebarFirstLink(item.items); } return ""; } //#endregion //#region src/client/composables/sidebar.ts /** * Check if the given sidebar item contains any active link. */ function hasActiveLink(path, items$1) { if (Array.isArray(items$1)) return items$1.some((item) => hasActiveLink(path, item)); return isActive(path, items$1.link ? resolveRouteFullPath(items$1.link) : void 0) ? true : items$1.items ? hasActiveLink(path, items$1.items) : false; } const containsActiveLink = hasActiveLink; function useSidebar() { const { theme, frontmatter, page } = useData(); const routeLocal = useRouteLocale(); const is960 = useMediaQuery("(min-width: 960px)"); const { isPageDecrypted } = useEncrypt(); 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$1 = useSidebarData(); const hasSidebar = computed(() => { return frontmatter.value.sidebar !== false && sidebar$1.value.length > 0 && frontmatter.value.pageLayout !== "home"; }); const hasAside = computed(() => { if (frontmatter.value.pageLayout === "home" || frontmatter.value.home) return false; if (frontmatter.value.pageLayout === "friends" || frontmatter.value.friends) return false; if (!isPageDecrypted.value) return false; if (frontmatter.value.aside != null) return !!frontmatter.value.aside; return theme.value.aside !== false; }); const leftAside = computed(() => { if (hasAside.value) return frontmatter.value.aside == null ? theme.value.aside === "left" : frontmatter.value.aside === "left"; return false; }); const isSidebarEnabled = computed(() => hasSidebar.value && is960.value); const sidebarGroups = computed(() => { return hasSidebar.value ? getSidebarGroups(sidebar$1.value) : []; }); const open = () => { isOpen.value = true; }; const close = () => { isOpen.value = false; }; const toggle = () => { if (isOpen.value) close(); else open(); }; return { isOpen, sidebar: sidebar$1, sidebarKey, sidebarGroups, hasSidebar, hasAside, leftAside, isSidebarEnabled, open, close, toggle }; } /** * a11y: cache the element that opened the Sidebar (the menu button) then * focus that button again when Menu is closed with Escape key. */ function useCloseSidebarOnEscape(isOpen, close) { let triggerElement; watchEffect(() => { triggerElement = isOpen.value ? document.activeElement : void 0; }); onMounted(() => { window.addEventListener("keyup", onEscape); }); onUnmounted(() => { window.removeEventListener("keyup", onEscape); }); function onEscape(e) { if (e.key === "Escape" && isOpen.value) { close(); triggerElement?.focus(); } } } function useSidebarControl(item) { const { page } = useData(); const route = useRoute(); const collapsed = ref(false); const collapsible = computed(() => { return item.value.collapsed != null; }); const isLink = computed(() => { return !!item.value.link; }); const isActiveLink = ref(false); const updateIsActiveLink = () => { isActiveLink.value = isActive(page.value.path, item.value.link ? resolveRouteFullPath(item.value.link) : void 0); }; watch([ () => page.value.path, item, () => route.hash ], updateIsActiveLink); onMounted(updateIsActiveLink); const hasActiveLink$1 = computed(() => { if (isActiveLink.value) return true; return item.value.items ? containsActiveLink(page.value.path, item.value.items) : false; }); const hasChildren = computed(() => { return !!(item.value.items && item.value.items.length); }); watch(() => [collapsible.value, item.value.collapsed], (n, o) => { if (n[0] !== o?.[0] || n[1] !== o?.[1]) collapsed.value = !!(collapsible.value && item.value.collapsed); }, { immediate: true }); watch(() => [ page.value.path, isActiveLink.value, hasActiveLink$1.value ], () => { if (isActiveLink.value || hasActiveLink$1.value) collapsed.value = false; }, { immediate: true, flush: "post" }); const toggle = () => { if (collapsible.value) collapsed.value = !collapsed.value; }; return { collapsed, collapsible, isLink, isActiveLink, hasActiveLink: hasActiveLink$1, hasChildren, toggle }; } //#endregion //#region src/client/composables/aside.ts function useAside() { const { hasSidebar } = useSidebar(); const is960 = useMediaQuery("(min-width: 960px)"); const is1280 = useMediaQuery("(min-width: 1280px)"); return { isAsideEnabled: computed(() => { if (!is1280.value && !is960.value) return false; return hasSidebar.value ? is1280.value : is960.value; }) }; } //#endregion //#region src/client/composables/bulletin.ts const showBulletin = ref(false); function useBulletin() { const { theme } = useData(); return computed(() => theme.value.bulletin === true ? {} : theme.value.bulletin); } function useBulletinControl() { const session = useSessionStorage("plume:bulletin", ""); const local = useLocalStorage("plume:bulletin", ""); const { page } = useData(); const bulletin = useBulletin(); const enableBulletin = computed(() => page.value.bulletin ?? true); watch(() => bulletin.value?.lifetime, (lifetime) => { const id = bulletin.value?.id; if (lifetime === "session") showBulletin.value = session.value !== id; else if (lifetime === "once") showBulletin.value = local.value !== id; else showBulletin.value = true; }, { immediate: true }); function close() { showBulletin.value = false; const lifetime = bulletin.value?.lifetime; const id = bulletin.value?.id; if (lifetime === "session") session.value = id; else if (lifetime === "once") local.value = id; } return { bulletin, enableBulletin, showBulletin, close }; } //#endregion //#region src/client/composables/contributors.ts function useContributors() { const { frontmatter } = useData(); const list = useContributors$1(); const theme = useThemeData(); const mode = computed(() => { const config = theme.value.contributors; if (isPlainObject$1(config)) return config.mode || "inline"; return "inline"; }); const contributors = computed(() => { if ((frontmatter.value.contributors ?? !!theme.value.contributors) === false) return []; return list.value; }); return { mode, contributors, hasContributors: computed(() => contributors.value.length > 0) }; } //#endregion //#region src/client/composables/preset-locales.ts const presetLocales = __PLUME_PRESET_LOCALE__; function getPresetLocaleData(locale, name) { return presetLocales[locale]?.[name] || presetLocales["/"][name]; } //#endregion //#region src/client/composables/copyright.ts const LICENSE_URL = { "CC0": { url: "https://creativecommons.org/publicdomain/zero/1.0/", icons: ["zero"] }, "CC-BY-4.0": { url: "https://creativecommons.org/licenses/by/4.0/", icons: ["cc", "by"] }, "CC-BY-NC-4.0": { url: "https://creativecommons.org/licenses/by-nc/4.0/", icons: [ "cc", "by", "nc" ] }, "CC-BY-NC-SA-4.0": { url: "https://creativecommons.org/licenses/by-nc-sa/4.0/", icons: [ "cc", "by", "nc", "sa" ] }, "CC-BY-NC-ND-4.0": { url: "https://creativecommons.org/licenses/by-nc-nd/4.0/", icons: [ "cc", "by", "nc", "nd" ] }, "CC-BY-ND-4.0": { url: "https://creativecommons.org/licenses/by-nd/4.0/", icons: [ "cc", "by", "nd" ] }, "CC-BY-SA-4.0": { url: "https://creativecommons.org/licenses/by-sa/4.0/", icons: [ "cc", "by", "sa" ] } }; function useCopyright(copyright) { const { theme } = useData(); const routeLocale = useRouteLocale(); const { contributors } = useContributors(); const hasCopyright = computed(() => Boolean(copyright.value)); const creation = computed(() => copyright.value.creation || "original"); const license = computed(() => resolveLicense(copyright.value.license, routeLocale.value)); const author = computed(() => resolveAuthor(copyright.value.author, creation.value, contributors.value)); const sourceUrl = computed(() => { if (creation.value === "original") { if (__VUEPRESS_SSR__) return ""; const url = new URL(location.href.split("#")[0]); url.searchParams.delete("giscus"); return url.toString(); } return copyright.value.source; }); return { license, author, hasCopyright, creation, creationText: computed(() => { const creation$1 = copyright.value.creation; if (creation$1 === "translate") return theme.value.copyrightCreationTranslateText || "This article is translated from"; else if (creation$1 === "reprint") return theme.value.copyrightCreationReprintText || "This article is reprint from"; return theme.value.copyrightCreationOriginalText || "This article link: "; }), sourceUrl }; } function resolveLicense(license = "CC-BY-4.0", locale) { const result = typeof license === "string" ? { name: license } : { ...license }; const fallback = LICENSE_URL[result.name]; const name = getPresetLocaleData(locale, result.name); if (name) result.name = `${name} (${result.name})`; result.url ||= fallback?.url; result.icons = fallback?.icons; return result; } function resolveAuthor(author, creation, contributors) { const contributor = contributors[0]; if (!author && contributor && creation === "original") return contributor; const options = typeof author === "string" ? { name: author } : author; if (options && !options.url) { const contributor$1 = contributors.find((c) => c.name === options.name); if (contributor$1) options.url = contributor$1.url; } return options; } //#endregion //#region src/client/composables/css-var.ts /** * Get css variable * @param prop css variable name * @param initialValue */ function useCssVar(prop, initialValue = "") { const isDark = useDarkMode(); const variable = shallowRef(initialValue); function updateCssVar() { const _window = typeof window ? window : null; const target = _window?.document?.documentElement; const key = toValue(prop); if (target && key) variable.value = _window.getComputedStyle(target).getPropertyValue(key)?.trim() || variable.value || initialValue; } watch([isDark, () => toValue(prop)], () => { updateCssVar(); }, { immediate: true, flush: "post" }); return computed(() => variable.value); } //#endregion //#region src/client/composables/edit-link.ts function useEditLink() { const { theme, page, frontmatter } = useData(); const themeData$2 = useThemeData(); return computed(() => { if (!(frontmatter.value.editLink ?? themeData$2.value.editLink ?? true)) return null; const { docsRepo, docsBranch = "main", docsDir = "" } = themeData$2.value; const { editLinkText } = theme.value; if (!docsRepo) return null; const editLink = resolveEditLink({ docsRepo, docsBranch, docsDir, filePathRelative: page.value.filePathRelative, editLinkPattern: frontmatter.value.editLinkPattern ?? theme.value.editLinkPattern }); if (!editLink) return null; return { text: editLinkText ?? "Edit this page", link: editLink }; }); } //#endregion //#region src/client/composables/flyout.ts const focusedElement = ref(); let active = false; let listeners = 0; function useFlyout(options) { const focus = ref(false); if (inBrowser) { if (!active) activateFocusTracking(); listeners++; const unwatch = watch(focusedElement, (el) => { if (el === options.el.value || options.el.value?.contains(el)) { focus.value = true; options.onFocus?.(); } else { focus.value = false; options.onBlur?.(); } }); onUnmounted(() => { unwatch(); listeners--; if (!listeners) deactivateFocusTracking(); }); } return readonly(focus); } function activateFocusTracking() { document.addEventListener("focusin", handleFocusIn); active = true; focusedElement.value = document.activeElement; } function deactivateFocusTracking() { document.removeEventListener("focusin", handleFocusIn); } function handleFocusIn() { focusedElement.value = document.activeElement; } //#endregion //#region src/client/composables/icons.ts const iconsData = ref(icons); const useIconsData = () => iconsData; if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateIcons = (data) => { iconsData.value = data; }; //#endregion //#region src/client/utils/resolveNavLink.ts function normalizeLink$1(base = "", link = "") { return isLinkAbsolute(link) || isLinkWithProtocol(link) ? link : ensureLeadingSlash(`${base}/${link}`.replace(/\/+/g, "/")); } //#endregion //#region src/client/composables/internal-link.ts function useInternalLink() { const { collection, theme } = useData(); const themeData$2 = useThemeData(); const routeLocale = useRouteLocale(); function resolveLink(link, fallback) { link = link ? removeLeadingSlash$1(link) : ""; return ensureEndingSlash(normalizeLink$1(routeLocale.value, link || fallback)); } const postCollection = computed(() => collection.value?.type === "post" ? collection.value : void 0); const home = computed(() => ({ link: normalizeLink$1(routeLocale.value), text: theme.value.homeText || themeData$2.value.homeText || "Home" })); const postsLink = computed(() => normalizeLink$1(routeLocale.value, resolveLink(postCollection.value?.link || postCollection.value?.dir, "posts/"))); return { home, posts: computed(() => postCollection.value?.postList !== false ? { text: postCollection.value?.title || removeEndingSlash(postCollection.value?.dir || "").split("/").pop() || theme.value.postsText, link: postsLink.value } : void 0), tags: computed(() => postCollection.value?.tags !== false ? { text: postCollection.value?.tagsText || theme.value.tagText || themeData$2.value.tagText || "Tags", link: resolveLink(postCollection.value?.tagsLink, "tags/") } : void 0), archive: computed(() => postCollection.value?.archives !== false ? { text: postCollection.value?.archivesText || theme.value.archiveText || themeData$2.value.archiveText || "Archives", link: resolveLink(postCollection.value?.archivesLink, "archives/") } : void 0), categories: computed(() => postCollection.value?.categories !== false ? { text: postCollection.value?.categoriesText || theme.value.categoryText || themeData$2.value.categoryText || "Categories", link: resolveLink(postCollection.value?.categoriesLink, "categories/") } : void 0) }; } //#endregion //#region src/client/composables/page.ts function usePostsPageData() { const { collection, page } = useData(); return { isPosts: computed(() => collection.value?.type === "post"), isPostsLayout: computed(() => { const type = page.value.type; return type === "posts" || type === "posts-archives" || type === "posts-tags" || type === "posts-categories"; }) }; } //#endregion //#region src/client/composables/langs.ts function useLangs({ removeCurrent = true } = {}) { const theme = useThemeData(); const { page, collection } = useData(); const routeLocale = useRouteLocale(); const { isPosts } = usePostsPageData(); const currentLang = computed(() => { const link = routeLocale.value; return { text: theme.value.locales?.[link]?.selectLanguageName, link }; }); const resolvePath = (locale, url) => { const { notFound, path } = resolveRoute(normalizeLink(locale, url.slice(routeLocale.value.length))); return notFound ? void 0 : path; }; const getPageLink = (locale) => { let path; if (page.value.filePathRelative) path = resolvePath(locale, `/${page.value.filePathRelative}`); path ??= resolvePath(locale, page.value.path); if (path) return path; if (isPosts.value && collection.value) { const col = collection.value; return normalizeLink(locale, removeLeadingSlash$1(col.link || col.dir)); } const home = theme.value.home || "/"; const fallbackResolve = resolveRoute(locale); return fallbackResolve.notFound ? home : fallbackResolve.path; }; return { localeLinks: computed(() => Object.entries(theme.value.locales || {}).flatMap(([key, locale]) => removeCurrent && currentLang.value.text === locale.selectLanguageName ? [] : { text: locale.selectLanguageName, link: getPageLink(key) })), currentLang }; } //#endregion //#region src/client/composables/latest-updated.ts function useLastUpdated() { const { theme, page, frontmatter } = useData(); const themeData$2 = useThemeData(); const lang = usePageLang(); const date = computed(() => page.value.git?.updatedTime ? new Date(page.value.git.updatedTime) : null); const isoDatetime = computed(() => date.value?.toISOString()); const datetime = ref(""); const lastUpdatedText = computed(() => { if (themeData$2.value.lastUpdated === false) return ""; return theme.value.lastUpdatedText || "Last updated"; }); onMounted(() => { watchEffect(() => { if (frontmatter.value.lastUpdated === false || themeData$2.value.lastUpdated === false) return; datetime.value = date.value ? new Intl.DateTimeFormat(themeData$2.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : void 0, themeData$2.value.lastUpdated?.formatOptions ?? { dateStyle: "short", timeStyle: "short" }).format(date.value) : ""; }); }); return { datetime, isoDatetime, lastUpdatedText }; } //#endregion //#region src/client/composables/link.ts function useLink(href, target) { const route = useRoute(); const { page } = useData(); const isExternal = computed(() => { const link$1 = toValue(href); const rawTarget = toValue(target); if (!link$1) return false; if (rawTarget === "_blank" || isLinkExternal(link$1)) return true; const filename = link$1.split(/[#?]/)[0]?.split("/").pop() || ""; if (filename === "" || filename.endsWith(".html") || filename.endsWith(".md")) return false; return filename.includes("."); }); const link = computed(() => { const link$1 = toValue(href); if (!link$1) return void 0; if (isExternal.value) return link$1; const path = resolveRouteFullPath(link$1, page.value.filePathRelative ? `/${page.value.filePathRelative}` : void 0); if (path.includes("#")) { if (path.slice(0, path.indexOf("#")) === route.path) return path.slice(path.indexOf("#")); } return path; }); return { isExternal, isExternalProtocol: computed(() => { if (!link.value || link.value[0] === "#") return false; return isLinkWithProtocol(link.value); }), link }; } //#endregion //#region src/client/composables/nav.ts function useNavbarData() { const { theme } = useData(); return computed(() => resolveNavbar(theme.value.navbar || [])); } function resolveNavbar(navbar, _prefix = "") { const resolved = []; navbar.forEach((item) => { if (typeof item === "string") resolved.push(resolveNavLink(normalizeLink(_prefix, item))); else { const { items: items$1, prefix, ...args } = item; const res = { ...args }; if ("link" in res) res.link = normalizeLink(_prefix, res.link); if (items$1?.length) res.items = resolveNavbar(items$1, normalizeLink(_prefix, prefix)); resolved.push(res); } }); return resolved; } function useNav() { const isScreenOpen = ref(false); function openScreen() { isScreenOpen.value = true; window.addEventListener("resize", closeScreenOnTabletWindow); } function closeScreen() { isScreenOpen.value = false; window.removeEventListener("resize", closeScreenOnTabletWindow); } function toggleScreen() { if (isScreenOpen.value) closeScreen(); else openScreen(); } /** * Close screen when the user resizes the window wider than tablet size. */ function closeScreenOnTabletWindow() { if (window.outerWidth >= 768) closeScreen(); } const route = useRoute(); watch(() => route.path, closeScreen); return { isScreenOpen, openScreen, closeScreen, toggleScreen }; } //#endregion //#region src/client/composables/outline.ts const resolvedHeaders = []; const headersSymbol = Symbol(__VUEPRESS_DEV__ ? "headers" : ""); function setupHeaders() { const { frontmatter, theme } = useData(); const headers = ref([]); onContentUpdated(() => { headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline); }); provide(headersSymbol, headers); return headers; } function useHeaders() { const headers = inject(headersSymbol); if (!headers) throw new Error("useHeaders() is called without provider."); return headers; } function getHeaders(range) { const heading = [ "h1", "h2", "h3", "h4", "h5", "h6" ]; const ignores = Array.from(document.querySelectorAll(heading.map((h) => `.vp-demo-wrapper ${h}`).join(","))); return resolveHeaders(Array.from(document.querySelectorAll(heading.map((h) => `.vp-doc ${h}`).join(","))).filter((el) => !ignores.includes(el) && el.id && el.hasChildNodes()).map((el) => { const level = Number(el.tagName[1]); return { element: el, title: serializeHeader(el), link: `#${el.id}`, level }; }), range); } function serializeHeader(h) { const anchor = h.firstChild; const el = anchor?.firstChild; let ret = ""; for (const node of Array.from(el?.childNodes ?? [])) if (node.nodeType === 1) { if (node.classList.contains("vp-badge") || node.classList.contains("ignore-header")) continue; const clone = node.cloneNode(true); clearHeaderNodeList(Array.from(clone.childNodes)); ret += clone.textContent; } else if (node.nodeType === 3) ret += node.textContent; let next = anchor?.nextSibling; while (next) { if (next.nodeType === 1 || next.nodeType === 3) ret += next.textContent; next = next.nextSibling; } return ret.trim(); } function clearHeaderNodeList(list) { if (list?.length) { for (const node of list) if (node.nodeType === 1) if (node.classList.contains("ignore-header")) node.remove(); else clearHeaderNodeList(Array.from(node.childNodes)); } } function resolveHeaders(headers, range) { if (range === false) return []; const levelsRange = range || 2; const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange; headers = headers.filter((h) => h.level >= high && h.level <= low); resolvedHeaders.length = 0; for (const { element, link } of headers) resolvedHeaders.push({ element, link }); const ret = []; outer: for (let i = 0; i < headers.length; i++) { const cur = headers[i]; if (i === 0) ret.push(cur); else { for (let j = i - 1; j >= 0; j--) { const prev = headers[j]; if (prev.level < cur.level) { (prev.children || (prev.children = [])).push(cur); continue outer; } } ret.push(cur); } } return ret; } function useActiveAnchor(container, marker) { const { isAsideEnabled } = useAside(); const router = useRouter(); const routeHash = ref(router.currentRoute.value.hash); let prevActiveLink = null; const setActiveLink = () => { if (!isAsideEnabled.value) return; const scrollY = Math.round(window.scrollY); const innerHeight$1 = window.innerHeight; const offsetHeight = document.body.offsetHeight; const isBottom = Math.abs(scrollY + innerHeight$1 - offsetHeight) < 1; const headers = resolvedHeaders.map(({ element, link }) => ({ link, top: getAbsoluteTop(element) })).filter(({ top }) => !Number.isNaN(top)).sort((a, b) => a.top - b.top); if (!headers.length) { activateLink(null); return; } if (scrollY < 1) { activateLink(null); return; } if (isBottom) { activateLink(headers[headers.length - 1].link); return; } let activeLink = null; for (const { link, top } of headers) { if (top > scrollY + 80) break; activeLink = link; } activateLink(activeLink); }; function activateLink(hash) { routeHash.value = hash || ""; if (prevActiveLink) prevActiveLink.classList.remove("active"); if (hash == null) prevActiveLink = null; else prevActiveLink = container.value?.querySelector(`a[href="${decodeURIComponent(hash)}"]`) ?? null; const activeLink = prevActiveLink; if (activeLink) { activeLink.classList.add("active"); if (marker.value) { marker.value.style.top = `${activeLink.offsetTop + 39}px`; marker.value.style.opacity = "1"; } } else if (marker.value) { marker.value.style.top = "33px"; marker.value.style.opacity = "0"; } } const onScroll = useThrottleFn(setActiveLink, 100); watchDebounced(routeHash, () => { updateHash(router, routeHash.value); }, { debounce: 500 }); onMounted(() => { setTimeout(() => { setActiveLink(); window.addEventListener("scroll", onScroll); }, 1e3); }); onUpdated(() => { activateLink(location.hash); }); onUnmounted(() => { window.removeEventListener("scroll", onScroll); }); } function getAbsoluteTop(element) { let offsetTop = 0; while (element && element !== document.body) { if (window.getComputedStyle(element).position === "fixed") return element.offsetTop; offsetTop += element.offsetTop; element = element.offsetParent; } return element ? offsetTop : NaN; } /** * Update current hash and do not trigger `scrollBehavior` */ async function updateHash(router, hash) { const { path, query } = router.currentRoute.value; const { scrollBehavior } = router.options; router.options.scrollBehavior = void 0; await router.replace({ path, query, hash }); router.options.scrollBehavior = scrollBehavior; } //#endregion //#region src/client/composables/posts-data.ts const postsData = ref(postsData$1); function usePostsData() { return postsData; } function useLocalePostList() { const collection = useCollection(); const routeLocale = useRouteLocale(); return computed(() => { if (collection.value) return postsData.value[normalizeLink$1(routeLocale.value, ensureEndingSlash(removeLeadingSlash$1(collection.value.dir)))] || []; return []; }); } if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updatePostsData = (data) => { postsData.value = data; }; //#endregion //#region src/client/composables/posts-archives.ts function useArchives() { const themeData$2 = useThemeData(); const list = useLocalePostList(); const { theme } = useData(); return { archives: computed(() => { const archives = []; const countLocale = theme.value.archiveTotalText || themeData$2.value.archiveTotalText; list.value.forEach((item) => { const createTime = item.createTime?.split(/\s|T/)[0] || ""; const year = createTime.split("/")[0]; let current = archives.find((archive) => archive.title === year); if (!current) { current = { title: year, list: [], label: "" }; archives.push(current); } current.list.push({ title: item.title, path: item.path, createTime: createTime.slice(year.length + 1).replace(/\//g, "-") }); }); archives.forEach((item) => { item.label = countLocale?.replace("{count}", item.list.length.toString()) || ""; }); return archives; }) }; } //#endregion //#region src/client/composables/posts-category.ts function usePostsCategory() { const postList = useLocalePostList(); return { categories: computed(() => { const list = []; postList.value.forEach((item) => { const categoryList = item.categoryList; if (!categoryList || categoryList.length === 0) list.push({ type: "post", title: item.title, path: item.path }); else { let cate = list; let i = 0; while (i < categoryList.length) { const { id, name, sort } = categoryList[i]; const current = cate.find((item$1) => item$1.type === "category" && item$1.id === id); if (!current) { const items$1 = []; cate.push({ type: "category", title: name, id, sort, items: items$1 }); cate = items$1; } else cate = current.items; i++; } cate.push({ type: "post", title: item.title, path: item.path }); } }); return sortCategory(list); }) }; } function sortCategory(items$1) { for (const item of items$1) if (item.type === "category" && item.items.length) item.items = sortCategory(item.items); return items$1.sort((a, b) => { if (a.type === "category" && b.type === "category") return a.sort < b.sort ? -1 : 1; if (a.type === "category" && b.type === "post") return -1; if (a.type === "post" && b.type === "category") return 1; return 0; }); } //#endregion //#region src/client/composables/route-query.ts const _queue = /* @__PURE__ */ new WeakMap(); function useRouteQuery(name, defaultValue, options = {}) { const { mode = "replace", route = useRoute(), router = useRouter(), transform = (value) => value } = options; if (!_queue.has(router)) _queue.set(router, /* @__PURE__ */ new Map()); const _queriesQueue = _queue.get(router); let query = route.query[name]; tryOnScopeDispose(() => { query = void 0; }); let _trigger; const proxy = customRef((track, trigger) => { _trigger = trigger; return { get() { track(); return transform(query !== void 0 ? query : toValue(defaultValue)); }, set(v) { if (query === v) return; query = v; _queriesQueue.set(name, v); trigger(); nextTick(() => { if (_queriesQueue.size === 0) return; const newQueries = Object.fromEntries(_queriesQueue.entries()); _queriesQueue.clear(); const { query: query$1, hash, path } = route; router[toValue(mode)]({ path, query: { ...query$1, ...newQueries }, hash }); }); } }; }); watch(() => route.query[name], (v) => { query = v; _trigger(); }, { flush: "sync" }); return proxy; } //#endregion //#region src/client/composables/tag-colors.ts const tagColorsRef = ref(articleTagColors); const useTagColors = () => tagColorsRef; if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateArticleTagColors = (data) => { tagColorsRef.value = data; }; //#endregion //#region src/client/composables/posts-tags.ts function useTags() { const { collection } = useData(); const list = useLocalePostList(); const colors = useTagColors(); const postCollection = computed(() => { if (collection.value?.type === "post") return collection.value; }); const tags = computed(() => { const tagTheme = postCollection.value?.tagsTheme ?? "colored"; const tagMap = {}; list.value.forEach((item) => { if (item.tags) toArray(item.tags).forEach((tag) => { if (tagMap[tag]) tagMap[tag] += 1; else tagMap[tag] = 1; }); }); return Object.keys(tagMap).map((tag) => ({ name: tag, count: tagMap[tag] > 99 ? "99+" : tagMap[tag], className: colors.value[tag] ? `vp-tag-${colors.value[tag]}` : `tag-${tagTheme}` })); }); const currentTag = useRouteQuery("tag"); const postList = computed(() => { if (!currentTag.value) return []; return list.value.filter((item) => { if (item.tags) return toArray(item.tags).includes(currentTag.value); return false; }).map((item) => ({ title: item.title, path: item.path, createTime: item.createTime.split(" ")[0].replace(/\//g, "-") })); }); const handleTagClick = (tag) => { currentTag.value = tag; }; return { tags, currentTag, postList, handleTagClick }; } //#endregion //#region src/client/composables/posts-extract.ts function usePostsExtract() { const { collection } = useData(); const postList = useLocalePostList(); const { tags: tagsList } = useTags(); const { categories: categoryList } = usePostsCategory(); const links = useInternalLink(); return { hasPostsExtract: computed(() => collection.value?.type === "post" && (collection.value.archives !== false || collection.value.tags !== false || collection.value.categories !== false)), tags: computed(() => ({ link: links.tags.value?.link, text: links.tags.value?.text, total: tagsList.value.length })), archives: computed(() => ({ link: links.archive.value?.link, text: links.archive.value?.text, total: postList.value.length })), categories: computed(() => ({ link: links.categories.value?.link, text: links.categories.value?.text, total: getCategoriesTotal(categoryList.value) })) }; } function getCategoriesTotal(categories) { let total = 0; for (const category of categories) if (category.type === "category") { total += 1; if (category.items.length) total += getCategoriesTotal(category.items); } return total; } //#endregion //#region src/client/composables/posts-post-list.ts const DEFAULT_PER_PAGE = 15; function usePostListControl(homePage) { const { collection } = useData(); const list = useLocalePostList(); const is960 = useMediaQuery("(max-width: 960px)"); const postCollection = computed(() => { if (collection.value?.type === "post") return collection.value; }); const postList = computed(() => { const stickyList = list.value.filter((item) => item.sticky === true || typeof item.sticky === "number"); const otherList = list.value.filter((item) => item.sticky === void 0 || item.sticky === false); return [...stickyList.sort((prev, next) => { if (next.sticky === true && prev.sticky === true) return 0; return next.sticky > prev.sticky ? 1 : -1; }), ...otherList]; }); const page = useRouteQuery("p", 1, { mode: "push", transform(val) { const page$1 = Number(val); if (!Number.isNaN(page$1) && page$1 > 0) return page$1; return 1; } }); const perPage = computed(() => { if (postCollection.value?.pagination === false) return 0; if (typeof postCollection.value?.pagination === "number") return postCollection.value.pagination; return postCollection.value?.pagination?.perPage || DEFAULT_PER_PAGE; }); const totalPage = computed(() => { if (postCollection.value?.pagination === false) return 0; return Math.ceil(postList.value.length / perPage.value); }); const isLastPage = computed(() => page.value >= totalPage.value); const isFirstPage = computed(() => page.value <= 1); const isPaginationEnabled = computed(() => postCollection.value?.pagination !== false && totalPage.value > 1); const finalList = computed(() => { if (postCollection.value?.pagination === false) return postList.value; if (postList.value.length <= perPage.value) return postList.value; return postList.value.slice((page.valu