canon 0.1.7 → 0.1.9
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/.rubocop_todo.yml +69 -92
- data/README.adoc +13 -13
- data/docs/.lycheeignore +69 -0
- data/docs/Gemfile +1 -0
- data/docs/_config.yml +90 -1
- data/docs/advanced/diff-classification.adoc +82 -2
- data/docs/advanced/extending-canon.adoc +193 -0
- data/docs/features/match-options/index.adoc +239 -1
- data/docs/internals/diffnode-enrichment.adoc +611 -0
- data/docs/internals/index.adoc +251 -0
- data/docs/lychee.toml +13 -6
- data/docs/understanding/architecture.adoc +749 -33
- data/docs/understanding/comparison-pipeline.adoc +122 -0
- data/lib/canon/cache.rb +129 -0
- data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
- data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
- data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
- data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
- data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
- data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
- data/lib/canon/comparison/dimensions/registry.rb +77 -0
- data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
- data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
- data/lib/canon/comparison/dimensions.rb +54 -0
- data/lib/canon/comparison/format_detector.rb +87 -0
- data/lib/canon/comparison/html_comparator.rb +70 -26
- data/lib/canon/comparison/html_compare_profile.rb +8 -2
- data/lib/canon/comparison/html_parser.rb +80 -0
- data/lib/canon/comparison/json_comparator.rb +12 -0
- data/lib/canon/comparison/json_parser.rb +19 -0
- data/lib/canon/comparison/markup_comparator.rb +293 -0
- data/lib/canon/comparison/match_options/base_resolver.rb +150 -0
- data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
- data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
- data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
- data/lib/canon/comparison/match_options.rb +68 -463
- data/lib/canon/comparison/profile_definition.rb +149 -0
- data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
- data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
- data/lib/canon/comparison/whitespace_sensitivity.rb +208 -0
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
- data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +197 -0
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
- data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
- data/lib/canon/comparison/xml_comparator/node_parser.rb +79 -0
- data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +102 -0
- data/lib/canon/comparison/xml_comparator.rb +97 -684
- data/lib/canon/comparison/xml_node_comparison.rb +319 -0
- data/lib/canon/comparison/xml_parser.rb +19 -0
- data/lib/canon/comparison/yaml_comparator.rb +3 -3
- data/lib/canon/comparison.rb +265 -110
- data/lib/canon/diff/diff_classifier.rb +101 -2
- data/lib/canon/diff/diff_node.rb +32 -2
- data/lib/canon/diff/formatting_detector.rb +1 -1
- data/lib/canon/diff/node_serializer.rb +191 -0
- data/lib/canon/diff/path_builder.rb +143 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
- data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
- data/lib/canon/diff_formatter.rb +1 -1
- data/lib/canon/rspec_matchers.rb +38 -9
- data/lib/canon/tree_diff/operation_converter.rb +92 -338
- data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
- data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
- data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
- data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +24 -13
- metadata +48 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../xml/namespace_helper"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
class DiffFormatter
|
|
7
|
+
module DiffDetailFormatterHelpers
|
|
8
|
+
# Node utility methods
|
|
9
|
+
#
|
|
10
|
+
# Provides helper methods for extracting information from nodes.
|
|
11
|
+
module NodeUtils
|
|
12
|
+
# Get attribute names from a node
|
|
13
|
+
#
|
|
14
|
+
# @param node [Object] Node to extract attributes from
|
|
15
|
+
# @return [Array<String>] Array of attribute names
|
|
16
|
+
def self.get_attribute_names(node)
|
|
17
|
+
return [] unless node
|
|
18
|
+
|
|
19
|
+
attrs = if node.respond_to?(:attribute_nodes)
|
|
20
|
+
node.attribute_nodes
|
|
21
|
+
elsif node.respond_to?(:attributes)
|
|
22
|
+
node.attributes
|
|
23
|
+
elsif node.respond_to?(:[]) && node.respond_to?(:each)
|
|
24
|
+
# Hash-like node
|
|
25
|
+
node.keys
|
|
26
|
+
else
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return [] unless attrs
|
|
31
|
+
|
|
32
|
+
# Handle different attribute formats
|
|
33
|
+
if attrs.is_a?(Array)
|
|
34
|
+
attrs.map { |attr| attr.respond_to?(:name) ? attr.name : attr.to_s }
|
|
35
|
+
elsif attrs.respond_to?(:keys)
|
|
36
|
+
attrs.keys.map(&:to_s)
|
|
37
|
+
else
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Find all differing attributes between two nodes
|
|
43
|
+
#
|
|
44
|
+
# @param node1 [Object] First node
|
|
45
|
+
# @param node2 [Object] Second node
|
|
46
|
+
# @return [Array<String>] Array of attribute names with different values
|
|
47
|
+
def self.find_all_differing_attributes(node1, node2)
|
|
48
|
+
return [] unless node1 && node2
|
|
49
|
+
|
|
50
|
+
attrs1 = get_attributes_hash(node1)
|
|
51
|
+
attrs2 = get_attributes_hash(node2)
|
|
52
|
+
|
|
53
|
+
all_keys = (attrs1.keys | attrs2.keys)
|
|
54
|
+
|
|
55
|
+
all_keys.reject do |key|
|
|
56
|
+
attrs1[key.to_s] == attrs2[key.to_s]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get attribute names in order from a node
|
|
61
|
+
#
|
|
62
|
+
# @param node [Object] Node to extract from
|
|
63
|
+
# @return [Array<String>] Ordered array of attribute names
|
|
64
|
+
def self.get_attribute_names_in_order(node)
|
|
65
|
+
return [] unless node
|
|
66
|
+
|
|
67
|
+
attrs = if node.respond_to?(:attribute_nodes)
|
|
68
|
+
node.attribute_nodes
|
|
69
|
+
elsif node.respond_to?(:attributes)
|
|
70
|
+
node.attributes
|
|
71
|
+
else
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return [] unless attrs
|
|
76
|
+
|
|
77
|
+
if attrs.is_a?(Array)
|
|
78
|
+
attrs.map { |attr| attr.respond_to?(:name) ? attr.name : attr.to_s }
|
|
79
|
+
else
|
|
80
|
+
attrs.keys.map(&:to_s)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get attributes as a hash
|
|
85
|
+
#
|
|
86
|
+
# @param node [Object] Node to extract from
|
|
87
|
+
# @return [Hash] Attributes hash
|
|
88
|
+
def self.get_attributes_hash(node)
|
|
89
|
+
return {} unless node
|
|
90
|
+
|
|
91
|
+
attrs = if node.respond_to?(:attribute_nodes)
|
|
92
|
+
node.attribute_nodes
|
|
93
|
+
elsif node.respond_to?(:attributes)
|
|
94
|
+
node.attributes
|
|
95
|
+
else
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return {} unless attrs
|
|
100
|
+
|
|
101
|
+
result = {}
|
|
102
|
+
if attrs.is_a?(Array)
|
|
103
|
+
attrs.each do |attr|
|
|
104
|
+
name = attr.respond_to?(:name) ? attr.name : attr.to_s
|
|
105
|
+
value = attr.respond_to?(:value) ? attr.value : attr.to_s
|
|
106
|
+
result[name] = value
|
|
107
|
+
end
|
|
108
|
+
elsif attrs.respond_to?(:each)
|
|
109
|
+
attrs.each do |key, val|
|
|
110
|
+
name = key.to_s
|
|
111
|
+
value = if val.respond_to?(:value)
|
|
112
|
+
val.value
|
|
113
|
+
elsif val.respond_to?(:content)
|
|
114
|
+
val.content
|
|
115
|
+
else
|
|
116
|
+
val.to_s
|
|
117
|
+
end
|
|
118
|
+
result[name] = value
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get attribute value from a node
|
|
126
|
+
#
|
|
127
|
+
# @param node [Object] Node to extract from
|
|
128
|
+
# @param attr_name [String] Attribute name
|
|
129
|
+
# @return [String, nil] Attribute value or nil
|
|
130
|
+
def self.get_attribute_value(node, attr_name)
|
|
131
|
+
return nil unless node && attr_name
|
|
132
|
+
|
|
133
|
+
if node.respond_to?(:[])
|
|
134
|
+
value = node[attr_name]
|
|
135
|
+
if value.respond_to?(:value)
|
|
136
|
+
value.value
|
|
137
|
+
elsif value.respond_to?(:content)
|
|
138
|
+
value.content
|
|
139
|
+
elsif value.respond_to?(:to_s)
|
|
140
|
+
value.to_s
|
|
141
|
+
else
|
|
142
|
+
value
|
|
143
|
+
end
|
|
144
|
+
elsif node.respond_to?(:get_attribute)
|
|
145
|
+
attr = node.get_attribute(attr_name)
|
|
146
|
+
attr.respond_to?(:value) ? attr.value : attr
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get text content from a node
|
|
151
|
+
#
|
|
152
|
+
# @param node [Object] Node to extract from
|
|
153
|
+
# @return [String] Text content
|
|
154
|
+
def self.get_node_text(node)
|
|
155
|
+
return "" unless node
|
|
156
|
+
|
|
157
|
+
if node.respond_to?(:text)
|
|
158
|
+
node.text
|
|
159
|
+
elsif node.respond_to?(:content)
|
|
160
|
+
node.content
|
|
161
|
+
elsif node.respond_to?(:inner_text)
|
|
162
|
+
node.inner_text
|
|
163
|
+
else
|
|
164
|
+
""
|
|
165
|
+
end.to_s.strip
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get element name for display
|
|
169
|
+
#
|
|
170
|
+
# @param node [Object] Node to get name from
|
|
171
|
+
# @return [String] Element name
|
|
172
|
+
def self.get_element_name_for_display(node)
|
|
173
|
+
return "" unless node
|
|
174
|
+
|
|
175
|
+
if node.respond_to?(:name)
|
|
176
|
+
node.name.to_s
|
|
177
|
+
else
|
|
178
|
+
node.class.name
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get namespace URI for display
|
|
183
|
+
#
|
|
184
|
+
# @param node [Object] Node to get namespace from
|
|
185
|
+
# @return [String] Namespace URI
|
|
186
|
+
def self.get_namespace_uri_for_display(node)
|
|
187
|
+
return "" unless node
|
|
188
|
+
|
|
189
|
+
if node.respond_to?(:namespace_uri)
|
|
190
|
+
node.namespace_uri.to_s
|
|
191
|
+
elsif node.respond_to?(:namespace)
|
|
192
|
+
ns = node.namespace
|
|
193
|
+
ns.respond_to?(:href) ? ns.href.to_s : ""
|
|
194
|
+
else
|
|
195
|
+
""
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Format node briefly for display
|
|
200
|
+
#
|
|
201
|
+
# @param node [Object] Node to format
|
|
202
|
+
# @return [String] Brief node description
|
|
203
|
+
def self.format_node_brief(node)
|
|
204
|
+
return "" unless node
|
|
205
|
+
|
|
206
|
+
name = get_element_name_for_display(node)
|
|
207
|
+
text = get_node_text(node)
|
|
208
|
+
|
|
209
|
+
if text && !text.empty?
|
|
210
|
+
"#{name}(\"#{text}\")"
|
|
211
|
+
else
|
|
212
|
+
name
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Check if node is inside a preserve-whitespace element
|
|
217
|
+
#
|
|
218
|
+
# @param node [Object] Node to check
|
|
219
|
+
# @return [Boolean] true if inside preserve element
|
|
220
|
+
def self.inside_preserve_element?(node)
|
|
221
|
+
return false unless node
|
|
222
|
+
|
|
223
|
+
preserve_elements = %w[pre code textarea script style]
|
|
224
|
+
|
|
225
|
+
# Check the node itself
|
|
226
|
+
if node.respond_to?(:name) && preserve_elements.include?(node.name.to_s.downcase)
|
|
227
|
+
return true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Check ancestors
|
|
231
|
+
current = node
|
|
232
|
+
while current
|
|
233
|
+
if current.respond_to?(:parent)
|
|
234
|
+
current = current.parent
|
|
235
|
+
elsif current.respond_to?(:parent_node)
|
|
236
|
+
current = current.parent_node
|
|
237
|
+
else
|
|
238
|
+
break
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
next unless current
|
|
242
|
+
|
|
243
|
+
if current.respond_to?(:name) && preserve_elements.include?(current.name.to_s.downcase)
|
|
244
|
+
return true
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
false
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
class DiffFormatter
|
|
5
|
+
module DiffDetailFormatterHelpers
|
|
6
|
+
# Text utility methods for diff formatting
|
|
7
|
+
#
|
|
8
|
+
# Provides helper methods for text manipulation and visualization.
|
|
9
|
+
module TextUtils
|
|
10
|
+
# Truncate text to a maximum length with ellipsis
|
|
11
|
+
#
|
|
12
|
+
# @param text [String] Text to truncate
|
|
13
|
+
# @param max_length [Integer] Maximum length
|
|
14
|
+
# @return [String] Truncated text
|
|
15
|
+
def self.truncate_text(text, max_length)
|
|
16
|
+
return "" if text.nil?
|
|
17
|
+
|
|
18
|
+
text.length > max_length ? "#{text[0...max_length]}..." : text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Visualize whitespace characters in text
|
|
22
|
+
#
|
|
23
|
+
# Shows spaces as ·, tabs as →, newlines as ¬
|
|
24
|
+
#
|
|
25
|
+
# @param text [String] Text to visualize
|
|
26
|
+
# @return [String] Text with visible whitespace
|
|
27
|
+
def self.visualize_whitespace(text)
|
|
28
|
+
return "" if text.nil?
|
|
29
|
+
|
|
30
|
+
text
|
|
31
|
+
.gsub(" ", "·")
|
|
32
|
+
.gsub("\t", "→")
|
|
33
|
+
.gsub("\n", "¬")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extract a content preview from a node
|
|
37
|
+
#
|
|
38
|
+
# @param node [Object] Node to extract from
|
|
39
|
+
# @param max_length [Integer] Maximum length of preview
|
|
40
|
+
# @return [String] Content preview
|
|
41
|
+
def self.extract_content_preview(node, max_length = 50)
|
|
42
|
+
return "" unless node
|
|
43
|
+
|
|
44
|
+
text = if node.respond_to?(:text)
|
|
45
|
+
node.text
|
|
46
|
+
elsif node.respond_to?(:content)
|
|
47
|
+
node.content
|
|
48
|
+
else
|
|
49
|
+
node.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return "" if text.nil? || text.empty?
|
|
53
|
+
|
|
54
|
+
# Clean up whitespace
|
|
55
|
+
text = text.strip.gsub(/\s+/, " ")
|
|
56
|
+
truncate_text(text, max_length)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|