rack-pagespeed-fork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +20 -0
  3. data/README.md +3 -0
  4. data/Rakefile +41 -0
  5. data/VERSION +1 -0
  6. data/lib/rack-pagespeed.rb +1 -0
  7. data/lib/rack/pagespeed.rb +50 -0
  8. data/lib/rack/pagespeed/config.rb +79 -0
  9. data/lib/rack/pagespeed/filters/all.rb +8 -0
  10. data/lib/rack/pagespeed/filters/base.rb +64 -0
  11. data/lib/rack/pagespeed/filters/combine_css.rb +81 -0
  12. data/lib/rack/pagespeed/filters/combine_javascripts.rb +77 -0
  13. data/lib/rack/pagespeed/filters/inline_css.rb +16 -0
  14. data/lib/rack/pagespeed/filters/inline_images.rb +17 -0
  15. data/lib/rack/pagespeed/filters/inline_javascripts.rb +17 -0
  16. data/lib/rack/pagespeed/filters/minify_javascripts.rb +41 -0
  17. data/lib/rack/pagespeed/store/disk.rb +20 -0
  18. data/lib/rack/pagespeed/store/memcached.rb +21 -0
  19. data/lib/rack/pagespeed/store/redis.rb +19 -0
  20. data/rack-pagespeed-fork.gemspec +103 -0
  21. data/spec/config_spec.rb +110 -0
  22. data/spec/filters/combine_css_spec.rb +30 -0
  23. data/spec/filters/combine_javascripts_spec.rb +58 -0
  24. data/spec/filters/filter_spec.rb +62 -0
  25. data/spec/filters/inline_css_spec.rb +33 -0
  26. data/spec/filters/inline_images_spec.rb +28 -0
  27. data/spec/filters/inline_javascript_spec.rb +30 -0
  28. data/spec/filters/minify_javascript_spec.rb +59 -0
  29. data/spec/fixtures/all-small-dog-breeds.jpg +0 -0
  30. data/spec/fixtures/complex.html +33 -0
  31. data/spec/fixtures/foo.js +1 -0
  32. data/spec/fixtures/hh-reset.css +1 -0
  33. data/spec/fixtures/huge.css +1176 -0
  34. data/spec/fixtures/iphone.css +1 -0
  35. data/spec/fixtures/jquery-1.4.1.min.js +152 -0
  36. data/spec/fixtures/medialess1.css +2 -0
  37. data/spec/fixtures/medialess2.css +2 -0
  38. data/spec/fixtures/mock_store.rb +2 -0
  39. data/spec/fixtures/mylib.js +3 -0
  40. data/spec/fixtures/noexternalcss.html +11 -0
  41. data/spec/fixtures/noscripts.html +9 -0
  42. data/spec/fixtures/ohno.js +1 -0
  43. data/spec/fixtures/reset.css +1 -0
  44. data/spec/fixtures/screen.css +2 -0
  45. data/spec/fixtures/styles.html +10 -0
  46. data/spec/pagespeed_spec.rb +106 -0
  47. data/spec/spec_helper.rb +60 -0
  48. data/spec/store/disk_spec.rb +34 -0
  49. data/spec/store/memcached_spec.rb +23 -0
  50. data/spec/store/redis_spec.rb +22 -0
  51. metadata +178 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ # rack-pagespeed-fork
2
+
3
+ This is a fork of [rack-pagespeed](https://github.com/juliocesar/rack-pagespeed).
@@ -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