mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 23:00:56 +01:00
473 lines
10 KiB
JavaScript
473 lines
10 KiB
JavaScript
/**
|
|
* EGroupware eTemplate2 - A simple PHP expression parser written in JS
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link http://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
* @copyright Stylite 2011
|
|
* @version $Id$
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
/*egw:uses
|
|
et2_core_common;
|
|
*/
|
|
|
|
(function() {
|
|
|
|
var STATE_DEFAULT = 0;
|
|
var STATE_ESCAPED = 1;
|
|
var STATE_CURLY_BRACE_OPEN = 2;
|
|
var STATE_EXPECT_CURLY_BRACE_CLOSE = 3;
|
|
var STATE_EXPECT_RECT_BRACE_CLOSE = 4;
|
|
var STATE_EXPR_BEGIN = 5;
|
|
var STATE_EXPR_END = 6;
|
|
|
|
function _throwParserErr(_p, _err)
|
|
{
|
|
throw("Syntax error while parsing '" + _p.expr + "' at " +
|
|
_p.pos + ", " + _err);
|
|
}
|
|
|
|
function _php_parseDoubleQuoteString(_p, _tree)
|
|
{
|
|
// Extract all PHP variables from the string
|
|
var state = STATE_DEFAULT;
|
|
var str = "";
|
|
|
|
while (_p.pos < _p.expr.length)
|
|
{
|
|
// Read the current char and then increment the parser position by
|
|
// one
|
|
var c = _p.expr.charAt(_p.pos++);
|
|
|
|
switch (state)
|
|
{
|
|
|
|
case STATE_DEFAULT:
|
|
case STATE_CURLY_BRACE_OPEN:
|
|
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
state = STATE_ESCAPED;
|
|
break;
|
|
|
|
case '$':
|
|
// check for '$$' as used in placeholder syntax, it is NOT expanded and returned as is
|
|
if (_p.expr.charAt(_p.pos) == "$" && state == STATE_DEFAULT)
|
|
{
|
|
_p.pos++;
|
|
str += '$$';
|
|
break;
|
|
}
|
|
// check for '$' as last char, as in PHP "test$" === 'test$', $ as last char is NOT expanded
|
|
if (_p.pos == _p.expr.length)
|
|
{
|
|
str += '$';
|
|
break;
|
|
}
|
|
// check for regular expression "/ $/"
|
|
if (_p.expr.charAt(_p.pos) == '/' && _p.expr.charAt(0) == '/')
|
|
{
|
|
str += '$';
|
|
break;
|
|
}
|
|
if (str)
|
|
{
|
|
_tree.push(str); str = "";
|
|
}
|
|
|
|
// Support for the ${[expr] sytax
|
|
if (_p.expr.charAt(_p.pos) == "{" && state != STATE_CURLY_BRACE_OPEN)
|
|
{
|
|
state = STATE_CURLY_BRACE_OPEN;
|
|
_p.pos++;
|
|
}
|
|
|
|
if (state == STATE_CURLY_BRACE_OPEN)
|
|
{
|
|
_tree.push(_php_parseVariable(_p));
|
|
state = STATE_EXPECT_CURLY_BRACE_CLOSE;
|
|
}
|
|
else
|
|
{
|
|
_tree.push(_php_parseVariable(_p));
|
|
}
|
|
|
|
break;
|
|
|
|
case '{':
|
|
state = STATE_CURLY_BRACE_OPEN;
|
|
break;
|
|
|
|
default:
|
|
if (state == STATE_CURLY_BRACE_OPEN)
|
|
{
|
|
str += '{';
|
|
state = STATE_DEFAULT;
|
|
}
|
|
str += c;
|
|
}
|
|
|
|
break;
|
|
|
|
case STATE_ESCAPED:
|
|
str += c;
|
|
break;
|
|
|
|
case STATE_EXPECT_CURLY_BRACE_CLOSE:
|
|
// When returning from the variableEx parser,
|
|
// the current char must be a "}"
|
|
if (c != "}")
|
|
{
|
|
_throwParserErr(_p, "expected '}', but got " + c);
|
|
}
|
|
|
|
state = STATE_DEFAULT;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Throw an error when reaching the end of the string but expecting
|
|
// "}"
|
|
if (state == STATE_EXPECT_CURLY_BRACE_CLOSE)
|
|
{
|
|
_throwParserErr(_p, "unexpected end of string, expected '}'");
|
|
}
|
|
|
|
// Push the last part of the string onto the syntax tree
|
|
if (state == STATE_CURLY_BRACE_OPEN)
|
|
{
|
|
str += "{";
|
|
}
|
|
|
|
if (str)
|
|
{
|
|
_tree.push(str);
|
|
}
|
|
}
|
|
|
|
// Regular expression which matches on PHP variable identifiers (without the $)
|
|
var PHP_VAR_PREG = /^([A-Za-z0-9_]+)/;
|
|
|
|
function _php_parseVariableName(_p)
|
|
{
|
|
// Extract the variable name form the expression
|
|
var vname = PHP_VAR_PREG.exec(_p.expr.substr(_p.pos));
|
|
|
|
if (vname)
|
|
{
|
|
// Increment the parser position by the length of vname
|
|
_p.pos += vname[0].length;
|
|
return {"variable": vname[0], "accessExpressions": []};
|
|
}
|
|
|
|
_throwParserErr(_p, "expected variable identifier.");
|
|
}
|
|
|
|
function _php_parseVariable(_p)
|
|
{
|
|
// Parse the first variable
|
|
var variable = _php_parseVariableName(_p);
|
|
|
|
// Parse all following variable access identifiers
|
|
var state = STATE_DEFAULT;
|
|
|
|
while (_p.pos < _p.expr.length)
|
|
{
|
|
var c = _p.expr.charAt(_p.pos++);
|
|
|
|
switch (state)
|
|
{
|
|
case STATE_DEFAULT:
|
|
switch (c)
|
|
{
|
|
case "[":
|
|
// Parse the expression inside the rect brace
|
|
variable.accessExpressions.push(_php_parseExpression(_p));
|
|
state = STATE_EXPECT_RECT_BRACE_CLOSE;
|
|
break;
|
|
|
|
default:
|
|
_p.pos--;
|
|
return variable;
|
|
}
|
|
break;
|
|
|
|
case STATE_EXPECT_RECT_BRACE_CLOSE:
|
|
if (c != "]")
|
|
{
|
|
_throwParserErr(_p, " expected ']', but got " + c);
|
|
}
|
|
|
|
state = STATE_DEFAULT;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Reads a string delimited by the char _delim or the regExp _delim from the
|
|
* current parser context and returns it.
|
|
*
|
|
* @param {object} _p parser contect
|
|
* @param {string} _delim delimiter
|
|
* @return {string} string read (or throws an exception)
|
|
*/
|
|
function _php_readString(_p, _delim)
|
|
{
|
|
var state = STATE_DEFAULT;
|
|
var str = "";
|
|
|
|
while (_p.pos < _p.expr.length)
|
|
{
|
|
var c = _p.expr.charAt(_p.pos++);
|
|
|
|
switch (state)
|
|
{
|
|
|
|
case STATE_DEFAULT:
|
|
if (c == "\\")
|
|
{
|
|
state = STATE_ESCAPED;
|
|
}
|
|
else if (c === _delim || (typeof _delim != "string" && _delim.test(c)))
|
|
{
|
|
return str;
|
|
}
|
|
else
|
|
{
|
|
str += c;
|
|
}
|
|
break;
|
|
|
|
case STATE_ESCAPED:
|
|
str += c;
|
|
state = STATE_DEFAULT;
|
|
break;
|
|
}
|
|
}
|
|
|
|
_throwParserErr(_p, "unexpected end of string while parsing string!");
|
|
}
|
|
|
|
function _php_parseExpression(_p)
|
|
{
|
|
var state = STATE_EXPR_BEGIN;
|
|
var result = null;
|
|
|
|
while (_p.pos < _p.expr.length)
|
|
{
|
|
var c = _p.expr.charAt(_p.pos++);
|
|
|
|
switch (state)
|
|
{
|
|
case STATE_EXPR_BEGIN:
|
|
switch(c)
|
|
{
|
|
// Skip whitespace
|
|
case " ": case "\n": case "\r": case "\t":
|
|
break;
|
|
|
|
case "\"":
|
|
result = [];
|
|
|
|
var p = _php_parser(_php_readString(_p, "\""));
|
|
_php_parseDoubleQuoteString(p, result);
|
|
state = STATE_EXPR_END;
|
|
break;
|
|
case "\'":
|
|
var result = _php_readString(_p, "'");
|
|
state = STATE_EXPR_END;
|
|
break;
|
|
case "$":
|
|
var result = _php_parseVariable(_p);
|
|
state = STATE_EXPR_END;
|
|
break;
|
|
default:
|
|
_p.pos--;
|
|
var result = _php_readString(_p, /[^A-Za-z0-9_#]/);
|
|
|
|
if (!result)
|
|
{
|
|
_throwParserErr(_p, "unexpected char " + c);
|
|
}
|
|
|
|
_p.pos--;
|
|
state = STATE_EXPR_END;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case STATE_EXPR_END:
|
|
switch(c)
|
|
{
|
|
// Skip whitespace
|
|
case " ": case "\n": case "\r": case "\t":
|
|
break;
|
|
|
|
default:
|
|
_p.pos--;
|
|
return result;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
_throwParserErr(_p, "unexpected end of string while parsing access expressions!");
|
|
}
|
|
|
|
function _php_parser(_expr)
|
|
{
|
|
return {
|
|
expr: _expr,
|
|
pos: 0
|
|
};
|
|
}
|
|
|
|
function _throwCompilerErr(_err)
|
|
{
|
|
throw("PHP to JS compiler error, " + _err);
|
|
}
|
|
|
|
function _php_compileVariable(_vars, _variable)
|
|
{
|
|
if (_vars.indexOf(_variable.variable) >= 0)
|
|
{
|
|
// Attach a "_" to the variable name as PHP variable names may start
|
|
// with numeric values
|
|
var result = "_" + _variable.variable;
|
|
|
|
// Create the access functions
|
|
for (var i = 0; i < _variable.accessExpressions.length; i++)
|
|
{
|
|
result += "[" +
|
|
_php_compileString(_vars, _variable.accessExpressions[i]) +
|
|
"]";
|
|
}
|
|
|
|
return '(typeof _'+_variable.variable+' != "undefined" && typeof '+result + '!="undefined" && ' + result + ' != null ? ' + result + ':"")';
|
|
}
|
|
|
|
_throwCompilerErr("Variable $" + _variable.variable + " is not defined.");
|
|
}
|
|
|
|
function _php_compileString(_vars, _string)
|
|
{
|
|
if (!(_string instanceof Array))
|
|
{
|
|
_string = [_string];
|
|
}
|
|
|
|
var parts = [];
|
|
var hasString = false;
|
|
for (var i = 0; i < _string.length; i++)
|
|
{
|
|
var part = _string[i];
|
|
|
|
if (typeof part == "string")
|
|
{
|
|
hasString = true;
|
|
// Escape all "'" and "\" chars and add the string to the parts array
|
|
parts.push("'" + part.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'");
|
|
}
|
|
else
|
|
{
|
|
parts.push(_php_compileVariable(_vars, part));
|
|
}
|
|
}
|
|
|
|
if (!hasString) // Force the result to be of the type string
|
|
{
|
|
parts.push('""');
|
|
}
|
|
|
|
return parts.join(" + ");
|
|
}
|
|
|
|
function _php_compileJSCode(_vars, _tree)
|
|
{
|
|
// Each tree starts with a "string"
|
|
return "return " + _php_compileString(_vars, _tree) + ";";
|
|
}
|
|
|
|
/**
|
|
* Function which compiles the given PHP string to a JS function which can be
|
|
* easily executed.
|
|
*
|
|
* @param _expr is the PHP string expression
|
|
* @param _vars is an array with variable names (without the PHP $).
|
|
* The parameters have to be passed to the resulting JS function in the same
|
|
* order.
|
|
*/
|
|
this.et2_compilePHPExpression = function(_expr, _vars)
|
|
{
|
|
if (typeof _vars == "undefined")
|
|
{
|
|
_vars = [];
|
|
}
|
|
|
|
try {
|
|
// Initialize the parser object and create the syntax tree for the given
|
|
// expression
|
|
var parser = _php_parser(_expr);
|
|
|
|
var syntaxTree = [];
|
|
|
|
// Parse the given expression as if it was a double quoted string
|
|
_php_parseDoubleQuoteString(parser, syntaxTree);
|
|
|
|
// Transform the generated syntaxTree into a JS string
|
|
var js = _php_compileJSCode(_vars, syntaxTree);
|
|
|
|
// Log the successfull compiling
|
|
egw.debug("log", "Compiled PHP " + _expr + " --> " + js);
|
|
}
|
|
catch(e) {
|
|
// if expression does NOT compile use it literally and log a warning, but not stop execution
|
|
egw.debug("warn", "Error compiling PHP "+_expr+" --> using it literally ("+
|
|
(typeof e == 'string' ? e : e.message)+")!");
|
|
return function(){ return _expr; };
|
|
}
|
|
// Prepate the attributes for the function constuctor
|
|
var attrs = [];
|
|
for (var i = 0; i < _vars.length; i++)
|
|
{
|
|
attrs.push("_" + _vars[i]);
|
|
}
|
|
attrs.push(js);
|
|
|
|
// Create the function and return it
|
|
return (Function.apply(Function, attrs));
|
|
};
|
|
|
|
}).call(window);
|
|
|
|
// Include this code in in order to test the above code
|
|
/*(function () {
|
|
var row = 10;
|
|
var row_cont = {"title": "Hello World!"};
|
|
var cont = {10: row_cont};
|
|
|
|
function test(_php, _res)
|
|
{
|
|
console.log(
|
|
et2_compilePHPExpression(_php, ["row", "row_cont", "cont"])
|
|
(row, row_cont, cont) === _res);
|
|
}
|
|
|
|
test("${row}[title]", "10[title]");
|
|
test("{$row_cont[title]}", "Hello World!");
|
|
test('{$cont["$row"][\'title\']}', "Hello World!");
|
|
test("$row_cont[${row}[title]]");
|
|
test("\\\\", "\\");
|
|
test("", "");
|
|
})();*/
|
|
|