Simplify Snapshot and its presenter

openid
Marcin Kulik 11 years ago
parent 34b39d0db3
commit 31a880b7aa

@ -35,9 +35,9 @@ class AsciicastDecorator < ApplicationDecorator
end
def thumbnail(width = THUMBNAIL_WIDTH, height = THUMBNAIL_HEIGHT)
snapshot = Snapshot.build(model.snapshot || [])
thumbnail = snapshot.rstrip.crop(width, height).expand(height)
SnapshotPresenter.new(thumbnail).to_html
snapshot = Snapshot.new(model.snapshot || [])
thumbnail = SnapshotDecorator.new(snapshot.thumbnail(width, height))
h.render 'asciicasts/thumbnail', :thumbnail => thumbnail
end
def description

@ -0,0 +1,15 @@
class SnapshotDecorator < ApplicationDecorator
delegate_all
def lines
(0...height).map { |line_no| line(line_no) }
end
private
def line(line_no)
(0...width).map { |column_no| model.cell(column_no, line_no) }
end
end

@ -0,0 +1,22 @@
class Cell
attr_reader :text, :brush
def initialize(text, brush)
@text = text
@brush = brush
end
def empty?
text.blank? && brush.default?
end
def ==(other)
text == other.text && brush == other.brush
end
def css_class
BrushPresenter.new(brush).to_css_class
end
end

@ -1,55 +1,65 @@
class Snapshot
include Enumerable
delegate :each, :to => :lines
attr_reader :width, :height
def self.build(lines)
lines = lines.map { |fragments| SnapshotLine.build(fragments) }
new(lines)
end
def initialize(lines = [])
@lines = lines
def initialize(data, raw = true)
@lines = raw ? cellify(data) : data
@width = lines.first && lines.first.size || 0
@height = lines.size
end
def ==(other)
other.lines == lines
def cell(column, line)
lines[line][column]
end
def crop(width, height)
min_height = [height, lines.size].min
new_lines = lines.drop(lines.size - min_height).map { |line|
line.crop(width)
}
def thumbnail(width, height)
new_lines = strip_trailing_blank_lines(lines)
new_lines = crop_at_bottom_left(new_lines, width, height)
new_lines = expand(new_lines, width, height)
self.class.new(new_lines)
self.class.new(new_lines, false)
end
def rstrip
protected
def strip_trailing_blank_lines(lines)
i = lines.size - 1
while i >= 0 && lines[i].empty?
while i >= 0 && empty_line?(lines[i])
i -= 1
end
new_lines = i > -1 ? lines[0..i] : []
self.class.new(new_lines)
i > -1 ? lines[0..i] : []
end
def expand(height)
new_lines = lines
def crop_at_bottom_left(lines, width, height)
min_height = [height, lines.size].min
while new_lines.size < height
new_lines << []
lines.drop(lines.size - min_height).map { |line| line.take(width) }
end
def expand(lines, width, height)
while lines.size < height
lines << [Cell.new(' ', Brush.new)] * width
end
self.class.new(new_lines)
lines
end
protected
private
attr_reader :lines
def cellify(lines)
lines.map { |cells|
cells.map { |cell|
Cell.new(cell[0], Brush.new(cell[1]))
}
}
end
def empty_line?(cells)
cells.all?(&:empty?)
end
end

@ -1,28 +0,0 @@
class SnapshotFragment # TODO: rename to Cell or SnapshotCell
attr_reader :text, :brush
delegate :size, :to => :text
def initialize(text, brush)
@text = text
@brush = brush
end
def ==(other)
other.text == text && other.brush == brush
end
def crop(size)
if size >= text.size
self
else
self.class.new(text[0...size], brush)
end
end
def empty?
text.blank? && brush.default?
end
end

