From 9bb88229d201c7bf9a2acf46bc86b2b816ceb072 Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Thu, 10 Sep 2020 18:25:59 +0300 Subject: [PATCH 1/2] feat: update version of lab to support newer JS syntax on paper 2.x --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52d88cd7..ad13ccb1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,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" } } From 77b2082762a436a2a7b49630e19ea3165242cd2d Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Thu, 10 Sep 2020 21:38:07 +0300 Subject: [PATCH 2/2] feat: (strf-8652) eliminate lodash from paper 2.x --- helpers/all.js | 35 ++------ helpers/any.js | 49 ++++------- helpers/contains.js | 15 ++-- helpers/deprecated.js | 6 +- helpers/for.js | 19 ++-- helpers/getImage.js | 17 ++-- helpers/limit.js | 7 +- helpers/money.js | 13 ++- helpers/or.js | 36 ++------ helpers/pluck.js | 4 +- helpers/resourceHints.js | 27 +++--- helpers/stylesheet.js | 14 +-- helpers/thirdParty.js | 9 +- index.js | 58 ++++++------ lib/fonts.js | 92 ++++++++----------- lib/translator/filter.js | 21 +++-- lib/translator/index.js | 3 +- lib/translator/locale-parser.js | 20 ++--- lib/translator/transformer.js | 85 +++++++++--------- lib/utils/includes.js | 46 ++++++++++ lib/utils/isMatch.js | 42 +++++++++ lib/utils/isObject.js | 22 +++++ lib/utils/isString.js | 19 ++++ lib/utils/mapValues.js | 23 +++++ lib/utils/merge.js | 45 ++++++++++ lib/utils/pickBy.js | 31 +++++++ package.json | 3 +- test/lib/utils/includes.spec.js | 73 +++++++++++++++ test/lib/utils/isMatch.spec.js | 151 ++++++++++++++++++++++++++++++++ 29 files changed, 671 insertions(+), 314 deletions(-) create mode 100644 lib/utils/includes.js create mode 100644 lib/utils/isMatch.js create mode 100644 lib/utils/isObject.js create mode 100644 lib/utils/isString.js create mode 100644 lib/utils/mapValues.js create mode 100644 lib/utils/merge.js create mode 100644 lib/utils/pickBy.js create mode 100644 test/lib/utils/includes.spec.js create mode 100644 test/lib/utils/isMatch.spec.js 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 ad13ccb1..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" 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); + }); + } +});