Something went wrong on our end
Select Git revision
Monster.Logging.Handler.ConsoleHandler.html
-
Volker Schukai authoredVolker Schukai authored
transformer.mjs 21.36 KiB
/**
* 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,
validateArray,
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 { UUID } from "../types/uuid.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-array":
case "toarray":
if (isArray(value)) {
return value;
}
if (isObject(value)) {
return Object.values(value);
}
return [value];
case "listtoarray":
case "list-to-array":
validateString(value);
const listDel = args.shift() || ",";
return value.split(listDel);
case "arraytolist":
case "array-to-list":
validateArray(value);
const listDel2 = args.shift() || ",";
return value.join(listDel2);
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 "ltrim":
validateString(value);
return value.replace(/^\s+/, "");
case "rtrim":
validateString(value);
return value.replace(/\s+$/, "");
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 "replace":
const search = args.shift();
const replace = args.shift();
validateString(search);
validateString(replace);
validateString(value);
return value.replace(new RegExp(search, "g"), replace);
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 "gt":
case "greaterthan":
case "greater-than":
case ">":
validatePrimitive(value);
const compareValue = args.shift();
validatePrimitive(compareValue);
return value > compareValue;
case "gte":
case "greaterthanorequal":
case "greater-than-or-equal":
validatePrimitive(value);
const compareValue3 = args.shift();
validatePrimitive(compareValue3);
return value >= compareValue3;
case "lt":
case "lessthan":
case "less-than":
case "<":
validatePrimitive(value);
const compareValue2 = args.shift();
validatePrimitive(compareValue2);
return value < compareValue2;
case "lte":
case "lessthanorequal":
case "less-than-or-equal":
validatePrimitive(value);
const compareValue4 = args.shift();
validatePrimitive(compareValue4);
return value <= compareValue4;
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.groupCollapsed("Transformer Debug");
console.log("Value", value);
console.log("Transformer", this);
console.groupEnd();
}
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 "uuid":
return new UUID().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 "array":
if (defaultValue === "") {
return [];
}
return defaultValue.split(",");
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
);
}