canon 0.3.60 → 0.3.62

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: f1cbc73f1d667885f1f989a9534d8123dbcc67ae7eda250cc24cba00e7ad24a7
4
- data.tar.gz: b9a27d00c02f533c6679237e190eb7f6bc6532fe0f945957bdc5d521bb84217b
3
+ metadata.gz: c5ab88d04204cba8fb555ca3ebae0091a6a9d22fbb5e06e58974195e6bace30d
4
+ data.tar.gz: 3f952b1cb790351630ba1cb659e69492e67ff34ede453c98624877df4bb9b5f5
5
5
  SHA512:
6
- metadata.gz: b060895cd58c7321db7d0640a02ca166f3efb9b5cb4c90d5fafb7d615b3faf6c73f4e29d42533d56ac9fd393e625bac473d9cd122273dc81b3598b0be76b86f9
7
- data.tar.gz: 4b160273200d229151e61842bc994cd8e1d23f495107f495edde383b2fdccda2e9de150a65160d34c5e3177a7bf236282a6e1bb675c72b2bf667deb200e3dab4
6
+ metadata.gz: 6128ca89822cbf059a28e3f753d42a56e3ab40fd5ed98bb4cca9a467ae1124387b443747d0d480eb0da1bfd8358bc17e870dc4709e12c857b427bd35c56479c4
7
+ data.tar.gz: 1f6100fa01d92715eac814bb0538172b7ebc993cddbeaea34855631a76880cbf5b791403ba581c9b446a7903784aa1217542bc7c45ea18174b3e27eec2d7fb1d
@@ -201,11 +201,17 @@ module Canon
201
201
  # side answers both; any hit falls through to the pipeline.
202
202
  def verbose_report_proven_empty?(n1, n2, opts)
203
203
  return true unless opts[:verbose]
204
+
204
205
  # Identical input parses identically — differences and
205
- # attribute order coincide by construction; only the SAX
206
- # recover-error surface (parse-error banner, issue #130)
207
- # needs its one scan.
208
- return !Xml::Sax.probe(n1).saw_error? if n1 == n2
206
+ # attribute order coincide by construction. Recover errors
207
+ # ride the fingerprint's diag check when the engine exposes
208
+ # them (libleptris 1.9.205+, #1200); otherwise the one SAX
209
+ # scan covers the parse-error banner (issue #130).
210
+ if n1 == n2
211
+ return true if Xml::DigestGate.recover_diags_available?
212
+
213
+ return !Xml::Sax.probe(n1).saw_error?
214
+ end
209
215
 
210
216
  left = Xml::Sax.probe(n1)
211
217
  return false if left.saw_error?
@@ -107,8 +107,14 @@ module Canon
107
107
  # case-insensitive (e.g., "UTF-8" equals "utf-8").
108
108
  # The standalone declaration in XML 1.1 is also case-insensitive.
109
109
  CASE_INSENSITIVE_ATTRS = %w[encoding standalone].freeze
110
- QUOTE_CHARS = ["\"", "'"].freeze
111
- SKIP_CHARS = [" ", "="].freeze
110
+ # Byte-level scan sets — the tokenizer reads via getbyte, whose
111
+ # Integer results allocate nothing (String#[] mints a 1-char
112
+ # string per character scanned).
113
+ NAME_END_BYTES = [32, 47, 62].freeze # space, /, >
114
+ SKIP_BYTES = [32, 61].freeze # space, =
115
+ QUOTE_BYTES = [34, 39].freeze # ", '
116
+ SPACE_BYTE = 32
117
+ EMPTY_CONTENT_RE = /\A[ \t\r\n\f\v]*\z/
112
118
 
113
119
  # Normalize attribute order within XML tags so that
114
120
  # <elem b="2" a="1"> compares equal to <elem a="1" b="2">.
@@ -121,19 +127,23 @@ module Canon
121
127
  i = 0
122
128
 
123
129
  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
130
+ lt = text.index("<", i)
131
+ if lt.nil?
132
+ result << text[i..]
133
+ break
134
+ end
135
+ result << text[i...lt] if lt > i
136
+
137
+ # Handle processing instruction <?...?>, comment <!--...-->,
138
+ # and regular tags
139
+ new_i, tag_output = process_tag(text, lt)
140
+ if new_i
141
+ result << tag_output
142
+ i = new_i
143
+ else
144
+ result << "<"
145
+ i = lt + 1
133
146
  end
134
-
135
- result << text[i]
136
- i += 1
137
147
  end
138
148
 
139
149
  result
@@ -178,57 +188,59 @@ module Canon
178
188
  # @param tag_content [String] Content between < and >
179
189
  # @return [Hash, nil] { name: String, attrs: Array<{name:, value:}> }
180
190
  def self.tokenize_tag_content(tag_content)
181
- return nil if tag_content.strip.empty?
191
+ return nil if tag_content.match?(EMPTY_CONTENT_RE)
182
192
 
183
193
  i = 0
194
+ length = tag_content.length
184
195
  # 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
196
+ i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
197
+ return nil if i >= length
187
198
 
