canon 0.1.6 → 0.1.7

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -67
  3. data/README.adoc +400 -7
  4. data/docs/Gemfile +9 -0
  5. data/docs/INDEX.adoc +99 -182
  6. data/docs/_config.yml +100 -0
  7. data/docs/advanced/diff-classification.adoc +547 -0
  8. data/docs/advanced/diff-pipeline.adoc +358 -0
  9. data/docs/advanced/index.adoc +214 -0
  10. data/docs/advanced/semantic-diff-report.adoc +390 -0
  11. data/docs/{VERBOSE.adoc → advanced/verbose-mode-architecture.adoc} +51 -53
  12. data/docs/features/diff-formatting/algorithm-specific-output.adoc +533 -0
  13. data/docs/{CHARACTER_VISUALIZATION.adoc → features/diff-formatting/character-visualization.adoc} +23 -62
  14. data/docs/features/diff-formatting/colors-and-symbols.adoc +606 -0
  15. data/docs/features/diff-formatting/context-and-grouping.adoc +490 -0
  16. data/docs/features/diff-formatting/display-filtering.adoc +472 -0
  17. data/docs/features/diff-formatting/index.adoc +140 -0
  18. data/docs/features/environment-configuration/index.adoc +327 -0
  19. data/docs/features/environment-configuration/override-system.adoc +436 -0
  20. data/docs/features/environment-configuration/size-limits.adoc +273 -0
  21. data/docs/features/index.adoc +173 -0
  22. data/docs/features/input-validation/index.adoc +521 -0
  23. data/docs/features/match-options/algorithm-specific-behavior.adoc +365 -0
  24. data/docs/features/match-options/html-policies.adoc +312 -0
  25. data/docs/features/match-options/index.adoc +621 -0
  26. data/docs/getting-started/index.adoc +83 -0
  27. data/docs/getting-started/quick-start.adoc +76 -0
  28. data/docs/guides/choosing-configuration.adoc +689 -0
  29. data/docs/guides/index.adoc +181 -0
  30. data/docs/{CLI.adoc → interfaces/cli/index.adoc} +18 -13
  31. data/docs/interfaces/index.adoc +101 -0
  32. data/docs/{RSPEC.adoc → interfaces/rspec/index.adoc} +242 -31
  33. data/docs/{RUBY_API.adoc → interfaces/ruby-api/index.adoc} +118 -16
  34. data/docs/lychee.toml +65 -0
  35. data/docs/reference/cli-options.adoc +418 -0
  36. data/docs/reference/environment-variables.adoc +375 -0
  37. data/docs/reference/index.adoc +204 -0
  38. data/docs/reference/options-across-interfaces.adoc +417 -0
  39. data/docs/understanding/algorithms/dom-diff.adoc +389 -0
  40. data/docs/understanding/algorithms/index.adoc +314 -0
  41. data/docs/understanding/algorithms/semantic-tree-diff.adoc +533 -0
  42. data/docs/understanding/architecture.adoc +447 -0
  43. data/docs/understanding/comparison-pipeline.adoc +317 -0
  44. data/docs/understanding/formats/html.adoc +380 -0
  45. data/docs/understanding/formats/index.adoc +261 -0
  46. data/docs/understanding/formats/json.adoc +390 -0
  47. data/docs/understanding/formats/xml.adoc +366 -0
  48. data/docs/understanding/formats/yaml.adoc +504 -0
  49. data/docs/understanding/index.adoc +130 -0
  50. data/lib/canon/cli.rb +42 -1
  51. data/lib/canon/commands/diff_command.rb +108 -23
  52. data/lib/canon/comparison/compare_profile.rb +101 -0
  53. data/lib/canon/comparison/comparison_result.rb +41 -2
  54. data/lib/canon/comparison/html_comparator.rb +292 -71
  55. data/lib/canon/comparison/html_compare_profile.rb +117 -0
  56. data/lib/canon/comparison/match_options.rb +42 -4
  57. data/lib/canon/comparison/strategies/base_match_strategy.rb +99 -0
  58. data/lib/canon/comparison/strategies/match_strategy_factory.rb +74 -0
  59. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +220 -0
  60. data/lib/canon/comparison/xml_comparator.rb +695 -91
  61. data/lib/canon/comparison.rb +207 -2
  62. data/lib/canon/config/env_provider.rb +71 -0
  63. data/lib/canon/config/env_schema.rb +58 -0
  64. data/lib/canon/config/override_resolver.rb +55 -0
  65. data/lib/canon/config/type_converter.rb +59 -0
  66. data/lib/canon/config.rb +158 -29
  67. data/lib/canon/data_model.rb +29 -0
  68. data/lib/canon/diff/diff_classifier.rb +74 -14
  69. data/lib/canon/diff/diff_context_builder.rb +41 -0
  70. data/lib/canon/diff/diff_line.rb +18 -2
  71. data/lib/canon/diff/diff_node.rb +18 -3
  72. data/lib/canon/diff/diff_node_mapper.rb +71 -12
  73. data/lib/canon/diff/formatting_detector.rb +53 -0
  74. data/lib/canon/diff_formatter/by_line/base_formatter.rb +60 -5
  75. data/lib/canon/diff_formatter/by_line/html_formatter.rb +68 -16
  76. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -37
  77. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -42
  78. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +116 -31
  79. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -37
  80. data/lib/canon/diff_formatter/by_object/base_formatter.rb +126 -19
  81. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +30 -1
  82. data/lib/canon/diff_formatter/debug_output.rb +7 -1
  83. data/lib/canon/diff_formatter/diff_detail_formatter.rb +674 -57
  84. data/lib/canon/diff_formatter/legend.rb +42 -0
  85. data/lib/canon/diff_formatter.rb +78 -9
  86. data/lib/canon/errors.rb +56 -0
  87. data/lib/canon/formatters/html_formatter_base.rb +35 -1
  88. data/lib/canon/formatters/json_formatter.rb +3 -0
  89. data/lib/canon/formatters/yaml_formatter.rb +3 -0
  90. data/lib/canon/html/data_model.rb +229 -0
  91. data/lib/canon/html.rb +9 -0
  92. data/lib/canon/options/cli_generator.rb +70 -0
  93. data/lib/canon/options/registry.rb +234 -0
  94. data/lib/canon/rspec_matchers.rb +34 -13
  95. data/lib/canon/tree_diff/adapters/html_adapter.rb +316 -0
  96. data/lib/canon/tree_diff/adapters/json_adapter.rb +204 -0
  97. data/lib/canon/tree_diff/adapters/xml_adapter.rb +285 -0
  98. data/lib/canon/tree_diff/adapters/yaml_adapter.rb +213 -0
  99. data/lib/canon/tree_diff/core/attribute_comparator.rb +84 -0
  100. data/lib/canon/tree_diff/core/matching.rb +241 -0
  101. data/lib/canon/tree_diff/core/node_signature.rb +164 -0
  102. data/lib/canon/tree_diff/core/node_weight.rb +135 -0
  103. data/lib/canon/tree_diff/core/tree_node.rb +450 -0
  104. data/lib/canon/tree_diff/matchers/hash_matcher.rb +258 -0
  105. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +168 -0
  106. data/lib/canon/tree_diff/matchers/structural_propagator.rb +242 -0
  107. data/lib/canon/tree_diff/matchers/universal_matcher.rb +220 -0
  108. data/lib/canon/tree_diff/operation_converter.rb +631 -0
  109. data/lib/canon/tree_diff/operations/operation.rb +92 -0
  110. data/lib/canon/tree_diff/operations/operation_detector.rb +626 -0
  111. data/lib/canon/tree_diff/tree_diff_integrator.rb +140 -0
  112. data/lib/canon/tree_diff.rb +33 -0
  113. data/lib/canon/validators/json_validator.rb +3 -1
  114. data/lib/canon/validators/yaml_validator.rb +3 -1
  115. data/lib/canon/version.rb +1 -1
  116. data/lib/canon/xml/data_model.rb +22 -23
  117. data/lib/canon/xml/element_matcher.rb +128 -20
  118. data/lib/canon/xml/namespace_helper.rb +110 -0
  119. data/lib/canon.rb +3 -0
  120. metadata +81 -23
  121. data/_config.yml +0 -116
  122. data/docs/ADVANCED_TOPICS.adoc +0 -20
  123. data/docs/BASIC_USAGE.adoc +0 -16
  124. data/docs/CUSTOMIZING_BEHAVIOR.adoc +0 -19
  125. data/docs/DIFF_ARCHITECTURE.adoc +0 -435
  126. data/docs/DIFF_FORMATTING.adoc +0 -540
  127. data/docs/FORMATS.adoc +0 -447
  128. data/docs/INPUT_VALIDATION.adoc +0 -477
  129. data/docs/MATCH_ARCHITECTURE.adoc +0 -463
  130. data/docs/MATCH_OPTIONS.adoc +0 -719
  131. data/docs/MODES.adoc +0 -432
  132. data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +0 -219
  133. data/docs/OPTIONS.adoc +0 -1387
  134. data/docs/PREPROCESSING.adoc +0 -491
  135. data/docs/SEMANTIC_DIFF_REPORT.adoc +0 -528
  136. data/docs/UNDERSTANDING_CANON.adoc +0 -17
