Skip to content
Snippets Groups Projects
Select Git revision
  • 11efd9861637da52c04f3f5d8c2d4236e32fdea8
  • master default protected
  • 1.31
  • 4.38.7
  • 4.38.6
  • 4.38.5
  • 4.38.4
  • 4.38.3
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
23 results

Monster_Data.html

Blame
  • transformer.mjs 21.63 KiB
    /**
     * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
     * Node module: @schukai/monster
     * This file is licensed under the AGPLv3 License.
     * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
     */
    
    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";
    
    export {Transformer};
    
    /**
     * The transformer class is a swiss army knife for manipulating values. especially in combination with the pipe, processing chains can be built up.
     *
     * A simple example is the conversion of all characters to lowercase. for this purpose the command `tolower` must be used.
     *
     * ```
     * let t = new Transformer('tolower').run('ABC'); // ↦ abc
     * ```
     *
     * @see {@link https://monsterjs.org/en/doc/#transformer|Monster Docs}
     *
     * @externalExample ../../example/data/transformer.mjs
     * @license AGPLv3
     * @since 1.5.0
     * @copyright schukai GmbH
     * @memberOf Monster.Data
     */
    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
         * @returns {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
         * @returns {*}
         * @throws {Error} unknown command
         * @throws {TypeError} unsupported type
         * @throws {Error} type not supported
         */
        run(value) {
            return transform.apply(this, [value]);
        }
    }
    
    /**
     *
     * @param {string} command
     * @returns {array}
     * @private
     */
    function disassemble(command) {
        validateString(command);
    
        let 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
        let result = command.matchAll(regex);
    
        for (let m of result) {
            let g = m?.["groups"];
            if (!isObject(g)) {
                continue;
            }
    
            let p = g?.["pattern"];
            let c = g?.["char"];
    
            if (p && c) {
                let 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 (let 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
     * @returns {string}
     * @private
     */
    function convertToString(value) {
        if (isObject(value) && value.hasOwnProperty("toString")) {
            value = value.toString();
        }
    
        validateString(value);
        return value;
    }
    
    /**
     *
     * @param {*} value
     * @returns {*}
     * @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");
    
        let 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 "tostring":
                return `${value}`;
    
            case "tointeger":
                let 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;
                let callbackName = args.shift();
                let context = getGlobal();
    
                if (isObject(value) && value.hasOwnProperty(callbackName)) {
                    callback = value[callbackName];
                } else if (this.callbacks.has(callbackName)) {
                    let 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);
                let 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;
    
                if (trueStatement === "value") {
                    trueStatement = value;
                }
                if (trueStatement === "\\value") {
                    trueStatement = "value";
                }
    
                if (trueStatement === "\\undefined") {
                    trueStatement = undefined;
                }
    
                if (trueStatement === "\\null") {
                    trueStatement = null;
                }
    
                if (falseStatement === "value") {
                    falseStatement = value;
                }
                if (falseStatement === "\\value") {
                    falseStatement = "value";
                }
    
                if (falseStatement === "\\undefined") {
                    falseStatement = undefined;
                }
    
                if (falseStatement === "\\null") {
                    falseStatement = null;
                }
    
                let condition =
                    (value !== undefined && value !== "" && value !== "off" && value !== "false" && value !== false) ||
                    value === "on" ||
                    value === "true" ||
                    value === true;
                return condition ? trueStatement : falseStatement;
    
            case "ucfirst":
                validateString(value);
    
                let 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);
                let prefix = args?.[0];
                return prefix + value;
    
            case "suffix":
                validateString(value);
                let 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() || "";
    
                let 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":
                let 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;
                    }
                    let 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");
                }
    
                let pf = new Pathfinder(value);
    
                if (!pf.exists(key)) {
                    return undefined;
                }
    
                return pf.getVia(key);
    
            case "substring":
                validateString(value);
    
                let start = parseInt(args[0]) || 0;
                let 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) {
                    if (equalsValue === "null") {
                        return true;
                    }
                    return false;
                }
    
                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})`);
                }
    
                const currency = value.substring(0, 3);
                if (!currency) {
                    throw new Error("missing currency parameter");
                }
    
                const maximumFractionDigits = args?.[0] || 2;
                const roundingIncrement = args?.[1] || 5;
    
                const nf = new Intl.NumberFormat(locale, {
                    style: "currency",
                    currency: currency,
                    maximumFractionDigits: maximumFractionDigits,
                    roundingIncrement: roundingIncrement,
                });
    
                return nf.format(value.substring(3));
    
            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);
                } 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: args.shift() || "medium",
                    timeStyle: args.shift() || "medium",
                };
    
                try {
                    locale = getLocaleOfDocument();
                    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);
                } 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);
                } 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;
                return translations.getText(key, defaultValue);
    
            default:
                throw new Error(`unknown command ${this.command}`);
        }
    }