Skip to content

Latest commit

 

History

History
888 lines (647 loc) · 32.1 KB

template_guide.md

File metadata and controls

888 lines (647 loc) · 32.1 KB

Template Author's Guide

This document details how to use Rythm to write template source.

[introduction]Introduction

Rythm Template Engine is a text generator that merges dynamic content into static template.

java-version

In order to use Rythm to generate a text file, the essential work is to create template file which write down the static part and use Rythm syntax to define the dynamic parts. It is also important to understand that a template file has one or more arguments which is the origin of the dynamic content. The Java program pass parameters to template file via arguments to generate different result.

So a template file is a text file, some parts of which have placeholders for dynamically generated content. The template’s dynamic elements are written using the Java language.

Dynamic elements are resolved during template execution. The rendered result is then sent as part of the HTTP response.

In summary, a rythm template is composed of the following types of components:

  • static content, which is literally output to the render result without any changes
  • expressions, which output the dynamic content to the render result. See more about expressions
  • directives, which control the behavior of the template in this and that way. Please refer to directive reference

[at]The magic @

The Rythm template syntax is designed to be easy to learn and what you need to understand is one fact:

The magic @ character is used to lead all rythm element.

If you want to output a literal @ character, you need to double it. E.g.
This is an email address in template: someone@@gmail.com

[static-content]Static content

All static content (those text not defined in Rythm elements including script blocks) are output literally including whitespace and new lines.

When a template part is executed in compact mode, the additional whitespace and new lines are removed. See Compact output

[comment]Comment

one line comment start with @//

@// this is one line comment

multiple lines comment are put inside @* and *@ block.

@*
    this is a multiple line comment.
    The text inside this block will be ignore by Rythm template processor
*@

[argument]Template arguments

When a template is rendered, it nearly always needs to pass some dynamic information to the template. This is done via template arguments. Unless rare cases you should declare arguments of a template using @args directive:

@args User user, Order order
...

The main reason that argument declaration is needed is that Rythm is a static typed template engine, it needs to parse your template source code and convert it into a valid java source code and then call a compiler to compile the template into byte code. The benefit of this static typed processing (vs. interpreted processing as velocity, freemarker etc) is:

  1. It's type safe and thus compiler help you capture some errors
  2. It's much faster than interpreting processing

[more_about_template_argument]More about template argument:

  • You are not required to declare template arguments at top of the template source code. It's more like a c++ style, you just need to declare arguments before they are used.
  • You can have multiple @args declaration in a single template source file

[when_no_args_decl]When you can waive template arguments declaration

In some cases you can waive for template argument declaration:

  1. The template does not have arguments. All dynamic information is retrieved out via method calls, tag calls etc. Obviously you don't need to declare something that doesn't exists
  2. Your template is simple enough that you don't need to call any field or method on your template arguments, the only thing you need is the toString() method, in that case you can waive argument declaration and Rythm will treat those arguments as an Object type. Note you can't have complex expression on those arguments. E.g Hi @username is okay, but Hi @user.name won't be compiled unless you declared @args User user
  3. You enabled the feature.type_inference configuration and you are sure that the first time running the template, all parameters are not null
Note: it is always recommended to declare your template arguments, not only because it is required, but also it improves your template readability

For more about template argument declaration, please refer to @args directive

[expression]Expression

Output expression is the core function of Rythm template, it is used to emit dynamic content passed into the template, including emission of object fields and methods, class fields and methods:

@args User user, Foo foo
  
// emit instance field
@foo.bar

// emit instance method
@user.getName()

// emit class field
@Long.MAX_VALUE
@Integer.MAX_VALUE

// emit class method
@Boolean.parseBoolean("true")

The bracket ( ) can be used to compose complicated expressions or to separate an expression from other part of the template:

@(1 + 5) @// print out the result of 1 + 5

@// use ( ) to separate expression from other part of the template
@(foo.bar)_and_some_other_string 

