@cocreate/utils 1.44.2 → 1.44.3

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.
@@ -1,29 +1,48 @@
1
- import { ObjectId } from "./ObjectId.js";
2
- import { uid } from "./uid.js";
3
- import { queryElements } from "./queryElements.js";
4
- import { getValueFromObject } from "./getValueFromObject.js";
5
-
6
- // --- AST EVALUATION ENGINE (safeParse) ---
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ import { getValueFromObject } from "../getValueFromObject.js";
20
+ import { queryElements } from "../queryElements.js";
21
+ import { ObjectId } from "../ObjectId.js";
22
+ import { uuid } from "../uuid.js";
23
+
24
+ // Late-binding references dynamically set at boot time to decouple circular import loops
25
+ let astSyncRef = null;
26
+ let astAsyncRef = null;
27
+
28
+ export function setASTEvaluators(syncFn, asyncFn) {
29
+ astSyncRef = syncFn;
30
+ astAsyncRef = asyncFn;
31
+ }
7
32
 
8
33
  const mathConstants = { PI: Math.PI, E: Math.E };
9
34
  const mathFunctions = {
10
35
  abs: Math.abs, ceil: Math.ceil, floor: Math.floor, round: Math.round,
11
36
  max: Math.max, min: Math.min, pow: Math.pow, sqrt: Math.sqrt,
12
37
  log: Math.log, sin: Math.sin, cos: Math.cos, tan: Math.tan,
13
- Number: (v) => Number(v) // Explicit numeric casting
38
+ Number: (v) => Number(v)
14
39
  };
15
40
 
