diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 71d000995e..0ecb89ddf1 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -88,6 +88,35 @@ function handleError(next) { }; } +function setResponseContext(req, res, data) { + var contexts = [], + pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1; + + // paged context + if (!isNaN(pageParam) && pageParam > 1) { + contexts.push('paged'); + } + + if (req.route.path === '/page/:page/') { + contexts.push('index'); + } else if (req.route.path === '/') { + contexts.push('home'); + contexts.push('index'); + } else if (/\/rss\/(:page\/)?$/.test(req.route.path)) { + contexts.push('rss'); + } else if (/^\/tag\//.test(req.route.path)) { + contexts.push('tag'); + } else if (/^\/author\//.test(req.route.path)) { + contexts.push('author'); + } else if (data && data.post && data.post.page) { + contexts.push('page'); + } else { + contexts.push('post'); + } + + res.locals.context = contexts; +} + // Add Request context parameter to the data object // to be passed down to the templates function setReqCtx(req, data) { @@ -146,6 +175,7 @@ frontendControllers = { view = 'index'; } + setResponseContext(req, res); res.render(view, formatPageResponse(posts, page)); }); }); @@ -200,6 +230,7 @@ frontendControllers = { if (!result.tag) { return next(); } + setResponseContext(req, res); res.render(view, result); }); }); @@ -254,6 +285,8 @@ frontendControllers = { if (!result.author) { return next(); } + + setResponseContext(req, res); res.render(view, result); }); }); @@ -325,9 +358,12 @@ frontendControllers = { filters.doFilter('prePostsRender', post).then(function (post) { getActiveThemePaths().then(function (paths) { - var view = template.getThemeViewForPost(paths, post); + var view = template.getThemeViewForPost(paths, post), + response = formatResponse(post); - res.render(view, formatResponse(post)); + setResponseContext(req, res, response); + + res.render(view, response); }); }); } @@ -470,6 +506,7 @@ frontendControllers = { } setReqCtx(req, page.posts); + setResponseContext(req, res); filters.doFilter('prePostsRender', page.posts).then(function (posts) { posts.forEach(function (post) { diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index b43af537e3..e9a303603f 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -673,6 +673,34 @@ coreHelpers.foreach = function (context, options) { return ret; }; +// ### Is Helper +// `{{#is "paged"}}` +// `{{#is "index, paged"}}` +// Checks whether we're in a given context. +coreHelpers.is = function (context, options) { + options = options || {}; + + var currentContext = options.data.root.context; + + if (!_.isString(context)) { + errors.logWarn('Invalid or no attribute given to is helper'); + return; + } + + function evaluateContext(expr) { + return expr.split(',').map(function (v) { + return v.trim(); + }).reduce(function (p, c) { + return p || _.contains(currentContext, c); + }, false); + } + + if (evaluateContext(context)) { + return options.fn(this); + } + return options.inverse(this); +}; + // ### Has Helper // `{{#has tag="video, music"}}` // `{{#has author="sam, pat"}}` @@ -851,6 +879,8 @@ registerHelpers = function (adminHbs, assetHash) { registerThemeHelper('foreach', coreHelpers.foreach); + registerThemeHelper('is', coreHelpers.is); + registerThemeHelper('has', coreHelpers.has); registerThemeHelper('page_url', coreHelpers.page_url); diff --git a/core/test/unit/frontend_spec.js b/core/test/unit/frontend_spec.js index 403daf12ee..3c7c95ca7c 100644 --- a/core/test/unit/frontend_spec.js +++ b/core/test/unit/frontend_spec.js @@ -202,6 +202,7 @@ describe('Frontend Controller', function () { route: {} }, res = { + locals: {}, render: function (view) { assert.equal(view, 'home'); done(); @@ -220,6 +221,7 @@ describe('Frontend Controller', function () { route: {} }, res = { + locals: {}, render: function (view) { assert.equal(view, 'index'); done(); @@ -251,6 +253,7 @@ describe('Frontend Controller', function () { route: {} }, res = { + locals: {}, render: function (view) { assert.equal(view, 'index'); done(); @@ -360,9 +363,13 @@ describe('Frontend Controller', function () { it('it will render custom tag template if it exists', function (done) { var req = { path: '/tag/' + mockTags[0].slug, - params: {} + params: {}, + route: { + path: '/tag/:slug' + } }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'tag'); assert.equal(context.tag, mockTags[0]); @@ -576,9 +583,14 @@ describe('Frontend Controller', function () { it('it will render custom page template if it exists', function (done) { var req = { - path: '/' + mockPosts[2].posts[0].slug + path: '/' + mockPosts[2].posts[0].slug, + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'page-' + mockPosts[2].posts[0].slug); assert.equal(context.post, mockPosts[2].posts[0]); @@ -601,9 +613,14 @@ describe('Frontend Controller', function () { it('will render static page via /:slug', function (done) { var req = { - path: '/' + mockPosts[0].posts[0].slug + path: '/' + mockPosts[0].posts[0].slug, + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'page'); assert.equal(context.post, mockPosts[0].posts[0]); @@ -620,6 +637,7 @@ describe('Frontend Controller', function () { path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') }, res = { + locals: {}, render: sinon.spy() }; @@ -634,6 +652,7 @@ describe('Frontend Controller', function () { path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: function (arg) { res.render.called.should.be.false; @@ -650,6 +669,7 @@ describe('Frontend Controller', function () { path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: sinon.spy() }; @@ -673,9 +693,14 @@ describe('Frontend Controller', function () { it('will render static page via /:slug', function (done) { var req = { - path: '/' + mockPosts[0].posts[0].slug + path: '/' + mockPosts[0].posts[0].slug, + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'page'); assert.equal(context.post, mockPosts[0].posts[0]); @@ -705,6 +730,7 @@ describe('Frontend Controller', function () { path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: function (arg) { res.render.called.should.be.false; @@ -721,6 +747,7 @@ describe('Frontend Controller', function () { path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: sinon.spy() }; @@ -746,9 +773,14 @@ describe('Frontend Controller', function () { it('will render post via /:slug', function (done) { var req = { - path: '/' + mockPosts[1].posts[0].slug + path: '/' + mockPosts[1].posts[0].slug, + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'post'); assert(context.post, 'Context object has post attribute'); @@ -766,6 +798,7 @@ describe('Frontend Controller', function () { path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug].join('/') }, res = { + locals: {}, render: sinon.spy() }; @@ -781,6 +814,7 @@ describe('Frontend Controller', function () { path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: function (arg) { res.render.called.should.be.false; @@ -797,6 +831,7 @@ describe('Frontend Controller', function () { path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: sinon.spy() }; @@ -821,9 +856,14 @@ describe('Frontend Controller', function () { it('will render post via /YYYY/MM/DD/:slug', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/'), + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'post'); assert(context.post, 'Context object has post attribute'); @@ -842,6 +882,7 @@ describe('Frontend Controller', function () { path: '/' + [date, mockPosts[1].posts[0].slug].join('/') }, res = { + locals: {}, render: sinon.spy() }; @@ -856,6 +897,7 @@ describe('Frontend Controller', function () { path: '/' + mockPosts[1].posts[0].slug }, res = { + locals: {}, render: sinon.spy() }; @@ -872,6 +914,7 @@ describe('Frontend Controller', function () { path: '/' + [dateFormat, mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: function (arg) { res.render.called.should.be.false; @@ -888,6 +931,7 @@ describe('Frontend Controller', function () { path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: sinon.spy() }; @@ -912,9 +956,14 @@ describe('Frontend Controller', function () { it('will render post via /:year/:slug', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/'), + route: { + path: '*' + }, + params: {} }, res = { + locals: {}, render: function (view, context) { assert.equal(view, 'post'); assert(context.post, 'Context object has post attribute'); @@ -933,6 +982,7 @@ describe('Frontend Controller', function () { path: '/' + [date, mockPosts[1].posts[0].slug].join('/') }, res = { + locals: {}, render: sinon.spy() }; @@ -948,6 +998,7 @@ describe('Frontend Controller', function () { path: '/' + [date, mockPosts[1].posts[0].slug].join('/') }, res = { + locals: {}, render: sinon.spy() }; @@ -962,6 +1013,7 @@ describe('Frontend Controller', function () { path: '/' + mockPosts[1].posts[0].slug }, res = { + locals: {}, render: sinon.spy() }; @@ -978,6 +1030,7 @@ describe('Frontend Controller', function () { path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: function (arg) { res.render.called.should.be.false; @@ -994,6 +1047,7 @@ describe('Frontend Controller', function () { path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') }, res = { + locals: {}, render: sinon.spy(), redirect: sinon.spy() }; diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js index 56e53fbbd1..e6eb3e3205 100644 --- a/core/test/unit/server_helpers_index_spec.js +++ b/core/test/unit/server_helpers_index_spec.js @@ -669,6 +669,55 @@ describe('Core Helpers', function () { }); }); + describe('is Block Helper', function () { + it('has loaded is block helper', function () { + should.exist(handlebars.helpers.is); + }); + + // All positive tests + it('should match single context "index"', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.is.call( + {}, + 'index', + {fn: fn, inverse: inverse, data: {root: {context: ['home', 'index']}}} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should match OR context "index, paged"', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.is.call( + {}, + 'index, paged', + {fn: fn, inverse: inverse, data: {root: {context: ['tag', 'paged']}}} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should not match "paged"', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.is.call( + {}, + 'paged', + {fn: fn, inverse: inverse, data: {root: {context: ['index', 'home']}}} + ); + + fn.called.should.be.false; + inverse.called.should.be.true; + }); + }); + describe('has Block Helper', function () { it('has loaded has block helper', function () { should.exist(handlebars.helpers.has);