All files / toc / mod.ts

100.00% Branches 24/24
100.00% Lines 56/56
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 
x5
 
 
 
x5
x5
x5
x5
x5
x28
x5
x5
x18
x19
x19
x30
x18
x20
x20
x18
x18
x19
x19
x18
x108
x18
x20
x20
x25
x25
x18
x5
 
 
x5
 
 
x5
x24
x24
x25
x25
x24
x24
x24
x25
x25
x42
x42
x24
x40
x40
x44
x44
x40
x40
x50
x300
x50
x40
x24
x42
x24





























































// Imports
import { type Cache, type Directive, Phase, type Renderer } from "@mizu/internal/engine"
export type * from "@mizu/internal/engine"

/** `*toc` directive. */
export const _toc = {
  name: "*toc",
  phase: Phase.CONTENT,
  default: "'main'",
  init(renderer) {
    renderer.cache<Cache<typeof _toc>>(this.name, new WeakSet())
  },
  async execute(renderer, element, { attributes: [attribute], cache, ...options }) {
    if (!renderer.isHtmlElement(element)) {
      return
    }
    const { tag, value } = renderer.parseAttribute(attribute)
    if (tag === "ignore") {
      return
    }
    const target = renderer.document.querySelector<HTMLElement>(`${await renderer.evaluate(element, value || this.default, options)}`)
    if (!target) {
      return
    }
    const { level, plus, maxlevel = plus ? "Infinity" : level } = tag.match(/^(?:h(?<level>\d+)(?:(?<plus>\+)?|(?:-h(?<maxlevel>\d+))?))?$/i)?.groups ?? {} as Record<PropertyKey, string>
    const ul = headings(renderer, target, { level: Number(level ?? 1), maxlevel: Number(maxlevel ?? 6), ul: renderer.document.createElement("ul"), skip: cache })
    if (!ul.children.length) {
      return
    }
    cache.add(ul)
    element.innerHTML = ul.outerHTML
  },
} as Directive<WeakSet<HTMLUListElement>> & { default: NonNullable<Directive["default"]> }

/** Default exports. */
export default _toc

/** Generate a `<ul>` element containing all heading elements from specified level and recurse on sub-levels. */
function headings(renderer: Renderer, element: HTMLElement, { level, maxlevel, ul, skip }: { level: number; maxlevel: number; ul: HTMLUListElement; skip: Cache<typeof _toc> }) {
  const hx = Array.from(element.querySelectorAll(`h${level}[id]`)) as HTMLElement[]
  if (!hx.length) {
    return ul
  }
  for (const h of hx) {
    const a = Array.from(h.children).find((child): child is HTMLAnchorElement => child.matches('a[href*="#"]'))
    if (!a) {
      continue
    }
    const li = ul.appendChild(renderer.document.createElement("li"))
    li.appendChild(a.cloneNode(true))
    if (level + 1 <= maxlevel) {
      let element = h.parentElement!
      while ((element.tagName === "HGROUP") || (element.hasAttribute("*toc[ignore]"))) {
        element = element.parentElement!
      }
      const hy = Array.from(element.querySelectorAll(`h${level + 1}[id]`)) as HTMLElement[]
      if (hy.length) {
        const ul = li.appendChild(renderer.document.createElement("ul"))
        headings(renderer, element, { level: level + 1, maxlevel, ul, skip })
      }
    }
  }
  return ul
}