Added documentation for the templatesystem.

Also some bugs with errorhandling in the rendering chain have been fixed.
This commit is contained in:
Anders Hellerup Madsen 2010-03-10 00:17:56 +01:00
Родитель 3ccef9d7cc
Коммит fc97ff0a56
10 изменённых файлов: 398 добавлений и 15 удалений

316
TEMPLATES.md Normal file
Просмотреть файл

@ -0,0 +1,316 @@
Djangode Templates
==================
The templatesystem is a complete port of the real Django template system
implemented for node.js. It uses the same syntax and it supports (almost) all
the default Django (version 1.1) template tags and filters, so if you are used
to writing Django templates, you can jump right in!
If not, I suggest you take a look at the really excellent documentation that is
provided for it - most of it is just as useful for Djangode templates, as it
is for Django:
http://docs.djangoproject.com/en/1.1/topics/templates/
For now, let's look at how to use the templatesystem with Djangode and Node.js:
var dj = require('./djangode'),
template = require('./template/template');
var app = dj.makeApp([
['^/$', function(req, res) {
var t = template.parse('{% for name in list %}Hello, {{ name|capfirst }}!\n{% endfor %}');
var context = { list: [ 'alice', 'bob', 'caitlyn' ] };
t.render(context, function (error, result) {
if (error) {
dj.default_show_500(req, res, error);
} else {
dj.respond(res, result, 'text/plain');
}
});
}]
]);
dj.serve(app, 8000);
The parse() function parses a string into a template object, and the template
objects render() function renders the template with a context.
The render() function uses the same callback style as the node.js standard API
function; The last argument is a callback that is executed when the template is
rendered, and the first argument to the callback is an error flag that is
raised if something goes wrong. The result is passed along as the second
argument to the callback.
Template loader
---------------
Most of the time you do not want to serve your templates from strings, you will
want to keep them in files and use the template_loader module.
var dj = require('./djangode'),
template = require('./template/template'),
loader = require('./template/loader');
loader.set_path('.');
var app = dj.makeApp([
['^/$', function (req, res) {
loader.load('template.html', function (error, t) {
t.render({ list: ['alice', 'bob', 'caitlyn' ] }, function (error, result) {
if (error) {
dj.default_show_500(req, res, error);
} else {
dj.respond(res, result, 'text/plain');
}
});
});
}]
]);
dj.serve(app, 8000);
Now, the template will be rendered from the ./template.html file. The above
chaining of load and render is pretty common so you could use the
load_and_render shortcut:
loader.load_and_render('template.html', context, function (error, result) {
if (error) {
dj.default_show_500(req, res, error);
} else {
dj.respond(res, result, 'text/plain');
}
});
Template inheritance
--------------------
You can extend and inherit your templates from other templates by using the
extend and block tags. See the Django template documentation for a description
of this and some examples. Djangode templates work the same way!
http://docs.djangoproject.com/en/1.1/topics/templates/#id1
Autoescaping
------------
Djangode templates autoescapes everything that is not explicitly marked as safe
with the safe filter. Let's say you have a context that looks like this:
{ str: '<script type="text/javascript">alert("hey!")</script>' };
Rendering that will provide output like this:
{{ str }} <!-- &lt;script type=&qout;text/javascript&qout;&gt;alert(&qout;hey!&qout;)&lt;/script&gt; -->
{{ str|safe }} <!-- <script type="text/javascript">alert("hey!")</script> -->
{% autoescape off %}
{{ str }} <!-- <script type="text/javascript">alert("hey!")</script> -->
{% endautoescape %}
The autoescaping in djangode follows the same rules as django templates - read
in detail about it here:
http://docs.djangoproject.com/en/1.1/topics/templates/#id2
Extending the template system
-----------------------------
Djangode supports (almost) all the Django default templates and filters, and
they should cover most of what you need in your templates, however, there may
be times when it is convenient or even neccessarry to implement your own tags
or filters. In Djangode an extention package is simply any standard node.js
module that exports the two objects tags and filters.
Before you start making your own tags and filters you should read the Django
documentation on the subject. Djangode is JavaScript, not Python, but even
though things are different Djangode templates builds upon the same ideas and
uses the same concepts as Django:
http://docs.djangoproject.com/en/1.1/howto/custom-template-tags/
exports.filters = {
firstletter: function (value, arg, safety) {
return String(value)[0];
}
};
exports.tags = {
uppercase_all: function (parser, token) {
var nodelist = parser.parse('enduppercase_all');
parser.delete_first_token();
return function (context, callback) {
nodelist.evaluate(context, function (error, result) {
if (error) {
callback(error);
} else {
callback(false, result.toUpperCase());
}
});
};
}
};
Here's an example of a template that uses the above package (you will need to
save the above module as tagtest.js and put it in a directory somewhere on your
require path for this example to work):
{% load tagtest %}
{{ str|firstletter }}
{% uppercase_all %}
This text will all be uppercased.
{% enduppercase_all %}
### Custom Filters ########
Filters are javascript functions. They receive the value they should operate on
an the filterarguments. The third argument (safety) is an object that describes
the current safetystate of the value.
function my_filter(value, args, safety) {
return 'some_result';
}
Safety has two properties 'is_safe' and 'must_escape'. 'is_safe' tells wether
or incoming value is considered safe, an therefore should not be escaped.
If the value is not safe, the 'must_escape' property tells if it should be
escaped, that is, if autoescaping is enabled or the escape filter has been
applied to the current filterchain.
If your filter will output any "unsafe" characters such as <, >, & or " it must
escape the entire value if the string if the 'must_escape' property is enabled.
When you have escaped the string, set safety.is_safe to true.
If 'is_safe' is allready marked as true the value may already contain html
characters that should not be escaped. If you cannot correctly handle that your
filter should fail and return an empty string.
function my_filter(value, args, safety) {
/* do processing */
html = require('./utils/html');
if (!safety.is_safe && safety.must_escape) {
result = html.escape(result);
safety.is_safe = true;
}
return result;
}
If your filter does not output any "unsafe" characters, you can completely
disregard the safety argument.
### Custom Tags ########
Tags are called with a parser and and token argument. The token represents the
tag itself (or the opening tag) an contains the tagname and the tags arguments.
Use the token.split_contents() function to split the arguments into words. The
split_contents() function is smart enough not to split qouted values.
// {% my_tag is "the best" %}
function (parser, token) {
token.type // 'my_tag'
token.contents // 'my_tag is "the best"'
token.split_contents() // ['my_tag', 'is', 'the best'];
// see the utils/tags module for some helperfunctions for this parsing and verifying tokens.
}
Your tag function must return a function that takes a context and a callback as
arguments. This function is called when the node is rendered, and when you have
generated the tags output, you must call the callback function - it takes an
error state as it's first argument and the result of the node as it's second.
function (parser, token) {
// ... parse token ...
return function (context, callback) {
context.push(); // push a new scope level onto the scope stack.
// any variables set in the context will have precedence over
// similarly names variables on lower scope levels
context.set('name', 15); // set the variable "name" to 15
var variable = context.get('variable'); // get the value of "variable"
context.pop(); // pop the topmost scope level off the scope stack.
callback(false, result); // complete rendering by calling the callback with the result of the node
}
}
If you want to create a tag that contains other tags (like the uppercase_all
tag above) you must use the parser to parse them:
function (parser, token) {
// parses until it reaches an 'endfor'-tag or an 'else'-tag
var node_list = parser.parse('endfor', 'else');
// parsing stops at the tag it reaches, so use this to get rid of it before returning.
parser.delete_first_token();
var next_token = parser.next_token(); // parse a single token.
return function (context, callback) {
node_list.evaluate(context, function (error, result) {
// process the result of the evaluated nodelist an return by calling callback().
// remember to check the error flag!
}
}
}
Other differences from Django
-----------------------------
### Cycle tag ########
The cycle tag does not support the legacy notation {% cycle row1,row2,row3 %}.
Use the new and improved syntax described in the django docs:
http://docs.djangoproject.com/en/1.1/ref/templates/builtins/#cycle
### Stringformat filter #######
The stringformat filter is based on Alexandru Marasteanu's sprintf() function
and it behaves like regular C-style sprintf. Django uses Pythons sprintf
function, and it has some (very few) nonstandard extensions. These are not
supported by the djangode tag. Most of the time you won't have any problems
though.
http://code.google.com/p/sprintf/
### Url Tag #########
The url tag only supports named urls, and you have to register them with the
templatesystem before you can use them by assigning your url's to the special
variable process.djangode_urls, like this:
var app = dj.makeApp([
['^/item/(\w+)/(\d+)$', function (req, res) { /* ... */ }, 'item']
]);
dj.serve(app, 8000);
process.djangode_urls = app.urls;
Then you can use the url tag in any template:
{% url "item" 'something',27 %} <!-- outputs: /item/something/27 -->
Like in django you can also store the url in a variable and use it later in the
site.
{% url "item" 'something',27 as the_url %}
<a href="{{ the_url }}">This is a link to {{ the_url }}</a>
Read more about the url tag here:
http://docs.djangoproject.com/en/1.1/ref/templates/builtins/#url
### Unsupported Tags and Filters ########
The plan is to support all Django tags and filters, but currently the filters
iriencode and unordered_list and the tags ssi and debug are not supported.

13
template/load_tag_test.js Normal file
Просмотреть файл

@ -0,0 +1,13 @@
exports.filters = {
testfilter: function () {
return 'hestgiraf';
}
};
exports.tags = {
testtag: function () {
return function (context, callback) {
callback('', 'hestgiraf');
};
}
};

Просмотреть файл

@ -12,12 +12,6 @@ var template_path = '/tmp';
// TODO: get_template
// should support subdirectories
/*
template_loader.load_and_render('template.html', test_context, function(rendered) {
dj.respond(res, rendered);
});
*/
var load = exports.load = function (name, callback) {
if (!callback) { throw 'loader.load() must be called with a callback'; }

Просмотреть файл

@ -367,7 +367,7 @@ process.mixin(Template.prototype, {
context.extends = '';
this.node_list.evaluate(context, function (error, rendered) {
if (error) { callback(error); }
if (error) { return callback(error); }
if (context.extends) {
var template_loader = require('./loader');

Просмотреть файл

@ -28,7 +28,7 @@ Missing tags:
NOTE:
cycle tag does not support legacy syntax (row1,row2,row3)
load takes a path - like require. Loaded module must expose tags and filters objects.
url tag relies on app being set in process.djangode_app_config
url tag relies on app being set in process.djangode_urls
*/
var filters = exports.filters = {
@ -609,10 +609,10 @@ var nodes = exports.nodes = {
return function (context, callback) {
var match = process.djangode_urls[context.get(url_name)]
if (!match) { return callback('no matching urls for ' + url_name_val); }
if (!match) { return callback('no matching urls for ' + url_name); }
var url = string_utils.regex_to_string(match, replacements.map(function (x) { return context.get(x); }));
url = '/' + url;
if (url[0] !== '/') { url = '/' + url; }
if (item_name) {
context.set( item_name, url);

Просмотреть файл

@ -44,13 +44,21 @@ var app = dj.makeApp([
['^/text$', function (req, res) {
template_loader.load_and_render('template.html', test_context, function (error, result) {
dj.respond(res, result, 'text/plain');
if (error) {
dj.default_show_500(req, res, error);
} else {
dj.respond(res, result, 'text/plain');
}
});
}],
['^/html$', function (req, res) {
template_loader.load_and_render('template.html', test_context, function (error, result) {
dj.respond(res, result, 'text/html');
if (error) {
dj.default_show_500(req, res, error);
} else {
dj.respond(res, result, 'text/plain');
}
});
}],

Просмотреть файл

@ -9,7 +9,9 @@ exports.reduce = function reduce(array, iter_callback, initial, result_callback)
(function inner (error, value) {
if (error) { result_callback(error); }
if (error) {
return result_callback(error);
}
if (index < array.length) {
process.nextTick( function () {

Просмотреть файл

@ -25,5 +25,41 @@ testcase('reduce');
);
});
test_async('should handle thrown error in iterfunction', function (content, callback) {
var list = [];
for (var i = 0; i < 100; i++) {
list.push(i);
}
var been_here = false;
reduce(list, function (p, c, idx, list, callback) { undefined.will.raise.exception }, 0,
function (error, actual) {
assertIsFalse(been_here);
been_here = true;
assertIsTrue(error, callback);
callback();
}
);
});
test_async('should handle error returned with callback from iterfunction', function (content, callback) {
var list = [];
for (var i = 0; i < 100; i++) {
list.push(i);
}
var been_here = false;
reduce(list, function (p, c, idx, list, callback) { callback('raised error'); }, 0,
function (error, actual) {
assertIsFalse(been_here, callback);
been_here = true;
assertIsTrue(error, callback);
callback();
}
);
});
run();

Просмотреть файл

@ -255,5 +255,3 @@ exports.regex_to_string = function (re, group_replacements) {
return s;
}

Просмотреть файл

@ -166,6 +166,22 @@ exports.dsl = {
};
},
assertIsTrue: function (actual, callback) {
if (async_test_has_failed) { return; }
if (!actual) {
var exception = new AssertFailedException('\nExpected ' + sys.inspect(actual) + ' to be true\n');
if (callback) { callback(exception); } else { throw exception; }
}
},
assertIsFalse: function (actual, callback) {
if (async_test_has_failed) { return; }
if (actual) {
var exception = new AssertFailedException('\nExpected ' + sys.inspect(actual) + ' to be false\n');
if (callback) { callback(exception); } else { throw exception; }
}
},
shouldThrow: function (func, args, this_context, callback) {
if (async_test_has_failed) { return; }
try {