canon 0.2.11 → 0.3.0

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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +62 -25
  3. data/CLAUDE.md +197 -0
  4. data/Rakefile +5 -2
  5. data/lib/canon/cache.rb +3 -1
  6. data/lib/canon/cli.rb +0 -3
  7. data/lib/canon/commands/diff_command.rb +1 -8
  8. data/lib/canon/commands/format_command.rb +0 -4
  9. data/lib/canon/commands.rb +9 -0
  10. data/lib/canon/comparison/child_realignment.rb +0 -2
  11. data/lib/canon/comparison/compare_profile.rb +30 -36
  12. data/lib/canon/comparison/comparison_result.rb +1 -4
  13. data/lib/canon/comparison/diff_node_builder.rb +353 -0
  14. data/lib/canon/comparison/dimensions/dimension.rb +51 -0
  15. data/lib/canon/comparison/dimensions/dimension_set.rb +49 -0
  16. data/lib/canon/comparison/dimensions/registry.rb +101 -60
  17. data/lib/canon/comparison/dimensions.rb +15 -46
  18. data/lib/canon/comparison/format_detector.rb +27 -29
  19. data/lib/canon/comparison/html_comparator.rb +20 -143
  20. data/lib/canon/comparison/html_compare_profile.rb +15 -18
  21. data/lib/canon/comparison/json_comparator.rb +4 -165
  22. data/lib/canon/comparison/json_parser.rb +0 -2
  23. data/lib/canon/comparison/markup_comparator.rb +14 -210
  24. data/lib/canon/comparison/match_options/base_resolver.rb +18 -29
  25. data/lib/canon/comparison/match_options/json_resolver.rb +4 -28
  26. data/lib/canon/comparison/match_options/xml_resolver.rb +4 -45
  27. data/lib/canon/comparison/match_options/yaml_resolver.rb +4 -30
  28. data/lib/canon/comparison/match_options.rb +13 -88
  29. data/lib/canon/comparison/node_inspector.rb +5 -9
  30. data/lib/canon/comparison/pipeline.rb +269 -0
  31. data/lib/canon/comparison/profile_definition.rb +0 -2
  32. data/lib/canon/comparison/ruby_object_comparator.rb +1 -1
  33. data/lib/canon/comparison/strategies/match_strategy_factory.rb +9 -58
  34. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +4 -11
  35. data/lib/canon/comparison/strategies.rb +16 -0
  36. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +1 -4
  37. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +0 -3
  38. data/lib/canon/comparison/xml_comparator/child_comparison.rb +0 -6
  39. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +1 -6
  40. data/lib/canon/comparison/xml_comparator/node_parser.rb +3 -7
  41. data/lib/canon/comparison/xml_comparator.rb +4 -492
  42. data/lib/canon/comparison/xml_comparator_helpers.rb +21 -0
  43. data/lib/canon/comparison/xml_node_comparison.rb +6 -121
  44. data/lib/canon/comparison/yaml_comparator.rb +0 -3
  45. data/lib/canon/comparison.rb +144 -267
  46. data/lib/canon/config/config_dsl.rb +159 -0
  47. data/lib/canon/config/env_provider.rb +0 -3
  48. data/lib/canon/config/env_schema.rb +48 -58
  49. data/lib/canon/config/profile_loader.rb +0 -1
  50. data/lib/canon/config.rb +116 -468
  51. data/lib/canon/diff/diff_block_builder.rb +0 -2
  52. data/lib/canon/diff/diff_classifier.rb +0 -5
  53. data/lib/canon/diff/diff_context.rb +0 -2
  54. data/lib/canon/diff/diff_context_builder.rb +0 -2
  55. data/lib/canon/diff/diff_line_builder.rb +0 -3
  56. data/lib/canon/diff/diff_node_enricher.rb +0 -4
  57. data/lib/canon/diff/diff_node_mapper.rb +0 -4
  58. data/lib/canon/diff/diff_report_builder.rb +0 -4
  59. data/lib/canon/diff/formatting_detector.rb +0 -1
  60. data/lib/canon/diff/node_serializer.rb +0 -7
  61. data/lib/canon/diff.rb +39 -0
  62. data/lib/canon/diff_formatter/by_line/base_formatter.rb +5 -19
  63. data/lib/canon/diff_formatter/by_line/html_formatter.rb +8 -21
  64. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -3
  65. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -3
  66. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +7 -26
  67. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -3
  68. data/lib/canon/diff_formatter/by_line_formatter.rb +1 -2
  69. data/lib/canon/diff_formatter/by_object/base_formatter.rb +8 -15
  70. data/lib/canon/diff_formatter/by_object/json_formatter.rb +0 -2
  71. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +0 -2
  72. data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +0 -2
  73. data/lib/canon/diff_formatter/debug_output.rb +0 -2
  74. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +24 -58
  75. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +0 -2
  76. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +3 -4
  77. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +1 -7
  78. data/lib/canon/diff_formatter/diff_detail_formatter.rb +0 -7
  79. data/lib/canon/diff_formatter/diff_detail_formatter_helpers.rb +23 -0
  80. data/lib/canon/diff_formatter/theme.rb +2 -2
  81. data/lib/canon/diff_formatter.rb +13 -13
  82. data/lib/canon/formatters/html4_formatter.rb +0 -2
  83. data/lib/canon/formatters/html5_formatter.rb +0 -2
  84. data/lib/canon/formatters/html_formatter.rb +0 -3
  85. data/lib/canon/formatters/json_formatter.rb +0 -1
  86. data/lib/canon/formatters/xml_formatter.rb +0 -4
  87. data/lib/canon/formatters/yaml_formatter.rb +0 -1
  88. data/lib/canon/formatters.rb +16 -0
  89. data/lib/canon/html/data_model.rb +0 -10
  90. data/lib/canon/html/nokogiri_support.rb +60 -0
  91. data/lib/canon/html.rb +5 -3
  92. data/lib/canon/options/cli_generator.rb +0 -2
  93. data/lib/canon/options/registry.rb +0 -2
  94. data/lib/canon/options.rb +9 -0
  95. data/lib/canon/pretty_printer/html.rb +0 -1
  96. data/lib/canon/pretty_printer/xml.rb +6 -1
  97. data/lib/canon/pretty_printer/xml_normalized.rb +5 -7
  98. data/lib/canon/pretty_printer.rb +12 -0
  99. data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
  100. data/lib/canon/tree_diff/adapters.rb +14 -0
  101. data/lib/canon/tree_diff/core/attribute_comparator.rb +0 -6
  102. data/lib/canon/tree_diff/core/node_signature.rb +1 -1
  103. data/lib/canon/tree_diff/core/tree_node.rb +13 -5
  104. data/lib/canon/tree_diff/core.rb +17 -0
  105. data/lib/canon/tree_diff/matchers/hash_matcher.rb +0 -7
  106. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -5
  107. data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -5
  108. data/lib/canon/tree_diff/matchers.rb +15 -0
  109. data/lib/canon/tree_diff/operation_converter.rb +0 -8
  110. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +2 -12
  111. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +13 -7
  112. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +2 -2
  113. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +4 -6
  114. data/lib/canon/tree_diff/operation_converter_helpers.rb +18 -0
  115. data/lib/canon/tree_diff/operations/operation_detector.rb +2 -5
  116. data/lib/canon/tree_diff/operations.rb +13 -0
  117. data/lib/canon/tree_diff.rb +26 -27
  118. data/lib/canon/validators/base_validator.rb +0 -2
  119. data/lib/canon/validators/html_validator.rb +0 -1
  120. data/lib/canon/validators/json_validator.rb +0 -1
  121. data/lib/canon/validators/xml_validator.rb +0 -1
  122. data/lib/canon/validators/yaml_validator.rb +0 -1
  123. data/lib/canon/validators.rb +12 -0
  124. data/lib/canon/version.rb +1 -1
  125. data/lib/canon/xml/c14n.rb +0 -4
  126. data/lib/canon/xml/data_model.rb +206 -43
  127. data/lib/canon/xml/line_range_mapper.rb +0 -2
  128. data/lib/canon/xml/nodes/attribute_node.rb +0 -2
  129. data/lib/canon/xml/nodes/comment_node.rb +0 -2
  130. data/lib/canon/xml/nodes/element_node.rb +0 -2
  131. data/lib/canon/xml/nodes/namespace_node.rb +0 -2
  132. data/lib/canon/xml/nodes/processing_instruction_node.rb +0 -2
  133. data/lib/canon/xml/nodes/root_node.rb +0 -2
  134. data/lib/canon/xml/nodes/text_node.rb +0 -2
  135. data/lib/canon/xml/nodes.rb +19 -0
  136. data/lib/canon/xml/processor.rb +10 -7
  137. data/lib/canon/xml/sax/moxml_driver.rb +58 -0
  138. data/lib/canon/xml/sax/nokogiri_driver.rb +54 -0
  139. data/lib/canon/xml/sax.rb +37 -0
  140. data/lib/canon/xml/sax_builder.rb +31 -27
  141. data/lib/canon/xml.rb +34 -0
  142. data/lib/canon/xml_backend.rb +40 -19
  143. data/lib/canon/xml_parsing.rb +89 -116
  144. data/lib/canon.rb +26 -17
  145. data/lib/tasks/benchmark_runner.rb +4 -0
  146. data/lib/tasks/performance.rake +2 -58
  147. data/lib/tasks/performance_comparator.rb +132 -65
  148. data/lib/tasks/performance_helpers.rb +4 -249
  149. data/lib/tasks/performance_report.rb +313 -0
  150. metadata +31 -13
  151. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +0 -64
  152. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +0 -64
  153. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +0 -167
  154. data/lib/canon/comparison/dimensions/base_dimension.rb +0 -107
  155. data/lib/canon/comparison/dimensions/comments_dimension.rb +0 -117
  156. data/lib/canon/comparison/dimensions/element_position_dimension.rb +0 -86
  157. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +0 -115
  158. data/lib/canon/comparison/dimensions/text_content_dimension.rb +0 -102
  159. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +0 -300
