canon 0.2.8 → 0.2.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec-opal +7 -0
  3. data/.rubocop_todo.yml +14 -71
  4. data/Rakefile +17 -0
  5. data/lib/canon/cli.rb +1 -1
  6. data/lib/canon/color_detector.rb +3 -5
  7. data/lib/canon/comparison/compare_profile.rb +1 -4
  8. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +2 -6
  9. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +2 -6
  10. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +2 -6
  11. data/lib/canon/comparison/dimensions/comments_dimension.rb +2 -6
  12. data/lib/canon/comparison/dimensions/element_position_dimension.rb +2 -6
  13. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +2 -6
  14. data/lib/canon/comparison/dimensions/text_content_dimension.rb +3 -5
  15. data/lib/canon/comparison/format_detector.rb +29 -20
  16. data/lib/canon/comparison/html_comparator.rb +18 -29
  17. data/lib/canon/comparison/html_compare_profile.rb +3 -10
  18. data/lib/canon/comparison/html_parser.rb +1 -1
  19. data/lib/canon/comparison/json_comparator.rb +8 -0
  20. data/lib/canon/comparison/node_inspector.rb +146 -80
  21. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +6 -8
  22. data/lib/canon/comparison/whitespace_sensitivity.rb +55 -193
  23. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +5 -10
  24. data/lib/canon/comparison/xml_comparator/child_comparison.rb +4 -4
  25. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +10 -8
  26. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +14 -28
  27. data/lib/canon/comparison/xml_comparator/node_parser.rb +12 -11
  28. data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +30 -58
  29. data/lib/canon/comparison/xml_comparator.rb +61 -83
  30. data/lib/canon/comparison/xml_node_comparison.rb +15 -15
  31. data/lib/canon/comparison/yaml_comparator.rb +8 -0
  32. data/lib/canon/comparison.rb +23 -23
  33. data/lib/canon/config/profile_loader.rb +13 -13
  34. data/lib/canon/config.rb +29 -5
  35. data/lib/canon/diff/diff_classifier.rb +7 -41
  36. data/lib/canon/diff/diff_line.rb +1 -1
  37. data/lib/canon/diff/diff_node_enricher.rb +22 -24
  38. data/lib/canon/diff/node_serializer.rb +23 -30
  39. data/lib/canon/diff/path_builder.rb +24 -37
  40. data/lib/canon/diff/source_locator.rb +0 -3
  41. data/lib/canon/diff/xml_serialization_formatter.rb +8 -81
  42. data/lib/canon/diff_formatter/by_line/base_formatter.rb +7 -7
  43. data/lib/canon/diff_formatter/by_line/json_formatter.rb +1 -1
  44. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +1 -1
  45. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +2 -2
  46. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +1 -1
  47. data/lib/canon/diff_formatter/by_line_formatter.rb +1 -1
  48. data/lib/canon/diff_formatter/by_object/base_formatter.rb +11 -15
  49. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +8 -10
  50. data/lib/canon/diff_formatter/by_object_formatter.rb +1 -1
  51. data/lib/canon/diff_formatter/debug_output.rb +12 -24
  52. data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +2 -2
  53. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +146 -318
  54. data/lib/canon/diff_formatter/diff_detail_formatter.rb +28 -20
  55. data/lib/canon/diff_formatter/legend.rb +2 -2
  56. data/lib/canon/diff_formatter/pretty_diff_formatter.rb +2 -2
  57. data/lib/canon/diff_formatter/theme.rb +4 -4
  58. data/lib/canon/diff_formatter.rb +2 -2
  59. data/lib/canon/formatters/html_formatter.rb +1 -1
  60. data/lib/canon/formatters/html_formatter_base.rb +1 -1
  61. data/lib/canon/formatters/xml_formatter.rb +7 -32
  62. data/lib/canon/html/data_model.rb +1 -1
  63. data/lib/canon/pretty_printer/html.rb +1 -1
  64. data/lib/canon/pretty_printer/xml.rb +16 -7
  65. data/lib/canon/pretty_printer/xml_normalized.rb +9 -3
  66. data/lib/canon/rspec_matchers.rb +2 -2
  67. data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
  68. data/lib/canon/tree_diff/adapters/xml_adapter.rb +1 -1
  69. data/lib/canon/tree_diff/core/tree_node.rb +1 -3
  70. data/lib/canon/validators/html_validator.rb +1 -1
  71. data/lib/canon/validators/xml_validator.rb +1 -1
  72. data/lib/canon/version.rb +1 -1
  73. data/lib/canon/xml/data_model.rb +131 -137
  74. data/lib/canon/xml/namespace_helper.rb +5 -0
  75. data/lib/canon/xml/node.rb +2 -1
  76. data/lib/canon/xml/nodes/root_node.rb +4 -0
  77. data/lib/canon/xml/nodes/text_node.rb +6 -1
  78. data/lib/canon/xml/sax_builder.rb +4 -6
  79. data/lib/canon/xml_backend.rb +49 -0
  80. data/lib/canon/xml_parsing.rb +271 -0
  81. data/lib/canon.rb +3 -1
  82. data/lib/tasks/benchmark_runner.rb +1 -1
  83. data/lib/tasks/performance_helpers.rb +1 -1
  84. metadata +5 -2
