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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5b90b7fc1d45d8f8e58e804f3f67dae934cc225639f3196c64f43cdd84ae005
4
- data.tar.gz: c6615312bb2ac6eba00700c60b7dfeeb18277b8677532f98f91e4ad24a29c562
3
+ metadata.gz: 0a25343915041867345d470570eb9a84d2266e4b95b89b730bee32a0a99b112f
4
+ data.tar.gz: 501f8c0e6c7430611a0c68c3b31b2d30ce63ec411bd6426e854fdb9c57a4f6ac
5
5
  SHA512:
6
- metadata.gz: 75e2773c9597d616988157bdd40f4df3478dfd5695c1b9c7b9fc3709a6b043e22e98d3a1d9f2221ad06f7d4bb249331b1356606dce4acf10ce4bdfd8c254d209
7
- data.tar.gz: 2877cc2367ac8da5ef68ea9b2ba2cc94883754a47aaf7dba51aef1050d67b9f3d95027238a539e119c92704fa696bdd8de8412bf178098a00e03061fbbb48888
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
- # Compare normalized versions
21
- normalize_for_comparison(line1) == normalize_for_comparison(line2)
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
- QUOTE_CHARS = ["\"", "'"].freeze
111
- SKIP_CHARS = [" ", "="].freeze
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
- if text[i] == "<"
125
- # Handle processing instruction <?...?>, comment <!--...-->,
126
- # and regular tags
127
- new_i, tag_output = process_tag(text, i)
128
- if new_i
129
- result << tag_output
130
- i = new_i
131
- next
132
- end
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.strip.empty?
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 < tag_content.length && tag_content[i] == " "
186
- return nil if i >= tag_content.length
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 < tag_content.length && tag_content[i] != " " &&
190
- tag_content[i] != "/" && tag_content[i] != ">"
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 < tag_content.length && tag_content[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 < tag_content.length
261
+ while i < length
199
262
  # Skip whitespace
200
- i += 1 while i < tag_content.length && tag_content[i] == " "
201
- break if i >= tag_content.length
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 < tag_content.length && tag_content[i] != "=" &&
206
- tag_content[i] != " " && tag_content[i] != "/" &&
207
- tag_content[i] != ">"
208
- break if i >= tag_content.length || i == attr_start
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 < tag_content.length &&
214
- SKIP_CHARS.include?(tag_content[i])
215
- break if i >= tag_content.length
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[i]
219
- break unless QUOTE_CHARS.include?(quote)
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 < tag_content.length && tag_content[i] != quote
286
+ while i < length && tag_content.getbyte(i) != quote
224
287
  i += 1
225
288
  end
226
- break if i >= tag_content.length
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
- attrs << { name: attr_name, value: "#{quote}#{attr_value}#{quote}" }
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[pos + 1] == "?"
308
+ if text.getbyte(pos + 1) == 63 # ?
245
309
  process_processing_instruction(text, pos)
246
- elsif text[pos + 1] == "!" && text[(pos + 2)...(pos + 4)] == "--"
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.chars.map do |char|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.61"
4
+ VERSION = "0.3.63"
5
5
  end
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.61
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-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs