paper_view 0.2.3 → 0.2.4

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: b5987bab41c6b08b669d05da7cb0331e056f1d05dfa1d9bceae4e1555615a020
4
- data.tar.gz: c37ad5a8c3499c47b191fc12219a1cf3cb515193b0c5ca44e4e28153a6916fa5
3
+ metadata.gz: d9edde8960fedca15affc4c074a0602b55ce7d7f6bc897ecfcb667270bff1eae
4
+ data.tar.gz: 1bfb0849b90c525c51affda3136a2c284a3fe1eb98ae4bb4b335964c64d7f8e8
5
5
  SHA512:
6
- metadata.gz: 3b00f0527ce236c1b3184a6004ea6811a40e8abc64dbcfe266554ec9c8a8c52ec0afc6ad55aa5217bcdf2b5874946d4be19ea060a863e41ea7ee869e66ec7d5b
7
- data.tar.gz: 72bbab79a05fc2e021f90c7b7baa65e1ca0cba077d0cf5dc009589f0c1cd1c5895b68e52922ef550211a63cec792158a08852853939043979b8f2dbc7ff09032
6
+ metadata.gz: 44513dfb49200317ba711a41d90395446cfbfe715b43c00d398aa98ae66b01d5682a5d458ee1bf88f2fcb6c2d09e0534407c292b6456fc22f351a1275dc0ddf9
7
+ data.tar.gz: 4b6285b3341e21e3685307ae903b35de96b37836faa0ec949a521ecc2cf5c7ef14a753eadec8f8f989b91f734ba641275e3a139e54f8dad525e26b5e06b2883a
@@ -79,6 +79,11 @@ module PaperView
79
79
  @new_text ||= text(new_value)
80
80
  end
81
81
 
82
+ # Long texts read as a diff instead of two blocks that have to be compared by eye.
83
+ def diff
84
+ leaf.diff unless nested?
85
+ end
86
+
82
87
  def leaf_changes
83
88
  leaf_pairs.filter_map { |path, before, after| LeafChange.new(path, before, after) if before != after }
84
89
  end
@@ -88,13 +93,17 @@ module PaperView
88
93
  end
89
94
 
90
95
  def leaves
91
- return [LeafChange.new(name, old_value, new_value)] unless nested?
96
+ return [leaf] unless nested?
92
97
 
93
98
  leaf_changes.map { |change| change.under(name) }
94
99
  end
95
100
 
96
101
  private
97
102
 
103
+ def leaf
104
+ @leaf ||= LeafChange.new(name, old_value, new_value)
105
+ end
106
+
98
107
  def text(value)
99
108
  self.class.display(self.class.structure(value) || value, pretty: true)
100
109
  end
@@ -18,6 +18,12 @@ module PaperView
18
18
  path.start_with?("[") ? "#{prefix}#{path}" : "#{prefix}.#{path}"
19
19
  end
20
20
 
21
+ def diff
22
+ return @diff if defined?(@diff)
23
+
24
+ @diff = TextDiff.for(old_value, new_value)
25
+ end
26
+
21
27
  def old_text
22
28
  AttributeChange.display(old_value)
23
29
  end
