Shotenjin - Post-modern JavaScript template engine
Always classic:
var tenjin = new Shotenjin.Template({
sources : 'Hello [% world %]'
})
var rendered = tenjin.render({
world : 'world'
})
Post-modern:
Template('Table.Cell', {
template : '<td>[% text %]</td>'
})
Template('Table.Row', {
use : 'Table.Cell',
template : '<tr>[%\\ Joose.A.each(row, function (cell, index) { %][%= Table.Cell({ text : cell }) %][%\\})%]</tr>'
})
Template('Table', {
use : 'Table.Row',
template : '<table>[%\\ Joose.A.each(table, function (row, index) { %][%= Table.Row({ row : this.helper(row) }) %][%\\})%]</table>',
methods : {
helper : function () { ... }
}
})
var tableData = [
[ '1', '1', '2' ],
[ '3', '5', '8' ],
[ '13','21','34' ]
]
var rendered = new Table({ table : tableData })
// or just
var rendered = Table({ table : tableData })
Less-noisy with the helper script:
Template('Chapter', {
template : {
/*tj
<h2>[% title %]</h2>
<p> [% content %]</p>
tj*/
/* GENERATED BY SHOTENJIN HELPER, DO NOT MODIFY DIRECTLY */
sources : '<h2>[% title %]</h2>\n<p> [% content %]</p>'
}
})
Template('Book', {
template : {
/*tj
[%\ this.wrapper(Chapter, { title : 'Chapter1' }, function () { %]
Text of first chapter.
[%\ }); %]
[%\ this.wrapper(Chapter, { title : 'Chapter2' }, function () { %]
Text of second chapter.
[%\ }) %]
tj*/
/* GENERATED BY SHOTENJIN HELPER, DO NOT MODIFY DIRECTLY */
sources : '[%\\ this.wrapper(Chapter, { title : \'Chapter1\' }, function () { %] \nText of first chapter.\n[%\\ }); %][%\\ this.wrap(Chapter, { title : \'Chapter2\' }, function () { %]\nText of second chapter.\n[%\\ }) %]'
}
})
var rendered = Book()
If you are reading this file as README from github, you may want to open this link instead.
From npm
:
> [sudo] npm install shotenjin
Tarballs are available for downloading at: http://search.npmjs.org/#/shotenjin
In NodeJS:
require('task-joose-nodejs')
require('shotenjin')
In browsers (assuming you've completed the 3.1 item from this document):
<script type="text/javascript" src="/jsan/Task/Joose/Core.js"></script>
<script type="text/javascript" src="/jsan/Task/Shotenjin/Core.js"></script>
Shotenjin is a Yet Another JavaScript Templating Engine, based on Tenjin by Makoto Kuwata Shotenjin was ported to Joose, along with some improvements.
The main difference of Shotenjin from other templating solutions is that for the templating language it uses JavaScript itself. Thus, Shotenjin templates are not compiled into JavaScript, they are only parsed.
Shotenjin uses 3 types of template instructions. Keep in mind, that all them, in the same time, are just ordinary JavaScript expressions, which are evaluated directly by the JavaScript engine of your choice, in the context of the template function (see below for its internal structure).
Such expression are represented with the following construct:
[% name %]
[% name + ' ' + surname %]
[% Digest.MD5.md5_hex(response) %]
The value of the expression will be escaped before adding to template, according to this table:
'&' : '&'
'<' : '<'
'>' : '>'
'"' : '"'
Such expression are represented with the following construct
[%= person.name %]
[%= document.body.innerHTML %]
Memo - passed-through exactly (equally) as calculated
The value of the expression will not be escaped before adding to template. Thus, this expressions can be used to generate HTML markup.
This instruction can be used to include the resulting content of another template for example, (see example with Table
in Synopsys above)
This type of instructions represent arbitrary JavaScript code, which will be added to template function unmodified. So, generally, it shouldn't return a value, but should modify the control flow:
[%\ for (var i = 0; i < persons.length; i++) { %]
<tr><td>[% persons[i].name %]</td></tr>
[%\ } %]
[%\ if (a == b) { %]
<tr><td>[% person.name %]</td></tr>
[%\ } else { %]
<tr><td>[% person.surname %]</td></tr>
[%\ } %]
Multi-line code is ok:
[%\
var sum = 0
Joose.A.each(persons, function (person, index) {
sum += person.parameter
})
%]
Totally: [% sum %]
Memo - lambda function in Haskell.
As you can see any code can be embedded into templating function, and you don't need to learn one more language to create a template.
Shotenjin can be used in "classic" way, in which you are responsible for instantiation and rendering:
var tenjin = new Shotenjin.Template({
sources : 'Hello [% world %]'
})
var rendered = tenjin.render({
world : 'Shotenjin'
})
In the "post-modern" usage scenario, the template instance is embedded into Joose class:
Class('Table.Cell', {
meta : 'Shotenjin',
template : '<td>[% text %]</td>'
})
Additional helper Template
is introduced to simplify the declaration:
Template('Table.Cell', {
template : '<td>[% text %]</td>'
})
By default, the class with the embedded template is a subclass of Shotenjin.Template
.
You can define additional methods (they will be available in templating function) or attributes, inherit from another template, apply Roles or something else. Please refer to Joose manual to know what you can do with Joose classes. A simple example:
Template('Table.Cell', {
use : 'Text.Format',
template : '<td>[% this.cellFormatter(text) %]</td>'
methods : {
cellFormatter : function (value) {
...
}
}
})
The created Table.Cell
class will be a singleton, which is instantiated internally. Its external constructor (Table.Cell
) will be
binded to the render
method, so, to render the template, call the constructor with the required data:
var rendered = new Table.Cell({ text : 'some text' })
// or just
var rendered = Table.Cell({ text : 'some text' })
Note: Constructor will return instance of String (new String()), so in the example above the following will be true: `typeof rendered == 'object``
Naturally, you can easily include such template into another template:
Template('Table.Row', {
use : 'Table.Cell',
template : '<tr>[%\\ Joose.A.each(row, function (cell, index) { %][%= Table.Cell({ text : cell }) %][%\\})%]</tr>'
})
You may have noticed, that writing templates into JavaScript can be a cumbersome task, because you need to manually escape each special symbol, like \
To address this issue Shotenjin
comes with the helper script shotenjin_embed.pl
. Its a simple shell script, which examine the passed file
(or files in directory) for the templates, embedded into JavaScript comments /*tj ... tj*/
Template('Book', {
template : {
/*tj
[%\ this.wrapper(Chapter, { title : 'Chapter1' }, function () { %]
Text of first chapter.
[%\ }); %]
[%\ this.wrapper(Chapter, { title : 'Chapter2' }, function () { %]
Text of second chapter.
[%\ }) %]
tj*/
}
})
Once found, script append such comments with the escaped version of the template:
Template('Book', {
template : {
/*tj
[%\ this.wrap(Chapter, { title : 'Chapter1' }, function () { %]
Text of first chapter.
[%\ }); %]
[%\ this.wrap(Chapter, { title : 'Chapter2' }, function () { %]
Text of second chapter.
[%\ }) %]
tj*/
/* GENERATED BY SHOTENJIN.JOOSED HELPER, DO NOT MODIFY DIRECTLY */
sources : '[%\\ this.wrap(Chapter, { title : \'Chapter1\' }, function () { %] \nText of first chapter.\n[%\\ }); %][%\\ this.wrap(Chapter, { title : \'Chapter2\' }, function () { %]\nText of second chapter.\n[%\\ }) %]'
}
})
Also, its possible to extract the content of the template from the external file:
Template('Book', {
template : {
/*tjfile(Book.tj.html)tj*/
}
})
By default, the location of the template file is determined relative of the source file itself, you can switch the relativity to the current working directory with the --absolute
option.
Script is supposed to be run during your project's build phase. All modern IDE will allow you to automatically run the script, when one of the files in the project was modified, so spend some time to examine your IDE's documentation for that.
Script accepts a single command-line argument, which should be either the path to file to examine, or the directory, which will be scanned for all *.js
files.
Script also accepts an --absolute
option, which will makes it to look for template files relative of current working directory.
During evaluation of the template, this
value is associated with the instance of Shotenjin.Template
(or its subclass), so naturally all the methods of this class are available.
this.render(Object stash)
Renders the template using data, passed in stash
this.capture(Function func)
Captures the output, generated inside the passed function and returns it. For example:
[%\ var names = this.capture(function () {
for (var i = 1; i <= 5; i++) {
%]
[% persons[i].name %]
[%\ }
})
%]
[% names %]
A new context is derived for the passed function and the output of the outer template isn't modified.
this.echo(Object str1, Object str2, ...)
Escapes and adds each of passed arguments to the output of the current context. Usually a context is a template itself, however nested contexts may be derived (see this.capture
and this.wrapper
)
this.echoRaw(Object str1, Object str2, ...)
Unescaped version of echo
.
this.wrap(Class|Shotenjin.Template template, Object stash, Function func)
First captures the content generated into the passed func
, then assign it to the content
key of the passed stash
object and renders the wrapping template with it.
Wrapping template
can be passed as the instance of Shotenjin.Template
or as Class.
this.escapeXml(String str)
Escapes reserved HTML symbols in the str
and returns modified string. Override this method if you need to implement more precise escaping rules.
These attributes can be either passed to constructor ("classic" way) or specified for class using has
builder ("post-modern" way)
startTag
String for opening tag. Default value is [%
endTag
String for closing tag. Default value is %]
statementTagModifier
Modifier to specify the statement instruction. Default value is \
expressionTagModifier
Modifier to specify the unescaped expression. Default value is =
The rules for whitespace processing:
-
First of all, leading and trailing whitespace on each line is trimmed.
-
If the control flow statement is followed with the newline, that newline will be "ate" and don't included into template's output.
-
All other whitespace (including newlines after expressions) is preserved.
You may skip this section for the first-time reading.
In the simplest case of empty template body, the template function looks like:
function (stash) {
this.startContext();
eval(this.expandStashToVarsCode(stash));
return this.endContext();
}
This function accepts a single argument, which should be an Object
(in JavaScript meaning) and which is called stash.
This statement
eval(this.expandStashToVarsCode(stash));
creates a local variable for each key of the stash, so later, any key of the stash can accessed directly by its name.
Stash can be also accessed directly, if you prefer:
[% stash.name %]
[% stash.person.name %]
If the template contains an ordinary text, like:
foo 'bar'
its translated as:
function (stash) {
this.startContext();
eval(this.expandStashToVarsCode(stash));
__contexts[0].output.push('foo \'bar\'\\n', "");
return this.endContext();
}
[% name[1] %]
Corresponds to:
function (stash) {
this.startContext();
eval(this.expandStashToVarsCode(stash));
__contexts[0].output.push(__me.escapeXml( name[1] ), "");
return this.endContext();
}
Note:
- Whitespace immediately after opening tag / before closing tag is preserved.
- A semicolon is added after expression.
[%= name[1] %]
Is just passed through:
function (stash) {
this.startContext();
eval(this.expandStashToVarsCode(stash));
__contexts[0].output.push( name[1] , "");
return this.endContext();
}
[%\
for(var i in stash) {
this.someFunc(p1, p2)
}
%]
Is passed through completely unmodified. No semi-colons are appended.
function (stash) {
this.startContext();
eval(this.expandStashToVarsCode(stash));
for(var i in stash) {
this.someFunc(p1, p2)
}
return this.endContext();
}
This means you need to care about any needed punctuation (as with usual code). For example this template
[%\ for(var i in stash) { %][%\ this.echo('123') %][%\ this.echo('123') } %]
will produce
missing ; before statement
error. To fix it, add punctuation:
[%\ for(var i in stash) { %][%\ this.echo('123'); %][%\ this.echo('123') } %]
This extension is supported via github issues tracker: http://github.com/SamuraiJack/Shotenjin/issues
For general Joose questions you can also visit #joose on irc.freenode.org or mailing list at: http://groups.google.com/group/joose-js
Many thanks to Makoto Kuwata for his Tenjin implementation, on which this engine is based.
Port of Template::Toolkit to JavaScript: Jemplate
Google solution: Closure Templates
Templating solution of the ExtJS framework: Ext.XTemplate
General documentation for Joose: http://joose.github.com/Joose/doc/html/Joose.html
All complex software has bugs lurking in it, and this module is no exception.
Please report any bugs through the web interface at http://github.com/SamuraiJack/Shotenjin/issues
Nickolay Platonov nplatonov@cpan.org
Copyright (c) 2011, Nickolay Platonov
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of Nickolay Platonov nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.