Refactor helpers & tests into individual files

no issue

- Split theme helpers into individual files for each
- Do the same for tests
- Have utils to share some things between them
- Move assetHash onto config
This commit is contained in:
Hannah Wolfe 2014-10-10 15:54:07 +01:00
parent 3e49fb3034
commit 2c6d43a0c0
51 changed files with 3238 additions and 2757 deletions

View File

@ -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']
},

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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('<link rel="canonical" href="' + url + '" />');
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('<link rel="prev" href="' + config.urlFor({relativeUrl: prev}, true) + '" />');
}
if (self.pagination.next) {
next = '/page/' + self.pagination.next + '/';
next = (trimmedUrl) ? '/' + trimmedUrl + next : next;
head.push('<link rel="next" href="' + config.urlFor({relativeUrl: next}, true) + '" />');
}
}
// 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('<meta property="' + property + '" content="' + content + '" />');
});
// 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('<meta property="article:tag" content="' + tag.trim() + '" />');
}
});
}
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="' +
_.escape(blog.title) + '" href="' + config.urlFor('rss') + '" />');
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;

View File

@ -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;

View File

@ -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;

View File

@ -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('<a href="<%= url %>"><%= text %></a>'),
scriptTemplate = _.template('<script src="<%= source %>?v=<%= version %>"></script>'),
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('<link rel="canonical" href="' + url + '" />');
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('<link rel="prev" href="' + config.urlFor({relativeUrl: prev}, true) + '" />');
}
if (self.pagination.next) {
next = '/page/' + self.pagination.next + '/';
next = (trimmedUrl) ? '/' + trimmedUrl + next : next;
head.push('<link rel="next" href="' + config.urlFor({relativeUrl: next}, true) + '" />');
}
}
// 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('<meta property="' + property + '" content="' + content + '" />');
});
// 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('<meta property="article:tag" content="' + tag.trim() + '" />');
}
});
}
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="' +
_.escape(blog.title) + '" href="' + config.urlFor('rss') + '" />');
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;

32
core/server/helpers/is.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,22 @@
var _ = require('lodash'),
utils;
utils = {
assetTemplate: _.template('<%= source %>?v=<%= version %>'),
linkTemplate: _.template('<a href="<%= url %>"><%= text %></a>'),
scriptTemplate: _.template('<script src="<%= source %>?v=<%= version %>"></script>'),
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;

View File

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

View File

@ -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');
});
});
});

View File

@ -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('<a href="/author/abc123/">abc 123</a>');
});
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');
});
});

View File

@ -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);
});
});

View File

@ -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 = '<p>Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: 2}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p>Hello <strong>World</strong></p>');
});
it('can truncate html to 0 words', function () {
var html = '<p>Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p></p>');
});
it('can truncate html to 0 words, leaving image tag if it is first', function () {
var html = '<p><img src="example.jpg" />Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="example.jpg" /></p>');
});
it('can truncate html to 0 words, leaving image tag with attributes', function () {
var html = '<p><img src="example.png" alt="Alternative" title="Title"></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="example.png" alt="Alternative" title="Title"></p>');
});
it('can truncate html to 0 words, leaving first image tag & if alt text has a single quote', function () {
var html = '<p><img src="example.jpg" alt="It\'s me!" />Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="example.jpg" alt="It\'s me!" /></p>');
});
it('can truncate html to 0 words, leaving first image tag & if alt text has a double quote', function () {
var html = '<p><img src="example.jpg" alt="A double quote is \'" />' +
'Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="example.jpg" alt="A double quote is \'" /></p>');
});
it('can truncate html to 0 words, leaving first image tag if it contains > & <', function () {
var html = '<p><img src="examp>><><>le.png"></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="examp>><><>le.png"></p>');
});
it('can truncate html to 0 words, leaving first two image tags', function () {
var html = '<p><img src="example.png"><img src="example.png">Hi<img src="example.png"></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><img src="example.png"><img src="example.png"></p>');
});
it('can truncate html to 0 words, removing image if text comes first', function () {
var html = '<p><a>Bli<a><a><img src="example.png"></a></a>Blob</a></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><a></a></p>');
});
it('can truncate html to 0 words, leaving video tag', function () {
var html = '<p><video><source src="movie.mp4"><source src="movie.ogg"></video></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {words: '0'}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p><video><source src="movie.mp4"><source src="movie.ogg"></video></p>');
});
it('can truncate html by character', function () {
var html = '<p>Hello <strong>World! It\'s me!</strong></p>',
rendered = (
helpers.content
.call(
{html: html},
{hash: {characters: 8}}
)
);
should.exist(rendered);
rendered.string.should.equal('<p>Hello <strong>Wo</strong></p>');
});
});

