canon 0.3.58 → 0.3.60
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 +4 -4
- data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
- data/lib/canon/tree_diff/core/matching.rb +3 -2
- data/lib/canon/tree_diff/core/node_signature.rb +1 -28
- data/lib/canon/tree_diff/core/tree_node.rb +49 -8
- data/lib/canon/tree_diff/matchers/hash_matcher.rb +16 -3
- data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -1
- data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -1
- data/lib/canon/tree_diff/operations/operation_detector.rb +5 -6
- data/lib/canon/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1cbc73f1d667885f1f989a9534d8123dbcc67ae7eda250cc24cba00e7ad24a7
|
|
4
|
+
data.tar.gz: b9a27d00c02f533c6679237e190eb7f6bc6532fe0f945957bdc5d521bb84217b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b060895cd58c7321db7d0640a02ca166f3efb9b5cb4c90d5fafb7d615b3faf6c73f4e29d42533d56ac9fd393e625bac473d9cd122273dc81b3598b0be76b86f9
|
|
7
|
+
data.tar.gz: 4b160273200d229151e61842bc994cd8e1d23f495107f495edde383b2fdccda2e9de150a65160d34c5e3177a7bf236282a6e1bb675c72b2bf667deb200e3dab4
|
|
@@ -196,7 +196,7 @@ module Canon
|
|
|
196
196
|
return false unless Canon::Comparison::NodeInspector.element_node?(element)
|
|
197
197
|
|
|
198
198
|
# List of HTML elements where whitespace is semantically significant
|
|
199
|
-
whitespace_sensitive_tags =
|
|
199
|
+
whitespace_sensitive_tags = Core::TreeNode::WHITESPACE_SENSITIVE_TAGS
|
|
200
200
|
whitespace_sensitive_tags.include?(element.name.downcase)
|
|
201
201
|
end
|
|
202
202
|
|
|
@@ -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
|
|
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,34 +81,7 @@ module Canon
|
|
|
81
81
|
#
|
|
82
82
|
# @return [Array<String>] Path components
|
|
83
83
|
def compute_path
|
|
84
|
-
|
|
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
|
-
# Memoized on the node — see TreeNode#signature_component.
|
|
110
|
-
node.signature_component(include_attributes: @include_attributes)
|
|
111
|
-
end
|
|
84
|
+
@node.signature_path(include_attributes: @include_attributes)
|
|
112
85
|
end
|
|
113
86
|
|
|
114
87
|
# Compute signature string from path
|
|
@@ -66,6 +66,20 @@ module Canon
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
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
|
+
|
|
69
83
|
# Check if this is a leaf node (no children)
|
|
70
84
|
#
|
|
71
85
|
# @return [Boolean]
|
|
@@ -73,17 +87,40 @@ module Canon
|
|
|
73
87
|
children.empty?
|
|
74
88
|
end
|
|
75
89
|
|
|
76
|
-
# Elements where whitespace is semantically significant
|
|
77
|
-
# text value participates in
|
|
90
|
+
# Elements where whitespace is semantically significant (HTML):
|
|
91
|
+
# their text value participates in signatures and matching.
|
|
92
|
+
# Single source for the tree_diff components.
|
|
78
93
|
WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
|
|
79
94
|
|
|
95
|
+
def build_signature_path(include_attributes)
|
|
96
|
+
component = signature_component(include_attributes: include_attributes)
|
|
97
|
+
if parent
|
|
98
|
+
parent.signature_path(include_attributes: include_attributes) + [component]
|
|
99
|
+
else
|
|
100
|
+
[component]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
80
104
|
def build_signature_component(include_attributes)
|
|
105
|
+
label_str = label.to_s.downcase
|
|
106
|
+
if label.nil? || label_str.empty? || label_str == "#text" || label_str == "text"
|
|
107
|
+
return "#text"
|
|
108
|
+
end
|
|
109
|
+
|
|
81
110
|
component = label.to_s
|
|
82
111
|
|
|
83
112
|
# Sorted attributes distinguish same-label nodes while
|
|
84
|
-
# ignoring attribute order.
|
|
113
|
+
# ignoring attribute order. Built directly into the
|
|
114
|
+
# component — no intermediate sorted/map/join arrays.
|
|
85
115
|
if include_attributes && !attributes.empty?
|
|
86
|
-
component += "{
|
|
116
|
+
component += "{"
|
|
117
|
+
first = true
|
|
118
|
+
attributes.sort.each do |k, v|
|
|
119
|
+
component << "," unless first
|
|
120
|
+
first = false
|
|
121
|
+
component << k.to_s << "=" << v.to_s
|
|
122
|
+
end
|
|
123
|
+
component << "}"
|
|
87
124
|
end
|
|
88
125
|
|
|
89
126
|
if include_attributes &&
|
|
@@ -95,7 +132,7 @@ module Canon
|
|
|
95
132
|
|
|
96
133
|
component
|
|
97
134
|
end
|
|
98
|
-
private :build_signature_component
|
|
135
|
+
private :build_signature_component, :build_signature_path
|
|
99
136
|
|
|
100
137
|
# Check if this is a text node (leaf with value)
|
|
101
138
|
#
|
|
@@ -137,10 +174,14 @@ module Canon
|
|
|
137
174
|
#
|
|
138
175
|
# @return [Array<TreeNode>]
|
|
139
176
|
def descendants
|
|
177
|
+
# Iterative pre-order walk — the recursive form allocated an
|
|
178
|
+
# intermediate array per level.
|
|
140
179
|
result = []
|
|
141
|
-
children.
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
stack = children.reverse_each.to_a
|
|
181
|
+
until stack.empty?
|
|
182
|
+
node = stack.pop
|
|
183
|
+
result << node
|
|
184
|
+
stack.concat(node.children.reverse_each.to_a) if node.children.any?
|
|
144
185
|
end
|
|
145
186
|
result
|
|
146
187
|
end
|
|
@@ -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
|
|
124
|
-
|
|
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?
|
|
@@ -158,7 +158,7 @@ module Canon
|
|
|
158
158
|
return false unless node
|
|
159
159
|
|
|
160
160
|
# List of HTML elements where whitespace is semantically significant
|
|
161
|
-
whitespace_sensitive_tags =
|
|
161
|
+
whitespace_sensitive_tags = Core::TreeNode::WHITESPACE_SENSITIVE_TAGS
|
|
162
162
|
|
|
163
163
|
# Check if this node is whitespace-sensitive
|
|
164
164
|
if node.is_a?(Core::TreeNode)
|
|
@@ -211,7 +211,7 @@ module Canon
|
|
|
211
211
|
return false unless node
|
|
212
212
|
|
|
213
213
|
# List of HTML elements where whitespace is semantically significant
|
|
214
|
-
whitespace_sensitive_tags =
|
|
214
|
+
whitespace_sensitive_tags = Core::TreeNode::WHITESPACE_SENSITIVE_TAGS
|
|
215
215
|
|
|
216
216
|
# Check if this node is whitespace-sensitive
|
|
217
217
|
if node.is_a?(Core::TreeNode)
|
|
@@ -205,8 +205,10 @@ module Canon
|
|
|
205
205
|
attrs1 = node1.attributes
|
|
206
206
|
attrs2 = node2.attributes
|
|
207
207
|
|
|
208
|
-
# Check if attribute values differ (ignoring order)
|
|
209
|
-
|
|
208
|
+
# Check if attribute values differ (ignoring order).
|
|
209
|
+
# Identical raw hashes (order included) are trivially equal
|
|
210
|
+
# after sorting — the overwhelmingly common matched case.
|
|
211
|
+
if attrs1 != attrs2 && attrs1.sort.to_h != attrs2.sort.to_h
|
|
210
212
|
# Actual attribute value differences
|
|
211
213
|
changes[:attributes] = {
|
|
212
214
|
old: attrs1,
|
|
@@ -611,15 +613,12 @@ module Canon
|
|
|
611
613
|
def whitespace_sensitive?(node)
|
|
612
614
|
return false unless node
|
|
613
615
|
|
|
614
|
-
# List of HTML elements where whitespace is semantically significant
|
|
615
|
-
whitespace_sensitive_tags = %w[pre code textarea script style]
|
|
616
|
-
|
|
617
616
|
# Check if this node or any ancestor is whitespace-sensitive
|
|
618
617
|
current = node
|
|
619
618
|
while current
|
|
620
619
|
if current.is_a?(Core::TreeNode)
|
|
621
620
|
label = current.label.to_s.downcase
|
|
622
|
-
return true if
|
|
621
|
+
return true if Core::TreeNode::WHITESPACE_SENSITIVE_TAGS.include?(label)
|
|
623
622
|
end
|
|
624
623
|
|
|
625
624
|
current = current.is_a?(Core::TreeNode) ? current.parent : nil
|
data/lib/canon/version.rb
CHANGED