canon 0.1.13 → 0.1.15

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +69 -21
  3. data/README.adoc +41 -0
  4. data/docs/interfaces/ruby-api/index.adoc +26 -0
  5. data/docs/understanding/formats/xml.adoc +25 -0
  6. data/lib/canon/color_detector.rb +18 -15
  7. data/lib/canon/comparison/comparison_result.rb +1 -3
  8. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +1 -1
  9. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +1 -1
  10. data/lib/canon/comparison/dimensions/comments_dimension.rb +2 -2
  11. data/lib/canon/comparison/dimensions/element_position_dimension.rb +1 -1
  12. data/lib/canon/comparison/dimensions/registry.rb +1 -1
  13. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +2 -2
  14. data/lib/canon/comparison/dimensions/text_content_dimension.rb +11 -3
  15. data/lib/canon/comparison/format_detector.rb +1 -1
  16. data/lib/canon/comparison/html_comparator.rb +91 -41
  17. data/lib/canon/comparison/html_parser.rb +22 -0
  18. data/lib/canon/comparison/markup_comparator.rb +46 -11
  19. data/lib/canon/comparison/profile_definition.rb +1 -1
  20. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +3 -3
  21. data/lib/canon/comparison/xml_comparator/child_comparison.rb +87 -9
  22. data/lib/canon/comparison/xml_comparator/node_parser.rb +1 -1
  23. data/lib/canon/comparison/xml_comparator.rb +48 -15
  24. data/lib/canon/comparison/xml_node_comparison.rb +110 -12
  25. data/lib/canon/comparison.rb +45 -1
  26. data/lib/canon/config/env_provider.rb +3 -3
  27. data/lib/canon/config/env_schema.rb +2 -1
  28. data/lib/canon/config.rb +10 -0
  29. data/lib/canon/diff/diff_block_builder.rb +2 -2
  30. data/lib/canon/diff/diff_context_builder.rb +1 -1
  31. data/lib/canon/diff/diff_node_mapper.rb +1 -1
  32. data/lib/canon/diff_formatter/by_line/base_formatter.rb +2 -2
  33. data/lib/canon/diff_formatter/by_line/html_formatter.rb +2 -2
  34. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +2 -2
  35. data/lib/canon/diff_formatter/by_object/base_formatter.rb +1 -1
  36. data/lib/canon/diff_formatter/by_object/json_formatter.rb +3 -1
  37. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +1 -1
  38. data/lib/canon/diff_formatter/debug_output.rb +4 -6
  39. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +10 -1
  40. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +11 -0
  41. data/lib/canon/diff_formatter/diff_detail_formatter.rb +6 -0
  42. data/lib/canon/diff_formatter.rb +5 -5
  43. data/lib/canon/errors.rb +3 -3
  44. data/lib/canon/formatters/xml_formatter.rb +20 -0
  45. data/lib/canon/html/data_model.rb +26 -4
  46. data/lib/canon/rspec_matchers.rb +17 -2
  47. data/lib/canon/tree_diff/adapters/html_adapter.rb +20 -2
  48. data/lib/canon/tree_diff/adapters/json_adapter.rb +2 -6
  49. data/lib/canon/tree_diff/adapters/yaml_adapter.rb +2 -6
  50. data/lib/canon/tree_diff/core/matching.rb +2 -2
  51. data/lib/canon/tree_diff/core/node_signature.rb +2 -4
  52. data/lib/canon/tree_diff/core/tree_node.rb +1 -1
  53. data/lib/canon/tree_diff/matchers/hash_matcher.rb +12 -2
  54. data/lib/canon/tree_diff/operation_converter.rb +1 -1
  55. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +1 -1
  56. data/lib/canon/version.rb +1 -1
  57. data/lib/canon/xml/element_matcher.rb +70 -1
  58. data/lib/canon/xml/line_range_mapper.rb +1 -1
  59. data/lib/canon/xml/nodes/attribute_node.rb +4 -0
  60. data/lib/canon/xml/nodes/element_node.rb +4 -0
  61. data/lib/canon/xml/nodes/namespace_node.rb +4 -0
  62. data/lib/canon/xml/nodes/processing_instruction_node.rb +4 -0
  63. data/lib/canon.rb +1 -1
  64. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60a7f65c6d95c4c10672244fe19a027022aadb8de563d9f3ac58da151085e883
