require 'nitro/template' require 'nitro/compiler/errors' require 'nitro/compiler/elements' 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' # The default transformation pipeline. Nitro allows fine # grained customization pipelines per controller or even per # action. The Template transformation is added by default. # Here come examples of pipeline customization: # # class MyController # ann :self, :transformation_pipeline => [MyTransformer, AnotherXForm] # # ... # # def my_action # ... # end # ann :my_action, :transformation_pipeline => Compiler.transformation_pipeline.dup.shift(CustomXForm) # # ... # end setting :transformation_pipeline, :default => [StaticInclude, Morphing, ElementCompiler, MarkupCompiler, ScriptCompiler, Cleanup], :doc => 'The default transformation pipeline' # This specifies if # def my_action(oid, val) # should handle both # my_action/1/23 # and # my_action?oid=1;val=23 setting :mixin_get_parameters, :default => true # Treats # def my_action(oid, val) # as # def my_action(oid = nil, val = nil) setting :non_strict_action_calling, :default => false def initialize(controller = nil) @controller = controller @shared = {} end # Traverse the template_root stack to find a template for # this action. # # 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) action = action.to_s for template_root in @controller.instance_variable_get(:@template_root) # 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 return nil 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. # # The Template transformation stage is added by default. # # You can override this method or use aspects for really # special transformation pipelines. Your imagination is the # limit. #-- # TODO: make Template stage pluggable. # gmosx: This method is also called from the scaffolding # code. #++ def transform_template(action, template) # Check for an action specific transformation pipeline. if (transformers = @controller.ann(action.to_sym).transformation_pipeline).nil? # Check for a controller specific transformation pipeline. if (transformers = @controller.ann.self.transformation_pipeline).nil? # Use the default transformation pipeline. transformers = Compiler.transformation_pipeline end end transformers.each do |transformer| template = transformer.transform(template, self) end # Add Template transformation stage by default. 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(action, template)} end } @controller.class_eval(code, path) 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 = action.to_s.gsub(/_action$/, '') return false unless action Logger.debug "Compiling action '#@controller##{action}'" if $DBG valid = false #-- # FIXME: parent_action_name does not work as expected, # if you include actions from other controllers!! #++ 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 arity = @controller.instance_method(action.to_sym).arity # Call action with given parameters, raises ActionError when the params # are wrong. # TODO: return 404 or other error code for browser? code << %{ params = context.action_params || [] # Fill not given parameters with nils arity = (#{arity} < -1) ? (#{arity}.abs - 1) : #{arity} if Nitro::Compiler.non_strict_action_calling arity.times {|i| params[i] ? nil : params[i] = nil } end # When arity zero, ignore params params.clear if arity == 0 if Nitro::Compiler.mixin_get_parameters && params.empty? && arity > 0 context.params.each_with_index do |(k,v),i| break if i > arity params << v end end begin action_return_value = #{action}(*params) rescue ArgumentError => e raise ActionError, "Wrong parameter count for \#{@action_name}(\#{params.join(', ')})." 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 :template annotation into account. unless template = @controller.ann(action.to_sym)[:template] # FIXME template = action end # Search the [controller] class and it's ancestors for the template template_path = template_for_action(template.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_to_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. @controller.class_eval(code) unless @controller.respond_to?("#{action}_template") # If there is not method {action}_template in the controller # search for a file 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 # 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