diff --git a/.gitignore b/.gitignore
index 0433bbc0..28be7557 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
node_modules
.idea
package-lock.json
+dist
+.DS_Store
diff --git a/blackbird-helpers.js b/blackbird-helpers.js
new file mode 100644
index 00000000..7f68af75
--- /dev/null
+++ b/blackbird-helpers.js
@@ -0,0 +1,57 @@
+// A version of helpers.js that lets webpack figure
+// out what to require (ie doesn't have any dynamic logic mixed in w/ require statements).
+
+// Export full list of helpers
+module.exports = [
+ require('./helpers/all'),
+ require('./helpers/any'),
+ require('./helpers/assignVar'),
+ require('./helpers/block'),
+ require('./helpers/cdn'),
+ require('./helpers/compare'),
+ require('./helpers/concat'),
+ require('./helpers/contains'),
+ require('./helpers/decrementVar'),
+ require('./helpers/dynamicComponent'),
+ require('./helpers/encodeHtmlEntities'),
+ require('./helpers/for'),
+ require('./helpers/getContentImage'),
+ require('./helpers/getContentImageSrcset'),
+ require('./helpers/getFontLoaderConfig'),
+ require('./helpers/getFontsCollection'),
+ require('./helpers/getImage'),
+ require('./helpers/getImageManagerImage'),
+ require('./helpers/getImageManagerImageSrcset'),
+ require('./helpers/getImageSrcset'),
+ require('./helpers/getImageSrcset1x2x'),
+ require('./helpers/getVar'),
+ require('./helpers/helperMissing'),
+ require('./helpers/if'),
+ require('./helpers/incrementVar'),
+ require('./helpers/inject'),
+ require('./helpers/join'),
+ require('./helpers/jsContext'),
+ require('./helpers/json'),
+ require('./helpers/lang'),
+ require('./helpers/langJson'),
+ require('./helpers/limit'),
+ require('./helpers/money'),
+ require('./helpers/nl2br'),
+ require('./helpers/occurrences'),
+ require('./helpers/or'),
+ require('./helpers/partial'),
+ require('./helpers/pluck'),
+ require('./helpers/pre'),
+ require('./helpers/region'),
+ require('./helpers/replace'),
+ require('./helpers/resourceHints'),
+ require('./helpers/setURLQueryParam'),
+ require('./helpers/snippets'),
+ require('./helpers/stripQuerystring'),
+ require('./helpers/strReplace'),
+ require('./helpers/stylesheet'),
+ require('./helpers/thirdParty'),
+ require('./helpers/toLowerCase'),
+ require('./helpers/truncate'),
+ require('./helpers/unless'),
+];
diff --git a/blackbird.js b/blackbird.js
new file mode 100644
index 00000000..ac3bd80d
--- /dev/null
+++ b/blackbird.js
@@ -0,0 +1,420 @@
+'use strict';
+
+// This file is a copy pasta of index.js for now. Mostly the addition of the
+// renderSync method but wanted the ability to freely experiment and switch between
+// the old and new versions.
+
+
+const HandlebarsV3 = require('handlebars');
+const HandlebarsV4 = require('@bigcommerce/handlebars-v4');
+const helpers = require('./blackbird-helpers');
+const AppError = require('./lib/appError');
+
+const Translator = require('./lib/translator');
+
+class CompileError extends AppError { }; // Error compiling template
+class FormatError extends AppError { }; // Error restoring precompiled template
+class RenderError extends AppError { }; // Error rendering template
+class DecoratorError extends AppError { }; // Error applying decorator
+class TemplateNotFoundError extends AppError { }; // Template not registered
+
+const handlebarsOptions = {
+ preventIndent: true
+};
+
+// HandlebarsRenderer implements the interface Paper requires for its
+// rendering needs, and does so with Handlebars.
+class HandlebarsRenderer {
+ // Add static accessor to reference custom errors
+ static get errors() {
+ return {
+ CompileError,
+ FormatError,
+ RenderError,
+ DecoratorError,
+ TemplateNotFoundError,
+ };
+ }
+
+ /**
+ * Constructor
+ *
+ * @param {Object} siteSettings - Global site settings, passed to helpers
+ * @param {Object} themeSettings - Theme settings (configuration), passed to helpers
+ * @param {String} hbVersion - Which version of handlebars to use. One of ['v3', 'v4'] - defaults to 'v3'.
+ * @param {Object} logger - A console-like object to use for logging
+ * @param {String} logLevel - log level which will be overriden by renderer
+ */
+ constructor(siteSettings, themeSettings, hbVersion, logger = console, logLevel = 'info') {
+ // Figure out which version of Handlebars to use.
+ switch (hbVersion) {
+ case 'v4':
+ this.handlebars = HandlebarsV4.create();
+ break;
+ case 'v3':
+ default:
+ this.handlebars = HandlebarsV3.create();
+ break;
+ }
+
+ this.logger = logger;
+ this._setHandlebarsLogger();
+ this.setSiteSettings(siteSettings || {});
+ this.setThemeSettings(themeSettings || {});
+ this.setTranslator(null);
+ this.setContent({});
+ this.resetDecorators();
+ this.setLoggerLevel(logLevel);
+
+ // Build global context for helpers
+ this.helperContext = {
+ handlebars: this.handlebars,
+ getSiteSettings: this.getSiteSettings.bind(this),
+ getThemeSettings: this.getThemeSettings.bind(this),
+ getTranslator: this.getTranslator.bind(this),
+ getContent: this.getContent.bind(this),
+ storage: {}, // global storage used by helpers to keep state
+ };
+
+ // Register helpers with Handlebars
+ for (let i = 0; i < helpers.length; i++) {
+ let specs = helpers[i];
+
+ for (let j = 0; j < specs.length; j++) {
+ let spec = specs[j];
+ // log("registering helper " + JSON.stringify(spec));
+ this.handlebars.registerHelper(spec.name, spec.factory(this.helperContext));
+ }
+ }
+ }
+
+ loadTranslations(acceptLanguage, translations, translator_logger = logger) {
+ let translator = Translator.create(acceptLanguage, translations, translator_logger);
+ this.setTranslator(translator);
+ }
+
+ /**
+ * Set the paper.Translator instance used to translate strings in helpers.
+ *
+ * @param {Translator} translator A paper.Translator instance used to translate strings in helpers
+ */
+ setTranslator(translator) {
+ this._translator = translator;
+ };
+
+ /**
+ * Get the paper.Translator instance used to translate strings in helpers.
+ *
+ * @return {Translator} A paper.Translator instance used to translate strings in helpers
+ */
+ getTranslator() {
+ return this._translator;
+ };
+
+ /**
+ * Set the siteSettings object containing global site settings.
+ *
+ * @param {object} settings An object containing global site settings.
+ */
+ setSiteSettings(settings) {
+ this._siteSettings = settings;
+ };
+
+ /**
+ * Get the siteSettings object containing global site settings.
+ *
+ * @return {object} settings An object containing global site settings.
+ */
+ getSiteSettings() {
+ return this._siteSettings;
+ };
+
+ /**
+ * Set the themeSettings object containing the theme configuration.
+ *
+ * @param {object} settings An object containing the theme configuration.
+ */
+ setThemeSettings(settings) {
+ this._themeSettings = settings;
+ };
+
+ /**
+ * Get the themeSettings object containing the theme configuration.
+ *
+ * @return {object} settings An object containing the theme configuration.
+ */
+ getThemeSettings() {
+ return this._themeSettings;
+ };
+
+ /**
+ * Reset decorator list.
+ */
+ resetDecorators() {
+ this._decorators = [];
+ };
+
+ /**
+ * Add a decorator to be applied at render time.
+ *
+ * @param {Function} decorator
+ */
+ addDecorator(decorator) {
+ this._decorators.push(decorator);
+ };
+
+ /**
+ * Setup content regions to be used by the `region` helper.
+ *
+ * @param {Object} Regions with widgets
+ */
+ setContent(regions) {
+ this._contentRegions = regions;
+ };
+
+ /**
+ * Get content regions.
+ *
+ * @param {Object} Regions with widgets
+ */
+ getContent() {
+ return this._contentRegions;
+ };
+
+ /**
+ * Add templates to the active set of partials. The templates can either be raw
+ * template strings, or the result coming from the preProcessor function.
+ *
+ * @param {Object} A set of templates to register with handlebars
+ */
+ addTemplates(templates) {
+ const paths = Object.keys(templates);
+
+ for (let i = 0; i < paths.length; i++) {
+ const path = paths[i];
+
+ if (typeof this.handlebars.partials[path] !== 'undefined') {
+ continue;
+ }
+
+ try {
+ // Check if it is a precompiled template
+ const template = this._tryRestoringPrecompiled(templates[path]);
+
+ // Register it with handlebars
+ this.handlebars.registerPartial(path, template);
+ } catch (e) {
+ throw new FormatError(e.message);
+ }
+ }
+ };
+
+ _tryRestoringPrecompiled(precompiled) {
+ let template;
+
+ if (typeof precompiled == 'string') {
+ // Let's analyze the string to make sure it at least looks
+ // something like a handlebars precompiled template. It should
+ // be a string representation of an object containing a `main`
+ // function and a `compiler` array. We do this because the next
+ // step is a potentially dangerous eval.
+ const re = /"compiler":\[.*\],"main":function/;
+ if (!re.test(precompiled)) {
+ // This is not a valid precompiled template, so this is
+ // a raw template that can be registered directly.
+ return precompiled;
+ }
+
+ // We need to take the string representation and turn it into a
+ // valid JavaScript object. eval is evil, but necessary in this case.
+
+ eval(`template = ${precompiled}`);
+ } else if (typeof precompiled == 'object' && typeof precompiled.main == 'function') {
+ template = precompiled;
+ }
+
+ // Take the precompiled object and get the actual function out of it,
+ // after first testing for runtime version compatibility.
+ return this.handlebars.template(template);
+ }
+
+ /**
+ * Detect whether a given template has been loaded.
+ */
+ isTemplateLoaded(path) {
+ return typeof this.handlebars.partials[path] !== 'undefined';
+ }
+
+ /**
+ * Return a function that performs any preprocessing we want to do on the templates.
+ * In our case, run them through the Handlebars precompiler. This returns a string
+ * representation of an object understood by Handlebars to be a precompiled template.
+ */
+ getPreProcessor() {
+ return templates => {
+ const paths = Object.keys(templates);
+ const processed = {};
+
+ for (let i = 0; i < paths.length; i++) {
+ const path = paths[i];
+ try {
+ processed[path] = this.handlebars.precompile(templates[path], handlebarsOptions);
+ } catch (e) {
+ throw new CompileError(e.message, { path });
+ }
+ }
+ return processed;
+ };
+ }
+
+ /**
+ * Render a template with the given context
+ *
+ * @param {String} path The path to the template
+ * @param {Object} context The context to provide to the template
+ * @return {Promise} A promise to return the rendered template
+ * @throws [TemplateNotFoundError|RenderError|DecoratorError]
+ */
+ render(path, context) {
+ return new Promise((resolve, reject) => {
+ context = context || {};
+
+ // Add some data to the context
+ context.template = path;
+ if (this._translator) {
+ context.locale_name = this._translator.getLocale();
+ }
+
+ // Look up the template
+ const template = this.handlebars.partials[path];
+ if (typeof template === 'undefined') {
+ return reject(new TemplateNotFoundError(`template not found: ${path}`));
+ }
+
+ // Render the template
+ let result;
+ try {
+ result = template(context);
+ } catch (e) {
+ return reject(new RenderError(e.message));
+ }
+
+ // Apply decorators
+ try {
+ for (let i = 0; i < this._decorators.length; i++) {
+ result = this._decorators[i](result);
+ }
+ } catch (e) {
+ return reject(new DecoratorError(e.message));
+ }
+
+ resolve(result);
+ });
+ };
+
+ /**
+ * Render a template with the given context
+ *
+ * @param {String} path The path to the template
+ * @param {Object} context The context to provide to the template
+ * @return {Promise} A promise to return the rendered template
+ * @throws [TemplateNotFoundError|RenderError|DecoratorError]
+ */
+ renderSync(path, context) {
+ context = context || {};
+
+ // Add some data to the context
+ context.template = path;
+ if (this._translator) {
+ context.locale_name = this._translator.getLocale();
+ }
+
+ // Look up the template
+ const template = this.handlebars.partials[path];
+ if (typeof template === 'undefined') {
+ throw new TemplateNotFoundError(`template not found: ${path}`);
+ }
+
+ // Render the template
+ let result;
+ try {
+ result = template(context);
+ } catch (e) {
+ throw new RenderError(e.message);
+ }
+
+ // Apply decorators
+ try {
+ for (let i = 0; i < this._decorators.length; i++) {
+ result = this._decorators[i](result);
+ }
+ } catch (e) {
+ throw new DecoratorError(e.message);
+ }
+
+ return result;
+ };
+
+ /**
+ * Renders a string with the given context
+ *
+ * @param {String} template
+ * @param {Object} context
+ * @return {String}
+ * @throws [CompileError|RenderError]
+ */
+ renderString(template, context) {
+ return new Promise((resolve, reject) => {
+ context = context || {};
+
+ // Compile the template
+ try {
+ template = this.handlebars.compile(template);
+ } catch (e) {
+ return reject(new CompileError(e.message));
+ }
+
+ // Render the result
+ let result;
+ try {
+ result = template(context);
+ } catch (e) {
+ return reject(new RenderError(e.message));
+ }
+
+ resolve(result);
+ });
+ }
+
+ /**
+ * Internal method. Set the Handlebars logger to use the given console alternative. This is an override
+ * of https://github.com/wycats/handlebars.js/blob/148b19182d70278237a62d8293db540483a0c46c/lib/handlebars/logger.js#L22
+ */
+ _setHandlebarsLogger() {
+ // Normalize on the v4 implementation
+ this.handlebars.logger = HandlebarsV4.logger;
+
+ // Override logger.log to use the given console alternative
+ this.handlebars.log = this.handlebars.logger.log = (level, ...message) => {
+ level = this.handlebars.logger.lookupLevel(level);
+
+ if (this.handlebars.logger.lookupLevel(this.handlebars.logger.level) <= level) {
+ let method = this.handlebars.logger.methodMap[level];
+ if (typeof this.logger[method] !== 'function') {
+ method = 'log';
+ }
+ this.logger[method](...message);
+ }
+ };
+ }
+
+ /**
+ *
+ * @param {String} level
+ */
+ setLoggerLevel(level) {
+ this.handlebars.logger.level = level;
+ }
+}
+
+export default HandlebarsRenderer;
diff --git a/helpers/3p/array.js b/helpers/3p/array.js
new file mode 100644
index 00000000..4e358547
--- /dev/null
+++ b/helpers/3p/array.js
@@ -0,0 +1,757 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Returns all of the items in an array after the specified index.
+ * Opposite of [before](#before).
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{after array 1}}
+ * //=> '["c"]'
+ * ```
+ *
+ * @param {Array} `array` Collection
+ * @param {Number} `n` Starting index (number of items to exclude)
+ * @return {Array} Array exluding `n` items.
+ * @api public
+ */
+
+helpers.after = function(array, n) {
+ if (utils.isUndefined(array)) return '';
+ return array.slice(n);
+};
+
+/**
+ * Cast the given `value` to an array.
+ *
+ * ```handlebars
+ * {{arrayify "foo"}}
+ * //=> '["foo"]'
+ * ```
+ * @param {any} `value`
+ * @return {Array}
+ * @api public
+ */
+
+helpers.arrayify = function(value) {
+ return value ? (Array.isArray(value) ? value : [value]) : [];
+};
+
+/**
+ * Return all of the items in the collection before the specified
+ * count. Opposite of [after](#after).
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{before array 2}}
+ * //=> '["a", "b"]'
+ * ```
+ *
+ * @param {Array} `array`
+ * @param {Number} `n`
+ * @return {Array} Array excluding items after the given number.
+ * @api public
+ */
+
+helpers.before = function(array, n) {
+ if (utils.isUndefined(array)) return '';
+ return array.slice(0, -n);
+};
+
+/**
+ * ```handlebars
+ * {{#eachIndex collection}}
+ * {{item}} is {{index}}
+ * {{/eachIndex}}
+ * ```
+ * @param {Array} `array`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.eachIndex = function(array, options) {
+ var result = '';
+ for (var i = 0; i < array.length; i++) {
+ result += options.fn({item: array[i], index: i});
+ }
+ return result;
+};
+
+/**
+ * Block helper that filters the given array and renders the block for values that
+ * evaluate to `true`, otherwise the inverse block is returned.
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{#filter array "foo"}}AAA{{else}}BBB{{/filter}}
+ * //=> 'BBB'
+ * ```
+ *
+ * @name .filter
+ * @param {Array} `array`
+ * @param {any} `value`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.filter = function(array, value, options) {
+ var content = '';
+ var results = [];
+
+ // filter on a specific property
+ var prop = options.hash && options.hash.property;
+ if (prop) {
+ results = utils.filter(array, function(val) {
+ return utils.get(val, prop) === value;
+ });
+ } else {
+
+ // filter on a string value
+ results = utils.filter(array, function(v) {
+ return value === v;
+ });
+ }
+
+ if (results && results.length > 0) {
+ for (var i = 0; i < results.length; i++) {
+ content += options.fn(results[i]);
+ }
+ return content;
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Returns the first item, or first `n` items of an array.
+ *
+ * Given the array `['a', 'b', 'c', 'd', 'e']`:
+ *
+ * ```handlebars
+ * {{first array 2}}
+ * //=> '["a", "b"]'
+ * ```
+ *
+ * @param {Array} `array`
+ * @param {Number} `n` Number of items to return, starting at `0`.
+ * @return {Array}
+ * @api public
+ */
+
+helpers.first = function(array, n) {
+ if (utils.isUndefined(array)) return '';
+ if (!utils.isNumber(n)) {
+ return array[0];
+ }
+ return array.slice(0, n);
+};
+
+/**
+ * Iterates over each item in an array and exposes the current item
+ * in the array as context to the inner block. In addition to
+ * the current array item, the helper exposes the following variables
+ * to the inner block:
+ *
+ * - `index`
+ * - `total`
+ * - `isFirst`
+ * - `isLast`
+ *
+ * Also, `@index` is exposed as a private variable, and additional
+ * private variables may be defined as hash arguments.
+ *
+ * ```js
+ * var accounts = [
+ * {'name': 'John', 'email': 'john@example.com'},
+ * {'name': 'Malcolm', 'email': 'malcolm@example.com'},
+ * {'name': 'David', 'email': 'david@example.com'}
+ * ];
+ *
+ * // example usage
+ * // {{#forEach accounts}}
+ * //
+ * // {{ name }}
+ * // {{#unless isLast}}, {{/unless}}
+ * // {{/forEach}}
+ * ```
+ * @source
+ * @param {Array} `array`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.forEach = function(array, options) {
+ var data = utils.createFrame(options, options.hash);
+ var len = array.length;
+ var buffer = '';
+ var i = -1;
+
+ while (++i < len) {
+ var item = array[i];
+ data.index = i;
+ item.index = i + 1;
+ item.total = len;
+ item.isFirst = i === 0;
+ item.isLast = i === (len - 1);
+ buffer += options.fn(item, {data: data});
+ }
+ return buffer;
+};
+
+/**
+ * Block helper that renders the block if an array has the
+ * given `value`. Optionally specify an inverse block to render
+ * when the array does not have the given value.
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{#inArray array "d"}}
+ * foo
+ * {{else}}
+ * bar
+ * {{/inArray}}
+ * //=> 'bar'
+ * ```
+ *
+ * @name .inArray
+ * @param {Array} `array`
+ * @param {any} `value`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.inArray = function(array, value, options) {
+ if (utils.indexOf(array, value) > -1) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Returns true if `value` is an es5 array.
+ *
+ * ```handlebars
+ * {{isArray "abc"}}
+ * //=> 'false'
+ * ```
+ *
+ * @param {any} `value` The value to test.
+ * @return {Boolean}
+ * @api public
+ */
+
+helpers.isArray = function(value) {
+ return Array.isArray(value);
+};
+
+/**
+ * Block helper that returns the item with specified index.
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{itemAt array 1}}
+ * //=> 'b'
+ * ```
+ *
+ * @name .itemAt
+ * @param {Array} `array`
+ * @param {Number} `idx`
+ * @return {any} `value`
+ * @block
+ * @api public
+ */
+
+helpers.itemAt = function(array, idx) {
+ if (utils.isUndefined(array)) return null;
+ array = utils.result(array);
+
+ if (!array || typeof(array) != 'object') return null;
+
+ if (!utils.isUndefined(idx)) {
+ idx = parseFloat(utils.result(idx));
+ }
+ else {
+ idx = 0;
+ }
+
+ if (idx < 0) {
+ idx = array.length + idx;
+ }
+
+ if (idx < 0 || idx >= array.length) {
+ return null;
+ }
+
+ return array[idx];
+};
+
+/**
+ * Join all elements of array into a string, optionally using a
+ * given separator.
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{join array}}
+ * //=> 'a, b, c'
+ *
+ * {{join array '-'}}
+ * //=> 'a-b-c'
+ * ```
+ *
+ * @param {Array} `array`
+ * @param {String} `sep` The separator to use.
+ * @return {String}
+ * @api public
+ */
+
+helpers.join = function(array, sep) {
+ if (utils.isUndefined(array)) return '';
+ sep = typeof sep !== 'string'
+ ? ', '
+ : sep;
+ return array.join(sep);
+};
+
+/**
+ * Returns the last item, or last `n` items of an array.
+ * Opposite of [first](#first).
+ *
+ * Given the array `['a', 'b', 'c', 'd', 'e']`:
+ *
+ * ```handlebars
+ * {{last array 2}}
+ * //=> '["d", "e"]'
+ * ```
+ * @param {Array} `array`
+ * @param {Number} `n` Number of items to return, starting with the last item.
+ * @return {Array}
+ * @api public
+ */
+
+helpers.last = function(array, n) {
+ if (!utils.isNumber(n)) {
+ return array[array.length - 1];
+ }
+ return array.slice(-n);
+};
+
+/**
+ * Block helper that compares the length of the given array to
+ * the number passed as the second argument. If the array length
+ * is equal to the given `length`, the block is returned,
+ * otherwise an inverse block may optionally be returned.
+ *
+ * Given the array `['a', 'b', 'c', 'd', 'e']`:
+ *
+ * ```handlebars
+ * {{#lengthEqual array 10}}AAA{{else}}BBB{{/lengthEqual}}
+ * //=> 'BBB'
+ * ```
+ *
+ * @name .lengthEqual
+ * @param {Array} `array`
+ * @param {Number} `length`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.lengthEqual = function(array, length, options) {
+ if (array.length === length) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Returns a new array, created by calling `function`
+ * on each element of the given `array`.
+ *
+ * Given an array `['a', 'b', 'c']`:
+ *
+ * ```js
+ * // register `double` as a helper
+ * function double(str) {
+ * return str + str;
+ * }
+ * // then used like this:
+ * // {{map array double}}
+ * //=> '["aa", "bb", "cc"]'
+ * ```
+ *
+ * @param {Array} `array`
+ * @param {Function} `fn`
+ * @return {String}
+ * @api public
+ */
+
+helpers.map = function(array, fn) {
+ if (utils.isUndefined(array)) return '';
+ if (typeof array === 'string' && /[[]/.test(array)) {
+ array = utils.tryParse(array) || [];
+ }
+ var len = array.length;
+ var res = new Array(len);
+ var i = -1;
+
+ while (++i < len) {
+ res[i] = fn(array[i], i, array);
+ }
+ return res;
+};
+
+/**
+ * Block helper that returns the block if the callback returns true
+ * for some value in the given array.
+ *
+ * Given the array `[1, 'b', 3]`:
+ *
+ * ```handlebars
+ * {{#some array isString}}
+ * Render me if the array has a string.
+ * {{else}}
+ * Render me if it doesn't.
+ * {{/some}}
+ * //=> 'Render me if the array has a string.'
+ * ```
+ * @name .some
+ * @param {Array} `array`
+ * @param {Function} `cb` callback function
+ * @param {Options} Handlebars provided options object
+ * @return {Array}
+ * @block
+ * @api public
+ */
+
+helpers.some = function(arr, cb, options) {
+ cb = utils.iterator(cb, this);
+ if (arr == null) {
+ return options.inverse(this);
+ }
+ var len = arr.length, i = -1;
+ while (++i < len) {
+ if (cb(arr[i], i, arr)) {
+ return options.fn(this);
+ }
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Sort the given `array`. If an array of objects is passed,
+ * you may optionally pass a `key` to sort on as the second
+ * argument. You may alternatively pass a sorting function as
+ * the second argument.
+ *
+ * Given an array `['b', 'a', 'c']`:
+ *
+ * ```handlebars
+ * {{sort array}}
+ * //=> '["a", "b", "c"]'
+ * ```
+ *
+ * @param {Array} `array` the array to sort.
+ * @param {String|Function} `key` The object key to sort by, or sorting function.
+ * @api public
+ */
+
+helpers.sort = function(arr, options) {
+ if (utils.isUndefined(arr)) return '';
+ if (utils.get(options, 'hash.reverse')) {
+ return arr.sort().reverse();
+ }
+ return arr.sort();
+};
+
+/**
+ * Sort an `array`. If an array of objects is passed,
+ * you may optionally pass a `key` to sort on as the second
+ * argument. You may alternatively pass a sorting function as
+ * the second argument.
+ *
+ * Given an array `[{a: 'zzz'}, {a: 'aaa'}]`:
+ *
+ * ```handlebars
+ * {{sortBy array "a"}}
+ * //=> '[{"a":"aaa"}, {"a":"zzz"}]'
+ * ```
+ *
+ * @param {Array} `array` the array to sort.
+ * @param {String|Function} `props` One or more properties to sort by, or sorting functions to use.
+ * @api public
+ */
+
+helpers.sortBy = function(arr/*, prop*/) {
+ if (utils.isUndefined(arr)) return '';
+ var args = [].slice.call(arguments);
+ args.pop(); // remove hbs options object
+
+ if (typeof args[0] === 'string' && /[[]/.test(args[0])) {
+ args[0] = utils.tryParse(args[0]) || [];
+ }
+ if (utils.isUndefined(args[1])) {
+ return args[0].sort();
+ }
+ return utils.sortBy.apply(null, args);
+};
+
+/**
+ * Use the items in the array _after_ the specified index
+ * as context inside a block. Opposite of [withBefore](#withBefore).
+ *
+ * Given the array `['a', 'b', 'c', 'd', 'e']`:
+ *
+ * ```handlebars
+ * {{#withAfter array 3}}
+ * {{this}}
+ * {{/withAfter}}
+ * //=> "de"
+ * ```
+ * @param {Array} `array`
+ * @param {Number} `idx`
+ * @param {Object} `options`
+ * @return {Array}
+ * @block
+ * @api public
+ */
+
+helpers.withAfter = function(array, idx, options) {
+ array = array.slice(idx);
+ var result = '';
+
+ var len = array.length, i = -1;
+ while (++i < len) {
+ result += options.fn(array[i]);
+ }
+ return result;
+};
+
+/**
+ * Use the items in the array _before_ the specified index
+ * as context inside a block. Opposite of [withAfter](#withAfter).
+ *
+ * Given the array `['a', 'b', 'c', 'd', 'e']`:
+ *
+ * ```handlebars
+ * {{#withBefore array 3}}
+ * {{this}}
+ * {{/withBefore}}
+ * //=> 'ab'
+ * ```
+ * @param {Array} `array`
+ * @param {Number} `idx`
+ * @param {Object} `options`
+ * @return {Array}
+ * @block
+ * @api public
+ */
+
+helpers.withBefore = function(array, idx, options) {
+ array = array.slice(0, -idx);
+ var result = '';
+
+ var len = array.length, i = -1;
+ while (++i < len) {
+ result += options.fn(array[i]);
+ }
+ return result;
+};
+
+/**
+ * Use the first item in a collection inside a handlebars
+ * block expression. Opposite of [withLast](#withLast).
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{#withFirst array}}
+ * {{this}}
+ * {{/withFirst}}
+ * //=> 'a'
+ * ```
+ * @param {Array} `array`
+ * @param {Number} `idx`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.withFirst = function(arr, idx, options) {
+ if (utils.isUndefined(arr)) return '';
+ arr = utils.result(arr);
+
+ if (!utils.isUndefined(idx)) {
+ idx = parseFloat(utils.result(idx));
+ }
+
+ if (utils.isUndefined(idx)) {
+ options = idx;
+ return options.fn(arr[0]);
+ }
+
+ arr = arr.slice(0, idx);
+ var len = arr.length, i = -1;
+ var result = '';
+ while (++i < len) {
+ result += options.fn(arr[i]);
+ }
+ return result;
+};
+
+/**
+ * Block helper that groups array elements by given `value`.
+ *
+ * Given the array `['a', 'b', 'c', 'd','e','f','g','h']`:
+ *
+ * ```handlebars
+ * {{#withGroup array 4}}
+ * {{#each this}}
+ * {{.}}
+ * {{each}}
+ *
+ * {{/withGroup}}
+ * ```
+ * //=> 'a','b','c','d'
+ * //=> 'e','f','g','h'
+ *
+ * @name .withGroup
+ * @param {Array} `array`
+ * @param {Number} `every`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.withGroup = function (array, every, options) {
+ var out = '', subcontext = [], i;
+ if (array && array.length > 0) {
+ for (i = 0; i < array.length; i++) {
+ if (i > 0 && i % every === 0) {
+ out += options.fn(subcontext);
+ subcontext = [];
+ }
+ subcontext.push(array[i]);
+ }
+ out += options.fn(subcontext);
+ }
+ return out;
+};
+
+/**
+ * Use the last item or `n` items in an array as context inside a block.
+ * Opposite of [withFirst](#withFirst).
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{#withLast array}}
+ * {{this}}
+ * {{/withLast}}
+ * //=> 'c'
+ * ```
+ * @param {Array} `array`
+ * @param {Number} `idx` The starting index.
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.withLast = function(array, idx, options) {
+ if (utils.isUndefined(array)) return '';
+ array = utils.result(array);
+
+ if (!utils.isUndefined(idx)) {
+ idx = parseFloat(utils.result(idx));
+ }
+
+ if (utils.isUndefined(idx)) {
+ options = idx;
+ return options.fn(array[array.length - 1]);
+ }
+
+ array = array.slice(-idx);
+ var len = array.length, i = -1;
+ var result = '';
+ while (++i < len) {
+ result += options.fn(array[i]);
+ }
+ return result;
+};
+
+/**
+ * Block helper that sorts a collection and exposes the sorted
+ * collection as context inside the block.
+ *
+ * Given the array `['b', 'a', 'c']`:
+ *
+ * ```handlebars
+ * {{#withSort array}}{{this}}{{/withSort}}
+ * //=> 'abc'
+ * ```
+ * @name .withSort
+ * @param {Array} `array`
+ * @param {String} `prop`
+ * @param {Object} `options` Specify `reverse="true"` to reverse the array.
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.withSort = function(array, prop, options) {
+ if (utils.isUndefined(array)) return '';
+ var result = '';
+
+ if (utils.isUndefined(prop)) {
+ options = prop;
+
+ array = array.sort();
+ if (utils.get(options, 'hash.reverse')) {
+ array = array.reverse();
+ }
+
+ for (var i = 0, len = array.length; i < len; i++) {
+ result += options.fn(array[i]);
+ }
+ return result;
+ }
+
+ array.sort(function(a, b) {
+ a = utils.get(a, prop);
+ b = utils.get(b, prop);
+ return a > b ? 1 : (a < b ? -1 : 0);
+ });
+
+ if (utils.get(options, 'hash.reverse')) {
+ array = array.reverse();
+ }
+
+ var alen = array.length, j = -1;
+ while (++j < alen) {
+ result += options.fn(array[j]);
+ }
+ return result;
+};
diff --git a/helpers/3p/collection.js b/helpers/3p/collection.js
new file mode 100644
index 00000000..3f3308ce
--- /dev/null
+++ b/helpers/3p/collection.js
@@ -0,0 +1,96 @@
+'use strict';
+
+var array = require('./array');
+var object = require('./object');
+var utils = require('./utils');
+var forEach = array.forEach;
+var forOwn = object.forOwn;
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Block helper that returns a block if the given collection is
+ * empty. If the collection is not empty the inverse block is returned
+ * (if supplied).
+ *
+ * @name .isEmpty
+ * @param {Object} `collection`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.isEmpty = function(collection, options) {
+ if (options == null) {
+ options = collection;
+ return options.fn(this);
+ }
+
+ if (Array.isArray(collection) && !collection.length) {
+ return options.fn(this);
+ }
+
+ var keys = Object.keys(collection);
+ if (typeof collection === 'object' && !keys.length) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Iterate over an array or object,
+ *
+ * @name .iterate
+ * @param {Object|Array} `context` The collection to iterate over
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.iterate = function(context, options) {
+ if (Array.isArray(context)) {
+ return forEach.apply(forEach, arguments);
+ } else if (utils.isObject(context)) {
+ return forOwn.apply(forOwn, arguments);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Returns the length of the given collection. When using a string literal in the
+ * template, the string must be value JSON. See the example below. Otherwise pass
+ * in an array or object from the context
+ *
+ * ```handlebars
+ * {{length '["a", "b", "c"]'}}
+ * //=> 3
+ *
+ * //=> myArray = ['a', 'b', 'c', 'd', 'e'];
+ * {{length myArray}}
+ * //=> 5
+ *
+ * //=> myObject = {'a': 'a', 'b': 'b'};
+ * {{length myObject}}
+ * //=> 2
+ * ```
+ * @param {Array|Object|String} `value`
+ * @return {Number} The length of the value.
+ * @api public
+ */
+
+helpers.length = function(value) {
+ if (utils.isUndefined(value)) return '';
+ if (typeof value === 'string' && /[[]/.test(value)) {
+ value = utils.tryParse(value) || [];
+ }
+ if (utils.isObject(value)) {
+ value = Object.keys(value);
+ }
+ return value.length;
+};
diff --git a/helpers/3p/comparison.js b/helpers/3p/comparison.js
new file mode 100644
index 00000000..9b8726fa
--- /dev/null
+++ b/helpers/3p/comparison.js
@@ -0,0 +1,559 @@
+'use strict';
+
+var isObject = require('./object').isObject;
+var isString = require('./string').isString;
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Block helper that renders the block if **both** of the given values
+ * are truthy. If an inverse block is specified it will be rendered
+ * when falsy.
+ *
+ * @param {any} `a`
+ * @param {any} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.and = function(a, b, options) {
+ if (a && b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Render a block when a comparison of the first and third
+ * arguments returns true. The second argument is
+ * the [arithemetic operator][operators] to use. You may also
+ * optionally specify an inverse block to render when falsy.
+ *
+ * @param `a`
+ * @param `operator` The operator to use. Operators must be enclosed in quotes: `">"`, `"="`, `"<="`, and so on.
+ * @param `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or if specified the inverse block is rendered if falsey.
+ * @block
+ * @api public
+ */
+
+helpers.compare = function(a, operator, b, options) {
+ /*eslint eqeqeq: 0*/
+
+ if (arguments.length < 4) {
+ throw new Error('handlebars Helper {{compare}} expects 4 arguments');
+ }
+
+ var result;
+ switch (operator) {
+ case '==':
+ result = a == b;
+ break;
+ case '===':
+ result = a === b;
+ break;
+ case '!=':
+ result = a != b;
+ break;
+ case '!==':
+ result = a !== b;
+ break;
+ case '<':
+ result = a < b;
+ break;
+ case '>':
+ result = a > b;
+ break;
+ case '<=':
+ result = a <= b;
+ break;
+ case '>=':
+ result = a >= b;
+ break;
+ case 'typeof':
+ result = typeof a === b;
+ break;
+ default: {
+ throw new Error('helper {{compare}}: invalid operator: `' + operator + '`');
+ }
+ }
+
+ if (result === false) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
+
+/**
+ * Block helper that renders the block if `collection` has the
+ * given `value`, using strict equality (`===`) for comparison,
+ * otherwise the inverse block is rendered (if specified). If a
+ * `startIndex` is specified and is negative, it is used as the
+ * offset from the end of the collection.
+ *
+ * Given the array `['a', 'b', 'c']`:
+ *
+ * ```handlebars
+ * {{#contains array "d"}}
+ * This will not be rendered.
+ * {{else}}
+ * This will be rendered.
+ * {{/contains}}
+ * ```
+ * @param {Array|Object|String} `collection` The collection to iterate over.
+ * @param {any} `value` The value to check for.
+ * @param {Number} `[startIndex=0]` Optionally define the starting index.
+ * @param {Object} `options` Handlebars provided options object.
+ * @block
+ * @api public
+ */
+
+helpers.contains = function(collection, value, startIndex, options) {
+ if (typeof startIndex === 'object') {
+ options = startIndex;
+ startIndex = undefined;
+ }
+ if (utils.contains(collection, value, startIndex)) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **greater than** `b`.
+ *
+ * If an inverse block is specified it will be rendered when falsy.
+ * You may optionally use the `compare=""` hash argument for the
+ * second value.
+ *
+ * @name .gt
+ * @param {String} `a`
+ * @param {String} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.gt = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a > b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **greater than or
+ * equal to** `b`.
+ *
+ * If an inverse block is specified it will be rendered when falsy.
+ * You may optionally use the `compare=""` hash argument for the
+ * second value.
+ *
+ * @name .gte
+ * @param {String} `a`
+ * @param {String} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.gte = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a >= b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `value` has `pattern`.
+ * If an inverse block is specified it will be rendered when falsy.
+ *
+ * @param {any} `val` The value to check.
+ * @param {any} `pattern` The pattern to check for.
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.has = function(value, pattern, options) {
+ if (arguments.length === 2) {
+ return pattern.inverse(this);
+ }
+
+ if (arguments.length === 1) {
+ return value.inverse(this);
+ }
+
+ if ((Array.isArray(value) || isString(value)) && isString(pattern)) {
+ if (value.indexOf(pattern) > -1) {
+ return options.fn(this);
+ }
+ }
+ if (isObject(value) && isString(pattern) && pattern in value) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **equal to** `b`.
+ * If an inverse block is specified it will be rendered when falsy.
+ * You may optionally use the `compare=""` hash argument for the
+ * second value.
+ *
+ * @name .eq
+ * @param {String} `a`
+ * @param {String} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.eq = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a === b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Return true if the given value is an even number.
+ *
+ * ```handlebars
+ * {{#ifEven value}}
+ * render A
+ * {{else}}
+ * render B
+ * {{/ifEven}}
+ * ```
+ * @param {Number} `number`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.ifEven = function(num, options) {
+ return utils.isEven(num)
+ ? options.fn(this)
+ : options.inverse(this);
+};
+
+/**
+ * Conditionally renders a block if the remainder is zero when
+ * `a` operand is divided by `b`. If an inverse block is specified
+ * it will be rendered when the remainder is **not zero**.
+ *
+ * @param {Number}
+ * @param {Number}
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.ifNth = function(a, b, options) {
+ if (utils.isNumber(a) && utils.isNumber(b) && b % a === 0) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `value` is **an odd number**.
+ * If an inverse block is specified it will be rendered when falsy.
+ *
+ * ```handlebars
+ * {{#ifOdd value}}
+ * render A
+ * {{else}}
+ * render B
+ * {{/ifOdd}}
+ * ```
+ * @param {Object} `value`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.ifOdd = function(val, options) {
+ return utils.isOdd(val)
+ ? options.fn(this)
+ : options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **equal to** `b`.
+ * If an inverse block is specified it will be rendered when falsy.
+ *
+ * @name .is
+ * @param {any} `a`
+ * @param {any} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.is = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a === b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **not equal to** `b`.
+ * If an inverse block is specified it will be rendered when falsy.
+ *
+ * @name .isnt
+ * @param {String} `a`
+ * @param {String} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.isnt = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a !== b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **less than** `b`.
+ *
+ * If an inverse block is specified it will be rendered when falsy.
+ * You may optionally use the `compare=""` hash argument for the
+ * second value.
+ *
+ * @name .lt
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.lt = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a < b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if `a` is **less than or
+ * equal to** `b`.
+ *
+ * If an inverse block is specified it will be rendered when falsy.
+ * You may optionally use the `compare=""` hash argument for the
+ * second value.
+ *
+ * @name .lte
+ * @param {Sring} `a`
+ * @param {Sring} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.lte = function(a, b, options) {
+ if (arguments.length === 2) {
+ options = b;
+ b = options.hash.compare;
+ }
+ if (a <= b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if **neither of** the given values
+ * are truthy. If an inverse block is specified it will be rendered
+ * when falsy.
+ *
+ * @name .neither
+ * @param {any} `a`
+ * @param {any} `b`
+ * @param `options` Handlebars options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.neither = function(a, b, options) {
+ if (!a && !b) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that renders a block if **any of** the given values
+ * is truthy. If an inverse block is specified it will be rendered
+ * when falsy.
+ *
+ * ```handlebars
+ * {{#or a b c}}
+ * If any value is true this will be rendered.
+ * {{/or}}
+ * ```
+ *
+ * @name .or
+ * @param {...any} `arguments` Variable number of arguments
+ * @param {Object} `options` Handlebars options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.or = function(/* any, any, ..., options */) {
+ var len = arguments.length - 1;
+ var options = arguments[len];
+
+ for (var i = 0; i < len; i++) {
+ if (arguments[i]) {
+ return options.fn(this);
+ }
+ }
+
+ return options.inverse(this);
+};
+
+/**
+ * Block helper that always renders the inverse block **unless `a` is
+ * is equal to `b`**.
+ *
+ * @name .unlessEq
+ * @param {String} `a`
+ * @param {String} `b`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Inverse block by default, or block if falsey.
+ * @block
+ * @api public
+ */
+
+helpers.unlessEq = function(context, options) {
+ if (context === options.hash.compare) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
+
+/**
+ * Block helper that always renders the inverse block **unless `a` is
+ * is greater than `b`**.
+ *
+ * @name .unlessGt
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Inverse block by default, or block if falsey.
+ * @block
+ * @api public
+ */
+
+helpers.unlessGt = function(context, options) {
+ if (context > options.hash.compare) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
+
+/**
+ * Block helper that always renders the inverse block **unless `a` is
+ * is less than `b`**.
+ *
+ * @name .unlessLt
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.unlessLt = function(context, options) {
+ if (context < options.hash.compare) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
+
+/**
+ * Block helper that always renders the inverse block **unless `a` is
+ * is greater than or equal to `b`**.
+ *
+ * @name .unlessGteq
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.unlessGteq = function(context, options) {
+ if (context >= options.hash.compare) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
+
+/**
+ * Block helper that always renders the inverse block **unless `a` is
+ * is less than or equal to `b`**.
+ *
+ * @name .unlessLteq
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars provided options object
+ * @return {String} Block, or inverse block if specified and falsey.
+ * @block
+ * @api public
+ */
+
+helpers.unlessLteq = function(context, options) {
+ if (context <= options.hash.compare) {
+ return options.inverse(this);
+ }
+ return options.fn(this);
+};
diff --git a/helpers/3p/date.js b/helpers/3p/date.js
new file mode 100644
index 00000000..84cf8e3d
--- /dev/null
+++ b/helpers/3p/date.js
@@ -0,0 +1,15 @@
+'use strict';
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Expose `moment` helper
+ * @exposes helper-date as moment
+ * @api public
+ */
+
+helpers.moment = require('helper-date');
diff --git a/helpers/3p/html.js b/helpers/3p/html.js
new file mode 100644
index 00000000..4f507551
--- /dev/null
+++ b/helpers/3p/html.js
@@ -0,0 +1,245 @@
+'use strict';
+
+var path = require('path');
+var html = require('./utils/html');
+var utils = require('./utils');
+var parseAttr = html.parseAttributes;
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Add an array of `` tags. Automatically resolves
+ * relative paths to `options.assets` if passed on the context.
+ *
+ * @param {Object} `context`
+ * @return {String}
+ * @api public
+ */
+
+helpers.css = function(array, options) {
+ if (arguments.length < 2) {
+ options = array;
+ array = [];
+ }
+
+ var styles = utils.arrayify(array);
+ var assets = '';
+
+ if (this && this.options) {
+ assets = this.options.assets || '';
+ }
+
+ if (options.hash.href) {
+ styles = utils.arrayify(options.hash.href);
+ }
+
+ return styles.map(function(item) {
+ var ext = path.extname(item);
+ var fp = path.join(assets, item);
+
+ if (ext === '.less') {
+ return '';
+ }
+ return '';
+ }).join('\n');
+};
+
+/**
+ * Truncates a string to the specified `length`, and appends
+ * it with an elipsis, `…`.
+ *
+ * ```js
+ * {{ellipsis "foo bar baz", 7}}
+ * //=> 'foo bar…'
+ * ```
+ * @name .ellipsis
+ * @param {String} `str`
+ * @param {Number} `length` The desired length of the returned string.
+ * @return {String} The truncated string.
+ * @api public
+ */
+
+helpers.ellipsis = function(str, limit) {
+ if (str && typeof str === 'string') {
+ if (str.length <= limit) {
+ return str;
+ }
+ return helpers.truncate(str, limit) + '…';
+ }
+};
+
+/**
+ * Generate one or more `` tags with paths/urls to
+ * javascript or coffeescript files.
+ *
+ * ```handlebars
+ * {{js scripts}}
+ * ```
+ *
+ * @param {Object} `context`
+ * @return {String}
+ * @api public
+ */
+
+helpers.js = function(context) {
+ if (utils.typeOf(context) === 'object') {
+ var attr = html.toAttributes(context.hash);
+ return '';
+ }
+
+ if (utils.typeOf(context) === 'string') {
+ return '';
+ }
+
+ context = utils.arrayify(context);
+ return context.map(function(fp) {
+ return (path.extname(fp) === '.coffee')
+ ? utils.tag('script', {type: 'text/coffeescript', src: fp})
+ : utils.tag('script', {src: fp});
+ }).join('\n');
+};
+
+/**
+ * Strip HTML tags from a string, so that only the text nodes
+ * are preserved.
+ *
+ * ```js
+ * {{sanitize "foo"}}
+ * //=> 'foo'
+ * ```
+ *
+ * @param {String} `str` The string of HTML to sanitize.
+ * @return {String}
+ * @api public
+ */
+
+helpers.sanitize = function(str) {
+ return html.sanitize(str);
+};
+
+/**
+ * Truncate a string by removing all HTML tags and limiting the result
+ * to the specified `length`. Aslo see [ellipsis](#ellipsis).
+ *
+ * ```js
+ * truncate("foo bar baz", 7);
+ * //=> 'foo bar'
+ * ```
+ *
+ * @name .truncate
+ * @param {String} `str`
+ * @param {Number} `limit` The desired length of the returned string.
+ * @param {String} `suffix` Optionally supply a string to use as a suffix to
+ * denote when the string has been truncated.
+ * @return {String} The truncated string.
+ * @api public
+ */
+
+helpers.truncate = function(str, limit, suffix) {
+ if (str && typeof str === 'string') {
+ var ch = typeof suffix === 'string' ? suffix : '';
+ if (str.length > limit) {
+ return html.sanitize(str).slice(0, limit - ch.length) + ch;
+ }
+ return str;
+ }
+};
+
+/**
+ * Block helper for creating unordered lists (``)
+ *
+ * @param {Object} `context`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.ul = function(context, options) {
+ return ('') + context.map(function(item) {
+ if (typeof item !== 'string') {
+ item = options.fn(item);
+ }
+ return '- ' + item + '
';
+ }).join('\n') + '
';
+};
+
+/**
+ * Block helper for creating ordered lists (`
`)
+ *
+ * @param {Object} `context`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.ol = function(context, options) {
+ return ('') + context.map(function(item) {
+ if (typeof item !== 'string') {
+ item = options.fn(item);
+ }
+ return '- ' + item + '
';
+ }).join('\n') + '
';
+};
+
+/**
+ * Returns a `` with a thumbnail linked to a full picture
+ *
+ * @param {Object} `context` Object with values/attributes to add to the generated elements:
+ * @param {String} `context.alt`
+ * @param {String} `context.src`
+ * @param {Number} `context.width`
+ * @param {Number} `context.height`
+ * @return {String} HTML `` element with image and optional caption/link.
+ * @contributor: Marie Hogebrandt
+ * @api public
+ */
+
+helpers.thumbnailImage = function(context) {
+ var figure = '';
+ var image = '';
+
+ var link = context.full || false;
+ var imageAttributes = {
+ alt: context.alt,
+ src: context.thumbnail,
+ width: context.size.width,
+ height: context.size.height
+ };
+
+ var figureAttributes = { id: 'image-' + context.id };
+ var linkAttributes = { href: link, rel: 'thumbnail' };
+
+ if (context.classes) {
+ if (context.classes.image) {
+ imageAttributes.class = context.classes.image.join(' ');
+ }
+ if (context.classes.figure) {
+ figureAttributes.class = context.classes.figure.join(' ');
+ }
+ if (context.classes.link) {
+ linkAttributes.class = context.classes.link.join(' ');
+ }
+ }
+
+ figure += '\n';
+ image += '
\n';
+
+ if (link) {
+ figure += '\n' + image + '\n';
+ } else {
+ figure += image;
+ }
+
+ if (context.caption) {
+ figure += '' + context.caption + '\n';
+ }
+
+ figure += '';
+ return figure;
+};
diff --git a/helpers/3p/i18n.js b/helpers/3p/i18n.js
new file mode 100644
index 00000000..ffa59e35
--- /dev/null
+++ b/helpers/3p/i18n.js
@@ -0,0 +1,57 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * i18n helper. See [button-i18n](https://github.com/assemble/buttons)
+ * for a working example.
+ *
+ * @contributor Laurent Goderre
+ * @param {String} `key`
+ * @param {Object} `options`
+ * @return {String}
+ * @api public
+ */
+
+helpers.i18n = function(prop, context, options) {
+ if (utils.isOptions(context)) {
+ options = context;
+ context = {};
+ }
+
+ if (typeof prop !== 'string') {
+ throw new Error('{{i18n}} helper expected "key" to be a string');
+ }
+
+ var opts = utils.merge({}, this, options.hash);
+
+ // account for `options` being passed on the context
+ if (opts.options) {
+ opts = utils.merge({}, opts, opts.options);
+ delete opts.options;
+ }
+
+ var lang = opts.language || opts.lang;
+
+ if (typeof lang !== 'string') {
+ throw new Error('{{i18n}} helper expected "language" parameter to be a string');
+ }
+
+ var value = utils.get(opts, lang);
+ if (typeof value === 'undefined') {
+ throw new Error('{{i18n}} helper cannot find language "' + lang + '"');
+ }
+
+ var result = utils.get(value, prop);
+ if (typeof result === 'undefined') {
+ throw new Error('{{i18n}} helper cannot find property "' + prop + '" for language "' + lang + '"');
+ }
+
+ return result;
+};
diff --git a/helpers/3p/inflection.js b/helpers/3p/inflection.js
new file mode 100644
index 00000000..68317c82
--- /dev/null
+++ b/helpers/3p/inflection.js
@@ -0,0 +1,69 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * @name .inflect
+ * @param {Number} `count`
+ * @param {String} `singular` The singular form
+ * @param {String} `plural` The plural form
+ * @param {String} `include`
+ * @return {String}
+ * @api public
+ */
+
+helpers.inflect = function(count, singular, plural, include) {
+ var word = (count > 1 || count === 0) ? plural : singular;
+
+ if (utils.isUndefined(include) || include === false) {
+ return word;
+ } else {
+ return String(count) + ' ' + word;
+ }
+};
+
+/**
+ * Returns an ordinalized number (as a string).
+ *
+ * ```handlebars
+ * {{ordinalize 1}}
+ * //=> '1st'
+ * {{ordinalize 21}}
+ * //=> '21st'
+ * {{ordinalize 29}}
+ * //=> '29th'
+ * {{ordinalize 22}}
+ * //=> '22nd'
+ * ```
+ *
+ * @param {String} `val` The value to ordinalize.
+ * @return {String} The ordinalized number
+ * @api public
+ */
+
+helpers.ordinalize = function(val) {
+ var num = Math.abs(Math.round(val));
+ var res = num % 100;
+
+ if (utils.indexOf([11, 12, 13], res) >= 0) {
+ return '' + val + 'th';
+ }
+
+ switch (num % 10) {
+ case 1:
+ return '' + val + 'st';
+ case 2:
+ return '' + val + 'nd';
+ case 3:
+ return '' + val + 'rd';
+ default: {
+ return '' + val + 'th';
+ }
+ }
+};
diff --git a/helpers/3p/markdown.js b/helpers/3p/markdown.js
new file mode 100644
index 00000000..72f67efb
--- /dev/null
+++ b/helpers/3p/markdown.js
@@ -0,0 +1,56 @@
+'use strict';
+
+/**
+ * Expose markdown `helpers` (for performance we're using getters so
+ * that the helpers are only loaded if called)
+ */
+
+var helpers = module.exports;
+var markdown;
+
+/**
+ * Block helper that converts a string of inline markdown to HTML.
+ *
+ * ```html
+ * {{#markdown}}
+ * # Foo
+ * {{/markdown}}
+ * //=> Foo
+ * ```
+ * @name .markdown
+ * @param {Object} `context`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+Object.defineProperty(helpers, 'markdown', {
+ configurable: true,
+ enumerable: true,
+ set: function(val) {
+ markdown = val;
+ },
+ get: function() {
+ // this is defined as a getter to avoid calling this function
+ // unless the helper is actually used
+ return markdown || (markdown = require('helper-markdown')());
+ }
+});
+
+/**
+ * Read a markdown file from the file system and inject its contents after
+ * converting it to HTML.
+ *
+ * ```html
+ * {{md "foo/bar.md"}}
+ * ```
+ * @name .md
+ * @param {Object} `context`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.md = require('helper-md');
diff --git a/helpers/3p/math.js b/helpers/3p/math.js
new file mode 100644
index 00000000..ed217fee
--- /dev/null
+++ b/helpers/3p/math.js
@@ -0,0 +1,137 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Return the product of `a` plus `b`.
+ *
+ * @param {Number} `a`
+ * @param {Number} `b`
+ * @api public
+ */
+
+helpers.add = function(a, b) {
+ return a + b;
+};
+
+/**
+ * Return the product of `a` minus `b`.
+ *
+ * @param {Number} `a`
+ * @param {Number} `b`
+ * @api public
+ */
+
+helpers.subtract = function(a, b) {
+ return a - b;
+};
+
+/**
+ * Divide `a` by `b`
+ *
+ * @param {Number} `a` numerator
+ * @param {Number} `b` denominator
+ * @api public
+ */
+
+helpers.divide = function(a, b) {
+ return a / b;
+};
+
+/**
+ * Multiply `a` by `b`.
+ *
+ * @param {Number} `a` factor
+ * @param {Number} `b` multiplier
+ * @api public
+ */
+
+helpers.multiply = function(a, b) {
+ return a * b;
+};
+
+/**
+ * Get the `Math.floor()` of the given value.
+ *
+ * @param {Number} `value`
+ * @api public
+ */
+
+helpers.floor = function(value) {
+ return Math.floor(value);
+};
+
+/**
+ * Get the `Math.ceil()` of the given value.
+ *
+ * @param {Number} `value`
+ * @api public
+ */
+
+helpers.ceil = function(value) {
+ return Math.ceil(value);
+};
+
+/**
+ * Round the given value.
+ *
+ * @param {Number} `value`
+ * @api public
+ */
+
+helpers.round = function(value) {
+ return Math.round(value);
+};
+
+/**
+ * Returns the sum of all numbers in the given array.
+ *
+ * ```handlebars
+ * {{sum "[1, 2, 3, 4, 5]"}}
+ * //=> '15'
+ * ```
+ *
+ * @name .sum
+ * @param {Array} `array` Array of numbers to add up.
+ * @return {Number}
+ * @api public
+ */
+
+helpers.sum = function() {
+ var args = utils.flatten([].concat.apply([], arguments));
+ var i = args.length, sum = 0;
+ while (i--) {
+ if (!utils.isNumber(args[i])) {
+ continue;
+ }
+ sum += (+args[i]);
+ }
+ return sum;
+};
+
+/**
+ * Returns the average of all numbers in the given array.
+ *
+ * ```handlebars
+ * {{avg "[1, 2, 3, 4, 5]"}}
+ * //=> '3'
+ * ```
+ *
+ * @name .avg
+ * @param {Array} `array` Array of numbers to add up.
+ * @return {Number}
+ * @api public
+ */
+
+helpers.avg = function() {
+ var args = utils.flatten([].concat.apply([], arguments));
+ // remove handlebars options object
+ args.pop();
+ return exports.sum(args) / args.length;
+};
diff --git a/helpers/3p/misc.js b/helpers/3p/misc.js
new file mode 100644
index 00000000..f2ad0533
--- /dev/null
+++ b/helpers/3p/misc.js
@@ -0,0 +1,74 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Returns the first value if defined, otherwise the "default" value is returned.
+ *
+ * @param {any} `value`
+ * @param {any} `defaultValue`
+ * @return {String}
+ * @alias .or
+ * @api public
+ */
+
+helpers.default = function(value, defaultValue) {
+ return value == null
+ ? defaultValue
+ : value;
+};
+
+/**
+ * Return the given value of `prop` from `this.options`. Given the context `{options: {a: {b: {c: 'ddd'}}}}`
+ *
+ * ```handlebars
+ * {{option "a.b.c"}}
+ *
+ * ```
+ *
+ * @param {String} `prop`
+ * @return {any}
+ * @api public
+ */
+
+helpers.option = function(prop) {
+ var opts = (this && this.options) || {};
+ return utils.get(opts, prop);
+};
+
+/**
+ * Block helper that renders the block without taking any arguments.
+ *
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.noop = function(options) {
+ return options.fn(this);
+};
+
+/**
+ * Block helper that builds the context for the block
+ * from the options hash.
+ *
+ * @param {Object} `options` Handlebars provided options object.
+ * @contributor Vladimir Kuznetsov
+ * @block
+ * @api public
+ */
+
+helpers.withHash = function(options) {
+ if (options.hash && Object.keys(options.hash).length) {
+ return options.fn(options.hash);
+ } else {
+ return options.inverse(this);
+ }
+};
diff --git a/helpers/3p/number.js b/helpers/3p/number.js
new file mode 100644
index 00000000..e8a24bfb
--- /dev/null
+++ b/helpers/3p/number.js
@@ -0,0 +1,169 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Add commas to numbers
+ *
+ * @param {Number} `num`
+ * @return {Number}
+ * @api public
+ */
+
+helpers.addCommas = function(num) {
+ return num.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
+};
+
+/**
+ * Convert a string or number to a formatted phone number.
+ *
+ * @param {Number|String} `num` The phone number to format, e.g. `8005551212`
+ * @return {Number} Formatted phone number: `(800) 555-1212`
+ * @source http://bit.ly/QlPmPr
+ * @api public
+ */
+
+helpers.phoneNumber = function(num) {
+ num = num.toString();
+
+ return '(' + num.substr(0, 3) + ') '
+ + num.substr(3, 3) + '-'
+ + num.substr(6, 4);
+};
+
+/**
+ * Generate a random number between two values
+ *
+ * @param {Number} `min`
+ * @param {Number} `max`
+ * @contributor Tim Douglas
+ * @return {String}
+ * @api public
+ */
+
+helpers.random = function(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+};
+
+/**
+ * Abbreviate numbers to the given number of `precision`. This is for
+ * general numbers, not size in bytes.
+ *
+ * @param {Number} `number`
+ * @param {Number} `precision`
+ * @return {String}
+ * @api public
+ */
+
+helpers.toAbbr = function(number, precision) {
+ if (!utils.isNumber(number)) {
+ number = 0;
+ }
+ if (utils.isUndefined(precision)) {
+ precision = 2;
+ }
+
+ number = +number;
+ // 2 decimal places => 100, 3 => 1000, etc.
+ precision = Math.pow(10, precision);
+ var abbr = ['k', 'm', 'b', 't', 'q'];
+ var len = abbr.length - 1;
+
+ while (len >= 0) {
+ var size = Math.pow(10, (len + 1) * 3);
+ if (size <= (number + 1)) {
+ number = Math.round(number * precision / size) / precision;
+ number += abbr[len];
+ break;
+ }
+ len--;
+ }
+ return number;
+};
+
+/**
+ * Returns a string representing the given number in exponential notation.
+ *
+ * ```js
+ * {{toExponential number digits}};
+ * ```
+ * @param {Number} `number`
+ * @param {Number} `fractionDigits` Optional. An integer specifying the number of digits to use after the decimal point. Defaults to as many digits as necessary to specify the number.
+ * @return {Number}
+ * @api public
+ */
+
+helpers.toExponential = function(number, digits) {
+ if (!utils.isNumber(number)) {
+ number = 0;
+ }
+ if (utils.isUndefined(digits)) {
+ digits = 0;
+ }
+ number = +number;
+ return number.toExponential(digits);
+};
+
+/**
+ * Formats the given number using fixed-point notation.
+ *
+ * @param {Number} `number`
+ * @param {Number} `digits` Optional. The number of digits to use after the decimal point; this may be a value between 0 and 20, inclusive, and implementations may optionally support a larger range of values. If this argument is omitted, it is treated as 0.
+ * @return {Number}
+ * @api public
+ */
+
+helpers.toFixed = function(number, digits) {
+ if (!utils.isNumber(number)) {
+ number = 0;
+ }
+ if (utils.isUndefined(digits)) {
+ digits = 0;
+ }
+ number = +number;
+ return number.toFixed(digits);
+};
+
+/**
+ * @param {Number} `number`
+ * @return {Number}
+ * @api public
+ */
+
+helpers.toFloat = function(number) {
+ return parseFloat(number);
+};
+
+/**
+ * @param {Number} `number`
+ * @return {Number}
+ * @api public
+ */
+
+helpers.toInt = function(number) {
+ return parseInt(number, 10);
+};
+
+/**
+ * @param {Number} `number`
+ * @param {Number} `precision` Optional. The number of significant digits.
+ * @return {Number}
+ * @api public
+ */
+
+helpers.toPrecision = function(number, precision) {
+ if (!utils.isNumber(number)) {
+ number = 0;
+ }
+ if (utils.isUndefined(precision)) {
+ precision = 1;
+ }
+ number = +number;
+ return number.toPrecision(precision);
+};
diff --git a/helpers/3p/object.js b/helpers/3p/object.js
new file mode 100644
index 00000000..4db5fa39
--- /dev/null
+++ b/helpers/3p/object.js
@@ -0,0 +1,296 @@
+'use strict';
+
+var hasOwn = Object.hasOwnProperty;
+var array = require('./array');
+var utils = require('./utils/');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * Extend the context with the properties of other objects.
+ * A shallow merge is performed to avoid mutating the context.
+ *
+ * @param {Object} `objects` One or more objects to extend.
+ * @return {Object}
+ * @api public
+ */
+
+helpers.extend = function(/*objects*/) {
+ var args = [].slice.call(arguments);
+ var last = args[args.length - 1];
+
+ if (last && utils.isObject(last) && last.hash) {
+ last = last.hash;
+ args.pop(); // remove handlebars options object
+ args.push(last);
+ }
+
+ var len = args.length;
+ var context = {};
+ var i = -1;
+
+ while (++i < len) {
+ var obj = args[i];
+ if (utils.isObject(obj)) {
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ context[key] = obj[key];
+ }
+ }
+ }
+ }
+ return context;
+};
+
+/**
+ * Block helper that iterates over the properties of
+ * an object, exposing each key and value on the context.
+ *
+ * @name .forIn
+ * @param {Object} `context`
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.forIn = function(obj, options) {
+ if (!utils.isOptions(options)) {
+ return obj.inverse(this);
+ }
+
+ var data = utils.createFrame(options, options.hash);
+ var result = '';
+
+ for (var key in obj) {
+ data.key = key;
+ result += options.fn(obj[key], {data: data});
+ }
+ return result;
+};
+
+/**
+ * Block helper that iterates over the **own** properties of
+ * an object, exposing each key and value on the context.
+ *
+ * @name .forOwn
+ * @param {Object} `obj` The object to iterate over.
+ * @param {Object} `options`
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.forOwn = function(obj, options) {
+ if (!utils.isOptions(options)) {
+ return obj.inverse(this);
+ }
+
+ var data = utils.createFrame(options, options.hash);
+ var result = '';
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ data.key = key;
+ result += options.fn(obj[key], {data: data});
+ }
+ }
+ return result;
+};
+
+/**
+ * Take arguments and, if they are string or number, convert them to a dot-delineated object property path.
+ *
+ * @name .toPath
+ * @param {String|Number} `prop` The property segments to assemble (can be multiple).
+ * @return {String}
+ * @api public
+ */
+helpers.toPath = function (/*prop*/) {
+ var prop = [];
+ for (var i = 0; i < arguments.length; i++) {
+ if (typeof arguments[i] === "string" || typeof arguments[i] === "number") {
+ prop.push(arguments[i]);
+ }
+ }
+ return prop.join('.');
+};
+
+/**
+ * Use property paths (`a.b.c`) to get a value or nested value from
+ * the context. Works as a regular helper or block helper.
+ *
+ * @name .get
+ * @param {String} `prop` The property to get, optionally using dot notation for nested properties.
+ * @param {Object} `context` The context object
+ * @param {Object} `options` The handlebars options object, if used as a block helper.
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.get = function(prop, context, options) {
+ var val = utils.get(context, prop);
+ if (options && options.fn) {
+ return val ? options.fn(val) : options.inverse(context);
+ }
+ return val;
+};
+
+/**
+ * Use property paths (`a.b.c`) to get an object from
+ * the context. Differs from the `get` helper in that this
+ * helper will return the actual object, including the
+ * given property key. Also, this helper does not work as a
+ * block helper.
+ *
+ * @name .getObject
+ * @param {String} `prop` The property to get, optionally using dot notation for nested properties.
+ * @param {Object} `context` The context object
+ * @return {String}
+ * @api public
+ */
+
+helpers.getObject = function(prop, context) {
+ return utils.getObject(context, prop);
+};
+
+/**
+ * Return true if `key` is an own, enumerable property
+ * of the given `context` object.
+ *
+ * ```handlebars
+ * {{hasOwn context key}}
+ * ```
+ *
+ * @name .hasOwn
+ * @param {String} `key`
+ * @param {Object} `context` The context object.
+ * @return {Boolean}
+ * @api public
+ */
+
+helpers.hasOwn = function(context, key) {
+ return hasOwn.call(context, key);
+};
+
+/**
+ * Return true if `value` is an object.
+ *
+ * ```handlebars
+ * {{isObject "foo"}}
+ * //=> false
+ * ```
+ * @name .isObject
+ * @param {String} `value`
+ * @return {Boolean}
+ * @api public
+ */
+
+helpers.isObject = function(value) {
+ return value && typeof value === 'object'
+ && !Array.isArray(value);
+};
+
+/**
+ * Deeply merge the properties of the given `objects` with the
+ * context object.
+ *
+ * @name .merge
+ * @param {Object} `object` The target object. Pass an empty object to shallow clone.
+ * @param {Object} `objects`
+ * @return {Object}
+ * @api public
+ */
+
+helpers.merge = function(context/*, objects, options*/) {
+ var args = [].slice.call(arguments);
+ var last = args[args.length - 1];
+
+ if (last && typeof last === 'object' && last.hash) {
+ last = last.hash;
+ args.pop(); // remove handlebars options object
+ args.push(last);
+ }
+
+ context = utils.merge.apply(utils.merge, args);
+ return context;
+};
+
+/**
+ * Block helper that parses a string using `JSON.parse`,
+ * then passes the parsed object to the block as context.
+ *
+ * @param {String} `string` The string to parse
+ * @param {Object} `options` Handlebars options object
+ * @contributor github.com/keeganstreet
+ * @block
+ * @api public
+ */
+
+helpers.JSONparse = function(str, options) {
+ return options.fn(JSON.parse(str));
+};
+
+/**
+ * Alias for parseJSON. this will be
+ * deprecated in a future release
+ */
+
+helpers.parseJSON = helpers.JSONparse;
+
+/**
+ * Pick properties from the context object.
+ *
+ * @param {Array|String} `properties` One or more properties to pick.
+ * @param {Object} `context`
+ * @param {Object} `options` Handlebars options object.
+ * @return {Object} Returns an object with the picked values. If used as a block helper, the values are passed as context to the inner block. If no values are found, the context is passed to the inverse block.
+ * @block
+ * @api public
+ */
+
+helpers.pick = function(props, context, options) {
+ var keys = array.arrayify(props);
+ var len = keys.length, i = -1;
+ var result = {};
+
+ while (++i < len) {
+ result = helpers.extend(result, utils.getObject(context, keys[i]));
+ }
+
+ if (options.fn) {
+ if (Object.keys(result).length) {
+ return options.fn(result);
+ } else {
+ return options.inverse(context);
+ }
+ }
+ return result;
+};
+
+/**
+ * Stringify an object using `JSON.stringify`.
+ *
+ * @param {Object} `obj` Object to stringify
+ * @return {String}
+ * @api public
+ */
+
+helpers.JSONstringify = function(obj, indent) {
+ if (!utils.isNumber(indent)) {
+ indent = 0;
+ }
+ return JSON.stringify(obj, null, indent);
+};
+
+/**
+ * Alias for JSONstringify. this will be
+ * deprecated in a future release
+ */
+
+helpers.stringify = helpers.JSONstringify;
diff --git a/helpers/3p/string.js b/helpers/3p/string.js
new file mode 100644
index 00000000..64b8285c
--- /dev/null
+++ b/helpers/3p/string.js
@@ -0,0 +1,500 @@
+'use strict';
+
+var utils = require('./utils');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = module.exports;
+
+/**
+ * camelCase the characters in the given `string`.
+ *
+ * ```js
+ * {{camelcase "foo bar baz"}};
+ * //=> 'fooBarBaz'
+ * ```
+ *
+ * @name .camelcase
+ * @param {String} `string` The string to camelcase.
+ * @return {String}
+ * @api public
+ */
+
+helpers.camelcase = function(str) {
+ return utils.changecase(str, function(ch) {
+ return ch.toUpperCase();
+ });
+};
+
+/**
+ * Capitalize the first word in a sentence.
+ *
+ * ```handlebars
+ * {{capitalize "foo bar baz"}}
+ * //=> "Foo bar baz"
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.capitalize = function(str) {
+ if (str && typeof str === 'string') {
+ return str.charAt(0).toUpperCase()
+ + str.slice(1);
+ }
+};
+
+/**
+ * Capitalize all words in a string.
+ *
+ * ```handlebars
+ * {{capitalizeAll "foo bar baz"}}
+ * //=> "Foo Bar Baz"
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.capitalizeAll = function(str) {
+ if (str && typeof str === 'string') {
+ return str.replace(/\w\S*/g, function(word) {
+ return exports.capitalize(word);
+ });
+ }
+};
+
+/**
+ * Center a string using non-breaking spaces
+ *
+ * @param {String} `str`
+ * @param {String} `spaces`
+ * @return {String}
+ * @api public
+ */
+
+helpers.center = function(str, spaces) {
+ if (str && typeof str === 'string') {
+ var space = '';
+ var i = 0;
+ while (i < spaces) {
+ space += ' ';
+ i++;
+ }
+ return space + str + space;
+ }
+};
+
+/**
+ * Like trim, but removes both extraneous whitespace **and
+ * non-word characters** from the beginning and end of a string.
+ *
+ * ```js
+ * {{chop "_ABC_"}}
+ * //=> 'ABC'
+ *
+ * {{chop "-ABC-"}}
+ * //=> 'ABC'
+ *
+ * {{chop " ABC "}}
+ * //=> 'ABC'
+ * ```
+ *
+ * @name .chop
+ * @param {String} `string` The string to chop.
+ * @return {String}
+ * @api public
+ */
+
+helpers.chop = function(str) {
+ return utils.chop(str);
+};
+
+/**
+ * dash-case the characters in `string`. Replaces non-word
+ * characters and periods with hyphens.
+ *
+ * ```js
+ * {{dashcase "a-b-c d_e"}}
+ * //=> 'a-b-c-d-e'
+ * ```
+ *
+ * @param {String} `string`
+ * @return {String}
+ * @api public
+ */
+
+helpers.dashcase = function(str) {
+ return utils.changecase(str, function(ch) {
+ return '-' + ch;
+ });
+};
+
+/**
+ * dot.case the characters in `string`.
+ *
+ * ```js
+ * {{dotcase "a-b-c d_e"}}
+ * //=> 'a.b.c.d.e'
+ * ```
+ *
+ * @param {String} `string`
+ * @return {String}
+ * @api public
+ */
+
+helpers.dotcase = function(str) {
+ return utils.changecase(str, function(ch) {
+ return '.' + ch;
+ });
+};
+
+/**
+ * Replace spaces in a string with hyphens.
+ *
+ * ```handlebars
+ * {{hyphenate "foo bar baz qux"}}
+ * //=> "foo-bar-baz-qux"
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.hyphenate = function(str) {
+ if (str && typeof str === 'string') {
+ return str.split(' ').join('-');
+ }
+};
+
+/**
+ * Return true if `value` is a string.
+ *
+ * ```handlebars
+ * {{isString "foo"}}
+ * //=> 'true'
+ * ```
+ * @param {String} `value`
+ * @return {Boolean}
+ * @api public
+ */
+
+helpers.isString = function(value) {
+ return utils.isString(value);
+};
+
+/**
+ * Lowercase all characters in the given string.
+ *
+ * ```handlebars
+ * {{lowercase "Foo BAR baZ"}}
+ * //=> 'foo bar baz'
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.lowercase = function(str) {
+ if (str && typeof str === 'string') {
+ return str.toLowerCase();
+ }
+};
+
+/**
+ * Return the number of occurrences of `substring` within the
+ * given `string`.
+ *
+ * ```handlebars
+ * {{occurrences "foo bar foo bar baz" "foo"}}
+ * //=> 2
+ * ```
+ * @param {String} `str`
+ * @param {String} `substring`
+ * @return {Number} Number of occurrences
+ * @api public
+ */
+
+helpers.occurrences = function(str, substring) {
+ if (str && typeof str === 'string') {
+ var len = substring.length;
+ var pos = 0;
+ var n = 0;
+
+ while ((pos = str.indexOf(substring, pos)) > -1) {
+ n++;
+ pos += len;
+ }
+ return n;
+ }
+};
+
+/**
+ * PascalCase the characters in `string`.
+ *
+ * ```js
+ * {{pascalcase "foo bar baz"}}
+ * //=> 'FooBarBaz'
+ * ```
+ *
+ * @name .pascalcase
+ * @param {String} `string`
+ * @return {String}
+ * @api public
+ */
+
+helpers.pascalcase = function(str) {
+ str = utils.changecase(str, function(ch) {
+ return ch.toUpperCase();
+ });
+ return str.charAt(0).toUpperCase()
+ + str.slice(1);
+};
+
+/**
+ * path/case the characters in `string`.
+ *
+ * ```js
+ * {{pathcase "a-b-c d_e"}}
+ * //=> 'a/b/c/d/e'
+ * ```
+ *
+ * @param {String} `string`
+ * @return {String}
+ * @api public
+ */
+
+helpers.pathcase = function(str) {
+ return utils.changecase(str, function(ch) {
+ return '/' + ch;
+ });
+};
+
+/**
+ * Replace spaces in the given string with pluses.
+ *
+ * ```handlebars
+ * {{plusify "foo bar baz"}}
+ * //=> 'foo+bar+baz'
+ * ```
+ * @param {String} `str` The input string
+ * @return {String} Input string with spaces replaced by plus signs
+ * @source Stephen Way
+ * @api public
+ */
+
+helpers.plusify = function(str) {
+ if (str && typeof str === 'string') {
+ return str.split(' ').join('+');
+ }
+};
+
+/**
+ * Reverse a string.
+ *
+ * ```handlebars
+ * {{reverse "abcde"}}
+ * //=> 'edcba'
+ * ```
+ * @name .reverse
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.reverse = function(str) {
+ if (str && typeof str === 'string') {
+ return str.split('').reverse().join('');
+ }
+};
+
+/**
+ * Replace all occurrences of `a` with `b`.
+ *
+ * ```handlebars
+ * {{replace "a b a b a b" "a" "z"}}
+ * //=> 'z b z b z b'
+ * ```
+ * @param {String} `str`
+ * @param {String} `a`
+ * @param {String} `b`
+ * @return {String}
+ * @api public
+ */
+
+helpers.replace = function(str, a, b) {
+ if (str && typeof str === 'string') {
+ if (!a || typeof a !== 'string') return str;
+ if (!b || typeof b !== 'string') b = '';
+ return str.split(a).join(b);
+ }
+};
+
+/**
+ * Sentence case the given string
+ *
+ * ```handlebars
+ * {{sentence "hello world. goodbye world."}}
+ * //=> 'Hello world. Goodbye world.'
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.sentence = function(str) {
+ if (str && typeof str === 'string') {
+ var re = /((?:\S[^\.\?\!]*)[\.\?\!]*)/g;
+ return str.replace(re, function(txt) {
+ return txt.charAt(0).toUpperCase()
+ + txt.substr(1).toLowerCase();
+ });
+ }
+};
+
+/**
+ * snake_case the characters in the given `string`.
+ *
+ * ```js
+ * {{snakecase "a-b-c d_e"}}
+ * //=> 'a_b_c_d_e'
+ * ```
+ *
+ * @param {String} `string`
+ * @return {String}
+ * @api public
+ */
+
+helpers.snakecase = function(str) {
+ return utils.changecase(str, function(ch) {
+ return '_' + ch;
+ });
+};
+
+/**
+ * Split `string` by the given `character`.
+ *
+ * ```js
+ * {{split "a,b,c" ","}}
+ * //=> ['a', 'b', 'c']
+ * ```
+ *
+ * @param {String} `string` The string to split.
+ * @return {String} `character` Default is `,`
+ * @api public
+ */
+
+helpers.split = function(str, ch) {
+ if (!helpers.isString(str)) return '';
+ if (typeof ch !== 'string') ch = ',';
+ return str.split(ch);
+};
+
+/**
+ * Tests whether a string begins with the given prefix.
+ *
+ * ```handlebars
+ * {{#startsWith "Goodbye" "Hello, world!"}}
+ * Whoops
+ * {{else}}
+ * Bro, do you even hello world?
+ * {{/startsWith}}
+ * ```
+ * @param {String} `prefix`
+ * @param {String} `testString`
+ * @param {String} `options`
+ * @contributor Dan Fox
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.startsWith = function(prefix, str, options) {
+ var args = [].slice.call(arguments);
+ options = args.pop();
+ if (str && typeof str === 'string') {
+ if (str.indexOf(prefix) === 0) {
+ return options.fn(this);
+ }
+ }
+ if (typeof options.inverse === 'function') {
+ return options.inverse(this);
+ }
+ return '';
+};
+
+/**
+ * Title case the given string.
+ *
+ * ```handlebars
+ * {{titleize "this is title case"}}
+ * //=> 'This Is Title Case'
+ * ```
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.titleize = function(str) {
+ if (str && typeof str === 'string') {
+ var title = str.replace(/[ \-_]+/g, ' ');
+ var words = title.match(/\w+/g);
+ var len = words.length;
+ var res = [];
+ var i = 0;
+ while (len--) {
+ var word = words[i++];
+ res.push(exports.capitalize(word));
+ }
+ return res.join(' ');
+ }
+};
+
+/**
+ * Removes extraneous whitespace from the beginning and end
+ * of a string.
+ *
+ * ```js
+ * {{trim " ABC "}}
+ * //=> 'ABC'
+ * ```
+ *
+ * @name .trim
+ * @param {String} `string` The string to trim.
+ * @return {String}
+ * @api public
+ */
+
+helpers.trim = function(str) {
+ if (!helpers.isString(str)) return '';
+ return str.trim();
+};
+
+/**
+ * Uppercase all of the characters in the given string. If used as a
+ * block helper it will uppercase the entire block. This helper
+ * does not support inverse blocks.
+ *
+ * @name .uppercase
+ * @related capitalize capitalizeAll
+ * @param {String} `str` The string to uppercase
+ * @param {Object} `options` Handlebars options object
+ * @return {String}
+ * @block
+ * @api public
+ */
+
+helpers.uppercase = function(str, options) {
+ if (str && typeof str === 'string') {
+ return str.toUpperCase();
+ } else {
+ options = str;
+ }
+ if (typeof options === 'object' && options.fn) {
+ return options.fn(this).toUpperCase();
+ }
+ return '';
+};
diff --git a/helpers/3p/url.js b/helpers/3p/url.js
new file mode 100644
index 00000000..d7a61f80
--- /dev/null
+++ b/helpers/3p/url.js
@@ -0,0 +1,98 @@
+'use strict';
+
+const Url = require('url-parse');
+
+/**
+ * Expose `helpers`
+ */
+
+var helpers = {};
+
+/**
+ * Encodes a Uniform Resource Identifier (URI) component
+ * by replacing each instance of certain characters by
+ * one, two, three, or four escape sequences representing
+ * the UTF-8 encoding of the character.
+ *
+ * @param {String} `str` The un-encoded string
+ * @return {String} The endcoded string
+ * @api public
+ */
+
+helpers.encodeURI = function (str) {
+ return encodeURIComponent(str);
+};
+
+/**
+ * Decode a Uniform Resource Identifier (URI) component.
+ *
+ * @param {String} `str`
+ * @return {String}
+ * @api public
+ */
+
+helpers.decodeURI = function (str) {
+ return decodeURIComponent(str);
+};
+
+/**
+ * Take a base URL, and a href URL, and resolve them as a
+ * browser would for an anchor tag.
+ *
+ * FIXME not sure what this is doing
+ *
+ * @param {String} `base`
+ * @param {String} `href`
+ * @return {String}
+ * @api public
+ */
+
+helpers.urlResolve = function (base, href) {
+ return Url.resolve(base, href);
+};
+
+/**
+ * Parses a `url` string into an object.
+ *
+ * @param {String} `str` URL string
+ * @return {String} Returns stringified JSON
+ * @api public
+ */
+
+helpers.urlParse = function (str) {
+ return new Url(str);
+};
+
+/**
+ * Strip the query string from a `url`.
+ *
+ * @name .stripQuerystring
+ * @param {String} `url`
+ * @return {String} the url without the queryString
+ * @api public
+ */
+
+helpers.stripQuerystring = function (url) {
+ return url.split('?')[0];
+};
+
+/**
+ * Strip protocol from a `url`.
+ *
+ * Useful for displaying media that may have an 'http' protocol
+ * on secure connections. Will change 'http://foo.bar to `//foo.bar`
+ *
+ * @name .stripProtocol
+ * @param {String} `str`
+ * @return {String} the url with http protocol stripped
+ * @api public
+ */
+
+helpers.stripProtocol = function (str) {
+ var parsed = new Url(str);
+ delete parsed.protocol;
+ return parsed.format();
+};
+
+
+module.exports = helpers;
\ No newline at end of file
diff --git a/helpers/3p/utils/html.js b/helpers/3p/utils/html.js
new file mode 100644
index 00000000..9681ac09
--- /dev/null
+++ b/helpers/3p/utils/html.js
@@ -0,0 +1,78 @@
+'use strict';
+
+var striptags = require('striptags');
+var typeOf = require('kind-of');
+var utils = require('./');
+
+/**
+ * Expose `utils`
+ */
+
+var html = module.exports;
+
+/**
+ * Remove extra newlines from HTML, respect indentation.
+ *
+ * @param {String} html
+ * @return {String}
+ * @api public
+ */
+
+html.condense = function(str) {
+ return str.replace(/(\r\n|\r|\n|\u2028|\u2029) {2,}/g, '\n');
+};
+
+/**
+ * Add a single newline above code comments in HTML
+ *
+ * @param {String} `html`
+ * @return {String}
+ * @api public
+ */
+
+html.padcomments = function(str) {
+ return str.replace(/(\s*