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,551 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiffMatchPatchES
|
|
4
|
+
# One patch operation.
|
|
5
|
+
class Patch
|
|
6
|
+
attr_accessor :diffs, :start1, :start2, :length1, :length2
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@diffs = []
|
|
10
|
+
@start1 = nil
|
|
11
|
+
@start2 = nil
|
|
12
|
+
@length1 = 0
|
|
13
|
+
@length2 = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Emulate GNU diff's format.
|
|
17
|
+
# Header: @@ -382,8 +481,9 @@
|
|
18
|
+
# Indices are printed as 1-based, not 0-based.
|
|
19
|
+
def to_s
|
|
20
|
+
coords1 = if length1.zero?
|
|
21
|
+
"#{start1},0"
|
|
22
|
+
elsif length1 == 1
|
|
23
|
+
(start1 + 1).to_s
|
|
24
|
+
else
|
|
25
|
+
"#{start1 + 1},#{length1}"
|
|
26
|
+
end
|
|
27
|
+
coords2 = if length2.zero?
|
|
28
|
+
"#{start2},0"
|
|
29
|
+
elsif length2 == 1
|
|
30
|
+
(start2 + 1).to_s
|
|
31
|
+
else
|
|
32
|
+
"#{start2 + 1},#{length2}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
text = +"@@ -#{coords1} +#{coords2} @@\n"
|
|
36
|
+
# Escape the body of the patch with %xx notation.
|
|
37
|
+
diffs.each do |(op, data)|
|
|
38
|
+
sign = case op
|
|
39
|
+
when DIFF_INSERT then '+'
|
|
40
|
+
when DIFF_DELETE then '-'
|
|
41
|
+
when DIFF_EQUAL then ' '
|
|
42
|
+
end
|
|
43
|
+
text << sign << JsString.encode_uri(data) << "\n"
|
|
44
|
+
end
|
|
45
|
+
text.gsub('%20', ' ')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ==(other)
|
|
49
|
+
other.is_a?(Patch) &&
|
|
50
|
+
diffs == other.diffs &&
|
|
51
|
+
start1 == other.start1 &&
|
|
52
|
+
start2 == other.start2 &&
|
|
53
|
+
length1 == other.length1 &&
|
|
54
|
+
length2 == other.length2
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
def create_patch
|
|
61
|
+
Patch.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Increase the context until it is unique,
|
|
65
|
+
# but don't let the pattern expand beyond match_max_bits.
|
|
66
|
+
def patch_add_context(patch, text, options)
|
|
67
|
+
return if text.empty?
|
|
68
|
+
|
|
69
|
+
raise Error, 'patch not initialized' if patch.start2.nil?
|
|
70
|
+
|
|
71
|
+
resolved = resolve_options(options)
|
|
72
|
+
match_max_bits = resolved.match_max_bits
|
|
73
|
+
patch_margin = resolved.patch_margin
|
|
74
|
+
|
|
75
|
+
pattern = JsString.substring(text, patch.start2, patch.start2 + patch.length1)
|
|
76
|
+
padding = 0
|
|
77
|
+
|
|
78
|
+
# Look for the first and last matches of pattern in text. If two
|
|
79
|
+
# different matches are found, increase the pattern length.
|
|
80
|
+
while JsString.index_of(text, pattern) != JsString.last_index_of(text, pattern, text.length) &&
|
|
81
|
+
pattern.length < match_max_bits - patch_margin - patch_margin
|
|
82
|
+
padding += patch_margin
|
|
83
|
+
pattern = JsString.substring(text, patch.start2 - padding, patch.start2 + patch.length1 + padding)
|
|
84
|
+
end
|
|
85
|
+
# Add one chunk for good luck.
|
|
86
|
+
padding += patch_margin
|
|
87
|
+
|
|
88
|
+
# Add the prefix.
|
|
89
|
+
prefix = JsString.substring(text, patch.start2 - padding, patch.start2)
|
|
90
|
+
patch.diffs.unshift(create_diff(DIFF_EQUAL, prefix)) unless prefix.empty?
|
|
91
|
+
|
|
92
|
+
# Add the suffix.
|
|
93
|
+
suffix = JsString.substring(text, patch.start2 + patch.length1, patch.start2 + patch.length1 + padding)
|
|
94
|
+
patch.diffs.push(create_diff(DIFF_EQUAL, suffix)) unless suffix.empty?
|
|
95
|
+
|
|
96
|
+
# Roll back the start points.
|
|
97
|
+
patch.start1 -= prefix.length
|
|
98
|
+
patch.start2 -= prefix.length
|
|
99
|
+
# Extend the lengths.
|
|
100
|
+
patch.length1 += prefix.length + suffix.length
|
|
101
|
+
patch.length2 += prefix.length + suffix.length
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Compute a list of patches to turn text1 into text2.
|
|
106
|
+
# Use diffs if provided, otherwise compute it ourselves.
|
|
107
|
+
# There are four ways to call this function, depending on what data is
|
|
108
|
+
# available to the caller:
|
|
109
|
+
# Method 1: a = text1, b = text2
|
|
110
|
+
# Method 2: a = diffs
|
|
111
|
+
# Method 3 (optimal): a = text1, b = diffs
|
|
112
|
+
# Method 4 (deprecated, use method 3): a = text1, b = text2, c = diffs
|
|
113
|
+
def patch_make(a, opt_b = nil, opt_c = nil, options = nil)
|
|
114
|
+
resolved = resolve_options(options)
|
|
115
|
+
|
|
116
|
+
if a.is_a?(String) && opt_b.is_a?(String) && opt_c.nil?
|
|
117
|
+
# Method 1: text1, text2
|
|
118
|
+
# Compute diffs from text1 and text2.
|
|
119
|
+
text1 = a
|
|
120
|
+
diffs = diff_main(text1, opt_b, resolved, true)
|
|
121
|
+
if diffs.length > 2
|
|
122
|
+
diff_cleanup_semantic(diffs)
|
|
123
|
+
diff_cleanup_efficiency(diffs, resolved)
|
|
124
|
+
end
|
|
125
|
+
elsif a.is_a?(Array) && opt_b.nil? && opt_c.nil?
|
|
126
|
+
# Method 2: diffs
|
|
127
|
+
# Compute text1 from diffs.
|
|
128
|
+
diffs = a
|
|
129
|
+
text1 = diff_text1(diffs)
|
|
130
|
+
elsif a.is_a?(String) && opt_b.is_a?(Array) && opt_c.nil?
|
|
131
|
+
# Method 3: text1, diffs
|
|
132
|
+
text1 = a
|
|
133
|
+
diffs = opt_b
|
|
134
|
+
elsif a.is_a?(String) && opt_b.is_a?(String) && opt_c.is_a?(Array)
|
|
135
|
+
# Method 4: text1, text2, diffs
|
|
136
|
+
# text2 is not used.
|
|
137
|
+
text1 = a
|
|
138
|
+
diffs = opt_c
|
|
139
|
+
else
|
|
140
|
+
raise Error, 'Unknown call format to patch_make.'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return [] if diffs.empty? # Get rid of the null case.
|
|
144
|
+
|
|
145
|
+
patches = []
|
|
146
|
+
patch = Patch.new
|
|
147
|
+
patch_diff_length = 0 # Keeping our own length var is faster in JS.
|
|
148
|
+
char_count1 = 0 # Number of characters into the text1 string.
|
|
149
|
+
char_count2 = 0 # Number of characters into the text2 string.
|
|
150
|
+
# Start with text1 (prepatch_text) and apply the diffs until we arrive at
|
|
151
|
+
# text2 (postpatch_text). We recreate the patches one by one to determine
|
|
152
|
+
# context info.
|
|
153
|
+
prepatch_text = text1
|
|
154
|
+
postpatch_text = text1
|
|
155
|
+
diffs.each_with_index do |diff, x|
|
|
156
|
+
diff_type = diff[0]
|
|
157
|
+
diff_text = diff[1]
|
|
158
|
+
|
|
159
|
+
if patch_diff_length.zero? && diff_type != DIFF_EQUAL
|
|
160
|
+
# A new patch starts here.
|
|
161
|
+
patch.start1 = char_count1
|
|
162
|
+
patch.start2 = char_count2
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
case diff_type
|
|
166
|
+
when DIFF_INSERT
|
|
167
|
+
patch.diffs[patch_diff_length] = diff
|
|
168
|
+
patch_diff_length += 1
|
|
169
|
+
patch.length2 += diff_text.length
|
|
170
|
+
postpatch_text = JsString.substring(postpatch_text, 0, char_count2) + diff_text +
|
|
171
|
+
JsString.substring(postpatch_text, char_count2)
|
|
172
|
+
when DIFF_DELETE
|
|
173
|
+
patch.length1 += diff_text.length
|
|
174
|
+
patch.diffs[patch_diff_length] = diff
|
|
175
|
+
patch_diff_length += 1
|
|
176
|
+
postpatch_text = JsString.substring(postpatch_text, 0, char_count2) +
|
|
177
|
+
JsString.substring(postpatch_text, char_count2 + diff_text.length)
|
|
178
|
+
when DIFF_EQUAL
|
|
179
|
+
if diff_text.length <= 2 * resolved.patch_margin &&
|
|
180
|
+
patch_diff_length.positive? && diffs.length != x + 1
|
|
181
|
+
# Small equality inside a patch.
|
|
182
|
+
patch.diffs[patch_diff_length] = diff
|
|
183
|
+
patch_diff_length += 1
|
|
184
|
+
patch.length1 += diff_text.length
|
|
185
|
+
patch.length2 += diff_text.length
|
|
186
|
+
elsif diff_text.length >= 2 * resolved.patch_margin
|
|
187
|
+
# Time for a new patch.
|
|
188
|
+
if patch_diff_length.positive?
|
|
189
|
+
patch_add_context(patch, prepatch_text, resolved)
|
|
190
|
+
patches.push(patch)
|
|
191
|
+
patch = Patch.new
|
|
192
|
+
patch_diff_length = 0
|
|
193
|
+
# Unlike Unidiff, our patch lists have a rolling context.
|
|
194
|
+
# https://github.com/google/diff-match-patch/wiki/Unidiff
|
|
195
|
+
# Update prepatch text & pos to reflect the application of the
|
|
196
|
+
# just completed patch.
|
|
197
|
+
prepatch_text = postpatch_text
|
|
198
|
+
char_count1 = char_count2
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Update the current character count.
|
|
204
|
+
char_count1 += diff_text.length if diff_type != DIFF_INSERT
|
|
205
|
+
char_count2 += diff_text.length if diff_type != DIFF_DELETE
|
|
206
|
+
end
|
|
207
|
+
# Pick up the leftover patch if not empty.
|
|
208
|
+
if patch_diff_length.positive?
|
|
209
|
+
patch_add_context(patch, prepatch_text, resolved)
|
|
210
|
+
patches.push(patch)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
patches
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Given an array of patches, return another array that is identical.
|
|
217
|
+
def patch_deep_copy(patches)
|
|
218
|
+
patches.map do |patch|
|
|
219
|
+
patch_copy = Patch.new
|
|
220
|
+
patch_copy.diffs = patch.diffs.map { |(op, text)| create_diff(op, text) }
|
|
221
|
+
patch_copy.start1 = patch.start1
|
|
222
|
+
patch_copy.start2 = patch.start2
|
|
223
|
+
patch_copy.length1 = patch.length1
|
|
224
|
+
patch_copy.length2 = patch.length2
|
|
225
|
+
patch_copy
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Merge a set of patches onto the text. Return a patched text, as well
|
|
230
|
+
# as a list of true/false values indicating which patches were applied.
|
|
231
|
+
def patch_apply(patches, text, options = nil)
|
|
232
|
+
return [text, []] if patches.empty?
|
|
233
|
+
|
|
234
|
+
# Deep copy the patches so that no changes are made to originals.
|
|
235
|
+
patches = patch_deep_copy(patches)
|
|
236
|
+
|
|
237
|
+
resolved = resolve_options(options)
|
|
238
|
+
null_padding = patch_add_padding(patches, resolved)
|
|
239
|
+
text = null_padding + text + null_padding
|
|
240
|
+
|
|
241
|
+
patch_split_max(patches, resolved)
|
|
242
|
+
# delta keeps track of the offset between the expected and actual
|
|
243
|
+
# location of the previous patch. If there are patches expected at
|
|
244
|
+
# positions 10 and 20, but the first patch was found at 12, delta is 2
|
|
245
|
+
# and the second patch has an effective expected position of 22.
|
|
246
|
+
delta = 0
|
|
247
|
+
results = []
|
|
248
|
+
patches.each_with_index do |patch, x|
|
|
249
|
+
expected_loc = patch.start2 + delta
|
|
250
|
+
text1 = diff_text1(patch.diffs)
|
|
251
|
+
end_loc = -1
|
|
252
|
+
if text1.length > resolved.match_max_bits
|
|
253
|
+
# patch_split_max will only provide an oversized pattern in the case
|
|
254
|
+
# of a monster delete.
|
|
255
|
+
start_loc = match_main(
|
|
256
|
+
text,
|
|
257
|
+
JsString.substring(text1, 0, resolved.match_max_bits),
|
|
258
|
+
expected_loc,
|
|
259
|
+
resolved,
|
|
260
|
+
)
|
|
261
|
+
if start_loc != -1
|
|
262
|
+
end_loc = match_main(
|
|
263
|
+
text,
|
|
264
|
+
JsString.substring(text1, text1.length - resolved.match_max_bits),
|
|
265
|
+
expected_loc + text1.length - resolved.match_max_bits,
|
|
266
|
+
resolved,
|
|
267
|
+
)
|
|
268
|
+
if end_loc == -1 || start_loc >= end_loc
|
|
269
|
+
# Can't find valid trailing context. Drop this patch.
|
|
270
|
+
start_loc = -1
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
start_loc = match_main(text, text1, expected_loc, resolved)
|
|
275
|
+
end
|
|
276
|
+
if start_loc == -1
|
|
277
|
+
# No match found. :(
|
|
278
|
+
results[x] = false
|
|
279
|
+
# Subtract the delta for this failed patch from subsequent patches.
|
|
280
|
+
delta -= patch.length2 - patch.length1
|
|
281
|
+
else
|
|
282
|
+
# Found a match. :)
|
|
283
|
+
results[x] = true
|
|
284
|
+
delta = start_loc - expected_loc
|
|
285
|
+
text2 = if end_loc == -1
|
|
286
|
+
JsString.substring(text, start_loc, start_loc + text1.length)
|
|
287
|
+
else
|
|
288
|
+
JsString.substring(text, start_loc, end_loc + resolved.match_max_bits)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
if text1 == text2
|
|
292
|
+
# Perfect match, just shove the replacement text in.
|
|
293
|
+
text = JsString.substring(text, 0, start_loc) +
|
|
294
|
+
diff_text2(patch.diffs) +
|
|
295
|
+
JsString.substring(text, start_loc + text1.length)
|
|
296
|
+
else
|
|
297
|
+
# Imperfect match. Run a diff to get a framework of equivalent
|
|
298
|
+
# indices.
|
|
299
|
+
diffs = diff_main(text1, text2, resolved, false)
|
|
300
|
+
if text1.length > resolved.match_max_bits &&
|
|
301
|
+
diff_levenshtein(diffs).fdiv(text1.length) > resolved.patch_delete_threshold
|
|
302
|
+
# The end points match, but the content is unacceptably bad.
|
|
303
|
+
results[x] = false
|
|
304
|
+
else
|
|
305
|
+
diff_cleanup_semantic_lossless(diffs)
|
|
306
|
+
index1 = 0
|
|
307
|
+
index2 = 0
|
|
308
|
+
patch.diffs.each do |mod|
|
|
309
|
+
index2 = diff_x_index(diffs, index1) if mod[0] != DIFF_EQUAL
|
|
310
|
+
if mod[0] == DIFF_INSERT # Insertion
|
|
311
|
+
text = JsString.substring(text, 0, start_loc + index2) + mod[1] +
|
|
312
|
+
JsString.substring(text, start_loc + index2)
|
|
313
|
+
elsif mod[0] == DIFF_DELETE # Deletion
|
|
314
|
+
text = JsString.substring(text, 0, start_loc + index2) +
|
|
315
|
+
JsString.substring(text, start_loc + diff_x_index(diffs, index1 + mod[1].length))
|
|
316
|
+
end
|
|
317
|
+
index1 += mod[1].length if mod[0] != DIFF_DELETE
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
# Strip the padding off.
|
|
324
|
+
text = JsString.substring(text, null_padding.length, text.length - null_padding.length)
|
|
325
|
+
[text, results]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Add some padding on text start and end so that edges can match something.
|
|
329
|
+
# Intended to be called only from within patch_apply.
|
|
330
|
+
# Returns the padding string added to each side.
|
|
331
|
+
def patch_add_padding(patches, options = nil)
|
|
332
|
+
padding_length = resolve_options(options).patch_margin
|
|
333
|
+
null_padding = +''
|
|
334
|
+
(1..padding_length).each { |x| null_padding << x.chr(Encoding::UTF_8) }
|
|
335
|
+
|
|
336
|
+
# Bump all the patches forward.
|
|
337
|
+
patches.each do |patch|
|
|
338
|
+
patch.start1 += padding_length
|
|
339
|
+
patch.start2 += padding_length
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Add some padding on start of first diff.
|
|
343
|
+
patch = patches[0]
|
|
344
|
+
diffs = patch.diffs
|
|
345
|
+
if diffs.empty? || diffs[0][0] != DIFF_EQUAL
|
|
346
|
+
# Add null_padding equality.
|
|
347
|
+
diffs.unshift(create_diff(DIFF_EQUAL, null_padding))
|
|
348
|
+
patch.start1 -= padding_length # Should be 0.
|
|
349
|
+
patch.start2 -= padding_length # Should be 0.
|
|
350
|
+
patch.length1 += padding_length
|
|
351
|
+
patch.length2 += padding_length
|
|
352
|
+
elsif padding_length > diffs[0][1].length
|
|
353
|
+
# Grow first equality.
|
|
354
|
+
extra_length = padding_length - diffs[0][1].length
|
|
355
|
+
diffs[0][1] = JsString.substring(null_padding, diffs[0][1].length) + diffs[0][1]
|
|
356
|
+
patch.start1 -= extra_length
|
|
357
|
+
patch.start2 -= extra_length
|
|
358
|
+
patch.length1 += extra_length
|
|
359
|
+
patch.length2 += extra_length
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Add some padding on end of last diff.
|
|
363
|
+
patch = patches[patches.length - 1]
|
|
364
|
+
diffs = patch.diffs
|
|
365
|
+
if diffs.empty? || diffs[diffs.length - 1][0] != DIFF_EQUAL
|
|
366
|
+
# Add null_padding equality.
|
|
367
|
+
diffs.push(create_diff(DIFF_EQUAL, null_padding))
|
|
368
|
+
patch.length1 += padding_length
|
|
369
|
+
patch.length2 += padding_length
|
|
370
|
+
elsif padding_length > diffs[diffs.length - 1][1].length
|
|
371
|
+
# Grow last equality.
|
|
372
|
+
extra_length = padding_length - diffs[diffs.length - 1][1].length
|
|
373
|
+
diffs[diffs.length - 1][1] += JsString.substring(null_padding, 0, extra_length)
|
|
374
|
+
patch.length1 += extra_length
|
|
375
|
+
patch.length2 += extra_length
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
null_padding
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Look through the patches and break up any which are longer than the
|
|
382
|
+
# maximum limit of the match algorithm.
|
|
383
|
+
# Intended to be called only from within patch_apply.
|
|
384
|
+
def patch_split_max(patches, options = nil)
|
|
385
|
+
resolved = resolve_options(options)
|
|
386
|
+
x = 0
|
|
387
|
+
while x < patches.length
|
|
388
|
+
if patches[x].length1 <= resolved.match_max_bits
|
|
389
|
+
x += 1
|
|
390
|
+
next
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
bigpatch = patches[x]
|
|
394
|
+
# Remove the big old patch.
|
|
395
|
+
patches.delete_at(x)
|
|
396
|
+
x -= 1
|
|
397
|
+
start1 = bigpatch.start1
|
|
398
|
+
start2 = bigpatch.start2
|
|
399
|
+
precontext = ''
|
|
400
|
+
until bigpatch.diffs.empty?
|
|
401
|
+
# Create one of several smaller patches.
|
|
402
|
+
patch = Patch.new
|
|
403
|
+
empty = true
|
|
404
|
+
patch.start1 = start1 - precontext.length
|
|
405
|
+
patch.start2 = start2 - precontext.length
|
|
406
|
+
unless precontext.empty?
|
|
407
|
+
patch.length1 = patch.length2 = precontext.length
|
|
408
|
+
patch.diffs.push(create_diff(DIFF_EQUAL, precontext))
|
|
409
|
+
end
|
|
410
|
+
while !bigpatch.diffs.empty? &&
|
|
411
|
+
patch.length1 < resolved.match_max_bits - resolved.patch_margin
|
|
412
|
+
diff_type = bigpatch.diffs[0][0]
|
|
413
|
+
diff_text = bigpatch.diffs[0][1]
|
|
414
|
+
if diff_type == DIFF_INSERT
|
|
415
|
+
# Insertions are harmless.
|
|
416
|
+
patch.length2 += diff_text.length
|
|
417
|
+
start2 += diff_text.length
|
|
418
|
+
patch.diffs.push(bigpatch.diffs.shift)
|
|
419
|
+
empty = false
|
|
420
|
+
elsif diff_type == DIFF_DELETE && patch.diffs.length == 1 &&
|
|
421
|
+
patch.diffs[0][0] == DIFF_EQUAL &&
|
|
422
|
+
diff_text.length > 2 * resolved.match_max_bits
|
|
423
|
+
# This is a large deletion. Let it pass in one chunk.
|
|
424
|
+
patch.length1 += diff_text.length
|
|
425
|
+
start1 += diff_text.length
|
|
426
|
+
empty = false
|
|
427
|
+
patch.diffs.push(create_diff(diff_type, diff_text))
|
|
428
|
+
bigpatch.diffs.shift
|
|
429
|
+
else
|
|
430
|
+
# Deletion or equality. Only take as much as we can stomach.
|
|
431
|
+
diff_text = JsString.substring(
|
|
432
|
+
diff_text, 0, resolved.match_max_bits - patch.length1 - resolved.patch_margin
|
|
433
|
+
)
|
|
434
|
+
patch.length1 += diff_text.length
|
|
435
|
+
start1 += diff_text.length
|
|
436
|
+
if diff_type == DIFF_EQUAL
|
|
437
|
+
patch.length2 += diff_text.length
|
|
438
|
+
start2 += diff_text.length
|
|
439
|
+
else
|
|
440
|
+
empty = false
|
|
441
|
+
end
|
|
442
|
+
patch.diffs.push(create_diff(diff_type, diff_text))
|
|
443
|
+
if diff_text == bigpatch.diffs[0][1]
|
|
444
|
+
bigpatch.diffs.shift
|
|
445
|
+
else
|
|
446
|
+
bigpatch.diffs[0][1] = JsString.substring(bigpatch.diffs[0][1], diff_text.length)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
# Compute the head context for the next patch.
|
|
451
|
+
precontext = diff_text2(patch.diffs)
|
|
452
|
+
precontext = JsString.substring(precontext, precontext.length - resolved.patch_margin)
|
|
453
|
+
# Append the end context for this patch.
|
|
454
|
+
postcontext = JsString.substring(diff_text1(bigpatch.diffs), 0, resolved.patch_margin)
|
|
455
|
+
unless postcontext.empty?
|
|
456
|
+
patch.length1 += postcontext.length
|
|
457
|
+
patch.length2 += postcontext.length
|
|
458
|
+
if !patch.diffs.empty? && patch.diffs[patch.diffs.length - 1][0] == DIFF_EQUAL
|
|
459
|
+
patch.diffs[patch.diffs.length - 1][1] += postcontext
|
|
460
|
+
else
|
|
461
|
+
patch.diffs.push(create_diff(DIFF_EQUAL, postcontext))
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
unless empty
|
|
465
|
+
x += 1
|
|
466
|
+
patches.insert(x, patch)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
x += 1
|
|
470
|
+
end
|
|
471
|
+
nil
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Take a list of patches and return a textual representation.
|
|
475
|
+
def patch_to_text(patches)
|
|
476
|
+
patches.map(&:to_s).join
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Parse a textual representation of patches and return a list of Patch
|
|
480
|
+
# objects.
|
|
481
|
+
def patch_from_text(textline)
|
|
482
|
+
patches = []
|
|
483
|
+
return patches if textline.nil? || textline.empty?
|
|
484
|
+
|
|
485
|
+
text = textline.split("\n", -1)
|
|
486
|
+
text_pointer = 0
|
|
487
|
+
patch_header = /\A@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@\z/
|
|
488
|
+
while text_pointer < text.length
|
|
489
|
+
m = patch_header.match(text[text_pointer])
|
|
490
|
+
raise Error, "Invalid patch string: #{text[text_pointer]}" unless m
|
|
491
|
+
|
|
492
|
+
patch = Patch.new
|
|
493
|
+
patches.push(patch)
|
|
494
|
+
patch.start1 = m[1].to_i
|
|
495
|
+
if m[2] == ''
|
|
496
|
+
patch.start1 -= 1
|
|
497
|
+
patch.length1 = 1
|
|
498
|
+
elsif m[2] == '0'
|
|
499
|
+
patch.length1 = 0
|
|
500
|
+
else
|
|
501
|
+
patch.start1 -= 1
|
|
502
|
+
patch.length1 = m[2].to_i
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
patch.start2 = m[3].to_i
|
|
506
|
+
if m[4] == ''
|
|
507
|
+
patch.start2 -= 1
|
|
508
|
+
patch.length2 = 1
|
|
509
|
+
elsif m[4] == '0'
|
|
510
|
+
patch.length2 = 0
|
|
511
|
+
else
|
|
512
|
+
patch.start2 -= 1
|
|
513
|
+
patch.length2 = m[4].to_i
|
|
514
|
+
end
|
|
515
|
+
text_pointer += 1
|
|
516
|
+
|
|
517
|
+
while text_pointer < text.length
|
|
518
|
+
sign = JsString.char_at(text[text_pointer], 0)
|
|
519
|
+
line = ''
|
|
520
|
+
begin
|
|
521
|
+
line = JsString.decode_uri(JsString.substring(text[text_pointer], 1))
|
|
522
|
+
rescue JsString::UriError
|
|
523
|
+
# Malformed URI sequence. (The reference implementation reports the
|
|
524
|
+
# pre-assignment value of `line`, which is always empty.)
|
|
525
|
+
raise Error, 'Illegal escape in patch_fromText: '
|
|
526
|
+
end
|
|
527
|
+
case sign
|
|
528
|
+
when '-'
|
|
529
|
+
# Deletion.
|
|
530
|
+
patch.diffs.push(create_diff(DIFF_DELETE, line))
|
|
531
|
+
when '+'
|
|
532
|
+
# Insertion.
|
|
533
|
+
patch.diffs.push(create_diff(DIFF_INSERT, line))
|
|
534
|
+
when ' '
|
|
535
|
+
# Minor equality.
|
|
536
|
+
patch.diffs.push(create_diff(DIFF_EQUAL, line))
|
|
537
|
+
when '@'
|
|
538
|
+
# Start of next patch.
|
|
539
|
+
break
|
|
540
|
+
when ''
|
|
541
|
+
# Blank line? Whatever.
|
|
542
|
+
else
|
|
543
|
+
# WTF?
|
|
544
|
+
raise Error, "Invalid patch mode \"#{sign}\" in: #{line}"
|
|
545
|
+
end
|
|
546
|
+
text_pointer += 1
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
patches
|
|
550
|
+
end
|
|
551
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Diff Match and Patch
|
|
4
|
+
# Copyright 2018 The diff-match-patch Authors.
|
|
5
|
+
# https://github.com/google/diff-match-patch
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
|
|
19
|
+
# Ruby port of diff-match-patch-es (the TypeScript/ESM rewrite by Anthony Fu
|
|
20
|
+
# of Neil Fraser's diff-match-patch), producing identical results.
|
|
21
|
+
#
|
|
22
|
+
# The API is a set of stateless module functions; nothing mutates shared
|
|
23
|
+
# state, DEFAULT_OPTIONS is frozen, and every call operates only on its
|
|
24
|
+
# arguments and locals, so the module is safe to use from multiple threads
|
|
25
|
+
# (and Ractors) concurrently.
|
|
26
|
+
module DiffMatchPatchES
|
|
27
|
+
class Error < StandardError; end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative 'diff_match_patch_es/version'
|
|
31
|
+
require_relative 'diff_match_patch_es/js_string'
|
|
32
|
+
require_relative 'diff_match_patch_es/options'
|
|
33
|
+
require_relative 'diff_match_patch_es/diff'
|
|
34
|
+
require_relative 'diff_match_patch_es/match'
|
|
35
|
+
require_relative 'diff_match_patch_es/patch'
|
|
36
|
+
|
|
37
|
+
module DiffMatchPatchES
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def diff(text1, text2, options = nil, checklines = true, deadline = nil)
|
|
41
|
+
diff_main(text1, text2, options, checklines, deadline)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def match(text, pattern, loc, options = nil)
|
|
45
|
+
match_main(text, pattern, loc, options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def patch(a, opt_b = nil, opt_c = nil, options = nil)
|
|
49
|
+
patch_make(a, opt_b, opt_c, options)
|
|
50
|
+
end
|
|
51
|
+
end
|