Class: Arrow::Template

Inherits:
Object
  • Object
show all
Extends:
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

  • Michael Granger

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

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

HashMergeFunction

Class Attribute Summary

Class Method Summary

Instance Method Summary

Methods included from HashUtilities

#stringify_keys, #symbolify_keys

Methods inherited from Object

deprecate_class_method, deprecate_method, inherited

Methods included from Loggable

#log

Constructor Details

- (Template) initialize(content = nil, config = {})

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

- (Object) method_missing(sym, *args, &block) (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

+ (Object) load_path

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

+ (Object) attr_underbarred_accessor(sym)

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

+ (Object) attr_underbarred_reader(sym)

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

+ (Object) find_file(file, path = [])

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

+ (Object) load(name, path = [])

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

- (Object) _enclosing_template

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

- (Object) add_attribute_accessor(sym) (protected)

Add a singleton accessor (getter) method for accessing the attribute specified by sym to the receiver.



683
684
685
686
687
688
689
690
691
692
693
694
# File 'lib/arrow/template.rb', line 683

def add_attribute_accessor( sym )
  name = sym.to_s.sub( /=$/, '' )

  code = %Q{
    def self::#{name}
      @attributes[#{name.inspect}]
    end
  }

  # $stderr.puts "Auto-defining accessor for #{name}: #{code}"
  eval( code, nil, "#{name} [Auto-defined]", __LINE__ )
end

- (Object) add_attribute_mutator(sym) (protected)

Add a singleton mutator (setter) method for accessing the attribute specified by sym to the receiver.



699
700
701
702
703
704
705
706
707
708
709
710
# File 'lib/arrow/template.rb', line 699

def add_attribute_mutator( sym )
  name = sym.to_s.sub( /=$/, '' )

  code = %Q{
    def self::#{name}=( arg )
      @attributes[ #{name.inspect} ] = arg
    end
  }

  # $stderr.puts "Auto-defining mutator for #{name}: #{code}"
  eval( code, nil, "#{name}= [Auto-defined]", __LINE__ )
end

- (Boolean) changed?

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

- (Object) get_prepped_nodes (protected)

Returns the syntax tree with its nodes prerendered in accordance with the template’s configuration.



613
614
615
616
617
618
619
620
# File 'lib/arrow/template.rb', line 613

def get_prepped_nodes
  tree = @syntax_tree.dup

  self.strip_directive_whitespace( tree ) if
    @config[:elideDirectiveLines]

  return tree
end

- (Object) initialize_copy(original)

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

- (Object) inspect

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

- (Object) install_node(node)

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

- (Object) install_syntax_tree(tree)

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

- (Object) make_rendering_scope

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

- (Object) memsize

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

- (Object) parse(source)

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

- (Object) postrender(enclosing_template = nil) 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

- (Boolean) postrender_done?

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

- (Object) prerender(enclosing_template = nil) 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

- (Boolean) prerender_done?

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

- (Object) render(nodes = nil, scope = nil, enclosing_template = nil) 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

- (Object) render_comment(message)

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

- (Object) render_objects(*objs)

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

- (Object) strip_directive_whitespace(tree) (protected)

Strip whitespace from the tails of textnodes before and the head of textnodes after lines consisting only of non-rendering directives in the given template syntax tree.



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
# File 'lib/arrow/template.rb', line 626

def strip_directive_whitespace( tree )
  # Make a flat list of all nodes
  nodes = tree.collect {|node| node.to_a}.flatten

  # Elide non-rendering directive lines. Match node lists like:
  #   <TextNode> =~ /\n\s*$/
  #   <NonRenderingNode>*
  #   <TextNode> =~ /^\n/
  # removing one "\n" from the tail of the leading textnode and the
  # head of the trailing textnode. Trailing textnode can also be a
  # leading textnode for another series.
  nodes.each_with_index do |node,i|
    leadingNode = nodes[i-1]

    # If both the leading node and the current one match the
    # criteria, look for a trailing node.
    if i.nonzero? && leadingNode.is_a?( TextNode ) &&
        leadingNode =~ /\n\s*$/s

      # Find the trailing node. Abandon the search on any
      # rendering directive or text node that includes a blank line.
      trailingNode = nodes[i..-1].find do |node|
        break nil if node.rendering?
        node.is_a?( TextNode ) && node =~ /^\n/
      end

      leadingNode.body.sub!( /\n\s*$/, '' ) if trailingNode
    end
  end
end

- (Object) with_overridden_attributes(scope, hash)

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