diff --git a/HISTORY.md b/HISTORY.md index ff29acc..4fc26c5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,7 @@ unreleased ========== + * Accept `function` value for `template` option * Send non-chunked response for `OPTIONS` * Stat parent directory when necessary * Use `Date.prototype.toLocaleDateString` to format date diff --git a/README.md b/README.md index 80dfb0d..5dde837 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,31 @@ Optional path to a CSS stylesheet. Defaults to a built-in stylesheet. ##### template -Optional path to an HTML template. Defaults to a built-in template. +Optional path to an HTML template or a function that will render a HTML +string. Defaults to a built-in template. -The following tokens are replaced in templates: +When given a string, the string is used as a file path to load and then the +following tokens are replaced in templates: * `{directory}` with the name of the directory. * `{files}` with the HTML of an unordered list of file links. * `{linked-path}` with the HTML of a link to the directory. * `{style}` with the specified stylesheet and embedded images. +When given as a function, the function is called as `template(locals, callback)` +and it needs to invoke `callback(error, htmlString)`. The following are the +provided locals: + + * `directory` is the directory being displayed (where `/` is the root). + * `displayIcons` is a Boolean for if icons should be rendered or not. + * `fileList` is a sorted array of files in the directory. The array contains + objects with the following properties: + - `name` is the relative name for the file. + - `stat` is a `fs.Stats` object for the file. + * `path` is the full filesystem path to `directory`. + * `style` is the default stylesheet or the contents of the `stylesheet` option. + * `viewName` is the view name provided by the `view` option. + ##### view Display mode. `tiles` and `details` are available. Defaults to `tiles`. diff --git a/index.js b/index.js index 1c01f1f..7b9d856 100644 --- a/index.js +++ b/index.js @@ -171,27 +171,44 @@ function serveIndex(root, options) { */ serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) { + var render = typeof template !== 'function' + ? createHtmlRender(template) + : template + if (showUp) { files.unshift('..'); } + // stat all files stat(path, files, function (err, stats) { if (err) return next(err); - fs.readFile(template, 'utf8', function(err, str){ + + // combine the stats into the file list + var fileList = files.map(function (file, i) { + return { name: file, stat: stats[i] }; + }); + + // sort file list + fileList.sort(fileSort); + + // read stylesheet + fs.readFile(stylesheet, 'utf8', function (err, style) { if (err) return next(err); - fs.readFile(stylesheet, 'utf8', function(err, style){ + + // create locals for rendering + var locals = { + directory: dir, + displayIcons: Boolean(icons), + fileList: fileList, + path: path, + style: style, + viewName: view + }; + + // render html + render(locals, function (err, body) { if (err) return next(err); - var fileData = files.map(function (file, i) { - return { name: file, stat: stats[i] }; - }).sort(fileSort); - - var body = str - .replace(/\{style\}/g, style.concat(iconStyle(fileData, icons))) - .replace(/\{files\}/g, createHtmlFileList(fileData, dir, icons, view)) - .replace(/\{directory\}/g, escapeHtml(dir)) - .replace(/\{linked-path\}/g, htmlPath(dir)); - var buf = new Buffer(body, 'utf8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Length', buf.length); @@ -288,6 +305,27 @@ function createHtmlFileList(files, dir, useIcons, view) { return html; } +/** + * Create function to render html. + */ + +function createHtmlRender(template) { + return function render(locals, callback) { + // read template + fs.readFile(template, 'utf8', function (err, str) { + if (err) return callback(err); + + var body = str + .replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons))) + .replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName)) + .replace(/\{directory\}/g, escapeHtml(locals.directory)) + .replace(/\{linked-path\}/g, htmlPath(locals.directory)); + + callback(null, body); + }); + }; +} + /** * Sort function for with directories first. */ @@ -385,7 +423,7 @@ function iconLookup(filename) { * Load icon images, return css string. */ -function iconStyle (files, useIcons) { +function iconStyle(files, useIcons) { if (!useIcons) return ''; var className; var i; diff --git a/test/test.js b/test/test.js index 19697d9..408ba92 100644 --- a/test/test.js +++ b/test/test.js @@ -304,6 +304,146 @@ describe('serveIndex(root)', function () { }); }); + describe('with "template" option', function () { + describe('when setting a custom template file', function () { + var server; + before(function () { + server = createServer(fixtures, {'template': __dirname + '/shared/template.html'}); + }); + + it('should respond with file list', function (done) { + request(server) + .get('/') + .set('Accept', 'text/html') + .expect(/