├── .gitignore ├── .travis.yml ├── Appraisals ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── rack_1.gemfile └── rack_2.gemfile ├── lib ├── rack-pjax.rb └── rack │ ├── pjax.rb │ └── pjax │ └── version.rb ├── rack-pjax.gemspec └── spec ├── rack └── pjax_spec.rb ├── spec.opts └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .rvmrc 6 | .rbenv-version 7 | gemfiles/*.lock 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.1.10 4 | - 2.2.7 5 | - 2.3.4 6 | - 2.4.1 7 | - jruby 8 | gemfile: 9 | - gemfiles/rack_1.gemfile 10 | - gemfiles/rack_2.gemfile 11 | matrix: 12 | exclude: 13 | - rvm: 1.9.3 14 | gemfile: gemfiles/rack_2.gemfile 15 | - rvm: 2.1.10 16 | gemfile: gemfiles/rack_2.gemfile 17 | - rvm: jruby 18 | gemfile: gemfiles/rack_2.gemfile 19 | cache: bundler 20 | sudo: false 21 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rack-1" do 2 | gem 'rack', '~> 1.0' 3 | gem 'nokogiri', '~> 1.6.8' 4 | end 5 | 6 | appraise "rack-2" do 7 | gem 'rack', '~> 2.0' 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'appraisal' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Gert Goet, ThinkCreate 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rack-pjax [![travis](https://secure.travis-ci.org/eval/rack-pjax.png?branch=master)](https://secure.travis-ci.org/#!/eval/rack-pjax) 2 | ======== 3 | 4 | Rack-pjax is middleware that lets you serve 'chrome-less' pages in respond to [pjax-requests](https://github.com/defunkt/jquery-pjax). 5 | 6 | It does this by stripping the generated body; only the title and inner-html of the pjax-container are sent to the client. 7 | 8 | While this won't save you any time rendering the page, it gives you more flexibility where and how to define the pjax-container. 9 | Ryan Bates featured [rack-pjax on Railscasts](http://railscasts.com/episodes/294-playing-with-pjax) and explains how this gem compares to [pjax_rails](https://github.com/rails/pjax_rails). 10 | 11 | [![railscast](http://railscasts.com/assets/railscasts_logo-7101a7cd0a48292a0c07276981855edb.png)](http://railscasts.com/) 12 | 13 | Installation 14 | ------------ 15 | 16 | Check out the [Railscasts' notes](http://railscasts.com/episodes/294-playing-with-pjax) how to integrate rack-pjax in your Rails 3.1 application. 17 | 18 | You can find the source from the screencast over [here](https://github.com/ryanb/railscasts-episodes/tree/master/episode-294). 19 | 20 | Another sample-app: the original [pjax-demo](http://pjax.herokuapp.com/) but with rack-pjax onboard can be found in the [sample-app](https://github.com/eval/rack-pjax/tree/sample-app) branch. 21 | 22 | The more generic installation comes down to: 23 | 24 | I. Add the gem to your Gemfile 25 | 26 | ```ruby 27 | # Gemfile 28 | gem "rack-pjax" 29 | ``` 30 | 31 | II. Include **rack-pjax** as middleware to your application(-stack) 32 | 33 | ```ruby 34 | # config.ru 35 | require ::File.expand_path('../config/environment', __FILE__) 36 | use Rack::Pjax 37 | run RackApp::Application 38 | ``` 39 | 40 | III. Install [jquery-pjax](https://github.com/defunkt/jquery-pjax). Make sure to add the 'data-pjax-container'-attribute to the container. 41 | 42 | ```html 43 | 44 | ... 45 | 46 | 47 | 52 | ... 53 | 54 | 55 |
56 | ... 57 |
58 | 59 | ``` 60 | 61 | (For more see [the docs of jquery-pjax](https://github.com/defunkt/jquery-pjax#usage).) 62 | 63 | IV. Fire up your [pushState-enabled browser](http://caniuse.com/#search=pushstate) and enjoy! 64 | 65 | 66 | Requirements 67 | ------------ 68 | 69 | - Nokogiri 70 | 71 | 72 | Contributors 73 | ------ 74 | 75 | [The contributors](https://github.com/eval/rack-pjax/graphs/contributors). 76 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new do |t| 5 | t.rspec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""] 6 | end 7 | 8 | desc "Run the specs" 9 | task :default => :spec 10 | 11 | desc 'Removes trailing whitespace' 12 | task :whitespace do 13 | sh %{find . -name '*.rb' -exec sed -i '' 's/ *$//g' {} \\;} 14 | end 15 | -------------------------------------------------------------------------------- /gemfiles/rack_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rack", "~> 1.0" 7 | gem "nokogiri", "~> 1.6.8" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rack_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "rack", "~> 2.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /lib/rack-pjax.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require "rack/pjax" 3 | -------------------------------------------------------------------------------- /lib/rack/pjax.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | module Rack 4 | class Pjax 5 | include Rack::Utils 6 | 7 | def initialize(app) 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | status, headers, body = @app.call(env) 13 | return [status, headers, body] unless pjax?(env) 14 | 15 | headers = HeaderHash.new(headers) 16 | 17 | new_body = "" 18 | body.each do |b| 19 | b = b.dup.force_encoding('UTF-8') if RUBY_VERSION > '1.9.0' 20 | 21 | parsed_body = Nokogiri::HTML(b) 22 | container = parsed_body.at(container_selector(env)) 23 | 24 | new_body << begin 25 | if container 26 | title = parsed_body.at("title") 27 | 28 | "%s%s" % [title, container.inner_html] 29 | else 30 | b 31 | end 32 | end 33 | end 34 | 35 | body.close if body.respond_to?(:close) 36 | 37 | headers['Content-Length'] &&= new_body.bytesize.to_s 38 | headers['X-PJAX-URL'] ||= Rack::Request.new(env).fullpath 39 | 40 | [status, headers, [new_body]] 41 | end 42 | 43 | protected 44 | def pjax?(env) 45 | env['HTTP_X_PJAX'] 46 | end 47 | 48 | def container_selector(env) 49 | env['HTTP_X_PJAX_CONTAINER'] || "[@data-pjax-container]" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/rack/pjax/version.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Pjax 3 | VERSION = "1.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /rack-pjax.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "rack/pjax/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "rack-pjax" 7 | s.version = Rack::Pjax::VERSION 8 | s.authors = ["Gert Goet"] 9 | s.email = ["gert@thinkcreate.nl"] 10 | s.homepage = "https://github.com/eval/rack-pjax" 11 | s.license = "MIT" 12 | s.summary = %q{Serve pjax responses through rack middleware} 13 | s.description = %q{Serve pjax responses through rack middleware} 14 | 15 | s.rubyforge_project = "rack-pjax" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency('rack', '>= 1.1') 23 | s.add_dependency('nokogiri', '~> 1.5') 24 | 25 | s.add_development_dependency "rake" 26 | s.add_development_dependency "rspec" 27 | s.add_development_dependency "rack-test" 28 | end 29 | -------------------------------------------------------------------------------- /spec/rack/pjax_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe Rack::Pjax do 4 | include Rack::Test::Methods # can be moved to config 5 | 6 | def generate_app(options={}) 7 | body = options[:body] 8 | 9 | Rack::Lint.new( 10 | Rack::Pjax.new( 11 | lambda do |env| 12 | [ 13 | 200, 14 | {'Content-Type' => 'text/plain', 'Content-Length' => body.bytesize.to_s}, 15 | [body] 16 | ] 17 | end 18 | ) 19 | ) 20 | end 21 | 22 | context "a pjaxified app, upon receiving a pjax-request" do 23 | before do 24 | self.class.app = generate_app(:body => 'Hello
World!
') 25 | end 26 | 27 | it "should return the title-tag in the body" do 28 | get "/", {}, {"HTTP_X_PJAX" => "true"} 29 | expect(body).to eq("HelloWorld!") 30 | end 31 | 32 | it "should return the inner-html of the pjax-container in the body" do 33 | self.class.app = generate_app(:body => '
World!
') 34 | 35 | get "/", {}, {"HTTP_X_PJAX" => "true"} 36 | expect(body).to eq("World!") 37 | end 38 | 39 | it "should return the inner-html of the custom pjax-container in the body" do 40 | self.class.app = generate_app(:body => '
World!
') 41 | 42 | get "/", {}, {"HTTP_X_PJAX" => "true", "HTTP_X_PJAX_CONTAINER" => "#container"} 43 | expect(body).to eq("World!") 44 | end 45 | 46 | it "should handle self closing tags with HTML5 elements" do 47 | self.class.app = generate_app(:body => '
World!
') 48 | 49 | get "/", {}, {"HTTP_X_PJAX" => "true"} 50 | 51 | expect(body).to eq('
World!
') 52 | end 53 | 54 | it "should handle nesting of elements inside anchor tags" do 55 | self.class.app = generate_app(:body => '

World!

') 56 | 57 | get "/", {}, {"HTTP_X_PJAX" => "true"} 58 | 59 | expect(body).to eq('

World!

') 60 | end 61 | 62 | it "should handle html5 br tags correctly" do 63 | self.class.app = generate_app(:body => '

foo
bar

') 64 | 65 | get "/", {}, {"HTTP_X_PJAX" => "true"} 66 | 67 | expect(body).to eq('

foo
bar

') 68 | end 69 | 70 | it "should handle frozen body string correctly" do 71 | self.class.app = generate_app(:body => '

foo
bar

'.freeze) 72 | 73 | get "/", {}, {"HTTP_X_PJAX" => "true"} 74 | 75 | expect(body).to eq('

foo
bar

') 76 | end 77 | 78 | it "should return the correct Content Length" do 79 | get "/", {}, {"HTTP_X_PJAX" => "true"} 80 | expect(headers['Content-Length']).to eq(body.bytesize.to_s) 81 | end 82 | 83 | it "should return the original body when there's no pjax-container" do 84 | self.class.app = generate_app(:body => 'Has no pjax-container') 85 | 86 | get "/", {}, {"HTTP_X_PJAX" => "true"} 87 | expect(body).to eq("Has no pjax-container") 88 | end 89 | 90 | it "should preserve whitespaces of the original body" do 91 | container = "\n

\nfirst paragraph

Second paragraph

\n" 92 | self.class.app = generate_app(:body =><<-BODY) 93 | 94 |
#{container}
95 | 96 | BODY 97 | 98 | get "/", {}, {"HTTP_X_PJAX" => "true"} 99 | expect(body).to eq(container) 100 | end 101 | end 102 | 103 | context "a pjaxified app, upon receiving a non-pjax request" do 104 | before do 105 | self.class.app = generate_app(:body => 'Hello
World!
') 106 | end 107 | 108 | it "should return the original body" do 109 | get "/" 110 | expect(body).to eq('Hello
World!
') 111 | end 112 | 113 | it "should return the correct Content Length" do 114 | get "/" 115 | expect(headers['Content-Length']).to eq(body.bytesize.to_s) 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --format d 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler" 3 | Bundler.setup 4 | 5 | $:.unshift File.expand_path("../../lib", __FILE__) 6 | require "rack-pjax" 7 | require 'rack/test' 8 | 9 | Bundler.require(:test) 10 | 11 | # helpers ripped from wycat's Rack::Offline 12 | # (https://github.com/wycats/rack-offline/blob/master/spec/spec_helper.rb) 13 | module Rack::Test::Methods 14 | def self.included(klass) 15 | class << klass 16 | attr_accessor :app 17 | end 18 | end 19 | 20 | def body 21 | last_response.body 22 | end 23 | 24 | def status 25 | last_response.status 26 | end 27 | 28 | def headers 29 | last_response.headers 30 | end 31 | 32 | def app 33 | self.class.app 34 | end 35 | end 36 | --------------------------------------------------------------------------------