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.
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