canon 0.3.66 → 0.3.68

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: 04a82e16556af6e2b31b4a137e5beef44d499df0d20a019f5406c9c127e94207
4
- data.tar.gz: 998418423acb82ccca419c02b4f7c0abf6574e3ba9e370a9d463b51ae3409d76
3
+ metadata.gz: 590320aa865898c6342d5c96566e6313e678a6d6f31a4d1e9463a50c9f434052
4
+ data.tar.gz: 6daacd998c4d52ae54096bc91329cd48686d33b7942970313891f8d69edf2141
5
5
  SHA512:
6
- metadata.gz: 259638f0ce661b7c766787071fa5b6174e3a0f2ca5d0b027375717e1f191315c44d10308819fd7d17ee019fde9d7418f01c8f13614c9c83154616ca4845c9eff
7
- data.tar.gz: 47eca8ccd4000c0f22712818d8e72d7e7f4e37f5f523db0a59ff7441c119ff1ae5e29f4c5bef58c04831ee5b65e9bea7283c064bb4008a5732080cdb80f2ca9c
6
+ metadata.gz: 5d9fe69844ee374debb42557852bdcfd3d8cd3cadb30d15f01c2e2b932ad7412339107996bb2c88f04669951b88752856c56b812d2fd4ecbd3f993390b682efe
7
+ data.tar.gz: bfee82458261d1b20f78d7745da3cf620ce9beff2a993c01edcf6ff1d22f2538e8ddf7c45f21237e64f41a0218d0a25432dffcf0ea6cbcff3443cd416d4a677b
data/README.adoc CHANGED
@@ -21,10 +21,9 @@ Key features:
21
21
  installed — Nokogiri otherwise and for HTML; both byte-compatible, engine
22
22
  parity is spec-gated
23
23
  ** JSON on https://github.com/leptris/yeptris[libyeptris]' strict JSON surface
24
- whenever the native materializer is installed (`gem install yeptris` —
25
- platform gems, zero compilation), the stdlib JSON extension otherwise
26
- ** YAML on yeptris opt-in (`CANON_YAML_BACKEND=yeptris`, ~3x Psych loads);
27
- Psych otherwise
24
+ (yeptris is a required gem — platform gems, zero compilation)
25
+ ** YAML on yeptris (default — byte-parity with Psych, gated by the dump
26
+ corpus in the suite); Psych via `CANON_YAML_BACKEND=psych`
28
27
 
29
28
  == When to use formatting vs comparison
30
29
 
@@ -486,6 +486,33 @@ Canon::Comparison.equivalent?(html1, html2,
486
486
 
487
487
  `:strict`:: Namespace URIs must match (default and only supported behavior)
488
488
 
489
+ === namespace_prefix
490
+
491
+ **Applies to**: XML only
492
+
493
+ **Purpose**: Controls whether namespace *prefix spelling* is significant.
494
+ Per XML Namespaces, the prefix is a binding convenience — `xmlns:ns="http://x.example"`
495
+ and `xmlns:m="http://x.example"` bind to the same URI, so `<ns:e>` and `<m:e>` are the
496
+ same `{uri}local` name. Documents that round-trip through XML tooling (Office,
497
+ serializers) often churn prefixes; with `:ignore`, such documents compare
498
+ equivalent as long as the *set of declared URIs* matches. Namespace
499
+ declarations that resolve differently still report differences.
500
+
501
+ **Behaviors**:
502
+
503
+ `:significant`:: Prefix spelling matters — `xmlns:ns` and `xmlns:m` are
504
+ different declarations (default; `strict` and `rendered` profiles)
505
+
506
+ `:ignore`:: Only the set of declared namespace URIs matters
507
+ (`spec_friendly` and `content_only` profiles)
508
+
509
+ [source,ruby]
510
+ ----
511
+ Canon::Comparison.equivalent?(xml1, xml2,
512
+ format: :xml,
513
+ match: { namespace_prefix: :ignore })
514
+ ----
515
+
489
516
  **Note**: This dimension is always `:strict` for XML. Namespace prefixes are not significant - only the namespace URI matters. Elements with different prefixes but the same namespace URI are considered equivalent.
490
517
 
491
518
  .Namespace URI comparison
@@ -601,6 +628,12 @@ Each format has sensible defaults based on typical usage:
601
628
  |—
602
629
  |`:strict`
603
630
 
631
+ |`namespace_prefix`
632
+ |`:significant`
633
+ |`:significant`
634
+ |—
635
+ |—
636
+
604
637
  |`namespace_uri`
605
638
  |`:strict`
606
639
  |—
@@ -9,6 +9,29 @@ nav_order: 1
9
9
 
10
10
  Canon's 4-layer architecture provides powerful flexibility, but this can be overwhelming. This guide helps you choose the right configuration for your use case through decision trees, use case scenarios, and practical recommendations.
11
11
 
12
+ == XML namespace prefixes
13
+
14
+ If your documents round-trip through XML tooling (Office files, serializers,
15
+ XSLT processors), namespace *prefixes* often churn while the resolved names
16
+ stay identical (`xmlns:ns="http://x.example"` vs `xmlns:m="http://x.example"`). Canon's
17
+ default treats prefix spelling as significant; choose `:ignore` when the
18
+ binding, not the spelling, is what matters:
19
+
20
+ [source,ruby]
21
+ ----
22
+ # Per comparison
23
+ Canon::Comparison.equivalent?(a, b,
24
+ format: :xml, match: { namespace_prefix: :ignore })
25
+
26
+ # Or per suite via config
27
+ Canon::Config.instance.xml.match.options[:namespace_prefix] = :ignore
28
+ ----
29
+
30
+ The `spec_friendly` and `content_only` profiles already set
31
+ `namespace_prefix: :ignore`. Canonicalization output (`Canon.format`,
32
+ C14N) never rewrites prefixes regardless of this option — canonical bytes
33
+ stay spec-conformant.
34
+
12
35
  == Quick Decision Tree
13
36
 
14
37
  [mermaid]
@@ -56,6 +56,23 @@ parse_errors_expected: nil, parse_errors_received: nil)
56
56
  @original_strings || preprocessed_strings
