From 2a1137f7c510035e2f164fbd7789a907c4729bef Mon Sep 17 00:00:00 2001 From: James Prior Date: Fri, 30 Apr 2010 14:36:41 -0400 Subject: [PATCH 1/4] Updating JS Min to work as a module and not a command line app, matching asset_packager to use module and stop shuffling files / invoking ruby --- init.rb | 1 + lib/jsmin.rb | 351 +++++++++++++++++---------------- lib/synthesis/asset_package.rb | 19 +- 3 files changed, 188 insertions(+), 183 deletions(-) diff --git a/init.rb b/init.rb index c5713fb..211b0f3 100644 --- a/init.rb +++ b/init.rb @@ -1,3 +1,4 @@ require 'synthesis/asset_package' require 'synthesis/asset_package_helper' +require 'jsmin' ActionView::Base.send :include, Synthesis::AssetPackageHelper \ No newline at end of file diff --git a/lib/jsmin.rb b/lib/jsmin.rb index 987747c..6c426d1 100644 --- a/lib/jsmin.rb +++ b/lib/jsmin.rb @@ -1,4 +1,5 @@ -#!/usr/bin/ruby +# Updated to work as a module instead of a command line version + # jsmin.rb 2007-07-20 # Author: Uladzislau Latynski # This work is a translation from C to Ruby of jsmin.c published by @@ -31,181 +32,201 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +require 'stringio' + # Ruby 1.9 Compatibility Fix - the below isAlphanum uses Fixnum#ord to be compatible with Ruby 1.9 and 1.8.7 # Fixnum#ord is not found by default in 1.8.6, so monkey patch it in: if RUBY_VERSION == '1.8.6' class Fixnum; def ord; return self; end; end end -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].ord > 126) -end - -# get -- return the next character from stdin. Watch out for lookahead. If -# the character is a control character, translate it to a space or linefeed. -def get() - c = $stdin.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 +class JSMin + # class variables + @@EOF = -1 + @@theA = "" + @@theB = "" + @@input = "" + @@output = "" -# Get the next character without getting it. -def peek() - lookaheadChar = $stdin.getc - $stdin.ungetc(lookaheadChar) - return lookaheadChar.chr -end + # singleton methods + class << self -# 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 + def compress(incoming) + @@output = StringIO.new("","w") + if incoming.is_a? String + @@input = StringIO.new(incoming,"r") + elsif incoming.kind_of? IO + @@input = incoming + else + raise ArgumentError.new("JSMin can only compress strings or files") + end + jsmin + @@output.string end - return c -end + + + protected + # 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].ord > 126) + end + # get -- return the next character from stdin. 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 -# 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) - $stdout.write $theA - end - if(a==1 || a==2) - $theA = $theB - if ($theA == "\'" || $theA == "\"") - while (true) - $stdout.write $theA - $theA = get - break if ($theA == $theB) - raise "Unterminated string literal" if ($theA <= "\n") - if ($theA == "\\") - $stdout.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")) - $stdout.write $theA - $stdout.write $theB - while (true) - $theA = get - if ($theA == "/") - break - elsif ($theA == "\\") - $stdout.write $theA - $theA = get - elsif ($theA <= "\n") - raise "Unterminated RegExp Literal" - end - $stdout.write $theA - end - $theB = mynext - end - end -end + # Get the next character without getting it. + def peek() + lookaheadChar = @@input.getc + @@input.ungetc(lookaheadChar) + return lookaheadChar.chr + 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 + # 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 -ARGV.each do |anArg| - $stdout.write "// #{anArg}\n" -end -jsmin + # 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 + end +end \ No newline at end of file diff --git a/lib/synthesis/asset_package.rb b/lib/synthesis/asset_package.rb index 99c9f7a..4a02036 100644 --- a/lib/synthesis/asset_package.rb +++ b/lib/synthesis/asset_package.rb @@ -153,24 +153,7 @@ def compressed_file end def compress_js(source) - jsmin_path = "#{Rails.root}/vendor/plugins/asset_packager/lib" - tmp_path = "#{Rails.root}/tmp/#{@target}_packaged" - - # write out to a temp file - File.open("#{tmp_path}_uncompressed.js", "w") {|f| f.write(source) } - - # compress file with JSMin library - `ruby #{jsmin_path}/jsmin.rb <#{tmp_path}_uncompressed.js >#{tmp_path}_compressed.js \n` - - # read it back in and trim it - result = "" - File.open("#{tmp_path}_compressed.js", "r") { |f| result += f.read.strip } - - # delete temp files if they exist - File.delete("#{tmp_path}_uncompressed.js") if File.exists?("#{tmp_path}_uncompressed.js") - File.delete("#{tmp_path}_compressed.js") if File.exists?("#{tmp_path}_compressed.js") - - result + JSMin.compress(source) end def compress_css(source) From b3df18e3f107951e8321a30bfd298a02a3df4fb1 Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 6 May 2010 15:51:56 -0400 Subject: [PATCH 2/4] Moving JSMin require for support with rake tasks. --- init.rb | 2 +- lib/synthesis/asset_package.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 211b0f3..fa05a7d 100644 --- a/init.rb +++ b/init.rb @@ -1,4 +1,4 @@ require 'synthesis/asset_package' require 'synthesis/asset_package_helper' -require 'jsmin' + ActionView::Base.send :include, Synthesis::AssetPackageHelper \ No newline at end of file diff --git a/lib/synthesis/asset_package.rb b/lib/synthesis/asset_package.rb index 4a02036..95e62ae 100644 --- a/lib/synthesis/asset_package.rb +++ b/lib/synthesis/asset_package.rb @@ -1,3 +1,4 @@ +require File.join(File.dirname(__FILE__), '..', 'jsmin') module Synthesis class AssetPackage From 08c175803facf82e21f3cbd2ef3e9591e29c082c Mon Sep 17 00:00:00 2001 From: Scott Windsor Date: Sun, 18 Apr 2010 10:47:28 -0700 Subject: [PATCH 3/4] Rails 3 XSS fixes (using html_safe) --- lib/synthesis/asset_package_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/synthesis/asset_package_helper.rb b/lib/synthesis/asset_package_helper.rb index 77674f4..785bebb 100644 --- a/lib/synthesis/asset_package_helper.rb +++ b/lib/synthesis/asset_package_helper.rb @@ -21,7 +21,7 @@ def javascript_include_merged(*sources) AssetPackage.targets_from_sources("javascripts", sources) : AssetPackage.sources_from_targets("javascripts", sources)) - sources.collect {|source| javascript_include_tag(source, options) }.join("\n") + sources.collect {|source| javascript_include_tag(source, options) }.join("\n").html_safe end def stylesheet_link_merged(*sources) @@ -32,8 +32,8 @@ def stylesheet_link_merged(*sources) AssetPackage.targets_from_sources("stylesheets", sources) : AssetPackage.sources_from_targets("stylesheets", sources)) - sources.collect { |source| stylesheet_link_tag(source, options) }.join("\n") + sources.collect { |source| stylesheet_link_tag(source, options) }.join("\n").html_safe end end -end \ No newline at end of file +end From d1b5959479cbc59e92a9b297cae5ccd1176fc2a8 Mon Sep 17 00:00:00 2001 From: Brady Bouchard Date: Sun, 6 Jun 2010 23:02:31 +1000 Subject: [PATCH 4/4] Packaged files should take their modification times from the most recently updated source file. This allows Rails asset IDs to work as intended - if the source files haven't been modified when a new build is done (typically at deploy time), the asset IDs will stay the same so browsers don't need to re-download. --- lib/synthesis/asset_package.rb | 40 +++++++++++++++++++----------- test/assets/javascripts/bar.js | 8 +++--- test/assets/javascripts/foo.js | 8 +++--- test/assets/stylesheets/header.css | 32 ++++++++++++------------ test/assets/stylesheets/screen.css | 32 ++++++++++++------------ 5 files changed, 66 insertions(+), 54 deletions(-) diff --git a/lib/synthesis/asset_package.rb b/lib/synthesis/asset_package.rb index 95e62ae..4a31f25 100644 --- a/lib/synthesis/asset_package.rb +++ b/lib/synthesis/asset_package.rb @@ -4,18 +4,18 @@ class AssetPackage @asset_base_path = "#{Rails.root}/public" @asset_packages_yml = File.exists?("#{Rails.root}/config/asset_packages.yml") ? YAML.load_file("#{Rails.root}/config/asset_packages.yml") : nil - + # singleton methods class << self attr_accessor :asset_base_path, :asset_packages_yml attr_writer :merge_environments - + def merge_environments @merge_environments ||= ["production"] end - + def parse_path(path) /^(?:(.*)\/)?([^\/]+)$/.match(path).to_a end @@ -89,10 +89,10 @@ def create_yml end end - + # instance methods attr_accessor :asset_type, :target, :target_dir, :sources - + def initialize(asset_type, package_hash) target_parts = self.class.parse_path(package_hash.keys.first) @target_dir = target_parts[1].to_s @@ -103,8 +103,9 @@ def initialize(asset_type, package_hash) @extension = get_extension @file_name = "#{@target}_packaged.#{@extension}" @full_path = File.join(@asset_path, @file_name) + @latest_mtime = get_latest_mtime end - + def package_exists? File.exists?(@full_path) end @@ -132,20 +133,31 @@ def create_new_build log "Latest version already exists: #{new_build_path}" else File.open(new_build_path, "w") {|f| f.write(compressed_file) } + File.utime(0, @latest_mtime, new_build_path) log "Created #{new_build_path}" end end def merged_file merged_file = "" - @sources.each {|s| - File.open("#{@asset_path}/#{s}.#{@extension}", "r") { |f| - merged_file += f.read + "\n" + @sources.each {|s| + File.open("#{@asset_path}/#{s}.#{@extension}", "r") { |f| + merged_file += f.read + "\n" } } merged_file end - + + # Store the latest mtime so that we can attach it to the merged archive. + # This allows the Rails asset IDs to work as intended for caching purposes - + # if none of the files in the archive have been modified since the last build, + # then the new build (typically done at deploy time) will keep the same mtime + # (and Rails asset ID). + # + def get_latest_mtime + return @sources.collect{ |s| File.mtime("#{@asset_path}/#{s}.#{@extension}") }.max + end + def compressed_file case @asset_type when "javascripts" then compress_js(merged_file) @@ -156,7 +168,7 @@ def compressed_file def compress_js(source) JSMin.compress(source) end - + def compress_css(source) source.gsub!(/\s+/, " ") # collapse space source.gsub!(/\/\*(.*?)\*\//, "") # remove comments - caution, might want to remove this if using css hacks @@ -173,11 +185,11 @@ def get_extension when "stylesheets" then "css" end end - + def log(message) self.class.log(message) end - + def self.log(message) puts message end @@ -189,6 +201,6 @@ def self.build_file_list(path, extension) file_list.reverse! if extension == "js" file_list end - + end end diff --git a/test/assets/javascripts/bar.js b/test/assets/javascripts/bar.js index 3a65f10..ba2b734 100755 --- a/test/assets/javascripts/bar.js +++ b/test/assets/javascripts/bar.js @@ -1,4 +1,4 @@ -bar bar bar -bar bar bar -bar bar bar - +bar bar bar +bar bar bar +bar bar bar + diff --git a/test/assets/javascripts/foo.js b/test/assets/javascripts/foo.js index 7e4cd9a..4a4dfba 100755 --- a/test/assets/javascripts/foo.js +++ b/test/assets/javascripts/foo.js @@ -1,4 +1,4 @@ -foo foo foo -foo foo foo -foo foo foo - +foo foo foo +foo foo foo +foo foo foo + diff --git a/test/assets/stylesheets/header.css b/test/assets/stylesheets/header.css index 473902e..85b2dcc 100755 --- a/test/assets/stylesheets/header.css +++ b/test/assets/stylesheets/header.css @@ -1,16 +1,16 @@ -#header1 { - background: #fff; - color: #000; - text-align: center; -} -#header2 { - background: #fff; - color: #000; - text-align: center; -} -#header3 { - background: #fff; - color: #000; - text-align: center; -} - +#header1 { + background: #fff; + color: #000; + text-align: center; +} +#header2 { + background: #fff; + color: #000; + text-align: center; +} +#header3 { + background: #fff; + color: #000; + text-align: center; +} + diff --git a/test/assets/stylesheets/screen.css b/test/assets/stylesheets/screen.css index 0d66fd4..9564de1 100755 --- a/test/assets/stylesheets/screen.css +++ b/test/assets/stylesheets/screen.css @@ -1,16 +1,16 @@ -#screen1 { - background: #fff; - color: #000; - text-align: center; -} -#screen2 { - background: #fff; - color: #000; - text-align: center; -} -#screen3 { - background: #fff; - color: #000; - text-align: center; -} - +#screen1 { + background: #fff; + color: #000; + text-align: center; +} +#screen2 { + background: #fff; + color: #000; + text-align: center; +} +#screen3 { + background: #fff; + color: #000; + text-align: center; +} +