Merge branch 'master' of https://github.com/ChrisShank/vitepress into feat/user-markdown-options

pull/128/head
chrisshank 5 years ago
commit 939c3b1967

2
.gitignore vendored

@ -1,4 +1,6 @@
/dist /dist
/node_modules /node_modules
/src/client/shared
/src/node/shared
*.log *.log
.DS_Store .DS_Store

@ -1,3 +1,10 @@
## [0.7.3](https://github.com/vuejs/vitepress/compare/v0.7.2...v0.7.3) (2020-11-06)
### Bug Fixes
- Fix sidebar page switch layout shifting
- Fix production hydration mismatch
## [0.7.2](https://github.com/vuejs/vitepress/compare/v0.7.1...v0.7.2) (2020-11-02) ## [0.7.2](https://github.com/vuejs/vitepress/compare/v0.7.1...v0.7.2) (2020-11-02)
### Bug Fixes ### Bug Fixes

@ -1,6 +1,6 @@
{ {
"name": "vitepress", "name": "vitepress",
"version": "0.7.2", "version": "0.7.3",
"description": "", "description": "",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"typings": "types/index.d.ts", "typings": "types/index.d.ts",
@ -27,14 +27,15 @@
}, },
"homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme",
"scripts": { "scripts": {
"dev": "run-p dev-client dev-client-copy dev-node dev-shared", "dev": "yarn dev-shared && yarn dev-start",
"dev-start": "run-p dev-client dev-node dev-watch",
"dev-client": "tsc -w -p src/client", "dev-client": "tsc -w -p src/client",
"dev-client-copy": "node scripts/watchAndCopy",
"dev-node": "tsc -w -p src/node", "dev-node": "tsc -w -p src/node",
"dev-shared": "tsc -w -p src/shared", "dev-shared": "node scripts/copyShared",
"release": "bash scripts/release.sh", "dev-watch": "node scripts/watchAndCopy",
"build": "rimraf -rf dist && tsc -p src/client && tsc -p src/node && tsc -p src/shared && node scripts/copy", "build": "rimraf -rf dist && node scripts/copyShared && tsc -p src/client && tsc -p src/node && node scripts/copyClient",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "bash scripts/release.sh"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

@ -0,0 +1,7 @@
const fs = require('fs-extra')
const glob = require('globby')
glob.sync('src/shared/**/*.ts').forEach((file) => {
fs.copy(file, file.replace(/^src\//, 'src/node/'))
fs.copy(file, file.replace(/^src\//, 'src/client/'))
})

@ -1,13 +1,31 @@
// copy and watch non-ts files in src/client
const fs = require('fs-extra') const fs = require('fs-extra')
const chokidar = require('chokidar') const chokidar = require('chokidar')
function toDest(file) { function toClientAndNode(method, file) {
if (method === 'copy') {
fs.copy(file, file.replace(/^src\//, 'src/node/'))
fs.copy(file, file.replace(/^src\//, 'src/client/'))
} else if (method === 'remove') {
fs.remove(file.replace(/^src\//, 'src/node/'))
fs.remove(file.replace(/^src\//, 'src/client/'))
}
}
function toDist(file) {
return file.replace(/^src\//, 'dist/') return file.replace(/^src\//, 'dist/')
} }
// copy shared files to the client and node directory whenever they change.
chokidar
.watch('src/shared/**/*.ts')
.on('change', (file) => toClientAndNode('copy', file))
.on('add', (file) => toClientAndNode('copy', file))
.on('unlink', (file) => toClientAndNode('remove', file))
// copy non ts files, such as an html or css, to the dist directory whenever
// they change.
chokidar chokidar
.watch('src/client/**/!(*.ts|tsconfig.json)') .watch('src/client/**/!(*.ts|tsconfig.json)')
.on('change', (file) => fs.copy(file, toDest(file))) .on('change', (file) => fs.copy(file, toDist(file)))
.on('add', (file) => fs.copy(file, toDest(file))) .on('add', (file) => fs.copy(file, toDist(file)))
.on('unlink', (file) => fs.remove(toDest(file))) .on('unlink', (file) => fs.remove(toDist(file)))

@ -9,6 +9,6 @@ export const Content = {
// in prod mode, enable intersectionObserver based pre-fetch. // in prod mode, enable intersectionObserver based pre-fetch.
usePrefetch() usePrefetch()
} }
return () => (route.contentComponent ? h(route.contentComponent) : null) return () => (route.component ? h(route.component) : null)
} }
} }

@ -1,11 +1,8 @@
import { watchEffect, Ref } from 'vue' import { watchEffect, Ref } from 'vue'
import { PageDataRef } from './pageData'
import { HeadConfig, SiteData } from '../../../../types/shared' import { HeadConfig, SiteData } from '../../../../types/shared'
import { Route } from '../router'
export function useUpdateHead( export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
pageDataRef: PageDataRef,
siteDataByRouteRef: Ref<SiteData>
) {
const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
let isFirstUpdate = true let isFirstUpdate = true
@ -28,7 +25,7 @@ export function useUpdateHead(
} }
watchEffect(() => { watchEffect(() => {
const pageData = pageDataRef.value const pageData = route.data
const siteData = siteDataByRouteRef.value const siteData = siteDataByRouteRef.value
const pageTitle = pageData && pageData.title const pageTitle = pageData && pageData.title
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title

@ -1,14 +0,0 @@
import { inject, InjectionKey, Ref } from 'vue'
import { PageData } from '../../../../types/shared'
export type PageDataRef = Ref<PageData>
export const pageDataSymbol: InjectionKey<PageDataRef> = Symbol()
export function usePageData(): PageDataRef {
const data = inject(pageDataSymbol)
if (!data) {
throw new Error('usePageData() is called without provider.')
}
return data
}

@ -1,12 +1,11 @@
// exports in this file are exposed to themes and md files via 'vitepress' // exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { usePageData } from 'vitepress'` // so the user can do `import { useRoute, useSiteData } from 'vitepress'`
// theme types // theme types
export * from './theme' export * from './theme'
// composables // composables
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData'
export { useSiteDataByRoute } from './composables/siteDataByRoute' export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { useRouter, useRoute, Router, Route } from './router' export { useRouter, useRoute, Router, Route } from './router'

@ -1,7 +1,6 @@
import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue' import { createApp as createClientApp, createSSRApp } from 'vue'
import { createRouter, RouterSymbol } from './router' import { createRouter, RouterSymbol } from './router'
import { useUpdateHead } from './composables/head' import { useUpdateHead } from './composables/head'
import { pageDataSymbol } from './composables/pageData'
import { Content } from './components/Content' import { Content } from './components/Content'
import Debug from './components/Debug.vue' import Debug from './components/Debug.vue'
import Theme from '/@theme/index' import Theme from '/@theme/index'
@ -12,63 +11,52 @@ import { siteDataRef } from './composables/siteData'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
export function createApp() { export function createApp() {
// unlike site data which is static across all requests, page data is
// distinct per-request.
const pageDataRef = ref()
if (import.meta.hot) {
// hot reload pageData
import.meta.hot!.on('vitepress:pageData', (data) => {
if (
data.path.replace(/(\bindex)?\.md$/, '') ===
location.pathname.replace(/(\bindex)?\.html$/, '')
) {
pageDataRef.value = data.pageData
}
})
}
let isInitialPageLoad = inBrowser let isInitialPageLoad = inBrowser
let initialPath: string let initialPath: string
const router = createRouter((route) => { const router = createRouter((path) => {
let pagePath = pathToFile(route.path) let pageFilePath = pathToFile(path)
if (isInitialPageLoad) { if (isInitialPageLoad) {
initialPath = pagePath initialPath = pageFilePath
} }
// use lean build if this is the initial page load or navigating back // use lean build if this is the initial page load or navigating back
// to the initial loaded path (the static vnodes already adopted the // to the initial loaded path (the static vnodes already adopted the
// static content on that load so no need to re-fetch the page) // static content on that load so no need to re-fetch the page)
if (isInitialPageLoad || initialPath === pagePath) { if (isInitialPageLoad || initialPath === pageFilePath) {
pagePath = pagePath.replace(/\.js$/, '.lean.js') pageFilePath = pageFilePath.replace(/\.js$/, '.lean.js')
} }
if (inBrowser) { if (inBrowser) {
isInitialPageLoad = false isInitialPageLoad = false
// in browser: native dynamic import // in browser: native dynamic import
return import(/*@vite-ignore*/ pagePath).then((page) => { return import(/*@vite-ignore*/ pageFilePath)
if (page.__pageData) {
pageDataRef.value = readonly(JSON.parse(page.__pageData))
}
return page.default
})
} else { } else {
// SSR, sync require // SSR, sync require
const page = require(pagePath) return require(pageFilePath)
pageDataRef.value = JSON.parse(page.__pageData)
return page.default
} }
}, NotFound) }, NotFound)
// update route.data on HMR updates of active page
if (import.meta.hot) {
// hot reload pageData
import.meta.hot!.on('vitepress:pageData', (payload) => {
if (
payload.path.replace(/(\bindex)?\.md$/, '') ===
location.pathname.replace(/(\bindex)?\.html$/, '')
) {
router.route.data = payload.pageData
}
})
}
const app = const app =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? createSSRApp(Theme.Layout) ? createSSRApp(Theme.Layout)
: createClientApp(Theme.Layout) : createClientApp(Theme.Layout)
app.provide(RouterSymbol, router) app.provide(RouterSymbol, router)
app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content) app.component('Content', Content)
app.component( app.component(
@ -80,7 +68,7 @@ export function createApp() {
if (inBrowser) { if (inBrowser) {
// dynamically update head tags // dynamically update head tags
useUpdateHead(pageDataRef, siteDataByRouteRef) useUpdateHead(router.route, siteDataByRouteRef)
} }
Object.defineProperties(app.config.globalProperties, { Object.defineProperties(app.config.globalProperties, {
@ -96,7 +84,7 @@ export function createApp() {
}, },
$page: { $page: {
get() { get() {
return pageDataRef.value return router.route.data
} }
}, },
$theme: { $theme: {

@ -1,9 +1,11 @@
import { reactive, inject, markRaw, nextTick } from 'vue' import { reactive, inject, markRaw, nextTick, readonly } from 'vue'
import type { Component, InjectionKey } from 'vue' import type { Component, InjectionKey } from 'vue'
import { PageData } from '../../../types/shared'
export interface Route { export interface Route {
path: string path: string
contentComponent: Component | null data: PageData
component: Component | null
} }
export interface Router { export interface Router {
@ -19,19 +21,26 @@ const fakeHost = `http://a.com`
const getDefaultRoute = (): Route => ({ const getDefaultRoute = (): Route => ({
path: '/', path: '/',
contentComponent: null component: null,
// this will be set upon initial page load, which is before
// the app is mounted, so it's guaranteed to be avaiable in
// components
data: null as any
}) })
interface PageModule {
__pageData: string
default: Component
}
export function createRouter( export function createRouter(
loadComponent: (route: Route) => Component | Promise<Component>, loadPageModule: (path: string) => PageModule | Promise<PageModule>,
fallbackComponent?: Component fallbackComponent?: Component
): Router { ): Router {
// TODO: the cast shouldn't be necessary const route = reactive(getDefaultRoute())
const route = reactive(getDefaultRoute()) as Route
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'
function go(href?: string) { function go(href: string = inBrowser ? location.href : '/') {
href = href || (inBrowser ? location.href : '/')
// ensure correct deep link so page refresh lands on correct files. // ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
@ -46,21 +55,30 @@ export function createRouter(
return loadPage(href) return loadPage(href)
} }
let latestPendingPath: string | null = null
async function loadPage(href: string, scrollPosition = 0) { async function loadPage(href: string, scrollPosition = 0) {
const targetLoc = new URL(href, fakeHost) const targetLoc = new URL(href, fakeHost)
const pendingPath = (route.path = targetLoc.pathname) const pendingPath = (latestPendingPath = targetLoc.pathname)
try { try {
let comp = loadComponent(route) let page = loadPageModule(pendingPath)
// only await if it returns a Promise - this allows sync resolution // only await if it returns a Promise - this allows sync resolution
// on initial render in SSR. // on initial render in SSR.
if ('then' in comp && typeof comp.then === 'function') { if ('then' in page && typeof page.then === 'function') {
comp = await comp page = await page
} }
if (route.path === pendingPath) { if (latestPendingPath === pendingPath) {
latestPendingPath = null
const { default: comp, __pageData } = page as PageModule
if (!comp) { if (!comp) {
throw new Error(`Invalid route component: ${comp}`) throw new Error(`Invalid route component: ${comp}`)
} }
route.contentComponent = markRaw(comp)
route.path = pendingPath
route.component = markRaw(comp)
route.data = readonly(JSON.parse(__pageData)) as PageData
if (inBrowser) { if (inBrowser) {
nextTick(() => { nextTick(() => {
if (targetLoc.hash && !scrollPosition) { if (targetLoc.hash && !scrollPosition) {
@ -80,10 +98,10 @@ export function createRouter(
if (!err.message.match(/fetch/)) { if (!err.message.match(/fetch/)) {
console.error(err) console.error(err)
} }
if (route.path === pendingPath) { if (latestPendingPath === pendingPath) {
route.contentComponent = fallbackComponent latestPendingPath = null
? markRaw(fallbackComponent) route.path = pendingPath
: null route.component = fallbackComponent ? markRaw(fallbackComponent) : null
} }
} }
} }
@ -152,7 +170,8 @@ export function useRoute(): Route {
} }
function scrollTo(el: HTMLElement, hash: string, smooth = false) { function scrollTo(el: HTMLElement, hash: string, smooth = false) {
const pageOffset = document.getElementById('app')!.offsetTop const pageOffset = (document.querySelector('.navbar') as HTMLElement)
.offsetHeight
const target = el.classList.contains('.header-anchor') const target = el.classList.contains('.header-anchor')
? el ? el
: document.querySelector(decodeURIComponent(hash)) : document.querySelector(decodeURIComponent(hash))

@ -54,7 +54,7 @@ import Home from './components/Home.vue'
import ToggleSideBarButton from './components/ToggleSideBarButton.vue' import ToggleSideBarButton from './components/ToggleSideBarButton.vue'
import SideBar from './components/SideBar.vue' import SideBar from './components/SideBar.vue'
import Page from './components/Page.vue' import Page from './components/Page.vue'
import { useRoute, usePageData, useSiteData, useSiteDataByRoute } from 'vitepress' import { useRoute, useSiteData, useSiteDataByRoute } from 'vitepress'
export default { export default {
components: { components: {
@ -67,16 +67,15 @@ export default {
setup() { setup() {
const route = useRoute() const route = useRoute()
const pageData = usePageData()
const siteData = useSiteData() const siteData = useSiteData()
const siteRouteData = useSiteDataByRoute() const siteRouteData = useSiteDataByRoute()
const openSideBar = ref(false) const openSideBar = ref(false)
const enableHome = computed(() => !!pageData.value.frontmatter.home) const enableHome = computed(() => !!route.data.frontmatter.home)
const showNavbar = computed(() => { const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value const { themeConfig } = siteRouteData.value
const { frontmatter } = pageData.value const { frontmatter } = route.data
if ( if (
frontmatter.navbar === false frontmatter.navbar === false
|| themeConfig.navbar === false) { || themeConfig.navbar === false) {
@ -91,7 +90,7 @@ export default {
}) })
const showSidebar = computed(() => { const showSidebar = computed(() => {
const { frontmatter } = pageData.value const { frontmatter } = route.data
const { themeConfig } = siteRouteData.value const { themeConfig } = siteRouteData.value
return ( return (
!frontmatter.home !frontmatter.home

@ -57,7 +57,7 @@
import { defineComponent, computed } from 'vue' import { defineComponent, computed } from 'vue'
import NavBarLink from './NavBarLink.vue' import NavBarLink from './NavBarLink.vue'
import { withBase } from '../utils' import { withBase } from '../utils'
import { usePageData, useSiteData } from 'vitepress' import { useRoute, useSiteData } from 'vitepress'
export default defineComponent({ export default defineComponent({
components: { components: {
@ -65,10 +65,10 @@ export default defineComponent({
}, },
setup() { setup() {
const pageData = usePageData() const route = useRoute()
const siteData = useSiteData() const siteData = useSiteData()
const data = computed(() => pageData.value.frontmatter) const data = computed(() => route.data.frontmatter)
const actionLink = computed(() => ({ const actionLink = computed(() => ({
link: data.value.actionLink, link: data.value.actionLink,
text: data.value.actionText text: data.value.actionText

@ -1,13 +1,14 @@
import { computed } from 'vue' import { computed } from 'vue'
import { usePageData, useSiteData } from 'vitepress' import { useRoute, useSiteData } from 'vitepress'
import { DefaultTheme } from '../config' import { DefaultTheme } from '../config'
export default { export default {
setup() { setup() {
const pageData = usePageData() const route = useRoute()
// TODO: could this be useSiteData<DefaultTheme.Config> or is the siteData // TODO: could this be useSiteData<DefaultTheme.Config> or is the siteData
// resolved and has a different structure? // resolved and has a different structure?
const siteData = useSiteData() const siteData = useSiteData()
const resolveLink = (targetLink: string) => { const resolveLink = (targetLink: string) => {
let target: DefaultTheme.SideBarLink | undefined let target: DefaultTheme.SideBarLink | undefined
Object.keys(siteData.value.themeConfig.sidebar).some((k) => { Object.keys(siteData.value.themeConfig.sidebar).some((k) => {
@ -24,27 +25,33 @@ export default {
}) })
return target return target
} }
const next = computed(() => { const next = computed(() => {
if (pageData.value.frontmatter.next === false) { const pageData = route.data
if (pageData.frontmatter.next === false) {
return undefined return undefined
} }
if (typeof pageData.value.frontmatter.next === 'string') { if (typeof pageData.frontmatter.next === 'string') {
return resolveLink(pageData.value.frontmatter.next) return resolveLink(pageData.frontmatter.next)
} }
return pageData.value.next return pageData.next
}) })
const prev = computed(() => { const prev = computed(() => {
if (pageData.value.frontmatter.prev === false) { const pageData = route.data
if (pageData.frontmatter.prev === false) {
return undefined return undefined
} }
if (typeof pageData.value.frontmatter.prev === 'string') { if (typeof pageData.frontmatter.prev === 'string') {
return resolveLink(pageData.value.frontmatter.prev) return resolveLink(pageData.frontmatter.prev)
} }
return pageData.value.prev return pageData.prev
}) })
const hasLinks = computed(() => { const hasLinks = computed(() => {
return !!next || !!prev return !!next || !!prev
}) })
return { return {
next, next,
prev, prev,

@ -1,7 +1,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import OutboundLink from './icons/OutboundLink.vue' import OutboundLink from './icons/OutboundLink.vue'
import { endingSlashRE, isExternal } from '../utils' import { endingSlashRE, isExternal } from '../utils'
import { usePageData, useSiteData } from 'vitepress' import { useRoute, useSiteData } from 'vitepress'
import { DefaultTheme } from '../config' import { DefaultTheme } from '../config'
function createEditLink( function createEditLink(
@ -42,14 +42,15 @@ export default {
}, },
setup() { setup() {
const pageData = usePageData() const route = useRoute()
const siteData = useSiteData<DefaultTheme.Config>() const siteData = useSiteData<DefaultTheme.Config>()
const editLink = computed(() => { const editLink = computed(() => {
const pageData = route.data
const showEditLink: boolean | undefined = const showEditLink: boolean | undefined =
pageData.value.frontmatter.editLink == null pageData.frontmatter.editLink == null
? siteData.value.themeConfig.editLinks ? siteData.value.themeConfig.editLinks
: pageData.value.frontmatter.editLink : pageData.frontmatter.editLink
const { const {
repo, repo,
docsDir = '', docsDir = '',
@ -57,7 +58,7 @@ export default {
docsRepo = repo docsRepo = repo
} = siteData.value.themeConfig } = siteData.value.themeConfig
const { relativePath } = pageData.value const { relativePath } = pageData
if (showEditLink && relativePath && repo) { if (showEditLink && relativePath && repo) {
return createEditLink( return createEditLink(
repo, repo,
@ -69,6 +70,7 @@ export default {
} }
return null return null
}) })
const editLinkText = computed( const editLinkText = computed(
() => siteData.value.themeConfig.editLinkText || 'Edit this page' () => siteData.value.themeConfig.editLinkText || 'Edit this page'
) )

@ -1,37 +1,21 @@
import { import { useRoute, useSiteDataByRoute } from 'vitepress'
usePageData, import { computed } from 'vue'
useRoute,
useSiteDataByRoute,
useSiteData
} from 'vitepress'
import { computed, h, FunctionalComponent, VNode } from 'vue'
import { Header } from '../../../../types/shared' import { Header } from '../../../../types/shared'
import { isActive, joinUrl, getPathDirName } from '../utils' import { getPathDirName } from '../utils'
import { DefaultTheme } from '../config' import { DefaultTheme } from '../config'
import { useActiveSidebarLinks } from '../composables/activeSidebarLink' import { useActiveSidebarLinks } from '../composables/activeSidebarLink'
import NavBarLinks from './NavBarLinks.vue' import NavBarLinks from './NavBarLinks.vue'
import { SideBarItem } from './SideBarItem'
const SideBarItem: FunctionalComponent<{ export interface ResolvedSidebarItem {
item: ResolvedSidebarItem text: string
}> = (props) => { link?: string
const { isGroup?: boolean
item: { link: relLink, text, children } children?: ResolvedSidebarItem[]
} = props
const route = useRoute()
const pageData = usePageData()
const siteData = useSiteData()
const link = resolveLink(siteData.value.base, relLink || '')
const active = isActive(route, link)
const headers = pageData.value.headers
return h('li', { class: 'sidebar-item' }, [
createLink(active, text, link),
createChildren(active, children, headers)
])
} }
type ResolvedSidebar = ResolvedSidebarItem[]
export default { export default {
components: { components: {
NavBarLinks, NavBarLinks,
@ -39,9 +23,8 @@ export default {
}, },
setup() { setup() {
const pageData = usePageData()
const siteData = useSiteDataByRoute()
const route = useRoute() const route = useRoute()
const siteData = useSiteDataByRoute()
useActiveSidebarLinks() useActiveSidebarLinks()
@ -50,7 +33,7 @@ export default {
const { const {
headers, headers,
frontmatter: { sidebar, sidebarDepth = 2 } frontmatter: { sidebar, sidebarDepth = 2 }
} = pageData.value } = route.data
if (sidebar === 'auto') { if (sidebar === 'auto') {
// auto, render headers of current page // auto, render headers of current page
@ -84,19 +67,6 @@ export default {
} }
} }
interface HeaderWithChildren extends Header {
children?: Header[]
}
type ResolvedSidebar = ResolvedSidebarItem[]
interface ResolvedSidebarItem {
text: string
link?: string
isGroup?: boolean
children?: ResolvedSidebarItem[]
}
function resolveAutoSidebar(headers: Header[], depth: number): ResolvedSidebar { function resolveAutoSidebar(headers: Header[], depth: number): ResolvedSidebar {
const ret: ResolvedSidebar = [] const ret: ResolvedSidebar = []
@ -151,68 +121,3 @@ function resolveMultiSidebar(
return [] return []
} }
function resolveLink(base: string, path: string): string | undefined {
return path
? // keep relative hash to the same page
path.startsWith('#')
? path
: joinUrl(base, path)
: undefined
}
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
}))
}

@ -0,0 +1,91 @@
import { useRoute, useSiteData } from 'vitepress'
import { FunctionalComponent, h, VNode } from 'vue'
import { Header } from '../../../../types/shared'
import { joinUrl, isActive } from '../utils'
import { ResolvedSidebarItem } from './SideBar'
interface HeaderWithChildren extends Header {
children?: Header[]
}
export const SideBarItem: FunctionalComponent<{
item: ResolvedSidebarItem
}> = (props) => {
const {
item: { link: relLink, text, children }
} = props
const route = useRoute()
const siteData = useSiteData()
const link = resolveLink(siteData.value.base, relLink || '')
const active = isActive(route, link)
const headers = route.data.headers
const childItems = createChildren(active, children, headers)
return h('li', { class: 'sidebar-item' }, [
h(
link ? 'a' : 'p',
{
class: { 'sidebar-link': true, active },
href: link
},
text
),
childItems
])
}
function resolveLink(base: string, path: string): string | undefined {
return path
? // keep relative hash to the same page
path.startsWith('#')
? path
: joinUrl(base, path)
: undefined
}
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
}))
}

@ -36,7 +36,8 @@ export function useActiveSidebarLinks() {
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash) sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
) as HTMLAnchorElement[] ) as HTMLAnchorElement[]
const pageOffset = document.getElementById('app')!.offsetTop const pageOffset = (document.querySelector('.navbar') as HTMLElement)
.offsetHeight
const scrollTop = window.scrollY const scrollTop = window.scrollY
const getAnchorTop = (anchor: HTMLAnchorElement): number => const getAnchorTop = (anchor: HTMLAnchorElement): number =>
@ -50,6 +51,8 @@ export function useActiveSidebarLinks() {
(scrollTop >= getAnchorTop(anchor) && (scrollTop >= getAnchorTop(anchor) &&
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor))) (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
// TODO: fix case when at page bottom
if (isActive) { if (isActive) {
const targetHash = decode(anchor.hash) const targetHash = decode(anchor.hash)
history.replaceState(null, document.title, targetHash) history.replaceState(null, document.title, targetHash)

@ -95,8 +95,8 @@ body {
} }
.theme main { .theme main {
margin-top: var(--header-height); padding-top: var(--header-height);
margin-left: var(--sidebar-width); padding-left: var(--sidebar-width);
} }
@media screen and (min-width: 720px) { @media screen and (min-width: 720px) {

@ -1,7 +1,7 @@
import { useSiteData, Route } from 'vitepress' import { useSiteData, Route } from 'vitepress'
export const hashRE = /#.*$/ export const hashRE = /#.*$/
export const extRE = /\.(md|html)$/ export const extRE = /(index)?\.(md|html)$/
export const endingSlashRE = /\/$/ export const endingSlashRE = /\/$/
export const outboundRE = /^[a-z]+:/i export const outboundRE = /^[a-z]+:/i

@ -2,22 +2,20 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"outDir": "../../dist", "outDir": "../../dist/client",
"module": "esnext", "module": "esnext",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"types": ["vite"], "types": ["vite"],
"paths": { "paths": {
"/@app/*": ["app/*"], "/@app/*": ["app/*"],
"/@theme/*": ["theme-default/*"], "/@theme/*": ["theme-default/*"],
"/@shared/*": ["../shared/*"], "/@shared/*": ["shared/*"],
"/@types/*": ["../../types/*"],
"vitepress": ["app/exports.ts"] "vitepress": ["app/exports.ts"]
} }
}, },
"include": [ "include": [
".", ".",
"../../types/shared.d.ts", "../../types/shared.d.ts",
],
"exclude": [
"../shared"
] ]
} }

@ -6,7 +6,7 @@ import { createResolver, APP_PATH } from './resolver'
import { Resolver } from 'vite' import { Resolver } from 'vite'
import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared' import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
export { resolveSiteDataByRoute } from '../shared/config' export { resolveSiteDataByRoute } from './shared/config'
const debug = require('debug')('vitepress:config') const debug = require('debug')('vitepress:config')

@ -1,10 +1,17 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../dist", "baseUrl": ".",
"outDir": "../../dist/node",
"module": "commonjs", "module": "commonjs",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"sourceMap": true "sourceMap": true,
"paths": {
"/@types/*": ["../../types/*"]
}
}, },
"include": [".", "../shared", "../../types/shared.d.ts"] "include": [
".",
"../../types/shared.d.ts"
]
} }

@ -1,4 +1,4 @@
import { SiteData } from '../../types/shared' import { SiteData } from '/@types/shared'
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'

@ -2,12 +2,12 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"outDir": "../../dist/client/shared", "paths": {
"module": "esnext", "/@types/*": ["../../types/*"]
"lib": ["ESNext", "DOM"], }
}, },
"include": [ "include": [
".", ".",
"../../types/shared.d.ts", "../../types/shared.d.ts"
] ]
} }

Loading…
Cancel
Save