@@ -186,31 +186,83 @@ module Canon
186
186
  diff_line.content)
187
187
  when :removed
188
188
  line_num = diff_line.line_number + 1
189
+ formatting = diff_line.formatting?
189
190
  informative = diff_line.informative?
190
- output << format_unified_line(line_num, nil, "-",
191
- diff_line.content,
192
- informative ? :cyan : :red,
193
- informative: informative)
191
+
192
+ output << if formatting
193
+ # Formatting-only removal: [ marker in dark gray
194
+ format_unified_line(line_num, nil, "[",
195
+ diff_line.content,
196
+ :black,
197
+ formatting: true)
198
+ elsif informative
199
+ # Informative removal: < marker in blue
200
+ format_unified_line(line_num, nil, "<",
201
+ diff_line.content,
202
+ :blue,
203
+ informative: true)
204
+ else
205
+ # Normative removal: - marker in red
206
+ format_unified_line(line_num, nil, "-",
207
+ diff_line.content,
208
+ :red)
209
+ end
194
210
  when :added
195
211
  line_num = diff_line.line_number + 1
212
+ formatting = diff_line.formatting?
196
213
  informative = diff_line.informative?
197
- output << format_unified_line(nil, line_num, "+",
198
- diff_line.content,
199
- informative ? :cyan : :green,
200
- informative: informative)
214
+
215
+ output << if formatting
216
+ # Formatting-only addition: ] marker in light gray
217
+ format_unified_line(nil, line_num, "]",
218
+ diff_line.content,
219
+ :white,
220
+ formatting: true)
221
+ elsif informative
222
+ # Informative addition: > marker in cyan
223
+ format_unified_line(nil, line_num, ">",
224
+ diff_line.content,
225
+ :cyan,
226
+ informative: true)
227
+ else
228
+ # Normative addition: + marker in green
229
+ format_unified_line(nil, line_num, "+",
230
+ diff_line.content,
231
+ :green)
232
+ end
201
233
  when :changed
