nokodiff 0.2.0 → 0.3.0

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: bd5f28a6d69fffa081eb6dd4ade65be6aa8b2e309c7be553ab6820ffb7ab480e
4
- data.tar.gz: 9b54a7aa1d6437c41ae47c5b3a6869ce97544426a9fae655940c227de34f5867
3
+ metadata.gz: e12bce0257af1ebff2c6de595dce5d68cefc73feb5ae4a419a98a81d9dbac212
4
+ data.tar.gz: 9479aa3c4ae7f3481a0a96baf002735fb02a4848530a9fc38565c72118e4a2eb
5
5
  SHA512:
6
- metadata.gz: 9f6088d65f6e4e007a39a4b30c07d6cc99080714952c68dd3ecc34cac4a10881910f3ce9d99826db4ecbd45917e8161e4f0f6b134c54d1d2ca31217b857b336e
7
- data.tar.gz: af6c8bcaf758d33001c07f23d94d6897a83e3425c3093d342e05dde16fc6a6a0f59c6cfe2d46f1356da75c7c51babaf496947d1f01f4cb921d58908fa8c2be2d
6
+ metadata.gz: 864f165ec8332dbcc0a61cffda45f6649ed00d5483e969ad7d55dc9af2e4acdbe202f54686424ff387f6f36ecd160b3834e89f4d1633a8bf311534b6c09eb0b3
7
+ data.tar.gz: f182809b76cf85e8ce23002575fb5c2708e527cd3659628d0ef248fdce79e1581ed6a551f99ca41a16dff7bacf9d67db87579185761052136e3f491d227f8bf6
data/README.md CHANGED
@@ -50,6 +50,116 @@ In your application.scss file include:
50
50
 
51
51
  This will include the styling for `<del>`, `<ins>` and `<strong>` tags to allow colour coding, highlighting and underlining of changes.
52
52
 
