require 'singleton' require 'sync' require 'stringio' require 'nano/string/blank' require 'glue/attribute' require 'glue/settings' require 'glue/template' require 'glue/builder' require 'glue/builder/xml' require 'nitro/helper/xhtml' require 'nitro/helper/form' require 'nitro/helper/table' require 'nitro/helper/buffer' module Nitro # Raise or Throw this exception to stop the current action. # Typically called to skip the template. class ActionExit < Exception; end # Raise or Throw this exception to stop rendering altogether. # Typically called by redirects. class RenderExit < Exception; end # The output buffer. #-- # TODO: Implement a FAST string (maybe in C) #++ class OutputBuffer < String end # The rendering mixin. This module is typically included in # published objects and/or controllers to provide rendering # functionality. #-- # TODO: handle template_root here instead of the # controller. #++ module Render include BufferHelper # If true, auto redirect to referer on empty buffer. setting :redirect_on_empty, :default => false, :doc => 'If true, auto redirect to referer on empty buffer' # The output buffer. The output of a script/action is # accumulated in this buffer. attr_accessor :out alias_method :body, :out # The context. attr_accessor :context alias_method :ctx, :context alias_method :ctx=, :context= # Aliases for context. attr_accessor :request, :response # An array holding the rendering errors for this # request. attr_accessor :rendering_errors # The name of the currently executing action. attr_accessor :action_name # The base url for this render. attr_accessor :base # The name of the current controller. #-- # gmosx: Needed for WEE. Will be deprecated. #++ attr_accessor :controller_name # Initialize the render. # # [+context+] # A parent render/controller acts as the context. def initialize(context, base = nil) @request = @response = @context = context @controller_name = @base = base @out = context.out end # Renders the action denoted by path. The path # is resolved by the dispatcher to get the correct # controller. # # Both relative and absolute paths are supported. Relative # paths are converted to absolute by prepending the @base # path of the controller. def render(path) # Convert relative paths to absolute paths. path = "#@base/#{path}" unless path =~ /^\// Logger.debug "Rendering '#{path}'." if $DBG klass, action, base = @context.dispatcher.dispatch(path, @context) # FIXME: @context.content_type = klass.instance_variable_get('@content_type') || 'text/html' raise 'No controller for action' unless klass if self.class == klass self.send(action) else klass.new(@context, base).send(action) end rescue RenderExit, ActionExit => e # Just stop rendering. # For example called by redirects. rescue Exception, StandardError => e log_error(e, path) # More fault tolerant, only flags the erroneous box with # error not the full page. @out << '(error)' end private # Helper method to exit the current action, typically used # to skip the template. def exit raise ActionExit.new end # Flush the IO object if we are in streaming mode. def flush @out.flush if @out.is_a?(IO) end # Send a redirect response. # # If the url starts with '/' it is considered absolute, else # the url is considered relative to the current controller and # the controller base is prepended. def redirect(url, status = 303) url = url.to_s unless url =~ /^http/ url = "#@base/#{url}" unless url =~ /^\// url = "#{@context.host_url}/#{url.gsub(/^\//, '')}" end @context.status = status @context.out = "#{url}.\n" @context.response_headers['location'] = url raise RenderExit end alias_method :redirect_to, :redirect # Redirect to the referer. def redirect_referer(postfix = nil, status = 303) redirect("#{@context.referer}#{postfix}", status) end alias_method :redirect_to_referer, :redirect_referer # Redirect to home. def redirect_home(status = 303) redirect('/', status) end alias_method :redirect_to_home, :redirect_home # Log a rendering error. def log_error(error, path) @rendering_errors ||= [] @rendering_errors << [error, path] # gmosx: Hmm perhaps this should not be logged # to avoid DOS attacks. Logger.error "Error while handling '#{path}'." Logger.error pp_exception(error) end # Convenience method to lookup the session. def session @context.session end # Add some text to the output buffer. def render_text(text) @out << text end alias_method :print, :render_text # Render a template into the output buffer. def render_template(filename) filename = "#{filename}.xhtml" unless filename =~ /\.xhtml$/ template = File.read("#{template_root}/#{filename}") Template.process_template(template, '@out', binding) end # Access the programmatic renderer (builder). def build(&block) if block.arity == 1 yield XmlBuilder.new(@out) else XmlBuilder.new(@out).instance_eval(&block) end end # Return a programmatic renderer that targets the # output buffer. def builder XmlBuilder.new(@out) end # A Helper class to access rendering mixins. Useful to avoid # poluting the Render with utility methods. #-- # TODO: find a less confusing name. #++ class Emitter include Singleton include XhtmlHelper include FormHelper include TableHelper end # A helper to access the utilities emitter: # # #{emit :form, entity} # #{emit :options, :labels => [..], :values => [..], :selected => 1} # # Useful to avoid poluting the render with mixin methods. def emit(meth, *options) Emitter.instance.send(meth, *options) end end end # * George Moschovitis