cs/Template

copyright (c) 2004 Sean O'Dell

Purpose

cs/Template is a fast, generic text templating engine for Ruby, written in C. Merge native Ruby data structures with text templates to produce final documents. Data structures may be any combination of Hashes, Arrays and Objects (works great with YAML input). Produces any type of text document. Expand variables with path-like addressing, include files, eval Ruby code expressions, execute conditional if/then/else blocks or looping blocks of text.

current version: 0.5.1
project homepage: http://rubyforge.org/projects/cstemplate/
download url: cstemplate-0.5.1.tar.gz
changes: CHANGELOG
license: GPL

Table of Contents
    Quick Lesson
    Tutorials
        About Templates
        Data Input
        Output
        Path Address Primer
        Expanding Variables
        If/Then/Else Blocks
        Changing the Context
        Looping Blocks
        Include Other Files
        Ruby Code Expressions
    How It Works
    Macro Reference
        var macro
        if macro
        else macro
        with macro
        each macro
        end macro
        include macro
        eval macro
    Template::Document Class
        Template::Document.new
        template#data
        template#data=
        template#load
        template#output
        template#resolve
        template#tree
        template#tree=

Installation

using Ruby Gems

# gem -Ri cstemplate

from source

Download cstemplate-0.5.1.tar.gz to a working directory, then issue the following commands:

$ tar -xzvf cstemplate-0.5.1.tar.gz

$ ruby setup.rb config
$ ruby setup.rb setup
# ruby setup.rb install

Using cs/Template

Quick Lesson

Ruby Gems

If you installed cs/Template with gem, be sure to add the following code to the top of your scripts:

require "rubygems"
require_gem "cstemplate"

Example

The following example uses YAML as a source of data to produce a simple document, and prints it to the screen. YAML is perfect as input to a cs/Template document because it represents data as a structure of Hashes, Arrays and Objects (as values).

require "celsoft.com/template"
require "yaml"

yaml_text = <<-END
---
author:
  name : Dallas Diegler
  email: whack@mole.com
title : Interesting things I've done
type  : leaflet
END

template_text = <<-END
${var title}
    A ${var type} by ${var author/name}
    email: ${var author/email}
END

yaml_doc = YAML::load(yaml_text)

template = Template::Document.new
template.load(template_text)
template.data = yaml_doc

print(template.output)
This code will produce the following document:
Interesting things I've done
    A leaflet by Dallas Diegler
    email: whack@mole.com

Tutorials

About Templates

Text templates are simply text with embedded macros that cs/Template recognizes and responds to. Macros take the following form:

${macroname [ parameter, ... ]}

You can embed these macros anywhere in your template; cs/Template considers text templates ordinary text files, and pays no attention to XML, HTML, TeX or any other syntax.

Loading a template

To load a template, create a Template::Document object and call its load method, passing either a string or an array of strings.

template = Template::Document.new
template.load("Now is the time for all good ${var plural_noun} to\
 come to the aid of their country.\n")

Escaping the '$' symbol

