зеркало из https://github.com/mozilla/djangode.git
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:
Родитель
9218725b8e
Коммит
e66d765aa1
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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. :-(');
|
||||
}
|
||||
|
Двоичный файл не отображается.
|
@ -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>
|
||||
|
Загрузка…
Ссылка в новой задаче