57
57
  end
58
58
 
59
+ # The lazy display-string builder is a Proc, which Marshal (and
60
+ # every serializing test-distribution tool) cannot dump.
61
+ # Materialize first — the restored copy carries plain strings
62
+ # and round-trips exactly like the pre-lazy results.
63
+ def marshal_dump
64
+ preprocessed_strings
65
+ [@differences, @preprocessed_strings, @original_strings,
66
+ @format, @html_version, @match_options, @algorithm,
67
+ @parse_errors_expected, @parse_errors_received]
68
+ end
69
+
70
+ def marshal_load(data)
71
+ @differences, @preprocessed_strings, @original_strings,
72
+ @format, @html_version, @match_options, @algorithm,
73
+ @parse_errors_expected, @parse_errors_received = data
74
+ end
75
+
59
76
  # Whether either side reported parse errors. Used by the diff
60
77
  # formatter to decide whether to render the parse-error banner.
61
78
  #
@@ -44,6 +44,10 @@ module Canon
44
44
  name: :comments,
45
45
  valid_behaviors: %i[strict ignore],
46
46
  ),
47
+ Dimension.new(
48
+ name: :namespace_prefix,
49
+ valid_behaviors: %i[significant ignore],
50
+ ),
47
51
  ]),
48
52
 
49
53
  json: DimensionSet.new(:json, [
@@ -23,6 +23,7 @@ module Canon
23
23
  element_position: :ignore,
24
24
  comments: :ignore,
25
25
  whitespace_type: :strict,
26
+ namespace_prefix: :significant,
26
27
  },
27
28
  xml: {
28
29
  preprocessing: :none,
@@ -34,6 +35,7 @@ module Canon
34
35
  element_position: :strict,
35
36
  comments: :strict,
36
37
  whitespace_type: :strict,
38
+ namespace_prefix: :significant,
37
39
  },
38
40
  }.freeze
39
41
 
@@ -49,6 +51,7 @@ module Canon
49
51
  element_position: :strict,
50
52
  comments: :strict,
51
53
  whitespace_type: :strict,
54
+ namespace_prefix: :significant,
52
55
  },
53
56
 
54
57
  rendered: {
@@ -56,11 +59,12 @@ module Canon
56
59
  text_content: :normalize,
57
60
  structural_whitespace: :normalize,
58
61
  attribute_presence: :strict,
59
- attribute_order: :strict,
62
+ attribute_order: :ignore,
60
63
  attribute_values: :strict,
61
64
  element_position: :strict,
62
65
  comments: :ignore,
63
66
  whitespace_type: :strict,
67
+ namespace_prefix: :significant,
64
68
  },
65
69
 
66
70
  html4: {
@@ -97,6 +101,7 @@ module Canon
97
101
  element_position: :ignore,
98
102
  comments: :ignore,
99
103
  whitespace_type: :strict,
104
+ namespace_prefix: :ignore,
100
105
  },
101
106
 
102
107
  content_only: {
@@ -109,6 +114,7 @@ module Canon
109
114
  element_position: :ignore,
110
115
  comments: :ignore,
111
116
  whitespace_type: :strict,
117
+ namespace_prefix: :ignore,
112
118
  },
