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 |
x8
x8
x8
x8
x8
x8
x8
x8
x8
x8
x8
x8
x34
x8
x8
x136
x13
x13
x13
x13
x13
x13
x13
x13
x13
x13
x8
x8
x20
x1
x1
x20
x1
x1
x1
x20
x20
x1
x1
x1
x20
x3
x3
x20
x2
x2
x2
x12
x12
x12
x12
x12
x12
x14
x1
x1
x13
x13
x13
x14
x17
x17
x12
x12
x17
x9
x9
x17
x17
x17
x17
x17
x13
x14
x13
x13
x14
x1
x1
x14
x12
x12
x20
x8
x8
x8
x8
x8
x8
x8 |
|
import { type Directive, type Nullable, Phase } from "@mizu/internal/engine"
import { isValidCustomElementName } from "@std/html/unstable-is-valid-custom-element-name"
export type * from "@mizu/internal/engine"
export const typings = {
modifiers: {
flat: { type: Boolean },
},
} as const
export const _custom_element = {
name: "*custom-element",
phase: Phase.CUSTOM_ELEMENT,
typings,
init(renderer) {
renderer.cache(this.name, new WeakMap())
},
setup(renderer, element, { cache }) {
if ((renderer.isHtmlElement(element)) && (cache.get(element))) {
return {
state: {
$slots: cache.get(element)!,
$attrs: new Proxy({}, {
has: (_, name: string) => element.hasAttribute(String(name)),
get: (_, name: string) => element.getAttribute(String(name)) ?? undefined,
}),
},
}
}
},
async execute(renderer, element, { cache, attributes: [attribute], ...options }) {
if (!renderer.isHtmlElement(element)) {
return
}
if ((element.tagName !== "TEMPLATE")) {
renderer.warn(`A [${this.name}] directive must be defined on a <template> element, ignoring`, element)
return { final: true }
}
const tagname = isValidCustomElementName(attribute.value) ? attribute.value : `${await renderer.evaluate(element, attribute.value || "''", options)}`
if (!tagname) {
renderer.warn(`A [${this.name}] directive must have a valid custom element name, ignoring`, element)
return { final: true }
}
if (cache.has(element)) {
return { final: true }
}
if (renderer.window.customElements.get(tagname)) {
renderer.warn(`<${tagname}> is already registered as a custom element, ignoring`, element)
return { final: true }
}
cache.set(element, null)
const parsed = renderer.parseAttribute(attribute, this.typings, { modifiers: true })
renderer.window.customElements.define(
tagname,
class extends renderer.window.HTMLElement {
connectedCallback(this: HTMLElement) {
if (renderer.elementHasPhase(this, Phase.EXPAND)) {
return
}
const content = Array.from(renderer.createElement("div", { innerHTML: this.innerHTML.trim() }).childNodes) as HTMLElement[]
this.innerHTML = element.innerHTML
const slots = cache.set(this, {}).get(this)!
for (const child of content) {
const names = []
if (child.nodeType === renderer.window.Node.ELEMENT_NODE) {
names.push(...renderer.getAttributes(child, _slot.name).map((attribute) => attribute.name.slice(_slot.prefix.length)))
}
if (!names.length) {
names.push("")
}
for (const name of names) {
slots[name] ??= renderer.createElement("slot")
slots[name].appendChild(child.cloneNode(true))
}
}
Object.entries(slots).forEach(([name, content]) => {
this.querySelectorAll<HTMLSlotElement>(`slot${name ? `[name="${name}"]` : ":not([name])"}`).forEach((slot) => renderer.replaceElementWithChildNodes(slot, content))
})
this.querySelectorAll<HTMLSlotElement>("slot").forEach((slot) => renderer.replaceElementWithChildNodes(slot, slot))
if (parsed.modifiers.flat) {
renderer.setAttribute(this, "*once.flat")
}
}
},
)
return { final: true }
},
} as const satisfies Directive<{
Cache: WeakMap<HTMLElement, Nullable<Record<PropertyKey, HTMLSlotElement>>>
Typings: typeof typings
}>
export const _slot = {
name: /^#(?<slot>)/,
prefix: "#",
phase: Phase.META,
} as const satisfies Directive<{ Name: RegExp }>
export default [_custom_element, _slot]
|