JSON Template Engine //JavaScript Repository

Description

Simple, extensible and efficient template engine. It's able to parse indented template tags and build its respective structure using JSON. The way it was implemented allows a better organization and turns the filling of the template easier.
Created: 2006.02.04

Code (Download)

//+ Carlos R. L. Rodrigues
//@ http://jsfromhell.com/classes/template [rev. #2]

Template = function(s, oo, oc, co, cc){
    var o = this; o.oo = oo || "<js:", o.oc = oc || ">", o.co = co || "</js:", o.cc = cc || ">";
    if(o.oo == o.co || o.oo == o.oc || o.oo == o.cc || o.co == o.oc || o.co == o.cc) throw Error("Tag definition not allowed");
    o.tags = [], o.root = o.node("_root", s, null), o.parse(s);
}
Template.prototype.node = function(n, v, p){
    var _ = this;
    return {
        _default: v, _value: [v], _parent: p, _children: [], _name: n,
        _render: function(_o){
            var o = this._children, s = (v = this._value)[v.length - 1], p = [], i = -1,
            t = _.oo, c = _.co, tc = _.oc, cc = _.cc, y, v, a, f, j, to;
            while((y = o[++i]) && (v = s.slice(0, j = s.indexOf(to = t + (a = y._name) + tc)), f = s.indexOf(a = c + a + cc, j))){
                while((j = s.indexOf(to, j + 1)) + 1 && j < f) f = s.indexOf(a, f + 1);
                if(f + 1 && (s = v + y._value.join("") + s.slice(a.length + f)), y._children[0]) p.push(i, o), o = y._children, i = -1;
                else if(!o[i + 1]) while(p.length && !(o = p.pop())[(i = p.pop()) + 1]);
            }
            return _o ? s : (i = (v = this._value).length, v[i] = v[--i], v[i] = s);
        },
        _set: function(s){return this._value[this._value.length - 1] = s;},
        _get: function(){
            var r = /([(){}|*+?.,^$\[\]\\])/g, f = function(s){return s.replace(r, "\\\$1")};
            return this._value.join("").replace(new RegExp((f(_.oo) + ".*?" + f(_.oc) + "|" + f(_.co) + ".*?" + f(_.cc)), "gm"), "");
        },
        _reset: function(c){
            if(c)
                for(var a, o = this, t = _.tags, i = 0, l = t.length; i < l; i++)
                    ((a = t[i]._parent) == o || a == this) && ((o = t[i])._value = [o._default]);
            return this._value = [this._default];
        },
        _output: function(){return this._render(1);}
    };
}
Template.prototype.parse = function(s){
    var _ = this, p = 0, r = _.root, l = [[-1, s.length, r]], y = _.tags, i = _.oo,
    e = _.co, $ = _.oc, d = _.cc, h = $.length, g, a, f, j, c, t, v;
    while((p = s.indexOf(i, p)) > -1){
        if(p == s.indexOf(e, p) && ++p) continue;
        a = (a = p + i.length) + (t = s.slice(a, j = s.indexOf($, p))).length + h, f = s.indexOf(g = e + t + d, p);
        while((j = s.indexOf(i + t + d, j + 1)) + 1 && j < f) f = s.indexOf(g, f + 1);
        if(t.charAt() == "_") throw Error("Tag name not allowed [" + t + "]");
        if(f < 0) throw Error("End of tag \"" + i + t + $ + "\" expected");
        for(v = s.slice(a, f), j = l.length; j--;)
            if((c = l[j])[2][t] && p > c[0] && f < c[1]) throw Error("Ambiguous tag name \"" + t + "\"");
            else if(p > c[0] && f < c[1] && l.push([p++, f, (v = c[2][t] = _.node(t, v, c[2]))]) &&
                (!(a = c[2]._parent) ? c[2] : a[c[2]._name])._children.push(v) && y.push(v)) break;
    }
}

Example (Example)

<script type="text/javascript">
//<![CDATA[

s =
"<html>\n" +
    "<head>[header]\n" +
        '[js]<script src="[src][/src]" type="text/javascript"><\/script>\n[/js]' +
    "[/header]</head>\n" +
    "<body>\n" +
    "[content]Default content[/content]\n" +
    "</body>\n" +
"</html>";

var t = new Template(s, "[", "]", "[/", "]");

var js = t.root.header.js, content = t.root.content;

js.src._set("file.js");
js._render();

js.src._set("file2.js");

content._set("I Changed the content");
alert(t.root._output());

content._reset();
alert(t.root._output());

js._reset(true);
alert(t.root._output());

js._set("");
content._set("");
alert(t.root._output());

//]]>
</script>

Help

How it works

This class is similar to a xml parser, it searches for template tags on the text and make them easily to access through the class, adding special properties and methods to each node.

By default is recognized as a template tag the tags that follow this pattern: <js:TagName>any content</js:TagName>. But this is configurable, example: [TagName]content[/TagName].

Template example:
<js:a>
	<js:b>content</js:b>
</js:a>

After loading the template above, you could access the text "content" by using templateInstance.root.a.b._get()

There are two restrictions on the tag naming:

  • The tag name can't start with "_".
    Wrong: [_myTag][/_myTag]
  • There must not be tags with the same name in the same indentation level.
    Wrong: [a][/a][a][/a].
    Correct: [a][/a][b][/b].

    Wrong: [a][b][/b][b][/b][/a].
    Correct: [a][b][/b][c][/c][/a].

    Correct: [a][b][/b][a][b][/b] (the second [b] is in another indentation level).

To get more details of how it works, look at the example.

Constructor

Template(content: String, [beginOpen: String = "<js:"], [beginClose: String = ">"], [endOpen: String = "</js:"], [endClose: String = ">"])
Generates an instance of Template.
Restrictions:
  • beginOpen can't be equal to endOpen
  • beginOpen can't be equal to beginClose
  • beginOpen can't be equal to endClose
  • endOpen can't be equal to beginClose
  • endOpen can't be equal to endClose
content
text to be parsed by the class
beginOpen
defines the opening tag prefix
beginClose
defines the opening tag suffix
endOpen
defines the closing tag prefix
endClose
defines the closing tag suffix

Properties

Template.root: Node
It's the root node, must be used to obtain access to the other nodes through "JSON".
Template.tags: Array
Array containing references to all the nodes.

Node Methods

Node._get(void): String
Returns the node value. If there are template tags inside this text, they will be removed, keeping just the default text inside them.
Node._set(value: String): String
Sets a new value to the node and, returns the own setted text. If the node have children, they will be ignored (but not really removed) on the output.
value
text to be setted
Node._reset([deep: Boolean = false]): void
Undo all the modifications done on the node by the methods "_render" or "_set".
deep
if true, it will also reset the child nodes
Node._render(void): String
The function returns the current node value and implements a kind of "content stack". The current node value is pushed (you have access to it through the "_value" property), and the node starts to work on a copy of the value.
Obs: The unqueueing will occur automatically on the output.
Node._output(void): String
Returns the text of the node and its child nodes. It's similar to "_get", but differently from "_get", that just returns the node value, it also gets the text of the child nodes.

Node Properties

Node._default: String
Keeps the initial node value.
Node._parent: Node
Reference to the parent node.
Node._name: String
Tag name.
Node._children: Array
Array containing the child nodes.
Node._value: Array
Keep the node text in a kind of queue structure.

Rank (Votes: 43)

3.07