compare-xml 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '044582cb43241dc59a65422b8c36a2cc96d1a3120f9648c0950b7ef9fe125be5'
4
- data.tar.gz: 3a33391e6f5a67f305546173a43725a64bd6ed4df12135434453da9bc3982e98
3
+ metadata.gz: ed20155c59d477002fe69c2883e0a8d3b3b9507aa042f95d41cb3693b044e56d
4
+ data.tar.gz: 99c9374d3ac9bd98fe3e4b1fbad74b7bb4e1bea0f55ee475b41f401d8a60123e
5
5
  SHA512:
6
- metadata.gz: 0f3a32d528e4298a96ce59238f7ed556fa110ea7a4d715b651e1e578887f6bde9c6852928d0971c114e6ca9fb77dca8c4e6ca53c9580143cc5d89785ca14d942
7
- data.tar.gz: fc8371bdce1193d15d42092c24b9b44d2d9c51a685331102ba69fd9290cafdd6faf588feab68871b1c7659c42ed1f1656718b97b72fbf0702bde153e3a2caf30
6
+ metadata.gz: ba79f1cd40d906b02752a50cdc37f7c82200077a7f1e7c9e5f530e046c67ad445a4830d87082715c9f96405363c116deb5cbc9d1df276906dc52dcd1098565d9
7
+ data.tar.gz: 9b25dfa936e79f2e15137ceece697f7f909344b628b79e51450598660989ca0b7604b6e44272fcbb4a19676ebd6eb719939e8b1c4b4a6d424edf57aac2786067
data/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # CompareXML
2
2
 