@@ -0,0 +1,152 @@
1
+ module PaperView
2
+ # A line based diff for values that are too long to read as two blocks of text.
3
+ class TextDiff
4
+ CONTEXT = 3
5
+ MIN_LINES = 4
6
+
7
+ # Past a couple of hundred changed lines nobody reads a diff anymore, and the search
8
+ # for the shortest one costs its square, so both sides become one replaced block.
9
+ MAX_CHANGES = 200
10
+
11
+ Line = Struct.new(:kind, :text, :old_number, :new_number) do
12
+ def context?
13
+ kind == :context
14
+ end
15
+ end
16
+
17
+ Hunk = Struct.new(:lines) do
18
+ def header
19
+ "@@ -#{old_start},#{lines.count(&:old_number)} +#{new_start},#{lines.count(&:new_number)} @@"
20
+ end
21
+
22
+ private
23
+
24
+ def old_start
25
+ lines.detect(&:old_number)&.old_number || 0
26
+ end
27
+
28
+ def new_start
29
+ lines.detect(&:new_number)&.new_number || 0
30
+ end
31
+ end
32
+
33
+ def self.for(old_text, new_text)
34
+ return unless old_text.is_a?(String) && new_text.is_a?(String)
35
+
36
+ old_lines = old_text.lines.map(&:chomp)
37
+ new_lines = new_text.lines.map(&:chomp)
38
+ return if old_lines == new_lines || [old_lines.size, new_lines.size].max < MIN_LINES
39
+
40
+ new(old_lines, new_lines)
41
+ end
42
+
43
+ def initialize(old_lines, new_lines)
44
+ @old_lines = old_lines
45
+ @new_lines = new_lines
46
+ end
47
+
48
+ def lines
49
+ @lines ||= (trace = shortest_edit) ? retrace(trace) : replaced
50
+ end
51
+
52
+ def hunks
53
+ @hunks ||= groups.map { |first, last| Hunk.new(lines[first..last]) }
54
+ end
55
+
56
+ def added_count
57
+ lines.count { |line| line.kind == :added }
58
+ end
59
+
60
+ def removed_count
61
+ lines.count { |line| line.kind == :deleted }
62
+ end
63
+
64
+ def unchanged_count
65
+ lines.count(&:context?)
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :old_lines, :new_lines
71
+
72
+ # Myers' diff, one difference at a time. Every path is tracked by its diagonal
73
+ # k = x - y, and ends[k] is how far into the old text the best path on that diagonal
74
+ # reaches. Each step moves one line sideways and then rides the diagonal of matching
75
+ # lines as far as it goes, so untouched stretches cost nothing.
76
+ def shortest_edit
77
+ ends = {1 => 0}
78
+ trace = []
79
+
80
+ (0..MAX_CHANGES).each do |differences|
81
+ trace << ends.dup
82
+
83
+ (-differences).step(differences, 2) do |k|
84
+ x = added?(ends, k, differences) ? ends[k + 1] : ends[k - 1] + 1
85
+ y = x - k
86
+
87
+ while x < old_lines.size && y < new_lines.size && old_lines[x] == new_lines[y]
88
+ x += 1
89
+ y += 1
90
+ end
91
+
92
+ ends[k] = x
93
+ return trace if x >= old_lines.size && y >= new_lines.size
94
+ end
95
+ end
96
+
97
+ nil
98
+ end
99
+
100
+ # A path reaches diagonal k either by moving down from k + 1, which consumes a new
101
+ # line, or by moving right from k - 1, which consumes an old one.
102
+ def added?(ends, k, differences)
103
+ k == -differences || (k != differences && ends[k - 1] < ends[k + 1])
104
+ end
105
+
106
+ # Retrace the winning path backwards: every diagonal stretch is context, every
107
+ # sideways step is the one line that was added or deleted there.
108
+ def retrace(trace)
109
+ x = old_lines.size
110
+ y = new_lines.size
111
+
112
+ trace.each_with_index.reverse_each.flat_map { |ends, differences|
113
+ k = x - y
114
+ previous_k = added?(ends, k, differences) ? k + 1 : k - 1
115
+ previous_x = ends[previous_k]
116
+ previous_y = previous_x - previous_k
117
+ steps = []
118
+
119
+ while x > previous_x && y > previous_y
120
+ x -= 1
121
+ y -= 1
122
+ steps << Line.new(:context, old_lines[x], x + 1, y + 1)
123
+ end
124
+
125
+ steps << step(previous_x, previous_y, added: x == previous_x) if differences > 0
126
+ x = previous_x
127
+ y = previous_y
128
+ steps
129
+ }.reverse
130
+ end
131
+
132
+ def step(old_index, new_index, added:)
133
+ if added
134
+ Line.new(:added, new_lines[new_index], nil, new_index + 1)
135
+ else
136
+ Line.new(:deleted, old_lines[old_index], old_index + 1, nil)
137
+ end
138
+ end
139
+
140
+ def replaced
141
+ old_lines.each_with_index.map { |text, index| Line.new(:deleted, text, index + 1, nil) } +
142
+ new_lines.each_with_index.map { |text, index| Line.new(:added, text, nil, index + 1) }
143
+ end
144
+
145
+ # Changed lines closer than two context blocks share their surroundings and stay in one hunk.
146
+ def groups
147
+ lines.each_index.reject { |index| lines[index].context? }
148
+ .slice_when { |before, after| after - before > CONTEXT * 2 }
149
+ .map { |run| [[run.first - CONTEXT, 0].max, [run.last + CONTEXT, lines.size - 1].min] }
150
+ end
151
+ end
152
+ end
@@ -324,6 +324,10 @@
324
324
  .pv-change { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font: 400 12px/18px var(--pv-mono); }
325
325
  .pv-change__path { color: var(--pv-ink); }
326
326
  .pv-change__arrow { color: var(--pv-ink-faint); }
