Skip to content

Loading…

Filters For Variable Output #4

Merged
merged 11 commits into from

2 participants

@paularmstrong

Added filters for variable output. Handy for transcoding raw data into something useful for display. This is definitely just a start, as they are relatively easy to implement. It'd be great to also look into a way to add custom filters.

Usage

{{ foo|filter }}
{{ foo|filter('options') }}

Example:

{{ myDate|date('F jS, Y') }} // August 6th, 2011
{{ foo|title }} // converts 'my title' to 'My Title'
{{ myArray|join(', ')|title }} // join array and then title case

Currently Implemented

  1. lower
  2. upper
  3. capitalize
  4. title
  5. join
  6. length
  7. url_encode
  8. url_decode
  9. json_encode
  10. striptags
  11. date

Tests!

Also added node unit as a devDependency and wrote simple unit tests for all filters.

@paularmstrong

Forgot to note: Also added pre-commit and post-merge hooks that will, once you've run make once, always auto-update themselves. The pre-commit hook strips trailing whitespace from all changed files and ensures JSLint and all tests pass before allowing any commits.

@skid skid merged commit 89a5cb8 into skid:master
@skid
Owner

Merged this one. Great job.

P.S. It's good to do this when you're creating the compiled code.

    output = '__filters.' + matches[1] + '(' + variable + ', ' + matches[2] + ')';

instead of

    output = '__filters["' + matches[1] + '"](' + variable + ', ' + matches[2] + ')';

Since we already have the filters predefined in the module and dot-based property lookup is faster.
I'll get to that and escaping the filter arguments in a few days if you don't do it first :)

@paularmstrong

Yeah, I went with brackets as the "safer" bet, but I guess that it's an arguable tradeoff: should there be an enforcement mechanism for (future) custom-filters to require being able to be used with dot notation? (I think yes)

@skid
Owner

