canon 0.3.57 → 0.3.59

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: 71a44b5ac36733a05ee0b762377f6ccaecd4ed3ae8a11527e3b067f21ded2db2
4
- data.tar.gz: 1054b2885a6cd55c479c78a0935b7d18fc790753f72bdc64ebd6854fa267aeff
3
+ metadata.gz: 59a766120de370a517050c389b3ec809574e8e0f876b6a9b3ae351386cfc7333
4
+ data.tar.gz: 6a64cdc990d0d295645ec1f893e37da8b03858c06a2b3992d0e00926d04c939b
5
5
  SHA512:
6
- metadata.gz: 87815f32fa049a8118764a77092e74fe23f6cd87203d67728c21d3c8ba9596f7d78e83e8201f1e8508b0ca79fb234b9e2b1c51cdb9dac27d1a8431ca7f7a7029
7
- data.tar.gz: 6d51c5547e43ff83a0e1e08ca9ab84a096a57f34c1c44c469343f9cd2ac31dfaa8b15609e7c5a09fd7496a33b62b39d5cd301fe58209831810b07c4a6a762366
6
+ metadata.gz: 0f659e6d2708dc675e2bc0df68eb57773ca16224083249e95a9906b1f7d967c92a917882f73a810490ca3b11c228afb9b581d1ceb80b0dc674d98d46014aba42
7
+ data.tar.gz: 974a7dfa4cdcacb21e76952c193160022ecd83de6887b4a5582c60d5853feb5c89a3e69be559ef36484c37a8f9228da1cad8a9168829d23b3e096d51d09fca6d
@@ -215,9 +215,10 @@ module Canon
215
215
 
216
216
  # Check prefix closure constraint
217
217
  # If ancestors are matched, they must be matched to each other
218
- node1.ancestors.each_with_index do |anc1, idx|
218
+ ancestors1 = node1.ancestors
219
+ anc2_ancestors = node2.ancestors
220
+ ancestors1.each_with_index do |anc1, idx|
219
221
  # Get corresponding ancestor in tree2
220
- anc2_ancestors = node2.ancestors
221
222
  return false if idx >= anc2_ancestors.size
222
223
 
223
224
  anc2 = anc2_ancestors[idx]
@@ -81,73 +81,7 @@ module Canon
81
81
  #
82
82
  # @return [Array<String>] Path components
83
83
  def compute_path
84
- # Build path from root to node
85
- ancestors = @node.ancestors.reverse
86
- components = ancestors.map do |ancestor|
87
- path_component(ancestor)
88
- end
89
-
90
- # Add the node itself
91
- components << path_component(@node)
92
-
93
- components
94
- end
95
-
96
- # Get path component for a node
97
- #
98
- # @param node [TreeNode] Node to get component for
99
- # @return [String]
100
- def path_component(node)
101
- # For element nodes: use label with sorted attributes
102
- # For text nodes: use "#text"
103
- # CRITICAL: Text nodes should use "#text" not "text"
104
- # Check the label - actual text nodes have no label or label == "text"
105
- label_str = node.label.to_s.downcase
106
- if node.label.nil? || label_str.empty? || label_str == "#text" || label_str == "text"
107
- "#text"
108
- else
109
- component = node.label.to_s
110
-
111
- # Include sorted attributes to distinguish nodes with same label
112
- # but different attributes (while ignoring attribute order)
113
- # Only include attributes if requested (for hash matching)
114
- if @include_attributes && !node.attributes.empty?
115
- sorted_attrs = node.attributes.sort.to_h
116
- attrs_str = sorted_attrs.map { |k, v| "#{k}=#{v}" }.join(",")
117
- component += "{#{attrs_str}}"
118
- end
119
-
120
- # CRITICAL: For whitespace-sensitive HTML elements, include the text value
121
- # in the signature to prevent incorrect matching of nodes with different whitespace
122
- if @include_attributes && whitespace_sensitive?(node) && node.value
123
- # Include text value in signature for whitespace-sensitive elements
124
- # Use inspect to make whitespace visible and handle special characters
125
- component += "[text=#{node.value.inspect}]"
126
- end
127
-
128
- component
129
- end
130
- end
131
-
132
- # Check if a node is in a whitespace-sensitive context
133
- #
134
- # HTML elements where whitespace is significant: <pre>, <code>, <textarea>, <script>, <style>
135
- #
136
- # @param node [TreeNode] Node to check
137
- # @return [Boolean] True if node is whitespace-sensitive
138
- def whitespace_sensitive?(node)
139
- return false unless node
140
-
141
- # List of HTML elements where whitespace is semantically significant
142
- whitespace_sensitive_tags = %w[pre code textarea script style]
143
-
144
- # Check if this node is whitespace-sensitive
145
- if node.is_a?(TreeNode)
146
- label = node.label.to_s.downcase
147
- return true if whitespace_sensitive_tags.include?(label)
148
- end
149
-
150
- false
84
+ @node.signature_path(include_attributes: @include_attributes)
151
85
  end