202
234
  line_num = diff_line.line_number + 1
235
+ formatting = diff_line.formatting?
203
236
  informative = diff_line.informative?
204
237
  old_content = lines1[diff_line.line_number]
205
238
  new_content = diff_line.content
206
- output << format_unified_line(line_num, nil, "-",
207
- old_content,
208
- informative ? :cyan : :red,
209
- informative: informative)
210
- output << format_unified_line(nil, line_num, "+",
211
- new_content,
212
- informative ? :cyan : :green,
213
- informative: informative)
239
+
240
+ if formatting
241
+ output << format_unified_line(line_num, nil, "[",
242
+ old_content,
243
+ :black,
244
+ formatting: true)
245
+ output << format_unified_line(nil, line_num, "]",
246
+ new_content,
247
+ :white,
248
+ formatting: true)
249
+ elsif informative
250
+ output << format_unified_line(line_num, nil, "<",
251
+ old_content,
252
+ :blue,
253
+ informative: true)
254
+ output << format_unified_line(nil, line_num, ">",
255
+ new_content,
256
+ :cyan,
257
+ informative: true)
258
+ else
259
+ output << format_unified_line(line_num, nil, "-",
260
+ old_content,
261
+ :red)
262
+ output << format_unified_line(nil, line_num, "+",
263
+ new_content,
264
+ :green)
265
+ end
214
266
  end
