@matrixreq/htmldiff 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@matrixreq/htmldiff",
3
+ "description": "Diff and markup HTML with <ins> and <del> tags",
4
+ "companyname": "Matrix Requirements",
5
+ "version": "1.0.0",
6
+ "keywords": [
7
+ "diff",
8
+ "html",
9
+ "htmldiff",
10
+ "ins",
11
+ "del"
12
+ ],
13
+ "homepage": "https://github.com/MatrixRequirements/htmldiff.js",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/MatrixRequirements/htmldiff.js"
17
+ },
18
+ "readme": "README.md",
19
+ "bugs": {
20
+ "url": "https://github.com/MatrixRequirements/htmldiff.js/issues"
21
+ },
22
+ "license": "MIT",
23
+ "main": "js/htmldiff.js",
24
+ "bin": {
25
+ "htmldiff": "./htmldiff-cli.js"
26
+ },
27
+ "files": [
28
+ "js/htmldiff.js",
29
+ "js/htmldiff.d.ts",
30
+ "sample/*",
31
+ "test/*",
32
+ "htmldiff-cli.js"
33
+ ],
34
+ "types": "js/htmldiff.d.ts",
35
+ "dependencies": {},
36
+ "devDependencies": {
37
+ "@types/node": "18.7.11",
38
+ "@typescript-eslint/eslint-plugin": "5.34.0",
39
+ "@typescript-eslint/parser": "5.34.0",
40
+ "chai": "*",
41
+ "eslint": "8.22.0",
42
+ "eslint-plugin-jsdoc": "39.3.6",
43
+ "pkg": "5.8.0",
44
+ "typescript": "4.7.4",
45
+ "mocha": "*"
46
+ },
47
+ "scripts": {
48
+ "test": "mocha -R min",
49
+ "testsample": "node htmldiff-cli.js sample/before.html sample/after.html - -c myDiffClass -p myPrefix",
50
+ "lint": "eslint -c eslintrc.json --parser-options=project:./tsconfig.json --ext .ts ./",
51
+ "premake": "npm run lint",
52
+ "make": "tsc -p tsconfig.json && node -e \"require('fs').unlinkSync('./htmldiff-cli.d.ts')\"",
53
+ "pack-macos-aarch64": "pkg -t node18-macos-arm64 -C GZip --output ./bin/macos-aarch64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
54
+ "pack-macos-x64": " pkg -t node18-macos-x64 -C GZip --output ./bin/macos-x64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
55
+ "pack-win32-x64": " pkg -t node18-win32-x64 -C GZip --output ./bin/win32-x64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
56
+ "pack-linux-x64": " pkg -t node18-linuxstatic-x64 -C GZip --output ./bin/linux-x64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
57
+ "pack-linux-aarch64": "pkg -t node18-linuxstatic-arm64 -C GZip --output ./bin/linux-aarch64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
58
+ "pack-all": "npm run pack-macos-aarch64 && npm run pack-macos-x64 && npm run pack-win32-x64 && npm run pack-linux-x64 && npm run pack-linux-aarch64"
59
+ }
60
+ }
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ </head>
6
+ <body>
7
+
8
+ <p>That is some more text.</p>
9
+
10
+ <figure>
11
+ <img src="after.png" alt="This is another image" />
12
+ <figcaption>This is another image</figcaption>
13
+ </figure>
14
+
15
+ An anchor to <a href="https://www.idesis.com">idesis GmbH</a>.
16
+
17
+ </body>
18
+ </html>
Binary file
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ </head>
6
+ <body>
7
+
8
+ <p>This is some text.</p>
9
+
10
+ <figure>
11
+ <img src="before.png" alt="This is an image" />
12
+ <figcaption>This is an image</figcaption>
13
+ </figure>
14
+
15
+ An anchor to <a href="http://www.idesis.com">idesis GmbH</a>.
16
+
17
+ </body>
18
+ </html>
Binary file
@@ -0,0 +1,278 @@
1
+ // Calculates the differences into a list of edit operations.
2
+ describe('calculateOperations', function(){
3
+ var cut, res, tokenize;
4
+
5
+ beforeEach(function(){
6
+ cut = require('../js/htmldiff').calculateOperations;
7
+ tokenize = require('../js/htmldiff').htmlToTokens;
8
+ });
9
+
10
+ it('should be a function', function(){
11
+ expect(cut).is.a('function');
12
+ });
13
+
14
+ describe('Actions', function(){
15
+ describe('In the middle', function(){
16
+ describe('Replace', function(){
17
+ beforeEach(function(){
18
+ var before = tokenize('working on it');
19
+ var after = tokenize('working in it');
20
+ res = cut(before, after);
21
+ });
22
+
23
+ it('should result in 3 operations', function(){
24
+ expect(res.length).to.equal(3);
25
+ });
26
+
27
+ it('should replace "on"', function(){
28
+ expect(res[1]).eql({
29
+ action : 'replace',
30
+ startInBefore : 2,
31
+ endInBefore : 2,
32
+ startInAfter : 2,
33
+ endInAfter : 2
34
+ });
35
+ });
36
+ });
37
+
38
+ describe('Insert', function(){
39
+ beforeEach(function(){
40
+ var before = tokenize('working it');
41
+ var after = tokenize('working in it');
42
+ res = cut(before, after);
43
+ });
44
+
45
+ it('should result in 3 operations', function(){
46
+ expect(res.length).to.equal(3);
47
+ });
48
+
49
+ it('should show an insert for "on"', function(){
50
+ expect(res[1]).eql({
51
+ action : 'insert',
52
+ startInBefore : 2,
53
+ endInBefore : null,
54
+ startInAfter : 2,
55
+ endInAfter : 3
56
+ });
57
+ });
58
+
59
+ describe('More than one word', function(){
60
+ beforeEach(function(){
61
+ var before = tokenize('working it');
62
+ var after = tokenize('working all up on it');
63
+ res = cut(before, after);
64
+ });
65
+
66
+ it('should still have 3 operations', function(){
67
+ expect(res.length).to.equal(3);
68
+ });
69
+
70
+ it('should show a big insert', function(){
71
+ expect(res[1]).eql({
72
+ action : 'insert',
73
+ startInBefore : 2,
74
+ endInBefore : null,
75
+ startInAfter : 2,
76
+ endInAfter : 7
77
+ });
78
+ });
79
+ });
80
+ });
81
+
82
+ describe('Delete', function(){
83
+ beforeEach(function(){
84
+ var before = tokenize('this is a lot of text');
85
+ var after = tokenize('this is text');
86
+ res = cut(before, after);
87
+ });
88
+
89
+ it('should return 3 operations', function(){
90
+ expect(res.length).to.equal(3);
91
+ });
92
+
93
+ it('should show the delete in the middle', function(){
94
+ expect(res[1]).eql({
95
+ action : 'delete',
96
+ startInBefore : 4,
97
+ endInBefore : 9,
98
+ startInAfter : 4,
99
+ endInAfter : null
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('Equal', function(){
105
+ beforeEach(function(){
106
+ var before = tokenize('this is what it sounds like');
107
+ var after = tokenize('this is what it sounds like');
108
+ res = cut(before, after);
109
+ });
110
+
111
+ it('should return a single op', function(){
112
+ expect(res.length).to.equal(1);
113
+ expect(res[0]).eql({
114
+ action : 'equal',
115
+ startInBefore : 0,
116
+ endInBefore : 10,
117
+ startInAfter : 0,
118
+ endInAfter : 10
119
+ });
120
+ });
121
+ });
122
+ });
123
+
124
+ describe('At the beginning', function(){
125
+ describe('Replace', function(){
126
+ beforeEach(function(){
127
+ var before = tokenize('I dont like veggies');
128
+ var after = tokenize('Joe loves veggies');
129
+ res = cut(before, after);
130
+ });
131
+
132
+ it('should return 2 operations', function(){
133
+ expect(res.length).to.equal(2);
134
+ });
135
+
136
+ it('should have a replace at the beginning', function(){
137
+ expect(res[0]).eql({
138
+ action : 'replace',
139
+ startInBefore : 0,
140
+ endInBefore : 4,
141
+ startInAfter : 0,
142
+ endInAfter : 2
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('Insert', function(){
148
+ beforeEach(function(){
149
+ var before = tokenize('dog');
150
+ var after = tokenize('the shaggy dog');
151
+ res = cut(before, after);
152
+ });
153
+
154
+ it('should return 2 operations', function(){
155
+ expect(res.length).to.equal(2);
156
+ });
157
+
158
+ it('should have an insert at the beginning', function(){
159
+ expect(res[0]).eql({
160
+ action : 'insert',
161
+ startInBefore : 0,
162
+ endInBefore : null,
163
+ startInAfter : 0,
164
+ endInAfter : 3
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('Delete', function(){
170
+ beforeEach(function(){
171
+ var before = tokenize('awesome dog barks');
172
+ var after = tokenize('dog barks');
173
+ res = cut(before, after);
174
+ });
175
+
176
+ it('should return 2 operations', function(){
177
+ expect(res.length).to.equal(2);
178
+ });
179
+
180
+ it('should have a delete at the beginning', function(){
181
+ expect(res[0]).eql({
182
+ action : 'delete',
183
+ startInBefore : 0,
184
+ endInBefore : 1,
185
+ startInAfter : 0,
186
+ endInAfter : null
187
+ });
188
+ });
189
+ });
190
+ });
191
+
192
+ describe('At the end', function(){
193
+ describe('Replace', function(){
194
+ beforeEach(function(){
195
+ var before = tokenize('the dog bit the cat');
196
+ var after = tokenize('the dog bit a bird');
197
+ res = cut(before, after);
198
+ });
199
+
200
+ it('should return 2 operations', function(){
201
+ expect(res.length).to.equal(2);
202
+ });
203
+
204
+ it('should have a replace at the end', function(){
205
+ expect(res[1]).eql({
206
+ action : 'replace',
207
+ startInBefore : 6,
208
+ endInBefore : 8,
209
+ startInAfter : 6,
210
+ endInAfter : 8
211
+ });
212
+ });
213
+ });
214
+
215
+ describe('Insert', function(){
216
+ beforeEach(function(){
217
+ var before = tokenize('this is a dog');
218
+ var after = tokenize('this is a dog that barks');
219
+ res = cut(before, after);
220
+ });
221
+
222
+ it('should return 2 operations', function(){
223
+ expect(res.length).to.equal(2);
224
+ });
225
+
226
+ it('should have an Insert at the end', function(){
227
+ expect(res[1]).eql({
228
+ action : 'insert',
229
+ startInBefore : 7,
230
+ endInBefore : null,
231
+ startInAfter : 7,
232
+ endInAfter : 10
233
+ });
234
+ });
235
+ });
236
+
237
+ describe('Delete', function(){
238
+ beforeEach(function(){
239
+ var before = tokenize('this is a dog that barks');
240
+ var after = tokenize('this is a dog');
241
+ res = cut(before, after);
242
+ });
243
+
244
+ it('should have 2 operations', function(){
245
+ expect(res.length).to.equal(2);
246
+ });
247
+
248
+ it('should have a delete at the end', function(){
249
+ expect(res[1]).eql({
250
+ action : 'delete',
251
+ startInBefore : 7,
252
+ endInBefore : 10,
253
+ startInAfter : 7,
254
+ endInAfter : null
255
+ });
256
+ });
257
+ });
258
+ });
259
+ });
260
+
261
+ describe('Action Combination', function(){
262
+ describe('dont absorb non-single-whitespace tokens', function(){
263
+ beforeEach(function(){
264
+ var before = tokenize('I am awesome');
265
+ var after = tokenize('You are great');
266
+ res = cut(before, after);
267
+ });
268
+
269
+ it('should return 3 actions', function(){
270
+ expect(res.length).to.equal(1);
271
+ });
272
+
273
+ it('should have a replace first', function(){
274
+ expect(res[0].action).to.equal('replace');
275
+ });
276
+ });
277
+ });
278
+ });
package/test/config.js ADDED
@@ -0,0 +1,2 @@
1
+ var chai = require('chai');
2
+ global.expect = chai.expect;
@@ -0,0 +1,210 @@
1
+ describe('Diff', function(){
2
+ var cut, res, html_to_tokens, calculate_operations;
3
+
4
+ beforeEach(function(){
5
+ cut = require('../js/htmldiff');
6
+ html_to_tokens = cut.htmlToTokens;
7
+ calculate_operations = cut.calculateOperations;
8
+ });
9
+
10
+ describe('When both inputs are the same', function(){
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
63
+ });
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
77
+ });
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
93
+ });
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
107
+ });
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
125
+ });
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
141
+ });
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
159
+ });
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
176
+ });
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
192
+ });
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,0 +1,31 @@
1
+ const diff = require('../js/htmldiff');
2
+
3
+ describe('Edge cases', () => {
4
+ it('removes deleted closing-opening tag pair', () => {
5
+ const before = `
6
+ <div>
7
+ <p><img src="img1.jpg" /></p>
8
+ <p><img src="img2.jpg" /></p>
9
+ <p>something</p>
10
+ </div>
11
+ `;
12
+ const after = `
13
+ <div>
14
+ <p>
15
+ <img src="img1.jpg" />
16
+ <img src="img2.jpg" />
17
+ </p>
18
+ <p>something</p>
19
+ </div>
20
+ `;
21
+ const res = `
22
+ <div>
23
+ <p><img src="img1.jpg" /><del>
24
+ </del><img src="img2.jpg" /></p>
25
+ <p>something</p>
26
+ </div>
27
+ `;
28
+
29
+ expect(diff(before, after)).to.eql(res);
30
+ });
31
+ });