4
- data.tar.gz: 47f17024dd3d1a7055cef281439038d42bfb92527765f92d68392fbb14390d39
3
+ metadata.gz: '04860609f8d3300ccebf84a0f2208510600dcfaac4b3f54f698eb2de7eed0493'
4
+ data.tar.gz: fc50abe2a915d7d7ff1cd630c0a5a50849b6fd5780664cb6bb419300b2388743
5
5
  SHA512:
6
- metadata.gz: 526cfa7a890447be2abc8bc358ac96a67a58ed6cb8016beebb65d087e44de48ef742c90be9ab50960c2b3d543bc7e3a7118af88a4f02af0a3e85eeea83161f14
7
- data.tar.gz: 95e16e97ee9b71a1f4d220bc3c4f004f3f39f76d1b2631c68e3a5805b9f9aea4390bab9f9537dc0be254c690e6fcfa4146497ce8867bb15a28fec85387d1a0d1
6
+ metadata.gz: d1bcc3ad7439fdd7f65627c53cd0dc4b92d781bdbdaf330d2bb8ecc89b3319a4fc02e78de8e961cdb69854661881db39bf59c664210c7a59b7a011355b1db71b
7
+ data.tar.gz: cca1b3f2eee48054b5431118350e64acb46f485096cd8c88798e0935210985d63eb93de4727a42d17cfaad8ef03c55e7d1745922500f3a3495a791d86fd60676
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-01-21 09:17:44 UTC using RuboCop version 1.81.7.
3
+ # on 2026-02-17 14:18:53 UTC using RuboCop version 1.81.7.
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
@@ -12,13 +12,52 @@ Gemspec/RequiredRubyVersion:
12
12
  Exclude:
13
13
  - 'canon.gemspec'
14
14
 
15
- # Offense count: 700
15
+ # Offense count: 1
16
+ # This cop supports safe autocorrection (--autocorrect).
17
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
18
+ # SupportedStyles: with_first_argument, with_fixed_indentation
19
+ Layout/ArgumentAlignment:
20
+ Exclude:
21
+ - 'lib/canon/xml/element_matcher.rb'
22
+
23
+ # Offense count: 23
24
+ # This cop supports safe autocorrection (--autocorrect).
25
+ # Configuration parameters: EnforcedStyleAlignWith.
26
+ # SupportedStylesAlignWith: either, start_of_block, start_of_line
27
+ Layout/BlockAlignment:
28
+ Exclude:
29
+ - 'spec/canon/fixtures/isodoc_spec.rb'
30
+ - 'spec/canon/table_class_attribute_bug_spec.rb'
31
+
32
+ # Offense count: 23
33
+ # This cop supports safe autocorrection (--autocorrect).
34
+ Layout/BlockEndNewline:
35
+ Exclude:
36
+ - 'spec/canon/fixtures/isodoc_spec.rb'
37
+ - 'spec/canon/table_class_attribute_bug_spec.rb'
38
+
39
+ # Offense count: 46
40
+ # This cop supports safe autocorrection (--autocorrect).
41
+ # Configuration parameters: Width, AllowedPatterns.
42
+ Layout/IndentationWidth:
43
+ Exclude:
44
+ - 'spec/canon/fixtures/isodoc_spec.rb'
45
+ - 'spec/canon/table_class_attribute_bug_spec.rb'
46
+
47
+ # Offense count: 780
16
48
  # This cop supports safe autocorrection (--autocorrect).
17
49
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
18
50
  # URISchemes: http, https
19
51
  Layout/LineLength:
20
52
  Enabled: false
21
53
 
54
+ # Offense count: 1
55
+ # This cop supports safe autocorrection (--autocorrect).
56
+ # Configuration parameters: AllowInHeredoc.
57
+ Layout/TrailingWhitespace:
58
+ Exclude:
59
+ - 'lib/canon/xml/element_matcher.rb'
60
+
22
61
  # Offense count: 48
23
62
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
24
63
  Lint/DuplicateBranch:
@@ -48,44 +87,45 @@ Lint/UnreachableCode:
48
87
  Exclude:
49
88
  - 'lib/canon/diff_formatter/debug_output.rb'
50
89
 
51
- # Offense count: 6
90
+ # Offense count: 7
52
91
  # This cop supports safe autocorrection (--autocorrect).
53
92
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
54
93
  # NotImplementedExceptions: NotImplementedError
55
94
  Lint/UnusedMethodArgument:
56
95
  Exclude:
96
+ - 'lib/canon/comparison.rb'
57
97
  - 'lib/canon/diff/path_builder.rb'
58
98
  - 'lib/canon/diff_formatter/by_line/base_formatter.rb'
59
99
  - 'lib/canon/diff_formatter/by_line/xml_formatter.rb'
60
100
  - 'lib/canon/diff_formatter/by_object/base_formatter.rb'
61
101
 
62
- # Offense count: 209
102
+ # Offense count: 215
63
103
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
64
104
  Metrics/AbcSize:
65
105
  Enabled: false
66
106
 
67
- # Offense count: 20
107
+ # Offense count: 21
68
108
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
69
109
  # AllowedMethods: refine
70
110
  Metrics/BlockLength:
71
111
  Max: 84
72
112
 
73
- # Offense count: 177
113
+ # Offense count: 183
74
114
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
75
115
  Metrics/CyclomaticComplexity:
76
116
  Enabled: false
77
117
 
78
- # Offense count: 363
118
+ # Offense count: 369
79
119
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
80
120
  Metrics/MethodLength:
81
- Max: 110
121
+ Max: 115
82
122
 
83
123
  # Offense count: 44
84
124
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
85
125
  Metrics/ParameterLists:
86
126
  Max: 9
87
127
 
88
- # Offense count: 143
128
+ # Offense count: 149
89
129
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
90
130
  Metrics/PerceivedComplexity:
91
131
  Enabled: false
@@ -119,12 +159,13 @@ Naming/VariableNumber:
119
159
  - 'lib/canon/comparison/markup_comparator.rb'
120
160
  - 'lib/canon/comparison/xml_comparator/diff_node_builder.rb'
121
161
 
122
- # Offense count: 2
162
+ # Offense count: 13
123
163
  # Configuration parameters: MinSize.
124
164
  Performance/CollectionLiteralInLoop:
125
165
  Exclude:
126
166
  - 'lib/canon/comparison/html_comparator.rb'
127
167
  - 'lib/canon/xml/xml_base_handler.rb'
168
+ - 'spec/canon/table_class_attribute_bug_spec.rb'
128
169
 
129
170
  # Offense count: 68
130
171
  # Configuration parameters: Prefixes, AllowedPatterns.
@@ -132,7 +173,7 @@ Performance/CollectionLiteralInLoop:
132
173
  RSpec/ContextWording:
133
174
  Enabled: false
134
175
 
135
- # Offense count: 25
176
+ # Offense count: 27
136
177
  # Configuration parameters: IgnoredMetadata.
137
178
  RSpec/DescribeClass:
138
179
  Enabled: false
@@ -143,13 +184,7 @@ RSpec/DescribeMethod:
143
184
  - 'spec/canon/comparison/multiple_differences_spec.rb'
144
185
  - 'spec/canon/diff_formatter/character_map_customization_spec.rb'
145
186
 
