All files / bind / mod.ts

100.00% Branches 37/37
100.00% Lines 82/82
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
 
x8
x8
 
 
 
x8
x8
x8
x8
x8
x8
x74
x74
x74
x8
x8
x93
x94
x94
x93
x407
 
 
x93
x93
x96
x96
x96
x499
x96
x97
x97
x96
 
 
x93
x221
x346
 
x456
x235
x235
x705
x256
x265
x256
x278
x278
x235
x235
x235
 
x459
x238
x238
x714
x262
x275
x297
x297
x297
x297
x297
x302
x302
x275
x262
x282
x282
x238
x238
x239
x239
x238
x238
 
x221
x305
x305
 
x221
x234
x236
x236
x236
x245
x221
x221
x93
x8
 
 
x8
 
 
x8
 
 
x8

































































































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

/** `:bind` directive. */
export const _bind = {
  name: /^:(?!:)(?<attribute>)/,
  prefix: ":",
  phase: Phase.ATTRIBUTE,
  multiple: true,
  init(renderer) {
    if (!renderer.cache(this.name)) {
      renderer.cache<Cache<typeof _bind>>(this.name, new WeakMap())
    }
  },
  async execute(renderer, element, { attributes, cache, ...options }) {
    if (!renderer.isHtmlElement(element)) {
      return
    }
    const cached = cache.get(element) ?? cache.set(element, {}).get(element)!
    const parsed = attributes.map((attribute) => renderer.parseAttribute(attribute, null, { prefix: this.prefix }))

    // Handle shorthand attributes binding
    const shorthand = parsed.findIndex(({ name }) => !name.length)
    if (~shorthand) {
      const [attribute] = parsed.splice(shorthand, 1)
      const value = await renderer.evaluate(element, attribute.value, options)
      if (typeof value === "object") {
        parsed.unshift(...Object.entries(value ?? {}).map(([name, value]) => ({ ...attribute, name, value })))
      } else {
        renderer.warn(`[${this.name}] empty shorthand expects an object but got ${typeof value}, ignoring`, element)
      }
    }

    // Bind attributes
    for (const { name, value: expression, attribute } of parsed) {
      const value = attribute.name === ":" ? expression : await renderer.evaluate(element, expression, options)
      switch (true) {
        // Class attributes
        case name === "class": {
          cached.class ??= element.getAttribute("class") ?? ""
          element.setAttribute("class", `${cached.class}`)
          ;[value].flat(Infinity).forEach((classlist) => {
            if ((typeof classlist === "object") && classlist) {
              Object.entries(classlist).forEach(([name, value]) => value && element.classList.add(name))
            } else if (typeof classlist === "string") {
              element.classList.add(...classlist.split(" ").map((name) => name.trim()).filter(Boolean))
            }
          })
          break
        }
        // Style attributes
        case name === "style": {
          cached.style ??= element.getAttribute("style") ?? ""
          element.setAttribute("style", `${cached.style}`)
          ;[value].flat(Infinity).forEach((style) => {
            if ((typeof style === "object") && style) {
              Object.entries(style).forEach(([name, value]) => {
                const property = name.startsWith("--") ? name : name.replace(/([a-z])([A-Z])/g, "$1-$2")
                const [match = "", priority = ""] = `${value}`.match(/\s!(important)[\s;]*$/) ?? []
                const style = `${value}`.replace(match, "")
                element.style.setProperty(property, style, priority)
                if ((typeof value === "number") && (!element.style.getPropertyValue(property))) {
                  element.style.setProperty(property, `${value}px`)
                }
              })
            } else if (typeof style === "string") {
              element.style.cssText += style
            }
          })
          if (!element.style.length) {
            element.removeAttribute("style")
          }
          break
        }
        // Boolean attributes
        case boolean(element.tagName, name):
          element.toggleAttribute(name, Boolean(value))
          break
        // Generic attributes
        default:
          if ((value === undefined) || (value === null)) {
            element.removeAttribute(name)
            break
          }
          element.setAttribute(name, `${value}`)
      }
    }
  },
} as Directive<WeakMap<HTMLElement, { class?: string; style?: string }>>

/** `:class` directive. */
export const _bind_class = _bind as typeof _bind

/** `:style` directive. */
export const _bind_style = _bind as typeof _bind

/** Default exports. */
export default _bind