Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First version of a django compatible template system. Parser and toke…

…nizer are somewhat usefull.

Text literals and variable tags (TODO: filters and escaping) and variable scoping are implemented.

Template tags implemented:
    For
    If

TODO:
 - parsing and executing is done outside of a scope that djangode can track, which means the server dies on errors instead of reporting 500.
 - implement filters and escaping of variables
 - implement more standard django tags
 - implement more standard django filters
  • Loading branch information...
commit e66d765aa1e83963500732bb0f4df82e3ca372f1 1 parent 9218725
Anders Hellerup Madsen authored
View
150 template_defaults.js
@@ -0,0 +1,150 @@
+var sys = require('sys');
+var template = require('./template_system');
+
+
+exports.callbacks = {
+ 'text': function (parser, token) { return TextNode(token.contents); },
+
+ 'variable': function (parser, token) {
+ // TODO: use split_token here
+ return VariableNode(token.contents[0], token.contents.slice(1));
+ },
+
+ 'for': function (parser, token) {
+
+ var parts = template.split_token(token.contents);
+
+ if (parts[0] !== 'for' || parts[2] !== 'in' || (parts[4] && parts[4] !== 'reversed')) {
+ throw 'unexpected syntax in "for" tag' + sys.inspect(parts);
+ }
+
+ var itemname = parts[1],
+ listname = parts[3],
+ isReversed = (parts[4] === 'reversed'),
+ node_list = parser.parse('endfor');
+
+ parser.delete_first_token();
+
+ return ForNode(itemname, listname, node_list, isReversed);
+ },
+
+ 'if': function (parser, token) {
+
+ var parts = template.split_token( token.contents );
+
+ if (parts[0] !== 'if') { throw 'unexpected syntax in "if" tag'; }
+
+ // get rid of if keyword
+ parts.shift();
+
+ var operator = '',
+ item_names = [],
+ not_item_names = [];
+
+ var p, next_should_be_item = true;
+
+ while (p = parts.shift()) {
+ if (next_should_be_item) {
+ if (p === 'not') {
+ p = parts.shift();
+ if (!p) { throw 'unexpected syntax in "if" tag. Expected item name after not'; }
+ not_item_names.push( p );
+ } else {
+ item_names.push( p );
+ }
+ next_should_be_item = false;
+ } else {
+ if (p !== 'and' && p !== 'or') { throw 'unexpected syntax in "if" tag. Expected "and" or "or"'; }
+ if (operator && p !== operator) { throw 'unexpected syntax in "if" tag. Cannot mix "and" and "or"'; }
+ operator = p;
+ expect_item = true;
+ }
+ }
+
+ var node_list, else_list = [];
+
+ node_list = parser.parse('else', 'endif');
+ if (parser.next_token().type === 'else') {
+ else_list = parser.parse('endif');
+ }
+
+ parser.delete_first_token();
+
+ return IfNode(item_names, not_item_names, operator, node_list, else_list);
+ }
+};
+
+function TextNode(text) {
+ return function () { return text; }
+}
+exports.TextNode = TextNode;
+
+
+function VariableNode(name, filters) {
+
+ // TODO: Filters
+ return function (context) { return context.get(name); }
+}
+exports.VariableNode = VariableNode;
+
+
+function ForNode(itemname, listname, node_list, isReversed) {
+
+ return function (context) {
+ var forloop = { parentloop: context.get('forloop') },
+ list = context.get(listname),
+ out = '';
+
+
+ if (! list instanceof Array) { return TextNode(''); }
+ if (isReversed) { list = list.slice(0).reverse(); }
+
+ context.push();
+ context.set('forloop', forloop);
+
+ list.forEach( function (o, idx, iter) {
+ process.mixin(forloop, {
+ counter: idx + 1,
+ counter0: idx,
+ revcounter: iter.length - idx,
+ revcounter0: iter.length - (idx + 1),
+ first: idx === 0,
+ last: idx === iter.length - 1,
+ });
+ context.set(itemname, o);
+
+ out += template.evaluate_node_list( node_list, context );
+ });
+
+ context.pop();
+
+ return out;
+ };
+}
+exports.ForNode = ForNode;
+
+function IfNode(item_names, not_item_names, operator, if_node_list, else_node_list) {
+
+ return function (context) {
+
+ function not(x) { return !x; }
+ function and(p,c) { return p && c; }
+ function or(p,c) { return p || c; }
+
+ var items = item_names.map( context.get, context ).concat(
+ not_item_names.map( context.get, context ).map( not )
+ );
+
+ var isTrue = items.reduce( operator === 'and' ? and : or, true );
+
+ if (isTrue) {
+ return template.evaluate_node_list( if_node_list, context );
+ } else if (else_node_list.length) {
+ return template.evaluate_node_list( else_node_list, context );
+ } else {
+ return '';
+ }
+ };
+}
+exports.IfNode = IfNode;
+
View
46 template_example.js
@@ -0,0 +1,46 @@
+var posix = require('posix'),
+ sys = require('sys'),
+ dj = require('./djangode'),
+ template = require('./template_system');
+
+var test_context = {
+ person_name: 'Thomas Hest',
+ company: 'Tobis A/S',
+ ship_date: '2. januar, 2010',
+ item: 'XXX',
+ item_list: [ 'Giraf', 'Fisk', 'Tapir'],
+ ordered_warranty: true,
+ ship: {
+ name: 'M/S Martha',
+ nationality: 'Danish',
+ }
+};
+
+var app = dj.makeApp([
+ ['^/raw$', function (req, res) {
+ posix.cat("templates/template.html").addCallback( function (content) {
+ dj.respond(res, content, 'text/plain');
+ });
+ }],
+ ['^/tokens$', function (req, res) {
+ posix.cat("templates/template.html").addCallback( function (content) {
+ var t = template.tokenize(content);
+ dj.respond(res, sys.inspect(t), 'text/plain');
+ });
+ }],
+ ['^/parsed$', function (req, res) {
+ posix.cat("templates/template.html").addCallback( function (content) {
+ var t = template.parse(content);
+ dj.respond(res, sys.inspect(t), 'text/plain');
+ });
+ }],
+ ['^/rendered$', function (req, res) {
+ posix.cat("templates/template.html").addCallback( function (content) {
+ var t = template.parse(content);
+ dj.respond(res, t.render(test_context), 'text/plain');
+ });
+ }],
+]);
+
+dj.serve(app, 8009);
+
View
227 template_system.js
@@ -0,0 +1,227 @@
+
+var sys = require('sys'),
+ template_defaults = require('./template_defaults');
+
+
+/***************** TOKENIZER ******************************/
+
+function tokenize(input) {
+ var re = /(?:{{|}}|{%|%})|[{}|]|[^{}%|]+/g;
+ var token_list = [];
+
+ function consume(re, input) {
+ var m = re.exec(input);
+ return m ? m[0] : null;
+ }
+
+ function consume_until() {
+ var next, s = '';
+ while (next = consume(re, input)) {
+ if (Array.prototype.slice.apply(arguments).indexOf(next) > -1) {
+ return [s, next];
+ }
+ s += next;
+ }
+ return [s];
+ }
+
+ function literal() {
+ var res = consume_until("{{", "{%");
+
+ if (res[0]) { token_list.push( {type: 'text', contents: res[0] } ); }
+
+ if (res[1] === "{{") { return variable_tag; }
+ if (res[1] === "{%") { return template_tag; }
+ return undefined;
+ }
+
+ function variable_tag() {
+ var res = consume_until("}}"),
+ token = { type: 'variable', contents: [] },
+ parts = res[0].trim().split(/\s*\|\s*/);
+
+ token.contents.push(parts.shift());
+
+ parts.forEach( function (filter) {
+ token.contents.push( filter.split(/\s*:\s*/) );
+ });
+
+ token_list.push( token );
+
+ if (res[1]) { return literal; }
+ return undefined;
+ }
+
+ function template_tag() {
+ var res = consume_until("%}"),
+ parts = res[0].trim().split(/\s/, 1);
+
+ token_list.push( { type: parts[0], contents: res[0].trim() });
+
+ if (res[1]) { return literal; }
+ return undefined;
+ }
+
+ var state = literal;
+
+ while (state) {
+ state = state();
+ }
+
+ return token_list;
+}
+
+function split_token(input) {
+ var re = /([^\s"]*"(?:[^"\\]*(?:\\.[^"\\]*)*)"\S*|[^\s']*'(?:[^'\\]*(?:\\.[^'\\]*)*)'\S*|\S+)/g,
+ out = [],
+ m = false;
+
+ while (m = re.exec(input)) {
+ out.push(m[0]);
+ }
+ return out;
+}
+
+
+/*********** PARSER **********************************/
+
+function parser_error(e) {
+ return 'Parsing exception: ' + JSON.stringify(e, 0, 2);
+}
+
+function Parser(input) {
+ this.token_list = tokenize(input);
+ this.indent = 0;
+}
+
+process.mixin(Parser.prototype, {
+
+ callbacks: template_defaults.callbacks,
+
+ parse: function () {
+
+ var stoppers = Array.prototype.slice.apply(arguments);
+ var node_list = [];
+ var token = this.token_list[0];
+ var callback = null;
+
+ //sys.debug('' + this.indent++ + ':starting parsing with stoppers ' + stoppers.join(', '));
+
+ while (this.token_list.length) {
+ if (stoppers.indexOf(this.token_list[0].type) > -1) {
+ //sys.debug('' + this.indent-- + ':parse done returning at ' + token[0] + ' (length: ' + node_list.length + ')');
+ return node_list;
+ }
+
+ token = this.next_token();
+
+ //sys.debug('' + this.indent + ': ' + token);
+
+ callback = this.callbacks[token.type];
+ if (callback && typeof callback === 'function') {
+ node_list.push( callback.call(null, this, token) );
+ } else {
+ //throw parser_error('Unknown tag: ' + token[0]);
+ node_list.push( template_defaults.TextNode('[[ UNKNOWN ' + token.type + ' ]]'));
+ }
+ }
+ if (stoppers.length) {
+ throw new parser_error('Tag not found: ' + stoppers.join(', '));
+ }
+
+ //sys.debug('' + this.indent-- + ':parse done returning end (length: ' + node_list.length + ')');
+
+ return node_list;
+ },
+
+ next_token: function () {
+ return this.token_list.shift();
+ },
+
+ delete_first_token: function () {
+ this.token_list.shift();
+ },
+
+});
+
+function evaluate_node_list (node_list, context) {
+ return node_list.reduce( function (p, c) { return p + c(context); }, '');
+}
+
+/*************** Context *********************************/
+
+function Context(o) {
+ this.scope = [ o ];
+}
+
+process.mixin(Context.prototype, {
+ get: function (name) {
+
+ if (name === 'true') { return true; }
+ if (name === 'false') { return false; }
+ if (/\d/.exec(name[0])) { return Number(name); }
+
+ var isStringLiteral = /^(["'])(.*?)\1$/.exec(name);
+ if (isStringLiteral) { return isStringLiteral.pop(); }
+
+ var parts = name.split('.');
+ name = parts.shift();
+
+ var val, level, next;
+ for (level = 0; level < this.scope.length; level++) {
+ if (this.scope[level].hasOwnProperty(name)) {
+ val = this.scope[level][name];
+ while (parts.length && val) {
+ next = val[parts.shift()];
+ if (typeof next === 'function') {
+ val = next.apply(val);
+ } else {
+ val = next;
+ }
+ }
+
+ return val;
+ }
+ }
+
+ return '';
+ },
+ set: function (name, value) {
+ this.scope[0][name] = value;
+ },
+ push: function (o) {
+ this.scope.unshift(o || {});
+ },
+ pop: function () {
+ return this.scope.shift();
+ }
+});
+
+
+/*********** Template **********************************/
+
+function Template(node_list) {
+ this.node_list = node_list;
+}
+
+process.mixin(Template.prototype, {
+ render: function (o) {
+ context = new Context(o);
+ return evaluate_node_list(this.node_list, context);
+ }
+});
+
+/********************************************************/
+
+exports.parse = function (input) {
+ var parser = new Parser(input);
+ return new Template(parser.parse());
+}
+exports.split_token = split_token;
+exports.evaluate_node_list = evaluate_node_list;
+
+// exported for test
+exports.Context = Context;
+exports.tokenize = tokenize;
+
+
View
139 template_system_test.js
@@ -0,0 +1,139 @@
+var sys = require('sys');
+var template = require('./template_system');
+process.mixin(GLOBAL, require('mjsunit'));
+
+function run_testcase(testcase) {
+ var test, fail, fail_cnt, success_cnt, context;
+
+ sys.puts('====\nTESTCASE: ' + testcase.title + '\n--');
+
+ context = testcase.setup ? context = testcase.setup() : {};
+ fail_cnt = success_cnt = 0;
+
+ for (test in testcase) {
+ if (testcase.hasOwnProperty(test) && test.slice(0,4) === 'test') {
+ if (testcase.before) {
+ context = testcase.before(context);
+ }
+ fail = '';
+ try {
+ testcase[test].call(testcase, context);
+ success_cnt++;
+ } catch (e) {
+ if ('stack' in e && 'type' in e) {
+ fail = e.stack;
+ } else {
+ fail = e.toString();
+ }
+ fail_cnt++;
+ }
+ if (fail) {
+ sys.puts('' + test + ': ' + fail);
+ } else {
+ sys.puts('' + test + ': passed');
+ }
+ }
+ }
+
+ if (fail_cnt > 0) {
+ sys.puts('--\nfailed: ' + success_cnt + ' tests passed, ' + fail_cnt + ' failed\n====\n');
+ } else {
+ sys.puts('--\nsuccess: ' + success_cnt + ' tests passed.\n====\n');
+ }
+
+ return fail_cnt;
+}
+
+var cnt = 0;
+
+cnt += run_testcase({
+ title: 'Tokenizer tests',
+
+ testTokenizer: function (t) {
+ var tokens = template.tokenize('Hest');
+ assertEquals(
+ JSON.stringify([{type:'text', contents: 'Hest'}]),
+ JSON.stringify(tokens)
+ );
+ },
+
+ testNoEmptyTextTokens: function (t) {
+ var tokens = template.tokenize('{{tag}}');
+ assertEquals(
+ JSON.stringify([{type:'variable', contents: ['tag']}]),
+ JSON.stringify(tokens)
+ );
+ },
+
+ testSplitToken: function (t) {
+ assertArrayEquals(
+ ['virker', 'det', 'her'],
+ template.split_token(' virker det her ')
+ );
+ assertArrayEquals(
+ ['her', 'er', '"noget der er i qoutes"', 'og', 'noget', 'der', 'ikke', 'er'],
+ template.split_token('her er "noget der er i qoutes" og noget der ikke er')
+ );
+
+ // TODO: Is this the correct result for these two tests?
+ assertArrayEquals( ['date:"F j, Y"'], template.split_token('date:"F j, Y"'));
+ assertArrayEquals( ['date:', '"F j, Y"'], template.split_token('date: "F j, Y"'));
+ }
+});
+
+
+cnt += run_testcase({
+ title: 'Context tests',
+
+ before: function (t) {
+ t.plain = {
+ a: 5,
+ b: 'hest',
+ c: true,
+ d: [ 1, 2, 3, 4 ],
+ };
+
+ var clone = JSON.parse(JSON.stringify(t.plain))
+
+ t.context = new template.Context(clone);
+
+ return t;
+ },
+
+ testGetFromFirstLevel: function (t) {
+ for (x in t.plain) {
+ if (typeof t.plain[x] === 'array') {
+ assertArrayEquals(t.plain[x], t.context.get(x));
+ } else {
+ assertEquals(t.plain[x], t.context.get(x));
+ }
+ }
+ },
+
+ testGetStringLiteral: function (t) {
+ assertEquals(5, t.context.get('a'));
+ assertEquals('a', t.context.get("'a'"));
+ assertEquals('a', t.context.get('"a"'));
+ },
+
+ testSet: function (t) {
+ t.context.set('a', t.plain.a + 100);
+ assertEquals(t.plain.a + 100, t.context.get('a'));
+ },
+
+ testPushAndPop: function (t) {
+ t.context.push();
+ assertEquals(t.plain.a, t.context.get('a'));
+ t.context.pop();
+ assertEquals(t.plain.a, t.context.get('a'));
+ },
+
+});
+
+
+if (cnt === 0) {
+ sys.puts('all tests passed. :-)');
+} else {
+ sys.puts('' + cnt + ' failed tests. :-(');
+}
+
View
BIN  templates/.template.html.swp
Binary file not shown
View
35 templates/template.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <title>Ordering notice - node.js test</title>
+ </head>
+ <body>
+ <h1>Ordering notice</h1>
+
+ <p>Dear {{person_name}},</p>
+
+ <p>Thanks for placing an order from {{ company }}. It's scheduled to
+ ship on {{ ship_date|date:"F j, Y" }}.</p>
+
+ <p>Here are the items you've ordered:</p>
+
+ <ul>
+ {% for item in item_list %}
+ <li>{{ forloop.revcounter0 }}: {{ item }}</li>
+ {% endfor %}
+ </ul>
+
+ {% if ordered_warranty or true or false %}
+ <p>Your warranty information will be included in the packaging.</p>
+ {% else %}
+ <p>You didn't order a warranty, so you're on your own when
+ the products inevitably stop working.</p>
+ {% endif %}
+
+ {{ship.name}}{{ ship.nationality.toUpperCase }}
+
+ <p>Sincerely,<br />{{ company }}</p>
+
+ </body>
+</html>
+
Please sign in to comment.
Something went wrong with that request. Please try again.