146
- # Offense count: 1
147
- # This cop supports safe autocorrection (--autocorrect).
148
- RSpec/EmptyHook:
149
- Exclude:
150
- - 'spec/canon/color_detector_spec.rb'
151
-
152
- # Offense count: 679
187
+ # Offense count: 696
153
188
  # Configuration parameters: CountAsOne.
154
189
  RSpec/ExampleLength:
155
190
  Max: 67
@@ -201,11 +236,11 @@ RSpec/MultipleDescribes:
201
236
  Exclude:
202
237
  - 'spec/canon/comparison/match_options_spec.rb'
203
238
 
204
- # Offense count: 522
239
+ # Offense count: 536
205
240
  RSpec/MultipleExpectations:
206
241
  Max: 15
207
242
 
208
- # Offense count: 69
243
+ # Offense count: 70
209
244
  # Configuration parameters: AllowSubject.
210
245
  RSpec/MultipleMemoizedHelpers:
211
246
  Max: 13
@@ -224,12 +259,13 @@ RSpec/NamedSubject:
224
259
  RSpec/NestedGroups:
225
260
  Max: 4
226
261
 
227
- # Offense count: 10
262
+ # Offense count: 11
228
263
  # Configuration parameters: AllowedPatterns.
229
264
  # AllowedPatterns: ^expect_, ^assert_
230
265
  RSpec/NoExpectationExample:
231
266
  Exclude:
232
267
  - 'spec/canon/context_grouping_spec.rb'
268
+ - 'spec/canon/fixtures/isodoc_spec.rb'
233
269
  - 'spec/canon/informative_diffs_debug_spec.rb'
234
270
  - 'spec/canon/isodoc_blockquotes_spec.rb'
235
271
  - 'spec/canon/match_scenarios_spec.rb'
@@ -257,6 +293,18 @@ RSpec/VerifiedDoubles:
257
293
  - 'spec/canon/diff/xml_serialization_formatter_spec.rb'
258
294
  - 'spec/canon/tree_diff/operation_converter_spec.rb'
259
295
 
296
+ # Offense count: 44
297
+ # This cop supports safe autocorrection (--autocorrect).
298
+ # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
299
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
300
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
301
+ # FunctionalMethods: let, let!, subject, watch
302
+ # AllowedMethods: lambda, proc, it
303
+ Style/BlockDelimiters:
304
+ Exclude:
305
+ - 'spec/canon/fixtures/isodoc_spec.rb'
306
+ - 'spec/canon/table_class_attribute_bug_spec.rb'
307
+
260
308
  # Offense count: 1
261
309
  # This cop supports safe autocorrection (--autocorrect).
262
310
  # Configuration parameters: EnforcedStyle, AllowComments.
data/README.adoc CHANGED
@@ -16,6 +16,47 @@ Key features:
16
16
  * **Multiple interfaces**: Ruby API, CLI, RSpec matchers
17
17
  * **Smart diff output**: By-line or by-object modes with syntax highlighting
18
18
 
19
+ == When to use formatting vs comparison
20
+
21
+ Canon provides two main APIs with different purposes:
22
+
23
+ *Use `Canon.format` for formatting/canonicalization:*
24
+
25
+ * Pretty-printing XML/JSON/YAML for display
26
+ * Canonicalizing documents for storage
27
+ * Normalizing formatting
28
+
29
+ *Use `Canon::Comparison.equivalent?` for semantic comparison:*
30
+
31
+ * Test assertions
32
+ * Document equivalence checking
33
+ * Diff generation
34
+
35
+ [IMPORTANT]
36
+ ====
37
+ Do NOT use `Canon.format_xml` output for string comparison in tests.
38
+ The formatting process changes line counts and formatting, which causes
39
+ false test failures.
40
+
41
+ Use `Canon::Comparison.equivalent?` instead, which performs semantic
42
+ comparison and properly handles XML declarations.
43
+ ====
44
+
45
+ [example]
46
+ ====
47
+ [source,ruby]
48
+ ----
49
+ # WRONG - formatting changes line counts
50
+ expect(Canon.format_xml(actual)).to eq(expected_formatted)
51
+
52
+ # RIGHT - semantic comparison ignores formatting differences
53
+ expect(Canon::Comparison.equivalent?(actual, expected, format: :xml)).to be true
54
+
55
+ # BEST - use RSpec matchers
56
+ expect(actual).to be_xml_equivalent_to(expected)
57
+ ----
58
+ ====
59
+
19
60
  == Installation