View File

@ -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());
});
});
});

View File

@ -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);
});
});

View File

@ -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 = '<p>There are <br />10<br> types<br/> of people in <img src="a">the world:' +
'<img src=b alt="c"> those who <img src="@" onclick="javascript:alert(\'hello\');">' +
'understand trinary</p>, those who don\'t <div style="" class=~/\'-,._?!|#>and' +
'< test > those<<< test >>> who mistake it &lt;for&gt; binary.',
expected = 'There are 10 types of people in the world: those who understand trinary, those who ' +
'don\'t and those>> who mistake it &lt;for&gt; binary.',
rendered = helpers.excerpt.call({html: html});
should.exist(rendered);
rendered.string.should.equal(expected);
});
it('can truncate html by word', function () {
var html = '<p>Hello <strong>World! It\'s me!</strong></p>',
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 = '<p>Едквюэ опортэат <strong>праэчынт ючю но, квуй эю</strong></p>',
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 = '<p>Hello <strong>World! It\'s me!</strong></p>',
expected = 'Hello Wo',
rendered = (
helpers.excerpt.call(
{html: html},
{hash: {characters: '8'}}
)
);
should.exist(rendered);
rendered.string.should.equal(expected);
});
});

View File

@ -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);
});
});

View File

@ -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(/<script src=".*\/public\/jquery.js\?v=abc"><\/script>/);
done();
}).catch(done);
});
it('outputs correct jquery for production mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
helpers.__set__('utils.isProduction', true);
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.min.js\?v=abc"><\/script>/);
done();
}).catch(done);
});
});

View File

@ -0,0 +1,144 @@
/*globals describe, before, after, it*/
/*jshint expr:true*/
var should = require('should'),
hbs = require('express-hbs'),
utils = require('./utils'),
moment = require('moment'),
// Stuff we are testing
handlebars = hbs.handlebars,
helpers = require('../../../server/helpers');
describe('{{ghost_head}} helper', function () {
before(function () {
utils.loadHelpers();
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost'
}
});
});
after(function () {
utils.restoreConfig();
});
it('has loaded ghost_head helper', function () {
should.exist(handlebars.helpers.ghost_head);
});
it('returns meta tag string', function (done) {
helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns meta tag string even if version is invalid', function (done) {
helpers.ghost_head.call({version: '0.9'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.9" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns open graph data on post page', function (done) {
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('<link rel="canonical" href="http://testurl.com/post/" />\n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost" />\n' +
' <meta property="og:description" content="blog description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="og:image" content="http://testurl.com/test-image.png" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns canonical URL', function (done) {
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/about/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for middle page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/3/" />\n' +
' <link rel="prev" href="http://testurl.com/page/2/" />\n' +
' <link rel="next" href="http://testurl.com/page/4/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for second page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/2/" />\n' +
' <link rel="prev" href="http://testurl.com/" />\n' +
' <link rel="next" href="http://testurl.com/page/3/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
describe('with /blog subdirectory', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/blog/',
theme: {
title: 'Ghost'
}
});
});
after(function () {
utils.restoreConfig();
});
it('returns correct rss url with subdirectory', function (done) {
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/blog/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/blog/rss/" />');
done();
}).catch(done);
});
});
});

View File

@ -0,0 +1,79 @@
/*globals describe, before, after, afterEach, it*/
/*jshint expr:true*/
var should = require('should'),
rewire = require('rewire'),
utils = require('./utils'),
// Stuff we are testing
helpers = rewire('../../../server/helpers');
// ## Admin only helpers
describe('ghost_script_tags helper', function () {
var rendered;
before(function () {
utils.loadHelpers();
utils.overrideConfig({assetHash: 'abc'});
});
after(function () {
utils.restoreConfig();
});
afterEach(function () {
helpers.__set__('utils.isProduction', false);
});
it('has loaded ghostScriptTags helper', function () {
should.exist(helpers.ghost_script_tags);
});
it('outputs correct scripts for development mode', function () {
rendered = helpers.ghost_script_tags();
should.exist(rendered);
String(rendered).should.equal(
'<script src="/ghost/scripts/vendor-dev.js?v=abc"></script>' +
'<script src="/ghost/scripts/templates-dev.js?v=abc"></script>' +
'<script src="/ghost/scripts/ghost-dev.js?v=abc"></script>'
);
});
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(
'<script src="/ghost/scripts/vendor.min.js?v=abc"></script>' +
'<script src="/ghost/scripts/ghost.min.js?v=abc"></script>'
);
});
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(
'<script src="/blog/ghost/scripts/vendor-dev.js?v=abc"></script>' +
'<script src="/blog/ghost/scripts/templates-dev.js?v=abc"></script>' +
'<script src="/blog/ghost/scripts/ghost-dev.js?v=abc"></script>'
);
});
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(
'<script src="/blog/ghost/scripts/vendor.min.js?v=abc"></script>' +
'<script src="/blog/ghost/scripts/ghost.min.js?v=abc"></script>'
);
});
});
});