@@ -53,9 +53,6 @@ expand_difference: false)
53
53
  # @param use_color [Boolean] Whether to use colors
54
54
  # @return [Array] Tuple of [detail1, detail2, changes]
55
55
  def self.format_namespace_uri_details(diff, use_color)
56
- require_relative "color_helper"
57
- require_relative "node_utils"
58
-
59
56
  node1 = extract_node1(diff)
60
57
  node2 = extract_node2(diff)
61
58
 
@@ -87,10 +84,6 @@ expand_difference: false)
87
84
  # @param use_color [Boolean] Whether to use colors
88
85
  # @return [Array] Tuple of [detail1, detail2, changes]
89
86
  def self.format_namespace_declarations_details(diff, use_color)
90
- require_relative "color_helper"
91
- require_relative "node_utils"
92
- require_relative "text_utils"
93
-
94
87
  node1 = extract_node1(diff)
95
88
  node2 = extract_node2(diff)
96
89
 
@@ -174,9 +167,6 @@ expand_difference: false)
174
167
  # @param use_color [Boolean] Whether to use colors
175
168
  # @return [Array] Tuple of [detail1, detail2, changes]
176
169
  def self.format_element_structure_details(diff, use_color)
177
- require_relative "color_helper"
178
- require_relative "node_utils"
179
-
180
170
  node1 = extract_node1(diff)
