rack-pagespeed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +59 -0
  3. data/README.md +15 -0
  4. data/Rakefile +36 -0
  5. data/VERSION +1 -0
  6. data/lib/rack-pagespeed.rb +1 -0
  7. data/lib/rack/pagespeed.rb +46 -0
  8. data/lib/rack/pagespeed/config.rb +72 -0
  9. data/lib/rack/pagespeed/filters/all.rb +7 -0
  10. data/lib/rack/pagespeed/filters/base.rb +53 -0
  11. data/lib/rack/pagespeed/filters/combine_css.rb +73 -0
  12. data/lib/rack/pagespeed/filters/combine_javascripts.rb +69 -0
  13. data/lib/rack/pagespeed/filters/inline_css.rb +16 -0
  14. data/lib/rack/pagespeed/filters/inline_javascripts.rb +17 -0
  15. data/lib/rack/pagespeed/filters/minify_javascripts.rb +41 -0
  16. data/lib/rack/pagespeed/store/all.rb +6 -0
  17. data/lib/rack/pagespeed/store/disk.rb +18 -0
  18. data/lib/rack/pagespeed/store/memcached.rb +17 -0
  19. data/spec/config_spec.rb +187 -0
  20. data/spec/filters/combine_css_spec.rb +30 -0
  21. data/spec/filters/combine_javascripts_spec.rb +48 -0
  22. data/spec/filters/filter_spec.rb +57 -0
  23. data/spec/filters/inline_css_spec.rb +33 -0
  24. data/spec/filters/inline_javascript_spec.rb +30 -0
  25. data/spec/filters/minify_javascript_spec.rb +59 -0
  26. data/spec/fixtures/complex.html +33 -0
  27. data/spec/fixtures/foo.js +1 -0
  28. data/spec/fixtures/hh-reset.css +1 -0
  29. data/spec/fixtures/huge.css +1176 -0
  30. data/spec/fixtures/iphone.css +1 -0
  31. data/spec/fixtures/jquery-1.4.1.min.js +152 -0
  32. data/spec/fixtures/medialess1.css +2 -0
  33. data/spec/fixtures/medialess2.css +2 -0
  34. data/spec/fixtures/mylib.js +3 -0
  35. data/spec/fixtures/noexternalcss.html +11 -0
  36. data/spec/fixtures/noscripts.html +9 -0
  37. data/spec/fixtures/ohno.js +1 -0
  38. data/spec/fixtures/reset.css +1 -0
  39. data/spec/fixtures/screen.css +2 -0
  40. data/spec/fixtures/styles.html +10 -0
  41. data/spec/fixtures/zecoolwebsite/css/awesomebydesign.css +94 -0
  42. data/spec/fixtures/zecoolwebsite/css/reset.css +190 -0
  43. data/spec/fixtures/zecoolwebsite/img/bg-idevice.png +0 -0
  44. data/spec/fixtures/zecoolwebsite/img/bg.png +0 -0
  45. data/spec/fixtures/zecoolwebsite/img/bottom-left-arrow.png +0 -0
  46. data/spec/fixtures/zecoolwebsite/img/bottom-right-arrow.png +0 -0
  47. data/spec/fixtures/zecoolwebsite/img/consulting-arrow.png +0 -0
  48. data/spec/fixtures/zecoolwebsite/img/design-arrow.png +0 -0
  49. data/spec/fixtures/zecoolwebsite/img/prototyping-arrow.png +0 -0
  50. data/spec/fixtures/zecoolwebsite/img/top-left-arrow.png +0 -0
  51. data/spec/fixtures/zecoolwebsite/img/top-right-arrow.png +0 -0
  52. data/spec/fixtures/zecoolwebsite/img/webdev-arrow.png +0 -0
  53. data/spec/fixtures/zecoolwebsite/index.html +87 -0
  54. data/spec/fixtures/zecoolwebsite/js/awesomebydesign.js +103 -0
  55. data/spec/fixtures/zecoolwebsite/js/jquery-1.4.2.min.js +154 -0
  56. data/spec/fixtures/zecoolwebsite/js/modernizr-1.5.min.js +28 -0
  57. data/spec/fixtures/zecoolwebsite/js/sayhi.js +1 -0
  58. data/spec/integration/integration_spec.rb +54 -0
  59. data/spec/pagespeed_spec.rb +101 -0
  60. data/spec/spec_helper.rb +54 -0
  61. data/spec/store/disk_spec.rb +34 -0
  62. data/spec/store/memcached_spec.rb +29 -0
  63. metadata +344 -0
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source :rubygems
2
+
3
+ gem 'nokogiri', '1.4.4'
4
+ gem 'rack', '1.2.1'
5
+ gem 'memcached', '1.0.2'
6
+ gem 'mime-types', '1.16'
7
+ gem 'jsmin', '1.0.1'
8
+
9
+ group :test do
10
+ gem 'rspec', '2.3.0'
11
+ gem 'steak', '1.0.0'
12
+ gem 'capybara', '0.4.0'
13
+ end
@@ -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)
@@ -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.
@@ -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