This commit is contained in:
Anders Hellerup Madsen 2010-01-18 02:15:48 +01:00
Родитель 5e764095ba
Коммит 0b8c50ceea
9 изменённых файлов: 351 добавлений и 199 удалений

Просмотреть файл

@ -1,16 +1,22 @@
#!/usr/bin/python
import re, os
from subprocess import Popen, PIPE
print ""
cmd = 'find . -name "*.test.js"';
files = [file.rstrip('\n') for file in os.popen(cmd).readlines()]
files = Popen(cmd, shell=True, stdout=PIPE).communicate()[0].splitlines()
failed_list = []
for file in files:
output = os.popen('node ' + file).readlines()
result = [line.rstrip('\n') for line in output if line.startswith('Total')][0]
output = Popen('node ' + file, shell=True, stdout=PIPE).communicate()[0].splitlines()
try:
result = [line for line in output if line.startswith('Total')][0]
except:
#bizarre, but sometimes popen apears to return empty strings
#I'm too tired to fix this right now, so for now just retry and hope for better results
output = Popen('node ' + file, shell=True, stdout=PIPE).communicate()[0].splitlines()
result = [line for line in output if line.startswith('Total')][0]
(total, failed, error) = re.split(r':|,', result)[1::2]
if int(failed) > 0 or int(error) > 0:

Просмотреть файл

