nokodiff 0.2.0 → 0.3.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 +113 -3
- data/app/assets/stylesheets/nokodiff.scss +9 -9
- data/lib/nokodiff/changes_in_fragments.rb +4 -4
- data/lib/nokodiff/differ.rb +13 -9
- data/lib/nokodiff/formatting_helpers.rb +7 -4
- data/lib/nokodiff/html_fragment.rb +40 -0
- data/lib/nokodiff/text_node_diffs.rb +2 -2
- data/lib/nokodiff/version.rb +1 -1
- data/lib/nokodiff.rb +24 -26
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4804027f53e38322927f18d80b25a0c9cdfa659154e56090ba61e895dbde68f8
|
|
4
|
+
data.tar.gz: 855959815a1284325a50326d962127ff44feee8d16adfd74fd0e7af300b99ab9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9484c5b5682e6d8299d1c8d7ff9b38d6a9c53c85ffc15cbef6bdfd234d3bd81df05ba90423924511db20381e44fb70c078cbbef15fdfea631ad5d583647aede
|
|
7
|
+
data.tar.gz: 3563899d45760342ac2a70ad7977b35a8e7bfc5aeb058aba2a69ad567865a6a8de628a4240f687439e2f48e5ef3ddba1d82e0916d72910d89e2b149897fbe659
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A Ruby Gem to highlight additions, deletions and character level changes while p
|
|
|
4
4
|
|
|
5
5
|
It includes functionality to:
|
|
6
6
|
* Compare two HTML fragments and output diffs with semantic HTML
|
|
7
|
-
* Inline character differences highlighting using `<
|
|
7
|
+
* Inline character differences highlighting using `<span class="diff-marker">` tagging
|
|
8
8
|
* Blocks of added or removed content wrapped in aria labelled `<ins>` and `<del>` tags
|
|
9
9
|
* Optional CSS for styling the visual differences
|
|
10
10
|
|
|
@@ -48,8 +48,118 @@ In your application.scss file include:
|
|
|
48
48
|
@import "nokodiff";
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
This will include the styling for `<del>`, `<ins>` and `<
|
|
51
|
+
This will include the styling for `<del>`, `<ins>` and `<span>` tags to allow colour coding, highlighting and underlining of changes.
|
|
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
|
+

|
|
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><span class="diff-marker">S</span>e<span class="diff-marker">cond</span> paragraph</p></del>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="diff">
|
|
145
|
+
<ins aria-label="added content"><p><span class="diff-marker">N</span>e<span class="diff-marker">w</span> paragraph</p></ins>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+

