mancha 0.7.3 → 0.8.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/dist/plugins.js CHANGED
@@ -1,6 +1,7 @@
1
- import { appendChild, attributeNameToCamelCase, cloneAttribute, createElement, firstElementChild, getAttribute, getNodeValue, insertBefore, removeAttribute, removeChild, replaceChildren, replaceWith, setAttribute, setNodeValue, setTextContent, traverse, } from "./dome.js";
1
+ import { appendChild, attributeNameToCamelCase, cloneAttribute, createElement, ellipsize, firstElementChild, getAttribute, getNodeValue, insertBefore, nodeToString, removeAttribute, removeChild, replaceChildren, replaceWith, setAttribute, setNodeValue, setTextContent, traverse, } from "./dome.js";
2
2
  import { isRelativePath } from "./core.js";
3
3
  import { Iterator } from "./iterator.js";
4
+ import { makeAsyncEvalFunction } from "./store.js";
4
5
  const KW_ATTRIBUTES = new Set([
5
6
  ":bind",
6
7
  ":bind-events",
@@ -18,7 +19,7 @@ export var RendererPlugins;
18
19
  // Early exit: node must be an <include> element.
19
20
  if (elem.tagName?.toLocaleLowerCase() !== "include")
20
21
  return;
21
- this.log("<include> tag found in:\n", node);
22
+ this.log("<include> tag found in:\n", nodeToString(node, 128));
22
23
  this.log("<include> params:", params);
23
24
  // Early exit: <include> tags must have a src attribute.
24
25
  const src = getAttribute(elem, "src");
@@ -129,7 +130,7 @@ export var RendererPlugins;
129
130
  if (elem.tagName?.toLowerCase() === "template" && getAttribute(elem, "is")) {
130
131
  const tagName = getAttribute(elem, "is")?.toLowerCase();
131
132
  if (!this._customElements.has(tagName)) {
132
- this.log(`Registering custom element: ${tagName}\n`, elem);
133
+ this.log(`Registering custom element: ${tagName}\n`, nodeToString(elem, 128));
133
134
  this._customElements.set(tagName, elem.cloneNode(true));
134
135
  // Remove the node from the DOM.
135
136
  removeChild(elem.parentNode, elem);
@@ -140,7 +141,7 @@ export var RendererPlugins;
140
141
  const elem = node;
141
142
  const tagName = elem.tagName?.toLowerCase();
142
143
  if (this._customElements.has(tagName)) {
143
- this.log(`Processing custom element: ${tagName}\n`, elem);
144
+ this.log(`Processing custom element: ${tagName}\n`, nodeToString(elem, 128));
144
145
  const template = this._customElements.get(tagName);
145
146
  const clone = (template.content || template).cloneNode(true);
146
147
  // Add whatever attributes the custom element tag had to the first child.
@@ -159,25 +160,23 @@ export var RendererPlugins;
159
160
  }
160
161
  };
161
162
  RendererPlugins.resolveTextNodeExpressions = async function (node, params) {
162
- if (node.nodeType !== 3)
163
- return;
164
163
  const content = getNodeValue(node) || "";
165
- this.log(`Processing node content value:\n`, content);
164
+ if (node.nodeType !== 3 || !content?.trim())
165
+ return;
166
+ this.log(`Processing node content value:\n`, ellipsize(content, 128));
166
167
  // Identify all the expressions found in the content.
167
168
  const matcher = new RegExp(/{{ ([^}]+) }}/gm);
168
169
  const expressions = Array.from(content.matchAll(matcher)).map((match) => match[1]);
169
170
  // To update the node, we have to re-evaluate all of the expressions since that's much simpler
170
171
  // than caching results.
171
- const updateNode = async () => {
172
+ return this.effect(function () {
172
173
  let updatedContent = content;
173
174
  for (const expr of expressions) {
174
- const [result] = await this.eval(expr, { $elem: node });
175
+ const result = this.eval(expr, { $elem: node });
175
176
  updatedContent = updatedContent.replace(`{{ ${expr} }}`, String(result));
176
177
  }
177
178
  setNodeValue(node, updatedContent);
178
- };
179
- // Trigger the eval and pass our full node update function as the callback.
180
- await Promise.all(expressions.map((expr) => this.watchExpr(expr, { $elem: node }, updateNode)));
179
+ });
181
180
  };
182
181
  RendererPlugins.resolveDataAttribute = async function (node, params) {
183
182
  if (this._skipNodes.has(node))
@@ -185,26 +184,24 @@ export var RendererPlugins;
185
184
  const elem = node;
186
185
  const dataAttr = getAttribute(elem, ":data");
187
186
  if (dataAttr) {
188
- this.log(":data attribute found in:\n", node);
187
+ this.log(":data attribute found in:\n", nodeToString(node, 128));
189
188
  // Remove the attribute from the node.
190
189
  removeAttribute(elem, ":data");
191
190
  // Create a subrenderer and process the tag, unless it's the root node.
192
- if (params?.rootNode === node) {
193
- const [result] = await this.eval(dataAttr, { $elem: node });
194
- await this.update(result);
195
- }
196
- else {
197
- const subrenderer = this.clone();
198
- node.renderer = subrenderer;
199
- const [result] = await subrenderer.eval(dataAttr, { $elem: node });
200
- await subrenderer.update(result);
201
- // Skip all the children of the current node.
191
+ const subrenderer = params?.rootNode === node ? this : this.clone();
192
+ node.renderer = subrenderer;
193
+ // Do not call eval() directly, we will use an async version instead.
194
+ const fn = makeAsyncEvalFunction(dataAttr, this.evalkeys);
195
+ const result = await fn.call(subrenderer.$, { $elem: node });
196
+ await Promise.all(Object.entries(result).map(([key, value]) => subrenderer.set(key, value)));
197
+ // Skip all the children of the current node, if it's a subrenderer.
198
+ if (subrenderer !== this) {
202
199
  for (const child of traverse(node, this._skipNodes)) {
203
200
  this._skipNodes.add(child);
204
201
  }
205
- // Mount the current node with the subrenderer.
206
- await subrenderer.mount(node, params);
207
202
  }
203
+ // Mount the current node with the subrenderer.
204
+ await subrenderer.mount(node, params);
208
205
  }
209
206
  };
210
207
  RendererPlugins.resolveWatchAttribute = async function (node, params) {
@@ -213,11 +210,13 @@ export var RendererPlugins;
213
210
  const elem = node;
214
211
  const watchAttr = getAttribute(elem, "@watch");
215
212
  if (watchAttr) {
216
- this.log("@watch attribute found in:\n", node);
213
+ this.log("@watch attribute found in:\n", nodeToString(node, 128));
217
214
  // Remove the attribute from the node.
218
215
  removeAttribute(elem, "@watch");
219
216
  // Compute the function's result.
220
- await this.watchExpr(watchAttr, { $elem: node }, () => { });
217
+ await this.effect(function () {
218
+ return this.eval(watchAttr, { $elem: node });
219
+ });
221
220
  }
222
221
  };
223
222
  RendererPlugins.resolveTextAttributes = async function (node, params) {
@@ -226,11 +225,13 @@ export var RendererPlugins;
226
225
  const elem = node;
227
226
  const textAttr = getAttribute(elem, "$text");
228
227
  if (textAttr) {
229
- this.log("$text attribute found in:\n", node);
228
+ this.log("$text attribute found in:\n", nodeToString(node, 128));
230
229
  // Remove the attribute from the node.
231
230
  removeAttribute(elem, "$text");
232
231
  // Compute the function's result and track dependencies.
233
- await this.watchExpr(textAttr, { $elem: node }, (result) => setTextContent(node, result));
232
+ return this.effect(function () {
233
+ setTextContent(node, this.eval(textAttr, { $elem: node }));
234
+ });
234
235
  }
235
236
  };
236
237
  RendererPlugins.resolveHtmlAttribute = async function (node, params) {
@@ -239,16 +240,18 @@ export var RendererPlugins;
239
240
  const elem = node;
240
241
  const htmlAttr = getAttribute(elem, "$html");
241
242
  if (htmlAttr) {
242
- this.log("$html attribute found in:\n", node);
243
+ this.log("$html attribute found in:\n", nodeToString(node, 128));
243
244
  // Remove the attribute from the node.
244
245
  removeAttribute(elem, "$html");
245
- // Obtain a subrenderer for the node contents.
246
- const subrenderer = this.clone();
247
246
  // Compute the function's result and track dependencies.
248
- await this.watchExpr(htmlAttr, { $elem: node }, async (result) => {
249
- const fragment = await subrenderer.preprocessString(result, params);
250
- await subrenderer.renderNode(fragment, params);
251
- replaceChildren(elem, fragment);
247
+ return this.effect(function () {
248
+ const result = this.eval(htmlAttr, { $elem: node });
249
+ return new Promise(async (resolve) => {
250
+ const fragment = await this.preprocessString(result, params);
251
+ await this.renderNode(fragment);
252
+ replaceChildren(elem, fragment);
253
+ resolve();
254
+ });
252
255
  });
253
256
  }
254
257
  };
@@ -258,12 +261,14 @@ export var RendererPlugins;
258
261
  const elem = node;
259
262
  for (const attr of Array.from(elem.attributes || [])) {
260
263
  if (attr.name.startsWith("$") && !KW_ATTRIBUTES.has(attr.name)) {
261
- this.log(attr.name, "attribute found in:\n", node);
264
+ this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
262
265
  // Remove the attribute from the node.
263
266
  removeAttribute(elem, attr.name);
264
267
  // Compute the function's result and track dependencies.
265
268
  const propName = attributeNameToCamelCase(attr.name.slice(1));
266
- await this.watchExpr(attr.value, { $elem: node }, (result) => (node[propName] = result));
269
+ await this.effect(function () {
270
+ node[propName] = this.eval(attr.value, { $elem: node });
271
+ });
267
272
  }
268
273
  }
269
274
  };
@@ -273,12 +278,14 @@ export var RendererPlugins;
273
278
  const elem = node;
274
279
  for (const attr of Array.from(elem.attributes || [])) {
275
280
  if (attr.name.startsWith(":") && !KW_ATTRIBUTES.has(attr.name)) {
276
- this.log(attr.name, "attribute found in:\n", node);
281
+ this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
277
282
  // Remove the processed attributes from node.
278
283
  removeAttribute(elem, attr.name);
279
284
  // Compute the function's result and track dependencies.
280
285
  const attrName = attr.name.slice(1);
281
- await this.watchExpr(attr.value, { $elem: node }, (result) => setAttribute(elem, attrName, result));
286
+ this.effect(function () {
287
+ setAttribute(elem, attrName, this.eval(attr.value, { $elem: node }));
288
+ });
282
289
  }
283
290
  }
284
291
  };
@@ -288,7 +295,7 @@ export var RendererPlugins;
288
295
  const elem = node;
289
296
  for (const attr of Array.from(elem.attributes || [])) {
290
297
  if (attr.name.startsWith("@") && !KW_ATTRIBUTES.has(attr.name)) {
291
- this.log(attr.name, "attribute found in:\n", node);
298
+ this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
292
299
  // Remove the processed attributes from node.
293
300
  removeAttribute(elem, attr.name);
294
301
  node.addEventListener?.(attr.name.substring(1), (event) => {
@@ -303,7 +310,7 @@ export var RendererPlugins;
303
310
  const elem = node;
304
311
  const forAttr = getAttribute(elem, ":for")?.trim();
305
312
  if (forAttr) {
306
- this.log(":for attribute found in:\n", node);
313
+ this.log(":for attribute found in:\n", nodeToString(node, 128));
307
314
  // Remove the processed attributes from node.
308
315
  removeAttribute(elem, ":for");
309
316
  // Ensure the node and its children are not processed by subsequent steps.
@@ -316,7 +323,7 @@ export var RendererPlugins;
316
323
  insertBefore(parent, template, node);
317
324
  removeChild(parent, node);
318
325
  appendChild(template, node);
319
- this.log(":for template:\n", template);
326
+ this.log(":for template:\n", nodeToString(template, 128));
320
327
  // Tokenize the input by splitting it based on the format "{key} in {expression}".
321
328
  const tokens = forAttr.split(" in ", 2);
322
329
  if (tokens.length !== 2) {
@@ -326,51 +333,41 @@ export var RendererPlugins;
326
333
  const children = [];
327
334
  // Compute the container expression and track dependencies.
328
335
  const [loopKey, itemsExpr] = tokens;
329
- await this.watchExpr(itemsExpr, { $elem: node }, (items) => {
336
+ await this.effect(function () {
337
+ const items = this.eval(itemsExpr, { $elem: node });
330
338
  this.log(":for list items:", items);
331
- // Acquire the lock atomically.
332
- this.lock = this.lock
333
- .then(() => new Promise(async (resolve) => {
334
- // Remove all the previously added children, if any.
335
- children.splice(0, children.length).forEach((child) => {
336
- removeChild(parent, child);
337
- this._skipNodes.delete(child);
338
- });
339
- // Validate that the expression returns a list of items.
340
- if (!Array.isArray(items)) {
341
- console.error(`Expression did not yield a list: \`${itemsExpr}\` => \`${items}\``);
342
- return resolve();
343
- }
344
- // Loop through the container items.
345
- for (const item of items) {
346
- // Create a subrenderer that will hold the loop item and all node descendants.
347
- const subrenderer = this.clone();
348
- await subrenderer.set(loopKey, item);
349
- // Create a new HTML element for each item and add them to parent node.
350
- const copy = node.cloneNode(true);
351
- // Also add the new element to the store.
352
- children.push(copy);
353
- // Since the element will be handled by a subrenderer, skip it in parent renderer.
354
- this._skipNodes.add(copy);
355
- // Render the element using the subrenderer.
356
- await subrenderer.mount(copy, params);
357
- this.log("Rendered list child:\n", copy, copy.outerHTML);
358
- }
359
- // Insert the new children into the parent container.
360
- const reference = template.nextSibling;
361
- for (const child of children) {
362
- insertBefore(parent, child, reference);
363
- }
364
- // Release the lock.
365
- resolve();
366
- }))
367
- .catch((exc) => {
368
- console.error(exc);
369
- throw new Error(exc);
370
- })
371
- .then();
372
- // Return the lock so the whole operation can be awaited.
373
- return this.lock;
339
+ // Remove all the previously added children, if any.
340
+ children.splice(0, children.length).forEach((child) => {
341
+ removeChild(parent, child);
342
+ this._skipNodes.delete(child);
343
+ });
344
+ // Validate that the expression returns a list of items.
345
+ if (!Array.isArray(items)) {
346
+ console.error(`Expression did not yield a list: \`${itemsExpr}\` => \`${items}\``);
347
+ return Promise.resolve();
348
+ }
349
+ // Loop through the container items.
350
+ const awaiters = [];
351
+ for (const item of items) {
352
+ // Create a subrenderer that will hold the loop item and all node descendants.
353
+ const subrenderer = this.clone();
354
+ subrenderer.set(loopKey, item);
355
+ // Create a new HTML element for each item and add them to parent node.
356
+ const copy = node.cloneNode(true);
357
+ // Also add the new element to the store.
358
+ children.push(copy);
359
+ // Since the element will be handled by a subrenderer, skip it in parent renderer.
360
+ this._skipNodes.add(copy);
361
+ // Render the element using the subrenderer.
362
+ awaiters.push(subrenderer.mount(copy, params));
363
+ this.log("Rendered list child:\n", nodeToString(copy, 128));
364
+ }
365
+ // Insert the new children into the parent container.
366
+ const reference = template.nextSibling;
367
+ for (const child of children) {
368
+ insertBefore(parent, child, reference);
369
+ }
370
+ return Promise.all(awaiters);
374
371
  });
375
372
  }
376
373
  };
@@ -380,7 +377,7 @@ export var RendererPlugins;
380
377
  const elem = node;
381
378
  const bindExpr = getAttribute(elem, ":bind");
382
379
  if (bindExpr) {
383
- this.log(":bind attribute found in:\n", node);
380
+ this.log(":bind attribute found in:\n", nodeToString(node, 128));
384
381
  // The change events we listen for can be overriden by user.
385
382
  const defaultEvents = ["change", "input"];
386
383
  const updateEvents = getAttribute(elem, ":bind-events")?.split(",") || defaultEvents;
@@ -391,7 +388,10 @@ export var RendererPlugins;
391
388
  const prop = getAttribute(elem, "type") === "checkbox" ? "checked" : "value";
392
389
  // Watch for updates in the store and bind our property ==> node value.
393
390
  const propExpr = `$elem.${prop} = ${bindExpr}`;
394
- await this.watchExpr(propExpr, { $elem: node }, (result) => (elem[prop] = result));
391
+ this.effect(function () {
392
+ const result = this.eval(propExpr, { $elem: node });
393
+ elem[prop] = result;
394
+ });
395
395
  // Bind node value ==> our property.
396
396
  const nodeExpr = `${bindExpr} = $elem.${prop}`;
397
397
  // Watch for updates in the node's value.
@@ -406,7 +406,7 @@ export var RendererPlugins;
406
406
  const elem = node;
407
407
  const showExpr = getAttribute(elem, ":show");
408
408
  if (showExpr) {
409
- this.log(":show attribute found in:\n", node);
409
+ this.log(":show attribute found in:\n", nodeToString(node, 128));
410
410
  // Remove the processed attributes from node.
411
411
  removeAttribute(elem, ":show");
412
412
  // TODO: Instead of using element display, insert a dummy <template> to track position of
@@ -422,7 +422,8 @@ export var RendererPlugins;
422
422
  ?.at(1)
423
423
  ?.trim();
424
424
  // Compute the function's result and track dependencies.
425
- await this.watchExpr(showExpr, { $elem: node }, (result) => {
425
+ this.effect(function () {
426
+ const result = this.eval(showExpr, { $elem: node });
426
427
  // If the result is false, set the node's display to none.
427
428
  if (elem.style)
428
429
  elem.style.display = result ? display : "none";
@@ -0,0 +1,50 @@
1
+ type SignalStoreProxy = SignalStore & {
2
+ [key: string]: any;
3
+ };
4
+ type Observer<T> = (this: SignalStoreProxy) => T;
5
+ declare abstract class IDebouncer {
6
+ timeouts: Map<Function, ReturnType<typeof setTimeout>>;
7
+ debounce<T>(millis: number, callback: () => T | Promise<T>): Promise<T>;
8
+ }
9
+ /** Default debouncer time in millis. */
10
+ export declare const REACTIVE_DEBOUNCE_MILLIS = 10;
11
+ /**
12
+ * Creates an evaluation function based on the provided code and arguments.
13
+ * @param code The code to be evaluated.
14
+ * @param args The arguments to be passed to the evaluation function. Default is an empty array.
15
+ * @returns The evaluation function.
16
+ */
17
+ export declare function makeEvalFunction(code: string, args?: string[]): Function;
18
+ export declare function makeAsyncEvalFunction(code: string, args?: string[]): Function;
19
+ export declare class SignalStore extends IDebouncer {
20
+ protected readonly evalkeys: string[];
21
+ protected readonly expressionCache: Map<string, Function>;
22
+ protected readonly store: Map<string, unknown>;
23
+ protected readonly observers: Map<string, Set<Observer<unknown>>>;
24
+ protected _observer: Observer<unknown> | null;
25
+ _lock: Promise<void>;
26
+ constructor(data?: {
27
+ [key: string]: any;
28
+ });
29
+ private wrapFunction;
30
+ private wrapObject;
31
+ private watch;
32
+ private notify;
33
+ get<T>(key: string, observer?: Observer<T>): unknown | null;
34
+ set(key: string, value: unknown): Promise<void>;
35
+ del(key: string): void;
36
+ has(key: string): boolean;
37
+ effect<T>(observer: Observer<T>): T;
38
+ private proxify;
39
+ get $(): SignalStoreProxy;
40
+ /**
41
+ * Retrieves or creates a cached expression function based on the provided expression.
42
+ * @param expr - The expression to retrieve or create a cached function for.
43
+ * @returns The cached expression function.
44
+ */
45
+ private cachedExpressionFunction;
46
+ eval(expr: string, args?: {
47
+ [key: string]: any;
48
+ }): unknown;
49
+ }
50
+ export {};
package/dist/store.js ADDED
@@ -0,0 +1,182 @@
1
+ class IDebouncer {
2
+ timeouts = new Map();
3
+ debounce(millis, callback) {
4
+ return new Promise((resolve, reject) => {
5
+ const timeout = this.timeouts.get(callback);
6
+ if (timeout)
7
+ clearTimeout(timeout);
8
+ this.timeouts.set(callback, setTimeout(() => {
9
+ try {
10
+ resolve(callback());
11
+ this.timeouts.delete(callback);
12
+ }
13
+ catch (exc) {
14
+ reject(exc);
15
+ }
16
+ }, millis));
17
+ });
18
+ }
19
+ }
20
+ /** Default debouncer time in millis. */
21
+ export const REACTIVE_DEBOUNCE_MILLIS = 10;
22
+ /**
23
+ * Creates an evaluation function based on the provided code and arguments.
24
+ * @param code The code to be evaluated.
25
+ * @param args The arguments to be passed to the evaluation function. Default is an empty array.
26
+ * @returns The evaluation function.
27
+ */
28
+ export function makeEvalFunction(code, args = []) {
29
+ return new Function(...args, `with (this) { return (${code}); }`);
30
+ }
31
+ export function makeAsyncEvalFunction(code, args = []) {
32
+ return new Function(...args, `with (this) { return (async () => (${code}))(); }`);
33
+ }
34
+ function isProxified(object) {
35
+ return object instanceof SignalStore || object["__is_proxy__"];
36
+ }
37
+ export class SignalStore extends IDebouncer {
38
+ evalkeys = ["$elem", "$event"];
39
+ expressionCache = new Map();
40
+ store = new Map();
41
+ observers = new Map();
42
+ _observer = null;
43
+ _lock = Promise.resolve();
44
+ constructor(data) {
45
+ super();
46
+ for (let [key, value] of Object.entries(data || {})) {
47
+ // Use our set method to ensure that callbacks and wrappers are appropriately set, but ignore
48
+ // the return value since we know that no observers will be triggered.
49
+ this.set(key, value);
50
+ }
51
+ }
52
+ wrapFunction(fn) {
53
+ return (...args) => fn.call(this.$, ...args);
54
+ }
55
+ wrapObject(obj, callback) {
56
+ // If this object is already a proxy or not a plain object (or array), return it as-is.
57
+ if (obj == null || isProxified(obj) || (obj.constructor !== Object && !Array.isArray(obj))) {
58
+ return obj;
59
+ }
60
+ return new Proxy(obj, {
61
+ deleteProperty: (target, property) => {
62
+ if (property in target) {
63
+ delete target[property];
64
+ callback();
65
+ return true;
66
+ }
67
+ else {
68
+ return false;
69
+ }
70
+ },
71
+ set: (target, prop, value, receiver) => {
72
+ if (typeof value === "object" && obj != null)
73
+ value = this.wrapObject(value, callback);
74
+ const ret = Reflect.set(target, prop, value, receiver);
75
+ callback();
76
+ return ret;
77
+ },
78
+ get: (target, prop, receiver) => {
79
+ if (prop === "__is_proxy__")
80
+ return true;
81
+ return Reflect.get(target, prop, receiver);
82
+ },
83
+ });
84
+ }
85
+ watch(key, observer) {
86
+ if (!this.observers.has(key)) {
87
+ this.observers.set(key, new Set());
88
+ }
89
+ if (!this.observers.get(key)?.has(observer)) {
90
+ this.observers.get(key)?.add(observer);
91
+ }
92
+ }
93
+ async notify(key) {
94
+ const observers = Array.from(this.observers.get(key) || []);
95
+ await this.debounce(REACTIVE_DEBOUNCE_MILLIS, () => Promise.all(observers.map((observer) => observer.call(this.proxify(observer)))));
96
+ }
97
+ get(key, observer) {
98
+ if (observer)
99
+ this.watch(key, observer);
100
+ return this.store.get(key);
101
+ }
102
+ async set(key, value) {
103
+ if (value === this.store.get(key))
104
+ return;
105
+ const callback = () => this.notify(key);
106
+ if (value && typeof value === "function") {
107
+ value = this.wrapFunction(value);
108
+ }
109
+ if (value && typeof value === "object") {
110
+ value = this.wrapObject(value, callback);
111
+ }
112
+ this.store.set(key, value);
113
+ await callback();
114
+ }
115
+ del(key) {
116
+ this.store.delete(key);
117
+ this.observers.delete(key);
118
+ }
119
+ has(key) {
120
+ return this.store.has(key);
121
+ }
122
+ effect(observer) {
123
+ return observer.call(this.proxify(observer));
124
+ }
125
+ proxify(observer) {
126
+ const keys = Array.from(this.store.entries()).map(([key]) => key);
127
+ const keyval = Object.fromEntries(keys.map((key) => [key, undefined]));
128
+ return new Proxy(keyval, {
129
+ get: (_, prop, receiver) => {
130
+ if (typeof prop === "string" && this.store.has(prop)) {
131
+ return this.get(prop, observer);
132
+ }
133
+ else if (prop === "$") {
134
+ return this.proxify(observer);
135
+ }
136
+ else {
137
+ return Reflect.get(this, prop, receiver);
138
+ }
139
+ },
140
+ set: (_, prop, value, receiver) => {
141
+ if (typeof prop !== "string" || prop in this) {
142
+ Reflect.set(this, prop, value, receiver);
143
+ }
144
+ else {
145
+ this.set(prop, value);
146
+ }
147
+ return true;
148
+ },
149
+ });
150
+ }
151
+ get $() {
152
+ return this.proxify();
153
+ }
154
+ /**
155
+ * Retrieves or creates a cached expression function based on the provided expression.
156
+ * @param expr - The expression to retrieve or create a cached function for.
157
+ * @returns The cached expression function.
158
+ */
159
+ cachedExpressionFunction(expr) {
160
+ if (!this.expressionCache.has(expr)) {
161
+ this.expressionCache.set(expr, makeEvalFunction(expr, this.evalkeys));
162
+ }
163
+ return this.expressionCache.get(expr);
164
+ }
165
+ eval(expr, args = {}) {
166
+ // Determine whether we have already been proxified to avoid doing it again.
167
+ const thisArg = this._observer ? this : this.$;
168
+ if (this.store.has(expr)) {
169
+ // Shortcut: if the expression is just an item from the value store, use that directly.
170
+ return thisArg[expr];
171
+ }
172
+ else {
173
+ // Otherwise, perform the expression evaluation.
174
+ const fn = this.cachedExpressionFunction(expr);
175
+ const argvals = this.evalkeys.map((key) => args[key]);
176
+ if (Object.keys(args).some((key) => !this.evalkeys.includes(key))) {
177
+ throw new Error(`Invalid argument key, must be one of: ${this.evalkeys.join(", ")}`);
178
+ }
179
+ return fn.call(thisArg, ...argvals);
180
+ }
181
+ }
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mancha",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Javscript HTML rendering engine",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -56,4 +56,4 @@
56
56
  "webpack": "5.91.0",
57
57
  "webpack-cli": "^5.1.4"
58
58
  }
59
- }
59
+ }