View File

@ -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;
});
});

View File

@ -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;
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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/');
});
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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: ' &bull;', prefix: '&hellip; ', autolink: 'false'}}
);
should.exist(rendered);
String(rendered).should.equal('&hellip; haunted, ghost &bull;');
});
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('<a href="/tag/foo-bar/">foo</a>, <a href="/tag/bar/">bar</a>');
});
});

View File

@ -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: '<h1>I am a title</h1>'});
rendered.string.should.equal('&lt;h1&gt;I am a title&lt;/h1&gt;');
});
it('returns a blank string where title is missing', function () {
var rendered = helpers.title.call({title: null});
rendered.string.should.equal('');
});
it('returns a blank string where data missing', function () {
var rendered = helpers.title.call({});
rendered.string.should.equal('');
});
});

View File

@ -0,0 +1,98 @@
/*globals describe, before, beforeEach, afterEach, 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('{{url}} helper', function () {
var sandbox;
before(function () {
sandbox = sinon.sandbox.create();
utils.overrideConfig({url: 'http://testurl.com/'});
utils.loadHelpers();
});
beforeEach(function () {
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({settings: [{value: '/:slug/'}]});
});
});
afterEach(function () {
sandbox.restore();
});
after(function () {
utils.restoreConfig();
});
it('has loaded url helper', function () {
should.exist(handlebars.helpers.url);
});
it('should return the slug with a prefix slash if the context is a post', function (done) {
helpers.url.call({
html: 'content',
markdown: 'ff',
title: 'title',
slug: 'slug',
created_at: new Date(0)
}).then(function (rendered) {
should.exist(rendered);
rendered.should.equal('/slug/');
done();
}).catch(done);
});
it('should output an absolute URL if the option is present', function (done) {
helpers.url.call(
{html: 'content', markdown: 'ff', title: 'title', slug: 'slug', created_at: new Date(0)},
{hash: {absolute: 'true'}}
).then(function (rendered) {
should.exist(rendered);
rendered.should.equal('http://testurl.com/slug/');
done();
}).catch(done);
});
it('should return the slug with a prefixed /tag/ if the context is a tag', function (done) {
helpers.url.call({
name: 'the tag',
slug: 'the-tag',
description: null,
parent: null
}).then(function (rendered) {
should.exist(rendered);
rendered.should.equal('/tag/the-tag/');
done();
}).catch(done);
});
it('should return / if not a post or tag', function (done) {
helpers.url.call({markdown: 'ff', title: 'title', slug: 'slug'}).then(function (rendered) {
rendered.should.equal('/');
}).then(function () {
return helpers.url.call({html: 'content', title: 'title', slug: 'slug'}).then(function (rendered) {
rendered.should.equal('/');
});
}).then(function () {
return helpers.url.call({html: 'content', markdown: 'ff', slug: 'slug'}).then(function (rendered) {
rendered.should.equal('/');
});
}).then(function () {
helpers.url.call({html: 'content', markdown: 'ff', title: 'title'}).then(function (rendered) {
rendered.should.equal('/');
done();
});
}).catch(done);
});
});

View File

@ -0,0 +1,32 @@
// # Helper Test Utils
//
// Contains shared code for intialising tests
//
// @TODO refactor this file out of existence
// I believe if we refactor the handlebars instances and helpers to be more self-contained and modular
// We can likely have init functions which replace the need for this file
var hbs = require('express-hbs'),
_ = require('lodash'),
// Stuff we are testing
helpers = require('../../../server/helpers'),
config = require('../../../server/config'),
origConfig = _.cloneDeep(config.get()),
utils = {};
utils.loadHelpers = function () {
var adminHbs = hbs.create();
helpers.loadCoreHelpers(adminHbs);
};
utils.overrideConfig = function (newConfig) {
config.set(newConfig);
};
utils.restoreConfig = function () {
config.set(origConfig);
};
module.exports = utils;
module.exports.config = config;

File diff suppressed because it is too large Load Diff