Skip to content

Commit 004844a

Browse files
committed
Enable diffing of video, math, widgets, iframes, imgs, svgs. This commit is basically a copy of PR inkling#10 from @ian97531. See https://github.com/inkling/htmldiff.js/pull/10/commits.
1 parent d55f0d8 commit 004844a

File tree

3 files changed

+240
-48
lines changed

3 files changed

+240
-48
lines changed

js/htmldiff.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* 'tag1|tag2|tag3|...' e. g. 'head|script|style|...'. An atomic tag is one whose child
1212
* nodes should not be compared - the entire tag should be treated as one token. This is
1313
* useful for tags where it does not make sense to insert <ins> and <del> tags.
14-
* If not used, the default list 'iframe|object|math|svg|script|head|style' will be used.
14+
* If not used, the default list 'iframe|object|math|svg|script|video|head|style' will be used.
1515
*
1616
* @return {string} The combined HTML content with differences wrapped in <ins> and <del> tags.
1717
*/

js/htmldiff.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
*/
7171
var atomicTagsRegExp;
7272
// Added head and style (for style tags inside the body)
73-
var defaultAtomicTagsRegExp = new RegExp('^<(iframe|object|math|svg|script|head|style)');
73+
var defaultAtomicTagsRegExp = new RegExp('^<(iframe|object|math|svg|script|video|head|style)');
7474

7575
/**
7676
* Checks if the current word is the beginning of an atomic tag. An atomic tag is one whose
@@ -120,7 +120,8 @@
120120
* @return {boolean} True if the token can be wrapped inside a tag, false otherwise.
121121
*/
122122
function isWrappable(token){
123-
return isntTag(token) || isStartOfAtomicTag(token) || isVoidTag(token);
123+
var is_img = /^<img[\s>]/.test(token);
124+
return is_img|| isntTag(token) || isStartOfAtomicTag(token) || isVoidTag(token);
124125
}
125126