JavaBeans specification

Rythm does not directly support evaluate object property following JavaBean specification. For example if you need to get bar property of foo object, you need to refer to the getter method foo.getBar() unless your bar property is public. Hoever Rythm does support JavaBeans via dynamic expression evaluation by suffix the expression with another @:

@foo.getBar() @//the normal way to get bar proeprty
@foo.bar@ @// dynamic expression evaluation, notice the "@" at the end of the expression

Note that using dynamic expression evaluation might suffer the template performance a little bit.

Source of expression

As shown in the above example, the instances/classes/fields/methods inside an expression could come from anyone of the followings:

See also

[scripting]Java code scripting

Rythm allows you to put any Java code inside a template:

@{
    String fullName = client.name.toUpperCase() + " " + client.forname;
}
<h1>Client @fullName</h1>

You can use p() function to print content to the current output inside scripting block:

@{
    String fullName = client.name.toUpperCase() + " " + client.forname;
    p("<h1>").p(fullName).p("</h1>");
}

But obviously this is not a goo way to go for most times.

[template_function] Template built-in functions

Rythm defines a very handy function on each template, the s(). It returns a string utility instance with a set of static methods built in. Here are some examples using the s() function in a template source:

@args String foo = "hello world"

@s().capFirst(foo) @// use s() utility to process a string
@// the above code will produce exactly the same result of
@foo.capFirst() @// use built-in transformer to process a string

@if (s().eq(foo, bar)) {
 ...
}

@s().escapeHtml(foo) @// use s() to escape a string
@// the above code will produce exactly the same output of
@foo.escapeHtml() @// use built-in transformer to process a string

@s().random() @// generate a random string
@s().random(8) @// generate a random string with 8 characters

@s().i18n(foo) @// get i18n message of foo
@// the above code will produce exactly the same result of 
@foo.i18n() @// use built-in transformer to get the message of foo

So you might notice a fact that in case s() function returns a processing result of a single parameter, then there is always a corresponding transformer corresponding with it. That's true because Rythm registered all those processing method of s() utility as built-in transformer. And normally using transfomer is easier and clean than s(). However in case you can't use transformer, you will probably come back to s()

[flow-control]Template flow control

Rythm provides three flow control directive for template author to control the template rendering:

[brancing] Branching

@if (isMobile) {
    @// render mobile block
} else {
    @// render pc block
}

[loop] Looping

<ul>
@for(Product product: products) {
    <li>@product.getName(). Price: @product.getPrice().format("## ###,00") €</li>
}
</ul>

[return] Stop rendering and return

@if (user.hasRole("guest")) {
    <p>You don't have permission to view this page</p>
    @return
}
@// continue rendering the secured page

For more about the flow control please refer to the directive reference:

[invoke_template]Invoke template

The capability of invoking other template from the current template is an important feature that facilitate template reuse and makes template user's life easier. Many template engine provides tools to support template reuse, e.g. the #include and #parse of velocity, the #include, #macro and #import of freemarker, like those template engines, Rythm also provides tools to support template reuse, just in a more general and much easier way.

Suppose you have a template foo.html and another one named bar.html in the same directory. The following command allows you invoke bar.html inside foo.html:

@foo()

See following topics for more about template invocation:

[inv_arg] Passing arguments when invoking a template

As stated above most template has arguments. Thus when calling a template with arguments you need to pass arguments to them. Again the foo and bar case. Now suppose the bar template has an argument declaration:

@** the bar.html template **@
@args String whoCallMe, boolean sayBye
Hi @whoCallMe, this is inside bar.
@if (sayBye) {
Bye @whoCallMe
}

Now the foo template needs a little bit update:

@** the foo.html template **@
@bar("foo", true)

In the above example, we pass parameters to template by parameter declaration position. But Rythm also provides a more friendly parameter passing approach when you have many parameters

@** the foo.html template **@
@bar(whoCallMe: "foo", sayBye: true)

