Refactor AsciicastDecorator#thumbnail to render colorful thumbnail

openid
Marcin Kulik 11 years ago
parent 86f728364c
commit d93ac84c21

@ -35,23 +35,8 @@ class AsciicastDecorator < ApplicationDecorator
end
def thumbnail(width = THUMBNAIL_WIDTH, height = THUMBNAIL_HEIGHT)
if @thumbnail.nil?
lines = model.snapshot.to_s.split("\n")
top_lines = lines[0...height]
top_text = prepare_lines(top_lines, width, height).join("\n")
bottom_lines = lines.reverse[0...height].reverse
bottom_text = prepare_lines(bottom_lines, width, height).join("\n")
if top_text.gsub(/\s+/, '').size > bottom_text.gsub(/\s+/, '').size
@thumbnail = top_text
else
@thumbnail = bottom_text
end
end
@thumbnail
thumbnail = model.snapshot.crop(width, height)
SnapshotPresenter.new(thumbnail).to_html
end
def description

@ -6,7 +6,7 @@ class Asciicast < ActiveRecord::Base
mount_uploader :stdout, BasicUploader
mount_uploader :stdout_timing, BasicUploader
serialize :snapshot, Snapshot::Serializer.new
serialize :snapshot, Snapshot
validates :stdout, :stdout_timing, :presence => true
validates :terminal_columns, :terminal_lines, :duration, :presence => true

@ -1,5 +1,4 @@
class Brush
attr_reader :attributes
def initialize(attributes = {})
@attributes = attributes
@ -24,4 +23,9 @@ class Brush
def inverse?
!!attributes[:inverse]
end
private
attr_reader :attributes
end

@ -1,4 +1,5 @@
class Snapshot
attr_reader :lines
def initialize(lines = [])
@ -9,13 +10,9 @@ class Snapshot
other.lines == lines
end
class Serializer
def dump(snapshot)
YAML.dump(snapshot.lines)
end
def load(value)
value.present? ? Snapshot.new(YAML.load(value)) : Snapshot.new
end
def crop(width, height)
new_lines = lines.drop(lines.size - height).map { |line| line.crop(width) }
self.class.new(new_lines)
end
end

@ -0,0 +1,22 @@
class SnapshotFragment
attr_reader :text, :brush
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
end

@ -0,0 +1,31 @@
class SnapshotLine
attr_reader :fragments
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
end

@ -0,0 +1,33 @@
class BrushPresenter < SimpleDelegator
def to_css_class
if default?
nil
else
[fg_class, bg_class, bold_class, underline_class, inverse_class].compact.join(' ')
end
end
private
def fg_class
"fg#{fg}" if fg
end
def bg_class
"bg#{bg}" if bg
end
def bold_class
'bold' if bold?
end
def underline_class
'underline' if underline?
end
def inverse_class
'inverse' if inverse?
end
end

@ -0,0 +1,15 @@
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

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

@ -0,0 +1,19 @@
class SnapshotPresenter < Draper::Decorator
delegate :lines
def to_html
h.content_tag(:pre, line_strings.join.html_safe, :class => 'thumbnail')
end
private
def line_strings
lines.map { |line| line_string(line) }
end
def line_string(line)
SnapshotLinePresenter.new(line).to_html
end
end

@ -134,7 +134,24 @@ describe AsciicastDecorator do
end
describe '#thumbnail' do
pending
let(:snapshot) { double('snapshot', :crop => thumbnail) }
let(:thumbnail) { double('thumbnail') }
let(:presenter) { double('presenter') }
before do
allow(asciicast).to receive(:snapshot) { snapshot }
allow(SnapshotPresenter).to receive(:new).with(thumbnail) { presenter }
allow(presenter).to receive(:to_html) { '<pre></pre>' }
end
it 'crops the snapshot' do
decorated.thumbnail(21, 13)
expect(snapshot).to have_received(:crop).with(21, 13)
end
it 'returns html snapshot rendered by SnapshotPresenter#to_html' do
expect(decorated.thumbnail).to eq('<pre></pre>')
end
end
describe '#author' do
@ -230,7 +247,7 @@ describe AsciicastDecorator do
end
it 'returns avatar_image_tag' do
decorated.stub!(:h => h)
decorated.stub(:h => h)
h.should_receive(:avatar_image_tag).with(nil).and_return(avatar_image)
subject.should == avatar_image
end
@ -243,7 +260,7 @@ describe AsciicastDecorator do
describe '#embed_script' do
before do
asciicast.stub!(:id => 123)
asciicast.stub(:id => 123)
end
it 'should be an async script tag including asciicast id' do

