canon 0.3.67 → 0.3.69

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: 58c68ab8bd9ce6ae758ef8a9b2aeb69272dac07ef633e6d400dce01498e4a45a
4
- data.tar.gz: d118362c2a39b59d6739c67623b31d8a957c093e8b280360be969643041b15f2
3
+ metadata.gz: 4f23fa758538acdbe352d58250d880b04e905191ce3c44b30528580cee06f224
4
+ data.tar.gz: c33b27f2839764f6d91fded3b3d7cb2042dc7e91d1d7a630037bcda0ce645c37
5
5
  SHA512:
6
- metadata.gz: dc4582dc42ff629f85115a37e5456f6339ec7fe27f6e9b568fa6af1096d42c362aa5eb6bd767ceb40fb8f2e0f781cf642cfecd454631e34e937961e5c5b3ca45
7
- data.tar.gz: d7c4b776c8720cd9c04bbc0b002d9a355ce23ea57668bb42df2bb7c3224a2a654776ff67ed8c0ad6b0c74a6173557ef650bb7a5f3f1178e042d379c8f833b6a6
6
+ metadata.gz: 6137233af91995ea5956d35fa154d2c206557e30c7fd9d1ced455667560f05810a034c4a1c407e4635fdbaefe8ad9929ad5b28cdf4cf5118990d83865611c4a3
7
+ data.tar.gz: 16a631574442c56fc87e5a48851643630ba9342d7e06f4b0b938f28a24792d5b414884099ec7c767fedb067971cc6ab82cf48871d55a1d1551f9cc5d7886e868
@@ -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]
data/lib/canon/cache.rb CHANGED
@@ -24,7 +24,14 @@ module Canon
24
24
  # Maximum number of entries per cache category
25
25
  MAX_CACHE_SIZE = 100
26
26
 
27
- # Fetch a value from cache, or compute and cache it
27
+ LOCK = Mutex.new
28
+
29
+ # Fetch a value from cache, or compute and cache it.
30
+ #
31
+ # The lock covers the cache operations only — never the compute
32
+ # block — so concurrent parses stay parallel; duplicate computes
33
+ # for the same key may race (last write wins), which any
34
+ # check-then-compute cache accepts.
28
35
  #
29
36
  # @param category [Symbol] Cache category (:document_parse, :format_detect, etc.)
30
37
  # @param key [String] Cache key
@@ -33,24 +40,24 @@ module Canon
33
40
  def fetch(category, key)
34
41
  cache = cache_for(category)
35
42
 
36
- # Check if key exists
37
- if cache.key?(key)
38
- # Update access time for LRU
39
- cache[key][:accessed] = Time.now
40
- return cache[key][:value]
43
+ LOCK.synchronize do
44
+ if cache.key?(key)
45
+ @clock = (@clock || 0) + 1
46
+ cache[key][:accessed] = @clock
47
+ return cache[key][:value]
48
+ end
41
49
  end
42
50
 
43
- # Compute and cache the value
44
51
  value = yield
45
52
 
46
- # Evict oldest entry if cache is full
47
- if cache.size >= MAX_CACHE_SIZE
48
- oldest_key = cache.min_by { |_, v| v[:accessed] }&.first
49
- cache.delete(oldest_key) if oldest_key
53
+ LOCK.synchronize do
54
+ if cache.size >= MAX_CACHE_SIZE
55
+ oldest_key = cache.min_by { |_, v| v[:accessed] }&.first
56
+ cache.delete(oldest_key) if oldest_key
57
+ end
58
+ @clock = (@clock || 0) + 1
59
+ cache[key] = { value: value, accessed: @clock }
50
60
  end
51
-
52
- @clock = (@clock || 0) + 1
53
- cache[key] = { value: value, accessed: @clock }
54
61
  value
55
62
  end
56
63
 
@@ -58,8 +65,10 @@ module Canon
58
65
  #
59
66
  # Useful for tests or when memory needs to be freed
60
67
  def clear_all
61
- @caches&.each_value(&:clear)
62
- @caches = nil
68
+ LOCK.synchronize do
69
+ @caches&.each_value(&:clear)
70
+ @caches = nil
71
+ end
63
72
  end
64
73
 
65
74
  # Clear a specific cache category
@@ -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
 
@@ -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.67"
4
+ VERSION = "0.3.69"
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.67
4
+ version: 0.3.69
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-22 00:00:00.000000000 Z
11
+ date: 2026-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs