All files / for / mod.ts

100.00% Branches 23/23
100.00% Lines 83/83
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
 
x9
x9
 
 
 
x9
x9
x9
x9
x61
x9
x9
x321
x340
x340
x9
x9
 
x48
x48
x48
x384
x48
x56
x57
x171
x57
 
x63
x252
x56
x177
x59
x254
x59
x56
x57
x171
x57
x56
 
 
x85
x48
x296
x296
x74
x85
x85
x218
 
 
x48
x48
x48
x48
x129
x903
x1083
x129
 
x129
x198
x594
x198
x198
x198
x228
x228
x198
x129
 
x129
x131
x131
 
x129
x129
x1161
x129
x85
 
 
x48
x138
x148
x148
x148
x138
x255
x48
x9
 
 
x9
x9
x9
x9
 
 
x36



































































































// 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 = {
  name: "*for",
  phase: Phase.EXPAND,
  init(renderer) {
    renderer.cache<Cache<typeof _for>>(this.name, new WeakMap())
  },
  setup(renderer, element, { cache, state }) {
    if ((cache.get(element) === null) && (!state[renderer.internal("iterating")])) {
      return false
    }
  },
  async execute(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 }
  },
} as Directive<WeakMap<HTMLElement | Comment, Nullable<{ element: HTMLElement; items: Map<string, HTMLElement> }>>>

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

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