rack-pagespeed 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Gemfile.lock +59 -0
- data/README.md +15 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/lib/rack-pagespeed.rb +1 -0
- data/lib/rack/pagespeed.rb +46 -0
- data/lib/rack/pagespeed/config.rb +72 -0
- data/lib/rack/pagespeed/filters/all.rb +7 -0
- data/lib/rack/pagespeed/filters/base.rb +53 -0
- data/lib/rack/pagespeed/filters/combine_css.rb +73 -0
- data/lib/rack/pagespeed/filters/combine_javascripts.rb +69 -0
- data/lib/rack/pagespeed/filters/inline_css.rb +16 -0
- data/lib/rack/pagespeed/filters/inline_javascripts.rb +17 -0
- data/lib/rack/pagespeed/filters/minify_javascripts.rb +41 -0
- data/lib/rack/pagespeed/store/all.rb +6 -0
- data/lib/rack/pagespeed/store/disk.rb +18 -0
- data/lib/rack/pagespeed/store/memcached.rb +17 -0
- data/spec/config_spec.rb +187 -0
- data/spec/filters/combine_css_spec.rb +30 -0
- data/spec/filters/combine_javascripts_spec.rb +48 -0
- data/spec/filters/filter_spec.rb +57 -0
- data/spec/filters/inline_css_spec.rb +33 -0
- data/spec/filters/inline_javascript_spec.rb +30 -0
- data/spec/filters/minify_javascript_spec.rb +59 -0
- data/spec/fixtures/complex.html +33 -0
- data/spec/fixtures/foo.js +1 -0
- data/spec/fixtures/hh-reset.css +1 -0
- data/spec/fixtures/huge.css +1176 -0
- data/spec/fixtures/iphone.css +1 -0
- data/spec/fixtures/jquery-1.4.1.min.js +152 -0
- data/spec/fixtures/medialess1.css +2 -0
- data/spec/fixtures/medialess2.css +2 -0
- data/spec/fixtures/mylib.js +3 -0
- data/spec/fixtures/noexternalcss.html +11 -0
- data/spec/fixtures/noscripts.html +9 -0
- data/spec/fixtures/ohno.js +1 -0
- data/spec/fixtures/reset.css +1 -0
- data/spec/fixtures/screen.css +2 -0
- data/spec/fixtures/styles.html +10 -0
- data/spec/fixtures/zecoolwebsite/css/awesomebydesign.css +94 -0
- data/spec/fixtures/zecoolwebsite/css/reset.css +190 -0
- data/spec/fixtures/zecoolwebsite/img/bg-idevice.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/bg.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/bottom-left-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/bottom-right-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/consulting-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/design-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/prototyping-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/top-left-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/top-right-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/img/webdev-arrow.png +0 -0
- data/spec/fixtures/zecoolwebsite/index.html +87 -0
- data/spec/fixtures/zecoolwebsite/js/awesomebydesign.js +103 -0
- data/spec/fixtures/zecoolwebsite/js/jquery-1.4.2.min.js +154 -0
- data/spec/fixtures/zecoolwebsite/js/modernizr-1.5.min.js +28 -0
- data/spec/fixtures/zecoolwebsite/js/sayhi.js +1 -0
- data/spec/integration/integration_spec.rb +54 -0
- data/spec/pagespeed_spec.rb +101 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/store/disk_spec.rb +34 -0
- data/spec/store/memcached_spec.rb +29 -0
- metadata +344 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
capybara (0.4.0)
|
5
|
+
celerity (>= 0.7.9)
|
6
|
+
culerity (>= 0.2.4)
|
7
|
+
mime-types (>= 1.16)
|
8
|
+
nokogiri (>= 1.3.3)
|
9
|
+
rack (>= 1.0.0)
|
10
|
+
rack-test (>= 0.5.4)
|
11
|
+
selenium-webdriver (>= 0.0.27)
|
12
|
+
xpath (~> 0.1.2)
|
13
|
+
celerity (0.8.5)
|
14
|
+
childprocess (0.1.4)
|
15
|
+
ffi (~> 0.6.3)
|
16
|
+
culerity (0.2.12)
|
17
|
+
diff-lcs (1.1.2)
|
18
|
+
ffi (0.6.3)
|
19
|
+
rake (>= 0.8.7)
|
20
|
+
jsmin (1.0.1)
|
21
|
+
json_pure (1.4.6)
|
22
|
+
memcached (1.0.2)
|
23
|
+
mime-types (1.16)
|
24
|
+
nokogiri (1.4.4)
|
25
|
+
rack (1.2.1)
|
26
|
+
rack-test (0.5.6)
|
27
|
+
rack (>= 1.0)
|
28
|
+
rake (0.8.7)
|
29
|
+
rspec (2.3.0)
|
30
|
+
rspec-core (~> 2.3.0)
|
31
|
+
rspec-expectations (~> 2.3.0)
|
32
|
+
rspec-mocks (~> 2.3.0)
|
33
|
+
rspec-core (2.3.0)
|
34
|
+
rspec-expectations (2.3.0)
|
35
|
+
diff-lcs (~> 1.1.2)
|
36
|
+
rspec-mocks (2.3.0)
|
37
|
+
rubyzip (0.9.4)
|
38
|
+
selenium-webdriver (0.1.1)
|
39
|
+
childprocess (= 0.1.4)
|
40
|
+
ffi (~> 0.6.3)
|
41
|
+
json_pure
|
42
|
+
rubyzip
|
43
|
+
steak (1.0.0)
|
44
|
+
rspec
|
45
|
+
xpath (0.1.2)
|
46
|
+
nokogiri (~> 1.3)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
ruby
|
50
|
+
|
51
|
+
DEPENDENCIES
|
52
|
+
capybara (= 0.4.0)
|
53
|
+
jsmin (= 1.0.1)
|
54
|
+
memcached (= 1.0.2)
|
55
|
+
mime-types (= 1.16)
|
56
|
+
nokogiri (= 1.4.4)
|
57
|
+
rack (= 1.2.1)
|
58
|
+
rspec (= 2.3.0)
|
59
|
+
steak (= 1.0.0)
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# rack-pagespeed
|
2
|
+
|
3
|
+
This middleware _will_ (as in this is a work in progress) replicate possibly every feature found in Google's [modpagespeed](http://www.modpagespeed.com/).
|
4
|
+
|
5
|
+
# To do
|
6
|
+
|
7
|
+
Everything. I'm just reserving the name for now.
|
8
|
+
|
9
|
+
# Where's rack-bundle?
|
10
|
+
|
11
|
+
I kept it in a branch called `bundle`. Though I strongly recommend you _not_ to use it. You can get the same effect (better actually, considering rack-pagespeed doesn't do some dumb stuff rack-bundle used to) by activating only the equivalent features in rack-pagespeed.
|
12
|
+
|
13
|
+
# License
|
14
|
+
|
15
|
+
It's as free as sneezing. Just [give me credit](http://twitter.com/julio_ody) if you make some extraordinary out of this.
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new do |t|
|
5
|
+
t.pattern = 'spec/**/*_spec.rb'
|
6
|
+
t.rspec_opts = ['-c', '-f nested', '-r ./spec/spec_helper']
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'jeweler'
|
11
|
+
Jeweler::Tasks.new do |gem|
|
12
|
+
gem.name = "rack-pagespeed"
|
13
|
+
gem.summary = "Web page speed optimizations at the Rack level"
|
14
|
+
gem.description = "Web page speed optimizations at the Rack level"
|
15
|
+
gem.email = "julio@awesomebydesign.com"
|
16
|
+
gem.homepage = "http://github.com/juliocesar/rack-pagespeed"
|
17
|
+
gem.authors = "Julio Cesar Ody"
|
18
|
+
gem.add_dependency 'nokogiri', '1.4.4'
|
19
|
+
gem.add_dependency 'rack', '1.2.1'
|
20
|
+
gem.add_dependency 'memcached', '1.0.2'
|
21
|
+
gem.add_dependency 'mime-types', '1.16'
|
22
|
+
gem.add_dependency 'jsmin', '1.0.1'
|
23
|
+
gem.add_development_dependency 'rspec', '2.1.0'
|
24
|
+
gem.add_development_dependency 'steak', '1.0.0'
|
25
|
+
gem.add_development_dependency 'capybara', '0.4.0'
|
26
|
+
end
|
27
|
+
rescue LoadError
|
28
|
+
puts 'Jeweler not available. gemspec tasks OFF.'
|
29
|
+
end
|
30
|
+
|
31
|
+
namespace :spec do
|
32
|
+
desc "Runs specs on Ruby 1.8.7 and 1.9.2"
|
33
|
+
task :rubies do
|
34
|
+
system "rvm 1.8.7-p174,1.9.2 specs"
|
35
|
+
end
|
36
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/pagespeed'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'mime/types'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class PageSpeed
|
7
|
+
attr_reader :config
|
8
|
+
load "#{::File.dirname(__FILE__)}/pagespeed/filters/all.rb"
|
9
|
+
autoload :Config, 'rack/pagespeed/config'
|
10
|
+
|
11
|
+
def initialize app, options, &block
|
12
|
+
@app = app
|
13
|
+
@config = Config.new options, &block
|
14
|
+
end
|
15
|
+
|
16
|
+
def call env
|
17
|
+
if match = %r(^/rack-pagespeed-(.*)).match(env['PATH_INFO'])
|
18
|
+
respond_with match[1]
|
19
|
+
else
|
20
|
+
status, headers, @response = @app.call(env)
|
21
|
+
return [status, headers, @response] unless headers['Content-Type'] =~ /html/
|
22
|
+
body = ""; @response.each do |part| body << part end
|
23
|
+
@document = Nokogiri::HTML(body)
|
24
|
+
@config.filters.each do |filter|
|
25
|
+
filter.execute! @document
|
26
|
+
end
|
27
|
+
body = @document.to_html
|
28
|
+
headers['Content-Length'] = body.length.to_s if headers['Content-Length'] # still UTF-8 unsafe
|
29
|
+
[status, headers, [body]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_with asset_id
|
34
|
+
store = @config.store
|
35
|
+
if asset = store[asset_id]
|
36
|
+
[
|
37
|
+
200,
|
38
|
+
{ 'Content-Type' => (MIME::Types.type_for(asset_id).first.content_type rescue 'text/plain') },
|
39
|
+
[asset]
|
40
|
+
]
|
41
|
+
else
|
42
|
+
[404, {'Content-Type' => 'text/plain'}, ['Not found']]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class Rack::PageSpeed::Config
|
2
|
+
class NoSuchFilter < StandardError; end
|
3
|
+
class NoSuchStorageMechanism < StandardError; end
|
4
|
+
load "#{::File.dirname(__FILE__)}/store/all.rb"
|
5
|
+
|
6
|
+
attr_reader :filters, :public
|
7
|
+
|
8
|
+
def initialize options = {}, &block
|
9
|
+
@filters, @options, @public = [], options, options[:public]
|
10
|
+
raise ArgumentError, ":public needs to be a directory" unless File.directory? @public.to_s
|
11
|
+
filters_to_methods
|
12
|
+
enable_filters_from_options
|
13
|
+
enable_store_from_options
|
14
|
+
instance_eval &block if block_given?
|
15
|
+
sort_filters
|
16
|
+
end
|
17
|
+
|
18
|
+
def store type = nil, *args
|
19
|
+
return @store unless type
|
20
|
+
case type
|
21
|
+
when :disk
|
22
|
+
@store = Rack::PageSpeed::Store::Disk.new *args
|
23
|
+
when :memcached
|
24
|
+
@store = Rack::PageSpeed::Store::Memcached.new *args
|
25
|
+
when {}
|
26
|
+
@store = {} # simple in-memory store
|
27
|
+
when Hash
|
28
|
+
store *type.to_a.first
|
29
|
+
else
|
30
|
+
raise NoSuchStorageMechanism, "No such storage mechanism: #{type}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing filter
|
35
|
+
raise NoSuchFilter, "No such filter \"#{filter}\". Available filters: #{(Rack::PageSpeed::Filters::Base.available_filters).join(', ')}"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def sort_filters
|
40
|
+
@filters = @filters.sort_by do |filter|
|
41
|
+
filter.class.priority || 0
|
42
|
+
end.reverse
|
43
|
+
end
|
44
|
+
|
45
|
+
def enable_store_from_options
|
46
|
+
return false unless @options[:store]
|
47
|
+
case @options[:store]
|
48
|
+
when Symbol then store @options[:store]
|
49
|
+
when Array then store *@options[:store]
|
50
|
+
when Hash
|
51
|
+
@options[:store] == {} ? store({}) : store(*@options[:store].to_a.first)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def enable_filters_from_options
|
56
|
+
return false unless @options[:filters]
|
57
|
+
case @options[:filters]
|
58
|
+
when Array then @options[:filters].map { |filter| self.send filter }
|
59
|
+
when Hash then @options[:filters].each { |filter, options| self.send filter, options }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def filters_to_methods
|
64
|
+
Rack::PageSpeed::Filters::Base.available_filters.each do |klass|
|
65
|
+
(class << self; self; end).send :define_method, klass.name do |*options|
|
66
|
+
default_options = {:public => @options[:public], :store => @store}
|
67
|
+
instance = klass.new(options.any? ? default_options.merge(*options) : default_options)
|
68
|
+
@filters << instance if instance and !@filters.select { |k| k.is_a? instance.class }.any?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
lib = File.join(File.dirname(__FILE__), '..')
|
2
|
+
require "#{lib}/filters/base.rb"
|
3
|
+
require "#{lib}/filters/inline_javascripts.rb"
|
4
|
+
require "#{lib}/filters/inline_css.rb"
|
5
|
+
require "#{lib}/filters/combine_javascripts.rb"
|
6
|
+
require "#{lib}/filters/combine_css.rb"
|
7
|
+
require "#{lib}/filters/minify_javascripts.rb"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rack::PageSpeed::Filters
|
2
|
+
class Base
|
3
|
+
attr_reader :document, :options
|
4
|
+
@@subclasses = []
|
5
|
+
|
6
|
+
def initialize options = {}
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def inherited klass
|
12
|
+
@@subclasses << klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def available_filters
|
16
|
+
@@subclasses
|
17
|
+
end
|
18
|
+
|
19
|
+
def requires_store
|
20
|
+
instance_eval do
|
21
|
+
def new options = {}
|
22
|
+
options[:store] ? super(options) : false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name _name = nil
|
28
|
+
_name ? @name = _name : @name ||= underscore(to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def priority _number = nil
|
32
|
+
_number ? @priority = _number.to_i : @priority
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def underscore word
|
37
|
+
word.split('::').last.
|
38
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
39
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
40
|
+
tr("-", "_").
|
41
|
+
downcase
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def file_for node
|
47
|
+
path = ::File.join(options[:public], node[node.name == 'script' ? 'src' : 'href'])
|
48
|
+
::File.open path if ::File.exists? path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
# shortcut
|
52
|
+
Rack::PageSpeed::Filter = Base
|
53
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
begin
|
2
|
+
require 'md5'
|
3
|
+
rescue LoadError
|
4
|
+
require 'digest/md5'
|
5
|
+
end
|
6
|
+
|
7
|
+
class Rack::PageSpeed::Filters::CombineCSS < Rack::PageSpeed::Filters::Base
|
8
|
+
requires_store
|
9
|
+
priority 9
|
10
|
+
|
11
|
+
def execute! document
|
12
|
+
nodes = document.css('link[rel="stylesheet"][href$=".css"]:not([href^="http"])')
|
13
|
+
return false unless nodes.count > 0
|
14
|
+
groups = group_siblings topmost_of_sequence(nodes)
|
15
|
+
groups.each do |group|
|
16
|
+
save group
|
17
|
+
merged = merge group, document
|
18
|
+
group.first.before merged
|
19
|
+
group.map { |node| node.remove }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def merge_contents nodes, separator = ';'
|
25
|
+
nodes.map { |node| file_for(node).read rescue "" }.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
def save nodes
|
29
|
+
contents = merge_contents nodes
|
30
|
+
nodes_id = unique_id nodes
|
31
|
+
@options[:store]["#{nodes_id}.css"] = contents
|
32
|
+
end
|
33
|
+
|
34
|
+
def local_css? node
|
35
|
+
node.name == 'link' && !(node['href'] =~ /^http/ or !(node['href'] =~ /.css$/))
|
36
|
+
end
|
37
|
+
|
38
|
+
def topmost_of_sequence nodes
|
39
|
+
result = []
|
40
|
+
nodes.each do |node|
|
41
|
+
_previous, _next = node.previous_sibling, node.next_sibling
|
42
|
+
if _previous && local_css?(_previous) &&
|
43
|
+
(!_next || !local_css?(_next))
|
44
|
+
result << node
|
45
|
+
end
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge nodes, document
|
51
|
+
nodes_id = unique_id nodes
|
52
|
+
node = Nokogiri::XML::Node.new 'link', document
|
53
|
+
node['rel'] = 'stylesheet'
|
54
|
+
node['href'] = "/rack-pagespeed-#{nodes_id}.css"
|
55
|
+
node
|
56
|
+
end
|
57
|
+
|
58
|
+
def unique_id nodes
|
59
|
+
Digest::MD5.hexdigest nodes.map { |node| file = file_for node; file.mtime.to_i.to_s + file.read }.join
|
60
|
+
end
|
61
|
+
|
62
|
+
def group_siblings nodes
|
63
|
+
nodes.inject([]) do |result, node|
|
64
|
+
group, current = [], node
|
65
|
+
group << node
|
66
|
+
while previous = current.previous_sibling and local_css?(previous)
|
67
|
+
current = previous
|
68
|
+
group.unshift current
|
69
|
+
end
|
70
|
+
result << group
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
begin
|
2
|
+
require 'md5'
|
3
|
+
rescue LoadError
|
4
|
+
require 'digest/md5'
|
5
|
+
end
|
6
|
+
|
7
|
+
class Rack::PageSpeed::Filters::CombineJavaScripts < Rack::PageSpeed::Filters::Base
|
8
|
+
requires_store
|
9
|
+
name 'combine_javascripts'
|
10
|
+
priority 2
|
11
|
+
|
12
|
+
def execute! document
|
13
|
+
nodes = document.css('script[src$=".js"]:not([src^="http"])')
|
14
|
+
return false unless nodes.count > 0
|
15
|
+
groups = group_siblings topmost_of_sequence(nodes)
|
16
|
+
groups.each do |group|
|
17
|
+
save group
|
18
|
+
merged = merge group, document
|
19
|
+
group.first.before merged
|
20
|
+
group.map { |node| node.remove }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def save nodes
|
26
|
+
contents = nodes.map { |node| file_for(node).read rescue "" }.join(';')
|
27
|
+
nodes_id = unique_id nodes
|
28
|
+
@options[:store]["#{nodes_id}.js"] = contents
|
29
|
+
end
|
30
|
+
|
31
|
+
def merge nodes, document
|
32
|
+
nodes_id = unique_id nodes
|
33
|
+
node = Nokogiri::XML::Node.new 'script', document
|
34
|
+
node['src'] = "/rack-pagespeed-#{nodes_id}.js"
|
35
|
+
node
|
36
|
+
end
|
37
|
+
|
38
|
+
def local_script? node
|
39
|
+
node.name == 'script' && !(node['src'] =~ /^http/ or !(node['src'] =~ /.js$/))
|
40
|
+
end
|
41
|
+
|
42
|
+
def topmost_of_sequence nodes
|
43
|
+
result = []
|
44
|
+
nodes.each do |node|
|
45
|
+
_previous, _next = node.previous_sibling, node.next_sibling
|
46
|
+
if _previous && local_script?(_previous) &&
|
47
|
+
(!_next || !local_script?(_next))
|
48
|
+
result << node
|
49
|
+
end
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def unique_id nodes
|
55
|
+
Digest::MD5.hexdigest nodes.map { |node| file = file_for node; file.mtime.to_i.to_s + file.read }.join
|
56
|
+
end
|
57
|
+
|
58
|
+
def group_siblings nodes
|
59
|
+
nodes.inject([]) do |result, node|
|
60
|
+
group, current = [], node
|
61
|
+
group << node
|
62
|
+
while previous = current.previous_sibling and local_script?(previous)
|
63
|
+
current = previous
|
64
|
+
group.unshift current
|
65
|
+
end
|
66
|
+
result << group
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|