From 3a905c1787275a3aff165d7dacc771be73d6c8fe Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Thu, 23 Jan 2025 19:12:11 +0100
Subject: [PATCH] feat(transformer): new command ellipsize

---
 source/data/transformer.mjs | 1623 ++++++++++++++++++-----------------
 1 file changed, 818 insertions(+), 805 deletions(-)

diff --git a/source/data/transformer.mjs b/source/data/transformer.mjs
index f2caccab7..d9383d528 100644
--- a/source/data/transformer.mjs
+++ b/source/data/transformer.mjs
@@ -12,28 +12,28 @@
  * 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 {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,
+    getDocumentTranslations,
+    Translations,
 } from "../i18n/translations.mjs";
 import {
-	validateFunction,
-	validateInteger,
-	validateObject,
-	validatePrimitive,
-	validateString,
-	validateBoolean,
+    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";
+import {clone} from "../util/clone.mjs";
+import {Pathfinder} from "./pathfinder.mjs";
+import {formatTimeAgo} from "../i18n/time-ago.mjs";
 
-export { Transformer };
+export {Transformer};
 
 /**
  * The transformer class is a swiss army knife for manipulating values.
@@ -53,53 +53,53 @@ export { Transformer };
  * @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} 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]);
+    }
 }
 
 /**
@@ -109,41 +109,41 @@ class Transformer extends Base {
  * @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;
+    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;
 }
 
 /**
@@ -154,12 +154,12 @@ function disassemble(command) {
  * @private
  */
 function convertToString(value) {
-	if (isObject(value) && value.hasOwnProperty("toString")) {
-		value = value.toString();
-	}
+    if (isObject(value) && value.hasOwnProperty("toString")) {
+        value = value.toString();
+    }
 
-	validateString(value);
-	return value;
+    validateString(value);
+    return value;
 }
 
 /**
@@ -173,682 +173,695 @@ function convertToString(value) {
  * @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 "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 "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}`);
-	}
+    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 "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}`);
+    }
 }
 
 /**
@@ -859,18 +872,18 @@ function transform(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;
-	}
+    switch (input) {
+        case "value":
+            return value;
+        case "\\value":
+            return "value";
+        case "\\undefined":
+            return undefined;
+        case "\\null":
+            return null;
+        default:
+            return input;
+    }
 }
 
 /**
@@ -879,17 +892,17 @@ function convertSpecialStrings(input, 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
-	);
+    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
+    );
 }
-- 
GitLab