kiwi
Version:
Simple, modular, fast and lightweight template engine, based on jQuery templates syntax.
352 lines (290 loc) • 8.03 kB
JavaScript
/*!
* Coolony's Kiwi
* Copyright ©2012 Pierre Matri <pierre.matri@coolony.com>
* MIT Licensed
*/
/**
* Module dependencies
*/
// if browser
var frame;
// end
// if node
var _ = require('underscore');
var fs = require('fs');
var path = require('path');
var exists = fs.exists || path.exists;
var frame = require('frame');
// end
/**
* Constants
*/
var DEFAULT_FILE_EXTENSION = '.kiwi';
var FILTER_SPLIT_RE = /\|(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/g;
var FILTER_SPLIT_ARGS_RE = /\,(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/g;
var FILTER_MATCH_RE = /^(\w+)\s*(?:\((.*)\))?$/i;
// if node
/**
* Load `filePath`, and invoke `callback(err, data)`.
*
* @param {String} filePath
* @param {Function} callback
* @api private
*/
exports.loadTemplate = function(filePath, callback) {
fs.readFile(filePath, 'utf-8', function onLoad(err, data) {
if(err) return callback(err);
callback(null, data);
});
};
/**
* Lookup `template` relative to `parentTemplate`, and invoke
* `callback(err, filePath)`.
*
* @param {String} template
* @param {Template} parentTemplate
* @param (Function} callback
* @api private
*/
exports.lookupTemplate = function(name, parentTemplate, callback) {
var ext = path.extname(name);
var parentPath = parentTemplate.options.path;
var additionalPaths = [process.cwd()];
if(parentPath) additionalPaths.push(path.dirname(parentPath));
var lookupPaths = parentTemplate.options.lookupPaths || [];
function doTry(tryPath, next) {
exists(tryPath, function(yes) {
if(yes) return callback(null, tryPath);
next();
});
}
var tryPaths = [];
if(name[0] === '/') tryPaths.push(name);
lookupPaths.concat(additionalPaths).forEach(function(lookupPath) {
tryPaths.push(path.join(lookupPath, name));
tryPaths.push(path.join(lookupPath, name + DEFAULT_FILE_EXTENSION));
});
frame.asyncForEach(tryPaths, doTry, function onDone() {
callback(new Error('RenderError: Can\'t locate template ' +
'`' + name + '`.'
));
});
};
// end
/**
* Asynchronous ForEach implementation.
* Iterates over `array`, invoking `fn(item, args…, next)` for each item, and
* invoke `callback(err)` when done.
*
* @see http://zef.me/3420/async-foreach-in-javascript
* @param {Array} array
* @param {Function} fn
* @param {Mixed[]} [args]
* @param {Function} callback
* @api puclic
*/
function tmpAsyncForEach(array, fn, args, callback) {
if(typeof args === 'function' && !callback) {
callback = args;
args = null;
}
if(!args) args = [];
array = array.slice(0);
function handleProcessedCallback(err) {
if(err) return callback(err);
if(array.length > 0) {
setTimeout(processOne, 0);
} else {
callback();
}
}
function processOne() {
var item = array.shift();
fn.apply(this, [item].concat(args).concat([handleProcessedCallback]));
}
if(array.length > 0) {
setTimeout(processOne, 0);
} else {
callback();
}
}
var asyncForEach = frame ? frame.asyncForEach : tmpAsyncForEach;
/**
* Asynchronously apply `processor` to `input`, and invoke
* `callback(err, result)`.
*
* @param {Mixed} input
* @param {Function} processor
* @param {Function} callback
* @api private
*/
var apply = exports.apply = function(input, processor, args, callback) {
if(typeof args === 'function' && !callback) {
callback = args;
args = null;
}
function done(err, result) {
if(err) return callback(err);
callback(null, result);
}
processor.apply(this, [input].concat(args || []).concat([done]));
};
/**
* Asynchronously apply `processors` to `input` with `args`, and invoke
* `callback(err, result)`.
*
* @param {Mixed} input
* @param {Function[]} processors
* @param {Mixed[]} [args]
* @param {Function} callback
* @api private
*/
exports.applyAll = function(input, processors, args, callback) {
function applyOne(processor, next) {
apply(input, processor, args || [], function onApplied(err, result) {
if(err) return next(err);
input = result;
next();
});
}
function done(err) {
if(err) return callback(err);
callback(null, input);
}
if(typeof args === 'function' && !callback) {
callback = args;
args = null;
}
asyncForEach(processors, applyOne, done);
};
/**
* Asynchronously compiles `tokens`, and invoke
* `callback(err, compiled)` with `compiled` as an array.
*
* @param {BaseToken[]} tokens
* @param {Function} callback
* @api private
*/
function compileTokenArray(tokens, compiler, callback) {
var acc = [];
var index = 0;
function compileOne(token, next) {
token.compile(compiler, function onCompiled(err, compiled) {
if(err) return next(err);
acc.push(compiled);
next(null, compiled);
});
index++;
}
function done(err) {
if(err) return callback(err);
callback(null, acc);
}
asyncForEach(tokens, compileOne, done);
}
exports.compileTokenArray = compileTokenArray;
/**
* Asynchronously compiles `tokens`, glue them, and invoke
* `callback(err, compiled)`.
*
* @param {BaseToken[]} tokens
* @param {Compiler} compiler
* @param {Function} callback
* @api private
*/
exports.compileTokens = function(tokens, compiler, callback) {
compileTokenArray(tokens, compiler, function(err, compiled) {
if(err) return callback(err);
callback(null, compiled.join(''));
});
};
/**
* Escape `str` for use in template compilation.
*
* @param {String} str
* @return {String} Escaped `str`.
* @api private
*/
exports.escapeCompiledString = function(str) {
return str.replace(/([\\"])/g, '\\$1')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
};
/**
* Return `true` if `input` is iterable and not a string.
*
* @param {Mixed} input
* @return {Boolean}
* @api private
*/
exports.isIterable = function(input) {
return typeof input === 'object' && !(input instanceof String);
};
/**
* Parse `filters` with `defaults`, and return the parsed string to include
* in compiled template.
*
* @param {String} filters
* @param {Mixed[]} defaults
* @return {String}
* @api private
*/
exports.parseFilters = function(filters, defaults) {
var raw = false;
defaults = defaults || [];
var splittedFilters = filters.split(FILTER_SPLIT_RE);
if(!splittedFilters) {
throw new Error('Compilation error: Unable to parse filters `' +
filters +
'`.'
);
}
var parsedFilters = splittedFilters
.filter(function filterOne(filter) {
if(filter === 'raw') {
raw = true;
return false;
}
return true;
})
.map(function mapOne(filter) {
var parsedFilter = filter.match(FILTER_MATCH_RE);
if(!parsedFilter) {
throw new Error('Compilation error: Unable to parse filter `' +
filter +
'`.'
);
}
parsedFilter[1] = parsedFilter[1].replace('"', '\"');
if(!parsedFilter[2]) {
return '"' + parsedFilter[1] + '"';
}
var splittedArgs = parsedFilter[2].split(FILTER_SPLIT_ARGS_RE);
return '["' + parsedFilter[1] + '", ' +
splittedArgs.join(',') + ']';
});
if(!raw) parsedFilters = parsedFilters.concat(defaults);
return _.uniq(parsedFilters);
};
/**
* Simple class inheritance.
* Make `subclass` inherit from `superclass`.
*
* based on http://peter.michaux.ca/articles/class-based-inheritance-in-javascript
* @param {Object} subclass
* @param {Object} superclass
* @api public
*/
exports.inherits = function(subclass, superclass) {
function Dummy(){}
Dummy.prototype = superclass.prototype;
subclass.prototype = new Dummy();
subclass.prototype.constructor = subclass;
subclass._superclass = superclass;
subclass._superproto = superclass.prototype;
};
/**
* Module exports
*/
module.exports = exports;