canon 0.2.9 → 0.2.12

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +21 -22
  3. data/Rakefile +25 -2
  4. data/lib/canon/cache.rb +18 -27
  5. data/lib/canon/cli.rb +0 -3
  6. data/lib/canon/commands/diff_command.rb +0 -6
  7. data/lib/canon/commands/format_command.rb +0 -4
  8. data/lib/canon/commands.rb +9 -0
  9. data/lib/canon/comparison/child_realignment.rb +0 -2
  10. data/lib/canon/comparison/compare_profile.rb +30 -36
  11. data/lib/canon/comparison/comparison_result.rb +0 -2
  12. data/lib/canon/comparison/diff_node_builder.rb +353 -0
  13. data/lib/canon/comparison/dimensions/dimension.rb +51 -0
  14. data/lib/canon/comparison/dimensions/dimension_set.rb +49 -0
  15. data/lib/canon/comparison/dimensions/registry.rb +101 -60
  16. data/lib/canon/comparison/dimensions.rb +15 -46
  17. data/lib/canon/comparison/html_comparator.rb +20 -141
  18. data/lib/canon/comparison/html_compare_profile.rb +15 -18
  19. data/lib/canon/comparison/json_comparator.rb +4 -165
  20. data/lib/canon/comparison/json_parser.rb +0 -2
  21. data/lib/canon/comparison/markup_comparator.rb +14 -210
  22. data/lib/canon/comparison/match_options/base_resolver.rb +18 -29
  23. data/lib/canon/comparison/match_options/json_resolver.rb +4 -28
  24. data/lib/canon/comparison/match_options/xml_resolver.rb +4 -45
  25. data/lib/canon/comparison/match_options/yaml_resolver.rb +4 -30
  26. data/lib/canon/comparison/match_options.rb +13 -88
  27. data/lib/canon/comparison/node_inspector.rb +13 -48
  28. data/lib/canon/comparison/pipeline.rb +269 -0
  29. data/lib/canon/comparison/profile_definition.rb +0 -2
  30. data/lib/canon/comparison/ruby_object_comparator.rb +1 -1
  31. data/lib/canon/comparison/strategies/match_strategy_factory.rb +9 -58
  32. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +4 -11
  33. data/lib/canon/comparison/strategies.rb +16 -0
  34. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +19 -5
  35. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +0 -3
  36. data/lib/canon/comparison/xml_comparator/child_comparison.rb +0 -6
  37. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +1 -6
  38. data/lib/canon/comparison/xml_comparator/node_parser.rb +2 -6
  39. data/lib/canon/comparison/xml_comparator.rb +4 -492
  40. data/lib/canon/comparison/xml_comparator_helpers.rb +21 -0
  41. data/lib/canon/comparison/xml_node_comparison.rb +4 -119
  42. data/lib/canon/comparison/yaml_comparator.rb +0 -3
  43. data/lib/canon/comparison.rb +144 -267
  44. data/lib/canon/config/config_dsl.rb +159 -0
  45. data/lib/canon/config/env_provider.rb +0 -3
  46. data/lib/canon/config/env_schema.rb +48 -58
  47. data/lib/canon/config/profile_loader.rb +0 -1
  48. data/lib/canon/config.rb +116 -468
  49. data/lib/canon/diff/diff_block_builder.rb +0 -2
  50. data/lib/canon/diff/diff_classifier.rb +0 -5
  51. data/lib/canon/diff/diff_context.rb +0 -2
  52. data/lib/canon/diff/diff_context_builder.rb +0 -2
  53. data/lib/canon/diff/diff_line_builder.rb +2 -3
  54. data/lib/canon/diff/diff_node_enricher.rb +0 -4
  55. data/lib/canon/diff/diff_node_mapper.rb +10 -12
  56. data/lib/canon/diff/diff_report_builder.rb +0 -4
  57. data/lib/canon/diff/formatting_detector.rb +3 -3
  58. data/lib/canon/diff/node_serializer.rb +0 -7
  59. data/lib/canon/diff/xml_serialization_formatter.rb +0 -3
  60. data/lib/canon/diff.rb +39 -0
  61. data/lib/canon/diff_formatter/by_line/base_formatter.rb +4 -17
  62. data/lib/canon/diff_formatter/by_line/html_formatter.rb +7 -19
  63. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -3
  64. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -3
  65. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +7 -26
  66. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -3
  67. data/lib/canon/diff_formatter/by_object/base_formatter.rb +20 -17
  68. data/lib/canon/diff_formatter/by_object/json_formatter.rb +0 -2
  69. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +119 -3
  70. data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +0 -2
  71. data/lib/canon/diff_formatter/by_object_formatter.rb +1 -5
  72. data/lib/canon/diff_formatter/debug_output.rb +0 -2
  73. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +27 -61
  74. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +26 -29
  75. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +1 -2
  76. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +1 -7
  77. data/lib/canon/diff_formatter/diff_detail_formatter.rb +0 -7
  78. data/lib/canon/diff_formatter/diff_detail_formatter_helpers.rb +23 -0
  79. data/lib/canon/diff_formatter.rb +26 -20
  80. data/lib/canon/formatters/html4_formatter.rb +0 -2
  81. data/lib/canon/formatters/html5_formatter.rb +0 -2
  82. data/lib/canon/formatters/html_formatter.rb +0 -3
  83. data/lib/canon/formatters/json_formatter.rb +0 -1
  84. data/lib/canon/formatters/xml_formatter.rb +0 -4
  85. data/lib/canon/formatters/yaml_formatter.rb +0 -1
  86. data/lib/canon/formatters.rb +16 -0
  87. data/lib/canon/html/data_model.rb +1 -11
  88. data/lib/canon/html.rb +4 -3
  89. data/lib/canon/options/cli_generator.rb +0 -2
  90. data/lib/canon/options/registry.rb +0 -2
  91. data/lib/canon/options.rb +9 -0
  92. data/lib/canon/pretty_printer/html.rb +0 -1
  93. data/lib/canon/pretty_printer/xml_normalized.rb +0 -2
  94. data/lib/canon/pretty_printer.rb +12 -0
  95. data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
  96. data/lib/canon/tree_diff/adapters.rb +14 -0
  97. data/lib/canon/tree_diff/core/attribute_comparator.rb +0 -6
  98. data/lib/canon/tree_diff/core/node_signature.rb +1 -1
  99. data/lib/canon/tree_diff/core/tree_node.rb +12 -5
  100. data/lib/canon/tree_diff/core.rb +17 -0
  101. data/lib/canon/tree_diff/matchers/hash_matcher.rb +0 -7
  102. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -5
  103. data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -5
  104. data/lib/canon/tree_diff/matchers.rb +15 -0
  105. data/lib/canon/tree_diff/operation_converter.rb +7 -15
  106. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +2 -12
  107. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +13 -7
  108. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +2 -2
  109. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +4 -6
  110. data/lib/canon/tree_diff/operation_converter_helpers.rb +18 -0
  111. data/lib/canon/tree_diff/operations/operation_detector.rb +6 -5
  112. data/lib/canon/tree_diff/operations.rb +13 -0
  113. data/lib/canon/tree_diff.rb +26 -27
  114. data/lib/canon/validators/base_validator.rb +5 -10
  115. data/lib/canon/validators/html_validator.rb +2 -8
  116. data/lib/canon/validators/json_validator.rb +0 -1
  117. data/lib/canon/validators/xml_validator.rb +2 -8
  118. data/lib/canon/validators/yaml_validator.rb +0 -1
  119. data/lib/canon/validators.rb +12 -0
  120. data/lib/canon/version.rb +1 -1
  121. data/lib/canon/xml/c14n.rb +0 -4
  122. data/lib/canon/xml/data_model.rb +5 -15
  123. data/lib/canon/xml/line_range_mapper.rb +0 -2
  124. data/lib/canon/xml/nodes/attribute_node.rb +0 -2
  125. data/lib/canon/xml/nodes/comment_node.rb +0 -2
  126. data/lib/canon/xml/nodes/element_node.rb +0 -2
  127. data/lib/canon/xml/nodes/namespace_node.rb +0 -2
  128. data/lib/canon/xml/nodes/processing_instruction_node.rb +0 -2
  129. data/lib/canon/xml/nodes/root_node.rb +0 -2
  130. data/lib/canon/xml/nodes/text_node.rb +0 -2
  131. data/lib/canon/xml/nodes.rb +19 -0
  132. data/lib/canon/xml/processor.rb +0 -5
  133. data/lib/canon/xml/sax_builder.rb +1 -8
  134. data/lib/canon/xml/whitespace_normalizer.rb +2 -2
  135. data/lib/canon/xml.rb +33 -0
  136. data/lib/canon/xml_backend.rb +50 -14
  137. data/lib/canon/xml_parsing.rb +32 -18
  138. data/lib/canon.rb +25 -15
  139. data/lib/tasks/performance.rake +0 -58
  140. data/lib/tasks/performance_comparator.rb +132 -65
  141. data/lib/tasks/performance_helpers.rb +4 -249
  142. data/lib/tasks/performance_report.rb +309 -0
  143. metadata +28 -15
  144. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +0 -64
  145. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +0 -64
  146. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +0 -167
  147. data/lib/canon/comparison/dimensions/base_dimension.rb +0 -107
  148. data/lib/canon/comparison/dimensions/comments_dimension.rb +0 -117
  149. data/lib/canon/comparison/dimensions/element_position_dimension.rb +0 -86
  150. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +0 -115
  151. data/lib/canon/comparison/dimensions/text_content_dimension.rb +0 -102
  152. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +0 -270
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_formatter"
4
-
5
3
  module Canon
