├── .gitignore ├── README.rdoc ├── Rakefile ├── lib ├── yui │ └── compressor.rb └── yuicompressor-2.4.8.jar ├── test └── compressor_test.rb └── yui-compressor.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Ruby-YUI Compressor 2 | 3 | Ruby-YUI Compressor provides a Ruby interface to the {YUI Compressor Java library}[http://developer.yahoo.com/yui/compressor/] for minifying JavaScript and CSS assets. 4 | 5 | Latest version: 0.12.0 (includes YUI Compressor 2.4.8) 6 | 7 | * {API documentation}[http://yui.rubyforge.org/] 8 | * {Source code}[http://github.com/sstephenson/ruby-yui-compressor/] 9 | * {Bug tracker}[http://github.com/sstephenson/ruby-yui-compressor/issues] 10 | 11 | Tested on Mac OS X Ruby 1.8.7 and Ruby 1.9.2. 12 | 13 | === Installing and loading Ruby-YUI Compressor 14 | 15 | Ruby-YUI Compressor is distributed as a Ruby Gem (yui-compressor). Because YUI Compressor's .jar file is included in the box, Java >= 1.4 is its only dependency. 16 | 17 | $ sudo gem install -r yui-compressor 18 | $ irb -rubygems 19 | >> require "yui/compressor" 20 | => true 21 | 22 | === Using Ruby-YUI Compressor 23 | 24 | Create either a YUI::CssCompressor or a YUI::JavaScriptCompressor. Then pass an IO or string to the YUI::Compressor#compress method and get the compressed contents back as a string or have them yielded to a block. 25 | 26 | ==== Example: Compress JavaScript 27 | compressor = YUI::JavaScriptCompressor.new 28 | compressor.compress('(function () { var foo = {}; foo["bar"] = "baz"; })()') 29 | # => "(function(){var foo={};foo.bar=\"baz\"})();" 30 | 31 | ==== Example: Compress JavaScript and shorten local variable names 32 | compressor = YUI::JavaScriptCompressor.new(:munge => true) 33 | compressor.compress('(function () { var foo = {}; foo["bar"] = "baz"; })()') 34 | # => "(function(){var a={};a.bar=\"baz\"})();" 35 | 36 | ==== Example: Compress CSS 37 | compressor = YUI::CssCompressor.new 38 | compressor.compress(<<-END_CSS) 39 | div.error { 40 | color: red; 41 | } 42 | div.warning { 43 | display: none; 44 | } 45 | END_CSS 46 | # => "div.error{color:red;}div.warning{display:none;}" 47 | 48 | ==== Overriding the path to Java or the YUI Compressor .jar file 49 | 50 | By default, YUI::Compressor looks for Java as the +java+ command in your path, and uses the YUI Compressor .jar file in YUI::Compressor's vendor directory. You can override both with the :java and :jar_file options: 51 | 52 | YUI::JavaScriptCompressor.new( 53 | :java => "/usr/bin/java", 54 | :jar_file => "/path/to/my/yuicompressor-2.4.8.jar" 55 | ) 56 | 57 | ==== Additional compression options 58 | 59 | See the YUI::CssCompressor and YUI::JavaScriptCompressor documentation for more information on format-specific compression options. 60 | 61 | 62 | == Licenses 63 | 64 | ==== Ruby-YUI Compressor code and documentation (MIT license) 65 | 66 | Copyright (c) 2011 Sam Stephenson 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining 69 | a copy of this software and associated documentation files (the 70 | "Software"), to deal in the Software without restriction, including 71 | without limitation the rights to use, copy, modify, merge, publish, 72 | distribute, sublicense, and/or sell copies of the Software, and to 73 | permit persons to whom the Software is furnished to do so, subject to 74 | the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be 77 | included in all copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 80 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 82 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 83 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 84 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 85 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 86 | 87 | ==== YUI Compressor (BSD license) 88 | 89 | Copyright (c) 2009, Yahoo! Inc. 90 | All rights reserved. 91 | 92 | Redistribution and use of this software in source and binary forms, 93 | with or without modification, are permitted provided that the following 94 | conditions are met: 95 | 96 | * Redistributions of source code must retain the above 97 | copyright notice, this list of conditions and the 98 | following disclaimer. 99 | 100 | * Redistributions in binary form must reproduce the above 101 | copyright notice, this list of conditions and the 102 | following disclaimer in the documentation and/or other 103 | materials provided with the distribution. 104 | 105 | * Neither the name of Yahoo! Inc. nor the names of its 106 | contributors may be used to endorse or promote products 107 | derived from this software without specific prior 108 | written permission of Yahoo! Inc. 109 | 110 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 111 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 112 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 113 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 114 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 115 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 116 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 117 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 118 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 119 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 120 | 121 | This software also requires access to software from the following sources: 122 | 123 | The Jarg Library v 1.0 ( http://jargs.sourceforge.net/ ) is available 124 | under a BSD License. Copyright (c) 2001-2003 Steve Purcell, 125 | Copyright (c) 2002 Vidar Holen, Copyright (c) 2002 Michal Ceresna and 126 | Copyright (c) 2005 Ewan Mellor. 127 | 128 | The Rhino Library ( http://www.mozilla.org/rhino/ ) is dually available 129 | under an MPL 1.1/GPL 2.0 license, with portions subject to a BSD license. 130 | 131 | Additionally, this software contains modified versions of the following 132 | component files from the Rhino Library: 133 | 134 | [org/mozilla/javascript/Decompiler.java] 135 | [org/mozilla/javascript/Parser.java] 136 | [org/mozilla/javascript/Token.java] 137 | [org/mozilla/javascript/TokenStream.java] 138 | 139 | The modified versions of these files are distributed under the MPL v 1.1 140 | ( http://www.mozilla.org/MPL/MPL-1.1.html ) 141 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rubygems/package_task" 3 | require "rdoc/task" 4 | require "rake/testtask" 5 | 6 | task :default => :test 7 | 8 | Rake::TestTask.new do |t| 9 | t.libs += ["lib", "test"] 10 | t.test_files = FileList["test/*_test.rb"] 11 | t.verbose = true 12 | end 13 | 14 | RDoc::Task.new do |t| 15 | t.rdoc_files.include("README.rdoc", "lib/**/*.rb") 16 | end 17 | 18 | Gem::PackageTask.new(eval(IO.read(File.join(File.dirname(__FILE__), "yui-compressor.gemspec")))) do |pkg| 19 | pkg.need_zip = true 20 | pkg.need_tar = true 21 | end 22 | -------------------------------------------------------------------------------- /lib/yui/compressor.rb: -------------------------------------------------------------------------------- 1 | require "shellwords" 2 | require "stringio" 3 | require "tempfile" 4 | require "rbconfig" 5 | 6 | module YUI #:nodoc: 7 | class Compressor 8 | VERSION = "0.12.0" 9 | 10 | class Error < StandardError; end 11 | class OptionError < Error; end 12 | class RuntimeError < Error; end 13 | 14 | attr_reader :options 15 | 16 | def self.default_options #:nodoc: 17 | { :charset => "utf-8", :line_break => nil } 18 | end 19 | 20 | def self.compressor_type #:nodoc: 21 | raise Error, "create a CssCompressor or JavaScriptCompressor instead" 22 | end 23 | 24 | def initialize(options = {}) #:nodoc: 25 | @options = self.class.default_options.merge(options) 26 | @command = [path_to_java] 27 | @command.push(*java_opts) 28 | @command.push("-jar") 29 | @command.push(path_to_jar_file) 30 | @command.push(*(command_option_for_type + command_options)) 31 | @command.compact! 32 | end 33 | 34 | def command #:nodoc: 35 | if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ 36 | # Shellwords is only for bourne shells, so windows shells get this 37 | # extremely remedial escaping 38 | escaped_cmd = @command.map do |word| 39 | if word =~ / / 40 | word = "\"%s\"" % word 41 | end 42 | 43 | word 44 | end 45 | else 46 | escaped_cmd = @command.map { |word| Shellwords.escape(word) } 47 | end 48 | 49 | escaped_cmd.join(" ") 50 | end 51 | 52 | # Compress a stream or string of code with YUI Compressor. (A stream is 53 | # any object that responds to +read+ and +close+ like an IO.) If a block 54 | # is given, you can read the compressed code from the block's argument. 55 | # Otherwise, +compress+ returns a string of compressed code. 56 | # 57 | # ==== Example: Compress CSS 58 | # compressor = YUI::CssCompressor.new 59 | # compressor.compress(<<-END_CSS) 60 | # div.error { 61 | # color: red; 62 | # } 63 | # div.warning { 64 | # display: none; 65 | # } 66 | # END_CSS 67 | # # => "div.error{color:red;}div.warning{display:none;}" 68 | # 69 | # ==== Example: Compress JavaScript 70 | # compressor = YUI::JavaScriptCompressor.new 71 | # compressor.compress('(function () { var foo = {}; foo["bar"] = "baz"; })()') 72 | # # => "(function(){var foo={};foo.bar=\"baz\"})();" 73 | # 74 | # ==== Example: Compress and gzip a file on disk 75 | # File.open("my.js", "r") do |source| 76 | # Zlib::GzipWriter.open("my.js.gz", "w") do |gzip| 77 | # compressor.compress(source) do |compressed| 78 | # while buffer = compressed.read(4096) 79 | # gzip.write(buffer) 80 | # end 81 | # end 82 | # end 83 | # end 84 | # 85 | def compress(stream_or_string) 86 | streamify(stream_or_string) do |stream| 87 | tempfile = Tempfile.new('yui_compress') 88 | tempfile.write stream.read 89 | tempfile.flush 90 | full_command = "%s %s" % [command, tempfile.path] 91 | 92 | begin 93 | output = `#{full_command}` 94 | rescue Exception => e 95 | # windows shells tend to blow up here when the command fails 96 | raise RuntimeError, "compression failed: %s" % e.message 97 | ensure 98 | tempfile.close! 99 | end 100 | 101 | if $?.exitstatus.zero? 102 | output 103 | else 104 | # Bourne shells tend to blow up here when the command fails, usually 105 | # because java is missing 106 | raise RuntimeError, "Command '%s' returned non-zero exit status" % 107 | full_command 108 | end 109 | end 110 | end 111 | 112 | private 113 | def command_options 114 | options.inject([]) do |command_options, (name, argument)| 115 | method = begin 116 | method(:"command_option_for_#{name}") 117 | rescue NameError 118 | raise OptionError, "undefined option #{name.inspect}" 119 | end 120 | 121 | command_options.concat(method.call(argument)) 122 | end 123 | end 124 | 125 | def path_to_java 126 | options.delete(:java) || "java" 127 | end 128 | 129 | def java_opts 130 | options.delete(:java_opts).to_s.split(/\s+/) 131 | end 132 | 133 | def path_to_jar_file 134 | options.delete(:jar_file) || File.join(File.dirname(__FILE__), *%w".. yuicompressor-2.4.8.jar") 135 | end 136 | 137 | def streamify(stream_or_string) 138 | if stream_or_string.respond_to?(:read) 139 | yield stream_or_string 140 | else 141 | yield StringIO.new(stream_or_string.to_s) 142 | end 143 | end 144 | 145 | def command_option_for_type 146 | ["--type", self.class.compressor_type.to_s] 147 | end 148 | 149 | def command_option_for_charset(charset) 150 | ["--charset", charset.to_s] 151 | end 152 | 153 | def command_option_for_line_break(line_break) 154 | line_break ? ["--line-break", line_break.to_s] : [] 155 | end 156 | end 157 | 158 | class CssCompressor < Compressor 159 | def self.compressor_type #:nodoc: 160 | "css" 161 | end 162 | 163 | # Creates a new YUI::CssCompressor for minifying CSS code. 164 | # 165 | # Options are: 166 | # 167 | # :charset:: Specifies the character encoding to use. Defaults to 168 | # "utf-8". 169 | # :line_break:: By default, CSS will be compressed onto a single 170 | # line. Use this option to specify the maximum 171 | # number of characters in each line before a newline 172 | # is added. If :line_break is 0, a newline 173 | # is added after each CSS rule. 174 | # 175 | def initialize(options = {}) 176 | super 177 | end 178 | end 179 | 180 | class JavaScriptCompressor < Compressor 181 | def self.compressor_type #:nodoc: 182 | "js" 183 | end 184 | 185 | def self.default_options #:nodoc: 186 | super.merge( 187 | :munge => false, 188 | :optimize => true, 189 | :preserve_semicolons => false 190 | ) 191 | end 192 | 193 | # Creates a new YUI::JavaScriptCompressor for minifying JavaScript code. 194 | # 195 | # Options are: 196 | # 197 | # :charset:: Specifies the character encoding to use. Defaults to 198 | # "utf-8". 199 | # :line_break:: By default, JavaScript will be compressed onto a 200 | # single line. Use this option to specify the 201 | # maximum number of characters in each line before a 202 | # newline is added. If :line_break is 0, a 203 | # newline is added after each JavaScript statement. 204 | # :munge:: Specifies whether YUI Compressor should shorten local 205 | # variable names when possible. Defaults to +false+. 206 | # :optimize:: Specifies whether YUI Compressor should optimize 207 | # JavaScript object property access and object literal 208 | # declarations to use as few characters as possible. 209 | # Defaults to +true+. 210 | # :preserve_semicolons:: Defaults to +false+. If +true+, YUI 211 | # Compressor will ensure semicolons exist 212 | # after each statement to appease tools like 213 | # JSLint. 214 | # 215 | def initialize(options = {}) 216 | super 217 | end 218 | 219 | private 220 | def command_option_for_munge(munge) 221 | munge ? [] : ["--nomunge"] 222 | end 223 | 224 | def command_option_for_optimize(optimize) 225 | optimize ? [] : ["--disable-optimizations"] 226 | end 227 | 228 | def command_option_for_preserve_semicolons(preserve_semicolons) 229 | preserve_semicolons ? ["--preserve-semi"] : [] 230 | end 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /lib/yuicompressor-2.4.8.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sstephenson/ruby-yui-compressor/fc914911329bbfaea7f4f5dfb9a8c2c38b8027f9/lib/yuicompressor-2.4.8.jar -------------------------------------------------------------------------------- /test/compressor_test.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "yui/compressor" 3 | 4 | module YUI 5 | class CompressorTest < Test::Unit::TestCase 6 | FIXTURE_CSS = <<-END_CSS 7 | div.warning { 8 | display: none; 9 | } 10 | 11 | div.error { 12 | background: red; 13 | color: white; 14 | } 15 | 16 | @media screen and (max-device-width: 640px) { 17 | body { font-size: 90%; } 18 | } 19 | END_CSS 20 | 21 | FIXTURE_JS = <<-END_JS 22 | // here's a comment 23 | var Foo = { "a": 1 }; 24 | Foo["bar"] = (function(baz) { 25 | /* here's a 26 | multiline comment */ 27 | if (false) { 28 | doSomething(); 29 | } else { 30 | for (var index = 0; index < baz.length; index++) { 31 | doSomething(baz[index]); 32 | } 33 | } 34 | })("hello"); 35 | END_JS 36 | 37 | FIXTURE_ERROR_JS = "var x = {class: 'name'};" 38 | 39 | def test_css_data_uri 40 | data_uri_css = 'div { ' \ 41 | 'background: white url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEU' \ 42 | 'gAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h' \ 43 | '/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAA' \ 44 | 'AAElFTkSuQmCC\') no-repeat scroll left top;}' 45 | 46 | assert_nothing_raised do 47 | compressor = YUI::CssCompressor.new 48 | compressor.compress(data_uri_css) 49 | end 50 | end 51 | 52 | def test_js_java_opts_one_opt 53 | @compressor = YUI::JavaScriptCompressor.new(:java_opts => "-Xms64M") 54 | assert_match(/^java -Xms64M/, @compressor.command) 55 | end 56 | 57 | def test_css_java_opts_two_opts 58 | @compressor = YUI::CssCompressor.new(:java_opts => "-Xms64M -Xmx64M") 59 | assert_match(/^java -Xms64M -Xmx64M/, @compressor.command) 60 | end 61 | 62 | def test_js_java_opts_no_opts 63 | @compressor = YUI::JavaScriptCompressor.new() 64 | assert_match(/^java -jar/, @compressor.command) 65 | end 66 | 67 | def test_compressor_should_raise_when_instantiated 68 | assert_raises YUI::Compressor::Error do 69 | YUI::Compressor.new 70 | end 71 | end 72 | 73 | def test_css_should_be_compressed 74 | @compressor = YUI::CssCompressor.new 75 | assert_equal "div.warning{display:none}div.error{background:red;color:white}@media screen and (max-device-width:640px){body{font-size:90%}}", @compressor.compress(FIXTURE_CSS) 76 | end 77 | 78 | def test_js_should_be_compressed 79 | @compressor = YUI::JavaScriptCompressor.new 80 | assert_equal "var Foo={a:1};Foo.bar=(function(baz){if(false){doSomething()}else{for(var index=0;index "bar") 93 | @compressor.compress(FIXTURE_JS) 94 | end 95 | end 96 | 97 | def test_compress_should_accept_an_io_argument 98 | @compressor = YUI::CssCompressor.new 99 | assert_equal "div.warning{display:none}div.error{background:red;color:white}@media screen and (max-device-width:640px){body{font-size:90%}}", @compressor.compress(StringIO.new(FIXTURE_CSS)) 100 | end 101 | 102 | def test_compress_should_accept_a_block_and_yield_an_io 103 | @compressor = YUI::CssCompressor.new 104 | @compressor.compress(FIXTURE_CSS) do |stream| 105 | assert_kind_of IO, stream 106 | assert_equal "div.warning{display:none}div.error{background:red;color:white}@media screen and (max-device-width:640px){body{font-size:90%}}", stream.read 107 | end 108 | end 109 | 110 | def test_line_break_option_should_insert_line_breaks_in_css 111 | @compressor = YUI::CssCompressor.new(:line_break => 0) 112 | assert_equal "div.warning{display:none}\ndiv.error{background:red;color:white}\n@media screen and (max-device-width:640px){body{font-size:90%}\n}", @compressor.compress(FIXTURE_CSS) 113 | end 114 | 115 | def test_line_break_option_should_insert_line_breaks_in_js 116 | @compressor = YUI::JavaScriptCompressor.new(:line_break => 0) 117 | assert_equal "var Foo={a:1};\nFoo.bar=(function(baz){if(false){doSomething()\n}else{for(var index=0;\nindex true) 122 | assert_equal "var Foo={a:1};Foo.bar=(function(b){if(false){doSomething()}else{for(var a=0;a false) 127 | assert_equal "var Foo={\"a\":1};Foo[\"bar\"]=(function(baz){if(false){doSomething()}else{for(var index=0;index true) 132 | assert_equal "var Foo={a:1};Foo.bar=(function(baz){if(false){doSomething();}else{for(var index=0;index 0 15 | end 16 | --------------------------------------------------------------------------------