nokodiff 0.4.2 → 1.0.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: 8574d5f3cb682d4797c8d6cfa9c979e10ca2307a4c55997a0ddd6b3299801856
4
- data.tar.gz: c3e4b13e2632ba3bf99de7c7a3ef5583618f68e6e94c03f877e5059b764c36e2
3
+ metadata.gz: 9b38060ba01f5d6e38c752d5b3736072cfb7ddb867b3f9ab85292ef731946695
4
+ data.tar.gz: 333e94464a25ee00c03c00c54210ef1a5c07f52804d1e8299a84cd20338c5a22
5
5
  SHA512:
6
- metadata.gz: b5a8d417f70b4d7fbc1cc2c4745dbd4494d3bf401ad7038e45c239cf9c69e1bca9af24cc0d7e2a078a5d29727d91d8acf29959a5d8e6d94c2d5ba600709f4e05
7
- data.tar.gz: af1d8b59965819a131bf96d6dcfd9e64e2563196bc74be4bb1c75310d2cd1f4862b40c92691cae1845e4fdf5fe7030fe52ec128376fcfcd09b3dc48649d935b6
6
+ metadata.gz: e1fd5d24c4c18f1d61b3f12afe10a1358a15b5504c2dd6ce1d28baadc37d156826b2dc45c309722c933691f4ae2f1a4e019dbdc4396dfece72e590772fbf5084
7
+ data.tar.gz: f962ce7effdf2bb103fe28c07c936fd8694e6edbb4e72059d0c46671887513bfcaf2d6ee31109446014dc7841332e34ba255b263c3712280e5cb75eaf711d401
data/README.md CHANGED
@@ -66,7 +66,7 @@ In your application.scss file include:
66
66
  @import "nokodiff";
67
67
  ```
68
68
 
69
- This will include the styling for `<del>`, `<ins>` and `<span>` tags to allow colour coding, highlighting, and underlining of changes.
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.
70
70
 
71
71
  ### More complex diffing with `data-diff-key`
72
72
 
@@ -111,9 +111,7 @@ Output:
111
111
  <p>First paragraph</p>
112
112
  </div>
113
113
  <div data-diff-key="zm7q">
114
- <div class="diff">
115
- <ins aria-label="added content"><p>Second paragraph</p></ins>
116
- </div>
114
+ <p class="diff ins"><span class="visually-hidden">Added content </span>Second paragraph</p>
117
115
  </div>
118
116
  </div>
119
117
  ```
@@ -154,15 +152,18 @@ Output:
154
152
 
155
153
  ```html
156
154
  <div>
157
- <div data-diff-key="ixn4"><p>First paragraph</p></div>
158
- <div data-diff-key="zm7q">
159
- <div class="diff">
160
- <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>
161
157
  </div>
162
- <div class="diff">
163
- <ins aria-label="added content"><p><span class="diff-marker">N</span>e<span class="diff-marker">w</span> paragraph</p></ins>
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>
164
166
  </div>
165
- </div>
166
167
  </div>
167
168
  ```
168
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,490 @@ $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
+ @mixin diff-block {
13
+ position: relative;
14
+ background: $added-color;
15
+ padding-left: $comparison-sidebar-padding;
16
+ margin-left: -$comparison-sidebar-padding;
17
+ width: calc(100% + $comparison-sidebar-padding);
18
+
19
+ &:before {
20
+ content: " ";
21
+ position: absolute;
22
+ left: -$comparison-sidebar-width;
23
+ width: $comparison-sidebar-width;
24
+ height: 100%;
25
+ font-weight: normal;
26
+ min-height: 1.5em;
27
+ line-height: 1.5em;
28
+ text-align: center;
29
+ z-index: 10;
30
+ background: $diff-marker-added-color;
31
+ }
32
+ }
33
+
34
+ .visually-hidden {
35
+ position: absolute !important;
36
+ width: 1px !important;
37
+ height: 1px !important;
38
+ // If margin is set to a negative value it can cause text to be announced in
39
+ // the wrong order in VoiceOver for OSX
40
+ margin: 0 !important;
41
+ padding: 0 !important;
42
+ overflow: hidden !important;
43
+
44
+ clip: rect(0 0 0 0) !important;
45
+ clip-path: inset(50%) !important;
46
+
47
+ border: 0 !important;
48
+
49
+ // For long content, line feeds are not interpreted as spaces and small width
50
+ // causes content to wrap 1 word per line:
51
+ // https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
52
+ white-space: nowrap !important;
53
+
54
+ // Prevent users from selecting or copying visually-hidden text. This prevents
55
+ // a user unintentionally copying more text than they intended and needing to
56
+ // manually trim it down again.
57
+ user-select: none !important;
58
+ }
10
59
 
