Class: Inversion::RenderState

Inherits:
Object
  • Object
show all
Extended by:
Loggability
Includes:
DataUtilities
Defined in:
lib/inversion/renderstate.rb

Overview

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

Defined Under Namespace

Classes: Scope

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from DataUtilities

deep_copy

Constructor Details

#initialize(containerstate = nil, initial_attributes = {}, options = {}, &block) ⇒ RenderState

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.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/inversion/renderstate.rb', line 82

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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object (protected)

Handle attribute methods.



496
497
498
499
500
# File 'lib/inversion/renderstate.rb', line 496

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

Instance Attribute Details

#blockObject (readonly)

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



131
132
133
# File 'lib/inversion/renderstate.rb', line 131

def block
  @block
end

#containerstateObject (readonly)

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



125
126
127
# File 'lib/inversion/renderstate.rb', line 125

def containerstate
  @containerstate
end

#default_errhandlerObject (readonly)

The default error handler



149
150
151
# File 'lib/inversion/renderstate.rb', line 149

def default_errhandler
  @default_errhandler
end

#destinationsObject (readonly)

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



143
144
145
# File 'lib/inversion/renderstate.rb', line 143

def destinations
  @destinations
end

#errhandlerObject (readonly)

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



146
147
148
# File 'lib/inversion/renderstate.rb', line 146

def errhandler
  @errhandler
end

#fragmentsObject (readonly)

Fragment nodes, keyed by fragment name



140
141
142
# File 'lib/inversion/renderstate.rb', line 140

def fragments
  @fragments
end

#optionsObject (readonly)

The config options passed in from the template



128
129
130
# File 'lib/inversion/renderstate.rb', line 128

def options
  @options
end

#published_nodesObject (readonly)

Published nodes, keyed by subscription



137
138
139
# File 'lib/inversion/renderstate.rb', line 137

def published_nodes
  @published_nodes
end

#start_timeObject (readonly)

The Time the object was created



152
153
154
# File 'lib/inversion/renderstate.rb', line 152

def start_time
  @start_time
end

#subscriptionsObject (readonly)

Subscribe placeholders for publish/subscribe



134
135
136
# File 'lib/inversion/renderstate.rb', line 134

def subscriptions
  @subscriptions
end

Instance Method Details

#<<(node) ⇒ Object

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.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/inversion/renderstate.rb', line 276

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

#add_fragment(name, *nodes) ⇒ Object

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



347
348
349
350
351
352
# File 'lib/inversion/renderstate.rb', line 347

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

#attributesObject

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



177
178
179
# File 'lib/inversion/renderstate.rb', line 177

def attributes
  return self.scope.__locals__
end

#default_error_handler(state, node, exception) ⇒ Object

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.



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/inversion/renderstate.rb', line 392

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

#destinationObject

Return the current rendered output destination.



248
249
250
# File 'lib/inversion/renderstate.rb', line 248

def destination
  return self.destinations.last
end

#disable_renderingObject

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



430
431
432
# File 'lib/inversion/renderstate.rb', line 430

def disable_rendering
  @rendering_enabled = false
end

#enable_renderingObject

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



424
425
426
# File 'lib/inversion/renderstate.rb', line 424

def enable_rendering
  @rendering_enabled = true
end

#eval(code) ⇒ Object

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



170
171
172
173
# File 'lib/inversion/renderstate.rb', line 170

def eval( code )
  # self.log.debug "Evaling: %p" [ code ]
  return self.scope.instance_eval( code )
end

#handle_render_error(node, exception) ⇒ Object

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



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/inversion/renderstate.rb', line 366

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

#inspectObject

Return a human-readable representation of the object.



449
450
451
452
453
454
455
456
457
# File 'lib/inversion/renderstate.rb', line 449

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

#merge(otherstate) ⇒ Object

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



255
256
257
258
259
# File 'lib/inversion/renderstate.rb', line 255

def merge( otherstate )
  merged = self.dup
  merged.merge!( otherstate )
  return merged
end

#merge!(otherstate) ⇒ Object

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



264
265
266
267
268
269
# File 'lib/inversion/renderstate.rb', line 264

def merge!( otherstate )
  @scopes.push( @scopes.pop + otherstate.scope )
  # self.attributes.merge!( otherstate.attributes )
  self.options.merge!( otherstate.options )
  return self
end

#publish(key, *nodes) ⇒ Object Also known as: publish_nodes

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



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/inversion/renderstate.rb', line 316

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

#rendered_fragmentsObject

Return the current fragments Hash rendered as Strings.



356
357
358
359
360
361
# File 'lib/inversion/renderstate.rb', line 356

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

#rendering_enabled?Boolean

Return true if rendered nodes are being saved for output.

Returns:

  • (Boolean)


418
419
420
# File 'lib/inversion/renderstate.rb', line 418

def rendering_enabled?
  return @rendering_enabled ? true : false
end

#scopeObject

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



157
158
159
# File 'lib/inversion/renderstate.rb', line 157

def scope
  return @scopes.last
end

#subscribe(key, node) ⇒ Object

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



332
333
334
335
336
337
338
339
340
341
342
# File 'lib/inversion/renderstate.rb', line 332

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

#tag_dataObject

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



163
164
165
# File 'lib/inversion/renderstate.rb', line 163

def tag_data
  return @tag_data.last
end

#time_elapsedObject

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



443
444
445
# File 'lib/inversion/renderstate.rb', line 443

def time_elapsed
  return Time.now - self.start_time
end

#to_sObject

Turn the rendered node structure into the final rendered String.



310
311
312
# File 'lib/inversion/renderstate.rb', line 310

def to_s
  return self.stringify_nodes( @output )
end

#toggle_renderingObject

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



436
437
438
# File 'lib/inversion/renderstate.rb', line 436

def toggle_rendering
  @rendering_enabled = !@rendering_enabled
end

#with_attributes(overrides) ⇒ Object

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

Raises:

  • (LocalJumpError)


184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/inversion/renderstate.rb', line 184

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

#with_destination(new_destination) ⇒ Object

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

Raises:

  • (LocalJumpError)


215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/inversion/renderstate.rb', line 215

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

#with_error_handler(handler) ⇒ Object

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.



235
236
237
238
239
240
241
242
243
244
# File 'lib/inversion/renderstate.rb', line 235

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

#with_tag_data(newhash = {}) ⇒ Object

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.

Raises:

  • (LocalJumpError)


200
201
202
203
204
205
206
207
208
209
210
# File 'lib/inversion/renderstate.rb', line 200

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