Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

js minimizer

some refactoring, code cleanup

git-svn-id: http://bundle-fu.googlecode.com/svn/trunk@50 1db77ec0-6337-0410-9320-454da9aca44f
  • Loading branch information...
commit e93ef1ce3729091f53a366f921d303d0fff40005 1 parent 561d05e
timcharper authored
View
4 environment.rb
@@ -0,0 +1,4 @@
+# load all files
+for file in ["/lib/bundle_fu.rb", "/lib/bundle_fu/js_minimizer.rb", "/lib/bundle_fu/css_url_rewriter.rb", "/lib/bundle_fu/file_list.rb"]
+ require File.expand_path(File.join(File.dirname(__FILE__), file))
+end
View
4 init.rb
@@ -1,6 +1,6 @@
# EZ Bundle
-for file in ["/lib/bundle_fu.rb", "/lib/bundle_fu/file_list.rb"]
- require File.expand_path(File.join(File.dirname(__FILE__), file))
+for file in ["/lib/bundle_fu.rb", "/lib/js_minimizer.rb", "/lib/bundle_fu/file_list.rb"]
end
+require File.expand_path(File.join(File.dirname(__FILE__), "environment.rb"))
ActionView::Base.send(:include, BundleFu::InstanceMethods)
View
68 lib/bundle_fu.rb
@@ -6,63 +6,38 @@ def init
@content_store = {}
end
- def each_read_file(filenames=[])
+ def bundle_files(filenames=[])
+ output = ""
filenames.each{ |filename|
- output = "/* -------------- #{filename} ------------- */ "
- output << "\n"
- output << (File.read(File.join(RAILS_ROOT, "public", filename)) rescue ( "/* FILE READ ERROR! */"))
+ output << "/* --------- #{filename} --------- */ "
output << "\n"
- yield filename, output
+ begin
+ content = (File.read(File.join(RAILS_ROOT, "public", filename)))
+ rescue
+ output << "/* FILE READ ERROR! */"
+ next
+ end
+
+ output << (yield(filename, content)||"")
}
+ output
end
def bundle_js_files(filenames=[], options={})
output = ""
- each_read_file(filenames) { |filename, content|
- output << content
- }
- output
- end
-
- # rewrites a relative path to an absolute path, removing excess "../" and "./"
- # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif"
- def rewrite_relative_path(source_filename, relative_url)
- relative_url = relative_url.strip
- return relative_url if relative_url.first == "/"
-
- elements = File.join("/", File.dirname(source_filename)).gsub(/\/+/, '/').split("/")
- elements += relative_url.gsub(/\/+/, '/').split("/")
-
- index = 0
- while(elements[index])
- if (elements[index]==".")
- elements.delete_at(index)
- elsif (elements[index]=="..")
- next if index==0
- index-=1
- 2.times { elements.delete_at(index)}
-
+ bundle_files(filenames) { |filename, content|
+ if options[:compress]
+ JSMinimizer.minimize_content(content)
else
- index+=1
+ content
end
- end
-
- elements * "/"
+ }
end
-
+
def bundle_css_files(filenames=[], options = {})
- output = ""
- each_read_file(filenames) { |filename, content|
- # rewrite the URL reference paths
- # url(../../../images/active_scaffold/default/add.gif);
- # url(/stylesheets/active_scaffold/default/../../../images/active_scaffold/default/add.gif);
- # url(/stylesheets/active_scaffold/../../images/active_scaffold/default/add.gif);
- # url(/stylesheets/../images/active_scaffold/default/add.gif);
- # url(/images/active_scaffold/default/add.gif);
- content.gsub!(/url *\(([^\)]+)\)/) { "url(#{rewrite_relative_path(filename, $1)})" }
- output << content
+ bundle_files(filenames) { |filename, content|
+ BundleFu::CSSUrlRewriter.rewrite_urls(filename, content)
}
- output
end
end
@@ -80,6 +55,7 @@ def bundle(options={}, &block)
:css_path => ($bundle_css_path || "/stylesheets/cache"),
:js_path => ($bundle_js_path || "/javascripts/cache"),
:name => ($bundle_default_name || "bundle"),
+ :compress => false,
:bundle_fu => ( session[:bundle_fu].nil? ? ($bundle_fu.nil? ? true : $bundle_fu) : session[:bundle_fu] )
}.merge(options)
@@ -127,7 +103,7 @@ def bundle(options={}, &block)
FileUtils.rm_f(abs_path)
else
# call bundle_css_files or bundle_js_files to bundle all files listed. output it's contents to a file
- output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames)
+ output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames, options)
File.open( abs_path, "w") {|f| f.puts output } if output
end
new_filelist.save_as(abs_filelist_path)
View
41 lib/bundle_fu/css_url_rewriter.rb
@@ -0,0 +1,41 @@
+class BundleFu::CSSUrlRewriter
+ class << self
+ # rewrites a relative path to an absolute path, removing excess "../" and "./"
+ # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif"
+ def rewrite_relative_path(source_filename, relative_url)
+ relative_url = relative_url.to_s.strip.gsub(/["']/, "")
+
+ return relative_url if relative_url.first == "/"
+
+ elements = File.join("/", File.dirname(source_filename)).gsub(/\/+/, '/').split("/")
+ elements += relative_url.gsub(/\/+/, '/').split("/")
+
+ index = 0
+ while(elements[index])
+ if (elements[index]==".")
+ elements.delete_at(index)
+ elsif (elements[index]=="..")
+ next if index==0
+ index-=1
+ 2.times { elements.delete_at(index)}
+
+ else
+ index+=1
+ end
+ end
+
+ elements * "/"
+ end
+
+ # rewrite the URL reference paths
+ # url(../../../images/active_scaffold/default/add.gif);
+ # url(/stylesheets/active_scaffold/default/../../../images/active_scaffold/default/add.gif);
+ # url(/stylesheets/active_scaffold/../../images/active_scaffold/default/add.gif);
+ # url(/stylesheets/../images/active_scaffold/default/add.gif);
+ # url('/images/active_scaffold/default/add.gif');
+ def rewrite_urls(filename, content)
+ content.gsub!(/url *\(([^\)]+)\)/) { "url(#{rewrite_relative_path(filename, $1)})" }
+ end
+
+ end
+end
View
217 lib/bundle_fu/js_minimizer.rb
@@ -0,0 +1,217 @@
+#!/usr/bin/ruby
+# jsmin.rb 2007-07-20
+# Author: Uladzislau Latynski
+# This work is a translation from C to Ruby of jsmin.c published by
+# Douglas Crockford. Permission is hereby granted to use the Ruby
+# version under the same conditions as the jsmin.c on which it is
+# based.
+#
+# /* jsmin.c
+# 2003-04-21
+#
+# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+#
+# 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 shall be used for Good, not Evil.
+#
+# 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.
+
+require 'stringio'
+
+class BundleFu::JSMinimizer
+ attr_accessor :input
+ attr_accessor :output
+
+ EOF = -1
+ @theA = ""
+ @theB = ""
+
+ # isAlphanum -- return true if the character is a letter, digit, underscore,
+ # dollar sign, or non-ASCII character
+ def isAlphanum(c)
+ return false if !c || c == EOF
+ return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
+ (c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
+ c == '\\' || c[0] > 126)
+ end
+
+ # get -- return the next character from input. Watch out for lookahead. If
+ # the character is a control character, translate it to a space or linefeed.
+ def get()
+ c = @input.getc
+ return EOF if(!c)
+ c = c.chr
+ return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
+ return "\n" if (c == "\r")
+ return " "
+ end
+
+ # Get the next character without getting it.
+ def peek()
+ lookaheadChar = @input.getc
+ @input.ungetc(lookaheadChar)
+ return lookaheadChar.chr
+ end
+
+ # mynext -- get the next character, excluding comments.
+ # peek() is used to see if a '/' is followed by a '/' or '*'.
+ def mynext()
+ c = get
+ if (c == "/")
+ if(peek == "/")
+ while(true)
+ c = get
+ if (c <= "\n")
+ return c
+ end
+ end
+ end
+ if(peek == "*")
+ get
+ while(true)
+ case get
+ when "*"
+ if (peek == "/")
+ get
+ return " "
+ end
+ when EOF
+ raise "Unterminated comment"
+ end
+ end
+ end
+ end
+ return c
+ end
+
+
+ # action -- do something! What you do is determined by the argument: 1
+ # Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
+ # (Delete A). 3 Get the next B. (Delete B). action treats a string as a
+ # single character. Wow! action recognizes a regular expression if it is
+ # preceded by ( or , or =.
+ def action(a)
+ if(a==1)
+ @output.write @theA
+ end
+ if(a==1 || a==2)
+ @theA = @theB
+ if (@theA == "\'" || @theA == "\"")
+ while (true)
+ @output.write @theA
+ @theA = get
+ break if (@theA == @theB)
+ raise "Unterminated string literal" if (@theA <= "\n")
+ if (@theA == "\\")
+ @output.write @theA
+ @theA = get
+ end
+ end
+ end
+ end
+ if(a==1 || a==2 || a==3)
+ @theB = mynext
+ if (@theB == "/" && (@theA == "(" || @theA == "," || @theA == "=" ||
+ @theA == ":" || @theA == "[" || @theA == "!" ||
+ @theA == "&" || @theA == "|" || @theA == "?" ||
+ @theA == "{" || @theA == "}" || @theA == ";" ||
+ @theA == "\n"))
+ @output.write @theA
+ @output.write @theB
+ while (true)
+ @theA = get
+ if (@theA == "/")
+ break
+ elsif (@theA == "\\")
+ @output.write @theA
+ @theA = get
+ elsif (@theA <= "\n")
+ raise "Unterminated RegExp Literal"
+ end
+ @output.write @theA
+ end
+ @theB = mynext
+ end
+ end
+ end
+
+ # jsmin -- Copy the input to the output, deleting the characters which are
+ # insignificant to JavaScript. Comments will be removed. Tabs will be
+ # replaced with spaces. Carriage returns will be replaced with linefeeds.
+ # Most spaces and linefeeds will be removed.
+ def jsmin
+ @theA = "\n"
+ action(3)
+ while (@theA != EOF)
+ case @theA
+ when " "
+ if (isAlphanum(@theB))
+ action(1)
+ else
+ action(2)
+ end
+ when "\n"
+ case (@theB)
+ when "{","[","(","+","-"
+ action(1)
+ when " "
+ action(3)
+ else
+ if (isAlphanum(@theB))
+ action(1)
+ else
+ action(2)
+ end
+ end
+ else
+ case (@theB)
+ when " "
+ if (isAlphanum(@theA))
+ action(1)
+ else
+ action(3)
+ end
+ when "\n"
+ case (@theA)
+ when "}","]",")","+","-","\"","\\", "'", '"'
+ action(1)
+ else
+ if (isAlphanum(@theA))
+ action(1)
+ else
+ action(3)
+ end
+ end
+ else
+ action(1)
+ end
+ end
+ end
+ end
+
+ def self.minimize_content(content)
+ js_minimizer = new
+ js_minimizer.input = StringIO.new(content)
+ js_minimizer.output = StringIO.new
+
+ js_minimizer.jsmin
+
+ js_minimizer.output.string
+ end
+
+end
View
13 test/fixtures/public/javascripts/js_1.js
@@ -1 +1,12 @@
-function js_1() { alert('hi')};
+function js_1() { alert('hi')};
+
+// this is a function
+function func() {
+ alert('hi')
+ return true
+}
+
+function func() {
+ alert('hi')
+ return true
+}
View
33 test/functional/bundle_fu_test.rb
@@ -5,19 +5,6 @@
# require "library_file_name"
class BundleFuTest < Test::Unit::TestCase
- @@content_include_some = <<-EOF
- <script src="/javascripts/js_1.js?1000" type="text/javascript"></script>
- <script src="/javascripts/js_2.js?1000" type="text/javascript"></script>
- <link href="/stylesheets/css_1.css?1000" media="screen" rel="Stylesheet" type="text/css" />
- <link href="/stylesheets/css_2.css?1000" media="screen" rel="Stylesheet" type="text/css" />
- EOF
-
- # the same content, slightly changed
- @@content_include_all = @@content_include_some + <<-EOF
- <script src="/javascripts/js_3.js?1000" type="text/javascript"></script>
- <link href="/stylesheets/css_3.css?1000" media="screen" rel="Stylesheet" type="text/css" />
- EOF
-
def setup
@mock_view = MockView.new
BundleFu.init # resets BundleFu
@@ -33,6 +20,23 @@ def test__bundle_js_files__should_include_js_content
assert_public_files_match("/javascripts/cache/bundle.js", "function js_1()")
end
+ def test__bundle_js_files__should_default_to_not_compressed_and_include_override_option
+ @mock_view.bundle() { @@content_include_all }
+ default_content = File.read(public_file("/javascripts/cache/bundle.js"))
+ purge_cache
+
+ @mock_view.bundle(:compress => false) { @@content_include_all }
+ uncompressed_content = File.read(public_file("/javascripts/cache/bundle.js"))
+ purge_cache
+
+ @mock_view.bundle(:compress => true) { @@content_include_all }
+ compressed_content = File.read(public_file("/javascripts/cache/bundle.js"))
+ purge_cache
+
+ assert default_content.length == uncompressed_content.length, "Should default to uncompressed"
+ assert uncompressed_content.length > compressed_content.length, "Didn't compress the content. (:compress => true) #{compressed_content.length}. (:compress => false) #{uncompressed_content.length}"
+ end
+
def test__content_remains_same__shouldnt_refresh_cache
@mock_view.bundle { @@content_include_some }
@@ -151,9 +155,6 @@ def test__bypass_param_set__should_honor_and_store_in_session
end
private
- def public_file(filename)
- File.join(::RAILS_ROOT, "public", filename)
- end
def purge_cache
# remove all fixtures named "bundle*"
View
15 test/functional/css_bundle_test.rb
@@ -2,15 +2,18 @@
class CSSBundleTest < Test::Unit::TestCase
def test__rewrite_relative_path__should_rewrite
- assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/spinner.gif"))
- assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/./../images/goober/../spinner.gif"))
- assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("stylesheets/active_scaffold/default/./stylesheet.css", "../../../images/spinner.gif"))
- assert_equal("/stylesheets/image.gif", BundleFu.rewrite_relative_path("stylesheets/main.css", "image.gif"))
- assert_equal("/stylesheets/image.gif", BundleFu.rewrite_relative_path("/stylesheets////default/main.css", "..//image.gif"))
- assert_equal("/images/image.gif", BundleFu.rewrite_relative_path("/stylesheets/default/main.css", "/images/image.gif"))
+ assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/spinner.gif"))
+ assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/./../images/goober/../spinner.gif"))
+ assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/active_scaffold/default/./stylesheet.css", "../../../images/spinner.gif"))
+ assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", "image.gif"))
+ assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", "'image.gif'"))
+ assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", " image.gif "))
+ assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets////default/main.css", "..//image.gif"))
+ assert_equal("/images/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/default/main.css", "/images/image.gif"))
end
def test__bundle_css_file__should_rewrite_relatiave_path
+# dbg
bundled_css = BundleFu.bundle_css_files(["/stylesheets/css_3.css"])
# puts bundled_css
assert_match("background-image: url(/images/background.gif)", bundled_css)
View
6 test/functional/js_bundle_test.rb
@@ -1,10 +1,12 @@
require File.join(File.dirname(__FILE__), '../test_helper.rb')
class JSBundleTest < Test::Unit::TestCase
-
+ def test__bundle_js_files__bypass_bundle__should_bypass
+ BundleFu.bundle_js_files
+ end
def test__bundle_js_files__should_include_contents
- bundled_js = BundleFu.bundle_css_files(["/javascripts/js_1.js"])
+ bundled_js = BundleFu.bundle_js_files(["/javascripts/js_1.js"])
# puts bundled_js
# function js_1
assert_match("function js_1", bundled_js)
View
12 test/functional/js_minimizer_test.rb
@@ -0,0 +1,12 @@
+require File.join(File.dirname(__FILE__), '../test_helper.rb')
+
+class BundleFu::JSMinimizerTest < Test::Unit::TestCase
+ def test_minimize_content__should_be_less
+ test_content = File.read(public_file("javascripts/js_1.js"))
+ content_size = test_content.length
+ minimized_size = BundleFu::JSMinimizer.minimize_content(test_content).length
+
+ assert(minimized_size > 0)
+ assert(content_size > minimized_size)
+ end
+end
View
3  test/run_all.rb
@@ -0,0 +1,3 @@
+Dir[File.join(File.dirname(__FILE__), "functional/*.rb")].each{|filename|
+ require filename
+}
View
21 test/test_helper.rb
@@ -2,7 +2,7 @@
require "rubygems"
require 'active_support'
-for file in ["../lib/bundle_fu.rb", "../lib/bundle_fu/file_list.rb", "../lib/bundle_fu/file_list.rb", "mock_view.rb"]
+for file in ["../environment.rb", "mock_view.rb"]
require File.expand_path(File.join(File.dirname(__FILE__), file))
end
@@ -16,4 +16,23 @@ class Object
def to_regexp
is_a?(Regexp) ? self : Regexp.new(Regexp.escape(self.to_s))
end
+end
+
+class Test::Unit::TestCase
+ @@content_include_some = <<-EOF
+ <script src="/javascripts/js_1.js?1000" type="text/javascript"></script>
+ <script src="/javascripts/js_2.js?1000" type="text/javascript"></script>
+ <link href="/stylesheets/css_1.css?1000" media="screen" rel="Stylesheet" type="text/css" />
+ <link href="/stylesheets/css_2.css?1000" media="screen" rel="Stylesheet" type="text/css" />
+ EOF
+
+ # the same content, slightly changed
+ @@content_include_all = @@content_include_some + <<-EOF
+ <script src="/javascripts/js_3.js?1000" type="text/javascript"></script>
+ <link href="/stylesheets/css_3.css?1000" media="screen" rel="Stylesheet" type="text/css" />
+ EOF
+
+ def public_file(filename)
+ File.join(::RAILS_ROOT, "public", filename)
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.