diff --git a/theme/src/client/composables/index.ts b/theme/src/client/composables/index.ts index 61cde179..73a81740 100644 --- a/theme/src/client/composables/index.ts +++ b/theme/src/client/composables/index.ts @@ -29,6 +29,7 @@ export * from './prev-next.js' export * from './route-query.js' export * from './scroll-behavior.js' export * from './scroll-promise.js' +export * from './sidebar-data.js' export * from './sidebar.js' export * from './tag-colors.js' export * from './theme-data.js' diff --git a/theme/src/client/composables/sidebar-data.ts b/theme/src/client/composables/sidebar-data.ts new file mode 100644 index 00000000..5c6db033 --- /dev/null +++ b/theme/src/client/composables/sidebar-data.ts @@ -0,0 +1,202 @@ +import type { InjectionKey, Ref } from 'vue' +import type { ResolvedSidebarItem, ThemeSidebar, ThemeSidebarItem } from '../../shared/index.js' +import { sidebar as sidebarRaw } from '@internal/sidebar' +import { + isArray, + isPlainObject, + isString, + removeLeadingSlash, +} from '@vuepress/helper/client' +import { + computed, + inject, + provide, + ref, +} from 'vue' +import { useRouteLocale } from 'vuepress/client' +import { normalizeLink, normalizePrefix, resolveNavLink } from '../utils/index.js' +import { useData } from './data.js' + +export type SidebarData = Record + +export type SidebarDataRef = Ref +export type AutoDirSidebarRef = Ref +export type AutoHomeDataRef = Ref> + +const { __auto__, __home__, ...items } = sidebarRaw + +export const sidebarData: SidebarDataRef = ref(items) +export const autoDirSidebar: AutoDirSidebarRef = ref(__auto__) +const autoHomeData: AutoHomeDataRef = ref(__home__) + +if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) { + __VUE_HMR_RUNTIME__.updateSidebar = (data: SidebarData) => { + const { __auto__, __home__, ...items } = data + sidebarData.value = items + autoDirSidebar.value = __auto__ as ThemeSidebarItem[] + autoHomeData.value = __home__ as Record + } +} + +const sidebarSymbol: InjectionKey> = Symbol( + __VUEPRESS_DEV__ ? 'sidebar' : '', +) + +export function setupSidebar(): void { + 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 = computed(() => { + return hasSidebar.value + ? getSidebar(typeof frontmatter.value.sidebar === 'string' + ? frontmatter.value.sidebar + : page.value.path, routeLocale.value) + : [] + }) + + provide(sidebarSymbol, sidebarData) +} + +export function useSidebarData(): Ref { + const sidebarData = inject(sidebarSymbol) + if (!sidebarData) { + throw new Error('useSidebarData() is called without provider.') + } + return sidebarData +} + +/** + * Get the `Sidebar` from sidebar option. This method will ensure to get correct + * sidebar config from `MultiSideBarConfig` with various path combinations such + * as matching `guide/` and `/guide/`. If no matching config was found, it will + * return empty array. + */ +export function getSidebar(routePath: string, routeLocal: string): ResolvedSidebarItem[] { + const _sidebar = sidebarData.value[routeLocal] + + if (_sidebar === 'auto') { + return resolveSidebarItems(autoDirSidebar.value[routeLocal]) + } + else if (isArray(_sidebar)) { + return resolveSidebarItems(_sidebar, routeLocal) + } + else if (isPlainObject(_sidebar)) { + routePath = decodeURIComponent(routePath) + const dir + = Object.keys(_sidebar) + .sort((a, b) => b.split('/').length - a.split('/').length) + .find((dir) => { + // make sure the multi sidebar key starts with slash too + return routePath.startsWith(`${routeLocal}${removeLeadingSlash(dir)}`) + }) || '' + const sidebar = dir ? _sidebar[dir] : undefined + + if (sidebar === 'auto') { + return resolveSidebarItems( + dir ? autoDirSidebar.value[dir] : [], + routeLocal, + ) + } + else if (isArray(sidebar)) { + return resolveSidebarItems(sidebar, dir) + } + else if (isPlainObject(sidebar)) { + const prefix = normalizePrefix(routeLocal, sidebar.prefix) + return resolveSidebarItems( + sidebar.items === 'auto' + ? autoDirSidebar.value[prefix] + : sidebar.items, + prefix, + ) + } + } + return [] +} + +function resolveSidebarItems( + sidebarItems: (string | ThemeSidebarItem)[], + _prefix = '', +): ResolvedSidebarItem[] { + const resolved: ResolvedSidebarItem[] = [] + sidebarItems.forEach((item) => { + if (isString(item)) { + resolved.push(resolveNavLink(normalizeLink(_prefix, item))) + } + else { + const { link, items, prefix, dir, ...args } = item + const navLink = { ...args } as ResolvedSidebarItem + if (link) { + navLink.link = 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 === '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?.length + ? resolveSidebarItems(items, nextPrefix) + : undefined + } + resolved.push(navLink) + } + }) + return resolved +} + +/** + * Get or generate sidebar group from the given sidebar items. + */ +export function getSidebarGroups(sidebar: ResolvedSidebarItem[]): ResolvedSidebarItem[] { + const groups: ResolvedSidebarItem[] = [] + + let lastGroupIndex = 0 + + for (const index in sidebar) { + const item = sidebar[index] + + if (item.items) { + lastGroupIndex = groups.push(item) + continue + } + + if (!groups[lastGroupIndex]) { + groups.push({ items: [] }) + } + + groups[lastGroupIndex]!.items!.push(item) + } + + return groups +} + +export function getSidebarFirstLink(sidebar: ResolvedSidebarItem[]): string { + for (const item of sidebar) { + if (item.link) + return item.link + if (item.items) + return getSidebarFirstLink(item.items) + } + return '' +} diff --git a/theme/src/client/composables/sidebar.ts b/theme/src/client/composables/sidebar.ts index 4ffe30c1..9cc77d53 100644 --- a/theme/src/client/composables/sidebar.ts +++ b/theme/src/client/composables/sidebar.ts @@ -1,202 +1,13 @@ -import type { ComputedRef, InjectionKey, Ref } from 'vue' -import type { ResolvedSidebarItem, ThemeSidebar, ThemeSidebarItem } from '../../shared/index.js' -import { sidebar as sidebarRaw } from '@internal/sidebar' -import { - ensureLeadingSlash, - isArray, - isPlainObject, - isString, - removeLeadingSlash, -} from '@vuepress/helper/client' +import type { ComputedRef, Ref } from 'vue' +import type { ResolvedSidebarItem } from '../../shared/index.js' +import { ensureLeadingSlash, isArray } from '@vuepress/helper/client' import { useMediaQuery } from '@vueuse/core' -import { - computed, - inject, - onMounted, - onUnmounted, - provide, - ref, - watch, - watchEffect, -} from 'vue' +import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue' import { resolveRouteFullPath, useRoute, useRouteLocale } from 'vuepress/client' -import { isActive, normalizeLink, normalizePrefix, resolveNavLink } from '../utils/index.js' +import { isActive } from '../utils/index.js' import { useData } from './data.js' import { useEncrypt } from './encrypt.js' - -export type SidebarData = Record - -export type SidebarDataRef = Ref -export type AutoDirSidebarRef = Ref -export type AutoHomeDataRef = Ref> - -const { __auto__, __home__, ...items } = sidebarRaw - -const sidebarData: SidebarDataRef = ref(items) -const autoDirSidebar: AutoDirSidebarRef = ref(__auto__) -const autoHomeData: AutoHomeDataRef = ref(__home__) - -if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) { - __VUE_HMR_RUNTIME__.updateSidebar = (data: SidebarData) => { - const { __auto__, __home__, ...items } = data - sidebarData.value = items - autoDirSidebar.value = __auto__ as ThemeSidebarItem[] - autoHomeData.value = __home__ as Record - } -} - -const sidebarSymbol: InjectionKey> = Symbol( - __VUEPRESS_DEV__ ? 'sidebar' : '', -) - -export function setupSidebar(): void { - 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 = computed(() => { - return hasSidebar.value - ? getSidebar(typeof frontmatter.value.sidebar === 'string' - ? frontmatter.value.sidebar - : page.value.path, routeLocale.value) - : [] - }) - - provide(sidebarSymbol, sidebarData) -} - -export function useSidebarData(): Ref { - const sidebarData = inject(sidebarSymbol) - if (!sidebarData) { - throw new Error('useSidebarData() is called without provider.') - } - return sidebarData -} - -/** - * Get the `Sidebar` from sidebar option. This method will ensure to get correct - * sidebar config from `MultiSideBarConfig` with various path combinations such - * as matching `guide/` and `/guide/`. If no matching config was found, it will - * return empty array. - */ -export function getSidebar(routePath: string, routeLocal: string): ResolvedSidebarItem[] { - const _sidebar = sidebarData.value[routeLocal] - - if (_sidebar === 'auto') { - return resolveSidebarItems(autoDirSidebar.value[routeLocal]) - } - else if (isArray(_sidebar)) { - return resolveSidebarItems(_sidebar, routeLocal) - } - else if (isPlainObject(_sidebar)) { - routePath = decodeURIComponent(routePath) - const dir - = Object.keys(_sidebar) - .sort((a, b) => b.split('/').length - a.split('/').length) - .find((dir) => { - // make sure the multi sidebar key starts with slash too - return routePath.startsWith(`${routeLocal}${removeLeadingSlash(dir)}`) - }) || '' - const sidebar = dir ? _sidebar[dir] : undefined - - if (sidebar === 'auto') { - return resolveSidebarItems( - dir ? autoDirSidebar.value[dir] : [], - routeLocal, - ) - } - else if (isArray(sidebar)) { - return resolveSidebarItems(sidebar, routeLocal) - } - else if (isPlainObject(sidebar)) { - const prefix = normalizePrefix(routeLocal, sidebar.prefix) - return resolveSidebarItems( - sidebar.items === 'auto' - ? autoDirSidebar.value[prefix] - : sidebar.items, - prefix, - ) - } - } - return [] -} - -function resolveSidebarItems( - sidebarItems: (string | ThemeSidebarItem)[], - _prefix = '', -): ResolvedSidebarItem[] { - const resolved: ResolvedSidebarItem[] = [] - sidebarItems.forEach((item) => { - if (isString(item)) { - resolved.push(resolveNavLink(normalizeLink(_prefix, item))) - } - else { - const { link, items, prefix, dir, ...args } = item - const navLink = { ...args } as ResolvedSidebarItem - if (link) { - navLink.link = 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 === '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?.length - ? resolveSidebarItems(items, nextPrefix) - : undefined - } - resolved.push(navLink) - } - }) - return resolved -} - -/** - * Get or generate sidebar group from the given sidebar items. - */ -export function getSidebarGroups(sidebar: ResolvedSidebarItem[]): ResolvedSidebarItem[] { - const groups: ResolvedSidebarItem[] = [] - - let lastGroupIndex = 0 - - for (const index in sidebar) { - const item = sidebar[index] - - if (item.items) { - lastGroupIndex = groups.push(item) - continue - } - - if (!groups[lastGroupIndex]) { - groups.push({ items: [] }) - } - - groups[lastGroupIndex]!.items!.push(item) - } - - return groups -} +import { getSidebarGroups, sidebarData, useSidebarData } from './sidebar-data.js' /** * Check if the given sidebar item contains any active link. @@ -429,13 +240,3 @@ export function useSidebarControl(item: ComputedRef): Sideb toggle, } } - -export function getSidebarFirstLink(sidebar: ResolvedSidebarItem[]): string { - for (const item of sidebar) { - if (item.link) - return item.link - if (item.items) - return getSidebarFirstLink(item.items) - } - return '' -}