@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
367 lines (316 loc) • 15.8 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
* ----
* 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");
compliant(null, window.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");
compliant(null, 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];
})();