rack-pagespeed-fork 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +20 -0
- data/README.md +3 -0
- data/Rakefile +41 -0
- data/VERSION +1 -0
- data/lib/rack-pagespeed.rb +1 -0
- data/lib/rack/pagespeed.rb +50 -0
- data/lib/rack/pagespeed/config.rb +79 -0
- data/lib/rack/pagespeed/filters/all.rb +8 -0
- data/lib/rack/pagespeed/filters/base.rb +64 -0
- data/lib/rack/pagespeed/filters/combine_css.rb +81 -0
- data/lib/rack/pagespeed/filters/combine_javascripts.rb +77 -0
- data/lib/rack/pagespeed/filters/inline_css.rb +16 -0
- data/lib/rack/pagespeed/filters/inline_images.rb +17 -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/disk.rb +20 -0
- data/lib/rack/pagespeed/store/memcached.rb +21 -0
- data/lib/rack/pagespeed/store/redis.rb +19 -0
- data/rack-pagespeed-fork.gemspec +103 -0
- data/spec/config_spec.rb +110 -0
- data/spec/filters/combine_css_spec.rb +30 -0
- data/spec/filters/combine_javascripts_spec.rb +58 -0
- data/spec/filters/filter_spec.rb +62 -0
- data/spec/filters/inline_css_spec.rb +33 -0
- data/spec/filters/inline_images_spec.rb +28 -0
- data/spec/filters/inline_javascript_spec.rb +30 -0
- data/spec/filters/minify_javascript_spec.rb +59 -0
- data/spec/fixtures/all-small-dog-breeds.jpg +0 -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/mock_store.rb +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/pagespeed_spec.rb +106 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/store/disk_spec.rb +34 -0
- data/spec/store/memcached_spec.rb +23 -0
- data/spec/store/redis_spec.rb +22 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 583d2f74f36b8f3f550ee2abeaf37459d51509cf
|
4
|
+
data.tar.gz: 093d507aede980e1871c6883fcda57a8ce46c5ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1aa719697358b8fe33516a2d6565b8099c9b28994a9bba69c8337fd03ee8ea4ab2d76c76717826f7cd55e62dc73919b45006a757f11464e59d3b12fe017c049a
|
7
|
+
data.tar.gz: 8d5a2a15f7523c740b3020d2172b8bad881d8edecb308bf3b7f9c1caf6f43358ccdf68c04d39ef81a155322c665c989769884cc2c2faa2f560626515d2ea330a
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem "bundler", "~> 1.0"
|
5
|
+
gem "jeweler", "~> 2.0.1"
|
6
|
+
end
|
7
|
+
|
8
|
+
gem 'nokogiri'
|
9
|
+
gem 'jsmin'
|
10
|
+
gem 'mime-types'
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'capybara', '1.0.0'
|
14
|
+
gem 'redis'
|
15
|
+
# gem 'memcached'
|
16
|
+
end
|
17
|
+
|
18
|
+
group :development, :test do
|
19
|
+
gem 'rspec', '2.6.0'
|
20
|
+
end
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
+
gem.name = "rack-pagespeed-fork"
|
18
|
+
gem.homepage = "http://github.com/wjordan/rack-pagespeed"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = "Web page speed optimizations at the Rack level - fork"
|
21
|
+
gem.description = "Web page speed optimizations at the Rack level - fork"
|
22
|
+
gem.email = "will@code.org"
|
23
|
+
gem.authors = ["Will Jordan", "Julio Cesar Ody"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new do |t|
|
30
|
+
t.pattern = 'spec/**/*_spec.rb'
|
31
|
+
t.rspec_opts = ['-c', '-f nested', '-r ./spec/spec_helper']
|
32
|
+
end
|
33
|
+
|
34
|
+
namespace :spec do
|
35
|
+
desc "Runs specs on Ruby 1.8.7 and 1.9.2"
|
36
|
+
task :rubies do
|
37
|
+
system "rvm 1.8.7-p174,1.9.2 specs"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/pagespeed'
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class PageSpeed
|
6
|
+
attr_reader :config
|
7
|
+
load "#{::File.dirname(__FILE__)}/pagespeed/filters/all.rb"
|
8
|
+
autoload :Config, 'rack/pagespeed/config'
|
9
|
+
|
10
|
+
def initialize app, options, &block
|
11
|
+
@app = app
|
12
|
+
@config = Config.new options, &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def call env
|
16
|
+
if match = %r(^/rack-pagespeed-(.*)).match(env['PATH_INFO'])
|
17
|
+
respond_with match[1]
|
18
|
+
else
|
19
|
+
status, headers, @response = @app.call(env)
|
20
|
+
return [status, headers, @response] unless headers['Content-Type'] =~ /html/
|
21
|
+
body = ""; @response.each do |part| body << part end
|
22
|
+
@document = Nokogiri::HTML(body)
|
23
|
+
@config.filters.each do |filter|
|
24
|
+
filter.execute! @document
|
25
|
+
end
|
26
|
+
body = @document.to_html
|
27
|
+
headers['Content-Length'] = body.length.to_s if headers['Content-Length'] # still UTF-8 unsafe
|
28
|
+
[status, headers, [body]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_with asset_id
|
33
|
+
store = @config.store
|
34
|
+
if asset = store[asset_id]
|
35
|
+
[
|
36
|
+
200,
|
37
|
+
{
|
38
|
+
'Content-Length' => asset.length,
|
39
|
+
'Content-Type' => (Rack::Mime.mime_type(::File.extname(asset_id))),
|
40
|
+
'Cache-Control' => "public, max-age=#{(60*60*24*365.25*10).to_i}",
|
41
|
+
'Expires' => (Time.now + 60*60*24*365.25*10).httpdate
|
42
|
+
},
|
43
|
+
[asset]
|
44
|
+
]
|
45
|
+
else
|
46
|
+
[404, {'Content-Type' => 'text/plain'}, ['Not found']]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Rack::PageSpeed::Config
|
2
|
+
class NoSuchFilter < StandardError; end
|
3
|
+
class NoSuchStorageMechanism < StandardError; end
|
4
|
+
load "#{::File.dirname(__FILE__)}/store/disk.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 {}
|
22
|
+
@store = {} # simple in-memory store
|
23
|
+
when Symbol
|
24
|
+
@store = load_storage type, *args
|
25
|
+
when Hash
|
26
|
+
store *type.to_a.first
|
27
|
+
else
|
28
|
+
raise NoSuchStorageMechanism, "No such storage mechanism: #{type}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing filter, *args
|
33
|
+
raise NoSuchFilter, "No such filter \"#{filter}\". Available filters: #{(Rack::PageSpeed::Filter.available_filters).join(', ')}"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def sort_filters
|
38
|
+
@filters = @filters.sort_by do |filter|
|
39
|
+
filter.class.priority || 0
|
40
|
+
end.reverse
|
41
|
+
end
|
42
|
+
|
43
|
+
def enable_store_from_options
|
44
|
+
return false unless @options[:store]
|
45
|
+
case @options[:store]
|
46
|
+
when Symbol then store @options[:store]
|
47
|
+
when Array then store *@options[:store]
|
48
|
+
when Hash
|
49
|
+
@options[:store] == {} ? store({}) : store(*@options[:store].to_a.first)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def enable_filters_from_options
|
54
|
+
return false unless @options[:filters]
|
55
|
+
case @options[:filters]
|
56
|
+
when Array then @options[:filters].map { |filter| self.send filter }
|
57
|
+
when Hash then @options[:filters].each { |filter, options| self.send filter, options }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def filters_to_methods
|
62
|
+
Rack::PageSpeed::Filter.available_filters.each do |klass|
|
63
|
+
(class << self; self; end).send :define_method, klass.name do |*options|
|
64
|
+
default_options = {:public => @options[:public], :store => @store}
|
65
|
+
instance = klass.new(options.any? ? default_options.merge(*options) : default_options)
|
66
|
+
@filters << instance if instance and !@filters.select { |k| k.is_a? instance.class }.any?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_storage type, *args
|
72
|
+
klass = type.to_s.capitalize
|
73
|
+
unless Rack::PageSpeed::Store.const_defined? klass
|
74
|
+
lib = ::File.join(::File.dirname(__FILE__), 'store', type.to_s)
|
75
|
+
require lib
|
76
|
+
end
|
77
|
+
Rack::PageSpeed::Store.const_get(klass).new *args
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,8 @@
|
|
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"
|
8
|
+
require "#{lib}/filters/inline_images.rb"
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Rack::PageSpeed::Filters
|
4
|
+
class Base
|
5
|
+
attr_reader :document, :options
|
6
|
+
@@subclasses = []
|
7
|
+
|
8
|
+
def initialize options = {}
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def inherited klass
|
14
|
+
@@subclasses << klass
|
15
|
+
end
|
16
|
+
|
17
|
+
def available_filters
|
18
|
+
@@subclasses
|
19
|
+
end
|
20
|
+
|
21
|
+
def requires_store
|
22
|
+
instance_eval do
|
23
|
+
def new options = {}
|
24
|
+
options[:store] ? super(options) : raise("#{name} requires :store to be specified.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def name _name = nil
|
30
|
+
_name ? @name = _name : @name ||= underscore(to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
def priority _number = nil
|
34
|
+
_number ? @priority = _number.to_i : @priority
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def underscore word
|
39
|
+
word.split('::').last.
|
40
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
41
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
42
|
+
tr("-", "_").
|
43
|
+
downcase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def file_for node
|
49
|
+
path = case node.name
|
50
|
+
when 'script'
|
51
|
+
node['src']
|
52
|
+
when 'img'
|
53
|
+
node['src']
|
54
|
+
when 'link'
|
55
|
+
node['href']
|
56
|
+
end
|
57
|
+
return false unless path
|
58
|
+
path = ::File.join(options[:public], URI.parse(path).path)
|
59
|
+
::File.open path if ::File.exists? path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# shortcut
|
63
|
+
Rack::PageSpeed::Filter = Base
|
64
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
begin
|
2
|
+
require 'md5'
|
3
|
+
rescue LoadError
|
4
|
+
require 'digest/md5'
|
5
|
+
end
|
6
|
+
|
7
|
+
class Rack::PageSpeed::Filters::CombineCSS < Rack::PageSpeed::Filter
|
8
|
+
requires_store
|
9
|
+
priority 9
|
10
|
+
|
11
|
+
def execute! document
|
12
|
+
nodes = document.css('link[rel="stylesheet"][href]')
|
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' and file_for(node)
|
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
|
+
return Digest::MD5.hexdigest nodes.map { |node|
|
60
|
+
file = file_for node
|
61
|
+
next unless file
|
62
|
+
file.mtime.to_i.to_s + file.read
|
63
|
+
}.join unless @options[:hash]
|
64
|
+
@options[:hash].each do |urls, hash|
|
65
|
+
next unless (nodes.map { |node| node['href'] } & urls).length == urls.length
|
66
|
+
return hash
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def group_siblings nodes
|
71
|
+
nodes.inject([]) do |result, node|
|
72
|
+
group, current = [], node
|
73
|
+
group << node
|
74
|
+
while previous = current.previous_sibling and local_css?(previous)
|
75
|
+
current = previous
|
76
|
+
group.unshift current
|
77
|
+
end
|
78
|
+
result << group
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
begin
|
2
|
+
require 'md5'
|
3
|
+
rescue LoadError
|
4
|
+
require 'digest/md5'
|
5
|
+
end
|
6
|
+
|
7
|
+
class Rack::PageSpeed::Filters::CombineJavaScripts < Rack::PageSpeed::Filter
|
8
|
+
requires_store
|
9
|
+
name 'combine_javascripts'
|
10
|
+
priority 8
|
11
|
+
|
12
|
+
def execute! document
|
13
|
+
nodes = document.css('script[src]')
|
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' and file_for(node)
|
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 || !file_for(_next))
|
48
|
+
result << node
|
49
|
+
end
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def unique_id nodes
|
55
|
+
return Digest::MD5.hexdigest nodes.map { |node|
|
56
|
+
file = file_for node
|
57
|
+
next unless file
|
58
|
+
file.mtime.to_i.to_s + file.read
|
59
|
+
}.join unless @options[:hash]
|
60
|
+
@options[:hash].each do |urls, hash|
|
61
|
+
next unless (nodes.map { |node| node['src'] } & urls).length == urls.length
|
62
|
+
return hash
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def group_siblings nodes
|
67
|
+
nodes.inject([]) do |result, node|
|
68
|
+
group, current = [], node
|
69
|
+
group << node
|
70
|
+
while previous = current.previous_sibling and local_script?(previous)
|
71
|
+
current = previous
|
72
|
+
group.unshift current
|
73
|
+
end
|
74
|
+
result << group
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|