@ -1,48 +0,0 @@
class SnapshotLine
include Enumerable
delegate :each, :to => :fragments
def self.build(blocks)
fragments = blocks.map { |block|
SnapshotFragment.new(block[0], Brush.new(block[1]))
}
new(fragments)
end
def initialize(fragments)
@fragments = fragments
end
def ==(other)
other.fragments == fragments
end
def crop(size)
new_fragments = []
current_size = 0
fragments.each do |fragment|
break if current_size == size
if current_size + fragment.size > size
fragment = fragment.crop(size - current_size)
end
new_fragments << fragment
current_size += fragment.size
end
self.class.new(new_fragments)
end
def empty?
fragments.all?(&:empty?)
end
protected
attr_reader :fragments
end

@ -1,15 +0,0 @@
class SnapshotFragmentPresenter < Draper::Decorator
delegate :text, :brush
def to_html
h.content_tag(:span, text, :class => css_class)
end
private
def css_class
BrushPresenter.new(brush).to_css_class
end
end

@ -1,19 +0,0 @@
class SnapshotLinePresenter < Draper::Decorator
delegate :map
def to_html
h.content_tag(:span, fragment_strings.html_safe, :class => 'line')
end
private
def fragment_strings
map { |fragment| fragment_string(fragment) }.join
end
def fragment_string(fragment)
SnapshotFragmentPresenter.new(fragment).to_html
end
end

@ -1,19 +0,0 @@
class SnapshotPresenter < Draper::Decorator
delegate :map
def to_html
h.content_tag(:pre, lines_html.html_safe, :class => 'terminal')
end
private
def lines_html
map { |line| line_html(line) }.join("\n") + "\n"
end
def line_html(line)
SnapshotLinePresenter.new(line).to_html
end
end

@ -0,0 +1,6 @@
pre.terminal
- for line in thumbnail.lines
span.line
- for cell in line
span class=cell.css_class = cell.text
= "\n"

@ -1,11 +1,12 @@
require 'spec_helper'
describe AsciicastDecorator do
include Draper::ViewHelpers
let(:asciicast) { Asciicast.new }
let(:decorated) { AsciicastDecorator.new(asciicast) }
let(:decorator) { described_class.new(asciicast) }
subject { decorated.send(method) }
subject { decorator.send(method) }
describe '#os' do
let(:method) { :os }
@ -136,38 +137,18 @@ describe AsciicastDecorator do
describe '#thumbnail' do
let(:json) { [:qux] }
let(:snapshot) { double('snapshot') }
let(:presenter) { double('presenter', :to_html => '<pre></pre>') }
let(:snapshot) { double('snapshot', :thumbnail => thumbnail) }
let(:thumbnail) { double('thumbnail') }
before do
allow(asciicast).to receive(:snapshot) { json }
allow(Snapshot).to receive(:build).with(json) { snapshot }
allow(snapshot).to receive(:rstrip) { snapshot }
allow(snapshot).to receive(:crop) { snapshot }
allow(snapshot).to receive(:expand) { snapshot }
allow(SnapshotPresenter).to receive(:new).with(snapshot) { presenter }
allow(Snapshot).to receive(:new).with(json) { snapshot }
allow(helpers).to receive(:render).
with('asciicasts/thumbnail', :thumbnail => thumbnail) { '<pre></pre>' }
end
it 'removes empty trailing lines from the snapshot' do
decorated.thumbnail(21, 13)
expect(snapshot).to have_received(:rstrip)
end
it 'crops the snapshot' do
decorated.thumbnail(21, 13)
expect(snapshot).to have_received(:crop).with(21, 13)
end
it 'adds the missing lines to the end of the snapshot' do
decorated.thumbnail(21, 13)
expect(snapshot).to have_received(:expand).with(13)
end
it 'returns html snapshot rendered by SnapshotPresenter#to_html' do
expect(decorated.thumbnail).to eq('<pre></pre>')
it "returns snapshot's thumbnail rendered by SnapshotPresenter" do
expect(decorator.thumbnail).to eq('<pre></pre>')
end
end
@ -183,7 +164,7 @@ describe AsciicastDecorator do
end
it 'returns nickname from decorated user' do
decorated.should_receive(:user).twice.and_return(user)
decorator.should_receive(:user).twice.and_return(user)
subject.should == nickname
end
end
@ -219,7 +200,7 @@ describe AsciicastDecorator do
end
it 'returns link from decorated user' do
decorated.should_receive(:user).twice.and_return(user)
decorator.should_receive(:user).twice.and_return(user)
subject.should == link
end
end
@ -232,7 +213,7 @@ describe AsciicastDecorator do
end
it 'returns author from decorated user' do
decorated.should_receive(:author).and_return(author)
decorator.should_receive(:author).and_return(author)
subject.should == author
end
end
@ -250,22 +231,20 @@ describe AsciicastDecorator do
end
it 'returns img_link from decorated user' do
decorated.should_receive(:user).twice.and_return(user)
decorator.should_receive(:user).twice.and_return(user)
subject.should == img_link
end
end
context 'when no user present' do
let(:avatar_image) { double('avatar_image') }
let(:h) { double('h') }
before do
asciicast.user = nil
allow(helpers).to receive(:avatar_image_tag).with(nil) { avatar_image }
end
it 'returns avatar_image_tag' do
decorated.stub(:h => h)
h.should_receive(:avatar_image_tag).with(nil).and_return(avatar_image)
subject.should == avatar_image
end
end
@ -291,12 +270,12 @@ describe AsciicastDecorator do
end
it 'should be an async script tag including asciicast id' do
expect(decorated.embed_script).to match(script_regexp)
expect(decorator.embed_script).to match(script_regexp)
end
end
describe '#duration' do
subject { decorated.duration }
subject { decorator.duration }
context "when it's below 1 minute" do
before do