or even with the nice javascript style

@** the foo.html template **@
@bar({
    whoCallMe: "foo",
    sayBye: true
})

[inv_path] Handling paths

Usually templates are organized in different folders in a non-trial project. This brings the issue of how to invoke a template that is not in the same folder of the current template. Following the idea of java package, Rythm allows it to use dot separated path to identify a template. Consider the following directory structure:

\---app
    \---rythm
        |   tmpl1.html
        |
        +---Application
        |   |   index.html
        |   |   tmplA.html
        |   |
        |   \---bar
        |           tmplB.html
        |
        \---foo
            |   tmpl2.html
            |
            \---zee
                    tmpl3.html

Now inside the app/rythm/Application/index.html, you call the other templates like follows:

@Application.tmplA() @// full path invocation
@Application.bar.tmplB() @// full path invocation
@tmplA() @// relative path invocation
@bar.tmplB() @// relative path invocation
@foo.zee.tmpl3() @// full path invocation

You can also save typing by using @import directive:

@import foo.*
 
@foo.tmpl2() @// full path invocation
@foo.zee.tmpl3() @// full path invocation
@tmpl2() @// import path invocation
@zee.tmpl3() @// import path invocation

[inv_ext] Invoke template with different extension

Normally you don't need to add template extension (e.g. .html) when you invoke another template if the template been invoked has the same extension as the current template. For example, if you invoken template bar.html from template foo.html, simply use @bar() is enough.

However if you want to invoke a template with a different extension, say you want to invoke a template bar.js from template foo.html, you need to add the extension: bar.js(). For the moment Rythm support template source file with the following extensions:

  • .html
  • .json
  • .js
  • .css
  • .csv
  • .tag
  • .xml
  • txt
  • rythm

[inv_dyna] Dynamic template invocation

There are some cases that template being invoked can only be determined at runtime. Rythm provides dynamic template invocation to handle this situation:

@args String platform @// could be pc, iphone, ipad ...
...
@invoke("designer." + platform)

In the above code, your templates are stored in different files which corresponding to platform types, and when you are writing the template, you don't know which template exactly to invoke. The above code enable you to invoke the needed template based on a runtime variable platform. So when the platform value is "pc", the above code exactly has the same effect as

@designer.pc()

[inv_body]Passing body with template invocation

In Rythm it is possible to pass additional content in addition to parameters when invoking a template:

@greenscript.js() {
    $(function()) {...}
}
This feature is known as nested body in freemarker
[inv_body_callback]Pass body callback parameter spec to template call

With the introduction of callback extension, Rythm make it possible to pass in parameters when callback the body in the callee template:

@lookupRole(permission: "superuser").callback(List<Role> roleList) {
    <ul>superusers
    @for(Role role: roleList) {
        <li>@role.getName()</li>
    }
    </ul>
}

and in your lookupRole.html template:

@args String permission
 
@{
    List<Role> roles = Role.find("permission", permission).asList()
}
@renderBody(roles)

[inv_process] Further processing template invocation result

Usually when a template is invoked, the rendering result is inserted at where the invocation is happening. Rythm makes it flexible to allow user to further processing the result before inserted.

[inv_esc]Escape invocation result

It is possible to escape template invocation result using specific format:

@foo().escape("csv")
Escape template invocation result is different from escaping a template block contains the invocation
@escape("csv") {@foo()}

The above code makes sure all expressions output within @foo() call will be escaped using “csv” format, while @foo().escape(“csv”) ensure the tag invocation result itself get escaped.

[inv_cache]Cache invocation result

One interesting tool Rythm provides is it allows you to cache the template call result when it returns and use the cached content next without calling the template again:

@bar("foo", true).cache() @// cache using default TTL, which is 1 hour
@bar("foo", true).cache("1h") @// cache invocation result for 1 hour
@bar("foo", true).cache(60 * 60) @// cache invocation result for 1 hour

