canon 0.3.14 → 0.3.15

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: d9954ac6056439abc3c5b211ecf17b9af2391ad0b809eb3c239fef55cbdc723a
4
- data.tar.gz: dbe215e817f160c840769b5bbd0969fe83d315d2b4429cec125de43312ec821a
3
+ metadata.gz: d48fa5b4488a85c95b613bcab72881a5e286fdb9859da76aafc78553ff506419
4
+ data.tar.gz: f0bd3b76b81a0181dc4ae801bed2288828f8d8bc85a6414a98c993d10d770b7a
5
5
  SHA512:
6
- metadata.gz: b1552e6f9a7735903f10b8f73902c6896cfab41b61da29b9bf725c6d5a1132483566eec58eeeb5ce2a59714a5e228ba8cb2979fa79f853cf5bd361f5ffcd849f
7
- data.tar.gz: c2b93d2248033a3680164769ab57163f44935314a0929155fa98e2a488f1ea278bff8dba5bbdc38b19d313db13e9ddc6ffeb8b56cdf7537d1ba5c08098ec9bb2
6
+ metadata.gz: 1b32e0477c8932748179074f076478747d3eb1bce3892e6d7fd8ecae3e1dc7e686fa51907cdbd6444ad87267eb9521206b4caaa6f2d85a93b36f5cc2b3758e4a
7
+ data.tar.gz: 11c89d97b5144a557746e7d84e9128db443e8951fc40381486da826935f012ccc15249de122232a0ca95b32b5c32ebffe64a2415b6db8f8b9790af2e1eda1b10
@@ -70,6 +70,10 @@ module Canon
70
70
  when :strict
71
71
  text1 == text2
72
72
  when :normalize
73
+ # Identical strings normalize identically — skip the two
74
+ # gsub+strip chains.
75
+ return true if text1 == text2
76
+
73
77
  if whitespace_type == :normalize
74
78
  normalize_text(text1) == normalize_text(text2)
75
79
  else
@@ -92,22 +96,34 @@ module Canon
92
96
  .strip # Remove leading/trailing whitespace
93
97
  end
94
98
 
95
- # Normalize text preserving Unicode whitespace type distinctions.
96
99
  # Fast form of `normalize_text(text).empty?`, called per text
97
100
  # node by node_excluded?. Pure-ASCII whitespace (the common
98
101
  # case — pretty-print indentation) matches a plain class with
99
102
  # no intermediate strings and no \p{} property (Opal's JS
100
- # regexes do not honor \p{Space}); anything else falls back to
101
- # normalize_text's exact semantics. NUL is included because
102
- # String#strip strips nulls too.
103
+ # regexes do not honor \p{Space}); a pure-ASCII miss is
104
+ # conclusively non-whitespace; only non-ASCII text falls back
105
+ # to normalize_text's exact Unicode semantics. NUL is included
106
+ # because String#strip strips nulls too.
103
107
  ASCII_WHITESPACE_ONLY = /\A[ \t\r\n\v\f\x00]*\z/
108
+ ASCII_ONLY = /\A[\x00-\x7f]*\z/
104
109
 
105
110
  def whitespace_only?(text)
106
111
  text = text.to_s
107
- text.empty? || text.match?(ASCII_WHITESPACE_ONLY) ||
108
- normalize_text(text).empty?
112
+ return true if text.empty?
113
+ return true if text.match?(ASCII_WHITESPACE_ONLY)
114
+ # A pure-ASCII string that failed the class contains an ASCII
115
+ # non-whitespace character, which survives both the collapse
116
+ # and the strip — conclusively not whitespace-only, with no
117
+ # intermediate strings. Only non-ASCII text (NBSP, U+3000,
118
+ # ...) needs normalize_text's exact Unicode semantics — and
119
+ # no \p{} classes appear in the fast paths, which Opal's JS
120
+ # regexes would not honor anyway.
121
+ return false if text.match?(ASCII_ONLY)
122
+
123
+ normalize_text(text).empty?
109
124
  end
110
125
 
126
+ # Normalize text preserving Unicode whitespace type distinctions.
111
127
  def normalize_text_preserving_type(text)
112
128
  return "" if text.nil?
113
129
 
@@ -26,6 +26,14 @@ module Canon
26
26
  match_opts = opts[:match_opts]
27
27
  attribute_order_behavior = match_opts[:attribute_order] || :strict
28
28
 