@ -0,0 +1,22 @@
require 'spec_helper'
describe SnapshotDecorator do
let(:decorator) { described_class.new(snapshot) }
let(:snapshot) { double('snapshot', :width => 2, :height => 2) }
let(:cells) { [
[:a, :b],
[:c, :d]
] }
describe '#lines' do
subject { decorator.lines }
before do
allow(snapshot).to receive(:cell) { |x, y| cells[y][x] }
end
it { should eq(cells) }
end
end

@ -0,0 +1,74 @@
require 'spec_helper'
describe Cell do
let(:cell) { described_class.new('a', brush) }
let(:brush) { double('brush') }
describe '#empty?' do
let(:cell) { described_class.new(text, brush) }
subject { cell.empty? }
context "when text is not blank" do
let(:text) { 'a' }
let(:brush) { double('brush', :default? => true) }
it { should be(false) }
end
context "when brush is not default" do
let(:text) { ' ' }
let(:brush) { double('brush', :default? => false) }
it { should be(false) }
end
context "when text is blank and brush is default" do
let(:text) { ' ' }
let(:brush) { double('brush', :default? => true) }
it { should be(true) }
end
end
describe '#==' do
let(:other) { described_class.new(text, other_brush) }
subject { cell == other }
context "when text differs" do
let(:text) { 'b' }
let(:other_brush) { double('brush', :== => true) }
it { should be(false) }
end
context "when brush differs" do
let(:text) { 'a' }
let(:other_brush) { double('brush', :== => false) }
it { should be(false) }
end
context "when text and brush are equal" do
let(:text) { 'a' }
let(:other_brush) { double('brush', :== => true) }
it { should be(true) }
end
end
describe '#css_class' do
let(:brush_presenter) { double('brush_presenter', :to_css_class => 'kls') }
subject { cell.css_class }
before do
allow(BrushPresenter).to receive(:new).with(brush) { brush_presenter }
end
it { should eq('kls') }
end
end

