publishing_platform_nokodiff 0.1.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 +7 -0
- data/LICENSE +22 -0
- data/README.md +3 -0
- data/app/assets/stylesheets/nokodiff.scss +72 -0
- data/lib/publishing_platform_nokodiff/changes_in_fragments.rb +67 -0
- data/lib/publishing_platform_nokodiff/differ.rb +198 -0
- data/lib/publishing_platform_nokodiff/engine.rb +7 -0
- data/lib/publishing_platform_nokodiff/formatting_helpers.rb +10 -0
- data/lib/publishing_platform_nokodiff/html_fragment.rb +40 -0
- data/lib/publishing_platform_nokodiff/text_node_diffs.rb +51 -0
- data/lib/publishing_platform_nokodiff/version.rb +5 -0
- data/lib/publishing_platform_nokodiff.rb +52 -0
- metadata +214 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3a68f8c34108df7e8436c255154a6d224f9e664fe4cc66a7fd95421563802369
|
|
4
|
+
data.tar.gz: 22ad1fe022f37d8b6b4b16d706a4ea99de0c64fdc8129f8fb90b1c83de7a0ddc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d8bfc56f097ec67694f32314df01b9933c25b8f9d706dd7fde499d25ee3d02256713137491e8319e9dd99d724bce7b529af41d0bbcafb2329d5534adb26d3109
|
|
7
|
+
data.tar.gz: b3d6b296c69de8776b0344ad7dd9497ae1e417d068a26d778fc46043f1ca075bf870f3783b9a011f9726fb227db11272c7cabf3347c076baff4609274d3fab10
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Publishing Platform
|
|
4
|
+
Copyright (c) 2025 Crown Copyright (Government Digital Service)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// stylelint-disable selector-no-qualifying-type, max-nesting-depth
|
|
2
|
+
// Diff of two editions
|
|
3
|
+
$added-color: #e6fff3;
|
|
4
|
+
$diff-marker-added-color: #99ffcf;
|
|
5
|
+
$removed-color: #fadede;
|
|
6
|
+
$diff-marker-removed-color: #f3aeac;
|
|
7
|
+
$text-colour: #0b0c0c;
|
|
8
|
+
$light-grey: #f3f2f1;
|
|
9
|
+
$black: #0b0c0c;
|
|
10
|
+
|
|
11
|
+
.compare-editions {
|
|
12
|
+
position: relative;
|
|
13
|
+
border-left: 40px solid $light-grey;
|
|
14
|
+
padding: 15px;
|
|
15
|
+
|
|
16
|
+
.diff {
|
|
17
|
+
position: static;
|
|
18
|
+
margin-bottom: 10px;
|
|
19
|
+
|
|
20
|
+
del, ins {
|
|
21
|
+
position: static !important;
|
|
22
|
+
display: block;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
border-radius: 3px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
del {
|
|
28
|
+
background-color: $removed-color;
|
|
29
|
+
padding-bottom: 2px;
|
|
30
|
+
|
|
31
|
+
span.diff-marker {
|
|
32
|
+
font-weight: normal;
|
|
33
|
+
background-color: $diff-marker-removed-color;
|
|
34
|
+
border-bottom: 2px dashed $black;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ins {
|
|
39
|
+
background-color: $added-color;
|
|
40
|
+
padding-bottom: 2px;
|
|
41
|
+
|
|
42
|
+
span.diff-marker {
|
|
43
|
+
font-weight: normal;
|
|
44
|
+
background-color: $diff-marker-added-color;
|
|
45
|
+
border-bottom: 2px dashed $black;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
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
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
del::before {
|
|
62
|
+
color: $text-colour;
|
|
63
|
+
background-color: $diff-marker-removed-color;
|
|
64
|
+
content: "−";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ins::before {
|
|
68
|
+
color: $text-colour;
|
|
69
|
+
background-color: $diff-marker-added-color;
|
|
70
|
+
content: "+";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module PublishingPlatformNokodiff
|
|
2
|
+
class ChangesInFragments
|
|
3
|
+
include FormattingHelpers
|
|
4
|
+
def initialize(diff)
|
|
5
|
+
@diff = diff
|
|
6
|
+
@before_fragment = Nokogiri::HTML::DocumentFragment.parse("")
|
|
7
|
+
@after_fragment = Nokogiri::HTML::DocumentFragment.parse("")
|
|
8
|
+
|
|
9
|
+
@accumulated_before_text = ""
|
|
10
|
+
@accumulated_after_text = ""
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
@diff.each do |change|
|
|
15
|
+
case change.action
|
|
16
|
+
when "="
|
|
17
|
+
no_change_emphasis(change)
|
|
18
|
+
when "!"
|
|
19
|
+
emphasise_change(change)
|
|
20
|
+
when "-"
|
|
21
|
+
emphasise_deletion(change)
|
|
22
|
+
when "+"
|
|
23
|
+
emphasise_addition(change)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
append_accumulated_text(before_fragment, accumulated_before_text)
|
|
28
|
+
append_accumulated_text(after_fragment, accumulated_after_text)
|
|
29
|
+
|
|
30
|
+
[before_fragment, after_fragment]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_accessor :before_fragment, :after_fragment, :accumulated_before_text, :accumulated_after_text
|
|
36
|
+
|
|
37
|
+
def no_change_emphasis(change)
|
|
38
|
+
accumulated_before_text << change.old_element
|
|
39
|
+
accumulated_after_text << change.new_element
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def emphasise_change(change)
|
|
43
|
+
append_accumulated_text(before_fragment, accumulated_before_text)
|
|
44
|
+
append_accumulated_text(after_fragment, accumulated_after_text)
|
|
45
|
+
|
|
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
|
+
end
|
|
49
|
+
|
|
50
|
+
def emphasise_deletion(change)
|
|
51
|
+
append_accumulated_text(before_fragment, accumulated_before_text)
|
|
52
|
+
before_fragment.add_child(highlight_changes(change.old_element, before_fragment))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def emphasise_addition(change)
|
|
56
|
+
append_accumulated_text(after_fragment, accumulated_after_text)
|
|
57
|
+
after_fragment.add_child(highlight_changes(change.new_element, after_fragment))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_accumulated_text(fragment, accumulated_text)
|
|
61
|
+
return if accumulated_text.empty?
|
|
62
|
+
|
|
63
|
+
fragment.add_child(Nokogiri::XML::Text.new(accumulated_text, fragment))
|
|
64
|
+
accumulated_text.clear
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
module PublishingPlatformNokodiff
|
|
2
|
+
class Differ
|
|
3
|
+
def initialize(before, after)
|
|
4
|
+
@before = before
|
|
5
|
+
@after = after
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_html
|
|
9
|
+
compared_blocks.map { |diff|
|
|
10
|
+
case diff[:status]
|
|
11
|
+
when :unchanged
|
|
12
|
+
unchanged_block(diff[:before])
|
|
13
|
+
when :changed
|
|
14
|
+
changed_block(diff[:before], diff[:after])
|
|
15
|
+
when :deleted
|
|
16
|
+
diff[:before].name == "li" ? deleted_li(diff[:before]) : deleted_block(diff[:before])
|
|
17
|
+
when :added
|
|
18
|
+
diff[:after].name == "li" ? added_li(diff[:after]) : added_block(diff[:after])
|
|
19
|
+
end
|
|
20
|
+
}.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def compared_blocks
|
|
26
|
+
before_nodes = @before.children.to_a
|
|
27
|
+
after_nodes = @after.children.to_a
|
|
28
|
+
|
|
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
|
|
61
|
+
|
|
62
|
+
def changed_block(before_node, after_node)
|
|
63
|
+
if structurally_similar?(before_node, after_node) && should_not_be_treated_as_single_change?(before_node)
|
|
64
|
+
inner_diff = Differ.new(before_node, after_node).to_html
|
|
65
|
+
rebuild_element(after_node, inner_diff)
|
|
66
|
+
else
|
|
67
|
+
before_diff, after_diff = if both_text_nodes?(before_node, after_node)
|
|
68
|
+
diff_raw_text(before_node, after_node)
|
|
69
|
+
else
|
|
70
|
+
diff_sub_elements(before_node, after_node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if before_node.name == "li"
|
|
74
|
+
deleted_li(before_diff) + added_li(after_diff)
|
|
75
|
+
else
|
|
76
|
+
deleted_block(before_diff) + added_block(after_diff)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def both_text_nodes?(before_node, after_node)
|
|
82
|
+
before_node.text? && after_node.text?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def structurally_similar?(before_node, after_node)
|
|
86
|
+
before_node.element? &&
|
|
87
|
+
after_node.element? &&
|
|
88
|
+
before_node.name == after_node.name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# We want all changes within a paragraph, heading, or list item to be treated as a single change, even if they are
|
|
92
|
+
# structurally different, to avoid overwhelming the user with changes, and ensure any nested elements are included
|
|
93
|
+
# within the diff, rather than being treated as added or removed content on their own.
|
|
94
|
+
def should_not_be_treated_as_single_change?(before_node)
|
|
95
|
+
before_node.name != "p" &&
|
|
96
|
+
!before_node.name.match(/^h[1-6]$/) &&
|
|
97
|
+
before_node.name != "li"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rebuild_element(template_node, inner_html)
|
|
101
|
+
result = template_node.dup
|
|
102
|
+
result.inner_html = inner_html
|
|
103
|
+
result.to_html
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def diff_raw_text(before_text, after_text)
|
|
107
|
+
diff = Diff::LCS.sdiff(before_text.text.chars, after_text.text.chars)
|
|
108
|
+
before_fragment, after_fragment = PublishingPlatformNokodiff::ChangesInFragments.new(diff).call
|
|
109
|
+
[merge_fragment_spans(before_fragment), merge_fragment_spans(after_fragment)]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def merge_fragment_spans(fragment)
|
|
113
|
+
doc = fragment.document
|
|
114
|
+
wrapper = Nokogiri::XML::Node.new("span", doc)
|
|
115
|
+
wrapper.inner_html = fragment.to_html
|
|
116
|
+
merge_adjacent_highlighted_changes(wrapper)
|
|
117
|
+
wrapper.inner_html
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def diff_sub_elements(before_html, after_html)
|
|
121
|
+
before_dup = before_html.dup
|
|
122
|
+
after_dup = after_html.dup
|
|
123
|
+
|
|
124
|
+
before_fragment, after_fragment = PublishingPlatformNokodiff::TextNodeDiffs.new(before_dup, after_dup).call
|
|
125
|
+
|
|
126
|
+
merge_adjacent_highlighted_changes(before_fragment)
|
|
127
|
+
merge_adjacent_highlighted_changes(after_fragment)
|
|
128
|
+
|
|
129
|
+
if before_html.name == "li"
|
|
130
|
+
[before_fragment.inner_html, after_fragment.inner_html]
|
|
131
|
+
else
|
|
132
|
+
[before_fragment.to_html, after_fragment.to_html]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def merge_adjacent_highlighted_changes(node)
|
|
137
|
+
return unless node.element?
|
|
138
|
+
|
|
139
|
+
node.children.each do |child|
|
|
140
|
+
merge_adjacent_highlighted_changes(child) if child.element?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
node.children.each_cons(2) do |left, right|
|
|
144
|
+
next unless node_is_a_change?(left) && node_is_a_change?(right)
|
|
145
|
+
|
|
146
|
+
left.content = left.content + right.content
|
|
147
|
+
right.remove
|
|
148
|
+
|
|
149
|
+
merge_adjacent_highlighted_changes(node)
|
|
150
|
+
break
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def node_is_a_change?(node)
|
|
155
|
+
node.name == "span" && node["class"] == "diff-marker"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def unchanged_block(node)
|
|
159
|
+
node.to_html
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def deleted_li(html)
|
|
163
|
+
%(
|
|
164
|
+
<li>
|
|
165
|
+
<div class="diff">
|
|
166
|
+
<del aria-label="removed content">#{html}</del>
|
|
167
|
+
</div>
|
|
168
|
+
</li>
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def deleted_block(html)
|
|
173
|
+
%(
|
|
174
|
+
<div class="diff">
|
|
175
|
+
<del aria-label="removed content">#{html}</del>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def added_block(html)
|
|
181
|
+
%(
|
|
182
|
+
<div class="diff">
|
|
183
|
+
<ins aria-label="added content">#{html}</ins>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def added_li(html)
|
|
189
|
+
%(
|
|
190
|
+
<li>
|
|
191
|
+
<div class="diff">
|
|
192
|
+
<ins aria-label="added content">#{html}</ins>
|
|
193
|
+
</div>
|
|
194
|
+
</li>
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
|
|
3
|
+
module PublishingPlatformNokodiff
|
|
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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module PublishingPlatformNokodiff
|
|
2
|
+
class TextNodeDiffs
|
|
3
|
+
include FormattingHelpers
|
|
4
|
+
def initialize(before_fragment, after_fragment)
|
|
5
|
+
@before_fragment = before_fragment
|
|
6
|
+
@after_fragment = after_fragment
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
diff_text_nodes(before_fragment, after_fragment)
|
|
11
|
+
[before_fragment, after_fragment]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_accessor :before_fragment, :after_fragment
|
|
17
|
+
|
|
18
|
+
def diff_text_nodes(before_node, after_node)
|
|
19
|
+
if before_node&.text? || after_node&.text?
|
|
20
|
+
diff_text_node_content(before_node, after_node)
|
|
21
|
+
elsif before_node&.element? || after_node&.element?
|
|
22
|
+
before_children = before_node ? before_node.children.to_a : []
|
|
23
|
+
after_children = after_node ? after_node.children.to_a : []
|
|
24
|
+
|
|
25
|
+
max_child_count = [before_children.length, after_children.length].max
|
|
26
|
+
|
|
27
|
+
(0..max_child_count).each do |i|
|
|
28
|
+
diff_text_nodes(before_children[i], after_children[i])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def diff_text_node_content(before_text_node, after_text_node)
|
|
34
|
+
before_chars = get_chars(before_text_node)
|
|
35
|
+
after_chars = get_chars(after_text_node)
|
|
36
|
+
|
|
37
|
+
diff = Diff::LCS.sdiff(before_chars, after_chars)
|
|
38
|
+
|
|
39
|
+
before_fragment, after_fragment = PublishingPlatformNokodiff::ChangesInFragments.new(diff).call
|
|
40
|
+
|
|
41
|
+
before_text_node&.replace(before_fragment)
|
|
42
|
+
after_text_node&.replace(after_fragment)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_chars(text_node)
|
|
46
|
+
return [] if text_node.nil?
|
|
47
|
+
|
|
48
|
+
text_node.text.chars
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "diff-lcs"
|
|
5
|
+
require "byebug"
|
|
6
|
+
|
|
7
|
+
require_relative "publishing_platform_nokodiff/formatting_helpers"
|
|
8
|
+
require_relative "publishing_platform_nokodiff/version"
|
|
9
|
+
require_relative "publishing_platform_nokodiff/differ"
|
|
10
|
+
require_relative "publishing_platform_nokodiff/engine"
|
|
11
|
+
require_relative "publishing_platform_nokodiff/text_node_diffs"
|
|
12
|
+
require_relative "publishing_platform_nokodiff/changes_in_fragments"
|
|
13
|
+
require_relative "publishing_platform_nokodiff/html_fragment"
|
|
14
|
+
|
|
15
|
+
module PublishingPlatformNokodiff
|
|
16
|
+
def self.diff(before_html, after_html)
|
|
17
|
+
before = PublishingPlatformNokodiff::HTMLFragment.new(before_html)
|
|
18
|
+
after = PublishingPlatformNokodiff::HTMLFragment.new(after_html)
|
|
19
|
+
|
|
20
|
+
before_nodes, after_nodes = nodes(before, after)
|
|
21
|
+
keys = (before_nodes.keys + after_nodes.keys).uniq
|
|
22
|
+
|
|
23
|
+
html = keys.any? ? diff_by_keys(after, keys, before_nodes, after_nodes) : Differ.new(before, after).to_html
|
|
24
|
+
safe_html(html)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.safe_html(html)
|
|
28
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_class_method def self.nodes(before, after)
|
|
32
|
+
[
|
|
33
|
+
fetch_diff_nodes(before),
|
|
34
|
+
fetch_diff_nodes(after),
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_class_method def self.fetch_diff_nodes(fragment)
|
|
39
|
+
fragment.css("[data-diff-key]").map { |node| [node["data-diff-key"], node] }.to_h
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method def self.diff_by_keys(after, keys, before_nodes, after_nodes)
|
|
43
|
+
keys.each do |key|
|
|
44
|
+
diff = Differ.new(
|
|
45
|
+
before_nodes.fetch(key, Nokogiri::HTML.fragment("")),
|
|
46
|
+
after_nodes.fetch(key, Nokogiri::HTML.fragment("")),
|
|
47
|
+
).to_html
|
|
48
|
+
after.at("[data-diff-key='#{key}']")&.inner_html = diff
|
|
49
|
+
end
|
|
50
|
+
after.to_html
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: publishing_platform_nokodiff
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Publishing Platform
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: publishing_platform_rubocop
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - '='
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 13.4.2
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - '='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 13.4.2
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-html-matchers
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - '='
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 0.10.0
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - '='
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 0.10.0
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec-rails
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: simplecov
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: actionview
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '6'
|
|
103
|
+
- - "<"
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 8.1.4
|
|
106
|
+
type: :runtime
|
|
107
|
+
prerelease: false
|
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '6'
|
|
113
|
+
- - "<"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 8.1.4
|
|
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'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: diff-lcs
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
type: :runtime
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: rails
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '6'
|
|
151
|
+
- - "<"
|
|
152
|
+
- !ruby/object:Gem::Version
|
|
153
|
+
version: 8.1.4
|
|
154
|
+
type: :runtime
|
|
155
|
+
prerelease: false
|
|
156
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '6'
|
|
161
|
+
- - "<"
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: 8.1.4
|
|
164
|
+
- !ruby/object:Gem::Dependency
|
|
165
|
+
name: view_component
|
|
166
|
+
requirement: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - "~>"
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '4'
|
|
171
|
+
type: :runtime
|
|
172
|
+
prerelease: false
|
|
173
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
174
|
+
requirements:
|
|
175
|
+
- - "~>"
|
|
176
|
+
- !ruby/object:Gem::Version
|
|
177
|
+
version: '4'
|
|
178
|
+
executables: []
|
|
179
|
+
extensions: []
|
|
180
|
+
extra_rdoc_files: []
|
|
181
|
+
files:
|
|
182
|
+
- LICENSE
|
|
183
|
+
- README.md
|
|
184
|
+
- app/assets/stylesheets/nokodiff.scss
|
|
185
|
+
- lib/publishing_platform_nokodiff.rb
|
|
186
|
+
- lib/publishing_platform_nokodiff/changes_in_fragments.rb
|
|
187
|
+
- lib/publishing_platform_nokodiff/differ.rb
|
|
188
|
+
- lib/publishing_platform_nokodiff/engine.rb
|
|
189
|
+
- lib/publishing_platform_nokodiff/formatting_helpers.rb
|
|
190
|
+
- lib/publishing_platform_nokodiff/html_fragment.rb
|
|
191
|
+
- lib/publishing_platform_nokodiff/text_node_diffs.rb
|
|
192
|
+
- lib/publishing_platform_nokodiff/version.rb
|
|
193
|
+
licenses:
|
|
194
|
+
- MIT
|
|
195
|
+
metadata: {}
|
|
196
|
+
rdoc_options: []
|
|
197
|
+
require_paths:
|
|
198
|
+
- lib
|
|
199
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
200
|
+
requirements:
|
|
201
|
+
- - ">="
|
|
202
|
+
- !ruby/object:Gem::Version
|
|
203
|
+
version: '3.2'
|
|
204
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - ">="
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
208
|
+
version: '0'
|
|
209
|
+
requirements: []
|
|
210
|
+
rubygems_version: 4.0.15
|
|
211
|
+
specification_version: 4
|
|
212
|
+
summary: A Ruby Gem to highlight additions, deletions and character level changes
|
|
213
|
+
while preserving original HTML
|
|
214
|
+
test_files: []
|