RenderState

class
Superclass
Object
Included Modules
Inversion::DataUtilities
Extended With
Loggability

An object that provides an encapsulation of the template's state while it is rendering.

Attributes

block[R]

The block passed to the template's render method, if there was one

containerstate[R]

The Inversion::RenderState of the containing template, if any

default_errhandler[R]

The default error handler

destinations[R]

The stack of rendered output destinations, most-recent last.

errhandler[R]

The callable object that handles exceptions raised when a node is appended

fragments[R]

Fragment nodes, keyed by fragment name

options[R]

The config options passed in from the template

published_nodes[R]

Published nodes, keyed by subscription

start_time[R]

The Time the object was created

subscriptions[R]

Subscribe placeholders for publish/subscribe

Public Class Methods

anchor
new( containerstate=nil, initial_attributes={}, options={}, &block )

Create a new RenderState. If the template is being rendered inside another one, the containing template's RenderState will be passed as the containerstate. The initial_attributes will be deep-copied, and the options will be merged with Inversion::Template::DEFAULT_CONFIG. The block is stored for use by template nodes.

# File lib/inversion/renderstate.rb, line 81
def initialize( containerstate=nil, initial_attributes={}, options={}, &block )

        # Shift hash arguments if created without a parent state
        if containerstate.is_a?( Hash )
                options = initial_attributes
                initial_attributes = containerstate
                containerstate = nil
        end

        # self.log.debug "Creating a render state with attributes: %p" %
        #     [ initial_attributes ]

        locals = deep_copy( initial_attributes )
        @scopes             = [ Scope.new(locals) ]

        @start_time         = Time.now
        @containerstate     = containerstate
        @options            = Inversion::Template::DEFAULT_CONFIG.merge( options )
        @block              = block
        @default_errhandler = self.method( :default_error_handler )
        @errhandler         = @default_errhandler
        @rendering_enabled  = true

        # The rendered output Array, the stack of render destinations and
        # tag states
        @output             = []
        @destinations       = [ @output ]
        @tag_data          = [ {} ]

        # Hash of subscribed Nodes and published data, keyed by the subscription key
        # as a Symbol
        @subscriptions      = Hash.new {|hsh, k| hsh[k] = [] } # Auto-vivify to an Array
        @published_nodes    = Hash.new {|hsh, k| hsh[k] = [] }
        @fragments          = Hash.new {|hsh, k| hsh[k] = [] }

end

Public Instance Methods

anchor
<<( node )

Append operator – add an node to the final rendered output. If the node renders as an object that itself responds to the render method, render will be called and the return value will be appended instead. This will continue until the returned object either doesn't respond to render or renders as itself.

# File lib/inversion/renderstate.rb, line 275
def <<( node )
        # self.log.debug "Appending a %p to %p" % [ node.class, self ]
        original_node = node
        original_node.before_rendering( self )

        if self.rendering_enabled?
                self.destination << self.make_node_comment( node ) if self.options[:debugging_comments]
                previous_node = nil
                enc = self.options[:encoding] || Encoding.default_internal

                begin
                        # Allow render to be delegated to subobjects
                        while node.respond_to?( :render ) && node != previous_node
                                # self.log.debug "    delegated rendering to: %p" % [ node ]
                                previous_node = node
                                node = node.render( self )
                        end

                        # self.log.debug "  adding a %p (%p; encoding: %s) to the destination (%p)" %
                        #   [ node.class, node, node.respond_to?(:encoding) ? node.encoding : 'n/a', self.destination.class ]
                        self.destination << node
                        # self.log.debug "    just appended %p to %p" % [ node, self.destination ]
                rescue ::StandardError => err
                        # self.log.debug "  handling a %p while rendering: %s" % [ err.class, err.message ]
                        self.destination << self.handle_render_error( original_node, err )
                end
        end

        original_node.after_rendering( self )
        return self
end
anchor
add_fragment( name, *nodes )

Add one or more rendered nodes to the state as a reusable fragment associated with the specified name.

# File lib/inversion/renderstate.rb, line 346
def add_fragment( name, *nodes )
        self.log.debug "Adding a %s fragment with %d nodes." % [ name, nodes.size ]
        nodes.flatten!
        self.fragments[ name.to_sym ] = nodes
        self.scope.__fragments__[ name.to_sym ] = nodes
end
anchor
attributes()

Backward-compatibility – return the tag locals of the current scope as a Hash.

# File lib/inversion/renderstate.rb, line 176
def attributes
        return self.scope.__locals__
end
anchor
default_error_handler( state, node, exception )

Default exception handler: Handle an exception while rendering node according to the behavior specified by the `on_render_error` option. Returns the string which should be appended to the output, if any.