11
60
  .compare-editions {
12
61
  position: relative;
13
- border-left: 40px solid $light-grey;
14
- padding: 15px;
62
+ border-left: $comparison-sidebar-width solid $light-grey;
63
+ padding: $comparison-sidebar-padding;
64
+ clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
65
+
66
+ td::before {
67
+ // Backup to handle any inputted classes which make use of a ::before attached to a td element
68
+ // unsetting all styles for it will prevent it interfering with the ::before that we need to use
69
+ // for the diff marker
70
+ all: unset;
71
+ }
15
72
 
16
73
  .diff {
17
74
  position: static;
18
75
  margin-bottom: 10px;
19
76
 
20
- del, ins {
21
- position: static !important;
77
+ &.del, &.ins {
78
+ &:is(li){ display: list-item; }
79
+
80
+ position: relative;
22
81
  display: block;
23
82
  text-decoration: none;
24
- border-radius: 3px;
83
+ background-color: transparent;
25
84
  }
26
85
 
27
- del {
28
- background-color: $removed-color;
29
- padding-bottom: 2px;
86
+ &::after {
87
+ content: "";
88
+ position: absolute;
89
+ top: 0;
90
+ bottom: 0;
91
+ left: -$comparison-sidebar-padding;
92
+ min-height: 1.5em;
93
+ right: 0;
94
+ display: block;
95
+ z-index: -1;
96
+ }
97
+
98
+ &.del {
99
+ &::after { background-color: $removed-color; }
30
100
 
31
101
  span.diff-marker {
32
- font-weight: normal;
33
102
  background-color: $diff-marker-removed-color;
34
103
  border-bottom: 2px dashed $black;
35
104
  }
36
105
  }
37
106
 
38
- ins {
39
- background-color: $added-color;
40
- padding-bottom: 2px;
107
+ &.ins {
108
+ &::after { background-color: $added-color; }
41
109
 
42
110
  span.diff-marker {
43
- font-weight: normal;
44
111
  background-color: $diff-marker-added-color;
45
112
  border-bottom: 2px dashed $black;
46
113
  }
47
114
  }
48
115
 
49
- del::before,
50
- ins::before {
116
+ &::before {
117
+ // Some govuk styles use ::before in ways we need to override to make this work
118
+ border-radius: unset !important;
119
+ border: unset !important;
120
+
51
121
  position: absolute;
52
- left: -40px;
53
- width: 40px;
54
- height: 1.5em;
122
+ top: 0;
123
+ left: -$comparison-sidebar-offset;
124
+ width: $comparison-sidebar-width;
125
+ height: 100%;
126
+ min-height: 1.5em;
55
127
  line-height: 1.5em;
56
128
  text-align: center;
129
+ font-weight: normal;
57
130
  z-index: 10;
58
131
  }
132
+
133
+ &.del::before {
134
+ color: $text-colour;
135
+ background-color: $diff-marker-removed-color;
136
+ content: "−" !important;
137
+ }
138
+
139
+ &.ins::before {
140
+ color: $text-colour;
141
+ background-color: $diff-marker-added-color;
142
+ content: "+" !important;
143
+ }
59
144
  }
60
145
 
61
- del::before {
62
- color: $text-colour;
63
- background-color: $diff-marker-removed-color;
64
- content: "−";
146
+ // Govspeak-specific overrides
147
+ .gem-c-govspeak & {
148
+ &::before { min-height: 5em; }
65
149
  }
66
150
 
67
- ins::before {
68
- color: $text-colour;
69
- background-color: $diff-marker-added-color;
70
- content: "+";
151
+ .diff .gem-c-button.govuk-button{
152
+ margin-top: 22px;
71
153
  }
72
- }
154
+
155
+ .form-download .diff {
156
+ padding-top: 10px;
157
+ padding-bottom: 3px;
158
+ }
159
+
160
+ // Inset/callout elements overrides
161
+
162
+ .address, .contact {
163
+ .diff{
164
+ $address-offset: -16px;
165
+
166
+ &::after {
167
+ left: -$comparison-sidebar-padding + $address-offset;
168
+ }
169
+
170
+ &::before {
171
+ left: -$comparison-sidebar-offset + $address-offset;
172
+ }
173
+ }
174
+ }
175
+
176
+ .help-notice {
177
+ $help-notice-offset: -45px;
178
+
179
+ &:before {
180
+ display: none;
181
+ }
182
+
183
+ p {
184
+ position: relative;
185
+
186
+ &:before {
187
+ content: "!";
188
+ font-weight: 700;
189
+ border-radius: 50%;
190
+ color: var(--govuk-body-background-colour, #ffffff);
191
+ background: var(--govuk-text-colour, #0b0c0c);
192
+ font-size: 30px;
193
+ line-height: 29px;
194
+ text-align: center;
195
+ position: absolute;
196
+ margin-top: -5px;
197
+ left: $help-notice-offset;
198
+ min-width: 35px;
199
+ min-height: 35px;
200
+ }
201
+
202
+ &:first-child:after {
203
+ content: ".";
204
+ display: block;
205
+ clear: both;
206
+ visibility: hidden;
207
+ }
208
+ }
209
+
210
+ .diff {
211
+ &::after, &::before {
212
+ top: -15px;
213
+ min-height: calc(1.5em + 30px);
214
+ left: -$comparison-sidebar-padding + $help-notice-offset;
215
+ }
216
+
217
+ &::before {
218
+ left: -$comparison-sidebar-offset + $help-notice-offset;
219
+ }
220
+ }
221
+ }
222
+
223
+ .example, .info-notice {
224
+ $callout-parent-offset: -10px;
225
+ $callout-child-offset: -30px;
226
+
227
+ &.diff {
228
+ &::after {
229
+ left: -$comparison-sidebar-padding + $callout-parent-offset;
230
+ }
231
+
232
+ &::before {
233
+ left: -$comparison-sidebar-offset + $callout-parent-offset;
234
+ }
235
+ }
236
+
237
+ .diff {
238
+ &::after {
239
+ left: -$comparison-sidebar-padding + $callout-child-offset;
240
+ }
241
+
242
+ &::before {
243
+ left: -$comparison-sidebar-offset + $callout-child-offset;
244
+ }
245
+ }
246
+ }
247
+
248
+ // List-specific overrides for non-govuk lists
249
+ $non-govuk-list-offset: -40px;
250
+
251
+ li span.diff, ul span.diff{
252
+ &::after {
253
+ left: -$comparison-sidebar-padding + $non-govuk-list-offset;
254
+ }
255
+
256
+ &::before {
257
+ left: -$comparison-sidebar-offset + $non-govuk-list-offset;
258
+ }
259
+ }
260
+
261
+ // List-specific overrides for govuk lists as we apply a non-standard offset to lists
262
+ $govuk-list-offset: -20px;
263
+
264
+ .gem-c-govspeak & {
265
+ ul.list-no-bullets {
266
+ list-style: none;
267
+ padding-left: 0;
268
+
269
+ span.diff:before {
270
+ left: -$comparison-sidebar-width - $comparison-sidebar-padding;
271
+ }
272
+ }
273
+
274
+ ul li span.diff, ul span.diff {
275
+ &::after {
276
+ left: -$comparison-sidebar-padding + $govuk-list-offset;
277
+ }
278
+
279
+ &::before {
280
+ left: -$comparison-sidebar-offset + $govuk-list-offset;
281
+ }
282
+ }
283
+
284
+ ol li span.diff {
285
+ list-style-type: decimal;
286
+
287
+ &::after {
288
+ left: $comparison-sidebar-padding;
289
+ }
290
+
291
+ &::before {
292
+ left: $comparison-sidebar-offset;
293
+ }
294
+ }
295
+
296
+ ol:not(.steps) span.diff {
297
+ &::before {
298
+ left: -75px;
299
+ }
300
+
301
+ &::after {
302
+ left: -75px;
303
+ }
304
+ }
305
+
306
+ // Deal with the number values of ordered lists (including steps)
307
+ // by changing the increment value depending on sequence of diffs
308
+ ol {
309
+ counter-reset: item 0;
310
+ list-style: none;
311
+ margin-left: 20px;
312
+
313
+ li {
314
+ position: relative;
315
+ }
316
+
317
+ li:before {
318
+ position: absolute;
319
+ left: -20px;
320
+ counter-increment: item 1;
321
+ content: counter(item);
322
+ }
323
+
324
+ // Over-ride style for <li> elements inside a span.diff
325
+ // Ideally the markup would be changed so this is not needed
326
+ &.steps > span.diff > li:before {
327
+ left: 0;
328
+ }
329
+
330
+ li:has(.diff) + li:not(:has(.diff)),
331
+ li:not(:has(.diff)) + li:has(.diff),
332
+ li:not(:has(.diff)) + li:not(:has(.diff)) {
333
+ &:before {
334
+ counter-increment: item 1;
335
+ content: counter(item);
336
+ };
337
+ }
338
+
339
+ li:has(.del) + li:has(.ins) {
340
+ &:before {
341
+ counter-increment: item 0;
342
+ content: counter(item);
343
+ }
344
+ }
345
+
346
+ // This is required to over-ride the visually-hidden element
347
+ // that is otherwise assigned the first counter
348
+ // when a new set of steps are added
349
+ & > span.steps > span + li:before {
350
+ content: "1";
351
+ }
352
+ }
353
+
354
+ //govuk "steps"-style ol overrides
355
+ $steps-list-offset: -48px;
356
+
357
+ ol.steps {
358
+ @extend .steps;
359
+
360
+ &:has(>span.diff) {
361
+ @include diff-block;
362
+ }
363
+
364
+ margin-left: -$comparison-sidebar-padding;
365
+ width: calc(100% + $comparison-sidebar-padding);
366
+
367
+ li {
368
+ position: relative;
369
+ margin-top: 0.75em;
370
+ margin-bottom: 0.75em;
371
+ padding-top: 0;
372
+ padding-bottom: 0;
373
+
374
+ & > p {
375
+ margin-left: $comparison-sidebar-padding;
376
+ }
377
+
378
+ &:has(.del) {
379
+ background: $removed-color;
380
+ }
381
+
382
+ &:has(.ins) {
383
+ background: $added-color;
384
+ }
385
+
386
+ &::before {
387
+ top: 0;
388
+ margin-left: $comparison-sidebar-padding;
389
+ }
390
+
391
+ & > span {
392
+ margin-left: $comparison-sidebar-padding;
393
+
394
+ p {
395
+ margin-bottom: 0;
396
+ }
397
+ }
398
+ }
399
+
400
+ li span.diff {
401
+ margin-bottom: 0;
402
+
403
+ &::before {
404
+ left: -$comparison-sidebar-offset + $steps-list-offset;
405
+ height: 100%;
406
+ width: $comparison-sidebar-width;
407
+ min-height: auto; // 1.6em;
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ // Table-specific overrides
414
+ $table-row-offset: -2px;
415
+
416
+ table {
417
+ overflow-x: visible;
418
+ }
419
+
420
+ // This element is generated before new tables
421
+ // We need to remove it from the display and
422
+ // style the table that follows it as a new element
423
+ & > span.diff.ins {
424
+ display: none;
425
+
426
+ & + table {
427
+ @include diff-block;
428
+
429
+ &:before {
430
+ content: "+";
431
+ }
432
+ }
433
+ }
434
+
435
+ & > span.diff.del {
436
+ display: none;
437
+
438
+ & + table {
439
+ @include diff-block;
440
+
441
+ &:before {
442
+ content: "−";
443
+ }
444
+ }
445
+ }
446
+
447
+ tr.diff {
448
+ position: static;
449
+ transform: scale(1);
450
+
451
+ &.del, &.ins {
452
+ display: table-row;
453
+ }
454
+
455
+ &.del::after, &.ins::after {
456
+ left: -$comparison-sidebar-padding + $table-row-offset;
457
+ // Need to account for border in row element
458
+ height: 100%;
459
+ top: 1px;
460
+ }
461
+
462
+ // Attaching a ::before psuedoelement to a <tr> creates a phantom column so we need to hide it
463
+ // and instead attach one to the first-child <td>
464
+ &::before {
465
+ display: none;
466
+ }
467
+
468
+ td:first-child::before, th:first-child::before {
469
+ position: absolute;
470
+ left: -$comparison-sidebar-offset;
471
+ width: $comparison-sidebar-width;
472
+ height: 100%;
473
+ min-height: 1.5em;
474
+ top: 1px;
475
+ line-height: 1.5em;
476
+ text-align: center;
477
+ z-index: 10;
478
+ }
479
+
480
+ &.del td:first-child::before, &.del th:first-child::before {
481
+ color: $text-colour;
482
+ background-color: $diff-marker-removed-color;
483
+ content: "−";
484
+ font-weight: normal;
485
+ }
486
+
487
+ &.ins td:first-child::before, &.ins th:first-child::before {
488
+ color: $text-colour;
489
+ background-color: $diff-marker-added-color;
490
+ content: "+";
491
+ font-weight: normal;
492
+ }
493
+ }
494
+ }
@@ -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(highlight_changes(change.old_element, before_fragment))
47
- after_fragment.add_child(highlight_changes(change.new_element, after_fragment))
46
+ before_fragment.add_child(wrap_character_as_change(change.old_element, before_fragment))
47
+ after_fragment.add_child(wrap_character_as_change(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(highlight_changes(change.old_element, before_fragment))
52
+ before_fragment.add_child(wrap_character_as_change(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(highlight_changes(change.new_element, after_fragment))
57
+ after_fragment.add_child(wrap_character_as_change(change.new_element, after_fragment))
58
58
  end
59
59
 
60
60
  def append_accumulated_text(fragment, accumulated_text)
@@ -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
- diff[:before].name == "li" ? deleted_li(diff[:before]) : deleted_block(diff[:before])
20
+ diff[:before].text? ? build_deleted_text_html(diff[:before]) : build_deleted_element_html(diff[:before])
17
21
  when :added
18
- diff[:after].name == "li" ? added_li(diff[:after]) : added_block(diff[:after])
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,21 +64,15 @@ module Nokodiff
60
64
  end
61
65
 
62
66
  def changed_block(before_node, after_node)
63
- if structurally_similar?(before_node, after_node) && should_not_be_treated_as_single_change?(before_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)
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)
66
73
  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
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)
78
76
  end
79
77
  end
80
78
 
@@ -88,13 +86,11 @@ module Nokodiff
88
86
  before_node.name == after_node.name
89
87
  end
90
88
 
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"
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)
98
94
  end
99
95
 
100
96
  def rebuild_element(template_node, inner_html)
@@ -118,19 +114,15 @@ module Nokodiff
118
114
  end
119
115
 
120
116
  def diff_sub_elements(before_html, after_html)
121
- before_dup = before_html.dup
122
- after_dup = after_html.dup
117
+ before_fragment = before_html.dup
118
+ after_fragment = after_html.dup
123
119
 
124
- before_fragment, after_fragment = Nokodiff::TextNodeDiffs.new(before_dup, after_dup).call
120
+ Nokodiff::TextNodeDiffs.new(before_fragment, after_fragment).call
125
121
 
126
122
  merge_adjacent_highlighted_changes(before_fragment)
127
123
  merge_adjacent_highlighted_changes(after_fragment)
128
124
 
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
125
+ [before_fragment, after_fragment]
134
126
  end
135
127
 
136
128
  def merge_adjacent_highlighted_changes(node)
@@ -143,7 +135,7 @@ module Nokodiff
143
135
  node.children.each_cons(2) do |left, right|
144
136
  next unless node_is_a_change?(left) && node_is_a_change?(right)
145
137
 
146
- left.content = left.content + right.content
138
+ left.children += right.children
147
139
  right.remove
148
140
 
149
141
  merge_adjacent_highlighted_changes(node)
@@ -159,40 +151,78 @@ module Nokodiff
159
151
  node.to_html
160
152
  end
161
153
 
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
- )
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
198
+ end
199
+
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
196
226
  end
197
227
  end
198
228
  end
@@ -1,10 +1,28 @@
1
1
  module Nokodiff
2
2
  module FormattingHelpers
3
- def highlight_changes(char, fragment)
3
+ def wrap_character_as_change(char, fragment)
4
4
  Nokogiri::XML::Node.new("span", fragment.document).tap do |n|
5
5
  n.content = char
6
6
  n["class"] = "diff-marker"
7
7
  end
8
8
  end
9
+
10
+ def wrap_node_as_change(node)
11
+ return unless node
12
+
13
+ wrapper = Nokogiri::XML::Node.new("span", node.document)
14
+ wrapper["class"] = "diff-marker"
15
+
16
+ node.replace(wrapper)
17
+ wrapper.add_child(node)
18
+ end
19
+
20
+ def insert_table_row_change_marker(element, message)
21
+ marker = Nokogiri::XML::Node.new("span", element.document)
22
+ marker.content = "#{message} "
23
+ marker["class"] = "visually-hidden"
24
+
25
+ element.children.first.prepend_child(marker)
26
+ end
9
27
  end
10
28
  end
@@ -16,8 +16,10 @@ module Nokodiff
16
16
  attr_accessor :before_fragment, :after_fragment
17
17
 
18
18
  def diff_text_nodes(before_node, after_node)
19
- if before_node&.text? || after_node&.text?
19
+ if !contains_another_element?(before_node) && !contains_another_element?(after_node)
20
20
  diff_text_node_content(before_node, after_node)
21
+ elsif mixed_text_and_element_nodes(before_node, after_node)
22
+ mark_nodes_as_changed(before_node, after_node)
21
23
  elsif before_node&.element? || after_node&.element?
22
24
  before_children = before_node ? before_node.children.to_a : []
23
25
  after_children = after_node ? after_node.children.to_a : []
@@ -30,6 +32,19 @@ module Nokodiff
30
32
  end
31
33
  end
32
34
 
35
+ def contains_another_element?(node)
36
+ node && !node.text?
37
+ end
38
+
39
+ def mixed_text_and_element_nodes(before_node, after_node)
40
+ (before_node&.text? && after_node&.element?) || (before_node&.element? && after_node&.text?)
41
+ end
42
+
43
+ def mark_nodes_as_changed(before_node, after_node)
44
+ wrap_node_as_change(before_node)
45
+ wrap_node_as_change(after_node)
46
+ end
47
+
33
48
  def diff_text_node_content(before_text_node, after_text_node)
34
49
  before_chars = get_chars(before_text_node)
35
50
  after_chars = get_chars(after_text_node)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nokodiff
4
- VERSION = "0.4.2"
4
+ VERSION = "1.0.0"
5
5
  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.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -23,20 +23,34 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: byebug
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: rake
28
42
  requirement: !ruby/object:Gem::Requirement
29
43
  requirements:
30
44
  - - '='
31
45
  - !ruby/object:Gem::Version
32
- version: 13.3.1
46
+ version: 13.4.2
33
47
  type: :development
34
48
  prerelease: false
35
49
  version_requirements: !ruby/object:Gem::Requirement
36
50
  requirements:
37
51
  - - '='
38
52
  - !ruby/object:Gem::Version
39
- version: 13.3.1
53
+ version: 13.4.2
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rspec-html-matchers
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -71,14 +85,14 @@ dependencies:
71
85
  requirements:
72
86
  - - '='
73
87
  - !ruby/object:Gem::Version
74
- version: 5.2.0
88
+ version: 5.2.1
75
89
  type: :development
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - '='
80
94
  - !ruby/object:Gem::Version
81
- version: 5.2.0
95
+ version: 5.2.1
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: simplecov
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -93,40 +107,6 @@ dependencies:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
109
  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
110
  - !ruby/object:Gem::Dependency
131
111
  name: diff-lcs
132
112
  requirement: !ruby/object:Gem::Requirement
@@ -141,74 +121,6 @@ dependencies:
141
121
  - - ">="
142
122
  - !ruby/object:Gem::Version
143
123
  version: '0'
144
- - !ruby/object:Gem::Dependency
145
- name: gds-api-adapters
146
- requirement: !ruby/object:Gem::Requirement
147
- requirements:
148
- - - ">="
149
- - !ruby/object:Gem::Version
150
- version: '101.0'
151
- - - "<"
152
- - !ruby/object:Gem::Version
153
- version: '103.3'
154
- type: :runtime
155
- prerelease: false
156
- version_requirements: !ruby/object:Gem::Requirement
157
- requirements:
158
- - - ">="
159
- - !ruby/object:Gem::Version
160
- version: '101.0'
161
- - - "<"
162
- - !ruby/object:Gem::Version
163
- version: '103.3'
164
- - !ruby/object:Gem::Dependency
165
- name: govspeak
166
- requirement: !ruby/object:Gem::Requirement
167
- requirements:
168
- - - ">="
169
- - !ruby/object:Gem::Version
170
- version: 10.6.3
171
- type: :runtime
172
- prerelease: false
173
- version_requirements: !ruby/object:Gem::Requirement
174
- requirements:
175
- - - ">="
176
- - !ruby/object:Gem::Version
177
- version: 10.6.3
178
- - !ruby/object:Gem::Dependency
179
- name: rails
180
- requirement: !ruby/object:Gem::Requirement
181
- requirements:
182
- - - ">="
183
- - !ruby/object:Gem::Version
184
- version: '6'
185
- - - "<"
186
- - !ruby/object:Gem::Version
187
- version: 8.1.4
188
- type: :runtime
189
- prerelease: false
190
- version_requirements: !ruby/object:Gem::Requirement
191
- requirements:
192
- - - ">="
193
- - !ruby/object:Gem::Version
194
- version: '6'
195
- - - "<"
196
- - !ruby/object:Gem::Version
197
- version: 8.1.4
198
- - !ruby/object:Gem::Dependency
199
- name: view_component
200
- requirement: !ruby/object:Gem::Requirement
201
- requirements:
202
- - - "~>"
203
- - !ruby/object:Gem::Version
204
- version: '4'
205
- type: :runtime
206
- prerelease: false
207
- version_requirements: !ruby/object:Gem::Requirement
208
- requirements:
209
- - - "~>"
210
- - !ruby/object:Gem::Version
211
- version: '4'
212
124
  email:
213
125
  - govuk-dev@digital.cabinet-office.gov.uk
214
126
  executables: []
@@ -237,14 +149,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
237
149
  requirements:
238
150
  - - ">="
239
151
  - !ruby/object:Gem::Version
240
- version: '3.2'
152
+ version: '3.3'
241
153
  required_rubygems_version: !ruby/object:Gem::Requirement
242
154
  requirements:
243
155
  - - ">="
244
156
  - !ruby/object:Gem::Version
245
157
  version: '0'
246
158
  requirements: []
247
- rubygems_version: 4.0.10
159
+ rubygems_version: 4.0.21
248
160
  specification_version: 4
249
161
  summary: A Ruby Gem to highlight additions, deletions and character level changes
250
162
  while preserving original HTML