181
171
  node2 = extract_node2(diff)
182
172
 
@@ -236,9 +226,6 @@ expand_difference: false)
236
226
  # @param use_color [Boolean] Whether to use colors
237
227
  # @return [Array] Tuple of [detail1, detail2, changes]
238
228
  def self.format_attribute_presence_details(diff, use_color)
239
- require_relative "color_helper"
240
- require_relative "node_utils"
241
-
242
229
  node1 = extract_node1(diff)
243
230
  node2 = extract_node2(diff)
244
231
 
@@ -291,9 +278,6 @@ expand_difference: false)
291
278
  # @param use_color [Boolean] Whether to use colors
292
279
  # @return [Array] Tuple of [detail1, detail2, changes]
293
280
  def self.format_attribute_values_details(diff, use_color)
294
- require_relative "color_helper"
295
- require_relative "node_utils"
296
-
297
281
  node1 = extract_node1(diff)
298
282
  node2 = extract_node2(diff)
299
283
 
@@ -330,9 +314,6 @@ expand_difference: false)
330
314
  # @param use_color [Boolean] Whether to use colors
331
315
  # @return [Array] Tuple of [detail1, detail2, changes]
332
316
  def self.format_attribute_order_details(diff, use_color)
333
- require_relative "color_helper"
334
- require_relative "node_utils"
335
-
336
317
  node1 = extract_node1(diff)
337
318
  node2 = extract_node2(diff)
338
319
 
@@ -361,10 +342,6 @@ expand_difference: false)
361
342
  # @param compact [Boolean] Whether to serialize element nodes as compact XML
362
343
  # @return [Array] Tuple of [detail1, detail2, changes]
363
344
  def self.format_text_content_details(diff, use_color, compact: false)
364
- require_relative "color_helper"
365
- require_relative "node_utils"
366
- require_relative "text_utils"
367
-
368
345
  node1 = extract_node1(diff)
369
346
  node2 = extract_node2(diff)
370
347
 
@@ -465,10 +442,6 @@ expand_difference: false)
465
442
  # @param use_color [Boolean] Whether to apply ANSI colours
466
443
  # @return [Array<String>] Tuple of [detail1, detail2, changes]
467
444
  def self.format_text_content_one_sided(node1, node2, use_color)
