diff --git a/lib/app.js b/lib/app.js index 65ef3de4..7b0f585f 100644 --- a/lib/app.js +++ b/lib/app.js @@ -7,25 +7,17 @@ var express = require('express'), app = express(), options = require('./config'), store = require('./store')(options.store), + models = require('./models'), handlers = require('./handlers'), middleware = require('./middleware'), flattened; -app.store = store; +app.store = store; +app.models = models.createModels(store); app.templates = {}; app.PRODUCTION = 'production'; app.DEVELOPMENT = 'development'; -Object.defineProperties(app, { - production: { - get: function () { - return this.set('env') === this.PRODUCTION; - } - } -}); - -// TODO: Refactor this! -handlers = handlers(app); // Apply the keys from the config file. All nested properties are // space delimited to match the express style. @@ -48,6 +40,7 @@ app.use(express.errorHandler({showStack: true, dumpExceptions: true})); app.use(middleware.noslashes()); app.use(middleware.ajax()); app.use(middleware.jsonp()); +app.use(middleware.helpers(app)); // Create a Hogan/Mustache handler for templates. app.engine('html', function (path, options, fn) { diff --git a/lib/handlers.js b/lib/handlers.js index b3e20563..af987804 100644 --- a/lib/handlers.js +++ b/lib/handlers.js @@ -1,7 +1,7 @@ var async = require('asyncjs'), path = require('path'), utils = require('./utils'), - createBinModel = require('./models/bin'); + handlers; // Create a not found error object. function NotFound() { @@ -9,259 +9,245 @@ function NotFound() { } NotFound.prototype = Object.create(Error.prototype); -module.exports = function (app) { - var binModel = createBinModel(app.store), handlers; - - handlers = { - getDefault: function (req, res) { - handlers.renderFiles(req, res); - }, - getBin: function (req, res, next) { - handlers.render(req, res, req.bin); - }, - getBinPreview: function (req, res, next) { - var options = {edit: !req.param('quiet')}; - handlers.formatPreview(req.bin, options, function (err, formatted) { - if (err) { - next(err); - } - - if (formatted) { - res.send(formatted); - } else { - res.contentType('js'); - res.send(req.bin.javascript); - } - }); - }, - getBinSource: function (req, res) { - res.contentType('json'); - var output = JSON.stringify(handlers.templateFromBin(req.bin)); - if (!req.ajax) { - res.contentType('js'); - output = 'var template = ' + output; +module.exports = handlers = { + getDefault: function (req, res) { + handlers.renderFiles(req, res); + }, + getBin: function (req, res, next) { + handlers.render(req, res, req.bin); + }, + getBinPreview: function (req, res, next) { + var options = {edit: !req.param('quiet')}; + handlers.formatPreview(req.bin, req.helpers, options, function (err, formatted) { + if (err) { + next(err); } - res.send(output); - }, - getBinSourceFile: function (req, res) { - var format = req.params.format; - res.contentType(format); - if (format !== 'html') { - if (format === 'js' || format === 'json') { - format = 'javascript'; - } - res.send(req.bin[format]); + if (formatted) { + res.send(formatted); } else { - handlers.getBinPreview(req, res); + res.contentType('js'); + res.send(req.bin.javascript); } - }, - redirectToLatest: function (req, res) { - var path = req.path.replace('latest', req.bin.revision); - res.redirect(303, path); - }, - createBin: function (req, res, next) { - var data = utils.extract(req.body, 'html', 'css', 'javascript'); + }); + }, + getBinSource: function (req, res) { + res.contentType('json'); + var output = JSON.stringify(handlers.templateFromBin(req.bin)); + if (!req.ajax) { + res.contentType('js'); + output = 'var template = ' + output; + } + res.send(output); + }, + getBinSourceFile: function (req, res) { + var format = req.params.format; - binModel.create(data, function (err, result) { + res.contentType(format); + if (format !== 'html') { + if (format === 'js' || format === 'json') { + format = 'javascript'; + } + res.send(req.bin[format]); + } else { + handlers.getBinPreview(req, res); + } + }, + redirectToLatest: function (req, res) { + var path = req.path.replace('latest', req.bin.revision); + res.redirect(303, path); + }, + createBin: function (req, res, next) { + var data = utils.extract(req.body, 'html', 'css', 'javascript'); + + req.models.bin.create(data, function (err, result) { + if (err) { + return next(err); + } + handlers.renderCreated(req, res, result); + }); + }, + createRevision: function (req, res, next) { + var panel = req.param('panel'), + params = {}; + + if (req.param('method') === 'save') { + params = utils.extract(req.body, 'html', 'css', 'javascript'); + params.url = req.bin.url; + params.revision = req.bin.revision + 1; + req.models.bin.createRevision(params, function (err, result) { if (err) { return next(err); } handlers.renderCreated(req, res, result); }); - }, - createRevision: function (req, res, next) { - var panel = req.param('panel'), - params = {}; + } else if (req.param('method') === 'update') { + params[panel] = req.param('content'); + params.streamingKey = req.param('checksum'); + params.revision = req.param('revision'); + params.url = req.param('code'); - if (req.param('method') === 'save') { - params = utils.extract(req.body, 'html', 'css', 'javascript'); - params.url = req.bin.url; - params.revision = req.bin.revision + 1; - binModel.createRevision(params, function (err, result) { - if (err) { - return next(err); - } - handlers.renderCreated(req, res, result); - }); - } else if (req.param('method') === 'update') { - params[panel] = req.param('content'); - params.streamingKey = req.param('checksum'); - params.revision = req.param('revision'); - params.url = req.param('code'); + req.models.bin.updatePanel(panel, params, function (err, result) { + if (err) { + return next(err); + } + res.json({ok: true, error: false}); + }); + } else { + next(); + } + }, + notFound: function (req, res) { + var files = handlers.defaultFiles(); + files[2] = 'not_found.js'; + handlers.renderFiles(req, res, files); + }, + loadBin: function (req, res, next) { + var rev = parseInt(req.params.rev, 10) || 1, + query = {id: req.params.bin, revision: rev}; - binModel.updatePanel(panel, params, function (err, result) { - if (err) { - return next(err); - } - res.json({ok: true, error: false}); - }); + function complete(err, result) { + if (err) { + return next(new NotFound('Could not find bin: ' + req.params.bin)); } else { + req.bin = result; next(); } - }, - notFound: function (req, res) { - var files = handlers.defaultFiles(); - files[2] = 'not_found.js'; - handlers.renderFiles(req, res, files); - }, - loadBin: function (req, res, next) { - var rev = parseInt(req.params.rev, 10) || 1, - query = {id: req.params.bin, revision: rev}; + } - function complete(err, result) { + // TODO: Re-factor this logic. + if ((req.params.rev || req.path.indexOf('latest') === -1) && req.path.indexOf('save') === -1) { + req.models.bin.load(query, complete); + } else { + req.models.bin.latest(query, complete); + } + }, + render: function (req, res, bin) { + var template = handlers.templateFromBin(bin), + jsbin = handlers.jsbin(bin, req.helpers.production ? req.helpers.set('version') : 'debug'); + + req.helpers.analytics(function (err, analytics) { + res.render('index', { + tips: '{}', + revision: bin.revision || 1, + jsbin: JSON.stringify(jsbin), + json_template: JSON.stringify(template), + version: jsbin.version, + analytics: analytics, + 'production?': req.helpers.production + }); + }); + }, + renderFiles: function (req, res, files) { + files = files || handlers.defaultFiles(); + async.files(files, req.helpers.set('views')).readFile("utf8").toArray(function (err, results) { + if (!err) { + handlers.render(req, res, { + html: results[0].data, + css: results[1].data, + javascript: results[2].data + }); + } + }); + }, + renderCreated: function (req, res, bin) { + var permalink = req.helpers.urlForBin(bin), + editPermalink = req.helpers.editUrlForBin(bin); + + if (req.ajax) { + res.json({ + code: bin.url, + root: req.helpers.set('url full'), + created: (new Date()).toISOString(), // Should be part of bin. + revision: bin.revision, + url: permalink, + edit: editPermalink, + html: editPermalink, + js: editPermalink, + title: utils.titleForBin(bin), + allowUpdate: false, + checksum: req.streamingKey + }); + } else { + res.redirect(303, '/' + bin.url + '/' + bin.revision + '/edit'); + } + }, + jsbin: function (bin, version) { + return { + root: '', + version: version, + state: { + stream: false, + code: bin.url || null, + revision: bin.revision || 1 + } + }; + }, + templateFromBin: function (bin) { + return utils.extract(bin, 'html', 'css', 'javascript'); + }, + defaultFiles: function () { + return ['html', 'css', 'js'].map(function (ext) { + return 'default.' + ext; + }); + }, + formatPreview: function (bin, helpers, options, fn) { + var formatted = bin.html || '', + insert = [], parts, last, context; + + // TODO: Re implement this entire block with an HTML parser. + if (formatted) { + helpers.analytics(function (err, analytics) { if (err) { - return next(new NotFound('Could not find bin: ' + req.params.bin)); + return fn(err); + } + + if (formatted.indexOf('%code%') > -1) { + formatted = formatted.replace(/%code%/g, bin.javascript); } else { - req.bin = result; - next(); + insert.push(''); } - } - // TODO: Re-factor this logic. - if ((req.params.rev || req.path.indexOf('latest') === -1) && req.path.indexOf('save') === -1) { - binModel.load(query, complete); - } else { - binModel.latest(query, complete); - } - }, - render: function (req, res, bin) { - var template = handlers.templateFromBin(bin), - jsbin = handlers.jsbin(bin); + if (!options || options.edit !== false) { + insert.push(''); + } - handlers.analytics(function (err, analytics) { - res.render('index', { - tips: '{}', - revision: bin.revision || 1, - jsbin: JSON.stringify(jsbin), - json_template: JSON.stringify(template), - version: jsbin.version, - analytics: analytics, - 'production?': app.production - }); - }); - }, - renderFiles: function (req, res, files) { - files = files || handlers.defaultFiles(); - async.files(files, app.set('views')).readFile("utf8").toArray(function (err, results) { - if (!err) { - handlers.render(req, res, { - html: results[0].data, - css: results[1].data, - javascript: results[2].data + if (helpers.production) { + insert.push(analytics); + } + + // Append scripts to the bottom of the page. + if (insert.length) { + parts = formatted.split(''); + last = parts.pop(); + formatted = parts.join('') + insert.join('\n') + '\n' + last; + } + + if (formatted.indexOf('%css%') > -1) { + formatted = formatted.replace(/%css%/g, bin.css); + } else { + insert = ''; + parts = formatted.split(''); + last = parts.pop(); + formatted = parts.join('') + insert + '' + last; + } + + context = { + domain: helpers.set('url host'), + permalink: helpers.editUrlForBin(bin, true) + }; + + // Append attribution comment to header. + helpers.render('comment', context, function (err, comment) { + formatted = formatted.replace(/]*>/, function ($0) { + return $0 + '\n' + comment.trim(); }); - } - }); - }, - renderCreated: function (req, res, bin) { - var permalink = handlers.urlForBin(bin), - editPermalink = handlers.editUrlForBin(bin); - - if (req.ajax) { - res.json({ - code: bin.url, - root: app.set('url full'), - created: (new Date()).toISOString(), // Should be part of bin. - revision: bin.revision, - url: permalink, - edit: editPermalink, - html: editPermalink, - js: editPermalink, - title: utils.titleForBin(bin), - allowUpdate: false, - checksum: req.streamingKey + return fn(err || null, err ? undefined : formatted); }); - } else { - res.redirect(303, '/' + bin.url + '/' + bin.revision + '/edit'); - } - }, - jsbin: function (bin) { - return { - root: '', - version: app.set('environment') === 'production' ? app.set('version') : 'debug', - state: { - stream: false, - code: bin.url || null, - revision: bin.revision || 1 - } - }; - }, - urlForBin: function (bin, full) { - return app.set(full ? 'url full' : 'url prefix') + bin.url + '/' + bin.revision; - }, - editUrlForBin: function (bin, full) { - return handlers.urlForBin(bin, full) + '/edit'; - }, - templateFromBin: function (bin) { - return utils.extract(bin, 'html', 'css', 'javascript'); - }, - defaultFiles: function () { - return ['html', 'css', 'js'].map(function (ext) { - return 'default.' + ext; }); - }, - analytics: function (fn) { - app.render('analytics', {id: app.set('analytics id')}, fn); - }, - formatPreview: function (bin, options, fn) { - var formatted = bin.html || '', - insert = [], parts, last, context; - - // TODO: Re implement this entire block with an HTML parser. - if (formatted) { - handlers.analytics(function (err, analytics) { - if (err) { - return fn(err); - } - - if (formatted.indexOf('%code%') > -1) { - formatted = formatted.replace(/%code%/g, bin.javascript); - } else { - insert.push(''); - } - - if (!options || options.edit !== false) { - insert.push(''); - } - - if (app.production) { - insert.push(analytics); - } - - // Append scripts to the bottom of the page. - if (insert.length) { - parts = formatted.split(''); - last = parts.pop(); - formatted = parts.join('') + insert.join('\n') + '\n' + last; - } - - if (formatted.indexOf('%css%') > -1) { - formatted = formatted.replace(/%css%/g, bin.css); - } else { - insert = ''; - parts = formatted.split(''); - last = parts.pop(); - formatted = parts.join('') + insert + '' + last; - } - - context = { - domain: app.get('url host'), - permalink: handlers.editUrlForBin(bin, true) - }; - - // Append attribution comment to header. - app.render('comment', context, function (err, comment) { - formatted = formatted.replace(/]*>/, function ($0) { - return $0 + '\n' + comment.trim(); - }); - return fn(err || null, err ? undefined : formatted); - }); - }); - } else { - fn(null, formatted); - } - }, - NotFound: NotFound - }; - return handlers; + } else { + fn(null, formatted); + } + }, + NotFound: NotFound }; diff --git a/lib/middleware.js b/lib/middleware.js index 32830d7d..c9da1b95 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -37,6 +37,36 @@ module.exports = { if (req.path !== '/' && req.path.slice(-1) === '/') { res.redirect(301, req.path.slice(0, -1)); } + next(); + }; + }, + helpers: function (app) { + return function (req, res, next) { + req.store = app.store; + req.models = app.models; + + req.helpers = { + set: app.set.bind(app), + render: app.render.bind(app), + analytics: function (fn) { + app.render('analytics', {id: req.helpers.set('analytics id')}, fn); + }, + urlForBin: function (bin, full) { + return app.set(full ? 'url full' : 'url prefix') + bin.url + '/' + bin.revision; + }, + editUrlForBin: function (bin, full) { + return req.helpers.urlForBin(bin, full) + '/edit'; + } + }; + + Object.defineProperties(req.helpers, { + production: { + get: function () { + return this.set('env') === this.PRODUCTION; + } + } + }); + next(); }; }