@cocreate/utils 1.42.0 → 1.42.1

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
@@ -3,578 +3,459 @@ import { uid } from "./uid.js";
3
3
  import { queryElements } from "./queryElements.js";
4
4
  import { getValueFromObject } from "./getValueFromObject.js";
5
5
 
6
- // Operators handled directly for simple, synchronous value retrieval
7
- const customOperators = new Map(
8
- Object.entries({
9
- $organization_id: () => localStorage.getItem("organization_id"),
10
- $user_id: () => localStorage.getItem("user_id"),
11
- $clientId: () => localStorage.getItem("clientId"),
12
- $session_id: () => localStorage.getItem("session_id"),
13
- $value: (element) => element.getValue() || "",
14
- // TODO: get length of value
15
- // $length: (element) => {element.getValue() || ""},
16
- $innerWidth: () => window.innerWidth,
17
- $innerHeight: () => window.innerHeight,
18
- $href: () => window.location.href.replace(/\/$/, ""),
19
- $origin: () => window.location.origin,
20
- $protocol: () => window.location.protocol,
21
- $hostname: () => window.location.hostname,
22
- $host: () => window.location.host,
23
- $port: () => window.location.port,
24
- $pathname: () => window.location.pathname.replace(/\/$/, ""),
25
- $hash: () => window.location.hash,
26
- $subdomain: () => getSubdomain() || "",
27
- $object_id: () => ObjectId().toString(),
28
- "ObjectId()": () => ObjectId().toString(),
29
- $relativePath: () => {
30
- let currentPath = window.location.pathname.replace(/\/[^\/]*$/, ""); // Remove file or last segment from path
31
- let depth = currentPath.split("/").filter(Boolean).length; // Count actual directory levels
32
- return depth > 0 ? "../".repeat(depth) : "./";
33
- },
34
- $path: () => {
35
- let path = window.location.pathname;
36
- if (path.split("/").pop().includes(".")) {
37
- path = path.replace(/\/[^\/]+$/, "/");
38
- }
39
- return path === "/" ? "" : path;
40
- },
41
- $param: (element, args) => args,
42
- $getObjectValue: (element, args) => {
43
- if (Array.isArray(args) && args.length >= 2) {
44
- return getValueFromObject(args[0], args[1]);
45
- }
46
- return "";
47
- },
48
- $setValue: (element, args) => element.setValue(...args) || "",
49
- $true: () => true,
50
- $false: () => false,
51
- // TODO: Handle uuid generation
52
- // $uid: () => uid.generate(6),
53
- $parse: (element, args) => {
54
- let value = args || "";
55
- try {
56
- return JSON.parse(value);
57
- } catch (e) {
58
- return value;
59
- }
60
- },
61
- // TODO: Implement number formatting
62
- $numberFormat: (element, args) => {
63
- let number = parseFloat(args[0]);
64
- // Simple, fixed arg mapping:
65
- // args[0] = locale (internationalization)
66
- // args[1] = options (object)
67
- // args[2] = number (if provided). If not provided, fall back to legacy behavior where args[0] might be the number.
68
- if (!Array.isArray(args)) args = [args];
69
-
70
- const locale = args[0] || undefined;
71
- const options = args[1] || {};
72
- const numCandidate = args[2] !== undefined ? args[2] : args[0];
73
-
74
- number = parseFloat(numCandidate);
75
-
76
- if (isNaN(number)) return String(numCandidate ?? "");
77
-
78
- return new Intl.NumberFormat(locale, options).format(number);
79
- },
80
- $uid: (element, args) => uid(args[0]) || "",
81
-
82
- })
83
- );
84
-
85
- // Operators that access a specific property of a target element
86
- const propertyOperators = new Set([
87
- "$scrollWidth",
88
- "$scrollHeight",
89
- "$offsetWidth",
90
- "$offsetHeight",
91
- "$id",
92
- "$tagName",
93
- "$className",
94
- "$textContent",
95
- "$innerHTML",
96
- "$getValue",
97
- "$reset"
98
- ]);
99
-
100
- // Combine all known operator keys for the main parsing regex
101
- const knownOperatorKeys = [
102
- ...customOperators.keys(),
103
- ...propertyOperators
104
- ].sort((a, b) => b.length - a.length);
6
+ // --- AST EVALUATION ENGINE (safeParse) ---
7
+
8
+ const mathConstants = { PI: Math.PI, E: Math.E };
9
+ const mathFunctions = {
10
+ abs: Math.abs, ceil: Math.ceil, floor: Math.floor, round: Math.round,
11
+ max: Math.max, min: Math.min, pow: Math.pow, sqrt: Math.sqrt,
12
+ log: Math.log, sin: Math.sin, cos: Math.cos, tan: Math.tan,
13
+ Number: (v) => Number(v) // Explicit numeric casting
14
+ };
105
15
 
