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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +69 -92
  3. data/README.adoc +13 -13
  4. data/docs/.lycheeignore +69 -0
  5. data/docs/Gemfile +1 -0
  6. data/docs/_config.yml +90 -1
  7. data/docs/advanced/diff-classification.adoc +82 -2
  8. data/docs/advanced/extending-canon.adoc +193 -0
  9. data/docs/features/match-options/index.adoc +239 -1
  10. data/docs/internals/diffnode-enrichment.adoc +611 -0
  11. data/docs/internals/index.adoc +251 -0
  12. data/docs/lychee.toml +13 -6
  13. data/docs/understanding/architecture.adoc +749 -33
  14. data/docs/understanding/comparison-pipeline.adoc +122 -0
  15. data/lib/canon/cache.rb +129 -0
  16. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
  17. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
  18. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
  19. data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
  20. data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
  21. data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
  22. data/lib/canon/comparison/dimensions/registry.rb +77 -0
  23. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
  24. data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
  25. data/lib/canon/comparison/dimensions.rb +54 -0
  26. data/lib/canon/comparison/format_detector.rb +87 -0
  27. data/lib/canon/comparison/html_comparator.rb +70 -26
  28. data/lib/canon/comparison/html_compare_profile.rb +8 -2
  29. data/lib/canon/comparison/html_parser.rb +80 -0
  30. data/lib/canon/comparison/json_comparator.rb +12 -0
  31. data/lib/canon/comparison/json_parser.rb +19 -0
  32. data/lib/canon/comparison/markup_comparator.rb +293 -0
  33. data/lib/canon/comparison/match_options/base_resolver.rb +150 -0
  34. data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
  35. data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
  36. data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
  37. data/lib/canon/comparison/match_options.rb +68 -463
  38. data/lib/canon/comparison/profile_definition.rb +149 -0
  39. data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
  40. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
  41. data/lib/canon/comparison/whitespace_sensitivity.rb +208 -0
  42. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
  43. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
  44. data/lib/canon/comparison/xml_comparator/child_comparison.rb +197 -0
  45. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
  46. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
  47. data/lib/canon/comparison/xml_comparator/node_parser.rb +79 -0
  48. data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +102 -0
  49. data/lib/canon/comparison/xml_comparator.rb +97 -684
  50. data/lib/canon/comparison/xml_node_comparison.rb +319 -0
  51. data/lib/canon/comparison/xml_parser.rb +19 -0
  52. data/lib/canon/comparison/yaml_comparator.rb +3 -3
  53. data/lib/canon/comparison.rb +265 -110
  54. data/lib/canon/diff/diff_classifier.rb +101 -2
  55. data/lib/canon/diff/diff_node.rb +32 -2
  56. data/lib/canon/diff/formatting_detector.rb +1 -1
  57. data/lib/canon/diff/node_serializer.rb +191 -0
  58. data/lib/canon/diff/path_builder.rb +143 -0
  59. data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
  60. data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
  61. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
  62. data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
  63. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
  64. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
  65. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
  66. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
  67. data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
  68. data/lib/canon/diff_formatter.rb +1 -1
  69. data/lib/canon/rspec_matchers.rb +38 -9
  70. data/lib/canon/tree_diff/operation_converter.rb +92 -338
  71. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
  72. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
  73. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
  74. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
  75. data/lib/canon/version.rb +1 -1
  76. data/lib/canon/xml/data_model.rb +24 -13
  77. 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