Permalink
Browse files

Add diff-like, optionally colorized output

  • Loading branch information...
1 parent 64620ee commit c37d2709f7f1f7566b1a4b3d239c04211e828d9b @andreyvit committed Apr 17, 2012
Showing with 142 additions and 21 deletions.
  1. +15 −4 lib/cli.iced
  2. +78 −0 lib/colorize.iced
  3. +3 −10 lib/index.iced
  4. +9 −0 lib/util.iced
  5. +2 −1 package.json
  6. +29 −0 test/colorize_test.coffee
  7. +6 −6 test/diff_test.coffee
View
@@ -1,7 +1,8 @@
-fs = require 'fs'
+fs = require 'fs'
+tty = require 'tty'
{ diff } = require './index'
-
+{ colorizeWithAnsiEscapes} = require './colorize'
module.exports = (argv) ->
options = require('dreamopt') [
@@ -13,8 +14,12 @@ module.exports = (argv) ->
"General options:"
" -v, --verbose Output progress info"
+ " -C, --[no-]color Colored output"
+ " -j, --raw-json Display raw JSON encoding of the diff #var(raw)"
], argv
+ process.stderr.write "#{JSON.stringify(options, null, 2)}\n" if options.verbose
+
process.stderr.write "Loading files...\n" if options.verbose
await
fs.readFile options.file1, 'utf8', defer(err1, data1)
@@ -31,5 +36,11 @@ module.exports = (argv) ->
process.stderr.write "Running diff...\n" if options.verbose
result = diff(json1, json2)
- process.stderr.write "Producing output...\n" if options.verbose
- process.stdout.write JSON.stringify(result, null, 2)
+ options.color ?= tty.isatty(process.stdout.fd)
+
+ if options.raw
+ process.stderr.write "Serializing JSON output...\n" if options.verbose
+ process.stdout.write JSON.stringify(result, null, 2)
+ else
+ process.stderr.write "Producing colored output...\n" if options.verbose
+ process.stdout.write colorizeWithAnsiEscapes(result, color: options.color)
View
@@ -0,0 +1,78 @@
+color = require 'cli-color'
+
+{ extendedTypeOf } = require './util'
+
+Theme =
+ ' ': (s) -> s
+ '+': color.green
+ '-': color.red
+
+
+subcolorize = (key, diff, output, color, indent) ->
+ prefix = if key then "#{key}: " else ''
+ subindent = indent + ' '
+
+ switch extendedTypeOf(diff)
+ when 'object'
+ if ('__old' of diff) and ('__new' of diff) and (Object.keys(diff).length is 2)
+ subcolorize(key, diff.__old, output, '-', indent)
+ subcolorize(key, diff.__new, output, '+', indent)
+ else
+ output color, "#{indent}#{prefix}{"
+ for own subkey, subvalue of diff
+ if m = subkey.match /^(.*)__removed$/
+ subcolorize(m[1], subvalue, output, '-', subindent)
+ else if m = subkey.match /^(.*)__added$/
+ subcolorize(m[1], subvalue, output, '+', subindent)
+ else
+ subcolorize(subkey, subvalue, output, color, subindent)
+ output color, "#{indent}}"
+
+ when 'array'
+ output color, "#{indent}#{prefix}["
+
+ looksLikeDiff = yes
+ for item in diff
+ if (extendedTypeOf(item) isnt 'array') or (item.length != 2) or !(typeof(item[0]) is 'string') or item[0].length != 1 or !(item[0] in [' ', '-', '+', '~'])
+ looksLikeDiff = no
+
+ if looksLikeDiff
+ for [op, subvalue] in diff
+ if op is ' ' && !subvalue?
+ subcolorize('', '...', output, ' ', subindent)
+ else
+ unless op in [' ', '~', '+', '-']
+ throw new Error("Unexpected op '#{op}' in #{JSON.stringify(diff, null, 2)}")
+ op = ' ' if op is '~'
+ subcolorize('', subvalue, output, op, subindent)
+ else
+ for subvalue in diff
+ subcolorize('', subvalue, output, color, subindent)
+
+ output color, "#{indent}]"
+
+ else
+ output(color, indent + prefix + JSON.stringify(diff))
+
+
+colorize = (diff, output) ->
+ subcolorize('', diff, output, ' ', '')
+
+
+colorizeToArray = (diff) ->
+ output = []
+ colorize(diff, (color, line) -> output.push "#{color}#{line}")
+ return output
+
+
+colorizeWithAnsiEscapes = (diff, options={}) ->
+ output = []
+ colorize diff, (color, line) ->
+ if options.color
+ output.push Theme[color]("#{color}#{line}") + "\n"
+ else
+ output.push "#{color}#{line}\n"
+ return output.join('')
+
+
+module.exports = { colorize, colorizeToArray, colorizeWithAnsiEscapes }
View
@@ -1,12 +1,5 @@
{ SequenceMatcher } = require 'difflib'
-
-
-extendedTypeOf = (obj) ->
- result = typeof obj
- if result is 'object' and obj.constructor is Array
- 'array'
- else
- result
+{ extendedTypeOf } = require './util'
isScalar = (obj) -> (typeof obj isnt 'object')
@@ -113,9 +106,9 @@ arrayDiff = (obj1, obj2, stats) ->
result.push ['~', change]
allEqual = no
else
- result.push ['=']
+ result.push [' ']
else
- result.push ['=', item]
+ result.push [' ', item]
score += 10
when 'delete'
for i in [i1 ... i2]
View
@@ -0,0 +1,9 @@
+
+extendedTypeOf = (obj) ->
+ result = typeof obj
+ if result is 'object' and obj.constructor is Array
+ 'array'
+ else
+ result
+
+module.exports = { extendedTypeOf }
View
@@ -14,7 +14,8 @@
},
"dependencies": {
"dreamopt": "~0.6.0",
- "difflib": "~0.2.1"
+ "difflib": "~0.2.1",
+ "cli-color": "~0.1.6"
},
"devDependencies": {
"mocha": "~1.0.1"
View
@@ -0,0 +1,29 @@
+assert = require 'assert'
+
+{ colorizeToArray } = require '../lib/colorize'
+
+describe 'colorize', ->
+
+ it "should return ' <value>' for a scalar value", ->
+ assert.deepEqual [' 42'], colorizeToArray(42)
+
+ it "should return '-<old value>', '+<new value>' for a scalar diff", ->
+ assert.deepEqual ['-42', '+10'], colorizeToArray({ __old: 42, __new: 10 })
+
+ it "should return '-<removed key>: <removed value>' for an object diff with a removed key", ->
+ assert.deepEqual [' {', '- foo: 42', ' }'], colorizeToArray({ foo__removed: 42 })
+
+ it "should return '+<added key>: <added value>' for an object diff with an added key", ->
+ assert.deepEqual [' {', '+ foo: 42', ' }'], colorizeToArray({ foo__added: 42 })
+
+ it "should return '+<added key>: <added stringified value>' for an object diff with an added key and a non-scalar value", ->
+ assert.deepEqual [' {', '+ foo: {', '+ bar: 42', '+ }', ' }'], colorizeToArray({ foo__added: { bar: 42 } })
+
+ it "should return ' <modified key>: <colorized diff>' for an object diff with a modified key", ->
+ assert.deepEqual [' {', '- foo: 42', '+ foo: 10', ' }'], colorizeToArray({ foo: { __old: 42, __new: 10 } })
+
+ it "should return '+<inserted item>' for an array diff", ->
+ assert.deepEqual [' [', ' 10', '+ 20', ' 30', ' ]'], colorizeToArray([[' ', 10], ['+', 20], [' ', 30]])
+
+ it "should return '-<deleted item>' for an array diff", ->
+ assert.deepEqual [' [', ' 10', '- 20', ' 30', ' ]'], colorizeToArray([[' ', 10], ['-', 20], [' ', 30]])
View
@@ -41,24 +41,24 @@ describe 'diff', ->
assert.deepEqual undefined, diff([10, 20, 30], [10, 20, 30])
it "should return [..., ['-', <removed item>], ...] for two arrays when the second array is missing a value", ->
- assert.deepEqual [['=', 10], ['-', 20], ['=', 30]], diff([10, 20, 30], [10, 30])
+ assert.deepEqual [[' ', 10], ['-', 20], [' ', 30]], diff([10, 20, 30], [10, 30])
it "should return [..., ['+', <added item>], ...] for two arrays when the second one has an extra value", ->
- assert.deepEqual [['=', 10], ['+', 20], ['=', 30]], diff([10, 30], [10, 20, 30])
+ assert.deepEqual [[' ', 10], ['+', 20], [' ', 30]], diff([10, 30], [10, 20, 30])
it "should return [..., ['+', <added item>]] for two arrays when the second one has an extra value at the end (edge case test)", ->
- assert.deepEqual [['=', 10], ['=', 20], ['+', 30]], diff([10, 20], [10, 20, 30])
+ assert.deepEqual [[' ', 10], [' ', 20], ['+', 30]], diff([10, 20], [10, 20, 30])
describe 'with arrays of objects', ->
it "should return undefined for two arrays with identical contents", ->
assert.deepEqual undefined, diff([{ foo: 10 }, { foo: 20 }, { foo: 30 }], [{ foo: 10 }, { foo: 20 }, { foo: 30 }])
it "should return [..., ['-', <removed item>], ...] for two arrays when the second array is missing a value", ->
- assert.deepEqual [['='], ['-', { foo: 20 }], ['=']], diff([{ foo: 10 }, { foo: 20 }, { foo: 30 }], [{ foo: 10 }, { foo: 30 }])
+ assert.deepEqual [[' '], ['-', { foo: 20 }], [' ']], diff([{ foo: 10 }, { foo: 20 }, { foo: 30 }], [{ foo: 10 }, { foo: 30 }])
it "should return [..., ['+', <added item>], ...] for two arrays when the second array has an extra value", ->
- assert.deepEqual [['='], ['+', { foo: 20 }], ['=']], diff([{ foo: 10 }, { foo: 30 }], [{ foo: 10 }, { foo: 20 }, { foo: 30 }])
+ assert.deepEqual [[' '], ['+', { foo: 20 }], [' ']], diff([{ foo: 10 }, { foo: 30 }], [{ foo: 10 }, { foo: 20 }, { foo: 30 }])
it "should return [..., ['~', <diff>], ...] for two arrays when an item has been modified (note: involves a crazy heuristic)", ->
- assert.deepEqual [['='], ['~', { foo: { __old: 20, __new: 21 } }], ['=']], diff([{ foo: 10, bar: { bbbar: 10, bbboz: 11 } }, { foo: 20, bar: { bbbar: 50, bbboz: 25 } }, { foo: 30, bar: { bbbar: 92, bbboz: 34 } }], [{ foo: 10, bar: { bbbar: 10, bbboz: 11 } }, { foo: 21, bar: { bbbar: 50, bbboz: 25 } }, { foo: 30, bar: { bbbar: 92, bbboz: 34 } }])
+ assert.deepEqual [[' '], ['~', { foo: { __old: 20, __new: 21 } }], [' ']], diff([{ foo: 10, bar: { bbbar: 10, bbboz: 11 } }, { foo: 20, bar: { bbbar: 50, bbboz: 25 } }, { foo: 30, bar: { bbbar: 92, bbboz: 34 } }], [{ foo: 10, bar: { bbbar: 10, bbboz: 11 } }, { foo: 21, bar: { bbbar: 50, bbboz: 25 } }, { foo: 30, bar: { bbbar: 92, bbboz: 34 } }])

0 comments on commit c37d270

Please sign in to comment.