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,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiffMatchPatchES
|
|
4
|
+
# Helpers that reproduce JavaScript string semantics exactly.
|
|
5
|
+
module JsString
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# JS String.prototype.substring: negative/overflowing indices clamp to [0, length] and the arguments swap when start > end.
|
|
9
|
+
def substring(str, start_index, end_index = nil)
|
|
10
|
+
len = str.length
|
|
11
|
+
a = start_index.clamp(0, len)
|
|
12
|
+
b = end_index.nil? ? len : end_index.clamp(0, len)
|
|
13
|
+
a, b = b, a if a > b
|
|
14
|
+
str[a...b]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# JS String.prototype.charAt: '' when out of range.
|
|
18
|
+
def char_at(str, index)
|
|
19
|
+
return '' if index.negative? || index >= str.length
|
|
20
|
+
|
|
21
|
+
str[index]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# JS String.prototype.indexOf.
|
|
25
|
+
def index_of(str, pattern, from = 0)
|
|
26
|
+
from = from.clamp(0, str.length)
|
|
27
|
+
str.index(pattern, from) || -1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# JS String.prototype.lastIndexOf: the match may start at or before +from+.
|
|
31
|
+
def last_index_of(str, pattern, from)
|
|
32
|
+
from = from.clamp(0, str.length)
|
|
33
|
+
str.rindex(pattern, from) || -1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# JS Number.parseInt(str, 10): optional leading whitespace and sign,
|
|
37
|
+
# JS StrWhiteSpace: ASCII whitespace, NBSP, BOM, and Unicode space separators.
|
|
38
|
+
JS_PARSE_INT = /\A[\t\n\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*([+-]?\d+)/
|
|
39
|
+
|
|
40
|
+
# then as many digits as possible. Returns nil where JS returns NaN.
|
|
41
|
+
def parse_int(str)
|
|
42
|
+
match = JS_PARSE_INT.match(str)
|
|
43
|
+
match && match[1].to_i
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Characters encodeURI leaves unescaped.
|
|
47
|
+
ENCODE_URI_UNESCAPED = /\A[A-Za-z0-9;,\/?:@&=+$\-_.!~*'()#]\z/
|
|
48
|
+
# Characters whose escape sequences decodeURI refuses to decode
|
|
49
|
+
# (uriReserved plus '#').
|
|
50
|
+
DECODE_URI_RESERVED = ";/?:@&=+$,#"
|
|
51
|
+
|
|
52
|
+
# JS encodeURI: percent-encodes the UTF-8 bytes of every character outside
|
|
53
|
+
# the unescaped set, using uppercase hex.
|
|
54
|
+
def encode_uri(str)
|
|
55
|
+
out = +''
|
|
56
|
+
str.each_char do |ch|
|
|
57
|
+
if ENCODE_URI_UNESCAPED.match?(ch)
|
|
58
|
+
out << ch
|
|
59
|
+
else
|
|
60
|
+
ch.each_byte { |b| out << format('%%%02X', b) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
out
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class UriError < StandardError; end
|
|
67
|
+
|
|
68
|
+
# JS decodeURI: decodes %XX escape sequences as strict UTF-8 (rejecting
|
|
69
|
+
# malformed, overlong, surrogate, and out-of-range sequences) while leaving
|
|
70
|
+
# escapes of reserved characters (";/?:@&=+$,#") untouched.
|
|
71
|
+
def decode_uri(str)
|
|
72
|
+
out = +''
|
|
73
|
+
i = 0
|
|
74
|
+
n = str.length
|
|
75
|
+
while i < n
|
|
76
|
+
ch = str[i]
|
|
77
|
+
unless ch == '%'
|
|
78
|
+
out << ch
|
|
79
|
+
i += 1
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
start = i
|
|
83
|
+
b0 = read_escaped_byte(str, i)
|
|
84
|
+
i += 3
|
|
85
|
+
if b0 < 0x80
|
|
86
|
+
decoded = b0.chr
|
|
87
|
+
if DECODE_URI_RESERVED.include?(decoded)
|
|
88
|
+
out << str[start, 3]
|
|
89
|
+
else
|
|
90
|
+
out << decoded
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
extra =
|
|
94
|
+
case b0
|
|
95
|
+
when 0xC2..0xDF then 1
|
|
96
|
+
when 0xE0..0xEF then 2
|
|
97
|
+
when 0xF0..0xF4 then 3
|
|
98
|
+
else raise UriError, 'URI malformed'
|
|
99
|
+
end
|
|
100
|
+
codepoint = b0 & (0x3F >> extra)
|
|
101
|
+
extra.times do
|
|
102
|
+
raise UriError, 'URI malformed' unless str[i] == '%'
|
|
103
|
+
|
|
104
|
+
byte = read_escaped_byte(str, i)
|
|
105
|
+
raise UriError, 'URI malformed' unless (byte & 0xC0) == 0x80
|
|
106
|
+
|
|
107
|
+
codepoint = (codepoint << 6) | (byte & 0x3F)
|
|
108
|
+
i += 3
|
|
109
|
+
end
|
|
110
|
+
minimum = [0x80, 0x800, 0x10000][extra - 1]
|
|
111
|
+
if codepoint < minimum || codepoint > 0x10FFFF || (0xD800..0xDFFF).cover?(codepoint)
|
|
112
|
+
raise UriError, 'URI malformed'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
out << codepoint.chr(Encoding::UTF_8)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
out
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def read_escaped_byte(str, index)
|
|
122
|
+
hex = str[index + 1, 2]
|
|
123
|
+
raise UriError, 'URI malformed' unless hex&.length == 2 && /\A\h\h\z/.match?(hex)
|
|
124
|
+
|
|
125
|
+
hex.to_i(16)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiffMatchPatchES
|
|
4
|
+
# JS bitwise operators coerce to 32-bit integers; Ruby integers are
|
|
5
|
+
# arbitrary precision, so shifts are masked to keep the two congruent.
|
|
6
|
+
MASK32 = 0xFFFFFFFF
|
|
7
|
+
private_constant :MASK32
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Locate the best instance of 'pattern' in 'text' near 'loc'.
|
|
12
|
+
# Returns the best match index or -1.
|
|
13
|
+
def match_main(text, pattern, loc, options = nil)
|
|
14
|
+
# Check for null inputs.
|
|
15
|
+
raise Error, 'Null input. (match_main)' if text.nil? || pattern.nil? || loc.nil?
|
|
16
|
+
|
|
17
|
+
loc = loc.clamp(0, text.length)
|
|
18
|
+
if text == pattern
|
|
19
|
+
# Shortcut (potentially not guaranteed by the algorithm)
|
|
20
|
+
0
|
|
21
|
+
elsif text.empty?
|
|
22
|
+
# Nothing to match.
|
|
23
|
+
-1
|
|
24
|
+
elsif JsString.substring(text, loc, loc + pattern.length) == pattern
|
|
25
|
+
# Perfect match at the perfect spot! (Includes case of null pattern)
|
|
26
|
+
loc
|
|
27
|
+
else
|
|
28
|
+
# Do a fuzzy compare.
|
|
29
|
+
match_bitap(text, pattern, loc, options)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Locate the best instance of 'pattern' in 'text' near 'loc' using the
|
|
34
|
+
# Bitap algorithm.
|
|
35
|
+
def match_bitap(text, pattern, loc, options = nil)
|
|
36
|
+
resolved = resolve_options(options)
|
|
37
|
+
|
|
38
|
+
raise Error, 'Pattern too long for this browser.' if pattern.length > resolved.match_max_bits
|
|
39
|
+
|
|
40
|
+
# Initialise the alphabet.
|
|
41
|
+
s = match_alphabet(pattern)
|
|
42
|
+
|
|
43
|
+
# Compute and return the score for a match with e errors and x location.
|
|
44
|
+
# (0.0 = good, 1.0 = bad)
|
|
45
|
+
bitap_score = lambda do |e, x|
|
|
46
|
+
accuracy = e.fdiv(pattern.length)
|
|
47
|
+
proximity = (loc - x).abs
|
|
48
|
+
if resolved.match_distance.zero?
|
|
49
|
+
# Dodge divide by zero error.
|
|
50
|
+
return proximity.zero? ? accuracy : 1.0
|
|
51
|
+
end
|
|
52
|
+
accuracy + proximity.fdiv(resolved.match_distance)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Highest score beyond which we give up.
|
|
56
|
+
score_threshold = resolved.match_threshold
|
|
57
|
+
# Is there a nearby exact match? (speedup)
|
|
58
|
+
best_loc = JsString.index_of(text, pattern, loc)
|
|
59
|
+
if best_loc != -1
|
|
60
|
+
score_threshold = [bitap_score.call(0, best_loc), score_threshold].min
|
|
61
|
+
# What about in the other direction? (speedup)
|
|
62
|
+
best_loc = JsString.last_index_of(text, pattern, loc + pattern.length)
|
|
63
|
+
score_threshold = [bitap_score.call(0, best_loc), score_threshold].min if best_loc != -1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Initialise the bit arrays.
|
|
67
|
+
matchmask = (1 << (pattern.length - 1)) & MASK32
|
|
68
|
+
best_loc = -1
|
|
69
|
+
|
|
70
|
+
bin_max = pattern.length + text.length
|
|
71
|
+
last_rd = []
|
|
72
|
+
d = 0
|
|
73
|
+
while d < pattern.length
|
|
74
|
+
# Scan for the best match; each iteration allows for one more error.
|
|
75
|
+
# Run a binary search to determine how far from 'loc' we can stray at
|
|
76
|
+
# this error level.
|
|
77
|
+
bin_min = 0
|
|
78
|
+
bin_mid = bin_max
|
|
79
|
+
while bin_min < bin_mid
|
|
80
|
+
if bitap_score.call(d, loc + bin_mid) <= score_threshold
|
|
81
|
+
bin_min = bin_mid
|
|
82
|
+
else
|
|
83
|
+
bin_max = bin_mid
|
|
84
|
+
end
|
|
85
|
+
bin_mid = (bin_max - bin_min) / 2 + bin_min
|
|
86
|
+
end
|
|
87
|
+
# Use the result from this iteration as the maximum for the next.
|
|
88
|
+
bin_max = bin_mid
|
|
89
|
+
start = [1, loc - bin_mid + 1].max
|
|
90
|
+
finish = [loc + bin_mid, text.length].min + pattern.length
|
|
91
|
+
|
|
92
|
+
rd = Array.new(finish + 2, 0)
|
|
93
|
+
rd[finish + 1] = ((1 << d) - 1) & MASK32
|
|
94
|
+
j = finish
|
|
95
|
+
while j >= start
|
|
96
|
+
char_match = s[JsString.char_at(text, j - 1)] || 0
|
|
97
|
+
rd[j] = if d.zero? # First pass: exact match.
|
|
98
|
+
(((rd[j + 1] << 1) | 1) & char_match) & MASK32
|
|
99
|
+
else # Subsequent passes: fuzzy match.
|
|
100
|
+
((((rd[j + 1] << 1) | 1) & char_match) |
|
|
101
|
+
((((last_rd[j + 1] || 0) | (last_rd[j] || 0)) << 1) | 1) |
|
|
102
|
+
(last_rd[j + 1] || 0)) & MASK32
|
|
103
|
+
end
|
|
104
|
+
if (rd[j] & matchmask) != 0
|
|
105
|
+
score = bitap_score.call(d, j - 1)
|
|
106
|
+
# This match will almost certainly be better than any existing
|
|
107
|
+
# match. But check anyway.
|
|
108
|
+
if score <= score_threshold
|
|
109
|
+
# Told you so.
|
|
110
|
+
score_threshold = score
|
|
111
|
+
best_loc = j - 1
|
|
112
|
+
if best_loc > loc
|
|
113
|
+
# When passing loc, don't exceed our current distance from loc.
|
|
114
|
+
start = [1, (2 * loc) - best_loc].max
|
|
115
|
+
else
|
|
116
|
+
# Already passed loc, downhill from here on in.
|
|
117
|
+
break
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
j -= 1
|
|
122
|
+
end
|
|
123
|
+
# No hope for a (better) match at greater error levels.
|
|
124
|
+
break if bitap_score.call(d + 1, loc) > score_threshold
|
|
125
|
+
|
|
126
|
+
last_rd = rd
|
|
127
|
+
d += 1
|
|
128
|
+
end
|
|
129
|
+
best_loc
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Initialise the alphabet for the Bitap algorithm.
|
|
133
|
+
def match_alphabet(pattern)
|
|
134
|
+
s = {}
|
|
135
|
+
pattern.each_char { |ch| s[ch] = 0 }
|
|
136
|
+
pattern.length.times do |i|
|
|
137
|
+
s[pattern[i]] |= (1 << (pattern.length - i - 1)) & MASK32
|
|
138
|
+
end
|
|
139
|
+
s
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiffMatchPatchES
|
|
4
|
+
class Options
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
diff_timeout
|
|
7
|
+
diff_edit_cost
|
|
8
|
+
match_threshold
|
|
9
|
+
match_distance
|
|
10
|
+
patch_delete_threshold
|
|
11
|
+
patch_margin
|
|
12
|
+
match_max_bits
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
attr_accessor(*ATTRIBUTES)
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
diff_timeout: 1.0,
|
|
19
|
+
diff_edit_cost: 4,
|
|
20
|
+
match_threshold: 0.5,
|
|
21
|
+
match_distance: 1000,
|
|
22
|
+
patch_delete_threshold: 0.5,
|
|
23
|
+
patch_margin: 4,
|
|
24
|
+
match_max_bits: 32
|
|
25
|
+
)
|
|
26
|
+
@diff_timeout = diff_timeout
|
|
27
|
+
@diff_edit_cost = diff_edit_cost
|
|
28
|
+
@match_threshold = match_threshold
|
|
29
|
+
@match_distance = match_distance
|
|
30
|
+
@patch_delete_threshold = patch_delete_threshold
|
|
31
|
+
@patch_margin = patch_margin
|
|
32
|
+
@match_max_bits = match_max_bits
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
ATTRIBUTES.to_h { |name| [name, public_send(name)] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ==(other)
|
|
40
|
+
other.is_a?(Options) && to_h == other.to_h
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
DEFAULT_OPTIONS = Options.new.freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# Accepts nil, a Hash of overrides, or an already-resolved Options.
|
|
49
|
+
def resolve_options(options = nil)
|
|
50
|
+
case options
|
|
51
|
+
when Options then options
|
|
52
|
+
when nil then Options.new
|
|
53
|
+
when Hash then Options.new(**options)
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "Unsupported options: #{options.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|