Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
4 environment.rb
... ... @@ -0,0 +1,4 @@
  1 +# load all files
  2 +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"]
  3 + require File.expand_path(File.join(File.dirname(__FILE__), file))
  4 +end
4 init.rb
... ... @@ -1,6 +1,6 @@
1 1 # EZ Bundle
2   -for file in ["/lib/bundle_fu.rb", "/lib/bundle_fu/file_list.rb"]
3   - require File.expand_path(File.join(File.dirname(__FILE__), file))
  2 +for file in ["/lib/bundle_fu.rb", "/lib/js_minimizer.rb", "/lib/bundle_fu/file_list.rb"]
4 3 end
  4 +require File.expand_path(File.join(File.dirname(__FILE__), "environment.rb"))
5 5
6 6 ActionView::Base.send(:include, BundleFu::InstanceMethods)
68 lib/bundle_fu.rb
@@ -6,63 +6,38 @@ def init
6 6 @content_store = {}
7 7 end
8 8
9   - def each_read_file(filenames=[])
  9 + def bundle_files(filenames=[])
  10 + output = ""
10 11 filenames.each{ |filename|
11   - output = "/* -------------- #{filename} ------------- */ "
12   - output << "\n"
13   - output << (File.read(File.join(RAILS_ROOT, "public", filename)) rescue ( "/* FILE READ ERROR! */"))
  12 + output << "/* --------- #{filename} --------- */ "
14 13 output << "\n"
15   - yield filename, output
  14 + begin
  15 + content = (File.read(File.join(RAILS_ROOT, "public", filename)))
  16 + rescue
  17 + output << "/* FILE READ ERROR! */"
  18 + next
  19 + end
  20 +
  21 + output << (yield(filename, content)||"")
16 22 }
  23 + output
17 24 end
18 25
19 26 def bundle_js_files(filenames=[], options={})
20 27 output = ""
21   - each_read_file(filenames) { |filename, content|
22   - output << content
23   - }
24   - output
25   - end
26   -
27   - # rewrites a relative path to an absolute path, removing excess "../" and "./"
28   - # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif"
29   - def rewrite_relative_path(source_filename, relative_url)
30   - relative_url = relative_url.strip
31   - return relative_url if relative_url.first == "/"
32   -
33   - elements = File.join("/", File.dirname(source_filename)).gsub(/\/+/, '/').split("/")
34   - elements += relative_url.gsub(/\/+/, '/').split("/")
35   -
36   - index = 0
37   - while(elements[index])
38   - if (elements[index]==".")
39   - elements.delete_at(index)
40   - elsif (elements[index]=="..")
41   - next if index==0
42   - index-=1
43   - 2.times { elements.delete_at(index)}
44   -
  28 + bundle_files(filenames) { |filename, content|
  29 + if options[:compress]
  30 + JSMinimizer.minimize_content(content)
45 31 else
46   - index+=1
  32 + content
47 33 end
48   - end
49   -
50   - elements * "/"
  34 + }
51 35 end
52   -
  36 +
53 37 def bundle_css_files(filenames=[], options = {})
54   - output = ""
55   - each_read_file(filenames) { |filename, content|
56   - # rewrite the URL reference paths
57   - # url(../../../images/active_scaffold/default/add.gif);
58   - # url(/stylesheets/active_scaffold/default/../../../images/active_scaffold/default/add.gif);
59   - # url(/stylesheets/active_scaffold/../../images/active_scaffold/default/add.gif);
60   - # url(/stylesheets/../images/active_scaffold/default/add.gif);
61   - # url(/images/active_scaffold/default/add.gif);
62   - content.gsub!(/url *\(([^\)]+)\)/) { "url(#{rewrite_relative_path(filename, $1)})" }
63   - output << content
  38 + bundle_files(filenames) { |filename, content|
  39 + BundleFu::CSSUrlRewriter.rewrite_urls(filename, content)
64 40 }
65   - output
66 41 end
67 42 end
68 43
@@ -80,6 +55,7 @@ def bundle(options={}, &block)
80 55 :css_path => ($bundle_css_path || "/stylesheets/cache"),
81 56 :js_path => ($bundle_js_path || "/javascripts/cache"),
82 57 :name => ($bundle_default_name || "bundle"),
  58 + :compress => false,
