require 'nano/kernel/singleton' require 'glue/template' require 'nitro/compiler/elements' require 'nitro/compiler/errors' require 'nitro/compiler/markup' require 'nitro/compiler/morphing' require 'nitro/compiler/script' require 'nitro/compiler/include' require 'nitro/compiler/cleanup' require 'nitro/compiler/layout' module Nitro # The Compiler transforms published methods (actions) and # assorted template files (views) into specialized code that # responds to a URI. The generated action methods are injects # in the Controller that handles the URI. class Compiler unless const_defined? :PROTO_TEMPLATE_ROOT PROTO_TEMPLATE_ROOT = "#{Nitro.proto_path}/public" end # The controller for this compiler. attr_accessor :controller # The compiler stages (compilers) can create multiple variables # or accumulation bufffers to communicate with each other. # Typically javascript and css acc-buffers are used. This is # the shared memory used by the compilers. attr_accessor :shared # 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' def initialize(controller = nil) @controller = controller @shared = {} end # 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, ext = Template.extension) cklass = @controller template_root = nil checked_proto = nil # search for template in controller and its ancestors's template_root # if there is not an action defined in controller then also check in # PROTO_TEMPLATE_ROOT loop do if cklass.respond_to?(:template_root) && template_root = cklass.template_root cklass = cklass.superclass elsif !@controller.respond_to?(:action) && !checked_proto template_root = PROTO_TEMPLATE_ROOT checked_proto = true else return nil # no template found end # attempt to find a template of the form # template_root/action.xhtml path = "#{template_root}/#{action.gsub(/__/, '/')}.#{ext}".squeeze('/') return path if 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('/') return path if File.exist?(path) end end alias_method :template?, :template_for_action # Helper. def action?(sym) return @controller.action_methods.include?(sym.to_s) end # This method 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, and uses the StaticInclude, # Morphing, Elements and Markup compiler modules. def transform_template(template) template = StaticInclude.transform(template) template = Morphing.transform(template, self) # template = LayoutCompiler.transform(template, self) template = Elements.transform(template, self) template = Markup.transform(template) template = ScriptCompiler.transform(template, self) template = Cleanup.transform(template) template = Template.transform(template) end # Compile the template into a render method. # Don't compile the template if the controller # responds_to? #{action}_template. def compile_template(action, path) Logger.debug "Compiling template '#{@controller}: #{path}'" if $DBG template = File.read(path) code = %{ def #{action}_template #{transform_template(template)} end } begin @controller.class_eval(code, path) rescue SyntaxError => e raise TemplateCompileError.new(code, template, e) end end # Compiles an action. The generated action combines the # action supplied by the programmer and an optional template # in an optimized method to handle the input URI. # # Passes the action name and the parent action name in the # @action_name and @parent_action_name respectively. # # === Example # # def my_method # template_root/my_method.xhtml # # are combined in: # # def my_method_action # # This generated method is called by the dispatcher. # # === Template root overloading # # Nitro provides a nice method of template_root overloading # that allows you to use OOP principles with templates. # Lets say you have the following controller inheritance. # # SpecificController < BaseController < Nitro::Controller # # When the compiler tries to find a template for the # SpecificController, it first looks into SC's template root. # If no suitable template is found, it looks into # BaseController's template_root etc. The final template_root # is always the Nitro proto dir (the prototype application). # This way you can reuse Controllers and templates and only # overriding the templates as required by placing a template # with the same name in the more specific template root. #-- # TODO: cleanup this method. #++ def compile_action(action) action = action.to_s.gsub(/_action$/, '') return false unless action Logger.debug "Compiling action '#@controller##{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, @controller.advices, :pre) # Call the action if @controller.action_methods.include?(action) valid = true # Annotated parameters. if params = @controller.ann(action.to_sym).params and (!params.nil?) params = params.collect { |p| "@#{p} = @context['#{p}']" } code << "#{params.join(';')}" end # Try to resolve action parameters. Returns negative # numbers for arbitrary parameters. param_count = @controller.instance_method(action.intern).arity if param_count != 0 if param_count > 0 code << "params = Array.new(#{param_count}, nil);" else code << "params = [];" end code << %{ if context.query_string context.query_string.split(/[&;]/).each_with_index do |qs, i| } # Don't pass more parameters than the action's arity. if param_count > 0 code << "break unless i < #{param_count};" end code << %{ params[i] = CGI.unescape(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 annotation into account. view = @controller.ann(action.to_sym).view # FIXME view = action if view.nil? # Search the [controller] class and it's ancestors for the template template_path = template_for_action(view.to_s) if template_path or @controller.instance_methods.include?("#{action}_template") valid = true code << %{ #{action}_template } end return false unless valid if @controller.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, @controller.advices, :post) code << %{ @action_name = @parent_action_name end } # First compile the action method. # begin @controller.class_eval(code) # rescue SyntaxError => e # raise ActionCompileError.new(code, action, e) # end unless @controller.respond_to?("#{action}_template") if template_path compile_template(action, template_path) end end return true end # Compiles an action method in the given (controller) class. # A sync is used to make compilation thread safe. def compile(action) compile_action(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 # Typically used to precompile css templates. def precompile(filename) src = File.join(Template.root, "#{filename}t") dst = File.join(Server.public_root, filename) if (!File.exist?(dst)) or (File.mtime(src) > File.mtime(dst)) Logger.info "Compiling template '#{src}' to '#{dst}'." template = FileTemplate.new(src) File.open(dst, 'w') do |f| f << template.process end end end end end end # * George Moschovitis # * Chris Farmiloe