NodeBB/NodeBB

View on GitHub
public/src/utils.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict';

(function (factory) {
    if (typeof module === 'object' && module.exports) {
        var winston = require('winston');

        module.exports = factory(require('xregexp'), winston);

        process.profile = function (operation, start) {
            console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start));
        };

        process.elapsedTimeSince = function (start) {
            var diff = process.hrtime(start);
            return (diff[0] * 1e3) + (diff[1] / 1e6);
        };
    } else {
        window.utils = factory(window.XRegExp, console);
    }
    // eslint-disable-next-line
}(function (XRegExp, console) {
    var freeze = Object.freeze || function (obj) { return obj; };

    // add default escape function for escaping HTML entities
    var escapeCharMap = freeze({
        '&': '&',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#x27;',
        '`': '&#x60;',
        '=': '&#x3D;',
    });
    function replaceChar(c) {
        return escapeCharMap[c];
    }
    var escapeChars = /[&<>"'`=]/g;

    var HTMLEntities = freeze({
        amp: '&',
        gt: '>',
        lt: '<',
        quot: '"',
        apos: "'",
        AElig: 198,
        Aacute: 193,
        Acirc: 194,
        Agrave: 192,
        Aring: 197,
        Atilde: 195,
        Auml: 196,
        Ccedil: 199,
        ETH: 208,
        Eacute: 201,
        Ecirc: 202,
        Egrave: 200,
        Euml: 203,
        Iacute: 205,
        Icirc: 206,
        Igrave: 204,
        Iuml: 207,
        Ntilde: 209,
        Oacute: 211,
        Ocirc: 212,
        Ograve: 210,
        Oslash: 216,
        Otilde: 213,
        Ouml: 214,
        THORN: 222,
        Uacute: 218,
        Ucirc: 219,
        Ugrave: 217,
        Uuml: 220,
        Yacute: 221,
        aacute: 225,
        acirc: 226,
        aelig: 230,
        agrave: 224,
        aring: 229,
        atilde: 227,
        auml: 228,
        ccedil: 231,
        eacute: 233,
        ecirc: 234,
        egrave: 232,
        eth: 240,
        euml: 235,
        iacute: 237,
        icirc: 238,
        igrave: 236,
        iuml: 239,
        ntilde: 241,
        oacute: 243,
        ocirc: 244,
        ograve: 242,
        oslash: 248,
        otilde: 245,
        ouml: 246,
        szlig: 223,
        thorn: 254,
        uacute: 250,
        ucirc: 251,
        ugrave: 249,
        uuml: 252,
        yacute: 253,
        yuml: 255,
        copy: 169,
        reg: 174,
        nbsp: 160,
        iexcl: 161,
        cent: 162,
        pound: 163,
        curren: 164,
        yen: 165,
        brvbar: 166,
        sect: 167,
        uml: 168,
        ordf: 170,
        laquo: 171,
        not: 172,
        shy: 173,
        macr: 175,
        deg: 176,
        plusmn: 177,
        sup1: 185,
        sup2: 178,
        sup3: 179,
        acute: 180,
        micro: 181,
        para: 182,
        middot: 183,
        cedil: 184,
        ordm: 186,
        raquo: 187,
        frac14: 188,
        frac12: 189,
        frac34: 190,
        iquest: 191,
        times: 215,
        divide: 247,
        'OElig;': 338,
        'oelig;': 339,
        'Scaron;': 352,
        'scaron;': 353,
        'Yuml;': 376,
        'fnof;': 402,
        'circ;': 710,
        'tilde;': 732,
        'Alpha;': 913,
        'Beta;': 914,
        'Gamma;': 915,
        'Delta;': 916,
        'Epsilon;': 917,
        'Zeta;': 918,
        'Eta;': 919,
        'Theta;': 920,
        'Iota;': 921,
        'Kappa;': 922,
        'Lambda;': 923,
        'Mu;': 924,
        'Nu;': 925,
        'Xi;': 926,
        'Omicron;': 927,
        'Pi;': 928,
        'Rho;': 929,
        'Sigma;': 931,
        'Tau;': 932,
        'Upsilon;': 933,
        'Phi;': 934,
        'Chi;': 935,
        'Psi;': 936,
        'Omega;': 937,
        'alpha;': 945,
        'beta;': 946,
        'gamma;': 947,
        'delta;': 948,
        'epsilon;': 949,
        'zeta;': 950,
        'eta;': 951,
        'theta;': 952,
        'iota;': 953,
        'kappa;': 954,
        'lambda;': 955,
        'mu;': 956,
        'nu;': 957,
        'xi;': 958,
        'omicron;': 959,
        'pi;': 960,
        'rho;': 961,
        'sigmaf;': 962,
        'sigma;': 963,
        'tau;': 964,
        'upsilon;': 965,
        'phi;': 966,
        'chi;': 967,
        'psi;': 968,
        'omega;': 969,
        'thetasym;': 977,
        'upsih;': 978,
        'piv;': 982,
        'ensp;': 8194,
        'emsp;': 8195,
        'thinsp;': 8201,
        'zwnj;': 8204,
        'zwj;': 8205,
        'lrm;': 8206,
        'rlm;': 8207,
        'ndash;': 8211,
        'mdash;': 8212,
        'lsquo;': 8216,
        'rsquo;': 8217,
        'sbquo;': 8218,
        'ldquo;': 8220,
        'rdquo;': 8221,
        'bdquo;': 8222,
        'dagger;': 8224,
        'Dagger;': 8225,
        'bull;': 8226,
        'hellip;': 8230,
        'permil;': 8240,
        'prime;': 8242,
        'Prime;': 8243,
        'lsaquo;': 8249,
        'rsaquo;': 8250,
        'oline;': 8254,
        'frasl;': 8260,
        'euro;': 8364,
        'image;': 8465,
        'weierp;': 8472,
        'real;': 8476,
        'trade;': 8482,
        'alefsym;': 8501,
        'larr;': 8592,
        'uarr;': 8593,
        'rarr;': 8594,
        'darr;': 8595,
        'harr;': 8596,
        'crarr;': 8629,
        'lArr;': 8656,
        'uArr;': 8657,
        'rArr;': 8658,
        'dArr;': 8659,
        'hArr;': 8660,
        'forall;': 8704,
        'part;': 8706,
        'exist;': 8707,
        'empty;': 8709,
        'nabla;': 8711,
        'isin;': 8712,
        'notin;': 8713,
        'ni;': 8715,
        'prod;': 8719,
        'sum;': 8721,
        'minus;': 8722,
        'lowast;': 8727,
        'radic;': 8730,
        'prop;': 8733,
        'infin;': 8734,
        'ang;': 8736,
        'and;': 8743,
        'or;': 8744,
        'cap;': 8745,
        'cup;': 8746,
        'int;': 8747,
        'there4;': 8756,
        'sim;': 8764,
        'cong;': 8773,
        'asymp;': 8776,
        'ne;': 8800,
        'equiv;': 8801,
        'le;': 8804,
        'ge;': 8805,
        'sub;': 8834,
        'sup;': 8835,
        'nsub;': 8836,
        'sube;': 8838,
        'supe;': 8839,
        'oplus;': 8853,
        'otimes;': 8855,
        'perp;': 8869,
        'sdot;': 8901,
        'lceil;': 8968,
        'rceil;': 8969,
        'lfloor;': 8970,
        'rfloor;': 8971,
        'lang;': 9001,
        'rang;': 9002,
        'loz;': 9674,
        'spades;': 9824,
        'clubs;': 9827,
        'hearts;': 9829,
        'diams;': 9830,
    });

    var utils = {
        generateUUID: function () {
            /* eslint-disable no-bitwise */
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                var r = Math.random() * 16 | 0;
                var v = c === 'x' ? r : ((r & 0x3) | 0x8);
                return v.toString(16);
            });
            /* eslint-enable no-bitwise */
        },
        // https://github.com/substack/node-ent/blob/master/index.js
        decodeHTMLEntities: function (html) {
            return String(html)
                .replace(/&#(\d+);?/g, function (_, code) {
                    return String.fromCharCode(code);
                })
                .replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
                    return String.fromCharCode(parseInt(hex, 16));
                })
                .replace(/&([^;\W]+;?)/g, function (m, e) {
                    var ee = e.replace(/;$/, '');
                    var target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);

                    if (typeof target === 'number') {
                        return String.fromCharCode(target);
                    } else if (typeof target === 'string') {
                        return target;
                    }

                    return m;
                });
        },
        // https://github.com/jprichardson/string.js/blob/master/lib/string.js
        stripHTMLTags: function (str, tags) {
            var pattern = (tags || ['']).join('|');
            return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
        },

        cleanUpTag: function (tag, maxLength) {
            if (typeof tag !== 'string' || !tag.length) {
                return '';
            }

            tag = tag.trim().toLowerCase();
            // see https://github.com/NodeBB/NodeBB/issues/4378
            tag = tag.replace(/\u202E/gi, '');
            tag = tag.replace(/[,/#!$%^*;:{}=_`<>'"~()?|]/g, '');
            tag = tag.substr(0, maxLength || 15).trim();
            var matches = tag.match(/^[.-]*(.+?)[.-]*$/);
            if (matches && matches.length > 1) {
                tag = matches[1];
            }
            return tag;
        },

        removePunctuation: function (str) {
            return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
        },

        isEmailValid: function (email) {
            return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
        },

        isUserNameValid: function (name) {
            return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
        },

        isPasswordValid: function (password) {
            return typeof password === 'string' && password.length;
        },

        isNumber: function (n) {
            // `isFinite('') === true` so isNan parseFloat check is necessary
            return !isNaN(parseFloat(n)) && isFinite(n);
        },

        languageKeyRegex: /\[\[[\w]+:.+\]\]/,
        hasLanguageKey: function (input) {
            return utils.languageKeyRegex.test(input);
        },
        userLangToTimeagoCode: function (userLang) {
            var mapping = {
                'en-GB': 'en',
                'en-US': 'en',
                'fa-IR': 'fa',
                'pt-BR': 'pt-br',
                nb: 'no',
            };
            return mapping[userLang] || userLang;
        },
        // shallow objects merge
        merge: function () {
            var result = {};
            var obj;
            var keys;
            for (var i = 0; i < arguments.length; i += 1) {
                obj = arguments[i] || {};
                keys = Object.keys(obj);
                for (var j = 0; j < keys.length; j += 1) {
                    result[keys[j]] = obj[keys[j]];
                }
            }
            return result;
        },

        fileExtension: function (path) {
            return ('' + path).split('.').pop();
        },

        extensionMimeTypeMap: {
            bmp: 'image/bmp',
            cmx: 'image/x-cmx',
            cod: 'image/cis-cod',
            gif: 'image/gif',
            ico: 'image/x-icon',
            ief: 'image/ief',
            jfif: 'image/pipeg',
            jpe: 'image/jpeg',
            jpeg: 'image/jpeg',
            jpg: 'image/jpeg',
            png: 'image/png',
            pbm: 'image/x-portable-bitmap',
            pgm: 'image/x-portable-graymap',
            pnm: 'image/x-portable-anymap',
            ppm: 'image/x-portable-pixmap',
            ras: 'image/x-cmu-raster',
            rgb: 'image/x-rgb',
            svg: 'image/svg+xml',
            tif: 'image/tiff',
            tiff: 'image/tiff',
            xbm: 'image/x-xbitmap',
            xpm: 'image/x-xpixmap',
            xwd: 'image/x-xwindowdump',
        },

        fileMimeType: function (path) {
            return utils.extensionToMimeType(utils.fileExtension(path));
        },

        extensionToMimeType: function (extension) {
            return utils.extensionMimeTypeMap[extension] || '*';
        },

        isPromise: function (object) {
            // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
            return object && typeof object.then === 'function';
        },

        promiseParallel: function (obj) {
            var keys = Object.keys(obj);
            return Promise.all(
                keys.map(function (k) { return obj[k]; })
            ).then(function (results) {
                var data = {};
                keys.forEach(function (k, i) {
                    data[k] = results[i];
                });
                return data;
            });
        },

        // https://github.com/sindresorhus/is-absolute-url
        isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
        isWinPathRE: /^[a-zA-Z]:\\/,
        isAbsoluteUrl: function (url) {
            if (utils.isWinPathRE.test(url)) {
                return false;
            }
            return utils.isAbsoluteUrlRE.test(url);
        },

        isRelativeUrl: function (url) {
            return !utils.isAbsoluteUrl(url);
        },

        makeNumbersHumanReadable: function (elements) {
            elements.each(function () {
                $(this)
                    .html(utils.makeNumberHumanReadable($(this).attr('title')))
                    .removeClass('hidden');
            });
        },

        makeNumberHumanReadable: function (num) {
            var n = parseInt(num, 10);
            if (!n) {
                return num;
            }
            if (n > 999999) {
                return (n / 1000000).toFixed(1) + 'm';
            } else if (n > 999) {
                return (n / 1000).toFixed(1) + 'k';
            }
            return n;
        },

        addCommasToNumbers: function (elements) {
            elements.each(function (index, element) {
                $(element)
                    .html(utils.addCommas($(element).html()))
                    .removeClass('hidden');
            });
        },

        // takes a string like 1000 and returns 1,000
        addCommas: function (text) {
            return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
        },

        toISOString: function (timestamp) {
            if (!timestamp || !Date.prototype.toISOString) {
                return '';
            }

            // Prevent too-high values to be passed to Date object
            timestamp = Math.min(timestamp, 8640000000000000);

            try {
                return new Date(parseInt(timestamp, 10)).toISOString();
            } catch (e) {
                return timestamp;
            }
        },

        tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
            'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
            'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
            'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
            'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
            'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
            'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
            'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'],

        stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
            'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
            'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
            'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
            'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
            'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
            'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
            'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'],

        escapeRegexChars: function (text) {
            return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
        },

        escapeHTML: function (str) {
            if (str == null) {
                return '';
            }
            if (!str) {
                return String(str);
            }

            return str.toString().replace(escapeChars, replaceChar);
        },

        isAndroidBrowser: function () {
            // http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
            var nua = navigator.userAgent;
            return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
        },

        isTouchDevice: function () {
            return 'ontouchstart' in document.documentElement;
        },

        findBootstrapEnvironment: function () {
            // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
            var envs = ['xs', 'sm', 'md', 'lg'];
            var $el = $('<div>');

            $el.appendTo($('body'));

            for (var i = envs.length - 1; i >= 0; i -= 1) {
                var env = envs[i];

                $el.addClass('hidden-' + env);
                if ($el.is(':hidden')) {
                    $el.remove();
                    return env;
                }
            }
        },

        isMobile: function () {
            var env = utils.findBootstrapEnvironment();
            return ['xs', 'sm'].some(function (targetEnv) {
                return targetEnv === env;
            });
        },

        getHoursArray: function () {
            var currentHour = new Date().getHours();
            var labels = [];

            for (var i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
                var hour = i < 0 ? 24 + i : i;
                labels.push(hour + ':00');
            }

            return labels.reverse();
        },

        getDaysArray: function (from, amount) {
            var currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
            var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
            var labels = [];
            var tmpDate;

            for (var x = (amount || 30) - 1; x >= 0; x -= 1) {
                tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
                labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
            }

            return labels;
        },

        /* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
        isElementInViewport: function (el) {
            // special bonus for those using jQuery
            if (typeof jQuery === 'function' && el instanceof jQuery) {
                el = el[0];
            }

            var rect = el.getBoundingClientRect();

            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
                rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
            );
        },

        // get all the url params in a single key/value hash
        params: function (options) {
            var hash = {};

            options = options || {};
            options.skipToType = options.skipToType || {};

            var searchStr = window.location.search;
            if (options.hasOwnProperty('url')) {
                if (options.url) {
                    var a = utils.urlToLocation(options.url);
                    searchStr = a ? a.search : '';
                } else {
                    searchStr = '';
                }
            }
            var params = searchStr.substring(1).split('&');

            params.forEach(function (param) {
                var val = param.split('=');
                var key = decodeURI(val[0]);
                var value = (
                    options.disableToType ||
                    options.skipToType[key] ? decodeURI(val[1]) : utils.toType(decodeURI(val[1]))
                );

                if (key) {
                    if (key.substr(-2, 2) === '[]') {
                        key = key.slice(0, -2);
                    }
                    if (!hash[key]) {
                        hash[key] = value;
                    } else {
                        if (!$.isArray(hash[key])) {
                            hash[key] = [hash[key]];
                        }
                        hash[key].push(value);
                    }
                }
            });
            return hash;
        },

        param: function (key) {
            return this.params()[key];
        },

        urlToLocation: function (url) {
            var a = document.createElement('a');
            a.href = url;
            return a;
        },

        // return boolean if string 'true' or string 'false', or if a parsable string which is a number
        // also supports JSON object and/or arrays parsing
        toType: function (str) {
            var type = typeof str;
            if (type !== 'string') {
                return str;
            }
            var nb = parseFloat(str);
            if (!isNaN(nb) && isFinite(str)) {
                return nb;
            }
            if (str === 'false') {
                return false;
            }
            if (str === 'true') {
                return true;
            }

            try {
                str = JSON.parse(str);
            } catch (e) {}

            return str;
        },

        // Safely get/set chained properties on an object
        // set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
        // get example: utils.props(A, 'a.b.c') // returns {d: 10}
        // get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
        // credits to github.com/gkindel
        props: function (obj, props, value) {
            if (obj === undefined) {
                obj = window;
            }
            if (props == null) {
                return undefined;
            }
            var i = props.indexOf('.');
            if (i === -1) {
                if (value !== undefined) {
                    obj[props] = value;
                }
                return obj[props];
            }
            var prop = props.slice(0, i);
            var newProps = props.slice(i + 1);

            if (props !== undefined && !(obj[prop] instanceof Object)) {
                obj[prop] = {};
            }

            return utils.props(obj[prop], newProps, value);
        },

        isInternalURI: function (targetLocation, referenceLocation, relative_path) {
            return targetLocation.host === '' ||    // Relative paths are always internal links
                (
                    targetLocation.host === referenceLocation.host &&
                    // Otherwise need to check if protocol and host match
                    targetLocation.protocol === referenceLocation.protocol &&
                    // Subfolder installs need this additional check
                    (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
                );
        },

        rtrim: function (str) {
            return str.replace(/\s+$/g, '');
        },

        debounce: function (func, wait, immediate) {
            // modified from https://davidwalsh.name/javascript-debounce-function
            var timeout;
            return function () {
                var context = this;
                var args = arguments;
                var later = function () {
                    timeout = null;
                    if (!immediate) {
                        func.apply(context, args);
                    }
                };
                var callNow = immediate && !timeout;
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
                if (callNow) {
                    func.apply(context, args);
                }
            };
        },
    };

    return utils;
}));