canon 0.3.54 → 0.3.55

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: c8094be0b06c7cd2a74e5e5fa4085f2a3ac654c54ed9ec106fcfa930490d4d11
4
- data.tar.gz: 71c81085b65c0648b059a9d6a4d12663d71867b2f032f42e6ed95df2c38fcb26
3
+ metadata.gz: 50a34a46b082f7aa0555199b09780ec1e8c5f38f76656e2a6899a54b3b96a90f
4
+ data.tar.gz: f2b7cbf27874cc6bbd6ab5615a14ad43a2ea6cb7e8ebdd9a3894efcc112c103c
5
5
  SHA512:
6
- metadata.gz: 2a2401a1455182a4c4dc9b553fdbc51f6d63b4a9153ecf99f248bbf42fe43ac2f1ee801df0e22e52aebd1d4bfbecd0409e39d35e4fa02f440de7c674ccb44777
7
- data.tar.gz: 894d3d399acd98b30c8dbd6da4fcce1915f12355347e5df6f77373c37fc7a9d7a2cfd21bbafbe53617d4ab6eb0e37a0820091ce61850554da25b5741a25945e1
6
+ metadata.gz: 5474c6581c262c497d51cebfe91b0882a096a3340acfd7f7c77d67ca064d12164cbc423b6c4865138a80a660115755597672c3e7a1dbe380ddf24f94e8c55d0d
7
+ data.tar.gz: 8dac986a479c7820ec6d780c116d906ef34489cbfdf15f19ebad9f013c0ec90070ad75801471e4cd8e4be4f1e9c62faaeaa8b97d42a296286f1c0842ce9d8d9b
@@ -82,14 +82,29 @@ module Canon
82
82
  # parse-time strip. Callers whose whitespace semantics
83
83
  # differ from that skip the gate: strict attribute order
84
84
  # (invisible to the digest), user-configured whitespace