20
61
 
21
62
  Add to your application's Gemfile:
@@ -18,6 +18,32 @@ For command-line usage, see link:../cli/[CLI documentation].
18
18
 
19
19
  For RSpec testing, see link:../rspec/[RSpec documentation].
20
20
 
21
+ == Choosing the right API
22
+
23
+ Canon provides two main categories of APIs with different purposes.
24
+
25
+ === Formatting APIs
26
+
27
+ Use `Canon.format` or `Canon.format_xml` when you need to:
28
+
29
+ * Pretty-print documents for display
30
+ * Canonicalize documents for storage
31
+ * Normalize document formatting
32
+
33
+ NOTE: XML declarations are preserved in pretty-print mode and removed in
34
+ canonicalization mode.
35
+
36
+ === Comparison APIs
37
+
38
+ Use `Canon::Comparison.equivalent?` when you need to:
39
+
40
+ * Compare documents semantically
41
+ * Generate diffs
42
+ * Make test assertions
43
+
44
+ NOTE: XML declarations are stripped during preprocessing for semantic comparison.
45
+ Documents with and without XML declarations are considered equivalent.
46
+
21
47
  == General
22
48
 
23
49
  Canon provides a unified Ruby API for working with XML, HTML, JSON, and YAML
@@ -183,6 +183,31 @@ configures preprocessing, match options, diff algorithm, and formatting.
183
183
 
184
184
  == XML-specific features
185
185
 
186
+ === XML declaration handling
187
+
188
+ The XML declaration (`<?xml version="1.0" encoding="UTF-8"?>`) is handled
189
+ differently depending on the operation:
190
+
191
+ [cols="2,3"]
192
+ |===
193
+ | Operation | XML Declaration
194
+
195
+ | `Canon.format_xml` (pretty)
196
+ | Preserved
197
+
198
+ | `Canon.format_xml` (c14n)
199
+ | Removed (per W3C C14N spec)
200
+
201
+ | `Canon::Comparison.equivalent?`
202
+ | Stripped during preprocessing
203
+
204
+ | RSpec matchers
205
+ | Stripped during preprocessing
206
+ |===
207
+
208
+ This means documents with and without XML declarations are considered
209
+ equivalent when using the comparison API.
210
+
186
211
  === Comment handling
187
212
 
188
213
  XML comments are preserved in canonical form unless `--with-comments` is explicitly set.
@@ -81,24 +81,27 @@ module Canon
81
81
  #
82
82
  # @return [Boolean] true if colors appear to be supported
83
83
  def detect_from_env
84
- # Check for known color-capable terminals
85
- colorterm = ENV["COLORTERM"]
86
- return true if COLOR_TERM_VALUES.include?(colorterm)
87
-
88
84
  # Check TERM variable
89
- term = ENV["TERM"]
90
- if term
85
+ term = ENV.fetch("TERM", nil)
86
+ if term && NO_COLOR_TERMS.any? { |t| term.include?(t) }
91
87
  # Known no-color terminals
92
- return false if NO_COLOR_TERMS.any? { |t| term.include?(t) }
88
+ return false
89
+ end
90
+
91
+ # Check CI environments
92
+ # Some CI systems support colors, others don't
93
+ return detect_ci_colors if ci_environment?
94
+
95
+ if term
93
96
  # Known color-capable terminals
94
97
  return true if COLOR_TERM_SUFFIXES.any? { |s| term.end_with?(s) }
