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 (''; +}; + +/** + * 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 '
    1. ' + item + '
    2. '; + }).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*