All files / toc / mod.ts

100.00% Branches 40/40
100.00% Functions 3/3
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
65
66
67
 
x5
 
 
 
x5
x5
x5
x5
x5
x23
x5
x5
x13
x1
x1
x12
x13
x2
x2
x13
x13
x1
x1
x13
x13
x13
x2
x2
x7
x7
x13
x5
 
 
 
 
 
x5
 
 
x5
x19
x19
x1
x1
x19
x19
x19
x1
x1
x18
x18
x19
x16
x16
x4
x4
x16
x16
x10
x10
x10
x16
x19
x18
x19
































































// 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 const satisfies Directive<{
  Cache: WeakSet<HTMLUListElement>
  Default: true
}>

/** 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
}