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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59a766120de370a517050c389b3ec809574e8e0f876b6a9b3ae351386cfc7333
|
|
4
|
+
data.tar.gz: 6a64cdc990d0d295645ec1f893e37da8b03858c06a2b3992d0e00926d04c939b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
|
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?
|
data/lib/canon/version.rb
CHANGED