29
+ # FAST PATH: identical filtered attributes in identical key
30
+ # order — exact equality implies behavioral equality for every
31
+ # value behavior, and no order difference exists. Most element
32
+ # pairs hit this and skip the sort/hash/value work entirely.
33
+ if attrs1 == attrs2 && attrs1.keys == attrs2.keys
34
+ return Comparison::EQUIVALENT
35
+ end
36
+
29
37
  # Check attribute order if not ignored
30
38
  keys1 = attrs1.keys.map(&:to_s)
31
39
  keys2 = attrs2.keys.map(&:to_s)
@@ -17,6 +17,9 @@ module Canon
17
17
  ns_decls1 = extract_declarations(node1)
18
18
  ns_decls2 = extract_declarations(node2)
19
19
 
20
+ # Most element pairs declare nothing — skip the set algebra.
21
+ return Comparison::EQUIVALENT if ns_decls1.empty? && ns_decls2.empty?
22
+
20
23
  # Find missing, extra, and changed namespace declarations
21
24
  missing = ns_decls1.keys - ns_decls2.keys # In node1 but not node2
22
25
  extra = ns_decls2.keys - ns_decls1.keys # In node2 but not node1
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.14"
4
+ VERSION = "0.3.15"
5
5
  end
@@ -4,6 +4,13 @@ module Canon
4
4
  module Xml
5
5
  # Base class for all XPath data model nodes
6
6
  class Node
7
+ # Shared by every childless node: leaves (text, attributes,
8
+ # namespaces, comments, PIs) and empty elements read `children`
9
+ # without materializing a private array. Mutating the returned
10
+ # array is a bug — writers go through add_child/children=, which
11
+ # install a private array first.
12
+ EMPTY_CHILDREN = [].freeze
13
+
7
14
  attr_reader :parent
8
15
 
9
16
  def initialize
@@ -12,12 +19,8 @@ module Canon
12
19
  @in_node_set = true
13
20
  end
14
21
 
15
- # Leaf nodes (text, attributes, namespaces, comments, PIs) never
16
- # gain children; allocating the array eagerly cost one Array per
17
- # node — the single largest retained-allocation source in a built
18
- # tree — so it materializes on first add or read.
19
22
  def children
20
- @children ||= []
23
+ @children || EMPTY_CHILDREN
21
24
  end
22
25
 
23
26
  def children=(new_children)
@@ -26,7 +29,7 @@ module Canon
26
29
 
27
30
  def add_child(child)
28
31
  child.parent = self
29
- children << child
32
+ (@children ||= []) << child
30
33
  end
31
34
 
32
35
  def in_node_set?
@@ -15,12 +15,16 @@ module Canon
15
15
  @attribute_nodes = nil
16
16
  end
17
17
 
18
- # Lazy: most elements carry no attributes, and inherited
19
- # namespace nodes arrive as one shared frozen array (see
20
- # TreeBuilder#attach_namespace_scope) eager per-element arrays
21
- # were the largest retained-allocation source in a built tree.
18
+ # Lazy: attribute-free elements (common in real documents) share
19
+ # one frozen empty array on read instead of materializing one
20
+ # per element; writers install a private array first. Inherited
21
+ # namespace nodes arrive as one shared frozen array from
22
+ # TreeBuilder#attach_namespace_scope.
23
+ EMPTY_ATTRIBUTE_NODES = [].freeze
24
+ EMPTY_NAMESPACE_NODES = [].freeze
25
+
22
26
  def namespace_nodes
23
- @namespace_nodes ||= []
27
+ @namespace_nodes || EMPTY_NAMESPACE_NODES
24
28
  end
25
29
 
26
30
  def namespace_nodes=(nodes)
@@ -28,7 +32,7 @@ module Canon
28
32
  end
29
33
 
30
34
  def attribute_nodes
31
- @attribute_nodes ||= []
35
+ @attribute_nodes || EMPTY_ATTRIBUTE_NODES
32
36
  end
33
37
 
34
38
  def node_type
@@ -41,12 +45,12 @@ module Canon
41
45
 
42
46
  def add_namespace(namespace_node)
43
47
  namespace_node.parent = self
44
- namespace_nodes << namespace_node
48
+ (@namespace_nodes ||= []) << namespace_node
45
49
  end
46
50
 
47
51
  def add_attribute(attribute_node)
48
52
  attribute_node.parent = self
49
- attribute_nodes << attribute_node
53
+ (@attribute_nodes ||= []) << attribute_node
50
54
  end
51
55
 
52
56
  # Get namespace nodes in sorted order (lexicographically by local name)
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.14
4
+ version: 0.3.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.