diff --git a/src/client/app/exports.ts b/src/client/app/exports.ts index de39bee1c8c5..f5d84533eb09 100644 --- a/src/client/app/exports.ts +++ b/src/client/app/exports.ts @@ -7,7 +7,7 @@ export * from './theme' // composables export { useSiteData } from './composables/siteData' export { usePageData } from './composables/pageData' -export { useRouter, useRoute } from './router' +export { useRouter, useRoute, Router, Route } from './router' // components export { Content } from './components/Content' diff --git a/src/client/theme-default/components/SideBar.ts b/src/client/theme-default/components/SideBar.ts index 34af2c2dac69..4edb86f7d585 100644 --- a/src/client/theme-default/components/SideBar.ts +++ b/src/client/theme-default/components/SideBar.ts @@ -1,6 +1,7 @@ import { useSiteData, usePageData, useRoute } from 'vitepress' -import { computed, h, FunctionalComponent } from 'vue' +import { computed, h, FunctionalComponent, VNode } from 'vue' import { Header } from '../../../../types/shared' +import { isActive } from '../utils' import { DefaultTheme } from '../config' import { useActiveSidebarLinks } from '../composables/activeSidebarLink' @@ -10,14 +11,16 @@ const SideBarItem: FunctionalComponent<{ const { item: { link, text, children } } = props - return h('li', [ - h('a', { href: link }, text), - children - ? h( - 'ul', - children.map((c) => h(SideBarItem, { item: c })) - ) - : null + + const route = useRoute() + const pageData = usePageData() + + const active = isActive(route, link) + const headers = pageData.value.headers + + return h('li', { class: 'sidebar-item' }, [ + createLink(active, text, link), + createChildren(active, children, headers) ]) } @@ -67,6 +70,10 @@ export default { } } +interface HeaderWithChildren extends Header { + children?: Header[] +} + type ResolvedSidebar = ResolvedSidebarItem[] interface ResolvedSidebarItem { @@ -104,7 +111,7 @@ function resolveArraySidebar( config: DefaultTheme.SideBarItem[], depth: number ): ResolvedSidebar { - return [] + return config } function resolveMultiSidebar( @@ -114,3 +121,59 @@ function resolveMultiSidebar( ): ResolvedSidebar { return [] } + +function createLink(active: boolean, text: string, link?: string): VNode { + const tag = link ? 'a' : 'p' + + const component = { + class: { 'sidebar-link': true, active }, + href: link + } + + return h(tag, component, text) +} + +function createChildren( + active: boolean, + children?: ResolvedSidebarItem[], + headers?: Header[] +): VNode | null { + if (children && children.length > 0) { + return h( + 'ul', + { class: 'sidebar-items' }, + children.map((c) => { + return h(SideBarItem, { item: c }) + }) + ) + } + + return active && headers + ? createChildren(false, resolveHeaders(headers)) + : null +} + +function resolveHeaders(headers: Header[]): ResolvedSidebarItem[] { + return mapHeaders(groupHeaders(headers)) +} + +function groupHeaders(headers: Header[]): HeaderWithChildren[] { + headers = headers.map((h) => Object.assign({}, h)) + let lastH2: HeaderWithChildren + headers.forEach((h) => { + if (h.level === 2) { + lastH2 = h + } else if (lastH2) { + ;(lastH2.children || (lastH2.children = [])).push(h) + } + }) + return headers.filter((h) => h.level === 2) +} + +function mapHeaders(headers: HeaderWithChildren[]): ResolvedSidebarItem[] { + return headers.map((header) => ({ + text: header.title, + link: `#${header.slug}`, + children: header.children ? mapHeaders(header.children) : undefined + })) +} diff --git a/src/client/theme-default/components/SideBar.vue b/src/client/theme-default/components/SideBar.vue index 0e44829a9abb..8ce13fddd396 100644 --- a/src/client/theme-default/components/SideBar.vue +++ b/src/client/theme-default/components/SideBar.vue @@ -1,43 +1,61 @@ diff --git a/src/client/theme-default/utils.ts b/src/client/theme-default/utils.ts index 4b644c3f9ab7..4c6aee47dca2 100644 --- a/src/client/theme-default/utils.ts +++ b/src/client/theme-default/utils.ts @@ -1,5 +1,23 @@ -import { useSiteData } from 'vitepress' +import { useSiteData, Route } from 'vitepress' + +export const hashRE = /#.*$/ +export const extRE = /\.(md|html)$/ export function withBase(path: string) { return (useSiteData().value.base + path).replace(/\/+/g, '/') } + +export function isActive(route: Route, path?: string): boolean { + if (path === undefined) { + return false + } + + const routePath = normalize(route.path) + const pagePath = normalize(path) + + return routePath === pagePath +} + +export function normalize(path: string): string { + return decodeURI(path).replace(hashRE, '').replace(extRE, '') +} diff --git a/src/node/server.ts b/src/node/server.ts index 10f6683c54c0..e9724010dee4 100644 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -43,8 +43,7 @@ function createVitePressPlugin({ customData: { path: resolver.fileToRequest(file), pageData - }, - timestamp: Date.now() + } }) // reload the content component