@createiq/htmldiff 1.0.2 → 1.0.4-beta.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/dist/HtmlDiff.js DELETED
@@ -1,827 +0,0 @@
1
- // src/Action.ts
2
- var Action = /* @__PURE__ */ ((Action2) => {
3
- Action2[Action2["Equal"] = 0] = "Equal";
4
- Action2[Action2["Delete"] = 1] = "Delete";
5
- Action2[Action2["Insert"] = 2] = "Insert";
6
- Action2[Action2["None"] = 3] = "None";
7
- Action2[Action2["Replace"] = 4] = "Replace";
8
- return Action2;
9
- })(Action || {});
10
- var Action_default = Action;
11
-
12
- // src/Match.ts
13
- var Match = class {
14
- _startInOld;
15
- _startInNew;
16
- _size;
17
- constructor(startInOld, startInNew, size) {
18
- this._startInOld = startInOld;
19
- this._startInNew = startInNew;
20
- this._size = size;
21
- }
22
- get startInOld() {
23
- return this._startInOld;
24
- }
25
- get startInNew() {
26
- return this._startInNew;
27
- }
28
- get size() {
29
- return this._size;
30
- }
31
- get endInOld() {
32
- return this._startInOld + this._size;
33
- }
34
- get endInNew() {
35
- return this._startInNew + this._size;
36
- }
37
- };
38
-
39
- // src/Utils.ts
40
- var openingTagRegex = /^\s*<[^>]+>\s*$/;
41
- var closingTagTexRegex = /^\s*<\/[^>]+>\s*$/;
42
- var tagWordRegex = /<[^\s>]+/;
43
- var whitespaceRegex = /^(\s|&nbsp;)+$/;
44
- var wordRegex = /[\w#@]+/;
45
- var tagRegex = /<\/?(?<name>[^\s/>]+)[^>]*>/;
46
- var SpecialCaseWordTags = ["<img"];
47
- function isTag(item) {
48
- if (SpecialCaseWordTags.some((re) => item?.startsWith(re))) {
49
- return false;
50
- }
51
- return isOpeningTag(item) || isClosingTag(item);
52
- }
53
- function isOpeningTag(item) {
54
- return openingTagRegex.test(item);
55
- }
56
- function isClosingTag(item) {
57
- return closingTagTexRegex.test(item);
58
- }
59
- function stripTagAttributes(word) {
60
- const match = tagWordRegex.exec(word);
61
- if (match) {
62
- return `${match[0]}${word.endsWith("/>") ? "/>" : ">"}`;
63
- }
64
- return word;
65
- }
66
- function wrapText(text, tagName, cssClass) {
67
- return `<${tagName} class='${cssClass}'>${text}</${tagName}>`;
68
- }
69
- function isStartOfTag(val) {
70
- return val === "<";
71
- }
72
- function isEndOfTag(val) {
73
- return val === ">";
74
- }
75
- function isStartOfEntity(val) {
76
- return val === "&";
77
- }
78
- function isEndOfEntity(val) {
79
- return val === ";";
80
- }
81
- function isWhiteSpace(value) {
82
- return whitespaceRegex.test(value);
83
- }
84
- function stripAnyAttributes(word) {
85
- if (isTag(word)) {
86
- return stripTagAttributes(word);
87
- }
88
- return word;
89
- }
90
- function isWord(text) {
91
- return wordRegex.test(text);
92
- }
93
- function getTagName(word) {
94
- if (word === null) {
95
- return "";
96
- }
97
- const match = tagRegex.exec(word);
98
- if (match) {
99
- return match.groups?.name.toLowerCase() ?? match[1].toLowerCase();
100
- }
101
- return "";
102
- }
103
- var Utils_default = {
104
- isTag,
105
- stripTagAttributes,
106
- wrapText,
107
- isStartOfTag,
108
- isEndOfTag,
109
- isStartOfEntity,
110
- isEndOfEntity,
111
- isWhiteSpace,
112
- stripAnyAttributes,
113
- isWord,
114
- getTagName
115
- };
116
-
117
- // src/MatchFinder.ts
118
- var MatchFinder = class _MatchFinder {
119
- oldWords;
120
- newWords;
121
- startInOld;
122
- endInOld;
123
- startInNew;
124
- endInNew;
125
- wordIndices = {};
126
- options;
127
- constructor(oldWords, newWords, startInOld, endInOld, startInNew, endInNew, options) {
128
- this.oldWords = oldWords;
129
- this.newWords = newWords;
130
- this.startInOld = startInOld;
131
- this.endInOld = endInOld;
132
- this.startInNew = startInNew;
133
- this.endInNew = endInNew;
134
- this.options = options;
135
- }
136
- indexNewWords() {
137
- this.wordIndices = {};
138
- const block = [];
139
- for (let i = this.startInNew; i < this.endInNew; i++) {
140
- const word = this.normalizeForIndex(this.newWords[i]);
141
- const key = _MatchFinder.putNewWord(block, word, this.options.blockSize);
142
- if (key === null) {
143
- continue;
144
- }
145
- if (!this.wordIndices[key]) {
146
- this.wordIndices[key] = [];
147
- }
148
- this.wordIndices[key].push(i);
149
- }
150
- }
151
- static putNewWord(block, word, blockSize) {
152
- block.push(word);
153
- if (block.length > blockSize) {
154
- block.shift();
155
- }
156
- if (block.length !== blockSize) {
157
- return null;
158
- }
159
- return block.join("");
160
- }
161
- normalizeForIndex(word) {
162
- const output = Utils_default.stripAnyAttributes(word);
163
- if (this.options.ignoreWhitespaceDifferences && Utils_default.isWhiteSpace(output)) {
164
- return " ";
165
- }
166
- return output;
167
- }
168
- findMatch() {
169
- this.indexNewWords();
170
- this.removeRepeatingWords();
171
- let hasIndices = false;
172
- for (const _key in this.wordIndices) {
173
- hasIndices = true;
174
- break;
175
- }
176
- if (!hasIndices) {
177
- return null;
178
- }
179
- let bestMatchInOld = this.startInOld;
180
- let bestMatchInNew = this.startInNew;
181
- let bestMatchSize = 0;
182
- let matchLengthAt = /* @__PURE__ */ new Map();
183
- const block = [];
184
- for (let indexInOld = this.startInOld; indexInOld < this.endInOld; indexInOld++) {
185
- const word = this.normalizeForIndex(this.oldWords[indexInOld]);
186
- const index = _MatchFinder.putNewWord(block, word, this.options.blockSize);
187
- if (index === null) {
188
- continue;
189
- }
190
- const newMatchLengthAt = /* @__PURE__ */ new Map();
191
- if (!this.wordIndices[index]) {
192
- matchLengthAt = newMatchLengthAt;
193
- continue;
194
- }
195
- for (const indexInNew of this.wordIndices[index]) {
196
- const newMatchLength = (matchLengthAt.has(indexInNew - 1) ? matchLengthAt.get(indexInNew - 1) : 0) + 1;
197
- newMatchLengthAt.set(indexInNew, newMatchLength);
198
- if (newMatchLength > bestMatchSize) {
199
- bestMatchInOld = indexInOld - newMatchLength - this.options.blockSize + 2;
200
- bestMatchInNew = indexInNew - newMatchLength - this.options.blockSize + 2;
201
- bestMatchSize = newMatchLength;
202
- }
203
- }
204
- matchLengthAt = newMatchLengthAt;
205
- }
206
- return bestMatchSize !== 0 ? new Match(bestMatchInOld, bestMatchInNew, bestMatchSize + this.options.blockSize - 1) : null;
207
- }
208
- /**
209
- * This method removes words that occur too many times. This way it reduces total count of comparison operations
210
- * and as result the diff algorithm takes less time. But the side effect is that it may detect false differences of
211
- * the repeating words.
212
- * @private
213
- */
214
- removeRepeatingWords() {
215
- const threshold = this.newWords.length * this.options.repeatingWordsAccuracy;
216
- const repeatingWords = Object.entries(this.wordIndices).filter(([, indices]) => indices.length > threshold).map(([word]) => word);
217
- for (const w of repeatingWords) {
218
- delete this.wordIndices[w];
219
- }
220
- }
221
- };
222
-
223
- // src/Operation.ts
224
- var Operation = class {
225
- action;
226
- startInOld;
227
- endInOld;
228
- startInNew;
229
- endInNew;
230
- constructor(action, startInOld, endInOld, startInNew, endInNew) {
231
- this.action = action;
232
- this.startInOld = startInOld;
233
- this.endInOld = endInOld;
234
- this.startInNew = startInNew;
235
- this.endInNew = endInNew;
236
- }
237
- };
238
-
239
- // src/Mode.ts
240
- var Mode = /* @__PURE__ */ ((Mode2) => {
241
- Mode2[Mode2["Character"] = 0] = "Character";
242
- Mode2[Mode2["Tag"] = 1] = "Tag";
243
- Mode2[Mode2["Whitespace"] = 2] = "Whitespace";
244
- Mode2[Mode2["Entity"] = 3] = "Entity";
245
- return Mode2;
246
- })(Mode || {});
247
- var Mode_default = Mode;
248
-
249
- // src/WordSplitter.ts
250
- var WordSplitter = class _WordSplitter {
251
- text;
252
- isBlockCheckRequired;
253
- blockLocations;
254
- mode;
255
- isGrouping = false;
256
- globbingUntil;
257
- currentWord;
258
- words;
259
- static NotGlobbing = -1;
260
- get currentWordHasChars() {
261
- return this.currentWord.length > 0;
262
- }
263
- constructor(text, blockExpressions) {
264
- this.text = text;
265
- this.blockLocations = new BlockFinder(text, blockExpressions).findBlocks();
266
- this.isBlockCheckRequired = this.blockLocations.hasBlocks;
267
- this.mode = Mode_default.Character;
268
- this.globbingUntil = _WordSplitter.NotGlobbing;
269
- this.currentWord = [];
270
- this.words = [];
271
- }
272
- process() {
273
- for (let index = 0; index < this.text.length; index++) {
274
- const character = this.text.charAt(index);
275
- this.processCharacter(index, character);
276
- }
277
- this.appendCurrentWordToWords();
278
- return this.words;
279
- }
280
- processCharacter(index, character) {
281
- if (this.isGlobbing(index, character)) {
282
- return;
283
- }
284
- switch (this.mode) {
285
- case Mode_default.Character:
286
- this.processTextCharacter(character);
287
- break;
288
- case Mode_default.Tag:
289
- this.processHtmlTagContinuation(character);
290
- break;
291
- case Mode_default.Whitespace:
292
- this.processWhiteSpaceContinuation(character);
293
- break;
294
- case Mode_default.Entity:
295
- this.processEntityContinuation(character);
296
- break;
297
- }
298
- }
299
- processEntityContinuation(character) {
300
- if (Utils_default.isStartOfTag(character)) {
301
- this.appendCurrentWordToWords();
302
- this.currentWord.push(character);
303
- this.mode = Mode_default.Tag;
304
- } else if (character.trim().length === 0) {
305
- this.appendCurrentWordToWords();
306
- this.currentWord.push(character);
307
- this.mode = Mode_default.Whitespace;
308
- } else if (Utils_default.isEndOfEntity(character)) {
309
- let switchToNextMode = true;
310
- if (this.currentWordHasChars) {
311
- this.currentWord.push(character);
312
- this.words.push(this.currentWord.join(""));
313
- if (this.words.length > 2 && Utils_default.isWhiteSpace(this.words[this.words.length - 2]) && Utils_default.isWhiteSpace(this.words[this.words.length - 1])) {
314
- const w1 = this.words[this.words.length - 2];
315
- const w2 = this.words[this.words.length - 1];
316
- this.words.splice(this.words.length - 2, 2);
317
- this.currentWord = `${w1}${w2}`.split("");
318
- this.mode = Mode_default.Whitespace;
319
- switchToNextMode = false;
320
- }
321
- }
322
- if (switchToNextMode) {
323
- this.currentWord = [];
324
- this.mode = Mode_default.Character;
325
- }
326
- } else if (Utils_default.isWord(character)) {
327
- this.currentWord.push(character);
328
- } else {
329
- this.appendCurrentWordToWords();
330
- this.currentWord.push(character);
331
- this.mode = Mode_default.Character;
332
- }
333
- }
334
- processWhiteSpaceContinuation(character) {
335
- if (Utils_default.isStartOfTag(character)) {
336
- this.appendCurrentWordToWords();
337
- this.currentWord.push(character);
338
- this.mode = Mode_default.Tag;
339
- } else if (Utils_default.isStartOfEntity(character)) {
340
- this.appendCurrentWordToWords();
341
- this.currentWord.push(character);
342
- this.mode = Mode_default.Entity;
343
- } else if (Utils_default.isWhiteSpace(character)) {
344
- this.currentWord.push(character);
345
- } else {
346
- this.appendCurrentWordToWords();
347
- this.currentWord.push(character);
348
- this.mode = Mode_default.Character;
349
- }
350
- }
351
- processHtmlTagContinuation(character) {
352
- if (Utils_default.isEndOfTag(character)) {
353
- this.currentWord.push(character);
354
- this.appendCurrentWordToWords();
355
- this.mode = Utils_default.isWhiteSpace(character) ? Mode_default.Whitespace : Mode_default.Character;
356
- } else {
357
- this.currentWord.push(character);
358
- }
359
- }
360
- processTextCharacter(character) {
361
- if (Utils_default.isStartOfTag(character)) {
362
- this.appendCurrentWordToWords();
363
- this.currentWord.push("<");
364
- this.mode = Mode_default.Tag;
365
- } else if (Utils_default.isStartOfEntity(character)) {
366
- this.appendCurrentWordToWords();
367
- this.currentWord.push(character);
368
- this.mode = Mode_default.Entity;
369
- } else if (Utils_default.isWhiteSpace(character)) {
370
- this.appendCurrentWordToWords();
371
- this.currentWord.push(character);
372
- this.mode = Mode_default.Whitespace;
373
- } else if (Utils_default.isWord(character) && (this.currentWord.length === 0 || Utils_default.isWord(this.currentWord[this.currentWord.length - 1]))) {
374
- this.currentWord.push(character);
375
- } else {
376
- this.appendCurrentWordToWords();
377
- this.currentWord.push(character);
378
- }
379
- }
380
- appendCurrentWordToWords() {
381
- if (this.currentWordHasChars) {
382
- this.words.push(this.currentWord.join(""));
383
- this.currentWord = [];
384
- }
385
- }
386
- isGlobbing(index, character) {
387
- if (!this.isBlockCheckRequired) {
388
- return false;
389
- }
390
- const isCurrentBlockTerminating = index === this.globbingUntil;
391
- if (isCurrentBlockTerminating) {
392
- this.globbingUntil = _WordSplitter.NotGlobbing;
393
- this.isGrouping = false;
394
- this.appendCurrentWordToWords();
395
- }
396
- const until = this.blockLocations.isInBlock(index);
397
- if (until) {
398
- this.isGrouping = true;
399
- this.globbingUntil = until;
400
- }
401
- if (this.isGrouping) {
402
- this.currentWord.push(character);
403
- this.mode = Mode_default.Character;
404
- }
405
- return this.isGrouping;
406
- }
407
- static convertHtmlToListOfWords(text, blockExpressions) {
408
- return new _WordSplitter(text, blockExpressions).process();
409
- }
410
- };
411
- var BlockFinderResult = class {
412
- blocks = /* @__PURE__ */ new Map();
413
- addBlock(from, to) {
414
- if (this.blocks.has(from)) {
415
- throw new ArgumentError("One or more block expressions result in a text sequence that overlaps.");
416
- }
417
- this.blocks.set(from, to);
418
- }
419
- isInBlock(location) {
420
- return this.blocks.get(location) ?? null;
421
- }
422
- get hasBlocks() {
423
- return this.blocks.size > 0;
424
- }
425
- };
426
- var ArgumentError = class extends Error {
427
- };
428
- var BlockFinder = class {
429
- text;
430
- blockExpressions;
431
- constructor(text, blockExpressions) {
432
- this.text = text;
433
- this.blockExpressions = blockExpressions;
434
- }
435
- findBlocks() {
436
- const result = new BlockFinderResult();
437
- for (const expression of this.blockExpressions) {
438
- this.processBlockMatcher(expression, result);
439
- }
440
- return result;
441
- }
442
- processBlockMatcher(exp, result) {
443
- let match;
444
- while ((match = exp.exec(this.text)) !== null) {
445
- this.tryAddBlock(exp, match, result);
446
- }
447
- }
448
- tryAddBlock(exp, match, result) {
449
- try {
450
- const from = match.index;
451
- const to = match.index + match[0].length;
452
- result.addBlock(from, to);
453
- } catch {
454
- throw new ArgumentError(
455
- `One or more block expressions result in a text sequence that overlaps. Current expression: ${exp}`
456
- );
457
- }
458
- }
459
- };
460
-
461
- // src/HtmlDiff.ts
462
- var HtmlDiff = class _HtmlDiff {
463
- /**
464
- * This value defines balance between speed and memory utilization. The higher it is the faster it works and more memory consumes.
465
- * @private
466
- */
467
- static MatchGranularityMaximum = 4;
468
- static DelTag = "del";
469
- static InsTag = "ins";
470
- // ignore case
471
- static SpecialCaseClosingTags = [
472
- "</strong>",
473
- "</em>",
474
- "</b>",
475
- "</i>",
476
- "</big>",
477
- "</small>",
478
- "</u>",
479
- "</sub>",
480
- "</sup>",
481
- "</strike>",
482
- "</s>",
483
- "</span>"
484
- ];
485
- static SpecialCaseClosingTagsSet = /* @__PURE__ */ new Set([
486
- "</strong>",
487
- "</em>",
488
- "</b>",
489
- "</i>",
490
- "</big>",
491
- "</small>",
492
- "</u>",
493
- "</sub>",
494
- "</sup>",
495
- "</strike>",
496
- "</s>",
497
- "</span>"
498
- ]);
499
- static SpecialCaseOpeningTagRegex = /<((strong)|(b)|(i)|(em)|(big)|(small)|(u)|(sub)|(sup)|(strike)|(s)|(span))[>\s]+/i;
500
- content = [];
501
- newText;
502
- oldText;
503
- specialTagDiffStack = [];
504
- newWords = [];
505
- oldWords = [];
506
- matchGranularity = 0;
507
- blockExpressions = [];
508
- /**
509
- * Defines how to compare repeating words. Valid values are from 0 to 1.
510
- * This value allows to exclude some words from comparison that eventually
511
- * reduces the total time of the diff algorithm.
512
- * 0 means that all words are excluded so the diff will not find any matching words at all.
513
- * 1 (default value) means that all words participate in comparison so this is the most accurate case.
514
- * 0.5 means that any word that occurs more than 50% times may be excluded from comparison. This doesn't
515
- * mean that such words will definitely be excluded but only gives a permission to exclude them if necessary.
516
- */
517
- repeatingWordsAccuracy = 1;
518
- /**
519
- * If true all whitespaces are considered as equal
520
- */
521
- ignoreWhitespaceDifferences = false;
522
- /**
523
- * If some match is too small and located far from its neighbors then it is considered as orphan
524
- * and removed. For example:
525
- * <code>
526
- * aaaaa bb ccccccccc dddddd ee
527
- * 11111 bb 222222222 dddddd ee
528
- * </code>
529
- * will find two matches <code>bb</code> and <code>dddddd ee</code> but the first will be considered
530
- * as orphan and ignored, as result it will consider texts <code>aaaaa bb ccccccccc</code> and
531
- * <code>11111 bb 222222222</code> as single replacement:
532
- * <code>
533
- * &lt;del&gt;aaaaa bb ccccccccc&lt;/del&gt;&lt;ins&gt;11111 bb 222222222&lt;/ins&gt; dddddd ee
534
- * </code>
535
- * This property defines relative size of the match to be considered as orphan, from 0 to 1.
536
- * 1 means that all matches will be considered as orphans.
537
- * 0 (default) means that no match will be considered as orphan.
538
- * 0.2 means that if match length is less than 20% of distance between its neighbors it is considered as orphan.
539
- */
540
- orphanMatchThreshold = 0;
541
- /**
542
- * Initializes a new instance of the class.
543
- * @param oldText The old text.
544
- * @param newText The new text.
545
- */
546
- constructor(oldText, newText) {
547
- this.oldText = oldText;
548
- this.newText = newText;
549
- }
550
- static execute(oldText, newText) {
551
- return new _HtmlDiff(oldText, newText).build();
552
- }
553
- /**
554
- * Builds the HTML diff output
555
- * @return HTML diff markup
556
- */
557
- build() {
558
- if (this.oldText === this.newText) {
559
- return this.newText;
560
- }
561
- this.splitInputsToWords();
562
- this.matchGranularity = Math.min(
563
- _HtmlDiff.MatchGranularityMaximum,
564
- Math.min(this.oldWords.length, this.newWords.length)
565
- );
566
- const operations = this.operations();
567
- for (const op of operations) {
568
- this.performOperation(op);
569
- }
570
- return this.content.join("");
571
- }
572
- /**
573
- * Uses {@link expression} to group text together so that any change detected within the group is treated as a single block
574
- * @param expression
575
- */
576
- addBlockExpression(expression) {
577
- this.blockExpressions.push(expression);
578
- }
579
- splitInputsToWords() {
580
- this.oldWords = WordSplitter.convertHtmlToListOfWords(this.oldText, this.blockExpressions);
581
- this.oldText = "";
582
- this.newWords = WordSplitter.convertHtmlToListOfWords(this.newText, this.blockExpressions);
583
- this.newText = "";
584
- }
585
- performOperation(operation) {
586
- switch (operation.action) {
587
- case Action_default.Equal:
588
- this.processEqualOperation(operation);
589
- break;
590
- case Action_default.Delete:
591
- this.processDeleteOperation(operation, "diffdel");
592
- break;
593
- case Action_default.Insert:
594
- this.processInsertOperation(operation, "diffins");
595
- break;
596
- case Action_default.None:
597
- break;
598
- case Action_default.Replace:
599
- this.processReplaceOperation(operation);
600
- break;
601
- }
602
- }
603
- processReplaceOperation(operation) {
604
- this.processDeleteOperation(operation, "diffmod");
605
- this.processInsertOperation(operation, "diffmod");
606
- }
607
- processInsertOperation(operation, cssClass) {
608
- const text = this.newWords.slice(operation.startInNew, operation.endInNew);
609
- this.insertTag(_HtmlDiff.InsTag, cssClass, text);
610
- }
611
- processDeleteOperation(operation, cssClass) {
612
- const text = this.oldWords.slice(operation.startInOld, operation.endInOld);
613
- this.insertTag(_HtmlDiff.DelTag, cssClass, text);
614
- }
615
- processEqualOperation(operation) {
616
- const result = this.newWords.slice(operation.startInNew, operation.endInNew);
617
- this.content.push(result.join(""));
618
- }
619
- /**
620
- * This method encloses words within a specified tag (ins or del), and adds this into "content",
621
- * with a twist: if there are words contain tags, it actually creates multiple ins or del,
622
- * so that they don't include any ins or del. This handles cases like
623
- * old: '<p>a</p>'
624
- * new: '<p>ab</p>
625
- * <p>
626
- * c</b>'
627
- * diff result: '<p>a<ins>b</ins></p>
628
- * <p>
629
- * <ins>c</ins>
630
- * </p>
631
- * '
632
- * this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or
633
- * del tags), but handles correctly more cases than the earlier version.
634
- * P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day.
635
- * @param tag
636
- * @param cssClass
637
- * @param words
638
- * @private
639
- */
640
- insertTag(tag, cssClass, words) {
641
- while (true) {
642
- if (words.length === 0) {
643
- break;
644
- }
645
- const allWordsUntilFirstTag = this.extractConsecutiveWords(words, (x) => !Utils_default.isTag(x));
646
- if (allWordsUntilFirstTag.length > 0) {
647
- const text = Utils_default.wrapText(allWordsUntilFirstTag.join(""), tag, cssClass);
648
- this.content.push(text);
649
- }
650
- const isInsertOpCompleted = words.length === 0;
651
- if (isInsertOpCompleted) {
652
- break;
653
- }
654
- const indexOfFirstNonTag = words.findIndex((x) => !Utils_default.isTag(x));
655
- const indexLastTagInFirstTagBlock = indexOfFirstNonTag === -1 ? words.length - 1 : indexOfFirstNonTag - 1;
656
- let specialCaseTagInjection = "";
657
- let specialCaseTagInjectionIsBefore = false;
658
- if (_HtmlDiff.SpecialCaseOpeningTagRegex.test(words[0])) {
659
- const tagNames = /* @__PURE__ */ new Set();
660
- for (const word of words) {
661
- if (Utils_default.isTag(word)) {
662
- tagNames.add(Utils_default.getTagName(word));
663
- }
664
- }
665
- const styledTagNames = Array.from(tagNames).join(" ");
666
- this.specialTagDiffStack.push(words[0]);
667
- specialCaseTagInjection = `<ins class='mod ${styledTagNames}'>`;
668
- if (tag === _HtmlDiff.DelTag) {
669
- words.shift();
670
- while (words.length > 0 && _HtmlDiff.SpecialCaseOpeningTagRegex.test(words[0])) {
671
- words.shift();
672
- }
673
- }
674
- } else if (_HtmlDiff.SpecialCaseClosingTagsSet.has(words[0].toLowerCase())) {
675
- const openingTag = this.specialTagDiffStack.length === 0 ? null : this.specialTagDiffStack.pop();
676
- const openingAndClosingTagsMatch = !!openingTag && Utils_default.getTagName(openingTag) === Utils_default.getTagName(words[indexLastTagInFirstTagBlock]);
677
- if (!!openingTag && openingAndClosingTagsMatch) {
678
- specialCaseTagInjection = "</ins>";
679
- specialCaseTagInjectionIsBefore = true;
680
- } else if (openingTag) {
681
- this.specialTagDiffStack.push(openingTag);
682
- }
683
- if (tag === _HtmlDiff.DelTag) {
684
- words.shift();
685
- while (words.length > 0 && _HtmlDiff.SpecialCaseClosingTagsSet.has(words[0].toLowerCase())) {
686
- words.shift();
687
- }
688
- }
689
- }
690
- if (words.length === 0 && specialCaseTagInjection.length === 0) {
691
- break;
692
- }
693
- if (specialCaseTagInjectionIsBefore) {
694
- this.content.push(specialCaseTagInjection + this.extractConsecutiveWords(words, Utils_default.isTag).join(""));
695
- } else {
696
- this.content.push(this.extractConsecutiveWords(words, Utils_default.isTag).join("") + specialCaseTagInjection);
697
- }
698
- if (words.length === 0) continue;
699
- this.insertTag(tag, cssClass, words);
700
- break;
701
- }
702
- }
703
- extractConsecutiveWords(words, condition) {
704
- let indexOfFirstTag = null;
705
- for (let i = 0; i < words.length; i++) {
706
- const word = words[i];
707
- if (i === 0 && word === " ") {
708
- words[i] = "&nbsp;";
709
- }
710
- if (!condition(word)) {
711
- indexOfFirstTag = i;
712
- break;
713
- }
714
- }
715
- if (indexOfFirstTag !== null) {
716
- const items2 = words.slice(0, indexOfFirstTag);
717
- if (indexOfFirstTag > 0) {
718
- words.splice(0, indexOfFirstTag);
719
- }
720
- return items2;
721
- }
722
- const items = words.slice(0);
723
- words.splice(0, words.length);
724
- return items;
725
- }
726
- operations() {
727
- let positionInOld = 0;
728
- let positionInNew = 0;
729
- const operations = [];
730
- const matches = this.matchingBlocks();
731
- matches.push(new Match(this.oldWords.length, this.newWords.length, 0));
732
- const matchesWithoutOrphans = this.removeOrphans(matches);
733
- for (const match of matchesWithoutOrphans) {
734
- const matchStartsAtCurrentPositionInOld = positionInOld === match.startInOld;
735
- const matchStartsAtCurrentPositionInNew = positionInNew === match.startInNew;
736
- let action;
737
- if (!matchStartsAtCurrentPositionInOld && !matchStartsAtCurrentPositionInNew) {
738
- action = Action_default.Replace;
739
- } else if (matchStartsAtCurrentPositionInOld && !matchStartsAtCurrentPositionInNew) {
740
- action = Action_default.Insert;
741
- } else if (!matchStartsAtCurrentPositionInOld) {
742
- action = Action_default.Delete;
743
- } else {
744
- action = Action_default.None;
745
- }
746
- if (action !== Action_default.None) {
747
- operations.push(new Operation(action, positionInOld, match.startInOld, positionInNew, match.startInNew));
748
- }
749
- if (match.size !== 0) {
750
- operations.push(new Operation(Action_default.Equal, match.startInOld, match.endInOld, match.startInNew, match.endInNew));
751
- }
752
- positionInOld = match.endInOld;
753
- positionInNew = match.endInNew;
754
- }
755
- return operations;
756
- }
757
- *removeOrphans(matches) {
758
- let prev = new Match(0, 0, 0);
759
- let curr = null;
760
- for (const next of matches) {
761
- if (curr === null) {
762
- curr = next;
763
- continue;
764
- }
765
- if (prev.endInOld === curr.startInOld && prev.endInNew === curr.startInNew || curr.endInOld === next.startInOld && curr.endInNew === next.startInNew) {
766
- yield curr;
767
- prev = curr;
768
- curr = next;
769
- continue;
770
- }
771
- let oldDistanceInChars = 0;
772
- for (let i = prev.endInOld; i < next.startInOld; i++) {
773
- oldDistanceInChars += this.oldWords[i].length;
774
- }
775
- let newDistanceInChars = 0;
776
- for (let i = prev.endInNew; i < next.startInNew; i++) {
777
- newDistanceInChars += this.newWords[i].length;
778
- }
779
- let currMatchLengthInChars = 0;
780
- for (let i = curr.startInNew; i < curr.endInNew; i++) {
781
- currMatchLengthInChars += this.newWords[i].length;
782
- }
783
- if (currMatchLengthInChars > Math.max(oldDistanceInChars, newDistanceInChars) * this.orphanMatchThreshold) {
784
- yield curr;
785
- }
786
- prev = curr;
787
- curr = next;
788
- }
789
- if (curr !== null) {
790
- yield curr;
791
- }
792
- }
793
- matchingBlocks() {
794
- const matchingBlocks = [];
795
- this.findMatchingBlocks(0, this.oldWords.length, 0, this.newWords.length, matchingBlocks);
796
- return matchingBlocks;
797
- }
798
- findMatchingBlocks(startInOld, endInOld, startInNew, endInNew, matchingBlocks) {
799
- const match = this.findMatch(startInOld, endInOld, startInNew, endInNew);
800
- if (match !== null) {
801
- if (startInOld < match.startInOld && startInNew < match.startInNew) {
802
- this.findMatchingBlocks(startInOld, match.startInOld, startInNew, match.startInNew, matchingBlocks);
803
- }
804
- matchingBlocks.push(match);
805
- if (match.endInOld < endInOld && match.endInNew < endInNew) {
806
- this.findMatchingBlocks(match.endInOld, endInOld, match.endInNew, endInNew, matchingBlocks);
807
- }
808
- }
809
- }
810
- findMatch(startInOld, endInOld, startInNew, endInNew) {
811
- for (let i = this.matchGranularity; i > 0; i--) {
812
- const options = {
813
- blockSize: i,
814
- repeatingWordsAccuracy: this.repeatingWordsAccuracy,
815
- ignoreWhitespaceDifferences: this.ignoreWhitespaceDifferences
816
- };
817
- const finder = new MatchFinder(this.oldWords, this.newWords, startInOld, endInOld, startInNew, endInNew, options);
818
- const match = finder.findMatch();
819
- if (match !== null) return match;
820
- }
821
- return null;
822
- }
823
- };
824
- export {
825
- HtmlDiff as default
826
- };
827
- //# sourceMappingURL=HtmlDiff.js.map