@matrixreq/htmldiff 1.2.0 → 1.4.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/js/htmldiff.js +51 -20
- package/package.json +1 -1
- package/test/diff.spec.js +50 -1
- package/test/html_to_tokens.spec.js +37 -0
- package/test/render_operations.spec.js +3 -2
package/js/htmldiff.js
CHANGED
|
@@ -89,17 +89,30 @@
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @param {string} tag The ending tag to look for.
|
|
97
|
-
*
|
|
98
|
-
* @return {boolean} True if the word is now a complete token (including the end tag),
|
|
99
|
-
* false otherwise.
|
|
92
|
+
* Inspects the last tag in the given string its slice from the final '<'. A '>' before the slice's end
|
|
93
|
+
* means text follows e.g. "a > b" in a <script>, not a clean tag, so returns false.
|
|
94
|
+
* @return {boolean} True if word ends with an opening (non-self-closing) tag for the given
|
|
95
|
+
* tag name.
|
|
100
96
|
*/
|
|
101
|
-
function
|
|
102
|
-
|
|
97
|
+
function isOpeningTagOf(word, tag){
|
|
98
|
+
var tagText = word.substring(word.lastIndexOf('<'));
|
|
99
|
+
if (tagText.indexOf('>') !== tagText.length - 1) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return new RegExp('^<' + tag + '(\\s|>)').test(tagText) && !/\/>$/.test(tagText);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Inspects the last tag in the given string its slice from the final '<'. A '>' before the slice's end
|
|
107
|
+
* means text follows e.g. "a > b" in a <script>, not a clean tag, so returns false.
|
|
108
|
+
* @return {boolean} True if word ends with a closing tag for the given tag name.
|
|
109
|
+
*/
|
|
110
|
+
function isClosingTagOf(word, tag){
|
|
111
|
+
var tagText = word.substring(word.lastIndexOf('<'));
|
|
112
|
+
if (tagText.indexOf('>') !== tagText.length - 1) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return new RegExp('^</' + tag + '(\\s|>)').test(tagText);
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
/**
|
|
@@ -175,6 +188,7 @@
|
|
|
175
188
|
var mode = 'char';
|
|
176
189
|
var currentWord = '';
|
|
177
190
|
var currentAtomicTag = '';
|
|
191
|
+
var currentAtomicTagDepth = 0;
|
|
178
192
|
var words = [];
|
|
179
193
|
for (var i = 0; i < html.length; i++){
|
|
180
194
|
var char = html[i];
|
|
@@ -184,6 +198,8 @@
|
|
|
184
198
|
if (atomicTag){
|
|
185
199
|
mode = 'atomic_tag';
|
|
186
200
|
currentAtomicTag = atomicTag;
|
|
201
|
+
// skip standalone tags like <script> and <style>
|
|
202
|
+
currentAtomicTagDepth = isEndOfTag(char) ? 1 : 0;
|
|
187
203
|
currentWord += char;
|
|
188
204
|
} else if (isStartofHTMLComment(currentWord)){
|
|
189
205
|
mode = 'html_comment';
|
|
@@ -202,14 +218,22 @@
|
|
|
202
218
|
}
|
|
203
219
|
break;
|
|
204
220
|
case 'atomic_tag':
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
currentWord += char;
|
|
222
|
+
// track the same name nested tags depth;
|
|
223
|
+
// end the atomic token only when it returns to 0.
|
|
224
|
+
if (isEndOfTag(char)){
|
|
225
|
+
if (isClosingTagOf(currentWord, currentAtomicTag)){
|
|
226
|
+
currentAtomicTagDepth--;
|
|
227
|
+
if (currentAtomicTagDepth <= 0){
|
|
228
|
+
words.push(createToken(currentWord));
|
|
229
|
+
currentWord = '';
|
|
230
|
+
currentAtomicTag = '';
|
|
231
|
+
currentAtomicTagDepth = 0;
|
|
232
|
+
mode = 'char';
|
|
233
|
+
}
|
|
234
|
+
} else if (isOpeningTagOf(currentWord, currentAtomicTag)){
|
|
235
|
+
currentAtomicTagDepth++;
|
|
236
|
+
}
|
|
213
237
|
}
|
|
214
238
|
break;
|
|
215
239
|
case 'html_comment':
|
|
@@ -823,7 +847,11 @@
|
|
|
823
847
|
data.status = notes[index].isWrappable;
|
|
824
848
|
}
|
|
825
849
|
var status = notes[index].isWrappable;
|
|
826
|
-
|
|
850
|
+
|
|
851
|
+
// Handling atomic tags wrapping independently
|
|
852
|
+
// each atomic tag is wrapped with their own ins/del tags
|
|
853
|
+
var isAtomic = isStartOfAtomicTag(token);
|
|
854
|
+
if (status !== data.status || (isAtomic && index > data.lastIndex) || data.lastWasAtomic){
|
|
827
855
|
data.list.push({
|
|
828
856
|
isWrappable: data.status,
|
|
829
857
|
tokens: tokens.slice(data.lastIndex, index)
|
|
@@ -831,6 +859,9 @@
|
|
|
831
859
|
data.lastIndex = index;
|
|
832
860
|
data.status = status;
|
|
833
861
|
}
|
|
862
|
+
// tracking if the last token was an atomic tag
|
|
863
|
+
// if so then we break the segment and wrap them
|
|
864
|
+
data.lastWasAtomic = isAtomic;
|
|
834
865
|
if (index === tokens.length - 1){
|
|
835
866
|
data.list.push({
|
|
836
867
|
isWrappable: data.status,
|
|
@@ -838,7 +869,7 @@
|
|
|
838
869
|
});
|
|
839
870
|
}
|
|
840
871
|
return data;
|
|
841
|
-
}, {list: [], status: null, lastIndex: 0}).list;
|
|
872
|
+
}, {list: [], status: null, lastIndex: 0, lastWasAtomic: false}).list;
|
|
842
873
|
|
|
843
874
|
return segments.map(mapFn).join('');
|
|
844
875
|
};
|
package/package.json
CHANGED
package/test/diff.spec.js
CHANGED
|
@@ -206,5 +206,54 @@ describe('Diff', function(){
|
|
|
206
206
|
});
|
|
207
207
|
});
|
|
208
208
|
}); // describe('iframe Differences')
|
|
209
|
-
|
|
209
|
+
|
|
210
|
+
describe('Adjacent atomic tag combining', function(){
|
|
211
|
+
it('should wrap each inserted atomic tag independently when multiple are inserted', function(){
|
|
212
|
+
var result = cut('', '<iframe src="a.html"></iframe><iframe src="b.html"></iframe>');
|
|
213
|
+
expect(result).to.equal(
|
|
214
|
+
'<ins data-operation-index="0"><iframe src="a.html"></iframe></ins>' +
|
|
215
|
+
'<ins data-operation-index="0"><iframe src="b.html"></iframe></ins>'
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should wrap each deleted atomic tag independently when multiple are deleted', function(){
|
|
220
|
+
var result = cut('<iframe src="a.html"></iframe><iframe src="b.html"></iframe>', '');
|
|
221
|
+
expect(result).to.equal(
|
|
222
|
+
'<del data-operation-index="0"><iframe src="a.html"></iframe></del>' +
|
|
223
|
+
'<del data-operation-index="0"><iframe src="b.html"></iframe></del>'
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should not merge atomic tag with adjacent text in same ins/del', function(){
|
|
228
|
+
var result = cut('hello world', 'hello<iframe src="a.html"></iframe>world');
|
|
229
|
+
expect(result).to.equal('hello<ins data-operation-index="1"><iframe src="a.html"></iframe></ins>world');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should wrap inserted <b> content inside the tag, not in a standalone ins', function(){
|
|
233
|
+
var result = cut('', '<b>hello</b>');
|
|
234
|
+
expect(result).to.equal(
|
|
235
|
+
'<b data-diff-node="ins" data-operation-index="0"><ins data-operation-index="0">hello</ins></b>'
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should mark non-atomic container tags with data-diff-node rather than wrapping with ins', function(){
|
|
240
|
+
var result = cut('', '<li><div>content</div></li>');
|
|
241
|
+
expect(result).to.equal(
|
|
242
|
+
'<li data-diff-node="ins" data-operation-index="0">' +
|
|
243
|
+
'<div data-diff-node="ins" data-operation-index="0">' +
|
|
244
|
+
'<ins data-operation-index="0">content</ins>' +
|
|
245
|
+
'</div>' +
|
|
246
|
+
'</li>'
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should keep adjacent inserted <b> tags as separate segments', function(){
|
|
251
|
+
var result = cut('', '<b>hello</b><b>world</b>');
|
|
252
|
+
expect(result).to.equal(
|
|
253
|
+
'<b data-diff-node="ins" data-operation-index="0"><ins data-operation-index="0">hello</ins></b>' +
|
|
254
|
+
'<b data-diff-node="ins" data-operation-index="0"><ins data-operation-index="0">world</ins></b>'
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}); // describe('Adjacent atomic tag combining')
|
|
258
|
+
|
|
210
259
|
}); // describe('Diff')
|
|
@@ -124,5 +124,42 @@ describe('htmlToTokens', function(){
|
|
|
124
124
|
]
|
|
125
125
|
));
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
describe('nested atomic tags wrapping', function(){
|
|
129
|
+
it('should keep a data-htmldiff-id wrapper with nested same-tag children as one token', function(){
|
|
130
|
+
var atomic = '<span data-htmldiff-id="u1">' +
|
|
131
|
+
'<span class="tooltip"><span class="avatar">AB</span></span>' +
|
|
132
|
+
'Name</span>';
|
|
133
|
+
expect(cut('<div>' + atomic + '</div>')).eql(
|
|
134
|
+
tokenize(['<div>', atomic, '</div>']));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should not close early on the first inner closing tag', function(){
|
|
138
|
+
var atomic = '<span data-htmldiff-id="1"><span>a</span><span>b</span></span>';
|
|
139
|
+
expect(cut(atomic)).eql(tokenize([atomic]));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should not treat a stray ">" in script content as a tag boundary', function(){
|
|
143
|
+
var atomic = '<script>if (a > b) { return a > 0; }</script>';
|
|
144
|
+
expect(cut('<p>' + atomic + '</p>')).eql(
|
|
145
|
+
tokenize(['<p>', atomic, '</p>']));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should ignore self-closing same-named children when counting depth', function(){
|
|
149
|
+
var atomic = '<span data-htmldiff-id="1">x<span/>y</span>';
|
|
150
|
+
expect(cut(atomic)).eql(tokenize([atomic]));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should ignore self-closing same-named children written with a space (<span />)', function(){
|
|
154
|
+
var atomic = '<span data-htmldiff-id="1">x<span />y</span>';
|
|
155
|
+
expect(cut(atomic)).eql(tokenize([atomic]));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should not bump depth on differently-named tags that share a prefix', function(){
|
|
159
|
+
// a tag must not be matched by an atomic tag named "a" appearing as <article>.
|
|
160
|
+
var atomic = '<a href="x"><article>hi</article></a>';
|
|
161
|
+
expect(cut(atomic)).eql(tokenize([atomic]));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
127
164
|
});
|
|
128
165
|
});
|
|
@@ -163,14 +163,15 @@ describe('renderOperations', function(){
|
|
|
163
163
|
'<ins data-operation-index="0">new<br/></ins> text');
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
it('should wrap atomic tags', function(){
|
|
166
|
+
it('should wrap atomic tags independently', function(){
|
|
167
167
|
var before = tokenize(['old', '<iframe src="source.html"></iframe>', ' ', 'text']);
|
|
168
168
|
var after = tokenize(['new', ' ', 'text']);
|
|
169
169
|
|
|
170
170
|
res = cut(before, after);
|
|
171
171
|
|
|
172
172
|
expect(res).to.equal(
|
|
173
|
-
'<del data-operation-index="0">old
|
|
173
|
+
'<del data-operation-index="0">old</del>' +
|
|
174
|
+
'<del data-operation-index="0"><iframe src="source.html"></iframe></del>' +
|
|
174
175
|
'<ins data-operation-index="0">new</ins> text');
|
|
175
176
|
});
|
|
176
177
|
});
|