215
267
  end
216
268
 
@@ -114,43 +114,6 @@ module Canon
114
114
  output.join("\n")
115
115
  end
116
116
 
117
- # Format a unified diff line
118
- #
119
- # @param old_num [Integer, nil] Line number in old file
120
- # @param new_num [Integer, nil] Line number in new file
121
- # @param marker [String] Diff marker
122
- # @param content [String] Line content
123
- # @param color [Symbol, nil] Color for diff lines
124
- # @return [String] Formatted line
125
- def format_unified_line(old_num, new_num, marker, content, color = nil)
126
- old_str = old_num ? "%4d" % old_num : " "
127
- new_str = new_num ? "%4d" % new_num : " "
128
- marker_part = "#{marker} "
129
-
130
- visualized_content = if color
131
- apply_visualization(content,
132
- color)
133
- else
134
- content
135
- end
136
-
137
- if @use_color
138
- yellow_old = colorize(old_str, :yellow)
139
- yellow_pipe1 = colorize("|", :yellow)
140
- yellow_new = colorize(new_str, :yellow)
141
- yellow_pipe2 = colorize("|", :yellow)
142
-
143
- if color
144
- colored_marker = colorize(marker, color)
145
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{colored_marker} #{yellow_pipe2} #{visualized_content}"
146
- else
147
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{marker} #{yellow_pipe2} #{visualized_content}"
148
- end
149
- else
150
- "#{old_str}|#{new_str}#{marker_part}| #{visualized_content}"
151
- end
152
- end
153
-
154
117
  # Format token diff lines
155
118
  #
156
119
  # @param old_line [Integer] Old line number
@@ -89,48 +89,6 @@ module Canon
89
89
  output.join("\n")
90
90
  end
91
91
 
92
- # Format a unified diff line
93
- #
94
- # @param old_num [Integer, nil] Line number in old file
95
- # @param new_num [Integer, nil] Line number in new file
96
- # @param marker [String] Diff marker (' ', '-', '+')
97
- # @param content [String] Line content
98
- # @param color [Symbol, nil] Color for diff lines
99
- # @return [String] Formatted line
100
- def format_unified_line(old_num, new_num, marker, content, color = nil)
101
- old_str = old_num ? "%4d" % old_num : " "
102
- new_str = new_num ? "%4d" % new_num : " "
103
- marker_part = "#{marker} "
104
-
105
- # Only apply visualization to diff lines (when color is provided),
106
- # not context lines
107
- visualized_content = if color
108
- apply_visualization(content, color)
109
- else
110
- content
111
- end
112
-
113
- if @use_color
114
- # Yellow for line numbers and pipes
115
- yellow_old = colorize(old_str, :yellow)
116
- yellow_pipe1 = colorize("|", :yellow)
117
- yellow_new = colorize(new_str, :yellow)
118
- yellow_pipe2 = colorize("|", :yellow)
119
-
120
- if color
121
- # Colored marker for additions/deletions
122
- colored_marker = colorize(marker, color)
123
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{colored_marker} #{yellow_pipe2} #{visualized_content}"
124
- else
125
- # Context line - apply visualization but no color
126
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{marker} #{yellow_pipe2} #{visualized_content}"
127
- end
128
- else
129
- # No color mode
130
- "#{old_str}|#{new_str}#{marker_part}| #{visualized_content}"
131
- end
132
- end
133
-
134
92
  # Format changed lines with basic character-level diff
135
93
  #
136
94
  # @param line_num [Integer] Line number
@@ -99,33 +99,85 @@ module Canon
99
99
  diff_line.content)
100
100
  when :removed
101
101
  line_num = diff_line.line_number + 1
102
+ formatting = diff_line.formatting?
102
103
  informative = diff_line.informative?
103
- output << format_unified_line(line_num, nil, "-",
104
- diff_line.content,
105
- informative ? :cyan : :red,
106
- informative: informative)
104
+
105
+ output << if formatting
106
+ # Formatting-only removal: [ marker in dark gray
107
+ format_unified_line(line_num, nil, "[",
108
+ diff_line.content,
109
+ :black,
110
+ formatting: true)
111
+ elsif informative
112
+ # Informative removal: < marker in blue
113
+ format_unified_line(line_num, nil, "<",
114
+ diff_line.content,
115
+ :blue,
116
+ informative: true)
117
+ else
118
+ # Normative removal: - marker in red
119
+ format_unified_line(line_num, nil, "-",
120
+ diff_line.content,
121
+ :red)
122
+ end
107
123
  when :added