@ -1,67 +0,0 @@
require 'spec_helper'
describe SnapshotFragment do
describe '#==' do
let(:snapshot_fragment) { SnapshotFragment.new('foo', brush_1) }
let(:brush_1) { double('brush_1') }
let(:brush_2) { double('brush_2') }
subject { snapshot_fragment == other }
context "when fragments have the same texts and brushes" do
let(:other) { SnapshotFragment.new('foo', brush_1) }
it { should be(true) }
end
context "when fragments have different texts" do
let(:other) { SnapshotFragment.new('bar', brush_1) }
it { should be(false) }
end
context "when fragments have different brushes" do
let(:other) { SnapshotFragment.new('foo', brush_2) }
it { should be(false) }
end
end
describe '#crop' do
let(:snapshot_fragment) { SnapshotFragment.new('foobar', brush) }
let(:brush) { double('brush') }
context "when size is smaller than fragment's size" do
subject { snapshot_fragment.crop(3) }
it 'returns a new instance of SnapshotFragment' do
expect(subject).to be_kind_of(SnapshotFragment)
expect(subject).to_not be(snapshot_fragment)
end
it 'trims the text to the requested size' do
expect(subject.text).to eq('foo')
end
it 'returns SnapshotFragment with the same brush' do
expect(subject.brush).to be(brush)
end
end
context "when size is equal or larger than the fragment's size" do
it 'returns self' do
expect(snapshot_fragment.crop(6)).to be(snapshot_fragment)
end
end
end
describe '#size' do
let(:snapshot_fragment) { SnapshotFragment.new('f' * 100, Brush.new) }
subject { snapshot_fragment.size }
it { should eq(100) }
end
end

@ -1,97 +0,0 @@
require 'spec_helper'
describe SnapshotLine do
describe '.build' do
let(:line) { SnapshotLine.build(input) }
let(:input) { [input_fragment_1, input_fragment_2] }
let(:input_fragment_1) { ['foo', { :fg => 1, :bold => true }] }
let(:input_fragment_2) { ['bar', { :bg => 2 }] }
it 'returns an instance of SnapshotLine' do
expect(line).to be_kind_of(SnapshotLine)
end
it 'returns properly joined fragments' do
fragment_1 = line.to_a[0]
fragment_2 = line.to_a[1]
expect(fragment_1.text).to eq('foo')
expect(fragment_1.brush).to eq(Brush.new(:fg => 1, :bold => true))
expect(fragment_2.text).to eq('bar')
expect(fragment_2.brush).to eq(Brush.new(:bg => 2))
end
end
describe '#each' do
let(:snapshot_line) { SnapshotLine.new([:fragment_1, :fragment_2]) }
it 'yields to the given block for each fragment' do
fragments = []
snapshot_line.each do |fragment|
fragments << fragment
end
expect(fragments).to eq([:fragment_1, :fragment_2])
end
end
describe '#==' do
let(:snapshot_line) { SnapshotLine.new([:foo]) }
subject { snapshot_line == other }
context "when lines have the same fragments" do
let(:other) { SnapshotLine.new([:foo]) }
it { should be(true) }
end
context "when lines have different fragments" do
let(:other) { SnapshotLine.new([:foo, :bar]) }
it { should be(false) }
end
end
describe '#crop' do
let(:snapshot_line) { SnapshotLine.new(fragments) }
let(:fragments) { [fragment_1, fragment_2] }
let(:fragment_1) { double('fragment_1', :size => 2, :crop => nil) }
let(:fragment_2) { double('fragment_2', :size => 3,
:crop => cropped_fragment_2) }
let(:fragment_3) { double('fragment_3', :size => 4, :crop => nil) }
let(:cropped_fragment_2) { double('cropped_fragment_2', :size => 2) }
context "when cropping point is at the end of the first fragment" do
it 'crops none of the fragments' do
snapshot_line.crop(2)
expect(fragment_1).to_not have_received(:crop)
expect(fragment_2).to_not have_received(:crop)
expect(fragment_3).to_not have_received(:crop)
end
it 'returns a new SnapshotLine with only the first fragment' do
expect(snapshot_line.crop(2)).to eq(SnapshotLine.new([fragment_1]))
end
end
context "when cropping point is inside of the second fragment" do
it 'crops only the second fragment' do
snapshot_line.crop(4)
expect(fragment_1).to_not have_received(:crop)
expect(fragment_2).to have_received(:crop).with(2)
expect(fragment_3).to_not have_received(:crop)
end
it 'returns a new SnapshotLine with first two fragments cropped' do
expect(snapshot_line.crop(4)).
to eq(SnapshotLine.new([fragment_1, cropped_fragment_2]))
end
end
end
end

