1748 lines
54 KiB
JavaScript
1748 lines
54 KiB
JavaScript
import { hasOwn, tryOnScopeDispose, useDark, useEventListener, useLocalStorage, useMediaQuery, useSessionStorage, useThrottleFn, watchDebounced } from "@vueuse/core";
|
|
import { computed, customRef, inject, nextTick, onMounted, onUnmounted, onUpdated, provide, readonly, ref, toValue, watch, watchEffect } from "vue";
|
|
import { sidebar } from "@internal/sidebar";
|
|
import { ensureLeadingSlash, isArray, 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 { themeData as themeData$1 } from "@internal/themePlumeData";
|
|
import { compare, genSaltSync } from "bcrypt-ts/browser";
|
|
import { encrypt as encrypt$1 } from "@internal/encrypt";
|
|
import { blogPostData as blogPostData$1 } from "@internal/blogData";
|
|
import { articleTagColors } from "@internal/articleTagColors";
|
|
import { useContributors as useContributors$1 } from "@vuepress/plugin-git/client";
|
|
import { isPlainObject as isPlainObject$1, isString as isString$1, removeLeadingSlash as removeLeadingSlash$1 } from "vuepress/shared";
|
|
import { icons } from "@internal/iconify";
|
|
import { defineWatermarkConfig } from "@vuepress/plugin-watermark/client";
|
|
|
|
//#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 themeData$2 = useThemeData();
|
|
const theme = useThemeLocaleData();
|
|
const page = usePageData();
|
|
const frontmatter = usePageFrontmatter();
|
|
const site = useSiteLocaleData();
|
|
const isDark = useDarkMode();
|
|
const lang = usePageLang();
|
|
const blog = computed(() => themeData$2.value.blog || {});
|
|
return {
|
|
theme,
|
|
page,
|
|
frontmatter,
|
|
lang,
|
|
site,
|
|
isDark,
|
|
blog
|
|
};
|
|
}
|
|
|
|
//#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]) {
|
|
return {
|
|
global,
|
|
separator,
|
|
matches,
|
|
admins: admin.split(separator),
|
|
ruleList: Object.keys(rules).map((key) => ({
|
|
key,
|
|
match: matches[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) || relativePath && regex.test(relativePath);
|
|
}
|
|
if (match.endsWith(".md")) return relativePath && relativePath.endsWith(match);
|
|
return pagePath.startsWith(match) || relativePath.startsWith(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(":") : [];
|
|
const pageRule = passwords.length ? {
|
|
key: pagePath.replace(/\//g, "").replace(/\.html$/, ""),
|
|
match: pagePath,
|
|
rules: passwords
|
|
} : void 0;
|
|
const rules = encrypt$2.value.ruleList.length ? encrypt$2.value.ruleList.filter((item) => toMatch(item.match, pagePath, filePathRelative)) : [];
|
|
return [pageRule, ...rules].filter(Boolean);
|
|
});
|
|
const isPageDecrypted = computed(() => {
|
|
if (!hasPageEncrypt.value) return true;
|
|
const hash = splitHash(storage.value.p.__GLOBAL__ || "");
|
|
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;
|
|
});
|
|
provide(EncryptSymbol, {
|
|
hasPageEncrypt,
|
|
isGlobalDecrypted,
|
|
isPageDecrypted,
|
|
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.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";
|
|
});
|
|
const sidebarData$1 = computed(() => {
|
|
return hasSidebar.value ? getSidebar(typeof frontmatter.value.sidebar === "string" ? frontmatter.value.sidebar : page.value.path, routeLocale.value) : [];
|
|
});
|
|
provide(sidebarSymbol, sidebarData$1);
|
|
}
|
|
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(dir, 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;
|
|
}
|
|
/**
|
|
* 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
|
|
};
|
|
}
|
|
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/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/blog-data.ts
|
|
const blogPostData = ref(blogPostData$1);
|
|
function usePostList() {
|
|
return blogPostData;
|
|
}
|
|
function useLocalePostList() {
|
|
const locale = usePageLang();
|
|
return computed(() => blogPostData.value.filter((item) => item.lang === locale.value));
|
|
}
|
|
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) __VUE_HMR_RUNTIME__.updateBlogPostData = (data) => {
|
|
blogPostData.value = data;
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/client/composables/blog-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/blog-category.ts
|
|
function useBlogCategory() {
|
|
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/blog-tags.ts
|
|
function useTags() {
|
|
const { blog } = useData();
|
|
const list = useLocalePostList();
|
|
const colors = useTagColors();
|
|
const tags = computed(() => {
|
|
const tagTheme = blog.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/internal-link.ts
|
|
function useInternalLink() {
|
|
const { blog, theme } = useData();
|
|
const themeData$2 = useThemeData();
|
|
const routeLocale = useRouteLocale();
|
|
function resolveLink(name, link) {
|
|
return {
|
|
link: (routeLocale.value + link).replace(/\/+/g, "/"),
|
|
text: theme.value[`${name}Text`] || themeData$2.value[`${name}Text`]
|
|
};
|
|
}
|
|
const home = computed(() => resolveLink("home", "/"));
|
|
const blogLink = computed(() => blog.value.postList !== false ? resolveLink("blog", blog.value.link || "blog/") : home.value);
|
|
const tags = computed(() => blog.value.tags !== false ? resolveLink("tag", blog.value.tagsLink || "blog/tags/") : void 0);
|
|
const archive = computed(() => blog.value.archives !== false ? resolveLink("archive", blog.value.archivesLink || "blog/archives/") : void 0);
|
|
const categories = computed(() => blog.value.categories !== false ? resolveLink("category", blog.value.categoriesLink || "blog/categories/") : void 0);
|
|
return {
|
|
home,
|
|
blog: blogLink,
|
|
tags,
|
|
archive,
|
|
categories
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/client/composables/blog-extract.ts
|
|
function useBlogExtract() {
|
|
const { blog } = useData();
|
|
const postList = useLocalePostList();
|
|
const { tags: tagsList } = useTags();
|
|
const { categories: categoryList } = useBlogCategory();
|
|
const links = useInternalLink();
|
|
const hasBlogExtract = computed(() => blog.value.archives !== false || blog.value.tags !== false || blog.value.categories !== false);
|
|
const tags = computed(() => ({
|
|
link: links.tags.value?.link,
|
|
text: links.tags.value?.text,
|
|
total: tagsList.value.length
|
|
}));
|
|
const archives = computed(() => ({
|
|
link: links.archive.value?.link,
|
|
text: links.archive.value?.text,
|
|
total: postList.value.length
|
|
}));
|
|
const categories = computed(() => ({
|
|
link: links.categories.value?.link,
|
|
text: links.categories.value?.text,
|
|
total: getCategoriesTotal(categoryList.value)
|
|
}));
|
|
return {
|
|
hasBlogExtract,
|
|
tags,
|
|
archives,
|
|
categories
|
|
};
|
|
}
|
|
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/blog-post-list.ts
|
|
const DEFAULT_PER_PAGE = 15;
|
|
function usePostListControl(homePage) {
|
|
const { blog } = useData();
|
|
const list = useLocalePostList();
|
|
const is960 = useMediaQuery("(max-width: 960px)");
|
|
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 (blog.value.pagination === false) return 0;
|
|
if (typeof blog.value.pagination === "number") return blog.value.pagination;
|
|
return blog.value.pagination?.perPage || DEFAULT_PER_PAGE;
|
|
});
|
|
const totalPage = computed(() => {
|
|
if (blog.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(() => blog.value.pagination !== false && totalPage.value > 1);
|
|
const finalList = computed(() => {
|
|
if (blog.value.pagination === false) return postList.value;
|
|
if (postList.value.length <= perPage.value) return postList.value;
|
|
return postList.value.slice((page.value - 1) * perPage.value, page.value * perPage.value);
|
|
});
|
|
const pageRange = computed(() => {
|
|
let range = [];
|
|
const total = totalPage.value;
|
|
const _page = page.value;
|
|
const per = is960.value ? 4 : 5;
|
|
if (total <= 0) return range;
|
|
if (total <= 10) range = Array.from({ length: total }, (_, i) => ({ value: i + 1 }));
|
|
else {
|
|
let i = 1;
|
|
let hasMore = false;
|
|
while (i <= total) {
|
|
if (_page <= per && i <= per || _page >= total - (per - 1) && i >= total - (per - 1)) {
|
|
hasMore = false;
|
|
range.push({ value: i });
|
|
} else if (i <= 2 || i >= total - 1) {
|
|
hasMore = false;
|
|
range.push({ value: i });
|
|
} else if ((_page > per + 1 || _page < total - (per + 1)) && _page - i < per - 2 && i - _page < per - 2) {
|
|
hasMore = false;
|
|
range.push({ value: i });
|
|
} else if (!hasMore) {
|
|
hasMore = true;
|
|
range.push({
|
|
value: i,
|
|
more: true
|
|
});
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
return range;
|
|
});
|
|
const changePage = (current) => {
|
|
if (page.value === current) return;
|
|
page.value = current;
|
|
setTimeout(() => {
|
|
let top = 0;
|
|
if (homePage.value) {
|
|
top = document.querySelector(".vp-blog")?.getBoundingClientRect().top || 0;
|
|
top += window.scrollY - 64;
|
|
}
|
|
window.scrollTo({
|
|
top,
|
|
behavior: "instant"
|
|
});
|
|
}, 0);
|
|
};
|
|
return {
|
|
postList: finalList,
|
|
page,
|
|
totalPage,
|
|
pageRange,
|
|
isLastPage,
|
|
isFirstPage,
|
|
isPaginationEnabled,
|
|
changePage
|
|
};
|
|
}
|
|
|
|
//#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;
|
|
});
|
|
const hasContributors = computed(() => contributors.value.length > 0);
|
|
return {
|
|
mode,
|
|
contributors,
|
|
hasContributors
|
|
};
|
|
}
|
|
|
|
//#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;
|
|
});
|
|
const 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: ";
|
|
});
|
|
return {
|
|
license,
|
|
author,
|
|
hasCopyright,
|
|
creation,
|
|
creationText,
|
|
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/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/home.ts
|
|
const lightTint = {
|
|
r: {
|
|
value: 200,
|
|
offset: 36
|
|
},
|
|
g: {
|
|
value: 200,
|
|
offset: 36
|
|
},
|
|
b: {
|
|
value: 200,
|
|
offset: 36
|
|
}
|
|
};
|
|
const darkTint = {
|
|
r: {
|
|
value: 32,
|
|
offset: 36
|
|
},
|
|
g: {
|
|
value: 32,
|
|
offset: 36
|
|
},
|
|
b: {
|
|
value: 32,
|
|
offset: 36
|
|
}
|
|
};
|
|
function useHomeHeroTintPlate(canvas, enable, tintPlate) {
|
|
const isDark = useDarkMode();
|
|
let ctx = null;
|
|
let t = 0;
|
|
let timer;
|
|
const plate = computed(() => {
|
|
const defaultTint = isDark.value ? darkTint : lightTint;
|
|
if (!tintPlate.value) return defaultTint;
|
|
const plate$1 = tintPlate.value;
|
|
if (typeof plate$1 === "string" || typeof plate$1 === "number") {
|
|
if (isDark.value) return darkTint;
|
|
const values = toPlate(plate$1);
|
|
return values.length !== 3 ? lightTint : toTint(values);
|
|
}
|
|
if (typeof plate$1 === "object") {
|
|
if ("r" in plate$1) {
|
|
if (isDark.value) return darkTint;
|
|
return toNumber({
|
|
...lightTint,
|
|
...plate$1
|
|
});
|
|
}
|
|
const key = isDark.value ? "dark" : "light";
|
|
if (key in plate$1) {
|
|
const _plate = plate$1[key];
|
|
if (typeof _plate === "string" || typeof _plate === "number") {
|
|
const values = toPlate(_plate);
|
|
return values.length !== 3 ? lightTint : toTint(values);
|
|
}
|
|
return toNumber({
|
|
...defaultTint,
|
|
...plate$1
|
|
});
|
|
}
|
|
}
|
|
return defaultTint;
|
|
});
|
|
onMounted(() => {
|
|
if (canvas.value && enable.value) {
|
|
ctx = canvas.value.getContext("2d");
|
|
if (timer) window.cancelAnimationFrame(timer);
|
|
run();
|
|
}
|
|
});
|
|
onUnmounted(() => {
|
|
if (timer) window.cancelAnimationFrame(timer);
|
|
});
|
|
function run() {
|
|
for (let x = 0; x <= 35; x++) for (let y = 0; y <= 35; y++) col(x, y, R(x, y, t), G(x, y, t), B(x, y, t));
|
|
t = t + .02;
|
|
timer = window.requestAnimationFrame(run);
|
|
}
|
|
function col(x, y, r, g, b) {
|
|
if (!ctx) return;
|
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
ctx.fillRect(x, y, 1, 1);
|
|
}
|
|
function R(x, y, t$1) {
|
|
const r = plate.value.r;
|
|
return Math.floor(r.value + r.offset * Math.cos((x * x - y * y) / 300 + t$1));
|
|
}
|
|
function G(x, y, t$1) {
|
|
const g = plate.value.g;
|
|
return Math.floor(g.value + g.offset * Math.sin((x * x * Math.cos(t$1 / 4) + y * y * Math.sin(t$1 / 3)) / 300));
|
|
}
|
|
function B(x, y, t$1) {
|
|
const b = plate.value.b;
|
|
return Math.floor(b.value + b.offset * Math.sin(5 * Math.sin(t$1 / 9) + ((x - 100) * (x - 100) + (y - 100) * (y - 100)) / 1100));
|
|
}
|
|
}
|
|
function toPlate(plate) {
|
|
return typeof plate === "number" || Number(plate) === Number.parseInt(plate) ? [
|
|
plate,
|
|
plate,
|
|
plate
|
|
].map((n) => Number(n)) : plate.includes(",") ? plate.replace(/\s/g, "").split(",").map((n) => Number(n)) : [];
|
|
}
|
|
function toTint([r, g, b]) {
|
|
return {
|
|
r: toColor(r),
|
|
g: toColor(g),
|
|
b: toColor(b)
|
|
};
|
|
}
|
|
function toColor(num) {
|
|
const offset = 256 - num;
|
|
return {
|
|
value: num,
|
|
offset: offset > 64 ? 64 : offset
|
|
};
|
|
}
|
|
function toNumber(tint) {
|
|
Object.keys(tint).forEach((key) => {
|
|
const p = tint[key];
|
|
p.value = Number(p.value);
|
|
p.offset = Number(p.offset);
|
|
});
|
|
return tint;
|
|
}
|
|
|
|
//#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/composables/page.ts
|
|
function useBlogPageData() {
|
|
const { page } = useData();
|
|
const postList = usePostList();
|
|
const isBlogPost = computed(() => {
|
|
return postList.value.some((item) => item.path === page.value.path);
|
|
});
|
|
const isBlogLayout = computed(() => {
|
|
const type = page.value.type;
|
|
return type === "blog" || type === "blog-archives" || type === "blog-tags" || type === "blog-categories";
|
|
});
|
|
return {
|
|
isBlogPost,
|
|
isBlogLayout
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/client/composables/langs.ts
|
|
function useLangs({ removeCurrent = true } = {}) {
|
|
const theme = useThemeData();
|
|
const { page } = useData();
|
|
const routeLocale = useRouteLocale();
|
|
const { isBlogPost } = useBlogPageData();
|
|
const currentLang = computed(() => {
|
|
const link = routeLocale.value;
|
|
return {
|
|
text: theme.value.locales?.[link]?.selectLanguageName,
|
|
link
|
|
};
|
|
});
|
|
const resolvePath = (locale, url) => {
|
|
const targetPath = normalizeLink(locale, url.slice(routeLocale.value.length));
|
|
const { notFound, path } = resolveRoute(targetPath);
|
|
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;
|
|
const blog = theme.value.blog;
|
|
if (isBlogPost.value && blog !== false) return normalizeLink(locale, removeLeadingSlash$1(blog?.link || "blog/"));
|
|
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 currentPath = page.value.filePathRelative ? `/${page.value.filePathRelative}` : void 0;
|
|
const path = resolveRouteFullPath(link$1, currentPath);
|
|
if (path.includes("#")) {
|
|
if (path.slice(0, path.indexOf("#")) === route.path) return path.slice(path.indexOf("#"));
|
|
}
|
|
return path;
|
|
});
|
|
const isExternalProtocol = computed(() => {
|
|
if (!link.value || link.value[0] === "#") return false;
|
|
return isLinkWithProtocol(link.value);
|
|
});
|
|
return {
|
|
isExternal,
|
|
isExternalProtocol,
|
|
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 headers = Array.from(document.querySelectorAll(".vp-doc :where(h1,h2,h3,h4,h5,h6):not(.vp-demo-wrapper :where(h1,h2,h3,h4,h5,h6))")).filter((el) => el.id && el.hasChildNodes()).map((el) => {
|
|
const level = Number(el.tagName[1]);
|
|
return {
|
|
element: el,
|
|
title: serializeHeader(el),
|
|
link: `#${el.id}`,
|
|
level
|
|
};
|
|
});
|
|
return resolveHeaders(headers, 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 = window.innerHeight;
|
|
const offsetHeight = document.body.offsetHeight;
|
|
const isBottom = Math.abs(scrollY + innerHeight - 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/prev-next.ts
|
|
function usePrevNext() {
|
|
const route = useRoute();
|
|
const { frontmatter, theme } = useData();
|
|
const { sidebar: sidebar$1 } = useSidebar();
|
|
const postList = usePostList();
|
|
const locale = usePageLang();
|
|
const { isBlogPost } = useBlogPageData();
|
|
const prevNavList = computed(() => {
|
|
if (theme.value.prevPage === false) return null;
|
|
const prevConfig = resolveFromFrontmatterConfig(frontmatter.value.prev);
|
|
if (prevConfig !== false) return prevConfig;
|
|
if (isBlogPost.value) return resolveFromBlogPostData(postList.value.filter((item) => item.lang === locale.value), route.path, -1);
|
|
else return resolveFromSidebarItems(flatSidebar(sidebar$1.value), route.path, -1);
|
|
});
|
|
const nextNavList = computed(() => {
|
|
if (theme.value.nextPage === false) return null;
|
|
const nextConfig = resolveFromFrontmatterConfig(frontmatter.value.next);
|
|
if (nextConfig !== false) return nextConfig;
|
|
if (isBlogPost.value) return resolveFromBlogPostData(postList.value.filter((item) => item.lang === locale.value), route.path, 1);
|
|
else return resolveFromSidebarItems(flatSidebar(sidebar$1.value), route.path, 1);
|
|
});
|
|
return {
|
|
prev: prevNavList,
|
|
next: nextNavList
|
|
};
|
|
}
|
|
/**
|
|
* Resolve `prev` or `next` config from frontmatter
|
|
*/
|
|
function resolveFromFrontmatterConfig(conf) {
|
|
if (conf === false) return null;
|
|
if (isString$1(conf)) return resolveNavLink(conf);
|
|
if (isPlainObject$1(conf)) return conf;
|
|
return false;
|
|
}
|
|
function flatSidebar(sidebar$1, res = []) {
|
|
for (const item of sidebar$1) {
|
|
if (item.link) res.push({
|
|
link: item.link,
|
|
text: item.text || item.dir || ""
|
|
});
|
|
if (Array.isArray(item.items) && item.items.length) flatSidebar(item.items, res);
|
|
}
|
|
return res;
|
|
}
|
|
/**
|
|
* Resolve `prev` or `next` config from sidebar items
|
|
*/
|
|
function resolveFromSidebarItems(sidebarItems, currentPath, offset) {
|
|
const index = sidebarItems.findIndex((item) => resolveRouteFullPath(item.link) === currentPath);
|
|
if (index !== -1) {
|
|
const targetItem = sidebarItems[index + offset];
|
|
if (targetItem?.link) return {
|
|
link: targetItem.link,
|
|
text: targetItem.text
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function resolveFromBlogPostData(postList, currentPath, offset) {
|
|
const index = postList.findIndex((item) => item.path === currentPath);
|
|
if (index !== -1) {
|
|
const targetItem = postList[index + offset];
|
|
if (!targetItem?.path) return null;
|
|
return {
|
|
link: targetItem.path,
|
|
text: targetItem.title
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/client/composables/scroll-promise.ts
|
|
let promise = null;
|
|
let promiseResolve = null;
|
|
const scrollPromise = {
|
|
wait: () => promise,
|
|
pending: () => {
|
|
promise = new Promise((resolve) => promiseResolve = resolve);
|
|
},
|
|
resolve: () => {
|
|
promiseResolve?.();
|
|
promise = null;
|
|
promiseResolve = null;
|
|
}
|
|
};
|
|
const useScrollPromise = () => scrollPromise;
|
|
|
|
//#endregion
|
|
//#region src/client/composables/scroll-behavior.ts
|
|
function enhanceScrollBehavior(router) {
|
|
router.options.scrollBehavior = async (to, from, savedPosition) => {
|
|
await useScrollPromise().wait();
|
|
if (savedPosition) return savedPosition;
|
|
if (to.hash) return {
|
|
el: to.hash,
|
|
top: 64
|
|
};
|
|
return { top: 0 };
|
|
};
|
|
router.beforeEach((to, from) => {
|
|
if (inBrowser) {
|
|
if (from.path !== to.path) document.documentElement.classList.remove("smooth");
|
|
}
|
|
});
|
|
router.afterEach(() => nextTick(() => {
|
|
if (inBrowser) setTimeout(() => {
|
|
document.documentElement.classList.add("smooth");
|
|
}, 1e3);
|
|
}));
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/client/composables/watermark.ts
|
|
const FP = __PLUME_WM_FP__;
|
|
function setupWatermark() {
|
|
const { frontmatter } = useData();
|
|
defineWatermarkConfig(computed(() => {
|
|
const disableFullPage = typeof frontmatter.value.watermark === "object" && frontmatter.value.watermark.fullPage === false;
|
|
return { parent: !FP || disableFullPage ? ".vp-doc" : "body" };
|
|
}));
|
|
}
|
|
|
|
//#endregion
|
|
export { EncryptSymbol, blogPostData, darkModeSymbol, enableTransitions, encrypt, enhanceScrollBehavior, focusedElement, getHeaders, getPresetLocaleData, getSidebar, getSidebarFirstLink, getSidebarGroups, hasActiveLink, headersSymbol, presetLocales, resolveHeaders, setupDarkMode, setupEncrypt, setupHeaders, setupSidebar, setupThemeData, setupWatermark, themeData, themeLocaleDataSymbol, useActiveAnchor, useArchives, useAside, useBlogCategory, useBlogExtract, useBlogPageData, useBulletin, useBulletinControl, useCloseSidebarOnEscape, useContributors, useCopyright, useDarkMode, useData, useEditLink, useEncrypt, useEncryptCompare, useEncryptData, useFlyout, useHeaders, useHomeHeroTintPlate, useIconsData, useInternalLink, useLangs, useLastUpdated, useLink, useLocalePostList, useNav, useNavbarData, usePostList, usePostListControl, usePrevNext, useRouteQuery, useScrollPromise, useSidebar, useSidebarControl, useSidebarData, useTagColors, useTags, useThemeData, useThemeLocaleData }; |