lib/conv.js

/*
 *  Author: Vlad Seryakov vseryakov@gmail.com
 *  backendjs 2018
 */

const util = require('util');
const logger = require(__dirname + '/../logger');
const lib = require(__dirname + '/../lib');

/**
 * Returns a floating number from the version string, it assumes common semver format as major.minor.patch, all non-digits will
 * be removed, underscores will be treated as dots. Returns a floating number which can be used in comparing versions.
 * @param {string} str - version like string
 * @return {number}
 * @example
 *      > lib.toVersion("1.0.3")
 *      1.000003
 *      > lib.toVersion("1.0.3.4")
 *      1.000003004
 *      > lib.toVersion("1.0.3.4") > lib.toVersion("1.0.3")
 *      true
 *      > lib.toVersion("1.0.3.4") > lib.toVersion("1.0.0")
 *      true
 *      > lib.toVersion("1.0.3.4") > lib.toVersion("1.1.0")
 *      false
 * @memberof module:lib
 * @method toVersion
 */
lib.toVersion = function(str)
{
    return str ? String(str).replace("_", ".").replace(/[^0-9.]/g, "").split(".").reduce((x, y, i) => (x + Number(y) / Math.pow(10, i * 3)), 0) : 0;
}

/**
 * Convert text into capitalized words, if it is less or equal than minlen leave it as is
 * @param {string} name
 * @param {int} [minlen]
 * @return {string}
 * @memberof module:lib
 * @method toTitle
 */
lib.toTitle = function(name, minlen)
{
    return typeof name == "string" ?
           minlen > 0 && name.length <= minlen ? name :
           name.replaceAll("_", " ").
                split(/[ ]+/).
                reduce((x, y) => (x + (y ? (y.substr(0,1).toUpperCase() + y.substr(1).toLowerCase() + " ") : "")), "").
                trim() : "";
}

/**
 * Convert into camelized form
 * @param {string} name
 * @param {string} [chars] can define the separators, default is [ _.:-]
 * @return {string}
 * @memberof module:lib
 * @method toCamel
 */
lib.toCamel = function(name, chars)
{
    var rx = typeof chars == "string" ? new RegExp("(?:[" + chars + "])(\\w)", "g") : this.rxCamel;
    return typeof name == "string" ? name.substr(0, 1).toLowerCase() + name.substr(1).replace(rx, (_, c) => (c ? c.toUpperCase () : '')) : "";
}

/**
 * Convert Camel names into names separated by the given separator or dash(-) if not.
 * @param {string} str
 * @param {string} [sep]
 * @return {string}
 * @memberof module:lib
 * @method toUncamel
 */
lib.toUncamel = function(str, sep)
{
    return typeof str == "string" ? str.replace(/([A-Z])/g, (_, c, index) => ((index ? sep || '-' : '') + c.toLowerCase())) : "";
}

/**
 * Safe convertion to a number, no expections, uses 0 instead of NaN, handle booleans, if float specified, returns as float.
 * @param {any} val - to be converted to a number
 * @param {object} [options]
 * @param {int} [options.dflt] - default value
 * @param {int} [options.float] - treat as floating number
 * @param {int} [options.min] - minimal value, clip
 * @param {int} [options.max] - maximum value, clip
 * @param {int} [options.incr] - a number to add before checking for other conditions
 * @param {int} [options.mult] - a number to multiply before checking for other conditions
 * @param {int} [options.novalue] - replace this number with default
 * @param {int} [options.zero] - replace with this number if result is 0
 * @param {int} [options.digits] - how many digits to keep after the floating point
 * @param {int} [options.bigint] - return BigInt if not a safe integer
 * @return {number}
 *
 * @example
 * lib.toNumber("123")
 * lib.toNumber("1.23", { float: 1, dflt: 0, min: 0, max: 2 })
 * @memberof module:lib
 * @method toNumber
 */
lib.toNumber = function(val, options)
{
    var n = 0;
    if (typeof val == "number") {
        n = val;
    } else
    if (typeof val == "boolean") {
        n = val ? 1 : 0;
    } else {
        if (typeof val != "string") {
            n = options?.dflt || 0;
        } else {
            // Autodetect floating number
            var f = typeof options?.float == "undefined" || options?.float == null ? this.rxFloat.test(val) : options?.float;
            n = val[0] == 't' ? 1 : val[0] == 'f' ? 0 : val == "infinity" ? Infinity : (f ? parseFloat(val, 10) : parseInt(val, 10));
        }
    }
    n = isNaN(n) ? options?.dflt || 0 : n;
    if (options) {
        if (typeof options.novalue == "number" && n === options.novalue) n = options.dflt || 0;
        if (typeof options.incr == "number") n += options.incr;
        if (typeof options.mult == "number") n *= options.mult;
        if (isNaN(n)) n = options.dflt || 0;
        if (typeof options.min == "number" && n < options.min) n = options.min;
        if (typeof options.max == "number" && n > options.max) n = options.max;
        if (typeof options.float != "undefined" && !options.float) n = Math.round(n);
        if (typeof options.zero == "number" && !n) n = options.zero;
        if (typeof options.digits == "number") n = parseFloat(n.toFixed(options.digits));
        if (options.bigint && typeof n == "number" && !Number.isSafeInteger(n)) n = BigInt(n);
    }
    return n;
}

/**
 * Strip all non-digit characters from a string
 * @param {string} str - input string
 * @return {string} only digit in the result
 * @memberof module:lib
 * @method toDigits
 */
lib.toDigits = function(str)
{
    return (typeof str == "string" ? str : String(str)).replace(this.rxNoDigits, "");
}

/**
 * Return true if value represents true condition, i.e. non empty value including yes, ok , t
 * @param {string|number|boolean} val
 * @param {any} [dflt]
 * @return {boolean}
 * @memberof module:lib
 * @method toBool
 */
lib.toBool = function(val, dflt)
{
    if (typeof val == "boolean") return val;
    if (typeof val == "number") return val > 0;
    if (typeof val == "undefined" || typeof val == "function") val = dflt;
    return this.rxTrue.test(val);
}

/**
 * Return Date object for given text or numeric date representation, for invalid date returns 1969 unless `invalid` parameter is given,
 * in this case invalid date returned as null. If `dflt` is NaN, null or 0 returns null as well.
 * @param {string|Date|number} val
 * @param {any} [dflt]
 * @param {boolean} [invalid]
 * @return {Date}
 * @memberof module:lib
 * @method toDate
 */
lib.toDate = function(val, dflt, invalid)
{
    if (this.isDate(val)) return val;
    var d = NaN;
    // String that looks like a number
    if (typeof val == "string") {
        val = /^[0-9.]+$/.test(val) ? this.toNumber(val) : val.replace(/([0-9])(AM|PM)/i, "$1 $2");
    }
    if (typeof val == "number") {
        // Convert nanoseconds to milliseconds
        if (val > 2147485547000) val = Math.round(val / 1000);
        // Convert seconds to milliseconds
        if (val < 2147483647) val *= 1000;
    }
    // Remove unsupported timezone names
    if (typeof val == "string") {
        var gmt = val.indexOf("GMT") > -1;
        for (const i in this.tzMap) {
            if ((gmt || this.tzMap[i][3] === false) && val.indexOf(this.tzMap[i][0]) > -1) {
                val = val.replace(this.tzMap[i][0], "");
            }
        }
    }
    if (typeof val != "string" && typeof val != "number") val = d;
    if (val) try { d = new Date(val); } catch (e) {}
    return this.isDate(d) ? d : invalid || (dflt !== undefined && isNaN(dflt)) || dflt === null || dflt === 0 ? null : new Date(dflt || 0);
}

/**
 * Return milliseconds from the date or date string, only number as dflt is supported, for invalid dates returns 0
 * @param {string|number|Date} val
 * @param {any} [dflt]
 * @return {number}
 * @memberof module:lib
 * @method toMtime
 */
lib.toMtime = function(val, dflt)
{
    val = this.toDate(val, null);
    return val ? val.getTime() : typeof dflt == "number" ? dflt : 0;
}

/**
 * Encode a string into Base64 url safe version
 * @param {string|Buffer|ArrayBuffer} str
 * @return {string}
 * @memberof module:lib
 * @method toBase64url
 */
lib.toBase64url = function(str)
{
    return Buffer.from(str).toString("base64").replace(/[+/]/g, (x) => (x == '+' ? '-' : '_'));
}

/**
 * Decode base64url into a string
 * @param {string} str
 * @param {boolean} [binary] - return as Buffer
 * @return {string|Buffer}
 * @memberof module:lib
 * @method fromBase64url
 */
lib.fromBase64url = function(str, binary)
{
    if (typeof str != "string") return "";

    var padding = 4 - str.length % 4;
    if (padding != 4) {
        for (let i = 0; i < padding; ++i) str += '=';
    }
    str = str.replace(/[_-]/g, (x) => (x == '-' ? '+' : '/'));
    str = Buffer.from(str, "base64");
    return binary ? str : str.toString();
}

/**
 * Return base62 representation for a number
 * @param {number} num
 * @param {string} alphabet
 * @return {string}
 * @memberof module:lib
 * @method toBase62
 */
lib.toBase62 = function(num, alphabet)
{
    var s = '';
    if (!alphabet) alphabet = this.base62;
    while (num > 0) {
        s = alphabet[num % alphabet.length] + s;
        num = Math.floor(num/alphabet.length);
    }
    return s;
}

/**
 * Convert base62 number as a string into base10 number
 * @param {string} num
 * @param {string} alphabet
 * @return {number}
 * @memberof module:lib
 * @method fromBase62
 */
lib.fromBase62 = function(num, alphabet)
{
    if (typeof num != "string") return 0;
    var total = 0, c;
    if (!alphabet) alphabet = this.base62;
    for (let i = 0; i < num.length; i++) {
        c = num[num.length - 1 - i];
        total += this.base62.indexOf(c) * 62 ** i;
    }
    return total;
}

/**
 * Return a well formatted and validated url or parsed URL object
 * @param {string} str
 * @param {object} [options]
 * @param {boolean} [options.url] - if true return the URL object not string
 * @return {string|URL}
 * @memberof module:lib
 * @method toUrl
 */
lib.toUrl = function(str, options)
{
    if (str) try {
        const u = new URL(str);
        return options?.url ? u : u.toString();
    } catch (e) {
        logger.error("toUrl:", e, str, options);
    }
    return "";
}

/**
 * Return a test representation of a number according to the money formatting rules,
 * @param {number} num
 * @param {object} [options]
 * @param {string} [options.locale=en-US]
 * @param {string} [options.currency=USD]
 * @param {string} [options.display=symbol]
 * @param {string} [options.sign=standard]
 * @param {int} [options.min=2]
 * @param {int} [options.max=3]
 * @memberof module:lib
 * @method toPrice
 */
lib.toPrice = function(num, options)
{
    try {
        return this.toNumber(num).toLocaleString(options?.locale || "en-US", { style: 'currency',
            currency: options?.currency || 'USD',
            currencyDisplay: options?.display || "symbol",
            currencySign: options?.sign || "standard",
            minimumFractionDigits: options?.min || 2,
            maximumFractionDigits: options?.max || 5 });
    } catch (e) {
        logger.error("toPrice:", e, num, options);
    }
    return "";
}

/**
 * Return an email address if valid
 * @param {string} val
 * @param {object} [options]
 * @param {boolean} [options.parse] extract the email from `name <email>` format]
 * @param {int} [options.max] - return "" if length greater than this
 * @return {string}
 * @memberof module:lib
 * @method toEmail
 */
lib.toEmail = function(val, options)
{
    if (typeof val != "string" || val.indexOf("..") > -1) return "";
    if (options?.parse) {
        var s = val.indexOf('<');
        if (s >= 0) {
            var e = val.indexOf('>', s);
            if (e > 0) val = val.substring(s + 1, e);
        }
    }
    if (options?.max && val.length > options.max) return "";
    return this.rxEmail.test(val) ? val.trim().toLowerCase() : "";
}

/**
 * Convert to an object from as string of key:val,... pairs
 * @param {string} val
 * @param {object} [options]
 * @param {boolean} [options.delimiter] - pairs separator
 * @param {boolean} [options.separator] - key and value separator
 * @param {boolean} [options.empty] - keep empty keys
 * @param {boolean} [options.noempty] - ignore empty values
 * @param {boolean} [options.mapcamel] - camelize keys
 * @param {string} [options.maptype] - convert values to this type
 * @return {object}
 * @memberof module:lib
 * @method toMap
 * @example
 * lib.toMap("a:1,b:2,c:4:5")
 * { a: '1', b: '2', c: [ '4', '5' ] }
 */
lib.toMap = function(val, options)
{
    return lib.split(val, options?.delimiter || ",").
        map((y) => (lib.split(y, options?.separator || /[:;]/, options))).
        reduce((a, b) => {
            let v;
            if (b.length < 2) {
                if (options?.empty) v = "";
            } else {
                v = b.length == 2 ? b[1] : b.slice(1);
                if (options?.maptype) v = lib.toValue(v, options.maptype, options);
            }
            if (options?.noempty && lib.isEmpty(v)) return a;
            if (options?.mapcamel) b[0] = lib.toCamel(b[0]);
            a[b[0]] = v;
            return a;
        }, {});
}

/**
 * Convert a value to the proper type, default is to return a string or convert the value to a string if no type is specified,
 * options is passed to all lib.toXXX functions as is, so type specific properties can be used.
 * @param {any} val
 * @param {string} [type]
 * - null|"" - return the value as is without any conversion
 * - auto - detect type with {@link module:lib.autoType}
 * - js - parse JSON into object or array
 * - set|list|array - use {@link module.lib.split} to convert into a list
 * - map - use {@link module.lib.toMap} to convert into an object
 * - real|float|double|decimal - to floating number
 * - int|long|number|now|counter|bigint - a number
 * - bool|boolean - boolean value
 * - date|time|datetime|timestamp - a Date object
 * - mtime - convert date into milliseconds
 * - url - use {@link module.lib.toUrl}
 * - email - use {@link module.lib.toEmail}
 * - phone|e164 - validate and convert into a valid phone number with only digits
 * - json - stringify a value
 * @param {object} [options]
 * @return {string|number|object|any}
 * @memberof module:lib
 * @method toValue
 */
lib.toValue = function(val, type, options)
{
    if (type === null || type === "") return val;
    type = typeof type == "string" && type.trim() || type;

    switch (type) {
    case "auto":
        if (typeof val == "undefined" || val === null) return "";
        type = this.autoType(val);
        return this.toValue(val, type, options);

    case "js":
        return typeof val == "string" ? this.jsonParse(val, options) : val;

    case "set":
    case "list":
    case "array":
        return this.split(val, options?.separator, options);

    case "map":
        return this.toMap(val, options);

    case "expr":
    case "buffer":
        return val;

    case "real":
    case "float":
    case "double":
    case "decimal":
        return this.toNumber(val, options, 1);

    case "int":
    case "int32":
    case "long":
    case "smallint":
    case "integer":
    case "number":
    case "bigint":
    case "numeric":
    case "counter":
    case "now":
    case "clock":
    case "ttl":
        return this.toNumber(val, options);

    case "bool":
    case "boolean":
        return this.toBool(val, options?.dflt);

    case "date":
    case "time":
    case "datetime":
    case "timestamp":
        return this.toDate(val, options?.dflt);

    case "mtime":
        return val ? this.toDate(val, options?.dflt).getTime() : 0;

    case "url":
        return this.toUrl(val, options);

    case "email":
        return this.toEmail(val, options);

    case "regexp":
        return this.toRegexp(val, options);

    case "phone":
    case "e164":
        if (typeof val == "number") {
            // Keep US phones without 1
            if (type[0] == "p" && val < 19999999999 && val > 10000000000) val -= 10000000000;
            if (type[0] == "e" && val < 10000000000) val += 10000000000;
            val = String(val);
        } else {
            if (typeof val != "string") return "";
            var d = val.match(this.rxPhone);
            if (!d) return "";
            val = this.toDigits(d[1]).slice(0, 15);
        }
        var min = typeof options?.min == "number" ? options.min : 5;
        if (min && val.length < min) return "";
        // Keep US phones without 1
        if (type[0] == "p" && val.length == 11 && val[0] == "1") val = val.substr(1);
        if (type[0] == "e" && val.length == 10) val = "1" + val;
        if (options?.max > 0 && val.length > options.max) return "";
        return val;

    case "json":
        return this.stringify(val);

    case "lower":
        return String(val).toLowerCase();

    case "upper":
        return String(val).toUpperCase();

    case "symbol":
        return this.rxSymbol.test(val) ? val : "";

    default:
        if (typeof options?.toValue == "function") {
            return options.toValue(val, options);
        }
        return this.toString(val, options);
    }
}

/**
 * Convert a value to a string, use default Javascript toString convertion of any object,
 * null|undefined will return ""
 * @param {any} val
 * @return {string}
 * @memberof module:lib
 * @method toString
 */
lib.toString = function(val)
{
    return typeof val == "string" ? val : val === null || val === undefined ? "" : String(val);
}

/**
 * Safely create a regexp object, if invalid returns undefined, the options can be a string with srandard RegExp
 * flags or an object with the following properties:
 * @param {string} str
 * @param {object} [options]
 * @param {boolean} [options.ingoreCase] - similar to i
 * @param {boolean} [options.globalMatch] - similar to m
 * @param {boolean} [options.multiLine] - similar to m
 * @param {boolean} [options.unicode] - similar to u
 * @param {boolean} [options.sticky] - similar to y
 * @param {boolean} [options.escape] - escape all special symbols or symbol `e`
 * @param {RegExp}
 * @memberof module:lib
 * @method toRegexp
 */
