Permalink
Browse files

Added a couple of filters

  • Loading branch information...
1 parent 5e76409 commit 0b8c50ceea5628fbcbbb411cfa16258682376c6a Anders Hellerup Madsen committed Jan 18, 2010
View
14 regression.py
@@ -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:
View
224 template/template.js
@@ -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,6 +80,98 @@ function tokenize(input) {
return token_list;
}
+/*********** FilterExpression **************************/
+
+var FilterExpression = function (expression, constant) {
+
+ // 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);
+ }
+ if (constant) {
+ if (parsed.variable) {
+ throw this.error(expression); // did not expect variable when constant is defined...
+ } else {
+ this.constant = constant;
+ }
+ } else {
+ if (parsed.variable) {
+ if (parsed.variable !== normalize(parsed.variable)) {
+ // if normalize changed the variable it must be some form of constant
+ this.constant = normalize(parsed.variable);
+ } else {
+ this.variable = parsed.variable;
+ }
+ } else {
+ throw this.error(expression);
+ }
+ }
+
+ this.filter_list = [];
+
+ while (parsed && parsed.filter_name) {
+ this.filter_list.push( { name: parsed.filter_name, arg: normalize(parsed.filter_arg) } );
+ parsed = this.consume(expression);
+ }
+
+ if (expression.length !== this.re.lastIndex) {
+ throw this.error(expression + ' - 4');
+ }
+};
+
+process.mixin(FilterExpression.prototype, {
+
+ consume: function (expression) {
+ var start = this.re.lastIndex;
+ var m = this.re.exec(expression);
+
+ return m[0] ? { variable: m[1], filter_name: m[2], filter_arg: m[3] } : null;
+ },
+
+ error: function (s) {
+ throw s + "\ncan't parse filterexception at char " + this.re.lastIndex + ". Make sure there is no spaces between filters or arguments\n";
+ },
+
+ resolve: function (context) {
+ var value;
+ if (this.hasOwnProperty('constant')) {
+ value = this.constant;
+ } else {
+ value = context.get(this.variable);
+ }
+
+ var safety = {
+ is_safe: false,
+ must_escape: context.autoescaping,
+ };
+
+ var out = this.filter_list.reduce( function (p,c) {
+
+ var filter = template_defaults.filters[c.name];
+
+ if ( filter && typeof filter === 'function') {
+ return filter(p, c.arg, safety);
+ } else {
+ // throw 'Cannot find filter';
+ sys.debug('Cannot find filter ' + c.name);
+ return p;
+ }
+ }, value);
+
+ if (safety.must_escape && !safety.is_safe) {
+ if (typeof out === 'string') {
+ return utils.html.escape(out)
+ } else if (out instanceof Array) {
+ return out.map( function (o) { return typeof o === 'string' ? utils.html.escape(o) : o; } );
+ }
+ }
+ return out;
+ }
+});
/*********** PARSER **********************************/
@@ -146,6 +250,10 @@ process.mixin(Parser.prototype, {
delete_first_token: function () {
this.token_list.shift();
+ },
+
+ make_filterexpression: function (expression, constant) {
+ return new FilterExpression(expression, constant);
}
});
@@ -167,6 +275,7 @@ function normalize(value) {
function Context(o) {
this.scope = [ o || {} ];
+ this.extends = '';
this.blocks = {};
this.autoescaping = true;
}
@@ -214,101 +323,6 @@ process.mixin(Context.prototype, {
},
});
-/*********** FilterExpression **************************/
-
-var FilterExpression = function (expression, constant) {
-
- // 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);
- }
- if (constant) {
- if (parsed.variable) {
- throw this.error(expression); // did not expect variable when constant is defined...
- } else {
- this.constant = constant;
- }
- } else {
- if (parsed.variable) {
- if (parsed.variable !== normalize(parsed.variable)) {
- // if normalize changed the variable it must be some form of constant
- this.constant = normalize(parsed.variable);
- } else {
- this.variable = parsed.variable;
- }
- } else {
- throw this.error(expression);
- }
- }
-
- this.filter_list = [];
-
- while (parsed && parsed.filter_name) {
- this.filter_list.push( { name: parsed.filter_name, arg: normalize(parsed.filter_arg) } );
- parsed = this.consume(expression);
- }
-
- if (expression.length !== this.re.lastIndex) {
- throw this.error(expression + ' - 4');
- }
-};
-
-process.mixin(FilterExpression.prototype, {
-
- consume: function (expression) {
- var start = this.re.lastIndex;
- var m = this.re.exec(expression);
-
- return m[0] ? { variable: m[1], filter_name: m[2], filter_arg: m[3] } : null;
- },
-
- error: function (s) {
- throw s + "\ncan't parse filterexception at char " + this.re.lastIndex + ". Make sure there is no spaces between filters or arguments\n";
- },
-
- resolve: function (context) {
- var value;
- if (this.hasOwnProperty('constant')) {
- value = this.constant;
- } else {
- value = context.get(this.variable);
- }
-
- var safety = {
- is_safe: false,
- must_escape: context.autoescaping,
- };
-
- var out = this.filter_list.reduce( function (p,c) {
-
- var filter = template_defaults.filters[c.name];
-
- if ( filter && typeof filter === 'function') {
- return filter(p, c.arg, safety);
- } else {
- // throw 'Cannot find filter';
- sys.debug('Cannot find filter ' + c.name);
- return p;
- }
- }, value);
-
- if (safety.must_escape && !safety.is_safe) {
- if (typeof out === 'string') {
- return utils.html.escape(out)
- } else if (out instanceof Array) {
- return out.map( function (o) { return typeof o === 'string' ? utils.html.escape(o) : o; } );
- }
- }
- return out;
- }
-});
-
-exports.FilterExpression = FilterExpression;
-
/*********** 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;
+
+
View
8 template/template.test.js
@@ -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');
View
80 template/template_defaults.js
@@ -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,15 +537,15 @@ 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];
return nodes.ExtendsNode(name);
},
'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);
}
};
View
17 template/template_defaults.tags.test.js
@@ -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();
View
10 template/template_defaults.test.js
@@ -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();
View
77 utils/html.js
@@ -3,32 +3,101 @@
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;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&qout;');
-}
+};
/* Function: linebreaks(value, options);
Converts newlines into <p> and <br />s.
Arguments:
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) + ' ...');
+};
+
+
View
9 utils/html.test.js
@@ -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();
View
7 utils/string.js
@@ -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);

0 comments on commit 0b8c50c

Please sign in to comment.