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.



81
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
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

Handle attribute methods.



495
496
497
498
499
# 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

Instance Attribute Details

#blockObject (readonly)

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



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

def block
  @block
end

#containerstateObject (readonly)

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



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

def containerstate
  @containerstate
end

#default_errhandlerObject (readonly)

The default error handler



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

def default_errhandler
  @default_errhandler
end

#destinationsObject (readonly)

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



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

def destinations
  @destinations
end

#errhandlerObject (readonly)

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



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

def errhandler
  @errhandler
end

#fragmentsObject (readonly)

Fragment nodes, keyed by fragment name



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

def fragments
  @fragments
end

#optionsObject (readonly)

The config options passed in from the template



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

def options
  @options
end

#published_nodesObject (readonly)

Published nodes, keyed by subscription



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

def published_nodes
  @published_nodes
end

#start_timeObject (readonly)

The Time the object was created



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

def start_time
  @start_time
end

#subscriptionsObject (readonly)

Subscribe placeholders for publish/subscribe



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

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.



275
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
# 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

#add_fragment(name, *nodes) ⇒ Object

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



346
347
348
349
350
351
# 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

#attributesObject

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



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

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.



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# 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

#destinationObject

Return the current rendered output destination.



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

def destination
	return self.destinations.last
end

#disable_renderingObject

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



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

def disable_rendering
	@rendering_enabled = false
end

#enable_renderingObject

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



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

def enable_rendering
	@rendering_enabled = true
end

#eval(code) ⇒ Object

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



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

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.



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# 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

#inspectObject

Return a human-readable representation of the object.



448
449
450
451
452
453
454
455
456
# 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

#merge(otherstate) ⇒ Object

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



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

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.



263
264
265
266
267
268
# 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

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

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



315
316
317
318
319
320
321
322
323
324
325
326
# 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

#rendered_fragmentsObject

Return the current fragments Hash rendered as Strings.



355
356
357
358
359
360
# 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

#rendering_enabled?Boolean

Return ‘true` if rendered nodes are being saved for output.

Returns:

  • (Boolean)


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

def rendering_enabled?
	return @rendering_enabled ? true : false
end

#scopeObject

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



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

def scope
	return @scopes.last
end

#subscribe(key, node) ⇒ Object

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



331
332
333
334
335
336
337
338
339
340
341
# 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

#tag_dataObject

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



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

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.



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

def time_elapsed
	return Time.now - self.start_time
end

#to_sObject

Turn the rendered node structure into the final rendered String.



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

def to_s
	return self.stringify_nodes( @output )
end

#toggle_renderingObject

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



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

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)


183
184
185
186
187
188
189
190
191
192
193
194
# 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

#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)


214
215
216
217
218
219
220
221
222
223
224
225
226
227
# 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

#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.



234
235
236
237
238
239
240
241
242
243
# 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

#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)


199
200
201
202
203
204
205
206
207
208
209
# 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