@ -120,24 +120,31 @@ describe Asciicast do
describe '#snapshot' do
let(:asciicast) { Asciicast.new }
let(:snapshot) {
Snapshot.new([
SnapshotLine.new([
SnapshotFragment.new('foo', { :fg => 1 })
])
])
}
it 'is empty Snapshot instance initially' do
expect(asciicast.snapshot).to eq(Snapshot.new)
end
it 'is a Snapshot instance before persisting' do
asciicast.snapshot = Snapshot.new({ :foo => 1 })
asciicast.snapshot = snapshot
expect(asciicast.snapshot).to eq(Snapshot.new({ :foo => 1 }))
expect(asciicast.snapshot).to eq(snapshot)
end
it 'is a Snapshot instance after persisting and loading' do
asciicast = build(:asciicast)
asciicast.snapshot = Snapshot.new({ :foo => 1 })
asciicast.snapshot = snapshot
asciicast.save!
expect(asciicast.snapshot).to eq(Snapshot.new({ :foo => 1 }))
expect(Asciicast.find(asciicast.id).snapshot).to eq(Snapshot.new({ :foo => 1 }))
expect(asciicast.snapshot).to eq(snapshot)
expect(Asciicast.find(asciicast.id).snapshot).to eq(snapshot)
end
end
end

@ -0,0 +1,57 @@
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
end

@ -0,0 +1,60 @@
require 'spec_helper'
describe SnapshotLine do
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

@ -20,4 +20,25 @@ describe Snapshot do
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 => nil) }
let(:line_2) { double('line_2', :crop => cropped_line_2) }
let(:line_3) { double('line_3', :crop => cropped_line_3) }
let(:cropped_line_2) { double('cropped_line_2') }
let(:cropped_line_3) { double('cropped_line_3') }
it 'crops the last "height" lines' do
snapshot.crop(3, 2)
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)
end
it 'returns a new Snapshot with cropped lines' do
expect(snapshot.crop(3, 2)).to eq(Snapshot.new([cropped_line_2, cropped_line_3]))
end
end
end

@ -0,0 +1,91 @@
require 'spec_helper'
describe BrushPresenter do
let(:brush_presenter) { BrushPresenter.new(brush) }
let(:brush) { double('brush', :fg => nil, :bg => nil, :bold? => false,
:underline? => false, :inverse? => false) }
describe '#to_css_class' do
subject { brush_presenter.to_css_class }
context "when brush is a default one" do
before do
allow(brush).to receive(:default?) { true }
end
it { should be(nil) }
end
context "when brush is not a default one" do
before do
allow(brush).to receive(:default?) { false }
end
context "when fg is default" do
before do
allow(brush).to receive(:fg) { nil }
end
it { should_not match(/\bfg/) }
end
context "when fg is non-default" do
before do
allow(brush).to receive(:fg) { 1 }
end
it { should match(/\bfg1\b/) }
end
context "when bg is default" do
before do
allow(brush).to receive(:bg) { nil }
end
it { should_not match(/\bbg/) }
end
context "when bg is non-default" do
before do
allow(brush).to receive(:bg) { 2 }
end
it { should match(/\bbg2\b/) }
end
context "when both fg and bg are non-default" do
before do
allow(brush).to receive(:fg) { 1 }
allow(brush).to receive(:bg) { 2 }
end
it { should match(/\bfg1\b/) }
it { should match(/\bbg2\b/) }
end
context "when it's bold" do
before do
allow(brush).to receive(:bold?) { true }
end
it { should match(/\bbold\b/) }
end
context "when it's underline" do
before do
allow(brush).to receive(:underline?) { true }
end
it { should match(/\bunderline\b/) }
end
context "when it's inverse" do
before do
allow(brush).to receive(:inverse?) { true }
end
it { should match(/\binverse\b/) }
end
end
end
end

@ -0,0 +1,27 @@
require 'spec_helper'
describe SnapshotFragmentPresenter do
let(:snapshot_fragment_presenter) { SnapshotFragmentPresenter.new(snapshot_fragment) }
let(:snapshot_fragment) { SnapshotFragment.new('foo > bar', brush) }
let(:brush) { double('brush') }
let(:brush_presenter) { double('brush_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

@ -0,0 +1,24 @@
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

@ -0,0 +1,24 @@
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="thumbnail"><line_1><line_2></pre>') }
end
end
Loading…
Cancel
Save