6
4
  class DiffFormatter
7
5
  module ByObject
@@ -136,7 +134,16 @@ module Canon
136
134
  theme_color(:changed, :marker) || :yellow,
137
135
  )}"
138
136
  end
139
- when :structural_whitespace, :attribute_whitespace, :attribute_values
137
+ when :attribute_values
138
+ render_attribute_values_diffnode(diff_node, node1, node2, prefix,
139
+ output)
140
+ when :attribute_presence
141
+ render_attribute_presence_diffnode(diff_node, node1, node2, prefix,
142
+ output)
143
+ when :attribute_order
144
+ render_attribute_order_diffnode(diff_node, node1, node2, prefix,
145
+ output)
146
+ when :structural_whitespace, :attribute_whitespace
140
147
  output << "#{prefix}└── #{colorize(
141
148
  "[#{diff_node.dimension}: #{diff_node.reason}]",
142
149
  theme_color(:changed, :marker) || :yellow,
@@ -150,6 +157,115 @@ module Canon
150
157
  end
151
158
  end
152
159
 
160
+ # Render attribute value DiffNode with per-attribute before/after
161
+ def render_attribute_values_diffnode(diff_node, node1, node2, prefix,
162
+ output)
163
+ attrs1 = diff_node.attributes_before ||
164
+ extract_attributes_hash(node1)
165
+ attrs2 = diff_node.attributes_after ||
166
+ extract_attributes_hash(node2)
167
+ element_name = if node1
168
+ node1.name
169
+ else
170
+ (node2 ? node2.name : "?")
171
+ end
172
+
173
+ output << "#{prefix}└── #{colorize(
174
+ "Element: <#{element_name}>",
175
+ theme_color(:changed, :marker) || :yellow,
176
+ )}"
177
+
178
+ differing = find_differing_attributes(attrs1, attrs2)
179
+ differing.each_with_index do |name, i|
180
+ connector = i < differing.size - 1 ? "├──" : "└──"
181
+ val1 = attrs1[name].to_s
182
+ val2 = attrs2[name].to_s
183
+ output << "#{prefix} #{connector} " \
184
+ "#{colorize("#{name}:", :cyan)} " \
185
+ "#{colorize(val1,
186
+ theme_color(:removed, :content) || :red)} " \
187
+ "→ " \
188
+ "#{colorize(val2,
189
+ theme_color(:added, :content) || :green)}"
190
+ end
191
+ end
192
+
193
+ # Render attribute presence DiffNode with added/removed attributes
194
+ def render_attribute_presence_diffnode(diff_node, node1, node2, prefix,
195
+ output)
196
+ attrs1 = diff_node.attributes_before ||
197
+ extract_attributes_hash(node1)
198
+ attrs2 = diff_node.attributes_after ||
199
+ extract_attributes_hash(node2)
200
+ element_name = if node1
201
+ node1.name
202
+ else
203
+ (node2 ? node2.name : "?")
204
+ end
205
+
206
+ output << "#{prefix}└── #{colorize(
207
+ "Element: <#{element_name}>",
208
+ theme_color(:changed, :marker) || :yellow,
209
+ )}"
210
+
211
+ keys1 = attrs1.keys.to_set
212
+ keys2 = attrs2.keys.to_set
213
+ removed = (keys1 - keys2).sort
214
+ added = (keys2 - keys1).sort
215
+ items = removed.map { |k| [:removed, k, attrs1[k]] }
216
+ added.each { |k| items << [:added, k, attrs2[k]] }
217
+
218
+ items.each_with_index do |(type, name, val), i|
219
+ connector = i < items.size - 1 ? "├──" : "└──"
220
+ color = if type == :removed
221
+ theme_color(:removed,
222
+ :content) || :red
223
+ else
224
+ theme_color(
225
+ :added, :content
226
+ ) || :green
227
+ end
228
+ sign = type == :removed ? "-" : "+"
229
+ output << "#{prefix} #{connector} " \
230
+ "#{colorize("#{sign} #{name}=\"#{val}\"", color)}"
231
+ end
232
+ end
233
+
234
+ # Render attribute order DiffNode with before/after order
235
+ def render_attribute_order_diffnode(diff_node, node1, node2, prefix,
236
+ output)
237
+ attrs1 = diff_node.attributes_before ||
238
+ extract_attributes_hash(node1)
239
+ attrs2 = diff_node.attributes_after ||
240
+ extract_attributes_hash(node2)
241
+
242
+ order1 = attrs1.keys.join(", ")
243
+ order2 = attrs2.keys.join(", ")
244
+
245
+ output << "#{prefix}├── #{colorize(
246
+ "- [#{order1}]",
247
+ theme_color(:removed, :content) || :red,
248
+ )}"
249
+ output << "#{prefix}└── #{colorize(
250
+ "+ [#{order2}]",
251
+ theme_color(:added, :content) || :green,
252
+ )}"
253
+ end
254
+
255
+ # Extract attributes hash from a node
256
+ def extract_attributes_hash(node)
257
+ return {} unless node
258
+
259
+ Canon::Diff::NodeSerializer.extract_attributes(node) || {}
260
+ end
261
+
262
+ # Find attributes that differ between two attribute hashes
263
+ def find_differing_attributes(attrs1, attrs2)
264
+ (attrs1.keys | attrs2.keys).reject do |k|
265
+ attrs1[k.to_s] == attrs2[k.to_s]
266
+ end.sort
267
+ end
268
+
153
269
  # Render unequal elements
