diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 0ca6ac78fd..ea94fcf501 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -37,7 +37,27 @@ function getPostPage(options) { }); } -function formatPageResponse(posts, page) { +/** + * returns a promise with an array of values used in {{navigation}} + * TODO(nsfmc): should this be in the 'prePostsRender' pipeline? + * @return {Promise} containing an array of navigation items + */ +function getSiteNavigation() { + return Promise.resolve(api.settings.read('navigation')).then(function (result) { + if (result && result.settings && result.settings.length) { + return JSON.parse(result.settings[0].value) || []; + } + return []; + }); +} + +/** + * formats variables for handlebars in multi-post contexts. + * If extraValues are available, they are merged in the final value + * TODO(nsfmc): should this be in the 'prePostsRender' pipeline? + * @return {Promise} containing page variables + */ +function formatPageResponse(posts, page, extraValues) { // Delete email from author for frontend output // TODO: do this on API level if no context is available posts = _.each(posts, function (post) { @@ -46,19 +66,36 @@ function formatPageResponse(posts, page) { } return post; }); - return { - posts: posts, - pagination: page.meta.pagination - }; + extraValues = extraValues || {}; + + return getSiteNavigation().then(function (navigation) { + var resp = { + posts: posts, + pagination: page.meta.pagination, + nav: navigation || {} + }; + return _.extend(resp, extraValues); + }); } +/** + * similar to formatPageResponse, but for single post pages + * TODO(nsfmc): should this be in the 'prePostsRender' pipeline? + * @return {Promise} containing page variables + */ function formatResponse(post) { // Delete email from author for frontend output // TODO: do this on API level if no context is available if (post.author) { delete post.author.email; } - return {post: post}; + + return getSiteNavigation().then(function (navigation) { + return { + post: post, + nav: navigation + }; + }); } function handleError(next) { @@ -155,7 +192,9 @@ frontendControllers = { } setResponseContext(req, res); - res.render(view, formatPageResponse(posts, page)); + formatPageResponse(posts, page).then(function (result) { + res.render(view, result); + }); }); }); }).catch(handleError(next)); @@ -198,19 +237,19 @@ frontendControllers = { // Render the page of posts filters.doFilter('prePostsRender', page.posts).then(function (posts) { getActiveThemePaths().then(function (paths) { - var view = template.getThemeViewForTag(paths, options.tag), + var view = template.getThemeViewForTag(paths, options.tag); // Format data for template - result = _.extend(formatPageResponse(posts, page), { - tag: page.meta.filters.tags ? page.meta.filters.tags[0] : '' - }); - - // If the resulting tag is '' then 404. - if (!result.tag) { - return next(); - } - setResponseContext(req, res); - res.render(view, result); + formatPageResponse(posts, page, { + tag: page.meta.filters.tags ? page.meta.filters.tags[0] : '' + }).then(function (result) { + // If the resulting tag is '' then 404. + if (!result.tag) { + return next(); + } + setResponseContext(req, res); + res.render(view, result); + }); }); }); }).catch(handleError(next)); @@ -253,20 +292,20 @@ frontendControllers = { // Render the page of posts filters.doFilter('prePostsRender', page.posts).then(function (posts) { getActiveThemePaths().then(function (paths) { - var view = paths.hasOwnProperty('author.hbs') ? 'author' : 'index', + var view = paths.hasOwnProperty('author.hbs') ? 'author' : 'index'; // Format data for template - result = _.extend(formatPageResponse(posts, page), { - author: page.meta.filters.author ? page.meta.filters.author : '' - }); + formatPageResponse(posts, page, { + author: page.meta.filters.author ? page.meta.filters.author : '' + }).then(function (result) { + // If the resulting author is '' then 404. + if (!result.author) { + return next(); + } - // If the resulting author is '' then 404. - if (!result.author) { - return next(); - } - - setResponseContext(req, res); - res.render(view, result); + setResponseContext(req, res); + res.render(view, result); + }); }); }); }).catch(handleError(next)); @@ -339,12 +378,13 @@ frontendControllers = { filters.doFilter('prePostsRender', post).then(function (post) { getActiveThemePaths().then(function (paths) { - var view = template.getThemeViewForPost(paths, post), - response = formatResponse(post); + var view = template.getThemeViewForPost(paths, post); - setResponseContext(req, res, response); + return formatResponse(post).then(function (response) { + setResponseContext(req, res, response); - res.render(view, response); + res.render(view, response); + }); }); }); } diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index 2d4a355ec0..d067bc0d12 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -27,6 +27,7 @@ coreHelpers.is = require('./is'); coreHelpers.has = require('./has'); coreHelpers.meta_description = require('./meta_description'); coreHelpers.meta_title = require('./meta_title'); +coreHelpers.navigation = require('./navigation'); coreHelpers.page_url = require('./page_url'); coreHelpers.pageUrl = require('./page_url').deprecated; coreHelpers.pagination = require('./pagination'); @@ -89,6 +90,7 @@ registerHelpers = function (adminHbs) { registerThemeHelper('foreach', coreHelpers.foreach); registerThemeHelper('is', coreHelpers.is); registerThemeHelper('has', coreHelpers.has); + registerThemeHelper('navigation', coreHelpers.navigation); registerThemeHelper('page_url', coreHelpers.page_url); registerThemeHelper('pageUrl', coreHelpers.pageUrl); registerThemeHelper('pagination', coreHelpers.pagination); diff --git a/core/server/helpers/navigation.js b/core/server/helpers/navigation.js new file mode 100644 index 0000000000..0c4d15d78d --- /dev/null +++ b/core/server/helpers/navigation.js @@ -0,0 +1,58 @@ +// ### Navigation Helper +// `{{navigation}}` +// Outputs navigation menu of static urls + +var _ = require('lodash'), + hbs = require('express-hbs'), + errors = require('../errors'), + template = require('./template'), + navigation; + +navigation = function (options) { + /*jshint unused:false*/ + var nav, + context, + currentUrl = this.relativeUrl; + + if (!_.isObject(this.nav) || _.isFunction(this.nav)) { + return errors.logAndThrowError('navigation data is not an object or is a function'); + } + + if (this.nav.filter(function (e) { + return (_.isUndefined(e.label) || _.isUndefined(e.url)); + }).length > 0) { + return errors.logAndThrowError('All values must be defined for label, url and current'); + } + + // check for non-null string values + if (this.nav.filter(function (e) { + return ((!_.isNull(e.label) && !_.isString(e.label)) || + (!_.isNull(e.url) && !_.isString(e.url))); + }).length > 0) { + return errors.logAndThrowError('Invalid value, Url and Label must be strings'); + } + + function _slugify(label) { + return label.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-'); + } + + // {{navigation}} should no-op if no data passed in + if (this.nav.length === 0) { + return new hbs.SafeString(''); + } + + nav = this.nav.map(function (e) { + var out = {}; + out.current = e.url === currentUrl; + out.label = e.label; + out.slug = _slugify(e.label); + out.url = hbs.handlebars.Utils.escapeExpression(e.url); + return out; + }); + + context = _.merge({}, {nav: nav}); + + return template.execute('navigation', context); +}; + +module.exports = navigation; diff --git a/core/server/helpers/tpl/navigation.hbs b/core/server/helpers/tpl/navigation.hbs new file mode 100644 index 0000000000..63d986f212 --- /dev/null +++ b/core/server/helpers/tpl/navigation.hbs @@ -0,0 +1,5 @@ + diff --git a/core/test/unit/server_helpers/navigation_spec.js b/core/test/unit/server_helpers/navigation_spec.js new file mode 100644 index 0000000000..ed1602936a --- /dev/null +++ b/core/test/unit/server_helpers/navigation_spec.js @@ -0,0 +1,85 @@ +/*globals describe, before, it*/ +/*jshint expr:true*/ +var should = require('should'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'); + +describe('{{navigation}} helper', function () { + before(function (done) { + utils.loadHelpers(); + hbs.express3({partialsDir: [utils.config.paths.helperTemplates]}); + hbs.cachePartials(function () { + done(); + }); + }); + + it('has loaded navigation helper', function () { + should.exist(handlebars.helpers.navigation); + }); + + it('should throw errors on invalid data', function () { + var runHelper = function (data) { + return function () { + helpers.navigation.call(data); + }; + }; + + runHelper('not an object').should.throwError('navigation data is not an object or is a function'); + runHelper(function () {}).should.throwError('navigation data is not an object or is a function'); + + runHelper({nav: [{label: 1, url: 'bar'}]}).should.throwError('Invalid value, Url and Label must be strings'); + runHelper({nav: [{label: 'foo', url: 1}]}).should.throwError('Invalid value, Url and Label must be strings'); + }); + + it('can render empty nav', function () { + var navigation = {nav:[]}, + rendered = helpers.navigation.call(navigation); + + should.exist(rendered); + rendered.string.should.be.equal(''); + }); + + it('can render one item', function () { + var singleItem = {label: 'Foo', url: '/foo'}, + navigation = {nav: [singleItem]}, + rendered = helpers.navigation.call(navigation); + + should.exist(rendered); + rendered.string.should.containEql('li'); + rendered.string.should.containEql('nav-foo'); + rendered.string.should.containEql('href="/foo"'); + }); + + it('can render multiple items', function () { + var firstItem = {label: 'Foo', url: '/foo'}, + secondItem = {label: 'Bar Baz Qux', url: '/qux'}, + navigation = {nav: [firstItem, secondItem]}, + rendered = helpers.navigation.call(navigation); + + should.exist(rendered); + rendered.string.should.containEql('nav-foo'); + rendered.string.should.containEql('nav-bar-baz-qux'); + rendered.string.should.containEql('href="/foo"'); + rendered.string.should.containEql('href="/qux"'); + }); + + it('can annotate the current url', function () { + var firstItem = {label: 'Foo', url: '/foo'}, + secondItem = {label: 'Bar', url: '/qux'}, + navigation = { + relativeUrl: '/foo', + nav: [firstItem, secondItem] + }, + rendered = helpers.navigation.call(navigation); + + should.exist(rendered); + rendered.string.should.containEql('nav-foo'); + rendered.string.should.containEql('nav-current'); + rendered.string.should.containEql('nav-foo nav-current'); + rendered.string.should.containEql('nav-bar "'); + }); +});