Skip to content

Commit e6c45b0

Browse files
Add a oneChangePerToken option (#460)
* Add a oneChangePerToken option to emit one change object per token instead of combining consecutive tokens into one change object * Document oneChangePerToken * Add release notes * Add test (which fails; needs changes from #439) * Add test of case with identical texts * Add another test * Move release notes to correct place now that I'm planning this for 6.0.0
1 parent 12e092d commit e6c45b0

File tree

5 files changed

+46
-5
lines changed

5 files changed

+46
-5
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Certain options can be provided in the `options` object of *any* method that cal
167167
168168
(Note that if the ONLY option you want to provide is a callback, you can pass the callback function directly as the `options` parameter instead of passing an object with a `callback` property.)
169169
* `maxEditLength`: a number specifying the maximum edit distance to consider between the old and new texts. If the edit distance is higher than this, jsdiff will return `undefined` instead of a diff. You can use this to limit the computational cost of diffing large, very different texts by giving up early if the cost will be huge. Works for functions that return change objects and also for `structuredPatch`, but not other patch-generation functions.
170+
* `oneChangePerToken`: if `true`, the array of change objects returned will contain one change object per token (e.g. one per line if calling `diffLines`), instead of runs of consecutive tokens that are all added / all removed / all conserved being combined into a single change object.
170171
171172
### Defining custom diffing behaviors
172173

Diff for: release-notes.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [#439](https://github.com/kpdecker/jsdiff/pull/439) Prefer diffs that order deletions before insertions. When faced with a choice between two diffs with an equal total edit distance, the Myers diff algorithm generally prefers one that does deletions before insertions rather than insertions before deletions. For instance, when diffing `abcd` against `acbd`, it will prefer a diff that says to delete the `b` and then insert a new `b` after the `c`, over a diff that says to insert a `c` before the `b` and then delete the existing `c`. JsDiff deviated from the published Myers algorithm in a way that led to it having the opposite preference in many cases, including that example. This is now fixed, meaning diffs output by JsDiff will more accurately reflect what the published Myers diff algorithm would output.
99
- [#455](https://github.com/kpdecker/jsdiff/pull/455) The `added` and `removed` properties of change objects are now guaranteed to be set to a boolean value. (Previously, they would be set to `undefined` or omitted entirely instead of setting them to false.)
1010
- [#464](https://github.com/kpdecker/jsdiff/pull/464) Specifying `{maxEditLength: 0}` now sets a max edit length of 0 instead of no maximum.
11+
- [#460][https://github.com/kpdecker/jsdiff/pull/460] Added `oneChangePerToken` option.
1112

1213
## Development
1314

Diff for: src/diff/base.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Diff.prototype = {
4040
let newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
4141
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
4242
// Identity per the equality and tokenizer
43-
return done([{value: this.join(newString), count: newString.length, added: false, removed: false}]);
43+
return done(buildValues(self, bestPath[0].lastComponent, newString, oldString, self.useLongestToken));
4444
}
4545

4646
// Once we hit the right edge of the edit graph on some diagonal k, we can
@@ -147,7 +147,7 @@ Diff.prototype = {
147147

148148
addToPath(path, added, removed, oldPosInc) {
149149
let last = path.lastComponent;
150-
if (last && last.added === added && last.removed === removed) {
150+
if (last && !this.options.oneChangePerToken && last.added === added && last.removed === removed) {
151151
return {
152152
oldPos: path.oldPos + oldPosInc,
153153
lastComponent: {count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
@@ -170,9 +170,12 @@ Diff.prototype = {
170170
newPos++;
171171
oldPos++;
172172
commonCount++;
173+
if (this.options.oneChangePerToken) {
174+
basePath.lastComponent = {count: 1, previousComponent: basePath.lastComponent, added: false, removed: false};
175+
}
173176
}
174177

175-
if (commonCount) {
178+
if (commonCount && !this.options.oneChangePerToken) {
176179
basePath.lastComponent = {count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false};
177180
}
178181

Diff for: test/diff/character.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,25 @@ import {expect} from 'chai';
66
describe('diff/character', function() {
77
describe('#diffChars', function() {
88
it('Should diff chars', function() {
9-
const diffResult = diffChars('New Value.', 'New ValueMoreData.');
10-
expect(convertChangesToXML(diffResult)).to.equal('New Value<ins>MoreData</ins>.');
9+
const diffResult = diffChars('Old Value.', 'New ValueMoreData.');
10+
expect(convertChangesToXML(diffResult)).to.equal('<del>Old</del><ins>New</ins> Value<ins>MoreData</ins>.');
11+
});
12+
13+
describe('oneChangePerToken option', function() {
14+
it('emits one change per character', function() {
15+
const diffResult = diffChars('Old Value.', 'New ValueMoreData.', {oneChangePerToken: true});
16+
expect(diffResult.length).to.equal(21);
17+
expect(convertChangesToXML(diffResult)).to.equal('<del>O</del><del>l</del><del>d</del><ins>N</ins><ins>e</ins><ins>w</ins> Value<ins>M</ins><ins>o</ins><ins>r</ins><ins>e</ins><ins>D</ins><ins>a</ins><ins>t</ins><ins>a</ins>.');
18+
});
19+
20+
it('correctly handles the case where the texts are identical', function() {
21+
const diffResult = diffChars('foo bar baz qux', 'foo bar baz qux', {oneChangePerToken: true});
22+
expect(diffResult).to.deep.equal(
23+
['f', 'o', 'o', ' ', 'b', 'a', 'r', ' ', 'b', 'a', 'z', ' ', 'q', 'u', 'x'].map(
24+
char => ({value: char, count: 1, added: false, removed: false})
25+
)
26+
);
27+
});
1128
});
1229

1330
describe('case insensitivity', function() {

Diff for: test/diff/line.js

+19
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ describe('diff/line', function() {
101101
});
102102
});
103103

104+
describe('oneChangePerToken option', function() {
105+
it('emits one change per line', function() {
106+
const diffResult = diffLines(
107+
'foo\nbar\nbaz\nqux\n',
108+
'fox\nbar\nbaz\nqux\n',
109+
{ oneChangePerToken: true }
110+
);
111+
expect(diffResult).to.deep.equal(
112+
[
113+
{value: 'foo\n', count: 1, added: false, removed: true},
114+
{value: 'fox\n', count: 1, added: true, removed: false},
115+
{value: 'bar\n', count: 1, added: false, removed: false},
116+
{value: 'baz\n', count: 1, added: false, removed: false},
117+
{value: 'qux\n', count: 1, added: false, removed: false}
118+
]
119+
);
120+
});
121+
});
122+
104123
// Trimmed Line Diff
105124
describe('#TrimmedLineDiff', function() {
106125
it('should diff lines', function() {

0 commit comments

Comments
 (0)