85
- # element lists, xml:space documents (checked inside the
86
- # gate), and verbose callers who need the report.
87
- if !(opts[:verbose] ||
88
- match_opts_hash[:attribute_order] == :strict ||
85
+ # element lists, and xml:space documents (checked inside the
86
+ # gate). Attribute order is invisible to the digest, so
87
+ # :strict always falls through for the verdict itself.
88
+ # Verbose reports additionally carry two digest-invisible
89
+ # entries: informative attribute-order DiffNodes (emitted
90
+ # even under :ignore for order-differing documents) and the
91
+ # SAX lane's parse-error banner (issue #130). The verbose
92
+ # lane therefore re-scans both sides through the SAX probe —
93
+ # identical attribute-order signatures and no recover errors
94
+ # prove both entries empty, and a hit returns the same shape
95
+ # the pipeline builds for equivalent documents (empty
96
+ # differences, deferred display strings) without the two
97
+ # full parses and the walk.
98
+ if !(match_opts_hash[:attribute_order] == :strict ||
89
99
  match_opts_hash[:preserve_whitespace_elements] ||
90
100
  match_opts_hash[:collapse_whitespace_elements] ||
91
101
  match_opts_hash[:strip_whitespace_elements]) &&
92
- Xml::DigestGate.equal?(n1, n2)
102
+ Xml::DigestGate.certify(n1, n2) &&
103
+ verbose_report_proven_empty?(n1, n2, opts)
104
+ if opts[:verbose]
105
+ return build_digest_equivalent_result(n1, n2, match_opts_hash)
106
+ end
107
+
93
108
  return true
94
109
  end
95
110
 
@@ -133,12 +148,12 @@ module Canon
133
148
  # Serialize parsed nodes for consistent formatting
134
149
  # This ensures both sides formatted identically, showing only real
135
150
  # differences. Deferred: by_object consumers never read these.
136
- preprocessed = lambda do
137
- [
138
- serialize_node(node1).gsub("><", ">\n<"),
139
- serialize_node(node2).gsub("><", ">\n<"),
140
- ]
141
- end
151
+ # node1/node2 are parsed Canon nodes, so the deferred
152
+ # re-parse inside is a pass-through.
153
+ preprocessed = deferred_preprocessed(
154
+ node1, node2,
155
+ match_opts_hash[:preprocessing], preserve_whitespace
156
+ )
142
157
 
143
158
  ComparisonResult.new(
144
159
  differences: differences,
@@ -180,20 +195,71 @@ module Canon
180
195
  # @param n1 [Object] First input
181
196
  # @param n2 [Object] Second input
182
197
  # @param opts [Hash] Raw options (before merge with DEFAULT_OPTS)
198
+ # The verbose-report half of the gate: the digest cannot see
199
+ # attribute order (informative DiffNodes) or SAX recover
200
+ # errors (parse-error banner). One tree-free probe scan per
201
+ # side answers both; any hit falls through to the pipeline.
202
+ def verbose_report_proven_empty?(n1, n2, opts)
203
+ return true unless opts[:verbose]
204
+ # 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
209
+
210
+ left = Xml::Sax.probe(n1)
211
+ return false if left.saw_error?
212
+
213
+ right = Xml::Sax.probe(n2)
214
+ !right.saw_error? && left.signature == right.signature
215
+ end
216
+
217
+ # Digest-proven equivalence for verbose callers. The report is
218
+ # indistinguishable from the full pipeline's for such inputs —
219
+ # certify guarantees no recover errors, the digest guarantees
220
+ # no differences — with display strings deferred exactly like
221
+ # the pipeline's lazy lane.
222
+ #
223
+ # @return [ComparisonResult]
224
+ def build_digest_equivalent_result(n1, n2, match_opts_hash)
225
+ preserve_whitespace = match_opts_hash[:structural_whitespace] == :strict
226
+ ComparisonResult.new(
227
+ differences: [],
228
+ preprocessed_strings: deferred_preprocessed(
229
+ n1, n2,
230
+ match_opts_hash[:preprocessing], preserve_whitespace
231
+ ),
232
+ original_strings: [n1, n2],
233
+ format: :xml,
234
+ match_options: match_opts_hash,
235
+ algorithm: :dom,
236
+ )
237
+ end
238
+
239
+ # Deferred display-string pair. Inputs may be raw strings
240
+ # (trivial/digest paths — parsed on materialization with the
241
+ # caller's preprocessing) or already-parsed Canon nodes
242
+ # (compare path — NodeParser passes them through unchanged).
243
+ #
244
+ # @return [Proc] Array<String, String> on call
245
+ def deferred_preprocessed(n1, n2, preprocessing, preserve_whitespace)
246
+ lambda do
247
+ [
248
+ serialize_node(parse_node(n1, preprocessing,
249
+ preserve_whitespace: preserve_whitespace)).gsub("><", ">\n<"),
250
+ serialize_node(parse_node(n2, preprocessing,
251
+ preserve_whitespace: preserve_whitespace)).gsub("><", ">\n<"),
252
+ ]
253
+ end
254
+ end
255
+
183
256
  # @return [Boolean, ComparisonResult]
184
257
  def build_trivial_equivalent_result(n1, n2, opts)
185
258
  return true unless opts[:verbose]
186
259
 
187
260
  # Parse nodes for verbose display — deferred with the strings:
188
261
  # nothing but the display pair ever reads them.
189
- preprocessed = lambda do
190
- [
191
- serialize_node(parse_node(n1, :none,
192
- preserve_whitespace: true)).gsub("><", ">\n<"),
193
- serialize_node(parse_node(n2, :none,
194
- preserve_whitespace: true)).gsub("><", ">\n<"),
195
- ]
196
- end
262
+ preprocessed = deferred_preprocessed(n1, n2, :none, true)
197
263
  original1 = n1.is_a?(String) ? n1 : serialize_node(n1)
198
264
  original2 = n2.is_a?(String) ? n2 : serialize_node(n2)
199
265
 
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.54"
4
+ VERSION = "0.3.55"
5
5
  end
@@ -71,11 +71,42 @@ module Canon
71
71
  end
72
72
  end
73
73
 
74
+ # The shared fingerprint ([root digest, doc-level skeleton])
75
+ # when both documents digest identically AND both parse clean of
76
+ # recover errors; nil otherwise. The verbose lane certifies
77
+ # before skipping the pipeline: error-bearing documents decline
78
+ # so the pipeline can produce the identical verbose report
79
+ # (parse-error banner included) it always has.
80
+ def certify(xml1, xml2)
81
+ return nil unless xml1.is_a?(String) && xml2.is_a?(String)
82
+
83
+ begin
84
+ return nil if xml1.include?("xml:space") || xml2.include?("xml:space")
85
+ rescue Encoding::CompatibilityError
86
+ return nil
87
+ end
88
+
89
+ context = Canon::XmlParsing.moxml_context
90
+ begin
91
+ left = fingerprint(context, xml1, clean: true)
92
+ right = fingerprint(context, xml2, clean: true)
93
+ return nil if left.nil? || left != right
94
+
95
+ left
96
+ rescue StandardError
97
+ nil
98
+ end
99
+ end
100
+
74
101
  # [root digest, doc-level skeleton] or nil when unparseable.
75
- def fingerprint(context, xml)
102
+ # With `clean:` a document carrying recover errors also answers
103
+ # nil — only certify uses that; the boolean lane keeps the
104
+ # historical verdict for recovered-equal pairs.
105
+ def fingerprint(context, xml, clean: false)
76
106
  doc = context.parse(xml, readonly: true, strict: false)
77
107
  root = doc.root
78
108
  return nil unless root
109
+ return nil if clean && doc.parse_errors.any?
79
110
 
80
111
  skeleton = doc.children.filter_map do |child|
81
112
  next if Canon::XmlParsing.same_engine_node?(child, root)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ module Sax
6
+ # Tree-free scan stand-in for the builder protocol. Records
7
+ # recover errors and accumulates an attribute-name-order
8
+ # signature (element open/close tokens plus each non-xmlns
9
+ # attribute name in document order); content itself is ignored —
10
+ # the digest gate covers it. The verbose lane compares the two
11
+ # sides' signatures to prove the absence of the two
12
+ # digest-invisible report entries: informative attribute-order
13
+ # DiffNodes and the SAX-lane parse-error banner (issue #130).
14
+ class Probe
15
+ attr_reader :signature
16
+
17
+ def initialize
18
+ @saw_error = false
19
+ @signature = +""
20
+ end
21
+
22
+ def saw_error?
23
+ @saw_error
24
+ end
25
+
26
+ def error(_string)
27
+ @saw_error = true
28
+ end
29
+
30
+ def warning(_string); end
31
+
32
+ def start_element(name, attrs = [])
33
+ @signature << "/" << name << "\0"
34
+ attrs.each do |attr|
35
+ attr_name = attr[0].to_s
36
+ next if attr_name == "xmlns" || attr_name.start_with?("xmlns:")
37
+
38
+ @signature << "@" << attr_name << "\0"
39
+ end
40
+ end
41
+
42
+ def end_element(_name)
43
+ @signature << "\\"
44
+ end
45
+
46
+ def characters(_string); end
47
+
48
+ def cdata_block(_string); end
49
+
50
+ def comment(_string); end
51
+
52
+ def processing_instruction(_name, _content); end
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/canon/xml/sax.rb CHANGED
@@ -20,6 +20,7 @@ module Canon
20
20
  module Sax
21
21
  autoload :NokogiriDriver, "canon/xml/sax/nokogiri_driver"
22
22
  autoload :MoxmlDriver, "canon/xml/sax/moxml_driver"
23
+ autoload :Probe, "canon/xml/sax/probe"
23
24
 
24
25
  class << self
25
26
  # Drive `builder` with the runtime's SAX engine.
@@ -31,6 +32,19 @@ module Canon
31
32
  end
32
33
  nil
33
34
  end
35
+
36
+ # Tree-free scan through the runtime's SAX engine: answers
37
+ # recover errors and an attribute-name-order signature for
38
+ # `xml_string` without building anything.
39
+ def probe(xml_string)
40
+ probe = Probe.new
41
+ if RUBY_ENGINE == "opal" || Canon::XmlParsing.moxml_adapter_name != :nokogiri
42
+ MoxmlDriver.new(probe).parse(xml_string)
43
+ else
44
+ NokogiriDriver.new(probe).parse(xml_string)
45
+ end
46
+ probe
47
+ end
34
48
  end
35
49
  end
36
50
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.54
4
+ version: 0.3.55
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -437,6 +437,7 @@ files:
437
437
  - lib/canon/xml/sax.rb
438
438
  - lib/canon/xml/sax/moxml_driver.rb
439
439
  - lib/canon/xml/sax/nokogiri_driver.rb
440
+ - lib/canon/xml/sax/probe.rb
440
441
  - lib/canon/xml/sax_builder.rb
441
442
  - lib/canon/xml/tree_builder.rb
442
443
  - lib/canon/xml/whitespace_normalizer.rb