├── .gitignore
├── .npmignore
├── Makefile
├── README.md
├── index.js
├── lib
├── filters.js
├── helpers.js
├── parser.js
├── tags.js
└── widgets.js
├── package.json
├── scripts
├── config-lint.js
├── config-test.js
├── githooks
│ ├── post-merge
│ └── pre-commit
├── runlint.js
└── runtests.js
└── tests
├── filters.test.js
├── helpers.test.js
├── parser.test.js
├── speed.js
└── templates
├── extends_1.html
├── extends_2.html
├── extends_base.html
├── include_base.html
├── included.html
└── included_2.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules/*
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | tests/*
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | @npm install -d
3 | @cp scripts/githooks/* .git/hooks/
4 | @chmod -R +x .git/hooks/
5 |
6 | test:
7 | @node tests/speed.js
8 | @node scripts/runtests.js
9 |
10 | lint:
11 | @node scripts/runlint.js
12 |
13 | .PHONY: all test lint
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Update
2 |
3 | I suggest using Paul Armstrong's fork of Node-t as it covers more features and is actively developed:
4 |
5 | https://github.com/paularmstrong/swig
6 |
7 |
8 | # Node-T
9 |
10 | A fast django-like templating engine for node.js.
11 |
12 | Node-T is a templating engine inspired by the django syntax. It has a few extensions and the templates are compiled to native javascript functions which make them really fast. For now it's synchronous, but once a template is read and compiled, it is cached in memory.
13 |
14 | ### Example template code
15 |
16 |
17 |
18 | Example
19 | {% for name in names %}
20 |
21 | {{forloop.counter}}
22 | {# This is a comment #}
23 | {{name}}{% if name == "Django" %} Reinhardt{% end %}
24 |
25 | {% end %}
26 |
27 |
28 |
29 | ### Example node code
30 |
31 | var template = require('node-t');
32 | var tmpl = template.fromFile("/path/to/template.html");
33 | console.log(tmpl.render({names: ["Duke", "Django", "Louis"]}));
34 |
35 | ### How it works
36 |
37 | Node-T reads template files and translates them into javascript functions using the Function constructor. When we later render a template we call the evaled function passing a context object as an argument. This makes the rendering very fast. The template tags are defined as strings of Javascript code - which is a bit ugly, but there are helpers that will make writing tags easier for you.
38 |
39 | The slots system will allow you to define your own HTML snippets that will be rendered with a special context.
40 |
41 | ## The API
42 |
43 | You have 2 methods for creating a template object:
44 |
45 | var template = require('node-slots');
46 | template.fromFile("path/to/template/file.html");
47 | template.fromString("Template string here");
48 |
49 | Both of them will give you a template object on which you call the render method passing it a map of context values.
50 |
51 | var tmpl = template.fromFile("path/to/template/file.html");
52 | var renderdHtml = tmpl.render({});
53 |
54 | ## Template syntax
55 |
56 | You should be familiar with the [Django template syntax][1]. Here I'll just sum up the diferences:
57 |
58 | - There are no filters implemented yet
59 | - Tags like {% for %} and {% if %} are closed with a simple {% end %} tag
60 | - Not all tags are implemented
61 | - Some extra tags are implemented
62 | - Syntax for some tags is a bit different.
63 |
64 | Here's a list of currently implemented tags:
65 |
66 | ### Variable tags
67 |
68 | Used to print a variable to the template. If the variable is not in the context we don't get an error, rather an empty string. You can use dot notation to access object proerties or array memebers.
69 |
70 | First Name: {{users.0.first_name}}
71 |
72 | #### Variable Filters
73 |
74 | Used to modify variables. Filters are added directly after variable names, separated by the pipe (|) character. You can chain multiple filters together, applying one after the other in succession.
75 |
76 | {{ foo|reverse|join(' ')|title }}
77 |
78 | ##### default(default_value)
79 |
80 | If the variable is `undefined`, `null`, or `false`, a default return value can be specified.
81 |
82 | {{ foo|default('foo is not defined') }}
83 |
84 | ##### lower
85 |
86 | Return the variable in all lowercase letters.
87 |
88 | ##### upper
89 |
90 | Return the variable in all uppercase letters
91 |
92 | ##### capitalize
93 |
94 | Capitalize the first character in the string.
95 |
96 | ##### title
97 |
98 | Change the output to title case–the first letter of every word will uppercase, while all the rest will be lowercase.
99 |
100 | ##### join
101 |
102 | If the value is an Array, you can join each value with a delimiter and return it as a string.
103 |
104 | {{ authors|join(', ') }}
105 |
106 | ##### reverse
107 |
108 | If the value is an Array, this filter will reverse all items in the array.
109 |
110 | ##### length
111 |
112 | Return the `length` property of the value.
113 |
114 | ##### url_encode
115 |
116 | Encode a URI component.
117 |
118 | ##### url_decode
119 |
120 | Decode a URI component.
121 |
122 | ##### json_encode
123 |
124 | Return a JSON string of the variable.
125 |
126 | ##### striptags
127 |
128 | Strip all HTML/XML tags.
129 |
130 | ##### date
131 |
132 | Convert a valid date into a format as specified. Mostly conforms to (php.net's date formatting)[http://php.net/date].
133 |
134 | {{ post.created|date('F jS, Y') }}
135 |
136 | ### Comment tags
137 |
138 | Comment tags are simply ignored. Comments can't span multitple lines.
139 |
140 | {# This is a comment #}
141 |
142 | ### Logic tags
143 |
144 | #### extends / block
145 |
146 | Check django's template inheritance system for more info. Unlike django, the block tags are terminated with {% end %}, not with {% endblock %}
147 |
148 | #### include
149 |
150 | Includes a template in it's place. The template is rendered within the current context. Does not requre closing with {% end %}.
151 |
152 | {% include template_path %}
153 | {% include "path/to/template.js" %}
154 |
155 | #### for
156 |
157 | You can iterate arrays and objects. Access the current iteration index through 'forloop.index' which is available inside the loop.
158 |
159 | {% for x in y %}
160 | {% forloop.index %}
161 | {% end %}
162 |
163 | #### if
164 |
165 | Supports the following expressions. No else tag yet.
166 |
167 | {% if x %}{% end %}
168 | {% if !x %}{% end %}
169 | {% if x operator y %}
170 | Operators: ==, !=, <, <=, >, >=, ===, !==, in
171 | The 'in' operator checks for presence in arrays too.
172 | {% end %}
173 | {% if x == 'five' %}
174 | The operands can be also be string or number literals
175 | {% end %}
176 |
177 | #### slot
178 |
179 | Use slots where you want highly customized content that depends heavily on dynamic data. They work in pair with widget functions that you can write yourself.
180 |
181 | Template code
182 |
183 |
184 | {% slot main %}
185 |
186 |
187 | {% slot sidebar %}
188 |
189 |
190 | Node.js code
191 |
192 | context.slots = {
193 | main: [
194 | "This is a paragraph as a normal string.
", // String
195 |
196 | { tagname: 'analytics', // Widget object
197 | uaCode: 'UA-XXXXX-X' },
198 | ],
199 |
200 | sidebar: [
201 | "Navigation
", // String
202 |
203 | { tagname: 'navigation', // Widget object
204 | links: [
205 | '/home',
206 | '/about',
207 | '/kittens'
208 | ]}
209 | ]
210 | }
211 |
212 | context.widgets = {
213 | analytics: function (context) {
214 | // this inside widget functions is bound to the widget object
215 | return "";
216 | },
217 | navigation: function (context) {
218 | var i, html = "";
219 | for (i=0; i" + links[i] + "";
221 | return html;
222 | }
223 | }
224 |
225 | template.render(context)
226 |
227 | Result
228 |
229 |
230 |
This is a paragraph as a normal string.
231 |
232 |
233 |
239 |
240 |
241 | ## License
242 |
243 | (The MIT License)
244 |
245 | Copyright (c) 2010 Dusko Jordanovski
246 |
247 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
248 |
249 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
250 |
251 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
252 |
253 | [1]: http://djangoproject.com/
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require.paths.unshift(__dirname + '/lib');
2 |
3 | var fs = require("fs"),
4 | util = require("util"),
5 | path = require("path"),
6 | crypto = require("crypto"),
7 |
8 | tags = require("tags"),
9 | parser = require("parser"),
10 | widgets = require("widgets"),
11 | filters = require('filters'),
12 |
13 | CACHE = {},
14 | DEBUG = false,
15 | ROOT = "./",
16 |
17 | fromString, fromFile, createTemplate;
18 |
19 | // Call this before using the templates
20 | exports.init = function (root, debug) {
21 | DEBUG = debug;
22 | ROOT = root;
23 | };
24 |
25 | createTemplate = function (data, id) {
26 | var template = {
27 | // Allows us to include templates from the compiled code
28 | fromFile: fromFile,
29 | // These are the blocks inside the template
30 | blocks: {},
31 | // Distinguish from other tokens
32 | type: parser.TEMPLATE,
33 | // Allows us to print debug info from the compiled code
34 | util: util,
35 | // The template ID (path relative to tempalte dir)
36 | id: id
37 | },
38 | tokens,
39 | code,
40 | render;
41 |
42 | // The template token tree before compiled into javascript
43 | template.tokens = parser.parse.call(template, data, tags);
44 |
45 | // The raw template code - can be inserted into other templates
46 | // We don't need this in production
47 | code = parser.compile.call(template);
48 |
49 | if (DEBUG) {
50 | template.code = code;
51 | }
52 |
53 | // The compiled render function - this is all we need
54 | render = new Function("__context", "__parents", "__filters", "__widgets",
55 | [ '__parents = __parents ? __parents.slice() : [];'
56 | // Prevents circular includes (which will crash node without warning)
57 | , 'for (var i=0, j=__parents.length; i v1.0
126 | options.locals.body = options.body;
127 | }
128 |
129 | return tmpl.render(options.locals);
130 | };
131 | } else {
132 | return source;
133 | }
134 | },
135 | render: function (template, options) {
136 | template = this.compile(template, options);
137 | return template(options);
138 | }
139 | };
--------------------------------------------------------------------------------
/lib/filters.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | escape = helpers.escape,
3 | _dateFormats;
4 |
5 | _dateFormats = {
6 | _months: {
7 | full: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
8 | abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
9 | },
10 | _days: {
11 | full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
12 | abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
13 | alt: {'-1': 'Yesterday', 0: 'Today', 1: 'Tomorrow'}
14 | },
15 | // Day
16 | d: function (input) {
17 | return (input.getDate() < 10 ? '0' : '') + input.getDate();
18 | },
19 | D: function (input) {
20 | return _dateFormats._days.abbr[input.getDay()];
21 | },
22 | j: function (input) {
23 | return input.getDate();
24 | },
25 | l: function (input) {
26 | return _dateFormats._days.full[input.getDay()];
27 | },
28 | N: function (input) {
29 | return input.getDay();
30 | },
31 | S: function (input) {
32 | return (input.getDate() % 10 === 1 && input.getDate() !== 11 ? 'st' : (input.getDate() % 10 === 2 && input.getDate() !== 12 ? 'nd' : (input.getDate() % 10 === 3 && input.getDate() !== 13 ? 'rd' : 'th')));
33 | },
34 | w: function (input) {
35 | return input.getDay() - 1;
36 | },
37 | //z = function (input) { return ''; },
38 |
39 | // Week
40 | //W = function (input) { return ''; },
41 |
42 | // Month
43 | F: function (input) {
44 | return _dateFormats._months.full[input.getMonth()];
45 | },
46 | m: function (input) {
47 | return (input.getMonth() < 8 ? '0' : '') + (input.getMonth() + 1);
48 | },
49 | M: function (input) {
50 | return _dateFormats._months.abbr[input.getMonth()];
51 | },
52 | n: function (input) {
53 | return input.getMonth() + 1;
54 | },
55 | //t = function (input) { return ''; },
56 |
57 | // Year
58 | //L = function (input) { return ''; },
59 | //o = function (input) { return ''; },
60 | Y: function (input) {
61 | return input.getFullYear();
62 | },
63 | y: function (input) {
64 | return ('' + input.getFullYear()).substr(2);
65 | },
66 |
67 | // Time
68 | a: function (input) {
69 | return input.getHours() < 12 ? 'am' : 'pm';
70 | },
71 | A: function (input) {
72 | return input.getHours() < 12 ? 'AM' : 'PM';
73 | },
74 | //B = function () { return ''; },
75 | g: function (input) {
76 | return input.getHours() === 0 ? 12 : (input.getHours() > 12 ? input.getHours() - 12 : input.getHours());
77 | },
78 | G: function (input) {
79 | return input.getHours();
80 | },
81 | h: function (input) {
82 | return (input.getHours() < 10 || (12 < input.getHours() < 22) ? '0' : '') + (input.getHours() < 10 ? input.getHours() : input.getHours() - 12);
83 | },
84 | H: function (input) {
85 | return (input.getHours() < 10 ? '0' : '') + input.getHours();
86 | },
87 | i: function (input) {
88 | return (input.getMinutes() < 10 ? '0' : '') + input.getMinutes();
89 | },
90 | s: function (input) {
91 | return (input.getSeconds() < 10 ? '0' : '') + input.getSeconds();
92 | },
93 | //u = function () { return ''; },
94 |
95 | // Timezone
96 | //e = function () { return ''; },
97 | //I = function () { return ''; },
98 | O: function (input) {
99 | return (input.getTimezoneOffset() < 0 ? '-' : '+') + (input.getTimezoneOffset() / 60 < 10 ? '0' : '') + (input.getTimezoneOffset() / 60) + '00';
100 | },
101 | //T = function () { return ''; },
102 | Z: function (input) {
103 | return input.getTimezoneOffset() * 60;
104 | },
105 |
106 | // Full Date/Time
107 | //c = function () { return ''; },
108 | r: function (input) {
109 | return input.toString();
110 | },
111 | U: function (input) {
112 | return input.getTime() / 1000;
113 | }
114 | };
115 |
116 | exports.default = function (input, def) {
117 | return (input || typeof input === 'number') ? input : def;
118 | };
119 |
120 | exports.lower = function (input) {
121 | return input.toString().toLowerCase();
122 | };
123 |
124 | exports.upper = function (input) {
125 | return input.toString().toUpperCase();
126 | };
127 |
128 | exports.capitalize = function (input) {
129 | return input.toString().charAt(0).toUpperCase() + input.toString().substr(1).toLowerCase();
130 | };
131 |
132 | exports.title = function (input) {
133 | return input.toString().replace(/\w\S*/g, function (str) {
134 | return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
135 | });
136 | };
137 |
138 | exports.join = function (input, separator) {
139 | if (Array.isArray(input)) {
140 | return input.join(separator);
141 | } else {
142 | return input;
143 | }
144 | };
145 |
146 | exports.reverse = function (input) {
147 | if (Array.isArray(input)) {
148 | return input.reverse();
149 | } else {
150 | return input;
151 | }
152 | };
153 |
154 | exports.length = function (input) {
155 | return input.length;
156 | };
157 |
158 | exports.url_encode = function (input) {
159 | return encodeURIComponent(input);
160 | };
161 |
162 | exports.url_decode = function (input) {
163 | return decodeURIComponent(input);
164 | };
165 |
166 | exports.json_encode = function (input) {
167 | return JSON.stringify(input);
168 | };
169 |
170 | exports.striptags = function (input) {
171 | return input.toString().replace(/(<([^>]+)>)/ig, "");
172 | };
173 |
174 | exports.date = function (input, format) {
175 | var l = format.length,
176 | date = new Date(input),
177 | cur, i = 0,
178 | out = '';
179 |
180 | for (i; i < l; i += 1) {
181 | cur = format.charAt(i);
182 | if (_dateFormats.hasOwnProperty(cur)) {
183 | out += _dateFormats[cur](date);
184 | } else {
185 | out += cur;
186 | }
187 | }
188 | return out;
189 | };
190 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | // Checks if the string is a number literal
2 | var NUMBER_LITERAL = /^\d+([.]\d+)?$/;
3 | // Checks if there are unescaped single quotes (the string needs to be reversed first)
4 | var UNESCAPED_QUOTE = /'(?!\\)/;
5 | // Checks if there are unescaped double quotes (the string needs to be reversed first)
6 | var UNESCAPED_DQUOTE = /"(?!\\)/;
7 | // Valid Javascript name: 'name' or 'property.accessor.chain'
8 | var VALID_NAME = /^([$A-Za-z_]+[$A-Za-z_0-9]*)(\.?([$A-Za-z_]+[$A-Za-z_0-9]*))*$/;
9 | // Valid Javascript name: 'name'
10 | var VALID_SHORT_NAME = /^[$A-Za-z_]+[$A-Za-z_0-9]*$/;
11 | // Javascript keywords can't be a name: 'for.is_invalid' as well as 'for' but not 'for_' or '_for'
12 | var KEYWORDS = /^(Array|RegExpt|Object|String|Number|Math|Error|break|continue|do|for|new|case|default|else|function|in|return|typeof|while|delete|if|switch|var|with)(?=(\.|$))/;
13 | // Valid block name
14 | var VALID_BLOCK_NAME = /^[A-Za-z]+[A-Za-z_0-9]*$/;
15 |
16 | // Returns TRUE if the passed string is a valid javascript number or string literal
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;
33 | }
34 |
35 | return literal;
36 | }
37 |
38 | // Returns TRUE if the passed string is a valid javascript string literal
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;
49 | }
50 |
51 | return false;
52 | }
53 |
54 | // Variable names starting with __ are reserved.
55 | function isValidName(string) {
56 | return VALID_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0, 2) !== "__";
57 | }
58 |
59 | // Variable names starting with __ are reserved.
60 | function isValidShortName(string) {
61 | return VALID_SHORT_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0, 2) !== "__";
62 | }
63 |
64 | // Checks if a name is a vlaid block name
65 | function isValidBlockName(string) {
66 | return VALID_BLOCK_NAME.test(string);
67 | }
68 |
69 | /**
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 | };
101 |
102 | /**
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 | };
127 |
128 | exports.clone = function (obj) {
129 | var clone = {},
130 | key;
131 | for (key in obj) {
132 | if (obj.hasOwnProperty(key)) {
133 | if (typeof(obj[key]) === "object") {
134 | clone[key] = exports.clone(obj[key]);
135 | } else {
136 | clone[key] = obj[key];
137 | }
138 | }
139 | }
140 | return clone;
141 | };
142 |
143 | /**
144 | * Merges b into a and returns a
145 | */
146 | exports.merge = function (a, b) {
147 | var key, temp = null;
148 |
149 | if (a && b) {
150 | temp = exports.clone(a);
151 | for (key in b) {
152 | if (b.hasOwnProperty(key)) {
153 | temp[key] = b[key];
154 | }
155 | }
156 | }
157 |
158 | return temp;
159 | };
160 |
161 | exports.isLiteral = isLiteral;
162 | exports.isValidName = isValidName;
163 | exports.isValidShortName = isValidShortName;
164 | exports.isValidBlockName = isValidBlockName;
165 | exports.isStringLiteral = isStringLiteral;
--------------------------------------------------------------------------------
/lib/parser.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers'),
2 | filters = require('./filters'),
3 |
4 | check = helpers.check,
5 |
6 | variableRegexp = /^\{\{.*?\}\}$/,
7 | logicRegexp = /^\{%.*?%\}$/,
8 | commentRegexp = /^\{#.*?#\}$/,
9 |
10 | TEMPLATE = exports.TEMPLATE = 0,
11 | LOGIC_TOKEN = 1,
12 | VAR_TOKEN = 2;
13 |
14 | exports.TOKEN_TYPES = {
15 | TEMPLATE: TEMPLATE,
16 | LOGIC: LOGIC_TOKEN,
17 | VAR: VAR_TOKEN
18 | };
19 |
20 | exports.parse = function (data, tags) {
21 | var rawtokens = data.trim().replace(/(^\n+)|(\n+$)/, "").split(/(\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\})/),
22 | stack = [[]],
23 | index = 0,
24 | i = 0, j = rawtokens.length,
25 | filters = [], filter_name,
26 | varname, token, parts, part, names, matches, tagname;
27 |
28 | for (i, j; i < j; i += 1) {
29 | token = rawtokens[i];
30 |
31 | // Ignore empty strings and comments
32 | if (token.length === 0 || commentRegexp.test(token)) {
33 | continue;
34 | } else if (/^(\s|\n)+$/.test(token)) {
35 | token = token.replace(/ +/, " ").replace(/\n+/, "\n");
36 | } else if (variableRegexp.test(token)) {
37 | filters = [];
38 | parts = token.replace(/^\{\{ *| *\}\}$/g, '').split('|');
39 | varname = parts.shift();
40 |
41 | for (part in parts) {
42 | if (parts.hasOwnProperty(part)) {
43 | part = parts[part];
44 | filter_name = part.match(/^\w+/);
45 | if (/\(/.test(part)) {
46 | filters.push({ name: filter_name[0], args: part.replace(/^\w+\(|\'|\"|\)$/g, '').split(',') });
47 | } else {
48 | filters.push({ name: filter_name[0], args: [] });
49 | }
50 | }
51 | }
52 |
53 | token = {
54 | type: VAR_TOKEN,
55 | name: varname,
56 | filters: filters
57 | };
58 | } else if (logicRegexp.test(token)) {
59 | parts = token.replace(/^\{% *| *%\}$/g, "").split(" ");
60 | tagname = parts.shift();
61 |
62 | if (tagname === 'end') {
63 | stack.pop();
64 | index--;
65 | continue;
66 | }
67 |
68 | if (!(tagname in tags)) {
69 | throw new Error("Unknown logic tag: " + tagname);
70 | }
71 |
72 | token = {
73 | type: LOGIC_TOKEN,
74 | name: tagname,
75 | args: parts.length ? parts : [],
76 | compile: tags[tagname]
77 | };
78 |
79 | if (tags[tagname].ends) {
80 | stack[index].push(token);
81 | stack.push(token.tokens = []);
82 | index++;
83 | continue;
84 | }
85 | }
86 |
87 | // Everything else is treated as a string
88 | stack[index].push(token);
89 | }
90 |
91 | if (index !== 0) {
92 | throw new Error('Some tags have not been closed');
93 | }
94 |
95 | return stack[index];
96 | };
97 |
98 | function wrapFilter(variable, filter) {
99 | var output = variable,
100 | args;
101 |
102 | if (filters.hasOwnProperty(filter.name)) {
103 | args = [variable];
104 | filter.args.forEach(function (f) {
105 | args.push('\'' + f + '\'');
106 | });
107 | output = '__filters.' + filter.name + '.apply(this, [' + args.toString() + '])';
108 | }
109 |
110 | return output;
111 | }
112 |
113 | function wrapFilters(variable, filters, context) {
114 | var output = helpers.escape(variable, context);
115 |
116 | if (filters && filters.length > 0) {
117 | filters.forEach(function (filter) {
118 | output = wrapFilter(output, filter);
119 | });
120 | }
121 |
122 | return output;
123 | }
124 |
125 | exports.compile = function compile(indent) {
126 | var code = [''],
127 | tokens = [],
128 | parent, filepath, blockname, varOutput;
129 |
130 | indent = indent || '';
131 |
132 | // Precompile - extract blocks and create hierarchy based on 'extends' tags
133 | // TODO: make block and extends tags accept context variables
134 | if (this.type === TEMPLATE) {
135 | this.tokens.forEach(function (token, index) {
136 | // TODO: Check for circular extends
137 | // Load the parent template
138 | if (token.name === 'extends') {
139 | filepath = token.args[0];
140 | if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
141 | throw new Error("Extends tag accepts exactly one strings literal as an argument.");
142 | }
143 | if (index > 0) {
144 | throw new Error("Extends tag must be the first tag in the template.");
145 | }
146 | token.template = this.fromFile(filepath.replace(/['"]/g, ''));
147 | } else if (token.name === 'block') { // Make a list of blocks
148 | blockname = token.args[0];
149 | if (!helpers.isValidBlockName(blockname) || token.args.length > 1) {
150 | throw new Error("Invalid block tag syntax.");
151 | }
152 | if (this.type !== TEMPLATE) {
153 | throw new Error("Block tag found inside another tag.");
154 | }
155 | this.blocks[blockname] = compile.call(token, indent + ' ');
156 | }
157 | tokens.push(token);
158 | }, this);
159 |
160 | if (tokens[0].name === 'extends') {
161 | parent = tokens[0].template;
162 | this.blocks = helpers.merge(parent.blocks, this.blocks);
163 | this.tokens = parent.tokens;
164 | }
165 | }
166 |
167 | // If this is not a template then just iterate through its tokens
168 | this.tokens.forEach(function (token, index) {
169 | if (typeof token === 'string') {
170 | return code.push('__output.push("' + token.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '");');
171 | }
172 |
173 | if (typeof token !== 'object') {
174 | return; // Tokens can be either strings or objects
175 | }
176 |
177 | if (token.type === VAR_TOKEN) {
178 | varOutput = token.name;
179 | return code.push(
180 | 'if (' + check(varOutput) + ') {'
181 | , ' __output.push(' + wrapFilters(varOutput, token.filters) + ');'
182 | , '} else if (' + check(varOutput, '__context') + ') {'
183 | , ' __output.push(' + wrapFilters(varOutput, token.filters, '__context') + ');'
184 | , '}'
185 | );
186 | }
187 |
188 | if (token.type !== LOGIC_TOKEN) {
189 | return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
190 | }
191 |
192 | if (token.name === 'extends') {
193 | if (this.type !== TEMPLATE) {
194 | throw new Error("Extends tag must be the first tag in the template.");
195 | }
196 | } else if (token.name === 'block') {
197 | if (this.type !== TEMPLATE) {
198 | throw new Error("You can not nest block tags into other tags.");
199 | }
200 |
201 | code.push(this.blocks[token.args[0]]); // Blocks are already compiled in the precompile part
202 | } else {
203 | code.push(token.compile(indent + ' '));
204 | }
205 |
206 | }, this);
207 |
208 | return code.join("\n" + indent);
209 | };
--------------------------------------------------------------------------------
/lib/tags.js:
--------------------------------------------------------------------------------
1 | var parser = require('./parser');
2 | var helpers = require('./helpers');
3 |
4 | var check = helpers.check;
5 | var escape = helpers.escape;
6 | var compile = parser.compile;
7 |
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 };
15 |
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 with a link'},
27 | *
28 | * "This is a paragraph as a normal string.
",
29 | *
30 | * "Normal strings get echoed into the template directly.
",
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: '
'}]
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 | };
64 |
65 | /**
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 | };
100 |
101 |
102 | /**
103 | * This is the 'if' tag compiler
104 | * Example 'If' tag syntax:
105 | * {% if x %}
106 | * {{x}}
107 | * {% end %}
108 | *
109 | * {% if !x %}
110 | * No x found
111 | * {% else %}
112 | * {{x}}
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) + ';');
156 | out.push(' }');
157 | out.push(' else if (' + check(operand1, '__context') + ') {');
158 | out.push(' __op1 = ' + escape(operand1, '__context') + ';');
159 | out.push(' }');
160 | if (typeof operand2 === 'undefined') {
161 | out.push(' if (' + (negation ? '!' : '!!') + '__op1) {');
162 | out.push(compile.call(this, indent + ' '));
163 | out.push(' }');
164 | }
165 | else {
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(' }');
184 | }
185 | out.push('})();');
186 | return out.join("\n" + indent);
187 | };
188 | exports['if'].ends = true;
189 |
190 | /**
191 | * This is the 'for' tag compiler
192 | * Example 'For' tag syntax:
193 | * {% for x in y.some.items %}
194 | * {{x}}
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;'
232 | , ' ' + escape(operand1) + ' = __forloopIter[__forloopIndex];'
233 | , compile.call(this, indent + ' ')
234 | , ' }'
235 | , ' }'
236 | , ' else if (typeof __forloopIter === "object") {'
237 | , ' var __forloopIndex;'
238 | , ' for (__forloopIndex in __forloopIter) {'
239 | , ' forloop.index = __forloopIndex;'
240 | , ' ' + escape(operand1) + ' = __forloopIter[__forloopIndex];'
241 | , compile.call(this, indent + ' ')
242 | , ' }'
243 | , ' }'
244 | , '})();'].join("\n" + indent);
245 | };
246 | exports['for'].ends = true;
--------------------------------------------------------------------------------
/lib/widgets.js:
--------------------------------------------------------------------------------
1 | var textWidgetGenerator = function (tagname) {
2 | return function () {
3 | var output = ["<", tagname],
4 | i;
5 |
6 | for (i in this) {
7 | if (this.hasOwnProperty(i) && i !== "content" && 1 !== "tagname") {
8 | output.push(" ", i, "='", this[i], "'");
9 | }
10 | }
11 |
12 | output.push(">", this.content, "", tagname, ">");
13 | return output.join("");
14 | };
15 | };
16 |
17 | /**
18 | * Renders a paragraph. This is fairly simple.
19 | */
20 | exports.p = textWidgetGenerator("p");
21 | /**
22 | * Renders a paragraph. This is fairly simple.
23 | */
24 | exports.h1 = textWidgetGenerator("h1");
25 |
26 | /**
27 | * Renders a paragraph. This is fairly simple.
28 | */
29 | exports.h2 = textWidgetGenerator("h2");
30 |
31 | /**
32 | * Renders a paragraph. This is fairly simple.
33 | */
34 | exports.h3 = textWidgetGenerator("h3");
35 |
36 | /**
37 | * Renders an oredered list. This is fairly simple.
38 | */
39 | exports.ol = textWidgetGenerator("ol");
40 |
41 | /**
42 | * Renders an unoredered list. This is fairly simple.
43 | */
44 | exports.ul = textWidgetGenerator("ul");
45 |
46 | /**
47 | * Renders a blockquote. This is fairly simple.
48 | */
49 | exports.q = textWidgetGenerator("q");
50 |
51 | /**
52 | * Renders a simple composite widget.
53 | */
54 | exports.list = exports.image = function (context) {
55 | var output = ["", this.content, "
");
66 |
67 | return output.join("");
68 | };
69 |
70 | /**
71 | * This helper renders a slot independently from a template.
72 | */
73 | exports.renderSlot = function (slotContent, context) {
74 | var slot = slotContent || [],
75 | output = [],
76 | widget,
77 | i = 0,
78 | j = slot.length;
79 |
80 | for (i, j; i < j; i += 1) {
81 | widget = slot[i];
82 | if (widget === undefined || widget === null || widget === false) {
83 | continue;
84 | }
85 |
86 | if (typeof widget === 'string') {
87 | output.push(widget);
88 | } else if (widget.tagname && typeof exports[widget.tagname] === "function") {
89 | output.push(exports[widget.tagname].call(widget, context));
90 | }
91 | }
92 |
93 | return output.join("");
94 | };
95 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-t",
3 | "version": "0.1.0",
4 | "description": "A fast django-like templating engine for node.js",
5 | "keywords": ["template", "html", "django", "sandbox"],
6 | "repository": "git://github.com/skid/node-t.git",
7 | "author": "Dusko Jordanovski ",
8 | "dependencies": {},
9 | "devDependencies": {
10 | "nodelint": "0.4.0",
11 | "nodeunit": "0.5.3"
12 | },
13 | "main": "index",
14 | "engines": {
15 | "node": ">= 0.4.1"
16 | },
17 | "scripts": {
18 | "lint": "make lint",
19 | "test": "make test"
20 | }
21 | }
--------------------------------------------------------------------------------
/scripts/config-lint.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: __dirname + '/../',
3 | pathIgnore: ['*node_modules*']
4 | };
5 |
6 | var options = {
7 | adsafe: false,
8 | bitwise: false,
9 | browser: true,
10 | cap: false,
11 | css: false,
12 | debug: false,
13 | devel: true,
14 | eqeqeq: true,
15 | evil: true,
16 | forin: false,
17 | fragment: false,
18 | immed: false,
19 | indent: 4,
20 | laxbreak: true,
21 | maxerr: 300,
22 | maxlen: 600,
23 | nomen: false,
24 | newcap: true,
25 | node: true, // jslint.com has an option for node, but the node module is not up to date yet
26 | on: true,
27 | onevar: true,
28 | passfail: false,
29 | plusplus: false,
30 | predef: ['module', 'util', 'require', 'process', 'exports', 'escape', '__dirname', 'setTimeout'],
31 | regexp: false,
32 | rhino: false,
33 | safe: false,
34 | strict: false,
35 | sub: false,
36 | undef: true,
37 | white: true,
38 | widget: false,
39 | windows: false
40 | };
41 |
42 |
--------------------------------------------------------------------------------
/scripts/config-test.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: __dirname + '/../',
3 | testRunner: 'default',
4 | pathIgnore: ['*node_modules*']
5 | };
6 |
--------------------------------------------------------------------------------
/scripts/githooks/post-merge:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | make
4 |
--------------------------------------------------------------------------------
/scripts/githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | function failCommit() {
4 | echo "\033[31m----------------------------------------\033[0m"
5 | echo "FATAL ERROR: $1"
6 | echo "\033[31m----------------------------------------\033[0m"
7 | exit 1
8 | }
9 |
10 | function testFail() {
11 | echo "\033[33m----------------------------------------\033[0m"
12 | echo "$1"
13 | echo "\033[33m----------------------------------------\033[0m"
14 |
15 | }
16 |
17 | if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
18 | against=HEAD
19 | else
20 | # Initial commit: diff against an empty tree object
21 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
22 | fi
23 |
24 | # Remove all of the trailing whitespace in this commit
25 | for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq` ; do
26 | sed -i '' -E 's/[[:space:]]*$//' "$FILE"
27 | git add $FILE
28 | done
29 |
30 | echo 'Running JSLint...'
31 | result=$(make lint)
32 | if ! grep -q "^0 errors" <<< $result; then
33 | num=$(grep "[0-9] error" <<< "$result")
34 | testFail "JSLint: $num"
35 | echo "$result"
36 | echo ''
37 | lintFailed=1
38 | fi
39 |
40 | if [[ $lint_errors -gt 0 ]]; then
41 | failCommit "Lint Errors"
42 | fi
43 |
44 | echo 'Running Tests...'
45 | result=$(make test)
46 | if grep -q FAILURES <<< $result; then
47 | num=$(grep "FAILURES" <<< "$result")
48 | testFail "Test $num"
49 | echo "$result"
50 | echo ''
51 | testsFailed=1
52 | fi
53 |
54 | if [[ $testsFailed || $lintFailed ]]; then
55 | failCommit "Unable To Commit"
56 | fi
57 |
--------------------------------------------------------------------------------
/scripts/runlint.js:
--------------------------------------------------------------------------------
1 | require.paths.unshift(__dirname + '/../node_modules/');
2 |
3 | var util = require('util'),
4 | child_process = require('child_process'),
5 | configFile = __dirname + '/config-lint',
6 | ignore = '',
7 | config, root, i;
8 |
9 | process.argv.forEach(function (val, index, array) {
10 | if (index < 2) {
11 | return;
12 | }
13 |
14 | if (val === '-c') {
15 | configFile = process.argv[~~index + 1];
16 | }
17 | });
18 |
19 | config = require(configFile);
20 |
21 | function runLint(error, stdout, stderr) {
22 | var files = stdout.trim().replace(/\n/g, ' ');
23 |
24 | child_process.exec('node ' + __dirname + '/../node_modules/nodelint/nodelint ' + files + ' --config ' + configFile + '.js', { cwd: config.root }, function (error, stdout, stderr) {
25 | util.puts(stderr);
26 | });
27 | }
28 |
29 | i = config.pathIgnore.length;
30 | while (i--) {
31 | ignore += ' ! -path "' + config.pathIgnore[i] + '"';
32 | }
33 |
34 | child_process.exec('find . -name "*.js"' + ignore, { cwd: config.root }, runLint);
35 |
--------------------------------------------------------------------------------
/scripts/runtests.js:
--------------------------------------------------------------------------------
1 | require.paths.unshift(__dirname + '/../node_modules/');
2 |
3 | var util = require('util'),
4 | child_process = require('child_process'),
5 | nodeunit = require('nodeunit'),
6 | configFile = __dirname + '/config-test',
7 | ignore = '',
8 | config, test_runner, i;
9 |
10 | process.argv.forEach(function (val, index, array) {
11 | if (index < 2) {
12 | return;
13 | }
14 |
15 | if (val === '-c') {
16 | configFile = process.argv[~~index + 1];
17 | }
18 | });
19 |
20 | config = require(configFile);
21 | test_runner = nodeunit.reporters[config.testRunner];
22 |
23 | function runTests(error, stdout, stderr) {
24 | var tests = stdout.trim().split("\n");
25 | if (tests.length && tests[0] !== '') {
26 | test_runner.run(tests);
27 | }
28 | }
29 |
30 | i = config.pathIgnore.length;
31 | while (i--) {
32 | ignore += ' ! -path "' + config.pathIgnore[i] + '"';
33 | }
34 |
35 | child_process.exec('find . -name "*.test.js" ' + ignore, { cwd: config.root }, runTests);
36 |
--------------------------------------------------------------------------------
/tests/filters.test.js:
--------------------------------------------------------------------------------
1 | var filters = require('../lib/filters');
2 |
3 | exports.default = function (test) {
4 | var defOut = 'blah';
5 | test.strictEqual('foo', filters.default('foo', defOut), 'string not overridden by default');
6 | test.strictEqual(0, filters.default(0, defOut), 'zero not overridden by default');
7 |
8 | test.strictEqual(defOut, filters.default('', defOut), 'empty string overridden by default');
9 | test.strictEqual(defOut, filters.default(undefined, defOut), 'default overrides undefined');
10 | test.strictEqual(defOut, filters.default(null, defOut), 'default overrides null');
11 | test.strictEqual(defOut, filters.default(false, defOut), 'default overrides false');
12 | test.done();
13 | };
14 |
15 | exports.lower = function (test) {
16 | var input = 'BaR';
17 | test.strictEqual('bar', filters.lower(input));
18 | input = 345;
19 | test.strictEqual('345', filters.lower(input));
20 | test.done();
21 | };
22 |
23 | exports.upper = function (test) {
24 | var input = 'bar';
25 | test.strictEqual('BAR', filters.upper(input));
26 | input = 345;
27 | test.strictEqual('345', filters.upper(input));
28 | test.done();
29 | };
30 |
31 | exports.capitalize = function (test) {
32 | var input = 'awesome sauce.';
33 | test.strictEqual('Awesome sauce.', filters.capitalize(input));
34 | input = 345;
35 | test.strictEqual('345', filters.capitalize(input));
36 | test.done();
37 | };
38 |
39 | exports.title = function (test) {
40 | var input = 'this is title case';
41 | test.strictEqual('This Is Title Case', filters.title(input));
42 | test.done();
43 | };
44 |
45 | exports.join = function (test) {
46 | var input = [1, 2, 3];
47 | test.strictEqual('1+2+3', filters.join(input, '+'));
48 | test.strictEqual('1 * 2 * 3', filters.join(input, ' * '));
49 | input = 'asdf';
50 | test.strictEqual('asdf', filters.join(input, '-'), 'Non-array input is not joined.');
51 | test.done();
52 | };
53 |
54 | exports.reverse = function (test) {
55 | test.deepEqual([3, 2, 1], filters.reverse([1, 2, 3]), 'reverse array');
56 | test.strictEqual('asdf', filters.reverse('asdf'), 'reverse string does nothing');
57 | test.deepEqual({ 'foo': 'bar' }, filters.reverse({ 'foo': 'bar' }), 'reverse object does nothing');
58 | test.done();
59 | };
60 |
61 | exports.length = function (test) {
62 | var input = [1, 2, 3];
63 | test.strictEqual(3, filters.length(input));
64 | input = 'foobar';
65 | test.strictEqual(6, filters.length(input));
66 | test.done();
67 | };
68 |
69 | exports.url_encode = function (test) {
70 | var input = "param=1&anotherParam=2";
71 | test.strictEqual("param%3D1%26anotherParam%3D2", filters.url_encode(input));
72 | test.done();
73 | };
74 |
75 | exports.url_decode = function (test) {
76 | var input = "param%3D1%26anotherParam%3D2";
77 | test.strictEqual("param=1&anotherParam=2", filters.url_decode(input));
78 | test.done();
79 | };
80 |
81 | exports.json_encode = function (test) {
82 | var input = { foo: 'bar', baz: [1, 2, 3] };
83 | test.strictEqual('{"foo":"bar","baz":[1,2,3]}', filters.json_encode(input));
84 | test.done();
85 | };
86 |
87 | exports.striptags = function (test) {
88 | var input = 'foo
hi
';
89 | test.strictEqual('foo hi', filters.striptags(input));
90 | test.done();
91 | };
92 |
93 | exports.multiple = function (test) {
94 | var input = ['aWEsoMe', 'sAuCe'];
95 | test.strictEqual('Awesome Sauce', filters.title(filters.join(input, ' ')));
96 | test.done();
97 | };
98 |
99 | exports.date = function (test) {
100 | var input = 'Sat Aug 06 2011 09:05:02 GMT-0700 (PDT)';
101 |
102 | test.strictEqual('06', filters.date(input, "d"), 'format: d http://www.php.net/date');
103 | test.strictEqual('Sat', filters.date(input, "D"), 'format: D http://www.php.net/date');
104 | test.strictEqual('6', filters.date(input, "j"), 'format: j http://www.php.net/date');
105 | test.strictEqual('Saturday', filters.date(input, "l"), 'format: l http://www.php.net/date');
106 | test.strictEqual('6', filters.date(input, "N"), 'format: N http://www.php.net/date');
107 | test.strictEqual('th', filters.date(input, "S"), 'format: S http://www.php.net/date');
108 | test.strictEqual('5', filters.date(input, "w"), 'format: w http://www.php.net/date');
109 | test.strictEqual('August', filters.date(input, "F"), 'format: F http://www.php.net/date');
110 | test.strictEqual('08', filters.date(input, "m"), 'format: m http://www.php.net/date');
111 | test.strictEqual('Aug', filters.date(input, "M"), 'format: M http://www.php.net/date');
112 | test.strictEqual('8', filters.date(input, "n"), 'format: n http://www.php.net/date');
113 |
114 | test.strictEqual('2011', filters.date(input, "Y"), 'format: Y http://www.php.net/date');
115 | test.strictEqual('11', filters.date(input, "y"), 'format: y http://www.php.net/date');
116 | test.strictEqual('2011', filters.date(input, "Y"), 'format: Y http://www.php.net/date');
117 | test.strictEqual('am', filters.date(input, "a"), 'format: a http://www.php.net/date');
118 | test.strictEqual('AM', filters.date(input, "A"), 'format: A http://www.php.net/date');
119 | test.strictEqual('9', filters.date(input, "g"), 'format: g http://www.php.net/date');
120 | test.strictEqual('9', filters.date(input, "G"), 'format: G http://www.php.net/date');
121 | test.strictEqual('09', filters.date(input, "h"), 'format: h http://www.php.net/date');
122 | test.strictEqual('09', filters.date(input, "H"), 'format: H http://www.php.net/date');
123 | test.strictEqual('05', filters.date(input, "i"), 'format: i http://www.php.net/date');
124 | test.strictEqual('02', filters.date(input, "s"), 'format: s http://www.php.net/date');
125 |
126 | test.strictEqual('+0700', filters.date(input, "O"), 'format: O http://www.php.net/date');
127 | test.strictEqual('25200', filters.date(input, "Z"), 'format: Z http://www.php.net/date');
128 | test.strictEqual('Sat Aug 06 2011 09:05:02 GMT-0700 (PDT)', filters.date(input, "r"), 'format: r http://www.php.net/date');
129 | test.strictEqual('1312646702', filters.date(input, "U"), 'format: U http://www.php.net/date');
130 |
131 | test.strictEqual('06-08-2011', filters.date(input, "d-m-Y"));
132 |
133 | test.done();
134 | };
135 |
--------------------------------------------------------------------------------
/tests/helpers.test.js:
--------------------------------------------------------------------------------
1 | var helpers = require('../lib/helpers');
2 |
3 | exports.clone = function (test) {
4 | var obj = { foo: 1, bar: 2, baz: { bop: 3 } },
5 | out = helpers.clone(obj);
6 |
7 | test.deepEqual(out, { foo: 1, bar: 2, baz: { bop: 3 } });
8 | test.done();
9 | };
10 |
11 | exports.merge = function (test) {
12 | var a = { foo: 1, bar: 2 },
13 | b = { foo: 2 },
14 | out;
15 |
16 | out = helpers.merge(a, b);
17 | test.deepEqual(out, { foo: 2, bar: 2 }, 'returns merged object');
18 | test.deepEqual(a, { foo: 1, bar: 2 }, 'does not overwrite original object');
19 |
20 | test.done();
21 | };
22 |
--------------------------------------------------------------------------------
/tests/parser.test.js:
--------------------------------------------------------------------------------
1 | var testCase = require('nodeunit').testCase,
2 | parser = require('../lib/parser');
3 |
4 | exports.Tags = testCase({
5 | 'undefined tag throws error': function (test) {
6 | test.throws(function () {
7 | parser.parse('{% foobar %}', {});
8 | }, Error);
9 | test.done();
10 | },
11 |
12 | 'basic tag': function (test) {
13 | var output = parser.parse('{% blah %}', { blah: {} });
14 | test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, name: 'blah', args: [], compile: {} }], output);
15 |
16 | output = parser.parse('{% blah "foobar" %}', { blah: {} });
17 | test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, name: 'blah', args: ['"foobar"'], compile: {} }], output, 'args appended');
18 |
19 | output = parser.parse('{% blah "foobar" barfoo %}', { blah: {} });
20 | test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, name: 'blah', args: ['"foobar"', 'barfoo'], compile: {} }], output, 'multiple args appended');
21 |
22 | test.done();
23 | },
24 |
25 | 'basic tag with ends': function (test) {
26 | var output = parser.parse('{% blah %}{% end %}', { blah: { ends: true } });
27 | test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, name: 'blah', args: [], compile: { ends: true }, tokens: [] }], output);
28 | test.done();
29 | },
30 |
31 | 'throws if requires end but no end found': function (test) {
32 | test.throws(function () {
33 | parser.parse('{% blah %}', { blah: { ends: true }});
34 | }, Error);
35 | test.done();
36 | },
37 |
38 | 'throws if not end but end found': function (test) {
39 | test.throws(function () {
40 | parser.parse('{% blah %}{% end %}', { blah: {}});
41 | }, Error);
42 | test.done();
43 | },
44 |
45 | 'tag with contents': function (test) {
46 | var output = parser.parse('{% blah %}hello{% end %}', { blah: { ends: true } });
47 | test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, name: 'blah', args: [], compile: { ends: true }, tokens: ['hello'] }], output);
48 | test.done();
49 | }
50 | });
51 |
52 | exports.Comments = testCase({
53 | 'empty strings are ignored': function (test) {
54 | var output = parser.parse('');
55 | test.deepEqual([], output);
56 |
57 | output = parser.parse(' ');
58 | test.deepEqual([], output);
59 |
60 | output = parser.parse(' \n');
61 | test.deepEqual([], output);
62 |
63 | test.done();
64 | },
65 |
66 | 'comments are ignored': function (test) {
67 | var output = parser.parse('{# foobar #}');
68 | test.deepEqual([], output);
69 | test.done();
70 | }
71 | });
72 |
73 | exports.Variable = testCase({
74 | 'basic variable': function (test) {
75 | var output = parser.parse('{{ foobar }}');
76 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foobar', filters: [] }], output, 'with spaces');
77 |
78 | output = parser.parse('{{foobar}}');
79 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foobar', filters: [] }], output, 'without spaces');
80 |
81 | test.done();
82 | },
83 |
84 | 'dot-notation variable': function (test) {
85 | var output = parser.parse('{{ foo.bar }}');
86 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foo.bar', filters: [] }], output);
87 | test.done();
88 | },
89 |
90 | 'variable with filter': function (test) {
91 | var output = parser.parse('{{ foobar|awesome }}');
92 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foobar', filters: [{ name: 'awesome', args: [] }] }], output, 'filter by name');
93 |
94 | output = parser.parse('{{ foobar|awesome("param", 2) }}');
95 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foobar', filters: [{ name: 'awesome', args: ['param', 2] }] }], output, 'filter with params');
96 |
97 | test.done();
98 | },
99 |
100 | 'multiple filters': function (test) {
101 | var output = parser.parse('{{ foobar|baz(1)|rad|awesome("param", 2) }}');
102 | test.deepEqual([{ type: parser.TOKEN_TYPES.VAR, name: 'foobar', filters: [
103 | { name: 'baz', args: [1] },
104 | { name: 'rad', args: [] },
105 | { name: 'awesome', args: ['param', 2] }
106 | ] }], output);
107 |
108 | test.done();
109 | },
110 |
111 | 'filters do not carry over': function (test) {
112 | var output = parser.parse('{{ foo|baz }}{{ bar }}');
113 | test.deepEqual([
114 | { type: parser.TOKEN_TYPES.VAR, name: 'foo', filters: [{ name: 'baz', args: [] }] },
115 | { type: parser.TOKEN_TYPES.VAR, name: 'bar', filters: [] }
116 | ], output);
117 | test.done();
118 | }
119 | });
120 |
121 | exports.Compiling = testCase({
122 | // TODO: fill in tests for compiling
123 | });
124 |
--------------------------------------------------------------------------------
/tests/speed.js:
--------------------------------------------------------------------------------
1 | var template = require('../index'),
2 | tplF, tplS, array, output, d, i;
3 |
4 | console.log();
5 | console.log('Starting speed tests...');
6 |
7 | template.init(__dirname, false);
8 |
9 | tplS = template.fromString(
10 | "{% for v in array %}"
11 | + "{% if 1 %}"
12 | + "{% for k in v %}"
13 | + "\n{{forloop.index}} {{k}}: "
14 | + "{% if forloop.index in 'msafas' %}"
15 | + "Hello World {{k}}{{foo}}{{k}}{{foo}}{{k}}{{foo}}
"
16 | + "{% end %}"
17 | + "{% end %}"
18 | + "{% end %}"
19 | + "{% end %}"
20 | );
21 |
22 | template.init(__dirname + '/templates');
23 |
24 | array = [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], { af: "s", baz: "d", d: "f" }, "zeus"];
25 | tplF = template.fromFile("include_base.html");
26 | template.fromFile("included_2.html");
27 | template.fromFile("included.html");
28 |
29 | i = 1000;
30 | console.time('Render 1000 Includes Templates');
31 | d = new Date();
32 | while (i--) {
33 | tplF.render({ array: array, foo: "baz", "included": "included.html" });
34 | }
35 | console.timeEnd('Render 1000 Includes Templates');
36 | console.log(" ~ " + Math.round(1000000 / (new Date() - d)) + " renders per sec.");
37 |
38 | template.fromFile("extends_base.html");
39 | template.fromFile("extends_1.html");
40 | tplF = template.fromFile("extends_2.html");
41 |
42 | i = 1000;
43 | console.time('Render 1000 Extends Templates');
44 | d = new Date();
45 | while (i--) {
46 | tplF.render({ array: array, foo: "baz", "included": "included.html" });
47 | }
48 | console.timeEnd('Render 1000 Extends Templates');
49 | console.log(" ~ " + Math.round(1000000 / (new Date() - d)) + " renders per sec.");
50 |
--------------------------------------------------------------------------------
/tests/templates/extends_1.html:
--------------------------------------------------------------------------------
1 | {% extends "extends_base.html" %}
2 | This is content from "extends_1.html", you should not see it
3 |
4 | {% block one %}
5 | This is the "extends_1.html" content in block 'one'
6 | {% end %}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/templates/extends_2.html:
--------------------------------------------------------------------------------
1 | {% extends "extends_1.html" %}
2 | This is content from "extends_2.html", you should not see it
3 |
4 | {% block two %}
5 | This is the "extends_2.html" content in block 'two'
6 | {% include "include_base.html" %}
7 | {% end %}
--------------------------------------------------------------------------------
/tests/templates/extends_base.html:
--------------------------------------------------------------------------------
1 | This is from the "extends_base.html" template.
2 |
3 | {% block one %}
4 | This is the default content in block 'one'
5 | {% end %}
6 |
7 | {% block two %}
8 | This is the default content in block 'two'
9 | {% end %}
--------------------------------------------------------------------------------
/tests/templates/include_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This is a test
5 | {% include included %}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/templates/included.html:
--------------------------------------------------------------------------------
1 | {% for v in array %}
2 | {% if 1 %}
3 | {% for k in v %}
4 | {{k}}: {{v}}
5 | {% if forloop.index === "af" %}
6 | foo: {{foo}}
7 | Hello World {{k}} {{foo}}
8 | array.length: {% include "included_2.html" %}
9 | {% end %}
10 | {% end %}
11 | {% end %}
12 | {% end %}
--------------------------------------------------------------------------------
/tests/templates/included_2.html:
--------------------------------------------------------------------------------
1 | {{array.length}}
--------------------------------------------------------------------------------