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 |
x8
x8
x8
x8
x24
x8
x8
x8
x8
x8
x8
x8
x38
x8
x8
x120
x131
x131
x131
x131
x131
x131
x131
x131
x131
x131
x8
x8
x25
x26
x26
x25
x26
x78
x26
x25
x25
x26
x78
x26
x25
x78
x26
x25
x27
x81
x27
x36
x108
x36
x36
x36
x36
x48
x49
x49
x177
x59
x59
x48
x63
x63
x71
x71
x63
x71
x71
x63
x63
x63
x63
x63
x59
x71
x59
x59
x48
x49
x49
x48
x36
x108
x25
x8
x8
x8
x8
x8
x8
x32 |
|
import { type Cache, 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<Cache<typeof _custom_element>>(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(name),
get: (_, name: string) => element.getAttribute(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.replaceElementWithChildNodes(this, this)
}
}
},
)
return { final: true }
},
} as Directive<WeakMap<HTMLElement, Nullable<Record<PropertyKey, HTMLSlotElement>>>, typeof typings> & { name: string }
export const _slot = {
name: /^#(?<slot>)/,
prefix: "#",
phase: Phase.META,
} as Directive & { prefix: string }
export default [_custom_element, _slot]
|