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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a13457a67f3e2ab91e00cec19684c502605ab807bdd87eb1120e77d190a99c2e
4
- data.tar.gz: 35c0c873340e12c63048adf2222fda2f8c2ae3972337dcc212b26d391191ac35
3
+ metadata.gz: 48895efa0afcaaa525d8dcb9b04c1eb5969aeacd96c2e9b1f575383109229ce8
4
+ data.tar.gz: 12baa3b412f857b7b6038149f8344f2aa295d8b29e96ed2a0401e64010df5478
5
5
  SHA512:
6
- metadata.gz: 8db915564eebd4ca4dfadd65358f721aa70bca318c22dc1c02eff5e3527cf646ea19722b760072851f358b3fabefd12fc5f6dfc216bce146423c7091f3bf7eac
7
- data.tar.gz: f92e7491d781c8762483335558ede985a1653bcfb88613858115aa87e50bb326f95b0b76b845c54154e657fb9f25b3d1f348bf8e9baa926ea1c6bfbbd77d6ca6
6
+ metadata.gz: 9d9e2d1bfce446d850e1c991e161991a5112bdccdcb3d57d28170ba9d3180bcd0897aba7c78c7c2e6fe3ec3f5d5ba0e295c9c80627fd0d3c9e2a8bfef1434125
7
+ data.tar.gz: ec91e939f8d39f21e9c93fcfea178241eb601c6e5f66e9956468bfef0bf480ba05d587b705d108f39de661e184d7aaae3e0b79cf4c907a2d0c98b062d4564ec5
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-05-24 10:34:05 UTC using RuboCop version 1.86.0.
3
+ # on 2026-06-15 09:49:31 UTC using RuboCop version 1.87.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,14 +11,14 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'canon.gemspec'
13
13
 
14
- # Offense count: 1358
14
+ # Offense count: 1272
15
15
  # This cop supports safe autocorrection (--autocorrect).
16
16
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
17
17
  # URISchemes: http, https
18
18
  Layout/LineLength:
19
19
  Enabled: false
20
20
 
21
- # Offense count: 58
21
+ # Offense count: 57
22
22
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
23
23
  Lint/DuplicateBranch:
24
24
  Enabled: false
@@ -62,7 +62,7 @@ Lint/UselessConstantScoping:
62
62
  Exclude:
63
63
  - 'lib/canon/diff_formatter/theme.rb'
64
64
 
65
- # Offense count: 313
65
+ # Offense count: 298
66
66
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
67
67
  Metrics/AbcSize:
68
68
  Enabled: false
@@ -78,12 +78,12 @@ Metrics/BlockLength:
78
78
  Metrics/BlockNesting:
79
79
  Max: 4
80
80
 
81
- # Offense count: 276
81
+ # Offense count: 261
82
82
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
83
83
  Metrics/CyclomaticComplexity:
84
84
  Enabled: false
85
85
 
86
- # Offense count: 523
86
+ # Offense count: 501
87
87
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
88
88
  Metrics/MethodLength:
89
89
  Max: 146
@@ -93,12 +93,12 @@ Metrics/MethodLength:
93
93
  Metrics/ParameterLists:
94
94
  Max: 10
95
95
 
96
- # Offense count: 214
96
+ # Offense count: 207
97
97
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
98
98
  Metrics/PerceivedComplexity:
99
99
  Enabled: false
100
100
 
101
- # Offense count: 30
101
+ # Offense count: 28
102
102
  # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
103
103
  # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
104
104
  Naming/MethodParameterName:
@@ -107,15 +107,13 @@ Naming/MethodParameterName:
107
107
  - 'lib/canon/comparison/xml_comparator/attribute_comparator.rb'
108
108
  - 'lib/canon/xml/namespace_handler.rb'
109
109
 