83 59 :bundle_fu => ( session[:bundle_fu].nil? ? ($bundle_fu.nil? ? true : $bundle_fu) : session[:bundle_fu] )
84 60 }.merge(options)
85 61
@@ -127,7 +103,7 @@ def bundle(options={}, &block)
127 103 FileUtils.rm_f(abs_path)
128 104 else
129 105 # call bundle_css_files or bundle_js_files to bundle all files listed. output it's contents to a file
130   - output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames)
  106 + output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames, options)
131 107 File.open( abs_path, "w") {|f| f.puts output } if output
132 108 end
133 109 new_filelist.save_as(abs_filelist_path)
41 lib/bundle_fu/css_url_rewriter.rb
... ... @@ -0,0 +1,41 @@
  1 +class BundleFu::CSSUrlRewriter
  2 + class << self
  3 + # rewrites a relative path to an absolute path, removing excess "../" and "./"
  4 + # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif"
  5 + def rewrite_relative_path(source_filename, relative_url)
  6 + relative_url = relative_url.to_s.strip.gsub(/["']/, "")
  7 +
  8 + return relative_url if relative_url.first == "/"
  9 +
  10 + elements = File.join("/", File.dirname(source_filename)).gsub(/\/+/, '/').split("/")
  11 + elements += relative_url.gsub(/\/+/, '/').split("/")
  12 +
  13 + index = 0
  14 + while(elements[index])
  15 + if (elements[index]==".")
  16 + elements.delete_at(index)
  17 + elsif (elements[index]=="..")
  18 + next if index==0
  19 + index-=1
  20 + 2.times { elements.delete_at(index)}
  21 +
  22 + else
  23 + index+=1
  24 + end
  25 + end
  26 +
  27 + elements * "/"
  28 + end
  29 +
  30 + # rewrite the URL reference paths
  31 + # url(../../../images/active_scaffold/default/add.gif);
  32 + # url(/stylesheets/active_scaffold/default/../../../images/active_scaffold/default/add.gif);
  33 + # url(/stylesheets/active_scaffold/../../images/active_scaffold/default/add.gif);
  34 + # url(/stylesheets/../images/active_scaffold/default/add.gif);
  35 + # url('/images/active_scaffold/default/add.gif');
  36 + def rewrite_urls(filename, content)
  37 + content.gsub!(/url *\(([^\)]+)\)/) { "url(#{rewrite_relative_path(filename, $1)})" }
  38 + end
  39 +
  40 + end
  41 +end
217 lib/bundle_fu/js_minimizer.rb
... ... @@ -0,0 +1,217 @@
  1 +#!/usr/bin/ruby
  2 +# jsmin.rb 2007-07-20
  3 +# Author: Uladzislau Latynski
  4 +# This work is a translation from C to Ruby of jsmin.c published by
  5 +# Douglas Crockford. Permission is hereby granted to use the Ruby
  6 +# version under the same conditions as the jsmin.c on which it is
  7 +# based.
  8 +#
  9 +# /* jsmin.c
  10 +# 2003-04-21
  11 +#
  12 +# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
  13 +#
  14 +# Permission is hereby granted, free of charge, to any person obtaining a copy of
  15 +# this software and associated documentation files (the "Software"), to deal in
  16 +# the Software without restriction, including without limitation the rights to
  17 +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  18 +# of the Software, and to permit persons to whom the Software is furnished to do
  19 +# so, subject to the following conditions:
  20 +#
  21 +# The above copyright notice and this permission notice shall be included in all
  22 +# copies or substantial portions of the Software.
  23 +#
  24 +# The Software shall be used for Good, not Evil.
  25 +#
  26 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  27 +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  28 +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  29 +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  30 +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  31 +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  32 +# SOFTWARE.
  33 +
  34 +require 'stringio'
  35 +
  36 +class BundleFu::JSMinimizer
  37 + attr_accessor :input
  38 + attr_accessor :output
  39 +
  40 + EOF = -1
  41 + @theA = ""
  42 + @theB = ""
  43 +
  44 + # isAlphanum -- return true if the character is a letter, digit, underscore,
  45 + # dollar sign, or non-ASCII character
  46 + def isAlphanum(c)
  47 + return false if !c || c == EOF
  48 + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
  49 + (c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
  50 + c == '\\' || c[0] > 126)
  51 + end
  52 +
  53 + # get -- return the next character from input. Watch out for lookahead. If
  54 + # the character is a control character, translate it to a space or linefeed.
  55 + def get()
  56 + c = @input.getc
  57 + return EOF if(!c)
  58 + c = c.chr
  59 + return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
  60 + return "\n" if (c == "\r")
  61 + return " "
  62 + end
  63 +
  64 + # Get the next character without getting it.
  65 + def peek()
  66 + lookaheadChar = @input.getc
  67 + @input.ungetc(lookaheadChar)
  68 + return lookaheadChar.chr
  69 + end
  70 +
  71 + # mynext -- get the next character, excluding comments.
  72 + # peek() is used to see if a '/' is followed by a '/' or '*'.
  73 + def mynext()
  74 + c = get
  75 + if (c == "/")
  76 + if(peek == "/")
  77 + while(true)
  78 + c = get
  79 + if (c <= "\n")
  80 + return c
  81 + end
  82 + end
  83 + end
  84 + if(peek == "*")
  85 + get
  86 + while(true)
  87 + case get
  88 + when "*"
  89 + if (peek == "/")
  90 + get
  91 + return " "
  92 + end
  93 + when EOF
  94 + raise "Unterminated comment"
  95 + end
  96 + end
  97 + end
  98 + end
  99 + return c
  100 + end
  101 +
  102 +
  103 + # action -- do something! What you do is determined by the argument: 1
  104 + # Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
  105 + # (Delete A). 3 Get the next B. (Delete B). action treats a string as a
  106 + # single character. Wow! action recognizes a regular expression if it is
  107 + # preceded by ( or , or =.
  108 + def action(a)
  109 + if(a==1)
  110 + @output.write @theA
  111 + end
  112 + if(a==1 || a==2)
  113 + @theA = @theB
  114 + if (@theA == "\'" || @theA == "\"")
  115 + while (true)
  116 + @output.write @theA
  117 + @theA = get
  118 + break if (@theA == @theB)
  119 + raise "Unterminated string literal" if (@theA <= "\n")
  120 + if (@theA == "\\")
  121 + @output.write @theA
  122 + @theA = get
  123 + end
  124 + end
  125 + end
  126 + end
  127 + if(a==1 || a==2 || a==3)
  128 + @theB = mynext
  129 + if (@theB == "/" && (@theA == "(" || @theA == "," || @theA == "=" ||
  130 + @theA == ":" || @theA == "[" || @theA == "!" ||
  131 + @theA == "&" || @theA == "|" || @theA == "?" ||
  132 + @theA == "{" || @theA == "}" || @theA == ";" ||
  133 + @theA == "\n"))
  134 + @output.write @theA
  135 + @output.write @theB
  136 + while (true)
  137 + @theA = get
  138 + if (@theA == "/")
  139 + break
  140 + elsif (@theA == "\\")
  141 + @output.write @theA
  142 + @theA = get
  143 + elsif (@theA <= "\n")
  144 + raise "Unterminated RegExp Literal"
  145 + end
  146 + @output.write @theA
  147 + end
  148 + @theB = mynext
  149 + end
  150 + end
  151 + end
  152 +
  153 + # jsmin -- Copy the input to the output, deleting the characters which are
  154 + # insignificant to JavaScript. Comments will be removed. Tabs will be
  155 + # replaced with spaces. Carriage returns will be replaced with linefeeds.
  156 + # Most spaces and linefeeds will be removed.
  157 + def jsmin
  158 + @theA = "\n"
  159 + action(3)
  160 + while (@theA != EOF)
  161 + case @theA
  162 + when " "
  163 + if (isAlphanum(@theB))
  164 + action(1)
  165 + else
  166 + action(2)
  167 + end
  168 + when "\n"
  169 + case (@theB)
  170 + when "{","[","(","+","-"
  171 + action(1)
  172 + when " "
  173 + action(3)
  174 + else
  175 + if (isAlphanum(@theB))
  176 + action(1)
  177 + else
  178 + action(2)
  179 + end
  180 + end
  181 + else
  182 + case (@theB)
  183 + when " "
  184 + if (isAlphanum(@theA))
  185 + action(1)
  186 + else
  187 + action(3)
  188 + end
  189 + when "\n"
  190 + case (@theA)
  191 + when "}","]",")","+","-","\"","\\", "'", '"'
  192 + action(1)
  193 + else
  194 + if (isAlphanum(@theA))
  195 + action(1)
  196 + else
  197 + action(3)
  198 + end
  199 + end
  200 + else
  201 + action(1)
  202 + end
  203 + end
  204 + end
  205 + end
  206 +
  207 + def self.minimize_content(content)
  208 + js_minimizer = new
  209 + js_minimizer.input = StringIO.new(content)
  210 + js_minimizer.output = StringIO.new
  211 +
  212 + js_minimizer.jsmin
  213 +
  214 + js_minimizer.output.string
  215 + end
  216 +
  217 +end
13 test/fixtures/public/javascripts/js_1.js
... ... @@ -1 +1,12 @@
1   -function js_1() { alert('hi')};
  1 +function js_1() { alert('hi')};
  2 +
  3 +// this is a function
  4 +function func() {
  5 + alert('hi')
  6 + return true
  7 +}
  8 +
  9 +function func() {
  10 + alert('hi')
  11 + return true
  12 +}
33 test/functional/bundle_fu_test.rb
@@ -5,19 +5,6 @@
5 5 # require "library_file_name"
6 6
7 7 class BundleFuTest < Test::Unit::TestCase
8   - @@content_include_some = <<-EOF
9   - <script src="/javascripts/js_1.js?1000" type="text/javascript"></script>
10   - <script src="/javascripts/js_2.js?1000" type="text/javascript"></script>
11   - <link href="/stylesheets/css_1.css?1000" media="screen" rel="Stylesheet" type="text/css" />
12   - <link href="/stylesheets/css_2.css?1000" media="screen" rel="Stylesheet" type="text/css" />
13   - EOF
14   -
15   - # the same content, slightly changed
16   - @@content_include_all = @@content_include_some + <<-EOF
17   - <script src="/javascripts/js_3.js?1000" type="text/javascript"></script>
18   - <link href="/stylesheets/css_3.css?1000" media="screen" rel="Stylesheet" type="text/css" />
19   - EOF
20   -
21 8 def setup
22 9 @mock_view = MockView.new
23 10 BundleFu.init # resets BundleFu
@@ -33,6 +20,23 @@ def test__bundle_js_files__should_include_js_content
33 20 assert_public_files_match("/javascripts/cache/bundle.js", "function js_1()")
34 21 end
35 22
  23 + def test__bundle_js_files__should_default_to_not_compressed_and_include_override_option
  24 + @mock_view.bundle() { @@content_include_all }
  25 + default_content = File.read(public_file("/javascripts/cache/bundle.js"))
  26 + purge_cache
  27 +
  28 + @mock_view.bundle(:compress => false) { @@content_include_all }
  29 + uncompressed_content = File.read(public_file("/javascripts/cache/bundle.js"))
  30 + purge_cache
  31 +
  32 + @mock_view.bundle(:compress => true) { @@content_include_all }
  33 + compressed_content = File.read(public_file("/javascripts/cache/bundle.js"))
  34 + purge_cache
  35 +
  36 + assert default_content.length == uncompressed_content.length, "Should default to uncompressed"
  37 + assert uncompressed_content.length > compressed_content.length, "Didn't compress the content. (:compress => true) #{compressed_content.length}. (:compress => false) #{uncompressed_content.length}"
  38 + end
  39 +
36 40 def test__content_remains_same__shouldnt_refresh_cache
37 41 @mock_view.bundle { @@content_include_some }
38 42
@@ -151,9 +155,6 @@ def test__bypass_param_set__should_honor_and_store_in_session
151 155 end
152 156
153 157 private
154   - def public_file(filename)
155   - File.join(::RAILS_ROOT, "public", filename)
156   - end
157 158
158 159 def purge_cache
159 160 # remove all fixtures named "bundle*"
15 test/functional/css_bundle_test.rb
@@ -2,15 +2,18 @@
2 2
3 3 class CSSBundleTest < Test::Unit::TestCase
4 4 def test__rewrite_relative_path__should_rewrite
5   - assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/spinner.gif"))
6   - assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/./../images/goober/../spinner.gif"))
7   - assert_equal("/images/spinner.gif", BundleFu.rewrite_relative_path("stylesheets/active_scaffold/default/./stylesheet.css", "../../../images/spinner.gif"))
8   - assert_equal("/stylesheets/image.gif", BundleFu.rewrite_relative_path("stylesheets/main.css", "image.gif"))
9   - assert_equal("/stylesheets/image.gif", BundleFu.rewrite_relative_path("/stylesheets////default/main.css", "..//image.gif"))
10   - assert_equal("/images/image.gif", BundleFu.rewrite_relative_path("/stylesheets/default/main.css", "/images/image.gif"))
  5 + assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/spinner.gif"))
  6 + assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/active_scaffold/default/stylesheet.css", "../../../images/./../images/goober/../spinner.gif"))
  7 + assert_equal("/images/spinner.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/active_scaffold/default/./stylesheet.css", "../../../images/spinner.gif"))
  8 + assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", "image.gif"))
  9 + assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", "'image.gif'"))
  10 + assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("stylesheets/main.css", " image.gif "))
  11 + assert_equal("/stylesheets/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets////default/main.css", "..//image.gif"))
  12 + assert_equal("/images/image.gif", BundleFu::CSSUrlRewriter.rewrite_relative_path("/stylesheets/default/main.css", "/images/image.gif"))
11 13 end
12 14
13 15 def test__bundle_css_file__should_rewrite_relatiave_path
  16 +# dbg
14 17 bundled_css = BundleFu.bundle_css_files(["/stylesheets/css_3.css"])
15 18 # puts bundled_css
16 19 assert_match("background-image: url(/images/background.gif)", bundled_css)
6 test/functional/js_bundle_test.rb
... ... @@ -1,10 +1,12 @@
1 1 require File.join(File.dirname(__FILE__), '../test_helper.rb')
2 2
3 3 class JSBundleTest < Test::Unit::TestCase
4   -
  4 + def test__bundle_js_files__bypass_bundle__should_bypass
  5 + BundleFu.bundle_js_files
  6 + end
5 7
6 8 def test__bundle_js_files__should_include_contents
7   - bundled_js = BundleFu.bundle_css_files(["/javascripts/js_1.js"])
  9 + bundled_js = BundleFu.bundle_js_files(["/javascripts/js_1.js"])
8 10 # puts bundled_js
9 11 # function js_1
10 12 assert_match("function js_1", bundled_js)
12 test/functional/js_minimizer_test.rb
... ... @@ -0,0 +1,12 @@
  1 +require File.join(File.dirname(__FILE__), '../test_helper.rb')
  2 +
  3 +class BundleFu::JSMinimizerTest < Test::Unit::TestCase
  4 + def test_minimize_content__should_be_less
  5 + test_content = File.read(public_file("javascripts/js_1.js"))
  6 + content_size = test_content.length
  7 + minimized_size = BundleFu::JSMinimizer.minimize_content(test_content).length
  8 +
  9 + assert(minimized_size > 0)
  10 + assert(content_size > minimized_size)
  11 + end
  12 +end
