fix(theme): re-support dynamic headers

pull/2065/head
Evan You 2 years ago
parent f6cb4c0d44
commit 657a7d38df

@ -6,25 +6,16 @@ describe('client/theme-default/composables/outline', () => {
expect( expect(
resolveHeaders( resolveHeaders(
[ [
{
level: 1,
title: 'h1 - 1',
link: '#h1-1',
children: [
{ {
level: 2, level: 2,
title: 'h2 - 1', title: 'h2 - 1',
link: '#h2-1', link: '#h2-1'
children: [ },
{ {
level: 3, level: 3,
title: 'h3 - 1', title: 'h3 - 1',
link: '#h3-1' link: '#h3-1'
} }
]
}
]
}
], ],
[2, 3] [2, 3]
) )
@ -51,15 +42,13 @@ describe('client/theme-default/composables/outline', () => {
{ {
level: 2, level: 2,
title: 'h2 - 1', title: 'h2 - 1',
link: '#h2-1', link: '#h2-1'
children: [ },
{ {
level: 3, level: 3,
title: 'h3 - 1', title: 'h3 - 1',
link: '#h3-1' link: '#h3-1'
} }
]
}
], ],
2 2
) )
@ -79,53 +68,43 @@ describe('client/theme-default/composables/outline', () => {
{ {
level: 2, level: 2,
title: 'h2 - 1', title: 'h2 - 1',
link: '#h2-1', link: '#h2-1'
children: [ },
{ {
level: 3, level: 3,
title: 'h3 - 1', title: 'h3 - 1',
link: '#h3-1', link: '#h3-1'
children: [ },
{ {
level: 4, level: 4,
title: 'h4 - 1', title: 'h4 - 1',
link: '#h4-1' link: '#h4-1'
}
]
}, },
{ {
level: 3, level: 3,
title: 'h3 - 2', title: 'h3 - 2',
link: '#h3-2', link: '#h3-2'
children: [ },
{ {
level: 4, level: 4,
title: 'h4 - 2', title: 'h4 - 2',
link: '#h4-2' link: '#h4-2'
}
]
}
]
}, },
{ {
level: 2, level: 2,
title: 'h2 - 2', title: 'h2 - 2',
link: '#h2-2', link: '#h2-2'
children: [ },
{ {
level: 3, level: 3,
title: 'h3 - 3', title: 'h3 - 3',
link: '#h3-3', link: '#h3-3'
children: [ },
{ {
level: 4, level: 4,
title: 'h4 - 3', title: 'h4 - 3',
link: '#h4-3' link: '#h4-3'
} }
]
}
]
}
], ],
'deep' 'deep'
) )

