diff --git a/__tests__/unit/client/theme-default/composables/outline.test.ts b/__tests__/unit/client/theme-default/composables/outline.test.ts index 06180f1a..af659c9f 100644 --- a/__tests__/unit/client/theme-default/composables/outline.test.ts +++ b/__tests__/unit/client/theme-default/composables/outline.test.ts @@ -1,5 +1,11 @@ import { resolveHeaders } from 'client/theme-default/composables/outline' +const element = { + classList: { + contains: () => false + } +} as unknown as HTMLHeadElement + describe('client/theme-default/composables/outline', () => { describe('resolveHeader', () => { test('levels range', () => { @@ -9,12 +15,14 @@ describe('client/theme-default/composables/outline', () => { { level: 2, title: 'h2 - 1', - link: '#h2-1' + link: '#h2-1', + element }, { level: 3, title: 'h3 - 1', - link: '#h3-1' + link: '#h3-1', + element } ], [2, 3] @@ -28,9 +36,12 @@ describe('client/theme-default/composables/outline', () => { { level: 3, title: 'h3 - 1', - link: '#h3-1' + link: '#h3-1', + children: [], + element } - ] + ], + element } ]) }) @@ -42,12 +53,14 @@ describe('client/theme-default/composables/outline', () => { { level: 2, title: 'h2 - 1', - link: '#h2-1' + link: '#h2-1', + element }, { level: 3, title: 'h3 - 1', - link: '#h3-1' + link: '#h3-1', + element } ], 2 @@ -56,7 +69,9 @@ describe('client/theme-default/composables/outline', () => { { level: 2, title: 'h2 - 1', - link: '#h2-1' + link: '#h2-1', + children: [], + element } ]) }) @@ -68,42 +83,50 @@ describe('client/theme-default/composables/outline', () => { { level: 2, title: 'h2 - 1', - link: '#h2-1' + link: '#h2-1', + element }, { level: 3, title: 'h3 - 1', - link: '#h3-1' + link: '#h3-1', + element }, { level: 4, title: 'h4 - 1', - link: '#h4-1' + link: '#h4-1', + element }, { level: 3, title: 'h3 - 2', - link: '#h3-2' + link: '#h3-2', + element }, { level: 4, title: 'h4 - 2', - link: '#h4-2' + link: '#h4-2', + element }, { level: 2, title: 'h2 - 2', - link: '#h2-2' + link: '#h2-2', + element }, { level: 3, title: 'h3 - 3', - link: '#h3-3' + link: '#h3-3', + element }, { level: 4, title: 'h4 - 3', - link: '#h4-3' + link: '#h4-3', + element } ], 'deep' @@ -122,9 +145,12 @@ describe('client/theme-default/composables/outline', () => { { level: 4, title: 'h4 - 1', - link: '#h4-1' + link: '#h4-1', + children: [], + element } - ] + ], + element }, { level: 3, @@ -134,11 +160,15 @@ describe('client/theme-default/composables/outline', () => { { level: 4, title: 'h4 - 2', - link: '#h4-2' + link: '#h4-2', + children: [], + element } - ] + ], + element } - ] + ], + element }, { level: 2, @@ -153,11 +183,15 @@ describe('client/theme-default/composables/outline', () => { { level: 4, title: 'h4 - 3', - link: '#h4-3' + link: '#h4-3', + children: [], + element } - ] + ], + element } - ] + ], + element } ]) }) diff --git a/src/client/theme-default/composables/outline.ts b/src/client/theme-default/composables/outline.ts index ff9eca27..a3f561d0 100644 --- a/src/client/theme-default/composables/outline.ts +++ b/src/client/theme-default/composables/outline.ts @@ -13,7 +13,7 @@ export type MenuItem = Omit & { children?: MenuItem[] } -export function resolveTitle(theme: DefaultTheme.Config) { +export function resolveTitle(theme: DefaultTheme.Config): string { return ( (typeof theme.outline === 'object' && !Array.isArray(theme.outline) && @@ -23,7 +23,7 @@ export function resolveTitle(theme: DefaultTheme.Config) { ) } -export function getHeaders(range: DefaultTheme.Config['outline']) { +export function getHeaders(range: DefaultTheme.Config['outline']): MenuItem[] { const headers = [ ...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)') ] @@ -80,38 +80,13 @@ export function resolveHeaders( ? [2, 6] : levelsRange - headers = headers.filter((h) => h.level >= high && h.level <= low) - // clear previous caches - resolvedHeaders.length = 0 - // update global header list for active link rendering - for (const { element, link } of headers) { - resolvedHeaders.push({ element, link }) - } - - 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 - } - } - ret.push(cur) - } - } - - return ret + return buildTree(headers, high, low) } export function useActiveAnchor( container: Ref, marker: Ref -) { +): void { const { isAsideEnabled } = useAside() const onScroll = throttleAndDebounce(setActiveLink, 100) @@ -221,3 +196,38 @@ function getAbsoluteTop(element: HTMLElement): number { } return offsetTop } + +function buildTree(data: MenuItem[], min: number, max: number): MenuItem[] { + resolvedHeaders.length = 0 + + const result: MenuItem[] = [] + const stack: (MenuItem | { level: number; shouldIgnore: true })[] = [] + + data.forEach((item) => { + const node = { ...item, children: [] } + let parent = stack[stack.length - 1] + + while (parent && parent.level >= node.level) { + stack.pop() + parent = stack[stack.length - 1] + } + + if ( + node.element.classList.contains('ignore-header') || + (parent && 'shouldIgnore' in parent) + ) { + stack.push({ level: node.level, shouldIgnore: true }) + return + } + + if (node.level > max || node.level < min) return + resolvedHeaders.push({ element: node.element, link: node.link }) + + if (parent) parent.children!.push(node) + else result.push(node) + + stack.push(node) + }) + + return result +}