Chunk Documentation

From Chunk Java Template Engine

(Difference between revisions)
Jump to: navigation, search
(Leave out the extension:)
(Chunk Tag Defaults)
Line 136: Line 136:
Tag preservation means less code.  Chunk does all the heavy lifting!
Tag preservation means less code.  Chunk does all the heavy lifting!
-
When you call Chunk's .toString() method, a tag resolution algorithm
+
When you call Chunk's <code>.toString()</code> method, a tag resolution algorithm
recursively scans tag values for additional tags and attempts to
recursively scans tag values for additional tags and attempts to
resolve any that it finds.
resolve any that it finds.
Line 203: Line 203:
  // but for this example we want to illustrate null tag behavior.
  // but for this example we want to illustrate null tag behavior.
  return c.toString(); // returns "Hello there!  Welcome to the site! {~goobers} ..."
  return c.toString(); // returns "Hello there!  Welcome to the site! {~goobers} ..."
-
 
== Chunk Macros ==
== Chunk Macros ==

Revision as of 19:21, 1 January 2011

Contents

Documentation Wiki

Welcome to the Chunk Wiki, a place to document all the cool things you can do with Chunk.


THE "CHUNK" TEMPLATING SYSTEM

Intro

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, ...

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


Examples

Tags are denoted by {~tag_name} and function as placeholders for dynamic values provided at runtime. Multiple snippets of html may be defined in a single file.

For example, the hello.html file below defines three templates:

hello
hello#welcome
hello#footer