108
124
  line_num = diff_line.line_number + 1
125
+ formatting = diff_line.formatting?
109
126
  informative = diff_line.informative?
110
- output << format_unified_line(nil, line_num, "+",
111
- diff_line.content,
112
- informative ? :cyan : :green,
113
- informative: informative)
127
+
128
+ output << if formatting
129
+ # Formatting-only addition: ] marker in light gray
130
+ format_unified_line(nil, line_num, "]",
131
+ diff_line.content,
132
+ :white,
133
+ formatting: true)
134
+ elsif informative
135
+ # Informative addition: > marker in cyan
136
+ format_unified_line(nil, line_num, ">",
137
+ diff_line.content,
138
+ :cyan,
139
+ informative: true)
140
+ else
141
+ # Normative addition: + marker in green
142
+ format_unified_line(nil, line_num, "+",
143
+ diff_line.content,
144
+ :green)
145
+ end
114
146
  when :changed
115
147
  line_num = diff_line.line_number + 1
148
+ formatting = diff_line.formatting?
116
149
  informative = diff_line.informative?
117
150
  # For changed lines, we need both old and new content
118
151
  # For now, show as removed + added
119
152
  old_content = lines1[diff_line.line_number]
120
153
  new_content = diff_line.content
121
- output << format_unified_line(line_num, nil, "-",
122
- old_content,
123
- informative ? :cyan : :red,
124
- informative: informative)
125
- output << format_unified_line(nil, line_num, "+",
126
- new_content,
127
- informative ? :cyan : :green,
128
- informative: informative)
154
+
155
+ if formatting
156
+ output << format_unified_line(line_num, nil, "[",
157
+ old_content,
158
+ :black,
159
+ formatting: true)
160
+ output << format_unified_line(nil, line_num, "]",
161
+ new_content,
162
+ :white,
163
+ formatting: true)
164
+ elsif informative
165
+ output << format_unified_line(line_num, nil, "<",
166
+ old_content,
167
+ :blue,
168
+ informative: true)
169
+ output << format_unified_line(nil, line_num, ">",
170
+ new_content,
171
+ :cyan,
172
+ informative: true)
173
+ else
174
+ output << format_unified_line(line_num, nil, "-",
175
+ old_content,
176
+ :red)
177
+ output << format_unified_line(nil, line_num, "+",
178
+ new_content,
179
+ :green)
180
+ end
129
181
  end
130
182
  end
131
183
 
@@ -626,6 +678,8 @@ module Canon
626
678
 
627
679
  # Format a context
628
680
  def format_context(context, diffs, base_line1, base_line2)
681
+ require_relative "../../diff/formatting_detector"
682
+
629
683
  output = []
630
684
 
631
685
  (context.start_idx..context.end_idx).each do |idx|
@@ -639,22 +693,53 @@ module Canon
639
693
  output << format_unified_line(line1, line2, " ",
640
694
  change.old_element)
641
695
  when "-"
642
- output << format_unified_line(line1, nil, "-",
643
- change.old_element, :red)
696
+ # Check if removal is formatting-only
697
+ output << if Canon::Diff::FormattingDetector.formatting_only?(
698
+ change.old_element, ""
699
+ )
700
+ format_unified_line(line1, nil, "[",
701
+ change.old_element, :black,
702
+ formatting: true)
703
+ else
704
+ format_unified_line(line1, nil, "-",
705
+ change.old_element, :red)
706
+ end
644
707
  when "+"
645
- output << format_unified_line(nil, line2, "+",
646
- change.new_element, :green)
708
+ # Check if addition is formatting-only
709
+ output << if Canon::Diff::FormattingDetector.formatting_only?("",
710
+ change.new_element)
711
+ format_unified_line(nil, line2, "]",
712
+ change.new_element, :white,
713
+ formatting: true)
714
+ else
715
+ format_unified_line(nil, line2, "+",
716
+ change.new_element, :green)
717
+ end
647
718
  when "!"
