@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 CHANGED
@@ -89,17 +89,30 @@
89
89
  }
90
90
 
91
91
  /**
92
- * Checks if the current word is the end of an atomic tag (i.e. it has all the characters,
93
- * except for the end bracket of the closing tag, such as '<iframe></iframe').
94
- *
95
- * @param {string} word The characters of the current token read so far.
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 isEndOfAtomicTag(word, tag){
102
- return word.substring(word.length - tag.length - 2) === ('</' + tag);
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
- if (isEndOfTag(char) && isEndOfAtomicTag(currentWord, currentAtomicTag)){
206
- currentWord += '>';
207
- words.push(createToken(currentWord));
208
- currentWord = '';
209
- currentAtomicTag = '';
210
- mode = 'char';
211
- } else {
212
- currentWord += char;
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
- if (status !== data.status){
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
@@ -2,7 +2,7 @@
2
2
  "name": "@matrixreq/htmldiff",
3
3
  "description": "Diff and markup HTML with <ins> and <del> tags",
4
4
  "companyname": "Matrix Requirements",
5
- "version": "1.2.0",
5
+ "version": "1.4.0",
6
6
  "keywords": [
7
7
  "diff",
8
8
  "html",
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<iframe src="source.html"></iframe></del>' +
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
  });