@@ -4,59 +4,25 @@ module Canon
4
4
  module Diff
5
5
  # Detects and classifies XML serialization-level formatting differences.
6
6
  #
7
- # Serialization-level formatting differences are differences in XML syntax
8
- # that do not affect the semantic content of the document. These differences
9
- # arise from different valid ways to serialize the same semantic content.
10
- #
11
- # These differences are ALWAYS non-normative (formatting-only) regardless
12
- # of match options, because they are purely syntactic variations.
13
- #
14
- # Examples:
15
- # - Self-closing vs explicit closing tags: <tag/> vs <tag></tag>
16
- # - Attribute quote style: attr="value" vs attr='value' (parser-normalized)
17
- # - Whitespace within tags: <tag a="1" b="2"> vs <tag a="1" b="2"> (parser-normalized)
18
- #
19
- # Note: Some serialization differences are normalized away by XML parsers
20
- # (attribute quotes, tag spacing). This class focuses on differences that
21
- # survive parsing and comparison, such as self-closing vs explicit closing.
7
+ # Serialization formatting differences are ALWAYS non-normative (formatting-only)
8
+ # regardless of match options, because they are purely syntactic variations.
22
9
  class XmlSerializationFormatter
23
- # Detect if a diff node represents an XML serialization formatting difference.
24
- #
25
- # Serialization formatting differences are ALWAYS non-normative because they
26
- # represent different valid serializations of the same semantic content.
27
- #
28
- # @param diff_node [DiffNode] The diff node to check
29
- # @return [Boolean] true if this is a serialization formatting difference
10
+ NI = Canon::Comparison::NodeInspector
11
+
30
12
  def self.serialization_formatting?(diff_node)
31
- # Currently only handles text_content dimension
32
- # Future: add detection for other dimensions
33
13
  return false unless diff_node.dimension == :text_content
34
14
 
35
15
  empty_text_content_serialization_diff?(diff_node)
36
16
  end
37
17
 
38
- # Check if a text_content difference is from XML serialization format.
39
- #
40
- # Specifically detects self-closing tags (<tag/>) vs explicit closing tags
41
- # (<tag></tag>), which create different text node structures:
42
- # - Self-closing: no text node (nil)
43
- # - Explicit closing: empty or whitespace-only text node ("", " ", "\n", etc.)
44
- #
45
- # Per XML standards, these forms are semantically equivalent.
46
- #
47
- # @param diff_node [DiffNode] The diff node to check
48
- # @return [Boolean] true if this is a serialization formatting difference
49
18
  def self.empty_text_content_serialization_diff?(diff_node)