@ -2,97 +2,101 @@ require 'spec_helper'
describe Snapshot do
describe '.build' do
let(:snapshot) { Snapshot.build(input) }
let(:input) { [input_line_1, input_line_2] }
let(:input_line_1) { double('input_line_1') }
let(:input_line_2) { double('input_line_2') }
let(:line_1) { double('line_1') }
let(:line_2) { double('line_2') }
before do
allow(SnapshotLine).to receive(:build).with(input_line_1) { line_1 }
allow(SnapshotLine).to receive(:build).with(input_line_2) { line_2 }
end
let(:snapshot) { described_class.new(data) }
it 'returns an instance of Snapshot' do
expect(snapshot).to be_kind_of(Snapshot)
end
let(:data) { [
[['a', fg: 1], ['b', fg: 2], ['c', fg: 3]],
[['d', fg: 4], ['e', fg: 5], ['f', fg: 6]],
[['g', bg: 1], ['h', bg: 2], ['i', bg: 3]],
[[' ', {} ], ['k', bg: 5], ['l', bg: 6]],
[[' ', {} ], [' ', {} ], [' ', {} ]]
] }
it 'includes lines built by SnapshotLine.build' do
expect(snapshot.to_a[0]).to be(line_1)
expect(snapshot.to_a[1]).to be(line_2)
end
describe '#width' do
subject { snapshot.width }
it { should eq(3) }
end
describe '#each' do
let(:snapshot) { Snapshot.new([:line_1, :line_2]) }
describe '#height' do
subject { snapshot.height }
it { should eq(5) }
end
it 'yields to the given block for each line' do
lines = []
describe '#cell' do
subject { snapshot.cell(column, line) }
snapshot.each do |line|
lines << line
end
context "at 0,0" do
let(:column) { 0 }
let(:line) { 0 }
expect(lines).to eq([:line_1, :line_2])
it { should eq(Cell.new('a', Brush.new(fg: 1))) }
end
end
describe '#==' do
let(:snapshot) { Snapshot.new([:foo]) }
context "at 1,2" do
let(:column) { 1 }
let(:line) { 2 }
subject { snapshot == other }
it { should eq(Cell.new('h', Brush.new(bg: 2))) }
end
context "when the other has the same lines" do
let(:other) { Snapshot.new([:foo]) }
context "at 2,3" do
let(:column) { 2 }
let(:line) { 3 }
it { should be(true) }
it { should eq(Cell.new('l', Brush.new(bg: 6))) }
end
end
context "when the other has a different lines" do
let(:other) { Snapshot.new([:foo, :bar]) }
describe '#thumbnail' do
it { should be(false) }
def thumbnail_text(thumbnail)
''.tap do |text|
0.upto(thumbnail.height - 1) do |line|
0.upto(thumbnail.width - 1) do |column|
text << thumbnail.cell(column, line).text
end
text << "\n"
end
end
end
end
describe '#crop' do
let(:snapshot) { Snapshot.new(lines) }
let(:lines) { [line_1, line_2, line_3] }
let(:line_1) { double('line_1', :crop => cropped_line_1) }
let(:line_2) { double('line_2', :crop => cropped_line_2) }
let(:line_3) { double('line_3', :crop => cropped_line_3) }
let(:cropped_line_1) { double('cropped_line_1') }
let(:cropped_line_2) { double('cropped_line_2') }
let(:cropped_line_3) { double('cropped_line_3') }
let(:width) { 3 }
let(:height) { 3 }
let(:thumbnail) { snapshot.thumbnail(2, height) }
let(:text) { thumbnail_text(thumbnail) }
subject { snapshot.crop(width, height) }
it 'is a snapshot of requested width' do
expect(thumbnail.width).to eq(2)
end
context "when height is lower than lines count" do
let(:height) { 2 }
it 'is a snapshot of requested height' do
expect(thumbnail.height).to eq(3)
end
it 'crops the last "height" lines' do
subject
context "when height is 3" do
let(:height) { 3 }
expect(line_1).to_not have_received(:crop)
expect(line_2).to have_received(:crop).with(3)
expect(line_3).to have_received(:crop).with(3)
it 'returns thumbnail with 2nd, 3rd and 4th line cropped' do
expect(text).to eq("de\ngh\n k\n")
end
end
context "when height is 5" do
let(:height) { 5 }
it 'returns a new Snapshot with last 2 lines cropped' do
expect(subject).to eq(Snapshot.new([cropped_line_2, cropped_line_3]))
it 'returns thumbnail with all the lines cropped' do
expect(text).to eq("ab\nde\ngh\n k\n \n")
end
end
context "when height is equal to lines count" do
let(:height) { 3 }
context "when height is 6" do
let(:height) { 6 }
it 'returns a new Snapshot with all lines cropped' do
expect(subject).to eq(Snapshot.new([cropped_line_1, cropped_line_2,
cropped_line_3]))
it 'returns thumbnail with all the lines cropped + 1 empty line' do
expect(text).to eq("ab\nde\ngh\n k\n \n \n")
end
end
end
end