113
119
  }.freeze
114
120
 
@@ -122,6 +122,23 @@ module Canon
122
122
  end
123
123
 
124
124
  # Process ElementMatcher results
125
+ # Position differences carry element identity — a bare
126
+ # "position 0 vs position 1" next to identical-looking
127
+ # Expected/Actual payloads reads as a canon false positive
128
+ # when the real change is sibling order (e.g. id="1" and
129
+ # id="2" swapped).
130
+ def position_reason(elem, pos)
131
+ return "position #{pos}" if elem.nil?
132
+
133
+ label = elem.name.to_s
134
+ attrs = Canon::Diff::NodeSerializer.extract_attributes(elem)
135
+ return "position #{pos}" if attrs.nil? || attrs.empty?
136
+
137
+ id_like = %w[id href name].filter_map { |k| attrs[k] && "#{k}=#{attrs[k]}" }
138
+ id_like << attrs.first.then { |k, v| "#{k}=#{v}" } if id_like.empty?
139
+ "#{label}[#{id_like.first}] at position #{pos}"
140
+ end
141
+
125
142
  def process_matches(matches, _children1, _children2, _parent_node, comparator,
126
143
  opts, child_opts, diff_children, differences)
127
144
  all_equivalent = true
@@ -137,7 +154,8 @@ module Canon
137
154
  # Only create DiffNode if element_position is not :ignore
138
155
  if position_behavior != :ignore
139
156
  comparator.add_difference(match.elem1, match.elem2,
140
- "position #{match.pos1}", "position #{match.pos2}",
157
+ position_reason(match.elem1, match.pos1),
158
+ position_reason(match.elem2, match.pos2),
141
159
  :element_position, opts, differences)
142
160
  all_equivalent = false if position_behavior == :strict
143
161
  end
@@ -20,6 +20,22 @@ module Canon
20
20
  # Most element pairs declare nothing — skip the set algebra.
21
21
  return Comparison::EQUIVALENT if ns_decls1.empty? && ns_decls2.empty?
22
22
 
23
+ # namespace_prefix: :ignore — prefix spelling is cosmetic;
24
+ # what resolves differently is the set of declared URIs
25
+ # (XML Namespaces: the prefix is a binding convenience, the
26
+ # {uri}local name is the name).
27
+ if opts[:match_opts][:namespace_prefix] == :ignore
28
+ uris1 = ns_decls1.values.uniq.sort
29
+ uris2 = ns_decls2.values.uniq.sort
30
+ return Comparison::EQUIVALENT if uris1 == uris2
31
+
32
+ missing = (ns_decls1.values.uniq - ns_decls2.values).sort
33
+ extra = (ns_decls2.values.uniq - ns_decls1.values).sort
34
+ add_namespace_uri_difference(node1, node2, missing, extra,
35
+ opts, differences)
36
+ return Comparison::UNEQUAL_ATTRIBUTES
37
+ end
38
+
23
39
  # Find missing, extra, and changed namespace declarations
24
40
  missing = ns_decls1.keys - ns_decls2.keys # In node1 but not node2
25
41
  extra = ns_decls2.keys - ns_decls1.keys # In node2 but not node1
@@ -125,6 +141,27 @@ module Canon
125
141
  Canon::Xml::NamespaceHelper.namespace_declaration?(attr_name)
126
142
  end
127
143
 
144
+ # URI-flavored difference for namespace_prefix: :ignore —
145
+ # reports the URIs that resolve differently, not the prefix
146
+ # spellings.
147
+ def self.add_namespace_uri_difference(node1, node2, missing, extra,
148
+ opts, differences)
149
+ reasons = []
150
+ reasons << "removed URIs: #{missing.join(', ')}" if missing.any?
151
+ reasons << "added URIs: #{extra.join(', ')}" if extra.any?
152
+
153
+ diff_node = Canon::Comparison::DiffNodeBuilder.build(
154
+ node1: node1,
155
+ node2: node2,
156
+ diff1: Comparison::UNEQUAL_ATTRIBUTES,
157
+ diff2: Comparison::UNEQUAL_ATTRIBUTES,
158
+ dimension: :namespace_declarations,
159
+ **opts,
160
+ )
161
+ diff_node.reason = reasons.join("; ") if diff_node && reasons.any?
162
+ differences << diff_node if diff_node
163
+ end
164
+
128
165
  # Add a namespace declaration difference
129
166
  #
130
167
  # @param node1 [Object] First node
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.66"
4
+ VERSION = "0.3.68"
5
5
  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.66
4
+ version: 0.3.68
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.