Comparing version 1.1.0 to 2.0.0rc1
module.exports = require('./lib/jqtpl'); | ||
module.exports.express = require('./lib/jqtpl.express'); | ||
module.exports.__express = require('./lib/express').render; |
386
lib/jqtpl.js
@@ -1,4 +0,7 @@ | ||
/*jslint evil: true*/ | ||
(function(exports) { | ||
/** | ||
* Port of jQuery's Template Engine to Nodejs. | ||
* A template engine for nodejs, browser and any other javascript environment. | ||
* | ||
* Originally started as a port of jQuery's Template Engine to Nodejs. | ||
* http://github.com/jquery/jquery-tmpl | ||
@@ -11,2 +14,3 @@ * | ||
var isArray = Array.isArray; | ||
@@ -21,95 +25,13 @@ /** | ||
*/ | ||
var isArray = Array.isArray || function(obj) { | ||
return Object.prototype.toString.call(obj) === '[object Array]'; | ||
}; | ||
if (!isArray) { | ||
isArray = function(obj) { | ||
return {}.toString.call(obj) === '[object Array]'; | ||
}; | ||
} | ||
/** | ||
* Iterate over array or object ala jquery. | ||
* @param {Object|Array} obj object or array to iterate over. | ||
* @param {Function} callback function. | ||
* @api public | ||
*/ | ||
exports.each = function(obj, callback) { | ||
var key; | ||
if (isArray(obj)) { | ||
for (key = 0; key < obj.length; ++key) { | ||
callback.call(obj[key], key, obj[key]); | ||
} | ||
} else { | ||
for (key in obj) { | ||
callback.call(obj[key], key, obj[key]); | ||
} | ||
} | ||
}; | ||
/** | ||
* Escape html chars. | ||
* @param {String} str html string. | ||
* @return {String} str escaped html string. | ||
* @api public | ||
*/ | ||
exports.encode = function(str) { | ||
return String(str) | ||
.replace(/&/g, '&') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>') | ||
.replace(/'/g, ''') | ||
.replace(/"/g, '"'); | ||
}; | ||
/** | ||
* Tags supported by the engine. | ||
* @type {Object} | ||
* @api public | ||
*/ | ||
exports.tag = { | ||
'tmpl': { | ||
_default: { $2: 'null' }, | ||
open: 'if($notnull_1){_=_.concat($item.nest($1,$2));}' | ||
// tmpl target parameter can be of type function, so use $1, | ||
// not $1a (so not auto detection of functions) This means that | ||
// {{tmpl foo}} treats foo as a template (which IS a function). | ||
// Explicit parens can be used if foo is a function that returns | ||
// a template: {{tmpl foo()}}. | ||
}, | ||
'wrap': { | ||
_default: { $2: 'null' }, | ||
open: '$item.calls(_,$1,$2);_=[];', | ||
close: 'call=$item.calls();_=call._.concat($item.wrap(call,_));' | ||
}, | ||
'each': { | ||
_default: { $2: '$index, $value' }, | ||
open: 'if($notnull_1){$.each($1a,function($2){with(this){', | ||
close: '}});}' | ||
}, | ||
'if': { | ||
open: 'if(($notnull_1) && $1a){', | ||
close: '}' | ||
}, | ||
'else': { | ||
_default: { $1: 'true' }, | ||
open: '}else if(($notnull_1) && $1a){' | ||
}, | ||
'html': { | ||
// Unecoded expression evaluation. | ||
open: 'if($notnull_1){_.push($1a);}' | ||
}, | ||
'=': { | ||
// Encoded expression evaluation. Abbreviated form is ${}. | ||
_default: { $1: '$data' }, | ||
open: 'if($notnull_1){_.push($.encode($1a));}' | ||
}, | ||
'!': { | ||
// Comment tag. Skipped by parser | ||
open: '' | ||
}, | ||
'verbatim': {} | ||
}; | ||
/** | ||
* Escape template. | ||
* @param {String} args template. | ||
* @return {String} args | ||
* | ||
* @param {String} template. | ||
* @return {String} | ||
* @api private | ||
@@ -126,4 +48,5 @@ */ | ||
* Unescape template. | ||
* @param {String} args template. | ||
* @return {String} args | ||
* | ||
* @param {String} template. | ||
* @return {String} | ||
* @api private | ||
@@ -135,2 +58,7 @@ */ | ||
var rVerbatim = /\{\{verbatim\}\}((.|\n)*?)\{\{\/verbatim\}\}/g, | ||
rTags = /\$\{([^\}]*)\}/g, | ||
rParser = /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g, | ||
rEscapedWhite = /\\n|\\t|\\r/g; | ||
/** | ||
@@ -143,32 +71,30 @@ * Build reusable function for template generation | ||
*/ | ||
function buildTmplFn(markup) { | ||
function build(markup) { | ||
var verbatims = [], | ||
rverbatim = /\{\{verbatim\}\}((.|\n)*?)\{\{\/verbatim\}\}/g; | ||
body; | ||
// save all the data inside the verbatim tags | ||
markup = markup.replace(rverbatim, function(all, content) { | ||
verbatims.push(content); | ||
body = | ||
'var __body="";' + | ||
// replace the {{verbatim}}data{{/verbatim}} with just {{verbatim}} | ||
// this tag will let the parser know where to inject the corresponding data | ||
return "{{verbatim}}"; | ||
}); | ||
// Introduce the data as local variables using with(){}. | ||
'with(__data){' + | ||
'__body+="' + | ||
// Convert the template into pure JavaScript. | ||
escape(markup) | ||
.trim() | ||
// Save all the data inside the verbatim tags. | ||
.replace(rVerbatim, function(all, content) { | ||
verbatims.push(content); | ||
return new Function('$', '$item', | ||
'var call,_=[],$data=$item.data;' + | ||
// Replace the {{verbatim}}data{{/verbatim}} with just {{verbatim}} | ||
// this tag will let the parser know where to inject the corresponding data. | ||
return "{{verbatim}}"; | ||
}) | ||
.replace(rTags, '{{= $1}}') | ||
.replace(rParser, function(all, slash, type, fnargs, target, parens, args) { | ||
var tag = exports.tag[type], def, expr, exprAutoFnDetect; | ||
// Introduce the data as local variables using with(){} | ||
'with($data){_.push("' + | ||
// Convert the template into pure JavaScript | ||
escape(markup.trim()) | ||
.replace(/\$\{([^\}]*)\}/g, '{{= $1}}') | ||
.replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g, | ||
function(all, slash, type, fnargs, target, parens, args) { | ||
var tag = exports.tag[type], def, expr, exprAutoFnDetect, | ||
rEscapedWhite = /\\n|\\t|\\r/g; | ||
if (!tag) { | ||
throw new Error('Template command not found: ' + type); | ||
throw new Error('Unknown template tag: ' + type); | ||
} | ||
@@ -178,7 +104,7 @@ | ||
// inject the corresponding verbatim data | ||
// Inject the corresponding verbatim data. | ||
return escape(verbatims.shift()); | ||
} | ||
def = tag._default || []; | ||
def = tag.default || []; | ||
@@ -202,88 +128,204 @@ if (parens && !/\w$/.test(target)) { | ||
// In that case don't call with template item as 'this' pointer. Just evaluate... | ||
expr = parens ? (target.indexOf('.') > -1 ? target + unescape(parens) : ('(' + target + ').call($item' + args)) : target; | ||
exprAutoFnDetect = parens ? expr : '(typeof(' + target + ')==="function"?(' + target + ').call($item):(' + target + '))'; | ||
expr = parens ? (target.indexOf('.') > -1 ? target + unescape(parens) : ('(' + target + ').call(__data' + args)) : target; | ||
exprAutoFnDetect = parens ? expr : '(typeof(' + target + ')==="function"?(' + target + ').call(__data):(' + target + '))'; | ||
} else { | ||
exprAutoFnDetect = expr = def.$1 || 'null'; | ||
exprAutoFnDetect = expr = def.__1 || 'null'; | ||
} | ||
fnargs = unescape(fnargs); | ||
return '");' + | ||
return '";' + | ||
tag[slash ? 'close' : 'open'] | ||
.split('$notnull_1').join(target ? 'typeof(' + target + ')!=="undefined" && (' + target + ')!=null' : 'true') | ||
.split('$1a').join(exprAutoFnDetect) | ||
.split('$1').join(expr) | ||
.split('$2').join(fnargs || def.$2 || '') + | ||
'_.push("'; | ||
} | ||
) + | ||
'");}return _.join("");' | ||
); | ||
.split('__notnull_1').join(target ? 'typeof(' + target + ')!=="undefined" && (' + target + ')!=null' : 'true') | ||
.split('__1a').join(exprAutoFnDetect) | ||
.split('__1').join(expr) | ||
.split('__2').join(fnargs || def.__2 || '') + | ||
'__body += "'; | ||
}) + | ||
'"' + | ||
'}' + | ||
'return __body;'; | ||
return new Function('$', '__data', body); | ||
} | ||
/** | ||
* Generate reusable function and cache it using name or markup as a key | ||
* Render a template. | ||
* | ||
* @param {String} name of the template. | ||
* @param {String} markup html string - optional. | ||
* @return {Function|undefined} reusable template generator function. | ||
* @param {String} cached tempalte name. | ||
* @param {Object?} data used as template vars. | ||
* @return {String} rendered markup string. | ||
* @api private | ||
*/ | ||
function render(name, data) { | ||
var fn = exports.cache[name], | ||
ret, i; | ||
if (data && isArray(data)) { | ||
ret = ''; | ||
for (i = 0; i < data.length; ++i) { | ||
ret += fn.call({}, $, data[i]); | ||
} | ||
} else { | ||
ret = fn.call({}, $, data || {}); | ||
} | ||
return ret; | ||
} | ||
/** | ||
* Tags supported by the engine. | ||
* | ||
* @type {Object} | ||
* @api public | ||
*/ | ||
exports.template = function template(name, markup) { | ||
name = name || markup; | ||
exports.tag = { | ||
partial: { | ||
default: {__2: 'null'}, | ||
var fn = template[name]; | ||
// Partal target parameter can be of type function, so use __1, | ||
// not __1a (so not auto detection of functions) This means that | ||
// {{partial foo}} treats foo as a template (which IS a function). | ||
// Explicit parens can be used if foo is a function that returns | ||
// a template: {{partial foo()}}. | ||
open: 'if(__notnull_1){__body+=$.partial(__1,__2,__data)}' | ||
}, | ||
each: { | ||
default: {__2: '$value, $index'}, | ||
open: 'if(__notnull_1){$.each(__1a,function(__2){', | ||
close: '});}' | ||
}, | ||
verbatim: {}, | ||
if (markup != null && !fn) { | ||
// Uncoded expression evaluation. | ||
html: { | ||
open: 'if(__notnull_1){__body+=__1a}' | ||
}, | ||
'if': { | ||
open: 'if((__notnull_1) && __1a){', | ||
close: '}' | ||
}, | ||
'else': { | ||
default: {__1: 'true'}, | ||
open: '}else if((__notnull_1) && __1a){' | ||
}, | ||
// Encoded expression evaluation. Abbreviated form is ${}. | ||
'=': { | ||
default: {__1: '__data'}, | ||
open: 'if(__notnull_1){__body+=$.escapeHtml(__1a)}' | ||
}, | ||
// Comment tag. Skipped by parser. | ||
'!': { | ||
open: '' | ||
} | ||
}; | ||
/** | ||
* Cached template generator functions. | ||
* | ||
* - `key` - template name or markup string. | ||
* - `value` - compiled template function. | ||
* | ||
* @type {Object} | ||
* @api public | ||
*/ | ||
exports.cache = {}; | ||
/** | ||
* Build a reusable function and cache it using name or markup as a key. | ||
* | ||
* @param {String} markup html string. | ||
* @param {String?} optional name of the template. | ||
* @return {Function} reusable template generator function. | ||
* @api public | ||
*/ | ||
exports.compile = function(markup, name) { | ||
if (markup == null) { | ||
throw new Error('Param `markup` is required.'); | ||
} | ||
name || (name = markup); | ||
if (!exports.cache[name]) { | ||
// Generate a reusable function that will serve as a template | ||
// generator (and which will be cached). | ||
try { | ||
fn = template[name] = buildTmplFn(markup); | ||
exports.cache[name] = build(markup); | ||
} catch(err) { | ||
throw new Error('CompilationError: ' + err + '\nTemplate: ' + name); | ||
err.message += ': ' + name; | ||
err.markup = markup; | ||
throw err; | ||
} | ||
} | ||
return fn; | ||
return function(data) { | ||
return render(name, data); | ||
}; | ||
}; | ||
/** | ||
* Compile and render a template. | ||
* | ||
* @param {String} markup or cached name. | ||
* @param {Object?} data used as template vars. | ||
* @return {String} rendered markup string. | ||
* @api public | ||
*/ | ||
exports.render = function(markup, data) { | ||
return exports.compile(markup)(data); | ||
}; | ||
/** | ||
* Global helpers namespace - make functions available in every template. | ||
* Use this namespace in custom tags. | ||
* | ||
* @type {Object} | ||
* @api public | ||
*/ | ||
var $ = exports.$ = {}; | ||
/** | ||
* Render nested template | ||
* @param {String} tmpl path to the view | ||
* @param {Object} data object optional. | ||
* @param {Object} options optional. | ||
* @return {String} rendered template. | ||
* @api private | ||
* Can be overridden to provide platform specific functionality like | ||
* loading files by name. | ||
*/ | ||
function nest(tmpl, data, options) { | ||
return exports.tmpl(exports.template(tmpl), data, options); | ||
} | ||
$.partial = exports.render; | ||
/** | ||
* Render template | ||
* @param {String|Function} markup html markup or precompiled markup name. | ||
* @param {Object} data can be used in template as template vars. | ||
* @param {Object} options additional options. | ||
* @return {String} ret rendered markup string. | ||
* @api public | ||
* Iterate over array or object. | ||
* | ||
* @param {Object|Array} obj object or array to iterate over. | ||
* @param {Function} callback function. | ||
* @api private | ||
*/ | ||
exports.tmpl = function(markup, data, options) { | ||
var fn = typeof markup === 'function' ? markup : exports.template(null, markup), | ||
ret = '', i; | ||
$.each = function(obj, callback) { | ||
var key; | ||
data = data || {}; | ||
options = options || {}; | ||
options.data = data; | ||
options.nest = nest; | ||
if (isArray(data)) { | ||
for (i = 0; i < data.length; ++i) { | ||
options.data = data[i]; | ||
ret += fn.call(options.scope, exports, options); | ||
if (isArray(obj)) { | ||
for (key = 0; key < obj.length; ++key) { | ||
callback.call(obj[key], obj[key], key); | ||
} | ||
} else { | ||
ret = fn.call(options.scope, exports, options); | ||
for (key in obj) { | ||
callback.call(obj[key], obj[key], key); | ||
} | ||
} | ||
}; | ||
return ret; | ||
/** | ||
* Escape html chars. | ||
* | ||
* @param {String} str html string. | ||
* @return {String} str escaped html string. | ||
* @api private | ||
*/ | ||
$.escapeHtml = function(str) { | ||
return String(str) | ||
.replace(/&/g, '&') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>') | ||
.replace(/'/g, ''') | ||
.replace(/"/g, '"'); | ||
}; | ||
}(typeof exports == 'object' ? exports : (this.jqtpl = {}))); |
@@ -1,21 +0,18 @@ | ||
{ | ||
{ | ||
"name": "jqtpl", | ||
"description": "A port of jQuery's template engine", | ||
"version": "1.1.0", | ||
"description": "A template engine for nodejs, browser and any other javascript environment", | ||
"version": "2.0.0rc1", | ||
"author": "Oleg Slobodskoi <oleg008@gmail.com>", | ||
"contributors": [ | ||
{ "name": "John Resig", "email": "jeresig@gmail.com" }, | ||
{ "name": "Boris Moore", "email": "borismoore@gmail.com"} | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/kof/node-jqtpl.git" | ||
"url": "http://github.com/kof/jqtpl.git" | ||
}, | ||
"keywords": ["template", "engine", "jquery", "jquery-tmpl", "django", "logicless", "express"], | ||
"directories": { "lib": "./lib" }, | ||
"engines": { "node": ">= 0.3.7" }, | ||
"keywords": ["template", "engine", "jquery", "jquery-tmpl", "logic-less"], | ||
"engines": {"node": "*"}, | ||
"dependencies": { | ||
"underscore": "1.4.x" | ||
}, | ||
"devDependencies": { | ||
"express": ">= 2.2.1 < 3.0.0", | ||
"qunit": "0.2.x", | ||
"underscore": "1.3.x", | ||
"express": "3.0.x", | ||
"qunit": "0.5.x", | ||
"request": "2.9.x" | ||
@@ -22,0 +19,0 @@ }, |
285
readme.md
@@ -1,125 +0,54 @@ | ||
## This is a port of jQuery's Template Engine to nodejs | ||
## A template engine for nodejs, browser and any other javascript environment. | ||
- Logic-less. | ||
- Extendable - implement your own tags. | ||
- Html escaped per default. | ||
### Originally started as a port of jquery templates. | ||
http://github.com/jquery/jquery-tmpl | ||
## Full API documentation of the original plugin | ||
http://api.jquery.com/category/plugins/templates/ | ||
Note: currently not implemented: wrap tag and tmplItem method. | ||
**Now compatibility to the original engine is dropped as jquery-tmpl is not any more developed.** | ||
## Philosophy is similar to django | ||
http://docs.djangoproject.com/en/dev/topics/templates/ | ||
* no program logic in templates | ||
* no embeded script language like in ejs | ||
1. this is evil because it enables program logic in templates | ||
1. bad usability | ||
1. because of the "var" problem in javascript | ||
## Features | ||
* jquery tmpl plugin conform | ||
* extendable - you can implement new statements | ||
* html escaping per default | ||
* simple syntax | ||
* tiny and fast | ||
## Installation via npm | ||
npm install jqtpl | ||
## Run tests | ||
### Installation | ||
$ npm i jqtpl | ||
$ make test | ||
## Template API | ||
## Usage | ||
### ${}, {{=}} print variable, array or function (escaped) | ||
### require the module | ||
var jqtpl = require("jqtpl"); | ||
- Print variable | ||
### jqtpl.tmpl(markup, data, options); | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.render(tpl, {a:123}); | ||
// output | ||
<div>123</div> | ||
Compile and render a template. It uses `jqtpl.template` method. | ||
- Print array | ||
- `markup` html code string | ||
- `data` object or array of data | ||
- `options` optional options object | ||
//tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.render(tpl, [{a:1},{a:2},{a:3}]); | ||
// output | ||
<div>1</div><div>2</div><div>3</div> | ||
### jqtpl.template(name, tpl) | ||
- Print automatically detected function | ||
Named templates - there is a way to precompile the template using a string, so you can render this template later using its name. | ||
**Template is cached after this fn call.** | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.render(tpl, { | ||
a: function() { | ||
return 1 + 5; | ||
} | ||
}); | ||
//output | ||
<div>6</div> | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
// precompile an cache it | ||
jqtpl.template( "templateName", tpl ); | ||
// render | ||
jqtpl.tmpl( "templateName", {a:1} ); | ||
// you can also delete the template from cache | ||
delete jqtpl.template["templateName"]; | ||
// output | ||
<div>1</div> | ||
### Local variables | ||
- `$data` - data object passed to render method | ||
- `$item` - contains $data via $item.data as well as user options - an optional map of user-defined key-value pairs. | ||
Examples: | ||
// tpl | ||
<div>${ $item.someMethod() }</div> | ||
// code | ||
jqtpl.tmpl( tpl, {a:1}, { | ||
someMethod: function(){ return 1; } | ||
}); | ||
//output | ||
<div>1</div> | ||
## Tags | ||
### ${} - simple output (escaped per default) | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.tmpl( tpl, {a:123}); | ||
// output | ||
<div>123</div> | ||
### ${} - simple output but with array as data argument (escaped per default) | ||
//tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.tmpl( tpl, [{a:1},{a:2},{a:3}]); | ||
// output | ||
<div>1</div><div>2</div><div>3</div> | ||
### ${} - if property is a function - it will be called automatically (escaped per default) | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
jqtpl.tmpl( tpl, { | ||
a:function() { | ||
return 1 + 5; | ||
} | ||
}); | ||
//output | ||
<div>6</div> | ||
### {{if}} and {{else}} | ||
@@ -137,3 +66,3 @@ | ||
// code | ||
jqtpl.tmpl( tpl, {a:6}); | ||
jqtpl.render(tpl, {a:6}); | ||
@@ -144,3 +73,3 @@ // output | ||
// code | ||
jqtpl.tmpl( tpl, {a:5}); | ||
jqtpl.render(tpl, {a:5}); | ||
@@ -153,3 +82,3 @@ // output | ||
// tpl | ||
{{each(i, name) names}} | ||
{{each(name, i) names}} | ||
<div>${i}.${name}</div> | ||
@@ -159,3 +88,2 @@ {{/each}} | ||
// alternative syntax | ||
{{each names}} | ||
@@ -166,3 +94,3 @@ <div>${$index}.${$value}</div> | ||
// code | ||
jqtpl.tmpl( tpl, {names: ["A", "B"]}); | ||
jqtpl.render(tpl, {names: ['A', 'B']}); | ||
@@ -172,3 +100,3 @@ // output | ||
### {{html}} - there is a way to avoid escaping if you know what you do :) | ||
### {{html}} - print unescaped html. | ||
@@ -179,3 +107,3 @@ // tpl | ||
// code | ||
jqtpl.tmpl( tpl, {a:'<div id="123">2</div>'}); | ||
jqtpl.render(tpl, {a:'<div id="123">2</div>'}); | ||
@@ -192,3 +120,3 @@ // output | ||
// code | ||
jqtpl.tmpl( tpl ); | ||
jqtpl.render(tpl); | ||
@@ -198,11 +126,15 @@ // output | ||
### {{tmpl}} - subtemplates. | ||
### {{partial}} - subtemplates. | ||
Note: passing json object with 2 curly brackets without any separation will break the engine: {{tmpl({a: {b: 1}}) "mypartial"}} | ||
Render subtemplates by passing a template string, template name or file name (serverside). | ||
**Note: passing json object with 2 curly brackets without any separation will break the engine: {{partial({a: {b: 1}}) 'mypartial'}}** | ||
// tpl | ||
<div>{{tmpl({name: "Test"}) '${name}'}}</div> | ||
<div>{{partial({name: 'Test'}) '${name}'}}</div> | ||
<div>{{partial 'myTemplate'}}</div> | ||
<div>{{partial 'myTemplate.html'}}</div> | ||
// code | ||
jqtpl.tmpl(tpl); | ||
jqtpl.render(tpl); | ||
@@ -212,14 +144,15 @@ // output | ||
# Not jquery-tmpl compatible stuff | ||
## Specific tags | ||
### {{verbatim}} tag | ||
If you want to skip a part of your template, which should be rendered on the client, you can use now verbatim tag. | ||
Skip a part of your template - leave it in original on the same place but without "verbatim" tag. If you render the result as a template again - it will be rendered. | ||
The use case is to be able to render the same template partially on the server and on the client. F.e. a layout template can contain variables which needs to be rendered on the server and templates which need to be rendered on the client. | ||
// mytemplate.html | ||
<div>my name is ${name}</div> | ||
{{verbatim}} | ||
<script id="my-template"> | ||
<div>your name is ${userName}</div> | ||
</script> | ||
{{/verbatim}} | ||
@@ -232,72 +165,68 @@ | ||
<div>my name is Kof</div> | ||
<div>your name is ${userName}</div> | ||
<script id="my-template"> | ||
<div>your name is ${userName}</div> | ||
</script> | ||
## Express specific stuff | ||
## Engine API | ||
**Note: express is caching all templates in production!** | ||
### require the module | ||
var jqtpl = require('jqtpl'); | ||
### Usage | ||
### jqtpl.render(markup, [data]); | ||
app.set("view engine", "html"); | ||
app.register(".html", require("jqtpl").express); | ||
Compile and render a template. It uses `jqtpl.template` method. Returns a rendered html string. | ||
### {{partial}} tag | ||
- `markup` html code or precompiled template name. | ||
- `data` optional object or array of data. | ||
Read express documentation here http://expressjs.com/guide.html#res.partial() | ||
### jqtpl.compile(markup, [name]) | ||
// tpl | ||
Compile and cache a template string. Returns a `render` function which can be called to render the template, see `jtpl.render`. | ||
// myaction.html | ||
<div>{{partial(test) "mypartial"}}</div> | ||
- `markup` html string. | ||
- `name` optional template name, if no name is passed - markup string will be used as a name. | ||
// mypartial.html | ||
${name} | ||
// tpl | ||
<div>${a}</div> | ||
// code | ||
app.get('/myaction', function(req, res) { | ||
res.render('myaction', {test: {name: 'Test'}}); | ||
}) | ||
// code | ||
// output | ||
<div>Test</div> | ||
// precompile an cache it | ||
jqtpl.compile(tpl, 'myTemplate'); | ||
Using array of data: | ||
// render user a name | ||
jqtpl.render('myTemplate', {a:1}); | ||
// tpl | ||
// delete the template from cache | ||
delete jqtpl.cache['myTemplate']; | ||
// myaction.html | ||
<div id="main"> | ||
{{partial(test) "mypartial"}} | ||
</div> | ||
// output | ||
<div>1</div> | ||
// mypartial.html | ||
<div class="partial"> | ||
${name} | ||
</div> | ||
### jqtpl.cache | ||
// code | ||
app.get('/myaction', function(req, res) { | ||
res.render('myaction', { | ||
as: global, | ||
test: [ | ||
{name: "Test1"}, | ||
{name: "Test2"} | ||
] | ||
}); | ||
}) | ||
A map of compiled templates. | ||
// output | ||
<div id="main"> | ||
<div class="partial">Test1</div> | ||
<div class="partial">Test2</div> | ||
</div> | ||
- `key` - template name or markup string. | ||
- `value` - compiled template function. | ||
### jqtpl.$ | ||
A namespace for global helper functions, which can be used in every template. | ||
## Express specific stuff | ||
**Note: express will cache all templates in production!** | ||
### Usage | ||
app.set('views', '/path/to/the/views/dir'); | ||
app.set('view engine', 'html'); | ||
app.engine('html', require('jqtpl').__express); | ||
### {{layout}} tag | ||
Using layout tag in a view it is possible to define a layout within this view. | ||
Note: it is possible since express@2.2.1. | ||
// tpl | ||
// mylayout.html | ||
@@ -309,8 +238,16 @@ <html> | ||
// myview.html | ||
{{layout "mylayout"}} | ||
<div>myview</div> | ||
{{layout 'mylayout'}} | ||
<div>myview</div> | ||
// myview1.html | ||
{{layout({a: 1}) 'mylayout'}} | ||
<div>myview1</div> | ||
// output | ||
<html> | ||
<div>myview</div> | ||
<div>myview</div> | ||
</html> | ||
## Licence | ||
See package.json |
var express = require('express'), | ||
_ = require('underscore'), | ||
request = require('request'); | ||
request = require('request'), | ||
fs = require('fs'); | ||
var server, | ||
options; | ||
var options; | ||
@@ -15,10 +15,19 @@ options = { | ||
options.express = { | ||
views: options.root + '/1', | ||
'view engine': 'html', | ||
'view options': {layout: false} | ||
}; | ||
var views = options.root + '/1'; | ||
server = createServer(); | ||
function create() { | ||
var app = express(); | ||
app.engine('html', render); | ||
app.set('views', views); | ||
app.set('view engine', 'html'); | ||
app.use(express.bodyParser()); | ||
app.post('/*', function(req, res){ | ||
res.render(req.url.substr(1), req.body); | ||
}); | ||
app.listen(options.port); | ||
return app; | ||
} | ||
function post(path, data, callback) { | ||
@@ -29,2 +38,3 @@ if (!callback) { | ||
} | ||
request({ | ||
@@ -36,3 +46,4 @@ method: 'post', | ||
if (err) { | ||
throw new Error(err); | ||
console.error(err); | ||
equal(err, null, 'Request errored.'); | ||
} | ||
@@ -44,24 +55,4 @@ | ||
function createServer(opts) { | ||
var server = express.createServer(express.bodyParser()), | ||
eo = _.defaults(opts || {}, options.express); | ||
var app = create(); | ||
_.each(eo, function(val, name) { | ||
server.set(name, val); | ||
}); | ||
// qunit copies jqtpl.express exports to global | ||
server.register('.html', global); | ||
server.post('/*', function(req, res){ | ||
if (req.body) { | ||
req.body.as = global; | ||
} | ||
res.render(req.url.substr(1), req.body); | ||
}); | ||
server.listen(options.port); | ||
return server; | ||
} | ||
test("locals", function() { | ||
@@ -76,26 +67,3 @@ expect(1); | ||
test("scope option", function() { | ||
var fn = compile( "<div>${this.test}</div>", {scope: {test: 123}} ); | ||
same(fn({a:1}), '<div>123</div>', "scope is correct"); | ||
}); | ||
test("debug option", function() { | ||
var printed, | ||
util = require( "util" ), | ||
debug = util.debug; | ||
// mock print method | ||
util.debug = function( str ) { | ||
printed = true; | ||
}; | ||
compile( 'test', {debug: true} )(); | ||
// restore orig. print function | ||
util.debug = debug; | ||
ok( printed, "debug option works" ); | ||
}); | ||
test("partials using `partial`", function() { | ||
test("partials", function() { | ||
expect(1); | ||
@@ -109,3 +77,3 @@ stop(); | ||
test("partials using `partial`", function() { | ||
test("partials 2", function() { | ||
expect(1); | ||
@@ -126,56 +94,58 @@ stop(); | ||
test("layout tag", function() { | ||
var html = 'mylayout requested view mylayout'; | ||
expect(2); | ||
test('render template with a layout', function() { | ||
expect(1); | ||
stop(); | ||
post('/layouttest', function(data) { | ||
equal(data, html, 'served html is correct'); | ||
post('/layouttest', function(data) { | ||
ok(data, html, 'if caching is turned, second call should work too #46'); | ||
start(); | ||
}); | ||
app.set('view options', {layout: true}); | ||
app.set('views', options.root + '/2'); | ||
post('/views/test', {mylocal: "mylocal"}, function(data) { | ||
equal(data, 'abc mylocal', 'template and layout rendered correctly'); | ||
app.disable('view options'); | ||
app.set('views', views); | ||
start(); | ||
}); | ||
}); | ||
test("rendering multiple times of the same template #29", function() { | ||
var template = 'Just example ${example}' | ||
var je = require('../').express | ||
var render = je.compile(template, {filename: 'example.html'}) | ||
test("render multiple times the same template #29", function() { | ||
var data; | ||
equal(render({example: 'Hello'}), 'Just example Hello', 'template rendered correctly'); | ||
equal(render({example: 'Hello'}), 'Just example Hello', 'template rendered correctly'); | ||
data = { | ||
a: 'Hello', | ||
settings: { | ||
'view options': {} | ||
} | ||
}; | ||
equal(render(views + '/view.html', data), '<div>Hello</div>', 'template rendered correctly'); | ||
equal(render(views + '/view.html', data), '<div>Hello</div>', 'template rendered correctly 2'); | ||
equal(render(views + '/view.html', data), '<div>Hello</div>', 'template rendered correctly 3'); | ||
}); | ||
test("clean cache if template have to be recompiled", function() { | ||
var je = require('../').express | ||
test("template recompiled if cache disabled", function() { | ||
var data, | ||
view = views + '/view.html'; | ||
var template = 'my template 1'; | ||
var render = je.compile(template, {filename: 'template.html'}); | ||
equal(render(), 'my template 1', 'template 1 rendered correctly'); | ||
// now template has been changed | ||
template = 'my template 2'; | ||
render = je.compile(template, {filename: 'template.html'}) | ||
equal(render(), 'my template 2', 'template 2 rendered correctly after recompile'); | ||
data = { | ||
a: 'Hello', | ||
settings: { | ||
'view options': {} | ||
} | ||
}; | ||
equal(render(view, data), '<div>Hello</div>', 'template rendered correctly'); | ||
fs.writeFileSync(view, 'new template ${a}'); | ||
equal(render(view, data), 'new template Hello', 'template was recompiled'); | ||
fs.writeFileSync(view, '<div>${a}</div>'); | ||
}); | ||
test("rendering template with a layout turned on", function() { | ||
expect(1); | ||
test("layout tag", function() { | ||
var html = 'mylayout requested view mylayout'; | ||
expect(2); | ||
stop(); | ||
server.close(); | ||
server = createServer({ | ||
'view options': {layout: true}, | ||
views: options.root + '/2' | ||
post('/layouttest', function(data) { | ||
equal(data, html, 'layout rendered correctly'); | ||
post('/layouttest', function(data) { | ||
equal(data, html, 'if caching is turned on, second call should work too #46'); | ||
start(); | ||
}); | ||
}); | ||
post('/views/test', {mylocal: "mylocal"}, function(data) { | ||
equal(data, 'abc mylocal', 'template and layout rendered correctly'); | ||
server.close(); | ||
server = createServer(); | ||
start(); | ||
}); | ||
}); | ||
@@ -1,78 +0,194 @@ | ||
QUnit.module('jqtpl'); | ||
test('Variables ${}, {{=}}', function() { | ||
equal(render('lorem ipsum'), 'lorem ipsum', 'plain text passes through untouched'); | ||
equal(render('${ a }', {a: 1}), '1', 'simple variable output'); | ||
equal(render(''), '', 'empty template string rendered without errors'); | ||
equal(render('{{}}'), '{{}}', 'empty tag'); | ||
equal(render('{{\t\t}}'), '{{\t\t}}', 'empty tag with tabs whitespace'); | ||
equal(render('<div>${a}/${a}</div>', {a:1}), '<div>1/1</div>', 'many variables'); | ||
equal(render('<div>{{= a}}</div>', {a:1}), '<div>1</div>', 'use simple data object, using {{= }} in the template '); | ||
equal(render('<div>${a}</div>', [{a:1}]), '<div>1</div>', 'use an array with one object element'); | ||
equal(render('<div>{{= a}}</div>', [{a:1}]), '<div>1</div>', 'use an array with one object element, using {{= }} in the template'); | ||
equal(render('<div>${a}</div>', [{a:1},{a:2}]), '<div>1</div><div>2</div>', 'use an array with 2 objects'); | ||
equal(render('<div>${a}</div>', {a: function(){return 1}}), '<div>1</div>', 'use function as a value'); | ||
equal(render('${ "string" }'), 'string', 'basic string output (double)'); | ||
equal(render("${ 'string' }"), 'string', 'basic string output (single)'); | ||
equal(render('${ isUndefined }'), '', 'variable lookup error suppression'); | ||
var tpl1 = "<div>${a}</div>", | ||
tpl2 = "<div>{{= a}}</div>", | ||
tpl3 = "<div>${$data.a}</div>", | ||
tpl4 = "<div>${$item.data.a}</div>", | ||
tpl5 = "<div>${$item.someFunction()}</div>", | ||
tpl6 = "{{html a}}", | ||
tpl9 = "{{if a == 1}}<div>${a}</div>{{/if}}", | ||
tpl11 = "{{if a == 1}}<div>${a}</div>{{else}}2{{/if}}", | ||
tpl12 = "{{if a == 1}}<div>${a}</div>{{else a==2 }}2{{else}}3{{/if}}"; | ||
equal(render('A${ a }', {a: 1}), 'A1', 'variable and text (1)'); | ||
equal(render('${ a }B', {a: 1}), '1B', 'variable and text (2)'); | ||
equal(render('A${ a }B', {a: 1}), 'A1B', 'variable and text (3)'); | ||
test('method "template"', function() { | ||
equal( typeof template("test", tpl1), "function", "precompile template and cache using template name" ); | ||
equal( tmpl("test", {a:1}), "<div>1</div>", "render using template name" ); | ||
ok( delete template["test"], "remove cache item" ); | ||
equal(render('${ a.b.c }', {a:{b:{c:'abc'}}}), 'abc', 'lookups work for submembers'); | ||
equal(render('<div>${a()}</div>', {a: function(){return 1}}), '<div>1</div>', 'function can be called within tag'); | ||
equal(render('<div>${a("aaa")}</div>', {a: function(arg){return arg}}), '<div>aaa</div>', 'functions pass strings correctly'); | ||
equal(render('<div>${a(aaa)}</div>', {a: function(arg){return arg}, aaa: 123}), '<div>123</div>', 'functions pass arguments correctly'); | ||
equal( | ||
render('${ foo }', { | ||
foo: { | ||
toString: function () {return 'S';}, | ||
toValue:function () {return 'V';} | ||
} | ||
}), | ||
'S', | ||
'variables use toString, not toValue' | ||
); | ||
equal(render('${ dot,dot,comma,dash }', {dot:'.','comma':',','dash':'-'}), '-', 'Comma passes variables correctly.'); | ||
equal(render('${ fun }', {fun: function() {return 123;}}), 123, 'variable gets called if it is callable'); | ||
equal(render('${ obj.fun }', {obj: {fun: function() {return 123;}}}), 123, 'last variable in sequence gets called if it is callable'); | ||
equal( | ||
render('${ foo.bar }', { | ||
foo: function() { | ||
return { bar: function () {return 'BAZ'; } }; | ||
} | ||
}), | ||
'', | ||
'member functions in a sequence do not get called' | ||
); | ||
// FIXME | ||
// equal(render('${ "str\\"i\\"ng" }'), 'str"i"ng', 'string quote escapes (double)'); | ||
// equal(render("${ 'str\\'i\\'ng' }"), "str'i'ng", 'string quote escapes (single)'); | ||
// equal(render('{{ }}'), '{{ }}', 'empty tag with whitespace'); | ||
// equal(render('${ isUndefined.member }'), '', 'variable lookup error suppression (with member)'); | ||
}); | ||
test('escaping', function() { | ||
equal(tmpl("${ 'foo<div>bar</div>baz' }"), 'foo<div>bar</div>baz', 'echoing escapes html'); | ||
equal(tmpl("${ r }", {r:'foo<div>bar</div>baz'}), 'foo<div>bar</div>baz', 'echoing escapes html (lookup)'); | ||
equal(tmpl("${ '&' }"), '&', 'echoing escapes ampersands 1'); | ||
equal(tmpl("${ '&' }"), '&amp;', 'echoing escapes ampersands 2'); | ||
equal(tmpl("${ '-<&>-<&>-' }"), '-<&>-<&>-', 'echoing escapes & < >'); | ||
test("Falsy values", function() { | ||
equal(render('${ 0 }'), '0', '0'); | ||
equal(render('${ false }'), 'false', 'false'); | ||
equal(render('${ null }'), '', 'null'); | ||
equal(render('${ undefined }'), '', 'undefined'); | ||
equal(render("${ '' }"), '', 'empty string'); | ||
equal(render('${ "" }'), '', 'empty string 2'); | ||
}); | ||
test('${}', function() { | ||
equal( tmpl(tpl1, {a:1}), "<div>1</div>", "use simple data object" ); | ||
equal( tmpl(tpl1,{a:'<div id="123">2</div>'}), "<div><div id="123">2</div></div>", "escaping per default" ); | ||
equal( tmpl(tpl2, {a:1}), "<div>1</div>", "use simple data object, using {{= }} in the template " ); | ||
equal( tmpl(tpl1, [{a:1}]), "<div>1</div>", "use an array with one object element" ); | ||
equal( tmpl(tpl2, [{a:1}]), "<div>1</div>", "use an array with one object element, using {{= }} in the template" ); | ||
equal( tmpl(tpl1, [{a:1},{a:2}]), "<div>1</div><div>2</div>", "use an array with 2 objects" ); | ||
equal( tmpl(tpl1, {a: function(){return 1}}), "<div>1</div>", "use function as a value" ); | ||
}) | ||
test("Falsy lookups", function() { | ||
equal(render('${ zero }', {zero: 0}), '0', '0'); | ||
equal(render('${ zero }', {zero: false}), 'false', 'false'); | ||
equal(render('${ zero }', {zero: null}), '', 'null'); | ||
equal(render('${ zero }', {zero: undefined}), '', 'undefined'); | ||
equal(render("${ zero }", {zero: ''}), '', 'empty string'); | ||
}); | ||
test('local variables', function() { | ||
equal( tmpl(tpl3, {a:1}), "<div>1</div>", "test access to $data" ); | ||
equal( tmpl(tpl4, {a:1}), "<div>1</div>", "test access to $item" ); | ||
equal( tmpl(tpl5, null, {someFunction: function() {return 1}}), "<div>1</div>", "test access to $item" ); | ||
test("Javascript operations", function() { | ||
equal(render('${ one + "foo" }', {one: 'first'}), 'firstfoo', 'string concatination'); | ||
equal(render('${ 1 + 5 }'), '6', 'adding'); | ||
equal(render('${ 9 - 5 }'), '4', 'subtracting'); | ||
equal(render('${ 5 % 2 }'), '1', 'modulo'); | ||
equal(render('${ -n }', {n:10}), '-10', 'unary minus'); | ||
equal(render('${ +n }', {n:"10"}), '10', 'unary plus'); | ||
equal(render('${ "bar" in foo }', {foo:{bar:'baz'}}), 'true', 'in operator'); | ||
equal(render('${ foo instanceof Date }', {foo:new Date()}), 'true', 'instanceof operator'); | ||
equal(render('${ typeof "str" }'), 'string', 'typeof operator'); | ||
equal(render('${ n & 1 }', {n:5}), '1', 'bitwise AND'); | ||
equal(render('${ n | 1 }', {n:4}), '5', 'bitwise OR'); | ||
equal(render('${ n ^ 1 }', {n:5}), '4', 'bitwise XOR'); | ||
equal(render('${ ~n }', {n:5}), '-6', 'bitwise NOT'); | ||
equal(render('${ n << 1 }', {n:5}), '10', 'left shift'); | ||
equal(render('${ n >> 1 }', {n:5}), '2', 'right shift'); | ||
equal(render('${ n >>> 1 }', {n:5}), '2', 'zero-fill right shift'); | ||
equal(render('${ 1 == 5 }'), 'false', 'comparing =='); | ||
equal(render('${ 1 != 5 }'), 'true', 'comparing !='); | ||
equal(render('${ 5 === 5 }'), 'true', 'comparing ==='); | ||
equal(render('${ 5 !== 5 }'), 'false', 'comparing !=='); | ||
equal(render('${ 1 >= 5 }'), 'false', 'comparing >='); | ||
equal(render('${ 1 > 5 }'), 'false', 'comparing >'); | ||
equal(render('${ 1 <= 5 }'), 'true', 'comparing <='); | ||
equal(render('${ 1 < 5 }'), 'true', 'comparing <'); | ||
equal(render('${ zero || "FALSY" }', {zero: 0}), 'FALSY', 'Logical OR'); | ||
equal(render('${ zero && "TRUEY" }', {zero: 1}), 'TRUEY', 'Logical AND'); | ||
equal(render('${ zero ? "zero" : "other" }', {zero: 1}), 'zero', 'Conditional Operator'); | ||
equal(render('${ !zero }', {zero: 1}), 'false', 'Unary logical NOT'); | ||
equal(render("${ 'test' }"), 'test', 'Single-Quoted Strings'); | ||
equal(render("${ 'test' == testvar }", { testvar: 'test' }), 'true', 'Single-Quoted Comparison'); | ||
}); | ||
var testData = {}, | ||
out; | ||
function R(a, b) { | ||
out = "equal(render('" + a + "'), "; | ||
} | ||
function test_handler(msg, actual, expected) { | ||
out += "'" + expected + "', '" + msg + "');"; | ||
console.log(out); | ||
} | ||
/* | ||
FIXME | ||
test("Disallowed / illegal", function() { | ||
equal(render('${ a += 1 }', {a: 1}), SyntaxError, 'Disallow incremental assignment'); | ||
equal(render('${ a -= 1 }', {a: 1}), SyntaxError, 'Disallow decremental assignment'); | ||
equal(render('${ a *= 1 }', {a: 1}), SyntaxError, 'Disallow multiply assignment'); | ||
equal(render('${ a /= 1 }', {a: 1}), SyntaxError, 'Disallow division assignment'); | ||
equal(render('${ a <<= 1 }', {a: 1}), SyntaxError, 'Disallow left shift assignment'); | ||
equal(render('${ a >>= 1 }', {a: 1}), SyntaxError, 'Disallow right shift assignment'); | ||
equal(render('${ a >>>= 1 }', {a: 1}), SyntaxError, 'Disallow zero-fill right shift assignment'); | ||
equal(render('${ a &= 1 }', {a: 1}), SyntaxError, 'Disallow bitwise AND assignment'); | ||
equal(render('${ a |= 1 }', {a: 1}), SyntaxError, 'Disallow bitwise OR assignment'); | ||
equal(render('${ a ^= 1 }', {a: 1}), SyntaxError, 'Disallow bitwise XOR assignment'); | ||
equal(render('${ { a:"a"} }', {a: 1}), SyntaxError, 'Disallow literal object creation'); | ||
equal(render('${ [1,2,3] }', {a: 1}), SyntaxError, 'Disallow literal array creation'); | ||
equal(render('${ --a }', {a: 1}), SyntaxError, 'Disallow decrement'); | ||
equal(render('${ (a = 2) }', {a: 1}), SyntaxError, 'Disallow assignments'); | ||
}); | ||
*/ | ||
test("Bracketed accessors", function() { | ||
equal(render('${ foo["bar"] }',{foo:{bar:'baz'}}), 'baz', 'foo["bar"]'); | ||
equal(render("${ foo['bar'] }",{foo:{bar:'baz'}}), 'baz', "foo['bar']"); | ||
}); | ||
test('${html}', function() { | ||
equal( tmpl(tpl6,{a:'<div id="123">2</div>'}), '<div id="123">2</div>', 'output html without escaping'); | ||
equal( | ||
render('{{html a}}', {a:'<div id="123">2</div>'}), | ||
'<div id="123">2</div>', | ||
'output html without escaping' | ||
); | ||
}); | ||
test('${if}', function() { | ||
equal( tmpl(tpl9,{a:1}), "<div>1</div>", "test 'if' when true" ); | ||
equal( tmpl(tpl9,{a:2}), "", "test 'if' when false" ); | ||
test('{{if}} {{else}}', function() { | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: true}), 'TRUE', 'if:true'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: false}), 'FALSE', 'if:false'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: null}), 'FALSE', 'if:null'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: undefined}), 'FALSE', 'if:undefined'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: {}}), 'TRUE', 'if:[]'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: []}), 'TRUE', 'if:{}'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: ''}), 'FALSE', 'if:""'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: 'A'}), 'TRUE', 'if:A'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: 0}), 'FALSE', 'if:0'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if}}', {a: 1}), 'TRUE', 'if:1'); | ||
equal(render('{{if a}}TRUE{{else}}FALSE{{/if a}}', {a: 1}), 'TRUE', '/if ignores following text'); | ||
}); | ||
test('{else}', function() { | ||
equal( tmpl(tpl11,{a:1}), "<div>1</div>", "test else when true" ); | ||
equal( tmpl(tpl11,{a:2}), "2", "test else when false" ); | ||
equal( tmpl(tpl12,{a:2}), "2", "test else =2" ); | ||
equal( tmpl(tpl12,{a:3}), "3", "test else =3" ); | ||
test("Incorrect nesting", function() { | ||
throws(function() { render('{{if 1}}{{if 1}}{{/if}}') }, SyntaxError, 'defaut'); | ||
throws(function() { render('{{if 1}}{{/if}}{{/if}}') }, SyntaxError, 'extra /if'); | ||
throws(function() { render('{{if 1}}{{each arr}}{{/if}}{{/each}}', {arr: []}) }, SyntaxError, 'but terminated'); | ||
}); | ||
test('{{each}}', function() { | ||
equal( | ||
tmpl( | ||
"{{each(index, value) names}}<div>${index}.${value}</div>{{/each}}", | ||
{names: ["A", "B"]} | ||
), | ||
"<div>0.A</div><div>1.B</div>", "test 'each', use index and value, explizitely mapping them " | ||
); | ||
render( | ||
'{{each(value, index) names}}<div>${index}.${value}</div>{{/each}}', | ||
{names: ['A', 'B']} | ||
), | ||
'<div>0.A</div><div>1.B</div>', 'test "each", use index and value, explizitely mapping them ' | ||
); | ||
equal( | ||
tmpl( | ||
"{{each names}}<div>${$index}.${$value}</div>{{/each}}", | ||
{names: ["A", "B"]} | ||
), | ||
"<div>0.A</div><div>1.B</div>", "test 'each', use index and name with auto mapping" | ||
); | ||
render( | ||
'{{each names}}<div>${$index}.${$value}</div>{{/each}}', | ||
{names: ['A', 'B']} | ||
), | ||
'<div>0.A</div><div>1.B</div>', 'test "each", use index and name with auto mapping' | ||
); | ||
equal( | ||
tmpl( | ||
"{{each $item.getData()}}<div>${$value}</div>{{/each}}", | ||
null, | ||
render( | ||
'{{each getData()}}<div>${$value}</div>{{/each}}', | ||
{ | ||
@@ -83,105 +199,141 @@ getData: function(){ | ||
} | ||
), | ||
"<div>1</div><div>2</div><div>3</div>", "test 'each', using templates variables" | ||
); | ||
), | ||
'<div>1</div><div>2</div><div>3</div>', 'test "each", using template variables' | ||
); | ||
equal( | ||
tmpl( | ||
"{{each data }}<div>${$value}</div>{{/each}}", | ||
render( | ||
'{{each data }}<div>${$value}</div>{{/each}}', | ||
{ | ||
data: {1:1, 2:2, 3:3} | ||
} | ||
), | ||
"<div>1</div><div>2</div><div>3</div>", | ||
"iterate over json in each loop" | ||
); | ||
), | ||
'<div>1</div><div>2</div><div>3</div>', | ||
'iterate over json in each loop' | ||
); | ||
}); | ||
test('{{partial}}', function() { | ||
compile('${ "test text" }', 'test'); | ||
equal(render('{{partial "test"}}'), 'test text', 'rendered partial from cache'); | ||
test('{{tmpl}}', function() { | ||
compile('{{partial "test" }', 'nested'); | ||
equal(render('{{partial "nested"}}'), 'nested test text', 'rendered nested partial from cache'); | ||
equal( | ||
tmpl( | ||
"{{tmpl(data) extTpl}}", | ||
render( | ||
'{{partial(data) extTpl}}', | ||
{ | ||
extTpl: "<div>${a}</div>", | ||
extTpl: '<div>${a}</div>', | ||
data: {a:123456} | ||
} | ||
), | ||
"<div>123456</div>", | ||
"include template {{tmpl}} and pass data object" | ||
); | ||
), | ||
'<div>123456</div>', | ||
'include template {{tmpl}} and pass data object' | ||
); | ||
equal( | ||
tmpl( | ||
"{{tmpl(data) extTpl}}", | ||
render( | ||
'{{partial(data) extTpl}}', | ||
{ | ||
extTpl: "<div>${a}</div>", | ||
extTpl: '<div>${a}</div>', | ||
data: [{a:1}, {a:2}] | ||
} | ||
), | ||
"<div>1</div><div>2</div>", | ||
"include template {{tmpl}} and pass data array" | ||
); | ||
), | ||
'<div>1</div><div>2</div>', | ||
'include template {{tmpl}} and pass data array' | ||
); | ||
}); | ||
test('{{!}}', function() { | ||
equal( tmpl("<div>{{! its a comment}}</div>", {a:1}), "<div></div>", "comments work" ); | ||
}); | ||
equal(render('A{{! its a comment}}B'), 'AB', 'comments are removed'); | ||
equal(render('{{! inky }}foo{{! blinky }}'), 'foo', 'comments are removed 2'); | ||
test('empty template', function() { | ||
try { | ||
equal(tmpl(''), '', 'empty template string rendered without errors'); | ||
} catch(e) { | ||
ok(false, 'empty template shouldn\'t throw errors'); | ||
} | ||
// FIXME | ||
// equal(render('A{{! comments "}}" test }}B'), 'AB', 'comments may include string of comments'); | ||
// equal(render('A{# C{# E #}D #}B'), 'AD #}B', 'comments cannot nest other comments'); | ||
// test_handler( "comments may include strings with escapes (double)", R('A{# comments "str\"ing" test #}B', testData), "AB" ); | ||
// test_handler( "comments may include strings with escapes (single)", R("A{# comments 'str\'ing' test #}B", testData), "AB" ); | ||
// test_handler( "comments may include tags", R("A{# {{= v }} #}B", testData), "AB" ); | ||
// test_handler( "comments may span lines", R("A{# \ncomments test\n #}B", testData), "AB" ); | ||
// test_handler( "comments may contain invalid content (invalid tag)", R('1{{! {{ INVALID_TAG }} }}2', testData), '12' ); | ||
// test_handler( "comments may contain invalid content (stray end tag)", R('1{{! {{/if}} }}2', testData), '12' ); | ||
// test_handler( "comments may contain invalid content (stray else)", R('1{{! {{else}} }}2', testData), '12' ); | ||
// test_handler( "comments may contain invalid content (invalid javascript)", R('1{{! {{if ...}} }}2', testData), '12' ); | ||
}); | ||
test('preserve whitespaces', function() { | ||
var html; | ||
var html = render("<div>\n{{= [{\tkey: \n'value'\r}] }}\n</div>", {}); | ||
try { | ||
html = tmpl( | ||
'<div>\n{{= [{\tkey: \n"value"\r}] }}\n</div>', | ||
{} | ||
); | ||
ok(true, 'no compilation errors'); | ||
} catch(err) { | ||
ok(false, err.message); | ||
} | ||
equal( | ||
html, | ||
'<div>\n[object Object]\n</div>', | ||
'whitespaces preserved' | ||
); | ||
"<div>\n[object Object]\n</div>", | ||
"whitespaces preserved" | ||
); | ||
try { | ||
html = tmpl( | ||
'<div>\n{{= someFunction({\tkey: \n"value"\r}) }}\n</div>', | ||
{ | ||
someFunction: function(data) { | ||
return 'some text ' + data.key; | ||
} | ||
html = render( | ||
"<div>\n{{= someFunction({\tkey: \n'value'\r}) }}\n</div>", | ||
{ | ||
someFunction: function(data) { | ||
return "some text " + data.key; | ||
} | ||
); | ||
ok(true, 'no compilation errors'); | ||
} catch(err) { | ||
ok(false, err.message); | ||
} | ||
} | ||
); | ||
equal( | ||
html, | ||
'<div>\nsome text value\n</div>', | ||
'whitespaces preserved' | ||
); | ||
"<div>\nsome text value\n</div>", | ||
"whitespaces preserved" | ||
); | ||
}); | ||
test('{{verbatim}}', function() { | ||
test("{{verbatim}}", function() { | ||
equal( | ||
tmpl( | ||
"<div>{{= a}}{{verbatim}}${a}12345{{/verbatim}{{/verbatim}}{{verbatim}}}{{= a}}{{/verbatim}}${a}</div>", | ||
render( | ||
'<div>{{= a}}{{verbatim}}${a}12345{{/verbatim}{{/verbatim}}{{verbatim}}}{{= a}}{{/verbatim}}${a}</div>', | ||
{a:1} | ||
), | ||
"<div>1${a}12345{{/verbatim}}{{= a}}1</div>", | ||
"verbatim" | ||
); | ||
), | ||
'<div>1${a}12345{{/verbatim}}{{= a}}1</div>', | ||
'verbatim' | ||
); | ||
}); | ||
test('Error reporting', function() { | ||
throws(function() { render('${ a b c }}'); }, SyntaxError, 'syntax error'); | ||
throws(function() { render('${a.b}'); }, ReferenceError, 'reference error'); | ||
throws(function() { render('${[]()}'); }, TypeError, 'type error'); | ||
}); | ||
test('Escaping', function() { | ||
equal(render('${ "foo<div>bar</div>baz" }'), "foo<div>bar</div>baz", "echoing escapes html"); | ||
equal(render('${ r }', {r:"foo<div>bar</div>baz"}), "foo<div>bar</div>baz", "echoing escapes html (lookup)"); | ||
equal(render('${ "&" }'), "&", "echoing escapes ampersands 1"); | ||
equal(render('${ "&" }'), "&amp;", "echoing escapes ampersands 2"); | ||
equal(render('${ "-<&>-<&>-" }'), "-<&>-<&>-", "echoing escapes & < >"); | ||
equal(render('${\n \na\n }', {a: 1}), '1', 'newlines do not kill tags'); | ||
equal(render('${ "on\ne" }'), 'one', 'newlines in strings do not kill tags'); | ||
equal(render('${\r \r\na\r\n }', {a: 1}), '1', 'returns do not kill tags'); | ||
equal(render('${ "on\re" }'), 'one', 'returns in strings do not kill tags'); | ||
equal(render('${ "on\\e" }'), 'one', 'slashes in strings do not kill tags'); | ||
equal(render('a\nb\nc${ 8 }.'), 'a\nb\nc8.', 'newlines do not kill parsing'); | ||
}); | ||
test("Ignore malformed tags", function() { | ||
equal(render('a {{one } b'), 'a {{one } b', 'a {{one } b'); | ||
equal(render('${ a }} {{b }', {a: '1', b: '1'}), '1} {{b }', '1} {{b }'); | ||
equal(render('{{one }'), '{{one }', '{{one }'); | ||
}); | ||
test("Reserved words", function() { | ||
// FIXME | ||
// throws(function() { render('${ new Object() }'); }, SyntaxError, 'Disallow new operator'); | ||
// throws(function() { render('${ delete a }'); }, SyntaxError, 'Disallow delete operator'); | ||
throws(function() { render('${ function(){} }'); }, SyntaxError, 'Disallow function operator'); | ||
throws(function() { render('${ return a }'); }, SyntaxError, 'Disallow return'); | ||
throws(function() { render('${ for a }'); }, SyntaxError, 'Disallow for'); | ||
throws(function() { render('${ do{ a }while(a) }'); }, SyntaxError, 'Disallow do/while'); | ||
throws(function() { render('${ if a }'); }, SyntaxError, 'Disallow if'); | ||
throws(function() { render('${ try{b.s}catch(e){} }'); }, SyntaxError, 'Disallow try/catch'); | ||
throws(function() { render('${ with (s) }'); }, SyntaxError, 'Disallow with keyword'); | ||
throws(function() { render('${ throw "foo" }'); }, SyntaxError, 'Disallow throw keyword'); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
40252
3
17
856
1
2
242
2
1
+ Addedunderscore@1.4.x
+ Addedunderscore@1.4.4(transitive)