/** * Copyright schukai GmbH and contributors 2022. 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 {Base} from "../types/base.mjs"; import {getGlobal, getGlobalObject} from "../types/global.mjs"; import {ID} from "../types/id.mjs"; import {isArray, isObject, isString} from "../types/is.mjs"; import { validateFunction, validateInteger, validateObject, validatePrimitive, validateString, } 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; switch (this.command) { case "static": return this.args.join(":"); case "tolower": case "strtolower": case "tolowercase": validateString(value); return value.toLowerCase(); 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 "tojson": return JSON.stringify(value); 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 (falseStatement === "value") { falseStatement = value; } if (falseStatement === "\\value") { falseStatement = "value"; } 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 "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"); default: throw new Error(`unknown command ${this.command}`); } }