mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-09 02:23:30 +01:00
new code editor
This commit is contained in:
314
packages/noodl-runtime/src/expression-evaluator.js
Normal file
314
packages/noodl-runtime/src/expression-evaluator.js
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Expression Evaluator
|
||||
*
|
||||
* Compiles JavaScript expressions with access to Noodl globals
|
||||
* and tracks dependencies for reactive updates.
|
||||
*
|
||||
* Features:
|
||||
* - Full Noodl.Variables, Noodl.Objects, Noodl.Arrays access
|
||||
* - Math helpers (min, max, cos, sin, etc.)
|
||||
* - Dependency detection and change subscription
|
||||
* - Expression versioning for future compatibility
|
||||
* - Caching of compiled functions
|
||||
*
|
||||
* @module expression-evaluator
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Model = require('./model');
|
||||
|
||||
// Expression system version - increment when context changes
|
||||
const EXPRESSION_VERSION = 1;
|
||||
|
||||
// Cache for compiled functions
|
||||
const compiledFunctionsCache = new Map();
|
||||
|
||||
// Math helpers to inject into expression context
|
||||
const mathHelpers = {
|
||||
min: Math.min,
|
||||
max: Math.max,
|
||||
cos: Math.cos,
|
||||
sin: Math.sin,
|
||||
tan: Math.tan,
|
||||
sqrt: Math.sqrt,
|
||||
pi: Math.PI,
|
||||
round: Math.round,
|
||||
floor: Math.floor,
|
||||
ceil: Math.ceil,
|
||||
abs: Math.abs,
|
||||
random: Math.random,
|
||||
pow: Math.pow,
|
||||
log: Math.log,
|
||||
exp: Math.exp
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect dependencies in an expression string
|
||||
* Returns { variables: string[], objects: string[], arrays: string[] }
|
||||
*
|
||||
* @param {string} expression - The JavaScript expression to analyze
|
||||
* @returns {{ variables: string[], objects: string[], arrays: string[] }}
|
||||
*
|
||||
* @example
|
||||
* detectDependencies('Noodl.Variables.isLoggedIn ? "Hi" : "Login"')
|
||||
* // Returns: { variables: ['isLoggedIn'], objects: [], arrays: [] }
|
||||
*/
|
||||
function detectDependencies(expression) {
|
||||
const dependencies = {
|
||||
variables: [],
|
||||
objects: [],
|
||||
arrays: []
|
||||
};
|
||||
|
||||
// Remove strings to avoid false matches
|
||||
const exprWithoutStrings = expression
|
||||
.replace(/"([^"\\]|\\.)*"/g, '""')
|
||||
.replace(/'([^'\\]|\\.)*'/g, "''")
|
||||
.replace(/`([^`\\]|\\.)*`/g, '``');
|
||||
|
||||
// Match Noodl.Variables.X or Noodl.Variables["X"] or Variables.X or Variables["X"]
|
||||
const variableMatches = exprWithoutStrings.matchAll(
|
||||
/(?:Noodl\.)?Variables\.([a-zA-Z_$][a-zA-Z0-9_$]*)|(?:Noodl\.)?Variables\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of variableMatches) {
|
||||
const varName = match[1] || match[2];
|
||||
if (varName && !dependencies.variables.includes(varName)) {
|
||||
dependencies.variables.push(varName);
|
||||
}
|
||||
}
|
||||
|
||||
// Match Noodl.Objects.X or Noodl.Objects["X"] or Objects.X or Objects["X"]
|
||||
const objectMatches = exprWithoutStrings.matchAll(
|
||||
/(?:Noodl\.)?Objects\.([a-zA-Z_$][a-zA-Z0-9_$]*)|(?:Noodl\.)?Objects\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of objectMatches) {
|
||||
const objId = match[1] || match[2];
|
||||
if (objId && !dependencies.objects.includes(objId)) {
|
||||
dependencies.objects.push(objId);
|
||||
}
|
||||
}
|
||||
|
||||
// Match Noodl.Arrays.X or Noodl.Arrays["X"] or Arrays.X or Arrays["X"]
|
||||
const arrayMatches = exprWithoutStrings.matchAll(
|
||||
/(?:Noodl\.)?Arrays\.([a-zA-Z_$][a-zA-Z0-9_$]*)|(?:Noodl\.)?Arrays\[["']([^"']+)["']\]/g
|
||||
);
|
||||
for (const match of arrayMatches) {
|
||||
const arrId = match[1] || match[2];
|
||||
if (arrId && !dependencies.arrays.includes(arrId)) {
|
||||
dependencies.arrays.push(arrId);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Noodl context object for expression evaluation
|
||||
*
|
||||
* @param {Model.Scope} [modelScope] - Optional model scope (defaults to global Model)
|
||||
* @returns {Object} Noodl context with Variables, Objects, Arrays accessors
|
||||
*/
|
||||
function createNoodlContext(modelScope) {
|
||||
const scope = modelScope || Model;
|
||||
|
||||
// Get the global variables model
|
||||
const variablesModel = scope.get('--ndl--global-variables');
|
||||
|
||||
return {
|
||||
Variables: variablesModel ? variablesModel.data : {},
|
||||
Objects: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'symbol') return undefined;
|
||||
const obj = scope.get(prop);
|
||||
return obj ? obj.data : undefined;
|
||||
}
|
||||
}
|
||||
),
|
||||
Arrays: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'symbol') return undefined;
|
||||
const arr = scope.get(prop);
|
||||
return arr ? arr.data : undefined;
|
||||
}
|
||||
}
|
||||
),
|
||||
Object: scope
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile an expression string into a callable function
|
||||
*
|
||||
* @param {string} expression - The JavaScript expression to compile
|
||||
* @returns {Function|null} Compiled function or null if compilation fails
|
||||
*
|
||||
* @example
|
||||
* const fn = compileExpression('min(10, 5) + 2');
|
||||
* const result = evaluateExpression(fn); // 7
|
||||
*/
|
||||
function compileExpression(expression) {
|
||||
const cacheKey = `v${EXPRESSION_VERSION}:${expression}`;
|
||||
|
||||
if (compiledFunctionsCache.has(cacheKey)) {
|
||||
return compiledFunctionsCache.get(cacheKey);
|
||||
}
|
||||
|
||||
// Build parameter list for the function
|
||||
const paramNames = ['Noodl', 'Variables', 'Objects', 'Arrays', ...Object.keys(mathHelpers)];
|
||||
|
||||
// Wrap expression in return statement with error handling
|
||||
const functionBody = `
|
||||
"use strict";
|
||||
try {
|
||||
return (${expression});
|
||||
} catch (e) {
|
||||
console.error('Expression evaluation error:', e.message);
|
||||
return undefined;
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const fn = new Function(...paramNames, functionBody);
|
||||
compiledFunctionsCache.set(cacheKey, fn);
|
||||
return fn;
|
||||
} catch (e) {
|
||||
console.error('Expression compilation error:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a compiled expression with the current context
|
||||
*
|
||||
* @param {Function|null} compiledFn - The compiled expression function
|
||||
* @param {Model.Scope} [modelScope] - Optional model scope
|
||||
* @returns {*} The result of the expression evaluation
|
||||
*/
|
||||
function evaluateExpression(compiledFn, modelScope) {
|
||||
if (!compiledFn) return undefined;
|
||||
|
||||
const noodlContext = createNoodlContext(modelScope);
|
||||
const mathValues = Object.values(mathHelpers);
|
||||
|
||||
try {
|
||||
// Pass Noodl context plus shorthand accessors
|
||||
return compiledFn(noodlContext, noodlContext.Variables, noodlContext.Objects, noodlContext.Arrays, ...mathValues);
|
||||
} catch (e) {
|
||||
console.error('Expression evaluation error:', e.message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in expression dependencies
|
||||
* Returns an unsubscribe function
|
||||
*
|
||||
* @param {{ variables: string[], objects: string[], arrays: string[] }} dependencies
|
||||
* @param {Function} callback - Called when any dependency changes
|
||||
* @param {Model.Scope} [modelScope] - Optional model scope
|
||||
* @returns {Function} Unsubscribe function
|
||||
*
|
||||
* @example
|
||||
* const deps = { variables: ['userName'], objects: [], arrays: [] };
|
||||
* const unsub = subscribeToChanges(deps, () => console.log('Changed!'));
|
||||
* // Later: unsub();
|
||||
*/
|
||||
function subscribeToChanges(dependencies, callback, modelScope) {
|
||||
const scope = modelScope || Model;
|
||||
const listeners = [];
|
||||
|
||||
// Subscribe to variable changes
|
||||
if (dependencies.variables.length > 0) {
|
||||
const variablesModel = scope.get('--ndl--global-variables');
|
||||
if (variablesModel) {
|
||||
const handler = (args) => {
|
||||
// Check if any of our dependencies changed
|
||||
if (dependencies.variables.some((v) => args.name === v || !args.name)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
variablesModel.on('change', handler);
|
||||
listeners.push(() => variablesModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to object changes
|
||||
for (const objId of dependencies.objects) {
|
||||
const objModel = scope.get(objId);
|
||||
if (objModel) {
|
||||
const handler = () => callback();
|
||||
objModel.on('change', handler);
|
||||
listeners.push(() => objModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to array changes
|
||||
for (const arrId of dependencies.arrays) {
|
||||
const arrModel = scope.get(arrId);
|
||||
if (arrModel) {
|
||||
const handler = () => callback();
|
||||
arrModel.on('change', handler);
|
||||
listeners.push(() => arrModel.off('change', handler));
|
||||
}
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
listeners.forEach((unsub) => unsub());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate expression syntax without executing
|
||||
*
|
||||
* @param {string} expression - The expression to validate
|
||||
* @returns {{ valid: boolean, error: string|null }}
|
||||
*
|
||||
* @example
|
||||
* validateExpression('1 + 1'); // { valid: true, error: null }
|
||||
* validateExpression('1 +'); // { valid: false, error: 'Unexpected end of input' }
|
||||
*/
|
||||
function validateExpression(expression) {
|
||||
try {
|
||||
new Function(`return (${expression})`);
|
||||
return { valid: true, error: null };
|
||||
} catch (e) {
|
||||
return { valid: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current expression system version
|
||||
* Used for migration when expression context changes
|
||||
*
|
||||
* @returns {number} Current version number
|
||||
*/
|
||||
function getExpressionVersion() {
|
||||
return EXPRESSION_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the compiled functions cache
|
||||
* Useful for testing or when context changes
|
||||
*/
|
||||
function clearCache() {
|
||||
compiledFunctionsCache.clear();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectDependencies,
|
||||
compileExpression,
|
||||
evaluateExpression,
|
||||
subscribeToChanges,
|
||||
validateExpression,
|
||||
createNoodlContext,
|
||||
getExpressionVersion,
|
||||
clearCache,
|
||||
EXPRESSION_VERSION
|
||||
};
|
||||
111
packages/noodl-runtime/src/expression-type-coercion.js
Normal file
111
packages/noodl-runtime/src/expression-type-coercion.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Expression Type Coercion
|
||||
*
|
||||
* Coerces expression evaluation results to match expected property types.
|
||||
* Ensures type safety when expressions are used for node properties.
|
||||
*
|
||||
* @module expression-type-coercion
|
||||
* @since 1.1.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Coerce expression result to expected property type
|
||||
*
|
||||
* @param {*} value - The value from expression evaluation
|
||||
* @param {string} expectedType - The expected type (string, number, boolean, color, enum, etc.)
|
||||
* @param {*} [fallback] - Fallback value if coercion fails
|
||||
* @param {Array} [enumOptions] - Valid options for enum type
|
||||
* @returns {*} Coerced value or fallback
|
||||
*
|
||||
* @example
|
||||
* coerceToType('42', 'number') // 42
|
||||
* coerceToType(true, 'string') // 'true'
|
||||
* coerceToType('#ff0000', 'color') // '#ff0000'
|
||||
*/
|
||||
function coerceToType(value, expectedType, fallback, enumOptions) {
|
||||
// Handle undefined/null upfront
|
||||
if (value === undefined || value === null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
switch (expectedType) {
|
||||
case 'string':
|
||||
return String(value);
|
||||
|
||||
case 'number': {
|
||||
const num = Number(value);
|
||||
// Check for NaN (includes invalid strings, NaN itself, etc.)
|
||||
return isNaN(num) ? fallback : num;
|
||||
}
|
||||
|
||||
case 'boolean':
|
||||
return !!value;
|
||||
|
||||
case 'color':
|
||||
return coerceToColor(value, fallback);
|
||||
|
||||
case 'enum':
|
||||
return coerceToEnum(value, fallback, enumOptions);
|
||||
|
||||
default:
|
||||
// Unknown types pass through as-is
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce value to valid color string
|
||||
*
|
||||
* @param {*} value - The value to coerce
|
||||
* @param {*} fallback - Fallback color
|
||||
* @returns {string} Valid color or fallback
|
||||
*/
|
||||
function coerceToColor(value, fallback) {
|
||||
const str = String(value);
|
||||
|
||||
// Validate hex colors: #RGB or #RRGGBB (case insensitive)
|
||||
if (/^#[0-9A-Fa-f]{3}$/.test(str) || /^#[0-9A-Fa-f]{6}$/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Validate rgb() or rgba() format
|
||||
if (/^rgba?\(/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Invalid color format
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce value to valid enum option
|
||||
*
|
||||
* @param {*} value - The value to coerce
|
||||
* @param {*} fallback - Fallback enum value
|
||||
* @param {Array} enumOptions - Valid enum options (strings or {value, label} objects)
|
||||
* @returns {string} Valid enum value or fallback
|
||||
*/
|
||||
function coerceToEnum(value, fallback, enumOptions) {
|
||||
if (!enumOptions) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const enumVal = String(value);
|
||||
|
||||
// Check if value matches any option
|
||||
const isValid = enumOptions.some((opt) => {
|
||||
if (typeof opt === 'string') {
|
||||
return opt === enumVal;
|
||||
}
|
||||
// Handle {value, label} format
|
||||
return opt.value === enumVal;
|
||||
});
|
||||
|
||||
return isValid ? enumVal : fallback;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
coerceToType
|
||||
};
|
||||
@@ -1,4 +1,21 @@
|
||||
const OutputProperty = require('./outputproperty');
|
||||
const { evaluateExpression } = require('./expression-evaluator');
|
||||
const { coerceToType } = require('./expression-type-coercion');
|
||||
|
||||
/**
|
||||
* Helper to check if a value is an expression parameter
|
||||
* @param {*} value - The value to check
|
||||
* @returns {boolean} True if value is an expression parameter
|
||||
*/
|
||||
function isExpressionParameter(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
value !== undefined &&
|
||||
typeof value === 'object' &&
|
||||
value.mode === 'expression' &&
|
||||
typeof value.expression === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all Nodes
|
||||
@@ -83,6 +100,63 @@ Node.prototype.registerInputIfNeeded = function () {
|
||||
//noop, can be overriden by subclasses
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate an expression parameter and return the coerced result
|
||||
*
|
||||
* @param {*} paramValue - The parameter value (might be an ExpressionParameter)
|
||||
* @param {string} portName - The input port name
|
||||
* @returns {*} The evaluated and coerced value (or original if not an expression)
|
||||
*/
|
||||
Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
|
||||
// Check if this is an expression parameter
|
||||
if (!isExpressionParameter(paramValue)) {
|
||||
return paramValue; // Simple value, return as-is
|
||||
}
|
||||
|
||||
const input = this.getInput(portName);
|
||||
if (!input) {
|
||||
return paramValue.fallback; // No input definition, use fallback
|
||||
}
|
||||
|
||||
try {
|
||||
// Evaluate the expression with access to context
|
||||
const result = evaluateExpression(paramValue.expression, this.context);
|
||||
|
||||
// Coerce to expected type
|
||||
const coercedValue = coerceToType(result, input.type, paramValue.fallback);
|
||||
|
||||
// Clear any previous expression errors
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'expression-error-' + portName
|
||||
);
|
||||
}
|
||||
|
||||
return coercedValue;
|
||||
} catch (error) {
|
||||
// Expression evaluation failed
|
||||
console.warn(`Expression evaluation failed for ${this.name}.${portName}:`, error);
|
||||
|
||||
// Show warning in editor
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'expression-error-' + portName,
|
||||
{
|
||||
showGlobally: true,
|
||||
message: `Expression error: ${error.message}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return fallback value
|
||||
return paramValue.fallback;
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.setInputValue = function (name, value) {
|
||||
// DEBUG: Track input value setting for HTTP node
|
||||
if (this.name === 'net.noodl.HTTP') {
|
||||
@@ -115,6 +189,9 @@ Node.prototype.setInputValue = function (name, value) {
|
||||
//Save the current input value. Save it before resolving color styles so delta updates on color styles work correctly
|
||||
this._inputValues[name] = value;
|
||||
|
||||
// Evaluate expression parameters before further processing
|
||||
value = this._evaluateExpressionParameter(value, name);
|
||||
|
||||
if (input.type === 'color' && this.context && this.context.styles) {
|
||||
value = this.context.styles.resolveColor(value);
|
||||
} else if (input.type === 'array' && typeof value === 'string') {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const difference = require('lodash.difference');
|
||||
|
||||
//const Model = require('./data/model');
|
||||
const ExpressionEvaluator = require('../../expression-evaluator');
|
||||
|
||||
const ExpressionNode = {
|
||||
name: 'Expression',
|
||||
@@ -26,6 +25,19 @@ const ExpressionNode = {
|
||||
internal.compiledFunction = undefined;
|
||||
internal.inputNames = [];
|
||||
internal.inputValues = [];
|
||||
|
||||
// New: Expression evaluator integration
|
||||
internal.noodlDependencies = { variables: [], objects: [], arrays: [] };
|
||||
internal.unsubscribe = null;
|
||||
},
|
||||
methods: {
|
||||
_onNodeDeleted: function () {
|
||||
// Clean up reactive subscriptions to prevent memory leaks
|
||||
if (this._internal.unsubscribe) {
|
||||
this._internal.unsubscribe();
|
||||
this._internal.unsubscribe = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.cachedValue;
|
||||
@@ -72,15 +84,31 @@ const ExpressionNode = {
|
||||
self._inputValues[name] = 0;
|
||||
});
|
||||
|
||||
/* if(value.indexOf('Vars') !== -1 || value.indexOf('Variables') !== -1) {
|
||||
// This expression is using variables, it should listen for changes
|
||||
this._internal.onVariablesChangedCallback = (args) => {
|
||||
this._scheduleEvaluateExpression()
|
||||
}
|
||||
// Detect dependencies for reactive updates
|
||||
internal.noodlDependencies = ExpressionEvaluator.detectDependencies(value);
|
||||
|
||||
Model.get('--ndl--global-variables').off('change',this._internal.onVariablesChangedCallback)
|
||||
Model.get('--ndl--global-variables').on('change',this._internal.onVariablesChangedCallback)
|
||||
}*/
|
||||
// Clean up old subscription
|
||||
if (internal.unsubscribe) {
|
||||
internal.unsubscribe();
|
||||
internal.unsubscribe = null;
|
||||
}
|
||||
|
||||
// Subscribe to Noodl global changes if expression uses them
|
||||
if (
|
||||
internal.noodlDependencies.variables.length > 0 ||
|
||||
internal.noodlDependencies.objects.length > 0 ||
|
||||
internal.noodlDependencies.arrays.length > 0
|
||||
) {
|
||||
internal.unsubscribe = ExpressionEvaluator.subscribeToChanges(
|
||||
internal.noodlDependencies,
|
||||
function () {
|
||||
if (!self.isInputConnected('run')) {
|
||||
self._scheduleEvaluateExpression();
|
||||
}
|
||||
},
|
||||
self.context && self.context.modelScope
|
||||
);
|
||||
}
|
||||
|
||||
internal.inputNames = Object.keys(internal.scope);
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
@@ -141,6 +169,33 @@ const ExpressionNode = {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'On False'
|
||||
},
|
||||
// New typed outputs for better downstream compatibility
|
||||
asString: {
|
||||
group: 'Typed Results',
|
||||
type: 'string',
|
||||
displayName: 'As String',
|
||||
getter: function () {
|
||||
const val = this._internal.cachedValue;
|
||||
return val !== undefined && val !== null ? String(val) : '';
|
||||
}
|
||||
},
|
||||
asNumber: {
|
||||
group: 'Typed Results',
|
||||
type: 'number',
|
||||
displayName: 'As Number',
|
||||
getter: function () {
|
||||
const val = this._internal.cachedValue;
|
||||
return typeof val === 'number' ? val : Number(val) || 0;
|
||||
}
|
||||
},
|
||||
asBoolean: {
|
||||
group: 'Typed Results',
|
||||
type: 'boolean',
|
||||
displayName: 'As Boolean',
|
||||
getter: function () {
|
||||
return !!this._internal.cachedValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
@@ -235,8 +290,19 @@ var functionPreamble = [
|
||||
' floor = Math.floor,' +
|
||||
' ceil = Math.ceil,' +
|
||||
' abs = Math.abs,' +
|
||||
' random = Math.random;'
|
||||
/* ' Vars = Variables = Noodl.Object.get("--ndl--global-variables");' */
|
||||
' random = Math.random,' +
|
||||
' pow = Math.pow,' +
|
||||
' log = Math.log,' +
|
||||
' exp = Math.exp;' +
|
||||
// Add Noodl global context
|
||||
'try {' +
|
||||
' var NoodlContext = (typeof Noodl !== "undefined") ? Noodl : (typeof global !== "undefined" && global.Noodl) || {};' +
|
||||
' var Variables = NoodlContext.Variables || {};' +
|
||||
' var Objects = NoodlContext.Objects || {};' +
|
||||
' var Arrays = NoodlContext.Arrays || {};' +
|
||||
'} catch (e) {' +
|
||||
' var Variables = {}, Objects = {}, Arrays = {};' +
|
||||
'}'
|
||||
].join('');
|
||||
|
||||
//Since apply cannot be used on constructors (i.e. new Something) we need this hax
|
||||
@@ -264,11 +330,19 @@ var portsToIgnore = [
|
||||
'ceil',
|
||||
'abs',
|
||||
'random',
|
||||
'pow',
|
||||
'log',
|
||||
'exp',
|
||||
'Math',
|
||||
'window',
|
||||
'document',
|
||||
'undefined',
|
||||
'Vars',
|
||||
'Variables',
|
||||
'Objects',
|
||||
'Arrays',
|
||||
'Noodl',
|
||||
'NoodlContext',
|
||||
'true',
|
||||
'false',
|
||||
'null',
|
||||
@@ -326,13 +400,43 @@ function updatePorts(nodeId, expression, editorConnection) {
|
||||
}
|
||||
|
||||
function evalCompileWarnings(editorConnection, node) {
|
||||
try {
|
||||
new Function(node.parameters.expression);
|
||||
const expression = node.parameters.expression;
|
||||
if (!expression) {
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate expression syntax
|
||||
const validation = ExpressionEvaluator.validateExpression(expression);
|
||||
|
||||
if (!validation.valid) {
|
||||
editorConnection.sendWarning(node.component.name, node.id, 'expression-compile-error', {
|
||||
message: e.message
|
||||
message: 'Syntax error: ' + validation.error
|
||||
});
|
||||
} else {
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
|
||||
// Optionally show detected dependencies as info (helpful for users)
|
||||
const deps = ExpressionEvaluator.detectDependencies(expression);
|
||||
const depCount = deps.variables.length + deps.objects.length + deps.arrays.length;
|
||||
|
||||
if (depCount > 0) {
|
||||
const depList = [];
|
||||
if (deps.variables.length > 0) {
|
||||
depList.push('Variables: ' + deps.variables.join(', '));
|
||||
}
|
||||
if (deps.objects.length > 0) {
|
||||
depList.push('Objects: ' + deps.objects.join(', '));
|
||||
}
|
||||
if (deps.arrays.length > 0) {
|
||||
depList.push('Arrays: ' + deps.arrays.join(', '));
|
||||
}
|
||||
|
||||
// This is just informational, not an error
|
||||
// Could be shown in a future info panel
|
||||
// For now, we'll just log it
|
||||
console.log('[Expression Node] Reactive dependencies detected:', depList.join('; '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user