95
98
  # Most modern terminals support basic ANSI colors
96
99
  return true unless term.empty? || term == "unknown"
97
100
  end
98
101
 
99
- # Check CI environments
100
- # Some CI systems support colors, others don't
101
- return detect_ci_colors if ci_environment?
102
+ # Check for known color-capable terminals
103
+ colorterm = ENV.fetch("COLORTERM", nil)
104
+ return true if COLOR_TERM_VALUES.include?(colorterm)
102
105
 
103
106
  # Default: assume colors are supported on modern terminals
104
107
  # This is a safe default for most use cases
@@ -122,17 +125,17 @@ module Canon
122
125
  # - Generic CI: check for specific TeamCity/Terminal variables
123
126
  #
124
127
  # @return [Boolean] true if CI environment likely supports colors
125
- def detect_ci_colors
128
+ def detect_ci_colors # rubocop:disable Naming/PredicateMethod
129
+ # Most modern CI systems support ANSI colors
130
+ # Only disable for explicitly known non-color CI
131
+ return false if ENV["TERM"] == "dumb"
132
+
126
133
  # GitHub Actions explicitly supports colors
127
134
  return true if ENV["GITHUB_ACTIONS"]
128
135
 
129
136
  # TeamCity supports colors with specific env var
130
137
  return true if ENV["TEAMCITY_VERSION"]
131
138
 
132
- # Most modern CI systems support ANSI colors
133
- # Only disable for explicitly known non-color CI
134
- return false if ENV["TERM"] == "dumb"
135
-
136
139
  # Default to supporting colors in CI
137
140
  true
138
141
  end
@@ -44,10 +44,8 @@ html_version: nil, match_options: nil, algorithm: :dom, original_strings: nil)
44
44
  if diff.is_a?(Canon::Diff::DiffNode)
45
45
  diff.normative?
46
46
  # Legacy Hash format - always considered normative (structural differences)
47
- elsif diff.is_a?(Hash)
48
- true
49
47
  else
50
- false
48
+ diff.is_a?(Hash)
51
49
  end
52
50
  end
53
51
  end
@@ -37,7 +37,7 @@ module Canon
37
37
  # @param order1 [Array<Symbol>] First attribute order
38
38
  # @param order2 [Array<Symbol>] Second attribute order
39
39
  # @return [Boolean] true if attribute order is exactly the same
40
- def compare_strict(order1, order2)
40
+ def compare_strict(order1, order2) # rubocop:disable Naming/PredicateMethod
41
41
  order1 == order2
42
42
  end
43
43
 
@@ -37,7 +37,7 @@ module Canon
37
37
  # @param names1 [Array<Symbol>] First attribute names
38
38
  # @param names2 [Array<Symbol>] Second attribute names
39
39
  # @return [Boolean] true if attribute names are exactly equal
40
- def compare_strict(names1, names2)
40
+ def compare_strict(names1, names2) # rubocop:disable Naming/PredicateMethod
41
41
  names1.sort == names2.sort
42
42
  end
43
43
 
@@ -37,7 +37,7 @@ module Canon
37
37
  # @param comments1 [Array<String>] First comments array
38
38
  # @param comments2 [Array<String>] Second comments array
39
39
  # @return [Boolean] true if comments are exactly equal
40
- def compare_strict(comments1, comments2)
40
+ def compare_strict(comments1, comments2) # rubocop:disable Naming/PredicateMethod
41
41
  comments1 == comments2
42
42
  end
43
43
 
@@ -48,7 +48,7 @@ module Canon
48
48
  # @param comments1 [Array<String>] First comments array
49
49
  # @param comments2 [Array<String>] Second comments array
50
50
  # @return [Boolean] true if normalized comments are equal
51
- def compare_normalize(comments1, comments2)
51
+ def compare_normalize(comments1, comments2) # rubocop:disable Naming/PredicateMethod
52
52
  normalize_comments(comments1) == normalize_comments(comments2)