110
- # Offense count: 6
111
- # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
112
- # SupportedStyles: snake_case, normalcase, non_integer
113
- # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
114
- Naming/VariableNumber:
110
+ # Offense count: 1
111
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
112
+ # AllowedMethods: call
113
+ # WaywardPredicates: infinite?, nonzero?
114
+ Naming/PredicateMethod:
115
115
  Exclude:
116
- - 'lib/canon/comparison/json_comparator.rb'
117
- - 'lib/canon/comparison/markup_comparator.rb'
118
- - 'lib/canon/comparison/xml_comparator/diff_node_builder.rb'
116
+ - 'spec/canon/comparison/xml_attribute_diff_spec.rb'
119
117
 
120
118
  # Offense count: 4
121
119
  # Configuration parameters: MinSize.
@@ -126,13 +124,13 @@ Performance/CollectionLiteralInLoop:
126
124
  - 'lib/canon/xml/xml_base_handler.rb'
127
125
  - 'spec/canon/diff/diff_node_mapper_comments_spec.rb'
128
126
 
129
- # Offense count: 107
127
+ # Offense count: 111
130
128
  # Configuration parameters: Prefixes, AllowedPatterns.
131
129
  # Prefixes: when, with, without
132
130
  RSpec/ContextWording:
133
131
  Enabled: false
134
132
 
135
- # Offense count: 47
133
+ # Offense count: 48
136
134
  # Configuration parameters: IgnoredMetadata.
137
135
  RSpec/DescribeClass:
138
136
  Enabled: false
@@ -143,7 +141,7 @@ RSpec/DescribeMethod:
143
141
  - 'spec/canon/comparison/multiple_differences_spec.rb'
144
142
  - 'spec/canon/diff_formatter/character_map_customization_spec.rb'
145
143
 
146
- # Offense count: 874
144
+ # Offense count: 916
147
145
  # Configuration parameters: CountAsOne.
148
146
  RSpec/ExampleLength:
149
147
  Max: 44
@@ -195,7 +193,7 @@ RSpec/MultipleDescribes:
195
193
  Exclude:
196
194
  - 'spec/canon/comparison/match_options_spec.rb'
197
195
 
198
- # Offense count: 736
196
+ # Offense count: 783
199
197
  RSpec/MultipleExpectations:
200
198
  Max: 15
201
199
 
@@ -248,7 +246,7 @@ RSpec/SpecFilePathFormat:
248
246
  - 'spec/canon/yaml/formatter_spec.rb'
249
247
  - 'spec/xml_c14n_spec.rb'
250
248
 
251
- # Offense count: 72
249
+ # Offense count: 18
252
250
  # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
253
251
  RSpec/VerifiedDoubles:
254
252
  Exclude:
@@ -274,12 +272,13 @@ Style/HashLikeCase:
274
272
  - 'lib/canon/diff/diff_block_builder.rb'
275
273
  - 'lib/canon/xml/character_encoder.rb'
276
274
 
277
- # Offense count: 4
275
+ # Offense count: 6
278
276
  # This cop supports unsafe autocorrection (--autocorrect-all).
279
277
  Style/IdenticalConditionalBranches:
280
278
  Exclude:
281
279
  - 'lib/canon/diff_formatter/by_object/base_formatter.rb'
282
280
  - 'lib/canon/diff_formatter/legend.rb'
281
+ - 'lib/canon/xml/whitespace_normalizer.rb'
283
282
 
284
283
  # Offense count: 1
285
284
  # Configuration parameters: AllowedMethods.
data/Rakefile CHANGED
@@ -7,6 +7,28 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  begin
9
9
  require "opal/rspec/rake_task"