@ -7,6 +7,19 @@ var utils = require('utils/utils');
var template_defaults = require('template/template_defaults');
var template_loader = require('template/loader');
/***************** TOKEN **********************************/
function Token(type, contents) {
this.type = type;
this.contents = contents;
}
process.mixin(Token.prototype, {
split_contents: function () {
return utils.string.smart_split(this.contents);
}
});
/***************** TOKENIZER ******************************/
function tokenize(input) {
@ -33,7 +46,7 @@ function tokenize(input) {
function literal() {
var res = consume_until("{{", "{%");
if (res[0]) { token_list.push( {type: 'text', contents: res[0] } ); }
if (res[0]) { token_list.push( new Token('text', res[0]) ) }
if (res[1] === "{{") { return variable_tag; }
if (res[1] === "{%") { return template_tag; }
@ -43,8 +56,7 @@ function tokenize(input) {
function variable_tag() {
var res = consume_until("}}");
if (res[0]) { token_list.push( {type: 'variable', contents: res[0].trim() } ); }
if (res[0]) { token_list.push( new Token('variable', res[0].trim()) ) }
if (res[1]) { return literal; }
return undefined;
}
@ -53,7 +65,7 @@ function tokenize(input) {
var res = consume_until("%}"),
parts = res[0].trim().split(/\s/, 1);
token_list.push( { type: parts[0], contents: res[0].trim() });
token_list.push( new Token(parts[0], res[0].trim()) );
if (res[1]) { return literal; }
return undefined;
@ -68,152 +80,6 @@ function tokenize(input) {
return token_list;
}
/*********** 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 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, {
callbacks: template_defaults.callbacks,
parse: function () {
var stoppers = Array.prototype.slice.apply(arguments);
var node_list = make_nodelist();
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.append( callback(this, token), token.type );
} else {
//throw parser_error('Unknown tag: ' + token[0]);
node_list.append(
template_defaults.nodes.TextNode('[[ UNKNOWN ' + token.type + ' ]]'),
'UNKNOWN'
);
}
}
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 normalize(value) {
if (typeof value !== 'string') { return value; }
if (value === 'true') { return true; }
if (value === 'false') { return false; }
if (/^\d/.exec(value)) { return value - 0; }
var isStringLiteral = /^(["'])(.*?)\1$/.exec(value);
if (isStringLiteral) { return isStringLiteral.pop(); }
return value;
}
/*************** Context *********************************/
function Context(o) {
this.scope = [ o || {} ];
this.blocks = {};
this.autoescaping = true;
}
process.mixin(Context.prototype, {
get: function (name) {
var normalized = normalize(name);
if (name !== normalized) { return normalized; }
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;
}
}
if (typeof val === 'function') {
return val();
} else {
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();
},
});
/*********** FilterExpression **************************/
var FilterExpression = function (expression, constant) {
@ -307,7 +173,155 @@ process.mixin(FilterExpression.prototype, {
}
});
exports.FilterExpression = FilterExpression;
/*********** 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 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, {
callbacks: template_defaults.callbacks,
parse: function () {
var stoppers = Array.prototype.slice.apply(arguments);
var node_list = make_nodelist();
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.append( callback(this, token), token.type );
} else {
//throw parser_error('Unknown tag: ' + token[0]);
node_list.append(
template_defaults.nodes.TextNode('[[ UNKNOWN ' + token.type + ' ]]'),
'UNKNOWN'
);
}
}
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();
},
make_filterexpression: function (expression, constant) {
return new FilterExpression(expression, constant);
}
});
function normalize(value) {
if (typeof value !== 'string') { return value; }
if (value === 'true') { return true; }
if (value === 'false') { return false; }
if (/^\d/.exec(value)) { return value - 0; }
var isStringLiteral = /^(["'])(.*?)\1$/.exec(value);
if (isStringLiteral) { return isStringLiteral.pop(); }
return value;
}
/*************** Context *********************************/
function Context(o) {
this.scope = [ o || {} ];
this.extends = '';
this.blocks = {};
this.autoescaping = true;
}
process.mixin(Context.prototype, {
get: function (name) {
var normalized = normalize(name);
if (name !== normalized) { return normalized; }
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;
}
}
if (typeof val === 'function') {
return val();
} else {
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 **********************************/
@ -321,7 +335,7 @@ process.mixin(Template.prototype, {
render: function (o) {
var context = (o instanceof Context) ? o : new Context(o || {});
context.extends = false;
context.extends = '';
var rendered = this.node_list.evaluate(context);
@ -342,16 +356,12 @@ exports.parse = function (input) {
return new Template(input);
};
// TODO: Make this a property on a token class
function split_token(input) {
return utils.string.smart_split(input);
}
exports.split_token = split_token;
// exported for test
exports.Context = Context;
exports.FilterExpression = FilterExpression;
exports.tokenize = tokenize;

Просмотреть файл

@ -14,15 +14,15 @@ testcase('Test tokenizer');
test('split token contents', function () {
assertEquals(
['virker', 'det', 'her'],
split_token(' virker det her ')
tokenize(' virker det her ')[0].split_contents()
);
assertEquals(
['her', 'er', '"noget der er i qoutes"', 'og', 'noget', 'der', 'ikke', 'er'],
split_token('her er "noget der er i qoutes" og noget der ikke er')
tokenize('her er "noget der er i qoutes" og noget der ikke er')[0].split_contents()
);
assertEquals( ['date:"F j, Y"'], split_token('date:"F j, Y"'));
assertEquals( ['date:', '"F j, Y"'], split_token('date: "F j, Y"'));
assertEquals( ['date:"F j, Y"'], tokenize('date:"F j, Y"')[0].split_contents());
assertEquals( ['date:', '"F j, Y"'], tokenize('date: "F j, Y"')[0].split_contents());
});
testcase('Filter Expression tests');

Просмотреть файл

@ -2,8 +2,6 @@
/*global require, process, exports, escape */
var sys = require('sys');
var template = require('template/template');
var utils = require('utils/utils');
/* TODO: Missing filters
@ -15,7 +13,6 @@ var utils = require('utils/utils');
time
timesince
timeuntil
truncatewords_html
unordered_list
urlize
urlizetrunc
@ -35,7 +32,7 @@ Missing tags:
load
debug
firstof
ifchanged
ifequal
ifnotequal
@ -45,7 +42,6 @@ Missing tags:
templatetag
url
widthratio
with
NOTE:
cycle tag does not support legacy syntax (row1,row2,row3)
@ -129,24 +125,19 @@ var filters = exports.filters = {
length: function (value, arg) { return value.length ? value.length : 0; },
length_is: function (value, arg) { return value.length === arg; },
linebreaks: function (value, arg, safety) {
if (!safety.is_safe && safety.must_escape) {
value = utils.html.escape("" + value);
}
var out = utils.html.linebreaks("" + value, { escape: !safety.is_safe && safety.must_escape });
safety.is_safe = true;
return utils.html.linebreaks("" + value);
return out;
},
linebreaksbr: function (value, arg, safety) {
if (!safety.is_safe && safety.must_escape) {
value = utils.html.escape("" + value);
}
var out = utils.html.linebreaks("" + value, { onlybr: true, escape: !safety.is_safe && safety.must_escape });
safety.is_safe = true;
return "" + value.replace(/\n/g, '<br />');
return out;
},
linenumbers: function (value, arg, safety) {
var lines = String(value).split('\n');
var len = String(lines.length).length;
// TODO: escape if string is not safe, and autoescaping is active
var out = lines
.map(function (s, idx) {
if (!safety.is_safe && safety.must_escape) {
@ -241,6 +232,10 @@ var filters = exports.filters = {
truncatewords: function (value, arg) {
return String(value).split(/\s+/g).slice(0, arg).join(' ') + ' ...';
},
truncatewords_html: function (value, arg, safety) {
safety.is_safe = true;
return utils.html.truncate_html_words(value, arg);
},
upper: function (value, arg) {
return (value + '').toUpperCase();
},
@ -384,6 +379,28 @@ var nodes = exports.nodes = {
context.autoescaping = before;
return out;
}
},
FirstOfNode: function (choices) {
return function (context) {
var i, val;
for (i = 0; i < choices.length; i++) {
var val = context.get(choices[i]);
if (val) { return val; }
}
return '';
}
},
WithNode: function (variable, name, node_list) {
return function (context) {
var item = context.get(variable);
context.push();
context.set(name, item);
var out = node_list.evaluate( context );
context.pop();
return out;
}
}
};
@ -392,7 +409,7 @@ var callbacks = exports.callbacks = {
'text': function (parser, token) { return nodes.TextNode(token.contents); },
'variable': function (parser, token) {
return nodes.VariableNode( new template.FilterExpression(token.contents) );
return nodes.VariableNode( parser.make_filterexpression(token.contents) );
},
'comment': function (parser, token) {
@ -403,7 +420,7 @@ var callbacks = exports.callbacks = {
'for': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'for' || parts[2] !== 'in' || (parts[4] && parts[4] !== 'reversed')) {
throw 'unexpected syntax in "for" tag: ' + token.contents;
@ -421,7 +438,7 @@ var callbacks = exports.callbacks = {
'if': function (parser, token) {
var parts = template.split_token( token.contents );
var parts = token.split_contents();
if (parts[0] !== 'if') { throw 'unexpected syntax in "if" tag'; }
@ -464,7 +481,7 @@ var callbacks = exports.callbacks = {
},
'cycle': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'cycle') { throw 'unexpected syntax in "cycle" tag'; }
@ -497,10 +514,10 @@ var callbacks = exports.callbacks = {
},
'filter': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'filter' || parts.length > 2) { throw 'unexpected syntax in "filter" tag'; }
var expr = new template.FilterExpression('|' + parts[1], ' ');
var expr = parser.make_filterexpression('|' + parts[1], ' ');
var node_list = parser.parse('endfilter');
parser.delete_first_token();
@ -509,7 +526,7 @@ var callbacks = exports.callbacks = {
},
'block': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'block' || parts.length !== 2) { throw 'unexpected syntax in "block" tag'; }
var name = parts[1];
@ -520,7 +537,7 @@ var callbacks = exports.callbacks = {
},
'extends': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'extends' || parts.length !== 2) { throw 'unexpected syntax in "extends" tag'; }
var name = parts[1];
@ -528,7 +545,7 @@ var callbacks = exports.callbacks = {
},
'autoescape': function (parser, token) {
var parts = template.split_token(token.contents);
var parts = token.split_contents();
if (parts[0] !== 'autoescape' || parts.length !== 2) { throw 'unexpected syntax in "autoescape" tag'; }
var enable;
if (parts[1] === 'on') {
@ -543,6 +560,23 @@ var callbacks = exports.callbacks = {
parser.delete_first_token();
return nodes.AutoescapeNode(enable, node_list);
},
'firstof': function (parser, token) {
var parts = token.split_contents();
if (parts[0] !== 'firstof') { throw 'unexpected syntax in "firstof" tag'; }
return nodes.FirstOfNode( parts.slice(1) );
},
'with': function (parser, token) {
var parts = token.split_contents();
if (parts[0] !== 'with' || parts[2] !== 'as' || parts.length !== 4) {
throw 'unexpected syntax in "with" tag';
}
var node_list = parser.parse('endwith');
parser.delete_first_token();
return nodes.WithNode(parts[1], parts[3], node_list);
}
};

Просмотреть файл

@ -160,6 +160,23 @@ testcase('autoescape')
var t = template.parse('{% autoescape on %}{{ test }}{% endautoescape %}');
assertEquals( '&lt;script&gt;', t.render( {test: '<script>'} ));
});
testcase('firstof')
test('should parse and evaluate', function () {
var t = template.parse('{% firstof var1 var2 var3 %}');
assertEquals('hest', t.render( { var1: 'hest' } ));
assertEquals('hest', t.render( { var2: 'hest' } ));
assertEquals('', t.render());
t = template.parse('{% firstof var1 var2 var3 "fallback" %}');
assertEquals('fallback', t.render());
});
testcase('with')
test('function result should be cached', function () {
var t = template.parse('{% with test.sub.func as tmp %}{{ tmp }}:{{ tmp }}{% endwith %}');
var cnt = 0;
var o = { test: { sub: { func: function () { cnt++; return cnt; } } } }
assertEquals('1:1', t.render(o));
assertEquals(1, cnt);
});
run();

Просмотреть файл

@ -377,5 +377,15 @@ testcase('escape');
filters.escape('hurra', null, safety);
assertEquals(true, safety.must_escape);
});
testcase('truncatewords_html');
test('should truncate and close tags', function () {
assertEquals('Joel is ...', filters.truncatewords_html('Joel is a slug', 2, {}));
assertEquals('<p>Joel is ...</p>', filters.truncatewords_html('<p>Joel is a slug</p>', 2, {}));
});
test('should mark output as safe', function () {
var safety = {};
filters.truncatewords_html('<p>Joel is a slug</p>', 2, safety);
assertEquals(true, safety.is_safe);
});
run();

Просмотреть файл

@ -3,6 +3,11 @@
var sys = require('sys');
/* Function: escape(value);
Escapes the characters &, <, >, ' and " in string with html entities.
Arguments:
value - string to escape
*/
var escape = exports.escape = function (value) {
return value
.replace(/&/g, '&amp;')
@ -10,7 +15,7 @@ var escape = exports.escape = function (value) {
.replace(/>/g, '&gt;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&qout;');
}
};
/* Function: linebreaks(value, options);
Converts newlines into <p> and <br />s.
@ -18,17 +23,81 @@ var escape = exports.escape = function (value) {
value - string, the string to convert.
options - optional, see options
Options:
autoescape - boolean, if true pass the string through escape()
escape - boolean, if true pass the string through escape()
onlybr - boolean, if true only br tags will be created.
*/
var linebreaks = exports.linebreaks = function (value, options) {
options = options || {};
value = value.replace(/\r\n|\r|\n/g, '\n');
if (options.onlybr) {
return (options.escape ? escape(value) : value).replace(/\n/g, '<br />');
}
var lines = value.split(/\n{2,}/);
if (options.autoescape) {
if (options.escape) {
lines = lines.map( function (x) { return '<p>' + escape(x).replace('\n', '<br />') + '</p>'; } );
} else {
lines = lines.map( function (x) { return '<p>' + x.replace('\n', '<br />') + '</p>'; } );
}
return lines.join('\n\n');
}
};
var re_words = /&.*?;|<.*?>|(\w[\w\-]*)/g;
var re_tag = /<(\/)?([^ ]+?)(?: (\/)| .*?)?>/;
var html4_singlets = ['br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input'];
var truncate_html_words = exports.truncate_html_words = function (input, cnt) {
var words = 0, pos = 0, elipsis_pos = 0, length = cnt - 0;
var open_tags = [];
if (!length) { return ''; }
re_words.lastIndex = 0;
while (words <= length) {
var m = re_words( input );
if (!m) {
// parsed through string
break;
}
pos = re_words.lastIndex;
if (m[1]) {
// this is not a tag
words += 1;
if (words === length) {
elipsis_pos = pos;
}
continue;
}
var tag = re_tag( m[0] );
if (!tag || elipsis_pos) {
// don't worry about non-tags or tags after truncate point
continue;
}
var closing_tag = tag[1], tagname = tag[2].toLowerCase(), self_closing = tag[3];
if (self_closing || html4_singlets.indexOf(tagname) > -1) {
continue;
} else if (closing_tag) {
var idx = open_tags.indexOf(tagname);
if (idx > -1) {
// SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
open_tags = open_tags.slice(idx + 1);
}
} else {
open_tags.unshift( tagname );
}
}
if (words <= length) {
return input;
}
return open_tags.reduce( function (p,c) { return p + '</' + c + '>'; }, input.slice(0, elipsis_pos) + ' ...');
};

Просмотреть файл

@ -16,7 +16,14 @@ testcase('tests for linebreaks()')
+ '\n'
+ '<p>The days are just packed!<br /></p>';
assertEquals(expected, linebreaks(input));
assertEquals(expected_escaped, linebreaks(input, { autoescape: true }));
assertEquals(expected_escaped, linebreaks(input, { escape: true }));
})
testcase('truncate_html_words');
test('should truncate strings without tags', function () {
assertEquals('Joel is ...', truncate_html_words('Joel is a slug', 2));
});
test('should close tags on truncate', function () {
assertEquals('<p>Joel is ...</p>', truncate_html_words('<p>Joel is a slug</p>', 2));
});
run();

Просмотреть файл

@ -102,6 +102,9 @@ function sprintf () {
}
/*************************************************************************/
exports.sprintf = sprintf;
exports.str_repeat = str_repeat;
/*************************************************************************
* titleCaps from http://ejohn.org/files/titleCaps.js (by John Resig)
*/
@ -151,10 +154,6 @@ exports.titleCaps = titleCaps;
/*************************************************************************/
exports.sprintf = sprintf;
exports.str_repeat = str_repeat;
function center(s, width) {
if (s.length > width) { return s; }
var right = Math.round((width - s.length) / 2);