Skip to content

Commit

Permalink
Refactoring to avoid creating the "replaceP"-function multiple times …
Browse files Browse the repository at this point in the history
…(createrMarkerClass-function with Promise-constructor as argument)

Include extract "lib/*"-files into
Extract utility functions to separate file.
  • Loading branch information
nknapp committed Aug 30, 2016
1 parent 3550f1d commit 2ec5c2c
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 172 deletions.
180 changes: 10 additions & 170 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
'use strict'

var deepAplus = require('deep-aplus')
var replaceP = require('./lib/replaceP')
var createMarkers = require('./lib/markers')
var utils = require('./lib/utils')

// Basic utility functions
var values = utils.values
var wrap = utils.wrap
var mapValues = utils.mapValues
var anyApplies = utils.anyApplies
var isPromiseAlike = utils.isPromiseAlike

module.exports = promisedHandlebars

Expand Down Expand Up @@ -39,6 +47,7 @@ function promisedHandlebars (Handlebars, options) {
}

var deep = deepAplus(Promise)
var Markers = createMarkers(Promise)

var engine = Handlebars.create()
var markers = null
Expand Down Expand Up @@ -136,172 +145,3 @@ function promisedHandlebars (Handlebars, options) {
return engine
}

/**
* Wrap a function with a wrapper function
* @param {function} fn the original function
* @param {function(function,array)} wrapperFunction the wrapper-function
* receiving `fn` as first parameter and the arguments as second
* @returns {function} a function that calls the wrapper with
* the original function and the arguments
*/
function wrap (fn, wrapperFunction) {
return function () {
return wrapperFunction.call(this, fn, toArray(arguments))
}
}

/**
* A class the handles the creation and resolution of markers in the Handlebars output.
* Markes are used as placeholders in the output string for promises returned by helpers.
* They are replaced as soon as the promises are resolved.
* @param {Handlebars} engine a Handlebars instance (needed for the `escapeExpression` function)
* @param {string} prefix the prefix to identify placeholders (this prefix should never occur in the template).
* @constructor
*/
function Markers (engine, prefix) {
/**
* This array stores the promises created in the current event-loop cycle
* @type {Promise[]}
*/
this.promiseStore = []
this.engine = engine
this.prefix = prefix
// one line from substack's quotemeta-package
var placeHolderRegexEscaped = String(this.prefix).replace(/(\W)/g, '\\$1')
this.regex = new RegExp(placeHolderRegexEscaped + '(\\d+)(>|>)', 'g')
this.replaceP = replaceP(engine.Promise)
}

/**
* Add a promise the the store and return a placeholder.
* A placeholder consists of
* * The configured prefix (or \u0001), followed by
* * the index in the array
* * ">"
* @param {Promise} promise the promise
* @return {Promise} a new promise with a toString-method returning the placeholder
*/
Markers.prototype.asMarker = function asMarker (promise) {
// The placeholder: "prefix" for identification, index of promise in the store for retrieval, '>' for escaping
var placeholder = this.prefix + this.promiseStore.length + '>'
// Create a new promise, don't modify the input
var result = this.engine.Promise.resolve(promise)
result.toString = function () {
return placeholder
}
this.promiseStore.push(promise)
return result
}

/**
* Replace the placeholder found in a string by the resolved promise values.
* The input may be Promise, in which case it will be resolved first.
* Non-string values are returned directly since they cannot contain placeholders.
* String values are search for placeholders, which are then replaced by their resolved values.
* If the '>' part of the placeholder has been escaped (i.e. as '>') the resolved value
* will be escaped as well.
*
* @param {Promise<any>} input the string with placeholders
* @return {Promise<string>} a promise for the string with resolved placeholders
*/
Markers.prototype.resolve = function resolve (input) {
var self = this
return this.engine.Promise.resolve(input).then(function (output) {
if (typeof output !== 'string') {
// Make sure that non-string values (e.g. numbers) are not converted to a string.
return output
}
return self.engine.Promise.all(self.promiseStore)
.then(function (promiseResults) {
/**
* Replace placeholders in a string. Looks for placeholders
* in the replacement string recursively.
* @param {string|Promise|Handlebars.SafeString} string
* @returns {Promise<string>}
*/
function replacePlaceholdersRecursivelyIn (string) {
if (isPromiseAlike(string)) {
return string.then(function (string) {
return replacePlaceholdersRecursivelyIn(string)
})
}
if (typeof string.toHTML === 'function' && string.string) {
// This is a Handlebars.SafeString or something like it
return replacePlaceholdersRecursivelyIn(string.string)
}

// Must be a string, or something that can be converted to a string
return self.replaceP(String(string), self.regex, function (match, index, gt) {
// Check whether promise result must be escaped
var resolvedValue = promiseResults[index]
var result = gt === '>' ? resolvedValue : self.engine.escapeExpression(resolvedValue)
return replacePlaceholdersRecursivelyIn(result)
})
}

// Promises are fulfilled. Insert real values into the result.
return replacePlaceholdersRecursivelyIn(output)
})
})
}

/**
* Apply the mapFn to all values of the object and return a new object with the applied values
* @param obj the input object
* @param {function(any, string, object): any} mapFn the map function (receives the value, the key and the whole object as parameter)
* @returns {object} an object with the same keys as the input object
*/
function mapValues (obj, mapFn) {
return Object.keys(obj).reduce(function (result, key) {
result[key] = mapFn(obj[key], key, obj)
return result
}, {})
}