154
270
  def render_unequal_elements(diff, prefix, output)
155
271
  node1 = diff[:node1]
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "json_formatter"
4
-
5
3
  module Canon
6
4
  class DiffFormatter
7
5
  module ByObject
@@ -21,9 +21,6 @@ module Canon
21
21
  # @param format [Symbol] Document format (:xml, :json, :yaml)
22
22
  # @return [String] Formatted diff output
23
23
  def format(differences, format)
24
- output = []
25
- output << colorize("Visual Diff:", :cyan, :bold)
26
-
27
24
  diffs_array = if differences.is_a?(Canon::Comparison::ComparisonResult)
28
25
  differences.differences
29
26
  else
@@ -37,8 +34,7 @@ module Canon
37
34
  show_diffs: @show_diffs,
38
35
  )
39
36
 
40
- output << formatter.format(diffs_array, format)
41
- output.join("\n")
37
+ formatter.format(diffs_array, format)
42
38
  end
43
39
 
44
40
  private
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_detail_formatter"
4
-
5
3
  module Canon
6
4
  class DiffFormatter
7
5
  # Verbose diff output helper for CANON_VERBOSE mode
@@ -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
 
@@ -707,7 +664,7 @@ expand_difference: false)
707
664
  # @param diff [DiffNode, Hash] Difference node
