TDTemplateEngine is a template engine implemented in Objective-C, and intended for use on Apple's OS X and iOS platforms. It currently has decent unit test coverage.
Always use the Xcode Workspace TDTemplateEngine.xcworkspace
, NOT the Xcode Project.
TDTemplateEngine is built on top of PEGKit, my parsing toolkit for Cocoa. The PEGKit dependency is managed via git externals.
TDTemplateEngine is inspired by the Django template language, Java ServerPages, and MGTemplateEngine by Matt Gemmell.
In fact, TDTemplateEngine's API is very much inspired by MGTemplateEngine. And usage scenarios of the two tools are very similar. If you are in need of a template engine for Cocoa, you should definitely investigate MGTemplateEngine as well. It is more mature.
So if MGTemplateEngine exists, why create another one? What makes TDTemplateEngine different?
TDTemplateEngine offers a multiple-pass architecture. This means that TDTemplateEngine's API allows renderinging of any given template in 2 distinct phases:
- Compile the template source to an intermediate Abstract Syntax Tree (AST) representation.
- Render the AST to the final template text result.
This multiple-pass architecture has many advantages:
- A clean, well-factored, classic archecture that should be recognizable for anyone familiar with compiler design basics. It should be easy to jump into the code and add or remove features without breaking things.
- Any use case where a template must be loaded into memory and compiled once, but rendered with runtime data multiple times should see a performance benefit.
- TDTemplateEngine's expression language should exhibit good performance in such compile-once, render-often scenarios: tempate expressions are compiled once, and some compile-time optimizations are performed before render-time. Currently, the expression language optimizations are quite rudimentary, but the hooks are there, and they could be further improved with well-known compiler optimization techniques.
- With a powerful parser, multiple parser passes, and an intermediate AST representation, TDTemplateEngine's expression language should be easy to extend and improve with new features.
This multiple-pass architecture may have disadvantages:
- One man's clean, well-factored design, is another man's lasanga code -- too many layers.
- MGTemplateEngine (which I believe is a single-pass architecture) offers a smaller code footprint and simpler architecture. Again, if you're in the market, you should check it out too.
TDTemplateEngine also uses NSOutputStream
for output, rather than only offering an in-memory string output option. This should offer performance and memory-usage benefits for use cases where rendering a template produces large text output at runtime.
The downside of streaming output is that the simple render-to-in-memory-string use case is slightly more complex (but only slightly).
TDTemplateEngine template syntax is very similar to MGTemplateEngine and Django. Tag delimiters like {{
}}
and {%
%}
are easily configurable.
Print Tags print the value an expression to the text output:
My name is {{name}}.
Builtin Filters are available:
My name is {{firstName|capitalize}} {{lastName|capitalize}}, and I'm a {{profession|lowercase}}.
Mah kitteh sez "{{lolSpeak|trim|uppercase}}".
{{'Hello World!'|replace:'hello', 'Goodbye Cruel', 'i'}}
{{degrees|round}}
{{degrees|fmt:'%0.1f'}}
{{degrees|ceil|fmt:'%0.1f'}}
{{'now'|fmtDate:'EEE, MMM d, yy'}}
You can define your own Filters in ObjC by subclassing TDFilter
and overriding -[TDFilter doFilter:withArguments:]
.
If Tags offer conditional rendering based on input variables at render time:
{% if testVal <= expectedMax || force %}
Text 1.
{% elif shortCircuit or ('foo' == bar and 'baz' != bat) %}
Text 2.
{% else %}
Default Text.
{% /if %}
(Note the boolean test expressions in this example are nonsense, and just intended to demonstrate some of the expression language features.)
For Tags can loop thru arbitrary numerical ranges, and may nest:
{% for i in 0 to 10 %}
{% for j in 0 to 2 %}
{{i}}:{{j}}
{% /for %}
{% /for %}
Numerical ranges may iterate in reverse order, and also offer a "step" option specified after the by
keyword
{% for i in 70 to 60 by 2 %}
{{i}}{% if not currentLoop.isLast %},{% /if %}
{% /for %}
Prints:
70,68,66,64,62,60
Note that each For Tag offers access to a currentLoop
variable which provides information like currentIndex
, isFirst
, isLast
, and parentLoop
.
For Tags can also loop thru variables representing Cocoa collection objects like NSArray
, or NSSet
:
{% for obj in vec %}
{{obj}}
{% /for %}
and NSDictionary
(note the convenient unpacking of both key and value):
{% for key, val in dict %}
{{key}}:{{val}}
{% /for %}
Skip Tags can be used to skip the remainder of the current iteration of a For Tag loop.
Skip Tags are are similar to the continue
statement in most C-inspired languages (like JavaScript, Java, C++, ObjC, etc) with one important enhancement.
Skip Tags may contain an optional boolean expression. If the expression evaluates true, the Skip Tag is respected, and the remainder of the current iteration of the enclosing For Tag is skipped. However, if the expression evaluates false, the skip tag is ignored, and the current iteration of the For Tag carries on as normal.
The expression within the Skip Tag is essentially a syntactical shortcut. The two following forms are semantically equivalent, but the second is more convenient:
{% for i in 1 to 3 %}
{% if i == 2 %}
{% skip %}
{% /if %}
{{i}}
{% /if %}
{% for i in 1 to 3 %}
{% skip i == 2 %}
{{i}}
{% /if %}
Both examples produce the following output:
13
If no expression is present in the Skip Tag, it is always respected, and the current iteration of the enclosing For Tag is always skipped.
As with any templating mechanism, whitespace handling is often a significant concern. TDTemplateEngine includes two optional tags that can be used to simplify whitespace handling.
The Trim Tag is a block tag that trims both the leading and trailing whitespace from any lines contained within their body content.
Also, any lines inside the Trim Tag bodies containing only other Tags are removed from the output entirely. All empty lines are preserved in the output (but their leading and trailing whitespace is trimmed).
So the following:
{% trim %}
{% if true %}
Make it so.
{% /if %}
{% /trim %}
Produces a single line with all leading and trailing whitespace trimmed:
Make it so.
Indentation within Trim Tags may be controlled with nested Indent Tags. The following:
{% trim %}
{% if true %}
{% indent %}
Make it so.
{% /indent %}
{% /if %}
{% /trim %}
Produces a single line indented by 4 spaces:
Make it so.
You can implement your own custom Tags by subclassing TDTag
and overriding -[TDTag doTagInContext:]
.
As you have seen in the examples above, many tags may contain simple expressions which should be familiar to anyone with experience using JavaScript.
Logical And Or and Not may be expressed using either the familiar JavaScript operators (&&
, ||
, !
), or their english equivalents:
a && b
a and b
a || b
a or b
!a
not a
Variable equality and inequality may be tested using either the familiar JavaScript operators (==
, !=
), or their equivalents (eq
, ne
):
a == b
a eq b
a != b
a ne b
Note that a = b
is a syntax error, as assignments are not allowed in the expression language, and the correct equality operator is ==
, not =
.
Variables may be compared using either the familiar JavaScript operators (<
, <=
, >
, >=
), or their equivalents (lt
, le
, gt
, ge
):
a < b
a lt b
a <= b
a le b
a > b
a gt b
a >= b
a ge b
Arithmetic may be performed using either the familiar JavaScript operators:
a + b
a - b
a * b
a / b
The modulo operator for finding the remainder after division of one number by another is supported:
a % b
And explicity negative numbers are supported:
-a
Properties of objects may be reached using a chain of property references called a Path Expression:
person.address.zipCode
Boolean literals are available matching the JavaScript and Objective-C languages:
true
false
YES
NO
Number literals may appear either as integers or as floating point numbers with an optional exponent:
42
3.14
16.162e10−36
String literals may be wrapped in either single or double quotes:
"I'm surrounded by assholes."
'Evil will always triumph, because Good is dumb.'
Any expression may be wrapped in parentheses for clarity or to alter the order of operations.
(a + b) / c
((a or b) and (c or d))
Create a TDTemplateEngine
object and render a template in two distinct phases: (compile and render) to an NSOutputStream
:
// create an engine
TDTemplateEngine *eng = [TDTemplateEngine templateEngine];
// compile the template at a given file path to an AST
NSError *err = nil;
TDNode *tree = [eng compileTemplateFile:path encoding:NSUTF8StringEncoding error:&err];
// provide a streaming output destination
NSOutputStream *stream = [NSOutputStream outputStreamToMemory];
// establish runtime template variable values
id vars = @{@"foo": @"bar"};
// render tree to template text
err = nil;
[eng renderTemplateTree:tree withVariables:vars toStream:stream error:&err];
Or use the convenience API for compile+render via one method call:
TDTemplateEngine *eng = [TDTemplateEngine templateEngine];
NSOutputStream *stream = [NSOutputStream outputStreamToMemory];
NSError *err = nil;
BOOL success = [eng processTemplateString:input withVariables:vars toStream:stream error:&err];
NSString *output = [[[NSString alloc] initWithData:[stream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] encoding:NSUTF8StringEncoding] autorelease];
The TDTemplateEngine
class is NOT thread-safe. Each TDTemplateEngine
object must be created on and receive messages on only one thread. However, this need not be the main thread. It may be done on a background thread if you like. In fact, it's probably best to restrict creation and usage of a TDTemplateEngine
object to a background thread.