16
- /**
17
- * Reference class used by safeParse to track object properties for assignments.
18
- * Prevents dot-notation from just returning values when we need to assign to them (e.g., el.value = 5)
19
- * Upgraded to handle array contexts: if the object is an array, properties map across all items natively.
20
- */
21
41
  class Ref {
22
42
  constructor(obj, prop) {
23
43
  this.obj = obj;
24
44
  this.prop = prop;
25
45
 
26
- // Ensure we recognize standard arrays AND DOM collections (NodeList/HTMLCollection)
27
46
  let isList = Array.isArray(obj) ||
28
47
  (typeof NodeList !== "undefined" && obj instanceof NodeList) ||
29
48
  (typeof HTMLCollection !== "undefined" && obj instanceof HTMLCollection);
@@ -36,12 +55,9 @@ class Ref {
36
55
  }
37
56
  get() {
38
57
  if (this.isArrayContext) {
39
- // Return the raw Array of extracted properties so the user can use .join() or other array methods
40
- let res = this.obj.map(item => item ? item[this.prop] : undefined);
41
- return res;
58
+ return this.obj.map(item => item ? item[this.prop] : undefined);
42
59
  }
43
- let res = this.obj ? this.obj[this.prop] : undefined;
44
- return res;
60
+ return this.obj ? this.obj[this.prop] : undefined;
45
61
  }
46
62
  set(val) {
47
63
  if (this.isArrayContext) {
@@ -57,10 +73,7 @@ function unref(val) {
57
73
  return val instanceof Ref ? val.get() : val;
58
74
  }
59
75
 
60
- /**
61
- * Parses math, logic, ternaries, dot notation, and property assignments securely.
62
- */
63
- function safeParse(expression, registry = new Map()) {
76
+ export function safeParse(expression, registry = new Map()) {
64
77
  if (typeof expression !== "string") return expression;
65
78
 
66
79
  let currentExpr = expression.trim();
@@ -83,7 +96,7 @@ function safeParse(expression, registry = new Map()) {
83
96
  consume();
84
97
  let right = unref(parseAssignment());
85
98
  if (left instanceof Ref) {
86
- return left.set(right); // Assign value to the actual object property (or all properties in an array)
99
+ return left.set(right);
87
100
  }
88
101
  return right;
89
102
  }
@@ -194,12 +207,11 @@ function safeParse(expression, registry = new Map()) {
194
207
  return mathFunctions[token](...args);
195
208
  }
196
209
 
197
- // --- Context & Object Registry Traversal ---
198
210
  let path = token.split(".");
199
211
  let baseToken = path[0];
200
212
  let val;
201
213
 
202
- if (registry.has(baseToken)) {
214
+ if (registry && typeof registry.get === 'function' && registry.has(baseToken)) {
203
215
  val = registry.get(baseToken);
204
216
  } else if (typeof window !== "undefined" && window[baseToken]) {
205
217
  val = window[baseToken];
@@ -208,7 +220,6 @@ function safeParse(expression, registry = new Map()) {
208
220
  }
209
221
 
210
222
  if (path.length === 1) {
211
- // Support explicit call for root variables
212
223
  if (peek() === "(" && typeof val === "function") {
213
224
  consume();
214
225
  let args = [];
@@ -227,7 +238,6 @@ function safeParse(expression, registry = new Map()) {
227
238
 
228
239
  for (let i = 1; i < path.length - 1; i++) {
229
240
  if (val !== null && val !== undefined) {
230
- // Support deep traversal across arrays
231
241
  let isList = Array.isArray(val) || (typeof NodeList !== "undefined" && val instanceof NodeList) || (typeof HTMLCollection !== "undefined" && val instanceof HTMLCollection);
232
242
  if (isList && !(path[i] in val)) {
233
243
  val = Array.from(val).map(item => item ? item[path[i]] : undefined);
@@ -241,7 +251,6 @@ function safeParse(expression, registry = new Map()) {
241
251
 
242
252
  let ref = new Ref(val, path[path.length - 1]);
243
253
 
244
- // Support explicit method calls (e.g., obj.method(arg))
245
254
  if (peek() === "(") {
246
255
  consume();
247
256
  let args = [];
@@ -254,24 +263,15 @@ function safeParse(expression, registry = new Map()) {
254
263
  }
255
264
  if (peek() === ")") consume();
256
265
 
257
- // Map the method call across all items if it's an array context
258
266
  if (ref.isArrayContext) {
259
- let results = ref.obj.map(item => {
267
+ return ref.obj.map(item => {
260
268
  let func = item ? item[ref.prop] : undefined;
261
- if (func !== undefined && typeof func !== "function") {
262
- console.warn(`Operator Engine: '${ref.prop}' is not a valid function on the target element.`);
263
- }
264
- let res = typeof func === "function" ? func.apply(item, args) : undefined;
265
- return res;
269
+ return typeof func === "function" ? func.apply(item, args) : undefined;
266
270
  });
267
- return results; // Return the natively mapped Array so user can process it!
268
271
  } else {
269
272
  let func = ref.obj ? ref.obj[ref.prop] : undefined;
270
273
  if (typeof func === "function") {
271
- let res = func.apply(ref.obj, args);
272
- return res;
273
- } else {
274
- console.warn(`Operator Engine: Method '${ref.prop}' does not exist on the target object.`);
274
+ return func.apply(ref.obj, args);
275
275
  }
276
276
  }
277
277
  }
@@ -280,45 +280,43 @@ function safeParse(expression, registry = new Map()) {
280
280
  }
281
281
 
282
282
  try {
283
- const result = parse();
284
- return unref(result);
283
+ return unref(parse());
285
284
  } catch (error) {
286
285
  console.warn(`safeParse error: ${error.message} (Expr: "${expression}")`, error);
287
286
  return null;
288
287
  }
289
288
  }
290
289
 
291
-
292
- // --- CORE OPERATOR ENGINE ---
293
-
294
- const customOperators = new Map(
290
+ export const customOperators = new Map(
295
291
  Object.entries({
296
- $organization_id: () => localStorage.getItem("organization_id"),
297
- $user_id: () => localStorage.getItem("user_id"),
298
- $client_id: () => localStorage.getItem("clientId"),
299
- $session_id: () => localStorage.getItem("session_id"),
300
- $this: function() { return this; },
301
- $value: function() { return this && typeof this.getValue === 'function' ? this.getValue() : ""; },
302
- $innerWidth: () => window.innerWidth,
303
- $innerHeight: () => window.innerHeight,
304
- $href: () => window.location.href.replace(/\/$/, ""),
305
- $origin: () => window.location.origin,
306
- $protocol: () => window.location.protocol,
307
- $hostname: () => window.location.hostname,
308
- $host: () => window.location.host,
309
- $port: () => window.location.port,
310
- $pathname: () => window.location.pathname.replace(/\/$/, ""),
311
- $hash: () => window.location.hash,
312
- $subdomain: () => getSubdomain() || "",
292
+ $organization_id: () => typeof localStorage !== 'undefined' ? localStorage.getItem("organization_id") : "",
293
+ $user_id: () => typeof localStorage !== 'undefined' ? localStorage.getItem("user_id") : "",
294
+ $client_id: () => typeof localStorage !== 'undefined' ? localStorage.getItem("clientId") : "",
295
+ $session_id: () => typeof localStorage !== 'undefined' ? localStorage.getItem("session_id") : "",
296
+ $this: function() { return this.element; },
297
+ $value: function() { return this.element && typeof this.element.getValue === 'function' ? this.element.getValue() : ""; },
298
+ $innerWidth: () => typeof window !== 'undefined' ? window.innerWidth : 0,
299
+ $innerHeight: () => typeof window !== 'undefined' ? window.innerHeight : 0,
300
+ $href: () => typeof window !== 'undefined' ? window.location.href.replace(/\/$/, "") : "",
301
+ $origin: () => typeof window !== 'undefined' ? window.location.origin : "",
302
+ $protocol: () => typeof window !== 'undefined' ? window.location.protocol : "",
303
+ $hostname: () => typeof window !== 'undefined' ? window.location.hostname : "",
304
+ $host: () => typeof window !== 'undefined' ? window.location.host : "",
305
+ $port: () => typeof window !== 'undefined' ? window.location.port : "",
306
+ $pathname: () => typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, "") : "",
307
+ $hash: () => typeof window !== 'undefined' ? window.location.hash : "",
308
+ $subdomain: () => {
309
+ if (typeof window === 'undefined') return "";
310
+ const parts = window.location.hostname.split('.');
311
+ return parts.length > 2 ? parts[0] : "";
312
+ },
313
313
  $object_id: () => ObjectId().toString(),
314
314
  "ObjectId()": () => ObjectId().toString(),
315
315
 
316
- // Unwrap query results correctly: Returns only the single first item
317
316
  $query: function(selector) {
318
- let element = this;
317
+ let element = this.element;
319
318
  if (typeof selector === 'string') {
320
319
  selector = selector.trim();
321
- // Strip quotes out of the selector in case they were passed (e.g., $query('#id1'))
322
320
  if ((selector.startsWith("'") && selector.endsWith("'")) ||
323
321
  (selector.startsWith('"') && selector.endsWith('"'))) {
324
322
  selector = selector.slice(1, -1);
@@ -327,43 +325,67 @@ const customOperators = new Map(
327
325
  try {
328
326
  let results = queryElements({ element, selector });
329
327
  if (!results || results.length === 0) return undefined;
330
- return results[0]; // Strict single item return
328
+ return results[0];
331
329
  } catch (error) {
332
330
  console.warn(`Operator Engine: Invalid $query selector => "${selector}"`, error);
333
331
  return undefined;
334
332
  }
335
333
  },
336
- // Unwrap query results correctly: Always returns an array
337
334
  $queryAll: function(selector) {
338
- let element = this;
335
+ let element = this.element;
339
336
  if (typeof selector === 'string') {
340
337
  selector = selector.trim();
341
- // Strip quotes out of the selector in case they were passed (e.g., $queryAll('.items'))
342
338
  if ((selector.startsWith("'") && selector.endsWith("'")) ||
343
339
  (selector.startsWith('"') && selector.endsWith('"'))) {
344
340
  selector = selector.slice(1, -1);
345
341
  }
346
-
347
- // Safely ignore queries targeting $document to prevent querySelector crashes
348
342
  if (selector.includes('$document')) return [];
349
343
  }
350
344
  try {
351
345
  let results = queryElements({ element, selector });
352
346
  if (!results || results.length === 0) return [];
353
- // Always cast to array so Ref mapping handles methods uniformly
354
347
  return Array.from(results);
355
348
  } catch (error) {
356
349
  console.warn(`Operator Engine: Invalid $queryAll selector => "${selector}"`, error);
357
350
  return [];
358
351
  }
359
352
  },
360
- $eval: function(expression) { return safeParse(expression); },
353
+ $eval: function(expression) {
354
+ return safeParse(expression, this.registry);
355
+ },
356
+ "$": function(expression) {
357
+ return safeParse(expression, this.registry);
358
+ },
359
+ $ast: function(expression) {
360
+ if (!astSyncRef) {
361
+ console.warn("[Operator Engine] AST Evaluator reference not registered.");
362
+ return expression;
363
+ }
364
+ return astSyncRef({
365
+ executeAST: expression,
366
+ node: this.element,
367
+ registry: this.registry
368
+ });
369
+ },
370
+ $astAsync: async function(expression) {
371
+ if (!astAsyncRef) {
372
+ console.warn("[Operator Engine] AST Async Evaluator reference not registered.");
373
+ return expression;
374
+ }
375
+ return await astAsyncRef({
376
+ executeAST: expression,
377
+ node: this.element,
378
+ registry: this.registry
379
+ });
380
+ },
361
381
  $relativePath: () => {
382
+ if (typeof window === 'undefined') return "./";
362
383
  let currentPath = window.location.pathname.replace(/\/[^\/]*$/, "");
363
384
  let depth = currentPath.split("/").filter(Boolean).length;
364
385
  return depth > 0 ? "../".repeat(depth) : "./";
365
386
  },
366
387
  $path: () => {
388
+ if (typeof window === 'undefined') return "";
367
389
  let path = window.location.pathname;
368
390
  if (path.split("/").pop().includes(".")) path = path.replace(/\/[^\/]+$/, "/");
369
391
  return path === "/" ? "" : path;
@@ -373,7 +395,7 @@ const customOperators = new Map(
373
395
  return getValueFromObject(obj, key);
374
396
  },
375
397
  $setValue: function(...args) {
376
- return typeof this.setValue === 'function' ? this.setValue(...args) || "" : "";
398
+ return this.element && typeof this.element.setValue === 'function' ? this.element.setValue(...args) || "" : "";
377
399
  },
378
400
  $true: () => true,
379
401
  $false: () => false,
@@ -389,7 +411,14 @@ const customOperators = new Map(
389
411
  if (isNaN(number)) return String(numCandidate ?? "");
390
412
  return new Intl.NumberFormat(locale, options).format(number);
391
413
  },
392
- $uid: function(prefix) { return uid(prefix) || ""; },
414
+ $uuid: function(prefix) { return uuid(prefix) || ""; },
415
+
416
+ $add: function(value) {
417
+ return Array.isArray(value) ? value.flat().reduce((a, b) => Number(a) + Number(b), 0) : Number(value) || 0;
418
+ },
419
+ $concat: function(value) {
420
+ return Array.isArray(value) ? value.flat().map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join('') : String(value ?? "");
421
+ }
393
422
  })
394
423
  );
395
424
 
@@ -409,7 +438,6 @@ const findInnermostOperator = (expression) => {
409
438
  let stack = [];
410
439
  let inSQ = false, inDQ = false;
411
440
 
412
- // 1. Capture all parenthesis pairs accurately (The Parenthesis-First "Animals" boundary)
413
441
  for (let i = 0; i < expression.length; i++) {
414
442
  const char = expression[i];
415
443
  if (char === '\\') { i++; continue; }
@@ -427,7 +455,6 @@ const findInnermostOperator = (expression) => {
427
455
  }
428
456
  }
429
457
 
430
- // Sort by depth descending (innermost first), then by start descending (right-most first)
431
458
  parens.sort((a, b) => b.depth - a.depth || b.start - a.start);
432
459
 
433
460
  function stripParentheses(str) {
@@ -447,35 +474,29 @@ const findInnermostOperator = (expression) => {
447
474
  return result;
448
475
  }
449
476
 
450
- // 2. Look to the left of the innermost parentheses to find the keyword operator
451
477
  for (const p of parens) {
452
- // We safely isolate ONLY what is inside the parens, bypassing the need for string replacements
453
478
  const insideParens = expression.substring(p.start + 1, p.end).trim();
454
479
  const leftPart = expression.substring(0, p.start);
455
-
456
- // Check for an operator keyword directly attached to the left of the `(`
457
- const match = leftPart.match(/(?:^|[^\w\$])(\$[\w\-]+|ObjectId\b)\s*$/);
480
+
481
+ const match = leftPart.match(/(?:^|[^\w\$])(\$[\w\-]*|ObjectId\b)\s*$/);
458
482
 
459
483
  if (match) {
460
484
  let opName = match[1] === 'ObjectId' ? 'ObjectId()' : match[1];
461
- if (customOperators.has(opName) || /^\$[\w\-]+$/.test(opName)) {
462
- // Pinpoint exactly where the operator string began
485
+ if (customOperators.has(opName) || /^\$[\w\-]*$/.test(opName)) {
463
486
  const spacesMatch = leftPart.match(/\s*$/);
464
487
  const spacesAtEnd = spacesMatch ? spacesMatch[0].length : 0;
465
488
  const fullMatchStart = match.index + match[0].length - match[1].length - spacesAtEnd;
466
489
 
467
- const resultBlock = {
490
+ return {
468
491
  operator: opName,
469
- args: insideParens, // Perfectly isolated arguments, handling nested chains
492
+ args: insideParens,
470
493
  fullMatch: expression.substring(fullMatchStart, p.end + 1),
471
494
  rawContent: expression.substring(fullMatchStart, p.end + 1)
472
495
  };
473
- return resultBlock;
474
496
  }
475
497
  }
476
498
  }
477
499
 
478
- // 3. Fallback: Find bare operators (like $this) that have no parentheses attached
479
500
  const bareRegex = /(?:^|[^\w\$])(\$[\w\-]+|ObjectId\b)/g;
480
501
  let bareMatch;
481
502
  let bareCandidates = [];
@@ -483,8 +504,6 @@ const findInnermostOperator = (expression) => {
483
504
  while ((bareMatch = bareRegex.exec(expression)) !== null) {
484
505
  let possibleOp = bareMatch[1];
485
506
  let opName = possibleOp === 'ObjectId' ? 'ObjectId()' : possibleOp;
486
-
487
- // Ensure it isnt followed by a `(` (because if it was, it would have been caught above)
488
507
  let remainder = expression.substring(bareMatch.index + bareMatch[0].length);
489
508
  if (!/^\s*\(/.test(remainder)) {
490
509
  if (customOperators.has(opName) || /^\$[\w\-]+$/.test(opName)) {
@@ -504,136 +523,11 @@ const findInnermostOperator = (expression) => {
504
523
  return bareCandidates[0];
505
524
  }
506
525
 
507
- const defaultFallback = { operator: null, args: stripParentheses(expression), rawContent: expression.trim() };
508
- return defaultFallback;
526
+ return { operator: null, args: stripParentheses(expression), rawContent: expression.trim() };
509
527
  };
510
528
 
511
- function processOperators(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
512
- if (typeof value !== "string" || (!value.includes("$") && !value.includes("ObjectId()"))) return value;
513
- let processedValue = value, hasPromise = false, unresolvedTokens = new Map();
514
- while (processedValue.includes("$") || processedValue.includes("ObjectId()")) {
515
- const paramMatch = processedValue.match(/^\$\$PARAM_(\d+)\$\$/);
516
- if (paramMatch && Array.isArray(params) && params.length > 0) {
517
- const index = parseInt(paramMatch[1], 10);
518
- if (index < params.length) { processedValue = processedValue.replace(paramMatch[0], params[index]); continue; }
519
- }
520
- const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
521
- if (!operator || (operator === "$param" && !args)) break;
522
- const textToReplace = fullMatch || rawContent;
523
- if (exclude.includes(operator)) {
524
- const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
525
- unresolvedTokens.set(token, textToReplace);
526
- processedValue = processedValue.replace(textToReplace, token);
527
- continue;
528
- }
529
- let resolvedValue = resolveOperator(element, operator, args, parent, params, objectRegistry);
530
- if (resolvedValue === undefined) {
531
- const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
532
- unresolvedTokens.set(token, textToReplace);
533
- processedValue = processedValue.replace(textToReplace, token);
534
- continue;
535
- }
536
- if (resolvedValue instanceof Promise) {
537
- const paramIndex = params.length;
538
- params.push(resolvedValue);
539
- processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
540
- hasPromise = true;
541
- break;
542
- }
543
- if (params.some((p) => p instanceof Promise)) { hasPromise = true; break; }
544
- let replacement = "";
545
- if (operator === "$param") params.push(resolvedValue);
546
- else if (resolvedValue !== null && (typeof resolvedValue === "object" || typeof resolvedValue === "function")) {
547
- const token = `__OBJ_${objectRegistry.size}__`;
548
- objectRegistry.set(token, resolvedValue);
549
- replacement = token;
550
- } else replacement = resolvedValue ?? "";
551
- if (processedValue === textToReplace) { processedValue = replacement; break; }
552
- processedValue = processedValue.replace(textToReplace, replacement);
553
- }
554
-
555
- // Restore unresolved exclusions (Reverse-order replaces ensure perfectly unnested loops)
556
- if (typeof processedValue === "string") {
557
- let keys = Array.from(unresolvedTokens.keys()).reverse();
558
- for (const token of keys) {
559
- processedValue = processedValue.replaceAll(token, unresolvedTokens.get(token));
560
- }
561
-
562
- // Native method and property chaining evaluation
563
- // 1. If the ENTIRE string is a pure method/property chain (e.g. __OBJ_0__.getValue() )
564
- const exactMethodMatch = processedValue.match(/^__OBJ_\d+__(?:\.[a-zA-Z0-9_]+)+(?:\(.*\))?$/);
565
- if (exactMethodMatch) {
566
- const parsed = safeParse(processedValue, objectRegistry);
567
- // DO NOT LEAK tokens to the UI. Fallback to "" instead of the processedValue placeholder if it fails.
568
- return parsed !== null && parsed !== undefined ? parsed : "";
569
- }
570
-
571
- // 2. Unpack pure object variables
572
- const exactMatch = processedValue.match(/^__OBJ_(\d+)__$/);
573
- if (exactMatch && objectRegistry.has(processedValue)) {
574
- processedValue = objectRegistry.get(processedValue);
575
- } else {
576
- // 3. Inline interpolation for methods/properties (e.g., "User clicked __OBJ_0__.getValue()")
577
- const objRegex = /__OBJ_\d+__(?:\.[a-zA-Z0-9_]+)+(?:\([^)]*\))?/g;
578
- if (processedValue.includes("__OBJ_")) {
579
- processedValue = processedValue.replace(objRegex, (match) => {
580
- const parsed = safeParse(match, objectRegistry);
581
- // DO NOT LEAK tokens to the UI. Fallback to "" instead of the match placeholder if it fails.
582
- return parsed !== null && parsed !== undefined ? parsed : "";
583
- });
584
- }
585
- }
586
- }
587
-
588
- if (hasPromise) return { value: processedValue, params, objectRegistry };
589
- return processedValue;
590
- }
591
-
592
- async function processOperatorsAsync(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
593
- let result = processOperators(element, value, exclude, parent, params, objectRegistry);
594
- while (typeof result === "object" && result.params) {
595
- const resolvedParams = await Promise.all(result.params);
596
- result = processOperators(element, result.value, exclude, parent, resolvedParams, result.objectRegistry || objectRegistry);
597
- }
598
- return result;
599
- }
600
-
601
- function resolveOperator(element, operator, args, parent, params, objectRegistry) {
602
- if (params.some((p) => p instanceof Promise)) return "";
603
- if (args && typeof args === "string" && args.includes("$")) {
604
- args = processOperators(element, args, [], operator, params, objectRegistry);
605
- }
606
-
607
- let targetElements = element ? [element] : [];
608
- if (args && typeof args === "string") {
609
- const objMatch = args.match(/^__OBJ_(\d+)__$/);
610
- if (objMatch && objectRegistry.has(args)) {
611
- let registeredVal = objectRegistry.get(args);
612
- // Ensure unrolled array natively
613
- let isList = Array.isArray(registeredVal) || (typeof NodeList !== "undefined" && registeredVal instanceof NodeList) || (typeof HTMLCollection !== "undefined" && registeredVal instanceof HTMLCollection);
614
- targetElements = isList ? Array.from(registeredVal) : [registeredVal];
615
- } else if (!customOperators.has(operator) && (operator === "$query")) {
616
- let qResults = queryElements({ element, selector: args });
617
- if (!qResults) {
618
- targetElements = [];
619
- } else {
620
- let isList = Array.isArray(qResults) || (typeof NodeList !== "undefined" && qResults instanceof NodeList) || (typeof HTMLCollection !== "undefined" && qResults instanceof HTMLCollection);
621
- targetElements = isList ? Array.from(qResults) : [qResults];
622
- }
623
- if (!targetElements.length) return undefined;
624
- }
625
- }
626
-
627
- let value = processValues(targetElements, operator, args, parent, objectRegistry);
628
- if (value && typeof value === "string" && value.includes("$")) {
629
- value = processOperators(element, value, [], parent, params, objectRegistry);
630
- }
631
- return value;
632
- }
633
-
634
529
  function processValues(elements, operator, args, parent, objectRegistry) {
635
530
  let customOp = customOperators.get(operator);
636
-
637
531
  let results = [];
638
532
  let hasValidProperty = customOp ? true : false;
639
533
 
@@ -645,13 +539,11 @@ function processValues(elements, operator, args, parent, objectRegistry) {
645
539
  if (propName in el) {
646
540
  hasValidProperty = true;
647
541
  rawValue = el[propName];
648
- }
649
- else {
542
+ } else {
650
543
  continue;
651
544
  }
652
545
  }
653
546
 
654
- // --- 1. RECONSTRUCT THE "GHOST" STRUCTURE (Tokens -> Actual JS Types) ---
655
547
  let preparedArgs = [];
656
548
  const originalWasArray = Array.isArray(args);
657
549
  const hasArgs = originalWasArray || args !== undefined;
@@ -660,9 +552,8 @@ function processValues(elements, operator, args, parent, objectRegistry) {
660
552
  preparedArgs = Array.from(args);
661
553
  } else if (typeof args === "string") {
662
554
  if (args === "") {
663
- preparedArgs = [""]; // Explicitly retain empty string arguments
555
+ preparedArgs = [""];
664
556
  } else {
665
- // Smart parse for comma-separated arguments respecting strings and brackets
666
557
  let parsedArgs = [];
667
558
  let currentArg = '';
668
559
  let inSQ = false, inDQ = false;
@@ -678,7 +569,6 @@ function processValues(elements, operator, args, parent, objectRegistry) {
678
569
  if (char === "'" && !inDQ) inSQ = !inSQ;
679
570
  if (char === '"' && !inSQ) inDQ = !inDQ;
680
571
 
681
- // Keep track of brackets and parens grouping so we don't accidentally split them inside
682
572
  if (!inSQ && !inDQ) {
683
573
  if (char === '(' || char === '[' || char === '{') depth++;
684
574
  else if (char === ')' || char === ']' || char === '}') depth--;
@@ -693,25 +583,18 @@ function processValues(elements, operator, args, parent, objectRegistry) {
693
583
  }
694
584
  if (currentArg.trim() !== "") parsedArgs.push(currentArg.trim());
695
585
 
696
- // Reconstruct actual JavaScript types
697
586
  preparedArgs = parsedArgs.map(arg => {
698
- // 1. Direct object token match (brings back exactly the array, obj, or variable!)
699
587
  if (/^__OBJ_\d+__$/.test(arg) && objectRegistry && objectRegistry.has(arg)) {
700
588
  return objectRegistry.get(arg);
701
589
  }
702
-
703
- // 2. Numbers
704
590
  if (!isNaN(arg) && arg.trim() !== "") {
705
591
  return Number(arg);
706
592
  }
707
-
708
- // 3. Booleans and Null
709
593
  if (arg === "true") return true;
710
594
  if (arg === "false") return false;
711
595
  if (arg === "null") return null;
712
596
  if (arg === "undefined") return undefined;
713
597
 
714
- // 4. Strings with quotes (strip outer quotes reliably)
715
598
  if ((arg.startsWith("'") && arg.endsWith("'")) || (arg.startsWith('"') && arg.endsWith('"'))) {
716
599
  let unquoted = arg.slice(1, -1);
717
600
  if (unquoted.includes("__OBJ_")) {
@@ -726,7 +609,6 @@ function processValues(elements, operator, args, parent, objectRegistry) {
726
609
  return unquoted;
727
610
  }
728
611
 
729
- // 5. Inline token replacements for mixed strings (e.g., "Welcome __OBJ_1__")
730
612
  if (typeof arg === "string" && arg.includes("__OBJ_")) {
731
613
  return arg.replace(/__OBJ_\d+__/g, (match) => {
732
614
  if (objectRegistry && objectRegistry.has(match)) {
@@ -736,8 +618,6 @@ function processValues(elements, operator, args, parent, objectRegistry) {
736
618
  return match;
737
619
  });
738
620
  }
739
-
740
- // Fallback literal string
741
621
  return arg;
742
622
  });
743
623
  }
@@ -745,7 +625,6 @@ function processValues(elements, operator, args, parent, objectRegistry) {
745
625
  preparedArgs = [args];
746
626
  }
747
627
 
748
- // Extra safety: Make sure tokens passed directly as Array items are mapped
749
628
  if (originalWasArray) {
750
629
  preparedArgs = preparedArgs.map(a => {
751
630
  if (typeof a === 'string' && /^__OBJ_\d+__$/.test(a) && objectRegistry && objectRegistry.has(a)) {
@@ -755,7 +634,6 @@ function processValues(elements, operator, args, parent, objectRegistry) {
755
634
  });
756
635
  }
757
636
 
758
- // Create the resolvedArgs shape the engine natively expects
759
637
  let resolvedArgs;
760
638
  if (originalWasArray || preparedArgs.length > 1) {
761
639
  resolvedArgs = preparedArgs;
@@ -765,9 +643,9 @@ function processValues(elements, operator, args, parent, objectRegistry) {
765
643
  resolvedArgs = undefined;
766
644
  }
767
645
 
768
- // --- 2. EXECUTE OR ASSIGN ---
769
646
  if (typeof rawValue === "function") {
770
647
  try {
648
+ let execCtx = { element: el, registry: objectRegistry };
771
649
  if (isConstructor(rawValue, propName)) {
772
650
  if (Array.isArray(resolvedArgs)) {
773
651
  rawValue = new rawValue(...resolvedArgs);
@@ -778,22 +656,21 @@ function processValues(elements, operator, args, parent, objectRegistry) {
778
656
  }
779
657
  } else {
780
658
  if (Array.isArray(resolvedArgs)) {
781
- rawValue = rawValue.apply(el, resolvedArgs); // Bind context correctly to element!
659
+ rawValue = rawValue.apply(execCtx, resolvedArgs);
782
660
  } else if (resolvedArgs !== undefined) {
783
- rawValue = rawValue.call(el, resolvedArgs);
661
+ rawValue = rawValue.call(execCtx, resolvedArgs);
784
662
  } else {
785
- rawValue = rawValue.call(el);
663
+ rawValue = rawValue.call(execCtx);
786
664
  }
787
665
  }
788
666
  } catch (err) {
789
667
  console.warn(`Operator Engine: Failed to execute method or operator "${operator}"`, err);
790
- continue; // Skip this element's value if execution failed
668
+ continue;
791
669
  }
792
670
  } else if (hasArgs && propName) {
793
- // PROPERTY ASSIGNMENT
794
671
  try {
795
672
  el[propName] = resolvedArgs;
796
- rawValue = el[propName]; // Read it back to ensure success/validate formatting
673
+ rawValue = el[propName];
797
674
  } catch (err) {
798
675
  console.warn(`Operator Engine: Failed to assign property "${propName}"`, err);
799
676
  continue;
@@ -806,17 +683,137 @@ function processValues(elements, operator, args, parent, objectRegistry) {
806
683
  }
807
684
 
808
685
  if (!hasValidProperty) return undefined;
809
- if (results.length === 0) return ""; // Explicitly return empty string if elements are found but have no matches to prevent Token masking.
810
-
811
- // Return the array wrapper (even for length 1) to ensure .map array methods remain accessible!
686
+ if (results.length === 0) return "";
812
687
  if (results.length === 1) return results[0];
813
688
  return results;
814
689
  }
815
690
 
816
- function getSubdomain() {
817
- const hostname = window.location.hostname, parts = hostname.split(".");
818
- if (parts.length > 2 && isNaN(parseInt(parts[parts.length - 1]))) return parts.slice(0, parts.length - 2).join(".");
819
- return null;
691
+ export function unpackTokens(val, registry) {
692
+ if (typeof val === "string") {
693
+ const exactMatch = val.match(/^__OBJ_(\d+)__$/);
694
+ if (exactMatch && registry.has(val)) {
695
+ return registry.get(val);
696
+ }
697
+ if (val.includes("__OBJ_")) {
698
+ return val.replace(/__OBJ_\d+__(?:\.[a-zA-Z0-9_]+)*(?:\([^)]*\))?/g, (match) => {
699
+ const parsed = safeParse(match, registry);
700
+ if (parsed !== null && parsed !== undefined) {
701
+ return typeof parsed === "object" ? JSON.stringify(parsed) : String(parsed);
702
+ }
703
+ const baseTokenMatch = match.match(/^__OBJ_\d+__/);
704
+ if (baseTokenMatch && registry.has(baseTokenMatch[0])) {
705
+ const baseVal = registry.get(baseTokenMatch[0]);
706
+ return typeof baseVal === "object" ? JSON.stringify(baseVal) : String(baseVal);
707
+ }
708
+ return "";
709
+ });
710
+ }
711
+ }
712
+ return val;
713
+ }
714
+
715
+ function resolveOperator(element, operator, args, parent, params, objectRegistry) {
716
+ if (params.some((p) => p instanceof Promise)) return "";
717
+ if (args && typeof args === "string" && args.includes("$")) {
718
+ args = processOperators(element, args, [], operator, params, objectRegistry);
719
+ }
720
+
721
+ let targetElements = element ? [element] : [];
722
+ if (args && typeof args === "string") {
723
+ const objMatch = args.match(/^__OBJ_(\d+)__$/);
724
+ if (objMatch && objectRegistry.has(args)) {
725
+ let registeredVal = objectRegistry.get(args);
726
+ let isList = Array.isArray(registeredVal) || (typeof NodeList !== "undefined" && registeredVal instanceof NodeList) || (typeof HTMLCollection !== "undefined" && registeredVal instanceof HTMLCollection);
727
+ targetElements = isList ? Array.from(registeredVal) : [registeredVal];
728
+ } else if (!customOperators.has(operator) && (operator === "$query")) {
729
+ let qResults = queryElements({ element, selector: args });
730
+ if (!qResults) {
731
+ targetElements = [];
732
+ } else {
733
+ let isList = Array.isArray(qResults) || (typeof NodeList !== "undefined" && qResults instanceof NodeList) || (typeof HTMLCollection !== "undefined" && qResults instanceof HTMLCollection);
734
+ targetElements = isList ? Array.from(qResults) : [qResults];
735
+ }
736
+ if (!targetElements.length) return undefined;
737
+ }
738
+ }
739
+
740
+ let value = processValues(targetElements, operator, args, parent, objectRegistry);
741
+ if (value && typeof value === "string" && value.includes("$")) {
742
+ value = processOperators(element, value, [], parent, params, objectRegistry);
743
+ }
744
+ return value;
820
745
  }
821
746
 
822
- export { processOperators, processOperatorsAsync, customOperators, safeParse };
747
+ export function processOperators(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
748
+ if (typeof value !== "string" || (!value.includes("$") && !value.includes("ObjectId()"))) return value;
749
+ let processedValue = value, hasPromise = false, unresolvedTokens = new Map();
750
+
751
+ while (processedValue.includes("$") || processedValue.includes("ObjectId()")) {
752
+ const paramMatch = processedValue.match(/^\$$PARAM_(\d+)\$\$/);
753
+ if (paramMatch && Array.isArray(params) && params.length > 0) {
754
+ const index = parseInt(paramMatch[1], 10);
755
+ if (index < params.length) {
756
+ processedValue = processedValue.replace(paramMatch[0], params[index]);
757
+ continue;
758
+ }
759
+ }
760
+
761
+ const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
762
+ if (!operator || (operator === "$param" && !args)) break;
763
+
764
+ const textToReplace = fullMatch || rawContent;
765
+ if (exclude.includes(operator)) {
766
+ const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
767
+ unresolvedTokens.set(token, textToReplace);
768
+ processedValue = processedValue.replace(textToReplace, token);
769
+ continue;
770
+ }
771
+
772
+ let resolvedValue = resolveOperator(element, operator, args, parent, params, objectRegistry);
773
+ if (resolvedValue === undefined) {
774
+ const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
775
+ unresolvedTokens.set(token, textToReplace);
776
+ processedValue = processedValue.replace(textToReplace, token);
777
+ continue;
778
+ }
779
+ if (resolvedValue instanceof Promise) {
780
+ const paramIndex = params.length;
781
+ params.push(resolvedValue);
782
+ processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
783
+ hasPromise = true;
784
+ break;
785
+ }
786
+ if (params.some((p) => p instanceof Promise)) { hasPromise = true; break; }
787
+
788
+ let replacement = "";
789
+ if (operator === "$param") params.push(resolvedValue);
790
+ else if (resolvedValue !== null && (typeof resolvedValue === "object" || typeof resolvedValue === "function")) {
791
+ const token = `__OBJ_${objectRegistry.size}__`;
792
+ objectRegistry.set(token, resolvedValue);
793
+ replacement = token;
794
+ } else replacement = resolvedValue ?? "";
795
+
796
+ if (processedValue === textToReplace) { processedValue = replacement; break; }
797
+ processedValue = processedValue.replace(textToReplace, replacement);
798
+ }
799
+
800
+ if (typeof processedValue === "string") {
801
+ let keys = Array.from(unresolvedTokens.keys()).reverse();
802
+ for (const token of keys) {
803
+ processedValue = processedValue.replaceAll(token, unresolvedTokens.get(token));
804
+ }
805
+ processedValue = unpackTokens(processedValue, objectRegistry);
806
+ }
807
+
808
+ if (hasPromise) return { value: processedValue, params, objectRegistry };
809
+ return processedValue;
810
+ }
811
+
812
+ export async function processOperatorsAsync(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
813
+ let result = processOperators(element, value, exclude, parent, params, objectRegistry);
814
+ while (typeof result === "object" && result.params) {
815
+ const resolvedParams = await Promise.all(result.params);
816
+ result = processOperators(element, result.value, exclude, parent, resolvedParams, result.objectRegistry || objectRegistry);
817
+ }
818
+ return result;
819
+ }