Class: Arrow::Template

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
HashUtilities
Defined in:
lib/arrow/template.rb,
lib/arrow/template/nodes.rb

Overview

The Arrow::Template class, instances of which are used to generate output for Arrow applications.

Synopsis

:TODO: Write some useful Arrow::Template examples

Authors

Please see the file LICENSE in the top-level directory for licensing details.

Defined Under Namespace

Modules: ConditionalDirective Classes: AttrDirective, AttributeDirective, BracketingDirective, CallDirective, CommentDirective, CommentNode, Container, Directive, ElseDirective, ElsifDirective, EscapeDirective, ExportDirective, ForDirective, IfDirective, ImportDirective, IncludeDirective, Iterator, Node, Parser, PrettyPrintDirective, RenderDirective, RenderingScope, SelectListDirective, SetDirective, TextNode, TimeDeltaDirective, URLEncodeDirective, UnlessDirective, YieldDirective

Constant Summary collapse

DEFAULTS =

Configuration defaults. Valid members are the same as those listed for the config item of the #new method.

{
	:parserClass			=> Arrow::Template::Parser,
	:elideDirectiveLines	=> true,
	:debuggingComments		=> false,
	:commentStart			=> '<!-- ',
	:commentEnd				=> ' -->',
	:strictAttributes		=> false,
}
DEFAULT_RENDERERS =

A Hash which specifies the default renderers for different classes of objects.

{
	Arrow::Template	=> lambda {|subtempl,templ|
		subtempl.render( nil, nil, templ )
	},
	::Object		=> :to_s,
	::Array			=> lambda {|ary,tmpl|
		tmpl.render_objects(*ary)
	},
	::Hash			=> lambda {|hsh,tmpl|
		hsh.collect do |k,v| tmpl.render_objects(k, ": ", v) end
	},
	::Method		=> lambda {|meth,tmpl|
		tmpl.render_objects( meth.call )
	},
	::Exception		=> lambda {|err,tmpl|
		tmpl.render_comment "%s: %s: %s" % [
			err.class.name,
			err.message,
			err.backtrace ? err.backtrace[0] : "Stupid exception with no backtrace.",
		]
	},
}

Constants included from HashUtilities

HashUtilities::HashMergeFunction

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HashUtilities

stringify_keys, symbolify_keys

Methods inherited from Object

deprecate_class_method, deprecate_method, inherited

Constructor Details

#initialize(content = nil, config = {}) ⇒ Template

Create a new template object with the specified content (a String) and config hash. The config can contain one or more of the following keys:

:parserClass

The class object that will be instantiated to parse the template text into nodes. Defaults to Arrow::Template::Parser.

:elideDirectiveLines

If set to a true value, lines of the template which contain only whitespace and one or more non-rendering directives will be discarded from the rendered output.

:debuggingComments

If set to a true value, nodes which are set up to do so will insert a comment with debugging information immediately before their rendered output.

:commentStart

The String which will be prepended to all comments rendered in the output. See #render_comment.

:commentEnd

The String which will be appended to all comments rendered in the output. See #render_comment.

:strictAttributes

If set to a true value, method calls which don’t match already-extant attributes will result in NameErrors. This is false by default, which causes method calls to generate attributes with the same name.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/arrow/template.rb', line 299

def initialize( content=nil, config={} )
	@config = DEFAULTS.merge( config, &HashMergeFunction )
	@renderers = DEFAULT_RENDERERS.dup
	@attributes = {}
	@syntax_tree = []
	@source = content
	@file = nil
	@creation_time = Time.now
	@load_path = self.class.load_path.dup
	@prerender_done = false
	@postrender_done = false

	@enclosing_templates = []

	case content
	when String
		self.parse( content )
	when Array
		self.install_syntax_tree( content )
	when NilClass
		# No-op
	else
		raise TemplateError,
			"Can't handle a %s as template content" % content.class.name
	end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

