Skip to content

Commit

Permalink
feat(helper): add getHeaders() (#189)
Browse files Browse the repository at this point in the history
Co-authored-by: Mister-Hope <mister-hope@outlook.com>
  • Loading branch information
pengzhanbo and Mister-Hope authored Jun 2, 2024
1 parent 03646b9 commit 421fece
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 0 deletions.
100 changes: 100 additions & 0 deletions docs/tools/helper/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,103 @@ locale.value // '标题'
```

:::

## Utils

### getHeaders

Get headers from current page.

```ts
export const getHeaders: (options: GetHeadersOptions) => MenuItem[]
```
**Params:**
```ts
export interface GetHeadersOptions {
/**
* The selector of the headers.
*
* It will be passed as an argument to `document.querySelectorAll(selector)`,
* so you should pass a `CSS Selector` string.
*
* @default '#vp-content h1, #vp-content h2, #vp-content h3, #vp-content h4, #vp-content h5, #vp-content h6'
*/
selector?: string
/**
* Ignore specific elements within the header.
*
* The Array of `CSS Selector`
*
* @default []
*/
ignore?: string[]
/**
* The levels of the headers
*
* - `false`: No headers.
* - `number`: only headings of that level will be displayed.
* - `[number, number]: headings level tuple, where the first number should be less than the second number, for example, `[2, 4]` which means all headings from `<h2>` to `<h4>` will be displayed.
* - `deep`: same as `[2, 6]`, which means all headings from `<h2>` to `<h6>` will be displayed.
*
* @default 2
*/
levels?: HeaderLevels
}
```

**Result:**

```ts
export interface Header {
/**
* The level of the header
*
* `1` to `6` for `<h1>` to `<h6>`
*/
level: number
/**
* The title of the header
*/
title: string
/**
* The slug of the header
*
* Typically the `id` attr of the header anchor
*/
slug: string
/**
* Link of the header
*
* Typically using `#${slug}` as the anchor hash
*/
link: string
/**
* The children of the header
*/
children: Header[]
}

export type HeaderLevels = false | number | [number, number] | 'deep'

export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
}
```
::: details Examples
```ts
onMounted(() => {
const headers = getHeaders({
selector: '#vp-content :where(h1,h2,h3,h4,h5,h6)',
levels: [2, 3], // only h2 and h3
ignore: ['.badge'], // ignore the <Badge /> within the header
})
console.log(headers)
})
```
:::
100 changes: 100 additions & 0 deletions docs/zh/tools/helper/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,103 @@ locale.value // '标题'
```

:::

## 工具

### getHeaders

获取当前页面指定的 标题列表。

```ts
export const getHeaders: (options: GetHeadersOptions) => MenuItem[]
```
**参数:**
```ts
export interface GetHeadersOptions {
/**
* 页面标题选择器
*
* @default '#vp-content h1, #vp-content h2, #vp-content h3, #vp-content h4, #vp-content h5, #vp-content h6'
*/
selector?: string
/**
* 忽略标题内的特定元素选择器
*
* 它将作为 `document.querySelectorAll` 的参数。
* 因此,你应该传入一个 `CSS 选择器` 字符串
*
* @default []
*/
ignore?: string[]
/**
* 指定获取的标题层级
*
* `1` 至 `6` 表示 `<h1>` 至 `<h6>`
*
* - `false`: 不返回标题列表
* - `number`: 只获取指定的单个层级的标题。
* - `[number, number]: 标题层级元组,第一个数字应小于第二个数字。例如,`[2, 4]` 表示显示从 `<h2>` 到 `<h4>` 的所有标题。
* - `deep`: 等同于 `[2, 6]`, 表示获取从 `<h2>` 到 `<h6>` 的所有标题。
*
* @default 2
*/
levels?: HeaderLevels
}
```

**返回结果:**

```ts
export interface Header {
/**
* 当前标题的层级
*
* `1` 至 `6` 表示 `<h1>` 至 `<h6>`
*/
level: number
/**
* 当前标题的内容
*/
title: string
/**
* 标题的 标识
*
* 这通常是标题元素的 `id` 属性值
*/
slug: string
/**
* 标题的链接
*
* 通常使用`#${slug}`作为锚点哈希
*/
link: string
/**
* 标题的子标题列表
*/
children: Header[]
}

