canon 0.1.22 → 0.2.0
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_todo.yml +174 -25
- data/docs/INDEX.adoc +4 -0
- data/docs/advanced/diff-classification.adoc +3 -2
- data/docs/features/configuration-profiles.adoc +288 -0
- data/docs/features/diff-formatting/character-visualization.adoc +153 -454
- data/docs/features/diff-formatting/display-filtering.adoc +44 -0
- data/docs/features/diff-formatting/display-preprocessing.adoc +656 -0
- data/docs/features/diff-formatting/index.adoc +47 -0
- data/docs/features/diff-formatting/pretty-diff-mode.adoc +154 -0
- data/docs/features/environment-configuration/override-system.adoc +10 -3
- data/docs/features/index.adoc +9 -0
- data/docs/features/match-options/index.adoc +32 -42
- data/docs/features/match-options/pretty-printed-fixtures.adoc +270 -0
- data/docs/guides/choosing-configuration.adoc +22 -0
- data/docs/reference/environment-variables.adoc +121 -1
- data/docs/reference/options-across-interfaces.adoc +182 -2
- data/lib/canon/cli.rb +20 -0
- data/lib/canon/commands/diff_command.rb +7 -2
- data/lib/canon/commands/format_command.rb +1 -1
- data/lib/canon/comparison/html_comparator.rb +20 -15
- data/lib/canon/comparison/html_compare_profile.rb +4 -4
- data/lib/canon/comparison/markup_comparator.rb +12 -3
- data/lib/canon/comparison/match_options/base_resolver.rb +29 -7
- data/lib/canon/comparison/match_options/json_resolver.rb +9 -0
- data/lib/canon/comparison/match_options/xml_resolver.rb +16 -2
- data/lib/canon/comparison/match_options/yaml_resolver.rb +10 -0
- data/lib/canon/comparison/match_options.rb +4 -1
- data/lib/canon/comparison/whitespace_sensitivity.rb +189 -137
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +21 -4
- data/lib/canon/comparison/xml_comparator.rb +14 -12
- data/lib/canon/comparison/xml_node_comparison.rb +51 -6
- data/lib/canon/comparison.rb +52 -9
- data/lib/canon/config/env_schema.rb +32 -4
- data/lib/canon/config/override_resolver.rb +16 -3
- data/lib/canon/config/profile_loader.rb +135 -0
- data/lib/canon/config/profiles/metanorma.yml +74 -0
- data/lib/canon/config/profiles/metanorma_debug.yml +8 -0
- data/lib/canon/config/type_converter.rb +8 -0
- data/lib/canon/config.rb +469 -5
- data/lib/canon/diff/diff_classifier.rb +41 -11
- data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +48 -17
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +58 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +22 -7
- data/lib/canon/diff_formatter/theme.rb +24 -17
- data/lib/canon/diff_formatter.rb +493 -36
- data/lib/canon/pretty_printer/xml_normalized.rb +395 -0
- data/lib/canon/rspec_matchers.rb +36 -0
- data/lib/canon/tree_diff/matchers/hash_matcher.rb +26 -11
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/nodes/namespace_node.rb +4 -0
- data/lib/canon/xml/nodes/processing_instruction_node.rb +4 -0
- data/lib/canon/xml/nodes/root_node.rb +4 -0
- data/lib/canon/xml/nodes/text_node.rb +4 -0
- data/lib/tasks/performance_helpers.rb +2 -2
- metadata +24 -2
|
@@ -11,8 +11,10 @@ module Canon
|
|
|
11
11
|
#
|
|
12
12
|
# @param diff [DiffNode, Hash] Difference node
|
|
13
13
|
# @param use_color [Boolean] Whether to use colors
|
|
14
|
+
# @param compact [Boolean] Whether to serialize element nodes as compact XML
|
|
14
15
|
# @return [String] Formatted dimension details
|
|
15
|
-
def self.format_dimension_details(diff, use_color
|
|
16
|
+
def self.format_dimension_details(diff, use_color, compact: false,
|
|
17
|
+
expand_difference: false)
|
|
16
18
|
dimension = extract_dimension(diff)
|
|
17
19
|
|
|
18
20
|
case dimension
|
|
@@ -21,7 +23,8 @@ module Canon
|
|
|
21
23
|
when :namespace_declarations
|
|
22
24
|
format_namespace_declarations_details(diff, use_color)
|
|
23
25
|
when :element_structure
|
|
24
|
-
format_element_structure_details(diff, use_color
|
|
26
|
+
format_element_structure_details(diff, use_color,
|
|
27
|
+
expand_difference: expand_difference)
|
|
25
28
|
when :attribute_presence
|
|
26
29
|
format_attribute_presence_details(diff, use_color)
|
|
27
30
|
when :attribute_values
|
|
@@ -29,7 +32,7 @@ module Canon
|
|
|
29
32
|
when :attribute_order
|
|
30
33
|
format_attribute_order_details(diff, use_color)
|
|
31
34
|
when :text_content
|
|
32
|
-
format_text_content_details(diff, use_color)
|
|
35
|
+
format_text_content_details(diff, use_color, compact: compact)
|
|
33
36
|
when :structural_whitespace
|
|
34
37
|
format_structural_whitespace_details(diff, use_color)
|
|
35
38
|
when :comments
|
|
@@ -37,7 +40,7 @@ module Canon
|
|
|
37
40
|
when :hash_diff
|
|
38
41
|
format_hash_diff_details(diff, use_color)
|
|
39
42
|
else
|
|
40
|
-
format_fallback_details(diff, use_color)
|
|
43
|
+
format_fallback_details(diff, use_color, compact: compact)
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -163,7 +166,8 @@ module Canon
|
|
|
163
166
|
# @param diff [DiffNode, Hash] Difference node
|
|
164
167
|
# @param use_color [Boolean] Whether to use colors
|
|
165
168
|
# @return [Array] Tuple of [detail1, detail2, changes]
|
|
166
|
-
def self.format_element_structure_details(diff, use_color
|
|
169
|
+
def self.format_element_structure_details(diff, use_color,
|
|
170
|
+
expand_difference: false)
|
|
167
171
|
require_relative "color_helper"
|
|
168
172
|
require_relative "node_utils"
|
|
169
173
|
|
|
@@ -173,8 +177,15 @@ module Canon
|
|
|
173
177
|
name1 = NodeUtils.get_element_name_for_display(node1)
|
|
174
178
|
name2 = NodeUtils.get_element_name_for_display(node2)
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
if expand_difference
|
|
181
|
+
display1 = NodeUtils.serialize_node_compact(node1)
|
|
182
|
+
display2 = NodeUtils.serialize_node_compact(node2)
|
|
183
|
+
detail1 = ColorHelper.colorize(display1, :red, use_color)
|
|
184
|
+
detail2 = ColorHelper.colorize(display2, :green, use_color)
|
|
185
|
+
else
|
|
186
|
+
detail1 = "<#{ColorHelper.colorize(name1, :red, use_color)}>"
|
|
187
|
+
detail2 = "<#{ColorHelper.colorize(name2, :green, use_color)}>"
|
|
188
|
+
end
|
|
178
189
|
|
|
179
190
|
changes = "Element differs: #{ColorHelper.colorize(name1, :red,
|
|
180
191
|
use_color)} → " \
|
|
@@ -311,8 +322,9 @@ module Canon
|
|
|
311
322
|
#
|
|
312
323
|
# @param diff [DiffNode, Hash] Difference node
|
|
313
324
|
# @param use_color [Boolean] Whether to use colors
|
|
325
|
+
# @param compact [Boolean] Whether to serialize element nodes as compact XML
|
|
314
326
|
# @return [Array] Tuple of [detail1, detail2, changes]
|
|
315
|
-
def self.format_text_content_details(diff, use_color)
|
|
327
|
+
def self.format_text_content_details(diff, use_color, compact: false)
|
|
316
328
|
require_relative "color_helper"
|
|
317
329
|
require_relative "node_utils"
|
|
318
330
|
require_relative "text_utils"
|
|
@@ -320,15 +332,15 @@ module Canon
|
|
|
320
332
|
node1 = extract_node1(diff)
|
|
321
333
|
node2 = extract_node2(diff)
|
|
322
334
|
|
|
323
|
-
text1 = NodeUtils.
|
|
324
|
-
text2 = NodeUtils.
|
|
335
|
+
text1 = NodeUtils.node_to_display(node1, compact: compact)
|
|
336
|
+
text2 = NodeUtils.node_to_display(node2, compact: compact)
|
|
325
337
|
|
|
326
338
|
# Handle cases where one node is missing (e.g. text added or removed)
|
|
327
339
|
if node1.nil? || node2.nil?
|
|
328
340
|
if node1.nil?
|
|
329
|
-
text2 = NodeUtils.
|
|
341
|
+
text2 = NodeUtils.node_to_display(node2, compact: compact)
|
|
330
342
|
else
|
|
331
|
-
text1 = NodeUtils.
|
|
343
|
+
text1 = NodeUtils.node_to_display(node1, compact: compact)
|
|
332
344
|
end
|
|
333
345
|
end
|
|
334
346
|
|
|
@@ -339,6 +351,13 @@ module Canon
|
|
|
339
351
|
detail2 = ColorHelper.colorize(
|
|
340
352
|
TextUtils.visualize_whitespace(text2), :green, use_color
|
|
341
353
|
)
|
|
354
|
+
elsif compact && (node1.is_a?(Canon::Xml::Nodes::ElementNode) ||
|
|
355
|
+
node2.is_a?(Canon::Xml::Nodes::ElementNode))
|
|
356
|
+
# In compact mode with element nodes, display as raw XML without
|
|
357
|
+
# JSON-string quoting. In normal mode (or for plain text), apply
|
|
358
|
+
# the standard escaping/quoting logic.
|
|
359
|
+
detail1 = ColorHelper.colorize(text1, :red, use_color)
|
|
360
|
+
detail2 = ColorHelper.colorize(text2, :green, use_color)
|
|
342
361
|
else
|
|
343
362
|
# Escape non-ASCII characters for better terminal display
|
|
344
363
|
# JSON.generate doesn't escape chars like NBSP (U+00A0) or em-dash (U+2014)
|
|
@@ -453,18 +472,30 @@ module Canon
|
|
|
453
472
|
#
|
|
454
473
|
# @param diff [DiffNode, Hash] Difference node
|
|
455
474
|
# @param use_color [Boolean] Whether to use colors
|
|
475
|
+
# @param compact [Boolean] Whether to serialize element nodes as compact XML
|
|
456
476
|
# @return [Array] Tuple of [detail1, detail2, changes]
|
|
457
|
-
def self.format_fallback_details(diff, use_color)
|
|
477
|
+
def self.format_fallback_details(diff, use_color, compact: false)
|
|
458
478
|
require_relative "color_helper"
|
|
459
479
|
require_relative "node_utils"
|
|
460
480
|
|
|
461
481
|
node1 = extract_node1(diff)
|
|
462
482
|
node2 = extract_node2(diff)
|
|
463
483
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
484
|
+
raw1 = if compact
|
|
485
|
+
NodeUtils.node_to_display(node1,
|
|
486
|
+
compact: true)
|
|
487
|
+
else
|
|
488
|
+
NodeUtils.format_node_brief(node1)
|
|
489
|
+
end
|
|
490
|
+
raw2 = if compact
|
|
491
|
+
NodeUtils.node_to_display(node2,
|
|
492
|
+
compact: true)
|
|
493
|
+
else
|
|
494
|
+
NodeUtils.format_node_brief(node2)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
detail1 = ColorHelper.colorize(raw1, :red, use_color)
|
|
498
|
+
detail2 = ColorHelper.colorize(raw2, :green, use_color)
|
|
468
499
|
|
|
469
500
|
dimension = extract_dimension(diff)
|
|
470
501
|
changes = "#{dimension.to_s.capitalize} differs: #{detail1} → #{detail2}"
|
|
@@ -260,6 +260,64 @@ module Canon
|
|
|
260
260
|
end
|
|
261
261
|
end
|
|
262
262
|
|
|
263
|
+
# Serialize a Canon Xml node tree as compact XML for display.
|
|
264
|
+
#
|
|
265
|
+
# Produces a human-readable inline XML string without namespace
|
|
266
|
+
# declarations and without indentation — suitable for use in Semantic
|
|
267
|
+
# Diff Report entries. Only handles Canon::Xml::Nodes types; for any
|
|
268
|
+
# other node (Nokogiri, etc.) falls back to +get_node_text+.
|
|
269
|
+
#
|
|
270
|
+
# @param node [Object] Node to serialize
|
|
271
|
+
# @return [String] Compact XML string
|
|
272
|
+
def self.serialize_node_compact(node)
|
|
273
|
+
require "cgi"
|
|
274
|
+
return "" unless node
|
|
275
|
+
|
|
276
|
+
case node
|
|
277
|
+
when Canon::Xml::Nodes::TextNode
|
|
278
|
+
CGI.escapeHTML(node.value.to_s)
|
|
279
|
+
when Canon::Xml::Nodes::ElementNode
|
|
280
|
+
tag = node.name.to_s
|
|
281
|
+
attrs = node.attribute_nodes.map do |attr|
|
|
282
|
+
attr_name = attr.respond_to?(:name) ? attr.name.to_s : attr.to_s
|
|
283
|
+
attr_value = attr.respond_to?(:value) ? attr.value.to_s : ""
|
|
284
|
+
" #{attr_name}=\"#{CGI.escapeHTML(attr_value)}\""
|
|
285
|
+
end.join
|
|
286
|
+
children_xml = node.children.map do |c|
|
|
287
|
+
serialize_node_compact(c)
|
|
288
|
+
end.join
|
|
289
|
+
if children_xml.empty?
|
|
290
|
+
"<#{tag}#{attrs}/>"
|
|
291
|
+
else
|
|
292
|
+
"<#{tag}#{attrs}>#{children_xml}</#{tag}>"
|
|
293
|
+
end
|
|
294
|
+
when Canon::Xml::Nodes::CommentNode
|
|
295
|
+
text = node.respond_to?(:value) ? node.value.to_s : ""
|
|
296
|
+
"<!--#{CGI.escapeHTML(text)}-->"
|
|
297
|
+
else
|
|
298
|
+
# Nokogiri nodes or other unknown types — fall back to text extraction
|
|
299
|
+
get_node_text(node)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Return the best display string for a node.
|
|
304
|
+
#
|
|
305
|
+
# When +compact: true+ and the node is a Canon ElementNode, returns a
|
|
306
|
+
# compact XML serialization (e.g. +<strong>Annex</strong>+) instead of
|
|
307
|
+
# the +node_info+ description string that +get_node_text+ would produce.
|
|
308
|
+
# In all other cases, delegates to +get_node_text+.
|
|
309
|
+
#
|
|
310
|
+
# @param node [Object] Node to display
|
|
311
|
+
# @param compact [Boolean] Whether to use compact XML for element nodes
|
|
312
|
+
# @return [String] Display string
|
|
313
|
+
def self.node_to_display(node, compact: false)
|
|
314
|
+
if compact && node.is_a?(Canon::Xml::Nodes::ElementNode)
|
|
315
|
+
serialize_node_compact(node)
|
|
316
|
+
else
|
|
317
|
+
get_node_text(node)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
263
321
|
# Check if node is inside a preserve-whitespace element
|
|
264
322
|
#
|
|
265
323
|
# @param node [Object] Node to check
|
|
@@ -19,8 +19,14 @@ module Canon
|
|
|
19
19
|
#
|
|
20
20
|
# @param differences [Array<DiffNode>] Array of differences
|
|
21
21
|
# @param use_color [Boolean] Whether to use colors
|
|
22
|
+
# @param show_diffs [Symbol] Filter: :all (default), :normative, :informative
|
|
23
|
+
# @param compact_semantic_report [Boolean] When true, serialize element nodes
|
|
24
|
+
# as compact XML (e.g. <strong>Annex</strong>) instead of the verbose
|
|
25
|
+
# node_info description (e.g. "name: strong namespace_uri: …")
|
|
22
26
|
# @return [String] Formatted semantic diff report
|
|
23
|
-
def format_report(differences, use_color: true
|
|
27
|
+
def format_report(differences, use_color: true, show_diffs: :all,
|
|
28
|
+
compact_semantic_report: false,
|
|
29
|
+
expand_difference: false)
|
|
24
30
|
return "" if differences.empty?
|
|
25
31
|
|
|
26
32
|
# Group differences by normative status
|
|
@@ -31,6 +37,10 @@ module Canon
|
|
|
31
37
|
diff.respond_to?(:normative?) && !diff.normative?
|
|
32
38
|
end
|
|
33
39
|
|
|
40
|
+
# Apply show_diffs filter — same semantics as the line-diff filter
|
|
41
|
+
show_normative = show_diffs != :informative
|
|
42
|
+
show_informative = show_diffs != :normative
|
|
43
|
+
|
|
34
44
|
output = []
|
|
35
45
|
output << ""
|
|
36
46
|
output << colorize("=" * 70, :cyan, use_color, bold: true)
|
|
@@ -40,7 +50,7 @@ module Canon
|
|
|
40
50
|
output << colorize("=" * 70, :cyan, use_color, bold: true)
|
|
41
51
|
|
|
42
52
|
# Show normative differences first
|
|
43
|
-
if normative.any?
|
|
53
|
+
if normative.any? && show_normative
|
|
44
54
|
output << ""
|
|
45
55
|
output << colorize(
|
|
46
56
|
"┌─ NORMATIVE DIFFERENCES (#{normative.length}) ─┐", :green, use_color, bold: true
|
|
@@ -49,12 +59,14 @@ module Canon
|
|
|
49
59
|
normative.each_with_index do |diff, i|
|
|
50
60
|
output << ""
|
|
51
61
|
output << format_single_diff(diff, i + 1, normative.length,
|
|
52
|
-
use_color, section: "NORMATIVE"
|
|
62
|
+
use_color, section: "NORMATIVE",
|
|
63
|
+
compact: compact_semantic_report,
|
|
64
|
+
expand_difference: expand_difference)
|
|
53
65
|
end
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
# Show informative differences second
|
|
57
|
-
if informative.any?
|
|
69
|
+
if informative.any? && show_informative
|
|
58
70
|
output << ""
|
|
59
71
|
output << ""
|
|
60
72
|
output << colorize(
|
|
@@ -64,7 +76,9 @@ module Canon
|
|
|
64
76
|
informative.each_with_index do |diff, i|
|
|
65
77
|
output << ""
|
|
66
78
|
output << format_single_diff(diff, i + 1, informative.length,
|
|
67
|
-
use_color, section: "INFORMATIVE"
|
|
79
|
+
use_color, section: "INFORMATIVE",
|
|
80
|
+
compact: compact_semantic_report,
|
|
81
|
+
expand_difference: expand_difference)
|
|
68
82
|
end
|
|
69
83
|
end
|
|
70
84
|
|
|
@@ -78,7 +92,8 @@ module Canon
|
|
|
78
92
|
private
|
|
79
93
|
|
|
80
94
|
# Format a single difference with dimension-specific details
|
|
81
|
-
def format_single_diff(diff, number, total, use_color, section: nil
|
|
95
|
+
def format_single_diff(diff, number, total, use_color, section: nil,
|
|
96
|
+
compact: false, expand_difference: false)
|
|
82
97
|
output = []
|
|
83
98
|
|
|
84
99
|
# Header - handle both DiffNode and Hash
|
|
@@ -120,7 +135,7 @@ module Canon
|
|
|
120
135
|
|
|
121
136
|
# Dimension-specific details
|
|
122
137
|
detail1, detail2, changes = DiffDetailFormatterHelpers::DimensionFormatter.format_dimension_details(
|
|
123
|
-
diff, use_color
|
|
138
|
+
diff, use_color, compact: compact, expand_difference: expand_difference
|
|
124
139
|
)
|
|
125
140
|
|
|
126
141
|
output << colorize("⊖ Expected (File 1):", :red, use_color,
|
|
@@ -497,22 +497,9 @@ module Canon
|
|
|
497
497
|
|
|
498
498
|
private
|
|
499
499
|
|
|
500
|
-
#
|
|
500
|
+
# Delegate to module-level deep_dup
|
|
501
501
|
def deep_dup(obj)
|
|
502
|
-
|
|
503
|
-
when Hash
|
|
504
|
-
obj.transform_values { |v| deep_dup(v) }
|
|
505
|
-
when Array
|
|
506
|
-
obj.map { |v| deep_dup(v) }
|
|
507
|
-
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
|
508
|
-
obj
|
|
509
|
-
else
|
|
510
|
-
begin
|
|
511
|
-
obj.dup
|
|
512
|
-
rescue StandardError
|
|
513
|
-
obj
|
|
514
|
-
end
|
|
515
|
-
end
|
|
502
|
+
Theme.deep_dup(obj)
|
|
516
503
|
end
|
|
517
504
|
|
|
518
505
|
def deep_merge!(target, source)
|
|
@@ -606,9 +593,29 @@ module Canon
|
|
|
606
593
|
# @param name [Symbol] Theme name
|
|
607
594
|
# @return [Hash] Theme hash
|
|
608
595
|
# @raise [ArgumentError] if theme not found
|
|
596
|
+
# Deep copy a value, handling nested hashes and arrays
|
|
597
|
+
def self.deep_dup(obj)
|
|
598
|
+
case obj
|
|
599
|
+
when Hash
|
|
600
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
601
|
+
when Array
|
|
602
|
+
obj.map { |v| deep_dup(v) }
|
|
603
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
|
604
|
+
obj
|
|
605
|
+
else
|
|
606
|
+
begin
|
|
607
|
+
obj.dup
|
|
608
|
+
rescue StandardError
|
|
609
|
+
obj
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
609
614
|
def self.[](name)
|
|
610
|
-
THEMES[name] || raise(ArgumentError,
|
|
611
|
-
|
|
615
|
+
theme = THEMES[name] || raise(ArgumentError,
|
|
616
|
+
"Unknown theme: #{name}. Valid: #{THEMES.keys}")
|
|
617
|
+
# Return a deep copy to prevent mutation of theme constants
|
|
618
|
+
deep_dup(theme)
|
|
612
619
|
end
|
|
613
620
|
|
|
614
621
|
# List available theme names
|