All files / for / mod.ts

100.00% Branches 40/40
100.00% Functions 3/3
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
103
104
 
x10
x10
 
 
 
x10
x10
x10
x10
x56
x10
x10
x337
x19
x19
x10
x10
 
x41
x41
x41
x41
x41
x9
x1
x1
x1
 
x8
x8
x9
x3
x4
x11
x4
x9
x1
x1
x1
x9
 
 
x39
x41
x28
x28
x28
x39
x39
x41
 
 
x41
x41
x41
x41
x86
x86
x86
x86
 
x86
x74
x74
x74
x74
x74
x30
x30
x74
x86
 
x86
x2
x2
 
x86
x86
x86
x86
x39
 
 
x41
x95
x10
x10
x10
x95
x39
x41
x10
 
 
 
 
x10
x10
x10
x10
 
 
x10





































































































// 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 const satisfies Directive<{
  Cache: 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]