├── .gitignore ├── History.txt ├── lib ├── yui │ ├── yuicompressor.jar │ └── LICENSE └── front_end_architect │ ├── hash.rb │ └── blender.rb ├── Manifest.txt ├── License.txt ├── Rakefile ├── blender.gemspec ├── bin └── blend └── README.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_Store 3 | doc -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 0.20.0 / 2008-10-13 2 | 3 | Release Candidate 1 4 | -------------------------------------------------------------------------------- /lib/yui/yuicompressor.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/front-end/front-end-blender/master/lib/yui/yuicompressor.jar -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | License.txt 3 | Manifest.txt 4 | README.txt 5 | Rakefile 6 | bin/blend 7 | lib/front_end_architect/blender.rb 8 | lib/front_end_architect/hash.rb 9 | lib/yui/LICENSE 10 | lib/yui/yuicompressor.jar 11 | -------------------------------------------------------------------------------- /lib/front_end_architect/hash.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def deep_merge(hash) 3 | target = dup 4 | 5 | hash.keys.each do |key| 6 | if hash[key].is_a? Hash and self[key].is_a? Hash 7 | target[key] = target[key].deep_merge(hash[key]) 8 | next 9 | end 10 | 11 | target[key] = hash[key] 12 | end 13 | 14 | target 15 | end 16 | 17 | def deep_merge!(second) 18 | second.each_pair do |k,v| 19 | if self[k].is_a?(Hash) and second[k].is_a? Hash 20 | self[k].deep_merge!(second[k]) 21 | else 22 | self[k] = second[k] 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Chris Griego 2 | (c) 2008 Blake Elshire 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008 Blake Elshire, Chris Griego 2 | # 3 | # Blender is freely distributable under the terms of an MIT-style license. 4 | # For details, see http://www.opensource.org/licenses/mit-license.php 5 | 6 | $:.unshift File.join(File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__), *%w[lib]) 7 | 8 | require 'rubygems' 9 | require 'hoe' 10 | require 'front_end_architect/blender' 11 | 12 | hoe = Hoe.new('blender', FrontEndArchitect::Blender::VERSION) do |p| 13 | p.author = ['Blake Elshire', 'Chris Griego'] 14 | p.email = 'blender@front-end-architect.com' 15 | p.summary = 'Blender outputs efficient, production-ready CSS and/or JavaScript assets.' 16 | p.description = 'Blender is like ant or make for the front-end. It aggregates and compresses CSS and/or JavaScript assets for a site into efficient, production-ready files.' 17 | p.url = 'http://www.front-end-architect.com/blender/' 18 | 19 | p.extra_deps << ['mime-types', '>= 1.15'] 20 | p.extra_deps << ['colored', '>= 1.1'] 21 | 22 | p.spec_extras[:requirements] = 'Java, v1.4 or greater' 23 | 24 | p.remote_rdoc_dir = '' # Release to root 25 | p.rdoc_pattern = /^(lib\/front_end_architect|bin|ext)|txt$/ 26 | end 27 | 28 | task :update_gemspec do 29 | File.open("#{hoe.name}.gemspec", 'w') do |gemspec| 30 | gemspec << hoe.spec.to_ruby 31 | end 32 | end 33 | 34 | task :debug_changes do 35 | puts hoe.changes 36 | end 37 | -------------------------------------------------------------------------------- /lib/yui/LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2008, Yahoo! Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use of this software in source and binary forms, with or without modification, are 7 | permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above 10 | copyright notice, this list of conditions and the 11 | following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the 15 | following disclaimer in the documentation and/or other 16 | materials provided with the distribution. 17 | 18 | * Neither the name of Yahoo! Inc. nor the names of its 19 | contributors may be used to endorse or promote products 20 | derived from this software without specific prior 21 | written permission of Yahoo! Inc. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 24 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 29 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /blender.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{blender} 5 | s.version = "0.24" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Blake Elshire", "Chris Griego"] 9 | s.date = %q{2008-12-08} 10 | s.default_executable = %q{blend} 11 | s.description = %q{Blender is like ant or make for the front-end. It aggregates and compresses CSS and/or JavaScript assets for a site into efficient, production-ready files.} 12 | s.email = %q{blender@front-end-architect.com} 13 | s.executables = ["blend"] 14 | s.extra_rdoc_files = ["History.txt", "License.txt", "Manifest.txt", "README.txt"] 15 | s.files = ["History.txt", "License.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/blend", "lib/front_end_architect/blender.rb", "lib/front_end_architect/hash.rb", "lib/yui/LICENSE", "lib/yui/yuicompressor.jar"] 16 | s.has_rdoc = true 17 | s.homepage = %q{http://www.front-end-architect.com/blender/} 18 | s.rdoc_options = ["--main", "README.txt"] 19 | s.require_paths = ["lib"] 20 | s.requirements = ["Java, v1.4 or greater"] 21 | s.rubyforge_project = %q{blender} 22 | s.rubygems_version = %q{1.3.1} 23 | s.summary = %q{Blender outputs efficient, production-ready CSS and/or JavaScript assets.} 24 | 25 | if s.respond_to? :specification_version then 26 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 27 | s.specification_version = 2 28 | 29 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 30 | s.add_runtime_dependency(%q, [">= 1.15"]) 31 | s.add_runtime_dependency(%q, [">= 1.1"]) 32 | s.add_development_dependency(%q, [">= 1.8.0"]) 33 | else 34 | s.add_dependency(%q, [">= 1.15"]) 35 | s.add_dependency(%q, [">= 1.1"]) 36 | s.add_dependency(%q, [">= 1.8.0"]) 37 | end 38 | else 39 | s.add_dependency(%q, [">= 1.15"]) 40 | s.add_dependency(%q, [">= 1.1"]) 41 | s.add_dependency(%q, [">= 1.8.0"]) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /bin/blend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (c) 2008 Blake Elshire, Chris Griego 4 | # 5 | # Blender is freely distributable under the terms of an MIT-style license. 6 | # For details, see http://www.opensource.org/licenses/mit-license.php 7 | 8 | $:.unshift File.join(File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__), *%w[.. lib]) 9 | require 'optparse' 10 | require 'front_end_architect/blender' 11 | 12 | options = {} 13 | 14 | def section(opts, title) 15 | opts.separator '' 16 | opts.separator title + ':' 17 | end 18 | 19 | opts = OptionParser.new do |opts| 20 | version = "Front-End Blender v#{FrontEndArchitect::Blender::VERSION}" 21 | 22 | opts.on('-g', '--generate', String, "Generate a stub Blendfile") { options[:generate] = true } 23 | opts.on('-f FILE', '--file FILE', String, "Use specified Blendfile") {|f| options[:blendfile] = f; options[:root] = File.dirname(f) } 24 | opts.on('-r ROOT', '--root ROOT', String, "Specify the path to the web root directory") {|r| options[:root] = r } 25 | opts.on('-t TYPE', '--type TYPE', [:css, :js], "Select file type to blend (css, js)") {|t| options[:file_type] = t } 26 | opts.on('-m [MINIFIER]', '--min [MINIFIER]', [:yui, :none], "Select minifier to use (yui, none)") {|m| options[:min] = m.nil? ? :none : m.to_sym } 27 | opts.on('-c [BUSTER]', '--cache-buster [BUSTER]', String, "Add cache busters to URLs in CSS") {|b| options[:cache_buster] = b.nil? ? :mtime : b } 28 | opts.on( '--force', String, "Don't allow output files to be skipped") { options[:force] = true } 29 | opts.on( '--yui=YUIOPTS', String, "Pass arguments to YUI Compressor") {|o| options[:yuiopts] = o } 30 | section opts, 'Experimental' 31 | opts.on('-d', '--data', String, "Convert url(file.ext) to url(data:) in CSS") { options[:data] = true } 32 | opts.on('-z', '--gzip', String, "Additionally generate gzipped output files") { options[:gzip] = true } 33 | section opts, 'Meta' 34 | opts.on('-h', '--help', "Show this message") { puts opts; exit 0 } 35 | opts.on('-V', '--version', "Show the version number") { puts version; exit 0 } 36 | 37 | opts.parse!(ARGV) rescue return false 38 | end 39 | 40 | begin 41 | blender = FrontEndArchitect::Blender.new(options) 42 | 43 | if (options[:generate]) 44 | blender.generate 45 | else 46 | blender.blend 47 | end 48 | rescue Exception => e 49 | puts e 50 | exit 1 51 | end 52 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | = Front-End Blender 2 | 3 | == What is Blender? 4 | 5 | Blender is like ant or make for the front-end. It aggregates and compresses 6 | CSS and/or JavaScript assets for a site into efficient, production-ready files. 7 | 8 | == The Blendfile 9 | 10 | The Blendfile, named Blendfile.yaml by default, is the configuration file 11 | that tells Blender which source files are combined into which output files. 12 | The file uses the YAML format. The output file is listed as hash key and 13 | source files are the hash values as an array. Here is a sample Blendfile: 14 | 15 | # Blendfile.yaml for www.boldpx.com 16 | _behavior: 17 | _global-min.js: 18 | - ../_vendor/jquery/jquery.js 19 | - ../_vendor/shadowbox/src/js/adapter/shadowbox-jquery.js 20 | - ../_vendor/shadowbox/src/js/shadowbox.js 21 | - _global.js 22 | - _analytics.js 23 | - ../vendor/google-analytics/ga.js 24 | _style: 25 | _global: 26 | min.css: 27 | - ../../_vendor/shadowbox/src/css/shadowbox.css 28 | - typography.css 29 | - typography-print.css 30 | - colors.css 31 | - colors-print.css 32 | - layout-screen.css 33 | - layout-print.css 34 | 35 | == Usage 36 | 37 | Usage: blend [options] 38 | -g, --generate Generate a stub Blendfile 39 | -f, --file FILE Use specified Blendfile 40 | -r, --root ROOT Specify the path to the web root directory 41 | -t, --type TYPE Select file type to blend (css, js) 42 | -m, --min [MINIFIER] Select minifier to use (yui, none) 43 | -c, --cache-buster [BUSTER] Add cache busters to URLs in CSS 44 | --force Don't allow output files to be skipped 45 | --yui=YUIOPTS Pass arguments to YUI Compressor 46 | 47 | Experimental: 48 | -d, --data Convert url(file.ext) to url(data:) in CSS 49 | -z, --gzip Additionally generate gzipped output files 50 | 51 | Meta: 52 | -h, --help Show this message 53 | -V, --version Show the version number 54 | 55 | == Examples 56 | 57 | In your site directory run 'blend' to minify CSS and JavaScript. 58 | blend 59 | 60 | Other examples: 61 | blend --generate 62 | blend --yui='--preserve-semi' 63 | blend -t css 64 | blend -t css -d 65 | blend -f public/Blendfile.yaml 66 | 67 | == Installation 68 | 69 | To install the RubyGem, run the following at the command line (you may need to use a command such as su or sudo): 70 | gem install blender 71 | 72 | If you're using Windows, you'll also want to run the following to get colored command line output: 73 | gem install win32console 74 | 75 | * Java[http://java.com/en/] v1.4 or greater is required 76 | * Ruby[http://www.ruby-lang.org/en/downloads/] v1.8.6 or greater is required 77 | * RubyGems[http://rubygems.org/read/chapter/3] v1.2 or greater is recommended 78 | 79 | == License 80 | 81 | Copyright (c) 2008 Blake Elshire, Chris Griego 82 | 83 | Blender is freely distributable under the terms of an MIT-style license. 84 | For details, see http://www.opensource.org/licenses/mit-license.php 85 | -------------------------------------------------------------------------------- /lib/front_end_architect/blender.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008 Blake Elshire, Chris Griego 2 | # 3 | # Blender is freely distributable under the terms of an MIT-style license. 4 | # For details, see http://www.opensource.org/licenses/mit-license.php 5 | 6 | $:.unshift File.join(File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__), *%w[..]) 7 | 8 | require 'rubygems' 9 | 10 | require 'base64' 11 | require 'benchmark' 12 | require 'colored' unless PLATFORM =~ /win32/ && !Gem.available?('win32console') 13 | require 'find' 14 | require 'mime/types' 15 | require 'pathname' 16 | require 'zlib' 17 | require 'yaml' 18 | 19 | require 'front_end_architect/hash' 20 | 21 | module FrontEndArchitect 22 | class Blender 23 | VERSION = '0.24' 24 | 25 | ALPHA_REGEX = /(-ms-)?filter:\s*(['"]?)progid:DXImageTransform\.Microsoft\.AlphaImageLoader\(\s*src=(['"])([^\?'"\)]+)(\?(?:[^'"\)]+)?)?\3,\s*sizingMethod=(['"])(image|scale|crop)\6\s*\)\2/im 26 | IMPORT_REGEX = /@import(?: url\(| )(['"]?)([^\?'"\)\s]+)(\?(?:[^'"\)]+)?)?\1\)?(?:[^?;]+)?;/im # shouldn't the semicolon be optional? 27 | URL_REGEX = /url\((['"]?)([^\?'"\)]+)(\?(?:[^'"\)]+)?)?\1?\)/im 28 | 29 | DEFAULT_OPTIONS = { 30 | :blendfile => 'Blendfile.yaml', 31 | :data => false, 32 | :force => false, 33 | :root => File.dirname('Blendfile.yaml'), 34 | :min => :yui, 35 | :colored => (Object.const_defined? :Colored), 36 | } 37 | 38 | def initialize(opts) 39 | @options = DEFAULT_OPTIONS.merge(opts) 40 | end 41 | 42 | def blend 43 | elapsed = Benchmark.realtime do 44 | unless File.exists? @options[:blendfile] 45 | raise "Couldn't find '#{@options[:blendfile]}'" 46 | end 47 | 48 | blendfile_mtime = File.mtime(@options[:blendfile]) 49 | blender = YAML::load_file @options[:blendfile] 50 | 51 | Dir.chdir(File.dirname(@options[:blendfile])) 52 | 53 | blender = flatten_blendfile(blender) 54 | 55 | blender.each do |output_name, sources| 56 | output_name = Pathname.new(output_name).cleanpath.to_s 57 | 58 | output_new = false 59 | gzip_output_name = output_name + '.gz' 60 | 61 | # Checks the type flag and if the current file meets the type requirements continues 62 | if output_name.match '.' + @options[:file_type].to_s 63 | file_type = output_name.match(/\.css/) ? 'css' : 'js' 64 | 65 | # Checks if output file exists and checks the mtimes of the source files to the output file if new creates a new file 66 | if File.exists?(output_name) && (!@options[:gzip] || File.exists?(gzip_output_name)) 67 | output_files = [] 68 | output_files << File.mtime(output_name) 69 | output_files << File.mtime(gzip_output_name) if @options[:gzip] && File.exists?(gzip_output_name) 70 | 71 | oldest_output = output_files.sort.first 72 | 73 | if blendfile_mtime > oldest_output 74 | output_new = true 75 | else 76 | sources.each do |i| 77 | if File.mtime(i) > oldest_output 78 | output_new = true 79 | break 80 | end 81 | end 82 | end 83 | 84 | if output_new || @options[:force] 85 | if File.writable?(output_name) && !(@options[:gzip] && !File.writable?(gzip_output_name)) 86 | create_output(output_name, sources, file_type) 87 | else 88 | puts_colored 'Permission Denied:' + ' ' + output_name, :red 89 | puts_colored 'Permission Denied:' + ' ' + gzip_output_name, :red if @options[:gzip] 90 | end 91 | else 92 | puts_colored 'Skipping: ' + output_name, :yellow 93 | puts_colored 'Skipping: ' + gzip_output_name, :yellow if @options[:gzip] 94 | end 95 | else 96 | create_output(output_name, sources, file_type) 97 | end 98 | end 99 | end 100 | end 101 | 102 | puts sprintf('%.5f', elapsed) + ' seconds' 103 | end 104 | 105 | def generate 106 | if File.exists?(@options[:blendfile]) && !@options[:force] 107 | raise "'#{@options[:blendfile]}' already exists" 108 | end 109 | 110 | blend_files = Hash.new 111 | 112 | Find.find(Dir.getwd) do |f| 113 | basename = File.basename(f) 114 | 115 | if FileTest.directory?(f) && (basename[0] == ?. || basename.match(/^(yui|tinymce|dojo|wp-includes|wp-admin|mint)$/i) || (File.basename(f).downcase == 'rails' && File.basename(File.dirname(f)).downcase == 'vendor')) 116 | Find.prune 117 | elsif !(basename.match(/(^|[-.])(pack|min)\.(css|js)$/i) || basename.match(/^(sifr\.js|ext\.js|mootools.*\.js)$/i)) 118 | # TODO Check file contents instead of name for minification (port YSlow's isMinified) 119 | f.gsub!(Dir.getwd.to_s + '/', '') 120 | 121 | if File.extname(f).downcase == '.css' || File.extname(f).downcase == '.js' 122 | min_file = basename.sub(/\.(css|js)$/i, '-min.\1') 123 | path = File.dirname(f).split('/') # File::dirname depends on / 124 | 125 | path.push min_file 126 | path.push [basename] 127 | 128 | h = path.reverse.inject { |m,v| { v => m } } 129 | 130 | blend_files.deep_merge!(h).inspect 131 | end 132 | end 133 | end 134 | 135 | File.open(@options[:blendfile], 'w') do |blendfile| 136 | blendfile << blend_files.to_yaml 137 | end 138 | end 139 | 140 | protected 141 | 142 | def puts_colored(output, color) 143 | if @options[:colored] 144 | puts Colored.colorize(output, { :foreground => color }) 145 | else 146 | puts output 147 | end 148 | end 149 | 150 | def flatten_blendfile(value, key=nil, context=[]) 151 | if value.is_a? Hash 152 | context << key unless key.nil? 153 | 154 | new_hash = {} 155 | 156 | value.each do |k, v| 157 | new_hash.merge! flatten_blendfile(v, k, context.dup) 158 | end 159 | 160 | new_hash 161 | else 162 | prefix = context.join(File::SEPARATOR) 163 | prefix += File::SEPARATOR unless context.empty? 164 | 165 | value.each_index do |i| 166 | unless value[i].match(/^(\/[^\/]+.+)$/) 167 | value[i] = prefix + value[i] 168 | else 169 | value[i] = @options[:root] + value[i] 170 | end 171 | end 172 | 173 | return { (prefix + key) => value } 174 | end 175 | end 176 | 177 | def create_output(output_name, sources, type) 178 | output = '' 179 | 180 | File.open(output_name, 'w') do |output_file| 181 | # Determine full path of the output file 182 | output_path = Pathname.new(File.expand_path(File.dirname(output_name))) 183 | imports = '' 184 | 185 | sources.each do |i| 186 | if File.extname(i).downcase == '.css' 187 | processed_output, processed_imports = process_css(i, output_path) 188 | 189 | output << processed_output 190 | imports << processed_imports 191 | else 192 | output << IO.read(i) 193 | end 194 | end 195 | 196 | if File.extname(output_name).downcase == '.css' && !imports.empty? 197 | output.insert(0, imports) 198 | end 199 | 200 | # Compress 201 | if @options[:min] == :yui 202 | libdir = File.join(File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__), *%w[.. .. lib]) 203 | 204 | IO.popen("java -jar #{libdir}/yui/yuicompressor.jar #{@options[:yuiopts]} --type #{type}", 'r+') do |io| 205 | io.write output 206 | io.close_write 207 | 208 | output = io.read 209 | 210 | if File.extname(output_name) == '.css' 211 | output.gsub! ' and(', ' and (' # Workaround for YUI Compressor Bug #1938329 212 | output.gsub! '*/;}', '*/}' # Workaround for YUI Compressor Bug #1961175 213 | end 214 | end 215 | 216 | if $? == 32512 # command not found 217 | raise "\nBlender requires Java, v1.4 or greater, to be installed for YUI Compressor" 218 | end 219 | end 220 | 221 | # Data 222 | if @options[:data] 223 | if File.extname(output_name).downcase == '.css' 224 | output = output.gsub(URL_REGEX) do 225 | url = $2 226 | query = $3 227 | 228 | unless url.downcase.include?('.css') 229 | mime_type = MIME::Types.type_for(url) 230 | url_contents = make_data_uri(IO.read(url), mime_type[0]) 231 | else 232 | url_contents = url 233 | end 234 | %Q!url(#{url_contents})! 235 | end 236 | end 237 | end 238 | 239 | output_file << output 240 | end 241 | 242 | puts_colored output_name, :green 243 | 244 | if @options[:gzip] 245 | output_gzip = output_name + '.gz' 246 | 247 | Zlib::GzipWriter.open(output_gzip) do |gz| 248 | gz.write(output) 249 | end 250 | 251 | puts_colored output_gzip, :green 252 | end 253 | end 254 | 255 | def process_css(input_file, output_path) 256 | # TODO Move this to a seperate class and clean it up A LOT. For 2.0 257 | 258 | # Determine full path of input file 259 | input_path = Pathname.new(File.dirname(input_file)) 260 | input = IO.read(input_file) 261 | found_imports = '' 262 | 263 | # Find filter statements and append cache busters to URLs 264 | if @options[:cache_buster] 265 | input = input.gsub(ALPHA_REGEX) do |alpha| 266 | prefix = $1 267 | outter_quote = $2 268 | inner_quote1 = $3 269 | url = $4 270 | query = $5 271 | inner_quote2 = $6 272 | sizing = $7 273 | 274 | # TODO Rewrite to root relative (if :root specified?) 275 | 276 | unless url.match(/^(https:\/\/|http:\/\/|\/\/)/i) 277 | full_path = File.expand_path(url, File.dirname(input_file)) 278 | query = make_cache_buster(full_path, query) 279 | end 280 | 281 | "#{prefix}filter:#{outter_quote}progid:DXImageTransform.Microsoft.AlphaImageLoader(src=#{inner_quote1}#{url}#{query}#{inner_quote1},sizingMethod=#{inner_quote2}#{sizing}#{inner_quote2})#{outter_quote}" 282 | end 283 | end 284 | 285 | # Handle @import statements URL rewrite and adding cache busters 286 | input = input.gsub(IMPORT_REGEX) do |import| 287 | url = $2 288 | query = $3 289 | asset_path = Pathname.new(File.expand_path(url, input_path)) 290 | 291 | if url.match(/^(\/[^\/]+.+)$/) 292 | asset_path = Pathname.new(File.join(File.expand_path(@options[:root]), url)) 293 | end 294 | 295 | unless url.match(/^(https:\/\/|http:\/\/|\/\/)/i) 296 | if (output_path != input_path) 297 | new_path = asset_path.relative_path_from(output_path) 298 | 299 | if @options[:cache_buster] 300 | buster = make_cache_buster(asset_path, query) 301 | import.gsub!(url, new_path.to_s + buster) 302 | else 303 | import.gsub!(url, new_path) 304 | end 305 | else 306 | if @options[:cache_buster] 307 | buster = make_cache_buster(asset_path, query) 308 | import.gsub!(url, asset_path.to_s + buster) 309 | end 310 | end 311 | end 312 | 313 | found_imports << import 314 | 315 | %Q!! 316 | end 317 | 318 | if output_path == input_path 319 | if @options[:data] 320 | input = input.gsub(URL_REGEX) do 321 | url = $2 322 | query = $3 323 | 324 | unless url.match(/^(https:\/\/|http:\/\/|\/\/)/i) 325 | new_path = File.expand_path(url, File.dirname(input_file)) 326 | 327 | if url.match(/^(\/[^\/]+.+)$/) 328 | new_path = Pathname.new(File.join(File.expand_path(@options[:root]), url)) 329 | end 330 | 331 | %Q!url(#{new_path})! 332 | else 333 | %Q!url(#{url}#{query})! 334 | end 335 | end 336 | elsif @options[:cache_buster] 337 | input = input.gsub(URL_REGEX) do 338 | url = $2 339 | query = $3 340 | 341 | unless url.match(/^(https:\/\/|http:\/\/|\/\/)/i) 342 | if url.match(/^(\/[^\/]+.+)$/) 343 | url = Pathname.new(File.join(File.expand_path(@options[:root]), url)) 344 | end 345 | 346 | if @options[:cache_buster] 347 | buster = make_cache_buster(url, query) 348 | new_path = url.to_s + buster 349 | end 350 | 351 | %Q!url(#{new_path})! 352 | else 353 | %Q!url(#{url}#{query})! 354 | end 355 | end 356 | end 357 | 358 | return input, found_imports 359 | else 360 | # Find all url(.ext) in file and rewrite relative url from output directory. 361 | input = input.gsub(URL_REGEX) do 362 | url = $2 363 | query = $3 364 | 365 | unless url.match(/^(https:\/\/|http:\/\/|\/\/)/i) 366 | if @options[:data] 367 | # if doing data conversion rewrite url as an absolute path 368 | new_path = File.expand_path(url, File.dirname(input_file)) 369 | 370 | if url.match(/^(\/[^\/]+.+)$/) 371 | new_path = Pathname.new(File.join(File.expand_path(@options[:root]), url)) 372 | end 373 | else 374 | asset_path = Pathname.new(File.expand_path(url, File.dirname(input_file))) 375 | 376 | if url.match(/^(\/[^\/]+.+)$/) 377 | asset_path = Pathname.new(File.join(File.expand_path(@options[:root]), url)) 378 | end 379 | 380 | new_path = asset_path.relative_path_from(output_path) 381 | 382 | if @options[:cache_buster] 383 | buster = make_cache_buster(asset_path, query) 384 | new_path = new_path.to_s + buster 385 | else 386 | new_path = new_path.to_s + query unless query.nil? 387 | end 388 | end 389 | 390 | %Q!url(#{new_path})! 391 | else 392 | %Q!url(#{url}#{query})! 393 | end 394 | end 395 | 396 | return input, found_imports 397 | end 398 | end 399 | 400 | def make_cache_buster(asset_path, query) 401 | unless query.nil? 402 | query += '&' 403 | else 404 | query = '?' 405 | end 406 | 407 | if @options[:cache_buster] == :mtime 408 | file_mtime = File.mtime(asset_path).to_i 409 | buster = query + file_mtime.to_s 410 | else 411 | buster = query + @options[:cache_buster] 412 | end 413 | 414 | return buster 415 | end 416 | 417 | def make_data_uri(content, content_type) 418 | "data:#{content_type};base64,#{Base64.encode64(content)}".gsub("\n", '') 419 | end 420 | end 421 | end 422 | --------------------------------------------------------------------------------