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 end
def thumbnail(width = THUMBNAIL_WIDTH, height = THUMBNAIL_HEIGHT) def thumbnail(width = THUMBNAIL_WIDTH, height = THUMBNAIL_HEIGHT)
if @thumbnail.nil? thumbnail = model.snapshot.crop(width, height)
lines = model.snapshot.to_s.split("\n") SnapshotPresenter.new(thumbnail).to_html
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
end end
def description def description

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

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

@ -1,4 +1,5 @@
class Snapshot class Snapshot
attr_reader :lines attr_reader :lines
def initialize(lines = []) def initialize(lines = [])
@ -9,13 +10,9 @@ class Snapshot
other.lines == lines other.lines == lines
end end
class Serializer def crop(width, height)
def dump(snapshot) new_lines = lines.drop(lines.size - height).map { |line| line.crop(width) }
YAML.dump(snapshot.lines) self.class.new(new_lines)
end
def load(value)
value.present? ? Snapshot.new(YAML.load(value)) : Snapshot.new
end
end end
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 end
describe '#thumbnail' do 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 end
describe '#author' do describe '#author' do
@ -230,7 +247,7 @@ describe AsciicastDecorator do
end end
it 'returns avatar_image_tag' do 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) h.should_receive(:avatar_image_tag).with(nil).and_return(avatar_image)
subject.should == avatar_image subject.should == avatar_image
end end
@ -243,7 +260,7 @@ describe AsciicastDecorator do
describe '#embed_script' do describe '#embed_script' do
before do before do
asciicast.stub!(:id => 123) asciicast.stub(:id => 123)
end end
it 'should be an async script tag including asciicast id' do it 'should be an async script tag including asciicast id' do

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