50
19
  return false unless diff_node.dimension == :text_content
51
20
 
52
21
  node1 = diff_node.node1
53
22
  node2 = diff_node.node2
54
23
 
55
- # Both nodes are nil - no actual difference, not a serialization formatting diff
56
24
  return false if node1.nil? && node2.nil?
57
25
 
58
- # Only one is nil (e.g., one doc has self-closing, other has text)
59
- # If the non-nil one is blank, it's still serialization formatting
60
26
  if node1.nil? || node2.nil?
61
27
  non_nil = node1 || node2
62
28
  return false unless text_node?(non_nil)
@@ -65,68 +31,29 @@ module Canon
65
31
  return blank?(text)
66
32
  end
67
33
 
68
- # Both must be text nodes
69
34
  return false unless text_node?(node1) && text_node?(node2)
70
35
 
71
36
  text1 = extract_text_content(node1)
72
37
  text2 = extract_text_content(node2)
73
38
 
74
- # Check if both texts are blank/whitespace-only
75
- # This indicates self-closing vs explicit closing tag syntax
76
39
  blank?(text1) && blank?(text2)
77
40
  end
78
41
 
79
- # Check if a value is blank (nil or whitespace-only)
80
- # @param value [String, nil] Value to check
81
- # @return [Boolean] true if blank
82
42
  def self.blank?(value)
83
- value.nil? ||
84
- (value.respond_to?(:empty?) && value.empty?) ||
85
- (value.respond_to?(:strip) && value.strip.empty?)
43
+ value.nil? || value.to_s.strip.empty?
86
44
  end
87
45
 
88
- # Check if a node is a text node
89
- # @param node [Object] The node to check
90
- # @return [Boolean] true if the node is a text node
91
46
  def self.text_node?(node)
92
47
  return false if node.nil?
48
+ return true if node.is_a?(String)
93
49
 
94
- case node
95
- when Canon::Xml::Nodes::TextNode
96
- true
97
- when Canon::Xml::Node
98
- node.node_type == :text
99
- when Nokogiri::XML::Node
100
- node.node_type == Nokogiri::XML::Node::TEXT_NODE
101
- when Moxml::Node
102
- node.text?
103
- when String
104
- true
105
- else
106
- false
107
- end
50
+ NI.text_node?(node)
108
51
  end
109
52
 
110
- # Extract text content from a node
111
- # @param node [Object] The node to extract text from
112
- # @return [String, nil] The text content or nil
113
53
  def self.extract_text_content(node)
114
54
  return nil if node.nil?
115
55
 
116
- case node
117
- when Canon::Xml::Nodes::TextNode
118
- node.value
119
- when Canon::Xml::Node
120
- node.text_content
121
- when Nokogiri::XML::Node
122
- node.content.to_s
123
- when Moxml::Node
124
- node.content.to_s
125
- when String
126
- node
127
- else
128
- node.to_s
129
- end
56
+ NI.text_content(node)
130
57
  rescue StandardError
131
58
  nil
132
59
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "diff/lcs"
4
- require "diff/lcs/hunk"
3
+ require "diff/lcs" unless RUBY_ENGINE == "opal"
4
+ require "diff/lcs/hunk" unless RUBY_ENGINE == "opal"
5
5
  require_relative "../debug_output"
6
6
  require_relative "../theme"
7
7
 
@@ -233,7 +233,7 @@ module Canon
233
233
  # @return [Rainbow::Presenter] Colorized presenter
234
234
  def apply_color(presenter, color)
235
235
  valid_colors = normalize_color_for_rainbow(color)
236
- valid_colors.each { |c| presenter = presenter.send(c) }
236
+ valid_colors.each { |c| presenter = presenter.public_send(c) }
237
237
  presenter
238
238
  end
239
239
 
