diff --git a/Gruntfile.js b/Gruntfile.js index 031eff40cb..85b2aeda90 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -231,6 +231,10 @@ var _ = require('lodash'), src: ['core/test/unit/**/server*_spec.js'] }, + helpers: { + src: ['core/test/unit/server_helpers/*_spec.js'] + }, + showdown: { src: ['core/test/unit/**/showdown*_spec.js'] }, diff --git a/core/server/config/index.js b/core/server/config/index.js index 5bdca30d94..867d1562f5 100644 --- a/core/server/config/index.js +++ b/core/server/config/index.js @@ -5,6 +5,7 @@ var path = require('path'), Promise = require('bluebird'), + crypto = require('crypto'), fs = require('fs'), url = require('url'), _ = require('lodash'), @@ -13,6 +14,7 @@ var path = require('path'), requireTree = require('../require-tree').readAll, errors = require('../errors'), configUrl = require('./url'), + packageInfo = require('../../../package.json'), appRoot = path.resolve(__dirname, '../../../'), corePath = path.resolve(appRoot, 'core/'), testingEnvs = ['testing', 'testing-mysql', 'testing-pg'], @@ -70,7 +72,8 @@ ConfigManager.prototype.init = function (rawConfig) { ConfigManager.prototype.set = function (config) { var localPath = '', contentPath, - subdir; + subdir, + assetHash; // Merge passed in config object onto our existing config object. // We're using merge here as it doesn't assign `undefined` properties @@ -98,6 +101,9 @@ ConfigManager.prototype.set = function (config) { // Otherwise default to default content path location contentPath = this._config.paths.contentPath || path.resolve(appRoot, 'content'); + assetHash = this._config.assetHash || + (crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10); + if (!knexInstance && this._config.database && this._config.database.client) { knexInstance = knex(this._config.database); } @@ -145,12 +151,14 @@ ConfigManager.prototype.set = function (config) { extensions: ['.jpg', '.jpeg', '.gif', '.png', '.svg', '.svgz'], contentTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'] }, - deprecatedItems: ['updateCheck', 'mail.fromaddress'] + deprecatedItems: ['updateCheck', 'mail.fromaddress'], + // create a hash for cache busting assets + assetHash: assetHash }); // Also pass config object to // configUrl object to maintain - // clean depedency tree + // clean dependency tree configUrl.setConfig(this._config); // For now we're going to copy the current state of this._config diff --git a/core/server/helpers/asset.js b/core/server/helpers/asset.js new file mode 100644 index 0000000000..251fc96c3f --- /dev/null +++ b/core/server/helpers/asset.js @@ -0,0 +1,39 @@ +// # Asset helper +// Usage: `{{asset "css/screen.css"}}`, `{{asset "css/screen.css" ghost="true"}}` +// +// Returns the path to the specified asset. The ghost flag outputs the asset path for the Ghost admin + +var hbs = require('express-hbs'), + config = require('../config'), + utils = require('./utils'), + asset; + +asset = function (context, options) { + var output = '', + isAdmin = options && options.hash && options.hash.ghost; + + output += config.paths.subdir + '/'; + + if (!context.match(/^favicon\.ico$/) && !context.match(/^shared/) && !context.match(/^asset/)) { + if (isAdmin) { + output += 'ghost/'; + } else { + output += 'assets/'; + } + } + + // Get rid of any leading slash on the context + context = context.replace(/^\//, ''); + output += context; + + if (!context.match(/^favicon\.ico$/)) { + output = utils.assetTemplate({ + source: output, + version: config.assetHash + }); + } + + return new hbs.handlebars.SafeString(output); +}; + +module.exports = asset; diff --git a/core/server/helpers/author.js b/core/server/helpers/author.js new file mode 100644 index 0000000000..ce48bf48de --- /dev/null +++ b/core/server/helpers/author.js @@ -0,0 +1,45 @@ +// # Author Helper +// Usage: `{{author}}` OR `{{#author}}{{/author}}` +// +// Can be used as either an output or a block helper +// +// Output helper: `{{author}}` +// Returns the full name of the author of a given post, or a blank string +// if the author could not be determined. +// +// Block helper: `{{#author}}{{/author}}` +// This is the default handlebars behaviour of dropping into the author object scope + +var hbs = require('express-hbs'), + _ = require('lodash'), + config = require('../config'), + utils = require('./utils'), + author; + +author = function (context, options) { + if (_.isUndefined(options)) { + options = context; + } + + if (options.fn) { + return hbs.handlebars.helpers['with'].call(this, this.author, options); + } + + var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, + output = ''; + + if (this.author && this.author.name) { + if (autolink) { + output = utils.linkTemplate({ + url: config.urlFor('author', {author: this.author}), + text: _.escape(this.author.name) + }); + } else { + output = _.escape(this.author.name); + } + } + + return new hbs.handlebars.SafeString(output); +}; + +module.exports = author; diff --git a/core/server/helpers/body_class.js b/core/server/helpers/body_class.js new file mode 100644 index 0000000000..1764e5f50e --- /dev/null +++ b/core/server/helpers/body_class.js @@ -0,0 +1,78 @@ +// # Body Class Helper +// Usage: `{{body_class}}` +// +// Output classes for the body element +// +// We use the name body_class to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var hbs = require('express-hbs'), + _ = require('lodash'), + api = require('../api'), + config = require('../config'), + filters = require('../filters'), + template = require('./template'), + body_class; + +body_class = function () { + var classes = [], + post = this.post, + tags = this.post && this.post.tags ? this.post.tags : this.tags || [], + page = this.post && this.post.page ? this.post.page : this.page || false; + + if (this.tag !== undefined) { + classes.push('tag-template'); + classes.push('tag-' + this.tag.slug); + } + + if (this.author !== undefined) { + classes.push('author-template'); + classes.push('author-' + this.author.slug); + } + + if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/(page\/\d)/)) { + classes.push('paged'); + // To be removed from pages by #2597 when we're ready to deprecate this + classes.push('archive-template'); + } else if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { + classes.push('home-template'); + } else if (post) { + // To be removed from pages by #2597 when we're ready to deprecate this + // i.e. this should be if (post && !page) { ... } + classes.push('post-template'); + } + + if (page) { + classes.push('page-template'); + // To be removed by #2597 when we're ready to deprecate this + classes.push('page'); + } + + if (tags) { + classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); + } + + return api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { + var activeTheme = response.settings[0], + paths = config.paths.availableThemes[activeTheme.value], + view; + + if (post && page) { + view = template.getThemeViewForPost(paths, post).split('-'); + + if (view[0] === 'page' && view.length > 1) { + classes.push(view.join('-')); + // To be removed by #2597 when we're ready to deprecate this + view.splice(1, 0, 'template'); + classes.push(view.join('-')); + } + } + + return filters.doFilter('body_class', classes).then(function (classes) { + var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); + return new hbs.handlebars.SafeString(classString.trim()); + }); + }); +}; + +module.exports = body_class; diff --git a/core/server/helpers/content.js b/core/server/helpers/content.js new file mode 100644 index 0000000000..ce29b42e60 --- /dev/null +++ b/core/server/helpers/content.js @@ -0,0 +1,38 @@ +// # Content Helper +// Usage: `{{content}}`, `{{content words="20"}}`, `{{content characters="256"}}` +// +// Turns content html into a safestring so that the user doesn't have to +// escape it or tell handlebars to leave it alone with a triple-brace. +// +// Enables tag-safe truncation of content by characters or words. + +var hbs = require('express-hbs'), + _ = require('lodash'), + downsize = require('downsize'), + downzero = require('../utils/downzero'), + content; + +content = function (options) { + var truncateOptions = (options || {}).hash || {}; + truncateOptions = _.pick(truncateOptions, ['words', 'characters']); + _.keys(truncateOptions).map(function (key) { + truncateOptions[key] = parseInt(truncateOptions[key], 10); + }); + + if (truncateOptions.hasOwnProperty('words') || truncateOptions.hasOwnProperty('characters')) { + // Legacy function: {{content words="0"}} should return leading tags. + if (truncateOptions.hasOwnProperty('words') && truncateOptions.words === 0) { + return new hbs.handlebars.SafeString( + downzero(this.html) + ); + } + + return new hbs.handlebars.SafeString( + downsize(this.html, truncateOptions) + ); + } + + return new hbs.handlebars.SafeString(this.html); +}; + +module.exports = content; diff --git a/core/server/helpers/date.js b/core/server/helpers/date.js new file mode 100644 index 0000000000..1886b1fde6 --- /dev/null +++ b/core/server/helpers/date.js @@ -0,0 +1,36 @@ +// # Date Helper +// Usage: `{{date format="DD MM, YYYY"}}`, `{{date updated_at format="DD MM, YYYY"}}` +// +// Formats a date using moment.js. Formats published_at by default but will also take a date as a parameter + +var moment = require('moment'), + date; + +date = function (context, options) { + if (!options && context.hasOwnProperty('hash')) { + options = context; + context = undefined; + + // set to published_at by default, if it's available + // otherwise, this will print the current date + if (this.published_at) { + context = this.published_at; + } + } + + // ensure that context is undefined, not null, as that can cause errors + context = context === null ? undefined : context; + + var f = options.hash.format || 'MMM Do, YYYY', + timeago = options.hash.timeago, + date; + + if (timeago) { + date = moment(context).fromNow(); + } else { + date = moment(context).format(f); + } + return date; +}; + +module.exports = date; diff --git a/core/server/helpers/encode.js b/core/server/helpers/encode.js new file mode 100644 index 0000000000..96b2d1087c --- /dev/null +++ b/core/server/helpers/encode.js @@ -0,0 +1,15 @@ +// # Encode Helper +// +// Usage: `{{encode uri}}` +// +// Returns URI encoded string + +var hbs = require('express-hbs'), + encode; + +encode = function (context, str) { + var uri = context || str; + return new hbs.handlebars.SafeString(encodeURIComponent(uri)); +}; + +module.exports = encode; diff --git a/core/server/helpers/excerpt.js b/core/server/helpers/excerpt.js new file mode 100644 index 0000000000..c3cc8a6a85 --- /dev/null +++ b/core/server/helpers/excerpt.js @@ -0,0 +1,36 @@ +// # Excerpt Helper +// Usage: `{{excerpt}}`, `{{excerpt words="50"}}`, `{{excerpt characters="256"}}` +// +// Attempts to remove all HTML from the string, and then shortens the result according to the provided option. +// +// Defaults to words="50" + +var hbs = require('express-hbs'), + _ = require('lodash'), + downsize = require('downsize'), + excerpt; + +excerpt = function (options) { + var truncateOptions = (options || {}).hash || {}, + excerpt; + + truncateOptions = _.pick(truncateOptions, ['words', 'characters']); + _.keys(truncateOptions).map(function (key) { + truncateOptions[key] = parseInt(truncateOptions[key], 10); + }); + + /*jslint regexp:true */ + excerpt = String(this.html).replace(/<\/?[^>]+>/gi, ''); + excerpt = excerpt.replace(/(\r\n|\n|\r)+/gm, ' '); + /*jslint regexp:false */ + + if (!truncateOptions.words && !truncateOptions.characters) { + truncateOptions.words = 50; + } + + return new hbs.handlebars.SafeString( + downsize(excerpt, truncateOptions) + ); +}; + +module.exports = excerpt; diff --git a/core/server/helpers/foreach.js b/core/server/helpers/foreach.js new file mode 100644 index 0000000000..2aac6c05e4 --- /dev/null +++ b/core/server/helpers/foreach.js @@ -0,0 +1,80 @@ +// # Foreach Helper +// Usage: `{{#foreach data}}{{/foreach}}` +// +// Block helper designed for looping through posts + +var hbs = require('express-hbs'), + foreach; + +foreach = function (context, options) { + var fn = options.fn, + inverse = options.inverse, + i = 0, + j = 0, + columns = options.hash.columns, + key, + ret = '', + data; + + if (options.data) { + data = hbs.handlebars.createFrame(options.data); + } + + function setKeys(_data, _i, _j, _columns) { + if (_i === 0) { + _data.first = true; + } + if (_i === _j - 1) { + _data.last = true; + } + // first post is index zero but still needs to be odd + if (_i % 2 === 1) { + _data.even = true; + } else { + _data.odd = true; + } + if (_i % _columns === 0) { + _data.rowStart = true; + } else if (_i % _columns === (_columns - 1)) { + _data.rowEnd = true; + } + return _data; + } + if (context && typeof context === 'object') { + if (context instanceof Array) { + for (j = context.length; i < j; i += 1) { + if (data) { + data.index = i; + data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; + data = setKeys(data, i, j, columns); + } + ret = ret + fn(context[i], {data: data}); + } + } else { + for (key in context) { + if (context.hasOwnProperty(key)) { + j += 1; + } + } + for (key in context) { + if (context.hasOwnProperty(key)) { + if (data) { + data.key = key; + data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; + data = setKeys(data, i, j, columns); + } + ret = ret + fn(context[key], {data: data}); + i += 1; + } + } + } + } + + if (i === 0) { + ret = inverse(this); + } + + return ret; +}; + +module.exports = foreach; diff --git a/core/server/helpers/ghost_foot.js b/core/server/helpers/ghost_foot.js new file mode 100644 index 0000000000..d2b78dcf38 --- /dev/null +++ b/core/server/helpers/ghost_foot.js @@ -0,0 +1,32 @@ +// # Ghost Foot Helper +// Usage: `{{ghost_foot}}` +// +// Outputs scripts and other assets at the bottom of a Ghost theme +// +// We use the name ghost_foot to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var hbs = require('express-hbs'), + _ = require('lodash'), + config = require('../config'), + filters = require('../filters'), + utils = require('./utils'), + ghost_foot; + +ghost_foot = function (options) { + /*jshint unused:false*/ + var jquery = utils.isProduction ? 'jquery.min.js' : 'jquery.js', + foot = []; + + foot.push(utils.scriptTemplate({ + source: config.paths.subdir + '/public/' + jquery, + version: config.assetHash + })); + + return filters.doFilter('ghost_foot', foot).then(function (foot) { + var footString = _.reduce(foot, function (memo, item) { return memo + '\n' + item; }, '\n'); + return new hbs.handlebars.SafeString(footString.trim()); + }); +}; + +module.exports = ghost_foot; diff --git a/core/server/helpers/ghost_head.js b/core/server/helpers/ghost_head.js new file mode 100644 index 0000000000..62e32fda61 --- /dev/null +++ b/core/server/helpers/ghost_head.js @@ -0,0 +1,113 @@ +// # Ghost Head Helper +// Usage: `{{ghost_head}}` +// +// Outputs scripts and other assets at the top of a Ghost theme +// +// We use the name ghost_head to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var hbs = require('express-hbs'), + moment = require('moment'), + _ = require('lodash'), + Promise = require('bluebird'), + + config = require('../config'), + filters = require('../filters'), + + urlHelper = require('./url'), + meta_description = require('./meta_description'), + meta_title = require('./meta_title'), + excerpt = require('./excerpt'), + tagsHelper = require('./tags'), + ghost_head; + +ghost_head = function (options) { + /*jshint unused:false*/ + var self = this, + blog = config.theme, + useStructuredData = !config.isPrivacyDisabled('useStructuredData'), + head = [], + majorMinor = /^(\d+\.)?(\d+)/, + trimmedVersion = this.version, + trimmedUrlpattern = /.+(?=\/page\/\d*\/)/, + trimmedUrl, next, prev, tags, + ops = [], + structuredData; + + trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?'; + + // Push Async calls to an array of promises + ops.push(urlHelper.call(self, {hash: {absolute: true}})); + ops.push(meta_description.call(self)); + ops.push(meta_title.call(self)); + + // Resolves promises then push pushes meta data into ghost_head + return Promise.settle(ops).then(function (results) { + var url = results[0].value(), + metaDescription = results[1].value(), + metaTitle = results[2].value(), + publishedDate, modifiedDate; + + if (!metaDescription) { + metaDescription = excerpt.call(self.post, {hash: {words: '40'}}); + } + + head.push(''); + + if (self.pagination) { + trimmedUrl = self.relativeUrl.match(trimmedUrlpattern); + if (self.pagination.prev) { + prev = (self.pagination.prev > 1 ? prev = '/page/' + self.pagination.prev + '/' : prev = '/'); + prev = (trimmedUrl) ? '/' + trimmedUrl + prev : prev; + head.push(''); + } + if (self.pagination.next) { + next = '/page/' + self.pagination.next + '/'; + next = (trimmedUrl) ? '/' + trimmedUrl + next : next; + head.push(''); + } + } + + // Test to see if we are on a post page and that Structured data has not been disabled in config.js + if (self.post && useStructuredData) { + publishedDate = moment(self.post.published_at).toISOString(); + modifiedDate = moment(self.post.updated_at).toISOString(); + + structuredData = { + 'og:site_name': _.escape(blog.title), + 'og:type': 'article', + 'og:title': metaTitle, + 'og:description': metaDescription + '...', + 'og:url': url, + 'article:published_time': publishedDate, + 'article:modified_time': modifiedDate + }; + + if (self.post.image) { + structuredData['og:image'] = _.escape(blog.url) + self.post.image; + } + + _.each(structuredData, function (content, property) { + head.push(''); + }); + + // Calls tag helper and assigns an array of tag names for a post + tags = tagsHelper.call(self.post, {hash: {autolink: 'false'}}).string.split(','); + + _.each(tags, function (tag) { + if (tag !== '') { + head.push(''); + } + }); + } + head.push(''); + head.push(''); + return filters.doFilter('ghost_head', head); + }).then(function (head) { + var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, ''); + return new hbs.handlebars.SafeString(headString.trim()); + }); +}; + +module.exports = ghost_head; diff --git a/core/server/helpers/ghost_script_tags.js b/core/server/helpers/ghost_script_tags.js new file mode 100644 index 0000000000..8936b5ba06 --- /dev/null +++ b/core/server/helpers/ghost_script_tags.js @@ -0,0 +1,25 @@ +// # Ghost Script Tags Helpers +// Used in the ghost admin only +// +// We use the name ghost_script_tags to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var _ = require('lodash'), + utils = require('./utils'), + config = require('../config'), + ghost_script_tags; + +ghost_script_tags = function () { + var scriptList = utils.isProduction ? utils.scriptFiles.production : utils.scriptFiles.development; + + scriptList = _.map(scriptList, function (fileName) { + return utils.scriptTemplate({ + source: config.paths.subdir + '/ghost/scripts/' + fileName, + version: config.assetHash + }); + }); + + return scriptList.join(''); +}; + +module.exports = ghost_script_tags; diff --git a/core/server/helpers/has.js b/core/server/helpers/has.js new file mode 100644 index 0000000000..ae4296a8e4 --- /dev/null +++ b/core/server/helpers/has.js @@ -0,0 +1,56 @@ +// # Has Helper +// Usage: `{{#has tag="video, music"}}`, `{{#has author="sam, pat"}}` +// +// Checks if a post has a particular property + +var _ = require('lodash'), + errors = require('../errors'), + has; + +has = function (options) { + options = options || {}; + options.hash = options.hash || {}; + + var tags = _.pluck(this.tags, 'name'), + author = this.author ? this.author.name : null, + tagList = options.hash.tag || false, + authorList = options.hash.author || false, + tagsOk, + authorOk; + + function evaluateTagList(expr, tags) { + return expr.split(',').map(function (v) { + return v.trim(); + }).reduce(function (p, c) { + return p || (_.findIndex(tags, function (item) { + // Escape regex special characters + item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); + item = new RegExp(item, 'i'); + return item.test(c); + }) !== -1); + }, false); + } + + function evaluateAuthorList(expr, author) { + var authorList = expr.split(',').map(function (v) { + return v.trim().toLocaleLowerCase(); + }); + + return _.contains(authorList, author.toLocaleLowerCase()); + } + + if (!tagList && !authorList) { + errors.logWarn('Invalid or no attribute given to has helper'); + return; + } + + tagsOk = tagList && evaluateTagList(tagList, tags) || false; + authorOk = authorList && evaluateAuthorList(authorList, author) || false; + + if (tagsOk || authorOk) { + return options.fn(this); + } + return options.inverse(this); +}; + +module.exports = has; diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index 4987435271..2b559e2fdd 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -1,348 +1,47 @@ -var downsize = require('downsize'), - hbs = require('express-hbs'), - moment = require('moment'), +var hbs = require('express-hbs'), _ = require('lodash'), Promise = require('bluebird'), - api = require('../api'), + config = require('../config'), errors = require('../errors'), - filters = require('../filters'), - template = require('./template'), - schema = require('../data/schema').checks, - downzero = require('../utils/downzero'), - assetTemplate = _.template('<%= source %>?v=<%= version %>'), - linkTemplate = _.template('<%= text %>'), - scriptTemplate = _.template(''), - isProduction = process.env.NODE_ENV === 'production', + utils = require('./utils'), coreHelpers = {}, - registerHelpers, + registerHelpers; - scriptFiles = { - production: [ - 'vendor.min.js', - 'ghost.min.js' - ], - development: [ - 'vendor-dev.js', - 'templates-dev.js', - 'ghost-dev.js' - ] - }; +// Pre-load settings data: +// - activeTheme +// - permalinks -if (!isProduction) { +if (!utils.isProduction) { hbs.handlebars.logger.level = 0; } - // [ description] - // - // @param {Object} context date object - // @param {*} options - // @return {Object} A Moment time / date object +coreHelpers.asset = require('./asset'); +coreHelpers.author = require('./author'); +coreHelpers.body_class = require('./body_class'); +coreHelpers.content = require('./content'); +coreHelpers.date = require('./date'); +coreHelpers.encode = require('./encode'); +coreHelpers.excerpt = require('./excerpt'); +coreHelpers.foreach = require('./foreach'); +coreHelpers.ghost_foot = require('./ghost_foot'); +coreHelpers.ghost_head = require('./ghost_head'); +coreHelpers.is = require('./is'); +coreHelpers.has = require('./has'); +coreHelpers.meta_description = require('./meta_description'); +coreHelpers.meta_title = require('./meta_title'); +coreHelpers.page_url = require('./page_url'); +coreHelpers.pageUrl = require('./page_url').deprecated; +coreHelpers.pagination = require('./pagination'); +coreHelpers.plural = require('./plural'); +coreHelpers.post_class = require('./post_class'); +coreHelpers.tags = require('./tags'); +coreHelpers.title = require('./title'); +coreHelpers.url = require('./url'); -coreHelpers.date = function (context, options) { - if (!options && context.hasOwnProperty('hash')) { - options = context; - context = undefined; - - // set to published_at by default, if it's available - // otherwise, this will print the current date - if (this.published_at) { - context = this.published_at; - } - } - - // ensure that context is undefined, not null, as that can cause errors - context = context === null ? undefined : context; - - var f = options.hash.format || 'MMM Do, YYYY', - timeago = options.hash.timeago, - date; - - if (timeago) { - date = moment(context).fromNow(); - } else { - date = moment(context).format(f); - } - return date; -}; - -// -// ### URI Encoding helper -// -// *Usage example:* -// `{{encode uri}}` -// -// Returns URI encoded string -// -coreHelpers.encode = function (context, str) { - var uri = context || str; - return new hbs.handlebars.SafeString(encodeURIComponent(uri)); -}; - -// ### Page URL Helper -// -// *Usage example:* -// `{{page_url 2}}` -// -// Returns the URL for the page specified in the current object -// context. -// -coreHelpers.page_url = function (context, block) { - /*jshint unused:false*/ - var url = config.paths.subdir; - - if (this.tagSlug !== undefined) { - url += '/tag/' + this.tagSlug; - } - - if (this.authorSlug !== undefined) { - url += '/author/' + this.authorSlug; - } - - if (context > 1) { - url += '/page/' + context; - } - - url += '/'; - - return url; -}; - -// ### Page URL Helper: DEPRECATED -// -// *Usage example:* -// `{{pageUrl 2}}` -// -// Returns the URL for the page specified in the current object -// context. This helper is deprecated and will be removed in future versions. -// -coreHelpers.pageUrl = function (context, block) { - errors.logWarn('Warning: pageUrl is deprecated, please use page_url instead\n' + - 'The helper pageUrl has been replaced with page_url in Ghost 0.4.2, and will be removed entirely in Ghost 0.6\n' + - 'In your theme\'s pagination.hbs file, pageUrl should be renamed to page_url'); - - /*jshint unused:false*/ - var self = this; - - return coreHelpers.page_url.call(self, context, block); -}; - -// ### URL helper -// -// *Usage example:* -// `{{url}}` -// `{{url absolute="true"}}` -// -// Returns the URL for the current object context -// i.e. If inside a post context will return post permalink -// absolute flag outputs absolute URL, else URL is relative -coreHelpers.url = function (options) { - var absolute = options && options.hash.absolute; - - if (schema.isPost(this)) { - return config.urlForPost(api.settings, this, absolute); - } - - if (schema.isTag(this)) { - return Promise.resolve(config.urlFor('tag', {tag: this}, absolute)); - } - - if (schema.isUser(this)) { - return Promise.resolve(config.urlFor('author', {author: this}, absolute)); - } - - return Promise.resolve(config.urlFor(this, absolute)); -}; - -// ### Asset helper -// -// *Usage example:* -// `{{asset "css/screen.css"}}` -// `{{asset "css/screen.css" ghost="true"}}` -// Returns the path to the specified asset. The ghost -// flag outputs the asset path for the Ghost admin -coreHelpers.asset = function (context, options) { - var output = '', - isAdmin = options && options.hash && options.hash.ghost; - - output += config.paths.subdir + '/'; - - if (!context.match(/^favicon\.ico$/) && !context.match(/^shared/) && !context.match(/^asset/)) { - if (isAdmin) { - output += 'ghost/'; - } else { - output += 'assets/'; - } - } - - // Get rid of any leading slash on the context - context = context.replace(/^\//, ''); - output += context; - - if (!context.match(/^favicon\.ico$/)) { - output = assetTemplate({ - source: output, - version: coreHelpers.assetHash - }); - } - - return new hbs.handlebars.SafeString(output); -}; - -// ### Author Helper -// -// *Usage example:* -// `{{author}}` -// -// Returns the full name of the author of a given post, or a blank string -// if the author could not be determined. -// -coreHelpers.author = function (context, options) { - if (_.isUndefined(options)) { - options = context; - } - - if (options.fn) { - return hbs.handlebars.helpers['with'].call(this, this.author, options); - } - - var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, - output = ''; - - if (this.author && this.author.name) { - if (autolink) { - output = linkTemplate({ - url: config.urlFor('author', {author: this.author}), - text: _.escape(this.author.name) - }); - } else { - output = _.escape(this.author.name); - } - } - - return new hbs.handlebars.SafeString(output); -}; - -// ### Tags Helper -// -// *Usage example:* -// `{{tags}}` -// `{{tags separator=' - '}}` -// -// Returns a string of the tags on the post. -// By default, tags are separated by commas. -// -// Note that the standard {{#each tags}} implementation is unaffected by this helper -// and can be used for more complex templates. -coreHelpers.tags = function (options) { - options = options || {}; - options.hash = options.hash || {}; - - var autolink = options.hash && _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, - separator = options.hash && _.isString(options.hash.separator) ? options.hash.separator : ', ', - prefix = options.hash && _.isString(options.hash.prefix) ? options.hash.prefix : '', - suffix = options.hash && _.isString(options.hash.suffix) ? options.hash.suffix : '', - output = ''; - - function createTagList(tags) { - var tagNames = _.pluck(tags, 'name'); - - if (autolink) { - return _.map(tags, function (tag) { - return linkTemplate({ - url: config.urlFor('tag', {tag: tag}), - text: _.escape(tag.name) - }); - }).join(separator); - } - return _.escape(tagNames.join(separator)); - } - - if (this.tags && this.tags.length) { - output = prefix + createTagList(this.tags) + suffix; - } - - return new hbs.handlebars.SafeString(output); -}; - -// ### Content Helper -// -// *Usage example:* -// `{{content}}` -// `{{content words="20"}}` -// `{{content characters="256"}}` -// -// Turns content html into a safestring so that the user doesn't have to -// escape it or tell handlebars to leave it alone with a triple-brace. -// -// Enables tag-safe truncation of content by characters or words. -// -// **returns** SafeString content html, complete or truncated. -// -coreHelpers.content = function (options) { - var truncateOptions = (options || {}).hash || {}; - truncateOptions = _.pick(truncateOptions, ['words', 'characters']); - _.keys(truncateOptions).map(function (key) { - truncateOptions[key] = parseInt(truncateOptions[key], 10); - }); - - if (truncateOptions.hasOwnProperty('words') || truncateOptions.hasOwnProperty('characters')) { - // Legacy function: {{content words="0"}} should return leading tags. - if (truncateOptions.hasOwnProperty('words') && truncateOptions.words === 0) { - return new hbs.handlebars.SafeString( - downzero(this.html) - ); - } - - return new hbs.handlebars.SafeString( - downsize(this.html, truncateOptions) - ); - } - - return new hbs.handlebars.SafeString(this.html); -}; - -coreHelpers.title = function () { - return new hbs.handlebars.SafeString(hbs.handlebars.Utils.escapeExpression(this.title || '')); -}; - -// ### Excerpt Helper -// -// *Usage example:* -// `{{excerpt}}` -// `{{excerpt words="50"}}` -// `{{excerpt characters="256"}}` -// -// Attempts to remove all HTML from the string, and then shortens the result according to the provided option. -// -// Defaults to words="50" -// -// **returns** SafeString truncated, HTML-free content. -// -coreHelpers.excerpt = function (options) { - var truncateOptions = (options || {}).hash || {}, - excerpt; - - truncateOptions = _.pick(truncateOptions, ['words', 'characters']); - _.keys(truncateOptions).map(function (key) { - truncateOptions[key] = parseInt(truncateOptions[key], 10); - }); - - /*jslint regexp:true */ - excerpt = String(this.html).replace(/<\/?[^>]+>/gi, ''); - excerpt = excerpt.replace(/(\r\n|\n|\r)+/gm, ' '); - /*jslint regexp:false */ - - if (!truncateOptions.words && !truncateOptions.characters) { - truncateOptions.words = 50; - } - - return new hbs.handlebars.SafeString( - downsize(excerpt, truncateOptions) - ); -}; +coreHelpers.ghost_script_tags = require('./ghost_script_tags'); // ### Filestorage helper // @@ -383,481 +82,6 @@ coreHelpers.blog_url = function (context, options) { return config.theme.url.toString(); }; -coreHelpers.ghost_script_tags = function () { - var scriptList = isProduction ? scriptFiles.production : scriptFiles.development; - - scriptList = _.map(scriptList, function (fileName) { - return scriptTemplate({ - source: config.paths.subdir + '/ghost/scripts/' + fileName, - version: coreHelpers.assetHash - }); - }); - - return scriptList.join(''); -}; - -/* - * Asynchronous Theme Helpers (Registered with registerAsyncThemeHelper) - */ - -coreHelpers.body_class = function (options) { - /*jshint unused:false*/ - var classes = [], - post = this.post, - tags = this.post && this.post.tags ? this.post.tags : this.tags || [], - page = this.post && this.post.page ? this.post.page : this.page || false; - - if (this.tag !== undefined) { - classes.push('tag-template'); - classes.push('tag-' + this.tag.slug); - } - - if (this.author !== undefined) { - classes.push('author-template'); - classes.push('author-' + this.author.slug); - } - - if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/(page\/\d)/)) { - classes.push('paged'); - // To be removed from pages by #2597 when we're ready to deprecate this - classes.push('archive-template'); - } else if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { - classes.push('home-template'); - } else if (post) { - // To be removed from pages by #2597 when we're ready to deprecate this - // i.e. this should be if (post && !page) { ... } - classes.push('post-template'); - } - - if (page) { - classes.push('page-template'); - // To be removed by #2597 when we're ready to deprecate this - classes.push('page'); - } - - if (tags) { - classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); - } - - return api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { - var activeTheme = response.settings[0], - paths = config.paths.availableThemes[activeTheme.value], - view; - - if (post && page) { - view = template.getThemeViewForPost(paths, post).split('-'); - - if (view[0] === 'page' && view.length > 1) { - classes.push(view.join('-')); - // To be removed by #2597 when we're ready to deprecate this - view.splice(1, 0, 'template'); - classes.push(view.join('-')); - } - } - - return filters.doFilter('body_class', classes).then(function (classes) { - var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); - return new hbs.handlebars.SafeString(classString.trim()); - }); - }); -}; - -coreHelpers.post_class = function (options) { - /*jshint unused:false*/ - var classes = ['post'], - tags = this.post && this.post.tags ? this.post.tags : this.tags || [], - featured = this.post && this.post.featured ? this.post.featured : this.featured || false, - page = this.post && this.post.page ? this.post.page : this.page || false; - - if (tags) { - classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); - } - - if (featured) { - classes.push('featured'); - } - - if (page) { - classes.push('page'); - } - - return filters.doFilter('post_class', classes).then(function (classes) { - var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); - return new hbs.handlebars.SafeString(classString.trim()); - }); -}; - -coreHelpers.ghost_head = function (options) { - /*jshint unused:false*/ - var self = this, - blog = config.theme, - useStructuredData = !config.isPrivacyDisabled('useStructuredData'), - head = [], - majorMinor = /^(\d+\.)?(\d+)/, - trimmedVersion = this.version, - trimmedUrlpattern = /.+(?=\/page\/\d*\/)/, - trimmedUrl, next, prev, tags, - ops = [], - structuredData; - - trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?'; - - // Push Async calls to an array of promises - ops.push(coreHelpers.url.call(self, {hash: {absolute: true}})); - ops.push(coreHelpers.meta_description.call(self)); - ops.push(coreHelpers.meta_title.call(self)); - - // Resolves promises then push pushes meta data into ghost_head - return Promise.settle(ops).then(function (results) { - var url = results[0].value(), - metaDescription = results[1].value(), - metaTitle = results[2].value(), - publishedDate, modifiedDate; - - if (!metaDescription) { - metaDescription = coreHelpers.excerpt.call(self.post, {hash: {words: '40'}}); - } - - head.push(''); - - if (self.pagination) { - trimmedUrl = self.relativeUrl.match(trimmedUrlpattern); - if (self.pagination.prev) { - prev = (self.pagination.prev > 1 ? prev = '/page/' + self.pagination.prev + '/' : prev = '/'); - prev = (trimmedUrl) ? '/' + trimmedUrl + prev : prev; - head.push(''); - } - if (self.pagination.next) { - next = '/page/' + self.pagination.next + '/'; - next = (trimmedUrl) ? '/' + trimmedUrl + next : next; - head.push(''); - } - } - - // Test to see if we are on a post page and that Structured data has not been disabled in config.js - if (self.post && useStructuredData) { - publishedDate = moment(self.post.published_at).toISOString(); - modifiedDate = moment(self.post.updated_at).toISOString(); - - structuredData = { - 'og:site_name': _.escape(blog.title), - 'og:type': 'article', - 'og:title': metaTitle, - 'og:description': metaDescription + '...', - 'og:url': url, - 'article:published_time': publishedDate, - 'article:modified_time': modifiedDate - }; - - if (self.post.image) { - structuredData['og:image'] = _.escape(blog.url) + self.post.image; - } - - _.each(structuredData, function (content, property) { - head.push(''); - }); - - // Calls tag helper and assigns an array of tag names for a post - tags = coreHelpers.tags.call(self.post, {hash: {autolink: 'false'}}).string.split(','); - - _.each(tags, function (tag) { - if (tag !== '') { - head.push(''); - } - }); - } - head.push(''); - head.push(''); - return filters.doFilter('ghost_head', head); - }).then(function (head) { - var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, ''); - return new hbs.handlebars.SafeString(headString.trim()); - }); -}; - -coreHelpers.ghost_foot = function (options) { - /*jshint unused:false*/ - var jquery = isProduction ? 'jquery.min.js' : 'jquery.js', - foot = []; - - foot.push(scriptTemplate({ - source: config.paths.subdir + '/public/' + jquery, - version: coreHelpers.assetHash - })); - - return filters.doFilter('ghost_foot', foot).then(function (foot) { - var footString = _.reduce(foot, function (memo, item) { return memo + '\n' + item; }, '\n'); - return new hbs.handlebars.SafeString(footString.trim()); - }); -}; - -coreHelpers.meta_title = function (options) { - /*jshint unused:false*/ - var title = '', - blog, - page, - pageString = ''; - - if (_.isString(this.relativeUrl)) { - blog = config.theme; - - page = this.relativeUrl.match(/\/page\/(\d+)/); - - if (page) { - pageString = ' - Page ' + page[1]; - } - - if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { - title = blog.title; - } else if (this.author) { - title = this.author.name + pageString + ' - ' + blog.title; - } else if (this.tag) { - title = this.tag.name + pageString + ' - ' + blog.title; - } else if (this.post) { - title = _.isEmpty(this.post.meta_title) ? this.post.title : this.post.meta_title; - } else { - title = blog.title + pageString; - } - } - return filters.doFilter('meta_title', title).then(function (title) { - title = title || ''; - return title.trim(); - }); -}; - -coreHelpers.meta_description = function (options) { - /*jshint unused:false*/ - var description, - blog; - - if (_.isString(this.relativeUrl)) { - blog = config.theme; - if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { - description = blog.description; - } else if (this.author) { - description = /\/page\//.test(this.relativeUrl) ? '' : this.author.bio; - } else if (this.tag || /\/page\//.test(this.relativeUrl)) { - description = ''; - } else if (this.post) { - description = _.isEmpty(this.post.meta_description) ? '' : this.post.meta_description; - } - } - - return filters.doFilter('meta_description', description).then(function (description) { - description = description || ''; - return description.trim(); - }); -}; - -coreHelpers.foreach = function (context, options) { - var fn = options.fn, - inverse = options.inverse, - i = 0, - j = 0, - columns = options.hash.columns, - key, - ret = '', - data; - - if (options.data) { - data = hbs.handlebars.createFrame(options.data); - } - - function setKeys(_data, _i, _j, _columns) { - if (_i === 0) { - _data.first = true; - } - if (_i === _j - 1) { - _data.last = true; - } - // first post is index zero but still needs to be odd - if (_i % 2 === 1) { - _data.even = true; - } else { - _data.odd = true; - } - if (_i % _columns === 0) { - _data.rowStart = true; - } else if (_i % _columns === (_columns - 1)) { - _data.rowEnd = true; - } - return _data; - } - if (context && typeof context === 'object') { - if (context instanceof Array) { - for (j = context.length; i < j; i += 1) { - if (data) { - data.index = i; - data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; - data = setKeys(data, i, j, columns); - } - ret = ret + fn(context[i], {data: data}); - } - } else { - for (key in context) { - if (context.hasOwnProperty(key)) { - j += 1; - } - } - for (key in context) { - if (context.hasOwnProperty(key)) { - if (data) { - data.key = key; - data.first = data.rowEnd = data.rowStart = data.last = data.even = data.odd = false; - data = setKeys(data, i, j, columns); - } - ret = ret + fn(context[key], {data: data}); - i += 1; - } - } - } - } - - if (i === 0) { - ret = inverse(this); - } - - 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"}}` -// Checks whether a post has at least one of the tags -coreHelpers.has = function (options) { - options = options || {}; - options.hash = options.hash || {}; - - var tags = _.pluck(this.tags, 'name'), - author = this.author ? this.author.name : null, - tagList = options.hash.tag || false, - authorList = options.hash.author || false, - tagsOk, - authorOk; - - function evaluateTagList(expr, tags) { - return expr.split(',').map(function (v) { - return v.trim(); - }).reduce(function (p, c) { - return p || (_.findIndex(tags, function (item) { - // Escape regex special characters - item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); - item = new RegExp(item, 'i'); - return item.test(c); - }) !== -1); - }, false); - } - - function evaluateAuthorList(expr, author) { - var authorList = expr.split(',').map(function (v) { - return v.trim().toLocaleLowerCase(); - }); - - return _.contains(authorList, author.toLocaleLowerCase()); - } - - if (!tagList && !authorList) { - errors.logWarn('Invalid or no attribute given to has helper'); - return; - } - - tagsOk = tagList && evaluateTagList(tagList, tags) || false; - authorOk = authorList && evaluateAuthorList(authorList, author) || false; - - if (tagsOk || authorOk) { - return options.fn(this); - } - return options.inverse(this); -}; - -// ### Pagination Helper -// `{{pagination}}` -// Outputs previous and next buttons, along with info about the current page -coreHelpers.pagination = function (options) { - /*jshint unused:false*/ - if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { - return errors.logAndThrowError('pagination data is not an object or is a function'); - } - - if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || - _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { - return errors.logAndThrowError('All values must be defined for page, pages, limit and total'); - } - - if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) || - (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { - return errors.logAndThrowError('Invalid value, Next/Prev must be a number'); - } - - if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || - !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { - return errors.logAndThrowError('Invalid value, check page, pages, limit and total are numbers'); - } - - var context = _.merge({}, this.pagination); - - if (this.tag !== undefined) { - context.tagSlug = this.tag.slug; - } - - if (this.author !== undefined) { - context.authorSlug = this.author.slug; - } - - return template.execute('pagination', context); -}; - -// ## Pluralize strings depending on item count -// {{plural 0 empty='No posts' singular='% post' plural='% posts'}} -// The 1st argument is the numeric variable which the helper operates on -// The 2nd argument is the string that will be output if the variable's value is 0 -// The 3rd argument is the string that will be output if the variable's value is 1 -// The 4th argument is the string that will be output if the variable's value is 2+ -// coreHelpers.plural = function (number, empty, singular, plural) { -coreHelpers.plural = function (context, options) { - if (_.isUndefined(options.hash) || _.isUndefined(options.hash.empty) || - _.isUndefined(options.hash.singular) || _.isUndefined(options.hash.plural)) { - return errors.logAndThrowError('All values must be defined for empty, singular and plural'); - } - - if (context === 0) { - return new hbs.handlebars.SafeString(options.hash.empty); - } else if (context === 1) { - return new hbs.handlebars.SafeString(options.hash.singular.replace('%', context)); - } else if (context >= 2) { - return new hbs.handlebars.SafeString(options.hash.plural.replace('%', context)); - } -}; - coreHelpers.helperMissing = function (arg) { if (arguments.length === 2) { return undefined; @@ -865,16 +89,6 @@ coreHelpers.helperMissing = function (arg) { errors.logError('Missing helper: "' + arg + '"'); }; -// ## Admin URL helper -// uses urlFor to generate a URL for either the admin or the frontend. -coreHelpers.admin_url = function (options) { - var absolute = options && options.hash && options.hash.absolute, - // Ghost isn't a named route as currently it violates the must start-and-end with slash rule - context = !options || !options.hash || !options.hash.frontend ? {relativeUrl: '/ghost'} : 'home'; - - return config.urlFor(context, absolute); -}; - // Register an async handlebars helper for a given handlebars instance function registerAsyncHelper(hbs, name, fn) { hbs.registerAsyncHelper(name, function (options, cb) { @@ -903,69 +117,41 @@ function registerAdminHelper(name, fn) { coreHelpers.adminHbs.registerHelper(name, fn); } -registerHelpers = function (adminHbs, assetHash) { +registerHelpers = function (adminHbs) { // Expose hbs instance for admin coreHelpers.adminHbs = adminHbs; - // Store hash for assets - coreHelpers.assetHash = assetHash; - // Register theme helpers registerThemeHelper('asset', coreHelpers.asset); - registerThemeHelper('author', coreHelpers.author); - registerThemeHelper('content', coreHelpers.content); - registerThemeHelper('title', coreHelpers.title); - registerThemeHelper('date', coreHelpers.date); - registerThemeHelper('encode', coreHelpers.encode); - registerThemeHelper('excerpt', coreHelpers.excerpt); - registerThemeHelper('foreach', coreHelpers.foreach); - registerThemeHelper('is', coreHelpers.is); - registerThemeHelper('has', coreHelpers.has); - registerThemeHelper('page_url', coreHelpers.page_url); - registerThemeHelper('pageUrl', coreHelpers.pageUrl); - registerThemeHelper('pagination', coreHelpers.pagination); - registerThemeHelper('tags', coreHelpers.tags); - registerThemeHelper('plural', coreHelpers.plural); + // Async theme helpers registerAsyncThemeHelper('body_class', coreHelpers.body_class); - - registerAsyncThemeHelper('e', coreHelpers.e); - registerAsyncThemeHelper('ghost_foot', coreHelpers.ghost_foot); - registerAsyncThemeHelper('ghost_head', coreHelpers.ghost_head); - registerAsyncThemeHelper('meta_description', coreHelpers.meta_description); - registerAsyncThemeHelper('meta_title', coreHelpers.meta_title); - registerAsyncThemeHelper('post_class', coreHelpers.post_class); - registerAsyncThemeHelper('url', coreHelpers.url); // Register admin helpers registerAdminHelper('ghost_script_tags', coreHelpers.ghost_script_tags); - registerAdminHelper('asset', coreHelpers.asset); - registerAdminHelper('apps', coreHelpers.apps); - registerAdminHelper('file_storage', coreHelpers.file_storage); - registerAdminHelper('blog_url', coreHelpers.blog_url); }; @@ -973,4 +159,4 @@ module.exports = coreHelpers; module.exports.loadCoreHelpers = registerHelpers; module.exports.registerThemeHelper = registerThemeHelper; module.exports.registerAsyncThemeHelper = registerAsyncThemeHelper; -module.exports.scriptFiles = scriptFiles; +module.exports.scriptFiles = utils.scriptFiles; diff --git a/core/server/helpers/is.js b/core/server/helpers/is.js new file mode 100644 index 0000000000..236bbbaef3 --- /dev/null +++ b/core/server/helpers/is.js @@ -0,0 +1,32 @@ +// # Is Helper +// Usage: `{{#is "paged"}}`, `{{#is "index, paged"}}` +// Checks whether we're in a given context. +var _ = require('lodash'), + errors = require('../errors'), + is; + +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); +}; + +module.exports = is; diff --git a/core/server/helpers/meta_description.js b/core/server/helpers/meta_description.js new file mode 100644 index 0000000000..faf51393eb --- /dev/null +++ b/core/server/helpers/meta_description.js @@ -0,0 +1,37 @@ +// # Meta Description Helper +// Usage: `{{meta_description}}` +// +// Page description used for sharing and SEO +// +// We use the name meta_description to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var _ = require('lodash'), + config = require('../config'), + filters = require('../filters'), + meta_description; + +meta_description = function () { + var description, + blog; + + if (_.isString(this.relativeUrl)) { + blog = config.theme; + if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { + description = blog.description; + } else if (this.author) { + description = /\/page\//.test(this.relativeUrl) ? '' : this.author.bio; + } else if (this.tag || /\/page\//.test(this.relativeUrl)) { + description = ''; + } else if (this.post) { + description = _.isEmpty(this.post.meta_description) ? '' : this.post.meta_description; + } + } + + return filters.doFilter('meta_description', description).then(function (description) { + description = description || ''; + return description.trim(); + }); +}; + +module.exports = meta_description; diff --git a/core/server/helpers/meta_title.js b/core/server/helpers/meta_title.js new file mode 100644 index 0000000000..934713ac30 --- /dev/null +++ b/core/server/helpers/meta_title.js @@ -0,0 +1,48 @@ +// # Meta Title Helper +// Usage: `{{meta_title}}` +// +// Page title used for sharing and SEO +// +// We use the name meta_title to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var _ = require('lodash'), + config = require('../config'), + filters = require('../filters'), + meta_title; + +meta_title = function (options) { + /*jshint unused:false*/ + var title = '', + blog, + page, + pageString = ''; + + if (_.isString(this.relativeUrl)) { + blog = config.theme; + + page = this.relativeUrl.match(/\/page\/(\d+)/); + + if (page) { + pageString = ' - Page ' + page[1]; + } + + if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') { + title = blog.title; + } else if (this.author) { + title = this.author.name + pageString + ' - ' + blog.title; + } else if (this.tag) { + title = this.tag.name + pageString + ' - ' + blog.title; + } else if (this.post) { + title = _.isEmpty(this.post.meta_title) ? this.post.title : this.post.meta_title; + } else { + title = blog.title + pageString; + } + } + return filters.doFilter('meta_title', title).then(function (title) { + title = title || ''; + return title.trim(); + }); +}; + +module.exports = meta_title; diff --git a/core/server/helpers/page_url.js b/core/server/helpers/page_url.js new file mode 100644 index 0000000000..9a6e0b5ed0 --- /dev/null +++ b/core/server/helpers/page_url.js @@ -0,0 +1,58 @@ +// ### Page URL Helper +// +// *Usage example:* +// `{{page_url 2}}` +// +// Returns the URL for the page specified in the current object +// context. +// +// We use the name page_url to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var config = require('../config'), + errors = require('../errors'), + page_url, + pageUrl; + +page_url = function (context, block) { + /*jshint unused:false*/ + var url = config.paths.subdir; + + if (this.tagSlug !== undefined) { + url += '/tag/' + this.tagSlug; + } + + if (this.authorSlug !== undefined) { + url += '/author/' + this.authorSlug; + } + + if (context > 1) { + url += '/page/' + context; + } + + url += '/'; + + return url; +}; + +// ### Page URL Helper: DEPRECATED +// +// *Usage example:* +// `{{pageUrl 2}}` +// +// Returns the URL for the page specified in the current object +// context. This helper is deprecated and will be removed in future versions. +// +pageUrl = function (context, block) { + errors.logWarn('Warning: pageUrl is deprecated, please use page_url instead\n' + + 'The helper pageUrl has been replaced with page_url in Ghost 0.4.2, and will be removed entirely in Ghost 0.6\n' + + 'In your theme\'s pagination.hbs file, pageUrl should be renamed to page_url'); + + /*jshint unused:false*/ + var self = this; + + return page_url.call(self, context, block); +}; + +module.exports = page_url; +module.exports.deprecated = pageUrl; diff --git a/core/server/helpers/pagination.js b/core/server/helpers/pagination.js new file mode 100644 index 0000000000..2b322dc03f --- /dev/null +++ b/core/server/helpers/pagination.js @@ -0,0 +1,44 @@ +// ### Pagination Helper +// `{{pagination}}` +// Outputs previous and next buttons, along with info about the current page + +var _ = require('lodash'), + errors = require('../errors'), + template = require('./template'), + pagination; + +pagination = function (options) { + /*jshint unused:false*/ + if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { + return errors.logAndThrowError('pagination data is not an object or is a function'); + } + + if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || + _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { + return errors.logAndThrowError('All values must be defined for page, pages, limit and total'); + } + + if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) || + (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { + return errors.logAndThrowError('Invalid value, Next/Prev must be a number'); + } + + if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || + !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { + return errors.logAndThrowError('Invalid value, check page, pages, limit and total are numbers'); + } + + var context = _.merge({}, this.pagination); + + if (this.tag !== undefined) { + context.tagSlug = this.tag.slug; + } + + if (this.author !== undefined) { + context.authorSlug = this.author.slug; + } + + return template.execute('pagination', context); +}; + +module.exports = pagination; diff --git a/core/server/helpers/plural.js b/core/server/helpers/plural.js new file mode 100644 index 0000000000..0234720c0c --- /dev/null +++ b/core/server/helpers/plural.js @@ -0,0 +1,31 @@ +// # Plural Helper +// Usage: `{{plural 0 empty='No posts' singular='% post' plural='% posts'}}` +// +// pluralises strings depending on item count +// +// The 1st argument is the numeric variable which the helper operates on +// The 2nd argument is the string that will be output if the variable's value is 0 +// The 3rd argument is the string that will be output if the variable's value is 1 +// The 4th argument is the string that will be output if the variable's value is 2+ + +var hbs = require('express-hbs'), + errors = require('../errors'), + _ = require('lodash'), + plural; + +plural = function (context, options) { + if (_.isUndefined(options.hash) || _.isUndefined(options.hash.empty) || + _.isUndefined(options.hash.singular) || _.isUndefined(options.hash.plural)) { + return errors.logAndThrowError('All values must be defined for empty, singular and plural'); + } + + if (context === 0) { + return new hbs.handlebars.SafeString(options.hash.empty); + } else if (context === 1) { + return new hbs.handlebars.SafeString(options.hash.singular.replace('%', context)); + } else if (context >= 2) { + return new hbs.handlebars.SafeString(options.hash.plural.replace('%', context)); + } +}; + +module.exports = plural; diff --git a/core/server/helpers/post_class.js b/core/server/helpers/post_class.js new file mode 100644 index 0000000000..b55bc0774f --- /dev/null +++ b/core/server/helpers/post_class.js @@ -0,0 +1,39 @@ +// # Post Class Helper +// Usage: `{{post_class}}` +// +// Output classes for the body element +// +// We use the name body_class to match the helper for consistency: +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +var hbs = require('express-hbs'), + _ = require('lodash'), + filters = require('../filters'), + post_class; + +post_class = function (options) { + /*jshint unused:false*/ + var classes = ['post'], + tags = this.post && this.post.tags ? this.post.tags : this.tags || [], + featured = this.post && this.post.featured ? this.post.featured : this.featured || false, + page = this.post && this.post.page ? this.post.page : this.page || false; + + if (tags) { + classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); + } + + if (featured) { + classes.push('featured'); + } + + if (page) { + classes.push('page'); + } + + return filters.doFilter('post_class', classes).then(function (classes) { + var classString = _.reduce(classes, function (memo, item) { return memo + ' ' + item; }, ''); + return new hbs.handlebars.SafeString(classString.trim()); + }); +}; + +module.exports = post_class; diff --git a/core/server/helpers/tags.js b/core/server/helpers/tags.js new file mode 100644 index 0000000000..419530e4d4 --- /dev/null +++ b/core/server/helpers/tags.js @@ -0,0 +1,46 @@ +// # Tags Helper +// Usage: `{{tags}}`, `{{tags separator=' - '}}` +// +// Returns a string of the tags on the post. +// By default, tags are separated by commas. +// +// Note that the standard {{#each tags}} implementation is unaffected by this helper + +var hbs = require('express-hbs'), + _ = require('lodash'), + config = require('../config'), + utils = require('./utils'), + tags; + +tags = function (options) { + options = options || {}; + options.hash = options.hash || {}; + + var autolink = options.hash && _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, + separator = options.hash && _.isString(options.hash.separator) ? options.hash.separator : ', ', + prefix = options.hash && _.isString(options.hash.prefix) ? options.hash.prefix : '', + suffix = options.hash && _.isString(options.hash.suffix) ? options.hash.suffix : '', + output = ''; + + function createTagList(tags) { + var tagNames = _.pluck(tags, 'name'); + + if (autolink) { + return _.map(tags, function (tag) { + return utils.linkTemplate({ + url: config.urlFor('tag', {tag: tag}), + text: _.escape(tag.name) + }); + }).join(separator); + } + return _.escape(tagNames.join(separator)); + } + + if (this.tags && this.tags.length) { + output = prefix + createTagList(this.tags) + suffix; + } + + return new hbs.handlebars.SafeString(output); +}; + +module.exports = tags; diff --git a/core/server/helpers/title.js b/core/server/helpers/title.js new file mode 100644 index 0000000000..adf42a3cc5 --- /dev/null +++ b/core/server/helpers/title.js @@ -0,0 +1,13 @@ +// # Title Helper +// Usage: `{{title}}` +// +// Overrides the standard behaviour of `{[title}}` to ensure the content is correctly escaped + +var hbs = require('express-hbs'), + title; + +title = function () { + return new hbs.handlebars.SafeString(hbs.handlebars.Utils.escapeExpression(this.title || '')); +}; + +module.exports = title; diff --git a/core/server/helpers/url.js b/core/server/helpers/url.js new file mode 100644 index 0000000000..788769ebd3 --- /dev/null +++ b/core/server/helpers/url.js @@ -0,0 +1,31 @@ +// # URL helper +// Usage: `{{url}}`, `{{url absolute="true"}}` +// +// Returns the URL for the current object scope i.e. If inside a post scope will return post permalink +// `absolute` flag outputs absolute URL, else URL is relative + +var Promise = require('bluebird'), + config = require('../config'), + api = require('../api'), + schema = require('../data/schema').checks, + url; + +url = function (options) { + var absolute = options && options.hash.absolute; + + if (schema.isPost(this)) { + return config.urlForPost(api.settings, this, absolute); + } + + if (schema.isTag(this)) { + return Promise.resolve(config.urlFor('tag', {tag: this}, absolute)); + } + + if (schema.isUser(this)) { + return Promise.resolve(config.urlFor('author', {author: this}, absolute)); + } + + return Promise.resolve(config.urlFor(this, absolute)); +}; + +module.exports = url; diff --git a/core/server/helpers/utils.js b/core/server/helpers/utils.js new file mode 100644 index 0000000000..ac47438a10 --- /dev/null +++ b/core/server/helpers/utils.js @@ -0,0 +1,22 @@ +var _ = require('lodash'), + utils; + +utils = { + assetTemplate: _.template('<%= source %>?v=<%= version %>'), + linkTemplate: _.template('<%= text %>'), + scriptTemplate: _.template(''), + isProduction: process.env.NODE_ENV === 'production', + scriptFiles: { + production: [ + 'vendor.min.js', + 'ghost.min.js' + ], + development: [ + 'vendor-dev.js', + 'templates-dev.js', + 'ghost-dev.js' + ] + } +}; + +module.exports = utils; diff --git a/core/server/index.js b/core/server/index.js index 86968dade2..45c0ec2951 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -1,6 +1,5 @@ // Module dependencies -var crypto = require('crypto'), - express = require('express'), +var express = require('express'), hbs = require('express-hbs'), compress = require('compression'), fs = require('fs'), @@ -18,7 +17,6 @@ var crypto = require('crypto'), models = require('./models'), permissions = require('./permissions'), apps = require('./apps'), - packageInfo = require('../../package.json'), GhostServer = require('./ghost-server'), // Variables @@ -132,9 +130,7 @@ function initNotifications() { function init(options) { // Get reference to an express app instance. var blogApp = express(), - adminApp = express(), - // create a hash for cache busting assets - assetHash = (crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10); + adminApp = express(); // ### Initialisation // The server and its dependencies require a populated config @@ -196,7 +192,7 @@ function init(options) { adminApp.engine('hbs', adminHbs.express3({})); // Load helpers - helpers.loadCoreHelpers(adminHbs, assetHash); + helpers.loadCoreHelpers(adminHbs); // ## Middleware and Routing middleware(blogApp, adminApp); diff --git a/core/test/unit/server_helpers/asset_spec.js b/core/test/unit/server_helpers/asset_spec.js new file mode 100644 index 0000000000..67c0667383 --- /dev/null +++ b/core/test/unit/server_helpers/asset_spec.js @@ -0,0 +1,110 @@ +/*globals describe, before, after, 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('{{asset}} helper', function () { + var rendered; + + before(function () { + utils.loadHelpers(); + utils.overrideConfig({assetHash: 'abc'}); + }); + + after(function () { + utils.restoreConfig(); + }); + + it('has loaded asset helper', function () { + should.exist(handlebars.helpers.asset); + }); + + describe('no subdirectory', function () { + it('handles favicon correctly', function () { + // with ghost set + rendered = helpers.asset('favicon.ico', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/favicon.ico'); + + // without ghost set + rendered = helpers.asset('favicon.ico'); + should.exist(rendered); + String(rendered).should.equal('/favicon.ico'); + }); + + it('handles shared assets correctly', function () { + // with ghost set + rendered = helpers.asset('shared/asset.js', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/shared/asset.js?v=abc'); + + // without ghost set + rendered = helpers.asset('shared/asset.js'); + should.exist(rendered); + String(rendered).should.equal('/shared/asset.js?v=abc'); + }); + + it('handles admin assets correctly', function () { + // with ghost set + rendered = helpers.asset('js/asset.js', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/ghost/js/asset.js?v=abc'); + }); + + it('handles theme assets correctly', function () { + // with ghost set + rendered = helpers.asset('js/asset.js'); + should.exist(rendered); + String(rendered).should.equal('/assets/js/asset.js?v=abc'); + }); + }); + + describe('with /blog subdirectory', function () { + before(function () { + utils.overrideConfig({url: 'http://testurl.com/blog'}); + }); + + it('handles favicon correctly', function () { + // with ghost set + rendered = helpers.asset('favicon.ico', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/blog/favicon.ico'); + + // without ghost set + rendered = helpers.asset('favicon.ico'); + should.exist(rendered); + String(rendered).should.equal('/blog/favicon.ico'); + }); + + it('handles shared assets correctly', function () { + // with ghost set + rendered = helpers.asset('shared/asset.js', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/blog/shared/asset.js?v=abc'); + + // without ghost set + rendered = helpers.asset('shared/asset.js'); + should.exist(rendered); + String(rendered).should.equal('/blog/shared/asset.js?v=abc'); + }); + + it('handles admin assets correctly', function () { + // with ghost set + rendered = helpers.asset('js/asset.js', {hash: {ghost: 'true'}}); + should.exist(rendered); + String(rendered).should.equal('/blog/ghost/js/asset.js?v=abc'); + }); + + it('handles theme assets correctly', function () { + // with ghost set + rendered = helpers.asset('js/asset.js'); + should.exist(rendered); + String(rendered).should.equal('/blog/assets/js/asset.js?v=abc'); + }); + }); +}); diff --git a/core/test/unit/server_helpers/author_spec.js b/core/test/unit/server_helpers/author_spec.js new file mode 100644 index 0000000000..71beca687e --- /dev/null +++ b/core/test/unit/server_helpers/author_spec.js @@ -0,0 +1,49 @@ +/*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('{{author}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded author helper', function () { + should.exist(handlebars.helpers.author); + }); + + it('Returns the link to the author from the context', function () { + var data = {author: {name: 'abc 123', slug: 'abc123', bio: '', website: '', status: '', location: ''}}, + result = helpers.author.call(data, {hash: {}}); + + String(result).should.equal('abc 123'); + }); + + it('Returns the full name of the author from the context if no autolink', function () { + var data = {author: {name: 'abc 123', slug: 'abc123'}}, + result = helpers.author.call(data, {hash: {autolink: 'false'}}); + + String(result).should.equal('abc 123'); + }); + + it('Returns a blank string where author data is missing', function () { + var data = {author: null}, + result = helpers.author.call(data, {hash: {}}); + + String(result).should.equal(''); + }); + + it('Functions as block helper if called with #', function () { + var data = {author: {name: 'abc 123', slug: 'abc123'}}, + // including fn emulates the # + result = helpers.author.call(data, {hash: {}, fn: function () { return 'FN'; }}); + + // It outputs the result of fn + String(result).should.equal('FN'); + }); +}); diff --git a/core/test/unit/server_helpers/body_class_spec.js b/core/test/unit/server_helpers/body_class_spec.js new file mode 100644 index 0000000000..bb689de1bb --- /dev/null +++ b/core/test/unit/server_helpers/body_class_spec.js @@ -0,0 +1,119 @@ +/*globals describe, before, after, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + Promise = require('bluebird'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'), + api = require('../../../server/api'); + +describe('{{body_class}} helper', function () { + var sandbox; + + before(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(api.settings, 'read', function () { + return Promise.resolve({ + settings: [{value: 'casper'}] + }); + }); + utils.loadHelpers(); + utils.overrideConfig({paths: { + availableThemes: { + casper: { + assets: null, + 'default.hbs': '/content/themes/casper/default.hbs', + 'index.hbs': '/content/themes/casper/index.hbs', + 'page.hbs': '/content/themes/casper/page.hbs', + 'page-about.hbs': '/content/themes/casper/page-about.hbs', + 'post.hbs': '/content/themes/casper/post.hbs' + } + } + }}); + }); + + after(function () { + utils.restoreConfig(); + sandbox.restore(); + }); + + it('has loaded body_class helper', function () { + should.exist(handlebars.helpers.body_class); + }); + + it('can render class string', function (done) { + helpers.body_class.call({}).then(function (rendered) { + should.exist(rendered); + + rendered.string.should.equal('home-template'); + + done(); + }).catch(done); + }); + + it('can render class string for context', function (done) { + Promise.all([ + helpers.body_class.call({relativeUrl: '/'}), + helpers.body_class.call({relativeUrl: '/a-post-title', post: {}}), + helpers.body_class.call({relativeUrl: '/page/4'}), + helpers.body_class.call({relativeUrl: '/tag/foo', tag: {slug: 'foo'}}), + helpers.body_class.call({relativeUrl: '/tag/foo/page/2', tag: {slug: 'foo'}}), + helpers.body_class.call({relativeUrl: '/author/bar', author: {slug: 'bar'}}), + helpers.body_class.call({relativeUrl: '/author/bar/page/2', author: {slug: 'bar'}}) + ]).then(function (rendered) { + rendered.length.should.equal(7); + + should.exist(rendered[0]); + should.exist(rendered[1]); + should.exist(rendered[2]); + should.exist(rendered[3]); + should.exist(rendered[4]); + should.exist(rendered[5]); + should.exist(rendered[6]); + + rendered[0].string.should.equal('home-template'); + rendered[1].string.should.equal('post-template'); + rendered[2].string.should.equal('paged archive-template'); + rendered[3].string.should.equal('tag-template tag-foo'); + rendered[4].string.should.equal('tag-template tag-foo paged archive-template'); + rendered[5].string.should.equal('author-template author-bar'); + rendered[6].string.should.equal('author-template author-bar paged archive-template'); + + done(); + }).catch(done); + }); + + it('can render class for static page', function (done) { + helpers.body_class.call({ + relativeUrl: '/', + post: { + page: true + } + }).then(function (rendered) { + should.exist(rendered); + rendered.string.should.equal('home-template page-template page'); + + done(); + }).catch(done); + }); + + it('can render class for static page with custom template', function (done) { + helpers.body_class.call({ + relativeUrl: '/about', + post: { + page: true, + slug: 'about' + + } + }).then(function (rendered) { + should.exist(rendered); + rendered.string.should.equal('post-template page-template page page-about page-template-about'); + + done(); + }).catch(done); + }); +}); diff --git a/core/test/unit/server_helpers/content_spec.js b/core/test/unit/server_helpers/content_spec.js new file mode 100644 index 0000000000..dde9855d3b --- /dev/null +++ b/core/test/unit/server_helpers/content_spec.js @@ -0,0 +1,182 @@ +/*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('{{content}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded content helper', function () { + should.exist(handlebars.helpers.content); + }); + + it('can render content', function () { + var html = 'Hello World', + rendered = helpers.content.call({html: html}); + + should.exist(rendered); + rendered.string.should.equal(html); + }); + + it('can truncate html by word', function () { + var html = '
Hello World! It\'s me!
', + rendered = ( + helpers.content + .call( + {html: html}, + {hash: {words: 2}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal('Hello World
'); + }); + + it('can truncate html to 0 words', function () { + var html = 'Hello World! It\'s me!
', + rendered = ( + helpers.content + .call( + {html: html}, + {hash: {words: '0'}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal(''); + }); + + it('can truncate html to 0 words, leaving image tag if it is first', function () { + var html = 'Hello World! It\'s me!
Hello World! It\'s me!
' +
+ 'Hello World! It\'s me!
Hi
BliBlob
Hello World! It\'s me!
', + rendered = ( + helpers.content + .call( + {html: html}, + {hash: {characters: 8}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal('Hello Wo
'); + }); +}); diff --git a/core/test/unit/server_helpers/date_spec.js b/core/test/unit/server_helpers/date_spec.js new file mode 100644 index 0000000000..918ce41375 --- /dev/null +++ b/core/test/unit/server_helpers/date_spec.js @@ -0,0 +1,68 @@ +/*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'), + moment = require('moment'); + +describe('{{date}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('is loaded', function () { + should.exist(handlebars.helpers.date); + }); + + // TODO: When timezone support is added these tests should be updated + // to test the output of the helper against static strings instead + // of calling moment(). Without timezone support the output of this + // helper may differ depending on what timezone the tests are run in. + + it('creates properly formatted date strings', function () { + var testDates = [ + '2013-12-31T11:28:58.593Z', + '2014-01-01T01:28:58.593Z', + '2014-02-20T01:28:58.593Z', + '2014-03-01T01:28:58.593Z' + ], + format = 'MMM Do, YYYY', + context = { + hash: { + format: format + } + }; + + testDates.forEach(function (d) { + var rendered = helpers.date.call({published_at: d}, context); + + should.exist(rendered); + rendered.should.equal(moment(d).format(format)); + }); + }); + + it('creates properly formatted time ago date strings', function () { + var testDates = [ + '2013-12-31T23:58:58.593Z', + '2014-01-01T00:28:58.593Z', + '2014-11-20T01:28:58.593Z', + '2014-03-01T01:28:58.593Z' + ], + context = { + hash: { + timeago: true + } + }; + + testDates.forEach(function (d) { + var rendered = helpers.date.call({published_at: d}, context); + + should.exist(rendered); + rendered.should.equal(moment(d).fromNow()); + }); + }); +}); diff --git a/core/test/unit/server_helpers/encode_spec.js b/core/test/unit/server_helpers/encode_spec.js new file mode 100644 index 0000000000..1c261dbd2b --- /dev/null +++ b/core/test/unit/server_helpers/encode_spec.js @@ -0,0 +1,28 @@ +/*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('{{encode}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded encode helper', function () { + should.exist(handlebars.helpers.encode); + }); + + it('can escape URI', function () { + var uri = '$pecial!Charact3r(De[iver]y)Foo #Bar', + expected = '%24pecial!Charact3r(De%5Biver%5Dy)Foo%20%23Bar', + escaped = helpers.encode(uri); + + should.exist(escaped); + String(escaped).should.equal(expected); + }); +}); diff --git a/core/test/unit/server_helpers/excerpt_spec.js b/core/test/unit/server_helpers/excerpt_spec.js new file mode 100644 index 0000000000..50ac837dea --- /dev/null +++ b/core/test/unit/server_helpers/excerpt_spec.js @@ -0,0 +1,82 @@ +/*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('{{excerpt}} Helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded excerpt helper', function () { + should.exist(handlebars.helpers.excerpt); + }); + + it('can render excerpt', function () { + var html = 'Hello World', + rendered = helpers.excerpt.call({html: html}); + + should.exist(rendered); + rendered.string.should.equal(html); + }); + + it('does not output HTML', function () { + var html = 'There are
10
types
of people in the world:' +
+ '
those who
' +
+ 'understand trinary
Hello World! It\'s me!
', + expected = 'Hello World', + rendered = ( + helpers.excerpt.call( + {html: html}, + {hash: {words: '2'}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); + + it('can truncate html with non-ascii characters by word', function () { + var html = 'Едквюэ опортэат праэчынт ючю но, квуй эю
', + expected = 'Едквюэ опортэат', + rendered = ( + helpers.excerpt.call( + {html: html}, + {hash: {words: '2'}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); + + it('can truncate html by character', function () { + var html = 'Hello World! It\'s me!
', + expected = 'Hello Wo', + rendered = ( + helpers.excerpt.call( + {html: html}, + {hash: {characters: '8'}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); +}); diff --git a/core/test/unit/server_helpers/foreach_spec.js b/core/test/unit/server_helpers/foreach_spec.js new file mode 100644 index 0000000000..3a6ba960a5 --- /dev/null +++ b/core/test/unit/server_helpers/foreach_spec.js @@ -0,0 +1,165 @@ +/*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('{{#foreach}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + // passed into the foreach helper. takes the input string along with the metadata about + // the current row and builds a csv output string that can be used to check the results. + function fn(input, data) { + data = data.data; + + // if there was no private data passed into the helper, no metadata + // was created, so just return the input + if (!data) { + return input + '\n'; + } + + return input + ',' + data.first + ',' + data.rowEnd + ',' + data.rowStart + ',' + + data.last + ',' + data.even + ',' + data.odd + '\n'; + } + + function inverse(input) { + return input; + } + + it('is loaded', function () { + should.exist(handlebars.helpers.foreach); + }); + + it('should return the correct result when no private data is supplied', function () { + var options = {}, + context = [], + _this = {}, + rendered; + + options.fn = fn; + options.inverse = inverse; + options.hash = { + columns: 0 + }; + + // test with context as an array + + context = 'hello world this is ghost'.split(' '); + + rendered = helpers.foreach.call(_this, context, options); + rendered.should.equal('hello\nworld\nthis\nis\nghost\n'); + + // test with context as an object + + context = { + one: 'hello', + two: 'world', + three: 'this', + four: 'is', + five: 'ghost' + }; + + rendered = helpers.foreach.call(_this, context, options); + rendered.should.equal('hello\nworld\nthis\nis\nghost\n'); + }); + + it('should return the correct result when private data is supplied', function () { + var options = {}, + context = [], + _this = {}, + rendered, + result; + + options.fn = fn; + options.inverse = inverse; + + options.hash = { + columns: 0 + }; + + options.data = {}; + + context = 'hello world this is ghost'.split(' '); + + rendered = helpers.foreach.call(_this, context, options); + + result = rendered.split('\n'); + result[0].should.equal('hello,true,false,false,false,false,true'); + result[1].should.equal('world,false,false,false,false,true,false'); + result[2].should.equal('this,false,false,false,false,false,true'); + result[3].should.equal('is,false,false,false,false,true,false'); + result[4].should.equal('ghost,false,false,false,true,false,true'); + }); + + it('should return the correct result when private data is supplied & there are multiple columns', function () { + var options = {}, + context = [], + _this = {}, + rendered, + result; + + options.fn = fn; + options.inverse = inverse; + + options.hash = { + columns: 2 + }; + + options.data = {}; + + // test with context as an array + + context = 'hello world this is ghost'.split(' '); + + rendered = helpers.foreach.call(_this, context, options); + + result = rendered.split('\n'); + result[0].should.equal('hello,true,false,true,false,false,true'); + result[1].should.equal('world,false,true,false,false,true,false'); + result[2].should.equal('this,false,false,true,false,false,true'); + result[3].should.equal('is,false,true,false,false,true,false'); + result[4].should.equal('ghost,false,false,true,true,false,true'); + + // test with context as an object + + context = { + one: 'hello', + two: 'world', + three: 'this', + four: 'is', + five: 'ghost' + }; + + rendered = helpers.foreach.call(_this, context, options); + + result = rendered.split('\n'); + result[0].should.equal('hello,true,false,true,false,false,true'); + result[1].should.equal('world,false,true,false,false,true,false'); + result[2].should.equal('this,false,false,true,false,false,true'); + result[3].should.equal('is,false,true,false,false,true,false'); + result[4].should.equal('ghost,false,false,true,true,false,true'); + }); + + it('should return the correct inverse result if no context is provided', function () { + var options = {}, + context = [], + _this = 'the inverse data', + rendered; + + options.fn = function () {}; + options.inverse = inverse; + options.hash = { + columns: 0 + }; + options.data = {}; + + rendered = helpers.foreach.call(_this, context, options); + rendered.should.equal(_this); + }); +}); diff --git a/core/test/unit/server_helpers/ghost_foot_spec.js b/core/test/unit/server_helpers/ghost_foot_spec.js new file mode 100644 index 0000000000..89949c7761 --- /dev/null +++ b/core/test/unit/server_helpers/ghost_foot_spec.js @@ -0,0 +1,48 @@ +/*globals describe, before, afterEach, it*/ +/*jshint expr:true*/ +var should = require('should'), + rewire = require('rewire'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = rewire('../../../server/helpers'); + +describe('{{ghost_foot}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + afterEach(function () { + utils.restoreConfig(); + helpers.__set__('utils.isProduction', false); + }); + + it('has loaded ghost_foot helper', function () { + should.exist(handlebars.helpers.ghost_foot); + }); + + it('outputs correct jquery for development mode', function (done) { + utils.overrideConfig({assetHash: 'abc'}); + + helpers.ghost_foot.call().then(function (rendered) { + should.exist(rendered); + rendered.string.should.match(/' + + '' + + '' + ); + }); + + it('outputs correct scripts for production mode', function () { + helpers.__set__('utils.isProduction', true); + + rendered = helpers.ghost_script_tags(); + should.exist(rendered); + String(rendered).should.equal( + '' + + '' + ); + }); + + describe('with /blog subdirectory', function () { + before(function () { + utils.overrideConfig({url: 'http://testurl.com/blog'}); + }); + + it('outputs correct scripts for development mode', function () { + helpers.__set__('utils.isProduction', false); + rendered = helpers.ghost_script_tags(); + should.exist(rendered); + String(rendered).should.equal( + '' + + '' + + '' + ); + }); + + it('outputs correct scripts for production mode', function () { + helpers.__set__('utils.isProduction', true); + + rendered = helpers.ghost_script_tags(); + should.exist(rendered); + String(rendered).should.equal( + '' + + '' + ); + }); + }); +}); diff --git a/core/test/unit/server_helpers/has_spec.js b/core/test/unit/server_helpers/has_spec.js new file mode 100644 index 0000000000..64a17fd9ed --- /dev/null +++ b/core/test/unit/server_helpers/has_spec.js @@ -0,0 +1,163 @@ +/*globals describe, before, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'); + +describe('{{#has}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded has block helper', function () { + should.exist(handlebars.helpers.has); + }); + + it('should handle tag list that validates true', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {tag: 'invalid, bar, wat'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle tags with case-insensitivity', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {tags: [{name: 'ghost'}]}, + {hash: {tag: 'GhoSt'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle tag list that validates false', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {tag: 'much, such, wow'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.false; + inverse.called.should.be.true; + }); + + it('should not do anything if there are no attributes', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {fn: fn, inverse: inverse} + ); + + fn.called.should.be.false; + inverse.called.should.be.false; + }); + + it('should not do anything when an invalid attribute is given', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {invalid: 'nonsense'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.false; + inverse.called.should.be.false; + }); + + it('should handle author list that evaluates to true', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'sam'}}, + {hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle author list that evaluates to false', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'jamie'}}, + {hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.false; + inverse.called.should.be.true; + }); + + it('should handle authors with case-insensitivity', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'Sam'}}, + {hash: {author: 'joe, sAm, pat'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle tags and authors like an OR query (pass)', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'sam'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle tags and authors like an OR query (pass)', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'sam'}, tags: [{name: 'much'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.true; + inverse.called.should.be.false; + }); + + it('should handle tags and authors like an OR query (fail)', function () { + var fn = sinon.spy(), + inverse = sinon.spy(); + + helpers.has.call( + {author: {name: 'fred'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, + {hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse} + ); + + fn.called.should.be.false; + inverse.called.should.be.true; + }); +}); diff --git a/core/test/unit/server_helpers/is_spec.js b/core/test/unit/server_helpers/is_spec.js new file mode 100644 index 0000000000..98f19e487e --- /dev/null +++ b/core/test/unit/server_helpers/is_spec.js @@ -0,0 +1,63 @@ +/*globals describe, before, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'); + +describe('{{#is}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + 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; + }); +}); diff --git a/core/test/unit/server_helpers/meta_description_spec.js b/core/test/unit/server_helpers/meta_description_spec.js new file mode 100644 index 0000000000..9fc7a76c13 --- /dev/null +++ b/core/test/unit/server_helpers/meta_description_spec.js @@ -0,0 +1,106 @@ +/*globals describe, before, after, 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('{{meta_description}} helper', function () { + before(function () { + utils.loadHelpers(); + utils.overrideConfig({ + theme: { + description: 'Just a blogging platform.' + } + }); + }); + + after(function () { + utils.restoreConfig(); + }); + + it('has loaded meta_description helper', function () { + should.exist(handlebars.helpers.meta_description); + }); + + it('returns correct blog description', function (done) { + helpers.meta_description.call({relativeUrl: '/'}).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Just a blogging platform.'); + + done(); + }).catch(done); + }); + + it('returns empty description on paginated page', function (done) { + helpers.meta_description.call({relativeUrl: '/page/2/'}).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal(''); + + done(); + }).catch(done); + }); + + it('returns empty description for a tag page', function (done) { + var tag = {relativeUrl: '/tag/rasper-red', tag: {name: 'Rasper Red'}}; + helpers.meta_description.call(tag).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal(''); + + done(); + }).catch(done); + }); + + it('returns empty description for a paginated tag page', function (done) { + var tag = {relativeUrl: '/tag/rasper-red/page/2/', tag: {name: 'Rasper Red'}}; + helpers.meta_description.call(tag).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal(''); + + done(); + }).catch(done); + }); + + it('returns correct description for an author page', function (done) { + var author = {relativeUrl: '/author/donald', author: {bio: 'I am a Duck.'}}; + helpers.meta_description.call(author).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('I am a Duck.'); + + done(); + }).catch(done); + }); + + it('returns empty description for a paginated author page', function (done) { + var author = {relativeUrl: '/author/donald/page/2/', author: {name: 'Donald Duck'}}; + helpers.meta_description.call(author).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal(''); + + done(); + }).catch(done); + }); + + it('returns empty description when meta_description is not set', function (done) { + var post = {relativeUrl: '/nice-post', post: {title: 'Post Title', html: 'Very nice post indeed.'}}; + helpers.meta_description.call(post).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal(''); + + done(); + }).catch(done); + }); + + it('returns meta_description on post with meta_description set', function (done) { + var post = {relativeUrl: '/nice-post', post: {title: 'Post Title', meta_description: 'Nice post about stuff.'}}; + helpers.meta_description.call(post).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Nice post about stuff.'); + + done(); + }).catch(done); + }); +}); diff --git a/core/test/unit/server_helpers/meta_title_spec.js b/core/test/unit/server_helpers/meta_title_spec.js new file mode 100644 index 0000000000..843827e83c --- /dev/null +++ b/core/test/unit/server_helpers/meta_title_spec.js @@ -0,0 +1,116 @@ +/*globals describe, before, after, 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('{{meta_title}} helper', function () { + before(function () { + utils.loadHelpers(); + utils.overrideConfig({ + theme: { + title: 'Ghost' + } + }); + }); + + after(function () { + utils.restoreConfig(); + }); + + it('has loaded meta_title helper', function () { + should.exist(handlebars.helpers.meta_title); + }); + + it('returns correct title for homepage', function (done) { + helpers.meta_title.call({relativeUrl: '/'}).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Ghost'); + + done(); + }).catch(done); + }); + + it('returns correct title for paginated page', function (done) { + helpers.meta_title.call({relativeUrl: '/page/2/'}).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Ghost - Page 2'); + + done(); + }).catch(done); + }); + + it('returns correct title for a post', function (done) { + var post = {relativeUrl: '/nice-post', post: {title: 'Post Title'}}; + helpers.meta_title.call(post).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Post Title'); + + done(); + }).catch(done); + }); + + it('returns correct title for a post with meta_title set', function (done) { + var post = {relativeUrl: '/nice-post', post: {title: 'Post Title', meta_title: 'Awesome Post'}}; + helpers.meta_title.call(post).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Awesome Post'); + + done(); + }).catch(done); + }); + + it('returns correct title for a tag page', function (done) { + var tag = {relativeUrl: '/tag/rasper-red', tag: {name: 'Rasper Red'}}; + helpers.meta_title.call(tag).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Rasper Red - Ghost'); + + done(); + }).catch(done); + }); + + it('returns correct title for a paginated tag page', function (done) { + var tag = {relativeUrl: '/tag/rasper-red/page/2/', tag: {name: 'Rasper Red'}}; + helpers.meta_title.call(tag).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Rasper Red - Page 2 - Ghost'); + + done(); + }).catch(done); + }); + + it('returns correct title for an author page', function (done) { + var author = {relativeUrl: '/author/donald', author: {name: 'Donald Duck'}}; + helpers.meta_title.call(author).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Donald Duck - Ghost'); + + done(); + }).catch(done); + }); + + it('returns correct title for a paginated author page', function (done) { + var author = {relativeUrl: '/author/donald/page/2/', author: {name: 'Donald Duck'}}; + helpers.meta_title.call(author).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Donald Duck - Page 2 - Ghost'); + + done(); + }).catch(done); + }); + + it('returns correctly escaped title of a post', function (done) { + var post = {relativeUrl: '/nice-escaped-post', post: {title: 'Post Title ">'}}; + helpers.meta_title.call(post).then(function (rendered) { + should.exist(rendered); + String(rendered).should.equal('Post Title ">'); + + done(); + }).catch(done); + }); +}); diff --git a/core/test/unit/server_helpers/page_url_spec.js b/core/test/unit/server_helpers/page_url_spec.js new file mode 100644 index 0000000000..80c3f1adf6 --- /dev/null +++ b/core/test/unit/server_helpers/page_url_spec.js @@ -0,0 +1,145 @@ +/*globals describe, before, after, 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('{{page_url}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded page_url helper', function () { + should.exist(handlebars.helpers.page_url); + }); + + it('can return a valid url', function () { + helpers.page_url(1).should.equal('/'); + helpers.page_url(2).should.equal('/page/2/'); + helpers.page_url(50).should.equal('/page/50/'); + }); + + it('can return a valid url for tag pages', function () { + var tagContext = { + tagSlug: 'pumpkin' + }; + helpers.page_url.call(tagContext, 1).should.equal('/tag/pumpkin/'); + helpers.page_url.call(tagContext, 2).should.equal('/tag/pumpkin/page/2/'); + helpers.page_url.call(tagContext, 50).should.equal('/tag/pumpkin/page/50/'); + }); + + it('can return a valid url for author pages', function () { + var authorContext = { + authorSlug: 'pumpkin' + }; + helpers.page_url.call(authorContext, 1).should.equal('/author/pumpkin/'); + helpers.page_url.call(authorContext, 2).should.equal('/author/pumpkin/page/2/'); + helpers.page_url.call(authorContext, 50).should.equal('/author/pumpkin/page/50/'); + }); + + describe('with /blog subdirectory', function () { + before(function () { + utils.overrideConfig({url: 'http://testurl.com/blog'}); + }); + + after(function () { + utils.restoreConfig(); + }); + + it('can return a valid url with subdirectory', function () { + helpers.page_url(1).should.equal('/blog/'); + helpers.page_url(2).should.equal('/blog/page/2/'); + helpers.page_url(50).should.equal('/blog/page/50/'); + }); + + it('can return a valid url for tag pages with subdirectory', function () { + var authorContext = { + authorSlug: 'pumpkin' + }; + helpers.page_url.call(authorContext, 1).should.equal('/blog/author/pumpkin/'); + helpers.page_url.call(authorContext, 2).should.equal('/blog/author/pumpkin/page/2/'); + helpers.page_url.call(authorContext, 50).should.equal('/blog/author/pumpkin/page/50/'); + }); + + it('can return a valid url for tag pages with subdirectory', function () { + var tagContext = { + tagSlug: 'pumpkin' + }; + helpers.page_url.call(tagContext, 1).should.equal('/blog/tag/pumpkin/'); + helpers.page_url.call(tagContext, 2).should.equal('/blog/tag/pumpkin/page/2/'); + helpers.page_url.call(tagContext, 50).should.equal('/blog/tag/pumpkin/page/50/'); + }); + }); +}); + +describe('{{pageUrl}} helper [DEPRECATED]', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded pageUrl helper', function () { + should.exist(handlebars.helpers.pageUrl); + }); + + it('can return a valid url', function () { + helpers.pageUrl(1).should.equal('/'); + helpers.pageUrl(2).should.equal('/page/2/'); + helpers.pageUrl(50).should.equal('/page/50/'); + }); + + it('can return a valid url for author pages', function () { + var authorContext = { + authorSlug: 'pumpkin' + }; + helpers.pageUrl.call(authorContext, 1).should.equal('/author/pumpkin/'); + helpers.pageUrl.call(authorContext, 2).should.equal('/author/pumpkin/page/2/'); + helpers.pageUrl.call(authorContext, 50).should.equal('/author/pumpkin/page/50/'); + }); + + it('can return a valid url for tag pages', function () { + var tagContext = { + tagSlug: 'pumpkin' + }; + helpers.pageUrl.call(tagContext, 1).should.equal('/tag/pumpkin/'); + helpers.pageUrl.call(tagContext, 2).should.equal('/tag/pumpkin/page/2/'); + helpers.pageUrl.call(tagContext, 50).should.equal('/tag/pumpkin/page/50/'); + }); + + describe('with /blog subdirectory', function () { + before(function () { + utils.overrideConfig({url: 'http://testurl.com/blog'}); + }); + + after(function () { + utils.restoreConfig(); + }); + + it('can return a valid url with subdirectory', function () { + helpers.pageUrl(1).should.equal('/blog/'); + helpers.pageUrl(2).should.equal('/blog/page/2/'); + helpers.pageUrl(50).should.equal('/blog/page/50/'); + }); + + it('can return a valid url for tag pages with subdirectory', function () { + var tagContext = { + tagSlug: 'pumpkin' + }; + helpers.pageUrl.call(tagContext, 1).should.equal('/blog/tag/pumpkin/'); + helpers.pageUrl.call(tagContext, 2).should.equal('/blog/tag/pumpkin/page/2/'); + helpers.pageUrl.call(tagContext, 50).should.equal('/blog/tag/pumpkin/page/50/'); + }); + + it('can return a valid url for tag pages with subdirectory', function () { + var tagContext = { + tagSlug: 'pumpkin' + }; + helpers.pageUrl.call(tagContext, 1).should.equal('/blog/tag/pumpkin/'); + helpers.pageUrl.call(tagContext, 2).should.equal('/blog/tag/pumpkin/page/2/'); + helpers.pageUrl.call(tagContext, 50).should.equal('/blog/tag/pumpkin/page/50/'); + }); + }); +}); diff --git a/core/test/unit/server_helpers/pagination_spec.js b/core/test/unit/server_helpers/pagination_spec.js new file mode 100644 index 0000000000..107c5abf70 --- /dev/null +++ b/core/test/unit/server_helpers/pagination_spec.js @@ -0,0 +1,123 @@ +/*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('{{pagination}} helper', function () { + before(function (done) { + utils.loadHelpers(); + hbs.express3({partialsDir: [utils.config.paths.helperTemplates]}); + hbs.cachePartials(function () { + done(); + }); + }); + + var paginationRegex = /class="pagination"/, + newerRegex = /class="newer-posts"/, + olderRegex = /class="older-posts"/, + pageRegex = /class="page-number"/; + + it('has loaded pagination helper', function () { + should.exist(handlebars.helpers.pagination); + }); + + it('should throw if pagination data is incorrect', function () { + var runHelper = function (data) { + return function () { + helpers.pagination.call(data); + }; + }; + + runHelper('not an object').should.throwError('pagination data is not an object or is a function'); + runHelper(function () {}).should.throwError('pagination data is not an object or is a function'); + }); + + it('can render single page with no pagination necessary', function () { + var rendered = helpers.pagination.call({ + pagination: {page: 1, prev: null, next: null, limit: 15, total: 8, pages: 1}, + tag: {slug: 'slug'} + }); + should.exist(rendered); + // strip out carriage returns and compare. + rendered.string.should.match(paginationRegex); + rendered.string.should.match(pageRegex); + rendered.string.should.match(/Page 1 of 1/); + rendered.string.should.not.match(newerRegex); + rendered.string.should.not.match(olderRegex); + }); + + it('can render first page of many with older posts link', function () { + var rendered = helpers.pagination.call({ + pagination: {page: 1, prev: null, next: 2, limit: 15, total: 8, pages: 3} + }); + should.exist(rendered); + + rendered.string.should.match(paginationRegex); + rendered.string.should.match(pageRegex); + rendered.string.should.match(olderRegex); + rendered.string.should.match(/Page 1 of 3/); + rendered.string.should.not.match(newerRegex); + }); + + it('can render middle pages of many with older and newer posts link', function () { + var rendered = helpers.pagination.call({ + pagination: {page: 2, prev: 1, next: 3, limit: 15, total: 8, pages: 3} + }); + should.exist(rendered); + + rendered.string.should.match(paginationRegex); + rendered.string.should.match(pageRegex); + rendered.string.should.match(olderRegex); + rendered.string.should.match(newerRegex); + rendered.string.should.match(/Page 2 of 3/); + }); + + it('can render last page of many with newer posts link', function () { + var rendered = helpers.pagination.call({ + pagination: {page: 3, prev: 2, next: null, limit: 15, total: 8, pages: 3} + }); + should.exist(rendered); + + rendered.string.should.match(paginationRegex); + rendered.string.should.match(pageRegex); + rendered.string.should.match(newerRegex); + rendered.string.should.match(/Page 3 of 3/); + rendered.string.should.not.match(olderRegex); + }); + + it('validates values', function () { + var runErrorTest = function (data) { + return function () { + helpers.pagination.call(data); + }; + }; + + runErrorTest({pagination: {page: 3, prev: true, next: null, limit: 15, total: 8, pages: 3}}) + .should.throwError('Invalid value, Next/Prev must be a number'); + runErrorTest({pagination: {page: 3, prev: 2, next: true, limit: 15, total: 8, pages: 3}}) + .should.throwError('Invalid value, Next/Prev must be a number'); + + runErrorTest({pagination: {limit: 15, total: 8, pages: 3}}) + .should.throwError('All values must be defined for page, pages, limit and total'); + runErrorTest({pagination: {page: 3, total: 8, pages: 3}}) + .should.throwError('All values must be defined for page, pages, limit and total'); + runErrorTest({pagination: {page: 3, limit: 15, pages: 3}}) + .should.throwError('All values must be defined for page, pages, limit and total'); + runErrorTest({pagination: {page: 3, limit: 15, total: 8}}) + .should.throwError('All values must be defined for page, pages, limit and total'); + + runErrorTest({pagination: {page: null, prev: null, next: null, limit: 15, total: 8, pages: 3}}) + .should.throwError('Invalid value, check page, pages, limit and total are numbers'); + runErrorTest({pagination: {page: 1, prev: null, next: null, limit: null, total: 8, pages: 3}}) + .should.throwError('Invalid value, check page, pages, limit and total are numbers'); + runErrorTest({pagination: {page: 1, prev: null, next: null, limit: 15, total: null, pages: 3}}) + .should.throwError('Invalid value, check page, pages, limit and total are numbers'); + runErrorTest({pagination: {page: 1, prev: null, next: null, limit: 15, total: 8, pages: null}}) + .should.throwError('Invalid value, check page, pages, limit and total are numbers'); + }); +}); diff --git a/core/test/unit/server_helpers/plural_spec.js b/core/test/unit/server_helpers/plural_spec.js new file mode 100644 index 0000000000..2da93b151c --- /dev/null +++ b/core/test/unit/server_helpers/plural_spec.js @@ -0,0 +1,61 @@ +/*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('{{plural}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded plural helper', function () { + should.exist(handlebars.helpers.plural); + }); + + it('will show no-value string', function () { + var expected = 'No Posts', + rendered = helpers.plural.call({}, 0, { + hash: { + empty: 'No Posts', + singular: '% Post', + plural: '% Posts' + } + }); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); + + it('will show singular string', function () { + var expected = '1 Post', + rendered = helpers.plural.call({}, 1, { + hash: { + empty: 'No Posts', + singular: '% Post', + plural: '% Posts' + } + }); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); + + it('will show plural string', function () { + var expected = '2 Posts', + rendered = helpers.plural.call({}, 2, { + hash: { + empty: 'No Posts', + singular: '% Post', + plural: '% Posts' + } + }); + + should.exist(rendered); + rendered.string.should.equal(expected); + }); +}); diff --git a/core/test/unit/server_helpers/post_class_spec.js b/core/test/unit/server_helpers/post_class_spec.js new file mode 100644 index 0000000000..9a1ce22689 --- /dev/null +++ b/core/test/unit/server_helpers/post_class_spec.js @@ -0,0 +1,49 @@ +/*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('{{post_class}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded postclass helper', function () { + should.exist(handlebars.helpers.post_class); + }); + + it('can render class string', function (done) { + helpers.post_class.call({}).then(function (rendered) { + should.exist(rendered); + rendered.string.should.equal('post'); + done(); + }).catch(done); + }); + + it('can render featured class', function (done) { + var post = {featured: true}; + + helpers.post_class.call(post).then(function (rendered) { + should.exist(rendered); + rendered.string.should.equal('post featured'); + + done(); + }).catch(done); + }); + + it('can render page class', function (done) { + var post = {page: true}; + + helpers.post_class.call(post).then(function (rendered) { + should.exist(rendered); + rendered.string.should.equal('post page'); + + done(); + }).catch(done); + }); +}); diff --git a/core/test/unit/server_helpers/tags_spec.js b/core/test/unit/server_helpers/tags_spec.js new file mode 100644 index 0000000000..3cb183fcf4 --- /dev/null +++ b/core/test/unit/server_helpers/tags_spec.js @@ -0,0 +1,112 @@ +/*globals describe, before, it*/ +/*jshint expr:true*/ +var should = require('should'), + hbs = require('express-hbs'), + utils = require('./utils'), + rewire = require('rewire'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = rewire('../../../server/helpers'); + +describe('{{tags}} helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded tags helper', function () { + should.exist(handlebars.helpers.tags); + }); + + it('can return string with tags', function () { + var tags = [{name: 'foo'}, {name: 'bar'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {autolink: 'false'}} + ); + should.exist(rendered); + + String(rendered).should.equal('foo, bar'); + }); + + it('can use a different separator', function () { + var tags = [{name: 'haunted'}, {name: 'ghost'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {separator: '|', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal('haunted|ghost'); + }); + + it('can add a single prefix to multiple tags', function () { + var tags = [{name: 'haunted'}, {name: 'ghost'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {prefix: 'on ', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal('on haunted, ghost'); + }); + + it('can add a single suffix to multiple tags', function () { + var tags = [{name: 'haunted'}, {name: 'ghost'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {suffix: ' forever', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal('haunted, ghost forever'); + }); + + it('can add a prefix and suffix to multiple tags', function () { + var tags = [{name: 'haunted'}, {name: 'ghost'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {suffix: ' forever', prefix: 'on ', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal('on haunted, ghost forever'); + }); + + it('can add a prefix and suffix with HTML', function () { + var tags = [{name: 'haunted'}, {name: 'ghost'}], + rendered = helpers.tags.call( + {tags: tags}, + {hash: {suffix: ' •', prefix: '… ', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal('… haunted, ghost •'); + }); + + it('does not add prefix or suffix if no tags exist', function () { + var rendered = helpers.tags.call( + {}, + {hash: {prefix: 'on ', suffix: ' forever', autolink: 'false'}} + ); + + should.exist(rendered); + + String(rendered).should.equal(''); + }); + + it('can autolink tags to tag pages', function () { + var tags = [{name: 'foo', slug: 'foo-bar'}, {name: 'bar', slug: 'bar'}], + rendered = helpers.tags.call( + {tags: tags} + ); + should.exist(rendered); + + String(rendered).should.equal('foo, bar'); + }); +}); diff --git a/core/test/unit/server_helpers/title_spec.js b/core/test/unit/server_helpers/title_spec.js new file mode 100644 index 0000000000..5ddeaa2712 --- /dev/null +++ b/core/test/unit/server_helpers/title_spec.js @@ -0,0 +1,45 @@ +/*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('{{title}} Helper', function () { + before(function () { + utils.loadHelpers(); + }); + + it('has loaded title helper', function () { + should.exist(handlebars.helpers.title); + }); + + it('can render title', function () { + var title = 'Hello World', + rendered = helpers.title.call({title: title}); + + should.exist(rendered); + rendered.string.should.equal(title); + }); + + it('escapes correctly', function () { + var rendered = helpers.title.call({title: 'Hello World! It\'s me!
', - rendered = ( - helpers.content - .call( - {html: html}, - {hash: {words: 2}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal('Hello World
'); - }); - - it('can truncate html to 0 words', function () { - var html = 'Hello World! It\'s me!
', - rendered = ( - helpers.content - .call( - {html: html}, - {hash: {words: '0'}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal(''); - }); - - it('can truncate html to 0 words, leaving image tag if it is first', function () { - var html = 'Hello World! It\'s me!
Hello World! It\'s me!
' +
- 'Hello World! It\'s me!
Hi
BliBlob
Hello World! It\'s me!
', - rendered = ( - helpers.content - .call( - {html: html}, - {hash: {characters: 8}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal('Hello Wo
'); - }); - }); - - describe('Title Helper', function () { - it('has loaded title helper', function () { - should.exist(handlebars.helpers.title); - }); - - it('can render title', function () { - var title = 'Hello World', - rendered = helpers.title.call({title: title}); - - should.exist(rendered); - rendered.string.should.equal(title); - }); - - it('escapes correctly', function () { - var rendered = helpers.title.call({title: 'There are
10
types
of people in the world:' +
- '
those who
' +
- 'understand trinary
Hello World! It\'s me!
', - expected = 'Hello World', - rendered = ( - helpers.excerpt.call( - {html: html}, - {hash: {words: '2'}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal(expected); - }); - - it('can truncate html with non-ascii characters by word', function () { - var html = 'Едквюэ опортэат праэчынт ючю но, квуй эю
', - expected = 'Едквюэ опортэат', - rendered = ( - helpers.excerpt.call( - {html: html}, - {hash: {words: '2'}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal(expected); - }); - - it('can truncate html by character', function () { - var html = 'Hello World! It\'s me!
', - expected = 'Hello Wo', - rendered = ( - helpers.excerpt.call( - {html: html}, - {hash: {characters: '8'}} - ) - ); - - should.exist(rendered); - rendered.string.should.equal(expected); - }); - }); - - describe('body_class Helper', function () { - it('has loaded body_class helper', function () { - should.exist(handlebars.helpers.body_class); - }); - - it('can render class string', function (done) { - helpers.body_class.call({}).then(function (rendered) { - should.exist(rendered); - - rendered.string.should.equal('home-template'); - - done(); - }).catch(done); - }); - - it('can render class string for context', function (done) { - Promise.all([ - helpers.body_class.call({relativeUrl: '/'}), - helpers.body_class.call({relativeUrl: '/a-post-title', post: {}}), - helpers.body_class.call({relativeUrl: '/page/4'}), - helpers.body_class.call({relativeUrl: '/tag/foo', tag: {slug: 'foo'}}), - helpers.body_class.call({relativeUrl: '/tag/foo/page/2', tag: {slug: 'foo'}}), - helpers.body_class.call({relativeUrl: '/author/bar', author: {slug: 'bar'}}), - helpers.body_class.call({relativeUrl: '/author/bar/page/2', author: {slug: 'bar'}}) - ]).then(function (rendered) { - rendered.length.should.equal(7); - - should.exist(rendered[0]); - should.exist(rendered[1]); - should.exist(rendered[2]); - should.exist(rendered[3]); - should.exist(rendered[4]); - should.exist(rendered[5]); - should.exist(rendered[6]); - - rendered[0].string.should.equal('home-template'); - rendered[1].string.should.equal('post-template'); - rendered[2].string.should.equal('paged archive-template'); - rendered[3].string.should.equal('tag-template tag-foo'); - rendered[4].string.should.equal('tag-template tag-foo paged archive-template'); - rendered[5].string.should.equal('author-template author-bar'); - rendered[6].string.should.equal('author-template author-bar paged archive-template'); - - done(); - }).catch(done); - }); - - it('can render class for static page', function (done) { - helpers.body_class.call({ - relativeUrl: '/', - post: { - page: true - } - }).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('home-template page-template page'); - - done(); - }).catch(done); - }); - - it('can render class for static page with custom template', function (done) { - helpers.body_class.call({ - relativeUrl: '/about', - post: { - page: true, - slug: 'about' - - } - }).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('post-template page-template page page-about page-template-about'); - - done(); - }).catch(done); - }); - }); - - describe('post_class Helper', function () { - it('has loaded postclass helper', function () { - should.exist(handlebars.helpers.post_class); - }); - - it('can render class string', function (done) { - helpers.post_class.call({}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('post'); - done(); - }).catch(done); - }); - - it('can render featured class', function (done) { - var post = {featured: true}; - - helpers.post_class.call(post).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('post featured'); - - done(); - }).catch(done); - }); - - it('can render page class', function (done) { - var post = {page: true}; - - helpers.post_class.call(post).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('post page'); - - done(); - }).catch(done); - }); - }); - - describe('ghost_head Helper', function () { - // TODO: these tests should be easier to do! - var configUrl = config.url; - - afterEach(function () { - overrideConfig({ - url: configUrl, - theme: { - title: 'Ghost' - } - }); - }); - - it('has loaded ghost_head helper', function () { - should.exist(handlebars.helpers.ghost_head); - }); - - it('returns meta tag string', function (done) { - config.set({url: 'http://testurl.com/'}); - helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' '); - - done(); - }).catch(done); - }); - - it('returns meta tag string even if version is invalid', function (done) { - config.set({url: 'http://testurl.com/'}); - helpers.ghost_head.call({version: '0.9'}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' '); - - done(); - }).catch(done); - }); - - it('returns open graph data on post page', function (done) { - config.set({url: 'http://testurl.com/'}); - var post = { - meta_description: 'blog description', - title: 'Welcome to Ghost', - image: '/test-image.png', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}] - }; - - helpers.ghost_head.call({relativeUrl: '/post/', version: '0.3.0', post: post}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' '); - - done(); - }).catch(done); - }); - - it('returns correct rss url with subdirectory', function (done) { - config.set({url: 'http://testurl.com/blog/'}); - helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' '); - - done(); - }).catch(done); - }); - - it('returns canonical URL', function (done) { - config.set({url: 'http://testurl.com'}); - helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' '); - - done(); - }).catch(done); - }); - - it('returns next & prev URL correctly for middle page', function (done) { - config.set({url: 'http://testurl.com'}); - helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/3/', pagination: {next: '4', prev: '2'}}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' \n' + - ' \n' + - ' '); - done(); - }).catch(done); - }); - - it('returns next & prev URL correctly for second page', function (done) { - config.set({url: 'http://testurl.com'}); - helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/2/', pagination: {next: '3', prev: '1'}}).then(function (rendered) { - should.exist(rendered); - rendered.string.should.equal('\n' + - ' \n' + - ' \n' + - ' \n' + - ' '); - done(); - }).catch(done); - }); - }); - - describe('ghost_foot Helper', function () { - it('has loaded ghost_foot helper', function () { - should.exist(handlebars.helpers.ghost_foot); - }); - - it('outputs correct jquery for development mode', function (done) { - helpers.assetHash = 'abc'; - - helpers.ghost_foot.call().then(function (rendered) { - should.exist(rendered); - rendered.string.should.match(/' + - '' + - '' - ); - - overrideConfig({ - paths: {subdir: '/blog'} - }); - - // with subdirectory - rendered = helpers.ghost_script_tags(); - should.exist(rendered); - String(rendered).should.equal( - '' + - '' + - '' - ); - }); - - it('outputs correct scripts for production mode', function () { - helpers.__set__('isProduction', true); - - rendered = helpers.ghost_script_tags(); - should.exist(rendered); - String(rendered).should.equal( - '' + - '' - ); - - overrideConfig({ - paths: {subdir: '/blog'} - }); - - // with subdirectory - rendered = helpers.ghost_script_tags(); - should.exist(rendered); - String(rendered).should.equal( - '' + - '' - ); - }); - }); - - describe('adminUrl', function () { - var rendered, - configUrl = config.url; - - afterEach(function () { - config.set({url: configUrl}); - }); - - it('should output the path to admin', function () { - rendered = helpers.admin_url(); - should.exist(rendered); - rendered.should.equal('/ghost'); - }); - - it('should output the path to admin with subdirectory', function () { - config.set({url: 'http://testurl.com/blog/'}); - rendered = helpers.admin_url(); - should.exist(rendered); - rendered.should.equal('/blog/ghost'); - }); - - it('should output absolute path if absolute is set', function () { - // no trailing slash - config.set({url: 'http://testurl.com'}); - - rendered = helpers.admin_url({hash: {absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/ghost'); - - // test trailing slash - config.set({url: 'http://testurl.com/'}); - rendered = helpers.admin_url({hash: {absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/ghost'); - }); - - it('should output absolute path with subdirectory', function () { - config.set({url: 'http://testurl.com/blog'}); - rendered = helpers.admin_url({hash: {absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/blog/ghost'); - }); - - it('should output the path to frontend if frontend is set', function () { - rendered = helpers.admin_url({hash: {frontend: true}}); - should.exist(rendered); - rendered.should.equal('/'); - }); - - it('should output the absolute path to frontend if both are set', function () { - config.set({url: 'http://testurl.com'}); - - rendered = helpers.admin_url({hash: {frontend: true, absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/'); - - config.set({url: 'http://testurl.com/'}); - rendered = helpers.admin_url({hash: {frontend: true, absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/'); - }); - - it('should output the path to frontend with subdirectory', function () { - config.set({url: 'http://testurl.com/blog/'}); - rendered = helpers.admin_url({hash: {frontend: true}}); - should.exist(rendered); - rendered.should.equal('/blog/'); - }); - - it('should output the absolute path to frontend with subdirectory', function () { - config.set({url: 'http://testurl.com/blog/'}); - rendered = helpers.admin_url({hash: {frontend: true, absolute: true}}); - should.exist(rendered); - rendered.should.equal('http://testurl.com/blog/'); - }); - }); - describe('file storage helper', function () { it('is loaded', function () { should.exist(helpers.file_storage);