-
Notifications
You must be signed in to change notification settings - Fork 0
/
runtime.ts
228 lines (190 loc) · 6.03 KB
/
runtime.ts
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import { TypeOf } from '@sgrud/core';
import { elementClose, elementOpen, getKey, patch, text } from 'incremental-dom';
declare global {
/**
* Global string literal helper type. Enforces any assigned string to be a
* `keyof HTMLElementTagNameMap`, while excluding all custom element tag
* names, i.e., `${string}-${string}` keys of `HTMLElementTagNameMap`.
*/
type HTMLElementTagName =
Exclude<keyof HTMLElementTagNameMap, `${string}-${string}`>;
/**
* Global string literal helper type. Enforces any assigned string to be a
* `keyof HTMLElementTagNameMap`, while excluding built-in tag names, i.e.,
* `Extract`ing all `${string}-${string}` keys of `HTMLElementTagNameMap`.
*/
type CustomElementTagName =
Extract<keyof HTMLElementTagNameMap, `${string}-${string}`>;
/**
* Intrinsic JSX namespace.
*
* @see https://www.typescriptlang.org/docs/handbook/jsx.html
*/
namespace JSX {
/**
* Intrinsic JSX element type helper representing an array of bound
* [incremental-dom](https://google.github.io/incremental-dom) calls.
*/
type Element = (() => Node)[];
/**
* Intrinsic list of known JSX elements, comprised of the global
* `HTMLElementTagNameMap`.
*/
type IntrinsicElements = {
[K in keyof HTMLElementTagNameMap]: Partial<HTMLElementTagNameMap[K]> & {
/**
* Intrinsic built-in element extension attribute.
*/
readonly is?: K extends HTMLElementTagName
? CustomElementTagName
: never;
/**
* Intrinsic element reference key attribute.
*/
readonly key?: Key;
};
};
/**
* Element reference key type helper. Enforces any assigned value to adhere
* to the [incremental-dom](https://google.github.io/incremental-dom) `Key`
* type.
*/
type Key = string | number;
}
interface Node {
/**
* @see https://github.com/google/incremental-dom/pull/467
*/
readonly namespaceURI: string | null;
}
}
/**
* JSX element factory. Provides `jsx-runtime`-compliant bindings to the
* [incremental-dom](https://google.github.io/incremental-dom) library. This
* factory function is meant to be implicitly imported by the transpiler and
* returns an array of bound `incremental-dom` function calls, representing the
* created JSX element. This array of bound functions can be rendered into an
* element attached to the DOM through the {@link render} function.
*
* @param type - Element type.
* @param props - Element properties.
* @param ref - Element reference.
* @returns Array of bound calls.
*/
export function createElement(
type: Function | keyof JSX.IntrinsicElements,
props?: Record<string, any>,
ref?: JSX.Key
): JSX.Element {
if (TypeOf.function(type)) {
return type(props);
}
const attributes = [];
const children = [];
const element = [];
for (const key in props) {
switch (key) {
case 'children':
children.push(...[props[key]].flat(Infinity));
break;
case 'className':
attributes.push('class', props[key]);
break;
case 'is':
type = customElements.get(props[key]) || type;
break;
case 'key':
ref ??= props[key];
break;
default:
attributes.push(key, props[key]);
break;
}
}
element.push(elementOpen.bind(null, type, ref, null, ...attributes));
for (const child of children) {
if (TypeOf.string(child) || TypeOf.number(child)) {
element.push(text.bind(null, child));
} else if (TypeOf.function(child)) {
element.push(child);
}
}
element.push(elementClose.bind(null, type as keyof JSX.IntrinsicElements));
return element;
}
/**
* JSX fragment factory. Provides a `jsx-runtime`-compliant helper function used
* by the transpiler to create JSX fragments.
*
* @param props - Fragment properties.
* @returns Array of bound calls.
*/
export function createFragment(props?: Record<string, any>): JSX.Element {
const fragment = [];
if (props?.children?.length) {
const children = props.children.flat(Infinity);
for (const child of children) {
if (TypeOf.string(child) || TypeOf.number(child)) {
fragment.push(text.bind(null, child));
} else if (TypeOf.function(child)) {
fragment.push(child);
}
}
}
return fragment;
}
/**
* JSX reference helper. Calling this function while supplying a viable `target`
* will return all referenced JSX elements mapped by their corresponding keys
* known to the supplied `target`. A viable `target` may be any element, which
* previously was `target` to the {@link render} function.
*
* @param target - DOM element to resolve.
* @returns Resolved references.
*/
export function references(
target: Element | DocumentFragment
): Map<JSX.Key, Node> | undefined {
return resolved.get(target);
}
/**
* JSX rendering helper. This function is a wrapper around the `patch` function
* from the [incremental-dom](https://google.github.io/incremental-dom) library
* and renders a JSX element created through {@link createElement} into an
* element attached to the DOM.
*
* @param target - DOM element to render into.
* @param element - JSX element to be rendered.
* @returns Rendered `target` element.
*/
export function render(
target: Element | DocumentFragment,
element: JSX.Element
): Node {
return patch(target, () => {
const refs = new Map<JSX.Key, Node>();
for (const incrementalDom of element) {
const node = incrementalDom();
const ref = getKey(node);
if (TypeOf.number(ref) || TypeOf.string(ref)) {
refs.set(ref, node);
}
}
if (refs.size) {
resolved.set(target, refs);
}
});
}
/**
* Internal weak mapping of all rendered nodes containing element references to
* those references, mapped by their respective keys.
*/
const resolved = new WeakMap<Element | DocumentFragment, Map<JSX.Key, Node>>();
export {
CustomElementTagName,
HTMLElementTagName,
JSX,
createElement as jsx,
createElement as jsxs,
createFragment as Fragment
};