diff --git a/helpers/all.js b/helpers/all.js
index 7d294269..e7ce990d 100644
--- a/helpers/all.js
+++ b/helpers/all.js
@@ -1,6 +1,5 @@
'use strict';
-
-var _ = require('lodash');
+const isObject = require('../lib/utils/isObject');
/**
* Yield block only if all arguments are valid
@@ -9,39 +8,23 @@ var _ = require('lodash');
* {{#all items theme_settings.optionA theme_settings.optionB}} ... {{/all}}
*/
function helper(paper) {
- paper.handlebars.registerHelper('all', function () {
-
- var args = [], opts, result;
-
- // Translate arguments to array safely
- for (var i = 0; i < arguments.length; i++) {
- args.push(arguments[i]);
- }
-
- // Take the last argument (content) out of testing array
- opts = args.pop();
+ paper.handlebars.registerHelper('all', function (...args) {
+ const opts = args.pop();
+ let result;
// Check if all the arguments are valid / truthy
- result = _.all(args, function (arg) {
- if (_.isArray(arg)) {
+ result = args.every(arg => {
+ if (Array.isArray(arg)) {
return !!arg.length;
}
- // If an empty object is passed, arg is false
- else if (_.isEmpty(arg) && _.isObject(arg)) {
+ if (isObject(arg) && !Object.keys(arg).length) {
return false;
}
- // Everything else
- else {
- return !!arg;
- }
+ return !!arg;
});
// If everything was valid, then "all" condition satisfied
- if (result) {
- return opts.fn(this);
- } else {
- return opts.inverse(this);
- }
+ return result ? opts.fn(this) : opts.inverse(this);
});
}
diff --git a/helpers/any.js b/helpers/any.js
index 165863cb..db8d2d37 100644
--- a/helpers/any.js
+++ b/helpers/any.js
@@ -1,6 +1,6 @@
'use strict';
-
-var _ = require('lodash');
+const isObject = require('../lib/utils/isObject');
+const isMatch = require("../lib/utils/isMatch");
/**
* Yield block if any object within a collection matches supplied predicate
@@ -9,48 +9,29 @@ var _ = require('lodash');
* {{#any items selected=true}} ... {{/any}}
*/
function helper(paper) {
- paper.handlebars.registerHelper('any', function () {
-
- var args = [],
- opts,
- predicate,
- any;
-
- // Translate arguments to array safely
- for (var i = 0; i < arguments.length; i++) {
- args.push(arguments[i]);
- }
-
- // Take the last argument (content) out of testing array
- opts = args.pop();
- predicate = opts.hash;
-
- if (!_.isEmpty(predicate)) {
- // With options hash, we check the contents of first argument
- any = _.any(args[0], predicate);
+ paper.handlebars.registerHelper('any', function (...args) {
+ const opts = args.pop();
+ let any;
+
+ // With options hash, we check the contents of first argument
+ if (opts.hash && Object.keys(opts.hash).length) {
+ // This works fine for both arrays and objects
+ any = isObject(args[0]) && Object.values(args[0]).some(item => isMatch(item, opts.hash));
} else {
// DEPRECATED: Moved to #or helper
// Without options hash, we check all the arguments
- any = _.any(args, function (arg) {
- if (_.isArray(arg)) {
+ any = args.some(arg => {
+ if (Array.isArray(arg)) {
return !!arg.length;
}
- // If an empty object is passed, arg is false
- else if (_.isEmpty(arg) && _.isObject(arg)) {
+ if (isObject(arg) && !Object.keys(arg).length) {
return false;
}
- // Everything else
- else {
- return !!arg;
- }
+ return !!arg;
});
}
- if (any) {
- return opts.fn(this);
- }
-
- return opts.inverse(this);
+ return any ? opts.fn(this) : opts.inverse(this);
});
}
diff --git a/helpers/contains.js b/helpers/contains.js
index abb48fc2..ce94c8b8 100644
--- a/helpers/contains.js
+++ b/helpers/contains.js
@@ -1,6 +1,6 @@
'use strict';
-var _ = require('lodash');
+const includes = require('../lib/utils/includes');
/**
* Is any value included in a collection or a string?
@@ -10,17 +10,12 @@ var _ = require('lodash');
* {{#contains font_path "Roboto"}} ... {{/contains}}
*/
function helper(paper) {
- paper.handlebars.registerHelper('contains', function () {
- var args = Array.prototype.slice.call(arguments, 0, -1),
- options = _.last(arguments),
- contained = _.contains.apply(_, args);
+ paper.handlebars.registerHelper('contains', function (...args) {
+ const options = args.pop();
+ const contained = includes(...args);
// Yield block if true
- if (contained) {
- return options.fn(this);
- } else {
- return options.inverse(this);
- }
+ return contained ? options.fn(this) : options.inverse(this);
});
}
diff --git a/helpers/deprecated.js b/helpers/deprecated.js
index c4d7ff39..8672d9c2 100644
--- a/helpers/deprecated.js
+++ b/helpers/deprecated.js
@@ -1,10 +1,10 @@
'use strict';
-var _ = require('lodash');
+const pickBy = require("../lib/utils/pickBy");
function helper(paper) {
- paper.handlebars.registerHelper('pick', function () {
- return _.pick.apply(null, arguments);
+ paper.handlebars.registerHelper('pick', function (...args) {
+ return pickBy(...args);
});
/**
diff --git a/helpers/for.js b/helpers/for.js
index 686094e6..1935de03 100644
--- a/helpers/for.js
+++ b/helpers/for.js
@@ -1,28 +1,25 @@
'use strict';
-var _ = require('lodash');
+const isObject = require('../lib/utils/isObject');
function helper(paper) {
paper.handlebars.registerHelper('for', function (from, to, context) {
const options = arguments[arguments.length - 1];
const maxIterations = 100;
- var output = '';
+ let output = '';
function isOptions(obj) {
- return _.isObject(obj) && obj.fn;
+ return obj && obj.fn;
}
if (isOptions(to)) {
context = {};
to = from;
from = 1;
-
- } else if (isOptions(context)) {
- if (_.isObject(to)) {
- context = to;
- to = from;
- from = 1;
- }
+ } else if (isOptions(context) && isObject(to)) {
+ context = to;
+ to = from;
+ from = 1;
}
if (to < from) {
@@ -36,7 +33,7 @@ function helper(paper) {
to = from + maxIterations - 1;
}
- for (var i = from; i <= to; i++) {
+ for (let i = from; i <= to; i++) {
context.$index = i;
output += options.fn(context);
}
diff --git a/helpers/getImage.js b/helpers/getImage.js
index 78b51f45..2133cd35 100644
--- a/helpers/getImage.js
+++ b/helpers/getImage.js
@@ -1,6 +1,5 @@
'use strict';
-var _ = require('lodash');
const SafeString = require('handlebars').SafeString;
const common = require('./../lib/common');
@@ -9,18 +8,22 @@ function helper(paper) {
var sizeRegex = /^(\d+?)x(\d+?)$/g;
var settings = paper.themeSettings || {};
var presets = settings._images;
+ var isImageDataValid = image &&
+ typeof image.data === 'string' &&
+ common.isValidURL(image.data) &&
+ image.data.includes('{:size}')
var size;
var width;
var height;
- if (!_.isPlainObject(image) || !_.isString(image.data)
- || !common.isValidURL(image.data) || image.data.indexOf('{:size}') === -1) {
+ if (!isImageDataValid) {
// return empty string if not a valid image object
- defaultImageUrl = defaultImageUrl ? defaultImageUrl : '';
- return _.isString(image) ? image : defaultImageUrl;
+ return image && typeof image === 'string'
+ ? image
+ : (defaultImageUrl || '');
}
- if (_.isPlainObject(presets) && _.isPlainObject(presets[presetName])) {
+ if (presets && presets[presetName]) {
// If preset is one of the given presets in _images
width = parseInt(presets[presetName].width, 10) || 5120;
height = parseInt(presets[presetName].height, 10) || 5120;
@@ -42,6 +45,6 @@ function helper(paper) {
return new SafeString(image.data.replace('{:size}', size));
});
-};
+}
module.exports = helper;
diff --git a/helpers/limit.js b/helpers/limit.js
index 017b80c0..a03ad527 100644
--- a/helpers/limit.js
+++ b/helpers/limit.js
@@ -1,7 +1,5 @@
'use strict';
-var _ = require('lodash');
-
/**
* Limit an array to the second argument
*
@@ -10,11 +8,10 @@ var _ = require('lodash');
*/
function helper(paper) {
paper.handlebars.registerHelper('limit', function (data, limit) {
-
- if (_.isString(data)) {
+ if (typeof data === 'string') {
return data.substring(0, limit);
}
- if (!_.isArray(data)) {
+ if (!Array.isArray(data)) {
return [];
}
diff --git a/helpers/money.js b/helpers/money.js
index ff3163d8..3e1ddc2b 100644
--- a/helpers/money.js
+++ b/helpers/money.js
@@ -1,13 +1,12 @@
'use strict';
-var _ = require('lodash');
-
/**
* Format numbers
*
- * @param integer n: length of decimal
- * @param mixed s: thousands delimiter
- * @param mixed c: decimal delimiter
+ * @param {number} value
+ * @param {number} n - length of decimal
+ * @param {string} s - thousands delimiter
+ * @param {string} c - decimal delimiter
*/
function numberFormat(value, n, s, c) {
var re = '\\d(?=(\\d{3})+' + (n > 0 ? '\\D' : '$') + ')',
@@ -18,9 +17,9 @@ function numberFormat(value, n, s, c) {
function helper(paper) {
paper.handlebars.registerHelper('money', function (value) {
- var money = paper.settings.money;
+ const money = paper.settings.money;
- if (!_.isNumber(value)) {
+ if (typeof value !== 'number') {
return '';
}
diff --git a/helpers/or.js b/helpers/or.js
index 28425338..2c4fec90 100644
--- a/helpers/or.js
+++ b/helpers/or.js
@@ -1,6 +1,5 @@
'use strict';
-
-var _ = require('lodash');
+const isObject = require('../lib/utils/isObject');
/**
* Yield block if any object within a collection matches supplied predicate
@@ -9,39 +8,22 @@ var _ = require('lodash');
* {{#or 1 0 0 0 0 0}} ... {{/or}}
*/
function helper(paper) {
- paper.handlebars.registerHelper('or', function () {
- var args = [],
- opts,
- any;
-
- // Translate arguments to array safely
- for (var i = 0; i < arguments.length; i++) {
- args.push(arguments[i]);
- }
-
- // Take the last argument (content) out of testing array
- opts = args.pop();
+ paper.handlebars.registerHelper('or', function (...args) {
+ const opts = args.pop();
+ let any;
// Without options hash, we check all the arguments
- any = _.any(args, function (arg) {
- if (_.isArray(arg)) {
+ any = args.some(arg => {
+ if (Array.isArray(arg)) {
return !!arg.length;
}
- // If an empty object is passed, arg is false
- else if (_.isEmpty(arg) && _.isObject(arg)) {
+ if (isObject(arg) && !Object.keys(arg).length) {
return false;
}
- // Everything else
- else {
- return !!arg;
- }
+ return !!arg;
});
- if (any) {
- return opts.fn(this);
- }
-
- return opts.inverse(this);
+ return any ? opts.fn(this) : opts.inverse(this);
});
}
diff --git a/helpers/pluck.js b/helpers/pluck.js
index 64cdf5ae..8fc8860a 100644
--- a/helpers/pluck.js
+++ b/helpers/pluck.js
@@ -1,10 +1,8 @@
'use strict';
-var _ = require('lodash');
-
function helper(paper) {
paper.handlebars.registerHelper('pluck', function (collection, path) {
- return _.pluck(collection, path);
+ return collection.map(item => item[path])
});
}
diff --git a/helpers/resourceHints.js b/helpers/resourceHints.js
index ed857d68..633db8a2 100644
--- a/helpers/resourceHints.js
+++ b/helpers/resourceHints.js
@@ -1,6 +1,5 @@
'use strict';
-const _ = require('lodash');
const getFonts = require('../lib/fonts');
const fontResources = {
@@ -10,28 +9,26 @@ const fontResources = {
],
};
+function format(host) {
+ return ``;
+}
+
module.exports = function(paper) {
paper.handlebars.registerHelper('resourceHints', function() {
- function format(host) {
- return ``;
- }
-
- var hosts = [];
+ const hosts = [];
// Add cdn
- const cdnUrl = paper.settings['cdn_url'] || '';
- if (cdnUrl != '') {
- hosts.push(cdnUrl);
+ if (paper.settings['cdn_url']) {
+ hosts.push(paper.settings['cdn_url']);
}
// Add font providers
- const fontProviders = _.keys(getFonts(paper, 'providerLists'));
- _.each(fontProviders, function(provider) {
- if (typeof fontResources[provider] !== 'undefined') {
- hosts = hosts.concat(fontResources[provider]);
+ for (let provider of Object.keys(getFonts(paper, 'providerLists'))) {
+ if (fontResources[provider]) {
+ hosts.push(...fontResources[provider]);
}
- });
+ }
- return new paper.handlebars.SafeString(_.map(hosts, format).join(''));
+ return new paper.handlebars.SafeString(hosts.map(format).join(''));
});
}
diff --git a/helpers/stylesheet.js b/helpers/stylesheet.js
index 1d7d8ad0..6333f6fa 100644
--- a/helpers/stylesheet.js
+++ b/helpers/stylesheet.js
@@ -1,6 +1,6 @@
'use strict';
-const _ = require('lodash');
+const isObject = require("../lib/utils/isObject");
function helper(paper) {
paper.handlebars.registerHelper('stylesheet', function (assetPath) {
@@ -13,16 +13,18 @@ function helper(paper) {
const url = paper.cdnify(path);
- let attrs = { rel: 'stylesheet' };
+ let attrsObj = { rel: 'stylesheet' };
// check if there is any extra attribute
- if (_.isObject(options.hash)) {
- attrs = _.merge(attrs, options.hash);
+ if (isObject(options.hash)) {
+ attrsObj = { ...attrsObj, ...options.hash };
}
- attrs = _.map(attrs, (value, key) => `${key}="${value}"`).join( ' ');
+ const attrsString = Object.entries(attrsObj)
+ .map(([key, value]) => `${key}="${value}"`)
+ .join( ' ');
- return ``;
+ return ``;
});
}
diff --git a/helpers/thirdParty.js b/helpers/thirdParty.js
index 65e2b06a..eb9f171b 100644
--- a/helpers/thirdParty.js
+++ b/helpers/thirdParty.js
@@ -1,6 +1,6 @@
'use strict';
-const _ = require('lodash');
+const pickBy = require('../lib/utils/pickBy');
const helpers = require('handlebars-helpers');
const whitelist = [
{
@@ -143,7 +143,12 @@ const whitelist = [
];
function helper(paper) {
- whitelist.forEach(helper => paper.handlebars.registerHelper(_.pick(helpers[helper.name](), helper.include)));
+ for (const { name, include } of whitelist) {
+ const includeSet = new Set(include);
+ paper.handlebars.registerHelper(
+ pickBy(helpers[name](), (value, key) => includeSet.has(key))
+ )
+ }
}
module.exports = helper;
diff --git a/index.js b/index.js
index 09165918..07da1847 100644
--- a/index.js
+++ b/index.js
@@ -1,14 +1,14 @@
'use strict';
-var _ = require('lodash');
-var Translator = require('./lib/translator');
-var Logger = require('./lib/logger');
-var Path = require('path');
-var Fs = require('fs');
-var Handlebars = require('handlebars');
-var Async = require('async');
-var helpers = [];
-var handlebarsOptions = {
+const Translator = require('./lib/translator');
+const Logger = require('./lib/logger');
+const Path = require('path');
+const Fs = require('fs');
+const Handlebars = require('handlebars');
+const Async = require('async');
+
+const helpers = [];
+const handlebarsOptions = {
preventIndent: true
};
@@ -101,7 +101,7 @@ class Paper {
}
loadTheme(paths, acceptLanguage, done) {
- if (!_.isArray(paths)) {
+ if (!Array.isArray(paths)) {
paths = paths ? [paths] : [];
}
@@ -117,7 +117,7 @@ class Paper {
/**
* Load Partials/Templates
- * @param {Object} templates
+ * @param {Object} path
* @param {Function} callback
*/
loadTemplates(path, callback) {
@@ -128,13 +128,13 @@ class Paper {
return callback(error);
}
- _.each(templates, (precompiled, path) => {
- var template;
+ for (const [path, precompiled] of Object.entries(templates)) {
+ let template;
if (!this.handlebars.templates[path]) {
eval('template = ' + precompiled);
this.handlebars.templates[path] = this.handlebars.template(template);
}
- });
+ }
this.handlebars.partials = this.handlebars.templates;
@@ -144,13 +144,10 @@ class Paper {
getTemplateProcessor() {
return (templates) => {
- let precompiledTemplates = {};
-
- _.each(templates,(content, path) => {
+ return Object.entries(templates).reduce((precompiledTemplates, [path, content]) => {
precompiledTemplates[path] = this.handlebars.precompile(content, handlebarsOptions);
- });
-
- return precompiledTemplates;
+ return precompiledTemplates;
+ }, {});
}
}
@@ -160,9 +157,9 @@ class Paper {
* @return {Object}
*/
loadTemplatesSync(templates) {
- _.each(templates,(content, fileName) => {
+ for (const [fileName, content] of Object.entries(templates)) {
this.handlebars.templates[fileName] = this.handlebars.compile(content, handlebarsOptions);
- });
+ }
this.handlebars.partials = this.handlebars.templates;
@@ -171,7 +168,7 @@ class Paper {
/**
* @param {String} acceptLanguage
- * @param {Object} translations
+ * @param {Function} callback
*/
loadTranslations(acceptLanguage, callback) {
this.assembler.getTranslations((error, translations) => {
@@ -274,21 +271,18 @@ class Paper {
* @param {Object} context
* @return {String}
*/
- render(path, context) {
- let output;
-
- context = context || {};
+ render(path, context = {}) {
context.template = path;
if (this.translator) {
context.locale_name = this.translator.getLocale();
}
- output = this.handlebars.templates[path](context);
+ let output = this.handlebars.templates[path](context);
- _.each(this.decorators, function (decorator) {
+ for (const decorator of this.decorators) {
output = decorator(output);
- });
+ }
return output;
};
@@ -304,7 +298,7 @@ class Paper {
let output;
// Is an ajax request?
- if (data.remote || _.isArray(templatePath)) {
+ if (data.remote || Array.isArray(templatePath)) {
if (data.remote) {
data.context = Object.assign({}, data.context, data.remote_data);
@@ -313,7 +307,7 @@ class Paper {
// Is render_with ajax request?
if (templatePath) {
// if multiple render_with
- if (_.isArray(templatePath)) {
+ if (Array.isArray(templatePath)) {
// if templatePath is an array ( multiple templates using render_with option)
// compile all the template required files into a hash table
html = templatePath.reduce((table, file) => {
diff --git a/lib/fonts.js b/lib/fonts.js
index 720e4a8a..4b2d3bf5 100644
--- a/lib/fonts.js
+++ b/lib/fonts.js
@@ -1,13 +1,14 @@
'use strict';
-const _ = require('lodash');
+const mapValues = require("./utils/mapValues");
+
const fontKeyFormat = new RegExp(/\w+(-\w*)*-font$/);
const fontProviders = {
'Google': {
/**
* Parser for Google fonts
*
- * @param {Array} fonts - Array of fonts that might look like
+ * @param {string[]} fonts - Array of fonts that might look like
* Google_Open+Sans
* Google_Open+Sans_400
* Google_Open+Sans_400_sans
@@ -15,56 +16,39 @@ const fontProviders = {
* Google_Open+Sans_400,700italic
* Google_Open+Sans_400,700italic_sans
*
- * @returns {string}
+ * @returns {string[]}
*/
- parser: function(fonts) {
- var collection = [],
- familyHash = {};
-
- _.each(fonts, function fontsIterator(font) {
- var split = font.split('_'),
- familyKey = split[1], // Eg: Open+Sans
- weights = split[2]; // Eg: 400,700italic
+ parser(fonts) {
+ const familyHash = {};
- if (_.isEmpty(familyKey)) {
- return;
- }
+ for (const font of fonts) {
+ let [, familyKey, weights = ''] = font.split('_');
- if (_.isUndefined(weights)) {
- weights = '';
+ if (!familyKey) {
+ continue;
}
- if (!_.isArray(familyHash[familyKey])) {
- familyHash[familyKey] = [];
+ if (!familyHash[familyKey]) {
+ familyHash[familyKey] = new Set();
}
- weights = weights.split(',');
-
- familyHash[familyKey].push(weights);
- familyHash[familyKey] = _.uniq(_.flatten(familyHash[familyKey]));
- });
-
- _.each(familyHash, function fontHashIterator(weights, family) {
- collection.push(family + ':' + weights.join(','));
- });
+ weights.split(',').forEach(weight => familyHash[familyKey].add(weight));
+ }
- return collection;
+ return Object.entries(familyHash)
+ .map(([familyKey, weightsSet]) => familyKey + ':' + [...weightsSet].join(','));
},
- buildLink: function(fonts, fontDisplay) {
+ buildLink(fonts, fontDisplay) {
const displayTypes = ['auto', 'block', 'swap', 'fallback', 'optional'];
fontDisplay = displayTypes.includes(fontDisplay) ? fontDisplay : 'swap';
return ``;
},
- buildFontLoaderConfig: function(fonts) {
- function replaceSpaces(font) {
- return font.split('+').join(' ');
- }
-
+ buildFontLoaderConfig(fonts) {
return {
google: {
- families: _.map(fonts, replaceSpaces),
+ families: fonts.map(font => font.split('+').join(' ')),
}
};
},
@@ -74,61 +58,55 @@ const fontProviders = {
/**
* Get collection of fonts used in theme settings.
*
- * @param {Object} paper The paper instance
- * @param {string} format The desired return value. If format == 'providerLists', return an object with provider names for keys
+ * @param {Object} paper - The paper instance
+ * @param {string} format - The desired return value. If format == 'providerLists', return an object with provider names for keys
* and a list of fonts in the provider format as values, suitable for use with Web Font Loader. If format == 'linkElements',
* return a string containing elements to be directly inserted into the page. If format == 'webFontLoaderConfig', return an
* object that can be used to configure Web Font Loader.
- * @param {Object} options an optional object for additional configuration details
+ * @param {Object} [options] - an optional object for additional configuration details
* @returns {Object.|string}
*/
-module.exports = function(paper, format, options) {
+module.exports = function(paper, format, options = {}) {
// Collect font strings from theme settings
const collectedFonts = {};
- _.each(paper.themeSettings, function(value, key) {
+ for (const [key, value] of Object.entries(paper.themeSettings)) {
if (!fontKeyFormat.test(key)) {
- return;
+ continue;
}
- const splits = value.split('_');
- const provider = splits[0];
+ const [provider] = value.split('_');
- if (typeof fontProviders[provider] === 'undefined') {
- return;
+ if (!fontProviders[provider]) {
+ continue;
}
- if (typeof collectedFonts[provider] === 'undefined') {
+ if (!collectedFonts[provider]) {
collectedFonts[provider] = [];
}
collectedFonts[provider].push(value);
- });
+ }
// Parse font strings based on provider
- const parsedFonts = _.mapValues(collectedFonts, function(value, key) {
+ const parsedFonts = mapValues(collectedFonts, function(value, key) {
return fontProviders[key].parser(value);
});
// Format output based on requested format
switch(format) {
case 'linkElements':
- const formattedFonts = _.mapValues(parsedFonts, function(value, key) {
+ const formattedFonts = mapValues(parsedFonts, function(value, key) {
return fontProviders[key].buildLink(value, options.fontDisplay);
});
- return new paper.handlebars.SafeString(_.values(formattedFonts).join(''));
+ return new paper.handlebars.SafeString(Object.values(formattedFonts).join(''));
case 'webFontLoaderConfig':
// Build configs
- const configs = _.mapValues(parsedFonts, function(value, key) {
+ const configs = mapValues(parsedFonts, function(value, key) {
return fontProviders[key].buildFontLoaderConfig(value);
});
-
// Merge them
- const fontLoaderConfig = {};
- _.each(configs, function(config) {
- return Object.assign(fontLoaderConfig, config);
- });
- return fontLoaderConfig;
+ return Object.values(configs).reduce((res, config) => ({...res, ...config}), {});
case 'providerLists':
default:
diff --git a/lib/translator/filter.js b/lib/translator/filter.js
index 790da221..4c1ab3e6 100644
--- a/lib/translator/filter.js
+++ b/lib/translator/filter.js
@@ -4,22 +4,25 @@
* @module paper/lib/translator/filter
*/
-const _ = require('lodash');
+const pickBy = require('../utils/pickBy');
/**
- * Filter translation by key
+ * Filter translation and locales by keyFilter
* @param {Object.} language
* @param {string} keyFilter
* @returns {Object.}
*/
function filterByKey(language, keyFilter) {
- return _.transform(language, (result, value, key) => {
- if (key === 'translations' || key === 'locales') {
- result[key] = _.pick(value, (innerValue, innerKey) => innerKey.indexOf(keyFilter) === 0);
- } else {
- result[key] = value;
- }
- });
+ return Object.entries(language)
+ .reduce((result, [key, value]) => {
+ if (key === 'translations' || key === 'locales') {
+ result[key] = pickBy(value, (innerValue, innerKey) => innerKey.startsWith(keyFilter));
+ } else {
+ result[key] = language[key];
+ }
+
+ return result;
+ }, {});
}
module.exports = {
diff --git a/lib/translator/index.js b/lib/translator/index.js
index b0265172..41b7c787 100644
--- a/lib/translator/index.js
+++ b/lib/translator/index.js
@@ -4,7 +4,6 @@
* @module paper/lib/translator
*/
-const _ = require('lodash');
const MessageFormat = require('messageformat');
const Filter = require('./filter');
const LocaleParser = require('./locale-parser');
@@ -80,7 +79,7 @@ Translator.prototype.translate = function (key, parameters) {
return key;
}
- if (!_.isFunction(this._formatFunctions[key])) {
+ if (!(this._formatFunctions[key] instanceof Function)) {
this._formatFunctions[key] = this._compileTemplate(key);
}
diff --git a/lib/translator/locale-parser.js b/lib/translator/locale-parser.js
index 3822809c..0bee8b09 100644
--- a/lib/translator/locale-parser.js
+++ b/lib/translator/locale-parser.js
@@ -4,7 +4,6 @@
* @module paper/lib/translator/locale-parser
*/
-const _ = require('lodash');
const AcceptLanguageParser = require('accept-language-parser');
const MessageFormat = require('messageformat');
@@ -12,21 +11,13 @@ const MessageFormat = require('messageformat');
* Get preferred locale
* @param {string} acceptLanguage
* @param {Object} languages
+ * @param {string} defaultLocale
* @returns {string}
*/
function getPreferredLocale(acceptLanguage, languages, defaultLocale) {
- const locales = getLocales(acceptLanguage);
- let preferredLocale = defaultLocale;
+ const locales = getLocales(acceptLanguage).find((locale) => languages[locale]) || defaultLocale;
- _.each(locales, locale => {
- if (languages[locale]) {
- preferredLocale = locale;
-
- return false;
- }
- });
-
- return normalizeLocale(preferredLocale, defaultLocale);
+ return normalizeLocale(locales, defaultLocale);
}
/**
@@ -37,9 +28,7 @@ function getPreferredLocale(acceptLanguage, languages, defaultLocale) {
function getLocales(acceptLanguage) {
const localeObjects = AcceptLanguageParser.parse(acceptLanguage);
- const locales = _.map(localeObjects, localeObject => {
- return _.isString(localeObject.region) ? `${localeObject.code}-${localeObject.region}` : localeObject.code;
- });
+ const locales = localeObjects.map(({ region, code }) => region ? `${code}-${region}` : code);
// Safari sends only one language code, this is to have a default fallback in case we don't have that language
// As an example we may not have fr-FR so add fr to the header
@@ -53,6 +42,7 @@ function getLocales(acceptLanguage) {
* Normalize locale
* @private
* @param {string} locale
+ * @param {string} defaultLocale
* @returns {string}
*/
function normalizeLocale(locale, defaultLocale) {
diff --git a/lib/translator/transformer.js b/lib/translator/transformer.js
index d4adced7..e4572111 100644
--- a/lib/translator/transformer.js
+++ b/lib/translator/transformer.js
@@ -4,7 +4,7 @@
* @module paper/lib/translator/transformer
*/
-const _ = require('lodash');
+const isObject = require('../utils/isObject');
const Logger = require('../logger');
/**
@@ -25,15 +25,18 @@ function transform(allTranslations, defaultLocale, logger = Logger) {
* @param {Object} logger
*/
function flatten(allTranslations, logger = Logger) {
- return _.transform(allTranslations, (result, translation, locale) => {
- try {
- result[locale] = flattenObject(translation);
- } catch (err) {
- logger.error(`Failed to parse ${locale} - Error: ${err}`);
-
- result[locale] = {};
- }
- }, {});
+ return Object.entries(allTranslations)
+ .reduce((result, [locale, translation]) => {
+ try {
+ result[locale] = flattenObject(translation);
+ } catch (err) {
+ logger.error(`Failed to parse ${locale} - Error: ${err}`);
+
+ result[locale] = {};
+ }
+
+ return result;
+ }, {});
}
/**
@@ -43,29 +46,31 @@ function flatten(allTranslations, logger = Logger) {
* @returns {Object.} Language objects
*/
function cascade(allTranslations, defaultLocale) {
- return _.transform(allTranslations, (result, translations, locale) => {
- const regionCodes = locale.split('-');
-
- for (let regionIndex = regionCodes.length - 1; regionIndex >= 0; regionIndex--) {
- const parentLocale = getParentLocale(regionCodes, regionIndex, defaultLocale);
- const parentTranslations = allTranslations[parentLocale] || {};
- const translationKeys = _.union(Object.keys(parentTranslations), Object.keys(translations));
-
+ return Object.entries(allTranslations)
+ .reduce((result, [locale, translations]) => {
if (!result[locale]) {
result[locale] = { locale: locale, locales: {}, translations: {} };
}
- _.each(translationKeys, key => {
- if (translations[key]) {
- result[locale].locales[key] = locale;
- result[locale].translations[key] = translations[key];
- } else if (!result[locale].translations[key]) {
- result[locale].locales[key] = parentLocale;
- result[locale].translations[key] = parentTranslations[key];
- }
- });
- }
- }, {});
+ const regionCodes = locale.split('-');
+ for (let regionIndex = regionCodes.length - 1; regionIndex >= 0; regionIndex--) {
+ const parentLocale = getParentLocale(regionCodes, regionIndex, defaultLocale);
+ const parentTranslations = allTranslations[parentLocale] || {};
+
+ new Set(Object.keys(parentTranslations).concat(Object.keys(translations)))
+ .forEach((key) => {
+ if (translations[key]) {
+ result[locale].locales[key] = locale;
+ result[locale].translations[key] = translations[key];
+ } else if (!result[locale].translations[key]) {
+ result[locale].locales[key] = parentLocale;
+ result[locale].translations[key] = parentTranslations[key];
+ }
+ });
+ }
+
+ return result;
+ }, {});
}
/**
@@ -92,21 +97,19 @@ function getParentLocale(regionCodes, regionIndex, defaultLocale) {
* @param {string} [parentKey='']
* @returns {Object} Flatten object
*/
-function flattenObject(object, result, parentKey) {
- result = result || {};
- parentKey = parentKey || '';
+function flattenObject(object, result = {}, parentKey = '') {
+ return Object.entries(object)
+ .reduce((currentLayer, [key, innerValue]) => {
+ const resultKey = parentKey ? `${parentKey}.${key}` : key;
- _.forOwn(object, (value, key) => {
- const resultKey = parentKey ? `${parentKey}.${key}` : key;
-
- if (_.isObject(value)) {
- return flattenObject(value, result, resultKey);
- }
+ if (isObject(innerValue)) {
+ return flattenObject(innerValue, currentLayer, resultKey);
+ }
- result[resultKey] = value;
- });
+ currentLayer[resultKey] = innerValue;
- return result;
+ return currentLayer;
+ }, result);
}
module.exports = {
diff --git a/lib/utils/includes.js b/lib/utils/includes.js
new file mode 100644
index 00000000..f545726f
--- /dev/null
+++ b/lib/utils/includes.js
@@ -0,0 +1,46 @@
+const isString = require('./isString');
+const isObject = require('./isObject');
+
+/**
+ * Clone of https://lodash.com/docs/4.17.15#includes
+ *
+ * Checks if `value` is in `collection`. If `fromIndex` is negative, it is used as the offset
+ * from the end of `collection`.
+ *
+ * @param {Array|Object|string} collection - The collection to search.
+ * @param {any} target - The value to search for.
+ * @param {number} [fromIndex=0] - The index to search from.
+ * @returns {boolean} Returns `true` if a matching element is found, else `false`.
+ * @example
+ *
+ * includes([1, 2, 3], 1);
+ * // => true
+ *
+ * includes([1, 2, 3], 1, 2);
+ * // => false
+ *
+ * includes({ 'user': 'fred', 'age': 40 }, 'fred');
+ * // => true
+ *
+ * includes('pebbles', 'eb');
+ * // => true
+ */
+function includes(collection, target, fromIndex) {
+ if (!isString(collection) && !isObject(collection)) {
+ return false;
+ }
+
+ const values = Array.isArray(collection) || isString(collection)
+ ? collection
+ : Object.values(collection);
+ const formattedFromIndex = typeof fromIndex !== 'number'
+ ? 0
+ : fromIndex < 0
+ ? Math.max(values.length + fromIndex, 0)
+ : (fromIndex || 0);
+
+ return formattedFromIndex <= values.length
+ && values.indexOf(target, formattedFromIndex) > -1;
+}
+
+module.exports = includes;
diff --git a/lib/utils/isMatch.js b/lib/utils/isMatch.js
new file mode 100644
index 00000000..7e600b98
--- /dev/null
+++ b/lib/utils/isMatch.js
@@ -0,0 +1,42 @@
+const isObject = require('./isObject');
+
+/**
+ * Clone of https://lodash.com/docs/4.17.15#isMatch
+ *
+ * Checks if `pattern` object matches the `source` object.
+ * It performs deep comparison of all pattern's properties but doesn't try to find the pattern deeply in source.
+ *
+ * @param {Object} source - The object to inspect
+ * @param {Object} pattern - The object of property values to match
+ * @returns {Boolean} - Returns true if `pattern` has a `match` in source, else false
+ * @example
+ *
+ * var object = { 'a': 1, 'b': 2 };
+ *
+ * isMatch(object, { 'b': 2 });
+ * // => true
+ *
+ * isMatch(object, { 'b': 1 });
+ * // => false
+ */
+function isMatch(source, pattern) {
+ if (source === pattern) {
+ return true;
+ }
+
+ return Object.entries(pattern).every(([patternKey, patterValue]) => {
+ if (!(patternKey in source)) {
+ return false
+ }
+ const sourceValue = source[patternKey];
+ if (patterValue === sourceValue) {
+ return true
+ }
+ if (isObject(patterValue) && isObject(sourceValue)) {
+ return isMatch(sourceValue, patterValue)
+ }
+ return false;
+ })
+}
+
+module.exports = isMatch;
diff --git a/lib/utils/isObject.js b/lib/utils/isObject.js
new file mode 100644
index 00000000..a089d5c7
--- /dev/null
+++ b/lib/utils/isObject.js
@@ -0,0 +1,22 @@
+/**
+ * Checks if the value is an object
+ *
+ * @param {any} value - The value to check.
+ * @returns {boolean} - Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * isObject({});
+ * // => true
+ *
+ * isObject([1, 2, 3]);
+ * // => true
+ *
+ * isObject(_.noop);
+ * // => true
+ *
+ * isObject(null);
+ * // => false
+ */
+const isObject = value => value && typeof value === 'object';
+
+module.exports = isObject;
diff --git a/lib/utils/isString.js b/lib/utils/isString.js
new file mode 100644
index 00000000..bf1565f4
--- /dev/null
+++ b/lib/utils/isString.js
@@ -0,0 +1,19 @@
+/**
+ * Check if the value is classified as a string primitive or string object.
+ *
+ * @param {any} value - The value to check.
+ * @returns {boolean} - Returns `true` if `value` is a string, else `false`.
+ * @example
+ *
+ * isString('abc');
+ * // => true
+ *
+ * isString(new String('abc'));
+ * // => true
+ *
+ * isString(1);
+ * // => false
+ */
+const isString = value => typeof value === 'string' || value instanceof String;
+
+module.exports = isString;
diff --git a/lib/utils/mapValues.js b/lib/utils/mapValues.js
new file mode 100644
index 00000000..5a6208ba
--- /dev/null
+++ b/lib/utils/mapValues.js
@@ -0,0 +1,23 @@
+/**
+ * Clone of https://lodash.com/docs/3.10.1#mapValues
+ *
+ * Creates an object with the same keys as `object` and values generated by
+ * running each own enumerable property of `object` through `iteratee`. The
+ * iteratee function is invoked with three arguments: (value, key, object).
+ *
+ * @param {Object} object - The object to iterate over.
+ * @param {Function} iteratee - The function invoked per iteration.
+ * @returns {Object} Returns the new mapped object.
+ * @example
+ *
+ * mapValues({ 'a': 1, 'b': 2 }, n => n * 3);
+ * // => { 'a': 3, 'b': 6 }
+ */
+function mapValues(object, iteratee) {
+ return Object.entries(object).reduce((resObj, [key, value]) => {
+ resObj[key] = iteratee(value, key, object);
+ return resObj;
+ }, {});
+}
+
+module.exports = mapValues;
diff --git a/lib/utils/merge.js b/lib/utils/merge.js
new file mode 100644
index 00000000..54e7b834
--- /dev/null
+++ b/lib/utils/merge.js
@@ -0,0 +1,45 @@
+const isObject = require('./isObject');
+
+/**
+ * Clone of https://lodash.com/docs/4.17.15#merge
+ *
+ * Performs a deep merge of objects and returns a new object. Does not modify
+ * objects (immutable) and merges arrays via concatenation.
+ *
+ * @param {...Object} objects - Objects to merge
+ * @returns {Object} New object with merged key/values
+ * @example
+ *
+ * var users = {
+ * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }]
+ * };
+ *
+ * var ages = {
+ * 'data': [{ 'age': 36 }, { 'age': 40 }]
+ * };
+ *
+ * merge(users, ages);
+ * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] }
+ */
+function merge(...objects) {
+ return objects.reduce((prev, obj) => {
+ for (const key of Object.keys(obj)) {
+ const pVal = prev[key];
+ const oVal = obj[key];
+
+ if (Array.isArray(pVal) && Array.isArray(oVal)) {
+ prev[key] = pVal.concat(...oVal);
+ }
+ else if (isObject(pVal) && isObject(oVal)) {
+ prev[key] = merge(pVal, oVal);
+ }
+ else {
+ prev[key] = oVal;
+ }
+ }
+
+ return prev;
+ }, {});
+}
+
+module.exports = merge;
diff --git a/lib/utils/pickBy.js b/lib/utils/pickBy.js
new file mode 100644
index 00000000..d29ab6c4
--- /dev/null
+++ b/lib/utils/pickBy.js
@@ -0,0 +1,31 @@
+/**
+ * Clone of https://lodash.com/docs/4.17.15#pickBy
+ *
+ * Creates an object composed of the `object` properties `predicate` returns
+ * truthy for. The predicate is invoked with two arguments: (value, key).
+ *
+ * @param {Object} object The source object.
+ * @param {Function} predicate The function invoked per property.
+ * @returns {Object} Returns the new object.
+ * @example
+ *
+ * var object = { 'user': 'fred', 'age': 40 };
+ *
+ * pickBy(object, (value, key) => key === 'user');
+ * // => { 'user': 'fred' }
+ *
+ * pickBy(object, value => typeof value === 'string');
+ * // => { 'user': 'fred' }
+ */
+function pickBy(object, predicate) {
+ return Object.entries(object)
+ .reduce((resObj, [key, value]) => {
+ if (predicate(value, key)) {
+ resObj[key] = value;
+ }
+
+ return resObj;
+ }, {});
+}
+
+module.exports = pickBy;
diff --git a/package.json b/package.json
index 52d88cd7..1bbed31d 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"scripts": {
"lint": "eslint .",
"lint-and-fix": "eslint . --fix",
- "test": "lab -l -t 90"
+ "test": "lab -l -v -t 90"
},
"repository": {
"type": "git",
@@ -29,7 +29,6 @@
"handlebars-helpers": "^0.8.0",
"handlebars-utils": "^1.0.6",
"he": "^1.2.0",
- "lodash": "^3.6.0",
"messageformat": "^0.2.2",
"stringz": "^0.1.1",
"tarjan-graph": "^0.3.0"
@@ -38,7 +37,7 @@
"code": "^1.4.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^6.2.0",
- "lab": "^13.0.1",
+ "lab": "^14.3.4",
"sinon": "^1.17.2"
}
}
diff --git a/test/lib/utils/includes.spec.js b/test/lib/utils/includes.spec.js
new file mode 100644
index 00000000..156d3a5a
--- /dev/null
+++ b/test/lib/utils/includes.spec.js
@@ -0,0 +1,73 @@
+'use strict';
+
+const Code = require('code');
+const Lab = require('lab');
+const includes = require("../../../lib/utils/includes");
+
+const lab = exports.lab = Lab.script();
+const describe = lab.experiment;
+const expect = Code.expect;
+const it = lab.it;
+
+describe('includes', () => {
+ (function() {
+ const testData1 = {
+ 'an `arguments` object': arguments,
+ 'an array': [1, 2, 3, 4],
+ 'an object': { 'a': 1, 'b': 2, 'c': 3, 'd': 4 },
+ 'a string': '1234'
+ };
+ for (const [key, collection] of Object.entries(testData1)) {
+ it(`should return "true" for matched values of ${key}`, async () => {
+ expect(includes(collection, 3)).to.be.equal(true);
+ });
+
+ it(`should return "false" for unmatched values of ${key}`, async () => {
+ expect(includes(collection, 5)).to.be.equal(false);
+ });
+
+ it(`should floor "fromIndex" values of ${key}`, async () => {
+ expect(includes(collection, 2, 1.2)).to.be.equal(true);
+ });
+ }
+ })(1, 2, 3, 4);
+
+ const testData2 = {
+ 'literal': 'abc',
+ 'object': Object('abc')
+ };
+ Object.entries(testData2).forEach(([key, collection]) => {
+ it(`should work with a string ${key} for "collection"`, async () => {
+ expect(includes(collection, 'bc')).to.be.equal(true);
+ expect(includes(collection, 'd')).to.be.equal(false);
+ });
+ });
+
+ it('should return `false` for empty collections', async () => {
+ const empties = [[], {}, null, undefined, false, 0, NaN, ''];
+
+ for (const collection of empties) {
+ const res = includes(collection, 1);
+
+ expect(res).to.be.equal(false);
+ }
+ });
+
+ describe('called on a string collection with fromIndex >= length', () => {
+ const string = '1234';
+ const indexes = [4, 6, Math.pow(2, 32), Infinity];
+
+ indexes.forEach((fromIndex) => {
+ it(`should return a correct result for fromIndex = ${fromIndex}`, async () => {
+ expect(includes(string, 1, fromIndex)).to.be.equal(false);
+ expect(includes(string, undefined, fromIndex)).to.be.equal(false);
+ expect(includes(string, '', fromIndex)).to.be.equal(fromIndex === string.length);
+ });
+ });
+ });
+
+ it('should match -0 as 0', async () => {
+ expect(includes([-0], 0)).to.be.equal(true);
+ expect(includes([0], -0)).to.be.equal(true);
+ });
+});
diff --git a/test/lib/utils/isMatch.spec.js b/test/lib/utils/isMatch.spec.js
new file mode 100644
index 00000000..a948e9c2
--- /dev/null
+++ b/test/lib/utils/isMatch.spec.js
@@ -0,0 +1,151 @@
+'use strict';
+
+const Code = require('code');
+const Lab = require('lab');
+const isMatch = require('../../../lib/utils/isMatch');
+
+const lab = exports.lab = Lab.script();
+const describe = lab.experiment;
+const expect = Code.expect;
+const it = lab.it;
+
+describe('isMatch', () => {
+ const testCases = [
+ {
+ testName: 'should return true when source contains pattern and they are simple one-level objects',
+ source: { a: 1, b: 2, c: 'c' },
+ pattern: { b: 2 },
+ expectedResult: true,
+ },
+ {
+ testName: 'should return false when source contains a pattern key but their values are different',
+ source: { a: 1, b: 2, c: 'c' },
+ pattern: { b: 1 },
+ expectedResult: false,
+ },
+ {
+ testName: 'should return false when source contains a pattern key but their values are of different types',
+ source: { a: 1, b: 2, c: 'c' },
+ pattern: { a: '1' },
+ expectedResult: false,
+ },
+ {
+ testName: 'should return false when source and pattern match only partially',
+ source: { a: 1, b: 2, c: 'c' },
+ pattern: { b: 2, a: 3 },
+ expectedResult: false,
+ },
+ {
+ testName: 'should return true when source contains pattern and they contain nested properties',
+ source: {
+ a: {
+ b: null,
+ c: 1,
+ d: {
+ f: 2,
+ g: null,
+ h: false,
+ }
+ },
+ k: "2",
+ l: 'c'
+ },
+ pattern: {
+ a: {
+ b: null,
+ d: {
+ f: 2,
+ h: false,
+ }
+ },
+ k: "2",
+ },
+ expectedResult: true,
+ },
+ {
+ testName: 'should return false when source contains pattern only partially in the nested properties',
+ source: {
+ a: {
+ b: 'b1',
+ c: 1,
+ d: {
+ f: 2,
+ g: null,
+ h: false,
+ }
+ },
+ k: 2,
+ l: 'c'
+ },
+ pattern: {
+ a: {
+ b: 'b1',
+ d: {
+ f: 2,
+ h: true,
+ }
+ },
+ },
+ expectedResult: false,
+ },
+ {
+ testName: 'should return true when source contains pattern and they contain arrays',
+ source: {
+ a: {
+ b: [1, '2', false, null, [3]],
+ c: 1,
+ },
+ k: 2,
+ l: 'c'
+ },
+ pattern: {
+ a: {
+ b: [1, '2', false, null, [3]],
+ },
+ k: 2,
+ },
+ expectedResult: true,
+ },
+ {
+ testName: 'should return true when source contains pattern and they contain empty objects and arrays',
+ source: {
+ a: {
+ b: [],
+ c: {},
+ d: 2
+ },
+ k: 2,
+ l: 'c'
+ },
+ pattern: {
+ a: {
+ b: [],
+ c: {},
+ },
+ k: 2,
+ },
+ expectedResult: true,
+ },
+ {
+ testName: 'should return false when source contains pattern on a different level of nesting',
+ source: {
+ a: {
+ b: { c: 1, d: 'd' },
+ f: {},
+ s: 2
+ },
+ k: null,
+ l: 'c'
+ },
+ pattern: { c: 1, d: 'd' },
+ expectedResult: false,
+ },
+ ];
+
+ for (const { testName, source, pattern, expectedResult } of testCases) {
+ it(testName, async () => {
+ const actualResult = isMatch(source, pattern);
+ expect(actualResult).to.be.equal(expectedResult);
+ });
+ }
+});