lib.toRegexp = function(str, options)
{
    if (str instanceof RegExp) return str;
    try {
        // Check for JSON stringified format
        if (typeof str == "string" && str.startsWith("^/") && str.endsWith("$")) {
            const e = str.lastIndexOf("/");
            if (e > -1) {
                options = str.slice(e + 1, -1)
                str = str.slice(2, e);
            }
        }
        var flags = typeof options == "string" && /^[igmuye]+$/.test(options) ? options :
                    options ? (options.ignoreCase ? "i" : "") +
                              (options.globalMatch ? "g" : "") +
                              (options.multiLine ? "m" : "") +
                              (options.unicode ? "u" : "") +
                              (options.escape ? "e" : "") +
                              (options.sticky ? "y" : "") : "";
        if (flags.indexOf("e") > -1) {
            if (str) str = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            flags = flags.replace("e", "");
        }
        return new RegExp(str, flags);
    } catch (e) {
        logger.error('toRegexp:', str, options, e);
    }
}

/**
 * Add a regexp to the list of regexp objects, this is used in the config type `regexpmap`.
 * @param {object[]} obj
 * @param {string} val
 * @param {object} [options]
 * @return {object[]} in format [ { rx, list }, { rx, list }]
 * @memberof module:lib
 * @method toRegexpMap
 */
lib.toRegexpMap = function(obj, val, options)
{
    if (val == null) return [];
    if (this.typeName(obj) != "array") obj = [];
    if (options?.set) obj = [];
    val = this.jsonParse(val, { datatype: "obj", logger: "error" });
    if (!val && options?.errnull) return null;
    for (const p in val) {
        if (obj.some((x) => {
            var i = x.list.indexOf(p[0] == "!" ? p.substr(1) : p);
            if (i > -1 && p[0] == "!") {
                x.list.splice(i, 1);
                lib.toRegexpObj(x, "", options);
            }
            return i > -1;
        })) continue;
        var item = this.toRegexpObj(null, p, options);
        if (!item) continue;
        item.value = options?.json ? lib.jsonParse(val[p], options) :
                     options?.datatype ? lib.toValue(val[p], options) : val[p];
        if (item.reset) obj = [];
        obj.push(item);
    }
    return obj;
}

/**
 * Add a regexp to the object that consist of list of patterns and compiled regexp, this is used in the config type `regexpobj`
 * @param {object} obj
 * @param {string} val
 * @param {object} [options]
 * @return {object} in format { list, rx }
 * @memberof module:lib
 * @method toRegexpObj
 */
lib.toRegexpObj = function(obj, val, options)
{
    if (val == null) obj = null;
    if (this.typeName(obj) != "object") obj = {};
    if (!Array.isArray(obj.list)) obj.list = [];
    if (val) {
        if (typeof val == "string" && (options?.del || val[0] == "!")) {
            var idx = obj.list.indexOf(val[0] == "!" ? val.substr(1) : val);
            if (idx > -1) obj.list.splice(idx, 1);
        } else {
            if (options?.set) obj.list = [];
            if (!Array.isArray(val)) val = [ val ];
            for (var i in val) {
                if (typeof val[i] != "string") continue;
                if (obj.list.indexOf(val[i]) == -1) obj.list.push(val[i]);
            }
        }
    }
    if (obj.list.length) {
        try {
            var str = obj.list.map((x) => {
                if (options?.escape) x = x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                return "(" + x + ")";
            }).join("|")
            obj.rx = new RegExp(str, options?.regexp);
        } catch (e) {
            logger.error('toRegexpObj:', val, e);
            if (options?.errnull) return null;
        }
    } else {
        obj.rx = null;
    }
    return obj;
}

/**
 * Parse the given `duration` and return milliseconds.
 *
 * @param {string} duration
 * @return {number|undefined}
 * @memberof module:lib
 * @method toMilliseconds
 * @example
 * lib.toMilliseconds('-3 day')
 * -259200000
 * lib.toMilliseconds('2.5 hrs')
 * 9000000
 * lib.toMilliseconds('1m')
 * 60000
 * lib.toMilliseconds('2.5 hr')
 * 9000000
 * lib.toMilliseconds('2.5 mon')
 * 6480000000
 */
lib.toMilliseconds = function(duration)
{
    var d = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|mos?|mons?|months?|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(duration);
    if (!d) return;
    var n = parseFloat(d[1]);
    if (d[2]) {
        var type = d[2].toLowerCase();
        switch (type[0]) {
        case "s":
            return n * 1000;
        case "h":
            return n * 3600000;
        case "d":
            return n * 86400000;
        case "m":
            if (!type[1]) return n * 60000;
            if (type[1] == "o") return n * 30 * 86400000;
            if (type[2] == "n") return n * 60000;
            break;
        case "w":
            return n * 86400000 * 7;
        case "y":
            return n * 86400000 * 365.25;
        }
    }
    return n;
}

/**
 * Given time in milliseconds, return how long ago it happened
 * @param {number} mtime
 * @param {object} [options]
 * @param {boolean|int} [options.age] - if true make it duration from now or if age > 1 then since that date in milliseconds
 * @param {boolean} [options.short] - if true use first letters only
 * @param {boolean} [options.round] - a number, 1 return only 1st part, 2 - 1st and 2nd parts
 * @return {string}
 * @memberof module:lib
 * @method toDuration
 */
lib.toDuration = function(mtime, options)
{
    var str = "";
    mtime = typeof mtime == "number" ? mtime : util.types.isDate(mtime) ? mtime.getTime() : this.toNumber(mtime);
    if (mtime > 0) {
        var lang = options?.lang;
        if (options?.age > 1) mtime = options.age - mtime; else
        if (options?.age > 0) mtime = Date.now() - mtime;

        var secs = Math.max(0, Math.floor(mtime/1000));
        var d = Math.floor(secs / 86400);
        var mm = Math.floor(d / 30);
        var w = Math.floor(d / 7);
        var h = Math.floor((secs - d * 86400) / 3600);
        var m = Math.floor((secs - d * 86400 - h * 3600) / 60);
        var s = Math.floor(secs - d * 86400 - h * 3600 - m * 60);
        if (mm > 0) {
            str = mm > 1 ? this.__({ phrase: options?.short ? "%sm": "%s months", lang: lang }, mm) :
                           this.__({ phrase: options?.short ? "1m" : "1 month", lang: lang });
            if (options?.round == 1) return str;
            if (d > 0) str += " " + (d > 1 ? this.__({ phrase: options?.short ? "%sd" : "%s days", lang: lang }, d) :
                                             this.__({ phrase: options?.short ? "1d" : "1 day", lang: lang }));
            if (options?.round == 2) return str;
            if (h > 0) str += " " + (h > 1 ? this.__({ phrase: options?.short ? "%sh" : "%s hours", lang: lang }, h) :
                                             this.__({ phrase: options?.short ? "1h": "1 hour", lang: lang }));
        } else
        if (w > 0) {
            str = w > 1 ? this.__({ phrase: options?.short ? "%sw" : "%s weeks", lang: lang }, w) :
                          this.__({ phrase: options?.short ? "1w" : "1 week", lang: lang });
            if (options?.round == 1) return str;
            if (d > 0) str += " " + (d > 1 ? this.__({ phrase: options?.short ? "%sd" : "%s days", lang: lang }, d) :
                                             this.__({ phrase: options?.short ? "1d" : "1 day", lang: lang }));
            if (options?.round == 2) return str;
            if (h > 0) str += " " + (h > 1 ? this.__({ phrase: options?.short ? "%sh" : "%s hours", lang: lang }, h) :
                                             this.__({ phrase: options?.short ? "1h" : "1 hour", lang: lang }));
        } else
        if (d > 0) {
            str = d > 1 ? this.__({ phrase: options?.short ? "%sd" : "%s days", lang: lang }, d) :
                          this.__({ phrase: options?.short ? "1d" : "1 day", lang: lang });
            if (options?.round == 1) return str;
            if (h > 0) str += " " + (h > 1 ? this.__({ phrase: options?.short ? "%sh" : "%s hours", lang: lang }, h) :
                                             this.__({ phrase: options?.short ? "1h" : "1 hour", lang: lang }));
            if (options?.round == 2) return str;
            if (m > 0) str += " " + (m > 1 ? this.__({ phrase: options?.short ? "%sm" : "%s minutes", lang: lang }, m) :
                                             this.__({ phrase: options?.short ? "1m" : "1 minute", lang: lang }));
        } else
        if (h > 0) {
            str = h > 1 ? this.__({ phrase: options?.short ? "%sh" : "%s hours", lang: lang }, h) :
                          this.__({ phrase: options?.short ? "1h" : "1 hour", lang: lang });
            if (options?.round == 1) return str;
            if (m > 0) str += " " + (m > 1 ? this.__({ phrase: options?.short ? "%sm" : "%s minutes", lang: lang }, m) :
                                             this.__({ phrase: options?.short ? "1m" : "1 minute", lang: lang }));
        } else
        if (m > 0) {
            str = m > 1 ? this.__({ phrase: options?.short ? "%sm" : "%s minutes", lang: lang }, m) :
                          this.__({ phrase: options?.short ? "1m" : "1 minute", lang: lang });
            if (options?.round == 1) return str;
            if (s > 0) str += " " + (s > 1 ? this.__({ phrase: options?.short ? "%ss" : "%s seconds", lang: lang }, s) :
                                             this.__({ phrase: options?.short ? "1s" : "1 second", lang: lang }));
        } else {
            str = secs > 1 ? this.__({ phrase: options?.short ? "%ss" : "%s seconds", lang: lang }, secs) :
                             this.__({ phrase: options?.short ? "1s" : "1 second", lang: lang });
        }
    }
    return str;
}

/**
 * Return size human readable format
 * @param {number} size
 * @param {boolean} [decimals=2]
 * @memberof module:lib
 * @method toSize
 */
lib.toSize = function(size, decimals = 2)
{
    var i = size > 0 ? Math.floor(Math.log(size) / Math.log(1024)) : 0;
    return (size / Math.pow(1024, i)).toFixed(typeof decimals == "number" ? decimals : 2) * 1 + ' ' + [this.__('Bytes'), this.__('KBytes'), this.__('MBytes'), this.__('GBytes'), this.__('TBytes')][i];
}

/**
 * An object to be used with toParams for validation
 * @typedef {object} ParamsOptions
 * @memberof module:lib
 * @property {boolean} name - to save a value with different name than in the original query
 * @property {string} [type] - convert the input to the type format, default text
 *   Supported types:
 *   - string types: string, text,
 *   - boolean types: bool, boolean,
 *   - numeric types: int, bigint, long, number, float, real, double, counter, clock, now, random
 *   - object types: list, map, obj, object, array, json,
 *   - date/time types: mtime, date, time, timestamp, datetime
 *   - special types: set, email, symbol, url, phone, e164, regexp
 *
 * @property {boolean} [dflt] - use this value if property does not exists or undefined
 * @property {boolean} [dfltempty] - also use the dflt value for empty properties
 * @property {boolean} [required] - if true the target value must not be empty, the check is performed after type conversion,
 *       if an object it checks the target object using `lib.isMatched` at the end
 * @property {boolean} [errmsg] - return this error on error or invalid format or required condition, it may contain %s sprintf-like placeholders depending on the error
 * @property {boolean} [min] - minimum length for the target data, returns an error if smaller, for list type will skip item from the list
 * @property {boolean} [max] -  maximum length alowed, returns an error if longer
 * @property {boolean} [trunc] - if true and longer than max just truncate the value instead of returning an error or skipping
 * @property {boolean} [separator] - for list type default separator is `,|`, for map type default is `:;`
 * @property {boolean} [delimiter] - map type contains elements separated by , by default, use another if commas are expected
 * @property {boolean} [regexp] - validate input against this regexp and return an error if not matched, for list type skip items not matched
 * @property {boolean} [noregexp] - validate the input against this regexp and return an error if matched, for list type skip items matched
 * @property {boolean} [datatype] - convert each value or item into this type, used by string/list types
 * @property {boolean} [maptype] - for maps convert each value to this type
 * @property {boolean} [novalue] - if the target value equals then ignore the parameter,
 *       can be a list of values to be ignored or an object { name, value }.
 *       For lists this is a number of items in the list, if less or equal the list is ignored or reset.
 * @property {boolean} [ignore] - if true skip this parameter
 * @property {boolean} [optional] - for date types, if true do not assign the current time for empty values
 * @property {boolean} [value] - assign this value unconditionally
 * @property {boolean} [values] - a list of allowed values, if not present the parameter is ignored
 * @property {boolean} [values_map] - an object map for values, replace matching values with a new one
 * @property {boolean} [params] - an object with schema to validate for json/obj/array types
 * @property {boolean} [empty] - if true and the target value is empty return as empty, by default empty values are ignored
 * @property {boolean} [setempty] - to be used with `empty`, instead of skipping set with this value at the end
 * @property {boolean} [keepempty] - for list type keep empty items in the list, default is skip empty items
 * @property {boolean} [minlist] - min allowed length of the target array for list/map types, returns error if less
 * @property {boolean} [maxlist] - max allowed length of the target array for list/map types, returns error if longer
 * @property {boolean} [minnum] - min allowed number after convertion by toNumber, for numbers and mtime
 * @property {boolean} [maxnum] - max allowed number after convertion by toNumber, for numbers and mtime
 * @property {boolean} [mindate] - min allowed date after convertion by toDate, can be a Date or number
 * @property {boolean} [maxdate] - max allowed date after convertion by toDate, can be a Date or number
 * @property {boolean} [label] - alternative name to use in error messages which uses sprintf-like placeholders,
 *       all min/max like errors have name as first and threshold as second arg, label can be set to use friendlier name
 * @property {boolean} [strip] - a regexp with characters to strip from the final value
 * @property {boolean} [upper/lower] - transform case
 * @property {boolean} [cap] - capitalize the value
 * @property {boolean} [trim] - trim the final value if a string
 * @property {boolean} [replace] - an object map with characters to be replaced with other values
 * @property {boolean} [base64] - decode from base64
 */

/**
 * Process incoming query and convert parameters according to the type definition, the schema contains the definition of the paramaters against which to
 * validate incoming data. It is an object with property names and definitoons that at least must specify the type, all other options are type specific.
 *
 * Returns a string message on error or an object
 *
 * @param {object} query - request query object, usually req.query or req.body
 * @param {object} schema - an object in format: { name: {@link module:lib.ParamsOptions}, ...}
 * @param {object} [options] - options can define the following properties to customize convertion:
 * @param {boolean} [options.null] - always return null on any error
 * @param {boolean} [options.setnull] - if the value is equal this or any value if an array then set property to null, useful to reset lists, maps...
 * @param {boolean} [options.existing] - skip properties if not present in the query
 * @param {string} [options.prefix] - prefix to be used when searching for the parameters in the query, only properties with this prefix will be processed. The resulting
 *     object will not have this prefix in the properties.
 * @param {string} [options.dprefix] - prefix to use when checking for defaults, defaults are checks in this order: dprefix+name, name, *.type, *
 * @param {object} [options.defaults] - to pass realtime or other custom options for the validation or convertion utilities as the first argument if not defined in the definition,
 *     this is the place to customize/add/override global parameter conditions without changing it. Exact parameter name is used or a wildcard in the format
 *     `*.type` where type id any valid type supported or just `*` for all parameters. Special default '**' is always applied to all parameters.
 * @return {string|object} string in case of an error or an object
 * @example
 *
 * var query = lib.toParams(req.query, {
 *        id: { type: "int" },
 *        count: { type: "int", min: 1, max: 10, dflt: 5 },
 *        age: { type: "int", minnum: 10, maxnum: 99 },
 *        name: { type: "string", max: 32, trunc: 1 },
 *        pair: { type: "map", maptype: "int" },
 *        code: { type: "string", regexp: /^[a-z]-[0-9]+$/, errmsg: "Valid code is required" },
 *        start: { type: "token", required: 1 },
 *        email: { type: "list", datatype: "email", novalue: ["a@a"] },
 *        email1: { type: "email", required: { email: null } },
 *        data: { type: "json", datatype: "obj" },
 *        mtime: { type: "mtime", name: "timestamp" },
 *        date: { type: "date", mindate: new Date(2000,1,1) },
 *        flag: { type: "bool", novalue: false },
 *        descr: { novalue: { name: "name", value: "test" }, replace: { "<": "!" } },
 *        internal: { ignore: 1 },
 *        tm: { type: "timestamp", optional: 1 },
 *        ready: { value: "ready" },
 *        state: { values: [ "ok","bad","good" ] },
 *        status: { value: [ "ok","done" ] },
 *        obj: { type: "obj", params: { id: { type: "int" }, name: {} } },
 *        arr: { type: "array", params: { id: { type: "int" }, name: {} } },
 *        ssn: { type: "string", regexp: /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/, errmsg: "Valid SSN is required" },
 *        phone: { type: "list", datatype: "number" },
 *        }, {
 *        defaults: {
 *            start: { secret: req.user.secret },
 *            name: { dflt: "test" },
 *            count: { max: 100 },
 *            email: { ignore: req.user.roles != "admin" },
 *            "*.string": { max: 255 },
 *            '*': { maxlist: 255 },
 *        });
 *
 * if (typeof query == "string) return api.sendReply(res, 400, query);
 *
 * @memberof module:lib
 * @method toParams
 */
