rack-pagespeed 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/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
|