diff_match_patch_es 2.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.
@@ -0,0 +1,1050 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiffMatchPatchES
4
+ DIFF_DELETE = -1
5
+ DIFF_INSERT = 1
6
+ DIFF_EQUAL = 0
7
+
8
+ # Line-mode diffing maps each unique line to a single character. JavaScript
9
+ # strings tolerate lone surrogates (code units 0xD800..0xDFFF) but UTF-8
10
+ # cannot represent them, so indices at or above 0xD800 shift up by 0x800.
11
+ # The mapping is injective either way, and the diff algorithm only observes
12
+ # equality between these placeholder characters, so results are identical.
13
+ LINE_INDEX_SURROGATE_SHIFT = 0x800
14
+ private_constant :LINE_INDEX_SURROGATE_SHIFT
15
+
16
+ module_function
17
+
18
+ def create_diff(op, text)
19
+ [op, text]
20
+ end
21
+
22
+ def now_ms
23
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
24
+ end
25
+
26
+ # Find the differences between two texts.
27
+ def diff_main(text1, text2, options = nil, checklines = true, deadline = nil)
28
+ resolved = resolve_options(options)
29
+
30
+ if deadline.nil?
31
+ deadline = if resolved.diff_timeout <= 0
32
+ Float::MAX
33
+ else
34
+ now_ms + resolved.diff_timeout * 1000
35
+ end
36
+ end
37
+
38
+ raise Error, 'Null input. (diff_main)' if text1.nil? || text2.nil?
39
+
40
+ if text1 == text2
41
+ return [create_diff(DIFF_EQUAL, text1)] unless text1.empty?
42
+
43
+ return []
44
+ end
45
+
46
+ # Trim off common prefix (speedup).
47
+ commonlength = diff_common_prefix(text1, text2)
48
+ commonprefix = JsString.substring(text1, 0, commonlength)
49
+ text1 = JsString.substring(text1, commonlength)
50
+ text2 = JsString.substring(text2, commonlength)
51
+
52
+ # Trim off common suffix (speedup).
53
+ commonlength = diff_common_suffix(text1, text2)
54
+ commonsuffix = JsString.substring(text1, text1.length - commonlength)
55
+ text1 = JsString.substring(text1, 0, text1.length - commonlength)
56
+ text2 = JsString.substring(text2, 0, text2.length - commonlength)
57
+
58
+ # Compute the diff on the middle block.
59
+ diffs = diff_compute(text1, text2, resolved, checklines, deadline)
60
+
61
+ # Restore the prefix and suffix.
62
+ diffs.unshift(create_diff(DIFF_EQUAL, commonprefix)) unless commonprefix.empty?
63
+ diffs.push(create_diff(DIFF_EQUAL, commonsuffix)) unless commonsuffix.empty?
64
+
65
+ diff_cleanup_merge(diffs)
66
+ diffs
67
+ end
68
+
69
+ # Find the differences between two texts. Assumes that the texts do not
70
+ # have any common prefix or suffix.
71
+ def diff_compute(text1, text2, options, checklines, deadline)
72
+ # Just add some text (speedup).
73
+ return [create_diff(DIFF_INSERT, text2)] if text1.empty?
74
+
75
+ # Just delete some text (speedup).
76
+ return [create_diff(DIFF_DELETE, text1)] if text2.empty?
77
+
78
+ longtext = text1.length > text2.length ? text1 : text2
79
+ shorttext = text1.length > text2.length ? text2 : text1
80
+ i = longtext.index(shorttext)
81
+ unless i.nil?
82
+ # Shorter text is inside the longer text (speedup).
83
+ diffs = [
84
+ create_diff(DIFF_INSERT, JsString.substring(longtext, 0, i)),
85
+ create_diff(DIFF_EQUAL, shorttext),
86
+ create_diff(DIFF_INSERT, JsString.substring(longtext, i + shorttext.length)),
87
+ ]
88
+ # Swap insertions for deletions if diff is reversed.
89
+ diffs[0][0] = diffs[2][0] = DIFF_DELETE if text1.length > text2.length
90
+
91
+ return diffs
92
+ end
93
+
94
+ if shorttext.length == 1
95
+ # Single character string.
96
+ # After the previous speedup, the character can't be an equality.
97
+ return [create_diff(DIFF_DELETE, text1), create_diff(DIFF_INSERT, text2)]
98
+ end
99
+
100
+ # Check to see if the problem can be split in two.
101
+ hm = diff_half_match(text1, text2, options)
102
+ if hm
103
+ # A half-match was found, sort out the return data.
104
+ text1_a, text1_b, text2_a, text2_b, mid_common = hm
105
+ # Send both pairs off for separate processing.
106
+ diffs_a = diff_main(text1_a, text2_a, options, checklines, deadline)
107
+ diffs_b = diff_main(text1_b, text2_b, options, checklines, deadline)
108
+ # Merge the results.
109
+ return diffs_a.concat([create_diff(DIFF_EQUAL, mid_common)], diffs_b)
110
+ end
111
+
112
+ return diff_line_mode(text1, text2, options, deadline) if checklines && text1.length > 100 && text2.length > 100
113
+
114
+ diff_bisect(text1, text2, options, deadline)
115
+ end
116
+
117
+ # Do a quick line-level diff on both strings, then re-diff the parts for
118
+ # greater accuracy. This speedup can produce non-minimal diffs.
119
+ def diff_line_mode(text1, text2, options, deadline)
120
+ # Scan the text on a line-by-line basis first.
121
+ a = diff_lines_to_chars(text1, text2)
122
+ text1 = a[:chars1]
123
+ text2 = a[:chars2]
124
+ linearray = a[:line_array]
125
+
126
+ diffs = diff_main(text1, text2, options, false, deadline)
127
+
128
+ # Convert the diff back to original text.
129
+ diff_chars_to_lines(diffs, linearray)
130
+ # Eliminate freak matches (e.g. blank lines)
131
+ diff_cleanup_semantic(diffs)
132
+
133
+ # Re-diff any replacement blocks, this time character-by-character.
134
+ # Add a dummy entry at the end.
135
+ diffs.push(create_diff(DIFF_EQUAL, ''))
136
+ pointer = 0
137
+ count_delete = 0
138
+ count_insert = 0
139
+ text_delete = +''
140
+ text_insert = +''
141
+ while pointer < diffs.length
142
+ case diffs[pointer][0]
143
+ when DIFF_INSERT
144
+ count_insert += 1
145
+ text_insert += diffs[pointer][1]
146
+ when DIFF_DELETE
147
+ count_delete += 1
148
+ text_delete += diffs[pointer][1]
149
+ when DIFF_EQUAL
150
+ # Upon reaching an equality, check for prior redundancies.
151
+ if count_delete >= 1 && count_insert >= 1
152
+ # Delete the offending records and add the merged ones.
153
+ diffs.slice!(pointer - count_delete - count_insert, count_delete + count_insert)
154
+ pointer = pointer - count_delete - count_insert
155
+ sub_diff = diff_main(text_delete, text_insert, options, false, deadline)
156
+ (sub_diff.length - 1).downto(0) do |j|
157
+ diffs.insert(pointer, sub_diff[j])
158
+ end
159
+ pointer += sub_diff.length
160
+ end
161
+ count_insert = 0
162
+ count_delete = 0
163
+ text_delete = +''
164
+ text_insert = +''
165
+ end
166
+ pointer += 1
167
+ end
168
+ diffs.pop # Remove the dummy entry at the end.
169
+
170
+ diffs
171
+ end
172
+
173
+ # Find the 'middle snake' of a diff, split the problem in two
174
+ # and return the recursively constructed diff.
175
+ # See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
176
+ def diff_bisect(text1, text2, options, deadline)
177
+ # Cache the text lengths to prevent multiple calls.
178
+ text1_length = text1.length
179
+ text2_length = text2.length
180
+ max_d = (text1_length + text2_length + 1) / 2
181
+ v_offset = max_d
182
+ v_length = 2 * max_d
183
+ v1 = Array.new(v_length, -1)
184
+ v2 = Array.new(v_length, -1)
185
+ v1[v_offset + 1] = 0
186
+ v2[v_offset + 1] = 0
187
+ delta = text1_length - text2_length
188
+ # If the total number of characters is odd, then the front path will collide
189
+ # with the reverse path.
190
+ front = delta.odd?
191
+ # Offsets for start and end of k loop.
192
+ # Prevents mapping of space beyond the grid.
193
+ k1start = 0
194
+ k1end = 0
195
+ k2start = 0
196
+ k2end = 0
197
+ d = 0
198
+ while d < max_d
199
+ # Bail out if deadline is reached.
200
+ break if now_ms > deadline
201
+
202
+ # Walk the front path one step.
203
+ k1 = -d + k1start
204
+ while k1 <= d - k1end
205
+ k1_offset = v_offset + k1
206
+ x1 = if k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])
207
+ v1[k1_offset + 1]
208
+ else
209
+ v1[k1_offset - 1] + 1
210
+ end
211
+ y1 = x1 - k1
212
+ while x1 < text1_length && y1 < text2_length && text1[x1] == text2[y1]
213
+ x1 += 1
214
+ y1 += 1
215
+ end
216
+ v1[k1_offset] = x1
217
+ if x1 > text1_length
218
+ # Ran off the right of the graph.
219
+ k1end += 2
220
+ elsif y1 > text2_length
221
+ # Ran off the bottom of the graph.
222
+ k1start += 2
223
+ elsif front
224
+ k2_offset = v_offset + delta - k1
225
+ if k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1
226
+ # Mirror x2 onto top-left coordinate system.
227
+ x2 = text1_length - v2[k2_offset]
228
+ if x1 >= x2
229
+ # Overlap detected.
230
+ return diff_bisect_split(text1, text2, options, x1, y1, deadline)
231
+ end
232
+ end
233
+ end
234
+ k1 += 2
235
+ end
236
+
237
+ # Walk the reverse path one step.
238
+ k2 = -d + k2start
239
+ while k2 <= d - k2end
240
+ k2_offset = v_offset + k2
241
+ x2 = if k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])
242
+ v2[k2_offset + 1]
243
+ else
244
+ v2[k2_offset - 1] + 1
245
+ end
246
+ y2 = x2 - k2
247
+ while x2 < text1_length && y2 < text2_length &&
248
+ text1[text1_length - x2 - 1] == text2[text2_length - y2 - 1]
249
+ x2 += 1
250
+ y2 += 1
251
+ end
252
+ v2[k2_offset] = x2
253
+ if x2 > text1_length
254
+ # Ran off the left of the graph.
255
+ k2end += 2
256
+ elsif y2 > text2_length
257
+ # Ran off the top of the graph.
258
+ k2start += 2
259
+ elsif !front
260
+ k1_offset = v_offset + delta - k2
261
+ if k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1
262
+ x1 = v1[k1_offset]
263
+ y1 = v_offset + x1 - k1_offset
264
+ # Mirror x2 onto top-left coordinate system.
265
+ x2 = text1_length - x2
266
+ if x1 >= x2
267
+ # Overlap detected.
268
+ return diff_bisect_split(text1, text2, options, x1, y1, deadline)
269
+ end
270
+ end
271
+ end
272
+ k2 += 2
273
+ end
274
+ d += 1
275
+ end
276
+ # Diff took too long and hit the deadline or
277
+ # number of diffs equals number of characters, no commonality at all.
278
+ [create_diff(DIFF_DELETE, text1), create_diff(DIFF_INSERT, text2)]
279
+ end
280
+
281
+ # Given the location of the 'middle snake', split the diff in two parts
282
+ # and recurse.
283
+ def diff_bisect_split(text1, text2, options, x, y, deadline)
284
+ text1a = JsString.substring(text1, 0, x)
285
+ text2a = JsString.substring(text2, 0, y)
286
+ text1b = JsString.substring(text1, x)
287
+ text2b = JsString.substring(text2, y)
288
+
289
+ # Compute both diffs serially.
290
+ diffs = diff_main(text1a, text2a, options, false, deadline)
291
+ diffsb = diff_main(text1b, text2b, options, false, deadline)
292
+
293
+ diffs.concat(diffsb)
294
+ end
295
+
296
+ def line_char_for(index)
297
+ codepoint = index < 0xD800 ? index : index + LINE_INDEX_SURROGATE_SHIFT
298
+ codepoint.chr(Encoding::UTF_8)
299
+ end
300
+
301
+ def line_index_for(char)
302
+ codepoint = char.ord
303
+ codepoint < 0xD800 ? codepoint : codepoint - LINE_INDEX_SURROGATE_SHIFT
304
+ end
305
+
306
+ # Split two texts into an array of strings. Reduce the texts to a string of
307
+ # placeholder characters where each character represents one line.
308
+ def diff_lines_to_chars(text1, text2)
309
+ line_array = [] # e.g. line_array[4] == "Hello\n"
310
+ line_hash = {} # e.g. line_hash["Hello\n"] == 4
311
+
312
+ # Allocate 2/3rds of the space for text1, the rest for text2.
313
+ max_lines = 40_000
314
+
315
+ # '\x00' is a valid character, but various debuggers don't like it.
316
+ # So we'll insert a junk entry to avoid generating a null character.
317
+ line_array[0] = ''
318
+
319
+ munge = lambda do |text|
320
+ chars = +''
321
+ # Walk the text, pulling out a substring for each line.
322
+ line_start = 0
323
+ line_end = -1
324
+ line_array_length = line_array.length
325
+ while line_end < text.length - 1
326
+ line_end = text.index("\n", line_start) || -1
327
+ line_end = text.length - 1 if line_end == -1
328
+
329
+ line = JsString.substring(text, line_start, line_end + 1)
330
+
331
+ if line_hash.key?(line)
332
+ chars << line_char_for(line_hash[line])
333
+ else
334
+ if line_array_length == max_lines
335
+ # Bail out at 65535 because
336
+ # String.fromCharCode(65536) == String.fromCharCode(0)
337
+ line = JsString.substring(text, line_start)
338
+ line_end = text.length
339
+ end
340
+ chars << line_char_for(line_array_length)
341
+ line_hash[line] = line_array_length
342
+ line_array[line_array_length] = line
343
+ line_array_length += 1
344
+ end
345
+ line_start = line_end + 1
346
+ end
347
+ chars
348
+ end
349
+
350
+ chars1 = munge.call(text1)
351
+ max_lines = 65_535
352
+ chars2 = munge.call(text2)
353
+ { chars1: chars1, chars2: chars2, line_array: line_array }
354
+ end
355
+
356
+ # Rehydrate the text in a diff from a string of line placeholders to real
357
+ # lines of text.
358
+ def diff_chars_to_lines(diffs, line_array)
359
+ diffs.each do |diff|
360
+ chars = diff[1]
361
+ text = +''
362
+ chars.each_char { |ch| text << line_array[line_index_for(ch)] }
363
+ diff[1] = text
364
+ end
365
+ nil
366
+ end
367
+
368
+ # Determine the common prefix of two strings.
369
+ def diff_common_prefix(text1, text2)
370
+ # Quick check for common null cases.
371
+ return 0 if text1.empty? || text2.empty? || text1[0] != text2[0]
372
+
373
+ # Binary search.
374
+ # Performance analysis: https://neil.fraser.name/news/2007/10/09/
375
+ pointermin = 0
376
+ pointermax = [text1.length, text2.length].min
377
+ pointermid = pointermax
378
+ pointerstart = 0
379
+ while pointermin < pointermid
380
+ if JsString.substring(text1, pointerstart, pointermid) ==
381
+ JsString.substring(text2, pointerstart, pointermid)
382
+ pointermin = pointermid
383
+ pointerstart = pointermin
384
+ else
385
+ pointermax = pointermid
386
+ end
387
+ pointermid = (pointermax - pointermin) / 2 + pointermin
388
+ end
389
+ pointermid
390
+ end
391
+
392
+ # Determine the common suffix of two strings.
393
+ def diff_common_suffix(text1, text2)
394
+ # Quick check for common null cases.
395
+ return 0 if text1.empty? || text2.empty? || text1[-1] != text2[-1]
396
+
397
+ # Binary search.
398
+ # Performance analysis: https://neil.fraser.name/news/2007/10/09/
399
+ pointermin = 0
400
+ pointermax = [text1.length, text2.length].min
401
+ pointermid = pointermax
402
+ pointerend = 0
403
+ while pointermin < pointermid
404
+ if JsString.substring(text1, text1.length - pointermid, text1.length - pointerend) ==
405
+ JsString.substring(text2, text2.length - pointermid, text2.length - pointerend)
406
+ pointermin = pointermid
407
+ pointerend = pointermin
408
+ else
409
+ pointermax = pointermid
410
+ end
411
+ pointermid = (pointermax - pointermin) / 2 + pointermin
412
+ end
413
+ pointermid
414
+ end
415
+
416
+ # Determine if the suffix of one string is the prefix of another.
417
+ def diff_common_overlap(text1, text2)
418
+ # Cache the text lengths to prevent multiple calls.
419
+ text1_length = text1.length
420
+ text2_length = text2.length
421
+ # Eliminate the null case.
422
+ return 0 if text1_length.zero? || text2_length.zero?
423
+
424
+ # Truncate the longer string.
425
+ if text1_length > text2_length
426
+ text1 = JsString.substring(text1, text1_length - text2_length)
427
+ elsif text1_length < text2_length
428
+ text2 = JsString.substring(text2, 0, text1_length)
429
+ end
430
+
431
+ text_length = [text1_length, text2_length].min
432
+ # Quick check for the worst case.
433
+ return text_length if text1 == text2
434
+
435
+ # Start by looking for a single character match
436
+ # and increase length until no match is found.
437
+ # Performance analysis: https://neil.fraser.name/news/2010/11/04/
438
+ best = 0
439
+ length = 1
440
+ loop do
441
+ pattern = JsString.substring(text1, text_length - length)
442
+ found = JsString.index_of(text2, pattern)
443
+ return best if found == -1
444
+
445
+ length += found
446
+ if found.zero? || JsString.substring(text1, text_length - length) == JsString.substring(text2, 0, length)
447
+ best = length
448
+ length += 1
449
+ end
450
+ end
451
+ end
452
+
453
+ # Do the two texts share a substring which is at least half the length of
454
+ # the longer text? This speedup can produce non-minimal diffs.
455
+ def diff_half_match(text1, text2, options)
456
+ if options.diff_timeout <= 0
457
+ # Don't risk returning a non-optimal diff if we have unlimited time.
458
+ return nil
459
+ end
460
+ longtext = text1.length > text2.length ? text1 : text2
461
+ shorttext = text1.length > text2.length ? text2 : text1
462
+ return nil if longtext.length < 4 || shorttext.length * 2 < longtext.length # Pointless.
463
+
464
+ # First check if the second quarter is the seed for a half-match.
465
+ hm1 = diff_half_match_i(longtext, shorttext, (longtext.length + 3) / 4)
466
+ # Check again based on the third quarter.
467
+ hm2 = diff_half_match_i(longtext, shorttext, (longtext.length + 1) / 2)
468
+
469
+ if !hm1 && !hm2
470
+ return nil
471
+ elsif !hm2
472
+ hm = hm1
473
+ elsif !hm1
474
+ hm = hm2
475
+ else
476
+ # Both matched. Select the longest.
477
+ hm = hm1[4].length > hm2[4].length ? hm1 : hm2
478
+ end
479
+
480
+ # A half-match was found, sort out the return data.
481
+ if text1.length > text2.length
482
+ text1_a, text1_b, text2_a, text2_b = hm
483
+ else
484
+ text2_a, text2_b, text1_a, text1_b = hm
485
+ end
486
+ mid_common = hm[4]
487
+ [text1_a, text1_b, text2_a, text2_b, mid_common]
488
+ end
489
+
490
+ # Does a substring of shorttext exist within longtext such that the
491
+ # substring is at least half the length of longtext?
492
+ def diff_half_match_i(longtext, shorttext, i)
493
+ # Start with a 1/4 length substring at position i as a seed.
494
+ seed = JsString.substring(longtext, i, i + (longtext.length / 4))
495
+ j = -1
496
+ best_common = ''
497
+ best_longtext_a = best_longtext_b = best_shorttext_a = best_shorttext_b = nil
498
+ while (j = JsString.index_of(shorttext, seed, j + 1)) != -1
499
+ prefix_length = diff_common_prefix(JsString.substring(longtext, i), JsString.substring(shorttext, j))
500
+ suffix_length = diff_common_suffix(JsString.substring(longtext, 0, i), JsString.substring(shorttext, 0, j))
501
+ next unless best_common.length < suffix_length + prefix_length
502
+
503
+ best_common = JsString.substring(shorttext, j - suffix_length, j) +
504
+ JsString.substring(shorttext, j, j + prefix_length)
505
+ best_longtext_a = JsString.substring(longtext, 0, i - suffix_length)
506
+ best_longtext_b = JsString.substring(longtext, i + prefix_length)
507
+ best_shorttext_a = JsString.substring(shorttext, 0, j - suffix_length)
508
+ best_shorttext_b = JsString.substring(shorttext, j + prefix_length)
509
+ end
510
+ if best_common.length * 2 >= longtext.length
511
+ [best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common]
512
+ end
513
+ end
514
+
515
+ # Reduce the number of edits by eliminating semantically trivial equalities.
516
+ def diff_cleanup_semantic(diffs)
517
+ changes = false
518
+ # Stack of indices where equalities are found. The reference
519
+ # implementation lets the stack pointer go negative and stores into the
520
+ # JS array at index -1 (a plain property write), so a Hash reproduces
521
+ # those semantics.
522
+ equalities = {}
523
+ equalities_length = 0
524
+ last_equality = nil
525
+ # Always equal to diffs[equalities[equalities_length - 1]][1]
526
+ pointer = 0 # Index of current position.
527
+
528
+ # Number of characters that changed prior to the equality.
529
+ length_insertions1 = 0
530
+ length_deletions1 = 0
531
+ # Number of characters that changed after the equality.
532
+ length_insertions2 = 0
533
+ length_deletions2 = 0
534
+ while pointer < diffs.length
535
+ if diffs[pointer][0] == DIFF_EQUAL # Equality found.
536
+ equalities[equalities_length] = pointer
537
+ equalities_length += 1
538
+ length_insertions1 = length_insertions2
539
+ length_deletions1 = length_deletions2
540
+ length_insertions2 = 0
541
+ length_deletions2 = 0
542
+ last_equality = diffs[pointer][1]
543
+ else # An insertion or deletion.
544
+ if diffs[pointer][0] == DIFF_INSERT
545
+ length_insertions2 += diffs[pointer][1].length
546
+ else
547
+ length_deletions2 += diffs[pointer][1].length
548
+ end
549
+
550
+ # Eliminate an equality that is smaller or equal to the edits on both
551
+ # sides of it.
552
+ if last_equality &&
553
+ last_equality.length <= [length_insertions1, length_deletions1].max &&
554
+ last_equality.length <= [length_insertions2, length_deletions2].max
555
+ # Duplicate record.
556
+ diffs.insert(equalities[equalities_length - 1], create_diff(DIFF_DELETE, last_equality))
557
+ # Change second copy to insert.
558
+ diffs[equalities[equalities_length - 1] + 1][0] = DIFF_INSERT
559
+ # Throw away the equality we just deleted.
560
+ equalities_length -= 1
561
+ # Throw away the previous equality (it needs to be reevaluated).
562
+ equalities_length -= 1
563
+ pointer = equalities_length.positive? ? equalities[equalities_length - 1] : -1
564
+ length_insertions1 = 0 # Reset the counters.
565
+ length_deletions1 = 0
566
+ length_insertions2 = 0
567
+ length_deletions2 = 0
568
+ last_equality = nil
569
+ changes = true
570
+ end
571
+ end
572
+ pointer += 1
573
+ end
574
+
575
+ # Normalize the diff.
576
+ diff_cleanup_merge(diffs) if changes
577
+
578
+ diff_cleanup_semantic_lossless(diffs)
579
+
580
+ # Find any overlaps between deletions and insertions.
581
+ # e.g: <del>abcxxx</del><ins>xxxdef</ins>
582
+ # -> <del>abc</del>xxx<ins>def</ins>
583
+ # e.g: <del>xxxabc</del><ins>defxxx</ins>
584
+ # -> <ins>def</ins>xxx<del>abc</del>
585
+ # Only extract an overlap if it is as big as the edit ahead or behind it.
586
+ pointer = 1
587
+ while pointer < diffs.length
588
+ if diffs[pointer - 1][0] == DIFF_DELETE && diffs[pointer][0] == DIFF_INSERT
589
+ deletion = diffs[pointer - 1][1]
590
+ insertion = diffs[pointer][1]
591
+ overlap_length1 = diff_common_overlap(deletion, insertion)
592
+ overlap_length2 = diff_common_overlap(insertion, deletion)
593
+ if overlap_length1 >= overlap_length2
594
+ if overlap_length1 >= deletion.length / 2.0 || overlap_length1 >= insertion.length / 2.0
595
+ # Overlap found. Insert an equality and trim the surrounding edits.
596
+ diffs.insert(pointer, create_diff(DIFF_EQUAL, JsString.substring(insertion, 0, overlap_length1)))
597
+ diffs[pointer - 1][1] = JsString.substring(deletion, 0, deletion.length - overlap_length1)
598
+ diffs[pointer + 1][1] = JsString.substring(insertion, overlap_length1)
599
+ pointer += 1
600
+ end
601
+ elsif overlap_length2 >= deletion.length / 2.0 || overlap_length2 >= insertion.length / 2.0
602
+ # Reverse overlap found.
603
+ # Insert an equality and swap and trim the surrounding edits.
604
+ diffs.insert(pointer, create_diff(DIFF_EQUAL, JsString.substring(deletion, 0, overlap_length2)))
605
+ diffs[pointer - 1][0] = DIFF_INSERT
606
+ diffs[pointer - 1][1] = JsString.substring(insertion, 0, insertion.length - overlap_length2)
607
+ diffs[pointer + 1][0] = DIFF_DELETE
608
+ diffs[pointer + 1][1] = JsString.substring(deletion, overlap_length2)
609
+ pointer += 1
610
+ end
611
+ pointer += 1
612
+ end
613
+ pointer += 1
614
+ end
615
+ nil
616
+ end
617
+
618
+ NON_ALPHA_NUMERIC_REGEX = /[^a-z0-9]/i
619
+ # Matches the JS \s class (which the reference implementation uses).
620
+ WHITESPACE_REGEX = /[\t\n\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/
621
+ LINEBREAK_REGEX = /[\r\n]/
622
+ BLANKLINE_END_REGEX = /\n\r?\n\z/
623
+ BLANKLINE_START_REGEX = /\A\r?\n\r?\n/
624
+
625
+ # Given two strings, compute a score representing whether the internal
626
+ # boundary falls on logical boundaries.
627
+ # Scores range from 6 (best) to 0 (worst).
628
+ def diff_cleanup_semantic_score(one, two)
629
+ if one.empty? || two.empty?
630
+ # Edges are the best.
631
+ return 6
632
+ end
633
+
634
+ char1 = one[-1]
635
+ char2 = two[0]
636
+ non_alpha_numeric1 = NON_ALPHA_NUMERIC_REGEX.match?(char1)
637
+ non_alpha_numeric2 = NON_ALPHA_NUMERIC_REGEX.match?(char2)
638
+ whitespace1 = non_alpha_numeric1 && WHITESPACE_REGEX.match?(char1)
639
+ whitespace2 = non_alpha_numeric2 && WHITESPACE_REGEX.match?(char2)
640
+ line_break1 = whitespace1 && LINEBREAK_REGEX.match?(char1)
641
+ line_break2 = whitespace2 && LINEBREAK_REGEX.match?(char2)
642
+ blank_line1 = line_break1 && BLANKLINE_END_REGEX.match?(one)
643
+ blank_line2 = line_break2 && BLANKLINE_START_REGEX.match?(two)
644
+
645
+ if blank_line1 || blank_line2
646
+ # Five points for blank lines.
647
+ 5
648
+ elsif line_break1 || line_break2
649
+ # Four points for line breaks.
650
+ 4
651
+ elsif non_alpha_numeric1 && !whitespace1 && whitespace2
652
+ # Three points for end of sentences.
653
+ 3
654
+ elsif whitespace1 || whitespace2
655
+ # Two points for whitespace.
656
+ 2
657
+ elsif non_alpha_numeric1 || non_alpha_numeric2
658
+ # One point for non-alphanumeric.
659
+ 1
660
+ else
661
+ 0
662
+ end
663
+ end
664
+
665
+ # Look for single edits surrounded on both sides by equalities
666
+ # which can be shifted sideways to align the edit to a word boundary.
667
+ # e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
668
+ def diff_cleanup_semantic_lossless(diffs)
669
+ pointer = 1
670
+ # Intentionally ignore the first and last element (don't need checking).
671
+ while pointer < diffs.length - 1
672
+ if diffs[pointer - 1][0] == DIFF_EQUAL && diffs[pointer + 1][0] == DIFF_EQUAL
673
+ # This is a single edit surrounded by equalities.
674
+ equality1 = diffs[pointer - 1][1]
675
+ edit = diffs[pointer][1]
676
+ equality2 = diffs[pointer + 1][1]
677
+
678
+ # First, shift the edit as far left as possible.
679
+ common_offset = diff_common_suffix(equality1, edit)
680
+ if common_offset.positive?
681
+ common_string = JsString.substring(edit, edit.length - common_offset)
682
+ equality1 = JsString.substring(equality1, 0, equality1.length - common_offset)
683
+ edit = common_string + JsString.substring(edit, 0, edit.length - common_offset)
684
+ equality2 = common_string + equality2
685
+ end
686
+
687
+ # Second, step character by character right, looking for the best fit.
688
+ best_equality1 = equality1
689
+ best_edit = edit
690
+ best_equality2 = equality2
691
+ best_score = diff_cleanup_semantic_score(equality1, edit) +
692
+ diff_cleanup_semantic_score(edit, equality2)
693
+ while !edit.empty? && !equality2.empty? && edit[0] == equality2[0]
694
+ equality1 += edit[0]
695
+ edit = JsString.substring(edit, 1) + equality2[0]
696
+ equality2 = JsString.substring(equality2, 1)
697
+ score = diff_cleanup_semantic_score(equality1, edit) +
698
+ diff_cleanup_semantic_score(edit, equality2)
699
+ # The >= encourages trailing rather than leading whitespace on edits.
700
+ next unless score >= best_score
701
+
702
+ best_score = score
703
+ best_equality1 = equality1
704
+ best_edit = edit
705
+ best_equality2 = equality2
706
+ end
707
+
708
+ if diffs[pointer - 1][1] != best_equality1
709
+ # We have an improvement, save it back to the diff.
710
+ if best_equality1.empty?
711
+ diffs.delete_at(pointer - 1)
712
+ pointer -= 1
713
+ else
714
+ diffs[pointer - 1][1] = best_equality1
715
+ end
716
+ diffs[pointer][1] = best_edit
717
+ if best_equality2.empty?
718
+ diffs.delete_at(pointer + 1)
719
+ pointer -= 1
720
+ else
721
+ diffs[pointer + 1][1] = best_equality2
722
+ end
723
+ end
724
+ end
725
+ pointer += 1
726
+ end
727
+ nil
728
+ end
729
+
730
+ # Reduce the number of edits by eliminating operationally trivial equalities.
731
+ def diff_cleanup_efficiency(diffs, options = nil)
732
+ diff_edit_cost = resolve_options(options).diff_edit_cost
733
+
734
+ changes = false
735
+ # Stack of indices where equalities are found (Hash for the same
736
+ # negative-index reason as in diff_cleanup_semantic).
737
+ equalities = {}
738
+ equalities_length = 0
739
+ last_equality = nil
740
+ # Always equal to diffs[equalities[equalities_length - 1]][1]
741
+ pointer = 0 # Index of current position.
742
+
743
+ # Is there an insertion operation before the last equality.
744
+ pre_ins = false
745
+ # Is there a deletion operation before the last equality.
746
+ pre_del = false
747
+ # Is there an insertion operation after the last equality.
748
+ post_ins = false
749
+ # Is there a deletion operation after the last equality.
750
+ post_del = false
751
+ while pointer < diffs.length
752
+ if diffs[pointer][0] == DIFF_EQUAL # Equality found.
753
+ if diffs[pointer][1].length < diff_edit_cost && (post_ins || post_del)
754
+ # Candidate found.
755
+ equalities[equalities_length] = pointer
756
+ equalities_length += 1
757
+ pre_ins = post_ins
758
+ pre_del = post_del
759
+ last_equality = diffs[pointer][1]
760
+ else
761
+ # Not a candidate, and can never become one.
762
+ equalities_length = 0
763
+ last_equality = nil
764
+ end
765
+ post_ins = post_del = false
766
+ else # An insertion or deletion.
767
+ if diffs[pointer][0] == DIFF_DELETE
768
+ post_del = true
769
+ else
770
+ post_ins = true
771
+ end
772
+
773
+ # Five types to be split:
774
+ # <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
775
+ # <ins>A</ins>X<ins>C</ins><del>D</del>
776
+ # <ins>A</ins><del>B</del>X<ins>C</ins>
777
+ # <ins>A</del>X<ins>C</ins><del>D</del>
778
+ # <ins>A</ins><del>B</del>X<del>C</del>
779
+ if last_equality && ((pre_ins && pre_del && post_ins && post_del) ||
780
+ ((last_equality.length < diff_edit_cost / 2.0) &&
781
+ [pre_ins, pre_del, post_ins, post_del].count(true) == 3))
782
+ # Duplicate record.
783
+ diffs.insert(equalities[equalities_length - 1], create_diff(DIFF_DELETE, last_equality))
784
+ # Change second copy to insert.
785
+ diffs[equalities[equalities_length - 1] + 1][0] = DIFF_INSERT
786
+ equalities_length -= 1 # Throw away the equality we just deleted;
787
+ last_equality = nil
788
+ if pre_ins && pre_del
789
+ # No changes made which could affect previous entry, keep going.
790
+ post_ins = post_del = true
791
+ equalities_length = 0
792
+ else
793
+ equalities_length -= 1 # Throw away the previous equality.
794
+ pointer = equalities_length.positive? ? equalities[equalities_length - 1] : -1
795
+ post_ins = post_del = false
796
+ end
797
+ changes = true
798
+ end
799
+ end
800
+ pointer += 1
801
+ end
802
+
803
+ diff_cleanup_merge(diffs) if changes
804
+ nil
805
+ end
806
+
807
+ # Reorder and merge like edit sections. Merge equalities.
808
+ # Any edit section can move as long as it doesn't cross an equality.
809
+ def diff_cleanup_merge(diffs)
810
+ # Add a dummy entry at the end.
811
+ diffs.push(create_diff(DIFF_EQUAL, ''))
812
+ pointer = 0
813
+ count_delete = 0
814
+ count_insert = 0
815
+ text_delete = +''
816
+ text_insert = +''
817
+ while pointer < diffs.length
818
+ case diffs[pointer][0]
819
+ when DIFF_INSERT
820
+ count_insert += 1
821
+ text_insert += diffs[pointer][1]
822
+ pointer += 1
823
+ when DIFF_DELETE
824
+ count_delete += 1
825
+ text_delete += diffs[pointer][1]
826
+ pointer += 1
827
+ when DIFF_EQUAL
828
+ # Upon reaching an equality, check for prior redundancies.
829
+ if count_delete + count_insert > 1
830
+ if count_delete != 0 && count_insert != 0
831
+ # Factor out any common prefixes.
832
+ commonlength = diff_common_prefix(text_insert, text_delete)
833
+ if commonlength != 0
834
+ if (pointer - count_delete - count_insert).positive? &&
835
+ diffs[pointer - count_delete - count_insert - 1][0] == DIFF_EQUAL
836
+ diffs[pointer - count_delete - count_insert - 1][1] += JsString.substring(text_insert, 0, commonlength)
837
+ else
838
+ diffs.insert(0, create_diff(DIFF_EQUAL, JsString.substring(text_insert, 0, commonlength)))
839
+ pointer += 1
840
+ end
841
+ text_insert = JsString.substring(text_insert, commonlength)
842
+ text_delete = JsString.substring(text_delete, commonlength)
843
+ end
844
+ # Factor out any common suffixes.
845
+ commonlength = diff_common_suffix(text_insert, text_delete)
846
+ if commonlength != 0
847
+ diffs[pointer][1] = JsString.substring(text_insert, text_insert.length - commonlength) + diffs[pointer][1]
848
+ text_insert = JsString.substring(text_insert, 0, text_insert.length - commonlength)
849
+ text_delete = JsString.substring(text_delete, 0, text_delete.length - commonlength)
850
+ end
851
+ end
852
+ # Delete the offending records and add the merged ones.
853
+ pointer -= count_delete + count_insert
854
+ diffs.slice!(pointer, count_delete + count_insert)
855
+ unless text_delete.empty?
856
+ diffs.insert(pointer, create_diff(DIFF_DELETE, text_delete))
857
+ pointer += 1
858
+ end
859
+ unless text_insert.empty?
860
+ diffs.insert(pointer, create_diff(DIFF_INSERT, text_insert))
861
+ pointer += 1
862
+ end
863
+ pointer += 1
864
+ elsif pointer != 0 && diffs[pointer - 1][0] == DIFF_EQUAL
865
+ # Merge this equality with the previous one.
866
+ diffs[pointer - 1][1] += diffs[pointer][1]
867
+ diffs.delete_at(pointer)
868
+ else
869
+ pointer += 1
870
+ end
871
+ count_insert = 0
872
+ count_delete = 0
873
+ text_delete = +''
874
+ text_insert = +''
875
+ end
876
+ end
877
+ diffs.pop if diffs[diffs.length - 1][1] == '' # Remove the dummy entry at the end.
878
+
879
+ # Second pass: look for single edits surrounded on both sides by equalities
880
+ # which can be shifted sideways to eliminate an equality.
881
+ # e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
882
+ changes = false
883
+ pointer = 1
884
+ # Intentionally ignore the first and last element (don't need checking).
885
+ while pointer < diffs.length - 1
886
+ if diffs[pointer - 1][0] == DIFF_EQUAL && diffs[pointer + 1][0] == DIFF_EQUAL
887
+ # This is a single edit surrounded by equalities.
888
+ if JsString.substring(diffs[pointer][1], diffs[pointer][1].length - diffs[pointer - 1][1].length) ==
889
+ diffs[pointer - 1][1]
890
+ # Shift the edit over the previous equality.
891
+ diffs[pointer][1] = diffs[pointer - 1][1] +
892
+ JsString.substring(diffs[pointer][1], 0,
893
+ diffs[pointer][1].length - diffs[pointer - 1][1].length)
894
+ diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]
895
+ diffs.delete_at(pointer - 1)
896
+ changes = true
897
+ elsif JsString.substring(diffs[pointer][1], 0, diffs[pointer + 1][1].length) == diffs[pointer + 1][1]
898
+ # Shift the edit over the next equality.
899
+ diffs[pointer - 1][1] += diffs[pointer + 1][1]
900
+ diffs[pointer][1] = JsString.substring(diffs[pointer][1], diffs[pointer + 1][1].length) +
901
+ diffs[pointer + 1][1]
902
+ diffs.delete_at(pointer + 1)
903
+ changes = true
904
+ end
905
+ end
906
+ pointer += 1
907
+ end
908
+ # If shifts were made, the diff needs reordering and another shift sweep.
909
+ diff_cleanup_merge(diffs) if changes
910
+ nil
911
+ end
912
+
913
+ # loc is a location in text1, compute and return the equivalent location in
914
+ # text2. e.g. 'The cat' vs 'The big cat', 1->1, 5->8
915
+ def diff_x_index(diffs, loc)
916
+ chars1 = 0
917
+ chars2 = 0
918
+ last_chars1 = 0
919
+ last_chars2 = 0
920
+ x = 0
921
+ while x < diffs.length
922
+ if diffs[x][0] != DIFF_INSERT # Equality or deletion.
923
+ chars1 += diffs[x][1].length
924
+ end
925
+ if diffs[x][0] != DIFF_DELETE # Equality or insertion.
926
+ chars2 += diffs[x][1].length
927
+ end
928
+ break if chars1 > loc # Overshot the location.
929
+
930
+ last_chars1 = chars1
931
+ last_chars2 = chars2
932
+ x += 1
933
+ end
934
+ # Was the location deleted?
935
+ return last_chars2 if diffs.length != x && diffs[x][0] == DIFF_DELETE
936
+
937
+ # Add the remaining character length.
938
+ last_chars2 + (loc - last_chars1)
939
+ end
940
+
941
+ # Convert a diff array into a pretty HTML report.
942
+ def diff_pretty_html(diffs)
943
+ html = +''
944
+ diffs.each do |(op, data)|
945
+ text = data.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub("\n", '&para;<br>')
946
+ case op
947
+ when DIFF_INSERT
948
+ html << %(<ins style="background:#e6ffe6;">#{text}</ins>)
949
+ when DIFF_DELETE
950
+ html << %(<del style="background:#ffe6e6;">#{text}</del>)
951
+ when DIFF_EQUAL
952
+ html << "<span>#{text}</span>"
953
+ end
954
+ end
955
+ html
956
+ end
957
+
958
+ # Compute and return the source text (all equalities and deletions).
959
+ def diff_text1(diffs)
960
+ text = +''
961
+ diffs.each { |(op, data)| text << data if op != DIFF_INSERT }
962
+ text
963
+ end
964
+
965
+ # Compute and return the destination text (all equalities and insertions).
966
+ def diff_text2(diffs)
967
+ text = +''
968
+ diffs.each { |(op, data)| text << data if op != DIFF_DELETE }
969
+ text
970
+ end
971
+
972
+ # Compute the Levenshtein distance; the number of inserted, deleted or
973
+ # substituted characters.
974
+ def diff_levenshtein(diffs)
975
+ levenshtein = 0
976
+ insertions = 0
977
+ deletions = 0
978
+ diffs.each do |(op, data)|
979
+ case op
980
+ when DIFF_INSERT
981
+ insertions += data.length
982
+ when DIFF_DELETE
983
+ deletions += data.length
984
+ when DIFF_EQUAL
985
+ # A deletion and an insertion is one substitution.
986
+ levenshtein += [insertions, deletions].max
987
+ insertions = 0
988
+ deletions = 0
989
+ end
990
+ end
991
+ levenshtein + [insertions, deletions].max
992
+ end
993
+
994
+ # Crush the diff into an encoded string which describes the operations
995
+ # required to transform text1 into text2.
996
+ # E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
997
+ # Operations are tab-separated. Inserted text is escaped using %xx notation.
998
+ def diff_to_delta(diffs)
999
+ text = diffs.map do |(op, data)|
1000
+ case op
1001
+ when DIFF_INSERT then "+#{JsString.encode_uri(data)}"
1002
+ when DIFF_DELETE then "-#{data.length}"
1003
+ when DIFF_EQUAL then "=#{data.length}"
1004
+ end
1005
+ end
1006
+ text.join("\t").gsub('%20', ' ')
1007
+ end
1008
+
1009
+ # Given the original text1, and an encoded string which describes the
1010
+ # operations required to transform text1 into text2, compute the full diff.
1011
+ def diff_from_delta(text1, delta)
1012
+ diffs = []
1013
+ pointer = 0 # Cursor in text1
1014
+ tokens = delta.split("\t", -1)
1015
+ tokens.each do |token|
1016
+ # Each token begins with a one character parameter which specifies the
1017
+ # operation of this token (delete, insert, equality).
1018
+ param = JsString.substring(token, 1)
1019
+ case JsString.char_at(token, 0)
1020
+ when '+'
1021
+ begin
1022
+ diffs << create_diff(DIFF_INSERT, JsString.decode_uri(param))
1023
+ rescue JsString::UriError
1024
+ # Malformed URI sequence.
1025
+ raise Error, "Illegal escape in diff_fromDelta: #{param}"
1026
+ end
1027
+ when '-', '='
1028
+ n = JsString.parse_int(param)
1029
+ raise Error, "Invalid number in diff_fromDelta: #{param}" if n.nil? || n.negative?
1030
+
1031
+ text = JsString.substring(text1, pointer, pointer + n)
1032
+ pointer += n
1033
+ if token[0] == '='
1034
+ diffs << create_diff(DIFF_EQUAL, text)
1035
+ else
1036
+ diffs << create_diff(DIFF_DELETE, text)
1037
+ end
1038
+ else
1039
+ # Blank tokens are ok (from a trailing \t).
1040
+ # Anything else is an error.
1041
+ raise Error, "Invalid diff operation in diff_fromDelta: #{token}" unless token.empty?
1042
+ end
1043
+ end
1044
+ if pointer != text1.length
1045
+ raise Error, "Delta length (#{pointer}) does not equal source text length (#{text1.length})."
1046
+ end
1047
+
1048
+ diffs
1049
+ end
1050
+ end