Class: Inversion::Template

Inherits:
Object
  • Object
show all
Extended by:
Configurability, Loggability
Includes:
DataUtilities, TemplateTiltAdditions
Defined in:
lib/inversion/tilt.rb,
lib/inversion/template.rb

Overview

The main template class.

Inversion templates are the primary objects you’ll be interacting with. Templates can be created from a string:

Inversion::Template.new( template_source )

or from a file:

Inversion::Template.load( 'path/to/template.tmpl' )

Template Options

Inversion supports the [Configurability](rubygems.org/gems/configurability) API, and registers itself with the ‘templates` key. This means you can either add a `templates` section to your Configurability config, or call ::configure yourself with a config Hash (or something that quacks like one).

To set options on a per-template basis, you can pass an options hash to either Inversion::Template::load or Inversion::Template::new, or set them from within the template itself using the [config tag](Tags@config).

The available options are:

:ignore_unknown_tags

Setting to false causes unknown tags used in templates to raise an Inversion::ParseError. Defaults to ‘true`.

:on_render_error

Dictates the behavior of exceptions during rendering. Defaults to ‘:comment`.

:ignore

Exceptions are silently ignored.

:comment

Exceptions are rendered inline as comments.

:propagate

Exceptions bubble up to the caller of Inversion::Template#render.

:debugging_comments

Insert various Inversion parse and render statements while rendering. Defaults to ‘false`.

:comment_start

When rendering debugging comments, the comment is started with these characters. Defaults to "<!--".

:comment_end

When rendering debugging comments, the comment is finished with these characters. Defaults to "-->".

:template_paths

An array of filesystem paths to search for templates within, when loaded or included with a relative path. The current working directory is always the last checked member of this. Defaults to [].

:escape_format

The escaping used by tags such as ‘escape` and `pp`. Default: `:html`.

:strip_tag_lines

If a tag’s presence introduces a blank line into the output, this option removes it. Defaults to ‘true`.

:stat_delay

Templates know when they’ve been altered on disk, and can dynamically reload themselves in long running applications. Setting this option creates a purposeful delay between reloads for busy servers. Defaults to ‘0` (disabled).

:strict_attributes

Disable getting/setting attributes that aren’t explicitly declared with a tag. Trying to get/set an attribute that isn’t declared in the template with this option enabled will result in a NoMethodError being raised.

Defined Under Namespace

Modules: ContainerTag Classes: AttrTag, BeginTag, CallTag, CodeTag, CommentTag, ConfigTag, DefaultTag, ElseTag, ElsifTag, EndTag, EscapeTag, ForTag, FragmentTag, IfTag, ImportTag, IncludeTag, Node, PpTag, PublishTag, RescueTag, SubscribeTag, Tag, TextNode, TimeDeltaTag, UnlessTag, UriencodeTag, YieldTag

Constant Summary collapse

VALID_ERROR_ACTIONS =

Valid actions for ‘on_render_error’

[
	:ignore,
	:comment,
	:propagate,
]
DEFAULT_CONFIG =

Default config values

{
	# Loading/parsing options
	:ignore_unknown_tags => true,
	:template_paths      => [],
	:stat_delay          => 0,
	:strict_attributes   => false,

	# Rendering options
	:on_render_error     => :comment,
	:debugging_comments  => false,
	:comment_start       => '<!-- ',
	:comment_end         => ' -->',
	:escape_format       => :html,
	:strip_tag_lines     => true,
}.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataUtilities

deep_copy

Methods included from TemplateTiltAdditions

#each

Constructor Details

#initialize(source, parsestate = nil, opts = {}) ⇒ Template

Create a new Inversion:Template with the given ‘source`.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/inversion/template.rb', line 260

def initialize( source, parsestate=nil, opts={} )
	if parsestate.is_a?( Hash )
		# self.log.debug "Shifting template options: %p" % [ parsestate ]
		opts = parsestate
		parsestate = nil
	else
		self.log.debug "Parse state is: %p" % [ parsestate ]
	end

	@source       = source
	@node_tree    = [] # Parser expects this to always be an Array
	@options      = self.class.config.merge( opts )
	@attributes   = {}
	@fragments    = {}
	@source_file  = nil
	@created_at   = Time.now
	@last_checked = @created_at

	self.parse( source, parsestate )
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

Proxy method: handle attribute readers/writers for attributes that aren’t yet defined.

Raises:

  • (NoMethodError)


393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/inversion/template.rb', line 393

def method_missing( sym, *args, &block )
	return super unless sym.to_s =~ /^([a-z]\w+)=?$/i
	attribute = $1

	raise NoMethodError, "no tag attribute '%s' (strict mode)" % [ attribute ] if
		self.options[:strict_attributes]

	self.install_accessors( attribute )

	# Call the new method via #method to avoid a method_missing loop.
	return self.method( sym ).call( *args, &block )
end

Class Attribute Details

.configObject

Returns the value of attribute config.



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

def config
  @config
end

.template_pathsObject

Returns the value of attribute template_paths.



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

def template_paths
  @template_paths
end

Instance Attribute Details

#attributesObject (readonly)

The Hash of template attributes



301
302
303
# File 'lib/inversion/template.rb', line 301

def attributes
  @attributes
end

#fragmentsObject (readonly)

The Hash of rendered template fragments



304
305
306
# File 'lib/inversion/template.rb', line 304

def fragments
  @fragments
end

#node_treeObject (readonly)

The node tree parsed from the template source



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

def node_tree
  @node_tree
end

#optionsObject (readonly)

The Template’s configuration options hash



307
308
309
# File 'lib/inversion/template.rb', line 307

def options
  @options
end

#sourceObject (readonly)

The raw template source from which the object was parsed.



295
296
297
# File 'lib/inversion/template.rb', line 295

