canon 0.3.14 → 0.3.16
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/lib/canon/comparison/diff_node_builder.rb +13 -2
- data/lib/canon/comparison/html_comparator.rb +4 -2
- data/lib/canon/comparison/markup_comparator.rb +15 -2
- data/lib/canon/comparison/match_options.rb +22 -6
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +10 -2
- data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +3 -0
- data/lib/canon/comparison/xml_comparator.rb +12 -8
- data/lib/canon/diff/diff_node.rb +1 -1
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/node.rb +9 -6
- data/lib/canon/xml/nodes/element_node.rb +12 -8
- data/lib/canon/xml/sax_builder.rb +37 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 133da0e86ad37cf095f738d15a37ebcc3b35ae92240327d459ed1d1bbedc3a5b
|
|
4
|
+
data.tar.gz: 6c7c2cf6e60712058a6f49f28cf9181316f9e410da5a3982b72fee7fdebde115
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd39028ce4e945d04ac83d44f9a0da8b516dadf8a454bb634147aabfa480f896ed25f7cf048b39d6b0173a6a1b6aab2881a607b6e8588ff124d4616f95fb944c
|
|
7
|
+
data.tar.gz: 20eedf19bb73922d482537bfe8411bf8b8816001a4df6d59130f08558d80e14d2bfd968c9029d176cae056b0a4d9b1c36d16fb4012329073cceb490846462f7a
|
|
@@ -10,10 +10,21 @@ module Canon
|
|
|
10
10
|
# attributes), and whitespace visualization — previously duplicated
|
|
11
11
|
# across MarkupComparator and XmlComparator.
|
|
12
12
|
class DiffNodeBuilder
|
|
13
|
-
# Build
|
|
14
|
-
|
|
13
|
+
# Build a DiffNode. Verbose callers get the fully enriched form —
|
|
14
|
+
# reason, path, serialized content, attributes; non-verbose
|
|
15
|
+
# callers only ever ask normative?, and DiffClassifier reads node
|
|
16
|
+
# refs and dimension alone, so reason building and metadata
|
|
17
|
+
# enrichment (five display operations per difference) are
|
|
18
|
+
# skipped entirely.
|
|
19
|
+
def self.build(node1:, node2:, diff1:, diff2:, dimension:,
|
|
20
|
+
verbose: false, **_opts)
|
|
15
21
|
raise ArgumentError, "dimension required for DiffNode" if dimension.nil?
|
|
16
22
|
|
|
23
|
+
unless verbose
|
|
24
|
+
return Canon::Diff::DiffNode.new(node1: node1, node2: node2,
|
|
25
|
+
dimension: dimension)
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
reason = build_reason(node1, node2, diff1, diff2, dimension)
|
|
18
29
|
metadata = enrich_metadata(node1, node2)
|
|
19
30
|
|
|
@@ -198,7 +198,7 @@ module Canon
|
|
|
198
198
|
|
|
199
199
|
return Comparison::EQUIVALENT if children1.empty? && children2.empty?
|
|
200
200
|
|
|
201
|
-
emitter = html_diff_emitter(differences)
|
|
201
|
+
emitter = html_diff_emitter(differences, opts)
|
|
202
202
|
ChildRealignment.walk(children1, children2, emitter,
|
|
203
203
|
emit_structural_orphans: true) do |c1, c2|
|
|
204
204
|
XmlNodeComparison.compare_nodes(c1, c2, opts, child_opts,
|
|
@@ -208,10 +208,12 @@ module Canon
|
|
|
208
208
|
|
|
209
209
|
# Build a diff emitter for the HTML comparator path that
|
|
210
210
|
# creates DiffNode objects via DiffNodeBuilder.
|
|
211
|
-
def html_diff_emitter(differences)
|
|
211
|
+
def html_diff_emitter(differences, opts)
|
|
212
|
+
verbose = opts[:verbose]
|
|
212
213
|
proc do |n1, n2, d1, d2, dim|
|
|
213
214
|
differences << Canon::Comparison::DiffNodeBuilder.build(
|
|
214
215
|
node1: n1, node2: n2, diff1: d1, diff2: d2, dimension: dim,
|
|
216
|
+
verbose: verbose
|
|
215
217
|
)
|
|
216
218
|
end
|
|
217
219
|
end
|
|
@@ -16,11 +16,11 @@ module Canon
|
|
|
16
16
|
#
|
|
17
17
|
# Delegates to DiffNodeBuilder, the single DiffNode factory for
|
|
18
18
|
# the DOM comparison path.
|
|
19
|
-
def add_difference(node1, node2, diff1, diff2, dimension,
|
|
19
|
+
def add_difference(node1, node2, diff1, diff2, dimension, opts,
|
|
20
20
|
differences)
|
|
21
21
|
differences << Canon::Comparison::DiffNodeBuilder.build(
|
|
22
22
|
node1: node1, node2: node2, diff1: diff1, diff2: diff2,
|
|
23
|
-
dimension: dimension
|
|
23
|
+
dimension: dimension, verbose: opts[:verbose]
|
|
24
24
|
)
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -53,6 +53,19 @@ module Canon
|
|
|
53
53
|
# @param opts [Hash] Comparison options
|
|
54
54
|
# @return [Array] Filtered array of children
|
|
55
55
|
def filter_children(children, opts)
|
|
56
|
+
# Fast path: with no ignore_nodes list, only text/comment
|
|
57
|
+
# children can ever be excluded (node_excluded? returns false
|
|
58
|
+
# for every other kind) — an all-element child list passes
|
|
59
|
+
# through untouched, with no reject copy and no per-child
|
|
60
|
+
# exclusion checks.
|
|
61
|
+
ignore_nodes = opts[:ignore_nodes]
|
|
62
|
+
if (ignore_nodes.nil? || ignore_nodes.empty?) && !children.empty?
|
|
63
|
+
excludable = children.any? do |child|
|
|
64
|
+
text_node?(child) || comment_node?(child)
|
|
65
|
+
end
|
|
66
|
+
return children unless excludable
|
|
67
|
+
end
|
|
68
|
+
|
|
56
69
|
children.reject do |child|
|
|
57
70
|
node_excluded?(child, opts)
|
|
58
71
|
end
|
|
@@ -70,6 +70,10 @@ module Canon
|
|
|
70
70
|
when :strict
|
|
71
71
|
text1 == text2
|
|
72
72
|
when :normalize
|
|
73
|
+
# Identical strings normalize identically — skip the two
|
|
74
|
+
# gsub+strip chains.
|
|
75
|
+
return true if text1 == text2
|
|
76
|
+
|
|
73
77
|
if whitespace_type == :normalize
|
|
74
78
|
normalize_text(text1) == normalize_text(text2)
|
|
75
79
|
else
|
|
@@ -92,22 +96,34 @@ module Canon
|
|
|
92
96
|
.strip # Remove leading/trailing whitespace
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
# Normalize text preserving Unicode whitespace type distinctions.
|
|
96
99
|
# Fast form of `normalize_text(text).empty?`, called per text
|
|
97
100
|
# node by node_excluded?. Pure-ASCII whitespace (the common
|
|
98
101
|
# case — pretty-print indentation) matches a plain class with
|
|
99
102
|
# no intermediate strings and no \p{} property (Opal's JS
|
|
100
|
-
# regexes do not honor \p{Space});
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
+
# regexes do not honor \p{Space}); a pure-ASCII miss is
|
|
104
|
+
# conclusively non-whitespace; only non-ASCII text falls back
|
|
105
|
+
# to normalize_text's exact Unicode semantics. NUL is included
|
|
106
|
+
# because String#strip strips nulls too.
|
|
103
107
|
ASCII_WHITESPACE_ONLY = /\A[ \t\r\n\v\f\x00]*\z/
|
|
108
|
+
ASCII_ONLY = /\A[\x00-\x7f]*\z/
|
|
104
109
|
|
|
105
110
|
def whitespace_only?(text)
|
|
106
111
|
text = text.to_s
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
return true if text.empty?
|
|
113
|
+
return true if text.match?(ASCII_WHITESPACE_ONLY)
|
|
114
|
+
# A pure-ASCII string that failed the class contains an ASCII
|
|
115
|
+
# non-whitespace character, which survives both the collapse
|
|
116
|
+
# and the strip — conclusively not whitespace-only, with no
|
|
117
|
+
# intermediate strings. Only non-ASCII text (NBSP, U+3000,
|
|
118
|
+
# ...) needs normalize_text's exact Unicode semantics — and
|
|
119
|
+
# no \p{} classes appear in the fast paths, which Opal's JS
|
|
120
|
+
# regexes would not honor anyway.
|
|
121
|
+
return false if text.match?(ASCII_ONLY)
|
|
122
|
+
|
|
123
|
+
normalize_text(text).empty?
|
|
109
124
|
end
|
|
110
125
|
|
|
126
|
+
# Normalize text preserving Unicode whitespace type distinctions.
|
|
111
127
|
def normalize_text_preserving_type(text)
|
|
112
128
|
return "" if text.nil?
|
|
113
129
|
|
|
@@ -26,6 +26,14 @@ module Canon
|
|
|
26
26
|
match_opts = opts[:match_opts]
|
|
27
27
|
attribute_order_behavior = match_opts[:attribute_order] || :strict
|
|
28
28
|
|
|
29
|
+
# FAST PATH: identical filtered attributes in identical key
|
|
30
|
+
# order — exact equality implies behavioral equality for every
|
|
31
|
+
# value behavior, and no order difference exists. Most element
|
|
32
|
+
# pairs hit this and skip the sort/hash/value work entirely.
|
|
33
|
+
if attrs1 == attrs2 && attrs1.keys == attrs2.keys
|
|
34
|
+
return Comparison::EQUIVALENT
|
|
35
|
+
end
|
|
36
|
+
|
|
29
37
|
# Check attribute order if not ignored
|
|
30
38
|
keys1 = attrs1.keys.map(&:to_s)
|
|
31
39
|
keys2 = attrs2.keys.map(&:to_s)
|
|
@@ -157,14 +165,14 @@ differences)
|
|
|
157
165
|
# @param opts [Hash] Options
|
|
158
166
|
# @param differences [Array] Array to append difference to
|
|
159
167
|
def self.add_attribute_difference(n1:, n2:, diff1:, diff2:,
|
|
160
|
-
dimension:, differences:,
|
|
168
|
+
dimension:, differences:, opts: {})
|
|
161
169
|
diff_node = Canon::Comparison::DiffNodeBuilder.build(
|
|
162
170
|
node1: n1,
|
|
163
171
|
node2: n2,
|
|
164
172
|
diff1: diff1,
|
|
165
173
|
diff2: diff2,
|
|
166
174
|
dimension: dimension,
|
|
167
|
-
|
|
175
|
+
verbose: opts[:verbose],
|
|
168
176
|
)
|
|
169
177
|
differences << diff_node if diff_node
|
|
170
178
|
end
|
|
@@ -17,6 +17,9 @@ module Canon
|
|
|
17
17
|
ns_decls1 = extract_declarations(node1)
|
|
18
18
|
ns_decls2 = extract_declarations(node2)
|
|
19
19
|
|
|
20
|
+
# Most element pairs declare nothing — skip the set algebra.
|
|
21
|
+
return Comparison::EQUIVALENT if ns_decls1.empty? && ns_decls2.empty?
|
|
22
|
+
|
|
20
23
|
# Find missing, extra, and changed namespace declarations
|
|
21
24
|
missing = ns_decls1.keys - ns_decls2.keys # In node1 but not node2
|
|
22
25
|
extra = ns_decls2.keys - ns_decls1.keys # In node2 but not node1
|
|
@@ -298,14 +298,18 @@ module Canon
|
|
|
298
298
|
ns2 = Canon::XmlParsing.namespace_uri(n2)
|
|
299
299
|
|
|
300
300
|
unless ns1 == ns2
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
301
|
+
# The DiffNode is only ever appended in verbose mode — skip
|
|
302
|
+
# building it entirely otherwise.
|
|
303
|
+
if opts[:verbose]
|
|
304
|
+
differences << Canon::Comparison::DiffNodeBuilder.build(
|
|
305
|
+
node1: n1,
|
|
306
|
+
node2: n2,
|
|
307
|
+
diff1: Comparison::UNEQUAL_ELEMENTS,
|
|
308
|
+
diff2: Comparison::UNEQUAL_ELEMENTS,
|
|
309
|
+
dimension: :namespace_uri,
|
|
310
|
+
verbose: true,
|
|
311
|
+
)
|
|
312
|
+
end
|
|
309
313
|
return Comparison::UNEQUAL_ELEMENTS
|
|
310
314
|
end
|
|
311
315
|
|
data/lib/canon/diff/diff_node.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Canon
|
|
|
34
34
|
# @param serialized_after [String, nil] Optional serialized content for display
|
|
35
35
|
# @param attributes_before [Hash, nil] Optional normalized attributes hash
|
|
36
36
|
# @param attributes_after [Hash, nil] Optional normalized attributes hash
|
|
37
|
-
def initialize(node1:, node2:, dimension:, reason
|
|
37
|
+
def initialize(node1:, node2:, dimension:, reason: nil,
|
|
38
38
|
path: nil, serialized_before: nil, serialized_after: nil,
|
|
39
39
|
attributes_before: nil, attributes_after: nil)
|
|
40
40
|
@node1 = node1
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/node.rb
CHANGED
|
@@ -4,6 +4,13 @@ module Canon
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Base class for all XPath data model nodes
|
|
6
6
|
class Node
|
|
7
|
+
# Shared by every childless node: leaves (text, attributes,
|
|
8
|
+
# namespaces, comments, PIs) and empty elements read `children`
|
|
9
|
+
# without materializing a private array. Mutating the returned
|
|
10
|
+
# array is a bug — writers go through add_child/children=, which
|
|
11
|
+
# install a private array first.
|
|
12
|
+
EMPTY_CHILDREN = [].freeze
|
|
13
|
+
|
|
7
14
|
attr_reader :parent
|
|
8
15
|
|
|
9
16
|
def initialize
|
|
@@ -12,12 +19,8 @@ module Canon
|
|
|
12
19
|
@in_node_set = true
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
# Leaf nodes (text, attributes, namespaces, comments, PIs) never
|
|
16
|
-
# gain children; allocating the array eagerly cost one Array per
|
|
17
|
-
# node — the single largest retained-allocation source in a built
|
|
18
|
-
# tree — so it materializes on first add or read.
|
|
19
22
|
def children
|
|
20
|
-
@children
|
|
23
|
+
@children || EMPTY_CHILDREN
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def children=(new_children)
|
|
@@ -26,7 +29,7 @@ module Canon
|
|
|
26
29
|
|
|
27
30
|
def add_child(child)
|
|
28
31
|
child.parent = self
|
|
29
|
-
children << child
|
|
32
|
+
(@children ||= []) << child
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def in_node_set?
|
|
@@ -15,12 +15,16 @@ module Canon
|
|
|
15
15
|
@attribute_nodes = nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Lazy:
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# Lazy: attribute-free elements (common in real documents) share
|
|
19
|
+
# one frozen empty array on read instead of materializing one
|
|
20
|
+
# per element; writers install a private array first. Inherited
|
|
21
|
+
# namespace nodes arrive as one shared frozen array from
|
|
22
|
+
# TreeBuilder#attach_namespace_scope.
|
|
23
|
+
EMPTY_ATTRIBUTE_NODES = [].freeze
|
|
24
|
+
EMPTY_NAMESPACE_NODES = [].freeze
|
|
25
|
+
|
|
22
26
|
def namespace_nodes
|
|
23
|
-
@namespace_nodes
|
|
27
|
+
@namespace_nodes || EMPTY_NAMESPACE_NODES
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def namespace_nodes=(nodes)
|
|
@@ -28,7 +32,7 @@ module Canon
|
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def attribute_nodes
|
|
31
|
-
@attribute_nodes
|
|
35
|
+
@attribute_nodes || EMPTY_ATTRIBUTE_NODES
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def node_type
|
|
@@ -41,12 +45,12 @@ module Canon
|
|
|
41
45
|
|
|
42
46
|
def add_namespace(namespace_node)
|
|
43
47
|
namespace_node.parent = self
|
|
44
|
-
namespace_nodes << namespace_node
|
|
48
|
+
(@namespace_nodes ||= []) << namespace_node
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def add_attribute(attribute_node)
|
|
48
52
|
attribute_node.parent = self
|
|
49
|
-
attribute_nodes << attribute_node
|
|
53
|
+
(@attribute_nodes ||= []) << attribute_node
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
# Get namespace nodes in sorted order (lexicographically by local name)
|
|
@@ -22,6 +22,10 @@ module Canon
|
|
|
22
22
|
# root = SaxBuilder.parse(xml_string, strip_doctype: true)
|
|
23
23
|
#
|
|
24
24
|
class SaxBuilder
|
|
25
|
+
# Shared empties for the no-declaration common case: both
|
|
26
|
+
# consumers only iterate them.
|
|
27
|
+
NO_NAMESPACE_DECLS = [].freeze
|
|
28
|
+
EMPTY_NS_HASH = {}.freeze
|
|
25
29
|
# Parse XML string and return Canon::Xml::Node tree
|
|
26
30
|
#
|
|
27
31
|
# @param xml_string [String] XML content to parse
|
|
@@ -110,6 +114,31 @@ strip_doctype: false)
|
|
|
110
114
|
def start_element(name, attrs = [])
|
|
111
115
|
parent = @stack.last
|
|
112
116
|
|
|
117
|
+
# Fast path: attribute-less elements (the bulk of most
|
|
118
|
+
# documents) declare no namespaces — no separation, no hash,
|
|
119
|
+
# no qname pair array; the inherited scope object is pushed
|
|
120
|
+
# as-is (shared scope → cached namespace-node array).
|
|
121
|
+
if attrs.empty?
|
|
122
|
+
if (colon = name.index(":"))
|
|
123
|
+
prefix = name[0...colon]
|
|
124
|
+
local_name = name[(colon + 1)..]
|
|
125
|
+
else
|
|
126
|
+
prefix = nil
|
|
127
|
+
local_name = name
|
|
128
|
+
end
|
|
129
|
+
new_scope = @namespace_stack.last
|
|
130
|
+
@namespace_stack.push(new_scope)
|
|
131
|
+
element = TreeBuilder::DEFAULT.element(
|
|
132
|
+
name: local_name,
|
|
133
|
+
prefix: prefix,
|
|
134
|
+
namespace_uri: new_scope[prefix.to_s],
|
|
135
|
+
namespace_scope: new_scope,
|
|
136
|
+
)
|
|
137
|
+
parent.add_child(element)
|
|
138
|
+
@stack.push(element)
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
113
142
|
# Parse namespace from name (prefix:localname or just localname)
|
|
114
143
|
prefix, local_name = parse_qname(name)
|
|
115
144
|
|
|
@@ -280,6 +309,8 @@ strip_doctype: false)
|
|
|
280
309
|
# @param ns_decls [Array] Array of [name, value] pairs for namespace declarations
|
|
281
310
|
# @return [Hash] Namespace prefix => URI mapping
|
|
282
311
|
def build_ns_hash(ns_decls)
|
|
312
|
+
return EMPTY_NS_HASH if ns_decls.empty?
|
|
313
|
+
|
|
283
314
|
result = {}
|
|
284
315
|
ns_decls.each do |name, uri|
|
|
285
316
|
# xmlns="..." for default namespace, xmlns:prefix="..." for prefixed
|
|
@@ -311,6 +342,12 @@ strip_doctype: false)
|
|
|
311
342
|
# @param attrs [Array] Array of [name, value] pairs
|
|
312
343
|
# @return [Array] Two arrays: [namespace_decls, regular_attrs]
|
|
313
344
|
def separate_namespaces(attrs)
|
|
345
|
+
# Fast path: no xmlns declarations — the attribute pairs pass
|
|
346
|
+
# through untouched and nothing is copied.
|
|
347
|
+
unless attrs.any? { |name, _| name == "xmlns" || name.start_with?("xmlns:") }
|
|
348
|
+
return [NO_NAMESPACE_DECLS, attrs]
|
|
349
|
+
end
|
|
350
|
+
|
|
314
351
|
ns_decls = []
|
|
315
352
|
regular_attrs = []
|
|
316
353
|
|