708
665
  # @return [Symbol] Dimension
709
666
  def self.extract_dimension(diff)
710
- if diff.respond_to?(:dimension)
667
+ if diff.is_a?(Canon::Diff::DiffNode)
711
668
  diff.dimension
712
669
  elsif diff.is_a?(Hash)
713
670
  diff[:dimension] || diff[:diff_code] || :unknown
@@ -721,7 +678,7 @@ expand_difference: false)
721
678
  # @param diff [DiffNode, Hash] Difference node
722
679
  # @return [Object] Node1
723
680
  def self.extract_node1(diff)
724
- if diff.respond_to?(:node1)
681
+ if diff.is_a?(Canon::Diff::DiffNode)
725
682
  diff.node1
726
683
  elsif diff.is_a?(Hash)
727
684
  diff[:node1]
@@ -733,7 +690,7 @@ expand_difference: false)
733
690
  # @param diff [DiffNode, Hash] Difference node
734
691
  # @return [Object] Node2
735
692
  def self.extract_node2(diff)
736
- if diff.respond_to?(:node2)
693
+ if diff.is_a?(Canon::Diff::DiffNode)
737
694
  diff.node2
738
695
  elsif diff.is_a?(Hash)
739
696
  diff[:node2]
@@ -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
@@ -17,12 +15,12 @@ module Canon
17
15
  return "" unless diff