/**
* Return the values of the object
* @param {object} obj an object
* @returns {Array} the values of the object
*/
function values (obj) {
return Object.keys(obj).map(function (key) {
return obj[key]
})
}

/**
* Check if the predicate is true for any element of the array
* @param {Array} array
* @param {function(any):boolean} predicate
* @returns {boolean}
*/
function anyApplies (array, predicate) {
for (var i = 0; i < array.length; i++) {
if (predicate(array[i])) {
return true
}
}
return false
}

/**
* Convert arrayLike-objects (like 'arguments') to an array
* @param arrayLike
* @returns {Array.<T>}
*/
function toArray (arrayLike) {
return Array.prototype.slice.call(arrayLike)
}

/**
* Test an object to see if it is a Promise
* @param {Object} obj The object to test
* @returns {Boolean} Whether it's a PromiseA like object
*/
function isPromiseAlike (obj) {
if (obj == null) {
return false
}
return (typeof obj === 'object') && (typeof obj.then === 'function')
}
117 changes: 117 additions & 0 deletions lib/markers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*!
* promised-handlebars <https://github.com/nknapp/promised-handlebars>
*
* Copyright (c) 2015 Nils Knappmeier.
* Released under the MIT license.
*/

'use strict'

var isPromiseAlike = require('./utils').isPromiseAlike
var createReplaceP = require('./replaceP')

/**
* Returns a `Markers` constructor that uses a specific Promise constructor
* @param Promise a Promise constructor
* @return {Markers} the Markers class
*/
module.exports = function createMarkersClass (Promise) {
var replaceP = createReplaceP(Promise)

/**
* A class the handles the creation and resolution of markers in the Handlebars output.
* Markes are used as placeholders in the output string for promises returned by helpers.
* They are replaced as soon as the promises are resolved.
* @param {Handlebars} engine a Handlebars instance (needed for the `escapeExpression` function)
* @param {string} prefix the prefix to identify placeholders (this prefix should never occur in the template).
* @constructor
*/
function Markers (engine, prefix) {
/**
* This array stores the promises created in the current event-loop cycle
* @type {Promise[]}
*/
this.promiseStore = []
this.engine = engine
this.prefix = prefix
// one line from substack's quotemeta-package
var placeHolderRegexEscaped = String(this.prefix).replace(/(\W)/g, '\\$1')
this.regex = new RegExp(placeHolderRegexEscaped + '(\\d+)(>|&gt;)', 'g')
}

/**
* Add a promise the the store and return a placeholder.
* A placeholder consists of
* * The configured prefix (or \u0001), followed by
* * the index in the array
* * ">"
* @param {Promise} promise the promise
* @return {Promise} a new promise with a toString-method returning the placeholder
*/
Markers.prototype.asMarker = function asMarker (promise) {
// The placeholder: "prefix" for identification, index of promise in the store for retrieval, '>' for escaping
var placeholder = this.prefix + this.promiseStore.length + '>'
// Create a new promise, don't modify the input
var result = Promise.resolve(promise)
result.toString = function () {
return placeholder
}
this.promiseStore.push(promise)
return result
}

/**
* Replace the placeholder found in a string by the resolved promise values.
* The input may be Promise, in which case it will be resolved first.
* Non-string values are returned directly since they cannot contain placeholders.
* String values are search for placeholders, which are then replaced by their resolved values.
* If the '>' part of the placeholder has been escaped (i.e. as '&gt;') the resolved value
* will be escaped as well.
*
* @param {Promise<any>} input the string with placeholders
* @return {Promise<string>} a promise for the string with resolved placeholders
*/
Markers.prototype.resolve = function resolve (input) {
var self = this
return Promise.resolve(input).then(function (output) {
if (typeof output !== 'string') {
// Make sure that non-string values (e.g. numbers) are not converted to a string.
return output
}
return Promise.all(self.promiseStore)
.then(function (promiseResults) {
/**
* Replace placeholders in a string. Looks for placeholders
* in the replacement string recursively.
* @param {string|Promise|Handlebars.SafeString} string
* @returns {Promise<string>}
*/
function replacePlaceholdersRecursivelyIn (string) {
if (isPromiseAlike(string)) {
return string.then(function (string) {
return replacePlaceholdersRecursivelyIn(string)
})
}
if (typeof string.toHTML === 'function' && string.string) {
// This is a Handlebars.SafeString or something like it
return replacePlaceholdersRecursivelyIn(string.string)
}

// Must be a string, or something that can be converted to a string
return replaceP(String(string), self.regex, function (match, index, gt) {
// Check whether promise result must be escaped
var resolvedValue = promiseResults[ index ]
var result = gt === '>' ? resolvedValue : self.engine.escapeExpression(resolvedValue)
return replacePlaceholdersRecursivelyIn(result)
})
}

// Promises are fulfilled. Insert real values into the result.
return replacePlaceholdersRecursivelyIn(output)
})
})
}

return Markers
}
4 changes: 3 additions & 1 deletion lib/replaceP.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
* Released under the MIT license.
*/

'use strict'

/**
* Creates a String.prototype.replace function with support for Promises using a specific Promise constructor.
*
* @param {function(new:Promise)} Promise a promise constructor
* @returns {function(string, RegExp, function)} an equivalent for String.prototype.replace which
* can handle promises
*/
module.exports = function (Promise) {
module.exports = function createReplaceP(Promise) {
var deepAPlus = require('deep-aplus')(Promise)
/**
* Similar to String.prototype.replace, but it returns a promise instead of a string
Expand Down

0 comments on commit 2ec5c2c

Please sign in to comment.