diff --git a/objtree.js b/objtree.js new file mode 100644 index 0000000..175a424 --- /dev/null +++ b/objtree.js @@ -0,0 +1,194 @@ +const OBJTREE_OBJECT = 0; +const OBJTREE_UNKNOWN = 1; +const OBJTREE_COMPLEX_FUNCTION = 2; +const OBJTREE_SIMPLE_FUNCTION = 3; +const OBJTREE_PRIMARRAY = 4; +const OBJTREE_VARIABLE = 5; +const OBJTREE_TOODEEP = 6; + +const OBJTREE_NAMES = [ + "[obj]", "[???]", "[fun]", "[fun]", "[arr]", "[var]", "[!!!]" +]; + +var objtree = function(target, { + maxlevel = 10, + grandparent = "", + exclude = [] +} = {}){ + var excludeRules = exclude.map(rule => new RegExp(rule)); + + var getObjectDesc = function(obj){ + switch(typeof obj){ + case "object": + if (obj === null){ + return [ OBJTREE_VARIABLE, "(null)" ]; + } + else if (Array.isArray(obj)){ + if (obj.length === 0){ + return [ OBJTREE_PRIMARRAY, "[]" ]; + } + else if (obj.every(ele => { + let type = typeof ele; + return type === "boolean" || type === "number" || (type === "string" && ele.indexOf('\n') === -1); + })){ + return [ OBJTREE_PRIMARRAY, "[ "+obj.join(", ")+" ]" ]; + } + } + + return [ OBJTREE_OBJECT ]; // special handling + + case "function": + if (Object.keys(obj).length === 0 && (!obj.prototype || Object.keys(obj.prototype).length === 0)){ + return [ OBJTREE_SIMPLE_FUNCTION, obj.toString().match(/\((.*?)\)/)[0] ]; + } + else{ + return [ OBJTREE_COMPLEX_FUNCTION, obj.toString().match(/\((.*?)\)/)[0] ]; + } + + case "boolean": + case "number": + case "string": + return [ OBJTREE_VARIABLE, obj ]; + + case "undefined": + return [ OBJTREE_VARIABLE, "(undefined)" ]; + + default: + return [ OBJTREE_UNKNOWN, obj.toString() ]; + } + }; + + var generateTree = function(node, parents, level){ + let res = {}; + + for(let key in node){ + if (excludeRules.length){ + let fullKey = parents+key; + + if (excludeRules.some(rule => rule.test(fullKey))){ + res[key] = { + type: OBJTREE_UNKNOWN, + value: "(excluded)" + }; + + continue; + } + } + + let obj = node[key]; + let [ type, value ] = getObjectDesc(obj); + + if (type === OBJTREE_OBJECT){ + if (level > maxlevel){ + type = OBJTREE_TOODEEP; + value = "(too deep)"; + } + else{ + value = generateTree(obj, parents+key+"/", level+1); + } + } + else if (type === OBJTREE_COMPLEX_FUNCTION){ + let data = { __args: value }; + + for(let fkey in obj){ + data[fkey] = obj[fkey]; + } + + if (obj.prototype){ + data.prototype = {}; + + for(let pkey in obj.prototype){ + if (pkey !== "constructor"){ + data.prototype[pkey] = obj.prototype[pkey]; + } + } + } + + value = generateTree(data, parents+key+"/", level+1); + } + + res[key] = { type, value }; + } + + return res; + }; + + var tree = generateTree(target, "", 1); + + var obj = { + asObj: function(){ + return tree; + }, + + asText: function(){ + var lines = [ "OBJECT TREE", "===========" ]; + var grandpa = " "+(grandparent ? grandparent+"." : ""); + + var sorter = function(entry1, entry2){ + let v = entry1[1].type - entry2[1].type; + return v === 0 ? +(entry1[0] > entry2[0]) : v; + }; + + var keyRegex = /^[a-z_$][a-z0-9_$]+$/i; + + var getKeyAccess = function(key){ + return keyRegex.test(key) ? "."+key : "['"+key+"']"; + } + + var varTypes = [ + OBJTREE_VARIABLE, OBJTREE_PRIMARRAY, OBJTREE_UNKNOWN, OBJTREE_TOODEEP + ]; + + var addLines = function(node, parents, level){ + let entries = Object.entries(node); + let prefix = " ".repeat(level)+"|-- "; + + let longest = Math.max.apply(null, entries + .filter(entry => varTypes.includes(entry[1].type)) + .map(entry => (level === 0 ? entry[0] : getKeyAccess(entry[0])).length) + ); + + for(let [key, desc] of entries.sort(sorter)){ + let keyText = level === 0 ? key : getKeyAccess(key); + + if (desc.type === OBJTREE_OBJECT){ + lines.push(prefix+grandpa+parents+keyText); + addLines(desc.value, parents+keyText, level+1); + } + else{ + let commonPre = prefix+OBJTREE_NAMES[desc.type]+grandpa+parents+keyText; + + if (desc.type === OBJTREE_SIMPLE_FUNCTION){ + lines.push(commonPre+desc.value); + } + else if (desc.type === OBJTREE_COMPLEX_FUNCTION){ + lines.push(commonPre+desc.value.__args.value); + delete desc.value.__args; + addLines(desc.value, parents+keyText, level+1); + } + else{ + lines.push(commonPre+(" ".repeat(longest-keyText.length))+" > "+desc.value); + } + } + } + }; + + addLines(tree, "", 0); + return lines.join("\n"); + }, + + downloadText: function(filename){ + let url = window.URL.createObjectURL(new Blob([obj.asText()], { "type": "octet/stream" })); + let ele = document.createElement("a"); + document.body.appendChild(ele); + ele.href = url; + ele.download = filename; + ele.style.display = "none"; + ele.click(); + document.body.removeChild(ele); + window.URL.revokeObjectURL(url); + } + }; + + return obj; +};