648
- # Token-level highlighting
649
- old_tokens = tokenize_xml(change.old_element)
650
- new_tokens = tokenize_xml(change.new_element)
651
- token_diffs = ::Diff::LCS.sdiff(old_tokens, new_tokens)
652
-
653
- old_highlighted = build_token_highlighted_text(token_diffs, :old)
654
- new_highlighted = build_token_highlighted_text(token_diffs, :new)
655
-
656
- output << format_token_diff_line(line1, line2, old_highlighted,
657
- new_highlighted)
719
+ # Check if change is formatting-only
720
+ if Canon::Diff::FormattingDetector.formatting_only?(
721
+ change.old_element, change.new_element
722
+ )
723
+ output << format_unified_line(line1, nil, "[",
724
+ change.old_element, :black,
725
+ formatting: true)
726
+ output << format_unified_line(nil, line2, "]",
727
+ change.new_element, :white,
728
+ formatting: true)
729
+ else
730
+ # Token-level highlighting
731
+ old_tokens = tokenize_xml(change.old_element)
732
+ new_tokens = tokenize_xml(change.new_element)
733
+ token_diffs = ::Diff::LCS.sdiff(old_tokens, new_tokens)
734
+
735
+ old_highlighted = build_token_highlighted_text(token_diffs,
736
+ :old)
737
+ new_highlighted = build_token_highlighted_text(token_diffs,
738
+ :new)
739
+
740
+ output << format_token_diff_line(line1, line2, old_highlighted,
741
+ new_highlighted)
742
+ end
658
743
  end
659
744
  end
660
745
 
@@ -722,7 +807,7 @@ module Canon
722
807
 
723
808
  # Format a unified diff line
724
809
  def format_unified_line(old_num, new_num, marker, content, color = nil,
725
- informative: false)
810
+ informative: false, formatting: false)
726
811
  old_str = old_num ? "%4d" % old_num : " "
727
812
  new_str = new_num ? "%4d" % new_num : " "
728
813
  marker_part = "#{marker} "
@@ -113,43 +113,6 @@ module Canon
113
113
  output.join("\n")
114
114
  end
115
115
 
116
- # Format a unified diff line
117
- #
118
- # @param old_num [Integer, nil] Line number in old file
119
- # @param new_num [Integer, nil] Line number in new file
120
- # @param marker [String] Diff marker
121
- # @param content [String] Line content
122
- # @param color [Symbol, nil] Color for diff lines
123
- # @return [String] Formatted line
124
- def format_unified_line(old_num, new_num, marker, content, color = nil)
125
- old_str = old_num ? "%4d" % old_num : " "
126
- new_str = new_num ? "%4d" % new_num : " "
127
- marker_part = "#{marker} "
128
-
129
- visualized_content = if color
130
- apply_visualization(content,
131
- color)
132
- else
133
- content
134
- end
135
-
136
- if @use_color
137
- yellow_old = colorize(old_str, :yellow)
138
- yellow_pipe1 = colorize("|", :yellow)
139
- yellow_new = colorize(new_str, :yellow)
140
- yellow_pipe2 = colorize("|", :yellow)
141
-
142
- if color
143
- colored_marker = colorize(marker, color)
144
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{colored_marker} #{yellow_pipe2} #{visualized_content}"
145
- else
146
- "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{marker} #{yellow_pipe2} #{visualized_content}"
147
- end
148
- else
149
- "#{old_str}|#{new_str}#{marker_part}| #{visualized_content}"
150
- end
151
- end
152
-
153
116
  # Format token diff lines
154
117
  #
155
118
  # @param old_line [Integer] Old line number
@@ -8,10 +8,12 @@ module Canon
8
8
  class BaseFormatter
9
9
  attr_reader :use_color, :visualization_map
10
10
 
11
- def initialize(use_color: true, visualization_map: nil)
11
+ def initialize(use_color: true, visualization_map: nil,
12
+ show_diffs: :all)
12
13
  @use_color = use_color
13
14
  @visualization_map = visualization_map ||
14
- DiffFormatter::DEFAULT_VISUALIZATION_MAP
15
+ Canon::DiffFormatter::DEFAULT_VISUALIZATION_MAP
16
+ @show_diffs = show_diffs
15
17
  end
