rack-reshow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 copypastel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,52 @@
1
+ h1. rack-reshow
2
+
3
+ h2. the little toolbar that could
4
+
5
+ p. @Rack::Reshow@ adds a toolbar to rendered views that stores different versions of the page. Afterwards, reviewing and comparing outputted changes is a single click away.
6
+
7
+ h2. use
8
+
9
+ To install:
10
+
11
+ @gem install rack-reshow@
12
+
13
+ With Rails:
14
+
15
+ bq. config.gem "rack-reshow", :lib => "rack/reshow"
16
+ config.middleware.use "Rack::Reshow"
17
+
18
+ In your rackup file:
19
+
20
+ bq. require 'rack/reshow'
21
+ use Rack::Reshow
22
+
23
+
24
+ h2. why
25
+
26
+ p. Development of a view is a trial and error process by far: change code, reload the browser, change to previous version, reload the browser, compare the two versions, repeat until satisfied.
27
+
28
+ p. By storing the rendered views each time they change, @Rack::Reshow@ tightens the review loop, effectively creating a history for each page that can be clicked through.
29
+
30
+ h2. how
31
+
32
+ p. @Rack::Reshow@ uses @PStore@ to store different versions of a page.
33
+
34
+ p. Each time a request is received, @Rack::Reshow@ checks if the @<body>@ of the response differs from what it last was. If so, it adds it to the store. Afterwards, it injects all previous versions into the response, and makes them transversable via a small, elegant bar.
35
+
36
+ h2. limitations
37
+
38
+ p. @Rack::Reshow@ only works on responses that have a @<head>@ and @<body>@, namely the type of page one works with when dealing with a layout's look and feel.
39
+
40
+ p. Furthermore, it works best when javascript is applied unobtrusively; if there's any javascript inside the response's @<body>@, it'll get called once for each time said javascript code appears in the page's history.
41
+
42
+ p. Finally, the same page may have different content depending on session variables (i.e. after one logs in), or simply dynamic variables (i.e. displaying the date/time). @Rack::Reshow@ will store both versions separately, even though there's no code modification going on behind the scenes.
43
+
44
+ h2. thanks
45
+
46
+ p. "famfamfam":http://famfamfam.com for the elegant icons.
47
+ "Rack::Bug":http://github.com/brynary/rack-bug for insight into how to serve static content and inject content into a response.
48
+ "Jeweler":http://github.com/technicalpickles/jeweler for making publishing gems so simple.
49
+
50
+ h2. copyright
51
+
52
+ p. Copyright (c) 2010 copypastel. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rack-reshow"
8
+ gem.summary = %Q{Time machine for your views in the shape of a Rack middleware.}
9
+ #gem.description = %Q{TODO: longer description of your gem}
10
+ gem.email = "ecin@copypastel.com"
11
+ gem.homepage = "http://github.com/ecin/rack-reshow"
12
+ gem.authors = ["ecin"]
13
+ gem.add_dependency "rack", ">= 1.0"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "rack-reshow #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
Binary file
Binary file
Binary file
@@ -0,0 +1,44 @@
1
+ #__reshow_bar__{
2
+ padding: 3px 15px 3px 15px;
3
+ margin: 0;
4
+ font-family: helvetica;
5
+ text-align: center;
6
+ position: fixed;
7
+ top: -25px;
8
+ left: 50px;
9
+ height: 25px;
10
+ background: url(/__reshow__/border.png) white repeat-x bottom left;
11
+ border-left: 1px solid #ccc;
12
+ border-right: 1px solid #ccc;
13
+ z-index: 1024;
14
+ }
15
+
16
+ #__reshow_bar__ img{
17
+ cursor: pointer;
18
+ position: relative;
19
+ top: 3px;
20
+ }
21
+
22
+ #__reshow_version__{
23
+ font-weight: bold;
24
+ margin-right: 10px;
25
+ color: steelblue;
26
+ }
27
+
28
+ #__reshow_info__{
29
+ opacity: 0.33;
30
+ }
31
+
32
+ #__reshow_next__{
33
+ opacity: 0.33;
34
+ }
35
+
36
+ .__reshow_body__{
37
+ opacity: 0;
38
+ display: none;
39
+ }
40
+
41
+ .__reshow_active__{
42
+ opacity: 1;
43
+ display: block;
44
+ }
@@ -0,0 +1,107 @@
1
+ jQuery(document).ready( function(){
2
+
3
+ jQuery.extend( jQuery.easing, {
4
+ 'easeOutBounce': function(x, t, b, c, d) {
5
+ if ((t/=d) < (1/2.75)) {
6
+ return c*(7.5625*t*t) + b;
7
+ } else if (t < (2/2.75)) {
8
+ return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
9
+ } else if (t < (2.5/2.75)) {
10
+ return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
11
+ } else {
12
+ return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
13
+ }
14
+ }
15
+ });
16
+
17
+ var __reshow__ = {
18
+ prev: function(){
19
+ var active = jQuery('.__reshow_active__');
20
+ var el = jQuery(this);
21
+ if(active.prev().length > 0){
22
+ active.animate({opacity: 0},
23
+ {
24
+ complete: function(){
25
+ active.css('display', 'none');
26
+ active.toggleClass('__reshow_active__')
27
+ active = active.prev('.__reshow_body__');
28
+ active.toggleClass( '__reshow_active__')
29
+ active.css('opacity', 0);
30
+ active.css('display', 'block');
31
+ active.animate({opacity: 1});
32
+ var version = Number(jQuery('#__reshow_version__').text()) - 1;
33
+ version = (version < 10 ? '0' : '') + version
34
+ jQuery('#__reshow_version__').text(version);
35
+ // Okay, bind the click again.
36
+ el.one('click', __reshow__.prev);
37
+ if(active.prev().length > 0){
38
+ // Okay, bind the click again.
39
+ el.one('click', __reshow__.prev);
40
+ } else{
41
+ el.animate({opacity: 0.33});
42
+ }
43
+ jQuery('#__reshow_next__').animate({opacity: 1}).one('click', __reshow__.next);
44
+ }
45
+ });
46
+ } else{
47
+ el.one('click', __reshow__.prev);
48
+ }
49
+ },
50
+ next: function(){
51
+ var active = jQuery('.__reshow_active__');
52
+ var el = jQuery(this);
53
+ if(active.next().length > 0){
54
+ active.animate({opacity: 0},
55
+ {
56
+ complete: function(){
57
+ active.css('display', 'none');
58
+ active.toggleClass('__reshow_active__')
59
+ active = active.next()
60
+ active.toggleClass( '__reshow_active__')
61
+ active.css('opacity', 0);
62
+ active.css('display', 'block');
63
+ active.animate({opacity: 1});
64
+ var version = Number(jQuery('#__reshow_version__').text()) + 1;
65
+ version = (version < 10 ? '0' : '') + version
66
+ jQuery('#__reshow_version__').text(version);
67
+ if(active.next().length > 0){
68
+ // Okay, bind the click again.
69
+ el.one('click', __reshow__.next);
70
+ } else{
71
+ el.animate({opacity: 0.33});
72
+ }
73
+ jQuery('#__reshow_prev__').animate({opacity: 1}).one('click', __reshow__.prev);
74
+ }
75
+ });
76
+ } else{
77
+ el.one('click', __reshow__.next);
78
+ }
79
+ }
80
+ };
81
+
82
+ jQuery('.__reshow_body__:last').toggleClass('__reshow_active__');
83
+ if(jQuery('.__reshow_active__').prev().length > 0)
84
+ jQuery('#__reshow_prev__').one('click', __reshow__.prev);
85
+ //jQuery('#__reshow_next__').one('click', __reshow__.next);
86
+ jQuery('#__reshow_info__').click( function(){
87
+ console.log("To be replaced with a Swift.js tooltip.");
88
+ });
89
+
90
+ var bar = jQuery('#__reshow_bar__');
91
+
92
+ setTimeout(function(){
93
+ bar.animate({'top': 0}, 'slow', 'easeOutBounce').animate({'opacity': 0.3},
94
+ {
95
+ complete: function(){
96
+ bar.bind('mouseover', function(){
97
+ jQuery(this).stop().animate({'opacity': 1});
98
+ });
99
+
100
+ bar.bind('mouseout', function(){
101
+ jQuery(this).stop().animate({'opacity': 0.3});
102
+ });
103
+ }
104
+ });
105
+ }, 1000);
106
+
107
+ });
@@ -0,0 +1,115 @@
1
+ require 'pstore'
2
+ require 'rack/static'
3
+
4
+ module Rack
5
+ class Reshow
6
+
7
+ class RackStaticBugAvoider
8
+ def initialize(app, static_app)
9
+ @app = app
10
+ @static_app = static_app
11
+ end
12
+
13
+ def call(env)
14
+ if env["PATH_INFO"]
15
+ @static_app.call(env)
16
+ else
17
+ @app.call(env)
18
+ end
19
+ end
20
+ end
21
+
22
+ @@store_file = 'reshow.ps'
23
+
24
+ def initialize( app, opts = {} )
25
+ root = Object::File.expand_path(Object::File.dirname(__FILE__))
26
+ @app = RackStaticBugAvoider.new(app, Rack::Static.new(app, :urls => ["/__reshow__"], :root => root))
27
+ @store = PStore.new(@@store_file)
28
+ end
29
+
30
+ def call( env )
31
+ status, headers, body = @app.call(env)
32
+ request = Request.new(env)
33
+ if request.get? and status == 200
34
+ path = request.path
35
+ if body.respond_to? :join
36
+ body = body.join
37
+ @store.transaction do |store|
38
+ store[path] ||= []
39
+ content = body.scan(/<body>(.*?)<\/body>/m).flatten.first
40
+ store[path] << content unless content.nil? or store[path].last.eql?(content)
41
+ body.sub! /<body>.*<\/body>/m, %q{<body><div id="__reshow_bodies__"></div></body>}
42
+ store[path].reverse.each do |c|
43
+ prepend_to_tag '<div id="__reshow_bodies__">', body, tag(:div, c, :class => '__reshow_body__')
44
+ end
45
+ insert_reshow_bar body, store[path].size
46
+ end
47
+ headers['Content-Length'] = body.length.to_s
48
+ body = [body]
49
+ end
50
+ end
51
+ [status, headers, body]
52
+ end
53
+
54
+ def []( path )
55
+ @store.transaction(true) {|store| store[path] || Array.new}
56
+ end
57
+
58
+ def purge!
59
+ Object::File.delete(@@store_file) if Object::File.exists?(@@store_file)
60
+ end
61
+
62
+ private
63
+
64
+ def prepend_to_tag(tag, page, string)
65
+ page.sub! /#{tag}/, "#{tag}\n" + string
66
+ end
67
+
68
+ def append_to_tag(tag, page, string)
69
+ page.sub! /#{tag}/, string + "\n#{tag}"
70
+ end
71
+
72
+ def insert_reshow_bar(page, versions)
73
+ append_to_tag '</head>', page, style
74
+ append_to_tag '</head>', page, jquery
75
+ append_to_tag '</head>', page, javascript
76
+ append_to_tag '</body>', page, toolbar(versions)
77
+ end
78
+
79
+ def tag(type, body, options={})
80
+ options = options.map {|key, value| "#{key}=\"#{value}\""}.join(' ')
81
+ <<-EOF
82
+ <#{type} #{options}">
83
+ #{body}
84
+ </#{type}>
85
+ EOF
86
+ end
87
+
88
+ def toolbar(versions)
89
+ versions = (versions < 10 ? '0' : '') + versions.to_s
90
+ <<-EOF
91
+ <div id="__reshow_bar__">
92
+ <span id="__reshow_version__">#{versions}</span>
93
+ <span style="margin-right: 10px;">
94
+ <img id="__reshow_prev__" src="/__reshow__/action_back.gif" style="margin-right: 7px;"/>
95
+ <img id="__reshow_next__" src="/__reshow__/action_forward.gif" />
96
+ </span>
97
+ <span><img id="__reshow_info__" src="/__reshow__/icon_alert.gif" /></span>
98
+ </div>
99
+ EOF
100
+ end
101
+
102
+ def style
103
+ %q{<link charset='utf-8' href="/__reshow__/reshow.css" rel='stylesheet' type='text/css'>}
104
+ end
105
+
106
+ def javascript
107
+ %q{<script src="/__reshow__/reshow.js"></script>}
108
+ end
109
+
110
+ def jquery
111
+ %q{<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>}
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ include Rack
4
+
5
+ # Need to change the app for some test cases
6
+ class Rack::Reshow; attr_accessor :app; end
7
+
8
+ describe Rack::Reshow do
9
+
10
+ # Set up configuration variables
11
+
12
+ before :all do
13
+ @post_url = '/comments'
14
+ @env = Rack::MockRequest.env_for '/'
15
+ # The Lambdacat App
16
+ @body = ["<body>Lambda, lambda, lambda app, hoooo!</body>"]
17
+ @body2 = ["<body>Lambda app is on the run, lambda app is loose!</body>"]
18
+ @app = lambda {|env| [200, {}, @body]}
19
+ end
20
+
21
+ # Rack::Reshow middleware can be instantiated
22
+ it 'should accept an :app and an :opts hash at instantiation' do
23
+ lambda { Reshow.new(@app, {}) }.should_not raise_error
24
+ end
25
+
26
+ before(:each) { @middleware = Reshow.new @app }
27
+
28
+ it 'should return an array with stored versions given a path' do
29
+ @middleware['/'].should be_empty
30
+ @middleware.call @env
31
+ @middleware['/'].size.should == 1
32
+ end
33
+
34
+ it 'should allow to purge the pstore' do
35
+ @middleware.call @env
36
+ @middleware['/'].should_not be_empty
37
+ @middleware.purge!
38
+ @middleware['/'].should be_empty
39
+ end
40
+
41
+ after(:each) { @middleware.purge! }
42
+
43
+ # Rack::Reshow#call is defined, as in any middleware
44
+ it 'should return an array with status, headers, and body when sent :call' do
45
+ response = @middleware.call @env
46
+ response.class.should be(Array)
47
+ response.size.should == 3
48
+ response[2] == @body.to_s.scan(/<body>(.*?)<\/body>/m).flatten.first
49
+ end
50
+
51
+ it 'should save a version of a page when the content changes' do
52
+ @middleware.call @env
53
+ @middleware.app = lambda {|env| [200, {}, @body2]}
54
+ @middleware.call @env
55
+ @middleware['/'].size.should == 2
56
+ versions = @middleware['/']
57
+ versions[0].should match("Lambda, lambda, lambda app, hoooo!")
58
+ versions[1].should match("Lambda app is on the run, lambda app is loose!")
59
+ end
60
+
61
+ it 'should return all bodies in history (though hidden by css)' do
62
+ @middleware.call @env
63
+ @middleware.app = lambda {|env| [200, {}, @body2]}
64
+ status, headers, body = @middleware.call @env
65
+ body.to_s.should match(/Lambda app is on the run, lambda app is loose!/)
66
+ body.to_s.should match(/Lambda, lambda, lambda app, hoooo!/)
67
+ end
68
+
69
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rack/test'
4
+ require 'rack/reshow'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+
8
+ Spec::Runner.configure do |config|
9
+
10
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-reshow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ecin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-07 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "1.0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.9
34
+ version:
35
+ description:
36
+ email: ecin@copypastel.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.textile
44
+ files:
45
+ - .gitignore
46
+ - LICENSE
47
+ - README.textile
48
+ - Rakefile
49
+ - VERSION
50
+ - lib/rack/__reshow__/action_back.gif
51
+ - lib/rack/__reshow__/action_forward.gif
52
+ - lib/rack/__reshow__/border.png
53
+ - lib/rack/__reshow__/icon_alert.gif
54
+ - lib/rack/__reshow__/reshow.css
55
+ - lib/rack/__reshow__/reshow.js
56
+ - lib/rack/reshow.rb
57
+ - spec/reshow_spec.rb
58
+ - spec/spec.opts
59
+ - spec/spec_helper.rb
60
+ has_rdoc: true
61
+ homepage: http://github.com/ecin/rack-reshow
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options:
66
+ - --charset=UTF-8
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.5
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Time machine for your views in the shape of a Rack middleware.
88
+ test_files:
89
+ - spec/reshow_spec.rb
90
+ - spec/spec_helper.rb