@cocreate/utils 1.42.1 → 1.44.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/src/operators.js CHANGED
@@ -16,14 +16,38 @@ const mathFunctions = {
16
16
  /**
17
17
  * Reference class used by safeParse to track object properties for assignments.
18
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.
19
20
  */
20
21
  class Ref {
21
22
  constructor(obj, prop) {
22
23
  this.obj = obj;
23
24
  this.prop = prop;
25
+
26
+ // Ensure we recognize standard arrays AND DOM collections (NodeList/HTMLCollection)
27
+ let isList = Array.isArray(obj) ||
28
+ (typeof NodeList !== "undefined" && obj instanceof NodeList) ||
29
+ (typeof HTMLCollection !== "undefined" && obj instanceof HTMLCollection);
30
+
31
+ this.isArrayContext = isList && !(prop in obj);
32
+
33
+ if (this.isArrayContext && !Array.isArray(this.obj)) {
34
+ this.obj = Array.from(this.obj);
35
+ }
36
+ }
37
+ get() {
38
+ 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;
42
+ }
43
+ let res = this.obj ? this.obj[this.prop] : undefined;
44
+ return res;
24
45
  }
25
- get() { return this.obj ? this.obj[this.prop] : undefined; }
26
46
  set(val) {
47
+ if (this.isArrayContext) {
48
+ this.obj.forEach(item => { if (item) item[this.prop] = val; });
49
+ return val;
50
+ }
27
51
  if (this.obj) this.obj[this.prop] = val;
28
52
  return val;
29
53
  }
@@ -59,7 +83,7 @@ function safeParse(expression, registry = new Map()) {
59
83
  consume();
60
84
  let right = unref(parseAssignment());
61
85
  if (left instanceof Ref) {
62
- return left.set(right); // Assign value to the actual object property
86
+ return left.set(right); // Assign value to the actual object property (or all properties in an array)
63
87
  }
64
88
  return right;
65
89
  }
@@ -183,17 +207,76 @@ function safeParse(expression, registry = new Map()) {
183
207
  val = undefined;
184
208
  }
185
209
 
186
- if (path.length === 1) return val;
210
+ if (path.length === 1) {
211
+ // Support explicit call for root variables
212
+ if (peek() === "(" && typeof val === "function") {
213
+ consume();
214
+ let args = [];
215
+ if (peek() !== ")") {
216
+ args.push(unref(parse()));
217
+ while (peek() === ",") {
218
+ consume();
219
+ args.push(unref(parse()));
220
+ }
221
+ }
222
+ if (peek() === ")") consume();
223
+ return val(...args);
224
+ }
225
+ return val;
226
+ }
187
227
 
188
228
  for (let i = 1; i < path.length - 1; i++) {
189
229
  if (val !== null && val !== undefined) {
190
- val = val[path[i]];
230
+ // Support deep traversal across arrays
231
+ let isList = Array.isArray(val) || (typeof NodeList !== "undefined" && val instanceof NodeList) || (typeof HTMLCollection !== "undefined" && val instanceof HTMLCollection);
232
+ if (isList && !(path[i] in val)) {
233
+ val = Array.from(val).map(item => item ? item[path[i]] : undefined);
234
+ } else {
235
+ val = val[path[i]];
236
+ }
191
237
  } else {
192
238
  return undefined;
193
239
  }
194
240
  }
195
241
 
196
- return new Ref(val, path[path.length - 1]);
242
+ let ref = new Ref(val, path[path.length - 1]);
243
+
244
+ // Support explicit method calls (e.g., obj.method(arg))
245
+ if (peek() === "(") {
246
+ consume();
247
+ let args = [];
248
+ if (peek() !== ")") {
249
+ args.push(unref(parse()));
250
+ while (peek() === ",") {
251
+ consume();
252
+ args.push(unref(parse()));
253
+ }
254
+ }
255
+ if (peek() === ")") consume();
256
+
257
+ // Map the method call across all items if it's an array context
258
+ if (ref.isArrayContext) {
259
+ let results = ref.obj.map(item => {
260
+ 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;
266
+ });
267
+ return results; // Return the natively mapped Array so user can process it!
268
+ } else {
269
+ let func = ref.obj ? ref.obj[ref.prop] : undefined;
270
+ 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.`);
275
+ }
276
+ }
277
+ }
278
+
279
+ return ref;
197
280
  }
198
281
 
199
282
  try {
@@ -212,10 +295,10 @@ const customOperators = new Map(
212
295
  Object.entries({
213
296
  $organization_id: () => localStorage.getItem("organization_id"),
214
297
  $user_id: () => localStorage.getItem("user_id"),
215
- $clientId: () => localStorage.getItem("clientId"),
298
+ $client_id: () => localStorage.getItem("clientId"),
216
299
  $session_id: () => localStorage.getItem("session_id"),
217
- $this: (element) => element,
218
- $value: (element) => element.getValue() || "",
300
+ $this: function() { return this; },
301
+ $value: function() { return this && typeof this.getValue === 'function' ? this.getValue() : ""; },
219
302
  $innerWidth: () => window.innerWidth,
220
303
  $innerHeight: () => window.innerHeight,
221
304
  $href: () => window.location.href.replace(/\/$/, ""),
@@ -229,12 +312,52 @@ const customOperators = new Map(
229
312
  $subdomain: () => getSubdomain() || "",
230
313
  $object_id: () => ObjectId().toString(),
231
314
  "ObjectId()": () => ObjectId().toString(),
232
- // Unwrap query results if only one element is found
233
- $query: (element, args) => {
234
- const results = queryElements({ element, selector: args });
235
- return results.length === 1 ? results[0] : results;
315
+
316
+ // Unwrap query results correctly: Returns only the single first item
317
+ $query: function(selector) {
318
+ let element = this;
319
+ if (typeof selector === 'string') {
320
+ selector = selector.trim();
321
+ // Strip quotes out of the selector in case they were passed (e.g., $query('#id1'))
322
+ if ((selector.startsWith("'") && selector.endsWith("'")) ||
323
+ (selector.startsWith('"') && selector.endsWith('"'))) {
324
+ selector = selector.slice(1, -1);
325
+ }
326
+ }
327
+ try {
328
+ let results = queryElements({ element, selector });
329
+ if (!results || results.length === 0) return undefined;
330
+ return results[0]; // Strict single item return
331
+ } catch (error) {
332
+ console.warn(`Operator Engine: Invalid $query selector => "${selector}"`, error);
333
+ return undefined;
334
+ }
335
+ },
336
+ // Unwrap query results correctly: Always returns an array
337
+ $queryAll: function(selector) {
338
+ let element = this;
339
+ if (typeof selector === 'string') {
340
+ selector = selector.trim();
341
+ // Strip quotes out of the selector in case they were passed (e.g., $queryAll('.items'))
342
+ if ((selector.startsWith("'") && selector.endsWith("'")) ||
343
+ (selector.startsWith('"') && selector.endsWith('"'))) {
344
+ selector = selector.slice(1, -1);
345
+ }
346
+
347
+ // Safely ignore queries targeting $document to prevent querySelector crashes
348
+ if (selector.includes('$document')) return [];
349
+ }
350
+ try {
351
+ let results = queryElements({ element, selector });
352
+ if (!results || results.length === 0) return [];
353
+ // Always cast to array so Ref mapping handles methods uniformly
354
+ return Array.from(results);
355
+ } catch (error) {
356
+ console.warn(`Operator Engine: Invalid $queryAll selector => "${selector}"`, error);
357
+ return [];
358
+ }
236
359
  },
237
- $eval: (element, args, context) => safeParse(args, context.registry),
360
+ $eval: function(expression) { return safeParse(expression); },
238
361
  $relativePath: () => {
239
362
  let currentPath = window.location.pathname.replace(/\/[^\/]*$/, "");
240
363
  let depth = currentPath.split("/").filter(Boolean).length;
@@ -245,29 +368,28 @@ const customOperators = new Map(
245
368
  if (path.split("/").pop().includes(".")) path = path.replace(/\/[^\/]+$/, "/");
246
369
  return path === "/" ? "" : path;
247
370
  },
248
- $param: (element, args) => args,
249
- $getObjectValue: (element, args) => {
250
- if (Array.isArray(args) && args.length >= 2) return getValueFromObject(args[0], args[1]);
251
- return "";
371
+ $param: function(...args) { return args.length === 1 ? args[0] : args; },
372
+ $getObjectValue: function(obj, key) {
373
+ return getValueFromObject(obj, key);
374
+ },
375
+ $setValue: function(...args) {
376
+ return typeof this.setValue === 'function' ? this.setValue(...args) || "" : "";
252
377
  },
253
- $setValue: (element, args) => element.setValue(...args) || "",
254
378
  $true: () => true,
255
379
  $false: () => false,
256
- $parse: (element, args) => {
257
- let value = args || "";
380
+ $parse: function(value = "") {
258
381
  try { return JSON.parse(value); } catch (e) { return value; }
259
382
  },
260
- $numberFormat: (element, args) => {
261
- let number = parseFloat(args[0]);
262
- if (!Array.isArray(args)) args = [args];
383
+ $numberFormat: function() {
384
+ let args = Array.from(arguments);
263
385
  const locale = args[0] || undefined;
264
386
  const options = args[1] || {};
265
387
  const numCandidate = args[2] !== undefined ? args[2] : args[0];
266
- number = parseFloat(numCandidate);
388
+ let number = parseFloat(numCandidate);
267
389
  if (isNaN(number)) return String(numCandidate ?? "");
268
390
  return new Intl.NumberFormat(locale, options).format(number);
269
391
  },
270
- $uid: (element, args) => uid(args[0]) || "",
392
+ $uid: function(prefix) { return uid(prefix) || ""; },
271
393
  })
272
394
  );
273
395
 
@@ -282,62 +404,108 @@ const isConstructor = (func, name) => {
282
404
  return false;
283
405
  };
284
406
 
285
- const findBareOperatorInPath = (path) => {
286
- const trimmedPath = path.trim();
287
- const match = trimmedPath.match(/^(\$[\w\-]+)/);
288
- if (match) {
289
- const key = match[1];
290
- const remaining = trimmedPath.substring(key.length);
291
- if (remaining.length === 0 || /^\s|\[|\./.test(remaining)) return key;
292
- }
293
- return null;
294
- }
407
+ const findInnermostOperator = (expression) => {
408
+ let parens = [];
409
+ let stack = [];
410
+ let inSQ = false, inDQ = false;
295
411
 
296
- const findInnermostFunctionCall = (expression) => {
297
- let balance = 0, deepestStart = -1, deepestEnd = -1, deepestBalance = -1;
298
- let inSingleQuote = false, inDoubleQuote = false;
412
+ // 1. Capture all parenthesis pairs accurately (The Parenthesis-First "Animals" boundary)
299
413
  for (let i = 0; i < expression.length; i++) {
300
414
  const char = expression[i];
301
- if (char === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; continue; }
302
- else if (char === "'" && !inDoubleQuote) { inSingleQuote = !inDoubleQuote; continue; }
303
- if (inSingleQuote || inDoubleQuote) continue;
415
+ if (char === '\\') { i++; continue; }
416
+ if (char === '"' && !inSQ) inDQ = !inDQ;
417
+ if (char === "'" && !inDQ) inSQ = !inSQ;
418
+ if (inSQ || inDQ) continue;
419
+
304
420
  if (char === '(') {
305
- balance++;
306
- if (balance > deepestBalance) { deepestBalance = balance; deepestStart = i; deepestEnd = -1; }
421
+ stack.push(i);
307
422
  } else if (char === ')') {
308
- if (balance === deepestBalance) deepestEnd = i;
309
- balance--;
423
+ if (stack.length > 0) {
424
+ const start = stack.pop();
425
+ parens.push({ start: start, end: i, depth: stack.length });
426
+ }
310
427
  }
311
428
  }
312
- if (deepestStart === -1 || deepestEnd === -1 || deepestEnd <= deepestStart) return null;
313
- const rawArgs = expression.substring(deepestStart + 1, deepestEnd).trim();
314
- let operatorStart = -1, nonWhitespaceFound = false;
315
- for (let i = deepestStart - 1; i >= 0; i--) {
316
- const char = expression[i];
317
- if (!nonWhitespaceFound) { if (/\s/.test(char)) continue; nonWhitespaceFound = true; }
318
- if (!/[\w\-\$]/.test(char)) { operatorStart = i + 1; break; }
319
- operatorStart = i;
320
- }
321
- if (operatorStart === -1) operatorStart = 0;
322
- const operatorNameCandidate = expression.substring(operatorStart, deepestStart).trim();
323
- if (/^\$[\w\-]+$/.test(operatorNameCandidate) || customOperators.has(operatorNameCandidate)) {
324
- return { operator: operatorNameCandidate, args: rawArgs, fullMatch: expression.substring(operatorStart, deepestEnd + 1) };
325
- }
326
- return null;
327
- };
328
429
 
329
- const findInnermostOperator = (expression) => {
430
+ // Sort by depth descending (innermost first), then by start descending (right-most first)
431
+ parens.sort((a, b) => b.depth - a.depth || b.start - a.start);
432
+
330
433
  function stripParentheses(str) {
331
- let result = str;
332
- if (result.startsWith("(")) result = result.substring(1);
333
- if (result.endsWith(")")) result = result.substring(0, result.length - 1);
434
+ let result = str.trim();
435
+ if (result.startsWith("(") && result.endsWith(")")) {
436
+ let balance = 0;
437
+ let isMatchedPair = true;
438
+ for (let i = 0; i < result.length - 1; i++) {
439
+ if (result[i] === '(') balance++;
440
+ else if (result[i] === ')') balance--;
441
+ if (balance === 0) { isMatchedPair = false; break; }
442
+ }
443
+ if (isMatchedPair && balance === 1) {
444
+ return result.substring(1, result.length - 1);
445
+ }
446
+ }
334
447
  return result;
335
448
  }
336
- const functionCall = findInnermostFunctionCall(expression);
337
- if (functionCall) return { operator: functionCall.operator, args: stripParentheses(functionCall.args), rawContent: functionCall.args, fullMatch: functionCall.fullMatch };
338
- const rawContent = expression.trim(), innermostOperator = findBareOperatorInPath(rawContent);
339
- if (innermostOperator) return { operator: innermostOperator, args: stripParentheses(rawContent.substring(innermostOperator.length).trim()), rawContent: rawContent };
340
- return { operator: null, args: stripParentheses(rawContent), rawContent: rawContent };
449
+
450
+ // 2. Look to the left of the innermost parentheses to find the keyword operator
451
+ for (const p of parens) {
452
+ // We safely isolate ONLY what is inside the parens, bypassing the need for string replacements
453
+ const insideParens = expression.substring(p.start + 1, p.end).trim();
454
+ 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*$/);
458
+
459
+ if (match) {
460
+ let opName = match[1] === 'ObjectId' ? 'ObjectId()' : match[1];
461
+ if (customOperators.has(opName) || /^\$[\w\-]+$/.test(opName)) {
462
+ // Pinpoint exactly where the operator string began
463
+ const spacesMatch = leftPart.match(/\s*$/);
464
+ const spacesAtEnd = spacesMatch ? spacesMatch[0].length : 0;
465
+ const fullMatchStart = match.index + match[0].length - match[1].length - spacesAtEnd;
466
+
467
+ const resultBlock = {
468
+ operator: opName,
469
+ args: insideParens, // Perfectly isolated arguments, handling nested chains
470
+ fullMatch: expression.substring(fullMatchStart, p.end + 1),
471
+ rawContent: expression.substring(fullMatchStart, p.end + 1)
472
+ };
473
+ return resultBlock;
474
+ }
475
+ }
476
+ }
477
+
478
+ // 3. Fallback: Find bare operators (like $this) that have no parentheses attached
479
+ const bareRegex = /(?:^|[^\w\$])(\$[\w\-]+|ObjectId\b)/g;
480
+ let bareMatch;
481
+ let bareCandidates = [];
482
+
483
+ while ((bareMatch = bareRegex.exec(expression)) !== null) {
484
+ let possibleOp = bareMatch[1];
485
+ 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
+ let remainder = expression.substring(bareMatch.index + bareMatch[0].length);
489
+ if (!/^\s*\(/.test(remainder)) {
490
+ if (customOperators.has(opName) || /^\$[\w\-]+$/.test(opName)) {
491
+ bareCandidates.push({
492
+ operator: opName,
493
+ args: undefined,
494
+ rawContent: possibleOp,
495
+ fullMatch: possibleOp,
496
+ start: bareMatch.index + (bareMatch[0].length - possibleOp.length)
497
+ });
498
+ }
499
+ }
500
+ }
501
+
502
+ if (bareCandidates.length > 0) {
503
+ bareCandidates.sort((a, b) => b.start - a.start);
504
+ return bareCandidates[0];
505
+ }
506
+
507
+ const defaultFallback = { operator: null, args: stripParentheses(expression), rawContent: expression.trim() };
508
+ return defaultFallback;
341
509
  };
342
510
 
343
511
  function processOperators(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
@@ -383,11 +551,40 @@ function processOperators(element, value, exclude = [], parent, params = [], obj
383
551
  if (processedValue === textToReplace) { processedValue = replacement; break; }
384
552
  processedValue = processedValue.replace(textToReplace, replacement);
385
553
  }
386
- for (const [token, originalText] of unresolvedTokens.entries()) processedValue = processedValue.replace(token, originalText);
554
+
555
+ // Restore unresolved exclusions (Reverse-order replaces ensure perfectly unnested loops)
387
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
388
572
  const exactMatch = processedValue.match(/^__OBJ_(\d+)__$/);
389
- if (exactMatch && objectRegistry.has(processedValue)) processedValue = objectRegistry.get(processedValue);
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
+ }
390
586
  }
587
+
391
588
  if (hasPromise) return { value: processedValue, params, objectRegistry };
392
589
  return processedValue;
393
590
  }
@@ -406,16 +603,27 @@ function resolveOperator(element, operator, args, parent, params, objectRegistry
406
603
  if (args && typeof args === "string" && args.includes("$")) {
407
604
  args = processOperators(element, args, [], operator, params, objectRegistry);
408
605
  }
606
+
409
607
  let targetElements = element ? [element] : [];
410
608
  if (args && typeof args === "string") {
411
609
  const objMatch = args.match(/^__OBJ_(\d+)__$/);
412
610
  if (objMatch && objectRegistry.has(args)) {
413
- targetElements = [objectRegistry.get(args)];
414
- } else if (!customOperators.has(operator)) {
415
- targetElements = queryElements({ element, selector: 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
+ }
416
623
  if (!targetElements.length) return undefined;
417
624
  }
418
625
  }
626
+
419
627
  let value = processValues(targetElements, operator, args, parent, objectRegistry);
420
628
  if (value && typeof value === "string" && value.includes("$")) {
421
629
  value = processOperators(element, value, [], parent, params, objectRegistry);
@@ -425,31 +633,184 @@ function resolveOperator(element, operator, args, parent, params, objectRegistry
425
633
 
426
634
  function processValues(elements, operator, args, parent, objectRegistry) {
427
635
  let customOp = customOperators.get(operator);
428
- let aggregatedString = "";
636
+
637
+ let results = [];
429
638
  let hasValidProperty = customOp ? true : false;
430
- const context = { registry: objectRegistry, element: elements[0] };
639
+
431
640
  for (const el of elements) {
432
641
  if (!el) continue;
433
642
  let rawValue = customOp;
434
643
  const propName = customOp ? null : operator.substring(1);
435
644
  if (!customOp) {
436
- if (propName in el) { hasValidProperty = true; rawValue = el[propName]; }
437
- else continue;
645
+ if (propName in el) {
646
+ hasValidProperty = true;
647
+ rawValue = el[propName];
648
+ }
649
+ else {
650
+ continue;
651
+ }
652
+ }
653
+
654
+ // --- 1. RECONSTRUCT THE "GHOST" STRUCTURE (Tokens -> Actual JS Types) ---
655
+ let preparedArgs = [];
656
+ const originalWasArray = Array.isArray(args);
657
+ const hasArgs = originalWasArray || args !== undefined;
658
+
659
+ if (originalWasArray) {
660
+ preparedArgs = Array.from(args);
661
+ } else if (typeof args === "string") {
662
+ if (args === "") {
663
+ preparedArgs = [""]; // Explicitly retain empty string arguments
664
+ } else {
665
+ // Smart parse for comma-separated arguments respecting strings and brackets
666
+ let parsedArgs = [];
667
+ let currentArg = '';
668
+ let inSQ = false, inDQ = false;
669
+ let depth = 0;
670
+
671
+ for (let i = 0; i < args.length; i++) {
672
+ const char = args[i];
673
+ if (char === '\\') {
674
+ currentArg += char;
675
+ if (i + 1 < args.length) currentArg += args[++i];
676
+ continue;
677
+ }
678
+ if (char === "'" && !inDQ) inSQ = !inSQ;
679
+ if (char === '"' && !inSQ) inDQ = !inDQ;
680
+
681
+ // Keep track of brackets and parens grouping so we don't accidentally split them inside
682
+ if (!inSQ && !inDQ) {
683
+ if (char === '(' || char === '[' || char === '{') depth++;
684
+ else if (char === ')' || char === ']' || char === '}') depth--;
685
+ }
686
+
687
+ if (char === ',' && !inSQ && !inDQ && depth === 0) {
688
+ parsedArgs.push(currentArg.trim());
689
+ currentArg = '';
690
+ } else {
691
+ currentArg += char;
692
+ }
693
+ }
694
+ if (currentArg.trim() !== "") parsedArgs.push(currentArg.trim());
695
+
696
+ // Reconstruct actual JavaScript types
697
+ preparedArgs = parsedArgs.map(arg => {
698
+ // 1. Direct object token match (brings back exactly the array, obj, or variable!)
699
+ if (/^__OBJ_\d+__$/.test(arg) && objectRegistry && objectRegistry.has(arg)) {
700
+ return objectRegistry.get(arg);
701
+ }
702
+
703
+ // 2. Numbers
704
+ if (!isNaN(arg) && arg.trim() !== "") {
705
+ return Number(arg);
706
+ }
707
+
708
+ // 3. Booleans and Null
709
+ if (arg === "true") return true;
710
+ if (arg === "false") return false;
711
+ if (arg === "null") return null;
712
+ if (arg === "undefined") return undefined;
713
+
714
+ // 4. Strings with quotes (strip outer quotes reliably)
715
+ if ((arg.startsWith("'") && arg.endsWith("'")) || (arg.startsWith('"') && arg.endsWith('"'))) {
716
+ let unquoted = arg.slice(1, -1);
717
+ if (unquoted.includes("__OBJ_")) {
718
+ return unquoted.replace(/__OBJ_\d+__/g, (match) => {
719
+ if (objectRegistry && objectRegistry.has(match)) {
720
+ let val = objectRegistry.get(match);
721
+ return typeof val === "object" ? JSON.stringify(val) : String(val);
722
+ }
723
+ return match;
724
+ });
725
+ }
726
+ return unquoted;
727
+ }
728
+
729
+ // 5. Inline token replacements for mixed strings (e.g., "Welcome __OBJ_1__")
730
+ if (typeof arg === "string" && arg.includes("__OBJ_")) {
731
+ return arg.replace(/__OBJ_\d+__/g, (match) => {
732
+ if (objectRegistry && objectRegistry.has(match)) {
733
+ let val = objectRegistry.get(match);
734
+ return typeof val === "object" ? JSON.stringify(val) : String(val);
735
+ }
736
+ return match;
737
+ });
738
+ }
739
+
740
+ // Fallback literal string
741
+ return arg;
742
+ });
743
+ }
744
+ } else if (hasArgs) {
745
+ preparedArgs = [args];
746
+ }
747
+
748
+ // Extra safety: Make sure tokens passed directly as Array items are mapped
749
+ if (originalWasArray) {
750
+ preparedArgs = preparedArgs.map(a => {
751
+ if (typeof a === 'string' && /^__OBJ_\d+__$/.test(a) && objectRegistry && objectRegistry.has(a)) {
752
+ return objectRegistry.get(a);
753
+ }
754
+ return a;
755
+ });
756
+ }
757
+
758
+ // Create the resolvedArgs shape the engine natively expects
759
+ let resolvedArgs;
760
+ if (originalWasArray || preparedArgs.length > 1) {
761
+ resolvedArgs = preparedArgs;
762
+ } else if (preparedArgs.length === 1) {
763
+ resolvedArgs = preparedArgs[0];
764
+ } else {
765
+ resolvedArgs = undefined;
438
766
  }
767
+
768
+ // --- 2. EXECUTE OR ASSIGN ---
439
769
  if (typeof rawValue === "function") {
440
- if (customOp) rawValue = Array.isArray(args) ? rawValue(el, ...args, context) : rawValue(el, args, context);
441
- else {
442
- if (isConstructor(rawValue, propName)) rawValue = Array.isArray(args) ? new rawValue(...args) : new rawValue(args);
443
- else rawValue = Array.isArray(args) ? rawValue.apply(el, args) : rawValue.call(el, args);
770
+ try {
771
+ if (isConstructor(rawValue, propName)) {
772
+ if (Array.isArray(resolvedArgs)) {
773
+ rawValue = new rawValue(...resolvedArgs);
774
+ } else if (resolvedArgs !== undefined) {
775
+ rawValue = new rawValue(resolvedArgs);
776
+ } else {
777
+ rawValue = new rawValue();
778
+ }
779
+ } else {
780
+ if (Array.isArray(resolvedArgs)) {
781
+ rawValue = rawValue.apply(el, resolvedArgs); // Bind context correctly to element!
782
+ } else if (resolvedArgs !== undefined) {
783
+ rawValue = rawValue.call(el, resolvedArgs);
784
+ } else {
785
+ rawValue = rawValue.call(el);
786
+ }
787
+ }
788
+ } catch (err) {
789
+ console.warn(`Operator Engine: Failed to execute method or operator "${operator}"`, err);
790
+ continue; // Skip this element's value if execution failed
791
+ }
792
+ } else if (hasArgs && propName) {
793
+ // PROPERTY ASSIGNMENT
794
+ try {
795
+ el[propName] = resolvedArgs;
796
+ rawValue = el[propName]; // Read it back to ensure success/validate formatting
797
+ } catch (err) {
798
+ console.warn(`Operator Engine: Failed to assign property "${propName}"`, err);
799
+ continue;
444
800
  }
445
801
  }
446
- if (parent === "$param") { if (rawValue !== undefined && rawValue !== null) return rawValue; }
447
- else {
448
- if (rawValue instanceof Promise || (typeof rawValue === "object" && rawValue !== null) || typeof rawValue === "function") return rawValue;
449
- aggregatedString += String(rawValue ?? "");
802
+
803
+ if (rawValue !== undefined && rawValue !== null) {
804
+ results.push(rawValue);
450
805
  }
451
806
  }
452
- return hasValidProperty ? aggregatedString : undefined;
807
+
808
+ 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!
812
+ if (results.length === 1) return results[0];
813
+ return results;
453
814
  }
454
815
 
455
816
  function getSubdomain() {
@@ -458,4 +819,4 @@ function getSubdomain() {
458
819
  return null;
459
820
  }
460
821
 
461
- export { processOperators, processOperatorsAsync, customOperators };
822
+ export { processOperators, processOperatorsAsync, customOperators, safeParse };