feat(theme): add support for {data-outline="level"} attribute syntax for headings, close #757 (#759)

This commit is contained in:
pengzhanbo 2025-11-26 01:13:24 +08:00 committed by GitHub
parent 73f4935ca9
commit fc3676d6dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 95 additions and 15 deletions

View File

@ -139,6 +139,30 @@ permalink: /config/frontmatter/basic/
`'deep'``[2, 6]` 相同,将显示从 `<h2>``<h6>` 的所有标题。
::: 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 }`

View File

@ -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 `<h2>` to `<h6>`.
::: 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"} <!-- Only affects descendant headings of the current heading -->
### Level 3 Heading
#### Level 4 Heading
##### Level 5 Heading <!-- Level 3, 4, and 5 headings will appear in the sidebar -->
###### Level 6 Heading <!-- This heading will NOT appear in the sidebar -->
## Heading 2 <!-- Headings at the same level are not affected -->
### Level 3 Heading <!-- By default, only up to level 3 headings are shown -->
#### Level 4 Heading <!-- Level 4 headings are not shown -->
```
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 }`

View File

@ -42,6 +42,7 @@ const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
lowLevel?: number
}
export const headersSymbol: InjectionKey<Ref<MenuItem[]>> = 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<ThemeOutline, boolean>): 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<HTMLElement | null>, marker: Ref<HTMLElement | null>): void {
const { isAsideEnabled } = useAside()
const router = useRouter()