diff --git a/README.md b/README.md index 7a92158..c4f6f76 100644 --- a/README.md +++ b/README.md @@ -43,18 +43,19 @@ customization ------------- These are the default styles and settings used by _eyes_. + styles: { // Styles applied to stdout all: 'cyan', // Overall style applied to everything label: 'underline', // Inspection labels, like 'array' in `array: [1, 2, 3]` other: 'inverted', // Objects which don't have a literal representation, such as functions key: 'bold', // The keys in object literals, like 'a' in `{a: 1}` - special: 'grey', // null, undefined... string: 'green', number: 'magenta', bool: 'blue', // true false regexp: 'green', // /\d+/ }, + pretty: true, // Indent object literals hideFunctions: false, // Don't output functions at all stream: process.stdout, // Stream to write to, or null diff --git a/lib/eyes.js b/lib/eyes.js index 23caafe..43bde8c 100644 --- a/lib/eyes.js +++ b/lib/eyes.js @@ -27,17 +27,30 @@ eyes.defaults = { regexp: 'green', // /\d+/ }, pretty: true, // Indent object literals + indent: 4, hideFunctions: false, - stream: process.stdout, - maxLength: 2048 // Truncate output if longer + showHidden: false, + sortKeys: false, + stream: process && process.stdout, + maxLength: 2048, // Truncate output if longer + colors: true, + html: false, + json: false, + escape: true, + functions: false }; // Return a curried inspect() function, with the `options` argument filled in. eyes.inspector = function (options) { var that = this; return function (obj, label, opts) { - return that.inspect.call(that, obj, label, - merge(options || {}, opts || {})); + var myopts=merge(options || {}, opts || {}); + var result=that.inspect.call(that, obj, label, myopts); + if (myopts.html && !myopts.stream) { + return '
'+result+'
'; + } else { + return result; + } }; }; @@ -45,11 +58,11 @@ eyes.inspector = function (options) { // if not, we just return the stringified object. eyes.inspect = function (obj, label, options) { options = merge(this.defaults, options || {}); - + stack = []; if (options.stream) { return this.print(stringify(obj, options), label, options); } else { - return stringify(obj, options) + (options.styles ? '\033[39m' : ''); + return stringify(obj, options) + (options.styles ? (options.html?'':(options.colors?'\033[39m':'')) : ''); } }; @@ -61,41 +74,63 @@ eyes.inspect = function (obj, label, options) { // versus a printable character (`c`). So we resort to counting the // length manually. eyes.print = function (str, label, options) { - for (var c = 0, i = 0; i < str.length; i++) { - if (str.charAt(i) === '\033') { i += 4 } // `4` because '\033[25m'.length + 1 == 5 - else if (c === options.maxLength) { - str = str.slice(0, i - 1) + '…'; - break; - } else { c++ } + if (!this.html) { + for (var c = 0, i = 0; i < str.length; i++) { + if (str.charAt(i) === '\033') { i += 4 } // `4` because '\033[25m'.length + 1 == 5 + else if (c === options.maxLength) { + str = str.slice(0, i - 1) + '…'; + break; + } else { c++ } + } } return options.stream.write.call(options.stream, (label ? - this.stylize(label, options.styles.label, options.styles) + ': ' : '') + - this.stylize(str, options.styles.all, options.styles) + '\033[0m' + "\n"); + this.stylize(label, options.styles.label, options) + ': ' : '') + + this.stylize(str, options.styles.all, options) + (this.html?'':(options.colors?'\033[0m':'')) + "\n"); }; // Apply a style to a string, eventually, // I'd like this to support passing multiple // styles. -eyes.stylize = function (str, style, styles) { - var codes = { - 'bold' : [1, 22], - 'underline' : [4, 24], - 'inverse' : [7, 27], - 'cyan' : [36, 39], - 'magenta' : [35, 39], - 'blue' : [34, 39], - 'yellow' : [33, 39], - 'green' : [32, 39], - 'red' : [31, 39], - 'grey' : [90, 39] - }, endCode; - - if (style && codes[style]) { - endCode = (codes[style][1] === 39 && styles.all) ? codes[styles.all][0] - : codes[style][1]; - return '\033[' + codes[style][0] + 'm' + str + - '\033[' + endCode + 'm'; - } else { return str } +eyes.stylize = function (str, style, options) { + if (options.colors) { + if (options.html) { + var codes = { + 'bold' : 'font-weight:bold', + 'underline' : 'text-decoration: underline', + }; + if (style) { + if (codes[style]) { + return ''+str+''; + } else { + return ''+str+''; + } + } else { + return str; + } + } else { + var codes = { + 'bold' : [1, 22], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'cyan' : [36, 39], + 'magenta' : [35, 39], + 'blue' : [34, 39], + 'yellow' : [33, 39], + 'green' : [32, 39], + 'red' : [31, 39], + 'grey' : [90, 39] + }, endCode; + + if (style && codes[style]) { + endCode = (codes[style][1] === 39 && options.styles.all) ? codes[options.styles.all][0] + : codes[style][1]; + return '\033[' + codes[style][0] + 'm' + str + + '\033[' + endCode + 'm'; + } else { return str } + } + } else { + return str; + } }; // Convert any object to a string, ready for output. @@ -104,7 +139,7 @@ eyes.stylize = function (str, style, styles) { // stringify(). function stringify(obj, options) { var that = this, stylize = function (str, style) { - return eyes.stylize(str, options.styles[style], options.styles) + return eyes.stylize(str, options.styles[style], options) }, index, result; if ((index = stack.indexOf(obj)) !== -1) { @@ -114,16 +149,16 @@ function stringify(obj, options) { result = (function (obj) { switch (typeOf(obj)) { - case "string" : obj = stringifyString(obj.indexOf("'") === -1 ? "'" + obj + "'" - : '"' + obj + '"'); + + case "string" : obj = stringifyString((obj.indexOf("'") === -1 && !options.json)? ("'" + obj.replace(/'/g,"\\'") + "'") : ('"' + obj.replace(/"/g,'\\"') + '"'),options); return stylize(obj, 'string'); case "regexp" : return stylize('/' + obj.source + '/', 'regexp'); case "number" : return stylize(obj + '', 'number'); - case "function" : return options.stream ? stylize("Function", 'other') : '[Function]'; + case "function" : return options.stream ? stylize(options.functions?obj.toString():"Function", 'other') : '[Function]'; case "null" : return stylize("null", 'special'); case "undefined": return stylize("undefined", 'special'); case "boolean" : return stylize(obj + '', 'bool'); - case "date" : return stylize(obj.toUTCString()); + case "date" : return stylize(obj.toUTCString(),'date'); case "array" : return stringifyArray(obj, options, stack.length); case "object" : return stringifyObject(obj, options, stack.length); } @@ -133,13 +168,73 @@ function stringify(obj, options) { return result; }; +function htmlspecialchars (string, quote_style, charset, double_encode) { + var optTemp = 0, + i = 0, + noquotes = false; + if (typeof quote_style === 'undefined' || quote_style === null) { + quote_style = 2; + } + string = string.toString(); + if (double_encode !== false) { + string = string.replace(/&/g, '&'); + } + string = string.replace(//g, '>'); +// console.log(string); + var OPTS = { + 'ENT_NOQUOTES': 0, + 'ENT_HTML_QUOTE_SINGLE': 1, + 'ENT_HTML_QUOTE_DOUBLE': 2, + 'ENT_COMPAT': 2, + 'ENT_QUOTES': 3, + 'ENT_IGNORE': 4 + }; + if (quote_style === 0) { + noquotes = true; + } + if (typeof quote_style !== 'number') { + quote_style = [].concat(quote_style); + for (var i = 0; i < quote_style.length; i++) { + if (OPTS[quote_style[i]] === 0) { + noquotes = true; + } + else if (OPTS[quote_style[i]]) { + optTemp = optTemp | OPTS[quote_style[i]]; + } + } + quote_style = optTemp; + } + if (quote_style & OPTS.ENT_HTML_QUOTE_SINGLE) { + string = string.replace(/'/g, '''); + } + if (!noquotes) { + string = string.replace(/"/g, '"'); + } + return string; +} + // Escape invisible characters in a string function stringifyString (str, options) { - return str.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\n') - .replace(/[\u0001-\u001F]/g, function (match) { - return '\\0' + match[0].charCodeAt(0).toString(8); - }); + var result; + if (options.escape) { + result=str.replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/[\u0001-\u001F]/g, function (match) { + return '\\0' + match[0].charCodeAt(0).toString(8); + }); + if (options.html) { + result=htmlspecialchars(result); + } + } else { + result=str; + } + + // Truncate the string if a maximum length is configured + var truncate = options.hasOwnProperty('maxStringLength') && options.maxStringLength >= 0; + if(truncate && result.length > options.maxStringLength) { + var length = Math.min(result.length, options.maxStringLength - 3); + result = result.substr(0, length) + "..."; + } + + return result; } // Convert an array to a string, such as [1, 2, 3]. @@ -148,47 +243,84 @@ function stringifyString (str, options) { function stringifyArray(ary, options, level) { var out = []; var pretty = options.pretty && (ary.length > 4 || ary.some(function (o) { - return (typeof(o) === 'object' && Object.keys(o).length > 0) || + return (o !== null && typeof(o) === 'object' && Object.keys(o).length > 0) || (Array.isArray(o) && o.length > 0); })); - var ws = pretty ? '\n' + new(Array)(level * 4 + 1).join(' ') : ' '; + var ws = pretty ? '\n' + new(Array)(level * options.indent + 1).join(' ') : ' '; - for (var i = 0; i < ary.length; i++) { + var truncate = options.hasOwnProperty('maxArrayLength') && options.maxArrayLength >= 0; + + var length = truncate ? Math.min(ary.length, options.maxArrayLength) : ary.length; + for (var i = 0; i < length; i++) { out.push(stringify(ary[i], options)); } + // Add a special String if the array was truncated + if(length < ary.length) { + out.push('<>'); + } + if (out.length === 0) { return '[]'; } else { return '[' + ws + out.join(',' + (pretty ? ws : ' ')) - + (pretty ? ws.slice(0, -4) : ws) + + + (pretty ? ws.slice(0, -1*options.indent) : ws) + ']'; } }; +var quote_reserved_words = function(word) { + var reserved_words=['abstract', 'else', 'instanceof', 'switch', 'boolean', 'enum', 'int', 'synchronized', 'break', 'export', 'interface', 'this', 'byte', 'extends', 'long', 'throw', 'case', 'false', 'native', 'throws', 'catch', 'final', 'new', 'transient', 'char', 'finally', 'null', 'true', 'class', 'float', 'package', 'try', 'const', 'for', 'private', 'typeof', 'continue', 'function', 'protected', 'var', 'debugger', 'goto', 'public', 'void', 'default', 'if', 'return', 'volatile', 'delete', 'implements', 'short', 'while', 'do', 'import', 'static', 'with', 'double', 'in', 'super']; + return function(word) { + for (var i in reserved_words) { + if (word === reserved_words[i]) { + return "'" + word + "'"; + } + } + return word; + } +}(); + + // Convert an object to a string, such as {a: 1}. // This function calls stringify() for each of its values, // and does not output functions or prototype values. function stringifyObject(obj, options, level) { + if ( obj instanceof Buffer) return 'Buffer'; var out = []; var pretty = options.pretty && (Object.keys(obj).length > 2 || Object.keys(obj).some(function (k) { return typeof(obj[k]) === 'object' })); - var ws = pretty ? '\n' + new(Array)(level * 4 + 1).join(' ') : ' '; + var ws = pretty ? '\n' + new(Array)(level * options.indent + 1).join(' ') : ' '; - Object.keys(obj).forEach(function (k) { - if (obj.hasOwnProperty(k) && !(obj[k] instanceof Function && options.hideFunctions)) { - out.push(eyes.stylize(k, options.styles.key, options.styles) + ': ' + - stringify(obj[k], options)); - } + var keys = options.showHidden ? Object.keys(obj) : Object.getOwnPropertyNames(obj); + if (options.sortKeys) keys.sort(); + + var truncate = options.hasOwnProperty('maxObjectKeys') && options.maxObjectKeys >= 0; + + // Slice the keys to the maximum length if they exceed the maxObjectKeys option + var includeKeys = (truncate) ? keys.slice(0, options.maxObjectKeys) : keys; + includeKeys.forEach(function (k) { + if (!(level== 1 && options.exclude && ~options.exclude.indexOf(k))) { + if (Object.prototype.hasOwnProperty.call(obj, k) + && !(obj[k] instanceof Function && options.hideFunctions)) { + out.push((options.json?'"':'')+eyes.stylize(options.json?k:quote_reserved_words(k), options.styles.key, options) + (options.json?'"':'') + ': ' + + stringify(obj[k], options)); + } + } }); + // Append a special String if the Object was truncated + if (includeKeys.length < keys.length) { + out.push(eyes.stylize('<>', options.styles.key, options.styles)); + } + if (out.length === 0) { return '{}'; } else { return "{" + ws + out.join(',' + (pretty ? ws : ' ')) - + (pretty ? ws.slice(0, -4) : ws) + + + (pretty ? ws.slice(0, -1*options.indent) : ws) + "}"; } }; @@ -230,4 +362,3 @@ function merge(/* variable args */) { }); return target; } - diff --git a/package.json b/package.json index b9903fb..de5484c 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,14 @@ "name" : "eyes", "description" : "a customizable value inspector", "url" : "http://github.com/cloudhead/eyes.js", + "repository" : { "type": "git", "url": "https://github.com/cloudhead/eyes.js.git" }, "keywords" : ["inspector", "debug", "inspect", "print"], "author" : "Alexis Sellier ", - "contributors" : [], + "contributors" : [{ "name": "Charlie Robbins", "email": "charlie@nodejitsu.com" }], "licenses" : ["MIT"], - "dependencies" : [], "main" : "./lib/eyes", - "version" : "0.1.6", + "version" : "0.1.8", + "scripts" : { "test": "node test/*-test.js" }, "directories" : { "lib": "./lib", "test": "./test" }, "engines" : { "node": "> 0.1.90" } } diff --git a/test/eyes-test.js b/test/eyes-test.js index d3a1d26..a0699f3 100644 --- a/test/eyes-test.js +++ b/test/eyes-test.js @@ -1,4 +1,4 @@ -var sys = require('sys'); +var util = require('util'); var eyes = require('../lib/eyes'); eyes.inspect({ @@ -46,10 +46,52 @@ eyes.inspect({ }] }, 'complex'); +eyes.inspect([null], "null in array"); var inspect = eyes.inspector({ stream: null }); -sys.puts(inspect('something', "something")); -sys.puts(inspect("something else")); +util.puts(inspect('something', "something")); +util.puts(inspect("something else")); -sys.puts(inspect(["no color"], null, { styles: false })); +util.puts(inspect(["no color"], null, { styles: false })); + +eyes.inspect('This String is truncated completely', 'String truncated completely', { maxStringLength: 0 }); +eyes.inspect('This String is way too long', 'String too long', { maxStringLength: 12 }); +eyes.inspect('This String is exactly right', 'String exactly short enough', { maxStringLength: 29 }); +eyes.inspect('This String is short enough', 'String is shorter', { maxStringLength: 30 }); + +eyes.inspect(['a', 'b', 'c'], 'Array short enough', { maxArrayLength: 4 }); +eyes.inspect(['a', 'b', 'c'], 'Array exactly short enough', { maxArrayLength: 3 }); +eyes.inspect(['a', 'b', 'c'], 'Array length too long', { maxArrayLength: 2 }); +eyes.inspect(['a', 'b', 'c'], 'Array length too long', { maxArrayLength: 1 }); +eyes.inspect(['a', 'b', 'c'], 'Array trunctated completely', { maxArrayLength: 0 }); + +eyes.inspect({ 'a': 'A', 'b': 'B', 'c': 'C' }, 'Object short enough', { maxObjectKeys: 4 }); +eyes.inspect({ 'a': 'A', 'b': 'B', 'c': 'C' }, 'Object exactly short enough', { maxObjectKeys: 3 }); +eyes.inspect({ 'a': 'A', 'b': 'B', 'c': 'C' }, 'Object has too many keys', { maxObjectKeys: 2 }); +eyes.inspect({ 'a': 'A', 'b': 'B', 'c': 'C' }, 'Object has too many keys', { maxObjectKeys: 1 }); +eyes.inspect({ 'a': 'A', 'b': 'B', 'c': 'C' }, 'Object truncated completely', { maxObjectKeys: 0 }); + +eyes.inspect(1234567890, 'Number too long', { maxStringLength: 6 }); + +eyes.inspect({ + name: "Something about ogres", + story: "Once upon a time, in a land far far away.", + tags: [ + "ogres", + "donkey", + "fairytail", + "prince", + "evil" + ], + related: [ + "A story about an angry prince and his quest to rule the land" + ], + link: "http://farfaraway.ff" +}, +'Combination truncated', +{ + maxObjectKeys: 4, + maxArrayLength: 2, + maxStringLength: 39 +});