compare-xml 0.66 → 1.0.1
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/README.md +108 -47
- data/lib/compare-xml/version.rb +1 -1
- data/lib/compare-xml.rb +227 -140
- metadata +17 -27
- data/.gitignore +0 -13
- data/.rubocop.yml +0 -45
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/compare-xml.gemspec +0 -22
- data/img/diffing.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7e6709bb5f5150832a22634027282fb7a2e80571f0a5c8210f7f8982f59bb67
|
|
4
|
+
data.tar.gz: ba0a234e1c095d4d0a97b72c22a8e6831d53b2203acf860965602dc81bdfbb85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2e9309f2980da1b6faa1e1ecea38293654492e04f216b1aa45e53c93817f2b394811511b4b233a8e74e3d498f6f71df0020b4d55635b5c95b6314aea7701f5b
|
|
7
|
+
data.tar.gz: ac6934cf5624163ea039cdd3985f07b28bd7f60e7f77e9329b70343d87b48e237b790e380316294293073a8d40de8f111d6f44addfb39a477d6b3f5389332f61
|
data/README.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# CompareXML
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/compare-xml)
|
|
4
|
+
[](https://github.com/vkononov/compare-xml/blob/main/.github/workflows/test.yml)
|
|
5
|
+
[](https://github.com/vkononov/compare-xml/actions/workflows/test.yml)
|
|
6
|
+
[](https://github.com/vkononov/compare-xml/actions/workflows/lint.yml)
|
|
7
|
+
[](https://opensource.org/license/MIT)
|
|
4
8
|
|
|
5
|
-
CompareXML is a fast, lightweight and feature-rich tool that will solve your XML/HTML comparison or diffing needs.
|
|
9
|
+
CompareXML is a fast, lightweight and feature-rich tool that will solve your XML/HTML comparison or diffing needs. Its purpose is to compare two instances of `Nokogiri::XML::Node` or `Nokogiri::XML::NodeSet` for equality or equivalency.
|
|
10
|
+
|
|
11
|
+
Supports **Ruby 2.4 → latest**, with the full Ruby matrix tested daily in CI.
|
|
12
|
+
|
|
13
|
+
[](https://www.buymeacoffee.com/vkononov)
|
|
6
14
|
|
|
7
15
|
**Features**
|
|
8
16
|
|
|
@@ -44,8 +52,8 @@ where `doc1` and `doc2` are instances of `Nokogiri::XML::Node` or `Nokogiri::XML
|
|
|
44
52
|
Suppose you have two files `1.html` and `2.html` that you would like to compare. You could do it as follows:
|
|
45
53
|
|
|
46
54
|
```ruby
|
|
47
|
-
doc1 = Nokogiri::HTML(
|
|
48
|
-
doc2 = Nokogiri::HTML(
|
|
55
|
+
doc1 = Nokogiri::HTML(File.read('1.html'))
|
|
56
|
+
doc2 = Nokogiri::HTML(File.read('2.html'))
|
|
49
57
|
puts CompareXML.equivalent?(doc1, doc2)
|
|
50
58
|
```
|
|
51
59
|
|
|
@@ -63,7 +71,7 @@ require 'compare-xml'
|
|
|
63
71
|
CompareXML has a variety of options that can be invoked as an optional argument, e.g.:
|
|
64
72
|
|
|
65
73
|
```ruby
|
|
66
|
-
CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true
|
|
74
|
+
CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
|
|
67
75
|
```
|
|
68
76
|
|
|
69
77
|
- `collapse_whitespace: {true|false}` default: **`true`** [show examples ⇨](#collapse_whitespace)
|
|
@@ -76,16 +84,16 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
76
84
|
- when provided, ignores all attributes that contain substrings `string`, `string2`, etc.
|
|
77
85
|
|
|
78
86
|
- `ignore_attrs: [css_selector1, css_selector1, ...]` default: **`[]`** [show examples ⇨](#ignore_attrs)
|
|
79
|
-
- when provided, ignores specific *attributes* using [CSS selectors](
|
|
87
|
+
- when provided, ignores specific *attributes* using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php)
|
|
80
88
|
|
|
81
|
-
- `ignore_attrs_by_name: [
|
|
82
|
-
- when provided, ignores
|
|
89
|
+
- `ignore_attrs_by_name: [matcher1, matcher2, ...]` default: **`[]`** [show examples ⇨](#ignore_attrs_by_name)
|
|
90
|
+
- when provided, ignores *attributes* whose name matches a `String` (exact) or `Regexp` (pattern)
|
|
83
91
|
|
|
84
92
|
- `ignore_comments: {true|false}` default: **`true`** [show examples ⇨](#ignore_comments)
|
|
85
93
|
- when `true`, ignores comments, such as `<!-- comment -->`
|
|
86
94
|
|
|
87
95
|
- `ignore_nodes: [css_selector1, css_selector1, ...]` default: **`[]`** [show examples ⇨](#ignore_nodes)
|
|
88
|
-
- when provided, ignores specific *nodes* using [CSS selectors](
|
|
96
|
+
- when provided, ignores specific *nodes* using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php)
|
|
89
97
|
|
|
90
98
|
- `ignore_text_nodes: {true|false}` default: **`false`** [show examples ⇨](#ignore_text_nodes)
|
|
91
99
|
- when `true`, ignores all text content within a document
|
|
@@ -99,6 +107,9 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
99
107
|
- `force_children {true|false}` default **`false`** [show examples ⇨](#force_children)
|
|
100
108
|
- when `true`, the subnodes of a node are checked independently of the status of the parent node
|
|
101
109
|
|
|
110
|
+
- `align_children {true|false}` default **`false`** [show examples ⇨](#align_children)
|
|
111
|
+
- when `true` (in `verbose` mode), aligns child nodes so insertions and removals are reported as additions/removals instead of changes
|
|
112
|
+
|
|
102
113
|
|
|
103
114
|
## Options in Depth
|
|
104
115
|
|
|
@@ -180,7 +191,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
180
191
|
|
|
181
192
|
- <a id="ignore_attrs"></a>`ignore_attrs: [css_selector1, css_selector1, ...]` default: **`[]`**
|
|
182
193
|
|
|
183
|
-
When provided, ignores all **attributes** that satisfy a particular rule using [CSS selectors](
|
|
194
|
+
When provided, ignores all **attributes** that satisfy a particular rule using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php).
|
|
184
195
|
|
|
185
196
|
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_attrs: ['a[rel="nofollow"]', 'input[type="hidden"']})`
|
|
186
197
|
|
|
@@ -196,17 +207,27 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
196
207
|
|
|
197
208
|
----------
|
|
198
209
|
|
|
199
|
-
- <a id="ignore_attrs_by_name"></a>`ignore_attrs_by_name: [
|
|
210
|
+
- <a id="ignore_attrs_by_name"></a>`ignore_attrs_by_name: [matcher1, matcher2, ...]` default: **`[]`**
|
|
200
211
|
|
|
201
|
-
When provided, ignores all **attributes**
|
|
212
|
+
When provided, ignores all **attributes** whose name matches one of the given matchers. A `String` matches the attribute name exactly, while a `Regexp` matches it as a pattern.
|
|
202
213
|
|
|
203
|
-
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_attrs_by_name: ['target'])`
|
|
214
|
+
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_attrs_by_name: ['target', /^data-/]})`
|
|
204
215
|
|
|
205
216
|
**Example:** With `ignore_attrs_by_name: ['target', 'rel']` the following HTML strings are considered equal:
|
|
206
217
|
|
|
207
218
|
<a href="/admin" class="button" target="_blank">Link</a>
|
|
208
219
|
<a href="/admin" class="button" target="_self" rel="nofollow">Link</a>
|
|
209
220
|
|
|
221
|
+
**Example:** With `ignore_attrs_by_name: [/^data-/]` the following HTML strings are considered equal:
|
|
222
|
+
|
|
223
|
+
<div data-id="1" data-role="row">Link</div>
|
|
224
|
+
<div data-id="2" data-role="cell">Link</div>
|
|
225
|
+
|
|
226
|
+
An ignored attribute does not need to be present on both elements. With `ignore_attrs_by_name: ['class']` the following HTML strings are considered equal:
|
|
227
|
+
|
|
228
|
+
<div class="foo"></div>
|
|
229
|
+
<div></div>
|
|
230
|
+
|
|
210
231
|
----------
|
|
211
232
|
|
|
212
233
|
|
|
@@ -232,7 +253,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
232
253
|
|
|
233
254
|
- <a id="ignore_nodes"></a>`ignore_nodes: [css_selector1, css_selector1, ...]` default: **`[]`**
|
|
234
255
|
|
|
235
|
-
When provided, ignores all **nodes** that satisfy a particular rule using [CSS selectors](
|
|
256
|
+
When provided, ignores all **nodes** that satisfy a particular rule using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php).
|
|
236
257
|
|
|
237
258
|
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_nodes: ['script', 'object']})`
|
|
238
259
|
|
|
@@ -280,42 +301,42 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
280
301
|
|
|
281
302
|
**Example:** When `true` given the following HTML strings:
|
|
282
303
|
|
|
283
|
-

|
|
284
305
|
|
|
285
306
|
`CompareXML.equivalent?(doc1, doc2, {verbose: true})` will produce an array shown below.
|
|
286
307
|
|
|
287
308
|
```ruby
|
|
288
309
|
[
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
310
|
+
{
|
|
311
|
+
node1: '<title>TITLE</title>',
|
|
312
|
+
node2: '<title>ANOTHER TITLE</title>',
|
|
313
|
+
diff1: 'TITLE',
|
|
314
|
+
diff2: 'ANOTHER TITLE'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
node1: '<h1>SOME HEADING</h1>',
|
|
318
|
+
node2: '<h1 id="main">SOME HEADING</h1>',
|
|
319
|
+
diff1: nil,
|
|
320
|
+
diff2: 'id="main"'
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
node1: '<a href="/admin" rel="icon">Link</a>',
|
|
324
|
+
node2: '<a rel="button" href="/admin">Link</a>',
|
|
325
|
+
diff1: '"rel="icon"',
|
|
326
|
+
diff2: '"rel="button"'
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
node1: '<cite>Author Name</cite>',
|
|
330
|
+
node2: nil,
|
|
331
|
+
diff1: '<cite>Author Name</cite>',
|
|
332
|
+
diff2: nil
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
node1: '<p class="footer">FOOTER</p>',
|
|
336
|
+
node2: '<div class="footer">FOOTER</div>',
|
|
337
|
+
diff1: 'p',
|
|
338
|
+
diff2: 'div'
|
|
339
|
+
}
|
|
319
340
|
]
|
|
320
341
|
```
|
|
321
342
|
|
|
@@ -345,7 +366,47 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true, .
|
|
|
345
366
|
|
|
346
367
|
When provided, compares all **subnodes** of any node.
|
|
347
368
|
|
|
348
|
-
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {
|
|
369
|
+
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {force_children: true})`
|
|
370
|
+
|
|
371
|
+
----------
|
|
372
|
+
|
|
373
|
+
- <a id="align_children"></a>`align_children: {true|false}` default: **`false`**
|
|
374
|
+
|
|
375
|
+
Only takes effect in `verbose` mode. When `true`, sibling nodes are aligned before comparison so that an inserted or removed node is reported as an addition or removal (with one side `nil`) rather than causing the remaining siblings to be reported as a series of changes. Identical subtrees are matched as anchors, and the unmatched nodes between them are compared in order.
|
|
376
|
+
|
|
377
|
+
**Usage Example:** `CompareXML.equivalent?(doc1, doc2, {verbose: true, align_children: true})`
|
|
378
|
+
|
|
379
|
+
**Example:** Given a left document with three children and a right document with only the third:
|
|
380
|
+
|
|
381
|
+
<properties><a/><b/><c/></properties>
|
|
382
|
+
<properties><c/></properties>
|
|
383
|
+
|
|
384
|
+
With `align_children: true`, `<a>` and `<b>` are reported as removals (`node2` is `nil`); without it, they collapse into a single positional change.
|
|
385
|
+
|
|
386
|
+
----------
|
|
387
|
+
|
|
388
|
+
## Separate Options for Child Nodes
|
|
389
|
+
|
|
390
|
+
`CompareXML.equivalent?` accepts two further optional arguments that let child nodes be compared under a different set of options than the root:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
CompareXML.equivalent?(doc1, doc2, opts = {}, child_opts = {}, diff_children = false)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- `child_opts` - a second options hash, merged over the defaults the same way as `opts`, applied to child nodes
|
|
397
|
+
- `diff_children` - when `true`, child nodes are compared using `child_opts` instead of `opts`
|
|
398
|
+
|
|
399
|
+
`child_opts` is ignored unless `diff_children` is `true`.
|
|
400
|
+
|
|
401
|
+
**Example:** compare the root strictly, but ignore text content within child nodes:
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
doc1 = Nokogiri::HTML('<html><body><p>Hello</p></body></html>')
|
|
405
|
+
doc2 = Nokogiri::HTML('<html><body><p>Goodbye</p></body></html>')
|
|
406
|
+
|
|
407
|
+
CompareXML.equivalent?(doc1, doc2) # => false
|
|
408
|
+
CompareXML.equivalent?(doc1, doc2, {}, { ignore_text_nodes: true }, true) # => true
|
|
409
|
+
```
|
|
349
410
|
|
|
350
411
|
----------
|
|
351
412
|
|
|
@@ -367,4 +428,4 @@ This gem was inspired by [Michael B. Klein](https://github.com/mbklein)'s gem [`
|
|
|
367
428
|
|
|
368
429
|
## License
|
|
369
430
|
|
|
370
|
-
The gem is available as open source under the terms of the [MIT License](
|
|
431
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
|
data/lib/compare-xml/version.rb
CHANGED
data/lib/compare-xml.rb
CHANGED
|
@@ -16,6 +16,10 @@ module CompareXML
|
|
|
16
16
|
# when false, children of elements are not compared if the root is different
|
|
17
17
|
force_children: false,
|
|
18
18
|
|
|
19
|
+
# when true (verbose mode only), child nodes are aligned before being compared so that
|
|
20
|
+
# inserted or removed nodes are reported as additions/removals instead of as changes
|
|
21
|
+
align_children: false,
|
|
22
|
+
|
|
19
23
|
# when true, children of elements are never compared
|
|
20
24
|
# when false, children of elements are compared if root is different or see force_children
|
|
21
25
|
ignore_children: false,
|
|
@@ -72,16 +76,16 @@ module CompareXML
|
|
|
72
76
|
# @param [Nokogiri::XML::Element] n1 left node element
|
|
73
77
|
# @param [Nokogiri::XML::Element] n2 right node element
|
|
74
78
|
# @param [Hash] opts user-overridden options
|
|
75
|
-
# @param [Hash]
|
|
76
|
-
# @param [Bool]
|
|
79
|
+
# @param [Hash] child_opts user-overridden options used for the child nodes
|
|
80
|
+
# @param [Bool] diff_children use different options for the child nodes
|
|
77
81
|
#
|
|
78
82
|
# @return true if equal, [Array] differences otherwise
|
|
79
83
|
#
|
|
80
|
-
def equivalent?(n1, n2, opts = {},
|
|
84
|
+
def equivalent?(n1, n2, opts = {}, child_opts = {}, diff_children = false)
|
|
81
85
|
opts = DEFAULTS_OPTS.merge(opts)
|
|
82
|
-
|
|
86
|
+
child_opts = DEFAULTS_OPTS.merge(child_opts)
|
|
83
87
|
differences = []
|
|
84
|
-
result =
|
|
88
|
+
result = diff_children ? compare_nodes(n1, n2, opts, differences, child_opts, diff_children: diff_children) : compare_nodes(n1, n2, opts, differences)
|
|
85
89
|
opts[:verbose] ? differences : result == EQUIVALENT
|
|
86
90
|
end
|
|
87
91
|
|
|
@@ -91,42 +95,43 @@ module CompareXML
|
|
|
91
95
|
# Compares two nodes for equivalence. The nodes could be any subclass
|
|
92
96
|
# of Nokogiri::XML::Node including node sets and document fragments.
|
|
93
97
|
#
|
|
94
|
-
# @param [Nokogiri::XML::
|
|
95
|
-
# @param [Nokogiri::XML::
|
|
98
|
+
# @param [Nokogiri::XML::Element] n1 left node
|
|
99
|
+
# @param [Nokogiri::XML::Element] n2 right node
|
|
96
100
|
# @param [Hash] opts user-overridden options
|
|
97
101
|
# @param [Array] differences inequivalence messages
|
|
98
|
-
# @param [Hash]
|
|
99
|
-
# @param [Bool]
|
|
100
|
-
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
102
|
+
# @param [Hash] child_opts user-overridden options used for the child nodes
|
|
103
|
+
# @param [Bool] diff_children use different options for the child nodes
|
|
101
104
|
#
|
|
102
105
|
# @return type of equivalence (from equivalence constants)
|
|
103
106
|
#
|
|
104
|
-
def
|
|
105
|
-
if n1.
|
|
107
|
+
def compare_nodes(n1, n2, opts, differences, child_opts = {}, diff_children: false)
|
|
108
|
+
if n1.instance_of?(n2.class)
|
|
106
109
|
case n1
|
|
107
110
|
when Nokogiri::XML::Comment
|
|
108
|
-
status =
|
|
111
|
+
status = compare_comment_nodes(n1, n2, opts, differences)
|
|
109
112
|
when Nokogiri::HTML::Document
|
|
110
|
-
status =
|
|
113
|
+
status = diff_children ? compare_document_nodes(n1, n2, opts, differences, child_opts, diff_children: diff_children) : compare_document_nodes(n1, n2, opts, differences)
|
|
111
114
|
when Nokogiri::XML::Element
|
|
112
|
-
status =
|
|
115
|
+
status = diff_children ? compare_element_nodes(n1, n2, opts, differences, child_opts, diff_children: diff_children) : compare_element_nodes(n1, n2, opts, differences)
|
|
113
116
|
when Nokogiri::XML::Text
|
|
114
|
-
status =
|
|
117
|
+
status = compare_text_nodes(n1, n2, opts, differences)
|
|
118
|
+
when Nokogiri::XML::NodeSet
|
|
119
|
+
status = diff_children ? compare_children(n1, n2, child_opts, differences, diff_children: diff_children) : compare_children(n1, n2, opts, differences)
|
|
115
120
|
else
|
|
116
121
|
raise 'Comparison only allowed between objects of type Nokogiri::XML::Node and Nokogiri::XML::NodeSet.' unless n1.is_a?(Nokogiri::XML::Node) || n1.is_a?(Nokogiri::XML::NodeSet)
|
|
117
|
-
status =
|
|
122
|
+
status = compare_children(n1.children, n2.children, opts, differences)
|
|
118
123
|
end
|
|
119
124
|
elsif n1.nil? || n2.nil?
|
|
120
125
|
status = MISSING_NODE
|
|
121
|
-
|
|
126
|
+
add_difference(n1, n2, n1, n2, opts, differences)
|
|
122
127
|
else
|
|
123
128
|
status = UNEQUAL_NODES_TYPES
|
|
124
129
|
if n1.is_a? Nokogiri::XML::Text
|
|
125
|
-
|
|
130
|
+
add_difference(n1.parent, n2, n1, n2, opts, differences)
|
|
126
131
|
elsif n2.is_a? Nokogiri::XML::Text
|
|
127
|
-
|
|
132
|
+
add_difference(n1, n2.parent, n1, n2, opts, differences)
|
|
128
133
|
else
|
|
129
|
-
|
|
134
|
+
add_difference(n1, n2, n1, n2, opts, differences)
|
|
130
135
|
end
|
|
131
136
|
end
|
|
132
137
|
status
|
|
@@ -135,16 +140,16 @@ module CompareXML
|
|
|
135
140
|
##
|
|
136
141
|
# Compares two nodes of type Nokogiri::HTML::Comment.
|
|
137
142
|
#
|
|
138
|
-
# @param [Nokogiri::XML::
|
|
139
|
-
# @param [Nokogiri::XML::
|
|
143
|
+
# @param [Nokogiri::XML::Element] n1 left comment
|
|
144
|
+
# @param [Nokogiri::XML::Element] n2 right comment
|
|
140
145
|
# @param [Hash] opts user-overridden options
|
|
141
146
|
# @param [Array] differences inequivalence messages
|
|
142
147
|
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
143
148
|
#
|
|
144
149
|
# @return type of equivalence (from equivalence constants)
|
|
145
150
|
#
|
|
146
|
-
def
|
|
147
|
-
return
|
|
151
|
+
def compare_comment_nodes(n1, n2, opts, differences, status: EQUIVALENT)
|
|
152
|
+
return status if opts[:ignore_comments]
|
|
148
153
|
t1 = n1.content
|
|
149
154
|
t2 = n2.content
|
|
150
155
|
if opts[:collapse_whitespace]
|
|
@@ -153,7 +158,7 @@ module CompareXML
|
|
|
153
158
|
end
|
|
154
159
|
unless t1 == t2
|
|
155
160
|
status = UNEQUAL_COMMENTS
|
|
156
|
-
|
|
161
|
+
add_difference(n1.parent, n2.parent, t1, t2, opts, differences)
|
|
157
162
|
end
|
|
158
163
|
status
|
|
159
164
|
end
|
|
@@ -161,22 +166,21 @@ module CompareXML
|
|
|
161
166
|
##
|
|
162
167
|
# Compares two nodes of type Nokogiri::HTML::Document.
|
|
163
168
|
#
|
|
164
|
-
# @param [Nokogiri::XML::
|
|
165
|
-
# @param [Nokogiri::XML::
|
|
169
|
+
# @param [Nokogiri::XML::Element] n1 left document
|
|
170
|
+
# @param [Nokogiri::XML::Element] n2 right document
|
|
166
171
|
# @param [Hash] opts user-overridden options
|
|
167
172
|
# @param [Array] differences inequivalence messages
|
|
168
|
-
# @param [Hash]
|
|
169
|
-
# @param [Bool]
|
|
170
|
-
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
173
|
+
# @param [Hash] child_opts user-overridden options used for the child nodes
|
|
174
|
+
# @param [Bool] diff_children use different options for the child nodes
|
|
171
175
|
#
|
|
172
176
|
# @return type of equivalence (from equivalence constants)
|
|
173
177
|
#
|
|
174
|
-
def
|
|
178
|
+
def compare_document_nodes(n1, n2, opts, differences, child_opts = {}, diff_children: false)
|
|
175
179
|
if n1.name == n2.name
|
|
176
|
-
status =
|
|
180
|
+
status = diff_children ? compare_children(n1.children, n2.children, child_opts, differences, diff_children: diff_children) : compare_children(n1.children, n2.children, opts, differences)
|
|
177
181
|
else
|
|
178
182
|
status = UNEQUAL_DOCUMENTS
|
|
179
|
-
|
|
183
|
+
add_difference(n1, n2, n1, n2, opts, differences)
|
|
180
184
|
end
|
|
181
185
|
status
|
|
182
186
|
end
|
|
@@ -188,21 +192,23 @@ module CompareXML
|
|
|
188
192
|
# @param [Nokogiri::XML::NodeSet] n2_set right set of Nokogiri::XML::Node elements
|
|
189
193
|
# @param [Hash] opts user-overridden options
|
|
190
194
|
# @param [Array] differences inequivalence messages
|
|
191
|
-
# @param [Bool]
|
|
195
|
+
# @param [Bool] diff_children use different options for the child nodes
|
|
192
196
|
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
193
197
|
#
|
|
194
198
|
# @return type of equivalence (from equivalence constants)
|
|
195
199
|
#
|
|
196
|
-
def
|
|
197
|
-
|
|
198
|
-
return if opts[:
|
|
200
|
+
def compare_children(n1_set, n2_set, opts, differences, diff_children: false, status: EQUIVALENT)
|
|
201
|
+
return status if opts[:ignore_children]
|
|
202
|
+
return compare_aligned_children(n1_set, n2_set, opts, differences, diff_children: diff_children, status: status) if opts[:verbose] && opts[:align_children]
|
|
203
|
+
i = 0
|
|
204
|
+
j = 0
|
|
199
205
|
while i < n1_set.length || j < n2_set.length
|
|
200
|
-
if !n1_set[i].nil? &&
|
|
206
|
+
if !n1_set[i].nil? && node_excluded?(n1_set[i], opts)
|
|
201
207
|
i += 1 # increment counter if left node is excluded
|
|
202
|
-
elsif !n2_set[j].nil? &&
|
|
208
|
+
elsif !n2_set[j].nil? && node_excluded?(n2_set[j], opts)
|
|
203
209
|
j += 1 # increment counter if right node is excluded
|
|
204
210
|
else
|
|
205
|
-
result =
|
|
211
|
+
result = diff_children ? compare_nodes(n1_set[i], n2_set[j], opts, differences, opts, diff_children: diff_children) : compare_nodes(n1_set[i], n2_set[j], opts, differences)
|
|
206
212
|
status = result unless result == EQUIVALENT
|
|
207
213
|
|
|
208
214
|
# return false so that this subtree could halt comparison on error
|
|
@@ -213,10 +219,116 @@ module CompareXML
|
|
|
213
219
|
break unless status == EQUIVALENT || opts[:verbose]
|
|
214
220
|
|
|
215
221
|
# increment both counters when both nodes have been compared
|
|
216
|
-
i += 1
|
|
222
|
+
i += 1
|
|
223
|
+
j += 1
|
|
217
224
|
end
|
|
218
|
-
status
|
|
219
225
|
end
|
|
226
|
+
status
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
##
|
|
230
|
+
# Compares two sets of child nodes by first aligning them, so that inserted or
|
|
231
|
+
# removed nodes are reported as additions/removals rather than as changes.
|
|
232
|
+
# Identical subtrees are matched as anchors (via a longest common subsequence
|
|
233
|
+
# of their fingerprints); the unmatched nodes between anchors are compared
|
|
234
|
+
# positionally, and any leftovers become additions or removals.
|
|
235
|
+
#
|
|
236
|
+
def compare_aligned_children(n1_set, n2_set, opts, differences, diff_children: false, status: EQUIVALENT)
|
|
237
|
+
left = n1_set.reject { |n| node_excluded?(n, opts) }
|
|
238
|
+
right = n2_set.reject { |n| node_excluded?(n, opts) }
|
|
239
|
+
cache = {}.compare_by_identity
|
|
240
|
+
fingerprints_left = left.map { |n| node_fingerprint(n, opts, cache) }
|
|
241
|
+
fingerprints_right = right.map { |n| node_fingerprint(n, opts, cache) }
|
|
242
|
+
|
|
243
|
+
prev_i = 0
|
|
244
|
+
prev_j = 0
|
|
245
|
+
anchors = longest_common_subsequence(fingerprints_left, fingerprints_right)
|
|
246
|
+
(anchors + [[left.length, right.length]]).each do |anchor_i, anchor_j|
|
|
247
|
+
result = compare_child_gap(left[prev_i...anchor_i], right[prev_j...anchor_j], opts, differences, diff_children)
|
|
248
|
+
status = result unless result == EQUIVALENT
|
|
249
|
+
prev_i = anchor_i + 1
|
|
250
|
+
prev_j = anchor_j + 1
|
|
251
|
+
end
|
|
252
|
+
status
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# Compares a run of unmatched left and right nodes that lies between two anchors.
|
|
257
|
+
# Overlapping nodes are compared positionally; remaining left nodes are reported
|
|
258
|
+
# as removals and remaining right nodes as additions.
|
|
259
|
+
#
|
|
260
|
+
def compare_child_gap(left, right, opts, differences, diff_children, status: EQUIVALENT)
|
|
261
|
+
common = [left.length, right.length].min
|
|
262
|
+
common.times do |k|
|
|
263
|
+
result = diff_children ? compare_nodes(left[k], right[k], opts, differences, opts, diff_children: diff_children) : compare_nodes(left[k], right[k], opts, differences)
|
|
264
|
+
status = result unless result == EQUIVALENT
|
|
265
|
+
end
|
|
266
|
+
left.drop(common).each do |n|
|
|
267
|
+
status = MISSING_NODE
|
|
268
|
+
add_difference(n, nil, n, nil, opts, differences)
|
|
269
|
+
end
|
|
270
|
+
right.drop(common).each do |n|
|
|
271
|
+
status = MISSING_NODE
|
|
272
|
+
add_difference(nil, n, nil, n, opts, differences)
|
|
273
|
+
end
|
|
274
|
+
status
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
# Builds a canonical, option-aware string for a node so that two nodes with equal
|
|
279
|
+
# fingerprints are guaranteed to be equivalent under the current options. Used to
|
|
280
|
+
# anchor identical subtrees during alignment. Results are memoized per comparison.
|
|
281
|
+
#
|
|
282
|
+
def node_fingerprint(node, opts, cache)
|
|
283
|
+
cache[node] ||= build_node_fingerprint(node, opts, cache)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_node_fingerprint(node, opts, cache)
|
|
287
|
+
case node
|
|
288
|
+
when Nokogiri::XML::Comment
|
|
289
|
+
"C:#{opts[:collapse_whitespace] ? collapse(node.content) : node.content}"
|
|
290
|
+
when Nokogiri::XML::Text
|
|
291
|
+
"T:#{opts[:collapse_whitespace] ? collapse(node.content) : node.content}"
|
|
292
|
+
when Nokogiri::XML::Element
|
|
293
|
+
attrs = node.attribute_nodes
|
|
294
|
+
attrs = attrs.sort_by(&:name) if opts[:ignore_attr_order]
|
|
295
|
+
attr_str = attrs.map { |a| "#{a.name}=#{a.value}" }.join(' ')
|
|
296
|
+
children = node.children.reject { |c| node_excluded?(c, opts) }
|
|
297
|
+
child_str = children.map { |c| node_fingerprint(c, opts, cache) }.join
|
|
298
|
+
"E:#{element_name(node)}[#{attr_str}](#{child_str})"
|
|
299
|
+
else
|
|
300
|
+
"O:#{node.class}:#{node}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
##
|
|
305
|
+
# Returns the index pairs of a longest common subsequence of the two arrays.
|
|
306
|
+
#
|
|
307
|
+
def longest_common_subsequence(a, b)
|
|
308
|
+
m = a.length
|
|
309
|
+
n = b.length
|
|
310
|
+
lengths = Array.new(m + 1) { Array.new(n + 1, 0) }
|
|
311
|
+
(m - 1).downto(0) do |i|
|
|
312
|
+
(n - 1).downto(0) do |j|
|
|
313
|
+
lengths[i][j] = a[i] == b[j] ? lengths[i + 1][j + 1] + 1 : [lengths[i + 1][j], lengths[i][j + 1]].max
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
pairs = []
|
|
318
|
+
i = 0
|
|
319
|
+
j = 0
|
|
320
|
+
while i < m && j < n
|
|
321
|
+
if a[i] == b[j]
|
|
322
|
+
pairs << [i, j]
|
|
323
|
+
i += 1
|
|
324
|
+
j += 1
|
|
325
|
+
elsif lengths[i + 1][j] >= lengths[i][j + 1]
|
|
326
|
+
i += 1
|
|
327
|
+
else
|
|
328
|
+
j += 1
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
pairs
|
|
220
332
|
end
|
|
221
333
|
|
|
222
334
|
##
|
|
@@ -228,38 +340,52 @@ module CompareXML
|
|
|
228
340
|
# @param [Nokogiri::XML::Element] n2 right node element
|
|
229
341
|
# @param [Hash] opts user-overridden options
|
|
230
342
|
# @param [Array] differences inequivalence messages
|
|
231
|
-
# @param [Hash]
|
|
232
|
-
# @param [Bool]
|
|
343
|
+
# @param [Hash] child_opts user-overridden options used for the child nodes
|
|
344
|
+
# @param [Bool] diff_children use different options for the child nodes
|
|
233
345
|
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
234
346
|
#
|
|
235
347
|
# @return type of equivalence (from equivalence constants)
|
|
236
348
|
#
|
|
237
|
-
def
|
|
238
|
-
if n1
|
|
239
|
-
result =
|
|
349
|
+
def compare_element_nodes(n1, n2, opts, differences, child_opts = {}, diff_children: false, status: EQUIVALENT)
|
|
350
|
+
if element_name(n1) == element_name(n2)
|
|
351
|
+
result = compare_attribute_sets(n1, n2, n1.attribute_nodes, n2.attribute_nodes, opts, differences)
|
|
240
352
|
return result unless result == EQUIVALENT || opts[:force_children] == true
|
|
241
|
-
|
|
353
|
+
status = result unless result == EQUIVALENT
|
|
354
|
+
result = diff_children ? compare_children(n1.children, n2.children, child_opts, differences, diff_children: diff_children) : compare_children(n1.children, n2.children, opts, differences)
|
|
242
355
|
status = result unless result == EQUIVALENT
|
|
243
356
|
else
|
|
244
357
|
status = UNEQUAL_ELEMENTS
|
|
245
|
-
|
|
358
|
+
add_difference(n1, n2, element_name(n1), element_name(n2), opts, differences)
|
|
246
359
|
end
|
|
247
360
|
status
|
|
248
361
|
end
|
|
249
362
|
|
|
363
|
+
##
|
|
364
|
+
# Returns the element name qualified by its namespace URI, so that elements with
|
|
365
|
+
# the same local name in different namespaces are not treated as equal.
|
|
366
|
+
#
|
|
367
|
+
# @param [Nokogiri::XML::Element] node the element
|
|
368
|
+
#
|
|
369
|
+
# @return [String] the local name, prefixed with "{uri}" when namespaced
|
|
370
|
+
#
|
|
371
|
+
def element_name(node)
|
|
372
|
+
href = node.namespace&.href
|
|
373
|
+
href ? "{#{href}}#{node.name}" : node.name
|
|
374
|
+
end
|
|
375
|
+
|
|
250
376
|
##
|
|
251
377
|
# Compares two nodes of type Nokogiri::XML::Text.
|
|
252
378
|
#
|
|
253
|
-
# @param [Nokogiri::XML::
|
|
254
|
-
# @param [Nokogiri::XML::
|
|
379
|
+
# @param [Nokogiri::XML::Element] n1 left text node
|
|
380
|
+
# @param [Nokogiri::XML::Element] n2 right text node
|
|
255
381
|
# @param [Hash] opts user-overridden options
|
|
256
382
|
# @param [Array] differences inequivalence messages
|
|
257
383
|
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
258
384
|
#
|
|
259
385
|
# @return type of equivalence (from equivalence constants)
|
|
260
386
|
#
|
|
261
|
-
def
|
|
262
|
-
return
|
|
387
|
+
def compare_text_nodes(n1, n2, opts, differences, status = EQUIVALENT)
|
|
388
|
+
return status if opts[:ignore_text_nodes]
|
|
263
389
|
t1 = n1.content
|
|
264
390
|
t2 = n2.content
|
|
265
391
|
if opts[:collapse_whitespace]
|
|
@@ -268,7 +394,7 @@ module CompareXML
|
|
|
268
394
|
end
|
|
269
395
|
unless t1 == t2
|
|
270
396
|
status = UNEQUAL_TEXT_CONTENTS
|
|
271
|
-
|
|
397
|
+
add_difference(n1.parent, n2.parent, t1, t2, opts, differences)
|
|
272
398
|
end
|
|
273
399
|
status
|
|
274
400
|
end
|
|
@@ -285,12 +411,14 @@ module CompareXML
|
|
|
285
411
|
#
|
|
286
412
|
# @return type of equivalence (from equivalence constants)
|
|
287
413
|
#
|
|
288
|
-
def
|
|
414
|
+
def compare_attribute_sets(n1, n2, a1_set, a2_set, opts, differences)
|
|
415
|
+
a1_set = a1_set.reject { |a| attribute_excluded?(a, opts) }
|
|
416
|
+
a2_set = a2_set.reject { |a| attribute_excluded?(a, opts) }
|
|
289
417
|
return false unless a1_set.length == a2_set.length || opts[:verbose]
|
|
290
418
|
if opts[:ignore_attr_order]
|
|
291
|
-
|
|
419
|
+
compare_sorted_attribute_sets(n1, n2, a1_set, a2_set, opts, differences)
|
|
292
420
|
else
|
|
293
|
-
|
|
421
|
+
compare_unsorted_attribute_sets(n1, n2, a1_set, a2_set, opts, differences)
|
|
294
422
|
end
|
|
295
423
|
end
|
|
296
424
|
|
|
@@ -309,7 +437,7 @@ module CompareXML
|
|
|
309
437
|
#
|
|
310
438
|
# @return type of equivalence (from equivalence constants)
|
|
311
439
|
#
|
|
312
|
-
def
|
|
440
|
+
def compare_sorted_attribute_sets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT)
|
|
313
441
|
a1_set = a1_set.sort_by(&:name)
|
|
314
442
|
a2_set = a2_set.sort_by(&:name)
|
|
315
443
|
i = j = 0
|
|
@@ -317,15 +445,21 @@ module CompareXML
|
|
|
317
445
|
while i < a1_set.length || j < a2_set.length
|
|
318
446
|
|
|
319
447
|
if a1_set[i].nil?
|
|
320
|
-
result =
|
|
448
|
+
result = compare_attributes(n1, n2, nil, a2_set[j], opts, differences)
|
|
449
|
+
j += 1
|
|
321
450
|
elsif a2_set[j].nil?
|
|
322
|
-
result =
|
|
451
|
+
result = compare_attributes(n1, n2, a1_set[i], nil, opts, differences)
|
|
452
|
+
i += 1
|
|
323
453
|
elsif a1_set[i].name < a2_set[j].name
|
|
324
|
-
result =
|
|
454
|
+
result = compare_attributes(n1, n2, a1_set[i], nil, opts, differences)
|
|
455
|
+
i += 1
|
|
325
456
|
elsif a1_set[i].name > a2_set[j].name
|
|
326
|
-
result =
|
|
457
|
+
result = compare_attributes(n1, n2, nil, a2_set[j], opts, differences)
|
|
458
|
+
j += 1
|
|
327
459
|
else
|
|
328
|
-
result =
|
|
460
|
+
result = compare_attributes(n1, n2, a1_set[i], a2_set[j], opts, differences)
|
|
461
|
+
i += 1
|
|
462
|
+
j += 1
|
|
329
463
|
end
|
|
330
464
|
|
|
331
465
|
status = result unless result == EQUIVALENT
|
|
@@ -350,11 +484,11 @@ module CompareXML
|
|
|
350
484
|
#
|
|
351
485
|
# @return type of equivalence (from equivalence constants)
|
|
352
486
|
#
|
|
353
|
-
def
|
|
487
|
+
def compare_unsorted_attribute_sets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT)
|
|
354
488
|
[a1_set.length, a2_set.length].max.times do |i|
|
|
355
|
-
result =
|
|
489
|
+
result = compare_attributes(n1, n2, a1_set[i], a2_set[i], opts, differences)
|
|
356
490
|
status = result unless result == EQUIVALENT
|
|
357
|
-
break unless status == EQUIVALENT
|
|
491
|
+
break unless status == EQUIVALENT || opts[:verbose]
|
|
358
492
|
end
|
|
359
493
|
status
|
|
360
494
|
end
|
|
@@ -362,34 +496,31 @@ module CompareXML
|
|
|
362
496
|
##
|
|
363
497
|
# Compares two attributes by name and value.
|
|
364
498
|
#
|
|
365
|
-
# @param [Nokogiri::XML::Element] n1 left node element
|
|
366
|
-
# @param [Nokogiri::XML::Element] n2 right node element
|
|
367
|
-
# @param [Nokogiri::XML::Attr] a1 left attribute
|
|
368
|
-
# @param [Nokogiri::XML::Attr] a2 right attribute
|
|
499
|
+
# @param [Nokogiri::XML::Element|NilClass] n1 left node element
|
|
500
|
+
# @param [Nokogiri::XML::Element|NilClass] n2 right node element
|
|
501
|
+
# @param [Nokogiri::XML::Attr|NilClass] a1 left attribute
|
|
502
|
+
# @param [Nokogiri::XML::Attr|NilClass] a2 right attribute
|
|
369
503
|
# @param [Hash] opts user-overridden options
|
|
370
504
|
# @param [Array] differences inequivalence messages
|
|
371
505
|
# @param [int] status comparison status code (EQUIVALENT by default)
|
|
372
506
|
#
|
|
373
507
|
# @return type of equivalence (from equivalence constants)
|
|
374
508
|
#
|
|
375
|
-
def
|
|
509
|
+
def compare_attributes(n1, n2, a1, a2, opts, differences, status = EQUIVALENT)
|
|
376
510
|
if a1.nil?
|
|
377
511
|
status = MISSING_ATTRIBUTE
|
|
378
|
-
|
|
512
|
+
add_difference(n1, n2, nil, "#{a2.name}=\"#{a2.value}\"", opts, differences)
|
|
379
513
|
elsif a2.nil?
|
|
380
514
|
status = MISSING_ATTRIBUTE
|
|
381
|
-
|
|
515
|
+
add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", nil, opts, differences)
|
|
382
516
|
elsif a1.name == a2.name
|
|
383
|
-
return status if attrNameExcluded?(a1.name, a2.name, opts)
|
|
384
|
-
return status if attrsExcluded?(a1, a2, opts)
|
|
385
|
-
return status if attrContentExcluded?(a1, a2, opts)
|
|
386
517
|
if a1.value != a2.value
|
|
387
518
|
status = UNEQUAL_ATTRIBUTES
|
|
388
|
-
|
|
519
|
+
add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
|
|
389
520
|
end
|
|
390
521
|
else
|
|
391
522
|
status = UNEQUAL_ATTRIBUTES
|
|
392
|
-
|
|
523
|
+
add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
|
|
393
524
|
end
|
|
394
525
|
status
|
|
395
526
|
end
|
|
@@ -408,7 +539,7 @@ module CompareXML
|
|
|
408
539
|
#
|
|
409
540
|
# @return true if excluded, false otherwise
|
|
410
541
|
#
|
|
411
|
-
def
|
|
542
|
+
def node_excluded?(n, opts)
|
|
412
543
|
return true if n.is_a?(Nokogiri::XML::DTD)
|
|
413
544
|
return true if n.is_a?(Nokogiri::XML::Comment) && opts[:ignore_comments]
|
|
414
545
|
return true if n.is_a?(Nokogiri::XML::Text) && (opts[:ignore_text_nodes] || collapse(n.content).empty?)
|
|
@@ -417,84 +548,40 @@ module CompareXML
|
|
|
417
548
|
end
|
|
418
549
|
|
|
419
550
|
##
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
# @param [Nokogiri::XML::Attr] a1 left attribute
|
|
426
|
-
# @param [Nokogiri::XML::Attr] a2 right attribute
|
|
427
|
-
# @param [Hash] opts user-overridden options
|
|
428
|
-
#
|
|
429
|
-
# @return true if excluded, false otherwise
|
|
551
|
+
# Determines whether a single attribute should be excluded from comparison, based on
|
|
552
|
+
# the +ignore_attrs_by_name+, +ignore_attr_content+ and +ignore_attrs+ options. The
|
|
553
|
+
# check is one-sided, so an attribute is dropped even when its counterpart is missing
|
|
554
|
+
# on the other element.
|
|
430
555
|
#
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if css.include?(a1.name) && css.include?(a2.name)
|
|
434
|
-
return true if a1.parent.xpath('../*').css(css).include?(a1.parent) && a2.parent.xpath('../*').css(css).include?(a2.parent)
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
false
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
##
|
|
441
|
-
# Checks whether two given attributes should be excluded, based on their content.
|
|
442
|
-
# Checks whether both attributes contain content that should be excluded, and
|
|
443
|
-
# returns true only if an excluded string is contained in both attribute values.
|
|
556
|
+
# Name matchers are compared with +===+: a String matches the attribute name exactly,
|
|
557
|
+
# while a Regexp matches it as a pattern.
|
|
444
558
|
#
|
|
445
|
-
# @param [Nokogiri::XML::Attr]
|
|
446
|
-
# @param [Nokogiri::XML::Attr] a2 right attribute
|
|
559
|
+
# @param [Nokogiri::XML::Attr] attr the attribute being tested
|
|
447
560
|
# @param [Hash] opts user-overridden options
|
|
448
561
|
#
|
|
449
562
|
# @return true if excluded, false otherwise
|
|
450
563
|
#
|
|
451
|
-
def
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
opts[:ignore_attr_content].each do |content|
|
|
455
|
-
a1_excluded ||= a1.value.include?(content)
|
|
456
|
-
a2_excluded ||= a2.value.include?(content)
|
|
457
|
-
return true if a1_excluded && a2_excluded
|
|
458
|
-
end
|
|
459
|
-
false
|
|
460
|
-
end
|
|
564
|
+
def attribute_excluded?(attr, opts)
|
|
565
|
+
return true if opts[:ignore_attrs_by_name].any? { |matcher| matcher === attr.name }
|
|
566
|
+
return true if opts[:ignore_attr_content].any? { |content| attr.value.include?(content) }
|
|
461
567
|
|
|
462
|
-
|
|
463
|
-
# Checks whether two given attributes should be excluded, based on their content.
|
|
464
|
-
# Checks whether both attributes contain content that should be excluded, and
|
|
465
|
-
# returns true only if an excluded string is contained in both attribute values.
|
|
466
|
-
#
|
|
467
|
-
# @param [Nokogiri::XML::Attr] a1 left attribute
|
|
468
|
-
# @param [Nokogiri::XML::Attr] a2 right attribute
|
|
469
|
-
# @param [Hash] opts user-overridden options
|
|
470
|
-
#
|
|
471
|
-
# @return true if excluded, false otherwise
|
|
472
|
-
#
|
|
473
|
-
def attrNameExcluded?(a1, a2, opts)
|
|
474
|
-
a1_excluded = false
|
|
475
|
-
a2_excluded = false
|
|
476
|
-
opts[:ignore_attrs_by_name].each do |name|
|
|
477
|
-
a1_excluded ||= a1.to_s.include?(name)
|
|
478
|
-
a2_excluded ||= a2.to_s.include?(name)
|
|
479
|
-
return true if a1_excluded && a2_excluded
|
|
480
|
-
end
|
|
481
|
-
false
|
|
568
|
+
opts[:ignore_attrs].any? { |css| css.include?(attr.name) && attr.parent.xpath('../*').css(css).include?(attr.parent) }
|
|
482
569
|
end
|
|
483
570
|
|
|
484
571
|
##
|
|
485
572
|
# Strips the whitespace (from beginning and end) and collapses it,
|
|
486
573
|
# i.e. multiple spaces, new lines and tabs are all collapsed to a single space.
|
|
487
574
|
#
|
|
488
|
-
# @param [Nokogiri::XML::
|
|
489
|
-
# @param [Nokogiri::XML::
|
|
490
|
-
# @param [String] diff1 left diffing value
|
|
491
|
-
# @param [String] diff2 right diffing value
|
|
575
|
+
# @param [Nokogiri::XML::Element|NilClass] node1 left node
|
|
576
|
+
# @param [Nokogiri::XML::Element|NilClass] node2 right node
|
|
577
|
+
# @param [String|NilClass] diff1 left diffing value
|
|
578
|
+
# @param [String|NilClass] diff2 right diffing value
|
|
492
579
|
# @param [Hash] opts user-overridden options
|
|
493
580
|
# @param [Array] differences inequivalence messages
|
|
494
581
|
#
|
|
495
582
|
# @return collapsed string
|
|
496
583
|
#
|
|
497
|
-
def
|
|
584
|
+
def add_difference(node1, node2, diff1, diff2, opts, differences)
|
|
498
585
|
differences << { node1: node1, node2: node2, diff1: diff1, diff2: diff2 } if opts[:verbose]
|
|
499
586
|
end
|
|
500
587
|
|
metadata
CHANGED
|
@@ -1,55 +1,48 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: compare-xml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vadim Kononov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: nokogiri
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
|
-
- - "
|
|
16
|
+
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.
|
|
18
|
+
version: '1.6'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
|
-
- - "
|
|
23
|
+
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.
|
|
27
|
-
description:
|
|
28
|
-
|
|
29
|
-
of Nokogiri::XML::Node or Nokogiri::XML::NodeSet for equality or equivalency.
|
|
25
|
+
version: '1.6'
|
|
26
|
+
description: Compares Nokogiri XML and HTML documents, nodes and node sets for equality
|
|
27
|
+
or equivalency. Returns a list of differences and can exclude chosen nodes or attributes.
|
|
30
28
|
email:
|
|
31
|
-
- vadim@
|
|
29
|
+
- vadim@konoson.com
|
|
32
30
|
executables: []
|
|
33
31
|
extensions: []
|
|
34
32
|
extra_rdoc_files: []
|
|
35
33
|
files:
|
|
36
|
-
- ".gitignore"
|
|
37
|
-
- ".rubocop.yml"
|
|
38
|
-
- Gemfile
|
|
39
34
|
- LICENSE.txt
|
|
40
35
|
- README.md
|
|
41
|
-
- Rakefile
|
|
42
|
-
- bin/console
|
|
43
|
-
- bin/setup
|
|
44
|
-
- compare-xml.gemspec
|
|
45
|
-
- img/diffing.png
|
|
46
36
|
- lib/compare-xml.rb
|
|
47
37
|
- lib/compare-xml/version.rb
|
|
48
38
|
homepage: https://github.com/vkononov/compare-xml
|
|
49
39
|
licenses:
|
|
50
40
|
- MIT
|
|
51
|
-
metadata:
|
|
52
|
-
|
|
41
|
+
metadata:
|
|
42
|
+
homepage_uri: https://github.com/vkononov/compare-xml
|
|
43
|
+
source_code_uri: https://github.com/vkononov/compare-xml
|
|
44
|
+
changelog_uri: https://github.com/vkononov/compare-xml/releases
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
53
46
|
rdoc_options: []
|
|
54
47
|
require_paths:
|
|
55
48
|
- lib
|
|
@@ -57,17 +50,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
57
50
|
requirements:
|
|
58
51
|
- - ">="
|
|
59
52
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
53
|
+
version: 2.4.0
|
|
61
54
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
55
|
requirements:
|
|
63
56
|
- - ">="
|
|
64
57
|
- !ruby/object:Gem::Version
|
|
65
58
|
version: '0'
|
|
66
59
|
requirements: []
|
|
67
|
-
|
|
68
|
-
rubygems_version: 2.7.3
|
|
69
|
-
signing_key:
|
|
60
|
+
rubygems_version: 4.0.10
|
|
70
61
|
specification_version: 4
|
|
71
|
-
summary:
|
|
72
|
-
equality or equivalency.
|
|
62
|
+
summary: Compare XML and HTML documents or Nokogiri nodes for equality with a diff
|
|
73
63
|
test_files: []
|
data/.gitignore
DELETED
data/.rubocop.yml
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
# Exclude:
|
|
3
|
-
# - config/routes.rb
|
|
4
|
-
# - db/migrate/*
|
|
5
|
-
# - db/schema.rb
|
|
6
|
-
TargetRubyVersion: 2.5
|
|
7
|
-
|
|
8
|
-
Metrics/AbcSize:
|
|
9
|
-
Enabled: false
|
|
10
|
-
|
|
11
|
-
Metrics/CyclomaticComplexity:
|
|
12
|
-
Enabled: false
|
|
13
|
-
|
|
14
|
-
Metrics/LineLength:
|
|
15
|
-
Enabled: false
|
|
16
|
-
|
|
17
|
-
Metrics/MethodLength:
|
|
18
|
-
Enabled: false
|
|
19
|
-
|
|
20
|
-
Metrics/ModuleLength:
|
|
21
|
-
Enabled: false
|
|
22
|
-
|
|
23
|
-
Metrics/ParameterLists:
|
|
24
|
-
Enabled: false
|
|
25
|
-
|
|
26
|
-
Metrics/PerceivedComplexity:
|
|
27
|
-
Enabled: false
|
|
28
|
-
|
|
29
|
-
Naming/FileName:
|
|
30
|
-
Enabled: false
|
|
31
|
-
|
|
32
|
-
Naming/MethodName:
|
|
33
|
-
Enabled: false
|
|
34
|
-
|
|
35
|
-
Naming/UncommunicativeMethodParamName:
|
|
36
|
-
Enabled: false
|
|
37
|
-
|
|
38
|
-
Style/Documentation:
|
|
39
|
-
Enabled: false
|
|
40
|
-
|
|
41
|
-
Style/FrozenStringLiteralComment:
|
|
42
|
-
Enabled: false
|
|
43
|
-
|
|
44
|
-
Style/Semicolon:
|
|
45
|
-
Enabled: false
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/bin/console
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require 'bundler/setup'
|
|
4
|
-
require 'compare-xml/xml'
|
|
5
|
-
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
-
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
-
# require 'pry'
|
|
11
|
-
# Pry.start
|
|
12
|
-
|
|
13
|
-
require 'irb'
|
|
14
|
-
IRB.start
|
data/bin/setup
DELETED
data/compare-xml.gemspec
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
lib = File.expand_path('lib', __dir__)
|
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
-
require 'compare-xml/version'
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = 'compare-xml'
|
|
7
|
-
spec.version = CompareXML::VERSION
|
|
8
|
-
spec.authors = ['Vadim Kononov']
|
|
9
|
-
spec.email = ['vadim@poetic.com']
|
|
10
|
-
|
|
11
|
-
spec.summary = 'A customizable tool that compares two instances of Nokogiri::XML::Node for equality or equivalency.'
|
|
12
|
-
spec.description = 'CompareXML is a fast, lightweight and feature-rich tool that will solve your XML/HTML comparison or diffing needs. its purpose is to compare two instances of Nokogiri::XML::Node or Nokogiri::XML::NodeSet for equality or equivalency.'
|
|
13
|
-
spec.homepage = 'https://github.com/vkononov/compare-xml'
|
|
14
|
-
spec.license = 'MIT'
|
|
15
|
-
|
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
17
|
-
spec.bindir = 'exe'
|
|
18
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
|
-
spec.require_paths = ['lib']
|
|
20
|
-
|
|
21
|
-
spec.add_runtime_dependency 'nokogiri', '~> 1.8'
|
|
22
|
-
end
|
data/img/diffing.png
DELETED
|
Binary file
|