@@ -385,7 +385,7 @@ module Canon
385
385
  rainbow = Rainbow.new
386
386
  rainbow.enabled = true
387
387
  presenter = rainbow.wrap(text)
388
- valid_colors.each { |c| presenter = presenter.send(c) }
388
+ valid_colors.each { |c| presenter = presenter.public_send(c) }
389
389
  presenter.to_s
390
390
  end
391
391
 
@@ -675,13 +675,13 @@ module Canon
675
675
  # Handle Rainbow color methods - :bright_blue -> .blue.bright, etc.
676
676
  if color.to_s.start_with?("bright_")
677
677
  base_color = color.to_s.sub(/^bright_/, "").to_sym
678
- presenter = presenter.send(base_color).bright
678
+ presenter = presenter.public_send(base_color).bright
679
679
  elsif color.to_s.start_with?("light_")
680
680
  # Rainbow doesn't have light_ versions, treat as white on bg
681
681
  base_color = color.to_s.sub(/^light_/, "").to_sym
682
- presenter = presenter.send(base_color)
682
+ presenter = presenter.public_send(base_color)
683
683
  else
684
- presenter = presenter.send(color)
684
+ presenter = presenter.public_send(color)
685
685
  end
686
686
 
687
687
  presenter.to_s
@@ -253,7 +253,7 @@ module Canon
253
253
 
254
254
  if color && @use_color
255
255
  require "rainbow"
256
- Rainbow(visual).send(color).bright.to_s
256
+ Rainbow(visual).public_send(color).bright.to_s
257
257
  else
258
258
  visual
259
259
  end
@@ -151,7 +151,7 @@ module Canon
151
151
  # Apply color if provided and color is enabled
152
152
  if color && @use_color
153
153
  require "rainbow"
154
- Rainbow(visual).send(color).bright.to_s
154
+ Rainbow(visual).public_send(color).bright.to_s
155
155
  else
156
156
  visual
157
157
  end
@@ -1205,11 +1205,11 @@ informative: false, formatting: false)
1205
1205
  # Apply effect if specified (map :strikethrough to :cross_out for Rainbow)
1206
1206
  if effect
1207
1207
  rainbow_effect = effect == :strikethrough ? :cross_out : effect
1208
- presenter = presenter.send(rainbow_effect)
1208
+ presenter = presenter.public_send(rainbow_effect)
1209
1209
  end
1210
1210
 
1211
1211
  # Apply color if specified
1212
- presenter = presenter.send(color) if color
1212
+ presenter = presenter.public_send(color) if color
1213
1213
 
1214
1214
  presenter.to_s
1215
1215
  else
@@ -261,7 +261,7 @@ module Canon
261
261
 
262
262
  if color && @use_color
263
263
  require "rainbow"
264
- Rainbow(visual).send(color).bright.to_s
264
+ Rainbow(visual).public_send(color).bright.to_s
265
265
  else
266
266
  visual
267
267
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "paint"
3
+ require "paint" unless RUBY_ENGINE == "opal"
4
4
 
5
5
  module Canon
6
6
  class DiffFormatter
@@ -26,7 +26,7 @@ show_diffs: :all, theme: nil)
26
26
  # @return [String] Formatted output
27
27
  def format(differences, _format)
28
28
  # Handle both ComparisonResult (production) and Array (low-level tests)
29
- if differences.respond_to?(:equivalent?)
29
+ if differences.is_a?(Canon::Comparison::ComparisonResult)
30
30
  # ComparisonResult object
31
31
  return success_message if differences.equivalent?
32
32
 
@@ -130,15 +130,11 @@ show_diffs: :all, theme: nil)
130
130
  return differences if @show_diffs.nil? || @show_diffs == :all
131
131
 
132
132
  differences.select do |diff|
