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,16 @@
1
+ class Rack::PageSpeed::Filters::InlineCSS < Rack::PageSpeed::Filter
2
+ priority 10
3
+
4
+ def execute! document
5
+ nodes = document.css('link[rel="stylesheet"][href]')
6
+ return false unless nodes.count > 0
7
+ nodes.each do |node|
8
+ file = file_for node
9
+ next if !file or file.stat.size > (@options[:max_size] or 2048)
10
+ inline = Nokogiri::XML::Node.new 'style', document
11
+ inline.content = file.read
12
+ node.before inline
13
+ node.remove
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ class Rack::PageSpeed::Filters::InlineImages < Rack::PageSpeed::Filter
2
+ priority 8
3
+
4
+ def execute! document
5
+ nodes = document.css('img')
6
+ return false unless nodes.count > 0
7
+ nodes.each do |node|
8
+ file = file_for node
9
+ next if !file or file.stat.size > (@options[:max_size] or 1024)
10
+ img = node.clone
11
+ img['src'] = "data:#{Rack::Mime.mime_type(File.extname(file.path))};base64,#{[file.read].pack('m')}"
12
+ img['alt'] = node['alt'] if node['alt']
13
+ node.before img
14
+ node.remove
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class Rack::PageSpeed::Filters::InlineJavaScripts < Rack::PageSpeed::Filter
2
+ name 'inline_javascripts'
3
+ priority 10
4
+
5
+ def execute! document
6
+ nodes = document.css('script[src]')
7
+ return false unless nodes.count > 0
8
+ nodes.each do |node|
9
+ file = file_for node
10
+ next if !file or file.stat.size > (@options[:max_size] or 2048)
11
+ inline = Nokogiri::XML::Node.new 'script', document
12
+ inline.content = file.read
13
+ node.before inline
14
+ node.remove
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require 'jsmin'
2
+ begin
3
+ require 'md5'
4
+ rescue LoadError
5
+ require 'digest/md5'
6
+ end
7
+
8
+ class Rack::PageSpeed::Filters::MinifyJavaScripts < Rack::PageSpeed::Filters::Base
9
+ requires_store
10
+ name 'minify_javascripts'
11
+ priority 2
12
+
13
+ def execute! document
14
+ nodes = document.css('script')
15
+ return false unless nodes.count > 0
16
+ nodes.each do |node|
17
+ if !node['src']
18
+ node.content = JSMin.minify node.content
19
+ else
20
+ if match = %r(^/rack-pagespeed-(.*)).match(node['src'])
21
+ store = @options[:store]
22
+ store[match[1]] = JSMin.minify store[match[1]]
23
+ else
24
+ next unless local_script? node
25
+ file = file_for node
26
+ javascript = file.read
27
+ hash = Digest::MD5.hexdigest file.mtime.to_i.to_s + javascript
28
+ compressed = Nokogiri::XML::Node.new 'script', document
29
+ compressed['src'] = "/rack-pagespeed-#{hash}.js"
30
+ @options[:store]["#{hash}.js"] = JSMin.minify javascript
31
+ node.before compressed
32
+ node.remove
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def local_script? node
39
+ node.name == 'script' and file_for(node)
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ require 'tmpdir'
2
+
3
+ module Rack::PageSpeed::Store; end
4
+
5
+ class Rack::PageSpeed::Store::Disk
6
+ def initialize path = Dir.tmpdir
7
+ raise ArgumentError, "#{path} is not a directory" unless File.directory? path
8
+ @path = path
9
+ end
10
+
11
+ def [] key
12
+ path = "#{@path}/rack-pagespeed-#{key}"
13
+ File.read path if File.exists? path
14
+ end
15
+
16
+ def []= key, value
17
+ File.open("#{@path}/rack-pagespeed-#{key}", 'w') { |file| file << value }
18
+ true
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'dalli'
3
+ rescue LoadError
4
+ raise LoadError, ":memcached store requires the dalli gem to be installed."
5
+ end
6
+
7
+ class Rack::PageSpeed::Store::Memcached
8
+ def initialize *args
9
+ @client = Dalli::Client.new *args
10
+ # @client.stats # let it raise errors if it can't connect
11
+ end
12
+
13
+ def [] key
14
+ @client.get key
15
+ end
16
+
17
+ def []= key, value
18
+ @client.set key, value
19
+ true
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'redis'
3
+ rescue LoadError
4
+ raise LoadError, ":redis store requires the redis gem to be installed."
5
+ end
6
+
7
+ class Rack::PageSpeed::Store::Redis
8
+ def initialize *args
9
+ @client = Redis.new *args
10
+ end
11
+
12
+ def [] key
13
+ @client.get key
14
+ end
15
+
16
+ def []= key, value
17
+ @client.set key, value
18
+ end
19
+ end
@@ -0,0 +1,103 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: rack-pagespeed-fork 0.1.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "rack-pagespeed-fork"
9
+ s.version = "0.1.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Will Jordan", "Julio Cesar Ody"]
14
+ s.date = "2014-10-08"
15
+ s.description = "Web page speed optimizations at the Rack level - fork"
16
+ s.email = "will@code.org"
17
+ s.extra_rdoc_files = [
18
+ "README.md"
19
+ ]
20
+ s.files = [
21
+ "Gemfile",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "lib/rack-pagespeed.rb",
26
+ "lib/rack/pagespeed.rb",
27
+ "lib/rack/pagespeed/config.rb",
28
+ "lib/rack/pagespeed/filters/all.rb",
29
+ "lib/rack/pagespeed/filters/base.rb",
30
+ "lib/rack/pagespeed/filters/combine_css.rb",
31
+ "lib/rack/pagespeed/filters/combine_javascripts.rb",
32
+ "lib/rack/pagespeed/filters/inline_css.rb",
33
+ "lib/rack/pagespeed/filters/inline_images.rb",
34
+ "lib/rack/pagespeed/filters/inline_javascripts.rb",
35
+ "lib/rack/pagespeed/filters/minify_javascripts.rb",
36
+ "lib/rack/pagespeed/store/disk.rb",
37
+ "lib/rack/pagespeed/store/memcached.rb",
38
+ "lib/rack/pagespeed/store/redis.rb",
39
+ "rack-pagespeed-fork.gemspec",
40
+ "spec/config_spec.rb",
41
+ "spec/filters/combine_css_spec.rb",
42
+ "spec/filters/combine_javascripts_spec.rb",
43
+ "spec/filters/filter_spec.rb",
44
+ "spec/filters/inline_css_spec.rb",
45
+ "spec/filters/inline_images_spec.rb",
46
+ "spec/filters/inline_javascript_spec.rb",
47
+ "spec/filters/minify_javascript_spec.rb",
48
+ "spec/fixtures/all-small-dog-breeds.jpg",
49
+ "spec/fixtures/complex.html",
50
+ "spec/fixtures/foo.js",
51
+ "spec/fixtures/hh-reset.css",
52
+ "spec/fixtures/huge.css",
53
+ "spec/fixtures/iphone.css",
54
+ "spec/fixtures/jquery-1.4.1.min.js",
55
+ "spec/fixtures/medialess1.css",
56
+ "spec/fixtures/medialess2.css",
57
+ "spec/fixtures/mock_store.rb",
58
+ "spec/fixtures/mylib.js",
59
+ "spec/fixtures/noexternalcss.html",
60
+ "spec/fixtures/noscripts.html",
61
+ "spec/fixtures/ohno.js",
62
+ "spec/fixtures/reset.css",
63
+ "spec/fixtures/screen.css",
64
+ "spec/fixtures/styles.html",
65
+ "spec/pagespeed_spec.rb",
66
+ "spec/spec_helper.rb",
67
+ "spec/store/disk_spec.rb",
68
+ "spec/store/memcached_spec.rb",
69
+ "spec/store/redis_spec.rb"
70
+ ]
71
+ s.homepage = "http://github.com/wjordan/rack-pagespeed"
72
+ s.licenses = ["MIT"]
73
+ s.rubygems_version = "2.4.2"
74
+ s.summary = "Web page speed optimizations at the Rack level - fork"
75
+
76
+ if s.respond_to? :specification_version then
77
+ s.specification_version = 4
78
+
79
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
80
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
81
+ s.add_runtime_dependency(%q<jsmin>, [">= 0"])
82
+ s.add_runtime_dependency(%q<mime-types>, [">= 0"])
83
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
84
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
85
+ s.add_development_dependency(%q<rspec>, ["= 2.6.0"])
86
+ else
87
+ s.add_dependency(%q<nokogiri>, [">= 0"])
88
+ s.add_dependency(%q<jsmin>, [">= 0"])
89
+ s.add_dependency(%q<mime-types>, [">= 0"])
90
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
91
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
92
+ s.add_dependency(%q<rspec>, ["= 2.6.0"])
93
+ end
94
+ else
95
+ s.add_dependency(%q<nokogiri>, [">= 0"])
96
+ s.add_dependency(%q<jsmin>, [">= 0"])
97
+ s.add_dependency(%q<mime-types>, [">= 0"])
98
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
99
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
100
+ s.add_dependency(%q<rspec>, ["= 2.6.0"])
101
+ end
102
+ end
103
+
@@ -0,0 +1,110 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'rack-pagespeed configuration' do
4
+ before do
5
+ class StripsNaked < Rack::PageSpeed::Filter; end
6
+ class MakesItLookGood < Rack::PageSpeed::Filter; end
7
+ end
8
+
9
+ context 'when instancing a new object' do
10
+ it 'creates methods for each filter class found in Rack::PageSpeed::Filters::Base.available_filters' do
11
+ Rack::PageSpeed::Config.new(:public => Dir.tmpdir).should respond_to :strips_naked
12
+ end
13
+
14
+ it "requires a :public parameter pointing to the app's public directory" do
15
+ expect { Rack::PageSpeed::Config.new("foo" => "bar") }.to raise_error(ArgumentError)
16
+ end
17
+ end
18
+
19
+ context 'sorts filter execution based on their specified order' do
20
+ before do
21
+ class Larry < Rack::PageSpeed::Filter; priority 3; end
22
+ class Moe < Rack::PageSpeed::Filter; priority 2; end
23
+ class Curly < Rack::PageSpeed::Filter; priority 1; end
24
+ @config = Rack::PageSpeed::Config.new :public => Fixtures.path do
25
+ curly
26
+ larry
27
+ moe
28
+ end
29
+ end
30
+
31
+ it "Larry is first" do
32
+ @config.filters.first.should be_a Larry
33
+ end
34
+
35
+ it "Moe is second" do
36
+ @config.filters[1].should be_a Moe
37
+ end
38
+
39
+ it "Curly is last" do
40
+ @config.filters.last.should be_a Curly
41
+ end
42
+ end
43
+
44
+ context 'enabling filters, options hash based' do
45
+ before { File.stub(:directory?).and_return(true) }
46
+
47
+ it "if it's an array, it enables filters listed in it with their default options" do
48
+ config = Rack::PageSpeed::Config.new :filters => [:makes_it_look_good]
49
+ config.filters.first.should be_a MakesItLookGood
50
+ end
51
+
52
+ it "if it's a hash, it let's you pass options to the filters" do
53
+ config = Rack::PageSpeed::Config.new :filters => {:makes_it_look_good => {:test => 6000}}
54
+ filter = config.filters.first
55
+ filter.should be_a MakesItLookGood
56
+ filter.options[:test].should == 6000 # yeah, 2 assertions, bad, bad
57
+ end
58
+
59
+ it 'raises a NoSuchFilter error when a non-existing filter is passed to :filters' do
60
+ expect { Rack::PageSpeed::Config.new :filters => [:whoops!] }.to raise_error(Rack::PageSpeed::Config::NoSuchFilter)
61
+ end
62
+ end
63
+
64
+ context 'enabling filters, block/DSL based' do
65
+ before { File.stub(:directory?).and_return(true) }
66
+
67
+ it "let's you invoke filter names in a DSL-like fashion through a block" do
68
+ config = Rack::PageSpeed::Config.new do
69
+ makes_it_look_good
70
+ strips_naked
71
+ end
72
+ config.filters.should include_an_instance_of MakesItLookGood
73
+ config.filters.should include_an_instance_of StripsNaked
74
+ end
75
+
76
+ # two specs below actually work with the optiosn hash based context too
77
+ it "won't add the same filter twice" do
78
+ config = Rack::PageSpeed::Config.new do
79
+ makes_it_look_good
80
+ makes_it_look_good
81
+ strips_naked
82
+ end
83
+ config.filters.count.should == 2
84
+ end
85
+
86
+ it "calling a filter that requires storage without specifying one raises an error" do
87
+ class NeedsStore < Rack::PageSpeed::Filter
88
+ requires_store
89
+ end
90
+ expect { Rack::PageSpeed::Config.new do needs_store end }.to raise_error
91
+ end
92
+
93
+ it 'raises a NoSuchFilter error when a non-existing filter is called ' do
94
+ expect { Rack::PageSpeed::Config.new do whateva :foo => 'bar' end }.to raise_error(Rack::PageSpeed::Config::NoSuchFilter)
95
+ end
96
+ end
97
+
98
+ context 'setting a storage mechanism' do
99
+ before { File.stub(:directory?).and_return(true) }
100
+
101
+ it 'loads the appropriate class on request' do
102
+ ::File.stub(:join).and_return(File.dirname(__FILE__) + '/fixtures/mock_store.rb')
103
+ expect {
104
+ Rack::PageSpeed::Config.new do
105
+ store :mock
106
+ end
107
+ }.to change { Rack::PageSpeed::Store.const_defined?('Mock') }.from(false).to(true)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'the combine_css filter' do
4
+ it "is called \"combine_css\" as far as Config is concerned" do
5
+ Rack::PageSpeed::Filters::CombineCSS.name.should == 'combine_css'
6
+ end
7
+
8
+ it "requires a store mechanism to be passed via :store when initializing" do
9
+ expect { Rack::PageSpeed::Filters::CombineCSS.new }.to raise_error
10
+ end
11
+
12
+ it "is a priority 9 filter" do
13
+ Rack::PageSpeed::Filters::CombineCSS.priority.should == 9
14
+ end
15
+
16
+ context 'execute!' do
17
+ before :each do
18
+ @filter = Rack::PageSpeed::Filters::CombineCSS.new :public => Fixtures.path, :store => {}
19
+ end
20
+
21
+ it 'cuts down the number of scripts in the fixtures from 5 to 1' do
22
+ expect { @filter.execute! Fixtures.complex }.to change { Fixtures.complex.css('link[rel="stylesheet"][href$=".css"]:not([href^="http"])').count }.from(5).to(1)
23
+ end
24
+
25
+ it "stores the nodes' contents in the store passed through the initializer" do
26
+ @filter.instance_variable_get(:@options)[:store].should_receive(:[]=)
27
+ @filter.execute! Fixtures.complex
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'the combine_javascripts filter' do
4
+ it "is called \"combine_javascripts\" as far as Config is concerned" do
5
+ Rack::PageSpeed::Filters::CombineJavaScripts.name.should == 'combine_javascripts'
6
+ end
7
+
8
+ it "is a priority 9 filter" do
9
+ Rack::PageSpeed::Filters::CombineJavaScripts.priority 9
10
+ end
11
+
12
+ context "requires a store mechanism to be passed via :store when initializing" do
13
+ specify { expect { Rack::PageSpeed::Filters::CombineJavaScripts.new }.to raise_error }
14
+ specify { expect { Rack::PageSpeed::Filters::CombineJavaScripts.new(:store => {}) }.to_not raise_error }
15
+ end
16
+
17
+ context 'execute!' do
18
+ before :each do
19
+ @filter = Rack::PageSpeed::Filters::CombineJavaScripts.new :public => Fixtures.path, :store => {}
20
+ end
21
+
22
+ it 'cuts down the number of scripts in the fixtures from 4 to 2' do
23
+ expect { @filter.execute! Fixtures.complex }.to change { Fixtures.complex.css('script[src$=".js"]:not([src^="http"])').count }.from(4).to(2)
24
+ end
25
+
26
+ it "stores the nodes' contents in the store passed through the initializer" do
27
+ @filter.instance_variable_get(:@options)[:store].should_receive(:[]=).twice
28
+ @filter.execute! Fixtures.complex
29
+ end
30
+ end
31
+
32
+ context 'yes, I test private methods, so what?' do
33
+ before do
34
+ @filter = Rack::PageSpeed::Filters::CombineJavaScripts.new :public => Fixtures.path, :store => {}
35
+ end
36
+
37
+ it 'returns an array of arrays containing JS nodes that are next to each other in #group_siblings' do
38
+ nodes = Fixtures.complex.css('script[src$=".js"]:not([src^="http"]) + script[src$=".js"]:not([src^="http"])')
39
+ result = @filter.send :group_siblings, nodes
40
+ result.should == [[nodes[0].previous_element, nodes[0]], [nodes[1].previous_element, nodes[1]]]
41
+ end
42
+
43
+ context 'the #unique_id method' do
44
+ it "generates a hash thats unique to the nodes' combination of content + mtime in the absence of @options[:hash]" do
45
+ nodes = Fixtures.complex.css('script[src$=".js"]:not([src^="http"]) + script[src$=".js"]:not([src^="http"])')
46
+ @filter.send(:unique_id, nodes).should == Digest::MD5.hexdigest(nodes.map { |node| file = @filter.send(:file_for, node); file.mtime.to_i.to_s + file.read }.join)
47
+ end
48
+
49
+ it "uses values for hashes estipulated in @options[:hash] if any" do
50
+ nodes = []
51
+ nodes << Fixtures.complex.at_css('script[src="ohno.js"]')
52
+ nodes << Fixtures.complex.at_css('script[src="foo.js"]')
53
+ filter = Rack::PageSpeed::Filters::CombineJavaScripts.new :store => {}, :hash => { %W(ohno.js foo.js) => "romanking" }
54
+ filter.send(:unique_id, nodes).should == "romanking"
55
+ end
56
+ end
57
+ end
58
+ end