3
- [![Gem Version](https://img.shields.io/gem/v/compare-xml.svg)](https://rubygems.org/gems/compare-xml)
4
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
- [![Lint Status](https://github.com/vkononov/compare-xml/actions/workflows/lint.yml/badge.svg)](https://github.com/vkononov/compare-xml/actions/workflows/lint.yml)
6
- [![Test Status](https://github.com/vkononov/compare-xml/actions/workflows/test.yml/badge.svg)](https://github.com/vkononov/compare-xml/actions/workflows/test.yml)
3
+ [![Gem Version](https://img.shields.io/gem/v/compare-xml.svg?label=Gem&logo=rubygems&logoColor=white)](https://rubygems.org/gems/compare-xml)
4
+ [![Ruby](https://img.shields.io/badge/Ruby-2.4%20to%204.0-CC342D?logo=ruby&logoColor=white)](https://github.com/vkononov/compare-xml/blob/main/.github/workflows/test.yml)
5
+ [![Test Matrix](https://img.shields.io/github/actions/workflow/status/vkononov/compare-xml/test.yml?branch=main&label=Test%20Matrix&logo=github)](https://github.com/vkononov/compare-xml/actions/workflows/test.yml)
6
+ [![Lint](https://img.shields.io/github/actions/workflow/status/vkononov/compare-xml/lint.yml?branch=main&label=Lint&logo=github)](https://github.com/vkononov/compare-xml/actions/workflows/lint.yml)
7
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)
7
8
 
8
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.
9
10
 
10
- [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/vkononov)
11
+ Supports **Ruby 2.4 → latest**, with the full Ruby matrix tested daily in CI.
11
12
 
12
13
  **Features**
13
14
 
@@ -30,7 +31,9 @@ And then execute:
30
31
 
31
32
  Or install it yourself as:
32
33
 
33
- gem install compare-xml
34
+ ```sh
35
+ gem install compare-xml
36
+ ```
34
37
 
35
38
 
36
39
 
@@ -49,8 +52,8 @@ where `doc1` and `doc2` are instances of `Nokogiri::XML::Node` or `Nokogiri::XML
49
52
  Suppose you have two files `1.html` and `2.html` that you would like to compare. You could do it as follows:
50
53
 
51
54
  ```ruby
52
- doc1 = Nokogiri::HTML(open('1.html'))
53
- doc2 = Nokogiri::HTML(open('2.html'))
55
+ doc1 = Nokogiri::HTML(File.read('1.html'))
56
+ doc2 = Nokogiri::HTML(File.read('2.html'))
54
57
  puts CompareXML.equivalent?(doc1, doc2)
55
58
  ```
56
59
 
@@ -81,7 +84,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
81
84
  - when provided, ignores all attributes that contain substrings `string`, `string2`, etc.
82
85
 
83
86
  - `ignore_attrs: [css_selector1, css_selector1, ...]` default: **`[]`**     [show examples ⇨](#ignore_attrs)
84
- - when provided, ignores specific *attributes* using [CSS selectors](http://www.w3schools.com/cssref/css_selectors.asp)
87
+ - when provided, ignores specific *attributes* using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php)
85
88
 
86
89
  - `ignore_attrs_by_name: [matcher1, matcher2, ...]` default: **`[]`**     [show examples ⇨](#ignore_attrs_by_name)
87
90
  - when provided, ignores *attributes* whose name matches a `String` (exact) or `Regexp` (pattern)
@@ -90,7 +93,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
90
93
  - when `true`, ignores comments, such as `<!-- comment -->`
91
94
 
92
95
  - `ignore_nodes: [css_selector1, css_selector1, ...]` default: **`[]`** &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[show examples ⇨](#ignore_nodes)
93
- - when provided, ignores specific *nodes* using [CSS selectors](http://www.w3schools.com/cssref/css_selectors.asp)
96
+ - when provided, ignores specific *nodes* using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php)
94
97
 
95
98
  - `ignore_text_nodes: {true|false}` default: **`false`**&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[show examples ⇨](#ignore_text_nodes)
96
99
  - when `true`, ignores all text content within a document
@@ -188,7 +191,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
188
191
 
189
192
  - <a id="ignore_attrs"></a>`ignore_attrs: [css_selector1, css_selector1, ...]` default: **`[]`**
190
193
 
191
- When provided, ignores all **attributes** that satisfy a particular rule using [CSS selectors](http://www.w3schools.com/cssref/css_selectors.asp).
194
+ When provided, ignores all **attributes** that satisfy a particular rule using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php).
192
195
 
193
196
  **Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_attrs: ['a[rel="nofollow"]', 'input[type="hidden"']})`
194
197
 
@@ -250,7 +253,7 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
250
253
 
251
254
  - <a id="ignore_nodes"></a>`ignore_nodes: [css_selector1, css_selector1, ...]` default: **`[]`**
252
255
 
253
- When provided, ignores all **nodes** that satisfy a particular rule using [CSS selectors](http://www.w3schools.com/cssref/css_selectors.asp).
256
+ When provided, ignores all **nodes** that satisfy a particular rule using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.php).
254
257
 
255
258
  **Usage Example:** `CompareXML.equivalent?(doc1, doc2, {ignore_nodes: ['script', 'object']})`
256
259
 
@@ -382,6 +385,31 @@ CompareXML.equivalent?(doc1, doc2, {collapse_whitespace: false, verbose: true})
382
385
 
383
386
  ----------
384
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
+ ```
410
+
411
+ ----------
412
+
385
413
  ## Contributing
386
414
 
387
415
  1. Fork it
@@ -400,4 +428,4 @@ This gem was inspired by [Michael B. Klein](https://github.com/mbklein)'s gem [`
400
428
 
401
429
  ## License
402
430
 
403
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
431
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
@@ -1,3 +1,3 @@
1
1
  module CompareXML
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.0.2'.freeze
3
3
  end
data/lib/compare-xml.rb CHANGED
@@ -107,6 +107,8 @@ module CompareXML
107
107
  def compare_nodes(n1, n2, opts, differences, child_opts = {}, diff_children: false)
108
108
  if n1.instance_of?(n2.class)
109
109
  case n1
110
+ when Nokogiri::XML::ProcessingInstruction
111
+ status = compare_processing_instructions(n1, n2, opts, differences)
110
112
  when Nokogiri::XML::Comment
111
113
  status = compare_comment_nodes(n1, n2, opts, differences)
112
114
  when Nokogiri::HTML::Document
@@ -115,6 +117,8 @@ module CompareXML
115
117
  status = diff_children ? compare_element_nodes(n1, n2, opts, differences, child_opts, diff_children: diff_children) : compare_element_nodes(n1, n2, opts, differences)
116
118
  when Nokogiri::XML::Text
117
119
  status = compare_text_nodes(n1, n2, opts, differences)
120
+ when Nokogiri::XML::NodeSet
121
+ status = diff_children ? compare_children(n1, n2, child_opts, differences, diff_children: diff_children) : compare_children(n1, n2, opts, differences)
118
122
  else
119
123
  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)
120
124
  status = compare_children(n1.children, n2.children, opts, differences)
@@ -135,6 +139,13 @@ module CompareXML
135
139
  status
136
140
  end
137
141
 
142
+ def compare_processing_instructions(n1, n2, opts, differences)
143
+ return EQUIVALENT if n1.name == n2.name && n1.content == n2.content
144
+
145
+ add_difference(n1, n2, n1, n2, opts, differences)
146
+ UNEQUAL_ELEMENTS
147
+ end
148
+
138
149
  ##
139
150
  # Compares two nodes of type Nokogiri::HTML::Comment.
140
151
  #
@@ -273,7 +284,7 @@ module CompareXML
273
284
  end
274
285
 
275
286
  ##
276
- # Builds a canonical, option-aware string for a node so that two nodes with equal
287
+ # Builds a structured, option-aware fingerprint for a node so that two nodes with equal
277
288
  # fingerprints are guaranteed to be equivalent under the current options. Used to
278
289
  # anchor identical subtrees during alignment. Results are memoized per comparison.
279
290
  #
@@ -283,19 +294,16 @@ module CompareXML
283
294
 
284
295
  def build_node_fingerprint(node, opts, cache)
285
296
  case node
286
- when Nokogiri::XML::Comment
287
- "C:#{opts[:collapse_whitespace] ? collapse(node.content) : node.content}"
288
- when Nokogiri::XML::Text
289
- "T:#{opts[:collapse_whitespace] ? collapse(node.content) : node.content}"
297
+ when Nokogiri::XML::Comment, Nokogiri::XML::Text
298
+ [node.class, opts[:collapse_whitespace] ? collapse(node.content) : node.content]
290
299
  when Nokogiri::XML::Element
291
- attrs = node.attribute_nodes
292
- attrs = attrs.sort_by(&:name) if opts[:ignore_attr_order]
293
- attr_str = attrs.map { |a| "#{a.name}=#{a.value}" }.join(' ')
294
- children = node.children.reject { |c| node_excluded?(c, opts) }
295
- child_str = children.map { |c| node_fingerprint(c, opts, cache) }.join
296
- "E:#{node.name}[#{attr_str}](#{child_str})"
300
+ attrs = node.attribute_nodes.reject { |attr| attribute_excluded?(attr, opts) }
301
+ attrs = attrs.sort_by { |attr| attribute_name(attr) } if opts[:ignore_attr_order]
302
+ attributes = attrs.map { |attr| [attribute_name(attr), attr.value] }
303
+ children = opts[:ignore_children] ? [] : node.children.reject { |child| node_excluded?(child, opts) }
304
+ [node.class, element_name(node), attributes, children.map { |child| node_fingerprint(child, opts, cache) }]
297
305
  else
298
- "O:#{node.class}:#{node}"
306
+ [node.class, node.to_s]
299
307
  end
300
308
  end
301
309
 
@@ -345,7 +353,7 @@ module CompareXML
345
353
  # @return type of equivalence (from equivalence constants)
346
354
  #
347
355
  def compare_element_nodes(n1, n2, opts, differences, child_opts = {}, diff_children: false, status: EQUIVALENT)
348
- if n1.name == n2.name
356
+ if element_name(n1) == element_name(n2)
349
357
  result = compare_attribute_sets(n1, n2, n1.attribute_nodes, n2.attribute_nodes, opts, differences)
350
358
  return result unless result == EQUIVALENT || opts[:force_children] == true
351
359
  status = result unless result == EQUIVALENT
@@ -353,11 +361,24 @@ module CompareXML
353
361
  status = result unless result == EQUIVALENT
354
362
  else
355
363
  status = UNEQUAL_ELEMENTS
356
- add_difference(n1, n2, n1.name, n2.name, opts, differences)
364
+ add_difference(n1, n2, element_name(n1), element_name(n2), opts, differences)
357
365
  end
358
366
  status
359
367
  end
360
368
 
369
+ ##
370
+ # Returns the element name qualified by its namespace URI, so that elements with
371
+ # the same local name in different namespaces are not treated as equal.
372
+ #
373
+ # @param [Nokogiri::XML::Element] node the element
374
+ #
375
+ # @return [String] the local name, prefixed with "{uri}" when namespaced
376
+ #
377
+ def element_name(node)
378
+ href = node.namespace&.href
379
+ href ? "{#{href}}#{node.name}" : node.name
380
+ end
381
+
361
382
  ##
362
383
  # Compares two nodes of type Nokogiri::XML::Text.
363
384
  #
@@ -423,8 +444,8 @@ module CompareXML
423
444
  # @return type of equivalence (from equivalence constants)
424
445
  #
425
446
  def compare_sorted_attribute_sets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT)
426
- a1_set = a1_set.sort_by(&:name)
427
- a2_set = a2_set.sort_by(&:name)
447
+ a1_set = a1_set.sort_by { |attr| attribute_name(attr) }
448
+ a2_set = a2_set.sort_by { |attr| attribute_name(attr) }
428
449
  i = j = 0
429
450
 
430
451
  while i < a1_set.length || j < a2_set.length
@@ -435,10 +456,10 @@ module CompareXML
435
456
  elsif a2_set[j].nil?
436
457
  result = compare_attributes(n1, n2, a1_set[i], nil, opts, differences)
437
458
  i += 1
438
- elsif a1_set[i].name < a2_set[j].name
459
+ elsif attribute_name(a1_set[i]) < attribute_name(a2_set[j])
439
460
  result = compare_attributes(n1, n2, a1_set[i], nil, opts, differences)
440
461
  i += 1
441
- elsif a1_set[i].name > a2_set[j].name
462
+ elsif attribute_name(a1_set[i]) > attribute_name(a2_set[j])
442
463
  result = compare_attributes(n1, n2, nil, a2_set[j], opts, differences)
443
464
  j += 1
444
465
  else
@@ -478,6 +499,11 @@ module CompareXML
478
499
  status
479
500
  end
480
501
 
502
+ def attribute_name(attribute)
503
+ href = attribute.namespace&.href
504
+ href ? "{#{href}}#{attribute.name}" : attribute.name
505
+ end
506
+
481
507
  ##
482
508
  # Compares two attributes by name and value.
483
509
  #
@@ -494,18 +520,18 @@ module CompareXML
494
520
  def compare_attributes(n1, n2, a1, a2, opts, differences, status = EQUIVALENT)
495
521
  if a1.nil?
496
522
  status = MISSING_ATTRIBUTE
497
- add_difference(n1, n2, nil, "#{a2.name}=\"#{a2.value}\"", opts, differences)
523
+ add_difference(n1, n2, nil, "#{attribute_name(a2)}=\"#{a2.value}\"", opts, differences)
498
524
  elsif a2.nil?
499
525
  status = MISSING_ATTRIBUTE
500
- add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", nil, opts, differences)
501
- elsif a1.name == a2.name
526
+ add_difference(n1, n2, "#{attribute_name(a1)}=\"#{a1.value}\"", nil, opts, differences)
527
+ elsif attribute_name(a1) == attribute_name(a2)
502
528
  if a1.value != a2.value
503
529
  status = UNEQUAL_ATTRIBUTES
504
- add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
530
+ add_difference(n1, n2, "#{attribute_name(a1)}=\"#{a1.value}\"", "#{attribute_name(a2)}=\"#{a2.value}\"", opts, differences)
505
531
  end
506
532
  else
507
533
  status = UNEQUAL_ATTRIBUTES
508
- add_difference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
534
+ add_difference(n1, n2, "#{attribute_name(a1)}=\"#{a1.value}\"", "#{attribute_name(a2)}=\"#{a2.value}\"", opts, differences)
509
535
  end
510
536
  status
511
537
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compare-xml
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -23,9 +23,8 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.6'
26
- description: CompareXML is a fast, lightweight and feature-rich tool that will solve
27
- your XML/HTML comparison or diffing needs. its purpose is to compare two instances
28
- of Nokogiri::XML::Node or Nokogiri::XML::NodeSet for equality or equivalency.
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.
29
28
  email:
30
29
  - vadim@konoson.com
31
30
  executables: []
@@ -58,8 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
57
  - !ruby/object:Gem::Version
59
58
  version: '0'
60
59
  requirements: []
61
- rubygems_version: 4.0.10
60
+ rubygems_version: 4.0.20
62
61
  specification_version: 4
63
- summary: A customizable tool that compares two instances of Nokogiri::XML::Node for
64
- equality or equivalency.
62
+ summary: Compare XML and HTML documents or Nokogiri nodes for equality with a diff
65
63
  test_files: []