From 3e11b6abf5fbe80c2bc733f590ab57c7b2cc06f2 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 1 Sep 2024 18:57:06 +0530 Subject: [PATCH 1/4] feat: allow ignoring certain headers and their subtrees completely in outline closes #4171 --- .../theme-default/composables/outline.test.ts | 80 +++++++++++++------ .../theme-default/composables/outline.ts | 68 +++++++++------- 2 files changed, 96 insertions(+), 52 deletions(-) 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 +} From 77beb4476e69724f4f2cc277be673628dcff3115 Mon Sep 17 00:00:00 2001 From: oponamarchuk <105201981+oponamarchuk@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:35:58 +0300 Subject: [PATCH 2/4] docs: add example for outline frontmatter config (#4138) --- docs/en/reference/frontmatter-config.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/reference/frontmatter-config.md b/docs/en/reference/frontmatter-config.md index db9dacd4..955d4ad7 100644 --- a/docs/en/reference/frontmatter-config.md +++ b/docs/en/reference/frontmatter-config.md @@ -161,6 +161,12 @@ aside: false The levels of header in the outline to display for the page. It's same as [config.themeConfig.outline.level](./default-theme-config#outline), and it overrides the value set in site-level config. +```yaml +--- +outline: [2, 4] +--- +``` + ### lastUpdated - Type: `boolean | Date` From 315c22004993f6f1cbdbb59178e46745d8e505a6 Mon Sep 17 00:00:00 2001 From: LJY <1329327499@qq.com> Date: Sun, 1 Sep 2024 21:42:34 +0800 Subject: [PATCH 3/4] feat(client): add `onAfterPageLoad` hook in router (#4126) --- docs/en/reference/runtime-api.md | 4 ++++ docs/zh/reference/runtime-api.md | 4 ++++ src/client/app/router.ts | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/docs/en/reference/runtime-api.md b/docs/en/reference/runtime-api.md index b65c2661..0f2e9ea0 100644 --- a/docs/en/reference/runtime-api.md +++ b/docs/en/reference/runtime-api.md @@ -107,6 +107,10 @@ interface Router { * updated). Return `false` to cancel the navigation. */ onBeforePageLoad?: (to: string) => Awaitable + /** + * Called after the page component is loaded (before the page component is updated). + */ + onAfterPageLoad?: (to: string) => Awaitable /** * Called after the route changes. */ diff --git a/docs/zh/reference/runtime-api.md b/docs/zh/reference/runtime-api.md index b64d44f5..105906da 100644 --- a/docs/zh/reference/runtime-api.md +++ b/docs/zh/reference/runtime-api.md @@ -102,6 +102,10 @@ interface Router { * 在页面组件加载前(history 状态更新后)调用。返回 `false` 表示取消导航 */ onBeforePageLoad?: (to: string) => Awaitable + /** + * 在页面组件加载后(页面组件实际更新前)调用 + */ + onAfterPageLoad?: (to: string) => Awaitable /** * 在路由更改后调用 */ diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 0e04703e..3c6fb4f7 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -29,6 +29,10 @@ export interface Router { * updated). Return `false` to cancel the navigation. */ onBeforePageLoad?: (to: string) => Awaitable + /** + * Called after the page component is loaded (before the page component is updated). + */ + onAfterPageLoad?: (to: string) => Awaitable /** * Called after the route changes. */ @@ -94,6 +98,8 @@ export function createRouter( throw new Error(`Invalid route component: ${comp}`) } + await router.onAfterPageLoad?.(href) + route.path = inBrowser ? pendingPath : withBase(pendingPath) route.component = markRaw(comp) route.data = import.meta.env.PROD From e8f7dd16f6139fdfd129b86caff4b6613dd1e887 Mon Sep 17 00:00:00 2001 From: Thy <44743274+Thy3634@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:58:54 +0800 Subject: [PATCH 4/4] feat: support adding extra attributes to snippet imports (useful for twoslash) (#4100) --- src/node/markdown/plugins/snippet.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index 99b84740..17131de2 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -16,7 +16,7 @@ import type { MarkdownEnv } from '../../shared' * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}', '[title]'] */ export const rawPathRegexp = - /^(.+?(?:(?:\.([a-z0-9]+))?))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/ + /^(.+?(?:(?:\.([a-z0-9]+))?))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)? ?(\S+)?}))? ?(?:\[(.+)\])?$/ export function rawPathToToken(rawPath: string) { const [ @@ -25,12 +25,13 @@ export function rawPathToToken(rawPath: string) { region = '', lines = '', lang = '', + attrs = '', rawTitle = '' ] = (rawPathRegexp.exec(rawPath) || []).slice(1) const title = rawTitle || filepath.split('/').pop() || '' - return { filepath, extension, region, lines, lang, title } + return { filepath, extension, region, lines, lang, attrs, title } } export function dedent(text: string): string { @@ -126,7 +127,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => { .replace(/^@/, srcDir) .trim() - const { filepath, extension, region, lines, lang, title } = + const { filepath, extension, region, lines, lang, attrs, title } = rawPathToToken(rawPath) state.line = startLine + 1 @@ -134,7 +135,7 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => { const token = state.push('fence', 'code', 0) token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${ title ? `[${title}]` : '' - }` + } ${attrs ?? ''}` const { realPath, path: _path } = state.env as MarkdownEnv const resolvedPath = path.resolve(path.dirname(realPath ?? _path), filepath)