18
16
 
19
17
  # Prefer pre-computed path if available (populated by MetadataEnricher)
20
- if diff.respond_to?(:path) && !diff.path.nil? && !diff.path.empty?
21
- return "Location: #{diff.path}"
18
+ if diff.is_a?(Canon::Diff::DiffNode) && diff.path && !diff.path.empty?
19
+ return diff.path
22
20
  end
23
21
 
24
22
  # Fall back to extracting from nodes
25
- node = if diff.respond_to?(:node1)
23
+ node = if diff.is_a?(Canon::Diff::DiffNode)
26
24
  diff.node1 || diff.node2
27
25
  elsif diff.is_a?(Hash)
28
26
  diff[:node1] || diff[:node2]
@@ -30,8 +28,7 @@ module Canon
30
28
 
31
29
  return "" unless node
32
30
 
33
- xpath = extract_xpath(node)
34
- xpath.empty? ? "" : "Location: #{xpath}"
31
+ extract_xpath(node)
35
32
  end
36
33
 
37
34
  # Extract XPath from a node
@@ -66,24 +63,26 @@ module Canon
66
63
  current = node
67
64
 
68
65
  while current
69
- break unless current.respond_to?(:name)
70
-
71
- name = current.name
66
+ name = case current
67
+ when Canon::Xml::Node, Nokogiri::XML::Node
68
+ current.name
69
+ else
70
+ break
71
+ end
72
72
  break if name.nil? || name.empty?
73
73
 
74
- # Calculate position among siblings
75
74
  index = calculate_sibling_index(current, name)
76
75
  parts.unshift("#{name}[#{index}]")
77
76
 
78
- # Move to parent
79
- current = if current.respond_to?(:parent)
77
+ current = case current
78
+ when Canon::Xml::Node, Nokogiri::XML::Node
80
79
  current.parent
81
- elsif current.respond_to?(:parent_node)
82
- current.parent_node
80
+ else
81
+ break
83
82
  end
84
83
 
85
- # Stop at document root
86
- break if current.respond_to?(:document) && current == current.document
84
+ break if current.is_a?(Nokogiri::XML::Document) ||
85
+ current.is_a?(Canon::Xml::Nodes::RootNode)
87
86
  end
88
87
 