10
+
11
+ # Configure Opal load paths at load time (same pattern as moxml)
12
+ if defined?(Opal)
13
+ Opal.append_path File.expand_path("lib", __dir__)
14
+
15
+ # moxml gem
16
+ begin
17
+ moxml_spec = Gem::Specification.find_by_name("moxml")
18
+ moxml_lib = moxml_spec.full_require_paths.first
19
+ Opal.append_path moxml_lib
20
+ moxml_compat = File.expand_path("compat/opal", moxml_lib)
21
+ Opal.append_path moxml_compat if File.directory?(moxml_compat)
22
+ rescue Gem::MissingSpecError
23
+ # moxml not installed
24
+ end
25
+
26
+ # REXML: bundled gem since Ruby 3.4
27
+ rexml_lib = $LOAD_PATH.find do |p|
28
+ File.exist?(File.join(p, "rexml", "document.rb"))
29
+ end
30
+ Opal.append_path rexml_lib if rexml_lib
31
+ end
10
32
  rescue LoadError
11
33
  # Opal not available or incompatible with current Ruby version
12
34
  end
@@ -20,9 +42,10 @@ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
20
42
  namespace :spec do
21
43
  if defined?(Opal::RSpec::RakeTask)
22
44
  desc "Run Opal (JavaScript) tests"
23
- Opal::RSpec::RakeTask.new(:opal) do |server, runner|
24
- server.append_path "lib"
45
+ Opal::RSpec::RakeTask.new(:opal) do |_server, runner|
25
46
  runner.default_path = "spec"
47
+ runner.requires = %w[rexml_compat rexml/document rexml/xpath
48
+ moxml moxml/adapter/rexml spec_helper]
26
49
  runner.pattern = "spec/canon/opal_xml_smoke_spec.rb"
27
50
  end
28
51
  end
data/lib/canon/cache.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
3
+ require "digest" unless RUBY_ENGINE == "opal"
4
4
 
5
5
  module Canon
6
6
  # Cache for expensive operations during document comparison
@@ -73,50 +73,41 @@ module Canon
73
73
  end
74
74
 
75
75
  # Generate cache key for document parsing
76
- #
77
- # @param content [String] Document content
78
- # @param format [Symbol] Document format
79
- # @param preprocessing [Symbol] Preprocessing option
80
- # @return [String] Cache key
81
76
  def key_for_document(content, format, preprocessing)
82
- digest = Digest::SHA256.hexdigest(content)
83
- "doc:#{format}:#{preprocessing}:#{digest[0..16]}"
77
+ "doc:#{format}:#{preprocessing}:#{content_hash(content)}"
84
78
  end
85
79
 
86
80
  # Generate cache key for format detection
87
- #
88
- # @param content [String] Document content
89
- # @return [String] Cache key
90
81
  def key_for_format_detection(content)
91
- # Use first 100 chars for quick key, plus length
92
- # Force to binary to avoid encoding compatibility issues
93
82
  preview = content[0..100].b
94
- digest = Digest::SHA256.hexdigest(preview + content.length.to_s)
95
- "fmt:#{digest[0..16]}"
83
+ "fmt:#{content_hash(preview + content.length.to_s)}"
96
84
  end
97
85
 
98
86
  # Generate cache key for XML canonicalization
99
- #
100
- # @param content [String] XML content
101
- # @param with_comments [Boolean] Whether to include comments
102
- # @return [String] Cache key
103
87
  def key_for_c14n(content, with_comments)
104
- digest = Digest::SHA256.hexdigest(content)
105
- "c14n:#{with_comments}:#{digest[0..16]}"
88
+ "c14n:#{with_comments}:#{content_hash(content)}"
106
89
  end
107
90
 
108
91
  # Generate cache key for preprocessing
109
- #
110
- # @param content [String] Original content
111
- # @param preprocessing [Symbol] Preprocessing type
112
- # @return [String] Cache key
113
92
  def key_for_preprocessing(content, preprocessing)
114
- digest = Digest::SHA256.hexdigest(content)
115
- "pre:#{preprocessing}:#{digest[0..16]}"
93
+ "pre:#{preprocessing}:#{content_hash(content)}"
116
94
  end
117
95
 
118
96
  private
119
97
 
