All files / for / mod.ts

100.00% Branches 24/24
100.00% Lines 84/84
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
 
x10
x10
 
 
 
x10
 
x10
x10
x10
x10
x66
x10
x10
x347
x366
x366
x10
x10
 
x51
x51
x51
x408
x51
x60
x61
x183
x61
 
x68
x272
x60
x201
x64
x278
x64
x60
x61
x183
x61
x60
 
 
x90
x51
x316
x316
x79
x90
x90
x231
 
 
x51
x51
x51
x51
x137
x959
x1139
x137
 
x137
x211
x633
x211
x211
x211
x241
x241
x211
x137
 
x137
x139
x139
 
x137
x137
x1233
x137
x90
 
 
x51
x146
x156
x156
x156
x146
x270
x51
x10
 
 
x10
x10
x10
x10
 
 
x40





































































































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

/** `*for` directive. */
export const _for: Directive<{
  Cache: WeakMap<HTMLElement | Comment, Nullable<{ element: HTMLElement; items: Map<string, HTMLElement> }>>
}> = {
  name: "*for",
  phase: Phase.EXPAND,
  init(this: typeof _for, renderer) {
    renderer.cache<Cache<typeof this>>(this.name, new WeakMap())
  },
  setup(this: typeof _for, renderer, element, { cache, state }) {
    if ((cache.get(element) === null) && (!state[renderer.internal("iterating")])) {
      return false
    }
  },
  async execute(this: typeof _for, renderer, element, { cache, context, state, attributes: [attribute] }) {
    // Compute iterations using the expression parser
    const iterations = [] as Record<PropertyKey, unknown>[]
    try {
      const loop = `()=>{for(${attribute.value}){${renderer.internal("iterations")}.push({${Expression.parse(attribute.value).join(",")}})}}`
      await renderer.evaluate(null, loop, { context, state: { ...state, [renderer.internal("iterations")]: iterations }, args: [] })
    } catch (error) {
      if (!(error instanceof SyntaxError)) {
        renderer.warn(`[${this.name}] error while evaluating expression: ${error}`, element)
        return { final: true }
      }
      // Fallback to iterable values
      try {
        const value = await renderer.evaluate(null, attribute.value, { context, state })
        if (typeof value === "number") {
          iterations.push(...Array.from({ length: value }, () => ({})))
        } else {
          iterations.push(...Object.entries(value as Arg<typeof Object.entries>).map(([$key, $value]) => ({ $key, $value })))
        }
      } catch {
        renderer.warn(`[${this.name}] syntax error in expression: ${attribute.value}`, element)
        return { final: true }
      }
    }

    // Comment out templated element
    let comment = element as unknown as Comment
    if (!renderer.isComment(element)) {
      comment = renderer.comment(element, { directive: this.name as string, expression: attribute.value })
      cache.set(comment, { element, items: new Map() })
    }
    const cached = cache.get(comment)!
    element = cached.element
    const identifiable = renderer.getAttributes(element, _id.name, { first: true })?.value

    // Generate items
    let position = comment as Comment | HTMLElement
    const generated = new Set<string>()
    const pending = []
    for (let i = 0; i < iterations.length; i++) {
      const iteration = context.with(iterations[i])
      const meta = { $i: i, $I: i + 1, $iterations: iterations.length, $first: i === 0, $last: i === iterations.length - 1 }
      const id = identifiable ? `${await renderer.evaluate(null, identifiable, { context: iteration, state: { ...state, ...meta } })}` : `${i}`
      generated.add(id)
      // Create item if non-existent
      if (!cached.items.has(id)) {
        const item = element.cloneNode(true) as HTMLElement
        item.removeAttributeNode(renderer.getAttributes(item, this.name, { first: true })!)
        cached.items.set(id, item)
        cache.set(item, null)
        if (identifiable) {
          renderer.setAttribute(item, _id.name, id)
        }
      }
      const item = cached.items.get(id)!
      // Handle commented out items
      if (renderer.getComment(item)) {
        renderer.uncomment(renderer.getComment(item)!)
      }
      // Insert item
      comment.parentNode?.insertBefore(item, position.nextSibling)
      position = item
      pending.push(await renderer.render(item, { context: iteration, state: { ...state, [renderer.internal("iterating")]: true, ...meta, $id: id } }))
    }
    await Promise.allSettled(pending)

    // Remove outdated items
    for (const [id, item] of cached.items) {
      if (!generated.has(id)) {
        cached.items.delete(id)
        item.remove()
      }
    }
    return { final: true }
  },
}

/** `*id` directive. */
export const _id = {
  name: "*id",
  phase: Phase.META,
} as Directive & { name: string }

/** Default exports. */
export default [_for, _id]