106
16
  /**
107
- * Helper function to check if a string path starts with a known bare operator.
108
- * This logic is necessary to separate '$value' from '[0].src' when no parentheses remain.
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)
109
19
  */
110
- const findBareOperatorInPath = (path, knownOperatorKeys) => {
111
- const trimmedPath = path.trim();
112
- for (const key of knownOperatorKeys) {
113
- if (trimmedPath.startsWith(key)) {
114
- const remaining = trimmedPath.substring(key.length);
115
- // Edge Case Fix: Ensure the operator is followed by a space, bracket, dot, or end-of-string.
116
- // This prevents "valueThing" from being incorrectly split into "$value" + "Thing".
117
- if (remaining.length === 0 || /^\s|\[|\./.test(remaining)) {
118
- return key;
119
- }
120
- }
20
+ class Ref {
21
+ constructor(obj, prop) {
22
+ this.obj = obj;
23
+ this.prop = prop;
24
+ }
25
+ get() { return this.obj ? this.obj[this.prop] : undefined; }
26
+ set(val) {
27
+ if (this.obj) this.obj[this.prop] = val;
28
+ return val;
121
29
  }
122
- return null;
30
+ }
31
+
32
+ function unref(val) {
33
+ return val instanceof Ref ? val.get() : val;
123
34
  }
124
35
 
125
36
  /**
126
- * Finds the innermost function call (operator + its balanced parentheses argument)
127
- * in the expression. This is the core logic for iterative parsing.
128
- * * @param {string} expression The full expression string.
129
- * @returns {{operator: string, args: string, fullMatch: string} | null} Details of the innermost function call, or null.
37
+ * Parses math, logic, ternaries, dot notation, and property assignments securely.
130
38
  */
131
- const findInnermostFunctionCall = (expression) => {
132
- let balance = 0;
133
- let deepestStart = -1;
134
- let deepestEnd = -1;
135
- let deepestBalance = -1;
136
- let inSingleQuote = false;
137
- let inDoubleQuote = false;
138
-
139
- // First pass: Find the indices of the DEEPEST balanced parenthesis pair.
140
- for (let i = 0; i < expression.length; i++) {
141
- const char = expression[i];
39
+ function safeParse(expression, registry = new Map()) {
40
+ if (typeof expression !== "string") return expression;
41
+
42
+ let currentExpr = expression.trim();
43
+ if (!currentExpr) return null;
142
44
 
143
- if (char === '"' && !inSingleQuote) {
144
- inDoubleQuote = !inDoubleQuote;
145
- continue;
146
- } else if (char === "'" && !inDoubleQuote) {
147
- inSingleQuote = !inDoubleQuote;
148
- continue;
45
+ const tokenizerRegex = /('[^']*'|"[^"]*"|\d+(?:\.\d+)?|>=|<=|===|!==|==|!=|&&|\|\||[a-zA-Z_][a-zA-Z0-9_\.]*|[\+\-\*\/\%\(\)\?\:\>\<\!\,\=])/g;
46
+ const tokens = currentExpr.match(tokenizerRegex) || [];
47
+ let pos = 0;
48
+
49
+ function peek() { return tokens[pos]; }
50
+ function consume() { return tokens[pos++]; }
51
+
52
+ function parse() {
53
+ return parseAssignment();
54
+ }
55
+
56
+ function parseAssignment() {
57
+ let left = parseTernary();
58
+ if (peek() === "=") {
59
+ consume();
60
+ let right = unref(parseAssignment());
61
+ if (left instanceof Ref) {
62
+ return left.set(right); // Assign value to the actual object property
63
+ }
64
+ return right;
149
65
  }
66
+ return left;
67
+ }
150
68
 
151
- if (inSingleQuote || inDoubleQuote) {
152
- continue;
69
+ function parseTernary() {
70
+ let left = parseLogical();
71
+ if (peek() === "?") {
72
+ consume();
73
+ let trueExpr = parseTernary();
74
+ if (peek() === ":") {
75
+ consume();
76
+ let falseExpr = parseTernary();
77
+ return unref(left) ? unref(trueExpr) : unref(falseExpr);
78
+ }
153
79
  }
80
+ return left;
81
+ }
154
82
 
155
- if (char === '(') {
156
- balance++;
157
- // Track the index of the open parenthesis that belongs to the deepest balance level
158
- if (balance > deepestBalance) {
159
- deepestBalance = balance;
160
- deepestStart = i;
161
- deepestEnd = -1; // Reset end until match is found
83
+ function parseLogical() {
84
+ let left = parseComparison();
85
+ while (peek() === "&&" || peek() === "||") {
86
+ let op = consume();
87
+ let right = parseComparison();
88
+ if (op === "&&") left = unref(left) && unref(right);
89
+ if (op === "||") left = unref(left) || unref(right);
90
+ }
91
+ return left;
92
+ }
93
+
94
+ function parseComparison() {
95
+ let left = parseAdditive();
96
+ while ([">", "<", ">=", "<=", "===", "!==", "==", "!="].includes(peek())) {
97
+ let op = consume();
98
+ let right = parseAdditive();
99
+ let l = unref(left), r = unref(right);
100
+ if (op === ">") left = l > r;
101
+ if (op === "<") left = l < r;
102
+ if (op === ">=") left = l >= r;
103
+ if (op === "<=") left = l <= r;
104
+ if (op === "===") left = l === r;
105
+ if (op === "!==") left = l !== r;
106
+ if (op === "==") left = l == r;
107
+ if (op === "!=") left = l != r;
108
+ }
109
+ return left;
110
+ }
111
+
112
+ function parseAdditive() {
113
+ let left = parseMultiplicative();
114
+ while (["+", "-"].includes(peek())) {
115
+ let op = consume();
116
+ let right = parseMultiplicative();
117
+ if (op === "+") left = unref(left) + unref(right);
118
+ if (op === "-") left = unref(left) - unref(right);
119
+ }
120
+ return left;
121
+ }
122
+
123
+ function parseMultiplicative() {
124
+ let left = parsePrimary();
125
+ while (["*", "/", "%"].includes(peek())) {
126
+ let op = consume();
127
+ let right = parsePrimary();
128
+ if (op === "*") left = unref(left) * unref(right);
129
+ if (op === "/") left = unref(left) / unref(right);
130
+ if (op === "%") left = unref(left) % unref(right);
131
+ }
132
+ return left;
133
+ }
134
+
135
+ function parsePrimary() {
136
+ let token = consume();
137
+ if (!token) return undefined;
138
+
139
+ if (/^\d/.test(token)) return parseFloat(token);
140
+
141
+ if (token.startsWith("'") || token.startsWith('"')) {
142
+ return token.slice(1, -1);
143
+ }
144
+
145
+ if (token === "true") return true;
146
+ if (token === "false") return false;
147
+
148
+ if (token === "(") {
149
+ let expr = unref(parse());
150
+ if (peek() === ")") consume();
151
+ return expr;
152
+ }
153
+
154
+ if (token === "-") return -unref(parsePrimary());
155
+ if (token === "!") return !unref(parsePrimary());
156
+
157
+ if (mathConstants.hasOwnProperty(token)) return mathConstants[token];
158
+
159
+ if (peek() === "(" && mathFunctions.hasOwnProperty(token)) {
160
+ consume();
161
+ let args = [];
162
+ if (peek() !== ")") {
163
+ args.push(unref(parse()));
164
+ while (peek() === ",") {
165
+ consume();
166
+ args.push(unref(parse()));
167
+ }
162
168
  }
163
- } else if (char === ')') {
164
- if (balance === deepestBalance) {
165
- // This is the closing parenthesis that matches the deepest open parenthesis found
166
- deepestEnd = i;
169
+ if (peek() === ")") consume();
170
+ return mathFunctions[token](...args);
171
+ }
172
+
173
+ // --- Context & Object Registry Traversal ---
174
+ let path = token.split(".");
175
+ let baseToken = path[0];
176
+ let val;
177
+
178
+ if (registry.has(baseToken)) {
179
+ val = registry.get(baseToken);
180
+ } else if (typeof window !== "undefined" && window[baseToken]) {
181
+ val = window[baseToken];
182
+ } else {
183
+ val = undefined;
184
+ }
185
+
186
+ if (path.length === 1) return val;
187
+
188
+ for (let i = 1; i < path.length - 1; i++) {
189
+ if (val !== null && val !== undefined) {
190
+ val = val[path[i]];
191
+ } else {
192
+ return undefined;
167
193
  }
168
- balance--;
169
194
  }
195
+
196
+ return new Ref(val, path[path.length - 1]);
170
197
  }
171
-
172
- // If we didn't find a balanced pair, or the match is invalid, exit.
173
- if (deepestStart === -1 || deepestEnd === -1 || deepestEnd <= deepestStart) {
198
+
199
+ try {
200
+ const result = parse();
201
+ return unref(result);
202
+ } catch (error) {
203
+ console.warn(`safeParse error: ${error.message} (Expr: "${expression}")`, error);
174
204
  return null;
175
205
  }
206
+ }
176
207
 
177
- // Now we have the innermost argument content indices (deepestStart + 1 to deepestEnd - 1)
178
- const rawArgs = expression.substring(deepestStart + 1, deepestEnd).trim();
179
-
180
- // Step 2: Find the operator name backward from the deepestStart index.
181
- let operatorStart = -1;
182
- let nonWhitespaceFound = false;
183
208
 
184
- for (let i = deepestStart - 1; i >= 0; i--) {
209
+ // --- CORE OPERATOR ENGINE ---
210
+
211
+ const customOperators = new Map(
212
+ Object.entries({
213
+ $organization_id: () => localStorage.getItem("organization_id"),
214
+ $user_id: () => localStorage.getItem("user_id"),
215
+ $clientId: () => localStorage.getItem("clientId"),
216
+ $session_id: () => localStorage.getItem("session_id"),
217
+ $this: (element) => element,
218
+ $value: (element) => element.getValue() || "",
219
+ $innerWidth: () => window.innerWidth,
220
+ $innerHeight: () => window.innerHeight,
221
+ $href: () => window.location.href.replace(/\/$/, ""),
222
+ $origin: () => window.location.origin,
223
+ $protocol: () => window.location.protocol,
224
+ $hostname: () => window.location.hostname,
225
+ $host: () => window.location.host,
226
+ $port: () => window.location.port,
227
+ $pathname: () => window.location.pathname.replace(/\/$/, ""),
228
+ $hash: () => window.location.hash,
229
+ $subdomain: () => getSubdomain() || "",
230
+ $object_id: () => ObjectId().toString(),
231
+ "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;
236
+ },
237
+ $eval: (element, args, context) => safeParse(args, context.registry),
238
+ $relativePath: () => {
239
+ let currentPath = window.location.pathname.replace(/\/[^\/]*$/, "");
240
+ let depth = currentPath.split("/").filter(Boolean).length;
241
+ return depth > 0 ? "../".repeat(depth) : "./";
242
+ },
243
+ $path: () => {
244
+ let path = window.location.pathname;
245
+ if (path.split("/").pop().includes(".")) path = path.replace(/\/[^\/]+$/, "/");
246
+ return path === "/" ? "" : path;
247
+ },
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 "";
252
+ },
253
+ $setValue: (element, args) => element.setValue(...args) || "",
254
+ $true: () => true,
255
+ $false: () => false,
256
+ $parse: (element, args) => {
257
+ let value = args || "";
258
+ try { return JSON.parse(value); } catch (e) { return value; }
259
+ },
260
+ $numberFormat: (element, args) => {
261
+ let number = parseFloat(args[0]);
262
+ if (!Array.isArray(args)) args = [args];
263
+ const locale = args[0] || undefined;
264
+ const options = args[1] || {};
265
+ const numCandidate = args[2] !== undefined ? args[2] : args[0];
266
+ number = parseFloat(numCandidate);
267
+ if (isNaN(number)) return String(numCandidate ?? "");
268
+ return new Intl.NumberFormat(locale, options).format(number);
269
+ },
270
+ $uid: (element, args) => uid(args[0]) || "",
271
+ })
272
+ );
273
+
274
+ const isConstructor = (func, name) => {
275
+ try {
276
+ if (typeof func !== 'function') return false;
277
+ if (/^\s*class\s+/.test(func.toString())) return true;
278
+ if (!func.prototype) return false;
279
+ const n = name || func.name;
280
+ if (n && /^[A-Z]/.test(n)) return true;
281
+ } catch(e) {}
282
+ return false;
283
+ };
284
+
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
+ }
295
+
296
+ const findInnermostFunctionCall = (expression) => {
297
+ let balance = 0, deepestStart = -1, deepestEnd = -1, deepestBalance = -1;
298
+ let inSingleQuote = false, inDoubleQuote = false;
299
+ for (let i = 0; i < expression.length; i++) {
185
300
  const char = expression[i];
186
-
187
- // Skip trailing whitespace between operator and '('
188
- if (!nonWhitespaceFound) {
189
- if (/\s/.test(char)) {
190
- continue;
191
- }
192
- nonWhitespaceFound = true;
193
- }
194
-
195
- // Find the start of the operator name
196
- // This regex captures letters, numbers, hyphens, and the required $
197
- let isOperatorChar = /[\w\-\$]/.test(char);
198
-
199
- if (!isOperatorChar) {
200
- operatorStart = i + 1;
201
- break;
301
+ if (char === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; continue; }
302
+ else if (char === "'" && !inDoubleQuote) { inSingleQuote = !inDoubleQuote; continue; }
303
+ if (inSingleQuote || inDoubleQuote) continue;
304
+ if (char === '(') {
305
+ balance++;
306
+ if (balance > deepestBalance) { deepestBalance = balance; deepestStart = i; deepestEnd = -1; }
307
+ } else if (char === ')') {
308
+ if (balance === deepestBalance) deepestEnd = i;
309
+ balance--;
202
310
  }
311
+ }
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; }
203
319
  operatorStart = i;
204
320
  }
205
-
206
321
  if (operatorStart === -1) operatorStart = 0;
207
-
208
322
  const operatorNameCandidate = expression.substring(operatorStart, deepestStart).trim();
209
-
210
- // Step 3: Validate the operator name
211
- if (knownOperatorKeys.includes(operatorNameCandidate)) {
212
- // Construct the full match string: operatorNameCandidate(rawArgs)
213
- const fullMatch = expression.substring(operatorStart, deepestEnd + 1);
214
-
215
- return {
216
- operator: operatorNameCandidate,
217
- args: rawArgs,
218
- fullMatch: fullMatch
219
- };
323
+ if (/^\$[\w\-]+$/.test(operatorNameCandidate) || customOperators.has(operatorNameCandidate)) {
324
+ return { operator: operatorNameCandidate, args: rawArgs, fullMatch: expression.substring(operatorStart, deepestEnd + 1) };
220
325
  }
221
-
222
- return null; // Operator name invalid or not found
326
+ return null;
223
327
  };
224
328
 
225
- /**
226
- * Main function to find the innermost operator.
227
- * * Logic flow is updated to prioritize finding the innermost FUNCTION CALL,
228
- * then fall back to finding a BARE OPERATOR if no function calls remain.
229
- * * @param {string} expression The expression to parse.
230
- * @returns {{operator: string, args: string, rawContent: string, fullMatch?: string} | {operator: null, args: string, rawContent: string}}
231
- */
232
329
  const findInnermostOperator = (expression) => {
233
- // Helper function to strip leading and trailing parentheses from a string
234
- function stripParentheses(str) {
235
- let result = str;
236
- if (result.startsWith("(")) {
237
- result = result.substring(1);
238
- }
239
- if (result.endsWith(")")) {
240
- result = result.substring(0, result.length - 1);
241
- }
242
- return result;
243
- }
244
- let args;
245
-
246
- // --- 1. PRIORITY: Find Innermost FUNCTION CALL (Operator with Parentheses) ---
330
+ 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);
334
+ return result;
335
+ }
247
336
  const functionCall = findInnermostFunctionCall(expression);
248
-
249
- if (functionCall) {
250
- // Return the full function expression details, including the full string section
251
- args = stripParentheses(functionCall.args);
252
- return {
253
- operator: functionCall.operator,
254
- args, // Arguments without parentheses
255
- rawContent: functionCall.args, // The content inside the parentheses (arguments)
256
- fullMatch: functionCall.fullMatch // The operator(args) string (the complete section to replace)
257
- };
258
- }
259
-
260
- // --- 2. FALLBACK: Find BARE OPERATOR (e.g., $value path) ---
261
-
262
- // If no function calls are found, the entire expression is treated as the raw content
263
- const rawContent = expression.trim();
264
-
265
- // Now check the raw content to see if it starts with a bare operator ($value)
266
- const innermostOperator = findBareOperatorInPath(rawContent, knownOperatorKeys);
267
-
268
- if (innermostOperator) {
269
- const operatorArgs = rawContent.substring(innermostOperator.length).trim();
270
- args = stripParentheses(operatorArgs);
271
- return {
272
- operator: innermostOperator,
273
- args, // Arguments without parentheses
274
- rawContent: rawContent,
275
- };
276
- }
277
-
278
-
279
- args = stripParentheses(rawContent);
280
-
281
- // Fallback if no known operator is found
282
- return {
283
- operator: null,
284
- args,
285
- rawContent: rawContent
286
- };
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 };
287
341
  };
288
342
 
289
- function escapeRegexKey(key) {
290
- if (key.startsWith("$")) {
291
- return "\\" + key; // Escape the leading $
292
- } else if (key === "ObjectId()") {
293
- return "ObjectId\\(\\)"; // Escape the parentheses
294
- }
295
- return key; // Should not happen with current keys, but fallback
296
- }
297
-
298
- /**
299
- * Synchronously processes a string, finding and replacing operators recursively.
300
- * Assumes ALL underlying operations (getValue, queryElements) are synchronous.
301
- * @param {Element | null} element - Context element.
302
- * @param {string} value - String containing operators.
303
- * @param {string[]} [exclude=[]] - Operator prefixes to ignore.
304
- * @returns {string | {value: string, params: Promise[]}} - Processed string or an object containing the partially processed string and unresolved Promises.
305
- */
306
- function processOperators(
307
- element,
308
- value,
309
- exclude = [],
310
- parent,
311
- params = []
312
- ) {
313
- // Early exit if no operators are possible or value is not a string
314
- if (typeof value !== "string" || !value.includes("$")) {
315
- return value;
316
- }
317
-
318
- let processedValue = value;
319
- let hasPromise = false;
320
- let parsedValue = null
321
-
322
- while (processedValue.includes("$")) {
323
-
324
- // --- PROMISE TOKEN RESOLUTION ---
325
- // If the processedValue starts with a resolved parameter token from the previous async step,
326
- // substitute the token with its actual (now resolved) value from the params array.
343
+ function processOperators(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
344
+ if (typeof value !== "string" || (!value.includes("$") && !value.includes("ObjectId()"))) return value;
345
+ let processedValue = value, hasPromise = false, unresolvedTokens = new Map();
346
+ while (processedValue.includes("$") || processedValue.includes("ObjectId()")) {
327
347
  const paramMatch = processedValue.match(/^\$\$PARAM_(\d+)\$\$/);
328
348
  if (paramMatch && Array.isArray(params) && params.length > 0) {
329
349
  const index = parseInt(paramMatch[1], 10);
330
- if (index < params.length) {
331
- const resolvedTokenValue = params[index];
332
- processedValue = processedValue.replace(paramMatch[0], resolvedTokenValue);
333
- // After replacement, we restart the loop to find the *next* innermost operator
334
- continue;
335
- }
350
+ if (index < params.length) { processedValue = processedValue.replace(paramMatch[0], params[index]); continue; }
351
+ }
352
+ const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
353
+ if (!operator || (operator === "$param" && !args)) break;
354
+ const textToReplace = fullMatch || rawContent;
355
+ if (exclude.includes(operator)) {
356
+ const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
357
+ unresolvedTokens.set(token, textToReplace);
358
+ processedValue = processedValue.replace(textToReplace, token);
359
+ continue;
336
360
  }
337
- // --- END TOKEN RESOLUTION ---
338
-
339
- const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
340
-
341
- if (!operator) {
342
- break; // No more operators found
343
- }
344
-
345
- if (operator === "$param" && !args) {
346
- break;
347
- }
348
-
349
- if (operator && !exclude.includes(operator)) {
350
-
351
- // --- Determine textToReplace ---
352
- // The fullMatch property from findInnermostOperator ensures we correctly replace
353
- // the whole expression (e.g., "$param(...)") or just the bare operator ($value path).
354
- const textToReplace = fullMatch || rawContent;
355
- // --- END textToReplace CALCULATION ---
356
-
357
- let resolvedValue = resolveOperator(element, operator, args, parent, params);
358
-
359
- if (resolvedValue instanceof Promise) {
360
- const paramIndex = params.length; // Get the index *before* push
361
- params.push(resolvedValue); // Store the Promise
362
-
363
- // CRITICAL FIX: Replace the matched expression with a unique token, then break.
364
- processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
365
- hasPromise = true;
366
- break; // Stop processing and yield
367
- }
368
-
369
- if (params.some((p) => p instanceof Promise)) {
370
- hasPromise = true;
371
- break; // A nested call found a promise, stop and yield
372
- }
373
-
374
- let replacement = "";
375
- if (operator === "$param") {
376
- params.push(resolvedValue);
377
- } else {
378
- replacement = resolvedValue ?? "";
379
- }
380
-
381
- if (processedValue === textToReplace) {
382
- processedValue = replacement;
383
- break;
384
- }
385
-
386
- processedValue = processedValue.replace(textToReplace, replacement);
387
-
388
- if (!processedValue.includes("$")) {
389
- // If there are still unresolved operators, we need to continue processing
390
- break;
391
- }
392
-
393
- } else {
394
- // If operator is excluded, we need to advance past it to avoid infinite loop
395
- break;
396
- }
397
- }
398
-
399
- if (hasPromise) {
400
- return { value: processedValue, params };
401
- }
402
-
403
- if (params.length) {
404
- if (processedValue.trim() === "") {
405
- return params;
406
- }
407
- }
408
-
409
- return processedValue;
361
+ let resolvedValue = resolveOperator(element, operator, args, parent, params, objectRegistry);
362
+ if (resolvedValue === undefined) {
363
+ const token = `__UNRESOLVED_${unresolvedTokens.size}__`;
364
+ unresolvedTokens.set(token, textToReplace);
365
+ processedValue = processedValue.replace(textToReplace, token);
366
+ continue;
367
+ }
368
+ if (resolvedValue instanceof Promise) {
369
+ const paramIndex = params.length;
370
+ params.push(resolvedValue);
371
+ processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
372
+ hasPromise = true;
373
+ break;
374
+ }
375
+ if (params.some((p) => p instanceof Promise)) { hasPromise = true; break; }
376
+ let replacement = "";
377
+ if (operator === "$param") params.push(resolvedValue);
378
+ else if (resolvedValue !== null && (typeof resolvedValue === "object" || typeof resolvedValue === "function")) {
379
+ const token = `__OBJ_${objectRegistry.size}__`;
380
+ objectRegistry.set(token, resolvedValue);
381
+ replacement = token;
382
+ } else replacement = resolvedValue ?? "";
383
+ if (processedValue === textToReplace) { processedValue = replacement; break; }
384
+ processedValue = processedValue.replace(textToReplace, replacement);
385
+ }
386
+ for (const [token, originalText] of unresolvedTokens.entries()) processedValue = processedValue.replace(token, originalText);
387
+ if (typeof processedValue === "string") {
388
+ const exactMatch = processedValue.match(/^__OBJ_(\d+)__$/);
389
+ if (exactMatch && objectRegistry.has(processedValue)) processedValue = objectRegistry.get(processedValue);
390
+ }
391
+ if (hasPromise) return { value: processedValue, params, objectRegistry };
392
+ return processedValue;
410
393
  }
411
394
 
412
- async function processOperatorsAsync(
413
- element,
414
- value,
415
- exclude = [],
416
- parent,
417
- params = []
418
- ) {
419
- let result = processOperators(element, value, exclude, parent, params);
420
-
421
- while (typeof result === "object" && result.params) {
422
- const resolvedParams = await Promise.all(result.params);
423
- // Note: The second argument passed to processOperators here is the partially processed string (result.value)
424
- // which now contains the PARAM tokens. The third argument is the array of resolved values (resolvedParams)
425
- // which will be used to replace those tokens in the subsequent processOperators call.
426
- result = processOperators(
427
- element,
428
- result.value,
429
- exclude,
430
- parent,
431
- resolvedParams
432
- );
433
- }
434
-
435
- if (result instanceof Promise) {
436
- return await result;
437
- }
438
-
439
- return result;
395
+ async function processOperatorsAsync(element, value, exclude = [], parent, params = [], objectRegistry = new Map()) {
396
+ let result = processOperators(element, value, exclude, parent, params, objectRegistry);
397
+ while (typeof result === "object" && result.params) {
398
+ const resolvedParams = await Promise.all(result.params);
399
+ result = processOperators(element, result.value, exclude, parent, resolvedParams, result.objectRegistry || objectRegistry);
400
+ }
401
+ return result;
440
402
  }
441
403
 
442
- /**
443
- * Synchronously determines and executes the action for processing a single operator token.
444
- * @param {HTMLElement|null} element - The context element from which to derive values or execute methods.
445
- * @param {string} operator - The operator to apply, indicating what actions or property/method to evaluate.
446
- * @param {string|Array} args - Arguments that may be used by the operator, which could be further processed if they contain a nested operator.
447
- * @param {string} parent - Context in which the function is called, potentially affecting behavior or processing.
448
- * @returns {string} The final resolved value after applying the operator to the given elements.
449
- */
450
- function resolveOperator(element, operator, args, parent, params) {
451
- // If a promise is already in the params, we must stop and wait for it to be resolved.
452
- if (params.some((p) => p instanceof Promise)) {
453
- return "";
454
- }
455
-
456
- // If args contain any operators (indicated by '$'), process them recursively
457
- if (args && typeof args === "string" && args.includes("$")) {
458
- // Reprocess args to resolve any nested operators
459
- args = processOperators(element, args, "", operator, params);
460
- }
461
-
462
- if (params.some((p) => p instanceof Promise)) {
463
- return operator;
464
- }
465
-
466
- // Initialize an array of elements to operate on, starting with the single element reference if provided
467
- let targetElements = element ? [element] : [];
468
-
469
- // If the argument is a string and the operator is NOT a custom utility that expects raw data
470
- // (like $param, $parse), we assume the argument is a selector.
471
- if (args && typeof args === "string" && !customOperators.has(operator)) {
472
- targetElements = queryElements({
473
- element, // Use the context element as the base for querying
474
- selector: args // Selector from args to find matching elements
475
- });
476
-
477
- // If no elements are found matching the selector in args, return args unmodified
478
- if (!targetElements.length) return args;
479
- }
480
-
481
- // Generate a processed value by applying the operator to each of the target elements
482
- let value = processValues(targetElements, operator, args, parent);
483
-
484
- // If the result is a string and still contains unresolved operators, process them further
485
- if (value && typeof value === "string" && value.includes("$")) {
486
- // Resolve any remaining operators within the value string
487
- value = processOperators(element, value, parent, params);
488
- }
489
-
490
- // Return the final processed value, fully resolved
491
- return value;
404
+ function resolveOperator(element, operator, args, parent, params, objectRegistry) {
405
+ if (params.some((p) => p instanceof Promise)) return "";
406
+ if (args && typeof args === "string" && args.includes("$")) {
407
+ args = processOperators(element, args, [], operator, params, objectRegistry);
408
+ }
409
+ let targetElements = element ? [element] : [];
410
+ if (args && typeof args === "string") {
411
+ const objMatch = args.match(/^__OBJ_(\d+)__$/);
412
+ if (objMatch && objectRegistry.has(args)) {
413
+ targetElements = [objectRegistry.get(args)];
414
+ } else if (!customOperators.has(operator)) {
415
+ targetElements = queryElements({ element, selector: args });
416
+ if (!targetElements.length) return undefined;
417
+ }
418
+ }
419
+ let value = processValues(targetElements, operator, args, parent, objectRegistry);
420
+ if (value && typeof value === "string" && value.includes("$")) {
421
+ value = processOperators(element, value, [], parent, params, objectRegistry);
422
+ }
423
+ return value;
492
424
  }
493
425
 
494
- /**
495
- * Synchronously processes and aggregates values from a set of elements based on a specified operator.
496
- * @param {Array<HTMLElement>} elements - Array of elements to be processed.
497
- * @param {string} operator - The operator to apply to each element, indicating which property or method to use.
498
- * @param {string|Array} args - Arguments that may be passed to the method if the operator corresponds to a function.
499
- * @param {string} parent - Context in which the function is called, possibly influencing behavior (e.g., special handling for "$param").
500
- * @returns {string} The combined string value obtained by processing elements with the specified operator.
501
- */
502
- function processValues(elements, operator, args, parent) {
503
- // Attempt to fetch a custom operator function associated with the operator
504
- let customOp = customOperators.get(operator);
505
-
506
- // Initialize an empty string to accumulate results from processing each element
507
- let aggregatedString = "";
508
-
509
- // Iterate over each element in the provided elements array
510
- for (const el of elements) {
511
- // If the element is null or undefined, skip to the next iteration
512
- if (!el) continue;
513
-
514
- // Determine the raw value from the custom operator or by accessing a property/method directly on the element
515
- let rawValue = customOp || el?.[operator.substring(1)];
516
-
517
- // Check if the rawValue is a function and process it using provided arguments
518
- if (typeof rawValue === "function") {
519
- // If arguments are provided as an array
520
- if (Array.isArray(args)) {
521
- // If the custom operator is NOT $param and has arguments, something is wrong with the flow.
522
- // However, if it's a generic method on an element (like $setValue), the args are passed via spread.
523
- if (customOperators.has(operator) && operator !== "$setValue" && operator !== "$getObjectValue" && args.length) {
524
- // For simple custom operators that don't take multiple args, return an empty string.
525
- return "";
526
- }
527
- // Invoke the function using the element and spread array arguments
528
- rawValue = rawValue(el, ...args);
529
- } else {
530
- // Otherwise, invoke the function using the element and direct arguments
531
- rawValue = rawValue(el, args);
532
- }
533
- }
534
-
535
- // If the parent context requires parameter resolution
536
- if (parent === "$param") {
537
- // Return the first evaluated rawValue that is not null or undefined
538
- if (rawValue) {
539
- return rawValue;
540
- }
541
- } else {
542
- if (
543
- rawValue instanceof Promise ||
544
- (typeof rawValue === "object" && rawValue !== null)
545
- ) {
546
- return rawValue;
547
- }
548
- // Otherwise, append the stringified rawValue to the aggregated result, defaulting to an empty string if it's nullish
549
- aggregatedString += String(rawValue ?? "");
550
- }
551
- }
552
-
553
- // Return the final aggregated string containing all processed values
554
- return aggregatedString;
426
+ function processValues(elements, operator, args, parent, objectRegistry) {
427
+ let customOp = customOperators.get(operator);
428
+ let aggregatedString = "";
429
+ let hasValidProperty = customOp ? true : false;
430
+ const context = { registry: objectRegistry, element: elements[0] };
431
+ for (const el of elements) {
432
+ if (!el) continue;
433
+ let rawValue = customOp;
434
+ const propName = customOp ? null : operator.substring(1);
435
+ if (!customOp) {
436
+ if (propName in el) { hasValidProperty = true; rawValue = el[propName]; }
437
+ else continue;
438
+ }
439
+ 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);
444
+ }
445
+ }
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 ?? "");
450
+ }
451
+ }
452
+ return hasValidProperty ? aggregatedString : undefined;
555
453
  }
556
454
 
557
- /**
558
- * Extracts the subdomain from the current window's hostname.
559
- * @returns {string|null} - The subdomain part of the hostname if it exists, or null if there is none.
560
- */
561
455
  function getSubdomain() {
562
- // Retrieve the hostname from the current window's location
563
- const hostname = window.location.hostname;
564
-
565
- // Split the hostname into parts divided by dots ('.')
566
- const parts = hostname.split(".");
567
-
568
- // Check if the hostname has more than two parts and ensure the last part isn't a number (a common TLD check)
569
- // A typical domain structure might look like "sub.domain.com",
570
- // where "sub" is the subdomain, "domain" is the second-level domain, and "com" is the top-level domain.
571
- if (parts.length > 2 && isNaN(parseInt(parts[parts.length - 1]))) {
572
- // Join all parts except the last two (which are assumed to be the domain and TLD) to get the subdomain
573
- return parts.slice(0, parts.length - 2).join(".");
574
- }
575
-
576
- // Return null if there's no valid subdomain structure
577
- return null;
456
+ const hostname = window.location.hostname, parts = hostname.split(".");
457
+ if (parts.length > 2 && isNaN(parseInt(parts[parts.length - 1]))) return parts.slice(0, parts.length - 2).join(".");
458
+ return null;
578
459
  }
579
460
 
580
- export { processOperators, processOperatorsAsync };
461
+ export { processOperators, processOperatorsAsync, customOperators };