@ -1,27 +0,0 @@
require 'spec_helper'
describe SnapshotFragmentPresenter do
let(:snapshot_fragment_presenter) { described_class.new(snapshot_fragment) }
let(:snapshot_fragment) { SnapshotFragment.new('foo > bar', brush) }
let(:brush) { double('brush') }
let(:brush_presenter) { double('presenter', :to_css_class => css_class) }
let(:css_class) { 'qux' }
describe '#to_html' do
subject { snapshot_fragment_presenter.to_html }
before do
allow(BrushPresenter).to receive(:new).with(brush).
and_return(brush_presenter)
end
it { should be_kind_of(ActiveSupport::SafeBuffer) }
it { should eq('<span class="qux">foo &gt; bar</span>') }
context "when css class is nil" do
let(:css_class) { nil }
it { should eq('<span>foo &gt; bar</span>') }
end
end
end

@ -1,24 +0,0 @@
require 'spec_helper'
describe SnapshotLinePresenter do
let(:snapshot_line_presenter) { SnapshotLinePresenter.new(snapshot_line) }
let(:snapshot_line) { SnapshotLine.new([fragment_1, fragment_2]) }
let(:fragment_1) { double('fragment_1') }
let(:fragment_2) { double('fragment_2') }
let(:fragment_1_presenter) { double(:to_html => '<fragment_1>') }
let(:fragment_2_presenter) { double(:to_html => '<fragment_2>') }
describe '#to_html' do
subject { snapshot_line_presenter.to_html }
before do
allow(SnapshotFragmentPresenter).to receive(:new).with(fragment_1).
and_return(fragment_1_presenter)
allow(SnapshotFragmentPresenter).to receive(:new).with(fragment_2).
and_return(fragment_2_presenter)
end
it { should be_kind_of(ActiveSupport::SafeBuffer) }
it { should eq('<span class="line"><fragment_1><fragment_2></span>') }
end
end

@ -1,24 +0,0 @@
require 'spec_helper'
describe SnapshotPresenter do
let(:snapshot_presenter) { SnapshotPresenter.new(snapshot) }
let(:snapshot) { Snapshot.new([line_1, line_2]) }
let(:line_1) { double('line_1') }
let(:line_2) { double('line_2') }
let(:line_1_presenter) { double(:to_html => '<line_1>') }
let(:line_2_presenter) { double(:to_html => '<line_2>') }
describe '#to_html' do
subject { snapshot_presenter.to_html }
before do
allow(SnapshotLinePresenter).to receive(:new).with(line_1).
and_return(line_1_presenter)
allow(SnapshotLinePresenter).to receive(:new).with(line_2).
and_return(line_2_presenter)
end
it { should be_kind_of(ActiveSupport::SafeBuffer) }
it { should eq(%(<pre class="terminal"><line_1>\n<line_2>\n</pre>)) }
end
end
Loading…
Cancel
Save