Skip to content

Commit

Permalink
fix(theme): re-support dynamic headers
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Mar 10, 2023
1 parent f6cb4c0 commit 657a7d3
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 105 deletions.
113 changes: 46 additions & 67 deletions __tests__/unit/client/theme-default/composables/outline.test.ts
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions docs/.vitepress/config.ts
Expand Up @@ -14,6 +14,12 @@ export default defineConfig({

head: [['meta', { name: 'theme-color', content: '#3c8772' }]],

markdown: {
headers: {
level: [0, 0]
}
},

themeConfig: {
nav: nav(),

Expand Down
10 changes: 9 additions & 1 deletion src/client/app/components/Content.ts
@@ -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',
Expand All @@ -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'
])
}
})
14 changes: 14 additions & 0 deletions src/client/app/utils.ts
@@ -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'

Expand Down Expand Up @@ -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)
})
}
2 changes: 1 addition & 1 deletion src/client/index.ts
Expand Up @@ -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'
20 changes: 11 additions & 9 deletions src/client/theme-default/components/VPDocAsideOutline.vue
@@ -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()
Expand All @@ -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" />

Expand Down
59 changes: 32 additions & 27 deletions src/client/theme-default/composables/outline.ts
Expand Up @@ -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 []
}
Expand All @@ -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(
Expand Down

1 comment on commit 657a7d3

@adrianthedev
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for bringing this back @yyx990803.

I'm using this feature to create components that have themselves h2s and without this feature, those h2s would not appear on the right sidebar as expected.

Please sign in to comment.