export type HeaderLevels = false | number | [number, number] | 'deep'

export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
}
```
::: details Examples
```ts
onMounted(() => {
const headers = getHeaders({
selector: '#vp-content :where(h1,h2,h3,h4,h5,h6)',
levels: [2, 3], // 只有 h2 和 h3
ignore: ['.badge'], // 忽略标题内的 <Badge />
})
console.log(headers)
})
```
:::
139 changes: 139 additions & 0 deletions tools/helper/src/client/utils/getHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
export interface Header {
/**
* The level of the header
*
* `1` to `6` for `<h1>` to `<h6>`
*/
level: number
/**
* The title of the header
*/
title: string
/**
* The slug of the header
*
* Typically the `id` attr of the header anchor
*/
slug: string
/**
* Link of the header
*
* Typically using `#${slug}` as the anchor hash
*/
link: string
/**
* The children of the header
*/
children: Header[]
}

export type HeaderLevels = false | number | [number, number] | 'deep'

export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
}

export interface GetHeadersOptions {
/**
* The selector of the headers.
*
* It will be passed as an argument to `document.querySelectorAll(selector)`,
* so you should pass a `CSS Selector` string.
*
* @default '#vp-content h1, #vp-content h2, #vp-content h3, #vp-content h4, #vp-content h5, #vp-content h6'
*/
selector?: string
/**
* Ignore specific elements within the header.
*
* The Array of `CSS Selector`
*
* @default []
*/
ignore?: string[]
/**
* The levels of the headers.
*
* `1` to `6` for `<h1>` to `<h6>`
*
* - `false`: No headers.
* - `number`: only headings of that level will be displayed.
* - `[number, number]: headings level tuple, where the first number should be less than the second number, for example, `[2, 4]` which means all headings from `<h2>` to `<h4>` will be displayed.
* - `deep`: same as `[2, 6]`, which means all headings from `<h2>` to `<h6>` will be displayed.
*
* @default 2
*/
levels?: HeaderLevels
}

export const getHeaders = ({
selector = [...new Array(6)].map((_, i) => `#vp-content h${i + 1}`).join(','),
levels = 2,
ignore = [],
}: GetHeadersOptions = {}): MenuItem[] => {
const headers = Array.from(document.querySelectorAll(selector))
.filter((el) => el.id && el.hasChildNodes())
.map((el) => {
const level = Number(el.tagName[1])
return {
element: el as HTMLHeadElement,
title: serializeHeader(el, ignore),
link: '#' + el.id,
slug: el.id,
level,
}
})
return resolveHeaders(headers, levels)
}

const serializeHeader = (h: Element, ignore: string[] = []): string => {
let text = ''
if (ignore.length) {
const clone = h.cloneNode(true) as Element
clone.querySelectorAll(ignore.join(',')).forEach((el) => el.remove())
text = clone.textContent || ''
} else {
text = h.textContent || ''
}
return text.trim()
}

export const resolveHeaders = (
headers: MenuItem[],
levels: HeaderLevels = 2,
): MenuItem[] => {
if (levels === false) {
return []
}

const [high, low]: [number, number] =
typeof levels === 'number'
? [levels, levels]
: levels === 'deep'
? [2, 6]
: levels

headers = headers.filter((h) => h.level >= high && h.level <= low)

const res: MenuItem[] = []
// eslint-disable-next-line no-labels
outer: for (let i = 0; i < headers.length; i++) {
const cur = headers[i]
if (i === 0) {
res.push(cur)
} else {
for (let j = i - 1; j >= 0; j--) {
const prev = headers[j]
if (prev.level < cur.level) {
;(prev.children ??= []).push(cur)
// eslint-disable-next-line no-labels
continue outer
}
}
res.push(cur)
}
}

return res
}
1 change: 1 addition & 0 deletions tools/helper/src/client/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './data.js'
export * from './hasGlobalComponent.js'
export * from './wait.js'
export * from './getHeaders.js'

0 comments on commit 421fece

Please sign in to comment.