lib.toParams = function(query, schema, options)
{
    var rc = {}, opts, dopts, dflts, p, n, v, required = [];
    dflts = options?.defaults || lib.empty;
    for (const name in schema) {
        v = schema[name];
        switch (this.typeName(v)) {
        case "undefined":
            continue;
        case "object":
            if (v.ignore) continue;
            break;
        default:
            v = { value: v };
        }
        opts = {};
        for (const c in v) opts[c] = v[c];
        dopts = (options?.dprefix ? dflts[options.dprefix + name] : null) ||
                dflts[name] ||
                dflts[opts.type ? '*.' + opts.type : '*.string'] ||
                dflts['*'];
        for (const p in dopts) if (opts[p] === undefined) opts[p] = dopts[p];
        for (const p in dflts["**"]) if (opts[p] === undefined) opts[p] = dflts["**"][p];
        if (opts.ignore) continue;
        opts.name = n = opts.name || name;
        p = options?.prefix ? options.prefix + name : name;
        if (options?.existing && !(p in query)) continue;
        v = query[p];
        if (options?.setnull && (options.setnull === v || lib.isFlag(options.setnull, v))) {
            rc[n] = null;
            continue;
        }
        if (v === undefined || (opts.dfltempty && this.isEmpty(v))) v = opts.dflt;
        if (opts.value !== undefined) {
            var val = opts.value;
            switch (this.typeName(val)) {
            case "object":
                val = [ val ];
            case "array":
                for (const i in val) {
                    var cond = val[i];
                    if (this.isTrue(cond.name ? rc[cond.name] : v, cond.value, cond.op, cond.type || opts.type)) {
                        opts.type = "set";
                        v = cond.set;
                        break;
                    }
                }
                break;
            default:
                opts.type = "set";
                v = val;
            }
        }
        logger.dev("toParams:", name, n, typeof v, v, "O:", opts, "D:", dopts);
        switch (opts.type) {
        case "set":
            if (v === undefined) {
                delete rc[n];
            } else {
                rc[n] = v;
            }
            break;

        case "boolean":
        case "bool":
            if (v !== undefined) rc[n] = this.toBool(v, opts.dflt);
            break;

        case "real":
        case "float":
        case "double":
            opts.float = 1;
        case "int":
        case "long":
        case "number":
        case "bigint":
        case "counter":
        case "clock":
        case "now":
        case "random":
            if (v !== undefined) rc[n] = this.toNumber(v, opts);
            break;

        case "regexp":
            if (typeof v != "string") break;
            if (opts.max > 0 && v.length > opts.max) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max length is %s", opts.label || name, opts.max);
            }
            rc[n] = this.toRegexp(v, opts);
            break;

        case "list":
            if (!v && !opts.empty) break;
            v = opts.keepempty ? (Array.isArray(v) ? v : this.phraseSplit(v, opts)) : this.split(v, opts.separator, opts);
            if (Array.isArray(opts.values)) v = v.filter((x) => (opts.values.indexOf(x) > -1));
            if (Array.isArray(opts.novalue)) v = v.filter((x) => (opts.novalue.indexOf(x) == -1));
            if (opts.minlist > 0 && v.length < opts.minlist) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too short, the min size is %s", opts.label || name, opts.minlist);
            }
            if (opts.maxlist > 0 && v.length > opts.maxlist) {
                if (!opts.trunc) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max size is %s", opts.label || name, opts.maxlist)
                }
                v = v.slice(0, opts.maxlist);
            }
            if ((!v || !v.length) && !opts.empty) break;
            if (v && opts.flatten) v = this.arrayFlatten(v);
            rc[n] = v || [];
            break;

        case "map":
            if (!v && !opts.empty) break;
            v = lib.split(v, opts.delimiter || ",");
            if (opts.maxlist > 0 && v.length > opts.maxlist) {
                if (!opts.trunc) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max size is %s", opts.label || name, opts.maxlist)
                }
                v = v.slice(0, opts.maxlist);
            }
            v = v.map((x) => (lib.split(x, opts.separator || /[:;]/, opts))).
                  reduce((a, b) => {
                      if (b.length < 2) return a;
                      a[b[0]] = b.length == 2 ? b[1] : b.slice(1);
                      if (opts.maptype) a[b[0]] = lib.toValue(a[b[0]], opts.maptype, opts);
                      return a;
                  }, {});
            if (this.isEmpty(v) && !opts.empty) break;
            if (!rc[n]) rc[n] = {};
            for (const p in v) rc[n][p] = v[p];
            break;

        case "obj":
            if (!v && !opts.empty) break;
            v = this.toParams(v || lib.empty, opts.params, { prefix: options?.prefix, dprefix: options?.dprefix, defaults: dflts });
            if (typeof v == "string") return options?.null ? null : v;
            if (opts.max > 0 && lib.objSize(v) > opts.max) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too large, the max size is %s", opts.label || name, opts.max)
            }
            if (!this.isEmpty(v) || opts.empty) rc[n] = v;
            break;

        case "object":
            if (!lib.isObject(v)) break;
            if (opts.params) {
                v = this.toParams(v, opts.params, { prefix: options?.prefix, dprefix: options?.dprefix, defaults: dflts });
                if (typeof v == "string") return options?.null ? null : v;
            }
            if (opts.max > 0 && lib.objSize(v) > opts.max) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too large, the max size is %s", opts.label || name, opts.max)
            }
            if (!this.isEmpty(v) || opts.empty) rc[n] = v;
            break;

        case "array":
            if (!v && !opts.empty) break;
            v = lib.isArray(v, []);
            if (opts.params) {
                const list = [];
                for (let a of v) {
                    a = lib.toParams(a, opts.params, { prefix: options?.prefix, dprefix: options?.dprefix, defaults: dflts })
                    if (typeof a == "string") return options?.null ? null : a;
                    list.push(a);
                }
                v = list;
            }
            if (opts.minlist > 0 && v.length < opts.minlist) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too short, the min length is %s", opts.label || name, opts.minlist)
            }
            if (opts.maxlist > 0 && v.length > opts.maxlist) {
                if (!opts.trunc) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max length is %s", opts.label || name, opts.maxlist)
                }
                v = v.slice(0, opts.maxlist);
            }
            if (v.length || opts.empty) rc[n] = v;
            break;

        case "token":
            if (!v) break;
            if (opts.max > 0 && v.length > opts.max) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max length is %s", opts.label || name, opts.max);
            }
            rc[n] = this.base64ToJson(v, opts.secret);
            break;

        case "mtime":
            if (!v) break;
            v = this.toDate(v, opts.dflt, true);
            if (v) {
                if (opts.mindate && v < opts.mindate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too soon, the earliest date is %s", opts.label || name, lib.toDate(opts.mindate));
                }
                if (opts.maxdate && v > opts.maxdate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too late, the latest date is %s", opts.label || name, lib.toDate(opts.maxdate));
                }
                rc[n] = v.getTime();
            }
            break;

        case "date":
        case "time":
            if (v) v = this.toDate(v, opts.dflt, true);
            if (v) {
                if (opts.mindate && v < opts.mindate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too soon, the earliest date is %s", opts.label || name, lib.toDate(opts.mindate));
                }
                if (opts.maxdate && v > opts.maxdate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too late, the latest date is %s", opts.label || name, lib.toDate(opts.maxdate));
                }
                rc[n] = v;
            }
            break;

        case "datetime":
            if (!opts.optional && (!v || (typeof v == "boolean" && v))) v = Date.now();
            if (v) v = this.toDate(v, opts.dflt);
            if (v) {
                if (opts.mindate && v < opts.mindate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too soon, the earliest date is %s", opts.label || name, lib.toDate(opts.mindate));
                }
                if (opts.maxdate && v > opts.maxdate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too late, the latest date is %s", opts.label || name, lib.toDate(opts.maxdate));
                }
                rc[n] = this.strftime(v, opts.format || "%Y/%m/%d %H:%M");
            }
            break;

        case "timestamp":
            if (!opts.optional && (!v || (typeof v == "boolean" && v))) v = Date.now();
            if (v) v = this.toDate(v, opts.dflt, true);
            if (v) {
                if (opts.mindate && v < opts.mindate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too soon, the earliest date is %s", opts.label || name, lib.toDate(opts.mindate));
                }
                if (opts.maxdate && v > opts.maxdate) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too late, the latest date is %s", opts.label || name, lib.toDate(opts.maxdate));
                }
                rc[n] = opts.format ? this.strftime(v, opts.format) : v.toISOString();
            }
            break;

        case "json":
            if (typeof v != "string") break;
            if (opts.max > 0 && v.length > opts.max) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max length is %s", opts.label || name, opts.max);
            }
            if (opts.base64) v = Buffer.from(v, "base64").toString();
            v = this.jsonParse(v, opts);
            if (opts.params) {
                v = this.toParams(v, opts.params, { prefix: options?.prefix, dprefix: options?.dprefix, defaults: dflts });
                if (typeof v == "string") return options?.null ? null : v;
            }
            if (v || opts.empty) rc[n] = v;
            break;

        default:
            if (typeof v == "undefined" || v === null) break;
            v = typeof v == "string" ? v : String(v);
            switch (opts.type) {
            case "symbol":
            case "email":
            case "phone":
            case "e164":
            case "url":
                if (v) {
                    v = this.toValue(v.trim(), opts.type, opts);
                }
                break;
            }
            if (opts.trim) v = v.trim();
            if (opts.base64) v = Buffer.from(v, "base64").toString();

            if (opts.max && v.length > opts.max) {
                if (!opts.trunc) {
                    return options?.null ? null : this.__(opts.errmsg || "%s is too long, the max length is %s", opts.label || name, opts.max);
                }
                v = v.substr(0, opts.max);
            }
            if (opts.min > 0 && v.length < opts.min) {
                return options?.null ? null : this.__(opts.errmsg || "%s is too short, the min length is %s", opts.label || name, opts.min);
            }
            if (opts.noregexp) {
                const rx = lib.isArray(opts.noregexp, [opts.noregexp]);
                if (rx.some((r) => (lib.testRegexp(v, r)))) {
                    if (!opts.required && opts.errmsg) return options?.null ? null : typeof opts.errmsg == "string" ? opts.errmsg : this.__("invalid characters in %s", opts.label || name);
                    break;
                }
            } else
            if (opts.regexp) {
                const rx = lib.isArray(opts.regexp, [opts.regexp]);
                if (!rx.some((r) => (lib.testRegexp(v, r)))) {
                    if (!opts.required && opts.errmsg) return options?.null ? null : typeof opts.errmsg == "string" ? opts.errmsg : this.__("invalid characters in %s", opts.label || name);
                    break;
                }
            }
            if (opts.replace) {
                for (const p in opts.replace) {
                    v = v.replaceAll(p, opts.replace[p]);
                }
            }
            if (opts.strip) v = v.replace(opts.strip, "");
            if (opts.upper) v = v.toUpperCase();
            if (opts.lower) v = v.toLowerCase();
            if (opts.camel) v = lib.toCamel(v, opts.camel);
            if (opts.cap) v = lib.toTitle(v, opts.cap);
            if (opts.datatype) v = lib.toValue(v, opts.datatype, opts);
            if (!v && !opts.empty) break;
            rc[n] = v;
            break;
        }
        v = rc[n];
        if (this.isEmpty(v)) {
            if (opts.setempty !== undefined) {
                v = rc[n] = opts.setempty;
            }
        } else {
            switch (opts.type) {
            case "list":
                if (typeof opts.novalue == "number" && v.length <= opts.novalue) {
                    delete rc[n];
                }
                break;

            default:
                if (typeof v == "number") {
                    if (opts.maxnum && v > opts.maxnum) {
                        return options?.null ? null : this.__(opts.errmsg || "%s is too large, the max value is %s", opts.label || name, opts.maxnum);
                    }
                    if (opts.minnum > 0 && v < opts.minnum) {
                        return options?.null ? null : this.__(opts.errmsg || "%s is too small, the min value is %s", opts.label || name, opts.minnum);
                    }
                }
                if (Array.isArray(opts.values) && !opts.values.includes(v)) {
                    delete rc[n];
                } else
                // Delete if equal to a special value(s)
                if (v === opts.novalue || Array.isArray(opts.novalue) && opts.novalue.includes(v)) {
                    delete rc[n];
                } else
                if (typeof opts.novalue == "object") {
                    if (v === rc[opts.novalue.name] || v === opts.novalue.value) delete rc[n];
                } else
                if (lib.isArray(opts.values_map)) {
                    for (let i = 0; i < opts.values_map.length - 1; i += 2) {
                        if (v === opts.values_map[i]) {
                            v = rc[n] = opts.values_map[i + 1];
                            break;
                        }
                    }
                }
            }
        }

        // Return an error if required, delay checks for complex conditions
        if (opts.required && this.isEmpty(rc[n])) {
            if (typeof opts.required != "object") {
                return options?.null ? null : opts.errmsg || this.__("%s is required", opts.label || name);
            }
            required.push(opts);
        }
    }
    // Delayed required checks against all properties
    for (const req of required) {
        if (this.isMatched(rc, req.required)) {
            return options?.null ? null : opts.errmsg || this.__("%s is required", req.label || req.name);
        }
    }
    return rc;
}

/**
 * Convert a list of records into the specified format, supported formats are: `xml, csv, json, jsontext`.
 * @param {string} format
 * - For `csv` the default separator is comma but can be specified with `options.separator`. To produce columns header specify `options.header`.
 * - For `json` format puts each record as a separate JSON object on each line, so to read it back
 *   it will require to read every line and parse it and add to the list.
 * - For `xml` format the name of the row tag is `<row>` but can be specified with `options.tag`.
 * @param {object|object[]} data
 * @param {object} [options]
 * @param {string[]} [options.allow] - which is a list of property names that are allowed only in the output for each record, non-existent
 * properties will be replaced by empty strings.
 * @param {object} [options.mapping] - object property can redefine different tag/header names to be put into the file
 * instead of the exact column names from the records.
 * @param {string} [options.quotes="] - quotes for CSV
 * @param {string} [options.separator=,] - CSV field separator
 * @memberof module:lib
 * @method toFormat
 */
lib.toFormat = function(format, data, options)
{
    var rows = Array.isArray(data) ? data : Array.isArray(data.data) ? data.data : this.isObject(data) ? [ data ] : [];
    if (!rows.length) return "";
    var allow = this.isArray(options?.allow);
    var v, map = options?.mapping || this.empty, text = "";

    switch (format) {
    case "xml":
        var tag = options?.tag || "row";
        for (var i = 0; i < rows.length; i++) {
            text += "<" + tag + ">\n";
            text += (allow || Object.keys(rows[i])).map((y) => {
                v = rows[i][y];
                v = Array.isArray(v) ? v.join(",") : typeof v == "object" ? lib.stringify(v) : String(v ?? "");
                var t = map[y] || y;
                return "<" + t + ">" + lib.textToXml(v) + "</" + t + ">\n";
            });
            text += "</" + tag + ">\n";
        }
        break;

    case "csv":
        var keys;
        var sep = options?.separator || ",";
        var quotes = options?.quotes || '"';
        var rx = new RegExp("[\r\n" + sep + quotes + "]");

        if (options?.header) {
            keys = allow || Object.keys(rows[0]);
            text += keys.map((x) => (map[x] || x)).join(sep) + "\r\n";
        }
        for (let i = 0; i < rows.length; i++) {
            keys = allow || Object.keys(rows[i]);
            text += keys.map((y) => {
                v = rows[i][y];
                v = Array.isArray(v) ? v.join(",") : typeof v == "object" ? lib.stringify(v) : String(v ?? "");
                if (!options?.newlines) {
                    v = v.replace(/[\r\n]/g, " ");
                }
                if (rx.test(v)) {
                    v = quotes + v.replaceAll(quotes, quotes + quotes) + quotes;
                }
                return v;
            }).join(sep) + "\r\n";
        }
        break;

    case "jsontext":
        for (let i = 0; i < rows.length; i++) {
            v = allow ? allow.reduce((x,y) => { if (!lib.isEmpty(rows[i][y])) x[map[y] || y] = rows[i][y]; return x }, {}) : rows[i];
            text += this.jsonFormat(v, options) + "\n";
        }
        break;

    default:
        for (let i = 0; i < rows.length; i++) {
            v = allow ? allow.reduce((x,y) => { if (!lib.isEmpty(rows[i][y])) x[map[y] || y] = rows[i][y]; return x }, {}) : rows[i];
            text += lib.stringify(v) + "\n";
        }
    }
    return text;
}

/**
 * Given a template with @..@ placeholders, replace each placeholder with the value from the obj.
 *
 * To use @ in the template specify it as @@
 *
 * Placeholders can have default or/and encoding
 *
 *  - default only: `@name|dflt@`
 *  - encoding with default: `@name|dflt|encoding@`
 *  - encoding no default: `@name||encoding@`
 *
 * Default placeholders:
 * - @exit@ - stop processing and return the template ignoring the rest
 * - @RAND@ - produce a random number using Math.random
 * - @n@ - produce a line break, newline
 * - @p@ - produce 2 newlines
 *
 * @param {string} text
 * @param {object|object[]} obj can be an object or an array of objects in which case all objects will be checked for the value until non empty.
 * @param {object} [options]
 * @param {string[]} [options.allow] - placeholders with a name present in this list will be replaced, all other will be replaced with empty string
 * @param {string[]} [options.skip] - placeholders with a name present in this list will be ignored, the placeholer will be kept
 * @param {string[]} [options.only] - placeholders with a name present in this list will be replaced only, all other will be ignored and kept as placeholders
 * @param {string} [options.encoding] - can be url or base64, the replaced values will be encoded accordingly
 * Encoding options:
 * - url, base64, entity, strftime, mtime
 * - d-url, d-base64, d-entity - decode value instead of encode it
 * @param {string} [options.separator1] - left side of the placehoder, default is @
 * @param {string} [options.separator2] - right side of the placeholder, default is @
 *
 * @example
 *
 * lib.toTemplate("http://www.site.com/@code@/@id@", { id: 123, code: "YYY" }, { encoding: "url" })
 * lib.toTemplate("Hello @name|friend@!", {})
 *
 * @memberof module:lib
 * @method toTemplate
 */