|
|
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!
|
|
52
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,9 +1,9 @@
|
|
|
1
1
|
// stylelint-disable selector-no-qualifying-type, max-nesting-depth
|
|
2
2
|
// Diff of two editions
|
|
3
3
|
$added-color: #e6fff3;
|
|
4
|
-
$
|
|
4
|
+
$diff-marker-added-color: #99ffcf;
|
|
5
5
|
$removed-color: #fadede;
|
|
6
|
-
$
|
|
6
|
+
$diff-marker-removed-color: #f3aeac;
|
|
7
7
|
$text-colour: #0b0c0c;
|
|
8
8
|
$light-grey: #f3f2f1;
|
|
9
9
|
$black: #0b0c0c;
|
|
@@ -33,9 +33,9 @@ $black: #0b0c0c;
|
|
|
33
33
|
background-color: $removed-color;
|
|
34
34
|
padding-bottom: 2px;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
span.diff-marker {
|
|
37
37
|
font-weight: normal;
|
|
38
|
-
background-color: $
|
|
38
|
+
background-color: $diff-marker-removed-color;
|
|
39
39
|
border-bottom: 2px dashed $black;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -44,9 +44,9 @@ $black: #0b0c0c;
|
|
|
44
44
|
background-color: $added-color;
|
|
45
45
|
padding-bottom: 2px;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
span.diff-marker {
|
|
48
48
|
font-weight: normal;
|
|
49
|
-
background-color: $
|
|
49
|
+
background-color: $diff-marker-added-color;
|
|
50
50
|
border-bottom: 2px dashed $black;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -63,13 +63,13 @@ $black: #0b0c0c;
|
|
|
63
63
|
|
|
64
64
|
del::before {
|
|
65
65
|
color: $text-colour;
|
|
66
|
-
background-color: $
|
|
66
|
+
background-color: $diff-marker-removed-color;
|
|
67
67
|
content: "−";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
ins::before {
|
|
71
71
|
color: $text-colour;
|
|
72
|
-
background-color: $
|
|
72
|
+
background-color: $diff-marker-added-color;
|
|
73
73
|
content: "+";
|
|
74
74
|
}
|
|
75
|
-
}
|
|
75
|
+
}
|
|
@@ -43,18 +43,18 @@ module Nokodiff
|
|
|
43
43
|
append_accumulated_text(before_fragment, accumulated_before_text)
|
|
44
44
|
append_accumulated_text(after_fragment, accumulated_after_text)
|
|
45
45
|
|
|
46
|
-
before_fragment.add_child(
|
|
47
|
-
after_fragment.add_child(
|
|
46
|
+
before_fragment.add_child(highlight_changes(change.old_element, before_fragment))
|
|
47
|
+
after_fragment.add_child(highlight_changes(change.new_element, after_fragment))
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def emphasise_deletion(change)
|
|
51
51
|
append_accumulated_text(before_fragment, accumulated_before_text)
|
|
52
|
-
before_fragment.add_child(
|
|
52
|
+
before_fragment.add_child(highlight_changes(change.old_element, before_fragment))
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def emphasise_addition(change)
|
|
56
56
|
append_accumulated_text(after_fragment, accumulated_after_text)
|
|
57
|
-
after_fragment.add_child(
|
|
57
|
+
after_fragment.add_child(highlight_changes(change.new_element, after_fragment))
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def append_accumulated_text(fragment, accumulated_text)
|
data/lib/nokodiff/differ.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Nokodiff
|
|
2
2
|
class Differ
|
|
3
|
-
def initialize(
|
|
4
|
-
@before =
|
|
5
|
-
@after =
|
|
3
|
+
def initialize(before, after)
|
|
4
|
+
@before = before
|
|
5
|
+
@after = after
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def to_html
|
|
@@ -42,8 +42,8 @@ module Nokodiff
|
|
|
42
42
|
|
|
43
43
|
before_fragment, after_fragment = Nokodiff::TextNodeDiffs.new(before_dup, after_dup).call
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
merge_adjacent_highlighted_changes(before_fragment)
|
|
46
|
+
merge_adjacent_highlighted_changes(after_fragment)
|
|
47
47
|
|
|
48
48
|
[before_fragment.to_html, after_fragment.to_html]
|
|
49
49
|
end
|
|
@@ -62,24 +62,28 @@ module Nokodiff
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def merge_adjacent_highlighted_changes(node)
|
|
66
66
|
return unless node.element?
|
|
67
67
|
|
|
68
68
|
node.children.each do |child|
|
|
69
|
-
|
|
69
|
+
merge_adjacent_highlighted_changes(child) if child.element?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
node.children.each_cons(2) do |left, right|
|
|
73
|
-
next unless left
|
|
73
|
+
next unless node_is_a_change?(left) && node_is_a_change?(right)
|
|
74
74
|
|
|
75
75
|
left.content = left.content + right.content
|
|
76
76
|
right.remove
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
merge_adjacent_highlighted_changes(node)
|
|
79
79
|
break
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def node_is_a_change?(node)
|
|
84
|
+
node.name == "span" && node["class"] == "diff-marker"
|
|
85
|
+
end
|
|
86
|
+
|
|
83
87
|
def unchanged_block(html)
|
|
84
88
|
html.to_s
|
|
85
89
|
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module Nokodiff
|
|
2
2
|
module FormattingHelpers
|
|
3
|
-
def
|
|
4
|
-
Nokogiri::XML::Node.new("
|
|
3
|
+
def highlight_changes(char, fragment)
|
|
4
|
+
Nokogiri::XML::Node.new("span", fragment.document).tap do |n|
|
|
5
|
+
n.content = char
|
|
6
|
+
n["class"] = "diff-marker"
|
|
7
|
+
end
|
|
5
8
|
end
|
|
6
9
|
|
|
7
|
-
def
|
|
8
|
-
text_node.replace(
|
|
10
|
+
def highlighted_change(text_node)
|
|
11
|
+
text_node.replace(highlight_changes(text_node.to_html, text_node.parent))
|
|
9
12
|
end
|
|
10
13
|
end
|
|
11
14
|
end
|
|
@@ -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
|
|
@@ -31,8 +31,8 @@ module Nokodiff
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def diff_text_node_content(before_text_node, after_text_node)
|
|
34
|
-
return
|
|
35
|
-
return
|
|
34
|
+
return highlighted_change(before_text_node) if text_removed?(before_text_node, after_text_node)
|
|
35
|
+
return highlighted_change(after_text_node) if text_added?(before_text_node, after_text_node)
|
|
36
36
|
|
|
37
37
|
before_chars = before_text_node.text.chars
|
|
38
38
|
after_chars = after_text_node.text.chars
|
data/lib/nokodiff/version.rb
CHANGED
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
|
-
|
|
16
|
-
|
|
16
|
+
before = Nokodiff::HTMLFragment.new(before_html)
|
|
17
|
+
after = Nokodiff::HTMLFragment.new(after_html)
|
|
17
18
|
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
version: 0.3.1
|
|
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.
|
|
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.
|
|
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.
|
|
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
|