All files / internal / testing / filter.ts

100.00% Branches 36/36
100.00% Functions 2/2
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x540
x540
x1
x1
x539
x540
x534
x534
x539
x540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x32
 
x4219
x42
x42
x42
 
x4219
x53
x53
x53
 
x4219
x20
x20
 
x4219
x1670
x39527
x4
x4
x39523
x456
x39523
x1670
x1670
 
x4219
x15
x15
 
x4177
x4177
x4219













































































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