Select Git revision
transformer.mjs
transformer.mjs 13.75 KiB
/**
* 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, isPrimitive} from "../types/is.mjs";
import {getDocumentTranslations, Translations} from "../i18n/translations.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;
let element;
let attribute;
let translations;
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 "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 (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 "concat":
let pf2 = new Pathfinder(value);
let concat = "";
while (args.length > 0) {
key = args.shift();
if (key === undefined) {
throw new Error("missing key parameter");
}
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 "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}`);
}
}