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 +4 -4
- data/app/models/paper_view/attribute_change.rb +10 -1
- data/app/models/paper_view/leaf_change.rb +6 -0
- data/app/models/paper_view/text_diff.rb +152 -0
- data/app/views/paper_view/shared/_styles.html.erb +17 -0
- data/app/views/paper_view/versions/_diff.html.erb +20 -0
- data/app/views/paper_view/versions/_full_value.html.erb +15 -0
- data/app/views/paper_view/versions/_panel.html.erb +22 -22
- data/app/views/paper_view/versions/_tally.html.erb +3 -0
- data/app/views/paper_view/versions/_timeline_row.html.erb +8 -3
- data/lib/paper_view/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9edde8960fedca15affc4c074a0602b55ce7d7f6bc897ecfcb667270bff1eae
|
|
4
|
+
data.tar.gz: 1bfb0849b90c525c51affda3136a2c284a3fe1eb98ae4bb4b335964c64d7f8e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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>
|
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
data/lib/paper_view/version.rb
CHANGED
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.
|
|
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
|