elementdrawing 1.0.0
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/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render - DOM Rendering Engine
|
|
3
|
+
* ElementDrawing Framework - Converts virtual DOM to real DOM nodes with
|
|
4
|
+
* attribute setting, style application, event delegation, ref assignment,
|
|
5
|
+
* portal rendering, suspense, error boundaries, hydration, SVG/MathML
|
|
6
|
+
* namespace support, and concurrent rendering hints.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, REACT_SUSPENSE_TYPE,
|
|
13
|
+
REACT_LAZY_TYPE, REACT_STRICT_MODE_TYPE, REACT_ERROR_BOUNDARY_TYPE,
|
|
14
|
+
isValidElement,
|
|
15
|
+
} = require('./createElement');
|
|
16
|
+
|
|
17
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const TEXT_NODE_TYPE = '#text';
|
|
20
|
+
const COMMENT_NODE_TYPE = '#comment';
|
|
21
|
+
|
|
22
|
+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
23
|
+
const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
|
24
|
+
|
|
25
|
+
const VOID_ELEMENTS = new Set([
|
|
26
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
27
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const SVG_TAGS = new Set([
|
|
31
|
+
'svg', 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath',
|
|
32
|
+
'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer',
|
|
33
|
+
'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap',
|
|
34
|
+
'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG',
|
|
35
|
+
'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology',
|
|
36
|
+
'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile',
|
|
37
|
+
'feTurbulence', 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient',
|
|
38
|
+
'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline',
|
|
39
|
+
'radialGradient', 'rect', 'set', 'stop', 'switch', 'symbol', 'text', 'textPath',
|
|
40
|
+
'tspan', 'use', 'view',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
44
|
+
'allowFullScreen', 'async', 'autofocus', 'autoPlay', 'checked', 'controls',
|
|
45
|
+
'default', 'disabled', 'enableRemotePlayback', 'formNoValidate', 'hidden',
|
|
46
|
+
'loop', 'multiple', 'muted', 'noModule', 'noValidate', 'open', 'playsInline',
|
|
47
|
+
'readOnly', 'required', 'reversed', 'scoped', 'seamless', 'selected',
|
|
48
|
+
'itemScope',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const PROPERTY_ATTRIBUTES = new Set([
|
|
52
|
+
'value', 'checked', 'selected', 'innerHTML', 'textContent',
|
|
53
|
+
'defaultValue', 'defaultChecked',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Event delegation map
|
|
57
|
+
const eventDelegationMap = new Map();
|
|
58
|
+
let rootContainer = null;
|
|
59
|
+
|
|
60
|
+
// ─── DOM Creation ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a real DOM element from a virtual element type.
|
|
64
|
+
* @param {string} type
|
|
65
|
+
* @param {Object} props
|
|
66
|
+
* @param {string|null} namespace
|
|
67
|
+
* @returns {HTMLElement|SVGElement}
|
|
68
|
+
*/
|
|
69
|
+
function createDOMElement(type, props, namespace) {
|
|
70
|
+
let domNode;
|
|
71
|
+
|
|
72
|
+
if (namespace === SVG_NAMESPACE || SVG_TAGS.has(type)) {
|
|
73
|
+
domNode = document.createElementNS(SVG_NAMESPACE, type);
|
|
74
|
+
} else if (namespace === MATH_NAMESPACE) {
|
|
75
|
+
domNode = document.createElementNS(MATH_NAMESPACE, type);
|
|
76
|
+
} else {
|
|
77
|
+
domNode = document.createElement(type);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return domNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a text node.
|
|
85
|
+
* @param {string} text
|
|
86
|
+
* @returns {Text}
|
|
87
|
+
*/
|
|
88
|
+
function createTextNode(text) {
|
|
89
|
+
return document.createTextNode(text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a comment node.
|
|
94
|
+
* @param {string} text
|
|
95
|
+
* @returns {Comment}
|
|
96
|
+
*/
|
|
97
|
+
function createCommentNode(text) {
|
|
98
|
+
return document.createComment(text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a document fragment.
|
|
103
|
+
* @returns {DocumentFragment}
|
|
104
|
+
*/
|
|
105
|
+
function createDocumentFragment() {
|
|
106
|
+
return document.createDocumentFragment();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Attribute Setting ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set an attribute on a DOM element with proper handling for different types.
|
|
113
|
+
* @param {HTMLElement} domNode
|
|
114
|
+
* @param {string} name
|
|
115
|
+
* @param {*} value
|
|
116
|
+
*/
|
|
117
|
+
function setAttribute(domNode, name, value) {
|
|
118
|
+
if (value === null || value === undefined || value === false) {
|
|
119
|
+
removeAttribute(domNode, name);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Boolean attributes
|
|
124
|
+
if (BOOLEAN_ATTRIBUTES.has(name)) {
|
|
125
|
+
if (value === true) {
|
|
126
|
+
domNode.setAttribute(name.toLowerCase(), '');
|
|
127
|
+
domNode[name] = true;
|
|
128
|
+
} else {
|
|
129
|
+
removeAttribute(domNode, name);
|
|
130
|
+
domNode[name] = false;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Property attributes
|
|
136
|
+
if (PROPERTY_ATTRIBUTES.has(name)) {
|
|
137
|
+
domNode[name] = value;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// className -> class
|
|
142
|
+
if (name === 'className') {
|
|
143
|
+
domNode.className = value;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// htmlFor -> for
|
|
148
|
+
if (name === 'htmlFor') {
|
|
149
|
+
domNode.setAttribute('for', value);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// tabIndex
|
|
154
|
+
if (name === 'tabIndex') {
|
|
155
|
+
domNode.tabIndex = value;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Style object
|
|
160
|
+
if (name === 'style') {
|
|
161
|
+
applyStyle(domNode, value);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Event handlers
|
|
166
|
+
if (name.startsWith('on')) {
|
|
167
|
+
setEventHandler(domNode, name, value);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Data attributes
|
|
172
|
+
if (name.startsWith('data-')) {
|
|
173
|
+
domNode.setAttribute(name, value);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ARIA attributes
|
|
178
|
+
if (name.startsWith('aria-')) {
|
|
179
|
+
domNode.setAttribute(name, value);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Custom element attributes
|
|
184
|
+
if (name.includes('-') && !(domNode instanceof SVGElement)) {
|
|
185
|
+
domNode.setAttribute(name, value);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Standard attributes
|
|
190
|
+
if (name in domNode) {
|
|
191
|
+
try {
|
|
192
|
+
domNode[name] = value;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
domNode.setAttribute(name, value);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
domNode.setAttribute(name, value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Remove an attribute from a DOM element.
|
|
203
|
+
* @param {HTMLElement} domNode
|
|
204
|
+
* @param {string} name
|
|
205
|
+
*/
|
|
206
|
+
function removeAttribute(domNode, name) {
|
|
207
|
+
if (name === 'className') {
|
|
208
|
+
domNode.className = '';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (name === 'style') {
|
|
213
|
+
domNode.removeAttribute('style');
|
|
214
|
+
domNode.style.cssText = '';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (name === 'htmlFor') {
|
|
219
|
+
domNode.removeAttribute('for');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (BOOLEAN_ATTRIBUTES.has(name)) {
|
|
224
|
+
domNode.removeAttribute(name.toLowerCase());
|
|
225
|
+
domNode[name] = false;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (PROPERTY_ATTRIBUTES.has(name)) {
|
|
230
|
+
if (name === 'value') domNode.value = '';
|
|
231
|
+
else if (name === 'checked') domNode.checked = false;
|
|
232
|
+
else if (name === 'selected') domNode.selected = false;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (name.startsWith('on')) {
|
|
237
|
+
removeEventHandler(domNode, name);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
domNode.removeAttribute(name);
|
|
243
|
+
if (name in domNode) domNode[name] = '';
|
|
244
|
+
} catch (e) {
|
|
245
|
+
// Some attributes can't be removed
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set a property directly on a DOM element.
|
|
251
|
+
*/
|
|
252
|
+
function setProperty(domNode, name, value) {
|
|
253
|
+
try {
|
|
254
|
+
domNode[name] = value;
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.warn('[render] Failed to set property "' + name + '":', e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Style Application ────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Apply inline styles to a DOM element.
|
|
264
|
+
* @param {HTMLElement} domNode
|
|
265
|
+
* @param {Object|string} style
|
|
266
|
+
*/
|
|
267
|
+
function applyStyle(domNode, style) {
|
|
268
|
+
if (!style) {
|
|
269
|
+
domNode.removeAttribute('style');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (typeof style === 'string') {
|
|
274
|
+
domNode.style.cssText = style;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (typeof style === 'object') {
|
|
279
|
+
for (const prop in style) {
|
|
280
|
+
if (style.hasOwnProperty(prop)) {
|
|
281
|
+
const cssProp = prop.includes('-') ? prop : camelToKebab(prop);
|
|
282
|
+
domNode.style.setProperty(cssProp, style[prop]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Convert camelCase to kebab-case.
|
|
290
|
+
*/
|
|
291
|
+
function camelToKebab(str) {
|
|
292
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Apply CSS classes to a DOM element.
|
|
297
|
+
*/
|
|
298
|
+
function applyClassName(domNode, className) {
|
|
299
|
+
if (!className) {
|
|
300
|
+
domNode.removeAttribute('class');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
domNode.className = className;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Event Handling ───────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Set an event handler on a DOM element.
|
|
310
|
+
*/
|
|
311
|
+
function setEventHandler(domNode, name, handler) {
|
|
312
|
+
const eventType = name.slice(2).toLowerCase();
|
|
313
|
+
const normalizedType = normalizeEventType(eventType);
|
|
314
|
+
|
|
315
|
+
const handlerKey = '__ed_handler_' + normalizedType;
|
|
316
|
+
const captureHandlerKey = '__ed_capture_' + normalizedType;
|
|
317
|
+
|
|
318
|
+
const isCapture = name.endsWith('Capture');
|
|
319
|
+
const key = isCapture ? captureHandlerKey : handlerKey;
|
|
320
|
+
|
|
321
|
+
// Remove old handler if exists
|
|
322
|
+
if (domNode[key]) {
|
|
323
|
+
domNode.removeEventListener(normalizedType, domNode[key], isCapture);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Set new handler
|
|
327
|
+
if (typeof handler === 'function') {
|
|
328
|
+
domNode[key] = handler;
|
|
329
|
+
domNode.addEventListener(normalizedType, handler, isCapture);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Remove an event handler from a DOM element.
|
|
335
|
+
*/
|
|
336
|
+
function removeEventHandler(domNode, name) {
|
|
337
|
+
const eventType = name.slice(2).toLowerCase();
|
|
338
|
+
const normalizedType = normalizeEventType(eventType);
|
|
339
|
+
const isCapture = name.endsWith('Capture');
|
|
340
|
+
const key = isCapture ? '__ed_capture_' + normalizedType : '__ed_handler_' + normalizedType;
|
|
341
|
+
|
|
342
|
+
if (domNode[key]) {
|
|
343
|
+
domNode.removeEventListener(normalizedType, domNode[key], isCapture);
|
|
344
|
+
domNode[key] = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Normalize event type names.
|
|
350
|
+
*/
|
|
351
|
+
function normalizeEventType(eventType) {
|
|
352
|
+
const map = {
|
|
353
|
+
'doubleclick': 'dblclick',
|
|
354
|
+
'change': 'change',
|
|
355
|
+
'input': 'input',
|
|
356
|
+
'submit': 'submit',
|
|
357
|
+
'focusin': 'focusin',
|
|
358
|
+
'focusout': 'focusout',
|
|
359
|
+
};
|
|
360
|
+
return map[eventType] || eventType;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── Event Delegation ─────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Set up event delegation on the root container.
|
|
367
|
+
* Captures events at the root and dispatches to the correct handler.
|
|
368
|
+
* @param {HTMLElement} container
|
|
369
|
+
*/
|
|
370
|
+
function setupEventDelegation(container) {
|
|
371
|
+
const delegatedEvents = ['click', 'input', 'change', 'submit', 'keydown', 'keyup', 'focus', 'blur'];
|
|
372
|
+
|
|
373
|
+
delegatedEvents.forEach((eventType) => {
|
|
374
|
+
container.addEventListener(eventType, function delegatedHandler(event) {
|
|
375
|
+
let target = event.target;
|
|
376
|
+
|
|
377
|
+
// Walk up the DOM tree to find the handler
|
|
378
|
+
while (target && target !== container) {
|
|
379
|
+
const handlerKey = '__ed_handler_' + eventType;
|
|
380
|
+
if (target[handlerKey]) {
|
|
381
|
+
try {
|
|
382
|
+
target[handlerKey](event);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('[render] Delegated event handler error:', error);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
target = target.parentElement;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Ref Assignment ───────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Assign a DOM node to a ref.
|
|
398
|
+
*/
|
|
399
|
+
function assignRef(ref, domNode) {
|
|
400
|
+
if (!ref) return;
|
|
401
|
+
|
|
402
|
+
if (typeof ref === 'function') {
|
|
403
|
+
try {
|
|
404
|
+
ref(domNode);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error('[render] Callback ref error:', error);
|
|
407
|
+
}
|
|
408
|
+
} else if (typeof ref === 'object' && ref._isRef) {
|
|
409
|
+
ref.current = domNode;
|
|
410
|
+
} else if (typeof ref === 'object' && ref !== null) {
|
|
411
|
+
ref.current = domNode;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Clean up a ref when an element is removed.
|
|
417
|
+
*/
|
|
418
|
+
function detachRef(ref) {
|
|
419
|
+
if (!ref) return;
|
|
420
|
+
|
|
421
|
+
if (typeof ref === 'function') {
|
|
422
|
+
try {
|
|
423
|
+
ref(null);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('[render] Detach ref error:', error);
|
|
426
|
+
}
|
|
427
|
+
} else if (typeof ref === 'object') {
|
|
428
|
+
ref.current = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Core Render Function ─────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Render a virtual DOM element into a real DOM container.
|
|
436
|
+
*
|
|
437
|
+
* @param {Object} vnode - Virtual DOM element
|
|
438
|
+
* @param {HTMLElement} container - Target DOM container
|
|
439
|
+
* @param {Function} [callback] - Called after render completes
|
|
440
|
+
* @returns {HTMLElement} The rendered DOM node
|
|
441
|
+
*/
|
|
442
|
+
function render(vnode, container, callback) {
|
|
443
|
+
if (!container) {
|
|
444
|
+
throw new Error('[render] Target container is not a DOM element');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!(container instanceof HTMLElement) && !(container instanceof DocumentFragment)) {
|
|
448
|
+
throw new Error('[render] Target container must be an HTMLElement or DocumentFragment');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
rootContainer = container;
|
|
452
|
+
|
|
453
|
+
// Set up event delegation on first render
|
|
454
|
+
if (!eventDelegationMap.has(container)) {
|
|
455
|
+
setupEventDelegation(container);
|
|
456
|
+
eventDelegationMap.set(container, true);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle null/undefined/boolean vnodes
|
|
460
|
+
if (vnode === null || vnode === undefined || typeof vnode === 'boolean') {
|
|
461
|
+
while (container.firstChild) {
|
|
462
|
+
container.removeChild(container.firstChild);
|
|
463
|
+
}
|
|
464
|
+
if (typeof callback === 'function') callback();
|
|
465
|
+
return container;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Render the vnode to DOM
|
|
469
|
+
const domNode = renderVNode(vnode, container, null);
|
|
470
|
+
|
|
471
|
+
// Clear container and insert new DOM
|
|
472
|
+
while (container.firstChild) {
|
|
473
|
+
container.removeChild(container.firstChild);
|
|
474
|
+
}
|
|
475
|
+
container.appendChild(domNode);
|
|
476
|
+
|
|
477
|
+
if (typeof callback === 'function') callback();
|
|
478
|
+
|
|
479
|
+
return domNode;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Render a virtual DOM node to a real DOM node.
|
|
484
|
+
* @param {Object} vnode
|
|
485
|
+
* @param {HTMLElement} container
|
|
486
|
+
* @param {string|null} namespace
|
|
487
|
+
* @returns {HTMLElement|Text}
|
|
488
|
+
*/
|
|
489
|
+
function renderVNode(vnode, container, namespace) {
|
|
490
|
+
// Handle primitive values
|
|
491
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
492
|
+
return createTextNode(String(vnode));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Handle null/undefined/boolean
|
|
496
|
+
if (vnode === null || vnode === undefined || typeof vnode === 'boolean') {
|
|
497
|
+
return createCommentNode('empty');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Handle arrays
|
|
501
|
+
if (Array.isArray(vnode)) {
|
|
502
|
+
const fragment = createDocumentFragment();
|
|
503
|
+
vnode.forEach((child) => {
|
|
504
|
+
const childNode = renderVNode(child, fragment, namespace);
|
|
505
|
+
if (childNode) fragment.appendChild(childNode);
|
|
506
|
+
});
|
|
507
|
+
return fragment;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Handle invalid vnodes
|
|
511
|
+
if (typeof vnode !== 'object') {
|
|
512
|
+
console.warn('[render] Invalid vnode type:', typeof vnode);
|
|
513
|
+
return createCommentNode('invalid');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Text node
|
|
517
|
+
if (vnode.type === TEXT_NODE_TYPE) {
|
|
518
|
+
return createTextNode(vnode.props.nodeValue || '');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Fragment
|
|
522
|
+
if (vnode.type === REACT_FRAGMENT_TYPE) {
|
|
523
|
+
return renderFragment(vnode, container, namespace);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Portal
|
|
527
|
+
if (vnode.$$typeof === Symbol.for('elementdrawing.portal')) {
|
|
528
|
+
return renderPortal(vnode, namespace);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Suspense
|
|
532
|
+
if (vnode.type === REACT_SUSPENSE_TYPE) {
|
|
533
|
+
return renderSuspense(vnode, container, namespace);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// StrictMode
|
|
537
|
+
if (vnode.type === REACT_STRICT_MODE_TYPE) {
|
|
538
|
+
return renderStrictMode(vnode, container, namespace);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Error Boundary
|
|
542
|
+
if (vnode.type === REACT_ERROR_BOUNDARY_TYPE) {
|
|
543
|
+
return renderErrorBoundaryVNode(vnode, container, namespace);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Lazy component
|
|
547
|
+
if (vnode.$$typeof === Symbol.for('elementdrawing.lazy')) {
|
|
548
|
+
return renderLazyComponent(vnode, container, namespace);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Function component
|
|
552
|
+
if (typeof vnode.type === 'function' && !vnode.type._isClassComponent) {
|
|
553
|
+
return renderFunctionComponent(vnode, container, namespace);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Class component
|
|
557
|
+
if (typeof vnode.type === 'function' && vnode.type._isClassComponent) {
|
|
558
|
+
return renderClassComponent(vnode, container, namespace);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Host element (HTML/SVG)
|
|
562
|
+
if (typeof vnode.type === 'string') {
|
|
563
|
+
return renderHostElement(vnode, container, namespace);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.warn('[render] Unknown vnode type:', vnode.type);
|
|
567
|
+
return createCommentNode('unknown');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ─── Special Renderers ────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
function renderFragment(vnode, container, namespace) {
|
|
573
|
+
const fragment = createDocumentFragment();
|
|
574
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
575
|
+
|
|
576
|
+
children.forEach((child) => {
|
|
577
|
+
const childNode = renderVNode(child, fragment, namespace);
|
|
578
|
+
if (childNode) fragment.appendChild(childNode);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return fragment;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderPortal(vnode, namespace) {
|
|
585
|
+
const { children, containerInfo } = vnode;
|
|
586
|
+
if (!containerInfo) {
|
|
587
|
+
console.warn('[render] Portal requires a container element');
|
|
588
|
+
return createCommentNode('portal-no-container');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const childNodes = Array.isArray(children) ? children : [children];
|
|
592
|
+
childNodes.forEach((child) => {
|
|
593
|
+
const domNode = renderVNode(child, containerInfo, namespace);
|
|
594
|
+
if (domNode) containerInfo.appendChild(domNode);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return createCommentNode('portal');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function renderSuspense(vnode, container, namespace) {
|
|
601
|
+
try {
|
|
602
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
603
|
+
const fragment = createDocumentFragment();
|
|
604
|
+
|
|
605
|
+
children.forEach((child) => {
|
|
606
|
+
const childNode = renderVNode(child, fragment, namespace);
|
|
607
|
+
if (childNode) fragment.appendChild(childNode);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return fragment;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
// If a child suspends, render the fallback
|
|
613
|
+
if (vnode.props && vnode.props.fallback) {
|
|
614
|
+
return renderVNode(vnode.props.fallback, container, namespace);
|
|
615
|
+
}
|
|
616
|
+
return createCommentNode('suspense-error');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function renderStrictMode(vnode, container, namespace) {
|
|
621
|
+
// StrictMode renders children normally but enables double-invoke
|
|
622
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
623
|
+
const fragment = createDocumentFragment();
|
|
624
|
+
|
|
625
|
+
children.forEach((child) => {
|
|
626
|
+
const childNode = renderVNode(child, fragment, namespace);
|
|
627
|
+
if (childNode) fragment.appendChild(childNode);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
return fragment;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function renderErrorBoundaryVNode(vnode, container, namespace) {
|
|
634
|
+
try {
|
|
635
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
636
|
+
const fragment = createDocumentFragment();
|
|
637
|
+
|
|
638
|
+
children.forEach((child) => {
|
|
639
|
+
const childNode = renderVNode(child, fragment, namespace);
|
|
640
|
+
if (childNode) fragment.appendChild(childNode);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
return fragment;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (vnode.props && vnode.props.fallback) {
|
|
646
|
+
return renderVNode(vnode.props.fallback, container, namespace);
|
|
647
|
+
}
|
|
648
|
+
if (vnode.props && typeof vnode.props.onError === 'function') {
|
|
649
|
+
vnode.props.onError(error);
|
|
650
|
+
}
|
|
651
|
+
return createCommentNode('error-boundary');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderLazyComponent(vnode, container, namespace) {
|
|
656
|
+
const payload = vnode._payload || vnode.type._payload;
|
|
657
|
+
if (!payload) return createCommentNode('lazy-no-payload');
|
|
658
|
+
|
|
659
|
+
if (payload._status === 0) {
|
|
660
|
+
// Resolved
|
|
661
|
+
const Component = payload._result;
|
|
662
|
+
return renderVNode(createElement(Component, vnode.props), container, namespace);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (payload._status === 1) {
|
|
666
|
+
// Rejected
|
|
667
|
+
throw payload._result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Pending - trigger init
|
|
671
|
+
if (vnode.type._init) {
|
|
672
|
+
vnode.type._init(payload);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return createCommentNode('lazy-pending');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function renderFunctionComponent(vnode, container, namespace) {
|
|
679
|
+
try {
|
|
680
|
+
const { type, props } = vnode;
|
|
681
|
+
const renderedVNode = type(props);
|
|
682
|
+
return renderVNode(renderedVNode, container, namespace);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.error('[render] Function component error:', error);
|
|
685
|
+
return renderErrorFallback(error, vnode);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function renderClassComponent(vnode, container, namespace) {
|
|
690
|
+
try {
|
|
691
|
+
const { type, props } = vnode;
|
|
692
|
+
const instance = new type(props);
|
|
693
|
+
instance._vnode = vnode;
|
|
694
|
+
|
|
695
|
+
if (vnode.ref) {
|
|
696
|
+
assignRef(vnode.ref, instance);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const renderedVNode = instance.render();
|
|
700
|
+
return renderVNode(renderedVNode, container, namespace);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
console.error('[render] Class component error:', error);
|
|
703
|
+
return renderErrorFallback(error, vnode);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function renderHostElement(vnode, container, namespace) {
|
|
708
|
+
const { type, props, ref } = vnode;
|
|
709
|
+
|
|
710
|
+
// Determine namespace
|
|
711
|
+
let currentNamespace = namespace;
|
|
712
|
+
if (type === 'svg') {
|
|
713
|
+
currentNamespace = SVG_NAMESPACE;
|
|
714
|
+
} else if (type === 'math') {
|
|
715
|
+
currentNamespace = MATH_NAMESPACE;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Create DOM element
|
|
719
|
+
const domNode = createDOMElement(type, props, currentNamespace);
|
|
720
|
+
|
|
721
|
+
// Apply attributes
|
|
722
|
+
for (const propName in props) {
|
|
723
|
+
if (!props.hasOwnProperty(propName)) continue;
|
|
724
|
+
if (propName === 'children') continue;
|
|
725
|
+
if (propName === 'dangerouslySetInnerHTML') continue;
|
|
726
|
+
|
|
727
|
+
setAttribute(domNode, propName, props[propName]);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Handle dangerouslySetInnerHTML
|
|
731
|
+
if (props.dangerouslySetInnerHTML && props.dangerouslySetInnerHTML.__html) {
|
|
732
|
+
domNode.innerHTML = props.dangerouslySetInnerHTML.__html;
|
|
733
|
+
} else if (!VOID_ELEMENTS.has(type)) {
|
|
734
|
+
// Render children
|
|
735
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
736
|
+
children.forEach((child) => {
|
|
737
|
+
const childNode = renderVNode(child, domNode, currentNamespace);
|
|
738
|
+
if (childNode) domNode.appendChild(childNode);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Assign ref
|
|
743
|
+
if (ref) {
|
|
744
|
+
assignRef(ref, domNode);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Store vnode reference on DOM node for reconciliation
|
|
748
|
+
domNode.__ed_vnode = vnode;
|
|
749
|
+
|
|
750
|
+
return domNode;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function renderErrorFallback(error, vnode) {
|
|
754
|
+
const fallback = vnode.props?.fallback;
|
|
755
|
+
if (fallback) {
|
|
756
|
+
return renderVNode(fallback, null, null);
|
|
757
|
+
}
|
|
758
|
+
return createCommentNode('error: ' + error.message);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ─── Hydration Support ────────────────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Hydrate a virtual DOM tree onto existing DOM nodes.
|
|
765
|
+
*
|
|
766
|
+
* @param {Object} vnode
|
|
767
|
+
* @param {HTMLElement} container
|
|
768
|
+
* @param {Function} [callback]
|
|
769
|
+
* @returns {HTMLElement}
|
|
770
|
+
*/
|
|
771
|
+
function hydrate(vnode, container, callback) {
|
|
772
|
+
if (!container) {
|
|
773
|
+
throw new Error('[hydrate] Target container is not a DOM element');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const domNode = hydrateVNode(vnode, container, container.firstChild, null);
|
|
777
|
+
|
|
778
|
+
if (typeof callback === 'function') callback();
|
|
779
|
+
|
|
780
|
+
return domNode;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Hydrate a single virtual DOM node.
|
|
785
|
+
*/
|
|
786
|
+
function hydrateVNode(vnode, parent, existingNode, namespace) {
|
|
787
|
+
if (!existingNode) {
|
|
788
|
+
return renderVNode(vnode, parent, namespace);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Handle text nodes
|
|
792
|
+
if (existingNode.nodeType === 3) { // Node.TEXT_NODE
|
|
793
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
794
|
+
const text = String(vnode);
|
|
795
|
+
if (existingNode.textContent !== text) {
|
|
796
|
+
existingNode.textContent = text;
|
|
797
|
+
}
|
|
798
|
+
return existingNode;
|
|
799
|
+
}
|
|
800
|
+
const newNode = renderVNode(vnode, parent, namespace);
|
|
801
|
+
parent.replaceChild(newNode, existingNode);
|
|
802
|
+
return newNode;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Handle element nodes
|
|
806
|
+
if (existingNode.nodeType === 1) { // Node.ELEMENT_NODE
|
|
807
|
+
if (typeof vnode === 'object' && vnode.type && typeof vnode.type === 'string' &&
|
|
808
|
+
vnode.type === existingNode.tagName.toLowerCase()) {
|
|
809
|
+
// Same tag - update attributes
|
|
810
|
+
for (const propName in vnode.props) {
|
|
811
|
+
if (propName === 'children') continue;
|
|
812
|
+
setAttribute(existingNode, propName, vnode.props[propName]);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Hydrate children
|
|
816
|
+
const children = flattenChildrenFromVNode(vnode);
|
|
817
|
+
let existingChild = existingNode.firstChild;
|
|
818
|
+
|
|
819
|
+
children.forEach((childVNode) => {
|
|
820
|
+
existingChild = hydrateVNode(childVNode, existingNode, existingChild, namespace);
|
|
821
|
+
if (existingChild) existingChild = existingChild.nextSibling;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Remove extra DOM children
|
|
825
|
+
while (existingChild) {
|
|
826
|
+
const next = existingChild.nextSibling;
|
|
827
|
+
existingNode.removeChild(existingChild);
|
|
828
|
+
existingChild = next;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Store vnode reference
|
|
832
|
+
existingNode.__ed_vnode = vnode;
|
|
833
|
+
|
|
834
|
+
// Assign ref
|
|
835
|
+
if (vnode.ref) {
|
|
836
|
+
assignRef(vnode.ref, existingNode);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return existingNode;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Different tag - replace
|
|
843
|
+
const newNode = renderVNode(vnode, parent, namespace);
|
|
844
|
+
parent.replaceChild(newNode, existingNode);
|
|
845
|
+
return newNode;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return renderVNode(vnode, parent, namespace);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ─── Utility ──────────────────────────────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
function flattenChildrenFromVNode(vnode) {
|
|
854
|
+
if (!vnode || !vnode.props || !vnode.props.children) return [];
|
|
855
|
+
const children = vnode.props.children;
|
|
856
|
+
if (Array.isArray(children)) return children;
|
|
857
|
+
return [children];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Unmount a rendered tree from a container.
|
|
862
|
+
* @param {HTMLElement} container
|
|
863
|
+
*/
|
|
864
|
+
function unmountContainer(container) {
|
|
865
|
+
if (!container) return;
|
|
866
|
+
|
|
867
|
+
// Detach refs and clean up
|
|
868
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
869
|
+
let node;
|
|
870
|
+
while ((node = walker.nextNode())) {
|
|
871
|
+
if (node.__ed_vnode && node.__ed_vnode.ref) {
|
|
872
|
+
detachRef(node.__ed_vnode.ref);
|
|
873
|
+
}
|
|
874
|
+
delete node.__ed_vnode;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Remove all children
|
|
878
|
+
while (container.firstChild) {
|
|
879
|
+
container.removeChild(container.firstChild);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
eventDelegationMap.delete(container);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
886
|
+
|
|
887
|
+
module.exports = {
|
|
888
|
+
render,
|
|
889
|
+
hydrate,
|
|
890
|
+
unmountContainer,
|
|
891
|
+
createDOMElement,
|
|
892
|
+
createTextNode,
|
|
893
|
+
createCommentNode,
|
|
894
|
+
createDocumentFragment,
|
|
895
|
+
setAttribute,
|
|
896
|
+
removeAttribute,
|
|
897
|
+
setProperty,
|
|
898
|
+
applyStyle,
|
|
899
|
+
applyClassName,
|
|
900
|
+
setEventHandler,
|
|
901
|
+
removeEventHandler,
|
|
902
|
+
setupEventDelegation,
|
|
903
|
+
assignRef,
|
|
904
|
+
detachRef,
|
|
905
|
+
renderVNode,
|
|
906
|
+
hydrateVNode,
|
|
907
|
+
camelToKebab,
|
|
908
|
+
normalizeEventType,
|
|
909
|
+
VOID_ELEMENTS,
|
|
910
|
+
SVG_TAGS,
|
|
911
|
+
BOOLEAN_ATTRIBUTES,
|
|
912
|
+
SVG_NAMESPACE,
|
|
913
|
+
MATH_NAMESPACE,
|
|
914
|
+
};
|