53
53
  end
54
54
 
@@ -39,7 +39,7 @@ module Canon
39
39
  # @param pos1 [Integer] First position
40
40
  # @param pos2 [Integer] Second position
41
41
  # @return [Boolean] true if positions are equal
42
- def compare_strict(pos1, pos2)
42
+ def compare_strict(pos1, pos2) # rubocop:disable Naming/PredicateMethod
43
43
  pos1 == pos2
44
44
  end
45
45
 
@@ -67,7 +67,7 @@ module Canon
67
67
  # @param node2 [Object] Second node
68
68
  # @param behavior [Symbol] Comparison behavior
69
69
  # @return [Boolean] true if nodes match for this dimension
70
- def self.compare(dimension_name, node1, node2, behavior)
70
+ def self.compare(dimension_name, node1, node2, behavior) # rubocop:disable Naming/PredicateMethod
71
71
  dimension = get(dimension_name)
72
72
  dimension.equivalent?(node1, node2, behavior)
73
73
  end
@@ -41,7 +41,7 @@ module Canon
41
41
  # @param ws1 [Array<String>] First whitespace array
42
42
  # @param ws2 [Array<String>] Second whitespace array
43
43
  # @return [Boolean] true if structural whitespace is exactly equal
44
- def compare_strict(ws1, ws2)
44
+ def compare_strict(ws1, ws2) # rubocop:disable Naming/PredicateMethod
45
45
  ws1 == ws2
46
46
  end
47
47
 
@@ -52,7 +52,7 @@ module Canon
52
52
  # @param ws1 [Array<String>] First whitespace array
53
53
  # @param ws2 [Array<String>] Second whitespace array
54
54
  # @return [Boolean] true if normalized structural whitespace is equal
55
- def compare_normalize(ws1, ws2)
55
+ def compare_normalize(ws1, ws2) # rubocop:disable Naming/PredicateMethod
56
56
  normalize_whitespace(ws1) == normalize_whitespace(ws2)
57
57
  end
58
58
 
@@ -37,19 +37,27 @@ module Canon
37
37
  # @param text1 [String, nil] First text
38
38
  # @param text2 [String, nil] Second text
39
39
  # @return [Boolean] true if texts are exactly equal
40
- def compare_strict(text1, text2)
40
+ def compare_strict(text1, text2) # rubocop:disable Naming/PredicateMethod
41
41
  text1.to_s == text2.to_s
42
42
  end
43
43
 
44
44
  # Normalized text comparison
45
45
  #
46
46
  # Collapses whitespace and compares.
47
+ # Two whitespace-only strings that both normalize to empty are equivalent.
47
48
  #
48
49
  # @param text1 [String, nil] First text
49
50
  # @param text2 [String, nil] Second text
50
51
  # @return [Boolean] true if normalized texts are equal
51
- def compare_normalize(text1, text2)
52
- normalize_text(text1) == normalize_text(text2)
52
+ def compare_normalize(text1, text2) # rubocop:disable Naming/PredicateMethod
53
+ normalized1 = normalize_text(text1)
54
+ normalized2 = normalize_text(text2)
55
+
56
+ # Both empty after normalization = equivalent
57
+ # This handles whitespace-only text nodes that normalize to empty
58
+ return true if normalized1.empty? && normalized2.empty?
59
+
60
+ normalized1 == normalized2
53
61
  end
54
62
 
55
63
  private
@@ -52,7 +52,7 @@ module Canon
52
52
  # @return [Symbol] Format type
53
53
  def detect_string(str)
54
54
  # Use cache for format detection
55
- Cache.fetch(:format_detect, Cache.key_for_format_detection(str)) do
55
+ Cache.fetch(:format_detect, Cache.key_for_format_detection(str)) do # rubocop:disable Lint/UselessDefaultValueArgument
56
56
  detect_string_uncached(str)
57
57
  end
58
58
  end