The above statement invoke template bar using parameter ["foo", true] and cache the result for one hour. Within the next one hour, the template will not be invoked, instead the cached result will be returned if the parameter passed in are still ["foo", true].

So you see Rythm is smart enough to cache template invocation against tag name and the parameter passed in. If the template invocation has a body, the body will also be taken into consideration when calculating the cache key.

In some cases where the template invocation result is not merely a function of the template, parameter and body, but also some implicit variables, where you might expect different result even you have completely the same signature of template invocation. Rythm provide way for you to take those additional implicit variable into account when calculating the cache key:

@{User user = User.current()}
@bar("foo", true).cache("1h", user.isAdmin())

The above statement shows how to pass additional parameter to cache decoration.

[inv_assign]Assign invocation result

In case you want to reuse a single invocation result in multiple place in the current template, you can use .assign() extension to assign the template invocation result into a variable:

@bar("foo", false).assign(fooResult)

Now you have a template variable fooResult contains the content of foo.html. You can process it and output it later on:

@fooResult.toLowerCase().raw()
[inv_chain] Chain the invocation decoration functions

It is possible to chain the invocation extension functions in one statement:

@someTemplate().raw().cache("1h").assign("xResult").callback(String name) {
    echo @name
}

[inline_tag] Define and use inline tag

Although template invocation is already very simple and easy to use compare to other competitors, Rythm provides an even more lightweight way for code reuse: the inline tag.

Suppose you have some repeated code structure in one template source, and you don't think it's heavy enough to isolate them out and put them into a separate template file, then inline tag is your friend. Seriously I found this feature is one of my favourite feature in my daily works.

The following code is picked up from the source code of rythm-web site project, which is what you are looking at now (unless you are reading this doc on github):

<ul class="nav-sub doc clearfix container">
    @def book(String bookId, String url) {
    <li class="book clearfix @bookId" data-url="@url">
        <i class="icon-book" style="font-size: 32px;margin-bottom: 10px;"></i>&nbsp;
        @{bookId = "main.book." + bookId;}
        <div>@i18n(bookId)</div>
    </li>
    }
    @book("index", "/doc/index.md")
    @book("tutorial", "/doc/tutorial.md")
    @book("template_guide", "/doc/template_guide.md")
    @book("developer_guide", "/doc/developer_guide.md")
    @book("configuration", "/doc/configuration.md")
    @book("directive", "/doc/directive.md")
    @book("builtin_transformer", "/doc/builtin_transformer.md")
</ul>

The code demonstrate how you define a inline tag "book" and use it immediately after definition. When I create html templates, I found the way is really a finger type saving feature.

[line_tag_limit] Limits of inline tag

There are certain limits you should be aware of using inline tag compare to a normal template

  • you can not use template invocation extensions (i.e. escape, cache, assign) on an inline tag call, but there are workarounds:

    @myInlineTag().cache("3h") @// this is NOT okay
    @// the following is good
    @cache("3h") {
        @myInlineTag()
    }
    
    @myInlineTag().raw().assign("myResult") @// this is NOT okay
    @// the following is good
    @chain().raw().assign("myResult") {
        @myInlineTag()
    }
    
  • You cannot pass argument by name when invoking inline tag. ALWAYS pass parameter to inline tag by position:

    @myInlineTag(varName: "value") @// this will cause compilation error
    @myInlineTag("value") @// this is okay
    

[inline_tag_return_type] Inline tag with return type

One of the cool thing is you are not limited to use inline tag to output repeated code, but also use it to define your utility functions, and yes they are translate into protected methods in the source code generated for your template:

@def boolean isMobile() {
    UserAgent ua = UserAgent.current();
    return ua.isMobile();
}

And later on it can be used as:

@if (isMobile()) {
   @mobile.designer()
} else {
   @pc.designer()
}

The following code demonstrate a subtle difference of Java code scripting inside inline tag when return type is presented or not:

