├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── filters │ ├── batch.js │ ├── groupby.js │ ├── indent.js │ ├── index.js │ ├── markdown.js │ ├── nl2br.js │ ├── pluck.js │ ├── split.js │ ├── trim.js │ └── truncate.js ├── tags │ ├── case.js │ ├── index.js │ ├── markdown.js │ └── switch.js └── utils.js ├── package.json ├── scripts ├── config-lint.js └── githooks │ ├── post-merge │ └── pre-commit └── tests ├── filters.test.js └── tags.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Makefile 3 | scripts/** 4 | tests/** 5 | .travis.yml 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Paul Armstrong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = node_modules/.bin 2 | 3 | all: 4 | @echo "Installing packages" 5 | @npm install --loglevel=error 6 | @cp scripts/githooks/* .git/hooks/ 7 | @chmod -R +x .git/hooks/ 8 | 9 | files := $(shell find . -name '*.js' ! -path "./node_modules/*") 10 | lint: 11 | @${BIN}/nodelint ${files} --config=scripts/config-lint.js 12 | 13 | tests := $(shell find ./tests -name '*.test.js' ! -path "*node_modules/*") 14 | reporter = dot 15 | opts = 16 | test: 17 | @${BIN}/mocha --reporter ${reporter} ${opts} ${tests} 18 | 19 | .PHONY: all lint test 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swig Extras 2 | =========== 3 | 4 | A collection of handy tags, filters, and extensions for [Swig](http://paularmstrong.github.io/swig/), the most awesome template engine for node.js. 5 | 6 | Usage 7 | ----- 8 | 9 | Use a filter: 10 | 11 | ```js 12 | var swig = require('swig'), 13 | extras = require('swig-extras'); 14 | extras.useFilter(swig, 'markdown'); 15 | ``` 16 | 17 | Use a tag: 18 | 19 | ```js 20 | var swig = require('swig'), 21 | extras = require('swig-extras'), 22 | mySwig = new swig.Swig(); 23 | extras.useTag(mySwig, 'markdown'); 24 | ``` 25 | 26 | Available Filters 27 | ----------------- 28 | 29 | * batch 30 | * groupby 31 | * indent 32 | * markdown 33 | * nl2br 34 | * pluck 35 | * split 36 | * trim 37 | * truncate 38 | 39 | Available Tags 40 | -------------- 41 | 42 | * markdown 43 | * switch/case 44 | 45 | License 46 | ------- 47 | 48 | Copyright (c) 2013 Paul Armstrong 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Raw tag objects. 3 | * @type {object} 4 | */ 5 | exports.tags = require('./lib/tags'); 6 | 7 | /** 8 | * Raw filter methods. 9 | * @type {object} 10 | */ 11 | exports.filters = require('./lib/filters'); 12 | 13 | /** 14 | * Add an Extras filter to your swig instance. 15 | * 16 | * @example 17 | * var swig = require('swig'), 18 | * extras = require('swig-extras'); 19 | * extras.useFilter(swig, 'markdown'); 20 | * 21 | * @param {object} swig Swig instance. 22 | * @param {string} filter Extras filter name. 23 | * @return {undefined} 24 | * @throws {Error} If Extras does not have a filter with the given name. 25 | */ 26 | exports.useFilter = function (swig, filter) { 27 | var f = exports.filters[filter]; 28 | if (!f) { 29 | throw new Error('Filter "' + filter + '" does not exist.'); 30 | } 31 | swig.setFilter(filter, f); 32 | }; 33 | 34 | /** 35 | * Add an Extras tag to your swig instance. 36 | * 37 | * @example 38 | * var swig = require('swig'), 39 | * extras = require('swig-extras'), 40 | * mySwig = new swig.Swig(); 41 | * extras.useTag(mySwig, 'markdown'); 42 | * 43 | * @param {object} swig Swig instance. 44 | * @param {string} tag Extras tag name. 45 | * @return {undefined} 46 | * @throws {Error} If Extras does not have a tag with the given name. 47 | */ 48 | exports.useTag = function (swig, tag) { 49 | var t = exports.tags[tag]; 50 | if (!t) { 51 | throw new Error('Tag "' + tag + '" does not exist.'); 52 | } 53 | swig.setTag(tag, t.parse, t.compile, t.ends, t.blockLevel); 54 | if (t.ext) { 55 | swig.setExtension(t.ext.name, t.ext.obj); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /lib/filters/batch.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'); 2 | 3 | /** 4 | * Batches items by returning a list of lists with the given number of items. 5 | * 6 | * @example 7 | * // items = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; 8 | * 9 | * {% for row in items|batch(3, 'No item') %} 10 | * 11 | * {% for column in row %} 12 | * 13 | * {% endfor %} 14 | * 15 | * {% endfor %} 16 | *
{{ column }}
17 | * // => 18 | * // 19 | * // 20 | * // 21 | * // 22 | * // 23 | * // 24 | * // 25 | * // 26 | * // 27 | * // 28 | * // 29 | * // 30 | * // 31 | * // 32 | * // 33 | * //
abc
def
gNo itemNo item
34 | * 35 | * @param {array} input 36 | * @param {number} num Number of items to batch into lists. 37 | * @param {*} fill Item to fill in if there are blanks when batching. 38 | * @return {array} 39 | */ 40 | module.exports = function (input, num, fill) { 41 | if (!utils.isArray(input)) { 42 | return input; 43 | } 44 | 45 | var o = [], 46 | l = (input.length % num !== 0) ? num - (input.length % num) : 0; 47 | utils.each(input, function (val, index) { 48 | if (index % num === 0) { 49 | o.push([]); 50 | } 51 | o[o.length - 1].push(val); 52 | }); 53 | 54 | if (typeof fill !== 'undefined' && l) { 55 | while (l) { 56 | o[o.length - 1].push(fill); 57 | l -= 1; 58 | } 59 | } 60 | 61 | return o; 62 | }; 63 | -------------------------------------------------------------------------------- /lib/filters/groupby.js: -------------------------------------------------------------------------------- 1 | var isArray = require('../utils').isArray; 2 | 3 | /** 4 | * Group an array of objects by a common key. If an array is not provided, the input value will be returned untouched. 5 | * 6 | * @example 7 | * // people = [{ age: 23, name: 'Paul' }, { age: 26, name: 'Jane' }, { age: 23, name: 'Jim' }]; 8 | * {% for agegroup in people|groupby('age') %} 9 | *

{{ loop.key }}

10 | * 15 | * {% endfor %} 16 | * 17 | * @param {*} input Input object. 18 | * @param {string} key Key to group by. 19 | * @return {object} Grouped arrays by given key. 20 | */ 21 | module.exports = function (input, key) { 22 | if (!isArray(input)) { 23 | return input; 24 | } 25 | 26 | var i = 0, 27 | l = input.length, 28 | o = {}, 29 | k; 30 | 31 | for (i; i < l; i += 1) { 32 | k = input[i]; 33 | if (!k.hasOwnProperty(key)) { 34 | continue; 35 | } 36 | 37 | if (!o[k[key]]) { 38 | o[k[key]] = []; 39 | } 40 | o[k[key]].push(k); 41 | } 42 | 43 | return o; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/filters/indent.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'); 2 | 3 | /** 4 | * Indent of block of text, usefull when using subtemplates 5 | * 6 | * @example 7 | * {{ "foo"|indent(4) }} 8 | * // => foo 9 | * 10 | * @param {*} input 11 | * @param {*} numIndent Number of time to reapeat the indentation character (default: 4) 12 | * @param {*} indentChar Character to be used for indentation (default: space) 13 | * @return {*} `input` or `def` value. 14 | */ 15 | 16 | module.exports = function (input, numIndent, indentChar) { 17 | if (typeof indentChar === 'undefined') { 18 | indentChar = ' '; 19 | } 20 | 21 | if (typeof numIndent === 'undefined') { 22 | numIndent = 4; 23 | } 24 | 25 | if (numIndent <= 0) { 26 | return input; 27 | } 28 | 29 | var out = utils.iterateFilter.apply(exports.indent, [input, numIndent, indentChar]), 30 | indentString = utils.repeat(indentChar, numIndent); 31 | 32 | if (out !== undefined) { 33 | return out; 34 | } 35 | 36 | return indentString + input.replace(/\n/g, '\n' + indentString); 37 | }; -------------------------------------------------------------------------------- /lib/filters/index.js: -------------------------------------------------------------------------------- 1 | exports.batch = require('./batch'); 2 | exports.groupby = require('./groupby'); 3 | exports.indent = require('./indent'); 4 | exports.markdown = require('./markdown'); 5 | exports.nl2br = require('./nl2br'); 6 | exports.pluck = require('./pluck'); 7 | exports.split = require('./split'); 8 | exports.trim = require('./trim'); 9 | exports.truncate = require('./truncate'); 10 | -------------------------------------------------------------------------------- /lib/filters/markdown.js: -------------------------------------------------------------------------------- 1 | var markdown = require('markdown').markdown; 2 | 3 | /** 4 | * Convert a variable's contents from Markdown to HTML. 5 | * 6 | * @example 7 | * {{ foo|markdown }} 8 | * // =>

Markdown

9 | * 10 | * @param {string} input 11 | * @return {string} HTML 12 | */ 13 | module.exports = function (input) { 14 | return markdown.toHTML(input); 15 | }; 16 | 17 | module.exports.safe = true; 18 | -------------------------------------------------------------------------------- /lib/filters/nl2br.js: -------------------------------------------------------------------------------- 1 | var each = require('../utils').each; 2 | 3 | /** 4 | * Insert HTML line breaks for all newlines in a string. 5 | * 6 | * @example 7 | * // foo = "This is nice.\nAnd so are you." 8 | * {{ foo|nl2br }} 9 | * // => This is nice.
And so are you. 10 | * 11 | * @example 12 | * // Alternatively, just use the `replace` filter: 13 | * {{ foo|replace('\n', '
', 'g')|safe }} 14 | * 15 | * @param {*} input All string values will be modified. 16 | * @return {*} 17 | */ 18 | module.exports = function (input) { 19 | if (typeof input === 'object') { 20 | each(input, function (value, key) { 21 | input[key] = module.exports(value); 22 | }); 23 | return input; 24 | } 25 | 26 | if (typeof input === 'string') { 27 | return input.replace(/\n/g, '
'); 28 | } 29 | 30 | return input; 31 | }; 32 | 33 | module.exports.safe = true; 34 | -------------------------------------------------------------------------------- /lib/filters/pluck.js: -------------------------------------------------------------------------------- 1 | var isArray = require('../utils').isArray; 2 | 3 | /** 4 | * Pluck out a key value from a list of objects as a single list. 5 | * 6 | * @example 7 | * // people = [{ age: 30, name: 'Paul' }, { age: 28, name: 'Nicole'}]; 8 | * {{ people|pluck('name') }} 9 | * // => ['Paul', 'Nicole'] 10 | * 11 | * @param {array} input Array of objects. 12 | * @param {string} key Key index of items to list. 13 | * @return {array} List of values for `key`. 14 | */ 15 | module.exports = function (input, key) { 16 | if (!isArray(input)) { 17 | return input; 18 | } 19 | 20 | var i = 0, 21 | l = input.length, 22 | o = []; 23 | for (i; i < l; i += 1) { 24 | if (input[i].hasOwnProperty(key)) { 25 | o.push(input[i][key]); 26 | } 27 | } 28 | 29 | return o; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/filters/split.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Split a string with the given delimiter, returning a list of string. 3 | * 4 | * @example 5 | * {{ "one,two,three"|split(',') }} 6 | * // => ['one', 'two', 'three'] 7 | * 8 | * @example 9 | * // Alternatively, just use JavaScript's built-in `split`: 10 | * {% set foo = "one,two,three" %} 11 | * {{ foo.split(',') }} 12 | * 13 | * @param {string} input 14 | * @param {string} delimiter String to split with. 15 | * @return {array} List of strings. 16 | */ 17 | module.exports = function (input, delimiter) { 18 | if (typeof input !== 'string') { 19 | return input; 20 | } 21 | 22 | return input.split(delimiter); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/filters/trim.js: -------------------------------------------------------------------------------- 1 | var each = require('../utils').each; 2 | 3 | /** 4 | * Trim leading and trailing whitespace from input strings. 5 | * 6 | * @example 7 | * // foo = " this has extra whitespace "; 8 | * {{ foo|trim }} 9 | * // => this has extra whitespace 10 | * 11 | * @example 12 | * // Alternatively, just use the `replace` filter: 13 | * {{ foo|replace('^\s*|\s*$', '', 'g') }} 14 | * 15 | * @param {*} input 16 | * @return {*} 17 | */ 18 | module.exports = function (input) { 19 | if (typeof input === 'object') { 20 | each(input, function (value, key) { 21 | input[key] = module.exports(value); 22 | }); 23 | return input; 24 | } 25 | 26 | if (typeof input === 'string') { 27 | return input.replace(/^\s*|\s*$/g, ''); 28 | } 29 | 30 | return input; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/filters/truncate.js: -------------------------------------------------------------------------------- 1 | var each = require('../utils').each; 2 | 3 | /** 4 | * Truncate input strings to the given length; 5 | * 6 | * @example 7 | * // foo = 'This is some text.'; 8 | * {{ foo|truncate(5) }} 9 | * // => This ... 10 | * 11 | * @param {*} input 12 | * @param {number} len Number of characters to truncate to. 13 | * @param {string} [end="..."] Text that will be appended if the string was truncated 14 | * @return {*} 15 | */ 16 | module.exports = function (input, len, end) { 17 | end = (typeof end === 'undefined') ? '...' : end; 18 | 19 | if (typeof input === 'object') { 20 | each(input, function (value, key) { 21 | input[key] = module.exports(value, len, end); 22 | }); 23 | return input; 24 | } 25 | 26 | if (typeof input === 'string') { 27 | return input.substring(0, len) + ((input.length > len) ? end : ''); 28 | } 29 | 30 | return input; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/tags/case.js: -------------------------------------------------------------------------------- 1 | exports.parse = function (str, line, parser, types, stack) { 2 | parser.on('*', function (token) { 3 | if (this.out.length) { 4 | throw new Error('Switch statements may only take one argument.'); 5 | } 6 | 7 | return true; 8 | }); 9 | 10 | return (stack.length && stack[stack.length - 1].name === 'switch'); 11 | }; 12 | 13 | 14 | exports.compile = function (compiler, args, content, parents, options, blockName) { 15 | return 'break;\n' + 16 | 'case (' + args[0] + '):\n'; 17 | }; 18 | 19 | exports.ends = false; 20 | -------------------------------------------------------------------------------- /lib/tags/index.js: -------------------------------------------------------------------------------- 1 | exports.markdown = require('./markdown'); 2 | exports.switch = require('./switch'); 3 | exports.case = require('./case'); 4 | -------------------------------------------------------------------------------- /lib/tags/markdown.js: -------------------------------------------------------------------------------- 1 | exports.parse = function (str, line, parser, types, options) { 2 | parser.on('*', function () { 3 | throw new Error('The markdown tag does not accept arguments'); 4 | }); 5 | 6 | return true; 7 | }; 8 | 9 | exports.compile = function (compiler, args, content, parents, options, blockName) { 10 | return '(function () {\n' + 11 | ' var __o = _output;\n' + 12 | ' _output = "";\n' + 13 | compiler(content, parents, options, blockName) + ';\n' + 14 | ' __o += _ext.markdown.toHTML(_output);\n' + 15 | ' _output = __o;\n' + 16 | '})();\n'; 17 | }; 18 | 19 | exports.ends = true; 20 | exports.blockLevel = false; 21 | 22 | exports.ext = { 23 | name: 'markdown', 24 | obj: require('markdown').markdown 25 | }; 26 | -------------------------------------------------------------------------------- /lib/tags/switch.js: -------------------------------------------------------------------------------- 1 | exports.parse = function (str, line, parser, types, stack, options) { 2 | parser.on('*', function (token) { 3 | if (this.out.length) { 4 | throw new Error('Switch statements may only take one argument.'); 5 | } 6 | 7 | return true; 8 | }); 9 | 10 | return true; 11 | }; 12 | 13 | exports.compile = function (compiler, args, content, parents, options, blockName) { 14 | return 'switch (' + args[0] + ') {\n' + 15 | 'default:\n' + 16 | compiler(content, parents, options, blockName) + '\n' + 17 | 'break;\n' + 18 | '}\n'; 19 | }; 20 | 21 | exports.ends = true; 22 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var isArray; 2 | 3 | /** 4 | * Strip leading and trailing whitespace from a string. 5 | * @param {string} input 6 | * @return {string} Stripped input. 7 | */ 8 | exports.strip = function (input) { 9 | return input.replace(/^\s+|\s+$/g, ''); 10 | }; 11 | 12 | /** 13 | * Test if a string starts with a given prefix. 14 | * @param {string} str String to test against. 15 | * @param {string} prefix Prefix to check for. 16 | * @return {boolean} 17 | */ 18 | exports.startsWith = function (str, prefix) { 19 | return str.indexOf(prefix) === 0; 20 | }; 21 | 22 | /** 23 | * Test if a string ends with a given suffix. 24 | * @param {string} str String to test against. 25 | * @param {string} suffix Suffix to check for. 26 | * @return {boolean} 27 | */ 28 | exports.endsWith = function (str, suffix) { 29 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 30 | }; 31 | 32 | /** 33 | * Iterate over an array or object. 34 | * @param {array|object} obj Enumerable object. 35 | * @param {Function} fn Callback function executed for each item. 36 | * @return {array|object} The original input object. 37 | */ 38 | exports.each = function (obj, fn) { 39 | var i, l; 40 | 41 | if (isArray(obj)) { 42 | i = 0; 43 | l = obj.length; 44 | for (i; i < l; i += 1) { 45 | if (fn(obj[i], i, obj) === false) { 46 | break; 47 | } 48 | } 49 | } else { 50 | for (i in obj) { 51 | if (obj.hasOwnProperty(i)) { 52 | if (fn(obj[i], i, obj) === false) { 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | 59 | return obj; 60 | }; 61 | 62 | /** 63 | * Test if an object is an Array. 64 | * @param {object} obj 65 | * @return {boolean} 66 | */ 67 | exports.isArray = isArray = (Array.hasOwnProperty('isArray')) ? Array.isArray : function (obj) { 68 | return (obj) ? (typeof obj === 'object' && Object.prototype.toString.call(obj).indexOf() !== -1) : false; 69 | }; 70 | 71 | /** 72 | * Test if an item in an enumerable matches your conditions. 73 | * @param {array|object} obj Enumerable object. 74 | * @param {Function} fn Executed for each item. Return true if your condition is met. 75 | * @return {boolean} 76 | */ 77 | exports.some = function (obj, fn) { 78 | var i = 0, 79 | result, 80 | l; 81 | if (isArray(obj)) { 82 | l = obj.length; 83 | 84 | for (i; i < l; i += 1) { 85 | result = fn(obj[i], i, obj); 86 | if (result) { 87 | break; 88 | } 89 | } 90 | } else { 91 | exports.each(obj, function (value, index, collection) { 92 | result = fn(value, index, obj); 93 | return !(result); 94 | }); 95 | } 96 | return !!result; 97 | }; 98 | 99 | /** 100 | * Return a new enumerable, mapped by a given iteration function. 101 | * @param {object} obj Enumerable object. 102 | * @param {Function} fn Executed for each item. Return the item to replace the original item with. 103 | * @return {object} New mapped object. 104 | */ 105 | exports.map = function (obj, fn) { 106 | var i = 0, 107 | result = [], 108 | l; 109 | 110 | if (isArray(obj)) { 111 | l = obj.length; 112 | for (i; i < l; i += 1) { 113 | result[i] = fn(obj[i], i); 114 | } 115 | } else { 116 | for (i in obj) { 117 | if (obj.hasOwnProperty(i)) { 118 | result[i] = fn(obj[i], i); 119 | } 120 | } 121 | } 122 | return result; 123 | }; 124 | 125 | /** 126 | * Copy all of the properties in the source objects over to the destination object, and return the destination object. It's in-order, so the last source will override properties of the same name in previous arguments. 127 | * @param {...object} arguments 128 | * @return {object} 129 | */ 130 | exports.extend = function () { 131 | var args = arguments, 132 | target = args[0], 133 | objs = (args.length > 1) ? Array.prototype.slice.call(args, 1) : [], 134 | i = 0, 135 | l = objs.length, 136 | key, 137 | obj; 138 | 139 | for (i; i < l; i += 1) { 140 | obj = objs[i] || {}; 141 | for (key in obj) { 142 | if (obj.hasOwnProperty(key)) { 143 | target[key] = obj[key]; 144 | } 145 | } 146 | } 147 | return target; 148 | }; 149 | 150 | /** 151 | * Get all of the keys on an object. 152 | * @param {object} obj 153 | * @return {array} 154 | */ 155 | exports.keys = function (obj) { 156 | if (Object.keys) { 157 | return Object.keys(obj); 158 | } 159 | 160 | return exports.map(obj, function (v, k) { 161 | return k; 162 | }); 163 | }; 164 | 165 | /** 166 | * Throw an error with possible line number and source file. 167 | * @param {string} message Error message 168 | * @param {number} [line] Line number in template. 169 | * @param {string} [file] Template file the error occured in. 170 | * @throws {Error} No seriously, the point is to throw an error. 171 | */ 172 | exports.throwError = function (message, line, file) { 173 | if (line) { 174 | message += ' on line ' + line; 175 | } 176 | if (file) { 177 | message += ' in file ' + file; 178 | } 179 | throw new Error(message + '.'); 180 | }; 181 | 182 | /** 183 | * Reapeat a string pattern a certain number of times 184 | * @param {string} [pattern] to be repeated 185 | * @param {number} [count] number of times to repeat pattern 186 | */ 187 | 188 | exports.repeat = function (pattern, count) { 189 | if (count < 1) { 190 | return ''; 191 | } 192 | 193 | var result = ''; 194 | while (count > 0) { 195 | if (count & 1) { 196 | result += pattern; 197 | } 198 | count >>= 1; 199 | pattern += pattern; 200 | } 201 | return result; 202 | }; 203 | 204 | /** 205 | * Helper method to recursively run a filter across an object/array and apply it to all of the object/array's values. 206 | * @param {*} input 207 | * @return {*} 208 | * @private 209 | */ 210 | exports.iterateFilter = function (input) { 211 | var self = this, 212 | out = {}; 213 | 214 | if (exports.isArray(input)) { 215 | return exports.map(input, function (value) { 216 | return self.apply(null, arguments); 217 | }); 218 | } 219 | 220 | if (typeof input === 'object') { 221 | exports.each(input, function (value, key) { 222 | out[key] = self.apply(null, arguments); 223 | }); 224 | return out; 225 | } 226 | 227 | return; 228 | }; 229 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swig-extras", 3 | "version": "0.0.1", 4 | "description": "A collection of extra handy tags and filters for Swig Templates.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make lint && make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:paularmstrong/swig-extras.git" 12 | }, 13 | "keywords": [ 14 | "Swig", 15 | "Filters", 16 | "Tags", 17 | "Extensions" 18 | ], 19 | "author": "Paul Armstrong ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/paularmstrong/swig-extras/issues" 23 | }, 24 | "devDependencies": { 25 | "nodelint": "~0.6.2", 26 | "expect.js": "~0.2.0", 27 | "mocha": "~1.12.0", 28 | "swig": ">=1" 29 | }, 30 | "dependencies": { 31 | "markdown": "~0.5.0" 32 | }, 33 | "scripts": { 34 | "prepublish": "npm prune" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/config-lint.js: -------------------------------------------------------------------------------- 1 | var options = { 2 | adsafe: false, 3 | bitwise: true, 4 | browser: false, 5 | cap: false, 6 | confusion: false, 7 | continue: true, 8 | css: false, 9 | debug: false, 10 | devel: false, 11 | eqeq: false, 12 | es5: true, 13 | evil: false, 14 | forin: false, 15 | fragment: false, 16 | indent: 2, 17 | maxerr: 300, 18 | maxlen: 600, 19 | newcap: false, 20 | node: true, 21 | nomen: true, 22 | on: false, 23 | passfail: false, 24 | plusplus: false, 25 | predef: [ 26 | // Mocha 27 | 'describe', 'it', 'after', 'afterEach', 'before', 'beforeEach', 28 | ], 29 | regexp: true, 30 | rhino: false, 31 | safe: false, 32 | sloppy: true, 33 | sub: false, 34 | undef: false, 35 | unparam: false, 36 | vars: false, 37 | white: false, 38 | widget: false, 39 | windows: false 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /scripts/githooks/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make 4 | -------------------------------------------------------------------------------- /scripts/githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function failCommit() { 4 | tput setaf 1 5 | echo "----------------------------------------" 6 | echo "FATAL ERROR: $1" 7 | echo "----------------------------------------" 8 | tput sgr0 9 | exit 1 10 | } 11 | 12 | function testFail() { 13 | tput setaf 3 14 | echo "----------------------------------------" 15 | echo "$1" 16 | echo "----------------------------------------" 17 | tput sgr0 18 | } 19 | 20 | if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then 21 | against=HEAD 22 | else 23 | # Initial commit: diff against an empty tree object 24 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 25 | fi 26 | 27 | # Remove all of the trailing whitespace in this commit 28 | for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq` ; do 29 | sed -i '' -E 's/[[:space:]]*$//' "$FILE" 30 | git add $FILE 31 | done 32 | 33 | echo 'Running JSLint...' 34 | result=$(make lint) 35 | if ! egrep "(^|[^\d])0 errors" <<< "$result"; then 36 | num=$(grep "[0-9]+ error" <<< "$result") 37 | testFail "JSLint: $num" 38 | echo "$result" 39 | echo '' 40 | lintFailed=1 41 | fi 42 | 43 | if [[ $lint_errors -gt 0 ]]; then 44 | failCommit "Lint Errors" 45 | fi 46 | 47 | echo 'Running Tests...' 48 | result=$(make test) 49 | if grep -q FAILURES <<< $result; then 50 | num=$(grep "FAILURES" <<< "$result") 51 | testFail "Test $num" 52 | echo "$result" 53 | echo '' 54 | testsFailed=1 55 | fi 56 | 57 | if [[ $testsFailed || $lintFailed ]]; then 58 | failCommit "Unable To Commit" 59 | fi 60 | -------------------------------------------------------------------------------- /tests/filters.test.js: -------------------------------------------------------------------------------- 1 | var swig = require('swig'), 2 | expect = require('expect.js'), 3 | extras = require('../'); 4 | 5 | describe('Filters:', function () { 6 | 7 | describe('batch', function () { 8 | extras.useFilter(swig, 'batch'); 9 | it('batches', function () { 10 | var opts = { locals: { items: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] }}; 11 | expect(swig.render('{% for row in items|batch(3, "no item") %}{{ row }} : {% endfor %}', opts)) 12 | .to.equal('a,b,c : d,e,f : g,no item,no item : '); 13 | }); 14 | }); 15 | 16 | describe('groupby', function () { 17 | extras.useFilter(swig, 'groupby'); 18 | it('groups arrays by a key', function () { 19 | var opts = { locals: { 20 | foo: [{ name: 'a', a: 1 }, { name: 'a', a: 2 }, { name: 'b', a: 3 }] 21 | }}; 22 | expect(swig.render('{% for r in foo|groupby("name") %}{{ loop.key }} = {% for val in r %}{{ val["a"] }}, {% endfor %}{% endfor %}', opts)) 23 | .to.equal('a = 1, 2, b = 3, '); 24 | }); 25 | }); 26 | 27 | describe('indent', function () { 28 | extras.useFilter(swig, 'indent'); 29 | it('{{ foo|indent }}', function () { 30 | expect(swig.render('{{ foo|indent }}', { locals: { foo: 'bar'}})) 31 | .to.equal(' bar'); 32 | expect(swig.render('{{ foo|indent(6) }}', { locals: { foo: 'bar'}})) 33 | .to.equal(' bar'); 34 | expect(swig.render('{{ foo|indent(2, "a") }}', { locals: { foo: 'bar'}})) 35 | .to.equal('aabar'); 36 | expect(swig.render('{{ foo|indent }}', { locals: { foo: 'bar\nbar'}})) 37 | .to.equal(' bar\n bar'); 38 | }); 39 | }); 40 | 41 | describe('markdown', function () { 42 | extras.useFilter(swig, 'markdown'); 43 | it('{{ foo|markdown }}', function () { 44 | expect(swig.render('{{ foo|markdown }}', { locals: { foo: '# This is an H1' }})) 45 | .to.equal('

This is an H1

'); 46 | }); 47 | }); 48 | 49 | describe('nl2br', function () { 50 | extras.useFilter(swig, 'nl2br'); 51 | it('{{ foo|nl2br }}', function () { 52 | expect(swig.render('{{ foo|nl2br }}', { locals: { foo: "a\nb" }})) 53 | .to.equal('a
b'); 54 | }); 55 | 56 | it('{{ bar|nl2br }}', function () { 57 | expect(swig.render('{{ bar|nl2br }}', { locals: { bar: ["a\nb"] }})) 58 | .to.equal('a
b'); 59 | }); 60 | }); 61 | 62 | describe('pluck', function () { 63 | extras.useFilter(swig, 'pluck'); 64 | it('{{ people|pluck("name") }}', function () { 65 | var opts = { locals: { people: [{ age: 30, name: 'Paul' }, { age: 28, name: 'Nicole'}] }}; 66 | expect(swig.render('{{ people|pluck("name") }}', opts)) 67 | .to.equal('Paul,Nicole'); 68 | }); 69 | }); 70 | 71 | describe('split', function () { 72 | extras.useFilter(swig, 'split'); 73 | it('{{ foo|split(",")|join(" & ") }}', function () { 74 | expect(swig.render('{{ "one,two,three"|split(",")|join(" & ")|raw }}')) 75 | .to.equal('one & two & three'); 76 | }); 77 | }); 78 | 79 | describe('trim', function () { 80 | extras.useFilter(swig, 'trim'); 81 | it('{{ foo|trim }}', function () { 82 | expect(swig.render('{{ foo|trim }}', { locals: { foo: " trim me " }})) 83 | .to.equal('trim me'); 84 | }); 85 | 86 | it('{{ bar|trim }}', function () { 87 | expect(swig.render('{{ bar|trim }}', { locals: { bar: [" trim me "] }})) 88 | .to.equal('trim me'); 89 | }); 90 | }); 91 | 92 | describe('truncate', function () { 93 | extras.useFilter(swig, 'truncate'); 94 | it('{{ foo|truncate(3) }}', function () { 95 | expect(swig.render('{{ foo|truncate(3) }}', { locals: { foo: "truncate me" }})) 96 | .to.equal('tru...'); 97 | }); 98 | 99 | it('{{ foo|truncate(3, "") }}', function () { 100 | expect(swig.render('{{ foo|truncate(3, "") }}', { locals: { foo: "truncate me" }})) 101 | .to.equal('tru'); 102 | }); 103 | 104 | it('{{ bar|truncate(3) }}', function () { 105 | expect(swig.render('{{ bar|truncate(3) }}', { locals: { bar: ["truncate me"] }})) 106 | .to.equal('tru...'); 107 | }); 108 | }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /tests/tags.test.js: -------------------------------------------------------------------------------- 1 | var swig = require('swig'), 2 | expect = require('expect.js'), 3 | extras = require('../'); 4 | 5 | describe('Tags:', function () { 6 | 7 | describe('markdown', function () { 8 | extras.useTag(swig, 'markdown'); 9 | it('{% markdown %}# This is an H1{% endmarkdown %}', function () { 10 | expect(swig.render('{% markdown %}# This is an H1{% endmarkdown %}')) 11 | .to.equal('

This is an H1

'); 12 | }); 13 | }); 14 | 15 | describe('switch', function () { 16 | extras.useTag(swig, 'switch'); 17 | extras.useTag(swig, 'case'); 18 | 19 | it('switches', function () { 20 | expect(swig.render('{% switch foo %}{% case "a" %}This is A{% case "b" %}This is B{% endswitch %}', { locals: { foo: 'a' }})) 21 | .to.equal('This is A'); 22 | }); 23 | }); 24 | 25 | }); 26 | --------------------------------------------------------------------------------