3  test/run_all.rb
... ... @@ -0,0 +1,3 @@
  1 +Dir[File.join(File.dirname(__FILE__), "functional/*.rb")].each{|filename|
  2 + require filename
  3 +}
21 test/test_helper.rb
@@ -2,7 +2,7 @@
2 2 require "rubygems"
3 3 require 'active_support'
4 4
5   -for file in ["../lib/bundle_fu.rb", "../lib/bundle_fu/file_list.rb", "../lib/bundle_fu/file_list.rb", "mock_view.rb"]
  5 +for file in ["../environment.rb", "mock_view.rb"]
6 6 require File.expand_path(File.join(File.dirname(__FILE__), file))
7 7 end
8 8
@@ -16,4 +16,23 @@ class Object
16 16 def to_regexp
17 17 is_a?(Regexp) ? self : Regexp.new(Regexp.escape(self.to_s))
18 18 end
  19 +end
  20 +
  21 +class Test::Unit::TestCase
  22 + @@content_include_some = <<-EOF
  23 + <script src="/javascripts/js_1.js?1000" type="text/javascript"></script>
  24 + <script src="/javascripts/js_2.js?1000" type="text/javascript"></script>
  25 + <link href="/stylesheets/css_1.css?1000" media="screen" rel="Stylesheet" type="text/css" />
  26 + <link href="/stylesheets/css_2.css?1000" media="screen" rel="Stylesheet" type="text/css" />
  27 + EOF
  28 +
  29 + # the same content, slightly changed
  30 + @@content_include_all = @@content_include_some + <<-EOF
  31 + <script src="/javascripts/js_3.js?1000" type="text/javascript"></script>
  32 + <link href="/stylesheets/css_3.css?1000" media="screen" rel="Stylesheet" type="text/css" />
  33 + EOF
  34 +
  35 + def public_file(filename)
  36 + File.join(::RAILS_ROOT, "public", filename)
  37 + end
19 38 end

0 comments on commit e93ef1c

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