Comparing version 0.0.3 to 0.1.0
436
index.js
// | ||
// (pre)compile/render mustache templates with hogan | ||
// Can be used as middleware or broadway plugin | ||
// (pre)compile/render mustache templates with hogan.js | ||
// can be used as middleware or broadway plugin | ||
// | ||
// common options { | ||
// debug:Boolean - check files for updates | ||
// cache:Object - use your own cache object | ||
// hoganOptions:Object - options for hoganjs | ||
// } | ||
// | ||
@@ -21,45 +15,87 @@ var hogan = require('hogan.js'), | ||
// | ||
// cache for compiled templates | ||
// exports | ||
// | ||
var objCache = {}; | ||
// for compiled template src strings | ||
var srcCache = {}; | ||
// | ||
// exports | ||
// broadway plugin | ||
// attach options: | ||
// @dir templates dir | ||
// @ext templates ext | ||
// @recompile recompile template on every call to render | ||
// | ||
// broadway plugin | ||
exports.plugin = { | ||
name: 'hoganyam', | ||
attach: attach | ||
attach: function(options) { | ||
options.cache = options.cache || {}; | ||
this.render = function(res, name, context) { | ||
context = context || {}; | ||
var file = path.join(options.dir, name + options.ext); | ||
render(file, context, options, function(err, str) { | ||
if (err) { | ||
winston.error(err); | ||
res.writeHead(200, {'Content-Type': 'text/plain'}); | ||
res.end(err.toString()); | ||
} else { | ||
res.writeHead(200, {'Content-Type': 'text/html'}); | ||
res.end(str); | ||
} | ||
}); | ||
}; | ||
} | ||
}; | ||
// connect-style middleware function | ||
// provide templates from a directory individually as connect-style middleware | ||
exports.provide = provide; | ||
// bundle templates form a directory into one js-file as connect-style middleware | ||
exports.bundle = bundle; | ||
// render a template | ||
exports.render = render; | ||
// | ||
// return a connect-style middleware function that writes the source | ||
// of the precompiled template to the response object | ||
// provide compiled templates individually through url | ||
// connect-style middleware function | ||
// | ||
// @srcDir absolute path to templates | ||
// @srcUrl base url for request | ||
// @options { | ||
// namespace:String - namespace for precompiled templates, default is 'templates' | ||
// prefixpath:String - virtual base path to request templates from | ||
// ext:String - template extension, default is '.html' | ||
// @namespace namespace for precompiled templates, default is this | ||
// @ext template extension, default is '.html' | ||
// @recompile do not use cache and recompile on every request | ||
// @hoganOptions options for the hogan.js template engine | ||
// } | ||
// | ||
function provide(srcDir, options) { | ||
options = options || {}; | ||
options.cache = options.cache || srcCache; | ||
options.namespace = options.namespace || 'templates'; | ||
options.ext = options.ext || '.html'; | ||
options.mount = options.mount || ''; | ||
options.namespace = options.namespace || 'this'; | ||
options.hoganOptions = options.hoganOptions || {}; | ||
options.hoganOptions.asString = true; | ||
options.processTemplate = createTemplateSource; | ||
options.ext = options.ext || '.html'; | ||
var dstExt = /\.js$/, | ||
srcExt = options.ext; | ||
srcExt = options.ext, | ||
cache = options.cache || {}, | ||
jsTemplate, | ||
jst = ''; | ||
return function compileAndSend(req, res, next) { | ||
jst += ';(function(root) {\n'; | ||
jst += ' var template = new Hogan.Template({{{template}}});\n'; | ||
jst += ' var partials = {\n'; | ||
jst += ' {{#partials}}'; | ||
jst += ' "{{name}}": new Hogan.Template({{{template}}}),\n'; | ||
jst += ' {{/partials}}'; | ||
jst += ' };\n'; | ||
jst += ' root.templates = root.templates || {};\n'; | ||
jst += ' root.templates["{{name}}"] = function(context) {\n'; | ||
jst += ' return template.render(context, partials);\n'; | ||
jst += ' };\n'; | ||
jst += '})({{namespace}});\n'; | ||
jsTemplate = hogan.compile(jst); | ||
return function(req, res, next) { | ||
if (req.method !== 'GET') return next(); | ||
@@ -69,24 +105,103 @@ | ||
var pathname = url.parse(req.url).pathname, | ||
opts = utile.clone(options), // clone to avoid async race | ||
parts, srcFile; | ||
srcFile, src, ux; | ||
if (!pathname.match(dstExt)) return next(); | ||
// remove the prefixpath if there is one | ||
parts = pathname.split('/'); | ||
if (opts.prefixpath) { | ||
if (parts[1] !== opts.prefixpath) return next(); | ||
pathname = '/' + parts.slice(2, parts.length).join('/'); | ||
if (options.mount) { | ||
ux = new RegExp('^' + options.mount); | ||
if (!pathname.match(ux)) return next(); | ||
// remove prefix url and leading slashes | ||
pathname = pathname.replace(ux, '').replace(/^\/*/, ''); | ||
} | ||
srcFile = path.join(srcDir, pathname).replace(dstExt, srcExt); | ||
opts.cacheKey = srcFile; | ||
winston.info('setting cachekey to: ' + srcFile); | ||
getTemplate(srcFile, opts, function(err,t) { | ||
if (!options.recompile && cache[srcFile]) { | ||
winston.verbose('providing template from cache: ', srcFile); | ||
sendResponse(res, cache[srcFile].source, cache[srcFile].mtime.toUTCString()); | ||
} | ||
else { | ||
getTemplate(srcFile, options, function(err,t) { | ||
if (err) return next(err); | ||
var name = createTemplateName(srcDir, srcFile, options.ext), | ||
context = { | ||
name: name, | ||
template: t.template, | ||
partials: [], | ||
namespace: options.namespace | ||
}; | ||
utile.each(t.partials, function(v, k) { | ||
context.partials.push({ | ||
name: k, | ||
template: v | ||
}); | ||
}); | ||
src = jsTemplate.render(context); | ||
if (!options.recompile) { | ||
cache[srcFile] = { source: src, mtime: t.mtime }; | ||
} | ||
sendResponse(res, src, t.mtime.toUTCString()); | ||
}); | ||
} | ||
}; | ||
} | ||
// | ||
// bundle all compiled templates into one js file | ||
// | ||
// @srcDir directory with templates | ||
// @options { | ||
// @namespace namespace for precompiled templates, default is this | ||
// @ext template extension, default is '.html' | ||
// @recompile do not use cache and recompile on every request | ||
// @hoganOptions options for the hogan.js template engine | ||
// } | ||
// | ||
function bundle(srcDir, options) { | ||
options = options || {}; | ||
options.mount = options.mount || '/templates.js'; | ||
options.ext = options.ext || '.html'; | ||
options.namespace = options.namespace || 'this'; | ||
options.hoganOptions = options.hoganOptions || {}; | ||
options.hoganOptions.asString = true; | ||
var jsTemplate = createTemplate(); | ||
function createTemplate() { | ||
var jst = ''; | ||
jst += '// autogenerated file\n'; | ||
jst += ';(function(root){\n'; | ||
jst += ' root = root || {};\n'; | ||
jst += ' var templates = {\n'; | ||
jst += ' {{#templates}}\n'; | ||
jst += ' "{{name}}": new Hogan.Template({{{template}}}),\n'; | ||
jst += ' {{/templates}}\n'; | ||
jst += ' };\n'; | ||
jst += ' var renderers = {\n'; | ||
jst += ' {{#templates}}\n'; | ||
jst += ' "{{name}}": function(context) {\n'; | ||
jst += ' return templates["{{name}}"].render(context, templates)\n'; | ||
jst += ' },\n'; | ||
jst += ' {{/templates}}\n'; | ||
jst += ' };\n'; | ||
jst += ' root.templates = renderers;\n'; | ||
jst += '})({{namespace}});\n'; | ||
return hogan.compile(jst); | ||
} | ||
return function(req, res, next) { | ||
if (req.method !== 'GET') return next(); | ||
var reqUrl = url.parse(req.url).pathname, | ||
src = ''; | ||
// only answer correct url | ||
if (reqUrl !== options.mount) return next(); | ||
compileDir(srcDir, options, function(err, templates) { | ||
winston.verbose("compiling template dir: ", srcDir); | ||
if (err) return next(err); | ||
res.setHeader('Date', new Date().toUTCString()); | ||
res.setHeader('Last-Modified', t.mtime.toUTCString()); | ||
res.setHeader('Content-Type', 'application/javascript'); | ||
res.setHeader('Content-Length', t.template.length); | ||
res.end(t.template); | ||
resolvePartialNames(templates); | ||
src = jsTemplate.render({ templates: templates, namespace: options.namespace}); | ||
sendResponse(res, src); | ||
}); | ||
@@ -98,23 +213,34 @@ }; | ||
// | ||
// plugin attach function flatiron.broadway-style | ||
// @options see file header | ||
// resolve partial name like '../header' to qualified template name | ||
// @templates dict with templates | ||
// | ||
function attach(options) { | ||
this.render = function(res, name, context) { | ||
context = context || {}; | ||
var file = path.join(options.dir, name + options.ext); | ||
render(file, context, options, function(err, str) { | ||
if (err) { | ||
winston.error(err); | ||
res.writeHead(200, {'Content-Type': 'text/plain'}); | ||
res.end(err.toString()); | ||
} else { | ||
res.writeHead(200, {'Content-Type': 'text/html'}); | ||
res.end(str); | ||
} | ||
function resolvePartialNames(templates) { | ||
templates.forEach(function(template) { | ||
var parts = template.name.split(path.sep), | ||
basePath = parts.length === 1 ? '' : parts.slice(0, parts.length - 1).join(path.sep); | ||
template.partials = template.partials.map(function(partialName) { | ||
return path.join(basePath, partialName); | ||
}); | ||
}; | ||
}); | ||
} | ||
function createTemplateName(basePath, filePath, ext) { | ||
var len = basePath.split(path.sep).length, | ||
relPath = filePath.split(path.sep).slice(len).join(path.sep), | ||
name = relPath.replace(new RegExp(ext + '$'), ''); | ||
return name; | ||
} | ||
function sendResponse(res, str, mtime) { | ||
res.setHeader('Date', new Date().toUTCString()); | ||
res.setHeader('Last-Modified', mtime || (new Date).toUTCString()); | ||
res.setHeader('Content-Type', 'application/javascript'); | ||
res.setHeader('Content-Length', str.length); | ||
res.end(str); | ||
} | ||
// | ||
@@ -125,3 +251,7 @@ // render a template file with context mapping | ||
// @context vars to map | ||
// @options see file header | ||
// @options { | ||
// @cache cache object to use | ||
// @recompile do not use cache and recompile on every request | ||
// @hoganOptions options for the hogan.js template engine | ||
// } | ||
// @callback is called with (err, str) | ||
@@ -132,9 +262,18 @@ // | ||
options = options || {}; | ||
options.cache = options.cache || objCache; | ||
options.cacheKey = file; | ||
options.cache = options.cache || {}; | ||
options.hoganOptions = options.hoganOptions || {}; | ||
getTemplate(file, options, function(err, t) { | ||
callback(err, t ? t.template.render(context, t.partials) : err.message); | ||
}); | ||
var key = file, | ||
ct = options.cache[key]; | ||
if (!options.recompile && ct) { | ||
winston.verbose('render template from cache: ' + file); | ||
callback(null, ct.template.render(context, ct.partials)); | ||
} | ||
else { | ||
getTemplate(file, options, function(err, t) { | ||
if (!options.recompile) options.cache[key] = t; | ||
callback(err, t ? t.template.render(context, t.partials) : err.message); | ||
}); | ||
} | ||
} | ||
@@ -144,9 +283,6 @@ | ||
// | ||
// get the template from file or cache | ||
// get the template file and compile it | ||
// | ||
function getTemplate(file, options, callback) { | ||
if (!options.debug && options.cache && options.cache[options.cacheKey]) { | ||
winston.verbose('get template from cache: ' + options.cacheKey); | ||
return callback(null, options.cache[options.cacheKey]); | ||
} | ||
winston.verbose('getting template: ' + file); | ||
findfilep(file, function(err, foundfile) { | ||
@@ -156,17 +292,7 @@ if (err) return callback(err); | ||
if (err) return callback(err); | ||
// use the cached version if it exists and is recent enough | ||
var c = options.cache[options.cacheKey], | ||
ext, dir; | ||
if (c && stats.mtime.getTime() <= c.mtime.getTime()) { | ||
winston.verbose('get template from cache: ' + options.cacheKey); | ||
return callback(null, c); | ||
} | ||
winston.verbose('compile template: ' + file); | ||
var ext, dir; | ||
compile(foundfile, options, function(err, t) { | ||
if (err) return callback(err); | ||
if (options.processTemplate) options.processTemplate(t, options); | ||
// if (options.processTemplate) options.processTemplate(t, options); | ||
t.mtime = stats.mtime; | ||
options.cache[options.cacheKey] = t; | ||
callback(null, t); | ||
@@ -180,8 +306,4 @@ }); | ||
// | ||
// compile template - passes a template object to the callback | ||
// compile template and partials | ||
// | ||
// { | ||
// template:Function, | ||
// partials:Dict of partial Functions | ||
// } | ||
function compile(file, options, callback) { | ||
@@ -191,8 +313,11 @@ var ext = path.extname(file), | ||
// compile the template file and all partials recusively | ||
hoganCompile(file, options, function(err, tmpl, partialNames) { | ||
// compile the template | ||
hoganCompile(file, options.hoganOptions, function(err, template, partialNames) { | ||
if (err) return callback(err); | ||
tmpl.name = path.basename(file, ext); | ||
var tmpl = { | ||
template: template, | ||
name: path.basename(file, ext), | ||
partials: {} | ||
}; | ||
// compile the partials | ||
async.forEach(partialNames, | ||
@@ -202,3 +327,2 @@ function(name, cb) { | ||
poptions = utile.clone(options); | ||
poptions.cacheKey = pfile; | ||
@@ -208,3 +332,2 @@ getTemplate(pfile, poptions, function(err, t) { | ||
tmpl.partials[name] = t.template; | ||
// _.extend(tmpl.partials, t.partials); | ||
tmpl.partials = utile.mixin(tmpl.partials, t.partials); | ||
@@ -221,3 +344,42 @@ cb(); | ||
// compiles the template and extracts names of the partials | ||
// | ||
// compile template files in the directory and all subdirectories asynchronously | ||
// @basePath base path | ||
// @options | ||
// @callback call when finished | ||
// | ||
function compileDir(basePath, options, callback) { | ||
var templates = [], | ||
compileIterator = function(filePath, callback) { | ||
if (!filePath.match(new RegExp(options.ext + '$'))) { | ||
return callback(); | ||
} | ||
hoganCompile(filePath, options.hoganOptions, function(err, template, partialNames) { | ||
if (err) return callback(err); | ||
// var len = basePath.split(path.sep).length, | ||
// relPath = filePath.split(path.sep).slice(len).join(path.sep), | ||
// name = relPath.replace(/.html$/, ''); | ||
templates.push({ | ||
name: createTemplateName(basePath, filePath, options.ext), | ||
template: template, | ||
partials: partialNames | ||
}); | ||
callback(); | ||
}); | ||
}; | ||
eachFileInDir(basePath, | ||
compileIterator, | ||
function(err) { | ||
callback(err, templates); | ||
}); | ||
} | ||
// | ||
// compiles the template and extracts partial names | ||
// @file the template file | ||
// @options hogan options | ||
// @callback is called when finished | ||
// | ||
function hoganCompile(file, options, callback) { | ||
@@ -229,18 +391,7 @@ fs.readFile(file, 'utf8', function(err, str) { | ||
var tokens = hogan.scan(str), | ||
// partialTokens = _.filter(tokens, function(t) { | ||
// return t.tag === '>'; | ||
// }), | ||
// partialNames = _.map(partialTokens, function(t) { | ||
// return t.n; | ||
// }), | ||
partialNames = tokens | ||
.filter(function(t) { return t.tag === '>'; }) | ||
.map(function(t) { return t.n; }), | ||
hgopts = options.hoganOptions, | ||
tmpl = {}; | ||
// compile the tokens | ||
tmpl.template = hogan.generate(hogan.parse(tokens, str, hgopts), str, hgopts); | ||
tmpl.partials = {}; | ||
callback(err, tmpl, partialNames); | ||
template = hogan.generate(hogan.parse(tokens, str, options), str, options); | ||
callback(err, template, partialNames); | ||
}); | ||
@@ -251,43 +402,2 @@ } | ||
// | ||
// transform template property of template object to proper js source | ||
// client can render template by calling | ||
// [namespace].[name].render(context); | ||
// | ||
function createTemplateSource(t, options) { | ||
var str = '', | ||
p; | ||
str += ';(function(root) {\n'; | ||
str += '\troot.' + t.name + ' = {\n'; | ||
str += '\t\ttemplate: new Hogan.Template(' + t.template + '),\n'; | ||
str += '\t\tpartials: {\n'; | ||
for (p in t.partials) { | ||
str += '\t\t\t' + p + ': new Hogan.Template(' + t.partials[p] + '),\n'; | ||
} | ||
str += '\t\t},\n'; | ||
str += '\t\trender: function(context){\n'; | ||
str += '\t\t\treturn this.template.render(context, this.partials);\n'; | ||
str += '\t\t}\n'; | ||
str += '\t};\n'; | ||
str += '})( this.' + options.namespace + ' || this);\n'; | ||
if (options.debug) str += 'console.log("template: ' + t.name + ' loaded");\n'; | ||
if (options && options.compress) { | ||
var jsp = require("uglify-js").parser, | ||
pro = require("uglify-js").uglify, | ||
ast = jsp.parse(str); // parse code and get the initial AST | ||
ast = pro.ast_mangle(ast); // get a new AST with mangled names | ||
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations | ||
str = pro.gen_code(ast); // compressed code here | ||
} | ||
t.template = str; | ||
return t; | ||
} | ||
// | ||
// find a file by walking up the path | ||
@@ -312,1 +422,29 @@ // | ||
} | ||
// | ||
// call iterator on all files in the path and subpaths asynchronously | ||
// @startPath base path | ||
// @iterator iterator function(filePath, callback) {} | ||
// @callback call when finished | ||
// | ||
function eachFileInDir(startPath, iterator, callback) { | ||
(function rec(basePath, callback) { | ||
fs.stat(basePath, function(err, stats) { | ||
if (err) return callback(err); | ||
if (stats.isFile()) { | ||
iterator(basePath, callback); | ||
} | ||
else if (stats.isDirectory()) { | ||
fs.readdir(basePath, function(err, nodes) { | ||
if (err) return callback(err); | ||
utile.async.forEach(nodes, | ||
function(node, callback) { | ||
rec(path.join(basePath, node), callback); | ||
}, | ||
callback); | ||
}); | ||
} | ||
}); | ||
})(startPath, callback); | ||
} |
@@ -5,3 +5,3 @@ { | ||
"description": "hogan-js template middleware and render function", | ||
"version": "0.0.3", | ||
"version": "0.1.0", | ||
"homepage": "https://github.com/skoni/hoganyam", | ||
@@ -8,0 +8,0 @@ "repository": { |
# hoganyam | ||
Yet another hogan.js(moustache templates) middleware. Can render templates with partials serverside or precompile them for use on the client. The templates are compiled, cached and updated when the file changes. | ||
Yet another hogan.js(moustache templates) middleware. Can render templates with partials serverside or precompile them for use on the client. | ||
@@ -11,6 +11,12 @@ ## Usage | ||
Makes the templates available individually. | ||
``` js | ||
app.use(hoganyam.provide(templatesDir, options)) | ||
app.use(hoganyam.provide(templatesDir, options)); | ||
``` | ||
Bundle all templates in directory into one js file | ||
``` js | ||
app.use(hoganyam.bundle(templatesDir, options)); | ||
``` | ||
### Serverside rendering | ||
@@ -26,3 +32,3 @@ | ||
``` js | ||
app.use(hoganyam.plugin, {dir: viewsDir, ext: '.html'}); | ||
app.use(hoganyam.plugin, {dir: templatesDir, ext: '.html'}); | ||
// now you can render directly to the response | ||
@@ -29,0 +35,0 @@ app.render(res, 'templatename', { title: 'Hello Hogan'}); |
@@ -8,9 +8,37 @@ var vows = require('vows'), | ||
fs = require('fs'), | ||
srcDir = path.join(__dirname, 'data'), | ||
testFile = path.join(srcDir, 'test.template'), | ||
dataDir = path.join(__dirname, 'data'), | ||
testFile = path.join(dataDir, 'test.template'), | ||
options = {}, | ||
data = { title: "a little test", name: "Hogan"}, | ||
resultStr = "This is " + data.title + " for Mr. " + data.name + '.'; | ||
correctResult = fs.readFileSync(path.join(dataDir, 'result.txt')).toString(); | ||
function puts(str) { | ||
process.stderr.write(str + '\n'); | ||
} | ||
function mockReq(url) { | ||
return { | ||
method: 'GET', | ||
url: url | ||
}; | ||
} | ||
function mockRes(callback) { | ||
return { | ||
setHeader: function() {}, | ||
end: function(str) { callback(null, str); } | ||
}; | ||
} | ||
function evalTemplate(templateStr, callStr, data) { | ||
var str = templateStr + 'result = ' + callStr + '(' + JSON.stringify(data) + ');\n', | ||
sandbox = { | ||
Hogan: hogan, | ||
result: null | ||
}, | ||
context = vm.createContext(sandbox); | ||
vm.runInContext(str, context); | ||
return context.result; | ||
} | ||
vows.describe('hoganyam').addBatch({ | ||
@@ -22,2 +50,3 @@ "The hoganyam module": { | ||
assert.isFunction(hoganyam.provide); | ||
assert.isFunction(hoganyam.bundle); | ||
assert.isFunction(hoganyam.render); | ||
@@ -30,33 +59,38 @@ assert.isObject(hoganyam.plugin); | ||
}, | ||
"is showing the correct output": function(str) { | ||
assert.equal(str, resultStr); | ||
"should show the correct output": function(str) { | ||
assert.equal(str, correctResult); | ||
} | ||
}, | ||
"used as middleware": { | ||
"used as middleware for single template": { | ||
topic: function() { | ||
var self = this, | ||
next = self.callback, | ||
req = { | ||
method: 'GET', | ||
url: 'test.js' | ||
}, | ||
res = { | ||
setHeader: function() {}, | ||
end: function(str) { self.callback(null, str); } | ||
}; | ||
hoganyam.provide(srcDir, {ext: '.template'})(req, res, next); | ||
var f = hoganyam.provide(dataDir, {ext: '.template'}), | ||
that = this; | ||
f(mockReq('test.js'), mockRes(this.callback), function(err) { | ||
that.callback(err || new Error('request failed')); | ||
}); | ||
}, | ||
"provides the correct source js template function ": function(str) { | ||
var templates = {}, | ||
sandbox = { | ||
Hogan: hogan, | ||
result: null | ||
}, | ||
context = vm.createContext(sandbox); | ||
str += 'result = test.render(' + JSON.stringify(data) + ');\n'; | ||
vm.runInContext(str, context); | ||
assert.equal(context.result, resultStr); | ||
"sould provide the compiled template and render the correct result": function(err, str) { | ||
assert.isNull(err); | ||
var result = evalTemplate(str, 'templates.test', data); | ||
assert.equal(result, correctResult); | ||
} | ||
}, | ||
"used as middleware for bundled templates": { | ||
topic: function() { | ||
var that = this, | ||
f = hoganyam.bundle(dataDir, {ext: '.template'}); | ||
f(mockReq('/templates.js'), mockRes(this.callback), function(err) { | ||
that.callback(err || new Error('request failed')); | ||
}); | ||
}, | ||
"should bundle the compiled templates and render the correct result": function(err, str) { | ||
assert.isNull(err); | ||
// puts(str); | ||
var result = evalTemplate(str, 'templates.test', data); | ||
assert.equal(result, correctResult); | ||
} | ||
} | ||
} | ||
}).export(module); |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
18833
11
468
45
1