nokodiff 0.3.0 → 0.4.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 +4 -4
- data/README.md +32 -4
- data/app/assets/stylesheets/nokodiff.scss +36 -39
- data/lib/nokodiff/changes_in_fragments.rb +4 -4
- data/lib/nokodiff/differ.rb +83 -30
- data/lib/nokodiff/formatting_helpers.rb +5 -6
- data/lib/nokodiff/text_node_diffs.rb +0 -11
- data/lib/nokodiff/version.rb +1 -1
- data/lib/nokodiff.rb +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9195a8ebd9adf0496df31641acd77e7fa84616b809ecb45cf86cd82346c3dd11
|
|
4
|
+
data.tar.gz: 7bc5d7fe060b960d7ac4fecf58f1f1e1f15b12d421a42552f0fb134cd61cc489
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd3a8e7d3386676defd112e12a8153d0c2f4af5256fe5c503e81bb6e47ca45395e97d34f8756dd0ad9b600c2719c8c94a9dd9341de14130b502e17634b37c5d9
|
|
7
|
+
data.tar.gz: 8af7196b25ff0eb368046046895b625f8df80394d8fa09c5076ba6d16f6721aa869a20823f026726dc2a50fd30354250104b9336c7ab2599d0176a43c674248e
|
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
|
|
|
@@ -22,6 +22,34 @@ or add it to your Gemfile
|
|
|
22
22
|
gem "nokodiff"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Local Development
|
|
26
|
+
|
|
27
|
+
The gem includes a dummy Rails application located in `spec/dummy`. It can be used to preview `Nokodiff` in a real browser environment without needing to integrate with an external host app.
|
|
28
|
+
|
|
29
|
+
To get the dummy app running you need to:
|
|
30
|
+
|
|
31
|
+
### Install dependencies
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Navigate to the app and install dependencies
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd spec/dummy
|
|
41
|
+
bundle install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Start the server
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bin/rails server
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The dummy app will now be running on `localhost:3000`
|
|
51
|
+
If you want to edit the content you are testing it is located in `spec/dummy/app/controllers/application_controller.rb`, alongside the code calling `Nokodiff.diff(@before, @after)`
|
|
52
|
+
|
|
25
53
|
## Usage
|
|
26
54
|
|
|
27
55
|
In the controller:
|
|
@@ -48,7 +76,7 @@ In your application.scss file include:
|
|
|
48
76
|
@import "nokodiff";
|
|
49
77
|
```
|
|
50
78
|
|
|
51
|
-
This will include the styling for `<del>`, `<ins>` and `<
|
|
79
|
+
This will include the styling for `<del>`, `<ins>` and `<span>` tags to allow colour coding, highlighting and underlining of changes.
|
|
52
80
|
|
|
53
81
|
### More complex diffing with `data-diff-key`
|
|
54
82
|
|
|
@@ -139,10 +167,10 @@ Output:
|
|
|
139
167
|
<div data-diff-key="ixn4"><p>First paragraph</p></div>
|
|
140
168
|
<div data-diff-key="zm7q">
|
|
141
169
|
<div class="diff">
|
|
142
|
-
<del aria-label="removed content"><p><
|
|
170
|
+
<del aria-label="removed content"><p><span class="diff-marker">S</span>e<span class="diff-marker">cond</span> paragraph</p></del>
|
|
143
171
|
</div>
|
|
144
172
|
<div class="diff">
|
|
145
|
-
<ins aria-label="added content"><p><
|
|
173
|
+
<ins aria-label="added content"><p><span class="diff-marker">N</span>e<span class="diff-marker">w</span> paragraph</p></ins>
|
|
146
174
|
</div>
|
|
147
175
|
</div>
|
|
148
176
|
</div>
|
|
@@ -1,75 +1,72 @@
|
|
|
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;
|
|
10
10
|
|
|
11
11
|
.compare-editions {
|
|
12
|
-
|
|
12
|
+
position: relative;
|
|
13
13
|
border-left: 40px solid $light-grey;
|
|
14
14
|
padding: 15px;
|
|
15
15
|
|
|
16
16
|
.diff {
|
|
17
|
-
|
|
18
|
-
padding: 0 15px;
|
|
19
|
-
word-wrap: break-word;
|
|
17
|
+
position: static;
|
|
20
18
|
margin-bottom: 10px;
|
|
21
|
-
position: relative;
|
|
22
19
|
|
|
23
|
-
del,
|
|
24
|
-
|
|
25
|
-
text-decoration: none;
|
|
20
|
+
del, ins, ul, li {
|
|
21
|
+
position: static !important;
|
|
26
22
|
display: block;
|
|
27
|
-
|
|
23
|
+
text-decoration: none;
|
|
28
24
|
border-radius: 3px;
|
|
29
25
|
}
|
|
30
|
-
}
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
del {
|
|
28
|
+
background-color: $removed-color;
|
|
29
|
+
padding-bottom: 2px;
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
span.diff-marker {
|
|
32
|
+
font-weight: normal;
|
|
33
|
+
background-color: $diff-marker-removed-color;
|
|
34
|
+
border-bottom: 2px dashed $black;
|
|
35
|
+
}
|
|
40
36
|
}
|
|
41
|
-
}
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
ins {
|
|
39
|
+
background-color: $added-color;
|
|
40
|
+
padding-bottom: 2px;
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
span.diff-marker {
|
|
43
|
+
font-weight: normal;
|
|
44
|
+
background-color: $diff-marker-added-color;
|
|
45
|
+
border-bottom: 2px dashed $black;
|
|
46
|
+
}
|
|
51
47
|
}
|
|
52
|
-
}
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
del::before,
|
|
50
|
+
ins::before {
|
|
51
|
+
position: absolute;
|
|
52
|
+
left: -40px;
|
|
53
|
+
width: 40px;
|
|
54
|
+
height: 1.5em;
|
|
55
|
+
line-height: 1.5em;
|
|
56
|
+
text-align: center;
|
|
57
|
+
z-index: 10;
|
|
58
|
+
}
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
del::before {
|
|
65
62
|
color: $text-colour;
|
|
66
|
-
background-color: $
|
|
63
|
+
background-color: $diff-marker-removed-color;
|
|
67
64
|
content: "−";
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
ins::before {
|
|
71
68
|
color: $text-colour;
|
|
72
|
-
background-color: $
|
|
69
|
+
background-color: $diff-marker-added-color;
|
|
73
70
|
content: "+";
|
|
74
71
|
}
|
|
75
|
-
}
|
|
72
|
+
}
|
|
@@ -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
|
@@ -11,7 +11,7 @@ module Nokodiff
|
|
|
11
11
|
when :unchanged
|
|
12
12
|
unchanged_block(diff[:before])
|
|
13
13
|
when :changed
|
|
14
|
-
|
|
14
|
+
changed_block(diff[:before], diff[:after])
|
|
15
15
|
when :deleted
|
|
16
16
|
deleted_block(diff[:before])
|
|
17
17
|
when :added
|
|
@@ -26,62 +26,115 @@ module Nokodiff
|
|
|
26
26
|
before_nodes = @before.children.to_a
|
|
27
27
|
after_nodes = @after.children.to_a
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
before_html_strings = before_nodes.map { |n| n.to_html.strip }
|
|
30
|
+
after_html_strings = after_nodes.map { |n| n.to_html.strip }
|
|
31
|
+
|
|
32
|
+
Diff::LCS.sdiff(before_html_strings, after_html_strings).map do |change|
|
|
33
|
+
case change.action
|
|
34
|
+
when "="
|
|
35
|
+
{
|
|
36
|
+
status: :unchanged,
|
|
37
|
+
before: before_nodes[change.old_position],
|
|
38
|
+
after: after_nodes[change.new_position],
|
|
39
|
+
}
|
|
40
|
+
when "!"
|
|
41
|
+
{
|
|
42
|
+
status: :changed,
|
|
43
|
+
before: before_nodes[change.old_position],
|
|
44
|
+
after: after_nodes[change.new_position],
|
|
45
|
+
}
|
|
46
|
+
when "-"
|
|
47
|
+
{
|
|
48
|
+
status: :deleted,
|
|
49
|
+
before: before_nodes[change.old_position],
|
|
50
|
+
after: nil,
|
|
51
|
+
}
|
|
52
|
+
when "+"
|
|
53
|
+
{
|
|
54
|
+
status: :added,
|
|
55
|
+
before: nil,
|
|
56
|
+
after: after_nodes[change.new_position],
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
34
61
|
|
|
35
|
-
|
|
62
|
+
def changed_block(before_node, after_node)
|
|
63
|
+
if structurally_similar?(before_node, after_node)
|
|
64
|
+
inner_diff = Differ.new(before_node, after_node).to_html
|
|
65
|
+
rebuild_element(after_node, inner_diff)
|
|
66
|
+
elsif before_node.text? && after_node.text?
|
|
67
|
+
before_diff, after_diff = diff_raw_text(before_node, after_node)
|
|
68
|
+
deleted_block(before_diff) + added_block(after_diff)
|
|
69
|
+
else
|
|
70
|
+
before_diff, after_diff = diff_sub_elements(before_node, after_node)
|
|
71
|
+
deleted_block(before_diff) + added_block(after_diff)
|
|
36
72
|
end
|
|
37
73
|
end
|
|
38
74
|
|
|
39
|
-
def
|
|
75
|
+
def structurally_similar?(before_node, after_node)
|
|
76
|
+
before_node.element? &&
|
|
77
|
+
after_node.element? &&
|
|
78
|
+
before_node.name == after_node.name &&
|
|
79
|
+
before_node.name != "p"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rebuild_element(template_node, inner_html)
|
|
83
|
+
result = template_node.dup
|
|
84
|
+
result.inner_html = inner_html
|
|
85
|
+
result.to_html
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def diff_raw_text(before_text, after_text)
|
|
89
|
+
diff = Diff::LCS.sdiff(before_text.text.chars, after_text.text.chars)
|
|
90
|
+
before_fragment, after_fragment = Nokodiff::ChangesInFragments.new(diff).call
|
|
91
|
+
[merge_fragment_spans(before_fragment), merge_fragment_spans(after_fragment)]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def merge_fragment_spans(fragment)
|
|
95
|
+
doc = fragment.document
|
|
96
|
+
wrapper = Nokogiri::XML::Node.new("span", doc)
|
|
97
|
+
wrapper.inner_html = fragment.to_html
|
|
98
|
+
merge_adjacent_highlighted_changes(wrapper)
|
|
99
|
+
wrapper.inner_html
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def diff_sub_elements(before_html, after_html)
|
|
40
103
|
before_dup = before_html.dup
|
|
41
104
|
after_dup = after_html.dup
|
|
42
105
|
|
|
43
106
|
before_fragment, after_fragment = Nokodiff::TextNodeDiffs.new(before_dup, after_dup).call
|
|
44
107
|
|
|
45
|
-
|
|
46
|
-
|
|
108
|
+
merge_adjacent_highlighted_changes(before_fragment)
|
|
109
|
+
merge_adjacent_highlighted_changes(after_fragment)
|
|
47
110
|
|
|
48
111
|
[before_fragment.to_html, after_fragment.to_html]
|
|
49
112
|
end
|
|
50
113
|
|
|
51
|
-
def
|
|
52
|
-
if before_node && after_node
|
|
53
|
-
if before_node.to_html.strip == after_node.to_html.strip
|
|
54
|
-
{ status: :unchanged, before: before_node, after: after_node }
|
|
55
|
-
else
|
|
56
|
-
{ status: :changed, before: before_node, after: after_node }
|
|
57
|
-
end
|
|
58
|
-
elsif before_node
|
|
59
|
-
{ status: :deleted, before: before_node, after: nil }
|
|
60
|
-
elsif after_node
|
|
61
|
-
{ status: :added, before: nil, after: after_node }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def merge_adjacent_strong_tags(node)
|
|
114
|
+
def merge_adjacent_highlighted_changes(node)
|
|
66
115
|
return unless node.element?
|
|
67
116
|
|
|
68
117
|
node.children.each do |child|
|
|
69
|
-
|
|
118
|
+
merge_adjacent_highlighted_changes(child) if child.element?
|
|
70
119
|
end
|
|
71
120
|
|
|
72
121
|
node.children.each_cons(2) do |left, right|
|
|
73
|
-
next unless left
|
|
122
|
+
next unless node_is_a_change?(left) && node_is_a_change?(right)
|
|
74
123
|
|
|
75
124
|
left.content = left.content + right.content
|
|
76
125
|
right.remove
|
|
77
126
|
|
|
78
|
-
|
|
127
|
+
merge_adjacent_highlighted_changes(node)
|
|
79
128
|
break
|
|
80
129
|
end
|
|
81
130
|
end
|
|
82
131
|
|
|
83
|
-
def
|
|
84
|
-
|
|
132
|
+
def node_is_a_change?(node)
|
|
133
|
+
node.name == "span" && node["class"] == "diff-marker"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def unchanged_block(node)
|
|
137
|
+
node.to_html
|
|
85
138
|
end
|
|
86
139
|
|
|
87
140
|
def deleted_block(html)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
module Nokodiff
|
|
2
2
|
module FormattingHelpers
|
|
3
|
-
def
|
|
4
|
-
Nokogiri::XML::Node.new("
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
text_node.replace(wrap_in_strong(text_node.to_html, text_node.parent))
|
|
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
|
|
9
8
|
end
|
|
10
9
|
end
|
|
11
10
|
end
|
|
@@ -31,9 +31,6 @@ module Nokodiff
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def diff_text_node_content(before_text_node, after_text_node)
|
|
34
|
-
return strong(before_text_node) if text_removed?(before_text_node, after_text_node)
|
|
35
|
-
return strong(after_text_node) if text_added?(before_text_node, after_text_node)
|
|
36
|
-
|
|
37
34
|
before_chars = before_text_node.text.chars
|
|
38
35
|
after_chars = after_text_node.text.chars
|
|
39
36
|
|
|
@@ -44,13 +41,5 @@ module Nokodiff
|
|
|
44
41
|
before_text_node.replace(before_fragment)
|
|
45
42
|
after_text_node.replace(after_fragment)
|
|
46
43
|
end
|
|
47
|
-
|
|
48
|
-
def text_removed?(before_node, after_node)
|
|
49
|
-
before_node && after_node.nil?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def text_added?(before_node, after_node)
|
|
53
|
-
before_node.nil? && after_node
|
|
54
|
-
end
|
|
55
44
|
end
|
|
56
45
|
end
|
data/lib/nokodiff/version.rb
CHANGED
data/lib/nokodiff.rb
CHANGED
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GOV.UK Dev
|
|
@@ -113,6 +113,20 @@ dependencies:
|
|
|
113
113
|
- - "<"
|
|
114
114
|
- !ruby/object:Gem::Version
|
|
115
115
|
version: 8.1.3
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: byebug
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
type: :runtime
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
116
130
|
- !ruby/object:Gem::Dependency
|
|
117
131
|
name: diff-lcs
|
|
118
132
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -230,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
230
244
|
- !ruby/object:Gem::Version
|
|
231
245
|
version: '0'
|
|
232
246
|
requirements: []
|
|
233
|
-
rubygems_version: 4.0.
|
|
247
|
+
rubygems_version: 4.0.9
|
|
234
248
|
specification_version: 4
|
|
235
249
|
summary: A Ruby Gem to highlight additions, deletions and character level changes
|
|
236
250
|
while preserving original HTML
|