Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(helper): add getHeaders() #189

Merged
merged 8 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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] =
pengzhanbo marked this conversation as resolved.
Show resolved Hide resolved
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'
Loading