@ -14,6 +14,12 @@ export default defineConfig({
head: [['meta', { name: 'theme-color', content: '#3c8772' }]], head: [['meta', { name: 'theme-color', content: '#3c8772' }]],
markdown: {
headers: {
level: [0, 0]
}
},
themeConfig: { themeConfig: {
nav: nav(), nav: nav(),

@ -1,5 +1,8 @@
import { defineComponent, h } from 'vue' import { defineComponent, h } from 'vue'
import { useRoute } from '../router.js' import { useRoute } from '../router.js'
import { contentUpdatedCallbacks } from '../utils.js'
const runCbs = () => contentUpdatedCallbacks.forEach((fn) => fn())
export const Content = defineComponent({ export const Content = defineComponent({
name: 'VitePressContent', name: 'VitePressContent',
@ -10,7 +13,12 @@ export const Content = defineComponent({
const route = useRoute() const route = useRoute()
return () => return () =>
h(props.as, { style: { position: 'relative' } }, [ h(props.as, { style: { position: 'relative' } }, [
route.component ? h(route.component) : '404 Page Not Found' route.component
? h(route.component, {
onVnodeMounted: runCbs,
onVnodeUpdated: runCbs
})
: '404 Page Not Found'
]) ])
} }
}) })

@ -1,5 +1,6 @@
import { siteDataRef } from './data.js' import { siteDataRef } from './data.js'
import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared.js' import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared.js'
import { onUnmounted } from 'vue'
export { inBrowser } from '../shared.js' export { inBrowser } from '../shared.js'
@ -56,3 +57,16 @@ export function pathToFile(path: string): string {
return pagePath return pagePath
} }
export let contentUpdatedCallbacks: (() => any)[] = []
/**
* Register callback that is called every time the markdown content is updated
* in the DOM.
*/
export function onContentUpdated(fn: () => any) {
contentUpdatedCallbacks.push(fn)
onUnmounted(() => {
contentUpdatedCallbacks = contentUpdatedCallbacks.filter((f) => f !== fn)
})
}

@ -19,7 +19,7 @@ export { useData } from './app/data.js'
export { useRouter, useRoute } from './app/router.js' export { useRouter, useRoute } from './app/router.js'
// utilities // utilities
export { inBrowser, withBase } from './app/utils.js' export { inBrowser, withBase, onContentUpdated } from './app/utils.js'
// components // components
export { Content } from './app/components/Content.js' export { Content } from './app/components/Content.js'

@ -1,21 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref, shallowRef } from 'vue'
import { useData } from '../composables/data.js' import { useData } from '../composables/data.js'
import { import {
resolveHeaders, getHeaders,
useActiveAnchor useActiveAnchor,
type MenuItem
} from '../composables/outline.js' } from '../composables/outline.js'
import VPDocAsideOutlineItem from './VPDocAsideOutlineItem.vue' import VPDocAsideOutlineItem from './VPDocAsideOutlineItem.vue'
import { onContentUpdated } from 'vitepress'
const { frontmatter, page, theme } = useData() const { frontmatter, theme } = useData()
const headers = computed(() => { const headers = shallowRef<MenuItem[]>([])
return resolveHeaders(
page.value.headers, onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme.value.outline frontmatter.value.outline ?? theme.value.outline
) )
}) })
const hasOutline = computed(() => headers.value.length > 0)
const container = ref() const container = ref()
const marker = ref() const marker = ref()
@ -32,7 +34,7 @@ function handleClick({ target: el }: Event) {
</script> </script>
<template> <template>
<div class="VPDocAsideOutline" :class="{ 'has-outline': hasOutline }" ref="container"> <div class="VPDocAsideOutline" :class="{ 'has-outline': headers.length > 0 }" ref="container">
<div class="content"> <div class="content">
<div class="outline-marker" ref="marker" /> <div class="outline-marker" ref="marker" />

@ -11,10 +11,25 @@ export type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[] children?: MenuItem[]
} }
export function getHeaders(range?: DefaultTheme.Config['outline']) {
const headers = [...document.querySelectorAll('.VPDoc h2,h3,h4,h5,h6')]
.filter((el) => el.id && el.firstChild && el.firstChild.nodeType === 3)
.map((el) => {
const level = Number(el.tagName[1])
return {
title: (el.firstChild as Text).data.trim(),
link: '#' + el.id,
level
}
})
return resolveHeaders(headers, range)
}
export function resolveHeaders( export function resolveHeaders(
headers: MenuItem[], headers: MenuItem[],
range?: DefaultTheme.Config['outline'] range?: DefaultTheme.Config['outline']
) { ): MenuItem[] {
if (range === false) { if (range === false) {
return [] return []
} }
@ -24,43 +39,33 @@ export function resolveHeaders(
? range.level ? range.level
: range) || 2 : range) || 2
const levels: [number, number] = const [high, low]: [number, number] =
typeof levelsRange === 'number' typeof levelsRange === 'number'
? [levelsRange, levelsRange] ? [levelsRange, levelsRange]
: levelsRange === 'deep' : levelsRange === 'deep'
? [2, 6] ? [2, 6]
: levelsRange : levelsRange
const isInRange = (h: MenuItem): boolean => headers = headers.filter((h) => h.level >= high && h.level <= low)
h.level >= levels[0] && h.level <= levels[1]
return filterHeaders(headers, isInRange) const ret: MenuItem[] = []
} outer: for (let i = 0; i < headers.length; i++) {
const cur = headers[i]
function filterHeaders( if (i === 0) {
headers: MenuItem[], ret.push(cur)
isInRange: (h: MenuItem) => boolean
) {
const result: MenuItem[] = []
headers = headers.map((h) => ({ ...h }))
headers.forEach((h) => {
if (isInRange(h)) {
if (h.children) {
const filteredChildren = filterHeaders(h.children, isInRange)
if (filteredChildren.length) {
h.children = filteredChildren
} else { } else {
delete h.children 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
} }
} }
result.push(h) ret.push(cur)
} else if (h.children) { }
result.push(...filterHeaders(h.children, isInRange))
} }
})
return result return ret
} }
export function useActiveAnchor( export function useActiveAnchor(

Loading…
Cancel
Save