I think it's better to make filter names only valid JS variable names and we can check while compiling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
View
5 Makefile
@@ -1,8 +1,11 @@
all:
- @echo ''
+ @npm install -d
+ @cp scripts/githooks/* .git/hooks/
+ @chmod -R +x .git/hooks/
test:
@node tests/tests.js
+ @node scripts/runtests.js
lint:
@node scripts/runlint.js
View
14 index.js
@@ -1,10 +1,14 @@
+require.paths.unshift(__dirname + '/lib');
+
var fs = require("fs"),
util = require("util"),
path = require("path"),
crypto = require("crypto"),
- tags = require("./tags"),
- parser = require("./parser"),
- widgets = require("./widgets"),
+
+ tags = require("tags"),
+ parser = require("parser"),
+ widgets = require("widgets"),
+ filters = require('filters'),
CACHE = {},
DEBUG = false,
@@ -47,7 +51,7 @@ createTemplate = function (data, id) {
}
// The compiled render function - this is all we need
- render = new Function("__context", "__parents", "__widgets",
+ render = new Function("__context", "__parents", "__filters", "__widgets",
[ '__parents = __parents ? __parents.slice() : [];'
// Prevents circular includes (which will crash node without warning)
, 'for (var i=0, j=__parents.length; i<j; ++i) {'
@@ -64,7 +68,7 @@ createTemplate = function (data, id) {
);
template.render = function (context, parents) {
- return render.call(this, context, parents, widgets);
+ return render.call(this, context, parents, filters, widgets);
};
return template;
View
178 lib/filters.js
@@ -0,0 +1,178 @@
+var helpers = require('./helpers'),
+ escape = helpers.escape,
+ _dateFormats;
+
+_dateFormats = {
+ _months: {
+ full: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ },
+ _days: {
+ full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ alt: {'-1': 'Yesterday', 0: 'Today', 1: 'Tomorrow'}
+ },
+ // Day
+ d: function (input) {
+ return (input.getDate() < 10 ? '0' : '') + input.getDate();
+ },
+ D: function (input) {
+ return _dateFormats._days.abbr[input.getDay()];
+ },
+ j: function (input) {
+ return input.getDate();
+ },
+ l: function (input) {
+ return _dateFormats._days.full[input.getDay()];
+ },
+ N: function (input) {
+ return input.getDay();
+ },
+ S: function (input) {
+ return (input.getDate() % 10 === 1 && input.getDate() !== 11 ? 'st' : (input.getDate() % 10 === 2 && input.getDate() !== 12 ? 'nd' : (input.getDate() % 10 === 3 && input.getDate() !== 13 ? 'rd' : 'th')));
+ },
+ w: function (input) {
+ return input.getDay() - 1;
+ },
+ //z = function (input) { return ''; },
+
+ // Week
+ //W = function (input) { return ''; },
+
+ // Month
+ F: function (input) {
+ return _dateFormats._months.full[input.getMonth()];
+ },
+ m: function (input) {
+ return (input.getMonth() < 8 ? '0' : '') + (input.getMonth() + 1);
+ },
+ M: function (input) {
+ return _dateFormats._months.abbr[input.getMonth()];
+ },
+ n: function (input) {
+ return input.getMonth() + 1;
+ },
+ //t = function (input) { return ''; },
+
+ // Year
+ //L = function (input) { return ''; },
+ //o = function (input) { return ''; },
+ Y: function (input) {
+ return input.getFullYear();
+ },
+ y: function (input) {
+ return ('' + input.getFullYear()).substr(2);
+ },
+
+ // Time
+ a: function (input) {
+ return input.getHours() < 12 ? 'am' : 'pm';
+ },
+ A: function (input) {
+ return input.getHours() < 12 ? 'AM' : 'PM';
+ },
+ //B = function () { return ''; },
+ g: function (input) {
+ return input.getHours() === 0 ? 12 : (input.getHours() > 12 ? input.getHours() - 12 : input.getHours());
+ },
+ G: function (input) {
+ return input.getHours();
+ },
+ h: function (input) {
+ return (input.getHours() < 10 || (12 < input.getHours() < 22) ? '0' : '') + (input.getHours() < 10 ? input.getHours() : input.getHours() - 12);
+ },
+ H: function (input) {
+ return (input.getHours() < 10 ? '0' : '') + input.getHours();
+ },
+ i: function (input) {
+ return (input.getMinutes() < 10 ? '0' : '') + input.getMinutes();
+ },
+ s: function (input) {
+ return (input.getSeconds() < 10 ? '0' : '') + input.getSeconds();
+ },
+ //u = function () { return ''; },
+
+ // Timezone
+ //e = function () { return ''; },
+ //I = function () { return ''; },
+ O: function (input) {
+ return (input.getTimezoneOffset() < 0 ? '-' : '+') + (input.getTimezoneOffset() / 60 < 10 ? '0' : '') + (input.getTimezoneOffset() / 60) + '00';
+ },
+ //T = function () { return ''; },
+ Z: function (input) {
+ return input.getTimezoneOffset() * 60;
+ },
+
+ // Full Date/Time
+ //c = function () { return ''; },
+ r: function (input) {
+ return input.toString();
+ },
+ U: function (input) {
+ return input.getTime() / 1000;
+ }
+};
+
+
+exports.lower = function (input) {
+ return input.toString().toLowerCase();
+};
+
+exports.upper = function (input) {
+ return input.toString().toUpperCase();
+};
+
+exports.capitalize = function (input) {
+ return input.toString().charAt(0).toUpperCase() + input.toString().substr(1).toLowerCase();
+};
+
+exports.title = function (input) {
+ return input.toString().replace(/\w\S*/g, function (str) {
+ return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
+ });
+};
+
+exports.join = function (input, separator) {
+ if (Array.isArray(input)) {
+ return input.join(separator);
+ } else {
+ return input;
+ }
+};
+
+exports.length = function (input) {
+ return input.length;
+};
+
+exports.url_encode = function (input) {
+ return encodeURIComponent(input);
+};
+
+exports.url_decode = function (input) {
+ return decodeURIComponent(input);
+};
+
+exports.json_encode = function (input) {
+ return JSON.stringify(input);
+};
+
+exports.striptags = function (input) {
+ return input.toString().replace(/(<([^>]+)>)/ig, "");
+};
+
+exports.date = function (input, format) {
+ var l = format.length,
+ date = new Date(input),
+ cur, i = 0,
+ out = '';
+
+ for (i; i < l; i += 1) {
+ cur = format.charAt(i);
+ if (_dateFormats.hasOwnProperty(cur)) {
+ out += _dateFormats[cur](date);
+ } else {
+ out += cur;
+ }
+ }
+ return out;
+};
View
0 helpers.js → lib/helpers.js
File renamed without changes.
View
37 parser.js → lib/parser.js
@@ -1,7 +1,7 @@
var helpers = require('./helpers'),
+ filters = require('./filters'),
check = helpers.check,
- escape = helpers.escape,
variableRegexp = /^\{\{.*?\}\}$/,
logicRegexp = /^\{%.*?%\}$/,
@@ -75,11 +75,35 @@ exports.parse = function (data, tags) {
return stack[index];
};
+function wrapFilter(variable, filter) {
+ var matches = filter.match(/(\w*)\((.*)\)/),
+ output = variable;
+
+ if (matches && matches.length && filters.hasOwnProperty(matches[1])) {
+ output = '__filters["' + matches[1] + '"](' + variable + ', ' + matches[2] + ')';
+ } else {
+ output = '__filters["' + filter + '"](' + variable + ')';
+ }
+
+ return output;
+}
+
+function wrapFilters(variable, filters, context) {
+ var output = helpers.escape(variable, context);
+
+ if (filters && filters.length > 0) {
+ filters.forEach(function (filter) {
+ output = wrapFilter(output, filter);
+ });
+ }
+
+ return output;
+}
exports.compile = function compile(indent) {
var code = [''],
tokens = [],
- parent, filepath, blockname;
+ parent, filepath, blockname, varOutput;
indent = indent || '';
@@ -129,11 +153,12 @@ exports.compile = function compile(indent) {
}
if (token.type === VAR_TOKEN) {
+ varOutput = token.name.split('|');
return code.push(
- 'if (' + check(token.name) + ') {'
- , ' __output.push(' + escape(token.name) + ');'
- , '} else if (' + check(token.name, '__context') + ') {'
- , ' __output.push(' + escape(token.name, '__context') + ');'
+ 'if (' + check(varOutput[0]) + ') {'
+ , ' __output.push(' + wrapFilters(varOutput[0], varOutput.slice(1)) + ');'
+ , '} else if (' + check(varOutput[0], '__context') + ') {'
+ , ' __output.push(' + wrapFilters(varOutput[0], varOutput.slice(1), '__context') + ');'
, '}'
);
}
View
0 tags.js → lib/tags.js
File renamed without changes.
View
0 widgets.js → lib/widgets.js
File renamed without changes.
View
4 package.json
@@ -7,13 +7,15 @@
"author": "Dusko Jordanovski <jordanovskid@gmail.com>",
"dependencies": {},
"devDependencies": {
- "nodelint": "0.4.0"
+ "nodelint": "0.4.0",
+ "nodeunit": "0.5.3"
},
"main": "index",
"engines": {
"node": ">= 0.4.1"
},
"scripts": {
+ "lint": "make lint",
"test": "make test"
}
}
View
5 scripts/config-test.js
@@ -0,0 +1,5 @@
+module.exports = {
+ root: __dirname + '/../',
+ testRunner: 'default',
+ pathIgnore: ['*node_modules*']
+};
View
3 scripts/githooks/post-merge
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+make
View
56 scripts/githooks/pre-commit
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+function failCommit() {
+ echo "\033[31m----------------------------------------\033[0m"
+ echo "FATAL ERROR: $1"
+ echo "\033[31m----------------------------------------\033[0m"
+ exit 1
+}
+
+function testFail() {
+ echo "\033[33m----------------------------------------\033[0m"
+ echo "$1"
+ echo "\033[33m----------------------------------------\033[0m"
+
+}
+
+if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+fi
+
+# Remove all of the trailing whitespace in this commit
+for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq` ; do
+ sed -i '' -E 's/[[:space:]]*$//' "$FILE"
+ git add $FILE
+done
+
+echo 'Running JSLint...'
+result=$(make lint)
+if ! grep -q "^0 errors" <<< $result; then
+ num=$(grep "[0-9] error" <<< "$result")
+ testFail "JSLint: $num"
+ echo "$result"
+ echo ''
+ lintFailed=1
+fi
+
+if [[ $lint_errors -gt 0 ]]; then
+ failCommit "Lint Errors"
+fi
+
+echo 'Running Tests...'
+result=$(make test)
+if grep -q FAILURES <<< $result; then
+ num=$(grep "FAILURES" <<< "$result")
+ testFail "Test $num"
+ echo "$result"
+ echo ''
+ testsFailed=1
+fi
+
+if [[ $testsFailed || $lintFailed ]]; then
+ failCommit "Unable To Commit"
+fi
View
35 scripts/runtests.js
@@ -0,0 +1,35 @@
+require.paths.unshift(__dirname + '/../node_modules/');
+
+var util = require('util'),
+ child_process = require('child_process'),
+ nodeunit = require('nodeunit'),
+ configFile = __dirname + '/config-test',
+ ignore = '',
+ config, test_runner, i;
+
+process.argv.forEach(function (val, index, array) {
+ if (index < 2) {
+ return;
+ }
+
+ if (val === '-c') {
+ configFile = process.argv[~~index + 1];
+ }
+});
+
+config = require(configFile);
+test_runner = nodeunit.reporters[config.testRunner];
+
+function runTests(error, stdout, stderr) {
+ var tests = stdout.trim().split("\n");
+ if (tests.length && tests[0] !== '') {
+ test_runner.run(tests);
+ }
+}
+
+i = config.pathIgnore.length;
+while (i--) {
+ ignore += ' ! -path "' + config.pathIgnore[i] + '"';
+}
+
+child_process.exec('find . -name "*.test.js" ' + ignore, { cwd: config.root }, runTests);
View
115 tests/filters.test.js
@@ -0,0 +1,115 @@
+var filters = require('../lib/filters');
+
+exports.lower = function (test) {
+ var input = 'BaR';
+ test.strictEqual('bar', filters.lower(input));
+ input = 345;
+ test.strictEqual('345', filters.lower(input));
+ test.done();
+};
+
+exports.upper = function (test) {
+ var input = 'bar';
+ test.strictEqual('BAR', filters.upper(input));
+ input = 345;
+ test.strictEqual('345', filters.upper(input));
+ test.done();
+};
+
+exports.capitalize = function (test) {
+ var input = 'awesome sauce.';
+ test.strictEqual('Awesome sauce.', filters.capitalize(input));
+ input = 345;
+ test.strictEqual('345', filters.capitalize(input));
+ test.done();
+};
+
+exports.title = function (test) {
+ var input = 'this is title case';
+ test.strictEqual('This Is Title Case', filters.title(input));
+ test.done();
+};
+
+exports.join = function (test) {
+ var input = [1, 2, 3];
+ test.strictEqual('1+2+3', filters.join(input, '+'));
+ test.strictEqual('1 * 2 * 3', filters.join(input, ' * '));
+ input = 'asdf';
+ test.strictEqual('asdf', filters.join(input, '-'), 'Non-array input is not joined.');
+ test.done();
+};
+
+exports.length = function (test) {
+ var input = [1, 2, 3];
+ test.strictEqual(3, filters.length(input));
+ input = 'foobar';
+ test.strictEqual(6, filters.length(input));
+ test.done();
+};
+
+exports.url_encode = function (test) {
+ var input = "param=1&anotherParam=2";
+ test.strictEqual("param%3D1%26anotherParam%3D2", filters.url_encode(input));
+ test.done();
+};
+
+exports.url_decode = function (test) {
+ var input = "param%3D1%26anotherParam%3D2";
+ test.strictEqual("param=1&anotherParam=2", filters.url_decode(input));
+ test.done();
+};
+
+exports.json_encode = function (test) {
+ var input = { foo: 'bar', baz: [1, 2, 3] };
+ test.strictEqual('{"foo":"bar","baz":[1,2,3]}', filters.json_encode(input));
+ test.done();
+};
+
+exports.striptags = function (test) {
+ var input = '<h1>foo</h1> <div class="blah">hi</div>';
+ test.strictEqual('foo hi', filters.striptags(input));
+ test.done();
+};
+
+exports.multiple = function (test) {
+ var input = ['aWEsoMe', 'sAuCe'];
+ test.strictEqual('Awesome Sauce', filters.title(filters.join(input, ' ')));
+ test.done();
+};
+
+exports.date = function (test) {
+ var input = 'Sat Aug 06 2011 09:05:02 GMT-0700 (PDT)';
+
+ test.strictEqual('06', filters.date(input, "d"), 'format: d http://www.php.net/date');
+ test.strictEqual('Sat', filters.date(input, "D"), 'format: D http://www.php.net/date');
+ test.strictEqual('6', filters.date(input, "j"), 'format: j http://www.php.net/date');
+ test.strictEqual('Saturday', filters.date(input, "l"), 'format: l http://www.php.net/date');
+ test.strictEqual('6', filters.date(input, "N"), 'format: N http://www.php.net/date');
+ test.strictEqual('th', filters.date(input, "S"), 'format: S http://www.php.net/date');
+ test.strictEqual('5', filters.date(input, "w"), 'format: w http://www.php.net/date');
+ test.strictEqual('August', filters.date(input, "F"), 'format: F http://www.php.net/date');
+ test.strictEqual('08', filters.date(input, "m"), 'format: m http://www.php.net/date');
+ test.strictEqual('Aug', filters.date(input, "M"), 'format: M http://www.php.net/date');
+ test.strictEqual('8', filters.date(input, "n"), 'format: n http://www.php.net/date');
+
+ test.strictEqual('2011', filters.date(input, "Y"), 'format: Y http://www.php.net/date');
+ test.strictEqual('11', filters.date(input, "y"), 'format: y http://www.php.net/date');
+ test.strictEqual('2011', filters.date(input, "Y"), 'format: Y http://www.php.net/date');
+ test.strictEqual('am', filters.date(input, "a"), 'format: a http://www.php.net/date');
+ test.strictEqual('AM', filters.date(input, "A"), 'format: A http://www.php.net/date');
+ test.strictEqual('9', filters.date(input, "g"), 'format: g http://www.php.net/date');
+ test.strictEqual('9', filters.date(input, "G"), 'format: G http://www.php.net/date');
+ test.strictEqual('09', filters.date(input, "h"), 'format: h http://www.php.net/date');
+ test.strictEqual('09', filters.date(input, "H"), 'format: H http://www.php.net/date');
+ test.strictEqual('05', filters.date(input, "i"), 'format: i http://www.php.net/date');
+ test.strictEqual('02', filters.date(input, "s"), 'format: s http://www.php.net/date');
+
+ test.strictEqual('+0700', filters.date(input, "O"), 'format: O http://www.php.net/date');
+ test.strictEqual('25200', filters.date(input, "Z"), 'format: Z http://www.php.net/date');
+ test.strictEqual('Sat Aug 06 2011 09:05:02 GMT-0700 (PDT)', filters.date(input, "r"), 'format: r http://www.php.net/date');
+ test.strictEqual('1312646702', filters.date(input, "U"), 'format: U http://www.php.net/date');
+
+ test.strictEqual('06-08-2011', filters.date(input, "d-m-Y"));
+
+ test.done();
+};
Something went wrong with that request. Please try again.