canon 0.1.3 → 0.1.5
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -1
- data/.rubocop_todo.yml +276 -7
- data/README.adoc +203 -138
- data/_config.yml +116 -0
- data/docs/ADVANCED_TOPICS.adoc +20 -0
- data/docs/BASIC_USAGE.adoc +16 -0
- data/docs/CHARACTER_VISUALIZATION.adoc +567 -0
- data/docs/CLI.adoc +493 -0
- data/docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
- data/docs/DIFF_ARCHITECTURE.adoc +435 -0
- data/docs/DIFF_FORMATTING.adoc +540 -0
- data/docs/FORMATS.adoc +447 -0
- data/docs/INDEX.adoc +222 -0
- data/docs/INPUT_VALIDATION.adoc +477 -0
- data/docs/MATCH_ARCHITECTURE.adoc +463 -0
- data/docs/MATCH_OPTIONS.adoc +719 -0
- data/docs/MODES.adoc +432 -0
- data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
- data/docs/OPTIONS.adoc +1387 -0
- data/docs/PREPROCESSING.adoc +491 -0
- data/docs/RSPEC.adoc +605 -0
- data/docs/RUBY_API.adoc +478 -0
- data/docs/SEMANTIC_DIFF_REPORT.adoc +528 -0
- data/docs/UNDERSTANDING_CANON.adoc +17 -0
- data/docs/VERBOSE.adoc +482 -0
- data/exe/canon +7 -0
- data/lib/canon/cli.rb +179 -0
- data/lib/canon/commands/diff_command.rb +195 -0
- data/lib/canon/commands/format_command.rb +113 -0
- data/lib/canon/comparison/base_comparator.rb +39 -0
- data/lib/canon/comparison/comparison_result.rb +79 -0
- data/lib/canon/comparison/html_comparator.rb +410 -0
- data/lib/canon/comparison/json_comparator.rb +212 -0
- data/lib/canon/comparison/match_options.rb +616 -0
- data/lib/canon/comparison/xml_comparator.rb +566 -0
- data/lib/canon/comparison/yaml_comparator.rb +93 -0
- data/lib/canon/comparison.rb +239 -0
- data/lib/canon/config.rb +172 -0
- data/lib/canon/diff/diff_block.rb +71 -0
- data/lib/canon/diff/diff_block_builder.rb +105 -0
- data/lib/canon/diff/diff_classifier.rb +46 -0
- data/lib/canon/diff/diff_context.rb +85 -0
- data/lib/canon/diff/diff_context_builder.rb +107 -0
- data/lib/canon/diff/diff_line.rb +77 -0
- data/lib/canon/diff/diff_node.rb +56 -0
- data/lib/canon/diff/diff_node_mapper.rb +148 -0
- data/lib/canon/diff/diff_report.rb +133 -0
- data/lib/canon/diff/diff_report_builder.rb +62 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +407 -0
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +672 -0
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +284 -0
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +190 -0
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +860 -0
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +292 -0
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +199 -0
- data/lib/canon/diff_formatter/by_object/json_formatter.rb +305 -0
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +248 -0
- data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +17 -0
- data/lib/canon/diff_formatter/character_map.yml +197 -0
- data/lib/canon/diff_formatter/debug_output.rb +431 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +551 -0
- data/lib/canon/diff_formatter/legend.rb +141 -0
- data/lib/canon/diff_formatter.rb +520 -0
- data/lib/canon/errors.rb +56 -0
- data/lib/canon/formatters/html4_formatter.rb +17 -0
- data/lib/canon/formatters/html5_formatter.rb +17 -0
- data/lib/canon/formatters/html_formatter.rb +37 -0
- data/lib/canon/formatters/html_formatter_base.rb +163 -0
- data/lib/canon/formatters/json_formatter.rb +3 -0
- data/lib/canon/formatters/xml_formatter.rb +20 -55
- data/lib/canon/formatters/yaml_formatter.rb +4 -1
- data/lib/canon/pretty_printer/html.rb +57 -0
- data/lib/canon/pretty_printer/json.rb +25 -0
- data/lib/canon/pretty_printer/xml.rb +29 -0
- data/lib/canon/rspec_matchers.rb +222 -80
- data/lib/canon/validators/base_validator.rb +49 -0
- data/lib/canon/validators/html_validator.rb +138 -0
- data/lib/canon/validators/json_validator.rb +89 -0
- data/lib/canon/validators/xml_validator.rb +53 -0
- data/lib/canon/validators/yaml_validator.rb +73 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/attribute_handler.rb +80 -0
- data/lib/canon/xml/c14n.rb +36 -0
- data/lib/canon/xml/character_encoder.rb +38 -0
- data/lib/canon/xml/data_model.rb +225 -0
- data/lib/canon/xml/element_matcher.rb +196 -0
- data/lib/canon/xml/line_range_mapper.rb +158 -0
- data/lib/canon/xml/namespace_handler.rb +86 -0
- data/lib/canon/xml/node.rb +32 -0
- data/lib/canon/xml/nodes/attribute_node.rb +54 -0
- data/lib/canon/xml/nodes/comment_node.rb +23 -0
- data/lib/canon/xml/nodes/element_node.rb +56 -0
- data/lib/canon/xml/nodes/namespace_node.rb +38 -0
- data/lib/canon/xml/nodes/processing_instruction_node.rb +24 -0
- data/lib/canon/xml/nodes/root_node.rb +16 -0
- data/lib/canon/xml/nodes/text_node.rb +23 -0
- data/lib/canon/xml/processor.rb +151 -0
- data/lib/canon/xml/whitespace_normalizer.rb +72 -0
- data/lib/canon/xml/xml_base_handler.rb +188 -0
- data/lib/canon.rb +14 -3
- metadata +116 -21
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
class DiffFormatter
|
|
7
|
+
module ByObject
|
|
8
|
+
# JSON tree formatter for by-object diffs
|
|
9
|
+
# Handles Ruby object differences (hashes and arrays)
|
|
10
|
+
class JsonFormatter < BaseFormatter
|
|
11
|
+
# Render a diff node for JSON/Ruby object differences
|
|
12
|
+
#
|
|
13
|
+
# @param key [String] Hash key or array index
|
|
14
|
+
# @param diff [Hash] Difference information
|
|
15
|
+
# @param prefix [String] Tree prefix for indentation
|
|
16
|
+
# @param connector [String] Box-drawing connector character
|
|
17
|
+
# @return [String] Formatted diff node
|
|
18
|
+
def render_diff_node(key, diff, prefix, connector)
|
|
19
|
+
output = []
|
|
20
|
+
|
|
21
|
+
# Show full path if available (path in cyan, no color on tree structure)
|
|
22
|
+
path_display = if diff[:path] && !diff[:path].empty?
|
|
23
|
+
colorize(diff[:path].to_s, :cyan, :bold)
|
|
24
|
+
else
|
|
25
|
+
colorize(key.to_s, :cyan)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
output << "#{prefix}#{connector}#{path_display}:"
|
|
29
|
+
|
|
30
|
+
# Determine continuation for nested values
|
|
31
|
+
continuation = connector.start_with?("├") ? "│ " : " "
|
|
32
|
+
value_prefix = prefix + continuation
|
|
33
|
+
|
|
34
|
+
diff_code = diff[:diff_code] || diff[:diff1]
|
|
35
|
+
|
|
36
|
+
case diff_code
|
|
37
|
+
when Comparison::MISSING_HASH_KEY
|
|
38
|
+
render_missing_key(diff, value_prefix, output)
|
|
39
|
+
when Comparison::UNEQUAL_PRIMITIVES
|
|
40
|
+
render_unequal_primitives(diff, value_prefix, output)
|
|
41
|
+
when Comparison::UNEQUAL_HASH_VALUES
|
|
42
|
+
render_unequal_hash_values(diff, value_prefix, output)
|
|
43
|
+
when Comparison::UNEQUAL_ARRAY_ELEMENTS
|
|
44
|
+
render_unequal_array_elements(diff, value_prefix, output)
|
|
45
|
+
when Comparison::UNEQUAL_ARRAY_LENGTHS
|
|
46
|
+
render_unequal_array_lengths(diff, value_prefix, output)
|
|
47
|
+
when Comparison::UNEQUAL_TYPES
|
|
48
|
+
render_unequal_types(diff, value_prefix, output)
|
|
49
|
+
else
|
|
50
|
+
# Fallback for unknown diff types
|
|
51
|
+
render_fallback(diff, value_prefix, output)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
output.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Render missing hash key
|
|
60
|
+
def render_missing_key(diff, prefix, output)
|
|
61
|
+
if diff[:value1].nil?
|
|
62
|
+
# Key added in file2
|
|
63
|
+
if diff[:value2].is_a?(Hash) && !diff[:value2].empty?
|
|
64
|
+
output.concat(render_added_hash(diff[:value2], prefix))
|
|
65
|
+
else
|
|
66
|
+
value_str = format_value_for_diff(diff[:value2])
|
|
67
|
+
output << "#{prefix}└── + #{colorize(value_str, :green)}"
|
|
68
|
+
end
|
|
69
|
+
elsif diff[:value1].is_a?(Hash) && !diff[:value1].empty?
|
|
70
|
+
# Key removed in file2
|
|
71
|
+
output.concat(render_removed_hash(diff[:value1], prefix))
|
|
72
|
+
else
|
|
73
|
+
value_str = format_value_for_diff(diff[:value1])
|
|
74
|
+
output << "#{prefix}└── - #{colorize(value_str, :red)}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Render unequal primitives
|
|
79
|
+
def render_unequal_primitives(diff, prefix, output)
|
|
80
|
+
output.concat(render_value_diff(diff[:value1], diff[:value2],
|
|
81
|
+
prefix))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Render unequal hash values
|
|
85
|
+
def render_unequal_hash_values(diff, prefix, output)
|
|
86
|
+
output.concat(render_value_diff(diff[:value1], diff[:value2],
|
|
87
|
+
prefix))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Render unequal array elements
|
|
91
|
+
def render_unequal_array_elements(diff, prefix, output)
|
|
92
|
+
output.concat(render_value_diff(diff[:value1], diff[:value2],
|
|
93
|
+
prefix))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Render unequal array lengths
|
|
97
|
+
def render_unequal_array_lengths(diff, prefix, output)
|
|
98
|
+
output.concat(render_value_diff(diff[:value1], diff[:value2],
|
|
99
|
+
prefix))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Render unequal types
|
|
103
|
+
def render_unequal_types(diff, prefix, output)
|
|
104
|
+
output << "#{prefix}├── - #{colorize(
|
|
105
|
+
"#{diff[:value1].class.name}: #{format_value_for_diff(diff[:value1])}",
|
|
106
|
+
:red,
|
|
107
|
+
)}"
|
|
108
|
+
output << "#{prefix}└── + #{colorize(
|
|
109
|
+
"#{diff[:value2].class.name}: #{format_value_for_diff(diff[:value2])}",
|
|
110
|
+
:green,
|
|
111
|
+
)}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Render fallback for unknown diff types
|
|
115
|
+
def render_fallback(diff, prefix, output)
|
|
116
|
+
if diff[:value1] && diff[:value2]
|
|
117
|
+
output.concat(render_value_diff(diff[:value1], diff[:value2],
|
|
118
|
+
prefix))
|
|
119
|
+
elsif diff[:value1]
|
|
120
|
+
value_str = format_value_for_diff(diff[:value1])
|
|
121
|
+
output << "#{prefix}└── - #{colorize(value_str, :red)}"
|
|
122
|
+
elsif diff[:value2]
|
|
123
|
+
value_str = format_value_for_diff(diff[:value2])
|
|
124
|
+
output << "#{prefix}└── + #{colorize(value_str, :green)}"
|
|
125
|
+
else
|
|
126
|
+
output << "#{prefix}└── [UNKNOWN CHANGE]"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Render an added hash with nested structure
|
|
131
|
+
def render_added_hash(hash, prefix)
|
|
132
|
+
output = []
|
|
133
|
+
sorted_keys = hash.keys.sort_by(&:to_s)
|
|
134
|
+
|
|
135
|
+
sorted_keys.each_with_index do |key, index|
|
|
136
|
+
is_last = (index == sorted_keys.length - 1)
|
|
137
|
+
connector = is_last ? "└──" : "├──"
|
|
138
|
+
continuation = is_last ? " " : "│ "
|
|
139
|
+
|
|
140
|
+
value = hash[key]
|
|
141
|
+
if value.is_a?(Hash) && !value.empty?
|
|
142
|
+
# Nested hash - recurse
|
|
143
|
+
output << "#{prefix}#{connector} + #{colorize(key.to_s, :green)}:"
|
|
144
|
+
output.concat(render_added_hash(value, prefix + continuation))
|
|
145
|
+
else
|
|
146
|
+
# Leaf value
|
|
147
|
+
value_str = format_value_for_diff(value)
|
|
148
|
+
output << "#{prefix}#{connector} + #{colorize(key.to_s,
|
|
149
|
+
:green)}: #{colorize(
|
|
150
|
+
value_str, :green
|
|
151
|
+
)}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
output
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Render a removed hash with nested structure
|
|
159
|
+
def render_removed_hash(hash, prefix)
|
|
160
|
+
output = []
|
|
161
|
+
sorted_keys = hash.keys.sort_by(&:to_s)
|
|
162
|
+
|
|
163
|
+
sorted_keys.each_with_index do |key, index|
|
|
164
|
+
is_last = (index == sorted_keys.length - 1)
|
|
165
|
+
connector = is_last ? "└──" : "├──"
|
|
166
|
+
continuation = is_last ? " " : "│ "
|
|
167
|
+
|
|
168
|
+
value = hash[key]
|
|
169
|
+
if value.is_a?(Hash) && !value.empty?
|
|
170
|
+
# Nested hash - recurse
|
|
171
|
+
output << "#{prefix}#{connector} - #{colorize(key.to_s, :red)}:"
|
|
172
|
+
output.concat(render_removed_hash(value, prefix + continuation))
|
|
173
|
+
else
|
|
174
|
+
# Leaf value
|
|
175
|
+
value_str = format_value_for_diff(value)
|
|
176
|
+
output << "#{prefix}#{connector} - #{colorize(key.to_s,
|
|
177
|
+
:red)}: #{colorize(
|
|
178
|
+
value_str, :red
|
|
179
|
+
)}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
output
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Render a detailed diff for two values
|
|
187
|
+
def render_value_diff(val1, val2, prefix)
|
|
188
|
+
output = []
|
|
189
|
+
|
|
190
|
+
# Handle arrays - show element-by-element comparison
|
|
191
|
+
if val1.is_a?(Array) && val2.is_a?(Array)
|
|
192
|
+
output.concat(render_array_diff(val1, val2, prefix))
|
|
193
|
+
elsif val1.is_a?(Hash) && val2.is_a?(Hash)
|
|
194
|
+
# For hashes, show summary (detailed comparison happens recursively)
|
|
195
|
+
val1_str = format_value_for_diff(val1)
|
|
196
|
+
val2_str = format_value_for_diff(val2)
|
|
197
|
+
output << "#{prefix}├── - #{colorize(val1_str, :red)}"
|
|
198
|
+
output << "#{prefix}└── + #{colorize(val2_str, :green)}"
|
|
199
|
+
else
|
|
200
|
+
# Primitives - show actual values
|
|
201
|
+
val1_str = format_value_for_diff(val1)
|
|
202
|
+
val2_str = format_value_for_diff(val2)
|
|
203
|
+
output << "#{prefix}├── - #{colorize(val1_str, :red)}"
|
|
204
|
+
output << "#{prefix}└── + #{colorize(val2_str, :green)}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
output
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Render array diff with element-by-element comparison
|
|
211
|
+
def render_array_diff(arr1, arr2, prefix)
|
|
212
|
+
output = []
|
|
213
|
+
max_len = [arr1.length, arr2.length].max
|
|
214
|
+
changes = []
|
|
215
|
+
|
|
216
|
+
(0...max_len).each do |i|
|
|
217
|
+
elem1 = i < arr1.length ? arr1[i] : nil
|
|
218
|
+
elem2 = i < arr2.length ? arr2[i] : nil
|
|
219
|
+
|
|
220
|
+
if elem1.nil?
|
|
221
|
+
# Element added
|
|
222
|
+
elem_str = format_value_for_diff(elem2)
|
|
223
|
+
changes << { type: :add, index: i, value: elem_str }
|
|
224
|
+
elsif elem2.nil?
|
|
225
|
+
# Element removed
|
|
226
|
+
elem_str = format_value_for_diff(elem1)
|
|
227
|
+
changes << { type: :remove, index: i, value: elem_str }
|
|
228
|
+
elsif elem1 != elem2
|
|
229
|
+
# Element changed
|
|
230
|
+
elem1_str = format_value_for_diff(elem1)
|
|
231
|
+
elem2_str = format_value_for_diff(elem2)
|
|
232
|
+
changes << { type: :change, index: i, old: elem1_str,
|
|
233
|
+
new: elem2_str }
|
|
234
|
+
end
|
|
235
|
+
# Skip if elements are equal
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Render changes with proper connectors
|
|
239
|
+
changes.each_with_index do |change, idx|
|
|
240
|
+
is_last = (idx == changes.length - 1)
|
|
241
|
+
connector = is_last ? "└──" : "├──"
|
|
242
|
+
|
|
243
|
+
case change[:type]
|
|
244
|
+
when :add
|
|
245
|
+
output << "#{prefix}#{connector} [#{change[:index]}] + #{colorize(
|
|
246
|
+
change[:value], :green
|
|
247
|
+
)}"
|
|
248
|
+
when :remove
|
|
249
|
+
output << "#{prefix}#{connector} [#{change[:index]}] - #{colorize(
|
|
250
|
+
change[:value], :red
|
|
251
|
+
)}"
|
|
252
|
+
when :change
|
|
253
|
+
output << "#{prefix}├── [#{change[:index]}] - #{colorize(
|
|
254
|
+
change[:old], :red
|
|
255
|
+
)}"
|
|
256
|
+
output << if is_last
|
|
257
|
+
"#{prefix}└── [#{change[:index]}] + #{colorize(
|
|
258
|
+
change[:new], :green
|
|
259
|
+
)}"
|
|
260
|
+
else
|
|
261
|
+
"#{prefix}├── [#{change[:index]}] + #{colorize(
|
|
262
|
+
change[:new], :green
|
|
263
|
+
)}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
output
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Format a value for diff display
|
|
272
|
+
def format_value_for_diff(value)
|
|
273
|
+
case value
|
|
274
|
+
when String
|
|
275
|
+
"\"#{value}\""
|
|
276
|
+
when Numeric, TrueClass, FalseClass
|
|
277
|
+
value.to_s
|
|
278
|
+
when NilClass
|
|
279
|
+
"nil"
|
|
280
|
+
when Array
|
|
281
|
+
if value.empty?
|
|
282
|
+
"[]"
|
|
283
|
+
elsif value.all? do |v|
|
|
284
|
+
v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass) || v.nil?
|
|
285
|
+
end
|
|
286
|
+
# Simple array - show inline
|
|
287
|
+
"[#{value.map { |v| format_value_for_diff(v) }.join(', ')}]"
|
|
288
|
+
else
|
|
289
|
+
# Complex array - show summary
|
|
290
|
+
"{Array with #{value.length} elements}"
|
|
291
|
+
end
|
|
292
|
+
when Hash
|
|
293
|
+
if value.empty?
|
|
294
|
+
"{}"
|
|
295
|
+
else
|
|
296
|
+
"{Hash with #{value.keys.length} keys: #{value.keys.take(3).map(&:to_s).join(', ')}#{value.keys.length > 3 ? '...' : ''}}"
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
value.inspect
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
class DiffFormatter
|
|
7
|
+
module ByObject
|
|
8
|
+
# XML/HTML tree formatter for by-object diffs
|
|
9
|
+
# Handles DOM node differences with proper path extraction
|
|
10
|
+
class XmlFormatter < BaseFormatter
|
|
11
|
+
# Render a diff node for XML/HTML DOM differences
|
|
12
|
+
#
|
|
13
|
+
# @param key [String] Node name or path segment
|
|
14
|
+
# @param diff [Hash] Difference information
|
|
15
|
+
# @param prefix [String] Tree prefix for indentation
|
|
16
|
+
# @param connector [String] Box-drawing connector character
|
|
17
|
+
# @return [String] Formatted diff node
|
|
18
|
+
def render_diff_node(key, diff, prefix, connector)
|
|
19
|
+
output = []
|
|
20
|
+
|
|
21
|
+
# Show full path if available (path in cyan, no color on tree structure)
|
|
22
|
+
path_display = if diff.is_a?(Hash) && diff[:path] && !diff[:path].empty?
|
|
23
|
+
colorize(diff[:path].to_s, :cyan, :bold)
|
|
24
|
+
else
|
|
25
|
+
colorize(key.to_s, :cyan)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
output << "#{prefix}#{connector}#{path_display}:"
|
|
29
|
+
|
|
30
|
+
# Determine continuation for nested values
|
|
31
|
+
continuation = connector.start_with?("├") ? "│ " : " "
|
|
32
|
+
value_prefix = prefix + continuation
|
|
33
|
+
|
|
34
|
+
# Handle both DiffNode and Hash formats
|
|
35
|
+
if diff.is_a?(Canon::Diff::DiffNode)
|
|
36
|
+
# DiffNode format - render based on dimension
|
|
37
|
+
render_diffnode(diff, value_prefix, output)
|
|
38
|
+
else
|
|
39
|
+
# Hash format - use diff codes
|
|
40
|
+
diff_code = diff[:diff_code] || diff[:diff1]
|
|
41
|
+
|
|
42
|
+
case diff_code
|
|
43
|
+
when Comparison::UNEQUAL_ELEMENTS
|
|
44
|
+
render_unequal_elements(diff, value_prefix, output)
|
|
45
|
+
when Comparison::UNEQUAL_TEXT_CONTENTS
|
|
46
|
+
render_unequal_text(diff, value_prefix, output)
|
|
47
|
+
when Comparison::UNEQUAL_ATTRIBUTES
|
|
48
|
+
render_unequal_attributes(diff, value_prefix, output)
|
|
49
|
+
when Comparison::MISSING_ATTRIBUTE
|
|
50
|
+
render_missing_attribute(diff, value_prefix, output)
|
|
51
|
+
when Comparison::UNEQUAL_COMMENTS
|
|
52
|
+
render_unequal_comments(diff, value_prefix, output)
|
|
53
|
+
when Comparison::MISSING_NODE
|
|
54
|
+
render_missing_node(diff, value_prefix, output)
|
|
55
|
+
else
|
|
56
|
+
# Fallback for unknown diff types
|
|
57
|
+
render_fallback(diff, value_prefix, output)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
output.join("\n")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Add a difference to the tree structure
|
|
65
|
+
# Handles DOM nodes by extracting their path
|
|
66
|
+
#
|
|
67
|
+
# @param tree [Hash] Tree structure to add to
|
|
68
|
+
# @param path [String, Array] Path to the difference
|
|
69
|
+
# @param diff [Hash, DiffNode] Difference information
|
|
70
|
+
def add_to_tree(tree, path, diff)
|
|
71
|
+
# For DOM differences (Hash format), extract path from node
|
|
72
|
+
if diff.is_a?(Hash) && !diff.key?(:path) && (diff[:node1] || diff[:node2])
|
|
73
|
+
path = extract_dom_path(diff)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
super(tree, path, diff)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Render DiffNode object
|
|
82
|
+
def render_diffnode(diff_node, prefix, output)
|
|
83
|
+
# Extract nodes for display
|
|
84
|
+
node1 = diff_node.node1
|
|
85
|
+
node2 = diff_node.node2
|
|
86
|
+
|
|
87
|
+
# Display based on dimension
|
|
88
|
+
case diff_node.dimension
|
|
89
|
+
when :text_content
|
|
90
|
+
if node1 && node2
|
|
91
|
+
text1 = extract_text(node1)
|
|
92
|
+
text2 = extract_text(node2)
|
|
93
|
+
output << "#{prefix}├── - #{colorize(format_text_inline(text1),
|
|
94
|
+
:red)}"
|
|
95
|
+
output << "#{prefix}└── + #{colorize(format_text_inline(text2),
|
|
96
|
+
:green)}"
|
|
97
|
+
else
|
|
98
|
+
output << "#{prefix}└── #{colorize(
|
|
99
|
+
"[#{diff_node.dimension}: #{diff_node.reason}]", :yellow
|
|
100
|
+
)}"
|
|
101
|
+
end
|
|
102
|
+
when :structural_whitespace, :attribute_whitespace, :attribute_values
|
|
103
|
+
output << "#{prefix}└── #{colorize(
|
|
104
|
+
"[#{diff_node.dimension}: #{diff_node.reason}]", :yellow
|
|
105
|
+
)}"
|
|
106
|
+
else
|
|
107
|
+
# Fallback
|
|
108
|
+
output << "#{prefix}└── #{colorize(
|
|
109
|
+
"[#{diff_node.dimension}: #{diff_node.reason}]", :cyan
|
|
110
|
+
)}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Render unequal elements
|
|
115
|
+
def render_unequal_elements(diff, prefix, output)
|
|
116
|
+
node1 = diff[:node1]
|
|
117
|
+
node2 = diff[:node2]
|
|
118
|
+
output << "#{prefix}├── - #{colorize("<#{node1.name}>", :red)}"
|
|
119
|
+
output << "#{prefix}└── + #{colorize("<#{node2.name}>", :green)}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Render unequal text contents
|
|
123
|
+
def render_unequal_text(diff, prefix, output)
|
|
124
|
+
node1 = diff[:node1]
|
|
125
|
+
node2 = diff[:node2]
|
|
126
|
+
|
|
127
|
+
text1 = extract_text(node1)
|
|
128
|
+
text2 = extract_text(node2)
|
|
129
|
+
|
|
130
|
+
# Show parent element if available
|
|
131
|
+
if node1.respond_to?(:parent) && node1.parent.respond_to?(:name)
|
|
132
|
+
output << "#{prefix} #{colorize(
|
|
133
|
+
"Element: <#{node1.parent.name}>", :blue
|
|
134
|
+
)}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
output << "#{prefix}├── - #{colorize(format_text_inline(text1),
|
|
138
|
+
:red)}"
|
|
139
|
+
output << "#{prefix}└── + #{colorize(format_text_inline(text2),
|
|
140
|
+
:green)}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Render unequal attributes
|
|
144
|
+
def render_unequal_attributes(diff, prefix, output)
|
|
145
|
+
node1 = diff[:node1]
|
|
146
|
+
output << "#{prefix}└── #{colorize(
|
|
147
|
+
"Element: <#{node1.name}> [attributes differ]", :yellow
|
|
148
|
+
)}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Render missing attribute
|
|
152
|
+
def render_missing_attribute(diff, prefix, output)
|
|
153
|
+
node1 = diff[:node1]
|
|
154
|
+
output << "#{prefix}└── #{colorize(
|
|
155
|
+
"Element: <#{node1.name}> [attribute mismatch]", :yellow
|
|
156
|
+
)}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Render unequal comments
|
|
160
|
+
def render_unequal_comments(diff, prefix, output)
|
|
161
|
+
node1 = diff[:node1]
|
|
162
|
+
node2 = diff[:node2]
|
|
163
|
+
|
|
164
|
+
content1 = extract_text(node1)
|
|
165
|
+
content2 = extract_text(node2)
|
|
166
|
+
|
|
167
|
+
output << "#{prefix}├── - #{colorize(
|
|
168
|
+
"<!-- #{format_text_inline(content1)} -->", :red
|
|
169
|
+
)}"
|
|
170
|
+
output << "#{prefix}└── + #{colorize(
|
|
171
|
+
"<!-- #{format_text_inline(content2)} -->", :green
|
|
172
|
+
)}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Render missing node
|
|
176
|
+
def render_missing_node(diff, prefix, output)
|
|
177
|
+
output << if diff[:node1] && !diff[:node2]
|
|
178
|
+
"#{prefix}└── - #{colorize('[node deleted]', :red)}"
|
|
179
|
+
elsif diff[:node2] && !diff[:node1]
|
|
180
|
+
"#{prefix}└── + #{colorize('[node inserted]', :green)}"
|
|
181
|
+
else
|
|
182
|
+
"#{prefix}└── #{colorize('[node mismatch]', :yellow)}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Render fallback for unknown diff types
|
|
187
|
+
def render_fallback(diff, prefix, output)
|
|
188
|
+
if diff[:node1] && diff[:node2]
|
|
189
|
+
output << "#{prefix}├── - #{colorize('[file1 node]', :red)}"
|
|
190
|
+
output << "#{prefix}└── + #{colorize('[file2 node]', :green)}"
|
|
191
|
+
elsif diff[:node1]
|
|
192
|
+
output << "#{prefix}└── - #{colorize('[file1 only]', :red)}"
|
|
193
|
+
elsif diff[:node2]
|
|
194
|
+
output << "#{prefix}└── + #{colorize('[file2 only]', :green)}"
|
|
195
|
+
else
|
|
196
|
+
output << "#{prefix}└── #{colorize('[unknown change]', :yellow)}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Extract DOM path from a difference
|
|
201
|
+
#
|
|
202
|
+
# @param diff [Hash] Difference with node1 or node2
|
|
203
|
+
# @return [String] Path string
|
|
204
|
+
def extract_dom_path(diff)
|
|
205
|
+
node = diff[:node1] || diff[:node2]
|
|
206
|
+
return "" unless node
|
|
207
|
+
|
|
208
|
+
parts = []
|
|
209
|
+
current = node
|
|
210
|
+
|
|
211
|
+
while current.respond_to?(:name)
|
|
212
|
+
parts.unshift(current.name) if current.name
|
|
213
|
+
current = current.parent if current.respond_to?(:parent)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
parts.join(".")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Extract text from a node
|
|
220
|
+
#
|
|
221
|
+
# @param node [Object] Node with content or text
|
|
222
|
+
# @return [String] Text content
|
|
223
|
+
def extract_text(node)
|
|
224
|
+
if node.respond_to?(:content)
|
|
225
|
+
node.content.to_s
|
|
226
|
+
elsif node.respond_to?(:text)
|
|
227
|
+
node.text.to_s
|
|
228
|
+
else
|
|
229
|
+
""
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Format text for inline display (truncate if too long)
|
|
234
|
+
#
|
|
235
|
+
# @param text [String] Text to format
|
|
236
|
+
# @return [String] Formatted text
|
|
237
|
+
def format_text_inline(text)
|
|
238
|
+
# Truncate long text
|
|
239
|
+
if text.length > 60
|
|
240
|
+
"\"#{text[0..57]}...\""
|
|
241
|
+
else
|
|
242
|
+
"\"#{text}\""
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "json_formatter"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
class DiffFormatter
|
|
7
|
+
module ByObject
|
|
8
|
+
# YAML tree formatter for by-object diffs
|
|
9
|
+
# Inherits from JsonFormatter since YAML and JSON share the same
|
|
10
|
+
# Ruby object structure (hashes and arrays)
|
|
11
|
+
class YamlFormatter < JsonFormatter
|
|
12
|
+
# YAML uses the same rendering logic as JSON since both formats
|
|
13
|
+
# represent Ruby objects (hashes and arrays) with the same structure
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|