The only time you need to escape the '$' symbol when you wish it to appear literally, is if it happens to appear immediately before a '{' character. In this case, simply add a backslash ('\') character so the sequence of characters looks like this: '\${'. No other characters need to be escaped.

Data Input

Data can be any arrangement of Hashes, Arrays and Objects (as values). YAML is a good source, but you can build your own data structure or use one from another source.

template.data = {"plural_noun" => "Rubyists"}

Output

Output is generated whenever you access a template object's output method.

p template.output == "Now is the time for all good Rubyists to\
 come to the aid of their country.\n"

Path Address Primer

Paths are string addresses that allow you to reach into your data structure to specify individual locations, known as "nodes." Many macros take a path parameter, so it helps to understand what a path really is, how to create and interpret them, and to understand the concept of the current "context" in which paths are resolved.

Path addresses

Path addresses are a series of node names, starting at the top node and working into your data structure to the node you are targetting. Each name is separated by a delimiter, either a slash ('/') or a hash ('#'). Slashes separate Hash key names, and hash marks separate Array indices (sounds confusing, but visually it is very clear). You can mix the delimiters any way you need to in order to address the node you are after.

Root and context

Initially, the object you set as your template data is both the root and context node. Some macros change the context node temporarily, however, and your paths may be addressed relative to either the root or context node. Paths beginning with the slash ('/') character are always relative to the root node. Paths beginning with a node name or index ('#' + number) are always relative to the context node.

A special way to address the current context node is using a single period ('.'). When '.' appears at the beginning of a path, it denotes the context itself; useful when the current context is itself just a value (not a Hash or Array).

Example data

yaml_text = <<-END
---
a: A
b:
  c: C
  d:
    - e: E0
      f: F0
    - e: E1
      f: F1
END

Root addresses

Assume the root and context node are the same and analyze the address examples:

"/a" == "A"
"/b/c" == "C"
"/b/d#0/e" == "E0"

Context addresses

Assume the context node has been changed to the node "b" and analyze the address examples:

"c" == "C"
"d#0/e" == "E0"
"/a" == "A"

Expanding Variables

Variables are macros which retrieve values from the data, and are expressed in templates using the var macro and a path address.

Addressing examples

yaml_text = <<-END
---
a: A
b:
  c: C
  d:
    - e: E0
      f: F0
    - e: E1
      f: F1
END

template.data = YAML::load(yaml_text)

template.load("${var a}")
p template.output == "A"

template.load("${var b/c}")
p template.output == "C"

template.load("${var b/d#0/e}")
p template.output == "E0"

template.load("${var b/d#0/f}")
p template.output == "F0"

template.load("${var b/d#1/e}")
p template.output == "E1"

If/Then/Else Blocks

If/then/else blocks are macros which execute the "then" block when true, and an "else" block when false. They are expressed in templates using the if macro with a path statement whose value determines the true/false condition. If there is a value located at the path address, and is not an empty string (true), the block immediately following the if macro is executed, up to the next matching else or end macro. If there is no value, or the value is an empty string (false), execution skips to the next matching else or end macro and then resumes.

If/then/else block examples

yaml_text = <<-END
---
a: A
b:
  c: C
  d:
    - e: E0
      f: F0
    - e: E1
      f: F1
g: ""
END

template.data = YAML::load(yaml_text)

template.load("${if a}TRUE${else}FALSE${end}")
p template.output == "TRUE"

template.load("${if g}TRUE${else}FALSE${end}")
p template.output == "FALSE"

template.load("${if z}TRUE${else}FALSE${end}")
p template.output == "FALSE"

Changing the Context

You can change the current context node using the with macro (also the each macro, but each is explained in the next topic).

Conditional execution

If the node at path does not exist (is nil or an empty string), the block will not be executed.

With macro examples

yaml_text = <<-END
---
a: A
b:
  c: C
  d:
    - e: E0
      f: F0
    - e: E1
      f: F1
END

template.data = YAML::load(yaml_text)

template.load("${with a}${var .}${end}")
p template.output == "A"

template.load("${with z}${var .}${end}")
p template.output == ""

template.load("${with b}${var c}${end}")
p template.output == "C"

template.load("${with b}${var /a}${end}")
p template.output == "A"

template.load("${with b/d}${var #0/e}${end}")
p template.output == "E0"

template.load("${with b/d#1/e}${var .}${end}")
p template.output == "E1"

Looping Blocks

Looping blocks are macros which execute once for each node in the path given and which temporarily change the context node with each pass. They are expressed in templates using the each macro with a path statement which points to the node that will be iterated.

Conditional execution

If the node at path does not exist (is nil or an empty string), the block will not be executed at all.

Looping Strings

The each macro splits Strings at newline characters ('\n') and iterates over the result as an Array; see the next topic.

Looping Arrays

If path points to an Array (or a multi-line String converted to an array), the block is executed repeatedly for each item in the array. The context node is changed to each item before every execution of the block.

Looping Hashes

If path points to a Hash, the block is executed repeatedly for each key/value pair in the Hash, in key-sorted-order. The context node is set to a 2-item Array containing the key in the first position ([0]) and its associated value in the second position ([1]) for each execution of the block.

Each macro examples

yaml_text = <<-END
---
a: A
b:
  c: C
  d:
    - e: E0
      f: F0
    - e: E1
      f: F1
g:
  h: H
  i: I
  j: J
  
k: |
  K1
  K2
  K3
END

template.data = YAML::load(yaml_text)

template.load("${each a}${var .}${end}")
p template.output == "A"

template.load("${each g}${var #0}${end}")
p template.output == "hij"

template.load("${each g}${var #1}${end}")
p template.output == "HIJ"

template.load("${each b}${var /a}${end}")
p template.output == "AA"

template.load("${each b/d}${var e}${end}")
p template.output == "E0E1"

template.load("${each b/d#1/e}${var .}${end}")
p template.output == "E1"

template.load("${each k}${var .}${end}")
p template.output == "K1K2K3"

Include Other Files

Include the text contents of other files with the include macro.

template.load("${include /proc/uptime}")
p (template.output =~ /^\d+\.\d+ \d+\.\d+/) == 0

Ruby Code Expressions

Insert the string results of a Ruby code expression with the eval macro. Ruby code expressions are executed using the binding of an anonymous object which has three instance variables: @data, @tree and @context. @data is the root of the data structure used by the template, @tree is the parsed template document and @context is the current context node.

template.load("${eval Time.local(2004, 6, 18, 12, 1, 30).asctime}")
p template.output == "Fri Jun 18 12:01:30 2004"

template.load("${with a}${eval @context}${end}")
p template.output == "A"

How It Works

Producing final documents is really a three-step process. First, loading the data; second, loading the template; and third, resolving the final output.

When data is loaded, it is kept in its original form. Data can be any arrangement of Hashes, Arrays or Strings, and works very well with YAML documents. In fact, setting the template data really just sticks whatever you provide into an instance variable of the template object, with no checking, cloning or parsing performed.

When the template source document is loaded, it is parsed into an array of literals and macros. All the parsing at this stage is done with raw C code, and goes pretty fast. The pieces of the parsed template are stuffed into a Ruby Array object and exposed through the template's tree method.

When it's time to generate the final resolved document, the parsed template tree is walked and emitted, modified by any macros encountered. Var macros simply emit values in the data, and eval macros emit the string results of a given Ruby expression. Each macros cause the resolver to be re-entered in a recursive fashion and return when an end macro appears. Nodes in the data are looked up by C routines which parse the paths and walk through the data structure looking for the indicated node.

Macro Reference

The following are macros which can be put into any cs/Template template.

var macro

${var path}

Emits the value at the path location.

if macro

${if path} ... [ ${else} ... ] ${end}

Evalutes the node at path and executes the following block if true, otherwise execution skips to the next matching else (optional) or end macro. A node evalutes to true when it exists and is not an empty string, otherwise it evaluates to false. Does not change the current context.

else macro

${else}

Block execution resumes after this macro if the previous matching if macro evaluated to false.

with macro

${with path} ... ${end}

Emits the block following this macro once, up to a matching end macro, after setting the context to the node indicated by path. If the node at the path does not exist, or is an empty string, the block is not executed.

each macro

${each path} ... ${end}

Repeatedly emits the block following this macro up to a matching end macro. If the node is a String, it is split at newline characters ('\n') and the result is iterated over as an Array. If the node is an Array (or a String converted to an Array), the block is repeated for every item in the array, after setting the context to each item. If the node indicated by path is a Hash, the block is emitted once for every value in the hash, sorted by the keys, setting the context to a 2-item array containing the key/value pair. If the node at the path does not exist, or is an empty string, the block is never executed. For any other node which is not nil, the block is executed once, after setting the current context to the node.

end macro

${end}

Must be placed at the end of any block started with a if, with or each macro.

include macro

${include filename}

Inserts a text file into the final document.

eval macro

${eval string_expression}

Executes Ruby code and places the string result in the final document. The code expression is executed using the binding of an anonymous object with three instance variables: @data, @tree and @context.

Template::Document Class

require "celsoft.com/template"

Template::Document.new

Template::Document.new()

Creates a new instance of Template::Document.

template#data

template#data

Returns the data structure used by the template.

template#data=

template#data = data_structure

Sets the data structure to be used by the template. This may be any arrangement of Hashes, Arrays and Objects (as values).

template#load

template#load(input)

Loads and parses the source template document. Input may be a String or an Array.

template#output

template#output

Merges the template data with the template source document and returns a string which is the final, fully resolved document.

template#resolve

template#resolve(path)

Returns the value located in the data indicated by path. Uses ordinary paths instead of macros.

template#tree

template#tree

Returns the parsed template.

template#tree=

template#tree = array_of_literals_and_macros

Sets the parsed template. A parsed template is an array of literal values and macros. Literal values are Strings and macros are 2-item Arrays which contain the name of the macro and the macro parameter. You can set the parsed template directly, instead of loading it from a source template document, if you wish.

template.load("Now is the time for all good ${var plural_noun} to\
  come to the aid of their country.\n")

p template.tree == [
  "Now is the time for all good ",
  ["var", "plural_noun"],
  " to come to the aid of their country.\n"
]