diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..7fa83f32 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "sourceMaps": "inline", + "presets": ["es3", "es2015-mod"], + "auxiliaryCommentBefore": "istanbul ignore start", + "auxiliaryCommentAfter": "istanbul ignore end" +} diff --git a/.npmignore b/.npmignore index 0840f4bc..b5b2b76a 100644 --- a/.npmignore +++ b/.npmignore @@ -13,3 +13,4 @@ Gruntfile.js index.html karma.conf.js style.css +.babelrc diff --git a/README.md b/README.md index 007bffb0..5747fe36 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ bower install jsdiff --save The optional `options` object may have the following keys: - `fuzzFactor`: Number of lines that are allowed to differ before rejecting a patch. Defaults to 0. - - `compareLine(lineNumber, line, operation, patchContent)`: Callback used to compare to given lines to determine if they should be considered equal when patching. Defaults to strict equality but may be overriden to provide fuzzier comparison. Should return false if the lines should be rejected. + - `compareLine(lineNumber, line, operation, patchContent)`: Callback used to compare to given lines to determine if they should be considered equal when patching. Defaults to strict equality but may be overridden to provide fuzzier comparison. Should return false if the lines should be rejected. * `JsDiff.applyPatches(patch, options)` - applies one or more patches. @@ -141,7 +141,7 @@ Note that some cases may omit a particular flag field. Comparison on the flag fi Basic example in Node ```js -require('colors') +require('colors'); var jsdiff = require('diff'); var one = 'beep boop'; @@ -157,7 +157,7 @@ diff.forEach(function(part){ process.stderr.write(part.value[color]); }); -console.log() +console.log(); ``` Running the above program should yield diff --git a/components/bower.json b/components/bower.json index f333ee90..2b0707d6 100644 --- a/components/bower.json +++ b/components/bower.json @@ -1,6 +1,6 @@ { "name": "jsdiff", - "version": "3.4.0", + "version": "3.5.0", "main": [ "diff.js" ], diff --git a/components/component.json b/components/component.json index f8755355..790543cf 100644 --- a/components/component.json +++ b/components/component.json @@ -6,7 +6,7 @@ "diff", "text" ], - "version": "3.4.0", + "version": "3.5.0", "scripts": [ "diff.js" ], "main": "diff.js", "license": "BSD" diff --git a/package.json b/package.json index 9e09dc2f..d2376bfb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "diff", - "version": "3.4.0", + "version": "3.5.0", "description": "A javascript text diff implementation.", "keywords": [ "diff", @@ -63,14 +63,5 @@ "webpack": "^1.12.2", "webpack-dev-server": "^1.12.0" }, - "optionalDependencies": {}, - "babel": { - "sourceMaps": "inline", - "presets": [ - "es3", - "es2015-mod" - ], - "auxiliaryCommentBefore": "istanbul ignore start", - "auxiliaryCommentAfter": "istanbul ignore end" - } + "optionalDependencies": {} } diff --git a/release-notes.md b/release-notes.md index fa407b04..0116a2b7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,25 @@ ## Development -[Commits](https://github.com/kpdecker/jsdiff/compare/v3.4.0...master) +[Commits](https://github.com/kpdecker/jsdiff/compare/v3.5.0...master) + +## v3.5.0 - March 4th, 2018 +- Omit redundant slice in join method of diffArrays - 1023590 +- Support patches with empty lines - fb0f208 +- Accept a custom JSON replacer function for JSON diffing - 69c7f0a +- Optimize parch header parser - 2aec429 +- Fix typos - e89c832 + +[Commits](https://github.com/kpdecker/jsdiff/compare/v3.5.0...v3.5.0) + +## v3.5.0 - March 4th, 2018 +- Omit redundant slice in join method of diffArrays - 1023590 +- Support patches with empty lines - fb0f208 +- Accept a custom JSON replacer function for JSON diffing - 69c7f0a +- Optimize parch header parser - 2aec429 +- Fix typos - e89c832 + +[Commits](https://github.com/kpdecker/jsdiff/compare/v3.4.0...v3.5.0) ## v3.4.0 - October 7th, 2017 - [#183](https://github.com/kpdecker/jsdiff/issues/183) - Feature request: ability to specify a custom equality checker for `diffArrays` @@ -119,7 +137,7 @@ Compatibility notes: - [#69](https://github.com/kpdecker/jsdiff/issues/69) - Missing count ([@wfalkwallace](https://api.github.com/users/wfalkwallace)) - [#68](https://github.com/kpdecker/jsdiff/issues/68) - diffLines seems broken ([@wfalkwallace](https://api.github.com/users/wfalkwallace)) - [#60](https://github.com/kpdecker/jsdiff/issues/60) - Support multiple diff hunks ([@piranna](https://api.github.com/users/piranna)) -- [#54](https://github.com/kpdecker/jsdiff/issues/54) - Feature Reuqest: 3-way merge ([@mog422](https://api.github.com/users/mog422)) +- [#54](https://github.com/kpdecker/jsdiff/issues/54) - Feature Request: 3-way merge ([@mog422](https://api.github.com/users/mog422)) - [#42](https://github.com/kpdecker/jsdiff/issues/42) - Fuzz factor for applyPatch ([@stuartpb](https://api.github.com/users/stuartpb)) - Move whitespace ignore out of equals method - 542063c - Include source maps in babel output - 7f7ab21 @@ -152,7 +170,7 @@ Compatibility notes: - [#29](https://github.com/kpdecker/jsdiff/issues/29) - word tokenizer works only for 7 bit ascii ([@plasmagunman](https://api.github.com/users/plasmagunman)) Compatibility notes: -- `this.removeEmpty` is now called automatically for all instances. If this is not desired, this may be overriden on a per instance basis. +- `this.removeEmpty` is now called automatically for all instances. If this is not desired, this may be overridden on a per instance basis. - The library has been refactored to use some ES6 features. The external APIs should remain the same, but bower projects that directly referenced the repository will now have to point to the [components/jsdiff](https://github.com/components/jsdiff) repository. [Commits](https://github.com/kpdecker/jsdiff/compare/v1.4.0...v2.0.0) diff --git a/src/diff/array.js b/src/diff/array.js index 28de3a0c..99de08fc 100644 --- a/src/diff/array.js +++ b/src/diff/array.js @@ -1,10 +1,10 @@ import Diff from './base'; export const arrayDiff = new Diff(); -arrayDiff.tokenize = arrayDiff.join = function(value) { +arrayDiff.tokenize = function(value) { return value.slice(); }; -arrayDiff.removeEmpty = function(value) { +arrayDiff.join = arrayDiff.removeEmpty = function(value) { return value; }; diff --git a/src/diff/json.js b/src/diff/json.js index 126351b7..6c06c9ab 100644 --- a/src/diff/json.js +++ b/src/diff/json.js @@ -11,15 +11,9 @@ jsonDiff.useLongestToken = true; jsonDiff.tokenize = lineDiff.tokenize; jsonDiff.castInput = function(value) { - const {undefinedReplacement} = this.options; + const {undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v} = this.options; - return typeof value === 'string' ? value : JSON.stringify(canonicalize(value), function(k, v) { - if (typeof v === 'undefined') { - return undefinedReplacement; - } - - return v; - }, ' '); + return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' '); }; jsonDiff.equals = function(left, right) { return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1')); @@ -28,11 +22,15 @@ jsonDiff.equals = function(left, right) { export function diffJson(oldObj, newObj, options) { return jsonDiff.diff(oldObj, newObj, options); } // This function handles the presence of circular references by bailing out when encountering an -// object that is already on the "stack" of items being processed. -export function canonicalize(obj, stack, replacementStack) { +// object that is already on the "stack" of items being processed. Accepts an optional replacer +export function canonicalize(obj, stack, replacementStack, replacer, key) { stack = stack || []; replacementStack = replacementStack || []; + if (replacer) { + obj = replacer(key, obj); + } + let i; for (i = 0; i < stack.length; i += 1) { @@ -48,7 +46,7 @@ export function canonicalize(obj, stack, replacementStack) { canonicalizedObj = new Array(obj.length); replacementStack.push(canonicalizedObj); for (i = 0; i < obj.length; i += 1) { - canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack); + canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key); } stack.pop(); replacementStack.pop(); @@ -74,7 +72,7 @@ export function canonicalize(obj, stack, replacementStack) { sortedKeys.sort(); for (i = 0; i < sortedKeys.length; i += 1) { key = sortedKeys[i]; - canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack); + canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack, replacer, key); } stack.pop(); replacementStack.pop(); diff --git a/src/patch/apply.js b/src/patch/apply.js index e830fc11..7bbdb6d0 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -34,8 +34,8 @@ export function applyPatch(source, uniDiff, options = {}) { function hunkFits(hunk, toPos) { for (let j = 0; j < hunk.lines.length; j++) { let line = hunk.lines[j], - operation = line[0], - content = line.substr(1); + operation = (line.length > 0 ? line[0] : ' '), + content = (line.length > 0 ? line.substr(1) : line); if (operation === ' ' || operation === '-') { // Context sanity check @@ -91,8 +91,8 @@ export function applyPatch(source, uniDiff, options = {}) { for (let j = 0; j < hunk.lines.length; j++) { let line = hunk.lines[j], - operation = line[0], - content = line.substr(1), + operation = (line.length > 0 ? line[0] : ' '), + content = (line.length > 0 ? line.substr(1) : line), delimiter = hunk.linedelimiters[j]; if (operation === ' ') { diff --git a/src/patch/parse.js b/src/patch/parse.js index 310e3a94..d1af8d3d 100755 --- a/src/patch/parse.js +++ b/src/patch/parse.js @@ -53,16 +53,16 @@ export function parsePatch(uniDiff, options = {}) { // Parses the --- and +++ headers, if none are found, no lines // are consumed. function parseFileHeader(index) { - const headerPattern = /^(---|\+\+\+)\s+([\S ]*)(?:\t(.*?)\s*)?$/; - const fileHeader = headerPattern.exec(diffstr[i]); + const fileHeader = (/^(---|\+\+\+)\s+(.*)$/).exec(diffstr[i]); if (fileHeader) { let keyPrefix = fileHeader[1] === '---' ? 'old' : 'new'; - let fileName = fileHeader[2].replace(/\\\\/g, '\\'); + const data = fileHeader[2].split('\t', 2); + let fileName = data[0].replace(/\\\\/g, '\\'); if (/^".*"$/.test(fileName)) { fileName = fileName.substr(1, fileName.length - 2); } index[keyPrefix + 'FileName'] = fileName; - index[keyPrefix + 'Header'] = fileHeader[3]; + index[keyPrefix + 'Header'] = (data[1] || '').trim(); i++; } @@ -95,7 +95,7 @@ export function parsePatch(uniDiff, options = {}) { && diffstr[i + 2].indexOf('@@') === 0) { break; } - let operation = diffstr[i][0]; + let operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0]; if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') { hunk.lines.push(diffstr[i]); diff --git a/test/diff/array.js b/test/diff/array.js index df15e82c..33dcf61a 100644 --- a/test/diff/array.js +++ b/test/diff/array.js @@ -33,7 +33,35 @@ describe('diff/array', function() { {count: 1, value: [c], removed: true, added: undefined} ]); }); + describe('anti-aliasing', function() { + // Test apparent contract that no chunk value is ever an input argument. + const value = [0, 1, 2]; + const expected = [ + {count: value.length, value: value} + ]; + const input = value.slice(); + const diffResult = diffArrays(input, input); + it('returns correct deep result for identical inputs', function() { + expect(diffResult).to.deep.equals(expected); + }); + it('does not return the input array', function() { + expect(diffResult[0].value).to.not.equal(input); + }); + + const input1 = value.slice(); + const input2 = value.slice(); + const diffResult2 = diffArrays(input1, input2); + it('returns correct deep result for equivalent inputs', function() { + expect(diffResult2).to.deep.equals(expected); + }); + it('does not return the first input array', function() { + expect(diffResult2[0].value).to.not.equal(input1); + }); + it('does not return the second input array', function() { + expect(diffResult2[0].value).to.not.equal(input2); + }); + }); it('Should diff arrays with comparator', function() { const a = {a: 0}, b = {a: 1}, c = {a: 2}, d = {a: 3}; function comparator(left, right) { diff --git a/test/diff/json.js b/test/diff/json.js index 977504f1..1754218a 100644 --- a/test/diff/json.js +++ b/test/diff/json.js @@ -15,6 +15,7 @@ describe('diff/json', function() { { count: 1, value: '}' } ]); }); + it('should accept objects with different order', function() { expect(diffJson( {a: 123, b: 456, c: 789}, @@ -136,6 +137,51 @@ describe('diff/json', function() { expect(getKeys(canonicalObj)).to.eql(['a', 'b']); expect(getKeys(canonicalObj.a)).to.eql(['a', 'b']); }); + + it('should accept a custom JSON.stringify() replacer function', function() { + expect(diffJson( + {a: 123}, + {a: /foo/} + )).to.eql([ + { count: 1, value: '{\n' }, + { count: 1, value: ' \"a\": 123\n', added: undefined, removed: true }, + { count: 1, value: ' \"a\": {}\n', added: true, removed: undefined }, + { count: 1, value: '}' } + ]); + + expect(diffJson( + {a: 123}, + {a: /foo/gi}, + {stringifyReplacer: (k, v) => v instanceof RegExp ? v.toString() : v} + )).to.eql([ + { count: 1, value: '{\n' }, + { count: 1, value: ' \"a\": 123\n', added: undefined, removed: true }, + { count: 1, value: ' \"a\": "/foo/gi"\n', added: true, removed: undefined }, + { count: 1, value: '}' } + ]); + + expect(diffJson( + {a: 123}, + {a: new Error('ohaider')}, + {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v} + )).to.eql([ + { count: 1, value: '{\n' }, + { count: 1, value: ' \"a\": 123\n', added: undefined, removed: true }, + { count: 1, value: ' \"a\": "Error: ohaider"\n', added: true, removed: undefined }, + { count: 1, value: '}' } + ]); + + expect(diffJson( + {a: 123}, + {a: [new Error('ohaider')]}, + {stringifyReplacer: (k, v) => v instanceof Error ? `${v.name}: ${v.message}` : v} + )).to.eql([ + { count: 1, value: '{\n' }, + { count: 1, value: ' \"a\": 123\n', added: undefined, removed: true }, + { count: 3, value: ' \"a\": [\n "Error: ohaider"\n ]\n', added: true, removed: undefined }, + { count: 1, value: '}' } + ]); + }); }); }); diff --git a/test/patch/apply.js b/test/patch/apply.js index 67965662..125199a8 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -180,6 +180,23 @@ describe('patch/apply', function() { + ' line4\n' + ' line4\n')) .to.equal('line1\nline2\nline3\nline4\nline4\nline4\nline4'); + + // Test empty lines in patches + expect(applyPatch( + 'line11\nline2\n\nline4', + + 'Index: test\n' + + '===================================================================\n' + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,4 +1,4 @@\n' + + '+line1\n' + + '-line11\n' + + ' line2\n' + + '\n' + + ' line4\n' + + '\\ No newline at end of file\n')) + .to.equal('line1\nline2\n\nline4'); }); it('should apply patches', function() {