nokodiff 0.4.1 → 1.0.0.rc1
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 +21 -30
- data/app/assets/stylesheets/nokodiff.scss +299 -29
- data/lib/nokodiff/differ.rb +98 -26
- data/lib/nokodiff/formatting_helpers.rb +8 -0
- data/lib/nokodiff/version.rb +1 -1
- metadata +12 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98b3f977e0c9f7e35ccff54cbab81944e22bbcd3b7bb29a5a383fbd0a2668186
|
|
4
|
+
data.tar.gz: 7dbb7c2c74e1d85d245a48722e91d2ab8f89dde3508e32cfb3fe350ea30fd75b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a8f9d05204c541ceba28f32d5d0f233dacf510d6b03391aab292fca7e649ca302b291543b14a9c27cfdc685d9fb3d250d66ebd9aa60107ffd36413c6b8c1256
|
|
7
|
+
data.tar.gz: d43cf00ad191e146e35f9a581d8a69de0076eec5618c94361e899a74cc11e2aba51deeda8bf99d16572b3da42397d79e7cc84704f16220fd22c04e3f0b2eb901
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nokodiff
|
|
2
2
|
|
|
3
|
-
A Ruby Gem to highlight additions, deletions and character level changes while preserving original HTML.
|
|
3
|
+
A Ruby Gem to highlight additions, deletions, and character level changes while preserving original HTML.
|
|
4
4
|
|
|
5
5
|
It includes functionality to:
|
|
6
6
|
* Compare two HTML fragments and output diffs with semantic HTML
|
|
@@ -24,31 +24,21 @@ gem "nokodiff"
|
|
|
24
24
|
|
|
25
25
|
## Local Development
|
|
26
26
|
|
|
27
|
-
The gem includes a dummy Rails application located in `spec/dummy`. It can be used to preview `Nokodiff` in a real
|
|
27
|
+
The gem includes a dummy Rails application located in `spec/dummy`. It can be used to preview `Nokodiff` in a real
|
|
28
|
+
browser environment without needing to integrate with an external host app.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### Install dependencies
|
|
30
|
+
You can run the dummy app with the following command:
|
|
32
31
|
|
|
33
32
|
```bash
|
|
34
|
-
|
|
33
|
+
bin/run-dummy-app
|
|
35
34
|
```
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
cd spec/dummy
|
|
41
|
-
bundle install
|
|
42
|
-
```
|
|
36
|
+
This installs the dependencies, builds the assets, and starts the server.
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
bin/rails server
|
|
48
|
-
```
|
|
38
|
+
The dummy app will now be running on `localhost:3000`.
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
If you want to edit the content you are testing it is located in `spec/dummy/fixtures/before.html`, and
|
|
41
|
+
`spec/dummy/fixtures/after.html`.
|
|
52
42
|
|
|
53
43
|
## Usage
|
|
54
44
|
|
|
@@ -76,7 +66,7 @@ In your application.scss file include:
|
|
|
76
66
|
@import "nokodiff";
|
|
77
67
|
```
|
|
78
68
|
|
|
79
|
-
This will include the styling for
|
|
69
|
+
This will include the styling for `.diff`, `.del`, `.ins` and other classes and `<span>` tags to allow colour coding, highlighting, and underlining of changes.
|
|
80
70
|
|
|
81
71
|
### More complex diffing with `data-diff-key`
|
|
82
72
|
|
|
@@ -121,9 +111,7 @@ Output:
|
|
|
121
111
|
<p>First paragraph</p>
|
|
122
112
|
</div>
|
|
123
113
|
<div data-diff-key="zm7q">
|
|
124
|
-
|
|
125
|
-
<ins aria-label="added content"><p>Second paragraph</p></ins>
|
|
126
|
-
</div>
|
|
114
|
+
<p class="diff ins"><span class="visually-hidden">Added content </span>Second paragraph</p>
|
|
127
115
|
</div>
|
|
128
116
|
</div>
|
|
129
117
|
```
|
|
@@ -164,15 +152,18 @@ Output:
|
|
|
164
152
|
|
|
165
153
|
```html
|
|
166
154
|
<div>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<div class="diff">
|
|
170
|
-
<del aria-label="removed content"><p><span class="diff-marker">S</span>e<span class="diff-marker">cond</span> paragraph</p></del>
|
|
155
|
+
<div data-diff-key="ixn4">
|
|
156
|
+
<p>First paragraph</p>
|
|
171
157
|
</div>
|
|
172
|
-
<div
|
|
173
|
-
|
|
158
|
+
<div data-diff-key="zm7q">
|
|
159
|
+
<p class="diff del">
|
|
160
|
+
<span class="diff-marker"><span class="visually-hidden">Diff marker </span><span class="visually-hidden">Removed content </span>S</span>e<span class="diff-marker"><span class="visually-hidden">Diff marker </span>cond</span> paragraph
|
|
161
|
+
</p>
|
|
162
|
+
|
|
163
|
+
<p class="diff ins">
|
|
164
|
+
<span class="diff-marker"><span class="visually-hidden">Diff marker </span><span class="visually-hidden">Added content </span>N</span>e<span class="diff-marker"><span class="visually-hidden">Diff marker </span>w</span> paragraph
|
|
165
|
+
</p>
|
|
174
166
|
</div>
|
|
175
|
-
</div>
|
|
176
167
|
</div>
|
|
177
168
|
```
|
|
178
169
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// stylelint-disable selector-no-qualifying-type, max-nesting-depth
|
|
2
|
-
// Diff of two editions
|
|
3
1
|
$added-color: #e6fff3;
|
|
4
2
|
$diff-marker-added-color: #99ffcf;
|
|
5
3
|
$removed-color: #fadede;
|
|
@@ -7,66 +5,338 @@ $diff-marker-removed-color: #f3aeac;
|
|
|
7
5
|
$text-colour: #0b0c0c;
|
|
8
6
|
$light-grey: #f3f2f1;
|
|
9
7
|
$black: #0b0c0c;
|
|
8
|
+
$comparison-sidebar-width: 40px;
|
|
9
|
+
$comparison-sidebar-padding: 15px;
|
|
10
|
+
$comparison-sidebar-offset: $comparison-sidebar-width + $comparison-sidebar-padding;
|
|
11
|
+
|
|
12
|
+
.visually-hidden {
|
|
13
|
+
position: absolute !important;
|
|
14
|
+
width: 1px !important;
|
|
15
|
+
height: 1px !important;
|
|
16
|
+
// If margin is set to a negative value it can cause text to be announced in
|
|
17
|
+
// the wrong order in VoiceOver for OSX
|
|
18
|
+
margin: 0 !important;
|
|
19
|
+
padding: 0 !important;
|
|
20
|
+
overflow: hidden !important;
|
|
21
|
+
|
|
22
|
+
clip: rect(0 0 0 0) !important;
|
|
23
|
+
clip-path: inset(50%) !important;
|
|
24
|
+
|
|
25
|
+
border: 0 !important;
|
|
26
|
+
|
|
27
|
+
// For long content, line feeds are not interpreted as spaces and small width
|
|
28
|
+
// causes content to wrap 1 word per line:
|
|
29
|
+
// https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
|
|
30
|
+
white-space: nowrap !important;
|
|
31
|
+
|
|
32
|
+
// Prevent users from selecting or copying visually-hidden text. This prevents
|
|
33
|
+
// a user unintentionally copying more text than they intended and needing to
|
|
34
|
+
// manually trim it down again.
|
|
35
|
+
user-select: none !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.visually-hidden {
|
|
39
|
+
position: absolute !important;
|
|
40
|
+
width: 1px !important;
|
|
41
|
+
height: 1px !important;
|
|
42
|
+
// If margin is set to a negative value it can cause text to be announced in
|
|
43
|
+
// the wrong order in VoiceOver for OSX
|
|
44
|
+
margin: 0 !important;
|
|
45
|
+
padding: 0 !important;
|
|
46
|
+
overflow: hidden !important;
|
|
47
|
+
|
|
48
|
+
clip: rect(0 0 0 0) !important;
|
|
49
|
+
clip-path: inset(50%) !important;
|
|
50
|
+
|
|
51
|
+
border: 0 !important;
|
|
52
|
+
|
|
53
|
+
// For long content, line feeds are not interpreted as spaces and small width
|
|
54
|
+
// causes content to wrap 1 word per line:
|
|
55
|
+
// https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
|
|
56
|
+
white-space: nowrap !important;
|
|
57
|
+
|
|
58
|
+
// Prevent users from selecting or copying visually-hidden text. This prevents
|
|
59
|
+
// a user unintentionally copying more text than they intended and needing to
|
|
60
|
+
// manually trim it down again.
|
|
61
|
+
user-select: none !important;
|
|
62
|
+
|
|
63
|
+
}
|
|
10
64
|
|
|
11
65
|
.compare-editions {
|
|
12
66
|
position: relative;
|
|
13
|
-
border-left:
|
|
14
|
-
padding:
|
|
67
|
+
border-left: $comparison-sidebar-width solid $light-grey;
|
|
68
|
+
padding: $comparison-sidebar-padding;
|
|
69
|
+
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
|
|
70
|
+
|
|
71
|
+
td::before {
|
|
72
|
+
// Backup to handle any inputted classes which make use of a ::before attached to a td element
|
|
73
|
+
// unsetting all styles for it will prevent it interfering with the ::before that we need to use
|
|
74
|
+
// for the diff marker
|
|
75
|
+
all: unset;
|
|
76
|
+
}
|
|
15
77
|
|
|
16
78
|
.diff {
|
|
17
79
|
position: static;
|
|
18
80
|
margin-bottom: 10px;
|
|
19
81
|
|
|
20
|
-
del, ins
|
|
21
|
-
|
|
82
|
+
&.del, &.ins {
|
|
83
|
+
&:is(li){ display: list-item; }
|
|
84
|
+
|
|
85
|
+
position: relative;
|
|
22
86
|
display: block;
|
|
23
87
|
text-decoration: none;
|
|
24
|
-
|
|
88
|
+
background-color: transparent;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&::after {
|
|
92
|
+
content: "";
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 0;
|
|
95
|
+
bottom: 0;
|
|
96
|
+
left: -$comparison-sidebar-padding;
|
|
97
|
+
min-height: 1.5em;
|
|
98
|
+
width: 100vw;
|
|
99
|
+
display: block;
|
|
100
|
+
z-index: -1;
|
|
25
101
|
}
|
|
26
102
|
|
|
27
|
-
del {
|
|
28
|
-
background-color: $removed-color;
|
|
29
|
-
padding-bottom: 2px;
|
|
103
|
+
&.del {
|
|
104
|
+
&::after { background-color: $removed-color; }
|
|
30
105
|
|
|
31
106
|
span.diff-marker {
|
|
32
|
-
font-weight: normal;
|
|
33
107
|
background-color: $diff-marker-removed-color;
|
|
34
108
|
border-bottom: 2px dashed $black;
|
|
35
109
|
}
|
|
36
110
|
}
|
|
37
111
|
|
|
38
|
-
ins {
|
|
39
|
-
background-color: $added-color;
|
|
40
|
-
padding-bottom: 2px;
|
|
112
|
+
&.ins {
|
|
113
|
+
&::after { background-color: $added-color; }
|
|
41
114
|
|
|
42
115
|
span.diff-marker {
|
|
43
|
-
font-weight: normal;
|
|
44
116
|
background-color: $diff-marker-added-color;
|
|
45
117
|
border-bottom: 2px dashed $black;
|
|
46
118
|
}
|
|
47
119
|
}
|
|
48
120
|
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
&::before {
|
|
122
|
+
// Some govuk styles use ::before in ways we need to override to make this work
|
|
123
|
+
border-radius: unset !important;
|
|
124
|
+
border: unset !important;
|
|
125
|
+
|
|
51
126
|
position: absolute;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
127
|
+
top: 0;
|
|
128
|
+
left: -$comparison-sidebar-offset;
|
|
129
|
+
width: $comparison-sidebar-width;
|
|
130
|
+
height: 100%;
|
|
131
|
+
min-height: 1.5em;
|
|
55
132
|
line-height: 1.5em;
|
|
56
133
|
text-align: center;
|
|
134
|
+
font-weight: normal;
|
|
57
135
|
z-index: 10;
|
|
58
136
|
}
|
|
137
|
+
|
|
138
|
+
&.del::before {
|
|
139
|
+
color: $text-colour;
|
|
140
|
+
background-color: $diff-marker-removed-color;
|
|
141
|
+
content: "−" !important;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&.ins::before {
|
|
145
|
+
color: $text-colour;
|
|
146
|
+
background-color: $diff-marker-added-color;
|
|
147
|
+
content: "+" !important;
|
|
148
|
+
}
|
|
59
149
|
}
|
|
60
150
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
content: "−";
|
|
151
|
+
// Global govspeak-specific overrides
|
|
152
|
+
.gem-c-govspeak & {
|
|
153
|
+
&::before { min-height: 1.3157894737em; }
|
|
65
154
|
}
|
|
66
155
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
156
|
+
// Inset/callout elements overrides
|
|
157
|
+
|
|
158
|
+
.address {
|
|
159
|
+
.diff{
|
|
160
|
+
$address-offset: -16px;
|
|
161
|
+
|
|
162
|
+
&::after {
|
|
163
|
+
left: -$comparison-sidebar-padding + $address-offset;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&::before {
|
|
167
|
+
left: -$comparison-sidebar-offset + $address-offset;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
71
170
|
}
|
|
72
|
-
|
|
171
|
+
|
|
172
|
+
.help-notice {
|
|
173
|
+
$help-notice-offset: -45px;
|
|
174
|
+
.diff {
|
|
175
|
+
&::after, &::before {
|
|
176
|
+
top: -15px;
|
|
177
|
+
min-height: calc(1.5em + 30px);
|
|
178
|
+
left: -$comparison-sidebar-padding + $help-notice-offset;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&::before {
|
|
182
|
+
top: -15px;
|
|
183
|
+
min-height: calc(1.5em + 30px);
|
|
184
|
+
left: -$comparison-sidebar-offset + $help-notice-offset;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.example, .info-notice {
|
|
190
|
+
$callout-parent-offset: -10px;
|
|
191
|
+
$callout-child-offset: -30px;
|
|
192
|
+
|
|
193
|
+
&.diff {
|
|
194
|
+
&::after {
|
|
195
|
+
left: -$comparison-sidebar-padding + $callout-parent-offset;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
&::before {
|
|
199
|
+
left: -$comparison-sidebar-offset + $callout-parent-offset;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.diff {
|
|
204
|
+
&::after {
|
|
205
|
+
left: -$comparison-sidebar-padding + $callout-child-offset;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&::before {
|
|
209
|
+
left: -$comparison-sidebar-offset + $callout-child-offset;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// List-specific overrides for non-govuk lists
|
|
215
|
+
$non-govuk-list-offset: -40px;
|
|
216
|
+
|
|
217
|
+
li span.diff, ul span.diff{
|
|
218
|
+
&::after {
|
|
219
|
+
left: -$comparison-sidebar-padding + $non-govuk-list-offset;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
&::before {
|
|
223
|
+
left: -$comparison-sidebar-offset + $non-govuk-list-offset;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// List-specific overrides for govuk lists as we apply a non-standard offset to lists
|
|
228
|
+
$govuk-list-offset: -20px;
|
|
229
|
+
|
|
230
|
+
.gem-c-govspeak & {
|
|
231
|
+
ul li span.diff, ul span.diff {
|
|
232
|
+
&::after {
|
|
233
|
+
left: -$comparison-sidebar-padding + $govuk-list-offset;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
&::before {
|
|
237
|
+
left: -$comparison-sidebar-offset + $govuk-list-offset;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ol li span.diff {
|
|
242
|
+
list-style-type: decimal;
|
|
243
|
+
|
|
244
|
+
&::marker {
|
|
245
|
+
background-color: #0b0c0c;
|
|
246
|
+
border-radius: 50%;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
&::after {
|
|
250
|
+
left: $comparison-sidebar-padding;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
&::before {
|
|
254
|
+
left: $comparison-sidebar-offset;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//govuk "steps"-style ol overrides
|
|
259
|
+
$steps-list-offset: -48px;
|
|
260
|
+
|
|
261
|
+
ol.steps {
|
|
262
|
+
li{
|
|
263
|
+
display: list-item;
|
|
264
|
+
list-style-type: decimal;
|
|
265
|
+
padding-left: 0.25em;
|
|
266
|
+
margin-left: 2.25em;
|
|
267
|
+
|
|
268
|
+
&::before {
|
|
269
|
+
display: none;
|
|
270
|
+
}
|
|
271
|
+
&::after { left: -$comparison-sidebar-padding + $steps-list-offset !important; }
|
|
272
|
+
&::marker { content: "#."; }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
li span.diff {
|
|
276
|
+
&::marker { content: unset; }
|
|
277
|
+
&::before {
|
|
278
|
+
top: -2px;
|
|
279
|
+
left: -$comparison-sidebar-offset + $steps-list-offset;
|
|
280
|
+
height: 100%;
|
|
281
|
+
width: $comparison-sidebar-width;
|
|
282
|
+
min-height: 1.6em;
|
|
283
|
+
}
|
|
284
|
+
&::after {
|
|
285
|
+
top: -2px;
|
|
286
|
+
left: -$comparison-sidebar-padding + $steps-list-offset;
|
|
287
|
+
min-height: 1.6em;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Table-specific overrides
|
|
294
|
+
$table-row-offset: -2px;
|
|
295
|
+
|
|
296
|
+
table {
|
|
297
|
+
overflow-x: visible;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
tr.diff {
|
|
301
|
+
position: static;
|
|
302
|
+
display: table-row;
|
|
303
|
+
transform: scale(1);
|
|
304
|
+
|
|
305
|
+
&.del::after, &.ins::after {
|
|
306
|
+
left: -$comparison-sidebar-padding + $table-row-offset;
|
|
307
|
+
// Need to account for border in row element
|
|
308
|
+
height: 100%;
|
|
309
|
+
top: 1px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Attaching a ::before psuedoelement to a <tr> creates a phantom column so we need to hide it
|
|
313
|
+
// and instead attach one to the first-child <td>
|
|
314
|
+
&::before {
|
|
315
|
+
display: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
td:first-child::before, th:first-child::before {
|
|
319
|
+
position: absolute;
|
|
320
|
+
left: -$comparison-sidebar-offset;
|
|
321
|
+
width: $comparison-sidebar-width;
|
|
322
|
+
height: 100%;
|
|
323
|
+
min-height: 1.5em;
|
|
324
|
+
top: 1px;
|
|
325
|
+
line-height: 1.5em;
|
|
326
|
+
text-align: center;
|
|
327
|
+
z-index: 10;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
&.del td:first-child::before, &.del th:first-child::before {
|
|
331
|
+
color: $text-colour;
|
|
332
|
+
background-color: $diff-marker-removed-color;
|
|
333
|
+
content: "−";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
&.ins td:first-child::before, &.ins th:first-child::before {
|
|
337
|
+
color: $text-colour;
|
|
338
|
+
background-color: $diff-marker-added-color;
|
|
339
|
+
content: "+";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
data/lib/nokodiff/differ.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module Nokodiff
|
|
2
2
|
class Differ
|
|
3
|
+
include FormattingHelpers
|
|
4
|
+
|
|
5
|
+
BOTTOM_LEVEL_ELEMENTS = %w[p li tr h1 h2 h3 h4 h5 h6].freeze
|
|
6
|
+
|
|
3
7
|
def initialize(before, after)
|
|
4
8
|
@before = before
|
|
5
9
|
@after = after
|
|
@@ -13,9 +17,9 @@ module Nokodiff
|
|
|
13
17
|
when :changed
|
|
14
18
|
changed_block(diff[:before], diff[:after])
|
|
15
19
|
when :deleted
|
|
16
|
-
|
|
20
|
+
diff[:before].text? ? build_deleted_text_html(diff[:before]) : build_deleted_element_html(diff[:before])
|
|
17
21
|
when :added
|
|
18
|
-
|
|
22
|
+
diff[:after].text? ? build_added_text_html(diff[:after]) : build_added_element_html(diff[:after])
|
|
19
23
|
end
|
|
20
24
|
}.join("\n")
|
|
21
25
|
end
|
|
@@ -60,23 +64,33 @@ module Nokodiff
|
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def changed_block(before_node, after_node)
|
|
63
|
-
if structurally_similar?(before_node, after_node)
|
|
67
|
+
if structurally_similar?(before_node, after_node) && !treat_element_as_single_change?(before_node)
|
|
64
68
|
inner_diff = Differ.new(before_node, after_node).to_html
|
|
65
69
|
rebuild_element(after_node, inner_diff)
|
|
66
|
-
elsif before_node
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
elsif both_text_nodes?(before_node, after_node)
|
|
71
|
+
before_node, after_node = diff_raw_text(before_node, after_node)
|
|
72
|
+
build_deleted_text_html(before_node) + build_added_text_html(after_node)
|
|
69
73
|
else
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
before_node, after_node = diff_sub_elements(before_node, after_node)
|
|
75
|
+
build_deleted_element_html(before_node) + build_added_element_html(after_node)
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
|
|
79
|
+
def both_text_nodes?(before_node, after_node)
|
|
80
|
+
before_node.text? && after_node.text?
|
|
81
|
+
end
|
|
82
|
+
|
|
75
83
|
def structurally_similar?(before_node, after_node)
|
|
76
84
|
before_node.element? &&
|
|
77
85
|
after_node.element? &&
|
|
78
|
-
before_node.name == after_node.name
|
|
79
|
-
|
|
86
|
+
before_node.name == after_node.name
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# We want to treat all content within certain elements as single changes, even if they are structurally different,
|
|
90
|
+
# to avoid overwhelming the user with changes, and ensure any nested elements are included within the diff,
|
|
91
|
+
# rather than being treated as added or removed content on their own.
|
|
92
|
+
def treat_element_as_single_change?(before_node)
|
|
93
|
+
BOTTOM_LEVEL_ELEMENTS.include?(before_node.name)
|
|
80
94
|
end
|
|
81
95
|
|
|
82
96
|
def rebuild_element(template_node, inner_html)
|
|
@@ -100,15 +114,15 @@ module Nokodiff
|
|
|
100
114
|
end
|
|
101
115
|
|
|
102
116
|
def diff_sub_elements(before_html, after_html)
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
before_fragment = before_html.dup
|
|
118
|
+
after_fragment = after_html.dup
|
|
105
119
|
|
|
106
|
-
|
|
120
|
+
Nokodiff::TextNodeDiffs.new(before_fragment, after_fragment).call
|
|
107
121
|
|
|
108
122
|
merge_adjacent_highlighted_changes(before_fragment)
|
|
109
123
|
merge_adjacent_highlighted_changes(after_fragment)
|
|
110
124
|
|
|
111
|
-
[before_fragment
|
|
125
|
+
[before_fragment, after_fragment]
|
|
112
126
|
end
|
|
113
127
|
|
|
114
128
|
def merge_adjacent_highlighted_changes(node)
|
|
@@ -137,20 +151,78 @@ module Nokodiff
|
|
|
137
151
|
node.to_html
|
|
138
152
|
end
|
|
139
153
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
def build_deleted_text_html(html)
|
|
155
|
+
<<~HTML
|
|
156
|
+
<span class="diff del">
|
|
157
|
+
<span class="visually-hidden">Removed content </span>
|
|
158
|
+
#{html}
|
|
159
|
+
</span>
|
|
160
|
+
HTML
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_added_text_html(html)
|
|
164
|
+
<<~HTML
|
|
165
|
+
<span class="diff ins">
|
|
166
|
+
<span class="visually-hidden">Added content </span>
|
|
167
|
+
#{html}
|
|
168
|
+
</span>
|
|
169
|
+
HTML
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_added_element_html(element)
|
|
173
|
+
if element.name == "tr"
|
|
174
|
+
insert_table_row_change_marker(element, "Added row")
|
|
175
|
+
class_string = "#{element['class']&.gsub('del', '')} diff ins".strip
|
|
176
|
+
|
|
177
|
+
<<~HTML
|
|
178
|
+
<#{element.name} class="#{class_string}">
|
|
179
|
+
#{element.inner_html}
|
|
180
|
+
</#{element.name}>
|
|
181
|
+
HTML
|
|
182
|
+
|
|
183
|
+
else
|
|
184
|
+
# Entirely new steps OLs require that the div also have the .steps class
|
|
185
|
+
# This is either a bug or weakness in the actual govuk CSS for that class
|
|
186
|
+
class_string = element["class"].nil? ? nil : " class= \"#{element['class']}\""
|
|
187
|
+
span_class_string = element["class"]&.include?("steps") ? "diff ins steps" : "diff ins"
|
|
188
|
+
|
|
189
|
+
<<~HTML
|
|
190
|
+
<#{element.name}#{class_string}>
|
|
191
|
+
<span class="#{span_class_string}">
|
|
192
|
+
<span class="visually-hidden">Added content </span>
|
|
193
|
+
#{element.inner_html}
|
|
194
|
+
</span>
|
|
195
|
+
</#{element.name}>
|
|
196
|
+
HTML
|
|
197
|
+
end
|
|
146
198
|
end
|
|
147
199
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
200
|
+
def build_deleted_element_html(element)
|
|
201
|
+
if element.name == "tr"
|
|
202
|
+
insert_table_row_change_marker(element, "Removed row")
|
|
203
|
+
class_string = "#{element['class']&.gsub('ins', '')} diff del".strip
|
|
204
|
+
|
|
205
|
+
<<~HTML
|
|
206
|
+
<#{element.name} class="#{class_string}">
|
|
207
|
+
#{element.inner_html}
|
|
208
|
+
</#{element.name}>
|
|
209
|
+
HTML
|
|
210
|
+
|
|
211
|
+
else
|
|
212
|
+
# Entirely removed steps OLs require that the span also have the .steps class
|
|
213
|
+
# This is either a bug or weakness in the actual govuk CSS for that class
|
|
214
|
+
class_string = element["class"].nil? ? nil : " class= \"#{element['class']}\""
|
|
215
|
+
span_class_string = element["class"]&.include?("steps") ? "diff del steps" : "diff del"
|
|
216
|
+
|
|
217
|
+
<<~HTML
|
|
218
|
+
<#{element.name}#{class_string}>
|
|
219
|
+
<span class="#{span_class_string}">
|
|
220
|
+
<span class="visually-hidden">Removed content </span>
|
|
221
|
+
#{element.inner_html}
|
|
222
|
+
</span>
|
|
223
|
+
</#{element.name}>
|
|
224
|
+
HTML
|
|
225
|
+
end
|
|
154
226
|
end
|
|
155
227
|
end
|
|
156
228
|
end
|
|
@@ -6,5 +6,13 @@ module Nokodiff
|
|
|
6
6
|
n["class"] = "diff-marker"
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
def insert_table_row_change_marker(element, message)
|
|
11
|
+
marker = Nokogiri::XML::Node.new("span", element.document)
|
|
12
|
+
marker.content = "#{message} "
|
|
13
|
+
marker["class"] = "visually-hidden"
|
|
14
|
+
|
|
15
|
+
element.children.first.prepend_child(marker)
|
|
16
|
+
end
|
|
9
17
|
end
|
|
10
18
|
end
|
data/lib/nokodiff/version.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: 1.0.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GOV.UK Dev
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 13.
|
|
32
|
+
version: 13.4.2
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 13.
|
|
39
|
+
version: 13.4.2
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: rspec-html-matchers
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - '='
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 5.2.
|
|
74
|
+
version: 5.2.1
|
|
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.2.
|
|
81
|
+
version: 5.2.1
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: simplecov
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -102,7 +102,7 @@ dependencies:
|
|
|
102
102
|
version: '6'
|
|
103
103
|
- - "<"
|
|
104
104
|
- !ruby/object:Gem::Version
|
|
105
|
-
version: 8.1.
|
|
105
|
+
version: 8.1.4
|
|
106
106
|
type: :runtime
|
|
107
107
|
prerelease: false
|
|
108
108
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -112,7 +112,7 @@ dependencies:
|
|
|
112
112
|
version: '6'
|
|
113
113
|
- - "<"
|
|
114
114
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: 8.1.
|
|
115
|
+
version: 8.1.4
|
|
116
116
|
- !ruby/object:Gem::Dependency
|
|
117
117
|
name: byebug
|
|
118
118
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -150,7 +150,7 @@ dependencies:
|
|
|
150
150
|
version: '101.0'
|
|
151
151
|
- - "<"
|
|
152
152
|
- !ruby/object:Gem::Version
|
|
153
|
-
version: '103.
|
|
153
|
+
version: '103.4'
|
|
154
154
|
type: :runtime
|
|
155
155
|
prerelease: false
|
|
156
156
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -160,7 +160,7 @@ dependencies:
|
|
|
160
160
|
version: '101.0'
|
|
161
161
|
- - "<"
|
|
162
162
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: '103.
|
|
163
|
+
version: '103.4'
|
|
164
164
|
- !ruby/object:Gem::Dependency
|
|
165
165
|
name: govspeak
|
|
166
166
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -184,7 +184,7 @@ dependencies:
|
|
|
184
184
|
version: '6'
|
|
185
185
|
- - "<"
|
|
186
186
|
- !ruby/object:Gem::Version
|
|
187
|
-
version: 8.1.
|
|
187
|
+
version: 8.1.4
|
|
188
188
|
type: :runtime
|
|
189
189
|
prerelease: false
|
|
190
190
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -194,7 +194,7 @@ dependencies:
|
|
|
194
194
|
version: '6'
|
|
195
195
|
- - "<"
|
|
196
196
|
- !ruby/object:Gem::Version
|
|
197
|
-
version: 8.1.
|
|
197
|
+
version: 8.1.4
|
|
198
198
|
- !ruby/object:Gem::Dependency
|
|
199
199
|
name: view_component
|
|
200
200
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -244,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
244
244
|
- !ruby/object:Gem::Version
|
|
245
245
|
version: '0'
|
|
246
246
|
requirements: []
|
|
247
|
-
rubygems_version: 4.0.
|
|
247
|
+
rubygems_version: 4.0.16
|
|
248
248
|
specification_version: 4
|
|
249
249
|
summary: A Ruby Gem to highlight additions, deletions and character level changes
|
|
250
250
|
while preserving original HTML
|