Chunk Documentation

From Chunk Java Template Engine

Jump to: navigation, search

Contents

[edit] How to get Chunk

Visit the Chunk Templates Project Home on Github or download the latest release.

The Chunk Templates library is now available as an artifact on Maven Central. Add to your maven pom.xml:

   <dependency>
     <groupId>com.x5dev</groupId>
     <artifactId>chunk-templates</artifactId>
     <version>3.6.2</version>
   </dependency>

[edit] What is Chunk

Chunk is a Template Engine for Java, similar to Apache Velocity or FreeMarker. Chunk has been field-tested in many applications.

Features:

  • Compatible with Android and GAE (Google App Engine) and Spring MVC.
  • Nestable loops and conditionals (if-elseIf-else).
  • Includes and Macros for easy composition.
  • Curly-brace {$tags} pop nicely in a backdrop full of <AngleBrackets>.
  • Flexible null-handling; template designer may specify default tag values.
  • Swiss army knife of powerful in-tag filters, including regex (regular expressions), sprintf.
  • Localization framework.
  • Rapid MVC: Glue a "model" object (or objects) to a "view" template with a single line of controller code.
  • Define multiple snippets per template file.
  • Support for theme layers and inheritance.
  • Highly optimized codebase.
  • Hooks for extending.
  • Eclipse Template Editor plugin available with syntax highlighting & more.


[edit] THE "CHUNK" TEMPLATING SYSTEM

[edit] Introduction

Philosophy: presentation code (HTML) and source code (Java) are like oil and water. They shouldn't mix, and when they do, it's not pretty.

cf php, jsp, asp, ... or java servlets with giant HTML strings hardcoded into the source. Blegh.

Fortunately, keeping layout and business logic apart is a breeze with a good templating system. Chunk aims to be just that.


Q: Does the Java community really need yet another templating engine?

A: Popular solutions like Freemarker and Velocity are targeted at software engineers, but require them to learn totally new (and ill-chosen) syntax for doing the same things they do in their base development languages. Chunk offers a much simpler, easy-to-grasp syntax and an intentionally simple, stateless featureset. Chunk templates have a laser-focused job to do: format data that's been "blessed" for presentation. The "View" layer of your app should not be triggering application code or making method calls on application objects. This division of responsibility keeps your presentation templates simple enough that designers can create and edit them independently. Simple template contracts encourage application developers to keep business logic in the Java code, and to pass clean static/model data off to the template engine for presentation.


Q: Does it cost anything to use?

A: Chunk will always be 100% free software.


Q: Where can I get my hands on it?

A: Download Latest Release of Chunk from GitHub. Tinker with a clone from GitHub. Or, just add it to your Maven build:

   <dependency>
     <groupId>com.x5dev</groupId>
     <artifactId>chunk-templates</artifactId>
     <version>3.6.2</version>
   </dependency>

Q: I hate stuffy documentation. I learn by example. Is there a recipes page?

A: Start here: Chunk Recipes - served using Chunk, of course!


Q: Requirements - What version of Java do I need? Any dependencies?

A: Chunk works best on Java 1.5 and up. Some features require json-smart or jeplite but most functionality just requires the main chunk templates jar.


Q: How do I use Chunk with Spring MVC?

A: See the Spring MVC integration guide.


Q: Who are you?

A: X5 Software is Tom McClure - web developer, musician and dad. My day job is maintaining part of the stack behind Pandora. Read my blog at dagblastit.com.



[edit] Getting Started: Creating HTML with Chunk

