255 lines
11 KiB
JavaScript
255 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
var transformers = require('./transformers');
|
|
|
|
var REGEXP_INTEGER = /^\d+$/;
|
|
|
|
var optionNames = ['transformations', 'propPath', 'dictPath', 'force'];
|
|
|
|
/**
|
|
* @classdesc This object holds a list of transformations used by {@link Synonomous.prototype.getSynonyms} and {@link Synonomous.prototype.decorateList}.
|
|
*
|
|
* Additional transformer functions may be mixed into the prototype (or added to an instance).
|
|
*
|
|
* @param {object} [options]
|
|
* @param {string[]} [options.transformations] - If omitted, {@link Synonomous.prototype.transformations} serves as a default.
|
|
* @param {string|string[]} [options.propPath] - If omitted, {@link Synonomous.prototype.propPath} serves as a default.
|
|
* @param {string|string[]} [options.dictPath] - If omitted, {@link Synonomous.prototype.dictPath} serves as a default.
|
|
* @param {boolean} [options.force=false] - If truthy, new property values override existing values; else new values are discarded.
|
|
* @constructor
|
|
*/
|
|
function Synonomous(options) {
|
|
if (options) {
|
|
optionNames.forEach(function(key) {
|
|
if (options[key]) {
|
|
this[key] = options[key];
|
|
}
|
|
}, this);
|
|
}
|
|
}
|
|
|
|
Synonomous.prototype = {
|
|
constructor: Synonomous,
|
|
|
|
/**
|
|
* @summary Default list of active registered transformations by _or_ an object whose keys name the transformations.
|
|
* @desc Used by {@link Synonomous.prototype.getSynonyms} and {@link Synonomous.prototype.decorateList}.
|
|
* When an object, the former just uses the keys, ignoring the values, while the latter uses both the keys and the values.
|
|
*
|
|
* An override may be defined on the instance, easily done by supplying as a constructor option.
|
|
*
|
|
* This is a global default; mutate only if you want to change the default for all your instances.
|
|
* @see {@link Synonomous.prototype.verbatim}
|
|
* @see {@link Synonomous.prototype.toCamelCase}
|
|
* @default
|
|
* @type {string[]|object}
|
|
* @memberOf Synonomous#
|
|
*/
|
|
transformations: [
|
|
'verbatim',
|
|
'toCamelCase'
|
|
],
|
|
|
|
/**
|
|
* @summary Drill down path for name to make synonyms of.
|
|
* @desc Used by {@link Synonomous.prototype.decorateList}.
|
|
*
|
|
* This is the default property dot-path within each list element to find the value to make synonyms of.
|
|
* If undefined (and no temporary override is given in the call to `decorateList`),
|
|
* the element value itself (coerced to a string) is used to make the synonyms.
|
|
*
|
|
* The setter accepts any falsy value to undefine; or a string of dot-separated parts; or an array of parts.
|
|
*
|
|
* The getter always returns an array with a `toString` override that returns dot-separated string when coerced to a string.
|
|
*
|
|
* An override may be defined on the instance, easily done by supplying as a constructor option.
|
|
*
|
|
* The global default for all instances can be reset using the setter with the prototype as the execution context,
|
|
* _e.g._ `Synonomous.prototype.propPath = newValue;`.
|
|
*
|
|
* @type {undefined|string|string[]}
|
|
* @memberOf Synonomous#
|
|
*/
|
|
set propPath(crumbs) {
|
|
this._propPath = newBreadcrumbs(crumbs);
|
|
},
|
|
get propPath() {
|
|
return this._propPath;
|
|
},
|
|
_propPath: ['name'], // default for all instances
|
|
|
|
/**
|
|
* @summary Default path to property to decorate; or undefined to decorate the object itself.
|
|
* @desc Used by {@link Synonomous.prototype.decorate} and {@link Synonomous.prototype.decorateList}.
|
|
*
|
|
* The setter accepts any falsy value to undefine; or a string of dot-separated parts; or an array of parts.
|
|
*
|
|
* The getter always returns an array with a `toString` override that returns dot-separated string when coerced to a string.
|
|
*
|
|
* If undefined, decorations are placed in `list[this.dictPath[0]][this.dictPath[1]][etc]`; else decorations are placed directly on `list` itself.
|
|
*
|
|
* An override may be defined on the instance, easily done by supplying to as a constructor option.
|
|
*
|
|
* The global default for all instances can be reset using the setter with the prototype as the execution context,
|
|
* _e.g._ `Synonomous.prototype.dictPath = newValue;`.
|
|
*
|
|
* @type {undefined|string|string[]}
|
|
* @memberOf Synonomous#
|
|
*/
|
|
set dictPath(crumbs) {
|
|
this._dictPath = newBreadcrumbs(crumbs);
|
|
},
|
|
get dictPath() {
|
|
return this._dictPath;
|
|
},
|
|
_dictPath: [], // default for all instances, settable by Synonomous.prototype.dictPath setter
|
|
|
|
/**
|
|
* If `name` is a string and non-blank, returns an array containing unique non-blank synonyms of `name` generated by the transformer functions named in `this.transformations`.
|
|
* @param {string} name - String to make synonyms of.
|
|
* @parma {string[]|object} transformations - When provided, temporarily overrides `this.transformations`.
|
|
* @memberOf Synonomous#
|
|
*/
|
|
getSynonyms: function(name, transformations) {
|
|
var synonyms = [];
|
|
if (typeof name === 'string' && name) {
|
|
transformations = transformations || this.transformations;
|
|
if (!Array.isArray(transformations)) {
|
|
transformations = Object.keys(transformations);
|
|
}
|
|
transformations.forEach(function(key) {
|
|
if (typeof transformers[key] !== 'function') {
|
|
throw new ReferenceError('Unknown transformer "' + key + '"');
|
|
}
|
|
var synonym = transformers[key](name);
|
|
if (synonym !== '' && !(synonym in synonyms)) {
|
|
synonyms.push(synonym);
|
|
}
|
|
});
|
|
}
|
|
return synonyms;
|
|
},
|
|
|
|
/**
|
|
* Decorate an object `obj` with properties named in `propNames` all referencing `item`.
|
|
* @param {object} obj - The object to decorate. If `this.dictPath` is defined, then decorate `obj[this.dictPath]` instead (created as needed).
|
|
*
|
|
* @param {string[]} propNames
|
|
* @param item
|
|
* @returns {object} `obj`, now with additional properties (possibly)
|
|
*/
|
|
decorate: function(obj, propNames, item) {
|
|
var drilldownContext = drilldown(obj, this.dictPath),
|
|
decoratingObjectItself = drilldownContext === obj,
|
|
force = this.force;
|
|
|
|
propNames.forEach(function(propName) {
|
|
if (
|
|
!(decoratingObjectItself && REGEXP_INTEGER.test(propName)) &&
|
|
(force || !(propName in drilldownContext))
|
|
) {
|
|
drilldownContext[propName] = item;
|
|
}
|
|
});
|
|
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* @summary Add dictionary synonyms to an array.
|
|
*
|
|
* @desc Adds synonyms for a single element (`index`) or the entire array, based on a given property of each element (`propPath`) or the element itself.
|
|
*
|
|
* That is, each element is either itself converted to a string; or is an object with a property named by following `propPath` which is converted to a string.
|
|
*
|
|
* For each element, all transformers named in `this.transformations` are run on that string.
|
|
* * _When `this.transformations` is an array:_
|
|
* **Create dictionary entries (synonyms) for the element.**
|
|
* Specifically: All the resulting unique non-blank "synonyms" are added as properties to the array with the value of the property being a reference to the element (if it was an object) or a copy of the element (if it was a string), subject to the following rules:
|
|
* 1. Duplicate synonyms are not added (unless `this.force` is truthy).
|
|
* 2. Blank synonyms are not added.
|
|
* 3. Integer synonyms are not added because they are indistinguishable from and may clash with array indexes.
|
|
* * _When `this.transformations` is an non-array object:_
|
|
* **Create a new property inside the element for each transformation.**
|
|
* Specifically: The keys of `this.transformations` name the transformers. The values are dot-paths (dot-separated-strings or arrays) to properties inside each element, set to the string returned by the transformer named by the key.
|
|
*
|
|
* @param {number} [index] - Index of element of `list` to add synonyms for. If omitted:
|
|
* 1. Adds synonyms for all elements of `list`.
|
|
* 2. `list` and `propPath` are promoted to the 1st and 2nd parameter positions, respectively.
|
|
* @param {(string|Object.<string, string>)[]} list - Array whose element(s) to make synonyms of _and_ the object to decorate. If `this.dictPath` is defined, then decorate `list[this.dictPath]` instead (created as needed).
|
|
* @param {string} [propPath=this.propPath] - Name of the property in each element of `list` to make synonyms of. If defined _and_ list element is an object, adds synonyms of `list[propPath]` as string; else adds synonyms of the list element itself as string.
|
|
* @returns {Array} `list`
|
|
* @memberOf Synonomous#
|
|
*/
|
|
decorateList: function(index, list, propPath) {
|
|
var elements;
|
|
if (typeof index === 'number') {
|
|
elements = [list[index]];
|
|
} else {
|
|
// promote args
|
|
list = elements = arguments[0];
|
|
propPath = arguments[1];
|
|
}
|
|
propPath = propPath ? newBreadcrumbs(propPath) : this.propPath;
|
|
elements.forEach(function(item) {
|
|
var value = propPath !== undefined && typeof item === 'object' ? drilldown(item, propPath) : item;
|
|
if (Array.isArray(this.transformations)) {
|
|
var synonyms = this.getSynonyms(value);
|
|
this.decorate(list, synonyms, item);
|
|
} else {
|
|
Object.keys(this.transformations).forEach(injectTransformedValueIntoItem.bind(this, item, value));
|
|
}
|
|
}, this);
|
|
return list;
|
|
}
|
|
};
|
|
|
|
|
|
// a.k.a.'s:
|
|
Synonomous.prototype.decorateObject = Synonomous.prototype.decorate;
|
|
Synonomous.prototype.decorateArray = Synonomous.prototype.decorateList;
|
|
|
|
|
|
function drilldown(collection, breadcrumbs) {
|
|
return breadcrumbs.reduce(function(result, crumb) {
|
|
return result[crumb] || (result[crumb] = Object.create(null));
|
|
}, collection);
|
|
}
|
|
|
|
function newBreadcrumbs(crumbs) {
|
|
if (!crumbs) {
|
|
crumbs = [];
|
|
} else if (Array.isArray(crumbs)) {
|
|
crumbs = crumbs.slice();
|
|
} else {
|
|
crumbs = (crumbs + '').split('.');
|
|
}
|
|
|
|
crumbs.toString = crumbsToString;
|
|
|
|
return crumbs;
|
|
}
|
|
|
|
/**
|
|
* @this {Array}
|
|
* @returns {string}
|
|
*/
|
|
function crumbsToString() {
|
|
return this.join('.');
|
|
}
|
|
|
|
function injectTransformedValueIntoItem(item, value, transformation) {
|
|
var transformer = transformers[transformation],
|
|
path = this.transformations[transformation],
|
|
pathList = Array.isArray(path) ? path.slice() : path.split('.'),
|
|
propName = pathList.splice(pathList.length - 1, 1)[0],
|
|
drillDownContext = drilldown(item, pathList);
|
|
|
|
if (this.force || !(propName in drillDownContext)) {
|
|
drillDownContext[propName] = transformer(value);
|
|
}
|
|
}
|
|
|
|
|
|
module.exports = Synonomous;
|