From fc3676d6dcb89ab4f175c8e54d3b61697e0f9233 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 26 Nov 2025 01:13:24 +0800 Subject: [PATCH] feat(theme): add support for `{data-outline="level"}` attribute syntax for headings, close #757 (#759) --- docs/config/frontmatter/basic.md | 24 ++++++++++ docs/en/config/frontmatter/basic.md | 24 ++++++++++ theme/src/client/composables/outline.ts | 62 +++++++++++++++++++------ 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/docs/config/frontmatter/basic.md b/docs/config/frontmatter/basic.md index a8f86723..e194a54c 100644 --- a/docs/config/frontmatter/basic.md +++ b/docs/config/frontmatter/basic.md @@ -139,6 +139,30 @@ permalink: /config/frontmatter/basic/ `'deep'` 与 `[2, 6]` 相同,将显示从 `

` 到 `

` 的所有标题。 +::: tip 小技巧 +在 markdown 内容的 标题后面,使用属性语法 `{data-outline="level"}` / `{outline="level"}`, +可以重新设置当前标题的后代标题的显示的最大级别。 + +**例如**: + +```md /{data-outline="5"}/ +## 标题 1 {data-outline="5"} + +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 + +## 标题 2 + +### 三级标题 +#### 四级标题 +``` + +需要注意的是, `level` 的值应该大于当前标题的级别,否则不会生效。 + +::: + ### prev - 类型: `string | { text: string, link: string, icon?: string }` diff --git a/docs/en/config/frontmatter/basic.md b/docs/en/config/frontmatter/basic.md index b683b4a9..d2f0cef1 100644 --- a/docs/en/config/frontmatter/basic.md +++ b/docs/en/config/frontmatter/basic.md @@ -138,6 +138,30 @@ Display a badge on the right side of the article title. `'deep'` is the same as `[2, 6]`, which displays all headings from `

` to `

`. +::: tip Tips +In markdown content, using the attribute syntax `{data-outline="level"}` / `{outline="level"}` +after a heading allows you to reset the maximum display level for descendant headings under the current heading. + +**For example**: + +```md /{data-outline="5"}/ +## Heading 1 {data-outline="5"} + +### Level 3 Heading +#### Level 4 Heading +##### Level 5 Heading +###### Level 6 Heading + +## Heading 2 + +### Level 3 Heading +#### Level 4 Heading +``` + +Note that the value of `level` should be greater than the level of the current heading; otherwise, it will not take effect. + +::: + ### prev - Type: `string | { text: string, link: string, icon?: string }` diff --git a/theme/src/client/composables/outline.ts b/theme/src/client/composables/outline.ts index b74637a5..a9219a0d 100644 --- a/theme/src/client/composables/outline.ts +++ b/theme/src/client/composables/outline.ts @@ -42,6 +42,7 @@ const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = [] export type MenuItem = Omit & { element: HTMLHeadElement children?: MenuItem[] + lowLevel?: number } export const headersSymbol: InjectionKey> = Symbol( @@ -85,9 +86,41 @@ export function getHeaders(range?: ThemeOutline): MenuItem[] { title: serializeHeader(el), link: `#${el.id}`, level, + lowLevel: getLowLevel(el as HTMLHeadElement, level), } }) - return resolveHeaders(headers, range) + if (range === false) + return [] + + const [high, low] = getRange(range) + return resolveSubRangeHeader(resolveHeaders(headers, high), low) +} + +function getRange(range?: Exclude): readonly [number, number] { + const levelsRange = range || 2 + // [high, low] + return typeof levelsRange === 'number' + ? [levelsRange, levelsRange] + : levelsRange === 'deep' + ? [2, 6] + : levelsRange +} + +function getLowLevel(el: HTMLHeadElement, level: number): number | undefined { + if (!el.hasAttribute('data-outline') && !el.hasAttribute('outline')) + return + + // only support + // data-outline="3" -> star, end -> [level, 3] + const str = (el.getAttribute('data-outline') || el.getAttribute('outline'))?.trim() + if (!str) + return + + const num = Number(str) + if (!Number.isNaN(num) && num >= level) + return num + + return undefined } function serializeHeader(h: Element): string { @@ -137,20 +170,8 @@ function clearHeaderNodeList(list?: ChildNode[]) { } } -export function resolveHeaders(headers: MenuItem[], range?: ThemeOutline): MenuItem[] { - if (range === false) - return [] - - const levelsRange = range || 2 - - const [high, low]: [number, number] - = typeof levelsRange === 'number' - ? [levelsRange, levelsRange] - : levelsRange === 'deep' - ? [2, 6] - : levelsRange - - headers = headers.filter(h => h.level >= high && h.level <= low) +export function resolveHeaders(headers: MenuItem[], high: number): MenuItem[] { + headers = headers.filter(h => h.level >= high) // clear previous caches resolvedHeaders.length = 0 // update global header list for active link rendering @@ -180,6 +201,17 @@ export function resolveHeaders(headers: MenuItem[], range?: ThemeOutline): MenuI return ret } +function resolveSubRangeHeader(headers: MenuItem[], low: number): MenuItem[] { + return headers.map((header) => { + if (header.children?.length) { + const current = header.lowLevel ? Math.max(header.lowLevel, low) : low + const children = header.children.filter(({ level }) => level <= current) + header.children = resolveSubRangeHeader(children, header.lowLevel || low) + } + return header + }) +} + export function useActiveAnchor(container: Ref, marker: Ref): void { const { isAsideEnabled } = useAside() const router = useRouter()