@def boolean isMobileAndShowPlatform() {
    UserAgent ua = UserAgent.current();
    boolean isMobile = ua.isMobile();
    p("The platform is ");
    p(isMobile ? "mobile" : "pc");
    return isMobile;
}

@def showPlatform() {
    @{ UserAgent ua = UserAgent.current() }
    The platform is @if (ua.isMobile()) {mobile} else {pc}
}

So the rules are

  • if inline tag return type is presented and is not void

    • Java code shall be put inside tag definition directly
    • Output message shall be done via p() function call
  • If inline tag return type is absent or is void

    • Java code shall be put inside @{ } scripting block
    • Output message shall be put inside tag definition directly

[include]Include other templates

As the third code reuse mechanism, Rythm support include other template inline:

@include("foo.bar")

The above statement will put the content of template foo.bar in place. “foo.bar” is translate into file name following template invocation convention.

The difference between @include(“foo.bar”) a tag and call the template via @foo.bar() is the former put the content of the template into the current template inline, while the latter invoke the template specified and insert the result in place. It is some how like #include vs. function call in c language. @include is super fast to reuse part of template because it suppress the function invocation at runtime. It’s a inline function call if you speak c++. In other words, @include process happen at template parsing time, while tag invocation happen at template executing time.

Things you can achieve with @include but not template invocation:

Things you can achieve with template invocation but not @include

Because @include are parsed at parsing time therefore it’s not possible to include template dynamically as shown below:

@// spec could be one of facebook, google
@args String spec
 
@include("page." + spec) @// THIS WON'T WORK
@invoke("page." + spec) @// THIS WORKS!

It is also not possible to apply invocation decorations to @include() for the same reason.

@include("my.common.tag.lib").cache().assign("someVariable").raw() @// THIS WON'T WORK
@my.common.tag.lib().cache().assign("someVariable").raw() @// this works
@// the following also works
@chain().cache().assign("someVariable").raw() {
    @include("my.common.tag.lib")
}

[reuse_inline_tag] Reuse inline tag across multiple views

A good feature provided with @include is that you can import the inline tag definition from the template being included:

Suppose you have created a template named rythm/util.html with a set of inline tags:

@def hi (String who) {
    Hi @who
}
 
@def bye (String who) {
    Bye @who
}

Now in your other template you can import all the inline tags hi, bye and call them like a local function:

@include("util")
@hi("rythm")
@bye("rythm")

[macro]Define and execute Macro

Macro is the forth code reuse mechanisn in Rythm. Like @include, macro provides a way to reuse template content at parsing time, but inside a template file. Suppose you defined a macro called “myMacro”:

@macro("myMacro") {
content inside myMacro
}

Later on in the same template file you can invoke "myMacro" using the following code:

...
@exec("myMacro") @// first method to execute a macro
@myMacro() @// second method to execute a macro

which produce the following output:

content inside myMacro

At first glance this effect could be achieved using inline tag or even assignment. However they are fundamentally different in that macro executing happen at parsing time while tag call and assignment are happened at runtime, which could result in subtle differences of content been rendered. Let’s take a look at the following code:

@macro("foo") {
<foo>
    <bar>abc</bar>
</foo>
}
@compact() {@exec("foo")}
---------------------------
@nocompact() {@exec("foo")}

So the above code will display “foo” content in compact and nocompact mode. If we change the implementation to:

@def foo() {
<foo>
    <bar>abc</bar>
</foo>
}
@compact() {@foo()}
----------------------
@nocompact() {@foo()}

The result will not be expected. The reason is assignment will evaluate the content at runtime, and when “foo” content is assigned, the content is already produced and later on calling it in @compact and @nocompact block will not make any change. For the same reason the following code works neither:

@assign("foo") {
<foo>
    <bar>abc</bar>
</foo>
}
@compact() {@foo.raw()}
-------------------------
@nocompact() {@foo.raw()}
Macro will be expanded at parsing time, therefore it is very fast at runtime but will generate larger class byte codes, furthermore macro will guaranteed to be executed when invoked with @exec, this is unlike @assign, which is executed for only once when assignment happen.
Macro has higher priority than tag. Meaning if you have both “foo” macro and “foo” tag defined, @foo() will invoke foo macro instead of foo tag

[reuse-methods-summary] Reuse mechanism summary

The following table brief the four reuse mechanisms of Rythm:

External Internal
Runtime @invoke @def
Parsing time @include @macro

As shown in the above table, each one of the four reuse mechanims has it's characters. Here are some general guide lines to choose which method to use:

  • For common code across multiple templates, use external reuse method, i.e. any one of @invoke and @include
  • For repeating patterns appeared within current template, use internal reuse method, say one of @def and @macro
  • If you need to pass parameters to the reuse part, use Runtime methods: @invoke or @def
  • If you don't need parameters to the reuse part, use Parsing time methods: @include or @macro

[reuse-priority] Priority of code reuse mechanism

Except@include, all other three reuse mechanism support call style of @foo(). This brings a concern of priority. Suppose you have a foo.html template file, an inline tag with name foo and a macro with name foo, in case @foo() is encountered, Rythm will decide which mechanism to use following the following rules:

The inline tag without arguments has higher priority than Macro

@def foo() {foo inside inline tag}
@macro(foo) {foo inside macro}

@foo()

Macro has higher priority than template invocation

  • the template content of foo.html
foo invoked by template call
  • the template content of the main template
@macro(foo) {foo inside macro}
@foo()

[inheritance] Template inheritance

Template inheritance is a good way to implement template layout management.

@// this type of extended template declaration is deprecated: @extends("main.html")
@extends(main)
<h1>Some code</h1>

The main template is the layout template, you can use the doLayout tag to include the content of the declaring template:

<h1>Main template</h1>
<div id="content">
    @doLayout()
</div>
render() or renderSection() without section name specified does the same thing with @doLayout()

You can even specify the default content if the sub template does not have any content provided:

<h1>Main template</h1>
<div id="content">
    @doLayout() {
        default content
    }
</div>

[ext_lookup]Extended template lookup

Rythm use the same approach to look up extended template and template being invoked, for example, if you want to extend rythm/layout/foo/bar.html, you can declare the extend statement as @extends(layout.foo.bar). Please refer to Handling paths for more detail.

[inheritance_section] Define section and output section

Like Razor, Rythm provides a section concept to enable you to define sections in sub templates and output them in parent template.

In your layout template, say main.html:

<!DOCTYPE html>
@args String title = "ABC Corporate"
<html>
<head>
...
</head>
<body>
  <div id="header">
    <h1>@title</h1>
  </div>
 
  <div id="sidebar">
    @// render "sidebar" section
    @render("sidebar")
  </div>
 
  <div id="content">
    @// render main content
    @render() @// or doLayout()
  </div>
 
  <div id="footer">
    @render("footer") {
        @// here we define default content for footer section
        @// if sub template failed to supply this section then
        @// the default content will be output instead
        <p>Site footer - &copy; Santa Clause</p>
    }
  </div>
</body>
</html>

And in your working template, say ‘index.html’:

@extends(main)
@set(title="Home page")
 
<h2>Welcome to my site</h2>
<p>This is our home page</p>
<p>Not super exciting is it?</p>
<p>Yada, Yada, Yada</p>
 
@section("sidebar") { @// define "sidebar" section
  <p>This sidebar has "Home Page" specific content</p>
  <ul>
    <li><a href="#">Link One</a></li>
    <li><a href="#">Link Two</a></li>
    <li><a href="#">Link Three</a></li>
  </ul>
}

In the above example people with sharp eyes can notice that the footer section render in the layout template is different from the sidebar section in that we supplied a default content to the former. So if user did not define the footer section in the sub template, the default content will be output instead.

[see-also] See also