All files / for / mod.ts

100.00% Branches 24/24
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
 
x10
x10
 
 
 
x10
x10
x10
x10
x64
x10
x10
x333
x352
x352
x10
x10
 
x50
x50
x50
x400
x50
x59
x60
x180
x60
 
x67
x268
x59
x198
x63
x274
x63
x59
x60
x180
x60
x59
 
 
x88
x50
x308
x308
x77
x88
x88
x226
 
 
x50
x50
x50
x50
x133
x931
x1111
x133
 
x133
x204
x612
x204
x204
x204
x234
x234
x204
x133
 
x133
x135
x135
 
x133
x133
x1197
x133
x88
 
 
x50
x142
x152
x152
x152
x142
x264
x50
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 = {
  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]