and tags', function () {
6 | var input = 'This is a \'nice\'\n'
7 | + 'way to spend the summer!\n'
8 | + '\n'
9 | + 'The days are just packed!\n';
10 | var expected = '
This is a \'nice\' '
11 | + 'way to spend the summer!
\n'
12 | + '\n'
13 | + '
The days are just packed!
';
14 | var expected_escaped = '
This is a 'nice' '
15 | + 'way to spend the summer!
\n'
16 | + '\n'
17 | + '
The days are just packed!
';
18 | assertEquals(expected, linebreaks(input));
19 | assertEquals(expected_escaped, linebreaks(input, { escape: true }));
20 | })
21 | testcase('truncate_html_words');
22 | test('should truncate strings without tags', function () {
23 | assertEquals('Joel is ...', truncate_html_words('Joel is a slug', 2));
24 | });
25 | test('should close tags on truncate', function () {
26 | assertEquals('
Joel is ...
', truncate_html_words('
Joel is a slug
', 2));
27 | });
28 | testcase('urlize')
29 | test('should urlize urls in text', function () {
30 | assertEquals(
31 | 'Check out www.djangoproject.com',
32 | urlize('Check out www.djangoproject.com')
33 | );
34 | assertEquals(
35 | 'Check out (www.djangoproject.com)',
36 | urlize('Check out (www.djangoproject.com)')
37 | );
38 | assertEquals(
39 | 'Skriv til test@test.se',
40 | urlize('Skriv til test@test.se')
41 | );
42 | assertEquals(
43 | 'Check out (www.djangoproject.com)\n' +
44 | 'Skriv til test@test.se',
45 | urlize('Check out (www.djangoproject.com)\nSkriv til test@test.se')
46 | );
47 | assertEquals(
48 | 'Check out www.djangopr...',
49 | urlize('Check out www.djangoproject.com', {limit: 15})
50 | );
51 | assertEquals(
52 | 'Se her: (www.dr.dk & ' +
53 | 'http://www.djangoproject.com)',
54 | urlize('Se her: (www.dr.dk & http://www.djangoproject.com)', { escape: true })
55 | );
56 | assertEquals(
57 | 'Se her: www.dr.dk?hest=4&test=tolv.',
58 | urlize('Se her: www.dr.dk?hest=4&test=tolv.', { escape: true })
59 | );
60 | assertEquals(
61 | 'Check out (www.djangoproject.com)',
62 | urlize('Check out (www.djangoproject.com)', { nofollow: true })
63 | );
64 | });
65 |
66 | run();
67 |
--------------------------------------------------------------------------------
/utils/tags.js:
--------------------------------------------------------------------------------
1 | /*jslint laxbreak: true, eqeqeq: true, undef: true, regexp: false */
2 | /*global require, process, exports */
3 |
4 |
5 | /* Function: get_args_from_token
6 | split token contents, remove the first part (the tagname) and return the
7 | rest. Optionally a set of rules to verify the arguments against can be
8 | provided.
9 |
10 | Syntax: get_args_from_token(token [,options])
11 |
12 | Arguments:
13 | token - the token
14 | options - optional, see options
15 |
16 | Options:
17 | argcount - number, verify that there is exactly this number of arguments
18 | exclude - mixed, a number or an array of numbers specifying arguments that should
19 | be excluded from the returned list
20 | mustbe - object, an object with numbers as keys and strings or arrays of strings as
21 | values. Arguments specified (by their number) in this object must match one
22 | of the values, if they are provided in the token.
23 |
24 | Example:
25 | // token contains "with 100 as hest"
26 | list = get_args_from_token(token, { exclude: 2 }); // list is [100, "hest"]
27 | list = get_args_from_token(token, { mustbe: { 2: "is" } }); // throws an error because "as" != "is"
28 | list = get_args_from_token(token, { argcount: 4 }); // throws an error because there is not 4 arguments
29 | */
30 | exports.get_args_from_token = function get_args_from_token(token, options) {
31 |
32 | options = options || {};
33 |
34 | var parts = token.split_contents();
35 |
36 | if (options.argcount !== undefined && parts.length !== options.argcount + 1) {
37 | throw 'unexpected syntax in "' + token.type + '" tag: Wrong number of arguments';
38 | }
39 |
40 | var i;
41 | for (i = 1; i < parts.length; i++) {
42 | if (options.mustbe && options.mustbe[i]) {
43 | var expected = options.mustbe[i];
44 | if (expected instanceof Array) {
45 | if (expected.indexOf(parts[i]) === -1) {
46 | throw 'unexpected syntax in "' + token.type + '" tag: Expected one of "' + expected.join('", "') + '"';
47 | }
48 | } else if (expected != parts[i]) {
49 | throw 'unexpected syntax in "' + token.type + '" tag: Expected "' + options.mustbe[i] + '"';
50 | }
51 | }
52 | }
53 |
54 | if (options.exclude) {
55 | if (!(options.exclude instanceof Array)) { options.exclude = [options.exclude] }
56 | var include = [];
57 | for (i = 1; i < parts.length; i++) {
58 | if (options.exclude.indexOf(i) === -1) { include.push(i); }
59 | }
60 | parts = include.map(function (x) { return parts[x]; });
61 | } else {
62 | parts = parts.slice(1);
63 | }
64 |
65 | return parts;
66 | }
67 |
68 |
69 | /* Function: simple_tag
70 | Creates a parsefunction for a simple tag. That is a tag that takes a
71 | number of arguments -- strings or a template variables -- and return a
72 | string after doing some processing based solely on the input argument
73 | and some external information.
74 |
75 | Syntax: simple_tag(node[, options]);
76 |
77 | Arguments:
78 | node - a function that returns a nodefunction when called with the tag arguments
79 | options - optional, passed on to get_args_from_token()
80 |
81 | Returns:
82 | a parsefunction
83 | */
84 | exports.simple_tag = function simple_tag(node, options) {
85 | return function (parser, token) {
86 | var parts = get_args_from_token(token, options);
87 | return node.apply(null, parts);
88 | };
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/utils/date.test.js:
--------------------------------------------------------------------------------
1 | process.mixin(GLOBAL, require('./test').dsl);
2 | process.mixin(GLOBAL, require('./date'));
3 | var sys = require('sys');
4 |
5 |
6 | testcase('date_format')
7 | test('should format each filter correctly', function () {
8 | var d = new Date(1981, 11, 2, 18, 31, 45, 123); // Random time on Britney Spears birthday :-)
9 | var tz = d.toString().substr(28, 5);
10 | assertEquals('p.m.', format_date(d, 'a'));
11 | assertEquals('PM', format_date(d, 'A'));
12 | assertEquals('dec', format_date(d, 'b'));
13 | shouldThrow(format_date, [d, 'B']);
14 | assertEquals('1981-12-02T18:31:45.123000', format_date(d, 'c'));
15 | assertEquals('02', format_date(d, 'd'));
16 | assertEquals('Wed', format_date(d, 'D'));
17 | assertEquals('6:31', format_date(d, 'f')); // x
18 | assertEquals('December', format_date(d, 'F'));
19 | assertEquals('6', format_date(d, 'g'));
20 | assertEquals('18', format_date(d, 'G')); //x
21 | assertEquals('06', format_date(d, 'h'));
22 | assertEquals('18', format_date(d, 'H')); // x
23 | assertEquals('31', format_date(d, 'i')); // x
24 | shouldThrow(format_date, [d, 'I']);
25 | assertEquals('Wednesday', format_date(d, 'l'));
26 | assertEquals('false', format_date(d, 'L'));
27 | assertEquals('12', format_date(d, 'm')); // x
28 | assertEquals('Dec', format_date(d, 'M'));
29 | assertEquals('12', format_date(d, 'n'));
30 | assertEquals('Dec.', format_date(d, 'N'));
31 | assertEquals(tz, format_date(d, 'O'));
32 |
33 | assertEquals('6:31 p.m.', format_date(d, 'P'));
34 | assertEquals('midnight', format_date(new Date(2000, 1, 1, 0, 0), 'P'));
35 | assertEquals('noon', format_date(new Date(2000, 1, 1, 12, 0), 'P'));
36 | assertEquals('6 a.m.', format_date(new Date(2000, 1, 1, 6, 0), 'P'));
37 |
38 | assertEquals('Wed, 2 Dec 1981 18:31:45 ' + tz, format_date(d, 'r'));
39 | assertEquals('45', format_date(d, 's')); // x
40 | assertEquals('nd', format_date(d, 'S')); // x (st, nd, rt or th)
41 |
42 | assertEquals('31', format_date(d, 't'));
43 | assertEquals('30', format_date(new Date(2000, 10, 3), 't'));
44 | assertEquals('29', format_date(new Date(2000, 1, 3), 't'));
45 | assertEquals('28', format_date(new Date(1999, 1, 3), 't'));
46 |
47 | assertEquals('GMT+0100', format_date(d, 'T')); // good enough for now...
48 | assertEquals('376162305', format_date(d, 'U'));
49 | assertEquals('3', format_date(d, 'w'));
50 | assertEquals('49', format_date(d, 'W'));
51 | assertEquals('81', format_date(d, 'y'));
52 | assertEquals('1981', format_date(d, 'Y'));
53 | assertEquals('336', format_date(d, 'z'));
54 | assertEquals(tz * -36 + "", format_date(d, 'Z'));
55 | });
56 |
57 | testcase('longer formats');
58 | test('l jS \\o\\f F Y h:i:s A', function () {
59 | var d = new Date(1981, 11, 2, 18, 31, 45, 123); // Random time on Britney Spears birthday :-)
60 | assertEquals('Wednesday 2nd of December 1981 06:31:45 PM', format_date(d, 'l jS \\o\\f F Y h:i:s A'));
61 | });
62 |
63 | testcase('timesince');
64 | test('correct results for known values', function () {
65 | var now = new Date("Wed Dec 02 1981 18:31:45 GMT+0100 (CET)"); // Random time on Britney Spears birthday :-)
66 |
67 | var date = new Date("Wed Dec 02 1981 15:15:45 GMT+0100 (CET)");
68 | assertEquals('3 hours, 16 minutes', timesince(date, now));
69 |
70 | date = new Date("Wed Nov 22 1981 15:15:45 GMT+0100 (CET)");
71 | assertEquals('1 week, 3 days', timesince(date, now));
72 |
73 | date = new Date("Sun Oct 19 1981 18:10:53 GMT+0100 (CET)");
74 | assertEquals('1 month, 2 weeks', timesince(date, now));
75 |
76 | date = new Date("Sat Dec 29 1970 04:52:13 GMT+0100 (CET)");
77 | assertEquals('10 years, 11 months', timesince(date, now));
78 |
79 | date = new Date("Wed Nov 13 1980 10:36:13 GMT+0100 (CET)");
80 | assertEquals('1 year', timesince(date, now));
81 |
82 | date = new Date("Wed Dec 02 1981 18:29:40 GMT+0100 (CET)"); // Random time on Britney Spears birthday :-)
83 | assertEquals('2 minutes', timesince(date, now));
84 |
85 | date = new Date("Wed Dec 02 1983 18:29:40 GMT+0100 (CET)"); // Random time on Britney Spears birthday :-)
86 | assertEquals('0 minutes', timesince(date, now));
87 | });
88 |
89 | run();
90 |
--------------------------------------------------------------------------------
/utils/html.js:
--------------------------------------------------------------------------------
1 | /*jslint laxbreak: true, eqeqeq: true, undef: true, regexp: false */
2 | /*global require, process, exports */
3 |
4 | var sys = require('sys');
5 |
6 | /* Function: escape(value);
7 | Escapes the characters &, <, >, ' and " in string with html entities.
8 | Arguments:
9 | value - string to escape
10 | */
11 | var escape = exports.escape = function (value) {
12 | return value
13 | .replace(/&/g, '&')
14 | .replace(//g, '>')
16 | .replace(/'/g, ''')
17 | .replace(/"/g, '&qout;');
18 | };
19 |
20 | /* Function: linebreaks(value, options);
21 | Converts newlines into
and s.
22 | Arguments:
23 | value - string, the string to convert.
24 | options - optional, see options
25 | Options:
26 | escape - boolean, if true pass the string through escape()
27 | onlybr - boolean, if true only br tags will be created.
28 | */
29 | var linebreaks = exports.linebreaks = function (value, options) {
30 | options = options || {};
31 | value = value.replace(/\r\n|\r|\n/g, '\n');
32 |
33 | if (options.onlybr) {
34 | return (options.escape ? escape(value) : value).replace(/\n/g, ' ');
35 | }
36 |
37 | var lines = value.split(/\n{2,}/);
38 | if (options.escape) {
39 | lines = lines.map( function (x) { return '
'; } );
42 | }
43 | return lines.join('\n\n');
44 | };
45 |
46 |
47 | var re_words = /&.*?;|<.*?>|(\w[\w\-]*)/g;
48 | var re_tag = /<(\/)?([^ ]+?)(?: (\/)| .*?)?>/;
49 | var html4_singlets = ['br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input'];
50 | var truncate_html_words = exports.truncate_html_words = function (input, cnt) {
51 | var words = 0, pos = 0, elipsis_pos = 0, length = cnt - 0;
52 | var open_tags = [];
53 |
54 | if (!length) { return ''; }
55 |
56 | re_words.lastIndex = 0;
57 |
58 | while (words <= length) {
59 | var m = re_words( input );
60 | if (!m) {
61 | // parsed through string
62 | break;
63 | }
64 |
65 | pos = re_words.lastIndex;
66 |
67 | if (m[1]) {
68 | // this is not a tag
69 | words += 1;
70 | if (words === length) {
71 | elipsis_pos = pos;
72 | }
73 | continue;
74 | }
75 |
76 | var tag = re_tag( m[0] );
77 | if (!tag || elipsis_pos) {
78 | // don't worry about non-tags or tags after truncate point
79 | continue;
80 | }
81 |
82 | var closing_tag = tag[1], tagname = tag[2].toLowerCase(), self_closing = tag[3];
83 | if (self_closing || html4_singlets.indexOf(tagname) > -1) {
84 | continue;
85 | } else if (closing_tag) {
86 | var idx = open_tags.indexOf(tagname);
87 | if (idx > -1) {
88 | // SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
89 | open_tags = open_tags.slice(idx + 1);
90 | }
91 | } else {
92 | open_tags.unshift( tagname );
93 | }
94 | }
95 |
96 | if (words <= length) {
97 | return input;
98 | }
99 | return open_tags.reduce( function (p,c) { return p + '' + c + '>'; }, input.slice(0, elipsis_pos) + ' ...');
100 | };
101 |
102 |
103 |
104 | var punctuation_re = /^((?:\(|<|<)*)(.*?)((?:\.|,|\)|>|\n|>)*)$/;
105 | var simple_email_re = /^\S+@[a-zA-Z0-9._\-]+\.[a-zA-Z0-9._\-]+$/;
106 |
107 | function trim_url(url, limit) {
108 | if (limit === undefined || limit > url.length) { return url; }
109 | return url.substr(0, limit - 3 > 0 ? limit - 3 : 0) + '...';
110 | }
111 |
112 | /* Function: urlize(text, options)
113 | Converts all urls found in text into links (URL).
114 | Arguments:
115 | text - string, the text to convert.
116 | options - optional, see options
117 | Options:
118 | escape - boolean, if true pass the string through escape()
119 | limit - number, if defined the shown urls will be truncated with '...' at this length
120 | nofollow - boolean, if true add rel="nofollow" to tags
121 | */
122 | function urlize(text, options) {
123 | options = options || {};
124 |
125 | var words = text.split(/(\s+)/g);
126 | var nofollow = options.nofollow ? ' rel="nofollow"' : '';
127 |
128 | words.forEach( function (word, i, words) {
129 | var match;
130 | if (word.indexOf('.') > -1 || word.indexOf('@') > -1 || word.indexOf(':') > -1) {
131 | match = punctuation_re(word);
132 | }
133 |
134 | if (match) {
135 | var url, lead = match[1], middle = match[2], trail = match[3];
136 | if (middle.substr(0,7) === 'http://' || middle.substr(0,8) === 'https://') {
137 | url = encodeURI(middle);
138 | } else if (middle.substr(0,4) === 'www.' || (
139 | middle.indexOf('@') === -1 && middle && middle[0].match(/[a-z0-9]/i) &&
140 | (middle.substr(-4) === '.org' || middle.substr(-4) === '.net' || middle.substr(-4) === '.com'))) {
141 | url = encodeURI('http://' + middle);
142 | } else if (middle.indexOf('@') > -1 && middle.indexOf(':') === -1 && simple_email_re(middle)) {
143 | url = 'mailto:' + middle;
144 | nofollow = '';
145 | }
146 |
147 | if (url) {
148 | var trimmed = trim_url(middle, options.limit);
149 | if (options.escape) {
150 | lead = escape(lead);
151 | trail = escape(trail);
152 | url = escape(url);
153 | trimmed = escape(trimmed);
154 | }
155 | middle = '' + trimmed + '';
156 | words[i] = lead + middle + trail;
157 | }
158 | } else if (options.escape) {
159 | words[i] = escape(word);
160 | }
161 | });
162 | return words.join('');
163 | }
164 |
165 | exports.urlize = urlize;
166 |
167 |
168 |
169 | /* Function: strip_spaces_between_tags
170 | Returns the given HTML with spaces between tags removed.
171 | Arguments:
172 | input: string, the html to process
173 | */
174 | exports.strip_spaces_between_tags = function (input) {
175 | return input.replace(/>\s+<');
176 | }
177 |
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/template/template.test.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | process.mixin(GLOBAL, require('../utils/test').dsl);
3 | process.mixin(GLOBAL, require('./template'));
4 |
5 | testcase('Test tokenizer');
6 | test('sanity test', function () {
7 | var tokens = tokenize('Hest');
8 | assertEquals([{'type': 'text', 'contents': 'Hest'}], tokens);
9 | });
10 | test('no empty tokens between tags', function () {
11 | var tokens = tokenize('{{tag}}');
12 | assertEquals( [{type:'variable', contents: 'tag'}], tokens );
13 | });
14 | test('split token contents', function () {
15 | assertEquals(
16 | ['virker', 'det', 'her'],
17 | tokenize(' virker det her ')[0].split_contents()
18 | );
19 | assertEquals(
20 | ['her', 'er', '"noget der er i qoutes"', 'og', 'noget', 'der', 'ikke', 'er'],
21 | tokenize('her er "noget der er i qoutes" og noget der ikke er')[0].split_contents()
22 | );
23 |
24 | assertEquals( ['date:"F j, Y"'], tokenize('date:"F j, Y"')[0].split_contents());
25 | assertEquals( ['date:', '"F j, Y"'], tokenize('date: "F j, Y"')[0].split_contents());
26 | });
27 |
28 | testcase('Filter Expression tests');
29 | test('should parse valid syntax', function () {
30 | assertEquals(
31 | { variable: 'item', filter_list: [ { name: 'add' } ] },
32 | new FilterExpression("item|add")
33 | );
34 | assertEquals(
35 | { variable: 'item.subitem', filter_list: [ { name: 'add' }, { name: 'sub' } ] },
36 | new FilterExpression("item.subitem|add|sub")
37 | );
38 | assertEquals(
39 | { variable: 'item', filter_list: [ { name: 'add', var_arg: 5 }, { name: 'sub', arg: "2" } ] },
40 | new FilterExpression('item|add:5|sub:"2"')
41 | );
42 | assertEquals(
43 | { variable: 'item', filter_list: [ { name: 'concat', arg: 'heste er naijs' } ] },
44 | new FilterExpression('item|concat:"heste er naijs"')
45 | );
46 | assertEquals(
47 | { variable: 'person_name', filter_list: [ ] },
48 | new FilterExpression('person_name')
49 | );
50 | assertEquals(
51 | { variable: 335, filter_list: [{name: 'test'}] },
52 | new FilterExpression('335|test')
53 | );
54 | assertEquals(
55 | { constant: "hest", filter_list: [{name: 'test'}] },
56 | new FilterExpression('"hest"|test')
57 | );
58 | assertEquals(
59 | { variable: "item", filter_list: [{name: 'add', var_arg: 'other' }] },
60 | new FilterExpression('item|add:other')
61 | );
62 | });
63 |
64 | test('should fail on invalid syntax', function () {
65 | function attempt(s) { return new FilterExpression(s); }
66 |
67 | shouldThrow(attempt, 'item |add:2');
68 | shouldThrow(attempt, 'item| add:2');
69 | shouldThrow(attempt, 'item|add :2');
70 | shouldThrow(attempt, 'item|add: 2');
71 | shouldThrow(attempt, 'item|add|:2|sub');
72 | shouldThrow(attempt, 'item|add:2 |sub');
73 | });
74 |
75 | test('output (without filters) should be escaped if autoescaping is on', function () {
76 | var context = new Context({test: '' };
102 |
103 | Rendering that will provide output like this:
104 |
105 | {{ str }}
106 | {{ str|safe }}
107 | {% autoescape off %}
108 | {{ str }}
109 | {% endautoescape %}
110 |
111 | The autoescaping in djangode follows the same rules as django templates - read
112 | in detail about it here:
113 |
114 | [Autoescaping](http://docs.djangoproject.com/en/1.1/topics/templates/#id2)
115 |
116 | Extending the template system
117 | -----------------------------
118 |
119 | Djangode supports (almost) all the Django default templates and filters, and
120 | they should cover most of what you need in your templates, however, there may
121 | be times when it is convenient or even neccessarry to implement your own tags
122 | or filters. In Djangode an extention package is simply any standard node.js
123 | module that exports the two objects tags and filters.
124 |
125 | Before you start making your own tags and filters you should read the Django
126 | documentation on the subject. Djangode is JavaScript, not Python, but even
127 | though things are different Djangode templates builds upon the same ideas and
128 | uses the same concepts as Django:
129 |
130 | [Extending the template system](http://docs.djangoproject.com/en/1.1/howto/custom-template-tags/)
131 |
132 | exports.filters = {
133 | firstletter: function (value, arg, safety) {
134 | return String(value)[0];
135 | }
136 | };
137 |
138 | exports.tags = {
139 | uppercase_all: function (parser, token) {
140 |
141 | var nodelist = parser.parse('enduppercase_all');
142 | parser.delete_first_token();
143 |
144 | return function (context, callback) {
145 | nodelist.evaluate(context, function (error, result) {
146 | if (error) {
147 | callback(error);
148 | } else {
149 | callback(false, result.toUpperCase());
150 | }
151 | });
152 | };
153 | }
154 | };
155 |
156 | Here's an example of a template that uses the above package (you will need to
157 | save the above module as tagtest.js and put it in a directory somewhere on your
158 | require path for this example to work):
159 |
160 | {% load tagtest %}
161 | {{ str|firstletter }}
162 | {% uppercase_all %}
163 | This text will all be uppercased.
164 | {% enduppercase_all %}
165 |
166 | ### Custom Filters ########
167 |
168 | Filters are javascript functions. They receive the value they should operate on
169 | an the filterarguments. The third argument (safety) is an object that describes
170 | the current safetystate of the value.
171 |
172 | function my_filter(value, args, safety) {
173 | return 'some_result';
174 | }
175 |
176 | Safety has two properties 'is_safe' and 'must_escape'. 'is_safe' tells wether
177 | or incoming value is considered safe, an therefore should not be escaped.
178 |
179 | If the value is not safe, the 'must_escape' property tells if it should be
180 | escaped, that is, if autoescaping is enabled or the escape filter has been
181 | applied to the current filterchain.
182 |
183 | If your filter will output any "unsafe" characters such as <, >, & or " it must
184 | escape the entire value if the string if the 'must_escape' property is enabled.
185 | When you have escaped the string, set safety.is_safe to true.
186 |
187 | If 'is_safe' is allready marked as true the value may already contain html
188 | characters that should not be escaped. If you cannot correctly handle that your
189 | filter should fail and return an empty string.
190 |
191 | function my_filter(value, args, safety) {
192 |
193 | /* do processing */
194 |
195 | html = require('./utils/html');
196 |
197 | if (!safety.is_safe && safety.must_escape) {
198 | result = html.escape(result);
199 | safety.is_safe = true;
200 | }
201 | return result;
202 | }
203 |
204 | If your filter does not output any "unsafe" characters, you can completely
205 | disregard the safety argument.
206 |
207 | ### Custom Tags ########
208 |
209 | Tags are called with a parser and and token argument. The token represents the
210 | tag itself (or the opening tag) an contains the tagname and the tags arguments.
211 | Use the token.split_contents() function to split the arguments into words. The
212 | split_contents() function is smart enough not to split qouted values.
213 |
214 | // {% my_tag is "the best" %}
215 | function (parser, token) {
216 | token.type // 'my_tag'
217 | token.contents // 'my_tag is "the best"'
218 | token.split_contents() // ['my_tag', 'is', 'the best'];
219 |
220 | // see the utils/tags module for some helperfunctions for this parsing and verifying tokens.
221 | }
222 |
223 | Your tag function must return a function that takes a context and a callback as
224 | arguments. This function is called when the node is rendered, and when you have
225 | generated the tags output, you must call the callback function - it takes an
226 | error state as it's first argument and the result of the node as it's second.
227 |
228 | function (parser, token) {
229 | // ... parse token ...
230 | return function (context, callback) {
231 | context.push(); // push a new scope level onto the scope stack.
232 | // any variables set in the context will have precedence over
233 | // similarly names variables on lower scope levels
234 |
235 | context.set('name', 15); // set the variable "name" to 15
236 |
237 | var variable = context.get('variable'); // get the value of "variable"
238 |
239 | context.pop(); // pop the topmost scope level off the scope stack.
240 |
241 | callback(false, result); // complete rendering by calling the callback with the result of the node
242 | }
243 | }
244 |
245 | If you want to create a tag that contains other tags (like the uppercase_all
246 | tag above) you must use the parser to parse them:
247 |
248 | function (parser, token) {
249 | // parses until it reaches an 'endfor'-tag or an 'else'-tag
250 | var node_list = parser.parse('endfor', 'else');
251 |
252 | // parsing stops at the tag it reaches, so use this to get rid of it before returning.
253 | parser.delete_first_token();
254 |
255 | var next_token = parser.next_token(); // parse a single token.
256 |
257 | return function (context, callback) {
258 | node_list.evaluate(context, function (error, result) {
259 | // process the result of the evaluated nodelist an return by calling callback().
260 | // remember to check the error flag!
261 | }
262 | }
263 | }
264 |
265 | Other differences from Django
266 | -----------------------------
267 |
268 | ### Cycle tag ########
269 |
270 | The cycle tag does not support the legacy notation {% cycle row1,row2,row3 %}.
271 | Use the new and improved syntax described in the django docs:
272 |
273 | [Documentation for the cycle tag](http://docs.djangoproject.com/en/1.1/ref/templates/builtins/#cycle)
274 |
275 | ### Stringformat filter #######
276 |
277 | The stringformat filter is based on Alexandru Marasteanu's sprintf() function
278 | and it behaves like regular C-style sprintf. Django uses Pythons sprintf
279 | function, and it has some (very few) nonstandard extensions. These are not
280 | supported by the djangode tag. Most of the time you won't have any problems
281 | though.
282 |
283 | [sprintf() on Google Code](http://code.google.com/p/sprintf/)
284 |
285 | ### Url Tag #########
286 |
287 | The url tag only supports named urls, and you have to register them with the
288 | templatesystem before you can use them by assigning your url's to the special
289 | variable process.djangode_urls, like this:
290 |
291 | var app = dj.makeApp([
292 | ['^/item/(\w+)/(\d+)$', function (req, res) { /* ... */ }, 'item']
293 | ]);
294 | dj.serve(app, 8000);
295 | process.djangode_urls = app.urls;
296 |
297 | Then you can use the url tag in any template:
298 |
299 | {% url "item" 'something',27 %}
300 |
301 | Like in django you can also store the url in a variable and use it later in the
302 | site.
303 |
304 | {% url "item" 'something',27 as the_url %}
305 | This is a link to {{ the_url }}
306 |
307 | Read more about the url tag here:
308 |
309 | [Django documentation for url tag](http://docs.djangoproject.com/en/1.1/ref/templates/builtins/#url)
310 |
311 | ### Unsupported Tags and Filters ########
312 |
313 | The plan is to support all Django tags and filters, but currently the filters
314 | iriencode and unordered_list and the tags ssi and debug are not supported.
315 |
316 |
--------------------------------------------------------------------------------
/template/template.js:
--------------------------------------------------------------------------------
1 | /*jslint laxbreak: true, eqeqeq: true, undef: true, regexp: false */
2 | /*global require, process, exports */
3 |
4 | var sys = require('sys');
5 | var string_utils = require('../utils/string');
6 | var html = require('../utils/html');
7 | var iter = require('../utils/iter');
8 |
9 | function normalize(value) {
10 | if (typeof value !== 'string') { return value; }
11 |
12 | if (value === 'true') { return true; }
13 | if (value === 'false') { return false; }
14 | if (/^\d/.exec(value)) { return value - 0; }
15 |
16 | var isStringLiteral = /^(["'])(.*?)\1$/.exec(value);
17 | if (isStringLiteral) { return isStringLiteral.pop(); }
18 |
19 | return value;
20 | }
21 |
22 | /***************** TOKEN **********************************/
23 |
24 | function Token(type, contents) {
25 | this.type = type;
26 | this.contents = contents;
27 | }
28 |
29 | process.mixin(Token.prototype, {
30 | split_contents: function () {
31 | return string_utils.smart_split(this.contents);
32 | }
33 | });
34 |
35 | /***************** TOKENIZER ******************************/
36 |
37 | function tokenize(input) {
38 | var re = /(?:\{\{|\}\}|\{%|%\})|[\{\}|]|[^\{\}%|]+/g;
39 | var token_list = [];
40 |
41 | function consume(re, input) {
42 | var m = re.exec(input);
43 | return m ? m[0] : null;
44 | }
45 |
46 | function consume_until() {
47 | var next, s = '';
48 | var slice = Array.prototype.slice;
49 | while (next = consume(re, input)) {
50 | if (slice.apply(arguments).indexOf(next) > -1) {
51 | return [s, next];
52 | }
53 | s += next;
54 | }
55 | return [s];
56 | }
57 |
58 | function literal() {
59 | var res = consume_until("{{", "{%");
60 |
61 | if (res[0]) { token_list.push( new Token('text', res[0]) ); }
62 |
63 | if (res[1] === "{{") { return variable_tag; }
64 | if (res[1] === "{%") { return template_tag; }
65 | return undefined;
66 | }
67 |
68 | function variable_tag() {
69 | var res = consume_until("}}");
70 |
71 | if (res[0]) { token_list.push( new Token('variable', res[0].trim()) ); }
72 | if (res[1]) { return literal; }
73 | return undefined;
74 | }
75 |
76 | function template_tag() {
77 | var res = consume_until("%}"),
78 | parts = res[0].trim().split(/\s/, 1);
79 |
80 | token_list.push( new Token(parts[0], res[0].trim()) );
81 |
82 | if (res[1]) { return literal; }
83 | return undefined;
84 | }
85 |
86 | var state = literal;
87 |
88 | while (state) {
89 | state = state();
90 | }
91 |
92 | return token_list;
93 | }
94 |
95 | /*********** FilterExpression **************************/
96 |
97 | // groups are: 1=variable, 2=constant, 3=filter_name, 4=filter_constant_arg, 5=filter_variable_arg
98 | var filter_re = /("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')|([\w\.]+|[\-+\.]?\d[\d\.e]*)|(?:\|(\w+)(?::(?:("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')|([\w\.]+|[\-+\.]?\d[\d\.e]*)))?)/g;
99 |
100 | var FilterExpression = function (expression, constant) {
101 |
102 | filter_re.lastIndex = 0;
103 |
104 | this.filter_list = [];
105 |
106 | var parsed = this.consume(expression);
107 |
108 | //sys.debug(expression + ' => ' + sys.inspect( parsed ) );
109 |
110 | if (!parsed) {
111 | throw this.error(expression);
112 | }
113 | if (constant !== undefined) {
114 | if (parsed.variable !== undefined || parsed.constant !== undefined) {
115 | throw this.error(expression + ' - did not expect variable when constant is defined');
116 | }
117 | parsed.constant = constant;
118 | }
119 |
120 | while (parsed) {
121 | if (parsed.constant !== undefined && parsed.variable !== undefined) {
122 | throw this.error(expression + ' - did not expect both variable and constant');
123 | }
124 | if ((parsed.constant !== undefined || parsed.variable !== undefined) &&
125 | (this.variable !== undefined || this.constant !== undefined)) {
126 | throw this.error(expression + ' - did not expect variable or constant at this point');
127 | }
128 | if (parsed.constant !== undefined) { this.constant = normalize(parsed.constant); }
129 | if (parsed.variable !== undefined) { this.variable = normalize(parsed.variable); }
130 |
131 | if (parsed.filter_name) {
132 | this.filter_list.push( this.make_filter_token(parsed) );
133 | }
134 |
135 | parsed = this.consume(expression);
136 |
137 | //sys.debug(expression + ' => ' + sys.inspect( parsed ) );
138 | }
139 |
140 | //sys.debug(expression + ' => ' + sys.inspect( this ) );
141 |
142 | };
143 |
144 | process.mixin(FilterExpression.prototype, {
145 |
146 | consume: function (expression) {
147 | var m = filter_re.exec(expression);
148 | return m ?
149 | { constant: m[1], variable: m[2], filter_name: m[3], filter_arg: m[4], filter_var_arg: m[5] }
150 | : null;
151 | },
152 |
153 | make_filter_token: function (parsed) {
154 | var token = { name: parsed.filter_name };
155 | if (parsed.filter_arg !== undefined) { token.arg = normalize(parsed.filter_arg); }
156 | if (parsed.filter_var_arg !== undefined) { token.var_arg = normalize(parsed.filter_var_arg); }
157 | return token;
158 | },
159 |
160 | error: function (s) {
161 | throw s + "\ncan't parse filterexception at char " + filter_re.lastIndex + ". Make sure there is no spaces between filters or arguments\n";
162 | },
163 |
164 | resolve: function (context) {
165 | var value;
166 | if (this.hasOwnProperty('constant')) {
167 | value = this.constant;
168 | } else {
169 | value = context.get(this.variable);
170 | }
171 |
172 | var safety = {
173 | is_safe: false,
174 | must_escape: context.autoescaping
175 | };
176 |
177 | var out = this.filter_list.reduce( function (p,c) {
178 |
179 | var filter = context.filters[c.name];
180 |
181 | var arg;
182 | if (c.arg) {
183 | arg = c.arg;
184 | } else if (c.var_arg) {
185 | arg = context.get(c.var_arg);
186 | }
187 |
188 | if ( filter && typeof filter === 'function') {
189 | return filter(p, arg, safety);
190 | } else {
191 | // throw 'Cannot find filter';
192 | sys.debug('Cannot find filter ' + c.name);
193 | return p;
194 | }
195 | }, value);
196 |
197 | if (safety.must_escape && !safety.is_safe) {
198 | if (typeof out === 'string') {
199 | return html.escape(out);
200 | } else if (out instanceof Array) {
201 | return out.map( function (o) { return typeof o === 'string' ? html.escape(o) : o; } );
202 | }
203 | }
204 | return out;
205 | }
206 | });
207 |
208 | /*********** PARSER **********************************/
209 |
210 | function Parser(input) {
211 | this.token_list = tokenize(input);
212 | this.indent = 0;
213 | this.blocks = {};
214 |
215 | var defaults = require('./template_defaults');
216 | this.tags = defaults.tags;
217 | this.nodes = defaults.nodes;
218 | }
219 |
220 | function parser_error(e) {
221 | return 'Parsing exception: ' + JSON.stringify(e, 0, 2);
222 | }
223 |
224 | function make_nodelist() {
225 | var node_list = [];
226 | node_list.evaluate = function (context, callback) {
227 | iter.reduce(this, function (p, c, idx, list, next) {
228 | c(context, function (error, result) { next(error, p + result); });
229 | }, '', callback);
230 | };
231 | node_list.only_types = function (/*args*/) {
232 | var args = Array.prototype.slice.apply(arguments);
233 | return this.filter( function (x) { return args.indexOf(x.type) > -1; } );
234 | };
235 | node_list.append = function (node, type) {
236 | node.type = type;
237 | this.push(node);
238 | };
239 | return node_list;
240 | }
241 |
242 | process.mixin(Parser.prototype, {
243 |
244 | parse: function () {
245 |
246 | var stoppers = Array.prototype.slice.apply(arguments);
247 | var node_list = make_nodelist();
248 | var token = this.token_list[0];
249 | var tag = null;
250 |
251 | //sys.debug('' + this.indent++ + ':starting parsing with stoppers ' + stoppers.join(', '));
252 |
253 | while (this.token_list.length) {
254 | if (stoppers.indexOf(this.token_list[0].type) > -1) {
255 | //sys.debug('' + this.indent-- + ':parse done returning at ' + token[0] + ' (length: ' + node_list.length + ')');
256 | return node_list;
257 | }
258 |
259 | token = this.next_token();
260 |
261 | //sys.debug('' + this.indent + ': ' + token);
262 |
263 | tag = this.tags[token.type];
264 | if (tag && typeof tag === 'function') {
265 | node_list.append( tag(this, token), token.type );
266 | } else {
267 | //throw parser_error('Unknown tag: ' + token[0]);
268 | node_list.append(
269 | this.nodes.TextNode('[[ UNKNOWN ' + token.type + ' ]]'),
270 | 'UNKNOWN'
271 | );
272 | }
273 | }
274 | if (stoppers.length) {
275 | throw new parser_error('Tag not found: ' + stoppers.join(', '));
276 | }
277 |
278 | //sys.debug('' + this.indent-- + ':parse done returning end (length: ' + node_list.length + ')');
279 |
280 | return node_list;
281 | },
282 |
283 | next_token: function () {
284 | return this.token_list.shift();
285 | },
286 |
287 | delete_first_token: function () {
288 | this.token_list.shift();
289 | },
290 |
291 | make_filterexpression: function (expression, constant) {
292 | return new FilterExpression(expression, constant);
293 | }
294 |
295 | });
296 |
297 | /*************** Context *********************************/
298 |
299 | function Context(o) {
300 | this.scope = [ o || {} ];
301 | this.extends = '';
302 | this.blocks = {};
303 | this.autoescaping = true;
304 | this.filters = require('./template_defaults').filters;
305 | }
306 |
307 | process.mixin(Context.prototype, {
308 | get: function (name) {
309 |
310 | if (typeof name !== 'string') { return name; }
311 |
312 | var normalized = normalize(name);
313 | if (name !== normalized) { return normalized; }
314 |
315 | var parts = name.split('.');
316 | name = parts.shift();
317 |
318 | var val, level, next;
319 | for (level = 0; level < this.scope.length; level++) {
320 | if (this.scope[level].hasOwnProperty(name)) {
321 | val = this.scope[level][name];
322 | while (parts.length && val) {
323 | next = val[parts.shift()];
324 |
325 | if (typeof next === 'function') {
326 | val = next.apply(val);
327 | } else {
328 | val = next;
329 | }
330 | }
331 |
332 | if (typeof val === 'function') {
333 | return val();
334 | } else {
335 | return val;
336 | }
337 | }
338 | }
339 |
340 | return '';
341 | },
342 | set: function (name, value) {
343 | this.scope[0][name] = value;
344 | },
345 | push: function (o) {
346 | this.scope.unshift(o || {});
347 | },
348 | pop: function () {
349 | return this.scope.shift();
350 | },
351 | });
352 |
353 |
354 | /*********** Template **********************************/
355 |
356 | function Template(input) {
357 | var parser = new Parser(input);
358 | this.node_list = parser.parse();
359 | }
360 |
361 | process.mixin(Template.prototype, {
362 | render: function (o, callback) {
363 |
364 | if (!callback) { throw 'template.render() must be called with a callback'; }
365 |
366 | var context = (o instanceof Context) ? o : new Context(o || {});
367 | context.extends = '';
368 |
369 | this.node_list.evaluate(context, function (error, rendered) {
370 | if (error) { return callback(error); }
371 |
372 | if (context.extends) {
373 | var template_loader = require('./loader');
374 | template_loader.load_and_render(context.extends, context, callback);
375 | } else {
376 | callback(false, rendered);
377 | }
378 | });
379 | }
380 | });
381 |
382 | /********************************************************/
383 |
384 | exports.parse = function (input) {
385 | return new Template(input);
386 | };
387 |
388 |
389 | // exported for test
390 | exports.Context = Context;
391 | exports.FilterExpression = FilterExpression;
392 | exports.tokenize = tokenize;
393 | exports.make_nodelist = make_nodelist;
394 |
395 |
396 |
397 |
398 |
--------------------------------------------------------------------------------
/template/template_defaults.tags.test.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys');
2 | var fs = require('fs');
3 | var template = require('./template');
4 |
5 | process.mixin(GLOBAL, require('../utils/test').dsl);
6 | process.mixin(GLOBAL, require('./template_defaults'));
7 |
8 | function write_file(path, content) {
9 | var file = fs.openSync(path, process.O_WRONLY | process.O_TRUNC | process.O_CREAT, 0666);
10 | fs.writeSync(file, content);
11 | fs.closeSync(file);
12 | }
13 |
14 | function make_parse_and_execute_test(expected, tpl, name) {
15 |
16 | name = name ||
17 | 'should parse "' + (tpl.length < 40 ? tpl : tpl.slice(0, 37) + ' ...') + '"' ;
18 |
19 | test_async(name, function (testcontext, complete) {
20 | var parsed = template.parse(tpl);
21 | parsed.render(testcontext.obj, function (error, actual) {
22 | if (error) {
23 | fail( error, complete );
24 | } else {
25 | assertEquals(expected, actual, complete);
26 | }
27 | end_async_test( complete );
28 | });
29 | });
30 | }
31 |
32 | testcase('fornode')
33 | setup( function () { return { obj: { items: [ 1,2,3,4 ], noitems: [] } }; });
34 | make_parse_and_execute_test(' 1 2 3 4 ', '{% for item in items %} {{ item }} {% endfor %}');
35 | make_parse_and_execute_test('hest',
36 | '{% for item in notitems %} {{ item }} {% empty %}hest{% endfor %}');
37 |
38 | testcase('variable')
39 | setup( function () {
40 | return {
41 | obj: {
42 | num: 18,
43 | str: 'hest',
44 | bool: false,
45 | list: [1,2,'giraf',4],
46 | func: function () { return 'tobis'; },
47 | obj: { a: 1, b: 2, c: { d: 23, e: { f: 'laks' } } },
48 | qstr: '"hest"'
49 | }
50 | };
51 | });
52 |
53 | make_parse_and_execute_test('100', '{{ 100 }}');
54 | make_parse_and_execute_test('18', '{{ num }}');
55 | make_parse_and_execute_test('hest', '{{ str }}');
56 | make_parse_and_execute_test('tobis', '{{ func }}');
57 | make_parse_and_execute_test('false', '{{ bool }}');
58 | make_parse_and_execute_test('1,2,giraf,4', '{{ list }}');
59 | make_parse_and_execute_test('1', '{{ obj.a }}');
60 | make_parse_and_execute_test('2', '{{ obj.b }}');
61 | make_parse_and_execute_test('laks', '{{ obj.c.e.f }}');
62 | make_parse_and_execute_test('', '{{ nonexisting }}');
63 | make_parse_and_execute_test('&qout;hest&qout;', '{{ qstr }}');
64 | make_parse_and_execute_test('HEST', '{{ "hest"|upper }}');
65 | make_parse_and_execute_test('16', '{{ 10|add:"6" }}');
66 | make_parse_and_execute_test('0', '{{ 6|add:6|add:"-12" }}');
67 |
68 |
69 | testcase('ifnode')
70 | setup(function () { return { obj: {a: true, b: false }}; });
71 |
72 | make_parse_and_execute_test('hest', '{% if a %}hest{% endif %}');
73 | make_parse_and_execute_test('', '{% if b %}hest{% endif %}');
74 | make_parse_and_execute_test('hest', '{% if not b %}hest{% endif %}');
75 | make_parse_and_execute_test('laks', '{% if b %}hest{% else %}laks{% endif %}');
76 | make_parse_and_execute_test('hest', '{% if not b and a %}hest{% endif %}');
77 | make_parse_and_execute_test('hest', '{% if a or b %}hest{% endif %}');
78 | make_parse_and_execute_test('hest', '{% if b or a %}hest{% endif %}');
79 |
80 | testcase('textnode')
81 | make_parse_and_execute_test('heste er gode laks', 'heste er gode laks');
82 |
83 | testcase('comment')
84 | make_parse_and_execute_test('', '{% comment %} do not parse {% hest %} any of this{% endcomment %}');
85 |
86 | testcase('cycle')
87 | setup(function () { return { obj: { c: 'C', items: [1,2,3,4,5,6,7,8,9] }}; });
88 | make_parse_and_execute_test('a1 b2 C3 a4 b5 C6 a7 b8 C9 ',
89 | '{% for item in items %}{% cycle \'a\' "b" c %}{{ item }} {% endfor %}');
90 |
91 | make_parse_and_execute_test('a H b J c H a',
92 | '{% cycle "a" "b" "c" as tmp %} {% cycle "H" "J" as tmp2 %} ' +
93 | '{% cycle tmp %} {% cycle tmp2 %} {% cycle tmp %} {% cycle tmp2 %} {%cycle tmp %}',
94 | 'should work with as tag'
95 | );
96 |
97 | testcase('filter')
98 | make_parse_and_execute_test(
99 | 'this text will be html-escaped & will appear in all lowercase.',
100 | '{% filter force_escape|lower %}' +
101 | 'This text will be HTML-escaped & will appear in all lowercase.{% endfilter %}'
102 | );
103 |
104 | testcase('block and extend')
105 | setup(function () {
106 | write_file('/tmp/block_test_1.html', 'Joel is a slug');
107 | write_file('/tmp/block_test_2.html', 'Her er en dejlig {% block test %}hest{% endblock %}.');
108 | write_file('/tmp/block_test_3.html',
109 | '{% block test1 %}hest{% endblock %}.'
110 | + '{% block test2 %} noget {% endblock %}'
111 | );
112 | write_file('/tmp/block_test_4.html',
113 | '{% extends "block_test_3.html" %}'
114 | + '{% block test1 %}{{ block.super }}{% block test3 %}{% endblock %}{% endblock %}'
115 | + '{% block test2 %} Et cirkus{{ block.super }}{% endblock %}'
116 | );
117 |
118 | var template_loader = require('./loader');
119 | template_loader.flush();
120 | template_loader.set_path('/tmp');
121 |
122 | return { obj: { parent: 'block_test_2.html' } };
123 | })
124 |
125 | make_parse_and_execute_test('her er en hestGiraf',
126 | '{% block test %}{% filter lower %}HER ER EN HEST{% endfilter %}Giraf{% endblock %}',
127 | 'block should parse and evaluate');
128 |
129 | make_parse_and_execute_test('Joel is a slug',
130 | '{% extends "block_test_1.html" %}',
131 | 'extend should parse and evaluate (without blocks)');
132 |
133 | make_parse_and_execute_test('Her er en dejlig giraf.',
134 | '{% extends "block_test_2.html" %}{% block test %}giraf{% endblock %}',
135 | 'block should override block in extend');
136 |
137 | make_parse_and_execute_test('Her er en dejlig hestgiraf.',
138 | '{% extends "block_test_2.html" %}{% block test %}{{ block.super }}giraf{% endblock %}',
139 | 'block.super variable should work');
140 |
141 | make_parse_and_execute_test('hestgiraf. Et cirkus noget tre',
142 | '{% extends "block_test_4.html" %}'
143 | + '{% block test2 %}{{ block.super }}tre{% endblock %}'
144 | + '{% block test3 %}giraf{% endblock %}',
145 | 'more than two levels');
146 |
147 | make_parse_and_execute_test('Her er en dejlig hestgiraf.',
148 | '{% extends parent %}{% block test %}{{ block.super }}giraf{% endblock %}',
149 | 'extend with variable key');
150 |
151 | // TODO: tests to specify behavior when blocks are name in subview but not parent
152 |
153 | testcase('autoescape')
154 | setup(function () { return { obj: {test: '', null, {})
128 | );
129 | });
130 | test('string should be marked as safe', function () {
131 | var safety = {};
132 | filters.force_escape('', null, safety)
133 | assertEquals(true, safety.is_safe);
134 | });
135 |
136 | testcase('get_digit');
137 | test('should get correct digit', function () {
138 | assertEquals(2, filters.get_digit(987654321, 2));
139 | assertEquals('987654321', filters.get_digit('987654321', 2));
140 | assertEquals('hest', filters.get_digit('hest'), 2);
141 | assertEquals(123, filters.get_digit(123), 5);
142 | assertEquals(123, filters.get_digit(123), 0);
143 | });
144 |
145 | testcase('join filter')
146 | test('should join list', function () {
147 | assertEquals('1, 2, 3, 4', filters.join([1,2,3,4], ', '));
148 | assertEquals('', filters.join('1,2,3,4', ', '));
149 | });
150 | testcase('last filter')
151 | test('should return last', function () {
152 | assertEquals('d', filters.last(['a', 'b', 'c', 'd']));
153 | assertEquals('', filters.last([]));
154 | assertEquals('', filters.last('hest'));
155 | });
156 | testcase('length filter')
157 | test('should return correct length', function () {
158 | assertEquals(5, filters.length([1,2,3,4,5]));
159 | assertEquals(4, filters.length('hest'));
160 | assertEquals(0, filters.length(16));
161 | });
162 | testcase('length_is filter')
163 | test('should return true on correct length', function () {
164 | assertEquals(true, filters.length_is([1,2,3,4,5], 5));
165 | assertEquals(true, filters.length_is('hest', 4));
166 | });
167 | test('should return false on incorrect length or bad arguments', function () {
168 | assertEquals(false, filters.length_is([1,2,3,4,5], 2));
169 | assertEquals(false, filters.length_is('hest', 16));
170 | assertEquals(false, filters.length_is(16, 4));
171 | assertEquals(false, filters.length_is('hest'));
172 | });
173 | testcase('linebreaks')
174 | test('linebreaks should be converted to
and tags.', function () {
175 | assertEquals('
Joel is a slug
', filters.linebreaks('Joel\nis a slug', null, {}));
176 | });
177 | test('string should be marked as safe', function () {
178 | var safety = {};
179 | filters.linebreaks('Joel\nis a slug', null, safety)
180 | assertEquals(true, safety.is_safe);
181 | });
182 | test('string should be escaped if requsted', function () {
183 | var safety = { must_escape: true };
184 | var actual = filters.linebreaks('Two is less than three\n2 < 3', null, safety)
185 | assertEquals('
Two is less than three 2 < 3
', actual)
186 | });
187 | testcase('linebreaksbr')
188 | test('linebreaks should be converted to tags.', function () {
189 | assertEquals('Joel is a slug. For sure...',
190 | filters.linebreaksbr('Joel\nis a slug.\nFor sure...', null, {})
191 | );
192 | });
193 | test('string should be marked as safe', function () {
194 | var safety = {};
195 | filters.linebreaksbr('Joel\nis a slug', null, safety)
196 | assertEquals(true, safety.is_safe);
197 | });
198 | test('string should be escaped if requsted', function () {
199 | var safety = { must_escape: true };
200 | var actual = filters.linebreaksbr('Two is less than three\n2 < 3', null, safety)
201 | assertEquals('Two is less than three 2 < 3', actual)
202 | });
203 | testcase('linenumbers')
204 | test('should add linenumbers to text', function () {
205 |
206 | var s = "But I must explain to you how all this mistaken idea of\n"
207 | + "denouncing pleasure and praising pain was born and I will\n"
208 | + "give you a complete account of the system, and expound the\n"
209 | + "actual teachings of the great explorer of the truth, the \n"
210 | + "aster-builder of human happiness. No one rejects, dislikes,\n"
211 | + "or avoids pleasure itself, because it is pleasure, but because\n"
212 | + "those who do not know how to pursue pleasure rationally\n"
213 | + "encounter consequences that are extremely painful. Nor again\n"
214 | + "is there anyone who loves or pursues or desires to obtain pain\n"
215 | + "of itself, because it is pain, but because occasionally\n"
216 | + "circumstances occur in which toil and pain can procure him\n"
217 | + "some great pleasure. To take a trivial example, which of us"
218 |
219 | var expected = "01. But I must explain to you how all this mistaken idea of\n"
220 | + "02. denouncing pleasure and praising pain was born and I will\n"
221 | + "03. give you a complete account of the system, and expound the\n"
222 | + "04. actual teachings of the great explorer of the truth, the \n"
223 | + "05. aster-builder of human happiness. No one rejects, dislikes,\n"
224 | + "06. or avoids pleasure itself, because it is pleasure, but because\n"
225 | + "07. those who do not know how to pursue pleasure rationally\n"
226 | + "08. encounter consequences that are extremely painful. Nor again\n"
227 | + "09. is there anyone who loves or pursues or desires to obtain pain\n"
228 | + "10. of itself, because it is pain, but because occasionally\n"
229 | + "11. circumstances occur in which toil and pain can procure him\n"
230 | + "12. some great pleasure. To take a trivial example, which of us"
231 |
232 | assertEquals(expected, filters.linenumbers(s, null, {}));
233 | });
234 | test('string should be marked as safe', function () {
235 | var safety = {};
236 | filters.linenumbers('Joel\nis a slug', null, safety)
237 | assertEquals(true, safety.is_safe);
238 | });
239 | test('string should be escaped if requsted', function () {
240 | var safety = { must_escape: true };
241 | var actual = filters.linenumbers('Two is less than three\n2 < 3', null, safety)
242 | assertEquals('1. Two is less than three\n2. 2 < 3', actual)
243 | });
244 | testcase('ljust')
245 | test('should left justify value i correctly sized field', function () {
246 | assertEquals('hest ', filters.ljust('hest', 10));
247 | assertEquals('', filters.ljust('hest'));
248 | assertEquals('he', filters.ljust('hest', 2));
249 | });
250 | testcase('lower')
251 | test('should lowercase value', function () {
252 | assertEquals('somewhere over the rainbow', filters.lower('Somewhere Over the Rainbow'));
253 | assertEquals('', filters.lower(19));
254 | });
255 | testcase('make_list');
256 | test('work as expected', function () {
257 | assertEquals(['J', 'o', 'e', 'l'], filters.make_list('Joel'));
258 | assertEquals(['1', '2', '3'], filters.make_list('123'));
259 | });
260 | testcase('phone2numeric')
261 | test('convert letters to numbers phone number style', function () {
262 | assertEquals('800-2655328', filters.phone2numeric('800-COLLECT'));
263 | assertEquals('2223334445556667q77888999z', filters.phone2numeric('abcdefghijklmnopqrstuvwxyz'));
264 | });
265 | testcase('pluralize');
266 | test('pluralize correctly', function() {
267 | assertEquals('', filters.pluralize('sytten'));
268 | assertEquals('', filters.pluralize(1));
269 | assertEquals('s', filters.pluralize(2));
270 | assertEquals('', filters.pluralize(1, 'es'));
271 | assertEquals('es', filters.pluralize(2, 'es'));
272 | assertEquals('y', filters.pluralize(1, 'y,ies'));
273 | assertEquals('ies', filters.pluralize(2, 'y,ies'));
274 | });
275 | testcase('pprint');
276 | test("should not throw and not return ''", function () {
277 | var response = filters.pprint( filters );
278 | if (!response) { fail('response is empty!'); }
279 | });
280 | testcase('random');
281 | // TODO: The testcase for random is pointless and should be improved
282 | test('should return an element from the list', function () {
283 | var arr = ['h', 'e', 's', 't'];
284 | var response = filters.random(arr);
285 | if (arr.indexOf(response) < 0) {
286 | fail('returned element not in array!');
287 | }
288 | });
289 | test('should return empty string when passed non array', function () {
290 | assertEquals('', filters.random( 25 ));
291 | });
292 | testcase('removetags');
293 | test('should remove tags', function () {
294 | assertEquals('Joel a slug',
295 | filters.removetags('Joel a slug', 'b span', {}));
296 | });
297 | test('string should be marked as safe', function () {
298 | var safety = {};
299 | filters.removetags('Joel a slug', 'b span', safety);
300 | assertEquals(true, safety.is_safe);
301 | });
302 | testcase('rjust')
303 | test('should right justify value in correctly sized field', function () {
304 | assertEquals(' hest', filters.rjust('hest', 10));
305 | assertEquals('', filters.rjust('hest'));
306 | assertEquals('he', filters.rjust('hest', 2));
307 | });
308 | testcase('slice')
309 | var arr = [0,1,2,3,4,5,6,7,8,9];
310 | test('slice should slice like python', function () {
311 | assertEquals([0,1,2,3], filters.slice(arr, ":4"));
312 | assertEquals([6,7,8,9], filters.slice(arr, "6:"));
313 | assertEquals([2,3,4], filters.slice(arr, "2:5"));
314 | assertEquals([2,5,8], filters.slice(arr, "2::3"));
315 | assertEquals([2,5], filters.slice(arr, "2:6:3"));
316 | });
317 | test('slice should handle bad values', function () {
318 | assertEquals([], filters.slice(36, ":4"));
319 | assertEquals([0,1,2,3,4,5,6,7,8,9], filters.slice(arr, 'hest'));
320 | assertEquals([0,1,2,3,4,5,6,7,8,9], filters.slice(arr));
321 | });
322 | testcase('slugify');
323 | test('should slugify correctly', function () {
324 | assertEquals('joel-is-a-slug', filters.slugify('Joel is a slug'));
325 | assertEquals('s-str-verden-da-ikke-lngere', filters.slugify('Så står Verden da ikke længere!'));
326 | assertEquals('super_max', filters.slugify('Super_Max'));
327 | });
328 | testcase('stringformat');
329 | test('return expected results', function () {
330 | assertEquals('002', filters.stringformat(2, '03d'));
331 | assertEquals('Hest', filters.stringformat('Hest', 's'));
332 | assertEquals('', filters.stringformat('Hest', ''));
333 | assertEquals('Hest ', filters.stringformat('Hest', '-10s'));
334 | });
335 | testcase('striptags');
336 | test('should remove tags', function () {
337 | assertEquals('jeg har en dejlig hest.',
338 | filters.striptags('
jeg har en dejlig hest.
', null, {})
339 | );
340 | });
341 | test('string should be marked as safe', function () {
342 | var safety = {};
343 | filters.striptags('
jeg har en dejlig hest.
', null, safety);
344 | assertEquals(true, safety.is_safe);
345 | });
346 | testcase('title');
347 | test('should titlecase correctly', function () {
348 | assertEquals('This Is Correct', filters.title('This is correct'));
349 | });
350 | testcase('truncatewords');
351 | test('should truncate', function () {
352 | assertEquals('Joel is ...', filters.truncatewords('Joel is a slug', 2));
353 | });
354 | testcase('upper');
355 | test('should uppercase correctly', function () {
356 | assertEquals('JOEL IS A SLUG', filters.upper('Joel is a slug'));
357 | });
358 | testcase('urlencode');
359 | test('should encode urls', function () {
360 | assertEquals('%22Aardvarks%20lurk%2C%20OK%3F%22', filters.urlencode('"Aardvarks lurk, OK?"'));
361 | });
362 | testcase('safe');
363 | test('string should be marked as safe', function () {
364 | var safety = {};
365 | filters.safe('Joel is a slug', null, safety);
366 | assertEquals(true, safety.is_safe);
367 | });
368 | testcase('safeseq');
369 | test('output should be marked as safe', function () {
370 | var safety = {};
371 | filters.safe(['hest', 'giraf'], null, safety);
372 | assertEquals(true, safety.is_safe);
373 | });
374 | testcase('escape');
375 | test('output should be marked as in need of escaping', function () {
376 | var safety = { must_escape: false };
377 | filters.escape('hurra', null, safety);
378 | assertEquals(true, safety.must_escape);
379 | });
380 | testcase('truncatewords_html');
381 | test('should truncate and close tags', function () {
382 | assertEquals('Joel is ...', filters.truncatewords_html('Joel is a slug', 2, {}));
383 | assertEquals('
Joel is ...
', filters.truncatewords_html('
Joel is a slug
', 2, {}));
384 | });
385 | test('should mark output as safe', function () {
386 | var safety = {};
387 | filters.truncatewords_html('
Joel is a slug
', 2, safety);
388 | assertEquals(true, safety.is_safe);
389 | });
390 | testcase('time');
391 | test('correctly format time', function () {
392 | var t = new Date();
393 | t.setHours('18');
394 | t.setMinutes('12');
395 | t.setSeconds('14');
396 | assertEquals('18:12:14', filters.time(t, 'H:i:s'));
397 | assertEquals('', filters.date('hest', 'H:i:s'));
398 | });
399 | testcase('timesince');
400 | test('should return time since', function () {
401 | var blog_date = new Date("1 June 2006 00:00:00");
402 | var comment_date = new Date("1 June 2006 08:00:00");
403 | assertEquals('8 hours', filters.timesince(blog_date, comment_date));
404 | });
405 | testcase('timeuntil');
406 | test('should return time since', function () {
407 | var today = new Date("1 June 2006");
408 | var from_date = new Date("22 June 2006");
409 | var conference_date = new Date("29 June 2006");
410 | assertEquals('4 weeks', filters.timeuntil(conference_date, today));
411 | assertEquals('1 week', filters.timeuntil(conference_date, from_date));
412 | });
413 | testcase('urlize');
414 | test('should urlize text', function () {
415 | assertEquals(
416 | 'Check out www.djangoproject.com',
417 | filters.urlize('Check out www.djangoproject.com', null, {})
418 | );
419 | });
420 | test('should escape if required', function () {
421 | var safety = { must_escape: true };
422 | assertEquals('hest & giraf', filters.urlize('hest & giraf', null, safety));
423 | });
424 | test('should mark output as safe if escaped', function () {
425 | var safety = { must_escape: true };
426 | filters.urlize('hest', null, safety);
427 | assertEquals(true, safety.is_safe);
428 | });
429 | testcase('urlizetrunc');
430 | test('should urlize text and truncate', function () {
431 | assertEquals(
432 | 'Check out www.djangopr...',
433 | filters.urlizetrunc('Check out www.djangoproject.com', 15, {})
434 | );
435 | });
436 | test('should escape if required', function () {
437 | var safety = { must_escape: true };
438 | assertEquals('hest & giraf', filters.urlizetrunc('hest & giraf', 15, safety));
439 | });
440 | test('should mark output as safe if escaped', function () {
441 | var safety = { must_escape: true };
442 | filters.urlizetrunc('hest', 15, safety);
443 | assertEquals(true, safety.is_safe);
444 | });
445 | testcase('wordcount')
446 | test('should count words', function () {
447 | assertEquals(6, filters.wordcount('I am not an atomic playboy'));
448 | });
449 | testcase('yesno')
450 | test('should return correct value', function () {
451 | assertEquals('yeah', filters.yesno(true, "yeah,no,maybe"));
452 | assertEquals('no', filters.yesno(false, "yeah,no,maybe"));
453 | assertEquals('maybe', filters.yesno(null, "yeah,no,maybe"));
454 | assertEquals('maybe', filters.yesno(undefined, "yeah,no,maybe"));
455 | assertEquals('no', filters.yesno(undefined, "yeah,no"));
456 | });
457 | run();
458 |
459 |
--------------------------------------------------------------------------------
/template/template_defaults.js:
--------------------------------------------------------------------------------
1 | /*jslint eqeqeq: true, undef: true, regexp: false */
2 | /*global require, process, exports, escape */
3 |
4 | var sys = require('sys');
5 | var string_utils = require('../utils/string');
6 | var date_utils = require('../utils/date');
7 | var html = require('../utils/html');
8 | var iter = require('../utils/iter');
9 |
10 | process.mixin(GLOBAL, require('../utils/tags'));
11 |
12 | /* TODO: Missing filters
13 |
14 | Don't know how:
15 | iriencode
16 |
17 | Not implemented (yet):
18 | unordered_list
19 |
20 | NOTE:
21 | stringformat() filter is regular sprintf compliant and doesn't have real python syntax
22 |
23 | Missing tags:
24 |
25 | ssi (will require ALLOWED_INCLUDE_ROOTS somehow)
26 | debug
27 |
28 | NOTE:
29 | cycle tag does not support legacy syntax (row1,row2,row3)
30 | load takes a path - like require. Loaded module must expose tags and filters objects.
31 | url tag relies on app being set in process.djangode_urls
32 | */
33 |
34 | var filters = exports.filters = {
35 | add: function (value, arg) {
36 | value = value - 0;
37 | arg = arg - 0;
38 | return (isNaN(value) || isNaN(arg)) ? '' : (value + arg);
39 | },
40 | addslashes: function (value, arg) { return string_utils.add_slashes("" + value); },
41 | capfirst: function (value, arg) { return string_utils.cap_first("" + value); },
42 | center: function (value, arg) { return string_utils.center("" + value, arg - 0); },
43 | cut: function (value, arg) { return ("" + value).replace(new RegExp(arg, 'g'), ""); },
44 | date: function (value, arg) {
45 | // TODO: this filter may be unsafe...
46 | return (value instanceof Date) ? date_utils.format_date(value, arg) : '';
47 | },
48 | 'default': function (value, arg) {
49 | // TODO: this filter may be unsafe...
50 | return value ? value : arg;
51 | },
52 | default_if_none: function (value, arg) {
53 | // TODO: this filter may be unsafe...
54 | return (value === null || value === undefined) ? arg : value;
55 | },
56 |
57 | dictsort: function (value, arg) {
58 | var clone = value.slice(0);
59 | clone.sort(function (a, b) { return a[arg] < b[arg] ? -1 : a[arg] > b[arg] ? 1 : 0; });
60 | return clone;
61 | },
62 |
63 | dictsortreversed: function (value, arg) {
64 | var tmp = filters.dictsort(value, arg);
65 | tmp.reverse();
66 | return tmp;
67 | },
68 | divisibleby: function (value, arg) { return value % arg === 0; },
69 |
70 | escape: function (value, arg, safety) {
71 | safety.must_escape = true;
72 | return value;
73 | },
74 | escapejs: function (value, arg) { return escape(value || ''); },
75 | filesizeformat: function (value, arg) {
76 | var bytes = value - 0;
77 | if (isNaN(bytes)) { return "0 bytes"; }
78 | if (bytes <= 1) { return '1 byte'; }
79 | if (bytes < 1024) { return bytes.toFixed(0) + ' bytes'; }
80 | if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(1) + 'KB'; }
81 | if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(1) + 'MB'; }
82 | return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';
83 | },
84 | first: function (value, arg) { return (value instanceof Array) ? value[0] : ""; },
85 | fix_ampersands: function (value, arg, safety) {
86 | safety.is_safe = true;
87 | return ("" + value).replace('&', '&');
88 | },
89 | floatformat: function (value, arg) {
90 | arg = arg - 0 || -1;
91 | var num = value - 0,
92 | show_zeroes = arg > 0,
93 | fix = Math.abs(arg);
94 | if (isNaN(num)) {
95 | return '';
96 | }
97 | var s = num.toFixed(fix);
98 | if (!show_zeroes && s % 1 === 0) {
99 | return num.toFixed(0);
100 | }
101 | return s;
102 | },
103 | force_escape: function (value, arg, safety) {
104 | safety.is_safe = true;
105 | return html.escape("" + value);
106 | },
107 | get_digit: function (value, arg) {
108 | if (typeof value !== 'number' || typeof arg !== 'number' || arg < 1) { return value; }
109 | var s = "" + value;
110 | return s[s.length - arg] - 0;
111 | },
112 | iriencode: function (value, arg) {
113 | // TODO: implement iriencode filter
114 | throw "iri encoding is not implemented";
115 | },
116 | join: function (value, arg) {
117 | // TODO: this filter may be unsafe...
118 | return (value instanceof Array) ? value.join(arg) : '';
119 | },
120 | last: function (value, arg) { return ((value instanceof Array) && value.length) ? value[value.length - 1] : ''; },
121 | length: function (value, arg) { return value.length ? value.length : 0; },
122 | length_is: function (value, arg) { return value.length === arg; },
123 | linebreaks: function (value, arg, safety) {
124 | var out = html.linebreaks("" + value, { escape: !safety.is_safe && safety.must_escape });
125 | safety.is_safe = true;
126 | return out;
127 | },
128 | linebreaksbr: function (value, arg, safety) {
129 | var out = html.linebreaks("" + value, { onlybr: true, escape: !safety.is_safe && safety.must_escape });
130 | safety.is_safe = true;
131 | return out;
132 | },
133 | linenumbers: function (value, arg, safety) {
134 | var lines = String(value).split('\n');
135 | var len = String(lines.length).length;
136 |
137 | var out = lines
138 | .map(function (s, idx) {
139 | if (!safety.is_safe && safety.must_escape) {
140 | s = html.escape("" + s);
141 | }
142 | return string_utils.sprintf('%0' + len + 'd. %s', idx + 1, s); })
143 | .join('\n');
144 | safety.is_safe = true;
145 | return out;
146 | },
147 | ljust: function (value, arg) {
148 | arg = arg - 0;
149 | try {
150 | return string_utils.sprintf('%-' + arg + 's', value).substr(0, arg);
151 | } catch (e) {
152 | return '';
153 | }
154 | },
155 | lower: function (value, arg) { return typeof value === 'string' ? value.toLowerCase() : ''; },
156 | make_list: function (value, arg) { return String(value).split(''); },
157 | phone2numeric: function (value, arg) {
158 | value = String(value).toLowerCase();
159 | return value.replace(/[a-pr-y]/g, function (x) {
160 | var code = x.charCodeAt(0) - 91;
161 | if (code > 22) { code = code - 1; }
162 | return Math.floor(code / 3);
163 | });
164 | },
165 | pluralize: function (value, arg) {
166 | // TODO: this filter may not be safe
167 | value = Number(value);
168 | var plural = arg ? String(arg).split(',') : ['', 's'];
169 | if (plural.length === 1) { plural.unshift(''); }
170 | if (isNaN(value)) { return ''; }
171 | return value === 1 ? plural[0] : plural[1];
172 | },
173 | pprint: function (value, arg) { return JSON.stringify(value); },
174 | random: function (value, arg) {
175 | return (value instanceof Array) ? value[ Math.floor( Math.random() * value.length ) ] : '';
176 | },
177 | removetags: function (value, arg, safety) {
178 | arg = String(arg).replace(/\s+/g, '|');
179 | var re = new RegExp( '?\\s*(' + arg + ')\\b[^>]*/?>', 'ig');
180 | safety.is_safe = true;
181 | return String(value).replace(re, '');
182 | },
183 | rjust: function (value, arg) {
184 | try {
185 | return string_utils.sprintf('%' + arg + 's', value).substr(0, arg);
186 | } catch (e) {
187 | return '';
188 | }
189 | },
190 | safe: function (value, arg, safety) {
191 | safety.is_safe = true;
192 | return value;
193 | },
194 | safeseq: function (value, arg) {
195 | safety.is_safe = true;
196 | return value;
197 | },
198 | slice: function (value, arg) {
199 | if (!(value instanceof Array)) { return []; }
200 | var parts = (arg || '').split(/:/g);
201 |
202 | if (parts[1] === '') {
203 | parts[1] = value.length;
204 | }
205 | parts = parts.map(Number);
206 |
207 | if (!parts[2]) {
208 | return value.slice(parts[0], parts[1]);
209 | }
210 | var out = [], i = parts[0], end = parts[1];
211 | for (;i < end; i += parts[2]) {
212 | out.push(value[i]);
213 | }
214 | return out;
215 |
216 | },
217 | slugify: function (value, arg) {
218 | return String(value).toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, '-');
219 | },
220 | stringformat: function (value, arg) {
221 | // TODO: this filter may not be safe
222 | try { return string_utils.sprintf('%' + arg, value); } catch (e) { return ''; }
223 | },
224 | striptags: function (value, arg, safety) {
225 | safety.is_safe = true;
226 | return String(value).replace(/<(.|\n)*?>/g, '');
227 | },
228 | title: function (value, arg) {
229 | return string_utils.titleCaps( String(value) );
230 | },
231 | time: function (value, arg) {
232 | // TODO: this filter may not be safe
233 | return (value instanceof Date) ? date_utils.format_time(value, arg) : '';
234 | },
235 | timesince: function (value, arg) {
236 | // TODO: this filter may not be safe (if people decides to put & or " in formatstrings"
237 | value = new Date(value);
238 | arg = new Date(arg);
239 | if (isNaN(value) || isNaN(arg)) { return ''; }
240 | return date_utils.timesince(value, arg);
241 | },
242 | timeuntil: function (value, arg) {
243 | // TODO: this filter may not be safe (if people decides to put & or " in formatstrings"
244 | value = new Date(value);
245 | arg = new Date(arg);
246 | if (isNaN(value) || isNaN(arg)) { return ''; }
247 | return date_utils.timeuntil(value, arg);
248 | },
249 | truncatewords: function (value, arg) {
250 | return String(value).split(/\s+/g).slice(0, arg).join(' ') + ' ...';
251 | },
252 | truncatewords_html: function (value, arg, safety) {
253 | safety.is_safe = true;
254 | return html.truncate_html_words(value, arg - 0);
255 | },
256 | upper: function (value, arg) {
257 | return (value + '').toUpperCase();
258 | },
259 | urlencode: function (value, arg) {
260 | return escape(value);
261 | },
262 | urlize: function (value, arg, safety) {
263 | if (!safety.is_safe && safety.must_escape) {
264 | var out = html.urlize(value + "", { escape: true });
265 | safety.is_safe = true;
266 | return out;
267 | }
268 | return html.urlize(value + "");
269 | },
270 | urlizetrunc: function (value, arg, safety) {
271 | if (!safety.is_safe && safety.must_escape) {
272 | var out = html.urlize(value + "", { escape: true, limit: arg });
273 | safety.is_safe = true;
274 | return out;
275 | }
276 | return html.urlize(value + "", { limit: arg });
277 | },
278 | wordcount: function (value, arg) {
279 | return (value + "").split(/\s+/g).length;
280 | },
281 | wordwrap: function (value, arg) {
282 | return string_utils.wordwrap(value + "", arg - 0);
283 | },
284 | yesno: function (value, arg) {
285 | var responses = (arg + "").split(/,/g);
286 | if (responses[2] && (value === undefined || value === null)) { return responses[2]; }
287 | return (value ? responses[0] : responses[1]) || '';
288 | }
289 |
290 | };
291 |
292 |
293 | var nodes = exports.nodes = {
294 |
295 | TextNode: function (text) {
296 | return function (context, callback) { callback(false, text); };
297 | },
298 |
299 | VariableNode: function (filterexpression) {
300 | return function (context, callback) {
301 | callback(false, filterexpression.resolve(context));
302 | };
303 | },
304 |
305 | ForNode: function (node_list, empty_list, itemname, listname, isReversed) {
306 |
307 | return function (context, callback) {
308 | var forloop = { parentloop: context.get('forloop') },
309 | list = context.get(listname),
310 | out = '';
311 |
312 | if (! list instanceof Array) { throw 'list not iterable' }
313 | if (isReversed) { list = list.slice(0).reverse(); }
314 |
315 | if (list.length === 0) {
316 | if (empty_list) {
317 | empty_list.evaluate(context, callback);
318 | } else {
319 | callback(false, '');
320 | }
321 | return;
322 | }
323 |
324 | context.push();
325 | context.set('forloop', forloop);
326 |
327 | function inner(p, c, idx, list, next) {
328 | process.mixin(forloop, {
329 | counter: idx + 1,
330 | counter0: idx,
331 | revcounter: list.length - idx,
332 | revcounter0: list.length - (idx + 1),
333 | first: idx === 0,
334 | last: idx === list.length - 1
335 | });
336 | context.set(itemname, c);
337 |
338 | node_list.evaluate( context, function (error, result) { next(error, p + result); });
339 | }
340 |
341 | iter.reduce(list, inner, '', function (error, result) {
342 | context.pop();
343 | callback(error, result);
344 | });
345 | };
346 | },
347 |
348 | IfNode: function (item_names, not_item_names, operator, if_node_list, else_node_list) {
349 |
350 | return function (context, callback) {
351 |
352 | function not(x) { return !x; }
353 | function and(p,c) { return p && c; }
354 | function or(p,c) { return p || c; }
355 |
356 | var items = item_names.map( context.get, context ).concat(
357 | not_item_names.map( context.get, context ).map( not )
358 | );
359 |
360 | var isTrue = items.reduce( operator === 'or' ? or : and, true );
361 |
362 | if (isTrue) {
363 | if_node_list.evaluate(context, function (error, result) { callback(error, result); });
364 | } else if (else_node_list) {
365 | else_node_list.evaluate(context, function (error, result) { callback(error, result); });
366 | } else {
367 | callback(false, '');
368 | }
369 | };
370 | },
371 |
372 | IfChangedNode: function (node_list, else_list, parts) {
373 | var last;
374 |
375 | return function (context, callback) {
376 | node_list.evaluate(context, function (error, result) {
377 | if (result !== last) {
378 | last = result;
379 | callback(error, result);
380 | } else if (!error && else_list) {
381 | else_list.evaluate(context, callback);
382 | } else {
383 | callback(error, '');
384 | }
385 | });
386 | };
387 | },
388 |
389 | IfEqualNode: function (node_list, else_list, first, second) {
390 | return function (context, callback) {
391 | if (context.get(first) == context.get(second)) {
392 | node_list.evaluate(context, callback);
393 | } else if (else_list) {
394 | else_list.evaluate(context, callback);
395 | } else {
396 | callback(false, '');
397 | }
398 | };
399 | },
400 |
401 | IfNotEqualNode: function (node_list, else_list, first, second) {
402 | return function (context, callback) {
403 | if (context.get(first) != context.get(second)) {
404 | node_list.evaluate(context, callback);
405 | } else if (else_list) {
406 | else_list.evaluate(context, callback);
407 | } else {
408 | callback(false, '');
409 | }
410 | };
411 | },
412 |
413 |
414 | CycleNode: function (items) {
415 |
416 | var cnt = 0;
417 |
418 | return function (context, callback) {
419 |
420 | var choices = items.map( context.get, context );
421 | var val = choices[cnt];
422 | cnt = (cnt + 1) % choices.length;
423 | callback(false, val);
424 | };
425 | },
426 |
427 | FilterNode: function (expression, node_list) {
428 | return function (context, callback) {
429 | node_list.evaluate( context, function (error, constant) {
430 | expression.constant = constant;
431 | callback(error, expression.resolve(context));
432 | });
433 | };
434 | },
435 |
436 | BlockNode: function (node_list, name) {
437 |
438 | /* upon execution each block stores it's nodelist in the context
439 | * indexed by the blocks name. As templates are executed from child to
440 | * parent, similar named blocks add their nodelist to an array of
441 | * nodelists (still indexed by the blocks name). When the root template
442 | * is reached, the blocks nodelists are executed one after each other
443 | * and the super variable is updated down through the hierachy.
444 | */
445 | return function (context, callback) {
446 |
447 | // init block list if it isn't already
448 | if (!context.blocks[name]) {
449 | context.blocks[name] = [];
450 | }
451 |
452 | // put this block in front of list
453 | context.blocks[name].unshift( node_list );
454 |
455 | // if this is a root template descend through templates and evaluate blocks for overrides
456 | if (!context.extends) {
457 |
458 | context.push();
459 |
460 | function inner(p, c, idx, block_list, next) {
461 | c.evaluate( context, function (error, result) {
462 | context.set('block', { super: result });
463 | next(error, result);
464 | });
465 | }
466 | iter.reduce( context.blocks[name], inner, '', function (error, result) {
467 | context.pop();
468 | callback(error, result);
469 | });
470 |
471 | } else {
472 | // else return empty string
473 | callback(false, '');
474 | }
475 | };
476 | },
477 |
478 | ExtendsNode: function (item) {
479 | return function (context, callback) {
480 | context.extends = context.get(item);
481 | callback(false, '');
482 | };
483 | },
484 |
485 | AutoescapeNode: function (node_list, enable) {
486 |
487 | if (enable.toLowerCase() === 'on') {
488 | enable = true;
489 | } else {
490 | enable = false;
491 | }
492 |
493 | return function (context, callback) {
494 | var before = context.autoescaping;
495 | context.autoescaping = enable;
496 | node_list.evaluate( context, function ( error, result ) {
497 | context.autoescaping = before;
498 | callback(error, result);
499 | });
500 | }
501 | },
502 |
503 | FirstOfNode: function (/*...*/) {
504 |
505 | var choices = Array.prototype.slice.apply(arguments);
506 |
507 | return function (context, callback) {
508 | var i, val, found;
509 | for (i = 0; i < choices.length; i++) {
510 | val = context.get(choices[i]);
511 | if (val) { found = true; break; }
512 | }
513 | callback(false, found ? val : '')
514 | };
515 | },
516 |
517 | WithNode: function (node_list, variable, name) {
518 | return function (context, callback) {
519 | var item = context.get(variable);
520 | context.push();
521 | context.set(name, item);
522 | node_list.evaluate( context, function (error, result) {
523 | context.pop();
524 | callback(error, result);
525 | });
526 | }
527 | },
528 |
529 | NowNode: function (format) {
530 | if (format.match(/^["']/)) {
531 | format = format.slice(1, -1);
532 | }
533 | return function (context, callback) {
534 | callback(false, date_utils.format_date(new Date(), format));
535 | };
536 | },
537 |
538 | IncludeNode: function (name) {
539 | return function (context, callback) {
540 | var loader = require('./loader');
541 | loader.load_and_render(context.get(name), context, callback);
542 | }
543 | },
544 |
545 | LoadNode: function (path, package) {
546 | return function (context, callback) {
547 | process.mixin(context.filters, package.filters);
548 | callback(false, '');
549 | }
550 | },
551 |
552 | TemplateTagNode: function (type) {
553 | return function (context, callback) {
554 | var bits = {
555 | openblock: '{%',
556 | closeblock: '%}',
557 | openvariable: '{{',
558 | closevariable: '}}',
559 | openbrace: '{',
560 | closebrace: '}',
561 | opencomment: '{#',
562 | closecomment: '#}'
563 | };
564 | if (!bits[type]) {
565 | callback('unknown bit');
566 | } else {
567 | callback(false, bits[type]);
568 | }
569 | }
570 | },
571 |
572 | SpacelessNode: function (node_list) {
573 | return function (context, callback) {
574 | node_list.evaluate(context, function (error, result) {
575 | callback(error, html.strip_spaces_between_tags(result + ""));
576 | });
577 | }
578 | },
579 |
580 | WithRatioNode: function (current, max, constant) {
581 | return function (context, callback) {
582 | current_val = context.get(current);
583 | max_val = context.get(max);
584 | constant_val = context.get(constant);
585 |
586 | callback(false, Math.round(current_val / max_val * constant_val) + "");
587 | }
588 | },
589 |
590 | RegroupNode: function (item, key, name) {
591 | return function (context, callback) {
592 | var list = context.get(item);
593 | if (!list instanceof Array) { callback(false, ''); }
594 |
595 | var dict = {};
596 | var grouped = list
597 | .map(function (x) { return x[key]; })
598 | .filter(function (x) { var val = dict[x]; dict[x] = x; return !val; })
599 | .map(function (grp) {
600 | return { grouper: grp, list: list.filter(function (o) { return o[key] === grp }) };
601 | });
602 |
603 | context.set(name, grouped);
604 | callback(false, '');
605 | }
606 | },
607 |
608 | UrlNode: function (url_name, replacements, item_name) {
609 |
610 | return function (context, callback) {
611 | var match = process.djangode_urls[context.get(url_name)]
612 | if (!match) { return callback('no matching urls for ' + url_name); }
613 |
614 | var url = string_utils.regex_to_string(match, replacements.map(function (x) { return context.get(x); }));
615 | if (url[0] !== '/') { url = '/' + url; }
616 |
617 | if (item_name) {
618 | context.set( item_name, url);
619 | callback(false, '');
620 | } else {
621 | callback(false, url);
622 | }
623 | }
624 | }
625 | };
626 |
627 | var tags = exports.tags = {
628 | 'text': function (parser, token) { return nodes.TextNode(token.contents); },
629 |
630 | 'variable': function (parser, token) {
631 | return nodes.VariableNode( parser.make_filterexpression(token.contents) );
632 | },
633 |
634 | 'comment': function (parser, token) {
635 | parser.parse('end' + token.type);
636 | parser.delete_first_token();
637 | return nodes.TextNode('');
638 | },
639 |
640 | 'for': function (parser, token) {
641 |
642 | var parts = get_args_from_token(token, { exclude: 2, mustbe: { 2: 'in', 4: 'reversed'} });
643 |
644 | var itemname = parts[0],
645 | listname = parts[1],
646 | isReversed = (parts[2] === 'reversed');
647 |
648 | var node_list = parser.parse('empty', 'end' + token.type);
649 | if (parser.next_token().type === 'empty') {
650 | var empty_list = parser.parse('end' + token.type);
651 | parser.delete_first_token();
652 | }
653 |
654 | return nodes.ForNode(node_list, empty_list, itemname, listname, isReversed);
655 | },
656 |
657 | 'if': function (parser, token) {
658 |
659 | var parts = token.split_contents();
660 |
661 | // get rid of if keyword
662 | parts.shift();
663 |
664 | var operator = '',
665 | item_names = [],
666 | not_item_names = [];
667 |
668 | var p, next_should_be_item = true;
669 |
670 | while (p = parts.shift()) {
671 | if (next_should_be_item) {
672 | if (p === 'not') {
673 | p = parts.shift();
674 | if (!p) { throw 'unexpected syntax in "if" tag. Expected item name after not'; }
675 | not_item_names.push( p );
676 | } else {
677 | item_names.push( p );
678 | }
679 | next_should_be_item = false;
680 | } else {
681 | if (p !== 'and' && p !== 'or') { throw 'unexpected syntax in "if" tag. Expected "and" or "or"'; }
682 | if (operator && p !== operator) { throw 'unexpected syntax in "if" tag. Cannot mix "and" and "or"'; }
683 | operator = p;
684 | next_should_be_item = true;
685 | }
686 | }
687 |
688 | var node_list, else_list;
689 |
690 | node_list = parser.parse('else', 'end' + token.type);
691 | if (parser.next_token().type === 'else') {
692 | else_list = parser.parse('end' + token.type);
693 | parser.delete_first_token();
694 | }
695 |
696 | return nodes.IfNode(item_names, not_item_names, operator, node_list, else_list);
697 | },
698 |
699 | 'ifchanged': function (parser, token) {
700 | var parts = get_args_from_token(token);
701 |
702 | var node_list, else_list;
703 |
704 | node_list = parser.parse('else', 'end' + token.type);
705 | if (parser.next_token().type === 'else') {
706 | else_list = parser.parse('end' + token.type);
707 | parser.delete_first_token();
708 | }
709 |
710 | return nodes.IfChangedNode(node_list, else_list, parts);
711 | },
712 |
713 | 'ifequal': function (parser, token) {
714 | var parts = get_args_from_token(token, { argcount: 2 });
715 |
716 | var node_list, else_list;
717 |
718 | node_list = parser.parse('else', 'end' + token.type);
719 | if (parser.next_token().type === 'else') {
720 | else_list = parser.parse('end' + token.type);
721 | parser.delete_first_token();
722 | }
723 |
724 | return nodes.IfEqualNode(node_list, else_list, parts[0], parts[1]);
725 | },
726 |
727 | 'ifnotequal': function (parser, token) {
728 | var parts = get_args_from_token(token, { argcount: 2 });
729 |
730 | var node_list, else_list;
731 |
732 | node_list = parser.parse('else', 'end' + token.type);
733 | if (parser.next_token().type === 'else') {
734 | else_list = parser.parse('end' + token.type);
735 | parser.delete_first_token();
736 | }
737 |
738 | return nodes.IfNotEqualNode(node_list, else_list, parts[0], parts[1]);
739 | },
740 |
741 | 'cycle': function (parser, token) {
742 | var parts = token.split_contents();
743 |
744 | var items = parts.slice(1);
745 | var as_idx = items.indexOf('as');
746 | var name = '';
747 |
748 | if (items.length === 1) {
749 | if (!parser.cycles || !parser.cycles[items[0]]) {
750 | throw 'no cycle named ' + items[0] + '!';
751 | } else {
752 | return parser.cycles[items[0]];
753 | }
754 | }
755 |
756 | if (as_idx > 0) {
757 | if (as_idx === items.length - 1) {
758 | throw 'unexpected syntax in "cycle" tag. Expected name after as';
759 | }
760 |
761 | name = items[items.length - 1];
762 | items = items.slice(0, items.length - 2);
763 |
764 | if (!parser.cycles) { parser.cycles = {}; }
765 | parser.cycles[name] = nodes.CycleNode(items);
766 | return parser.cycles[name];
767 | }
768 |
769 | return nodes.CycleNode(items);
770 | },
771 |
772 | 'filter': function (parser, token) {
773 | var parts = token.split_contents();
774 | if (parts.length > 2) { throw 'unexpected syntax in "filter" tag'; }
775 |
776 | var expr = parser.make_filterexpression('|' + parts[1]);
777 |
778 | var node_list = parser.parse('endfilter');
779 | parser.delete_first_token();
780 |
781 | return nodes.FilterNode(expr, node_list);
782 | },
783 |
784 | 'autoescape': function (parser, token) {
785 | var parts = get_args_from_token(token, { argcount: 1, mustbe: { 1: ['on', 'off'] }});
786 | var node_list = parser.parse('end' + token.type);
787 | parser.delete_first_token();
788 | return nodes.AutoescapeNode(node_list, parts[0]);
789 | },
790 |
791 | 'block': function (parser, token) {
792 | var parts = get_args_from_token(token, { argcount: 1 });
793 | var node_list = parser.parse('end' + token.type);
794 | parser.delete_first_token();
795 | return nodes.BlockNode(node_list, parts[0]);
796 | },
797 |
798 | 'extends': simple_tag(nodes.ExtendsNode, { argcount: 1 }),
799 |
800 | 'firstof': simple_tag(nodes.FirstOfNode),
801 |
802 | 'with': function (parser, token) {
803 | var parts = get_args_from_token(token, { argcount: 3, exclude: 2, mustbe: { 2: 'as' }});
804 | var node_list = parser.parse('end' + token.type);
805 | parser.delete_first_token();
806 | return nodes.WithNode(node_list, parts[0], parts[1], parts[2]);
807 | },
808 |
809 | 'now': simple_tag(nodes.NowNode, { argcount: 1 }),
810 | 'include': simple_tag(nodes.IncludeNode, { argcount: 1 }),
811 | 'load': function (parser, token) {
812 | var parts = get_args_from_token(token, { argcount: 1 });
813 | var name = parts[0];
814 | if (name[0] === '"' || name[0] === "'") {
815 | name = name.substr(1, name.length - 2);
816 | }
817 |
818 | var package = require(name);
819 | process.mixin(parser.tags, package.tags);
820 |
821 | return nodes.LoadNode(name, package);
822 | },
823 | 'templatetag': simple_tag(nodes.TemplateTagNode, { argcount: 1 }),
824 | 'spaceless': function (parser, token) {
825 | var parts = get_args_from_token(token, { argcount: 0 });
826 | var node_list = parser.parse('end' + token.type);
827 | parser.delete_first_token();
828 | return nodes.SpacelessNode(node_list);
829 | },
830 | 'widthratio': simple_tag(nodes.WithRatioNode, { argcount: 3 }),
831 | 'regroup': simple_tag(nodes.RegroupNode, { argcount: 5, mustbe: { 2: 'by', 4: 'as' }, exclude: [2, 4] }),
832 |
833 | 'url': function (parser, token) {
834 | var parts = token.split_contents();
835 | parts.shift();
836 |
837 | var url_name = parts.shift();
838 |
839 | if (parts[parts.length - 2] === 'as') {
840 | var item_name = parts.pop();
841 | parts.pop();
842 | }
843 |
844 | // TODO: handle qouted strings with commas in them correctly
845 | var replacements = parts.join('').split(/\s*,\s*/)
846 |
847 | return nodes.UrlNode(url_name, replacements, item_name);
848 | }
849 |
850 |
851 | };
852 |
853 |
--------------------------------------------------------------------------------