327
+ .pv-tally { padding: 1px 6px; border-radius: 4px; }
328
+ .pv-tally--add { color: var(--pv-add); background: var(--pv-add-bg); }
329
+ .pv-tally--del { color: var(--pv-del); background: var(--pv-del-bg); }
330
+ .pv-tally__unit { color: var(--pv-ink-faint); }
327
331
  .pv-value {
328
332
  max-width: 280px;
329
333
  padding: 1px 6px;
@@ -383,6 +387,19 @@
383
387
  .pv-keys__values { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font: 400 12px/1.5 var(--pv-mono); }
384
388
  .pv-keys__values .pv-value { max-width: none; overflow: visible; white-space: normal; word-break: break-all; }
385
389
 
390
+ .pv-diff { display: flex; flex-direction: column; gap: 6px; }
391
+ .pv-diff__stats { display: flex; gap: 8px; font: 400 11px/1.4 var(--pv-mono); }
392
+ .pv-diff__hunk { border: 1px solid var(--pv-border); border-radius: 6px; overflow: hidden; }
393
+ .pv-diff__range { padding: 3px 8px; background: var(--pv-surface-muted); border-bottom: 1px solid var(--pv-border); font: 400 11px/1.6 var(--pv-mono); color: var(--pv-ink-faint); }
394
+ .pv-diff__line { display: grid; grid-template-columns: 34px 34px 10px minmax(0, 1fr); gap: 4px; font: 400 12px/1.5 var(--pv-mono); color: var(--pv-ink-secondary); }
395
+ .pv-diff__line--deleted { color: var(--pv-del); background: var(--pv-del-bg); }
396
+ .pv-diff__line--added { color: var(--pv-add); background: var(--pv-add-bg); }
397
+ .pv-diff__num { padding-left: 6px; text-align: right; color: var(--pv-ink-faint); user-select: none; }
398
+ .pv-diff__sign { text-align: center; user-select: none; }
399
+ .pv-diff__line--deleted .pv-diff__sign::before { content: "-"; }
400
+ .pv-diff__line--added .pv-diff__sign::before { content: "+"; }
401
+ .pv-diff__text { padding-right: 8px; white-space: pre-wrap; word-break: break-all; }
402
+
386
403
  .pv-disclosures { display: grid; grid-template-columns: 1fr 1fr; column-gap: 12px; }
387
404
  .pv-disclosure {
388
405
  padding: 0;
@@ -0,0 +1,20 @@
1
+ <div class="pv-diff">
2
+ <div class="pv-diff__stats">
3
+ <%= render "paper_view/versions/tally", diff: diff %>
4
+ </div>
5
+
6
+ <% diff.hunks.each do |hunk| %>
7
+ <div class="pv-diff__hunk">
8
+ <div class="pv-diff__range"><%= hunk.header %></div>
9
+
10
+ <% hunk.lines.each do |line| %>
11
+ <div class="pv-diff__line pv-diff__line--<%= line.kind %>">
12
+ <span class="pv-diff__num"><%= line.old_number %></span>
13
+ <span class="pv-diff__num"><%= line.new_number %></span>
14
+ <span class="pv-diff__sign"></span>
15
+ <span class="pv-diff__text"><%= line.text %></span>
16
+ </div>
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ </div>
@@ -0,0 +1,15 @@
1
+ <button
2
+ type="button"
3
+ class="pv-attribute__unchanged"
4
+ aria-expanded="false"
5
+ aria-controls="<%= id %>"
6
+ data-pv-toggle="<%= id %>"
7
+ >
8
+ <span class="pv-disclosure__arrow">▸</span>
9
+ <%= count %> unchanged <%= unit.pluralize(count) %>
10
+ </button>
11
+
12
+ <div class="pv-bars" id="<%= id %>" hidden>
13
+ <div class="pv-bar pv-bar--old"><%= change.old_text %></div>
14
+ <div class="pv-bar pv-bar--new"><%= change.new_text %></div>
15
+ </div>
@@ -92,32 +92,32 @@
92
92
  <div class="pv-keys">
93
93
  <span class="pv-keys__name"><%= leaf.path %></span>
94
94
 
95
- <div class="pv-keys__values">
96
- <span class="pv-value pv-value--old"><%= leaf.old_text %></span>
97
- <span class="pv-change__arrow">→</span>
98
- <span class="pv-value pv-value--new"><%= leaf.new_text %></span>
99
- </div>
95
+ <% if leaf.diff %>
96
+ <%= render "paper_view/versions/diff", diff: leaf.diff %>
97
+ <% else %>
98
+ <div class="pv-keys__values">
99
+ <span class="pv-value pv-value--old"><%= leaf.old_text %></span>
100
+ <span class="pv-change__arrow">→</span>
101
+ <span class="pv-value pv-value--new"><%= leaf.new_text %></span>
102
+ </div>
103
+ <% end %>
100
104
  </div>
101
105
  <% end %>
102
106
  <% if change.unchanged_count.positive? %>
103
- <% whole_id = "pv-whole-#{row.id}-#{index}" %>
104
- <button
105
- type="button"
106
- class="pv-attribute__unchanged"
107
- aria-expanded="false"
108
- aria-controls="<%= whole_id %>"
109
- data-pv-toggle="<%= whole_id %>"
110
- >
111
- <span class="pv-disclosure__arrow">▸</span>
112
- <%= change.unchanged_count %> unchanged
113
- <%= "value".pluralize(change.unchanged_count) %>
114
- </button>
115
-
116
- <div class="pv-bars" id="<%= whole_id %>" hidden>
117
- <div class="pv-bar pv-bar--old"><%= change.old_text %></div>
118
- <div class="pv-bar pv-bar--new"><%= change.new_text %></div>
119
- </div>
107
+ <%= render "paper_view/versions/full_value",
108
+ change: change,
109
+ id: "pv-whole-#{row.id}-#{index}",
110
+ count: change.unchanged_count,
111
+ unit: "value" %>
120
112
  <% end %>
113
+ <% elsif change.diff %>
114
+ <%= render "paper_view/versions/diff", diff: change.diff %>
115
+
116
+ <%= render "paper_view/versions/full_value",
117
+ change: change,
118
+ id: "pv-whole-#{row.id}-#{index}",
119
+ count: change.diff.unchanged_count,
120
+ unit: "line" %>
121
121
  <% else %>
122
122
  <div class="pv-bars">
123
123
  <div class="pv-bar pv-bar--old"><%= change.old_text %></div>
@@ -0,0 +1,3 @@
1
+ <span class="pv-tally pv-tally--add">+<%= diff.added_count %></span>
2
+ <span class="pv-tally pv-tally--del">−<%= diff.removed_count %></span>
3
+ <span class="pv-tally__unit"><%= "line".pluralize(diff.added_count + diff.removed_count) %></span>
@@ -35,9 +35,14 @@
35
35
  <% row.lines.each do |line| %>
36
36
  <div class="pv-change">
37
37
  <span class="pv-change__path"><%= line.path %></span>
38
- <span class="pv-value pv-value--old" title="<%= line.old_text %>"><%= truncate(line.old_text, length: 140) %></span>
39
- <span class="pv-change__arrow">→</span>
40
- <span class="pv-value pv-value--new" title="<%= line.new_text %>"><%= truncate(line.new_text, length: 140) %></span>
38
+
39
+ <% if line.diff %>
40
+ <%= render "paper_view/versions/tally", diff: line.diff %>
41
+ <% else %>
42
+ <span class="pv-value pv-value--old" title="<%= line.old_text %>"><%= truncate(line.old_text, length: 140) %></span>
43
+ <span class="pv-change__arrow">→</span>
44
+ <span class="pv-value pv-value--new" title="<%= line.new_text %>"><%= truncate(line.new_text, length: 140) %></span>
45
+ <% end %>
41
46
  </div>
42
47
  <% end %>
43
48
 
@@ -1,3 +1,3 @@
1
1
  module PaperView
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leon Vogt
@@ -45,6 +45,7 @@ files:
45
45
  - app/models/paper_view/payload.rb
46
46
  - app/models/paper_view/payload_sizes.rb
47
47
  - app/models/paper_view/table_stats.rb
48
+ - app/models/paper_view/text_diff.rb
48
49
  - app/models/paper_view/timeline.rb
49
50
  - app/models/paper_view/version_query.rb
50
51
  - app/models/paper_view/version_row.rb
@@ -56,9 +57,12 @@ files:
56
57
  - app/views/paper_view/shared/_theme.html.erb
57
58
  - app/views/paper_view/stats/_stats.html.erb
58
59
  - app/views/paper_view/stats/show.html.erb
60
+ - app/views/paper_view/versions/_diff.html.erb
59
61
  - app/views/paper_view/versions/_filters.html.erb
60
62
  - app/views/paper_view/versions/_footer.html.erb
63
+ - app/views/paper_view/versions/_full_value.html.erb
61
64
  - app/views/paper_view/versions/_panel.html.erb
65
+ - app/views/paper_view/versions/_tally.html.erb
62
66
  - app/views/paper_view/versions/_timeline_row.html.erb
63
67
  - app/views/paper_view/versions/index.html.erb
64
68
  - config/routes.rb