53
+ ### More complex diffing with `data-diff-key`
54
+
55
+ For more complex HTML structures, a standard diff can produce surprising or misleading results when elements are added, removed, or reordered. To get around this, add a `data-diff-key` attribute to elements you want compared in isolation.
56
+
57
+ Each element with a unique `data-diff-key` value is diffed independently against its counterpart in the other HTML fragment. This prevents unrelated changes from affecting each other and produces more accurate, contextually meaningful output.
58
+
59
+ The key value can be any unique string — it just needs to match between your `before` and `after` HTML for the elements you want paired together.
60
+
61
+ #### Adding a new element
62
+
63
+ When a `data-diff-key` element is present in `after` but not in `before`, its content is wrapped in an `<ins>` tag:
64
+
65
+ ```ruby
66
+ before = <<~HTML
67
+ <div>
68
+ <div data-diff-key="ixn4">
69
+ <p>First paragraph</p>
70
+ </div>
71
+ </div>
72
+ HTML
73
+
74
+ after = <<~HTML
75
+ <div>
76
+ <div data-diff-key="ixn4">
77
+ <p>First paragraph</p>
78
+ </div>
79
+ <div data-diff-key="zm7q">
80
+ <p>Second paragraph</p>
81
+ </div>
82
+ </div>
83
+ HTML
84
+
85
+ Nokodiff.diff(before, after)
86
+ ```
87
+
88
+ Output:
89
+
90
+ ```html
91
+ <div>
92
+ <div data-diff-key="ixn4">
93
+ <p>First paragraph</p>
94
+ </div>
95
+ <div data-diff-key="zm7q">
96
+ <div class="diff">
97
+ <ins aria-label="added content"><p>Second paragraph</p></ins>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ```
102
+
103
+ ![Adding a new element](docs/adding-a-new-element.png)
104
+
105
+ #### Modifying an existing element
106
+
107
+ When a `data-diff-key` element exists in both fragments but its content has changed, the element is diffed in isolation. Character-level changes are highlighted within `<del>` and `<ins>` tags:
108
+
109
+ ```ruby
110
+ before = <<~HTML
111
+ <div>
112
+ <div data-diff-key="ixn4">
113
+ <p>First paragraph</p>
114
+ </div>
115
+ <div data-diff-key="zm7q">
116
+ <p>Second paragraph</p>
117
+ </div>
118
+ </div>
119
+ HTML
120
+
121
+ after = <<~HTML
122
+ <div>
123
+ <div data-diff-key="ixn4">
124
+ <p>First paragraph</p>
125
+ </div>
126
+ <div data-diff-key="zm7q">
127
+ <p>New paragraph</p>
128
+ </div>
129
+ </div>
130
+ HTML
131
+
132
+ Nokodiff.diff(before, after)
133
+ ```
134
+
135
+ Output:
136
+
137
+ ```html
138
+ <div>
139
+ <div data-diff-key="ixn4"><p>First paragraph</p></div>
140
+ <div data-diff-key="zm7q">
141
+ <div class="diff">
142
+ <del aria-label="removed content"><p><strong>S</strong>e<strong>cond</strong> paragraph</p></del>
143
+ </div>
144
+ <div class="diff">
145
+ <ins aria-label="added content"><p><strong>N</strong>e<strong>w</strong> paragraph</p></ins>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ ```
150
+
151
+ ![Modifying an existing element](docs/modifying-an-existing-element.png)
152
+
153
+ #### Unchanged elements
154
+
155
+ If a `data-diff-key` element's content is identical in both fragments, no diff markup is added and the element is returned as-is.
156
+
157
+ #### Limitations
158
+
159
+ Currently, the gem does not support highlighting removed `data-diff-key` elements. If a keyed element is present in `before` but absent from `after`, it will not be marked as deleted in the output.
160
+
161
+ Thanks to [HTML Diff](https://html-diff.lix.dev/index.html) for inspiring this approach!
162
+
53
163
  ## Licence
54
164
 
55
- The gem is available as open source under the terms of the [MIT License.](https://opensource.org/license/MIT)
165
+ The gem is available as open source under the terms of the [MIT License.](https://opensource.org/license/MIT)
@@ -1,8 +1,8 @@
1
1
  module Nokodiff
2
2
  class Differ
3
- def initialize(before_html, after_html)
4
- @before = Nokogiri::HTML.fragment(before_html)
5
- @after = Nokogiri::HTML.fragment(after_html)
3
+ def initialize(before, after)
4
+ @before = before
5
+ @after = after
6
6
  end
7
7
 
8
8
  def to_html
@@ -0,0 +1,40 @@
1
+ require "forwardable"
2
+
3
+ module Nokodiff
4
+ class HTMLFragment
5
+ extend Forwardable
6
+
7
+ class InvalidHTMLError < StandardError; end
8
+
9
+ def initialize(html)
10
+ @fragment = Nokogiri::HTML.fragment(html)
11
+ validate!
12
+ remove_blank_nodes!
13
+ remove_comments!
14
+ end
15
+
16
+ def_delegators :@fragment, :children, :css, :at, :to_html
17
+
18
+ private
19
+
20
+ def validate!
21
+ invalid_text_nodes = @fragment.children.reject do |node|
22
+ node.element? || node.comment? || (node.text? && node.text.strip.empty?)
23
+ end
24
+
25
+ unless invalid_text_nodes.empty?
26
+ raise InvalidHTMLError, "Invalid HTML input: #{@fragment.to_html}"
27
+ end
28
+ end
29
+
30
+ def remove_blank_nodes!
31
+ @fragment.traverse do |node|
32
+ node.remove if node.blank?
33
+ end
34
+ end
35
+
36
+ def remove_comments!
37
+ @fragment.css("comment()").remove
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nokodiff
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/nokodiff.rb CHANGED
@@ -9,13 +9,17 @@ require_relative "nokodiff/differ"
9
9
  require_relative "nokodiff/engine"
10
10
  require_relative "nokodiff/text_node_diffs"
11
11
  require_relative "nokodiff/changes_in_fragments"
12
+ require_relative "nokodiff/html_fragment"
12
13
 
13
14
  module Nokodiff
14
15
  def self.diff(before_html, after_html)
15
- HTMLFragmentValidator.validate_html!(before_html)
16
- HTMLFragmentValidator.validate_html!(after_html)
16
+ before = Nokodiff::HTMLFragment.new(before_html)
17
+ after = Nokodiff::HTMLFragment.new(after_html)
17
18
 
18
- html = Differ.new(before_html, after_html).to_html
19
+ before_nodes, after_nodes = nodes(before, after)
20
+ keys = (before_nodes.keys + after_nodes.keys).uniq
21
+
22
+ html = keys.any? ? diff_by_keys(after, keys, before_nodes, after_nodes) : Differ.new(before, after).to_html
19
23
  safe_html(html)
20
24
  end
21
25
 
@@ -23,31 +27,25 @@ module Nokodiff
23
27
  html.respond_to?(:html_safe) ? html.html_safe : html
24
28
  end
25
29
 
26
- class Error < StandardError; end
27
-
28
- module HTMLFragmentValidator
29
- module_function
30
-
31
- def validate_html!(html)
32
- document = Nokogiri::HTML::DocumentFragment.parse(html)
33
-
34
- invalid_text_nodes = document.children.select do |node|
35
- if node.element?
36
- false
37
- elsif node.comment?
38
- false
39
- elsif node.text?
40
- !node.text.strip.empty?
41
- else
42
- true
43
- end
44
- end
30
+ private_class_method def self.nodes(before, after)
31
+ [
32
+ fetch_diff_nodes(before),
33
+ fetch_diff_nodes(after),
34
+ ]
35
+ end
45
36
 
46
- unless invalid_text_nodes.empty?
47
- raise ArgumentError, "Invalid HTML input"
48
- end
37
+ private_class_method def self.fetch_diff_nodes(fragment)
38
+ fragment.css("[data-diff-key]").map { |node| [node["data-diff-key"], node] }.to_h
39
+ end
49
40
 
50
- document
41
+ private_class_method def self.diff_by_keys(after, keys, before_nodes, after_nodes)
42
+ keys.each do |key|
43
+ diff = Differ.new(
44
+ before_nodes.fetch(key, Nokogiri::HTML.fragment("")),
45
+ after_nodes.fetch(key, Nokogiri::HTML.fragment("")),
46
+ ).to_html
47
+ after.at("[data-diff-key='#{key}']")&.inner_html = diff
51
48
  end
49
+ after.to_html
52
50
  end
53
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nokodiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
- version: 5.1.20
74
+ version: 5.2.0
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - '='
80
80
  - !ruby/object:Gem::Version
81
- version: 5.1.20
81
+ version: 5.2.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: simplecov
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -209,6 +209,7 @@ files:
209
209
  - lib/nokodiff/differ.rb
210
210
  - lib/nokodiff/engine.rb
211
211
  - lib/nokodiff/formatting_helpers.rb
212
+ - lib/nokodiff/html_fragment.rb
212
213
  - lib/nokodiff/text_node_diffs.rb
213
214
  - lib/nokodiff/version.rb
214
215
  homepage: https://github.com/alphagov/nokodiff
@@ -229,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
229
230
  - !ruby/object:Gem::Version
230
231
  version: '0'
231
232
  requirements: []
232
- rubygems_version: 4.0.3
233
+ rubygems_version: 4.0.7
233
234
  specification_version: 4
234
235
  summary: A Ruby Gem to highlight additions, deletions and character level changes
235
236
  while preserving original HTML