const forbidden = new Set(["__proto__", "prototype", "constructor"]); export function valueAt(context, path) { return String(path) .split(".") .reduce((value, key) => { if (forbidden.has(key) || value == null) return undefined; if (typeof value === "string" && key) { try { value = JSON.parse(value); } catch { return undefined; } } return Object.hasOwn(value, key) ? value[key] : undefined; }, context); } const number = (value) => { if ( !["number", "string"].includes(typeof value) || String(value).trim() === "" || !Number.isFinite(Number(value)) ) throw new Error("O cálculo precisa de um valor numérico."); return Number(value); }; const functions = { round: (v) => Math.round(number(v)), floor: (v) => Math.floor(number(v)), ceil: (v) => Math.ceil(number(v)), abs: (v) => Math.abs(number(v)), min: (...v) => Math.min(...v.map(number)), max: (...v) => Math.max(...v.map(number)), lower: (v) => String(v ?? "").toLowerCase(), upper: (v) => String(v ?? "").toUpperCase(), length: (v) => v?.length ?? 0, contains: (a, b) => String(a ?? "").includes(String(b ?? "")), startsWith: (a, b) => String(a ?? "").startsWith(String(b ?? "")), endsWith: (a, b) => String(a ?? "").endsWith(String(b ?? "")), }; // A small expression parser: no eval, property calls, assignments or JavaScript execution. export function evaluate(expression, context = {}) { const source = String(expression).trim(); if (source.length > 2000) throw new Error("Expressão demasiado longa."); const tokens = []; let offset = 0; while (offset < source.length) { const match = /^(\s+|(?:\d+(?:\.\d*)?|\.\d+)|[A-Za-z_][\w]*(?:\.[\w]+)*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|===|!==|==|!=|>=|<=|&&|\|\||[+*/%(),!<>-])/.exec( source.slice(offset), ); if (!match) throw new Error("Expressão inválida."); offset += match[0].length; if (!/^\s+$/.test(match[0])) tokens.push(match[0]); } if (tokens.length > 250) throw new Error("Expressão demasiado complexa."); let i = 0, depth = 0; const precedence = { "||": 1, "&&": 2, "==": 3, "!=": 3, "===": 3, "!==": 3, ">": 4, "<": 4, ">=": 4, "<=": 4, "+": 5, "-": 5, "*": 6, "/": 6, "%": 6, }; const binary = (a, op, b) => { switch (op) { case "+": return number(a) + number(b); case "-": return number(a) - number(b); case "*": return number(a) * number(b); case "/": if (number(b) === 0) throw new Error("Divisão por zero."); return number(a) / number(b); case "%": if (number(b) === 0) throw new Error("Divisão por zero."); return number(a) % number(b); case "==": case "===": return String(a) === String(b); case "!=": case "!==": return String(a) !== String(b); case ">": return number(a) > number(b); case "<": return number(a) < number(b); case ">=": return number(a) >= number(b); case "<=": return number(a) <= number(b); case "&&": return Boolean(a) && Boolean(b); case "||": return Boolean(a) || Boolean(b); } }; function primary() { if (++depth > 40) throw new Error("Expressão demasiado complexa."); try { const t = tokens[i++]; if (!t) throw new Error("Expressão incompleta."); if (t === "!" || t === "-" || t === "+") { const get = primary(); return () => t === "!" ? !get() : t === "-" ? -number(get()) : number(get()); } if (t === "(") { const get = parse(1); if (tokens[i++] !== ")") throw new Error("Falta fechar parênteses."); return get; } if (/^\d|^\.\d/.test(t)) return () => Number(t); if (t[0] === '"') { const value = JSON.parse(t); return () => value; } if (t[0] === "'") { const value = t.slice(1, -1).replace(/\\(['\\])/g, "$1"); return () => value; } if (t === "true") return () => true; if (t === "false") return () => false; if (tokens[i] === "(") { if (!Object.hasOwn(functions, t)) throw new Error("Função não suportada."); i++; const args = []; if (tokens[i] !== ")") { do { args.push(parse(1)); if (tokens[i] !== ",") break; i++; } while (true); } if (tokens[i++] !== ")") throw new Error("Falta fechar função."); return () => functions[t](...args.map((get) => get())); } return () => valueAt(context, t); } finally { depth--; } } function parse(min) { let left = primary(); while ((precedence[tokens[i]] || 0) >= min) { const before = left, op = tokens[i++], right = parse(precedence[op] + 1); left = op === "&&" ? () => Boolean(before()) && Boolean(right()) : op === "||" ? () => Boolean(before()) || Boolean(right()) : () => binary(before(), op, right()); } return left; } if (!tokens.length) return ""; const result = parse(1); if (i !== tokens.length) throw new Error("Expressão inválida."); return result(); } export function timeValues(now = new Date(), timezone = "Europe/Lisbon") { const parts = Object.fromEntries( new Intl.DateTimeFormat("en-GB", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", weekday: "short", }) .formatToParts(now) .map((p) => [p.type, p.value]), ); return { time: `${parts.hour}:${parts.minute}`, seconds: parts.second, hours: parts.hour, minutes: parts.minute, date: `${parts.day}/${parts.month}/${parts.year}`, timestamp: Math.floor(now.getTime() / 1000), day: parts.day, month: parts.month, year: parts.year, dayOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf( parts.weekday, ), }; } const display = (value) => value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value); function splitFilters(raw) { const parts = []; let start = 0, quote = "", escaped = false; for (let i = 0; i < raw.length; i++) { const c = raw[i]; if (escaped) { escaped = false; continue; } if (c === "\\" && quote) { escaped = true; continue; } if (quote) { if (c === quote) quote = ""; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "|" && raw[i - 1] !== "|" && raw[i + 1] !== "|") { parts.push(raw.slice(start, i).trim()); start = i + 1; } } parts.push(raw.slice(start).trim()); return parts; } export function interpolate(text, context) { return String(text ?? "").replace(/\{\{([^{}]+)\}\}/g, (_, raw) => { const parts = splitFilters(raw); const path = parts.shift(); let value = path === "time" ? context.time : valueAt(context, path); if (value === undefined) { try { value = evaluate(path, { ...context, time: context.clock || context.time, }); } catch (error) { if (/^[\w.]+$/.test(path)) return ""; throw error; } } for (const filter of parts) { if (filter === "json") value = JSON.stringify(value); else if (filter === "uptime") { const seconds = Math.max(0, number(value)); value = `${Math.floor(seconds / 86400)}d ${Math.floor(seconds / 3600) % 24}h ${Math.floor(seconds / 60) % 60}m`; } else if (Object.hasOwn(functions, filter)) value = functions[filter](value); else throw new Error(`Filtro desconhecido: ${filter}`); } return display(value); }); } export function resolveObject(object, context) { if (typeof object === "string") return interpolate(object, context); if (Array.isArray(object)) return object.map((v) => resolveObject(v, context)); if (object && typeof object === "object") return Object.fromEntries( Object.entries(object) .filter(([key]) => !forbidden.has(key)) .map(([k, v]) => [k, resolveObject(v, context)]), ); return object; } export function compare(value, operator, other) { switch (operator) { case "greater": return number(value) > number(other); case "less": return number(value) < number(other); case "atLeast": return number(value) >= number(other); case "atMost": return number(value) <= number(other); case "contains": return String(value ?? "").includes(String(other)); case "startsWith": return String(value ?? "").startsWith(String(other)); case "endsWith": return String(value ?? "").endsWith(String(other)); case "exists": return value !== undefined && value !== null && value !== ""; case "notEquals": return String(value) !== String(other); case "equals": return String(value) === String(other); default: throw new Error("Comparação inválida."); } }