canon 0.3.61 → 0.3.63
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 +4 -4
- data/lib/canon/diff/formatting_detector.rb +103 -39
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +18 -3
- data/lib/canon/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a25343915041867345d470570eb9a84d2266e4b95b89b730bee32a0a99b112f
|
|
4
|
+
data.tar.gz: 501f8c0e6c7430611a0c68c3b31b2d30ce63ec411bd6426e854fdb9c57a4f6ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df2fb54642dffbc16670860f406bfce3e76d4665772d5d0394dd65762157ce821ab8d315c8a761abfd3d6ac9a72467039549fccaedfc5ea0ac7596852a898e28
|
|
7
|
+
data.tar.gz: 31781f1ef8b498980a1ca4036ebf9337f6716744f9f6d6e11519aa5ca4d0356d18c6679bc9091a10280b466abab0471e199c9db0d5c4138a27e992dff2af5623
|
|
@@ -17,8 +17,60 @@ module Canon
|
|
|
17
17
|
# If only one is blank, it's not just formatting
|
|
18
18
|
return false if blank?(line1) || blank?(line2)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
return true if line1 == line2
|
|
21
|
+
|
|
22
|
+
# Identical prefix/suffix bytes normalize identically — compare
|
|
23
|
+
# only the differing core, widened to tag boundaries so no tag,
|
|
24
|
+
# entity, or whitespace run spans a cut (the normalization is
|
|
25
|
+
# then compositional across the cut points).
|
|
26
|
+
core1, core2 = differing_core(line1, line2)
|
|
27
|
+
return false if core1.nil?
|
|
28
|
+
|
|
29
|
+
normalize_for_comparison(core1) == normalize_for_comparison(core2)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The differing core of two lines: the region between the common
|
|
33
|
+
# prefix and common suffix, widened outward to just after a ">" so
|
|
34
|
+
# the cuts never split a tag, an entity reference, or a whitespace
|
|
35
|
+
# run. Returns nil when the bytes are identical (no core).
|
|
36
|
+
#
|
|
37
|
+
# @param line1 [String]
|
|
38
|
+
# @param line2 [String]
|
|
39
|
+
# @return [Array(String, String), nil]
|
|
40
|
+
def self.differing_core(line1, line2)
|
|
41
|
+
return nil if line1 == line2
|
|
42
|
+
|
|
43
|
+
size1 = line1.bytesize
|
|
44
|
+
size2 = line2.bytesize
|
|
45
|
+
limit = [size1, size2].min
|
|
46
|
+
lo = 0
|
|
47
|
+
lo += 1 while lo < limit && line1.getbyte(lo) == line2.getbyte(lo)
|
|
48
|
+
|
|
49
|
+
suffix_max = limit - lo
|
|
50
|
+
s = 0
|
|
51
|
+
s += 1 while s < suffix_max &&
|
|
52
|
+
line1.getbyte(size1 - 1 - s) == line2.getbyte(size2 - 1 - s)
|
|
53
|
+
|
|
54
|
+
# Snap the cuts to tag boundaries (the shared regions are
|
|
55
|
+
# identical, so the cuts apply to both lines alike): the left
|
|
56
|
+
# cut moves to the start of the enclosing tag so attribute
|
|
57
|
+
# normalization always sees whole tags; the right cut moves
|
|
58
|
+
# past the closing ">".
|
|
59
|
+
tag_lo = line1.rindex("<", lo)
|
|
60
|
+
lo = tag_lo if tag_lo
|
|
61
|
+
|
|
62
|
+
hi1 = size1 - s
|
|
63
|
+
hi2 = size2 - s
|
|
64
|
+
tag_hi = line1.index(">", hi1)
|
|
65
|
+
if tag_hi
|
|
66
|
+
hi1 = tag_hi + 1
|
|
67
|
+
hi2 = hi1 + (size2 - size1) # same offset within the shared tail
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Degenerate widening (empty core) — compare the full lines.
|
|
71
|
+
return [line1, line2] if lo > hi1 || hi2 < lo
|
|
72
|
+
|
|
73
|
+
[line1[lo...hi1], line2[lo...hi2]]
|
|
22
74
|
end
|
|
23
75
|
|
|
24
76
|
# Aggressive normalization for formatting comparison.
|
|
@@ -107,8 +159,14 @@ module Canon
|
|
|
107
159
|
# case-insensitive (e.g., "UTF-8" equals "utf-8").
|
|
108
160
|
# The standalone declaration in XML 1.1 is also case-insensitive.
|
|
109
161
|
CASE_INSENSITIVE_ATTRS = %w[encoding standalone].freeze
|
|
110
|
-
|
|
111
|
-
|
|
162
|
+
# Byte-level scan sets — the tokenizer reads via getbyte, whose
|
|
163
|
+
# Integer results allocate nothing (String#[] mints a 1-char
|
|
164
|
+
# string per character scanned).
|
|
165
|
+
NAME_END_BYTES = [32, 47, 62].freeze # space, /, >
|
|
166
|
+
SKIP_BYTES = [32, 61].freeze # space, =
|
|
167
|
+
QUOTE_BYTES = [34, 39].freeze # ", '
|
|
168
|
+
SPACE_BYTE = 32
|
|
169
|
+
EMPTY_CONTENT_RE = /\A[ \t\r\n\f\v]*\z/
|
|
112
170
|
|
|
113
171
|
# Normalize attribute order within XML tags so that
|
|
114
172
|
# <elem b="2" a="1"> compares equal to <elem a="1" b="2">.
|
|
@@ -121,19 +179,23 @@ module Canon
|
|
|
121
179
|
i = 0
|
|
122
180
|
|
|
123
181
|
while i < text.length
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
lt = text.index("<", i)
|
|
183
|
+
if lt.nil?
|
|
184
|
+
result << text[i..]
|
|
185
|
+
break
|
|
186
|
+
end
|
|
187
|
+
result << text[i...lt] if lt > i
|
|
188
|
+
|
|
189
|
+
# Handle processing instruction <?...?>, comment <!--...-->,
|
|
190
|
+
# and regular tags
|
|
191
|
+
new_i, tag_output = process_tag(text, lt)
|
|
192
|
+
if new_i
|
|
193
|
+
result << tag_output
|
|
194
|
+
i = new_i
|
|
195
|
+
else
|
|
196
|
+
result << "<"
|
|
197
|
+
i = lt + 1
|
|
133
198
|
end
|
|
134
|
-
|
|
135
|
-
result << text[i]
|
|
136
|
-
i += 1
|
|
137
199
|
end
|
|
138
200
|
|
|
139
201
|
result
|
|
@@ -178,57 +240,59 @@ module Canon
|
|
|
178
240
|
# @param tag_content [String] Content between < and >
|
|
179
241
|
# @return [Hash, nil] { name: String, attrs: Array<{name:, value:}> }
|
|
180
242
|
def self.tokenize_tag_content(tag_content)
|
|
181
|
-
return nil if tag_content.
|
|
243
|
+
return nil if tag_content.match?(EMPTY_CONTENT_RE)
|
|
182
244
|
|
|
183
245
|
i = 0
|
|
246
|
+
length = tag_content.length
|
|
184
247
|
# Find tag name (first non-whitespace word)
|
|
185
|
-
i += 1 while i <
|
|
186
|
-
return nil if i >=
|
|
248
|
+
i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
|
|
249
|
+
return nil if i >= length
|
|
187
250
|
|
|
188
251
|
name_start = i
|
|
189
|
-
i += 1 while i <
|
|
190
|
-
tag_content
|
|
252
|
+
i += 1 while i < length &&
|
|
253
|
+
!NAME_END_BYTES.include?(tag_content.getbyte(i))
|
|
191
254
|
tag_name = tag_content[name_start...i]
|
|
192
255
|
|
|
193
256
|
# Skip whitespace
|
|
194
|
-
i += 1 while i <
|
|
257
|
+
i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
|
|
195
258
|
|
|
196
259
|
# Parse attributes
|
|
197
260
|
attrs = []
|
|
198
|
-
while i <
|
|
261
|
+
while i < length
|
|
199
262
|
# Skip whitespace
|
|
200
|
-
i += 1 while i <
|
|
201
|
-
break if i >=
|
|
263
|
+
i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
|
|
264
|
+
break if i >= length
|
|
202
265
|
|
|
203
266
|
# Read attribute name
|
|
204
267
|
attr_start = i
|
|
205
|
-
i += 1 while i <
|
|
206
|
-
tag_content
|
|
207
|
-
tag_content
|
|
208
|
-
break if i >=
|
|
268
|
+
i += 1 while i < length &&
|
|
269
|
+
!NAME_END_BYTES.include?(tag_content.getbyte(i)) &&
|
|
270
|
+
tag_content.getbyte(i) != 61
|
|
271
|
+
break if i >= length || i == attr_start
|
|
209
272
|
|
|
210
273
|
attr_name = tag_content[attr_start...i]
|
|
211
274
|
|
|
212
275
|
# Skip whitespace and =
|
|
213
|
-
i += 1 while i <
|
|
214
|
-
|
|
215
|
-
break if i >=
|
|
276
|
+
i += 1 while i < length &&
|
|
277
|
+
SKIP_BYTES.include?(tag_content.getbyte(i))
|
|
278
|
+
break if i >= length
|
|
216
279
|
|
|
217
280
|
# Read quoted value
|
|
218
|
-
quote = tag_content
|
|
219
|
-
break unless
|
|
281
|
+
quote = tag_content.getbyte(i)
|
|
282
|
+
break unless QUOTE_BYTES.include?(quote)
|
|
220
283
|
|
|
221
284
|
i += 1
|
|
222
285
|
value_start = i
|
|
223
|
-
while i <
|
|
286
|
+
while i < length && tag_content.getbyte(i) != quote
|
|
224
287
|
i += 1
|
|
225
288
|
end
|
|
226
|
-
break if i >=
|
|
289
|
+
break if i >= length
|
|
227
290
|
|
|
228
291
|
attr_value = tag_content[value_start...i]
|
|
229
292
|
i += 1 # skip closing quote
|
|
230
293
|
|
|
231
|
-
|
|
294
|
+
quote_char = quote.chr
|
|
295
|
+
attrs << { name: attr_name, value: "#{quote_char}#{attr_value}#{quote_char}" }
|
|
232
296
|
end
|
|
233
297
|
|
|
234
298
|
{ name: tag_name, attrs: attrs }
|
|
@@ -241,9 +305,9 @@ module Canon
|
|
|
241
305
|
# @param i [Integer] Position of '<'
|
|
242
306
|
# @return [Array(Integer, String), nil] [new_position, tag_string] or nil
|
|
243
307
|
def self.process_tag(text, pos)
|
|
244
|
-
if text
|
|
308
|
+
if text.getbyte(pos + 1) == 63 # ?
|
|
245
309
|
process_processing_instruction(text, pos)
|
|
246
|
-
elsif text
|
|
310
|
+
elsif text.getbyte(pos + 1) == 33 && text[(pos + 2)...(pos + 4)] == "--" # !
|
|
247
311
|
process_comment(text, pos)
|
|
248
312
|
else
|
|
249
313
|
process_regular_tag(text, pos)
|
|
@@ -642,6 +642,23 @@ module Canon
|
|
|
642
642
|
#
|
|
643
643
|
# @param token [String] The token to apply visualization to
|
|
644
644
|
# @param color [Symbol, nil] Optional color to apply
|
|
645
|
+
# Whole-string gsub through the map — the previous
|
|
646
|
+
# chars.map.fetch.join minted two arrays and one 1-char string
|
|
647
|
+
# per character of every displayed line.
|
|
648
|
+
def visualize(token)
|
|
649
|
+
return token if @visualization_map.empty?
|
|
650
|
+
|
|
651
|
+
token.gsub(visualization_union, @visualization_map)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Longer keys first so "\r\n" wins over "\r"/"\n" at the
|
|
655
|
+
# same position.
|
|
656
|
+
def visualization_union
|
|
657
|
+
@visualization_union ||= Regexp.union(
|
|
658
|
+
@visualization_map.keys.sort_by(&:length).reverse,
|
|
659
|
+
)
|
|
660
|
+
end
|
|
661
|
+
|
|
645
662
|
# @return [String] Visualized and optionally colored token
|
|
646
663
|
def apply_visualization(token, color = nil)
|
|
647
664
|
return "" if token.nil?
|
|
@@ -649,9 +666,7 @@ module Canon
|
|
|
649
666
|
visual = if @character_visualization == :content_only
|
|
650
667
|
visualize_content_only(token.to_s)
|
|
651
668
|
else
|
|
652
|
-
token.to_s
|
|
653
|
-
@visualization_map.fetch(char, char)
|
|
654
|
-
end.join
|
|
669
|
+
visualize(token.to_s)
|
|
655
670
|
end
|
|
656
671
|
|
|
657
672
|
if color && @use_color
|
data/lib/canon/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.63
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|