require "tilt" require 'net/http' require 'uri' class Roda module RodaPlugins module Assets def self.load_dependencies(app, opts={}) app.plugin :render end def self.configure(app, opts={}, &block) if app.opts[:assets] app.opts[:assets].merge!(opts) else app.opts[:assets] = opts.dup end opts = app.opts[:assets] opts[:css] ||= [] opts[:js] ||= [] opts[:js_folder] ||= 'js' opts[:css_folder] ||= 'css' opts[:path] ||= File.expand_path("assets", Dir.pwd) opts[:compiled_path] ||= opts[:path] opts[:compiled_name] ||= 'compiled.roda.assets' opts[:concat_name] ||= 'concat.roda.assets' opts[:route] ||= 'assets' opts[:css_engine] ||= 'scss' opts[:js_engine] ||= 'coffee' opts[:concat] ||= false opts[:compiled] ||= false opts[:headers] ||= {} if opts.fetch(:cache, true) opts[:cache] = app.thread_safe_cache end end module ClassMethods # Copy the assets options into the subclass, duping # them as necessary to prevent changes in the subclass # affecting the parent class. def inherited(subclass) super opts = subclass.opts[:assets] = assets_opts.dup opts[:cache] = thread_safe_cache if opts[:cache] end # Return the assets options for this class. def assets_opts opts[:assets] end # Generates a unique id, this is used to keep concat/compiled files # from caching in the browser when they are generated def assets_unique_id(type) if unique_id = instance_variable_get("@#{type}") unique_id else path = "#{assets_opts[:compiled_path]}/#{assets_opts[:"#{type}_folder"]}" file = "#{path}/#{assets_opts[:compiled_name]}.#{type}" content = File.exist?(file) ? File.read(file) : '' instance_variable_set("@#{type}", Digest::SHA1.hexdigest(content)) end end def compile_assets(concat_only = false) assets_opts[:concat_only] = concat_only %w(css js).each do |type| files = assets_opts[type.to_sym] if files.is_a? Array compile_process_files files, type, type else files.each do |folder, f| compile_process_files f, type, folder end end end end private def compile_process_files(files, type, folder) require 'yuicompressor' # start app instance app = new # content to render to file content = '' files.each do |file| if type != folder && !file[/^\.\//] && !file[/^http/] file = "#{folder}/#{file}" end content += app.read_asset_file file, type end path = assets_opts[:compiled_path] \ + "/#{assets_opts[:"#{type}_folder"]}/" \ + assets_opts[:compiled_name] \ + (type != folder ? ".#{folder}" : '') \ + ".#{type}" unless assets_opts[:concat_only] content = YUICompressor.send("compress_#{type}", content, munge: true) end File.write path, content end end module InstanceMethods # This will ouput the files with the appropriate tags def assets folder, options = {} attrs = options.map{|k,v| "#{k}=\"#{v}\""} tags = [] folder = [folder] unless folder.is_a? Array type = folder.first attr = type.to_s == 'js' ? 'src' : 'href' path = "#{assets_opts[:route]}/#{assets_opts[:"#{type}_folder"]}" files = folder.length == 1 \ ? assets_opts[:"#{folder[0]}"] \ : assets_opts[:"#{folder[0]}"][:"#{folder[1]}"] # Create a tag for each individual file if assets_opts[:compiled] || assets_opts[:concat] name = assets_opts[:compiled] ? assets_opts[:compiled_name] : assets_opts[:concat_name] name = "#{name}/#{folder.join('-')}" # Generate unique url so middleware knows to check for # compile/concat attrs.unshift("#{attr}=\"/#{path}/#{name}/#{assets_unique_id(type)}.#{type}\"") # Return tag string send("#{type}_assets_tag", attrs.join(' ')) else files.each do |file| # This allows you to do things like: # assets_opts[:css] = ['app', './bower/jquery/jquery-min.js'] file.gsub!(/\./, '$2E') # Add tags to the tags array tags << send( "#{type}_assets_tag", attrs.dup.unshift( "#{attr}=\"/#{path}/#{file}.#{type}\"").join(' ') ) end # Return tags as string tags.join "\n" end end def render_asset(file, type) # convert back url safe to period file.gsub!(/(\$2E|%242E)/, '.') if !assets_opts[:compiled] && !assets_opts[:concat] read_asset_file file, type elsif assets_opts[:compiled] folder = file.split('/')[1].split('-', 2) path = assets_opts[:compiled_path] \ + "/#{assets_opts[:"#{type}_folder"]}/" \ + assets_opts[:compiled_name] \ + (folder.length > 1 ? ".#{folder[1]}" : '') \ + ".#{type}" File.read path elsif assets_opts[:concat] # "concat.roda.assets/css/123" content = '' folder = file.split('/')[1].split('-', 2) files = folder.length == 1 \ ? assets_opts[:"#{folder[0]}"] \ : assets_opts[:"#{folder[0]}"][:"#{folder[1]}"] files.each { |f| content += read_asset_file f, type } content end end def read_asset_file(file, type) folder = assets_opts[:"#{type}_folder"] # If there is no file it must be a remote file request. # Lets set the file to the url if file == '' route = assets_opts[:route] file = env['SCRIPT_NAME'].gsub(/^\/#{route}\/#{folder}\//, '') end # If it's not a url or parent direct append the full path if !file[/^\.\//] && !file[/^http/] file = assets_opts[:path] + '/' + folder + "/#{file}" end # set the current engine engine = assets_opts[:"#{type}_engine"] if File.exists? "#{file}.#{engine}" # render via tilt render path: "#{file}.#{engine}" elsif File.exists? "#{file}.#{type}" # read file directly File.read "#{file}.#{type}" elsif file[/^http/] # grab remote file content Net::HTTP.get(URI.parse(file)) elsif File.exists?(file) && !file[/\.#{type}$/] # Render via tilt if the type isn't css or js render path: file else # if it is css/js read the file directly File.read file end end # Shortcut for class opts def assets_opts self.class.assets_opts end # Shortcut for class assets unique id def assets_unique_id(*args) self.class.assets_unique_id(*args) end private # CSS tag template # def css_assets_tag(attrs) "" end # JS tag template # def js_assets_tag(attrs) "" end end module RequestClassMethods # Shortcut for roda class asset opts def assets_opts roda_class.assets_opts end # The regex for the assets route def assets_route_regex Regexp.new( assets_opts[:route] + '/' + "(#{assets_opts[:"css_folder"]}|#{assets_opts[:"js_folder"]})" + '/(.*)(?:\.(css|js)|http.*)$' ) end end module RequestMethods # Handles calls to the assets route def assets on self.class.assets_route_regex do |type, file| content_type = type == 'css' ? 'text/css' : 'application/javascript' response.headers.merge!({ "Content-Type" => content_type + '; charset=UTF-8', }.merge(scope.assets_opts[:headers])) scope.render_asset file, type end end end end register_plugin(:assets, Assets) end end