The "hello" template is not used in this example but consists of all text in the file, minus the {#subtemplate}...{#} snippet definitions and {!-- comments --} which are stripped out.

Here's a quick-start guide:

        {#...}  {~...}  {^...}  {+...}  {*...}  {!-- ... --}
        snippet  tag    special include macro   comment

The engine also supports overriding a base theme with multiple "theme" layers.


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


hello.html
{!-- This template is used on the welcome page --}

{#welcome}
Hello {~name}!  Welcome to the site!
{#}

{#footer}
&copy; 2007 Tom McClure.  All Rights Reserved.
{#}



java
// Example template code:

// ...
Theme theme = getTheme(); // defined by you
Chunk c = theme.makeChunk("hello#welcome");
c.set("name", getName() );
c.set("site_name", "X5 Software");
buf.append( c.toString() ); // outputs "Hello Bob!  Welcome to X5 Software!"

c.resetTags();
buf.append( c.toString() ); // outputs "Hello there!  Welcome to the site!"
// ...


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.

Example template filenames:

 welcome.html
 message.xml
 account_settings.html

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)}
 {~url|s/ /%20/g}

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

 {^alt_repository.template_name}
 {^include.template_ref#snippet_xyz}
 {^includeIf(~secure==yes).#snippet_abc}


Leave out the extension:

Template references never contain the filename extension, so for example you'll never see a snippet reference like hello.html#welcome (BAD!!) -- this would make the template engine look for a file named hello.html.html -- not to worry, when a template can't be found the engine will insert some error text into the output (or to stderr if so configured) with a complete report on where it looked for the template.

Chunk Tag Defaults

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

 {~tag:DEFAULT VALUE}

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.

Tag preservation means less code. Chunk does all the heavy lifting! When you call Chunk's .toString() method, a tag resolution algorithm recursively scans tag values for additional tags and attempts to resolve any that it finds.

If you want a tag to disappear by default, 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 Tags

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 whole template or subtemplate to appear in the tag space by default. Read the INCLUDES section to learn more.


Null tag values

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. 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.html
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.
return c.toString(); // returns "Hello there!  Welcome to the site! {~goobers} ..."

Chunk Macros

Chunk supports template macros for achieving more complex behavior.

Chunk macros are great for some element that is repeated over and over in your project. The best thing about macros is that the values for the macro expansion are assigned directly in the template, not in the code.

Appropriate macro use can eliminate a lot of code, and can make your templates easier to read.

Macro syntax:

{* TEMPLATE_SNIPPET_NAME *}
 {~param1=}value1{=}
 {~param2=}value2{=}
 {~param3 = Simple Value}
 {~param4 = 400}
{*}

The end parameter markers "{=}" are optional but the end macro marker "{*}" is required.

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.

The syntax used for ~param1 and ~param2 is much more flexible. The value is whatever you sandwich between the start and end tags. It can be multi-line, can include tags, and could even contain another nested macro call.

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


Example macro invocation:


hello.html
{#welcome}

{* BOX *}

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

I'm trapped in a box!
 {=}

{*}

This idea will appear to be outside the box.

{#}



BOX.html
{!------------------------------------------------
  -- I hate writing HTML table code, so I just
  -- use this template over and over as a macro.
  ------------------------------------------------}

<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.

Chunk Includes

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}


includeIf(...)

A conditional include a la {^includeIf([cond]).[template]} is now available. 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.

In part to keep us from abusing this feature, there is no "else" clause (and no "for" and "while" constructs, and a host of other things that don't belong in a presentation templating framework). Remember, the goal of all this templating is to keep business logic and presentation layout separate, so try to avoid coding your entire app with chains of {^includeIf(...)...} tags.

Repeating the tag with the opposite condition can achieve the "else" effect if you really need it.


Example includeIf(...) uses:

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

{!-- 1. if exists, if 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 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 below.

Shorthand include/includeIf syntax:

{+my_content}       for {^include.my_content}
{+(cond)my_content} for {^includeIf(cond).my_content}

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

Keep in mind, the template engine only wakes up to do its "magic" stuff when it sees the following triggers:

        {#...}  {~...}  {^...}  {+...}  {*...}  {!-- ... --}
        snippet  tag    special include macro   comment

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

{^include.top_nav}

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

{#}



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


Combing Chunk Macros with Includes

You can combine macro and include syntax. This can greatly increase the readability of your layout code:


hello.html
{#welcome}

{* BOX *}

 {=box_title=}Welcome to {~site_name:the site}!{=}
 {=box_content=} {^include.hello#box_text} {=}

{*}

This idea will appear to be outside the box.

{#}

{!-- this snippet is included from the macro call above --}

{#box_text}
Help!<br/>
<br/>

I'm trapped in a box!
{#}

Nesting Snippets (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


Other Specials: ^tagStack and ^loop

The core library provides a few useful internal special tags.

1. The ^tagStack tag shows all available tagnames with data in the current chunk template expansion context.

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

2. The ^loop and ^grid tags can be quite handy.

{^loop template="#repeater" data="~data_var" no_data="#empty_list"}

The ~data_var can be 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:

[[header_a,header_b],
 [apples,4],
 [bananas,3],
 [canteloupe,7]]

Or a string array (a one-column table), or anything that implements the com.x5.util.TableData interface.

Chunk Tag Filters: Tag Transforms

A limited number of 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.

{~any_tag|qs} escapes a quoted string (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|sha} sha-1 hash (hex, or try shabase64)
{~any_tag|base64} base64-encode
{~any_tag|base64decode} decode base64-encoded string
{~any_tag|url} url-encode (safe+for+query+strings)
{~any_tag|urldecode} url decode (eg: my%20string -> my string)
{~any_tag|html} escapes html (uses &amp; &lt; &gt; &quot; and &apos;)
{~any_tag|trim} removes leading and trailing whitespace
{~any_tag|s/[0-9]/#/g} perl-style search+replace with regular expressions
{~any_tag|sprintf(%05.3f)} applies sprintf formatting
{~any_tag|defang} removes HTML tag markers etc. to foil xss script-injection attacks

New filters onmatch and ondefined open the door for xslt-style transforms.

A. ondefined usage

ondefined(output)
  • if tag value is null or zero-length: display nothing
  • otherwise, 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 specified output after 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

Pipe modifiers may be chained. Multiple filters will be applied in the order specified: {~any_tag|trim|lc|md5|uc}

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.

I am considering supporting a more legible sgml-inspired syntax, eg:

 {~any_tag ifnull="don't" transform="qs" transform_when_null="yes"} => don\'t
 {~any_tag ifnull="don't" transform="qs" transform_when_null="no"} => don't

but don't hold your breath.

Extending Chunk: 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.html
{!-- the ^include.top_nav tag will pull from the standard template set --}
{!-- the ^db.scores tag will look 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}

{#}
Personal tools