rack-embed 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 Daniel Mendler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ Rakefile
3
+ test/test_rack_embed.rb
4
+ test/test.image
5
+ rack-embed.gemspec
6
+ Manifest.txt
7
+ lib/rack/embed.rb
8
+ README.md
9
+
@@ -0,0 +1,16 @@
1
+ # Rack::Embed
2
+
3
+ Rack::Embed embeds small images via the data-url (base64) if the browser supports it.
4
+ This reduces http traffic.
5
+
6
+ # Installation
7
+
8
+ gem sources -a http://gemcutter.org
9
+ gem install rack-embed
10
+
11
+ # Usage
12
+
13
+ Add the following line to your rackup file:
14
+
15
+ use Rack::Embed, :max_size => 1024
16
+
@@ -0,0 +1,18 @@
1
+ require 'hoe'
2
+
3
+ namespace('notes') do
4
+ task('todo') do; system('ack TODO'); end
5
+ task('fixme') do; system('ack FIXME'); end
6
+ task('hack') do; system('ack HACK'); end
7
+ task('warning') do; system('ack WARNING'); end
8
+ task('important') do; system('ack IMPORTANT'); end
9
+ end
10
+
11
+ desc 'Show annotations'
12
+ task('notes' => %w(notes:todo notes:fixme notes:hack notes:warning notes:important))
13
+
14
+ Hoe.spec 'rack-esi' do
15
+ self.version = '0.0.1'
16
+ developer 'Daniel Mendler', 'mail@daniel-mendler.de'
17
+ end
18
+
@@ -0,0 +1,152 @@
1
+ require 'rack/utils'
2
+ require 'cgi'
3
+
4
+ module Rack
5
+ class Embed
6
+ def initialize(app, opts = {})
7
+ @app = app
8
+ @max_size = opts[:max_size] || 1024
9
+ @mime_types = opts[:mime_types] || %w(text/css application/xhtml+xml text/html)
10
+ @threaded = opts[:threaded] || false
11
+ @timeout = opts[:timeout] || 0.5
12
+ end
13
+
14
+ def call(env)
15
+ ua = env['HTTP_USER_AGENT']
16
+
17
+ # Replace this with something more sophisticated
18
+ # Supported according to http://en.wikipedia.org/wiki/Data_URI_scheme
19
+ if !ua || ua !~ /WebKit|Gecko|Opera|Konqueror|MSIE 8.0/
20
+ return @app.call(env)
21
+ end
22
+
23
+ original_env = env.clone
24
+ response = @app.call(env)
25
+ return response if !applies_to?(response)
26
+
27
+ status, header, body = response
28
+ body = css?(header) ? css_embed_images(body.first, original_env) : html_embed_images(body.first, original_env)
29
+ header['Content-Length'] = Rack::Utils.bytesize(body).to_s
30
+
31
+ [status, header, [body]]
32
+ rescue Exception => ex
33
+ env['rack.errors'].write("#{ex.message}\n") if env['rack.errors']
34
+ [500, {}, ex.message]
35
+ end
36
+
37
+ private
38
+
39
+ def unescape(url)
40
+ CGI.unescapeHTML(url)
41
+ end
42
+
43
+ def escape(url)
44
+ CGI.escapeHTML(url)
45
+ end
46
+
47
+ def css_embed_images(body, env)
48
+ body.gsub!(/url\(([^\)]+)\)/) do
49
+ "url(#{escape get_image(env, unescape($1))})"
50
+ end
51
+ body
52
+ end
53
+
54
+ def html_embed_images(body, env)
55
+ body.gsub!(/(<img[^>]+src=)("[^"]+"|'[^']+')/) do
56
+ img = $1
57
+ src = unescape($2)
58
+ "#{img}#{src[0..0]}#{escape(get_image(env, src[1..-2]))}#{src[-1..-1]}"
59
+ end
60
+ body
61
+ end
62
+
63
+ def get_image(env, src)
64
+ return src if src =~ %r{^\w+://|^data:}
65
+ path = src.dup
66
+ begin
67
+ if path[0..0] != '/'
68
+ base = env['PATH_INFO']
69
+ i = base.rindex('/')
70
+ path = base[0..i] + path
71
+ end
72
+
73
+ query = ''
74
+ i = path.index('?')
75
+ if i
76
+ query = path[i+1..-1]
77
+ path = path[0...i]
78
+ end
79
+
80
+ uri = query && !query.empty? ? "#{path}?#{query}" : path
81
+
82
+ inclusion_env = env.merge('PATH_INFO' => path,
83
+ 'REQUEST_PATH' => path,
84
+ 'REQUEST_URI' => uri,
85
+ 'REQUEST_METHOD' => 'GET',
86
+ 'QUERY_STRING' => query)
87
+ inclusion_env.delete('rack.request')
88
+
89
+ status, header, body = if @threaded
90
+ app = @app
91
+ result = nil
92
+ thread = Thread.new { result = app.call(inclusion_env) }
93
+ return src if !thread.join(@timeout)
94
+ result
95
+ else
96
+ @app.call(inclusion_env)
97
+ end
98
+
99
+ type = content_type(header)
100
+
101
+ return src if status != 200 || !type
102
+
103
+ return src if body.respond_to?(:to_path) && ::File.size(body.to_path) > @max_size
104
+
105
+ return src if body.respond_to?(:path) && ::File.size(body.path) > @max_size
106
+
107
+ body = join_body(body)
108
+ return src if Rack::Utils.bytesize(body) > @max_size
109
+
110
+ body = [body].pack('m')
111
+ body.gsub!("\n", '')
112
+ "data:#{type};base64,#{body}"
113
+ rescue => ex
114
+ src
115
+ end
116
+ end
117
+
118
+ def applies_to?(response)
119
+ status, header, body = response
120
+
121
+ # Some stati don't have to be processed
122
+ return false if [301, 302, 303, 307].include?(status)
123
+
124
+ # Check mime type
125
+ return false if !@mime_types.include?(content_type(header))
126
+
127
+ response[2] = [body = join_body(body)]
128
+
129
+ # Something to embed?
130
+ if css? header
131
+ body.include?('url(')
132
+ else
133
+ body =~ /<img[^>]+src=/
134
+ end
135
+ end
136
+
137
+ def content_type(header)
138
+ header['Content-Type'] && header['Content-Type'].split(';').first.strip
139
+ end
140
+
141
+ def css?(header)
142
+ content_type(header) == 'text/css'
143
+ end
144
+
145
+ # Join response body
146
+ def join_body(body)
147
+ parts = ''
148
+ body.each { |part| parts << part }
149
+ parts
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{rack-embed}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Daniel Mendler"]
9
+ s.date = %q{2009-03-04}
10
+ s.email = ["mail@daniel-mendler.de"]
11
+ s.extra_rdoc_files = ["README.md", "LICENSE"]
12
+ s.files = %w(LICENSE Rakefile test/test_rack_embed.rb test/test.image rack-embed.gemspec Manifest.txt lib/rack/embed.rb README.md)
13
+ s.has_rdoc = true
14
+ s.rdoc_options = ["--main", "README.txt"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{rack-embed}
17
+ s.rubygems_version = %q{1.3.1}
18
+ s.homepage = %q{http://github.com/minad/rack-embed}
19
+
20
+ s.summary = 'Rack::Embed embeds small images via the data-url (base64) if the browser supports it.'
21
+ s.test_files = ["test/test_rack_embed.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_development_dependency(%q<hoe>, [">= 1.8.3"])
29
+ else
30
+ s.add_dependency(%q<hoe>, [">= 1.8.3"])
31
+ end
32
+ else
33
+ s.add_dependency(%q<hoe>, [">= 1.8.3"])
34
+ end
35
+ end
36
+
@@ -0,0 +1 @@
1
+ This is a test image
@@ -0,0 +1,108 @@
1
+ require 'test/unit'
2
+ require 'rack/urlmap'
3
+
4
+ path = File.expand_path(File.dirname(__FILE__))
5
+ $: << path << File.join(path, 'lib')
6
+
7
+ require 'rack/embed'
8
+
9
+ class TestRackEmbed < Test::Unit::TestCase
10
+ def test_response_passthrough
11
+ mock_app = const([200, {}, ['Hei!']])
12
+ esi_app = Rack::Embed.new(mock_app)
13
+
14
+ assert_same_response(mock_app, esi_app)
15
+ end
16
+
17
+ def test_respect_for_content_type
18
+ mock_app = const([200, {'Content-Type' => 'application/x-y-z'}, ['Blabla']])
19
+ esi_app = Rack::Embed.new(mock_app)
20
+
21
+ assert_same_response(mock_app, esi_app)
22
+ end
23
+
24
+ def test_html
25
+ app = Rack::URLMap.new({
26
+ '/' => const([200, {'Content-Type' => 'text/html'}, ['<img src="/image"/>']]),
27
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
28
+ })
29
+
30
+ esi_app = Rack::Embed.new(app)
31
+ assert_equal ['<img src="data:image/png;base64,aW1hZ2VfZGF0YQ=="/>'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
32
+ end
33
+
34
+ def test_css
35
+ app = Rack::URLMap.new({
36
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
37
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
38
+ })
39
+
40
+ esi_app = Rack::Embed.new(app)
41
+ assert_equal ['background: url(data:image/png;base64,aW1hZ2VfZGF0YQ==)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
42
+ end
43
+
44
+ def test_threaded
45
+ app = Rack::URLMap.new({
46
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
47
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
48
+ })
49
+
50
+ esi_app = Rack::Embed.new(app, :threaded => true)
51
+ assert_equal ['background: url(data:image/png;base64,aW1hZ2VfZGF0YQ==)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
52
+ end
53
+
54
+ def test_threaded_timeout
55
+ app = Rack::URLMap.new({
56
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
57
+ '/image' => proc { sleep 2 }
58
+ })
59
+
60
+ esi_app = Rack::Embed.new(app, :threaded => true, :timeout => 1)
61
+ assert_equal ['background: url(/image)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
62
+ end
63
+
64
+ def test_too_large
65
+ app = Rack::URLMap.new({
66
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
67
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['bla' * 1024]])
68
+ })
69
+
70
+ esi_app = Rack::Embed.new(app)
71
+ assert_equal ['background: url(/image)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
72
+ end
73
+
74
+ def test_file
75
+ app = Rack::URLMap.new({
76
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
77
+ '/image' => const([200, {'Content-Type' => 'image/png'}, File.open('test/test.image')])
78
+ })
79
+
80
+ esi_app = Rack::Embed.new(app)
81
+ assert_equal ['background: url(data:image/png;base64,VGhpcyBpcyBhIHRlc3QgaW1hZ2U=)'],
82
+ esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
83
+ end
84
+
85
+ def test_invalid_browser
86
+ app = Rack::URLMap.new({
87
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
88
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
89
+ })
90
+
91
+ esi_app = Rack::Embed.new(app)
92
+ assert_equal ['background: url(/image)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'MSIE')[2]
93
+ end
94
+
95
+ private
96
+
97
+ def const(value)
98
+ lambda { |*_| value }
99
+ end
100
+
101
+ def assert_same_response(a, b)
102
+ x = a.call({'HTTP_USER_AGENT' => 'WebKit'})
103
+ y = b.call({'HTTP_USER_AGENT' => 'WebKit'})
104
+
105
+ assert_equal(x, y)
106
+ assert_equal(x.object_id, y.object_id)
107
+ end
108
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-embed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Mendler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-04 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.3
24
+ version:
25
+ description:
26
+ email:
27
+ - mail@daniel-mendler.de
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - README.md
34
+ - LICENSE
35
+ files:
36
+ - LICENSE
37
+ - Rakefile
38
+ - test/test_rack_embed.rb
39
+ - test/test.image
40
+ - rack-embed.gemspec
41
+ - Manifest.txt
42
+ - lib/rack/embed.rb
43
+ - README.md
44
+ has_rdoc: true
45
+ homepage: http://github.com/minad/rack-embed
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --main
49
+ - README.txt
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project: rack-embed
67
+ rubygems_version: 1.3.1
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: Rack::Embed embeds small images via the data-url (base64) if the browser supports it.
71
+ test_files:
72
+ - test/test_rack_embed.rb