All files / clean / mod.ts

100.00% Branches 47/47
100.00% Functions 5/5
100.00% Lines 96/96
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
 
x6
 
 
 
 
 
 
x6
 
 
x6
x6
x6
x6
x6
x6
 
 
x6
x6
x6
x6
x6
x6
x6
x6
 
 
x6
x6
x6
x6
x6
x21
x6
x6
x19
x1
x1
x18
 
 
x19
x2
x2
x19
x4
x4
x2
x2
x4
 
 
x18
x19
x2
x2
x19
x4
x4
x18
x18
x19
x13
x13
 
 
x18
 
x13
x1
x13
x13
x11
x11
x11
x11
x11
x3
x3
x11
x18
x6
x6
 
x126
x126
 
x4
x4
x2
x2
x4
x4
x4
x7
x7
 
 
x4
 
x11
x4
x4
x11
x11
x7
x180
x7
x7
x7
x4
x4
 
x126
x2
x2
x2
x126
x6
 
 
 
 
 
x6




























































































































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

/**
 * All spacing characters except non-breaking space.
 * {@link https://developer.mozilla.org/docs/Web/JavaScript/Guide/Regular_expressions/Cheatsheet | MDN reference}
 */
const spacing = "[\\fnrtvu0020u1680u2000-u200au2028u2029u202fu205fu3000ufeff]".replace(/([g-z])/g, "\\$1")

/** Spacing regular expressions. */
const regexp = {
  condense: new RegExp(`${spacing}+`, "g"),
  trim: new RegExp(`(?:^${spacing}*)|(?:${spacing}*$)`, "g"),
  clean: new RegExp(`${spacing}*(\\u00a0)${spacing}*`, "g"),
  closing: /^\[\/\S+\]$/,
}

/** `*clean` typings. */
export const typings = {
  modifiers: {
    comments: { type: Boolean },
    spaces: { type: Boolean },
    directives: { type: Boolean },
    templates: { type: Boolean },
  },
} as const

/** `*clean` directive. */
export const _clean = {
  name: "*clean",
  phase: Phase.CONTENT_CLEANING,
  typings,
  init(renderer) {
    renderer.cache<Cache<typeof this>>(this.name, { directives: new WeakSet(), templates: new WeakSet(), comments: new WeakSet() })
  },
  execute(renderer, element, { attributes: [attribute], cache }) {
    if (!renderer.isHtmlElement(element)) {
      return
    }
    const parsed = renderer.parseAttribute(attribute, this.typings, { modifiers: true })

    // Register delayed cleanup
    if (parsed.modifiers.templates) {
      cache.templates.add(element)
    }
    if (parsed.modifiers.directives) {
      cache.directives.add(element)
      if (parsed.modifiers.comments) {
        cache.comments.add(element)
      }
    }

    // Prepare tree walker
    let filter = 0
    if (parsed.modifiers.spaces) {
      filter |= renderer.window.NodeFilter.SHOW_TEXT
    }
    if (parsed.modifiers.comments) {
      filter |= renderer.window.NodeFilter.SHOW_COMMENT
    }
    const walker = renderer.document.createTreeWalker(element, filter, { acceptNode: () => renderer.window.NodeFilter.FILTER_ACCEPT })
    const nodes = [] as Node[]
    while (walker.nextNode()) {
      nodes.push(walker.currentNode)
    }

    // Cleanup filtered nodes
    nodes.forEach((node) => {
      // Cleanup comments
      if (parsed.modifiers.comments && (node.nodeType === renderer.window.Node.COMMENT_NODE) && (!renderer.cache("*").has(node as Comment))) {
        ;(node as Comment).remove()
      } // Cleanup text nodes
      else if (parsed.modifiers.spaces && (node.nodeType === renderer.window.Node.TEXT_NODE)) {
        node.textContent = node.textContent!
          .replace(regexp.condense, " ")
          .replace(regexp.trim, "")
          .replace(regexp.clean, "$1")
        if (!node.textContent) {
          ;(node as Text).remove()
        }
      }
    })
  },
  cleanup(renderer, _element, { cache }) {
    // Cleanup directives
    const element = _element as HTMLElement
    if (cache.directives.has(element)) {
      // Prepare tree walker
      let filter = renderer.window.NodeFilter.SHOW_ELEMENT
      if (cache.comments.has(element)) {
        filter |= renderer.window.NodeFilter.SHOW_COMMENT
      }
      const walker = renderer.document.createTreeWalker(element, filter, { acceptNode: () => renderer.window.NodeFilter.FILTER_ACCEPT })
      const nodes = [element] as Node[]
      while (walker.nextNode()) {
        nodes.push(walker.currentNode)
      }

      // Cleanup filtered nodes
      nodes.forEach((node) => {
        // Cleanup directives comments
        if ((node.nodeType === renderer.window.Node.COMMENT_NODE) && ((renderer.cache("*").has(node as Comment)) || (regexp.closing.test((node as Comment).data)))) {
          ;(node as Comment).remove()
          cache.comments.delete(element)
        } // Cleanup directives attributes
        else if (node.nodeType === renderer.window.Node.ELEMENT_NODE) {
          renderer.directives.forEach((directive) => {
            renderer.getAttributes(node as HTMLElement, directive.name).forEach((attribute) => (node as HTMLElement).removeAttributeNode(attribute))
          })
          cache.directives.delete(element)
        }
      })
    }
    // Cleanup templates
    if (cache.templates.has(element)) {
      Array.from(element.querySelectorAll("template")).forEach((template) => template.remove())
      cache.templates.delete(element)
    }
  },
} as const satisfies Directive<{
  Cache: { directives: WeakSet<HTMLElement>; templates: WeakSet<HTMLElement>; comments: WeakSet<HTMLElement> }
  Typings: typeof typings
}>

/** Default exports. */
export default _clean