468
- require_relative "color_helper"
469
- require_relative "node_utils"
470
- require_relative "text_utils"
471
-
472
445
  present = node1 || node2
473
446
 
474
447
  # Defensive: if a one-sided text-content diff carries an
@@ -515,10 +488,6 @@ expand_difference: false)
515
488
  # @param use_color [Boolean] Whether to use colors
516
489
  # @return [Array] Tuple of [detail1, detail2, changes]
517
490
  def self.format_whitespace_adjacency_details(diff, use_color)
518
- require_relative "color_helper"
519
- require_relative "node_utils"
520
- require_relative "text_utils"
521
-
522
491
  node1 = extract_node1(diff)
523
492
  node2 = extract_node2(diff)
524
493
 
@@ -568,10 +537,6 @@ expand_difference: false)
568
537
  # @param use_color [Boolean] Whether to use colors
569
538
  # @return [Array] Tuple of [detail1, detail2, changes]
570
539
  def self.format_structural_whitespace_details(diff, use_color)
571
- require_relative "color_helper"
572
- require_relative "node_utils"
573
- require_relative "text_utils"
574
-
575
540
  node1 = extract_node1(diff)
576
541
  node2 = extract_node2(diff)
577
542
 
@@ -594,9 +559,6 @@ expand_difference: false)
594
559
  # @param use_color [Boolean] Whether to use colors
595
560
  # @return [Array] Tuple of [detail1, detail2, changes]
596
561
  def self.format_comments_details(diff, use_color)
597
- require_relative "color_helper"
598
- require_relative "node_utils"
599
-
600
562
  node1 = extract_node1(diff)
601
563
  node2 = extract_node2(diff)
602
564
 
@@ -619,8 +581,6 @@ expand_difference: false)
619
581
  # @param use_color [Boolean] Whether to use colors
620
582
  # @return [Array] Tuple of [detail1, detail2, changes]
621
583
  def self.format_hash_diff_details(diff, use_color)
622
- require_relative "color_helper"
623
-
624
584
  detail1 = if diff.is_a?(Hash) && diff[:value1]
625
585
  ColorHelper.colorize(format_json_value(diff[:value1]),
626
586
  :red, use_color)
@@ -653,9 +613,6 @@ expand_difference: false)
653
613
  # @param compact [Boolean] Whether to serialize element nodes as compact XML
654
614
  # @return [Array] Tuple of [detail1, detail2, changes]
655
615
  def self.format_fallback_details(diff, use_color, compact: false)
656
- require_relative "color_helper"
657
- require_relative "node_utils"
658
-
659
616
  node1 = extract_node1(diff)
660
617
  node2 = extract_node2(diff)
661
618
 
@@ -749,8 +706,7 @@ expand_difference: false)
749
706
 
750
707
  declarations = {}
751
708
 
752
- # Handle Canon::Xml::Node (uses namespace_nodes)
753
- if node.respond_to?(:namespace_nodes)
709
+ if node.is_a?(Canon::Xml::Node)
754
710
  node.namespace_nodes.each do |ns|
755
711
  next if ns.prefix == "xml" && ns.uri == "http://www.w3.org/XML/1998/namespace"
756
712
 
@@ -760,30 +716,28 @@ expand_difference: false)
760
716
  return declarations
761
717
  end
762
718
 
763
- # Handle Nokogiri/Moxml nodes (use attributes)
764
- raw_attrs = node.respond_to?(:attribute_nodes) ? node.attribute_nodes : node.attributes
719
+ # Handle Nokogiri/Moxml nodes (use attribute_nodes or attributes).
720
+ raw_attrs = if node.is_a?(Nokogiri::XML::Node)
721
+ node.attribute_nodes
722
+ else
723
+ node.attributes
724
+ end
765
725
 
766
726
  if raw_attrs.is_a?(Array)
767
727
  raw_attrs.each do |attr|
768
- name = attr.respond_to?(:name) ? attr.name : attr.to_s
769
- value = attr.respond_to?(:value) ? attr.value : attr.to_s
728
+ name = attr.is_a?(Nokogiri::XML::Attr) ? attr.name : attr.to_s
729
+ value = attr.is_a?(Nokogiri::XML::Attr) ? attr.value : attr.to_s
770
730
 
771
731
  if namespace_declaration?(name)
772
732
  prefix = name == "xmlns" ? "" : name.split(":", 2)[1]
773
733
  declarations[prefix] = value
774
734
  end
775
735
  end
776
- elsif raw_attrs.respond_to?(:each)
736
+ elsif raw_attrs.is_a?(Hash)
777
737
  raw_attrs.each do |key, val|
