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' )
Inversion supports the 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 ::load or ::new, or set them from within the template itself using the config tag.
The available options are:
Setting to false causes unknown tags used in templates to raise an Inversion::ParseError. Defaults to
true
.
Dictates the behavior of exceptions during rendering. Defaults to
:comment
.
Exceptions are silently ignored.
Exceptions are rendered inline as comments.
Exceptions bubble up to the caller of #render.
Insert various Inversion parse and render
statements while rendering. Defaults to false
.
When rendering debugging comments, the comment is started with these
characters. Defaults to "<!--"
.
When rendering debugging comments, the comment is finished with these
characters. Defaults to "-->"
.
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 []
.
The escaping used by tags such as escape
and pp
.
Default: :html
.
If a tag's presence introduces a blank line into the output, this
option removes it. Defaults to true
.
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).
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.
Add the mixin to Template
Default config values
This is the parser for Inversion templates. It takes template source and returns a tree of Inversion::Template::Node objects (if parsing is successful).
Valid actions for 'on_render_error'
The Hash of template attributes
The Hash of rendered template fragments
The node tree parsed from the template source
The Template's configuration options hash
The raw template source from which the object was parsed.
The Pathname of the file the source was read from
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.
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
# File lib/inversion/template.rb, line 245
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 the templating system.
# File lib/inversion/template.rb, line 153
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
Read a template object from the specified path
.
# File lib/inversion/template.rb, line 168
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
source.untaint
# Load the instance and set the path to the source
template = self.new( source, parsestate, opts )
template.source_file = tmpl
return template
end
Create a new Inversion:Template with the given source
.
# File lib/inversion/template.rb, line 264
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
Returns true
if the template was loaded from a file and the
file's mtime is after the time the template was created.
# File lib/inversion/template.rb, line 330
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
Copy constructor – make copies of some internal data structures, too.
# File lib/inversion/template.rb, line 287
def initialize_copy( other )
@options = deep_copy( other.options )
@attributes = deep_copy( other.attributes )
@fragments = deep_copy( other.fragments )
end
Return a human-readable representation of the template object suitable for debugging.
# File lib/inversion/template.rb, line 373
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
If the template was loaded from a file, reload and reparse it from the same file.
# File lib/inversion/template.rb, line 318
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 the template, optionally passing a render state (if, for example, the template is being rendered inside another template).
# File lib/inversion/template.rb, line 346
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
Add attributes for the given node
's identifiers.
# File lib/inversion/template.rb, line 446
def add_attributes_from_node( node )
if node.respond_to?( :identifiers )
node.identifiers.each do |id|
next if @attributes.key?( id.to_sym )
@attributes[ id.to_sym ] = nil
end
end
end
Search for identifiers in the template's node tree and declare an accessor for each one that's found.
# File lib/inversion/template.rb, line 434
def define_attribute_accessors
self.walk_tree do |node|
self.add_attributes_from_node( node )
end
self.attributes.each do |key, _|
self.install_accessors( key )
end
end
Install reader and writer methods for the attribute associated with the
specified key
.
# File lib/inversion/template.rb, line 457
def install_accessors( key )
reader, writer = self.make_attribute_accessors( key )
self.singleton_class.send( :define_method, key, &reader )
self.singleton_class.send( :define_method, "#{key}=", &writer )
end
Make method bodies
# File lib/inversion/template.rb, line 466
def make_attribute_accessors( key )
key = key.to_sym
reader = lambda { self.attributes[key] }
writer = lambda {|newval| self.attributes[key] = newval }
return reader, writer
end
Proxy method: handle attribute readers/writers for attributes that aren't yet defined.
# File lib/inversion/template.rb, line 397
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
Parse the given source
into the template node tree.
# File lib/inversion/template.rb, line 412
def parse( source, parsestate=nil )
opts = self.class.config.merge( self.options )
parser = Inversion::Parser.new( self, opts )
@attributes.clear
@node_tree = parser.parse( source, parsestate )
@source = source
self.define_attribute_accessors
end
Walk the template's node tree, yielding each node in turn to the given block.
# File lib/inversion/template.rb, line 425
def walk_tree( nodes=@node_tree, &block )
nodes.each do |node|
yield( node )
end
end