Permalink
Browse files

Implemented block and extends tags

  • Loading branch information...
1 parent 61c3ed0 commit 658506a9703fe64551f23149726b3a2af8843f39 Anders Hellerup Madsen committed Jan 14, 2010
View
2 example.js
@@ -1,4 +1,4 @@
-var dj = require('./djangode');
+var dj = require('djangode');
var app = dj.makeApp([
['^/$', function(req, res) {
View
110 template/template.js
@@ -1,8 +1,10 @@
/*jslint laxbreak: true, eqeqeq: true, undef: true, regexp: false */
/*global require, process, exports */
+
var sys = require('sys');
-var utils = require('../utils/utils');
-var template_defaults = require('./template_defaults');
+
+var utils = require('utils/utils');
+var template_defaults = require('template/template_defaults');
/***************** TOKENIZER ******************************/
@@ -68,13 +70,30 @@ function tokenize(input) {
/*********** PARSER **********************************/
+function Parser(input) {
+ this.token_list = tokenize(input);
+ this.indent = 0;
+ this.blocks = {};
+}
+
function parser_error(e) {
return 'Parsing exception: ' + JSON.stringify(e, 0, 2);
}
-function Parser(input) {
- this.token_list = tokenize(input);
- this.indent = 0;
+function make_nodelist() {
+ var node_list = [];
+ node_list.evaluate = function (context) {
+ return this.reduce( function (p, c) { return p + c(context); }, '');
+ };
+ node_list.only_types = function (/*args*/) {
+ var args = Array.prototype.slice.apply(arguments);
+ return this.filter( function (x) { return args.indexOf(x.type) > -1; } );
+ };
+ node_list.append = function (node, type) {
+ node.type = type;
+ this.push(node);
+ };
+ return node_list;
}
process.mixin(Parser.prototype, {
@@ -84,7 +103,7 @@ process.mixin(Parser.prototype, {
parse: function () {
var stoppers = Array.prototype.slice.apply(arguments);
- var node_list = [];
+ var node_list = make_nodelist();
var token = this.token_list[0];
var callback = null;
@@ -102,10 +121,13 @@ process.mixin(Parser.prototype, {
callback = this.callbacks[token.type];
if (callback && typeof callback === 'function') {
- node_list.push( callback(this, token) );
+ node_list.append( callback(this, token), token.type );
} else {
//throw parser_error('Unknown tag: ' + token[0]);
- node_list.push( template_defaults.TextNode('[[ UNKNOWN ' + token.type + ' ]]'));
+ node_list.append(
+ template_defaults.nodes.TextNode('[[ UNKNOWN ' + token.type + ' ]]'),
+ 'UNKNOWN'
+ );
}
}
if (stoppers.length) {
@@ -142,8 +164,11 @@ function normalize(value) {
/*************** Context *********************************/
-function Context(o) {
+function Context(o, blockmark) {
this.scope = [ o ];
+ this.extends = [];
+ this.blocks = {};
+ this.blockmark = blockmark;
}
process.mixin(Context.prototype, {
@@ -168,7 +193,11 @@ process.mixin(Context.prototype, {
}
}
- return val;
+ if (typeof val === 'function') {
+ return val();
+ } else {
+ return val;
+ }
}
}
@@ -182,24 +211,27 @@ process.mixin(Context.prototype, {
},
pop: function () {
return this.scope.shift();
+ },
+ block_placeholder: function (name) {
+ return this.blockmark + name + this.blockmark;
}
});
/*********** FilterExpression **************************/
var FilterExpression = function (expression, constant) {
- // groups 1 = variable/constant, 2 = arg name, 3 = arg value without qoutes
- this.re = /(^"[^"\\]*(?:\\.[^"\\]*)*"|^[\w\.]+)?(?:\|(\w+\b)(?::"([^"\\]*(?:\\.[^"\\]*)*)")?)?(?=\S|$)/g;
+ // groups 1 = variable/constant, 2 = arg name, 3 = arg value
+ this.re = /(^"[^"\\]*(?:\\.[^"\\]*)*"|^[\w\.]+)?(?:\|(\w+\b)(?::("[^"\\]*(?:\\.[^"\\]*)*"|[^\|\s]+))?)?(?=\S|$)/g;
this.re.lastIndex = 0;
var parsed = this.consume(expression);
if (!parsed) {
- throw this.error(expression + ' - 1');
+ throw this.error(expression);
}
if (constant) {
if (parsed.variable) {
- throw this.error(expression + ' - 2'); // did not expect variable when constant is defined...
+ throw this.error(expression); // did not expect variable when constant is defined...
} else {
this.constant = constant;
}
@@ -212,7 +244,7 @@ var FilterExpression = function (expression, constant) {
this.variable = parsed.variable;
}
} else {
- throw this.error(expression + ' - 3');
+ throw this.error(expression);
}
}
@@ -267,32 +299,42 @@ exports.FilterExpression = FilterExpression;
/*********** Template **********************************/
-function Template(node_list) {
- this.node_list = node_list;
+function Template(input) {
+ var parser = new Parser(input);
+ this.node_list = parser.parse();
}
+var replace_blocks_re = /\u0000\u0000\u0000(\w+)\u0000\u0000\u0000/g
+
process.mixin(Template.prototype, {
- render: function (o) {
- var context = new Context(o);
- try {
- return evaluate_node_list(this.node_list, context);
- } catch (e) {
- sys.debug(e);
- return e;
+ render: function (o, delay_blocks) {
+
+ var context = (o instanceof Context) ? o : new Context(o || {}, '\u0000\u0000\u0000');
+ if (!o instanceof Context)
+ var context = new Context(o || {});
+
+ var rendered = this.node_list.evaluate(context);
+
+ if (context.extends.length > 0) {
+ rendered = context.extends.pop();
}
- }
+
+ if (!delay_blocks) {
+ rendered = rendered.replace( replace_blocks_re, function (str, name) {
+ return context.blocks[name];
+ });
+ }
+
+ return rendered;
+ },
});
/********************************************************/
exports.parse = function (input) {
- var parser = new Parser(input);
+ //var parser = new Parser(input);
// TODO: Better error handling, this is lame
- try {
- return new Template(parser.parse());
- } catch (e) {
- return new Template([ template_defaults.nodes.TextNode(e) ]);
- }
+ return new Template(input);
};
// TODO: Make this a property on a token class
@@ -302,12 +344,6 @@ function split_token(input) {
exports.split_token = split_token;
-// TODO: Node lists are always created by the Parser class, so it could extend the list with this method.
-function evaluate_node_list (node_list, context) {
- return node_list.reduce( function (p, c) { return p + c(context); }, '');
-}
-exports.evaluate_node_list = evaluate_node_list;
-
// exported for test
exports.Context = Context;
View
30 template/template.test.js
@@ -1,6 +1,6 @@
var sys = require('sys');
-process.mixin(GLOBAL, require('../utils/test').dsl);
-process.mixin(GLOBAL, require('./template'));
+process.mixin(GLOBAL, require('utils/test').dsl);
+process.mixin(GLOBAL, require('template/template'));
testcase('Test tokenizer');
test('sanity test', function () {
@@ -36,8 +36,8 @@ testcase('Filter Expression tests');
new FilterExpression("item.subitem|add|sub")
);
assertEquals(
- { variable: 'item', filter_list: [ { name: 'add', arg: 5 }, { name: 'sub', arg: 2 } ] },
- new FilterExpression('item|add:"5"|sub:"2"')
+ { variable: 'item', filter_list: [ { name: 'add', arg: 5 }, { name: 'sub', arg: "2" } ] },
+ new FilterExpression('item|add:5|sub:"2"')
);
assertEquals(
{ variable: 'item', filter_list: [ { name: 'concat', arg: 'heste er naijs' } ] },
@@ -60,11 +60,12 @@ testcase('Filter Expression tests');
test('should fail on invalid syntax', function () {
function attempt(s) { return new FilterExpression(s); }
- shouldThrow(attempt, 'item |add:"2"');
- shouldThrow(attempt, 'item| add:"2"');
- shouldThrow(attempt, 'item|add :"2"');
- shouldThrow(attempt, 'item|add: "2"');
- shouldThrow(attempt, 'item|add|:"2"|sub');
+ shouldThrow(attempt, 'item |add:2');
+ shouldThrow(attempt, 'item| add:2');
+ shouldThrow(attempt, 'item|add :2');
+ shouldThrow(attempt, 'item|add: 2');
+ shouldThrow(attempt, 'item|add|:2|sub');
+ shouldThrow(attempt, 'item|add:2 |sub');
});
testcase('Context test');
@@ -114,5 +115,16 @@ testcase('Context test');
assertEquals(tc.plain.a, tc.context.get('a'));
});
+testcase('parser')
+ test('should parse', function () {
+ t = parse('hest');
+ assertEquals('hest', t.render());
+ });
+ test('node_list only_types should return only requested typed', function () {
+ t = parse('{% comment %}hest{% endcomment %}hest{% comment %}laks{% endcomment %}{% hest %}');
+ assertEquals(['comment','comment'], t.node_list.only_types('comment').map(function(x){return x.type}));
+ assertEquals(['text','UNKNOWN'], t.node_list.only_types('text', 'UNKNOWN').map(function(x){return x.type}));
+ });
+
run();
View
156 template/template_defaults.js
@@ -1,11 +1,11 @@
-"use strict";
/*jslint eqeqeq: true, undef: true, regexp: false */
/*global require, process, exports, escape */
var sys = require('sys');
-var template = require('./template');
-var utils = require('../utils/utils');
+var template = require('template/template');
+var template_loader = require('template/loader');
+var utils = require('utils/utils');
/* TODO: Missing filters
@@ -23,7 +23,6 @@ var utils = require('../utils/utils');
timeuntil
truncatewords_html
unordered_list
- urlencode
urlize
urlizetrunc
wordcount
@@ -33,6 +32,31 @@ var utils = require('../utils/utils');
NOTE:
date() filter is not lozalized and has a few gotchas...
stringformat() filter is regular sprintf compliant and doesn't have real python syntax
+
+Missing tags:
+ for ( missing 'empty' tag )
+
+ autoescape
+
+ include
+ ssi
+ load
+
+ debug
+ firstof
+ ifchanged
+ ifequal
+ ifnotequal
+ now
+ regroup
+ spaceless
+ templatetag
+ url
+ widthratio
+ with
+
+NOTE:
+ cycle tag does not support legacy syntax (row1,row2,row3)
*/
var filters = exports.filters = {
@@ -244,7 +268,7 @@ var nodes = exports.nodes = {
});
context.set(itemname, o);
- out += template.evaluate_node_list( node_list, context );
+ out += node_list.evaluate( context );
});
context.pop();
@@ -265,16 +289,62 @@ var nodes = exports.nodes = {
not_item_names.map( context.get, context ).map( not )
);
- var isTrue = items.reduce( operator === 'and' ? and : or, true );
+ var isTrue = items.reduce( operator === 'or' ? or : and, true );
if (isTrue) {
- return template.evaluate_node_list( if_node_list, context );
+ return if_node_list.evaluate( context );
} else if (else_node_list.length) {
- return template.evaluate_node_list( else_node_list, context );
+ return else_node_list.evaluate( context );
} else {
return '';
}
};
+ },
+
+ CycleNode: function (items) {
+
+ var cnt = 0;
+
+ return function (context) {
+
+ var choices = items.map( context.get, context );
+ var val = choices[cnt];
+ cnt = (cnt + 1) % choices.length;
+ return val;
+ };
+ },
+
+ FilterNode: function (expression, node_list) {
+ return function (context) {
+ expression.constant = node_list.evaluate( context );
+ return expression.resolve(context);
+ };
+ },
+
+ BlockNode: function (name, node_list) {
+ return function (context) {
+
+ if (context.blocks[name]) {
+ context.push({ block: { super: context.blocks[name] }});
+ } else {
+ context.push();
+ }
+
+ context.blocks[name] = node_list.evaluate( context );
+ context.pop();
+
+ return context.block_placeholder(name);;
+ };
+ },
+
+ ExtendsNode: function (item) {
+ return function (context) {
+ var name = context.get(item);
+ var parent_template = template_loader.load(name);
+ var parent_rendered = parent_template.render(context, 'delay_blocks');
+ context.extends.push( parent_rendered );
+ return '';
+ };
}
};
@@ -286,6 +356,12 @@ var callbacks = exports.callbacks = {
return nodes.VariableNode( new template.FilterExpression(token.contents) );
},
+ 'comment': function (parser, token) {
+ parser.parse('endcomment');
+ parser.delete_first_token();
+ return nodes.TextNode('');
+ },
+
'for': function (parser, token) {
var parts = template.split_token(token.contents);
@@ -346,6 +422,70 @@ var callbacks = exports.callbacks = {
}
return nodes.IfNode(item_names, not_item_names, operator, node_list, else_list);
+ },
+
+ 'cycle': function (parser, token) {
+ var parts = template.split_token(token.contents);
+
+ if (parts[0] !== 'cycle') { throw 'unexpected syntax in "cycle" tag'; }
+
+ var items = parts.slice(1);
+ var as_idx = items.indexOf('as');
+ var name = '';
+
+ if (items.length === 1) {
+ if (!parser.cycles || !parser.cycles[items[0]]) {
+ throw 'no cycle named ' + items[0] + '!';
+ } else {
+ return parser.cycles[items[0]];
+ }
+ }
+
+ if (as_idx > 0) {
+ if (as_idx === items.length - 1) {
+ throw 'unexpected syntax in "cycle" tag. Expected name after as';
+ }
+
+ name = items[items.length - 1];
+ items = items.slice(0, items.length - 2);
+
+ if (!parser.cycles) { parser.cycles = {}; }
+ parser.cycles[name] = nodes.CycleNode(items);
+ return parser.cycles[name];
+ }
+
+ return nodes.CycleNode(items);
+ },
+
+ 'filter': function (parser, token) {
+ var parts = template.split_token(token.contents);
+ if (parts[0] !== 'filter' || parts.length > 2) { throw 'unexpected syntax in "filter" tag'; }
+
+ var expr = new template.FilterExpression('|' + parts[1], ' ');
+
+ var node_list = parser.parse('endfilter');
+ parser.delete_first_token();
+
+ return nodes.FilterNode(expr, node_list);
+ },
+
+ 'block': function (parser, token) {
+ var parts = template.split_token(token.contents);
+ if (parts[0] !== 'block' || parts.length !== 2) { throw 'unexpected syntax in "block" tag'; }
+ var name = parts[1];
+
+ var node_list = parser.parse('endblock');
+ parser.delete_first_token();
+
+ return nodes.BlockNode(name, node_list);
+ },
+
+ 'extends': function (parser, token) {
+ var parts = template.split_token(token.contents);
+ if (parts[0] !== 'extends' || parts.length !== 2) { throw 'unexpected syntax in "block" tag'; }
+ var name = parts[1];
+
+ return nodes.ExtendsNode(name);
}
};
View
4 template/template_defaults.test.js
@@ -1,6 +1,6 @@
var sys = require('sys');
-process.mixin(GLOBAL, require('../utils/test').dsl);
-process.mixin(GLOBAL, require('./template_defaults'));
+process.mixin(GLOBAL, require('utils/test').dsl);
+process.mixin(GLOBAL, require('template/template_defaults'));
testcase('add')
test('should add correctly', function () {
View
26 template_example.js
@@ -1,22 +1,12 @@
var posix = require('posix'),
sys = require('sys'),
- dj = require('./djangode'),
- template_system = require('./template/template');
+ dj = require('djangode'),
+ template_system = require('template/template');
+ template_loader = require('template/loader');
-// load templates
-var templates = {};
-function parse_templates(path) {
- posix.readdir(path).addCallback( function (files) {
- files.forEach( function (file) {
- posix.cat(path + '/' + file).addCallback(function (content) {
- templates[path + '/' + file] = template_system.parse(content);
- });
- });
- });
-}
-
-parse_templates('template-demo');
+// set template path
+template_loader.set_path('template-demo');
// context to use when rendering template. In a real app this would likely come from a database
var test_context = {
@@ -28,7 +18,7 @@ var test_context = {
ordered_warranty: true,
ship: {
name: 'M/S Martha',
- nationality: 'Danish',
+ nationality: 'Danish'
}
};
@@ -38,12 +28,12 @@ var app = dj.makeApp([
['^/(template-demo/.*)$', dj.serveFile],
['^/template$', function (req, res) {
- var html = templates["template-demo/template.html"].render(test_context);
+ var html = template_loader.load('template.html').render(test_context);
dj.respond(res, html);
}],
['^/text$', function (req, res) {
- var html = templates["template-demo/template.html"].render(test_context);
+ var html = template_loader.load('template.html').render(test_context);
dj.respond(res, html, 'text/plain');
}]
]);
View
4 utils/html.test.js
@@ -1,5 +1,5 @@
-process.mixin(GLOBAL, require('../utils/test').dsl);
-process.mixin(GLOBAL, require('./html'));
+process.mixin(GLOBAL, require('utils/test').dsl);
+process.mixin(GLOBAL, require('utils/html'));
testcase('tests for linebreaks()')
test('should break lines into <p> and <br /> tags', function () {
View
4 utils/string.test.js
@@ -1,6 +1,6 @@
var sys = require('sys');
-process.mixin(GLOBAL, require('../utils/test').dsl);
-process.mixin(GLOBAL, require('./string'));
+process.mixin(GLOBAL, require('utils/test').dsl);
+process.mixin(GLOBAL, require('utils/string'));
testcase('string utility functions');
test('smart_split should split correctly', function () {
View
6 utils/utils.js
@@ -1,6 +1,6 @@
/*jslint laxbreak: true, eqeqeq: true, undef: true, regexp: false */
/*global require, exports */
-exports.string = require('./string');
-exports.date = require('./date');
-exports.html = require('./html');
+exports.string = require('utils/string');
+exports.date = require('utils/date');
+exports.html = require('utils/html');

0 comments on commit 658506a

Please sign in to comment.