First version of a django compatible template system. Parser and tokenizer 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
This commit is contained in:
Anders Hellerup Madsen 2009-12-09 02:53:28 +01:00
Родитель 9218725b8e
Коммит e66d765aa1
6 изменённых файлов: 597 добавлений и 0 удалений

150
template_defaults.js Normal file
Просмотреть файл

@ -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;

46
template_example.js Normal file
Просмотреть файл

@ -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);

227
template_system.js Normal file
Просмотреть файл

@ -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;

139
template_system_test.js Normal file
Просмотреть файл

@ -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. :-(');
}

Двоичные данные
templates/.template.html.swp Normal file

Двоичный файл не отображается.

35
templates/template.html Normal file
Просмотреть файл

@ -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>