formatio.js 6.36 KB
"use strict";

var samsam = require("@sinonjs/samsam");
var functionName = require("@sinonjs/commons").functionName;
var typeOf = require("@sinonjs/commons").typeOf;

var formatio = {
    excludeConstructors: ["Object", /^.$/],
    quoteStrings: true,
    limitChildrenCount: 0
};

var specialObjects = [];
if (typeof global !== "undefined") {
    specialObjects.push({ object: global, value: "[object global]" });
}
if (typeof document !== "undefined") {
    specialObjects.push({
        object: document,
        value: "[object HTMLDocument]"
    });
}
if (typeof window !== "undefined") {
    specialObjects.push({ object: window, value: "[object Window]" });
}

function constructorName(f, object) {
    var name = functionName(object && object.constructor);
    var excludes = f.excludeConstructors ||
            formatio.excludeConstructors || [];

    var i, l;
    for (i = 0, l = excludes.length; i < l; ++i) {
        if (typeof excludes[i] === "string" && excludes[i] === name) {
            return "";
        } else if (excludes[i].test && excludes[i].test(name)) {
            return "";
        }
    }

    return name;
}

function isCircular(object, objects) {
    if (typeof object !== "object") { return false; }
    var i, l;
    for (i = 0, l = objects.length; i < l; ++i) {
        if (objects[i] === object) { return true; }
    }
    return false;
}

function ascii(f, object, processed, indent) {
    if (typeof object === "string") {
        if (object.length === 0) { return "(empty string)"; }
        var qs = f.quoteStrings;
        var quote = typeof qs !== "boolean" || qs;
        return processed || quote ? "\"" + object + "\"" : object;
    }

    if (typeof object === "symbol") {
        return object.toString();
    }

    if (typeof object === "function" && !(object instanceof RegExp)) {
        return ascii.func(object);
    }

    processed = processed || [];

    if (isCircular(object, processed)) { return "[Circular]"; }

    if (typeOf(object) === "array") {
        return ascii.array.call(f, object, processed);
    }

    if (!object) { return String((1 / object) === -Infinity ? "-0" : object); }
    if (samsam.isElement(object)) { return ascii.element(object); }

    if (typeof object.toString === "function" &&
            object.toString !== Object.prototype.toString) {
        return object.toString();
    }

    var i, l;
    for (i = 0, l = specialObjects.length; i < l; i++) {
        if (object === specialObjects[i].object) {
            return specialObjects[i].value;
        }
    }

    if (samsam.isSet(object)) {
        return ascii.set.call(f, object, processed);
    }

    return ascii.object.call(f, object, processed, indent);
}

ascii.func = function (func) {
    var funcName = functionName(func) || "";
    return "function " + funcName + "() {}";
};

function delimit(str, delimiters) {
    delimiters = delimiters || ["[", "]"];
    return delimiters[0] + str + delimiters[1];
}

ascii.array = function (array, processed, delimiters) {
    processed = processed || [];
    processed.push(array);
    var pieces = [];
    var i, l;
    l = (this.limitChildrenCount > 0) ?
        Math.min(this.limitChildrenCount, array.length) : array.length;

    for (i = 0; i < l; ++i) {
        pieces.push(ascii(this, array[i], processed));
    }

    if (l < array.length) {
        pieces.push("[... " + (array.length - l) + " more elements]");
    }

    return delimit(pieces.join(", "), delimiters);
};

ascii.set = function (set, processed) {
    return ascii.array.call(this, Array.from(set), processed, ["Set {", "}"]);
};

ascii.object = function (object, processed, indent) {
    processed = processed || [];
    processed.push(object);
    indent = indent || 0;
    var pieces = [];
    var symbols = typeof Object.getOwnPropertySymbols === "function"
        ? Object.getOwnPropertySymbols(object)
        : [];
    var properties = Object.keys(object).sort().concat(symbols);
    var length = 3;
    var prop, str, obj, i, k, l;
    l = (this.limitChildrenCount > 0) ?
        Math.min(this.limitChildrenCount, properties.length) : properties.length;

    for (i = 0; i < l; ++i) {
        prop = properties[i];
        obj = object[prop];

        if (isCircular(obj, processed)) {
            str = "[Circular]";
        } else {
            str = ascii(this, obj, processed, indent + 2);
        }

        str = (
            typeof prop === "string" && /\s/.test(prop) ?
                "\"" + prop + "\"" : prop.toString()
        ) + ": " + str;
        length += str.length;
        pieces.push(str);
    }

    var cons = constructorName(this, object);
    var prefix = cons ? "[" + cons + "] " : "";
    var is = "";
    for (i = 0, k = indent; i < k; ++i) { is += " "; }

    if (l < properties.length)
    {pieces.push("[... " + (properties.length - l) + " more elements]");}

    if (length + indent > 80) {
        return prefix + "{\n  " + is + pieces.join(",\n  " + is) + "\n" +
            is + "}";
    }
    return prefix + "{ " + pieces.join(", ") + " }";
};

ascii.element = function (element) {
    var tagName = element.tagName.toLowerCase();
    var attrs = element.attributes;
    var pairs = [];
    var attr, attrName, i, l, val;

    for (i = 0, l = attrs.length; i < l; ++i) {
        attr = attrs.item(i);
        attrName = attr.nodeName.toLowerCase().replace("html:", "");
        val = attr.nodeValue;
        if (attrName !== "contenteditable" || val !== "inherit") {
            if (val) { pairs.push(attrName + "=\"" + val + "\""); }
        }
    }

    var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
    // SVG elements have undefined innerHTML
    var content = element.innerHTML || "";

    if (content.length > 20) {
        content = content.substr(0, 20) + "[...]";
    }

    var res = formatted + pairs.join(" ") + ">" + content +
            "</" + tagName + ">";

    return res.replace(/ contentEditable="inherit"/, "");
};

function Formatio(options) {
    // eslint-disable-next-line guard-for-in
    for (var opt in options) {
        this[opt] = options[opt];
    }
}

Formatio.prototype = {
    functionName: functionName,

    configure: function (options) {
        return new Formatio(options);
    },

    constructorName: function (object) {
        return constructorName(this, object);
    },

    ascii: function (object, processed, indent) {
        return ascii(this, object, processed, indent);
    }
};

module.exports = Formatio.prototype;