rack-reshow 0.1.0

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.
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