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.
- checksums.yaml +7 -0
- data/README.md +87 -0
- data/lib/diff_match_patch_es/diff.rb +1050 -0
- data/lib/diff_match_patch_es/js_string.rb +128 -0
- data/lib/diff_match_patch_es/match.rb +141 -0
- data/lib/diff_match_patch_es/options.rb +58 -0
- data/lib/diff_match_patch_es/patch.rb +551 -0
- data/lib/diff_match_patch_es/version.rb +5 -0
- data/lib/diff_match_patch_es.rb +51 -0
- data/sig/diff_match_patch_es/diff.rbs +113 -0
- data/sig/diff_match_patch_es/js_string.rbs +37 -0
- data/sig/diff_match_patch_es/match.rbs +14 -0
- data/sig/diff_match_patch_es/options.rbs +38 -0
- data/sig/diff_match_patch_es/patch.rbs +60 -0
- data/sig/diff_match_patch_es/version.rbs +3 -0
- data/sig/diff_match_patch_es.rbs +34 -0
- metadata +57 -0
|
@@ -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('&', '&').gsub('<', '<').gsub('>', '>').gsub("\n", '¶<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
|