Autoload accessor/mutator methods for attributes.



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/arrow/template.rb', line 659

def method_missing( sym, *args, &block )
	name = sym.to_s.gsub( /=$/, '' )
	super unless @attributes.key?( name ) || !@config[:strictAttributes]

	#self.log.debug "Autoloading for #{sym}"

	# Mutator
	if /=$/ =~ sym.to_s
		#self.log.debug "Autoloading mutator %p" % sym
		self.add_attribute_mutator( sym )
	# Accessor
	else
		#self.log.debug "Autoloading accessor %p" % sym
		self.add_attribute_accessor( sym )
	end

	# Don't use #send to avoid infinite recursion in case method
	# definition has failed for some reason.
	self.method( sym ).call( *args )
end

Class Attribute Details

.load_pathObject

Returns the value of attribute load_path.



189
190
191
# File 'lib/arrow/template.rb', line 189

def load_path
  @load_path
end

Class Method Details

.attr_underbarred_accessor(sym) ⇒ Object

Create an attr_accessor method for the specified sym, but one which will look for instance variables with any leading underbars removed.



258
259
260
261
262
263
264
265
266
# File 'lib/arrow/template.rb', line 258

def self::attr_underbarred_accessor( sym )
	ivarname = '@' + sym.to_s.gsub( /^_+/, '' )
	define_method( sym ) {
		self.instance_variable_get( ivarname )
	}
	define_method( "#{sym}=" ) {|arg|
		self.instance_variable_set( ivarname, arg )
	}
end

.attr_underbarred_reader(sym) ⇒ Object

Create an attr_reader method for the specified sym, but one which will look for instance variables with any leading underbars removed.



248
249
250
251
252
253
# File 'lib/arrow/template.rb', line 248

def self::attr_underbarred_reader( sym )
	ivarname = '@' + sym.to_s.gsub( /^_+/, '' )
	define_method( sym ) {
		self.instance_variable_get( ivarname )
	}
end

.find_file(file, path = []) ⇒ Object