778
- name = if key.is_a?(String)
779
- key
780
- else
781
- (key.respond_to?(:name) ? key.name : key.to_s)
782
- end
783
- value = if val.respond_to?(:value)
738
+ name = hash_key_to_name(key)
739
+ value = if val.is_a?(Nokogiri::XML::Attr)
784
740
  val.value
785
- elsif val.respond_to?(:content)
786
- val.content
787
741
  else
788
742
  val.to_s
789
743
  end
@@ -805,6 +759,18 @@ expand_difference: false)
805
759
  def self.namespace_declaration?(name)
806
760
  name == "xmlns" || name.to_s.start_with?("xmlns:")
807
761
  end
762
+
763
+ # Coerce a hash key (from a backend-supplied attribute hash) into a
764
+ # plain String attribute name.
765
+ #
766
+ # @param key [String, Nokogiri::XML::Attr, Object]
767
+ # @return [String]
768
+ def self.hash_key_to_name(key)
769
+ return key if key.is_a?(String)
770
+ return key.name if key.is_a?(Nokogiri::XML::Attr)
771
+
772
+ key.to_s
773
+ end
808
774
  end
809
775
  end
810
776
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../xml/namespace_helper"
4
-
5
3
  module Canon
6
4
  class DiffFormatter
7
5
  module DiffDetailFormatterHelpers
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "cgi"
4
- require_relative "../../xml/namespace_helper"
5
4
 
6
5
  module Canon
7
6
  class DiffFormatter
@@ -94,7 +93,7 @@ module Canon
94
93
  def self.get_namespace_uri_for_display(node)
95
94
  return "" unless node
96
95
 
97
- Canon::Comparison::Canon::Comparison::NodeInspector.namespace_uri(node).to_s
96
+ Canon::Comparison::NodeInspector.namespace_uri(node).to_s
98
97
  end
99
98
 
100
99
  # --- Display helpers ---
@@ -227,7 +226,7 @@ module Canon
227
226
  end
228
227
 
229
228
  def serialize_backend_node_compact(node)
230
- if XmlBackend.nokogiri? && node.is_a?(Nokogiri::XML::Node)
229
+ if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
231
230
  serialize_nokogiri_node_compact(node)
232
231
  elsif node.is_a?(Canon::Xml::Node)
233
232
  serialize_node_compact(node)
@@ -257,7 +256,7 @@ module Canon
257
256
  end
258
257
 
259
258
  def serialize_backend_open_tag(node)
260
- if XmlBackend.nokogiri? && node.is_a?(Nokogiri::XML::Element)
259
+ if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
261
260
  tag = node.name.to_s
262
261
  attrs = node.attribute_nodes.map do |a|
263
262
  " #{a.name}=\"#{CGI.escapeHTML(a.value.to_s)}\""
@@ -45,13 +45,7 @@ module Canon
45
45
  def self.extract_content_preview(node, max_length = 50)
46
46
  return "" unless node
47
47
 
48
- text = if node.respond_to?(:text)
49
- node.text
50
- elsif node.respond_to?(:content)
51
- node.content
52
- else
53
- node.to_s
54
- end
48
+ text = Canon::Comparison::NodeInspector.text_content(node).to_s
55
49
 
56
50
  return "" if text.nil? || text.empty?
57
51
 
@@ -1,13 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rainbow" unless RUBY_ENGINE == "opal"
4
- require_relative "../xml/namespace_helper"
5
- # DiffDetailFormatter helper modules
6
- require_relative "diff_detail_formatter/text_utils"
7
- require_relative "diff_detail_formatter/color_helper"
8
- require_relative "diff_detail_formatter/location_extractor"
9
- require_relative "diff_detail_formatter/node_utils"
10
- require_relative "diff_detail_formatter/dimension_formatter"
11
4
 
12
5
  module Canon
13
6
  class DiffFormatter
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ class DiffFormatter
5
+ # Helper modules consumed by {DiffDetailFormatter} to format per-dimension
6
+ # difference details. Files live under `diff_detail_formatter/` but the
7
+ # constants live under this namespace.
8
+ #
9
+ # Children are autoloaded — never `require_relative` them.
10
+ module DiffDetailFormatterHelpers
11
+ autoload :ColorHelper,
12
+ "canon/diff_formatter/diff_detail_formatter/color_helper"
13
+ autoload :DimensionFormatter,
14
+ "canon/diff_formatter/diff_detail_formatter/dimension_formatter"
15
+ autoload :LocationExtractor,
16
+ "canon/diff_formatter/diff_detail_formatter/location_extractor"
17
+ autoload :NodeUtils,
18
+ "canon/diff_formatter/diff_detail_formatter/node_utils"
19
+ autoload :TextUtils,
20
+ "canon/diff_formatter/diff_detail_formatter/text_utils"
21
+ end
22
+ end
23
+ end
@@ -125,11 +125,11 @@ module Canon
125
125
  },
