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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 |
x13
x13
x13
x13
x13
x13
x82
x13
x13
x541
x541
x37
x37
x13
x13
x64
x64
x64
x64
x64
x12
x1
x1
x1
x11
x11
x12
x3
x7
x17
x7
x12
x1
x1
x1
x12
x62
x64
x42
x42
x42
x62
x62
x64
x228
x64
x64
x64
x64
x123
x123
x123
x48
x48
x48
x48
x123
x123
x123
x100
x100
x121
x23
x23
x123
x100
x100
x100
x11
x11
x100
x89
x89
x89
x35
x35
x89
x89
x100
x100
x100
x123
x123
x181
x181
x181
x123
x163
x163
x129
x129
x163
x123
x123
x64
x137
x15
x15
x15
x15
x137
x62
x64
x13
x13
x138
x138
x122
x122
x88
x11
x11
x5
x138
x32
x32
x5
x5
x32
x5
x138
x13
x13
x13
x13
x13 |
I
|
import { type Arg, type Cache, type Context, type Directive, type Nullable, Phase, type Renderer } 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 }) {
const original = (renderer.isComment(element) ? renderer.cache("*").get(element) : null) ?? element
if ((cache.get(original) === 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(), contexts: new Map(), iteration: null })
}
const cached = cache.get(comment)!
element = cached.element
const identifiable = renderer.getAttributes(element, _id.name, { first: true })?.value
const expandable = (element.tagName === "TEMPLATE") && (!renderer.directives.some((directive) => (![this.name, _id.name].includes(directive.name)) && (renderer.getAttributes(element, directive.name, { first: true }))))
let position = comment as Node
const generated = new Set<string>()
const pending = []
for (let i = 0; i < iterations.length; i++) {
const meta = { $i: i, $I: i + 1, $iterations: iterations.length, $first: i === 0, $last: i === iterations.length - 1 }
let id = `${i}`
if (identifiable) {
cached.iteration ??= context.with({ ...iterations[i] })
Object.assign(cached.iteration.target, iterations[i])
id = `${await renderer.evaluate(null, identifiable, { context: cached.iteration, state: { ...state, ...meta } })}`
}
generated.add(id)
let iteration = cached.contexts.get(id)
if (!iteration) {
iteration = context.with(iterations[i])
cached.contexts.set(id, iteration)
} else {
Object.assign(iteration.target, iterations[i])
}
if (!cached.items.has(id)) {
let nodes = [] as Node[]
let end = null as Nullable<Comment>
if (expandable) {
nodes = Array.from((element as HTMLTemplateElement).content.cloneNode(true).childNodes)
end = renderer.document.createComment(`[/${this.name}]`)
} else {
const item = element.cloneNode(true) as HTMLElement
item.removeAttributeNode(renderer.getAttributes(item, this.name, { first: true })!)
if (identifiable) {
renderer.setAttribute(item, _id.name, id)
}
nodes = [item]
}
nodes.forEach((node) => renderer.isHtmlElement(node as HTMLElement) && cache.set(node as HTMLElement, null))
cached.items.set(id, { nodes, end })
}
const item = cached.items.get(id)!
for (const node of range(renderer, item)) {
comment.parentNode?.insertBefore(node, position.nextSibling)
position = node
}
for (const node of item.nodes) {
const current = renderer.getComment(node as HTMLElement) ?? node
if (current.nodeType !== renderer.window.Node.TEXT_NODE) {
pending.push(await renderer.render(current as HTMLElement, { 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)
cached.contexts.delete(id)
range(renderer, item).forEach((node) => (node as ChildNode).remove())
}
}
return { final: true }
},
} as const satisfies Directive<{
Cache: WeakMap<HTMLElement | Comment, Nullable<{ element: HTMLElement; items: Map<string, { nodes: Node[]; end: Nullable<Comment> }>; contexts: Map<string, Context>; iteration: Nullable<Context> }>>
}>
function range(renderer: Renderer, { nodes, end }: { nodes: Node[]; end: Nullable<Comment> }) {
const current = nodes.map((node) => renderer.getComment(node as HTMLElement) ?? node)
if (!end) {
return current
}
if (!end.parentNode) {
return [...current, end]
}
const occupied = [] as Node[]
for (let node = (current[0] ?? end) as Nullable<ChildNode>; node; node = node.nextSibling) {
occupied.push(node)
if (node === end) {
break
}
}
return occupied
}
export const _id = {
name: "*id",
phase: Phase.META,
} as Directive & { name: string }
export default [_for, _id]
|