shablon 0.0.1-rc.3 → 0.0.1-rc.5
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 +8 -5
- package/package.json +1 -1
- package/src/router.js +3 -2
- package/src/state.js +168 -77
- package/src/template.js +11 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Shablon - No-build JavaScript frontend framework
|
|
|
6
6
|
>
|
|
7
7
|
> **Don't use it yet - it hasn't been actually tested in real applications and it may change without notice!**
|
|
8
8
|
|
|
9
|
-
**Shablon** _("template" in Bulgarian)_ is a ~
|
|
9
|
+
**Shablon** _("template" in Bulgarian)_ is a ~6KB JS framework that comes with deeply reactive state management, plain JS extendable templates and hash-based router.
|
|
10
10
|
|
|
11
11
|
Shablon has very small learning curve (**4 main exported functions**) and it is suitable for building Single-page applications (SPA):
|
|
12
12
|
|
|
@@ -142,7 +142,7 @@ importing it from [npm](https://www.npmjs.com/package/shablon).
|
|
|
142
142
|
|
|
143
143
|
The keys of an `obj` must be "stringifiable" because they are used internally to construct a path to the reactive value.
|
|
144
144
|
|
|
145
|
-
The values can be any valid JS value, including nested arrays and objects (aka. it is recursively reactive).
|
|
145
|
+
The values can be any valid JS primitive value, including nested plain arrays and objects (aka. it is recursively reactive).
|
|
146
146
|
|
|
147
147
|
Getters are also supported and can be used as reactive computed properties.
|
|
148
148
|
The value of a reactive getter is "cached", meaning that even if one of the getter dependency changes, as long as the resulting value is the same there will be no unnecessary watch events fired.
|
|
@@ -162,6 +162,9 @@ data.age++
|
|
|
162
162
|
data.activity = "rest"
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
> Note that only plain objects and arrays are wrapped in a nested `Proxy`! `Date`, `Set`, `Map`, `WeakRef`, `WeakSet`, `WeakMap` or any custom object will be resolved as they are to avoid access errors.
|
|
166
|
+
> You can access the original object without the Proxy trap using the special `__raw` key, e.g. `data.someObj.__raw.someKey`.
|
|
167
|
+
|
|
165
168
|
</details>
|
|
166
169
|
|
|
167
170
|
|
|
@@ -171,6 +174,8 @@ data.activity = "rest"
|
|
|
171
174
|
Watch registers a callback function that fires on initialization and
|
|
172
175
|
every time any of its evaluated `store` reactive properties change.
|
|
173
176
|
|
|
177
|
+
Note that for reactive getters, initially the watch `trackedFunc` will be invoked twice because we register a second internal watcher to cache the getter value.
|
|
178
|
+
|
|
174
179
|
It returns a "watcher" object that could be used to `unwatch()` the registered listener.
|
|
175
180
|
|
|
176
181
|
_Optionally also accepts a second callback function that is excluded from the evaluated
|
|
@@ -204,7 +209,7 @@ const data = store({
|
|
|
204
209
|
watch(() => [
|
|
205
210
|
data.a,
|
|
206
211
|
data.b,
|
|
207
|
-
], () => {
|
|
212
|
+
], (_) => { // receive the return result of trackedFunc
|
|
208
213
|
console.log(data.a)
|
|
209
214
|
console.log(data.b)
|
|
210
215
|
console.log(data.c)
|
|
@@ -215,8 +220,6 @@ data.b++ // trigger watch update
|
|
|
215
220
|
data.c++ // doesn't trigger watch update
|
|
216
221
|
```
|
|
217
222
|
|
|
218
|
-
Note that for reactive getters, initially the watch trackCallback will be invoked twice because we register a second internal watcher to cache the getter value.
|
|
219
|
-
|
|
220
223
|
</details>
|
|
221
224
|
|
|
222
225
|
|
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -49,7 +49,7 @@ export function router(routes, options = { fallbackPath: "#/", transition: true
|
|
|
49
49
|
if (!route) {
|
|
50
50
|
if (options.fallbackPath != path) {
|
|
51
51
|
window.location.hash = options.fallbackPath;
|
|
52
|
-
return
|
|
52
|
+
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
console.warn("missing route:", path);
|
|
@@ -120,7 +120,8 @@ function prepareRoutes(routes) {
|
|
|
120
120
|
parts[i].endsWith("}")
|
|
121
121
|
) {
|
|
122
122
|
// param
|
|
123
|
-
parts[i] =
|
|
123
|
+
parts[i] =
|
|
124
|
+
"(?<" + parts[i].substring(1, parts[i].length - 1) + ">[^\\/#?]+)";
|
|
124
125
|
} else {
|
|
125
126
|
// regular path segment
|
|
126
127
|
parts[i] = RegExp.escape(parts[i]);
|
package/src/state.js
CHANGED
|
@@ -7,11 +7,14 @@ let cleanTimeoutId;
|
|
|
7
7
|
|
|
8
8
|
let idSym = Symbol();
|
|
9
9
|
let parentSym = Symbol();
|
|
10
|
-
let pathsResetedSym = Symbol();
|
|
11
10
|
let childrenSym = Symbol();
|
|
12
11
|
let pathsSubsSym = Symbol();
|
|
13
12
|
let unwatchedSym = Symbol();
|
|
14
13
|
let onRemoveSym = Symbol();
|
|
14
|
+
let skipSym = Symbol();
|
|
15
|
+
let evictedSym = Symbol();
|
|
16
|
+
|
|
17
|
+
let pathSeparator = "/";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Watch registers a callback function that fires on initialization and
|
|
@@ -68,7 +71,7 @@ let onRemoveSym = Symbol();
|
|
|
68
71
|
export function watch(trackedFunc, optUntrackedFunc) {
|
|
69
72
|
let watcher = {
|
|
70
73
|
[idSym]: "_" + Math.random(),
|
|
71
|
-
}
|
|
74
|
+
};
|
|
72
75
|
|
|
73
76
|
allWatchers.set(watcher[idSym], watcher);
|
|
74
77
|
|
|
@@ -77,7 +80,7 @@ export function watch(trackedFunc, optUntrackedFunc) {
|
|
|
77
80
|
|
|
78
81
|
// nested watcher -> register previous watcher as parent
|
|
79
82
|
if (activeWatcher) {
|
|
80
|
-
oldActiveWatcher = activeWatcher
|
|
83
|
+
oldActiveWatcher = activeWatcher;
|
|
81
84
|
watcher[parentSym] = activeWatcher[idSym];
|
|
82
85
|
|
|
83
86
|
// store immediate children references for quicker cleanup
|
|
@@ -85,12 +88,44 @@ export function watch(trackedFunc, optUntrackedFunc) {
|
|
|
85
88
|
activeWatcher[childrenSym].push(watcher[idSym]);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// On watcher function run, resets any previous tracking paths
|
|
92
|
+
// because after this new run some of the old dependencies
|
|
93
|
+
// may no longer be reachable/evaluatable.
|
|
94
|
+
//
|
|
95
|
+
// For example, in the below code:
|
|
96
|
+
//
|
|
97
|
+
// ```js
|
|
98
|
+
// const data = store({ a: 0, b: 0, c: 0 })
|
|
99
|
+
//
|
|
100
|
+
// watch(() => {
|
|
101
|
+
// if (data.a > 0) {
|
|
102
|
+
// data.b
|
|
103
|
+
// } else {
|
|
104
|
+
// data.c
|
|
105
|
+
// }
|
|
106
|
+
// })
|
|
107
|
+
// ```
|
|
108
|
+
//
|
|
109
|
+
// initially ONLY "a" and "c" should be trackable because "b"
|
|
110
|
+
// is not reachable (aka. its getter is never invoked).
|
|
111
|
+
//
|
|
112
|
+
// If we increment `a++`, then in the new run ONLY "a" and "b" should be trackable
|
|
113
|
+
// because this time "c" is not reachable (aka. its getter is never invoked)
|
|
114
|
+
// and its previous tracking should be removed for this watcher.
|
|
115
|
+
//
|
|
116
|
+
// Note: The below code works because it reuses the same "subs" reference as in pathWatcherIds
|
|
117
|
+
// and this is intentional to avoid unnecessary iterations.
|
|
118
|
+
watcher[pathsSubsSym]?.forEach((subs) => {
|
|
119
|
+
subs.delete(watcher[idSym]);
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
activeWatcher = watcher;
|
|
89
|
-
|
|
90
|
-
watcher.last = trackedFunc();
|
|
123
|
+
const result = trackedFunc();
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
if (optUntrackedFunc) {
|
|
126
|
+
activeWatcher = null;
|
|
127
|
+
optUntrackedFunc(result);
|
|
128
|
+
}
|
|
94
129
|
|
|
95
130
|
// restore original ref (if any)
|
|
96
131
|
activeWatcher = oldActiveWatcher;
|
|
@@ -135,10 +170,8 @@ function removeWatcher(id) {
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
if (w?.[pathsSubsSym]) {
|
|
138
|
-
for (let
|
|
139
|
-
|
|
140
|
-
subset.delete(id);
|
|
141
|
-
}
|
|
173
|
+
for (let sub of w[pathsSubsSym]) {
|
|
174
|
+
sub.delete(id);
|
|
142
175
|
}
|
|
143
176
|
w[pathsSubsSym] = null;
|
|
144
177
|
}
|
|
@@ -185,12 +218,65 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
185
218
|
? Object.getOwnPropertyDescriptors(obj)
|
|
186
219
|
: {};
|
|
187
220
|
|
|
188
|
-
|
|
189
|
-
get(obj, prop,
|
|
190
|
-
if (prop
|
|
221
|
+
return new Proxy(obj, {
|
|
222
|
+
get(obj, prop, receiver) {
|
|
223
|
+
if (typeof prop == "symbol") {
|
|
224
|
+
return obj[prop];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (prop == "__raw") {
|
|
191
228
|
return obj;
|
|
192
229
|
}
|
|
193
230
|
|
|
231
|
+
// evicted child?
|
|
232
|
+
if (!obj[skipSym] && obj[parentSym]) {
|
|
233
|
+
let props = [];
|
|
234
|
+
let activeObj = obj;
|
|
235
|
+
|
|
236
|
+
let isEvicted = false;
|
|
237
|
+
|
|
238
|
+
// travel up to the root proxy
|
|
239
|
+
// (aka. x.a.b*.c -> x)
|
|
240
|
+
while (activeObj?.[parentSym]) {
|
|
241
|
+
if (activeObj[evictedSym]) {
|
|
242
|
+
isEvicted = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
props.push(activeObj[parentSym][1]);
|
|
246
|
+
activeObj = activeObj[parentSym][0];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// try to access the original path but this time from
|
|
250
|
+
// the root point of view to ensure that we are always accessing
|
|
251
|
+
// an up-to-date store child reference
|
|
252
|
+
// (we want: x.a.b(old).c -> x -> x.a.b(new).c)
|
|
253
|
+
//
|
|
254
|
+
// note: this technically could "leak" but for our case it should be fine
|
|
255
|
+
// because the evicted object will become again garbage collectable
|
|
256
|
+
// once the related watcher(s) are removed
|
|
257
|
+
if (isEvicted) {
|
|
258
|
+
for (let i = props.length - 1; i >= 0; i--) {
|
|
259
|
+
activeObj[skipSym] = true;
|
|
260
|
+
let item = activeObj?.[props[i]];
|
|
261
|
+
activeObj[skipSym] = false;
|
|
262
|
+
|
|
263
|
+
if (i == 0) {
|
|
264
|
+
activeObj = item?.__raw;
|
|
265
|
+
} else {
|
|
266
|
+
activeObj = item;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// the original full nested path is no longer available (null/undefined)
|
|
271
|
+
if (activeObj == undefined) {
|
|
272
|
+
return activeObj;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// update the current obj with the one from the retraced path
|
|
276
|
+
obj = activeObj;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
// getter?
|
|
195
281
|
let getterProp;
|
|
196
282
|
if (descriptors[prop]?.get) {
|
|
@@ -202,24 +288,30 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
202
288
|
|
|
203
289
|
getterProp = prop;
|
|
204
290
|
|
|
205
|
-
// replace with an internal "
|
|
291
|
+
// replace with an internal "@@prop" property so that
|
|
206
292
|
// reactive statements can be cached
|
|
207
|
-
prop = "
|
|
293
|
+
prop = "@@" + prop;
|
|
208
294
|
}
|
|
209
295
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
296
|
+
let propVal = obj[prop];
|
|
297
|
+
|
|
298
|
+
// directly return for functions (pop, push, etc.)
|
|
299
|
+
if (typeof propVal == "function") {
|
|
300
|
+
return propVal;
|
|
213
301
|
}
|
|
214
302
|
|
|
215
|
-
// wrap child object or array as sub store
|
|
303
|
+
// wrap child plain object or array as sub store
|
|
216
304
|
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
!
|
|
305
|
+
propVal != null &&
|
|
306
|
+
typeof propVal == "object" &&
|
|
307
|
+
!propVal[parentSym] &&
|
|
308
|
+
(propVal.constructor?.name == "Object" ||
|
|
309
|
+
propVal.constructor?.name == "Array" ||
|
|
310
|
+
propVal.constructor?.name == undefined) // e.g. Object.create(null)
|
|
220
311
|
) {
|
|
221
|
-
|
|
222
|
-
|
|
312
|
+
propVal[parentSym] = [receiver, prop];
|
|
313
|
+
propVal = createProxy(propVal, pathWatcherIds);
|
|
314
|
+
obj[prop] = propVal;
|
|
223
315
|
}
|
|
224
316
|
|
|
225
317
|
// register watch subscriber (if any)
|
|
@@ -227,52 +319,36 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
227
319
|
let currentPath = getPath(obj, prop);
|
|
228
320
|
let activeWatcherId = activeWatcher[idSym];
|
|
229
321
|
|
|
322
|
+
let propPaths = [currentPath];
|
|
323
|
+
|
|
324
|
+
// always construct all parent paths ("x.a.b.c" => ["a", "a.b", "a.b.c"])
|
|
325
|
+
// because a store child object can be passed as argument to a function
|
|
326
|
+
// and in that case the parents proxy get trap will not be invoked,
|
|
327
|
+
// and their path will not be registered
|
|
328
|
+
if (obj[parentSym]) {
|
|
329
|
+
let parts = currentPath.split(pathSeparator);
|
|
330
|
+
while (parts.pop() && parts.length) {
|
|
331
|
+
propPaths.push(parts.join(pathSeparator));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// initialize a watcher paths tracking set (if not already)
|
|
230
336
|
activeWatcher[pathsSubsSym] = activeWatcher[pathsSubsSym] || new Set();
|
|
231
337
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// const data = store({ a: 0, b: 0, c: 0 })
|
|
240
|
-
//
|
|
241
|
-
// watch(() => {
|
|
242
|
-
// if (data.a > 0) {
|
|
243
|
-
// data.b
|
|
244
|
-
// } else {
|
|
245
|
-
// data.c
|
|
246
|
-
// }
|
|
247
|
-
// })
|
|
248
|
-
// ```
|
|
249
|
-
//
|
|
250
|
-
// initially ONLY "a" and "c" should be trackable because "b"
|
|
251
|
-
// is not reachable (aka. its getter is never invoked).
|
|
252
|
-
//
|
|
253
|
-
// If we increment `a++`, then in the new run ONLY "a" and "b" should be trackable
|
|
254
|
-
// because this time "c" is not reachable (aka. its getter is never invoked)
|
|
255
|
-
// and its previous tracking should be removed for this watcher.
|
|
256
|
-
//
|
|
257
|
-
// Note: The below code works because it reuses the same "subs" reference as in pathWatcherIds
|
|
258
|
-
// and this is intentional to avoid unnecessary iterations.
|
|
259
|
-
if (!activeWatcher[pathsResetedSym]) {
|
|
260
|
-
activeWatcher[pathsSubsSym].forEach((subs) => {
|
|
261
|
-
subs.delete(activeWatcherId)
|
|
262
|
-
})
|
|
263
|
-
activeWatcher[pathsResetedSym] = true;
|
|
264
|
-
}
|
|
338
|
+
// register the paths to watch
|
|
339
|
+
for (let path of propPaths) {
|
|
340
|
+
let subs = pathWatcherIds.get(path);
|
|
341
|
+
if (!subs) {
|
|
342
|
+
subs = new Set();
|
|
343
|
+
pathWatcherIds.set(path, subs);
|
|
344
|
+
}
|
|
265
345
|
|
|
266
|
-
|
|
267
|
-
if (!subs) {
|
|
268
|
-
subs = new Set();
|
|
269
|
-
pathWatcherIds.set(currentPath, subs);
|
|
270
|
-
}
|
|
271
|
-
subs.add(activeWatcherId);
|
|
346
|
+
subs.add(activeWatcherId);
|
|
272
347
|
|
|
273
|
-
|
|
348
|
+
activeWatcher[pathsSubsSym].add(subs);
|
|
349
|
+
}
|
|
274
350
|
|
|
275
|
-
// register
|
|
351
|
+
// register an extra child watcher to update the custom getter prop replacement
|
|
276
352
|
// (should be removed automatically with the removal of the parent watcher)
|
|
277
353
|
if (
|
|
278
354
|
getterProp &&
|
|
@@ -284,7 +360,7 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
284
360
|
|
|
285
361
|
let getFunc = descriptors[getterProp].get.bind(obj);
|
|
286
362
|
|
|
287
|
-
let getWatcher = watch(() => (
|
|
363
|
+
let getWatcher = watch(() => (receiver[prop] = getFunc()));
|
|
288
364
|
|
|
289
365
|
getWatcher[onRemoveSym] = () => {
|
|
290
366
|
descriptors[getterProp]?.watchers?.delete(watcherId);
|
|
@@ -292,7 +368,7 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
292
368
|
}
|
|
293
369
|
}
|
|
294
370
|
|
|
295
|
-
return
|
|
371
|
+
return propVal;
|
|
296
372
|
},
|
|
297
373
|
set(obj, prop, value) {
|
|
298
374
|
if (typeof prop == "symbol") {
|
|
@@ -301,6 +377,17 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
301
377
|
}
|
|
302
378
|
|
|
303
379
|
let oldValue = obj[prop];
|
|
380
|
+
|
|
381
|
+
// mark as "evicted" in case a proxy child object/array is being replaced
|
|
382
|
+
if (oldValue?.[parentSym]) {
|
|
383
|
+
oldValue[evictedSym] = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// update the stored parent reference in case of index change (e.g. unshift)
|
|
387
|
+
if (value?.[parentSym] && Array.isArray(obj) && !isNaN(prop)) {
|
|
388
|
+
value[parentSym][1] = prop;
|
|
389
|
+
}
|
|
390
|
+
|
|
304
391
|
obj[prop] = value;
|
|
305
392
|
|
|
306
393
|
// trigger only on value change
|
|
@@ -316,18 +403,22 @@ function createProxy(obj, pathWatcherIds) {
|
|
|
316
403
|
callWatchers(obj, prop, pathWatcherIds);
|
|
317
404
|
|
|
318
405
|
let currentPath = getPath(obj, prop);
|
|
319
|
-
|
|
320
|
-
|
|
406
|
+
|
|
407
|
+
for (const item of pathWatcherIds) {
|
|
408
|
+
if (
|
|
409
|
+
// exact match
|
|
410
|
+
item[0] == currentPath ||
|
|
411
|
+
// child path
|
|
412
|
+
item[0].startsWith(currentPath + pathSeparator)
|
|
413
|
+
) {
|
|
414
|
+
pathWatcherIds.delete(item[0]);
|
|
415
|
+
}
|
|
321
416
|
}
|
|
322
417
|
}
|
|
323
418
|
|
|
324
|
-
delete obj[prop];
|
|
325
|
-
|
|
326
|
-
return true;
|
|
419
|
+
return delete obj[prop];
|
|
327
420
|
},
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
return new Proxy(obj, handler);
|
|
421
|
+
});
|
|
331
422
|
}
|
|
332
423
|
|
|
333
424
|
function getPath(obj, prop) {
|
|
@@ -335,7 +426,7 @@ function getPath(obj, prop) {
|
|
|
335
426
|
|
|
336
427
|
let parentData = obj?.[parentSym];
|
|
337
428
|
while (parentData) {
|
|
338
|
-
currentPath = parentData[1] +
|
|
429
|
+
currentPath = parentData[1] + pathSeparator + currentPath;
|
|
339
430
|
parentData = parentData[0][parentSym];
|
|
340
431
|
}
|
|
341
432
|
|
package/src/template.js
CHANGED
|
@@ -97,7 +97,9 @@ function tag(tagName, attrs = {}, ...children) {
|
|
|
97
97
|
attr = attr.substring(5);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
if (
|
|
100
|
+
if (typeof val === "undefined") {
|
|
101
|
+
el.removeAttribute(attr);
|
|
102
|
+
} else if (
|
|
101
103
|
// JS property or regular HTML attribute
|
|
102
104
|
typeof val != "function" ||
|
|
103
105
|
// event
|
|
@@ -192,6 +194,8 @@ function setChildren(el, children) {
|
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
// Note: Direct nested reactive functions or direct nested arrays are not supported,
|
|
198
|
+
// aka. childrenFunc must return a single element or plain array of elements.
|
|
195
199
|
function initChildrenFuncWatcher(el, childrenFunc) {
|
|
196
200
|
let endPlaceholder = document.createComment("");
|
|
197
201
|
el.appendChild(endPlaceholder);
|
|
@@ -398,7 +402,11 @@ function toArray(val) {
|
|
|
398
402
|
|
|
399
403
|
function normalizeNode(child) {
|
|
400
404
|
// wrap as TextNode so that it can be "tracked" and used with appendChild or other similar methods
|
|
401
|
-
if (
|
|
405
|
+
if (
|
|
406
|
+
typeof child == "string" ||
|
|
407
|
+
typeof child == "number" ||
|
|
408
|
+
typeof child == "boolean"
|
|
409
|
+
) {
|
|
402
410
|
let childNode = document.createTextNode(child);
|
|
403
411
|
childNode.rid = child;
|
|
404
412
|
return childNode;
|
|
@@ -406,7 +414,7 @@ function normalizeNode(child) {
|
|
|
406
414
|
|
|
407
415
|
// in case child is DOM Proxy element/array loaded from a store object
|
|
408
416
|
if (typeof child?.__raw != "undefined") {
|
|
409
|
-
return child.__raw
|
|
417
|
+
return child.__raw;
|
|
410
418
|
}
|
|
411
419
|
|
|
412
420
|
return child;
|