/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import {getLocaleOfDocument} from "../dom/locale.mjs"; import {Base} from "../types/base.mjs"; import {getGlobal, getGlobalObject} from "../types/global.mjs"; import {ID} from "../types/id.mjs"; import {isArray, isObject, isString, isPrimitive} from "../types/is.mjs"; import { getDocumentTranslations, Translations, } from "../i18n/translations.mjs"; import { validateFunction, validateInteger, validateObject, validatePrimitive, validateString, validateBoolean, } from "../types/validate.mjs"; import {clone} from "../util/clone.mjs"; import {Pathfinder} from "./pathfinder.mjs"; import {formatTimeAgo} from "../i18n/time-ago.mjs"; export {Transformer}; /** * The transformer class is a swiss army knife for manipulating values. * * A simple example is the conversion of all characters to lowercase. for this purpose the command `tolower` must be used. * * ```js * let t = new Transformer('tolower').run('ABC'); // ↦ abc * ``` * * @fragments /fragments/libraries/transformer * * @example /examples/libraries/transformer/simple * * @since 1.5.0 * @copyright schukai GmbH * @summary The transformer class is a swiss army knife for manipulating values. especially in combination with the pipe, processing chains can be built up. */ class Transformer extends Base { /** * * @param {string} definition */ constructor(definition) { super(); this.args = disassemble(definition); this.command = this.args.shift(); this.callbacks = new Map(); } /** * * @param {string} name * @param {function} callback * @param {object} context * @return {Transformer} * @throws {TypeError} value is not a string * @throws {TypeError} value is not a function */ setCallback(name, callback, context) { validateString(name); validateFunction(callback); if (context !== undefined) { validateObject(context); } this.callbacks.set(name, { callback: callback, context: context, }); return this; } /** * * @param {*} value * @return {*} * @throws {Error} unknown command * @throws {TypeError} unsupported type * @throws {Error} type not supported */ run(value) { return transform.apply(this, [value]); } } /** * * @param {string} command * @return {array} * @private */ function disassemble(command) { validateString(command); const placeholder = new Map(); const regex = /((?<pattern>\\(?<char>.)){1})/gim; // The separator for args must be escaped // undefined string which should not occur normally and is also not a regex const result = command.matchAll(regex); for (const m of result) { const g = m?.["groups"]; if (!isObject(g)) { continue; } const p = g?.["pattern"]; const c = g?.["char"]; if (p && c) { const r = `__${new ID().toString()}__`; placeholder.set(r, c); command = command.replace(p, r); } } let parts = command.split(":"); parts = parts.map(function (value) { let v = value.trim(); for (const k of placeholder) { v = v.replace(k[0], k[1]); } return v; }); return parts; } /** * tries to make a string out of value and if this succeeds to return it back * * @param {*} value * @return {string} * @private */ function convertToString(value) { if (isObject(value) && value.hasOwnProperty("toString")) { value = value.toString(); } validateString(value); return value; } /** * * @param {*} value * @return {*} * @private * @throws {Error} unknown command * @throws {TypeError} unsupported type * @throws {Error} type not supported * @throws {Error} missing key parameter */ function transform(value) { const console = getGlobalObject("console"); const args = clone(this.args); let key; let defaultValue; let translations; let date; let locale; let timestamp; let map; let keyValue; switch (this.command) { case "static": return this.args.join(":"); case "tolower": case "strtolower": case "tolowercase": validateString(value); return value.toLowerCase(); case "escape-html": case "escapehtml": validateString(value); return value.replace(/&/g, "&"). replace(/</g, "<"). replace(/>/g, ">"). replace(/"/g, """). replace(/'/g, '''); case "contains": if (isString(value)) { return value.includes(args[0]); } if (isArray(value)) { return value.includes(args[0]); } if (isObject(value)) { return value.hasOwnProperty(args[0]); } return false; case "has-entries": case "hasentries": if (isObject(value)) { return Object.keys(value).length > 0; } if (isArray(value)) { return value.length > 0; } return false; case "isundefined": case "is-undefined": return value === undefined; case "isnull": case "is-null": return value === null; case "isset": case "is-set": return value !== undefined && value !== null; case "isnumber": case "is-number": return isPrimitive(value) && !isNaN(value); case "isinteger": case "is-integer": return isPrimitive(value) && !isNaN(value) && value % 1 === 0; case "isfloat": case "is-float": return isPrimitive(value) && !isNaN(value) && value % 1 !== 0; case "isobject": case "is-object": return isObject(value); case "isarray": case "is-array": return Array.isArray(value); case "not": validateBoolean(value); return !value; case "toupper": case "strtoupper": case "touppercase": validateString(value); return value.toUpperCase(); case "to-string": case "tostring": return `${value}`; case "to-integer": case "tointeger": const n = parseInt(value); validateInteger(n); return n; case "to-json": case "tojson": return JSON.stringify(value); case "from-json": case "fromjson": return JSON.parse(value); case "trim": validateString(value); return value.trim(); case "rawurlencode": validateString(value); return encodeURIComponent(value) .replace(/!/g, "%21") .replace(/'/g, "%27") .replace(/\(/g, "%28") .replace(/\)/g, "%29") .replace(/\*/g, "%2A"); case "call": /** * callback-definition * function callback(value, ...args) { * return value; * } */ let callback; const callbackName = args.shift(); let context = getGlobal(); if (isObject(value) && value.hasOwnProperty(callbackName)) { callback = value[callbackName]; } else if (this.callbacks.has(callbackName)) { const s = this.callbacks.get(callbackName); callback = s?.["callback"]; context = s?.["context"]; } else if ( typeof window === "object" && window.hasOwnProperty(callbackName) ) { callback = window[callbackName]; } validateFunction(callback); args.unshift(value); return callback.call(context, ...args); case "plain": case "plaintext": validateString(value); const doc = new DOMParser().parseFromString(value, "text/html"); return doc.body.textContent || ""; case "if": case "?": validatePrimitive(value); let trueStatement = args.shift() || undefined; let falseStatement = args.shift() || undefined; trueStatement = convertSpecialStrings(trueStatement, value); falseStatement = convertSpecialStrings(falseStatement, value); const condition = evaluateCondition(value); return condition ? trueStatement : falseStatement; case "ucfirst": validateString(value); const firstchar = value.charAt(0).toUpperCase(); return firstchar + value.substr(1); case "ucwords": validateString(value); return value.replace( /^([a-z\u00E0-\u00FC])|\s+([a-z\u00E0-\u00FC])/g, function (v) { return v.toUpperCase(); }, ); case "count": case "length": if ( (isString(value) || isObject(value) || isArray(value)) && value.hasOwnProperty("length") ) { return value.length; } throw new TypeError(`unsupported type ${typeof value}`); case "to-base64": case "btoa": case "base64": return btoa(convertToString(value)); case "atob": case "from-base64": return atob(convertToString(value)); case "empty": return ""; case "undefined": return undefined; case "debug": if (isObject(console)) { console.log(value); } return value; case "prefix": validateString(value); const prefix = args?.[0]; return prefix + value; case "suffix": validateString(value); const suffix = args?.[0]; return value + suffix; case "uniqid": return new ID().toString(); case "first-key": case "last-key": case "nth-last-key": case "nth-key": if (!isObject(value)) { throw new Error("type not supported"); } const keys = Object.keys(value).sort(); if (this.command === "first-key") { key = 0; } else if (this.command === "last-key") { key = keys.length - 1; } else { key = validateInteger(parseInt(args.shift())); if (this.command === "nth-last-key") { key = keys.length - key - 1; } } defaultValue = args.shift() || ""; const useKey = keys?.[key]; if (value?.[useKey]) { return value?.[useKey]; } return defaultValue; case "key": case "property": case "index": key = args.shift() || undefined; if (key === undefined) { throw new Error("missing key parameter"); } defaultValue = args.shift() || undefined; if (value instanceof Map) { if (!value.has(key)) { return defaultValue; } return value.get(key); } if (isObject(value) || isArray(value)) { if (value?.[key]) { return value?.[key]; } return defaultValue; } throw new Error("type not supported"); case "path-exists": key = args.shift(); if (key === undefined) { throw new Error("missing key parameter"); } return new Pathfinder(value).exists(key); case "concat": const pf2 = new Pathfinder(value); let concat = ""; while (args.length > 0) { key = args.shift(); if (key === undefined) { throw new Error("missing key parameter"); } // add empty strings if (isString(key) && key.trim() === "") { concat += key; continue; } if (!pf2.exists(key)) { concat += key; continue; } const v = pf2.getVia(key); if (!isPrimitive(v)) { throw new Error("value is not primitive"); } concat += v; } return concat; case "path": key = args.shift(); if (key === undefined) { throw new Error("missing key parameter"); } const pf = new Pathfinder(value); if (!pf.exists(key)) { return undefined; } return pf.getVia(key); case 'ellipsize': case 'ellipsis': case 'ellipse': validateString(value); const length = parseInt(args[0]) || 0; const ellipsis = args[1] || '…'; if (value.length <= length) { return value; } return value.substring(0, length) + ellipsis; case "substring": validateString(value); const start = parseInt(args[0]) || 0; const end = (parseInt(args[1]) || 0) + start; return value.substring(start, end); case "nop": return value; case "??": case "default": if (value !== undefined && value !== null) { return value; } defaultValue = args.shift(); let defaultType = args.shift(); if (defaultType === undefined) { defaultType = "string"; } switch (defaultType) { case "int": case "integer": return parseInt(defaultValue); case "float": return parseFloat(defaultValue); case "undefined": return undefined; case "bool": case "boolean": defaultValue = defaultValue.toLowerCase(); return ( (defaultValue !== "undefined" && defaultValue !== "" && defaultValue !== "off" && defaultValue !== "false" && defaultValue !== "false") || defaultValue === "on" || defaultValue === "true" || defaultValue === "true" ); case "string": return `${defaultValue}`; case "object": return JSON.parse(atob(defaultValue)); } throw new Error("type not supported"); case "map": map = new Map(); while (args.length > 0) { keyValue = args.shift(); if (keyValue === undefined) { throw new Error("missing key parameter"); } keyValue = keyValue.split("="); map.set(keyValue[0], keyValue[1]); } return map.get(value); case "equals": if (args.length === 0) { throw new Error("missing value parameter"); } validatePrimitive(value); const equalsValue = args.shift(); /** * The history of “typeof null” * https://2ality.com/2013/10/typeof-null.html * In JavaScript, typeof null is 'object', which incorrectly suggests * that null is an object. */ if (value === null) { return equalsValue === "null"; } const typeOfValue = typeof value; switch (typeOfValue) { case "string": return value === equalsValue; case "number": return value === parseFloat(equalsValue); case "boolean": return value === (equalsValue === "true" || equalsValue === "on"); case "undefined": return equalsValue === "undefined"; default: throw new Error("type not supported"); } case "money": case "currency": try { locale = getLocaleOfDocument(); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } // Verwenden von RegExp, um Währung und Betrag zu extrahieren const match = value.match(/^([A-Z]{3})[\s-]*(\d+(\.\d+)?)$/); if (!match) { throw new Error("invalid currency format"); } const currency = match[1]; const amount = match[2]; const maximumFractionDigits = args?.[0] || 2; const roundingIncrement = args?.[1] || 5; const nf = new Intl.NumberFormat(locale.toString(), { style: "currency", currency: currency, maximumFractionDigits: maximumFractionDigits, roundingIncrement: roundingIncrement, }); return nf.format(amount); case "timestamp": date = new Date(value); timestamp = date.getTime(); if (isNaN(timestamp)) { throw new Error("invalid date"); } return timestamp; case "time": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } try { locale = getLocaleOfDocument(); return date.toLocaleTimeString(locale.toString(), { hour12: false, }); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } case "datetimeformat": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } const options = { dateStyle: "medium", timeStyle: "medium", hour12: false, }; if (args.length > 0) { options.dateStyle = args.shift(); } if (args.length > 0) { options.timeStyle = args.shift(); } try { locale = getLocaleOfDocument().toString(); return new Intl.DateTimeFormat(locale, options).format(date); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } case "datetime": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } try { locale = getLocaleOfDocument(); return date.toLocaleString(locale.toString(), { hour12: false, }); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } case "date": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } try { locale = getLocaleOfDocument(); return date.toLocaleDateString(locale.toString(), { year: "numeric", month: "2-digit", day: "2-digit", }); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } case "time-ago": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } try { locale = getLocaleOfDocument(); return formatTimeAgo(date, locale.toString()); } catch (e) { throw new Error(`unsupported locale or missing format (${e.message})`); } case "year": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getFullYear(); case "month": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getMonth() + 1; case "day": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getDate(); case "weekday": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getDay(); case "hour": case "hours": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getHours(); case "minute": case "minutes": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getMinutes(); case "second": case "seconds": date = new Date(value); if (isNaN(date.getTime())) { throw new Error("invalid date"); } return date.getSeconds(); case "i18n": case "translation": translations = getDocumentTranslations(); if (!(translations instanceof Translations)) { throw new Error("missing translations"); } key = args.shift() || undefined; if (key === undefined) { key = value; } defaultValue = args.shift() || undefined; defaultValue = convertSpecialStrings(defaultValue, value); return translations.getText(key, defaultValue); case "set-toggle": case "set-set": case "set-remove": const modifier = args.shift(); let delimiter = args.shift(); if (delimiter === undefined) { delimiter = " "; } const set = new Set(value.split(delimiter)); const toggle = new Set(modifier.split(delimiter)); if (this.command === "set-toggle") { for (const t of toggle) { if (set.has(t)) { set.delete(t); } else { set.add(t); } } } else if (this.command === "set-set") { for (const t of toggle) { set.add(t); } } else if (this.command === "set-remove") { for (const t of toggle) { set.delete(t); } } return Array.from(set).join(delimiter); default: throw new Error(`unknown command ${this.command}`); } } /** * converts special strings to their values * @private * @param input * @param value * @return {undefined|*|null|string} */ function convertSpecialStrings(input, value) { switch (input) { case "value": return value; case "\\value": return "value"; case "\\undefined": return undefined; case "\\null": return null; default: return input; } } /** * checks if a value is true or not * @param value * @return {boolean} */ function evaluateCondition(value) { const lowerValue = typeof value === "string" ? value.toLowerCase() : value; return ( (value !== undefined && value !== null && value !== "" && lowerValue !== "off" && lowerValue !== "false" && value !== false) || lowerValue === "on" || lowerValue === "true" || value === true ); }