Skip to content
This repository
Browse code

code conformity per jslint specifications.

  • Loading branch information...
commit 2071180cfb3eaf9f67ac88f92c805de9c8275913 1 parent aa848fa
Paul Armstrong authored
8 README.md
Source Rendered
@@ -23,7 +23,7 @@ Node-T is a templating engine inspired by the django syntax. It has a few extens
23 23
24 24 var template = require('node-t');
25 25 var tmpl = template.fromFile("/path/to/template.html");
26   - console.log( tmpl.render({names: ["Duke", "Django", "Louis"]}) );
  26 + console.log(tmpl.render({names: ["Duke", "Django", "Louis"]}));
27 27
28 28 ### How it works
29 29
@@ -139,13 +139,13 @@ Node.js code
139 139 }
140 140
141 141 context.widgets = {
142   - analytics: function(context){
  142 + analytics: function (context) {
143 143 // this inside widget functions is bound to the widget object
144 144 return "<script>..." + this.uaCode + "...</script>";
145 145 },
146   - navigation: function(context){
  146 + navigation: function (context) {
147 147 var i, html = "";
148   - for( i=0; i<this.links; i++ )
  148 + for (i=0; i<this.links; i++)
149 149 html += "<a href='" + links[i] + "'>" + links[i] + "</a>";
150 150 return html;
151 151 }
193 helpers.js
@@ -14,119 +14,132 @@ var KEYWORDS = /^(Array|RegExpt|Object|String|Number|Math|Error|break|continue|d
14 14 var VALID_BLOCK_NAME = /^[A-Za-z]+[A-Za-z_0-9]*$/;
15 15
16 16 // Returns TRUE if the passed string is a valid javascript number or string literal
17   -function isLiteral( string ){
18   - var literal = false;
19   - // Check if it's a number literal
20   - if( NUMBER_LITERAL.test( string ) ){
21   - literal = true;
22   - }
23   -
24   - // Check if it's a valid string literal (throw exception otherwise)
25   - else if( (string[0] === string[string.length-1]) && (string[0] === "'" || string[0] === '"') ) {
26   - var teststr = string.substr( 1, string.length-2 ).split("").reverse().join("");
27   - if( string[0] === "'" && UNESCAPED_QUOTE.test( teststr ) || string[1] === '"' && UNESCAPED_DQUOTE.test( teststr ) ){
28   - throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
  17 +function isLiteral(string) {
  18 + var literal = false,
  19 + teststr;
  20 +
  21 + // Check if it's a number literal
  22 + if (NUMBER_LITERAL.test(string)) {
  23 + literal = true;
  24 + } else if ((string[0] === string[string.length - 1]) && (string[0] === "'" || string[0] === '"')) {
  25 + // Check if it's a valid string literal (throw exception otherwise)
  26 + teststr = string.substr(1, string.length - 2).split("").reverse().join("");
  27 +
  28 + if (string[0] === "'" && UNESCAPED_QUOTE.test(teststr) || string[1] === '"' && UNESCAPED_DQUOTE.test(teststr)) {
  29 + throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
  30 + }
  31 +
  32 + literal = true;
29 33 }
30   - literal = true;
31   - }
32 34
33   - return literal;
  35 + return literal;
34 36 }
35 37
36 38 // Returns TRUE if the passed string is a valid javascript string literal
37   -function isStringLiteral( string ){
38   - // Check if it's a valid string literal (throw exception otherwise)
39   - if( (string[0] === string[string.length-1]) && (string[0] === "'" || string[0] === '"') ) {
40   - var teststr = string.substr( 1, string.length-2 ).split("").reverse().join("");
41   - if( string[0] === "'" && UNESCAPED_QUOTE.test( teststr ) || string[1] === '"' && UNESCAPED_DQUOTE.test( teststr ) ){
42   - throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
  39 +function isStringLiteral(string) {
  40 + // Check if it's a valid string literal (throw exception otherwise)
  41 + if ((string[0] === string[string.length - 1]) && (string[0] === "'" || string[0] === '"')) {
  42 + var teststr = string.substr(1, string.length - 2).split("").reverse().join("");
  43 +
  44 + if (string[0] === "'" && UNESCAPED_QUOTE.test(teststr) || string[1] === '"' && UNESCAPED_DQUOTE.test(teststr)) {
  45 + throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
  46 + }
  47 +
  48 + return true;
43 49 }
44   - return true;
45   - }
46   - return false;
  50 +
  51 + return false;
47 52 }
48 53
49 54 // Variable names starting with __ are reserved.
50   -function isValidName( string ){
51   - return VALID_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0,2) !== "__";
  55 +function isValidName(string) {
  56 + return VALID_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0, 2) !== "__";
52 57 }
53 58
54 59 // Variable names starting with __ are reserved.
55   -function isValidShortName( string ){
56   - return VALID_SHORT_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0,2) !== "__";
  60 +function isValidShortName(string) {
  61 + return VALID_SHORT_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0, 2) !== "__";
57 62 }
58 63
59 64 // Checks if a name is a vlaid block name
60   -function isValidBlockName( string ){
61   - return VALID_BLOCK_NAME.test(string);
  65 +function isValidBlockName(string) {
  66 + return VALID_BLOCK_NAME.test(string);
62 67 }
63 68
64 69 /**
65   - * Returns a valid javascript code that will
66   - * check if a variable (or property chain) exists
67   - * in the evaled context. For example:
68   - * check( "foo.bar.baz" )
69   - * will return the following string:
70   - * "typeof foo !== 'undefined' && typeof foo.bar !== 'undefined' && typeof foo.bar.baz !== 'undefined'"
71   - */
72   -exports.check = function( variable, context ){
73   - /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
74   - * '__this' is bound to the original template whose render function we called.
75   - * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
76   - * for binding data to a specific template - e.g. binding widget data.
77   - */
78   - variable = variable.replace(/^this/, '__this.__currentContext');
79   -
80   - if( isLiteral( variable ) )
81   - return "(true)";
82   -
83   - var props = variable.split("."), chain = "", output = [];
84   -
85   - if( typeof context === 'string' && context.length )
86   - props.unshift( context );
87   -
88   - props.forEach(function(prop){
89   - chain += (chain ? (isNaN(prop) ? "." + prop : "[" + prop + "]") : prop);
90   - output.push( "typeof " + chain + " !== 'undefined'" )
91   - });
92   - return "(" + output.join(" && ") + ")";
93   -}
  70 +* Returns a valid javascript code that will
  71 +* check if a variable (or property chain) exists
  72 +* in the evaled context. For example:
  73 +* check("foo.bar.baz")
  74 +* will return the following string:
  75 +* "typeof foo !== 'undefined' && typeof foo.bar !== 'undefined' && typeof foo.bar.baz !== 'undefined'"
  76 +*/
  77 +exports.check = function (variable, context) {
  78 + /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
  79 + * '__this' is bound to the original template whose render function we called.
  80 + * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
  81 + * for binding data to a specific template - e.g. binding widget data.
  82 + */
  83 + variable = variable.replace(/^this/, '__this.__currentContext');
  84 +
  85 + if (isLiteral(variable)) {
  86 + return "(true)";
  87 + }
  88 +
  89 + var props = variable.split("."), chain = "", output = [];
  90 +
  91 + if (typeof context === 'string' && context.length) {
  92 + props.unshift(context);
  93 + }
  94 +
  95 + props.forEach(function (prop) {
  96 + chain += (chain ? (isNaN(prop) ? "." + prop : "[" + prop + "]") : prop);
  97 + output.push("typeof " + chain + " !== 'undefined'");
  98 + });
  99 + return "(" + output.join(" && ") + ")";
  100 +};
94 101
95 102 /**
96   - * Returns an escaped string (safe for evaling). If context is passed
97   - * then returns a concatenation of context and the escaped variable name.
98   - */
99   -exports.escape = function( variable, context ){
100   - /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
101   - * '__this' is bound to the original template whose render function we called.
102   - * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
103   - * for binding data to a specific template - e.g. binding widget data.
104   - */
105   - variable = variable.replace(/^this/, '__this.__currentContext');
106   -
107   - if( isLiteral( variable ) )
108   - variable = "(" + variable + ")";
109   -
110   - else if( typeof context === 'string' && context.length )
111   - variable = context + '.' + variable;
112   -
113   - var chain = "", props = variable.split(".");
114   - props.forEach(function(prop){
115   - chain += (chain ? (isNaN(prop) ? "." + prop : "[" + prop + "]") : prop);
116   - });
117   -
118   - return chain.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
119   -}
  103 +* Returns an escaped string (safe for evaling). If context is passed
  104 +* then returns a concatenation of context and the escaped variable name.
  105 +*/
  106 +exports.escape = function (variable, context) {
  107 + /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
  108 + * '__this' is bound to the original template whose render function we called.
  109 + * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
  110 + * for binding data to a specific template - e.g. binding widget data.
  111 + */
  112 + variable = variable.replace(/^this/, '__this.__currentContext');
  113 +
  114 + if (isLiteral(variable)) {
  115 + variable = "(" + variable + ")";
  116 + } else if (typeof context === 'string' && context.length) {
  117 + variable = context + '.' + variable;
  118 + }
  119 +
  120 + var chain = "", props = variable.split(".");
  121 + props.forEach(function (prop) {
  122 + chain += (chain ? (isNaN(prop) ? "." + prop : "[" + prop + "]") : prop);
  123 + });
  124 +
  125 + return chain.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
  126 +};
120 127
121 128 /**
122   - * Merges b into a and returns a
123   - */
124   -exports.merge = function(a, b){
125   - if (a && b)
126   - for (var key in b) {
127   - a[key] = b[key];
  129 +* Merges b into a and returns a
  130 +*/
  131 +exports.merge = function (a, b) {
  132 + var key;
  133 +
  134 + if (a && b) {
  135 + for (key in b) {
  136 + if (b.hasOwnProperty(key)) {
  137 + a[key] = b[key];
  138 + }
  139 + }
128 140 }
129   - return a;
  141 +
  142 + return a;
130 143 };
131 144
132 145 exports.isLiteral = isLiteral;
206 index.js
... ... @@ -1,113 +1,127 @@
1   -var fs = require("fs");
2   -var util = require("util");
3   -var path = require("path");
4   -var crypto = require("crypto");
5   -var tags = require("./tags");
6   -var parser = require("./parser");
7   -var widgets = require("./widgets");
8   -
9   -var CACHE = {};
10   -var DEBUG = false;
11   -var ROOT = "/";
  1 +var fs = require("fs"),
  2 + util = require("util"),
  3 + path = require("path"),
  4 + crypto = require("crypto"),
  5 + tags = require("./tags"),
  6 + parser = require("./parser"),
  7 + widgets = require("./widgets"),
  8 +
  9 + CACHE = {},
  10 + DEBUG = false,
  11 + ROOT = "/",
  12 +
  13 + fromString, fromFile, createTemplate;
12 14
13 15 // Call this before using the templates
14   -exports.init = function(root, debug){
15   - DEBUG = debug
16   - ROOT = root;
17   -}
18   -
19   -function createTemplate( data, id ){
20   - var tokens;
21   - var template = {
22   - // Allows us to include templates from the compiled code
23   - fromFile: fromFile,
24   - // These are the blocks inside the template
25   - blocks: {},
26   - // Distinguish from other tokens
27   - type: parser.TEMPLATE,
28   - // Allows us to print debug info from the compiled code
29   - util: util,
30   - // The template ID (path relative to tempalte dir)
31   - id: id
32   - };
33   -
34   - // The template token tree before compiled into javascript
35   - template.tokens = parser.parse.call( template, data, tags );
36   -
37   - // The raw template code - can be inserted into other templates
38   - // We don't need this in production
39   - var code = parser.compile.call( template );
40   -
41   - if( DEBUG )
42   - template.code = code;
43   -
44   - // The compiled render function - this is all we need
45   - var render = new Function("__context", "__parents", "__widgets",
46   - [ '__parents = __parents ? __parents.slice() : [];'
47   - // Prevents circular includes (which will crash node without warning)
48   - , 'for(var i=0, j=__parents.length; i<j; ++i){'
49   - , ' if( __parents[i] === this.id ){'
50   - , ' return "Circular import of template " + this.id + " in " + __parents[__parents.length-1];'
51   - , ' }'
52   - , '}'
53   - // Add this template as a parent to all includes in its scope
54   - , '__parents.push( this.id );'
55   - , 'var __output = [];'
56   - , 'var __this = this;'
57   - , code
58   - , 'return __output.join("");'].join("\n")
59   - );
60   -
61   - template.render = function(context, parents){
62   - return render.call(this, context, parents, widgets);
63   - }
64   -
65   - return template;
66   -}
  16 +exports.init = function (root, debug) {
  17 + DEBUG = debug;
  18 + ROOT = root;
  19 +};
  20 +
  21 +createTemplate = function (data, id) {
  22 + var template = {
  23 + // Allows us to include templates from the compiled code
  24 + fromFile: fromFile,
  25 + // These are the blocks inside the template
  26 + blocks: {},
  27 + // Distinguish from other tokens
  28 + type: parser.TEMPLATE,
  29 + // Allows us to print debug info from the compiled code
  30 + util: util,
  31 + // The template ID (path relative to tempalte dir)
  32 + id: id
  33 + },
  34 + tokens,
  35 + code,
  36 + render;
  37 +
  38 + // The template token tree before compiled into javascript
  39 + template.tokens = parser.parse.call(template, data, tags);
  40 +
  41 + // The raw template code - can be inserted into other templates
  42 + // We don't need this in production
  43 + code = parser.compile.call(template);
  44 +
  45 + if (DEBUG) {
  46 + template.code = code;
  47 + }
  48 +
  49 + // The compiled render function - this is all we need
  50 + render = new Function("__context", "__parents", "__widgets",
  51 + [ '__parents = __parents ? __parents.slice() : [];'
  52 + // Prevents circular includes (which will crash node without warning)
  53 + , 'for (var i=0, j=__parents.length; i<j; ++i) {'
  54 + , ' if (__parents[i] === this.id) {'
  55 + , ' return "Circular import of template " + this.id + " in " + __parents[__parents.length-1];'
  56 + , ' }'
  57 + , '}'
  58 + // Add this template as a parent to all includes in its scope
  59 + , '__parents.push(this.id);'
  60 + , 'var __output = [];'
  61 + , 'var __this = this;'
  62 + , code
  63 + , 'return __output.join("");'].join("\n")
  64 + );
  65 +
  66 + template.render = function (context, parents) {
  67 + return render.call(this, context, parents, widgets);
  68 + };
  69 +
  70 + return template;
  71 +};
67 72
68 73 /*
69   - * Returns a template object from the given filepath.
70   - * The filepath needs to be relative to the template directory.
71   - */
72   -function fromFile( filepath ){
73   -
74   - if( filepath[0] == '/' )
75   - filepath = filepath.substr(1);
76   -
77   - if( filepath in CACHE && !DEBUG )
78   - return CACHE[filepath];
79   -
80   - var data = fs.readFileSync( ROOT + "/" + filepath, 'utf8' );
81   - // TODO: see what error readFileSync returns and warn about it
82   - if( data )
83   - return CACHE[filepath] = createTemplate(data, filepath);
84   -}
  74 +* Returns a template object from the given filepath.
  75 +* The filepath needs to be relative to the template directory.
  76 +*/
  77 +fromFile = function (filepath) {
  78 +
  79 + if (filepath[0] === '/') {
  80 + filepath = filepath.substr(1);
  81 + }
  82 +
  83 + if (filepath in CACHE && !DEBUG) {
  84 + return CACHE[filepath];
  85 + }
  86 +
  87 + var data = fs.readFileSync(ROOT + "/" + filepath, 'utf8');
  88 + // TODO: see what error readFileSync returns and warn about it
  89 + if (data) {
  90 + CACHE[filepath] = createTemplate(data, filepath);
  91 + return CACHE[filepath];
  92 + }
  93 +};
85 94
86 95 /*
87   - * Returns a template object from the given string.
88   - */
89   -function fromString( string ){
90   - var hash = crypto.createHash('md5').update(string).digest('hex');
91   -
92   - if( hash in CACHE && !DEBUG )
93   - return CACHE[hash];
  96 +* Returns a template object from the given string.
  97 +*/
  98 +fromString = function (string) {
  99 + var hash = crypto.createHash('md5').update(string).digest('hex');
94 100
95   - return CACHE[hash] = createTemplate(string, hash);
96   -}
  101 + if (!(hash in CACHE && !DEBUG)) {
  102 + CACHE[hash] = createTemplate(string, hash);
  103 + }
  104 +
  105 + return CACHE[hash];
  106 +};
97 107
98 108 module.exports = {
99 109 init: exports.init,
100   - fromFile: fromFile,
101   - fromString: fromString,
  110 + fromFile: fromFile,
  111 + fromString: fromString,
102 112 compile: function (source, options, callback) {
103   - self = this;
104   - if (typeof source == 'string') {
105   - return function(options) {
  113 + var self = this;
  114 + if (typeof source === 'string') {
  115 + return function (options) {
  116 + var tmpl = fromString(source);
  117 +
106 118 options.locals = options.locals || {};
107 119 options.partials = options.partials || {};
108   - if (options.body) // for express.js > v1.0
  120 +
  121 + if (options.body) { // for express.js > v1.0
109 122 options.locals.body = options.body;
110   - var tmpl = fromString(source);
  123 + }
  124 +
111 125 return tmpl.render(options.locals);
112 126 };
113 127 } else {
295 parser.js
... ... @@ -1,157 +1,162 @@
1   -var helpers = require('./helpers');
2   -
3   -var check = helpers.check;
4   -var escape = helpers.escape;
5   -
6   -var variableRegexp = /^\{\{.*?\}\}$/;
7   -var logicRegexp = /^\{%.*?%\}$/;
8   -var commentRegexp = /^\{#.*?#\}$/;
9   -
10   -var TEMPLATE = exports.TEMPLATE = 0;
11   -var LOGIC_TOKEN = 1;
12   -var VAR_TOKEN = 2;
13   -
14   -exports.parse = function(data, tags){
15   - var rawtokens = data.trim().replace( /(^\n+)|(\n+$)/, "" ).split( /(\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\})/ );
16   - var stack = [ [] ], index = 0, token, parts, names, matches, tagname;
17   -
18   - for( var i=0, j=rawtokens.length; i<j; ++i ) {
19   - token = rawtokens[i];
  1 +var helpers = require('./helpers'),
  2 +
  3 + check = helpers.check,
  4 + escape = helpers.escape,
  5 +
  6 + variableRegexp = /^\{\{.*?\}\}$/,
  7 + logicRegexp = /^\{%.*?%\}$/,
  8 + commentRegexp = /^\{#.*?#\}$/,
  9 +
  10 + TEMPLATE = exports.TEMPLATE = 0,
  11 + LOGIC_TOKEN = 1,
  12 + VAR_TOKEN = 2;
  13 +
  14 +exports.parse = function (data, tags) {
  15 + var rawtokens = data.trim().replace(/(^\n+)|(\n+$)/, "").split(/(\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\})/),
  16 + stack = [[]],
  17 + index = 0,
  18 + i = 0, j = rawtokens.length,
  19 + varname, token, parts, names, matches, tagname;
  20 +
  21 + for (i, j; i < j; i += 1) {
  22 + token = rawtokens[i];
  23 +
  24 + // Ignore empty strings and comments
  25 + if (token.length === 0 || commentRegexp.test(token)) {
  26 + continue;
  27 + } else if (/^(\s|\n)+$/.test(token)) {
  28 + token = token.replace(/ +/, " ").replace(/\n+/, "\n");
  29 + } else if (variableRegexp.test(token)) {
  30 + parts = token.replace(/^\{\{ *| *\}\}$/g, "").split(" ");
  31 + varname = parts.shift();
  32 +
  33 + token = {
  34 + type: VAR_TOKEN,
  35 + name: varname,
  36 + args: parts.length ? parts : []
  37 + };
  38 + } else if (logicRegexp.test(token)) {
  39 + parts = token.replace(/^\{% *| *%\}$/g, "").split(" ");
  40 + tagname = parts.shift();
  41 +
  42 + if (tagname === 'end') {
  43 + stack.pop();
  44 + index--;
  45 + continue;
  46 + }
  47 +
  48 + if (!(tagname in tags)) {
  49 + throw new Error("Unknown logic tag: " + tagname);
  50 + }
  51 +
  52 + token = {
  53 + type: LOGIC_TOKEN,
  54 + name: tagname,
  55 + args: parts.length ? parts : [],
  56 + compile: tags[tagname]
  57 + };
  58 +
  59 + if (tags[tagname].ends) {
  60 + stack[index].push(token);
  61 + stack.push(token.tokens = []);
  62 + index++;
  63 + continue;
  64 + }
  65 + }
20 66
21   - // Ignore empty strings and comments
22   - if( token.length === 0 || commentRegexp.test( token ) ) {
23   - continue;
24   - }
25   - else if( /^(\s|\n)+$/.test(token) ){
26   - token = token.replace( / +/, " " ).replace( /\n+/, "\n" );
  67 + // Everything else is treated as a string
  68 + stack[index].push(token);
27 69 }
28   - else if( variableRegexp.test(token) ){
29   - parts = token.replace(/^\{\{ *| *\}\}$/g, "").split(" ");
30   - var varname = parts.shift();
31   -
32   - token = {
33   - type: VAR_TOKEN,
34   - name: varname,
35   - args: parts.length ? parts : []
36   - };
  70 +
  71 + if (index !== 0) {
  72 + throw new Error('Some tags have not been closed');
37 73 }
38 74
39   - else if( logicRegexp.test(token) ){
40   - parts = token.replace(/^\{% *| *%\}$/g, "").split(" ");
41   - tagname = parts.shift();
42   -
43   - if( tagname === 'end' ) {
44   - stack.pop();
45   - index--;
46   - continue;
47   - }
48   -
49   - if( !(tagname in tags) ) {
50   - throw new Error( "Unknown logic tag: " + tagname );
51   - }
52   -
53   - token = {
54   - type: LOGIC_TOKEN,
55   - name: tagname,
56   - args: parts.length ? parts : [],
57   - compile: tags[tagname]
58   - };
59   -
60   - if( tags[tagname].ends ) {
61   - stack[ index ].push( token );
62   - stack.push( token.tokens = [] );
63   - index++;
64   - continue;
65   - }
  75 + return stack[index];
  76 +};
  77 +
  78 +
  79 +exports.compile = function compile(indent) {
  80 + var code = [''],
  81 + tokens = [],
  82 + parent, filepath, blockname;
  83 +
  84 + indent = indent || '';
  85 +
  86 + // Precompile - extract blocks and create hierarchy based on 'extends' tags
  87 + // TODO: make block and extends tags accept context variables
  88 + if (this.type === TEMPLATE) {
  89 + this.tokens.forEach(function (token, index) {
  90 + // TODO: Check for circular extends
  91 + // Load the parent template
  92 + if (token.name === 'extends') {
  93 + filepath = token.args[0];
  94 + if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
  95 + throw new Error("Extends tag accepts exactly one strings literal as an argument.");
  96 + }
  97 + if (index > 0) {
  98 + throw new Error("Extends tag must be the first tag in the template.");
  99 + }
  100 + token.template = this.fromFile(filepath.replace(/['"]/g, ''));
  101 + } else if (token.name === 'block') { // Make a list of blocks
  102 + blockname = token.args[0];
  103 + if (!helpers.isValidBlockName(blockname) || token.args.length > 1) {
  104 + throw new Error("Invalid block tag syntax.");
  105 + }
  106 + if (this.type !== TEMPLATE) {
  107 + throw new Error("Block tag found inside another tag.");
  108 + }
  109 + this.blocks[blockname] = compile.call(token, indent + ' ');
  110 + }
  111 + tokens.push(token);
  112 + }, this);
  113 +
  114 + if (tokens[0].name === 'extends') {
  115 + parent = tokens[0].template;
  116 + this.blocks = helpers.merge(parent.blocks, this.blocks);
  117 + this.tokens = parent.tokens;
  118 + }
66 119 }
67 120
68   - // Everything else is treated as a string
69   - stack[ index ].push( token );
70   - }
71   -
72   - if( index !== 0 ) {
73   - throw new Error('Some tags have not been closed');
74   - }
75   -
76   - return stack[index];
77   -}
78   -
79   -
80   -exports.compile = function compile( indent ){
81   - var code = [''], tokens = [], indent = indent || '';
82   - // Precompile - extract blocks and create hierarchy based on 'extends' tags
83   - // TODO: make block and extends tags accept context variables
84   - if( this.type === TEMPLATE ){
85   - this.tokens.forEach(function(token, index){
86   - // TODO: Check for circular extends
87   - // Load the parent template
88   - if( token.name === 'extends' ) {
89   - var filepath = token.args[0];
90   - if( !helpers.isStringLiteral( filepath ) || token.args.length > 1 ){
91   - throw new Error("Extends tag accepts exactly one strings literal as an argument.");
  121 + // If this is not a template then just iterate through its tokens
  122 + this.tokens.forEach(function (token, index) {
  123 + if (typeof token === 'string') {
  124 + return code.push('__output.push("' + token.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '");');
92 125 }
93   - if( index > 0 ){
94   - throw new Error("Extends tag must be the first tag in the template.");
  126 +
  127 + if (typeof token !== 'object') {
  128 + return; // Tokens can be either strings or objects
95 129 }
96   - token.template = this.fromFile( filepath.replace(/['"]/g, '') );
97   - }
98   - // Make a list of blocks
99   - else if( token.name === 'block' ) {
100   - var blockname = token.args[0];
101   - if( !helpers.isValidBlockName( blockname ) || token.args.length > 1 ){
102   - throw new Error("Invalid block tag syntax.");
  130 +
  131 + if (token.type === VAR_TOKEN) {
  132 + return code.push(
  133 + 'if (' + check(token.name) + ') {'
  134 + , ' __output.push(' + escape(token.name) + ');'
  135 + , '} else if (' + check(token.name, '__context') + ') {'
  136 + , ' __output.push(' + escape(token.name, '__context') + ');'
  137 + , '}'
  138 + );
103 139 }
104   - if( this.type !== TEMPLATE ){
105   - throw new Error("Block tag found inside another tag.");
  140 +
  141 + if (token.type !== LOGIC_TOKEN) {
  142 + return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
106 143 }
107   - this.blocks[ blockname ] = compile.call( token, indent + ' ' );
108   - }
109   - tokens.push(token);
110   - }, this);
111 144
112   - if( tokens[0].name === 'extends' ){
113   - var parent = tokens[0].template;
114   - this.blocks = helpers.merge( parent.blocks, this.blocks );
115   - this.tokens = parent.tokens;
116   - }
117   - }
118   -
119   - // If this is not a template then just iterate through its tokens
120   - this.tokens.forEach(function(token, index) {
121   - if( typeof token === 'string' )
122   - return code.push( '__output.push("' + token.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '");' );
123   -
124   - if( typeof token !== 'object')
125   - return; // Tokens can be either strings or objects
126   -
127   - if( token.type === VAR_TOKEN )
128   - return code.push(
129   - 'if( ' + check(token.name) + ' ){'
130   - , ' __output.push(' + escape( token.name ) + ');'
131   - , '} else if( ' + check( token.name, '__context' ) + ' ){'
132   - , ' __output.push(' + escape( token.name, '__context' ) + ');'
133   - , '}'
134   - );
135   -
136   - if( token.type !== LOGIC_TOKEN )
137   - return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
138   -
139   - if( token.name === 'extends' ) {
140   - if( this.type !== TEMPLATE )
141   - throw new Error("Extends tag must be the first tag in the template.");
142   - }
143   -
144   - else if( token.name === 'block' ) {
145   - if( this.type !== TEMPLATE )
146   - throw new Error("You can not nest block tags into other tags.");
  145 + if (token.name === 'extends') {
  146 + if (this.type !== TEMPLATE) {
  147 + throw new Error("Extends tag must be the first tag in the template.");
  148 + }
  149 + } else if (token.name === 'block') {
  150 + if (this.type !== TEMPLATE) {
  151 + throw new Error("You can not nest block tags into other tags.");
  152 + }
  153 +
  154 + code.push(this.blocks[token.args[0]]); // Blocks are already compiled in the precompile part
  155 + } else {
  156 + code.push(token.compile(indent + ' '));
  157 + }
147 158
148   - code.push( this.blocks[ token.args[0] ] ); // Blocks are already compiled in the precompile part
149   - }
150   -
151   - else
152   - code.push( token.compile( indent + ' ' ) );
153   -
154   - }, this);
155   -
156   - return code.join("\n" + indent);
157   -}
  159 + }, this);
  160 +
  161 + return code.join("\n" + indent);
  162 +};
4 scripts/config-lint.js
@@ -27,11 +27,11 @@ var options = {
27 27 onevar: true,
28 28 passfail: false,
29 29 plusplus: false,
30   - predef: ['util', 'require', 'process', 'exports', 'escape', '__dirname', 'setTimeout'],
  30 + predef: ['module', 'util', 'require', 'process', 'exports', 'escape', '__dirname', 'setTimeout'],
31 31 regexp: false,
32 32 rhino: false,
33 33 safe: false,
34   - strict: true,
  34 + strict: false,
35 35 sub: false,
36 36 undef: true,
37 37 white: true,
2  scripts/runlint.js
@@ -4,7 +4,7 @@ var util = require('util'),
4 4 child_process = require('child_process'),
5 5 configFile = __dirname + '/config-lint',
6 6 ignore = '',
7   - root, i;
  7 + config, root, i;
8 8
9 9 process.argv.forEach(function (val, index, array) {
10 10 if (index < 2) {
437 tags.js
@@ -6,234 +6,241 @@ var escape = helpers.escape;
6 6 var compile = parser.compile;
7 7
8 8 /**
9   - * Inheritance inspired by Django templates
10   - * The 'extends' and 'block' logic is hardwired in parser.compile
11   - * These are dummy tags.
12   - */
13   -exports['extends'] = {};
14   -exports['block'] = { ends: true };
  9 +* Inheritance inspired by Django templates
  10 +* The 'extends' and 'block' logic is hardwired in parser.compile
  11 +* These are dummy tags.
  12 +*/
  13 +exports.extends = {};
  14 +exports.block = { ends: true };
15 15
16 16 /**
17   - * TODO: This tag is tightly coupled with the context stricture of a specific project.
18   - * It is not part of the Django template specification.
19   - * Example slot data structure
20   - * slots: {
21   - * main_content: [
22   - *
23   - * { tagname: 'h1',
24   - * style: 'width:200px',
25   - * class: 'wm-page-element',
26   - * content: 'This is a heading <a href="http://example.com">with a link</a>'},
27   - *
28   - * "<p>This is a paragraph as a normal string.</p>",
29   - *
30   - * "<p>Normal strings get echoed into the template directly.</p>",
31   - *
32   - * { tagname: 'p',
33   - * style: '',
34   - * class: 'wm-page-element',
35   - * content: 'This is some text.'}],
36   - *
37   - * sidebar_content: [
38   - * { tagname: 'image',
39   - * style: '',
40   - * class: '',
41   - * content: '<img src="/static/uploads/image.jpg" title="Image Jpeg">'}]
42   - * }
43   -*/
44   -exports['slot'] = function( indent ) {
45   - var slot = this.args[0];
46   - var indent = indent || "";
47   -
48   - return ['(function(){'
49   - , ' if( ' + check( slot, '__context.slots' ) + ' ){'
50   - , ' var __widget, __slot = ' + escape( slot, '__context.slots' ) + '.content || [];'
51   - , ' for( var __i=0, __j = (+__slot.length) || 0; __i < __j; ++__i ){'
52   - , ' __widget = __slot[__i];'
53   - , ' if( __widget === undefined || __widget === null || __widget === false )'
54   - , ' continue;'
55   - , ' if( typeof __widget === "string" )'
56   - , ' __output.push(__widget)'
57   - , ' else if( __widget.tagname && __widgets && typeof __widgets[__widget.tagname] === "function" )'
58   - , ' __output.push( __widgets[__widget.tagname].call( __widget, __context, __parents ) );'
59   - , ' }'
60   - , ' }'
61   - , '})();'].join("\n" + indent);
62   -}
  17 +* TODO: This tag is tightly coupled with the context stricture of a specific project.
  18 +* It is not part of the Django template specification.
  19 +* Example slot data structure
  20 +* slots: {
  21 +* main_content: [
  22 +*
  23 +* { tagname: 'h1',
  24 +* style: 'width:200px',
  25 +* class: 'wm-page-element',
  26 +* content: 'This is a heading <a href="http://example.com">with a link</a>'},
  27 +*
  28 +* "<p>This is a paragraph as a normal string.</p>",
  29 +*
  30 +* "<p>Normal strings get echoed into the template directly.</p>",
  31 +*
  32 +* { tagname: 'p',
  33 +* style: '',
  34 +* class: 'wm-page-element',
  35 +* content: 'This is some text.'}],
  36 +*
  37 +* sidebar_content: [
  38 +* { tagname: 'image',
  39 +* style: '',
  40 +* class: '',
  41 +* content: '<img src="/static/uploads/image.jpg" title="Image Jpeg">'}]
  42 +* }
  43 +*/
  44 +exports.slot = function (indent) {
  45 + var slot = this.args[0];
  46 +
  47 + indent = indent || "";
  48 +
  49 + return ['(function () {'
  50 + , ' if (' + check(slot, '__context.slots') + ') {'
  51 + , ' var __widget, __slot = ' + escape(slot, '__context.slots') + '.content || [];'
  52 + , ' for (var __i=0, __j = (+__slot.length) || 0; __i < __j; ++__i) {'
  53 + , ' __widget = __slot[__i];'
  54 + , ' if (__widget === undefined || __widget === null || __widget === false)'
  55 + , ' continue;'
  56 + , ' if (typeof __widget === "string")'
  57 + , ' __output.push(__widget)'
  58 + , ' else if (__widget.tagname && __widgets && typeof __widgets[__widget.tagname] === "function")'
  59 + , ' __output.push(__widgets[__widget.tagname].call(__widget, __context, __parents));'
  60 + , ' }'
  61 + , ' }'
  62 + , '})();'].join("\n" + indent);
  63 +};
63 64
64 65 /**
65   - * Includes another template. The included template will have access to the
66   - * context, but won't have access to the variables defined in the parent template,
67   - * like for loop counters.
68   - *
69   - * Usage:
70   - * {% include context_variable %}
71   - * or
72   - * {% include "template_name.html" %}
73   - */
74   -exports['include'] = function( indent ) {
75   - var template = this.args[0];
76   - var indent = indent || "";
77   -
78   - if( !helpers.isLiteral( template ) && !helpers.isValidName( template ) ){
79   - throw new Error("Invalid arguments passed to 'include' tag.");
80   - }
81   -
82   - // Circular includes are VERBOTTEN. This will crash the server.
83   - return ['(function(){'
84   - , ' if( ' + check( template ) + ' ){'
85   - , ' var __template = ' + escape( template ) + ";"
86   - , ' }'
87   - , ' else if( ' + check( template, '__context' ) + ' ){'
88   - , ' var __template = ' + escape( template, '__context' ) + ";"
89   - , ' }'
90   - , ' if( typeof __template === "string"){'
91   - , ' __output.push( __this.fromFile( __template ).render( __context, __parents ) );'
92   - , ' }'
93   - , ' else if( typeof __template === "object" && __template.render ){'
94   - , ' __output.push( __template.render( __context, __parents ) );'
95   - , ' }'
96   - , '})();'].join("\n" + indent);
97   -}
  66 +* Includes another template. The included template will have access to the
  67 +* context, but won't have access to the variables defined in the parent template,
  68 +* like for loop counters.
  69 +*
  70 +* Usage:
  71 +* {% include context_variable %}
  72 +* or
  73 +* {% include "template_name.html" %}
  74 +*/
  75 +exports.include = function (indent) {
  76 + var template = this.args[0];
  77 +
  78 + indent = indent || "";
  79 +
  80 + if (!helpers.isLiteral(template) && !helpers.isValidName(template)) {
  81 + throw new Error("Invalid arguments passed to 'include' tag.");
  82 + }
  83 +
  84 + // Circular includes are VERBOTTEN. This will crash the server.
  85 + return ['(function () {'
  86 + , ' if (' + check(template) + ') {'
  87 + , ' var __template = ' + escape(template) + ";"
  88 + , ' }'
  89 + , ' else if (' + check(template, '__context') + ') {'
  90 + , ' var __template = ' + escape(template, '__context') + ";"
  91 + , ' }'
  92 + , ' if (typeof __template === "string") {'
  93 + , ' __output.push(__this.fromFile(__template).render(__context, __parents));'
  94 + , ' }'
  95 + , ' else if (typeof __template === "object" && __template.render) {'
  96 + , ' __output.push(__template.render(__context, __parents));'
  97 + , ' }'
  98 + , '})();'].join("\n" + indent);
  99 +};
98 100
99 101
100 102 /**
101   - * This is the 'if' tag compiler
102   - * Example 'If' tag syntax:
103   - * {% if x %}
104   - * <p>{{x}}</p>
105   - * {% end %}
106   - *
107   - * {% if !x %}
108   - * <p>No x found</p>
109   - * {% else %}
110   - * <p>{{x}}</p>
111   - * {% end %}
112   - *
113   - * {% if x == y %}, {% if x < y %}, {% if x in y %}, {% if x != y %}
114   - */
115   -exports['if'] = function( indent ){
116   - var operand1 = this.args[0];
117   - var operator = this.args[1];
118   - var operand2 = this.args[2];
119   - var indent = indent || "";
120   - var negation = false;
121   -
122   - // Check if there is negation
123   - if( operand1[0] === "!" ){
124   - negation = true;
125   - operand1 = operand1.substr(1);
126   - }
127   - // "!something == else" - this syntax is forbidden. Use "something != else" instead
128   - if( negation && operator ){
129   - throw new Error("Invalid syntax for 'if' tag");
130   - }
131   - // Check for valid argument
132   - if( !helpers.isLiteral( operand1 ) && !helpers.isValidName( operand1 ) )
133   - throw new Error( "Invalid arguments (" + operand1 + ") passed to 'if' tag" );
134   -
135   - // Check for valid operator
136   - if( operator && ["==", "<", ">", "!=", "<=", ">=", "===", "!==", "in"].indexOf(operator) === -1 ){
137   - throw new Error("Invalid operator (" + operator + ") passed to 'if' tag");
138   - }
139   - // Check for presence of operand 2 if operator is present
140   - if( operator && typeof operand2 === 'undefined' ){
141   - throw new Error("Missing argument in 'if' tag");
142   - }
143   - // Check for valid argument
144   -
145   - if( operator && !helpers.isLiteral( operand2 ) && !helpers.isValidName( operand2 ) ){
146   - throw new Error("Invalid arguments (" + operand2 + ") passed to 'if' tag");
147   - }
148   -
149   - var out = ['(function(){'];
150   - out.push(' var __op1;');
151   - out.push(' if( ' + check( operand1 ) + ' ){');
152   - out.push(' __op1 = ' + escape(operand1) + ';');
153   - out.push(' }');
154   - out.push(' else if( ' + check(operand1, '__context') + ' ){');
155   - out.push(' __op1 = ' + escape(operand1, '__context') + ';');
156   - out.push(' }');
157   - if( typeof operand2 === 'undefined' ){
158   - out.push(' if( ' + (negation ? '!' : '!!') + '__op1 ){');
159   - out.push( compile.call( this, indent + ' ' ) );
160   - out.push(' }');
161   - }
162   - else {
163   - out.push(' var __op2;');
164   - out.push(' if( ' + check(operand2) + ' ){');
165   - out.push(' __op2 = ' + escape(operand2) + ';');
  103 +* This is the 'if' tag compiler
  104 +* Example 'If' tag syntax:
  105 +* {% if x %}
  106 +* <p>{{x}}</p>
  107 +* {% end %}
  108 +*
  109 +* {% if !x %}
  110 +* <p>No x found</p>
  111 +* {% else %}
  112 +* <p>{{x}}</p>
  113 +* {% end %}
  114 +*
  115 +* {% if x == y %}, {% if x < y %}, {% if x in y %}, {% if x != y %}
  116 +*/
  117 +exports['if'] = function (indent) {
  118 + var operand1 = this.args[0],
  119 + operator = this.args[1],
  120 + operand2 = this.args[2],
  121 + negation = false,
  122 + out;
  123 +
  124 + indent = indent || "";
  125 +
  126 + // Check if there is negation
  127 + if (operand1[0] === "!") {
  128 + negation = true;
  129 + operand1 = operand1.substr(1);
  130 + }
  131 + // "!something == else" - this syntax is forbidden. Use "something != else" instead
  132 + if (negation && operator) {
  133 + throw new Error("Invalid syntax for 'if' tag");
  134 + }
  135 + // Check for valid argument
  136 + if (!helpers.isLiteral(operand1) && !helpers.isValidName(operand1)) {
  137 + throw new Error("Invalid arguments (" + operand1 + ") passed to 'if' tag");
  138 + }
  139 + // Check for valid operator
  140 + if (operator && ["==", "<", ">", "!=", "<=", ">=", "===", "!==", "in"].indexOf(operator) === -1) {
  141 + throw new Error("Invalid operator (" + operator + ") passed to 'if' tag");
  142 + }
  143 + // Check for presence of operand 2 if operator is present
  144 + if (operator && typeof operand2 === 'undefined') {
  145 + throw new Error("Missing argument in 'if' tag");
  146 + }
  147 + // Check for valid argument
  148 + if (operator && !helpers.isLiteral(operand2) && !helpers.isValidName(operand2)) {
  149 + throw new Error("Invalid arguments (" + operand2 + ") passed to 'if' tag");
  150 + }
  151 +
  152 + out = ['(function () {'];
  153 + out.push(' var __op1;');
  154 + out.push(' if (' + check(operand1) + ') {');
  155 + out.push(' __op1 = ' + escape(operand1) + ';');
166 156 out.push(' }');
167   - out.push(' else if( ' + check(operand2, '__context') + ' ){');
168   - out.push(' __op2 = ' + escape(operand2, '__context') + ';');
  157 + out.push(' else if (' + check(operand1, '__context') + ') {');
  158 + out.push(' __op1 = ' + escape(operand1, '__context') + ';');
169 159 out.push(' }');
170   -
171   - if( operator === 'in' ){
172   - out.push(' if((Array.isArray(__op2) && __op2.indexOf(__op1) > -1) ||');
173   - out.push(' (typeof __op2 === "string" && __op2.indexOf(__op1) > -1) ||');
174   - out.push(' (!Array.isArray(__op2) && typeof __op2 === "object" && __op1 in __op2)){');
  160 + if (typeof operand2 === 'undefined') {
  161 + out.push(' if (' + (negation ? '!' : '!!') + '__op1) {');
  162 + out.push(compile.call(this, indent + ' '));
  163 + out.push(' }');
175 164 }
176 165 else {
177   - out.push(' if( __op1 ' + escape(operator) + ' __op2 ){');
  166 + out.push(' var __op2;');
  167 + out.push(' if (' + check(operand2) + ') {');
  168 + out.push(' __op2 = ' + escape(operand2) + ';');
  169 + out.push(' }');
  170 + out.push(' else if (' + check(operand2, '__context') + ') {');
  171 + out.push(' __op2 = ' + escape(operand2, '__context') + ';');
  172 + out.push(' }');
  173 +
  174 + if (operator === 'in') {
  175 + out.push(' if ((Array.isArray(__op2) && __op2.indexOf(__op1) > -1) ||');
  176 + out.push(' (typeof __op2 === "string" && __op2.indexOf(__op1) > -1) ||');
  177 + out.push(' (!Array.isArray(__op2) && typeof __op2 === "object" && __op1 in __op2)) {');
  178 + }
  179 + else {
  180 + out.push(' if (__op1 ' + escape(operator) + ' __op2) {');
  181 + }
  182 + out.push(compile.call(this, indent + ' '));
  183 + out.push(' }');
178 184 }
179   - out.push( compile.call( this, indent + ' ' ) );
180   - out.push(' }');
181   - }
182   - out.push('})();');
183   - return out.join("\n" + indent);
184   -}
  185 + out.push('})();');
  186 + return out.join("\n" + indent);
  187 +};
185 188 exports['if'].ends = true;
186 189
187 190 /**
188   - * This is the 'for' tag compiler
189   - * Example 'For' tag syntax:
190   - * {% for x in y.some.items %}
191   - * <p>{{x}}</p>
192   - * {% end %}
193   - */
194   -exports['for'] = function( indent ){
195   - var operand1 = this.args[0];
196   - var operator = this.args[1];
197   - var operand2 = this.args[2];
198   - var indent = indent || "";
199   -
200   - if( typeof operator !== 'undefined' && operator !== 'in' )
201   - throw new Error("Invalid syntax in 'for' tag");
202   -
203   - if( !helpers.isValidShortName( operand1 ) )
204   - throw new Error("Invalid arguments (" + operand1 + ") passed to 'for' tag");
205   -
206   - if( !helpers.isValidName( operand2 ) )
207   - throw new Error("Invalid arguments (" + operand2 + ") passed to 'for' tag");
208   -
209   - return ['(function(){'
210   - , ' if( ' + check( operand2 ) + ' ){'
211   - , ' var __forloopIter = ' + escape( operand2 ) + ";"
212   - , ' }'
213   - , ' else if( ' + check( operand2, '__context' ) + ' ){'
214   - , ' var __forloopIter = ' + escape( operand2, '__context' ) + ";"
215   - , ' }'
216   - , ' else {'
217   - , ' return;'
218   - , ' }'
219   - , ' var ' + escape( operand1 ) + ';'
220   - , ' var forloop = {};'
221   - , ' if( Array.isArray(__forloopIter) ){'
222   - , ' var __forloopIndex, __forloopLength;'
223   - , ' for(var __forloopIndex=0, __forloopLength=__forloopIter.length; __forloopIndex<__forloopLength; ++__forloopIndex){'
224   - , ' forloop.index = __forloopIndex;'
225   - , ' ' + escape( operand1 ) + ' = __forloopIter[__forloopIndex];'
226   - , compile.call( this, indent + ' ' )
227   - , ' }'
228   - , ' }'
229   - , ' else if(typeof __forloopIter === "object"){'
230   - , ' var __forloopIndex;'
231   - , ' for(__forloopIndex in __forloopIter){'
232   - , ' forloop.index = __forloopIndex;'
233   - , ' ' + escape( operand1 ) + ' = __forloopIter[__forloopIndex];'
234   - , compile.call( this, indent + ' ' )
235   - , ' }'
236   - , ' }'
237   - , '})();'].join("\n" + indent);
238   -}
  191 +* This is the 'for' tag compiler
  192 +* Example 'For' tag syntax:
  193 +* {% for x in y.some.items %}
  194 +* <p>{{x}}</p>
  195 +* {% end %}
  196 +*/
  197 +exports['for'] = function (indent) {
  198 + var operand1 = this.args[0],
  199 + operator = this.args[1],
  200 + operand2 = this.args[2];
  201 +
  202 + indent = indent || "";
  203 +
  204 + if (typeof operator !== 'undefined' && operator !== 'in') {
  205 + throw new Error("Invalid syntax in 'for' tag");
  206 + }
  207 +
  208 + if (!helpers.isValidShortName(operand1)) {
  209 + throw new Error("Invalid arguments (" + operand1 + ") passed to 'for' tag");
  210 + }
  211 +
  212 + if (!helpers.isValidName(operand2)) {
  213 + throw new Error("Invalid arguments (" + operand2 + ") passed to 'for' tag");
  214 + }
  215 +
  216 + return ['(function () {'
  217 + , ' if (' + check(operand2) + ') {'
  218 + , ' var __forloopIter = ' + escape(operand2) + ";"
  219 + , ' }'
  220 + , ' else if (' + check(operand2, '__context') + ') {'
  221 + , ' var __forloopIter = ' + escape(operand2, '__context') + ";"
  222 + , ' }'
  223 + , ' else {'
  224 + , ' return;'
  225 + , ' }'
  226 + , ' var ' + escape(operand1) + ';'
  227 + , ' var forloop = {};'
  228 + , ' if (Array.isArray(__forloopIter)) {'
  229 + , ' var __forloopIndex, __forloopLength;'
  230 + , ' for (var __forloopIndex=0, __forloopLength=__forloopIter.length; __forloopIndex<__forloopLength; ++__forloopIndex) {'
  231 + , ' forloop.index = __forloopIndex;'