All files / internal / testing / filter.ts

100.00% Branches 24/24
100.00% Lines 39/39
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
68
69
70
71
72
73
74
75
76
77
78
79
80
 
 
x32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1724
x564
x565
x565
x5475
x564
x1090
x1090
x1095
x564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x32
 
x4145
x4187
x4187
x4187
 
x4145
x4198
x4198
x4198
 
x4145
x4168
x4168
 
x4145
x5774
x44198
x44202
x44202
x82618
x83069
x82618
x5774
x5774
 
x4145
x4157
x4157
 
x8216
x8216
x4145













































































// Imports
import type { Arg, Directive, Nullable, Renderer } from "../engine/mod.ts"
import { format } from "./format.ts"

/**
 * Recursively filters an {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/Element | Element} and its subtree and returns the {@linkcode https://developer.mozilla.org/docs/Web/API/Element/innerHTML | Element.innerHTML}.
 *
 * This function can be used to compare two HTML documents.
 *
 * Elements with a `filter-remove` attribute are filtered out.
 *
 * ```ts
 * import { expect } from "@libs/testing"
 * import { Window } from "@mizu/internal/vdom"
 * import { Renderer } from "@mizu/internal/engine"
 * const renderer = await new Renderer(new Window()).ready
 *
 * await using a = new Window(`<a>foo</a>`)
 * await using b = new Window(`<a>foo</a>`)
 * expect(filter(renderer, a.document.documentElement)).toBe(filter(renderer, b.document.documentElement))
 * ```
 */
export function filter(renderer: Renderer, node: Nullable<Element>, { format: _format = true, comments = true, directives = ["*warn", "*id"], clean = "" } = {} as FilterOptions): string {
  if (!node) {
    return ""
  }
  let html = _filter(renderer, node.cloneNode(true) as Element, { comments, directives, clean }).innerHTML
  if (_format) {
    html = format(html)
  }
  return html.trim()
}

/** {@linkcode filter()} options. */
export type FilterOptions = {
  /** Whether to format the output. */
  format?: boolean
  /** Whether to include comments. */
  comments?: boolean
  /** Directives to keep. */
  directives?: Array<Directive["name"]>
  /** Pattern used to clean attributes. */
  clean?: string
}

/** Called by {@linkcode filter()}. */
function _filter(renderer: Renderer, node: Element, { comments, directives, clean } = {} as Arg<typeof filter, 2, true>): Element {
  // Remove comments if asked
  if ((node.nodeType === renderer.window.Node.COMMENT_NODE) && (!comments)) {
    node.remove()
    return node
  }
  // Clean attributes if asked
  if ((node.nodeType === renderer.window.Node.ELEMENT_NODE) && clean) {
    const pattern = new RegExp(clean)
    Array.from(node.attributes).forEach((attribute) => pattern.test(attribute.name) && node.removeAttribute(attribute.name))
  }
  // Patch `style` attribute to be consistent with `deno fmt`
  if ((node.nodeType === renderer.window.Node.ELEMENT_NODE) && (node.hasAttribute("style")) && (!node.getAttribute("style")!.endsWith(";"))) {
    node.setAttribute("style", `${node.getAttribute("style")};`)
  }
  // Remove directives if asked
  if ((node.nodeType === renderer.window.Node.ELEMENT_NODE) && (Array.isArray(directives)) && (!directives.includes("*"))) {
    renderer.directives.forEach((directive) => {
      if (directives.includes(`${directive.name}`)) {
        return
      }
      renderer.getAttributes(node as HTMLElement, directive.name).forEach((attribute) => {
        node.removeAttribute(attribute.name)
      })
    })
  }
  // Remove node if asked
  if ((node.nodeType === renderer.window.Node.ELEMENT_NODE) && (node.hasAttribute("filter-remove"))) {
    node.remove()
  }
  // Recurse
  Array.from(node.childNodes).forEach((child) => _filter(renderer, child as Element, arguments[2]))
  return node
}