require 'facet/object/singleton_class' require 'nitro/template' require 'nitro/compiler/errors' module Nitro # The Compiler transforms published methods (actions) and # assorted template files (views) into specialized code that # responds to a URI. class Compiler unless const_defined? :PROTO_TEMPLATE_ROOT PROTO_TEMPLATE_ROOT = "#{Nitro.proto_path}/public" end # Set to true to force reloading of code and templates for # each request. Extremely useful during development. Must be # turned off in production servers to avoid the severe # performance penalty. setting :reload, :default => true, :doc => 'If true all code and templates are reloaded in each request' # Action names with double underscores (__) are converted # to subdirectories. Here are some example mappings: # # hello_world -> template_root/hello_world.xhtml # this__is__my__hello_world -> template_root/this/is/my/hello_world def template_for_action(action, template_root = Template.root, ext = Template.extension) # attempt to find a template of the form # template_root/action.xhtml path = "#{template_root}/#{action.gsub(/__/, '/')}.#{ext}".squeeze('/') unless File.exist?(path) # attempt to find a template of the form # template_root/action/index.xhtml path = "#{template_root}/#{action.gsub(/__/, '/')}/#{Template.default}.#{ext}".squeeze('/') unless File.exist?(path) # No template found! return nil end end return path end # This is methods transforms the template. Typically # template processors are added as aspects to this method # to allow for customized template transformation prior # to compilation. # # The default transformation extracts the Ruby code from # processing instructions. def transform_template(template) Template.transform(template) end # Compile the template into a render method. def compile_template(klass, action, path) Logger.debug "Compiling template '#{klass}: #{path}'" if $DBG template = File.read(path) code = %{ def #{action}_template #{transform_template(template)} end } begin klass.class_eval(code, path) rescue SyntaxError => e raise TemplateCompileError.new(code, template, e) end end # Compiles an action. # # Passes the action name and the parent action name in the # @action_name and @parent_action_name respectively. #-- # TODO: cleanup this method. #++ def compile_action(klass, action) #-- # gmosx: Move elsewhere. #++ Aspects.include_advice_modules(klass) action = action.to_s.gsub(/_action$/, '') return false unless action Logger.debug "Compiling action '#{klass}##{action}'" if $DBG valid = false code = %{ def #{action}_action @parent_action_name = @action_name @action_name = '#{action}' } # Inject the pre advices. code << Aspects.gen_advice_code(action, klass.advices, :pre) # Call the action if klass.action_methods.include?(action) valid = true # Annotated parameters. if meta = klass.action_metadata[action.intern] params = meta.params.keys params = params.collect { |p| "@#{p} = @context['#{p}']" } code << "#{params.join(';')}" end # Try to resolve action parameters. Returns negative # numbers for arbitrary parameters. param_count = klass.instance_method(action.intern).arity if param_count != 0 code << %{ params = [] if context.query_string context.query_string.split(/[&;]/).each do |qs| params << qs.split(/=/).last end end action_return_value = #{action}(*params) } else code << %{ action_return_value = #{action} } end code << %{ unless :stop == action_return_value } end # Try to call the template method if it exists. It is a # nice practice to put output related code in this method # instead of the main action so that this method can be # overloaded separately. # # If no template method exists, try to convert an external # template file into a template method. It is an even # better practice to place the output related code in an # external template file. # Take :view metadata into account. view = nil if md = klass.action_metadata[action.intern] view = md[:view] end view ||= action cklass = klass template_path = nil loop do template_root = nil if cklass.respond_to?(:template_root) template_root = cklass.template_root end # Don't use a proto template if there is an action # defined. template_root ||= PROTO_TEMPLATE_ROOT unless valid if template_root and template_path = template_for_action(view.to_s, template_root) valid = true code << %{ #{action}_template; } break end break unless cklass = cklass.superclass end return false unless valid if klass.action_methods.include?(action) code << %{ if @out.empty? and action_return_value.is_a?(String) print(action_return_value) end end } end if Render.redirect_on_empty code << %{ redirect_referer if @out.empty? } end # Inject the post advices. code << Aspects.gen_advice_code(action, klass.advices, :post) code << %{ @action_name = @parent_action_name end } # First compile the action method. # begin klass.class_eval(code) # rescue SyntaxError => e # raise ActionCompileError.new(code, action, e) # end compile_template(klass, action, template_path) if template_path return true end # Compiles an action method in the given (controller) class. # A sync is used to make compilation thread safe. def compile(klass, action) compile_action(klass, action) end # :section: Helper methods. class << self # Helper method for manipulating the template transformation # pipeline. def setup_transform_template(&block) send :define_method, :transform_template, block end alias_method :setup_template_transform, :setup_transform_template alias_method :setup_template_transformation, :setup_transform_template end end end