Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit e6e76aeec35fedf004964e2fdacaef222aebda3b Daniel Mendler committed Dec 9, 2009
Showing with 334 additions and 0 deletions.
  1. +22 −0 LICENSE
  2. +9 −0 Manifest.txt
  3. +16 −0 README.md
  4. +18 −0 Rakefile
  5. +144 −0 lib/rack/embed.rb
  6. +36 −0 rack-embed.gemspec
  7. +1 −0 test/test.image
  8. +88 −0 test/test_rack_embed.rb
22 LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2009 Daniel Mendler
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
9 Manifest.txt
@@ -0,0 +1,9 @@
+LICENSE
+Rakefile
+test/test_rack_embed.rb
+test/test.image
+rack-embed.gemspec
+Manifest.txt
+lib/rack/embed.rb
+README.md
+
16 README.md
@@ -0,0 +1,16 @@
+# Rack::Embed
+
+Rack::Embed embeds small images via the data-url (base64) if the browser supports it.
+This reduces http traffic.
+
+# Installation
+
+ gem sources -a http://gemcutter.org
+ gem install rack-embed
+
+# Usage
+
+Add the following line to your rackup file:
+
+ use Rack::Embed, :max_size => 1024
+
18 Rakefile
@@ -0,0 +1,18 @@
+require 'hoe'
+
+namespace('notes') do
+ task('todo') do; system('ack TODO'); end
+ task('fixme') do; system('ack FIXME'); end
+ task('hack') do; system('ack HACK'); end
+ task('warning') do; system('ack WARNING'); end
+ task('important') do; system('ack IMPORTANT'); end
+end
+
+desc 'Show annotations'
+task('notes' => %w(notes:todo notes:fixme notes:hack notes:warning notes:important))
+
+Hoe.spec 'rack-esi' do
+ self.version = '0.0.1'
+ developer 'Daniel Mendler', 'mail@daniel-mendler.de'
+end
+
144 lib/rack/embed.rb
@@ -0,0 +1,144 @@
+require 'rack/utils'
+require 'cgi'
+
+module Rack
+ class Embed
+ def initialize(app, opts = {})
+ @app = app
+ @max_size = opts[:max_size] || 1024
+ @mime_types = opts[:mime_types] || %w(text/css application/xhtml+xml text/html)
+ end
+
+ def call(env)
+ ua = env['HTTP_USER_AGENT']
+
+ # Replace this with something more sophisticated
+ # Supported according to http://en.wikipedia.org/wiki/Data_URI_scheme
+ if !ua || ua !~ /WebKit|Gecko|Opera|Konqueror|MSIE 8.0/
+ return @app.call(env)
+ end
+
+ original_env = env.clone
+ response = @app.call(env)
+ return response if !applies_to?(response)
+
+ status, header, body = response
+ body = css?(header) ? css_embed_images(body.first, original_env) : html_embed_images(body.first, original_env)
+ header['Content-Length'] = Rack::Utils.bytesize(body).to_s
+
+ [status, header, [body]]
+ rescue Exception => ex
+ env['rack.errors'].write("#{ex.message}\n") if env['rack.errors']
+ [500, {}, ex.message]
+ end
+
+ private
+
+ def unescape(url)
+ CGI.unescapeHTML(url)
+ end
+
+ def escape(url)
+ CGI.escapeHTML(url)
+ end
+
+ def css_embed_images(body, env)
+ body.gsub!(/url\(([^\)]+)\)/) do
+ "url(#{escape get_image(env, unescape($1))})"
+ end
+ body
+ end
+
+ def html_embed_images(body, env)
+ body.gsub!(/(<img[^>]+src=)("[^"]+"|'[^']+')/) do
+ img = $1
+ src = unescape($2)
+ "#{img}#{src[0..0]}#{escape(get_image(env, src[1..-2]))}#{src[-1..-1]}"
+ end
+ body
+ end
+
+ def get_image(env, src)
+ return src if src =~ %r{^\w+://|^data:}
+ begin
+ if src[0..0] != '/'
+ path = env['PATH_INFO']
+ i = path.rindex('/')
+ src = path[0..i] + src
+ end
+
+ uri = env['REQUEST_URI'] || env['PATH_INFO']
+ i = uri.index('?')
+ uri = src + (i ? uri[i..-1] : '')
+
+ i = src.index('?')
+ if i
+ path = src[0...i]
+ query = env['QUERY_STRING'] || ''
+ query += (query.empty? ? '' : '&') + src[i+1..-1]
+ else
+ path = src
+ query = env['QUERY_STRING']
+ end
+
+ inclusion_env = env.merge('PATH_INFO' => path,
+ 'REQUEST_PATH' => path,
+ 'REQUEST_URI' => uri,
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => query)
+ inclusion_env.delete('rack.request')
+ status, header, body = @app.call(inclusion_env)
+ type = content_type(header)
+
+ return src if status != 200 || !type
+
+ return src if body.respond_to?(:to_path) && ::File.size(body.to_path) > @max_size
+
+ return src if body.respond_to?(:path) && ::File.size(body.path) > @max_size
+
+ body = join_body(body)
+ return src if Rack::Utils.bytesize(body) > @max_size
+
+ body = [body].pack('m')
+ body.gsub!("\n", '')
+ "data:#{type};base64,#{body}"
+ rescue
+ src
+ end
+ end
+
+ def applies_to?(response)
+ status, header, body = response
+
+ # Some stati don't have to be processed
+ return false if [301, 302, 303, 307].include?(status)
+
+ # Check mime type
+ return false if !@mime_types.include?(content_type(header))
+
+ response[2] = [body = join_body(body)]
+
+ # Something to embed?
+ if css? header
+ body.include?('url(')
+ else
+ body =~ /<img[^>]+src=/
+ end
+ end
+
+ def content_type(header)
+ header['Content-Type'] && header['Content-Type'].split(';').first.strip
+ end
+
+ def css?(header)
+ content_type(header) == 'text/css'
+ end
+
+ # Join response body
+ def join_body(body)
+ parts = ''
+ body.each { |part| parts << part }
+ parts
+ end
+ end
+end
36 rack-embed.gemspec
@@ -0,0 +1,36 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{rack-embed}
+ s.version = "0.0.1"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Daniel Mendler"]
+ s.date = %q{2009-03-04}
+ s.email = ["mail@daniel-mendler.de"]
+ s.extra_rdoc_files = ["README.md", "LICENSE"]
+ s.files = %w(LICENSE Rakefile test/test_rack_embed.rb test/test.image rack-embed.gemspec Manifest.txt lib/rack/embed.rb README.md)
+ s.has_rdoc = true
+ s.rdoc_options = ["--main", "README.txt"]
+ s.require_paths = ["lib"]
+ s.rubyforge_project = %q{rack-embed}
+ s.rubygems_version = %q{1.3.1}
+ s.homepage = %q{http://github.com/minad/rack-embed}
+
+ s.summary = 'Rack::Embed embeds small images via the data-url (base64) if the browser supports it.'
+ s.test_files = ["test/test_rack_embed.rb"]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 2
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_development_dependency(%q<hoe>, [">= 1.8.3"])
+ else
+ s.add_dependency(%q<hoe>, [">= 1.8.3"])
+ end
+ else
+ s.add_dependency(%q<hoe>, [">= 1.8.3"])
+ end
+end
+
1 test/test.image
@@ -0,0 +1 @@
+This is a test image
88 test/test_rack_embed.rb
@@ -0,0 +1,88 @@
+require 'test/unit'
+require 'rack/urlmap'
+
+path = File.expand_path(File.dirname(__FILE__))
+$: << path << File.join(path, 'lib')
+
+require 'rack/embed'
+
+class TestRackEmbed < Test::Unit::TestCase
+ def test_response_passthrough
+ mock_app = const([200, {}, ['Hei!']])
+ esi_app = Rack::Embed.new(mock_app)
+
+ assert_same_response(mock_app, esi_app)
+ end
+
+ def test_respect_for_content_type
+ mock_app = const([200, {'Content-Type' => 'application/x-y-z'}, ['Blabla']])
+ esi_app = Rack::Embed.new(mock_app)
+
+ assert_same_response(mock_app, esi_app)
+ end
+
+ def test_html
+ app = Rack::URLMap.new({
+ '/' => const([200, {'Content-Type' => 'text/html'}, ['<img src="/image"/>']]),
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
+ })
+
+ esi_app = Rack::Embed.new(app)
+ assert_equal ['<img src="data:image/png;base64,aW1hZ2VfZGF0YQ=="/>'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
+ end
+
+ def test_css
+ app = Rack::URLMap.new({
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
+ })
+
+ esi_app = Rack::Embed.new(app)
+ assert_equal ['background: url(data:image/png;base64,aW1hZ2VfZGF0YQ==)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
+ end
+
+ def test_too_large
+ app = Rack::URLMap.new({
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['bla' * 1024]])
+ })
+
+ esi_app = Rack::Embed.new(app)
+ assert_equal ['background: url(/image)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
+ end
+
+ def test_file
+ app = Rack::URLMap.new({
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
+ '/image' => const([200, {'Content-Type' => 'image/png'}, File.open('test/test.image')])
+ })
+
+ esi_app = Rack::Embed.new(app)
+ assert_equal ['background: url(data:image/png;base64,VGhpcyBpcyBhIHRlc3QgaW1hZ2U=)'],
+ esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'WebKit')[2]
+ end
+
+ def test_invalid_browser
+ app = Rack::URLMap.new({
+ '/' => const([200, {'Content-Type' => 'text/css'}, ['background: url(/image)']]),
+ '/image' => const([200, {'Content-Type' => 'image/png'}, ['image_data']])
+ })
+
+ esi_app = Rack::Embed.new(app)
+ assert_equal ['background: url(/image)'], esi_app.call('SCRIPT_NAME' => '', 'PATH_INFO' => '/', 'HTTP_USER_AGENT' => 'MSIE')[2]
+ end
+
+ private
+
+ def const(value)
+ lambda { |*_| value }
+ end
+
+ def assert_same_response(a, b)
+ x = a.call({'HTTP_USER_AGENT' => 'WebKit'})
+ y = b.call({'HTTP_USER_AGENT' => 'WebKit'})
+
+ assert_equal(x, y)
+ assert_equal(x.object_id, y.object_id)
+ end
+end

0 comments on commit e6e76ae

Please sign in to comment.
Something went wrong with that request. Please try again.