152
86
 
153
87
  # Compute signature string from path
@@ -50,6 +50,36 @@ module Canon
50
50
  @weight = nil
51
51
  end
52
52
 
53
+ # Signature path component (see NodeSignature): the label plus
54
+ # sorted attributes (and whitespace-sensitive text value) when
55
+ # +include_attributes+. Memoized per variant — signatures
56
+ # rebuild ancestor components for every node they cover, which
57
+ # is quadratic over the tree without it.
58
+ #
59
+ # @param include_attributes [Boolean]
60
+ # @return [String]
61
+ def signature_component(include_attributes: true)
62
+ if include_attributes
63
+ @signature_component_attrs ||= build_signature_component(true)
64
+ else
65
+ @signature_component ||= build_signature_component(false)
66
+ end
67
+ end
68
+
69
+ # Signature path components from root to this node, built by
70
+ # parent chaining (see NodeSignature). Memoized per variant —
71
+ # signatures previously re-walked the ancestor chain per node.
72
+ #
73
+ # @param include_attributes [Boolean]
74
+ # @return [Array<String>]
75
+ def signature_path(include_attributes: true)
76
+ if include_attributes
77
+ @signature_path_attrs ||= build_signature_path(true)
78
+ else
79
+ @signature_path ||= build_signature_path(false)
80
+ end
81
+ end
82
+
53
83
  # Check if this is a leaf node (no children)
54
84
  #
55
85
  # @return [Boolean]
@@ -57,6 +87,44 @@ module Canon
57
87
  children.empty?
58
88
  end
59
89
 
90
+ # Elements where whitespace is semantically significant; their
91
+ # text value participates in the signature (see NodeSignature).
92
+ WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
93
+
94
+ def build_signature_path(include_attributes)
95
+ component = signature_component(include_attributes: include_attributes)
96
+ if parent
97
+ parent.signature_path(include_attributes: include_attributes) + [component]
98
+ else
99
+ [component]
100
+ end
101
+ end
102
+
103
+ def build_signature_component(include_attributes)
104
+ label_str = label.to_s.downcase
105
+ if label.nil? || label_str.empty? || label_str == "#text" || label_str == "text"
106
+ return "#text"
107
+ end
108
+
109
+ component = label.to_s
110
+
111
+ # Sorted attributes distinguish same-label nodes while
112
+ # ignoring attribute order.
113
+ if include_attributes && !attributes.empty?
114
+ component += "{#{attributes.sort.map { |k, v| "#{k}=#{v}" }.join(',')}}" # rubocop:disable Style/StringConcatenation
115
+ end
116
+
117
+ if include_attributes &&
118
+ WHITESPACE_SENSITIVE_TAGS.include?(label.to_s.downcase) && value
119
+ # inspect makes whitespace visible and handles special
120
+ # characters.
121
+ component += "[text=#{value.inspect}]"
122
+ end
123
+
124
+ component
125
+ end
126
+ private :build_signature_component, :build_signature_path
127
+
60
128
  # Check if this is a text node (leaf with value)
61
129
  #
62
130
  # @return [Boolean]
@@ -118,11 +118,19 @@ module Canon
118
118
 
119
119
  def subtrees_match?(node1, node2)
120
120
  return false unless nodes_match?(node1, node2)
121
- return false unless node1.children.size == node2.children.size
122
121
 
123
- node1.children.zip(node2.children).all? do |child1, child2|
124
- subtrees_match?(child1, child2)
122
+ children1 = node1.children
123
+ children2 = node2.children
124
+ return false unless children1.size == children2.size
125
+
126
+ index = 0
127
+ while index < children1.size
128
+ return false unless subtrees_match?(children1[index],
129
+ children2[index])
130
+
131
+ index += 1
125
132
  end
133
+ true
126
134
  end
127
135
 
128
136
  def nodes_match?(node1, node2)
@@ -142,6 +150,11 @@ module Canon
142
150
  return true if (text1.nil? || text1.empty?) && (text2.nil? || text2.empty?)
143
151
  return false if (text1.nil? || text1.empty?) || (text2.nil? || text2.empty?)
144
152
 
153
+ # Identical raw text is equivalent under every mode — skips
154
+ # the gsub normalization for the overwhelmingly common
155
+ # matched-leaf case.
156
+ return true if text1 == text2
157
+
145
158
  norm1 = normalize_text(text1)
146
159
  norm2 = normalize_text(text2)
147
160
  return true if norm1.empty? && norm2.empty?
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.57"
4
+ VERSION = "0.3.59"
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.57
4
+ version: 0.3.59
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.