lib.toTemplate = function(text, obj, options)
{
    function encoder(enc, v) {
        switch (enc) {
        case "url":
            if (typeof v != "string") v = String(v);
            v = this.encodeURIComponent(v);
            break;
        case "d-url":
            if (typeof v != "string") v = String(v);
            v = this.decodeURIComponent(v);
            break;
        case "base64":
            if (typeof v != "string") v = String(v);
            v = Buffer.from(v).toString("base64");
            break;
        case "d-base64":
            if (typeof v != "string") v = String(v);
            v = Buffer.from(v, "base64").toString();
            break;
        case "entity":
            v = this.textToEntity(v);
            break;
        case "d-entity":
            v = this.entityToText(v);
            break;
        case "strftime":
            v = lib.strftime(v);
            break;
        case "mtime":
            v = lib.toMtime(v);
            break;
        case "price":
            v = lib.toPrice(v, options);
            break;
        }
        return v;
    }
    return this._toTemplate(text, obj, options, encoder);
}

lib._toTemplate = function(text, obj, options, encoder)
{
    if (typeof text != "string" || !text) return "";
    var i, j, rc = [], top;
    if (!options) options = {};
    if (options.__exit === undefined) {
        top = 1;
        options.__exit = 0;
    }
    if (!Array.isArray(obj)) obj = [obj];
    for (i = 0; i < obj.length; i++) {
        if (typeof obj[i] == "object" && obj[i]) rc.push(obj[i]);
    }

    const rxVal = /^([a-zA-Z0-9._-]+)(\|.+)?$/;
    const rxIf = /^(if|ifnull|ifnotnull|ifempty|ifnotempty|ifne|ifeq|ifgt|ifge|iflt|ifle|ifnot|ifall|ifstr|ifnotstr) ([a-zA-Z0-9._-]+) *(.*)$/;

    var tmpl = "", str = text, sep1 = options.separator1 || "@", sep2 = options.separator2 || sep1;
    while (str) {
        var start = str.indexOf(sep1);
        if (start == -1) {
            tmpl += str;
            break;
        }
        var end = str.indexOf(sep2, start + sep1.length);
        if (end == -1) {
            tmpl += str;
            break;
        }
        var tag = str.substr(start + sep1.length, end - start - sep2.length);
        tmpl += str.substr(0, start);
        str = str.substr(end + sep2.length);
        var d, v = null, dflt = null, field = null, enc = options.encoding;

        if (tag == "") {
            v = sep1;
        } else

        if (tag == "exit") {
            options.__exit = 1;
            break;
        } else

        if (tag == "RAND") {
            v = Math.random();
            tmpl += v;
            continue;
        } else

        if (tag == "n" || tag == "p") {
            v = tag == "p" ? "\n\n" : "\n";
            tmpl += v;
            continue;
        } else

        if (tag.startsWith("if")) {
            // @if type tester,admin@
            // @endif@
            end = str.indexOf(sep1 + "endif" + sep2);
            if (end == -1) continue;
            var body = str.substr(0, end);
            str = str.substr(end + 5 + sep1.length + sep2.length);
            d = tag.match(rxIf)
            if (!d) continue;
            var ok, val = null, t = d[2];
            i = t.indexOf(".");
            if (i > 0) {
                field = t.substr(i + 1);
                t = t.substr(0, i);
            }
            for (i = 0; i < rc.length && !val; i++) {
                val = typeof rc[i][t] == "function" ? rc[i][t]() : rc[i][t];
                if (val && field && typeof val == "object") {
                    field = field.split(".");
                    for (j = 0; val && j < field.length; j++) {
                        val = val ? val[field[j]] : undefined;
                        if (typeof val == "function") val = val();
                    }
                }
            }
            switch (d[1]) {
            case "ifnull":
                ok = val === null || val === undefined;
                break;
            case "ifnotnull":
                ok = !!val;
                break;
            case "ifempty":
                ok = lib.isEmpty(val);
                break;
            case "ifnotempty":
                ok = !lib.isEmpty(val);
                break;
            case "if":
                ok = val && lib.isFlag(lib.split(d[3]), lib.split(val));
                break;
            case "ifne":
                ok = val != d[3];
                break;
            case "ifnot":
                ok = !val || !lib.isFlag(lib.split(d[3]), lib.split(val));
                break;
            case "ifall":
                val = lib.split(val);
                ok = lib.split(d[3]).every((x) => (val.includes(x)));
                break;
            case "ifstr":
                ok = lib.testRegexp(val || "", lib.toRegexp(d[3], "i"));
                break;
            case "ifnotstr":
                ok = !lib.testRegexp(val || "", lib.toRegexp(d[3], "i"));
                break;
            case "ifeq":
                ok = val == d[3];
                break;
            case "ifgt":
                ok = val > d[3];
                break;
            case "iflt":
                ok = val < d[3];
                break;
            case "ifge":
                ok = val >= d[3];
                break;
            case "ifle":
                ok = val <= d[3];
                break;
            }
            end = body.indexOf(sep1 + "else" + sep2);
            if (ok) {
                if (end > -1) body = body.substr(0, end);
                v = this.toTemplate(body, rc, options);
            } else
            if (end > -1) {
                body = body.substr(end + 4 + sep1.length + sep2.length);
                v = this.toTemplate(body, rc, options);
            }
        } else {
            d = tag.match(rxVal);
            if (d) {
                tag = d[1];
                if (d[2]) dflt = d[2].substr(1);
                i = tag.indexOf(".");
                if (i > 0) {
                    field = tag.substr(i + 1);
                    tag = tag.substr(0, i);
                }
                if (dflt) {
                    i = dflt.indexOf("|");
                    if (i >= 0) {
                        enc = dflt.substr(i + 1);
                        dflt = dflt.substr(0, i);
                    }
                }
                for (i = 0; i < rc.length && !v; i++) {
                    v = typeof rc[i][tag] == "function" ? rc[i][tag]() : rc[i][tag];
                    if (v && field && typeof v == "object") {
                        field = field.split(".");
                        for (j = 0; v && j < field.length; j++) {
                            v = v ? v[field[j]] : undefined;
                            if (typeof v == "function") v = v();
                        }
                    }
                }
                if (typeof options.preprocess == "function") v = options.preprocess(tag, field, v, dflt, enc);
            } else {
                tmpl += sep1 + tag;
                str = sep2 + str;
                continue;
            }
            if (Array.isArray(options.allow) && !options.allow.includes(tag)) continue;
            if (Array.isArray(options.skip) && options.skip.includes(tag)) continue;
            if (Array.isArray(options.only) && !options.only.includes(tag)) v = sep1 + tag + sep2;
        }
        v ??= dflt;
        if (v) {
            if (Array.isArray(v) && (typeof v[0] == "string" || typeof v[0] == "number")) v = v.toString(); else
            if (typeof v == "object") v = this.stringify(v);
            if (encoder) v = encoder(enc, v, options);
        }
        if (v !== null && v !== undefined && v !== "") tmpl += v;
        if (options.__exit) break;
    }
    if (options.noline) tmpl = tmpl.replace(/[\r\n]/g, "");
    if (options.nospace) tmpl = tmpl.replace(/ {2,}/g, " ").trim();
    if (top) delete options.__exit;
    return tmpl;
}

/**
 * Flags command utility, the commands are:
 * @param {string} cmd
 * - add - adds the `name` flags to the list if does not exists, returns the same array
 * - update - adds new flags and removes flags that starts with - , returns the same array
 * - concat - same as add but always returns a new list
 * - del - removes the flags `name`, returns the same array
 * - present - returns only flags that present in the list `name`
 * - absent - returns only flags that are not present in the list `name`
 * @param {string[]} list
 * @param {string|string[]} name
 * @return {string[]}
 * @memberof module:lib
 * @method toFlags
 */
lib.toFlags = function(cmd, list, name)
{
    switch (cmd) {
    case "concat":
        list = Array.isArray(list) ? list.slice(0) : [];
    case "add":
        if (!Array.isArray(list)) list = [];
        if (!Array.isArray(name)) {
            if (name && !list.includes(name)) list.push(name);
        } else {
            name.forEach((x) => {
                if (x && !list.includes(x)) list.push(x);
            });
        }
        break;

    case "update":
        if (!Array.isArray(list)) list = [];
        if (!Array.isArray(name)) name = [name];
        name.forEach((x) => {
            if (typeof x == "string" && x[0] == "-") {
                var i = list.indexOf(x.substr(1));
                if (i > -1) list.splice(i, 1);
            } else {
                if (x && !list.includes(x)) list.push(x);
            }
        });
        break;

    case "del":
        if (!Array.isArray(list)) return [];
        if (!Array.isArray(name)) name = [name];
        name.forEach((x) => {
            var i = x && list.indexOf(x);
            if (i > -1) list.splice(i, 1);
        });
        break;

    case "present":
        if (!Array.isArray(list)) return [];
        if (!Array.isArray(name)) return list;
        list = list.filter((x) => (name.includes(x)));
        break;

    case "absent":
        if (!Array.isArray(list)) return [];
        if (!Array.isArray(name)) return list;
        list = list.filter((x) => (!name.includes(x)));
        break;
    }
    return list;
}

/**
 * Return RFC3339 formatted timestamp for a date or current time
 * @param {string} [date]
 * @return {string}
 * @memberof module:lib
 * @method toRFC3339
 */
lib.toRFC3339 = function (date)
{
    date = date ? date : new Date();
    var offset = date.getTimezoneOffset();
    return this.zeropad(date.getFullYear(), 4)
            + "-" + this.zeropad(date.getMonth() + 1, 2)
            + "-" + this.zeropad(date.getDate(), 2)
            + "T" + this.zeropad(date.getHours(), 2)
            + ":" + this.zeropad(date.getMinutes(), 2)
            + ":" + this.zeropad(date.getSeconds(), 2)
            + "." + this.zeropad(date.getMilliseconds(), 3)
            + (offset > 0 ? "-" : "+")
            + this.zeropad(Math.floor(Math.abs(offset) / 60), 2)
            + ":" + this.zeropad(Math.abs(offset) % 60, 2);
}

/**
 * Stringify JSON into base64 string, if secret is given, sign the data with it
 * @param {any} data
 * @param {string} secret
 * @param {object} [options]
 * @return {string}
 * @memberof module:lib
 * @method jsonToBase64
 */
lib.jsonToBase64 = function(data, secret, options)
{
    data = this.stringify(data);
    if (secret) return this.encrypt(secret, data, options);
    return Buffer.from(data).toString("base64");
}

/**
 * Parse base64 JSON into JavaScript object, in some cases this can be just a number then it is passed as it is, if secret is given verify
 * that data is not changed and was signed with the same secret
 * @param {any} data
 * @param {string} secret
 * @param {object} [options]
 * @return {object}
 * @memberof module:lib
 * @method base64ToJson
 */
lib.base64ToJson = function(data, secret, options)
{
    var rc = "";
    if (typeof data == "undefined" || data == null) return rc;
    if (secret) data = this.decrypt(secret, data, options);
    try {
        if (typeof data == "number" || (typeof data == "string" && data.match(/^[0-9]+$/))) {
            rc = this.toNumber(data);
        } else {
            if (!secret) data = Buffer.from(data, "base64").toString();
            if (data) rc = JSON.parse(data);
        }
    } catch (e) {
        logger.debug("base64ToJson:", e.stack, data);
    }
    return rc;
}

lib._jsonFormatPresets = {
    default: {
        indent: "",
        ilevel: 0,
        nl1: "\n",
        nl2: "\n",
        sbracket1: "[",
        sbracket2: "]",
        cbracket1: "{",
        cbracket2: "}",
        quote1: '"',
        quote2: '"',
        squote1: '"',
        squote2: '"',
        space: " ",
        nspace: 4,
        comma: ", ",
        sep: ", ",
        prefix: "",
        delim: " \r\n\t.,:;?!/-",
        over: 1.25,
    },
    compact: {
        sbracket1: "",
        sbracket2: "",
        cbracket1: "",
        cbracket2: "",
        nl1: "\n",
        nl2: "",
        quote1: "",
        quote2: "",
        squote1: "",
        squote2: "",
        comma: "",
        sep: "",
        space: " ",
        prefix: "   - ",
        skipnull: 1,
        skipempty: 1,
        wrap: 80,
    },
    html: {
        nl1: "<br>",
        prefix: "&nbsp;&nbsp;-&nbsp;",
        space: "&nbsp;",
    },
};

/**
 * Register or update a jsonFormat preset
 * @param {string} name
 * @param {object} options
 * @memberof module:lib
 * @method jsonFormatPreset
 */
lib.jsonFormatPreset = function(name, options)
{
    if (!name) return;
    var preset = lib._jsonFormatPresets[name];
    if (!preset) preset = preset = lib._jsonFormatPresets[name] = {};
    for (const p in options) preset[p] = options[p];
    return preset;
}

/**
 * Nicely format an object with indentations, optional `indentlevel` can be used to control until which level deep to use newlines for objects.
 * @param {object} obj
 * @param {object} [options]
 * @param {string} [options.preset] - predefined set of options, `compact` prints yaml-like text version, if a list all presets are combined
 * @param {string} [options.indent] - initial indent, empty default
 * @param {string} [options.ilevel] - level to start to use spaces for indentation, 0 default
 * @param {string} [options.ignore] - regexp with properties to ingore
 * @param {string} [options.skipnull] - do not print null/undefined/""
 * @param {string} [options.skipempty] - skip all empty object accorsding to `lib.isEmpty`
 * @param {string} [options.map] - an object to map property names
 * @param {string} [options.replace] - an object for string values replacement: { ORIG: REPL... }
 * @param {string} [options.preprocess] - a function(name, val, options) to run before prints, return undefined to skip
 * @param {string} [options.sbracket1, sbracket2] - open/close brackets for arrays, [ ]
 * @param {string} [options.cbracket1, cbracket2] - open close brackets for obejcts, { }
 * @param {string} [options.nl1, nl2] - newline chars before and after a single property
 * @param {string} [options.quote1, quote2] - quotes for property names
 * @param {string} [options.squote1, squote2] - quotes for string values
 * @param {string} [options.comma] - comma separator between items
 * @param {string} [options.sep] - separator between array items, comma by default
 * @param {string} [options.space] - symbol for indentation
 * @param {string} [options.nspace] - how many spaces to use for indentation, 4
 * @param {string} [options.prefix] - prefix for array items, each item on new line, requires `nl1`
 * @param {string} [options.wrap] - wrap long strings at this length
 * @param {string} [options.over] - number greater than 1 to allow extra characters over wrap length
 * @param {string} [options.delim] - characters that trigger wrapping
 * @memberof module:lib
 * @method jsonFormat
 */
lib.jsonFormat = function(obj, options)
{
    if (typeof options == "string") options = { indent: options, __level: 0 };
    if (!options) options = { __level: 0 };
    if (typeof options.__level != "number") options = lib.objClone(options, { __level: 0 });

    // Shortcut to parse and format json from the string
    if (typeof obj == "string" && obj != "") {
        if (!/^[[{.+]}]$/.test(obj.trim())) return obj;
        obj = this.jsonParse(obj, { dflt: { data: obj } });
    }

    if (!options.__preset) {
        var presets = lib.isArray(options.preset, [options.preset]);
        var preset = Object.assign({}, lib._jsonFormatPresets.default, ...presets.map((x) => (lib._jsonFormatPresets[x])));
        for (const p in preset) {
            if (options[p] === undefined) options[p] = preset[p];
        }
        options.__preset = 1;
    }

    var type = this.typeName(obj);
    var count = 0, val, h, t, indent;
    var text = type == "array" ? options.sbracket1 : options.cbracket1;
    var map = options.map || lib.empty;
    // Insert newlines only until specified level deep
    var nline = !options.indentlevel || options.__level < options.indentlevel;
    // Top level prefix set, skip new line for the first item
    var prefix = options.__prefix;
    delete options.__prefix;

    for (let name in obj) {
        if (options.ignore && options.ignore.test(name)) continue;
        val = obj[name];
        if (typeof options.preprocess == "function") {
            val = options.preprocess(name, val, options);
            if (val === undefined) continue;
        }

        if (options.skipnull && (val === "" || val === null || val === undefined)) continue;
        if (options.skipempty && this.isEmpty(val)) continue;
        if (options.skipvalue && options.skipvalue.test(val)) continue;

        h = options.hide && options.hide.test(name);
        if (count > 0) {
            text += type == "array" ? options.sep : options.comma;
        }
        name = map[name] || name;
        if (type != "array") {
            if (nline && options.nl1) {
                text += !count && (prefix || !options.__level) ? "" : options.nl1;
            }
            indent = "";
            if (!prefix || count) indent = options.indent;
            if (!prefix && options.__level >= options.ilevel) {
                indent += options.space.repeat(options.nspace);
            }
            t = options.quote1 + name + options.quote2 + ": ";
            text += indent + t;
            indent += options.space.repeat(t.length);
        } else
        if (options.prefix && options.nl1) {
            indent = options.indent + options.prefix;
            text += options.nl1 + indent;
        }

        switch (this.typeName(val)) {
        case "array":
        case "object":
            if (h) {
                text += Array.isArray(val) ? val.length : Object.keys(val).length + "...";
                break;
            }
            if (!options.__seen) options.__seen = [];
            if (options.__seen.indexOf(val) > -1) {
                text += "...";
                break;
            }
            if (type == "array" && options.prefix && options.nl1) {
                indent = options.__prefix = options.space.repeat(options.prefix.length);
            } else {
                indent = options.space.repeat(options.nspace);
            }
            options.indent += indent;
            options.__seen.push(val);
            options.__level++;
            text += this.jsonFormat(val, options);
            options.__level--;
            options.__seen.pop(val);
            options.indent = options.indent.substr(0, options.indent.length - indent.length);
            break;

        case "boolean":
        case "number":
            text += h ? "..." : val.toString();
            break;

        case "null":
            text += "null";
            break;

        case "string":
            if (h) {
                text += "...";
                break;
            }
            for (const r in options.replace) {
                val = val.replaceAll(r, options.replace[r]);
            }
            if (options.wrap > 0 && val.length > options.wrap && options.nl1) {
                text += lib.strWrap(val, { quotes: [options.squote1, options.squote2], wrap: options.wrap, nl: options.nl1, over: options.over, delim: options.delim, indent });
            } else {
                text += options.squote1 + val + options.squote2;
            }
            break;

        case "error":
        case "date":
        case "regexp":
            text += h ? "..." : val.toString();
            break;

        default:
            text += ("unknown: " + typeof(val));
        }
        count++;
    }
    text += type == "array" ? options.sbracket2 : ((nline && options.nl2 ? options.nl2 + options.indent : "") + options.cbracket2);
    return text;
}

