├── .gitignore ├── static-demo └── hello.txt ├── template ├── load_tag_test.js ├── loader.js ├── template.test.js ├── template.js ├── template_defaults.tags.test.js ├── template_defaults.test.js └── template_defaults.js ├── README.txt ├── utils ├── iter.js ├── iter.test.js ├── string.test.js ├── html.test.js ├── tags.js ├── date.test.js ├── html.js ├── date.js ├── string.js └── test.js ├── regression.py ├── template-demo └── template.html ├── example.js ├── license.txt ├── template_example.js ├── djangode.js └── TEMPLATES.md /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore vim swp files 2 | .*.swp 3 | -------------------------------------------------------------------------------- /static-demo/hello.txt: -------------------------------------------------------------------------------- 1 | Hello from a static file. 2 | -------------------------------------------------------------------------------- /template/load_tag_test.js: -------------------------------------------------------------------------------- 1 | exports.filters = { 2 | testfilter: function () { 3 | return 'hestgiraf'; 4 | } 5 | }; 6 | exports.tags = { 7 | testtag: function () { 8 | return function (context, callback) { 9 | callback('', 'hestgiraf'); 10 | }; 11 | } 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | djangode 2 | ======== 3 | 4 | Utility functions for node.js that imitate some useful concepts from Django. 5 | 6 | http://nodejs.org/ 7 | http://www.djangoproject.com/ 8 | 9 | Example usage: 10 | 11 | var dj = require('./djangode'); 12 | dj.serve(dj.makeApp([ 13 | ['^/$', function(req, res) { 14 | dj.respond(res, '

Homepage

'); 15 | }], 16 | ['^/other$', function(req, res) { 17 | dj.respond(res, '

Other page

'); 18 | }], 19 | ['^/page/(\\d+)$', function(req, res, page) { 20 | dj.respond(res, '

Page ' + page + '

'); 21 | }] 22 | ]), 8008); // Serves on port 8008 23 | 24 | Run "node example.js" for a slightly more interesting example. 25 | -------------------------------------------------------------------------------- /utils/iter.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | exports.reduce = function reduce(array, iter_callback, initial, result_callback) { 4 | 5 | var index = 0; 6 | var depth = 0; 7 | 8 | if (!result_callback) { throw 'no result callback!!!'; } 9 | 10 | (function inner (error, value) { 11 | 12 | if (error) { 13 | return result_callback(error); 14 | } 15 | 16 | if (index < array.length) { 17 | process.nextTick( function () { 18 | try { 19 | index = index + 1; 20 | iter_callback( value, array[index - 1], index, array, inner ); 21 | } catch (e) { 22 | result_callback(e); 23 | } 24 | }); 25 | } else { 26 | result_callback( false, value ); 27 | } 28 | })( false, initial ); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /regression.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import re, os 3 | from subprocess import Popen, PIPE 4 | 5 | print "" 6 | 7 | cmd = 'find . -name "*.test.js"'; 8 | files = Popen(cmd, shell=True, stdout=PIPE).communicate()[0].splitlines() 9 | failed_list = [] 10 | 11 | for file in files: 12 | output = Popen('node ' + file, shell=True, stdout=PIPE).communicate()[0].splitlines() 13 | try: 14 | result = [line for line in output if line.startswith('Total')][0] 15 | except: 16 | #bizarre, but sometimes popen apears to return empty strings 17 | #I'm too tired to fix this right now, so for now just retry and hope for better results 18 | output = Popen('node ' + file, shell=True, stdout=PIPE).communicate()[0].splitlines() 19 | result = [line for line in output if line.startswith('Total')][0] 20 | (total, failed, error) = re.split(r':|,', result)[1::2] 21 | 22 | if int(failed) > 0 or int(error) > 0: 23 | failed_list.append(file) 24 | 25 | print file 26 | print '\t', result 27 | 28 | if failed_list: 29 | print '\nWARNING! There were failed tests:' 30 | for file in failed_list: 31 | print file 32 | print "" 33 | exit(1) 34 | print "" 35 | exit(0) 36 | 37 | -------------------------------------------------------------------------------- /template-demo/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ordering notice - node.js test 5 | 6 | 7 |

Ordering notice

8 | 9 | {{ "test" }} 10 | 11 |

Dear {{person_name}},

12 | 13 |

Thanks for placing an order from {{ company }}. It's scheduled to 14 | ship on {{ ship_date|date:"F j, Y" }}.

15 | 16 |

Here are the items you've ordered:

17 | 18 | 23 | 24 | {% if ordered_warranty or true or false %} 25 |

Your warranty information will be included in the packaging.

26 | {% else %} 27 |

You didn't order a warranty, so you're on your own when 28 | the products inevitably stop working.

29 | {% endif %} 30 | 31 | {{ship.name}}{{ ship.nationality.toUpperCase }} 32 | 33 |

Sincerely,
{{ company }}

34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var dj = require('./djangode'); 2 | 3 | var app = dj.makeApp([ 4 | ['^/$', function(req, res) { 5 | dj.respond(res, '

djangode demo

\ 6 | \ 13 | '); 14 | }], 15 | ['^/delayed/(\\d+)$', function(req, res, howlong) { 16 | setTimeout(function() { 17 | dj.respond(res, 'I delayed for ' + howlong); 18 | }, parseInt(howlong, 10)); 19 | }], 20 | ['^/error$', function(req, res) { 21 | "bob"("not a function"); // Demonstrates stacktrace page 22 | }], 23 | ['^/redirect$', function(req, res) { 24 | dj.redirect(res, '/'); 25 | }], 26 | ['^/favicon\.ico$', function(req, res) { 27 | dj.respond(res, 'Nothing to see here'); 28 | }], 29 | ['^/(static-demo/.*)$', dj.serveFile] // Serve files from static-demo/ 30 | ]); 31 | 32 | dj.serve(app, 8009); 33 | -------------------------------------------------------------------------------- /template/loader.js: -------------------------------------------------------------------------------- 1 | /*jslint eqeqeq: true, undef: true, regexp: false */ 2 | /*global require, process, exports, escape */ 3 | 4 | var sys = require('sys'); 5 | var fs = require('fs'); 6 | var template_system = require('./template'); 7 | 8 | var cache = {}; 9 | var template_path = '/tmp'; 10 | 11 | 12 | // TODO: get_template 13 | // should support subdirectories 14 | 15 | var load = exports.load = function (name, callback) { 16 | if (!callback) { throw 'loader.load() must be called with a callback'; } 17 | 18 | if (cache[name] != undefined) { 19 | callback(false, cache[name]); 20 | } else { 21 | fs.readFile(template_path + '/' + name, function (error, s) { 22 | if (error) { callback(error); } 23 | cache[name] = template_system.parse(s); 24 | callback(false, cache[name]); 25 | }); 26 | } 27 | }; 28 | 29 | exports.load_and_render = function (name, context, callback) { 30 | load(name, function (error, template) { 31 | if (error) { 32 | callback(error); 33 | } else { 34 | template.render(context, callback); 35 | } 36 | }); 37 | }; 38 | 39 | exports.flush = function () { 40 | cache = {}; 41 | }; 42 | 43 | exports.set_path = function (path) { 44 | template_path = path; 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Simon Willison and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /utils/iter.test.js: -------------------------------------------------------------------------------- 1 | process.mixin(GLOBAL, require('./test').dsl); 2 | process.mixin(GLOBAL, require('./iter')); 3 | 4 | var events = require('events'); 5 | var sys = require('sys'); 6 | 7 | testcase('reduce'); 8 | test_async('should work like regular reduce', function (content, callback) { 9 | var list = []; 10 | //for (var i = 0; i < 400000; i++) { 11 | for (var i = 0; i < 1000; i++) { 12 | list.push(i); 13 | } 14 | 15 | //var t = new Date(); 16 | var expected = list.reduce(function (p, c) { return p + c; }, 0); 17 | //sys.debug(new Date() - t); 18 | 19 | reduce(list, function (p, c, idx, list, callback) { callback(false, p + c); }, 0, 20 | function (error, actual) { 21 | //sys.debug(new Date() - t); 22 | assertEquals(expected, actual, callback); 23 | callback(); 24 | } 25 | ); 26 | }); 27 | 28 | test_async('should handle thrown error in iterfunction', function (content, callback) { 29 | var list = []; 30 | for (var i = 0; i < 100; i++) { 31 | list.push(i); 32 | } 33 | 34 | var been_here = false; 35 | 36 | reduce(list, function (p, c, idx, list, callback) { undefined.will.raise.exception }, 0, 37 | function (error, actual) { 38 | assertIsFalse(been_here); 39 | been_here = true; 40 | assertIsTrue(error, callback); 41 | callback(); 42 | } 43 | ); 44 | }); 45 | 46 | test_async('should handle error returned with callback from iterfunction', function (content, callback) { 47 | var list = []; 48 | for (var i = 0; i < 100; i++) { 49 | list.push(i); 50 | } 51 | 52 | var been_here = false; 53 | 54 | reduce(list, function (p, c, idx, list, callback) { callback('raised error'); }, 0, 55 | function (error, actual) { 56 | assertIsFalse(been_here, callback); 57 | been_here = true; 58 | assertIsTrue(error, callback); 59 | callback(); 60 | } 61 | ); 62 | }); 63 | 64 | run(); 65 | 66 | -------------------------------------------------------------------------------- /utils/string.test.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | process.mixin(GLOBAL, require('./test').dsl); 4 | process.mixin(GLOBAL, require('./string')); 5 | 6 | testcase('string utility functions'); 7 | test('smart_split should split correctly', function () { 8 | assertEquals(['this', 'is', '"the \\"correct\\" way"'], smart_split('this is "the \\"correct\\" way"')); 9 | }); 10 | test('add_slashes should add slashes', function () { 11 | assertEquals('this is \\"it\\"', add_slashes('this is "it"')); 12 | }); 13 | test('cap_first should capitalize first letter', function () { 14 | assertEquals('Yeah baby!', cap_first('yeah baby!')); 15 | }); 16 | test('center should center text', function () { 17 | assertEquals(' centered ', center('centered', 18)); 18 | assertEquals(' centere ', center('centere', 18)); 19 | assertEquals(' centered ', center('centered', 17)); 20 | assertEquals('centered', center('centered', 3)); 21 | }); 22 | testcase('titleCaps') 23 | test('should work as expected', function () { 24 | assertEquals("Nothing to Be Afraid Of?", titleCaps("Nothing to Be Afraid of?")); 25 | assertEquals("Q&A With Steve Jobs: 'That's What Happens in Technology'", 26 | titleCaps("Q&A With Steve Jobs: 'That's What Happens In Technology'") 27 | ); 28 | }) 29 | testcase('wrap') 30 | test('should wrap text', function () { 31 | assertEquals('Joel \nis a \nslug', wordwrap('Joel is a slug', 5)); 32 | }); 33 | testcase('regex_to_string') 34 | test('should work without groups', function () { 35 | assertEquals('hest', regex_to_string(/hest/)); 36 | assertEquals('hest', regex_to_string(/^hest$/)); 37 | assertEquals('hestgiraf', regex_to_string(/hest\s*giraf\d+/)); 38 | assertEquals('hest*', regex_to_string(/hest\*/)); 39 | assertEquals('hestgiraf', regex_to_string(/hest(tobis)giraf/)); 40 | }); 41 | 42 | test('should replace groups with input', function () { 43 | assertEquals('shows/hest/34/', regex_to_string(/^shows\/(\w+)\/(\d+)\/$/, ['hest', 34])); 44 | assertEquals('shows/giraf/90/', regex_to_string(/^shows\/(hest(?:laks|makrel))\/(\d+)\/$/, ['giraf', 90])); 45 | }); 46 | 47 | run(); 48 | 49 | -------------------------------------------------------------------------------- /template_example.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | dj = require('./djangode'), 3 | template_system = require('./template/template'); 4 | template_loader = require('./template/loader'); 5 | 6 | // set template path 7 | template_loader.set_path('template-demo'); 8 | 9 | // context to use when rendering template. In a real app this would likely come from a database 10 | var test_context = { 11 | person_name: 'Thomas Hest', 12 | company: 'Tobis A/S', 13 | ship_date: new Date('12-02-1981'), 14 | item: 'XXX', 15 | item_list: [ 'Giraf', 'Fisk', 'Tapir'], 16 | ordered_warranty: true, 17 | ship: { 18 | name: 'M/S Martha', 19 | nationality: 'Danish' 20 | } 21 | }; 22 | 23 | 24 | // make app 25 | var app = dj.makeApp([ 26 | ['^/$', function(req, res) { 27 | dj.respond(res, '

djangode template demo

\ 28 | \ 34 | '); 35 | }], 36 | 37 | ['^/template$', function (req, res) { 38 | dj.serveFile(req, res, 'template-demo/template.html'); 39 | }], 40 | 41 | ['^/context$', function (req, res) { 42 | dj.respond(res, sys.inspect(test_context), 'text/plain'); 43 | }], 44 | 45 | ['^/text$', function (req, res) { 46 | template_loader.load_and_render('template.html', test_context, function (error, result) { 47 | if (error) { 48 | dj.default_show_500(req, res, error); 49 | } else { 50 | dj.respond(res, result, 'text/plain'); 51 | } 52 | }); 53 | }], 54 | 55 | ['^/html$', function (req, res) { 56 | template_loader.load_and_render('template.html', test_context, function (error, result) { 57 | if (error) { 58 | dj.default_show_500(req, res, error); 59 | } else { 60 | dj.respond(res, result, 'text/plain'); 61 | } 62 | }); 63 | }], 64 | 65 | ['^/(template-demo/.*)$', dj.serveFile], 66 | 67 | ]); 68 | 69 | dj.serve(app, 8009); 70 | process.djangode_urls = app.urls; 71 | 72 | -------------------------------------------------------------------------------- /utils/html.test.js: -------------------------------------------------------------------------------- 1 | process.mixin(GLOBAL, require('./test').dsl); 2 | process.mixin(GLOBAL, require('./html')); 3 | 4 | testcase('tests for linebreaks()') 5 | test('should break lines into

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 '

' + escape(x).replace('\n', '
') + '

'; } ); 40 | } else { 41 | lines = lines.map( function (x) { return '

' + x.replace('\n', '
') + '

'; } ); 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 + ''; }, 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( ']*/?>', '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 | --------------------------------------------------------------------------------