188
199
  name_start = i
189
- i += 1 while i < tag_content.length && tag_content[i] != " " &&
190
- tag_content[i] != "/" && tag_content[i] != ">"
200
+ i += 1 while i < length &&
201
+ !NAME_END_BYTES.include?(tag_content.getbyte(i))
191
202
  tag_name = tag_content[name_start...i]
192
203
 
193
204
  # Skip whitespace
194
- i += 1 while i < tag_content.length && tag_content[i] == " "
205
+ i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
195
206
 
196
207
  # Parse attributes
197
208
  attrs = []
198
- while i < tag_content.length
209
+ while i < length
199
210
  # Skip whitespace
200
- i += 1 while i < tag_content.length && tag_content[i] == " "
201
- break if i >= tag_content.length
211
+ i += 1 while i < length && tag_content.getbyte(i) == SPACE_BYTE
212
+ break if i >= length
202
213
 
203
214
  # Read attribute name
204
215
  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
216
+ i += 1 while i < length &&
217
+ !NAME_END_BYTES.include?(tag_content.getbyte(i)) &&
218
+ tag_content.getbyte(i) != 61
219
+ break if i >= length || i == attr_start
209
220
 
210
221
  attr_name = tag_content[attr_start...i]
211
222
 
212
223
  # 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
224
+ i += 1 while i < length &&
225
+ SKIP_BYTES.include?(tag_content.getbyte(i))
226
+ break if i >= length
216
227
 
217
228
  # Read quoted value
218
- quote = tag_content[i]
219
- break unless QUOTE_CHARS.include?(quote)
229
+ quote = tag_content.getbyte(i)
230
+ break unless QUOTE_BYTES.include?(quote)
220
231
 
221
232
  i += 1
222
233
  value_start = i
223
- while i < tag_content.length && tag_content[i] != quote
234
+ while i < length && tag_content.getbyte(i) != quote
224
235
  i += 1
225
236
  end
226
- break if i >= tag_content.length
237
+ break if i >= length
227
238
 
228
239
  attr_value = tag_content[value_start...i]
229
240
  i += 1 # skip closing quote
230
241
 
231
- attrs << { name: attr_name, value: "#{quote}#{attr_value}#{quote}" }
242
+ quote_char = quote.chr
243
+ attrs << { name: attr_name, value: "#{quote_char}#{attr_value}#{quote_char}" }
232
244
  end
233
245
 
234
246
  { name: tag_name, attrs: attrs }
@@ -241,9 +253,9 @@ module Canon
241
253
  # @param i [Integer] Position of '<'
242
254
  # @return [Array(Integer, String), nil] [new_position, tag_string] or nil
243
255
  def self.process_tag(text, pos)
244
- if text[pos + 1] == "?"
256
+ if text.getbyte(pos + 1) == 63 # ?
245
257
  process_processing_instruction(text, pos)
246
- elsif text[pos + 1] == "!" && text[(pos + 2)...(pos + 4)] == "--"
258
+ elsif text.getbyte(pos + 1) == 33 && text[(pos + 2)...(pos + 4)] == "--" # !
247
259
  process_comment(text, pos)
248
260
  else
249
261
  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.60"
4
+ VERSION = "0.3.62"
5
5
  end
@@ -21,6 +21,28 @@ module Canon
21
21
  module DigestGate
22
22
  module_function
23
23
 
24
+ # libleptris 1.9.205+ records duplicate-attribute recover
25
+ # events on the parsed document (#1200): read through the
26
+ # FFI diag counter. Evaluated lazily at call time — a load-time
27
+ # constant would freeze before leptris loads (defined? never
28
+ # resolves pending autoloads). When the adapter is not leptris
29
+ # (or the binding predates the surface), the verbose lane
30
+ # falls back to the SAX probe for its error scan.
31
+ def recover_diags_available?
32
+ defined?(::Leptris::XML::FFI) &&
33
+ ::Leptris::XML::FFI.respond_to?(:leptris_document_parse_diag_count)
34
+ rescue StandardError
35
+ false
36
+ end
37
+
38
+ def recover_diags?(doc)
39
+ return false unless recover_diags_available?
40
+
41
+ native = doc.native
42
+ native = native.c_ptr if native.respond_to?(:c_ptr)
43
+ ::Leptris::XML::FFI.leptris_document_parse_diag_count(native).positive?
44
+ end
45
+
24
46
  def available?
25
47
  return false if RUBY_ENGINE == "opal"
26
48
  return false unless Canon::XmlBackend.moxml? &&
@@ -106,7 +128,11 @@ module Canon
106
128
  doc = context.parse(xml, readonly: true, strict: false)
107
129
  root = doc.root
108
130
  return nil unless root
109
- return nil if clean && doc.parse_errors.any?
131
+
132
+ if clean
133
+ return nil if doc.parse_errors.any?
134
+ return nil if recover_diags?(doc)
135
+ end
110
136
 
111
137
  skeleton = doc.children.filter_map do |child|
112
138
  next if Canon::XmlParsing.same_engine_node?(child, root)
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.60
4
+ version: 0.3.62
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