require 'spiderfw/content_utils' module Spider class Layout < Template # allow_blocks :HTML, :Text, :Render, :Yield, :If, :TagIf, :Each, :Pass, :Widget attr_accessor :template attr_accessor :asset_set def init(scene) super @template = @template.is_a?(Template) ? @template : Template.new(@template) @template.init(scene) unless @template.init_done? end def render(*args) $PUB_URL = Spider::HomeController.pub_url prepare_assets unless @assets_prepared super end def only_asset_profiles(*profiles) @only_asset_profiles = profiles end def no_asset_profiles(*profiles) @no_asset_profiles = profiles end def prepare_assets @template_assets = { :css => [], :js => [] } assets = {:css => [], :js => []} seen = {} js_messages = [] use_cdn = Spider.conf.get('assets.use_cdn') compress_assets = {:js => {}, :css => {}} cname = File.basename(@path, '.layout.shtml') cname = File.basename(cname, '.shtml') cname += "-#{@asset_set}" if @asset_set pub_dest = nil all_assets.each do |ass| seen_check = ass[:runtime] || ass[:src] next if ass[:src].blank? && !ass[:runtime] next if seen[seen_check] seen[seen_check] = true type = ass[:type].to_sym ass = compile_asset(ass) compress_config = case type when :js 'javascript.compress' when :css 'css.compress' end no_compress = @scene.__is_error_page || !Spider.conf.get(compress_config) || \ ass[:runtime] || ass[:if_ie_lte] || ass[:media] || (use_cdn && ass[:cdn]) if no_compress if ass[:runtime] assets[type] << {:src => Spider::Template.runtime_assets[ass[:runtime]].call(@request, @response, @scene)} else assets[type] << ass end else unless pub_dest pub_dest = self.class.compiled_folder_path FileUtils.mkdir_p(pub_dest) end if comp = ass[:compressed_path] # Already compressed assets name = File.basename(comp) if ass[:compressed_rel_path] # Keeps the compressed files in a subdir dir = File.dirname(ass[:compressed_rel_path]) if ass[:copy_dir] # Copies the source dir (which may contain resources used by the assets) start = dir if ass[:copy_dir].is_a?(Fixnum) # How many levels to go up ass[:copy_dir].downto(0) do |i| start = File.dirname(start) end end dst_dir = File.join(pub_dest, start) unless File.dirname(start) == '.' || File.directory?(File.dirname(dst_dir)) FileUtils.mkdir_p(File.join(pub_dest, File.dirname(dst_dir))) end unless File.directory?(dst_dir) FileUtils.cp_r(File.join(ass[:app].pub_path, start), dst_dir) end else FileUtils.mkdir_p(File.join(pub_dest, dir)) File.cp(comp, File.join(pub_dest, dir)) unless File.exist?(File.join(pub_dest, dir)) end src = dir+'/'+name else unless File.exist?(File.join(pub_dest, name)) File.cp(comp, pub_dest) end src = name end ass[:src] = Spider::HomeController.pub_url+'/'+COMPILED_FOLDER+'/'+src assets[type] << ass else # needs compression name = ass[:compress] || cname unless compress_assets[type][name] cpr = {:name => name, :assets => [], :cpr => true} assets[type] << cpr compress_assets[type][name] = cpr end compress_assets[type][name][:assets] << ass end end if ass[:gettext] && type == :js msg_path = asset_gettext_messages_file(ass[:path]) if File.exists?(msg_path) js_messages += JSON.parse(File.read(msg_path)) else Spider.logger.warn("Javascript Gettext file #{msg_path} not found") end end end assets[:js].each do |ass| if ass[:cpr] compressed = compress_javascript(ass) @template_assets[:js] << Spider::HomeController.pub_url+'/'+COMPILED_FOLDER+'/'+compressed else ass[:src] = ass[:cdn] if ass[:cdn] && use_cdn @template_assets[:js] << ass[:src] end end assets[:css].each do |ass| if ass[:cpr] compressed = compress_css(ass) @template_assets[:css] << Spider::HomeController.pub_url+'/'+COMPILED_FOLDER+'/'+compressed else ass[:src] = ass[:cdn] if ass[:cdn] && use_cdn is_dyn = ass[:if_ie_lte] || ass[:media] @template_assets[:css] << (is_dyn ? ass : ass[:src]) end end @content[:yield_to] = @template @scene.assets = @template_assets @scene.extend(LayoutScene) if js_messages.empty? @scene.js_translations = "" else translations = {} js_messages.each{ |msg| translations[msg] = _(msg) } @scene.js_translations = "var translations = #{translations.to_json}" end @assets_prepared = true end @@named_layouts = {} class << self def register_layout(name, file) @@named_layouts[name] = file end def named_layouts @@named_layouts end end def all_assets assets = @template.assets + self.assets if @only_asset_profiles assets = assets.select{ |ass| ass[:profiles] && !(ass[:profiles] & @only_asset_profiles).empty? } end if @no_asset_profiles assets = assets.select{ |ass| !ass[:profiles] || (ass[:profiles] & @no_asset_profiles).empty? } end assets end COMPILED_FOLDER = '_c' def self.compiled_folder_path File.join(Spider::HomeController.pub_path, COMPILED_FOLDER) end def asset_gettext_messages_file(path) dir = File.dirname(path) name = File.basename(path, '.*') File.join(dir, "#{name}.i18n.json") end def compile_asset(ass) return ass unless ass[:src] if ass[:type] == :css ext = File.extname(ass[:path]) if ['.scss', '.sass'].include?(ext) dir = File.dirname(ass[:path]) base = File.basename(ass[:path], ext) newname = "#{base}.css" tmpdestdir = File.join(dir, 'stylesheets') dest = File.join(tmpdestdir, newname) require 'spiderfw/templates/resources/sass' Spider::SassCompiler.compile(ass[:path], dest) ass[:path] = dest ass[:src] = File.join(File.dirname(ass[:src]), newname) end end return ass end def compress_javascript(cpr) require 'yui/compressor' pub_dest = self.class.compiled_folder_path name = cpr[:name] already_compressed = Dir.glob(pub_dest+'/'+name+'.*.js') unless already_compressed.empty? return File.basename(already_compressed.first) end tmp_combined = Spider.paths[:tmp]+'/_'+name+'.js' File.open(tmp_combined, 'w') do |f| cpr[:assets].each.each do |a| f.write IO.read(a[:path])+"\n" end end version = 0 curr = Dir.glob(pub_dest+"/._#{name}.*.js") unless curr.empty? curr.each do |f| currname = File.basename(f) if currname =~ /(\d+)\.js$/ version = $1.to_i if $1.to_i > version File.unlink(f) end end end version += 1 compiled_name = "#{name}.#{version}.js" combined = "#{pub_dest}/._#{compiled_name}" dest = "#{pub_dest}/#{compiled_name}" File.cp(tmp_combined, combined) File.unlink(tmp_combined) compressor = ::YUI::JavaScriptCompressor.new("charset" => "UTF-8") io = open(combined, 'r') cjs = compressor.compress(io) open(dest, 'w') do |f| f << cjs end return compiled_name end def compress_css(cpr) require 'yui/compressor' pub_dest = self.class.compiled_folder_path name = cpr[:name] already_compressed = Dir.glob(pub_dest+'/'+name+'.*.css') unless already_compressed.empty? return File.basename(already_compressed.first) end tmp_combined = Spider.paths[:tmp]+'/_'+name+'.css' File.open(tmp_combined, 'w') do |f| cpr[:assets].each do |a| path = a[:path] src_dir = File.dirname(path) app = a[:app] if app app_relative_path = a[:app].relative_path app_path = app.path elsif path.index(Spider::SpiderController.pub_path) == 0 app_relative_path = 'spider' app_path = Spider::SpiderController.pub_path end app_pathname = Pathname.new(app_path) pub_app = "#{pub_dest}/#{app_relative_path}" FileUtils.mkdir_p(pub_app) src_files = Spider::ContentUtils.resolve_css_includes(path) src = "" src_files.each do |src_file| src += IO.read(src_file)+"\n" end src.gsub!(/^\s*@import(?:\surl\(|\s)(['"]?)([^\?'"\)\s]+)(\?(?:[^'"\)]*))?\1\)?(?:[^?;]*);?/i, "") src.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).uniq.collect do |url| url = url.first next if url =~ %r{^/} || url =~ %r{^[a-z]+://} path = "" url_src = File.expand_path(File.join(src_dir, url)) src_pathname = Pathname.new(url_src) src_rel = nil begin src_rel = src_pathname.relative_path_from(app_pathname) rescue ArgumentError raise "Can't combine CSS if paths go outside app: #{url} in #{path}" end url_dest = File.join(pub_app, src_rel.to_s) FileUtils.mkdir_p(File.dirname(url_dest)) cachebuster = Spider.conf.get('css.cachebuster') new_url = "#{app_relative_path}/#{src_rel}" if File.exist?(url_src) mtime = File.mtime(url_src).to_i if cachebuster && File.exist?(url_dest) && mtime > File.mtime(url_dest).to_i if cachebuster == :soft File.cp(url_src, url_dest) new_url += "?cb=#{mtime}" elsif cachebuster == :hard || cachebuster == :hardcopy url_dir = File.dirname(url) url_ext = File.extname(url) url_basename = File.basename(url, url_ext) url_dest_dir = File.dirname(url_dest) cb_file_name = "#{url_basename}-cb#{mtime}#{url_ext}" new_url = "#{url_dir}/#{cb_file_name}" if cachebuster == :hard File.cp(url_src, url_dest) else File.cp(url_src, "#{url_dest_dir}/#{cb_file_name}") end end else File.cp(url_src, url_dest) end else Spider.logger.error("CSS referenced file not found: #{url_src}") end src.gsub!(/\([\s"']*#{url}[\s"']*\)/m, "(#{new_url})") end f.write(src+"\n") end end version = 0 curr = Dir.glob(pub_dest+"/._#{name}.*.css") unless curr.empty? curr.each do |f| currname = File.basename(f) if currname =~ /(\d+)\.js$/ version = $1.to_i if $1.to_i > version File.unlink(f) end end end version += 1 compiled_name = "#{name}.#{version}.css" combined = "#{pub_dest}/._#{compiled_name}" dest = "#{pub_dest}/#{compiled_name}" File.cp(tmp_combined, combined) File.unlink(tmp_combined) compressor = ::YUI::CssCompressor.new("charset" => "UTF-8") io = open(combined, 'r') cjs = compressor.compress(io) open(dest, 'w') do |f| f << cjs end return compiled_name end def self.clear_compiled_folder! FileUtils.rm_rf(Dir.glob(File.join(self.compiled_folder_path, '*'))) end end module LayoutScene def output_meta(type=nil) type ||= :default case type when :default $out << "" end end def output_assets(type=nil) types = type ? [type] : self.assets.keys use_cdn = Spider.conf.get('assets.use_cdn') if types.include?(:js) self.assets[:js].each do |ass| ass = {:src => ass} if ass.is_a?(String) $out << "\n" end unless @not_first_js $out << "" end end if types.include?(:css) self.assets[:css].each do |ass| ass = {:src => ass} if ass.is_a?(String) link = "