canon 0.2.8 → 0.2.11
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/.rspec-opal +7 -0
- data/.rubocop_todo.yml +25 -73
- data/Rakefile +37 -0
- data/lib/canon/cache.rb +16 -27
- data/lib/canon/cli.rb +1 -1
- data/lib/canon/color_detector.rb +3 -5
- data/lib/canon/comparison/compare_profile.rb +1 -4
- data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/comments_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/element_position_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +2 -6
- data/lib/canon/comparison/dimensions/text_content_dimension.rb +3 -5
- data/lib/canon/comparison/format_detector.rb +29 -20
- data/lib/canon/comparison/html_comparator.rb +20 -29
- data/lib/canon/comparison/html_compare_profile.rb +3 -10
- data/lib/canon/comparison/html_parser.rb +1 -1
- data/lib/canon/comparison/json_comparator.rb +8 -0
- data/lib/canon/comparison/node_inspector.rb +117 -86
- data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +6 -8
- data/lib/canon/comparison/whitespace_sensitivity.rb +55 -193
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +19 -2
- data/lib/canon/comparison/xml_comparator/attribute_filter.rb +5 -10
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +4 -4
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +40 -8
- data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +14 -28
- data/lib/canon/comparison/xml_comparator/node_parser.rb +14 -13
- data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +30 -58
- data/lib/canon/comparison/xml_comparator.rb +63 -85
- data/lib/canon/comparison/xml_node_comparison.rb +15 -15
- data/lib/canon/comparison/yaml_comparator.rb +8 -0
- data/lib/canon/comparison.rb +24 -24
- data/lib/canon/config/profile_loader.rb +13 -13
- data/lib/canon/config.rb +29 -5
- data/lib/canon/diff/diff_classifier.rb +7 -41
- data/lib/canon/diff/diff_line.rb +1 -1
- data/lib/canon/diff/diff_line_builder.rb +2 -0
- data/lib/canon/diff/diff_node_enricher.rb +22 -24
- data/lib/canon/diff/diff_node_mapper.rb +10 -8
- data/lib/canon/diff/formatting_detector.rb +3 -2
- data/lib/canon/diff/node_serializer.rb +23 -30
- data/lib/canon/diff/path_builder.rb +24 -37
- data/lib/canon/diff/source_locator.rb +0 -3
- data/lib/canon/diff/xml_serialization_formatter.rb +8 -84
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +7 -7
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +1 -1
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +1 -1
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +2 -2
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +1 -1
- data/lib/canon/diff_formatter/by_line_formatter.rb +1 -1
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +23 -17
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +127 -11
- data/lib/canon/diff_formatter/by_object_formatter.rb +2 -6
- data/lib/canon/diff_formatter/debug_output.rb +12 -24
- data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +2 -2
- data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +3 -3
- data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +26 -27
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +146 -318
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +28 -20
- data/lib/canon/diff_formatter/legend.rb +2 -2
- data/lib/canon/diff_formatter/pretty_diff_formatter.rb +2 -2
- data/lib/canon/diff_formatter/theme.rb +4 -4
- data/lib/canon/diff_formatter.rb +17 -13
- data/lib/canon/formatters/html_formatter.rb +1 -1
- data/lib/canon/formatters/html_formatter_base.rb +1 -1
- data/lib/canon/formatters/xml_formatter.rb +7 -32
- data/lib/canon/html/data_model.rb +2 -2
- data/lib/canon/pretty_printer/html.rb +1 -1
- data/lib/canon/pretty_printer/xml.rb +16 -7
- data/lib/canon/pretty_printer/xml_normalized.rb +9 -3
- data/lib/canon/rspec_matchers.rb +2 -2
- data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
- data/lib/canon/tree_diff/adapters/xml_adapter.rb +1 -1
- data/lib/canon/tree_diff/core/tree_node.rb +1 -3
- data/lib/canon/tree_diff/operation_converter.rb +7 -7
- data/lib/canon/tree_diff/operations/operation_detector.rb +4 -0
- data/lib/canon/validators/base_validator.rb +5 -8
- data/lib/canon/validators/html_validator.rb +3 -8
- data/lib/canon/validators/xml_validator.rb +3 -8
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +132 -138
- data/lib/canon/xml/namespace_helper.rb +5 -0
- data/lib/canon/xml/node.rb +2 -1
- data/lib/canon/xml/nodes/root_node.rb +4 -0
- data/lib/canon/xml/nodes/text_node.rb +6 -1
- data/lib/canon/xml/sax_builder.rb +5 -7
- data/lib/canon/xml/whitespace_normalizer.rb +2 -2
- data/lib/canon/xml_backend.rb +49 -0
- data/lib/canon/xml_parsing.rb +283 -0
- data/lib/canon.rb +3 -1
- data/lib/tasks/benchmark_runner.rb +1 -1
- data/lib/tasks/performance_helpers.rb +1 -1
- metadata +9 -6
|
@@ -54,10 +54,10 @@ module Canon
|
|
|
54
54
|
while current && depth < max_depth
|
|
55
55
|
segments.unshift(segment_for_node(current))
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
break unless
|
|
57
|
+
parent = node_parent(current)
|
|
58
|
+
break unless parent
|
|
59
59
|
|
|
60
|
-
current =
|
|
60
|
+
current = parent
|
|
61
61
|
depth += 1
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -71,27 +71,16 @@ module Canon
|
|
|
71
71
|
# @param tree_node [Object] Node (TreeNode, Canon::Xml::Node, or Nokogiri)
|
|
72
72
|
# @return [String] Path segment with ordinal index
|
|
73
73
|
def self.segment_for_node(tree_node)
|
|
74
|
-
|
|
75
|
-
label = if tree_node.respond_to?(:label)
|
|
76
|
-
tree_node.label
|
|
77
|
-
elsif tree_node.respond_to?(:name)
|
|
78
|
-
tree_node.name
|
|
79
|
-
else
|
|
80
|
-
"unknown"
|
|
81
|
-
end
|
|
74
|
+
label = node_label(tree_node)
|
|
82
75
|
|
|
83
76
|
# Get ordinal index (position among siblings with same label)
|
|
84
77
|
index = ordinal_index(tree_node)
|
|
85
78
|
|
|
86
79
|
# For text nodes, use parent element name for clarity
|
|
87
80
|
# e.g., instead of "/p/#text[0]" use "/p/text()[0]"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
parent_name =
|
|
91
|
-
tree_node.parent.label
|
|
92
|
-
elsif tree_node.parent.respond_to?(:name)
|
|
93
|
-
tree_node.parent.name
|
|
94
|
-
end
|
|
81
|
+
parent = node_parent(tree_node)
|
|
82
|
+
if ["text", "#text"].include?(label) && parent
|
|
83
|
+
parent_name = node_label(parent)
|
|
95
84
|
if parent_name && parent_name != "#document" && parent_name != "#document-fragment"
|
|
96
85
|
return "#{parent_name}/text()[#{index}]"
|
|
97
86
|
end
|
|
@@ -106,35 +95,21 @@ module Canon
|
|
|
106
95
|
# @param tree_node [Object] Node (TreeNode, Canon::Xml::Node, or Nokogiri)
|
|
107
96
|
# @return [Integer] Zero-based ordinal index
|
|
108
97
|
def self.ordinal_index(tree_node)
|
|
109
|
-
|
|
110
|
-
return 0 unless
|
|
111
|
-
return 0 unless tree_node.parent
|
|
98
|
+
parent = node_parent(tree_node)
|
|
99
|
+
return 0 unless parent
|
|
112
100
|
|
|
113
|
-
|
|
114
|
-
return 0 unless tree_node.parent.respond_to?(:children)
|
|
115
|
-
|
|
116
|
-
siblings = tree_node.parent.children
|
|
101
|
+
siblings = node_children(parent)
|
|
117
102
|
return 0 unless siblings
|
|
118
103
|
|
|
119
104
|
# Convert to array if it's a NodeSet (Nokogiri) or similar
|
|
120
105
|
siblings = siblings.to_a unless siblings.is_a?(Array)
|
|
121
106
|
|
|
122
|
-
|
|
123
|
-
my_label = if tree_node.respond_to?(:label)
|
|
124
|
-
tree_node.label
|
|
125
|
-
elsif tree_node.respond_to?(:name)
|
|
126
|
-
tree_node.name
|
|
127
|
-
end
|
|
128
|
-
|
|
107
|
+
my_label = node_label(tree_node)
|
|
129
108
|
return 0 unless my_label
|
|
130
109
|
|
|
131
110
|
# Count siblings with same label that appear before this node
|
|
132
111
|
same_label_siblings = siblings.select do |s|
|
|
133
|
-
sibling_label =
|
|
134
|
-
s.label
|
|
135
|
-
elsif s.respond_to?(:name)
|
|
136
|
-
s.name
|
|
137
|
-
end
|
|
112
|
+
sibling_label = node_label(s)
|
|
138
113
|
sibling_label == my_label
|
|
139
114
|
end
|
|
140
115
|
|
|
@@ -152,6 +127,18 @@ module Canon
|
|
|
152
127
|
segments = build_segments(tree_node)
|
|
153
128
|
segments.join(" → ")
|
|
154
129
|
end
|
|
130
|
+
|
|
131
|
+
def self.node_label(node)
|
|
132
|
+
Canon::Comparison::NodeInspector.name(node) || "unknown"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.node_parent(node)
|
|
136
|
+
Canon::Comparison::NodeInspector.parent(node)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.node_children(node)
|
|
140
|
+
Canon::Comparison::NodeInspector.children(node)
|
|
141
|
+
end
|
|
155
142
|
end
|
|
156
143
|
end
|
|
157
144
|
end
|
|
@@ -86,15 +86,12 @@ module Canon
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
class << self
|
|
89
|
-
private
|
|
90
|
-
|
|
91
89
|
# Binary search for the line containing a character offset.
|
|
92
90
|
#
|
|
93
91
|
# @param char_offset [Integer] the character offset
|
|
94
92
|
# @param line_map [Array<Hash>] the line offset map
|
|
95
93
|
# @return [Integer, nil] the 0-based line index, or nil
|
|
96
94
|
def find_line_for_offset(char_offset, line_map)
|
|
97
|
-
# Use bsearch for efficiency on large files
|
|
98
95
|
line_map.bsearch_index do |entry|
|
|
99
96
|
entry[:end_offset] > char_offset
|
|
100
97
|
end
|
|
@@ -4,59 +4,25 @@ module Canon
|
|
|
4
4
|
module Diff
|
|
5
5
|
# Detects and classifies XML serialization-level formatting differences.
|
|
6
6
|
#
|
|
7
|
-
# Serialization
|
|
8
|
-
#
|
|
9
|
-
# arise from different valid ways to serialize the same semantic content.
|
|
10
|
-
#
|
|
11
|
-
# These differences are ALWAYS non-normative (formatting-only) regardless
|
|
12
|
-
# of match options, because they are purely syntactic variations.
|
|
13
|
-
#
|
|
14
|
-
# Examples:
|
|
15
|
-
# - Self-closing vs explicit closing tags: <tag/> vs <tag></tag>
|
|
16
|
-
# - Attribute quote style: attr="value" vs attr='value' (parser-normalized)
|
|
17
|
-
# - Whitespace within tags: <tag a="1" b="2"> vs <tag a="1" b="2"> (parser-normalized)
|
|
18
|
-
#
|
|
19
|
-
# Note: Some serialization differences are normalized away by XML parsers
|
|
20
|
-
# (attribute quotes, tag spacing). This class focuses on differences that
|
|
21
|
-
# survive parsing and comparison, such as self-closing vs explicit closing.
|
|
7
|
+
# Serialization formatting differences are ALWAYS non-normative (formatting-only)
|
|
8
|
+
# regardless of match options, because they are purely syntactic variations.
|
|
22
9
|
class XmlSerializationFormatter
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Serialization formatting differences are ALWAYS non-normative because they
|
|
26
|
-
# represent different valid serializations of the same semantic content.
|
|
27
|
-
#
|
|
28
|
-
# @param diff_node [DiffNode] The diff node to check
|
|
29
|
-
# @return [Boolean] true if this is a serialization formatting difference
|
|
10
|
+
NI = Canon::Comparison::NodeInspector
|
|
11
|
+
|
|
30
12
|
def self.serialization_formatting?(diff_node)
|
|
31
|
-
# Currently only handles text_content dimension
|
|
32
|
-
# Future: add detection for other dimensions
|
|
33
13
|
return false unless diff_node.dimension == :text_content
|
|
34
14
|
|
|
35
15
|
empty_text_content_serialization_diff?(diff_node)
|
|
36
16
|
end
|
|
37
17
|
|
|
38
|
-
# Check if a text_content difference is from XML serialization format.
|
|
39
|
-
#
|
|
40
|
-
# Specifically detects self-closing tags (<tag/>) vs explicit closing tags
|
|
41
|
-
# (<tag></tag>), which create different text node structures:
|
|
42
|
-
# - Self-closing: no text node (nil)
|
|
43
|
-
# - Explicit closing: empty or whitespace-only text node ("", " ", "\n", etc.)
|
|
44
|
-
#
|
|
45
|
-
# Per XML standards, these forms are semantically equivalent.
|
|
46
|
-
#
|
|
47
|
-
# @param diff_node [DiffNode] The diff node to check
|
|
48
|
-
# @return [Boolean] true if this is a serialization formatting difference
|
|
49
18
|
def self.empty_text_content_serialization_diff?(diff_node)
|
|
50
19
|
return false unless diff_node.dimension == :text_content
|
|
51
20
|
|
|
52
21
|
node1 = diff_node.node1
|
|
53
22
|
node2 = diff_node.node2
|
|
54
23
|
|
|
55
|
-
# Both nodes are nil - no actual difference, not a serialization formatting diff
|
|
56
24
|
return false if node1.nil? && node2.nil?
|
|
57
25
|
|
|
58
|
-
# Only one is nil (e.g., one doc has self-closing, other has text)
|
|
59
|
-
# If the non-nil one is blank, it's still serialization formatting
|
|
60
26
|
if node1.nil? || node2.nil?
|
|
61
27
|
non_nil = node1 || node2
|
|
62
28
|
return false unless text_node?(non_nil)
|
|
@@ -65,74 +31,32 @@ module Canon
|
|
|
65
31
|
return blank?(text)
|
|
66
32
|
end
|
|
67
33
|
|
|
68
|
-
# Both must be text nodes
|
|
69
34
|
return false unless text_node?(node1) && text_node?(node2)
|
|
70
35
|
|
|
71
36
|
text1 = extract_text_content(node1)
|
|
72
37
|
text2 = extract_text_content(node2)
|
|
73
38
|
|
|
74
|
-
# Check if both texts are blank/whitespace-only
|
|
75
|
-
# This indicates self-closing vs explicit closing tag syntax
|
|
76
39
|
blank?(text1) && blank?(text2)
|
|
77
40
|
end
|
|
78
41
|
|
|
79
|
-
# Check if a value is blank (nil or whitespace-only)
|
|
80
|
-
# @param value [String, nil] Value to check
|
|
81
|
-
# @return [Boolean] true if blank
|
|
82
42
|
def self.blank?(value)
|
|
83
|
-
value.nil? ||
|
|
84
|
-
(value.respond_to?(:empty?) && value.empty?) ||
|
|
85
|
-
(value.respond_to?(:strip) && value.strip.empty?)
|
|
43
|
+
value.nil? || value.to_s.strip.empty?
|
|
86
44
|
end
|
|
87
45
|
|
|
88
|
-
# Check if a node is a text node
|
|
89
|
-
# @param node [Object] The node to check
|
|
90
|
-
# @return [Boolean] true if the node is a text node
|
|
91
46
|
def self.text_node?(node)
|
|
92
47
|
return false if node.nil?
|
|
48
|
+
return true if node.is_a?(String)
|
|
93
49
|
|
|
94
|
-
|
|
95
|
-
when Canon::Xml::Nodes::TextNode
|
|
96
|
-
true
|
|
97
|
-
when Canon::Xml::Node
|
|
98
|
-
node.node_type == :text
|
|
99
|
-
when Nokogiri::XML::Node
|
|
100
|
-
node.node_type == Nokogiri::XML::Node::TEXT_NODE
|
|
101
|
-
when Moxml::Node
|
|
102
|
-
node.text?
|
|
103
|
-
when String
|
|
104
|
-
true
|
|
105
|
-
else
|
|
106
|
-
false
|
|
107
|
-
end
|
|
50
|
+
NI.text_node?(node)
|
|
108
51
|
end
|
|
109
52
|
|
|
110
|
-
# Extract text content from a node
|
|
111
|
-
# @param node [Object] The node to extract text from
|
|
112
|
-
# @return [String, nil] The text content or nil
|
|
113
53
|
def self.extract_text_content(node)
|
|
114
54
|
return nil if node.nil?
|
|
115
55
|
|
|
116
|
-
|
|
117
|
-
when Canon::Xml::Nodes::TextNode
|
|
118
|
-
node.value
|
|
119
|
-
when Canon::Xml::Node
|
|
120
|
-
node.text_content
|
|
121
|
-
when Nokogiri::XML::Node
|
|
122
|
-
node.content.to_s
|
|
123
|
-
when Moxml::Node
|
|
124
|
-
node.content.to_s
|
|
125
|
-
when String
|
|
126
|
-
node
|
|
127
|
-
else
|
|
128
|
-
node.to_s
|
|
129
|
-
end
|
|
56
|
+
NI.text_content(node)
|
|
130
57
|
rescue StandardError
|
|
131
58
|
nil
|
|
132
59
|
end
|
|
133
|
-
|
|
134
|
-
private_class_method :blank?, :text_node?, :extract_text_content,
|
|
135
|
-
:empty_text_content_serialization_diff?
|
|
136
60
|
end
|
|
137
61
|
end
|
|
138
62
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "diff/lcs"
|
|
4
|
-
require "diff/lcs/hunk"
|
|
3
|
+
require "diff/lcs" unless RUBY_ENGINE == "opal"
|
|
4
|
+
require "diff/lcs/hunk" unless RUBY_ENGINE == "opal"
|
|
5
5
|
require_relative "../debug_output"
|
|
6
6
|
require_relative "../theme"
|
|
7
7
|
|
|
@@ -233,7 +233,7 @@ module Canon
|
|
|
233
233
|
# @return [Rainbow::Presenter] Colorized presenter
|
|
234
234
|
def apply_color(presenter, color)
|
|
235
235
|
valid_colors = normalize_color_for_rainbow(color)
|
|
236
|
-
valid_colors.each { |c| presenter = presenter.
|
|
236
|
+
valid_colors.each { |c| presenter = presenter.public_send(c) }
|
|
237
237
|
presenter
|
|
238
238
|
end
|
|
239
239
|
|
|
@@ -385,7 +385,7 @@ module Canon
|
|
|
385
385
|
rainbow = Rainbow.new
|
|
386
386
|
rainbow.enabled = true
|
|
387
387
|
presenter = rainbow.wrap(text)
|
|
388
|
-
valid_colors.each { |c| presenter = presenter.
|
|
388
|
+
valid_colors.each { |c| presenter = presenter.public_send(c) }
|
|
389
389
|
presenter.to_s
|
|
390
390
|
end
|
|
391
391
|
|
|
@@ -675,13 +675,13 @@ module Canon
|
|
|
675
675
|
# Handle Rainbow color methods - :bright_blue -> .blue.bright, etc.
|
|
676
676
|
if color.to_s.start_with?("bright_")
|
|
677
677
|
base_color = color.to_s.sub(/^bright_/, "").to_sym
|
|
678
|
-
presenter = presenter.
|
|
678
|
+
presenter = presenter.public_send(base_color).bright
|
|
679
679
|
elsif color.to_s.start_with?("light_")
|
|
680
680
|
# Rainbow doesn't have light_ versions, treat as white on bg
|
|
681
681
|
base_color = color.to_s.sub(/^light_/, "").to_sym
|
|
682
|
-
presenter = presenter.
|
|
682
|
+
presenter = presenter.public_send(base_color)
|
|
683
683
|
else
|
|
684
|
-
presenter = presenter.
|
|
684
|
+
presenter = presenter.public_send(color)
|
|
685
685
|
end
|
|
686
686
|
|
|
687
687
|
presenter.to_s
|
|
@@ -1205,11 +1205,11 @@ informative: false, formatting: false)
|
|
|
1205
1205
|
# Apply effect if specified (map :strikethrough to :cross_out for Rainbow)
|
|
1206
1206
|
if effect
|
|
1207
1207
|
rainbow_effect = effect == :strikethrough ? :cross_out : effect
|
|
1208
|
-
presenter = presenter.
|
|
1208
|
+
presenter = presenter.public_send(rainbow_effect)
|
|
1209
1209
|
end
|
|
1210
1210
|
|
|
1211
1211
|
# Apply color if specified
|
|
1212
|
-
presenter = presenter.
|
|
1212
|
+
presenter = presenter.public_send(color) if color
|
|
1213
1213
|
|
|
1214
1214
|
presenter.to_s
|
|
1215
1215
|
else
|
|
@@ -26,7 +26,7 @@ show_diffs: :all, theme: nil)
|
|
|
26
26
|
# @return [String] Formatted output
|
|
27
27
|
def format(differences, _format)
|
|
28
28
|
# Handle both ComparisonResult (production) and Array (low-level tests)
|
|
29
|
-
if differences.
|
|
29
|
+
if differences.is_a?(Canon::Comparison::ComparisonResult)
|
|
30
30
|
# ComparisonResult object
|
|
31
31
|
return success_message if differences.equivalent?
|
|
32
32
|
|
|
@@ -130,15 +130,11 @@ show_diffs: :all, theme: nil)
|
|
|
130
130
|
return differences if @show_diffs.nil? || @show_diffs == :all
|
|
131
131
|
|
|
132
132
|
differences.select do |diff|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else
|
|
139
|
-
# Default to normative if unknown
|
|
140
|
-
true
|
|
141
|
-
end
|
|
133
|
+
is_normative = begin
|
|
134
|
+
diff.normative?
|
|
135
|
+
rescue NoMethodError
|
|
136
|
+
diff.is_a?(Hash) && diff.key?(:normative) ? diff[:normative] : true
|
|
137
|
+
end
|
|
142
138
|
|
|
143
139
|
case @show_diffs
|
|
144
140
|
when :normative
|
|
@@ -207,9 +203,9 @@ show_diffs: :all, theme: nil)
|
|
|
207
203
|
parts = []
|
|
208
204
|
current = node
|
|
209
205
|
|
|
210
|
-
while current.
|
|
206
|
+
while Canon::XmlParsing.xml_node?(current) || current.is_a?(Canon::Xml::Node)
|
|
211
207
|
parts.unshift(current.name) if current.name
|
|
212
|
-
current = current.parent
|
|
208
|
+
current = current.parent
|
|
213
209
|
end
|
|
214
210
|
|
|
215
211
|
parts.join(".")
|
|
@@ -274,9 +270,12 @@ show_diffs: :all, theme: nil)
|
|
|
274
270
|
|
|
275
271
|
if diffs && !diffs.empty?
|
|
276
272
|
# Render all differences at this path
|
|
273
|
+
has_children = value.is_a?(Hash) &&
|
|
274
|
+
(value.keys - [:__diffs__]).any?
|
|
275
|
+
|
|
277
276
|
diffs.each_with_index do |diff, diff_idx|
|
|
278
|
-
|
|
279
|
-
current_connector = if
|
|
277
|
+
is_last_diff = diff_idx == diffs.length - 1
|
|
278
|
+
current_connector = if is_last_diff && !has_children
|
|
280
279
|
connector
|
|
281
280
|
else
|
|
282
281
|
is_last_item ? "├── " : "├── "
|
|
@@ -286,6 +285,13 @@ show_diffs: :all, theme: nil)
|
|
|
286
285
|
output << line
|
|
287
286
|
@line_count += line.count("\n") + 1
|
|
288
287
|
end
|
|
288
|
+
|
|
289
|
+
# Recurse into child diffs at this path
|
|
290
|
+
if has_children
|
|
291
|
+
subtree = render_tree(value, prefix: prefix + continuation,
|
|
292
|
+
is_last: is_last_item)
|
|
293
|
+
output << subtree
|
|
294
|
+
end
|
|
289
295
|
else
|
|
290
296
|
# Render intermediate path
|
|
291
297
|
line = colorize("#{prefix}#{connector}#{key}:",
|
|
@@ -328,13 +334,13 @@ show_diffs: :all, theme: nil)
|
|
|
328
334
|
# Handle bright_ colors: :bright_blue -> .blue.bright
|
|
329
335
|
if c.to_s.start_with?("bright_")
|
|
330
336
|
base = c.to_s.sub(/^bright_/, "").to_sym
|
|
331
|
-
presenter = presenter.
|
|
337
|
+
presenter = presenter.public_send(base).bright
|
|
332
338
|
elsif c.to_s.start_with?("light_")
|
|
333
339
|
# Rainbow doesn't have light_ versions
|
|
334
340
|
base = c.to_s.sub(/^light_/, "").to_sym
|
|
335
|
-
presenter = presenter.
|
|
341
|
+
presenter = presenter.public_send(base)
|
|
336
342
|
else
|
|
337
|
-
presenter = presenter.
|
|
343
|
+
presenter = presenter.public_send(c)
|
|
338
344
|
end
|
|
339
345
|
end
|
|
340
346
|
presenter.to_s
|
|
@@ -136,7 +136,16 @@ module Canon
|
|
|
136
136
|
theme_color(:changed, :marker) || :yellow,
|
|
137
137
|
)}"
|
|
138
138
|
end
|
|
139
|
-
when :
|
|
139
|
+
when :attribute_values
|
|
140
|
+
render_attribute_values_diffnode(diff_node, node1, node2, prefix,
|
|
141
|
+
output)
|
|
142
|
+
when :attribute_presence
|
|
143
|
+
render_attribute_presence_diffnode(diff_node, node1, node2, prefix,
|
|
144
|
+
output)
|
|
145
|
+
when :attribute_order
|
|
146
|
+
render_attribute_order_diffnode(diff_node, node1, node2, prefix,
|
|
147
|
+
output)
|
|
148
|
+
when :structural_whitespace, :attribute_whitespace
|
|
140
149
|
output << "#{prefix}└── #{colorize(
|
|
141
150
|
"[#{diff_node.dimension}: #{diff_node.reason}]",
|
|
142
151
|
theme_color(:changed, :marker) || :yellow,
|
|
@@ -150,6 +159,115 @@ module Canon
|
|
|
150
159
|
end
|
|
151
160
|
end
|
|
152
161
|
|
|
162
|
+
# Render attribute value DiffNode with per-attribute before/after
|
|
163
|
+
def render_attribute_values_diffnode(diff_node, node1, node2, prefix,
|
|
164
|
+
output)
|
|
165
|
+
attrs1 = diff_node.attributes_before ||
|
|
166
|
+
extract_attributes_hash(node1)
|
|
167
|
+
attrs2 = diff_node.attributes_after ||
|
|
168
|
+
extract_attributes_hash(node2)
|
|
169
|
+
element_name = if node1
|
|
170
|
+
node1.name
|
|
171
|
+
else
|
|
172
|
+
(node2 ? node2.name : "?")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
output << "#{prefix}└── #{colorize(
|
|
176
|
+
"Element: <#{element_name}>",
|
|
177
|
+
theme_color(:changed, :marker) || :yellow,
|
|
178
|
+
)}"
|
|
179
|
+
|
|
180
|
+
differing = find_differing_attributes(attrs1, attrs2)
|
|
181
|
+
differing.each_with_index do |name, i|
|
|
182
|
+
connector = i < differing.size - 1 ? "├──" : "└──"
|
|
183
|
+
val1 = attrs1[name].to_s
|
|
184
|
+
val2 = attrs2[name].to_s
|
|
185
|
+
output << "#{prefix} #{connector} " \
|
|
186
|
+
"#{colorize("#{name}:", :cyan)} " \
|
|
187
|
+
"#{colorize(val1,
|
|
188
|
+
theme_color(:removed, :content) || :red)} " \
|
|
189
|
+
"→ " \
|
|
190
|
+
"#{colorize(val2,
|
|
191
|
+
theme_color(:added, :content) || :green)}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Render attribute presence DiffNode with added/removed attributes
|
|
196
|
+
def render_attribute_presence_diffnode(diff_node, node1, node2, prefix,
|
|
197
|
+
output)
|
|
198
|
+
attrs1 = diff_node.attributes_before ||
|
|
199
|
+
extract_attributes_hash(node1)
|
|
200
|
+
attrs2 = diff_node.attributes_after ||
|
|
201
|
+
extract_attributes_hash(node2)
|
|
202
|
+
element_name = if node1
|
|
203
|
+
node1.name
|
|
204
|
+
else
|
|
205
|
+
(node2 ? node2.name : "?")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
output << "#{prefix}└── #{colorize(
|
|
209
|
+
"Element: <#{element_name}>",
|
|
210
|
+
theme_color(:changed, :marker) || :yellow,
|
|
211
|
+
)}"
|
|
212
|
+
|
|
213
|
+
keys1 = attrs1.keys.to_set
|
|
214
|
+
keys2 = attrs2.keys.to_set
|
|
215
|
+
removed = (keys1 - keys2).sort
|
|
216
|
+
added = (keys2 - keys1).sort
|
|
217
|
+
items = removed.map { |k| [:removed, k, attrs1[k]] }
|
|
218
|
+
added.each { |k| items << [:added, k, attrs2[k]] }
|
|
219
|
+
|
|
220
|
+
items.each_with_index do |(type, name, val), i|
|
|
221
|
+
connector = i < items.size - 1 ? "├──" : "└──"
|
|
222
|
+
color = if type == :removed
|
|
223
|
+
theme_color(:removed,
|
|
224
|
+
:content) || :red
|
|
225
|
+
else
|
|
226
|
+
theme_color(
|
|
227
|
+
:added, :content
|
|
228
|
+
) || :green
|
|
229
|
+
end
|
|
230
|
+
sign = type == :removed ? "-" : "+"
|
|
231
|
+
output << "#{prefix} #{connector} " \
|
|
232
|
+
"#{colorize("#{sign} #{name}=\"#{val}\"", color)}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Render attribute order DiffNode with before/after order
|
|
237
|
+
def render_attribute_order_diffnode(diff_node, node1, node2, prefix,
|
|
238
|
+
output)
|
|
239
|
+
attrs1 = diff_node.attributes_before ||
|
|
240
|
+
extract_attributes_hash(node1)
|
|
241
|
+
attrs2 = diff_node.attributes_after ||
|
|
242
|
+
extract_attributes_hash(node2)
|
|
243
|
+
|
|
244
|
+
order1 = attrs1.keys.join(", ")
|
|
245
|
+
order2 = attrs2.keys.join(", ")
|
|
246
|
+
|
|
247
|
+
output << "#{prefix}├── #{colorize(
|
|
248
|
+
"- [#{order1}]",
|
|
249
|
+
theme_color(:removed, :content) || :red,
|
|
250
|
+
)}"
|
|
251
|
+
output << "#{prefix}└── #{colorize(
|
|
252
|
+
"+ [#{order2}]",
|
|
253
|
+
theme_color(:added, :content) || :green,
|
|
254
|
+
)}"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Extract attributes hash from a node
|
|
258
|
+
def extract_attributes_hash(node)
|
|
259
|
+
return {} unless node
|
|
260
|
+
|
|
261
|
+
Canon::Diff::NodeSerializer.extract_attributes(node) || {}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Find attributes that differ between two attribute hashes
|
|
265
|
+
def find_differing_attributes(attrs1, attrs2)
|
|
266
|
+
(attrs1.keys | attrs2.keys).reject do |k|
|
|
267
|
+
attrs1[k.to_s] == attrs2[k.to_s]
|
|
268
|
+
end.sort
|
|
269
|
+
end
|
|
270
|
+
|
|
153
271
|
# Render unequal elements
|
|
154
272
|
def render_unequal_elements(diff, prefix, output)
|
|
155
273
|
node1 = diff[:node1]
|
|
@@ -171,7 +289,8 @@ module Canon
|
|
|
171
289
|
text2 = extract_text(node2)
|
|
172
290
|
|
|
173
291
|
# Show parent element if available
|
|
174
|
-
if
|
|
292
|
+
if (Canon::XmlParsing.xml_node?(node1) || node1.is_a?(Canon::Xml::Node)) &&
|
|
293
|
+
node1.parent
|
|
175
294
|
output << "#{prefix} #{colorize(
|
|
176
295
|
"Element: <#{node1.parent.name}>",
|
|
177
296
|
theme_color(:informative, :content) || :blue,
|
|
@@ -274,9 +393,9 @@ module Canon
|
|
|
274
393
|
parts = []
|
|
275
394
|
current = node
|
|
276
395
|
|
|
277
|
-
while current.
|
|
396
|
+
while Canon::XmlParsing.xml_node?(current) || current.is_a?(Canon::Xml::Node)
|
|
278
397
|
parts.unshift(current.name) if current.name
|
|
279
|
-
current = current.parent
|
|
398
|
+
current = current.parent
|
|
280
399
|
end
|
|
281
400
|
|
|
282
401
|
parts.join(".")
|
|
@@ -287,13 +406,10 @@ module Canon
|
|
|
287
406
|
# @param node [Object] Node with content or text
|
|
288
407
|
# @return [String] Text content
|
|
289
408
|
def extract_text(node)
|
|
290
|
-
if node.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
node.content.to_s
|
|
295
|
-
elsif node.respond_to?(:text)
|
|
296
|
-
node.text.to_s
|
|
409
|
+
if node.is_a?(Canon::Xml::Node)
|
|
410
|
+
node.text_content.to_s
|
|
411
|
+
elsif Canon::XmlParsing.xml_node?(node)
|
|
412
|
+
Canon::XmlParsing.text_content(node).to_s
|
|
297
413
|
else
|
|
298
414
|
""
|
|
299
415
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "paint"
|
|
3
|
+
require "paint" unless RUBY_ENGINE == "opal"
|
|
4
4
|
|
|
5
5
|
module Canon
|
|
6
6
|
class DiffFormatter
|
|
@@ -21,9 +21,6 @@ module Canon
|
|
|
21
21
|
# @param format [Symbol] Document format (:xml, :json, :yaml)
|
|
22
22
|
# @return [String] Formatted diff output
|
|
23
23
|
def format(differences, format)
|
|
24
|
-
output = []
|
|
25
|
-
output << colorize("Visual Diff:", :cyan, :bold)
|
|
26
|
-
|
|
27
24
|
diffs_array = if differences.is_a?(Canon::Comparison::ComparisonResult)
|
|
28
25
|
differences.differences
|
|
29
26
|
else
|
|
@@ -37,8 +34,7 @@ module Canon
|
|
|
37
34
|
show_diffs: @show_diffs,
|
|
38
35
|
)
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
output.join("\n")
|
|
37
|
+
formatter.format(diffs_array, format)
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
private
|