16
18
 
17
19
  # Format differences for display
@@ -35,37 +37,90 @@ module Canon
35
37
  output = []
36
38
  output << colorize("Visual Diff:", :cyan, :bold)
37
39
 
40
+ # Filter differences for display based on show_diffs setting
41
+ filtered_diffs = filter_differences_for_display(diffs_array)
42
+
38
43
  # Group differences by path for tree building
39
- tree = build_diff_tree(diffs_array)
44
+ tree = build_diff_tree(filtered_diffs)
45
+
46
+ # Render tree with line counting
47
+ @line_count = 0
48
+ @max_lines = get_max_diff_lines
49
+ rendered = render_tree(tree)
50
+
51
+ # Add truncation notice if needed
52
+ if @truncated
53
+ rendered += "\n\n"
54
+ rendered += colorize(
55
+ "... Output truncated at #{@max_lines} lines ...", :yellow, :bold
56
+ )
57
+ rendered += "\n"
58
+ rendered += colorize(
59
+ "Increase limit via CANON_MAX_DIFF_LINES or config.diff.max_diff_lines", :yellow
60
+ )
61
+ end
40
62
 
41
- # Render tree
42
- output << render_tree(tree)
63
+ output << rendered
43
64
 
44
65
  output.join("\n")
45
66
  end
46
67
 
47
68
  # Factory method to create format-specific formatter
48
- def self.for_format(format, use_color: true, visualization_map: nil)
69
+ def self.for_format(format, use_color: true, visualization_map: nil,
70
+ show_diffs: :all)
49
71
  case format
50
72
  when :xml, :html
51
73
  require_relative "xml_formatter"
52
74
  XmlFormatter.new(use_color: use_color,
53
- visualization_map: visualization_map)
75
+ visualization_map: visualization_map,
76
+ show_diffs: show_diffs)
54
77
  when :json
55
78
  require_relative "json_formatter"
56
79
  JsonFormatter.new(use_color: use_color,
57
- visualization_map: visualization_map)
80
+ visualization_map: visualization_map,
81
+ show_diffs: show_diffs)
58
82
  when :yaml
59
83
  require_relative "yaml_formatter"
60
84
  YamlFormatter.new(use_color: use_color,
61
- visualization_map: visualization_map)
85
+ visualization_map: visualization_map,
86
+ show_diffs: show_diffs)
62
87
  else
63
- new(use_color: use_color, visualization_map: visualization_map)
88
+ new(use_color: use_color, visualization_map: visualization_map,
89
+ show_diffs: show_diffs)
64
90
  end
65
91
  end
66
92
 
67
93
  private
68
94
 
95
+ # Filter differences for display based on show_diffs setting
96
+ #
97
+ # @param differences [Array<Canon::Diff::DiffNode>] Array of differences
98
+ # @return [Array<Canon::Diff::DiffNode>] Filtered differences
99
+ def filter_differences_for_display(differences)
100
+ return differences if @show_diffs.nil? || @show_diffs == :all
101
+
102
+ differences.select do |diff|
103
+ # Handle both DiffNode objects and legacy Hash format
104
+ is_normative = if diff.respond_to?(:normative?)
105
+ diff.normative?
106
+ elsif diff.is_a?(Hash) && diff.key?(:normative)
107
+ diff[:normative]
108
+ else
109
+ # Default to normative if unknown
110
+ true
111
+ end
112
+
113
+ case @show_diffs
114
+ when :normative
115
+ is_normative
116
+ when :informative
117
+ !is_normative
118
+ else
119
+ true # Unknown value, show all
120
+ end
121
+ end
122
+ end
123
+
69
124
  # Generate success message
70
125
  def success_message
71
126
  emoji = @use_color ? "✅ " : ""
@@ -104,7 +159,9 @@ module Canon
104
159
  parts.each_with_index do |part, index|
105
160
  current[part] ||= {}
106
161
  if index == parts.length - 1
107
- current[part][:__diff__] = diff
162
+ # Support multiple diffs at the same path
163
+ current[part][:__diffs__] ||= []
164
+ current[part][:__diffs__] << diff
108
165
  else
109
166
  current = current[part]
