# The Don't Repeat Yourself Markup Language # # Author:: Tom Locke (tom@tomlocke.com) # Copyright:: Copyright (c) 2008 # License:: Distributes under the same terms as Ruby require 'hobo_support' require 'action_pack' ActiveSupport::Dependencies.autoload_paths |= [File.dirname(__FILE__)] # The Don't Repeat Yourself Markup Language module Dryml VERSION = File.read(File.expand_path('../../VERSION', __FILE__)).strip @@root = Pathname.new File.expand_path('../..', __FILE__) def self.root; @@root; end class DrymlSyntaxError < RuntimeError; end class DrymlException < Exception def initialize(message, path=nil, line_num=nil) if path && line_num super(message + " -- at #{path}:#{line_num}") else super(message) end end end TagDef = Struct.new "TagDef", :name, :attrs, :proc RESERVED_WORDS = %w{if for while do class else elsif unless case when module in} ID_SEPARATOR = '; dryml-tag: ' APPLICATION_TAGLIB = { :src => "taglibs/application" } CORE_TAGLIB = { :src => "core", :plugin => "dryml" } DEFAULT_IMPORTS = defined?(ApplicationHelper) ? [ApplicationHelper] : [] @cache = {} extend self attr_accessor :last_if def precompile_taglibs Dir.chdir(Rails.root) do Dir["app/views/taglibs/**/*.dryml"].each do |f| Taglib.get(:template_dir => File.dirname(f), :src => File.basename(f).remove(".dryml")) end end end def clear_cache @cache = {} end def render_tag(view, tag, options={}) renderer = empty_page_renderer(view) renderer.render_tag(tag, options) end def empty_page_renderer(view) page_renderer(view, page_tag_identifier(view.controller.controller_path)) end def page_tag_identifier(controller_path, tag_name='') "controller: #{controller_path}#{ID_SEPARATOR}#{tag_name}" end def call_render(view, local_assigns, identifier) renderer = page_renderer(view, identifier, local_assigns.keys) this = view.controller.send(:dryml_context) || local_assigns[:this] view.instance_variable_set("@this", this) if identifier =~ /#{ID_SEPARATOR}/ tag_name = identifier.split(ID_SEPARATOR).last renderer.render_tag(tag_name, {:with => this} ) else renderer.render_page(this, local_assigns).strip end end def page_renderer(view, identifier, local_names=[], controller_path=nil) controller_path ||= view.controller.controller_path if identifier =~ /#{ID_SEPARATOR}/ identifier = identifier.split(ID_SEPARATOR).first @cache[identifier] ||= make_renderer_class("", "", local_names, taglibs_for(controller_path)) @cache[identifier].new(view) else mtime = File.mtime(identifier) renderer_class = @cache[identifier] # do we need to recompile? if (!renderer_class || # nothing cached? (local_names - renderer_class.compiled_local_names).any? || # any new local names? renderer_class.load_time < mtime) # cache out of date? renderer_class = make_renderer_class(File.read(identifier), identifier, local_names, taglibs_for(controller_path)) renderer_class.load_time = mtime @cache[identifier] = renderer_class end renderer_class.new(view) end end def get_field(object, field) return nil if object.nil? field_str = field.to_s case when object.respond_to?(field_str) object.send(field_str) when field_str =~ /^\d+$/ object[field.to_i] else object[field] end end def get_field_path(object, path) path = path.is_a?(String) ? path.split('.') : Array(path) parent = nil path.each do |field| return nil if object.nil? parent = object object = get_field(parent, field) end [parent, path.last, object] end def unreserve(word) word = word.to_s word += '_' if RESERVED_WORDS.include?(word) word end def static_tags @static_tags ||= begin path = if Object.const_defined?(:Rails) && FileTest.exists?("#{Rails.root}/config/dryml_static_tags.txt") "#{Rails.root}/config/dryml_static_tags.txt" else File.join(File.dirname(__FILE__), "dryml/static_tags") end File.readlines(path).*.chop end end attr_writer :static_tags # FIXME: This helper seems to be useless, since it does need Rails, # and with Rails it is useless since Dryml does not need Hobo # # Helper function for use outside Hobo/Rails # # Pass the template context in locals[:this] # # This function caches. If the mtime of template_path is older # than the last compilation time, the cached version will be # used. If no template_path is given, template_src is used as the # key to the cache. # # If a local variable is not present when the template is # compiled, it will be ignored when the template is used. In # other words, the variable values may change, but the names may # not. # # included_taglibs is only used during template compilation. # # @param [String] template_src the DRYML source # @param [Hash] locals local variables. # @param [String, nil] template_path the filename of the source. # @param [Array] included_taglibs A list of Taglibs to include. { :src => # "core", :plugin => "dryml" } is automatically # added to this list. # @param [ActionView::Base] view an ActionView instance def render(template_src, locals={}, template_path=nil, included_taglibs=[], view=nil) template_path ||= template_src view ||= ActionView::Base.new(ActionController::Base.view_paths, {}) this = locals.delete(:this) || nil renderer_class = Dryml::Template.build_cache[template_path]._?.environment || make_renderer_class(template_src, template_path, locals.keys) renderer_class.new(view).render_page(this, locals) end private def taglibs_for(controller_path) ( taglibs_in_dir('application').unshift(APPLICATION_TAGLIB) + subsite_taglibs(controller_path) + (controller_path.camelize+"Controller").constantize.try.included_taglibs||[] ).compact end def subsite_taglibs(controller_path) parts = controller_path.split("/") subsite = parts.length >= 2 ? parts[0..-2].join('_') : "front" src = "taglibs/#{subsite}_site" Object.const_defined?(:Rails) && File.exists?("#{Rails.root}/app/views/#{src}.dryml") ? taglibs_in_dir("#{subsite}_site").unshift({ :src => src }) : [] end def taglibs_in_dir(dir_name) Dir.chdir(Rails.root) do Dir["app/views/taglibs/#{dir_name}/**/*.dryml"].map{|f| File.basename f, '.dryml'}.map do |n| { :src => "taglibs/#{dir_name}/#{n}" } end end end # create and compile a renderer class (AKA Dryml::Template::Environment) # # template_src:: the DRYML source # template_path:: the filename of the source. This is used for # caching # locals:: local variables. # imports:: A list of helper modules to import. For example, Hobo # uses [Hobo::Helper, Hobo::Helper::Translations, # ApplicationHelper] # included_taglibs:: A list of Taglibs to include. { :src => # "core", :plugin => "dryml" } is automatically # added to this list. # def make_renderer_class(template_src, template_path, locals=[], taglibs=[]) renderer_class = Class.new(TemplateEnvironment) compile_renderer_class(renderer_class, template_src, template_path, locals, taglibs) renderer_class end def compile_renderer_class(renderer_class, template_src, template_path, locals, taglibs=[]) template = Dryml::Template.new(template_src, renderer_class, template_path) DEFAULT_IMPORTS.each {|m| template.import_module(m)} # the sum of all the names we've seen so far - eventually we'll be ready for all of 'em all_local_names = renderer_class.compiled_local_names | locals template.compile(all_local_names, [CORE_TAGLIB]+taglibs) end end require 'dryml/railtie' if Object.const_defined?(:Rails)