# File lib/inversion/renderstate.rb, line 391
def default_error_handler( state, node, exception )
        case self.options[:on_render_error].to_s
        when 'ignore'
                self.log.debug "  not rendering anything for the error"
                return ''

        when 'comment'
                self.log.debug "  rendering error as a comment"
                msg = "%s: %s" % [ exception.class.name, exception.message ]
                if self.options[:debugging_comments]
                        exception.backtrace.each {|line| msg << "\n" << line }
                end
                return self.make_comment( msg )

        when 'propagate'
                self.log.debug "  propagating error while rendering"
                raise( exception )

        else
                raise Inversion::OptionsError,
                        "unknown exception-handling mode: %p" % [ self.options[:on_render_error] ]
        end
end
anchor
destination()

Return the current rendered output destination.

# File lib/inversion/renderstate.rb, line 247
def destination
        return self.destinations.last
end
anchor
disable_rendering()

Disable rendering, causing rendered nodes to be discarded instead of appended.

# File lib/inversion/renderstate.rb, line 429
def disable_rendering
        @rendering_enabled = false
end
anchor
enable_rendering()

Enable rendering, causing nodes to be appended to the rendered output.

# File lib/inversion/renderstate.rb, line 423
def enable_rendering
        @rendering_enabled = true
end
anchor
eval( code )

Evaluate the specified code in the context of itself and return the result.

# File lib/inversion/renderstate.rb, line 169
def eval( code )
        # self.log.debug "Evaling: %p" [ code ]
        return self.scope.instance_eval( code )
end
anchor
handle_render_error( node, exception )

Handle an exception that was raised while appending a node by calling the errhandler.

# File lib/inversion/renderstate.rb, line 365
def handle_render_error( node, exception )
        self.log.error "%s while rendering %p: %s" %
                [ exception.class.name, node.as_comment_body, exception.message ]

        handler = self.errhandler
        raise ScriptError, "error handler %p isn't #call-able!" % [ handler ] unless
                handler.respond_to?( :call )

        self.log.debug "Handling %p with handler: %p" % [ exception.class, handler ]
        return handler.call( self, node, exception )

rescue ::StandardError => err
        # Handle exceptions from overridden error handlers (re-raised or errors in
        # the handler itself) via the default handler.
        if handler && handler != self.default_errhandler
                self.log.error "%p (re)raised from custom error handler %p" % [ err.class, handler ]
                self.default_errhandler.call( self, node, exception )
        else
                raise( err )
        end
end
anchor
inspect()

Return a human-readable representation of the object.

# File lib/inversion/renderstate.rb, line 448
def inspect
        return "#<%p:0x%08x containerstate: %s, scope locals: %s, destination: %p>" % [
                self.class,
                self.object_id / 2,
                self.containerstate ? "0x%08x" % [ self.containerstate.object_id ] : "nil",
                self.scope.__locals__.keys.sort.join(', '),
                self.destination.class,
        ]
end
anchor
merge( otherstate )

Returns a new RenderState containing the attributes and options of the receiver merged with those of the otherstate.

# File lib/inversion/renderstate.rb, line 254
def merge( otherstate )
        merged = self.dup
        merged.merge!( otherstate )
        return merged
end
anchor
merge!( otherstate )

Merge the attributes and options of the otherstate with those of the receiver, replacing any with the same keys.

# File lib/inversion/renderstate.rb, line 263
def merge!( otherstate )
        @scopes.push( @scopes.pop + otherstate.scope )
        # self.attributes.merge!( otherstate.attributes )
        self.options.merge!( otherstate.options )
        return self
end
anchor
publish( key, *nodes )

Publish the given nodes to all subscribers to the specified key.

# File lib/inversion/renderstate.rb, line 315
def publish( key, *nodes )
        key = key.to_sym
        # self.log.debug "[0x%016x] Publishing %p nodes: %p" % [ self.object_id * 2, key, nodes ]

        self.containerstate.publish( key, *nodes ) if self.containerstate
        self.subscriptions[ key ].each do |subscriber|
                # self.log.debug "  sending %d nodes to subscriber: %p (a %p)" %
                #     [ nodes.length, subscriber, subscriber.class ]
                subscriber.publish( *nodes )
        end
        self.published_nodes[ key ].concat( nodes )
end
Also aliased as: publish_nodes
anchor
publish_nodes( key, *nodes )
Alias for: publish
anchor
rendered_fragments()

Return the current fragments Hash rendered as Strings.

# File lib/inversion/renderstate.rb, line 355
def rendered_fragments
        self.log.debug "Rendering fragments: %p." % [ self.fragments.keys ]
        return self.fragments.each_with_object( {} ) do |(key, nodes), accum|
                accum[ key ] = self.stringify_nodes( nodes )
        end
end
anchor
rendering_enabled?()

Return true if rendered nodes are being saved for output.

# File lib/inversion/renderstate.rb, line 417
def rendering_enabled?
        return @rendering_enabled ? true : false
end
anchor
scope()

Return the hash of attributes that are currently in effect in the rendering state.

# File lib/inversion/renderstate.rb, line 156
def scope
        return @scopes.last
end
anchor
subscribe( key, node )