110
167
  end
@@ -135,9 +192,24 @@ module Canon
135
192
 
136
193
  parts = []
137
194
  current = node
195
+ visited = Set.new
138
196
 
139
197
  while current.respond_to?(:name)
198
+ # Prevent infinite loops by tracking visited nodes
199
+ break if visited.include?(current.object_id)
200
+
201
+ visited << current.object_id
202
+
140
203
  parts.unshift(current.name) if current.name
204
+
205
+ # Stop at document or fragment roots
206
+ break if current.is_a?(Nokogiri::XML::Document) ||
207
+ current.is_a?(Nokogiri::HTML4::Document) ||
208
+ current.is_a?(Nokogiri::HTML5::Document) ||
209
+ current.is_a?(Nokogiri::XML::DocumentFragment) ||
210
+ current.is_a?(Nokogiri::HTML4::DocumentFragment) ||
211
+ current.is_a?(Nokogiri::HTML5::DocumentFragment)
212
+
141
213
  current = current.parent if current.respond_to?(:parent)
142
214
  end
143
215
 
@@ -148,7 +220,7 @@ module Canon
148
220
  def render_tree(tree, prefix: "", is_last: true)
149
221
  output = []
150
222
 
151
- sorted_keys = tree.keys.reject { |k| k == :__diff__ }
223
+ sorted_keys = tree.keys.reject { |k| k == :__diffs__ }
152
224
  begin
153
225
  sorted_keys = sorted_keys.sort_by(&:to_s)
154
226
  rescue ArgumentError
@@ -156,25 +228,50 @@ module Canon
156
228
  end
157
229
 
158
230
  sorted_keys.each_with_index do |key, index|
231
+ # Check line limit
232
+ if @max_lines&.positive? && @line_count >= @max_lines
233
+ @truncated = true
234
+ break
235
+ end
236
+
159
237
  is_last_item = (index == sorted_keys.length - 1)
160
238
  connector = is_last_item ? "└── " : "├── "
161
239
  continuation = is_last_item ? " " : "│ "
162
240
 
163
241
  value = tree[key]
164
- diff = value[:__diff__] if value.is_a?(Hash)
165
-
166
- if diff
167
- # Render difference
168
- output << render_diff_node(key, diff, prefix, connector)
242
+ diffs = value[:__diffs__] if value.is_a?(Hash)
243
+
244
+ if diffs && !diffs.empty?
245
+ # Render all differences at this path
246
+ diffs.each_with_index do |diff, diff_idx|
247
+ # Use proper connector for each diff
248
+ current_connector = if diff_idx == diffs.length - 1
249
+ connector
250
+ else
251
+ is_last_item ? "├── " : "├── "
252
+ end
253
+
254
+ line = render_diff_node(key, diff, prefix, current_connector)
255
+ output << line
256
+ @line_count += line.count("\n") + 1
257
+ end
169
258
  else
170
259
  # Render intermediate path
171
- output << colorize("#{prefix}#{connector}#{key}:", :cyan)
260
+ line = colorize("#{prefix}#{connector}#{key}:", :cyan)
261
+ output << line
262
+ @line_count += 1
263
+
172
264
  # Recurse into subtree
173
265
  if value.is_a?(Hash)
174
- output << render_tree(value, prefix: prefix + continuation,
266
+ subtree = render_tree(value, prefix: prefix + continuation,
175
267
  is_last: is_last_item)
268
+ output << subtree
269
+ # line_count already updated in recursive call
176
270
  end
177
271
  end
272
+
273
+ # Check again after adding content
274
+ break if @truncated
178
275
  end
179
276
 
180
277
  output.join("\n")
@@ -193,6 +290,16 @@ module Canon
193
290
  require "paint"
194
291
  "\e[0m#{Paint[text, *colors]}"
195
292
  end
293
+
294
+ # Get max diff lines limit
295
+ #
296
+ # @return [Integer, nil] Max diff output lines
297
+ def get_max_diff_lines
298
+ # Try to get from config if available
299
+ config = Canon::Config.instance
300
+ # Default to 10,000 if config not available
301
+ config&.xml&.diff&.max_diff_lines || 10_000
302
+ end
196
303
  end
197
304
  end
198
305
  end