From 658506a9703fe64551f23149726b3a2af8843f39 Mon Sep 17 00:00:00 2001 From: Anders Hellerup Madsen Date: Thu, 14 Jan 2010 23:51:29 +0100 Subject: [PATCH] Implemented block and extends tags --- example.js | 2 +- template/template.js | 110 +++++++++++++------- template/template.test.js | 30 ++++-- template/template_defaults.js | 156 +++++++++++++++++++++++++++-- template/template_defaults.test.js | 4 +- template_example.js | 26 ++--- utils/html.test.js | 4 +- utils/string.test.js | 4 +- utils/utils.js | 6 +- 9 files changed, 260 insertions(+), 82 deletions(-) diff --git a/example.js b/example.js index 4e672ce..0f57467 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -var dj = require('./djangode'); +var dj = require('djangode'); var app = dj.makeApp([ ['^/$', function(req, res) { diff --git a/template/template.js b/template/template.js index 2a0262e..9aeaaa1 100644 --- a/template/template.js +++ b/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,6 +211,9 @@ process.mixin(Context.prototype, { }, pop: function () { return this.scope.shift(); + }, + block_placeholder: function (name) { + return this.blockmark + name + this.blockmark; } }); @@ -189,17 +221,17 @@ process.mixin(Context.prototype, { 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; diff --git a/template/template.test.js b/template/template.test.js index 958a46b..d138024 100644 --- a/template/template.test.js +++ b/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(); diff --git a/template/template_defaults.js b/template/template_defaults.js index f856de2..96fdda4 100644 --- a/template/template_defaults.js +++ b/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); } }; diff --git a/template/template_defaults.test.js b/template/template_defaults.test.js index a04e8b0..0dcdd84 100644 --- a/template/template_defaults.test.js +++ b/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 () { diff --git a/template_example.js b/template_example.js index 15d196d..11d0334 100644 --- a/template_example.js +++ b/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'); }] ]); diff --git a/utils/html.test.js b/utils/html.test.js index 4741d17..4ea4237 100644 --- a/utils/html.test.js +++ b/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

and
tags', function () { diff --git a/utils/string.test.js b/utils/string.test.js index fedb055..8d39e95 100644 --- a/utils/string.test.js +++ b/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 () { diff --git a/utils/utils.js b/utils/utils.js index 3b2a121..e0838d4 100644 --- a/utils/utils.js +++ b/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');