'use strict'
const PLACEHOLDER_START_SYMBOL = '{{'
const PLACEHOLDER_END_SYMBOL = '}}'
/** */
class StringInterpolator {
/**
* Returns true if the string contains a placeholder that can be interpolated
* @param {String} string the string to be interpolated
* @returns {boolean}
* @example
* const string = 'I got a new phone! It's a {{ phone.manufacture.name }}!'
*
* const canInterpolate = interpolator.canInterpolate(string)
* // canInterpolate => true
*/
canInterpolate(string) {
return string.includes(PLACEHOLDER_START_SYMBOL) && string.includes(PLACEHOLDER_END_SYMBOL)
}
/**
* Returns an object containing placeholders and static strings
* @param {String} string the string to be interpolated
* @returns {Object}
* @example
* const string = 'I got a new phone! It's a {{ phone.manufacture.name }}!'
*
* const canInterpolate = interpolator.interpolatables(string)
* // canInterpolate => true
*/
interpolatables(string) {
return this._tokenize(string)
}
/**
* Interpolates the given string by searching for placeholders/handlebars and resolving the values using
* the given context
* @param {String} string the string to be interpolated
* @param {Object} context the context the interpolated values are resolved against
* @returns {String} the interpolated String
* @example
* const string = 'I got a new phone! It's a {{ phone.manufacture.name }}!'
*
* const context = {
* phone: {
* manufacturer: {
* name: 'Pixus'
* }
* }
* }
*
* const propertyPath = 'phone.manufacturer.name'
* const value = interpolator.interpolate(propertyPath, context)
* // value => 'I got a new phone! It's a Pixus!'
*/
interpolate(string, context) {
const tokens = this._tokenize(string)
return this.interpolateWithTokens(tokens, context)
}
interpolateWithTokens(tokens, context) {
const staticTokens = tokens.statics
const interpolatableTokens = tokens.interpolatables
const result = []
const loopCount = Math.max(staticTokens.length, interpolatableTokens.length)
for(let i = 0, n = loopCount; i < n; i++) {
if (i < staticTokens.length) {
result.push(staticTokens[i])
}
if (i < interpolatableTokens.length) {
const value = this.valueFor(interpolatableTokens[i], context)
result.push(value)
}
}
return result.join('')
}
/**
* Extracts a property out of a given context. Properties can be looked up deeply by specifying the correct
* names of the objects all along the path to the value we want to get. Names have to be separated by dots.
* @param {String} propertyPath the path to lookup the value we want
* @param {Object} context the context the value is resolved against
* @returns {Any}
* @example
* const context = {
* phone: {
* manufacturer: {
* name: 'Sammy'
* }
* }
* }
*
* const propertyPath = 'phone.manufacturer.name'
* const value = interpolator.valueFor(propertyPath, context)
* // value => 'Sammy'
*/
valueFor(propertyPath, context) {
if (!propertyPath) {
return context
}
const splittedPath = propertyPath.split('.')
return splittedPath.reduce((previous, current) => {
return previous ? previous[current] : ''
}, context)
}
/** @private */
_tokenize(string) {
const statics = []
const interpolatables = []
let buffer = []
for (let i = 0, n = string.length; i<n; i++) {
const char = string.charAt(i)
const nextChar = string.charAt(i+1)
if (char === '{' && nextChar === '{') {
statics.push(buffer.join(''))
buffer = []
i++
} else if (char === '}' && nextChar === '}') {
interpolatables.push(buffer.join('').trim())
buffer = []
i++
} else {
buffer.push(char)
}
}
if (buffer.length) {
statics.push(buffer.join(''))
}
return { statics: statics, interpolatables: interpolatables }
}
}
module.exports = StringInterpolator