eleva 1.2.17-beta → 1.2.19-beta
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/README.md +27 -15
- package/dist/eleva.cjs.js +352 -155
- package/dist/eleva.cjs.js.map +1 -1
- package/dist/eleva.d.ts +271 -103
- package/dist/eleva.esm.js +352 -155
- package/dist/eleva.esm.js.map +1 -1
- package/dist/eleva.umd.js +352 -155
- package/dist/eleva.umd.js.map +1 -1
- package/dist/eleva.umd.min.js +2 -2
- package/dist/eleva.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/Eleva.js +143 -75
- package/src/modules/Emitter.js +23 -7
- package/src/modules/Renderer.js +187 -77
- package/src/modules/Signal.js +10 -7
- package/src/modules/TemplateEngine.js +6 -3
- package/types/core/Eleva.d.ts +164 -68
- package/types/core/Eleva.d.ts.map +1 -1
- package/types/modules/Emitter.d.ts +26 -10
- package/types/modules/Emitter.d.ts.map +1 -1
- package/types/modules/Renderer.d.ts +70 -16
- package/types/modules/Renderer.d.ts.map +1 -1
- package/types/modules/Signal.d.ts +11 -9
- package/types/modules/Signal.d.ts.map +1 -1
- package/types/modules/TemplateEngine.d.ts +8 -5
- package/types/modules/TemplateEngine.d.ts.map +1 -1
package/src/modules/Renderer.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A regular expression to match hyphenated lowercase letters.
|
|
5
|
+
* @private
|
|
6
|
+
* @type {RegExp}
|
|
7
|
+
*/
|
|
8
|
+
const CAMEL_RE = /-([a-z])/g;
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* @class 🎨 Renderer
|
|
5
|
-
* @classdesc A DOM renderer that
|
|
6
|
-
*
|
|
7
|
-
*
|
|
12
|
+
* @classdesc A high-performance DOM renderer that implements an optimized direct DOM diffing algorithm.
|
|
13
|
+
*
|
|
14
|
+
* Key features:
|
|
15
|
+
* - Single-pass diffing algorithm for efficient DOM updates
|
|
16
|
+
* - Key-based node reconciliation for optimal performance
|
|
17
|
+
* - Intelligent attribute handling for ARIA, data attributes, and boolean properties
|
|
18
|
+
* - Preservation of special Eleva-managed instances and style elements
|
|
19
|
+
* - Memory-efficient with reusable temporary containers
|
|
20
|
+
*
|
|
21
|
+
* The renderer is designed to minimize DOM operations while maintaining
|
|
22
|
+
* exact attribute synchronization and proper node identity preservation.
|
|
23
|
+
* It's particularly optimized for frequent updates and complex DOM structures.
|
|
8
24
|
*
|
|
9
25
|
* @example
|
|
10
26
|
* const renderer = new Renderer();
|
|
@@ -14,47 +30,46 @@
|
|
|
14
30
|
*/
|
|
15
31
|
export class Renderer {
|
|
16
32
|
/**
|
|
17
|
-
* Creates a new Renderer instance
|
|
33
|
+
* Creates a new Renderer instance.
|
|
18
34
|
* @public
|
|
19
35
|
*/
|
|
20
36
|
constructor() {
|
|
21
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* A temporary container to hold the new HTML content while diffing.
|
|
39
|
+
* @private
|
|
40
|
+
* @type {HTMLElement}
|
|
41
|
+
*/
|
|
22
42
|
this._tempContainer = document.createElement("div");
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
/**
|
|
26
|
-
* Patches the DOM of
|
|
27
|
-
* Efficiently updates the DOM by parsing new HTML into a reusable container
|
|
28
|
-
* and applying only the necessary changes.
|
|
46
|
+
* Patches the DOM of the given container with the provided HTML string.
|
|
29
47
|
*
|
|
30
48
|
* @public
|
|
31
49
|
* @param {HTMLElement} container - The container element to patch.
|
|
32
|
-
* @param {string} newHtml - The new HTML
|
|
50
|
+
* @param {string} newHtml - The new HTML string.
|
|
33
51
|
* @returns {void}
|
|
34
|
-
* @throws {
|
|
52
|
+
* @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
|
|
53
|
+
* @throws {Error} If DOM patching fails.
|
|
35
54
|
*/
|
|
36
55
|
patchDOM(container, newHtml) {
|
|
37
56
|
if (!(container instanceof HTMLElement)) {
|
|
38
|
-
throw new
|
|
57
|
+
throw new TypeError("Container must be an HTMLElement");
|
|
39
58
|
}
|
|
40
59
|
if (typeof newHtml !== "string") {
|
|
41
|
-
throw new
|
|
60
|
+
throw new TypeError("newHtml must be a string");
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
try {
|
|
45
|
-
// Directly set new HTML, replacing any existing content
|
|
46
64
|
this._tempContainer.innerHTML = newHtml;
|
|
47
|
-
|
|
48
65
|
this._diff(container, this._tempContainer);
|
|
49
|
-
} catch {
|
|
50
|
-
throw new Error(
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`Failed to patch DOM: ${error.message}`);
|
|
51
68
|
}
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
/**
|
|
55
|
-
*
|
|
56
|
-
* This method recursively compares nodes and their attributes, applying only
|
|
57
|
-
* the necessary changes to minimize DOM operations.
|
|
72
|
+
* Performs a diff between two DOM nodes and patches the old node to match the new node.
|
|
58
73
|
*
|
|
59
74
|
* @private
|
|
60
75
|
* @param {HTMLElement} oldParent - The original DOM element.
|
|
@@ -62,94 +77,127 @@ export class Renderer {
|
|
|
62
77
|
* @returns {void}
|
|
63
78
|
*/
|
|
64
79
|
_diff(oldParent, newParent) {
|
|
65
|
-
if (oldParent.isEqualNode(newParent)) return;
|
|
80
|
+
if (oldParent === newParent || oldParent.isEqualNode?.(newParent)) return;
|
|
66
81
|
|
|
67
|
-
const oldChildren = oldParent.childNodes;
|
|
68
|
-
const newChildren = newParent.childNodes;
|
|
69
|
-
|
|
82
|
+
const oldChildren = Array.from(oldParent.childNodes);
|
|
83
|
+
const newChildren = Array.from(newParent.childNodes);
|
|
84
|
+
let oldStartIdx = 0,
|
|
85
|
+
newStartIdx = 0;
|
|
86
|
+
let oldEndIdx = oldChildren.length - 1;
|
|
87
|
+
let newEndIdx = newChildren.length - 1;
|
|
88
|
+
let oldKeyMap = null;
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
|
91
|
+
let oldStartNode = oldChildren[oldStartIdx];
|
|
92
|
+
let newStartNode = newChildren[newStartIdx];
|
|
74
93
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
}
|
|
94
|
+
if (!oldStartNode) {
|
|
95
|
+
oldStartNode = oldChildren[++oldStartIdx];
|
|
96
|
+
} else if (this._isSameNode(oldStartNode, newStartNode)) {
|
|
97
|
+
this._patchNode(oldStartNode, newStartNode);
|
|
98
|
+
oldStartIdx++;
|
|
99
|
+
newStartIdx++;
|
|
100
|
+
} else {
|
|
101
|
+
if (!oldKeyMap) {
|
|
102
|
+
oldKeyMap = this._createKeyMap(oldChildren, oldStartIdx, oldEndIdx);
|
|
103
|
+
}
|
|
104
|
+
const key = this._getNodeKey(newStartNode);
|
|
105
|
+
const oldNodeToMove = key ? oldKeyMap.get(key) : null;
|
|
78
106
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
oldNode.nodeName === "STYLE" &&
|
|
86
|
-
oldNode.hasAttribute("data-e-style")
|
|
87
|
-
) {
|
|
88
|
-
continue;
|
|
107
|
+
if (oldNodeToMove) {
|
|
108
|
+
this._patchNode(oldNodeToMove, newStartNode);
|
|
109
|
+
oldParent.insertBefore(oldNodeToMove, oldStartNode);
|
|
110
|
+
oldChildren[oldChildren.indexOf(oldNodeToMove)] = null;
|
|
111
|
+
} else {
|
|
112
|
+
oldParent.insertBefore(newStartNode.cloneNode(true), oldStartNode);
|
|
89
113
|
}
|
|
90
|
-
|
|
91
|
-
continue;
|
|
114
|
+
newStartIdx++;
|
|
92
115
|
}
|
|
116
|
+
}
|
|
93
117
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
if (oldStartIdx > oldEndIdx) {
|
|
119
|
+
const refNode = newChildren[newEndIdx + 1]
|
|
120
|
+
? oldChildren[oldStartIdx]
|
|
121
|
+
: null;
|
|
122
|
+
for (let i = newStartIdx; i <= newEndIdx; i++) {
|
|
123
|
+
if (newChildren[i])
|
|
124
|
+
oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
|
|
125
|
+
}
|
|
126
|
+
} else if (newStartIdx > newEndIdx) {
|
|
127
|
+
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
|
|
128
|
+
if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
|
|
101
129
|
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
102
132
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Patches a single node.
|
|
135
|
+
*
|
|
136
|
+
* @private
|
|
137
|
+
* @param {Node} oldNode - The original DOM node.
|
|
138
|
+
* @param {Node} newNode - The new DOM node.
|
|
139
|
+
* @returns {void}
|
|
140
|
+
*/
|
|
141
|
+
_patchNode(oldNode, newNode) {
|
|
142
|
+
if (oldNode?._eleva_instance) return;
|
|
106
143
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
if (!this._isSameNode(oldNode, newNode)) {
|
|
145
|
+
oldNode.replaceWith(newNode.cloneNode(true));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
|
150
|
+
this._updateAttributes(oldNode, newNode);
|
|
151
|
+
this._diff(oldNode, newNode);
|
|
152
|
+
} else if (
|
|
153
|
+
oldNode.nodeType === Node.TEXT_NODE &&
|
|
154
|
+
oldNode.nodeValue !== newNode.nodeValue
|
|
155
|
+
) {
|
|
156
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
120
157
|
}
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
161
|
+
* Removes a node from its parent.
|
|
162
|
+
*
|
|
163
|
+
* @private
|
|
164
|
+
* @param {HTMLElement} parent - The parent element containing the node to remove.
|
|
165
|
+
* @param {Node} node - The node to remove.
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
_removeNode(parent, node) {
|
|
169
|
+
if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
|
|
170
|
+
|
|
171
|
+
parent.removeChild(node);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Updates the attributes of an element to match a new element's attributes.
|
|
126
176
|
*
|
|
127
177
|
* @private
|
|
128
|
-
* @param {HTMLElement} oldEl - The element to update.
|
|
129
|
-
* @param {HTMLElement} newEl - The element
|
|
178
|
+
* @param {HTMLElement} oldEl - The original element to update.
|
|
179
|
+
* @param {HTMLElement} newEl - The new element to update.
|
|
130
180
|
* @returns {void}
|
|
131
181
|
*/
|
|
132
182
|
_updateAttributes(oldEl, newEl) {
|
|
133
183
|
const oldAttrs = oldEl.attributes;
|
|
134
184
|
const newAttrs = newEl.attributes;
|
|
135
185
|
|
|
136
|
-
//
|
|
137
|
-
for (
|
|
186
|
+
// Single pass for new/updated attributes
|
|
187
|
+
for (let i = 0; i < newAttrs.length; i++) {
|
|
188
|
+
const { name, value } = newAttrs[i];
|
|
138
189
|
if (name.startsWith("@")) continue;
|
|
139
|
-
|
|
140
190
|
if (oldEl.getAttribute(name) === value) continue;
|
|
141
|
-
|
|
142
191
|
oldEl.setAttribute(name, value);
|
|
143
192
|
|
|
144
193
|
if (name.startsWith("aria-")) {
|
|
145
194
|
const prop =
|
|
146
|
-
"aria" +
|
|
147
|
-
name.slice(5).replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
195
|
+
"aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
|
|
148
196
|
oldEl[prop] = value;
|
|
149
197
|
} else if (name.startsWith("data-")) {
|
|
150
198
|
oldEl.dataset[name.slice(5)] = value;
|
|
151
199
|
} else {
|
|
152
|
-
const prop = name.replace(
|
|
200
|
+
const prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
|
|
153
201
|
if (prop in oldEl) {
|
|
154
202
|
const descriptor = Object.getOwnPropertyDescriptor(
|
|
155
203
|
Object.getPrototypeOf(oldEl),
|
|
@@ -159,7 +207,6 @@ export class Renderer {
|
|
|
159
207
|
typeof oldEl[prop] === "boolean" ||
|
|
160
208
|
(descriptor?.get &&
|
|
161
209
|
typeof descriptor.get.call(oldEl) === "boolean");
|
|
162
|
-
|
|
163
210
|
if (isBoolean) {
|
|
164
211
|
oldEl[prop] =
|
|
165
212
|
value !== "false" &&
|
|
@@ -171,11 +218,74 @@ export class Renderer {
|
|
|
171
218
|
}
|
|
172
219
|
}
|
|
173
220
|
|
|
174
|
-
// Remove
|
|
175
|
-
for (
|
|
221
|
+
// Remove any attributes no longer present
|
|
222
|
+
for (let i = oldAttrs.length - 1; i >= 0; i--) {
|
|
223
|
+
const name = oldAttrs[i].name;
|
|
176
224
|
if (!newEl.hasAttribute(name)) {
|
|
177
225
|
oldEl.removeAttribute(name);
|
|
178
226
|
}
|
|
179
227
|
}
|
|
180
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Determines if two nodes are the same based on their type, name, and key attributes.
|
|
232
|
+
*
|
|
233
|
+
* @private
|
|
234
|
+
* @param {Node} oldNode - The first node to compare.
|
|
235
|
+
* @param {Node} newNode - The second node to compare.
|
|
236
|
+
* @returns {boolean} True if the nodes are considered the same, false otherwise.
|
|
237
|
+
*/
|
|
238
|
+
_isSameNode(oldNode, newNode) {
|
|
239
|
+
if (!oldNode || !newNode) return false;
|
|
240
|
+
|
|
241
|
+
const oldKey =
|
|
242
|
+
oldNode.nodeType === Node.ELEMENT_NODE
|
|
243
|
+
? oldNode.getAttribute("key")
|
|
244
|
+
: null;
|
|
245
|
+
const newKey =
|
|
246
|
+
newNode.nodeType === Node.ELEMENT_NODE
|
|
247
|
+
? newNode.getAttribute("key")
|
|
248
|
+
: null;
|
|
249
|
+
|
|
250
|
+
if (oldKey && newKey) return oldKey === newKey;
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
!oldKey &&
|
|
254
|
+
!newKey &&
|
|
255
|
+
oldNode.nodeType === newNode.nodeType &&
|
|
256
|
+
oldNode.nodeName === newNode.nodeName
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Creates a key map for the children of a parent node.
|
|
262
|
+
*
|
|
263
|
+
* @private
|
|
264
|
+
* @param {Array<Node>} children - The children of the parent node.
|
|
265
|
+
* @param {number} start - The start index of the children.
|
|
266
|
+
* @param {number} end - The end index of the children.
|
|
267
|
+
* @returns {Map<string, Node>} A key map for the children.
|
|
268
|
+
*/
|
|
269
|
+
_createKeyMap(children, start, end) {
|
|
270
|
+
const map = new Map();
|
|
271
|
+
for (let i = start; i <= end; i++) {
|
|
272
|
+
const child = children[i];
|
|
273
|
+
const key = this._getNodeKey(child);
|
|
274
|
+
if (key) map.set(key, child);
|
|
275
|
+
}
|
|
276
|
+
return map;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Extracts the key attribute from a node if it exists.
|
|
281
|
+
*
|
|
282
|
+
* @private
|
|
283
|
+
* @param {Node} node - The node to extract the key from.
|
|
284
|
+
* @returns {string|null} The key attribute value or null if not found.
|
|
285
|
+
*/
|
|
286
|
+
_getNodeKey(node) {
|
|
287
|
+
return node?.nodeType === Node.ELEMENT_NODE
|
|
288
|
+
? node.getAttribute("key")
|
|
289
|
+
: null;
|
|
290
|
+
}
|
|
181
291
|
}
|
package/src/modules/Signal.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
|
|
6
6
|
* Signals notify registered watchers when their value changes, enabling efficient DOM updates
|
|
7
7
|
* through targeted patching rather than full re-renders.
|
|
8
|
+
* Updates are batched using microtasks to prevent multiple synchronous notifications.
|
|
9
|
+
* The class is generic, allowing type-safe handling of any value type T.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
12
|
* const count = new Signal(0);
|
|
@@ -17,12 +19,12 @@ export class Signal {
|
|
|
17
19
|
* Creates a new Signal instance with the specified initial value.
|
|
18
20
|
*
|
|
19
21
|
* @public
|
|
20
|
-
* @param {
|
|
22
|
+
* @param {T} value - The initial value of the signal.
|
|
21
23
|
*/
|
|
22
24
|
constructor(value) {
|
|
23
|
-
/** @private {T} Internal storage for the signal's current value
|
|
25
|
+
/** @private {T} Internal storage for the signal's current value */
|
|
24
26
|
this._value = value;
|
|
25
|
-
/** @private {Set<
|
|
27
|
+
/** @private {Set<(value: T) => void>} Collection of callback functions to be notified when value changes */
|
|
26
28
|
this._watchers = new Set();
|
|
27
29
|
/** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
|
|
28
30
|
this._pending = false;
|
|
@@ -32,7 +34,7 @@ export class Signal {
|
|
|
32
34
|
* Gets the current value of the signal.
|
|
33
35
|
*
|
|
34
36
|
* @public
|
|
35
|
-
* @returns {T} The current value
|
|
37
|
+
* @returns {T} The current value.
|
|
36
38
|
*/
|
|
37
39
|
get value() {
|
|
38
40
|
return this._value;
|
|
@@ -43,7 +45,7 @@ export class Signal {
|
|
|
43
45
|
* The notification is batched using microtasks to prevent multiple synchronous updates.
|
|
44
46
|
*
|
|
45
47
|
* @public
|
|
46
|
-
* @param {T} newVal - The new value to set
|
|
48
|
+
* @param {T} newVal - The new value to set.
|
|
47
49
|
* @returns {void}
|
|
48
50
|
*/
|
|
49
51
|
set value(newVal) {
|
|
@@ -58,8 +60,8 @@ export class Signal {
|
|
|
58
60
|
* The watcher will receive the new value as its argument.
|
|
59
61
|
*
|
|
60
62
|
* @public
|
|
61
|
-
* @param {
|
|
62
|
-
* @returns {
|
|
63
|
+
* @param {(value: T) => void} fn - The callback function to invoke on value change.
|
|
64
|
+
* @returns {() => boolean} A function to unsubscribe the watcher.
|
|
63
65
|
* @example
|
|
64
66
|
* const unsubscribe = signal.watch((value) => console.log(value));
|
|
65
67
|
* // Later...
|
|
@@ -83,6 +85,7 @@ export class Signal {
|
|
|
83
85
|
|
|
84
86
|
this._pending = true;
|
|
85
87
|
queueMicrotask(() => {
|
|
88
|
+
/** @type {(fn: (value: T) => void) => void} */
|
|
86
89
|
this._watchers.forEach((fn) => fn(this._value));
|
|
87
90
|
this._pending = false;
|
|
88
91
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export class TemplateEngine {
|
|
15
15
|
/**
|
|
16
16
|
* @private {RegExp} Regular expression for matching template expressions in the format {{ expression }}
|
|
17
|
+
* @type {RegExp}
|
|
17
18
|
*/
|
|
18
19
|
static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
|
|
19
20
|
|
|
@@ -24,7 +25,7 @@ export class TemplateEngine {
|
|
|
24
25
|
* @public
|
|
25
26
|
* @static
|
|
26
27
|
* @param {string} template - The template string to parse.
|
|
27
|
-
* @param {
|
|
28
|
+
* @param {Record<string, unknown>} data - The data context for evaluating expressions.
|
|
28
29
|
* @returns {string} The parsed template with expressions replaced by their values.
|
|
29
30
|
* @example
|
|
30
31
|
* const result = TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
|
|
@@ -41,12 +42,14 @@ export class TemplateEngine {
|
|
|
41
42
|
/**
|
|
42
43
|
* Evaluates an expression in the context of the provided data object.
|
|
43
44
|
* Note: This does not provide a true sandbox and evaluated expressions may access global scope.
|
|
45
|
+
* The use of the `with` statement is necessary for expression evaluation but has security implications.
|
|
46
|
+
* Expressions should be carefully validated before evaluation.
|
|
44
47
|
*
|
|
45
48
|
* @public
|
|
46
49
|
* @static
|
|
47
50
|
* @param {string} expression - The expression to evaluate.
|
|
48
|
-
* @param {
|
|
49
|
-
* @returns {
|
|
51
|
+
* @param {Record<string, unknown>} data - The data context for evaluation.
|
|
52
|
+
* @returns {unknown} The result of the evaluation, or an empty string if evaluation fails.
|
|
50
53
|
* @example
|
|
51
54
|
* const result = TemplateEngine.evaluate("user.name", { user: { name: "John" } }); // Returns: "John"
|
|
52
55
|
* const age = TemplateEngine.evaluate("user.age", { user: { age: 30 } }); // Returns: 30
|