@seanox/aspect-js
Version:
full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, virtual paths, unit test and some more
262 lines (220 loc) • 12.1 kB
JavaScript
/**
* LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt,
* im Folgenden Seanox Software Solutions oder kurz Seanox genannt.
* Diese Software unterliegt der Version 2 der Apache License.
*
* Seanox aspect-js, fullstack for single page applications
* Copyright (C) 2023 Seanox Software Solutions
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*
* DESCRIPTION
* ----
* Expressions or the Expression Language (EL) is a simple access to the
* client-side JavaScript and thus to the models and components. In the
* expressions the complete JavaScript API is supported, which is enhanced with
* additional keywords, so that also the numerous arithmetic and logical
* operators can be used.
*
* The expression language can be used from the HTML element BODY on in the
* complete markup as free text, as well as in all attributes. Exceptions are
* the HTML elements STYLE and SCRIPT whose content is not supported by the
* expression language.
*
* @author Seanox Software Solutions
* @version 1.6.0 20230330
*/
(() => {
compliant("Expression");
compliant(null, window.Expression = {
/**
* Interprets the passed expression. In case of an error, the error is
* returned and no error is thrown. A serial can be specified
* optionally. The serial is an alias for caching compiled expressions.
* Without, the expressions are always compiled. The function uses
* variable parameters and has the following signatures:
* function(expression)
* function(serial, expression)
* @param serial
* @param expression
* @return the return value of the interpreted expression or an error if
* an error or error has occurred
*/
eval(...variants) {
let expression;
if (variants.length > 1)
expression = String(variants[1]);
else if (variants.length > 0)
expression = String(variants[0]);
let serial;
if (variants.length > 1
&& variants[0])
serial = String(variants[0]);
let script = serial ? _cache.get(serial) : null;
if (!script)
script = _parse(TYPE_MIXED, expression);
if (serial)
_cache.set(serial, script);
try {return Scripting.run(script);
} catch (error) {
console.error(error.message + "\n\t" + script);
return error.message + " in " + script;
}
}
});
/** Cache (expression/script) */
const _cache = new Map();
const TYPE_MIXED = 0;
const TYPE_LITERAL = 1;
const TYPE_TEXT = 2;
const TYPE_SCRIPT = 3;
const KEYWORDS = ["and", "&&", "or", "||", "not", "!",
"eq", "==", "eeq", "===", "ne", "!=", "nee", "!==", "lt", "<", "gt", ">", "le", "<=", "ge", ">=", "empty", "!",
"div", "/", "mod", "%"];
const PATTERN_KEYWORDS = new RegExp("(^|[^\\w\\.])("
+ KEYWORDS.filter((keyword, index) => index % 2 === 0).join("|")
+ ")(?=[^\\w\\.]|$)", "ig");
const _fill = (expression, patches) =>
expression.replace(/[\t\r](\d+)\n/g, (match, id) => patches[id]);
/**
* Analyzes and finds the components of an expression and creates a
* JavaScript from them. Created scripts are cached a reused as needed.
* @param expression
* @param depth
* @return the created JavaScript
*/
const _parse = (type, expression, patches = []) => {
switch (type) {
case TYPE_MIXED:
// replace all line breaks and merge them with a space, so the
// characters CR\r and LF\n can be used as internal markers
expression = expression.replace(/[\r\n]/g, " ").trim();
let structure = expression;
// find all places that contain scripts
structure = structure.replace(/\{\{(.*?)\}\}/g,
(match, script) => _parse(TYPE_SCRIPT, script, patches));
// find all places outside the detected script replacements at
// the beginning ^...\r and between \n...\r and at the end \n...$
structure = structure.replace(/((?:[^\n]+$)|(?:[^\n]+(?=\r)))/g,
(match, text) => _parse(TYPE_TEXT, text, patches));
// if everything is replaced, the expression must have the
// following structure, deviations are syntax errors
if (!structure.match(/^(\r(\d+)\n)*$/))
throw Error("Error in the expression structure\n\t" + expression);
// placeholders must be filled, since they were created
// recursively, they do not have to be filled recursively
structure = structure.replace(/(?:\r(\d+)\n)/g,
(match, placeholder) => "\r" + patches[placeholder] + "\n");
// masked quotation marks will be restored.
structure = structure.replaceAll("\r\\u0022\n", '\\"');
structure = structure.replaceAll("\r\\u0027\n", "\\'");
structure = structure.replaceAll("\r\\u0060\n", "\\`");
// splices still need to be made scriptable
structure = structure.replace(/(\n\r)+/g, " + ");
structure = structure.replace(/(^\r)|(\n$)/g, "");
// CR/LF of markers must be removed so that the end of line is not
// interpreted as separation of commands or logic: a\nb == a;b
structure = structure.replace(/[\r\n]+/g, " ");
return structure
case TYPE_TEXT:
expression = "\"" + expression + "\"";
case TYPE_LITERAL:
patches.push(expression);
return "\r" + (patches.length -1) + "\n";
case TYPE_SCRIPT:
expression = expression.trim();
if (!expression)
return "";
// Mask escaped quotes (single and double) so that they are not
// mistakenly found by the parser as delimiting string/phrase.
// rule: search for an odd number of slashes followed by quotes
expression = expression.replace(/(^|[^\\])((?:\\{2})*)(\\\")/g, "$1$2\r\\u0022\n");
expression = expression.replace(/(^|[^\\])((?:\\{2})*)(\\\')/g, "$1$2\r\\u0027\n");
expression = expression.replace(/(^|[^\\])((?:\\{2})*)(\\\`)/g, "$1$2\r\\u0060\n");
// Replace all literals "..." / '...' / `...` with placeholders.
// This simplifies analysis because text can contain anything
// and the parser would have to constantly distinguish between
// logic and text. If the literals are replaced by numeric
// placeholders, only logic remains. Important is a flexible
// processing, because the order of ', " and ` is not defined.
expression = expression.replace(/((['\"\`]).*?\2)/gs,
(match, literal) => _parse(TYPE_LITERAL, literal, patches));
// without literals, tabs have no relevance and can be replaced
// by spaces, and we have and additional internal marker
expression = expression.replace(/\t+/g, " ");
// mapping of keywords to operators
// IMPORTANT: KEYWORDS ARE CASE-INSENSITIVE
// and && empty ! div /
// eq == eeq === ge >=
// gt > le <= lt <
// mod % ne != nee !==
// not ! or ||
expression = expression.replace(PATTERN_KEYWORDS, (match, group1, group2) =>
group1 + KEYWORDS[KEYWORDS.indexOf(group2.toLowerCase()) +1]
);
// Keywords must be replaced by placeholders so that they are
// not interpreted as variables. Keywords are case-insensitive.
expression = expression.replace(/(^|[^\w\.])(true|false|null|undefined|new|instanceof|typeof)(?=[^\w\.]|$)/ig,
(match, group1 = "", group2 = "") =>
group1 + group2.toLowerCase());
// element expressions are translated into JavaScript
//
// #[element] -> document.getElementById(element)
// #element -> document.getElementById(element)
//
// The version with square brackets is for more complex element
// IDs that do not follow the JavaScript syntax for variables.
//
// The order is important because the complex element ID, if the
// target uses unique identifiers, may contain a # that should
// not be misinterpreted.
expression = expression.replace(/#\[([^\[\]]*)\]/g,
(match, element) => {
patches.push(_fill("document.getElementById(\"" + element + "\")", patches));
return "\r" + (patches.length -1) + "\n";
});
expression = expression.replace(/#([_a-z]\w*)/ig,
(match, element) => {
patches.push(_fill("document.getElementById(\"" + element + "\")", patches));
return "\r" + (patches.length -1) + "\n";
});
// (?...) tolerates the enclosed code. If an error occurs there,
// the expression will be false, but will not cause the error
// itself. This is convenient if you want check/use references
// or variables that do not yet exist or errors of methods are
// to be suppressed.
// To avoid complicated parsing of round brackets, bracket
// expressions that have no other round bracket expressions are
// iteratively replaced by placeholders \t...\n. At the end
// there should be no more brackets.
if (expression.match(/\(\s*\?/)) {
for (let counts = -1; counts < patches.length;) {
counts = patches.length;
expression = expression.replace(/(\([^\(\)]*\))/g, match => {
match = match.replace(/^\( *\?+ *(.*?) *\)$/s, (match, logic) =>
"_tolerate(()=>(" + logic + "))");
patches.push(_fill(match, patches));
return "\t" + (patches.length -1) + "\n";
});
}
}
// small optimization by merging spaces
expression = expression.replace(/ {2,}/g, " ");
patches.push(_fill(expression, patches));
return "\r" + (patches.length -1) + "\n";
default:
throw new Error("Unexpected script type");
}
};
})();