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 |
|
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"
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] }) {
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 }
}
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 }
}
}
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
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)
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)!
if (renderer.getComment(item)) {
renderer.uncomment(renderer.getComment(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)
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> }>>
}>
export const _id = {
name: "*id",
phase: Phase.META,
} as Directive & { name: string }
export default [_for, _id]
|