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.home.controller.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 => []} compress_assets = {:js => {}, :css => {}} seen = {} js_translations = {} use_cdn = Spider.conf.get('assets.use_cdn') cname = File.basename(@path, '.layout.shtml') cname = File.basename(cname, '.shtml') cname += "-#{@asset_set}" if @asset_set @cname = cname 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 ass[:app] = Spider.home if ass[:app] == :home ass = compile_asset(ass) res = prepare_asset(ass, compress_assets, js_translations) assets[:css] += res[:css] assets[:js] += res[:js] end if @compile_less == false less = Spider::Template.get_named_asset('less') less.each do |ass| res = prepare_asset(parse_asset(ass[:type], ass[:src], ass).first) assets[:css] += res[:css] assets[:js] += res[:js] end end assets[:js].each do |ass| if ass[:cpr] compressed = compress_javascript(ass) @template_assets[:js] << Spider.home.controller.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.home.controller.pub_url+'/'+COMPILED_FOLDER+'/'+compressed else ass[:src] = ass[:cdn] if ass[:cdn] && use_cdn is_dyn = ass[:if_ie_lte] || ass[:media] || ass[:rel] @template_assets[:css] << (is_dyn ? ass : ass[:src]) end end @content[:yield_to] = @template @scene.assets = @template_assets @scene.extend(LayoutScene) if js_translations.empty? @scene.js_translations = "" else @scene.js_translations = "var translations = #{js_translations.to_json}" end @assets_prepared = true end def prepare_asset(ass, compress_assets={}, js_translations={}) type = ass[:type].to_sym assets = {:css => [], :js => []} pub_dest = nil use_cdn = Spider.conf.get('assets.use_cdn') 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[:no_compress] || 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)) FileUtils.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)) FileUtils.cp(comp, pub_dest) end src = name end ass[:src] = Spider.home.controller.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)) Spider::GetText.in_domain(ass[:app].short_name) do js_messages.each{ |msg| next if js_translations.key?(msg) js_translations[msg] = _(msg) } end else Spider.logger.warn("Javascript Gettext file #{msg_path} not found") end end assets 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 tpl_assets = @template.is_a?(Layout) ? @template.all_assets : @template.assets assets = self.assets + tpl_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.home.controller.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] && ass[:path] if ass[:type] == :css ext = File.extname(ass[:path]) compile_exts = ['.scss', '.sass', '.less'] if compile_exts.include?(ext) ass_type = nil if ext == '.less' ass_type = :less if @compile_less.nil? @compile_less = false if Spider.conf.get('css.compile_less') begin require 'spiderfw/templates/resources/less' @compile_less = true rescue LoadError Spider.logger.error("Unable to compile LESS. Please install less-js gem and a JS backend.") end end end unless @compile_less ass[:rel] = 'stylesheet/less' return ass end elsif ['.scss', '.sass'].include?(ext) ass_type = :sass end dir = File.dirname(ass[:path]) base = File.basename(ass[:path], ext) newname = "#{base}.css" parts = dir.split(File::SEPARATOR) if type_i = parts.index(ass_type.to_s) parts[type_i] = File.join(ass[:type].to_s, ass_type.to_s) destdir = parts.join(File::SEPARATOR) else destdir = dir end FileUtils.mkdir_p(destdir) dest = File.join(destdir, newname) if Spider.conf.get('css.compile') begin compiler_class = if ass_type == :sass require 'spiderfw/templates/resources/sass' Spider::SassCompiler elsif ass_type == :less Spider::LessCompiler end compiler = compiler_class.new(ass[:app].pub_path) compiler.compile(ass[:path], dest) rescue Exception => exc if ext == '.less' msg = "Unable to compile LESS file #{ass[:path]}." msg += "Please ensure you have a JS backend (see https://github.com/sstephenson/execjs)" elsif ext == '.scss' || ext == '.sass' msg = "Unable to compile SASS file #{ass[:path]}." msg += "Please ensure that you have the 'sass' (and optionally 'compass') gems installed." end Spider.logger.error(msg) if Spider.runmode == "production" && File.exist?(dest) Spider.logger.error(exc) else raise end end end ass[:path] = dest srcdir = File.dirname(ass[:src]) if destdir != dir srcdir = srcdir.sub(ass_type.to_s, File.join(ass[:type].to_s, ass_type.to_s)) end ass[:src] = File.join(srcdir, 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}" FileUtils.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 if app.is_a?(Spider::Home) app_relative_path = nil app_path = app.path else app_relative_path = a[:app].relative_path app_path = app.path end 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]+://} url, cb = url.split('?', 2) 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 ? "#{app_relative_path}/#{src_rel}" : src_rel if File.file?(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 FileUtils.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 FileUtils.cp(url_src, url_dest) else FileUtils.cp(url_src, "#{url_dest_dir}/#{cb_file_name}") end end else FileUtils.cp(url_src, url_dest) end else Spider.logger.error("CSS referenced file not found: #{url_src}") end if cb url += "?#{cb}" new_url += "?#{cb}" end src.gsub!(/\([\s"']*#{Regexp.quote(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}" FileUtils.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 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) rel = ass[:rel] || 'stylesheet' link = "