elit 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +4 -12
- package/dist/build.d.ts.map +1 -0
- package/dist/chokidar.d.ts +7 -9
- package/dist/chokidar.d.ts.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +17 -4
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/dom.d.ts +7 -14
- package/dist/dom.d.ts.map +1 -0
- package/dist/el.d.ts +19 -191
- package/dist/el.d.ts.map +1 -0
- package/dist/fs.d.ts +35 -35
- package/dist/fs.d.ts.map +1 -0
- package/dist/hmr.d.ts +3 -3
- package/dist/hmr.d.ts.map +1 -0
- package/dist/http.d.ts +20 -22
- package/dist/http.d.ts.map +1 -0
- package/dist/https.d.ts +12 -15
- package/dist/https.d.ts.map +1 -0
- package/dist/index.d.ts +10 -629
- package/dist/index.d.ts.map +1 -0
- package/dist/mime-types.d.ts +9 -9
- package/dist/mime-types.d.ts.map +1 -0
- package/dist/path.d.ts +22 -19
- package/dist/path.d.ts.map +1 -0
- package/dist/router.d.ts +10 -17
- package/dist/router.d.ts.map +1 -0
- package/dist/runtime.d.ts +5 -6
- package/dist/runtime.d.ts.map +1 -0
- package/dist/server.d.ts +105 -7
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +14 -2
- package/dist/server.mjs +14 -2
- package/dist/state.d.ts +21 -27
- package/dist/state.d.ts.map +1 -0
- package/dist/style.d.ts +14 -55
- package/dist/style.d.ts.map +1 -0
- package/dist/types.d.ts +26 -240
- package/dist/types.d.ts.map +1 -0
- package/dist/ws.d.ts +14 -17
- package/dist/ws.d.ts.map +1 -0
- package/dist/wss.d.ts +16 -16
- package/dist/wss.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/build.ts +337 -0
- package/src/chokidar.ts +401 -0
- package/src/cli.ts +638 -0
- package/src/config.ts +205 -0
- package/src/dom.ts +817 -0
- package/src/el.ts +164 -0
- package/src/fs.ts +727 -0
- package/src/hmr.ts +137 -0
- package/src/http.ts +775 -0
- package/src/https.ts +411 -0
- package/src/index.ts +14 -0
- package/src/mime-types.ts +222 -0
- package/src/path.ts +493 -0
- package/src/router.ts +237 -0
- package/src/runtime.ts +97 -0
- package/src/server.ts +1290 -0
- package/src/state.ts +468 -0
- package/src/style.ts +524 -0
- package/{dist/types-Du6kfwTm.d.ts → src/types.ts} +58 -141
- package/src/ws.ts +506 -0
- package/src/wss.ts +241 -0
- package/dist/build.d.mts +0 -20
- package/dist/chokidar.d.mts +0 -134
- package/dist/dom.d.mts +0 -87
- package/dist/el.d.mts +0 -207
- package/dist/fs.d.mts +0 -255
- package/dist/hmr.d.mts +0 -38
- package/dist/http.d.mts +0 -163
- package/dist/https.d.mts +0 -108
- package/dist/index.d.mts +0 -629
- package/dist/mime-types.d.mts +0 -48
- package/dist/path.d.mts +0 -163
- package/dist/router.d.mts +0 -47
- package/dist/runtime.d.mts +0 -97
- package/dist/server.d.mts +0 -7
- package/dist/state.d.mts +0 -111
- package/dist/style.d.mts +0 -159
- package/dist/types-C0nGi6MX.d.mts +0 -346
- package/dist/types.d.mts +0 -452
- package/dist/ws.d.mts +0 -195
- package/dist/wss.d.mts +0 -108
package/src/dom.ts
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elit - DomNode Core Class
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { VNode, Child, Children, Props, State, StateOptions, VirtualListController, JsonNode, VNodeJson } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper: Resolve element from string ID or HTMLElement (eliminates duplication in render methods)
|
|
9
|
+
*/
|
|
10
|
+
function resolveElement(rootElement: string | HTMLElement): HTMLElement | null {
|
|
11
|
+
return typeof rootElement === 'string'
|
|
12
|
+
? document.getElementById(rootElement.replace('#', ''))
|
|
13
|
+
: rootElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper: Ensure element exists or throw error (eliminates duplication in validation)
|
|
18
|
+
*/
|
|
19
|
+
function ensureElement(el: HTMLElement | null, rootElement: string | HTMLElement): HTMLElement {
|
|
20
|
+
if (!el) {
|
|
21
|
+
throw new Error(`Element not found: ${rootElement}`);
|
|
22
|
+
}
|
|
23
|
+
return el;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper: Check if child should be skipped (eliminates duplication in child rendering)
|
|
28
|
+
*/
|
|
29
|
+
function shouldSkipChild(child: any): boolean {
|
|
30
|
+
return child == null || child === false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Helper: Check if value is primitive JSON type (eliminates duplication in JSON conversion)
|
|
35
|
+
*/
|
|
36
|
+
function isPrimitiveJson(json: any): json is string | number | boolean | null | undefined {
|
|
37
|
+
return json == null || typeof json === 'boolean' || typeof json === 'string' || typeof json === 'number';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class DomNode {
|
|
41
|
+
private elementCache = new WeakMap<Element, boolean>();
|
|
42
|
+
|
|
43
|
+
createElement(tagName: string, props: Props = {}, children: Children = []): VNode {
|
|
44
|
+
return { tagName, props, children };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
renderToDOM(vNode: Child, parent: HTMLElement | SVGElement | DocumentFragment): void {
|
|
48
|
+
if (vNode == null || vNode === false) return;
|
|
49
|
+
|
|
50
|
+
if (typeof vNode !== 'object') {
|
|
51
|
+
parent.appendChild(document.createTextNode(String(vNode)));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { tagName, props, children } = vNode;
|
|
56
|
+
const isSVG = tagName === 'svg' || (tagName[0] === 's' && tagName[1] === 'v' && tagName[2] === 'g') ||
|
|
57
|
+
(parent as any).namespaceURI === 'http://www.w3.org/2000/svg';
|
|
58
|
+
|
|
59
|
+
const el = isSVG
|
|
60
|
+
? document.createElementNS('http://www.w3.org/2000/svg', tagName.replace('svg', '').toLowerCase() || tagName)
|
|
61
|
+
: document.createElement(tagName);
|
|
62
|
+
|
|
63
|
+
for (const key in props) {
|
|
64
|
+
const value = props[key];
|
|
65
|
+
if (value == null || value === false) continue;
|
|
66
|
+
|
|
67
|
+
const c = key.charCodeAt(0);
|
|
68
|
+
// class or className (c=99)
|
|
69
|
+
if (c === 99 && (key.length < 6 || key[5] === 'N')) {
|
|
70
|
+
const classValue = Array.isArray(value) ? value.join(' ') : value;
|
|
71
|
+
isSVG ? (el as SVGElement).setAttribute('class', classValue) : (el as HTMLElement).className = classValue;
|
|
72
|
+
}
|
|
73
|
+
// style (s=115)
|
|
74
|
+
else if (c === 115 && key.length === 5) {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
(el as HTMLElement).style.cssText = value;
|
|
77
|
+
} else {
|
|
78
|
+
const s = (el as HTMLElement).style;
|
|
79
|
+
for (const k in value) (s as any)[k] = value[k];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// on* events (o=111, n=110)
|
|
83
|
+
else if (c === 111 && key.charCodeAt(1) === 110) {
|
|
84
|
+
(el as any)[key.toLowerCase()] = value;
|
|
85
|
+
}
|
|
86
|
+
// dangerouslySetInnerHTML (d=100)
|
|
87
|
+
else if (c === 100 && key.length > 20) {
|
|
88
|
+
(el as HTMLElement).innerHTML = value.__html;
|
|
89
|
+
}
|
|
90
|
+
// ref (r=114)
|
|
91
|
+
else if (c === 114 && key.length === 3) {
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
typeof value === 'function' ? value(el as HTMLElement) : (value.current = el as HTMLElement);
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const len = children.length;
|
|
102
|
+
if (!len) {
|
|
103
|
+
parent.appendChild(el);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const renderChildren = (target: HTMLElement | SVGElement | DocumentFragment) => {
|
|
108
|
+
for (let i = 0; i < len; i++) {
|
|
109
|
+
const child = children[i];
|
|
110
|
+
if (shouldSkipChild(child)) continue;
|
|
111
|
+
|
|
112
|
+
if (Array.isArray(child)) {
|
|
113
|
+
for (let j = 0, cLen = child.length; j < cLen; j++) {
|
|
114
|
+
const c = child[j];
|
|
115
|
+
!shouldSkipChild(c) && this.renderToDOM(c, target);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
this.renderToDOM(child, target);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (len > 30) {
|
|
124
|
+
const fragment = document.createDocumentFragment();
|
|
125
|
+
renderChildren(fragment);
|
|
126
|
+
el.appendChild(fragment);
|
|
127
|
+
} else {
|
|
128
|
+
renderChildren(el);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
parent.appendChild(el);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render(rootElement: string | HTMLElement, vNode: VNode): HTMLElement {
|
|
135
|
+
const el = ensureElement(resolveElement(rootElement), rootElement);
|
|
136
|
+
|
|
137
|
+
// Clear existing content before rendering
|
|
138
|
+
el.innerHTML = '';
|
|
139
|
+
|
|
140
|
+
if (vNode.children && vNode.children.length > 500) {
|
|
141
|
+
const fragment = document.createDocumentFragment();
|
|
142
|
+
this.renderToDOM(vNode, fragment);
|
|
143
|
+
el.appendChild(fragment);
|
|
144
|
+
} else {
|
|
145
|
+
this.renderToDOM(vNode, el);
|
|
146
|
+
}
|
|
147
|
+
return el;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
batchRender(rootElement: string | HTMLElement, vNodes: VNode[]): HTMLElement {
|
|
151
|
+
const el = ensureElement(resolveElement(rootElement), rootElement);
|
|
152
|
+
|
|
153
|
+
const len = vNodes.length;
|
|
154
|
+
|
|
155
|
+
if (len > 3000) {
|
|
156
|
+
const fragment = document.createDocumentFragment();
|
|
157
|
+
let processed = 0;
|
|
158
|
+
const chunkSize = 1500;
|
|
159
|
+
|
|
160
|
+
const processChunk = (): void => {
|
|
161
|
+
const end = Math.min(processed + chunkSize, len);
|
|
162
|
+
for (let i = processed; i < end; i++) {
|
|
163
|
+
this.renderToDOM(vNodes[i], fragment);
|
|
164
|
+
}
|
|
165
|
+
processed = end;
|
|
166
|
+
|
|
167
|
+
if (processed >= len) {
|
|
168
|
+
el.appendChild(fragment);
|
|
169
|
+
} else {
|
|
170
|
+
requestAnimationFrame(processChunk);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
processChunk();
|
|
175
|
+
} else {
|
|
176
|
+
const fragment = document.createDocumentFragment();
|
|
177
|
+
for (let i = 0; i < len; i++) {
|
|
178
|
+
this.renderToDOM(vNodes[i], fragment);
|
|
179
|
+
}
|
|
180
|
+
el.appendChild(fragment);
|
|
181
|
+
}
|
|
182
|
+
return el;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
renderChunked(
|
|
186
|
+
rootElement: string | HTMLElement,
|
|
187
|
+
vNodes: VNode[],
|
|
188
|
+
chunkSize = 5000,
|
|
189
|
+
onProgress?: (current: number, total: number) => void
|
|
190
|
+
): HTMLElement {
|
|
191
|
+
const el = ensureElement(resolveElement(rootElement), rootElement);
|
|
192
|
+
|
|
193
|
+
const len = vNodes.length;
|
|
194
|
+
let index = 0;
|
|
195
|
+
|
|
196
|
+
const renderChunk = (): void => {
|
|
197
|
+
const end = Math.min(index + chunkSize, len);
|
|
198
|
+
const fragment = document.createDocumentFragment();
|
|
199
|
+
|
|
200
|
+
for (let i = index; i < end; i++) {
|
|
201
|
+
this.renderToDOM(vNodes[i], fragment);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
el.appendChild(fragment);
|
|
205
|
+
index = end;
|
|
206
|
+
|
|
207
|
+
if (onProgress) onProgress(index, len);
|
|
208
|
+
|
|
209
|
+
if (index < len) {
|
|
210
|
+
requestAnimationFrame(renderChunk);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
requestAnimationFrame(renderChunk);
|
|
215
|
+
return el;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
renderToHead(...vNodes: Array<VNode | VNode[]>): HTMLHeadElement | null {
|
|
219
|
+
const head = document.head;
|
|
220
|
+
if (head) {
|
|
221
|
+
for (const vNode of vNodes.flat()) {
|
|
222
|
+
vNode && this.renderToDOM(vNode, head);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return head;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
addStyle(cssText: string): HTMLStyleElement {
|
|
229
|
+
const el = document.createElement('style');
|
|
230
|
+
el.textContent = cssText;
|
|
231
|
+
return document.head.appendChild(el);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
addMeta(attrs: Record<string, string>): HTMLMetaElement {
|
|
235
|
+
const el = document.createElement('meta');
|
|
236
|
+
for (const k in attrs) el.setAttribute(k, attrs[k]);
|
|
237
|
+
return document.head.appendChild(el);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
addLink(attrs: Record<string, string>): HTMLLinkElement {
|
|
241
|
+
const el = document.createElement('link');
|
|
242
|
+
for (const k in attrs) el.setAttribute(k, attrs[k]);
|
|
243
|
+
return document.head.appendChild(el);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setTitle(text: string): string {
|
|
247
|
+
return document.title = text;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Reactive State Management
|
|
251
|
+
createState<T>(initialValue: T, options: StateOptions = {}): State<T> {
|
|
252
|
+
let value = initialValue;
|
|
253
|
+
const listeners = new Set<(value: T) => void>();
|
|
254
|
+
let updateTimer: NodeJS.Timeout | null = null;
|
|
255
|
+
const { throttle = 0, deep = false } = options;
|
|
256
|
+
|
|
257
|
+
const notify = () => listeners.forEach(fn => fn(value));
|
|
258
|
+
|
|
259
|
+
const scheduleUpdate = () => {
|
|
260
|
+
if (throttle > 0) {
|
|
261
|
+
if (!updateTimer) {
|
|
262
|
+
updateTimer = setTimeout(() => {
|
|
263
|
+
updateTimer = null;
|
|
264
|
+
notify();
|
|
265
|
+
}, throttle);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
notify();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
get value() { return value; },
|
|
274
|
+
set value(newValue: T) {
|
|
275
|
+
const changed = deep ? JSON.stringify(value) !== JSON.stringify(newValue) : value !== newValue;
|
|
276
|
+
if (changed) {
|
|
277
|
+
value = newValue;
|
|
278
|
+
scheduleUpdate();
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
subscribe(fn: (value: T) => void) {
|
|
282
|
+
listeners.add(fn);
|
|
283
|
+
return () => listeners.delete(fn);
|
|
284
|
+
},
|
|
285
|
+
destroy() {
|
|
286
|
+
listeners.clear();
|
|
287
|
+
updateTimer && clearTimeout(updateTimer);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
computed<T extends any[], R>(states: { [K in keyof T]: State<T[K]> }, computeFn: (...values: T) => R): State<R> {
|
|
293
|
+
const values = states.map(s => s.value) as unknown as T;
|
|
294
|
+
const result = this.createState(computeFn(...values));
|
|
295
|
+
|
|
296
|
+
states.forEach((state, index) => {
|
|
297
|
+
state.subscribe((newValue: any) => {
|
|
298
|
+
values[index] = newValue;
|
|
299
|
+
result.value = computeFn(...values);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
effect(stateFn: () => void): void {
|
|
307
|
+
stateFn();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Virtual scrolling helper for large lists
|
|
311
|
+
createVirtualList<T>(
|
|
312
|
+
container: HTMLElement,
|
|
313
|
+
items: T[],
|
|
314
|
+
renderItem: (item: T, index: number) => VNode,
|
|
315
|
+
itemHeight = 50,
|
|
316
|
+
bufferSize = 5
|
|
317
|
+
): VirtualListController {
|
|
318
|
+
const viewportHeight = container.clientHeight;
|
|
319
|
+
const totalHeight = items.length * itemHeight;
|
|
320
|
+
let scrollTop = 0;
|
|
321
|
+
|
|
322
|
+
const getVisibleRange = (): { start: number; end: number } => {
|
|
323
|
+
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
|
|
324
|
+
const end = Math.min(items.length, Math.ceil((scrollTop + viewportHeight) / itemHeight) + bufferSize);
|
|
325
|
+
return { start, end };
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const render = (): void => {
|
|
329
|
+
const { start, end } = getVisibleRange();
|
|
330
|
+
const wrapper = document.createElement('div');
|
|
331
|
+
wrapper.style.cssText = `height:${totalHeight}px;position:relative`;
|
|
332
|
+
|
|
333
|
+
for (let i = start; i < end; i++) {
|
|
334
|
+
const itemEl = document.createElement('div');
|
|
335
|
+
itemEl.style.cssText = `position:absolute;top:${i * itemHeight}px;height:${itemHeight}px;width:100%`;
|
|
336
|
+
this.renderToDOM(renderItem(items[i], i), itemEl);
|
|
337
|
+
wrapper.appendChild(itemEl);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
container.innerHTML = '';
|
|
341
|
+
container.appendChild(wrapper);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const scrollHandler = (): void => {
|
|
345
|
+
scrollTop = container.scrollTop;
|
|
346
|
+
requestAnimationFrame(render);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
container.addEventListener('scroll', scrollHandler);
|
|
350
|
+
|
|
351
|
+
render();
|
|
352
|
+
return {
|
|
353
|
+
render,
|
|
354
|
+
destroy: () => {
|
|
355
|
+
container.removeEventListener('scroll', scrollHandler);
|
|
356
|
+
container.innerHTML = '';
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Lazy load components
|
|
362
|
+
lazy<T extends any[], R>(loadFn: () => Promise<(...args: T) => R>): (...args: T) => Promise<R | VNode> {
|
|
363
|
+
let component: ((...args: T) => R) | null = null;
|
|
364
|
+
let loading = false;
|
|
365
|
+
|
|
366
|
+
return async (...args: T): Promise<R | VNode> => {
|
|
367
|
+
if (!component && !loading) {
|
|
368
|
+
loading = true;
|
|
369
|
+
component = await loadFn();
|
|
370
|
+
loading = false;
|
|
371
|
+
}
|
|
372
|
+
return component ? component(...args) : { tagName: 'div', props: { class: 'loading' }, children: ['Loading...'] };
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Memory management - cleanup unused elements
|
|
377
|
+
cleanupUnusedElements(root: HTMLElement): number {
|
|
378
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
379
|
+
const toRemove: Element[] = [];
|
|
380
|
+
|
|
381
|
+
while (walker.nextNode()) {
|
|
382
|
+
const node = walker.currentNode as Element;
|
|
383
|
+
if (node.id && node.id.startsWith('r') && !this.elementCache.has(node)) {
|
|
384
|
+
toRemove.push(node);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
toRemove.forEach(el => el.remove());
|
|
389
|
+
return toRemove.length;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Server-Side Rendering - convert VNode to HTML string
|
|
393
|
+
renderToString(vNode: Child, options: { pretty?: boolean; indent?: number } = {}): string {
|
|
394
|
+
const { pretty = false, indent = 0 } = options;
|
|
395
|
+
const indentStr = pretty ? ' '.repeat(indent) : '';
|
|
396
|
+
const newLine = pretty ? '\n' : '';
|
|
397
|
+
|
|
398
|
+
let resolvedVNode = this.resolveStateValue(vNode);
|
|
399
|
+
resolvedVNode = this.unwrapReactive(resolvedVNode);
|
|
400
|
+
|
|
401
|
+
if (Array.isArray(resolvedVNode)) {
|
|
402
|
+
return resolvedVNode.map(child => this.renderToString(child, options)).join('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (typeof resolvedVNode !== 'object' || resolvedVNode === null) {
|
|
406
|
+
if (resolvedVNode === null || resolvedVNode === undefined || resolvedVNode === false) {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
return this.escapeHtml(String(resolvedVNode));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const { tagName, props, children } = resolvedVNode;
|
|
413
|
+
const isSelfClosing = this.isSelfClosingTag(tagName);
|
|
414
|
+
|
|
415
|
+
let html = `${indentStr}<${tagName}`;
|
|
416
|
+
|
|
417
|
+
const attrs = this.propsToAttributes(props);
|
|
418
|
+
if (attrs) {
|
|
419
|
+
html += ` ${attrs}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (isSelfClosing) {
|
|
423
|
+
html += ` />${newLine}`;
|
|
424
|
+
return html;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
html += '>';
|
|
428
|
+
|
|
429
|
+
if (props.dangerouslySetInnerHTML) {
|
|
430
|
+
html += props.dangerouslySetInnerHTML.__html;
|
|
431
|
+
html += `</${tagName}>${newLine}`;
|
|
432
|
+
return html;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (children && children.length > 0) {
|
|
436
|
+
const resolvedChildren = children.map((c: Child) => {
|
|
437
|
+
const resolved = this.resolveStateValue(c);
|
|
438
|
+
return this.unwrapReactive(resolved);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const hasComplexChildren = resolvedChildren.some(
|
|
442
|
+
(c: any) => typeof c === 'object' && c !== null && !Array.isArray(c) && 'tagName' in c
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (pretty && hasComplexChildren) {
|
|
446
|
+
html += newLine;
|
|
447
|
+
for (const child of resolvedChildren) {
|
|
448
|
+
if (shouldSkipChild(child)) continue;
|
|
449
|
+
|
|
450
|
+
if (Array.isArray(child)) {
|
|
451
|
+
for (const c of child) {
|
|
452
|
+
if (!shouldSkipChild(c)) {
|
|
453
|
+
html += this.renderToString(c, { pretty, indent: indent + 1 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
html += this.renderToString(child, { pretty, indent: indent + 1 });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
html += indentStr;
|
|
461
|
+
} else {
|
|
462
|
+
for (const child of resolvedChildren) {
|
|
463
|
+
if (shouldSkipChild(child)) continue;
|
|
464
|
+
|
|
465
|
+
if (Array.isArray(child)) {
|
|
466
|
+
for (const c of child) {
|
|
467
|
+
if (!shouldSkipChild(c)) {
|
|
468
|
+
html += this.renderToString(c, { pretty: false, indent: 0 });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
html += this.renderToString(child, { pretty: false, indent: 0 });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
html += `</${tagName}>${newLine}`;
|
|
479
|
+
return html;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private resolveStateValue(value: any): any {
|
|
483
|
+
if (value && typeof value === 'object' && 'value' in value && 'subscribe' in value) {
|
|
484
|
+
return value.value;
|
|
485
|
+
}
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private isReactiveWrapper(vNode: any): boolean {
|
|
490
|
+
if (!vNode || typeof vNode !== 'object' || !vNode.tagName) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
return vNode.tagName === 'span' &&
|
|
494
|
+
vNode.props?.id &&
|
|
495
|
+
typeof vNode.props.id === 'string' &&
|
|
496
|
+
vNode.props.id.match(/^r[a-z0-9]{9}$/);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private unwrapReactive(vNode: any): Child {
|
|
500
|
+
if (!this.isReactiveWrapper(vNode)) {
|
|
501
|
+
return vNode;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const children = vNode.children;
|
|
505
|
+
if (!children || children.length === 0) {
|
|
506
|
+
return '';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (children.length === 1) {
|
|
510
|
+
const child = children[0];
|
|
511
|
+
|
|
512
|
+
if (child && typeof child === 'object' && child.tagName === 'span') {
|
|
513
|
+
const props = child.props;
|
|
514
|
+
const hasNoProps = !props || Object.keys(props).length === 0;
|
|
515
|
+
const hasSingleStringChild = child.children &&
|
|
516
|
+
child.children.length === 1 &&
|
|
517
|
+
typeof child.children[0] === 'string';
|
|
518
|
+
|
|
519
|
+
if (hasNoProps && hasSingleStringChild) {
|
|
520
|
+
return child.children[0];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return this.unwrapReactive(child);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return children.map((c: Child) => this.unwrapReactive(c));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private escapeHtml(text: string): string {
|
|
531
|
+
const htmlEscapes: Record<string, string> = {
|
|
532
|
+
'&': '&',
|
|
533
|
+
'<': '<',
|
|
534
|
+
'>': '>',
|
|
535
|
+
'"': '"',
|
|
536
|
+
"'": '''
|
|
537
|
+
};
|
|
538
|
+
return text.replace(/[&<>"']/g, char => htmlEscapes[char]);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private isSelfClosingTag(tagName: string): boolean {
|
|
542
|
+
const selfClosingTags = new Set([
|
|
543
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
544
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
545
|
+
]);
|
|
546
|
+
return selfClosingTags.has(tagName.toLowerCase());
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private propsToAttributes(props: Props): string {
|
|
550
|
+
const attrs: string[] = [];
|
|
551
|
+
|
|
552
|
+
for (const key in props) {
|
|
553
|
+
if (key === 'children' || key === 'dangerouslySetInnerHTML' || key === 'ref') {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
let value = props[key];
|
|
558
|
+
value = this.resolveStateValue(value);
|
|
559
|
+
|
|
560
|
+
if (value == null || value === false) continue;
|
|
561
|
+
|
|
562
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (key === 'className' || key === 'class') {
|
|
567
|
+
const className = Array.isArray(value) ? value.join(' ') : value;
|
|
568
|
+
if (className) {
|
|
569
|
+
attrs.push(`class="${this.escapeHtml(String(className))}"`);
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (key === 'style') {
|
|
575
|
+
const styleStr = this.styleToString(value);
|
|
576
|
+
if (styleStr) {
|
|
577
|
+
attrs.push(`style="${this.escapeHtml(styleStr)}"`);
|
|
578
|
+
}
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (value === true) {
|
|
583
|
+
attrs.push(key);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
attrs.push(`${key}="${this.escapeHtml(String(value))}"`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return attrs.join(' ');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private styleToString(style: any): string {
|
|
594
|
+
if (typeof style === 'string') {
|
|
595
|
+
return style;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (typeof style === 'object' && style !== null) {
|
|
599
|
+
const styles: string[] = [];
|
|
600
|
+
for (const key in style) {
|
|
601
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
602
|
+
styles.push(`${cssKey}:${style[key]}`);
|
|
603
|
+
}
|
|
604
|
+
return styles.join(';');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return '';
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private isState(value: any): value is State<any> {
|
|
611
|
+
return value && typeof value === 'object' && 'value' in value && 'subscribe' in value && typeof value.subscribe === 'function';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private reactiveNodes = new Map<State<any>, { node: Text | null, renderFn: (v: any) => Child }>();
|
|
615
|
+
|
|
616
|
+
private createReactiveChild(state: State<any>, renderFn: (value: any) => Child): Child {
|
|
617
|
+
const currentValue = renderFn(state.value);
|
|
618
|
+
|
|
619
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
620
|
+
const entry = { node: null as Text | null, renderFn };
|
|
621
|
+
this.reactiveNodes.set(state, entry);
|
|
622
|
+
|
|
623
|
+
state.subscribe(() => {
|
|
624
|
+
if (entry.node && entry.node.parentNode) {
|
|
625
|
+
const newValue = renderFn(state.value);
|
|
626
|
+
entry.node.textContent = String(newValue ?? '');
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return currentValue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
jsonToVNode(json: JsonNode | string | number | boolean | null | undefined | State<any>): Child {
|
|
635
|
+
if (this.isState(json)) {
|
|
636
|
+
return this.createReactiveChild(json, (v: any) => v);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (isPrimitiveJson(json)) {
|
|
640
|
+
return json as Child;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const { tag, attributes = {}, children } = json;
|
|
644
|
+
|
|
645
|
+
const props: Props = {};
|
|
646
|
+
for (const key in attributes) {
|
|
647
|
+
const value = attributes[key];
|
|
648
|
+
if (key === 'class') {
|
|
649
|
+
props.className = this.isState(value) ? value.value : value;
|
|
650
|
+
} else {
|
|
651
|
+
props[key] = this.isState(value) ? value.value : value;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const childrenArray: Children = [];
|
|
656
|
+
if (children != null) {
|
|
657
|
+
if (Array.isArray(children)) {
|
|
658
|
+
for (const child of children) {
|
|
659
|
+
if (this.isState(child)) {
|
|
660
|
+
childrenArray.push(this.createReactiveChild(child, (v: any) => v));
|
|
661
|
+
} else {
|
|
662
|
+
const converted = this.jsonToVNode(child);
|
|
663
|
+
if (converted != null && converted !== false) {
|
|
664
|
+
childrenArray.push(converted);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} else if (this.isState(children)) {
|
|
669
|
+
childrenArray.push(this.createReactiveChild(children, (v: any) => v));
|
|
670
|
+
} else if (typeof children === 'object' && 'tag' in children) {
|
|
671
|
+
const converted = this.jsonToVNode(children);
|
|
672
|
+
if (converted != null && converted !== false) {
|
|
673
|
+
childrenArray.push(converted);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
childrenArray.push(children as Child);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { tagName: tag, props, children: childrenArray };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
vNodeJsonToVNode(json: VNodeJson | State<any>): Child {
|
|
684
|
+
if (this.isState(json)) {
|
|
685
|
+
return this.createReactiveChild(json, (v: any) => v);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (isPrimitiveJson(json)) {
|
|
689
|
+
return json as Child;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { tagName, props = {}, children = [] } = json;
|
|
693
|
+
|
|
694
|
+
const resolvedProps: Props = {};
|
|
695
|
+
for (const key in props) {
|
|
696
|
+
const value = props[key];
|
|
697
|
+
resolvedProps[key] = this.isState(value) ? value.value : value;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const childrenArray: Children = [];
|
|
701
|
+
for (const child of children) {
|
|
702
|
+
if (this.isState(child)) {
|
|
703
|
+
childrenArray.push(this.createReactiveChild(child, (v: any) => v));
|
|
704
|
+
} else {
|
|
705
|
+
const converted = this.vNodeJsonToVNode(child);
|
|
706
|
+
if (converted != null && converted !== false) {
|
|
707
|
+
childrenArray.push(converted);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { tagName, props: resolvedProps, children: childrenArray };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
renderJson(rootElement: string | HTMLElement, json: JsonNode): HTMLElement {
|
|
716
|
+
const vNode = this.jsonToVNode(json);
|
|
717
|
+
if (!vNode || typeof vNode !== 'object' || !('tagName' in vNode)) {
|
|
718
|
+
throw new Error('Invalid JSON structure');
|
|
719
|
+
}
|
|
720
|
+
return this.render(rootElement, vNode as VNode);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
renderVNode(rootElement: string | HTMLElement, json: VNodeJson): HTMLElement {
|
|
724
|
+
const vNode = this.vNodeJsonToVNode(json);
|
|
725
|
+
if (!vNode || typeof vNode !== 'object' || !('tagName' in vNode)) {
|
|
726
|
+
throw new Error('Invalid VNode JSON structure');
|
|
727
|
+
}
|
|
728
|
+
return this.render(rootElement, vNode as VNode);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
renderJsonToString(json: JsonNode, options: { pretty?: boolean; indent?: number } = {}): string {
|
|
732
|
+
const vNode = this.jsonToVNode(json);
|
|
733
|
+
return this.renderToString(vNode, options);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
renderVNodeToString(json: VNodeJson, options: { pretty?: boolean; indent?: number } = {}): string {
|
|
737
|
+
const vNode = this.vNodeJsonToVNode(json);
|
|
738
|
+
return this.renderToString(vNode, options);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
// Generate complete HTML document as string (for SSR)
|
|
743
|
+
renderToHTMLDocument(vNode: Child, options: {
|
|
744
|
+
title?: string;
|
|
745
|
+
meta?: Array<Record<string, string>>;
|
|
746
|
+
links?: Array<Record<string, string>>;
|
|
747
|
+
scripts?: Array<{ src?: string; content?: string; async?: boolean; defer?: boolean; type?: string }>;
|
|
748
|
+
styles?: Array<{ href?: string; content?: string }>;
|
|
749
|
+
lang?: string;
|
|
750
|
+
head?: string;
|
|
751
|
+
bodyAttrs?: Record<string, string>;
|
|
752
|
+
pretty?: boolean;
|
|
753
|
+
} = {}): string {
|
|
754
|
+
const { title = '', meta = [], links = [], scripts = [], styles = [], lang = 'en', head = '', bodyAttrs = {}, pretty = false } = options;
|
|
755
|
+
const nl = pretty ? '\n' : '';
|
|
756
|
+
const indent = pretty ? ' ' : '';
|
|
757
|
+
const indent2 = pretty ? ' ' : '';
|
|
758
|
+
|
|
759
|
+
let html = `<!DOCTYPE html>${nl}<html lang="${lang}">${nl}${indent}<head>${nl}${indent2}<meta charset="UTF-8">${nl}${indent2}<meta name="viewport" content="width=device-width, initial-scale=1.0">${nl}`;
|
|
760
|
+
if (title) html += `${indent2}<title>${this.escapeHtml(title)}</title>${nl}`;
|
|
761
|
+
|
|
762
|
+
for (const m of meta) {
|
|
763
|
+
html += `${indent2}<meta`;
|
|
764
|
+
for (const k in m) html += ` ${k}="${this.escapeHtml(m[k])}"`;
|
|
765
|
+
html += `>${nl}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const l of links) {
|
|
769
|
+
html += `${indent2}<link`;
|
|
770
|
+
for (const k in l) html += ` ${k}="${this.escapeHtml(l[k])}"`;
|
|
771
|
+
html += `>${nl}`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
for (const s of styles) {
|
|
775
|
+
if (s.href) {
|
|
776
|
+
html += `${indent2}<link rel="stylesheet" href="${this.escapeHtml(s.href)}">${nl}`;
|
|
777
|
+
} else if (s.content) {
|
|
778
|
+
html += `${indent2}<style>${s.content}</style>${nl}`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (head) html += head + nl;
|
|
783
|
+
html += `${indent}</head>${nl}${indent}<body`;
|
|
784
|
+
for (const k in bodyAttrs) html += ` ${k}="${this.escapeHtml(bodyAttrs[k])}"`;
|
|
785
|
+
html += `>${nl}`;
|
|
786
|
+
html += this.renderToString(vNode, { pretty, indent: 2 });
|
|
787
|
+
|
|
788
|
+
for (const script of scripts) {
|
|
789
|
+
html += `${indent2}<script`;
|
|
790
|
+
if (script.type) html += ` type="${this.escapeHtml(script.type)}"`;
|
|
791
|
+
if (script.async) html += ` async`;
|
|
792
|
+
if (script.defer) html += ` defer`;
|
|
793
|
+
if (script.src) {
|
|
794
|
+
html += ` src="${this.escapeHtml(script.src)}"></script>${nl}`;
|
|
795
|
+
} else if (script.content) {
|
|
796
|
+
html += `>${script.content}</script>${nl}`;
|
|
797
|
+
} else {
|
|
798
|
+
html += `></script>${nl}`;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
html += `${indent}</body>${nl}</html>`;
|
|
803
|
+
return html;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Expose elementCache for reactive updates
|
|
807
|
+
getElementCache(): WeakMap<Element, boolean> {
|
|
808
|
+
return this.elementCache;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export const dom = new DomNode();
|
|
813
|
+
|
|
814
|
+
// Export helper functions for convenience
|
|
815
|
+
export const render = dom.render.bind(dom);
|
|
816
|
+
export const renderToString = dom.renderToString.bind(dom);
|
|
817
|
+
export const mount = render; // alias for render
|