98
+ # Generate a hash string for cache keys
99
+ def content_hash(content)
100
+ if defined?(Digest::SHA256)
101
+ Digest::SHA256.hexdigest(content)[0..16]
102
+ else
103
+ # Opal fallback: simple string hash
104
+ h = content.each_char.reduce(0) do |acc, c|
105
+ ((acc * 31) + c.ord) & 0xFFFFFFFF
106
+ end
107
+ h.to_s(16).rjust(8, "0")
108
+ end
109
+ end
110
+
120
111
  # Get or create cache for a category
121
112
  #
122
113
  # @param category [Symbol] Cache category
data/lib/canon/cli.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor" unless RUBY_ENGINE == "opal"
4
- require_relative "commands/format_command"
5
- require_relative "commands/diff_command"
6
- require_relative "options/registry"
7
4
 
8
5
  module Canon
9
6
  # Command-line interface for Canon
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../comparison"
4
- require_relative "../diff_formatter"
5
- require_relative "../color_detector"
6
3
  require "json"
7
4
  require "yaml"
8
5
 
@@ -66,7 +63,6 @@ module Canon
66
63
 
67
64
  # Show configuration in verbose mode using shared DebugOutput
68
65
  if @options[:verbose]
69
- require_relative "../diff_formatter/debug_output"
70
66
  config_output = Canon::DiffFormatter::DebugOutput.verbose_tables_only(
71
67
  result,
72
68
  {
@@ -219,12 +215,10 @@ module Canon
219
215
 
220
216
  case format
221
217
  when :xml
222
- require_relative "../pretty_printer/xml"
223
218
  formatted1 = Canon::PrettyPrinter::Xml.new(indent: 2).format(content1)
224
219
  formatted2 = Canon::PrettyPrinter::Xml.new(indent: 2).format(content2)
225
220
  [formatted1, formatted2]
226
221
  when :html
227
- require_relative "../pretty_printer/html"
228
222
  formatted1 = Canon::PrettyPrinter::Html.new(indent: 2).format(content1)
229
223
  formatted2 = Canon::PrettyPrinter::Html.new(indent: 2).format(content2)
230
224
  [formatted1, formatted2]
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../pretty_printer/xml"
4
- require_relative "../pretty_printer/json"
5
- require_relative "../pretty_printer/html"
6
-
7
3
  module Canon
8
4
  module Commands
9
5
  # Command for canonicalizing files
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ # Thor command implementations invoked by {CLI}. Children are autoloaded.
5
+ module Commands
6
+ autoload :DiffCommand, "canon/commands/diff_command"
7
+ autoload :FormatCommand, "canon/commands/format_command"
8
+ end
9
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node_inspector"
4
-
5
3
  module Canon
6
4
  module Comparison
7
5
  # Shared two-cursor walk over child arrays with noise-aware realignment.
@@ -3,7 +3,7 @@
3
3
  module Canon
4
4
  module Comparison
5
5
  # CompareProfile encapsulates the policy decisions about how differences
6
- # in various dimensions should be handled during comparison
6
+ # in various dimensions should be handled during comparison.
7
7
  #
8
8
  # This class provides separation of concerns:
9
9
  # - CompareProfile: Policy decisions (what to track, what affects equivalence)
@@ -17,69 +17,46 @@ module Canon
17
17
  @match_options = match_options
18
18
  end
19
19
 
20
- # Should DiffNodes be created for differences in this dimension?
21
- #
22
- # In verbose mode, we want to track ALL differences for reporting.
23
- # In non-verbose mode, we only need to track normative differences.
24
- #
25
- # @param dimension [Symbol] The match dimension to check
26
- # @return [Boolean] true if differences should be tracked
27
20
  def track_dimension?(_dimension)
28
- # Always track dimensions that affect equivalence
29
- # In verbose mode, also track informative dimensions
30
21
  true
31
22
  end
32
23
 
33
24
  # Should differences in this dimension affect equivalence?
34
25
  #
35
- # This determines the return value of the comparison:
36
- # - true: differences make documents non-equivalent
37
- # - false: differences are informative only
38
- #
39
26
  # @param dimension [Symbol] The match dimension to check
40
27
  # @return [Boolean] true if differences affect equivalence
41
28
  def affects_equivalence?(dimension)
42
29
  behavior = behavior_for(dimension)
43
-
44
- # :strict → affects equivalence
45
- # :normalize → might affect (if normalization fails)
46
- # :ignore → does NOT affect equivalence
47
30
  behavior != :ignore
48
31
  end
49
32
 
50
33
  # Is a difference in this dimension normative (affects equivalence)?
51
34
  #
52
- # This is used by DiffClassifier to determine the normative flag.
53
- #
54
- # Normative rules by dimension:
55
- # - structural_whitespace: only :strict is normative (:normalize and :ignore are informative)
56
- # - all other dimensions: normative unless behavior is :ignore
35
+ # Delegates to the Dimension object's normative? rule. Falls back to
36
+ # the default rule (normative unless :ignore) for dimensions not in the
37
+ # format's dimension set (e.g., derived dimensions like :element_structure).
57
38
  #
58
39
  # @param dimension [Symbol] The match dimension to check
59
40
  # @return [Boolean] true if normative, false if informative
60
41
  def normative_dimension?(dimension)
61
- # Structural whitespace with :normalize or :ignore behavior is INFORMATIVE
62
- # Only :strict mode makes whitespace normative
63
- if dimension == :structural_whitespace
64
- behavior = behavior_for(dimension)
65
- return behavior == :strict
42
+ dim = dimension_for(dimension)
43
+ if dim
44
+ dim.normative?(behavior_for(dimension))
45
+ else
46
+ behavior_for(dimension) != :ignore
66
47
  end
67
-
68
- # For all other dimensions, normative if behavior affects equivalence
69
- affects_equivalence?(dimension)
70
48
  end
71
49
 
72
50
  # Can a difference in this dimension be formatting-only?
73
51
  #
74
- # This determines whether FormattingDetector should be applied.
75
- # Only text/content dimensions can have formatting-only differences.
52
+ # Delegates to the Dimension object's supports_formatting_detection?
53
+ # flag. Falls back to false for unknown dimensions.
76
54
  #
77
55
  # @param dimension [Symbol] The match dimension to check
78
56
  # @return [Boolean] true if formatting detection should apply
79
57
  def supports_formatting_detection?(dimension)
80
- # Only text_content and structural_whitespace can have formatting-only diffs
81
- # Comments are policy-based (strict/ignore), not formatting-based
82
- %i[text_content structural_whitespace].include?(dimension)
58
+ dim = dimension_for(dimension)
59
+ dim ? dim.supports_formatting_detection? : false
83
60
  end
84
61
 
85
62
  # Get the behavior setting for a dimension
@@ -94,6 +71,23 @@ module Canon
94
71
  :strict
95
72
  end
96
73
  end
74
+
75
+ private
76
+
77
+ def dimension_for(name)
78
+ set = Dimensions::Registry.for(extract_format)
79
+ set[name]
80
+ end
81
+
82
+ def extract_format
83
+ if match_options.is_a?(ResolvedMatchOptions)
84
+ match_options.format
85
+ elsif match_options.is_a?(Hash)
86
+ match_options[:format]
87
+ else
88
+ :xml
89
+ end
90
+ end
97
91
  end
98
92
  end
99
93
  end
@@ -133,8 +133,6 @@ parse_errors_expected: nil, parse_errors_received: nil)
133
133
  # @return [String] Formatted diff output
134
134
  def diff(use_color: true, context_lines: 3, diff_grouping_lines: nil,
135
135
  show_diffs: :all, diff_mode: :separate, legacy_terminal: false)
136
- require_relative "../diff_formatter"
137
-
138
136
  formatter = Canon::DiffFormatter.new(
139
137
  use_color: use_color,
140
138
  mode: :by_line,