/Shotenjin

Post-modern javascript templating system

Primary LanguageJavaScript

Name

Shotenjin - Post-modern JavaScript template engine

SYNOPSIS

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.

INSTALLATION

From npm:

> [sudo] npm install shotenjin

Tarballs are available for downloading at: http://search.npmjs.org/#/shotenjin

SETUP

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>

DESCRIPTION

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.

SYNTAX

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

Escaped expressions

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:

    '&'     : '&amp;'
    '<'     : '&lt;' 
    '>'     : '&gt;' 
    '"'     : '&quot;'

Unescaped expressions

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)

Control statements

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.

USAGE

Classic

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'
    })

Post-modern

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>'
    })

Helper script

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.

API

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.

Methods

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

Attributes

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 =

WHITESPACE HANDLING

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.

TEMPLATE STRUCTURE

You may skip this section for the first-time reading.

Empty template body

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 %]

Ordinary text

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(); 
    }

Escaped expression

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

Unescaped expression

    [%= name[1] %]

Is just passed through:

    function (stash) {
        this.startContext();
        
        eval(this.expandStashToVarsCode(stash));
        
        __contexts[0].output.push( name[1] , "");
        
        return this.endContext();
    }

Statements

    [%\ 
        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') } %]

GETTING HELP

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

ACKNOWLEDGEMENTS

Many thanks to Makoto Kuwata for his Tenjin implementation, on which this engine is based.

SEE ALSO

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

BUGS

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

AUTHORS

Nickolay Platonov nplatonov@cpan.org

COPYRIGHT AND LICENSE

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.