Skip to content
Snippets Groups Projects
Select Git revision
  • b4366c7db5f377bda5569e46f4085d3803c1a89b
  • master default protected
  • 0.1.1
  • 0.1.0
4 results

oauth2.go

Blame
  • 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, "&amp;")
    				.replace(/</g, "&lt;")
    				.replace(/>/g, "&gt;")
    				.replace(/"/g, "&quot;")
    				.replace(/'/g, "&#39;");
    
    		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
    	);
    }