126127
/**
@@ -281,11 +282,48 @@
281282
* @return {string} The identifying key that should be used to match before and after tokens.
282283
*/
283284
function getKeyForToken(token){
285+
// If the token is an image element, grab it's src attribute to include in the key.
286+
var img = /^<img.*src=['"]([^"']*)['"].*>$/.exec(token);
287+
if (img) {
288+
return '<img src="' + img[1] + '">';
289+
}
290+
291+
// If the token is an object element, grab it's data attribute to include in the key.
292+
var object = /^<object.*data=['"]([^"']*)['"]/.exec(token);
293+
if (object) {
294+
return '<object src="' + object[1] + '"></object>';
295+
}
296+
297+
// If it's a video, math or svg element, the entire token should be compared except the
298+
// data-uuid.
299+
if(/^<(svg|math|video)[\s>]/.test(token)) {
300+
var uuid = token.indexOf('data-uuid="');
301+
if (uuid !== -1) {
302+
var start = token.slice(0, uuid);
303+
var end = token.slice(uuid + 44);
304+
return start + end;
305+
} else {
306+
return token;
307+
}
308+
}
309+
310+
// If the token is an iframe element, grab it's src attribute to include in it's key.
311+
var iframe = /^<iframe.*src=['"]([^"']*)['"].*>/.exec(token);
312+
if (iframe) {
313+
return '<iframe src="' + iframe[1] + '"></iframe>';
314+
}
315+
316+
// If the token is any other element, just grab the tag name.
284317
var tagName = /<([^\s>]+)[\s>]/.exec(token);
285318
if (tagName){
286319
return '<' + (tagName[1].toLowerCase()) + '>';
287320
}
288-
return token && token.replace(/(\s+|&nbsp;|&#160;)/g, ' ');
321+
322+
// Otherwise, the token is text, collapse the whitespace.
323+
if (token) {
324+
return token.replace(/(\s+|&nbsp;|&#160;)/g, ' ');
325+
}
326+
return token;
289327
}
290328

291329
/**
@@ -907,7 +945,7 @@
907945
* operation index data attribute will be named `data-${dataPrefix-}operation-index`.
908946
* @param {string} atomicTags (Optional) List of atomic tag names. The list has to be in the
909947
* form 'tag1|tag2|tag3|...' e. g. 'head|script|style|...'. If not used, the default list
910-
* 'iframe|object|math|svg|script|head|style' will be used.
948+
* 'iframe|object|math|svg|script|video|head|style' will be used.
911949
*
912950
* @return {string} The combined HTML content with differences wrapped in <ins> and <del> tags.
913951
*/

test/diff.spec.js

Lines changed: 197 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,210 @@
11
describe('Diff', function(){
2-
var cut, res;
3-
2+
var cut, res, html_to_tokens, calculate_operations;
3+
44
beforeEach(function(){
5-
cut = require('../js/htmldiff');
5+
cut = require('../js/htmldiff');
6+
html_to_tokens = cut.htmlToTokens;
7+
calculate_operations = cut.calculateOperations;
68
});
7-
9+
810
describe('When both inputs are the same', function(){
9-
beforeEach(function(){
10-
res = cut('input text', 'input text');
11+
beforeEach(function(){
12+
res = cut('input text', 'input text');
13+
});
14+
15+
it('should return the text', function(){
16+
expect(res).equal('input text');
17+
});
18+
}); // describe('When both inputs are the same')
19+
20+
describe('When a letter is added', function(){
21+
beforeEach(function(){
22+
res = cut('input', 'input 2');
23+
});
24+
25+
it('should mark the new letter', function(){
26+
expect(res).to.equal('input<ins data-operation-index="1"> 2</ins>');
27+
});
28+
}); // describe('When a letter is added')
29+
30+
describe('Whitespace differences', function(){
31+
it('should collapse adjacent whitespace', function(){
32+
expect(cut('Much \n\t spaces', 'Much spaces')).to.equal('Much spaces');
33+
});
34+
35+
it('should consider non-breaking spaces as equal', function(){
36+
expect(cut('Hello&nbsp;world', 'Hello&#160;world')).to.equal('Hello&#160;world');
37+
});
38+
39+
it('should consider non-breaking spaces and non-adjacent regular spaces as equal', function(){
40+
expect(cut('Hello&nbsp;world', 'Hello world')).to.equal('Hello world');
41+
});
42+
}); // describe('Whitespace differences')
43+
44+
describe('When a class name is specified', function(){
45+
it('should include the class in the wrapper tags', function(){
46+
expect(cut('input', 'input 2', 'diff-result')).to.equal(
47+
'input<ins data-operation-index="1" class="diff-result"> 2</ins>');
48+
});
49+
}); // describe('When a class name is specified')
50+
51+
describe('Image Differences', function(){
52+
it('show two images as different if their src attributes are different', function() {
53+
var before = html_to_tokens('<img src="a.jpg">');
54+
var after = html_to_tokens('<img src="b.jpg">');
55+
var ops = calculate_operations(before, after);
56+
expect(ops.length).to.equal(1);
57+
expect(ops[0]).to.eql({
58+
action: 'replace',
59+
startInBefore: 0,
60+
endInBefore: 0,
61+
startInAfter: 0,
62+
endInAfter: 0
1163
});
12-
13-
it('should return the text', function(){
14-
expect(res).equal('input text');
64+
});
65+
66+
it('should show two images are the same if their src attributes are the same', function() {
67+
var before = html_to_tokens('<img src="a.jpg">');
68+
var after = html_to_tokens('<img src="a.jpg" alt="hey!">');
69+
var ops = calculate_operations(before, after);
70+
expect(ops.length).to.equal(1);
71+
expect(ops[0]).to.eql({
72+
action: 'equal',
73+
startInBefore: 0,
74+
endInBefore: 0,
75+
startInAfter: 0,
76+
endInAfter: 0
1577
});
16-
});
17-
18-
describe('When a letter is added', function(){
19-
beforeEach(function(){
20-
res = cut('input', 'input 2');
78+
});
79+
}); // describe('Image Differences')
80+
81+
describe('Widget Differences', function(){
82+
it('show two widgets as different if their data attributes are different', function() {
83+
var before = html_to_tokens('<object data="a.jpg"></object>');
84+
var after = html_to_tokens('<object data="b.jpg"></object>');
85+
var ops = calculate_operations(before, after);
86+
expect(ops.length).to.equal(1);
87+
expect(ops[0]).to.eql({
88+
action: 'replace',
89+
startInBefore: 0,
90+
endInBefore: 0,
91+
startInAfter: 0,
92+
endInAfter: 0
2193
});
22-
23-
it('should mark the new letter', function(){
24-
expect(res).to.equal('input<ins data-operation-index="1"> 2</ins>');
94+
});
95+
96+
it('should show two widgets are the same if their data attributes are the same', function() {
97+
var before = html_to_tokens('<object data="a.jpg"><param>yo!</param></object>');
98+
var after = html_to_tokens('<object data="a.jpg"></object>');
99+
var ops = calculate_operations(before, after);
100+
expect(ops.length).to.equal(1);
101+
expect(ops[0]).to.eql({
102+
action: 'equal',
103+
startInBefore: 0,
104+
endInBefore: 0,
105+
startInAfter: 0,
106+
endInAfter: 0
25107
});
26-
});
27-
28-
describe('Whitespace differences', function(){
29-
it('should collapse adjacent whitespace', function(){
30-
expect(cut('Much \n\t spaces', 'Much spaces')).to.equal('Much spaces');
108+
});
109+
}); // describe('Widget Differences')
110+
111+
describe('Math Differences', function(){
112+
it('should show two math elements as different if their contents are different', function() {
113+
var before = html_to_tokens('<math data-uuid="55784cd906504787a8e459e80e3bb554"><msqrt>' +
114+
'<msup><mi>b</mi><mn>2</mn></msup></msqrt></math>');
115+
var after = html_to_tokens('<math data-uuid="55784cd906504787a8e459e80e3bb554"><msqrt>' +
116+
'<msup><mn>b</mn><mn>5</mn></msup></msqrt></math>');
117+
var ops = calculate_operations(before, after);
118+
expect(ops.length).to.equal(1);
119+
expect(ops[0]).to.eql({
120+
action: 'replace',
121+
startInBefore: 0,
122+
endInBefore: 0,
123+
startInAfter: 0,
124+
endInAfter: 0
31125
});
32-
33-
it('should consider non-breaking spaces equal', function(){
34-
expect(cut('Hello&nbsp;world', 'Hello&#160;world')).to.equal('Hello&#160;world');
126+
});
127+
128+
it('should show two math elements as the same if their contents are the same', function() {
129+
var before = html_to_tokens('<math data-uuid="15568cd906504876548459e80e356878"><msqrt>' +
130+
'<msup><mi>b</mi><mn>2</mn></msup></msqrt></math>');
131+
var after = html_to_tokens('<math data-uuid="55784cd906504787a8e459e80e3bb554"><msqrt>' +
132+
'<msup><mi>b</mi><mn>2</mn></msup></msqrt></math>');
133+
var ops = calculate_operations(before, after);
134+
expect(ops.length).to.equal(1);
135+
expect(ops[0]).to.eql({
136+
action: 'equal',
137+
startInBefore: 0,
138+
endInBefore: 0,
139+
startInAfter: 0,
140+
endInAfter: 0
35141
});
36-
37-
it('should consider non-breaking spaces and non-adjacent regular spaces equal', function(){
38-
expect(cut('Hello&nbsp;world', 'Hello world')).to.equal('Hello world');
142+
});
143+
}); // describe('Math Differences')
144+
145+
describe('Video Differences', function(){
146+
it('show two widgets as different if their data attributes are different', function() {
147+
var before = html_to_tokens('<video data-uuid="0787866ab5494d88b4b1ee423453224b">' +
148+
'<source src="inkling-video:///big_buck_bunny/webm_high" type="video/webm" /></video>');
149+
var after = html_to_tokens('<video data-uuid="0787866ab5494d88b4b1ee423453224b">' +
150+
'<source src="inkling-video:///big_buck_rabbit/mp4" type="video/webm" /></video>');
151+
var ops = calculate_operations(before, after);
152+
expect(ops.length).to.equal(1);
153+
expect(ops[0]).to.eql({
154+
action: 'replace',
155+
startInBefore: 0,
156+
endInBefore: 0,
157+
startInAfter: 0,
158+
endInAfter: 0
39159
});
40-
});
41-
42-
describe('When a class name is specified', function(){
43-
it('should include the class in the wrapper tags', function(){
44-
expect(cut('input', 'input 2', 'diff-result')).to.equal(
45-
'input<ins data-operation-index="1" class="diff-result"> 2</ins>');
160+
161+
});
162+
163+
it('should show two widgets are the same if their data attributes are the same', function() {
164+
var before = html_to_tokens('<video data-uuid="65656565655487787484545454548494">' +
165+
'<source src="inkling-video:///big_buck_bunny/webm_high" type="video/webm" /></video>');
166+
var after = html_to_tokens('<video data-uuid="0787866ab5494d88b4b1ee423453224b">' +
167+
'<source src="inkling-video:///big_buck_bunny/webm_high" type="video/webm" /></video>');
168+
var ops = calculate_operations(before, after);
169+
expect(ops.length).to.equal(1);
170+
expect(ops[0]).to.eql({
171+
action: 'equal',
172+
startInBefore: 0,
173+
endInBefore: 0,
174+
startInAfter: 0,
175+
endInAfter: 0
46176
});
47-
});
48-
49-
describe('When a data prefix is specified', function(){
50-
it('should include the data prefix in data attributes', function(){
51-
expect(cut('input', 'input <b>2</b>', 'diff-result', 'prefix')).to.equal(
52-
'input<b data-diff-node="ins" data-prefix-operation-index="1">' +
53-
'<ins data-prefix-operation-index="1" class="diff-result">2</ins></b>');
177+
});
178+
}); // describe('Video Differences')
179+
180+
describe('iframe Differences', function(){
181+
it('show two widgets as different if their data attributes are different', function() {
182+
var before = html_to_tokens('<iframe src="a.jpg"></iframe>');
183+
var after = html_to_tokens('<iframe src="b.jpg"></iframe>');
184+
var ops = calculate_operations(before, after);
185+
expect(ops.length).to.equal(1);
186+
expect(ops[0]).to.eql({
187+
action: 'replace',
188+
startInBefore: 0,
189+
endInBefore: 0,
190+
startInAfter: 0,
191+
endInAfter: 0
54192
});
55-
});
56-
});
193+
});
194+
195+
it('should show two widgets are the same if their data attributes are the same', function() {
196+
var before = html_to_tokens('<iframe src="a.jpg"></iframe>');
197+
var after = html_to_tokens('<iframe src="a.jpg" class="foo"></iframe>');
198+
var ops = calculate_operations(before, after);
199+
expect(ops.length).to.equal(1);
200+
expect(ops[0]).to.eql({
201+
action: 'equal',
202+
startInBefore: 0,
203+
endInBefore: 0,
204+
startInAfter: 0,
205+
endInAfter: 0
206+
});
207+
});
208+
}); // describe('iframe Differences')
209+
210+
}); // describe('Diff')

0 commit comments

Comments
 (0)