@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/.github/workflows/automated.yml +23 -11
- package/.github/workflows/manual.yml +1 -2
- package/CHANGELOG.md +43 -0
- package/package.json +11 -127
- package/release.config.js +12 -4
- package/src/dataQuery.js +1 -0
- package/src/index.js +90 -70
- package/src/operators.ast.js +287 -0
- package/src/operators.js +450 -89
- package/src/operators.v1.js +687 -0
- package/src/queryElements.js +7 -9
- package/index.cjs +0 -3
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
$
|
|
298
|
+
$client_id: () => localStorage.getItem("clientId"),
|
|
216
299
|
$session_id: () => localStorage.getItem("session_id"),
|
|
217
|
-
$this: (
|
|
218
|
-
$value: (
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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: (
|
|
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: (
|
|
249
|
-
$getObjectValue: (
|
|
250
|
-
|
|
251
|
-
|
|
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: (
|
|
257
|
-
let value = args || "";
|
|
380
|
+
$parse: function(value = "") {
|
|
258
381
|
try { return JSON.parse(value); } catch (e) { return value; }
|
|
259
382
|
},
|
|
260
|
-
$numberFormat: (
|
|
261
|
-
let
|
|
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: (
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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 === '
|
|
302
|
-
|
|
303
|
-
if (
|
|
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
|
-
|
|
306
|
-
if (balance > deepestBalance) { deepestBalance = balance; deepestStart = i; deepestEnd = -1; }
|
|
421
|
+
stack.push(i);
|
|
307
422
|
} else if (char === ')') {
|
|
308
|
-
if (
|
|
309
|
-
|
|
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
|
-
|
|
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("(")
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
636
|
+
|
|
637
|
+
let results = [];
|
|
429
638
|
let hasValidProperty = customOp ? true : false;
|
|
430
|
-
|
|
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) {
|
|
437
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
aggregatedString += String(rawValue ?? "");
|
|
802
|
+
|
|
803
|
+
if (rawValue !== undefined && rawValue !== null) {
|
|
804
|
+
results.push(rawValue);
|
|
450
805
|
}
|
|
451
806
|
}
|
|
452
|
-
|
|
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 };
|