UNPKG

kiwi

Version:

Simple, modular, fast and lightweight template engine, based on jQuery templates syntax.

352 lines (290 loc) 8.03 kB
/*! * 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;