///@INFO: SCRIPTS
// ******* LScript **************************
/**
* LScript allows to compile code during execution time having a clean context
* It adds some syntactic features and controls errors in a safe way
* @class LScript
* @constructor
*/
function LScript()
{
this.code = "function update(dt) {\n\n}";
this.exported_callbacks = []; //detects if there is a function with this name and exports it as a property
this.extracode = "";
this.extra_methods = null; //add object with methods here to attach methods
}
LScript.onerror = null; //global used to catch errors in scripts
LScript.eval = function(argv_names,code) { return eval("(function("+argv_names+"){\n"+code+"\n})"); }; //not used
LScript.catch_exceptions = false;
LScript.show_errors_in_console = true;
//compiles the string, tryes to keep the current state
LScript.prototype.compile = function( arg_vars, save_context_vars )
{
var argv_names = [];
var argv_values = [];
if(arg_vars)
{
for(var i in arg_vars)
{
argv_names.push(i);
argv_values.push( arg_vars[i]);
}
}
argv_names = argv_names.join(",");
var code = this.code;
code = LScript.expandCode( code );
var extra_code = "";
for(var i in this.exported_callbacks)
{
var callback_name = this.exported_callbacks[i];
extra_code += " if(typeof("+callback_name+") != 'undefined' && "+callback_name+" != window[\""+callback_name+"\"] ) this."+callback_name + " = "+callback_name+";\n";
}
code += extra_code;
this._last_executed_code = code;
var old_context = this._context;
if(!LScript.catch_exceptions)
{
this._class = new Function(argv_names, code);//<-- PARSING POINT HERE ***************************************
var context_function = LScript.applyToConstructor( this._class, argv_values, this.extra_methods ); //bind globals and methods to context
this._context = new context_function(); //<-- EXECUTION POINT HERE ***************************************
}
else
{
try
{
//LScript.eval(argv_names,code);
this._class = new Function(argv_names, code);
var context_function = LScript.applyToConstructor( this._class, argv_values, this.extra_methods ); //bind globals and methods to context
this._context = new context_function(); //<-- EXECUTION POINT HERE ***************************************
}
catch (err)
{
if(!this._class)
{
console.error("Parsing error in script\n" + err);
}
this._class = null;
this._context = null;
if(LScript.show_errors_in_console)
{
var error_line = LScript.computeLineFromError(err);
console.error("Error in script\n" + err);
if( console.groupCollapsed )
{
console.groupCollapsed("Error line: " + error_line + " Watch code");
LScript.showCodeInConsole( this._last_executed_code, error_line );
console.groupEnd();
}
else
console.error("Error line: " + error_line);
}
if(this.onerror)
this.onerror(err, this._last_executed_code);
if(LScript.onerror)
LScript.onerror(err, this._last_executed_code, this);
return false;
}
}
if(save_context_vars && old_context)
{
for(var i in old_context)
if( this._context[i] !== undefined && old_context[i] && old_context[i].constructor !== Function && (!this._context[i] || this._context[i].constructor !== Function) )
this._context[i] = old_context[i];
}
return true;
}
//does this script contains this method?
LScript.prototype.hasMethod = function(name)
{
if(!this._context || !this._context[name] || typeof(this._context[name]) != "function")
return false;
return true;
}
//argv must be an array with parameters, unless skip_expand is true
LScript.prototype.callMethod = function( name, argv, expand_parameters, parent_object )
{
if(!this._context || !this._context[name])
return;
if(!LScript.catch_exceptions)
{
//call expanding parameters
if(argv && argv.constructor === Array && expand_parameters)
return this._context[name].apply(this._context, argv);
//call without expanding parameters
return this._context[name].call(this._context, argv);
}
try
{
//call expanding parameters
if(argv && argv.constructor === Array && expand_parameters)
return this._context[name].apply(this._context, argv);
//call without expanding parameters
return this._context[name].call(this._context, argv);
}
catch(err)
{
//catch error in script, detect line and show console info
var error_line = LScript.computeLineFromError(err);
var parent_info = "";
if (parent_object && parent_object.toInfoString )
parent_info = " from " + parent_object.toInfoString();
console.error("Error from function " + name + parent_info + ": ", err.toString());
if( console.groupCollapsed )
{
console.groupCollapsed("Error line: " + error_line + " Watch code");
LScript.showCodeInConsole( this._last_executed_code, error_line );
console.groupEnd();
}
else
console.error("Error line: " + error_line);
if(this.onerror)
this.onerror({ error: err, msg: err.toString(), line: error_line, lscript: this, code: this._last_executed_code, method_name: name });
}
}
//Given a constructor, it attaches several global arguments and methods (from kybernetikos in stackoverflow)
LScript.applyToConstructor = function(constructor, argArray, methods) {
var args = [null].concat(argArray);
if(methods)
for(var i in methods)
Object.defineProperty( constructor.prototype, i, { value: methods[i], enumerable: true });
var factoryFunction = constructor.bind.apply(constructor, args);
return factoryFunction;
}
//dumps all the code to the console
LScript.showCodeInConsole = function( code, error_line)
{
if(!code)
return;
var lines = code.split("\n");
var gutter_style = "display: inline-block; width: 40px; background-color:#999; color: white;";
for(var i = 0; i < lines.length; i++ )
if(i == error_line)
console.log("%c "+i+". " + lines[i], "background-color: #A33; color: #FAA;" );
else
console.log("%c "+i+". ", gutter_style, lines[i] );
}
//remove comments and trims empty lines
LScript.cleanCode = function(code)
{
if(!code)
return "";
/* this should be removed
I write this to test this func using LScript.cleanCode( LScript.cleanCode.toString() );
*/
//var rx = /(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(\/\/.*)/g;
var rx = /\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/g;
var code = code.replace( rx ,"");
var lines = code.split("\n");
var result = [];
for(var i = 0; i < lines.length; ++i)
{
var line = lines[i];
var pos = line.indexOf("//");
if(pos != -1 && line.substr(0,pos).indexOf("\"") == -1) //avoid removing lines with comments inside strings
line = line.substr(0,pos);
line = line.trim();
if(line.length)
result.push(line);
}
return result.join("\n");
}
// Adds some extra features to JS like:
// - support for multiline strings (this feature was introduced in ES6 but in case is not supported)
// - the use of private or public in variables.
LScript.expandCode = function(code)
{
if(!code)
return "";
//allow support to multiline strings
if( code.indexOf("'''") != -1 )
{
var lines = code.split("'''");
code = "";
for(var i = 0; i < lines.length; i++)
{
if(i % 2 == 0)
{
code += lines[i];
continue;
}
code += '"' + lines[i].split("\n").join("\\n\\\n") + '"';
}
}
//allow to use public var foo = 10;
var lines = code.split("\n");
var update = false;
for(var i = 0; i < lines.length; ++i)
{
var line = lines[i].trim();
var pos = line.indexOf(" ");
var first_word = line.substr(0,pos);
//all this horrendous code to parse "public var name : type = value;" and all the possible combinations
if( first_word == "public" || first_word == "private" || first_word == "hidden" )
{
var index = line.indexOf("//");
if(index != -1)
line = line.substr(0,index); //remove one-line comments
var index = line.lastIndexOf(";");
if(index != -1)
line = line.substr(0,index); //remove semicolon
var t = line.split(" ");
if( t[1] != 'var')
continue;
var text = line;
var type = null;
var value = "undefined";
var equal_index = text.indexOf("=");
if( equal_index != -1 )
{
value = text.substr( equal_index + 1 ).trim();
text = text.substr( 0, equal_index ).trim();
}
var colon_index = text.indexOf(":");
if(colon_index != -1)
{
type = text.substr( colon_index + 1 ).trim();
text = text.substr( 0, colon_index ).trim();
}
var keywords = text.split(" ");
var varname = keywords[2];
if(!varname)
continue;
var type_options = {};
if(type)
{
var array_index = type.indexOf('[]');
if( array_index != -1 )
{
type_options.type = LS.TYPES.ARRAY;
type_options.data_type = type.substr( 0, array_index );
if(!value || value === "undefined")
value = "[]";
}
else
{
type_options.type = type;
}
if( LS.Components[ type ] ) //for components
{
type_options.component_class = type;
type_options.type = LS.TYPES.COMPONENT;
if(!value)
value = "null";
}
else if( LS.ResourceClasses[ type ] ) //for resources
{
type_options.resource_classname = type;
type_options.type = LS.TYPES.RESOURCE;
if(!value)
value = "null";
}
else if( type == "int" || type == "integer")
{
type_options.step = 1;
type_options.type = LS.TYPES.NUMBER;
if(!value)
value = 0;
}
}
if( keywords[0] == "private" || keywords[0] == "hidden" )
type_options.widget = "null";
lines[i] = "this.createProperty('" + varname + "'," + value + ", "+JSON.stringify( type_options )+" );";
update = true;
}
}
if(update)
code = lines.join("\n");
return code;
}
// In case of error inside the scripts, tries to determine the error line (not as easy as it seems)
// Doesnt work in all cases
LScript.computeLineFromError = function( err )
{
if(err.lineNumber !== undefined)
{
return err.lineNumber;
}
else if(err.stack)
{
var lines = err.stack.split("\n");
var line = lines[1].trim();
if(line.indexOf("(native)") != -1)
return -1;
var tokens = line.split(" ");
var pos = line.lastIndexOf(":");
var pos2 = line.lastIndexOf(":",pos-1);
var num = parseInt( line.substr(pos2+1,pos-pos2-1) );
var ch = parseInt( line.substr(pos+1, line.length - 2 - pos) );
if(tokens[1] == "Object.CodingModule.eval")
return -1;
if (line.indexOf("LScript") != -1 || line.indexOf("<anonymous>") != -1 )
num -= 3; //ignore the header lines of the LScript class
return num;
}
return -1;
}
global.LScript = LScript;