/**
 * JSON stringify without exceptions, on error just returns an empty string and logs the error
 * @param {any} obj
 * @param {function} [replacer]
 * @param {string|number} [space]
 * @memberof module:lib
 * @method stringify
 */
lib.stringify = function(obj, replacer, space)
{
    try {
        return this.escapeUnicode(replacer || space ? JSON.stringify(obj, replacer, space) : JSON.stringify(obj));
    } catch (e) {
        logger.error("stringify:", e);
        return "";
    }
}

/**
 * Encode with additional symbols, convert these into percent encoded:
 *          ! -> %21, * -> %2A, ' -> %27, ( -> %28, ) -> %29
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method encodeURIComponent
 */
lib.encodeURIComponent = function(str)
{
    if (typeof str == "undefined") return "";
    try {
        return encodeURIComponent(str).replace(/[!'()*]/g, (c) => (`%${c.charCodeAt(0).toString(16).toUpperCase()}`));
    } catch (e) {
        logger.error("encodeURIComponent:", str, e.stack);
        return ""
    }
}
lib.escape = lib.encodeURIComponent;

/**
 * No-exception version of the global function, on error return empty string
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method decodeURIComponent
 */
lib.decodeURIComponent = function(str)
{
    if (typeof str == "undefined") return "";
    try {
        return decodeURIComponent(str);
    } catch (e) {
        logger.error("decodeURIComponent:", str, e.stack);
        return "";
    }
}

/**
 * Convert all Unicode binary symbols into Javascript text representation
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method escapeUnicode
 */
lib.escapeUnicode = function(str)
{
    return String(str).replace(/[\u007F-\uFFFF]/g, (m) => ("\\u" + ("0000" + m.charCodeAt(0).toString(16)).substr(-4)));
}

lib._unicodeCache = {};

/**
 * Replace Unicode symbols with ASCII equivalents, types is a string with list of types of characters to
 * replace, default is: opqs, for quotes,other,punctuations,spaces
 * @param {string} str
 * @param {string} [types=opqs]
 * @return {string}
 * @memberof module:lib
 * @method unicode2Ascii
 */
lib.unicode2Ascii = function(str, types)
{
    if (typeof str != "string") return "";
    types = typeof types == "string" && types || "opqs";
    var map = this._unicodeCache[types];
    if (!map) {
        map = this._unicodeCache[types] = {};
        for (var t of types) {
            Object.assign(this._unicodeCache[types], this.unicodeAsciiMap[t]);
        }
    }
    var rc = "";
    for (var c of str) rc += map[c] || c;
    return rc.trim();
}

/**
 * Convert escaped characters into native symbols
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method unescape
 */
lib.unescape = function(str)
{
    return String(str).replace(/\\(.)/g, function(_, c) {
        switch (c) {
        case '"': return '"';
        case "'": return "'";
        case "f": return "\f";
        case "b": return "\b";
        case "\\": return "\\";
        case "n": return "\n";
        case "r": return "\r";
        case "t": return "\t";
        default: return c;
        }
    });
}

/**
 * Convert all special symbols into xml entities
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method textToXml
 */
lib.textToXml = function(str)
{
    return String(str || "").replace(/([&<>'":])/g, function(_, n) {
      switch (n) {
      case '&': return '&amp;'
      case '<': return '&lt;'
      case '>': return '&gt;'
      case '"': return '&quot;'
      case "'": return '&apos;'
      default: return n;
      }
    });
}

/**
 * Convert all special symbols into html entities
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method textToEntity
 */
lib.textToEntity = function(str)
{
    if (typeof str != "string") return "";
    if (!this.textEntities) {
        this.textEntities = {};
        for (var p in this.htmlEntities) this.textEntities[this.htmlEntities[p]] = "&" + p + ";";
    }
    return str.replace(/([&<>'":])/g, (_, n) => (lib.textEntities[n] || n));
}

/**
 * Convert html entities into their original symbols
 * @param {string} str
 * @return {string}
 * @memberof module:lib
 * @method entityToText
 */
lib.entityToText = function(str)
{
    if (typeof str != "string") return "";
    return str.replace(/&(#?[a-zA-Z0-9]+);/g, function(_, n) {
        if (n[0] === '#') return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1));
        return lib.htmlEntities[n.toLowerCase()] || "";
    });
}

/**
 * Convert a Buffer into base32 string
 * @param {Buffer} bug - inout buffer
 * @param {object} [options]
 * @param {string} [options.alphabet] - alphabet to use for encoding, default is base32
 * @param {boolean} [options.padding] - if true add padding using =
 * @return {string} encoded value
 * @memberof module:lib
 * @method toBase32
 */
lib.toBase32 = function(buf, options)
{
    if (!Buffer.isBuffer(buf)) return "";
    const alphabet = options?.alphabet || this.base32;
    let bits = 0, value = 0, str = "";

    for (let i = 0; i < buf.length; i++) {
        value = (value << 8) | buf[i];
        bits += 8
        while (bits >= 5) {
            str += alphabet[(value >>> (bits - 5)) & 31];
            bits -= 5;
        }
    }
    if (bits > 0) {
        str += alphabet[(value << (5 - bits)) & 31];
    }
    if (options?.padding) {
        while ((str.length % 8) !== 0) str += "=";
    }
    return str;
}

/**
 * Convert a string in base32 into a Buffer
 * @param {string} str - base32 encoded string
 * @param {object} [options]
 * @param {string} [options.alphabet] - alphabet to use for decoding
 * @memberof module:lib
 * @method fromBase32
 */
lib.fromBase32 = function(str, options)
{
    if (typeof str != "string") return "";
    const alphabet = options?.alphabet || this.base32;
    let bits = 0, value = 0, index = 0, idx;
    const buf = Buffer.alloc((str.length * 5 / 8) | 0);
    for (let i = 0; i < str.length; i++) {
        idx = alphabet.indexOf(str[i]);
        if (idx === -1) return null;
        value = (value << 5) | idx;
        bits += 5;
        if (bits >= 8) {
            buf[index++] = (value >>> (bits - 8)) & 255;
            bits -= 8;
        }
    }
    return buf;
}

lib.unicodeAsciiMap = {
    d: {
        "\uFF10": "0", "\uFF11": "1", "\uFF12": "2", "\uFF13": "3", "\uFF14": "4",
        "\uFF15": "5", "\uFF16": "6", "\uFF17": "7", "\uFF18": "8", "\uFF19": "9",
    },

    l: {
        "\uFF21": "A", "\u1D00": "A", "\uFF22": "B", "\u0299": "B", "\uFF23": "C", "\u1D04": "C",
        "\uFF24": "D", "\u1D05": "D", "\uFF25": "E", "\u1D07": "E", "\uFF26": "F", "\uA730": "F",
        "\uFF27": "G", "\u0262": "G", "\uFF28": "H", "\u029C": "H", "\uFF29": "I", "\u026A": "I",
        "\uFF2A": "J", "\u1D0A": "J", "\uFF2B": "K", "\u1D0B": "K", "\uFF2C": "L", "\u029F": "L",
        "\uFF2D": "M", "\u1D0D": "M", "\uFF2E": "N", "\u0274": "N", "\uFF2F": "O", "\u1D0F": "O",
        "\uFF30": "P", "\u1D18": "P", "\uFF31": "Q", "\uFF32": "R", "\u0280": "R", "\uFF33": "S",
        "\uA731": "S", "\uFF34": "T", "\u1D1B": "T", "\uFF35": "U", "\u1D1C": "U", "\uFF36": "V",
        "\u1D20": "V", "\uFF37": "W", "\u1D21": "W", "\uFF38": "X", "\uFF39": "Y", "\u028F": "Y",
        "\uFF3A": "Z", "\u1D22": "Z",
    },

    q: {
        "\u00AB": "\"", "\u00BB": "\"", "\u201C": "\"", "\u201D": "\"", "\u02BA": "\"", "\u02EE": "\"",
        "\u201F": "\"", "\u275D": "\"", "\u275E": "\"", "\u301D": "\"", "\u301E": "\"",
        "\uFF02": "\"", "\u2018": "'", "\u2019": "'", "\u02BB": "'", "\u02C8": "'", "\u02BC": "'",
        "\u02BD": "'", "\u02B9": "'", "\u201B": "'", "\uFF07": "'", "\u00B4": "'", "\u02CA": "'",
        "\u0060": "'", "\u02CB": "'", "\u275B": "'", "\u275C": "'", "\u0313": "'", "\u0314": "'",
        "\uFE10": "'", "\uFE11": "'", "\u00A0": "'", "\u2000": "'", "\u201E": "\"",
    },

    o: {
        "\u00BC": "1/4", "\u00BD": "1/2", "\u00BE": "3/4",

        "\u20D2": "|", "\u20D3": "|", "\u2223": "|", "\uFF5C": "|", "\u23B8": "|",
        "\u23B9": "|", "\u23D0": "|", "\u239C": "|", "\u239F": "|",

        "\uFE6B": "@", "\uFF20": "@",
        "\uFE69": "$", "\uFF04": "$",
        "\uFE5F": "#", "\uFF03": "#",
        "\uFE6A": "%", "\uFF05": "%",
        "\uFE60": "&", "\uFF06": "&",


        "\u2768": "(", "\u276A": "(", "\uFE59": "(", "\uFF08": "(", "\u27EE": "(", "\u2985": "(",
        "\u2769": ")", "\u276B": ")", "\uFE5A": ")", "\uFF09": ")", "\u27EF": ")", "\u2986": ")",

        "\u204E": "*", "\u2217": "*", "\u229B": "*", "\u2722": "*", "\u2723": "*",
        "\u2724": "*", "\u2725": "*", "\u2731": "*", "\u2732": "*", "\u2733": "*", "\u273A": "*",
        "\u273B": "*", "\u273C": "*", "\u273D": "*", "\u2743": "*", "\u2749": "*", "\u274A": "*",
        "\u274B": "*", "\u29C6": "*", "\uFE61": "*", "\uFF0A": "*",

        "\u02D6": "+", "\uFE62": "+", "\uFF0B": "+",

        "\u00F7": "/", "\u29F8": "/", "\u0337": "/", "\u0338": "/", "\u2044": "/", "\u2215": "/", "\uFF0F": "/",
        "\u29F9": "\\", "\u29F5": "\\", "\u20E5": "\\", "\uFE68": "\\", "\uFF3C": "\\",

        "\uFE64": "<", "\uFF1C": "<", "\u2039": ">", "\u203A": "<", "\uFE65": ">", "\uFF1E": ">",

        "\u0347": "=", "\uA78A": "=", "\uFE66": "=", "\uFF1D": "=",

        "\u02C6": "^", "\u0302": "^", "\uFF3E": "^", "\u1DCD": "^",
        "\u2774": "{", "\uFE5B": "{", "\uFF5B": "{", "\u2775": "}", "\uFE5C": "}", "\uFF5D": "}",
        "\uFF3B": "[", "\uFF3D": "]",
        "\u02DC": "~", "\u02F7": "~", "\u0303": "~", "\u0330": "~", "\u0334": "~", "\u223C": "~", "\uFF5E": "~",

    },

    p: {
        "\u3002": ".", "\uFE52": ".", "\uFF0E": ".", "\uFF61": ".",
        "\uFF64": ", ", "\u201A": ", ", "\u0326": ", ", "\uFE50": ", ", "\uFE51": ", ", "\uFF0C": ", ",
        "\u02D0": ":", "\u02F8": ":", "\u2982": ":", "\uA789": ":", "\uFE13": ":", "\uFF1A": ":",
        "\u204F": ";", "\uFE14": ";", "\uFE54": ";", "\uFF1B": ";",
        "\uFE16": "?", "\uFE56": "?", "\uFF1F": "?",
        "\u01C3": "!", "\uFE15": "!", "\uFE57": "!", "\uFF01": "!",
        "\u2026": "...", "\u203C": "!!",
        "\u0332": "_", "\uFF3F": "_", "\u2017": "_", "\u2014": "-", "\u2013": "-",
        "\u23BC": "-", "\u23BD": "-", "\u2015": "-", "\uFE63": "-", "\uFF0D": "-", "\u2010": "-", "\u2043": "-",
    },

    s: {
        "\u2000": " ", "\u2001": " ", "\u2002": " ", "\u2003": " ", "\u2004": " ", "\u2005": " ", "\u2006": " ",
        "\u2007": " ", "\u2008": " ", "\u2009": " ", "\u200A": " ", "\u200B": " ", "\u200E": " ",
        "\u202F": " ", "\u205F": " ", "\u2062": " ", "\u2063": " ", "\u2064": " ", "\u206B": " ",
        "\u008D": " ", "\u009F": " ", "\u0080": " ", "\u0090": " ", "\u009B": " ", "\u0010": " ",
        "\u0009": " ", "\u0000": " ", "\u0003": " ", "\u0004": " ", "\u0017": " ", "\u0019": " ",
        "\u0011": " ", "\u0012": " ", "\u0013": " ", "\u0014": " ", "\u2028": " ", "\u2029": " ",
        "\u2060": " ", "\u202C": " ",
        "\u3000": " ", "\u3164": " ",
        "\u00AD": " ", "\u00A0": " ",
        "\u1680": " ",

    }
};

lib.htmlEntities = {
        'AElig': 'Æ','AMP': '','Aacute': 'Á','Abreve': 'Ă','Acirc': 'Â',
        'Acy': 'А','Afr': '𝔄','Agrave': 'À','Alpha': 'Α','Amacr': 'Ā',
        'And': '⩓','Aogon': 'Ą','Aopf': '𝔸','ApplyFunction': '','Aring': 'Å',
        'Ascr': '𝒜','Assign': '≔','Atilde': 'Ã','Auml': 'Ä','Backslash': '∖',
        'Barv': '⫧','Barwed': '⌆','Bcy': 'Б','Because': '∵','Bernoullis': 'ℬ',
        'Beta': 'Β','Bfr': '𝔅','Bopf': '𝔹','Breve': '˘','Bscr': 'ℬ',
        'Bumpeq': '≎','CHcy': 'Ч','COPY': '©','Cacute': 'Ć','Cap': '⋒',
        'CapitalDifferentialD': 'ⅅ','Cayleys': 'ℭ','Ccaron': 'Č','Ccedil': 'Ç','Ccirc': 'Ĉ',
        'Cconint': '∰','Cdot': 'Ċ','Cedilla': '¸','CenterDot': '·','Cfr': 'ℭ',
        'Chi': 'Χ','CircleDot': '⊙','CircleMinus': '⊖','CirclePlus': '⊕','CircleTimes': '⊗',
        'ClockwiseContourIntegral': '∲','CloseCurlyDoubleQuote': '”','CloseCurlyQuote': '’','Colon': '∷','Colone': '⩴',
        'Congruent': '≡','Conint': '∯','ContourIntegral': '∮','Copf': 'ℂ','Coproduct': '∐',
        'CounterClockwiseContourIntegral': '∳','Cross': '⨯','Cscr': '𝒞','Cup': '⋓','CupCap': '≍',
        'DD': 'ⅅ','DDotrahd': '⤑','DJcy': 'Ђ','DScy': 'Ѕ','DZcy': 'Џ',
        'Dagger': '‡','Darr': '↡','Dashv': '⫤','Dcaron': 'Ď','Dcy': 'Д',
        'Del': '∇','Delta': 'Δ','Dfr': '𝔇','DiacriticalAcute': '´','DiacriticalDot': '˙',
        'DiacriticalDoubleAcute': '˝','DiacriticalGrave': '`','DiacriticalTilde': '˜','Diamond': '⋄','DifferentialD': 'ⅆ',
        'Dopf': '𝔻','Dot': '¨','DotDot': '⃜','DotEqual': '≐','DoubleContourIntegral': '∯',
        'DoubleDot': '¨','DoubleDownArrow': '⇓','DoubleLeftArrow': '⇐','DoubleLeftRightArrow': '⇔','DoubleLeftTee': '⫤',
        'DoubleLongLeftArrow': '⟸','DoubleLongLeftRightArrow': '⟺','DoubleLongRightArrow': '⟹','DoubleRightArrow': '⇒','DoubleRightTee': '⊨',
        'DoubleUpArrow': '⇑','DoubleUpDownArrow': '⇕','DoubleVerticalBar': '∥','DownArrow': '↓','DownArrowBar': '⤓',
        'DownArrowUpArrow': '⇵','DownBreve': '̑','DownLeftRightVector': '⥐','DownLeftTeeVector': '⥞','DownLeftVector': '↽',
        'DownLeftVectorBar': '⥖','DownRightTeeVector': '⥟','DownRightVector': '⇁','DownRightVectorBar': '⥗','DownTee': '⊤',
        'DownTeeArrow': '↧','Downarrow': '⇓','Dscr': '𝒟','Dstrok': 'Đ','ENG': 'Ŋ',
        'ETH': 'Ð','Eacute': 'É','Ecaron': 'Ě','Ecirc': 'Ê','Ecy': 'Э',
        'Edot': 'Ė','Efr': '𝔈','Egrave': 'È','Element': '∈','Emacr': 'Ē',
        'EmptySmallSquare': '◻','EmptyVerySmallSquare': '▫','Eogon': 'Ę','Eopf': '𝔼','Epsilon': 'Ε',
        'Equal': '⩵','EqualTilde': '≂','Equilibrium': '⇌','Escr': 'ℰ','Esim': '⩳',
        'Eta': 'Η','Euml': 'Ë','Exists': '∃','ExponentialE': 'ⅇ','Fcy': 'Ф',
        'Ffr': '𝔉','FilledSmallSquare': '◼','FilledVerySmallSquare': '▪','Fopf': '𝔽','ForAll': '∀',
        'Fouriertrf': 'ℱ','Fscr': 'ℱ','GJcy': 'Ѓ','GT': '>','Gamma': 'Γ',
        'Gammad': 'Ϝ','Gbreve': 'Ğ','Gcedil': 'Ģ','Gcirc': 'Ĝ','Gcy': 'Г',
        'Gdot': 'Ġ','Gfr': '𝔊','Gg': '⋙','Gopf': '𝔾','GreaterEqual': '≥',
        'GreaterEqualLess': '⋛','GreaterFullEqual': '≧','GreaterGreater': '⪢','GreaterLess': '≷','GreaterSlantEqual': '⩾',
        'GreaterTilde': '≳','Gscr': '𝒢','Gt': '≫','HARDcy': 'Ъ','Hacek': 'ˇ',
        'Hat': '^','Hcirc': 'Ĥ','Hfr': 'ℌ','HilbertSpace': 'ℋ','Hopf': 'ℍ',
        'HorizontalLine': '─','Hscr': 'ℋ','Hstrok': 'Ħ','HumpDownHump': '≎','HumpEqual': '≏',
        'IEcy': 'Е','IJlig': 'IJ','IOcy': 'Ё','Iacute': 'Í','Icirc': 'Î',
        'Icy': 'И','Idot': 'İ','Ifr': 'ℑ','Igrave': 'Ì','Im': 'ℑ',
        'Imacr': 'Ī','ImaginaryI': 'ⅈ','Implies': '⇒','Int': '∬','Integral': '∫',
        'Intersection': '⋂','InvisibleComma': '','InvisibleTimes': '','Iogon': 'Į','Iopf': '𝕀',
        'Iota': 'Ι','Iscr': 'ℐ','Itilde': 'Ĩ','Iukcy': 'І','Iuml': 'Ï',
        'Jcirc': 'Ĵ','Jcy': 'Й','Jfr': '𝔍','Jopf': '𝕁','Jscr': '𝒥',
        'Jsercy': 'Ј','Jukcy': 'Є','KHcy': 'Х','KJcy': 'Ќ','Kappa': 'Κ',
        'Kcedil': 'Ķ','Kcy': 'К','Kfr': '𝔎','Kopf': '𝕂','Kscr': '𝒦',
        'LJcy': 'Љ','LT': '<','Lacute': 'Ĺ','Lambda': 'Λ','Lang': '⟪',
        'Laplacetrf': 'ℒ','Larr': '↞','Lcaron': 'Ľ','Lcedil': 'Ļ','Lcy': 'Л',
        'LeftAngleBracket': '⟨','LeftArrow': '←','LeftArrowBar': '⇤','LeftArrowRightArrow': '⇆','LeftCeiling': '⌈',
        'LeftDoubleBracket': '⟦','LeftDownTeeVector': '⥡','LeftDownVector': '⇃','LeftDownVectorBar': '⥙','LeftFloor': '⌊',
        'LeftRightArrow': '↔','LeftRightVector': '⥎','LeftTee': '⊣','LeftTeeArrow': '↤','LeftTeeVector': '⥚',
        'LeftTriangle': '⊲','LeftTriangleBar': '⧏','LeftTriangleEqual': '⊴','LeftUpDownVector': '⥑','LeftUpTeeVector': '⥠',
        'LeftUpVector': '↿','LeftUpVectorBar': '⥘','LeftVector': '↼','LeftVectorBar': '⥒','Leftarrow': '⇐',
        'Leftrightarrow': '⇔','LessEqualGreater': '⋚','LessFullEqual': '≦','LessGreater': '≶','LessLess': '⪡',
        'LessSlantEqual': '⩽','LessTilde': '≲','Lfr': '𝔏','Ll': '⋘','Lleftarrow': '⇚',
        'Lmidot': 'Ŀ','LongLeftArrow': '⟵','LongLeftRightArrow': '⟷','LongRightArrow': '⟶','Longleftarrow': '⟸',
        'Longleftrightarrow': '⟺','Longrightarrow': '⟹','Lopf': '𝕃','LowerLeftArrow': '↙','LowerRightArrow': '↘',
        'Lscr': 'ℒ','Lsh': '↰','Lstrok': 'Ł','Lt': '≪','Map': '⤅',
        'Mcy': 'М','MediumSpace': ' ','Mellintrf': 'ℳ','Mfr': '𝔐','MinusPlus': '∓',
        'Mopf': '𝕄','Mscr': 'ℳ','Mu': 'Μ','NJcy': 'Њ','Nacute': 'Ń',
        'Ncaron': 'Ň','Ncedil': 'Ņ','Ncy': 'Н','NegativeMediumSpace': '','NegativeThickSpace': '',
        'NegativeThinSpace': '','NegativeVeryThinSpace': '','NestedGreaterGreater': '≫','NestedLessLess': '≪','NewLine': '\n',
        'Nfr': '𝔑','NoBreak': '','NonBreakingSpace': ' ','Nopf': 'ℕ','Not': '⫬',
        'NotCongruent': '≢','NotCupCap': '≭','NotDoubleVerticalBar': '∦','NotElement': '∉','NotEqual': '≠',
        'NotEqualTilde': '≂̸','NotExists': '∄','NotGreater': '≯','NotGreaterEqual': '≱','NotGreaterFullEqual': '≧̸',
        'NotGreaterGreater': '≫̸','NotGreaterLess': '≹','NotGreaterSlantEqual': '⩾̸','NotGreaterTilde': '≵','NotHumpDownHump': '≎̸',
        'NotHumpEqual': '≏̸','NotLeftTriangle': '⋪','NotLeftTriangleBar': '⧏̸','NotLeftTriangleEqual': '⋬','NotLess': '≮',
        'NotLessEqual': '≰','NotLessGreater': '≸','NotLessLess': '≪̸','NotLessSlantEqual': '⩽̸','NotLessTilde': '≴',
        'NotNestedGreaterGreater': '⪢̸','NotNestedLessLess': '⪡̸','NotPrecedes': '⊀','NotPrecedesEqual': '⪯̸','NotPrecedesSlantEqual': '⋠',
        'NotReverseElement': '∌','NotRightTriangle': '⋫','NotRightTriangleBar': '⧐̸','NotRightTriangleEqual': '⋭','NotSquareSubset': '⊏̸',
        'NotSquareSubsetEqual': '⋢','NotSquareSuperset': '⊐̸','NotSquareSupersetEqual': '⋣','NotSubset': '⊂⃒','NotSubsetEqual': '⊈',
        'NotSucceeds': '⊁','NotSucceedsEqual': '⪰̸','NotSucceedsSlantEqual': '⋡','NotSucceedsTilde': '≿̸','NotSuperset': '⊃⃒',
        'NotSupersetEqual': '⊉','NotTilde': '≁','NotTildeEqual': '≄','NotTildeFullEqual': '≇','NotTildeTilde': '≉',
        'NotVerticalBar': '∤','Nscr': '𝒩','Ntilde': 'Ñ','Nu': 'Ν','OElig': 'Œ',
        'Oacute': 'Ó','Ocirc': 'Ô','Ocy': 'О','Odblac': 'Ő','Ofr': '𝔒',
        'Ograve': 'Ò','Omacr': 'Ō','Omega': 'Ω','Omicron': 'Ο','Oopf': '𝕆',
        'OpenCurlyDoubleQuote': '“','OpenCurlyQuote': '‘','Or': '⩔','Oscr': '𝒪','Oslash': 'Ø',
        'Otilde': 'Õ','Otimes': '⨷','Ouml': 'Ö','OverBar': '‾','OverBrace': '⏞',
        'OverBracket': '⎴','OverParenthesis': '⏜','PartialD': '∂','Pcy': 'П','Pfr': '𝔓',
        'Phi': 'Φ','Pi': 'Π','PlusMinus': '±','Poincareplane': 'ℌ','Popf': 'ℙ',
        'Pr': '⪻','Precedes': '≺','PrecedesEqual': '⪯','PrecedesSlantEqual': '≼','PrecedesTilde': '≾',
        'Prime': '″','Product': '∏','Proportion': '∷','Proportional': '∝','Pscr': '𝒫',
        'Psi': 'Ψ','QUOT': '"','Qfr': '𝔔','Qopf': 'ℚ','Qscr': '𝒬',
        'RBarr': '⤐','REG': '®','Racute': 'Ŕ','Rang': '⟫','Rarr': '↠',
        'Rarrtl': '⤖','Rcaron': 'Ř','Rcedil': 'Ŗ','Rcy': 'Р','Re': 'ℜ',
        'ReverseElement': '∋','ReverseEquilibrium': '⇋','ReverseUpEquilibrium': '⥯','Rfr': 'ℜ','Rho': 'Ρ',
        'RightAngleBracket': '⟩','RightArrow': '→','RightArrowBar': '⇥','RightArrowLeftArrow': '⇄','RightCeiling': '⌉',
        'RightDoubleBracket': '⟧','RightDownTeeVector': '⥝','RightDownVector': '⇂','RightDownVectorBar': '⥕','RightFloor': '⌋',
        'RightTee': '⊢','RightTeeArrow': '↦','RightTeeVector': '⥛','RightTriangle': '⊳','RightTriangleBar': '⧐',
        'RightTriangleEqual': '⊵','RightUpDownVector': '⥏','RightUpTeeVector': '⥜','RightUpVector': '↾','RightUpVectorBar': '⥔',
        'RightVector': '⇀','RightVectorBar': '⥓','Rightarrow': '⇒','Ropf': 'ℝ','RoundImplies': '⥰',
        'Rrightarrow': '⇛','Rscr': 'ℛ','Rsh': '↱','RuleDelayed': '⧴','SHCHcy': 'Щ',
        'SHcy': 'Ш','SOFTcy': 'Ь','Sacute': 'Ś','Sc': '⪼','Scaron': 'Š',
        'Scedil': 'Ş','Scirc': 'Ŝ','Scy': 'С','Sfr': '𝔖','ShortDownArrow': '↓',
        'ShortLeftArrow': '←','ShortRightArrow': '→','ShortUpArrow': '↑','Sigma': 'Σ','SmallCircle': '∘',
        'Sopf': '𝕊','Sqrt': '√','Square': '□','SquareIntersection': '⊓','SquareSubset': '⊏',
        'SquareSubsetEqual': '⊑','SquareSuperset': '⊐','SquareSupersetEqual': '⊒','SquareUnion': '⊔','Sscr': '𝒮',
        'Star': '⋆','Sub': '⋐','Subset': '⋐','SubsetEqual': '⊆','Succeeds': '≻',
        'SucceedsEqual': '⪰','SucceedsSlantEqual': '≽','SucceedsTilde': '≿','SuchThat': '∋','Sum': '∑',
        'Sup': '⋑','Superset': '⊃','SupersetEqual': '⊇','Supset': '⋑','THORN': 'Þ',
        'TRADE': '™','TSHcy': 'Ћ','TScy': 'Ц','Tab': '  ','Tau': 'Τ',
        'Tcaron': 'Ť','Tcedil': 'Ţ','Tcy': 'Т','Tfr': '𝔗','Therefore': '∴',
        'Theta': 'Θ','ThickSpace': '  ','ThinSpace': ' ','Tilde': '∼','TildeEqual': '≃',
        'TildeFullEqual': '≅','TildeTilde': '≈','Topf': '𝕋','TripleDot': '⃛','Tscr': '𝒯',
        'Tstrok': 'Ŧ','Uacute': 'Ú','Uarr': '↟','Uarrocir': '⥉','Ubrcy': 'Ў',
        'Ubreve': 'Ŭ','Ucirc': 'Û','Ucy': 'У','Udblac': 'Ű','Ufr': '𝔘',
        'Ugrave': 'Ù','Umacr': 'Ū','UnderBar': '_','UnderBrace': '⏟','UnderBracket': '⎵',
        'UnderParenthesis': '⏝','Union': '⋃','UnionPlus': '⊎','Uogon': 'Ų','Uopf': '𝕌',
        'UpArrow': '↑','UpArrowBar': '⤒','UpArrowDownArrow': '⇅','UpDownArrow': '↕','UpEquilibrium': '⥮',
        'UpTee': '⊥','UpTeeArrow': '↥','Uparrow': '⇑','Updownarrow': '⇕','UpperLeftArrow': '↖',
        'UpperRightArrow': '↗','Upsi': 'ϒ','Upsilon': 'Υ','Uring': 'Ů','Uscr': '𝒰',
        'Utilde': 'Ũ','Uuml': 'Ü','VDash': '⊫','Vbar': '⫫','Vcy': 'В',
        'Vdash': '⊩','Vdashl': '⫦','Vee': '⋁','Verbar': '‖','Vert': '‖',
        'VerticalBar': '∣','VerticalLine': '|','VerticalSeparator': '❘','VerticalTilde': '≀','VeryThinSpace': ' ',
        'Vfr': '𝔙','Vopf': '𝕍','Vscr': '𝒱','Vvdash': '⊪','Wcirc': 'Ŵ',
        'Wedge': '⋀','Wfr': '𝔚','Wopf': '𝕎','Wscr': '𝒲','Xfr': '𝔛',
        'Xi': 'Ξ','Xopf': '𝕏','Xscr': '𝒳','YAcy': 'Я','YIcy': 'Ї',
        'YUcy': 'Ю','Yacute': 'Ý','Ycirc': 'Ŷ','Ycy': 'Ы','Yfr': '𝔜',
        'Yopf': '𝕐','Yscr': '𝒴','Yuml': 'Ÿ','ZHcy': 'Ж','Zacute': 'Ź',
        'Zcaron': 'Ž','Zcy': 'З','Zdot': 'Ż','ZeroWidthSpace': '','Zeta': 'Ζ',
        'Zfr': 'ℨ','Zopf': 'ℤ','Zscr': '𝒵','aacute': 'á','abreve': 'ă',
        'ac': '∾','acE': '∾̳','acd': '∿','acirc': 'â','acute': '´',
        'acy': 'а','aelig': 'æ','af': '','afr': '𝔞','agrave': 'à',
        'alefsym': 'ℵ','aleph': 'ℵ','alpha': 'α','amacr': 'ā','amalg': '⨿',
        'amp': '&','and': '∧','andand': '⩕','andd': '⩜','andslope': '⩘',
        'andv': '⩚','ang': '∠','ange': '⦤','angle': '∠','angmsd': '∡',
        'angmsdaa': '⦨','angmsdab': '⦩','angmsdac': '⦪','angmsdad': '⦫','angmsdae': '⦬',
        'angmsdaf': '⦭','angmsdag': '⦮','angmsdah': '⦯','angrt': '∟','angrtvb': '⊾',
        'angrtvbd': '⦝','angsph': '∢','angst': 'Å','angzarr': '⍼','aogon': 'ą',
        'aopf': '𝕒','ap': '≈','apE': '⩰','apacir': '⩯','ape': '≊',
        'apid': '≋','apos': "'",'approx': '≈','approxeq': '≊','aring': 'å',
        'ascr': '𝒶','ast': '*','asymp': '≈','asympeq': '≍','atilde': 'ã',
        'auml': 'ä','awconint': '∳','awint': '⨑','bNot': '⫭','backcong': '≌',
        'backepsilon': '϶','backprime': '‵','backsim': '∽','backsimeq': '⋍','barvee': '⊽',
        'barwed': '⌅','barwedge': '⌅','bbrk': '⎵','bbrktbrk': '⎶','bcong': '≌',
        'bcy': 'б','bdquo': '„','becaus': '∵','because': '∵','bemptyv': '⦰',
        'bepsi': '϶','bernou': 'ℬ','beta': 'β','beth': 'ℶ','between': '≬',
        'bfr': '𝔟','bigcap': '⋂','bigcirc': '◯','bigcup': '⋃','bigodot': '⨀',
        'bigoplus': '⨁','bigotimes': '⨂','bigsqcup': '⨆','bigstar': '★','bigtriangledown': '▽',
        'bigtriangleup': '△','biguplus': '⨄','bigvee': '⋁','bigwedge': '⋀','bkarow': '⤍',
        'blacklozenge': '⧫','blacksquare': '▪','blacktriangle': '▴','blacktriangledown': '▾','blacktriangleleft': '◂',
        'blacktriangleright': '▸','blank': '␣','blk12': '▒','blk14': '░','blk34': '▓',
        'block': '█','bne': '=⃥','bnequiv': '≡⃥','bnot': '⌐','bopf': '𝕓',
        'bot': '⊥','bottom': '⊥','bowtie': '⋈','boxDL': '╗','boxDR': '╔',
        'boxDl': '╖','boxDr': '╓','boxH': '═','boxHD': '╦','boxHU': '╩',
        'boxHd': '╤','boxHu': '╧','boxUL': '╝','boxUR': '╚','boxUl': '╜',
        'boxUr': '╙','boxV': '║','boxVH': '╬','boxVL': '╣','boxVR': '╠',
        'boxVh': '╫','boxVl': '╢','boxVr': '╟','boxbox': '⧉','boxdL': '╕',
        'boxdR': '╒','boxdl': '┐','boxdr': '┌','boxh': '─','boxhD': '╥',
        'boxhU': '╨','boxhd': '┬','boxhu': '┴','boxminus': '⊟','boxplus': '⊞',
        'boxtimes': '⊠','boxuL': '╛','boxuR': '╘','boxul': '┘','boxur': '└',
        'boxv': '│','boxvH': '╪','boxvL': '╡','boxvR': '╞','boxvh': '┼',
        'boxvl': '┤','boxvr': '├','bprime': '‵','breve': '˘','brvbar': '¦',
        'bscr': '𝒷','bsemi': '⁏','bsim': '∽','bsime': '⋍','bsol': '\\',
        'bsolb': '⧅','bsolhsub': '⟈','bull': '•','bullet': '•','bump': '≎',
        'bumpE': '⪮','bumpe': '≏','bumpeq': '≏','cacute': 'ć','cap': '∩',
        'capand': '⩄','capbrcup': '⩉','capcap': '⩋','capcup': '⩇','capdot': '⩀',
        'caps': '∩︀','caret': '⁁','caron': 'ˇ','ccaps': '⩍','ccaron': 'č',
        'ccedil': 'ç','ccirc': 'ĉ','ccups': '⩌','ccupssm': '⩐','cdot': 'ċ',
        'cedil': '¸','cemptyv': '⦲','cent': '¢','centerdot': '·','cfr': '𝔠',
        'chcy': 'ч','check': '✓','checkmark': '✓','chi': 'χ','cir': '○',
        'cirE': '⧃','circ': 'ˆ','circeq': '≗','circlearrowleft': '↺','circlearrowright': '↻',
        'circledR': '®','circledS': 'Ⓢ','circledast': '⊛','circledcirc': '⊚','circleddash': '⊝',
        'cire': '≗','cirfnint': '⨐','cirmid': '⫯','cirscir': '⧂','clubs': '♣',
        'clubsuit': '♣','colon': ':','colone': '≔','coloneq': '≔','comma': ',',
        'commat': '@','comp': '∁','compfn': '∘','complement': '∁','complexes': 'ℂ',
        'cong': '≅','congdot': '⩭','conint': '∮','copf': '𝕔','coprod': '∐',
        'copy': '©','copysr': '℗','crarr': '↵','cross': '✗','cscr': '𝒸',
        'csub': '⫏','csube': '⫑','csup': '⫐','csupe': '⫒','ctdot': '⋯',
        'cudarrl': '⤸','cudarrr': '⤵','cuepr': '⋞','cuesc': '⋟','cularr': '↶',
        'cularrp': '⤽','cup': '∪','cupbrcap': '⩈','cupcap': '⩆','cupcup': '⩊',
        'cupdot': '⊍','cupor': '⩅','cups': '∪︀','curarr': '↷','curarrm': '⤼',
        'curlyeqprec': '⋞','curlyeqsucc': '⋟','curlyvee': '⋎','curlywedge': '⋏','curren': '¤',
        'curvearrowleft': '↶','curvearrowright': '↷','cuvee': '⋎','cuwed': '⋏','cwconint': '∲',
        'cwint': '∱','cylcty': '⌭','dArr': '⇓','dHar': '⥥','dagger': '†',
        'daleth': 'ℸ','darr': '↓','dash': '‐','dashv': '⊣','dbkarow': '⤏',
        'dblac': '˝','dcaron': 'ď','dcy': 'д','dd': 'ⅆ','ddagger': '‡',
        'ddarr': '⇊','ddotseq': '⩷','deg': '°','delta': 'δ','demptyv': '⦱',
        'dfisht': '⥿','dfr': '𝔡','dharl': '⇃','dharr': '⇂','diam': '⋄',
        'diamond': '⋄','diamondsuit': '♦','diams': '♦','die': '¨','digamma': 'ϝ',
        'disin': '⋲','div': '÷','divide': '÷','divideontimes': '⋇','divonx': '⋇',
        'djcy': 'ђ','dlcorn': '⌞','dlcrop': '⌍','dollar': '$','dopf': '𝕕',
        'dot': '˙','doteq': '≐','doteqdot': '≑','dotminus': '∸','dotplus': '∔',
        'dotsquare': '⊡','doublebarwedge': '⌆','downarrow': '↓','downdownarrows': '⇊','downharpoonleft': '⇃',
        'downharpoonright': '⇂','drbkarow': '⤐','drcorn': '⌟','drcrop': '⌌','dscr': '𝒹',
        'dscy': 'ѕ','dsol': '⧶','dstrok': 'đ','dtdot': '⋱','dtri': '▿',
        'dtrif': '▾','duarr': '⇵','duhar': '⥯','dwangle': '⦦','dzcy': 'џ',
        'dzigrarr': '⟿','eDDot': '⩷','eDot': '≑','eacute': 'é','easter': '⩮',
        'ecaron': 'ě','ecir': '≖','ecirc': 'ê','ecolon': '≕','ecy': 'э',
        'edot': 'ė','ee': 'ⅇ','efDot': '≒','efr': '𝔢','eg': '⪚',
        'egrave': 'è','egs': '⪖','egsdot': '⪘','el': '⪙','elinters': '⏧',
        'ell': 'ℓ','els': '⪕','elsdot': '⪗','emacr': 'ē','empty': '∅',
        'emptyset': '∅','emptyv': '∅','emsp13': ' ','emsp14': ' ','emsp': ' ',
        'eng': 'ŋ','ensp': ' ','eogon': 'ę','eopf': '𝕖','epar': '⋕',
        'eparsl': '⧣','eplus': '⩱','epsi': 'ε','epsilon': 'ε','epsiv': 'ϵ',
        'eqcirc': '≖','eqcolon': '≕','eqsim': '≂','eqslantgtr': '⪖','eqslantless': '⪕',
        'equals': '=','equest': '≟','equiv': '≡','equivDD': '⩸','eqvparsl': '⧥',
        'erDot': '≓','erarr': '⥱','escr': 'ℯ','esdot': '≐','esim': '≂',
        'eta': 'η','eth': 'ð','euml': 'ë','euro': '€','excl': '!',
        'exist': '∃','expectation': 'ℰ','exponentiale': 'ⅇ','fallingdotseq': '≒','fcy': 'ф',
        'female': '♀','ffilig': 'ffi','fflig': 'ff','ffllig': 'ffl','ffr': '𝔣',
        'filig': 'fi','fjlig': 'fj','flat': '♭','fllig': 'fl','fltns': '▱',
        'fnof': 'ƒ','fopf': '𝕗','forall': '∀','fork': '⋔','forkv': '⫙',
        'fpartint': '⨍','frac12': '½','frac13': '⅓','frac14': '¼','frac15': '⅕',
        'frac16': '⅙','frac18': '⅛','frac23': '⅔','frac25': '⅖','frac34': '¾',
        'frac35': '⅗','frac38': '⅜','frac45': '⅘','frac56': '⅚','frac58': '⅝',
        'frac78': '⅞','frasl': '⁄','frown': '⌢','fscr': '𝒻','gE': '≧',
        'gEl': '⪌','gacute': 'ǵ','gamma': 'γ','gammad': 'ϝ','gap': '⪆',
        'gbreve': 'ğ','gcirc': 'ĝ','gcy': 'г','gdot': 'ġ','ge': '≥',
        'gel': '⋛','geq': '≥','geqq': '≧','geqslant': '⩾','ges': '⩾',
        'gescc': '⪩','gesdot': '⪀','gesdoto': '⪂','gesdotol': '⪄','gesl': '⋛︀',
        'gesles': '⪔','gfr': '𝔤','gg': '≫','ggg': '⋙','gimel': 'ℷ',
        'gjcy': 'ѓ','gl': '≷','glE': '⪒','gla': '⪥','glj': '⪤',
        'gnE': '≩','gnap': '⪊','gnapprox': '⪊','gne': '⪈','gneq': '⪈',
        'gneqq': '≩','gnsim': '⋧','gopf': '𝕘','grave': '`','gscr': 'ℊ',
        'gsim': '≳','gsime': '⪎','gsiml': '⪐','gt': '>','gtcc': '⪧',
        'gtcir': '⩺','gtdot': '⋗','gtlPar': '⦕','gtquest': '⩼','gtrapprox': '⪆',
        'gtrarr': '⥸','gtrdot': '⋗','gtreqless': '⋛','gtreqqless': '⪌','gtrless': '≷',
        'gtrsim': '≳','gvertneqq': '≩︀','gvnE': '≩︀','hArr': '⇔','hairsp': ' ',
        'half': '½','hamilt': 'ℋ','hardcy': 'ъ','harr': '↔','harrcir': '⥈',
        'harrw': '↭','hbar': 'ℏ','hcirc': 'ĥ','hearts': '♥','heartsuit': '♥',
        'hellip': '…','hercon': '⊹','hfr': '𝔥','hksearow': '⤥','hkswarow': '⤦',
        'hoarr': '⇿','homtht': '∻','hookleftarrow': '↩','hookrightarrow': '↪','hopf': '𝕙',
        'horbar': '―','hscr': '𝒽','hslash': 'ℏ','hstrok': 'ħ','hybull': '⁃',
        'hyphen': '‐','iacute': 'í','ic': '','icirc': 'î','icy': 'и',
        'iecy': 'е','iexcl': '¡','iff': '⇔','ifr': '𝔦','igrave': 'ì',
        'ii': 'ⅈ','iiiint': '⨌','iiint': '∭','iinfin': '⧜','iiota': '℩',
        'ijlig': 'ij','imacr': 'ī','image': 'ℑ','imagline': 'ℐ','imagpart': 'ℑ',
        'imath': 'ı','imof': '⊷','imped': 'Ƶ','in': '∈','incare': '℅',
        'infin': '∞','infintie': '⧝','inodot': 'ı','int': '∫','intcal': '⊺',
        'integers': 'ℤ','intercal': '⊺','intlarhk': '⨗','intprod': '⨼','iocy': 'ё',
        'iogon': 'į','iopf': '𝕚','iota': 'ι','iprod': '⨼','iquest': '¿',
        'iscr': '𝒾','isin': '∈','isinE': '⋹','isindot': '⋵','isins': '⋴',
        'isinsv': '⋳','isinv': '∈','it': '','itilde': 'ĩ','iukcy': 'і',
        'iuml': 'ï','jcirc': 'ĵ','jcy': 'й','jfr': '𝔧','jmath': 'ȷ',
        'jopf': '𝕛','jscr': '𝒿','jsercy': 'ј','jukcy': 'є','kappa': 'κ',
        'kappav': 'ϰ','kcedil': 'ķ','kcy': 'к','kfr': '𝔨','kgreen': 'ĸ',
        'khcy': 'х','kjcy': 'ќ','kopf': '𝕜','kscr': '𝓀','lAarr': '⇚',
        'lArr': '⇐','lAtail': '⤛','lBarr': '⤎','lE': '≦','lEg': '⪋',
        'lHar': '⥢','lacute': 'ĺ','laemptyv': '⦴','lagran': 'ℒ','lambda': 'λ',
        'lang': '⟨','langd': '⦑','langle': '⟨','lap': '⪅','laquo': '«',
        'larr': '←','larrb': '⇤','larrbfs': '⤟','larrfs': '⤝','larrhk': '↩',
        'larrlp': '↫','larrpl': '⤹','larrsim': '⥳','larrtl': '↢','lat': '⪫',
        'latail': '⤙','late': '⪭','lates': '⪭︀','lbarr': '⤌','lbbrk': '❲',
        'lbrace': '{','lbrack': '[','lbrke': '⦋','lbrksld': '⦏','lbrkslu': '⦍',
        'lcaron': 'ľ','lcedil': 'ļ','lceil': '⌈','lcub': '{','lcy': 'л',
        'ldca': '⤶','ldquo': '“','ldquor': '„','ldrdhar': '⥧','ldrushar': '⥋',
        'ldsh': '↲','le': '≤','leftarrow': '←','leftarrowtail': '↢','leftharpoondown': '↽',
        'leftharpoonup': '↼','leftleftarrows': '⇇','leftrightarrow': '↔','leftrightarrows': '⇆','leftrightharpoons': '⇋',
        'leftrightsquigarrow': '↭','leftthreetimes': '⋋','leg': '⋚','leq': '≤','leqq': '≦',
        'leqslant': '⩽','les': '⩽','lescc': '⪨','lesdot': '⩿','lesdoto': '⪁',
        'lesdotor': '⪃','lesg': '⋚︀','lesges': '⪓','lessapprox': '⪅','lessdot': '⋖',
        'lesseqgtr': '⋚','lesseqqgtr': '⪋','lessgtr': '≶','lesssim': '≲','lfisht': '⥼',
        'lfloor': '⌊','lfr': '𝔩','lg': '≶','lgE': '⪑','lhard': '↽',
        'lharu': '↼','lharul': '⥪','lhblk': '▄','ljcy': 'љ','ll': '≪',
        'llarr': '⇇','llcorner': '⌞','llhard': '⥫','lltri': '◺','lmidot': 'ŀ',
        'lmoust': '⎰','lmoustache': '⎰','lnE': '≨','lnap': '⪉','lnapprox': '⪉',
        'lne': '⪇','lneq': '⪇','lneqq': '≨','lnsim': '⋦','loang': '⟬',
        'loarr': '⇽','lobrk': '⟦','longleftarrow': '⟵','longleftrightarrow': '⟷','longmapsto': '⟼',
        'longrightarrow': '⟶','looparrowleft': '↫','looparrowright': '↬','lopar': '⦅','lopf': '𝕝',
        'loplus': '⨭','lotimes': '⨴','lowast': '∗','lowbar': '_','loz': '◊',
        'lozenge': '◊','lozf': '⧫','lpar': '(','lparlt': '⦓','lrarr': '⇆',
        'lrcorner': '⌟','lrhar': '⇋','lrhard': '⥭','lrm': '','lrtri': '⊿',
        'lsaquo': '‹','lscr': '𝓁','lsh': '↰','lsim': '≲','lsime': '⪍',
        'lsimg': '⪏','lsqb': '[','lsquo': '‘','lsquor': '‚','lstrok': 'ł',
        'lt': '<','ltcc': '⪦','ltcir': '⩹','ltdot': '⋖','lthree': '⋋',
        'ltimes': '⋉','ltlarr': '⥶','ltquest': '⩻','ltrPar': '⦖','ltri': '◃',
        'ltrie': '⊴','ltrif': '◂','lurdshar': '⥊','luruhar': '⥦','lvertneqq': '≨︀',
        'lvnE': '≨︀','mDDot': '∺','macr': '¯','male': '♂','malt': '✠',
        'maltese': '✠','map': '↦','mapsto': '↦','mapstodown': '↧','mapstoleft': '↤',
        'mapstoup': '↥','marker': '▮','mcomma': '⨩','mcy': 'м','mdash': '—',
        'measuredangle': '∡','mfr': '𝔪','mho': '℧','micro': 'µ','mid': '∣',
        'midast': '*','midcir': '⫰','middot': '·','minus': '−','minusb': '⊟',
        'minusd': '∸','minusdu': '⨪','mlcp': '⫛','mldr': '…','mnplus': '∓',
        'models': '⊧','mopf': '𝕞','mp': '∓','mscr': '𝓂','mstpos': '∾',
        'mu': 'μ','multimap': '⊸','mumap': '⊸','nGg': '⋙̸','nGt': '≫⃒',
        'nGtv': '≫̸','nLeftarrow': '⇍','nLeftrightarrow': '⇎','nLl': '⋘̸','nLt': '≪⃒',
        'nLtv': '≪̸','nRightarrow': '⇏','nVDash': '⊯','nVdash': '⊮','nabla': '∇',
        'nacute': 'ń','nang': '∠⃒','nap': '≉','napE': '⩰̸','napid': '≋̸',
        'napos': 'ʼn','napprox': '≉','natur': '♮','natural': '♮','naturals': 'ℕ',
        'nbsp': ' ','nbump': '≎̸','nbumpe': '≏̸','ncap': '⩃','ncaron': 'ň',
        'ncedil': 'ņ','ncong': '≇','ncongdot': '⩭̸','ncup': '⩂','ncy': 'н',
        'ndash': '–','ne': '≠','neArr': '⇗','nearhk': '⤤','nearr': '↗',
        'nearrow': '↗','nedot': '≐̸','nequiv': '≢','nesear': '⤨','nesim': '≂̸',
        'nexist': '∄','nexists': '∄','nfr': '𝔫','ngE': '≧̸','nge': '≱',
        'ngeq': '≱','ngeqq': '≧̸','ngeqslant': '⩾̸','nges': '⩾̸','ngsim': '≵',
        'ngt': '≯','ngtr': '≯','nhArr': '⇎','nharr': '↮','nhpar': '⫲',
        'ni': '∋','nis': '⋼','nisd': '⋺','niv': '∋','njcy': 'њ',
        'nlArr': '⇍','nlE': '≦̸','nlarr': '↚','nldr': '‥','nle': '≰',
        'nleftarrow': '↚','nleftrightarrow': '↮','nleq': '≰','nleqq': '≦̸','nleqslant': '⩽̸',
        'nles': '⩽̸','nless': '≮','nlsim': '≴','nlt': '≮','nltri': '⋪',
        'nltrie': '⋬','nmid': '∤','nopf': '𝕟','not': '¬','notin': '∉',
        'notinE': '⋹̸','notindot': '⋵̸','notinva': '∉','notinvb': '⋷','notinvc': '⋶',
        'notni': '∌','notniva': '∌','notnivb': '⋾','notnivc': '⋽','npar': '∦',
        'nparallel': '∦','nparsl': '⫽⃥','npart': '∂̸','npolint': '⨔','npr': '⊀',
        'nprcue': '⋠','npre': '⪯̸','nprec': '⊀','npreceq': '⪯̸','nrArr': '⇏',
        'nrarr': '↛','nrarrc': '⤳̸','nrarrw': '↝̸','nrightarrow': '↛','nrtri': '⋫',
        'nrtrie': '⋭','nsc': '⊁','nsccue': '⋡','nsce': '⪰̸','nscr': '𝓃',
        'nshortmid': '∤','nshortparallel': '∦','nsim': '≁','nsime': '≄','nsimeq': '≄',
        'nsmid': '∤','nspar': '∦','nsqsube': '⋢','nsqsupe': '⋣','nsub': '⊄',
        'nsubE': '⫅̸','nsube': '⊈','nsubset': '⊂⃒','nsubseteq': '⊈','nsubseteqq': '⫅̸',
        'nsucc': '⊁','nsucceq': '⪰̸','nsup': '⊅','nsupE': '⫆̸','nsupe': '⊉',
        'nsupset': '⊃⃒','nsupseteq': '⊉','nsupseteqq': '⫆̸','ntgl': '≹','ntilde': 'ñ',
        'ntlg': '≸','ntriangleleft': '⋪','ntrianglelefteq': '⋬','ntriangleright': '⋫','ntrianglerighteq': '⋭',
        'nu': 'ν','num': '#','numero': '№','numsp': ' ','nvDash': '⊭',
        'nvHarr': '⤄','nvap': '≍⃒','nvdash': '⊬','nvge': '≥⃒','nvgt': '>⃒',
        'nvinfin': '⧞','nvlArr': '⤂','nvle': '≤⃒','nvlt': '<⃒','nvltrie': '⊴⃒',
        'nvrArr': '⤃','nvrtrie': '⊵⃒','nvsim': '∼⃒','nwArr': '⇖','nwarhk': '⤣',
        'nwarr': '↖','nwarrow': '↖','nwnear': '⤧','oS': 'Ⓢ','oacute': 'ó',
        'oast': '⊛','ocir': '⊚','ocirc': 'ô','ocy': 'о','odash': '⊝',
        'odblac': 'ő','odiv': '⨸','odot': '⊙','odsold': '⦼','oelig': 'œ',
        'ofcir': '⦿','ofr': '𝔬','ogon': '˛','ograve': 'ò','ogt': '⧁',
        'ohbar': '⦵','ohm': 'Ω','oint': '∮','olarr': '↺','olcir': '⦾',
        'olcross': '⦻','oline': '‾','olt': '⧀','omacr': 'ō','omega': 'ω',
        'omicron': 'ο','omid': '⦶','ominus': '⊖','oopf': '𝕠','opar': '⦷',
        'operp': '⦹','oplus': '⊕','or': '∨','orarr': '↻','ord': '⩝',
        'order': 'ℴ','orderof': 'ℴ','ordf': 'ª','ordm': 'º','origof': '⊶',
        'oror': '⩖','orslope': '⩗','orv': '⩛','oscr': 'ℴ','oslash': 'ø',
        'osol': '⊘','otilde': 'õ','otimes': '⊗','otimesas': '⨶','ouml': 'ö',
        'ovbar': '⌽','par': '∥','para': '¶','parallel': '∥','parsim': '⫳',
        'parsl': '⫽','part': '∂','pcy': 'п','percnt': '%','period': '.',
        'permil': '‰','perp': '⊥','pertenk': '‱','pfr': '𝔭','phi': 'φ',
        'phiv': 'ϕ','phmmat': 'ℳ','phone': '☎','pi': 'π','pitchfork': '⋔',
        'piv': 'ϖ','planck': 'ℏ','planckh': 'ℎ','plankv': 'ℏ','plus': '+',
        'plusacir': '⨣','plusb': '⊞','pluscir': '⨢','plusdo': '∔','plusdu': '⨥',
        'pluse': '⩲','plusmn': '±','plussim': '⨦','plustwo': '⨧','pm': '±',
        'pointint': '⨕','popf': '𝕡','pound': '£','pr': '≺','prE': '⪳',
        'prap': '⪷','prcue': '≼','pre': '⪯','prec': '≺','precapprox': '⪷',
        'preccurlyeq': '≼','preceq': '⪯','precnapprox': '⪹','precneqq': '⪵','precnsim': '⋨',
        'precsim': '≾','prime': '′','primes': 'ℙ','prnE': '⪵','prnap': '⪹',
        'prnsim': '⋨','prod': '∏','profalar': '⌮','profline': '⌒','profsurf': '⌓',
        'prop': '∝','propto': '∝','prsim': '≾','prurel': '⊰','pscr': '𝓅',
        'psi': 'ψ','puncsp': ' ','qfr': '𝔮','qint': '⨌','qopf': '𝕢',
        'qprime': '⁗','qscr': '𝓆','quaternions': 'ℍ','quatint': '⨖','quest': '?',
        'questeq': '≟','quot': '"','rAarr': '⇛','rArr': '⇒','rAtail': '⤜',
        'rBarr': '⤏','rHar': '⥤','race': '∽̱','racute': 'ŕ','radic': '√',
        'raemptyv': '⦳','rang': '⟩','rangd': '⦒','range': '⦥','rangle': '⟩',
        'raquo': '»','rarr': '→','rarrap': '⥵','rarrb': '⇥','rarrbfs': '⤠',
        'rarrc': '⤳','rarrfs': '⤞','rarrhk': '↪','rarrlp': '↬','rarrpl': '⥅',
        'rarrsim': '⥴','rarrtl': '↣','rarrw': '↝','ratail': '⤚','ratio': '∶',
        'rationals': 'ℚ','rbarr': '⤍','rbbrk': '❳','rbrace': '}','rbrack': ']',
        'rbrke': '⦌','rbrksld': '⦎','rbrkslu': '⦐','rcaron': 'ř','rcedil': 'ŗ',
        'rceil': '⌉','rcub': '}','rcy': 'р','rdca': '⤷','rdldhar': '⥩',
        'rdquo': '”','rdquor': '”','rdsh': '↳','real': 'ℜ','realine': 'ℛ',
        'realpart': 'ℜ','reals': 'ℝ','rect': '▭','reg': '®','rfisht': '⥽',
        'rfloor': '⌋','rfr': '𝔯','rhard': '⇁','rharu': '⇀','rharul': '⥬',
        'rho': 'ρ','rhov': 'ϱ','rightarrow': '→','rightarrowtail': '↣','rightharpoondown': '⇁',
        'rightharpoonup': '⇀','rightleftarrows': '⇄','rightleftharpoons': '⇌','rightrightarrows': '⇉','rightsquigarrow': '↝',
        'rightthreetimes': '⋌','ring': '˚','risingdotseq': '≓','rlarr': '⇄','rlhar': '⇌',
        'rlm': '','rmoust': '⎱','rmoustache': '⎱','rnmid': '⫮','roang': '⟭',
        'roarr': '⇾','robrk': '⟧','ropar': '⦆','ropf': '𝕣','roplus': '⨮',
        'rotimes': '⨵','rpar': ')','rpargt': '⦔','rppolint': '⨒','rrarr': '⇉',
        'rsaquo': '›','rscr': '𝓇','rsh': '↱','rsqb': ']','rsquo': '’',
        'rsquor': '’','rthree': '⋌','rtimes': '⋊','rtri': '▹','rtrie': '⊵',
        'rtrif': '▸','rtriltri': '⧎','ruluhar': '⥨','rx': '℞','sacute': 'ś',
        'sbquo': '‚','sc': '≻','scE': '⪴','scap': '⪸','scaron': 'š',
        'sccue': '≽','sce': '⪰','scedil': 'ş','scirc': 'ŝ','scnE': '⪶',
        'scnap': '⪺','scnsim': '⋩','scpolint': '⨓','scsim': '≿','scy': 'с',
        'sdot': '⋅','sdotb': '⊡','sdote': '⩦','seArr': '⇘','searhk': '⤥',
        'searr': '↘','searrow': '↘','sect': '§','semi': '','seswar': '⤩',
        'setminus': '∖','setmn': '∖','sext': '✶','sfr': '𝔰','sfrown': '⌢',
        'sharp': '♯','shchcy': 'щ','shcy': 'ш','shortmid': '∣','shortparallel': '∥',
        'shy': '','sigma': 'σ','sigmaf': 'ς','sigmav': 'ς','sim': '∼',
        'simdot': '⩪','sime': '≃','simeq': '≃','simg': '⪞','simgE': '⪠',
        'siml': '⪝','simlE': '⪟','simne': '≆','simplus': '⨤','simrarr': '⥲',
        'slarr': '←','smallsetminus': '∖','smashp': '⨳','smeparsl': '⧤','smid': '∣',
        'smile': '⌣','smt': '⪪','smte': '⪬','smtes': '⪬︀','softcy': 'ь',
        'sol': '/','solb': '⧄','solbar': '⌿','sopf': '𝕤','spades': '♠',
        'spadesuit': '♠','spar': '∥','sqcap': '⊓','sqcaps': '⊓︀','sqcup': '⊔',
        'sqcups': '⊔︀','sqsub': '⊏','sqsube': '⊑','sqsubset': '⊏','sqsubseteq': '⊑',
        'sqsup': '⊐','sqsupe': '⊒','sqsupset': '⊐','sqsupseteq': '⊒','squ': '□',
        'square': '□','squarf': '▪','squf': '▪','srarr': '→','sscr': '𝓈',
        'ssetmn': '∖','ssmile': '⌣','sstarf': '⋆','star': '☆','starf': '★',
        'straightepsilon': 'ϵ','straightphi': 'ϕ','strns': '¯','sub': '⊂','subE': '⫅',
        'subdot': '⪽','sube': '⊆','subedot': '⫃','submult': '⫁','subnE': '⫋',
        'subne': '⊊','subplus': '⪿','subrarr': '⥹','subset': '⊂','subseteq': '⊆',
        'subseteqq': '⫅','subsetneq': '⊊','subsetneqq': '⫋','subsim': '⫇','subsub': '⫕',
        'subsup': '⫓','succ': '≻','succapprox': '⪸','succcurlyeq': '≽','succeq': '⪰',
        'succnapprox': '⪺','succneqq': '⪶','succnsim': '⋩','succsim': '≿','sum': '∑',
        'sung': '♪','sup1': '¹','sup2': '²','sup3': '³','sup': '⊃',
        'supE': '⫆','supdot': '⪾','supdsub': '⫘','supe': '⊇','supedot': '⫄',
        'suphsol': '⟉','suphsub': '⫗','suplarr': '⥻','supmult': '⫂','supnE': '⫌',
        'supne': '⊋','supplus': '⫀','supset': '⊃','supseteq': '⊇','supseteqq': '⫆',
        'supsetneq': '⊋','supsetneqq': '⫌','supsim': '⫈','supsub': '⫔','supsup': '⫖',
        'swArr': '⇙','swarhk': '⤦','swarr': '↙','swarrow': '↙','swnwar': '⤪',
        'szlig': 'ß','target': '⌖','tau': 'τ','tbrk': '⎴','tcaron': 'ť',
        'tcedil': 'ţ','tcy': 'т','tdot': '⃛','telrec': '⌕','tfr': '𝔱',
        'there4': '∴','therefore': '∴','theta': 'θ','thetasym': 'ϑ','thetav': 'ϑ',
        'thickapprox': '≈','thicksim': '∼','thinsp': ' ','thkap': '≈','thksim': '∼',
        'thorn': 'þ','tilde': '˜','times': '×','timesb': '⊠','timesbar': '⨱',
        'timesd': '⨰','tint': '∭','toea': '⤨','top': '⊤','topbot': '⌶',
        'topcir': '⫱','topf': '𝕥','topfork': '⫚','tosa': '⤩','tprime': '‴',
        'trade': '™','triangle': '▵','triangledown': '▿','triangleleft': '◃','trianglelefteq': '⊴',
        'triangleq': '≜','triangleright': '▹','trianglerighteq': '⊵','tridot': '◬','trie': '≜',
        'triminus': '⨺','triplus': '⨹','trisb': '⧍','tritime': '⨻','trpezium': '⏢',
        'tscr': '𝓉','tscy': 'ц','tshcy': 'ћ','tstrok': 'ŧ','twixt': '≬',
        'twoheadleftarrow': '↞','twoheadrightarrow': '↠','uArr': '⇑','uHar': '⥣','uacute': 'ú',
        'uarr': '↑','ubrcy': 'ў','ubreve': 'ŭ','ucirc': 'û','ucy': 'у',
        'udarr': '⇅','udblac': 'ű','udhar': '⥮','ufisht': '⥾','ufr': '𝔲',
        'ugrave': 'ù','uharl': '↿','uharr': '↾','uhblk': '▀','ulcorn': '⌜',
        'ulcorner': '⌜','ulcrop': '⌏','ultri': '◸','umacr': 'ū','uml': '¨',
        'uogon': 'ų','uopf': '𝕦','uparrow': '↑','updownarrow': '↕','upharpoonleft': '↿',
        'upharpoonright': '↾','uplus': '⊎','upsi': 'υ','upsih': 'ϒ','upsilon': 'υ',
        'upuparrows': '⇈','urcorn': '⌝','urcorner': '⌝','urcrop': '⌎','uring': 'ů',
        'urtri': '◹','uscr': '𝓊','utdot': '⋰','utilde': 'ũ','utri': '▵',
        'utrif': '▴','uuarr': '⇈','uuml': 'ü','uwangle': '⦧','vArr': '⇕',
        'vBar': '⫨','vBarv': '⫩','vDash': '⊨','vangrt': '⦜','varepsilon': 'ϵ',
        'varkappa': 'ϰ','varnothing': '∅','varphi': 'ϕ','varpi': 'ϖ','varpropto': '∝',
        'varr': '↕','varrho': 'ϱ','varsigma': 'ς','varsubsetneq': '⊊︀','varsubsetneqq': '⫋︀',
        'varsupsetneq': '⊋︀','varsupsetneqq': '⫌︀','vartheta': 'ϑ','vartriangleleft': '⊲','vartriangleright': '⊳',
        'vcy': 'в','vdash': '⊢','vee': '∨','veebar': '⊻','veeeq': '≚',
        'vellip': '⋮','verbar': '|','vert': '|','vfr': '𝔳','vltri': '⊲',
        'vnsub': '⊂⃒','vnsup': '⊃⃒','vopf': '𝕧','vprop': '∝','vrtri': '⊳',
        'vscr': '𝓋','vsubnE': '⫋︀','vsubne': '⊊︀','vsupnE': '⫌︀','vsupne': '⊋︀',
        'vzigzag': '⦚','wcirc': 'ŵ','wedbar': '⩟','wedge': '∧','wedgeq': '≙',
        'weierp': '℘','wfr': '𝔴','wopf': '𝕨','wp': '℘','wr': '≀',
        'wreath': '≀','wscr': '𝓌','xcap': '⋂','xcirc': '◯','xcup': '⋃',
        'xdtri': '▽','xfr': '𝔵','xhArr': '⟺','xharr': '⟷','xi': 'ξ',
        'xlArr': '⟸','xlarr': '⟵','xmap': '⟼','xnis': '⋻','xodot': '⨀',
        'xopf': '𝕩','xoplus': '⨁','xotime': '⨂','xrArr': '⟹','xrarr': '⟶',
        'xscr': '𝓍','xsqcup': '⨆','xuplus': '⨄','xutri': '△','xvee': '⋁',
        'xwedge': '⋀','yacute': 'ý','yacy': 'я','ycirc': 'ŷ','ycy': 'ы',
        'yen': '¥','yfr': '𝔶','yicy': 'ї','yopf': '𝕪','yscr': '𝓎',
        'yucy': 'ю','yuml': 'ÿ','zacute': 'ź','zcaron': 'ž','zcy': 'з',
        'zdot': 'ż','zeetrf': 'ℨ','zeta': 'ζ','zfr': '𝔷','zhcy': 'ж',
};