def source
  @source
end

#source_fileObject

The Pathname of the file the source was read from



298
299
300
# File 'lib/inversion/template.rb', line 298

def source_file
  @source_file
end

Class Method Details

.add_extensions(*modules) ⇒ Object

Add one or more extension ‘modules` to Inversion::Template. This allows tags to decorate the template class with new functionality.

Each one of the given ‘modules` will be included as a mixin, and if it also contains a constant called ClassMethods and/or PrependedMethods, it will also be extended/prepended (respectively) with it.

Example

Add a layout attribute to templates from a ‘layout’ tag:

class Inversion::Template::LayoutTag < Inversion::Tag

  module TemplateExtension

    def layout
      return @layout || 'default.tmpl'
    end

    module PrependedMethods
      def initialize( * )
        super
        @layout = nil
      end
  end

  Inversion::Template.add_extensions( TemplateExtension )

  # ... more tag stuff

end


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/inversion/template.rb', line 241

def self::add_extensions( *modules )
	self.log.info "Adding extensions to %p: %p" % [ self, modules ]

	modules.each do |mod|
		include( mod )
		if mod.const_defined?( :ClassMethods )
			submod = mod.const_get( :ClassMethods )
			extend( submod )
		end
		if mod.const_defined?( :PrependedMethods )
			submod = mod.const_get( :PrependedMethods )
			prepend( submod )
		end
	end

end

.configure(config) ⇒ Object

Configure the templating system.



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/inversion/template.rb', line 150

def self::configure( config )
	if config
		Inversion.log.debug "Merging config %p with current config %p" % [ config, self.config ]
		merged_config = DEFAULT_CONFIG.merge( config )
		self.template_paths = Array( merged_config.delete(:template_paths) )
		self.config = merged_config
	else
		defaults = DEFAULT_CONFIG.dup
		self.template_paths = defaults.delete( :template_paths )
		self.config = defaults
	end
end

.load(path, parsestate = nil, opts = {}) ⇒ Object

Read a template object from the specified ‘path`.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/inversion/template.rb', line 165

def self::load( path, parsestate=nil, opts={} )

	# Shift the options hash over if there isn't a parse state
	if parsestate.is_a?( Hash )
		opts = parsestate
		parsestate = nil
	end

	tmpl = nil
	path = Pathname( path )
	opts[:template_paths] ||= self.template_paths
	search_path = opts[:template_paths] + [ Dir.pwd ]
	self.log.debug "Searching template paths: %p" % [ search_path ]

	# Unrestricted template location.
	if path.absolute?
		tmpl = path

	# Template files searched under paths specified in 'template_paths', then
	# the current working directory. First match wins.
	else
		tmpl = search_path.collect {|dir| Pathname(dir) + path }.find do |fullpath|
			fullpath.exist?
		end

		raise RuntimeError, "Unable to find template %p within configured paths %p" %
			[ path.to_s, search_path ] if tmpl.nil?
	end

	# We trust files read from disk
	source = if opts.key?( :encoding )
			tmpl.read( encoding: opts[:encoding] )
		else
			tmpl.read
		end

	# Load the instance and set the path to the source
	template = self.new( source, parsestate, opts )
	template.source_file = tmpl

	return template
end

Instance Method Details

#changed?Boolean

Returns ‘true` if the template was loaded from a file and the file’s mtime is after the time the template was created.

Returns:

  • (Boolean)


326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/inversion/template.rb', line 326

def changed?
	return false unless file = self.source_file
	now = Time.now

	if now > ( @last_checked + self.options[ :stat_delay ].to_i )
		if file.mtime > @last_checked
			@last_checked = now
			return true
		end
	end
	return false
end

#initialize_copy(other) ⇒ Object

Copy constructor – make copies of some internal data structures, too.



283
284
285
286
287
# File 'lib/inversion/template.rb', line 283

def initialize_copy( other )
	@options    = deep_copy( other.options )
	@attributes = deep_copy( other.attributes )
	@fragments  = deep_copy( other.fragments )
end

#inspectObject

Return a human-readable representation of the template object suitable for debugging.



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/inversion/template.rb', line 369

def inspect
	nodemap = if $DEBUG
			", node_tree: %p" % [ self.node_tree.map(&:as_comment_body) ]
		else
			''
		end

	return "#<%s:%08x (loaded from %s) attributes: %p, options: %p%s>" % [
		self.class.name,
		self.object_id / 2,
		self.source_file ? self.source_file : "memory",
		self.attributes.keys,
		self.options,
		nodemap
	]
end

#reloadObject

If the template was loaded from a file, reload and reparse it from the same file.



314
315
316
317
318
319
320
321
# File 'lib/inversion/template.rb', line 314

def reload
	file = self.source_file or
		raise Inversion::Error, "template was not loaded from a file"

	self.log.debug "Reloading from %s" % [ file ]
	source = file.read
	self.parse( source )
end

#render(parentstate = nil, &block) ⇒ Object Also known as: to_s

Render the template, optionally passing a render state (if, for example, the template is being rendered inside another template).



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/inversion/template.rb', line 342

def render( parentstate=nil, &block )
	self.log.info "rendering template %#x" % [ self.object_id * 2 ]
	opts = self.options
	opts.merge!( parentstate.options ) if parentstate

	self.fragments.clear

	state = Inversion::RenderState.new( parentstate, self.attributes, opts, &block )

	# self.log.debug "  rendering node tree: %p" % [ @node_tree ]
	self.walk_tree {|node| state << node }
	self.log.info "  done rendering template %#x: %0.4fs" %
		[ self.object_id/2, state.time_elapsed ]

	if parentstate
		parentstate.fragments.merge!( state.fragments )
	else
		self.fragments.replace( state.rendered_fragments )
	end

	return state.to_s
end