зеркало из https://github.com/mozilla/djangode.git
Added documentation for the templatesystem.
Also some bugs with errorhandling in the rendering chain have been fixed.
This commit is contained in:
Родитель
3ccef9d7cc
Коммит
fc97ff0a56
|
@ -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 }} <!-- <script type=&qout;text/javascript&qout;>alert(&qout;hey!&qout;)</script> -->
|
||||
{{ 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.
|
||||
|
|
@ -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 {
|
||||
|
|
Загрузка…
Ссылка в новой задаче