UNPKG

@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

1,486 lines (1,310 loc) 205 kB
/** * 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 * ---- * General extension of the JavaScript API. * * @author Seanox Software Solutions * @version 1.6.0 20230326 */ // Compliant takes over the task that the existing JavaScript API can be // manipulated in a controlled way. Controlled means that errors occur when // trying to overwrite existing objects and functions. Originally, the mechanism // was removed after loading the page, but the feature has proven to be // convenient for other modules and therefore remains. // // In the code, the method is used in an unconventional form. // // compliant("Composite"); // compliant(null, window.Composite = {...}); // compliant("Object.prototype.ordinal"); // compliant(null, Object.prototype.ordinal = function() {...} // // This is only for the IDE so that syntax completion has a chance there. This // syntax will be simplified and corrected in the build process for the // releases. if (window.compliant !== undefined) throw new Error("JavaScript incompatibility detected for: compliant"); window.compliant = (context, payload) => { if (context === null || context === undefined) return payload; if (new Function(`return typeof ${context}`)() !== "undefined") throw new Error("JavaScript incompatibility detected for: " + context); return eval(`${context} = payload`); }; /** * Comparable to packages in other programming languages, namespaces can be used * for hierarchical structuring of components, resources and business logic. * Although packages are not a feature of JavaScript, they can be mapped at the * object level by concatenating objects into an object tree. Here, each level * of the object tree forms a namespace, which can also be considered a domain. * * As is typical for the identifiers of objects, namespaces also use letters, * numbers and underscores separated by a dot. As a special feature, arrays are * also supported. If a layer in the namespace uses an integer, this layer is * used as an array. */ (() => { compliant("Namespace", { /** Pattern for the namespace separator */ get PATTERN_NAMESPACE_SEPARATOR() {return /\./;}, /** Pattern for a valid namespace level at the beginning */ get PATTERN_NAMESPACE_LEVEL_START() {return /^[_a-z\$][\w\$]*$/i;}, /** Pattern for a valid namespace level */ get PATTERN_NAMESPACE_LEVEL() {return /^[\w\$]+$/;}, /** * Creates a namespace to the passed object, strings and numbers, if the * namespace contains arrays and the numbers can be used as index. * Levels of the namespace levels are separated by a dot. Levels can as * fragments also contain dots. Without arguments the global namespace * window is returned. * * The method has the following various signatures: * Namespace.use(); * Namespace.use(string); * Namespace.use(string, ...string|number); * Namespace.use(object); * Namespace.use(object, ...string|number); * * @param levels of the namespace * @return the created or already existing object(-level) * @throws An error occurs in case of invalid data types or syntax */ use(...levels) { if (levels.length <= 0) return window; _filter(...levels); let offset = levels.length; let namespace = null; if (levels.length > 0 && typeof levels[0] === "object") namespace = levels.shift(); offset -= levels.length; levels = levels.join("."); levels.split(Namespace.PATTERN_NAMESPACE_SEPARATOR).forEach((level, index, array) => { const pattern = index === 0 && namespace === null ? Namespace.PATTERN_NAMESPACE_LEVEL_START : Namespace.PATTERN_NAMESPACE_LEVEL; if (!level.match(pattern)) throw new Error(`Invalid namespace at level ${index +1}${level && level.trim() ? ": " + level.trim() : ""}`); // Composites use IDs which causes corresponding DOM objects // (Element) in the global namespace if there are no // corresponding data objects (models). Because namespaces are // based on data objects, if an element appears, we assume that // a data object does not exist and the recursive search is // aborted as unsuccessful. if (index === 0 && namespace === null) { namespace = _populate(namespace, level); if (namespace !== undefined && !(namespace instanceof Element)) return; namespace = window; } const item = _populate(namespace, level); const type = typeof item; if (type !== "undefined" && type !== "object") throw new TypeError(`Invalid namespace type at level ${index +1 +offset}: ${type}`); if (item === undefined || item === null || item instanceof Element) if (index < array.length -1 && array[index +1].match(/^\d+$/)) namespace[level] = []; else namespace[level] = {}; namespace = _populate(namespace, level); }); return namespace; }, /** * Creates a namespace with an initial value to the passed object, * strings and numbers, if the namespace contains arrays and the numbers * can be used as index. Levels of the namespace levels are separated by * a dot. Levels can as fragments also contain dots. Without arguments, * the global namespace window is used. * * The method has the following various signatures: * Namespace.create(string, value); * Namespace.create(string, ...string|number, value); * Namespace.create(object, value); * Namespace.create(object, ...string|number, value); * * @param levels of the namespace * @param value to initialize/set * @return the created or already existing object(-level) * @throws An error occurs in case of invalid data types or syntax */ create(...levels) { if (levels.length < 2) throw new Error("Invalid namespace for creation: Namespace and/or value is missing"); const value = levels.pop(); levels = _filter(...levels); const level = levels.pop(); const namespace = Namespace.use(...levels); if (namespace === null) return null; namespace[level] = value; return _populate(namespace, level); }, /** * Resolves a namespace and returns the determined object(-level). * If the namespace does not exist, undefined is returned. * * The method has the following various signatures: * Namespace.lookup(); * Namespace.lookup(string); * Namespace.lookup(string, ...string|number); * Namespace.lookup(object); * Namespace.lookup(object, ...string|number); * * @param levels of the namespace * @return the determined object(-level) * @throws An error occurs in case of invalid data types or syntax */ lookup(...levels) { if (levels.length <= 0) return window; _filter(...levels); let offset = levels.length; let namespace = null; if (levels.length > 0 && typeof levels[0] === "object") namespace = levels.shift(); offset -= levels.length; levels = levels.join("."); levels = levels.split(Namespace.PATTERN_NAMESPACE_SEPARATOR); for (let index = 0; index < levels.length; index++) { const level = levels[index]; const pattern = index +offset === 0 ? Namespace.PATTERN_NAMESPACE_LEVEL_START : Namespace.PATTERN_NAMESPACE_LEVEL; if (!level.match(pattern)) throw new Error(`Invalid namespace at level ${index +1 +offset}${level && level.trim() ? ": " + level.trim() : ""}`); // Composites use IDs which causes corresponding DOM objects // (Element) in the global namespace if there are no // corresponding data objects (models). Because namespaces are // based on data objects, if an element appears, we assume that // a data object does not exist and the recursive search is // aborted as unsuccessful. if (index === 0 && namespace === null) { namespace = _populate(namespace, level); if (namespace !== undefined) continue; namespace = window; } namespace = _populate(namespace, level); if (namespace === undefined || namespace === null) return namespace; if (namespace instanceof Element) return undefined; } return namespace; }, /** * Checks whether a namespace exists based on the passed object, strings * and numbers, if the namespace contains arrays and the numbers can be * used as index. Levels of the namespace chain are separated by a dot. * Levels can also be fragments that contain dots. Without arguments the * global namespace window is used. * * The method has the following various signatures: * Namespace.exists(); * Namespace.exists(string); * Namespace.exists(string, ...string|number); * Namespace.exists(object); * Namespace.exists(object, ...string|number); * * @param levels of the namespace * @return true if the namespace exists * @throws An error occurs in case of invalid levels or syntax */ exists(...levels) { if (levels.length < 1) return false; return Object.usable(Namespace.lookup(...levels)); } }); const _filter = (...levels) => { const chain = []; levels.forEach((level, index) => { if (index === 0 && typeof level !== "object" && typeof level !== "string") throw new TypeError(`Invalid namespace at level ${index +1}: ${typeof level}`); if (index === 0 && level === null) throw new TypeError(`Invalid namespace at level ${index +1}: null`); if (index > 0 && typeof level !== "string" && typeof level !== "number") throw new TypeError(`Invalid namespace at level ${index +1}: ${typeof level}`); level = typeof level === "string" ? level.split(Namespace.PATTERN_NAMESPACE_SEPARATOR) : [level]; chain.push(...level); }); return chain; }; const _populate = (namespace, level) => { if (namespace && namespace !== window) return namespace[level]; try {return eval(`typeof ${level} !== "undefined" ? ${level} : undefined`); } catch (error) { if (error instanceof ReferenceError && namespace === window) return window[level]; throw error; } } })(); /** * Enhancement of the JavaScript API * Modifies the method to support node and nodes as NodeList and Array. * If the option exclusive is used, existing children will be removed first. * @param node node(s) * @param exclusive existing children will be removed first */ (() => { const _appendChild = Element.prototype.appendChild; Element.prototype.appendChild = function(node, exclusive) { if (exclusive) this.innerHTML = ""; if (node instanceof Node) { _appendChild.call(this, node); } else if (Array.isArray(node) || node instanceof NodeList || (Symbol && Symbol.iterator && node && typeof node[Symbol.iterator])) { node = Array.from(node); for (let loop = 0; loop < node.length; loop++) _appendChild.call(this, node[loop]); } else _appendChild.call(this, node); }; })(); /** * Enhancement of the JavaScript API * Adds a static function to create an alphanumeric unique (U)UID with fixed size. * The quality of the ID is dependent of the length. * @param size optional, default is 16 */ compliant("Math.unique", (size) => { size = size || 16; if (size < 0) size = 16; let unique = ""; for (let loop = 0; loop < size; loop++) { const random = Math.floor(Math.random() * Math.floor(26)); if ((Math.floor(Math.random() *Math.floor(26))) % 2 === 0) unique += String(random % 10); else unique += String.fromCharCode(65 +random); } return unique; }); /** * Enhancement of the JavaScript API * Adds a static function to create a time based alphanumeric serial that is * chronologically sortable as text and contains the time and a counter if * serial are created at the same time. */ (() => { compliant("Math.serial", () => _serial.toString()); const _offset = -946684800000; const _serial = {timing:new Date().getTime() + _offset, number:0, toString() { const timing = new Date().getTime() + _offset; this.number = this.timing === timing ? this.number +1 : 0; this.timing = timing; const serial = this.timing.toString(36); const number = this.number.toString(36); return (serial.length.toString(36) + serial + number.length.toString(36) + number).toUpperCase(); }}; })(); /** * Enhancement of the JavaScript API * Creates a literal pattern for the specified text. * Metacharacters or escape sequences in the text thus lose their meaning. * @param text text to be literalized * @return a literal pattern for the specified text */ compliant("RegExp.quote", (text) => { if (!Object.usable(text)) return null; return String(text).replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); }); /** * Enhancement of the JavaScript API * Adds a capitalize function to the String objects. */ compliant("String.prototype.capitalize", function() { if (this.length <= 0) return this; return this.charAt(0).toUpperCase() + this.slice(1); }); /** * Enhancement of the JavaScript API * Adds an uncapitalize function to the String objects. */ compliant("String.prototype.uncapitalize", function() { if (this.length <= 0) return this; return this.charAt(0).toLowerCase() + this.slice(1); }); /** * Enhancement of the JavaScript API * Adds a function for encoding the string objects in hexadecimal code. */ compliant("String.prototype.encodeHex", function() { let result = ""; for (let loop = 0; loop < this.length; loop++) { let digit = Number(this.charCodeAt(loop)).toString(16).toUpperCase(); while (digit.length < 2) digit = "0" + digit; result += digit; } return "0x" + result; }); /** * Enhancement of the JavaScript API * Adds a function for decoding hexadecimal code to the string objects. */ compliant("String.prototype.decodeHex", function() { let text = this; if (text.match(/^0x/)) text = text.substring(2); let result = ""; for (let loop = 0; loop < text.length; loop += 2) result += String.fromCharCode(parseInt(text.substring(loop, 2), 16)); return result; }); /** * Enhancement of the JavaScript API * Adds a method for encoding Base64. */ compliant("String.prototype.encodeBase64", function() { try { return btoa(encodeURIComponent(this).replace(/%([0-9A-F]{2})/g, (match, code) => String.fromCharCode("0x" + code))); } catch (error) { throw new Error("Malformed character sequence"); } }); /** * Enhancement of the JavaScript API * Adds a method for decoding Base64. */ compliant("String.prototype.decodeBase64", function() { try { return decodeURIComponent(atob(this).split("").map((code) => "%" + ("00" + code.charCodeAt(0).toString(16)).slice(-2)).join("")); } catch (error) { throw new Error("Malformed character sequence"); } }); /** * Enhancement of the JavaScript API * Adds an HTML encode function to the String objects. */ compliant("String.prototype.encodeHtml", function() { const element = document.createElement("div"); element.textContent = this; return element.innerHTML; }); /** * Enhancement of the JavaScript API * Adds a method for calculating a hash value. */ compliant("String.prototype.hashCode", function() { if (this.hash !== undefined && this.hash !== 0) return this.hash; this.hash = 0; let hops = 0; for (let loop = 0; loop < this.length; loop++) { const temp = 31 *this.hash +this.charCodeAt(loop); if (!Number.isSafeInteger(temp)) { hops++; this.hash = Number.MAX_SAFE_INTEGER -this.hash +this.charCodeAt(loop); } else this.hash = temp; } this.hash = Math.abs(this.hash).toString(36); this.hash = this.hash.length.toString(36) + this.hash; this.hash = (this.hash + hops.toString(36)).toUpperCase(); return this.hash; }); /** * Enhancement of the JavaScript API * Adds a decoding of slash sequences (control characters). */ compliant("String.prototype.unescape", function() { let text = this .replace(/\r/g, "\\r") .replace(/\n/g, "\\n") .replace(/^(["'])/, "\$1") .replace(/([^\\])((?:\\{2})*)(?=["'])/g, "$1$2\\"); return eval(`"${text}"`); }); /** * Enhancement of the JavaScript API * Adds a property to get the UID for the window instance. */ compliant("window.serial"); Object.defineProperty(window, "serial", { value: Math.serial() }); /** * Enhancement of the JavaScript API * Adds a property to get the context path. * The context path is a part of the request URI and can be compared with the * current working directory. */ compliant("window.location.pathcontext"); Object.defineProperty(window.location, "pathcontext", { value: window.location.pathname.replace(/\/([^\/]*\.[^\/]*){0,}$/g, "") || "/" }); /** * Enhancement of the JavaScript API * Adds a method to combine paths to a new one. * The result will always start with a slash but ends without it. */ compliant("window.location.combine", (...paths) => "/" + paths.join("/") .replace(/[\/\\]+/g, "/") .replace(/(^\/+)|(\/+$)/g, "")); /** * DataSource is a NoSQL approach to data storage based on XML data in * combination with multilingual data separation, optional aggregation and * transformation. * A combination of the approaches of a read only DBS and a CMS. * * Files are defined by locator. * A locator is a URL (xml://... or xslt://...) that is used absolute and * relative to the DataSource directory, but does not contain a locale * (language specification) in the path. The locale is determined automatically * for the language setting of the browser, or if this is not supported, the * standard from the locales.xml in the DataSource directory is used. * * DataSource is based on static data. * Therefore, the implementation uses a cache to minimize network access. * * The data is queried with XPath, the result can be concatenated and * aggregated and the result can be transformed with XSLT. * * @author Seanox Software Solutions * @version 1.6.0 20230317 */ (() => { /** Path of the DataSource for: data (sub-directory of work path) */ const DATA = window.location.combine(window.location.pathcontext, "/data"); /** * Pattern for a DataSource locator, based on the URL syntax but only * the parts schema and path are used. A path segment begins with a word * character _ a-z 0-9, optionally more word characters and additionally * - can follow, but can not end with the - character. Paths are * separated by the / character. */ const PATTERN_LOCATOR = /^(?:([a-z]+):\/+)(\/((\w+)|(\w+(\-+\w+)+)))+$/; /** Pattern to detect JavaScript elements */ const PATTERN_JAVASCRIPT = /^\s*text\s*\/\s*javascript\s*$/i; /** Pattern to detect a word (_ 0-9 a-z A-Z -) */ const PATTERN_WORD = /(^\w+$)|(^((\w+\-+(?=\w))+)\w*$)/; /** Constant for attribute type */ const ATTRIBUTE_TYPE = "type"; compliant("DataSource", { /** The currently used language. */ get locale() {return DataSource.locales ? DataSource.locales.selection : null;}, /** * Changes the localization of the DataSource. * Only locales from locales.xml can be used, other values cause an * error. * @param locale * @throws Error in the case of invalid locales */ localize(locale) { if (!DataSource.data || !DataSource.locales) throw new Error("Locale not available"); if (typeof locale !== "string") throw new TypeError("Invalid locale: " + typeof locale); locale = (locale || "").trim().toLowerCase(); if (!locale || !DataSource.locales.includes(locale)) throw new Error("Locale not available"); DataSource.locales.selection = locale; }, /** * Transforms an XMLDocument based on a passed stylesheet. * The data and the stylesheet can be passed as Locator, XMLDocument an * in mix. The result as a DocumentFragment. Optionally, a meta object * or a map with parameters for the XSLTProcessor can be passed. * @param xml locator or XMLDocument * @param style locator or XMLDocument * @param meta optional parameters for the XSLTProcessor * @return the transformation result as a DocumentFragment */ transform(xml, style, meta) { if (typeof xml === "string" && xml.match(PATTERN_LOCATOR)) xml = DataSource.fetch(xml); if (typeof style === "string" && style.match(PATTERN_LOCATOR)) style = DataSource.fetch(style); if (!(xml instanceof XMLDocument)) throw new TypeError("Invalid xml document"); if (!(style instanceof XMLDocument)) throw new TypeError("Invalid xml stylesheet"); const processor = new XSLTProcessor(); processor.importStylesheet(style); if (meta && typeof meta === "object") { const set = typeof meta[Symbol.iterator] !== "function" ? Object.entries(meta) : meta for (const [key, value] of set) if (typeof meta[key] !== "function") processor.setParameter(null, key, value); } // Attribute escape converts text to HTML. Without, HTML tag symbols // < and > are masked and output as text. let escape = xml.evaluate("string(/*/@escape)", xml, null, XPathResult.ANY_TYPE, null).stringValue; escape = !!escape.match(/^yes|on|true|1$/i); // Workaround for some browsers, e.g. MS Edge, if they have problems // with !DOCTYPE + !ENTITY. Therefore the document is copied so that // the DOCTYPE declaration is omitted. let result = processor.transformToDocument(xml.clone()); let nodes = result.querySelectorAll(escape ? "*" : "*[escape]"); nodes.forEach((node) => { if (escape || (node.getAttribute("escape") || "on").match(/^yes|on|true|1$/i)) { const content = node.innerHTML; if (content.indexOf("<") < 0 && content.indexOf(">") < 0) node.innerHTML = node.textContent; } node.removeAttribute("escape"); }); // JavaScript are automatically changed to composite/javascript // during the import. Therefore, imported scripts are not executed // directly, but only by the renderer. This is important in // combination with ATTRIBUTE_CONDITION. nodes = result.querySelectorAll("script[type],script:not([type])"); nodes.forEach((node) => { if (!node.hasAttribute(ATTRIBUTE_TYPE) || (node.getAttribute(ATTRIBUTE_TYPE) || "").match(PATTERN_JAVASCRIPT)) node.setAttribute("type", "composite/javascript"); }); nodes = result.childNodes; if (result.body) nodes = result.body.childNodes; else if (result.firstChild && result.firstChild.nodeName.match(/^transformiix\b/i)) nodes = result.firstChild.childNodes; const fragment = document.createDocumentFragment(); nodes = Array.from(nodes); for (let loop = 0; loop < nodes.length; loop++) fragment.appendChild(nodes[loop]); return fragment; }, /** * Fetch the data to a locator as XMLDocument. * Optionally the data can be transformed via XSLT, for which a meta * object or map with parameters for the XSLTProcessor can be passed. * When using the transformation, the return type changes to a * DocumentFragment. * @param locators locator * @param transform locator of the transformation style * With the boolean true, the style is derived from the locator by * using the file extension xslt. * @param meta optional parameters for the XSLTProcessor * @return the fetched data as XMLDocument or as DocumentFragment, if * the transformation is used * @throws Error in the case of invalid arguments */ fetch(locator, transform, meta) { if (typeof locator !== "string" || !locator.match(PATTERN_LOCATOR)) throw new Error("Invalid locator: " + String(locator)); const type = locator.match(PATTERN_LOCATOR)[1]; const path = locator.match(PATTERN_LOCATOR)[2]; if (arguments.length === 1) { let data = DATA + "/" + DataSource.locale + "/" + path + "." + type; data = data.replace(/\/+/g, "/"); const hash = data.hashCode(); if (_cache.hasOwnProperty(hash)) return _cache[hash]; const request = new XMLHttpRequest(); request.overrideMimeType("application/xslt+xml"); request.open("GET", data, false); request.send(); if (request.status !== 200) throw new Error(`HTTP status ${request.status} for ${request.responseURL}`); data = request.responseXML; _cache[hash] = data; return data.clone(); } if (!type.match(/^xml$/) && transform) throw new Error("Transformation is not supported for this locator"); const data = DataSource.fetch(locator); if (!transform) return data.clone(); let style = locator.replace(/(^((\w+\-+(?=\w))+)\w*)|(^\w+)/, "xslt"); if (typeof transform !== "boolean") { style = transform; if (typeof style !== "string" || !style.match(PATTERN_LOCATOR)) throw new Error("Invalid style: " + String(style)); } return DataSource.transform(data, DataSource.fetch(style), meta); }, /** * Collects and concatenates multiple XML files in a new XMLDocument. * The method has the following various signatures: * DataSource.collect(locator, ...); * DataSource.collect(collector, [locators]); * @param collector name of the collector element in the XMLDocument * @param locators Array or VarArg with locators * @return the created XMLDocument, otherwise null * @throws Error in the case of invalid arguments */ collect(...variants) { if (variants.length <= 0) return null; let collection = []; let collector = "collection"; if (variants.length === 2 && typeof variants[0] === "string" && Array.isArray(variants[1])) { if (!variants[0].match(PATTERN_WORD)) throw new TypeError("Invalid collector"); collector = variants[0]; collection = Array.from(variants[1]); } else if (variants.length === 1 && Array.isArray(variants[0])) { collection = collection.concat(variants[0]); } else collection = Array.from(variants); let hash = collector.hashCode() + ":" + collection.join().hashCode(); collection.forEach((entry) => hash += ":" + String(entry).hashCode()); if (_cache.hasOwnProperty(hash)) return _cache[hash].clone(); const root = document.implementation.createDocument(null, collector, null); collection.forEach((entry) => { if (typeof entry !== "string") throw new TypeError("Invalid collection entry"); root.documentElement.appendChild(DataSource.fetch(entry).documentElement.cloneNode(true)); }); _cache[hash] = root; return root.clone(); } }); /** * Enhancement of the JavaScript API * Adds a method for cloning a XMLDocument. */ compliant("XMLDocument.prototype.clone", function() { const clone = this.implementation.createDocument(null, null); clone.appendChild(clone.importNode(this.documentElement, true)); return clone; }); // XML/XSLT data cache const _cache = {}; Object.defineProperty(DataSource, "cache", { value: {} }); // DataSource.locales // List of available locales (as standard marked are at the beginning) Object.defineProperty(DataSource, "locales", { value: [], enumerable: true }); let locale = []; locale = locale.concat(navigator.language); if (navigator.languages !== undefined) locale = locale.concat(navigator.languages); Array.from(locale).forEach((language) => { language = language.match(/^[a-z]+/i, ""); if (language && !locale.includes(language[0])) locale.push(language[0]); }); locale = locale.map((language) => language.trim().toLowerCase()); locale = locale.filter((item, index) => locale.indexOf(item) === index); if (locale.length <= 0) throw new Error("Locale not available"); const request = new XMLHttpRequest(); request.overrideMimeType("text/plain"); request.open("GET", DATA + "/locales.xml", false); request.send(); // DataSource.data // Cache of locales.xml, can also be used by other components Object.defineProperty(DataSource, "data", { value: request.status === 200 ? new DOMParser().parseFromString(request.responseText,"text/xml") : null }); if (!DataSource.data && request.status !== 404) throw new Error("Locale not available"); if (!DataSource.data) return; let xml = DataSource.data; let nodes = xml.evaluate("/locales/*[@default]", xml, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); for (let node = nodes.iterateNext(); node; node = nodes.iterateNext()) { let name = node.nodeName.toLowerCase(); if (!DataSource.locales.includes(name)) DataSource.locales.push(name); } nodes = xml.evaluate("/locales/*", xml, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); for (let node = nodes.iterateNext(); node; node = nodes.iterateNext()) { const name = node.nodeName.toLowerCase(); if (!DataSource.locales.includes(name)) DataSource.locales.push(name); } if (DataSource.locales.length <= 0) throw new Error("Locale not available"); locale.push(DataSource.locales[0]); locale = locale.filter(function(locale) { return DataSource.locales.includes(locale); }); DataSource.locales.selection = locale.length ? locale[0] : DataSource.locales[0]; })(); /** * (Resource)Messages is a static DataSource extension for internationalization * and localization. The implementation is based on a set of key-value or * label-value data which is stored in the locales.xml of the DataSource. * * + data * + de * + en * - locales.xml * + modules * + resources * - index.html * * The elements for the supported languages are organized in locales in this * file. Locales is a set of supported country codes. In each country code, the * key values are recorded as label entries. * * <?xml version="1.0"?> * <locales> * <de> * <label key="contact.title" value="Kontakt"/> * <label key="contact.development.title">Entwicklung</label> * ... * </de> * <en default="true"> * <label key="contact.title" value="Contact"/> * <label key="contact.development.title">Development</label> * ... * </en> * </locales> * * The language is selected automatically on the basis of the language setting * of the browser. If the language set there is not supported, the language * declared as 'default' is used. * * After loading the application, Messages are available as an associative * array and can be used directly in JavaScript and Markup via Expression * Language. * * Messages["contact.title"]; * * <h1 output="{{Messages['contact.title']}}"/> * * In addition, the object message is also provided. Unlike Messages, message is * an object tree analogous to the keys from Messages. The dot in the keys is * the indicator of the levels in the tree. * * messages.contact.title; * * <h1 output="{{messages.contact.title}}"/> * * Both objects are only available if there are also labels. * * @author Seanox Software Solutions * @version 1.6.0 20230304 */ (() => { compliant("messages", {}); compliant("Messages", {}); const _localize = DataSource.localize; DataSource.localize = (locale) => { _localize(locale); delete window.messages; delete window.Messages; window.Messages = { customize(label, ...values) { let text = Messages[label] || ""; for (let index = 0; index < values.length; index++) text = text.replace(new RegExp("\\{" + index + "\\}", "g"), values[index]); return text.replace(/\{\d+\}/g, ""); } } const map = new Map(); const xpath = "/locales/" + DataSource.locale + "/label"; const result = DataSource.data.evaluate(xpath, DataSource.data, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); for (let node = result.iterateNext(); node; node = result.iterateNext()) map.set((node.getAttribute("key") || "").trim(), ((node.getAttribute("value") || "").trim() || (node.textContent || "").trim()).unescape()); new Map([...map.entries()].sort()).forEach((value, key) => { const match = key.match(/^(?:((?:\w+\.)*\w+)\.)*(\w+)$/); if (match) { // In order for the object tree to branch from each level, each // level must be an object. Therefore, an anonymous object is // used for the level, which returns the actual text via // Object.prototype.toString(). const namespace = "messages" + (match[1] ? "." + match[1] : ""); Object.defineProperty(Namespace.use(namespace), match[2], { value: {toString() {return value;}} }); Object.defineProperty(Namespace.use("Messages"), key, { value }); } }); }; // Messages are based on DataSources. To initialize, DataSource.localize() // must be overwritten and loading of the key-value pairs is embedded. if (DataSource.data && DataSource.locale && DataSource.locales && DataSource.locales.includes(DataSource.locale)) DataSource.localize(DataSource.locale); })(); /** * 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. * * @author Seanox Software Solutions * @version 1.6.0 20230408 */ (() => { compliant("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 script * @return the return value from the script */ eval(script) { // 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 script * @return return value of the script, if available */ run(script) { if (script.trim()) 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; } }; })(); /** * 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", { /** * 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(/#\[([^\[\]]*)\]/