| OLD | NEW |
| (Empty) | |
| 1 # |
| 2 # Jekyll Asset Bundler |
| 3 # |
| 4 # Author : Colin Kennedy |
| 5 # Repo : http://github.com/moshen/jekyll-asset_bundler |
| 6 # License: MIT, see LICENSE file |
| 7 # |
| 8 |
| 9 require 'yaml' |
| 10 require 'digest/md5' |
| 11 require 'net/http' |
| 12 require 'uri' |
| 13 |
| 14 module Jekyll |
| 15 |
| 16 class BundleTag < Liquid::Block |
| 17 @@supported_types = ['js', 'css'] |
| 18 |
| 19 def initialize(tag_name, text, tokens) |
| 20 super |
| 21 @text = text |
| 22 @files = {} |
| 23 end |
| 24 |
| 25 def render(context) |
| 26 src = context.registers[:site].source |
| 27 raw_markup = super(context) |
| 28 begin |
| 29 @assets = YAML::load(raw_markup) |
| 30 rescue |
| 31 puts <<-END |
| 32 Asset Bundler - Error: Problem parsing a YAML bundle |
| 33 #{raw_markup} |
| 34 |
| 35 #{$!} |
| 36 END |
| 37 end |
| 38 |
| 39 if @assets.class != Array |
| 40 puts "Asset Bundler - Error: YAML bundle is not an Array\n#{raw_markup}" |
| 41 @assets = [] |
| 42 end |
| 43 |
| 44 add_files_from_list(src, @assets) |
| 45 |
| 46 markup = "" |
| 47 |
| 48 @files.each {|k,v| |
| 49 markup.concat(Bundle.new(v, k, context).markup()) |
| 50 } |
| 51 |
| 52 markup |
| 53 end |
| 54 |
| 55 def add_files_from_list(src, list) |
| 56 list.each {|a| |
| 57 path = File.join(src, a) |
| 58 if (File.basename(a) !~ /^\.+/ and File.file?(path)) or a =~ /^(https?:)
?\/\//i |
| 59 add_file_by_type(a) |
| 60 else |
| 61 puts "Asset Bundler Error - File: #{path} not found, ignoring..." |
| 62 end |
| 63 } |
| 64 end |
| 65 |
| 66 def add_file_by_type(file) |
| 67 if file =~ /\.([^\.]+)$/ |
| 68 type = $1.downcase() |
| 69 return if @@supported_types.index(type).nil? |
| 70 if !@files.key?(type) |
| 71 @files[type] = [] |
| 72 end |
| 73 |
| 74 @files[type].push(file) |
| 75 end |
| 76 end |
| 77 |
| 78 end |
| 79 |
| 80 class BundleGlobTag < BundleTag |
| 81 def add_files_from_list(src, list) |
| 82 list.each {|a| |
| 83 Dir.glob(File.join(src, a)) {|f| |
| 84 if f !~ /^\.+/ and File.file?(f) |
| 85 add_file_by_type(f.sub(src,'')) |
| 86 end |
| 87 } |
| 88 } |
| 89 end |
| 90 |
| 91 end |
| 92 |
| 93 class DevAssetsTag < BundleTag |
| 94 def render(context) |
| 95 if Bundle.config(context)['dev'] |
| 96 super(context) |
| 97 else |
| 98 '' |
| 99 end |
| 100 end |
| 101 |
| 102 def add_files_from_list(src, list) |
| 103 list.each {|a| |
| 104 add_file_by_type(a) |
| 105 } |
| 106 end |
| 107 end |
| 108 |
| 109 class Bundle |
| 110 @@bundles = {} |
| 111 @@default_config = { |
| 112 'compile' => { 'coffee' => false, 'less' => false }, |
| 113 'compress' => { 'js' => false, 'css' => false }, |
| 114 'base_path' => '/bundles/', |
| 115 'remove_bundled' => false, |
| 116 'dev' => false |
| 117 } |
| 118 attr_reader :content, :hash, :filename, :base |
| 119 |
| 120 def initialize(files, type, context) |
| 121 @files = files |
| 122 @type = type |
| 123 @context = context |
| 124 @content = '' |
| 125 @hash = '' |
| 126 @filename = '' |
| 127 |
| 128 @config = Bundle.config(@context) |
| 129 @base = @config['base_path'] |
| 130 |
| 131 @filename_hash = Digest::MD5.hexdigest(@files.join()) |
| 132 if @@bundles.key?(@filename_hash) |
| 133 @filename = @@bundles[@filename_hash].filename |
| 134 @base = @@bundles[@filename_hash].base |
| 135 else |
| 136 load_content() |
| 137 end |
| 138 end |
| 139 |
| 140 def self.config(context) |
| 141 ret_config = nil |
| 142 if context.registers[:site].config.key?("asset_bundler") |
| 143 ret_config = @@default_config.deep_merge(context.registers[:site].config
["asset_bundler"]) |
| 144 else |
| 145 ret_config = @@default_config |
| 146 end |
| 147 |
| 148 if context.registers[:site].config.key?("dev") |
| 149 ret_config['dev'] = context.registers[:site].config["dev"] ? true : fals
e |
| 150 end |
| 151 |
| 152 if context.registers[:site].config['server'] |
| 153 ret_config['dev'] = true |
| 154 end |
| 155 |
| 156 ret_config |
| 157 end |
| 158 |
| 159 def load_content() |
| 160 if @config['dev'] |
| 161 @@bundles[@filename_hash] = self |
| 162 return |
| 163 end |
| 164 |
| 165 src = @context.registers[:site].source |
| 166 |
| 167 @files.each {|f| |
| 168 if f =~ /^(https?:)?\/\//i |
| 169 # Make all requests via http |
| 170 f = "http:#{f}" if !$1 |
| 171 f.sub!( /^https/i, "http" ) if $1 =~ /^https/i |
| 172 @content.concat(remote_asset_cache(URI(f))) |
| 173 else |
| 174 @content.concat(File.read(File.join(src, f))) |
| 175 end |
| 176 } |
| 177 |
| 178 @hash = Digest::MD5.hexdigest(@content) |
| 179 @filename = "#{@hash}.#{@type}" |
| 180 cache_file = File.join(cache_dir(), @filename) |
| 181 |
| 182 if File.readable?(cache_file) and @config['compress'][@type] |
| 183 @content = File.read(cache_file) |
| 184 elsif @config['compress'][@type] |
| 185 # TODO: Compilation of Less and CoffeeScript would go here |
| 186 compress() |
| 187 File.open(cache_file, "w") {|f| |
| 188 f.write(@content) |
| 189 } |
| 190 end |
| 191 |
| 192 @context.registers[:site].static_files.push(self) |
| 193 remove_bundled() if @config['remove_bundled'] |
| 194 |
| 195 @@bundles[@filename_hash] = self |
| 196 end |
| 197 |
| 198 def cache_dir() |
| 199 cache_dir = File.expand_path( "../_asset_bundler_cache", |
| 200 @context.registers[:site].plugins ) |
| 201 if( !File.directory?(cache_dir) ) |
| 202 FileUtils.mkdir_p(cache_dir) |
| 203 end |
| 204 |
| 205 cache_dir |
| 206 end |
| 207 |
| 208 def remote_asset_cache(uri) |
| 209 cache_file = File.join(cache_dir(), |
| 210 "remote.#{Digest::MD5.hexdigest(uri.to_s)}.#{@type}
") |
| 211 content = "" |
| 212 |
| 213 if File.readable?(cache_file) |
| 214 content = File.read(cache_file) |
| 215 else |
| 216 begin |
| 217 puts "Asset Bundler - Downloading: #{uri.to_s}" |
| 218 content = Net::HTTP.get(uri) |
| 219 File.open(cache_file, "w") {|f| |
| 220 f.write( content ) |
| 221 } |
| 222 rescue |
| 223 puts "Asset Bundler - Error: There was a problem downloading #{f}\n #
{$!}" |
| 224 end |
| 225 end |
| 226 |
| 227 return content |
| 228 end |
| 229 |
| 230 # Removes StaticFiles from the _site if they are bundled |
| 231 # and the remove_bundled option is true |
| 232 # which... it isn't by default |
| 233 def remove_bundled() |
| 234 src = @context.registers[:site].source |
| 235 @files.each {|f| |
| 236 @context.registers[:site].static_files.select! {|s| |
| 237 if s.class == StaticFile |
| 238 s.path != File.join(src, f) |
| 239 else |
| 240 true |
| 241 end |
| 242 } |
| 243 } |
| 244 end |
| 245 |
| 246 def compress() |
| 247 return if @config['dev'] |
| 248 |
| 249 case @config['compress'][@type] |
| 250 when 'yui' |
| 251 compress_yui() |
| 252 when 'closure' |
| 253 compress_closure() |
| 254 else |
| 255 compress_command() |
| 256 end |
| 257 end |
| 258 |
| 259 def compress_command() |
| 260 temp_path = cache_dir() |
| 261 command = String.new(@config['compress'][@type]) |
| 262 infile = false |
| 263 outfile = false |
| 264 used_files = [] |
| 265 |
| 266 if command =~ /:infile/ |
| 267 File.open(File.join(temp_path, "infile.#{@filename_hash}.#{@type}"), mod
e="w") {|f| |
| 268 f.write(@content) |
| 269 used_files.push( f.path ) |
| 270 infile = f.path |
| 271 } |
| 272 command.sub!( /:infile/, "\"#{infile.gsub(File::SEPARATOR, |
| 273 File::ALT_SEPARATOR || File::SEPARATOR)}\"") |
| 274 end |
| 275 if command =~ /:outfile/ |
| 276 outfile = File.join(temp_path, "outfile.#{@filename_hash}.#{@type}") |
| 277 used_files.push( outfile ) |
| 278 command.sub!( /:outfile/, "\"#{outfile.gsub(File::SEPARATOR, |
| 279 File::ALT_SEPARATOR || File::SEPARATOR)}\"") |
| 280 end |
| 281 |
| 282 if infile and outfile |
| 283 `#{command}` |
| 284 else |
| 285 mode = "r" |
| 286 mode = "r+" if !infile |
| 287 IO.popen(command, mode) {|i| |
| 288 if !infile |
| 289 i.puts(@content) |
| 290 i.close_write() |
| 291 end |
| 292 @content = i.gets() if !outfile |
| 293 } |
| 294 end |
| 295 |
| 296 if outfile |
| 297 @content = File.read( outfile ) |
| 298 end |
| 299 |
| 300 used_files.each {|f| |
| 301 File.unlink( f ) |
| 302 } |
| 303 end |
| 304 |
| 305 def compress_yui() |
| 306 require 'yui/compressor' |
| 307 case @type |
| 308 when 'js' |
| 309 @content = YUI::JavaScriptCompressor.new.compress(@content) |
| 310 when 'css' |
| 311 @content = YUI::CssCompressor.new.compress(@content) |
| 312 end |
| 313 end |
| 314 |
| 315 def compress_closure() |
| 316 require 'closure-compiler' |
| 317 case @type |
| 318 when 'js' |
| 319 @content = Closure::Compiler.new.compile(@content) |
| 320 end |
| 321 end |
| 322 |
| 323 def markup() |
| 324 return dev_markup() if @config['dev'] |
| 325 case @type |
| 326 when 'js' |
| 327 "<script type='text/javascript' src='#{@base}#{@filename}'></script>\n
" |
| 328 when 'coffee' |
| 329 "<script type='text/coffeescript' src='#{@base}#{@filename}'></script>
\n" |
| 330 when 'css' |
| 331 "<link rel='stylesheet' type='text/css' href='#{@base}#{@filename}' />
\n" |
| 332 when 'less' |
| 333 "<link rel='stylesheet/less' type='text/css' href='#{@base}#{@filename
}' />\n" |
| 334 end |
| 335 end |
| 336 |
| 337 def dev_markup() |
| 338 output = '' |
| 339 @files.each {|f| |
| 340 case @type |
| 341 when 'js' |
| 342 output.concat("<script type='text/javascript' src='#{f}'></script>\n
") |
| 343 when 'coffee' |
| 344 output.concat("<script type='text/coffeescript' src='#{f}'></script>
\n") |
| 345 when 'css' |
| 346 output.concat("<link rel='stylesheet' type='text/css' href='#{f}' />
\n") |
| 347 when 'less' |
| 348 output.concat("<link rel='stylesheet/less' type='text/css' href='#{f
}' />\n") |
| 349 end |
| 350 } |
| 351 |
| 352 return output |
| 353 end |
| 354 |
| 355 # Methods required by Jekyll::Site to write out the bundle |
| 356 # This is where we give Jekyll::Bundle a Jekyll::StaticFile |
| 357 # duck call and send it on its way. |
| 358 def destination(dest) |
| 359 File.join(dest, @base, @filename) |
| 360 end |
| 361 |
| 362 def write(dest) |
| 363 dest_path = destination(dest) |
| 364 return false if File.exists?(dest_path) |
| 365 |
| 366 FileUtils.mkdir_p(File.dirname(dest_path)) |
| 367 File.open(dest_path, "w") {|o| |
| 368 o.write(@content) |
| 369 } |
| 370 |
| 371 true |
| 372 end |
| 373 # End o' the duck call |
| 374 |
| 375 end |
| 376 |
| 377 end |
| 378 |
| 379 Liquid::Template.register_tag('bundle' , Jekyll::BundleTag ) |
| 380 Liquid::Template.register_tag('bundle_glob', Jekyll::BundleGlobTag) |
| 381 Liquid::Template.register_tag('dev_assets' , Jekyll::DevAssetsTag ) |
| OLD | NEW |