egroupware/api/js/etemplate/et2_core_phpExpressionCompiler.js

353 lines
12 KiB
JavaScript
Raw Normal View History

/**
* 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 https://www.egroupware.org
* @author Andreas Stöckel
* @copyright EGroupware GmbH 2011-2021
*/
/*egw:uses
et2_core_common;
*/
import { egw } from "../jsapi/egw_global";
/**
* 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.
*/
export function et2_compilePHPExpression(_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));
}
const STATE_DEFAULT = 0;
const STATE_ESCAPED = 1;
const STATE_CURLY_BRACE_OPEN = 2;
const STATE_EXPECT_CURLY_BRACE_CLOSE = 3;
const STATE_EXPECT_RECT_BRACE_CLOSE = 4;
const STATE_EXPR_BEGIN = 5;
const 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 "\'":
result = _php_readString(_p, "'");
state = STATE_EXPR_END;
break;
case "$":
result = _php_parseVariable(_p);
state = STATE_EXPR_END;
break;
default:
_p.pos--;
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) + ";";
}
// 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("", "");
})();*/
//# sourceMappingURL=et2_core_phpExpressionCompiler.js.map