Skip to content

Commit 2734187

Browse files
committed
feat: improved operators to target element poperties and methods
1 parent 19eb2ae commit 2734187

File tree

1 file changed

+185
-66
lines changed

1 file changed

+185
-66
lines changed

src/utility.js

Lines changed: 185 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,200 @@
1-
import { ObjectId } from "@cocreate/utils";
2-
3-
const operatorsMap = {
4-
$organization_id: () => localStorage.getItem("organization_id"),
5-
$user_id: () => localStorage.getItem("user_id"),
6-
$clientId: () => localStorage.getItem("clientId"),
7-
$session_id: () => localStorage.getItem("session_id"),
8-
$innerWidth: () => window.innerWidth,
9-
$innerHeight: () => window.innerHeight,
10-
$value: (element) => element.getValue() || "",
11-
$href: () => window.location.href.replace(/\/$/, ""),
12-
$origin: () => window.location.origin,
13-
$protocol: () => window.location.protocol,
14-
$hostname: () => window.location.hostname,
15-
$host: () => window.location.host,
16-
$port: () => window.location.port,
17-
$pathname: () => window.location.pathname.replace(/\/$/, ""),
18-
$hash: () => window.location.hash,
19-
$subdomain: () => getSubdomain() || "",
20-
$object_id: () => ObjectId().toString(),
21-
"ObjectId()": () => ObjectId().toString(),
22-
$scrollWidth: (element) => element?.scrollWidth || 0,
23-
$relativePath: () => {
24-
let depth = window.location.pathname.split("/").length - 1;
25-
return depth > 0 ? "../".repeat(depth) : "./";
26-
},
27-
$path: () => {
28-
let path = window.location.pathname;
29-
30-
if (path.split("/").pop().includes(".")) {
31-
path = path.replace(/\/[^\/]+$/, "/");
32-
}
1+
import { ObjectId, queryElements } from "@cocreate/utils";
2+
3+
// Operators handled directly for simple, synchronous value retrieval
4+
const customOperators = new Map(
5+
Object.entries({
6+
$organization_id: () => localStorage.getItem("organization_id"),
7+
$user_id: () => localStorage.getItem("user_id"),
8+
$clientId: () => localStorage.getItem("clientId"),
9+
$session_id: () => localStorage.getItem("session_id"),
10+
$value: (element) => element.getValue() || "",
11+
$innerWidth: () => window.innerWidth,
12+
$innerHeight: () => window.innerHeight,
13+
$href: () => window.location.href.replace(/\/$/, ""),
14+
$origin: () => window.location.origin,
15+
$protocol: () => window.location.protocol,
16+
$hostname: () => window.location.hostname,
17+
$host: () => window.location.host,
18+
$port: () => window.location.port,
19+
$pathname: () => window.location.pathname.replace(/\/$/, ""),
20+
$hash: () => window.location.hash,
21+
$subdomain: () => getSubdomain() || "",
22+
$object_id: () => ObjectId().toString(),
23+
"ObjectId()": () => ObjectId().toString(),
24+
$relativePath: () => {
25+
let depth = window.location.pathname.split("/").length - 1;
26+
return depth > 0 ? "../".repeat(depth) : "./";
27+
},
28+
$path: () => {
29+
let path = window.location.pathname;
30+
if (path.split("/").pop().includes(".")) {
31+
path = path.replace(/\/[^\/]+$/, "/");
32+
}
33+
return path === "/" ? "" : path;
34+
},
35+
$param: (element, args) => args,
36+
$setValue: (element, args) => element.setValue(...args) || ""
37+
})
38+
);
39+
40+
// Operators that access a specific property of a target element
41+
const propertyOperators = new Set([
42+
"$scrollWidth",
43+
"$scrollHeight",
44+
"$offsetWidth",
45+
"$offsetHeight",
46+
"$id",
47+
"$tagName",
48+
"$className",
49+
"$textContent",
50+
"$innerHTML",
51+
"$getValue"
52+
]);
53+
54+
// Combine all known operator keys for the main parsing regex
55+
const knownOperatorKeys = [
56+
...customOperators.keys(),
57+
...propertyOperators
58+
].sort((a, b) => b.length - a.length);
3359

34-
return path === "/" ? "" : path;
60+
function escapeRegexKey(key) {
61+
if (key.startsWith("$")) {
62+
return "\\" + key; // Escape the leading $
63+
} else if (key === "ObjectId()") {
64+
return "ObjectId\\(\\)"; // Escape the parentheses
3565
}
36-
};
66+
return key; // Should not happen with current keys, but fallback
67+
}
68+
69+
const operatorPattern = knownOperatorKeys.map(escapeRegexKey).join("|");
3770

38-
function processOperators(element, value, exclude = []) {
71+
// Regex to find potential operators and their arguments
72+
// $1: Potential operator identifier (e.g., $user_id, $closestDiv)
73+
// $2: Arguments within parentheses (optional)
74+
const regex = new RegExp(`(${operatorPattern})(?:\\s*\\((.*?)\\))?`, "g");
75+
76+
/**
77+
* Synchronously processes a string, finding and replacing operators recursively.
78+
* Assumes ALL underlying operations (getValue, queryElements) are synchronous.
79+
* @param {Element | null} element - Context element.
80+
* @param {string} value - String containing operators.
81+
* @param {string[]} [exclude=[]] - Operator prefixes to ignore.
82+
* @returns {string} - Processed string.
83+
*/
84+
function processOperators(element, value, exclude = [], parent) {
85+
// Early exit if no operators are possible or value is not a string
3986
if (typeof value !== "string" || !value.includes("$")) {
40-
return value; // Return as-is for non-string input
87+
return value;
4188
}
89+
let params = [];
90+
const processedValue = value.replace(
91+
regex,
92+
(match, operator, args = "") => {
93+
// 'match' is the full matched string (e.g., "$closest(.myClass)")
94+
// 'operator' is the identifier part (e.g., "$closest")
95+
// 'args' is the content within parentheses (e.g., ".myClass") or "" if no parentheses
4296

43-
// Dynamically construct a regex from the keys in operatorsMap
44-
const operatorKeys = Object.keys(operatorsMap)
45-
.filter((key) => !exclude.includes(key)) // Exclude specified operators
46-
.map((key) => `\\${key}`)
47-
.join("|");
48-
const regex = new RegExp(operatorKeys, "g");
49-
50-
// Replace matched operators with their resolved values
51-
return value.replace(regex, (match) => {
52-
if (operatorsMap[match]) {
53-
console.log(`Replacing "${match}"`);
54-
let newValue = operatorsMap[match](element); // Pass `element` explicitly
55-
if (!newValue && newValue !== 0) {
56-
newValue = "";
97+
if (operator === "$param" && !args) {
98+
return match;
99+
}
100+
101+
// If a valid operator was identified AND it's not in the exclude list
102+
if (operator && !exclude.includes(operator)) {
103+
// Resolve the value for the identified operator and its arguments
104+
// Pass the *trimmed* arguments to the resolver
105+
let resolvedValue = resolveOperator(
106+
element,
107+
operator,
108+
args.replace(/^['"]|['"]$/g, "").trim(),
109+
parent
110+
);
111+
112+
if (operator === "$param") {
113+
params.push(resolvedValue);
114+
return "";
115+
}
116+
117+
return resolvedValue ?? "";
118+
} else {
119+
// If no known operator matched, or if it was excluded,
120+
// return the original matched string (no replacement).
121+
return match;
57122
}
58-
return newValue;
59123
}
60-
// Log a warning with suggestions for valid operators
61-
console.warn(
62-
`No match found for "${match}" in operatorsMap. ` +
63-
`Available operators: ${Object.keys(operatorsMap).join(", ")}`
64-
);
65-
return "";
66-
});
124+
);
125+
126+
if (params.length) {
127+
return params;
128+
}
129+
130+
return processedValue;
67131
}
68132

69-
function getSubdomain() {
70-
const hostname = window.location.hostname; // e.g., "api.dev.example.com"
71-
const parts = hostname.split(".");
133+
/**
134+
* Synchronously determines and executes the action for a single operator token.
135+
* @returns {string} The final string value for the token.
136+
*/
137+
function resolveOperator(element, operator, args, parent) {
138+
if (args && args.includes("$")) {
139+
args = processOperators(element, args, "", operator);
140+
}
141+
142+
let targetElements = element ? [element] : [];
143+
if (args && typeof args === "string") {
144+
targetElements = queryElements({
145+
element,
146+
args
147+
});
148+
}
72149

73-
// Handle edge cases for single-word hostnames or IPs
74-
if (parts.length > 2 && isNaN(parts[parts.length - 1])) {
75-
return parts.slice(0, parts.length - 2).join("."); // Subdomain
150+
let value = processValues(targetElements, operator, args, parent);
151+
if (value && typeof value === "string" && value.includes("$")) {
152+
value = processOperators(element, value, parent);
76153
}
77154

78-
return null; // No subdomain
155+
return value;
156+
}
157+
158+
/**
159+
* Synchronously aggregates values.
160+
* @returns {string} The aggregated string value.
161+
*/
162+
function processValues(elements, operator, args, parent) {
163+
let customOp = customOperators.get(operator);
164+
let aggregatedString = "";
165+
for (const el of elements) {
166+
if (!el) continue;
167+
let rawValue = customOp || el?.[operator.substring(1)];
168+
if (typeof rawValue === "function") {
169+
if (Array.isArray(args)) {
170+
if (args.length) {
171+
return "";
172+
}
173+
rawValue = rawValue(el, ...args);
174+
} else {
175+
rawValue = rawValue(el, args);
176+
}
177+
}
178+
179+
if (parent === "$param") {
180+
if (rawValue) {
181+
return rawValue;
182+
}
183+
} else {
184+
aggregatedString += String(rawValue ?? "");
185+
}
186+
}
187+
188+
return aggregatedString;
189+
}
190+
191+
function getSubdomain() {
192+
const hostname = window.location.hostname;
193+
const parts = hostname.split(".");
194+
if (parts.length > 2 && isNaN(parseInt(parts[parts.length - 1]))) {
195+
return parts.slice(0, parts.length - 2).join(".");
196+
}
197+
return null;
79198
}
80199

81-
export default { processOperators };
200+
export { processOperators };

0 commit comments

Comments
 (0)