canon 0.3.121 → 0.3.123
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.rb +56 -9
- data/lib/canon/pretty_printer/html.rb +6 -16
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/xpath_engine.rb +109 -30
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e746e8d1590e8be637395d2c68cb17a8b5aa9c742cbdc2c08d2e9becb42164a4
|
|
4
|
+
data.tar.gz: 8b0cefd9a990836130415fa12491628d992746d9521f00d5768c0c39717fc09c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 718f08d011e9df8965873679f6e0332b6dd871682257f73dc20046fd1138db5a279719b605833fdc339b05785a0f950337c69d9067eb446a7b4b1b9e468f2bfb
|
|
7
|
+
data.tar.gz: 0a874b40f3f194cd0998f7d01f59710b613f7b044ab7a17357d0fdc8877e71b4ae7058abe3a5d6705450dadb5c3ca93fa20c4f3437ca8c1f7e51aaf36a920af5
|
data/lib/canon/comparison.rb
CHANGED
|
@@ -646,6 +646,13 @@ module Canon
|
|
|
646
646
|
return array_diff(obj1, obj2, resolved)
|
|
647
647
|
end
|
|
648
648
|
|
|
649
|
+
# Hashes (metadata dumps the equivalent-xml matcher compared
|
|
650
|
+
# generically): key-wise deep comparison through the normal
|
|
651
|
+
# pipeline.
|
|
652
|
+
if obj1.is_a?(Hash) && obj2.is_a?(Hash)
|
|
653
|
+
return hash_diff(obj1, obj2, resolved)
|
|
654
|
+
end
|
|
655
|
+
|
|
649
656
|
# Detect formats (with explicit hint) and pre-parse HTML strings
|
|
650
657
|
# through Nokogiri::HTML5 so html4 and html5 share HTML's
|
|
651
658
|
# whitespace-sensitivity semantics (issue #118). Pre-parsing
|
|
@@ -699,29 +706,69 @@ module Canon
|
|
|
699
706
|
# @param format1 [Symbol]
|
|
700
707
|
# @param format2 [Symbol]
|
|
701
708
|
# @return [Symbol]
|
|
709
|
+
SCALAR_LEAVES = [String, Symbol, Numeric, NilClass,
|
|
710
|
+
TrueClass, FalseClass].freeze
|
|
711
|
+
|
|
712
|
+
def scalar_leaf?(obj)
|
|
713
|
+
SCALAR_LEAVES.any? { |k| obj.is_a?(k) }
|
|
714
|
+
end
|
|
715
|
+
|
|
702
716
|
def array_diff(arr1, arr2, opts = {})
|
|
703
717
|
verbose = opts[:verbose]
|
|
704
718
|
if arr1.size != arr2.size
|
|
705
719
|
return verbose ? [:size_mismatch] : false
|
|
706
720
|
end
|
|
707
721
|
|
|
708
|
-
# Elements are extracted values, not documents:
|
|
709
|
-
# literally (a matcher format hint must not turn "ref1"
|
|
710
|
-
# XML fragment, where every bare string parses equal
|
|
711
|
-
#
|
|
722
|
+
# Elements are extracted values, not documents: scalar leaves
|
|
723
|
+
# compare literally (a matcher format hint must not turn "ref1"
|
|
724
|
+
# into an XML fragment, where every bare string parses equal,
|
|
725
|
+
# and the detector has no format for numbers at all); containers
|
|
726
|
+
# and nodes recurse through the normal pipeline without the
|
|
727
|
+
# format hint.
|
|
728
|
+
scalar_opts = opts.except(:verbose, :format)
|
|
729
|
+
diffs = []
|
|
730
|
+
arr1.zip(arr2).each_with_index do |(element_before, element_after), i|
|
|
731
|
+
eq = if scalar_leaf?(element_before) && scalar_leaf?(element_after)
|
|
732
|
+
element_before == element_after
|
|
733
|
+
else
|
|
734
|
+
equivalent?(element_before, element_after, scalar_opts)
|
|
735
|
+
end
|
|
736
|
+
next if eq == true
|
|
737
|
+
|
|
738
|
+
return false unless verbose
|
|
739
|
+
|
|
740
|
+
diffs << leaf_diff(i.to_s, element_before, element_after)
|
|
741
|
+
end
|
|
742
|
+
verbose ? diffs : diffs.empty?
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Verbose ruby-object entries use the diff-code shape the
|
|
746
|
+
# DiffFormatter's by-object renderers consume.
|
|
747
|
+
def leaf_diff(path, value_before, value_after)
|
|
748
|
+
{ path: path, diff_code: UNEQUAL_PRIMITIVES,
|
|
749
|
+
value1: value_before, value2: value_after }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def hash_diff(hash1, hash2, opts = {})
|
|
753
|
+
verbose = opts[:verbose]
|
|
754
|
+
if hash1.keys.sort != hash2.keys.sort
|
|
755
|
+
return verbose ? [:key_mismatch] : false
|
|
756
|
+
end
|
|
757
|
+
|
|
712
758
|
scalar_opts = opts.except(:verbose, :format)
|
|
713
759
|
diffs = []
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
760
|
+
hash1.each do |key, value_before|
|
|
761
|
+
value_after = hash2[key]
|
|
762
|
+
eq = if scalar_leaf?(value_before) && scalar_leaf?(value_after)
|
|
763
|
+
value_before == value_after
|
|
717
764
|
else
|
|
718
|
-
equivalent?(
|
|
765
|
+
equivalent?(value_before, value_after, scalar_opts)
|
|
719
766
|
end
|
|
720
767
|
next if eq == true
|
|
721
768
|
|
|
722
769
|
return false unless verbose
|
|
723
770
|
|
|
724
|
-
diffs <<
|
|
771
|
+
diffs << leaf_diff(key.to_s, value_before, value_after)
|
|
725
772
|
end
|
|
726
773
|
verbose ? diffs : diffs.empty?
|
|
727
774
|
end
|
|
@@ -36,14 +36,10 @@ module Canon
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def format(html_string)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# served.
|
|
44
|
-
Canon::NokogiriLoader.require!("HTML fixture-ready display")
|
|
45
|
-
return format_fixture_ready(html_string)
|
|
46
|
-
end
|
|
39
|
+
# The fixture-ready display lane is engine-served end to end
|
|
40
|
+
# (moxml 0.6.2's XHTML serialization mode — moxml#390): no
|
|
41
|
+
# Nokogiri requirement remains in this mode.
|
|
42
|
+
return format_fixture_ready(html_string) if @fixture_ready
|
|
47
43
|
|
|
48
44
|
if xhtml?(html_string)
|
|
49
45
|
format_as_xhtml(html_string)
|
|
@@ -91,14 +87,8 @@ module Canon
|
|
|
91
87
|
end
|
|
92
88
|
end
|
|
93
89
|
|
|
94
|
-
# Fixture-ready serialisation
|
|
95
|
-
#
|
|
96
|
-
# input shapes), then write through libxml's XML writer with
|
|
97
|
-
# +FORMAT+ + +AS_XHTML+ + +NO_DECLARATION+. +FORMAT+ inserts
|
|
98
|
-
# indentation; +AS_XHTML+ produces well-shaped output (void
|
|
99
|
-
# elements self-closed, non-void paired); +NO_DECLARATION+
|
|
100
|
-
# suppresses the +<?xml ...?>+ prefix.
|
|
101
|
-
# Engine parse + significance-aware strip + the engine's
|
|
90
|
+
# Fixture-ready serialisation, engine-served end to end:
|
|
91
|
+
# engine HTML parse + significance-aware strip + moxml 0.6.2's
|
|
102
92
|
# XHTML serialization mode (moxml#390 delivered to_xhtml /
|
|
103
93
|
# to_html(xhtml: true)): void elements self-closed, non-void
|
|
104
94
|
# paired — the AS_XHTML contract, engine-served.
|
data/lib/canon/version.rb
CHANGED
|
@@ -14,9 +14,14 @@ module Canon
|
|
|
14
14
|
# - Predicates: [1] (position), [@attr], [@attr='value']
|
|
15
15
|
# - Wildcards: *
|
|
16
16
|
# - Union: expr1 | expr2
|
|
17
|
+
# - Self and parent steps: ., ..
|
|
18
|
+
# - Attribute steps: @attr (selects the attribute node; per C14N
|
|
19
|
+
# law it renders only when its owner element is in the node-set,
|
|
20
|
+
# so @attr/.. is the productive form — XPath 1.0 section 2.2
|
|
21
|
+
# names the owner element)
|
|
17
22
|
#
|
|
18
23
|
# Not supported (not needed for C14N subset):
|
|
19
|
-
# - Axes other than child
|
|
24
|
+
# - Axes other than child, parent, self, descendant-or-self
|
|
20
25
|
# - Functions (last(), position(), etc.)
|
|
21
26
|
# - Variables
|
|
22
27
|
#
|
|
@@ -56,6 +61,10 @@ module Canon
|
|
|
56
61
|
elsif xpath.start_with?("/")
|
|
57
62
|
# Absolute path
|
|
58
63
|
evaluate_absolute(xpath[1..])
|
|
64
|
+
elsif xpath.start_with?("./")
|
|
65
|
+
# Document-rooted self step: the subset context is the
|
|
66
|
+
# document, so ./rest resolves from the root like /rest
|
|
67
|
+
evaluate_absolute(xpath[2..])
|
|
59
68
|
else
|
|
60
69
|
# Relative path — treat as descendant
|
|
61
70
|
evaluate_descendant(xpath)
|
|
@@ -68,35 +77,47 @@ module Canon
|
|
|
68
77
|
steps = parse_steps(path)
|
|
69
78
|
return [] if steps.empty?
|
|
70
79
|
|
|
71
|
-
# Start from root's children
|
|
72
|
-
|
|
73
|
-
apply_steps(
|
|
80
|
+
# Start from root's children; the first element step filters
|
|
81
|
+
# this entry set directly
|
|
82
|
+
apply_steps(@root.children, steps, descend_first: false)
|
|
74
83
|
end
|
|
75
84
|
|
|
76
85
|
def evaluate_descendant(path)
|
|
77
86
|
steps = parse_steps(path)
|
|
78
87
|
return [] if steps.empty?
|
|
79
88
|
|
|
89
|
+
# ./ is transparent on the relative law: the subset context is
|
|
90
|
+
# the document, so ./rest searches descendants like rest
|
|
91
|
+
steps.shift while steps.first&.dig(:type) == :self
|
|
92
|
+
return [] if steps.empty?
|
|
93
|
+
|
|
80
94
|
# Collect all descendant element nodes
|
|
81
95
|
all_elements = []
|
|
82
96
|
collect_elements(@root, all_elements)
|
|
83
97
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
all_elements.each do |element|
|
|
87
|
-
first_step = steps.first
|
|
88
|
-
next unless step_matches?(element, first_step)
|
|
98
|
+
first_step = steps.first
|
|
99
|
+
remaining = steps[1..]
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
context =
|
|
102
|
+
case first_step[:type]
|
|
103
|
+
when :attribute
|
|
104
|
+
# //@attr — every matching attribute in the tree
|
|
105
|
+
all_elements.flat_map do |element|
|
|
106
|
+
attribute_matches(element, first_step)
|
|
107
|
+
end
|
|
108
|
+
when :parent
|
|
109
|
+
return [] # no meaningful parents above descendants
|
|
92
110
|
else
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
all_elements.select do |element|
|
|
112
|
+
step_matches?(element, first_step)
|
|
113
|
+
end
|
|
96
114
|
end
|
|
97
|
-
|
|
115
|
+
return context.uniq if remaining.empty?
|
|
98
116
|
|
|
99
|
-
|
|
117
|
+
# The remaining steps run from the matches: element steps
|
|
118
|
+
# descend (block semantics), axis steps apply to the nodes
|
|
119
|
+
# themselves
|
|
120
|
+
apply_steps(context, remaining, descend_first: true).uniq
|
|
100
121
|
end
|
|
101
122
|
|
|
102
123
|
def collect_elements(node, result)
|
|
@@ -108,20 +129,54 @@ module Canon
|
|
|
108
129
|
end
|
|
109
130
|
end
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
# Linear context model: each step transforms the current node
|
|
133
|
+
# set. Element steps filter one level down from the context
|
|
134
|
+
# (block semantics — the first step filters the entry set
|
|
135
|
+
# itself); parent/self/attribute steps are context steps and
|
|
136
|
+
# operate on the nodes themselves.
|
|
137
|
+
def apply_steps(nodes, steps, descend_first:)
|
|
138
|
+
steps.reduce(nodes) do |current, step|
|
|
139
|
+
break [] if current.empty?
|
|
140
|
+
|
|
141
|
+
if element_step?(step)
|
|
142
|
+
level = descend_first ? descend(current) : current
|
|
143
|
+
current = level.select { |n| step_matches?(n, step) }.uniq
|
|
144
|
+
else
|
|
145
|
+
current = axis_apply(current, step)
|
|
146
|
+
end
|
|
147
|
+
descend_first = true
|
|
148
|
+
current
|
|
149
|
+
end
|
|
150
|
+
end
|
|
113
151
|
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
def element_step?(step)
|
|
153
|
+
step[:type] == :element
|
|
154
|
+
end
|
|
116
155
|
|
|
117
|
-
|
|
156
|
+
def descend(nodes)
|
|
157
|
+
nodes.flat_map do |n|
|
|
158
|
+
next [] if n.is_a?(Nodes::AttributeNode)
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
n.children
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Context steps: navigate from the nodes themselves.
|
|
165
|
+
def axis_apply(nodes, step)
|
|
166
|
+
case step[:type]
|
|
167
|
+
when :parent
|
|
168
|
+
nodes.filter_map(&:parent).uniq
|
|
169
|
+
when :self
|
|
170
|
+
nodes
|
|
171
|
+
when :attribute
|
|
172
|
+
nodes.grep(Nodes::ElementNode)
|
|
173
|
+
.flat_map { |el| attribute_matches(el, step) }.uniq
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def attribute_matches(element, step)
|
|
178
|
+
element.attribute_nodes.select do |a|
|
|
179
|
+
attr_name_matches?(a, step[:name])
|
|
125
180
|
end
|
|
126
181
|
end
|
|
127
182
|
|
|
@@ -132,6 +187,17 @@ module Canon
|
|
|
132
187
|
predicates_match?(node, step[:predicates])
|
|
133
188
|
end
|
|
134
189
|
|
|
190
|
+
def attr_name_matches?(attr, name)
|
|
191
|
+
return true if name == "*"
|
|
192
|
+
|
|
193
|
+
if name.include?(":")
|
|
194
|
+
prefix, local = name.split(":", 2)
|
|
195
|
+
attr.prefix == prefix && attr.name == local
|
|
196
|
+
else
|
|
197
|
+
attr.name == name
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
135
201
|
def name_matches?(node, name)
|
|
136
202
|
return true if name == "*"
|
|
137
203
|
|
|
@@ -179,7 +245,7 @@ module Canon
|
|
|
179
245
|
# Parse a path string into an array of steps.
|
|
180
246
|
#
|
|
181
247
|
# @param path [String] XPath path (without leading /)
|
|
182
|
-
# @return [Array<Hash>] Array of { name:, predicates: }
|
|
248
|
+
# @return [Array<Hash>] Array of { type:, name:, predicates: }
|
|
183
249
|
def parse_steps(path)
|
|
184
250
|
steps = []
|
|
185
251
|
scanner = StringScanner.new(path)
|
|
@@ -189,14 +255,27 @@ module Canon
|
|
|
189
255
|
break if scanner.eos?
|
|
190
256
|
|
|
191
257
|
# Skip /
|
|
192
|
-
scanner.
|
|
258
|
+
scanner.skip(%r{/})
|
|
259
|
+
scanner.skip(/\s+/)
|
|
260
|
+
|
|
261
|
+
if scanner.scan("..")
|
|
262
|
+
steps << { type: :parent, name: nil, predicates: [] }
|
|
263
|
+
next
|
|
264
|
+
elsif scanner.scan(".")
|
|
265
|
+
steps << { type: :self, name: nil, predicates: [] }
|
|
266
|
+
next
|
|
267
|
+
elsif scanner.scan(/@/)
|
|
268
|
+
name = scanner.scan(/[a-zA-Z_][\w.-]*/)
|
|
269
|
+
steps << { type: :attribute, name: name, predicates: [] }
|
|
270
|
+
next
|
|
271
|
+
end
|
|
193
272
|
|
|
194
273
|
name = scan_name(scanner)
|
|
195
274
|
break if name.nil?
|
|
196
275
|
|
|
197
276
|
predicates = scan_predicates(scanner)
|
|
198
277
|
|
|
199
|
-
steps << { name: name, predicates: predicates }
|
|
278
|
+
steps << { type: :element, name: name, predicates: predicates }
|
|
200
279
|
end
|
|
201
280
|
|
|
202
281
|
steps
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.123
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.6.
|
|
61
|
+
version: 0.6.2
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.6.
|
|
68
|
+
version: 0.6.2
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: paint
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|