Find the specified file in the given path (or the Template class’s #load_path if not specified).

Raises:

  • (TemplateError)


222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/arrow/template.rb', line 222

def self::find_file( file, path=[] )
	raise TemplateError, "Filename #{file} is tainted." if
		file.tainted?

	filename = nil
	path.collect {|dir| File.expand_path(file, dir).untaint }.each do |fn|
		Arrow::Logger[self].debug "Checking path %p" % [ fn ]
		if File.file?( fn )
			Arrow::Logger[self].debug "  found the template file at %p" % [ fn ]
			filename = fn
			break
		end

		Arrow::Logger[self].debug "  %p does not exist or is not a plain file." % [ fn ]
	end

	raise Arrow::TemplateError,
		"Template '%s' not found. Search path was %p" %
		[ file, path ] unless filename

	return filename
end

.load(name, path = []) ⇒ Object

Load a template from a file.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/arrow/template.rb', line 194

def self::load( name, path=[] )

	# Find the file on either the specified or default path
	path = self.load_path if path.empty?
	Arrow::Logger[self].debug "Searching for template '%s' in %d directories" %
	 	[ name, path.size ]
	filename = self.find_file( name, path )
	Arrow::Logger[self].debug "Found '%s'" % [ filename ]

	# Read the template source
	source = File.read( filename )
	source.untaint

	# Create a new template object, set its path and filename, then tell it
	# to parse the loaded source to define its behaviour. Parse is called
	# after the file and path are set so directives in the template can
	# use them.
	obj = new()
	obj._file = filename
	obj._load_path.replace( path )
	obj.parse( source )

	return obj
end

Instance Method Details

#_enclosing_templateObject

Return the template that is enclosing the receiver in the current context, if any.



379
380
381
# File 'lib/arrow/template.rb', line 379

def _enclosing_template
	self._enclosing_templates.last
end

#changed?Boolean

Returns true if the source file from which the template was read has been modified since the receiver was instantiated. Always returns false if the template wasn’t loaded from a file.

Returns:

  • (Boolean)


442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/arrow/template.rb', line 442

def changed?
	return false unless @file

	if File.exists?( @file )
		self.log.debug "Comparing creation time '%s' with file mtime '%s'" %
			[ @creation_time, File.mtime(@file) ]
		rval = File.mtime( @file ) > @creation_time
	end

	self.log.debug "Template file '%s' has %s" %
		[ @file, rval ? "changed" : "not changed" ]
	return rval
end

#initialize_copy(original) ⇒ Object

Initialize a copy of the original template object.



328
329
330
331
332
333
334
# File 'lib/arrow/template.rb', line 328

def initialize_copy( original )
	super

	@attributes = {}
	tree = original._syntax_tree.collect {|node| node.clone}
	self.install_syntax_tree( tree )
end

#inspectObject

Return a human-readable representation of the template object.



385
386
387
388
389
390
391
392
# File 'lib/arrow/template.rb', line 385

def inspect
	"#<%s:0x%0x %s (%d nodes)>" % [
		self.class.name,
		self.object_id * 2,
		@file ? @file : '(anonymous)',
		@syntax_tree.length,
	]
end

#install_node(node) ⇒ Object

Install the given node into the template object.



423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/arrow/template.rb', line 423

def install_node( node )
	#self.log.debug "Installing a %s %p" % [node.type, node]

	if node.respond_to?( :name ) && node.name
		unless @attributes.key?( node.name )
			#self.log.debug "Installing an attribute for a node named %p" % node.name
			@attributes[ node.name ] = nil
			self.add_attribute_accessor( node.name.to_sym )
			self.add_attribute_mutator( node.name.to_sym )
		else
			#self.log.debug "Already have a attribute named %p" % node.name
		end
	end
end

#install_syntax_tree(tree) ⇒ Object

Install a new syntax tree in the template object, replacing the old one, if any.



416
417
418
419
# File 'lib/arrow/template.rb', line 416

def install_syntax_tree( tree )
	@syntax_tree = tree
	@syntax_tree.each do |node| node.add_to_template(self) end
end

#make_rendering_scopeObject

Create an anonymous module to act as a scope for any evals that take place during a single render.



530
531
532
533
534
# File 'lib/arrow/template.rb', line 530

def make_rendering_scope
	# self.log.debug "Making rendering scope with attributes: %p" % [@attributes]
	scope = RenderingScope.new( @attributes )
	return scope
end

#memsizeObject

Return the approximate size of the template, in bytes. Used by Arrow::Cache for size thresholds.



397
398
399
# File 'lib/arrow/template.rb', line 397

def memsize
	@source ? @source.length : 0
end

#parse(source) ⇒ Object

Parse the given template source (a String) and put the resulting nodes into the template’s syntax_tree.



404
405
406
407
408
409
410
411
# File 'lib/arrow/template.rb', line 404

def parse( source )
	@source = source
	parserClass = @config[:parserClass]
	tree = parserClass.new( @config ).parse( source, self )

	#self.log.debug "Parse complete: syntax tree is: %p" % tree
	return self.install_syntax_tree( tree )
end

#postrender(enclosing_template = nil) ⇒ Object Also known as: after_rendering

Clean up after template rendering, calling each of its nodes’ #after_rendering hook.



517
518
519
520
521
522
523
524
# File 'lib/arrow/template.rb', line 517

def postrender( enclosing_template=nil )
	@syntax_tree.each do |node|
		# self.log.debug "    post-rendering %p" % [node]
		node.after_rendering( self ) if
			node.respond_to?( :after_rendering )
	end
	@enclosing_templates.pop
end

#postrender_done?Boolean

Returns true if this template has already been through a post-render.

Returns:

  • (Boolean)


510
511
512
# File 'lib/arrow/template.rb', line 510

def postrender_done?
	return @postrender_done
end

#prerender(enclosing_template = nil) ⇒ Object Also known as: before_rendering

Prep the template for rendering, calling each of its nodes’ #before_rendering hook.



465
466
467
468
469
470
471
472
473
# File 'lib/arrow/template.rb', line 465

def prerender( enclosing_template=nil )
	@enclosing_templates << enclosing_template

	@syntax_tree.each do |node|
		# self.log.debug "    pre-rendering %p" % [node]
		node.before_rendering( self ) if
			node.respond_to?( :before_rendering )
	end
end

#prerender_done?Boolean

Returns true if this template has already been through a pre-render.

Returns:

  • (Boolean)


458
459
460
# File 'lib/arrow/template.rb', line 458

def prerender_done?
	return @prerender_done
end

#render(nodes = nil, scope = nil, enclosing_template = nil) ⇒ Object Also known as: to_s

Render the template to text and return it as a String. If called with an Array of nodes, the template will render them instead of its own syntax_tree. If given a scope (a Module object), a Binding of its internal state it will be used as the context of evaluation for the render. If not specified, a new anonymous Module instance is created for the render. If a enclosing_template is given, make it available during rendering for variable-sharing, etc. Returns the results of each nodes’ render joined together with the default string separator (+$,+).



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/arrow/template.rb', line 485

def render( nodes=nil, scope=nil, enclosing_template=nil )
	rval = []
	nodes ||= self.get_prepped_nodes
	scope ||= self.make_rendering_scope

	self.prerender( enclosing_template )

	# Render each node
	nodes.each do |node|
		# self.log.debug "  rendering %p" % [ node ]
		begin
			rval << node.render( self, scope )
		rescue ::Exception => err
			rval << err
		end
	end

	return self.render_objects( *rval )
ensure
	self.postrender
end

#render_comment(message) ⇒ Object

Render the given message as a comment as specified by the template configuration.



569
570
571
572
573
574
575
576
577
# File 'lib/arrow/template.rb', line 569

def render_comment( message )
	comment = "%s%s%s\n" % [
		@config[:commentStart],
		message,
		@config[:commentEnd],
	]
	#self.log.debug "Rendered comment: %s" % comment
	return comment
end

#render_objects(*objs) ⇒ Object

Render the specified objects into text.



538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/arrow/template.rb', line 538

def render_objects( *objs )
	objs.collect do |obj|
		rval = nil
		key = (@renderers.keys & obj.class.ancestors).sort {|a,b| a <=> b}.first

		begin
			if key
				case @renderers[ key ]
				when Proc, Method
					rval = @renderers[ key ].call( obj, self )
				when Symbol
					methodname = @renderers[ key ]
					rval = obj.send( methodname )
				else
					raise TypeError, "Unknown renderer type '%s' for %p" %
						[ @renderers[key], obj ]
				end
			else
				rval = obj.to_s
			end
		rescue => err
			self.log.error "rendering error while rendering %p (a %s): %s" % 
				[obj, obj.class.name, err.message]
			@renderers[ ::Exception ].call( err, self )
		end
	end.join
end

#with_overridden_attributes(scope, hash) ⇒ Object

Call the given block, overriding the contents of the template’s attributes and the definitions in the specified scope with those from the pairs in the given hash.



583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/arrow/template.rb', line 583

def with_overridden_attributes( scope, hash )
	oldvals = {}
	begin
		hash.each do |name, value|
			#self.log.debug "Overriding attribute %s with value: %p" %
			#	[ name, value ]
			oldvals[name] = @attributes.key?( name ) ? @attributes[ name ] : nil
			@attributes[ name ] = value
		end
		scope.override( hash ) do
			yield( self )
		end
	ensure
		oldvals.each do |name, value|
			#self.log.debug "Restoring old value: %s for attribute %p" %
			#	[ name, value ]
			@attributes.delete( name )
			@attributes[ name ] = oldvals[name] if oldvals[name]
		end
	end
end