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