@valaxyjs/utils
Version:
A utility library for Valaxy
147 lines (124 loc) • 3.82 kB
text/typescript
import type { MenuItem } from '../types'
/**
* @ref vitepress src/client/theme-default/composables/outline.ts
*/
// cached list of anchor elements from resolveHeaders
export const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
export interface GetHeadersOptions {
range?: number | [number, number] | 'deep' | { level: [number, number] }
selector?: string
filter?: (el: Element) => boolean
}
export 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
}
export function addToParent(
currIndex: number,
headers: MenuItem[],
levelsRange: [number, number],
) {
if (currIndex === 0)
return true
const currentHeader = headers[currIndex]
for (let index = currIndex - 1; index >= 0; index--) {
const header = headers[index]
if (
header.level < currentHeader.level
&& header.level >= levelsRange[0]
&& header.level <= levelsRange[1]
) {
if (header.children == null)
header.children = []
header.children.push(currentHeader)
return false
}
}
return true
}
export function resolveHeaders(
headers: MenuItem[],
range: GetHeadersOptions['range'] = [2, 4],
) {
const levelsRange
= (typeof range === 'object' && !Array.isArray(range)
? range.level
: range) || 2
const [high, low]: [number, number]
= typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange
return buildTree(headers, high, low)
}
export function serializeHeader(h: Element): string {
let ret = ''
for (const node of Array.from(h.childNodes)) {
if (node.nodeType === 1) {
if (
(node as Element).classList.contains('VABadge')
|| (node as Element).classList.contains('header-anchor')
) {
continue
}
ret += node.textContent
}
else if (node.nodeType === 3) {
ret += node.textContent
}
}
return ret.trim()
}
// el => el.id && el.hasChildNodes()
/**
* get headers from document directly
*/
export function getHeaders(options: GetHeadersOptions = {
range: [2, 4],
selector: '.markdown-body',
}) {
const mdBodySelector = options.selector || '.markdown-body'
// when transition, the markdown-body will be two
// the first is the old one, the last is the new one
const markdownBodyElements = document.querySelectorAll(mdBodySelector) as NodeListOf<HTMLElement>
const markdownBody = markdownBodyElements[markdownBodyElements.length - 1]
const headers = Array.from(markdownBody?.querySelectorAll(`${mdBodySelector} :where(h1,h2,h3,h4,h5,h6)`) || [])
.filter(el => options.filter ? options.filter(el) : true)
.map((el) => {
const level = Number(el.tagName[1])
return {
element: el as HTMLHeadElement,
title: serializeHeader(el),
link: `#${el.id}`,
level,
// @ts-expect-error lang
lang: el.lang,
}
})
return resolveHeaders(headers, options.range)
}