canon 0.2.9 → 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/.rubocop_todo.yml +19 -10
- data/Rakefile +22 -2
- data/lib/canon/cache.rb +16 -27
- data/lib/canon/comparison/html_comparator.rb +2 -0
- data/lib/canon/comparison/node_inspector.rb +13 -48
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +19 -2
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +30 -0
- data/lib/canon/comparison/xml_comparator/node_parser.rb +2 -2
- data/lib/canon/comparison/xml_comparator.rb +2 -2
- data/lib/canon/comparison.rb +1 -1
- data/lib/canon/diff/diff_line_builder.rb +2 -0
- data/lib/canon/diff/diff_node_mapper.rb +10 -8
- data/lib/canon/diff/formatting_detector.rb +3 -2
- data/lib/canon/diff/xml_serialization_formatter.rb +0 -3
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +12 -2
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +119 -1
- data/lib/canon/diff_formatter/by_object_formatter.rb +1 -5
- 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.rb +15 -11
- data/lib/canon/html/data_model.rb +1 -1
- 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 +2 -7
- data/lib/canon/validators/xml_validator.rb +2 -7
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +5 -5
- data/lib/canon/xml/sax_builder.rb +1 -1
- data/lib/canon/xml/whitespace_normalizer.rb +2 -2
- data/lib/canon/xml_parsing.rb +28 -16
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fcacddf719d0cb69472bc55b2a23d724c938fdf76f995eb77be962aeda42182
|
|
4
|
+
data.tar.gz: 3a1d14c811209e8353c7539d12c2f7a4d23d44c1e6742b6f48a8d94f65282bd5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4685b05d13d6cedad780751a8011a722511a9a5623a3a3fa14ce548b95d036c8fd36cabb10d8793ede0a97cf5a1e72d8f53729022c90c8b316c510dc133983e
|
|
7
|
+
data.tar.gz: 5201625d93a1ed9c2efb929905c1d14f4a292e914604149f8e5902a61d1ea6059b987c8875b073ec71340adb1833a33bbce450f58006f29736a37d810f326d6d
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
|
-
# `rubocop --auto-gen-config`
|
|
3
|
-
#
|
|
2
|
+
# `rubocop --auto-gen-config --no-auto-gen-timestamp`
|
|
3
|
+
# using RuboCop version 1.86.0.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,7 +11,7 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'canon.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 1357
|
|
15
15
|
# This cop supports safe autocorrection (--autocorrect).
|
|
16
16
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
17
17
|
# URISchemes: http, https
|
|
@@ -62,7 +62,7 @@ Lint/UselessConstantScoping:
|
|
|
62
62
|
Exclude:
|
|
63
63
|
- 'lib/canon/diff_formatter/theme.rb'
|
|
64
64
|
|
|
65
|
-
# Offense count:
|
|
65
|
+
# Offense count: 316
|
|
66
66
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
67
67
|
Metrics/AbcSize:
|
|
68
68
|
Enabled: false
|
|
@@ -83,7 +83,7 @@ Metrics/BlockNesting:
|
|
|
83
83
|
Metrics/CyclomaticComplexity:
|
|
84
84
|
Enabled: false
|
|
85
85
|
|
|
86
|
-
# Offense count:
|
|
86
|
+
# Offense count: 527
|
|
87
87
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
88
88
|
Metrics/MethodLength:
|
|
89
89
|
Max: 146
|
|
@@ -107,6 +107,14 @@ Naming/MethodParameterName:
|
|
|
107
107
|
- 'lib/canon/comparison/xml_comparator/attribute_comparator.rb'
|
|
108
108
|
- 'lib/canon/xml/namespace_handler.rb'
|
|
109
109
|
|
|
110
|
+
# Offense count: 1
|
|
111
|
+
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
|
112
|
+
# AllowedMethods: call
|
|
113
|
+
# WaywardPredicates: infinite?, nonzero?
|
|
114
|
+
Naming/PredicateMethod:
|
|
115
|
+
Exclude:
|
|
116
|
+
- 'spec/canon/comparison/xml_attribute_diff_spec.rb'
|
|
117
|
+
|
|
110
118
|
# Offense count: 6
|
|
111
119
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
112
120
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
@@ -132,7 +140,7 @@ Performance/CollectionLiteralInLoop:
|
|
|
132
140
|
RSpec/ContextWording:
|
|
133
141
|
Enabled: false
|
|
134
142
|
|
|
135
|
-
# Offense count:
|
|
143
|
+
# Offense count: 48
|
|
136
144
|
# Configuration parameters: IgnoredMetadata.
|
|
137
145
|
RSpec/DescribeClass:
|
|
138
146
|
Enabled: false
|
|
@@ -143,7 +151,7 @@ RSpec/DescribeMethod:
|
|
|
143
151
|
- 'spec/canon/comparison/multiple_differences_spec.rb'
|
|
144
152
|
- 'spec/canon/diff_formatter/character_map_customization_spec.rb'
|
|
145
153
|
|
|
146
|
-
# Offense count:
|
|
154
|
+
# Offense count: 893
|
|
147
155
|
# Configuration parameters: CountAsOne.
|
|
148
156
|
RSpec/ExampleLength:
|
|
149
157
|
Max: 44
|
|
@@ -195,7 +203,7 @@ RSpec/MultipleDescribes:
|
|
|
195
203
|
Exclude:
|
|
196
204
|
- 'spec/canon/comparison/match_options_spec.rb'
|
|
197
205
|
|
|
198
|
-
# Offense count:
|
|
206
|
+
# Offense count: 749
|
|
199
207
|
RSpec/MultipleExpectations:
|
|
200
208
|
Max: 15
|
|
201
209
|
|
|
@@ -248,7 +256,7 @@ RSpec/SpecFilePathFormat:
|
|
|
248
256
|
- 'spec/canon/yaml/formatter_spec.rb'
|
|
249
257
|
- 'spec/xml_c14n_spec.rb'
|
|
250
258
|
|
|
251
|
-
# Offense count:
|
|
259
|
+
# Offense count: 18
|
|
252
260
|
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
|
253
261
|
RSpec/VerifiedDoubles:
|
|
254
262
|
Exclude:
|
|
@@ -274,12 +282,13 @@ Style/HashLikeCase:
|
|
|
274
282
|
- 'lib/canon/diff/diff_block_builder.rb'
|
|
275
283
|
- 'lib/canon/xml/character_encoder.rb'
|
|
276
284
|
|
|
277
|
-
# Offense count:
|
|
285
|
+
# Offense count: 6
|
|
278
286
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
279
287
|
Style/IdenticalConditionalBranches:
|
|
280
288
|
Exclude:
|
|
281
289
|
- 'lib/canon/diff_formatter/by_object/base_formatter.rb'
|
|
282
290
|
- 'lib/canon/diff_formatter/legend.rb'
|
|
291
|
+
- 'lib/canon/xml/whitespace_normalizer.rb'
|
|
283
292
|
|
|
284
293
|
# Offense count: 1
|
|
285
294
|
# Configuration parameters: AllowedMethods.
|
data/Rakefile
CHANGED
|
@@ -7,6 +7,26 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
7
7
|
|
|
8
8
|
begin
|
|
9
9
|
require "opal/rspec/rake_task"
|
|
10
|
+
|
|
11
|
+
# Configure Opal load paths at load time (same pattern as moxml)
|
|
12
|
+
if defined?(Opal)
|
|
13
|
+
Opal.append_path File.expand_path("lib", __dir__)
|
|
14
|
+
|
|
15
|
+
# moxml gem
|
|
16
|
+
begin
|
|
17
|
+
moxml_spec = Gem::Specification.find_by_name("moxml")
|
|
18
|
+
moxml_lib = moxml_spec.full_require_paths.first
|
|
19
|
+
Opal.append_path moxml_lib
|
|
20
|
+
moxml_compat = File.expand_path("compat/opal", moxml_lib)
|
|
21
|
+
Opal.append_path moxml_compat if File.directory?(moxml_compat)
|
|
22
|
+
rescue Gem::MissingSpecError
|
|
23
|
+
# moxml not installed
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# REXML: bundled gem since Ruby 3.4
|
|
27
|
+
rexml_lib = $LOAD_PATH.find { |p| File.exist?(File.join(p, "rexml", "document.rb")) }
|
|
28
|
+
Opal.append_path rexml_lib if rexml_lib
|
|
29
|
+
end
|
|
10
30
|
rescue LoadError
|
|
11
31
|
# Opal not available or incompatible with current Ruby version
|
|
12
32
|
end
|
|
@@ -20,9 +40,9 @@ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
|
|
|
20
40
|
namespace :spec do
|
|
21
41
|
if defined?(Opal::RSpec::RakeTask)
|
|
22
42
|
desc "Run Opal (JavaScript) tests"
|
|
23
|
-
Opal::RSpec::RakeTask.new(:opal) do |
|
|
24
|
-
server.append_path "lib"
|
|
43
|
+
Opal::RSpec::RakeTask.new(:opal) do |_server, runner|
|
|
25
44
|
runner.default_path = "spec"
|
|
45
|
+
runner.requires = %w[rexml_compat rexml/document rexml/xpath moxml/adapter/rexml spec_helper]
|
|
26
46
|
runner.pattern = "spec/canon/opal_xml_smoke_spec.rb"
|
|
27
47
|
end
|
|
28
48
|
end
|
data/lib/canon/cache.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest"
|
|
3
|
+
require "digest" unless RUBY_ENGINE == "opal"
|
|
4
4
|
|
|
5
5
|
module Canon
|
|
6
6
|
# Cache for expensive operations during document comparison
|
|
@@ -73,50 +73,39 @@ module Canon
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# Generate cache key for document parsing
|
|
76
|
-
#
|
|
77
|
-
# @param content [String] Document content
|
|
78
|
-
# @param format [Symbol] Document format
|
|
79
|
-
# @param preprocessing [Symbol] Preprocessing option
|
|
80
|
-
# @return [String] Cache key
|
|
81
76
|
def key_for_document(content, format, preprocessing)
|
|
82
|
-
|
|
83
|
-
"doc:#{format}:#{preprocessing}:#{digest[0..16]}"
|
|
77
|
+
"doc:#{format}:#{preprocessing}:#{content_hash(content)}"
|
|
84
78
|
end
|
|
85
79
|
|
|
86
80
|
# Generate cache key for format detection
|
|
87
|
-
#
|
|
88
|
-
# @param content [String] Document content
|
|
89
|
-
# @return [String] Cache key
|
|
90
81
|
def key_for_format_detection(content)
|
|
91
|
-
# Use first 100 chars for quick key, plus length
|
|
92
|
-
# Force to binary to avoid encoding compatibility issues
|
|
93
82
|
preview = content[0..100].b
|
|
94
|
-
|
|
95
|
-
"fmt:#{digest[0..16]}"
|
|
83
|
+
"fmt:#{content_hash(preview + content.length.to_s)}"
|
|
96
84
|
end
|
|
97
85
|
|
|
98
86
|
# Generate cache key for XML canonicalization
|
|
99
|
-
#
|
|
100
|
-
# @param content [String] XML content
|
|
101
|
-
# @param with_comments [Boolean] Whether to include comments
|
|
102
|
-
# @return [String] Cache key
|
|
103
87
|
def key_for_c14n(content, with_comments)
|
|
104
|
-
|
|
105
|
-
"c14n:#{with_comments}:#{digest[0..16]}"
|
|
88
|
+
"c14n:#{with_comments}:#{content_hash(content)}"
|
|
106
89
|
end
|
|
107
90
|
|
|
108
91
|
# Generate cache key for preprocessing
|
|
109
|
-
#
|
|
110
|
-
# @param content [String] Original content
|
|
111
|
-
# @param preprocessing [Symbol] Preprocessing type
|
|
112
|
-
# @return [String] Cache key
|
|
113
92
|
def key_for_preprocessing(content, preprocessing)
|
|
114
|
-
|
|
115
|
-
"pre:#{preprocessing}:#{digest[0..16]}"
|
|
93
|
+
"pre:#{preprocessing}:#{content_hash(content)}"
|
|
116
94
|
end
|
|
117
95
|
|
|
118
96
|
private
|
|
119
97
|
|
|
98
|
+
# Generate a hash string for cache keys
|
|
99
|
+
def content_hash(content)
|
|
100
|
+
if defined?(Digest::SHA256)
|
|
101
|
+
Digest::SHA256.hexdigest(content)[0..16]
|
|
102
|
+
else
|
|
103
|
+
# Opal fallback: simple string hash
|
|
104
|
+
h = content.each_char.reduce(0) { |acc, c| ((acc * 31) + c.ord) & 0xFFFFFFFF }
|
|
105
|
+
h.to_s(16).rjust(8, "0")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
120
109
|
# Get or create cache for a category
|
|
121
110
|
#
|
|
122
111
|
# @param category [Symbol] Cache category
|
|
@@ -10,37 +10,25 @@ module Canon
|
|
|
10
10
|
# * Canon::TreeDiff::Core::TreeNode — semantic tree diff nodes.
|
|
11
11
|
# * Backend-specific nodes (Nokogiri or Moxml) — live parsed nodes.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# Every node query in the codebase should go through this module.
|
|
18
|
-
# Do not create private dispatch methods in consumers.
|
|
13
|
+
# Architecture: NodeInspector handles Canon-native types (Canon::Xml::Node,
|
|
14
|
+
# TreeNode) directly, then delegates ALL backend-specific queries to
|
|
15
|
+
# XmlParsing. No Moxml/Nokogiri constants are referenced here — that
|
|
16
|
+
# knowledge lives exclusively in XmlParsing.
|
|
19
17
|
module NodeInspector
|
|
20
|
-
NOKOGIRI_TEXT_TYPE = defined?(Nokogiri::XML::Node::TEXT_NODE) ? Nokogiri::XML::Node::TEXT_NODE : 3
|
|
21
|
-
|
|
22
18
|
# --- Type predicates ---
|
|
23
19
|
|
|
24
20
|
def self.text_node?(node)
|
|
25
21
|
return false unless node
|
|
26
22
|
return node.node_type == :text if node.is_a?(Canon::Xml::Node)
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
node.is_a?(Nokogiri::XML::Text) || node.is_a?(Moxml::Text)
|
|
30
|
-
else
|
|
31
|
-
node.is_a?(Moxml::Text)
|
|
32
|
-
end
|
|
24
|
+
XmlParsing.text_node?(node)
|
|
33
25
|
end
|
|
34
26
|
|
|
35
27
|
def self.element_node?(node)
|
|
36
28
|
return false unless node
|
|
37
29
|
return node.node_type == :element if node.is_a?(Canon::Xml::Node)
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
node.is_a?(Nokogiri::XML::Element) || node.is_a?(Moxml::Element)
|
|
41
|
-
else
|
|
42
|
-
node.is_a?(Moxml::Element)
|
|
43
|
-
end
|
|
31
|
+
XmlParsing.element?(node)
|
|
44
32
|
end
|
|
45
33
|
|
|
46
34
|
def self.comment_node?(node)
|
|
@@ -57,7 +45,7 @@ module Canon
|
|
|
57
45
|
end
|
|
58
46
|
false
|
|
59
47
|
else
|
|
60
|
-
|
|
48
|
+
XmlParsing.comment?(node)
|
|
61
49
|
end
|
|
62
50
|
end
|
|
63
51
|
|
|
@@ -100,7 +88,6 @@ module Canon
|
|
|
100
88
|
|
|
101
89
|
# --- Node queries ---
|
|
102
90
|
|
|
103
|
-
# Unified node name extraction across all node types.
|
|
104
91
|
def self.name(node)
|
|
105
92
|
return nil unless node
|
|
106
93
|
return node.name if node.is_a?(Canon::Xml::Node)
|
|
@@ -109,7 +96,6 @@ module Canon
|
|
|
109
96
|
XmlParsing.name(node)
|
|
110
97
|
end
|
|
111
98
|
|
|
112
|
-
# Unified parent access across all node types.
|
|
113
99
|
def self.parent(node)
|
|
114
100
|
return nil unless node
|
|
115
101
|
return node.parent if node.is_a?(Canon::Xml::Node)
|
|
@@ -118,7 +104,6 @@ module Canon
|
|
|
118
104
|
XmlParsing.parent(node)
|
|
119
105
|
end
|
|
120
106
|
|
|
121
|
-
# Unified children access across all node types.
|
|
122
107
|
def self.children(node)
|
|
123
108
|
return [] unless node
|
|
124
109
|
return node.children if node.is_a?(Canon::Xml::Node)
|
|
@@ -127,34 +112,21 @@ module Canon
|
|
|
127
112
|
XmlParsing.children(node)
|
|
128
113
|
end
|
|
129
114
|
|
|
130
|
-
# Extract the text content of +node+ as a String.
|
|
131
115
|
def self.text_content(node)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
node.text_content.to_s
|
|
137
|
-
when Moxml::Text
|
|
138
|
-
node.content.to_s
|
|
139
|
-
else
|
|
140
|
-
XmlParsing.text_content(node).to_s
|
|
141
|
-
end
|
|
116
|
+
return node.value.to_s if node.is_a?(Canon::Xml::Nodes::TextNode)
|
|
117
|
+
return node.text_content.to_s if node.is_a?(Canon::Xml::Node)
|
|
118
|
+
|
|
119
|
+
XmlParsing.text_content(node).to_s
|
|
142
120
|
end
|
|
143
121
|
|
|
144
|
-
# Unified node type that always returns a symbol.
|
|
145
|
-
# Returns nil for unrecognised nodes.
|
|
146
122
|
def self.node_type(node)
|
|
147
123
|
return nil unless node
|
|
148
124
|
return node.node_type if node.is_a?(Canon::Xml::Node)
|
|
125
|
+
return node.type&.to_sym if node.is_a?(Canon::TreeDiff::Core::TreeNode)
|
|
149
126
|
|
|
150
|
-
|
|
151
|
-
node.type&.to_sym
|
|
152
|
-
else
|
|
153
|
-
XmlParsing.node_type(node)
|
|
154
|
-
end
|
|
127
|
+
XmlParsing.node_type(node)
|
|
155
128
|
end
|
|
156
129
|
|
|
157
|
-
# Unified attribute value access.
|
|
158
130
|
def self.attribute_value(node, attr_name)
|
|
159
131
|
return nil unless node
|
|
160
132
|
|
|
@@ -168,7 +140,6 @@ module Canon
|
|
|
168
140
|
end
|
|
169
141
|
end
|
|
170
142
|
|
|
171
|
-
# Unified namespace URI access.
|
|
172
143
|
def self.namespace_uri(node)
|
|
173
144
|
return nil unless node
|
|
174
145
|
|
|
@@ -179,7 +150,6 @@ module Canon
|
|
|
179
150
|
end
|
|
180
151
|
end
|
|
181
152
|
|
|
182
|
-
# Extract parse-time errors carried on a node or its owning document.
|
|
183
153
|
def self.parse_errors(node)
|
|
184
154
|
return [] if node.nil?
|
|
185
155
|
return Array(node.parse_errors).map(&:to_s) if node.is_a?(Canon::Xml::Node)
|
|
@@ -194,11 +164,6 @@ module Canon
|
|
|
194
164
|
[]
|
|
195
165
|
end
|
|
196
166
|
end
|
|
197
|
-
|
|
198
|
-
# Deprecated: use NodeInspector.parent instead.
|
|
199
|
-
def self.parent_of(node)
|
|
200
|
-
parent(node)
|
|
201
|
-
end
|
|
202
167
|
end
|
|
203
168
|
end
|
|
204
169
|
end
|
|
@@ -15,8 +15,8 @@ module Canon
|
|
|
15
15
|
# @return [Symbol] Comparison result
|
|
16
16
|
def self.compare(node1, node2, opts, differences)
|
|
17
17
|
# Get attributes using the appropriate method for each node type
|
|
18
|
-
raw_attrs1 = node1
|
|
19
|
-
raw_attrs2 = node2
|
|
18
|
+
raw_attrs1 = get_raw_attributes(node1)
|
|
19
|
+
raw_attrs2 = get_raw_attributes(node2)
|
|
20
20
|
|
|
21
21
|
attrs1 = XmlComparatorHelpers::AttributeFilter.filter(raw_attrs1,
|
|
22
22
|
opts)
|
|
@@ -171,6 +171,23 @@ dimension:, differences:, **opts)
|
|
|
171
171
|
)
|
|
172
172
|
differences << diff_node if diff_node
|
|
173
173
|
end
|
|
174
|
+
|
|
175
|
+
# Get raw attributes from a node, dispatching by type.
|
|
176
|
+
# Nokogiri::XML::Element has attribute_nodes (NodeSet),
|
|
177
|
+
# Canon::Xml::Nodes::ElementNode has attribute_nodes (Array),
|
|
178
|
+
# Moxml::Element has attributes (Hash-like).
|
|
179
|
+
def self.get_raw_attributes(node)
|
|
180
|
+
case node
|
|
181
|
+
when Canon::Xml::Nodes::ElementNode
|
|
182
|
+
node.attribute_nodes
|
|
183
|
+
else
|
|
184
|
+
if Canon::XmlBackend.nokogiri? && node.is_a?(Nokogiri::XML::Element)
|
|
185
|
+
node.attribute_nodes
|
|
186
|
+
else
|
|
187
|
+
node.attributes
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
174
191
|
end
|
|
175
192
|
end
|
|
176
193
|
end
|
|
@@ -72,6 +72,11 @@ module Canon
|
|
|
72
72
|
return build_attribute_difference_reason(attrs1, attrs2)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# For attribute value differences, show which attributes changed
|
|
76
|
+
if dimension == :attribute_values
|
|
77
|
+
return build_attribute_values_reason(node1, node2)
|
|
78
|
+
end
|
|
79
|
+
|
|
75
80
|
# For text content differences, show the actual text (truncated if needed)
|
|
76
81
|
if dimension == :text_content
|
|
77
82
|
text1 = extract_text_content(node1)
|
|
@@ -186,6 +191,31 @@ module Canon
|
|
|
186
191
|
end
|
|
187
192
|
end
|
|
188
193
|
|
|
194
|
+
# Build a reason message for attribute value differences
|
|
195
|
+
# Shows each changed attribute with its before/after values
|
|
196
|
+
#
|
|
197
|
+
# @param node1 [Object, nil] First node
|
|
198
|
+
# @param node2 [Object, nil] Second node
|
|
199
|
+
# @return [String] Clear explanation of the attribute value differences
|
|
200
|
+
def self.build_attribute_values_reason(node1, node2)
|
|
201
|
+
attrs1 = extract_attributes(node1) || {}
|
|
202
|
+
attrs2 = extract_attributes(node2) || {}
|
|
203
|
+
|
|
204
|
+
differing = (attrs1.keys | attrs2.keys).sort.reject do |k|
|
|
205
|
+
attrs1[k.to_s] == attrs2[k.to_s]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
changed_parts = differing.map do |k|
|
|
209
|
+
"Changed: #{k}=\"#{attrs1[k.to_s]}\" → \"#{attrs2[k.to_s]}\""
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if changed_parts.empty?
|
|
213
|
+
"attributes differ"
|
|
214
|
+
else
|
|
215
|
+
"Attributes differ (#{changed_parts.join('; ')})"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
189
219
|
# Extract text content from a node
|
|
190
220
|
#
|
|
191
221
|
# @param node [Object, nil] Node to extract text from
|
|
@@ -37,7 +37,7 @@ module Canon
|
|
|
37
37
|
# Select parser backend
|
|
38
38
|
resolved_parser = parser || resolve_parser_config
|
|
39
39
|
|
|
40
|
-
if resolved_parser == :sax
|
|
40
|
+
if resolved_parser == :sax && RUBY_ENGINE != "opal"
|
|
41
41
|
require_relative "../../xml/sax_builder"
|
|
42
42
|
Canon::Xml::SaxBuilder.parse(xml_string,
|
|
43
43
|
preserve_whitespace: preserve_whitespace)
|
|
@@ -97,7 +97,7 @@ parser: nil)
|
|
|
97
97
|
|
|
98
98
|
resolved_parser = parser || resolve_parser_config
|
|
99
99
|
|
|
100
|
-
if resolved_parser == :sax
|
|
100
|
+
if resolved_parser == :sax && RUBY_ENGINE != "opal"
|
|
101
101
|
require_relative "../../xml/sax_builder"
|
|
102
102
|
Canon::Xml::SaxBuilder.parse(xml_str,
|
|
103
103
|
preserve_whitespace: preserve_whitespace)
|
|
@@ -875,7 +875,7 @@ differences)
|
|
|
875
875
|
end
|
|
876
876
|
|
|
877
877
|
def whitespace_adjacency_parent_label(ws_node)
|
|
878
|
-
parent = NodeInspector.
|
|
878
|
+
parent = NodeInspector.parent(ws_node)
|
|
879
879
|
return "(unknown parent)" unless parent
|
|
880
880
|
|
|
881
881
|
name = parent.name
|
|
@@ -890,7 +890,7 @@ differences)
|
|
|
890
890
|
# "adjacent to" as a degenerate fallback when neither neighbour
|
|
891
891
|
# exists.
|
|
892
892
|
def whitespace_partner_direction(ws_node)
|
|
893
|
-
parent = NodeInspector.
|
|
893
|
+
parent = NodeInspector.parent(ws_node)
|
|
894
894
|
return "adjacent to" unless parent
|
|
895
895
|
|
|
896
896
|
siblings = parent.children
|
data/lib/canon/comparison.rb
CHANGED
|
@@ -323,7 +323,7 @@ module Canon
|
|
|
323
323
|
# @param line2 [String] New line content (for +/!)
|
|
324
324
|
# @return [Boolean] true if formatting-only
|
|
325
325
|
def formatting?(node, line1, line2)
|
|
326
|
-
return true if node.
|
|
326
|
+
return true if node.is_a?(Canon::Diff::DiffNode) && node.formatting?
|
|
327
327
|
return false if node
|
|
328
328
|
return true if comment_only_line?(line1) || comment_only_line?(line2)
|
|
329
329
|
|
|
@@ -394,14 +394,14 @@ module Canon
|
|
|
394
394
|
end
|
|
395
395
|
|
|
396
396
|
nodes_to_check.any? do |node|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
elsif node.respond_to?(:parent) && node.parent.respond_to?(:name) && node.parent.name == line_element_name # rubocop:disable Style/IfWithBooleanLiteralBranches
|
|
397
|
+
next false unless node
|
|
398
|
+
|
|
399
|
+
node_name = Canon::Comparison::NodeInspector.name(node)
|
|
400
|
+
if node_name == line_element_name
|
|
402
401
|
true
|
|
403
402
|
else
|
|
404
|
-
|
|
403
|
+
parent = Canon::Comparison::NodeInspector.parent(node)
|
|
404
|
+
parent && Canon::Comparison::NodeInspector.name(parent) == line_element_name
|
|
405
405
|
end
|
|
406
406
|
end
|
|
407
407
|
end
|
|
@@ -453,6 +453,8 @@ module Canon
|
|
|
453
453
|
comment_lines
|
|
454
454
|
end
|
|
455
455
|
|
|
456
|
+
public :build_comment_lines
|
|
457
|
+
|
|
456
458
|
# Find a comment DiffNode for a line that falls within a comment range.
|
|
457
459
|
# Matches by checking if the DiffNode's source node has name "comment".
|
|
458
460
|
#
|
|
@@ -463,7 +465,7 @@ module Canon
|
|
|
463
465
|
@comment_diff_nodes&.find do |diff_node|
|
|
464
466
|
nodes_to_check = [diff_node.node1, diff_node.node2].compact
|
|
465
467
|
nodes_to_check.any? do |node|
|
|
466
|
-
|
|
468
|
+
Canon::Comparison::NodeInspector.name(node) == "comment"
|
|
467
469
|
end
|
|
468
470
|
end
|
|
469
471
|
end
|
|
@@ -297,8 +297,9 @@ module Canon
|
|
|
297
297
|
[close_idx + 1, "<#{sorted}#{suffix}"]
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
public_class_method :normalize_for_comparison, :blank?
|
|
301
|
+
|
|
302
|
+
private_class_method :decode_xml_entities, :normalize_attribute_order,
|
|
302
303
|
:sort_tag_attributes, :tokenize_tag_content,
|
|
303
304
|
:process_tag, :process_processing_instruction,
|
|
304
305
|
:process_comment, :process_regular_tag
|
|
@@ -270,9 +270,12 @@ show_diffs: :all, theme: nil)
|
|
|
270
270
|
|
|
271
271
|
if diffs && !diffs.empty?
|
|
272
272
|
# Render all differences at this path
|
|
273
|
+
has_children = value.is_a?(Hash) &&
|
|
274
|
+
(value.keys - [:__diffs__]).any?
|
|
275
|
+
|
|
273
276
|
diffs.each_with_index do |diff, diff_idx|
|
|
274
|
-
|
|
275
|
-
current_connector = if
|
|
277
|
+
is_last_diff = diff_idx == diffs.length - 1
|
|
278
|
+
current_connector = if is_last_diff && !has_children
|
|
276
279
|
connector
|
|
277
280
|
else
|
|
278
281
|
is_last_item ? "├── " : "├── "
|
|
@@ -282,6 +285,13 @@ show_diffs: :all, theme: nil)
|
|
|
282
285
|
output << line
|
|
283
286
|
@line_count += line.count("\n") + 1
|
|
284
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
|
|
285
295
|
else
|
|
286
296
|
# Render intermediate path
|
|
287
297
|
line = colorize("#{prefix}#{connector}#{key}:",
|