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