fix(theme): re-support dynamic headers

pull/2065/head
Evan You 1 year ago
parent f6cb4c0d44
commit 657a7d38df

@ -7,23 +7,14 @@ describe('client/theme-default/composables/outline', () => {
resolveHeaders(
[
{
level: 1,
title: 'h1 - 1',
link: '#h1-1',
children: [
{
level: 2,
title: 'h2 - 1',
link: '#h2-1',
children: [
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
}
]
}
]
level: 2,
title: 'h2 - 1',
link: '#h2-1'
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
}
],
[2, 3]
@ -51,14 +42,12 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1',
children: [
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
}
]
link: '#h2-1'
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
}
],
2
@ -79,52 +68,42 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1',
children: [
{
level: 3,
title: 'h3 - 1',
link: '#h3-1',
children: [
{
level: 4,
title: 'h4 - 1',
link: '#h4-1'
}
]
},
{
level: 3,
title: 'h3 - 2',
link: '#h3-2',
children: [
{
level: 4,
title: 'h4 - 2',
link: '#h4-2'
}
]
}
]
link: '#h2-1'
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
},
{
level: 4,
title: 'h4 - 1',
link: '#h4-1'
},
{
level: 3,
title: 'h3 - 2',
link: '#h3-2'
},
{
level: 4,
title: 'h4 - 2',
link: '#h4-2'
},
{
level: 2,
title: 'h2 - 2',
link: '#h2-2',
children: [
{
level: 3,
title: 'h3 - 3',
link: '#h3-3',
children: [
{
level: 4,
title: 'h4 - 3',
link: '#h4-3'
}
]
}
]
link: '#h2-2'
},
{
level: 3,
title: 'h3 - 3',
link: '#h3-3'
},
{
level: 4,
title: 'h4 - 3',
link: '#h4-3'
}
],
'deep'

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

@ -1,5 +1,8 @@
import { defineComponent, h } from 'vue'
import { useRoute } from '../router.js'
import { contentUpdatedCallbacks } from '../utils.js'
const runCbs = () => contentUpdatedCallbacks.forEach((fn) => fn())
export const Content = defineComponent({
name: 'VitePressContent',
@ -10,7 +13,12 @@ export const Content = defineComponent({
const route = useRoute()
return () =>
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 { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared.js'
import { onUnmounted } from 'vue'
export { inBrowser } from '../shared.js'
@ -56,3 +57,16 @@ export function pathToFile(path: string): string {
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'
// utilities
export { inBrowser, withBase } from './app/utils.js'
export { inBrowser, withBase, onContentUpdated } from './app/utils.js'
// components
export { Content } from './app/components/Content.js'

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

@ -11,10 +11,25 @@ export type MenuItem = Omit<Header, 'slug' | 'children'> & {
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(
headers: MenuItem[],
range?: DefaultTheme.Config['outline']
) {
): MenuItem[] {
if (range === false) {
return []
}
@ -24,43 +39,33 @@ export function resolveHeaders(
? range.level
: range) || 2
const levels: [number, number] =
const [high, low]: [number, number] =
typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange
const isInRange = (h: MenuItem): boolean =>
h.level >= levels[0] && h.level <= levels[1]
return filterHeaders(headers, isInRange)
}
headers = headers.filter((h) => h.level >= high && h.level <= low)
function filterHeaders(
headers: MenuItem[],
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 {
delete h.children
const ret: MenuItem[] = []
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
}
}
result.push(h)
} else if (h.children) {
result.push(...filterHeaders(h.children, isInRange))
ret.push(cur)
}
})
}
return result
return ret
}
export function useActiveAnchor(

Loading…
Cancel
Save