@seanox/aspect-js
Version:
full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, routing, paths, unit test and some more
314 lines (285 loc) • 13.9 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) 2025 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
* ----
* Expression language and composite JavaScript are two important components.
* Both are based on JavaScript enriched with macros. In addition, Composite
* JavaScript can be loaded at runtime and can itself load other Composite
* JavaScript scripts. Because in the end everything is based on a simple eval
* command, it was important to isolate the execution of the scripts so that
* internal methods and constants cannot be accessed unintentionally.
*/
(() => {
compliant("Scripting");
compliant(null, window.Scripting = {
/**
* As a special feature, Composite JavaScript supports macros.
*
* Macros are based on a keyword starting with a hash symbol followed by
* arguments separated by spaces. Macros end with the next line break, a
* semicolon or with the end of the file.
*
*
* #import
* ----
* Expects a space-separated list of composite modules whose path must
* be relative to the URL.
*
* #import io/api/connector and/much more
*
* Composite modules consist of the optional resources CSS, JS and HTML.
* The #import macro can only load CSS and JS. The behavior is the same
* as when loading composites in the markup. The server status 404 does
* not cause an error, because all resources of a composite are
* optional, also JavaScript. Server states other than 200 and 404 cause
* an error. CSS resources are added to the HEAD and lead to an error if
* no HEAD element exists in the DOM. Markup (HTML) is not loaded
* because no target can be set for the output. The macro can be used
* multiple in the Composite JavaScript.
*
*
* #export
* ----
* Expects a space-separated list of exports. Export are variables or
* constants in a module that are made usable for the global scope.
*
* #export connector and much more
*
* Primarily, an export argument is the name of the variable or constant
* in the module. Optionally, the name can be extended by an @ symbol to
* include the destination in the global scope.
*
* #export connector@io.example
*
* The macro #module is intended for debugging. It writes the following
* text as debug output to the console. The browser displays this output
* with source, which can then be used as an entry point for debugging.
*
*
* #module
* ----
* Expected a space-separated list of words to be output in the debug
* level of the browser console. The output is a string expression and
* supports the corresponding syntax.
*
* #module console debug output
*
*
* #use
* ----
* Expected to see a space-separated list of namespaces to create if
* they don't already exist.
*
* #use namespaces to be created
*
*
* (?...)
* ----
* Tolerant expressions are also a macro, although with different
* syntax. The logic enclosed in the parenthesis with question marks is
* executed fault-tolerantly. In case of an error the logic corresponds
* to the value false without causing an error itself, except for syntax
* errors.
*
* @param {string} script
* @returns {*} the return value from the script
*/
eval(script) {
if (typeof script !== "string")
throw new TypeError("Invalid data type");
// Performance is important here.
// The implementation parses and replaces macros in one pass.
// It was important to exclude literals and comments.
// - ignore: /*...*/
// - ignore: //...([\r\n]|$)
// - ignore: '...'
// - ignore: "..."
// - ignore: `...`
// - detect: (^|\W)#(import|export|module)\s+...(\W|$)
// - detect: \(\s*\?...\)
let pattern;
let brackets;
for (let cursor = 0; cursor < script.length; cursor++) {
let digit = script.charAt(cursor);
if (cursor >= script.length
&& !pattern)
continue;
// The macro for the tolerant logic is a bit more complicated,
// because round brackets have to be counted here. Therefore the
// parsing runs parallel to the other macros. In addition, the
// syntax is undefined by optional whitepsaces between ( and ?).
if (brackets < 0) {
if (digit === "?") {
brackets = 1;
let macro = "_tolerate(()=>";
script = script.substring(0, cursor) + macro + script.substring(cursor +1);
cursor += macro.length;
continue;
}
if (!digit.match(/\s/))
brackets = 0;
}
if (digit === "\\") {
cursor++
continue;
}
if (pattern) {
if (pattern === script.substring(cursor, cursor + pattern.length)
|| (pattern === "\n" && digit === "\r"))
pattern = null;
continue;
}
switch (digit) {
case "/":
digit = script.charAt(cursor +1);
if (digit === "/")
pattern = "\n";
if (digit === "*")
pattern = "*/";
continue;
case "(":
if (brackets > 0)
brackets++;
else brackets = -1;
continue;
case ")":
if (brackets <= 0)
continue;
if (--brackets > 0)
continue;
let macro = ")";
script = script.substring(0, cursor) + macro + script.substring(cursor);
cursor += macro.length;
continue;
case "\'":
case "\"":
case "\`":
pattern = digit;
continue;
case "#":
let string = script.substring(cursor -1, cursor +10);
let match = string.match(/(^|\W)(#(?:import|export|module|use))\s/);
if (match) {
let macro = match[2];
for (let offset = cursor +macro.length; offset <= script.length; offset++) {
string = script.charAt(offset);
if (!string.match(/[;\r\n]/)
&& offset < script.length)
continue;
let parameters = script.substring(cursor +macro.length, offset).trim();
switch (macro) {
case "#import":
if (!parameters.match(/^(\w+(\/\w+)*)(\s+(\w+(\/\w+)*))*$/))
throw new Error(("Invalid macro: #import " + parameters).trim());
const imports = parameters.split(/\s+/).map(entry => "\"" + entry + "\"");
macro = "_import(...[" + imports.join(",") + "])";
break;
case "#export":
const exports = [];
const pattern = /^([_a-z]\w*)(?:@((?:[_a-z]\w*)(?:\.[_a-z]\w*)*))?$/i;
parameters.split(/\s+/).forEach(entry => {
const match = entry.match(pattern);
if (!match)
throw new Error(("Invalid macro: #export " + parameters).trim());
parameters = [match[1], "\"" + match[1] + "\""];
if (match[2])
parameters.push("\"" + match[2] + "\"");
exports.push("[" + parameters.join(",") + "]");
});
macro = "_export(...[" + exports.join(",") + "])";
break;
case "#module":
macro = parameters.replace(/\\/g, "\\\\")
.replace(/`/g, "\\`").trim();
if (macro)
macro = "console.debug(`Module: " + macro + "`)";
break;
case "#use":
if (!parameters.match(/^([_a-z]\w*)(\.[_a-z]\w*)*(\s+([_a-z]\w*)(\.[_a-z]\w*)*)*$/i))
throw new Error(("Invalid macro: #use " + parameters).trim());
const uses = parameters.split(/\s+/).map(entry => "\"" + entry + "\"");
macro = "_use(...[" + uses.join(",") + "])";
break;
}
script = script.substring(0, cursor -1) + (match[1] || "")
+ macro + script.substring(offset);
cursor += macro.length;
break;
}
}
continue;
default:
continue;
}
}
return this.run(script);
},
/**
* Executes a script isolated in this context, so that no unwanted
* access to internals is possible.
* @param {string} script
* @returns {*} return value of the script, if available
*/
run(script) {
if (typeof script !== "string")
throw new TypeError("Invalid data type");
if (!script.trim())
return;
with (Composite.render.context)
return eval(script);
}
});
const _import = (...imports) => {
// Because it is an internal method, an additional validation of the
// exports as data structure was omitted.
imports.forEach(include =>
Composite.load(Composite.MODULES + "/" + include + ".js", true));
};
const _export = (...exports) => {
// Because it is an internal method, an additional validation of the
// exports as data structure was omitted.
exports.forEach(parameters => {
let context = window;
(parameters[2] ? parameters[2].split(/\./) : []).forEach(parameter => {
if (typeof context[parameter] === "undefined")
context[parameter] = {};
context = context[parameter]
});
const lookup = context[parameters[1]];
if (typeof lookup !== "undefined"
&& !(lookup instanceof Element)
&& !(lookup instanceof HTMLCollection))
throw new Error("Context for export is already in use: "
+ parameters[1] + (parameters[2] ? "@" + parameters[2] : ""));
context[parameters[1]] = parameters[0];
});
}
const _use = (...uses) => {
uses.forEach(use => Namespace.use(use));
}
const _tolerate = (invocation) => {
try {return invocation.call(window);
} catch (error) {
return false;
}
};
})();