rack-pagespeed 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 (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