rack-pagespeed-fork 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.
- 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
|