@matrixreq/htmldiff 1.3.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':
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.3.0",
5
+ "version": "1.4.0",
6
6
  "keywords": [
7
7
  "diff",
8
8
  "html",
@@ -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
  });