All files / if / mod.ts

97.96% Branches 48/49
100.00% Functions 3/3
100.00% Lines 68/68
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
 
x11
 
 
 
 
 
 
 
 
 
 
 
 
x11
x11
x11
x11
x49
x11
x11
x335
x335
x23
x23
x11
x11
x131
 
x131
x131
x260
x46
x46
x46
x15
x15
x46
x15
x15
x15
x15
x15
x46
x46
x46
x26
x22
x22
x108
x108
x108
x108
x22
x26
x104
x104
x39
x39
x104
x46
x46
x11
x49
x49
x11
x11
x46
x46
x85
 
x126
x14
x14
x14
 
x131
x43
x43
x43
x131
x131
x11
 
 
 
 
x11






























I






















































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

/** `*if` cache. */
export type Cache = { templates: WeakMap<Comment, { nodes: Node[]; end: Comment }>; generated: WeakMap<Node, Comment> }

/**
 * `*if` directive.
 *
 * @internal `_directive.value` Force the directive to use specified value rather than the attribute value.
 * @internal `_directive.expression` Set the original expression for the directive if element is commented out.
 * @internal `_directive.directive` Set the original directive name for the directive if element is commented out.
 */
export const _if = {
  name: "*if",
  phase: Phase.TOGGLE,
  init(renderer) {
    renderer.cache<Cache>(this.name, { templates: new WeakMap(), generated: new WeakMap() })
  },
  setup(renderer, element, { cache, state }) {
    const original = (renderer.isComment(element) ? renderer.cache("*").get(element) : null) ?? element
    if ((cache.generated.has(original)) && (!state[renderer.internal("templated")])) {
      return false
    }
  },
  async execute(renderer, element, { attributes: [attribute], context, state }) {
    const result = Boolean(await renderer.evaluate(element, arguments[2]._directive?.value ?? attribute.value, { context, state }))
    // Templates are kept commented out and their content is inserted after the comment when truthy (unless they carry other directives)
    const template = (renderer.isComment(element) ? renderer.cache("*").get(element) : element) as Nullable<HTMLElement>
    const names = [_if.name, arguments[2]._directive?.directive]
    if ((template?.tagName === "TEMPLATE") && (!renderer.directives.some((directive) => (!names.includes(directive.name)) && (renderer.getAttributes(template, directive.name, { first: true }))))) {
      const cache = renderer.cache<Cache>("*if") ?? renderer.cache<Cache>("*if", { templates: new WeakMap(), generated: new WeakMap() })
      let comment = element as Comment
      if (!renderer.isComment(element)) {
        comment = renderer.comment(element, { expression: attribute.value, directive: _if.name, ...arguments[2]._directive })
      }
      if (!cache.templates.has(comment)) {
        const nodes = Array.from((template as HTMLTemplateElement).content.cloneNode(true).childNodes)
        const end = renderer.document.createComment(`[/${arguments[2]._directive?.directive ?? _if.name}]`)
        ;[...nodes, end].forEach((node) => cache.generated.set(node, comment))
        cache.templates.set(comment, { nodes, end })
      }
      const cached = cache.templates.get(comment)!
      const attached = Boolean(cached.end.parentNode)
      if (result) {
        if (!attached) {
          let position = comment as Node
          for (const node of [...cached.nodes, cached.end]) {
            const current = renderer.getComment(node as HTMLElement) ?? node
            comment.parentNode?.insertBefore(current, position.nextSibling)
            position = current
          }
        }
        for (const node of cached.nodes) {
          const current = renderer.getComment(node as HTMLElement) ?? node
          if (current.nodeType !== renderer.window.Node.TEXT_NODE) {
            await renderer.render(current as HTMLElement, { context, state: { ...state, [renderer.internal("templated")]: true } })
          }
        }
      } // Remove content up to the end comment (including content generated by nested directives)
      else if (attached) {
        while ((comment.nextSibling) && (comment.nextSibling !== cached.end)) {
          comment.nextSibling.remove()
        }
        cached.end.remove()
      }
      return (comment !== element) ? { element: comment, final: true } : { final: true }
    }
    switch (true) {
      // Switch comment to element if truthy
      case result && (renderer.isComment(element)) && (renderer.cache("*").has(element)): {
        const original = renderer.uncomment(element)
        return { element: original }
      }
      // Switch element to comment if falsy
      case (!result) && (renderer.isHtmlElement(element)): {
        const comment = renderer.comment(element, { expression: attribute.value, directive: _if.name, ...arguments[2]._directive })
        return { element: comment, final: true }
      }
    }
  },
} as const satisfies Directive<{
  Cache: Cache
}>

/** Default exports. */
export default _if