Here's a quick-start guide (or skip straight to the Examples):

        Standard Chunk Tag Syntax
        
        {$...}  {.cmd}  {+...}  {!-- ... --}  {#...}[define template here]{#}
        tag     command include comment       snippet

[edit] Whitespace in braces: {% expr %}

Until 2.5, chunk tag syntax was very compact, and whitespace inside braces was either not allowed or highly restricted. This is fine for a simple {$tag} but can be hard on the eyes for flow control, commands, or tags with complex filter chains.

So, Chunk 2.5 introduced a new whitespace-friendly {% tag_expr %} syntax, as popularized by django, jinja, twig, etc.

  • Expressions are assumed to be commands (eg, flow control directives) so it is not necessary to prefix if, loop, exec, etc. with a period, but $tag variables still need the leading dollar sign.
   {% if ($x) %}
    {% $tag|default(Hello) %}
   {% endif %}
  • The old {/if} style for ending blocks just looked wrong in the new style, so writing out {% endif %} is now preferred.
  • Tags now have a little breathing room when they need it, and template flow is way easier on the eyes.

[edit] Example

Let's take a quick look at what it's like to code with Chunk templates:


hello.chtml
{!-- The snippets defined in this template are used on the welcome page --}

{!-- template for main content on welcome page --}
{#welcome}

<div>
Hello {$name}!  Welcome to the site!
</div>

{% loop in $widgets as $x %}

Widget Name: {$x.name}<br/>
Widget SKU: {$x.sku}<br/>

In Stock: {% if ($x.in_stock) %}Yes{% else %}No{% endif %}<br/>

  {% if ($x.related_widgets) %}
    <br/>
    {% loop in $x.related_widgets as $related %}
  Related: <a href="{$related.detail_url}">{$related.name}</a><br/>
    {% endloop %}
  {% endif %}

{% divider %}
<hr/>

{% endloop %}

{#}


{!-- template for footer on welcome page --}
{#footer}
&copy; 2011 Dewey Cheatham and Howe.  All Rights Reserved.
{#}

Tags are denoted by {$tag_name} -- tags function as placeholders for dynamic values provided at runtime.

Multiple snippets of html (or xml, or whatever) may be defined in a single file.

For example, the hello.chtml file above defines three templates:

hello
hello#welcome
hello#footer

The root "hello" template (just whitespace in this example) consists of all text in the file, minus the {#subtemplate}...{#} snippet definitions.

{!-- comments --} are also stripped out and will never appear in any output.


java
// Example java code:
import com.x5.template.Theme;
import com.x5.template.Chunk;

...

Theme theme = new Theme();         // a standard theme with no layers.
Chunk c = theme.makeChunk("hello#welcome");  // from src/themes/hello.chtml

c.set("name", getName() );
c.set("site_name", "Widgets International");

buf.append( c.toString() );        // outputs "Hello Bob!  Welcome to the site!"
// Need to use a template over and over?
// Let's re-use the chunk that we already set up.
//
c.resetTags();
buf.append( c.toString() );        // outputs "Hello {$name}!  Welcome to the site!"
// To grab a raw snippet and skip doing any tag processing,
// use theme.fetch( ... )
//
buf.append( theme.fetch("hello#footer") );
// outputs "&copy; 2011 Dewey Cheatham and Howe.  All Rights Reserved."

...
// Want to render directly to a stream?  Easy peasy (new in 1.6).
Theme theme = new Theme();
Chunk c = theme.makeChunk("hello#welcome");

c.set("this", "that");
c.set("tag", "value");

Writer out = getStreamWriter();
c.render( out );

[edit] Where should I put my template files?

Your templates should be in a folder named "themes" that lives somewhere on the classpath for your app. Template files can be on the filesystem or even in a jar. Tip: If you put them in src/themes/*.chtml then Eclipse will automatically copy the templates into the build folder. You can include template files in your jar build just as you would with any image resources (or jar the templates separately) and at runtime Chunk can fish the templates out of any jar in the classpath. During development, you can maintain the themes folder somewhere else if you prefer, as long as (A) your build routine copies the "themes" folder into your build or (B) your runtime classpath includes the "themes" folder's direct parent folder.

Advanced: Servlets are often deployed to appservers where each application has a special classloader and its own classpath. This classpath can be difficult for Chunk (or any 3rd-party library) to see. To address this disconnect, your servlet can give an explicit hint to the Theme object to share some visibility into the app's resource context. Here's how:

public void doGet(HttpServletRequest req, HttpServletResponse resp)
{
    Theme theme = new Theme();
    theme.setJarContext(getServletContext());
    ...
}

What if I want to load templates from a different folder?

public void doGet(HttpServletRequest req, HttpServletResponse resp)
{
    String templatePath = "/path/to/my/templates";
    String themeLayers = "";
    
    Theme theme = new Theme(templatePath, themeLayers);
    ...
}

[edit] Chunk Style Guide

Template filenames should be named in lowercase with no periods before the extension and no spaces anywhere. Use underscores in place of spaces where needed. You can use any extension but .chtml and .cxml are preferred for templates that contain html and xml.

Example template filenames:

 welcome.chtml
 message.cxml
 account_settings.chtml

Tags should follow the same guidelines. Mixed case is allowed but strongly discouraged, and the only legal punctuation is dashes and underscores with underscores strongly preferred over dashes. Spaces are not legal inside a tag name. Numbers are ok.

Example tags:

 {$name}
 {$home_phone}
 {$menu_bar}
 {$left_nav}
 {$header_1}

 {$results:Nothing found!}
 {$address_line_2:}

 {$price|sprintf($%,.2f)}
 {% $price|sprintf($%,.2f) %}
 
 {$url|s/ /%20/g}
 {% $url|urlencode %}

 {+other_template}
 {+template_ref#fully_qualified}
 {+#subtemplate}

 {% include template_ref#snippet_xyz %}
 {% alt_repository.template_name %}
 {.if ($cond) } A {.else} B {/if}
 
 ...or...
 
 {% if ($cond) %}
   A
 {% else %}
   B
 {% endif %}

[edit] Leave out the extension!

Template references usually omit the filename extension. For example, a snippet reference like hello.chtml#welcome is usually redundant since .chtml is already the default extension for most themes. hello#welcome is the recommended form.

However, if your theme contains files with multiple different extensions, you can fully qualify the reference (when the extension does not match the theme's default extension), eg: hello.txt or hello.txt#welcome.

Missing Templates

When a template can't be found, the engine will insert some helpful error text into the output (or to stderr if so configured) with a complete report on where it looked for the template on the filesystem/classpath.

If you don't want these errors to pass silently, you can call theme.setAbortOnMissingTemplate(true) and the engine will instead throw a TemplateNotFoundException.

[edit] Chunk Tag :Defaults

A tag can provide its own default value. Here's the syntax:

 {$tag:DEFAULT VALUE}

This is equivalent to:

 {% if ($tag) %}{$tag}{% else %}DEFAULT VALUE{% endif %}

But be careful! In both of the examples above, the "else" clause only fires if the {$tag} really is undefined. The empty string "" is not interpreted as null/undefined and will not trigger the default output. If you do want to trigger a default on empty string also, try the "|onempty(...)" filter.

If you want a tag to disappear by default, you must supply a blank default value. The tag will look like this:

 {$error_msg:}

Or if you're working with HTML, you could use a non-rendering comment:

 {$error_msg:<!-- no errors -->}

Advanced Usage Note - Nested Braces

Defaults may contain nested tags. Nesting braces, however, is not allowed, and will result in a parsing error. In fact, unescaped braces are not allowed anywhere inside a tag, because the final inside brace will be mistaken for the final outer brace by the speedy parser.*

For example, you could opt to show the username when no data is available for the full name:

Example 1:

Correct: {$full_name:$username}   THIS IS OK
INCORRECT: {$full_name:{$username}} <-- nested braces, WILL NOT WORK

Example 2:

BAD {$name:{.include.#default_name}} THIS WILL NOT WORK
GOOD {$name:.include.#default_name} THIS IS OK

As you can see, you can even use the special .include.template_ref syntax (like in the intro_paragraph tag below) to designate a template snippet to appear in the tag space by default. Read the INCLUDES section to learn more.

  • There is one exception: curly braces are legal inside a regular expression filter. So, this regex filter that truncates the tag value after the first 10 characters is OK:
{$long_value|s/^(.{10}).*$/$1/}

[edit] Tag Preservation (no value? leave tag alone)

Standard behavior when a tag value is null or never defined is for the tag to pass through to the final output unchanged. Tag preservation is by design, since the output can then be used as a template or tag value for re-processing in a "parent" context, ie later on where the presentation code finally has access to an appropriate value for the tag.

During development, tag preservation makes it easy to examine the final output of the template engine and see exactly which tags you still need to provide values for in the code.

[edit] A recursive template engine

Nest your chunks, use tags in your tag values. Chunk will unravel it all. When you call Chunk's .toString() or .render(out) methods, a tag resolution algorithm recursively scans all expanded tag values for additional tags and attempts to resolve any that it finds. A recursion depth limit prevents the template engine from getting stuck in infinite recursion loops.

[edit] Undefined tag values, Null-trapping

Notice how the {$friends:} tag below has an empty default value and disappears in the final output, but the next tag {$goobers} provides no default. Because of Chunk's tag-preservation feature, the {$goobers} tag simply passes through into the final output. This strategy could be leveraged to send the output through the processor again later on, when a good value for goobers might become available. If that's not your plan, it's generally better to supply a default for all your tags (even if it's something like "ERROR! Name Undefined!").


hello.chtml
Hello {$name:there}!  Welcome to {$site_name:the site}! {$friends:} {$goobers}

<p>{$intro_paragraph:.include.#default_intro}</p>

{#default_intro}
Here at Widgets of America, we specialize in widgets.  Nobody knows
widgets better than we do, and you can bet the widget farm that you
can't stump our <a href="{$webroot}/experts">Widget Experts</a>.
{#}



java
// ...
Chunk c = theme.makeChunk("hello");
// Normally there would be a bunch of c.set("tag_name", tagValue) calls here,
// but for this example we want to illustrate null tag behavior.
//
// A chunk with no rules will make all tags pass through or render to their default values
return c.toString();

output
"Hello there!  Welcome to the site!  {$goobers} ..."

Important usage note: Null-trapping and set() v. setOrDelete()

Chunk .set(tag, value) calls turn null values into the empty string "" by default. This null-trapping is usually helpful, since you avoid seeing "null" spelled out in your template output, or worse, risking that a template won't execute due to a NullPointerException.

Here's how it works: The standard method for creating tag replacement rules -

Chunk.set("tag", value);

interprets null values as the empty string!

If you really want null values to result in the tag being undefined, you must use

Chunk.setOrDelete("tag", value);

If you don't want to have to care about this distinction, just use the onempty() filter in your templates instead of the colon syntax.

Hello {$name|onempty(there)}! ...


Careful, because of null-trapping (see usage note above) this variation is not the same:


java
// ...
Chunk c = theme.makeChunk("hello");

String willBecomeEmptyString = null;

c.set("site_name", willBecomeEmptyString);
c.set("name",      willBecomeEmptyString);

return c.toString();

output
"Hello !  Welcome to !  {$goobers} ..."

To make the template defaults fire (i.e. to make the tag truly undefined), you have to use setOrDelete(x,y) instead of set(x,y). Or, call unset("tag") to delete a tag directly.


java
// ...
Chunk c = theme.makeChunk("hello");

String willBeUndefined = null;

c.setOrDelete("site_name", willBeUndefined);
c.setOrDelete("name",      willBeUndefined);

return c.toString();

output
"Hello there!  Welcome to the site!  {$goobers} ..."

[edit] Chunk Tag Filters: Powerful {$tag|transformations}

A helpful library of built-in text filters are available via the pipe (|) character.

Filters provide a way to alter/transform the tag value presentation on-the-fly, directly in the template.

You may contribute your own filters. See below for how.

The following built-in filters (follow the link for complete docs) are currently provided with the Chunk library:

{$any_tag|qs} escapes double and single quotes with a backslash (don't "flub" -> don\'t \"flub\")
{$any_tag|uc} will transform the text to all uppercase
{$any_tag|lc} all lowercase
{$any_tag|md5} md5 hash (hex, or try md5base64)
{$any_tag|sha1} sha-1 hash (hex, or try shabase64)
{$any_tag|base64} base64-encode
{$any_tag|base64decode} decode base64-encoded string
{$any_tag|url} url-encode (make+safe+for+query+strings).  Alias: |urlencode
{$any_tag|urldecode} url decode (eg: my%20string -> my string)
{$any_tag|html} escapes html (uses &amp; &lt; &gt; &quot; and &apos;)
{$any_tag|xml} same as |html
{$any_tag|unescape} (1.8) unescape xml-escaped entities, including eg &#123; and &#x03BB;
{$any_tag|trim} removes leading and trailing whitespace
{$any_tag|defang} removes HTML tag markers etc. to foil xss script-injection attacks
{$any_tag|translate} translate via the localization engine.  Aliases: |xlate,|_
{$any_tag|type} outputs STRING LIST OBJECT NULL CHUNK depending on the tag value type
{!-- dynamic/parameterized filters: ie these can take function-style arguments --}
{$any_tag|sprintf(%05.3f)} applies sprintf formatting
{$any_tag|filter(#x_template)} (2.0.1) executes #x_template, binding {$x} to the value of {$any_tag}
{$any_tag|selected(abc)} outputs selected="selected" if $any_tag == abc
{$any_tag|checked(abc)} outputs checked="checked" if $any_tag == abc
{$any_tag|indent(3)} indents multi-line values by x spaces
{$any_tag|alternate(even_output,odd_output)} new in 1.7 - if tag value is numeric and even...
{$any_tag|qcalc(+20)} perform a single math operation on a numeric tag value
{$any_tag|calc("*(y+z)",$y,$z)} perform a complex math operation (requires jeplite)
{$any_tag|default(oops)} (2.5) when tag is null/undefined, output "oops" (same as {$any_tag:oops})
{$any_tag|onempty(oops)} (1.8) normal tag-resolution - but when tag is empty or undefined, output "oops"
{$any_tag|ondefined(output)} see below
{$any_tag|onmatch(/RE1/,output1,/RE2/,output2)} see below
{!-- special filters for handling string arrays/lists --}
{$any_tag|join(, )} output a string array with a delimiter between each value
{$any_tag|get(0)} (1.8) access the 0th (ie first) element in the list/array
{!-- for regex fans, this one is especially cool: arbitrary regex-style search and replace! --}
{$any_tag|s/[0-9]/#/g} perl-style search+replace with regular expressions

These pipe modifiers may be chained. Multiple filters will be applied in the order specified:

{$any_tag|trim|lc|md5|uc}

In the example above, the tag value will have leading and trailing whitespace removed first, then the string will be converted to lowercase, the lowercase string will get MD5-hashed, and finally, the hex hash output (lowercase by default) will be converted to uppercase.


[edit] Mixing Tag |Filters and Tag :Defaults

The pipe modifier may be mixed with the colon modifier. The colon value may be placed before or after the filters (but not inside a filter chain). Use the colon first to have the filter transform applied to it, and last to skip the filter transform.

ie:

{$any_tag:don't|qs} and {$any_tag|qs:don't} are not equivalent.

When $any_tag is null:

   {$any_tag:don't|qs} => don\'t
   {$any_tag|qs:don't} => don't

The filter is applied in both cases if any_tag is non-null, but the default value is not filtered in the second case.

[edit] Use sparingly: |ondefined(...) and |onmatch(...)

Filters |onmatch(...) and |ondefined(...) open the door for ultra-compact (but hard to read) xslt-style transform logic. These can be very powerful but remember, don't code your entire app's logic in tag filter transforms! Use of these filters should be reserved for presentation/layout logic only. Extensive use of these filters will result in contorted, hard-to-follow templates.

If, after careful consideration, you decide that you really need branching logic to sit in your presentation layer templates, you probably want to use the more-readable {.if} construct instead (documented under "advanced" section below).

A. ondefined usage

ondefined(output)
  • if tag value is null or zero-length: display tag :default if provided, otherwise display nothing.
  • if tag value is non-null and non-empty, display the specified output.
{$any_tag|ondefined(some text)}
{$any_tag|ondefined($some_tag)}
{$any_tag|ondefined(+some_template)}
{$any_tag|ondefined(.some_external.content)}


B. onmatch usage

onmatch(/RE/,output[,/RE2/,output2[,...]])
onmatch(/RE/,output[,/RE2/,output2[,...]])nomatch(def_output)
  • if tag value is null or does not match any RE: display nothing (or def_output)
  • otherwise, display the output that is paired with the first matching regexp.

Example 1
{$any_tag|onmatch(/a/,ABC)}
 null -> ""
 fox  -> ""
 a    -> "ABC"
 cat  -> "ABC"  (for exact matches, use /^a$/ syntax)

Example 2: switch/case style
{$any_tag|onmatch(/ca/,Cat,/a/,ABC)nomatch(DEF)}
 null -> "DEF"
 fox  -> "DEF"
 a    -> "ABC"
 car  -> "Cat"
 bar  -> "ABC"

Note the optional nomatch clause. For example:


Example 3: not just static strings

The onmatch and ondefined output arguments allow tag references and template snippet "includes" to be used anywhere that you would place static output text.

{$any_tag|onmatch(/1/,+template_one,/2/,+template_two)nomatch($errmsg)}
 1 -> include template "template_one"
 2 -> include template "template_two"
 3/null/etc. -> display the value of the {$errmsg} tag

Future ideas:

{$any_tag|usd} formats numbers as U.S. currency eg 30000 -> $30,000.00
for now, just use {$amount|sprintf($%,.2f)}
{$any_tag|num(00.00)} applies DecimalFormat formatting
{$any_tag|date(yyyy-MM-dd)} applies SimpleDateFormat formatting

[edit] Chunk _[Localization]

Any string in your template may be marked for translation by wrapping it in the _[Localization] marker syntax.

Create a file in the classpath named locale/xx_XX/translate.csv and Chunk will find it and use it for translation when appropriate.

You can set the locale of a single Chunk or set a default locale for your whole Theme object.


java
Theme theme = new Theme();
theme.setLocale("fr_FR");
Chunk c = theme.makeChunk();
c.append("_[That's it!]  _[Localization].");
out.println( c.toString() );

locale/fr_FR/translate.csv
# Here are my welcome page strings
"That's it!","Et voila!"
"Localization","Localisation"

Et voila! Localisation.

Make sure your locale folder contains one folder for each locale code, with a csv file named translate.csv in each one.

locale/
 |-- de_DE/
 |    +-- translate.csv
 |-- es_ES/
 |    +-- translate.csv
 +-- fr_FR/
      +-- translate.csv

Expected encoding for csv files is UTF-8. Set the system property chunk.localedb.charset to override.

[edit] Advanced usage, filter tag

You can add curly braces and use %s format string markers for more advanced examples:

{_[Account #%s balance is %s],$account,$balance}

locale/fr_FR/translate.csv
# Bank balance page strings
"Account #%s balance is %s","Compte NÂș %s solde est %s"

And you can send the output of any tag through the translator with the "translate" filter, like so:

{$tag|translate}

or use these shortcut aliases "xlate" or "_" like so:

{$tag|xlate} {$tag|_}

[edit] Custom Locale files path

If you want to place translation csv files on the filesystem and not as resources in the classpath, set the System property chunk.localedb.path to the location of the "locale" folder.

[edit] Custom TranslationsProvider

Some deployment environments (for example, Android) have neither access to the filesystem nor access to resources from the classpath.

You can write your own class which implements com.x5.template.providers.TranslationsProvider and pass it to the Theme object:

class MyTranslationsProvider implements TranslationsProvider {
  public Map<String, String> getTranslations(String localeCode) {
    // ... load translations from app context assets? ...
  }
}
// ...
Theme theme = new Theme();
theme.setTranslationsProvider(new MyTranslationsProvider());
theme.setLocale("fr_FR");
Chunk chunk = theme.makeChunk()
// ...

[edit] Chunk MACROS with {% exec %}

Macros calls are a special way of including another template snippet. With an {.exec} block, template A can include template B and assign values to template B's tags all from within template A, relieving java of some of the busy work.

Let's say you have some presentation element that is repeated over and over in your project. Maybe it's a button or a box with a fancy border, or a three-column page layout that's used on a lot of pages in your project.

For example, I have a CSS sliding-doors button template with some fairly ugly HTML that I use over and over, just changing the label or adding a css class when I need to alter the default button. With macros, I don't have to copy and paste that button HTML over and over (and I don't have to look at it and remember what an awful hack it is every time I need a pretty button).

{% exec BUTTON %}
 {$btn_label = Add to Cart}
{% endexec %}

Normally the java-template contract leaves it to your java code to assign values/rules for tag replacement. With a macro, your template can take on a contract with another template and assume some of the responsibility for setting those tag values.

Appropriate macro use eliminates a lot of java code. I don't have to set up a button chunk in my java code and prep the label value there, and in fact this obscure presentation detail vanishes from the app logic, as it should.

Macros can make your templates easier to read too - instead of squinting at a dense block of HTML, you're looking at a clean macro call.

Macro syntax:

{% exec TEMPLATE_SNIPPET_NAME %}
 {$param1=}value1{=}
 {$param2=}value2{=}
 {$param3 = Simple Value}
 {$param4 = 400}
{% endexec %}

The end parameter markers "{=}" are optional.

Simple tag values may be provided using the syntax for $param3 and $param4 above, but the value must not contain special characters (including braces and the = and # signs) and must not include any template directives or tags.

In the example above, the assignment syntax used for $param1 and $param2 is much more flexible. Whatever you sandwich between the start-def {$...=} and end-def {=} tags (or everything up to the next start-def tag) is the value of that tag in the macro call.

Multiline values are okay, and chunk tags may be used or even a nested macro exec call.

However, longer values make the macro call harder to read. If your values are especially long, move the value out into a snippet and pull it in with {% include #snippet %}.

Style note: I strongly recommend naming template files and #snippets in ALL_UPPERCASE if they are to be used as macros.

[edit] Other macro exec parameter formats: JSON, XML

Starting in Chunk 1.7, macro invocation parameters can now be specified as a JSON object or even XML, ie in addition to the default/original parameter format. JSON is especially handy for providing data to your macro template since it has rich support for encoding simple strings, lists, and objects in infinite nesting combinations.

Notes: Chunk does not include a json parser. If you do want to specify args in json or json-strict format, you will need to add the json-smart lib to the classpath (a free download). Chunk does include a rudimentary, non-validating XML parser. XML element "attributes" (like <child gender="boy">) are available to the template by appending an "@" accessor to the tag (eg, {$child@gender} ).

{!-- standard param format example. --}
{% exec TEMPLATE#SNIPPET_NAME %}
 {$param1=}value1{=}
 {$param2=}value2{=}
 {$param3 = Simple Value}
 {$param4 = 400}
{% endexec %}

{!-- new json param format.
  -- Note 1: Parameters can have more exciting/useful names than just param1,param2.
  -- Note 2: This is technically not valid JSON. Use "@json-strict" if you care about such things.
  --}
{% exec TEMPLATE#SNIPPET_NAME @json %}
{ param1 : 'value1',
  param2 : '{$some_tag}',
  param3 : 'Simple Value',
  param4 : 400,
  param5 : ['an','array','of','strings'],
  param6 : {some:'object',with:'its own params'},
  param7 : [{name:'array of objects'},{name:'etc'}],
}
{% endexec %}

{!-- new xml param format example. --}
{% exec TEMPLATE#SNIPPET_NAME @xml %}
<values>
 <param1>value1</param1>
 <param2>{$some_tag}</param2>
 <param3><![CDATA[some <html>]]></param3>
 <param4>400</param4>
</values>
{% endexec %}

Since 1.7 exec supports an inline template, either by using the special {% body %}...{% endbody %} markers or (since 2.6.3) by encasing the data in {% data %}...{% enddata %} tags (or both).

{% exec %}
 
  {% data %}
    {$x = 2}
    {$y = Ringo}
  {% enddata %}
 
  {% body %}
The number {$x} Beatle was {$y}.
  {% endbody %}
 
{% endexec %}

Implicit inline body (no template ref in the exec call, data encased):

{% exec %}
  {% data @json %}
    { x: 2, y: 'Ringo' }
  {% enddata %}
The number {$x} Beatle was {$y}.
{% endexec %}

[edit] Examples

Example macro invocation:


hello.chtml
{#welcome}

{% exec BOX %} {!-- beginning of macro call --}

 {$box_title=}Welcome to {$site_name:the site}!{=}
 {$box_content=}
Help!<br/>
<br/>

I'm trapped in a box!
 {=}

{% endexec %} {!-- end of macro call --}

This idea will appear to be outside the box.

{#}



BOX.chtml
{!------------------------------------------------
  -- I hate writing HTML table code, so I just
  -- use this template over and over as a macro.
  -- I could even reimplement this layout with divs and css
  -- and not have to touch the 27 places like above in hello.chtml
  -- where it gets called.
  ------------------------------------------------}

<table border="1" width="{$box_width:300}">
 <tr><td>{$box_title}</td></tr>
 <tr><td>{$box_content}</td></tr>
</table>

java
Chunk c = theme.makeChunk("hello#welcome");
return c.toString();

output
<table border="1" width="300">
 <tr><td>Welcome to the site!</td></tr>
 <tr><td>
Help!<br/>
<br/>

I'm trapped in a box!
</td></tr>
</table>

This idea will appear to be outside the box.

[edit] Chunk Includes with {% include [...] %}

You don't have to use macros to provide simpler directives from the template. Want to just "include" another template? It's easy!

The recommended syntax for includes is:

{% include template_name %}

To include a subtemplate snippet:

{% include template_name#snippet_name %}

When the snippet is defined in the same file, you may omit the template filename:

{% include #snippet_name %}

Include expects a static reference. If the template reference is in the value of a tag, use backticks:

{% include `$template_name` %}


[edit] Conditional Includes with {% includeIf(...).[...] %}

A conditional include a la {% includeIf([cond]).[template] %} was added in 1.1. The syntax is compact but it's a bit hard to read for the uninitiated, so I recommend using the {% if %} construct with a distinct {% include %} tag instead. As a bonus for going that route, you get an "else" clause which {% includeIf %} does not support.

For example:

{% if ($potatoes == "yes") %}
 {% include #potatoes %}
{% else %}
 {% include #spam %}
{% endif %}

But if you like cryptic/compact, go for it. On template expansion, if the [cond] expression is true, the [template] is included.

Unlike standard tags, when the condition is not met, the entire tag disappears from the output.

Style note: Try to avoid coding your entire app with chains of {% includeIf(...)... %} tags.


Example includeIf(...) uses:

{!-- Only these three simple tests are supported --}

{!-- 1. if $tag exists, if $tag doesn't exist; ie, null test --}
{% includeIf($username).hello#hello_username %}
{% includeIf(!$username).hello#hello_anon %}

{!-- 2. if equals, if doesn't equal string/tag-var --}
{% includeIf($username == Bob).hello#hello_bob %}
{% includeIf($username != Bob).hello#hello_anon %}
{% includeIf($username == $def_username).hello#login %}

{!-- 3. if matches regex, if doesn't match regex --}
{% includeIf($username =~ /(jane|john)/i).hello#hello_janejohn %}
{% includeIf($username !~ /(jane|john)/i).hello#hello_anon %}

Note that these examples are roughly equivalent to the more efficient and more powerful (but even less readable) "ondefined" and "onmatch" filters:

{$username|ondefined(+tpl_a):+tpl_b}
{$username|onmatch(/regex/,+tpl_a)nomatch(+tpl_b)}


The "+" is shorthand for .include. More about filters.

Shorthand include/includeIf syntax:

{+my_content}        is the same as  {% include my_content %}
{+(cond)my_content}  is the same as  {% includeIf(cond).my_content %}

...but this shorthand should be used sparingly. I prefer to spell out the word "include" to promote readability.


hello.chtml
{!-----------------------------------------------------}
{#welcome}

{% include top_nav %}

Hello {$name}!  Welcome to {$site_name:the site}!

{#}



top_nav.chtml
{!-- put the sitewide navigation bar here --}
...

[edit] Combining Chunk Macros with Includes

You can combine macro and include syntax. Multi-line macro parameters never look great, so this technique can greatly increase the readability of your layout templates by tucking away that messy parameter into its own snippet:


hello.chtml
{!--------------------------------------------------------}
{#welcome}

{% exec BOX %}

 {$box_title=}Welcome to {$site_name:the site}!{=}
 {$box_content=} {% include hello#box_text %} {=}

{% endexec %}

This idea will appear to be outside the box.

{#}


{!--------------------------------------------------------}
{!--  This marks a new, separate snippet definition.    --}
{!--  This snippet is included from the snippet above.  --}
{!--------------------------------------------------------}
{#box_text}
Help!<br/>
<br/>

I'm trapped in a box!
{#}

Note: in the template above, {% include hello#box_text %} could be written as just {% include #box_text %} and the result would be the same. You can leave out the filename part of the snippet reference if the target snippet is defined in the same file.

[edit] Nesting Snippet Definitions (You can, but don't)

Snippet definitions may be nested, but this practice is strongly discouraged since it can lead to confusion.

For the headstrong, here's an example reference to a nested snippet definition:

{% include filename#snippet#nested_snippet %}

Which was defined like so:

...
{#snippet}
bla bla bla
 {#nested_snippet}foo foo {% include #important_stuff %} foo{#}
bla bla bla
{#}

{!-- put the important stuff down here --}
{#important_stuff}
...
{#}

In particular, this can be misleading because shorthand snippet references are always expanded with just the filename -- so the above include is a reference to filename#important_stuff and not to either of the following snippets, like you might expect:

filename#snippet#important_stuff
filename#snippet#nested_snippet#important_stuff

[edit] Advanced Usage

[edit] Special Directives: {% tagStack %} {% if %} {% loop %}

The core library provides a few useful internal special tags.

[edit] tagStack

The .tagStack tag shows all available tagnames with data in the current chunk template expansion context. Super-useful for debugging.

{% tagStack %} or try {% tagStack(html) %}

[edit] if

Chunk 1.3 introduces a full-featured nestable {% if %}...{% endif %} construct.

{% if ($weather) %}
 {% if ($weather == sunny) %}
  {% include #picture_of_sun %}
 {% elseIf ($weather == rainy)  %}
  {% include #picture_of_rain %}
 {% elseIf ($weather =~ /snowy|flurries/) %}
  {% include #picture_of_snow %}
 {% elseIf ($weather == $weather_in_philly) %}
  {% include #always_sunny %}
 {% else %}
  {% include #default_picture %}
 {% endif %}
{% endif %}

Starting in 3.3.0, Chunk's if-expressions now permit compound logic with grouping and the && and || operators.

Note: Chunk's conditional expressions are quite limited. The lightweight parser can only handle the simple string value tests listed below. Sorry, conditions may not include greater-than or less-than tests (eg > < <= gt lt etc) but the comp filter might fit your needs (handles numeric comparison with a constant).

Tag values are coerced to strings for template output in an earlier step. This means that when if-expressions are evaluated for true/false, all value comparisons are string comparisons only, never numeric equivalence nor any notion of java-style equality.

Booleans map to the string "TRUE" or null/undefined, so they will be interpreted as expected in a simple if-test for existence.

The following tests will function as expected:

{!-- existence --}
{% if ($tag) %}...{% endif %}
{% if (!tag) %}...{% endif %} or {% if (!$tag) %}...{% endif %}

{!-- equality / inequality --}
{% if ($tag == apples) %}...{% endif %}
{% if ($tag != apples) %}...{% endif %}
{% if ($tag == "apples") %}...{% endif %}
{% if ($tag != "apples") %}...{% endif %}
{% if ($tag_a == $tag_b) %}...{% endif %}
{% if ($tag_a != $tag_b) %}...{% endif %}
{!-- pattern matching --}
{% if ($tag =~ /apples|oranges/) %}...{% endif %}
{% if ($tag !~ /apples|oranges/) %}...{% endif %}

Filter modifiers are legal and may be applied to any if-condition tags.

{!-- this will execute even if tag_a is PICKLES and tag_b is pIcKLeS --}
{% if ($tag_a|lc == $tag_b|lc) %}...{% endif %}

{!-- this will execute when the tag is (a) undefined, (b) empty or (c) actually set to the string "EMPTY"
  -- the onempty(output) filter is new in 1.8 --}
{% if ($tag|onempty(EMPTY) == EMPTY) %}...{% endif %}

Philosophical note

Purists and dogmatic templaters may insist that {% loop %} and {% if %} tags have no place in a proper template engine. For a long time Chunk resisted anything resembling flow control within the templates themselves, but related snippets often had to be located very far from one another and readability suffered. So, rejoice! There is a happy medium.

Chunk still strives to leave business logic over on the java side of the fence (and therefore keeps visibility into java objects at a bare minimum). But dagblastit, sometimes you're presenting some content and you need to branch and loop. Have at it. But, you've been warned: don't code your whole app in your templates!

Compact syntax

For one-liners, sometimes the compactness of the pre-2.5 syntax can come in handy:

{.if ($tag) }...{.else}...{/if}

This compact syntax is used internally and will be supported indefinitely.

However, it can be confusing to mix compact style tags with the new easier-to-read (but less compact) expression tag syntax.

For this reason, the longer {% expr %} syntax is now the official preferred style in all our documentation and examples.



[edit] loop

The {% loop %} tag is also very versatile.

{!-- templates defined in-place between loop tag and matching endloop tag. --}
{% loop in $data_var as $x  array_index_tags="true" %}
...{$x[0]} {$x[1]}...
...{$x.fruit} {$x.quantity}...
{% onEmpty %}
no data!
{% endloop %}

Note: The two lines in the loop block above demonstrate array-index access vs column-name access. The outputs are equivalent (see table definition below).

[[fruit,quantity],
 [apples,4],
 [bananas,3],
 [canteloupe,7]]

loop tag options:

  • divider="<dividertext>"
  • counter="<$tagname>[,offset[,step]]"
  • counter_tags="true|false" - provide counter tags {$0} (zero-indexed loop counter) and {$1} (loop counter starting at 1) automatically.
  • array_index_tags="true|false" - access table row data by column index, eg {$x[2]}
  • first_last="true|false|<$firsttag,$lasttag,$placetag>" - provide {$place} tag (evaluates to "first" and/or "last") and {$first} and {$last} tags on matching iterations.

These options are specified after the "in $list as $x" clause like so (quoting the parameter values is optional):

{% loop in $list as $x  counter=$i,1 array_index_tags=true first_last=$primero,$ultimo %}

Looping Tips

  • A divider, if desired, is usually easier to specify after the loop body using a {% divider %}...{% enddivider %} section. The closing tag is optional.
  • An {% onEmpty %}...{% endonEmpty %} section should almost always be included, to output in case there is nothing to iterate over. The closing tag is optional.
  • Loop over a subset by adding a |slice(from:to) filter to the $list var.
  • Loop in reverse order by adding a |reverse filter to the $list var.

See [looping examples] in the recipes area.

The $data_var can be a tag of type LIST or whose value is an inline table (uses a combination of JSON and CSV styles; make sure to escape in-string commas and square brackets with backslash) defined in a string like so:

Other types that can be looped over with {% loop %}:

  • String array (treated as a one-column headerless table).
  • an array of objects that implement Map.
  • an array of POJOs/beans.
  • anything that implements the com.x5.util.TableData interface.
  • a single Map/POJO/Bean (use " as $key:value" to iterate over the object's properties).

For example, I use a QueryResultTable class that runs an arbitrary SELECT query. QueryResultTable groks the column labels and gathers up all the results in an array of String arrays (so, I only use it for relatively small result sets). It implements TableData so I can stash the results directly into the Chunk and let the template decide how to present the data.

String SQL = "SELECT ...";
QueryResultTable table = QueryResultTable.doSelect(SQL);

Chunk c = theme.makeChunk("pretty_table");
c.set("results", table);

buf.append( c.toString() );

[edit] Chunk Theme Layers: Inheritance

The engine also supports overriding a base theme with multiple "theme" layers. Let's say you are making a new "skin" for your app but you want to retain most of the existing template work.

Put your base theme templates in themes/base and any new "override" templates in themes/fancy.

themes/
   \-- base/
        \-- main_shell.chtml
        \-- hello.chtml
        \-- BUTTON.chtml
   \-- fancy/
        \-- hello.chtml
        \-- BUTTON.chtml

Then, in the "fancy" files, just define the snippets that you actually want to revamp. When initializing your theme, list the layers the way you plan to stack them up (in order from low precedence to high precedence).

Theme fancy = new Theme("base,fancy");

[edit] Inherit root template with {% super %}

Be careful! If you override a template and just include a few override snippets, you will wipe out the parent root template (ie, with the empty version in your override layer). All {% include file#snippet %} references will function as intended, but a theme reference to the bare {% include file %} will return your override layer's blank version of that file's root template.

There are a couple ways around this problem. (A) You could copy your root template content from the other layer -- but this is a bad idea since the two copies will get out of sync. Instead, try (B) place the special {% super %} tag at the top of your document like so:

fancy/hello.chtml

{!-- prettier hello --}
{% super %}

{#some_snippet}
<div class="purty">I'm so pretty now!</div>
{#}

This will signal to the template engine to defer to the layer underneath for the root template. You do not need to do this for named snippets, only for the root content! If you're happy with the snippet as defined in the layer below, just leave the snippet out of the higher layer completely.

[edit] Rapid MVC

[edit] Simple Binding with POJO/Bean

Since 2.1, Chunk no longer requires boxing your objects in a DataCapsule. Just bind the object to a tag prefix with a regular "set" call. At render time, Chunk will take a snapshot of the POJO's public attributes (or the Bean's public getters/accessors) and treat them like any other Map object. Even nested references will work!

Item item = new Item("Best Cat Videos", 9.99, "DVD");
item.recommend = new Item("Best Cat Videos II", 8.99, "DVD");
c.set("item", item);
<div class="item">
  ${$item.price|sprintf(%0.2f)}<br/>
  {$item.title}<br/>
  {$item.type}<br/>

  {% if ($item.recommend) %}
  <div class="recommendation">
    You may also like: {$item.recommend.title}
  </div>
  {% endif %}

</div>

The {% loop in $list as $x %} tag can loop over arrays/lists of "plain old" objects as well.

  • CamelCase fieldnames are converted to lowercase_with_underscores.
  • Primitives (int, long, double, float, etc. including boxed primitives Integer Long etc.) are converted to Strings.
  • Boolean values are the string "TRUE" if true, or null/undefined when false, to facilitate {% if ($x.is_something) %} branching.
  • Arrays and Lists can be looped over with {% loop in $obj.some_list as $x %}

Other types of fields (besides String) are treated as nested POJOs, so you can reference a child object's properties from the template as you would expect: {$obj.child.child_property}

An object's methods (aside from "getter" accessor methods) are *not* callable from the template, in keeping with the read-only stateless nature of Chunk templates.

[edit] POJO or Bean

Chunk uses reflection to check for non-private/protected fields. If it finds any, it will treat the class as a POJO and expose fields to the template, otherwise it assumes that it is dealing with a bean and stores the return values of the "getter" functions at render time in a map, which is passed to the template.

Bean methods like getFullName() become object properties in the template, with the "get" prefix stripped. Additionally, all properties undergo a CamelCase to lower_with_underscores conversion. For example:

  • getFullName() => {$x.full_name}
  • getPriceUSD() => {$x.price_usd}
  • getTotalJellyBeans() => {$x.total_jelly_beans}
  • isActive() => {$x.is_active}

If you wish to force a class to be treated as a POJO or Bean, annotate your class with @AccessAsPojo or @AccessAsBean annotations from the com.x5.util package (new in 3.4.0).

[edit] Encapsulation with DataCapsule

NB: Boxing objects with DataCapsule is no longer recommended. DataCapsule boxing is still supported, but since 2.1, binding objects to your templates is far easier using the POJO/Bean methods documented above.

Any object (a Model-style object) that implements com.x5.util.DataCapsule can be glued right into a template (your "view") with a single line of controller code. When you implement DataCapsule, use getExports() to provide a list of public methods that exist in your model class, with the following call signature: String getXXX() -- ie, simple accessor methods that take no arguments and return a String.

public class Widget implements DataCapsule
{
    ...

    public String[] getExports()
    {
        return new String[]{
            "getName",
            "getID",
            "getPrice price_usd" // change tag name to widget_price_usd
        };
    }

    public String getExportPrefix()
    {
        return "widget";
    }
}

In your controller...

...
Widget widget = getWidget();

Chunk c = theme.makeChunk("widget_detail");
c.addData(widget); // all exported Widget methods are now available tags in the template

// what if you need two widgets on the same page?
// No problem, provide the optional second arg and the tags will get an object-style prefix.
String objName = "x";
c.addData(getOtherWidget(), objName);
...

widget_detail.chtml:

Name: {$widget_name}<br/>
ID: {$widget_id}<br/>
Price: {$widget_price_usd}<br/>

Related widget:
Name: {$x.widget_name}<br/>
ID: {$x.widget_id}<br/>
Price: {$x.widget_price_usd}<br/>

You can also stash an array of these model objects and loop through the array from the template view:

...
Widget[] widgets = getWidgets();

Chunk c = theme.makeChunk("widget_list");
c.set("widgets", widgets);
...

widget_list.chtml:

 {% loop in $widgets as $w %}
{$w.widget_id} {$w.widget_name} {$w.widget_price_usd|sprintf($%,.02f)}<br/>
 {% onEmpty %}
No widgets found!
 {% endloop %}

[edit] Literals

Don't want tag interpolation? You can mark a whole section of any template to be skipped over by the parser and it will pass through untouched. Just use the following syntax:

{% literal %}...{% endliteral %}

Watch out, the surrounding {% literal %}{% endliteral %} marker tags will get passed into the final output. This is by design, so you don't risk interpreting the literal content on a second or third pass. If you don't want the markers to display in the browser, just place the literal start and end markers inside HTML comments like so:

<!-- {% literal %} -->
{#example}
 {!-- some example --}  {$with} {% some %} {.crazy} {+tags} ... {% exec %}{$x = 5}{$title=}Leave Me Intact{=}{% endexec %}
{#}
<!-- {% endliteral %} -->

Nesting literal blocks is not supported. The inner literal-end marker will be interpreted as the end of the outer literal block.

[edit] Backticks (dynamic tags)

Backticks let you use a local tag value to form part of the tag name, parameter or filter arguments, on-the-fly. For example, say you're in a loop, and you want to reference some tag data that's specific to one row in the loop, but it's in a tag that was set outside the loop context. Enter backticks. In this example, imagine looping through widgets with IDs ranging from 1-30. In the java code, only the tag {$chosen_widget_27} is set to something non-null.

{#widget_row}
{$widget_id} {$widget_name} {$chosen_widget_`$widget_id`|ondefined(SELECTED)}
{#}

Here is another common use case, working around the fact that include does not expect a dynamic argument:

{% include `$template_name` %}

[edit] Extending Chunk

[edit] External template repositories

The "include" protocol is available standard, but you can easily whip up your own class that provides template content. It just has to implement com.x5.template.ContentSource, which requires two methods, fetch(...) and getProtocol().

For instance, I have a class that fetches html from a wiki. The client was familiar with creating and editing wiki pages and so instead of building a content management system for his site content, I just installed a wiki. The wiki tags look like this: {% wiki.External_Page %}

Here's an example that fetches templates from a database:


DBTemplates.java
import com.x5.template.ContentSource;

public class DBTemplates implements ContentSource
{
   // ...

   public String fetch(String templateName)
   {
       return getFromDB(templateName);
   }

   public String getProtocol()
   {
       return "db";
   }

   // ...
}

MyApp.java
// ...
// To provide your chunk with this new source of templates:

TemplateSet theme = getTheme(); // defined by you
Chunk c = theme.makeChunk("hello#welcome");
c.set("name", getName() );

DBTemplates dbt = new DBTemplates();
c.addProtocol(dbt);

return c.toString();
// ...

hello.chtml
{!-- the .include tag will pull from the standard template set
  -- in the theme, eg from the filesystem or jar.
  -- The db.scores tag will look for a template named "scores" in the database. --}

{#welcome}
{% include top_nav %}

Hello {$name}!  Welcome to {$site_name:the site}!<br/>
<br/>

Here are today's sports scores:<br/>
{% db.scores %}

{#}

[edit] Contribute a filter

Another cool way to extend Chunk is to write your own filter to transform tag text within the template. For example, chunk provides no built-in ltrim filter but starting in 1.6, here's how you can supply your own:

import com.x5.template.Chunk;
import com.x5.template.Theme;
import com.x5.template.filters.BasicFilter;
import com.x5.template.filters.ChunkFilter;

...

Theme theme = new theme();
theme.registerFilter( new LeftTrimFilter() );
Chunk c = theme.makeChunk();
c.append("{$name|ltrim}");
c.render( out );

...

class LeftTrimFilter extends BasicFilter implements ChunkFilter
{
    public String transformText(Chunk chunk, String text, FilterArgs args)
    {
        if (text == null) return null;

        int i=0;
        while (i < text.length() && Character.isWhiteSpace(text.charAt(i))) i++;

        return (i == 0) ? text : text.substring(i);
    }

    public String getFilterName()
    {
        return "ltrim";
    }
}

You may use the Chunk object to reference tag values. Your filter may pass arguments in a function-call style. For example, the built-in filter "indent" can be called with one or two args:

{$multiline_tag|indent(3,*)}

In this case, args.getUnparsedArgs() will contain the entire argument string "3,*" but if your arguments are comma-delimited, you'd probably prefer not to parse them yourself.

args.getFilterArgs(chunk) (3.3.0+) will return the filter arguments as an array of strings (context-interpolated if the args start with $). In this example, the args to indent are returned as the array {"3","*"} -- the optional second arg to indent is the string to use for indenting - default is a single space " ".

Note: Exceptions (such as NullPointerException) thrown from user-contributed transformText methods will be caught and recorded to stderr, and the filter will be skipped.

[edit] User-Defined filters - exec an "x" template

Starting in 2.0.1, simple macros can be defined and executed via the |filter(#x_template) tag modifier. The value is bound to {$x} and the template gets executed in-place along the filter chain.

For example, let's say you don't want to go to the trouble of implementing rtrim in java, or you have a format string you use over and over:

{#my_template}
My dog's name is {% $dog_name|default(Ralph    )|filter(#rtrim) %}!
His new collar cost {% $price:4003.2|filter(#currency_us) %} dollars.
{#}

{!-- little macro-filter for eating trailing whitespace, using a regular expr --}
{#rtrim}{% $x:|s/\s*$// %}{#}

{!-- little macro-filter for formatting currency --}
{#currency_us}{% if ($x) %}$ {% $x|sprintf(%,.02f) %}{% endif %}{#}

Output:

My dog's name is Ralph!
His new collar cost $ 4,003.20 dollars.

Important: If the input is null/undefined, the template will still be executed, so be sure to account for that possibility in your #x_template definition.

Personal tools