133
- # Handle both DiffNode objects and legacy Hash format
134
- is_normative = if diff.respond_to?(:normative?)
135
- diff.normative?
136
- elsif diff.is_a?(Hash) && diff.key?(:normative)
137
- diff[:normative]
138
- else
139
- # Default to normative if unknown
140
- true
141
- end
133
+ is_normative = begin
134
+ diff.normative?
135
+ rescue NoMethodError
136
+ diff.is_a?(Hash) && diff.key?(:normative) ? diff[:normative] : true
137
+ end
142
138
 
143
139
  case @show_diffs
144
140
  when :normative
@@ -207,9 +203,9 @@ show_diffs: :all, theme: nil)
207
203
  parts = []
208
204
  current = node
209
205
 
210
- while current.respond_to?(:name)
206
+ while Canon::XmlParsing.xml_node?(current) || current.is_a?(Canon::Xml::Node)
211
207
  parts.unshift(current.name) if current.name
212
- current = current.parent if current.respond_to?(:parent)
208
+ current = current.parent
213
209
  end
214
210
 
215
211
  parts.join(".")
@@ -328,13 +324,13 @@ show_diffs: :all, theme: nil)
328
324
  # Handle bright_ colors: :bright_blue -> .blue.bright
329
325
  if c.to_s.start_with?("bright_")
330
326
  base = c.to_s.sub(/^bright_/, "").to_sym
331
- presenter = presenter.send(base).bright
327
+ presenter = presenter.public_send(base).bright
332
328
  elsif c.to_s.start_with?("light_")
333
329
  # Rainbow doesn't have light_ versions
334
330
  base = c.to_s.sub(/^light_/, "").to_sym
335
- presenter = presenter.send(base)
331
+ presenter = presenter.public_send(base)
336
332
  else
337
- presenter = presenter.send(c)
333
+ presenter = presenter.public_send(c)
338
334
  end
339
335
  end
340
336
  presenter.to_s
@@ -171,7 +171,8 @@ module Canon
171
171
  text2 = extract_text(node2)
172
172
 
173
173
  # Show parent element if available
174
- if node1.respond_to?(:parent) && node1.parent.respond_to?(:name)
174
+ if (Canon::XmlParsing.xml_node?(node1) || node1.is_a?(Canon::Xml::Node)) &&
175
+ node1.parent
175
176
  output << "#{prefix} #{colorize(
176
177
  "Element: <#{node1.parent.name}>",
177
178
  theme_color(:informative, :content) || :blue,
@@ -274,9 +275,9 @@ module Canon
274
275
  parts = []
275
276
  current = node
276
277
 
277
- while current.respond_to?(:name)
278
+ while Canon::XmlParsing.xml_node?(current) || current.is_a?(Canon::Xml::Node)
278
279
  parts.unshift(current.name) if current.name
279
- current = current.parent if current.respond_to?(:parent)
280
+ current = current.parent
280
281
  end
281
282
 
282
283
  parts.join(".")
@@ -287,13 +288,10 @@ module Canon
287
288
  # @param node [Object] Node with content or text
288
289
  # @return [String] Text content
289
290
  def extract_text(node)
290
- if node.respond_to?(:value)
291
- # CommentNode and similar nodes use .value
292
- node.value.to_s
293
- elsif node.respond_to?(:content)
294
- node.content.to_s
295
- elsif node.respond_to?(:text)
296
- node.text.to_s
291
+ if node.is_a?(Canon::Xml::Node)
292
+ node.text_content.to_s
293
+ elsif Canon::XmlParsing.xml_node?(node)
294
+ Canon::XmlParsing.text_content(node).to_s
297
295
  else
298
296
  ""
299
297
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "paint"
3
+ require "paint" unless RUBY_ENGINE == "opal"
4
4
 
5
5
  module Canon
6
6
  class DiffFormatter
@@ -283,22 +283,8 @@ module Canon
283
283
  content = node.value.to_s
284
284
  preview = content.length > 30 ? "#{content[0..27]}..." : content
