data/transformer.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster} from '../namespace.js';
import {Base} from '../types/base.js';
import {validateString, validatePrimitive, validateFunction, validateInteger} from '../types/validate.js';
import {isObject, isString, isArray} from '../types/is.js';
import {ID} from '../types/id.js';
import {clone} from "../util/clone.js";
import {Pathfinder} from "./pathfinder.js";

/**
 * the transformer class is a swiss army knife for manipulating values. especially in combination with the pipe, processing chains can be built up.
 *
 * you can call the method via the monster namespace `new Monster.Data.Transformer()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/data/transformer.js';
 * console.log(new Monster.Data.Transformer())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Transformer} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/data/transformer.js';
 * console.log(new Transformer())
 * </script>
 * ```
 *
 * 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
 * ```
 * 
 * **all commands**
 * 
 * in the following table all commands, parameters and existing aliases are described.
 * 
 *  | command      | parameter                  | alias                   | description                                                                                                                                                                                                                                                                                                                                                |
 *  |:-------------|:---------------------------|:------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 *  | base64       |                            |                         | Converts the value to base64                                                                                                                                                                                                                                                                                                                               |
 *  | call         | function,param1:param2:... |                         | Calling a callback function. The function can be defined in three places: either globally, in the context `addCallback` or in the passed object                                                                                                                                                                                                            |
 *  | empty        |                            |                         | Return empty String ""                                                                                                                                                                                                                                                                                                                                     |
 *  | if           | statement1:statement2      | ?                       | Is the ternary operator, the first parameter is the valid statement, the second is the false part. To use the current value in the queue, you can set the value keyword. On the other hand, if you want to have the static string "value", you have to put one backslash \\ in front of it and write value. the follow values are true: 'on', true, 'true' |
 *  | index        | key:default                | property, key           | Fetches a value from an object, an array, a map or a set                                                                                                                                                                                                                                                                                                   |
 *  | length       |                            | count                   | Length of the string or entries of an array or object                                                                                                                                                                                                                                                                                                      |
 *  | nop          |                            |                         | Do nothing                                                                                                                                                                                                                                                                                                                                                 |
 *  | path         | path                       |                         | The access to an object is done via a Pathfinder object                                                                                                                                                                                                                                                                                                    |
 *  | plaintext    |                            | plain                   | All HTML tags are removed (*)                                                                                                                                                                                                                                                                                                                              |
 *  | prefix       | text                       |                         | Adds a prefix                                                                                                                                                                                                                                                                                                                                              |
 *  | rawurlencode |                            |                         | URL coding                                                                                                                                                                                                                                                                                                                                                 |
 *  | static       |                            | none                    | The Arguments value is used and passed to the value. Special characters \ <space> and : can be quotet by a preceding \.                                                                                                                                                                                                                                    |
 *  | substring    | start:length               |                         | Returns a substring                                                                                                                                                                                                                                                                                                                                        |
 *  | suffix       | text                       |                         | Adds a suffix                                                                                                                                                                                                                                                                                                                                              |
 *  | tointeger    |                            |                         | Type conversion to an integer value                                                                                                                                                                                                                                                                                                                        |
 *  | tolower      |                            | strtolower, tolowercase | The input value is converted to lowercase letters                                                                                                                                                                                                                                                                                                          |
 *  | tostring     |                            |                         | Type conversion to a string                                                                                                                                                                                                                                                                                                                                |
 *  | toupper      |                            | strtoupper, touppercase | The input value is converted to uppercase letters                                                                                                                                                                                                                                                                                                          |
 *  | trim         |                            |                         | Remove spaces at the beginning and end                                                                                                                                                                                                                                                                                                                     |
 *  | ucfirst      |                            |                         | First character large                                                                                                                                                                                                                                                                                                                                      |
 *  | ucwords      |                            |                         | Any word beginning large                                                                                                                                                                                                                                                                                                                                   |
 *  | undefined    |                            |                         | Return undefined                                                                                                                                                                                                                                                                                                                                           |
 *  | uniqid       |                            |                         | Creates a string with a unique value (**)                                                                                                                                                                                                                                                                                                                  |
 * 
 *  (*) for this functionality the extension [jsdom](https://www.npmjs.com/package/jsdom) must be loaded in the nodejs context.
 * 
 *  ```
 *  // polyfill
 *  if (typeof window !== "object") {
 *     const {window} = new JSDOM('', {
 *         url: 'http://example.com/',
 *         pretendToBeVisual: true
 *     });
 * 
 *     [
 *         'self',
 *         'document',
 *         'Node',
 *         'Element',
 *         'HTMLElement',
 *         'DocumentFragment',
 *         'DOMParser',
 *         'XMLSerializer',
 *         'NodeFilter',
 *         'InputEvent',
 *         'CustomEvent'
 *     ].forEach(key => (global[key] = window[key]));
 * }
 *  ```
 * 
 *  (**) for this command the crypt library is necessary in the nodejs context.
 * 
 *  ```
 *  import * as Crypto from "@peculiar/webcrypto";
 *  global['crypto'] = new Crypto.Crypto();
 *  ```
 * 
 * 
 *
 * @since 1.5.0
 * @copyright schukai GmbH
 * @memberOf Monster/Data
 */
class Transformer extends Base {
    /**
     *
     * @param {string} definition
     */
    constructor(definition) {
        super();
        validateString(definition);

        this.args = disassemble(definition);
        this.command = this.args.shift()
        this.callbacks = new Map();

    }

    /**
     *
     * @param {string} name
     * @param {function} callback
     * @returns {Transformer}
     * @throws {TypeError} value is not a string
     * @throws {TypeError} value is not a function
     */
    setCallback(name, callback) {
        validateString(name)
        validateFunction(callback)
        this.callbacks.set(name, callback);
        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])
    }
}

Monster.assignToNamespace('Monster.Data', Transformer);
export {Monster, Transformer}

/**
 *
 * @param {string} command
 * @returns {array}
 * @private
 */
function disassemble(command) {

    validateString(command);
    
    let placeholder = new Map;
    const regex = /((?<pattern>\\(?<char>.)){1})/mig;

    // The separator for args must be quotable
    // 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
 */
function transform(value) {

    let args = clone(this.args);
    let key

    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 '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();

            if (isObject(value) && calue.hasOwnProperty(callbackName)) {
                callback = value[callbackName];
            } else if (this.callbacks.has(callbackName)) {
                callback = this.callbacks.get(callbackName);
            } else if (typeof window === 'object' && window.hasOwnProperty(callbackName)) {
                callback = window[callbackName];
            }
            validateFunction(callback);

            args.unshift(value);
            return callback(...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");

        case 'base64':
            convertToString(value);
            return btoa(value);

        case 'empty':
            return '';

        case 'undefined':
            return undefined;

        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 'key':
        case 'property':
        case 'index':

            key = (args.shift() || 'undefined');
            let defaultValue = (args.shift() || '');

            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':

            key = (args.shift() || 'undefined');
            return new Pathfinder(value).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;

        default:
            throw new Error("unknown command")
    }

    return value;
}