89
88
  parts.empty? ? "" : "/#{parts.join('/')}"
@@ -95,24 +94,22 @@ module Canon
95
94
  # @param name [String] Node name
96
95
  # @return [Integer] 1-based index
97
96
  def self.calculate_sibling_index(node, name)
98
- return 1 unless node.respond_to?(:parent) || node.respond_to?(:parent_node)
99
-
100
- parent = if node.respond_to?(:parent)
97
+ parent = case node
98
+ when Canon::Xml::Node, Nokogiri::XML::Node
101
99
  node.parent
102
- elsif node.respond_to?(:parent_node)
103
- node.parent_node
104
100
  end
105
101
 
106
102
  return 1 unless parent
107
103
 
108
- # Get siblings with same name
109
- siblings = if parent.respond_to?(:children)
104
+ siblings = case parent
105
+ when Canon::Xml::Node, Nokogiri::XML::Node
110
106
  parent.children.select do |n|
111
- n.respond_to?(:name) && n.name == name
112
- end
113
- elsif parent.respond_to?(:child_nodes)
114
- parent.child_nodes.select do |n|
115
- n.respond_to?(:name) && n.name == name
107
+ case n
108
+ when Canon::Xml::Node, Nokogiri::XML::Node
109
+ n.name == name
110
+ else
111
+ false
112
+ end
116
113
  end
117
114
  else
118
115
  [node]
@@ -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 ---
@@ -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
@@ -1,15 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "paint" unless RUBY_ENGINE == "opal"
4
- require "yaml"
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"
4
+ require "yaml" unless RUBY_ENGINE == "opal"
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"
@@ -135,16 +138,17 @@ module Canon
135
138
  end
136
139
 
137
140
  # Default character visualization map (loaded from YAML)
138
- DEFAULT_VISUALIZATION_MAP = character_map_data[:visualization_map].freeze
139
-
140
- # Character category map (loaded from YAML)
141
- CHARACTER_CATEGORY_MAP = character_map_data[:category_map].freeze
142
-
143
- # Category display names (loaded from YAML)
144
- CHARACTER_CATEGORY_NAMES = character_map_data[:category_names].freeze
145
-
146
- # Character metadata including names (loaded from YAML)
147
- CHARACTER_METADATA = character_map_data[:character_metadata].freeze
141
+ if RUBY_ENGINE == "opal"
142
+ DEFAULT_VISUALIZATION_MAP = {}.freeze
143
+ CHARACTER_CATEGORY_MAP = {}.freeze
144
+ CHARACTER_CATEGORY_NAMES = {}.freeze
145
+ CHARACTER_METADATA = {}.freeze
146
+ else
147
+ DEFAULT_VISUALIZATION_MAP = character_map_data[:visualization_map].freeze
148
+ CHARACTER_CATEGORY_MAP = character_map_data[:category_map].freeze
149
+ CHARACTER_CATEGORY_NAMES = character_map_data[:category_names].freeze
150
+ CHARACTER_METADATA = character_map_data[:character_metadata].freeze
151
+ end
148
152
 
149
153
  # Map difference codes to human-readable descriptions
150
154
  DIFF_DESCRIPTIONS = {
@@ -419,7 +423,6 @@ module Canon
419
423
  # 2. Semantic Diff Report (ALWAYS if diffs exist)
420
424
  if comparison_result.is_a?(Canon::Comparison::ComparisonResult) &&
421
425
  comparison_result.differences.any?
422
- require_relative "diff_formatter/diff_detail_formatter"
423
426
  output << DiffDetailFormatter.format_report(
424
427
  comparison_result.differences,
425
428
  use_color: @use_color,
@@ -757,6 +760,9 @@ module Canon
757
760
  output.join("\n")
758
761
  end
759
762
 
763
+ public :format_line_numbered_inputs, :format_raw_inputs,
764
+ :format_preprocessed_inputs
765
+
760
766
  # Build the final visualization map from various customization options
761
767
  #
762
768
  # @param visualization_map [Hash, nil] Complete custom visualization map
@@ -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