285
285
  "<!--#{preview}-->"
286
- elsif node.respond_to?(:name)
286
+ elsif Canon::XmlParsing.xml_node?(node) || node.is_a?(Canon::Xml::Node)
287
287
  "<#{node.name}>"
288
- elsif node.respond_to?(:content)
289
- content = node.content.to_s
290
- if content.length > 30
291
- "\"#{content[0..27]}...\""
292
- else
293
- "\"#{content || ''}\""
294
- end
295
- elsif node.respond_to?(:text)
296
- text = node.text.to_s
297
- if text.length > 30
298
- "\"#{text[0..27]}...\""
299
- else
300
- "\"#{text || ''}\""
301
- end
302
288
  else
303
289
  node.class.name
304
290
  end
@@ -311,14 +297,16 @@ module Canon
311
297
 
312
298
  # For attribute differences, show which attributes differ
313
299
  if diff.dimension == :attribute_whitespace &&
314
- node1.respond_to?(:attributes) && node2.respond_to?(:attributes)
300
+ (Canon::XmlParsing.xml_node?(node1) || node1.is_a?(Canon::Xml::Node)) &&
301
+ (Canon::XmlParsing.xml_node?(node2) || node2.is_a?(Canon::Xml::Node))
315
302
  attrs1 = format_attributes(node1)
316
303
  attrs2 = format_attributes(node2)
317
304
  return ["<#{node1.name}> #{attrs1}", "<#{node2.name}> #{attrs2}"]
318
305
  end
319
306
 
320
307
  # For element differences, show element names
321
- if node1.respond_to?(:name) && node2.respond_to?(:name)
308
+ if (Canon::XmlParsing.xml_node?(node1) || node1.is_a?(Canon::Xml::Node)) &&
309
+ (Canon::XmlParsing.xml_node?(node2) || node2.is_a?(Canon::Xml::Node))
322
310
  if node1.name == node2.name
323
311
  # Same element name, different content
324
312
  end
@@ -340,7 +328,7 @@ module Canon
340
328
  end
341
329
 
342
330
  def format_attributes(node)
343
- return "" unless node.respond_to?(:attributes)
331
+ return "" unless Canon::XmlParsing.xml_node?(node) || node.is_a?(Canon::Xml::Node)
344
332
 
345
333
  attrs = node.attributes
346
334
  return "" if attrs.empty?
@@ -350,9 +338,9 @@ module Canon
350
338
  name = if key.is_a?(String)
351
339
  key
352
340
  else
353
- (key.respond_to?(:name) ? key.name : key.to_s)
341
+ key.name
354
342
  end
355
- value = val.respond_to?(:value) ? val.value : val.to_s
343
+ value = val.is_a?(String) ? val : val.value
356
344
  "#{name}=\"#{value}\""
357
345
  end.sort
358
346
 
@@ -365,10 +353,10 @@ module Canon
365
353
  end
366
354
 
367
355
  def get_node_content(node)
368
- if node.respond_to?(:content)
369
- node.content.to_s
370
- elsif node.respond_to?(:text)
371
- node.text.to_s
356
+ if Canon::XmlParsing.xml_node?(node)
357
+ Canon::XmlParsing.text_content(node).to_s
358
+ elsif node.is_a?(Canon::Xml::Node)
359
+ node.text_content.to_s
372
360
  else
373
361
  ""
374
362
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rainbow"
3
+ require "rainbow" unless RUBY_ENGINE == "opal"
4
4
 
5
5
  module Canon
6
6
  class DiffFormatter
@@ -19,7 +19,7 @@ module Canon
19
19
  def self.colorize(text, color, use_color, bold: false)
20
20
  return text unless use_color
21
21
 
22
- presenter = Rainbow(text).send(color)
22
+ presenter = Rainbow(text).public_send(color)
23
23
  presenter = presenter.bright if bold
24
24
  presenter.to_s
25
25
  end