1 var traverse = require('traverse');
2 var Stream = require('stream').Stream;
3 var charm = require('charm');
4 var deepEqual = require('deep-equal');
6 var exports = module.exports = function (opts_) {
7 var fn = difflet.bind(null, opts_);
8 fn.compare = function (prev, next) {
9 var opts = Object.keys(opts_ || {}).reduce(function (acc, key) {
10 acc[key] = opts_[key];
13 var s = opts.stream = new Stream;
15 s.write = function (buf) { data += buf };
16 s.end = function () {};
20 difflet(opts, prev, next);
26 exports.compare = function (prev, next) {
27 return exports({}).compare(prev, next);
30 function difflet (opts, prev, next) {
31 var stream = opts.stream || new Stream;
33 stream.readable = true;
34 stream.writable = true;
35 stream.write = function (buf) { this.emit('data', buf) };
36 stream.end = function () { this.emit('end') };
40 if (opts.start === undefined && opts.stop === undefined) {
41 var c = charm(stream);
42 opts.start = function (type) {
51 opts.stop = function (type) {
55 var write = function (buf) {
56 if (opts.write) opts.write(buf, stream)
57 else stream.write(buf)
60 var commaFirst = opts.comma === 'first';
62 var stringify = function (node, params) {
63 return stringifier.call(this, true, node, params || opts);
65 var plainStringify = function (node, params) {
66 return stringifier.call(this, false, node, params || opts);
71 if (levels === 0) opts.start(type, stream);
75 function unset (type) {
76 if (--levels === 0) opts.stop(type, stream);
79 function stringifier (insertable, node, opts) {
80 var indent = opts.indent;
83 var prevNode = traverse.get(prev, this.path || []);
85 var inserted = insertable && prevNode === undefined;
87 var indentx = indent ? Array(
88 ((this.path || []).length + 1) * indent + 1
90 if (commaFirst) indentx = indentx.slice(indent);
92 if (Array.isArray(node)) {
93 var updated = (prevNode || traverse.has(prev, this.path))
94 && !Array.isArray(prevNode);
99 if (opts.comment && !Array.isArray(prevNode)) {
103 this.before(function () {
104 if (inserted) set('inserted');
105 if (indent && commaFirst) {
106 if ((this.path || []).length === 0
107 || Array.isArray(this.parent.node)) {
110 else write('\n' + indentx + '[ ');
113 write('[\n' + indentx);
120 this.post(function (child) {
121 if (!child.isLast && !(indent && commaFirst)) {
125 var prev = prevNode && prevNode[child.key];
126 if (indent && opts.comment && child.node !== prev
127 && (typeof child.node !== 'object' || typeof prev !== 'object')
131 traverse(prev).forEach(function (x) {
132 plainStringify.call(this, x, { indent : 0 });
138 if (indent && commaFirst) {
139 write('\n' + indentx + ', ');
142 write('\n' + indentx);
147 this.after(function () {
148 if (indent && commaFirst) write('\n' + indentx);
149 else if (indent) write('\n' + indentx.slice(indent));
152 if (updated) unset('updated');
153 if (inserted) unset('inserted');
156 else if (isRegExp(node)) {
161 write(node.toString());
164 else if (insertable && prevNode !== node) {
166 write(node.toString());
169 else write(node.toString());
171 else if (typeof node === 'object'
172 && node && typeof node.inspect === 'function') {
176 write(node.inspect());
179 else if (!(prevNode && typeof prevNode.inspect === 'function'
180 && prevNode.inspect() === node.inspect())) {
182 write(node.inspect());
185 else write(node.inspect());
187 else if (typeof node == 'object' && node !== null) {
188 var insertedKey = false;
189 var deleted = insertable && typeof prevNode === 'object' && prevNode
190 ? Object.keys(prevNode).filter(function (key) {
191 return !Object.hasOwnProperty.call(node, key);
196 this.before(function () {
197 if (inserted) set('inserted');
198 write(indent && commaFirst && !this.isRoot
199 ? '\n' + indentx + '{ '
204 this.pre(function (x, key) {
206 var obj = traverse.get(prev, this.path.concat(key));
207 if (obj === undefined) {
213 if (indent && !commaFirst) write('\n' + indentx);
216 write(indent ? ' : ' : ':');
219 this.post(function (child) {
220 if (!child.isLast && !(indent && commaFirst)) {
224 if (child.isLast && deleted.length) {
225 if (insertedKey) unset('inserted');
228 else if (insertedKey) {
233 var prev = prevNode && prevNode[child.key];
234 if (indent && opts.comment && child.node !== prev
235 && (typeof child.node !== 'object' || typeof prev !== 'object')
239 traverse(prev).forEach(function (x) {
240 plainStringify.call(this, x, { indent : 0 });
245 if (child.isLast && deleted.length) {
246 if (insertedKey) unset('inserted');
249 if (indent && commaFirst) {
250 write('\n' + indentx + ', ')
252 else if (opts.comment && indent) {
253 write('\n' + indentx);
256 write(',\n' + indentx);
262 if (indent && commaFirst) {
263 write('\n' + indentx + ', ');
269 this.after(function () {
270 if (inserted) unset('inserted');
272 if (deleted.length) {
273 if (indent && !commaFirst
274 && Object.keys(node).length === 0) {
275 write('\n' + indentx);
279 deleted.forEach(function (key, ix) {
280 if (indent && opts.comment) {
289 write(indent ? ' : ' : ':');
290 traverse(prevNode[key]).forEach(function (x) {
291 plainStringify.call(this, x, { indent : 0 });
294 var last = ix === deleted.length - 1;
295 if (insertable && !last) {
296 if (indent && commaFirst) {
297 write('\n' + indentx + ', ');
300 write(',\n' + indentx);
308 if (commaFirst && indent) {
309 write(indentx.slice(indent) + ' }');
312 write('\n' + indentx.slice(indent) + '}');
320 if (inserted) set('inserted');
321 else if (insertable && !deepEqual(prevNode, node)) {
326 if (typeof node === 'string') {
327 write('"' + node.toString().replace(/"/g, '\\"') + '"');
329 else if (isRegExp(node)) {
330 write(node.toString());
332 else if (typeof node === 'function') {
334 ? '[Function: ' + node.name + ']'
338 else if (node === undefined) {
341 else if (node === null) {
345 write(node.toString());
348 if (inserted) unset('inserted');
349 else if (changed) unset('updated');
354 traverse(next).forEach(stringify);
356 else process.nextTick(function () {
357 traverse(next).forEach(stringify);
364 function isRegExp (node) {
365 return node instanceof RegExp || (node
366 && typeof node.test === 'function'
367 && typeof node.exec === 'function'
368 && typeof node.compile === 'function'
369 && node.constructor && node.constructor.name === 'RegExp'