All files / clean / mod.ts

100.00% Branches 26/26
100.00% Lines 95/95
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
 
x5
 
 
 
 
 
 
x5
 
 
x5
x5
x5
x5
x5
 
 
x5
x5
x15
x15
x15
x15
x5
x5
 
 
x5
x5
x5
x5
x5
x90
x5
x5
x22
x23
x23
x114
 
 
x22
x24
x24
x22
x24
x24
x25
x25
x24
 
 
x38
x22
x24
x24
x22
x25
x25
x114
x38
x22
x35
x35
 
 
x38
 
x51
x52
x51
x51
x74
x74
x74
x74
x74
x77
x77
x74
x38
x5
x5
 
x84
x84
 
x86
x86
x87
x87
x258
x258
x86
x86
x86
 
 
x86
 
x90
x91
x91
x90
x90
x93
x168
x93
x93
x93
x86
x86
 
x84
x86
x86
x86
x84
x5
 
 
x5
























































































































// 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"),
}

/** `*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 _clean>>(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))) {
          ;(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 Directive<{ directives: WeakSet<HTMLElement>; templates: WeakSet<HTMLElement>; comments: WeakSet<HTMLElement> }, typeof typings>

/** Default exports. */
export default _clean