Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consistently use string.replace rather than concatenation #84

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
168 changes: 99 additions & 69 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ var cache = {};
*/

var defaultTemplate = join(__dirname, 'public', 'directory.html');
var templates = {
html: {
list: '<ul id="files" class="view-{view}">{header}{items}</ul>',
header: '<li class="header">'
+ '<span class="name">Name</span>'
+ '<span class="size">Size</span>'
+ '<span class="date">Modified</span>'
+ '</li>',
item: '<li><a href="{path}" class="{classes}" title="{file.name}">'
+ '<span class="name">{file.name}</span>'
+ '<span class="size">{file.size}</span>'
+ '<span class="date">{file.lastModified}</span>'
+ '</a></li>'
}
}

/*!
* Stylesheet.
Expand Down Expand Up @@ -154,13 +169,51 @@ function serveIndex(root, options) {
});
files.sort();

// add parent directory as first
if (showUp) {
files.unshift('..');
}

// content-negotiation
var accept = accepts(req);
var type = accept.type(mediaTypes);

// not acceptable
if (!type) return next(createError(406));
serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);

// stat all files
fstat(path, files, function (err, stats) {
if (err) return next(err);

// 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);

// make similar to file object (with stat)
var directory = {
name: originalDir,
type: 'inode/directory',
size: stat.size,
lastModified: stat.mtime
}

var nodes = fileList.map(function (file) {
var ext = extname(file.name)
var mimetype = mime.lookup(ext)
return {
name: file.name,
type: file.stat.isDirectory() ? 'inode/directory' : mimetype,
size: file.stat.size,
lastModified: file.stat.mtime
}
})

serveIndex[mediaType[type]](req, res, directory, nodes, next, showUp, icons, path, view, template, stylesheet)
});
});
});
};
Expand All @@ -170,46 +223,29 @@ function serveIndex(root, options) {
* Respond with text/html.
*/

serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) {
serveIndex.html = function _html(req, res, directory, files, next, 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) {
// read stylesheet
fs.readFile(stylesheet, 'utf8', function (err, style) {
if (err) return next(err);

// 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);
// create locals for rendering
var locals = {
directory: directory.name,
displayIcons: Boolean(icons),
fileList: files,
path: path,
style: style,
viewName: view
};

// read stylesheet
fs.readFile(stylesheet, 'utf8', function (err, style) {
// render html
render(locals, function (err, body) {
if (err) return next(err);

// 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);
send(res, 'text/html', body)
});
send(res, 'text/html', body)
});
});
};
Expand All @@ -218,36 +254,34 @@ serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path
* Respond with application/json.
*/

serveIndex.json = function _json(req, res, files) {
serveIndex.json = function _json(req, res, directory, nodes) {
var files = nodes.map(function (file) { return file.name })
send(res, 'application/json', JSON.stringify(files))
};
}

/**
* Respond with text/plain.
*/

serveIndex.plain = function _plain(req, res, files) {
serveIndex.plain = function _plain(req, res, directory, nodes) {
var files = nodes.map(function (file) { return file.name })
send(res, 'text/plain', (files.join('\n') + '\n'))
};
}

/**
* Map html `files`, returning an html unordered list.
* @private
*/

function createHtmlFileList(files, dir, useIcons, view) {
var html = '<ul id="files" class="view-' + escapeHtml(view) + '">'
+ (view === 'details' ? (
'<li class="header">'
+ '<span class="name">Name</span>'
+ '<span class="size">Size</span>'
+ '<span class="date">Modified</span>'
+ '</li>') : '');
function createHtmlFileList(files, dirname, useIcons, view) {
var html = templates.html.list
.replace(/{view}/g, view)
.replace(/{header}/g, view === 'details' ? templates.html.header : '')

html += files.map(function (file) {
var items = files.map(function (file) {
var classes = [];
var isDir = file.stat && file.stat.isDirectory();
var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
var isDir = 'inode/directory' === file.type
var path = dirname.split('/').map(function (c) { return encodeURIComponent(c); });

if (useIcons) {
classes.push('icon');
Expand All @@ -269,26 +303,22 @@ function createHtmlFileList(files, dir, useIcons, view) {

path.push(encodeURIComponent(file.name));

var date = file.stat && file.name !== '..'
? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
var date = file.lastModified && file.name !== '..'
? file.lastModified.toLocaleDateString() + ' ' + file.lastModified.toLocaleTimeString()
: '';
var size = file.stat && !isDir
? file.stat.size
var size = file.size && !isDir
? file.size
: '';

return '<li><a href="'
+ escapeHtml(normalizeSlashes(normalize(path.join('/'))))
+ '" class="' + escapeHtml(classes.join(' ')) + '"'
+ ' title="' + escapeHtml(file.name) + '">'
+ '<span class="name">' + escapeHtml(file.name) + '</span>'
+ '<span class="size">' + escapeHtml(size) + '</span>'
+ '<span class="date">' + escapeHtml(date) + '</span>'
+ '</a></li>';
return templates.html.item
.replace(/{path}/g, escapeHtml(normalizeSlashes(normalize(path.join('/')))))
.replace(/{classes}/g, escapeHtml(classes.join(' ')))
.replace(/{file\.name}/g, escapeHtml(file.name))
.replace(/{file\.size}/g, escapeHtml(size))
.replace(/{file\.lastModified/g, escapeHtml(date))
}).join('\n');

html += '</ul>';

return html;
return html.replace(/{items}/g, items)
}

/**
Expand All @@ -302,10 +332,10 @@ function createHtmlRender(template) {
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));
.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);
});
Expand Down Expand Up @@ -421,7 +451,7 @@ function iconStyle(files, useIcons) {
for (i = 0; i < files.length; i++) {
var file = files[i];

var isDir = file.stat && file.stat.isDirectory();
var isDir = 'inode/directory' === file.type
var icon = isDir
? { className: 'icon-directory', fileName: icons.folder }
: iconLookup(file.name);
Expand Down Expand Up @@ -511,7 +541,7 @@ function send (res, type, body) {
* in same order.
*/

function stat(dir, files, cb) {
function fstat(dir, files, cb) {
var batch = new Batch();

batch.concurrency(10);
Expand Down
24 changes: 13 additions & 11 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,17 @@ describe('serveIndex(root)', function () {
it('should provide "fileList" local', function (done) {
var server = createServer(fixtures, {'template': function (locals, callback) {
callback(null, JSON.stringify(locals.fileList.map(function (file) {
file.stat = file.stat instanceof fs.Stats;
file.lastModified = file.lastModified instanceof Date
file.size = file.size >= 0
file.type = /\//.test(file.type)
return file;
})));
}});

request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect('[{"name":"..","stat":true},{"name":"#dir","stat":true},{"name":"index.html","stat":true},{"name":"tobi.txt","stat":true}]')
.expect('[{"name":"..","type":true,"size":true,"lastModified":true},{"name":"#dir","type":true,"size":true,"lastModified":true},{"name":"index.html","type":true,"size":true,"lastModified":true},{"name":"tobi.txt","type":true,"size":true,"lastModified":true}]')
.expect(200, done);
});

Expand Down Expand Up @@ -504,9 +506,9 @@ describe('serveIndex(root)', function () {
it('should get file list', function (done) {
var server = createServer()

serveIndex.html = function (req, res, files) {
serveIndex.html = function (req, res, directory, files) {
var text = files
.filter(function (f) { return /\.txt$/.test(f) })
.filter(function (f) { return /\.txt$/.test(f.name) })
.sort()
res.setHeader('Content-Type', 'text/html')
res.end('<b>' + text.length + ' text files</b>')
Expand All @@ -521,9 +523,9 @@ describe('serveIndex(root)', function () {
it('should get dir name', function (done) {
var server = createServer()

serveIndex.html = function (req, res, files, next, dir) {
serveIndex.html = function (req, res, directory, files, next) {
res.setHeader('Content-Type', 'text/html')
res.end('<b>' + dir + '</b>')
res.end('<b>' + directory.name + '</b>')
}

request(server)
Expand All @@ -535,7 +537,7 @@ describe('serveIndex(root)', function () {
it('should get template path', function (done) {
var server = createServer()

serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) {
serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template) {
res.setHeader('Content-Type', 'text/html')
res.end(String(fs.existsSync(template)))
}
Expand All @@ -549,7 +551,7 @@ describe('serveIndex(root)', function () {
it('should get template with tokens', function (done) {
var server = createServer()

serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) {
serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template) {
res.setHeader('Content-Type', 'text/html')
res.end(fs.readFileSync(template, 'utf8'))
}
Expand All @@ -567,7 +569,7 @@ describe('serveIndex(root)', function () {
it('should get stylesheet path', function (done) {
var server = createServer()

serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) {
serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template, stylesheet) {
res.setHeader('Content-Type', 'text/html')
res.end(String(fs.existsSync(stylesheet)))
}
Expand All @@ -585,7 +587,7 @@ describe('serveIndex(root)', function () {
it('should get called with Accept: text/plain', function (done) {
var server = createServer()

serveIndex.plain = function (req, res, files) {
serveIndex.plain = function (req, res, directory, files) {
res.setHeader('Content-Type', 'text/plain');
res.end('called');
}
Expand All @@ -603,7 +605,7 @@ describe('serveIndex(root)', function () {
it('should get called with Accept: application/json', function (done) {
var server = createServer()

serveIndex.json = function (req, res, files) {
serveIndex.json = function (req, res, directory, files) {
res.setHeader('Content-Type', 'application/json');
res.end('"called"');
}
Expand Down