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,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