126
126
  added: {
127
127
  marker: { color: :green, bg: nil, bold: false },
128
- content: { color: :green, bg: nil, bold: false,
128
+ content: { color: :green, bg: nil, bold: false,
129
129
  underline: false, strikethrough: false },
130
130
  },
131
131
  changed: {
132
- marker: { color: :yellow, bg: nil, bold: true },
132
+ marker: { color: :yellow, bg: nil, bold: true },
133
133
  content_old: { color: :bright_red, bg: nil, bold: false,
134
134
  underline: false, strikethrough: true },
135
135
  content_new: { color: :bright_green, bg: nil, bold: false,
@@ -2,14 +2,6 @@
2
2
 
3
3
  require "paint" unless RUBY_ENGINE == "opal"
4
4
  require "yaml" unless RUBY_ENGINE == "opal"
5
- require_relative "comparison"
6
- require_relative "diff/diff_block"
7
- require_relative "diff/diff_context"
8
- require_relative "diff/diff_report"
9
- require_relative "diff_formatter/debug_output"
10
- require_relative "diff_formatter/by_line_formatter"
11
- require_relative "diff_formatter/by_object_formatter"
12
- require_relative "diff_formatter/pretty_diff_formatter"
13
5
 
14
6
  module Canon
15
7
  # Formatter for displaying semantic differences with color support
@@ -65,6 +57,16 @@ module Canon
65
57
  # )
66
58
  #
67
59
  class DiffFormatter
60
+ autoload :ByLineFormatter, "canon/diff_formatter/by_line_formatter"
61
+ autoload :ByObjectFormatter, "canon/diff_formatter/by_object_formatter"
62
+ autoload :DebugOutput, "canon/diff_formatter/debug_output"
63
+ autoload :DiffDetailFormatter, "canon/diff_formatter/diff_detail_formatter"
64
+ autoload :DiffDetailFormatterHelpers,
65
+ "canon/diff_formatter/diff_detail_formatter_helpers"
66
+ autoload :Legend, "canon/diff_formatter/legend"
67
+ autoload :PrettyDiffFormatter, "canon/diff_formatter/pretty_diff_formatter"
68
+ autoload :Theme, "canon/diff_formatter/theme"
69
+
68
70
  # Namespace for by-object mode formatters
69
71
  module ByObject
70
72
  autoload :BaseFormatter, "canon/diff_formatter/by_object/base_formatter"
@@ -76,6 +78,7 @@ module Canon
76
78
  # Namespace for by-line mode formatters
77
79
  module ByLine
78
80
  autoload :BaseFormatter, "canon/diff_formatter/by_line/base_formatter"
81
+ autoload :HtmlFormatter, "canon/diff_formatter/by_line/html_formatter"
79
82
  autoload :SimpleFormatter, "canon/diff_formatter/by_line/simple_formatter"
80
83
  autoload :XmlFormatter, "canon/diff_formatter/by_line/xml_formatter"
81
84
  autoload :JsonFormatter, "canon/diff_formatter/by_line/json_formatter"
@@ -308,11 +311,10 @@ module Canon
308
311
  # In by-line mode with both docs present, always use by-line diff
309
312
  if @mode == :by_line && doc1 && doc2
310
313
  doc1, doc2 = apply_display_preprocessing(doc1, doc2, format)
311
- # rubocop:disable Layout/HashAlignment
314
+ # rubocop:disable-next Layout/HashAlignment
312
315
  return by_line_formatter.format(doc1, doc2, format: format,
313
316
  html_version: html_version,
314
317
  differences: differences)
315
- # rubocop:enable Layout/HashAlignment
316
318
  end
317
319
 
318
320
  # In pretty_diff mode, always use text-LCS diff (bypasses DiffNodeMapper).
@@ -340,11 +342,10 @@ module Canon
340
342
  doc1, doc2 = apply_display_preprocessing(doc1, doc2,
341
343
  format)
342
344
  end
343
- # rubocop:disable Layout/HashAlignment
345
+ # rubocop:disable-next Layout/HashAlignment
344
346
  by_line_formatter.format(doc1, doc2, format: format,
345
347
  html_version: html_version,
346
348
  differences: differences)
347
- # rubocop:enable Layout/HashAlignment
348
349
  when :pretty_diff
349
350
  d1, d2 = if doc1 && doc2
350
351
  apply_display_preprocessing(doc1, doc2,
@@ -420,7 +421,6 @@ module Canon
420
421
  # 2. Semantic Diff Report (ALWAYS if diffs exist)
421
422
  if comparison_result.is_a?(Canon::Comparison::ComparisonResult) &&
422
423
  comparison_result.differences.any?
423
- require_relative "diff_formatter/diff_detail_formatter"
424
424
  output << DiffDetailFormatter.format_report(
425
425
  comparison_result.differences,
426
426
  use_color: @use_color,
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "html_formatter_base"
4
-
5
3
  module Canon
6
4
  module Formatters
7
5
  # HTML4 formatter using Nokogiri::HTML parser
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "html_formatter_base"
4
-
5
3
  module Canon
6
4
  module Formatters
7
5
  # HTML5 formatter using Nokogiri::HTML5 parser
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
- require_relative "html_formatter_base"
5
- require_relative "../pretty_printer/html"
6
- require_relative "../validators/html_validator"
7
4
 
8
5
  module Canon
9
6
  module Formatters
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "../validators/json_validator"
5
4
 
6
5
  module Canon
7
6
  module Formatters
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
- require_relative "../xml_backend"
5
- require_relative "../xml/c14n"
6
- require_relative "../pretty_printer/xml"
7
- require_relative "../validators/xml_validator"
8
4
 
9
5
  module Canon
10
6
  module Formatters
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "../validators/yaml_validator"
5
4
 
6
5
  module Canon
7
6
  module Formatters
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ # Format-specific canonicalizers and parsers for the top-level
5
+ # `Canon.format` / `Canon.parse` API. Distinct from {DiffFormatter},
6
+ # which renders comparison output.
7
+ module Formatters
8
+ autoload :Html4Formatter, "canon/formatters/html4_formatter"
9
+ autoload :Html5Formatter, "canon/formatters/html5_formatter"
10
+ autoload :HtmlFormatter, "canon/formatters/html_formatter"
11
+ autoload :HtmlFormatterBase, "canon/formatters/html_formatter_base"
12
+ autoload :JsonFormatter, "canon/formatters/json_formatter"
13
+ autoload :XmlFormatter, "canon/formatters/xml_formatter"
14
+ autoload :YamlFormatter, "canon/formatters/yaml_formatter"
15
+ end
16
+ end
@@ -1,14 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
- require_relative "../data_model"
5
- require_relative "../xml/nodes/root_node"
6
- require_relative "../xml/nodes/element_node"
7
- require_relative "../xml/nodes/namespace_node"
8
- require_relative "../xml/nodes/attribute_node"
9
- require_relative "../xml/nodes/text_node"
10
- require_relative "../xml/nodes/comment_node"
11
- require_relative "../xml/nodes/processing_instruction_node"
12
4
 
13
5
  module Canon
14
6
  module Html
@@ -81,7 +73,6 @@ module Canon
81
73
  def self.serialize(node)
82
74
  # HTML nodes use the same serialization as XML
83
75
  # Delegate to XML serialization implementation
84
- require_relative "../xml/data_model"
85
76
  Canon::Xml::DataModel.serialize(node)
86
77
  end
87
78
 
@@ -222,7 +213,6 @@ module Canon
222
213
  whitespace_sensitive_tags = %w[pre code textarea script style]
223
214
 
224
215
  # Check if whitespace is between inline siblings
225
- require_relative "../comparison/whitespace_sensitivity"
226
216
  unless whitespace_sensitive_tags.include?(parent_name) ||
227
217
  Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(nokogiri_text)
228
218
  return nil
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Html
5
+ # Nokogiri node utilities for the HTML pipeline.
6
+ #
7
+ # HTML parsing is Nokogiri's job on every CRuby runtime regardless of
8
+ # the XML engine (XmlBackend): moxml has no HTML adapter and leptris no
9
+ # HTML parser. Under Opal Nokogiri is unavailable and these helpers
10
+ # degrade the same way the moxml backend always did (false / nil /
11
+ # raise), which keeps HTML comparison unsupported there.
12
+ module NokogiriSupport
13
+ class << self
14
+ # Whether the node is a document fragment (any variant).
15
+ def document_fragment?(node)
16
+ return false unless defined?(Nokogiri)
17
+
18
+ node.is_a?(Nokogiri::XML::DocumentFragment) ||
19
+ node.is_a?(Nokogiri::HTML4::DocumentFragment) ||
20
+ node.is_a?(Nokogiri::HTML5::DocumentFragment)
21
+ end
22
+
23
+ # Whether the node is an HTML document (any variant).
24
+ def html_document?(node)
25
+ return false unless defined?(Nokogiri)
26
+
27
+ node.is_a?(Nokogiri::HTML::Document) ||
28
+ node.is_a?(Nokogiri::HTML4::Document) ||
29
+ node.is_a?(Nokogiri::HTML5::Document)
30
+ end
31
+
32
+ # Detect HTML version from a Nokogiri node.
33
+ # Returns :html5 or :html4. Defaults to :html5 for non-Nokogiri nodes.
34
+ def html_version_from_node(node)
35
+ return :html5 unless defined?(Nokogiri)
36
+
37
+ if node.is_a?(Nokogiri::HTML5::Document) ||
38
+ node.is_a?(Nokogiri::HTML5::DocumentFragment)
39
+ :html5
40
+ elsif node.is_a?(Nokogiri::HTML4::Document) ||
41
+ node.is_a?(Nokogiri::HTML4::DocumentFragment)
42
+ :html4
43
+ else
44
+ :html5
45
+ end
46
+ end
47
+
48
+ # Parse an HTML string into an XML fragment for the XML comparator.
49
+ def xml_fragment(html_string)
50
+ unless defined?(Nokogiri)
51
+ raise Canon::Error,
52
+ "HTML fragment parsing requires Nokogiri"
53
+ end
54
+
55
+ Nokogiri::XML.fragment(html_string)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/canon/html.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "html/data_model"
4
-
5
3
  module Canon
6
- # HTML-specific functionality for Canon
4
+ # HTML-specific functionality for Canon.
5
+ #
6
+ # Children are autoloaded — never `require_relative` them.
7
7
  module Html
8
+ autoload :DataModel, "canon/html/data_model"
9
+ autoload :NokogiriSupport, "canon/html/nokogiri_support"
8
10
  end
9
11
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "registry"
4
-
5
3
  module Canon
6
4
  module Options
7
5
  # Generates Thor CLI options from the Options Registry
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../comparison/match_options"
4
-
5
3
  module Canon
6
4
  module Options
7
5
  # Centralized registry for all Canon options
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ # CLI option definitions and the option registry.
5
+ module Options
6
+ autoload :CliGenerator, "canon/options/cli_generator"
7
+ autoload :Registry, "canon/options/registry"
8
+ end
9
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
4
  require "stringio"
5
- require_relative "html_void_elements"
6
5
 
7
6
  module Canon
8
7
  module PrettyPrinter
@@ -11,7 +11,12 @@ module Canon
11
11
  end
12
12
 
13
13
  def format(xml_string)
14
- if Canon::XmlBackend.nokogiri?
14
+ # Output parity: pretty-printed bytes are canon's product. The
15
+ # moxml serializer cannot yet match Nokogiri's output (empty
16
+ # element expansion, declaration placement, tab indent), so the
17
+ # Nokogiri pipeline is used whenever available — the moxml branch
18
+ # is the Opal fallback.
19
+ if defined?(Nokogiri)
15
20
  nokogiri_format(xml_string)
16
21
  else
17
22
  moxml_format(xml_string)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
- require_relative "html_void_elements"
5
4
 
6
5
  module Canon
7
6
  module PrettyPrinter
@@ -154,12 +153,12 @@ module Canon
154
153
  # @return [String] Serialized XML, one node per line, with content
155
154
  # whitespace visualized at line boundaries
156
155
  def format(xml_string)
157
- doc = if Canon::XmlBackend.moxml?
158
- Canon::XmlParsing.parse(xml_string)
159
- elsif @html_mode
160
- Nokogiri::HTML5(xml_string)
156
+ # Output parity: see PrettyPrinter::Xml — the Nokogiri pipeline
157
+ # produces canon's byte-stable output; moxml is the Opal fallback.
158
+ doc = if defined?(Nokogiri)
159
+ @html_mode ? Nokogiri::HTML5(xml_string) : Nokogiri::XML(xml_string)
161
160
  else
162
- Nokogiri::XML(xml_string)
161
+ Canon::XmlParsing.parse(xml_string)
163
162
  end
164
163
  lines = []
165
164
 
@@ -398,7 +397,6 @@ module Canon
398
397
 
399
398
  # Load the default visualization map from DiffFormatter constants.
400
399
  def default_vis_map
401
- require_relative "../diff_formatter"
402
400
  Canon::DiffFormatter::DEFAULT_VISUALIZATION_MAP
403
401
  rescue LoadError, NameError
404
402
  { " " => "░", "\t" => "⇥", "\n" => "↵", "\r" => "⏎", "\u00A0" => "␣" }