var _ = require('underscore'), moment = require('moment'), downsize = require('downsize'), path = require('path'), when = require('when'), hbs = require('express-hbs'), errors = require('../errorHandling'), models = require('../models'), packageInfo = require('../../../package.json'), version = packageInfo.version, scriptTemplate = _.template(""), isProduction = process.env.NODE_ENV === 'production', coreHelpers = {}, registerHelpers; /** * [ description] * @todo ghost core helpers + a way for themes to register them * @param {Object} context date object * @param {*} options * @return {Object} A Moment time / date object */ 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; } } 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:* // `{{pageUrl 2}}` // // Returns the URL for the page specified in the current object // context. // coreHelpers.pageUrl = function (context, block) { /*jslint unparam:true*/ return context === 1 ? '/' : ('/page/' + context + '/'); }; // ### URL helper // // *Usage example:* // `{{url}}` // `{{url absolute}}` // // 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 output = '', self = this, tags = { year: function () { return self.created_at.getFullYear(); }, month: function () { return self.created_at.getMonth() + 1; }, day: function () { return self.created_at.getDate(); }, slug: function () { return self.slug; }, id: function () { return self.id; } }, blog = coreHelpers.ghost.blogGlobals(), isAbsolute = options && options.hash.absolute; if (isAbsolute) { output += blog.url; } if (blog.path && blog.path !== '/') { output += blog.path; } if (models.isPost(this)) { output += coreHelpers.ghost.settings('permalinks'); output = output.replace(/(:[a-z]+)/g, function (match) { if (_.has(tags, match.substr(1))) { return tags[match.substr(1)](); } }); } return 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) { /*jslint unparam:true*/ return this.author ? this.author.name : ''; }; // ### 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) { var separator = _.isString(options.hash.separator) ? options.hash.separator : ', ', prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '', suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '', output = '', tagNames = _.pluck(this.tags, 'name'); if (tagNames.length) { output = prefix + tagNames.join(separator) + suffix; } return 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']); if (truncateOptions.words || truncateOptions.characters) { return new hbs.handlebars.SafeString( downsize(this.html, truncateOptions) ); } return new hbs.handlebars.SafeString(this.html); }; // ### 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']); /*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) ); }; // ### Filestorage helper // // *Usage example:* // `{{fileStorage}}` // // Returns the config value for fileStorage. coreHelpers.fileStorage = function (context, options) { /*jslint unparam:true*/ if (coreHelpers.config().hasOwnProperty('fileStorage')) { return coreHelpers.config().fileStorage.toString(); } return "true"; }; coreHelpers.ghostScriptTags = function () { var scriptFiles = [], blog = coreHelpers.ghost.blogGlobals(); if (isProduction) { scriptFiles.push("ghost.min.js"); } else { scriptFiles = [ 'vendor.js', 'helpers.js', 'templates.js', 'models.js', 'views.js' ]; } scriptFiles = _.map(scriptFiles, function (fileName) { return scriptTemplate({ source: path.join(blog.path, '/built/scripts/', fileName), version: version }); }); return scriptFiles.join(''); }; /* * Asynchronous Theme Helpers (Registered with ghost.registerAsyncThemeHelper) */ coreHelpers.body_class = function (options) { /*jslint unparam:true*/ var classes = [], tags = this.post && this.post.tags ? this.post.tags : this.tags || [], page = this.post && this.post.page ? this.post.page : this.page || false; if (_.isString(this.ghostRoot) && this.ghostRoot.match(/\/page/)) { classes.push('archive-template'); } else if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '') { classes.push('home-template'); } else { classes.push('post-template'); } if (tags) { classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; })); } if (page) { classes.push('page'); } return coreHelpers.ghost.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) { /*jslint unparam:true*/ 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 coreHelpers.ghost.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) { /*jslint unparam:true*/ var blog = coreHelpers.ghost.blogGlobals(), head = [], majorMinor = /^(\d+\.)?(\d+)/, trimmedVersion = this.version; trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?'; head.push(''); head.push(''); if (this.ghostRoot) { head.push(''); } return coreHelpers.ghost.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) { /*jslint unparam:true*/ var foot = []; foot.push(''); return coreHelpers.ghost.doFilter('ghost_foot', foot).then(function (foot) { var footString = _.reduce(foot, function (memo, item) { return memo + ' ' + item; }, ''); return new hbs.handlebars.SafeString(footString.trim()); }); }; coreHelpers.meta_title = function (options) { /*jslint unparam:true*/ var title, blog; if (_.isString(this.ghostRoot)) { if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '' || this.ghostRoot.match(/\/page/)) { blog = coreHelpers.ghost.blogGlobals(); title = blog.title; } else { title = this.post.title; } } return coreHelpers.ghost.doFilter('meta_title', title).then(function (title) { title = title || ""; return new hbs.handlebars.SafeString(title.trim()); }); }; coreHelpers.meta_description = function (options) { /*jslint unparam:true*/ var description, blog; if (_.isString(this.ghostRoot)) { if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '' || this.ghostRoot.match(/\/page/)) { blog = coreHelpers.ghost.blogGlobals(); description = blog.description; } else { description = ''; } } return coreHelpers.ghost.doFilter('meta_description', description).then(function (description) { description = description || ""; return new hbs.handlebars.SafeString(description.trim()); }); }; /** * Localised string helpers * * @param String key * @param String default translation * @param {Object} options * @return String A correctly internationalised string */ coreHelpers.e = function (key, defaultString, options) { var output; if (coreHelpers.ghost.settings('defaultLang') === 'en' && _.isEmpty(options.hash) && !coreHelpers.ghost.settings('forceI18n')) { output = defaultString; } else { output = coreHelpers.ghost.polyglot().t(key, options.hash); } return output; }; coreHelpers.json = function (object, options) { /*jslint unparam:true*/ return JSON.stringify(object); }; 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; }; // ### Check if a tag is contained on the tag list // // Usage example:* // // `{{#has_tag "test"}} {{tags}} {{else}} no tags {{/has_tag}}` // // @param {String} tag name to search on tag list // @return {String} list of tags formatted according to `tag` helper // coreHelpers.has_tag = function (name, options) { if (_.isArray(this.tags) && !_.isEmpty(this.tags)) { return (!_.isEmpty(_.filter(this.tags, function (tag) { return (_.has(tag, "name") && tag.name === name); }))) ? options.fn(this) : options.inverse(this); } return options.inverse(this); }; // ## Template driven helpers // Template driven helpers require that their template is loaded before they can be registered. coreHelpers.paginationTemplate = null; // ### Pagination Helper // `{{pagination}}` // Outputs previous and next buttons, along with info about the current page coreHelpers.pagination = function (options) { /*jslint unparam:true*/ if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { errors.logAndThrowError('pagination data is not an object or is a function'); return; } if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { errors.logAndThrowError('All values must be defined for page, pages, limit and total'); return; } if ((!_.isUndefined(this.pagination.next) && !_.isNumber(this.pagination.next)) || (!_.isUndefined(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { errors.logAndThrowError('Invalid value, Next/Prev must be a number'); return; } if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { errors.logAndThrowError('Invalid value, check page, pages, limit and total are numbers'); return; } return new hbs.handlebars.SafeString(coreHelpers.paginationTemplate(this.pagination)); }; coreHelpers.helperMissing = function (arg) { if (arguments.length === 2) { return undefined; } errors.logError('Missing helper: "' + arg + '"'); }; registerHelpers = function (ghost, config) { var paginationHelper; // Expose this so our helpers can use it in their code. coreHelpers.ghost = ghost; // And expose config coreHelpers.config = config; ghost.registerThemeHelper('date', coreHelpers.date); ghost.registerThemeHelper('encode', coreHelpers.encode); ghost.registerThemeHelper('pageUrl', coreHelpers.pageUrl); ghost.registerThemeHelper('url', coreHelpers.url); ghost.registerThemeHelper('author', coreHelpers.author); ghost.registerThemeHelper('tags', coreHelpers.tags); ghost.registerThemeHelper('content', coreHelpers.content); ghost.registerThemeHelper('excerpt', coreHelpers.excerpt); ghost.registerThemeHelper('fileStorage', coreHelpers.fileStorage); ghost.registerThemeHelper('ghostScriptTags', coreHelpers.ghostScriptTags); ghost.registerThemeHelper('e', coreHelpers.e); ghost.registerThemeHelper('json', coreHelpers.json); ghost.registerThemeHelper('foreach', coreHelpers.foreach); ghost.registerThemeHelper('helperMissing', coreHelpers.helperMissing); ghost.registerThemeHelper('has_tag', coreHelpers.has_tag); ghost.registerAsyncThemeHelper('body_class', coreHelpers.body_class); ghost.registerAsyncThemeHelper('post_class', coreHelpers.post_class); ghost.registerAsyncThemeHelper('meta_title', coreHelpers.meta_title); ghost.registerAsyncThemeHelper('meta_description', coreHelpers.meta_description); ghost.registerAsyncThemeHelper('ghost_head', coreHelpers.ghost_head); ghost.registerAsyncThemeHelper('ghost_foot', coreHelpers.ghost_foot); paginationHelper = ghost.loadTemplate('pagination').then(function (templateFn) { coreHelpers.paginationTemplate = templateFn; ghost.registerThemeHelper('pagination', coreHelpers.pagination); }); // Return once the template-driven helpers have loaded return when.join( paginationHelper ); }; module.exports = coreHelpers; module.exports.loadCoreHelpers = registerHelpers;