Subscribe the given node to nodes published with the specified key.

# File lib/inversion/renderstate.rb, line 331
def subscribe( key, node )
        key = key.to_sym
        self.log.debug "Adding subscription to %p nodes for %p" % [ key, node ]
        self.subscriptions[ key ] << node
        # self.log.debug "  now have subscriptions for: %p" % [ self.subscriptions.keys ]
        if self.published_nodes.key?( key )
                self.log.debug "    re-publishing %d %p nodes to late subscriber" %
                        [ self.published_nodes[key].length, key ]
                node.publish( *self.published_nodes[key] )
        end
end
anchor
tag_data()

Return a Hash that tags can use to track state for the current render.

# File lib/inversion/renderstate.rb, line 162
def tag_data
        return @tag_data.last
end
anchor
time_elapsed()

Return the number of floting-point seconds that have passed since the object was created. Used to time renders.

# File lib/inversion/renderstate.rb, line 442
def time_elapsed
        return Time.now - self.start_time
end
anchor
to_s()

Turn the rendered node structure into the final rendered String.

# File lib/inversion/renderstate.rb, line 309
def to_s
        return self.stringify_nodes( @output )
end
anchor
toggle_rendering()

Toggle rendering, enabling it if it was disabled, and vice-versa.

# File lib/inversion/renderstate.rb, line 435
def toggle_rendering
        @rendering_enabled = !@rendering_enabled
end
anchor
with_attributes( overrides ) { |self| ... }

Override the state's attributes with the given overrides, call the block, then restore the attributes to their original values.

# File lib/inversion/renderstate.rb, line 183
def with_attributes( overrides )
        raise LocalJumpError, "no block given" unless block_given?
        # self.log.debug "Overriding template attributes with: %p" % [ overrides ]

        begin
                newscope = self.scope + overrides
                @scopes.push( newscope )
                yield( self )
        ensure
                @scopes.pop
        end
end
anchor
with_destination( new_destination ) { || ... }

Override the state's render destination, call the block, then restore the original destination when the block returns.

# File lib/inversion/renderstate.rb, line 214
def with_destination( new_destination )
        raise LocalJumpError, "no block given" unless block_given?
        # self.log.debug "Overriding render destination with: %p" % [ new_destination ]

        begin
                @destinations.push( new_destination )
                yield
        ensure
                # self.log.debug "  removing overridden render destination: %p" % [ @destinations.last ]
                @destinations.pop
        end

        return new_destination
end
anchor
with_error_handler( handler ) { || ... }

Set the state's error handler to handler for the duration of the block, restoring the previous handler after the block exits. Handler must respond to call, and will be called with two arguments: the node that raised the exception, and the exception object itself.

# File lib/inversion/renderstate.rb, line 234
def with_error_handler( handler )
        original_handler = self.errhandler
        raise ArgumentError, "%p doesn't respond_to #call" unless handler.respond_to?( :call )
        @errhandler = handler

        yield

ensure
        @errhandler = original_handler
end
anchor
with_tag_data( newhash={} ) { |self| ... }

Add an overlay to the current tag state Hash, yield to the provided block, then revert the tag state back to what it was prior to running the block.

# File lib/inversion/renderstate.rb, line 199
def with_tag_data( newhash={} )
        raise LocalJumpError, "no block given" unless block_given?
        # self.log.debug "Overriding tag state with: %p" % [ newhash ]

        begin
                @tag_data.push( @tag_data.last.merge(newhash) )
                yield( self )
        ensure
                @tag_data.pop
        end
end

Protected Instance Methods

anchor
make_comment( content )

Return the specified content inside of the configured comment characters.

# File lib/inversion/renderstate.rb, line 471
def make_comment( content )
        return [
                self.options[:comment_start],
                content,
                self.options[:comment_end],
        ].join
end
anchor
make_node_comment( node )

Return the node as a comment if debugging comments are enabled.

# File lib/inversion/renderstate.rb, line 464
def make_node_comment( node )
        comment_body = node.as_comment_body or return ''
        return self.make_comment( comment_body )
end
anchor
method_missing( sym, *args, &block )

Handle attribute methods.

# File lib/inversion/renderstate.rb, line 495
def method_missing( sym, *args, &block )
        return super unless sym.to_s =~ /^\w+$/
        # self.log.debug "mapping missing method call to tag local: %p" % [ sym ]
        return self.scope[ sym ]
end
anchor
stringify_nodes( nodes )

Return the given nodes as a String in the configured encoding.

# File lib/inversion/renderstate.rb, line 481
def stringify_nodes( nodes )
        # self.log.debug "Rendering nodes: %p" % [ nodes ]
        strings = nodes.flatten.map( &:to_s )

        if enc = self.options[ :encoding ]
                self.log.debug "Encoding rendered template parts to %s" % [ enc ]
                strings.map! {|str| str.encode(enc, invalid: :replace, undef: :replace) }
        end

        return strings.join
end