cs/Templatecopyright (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 |
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= |
using Ruby Gems
# gem -Ri cstemplatefrom 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
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).
This code will produce the following document: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)Interesting things I've done A leaflet by Dallas Diegler email: whack@mole.com
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 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 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"
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 ENDRoot 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"
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 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"
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 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 the text contents of other files with the include macro.
template.load("${include /proc/uptime}") p (template.output =~ /^\d+\.\d+ \d+\.\d+/) == 0
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"
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.
The following are macros which can be put into any cs/Template template.
${var path}Emits the value at the path location.
${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}Block execution resumes after this macro if the previous matching if macro evaluated to false.
${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 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}Must be placed at the end of any block started with a if, with or each macro.
${include filename}Inserts a text file into the final document.
${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.
require "celsoft.com/template"
Template::Document.new()Creates a new instance of Template::Document.
template#dataReturns the data structure used by the template.
template#data = data_structureSets the data structure to be used by the template. This may be any arrangement of Hashes, Arrays and Objects (as values).
template#load(input)Loads and parses the source template document. Input may be a String or an Array.
template#outputMerges the template data with the template source document and returns a string which is the final, fully resolved document.
template#resolve(path)Returns the value located in the data indicated by path. Uses ordinary paths instead of macros.
template#treeReturns the parsed template.
template#tree = array_of_literals_and_macrosSets 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" ]