/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ /* eslint-disable no-shadow */ define([ 'jquery', 'underscore', 'mage/utils/objects', 'mage/utils/strings' ], function ($, _, utils, stringUtils) { 'use strict'; var tmplSettings = _.templateSettings, interpolate = /\$\{([\s\S]+?)\}/g, opener = '${', template, hasStringTmpls; /** * Identifies whether ES6 templates are supported. */ hasStringTmpls = (function () { var testString = 'var foo = "bar"; return `${ foo }` === foo'; try { return Function(testString)(); } catch (e) { return false; } })(); /** * Objects can specify how to use templating for their properties - getting that configuration. * * To disable rendering for all properties of your object add __disableTmpl: true. * To disable for specific property add __disableTmpl: {propertyName: true}. * To limit recursion for a specific property add __disableTmpl: {propertyName: numberOfCycles}. * * @param {String} tmpl * @param {Object | undefined} target * @returns {Boolean|Object} */ function isTmplIgnored(tmpl, target) { var parsedTmpl; try { parsedTmpl = JSON.parse(tmpl); if (typeof parsedTmpl === 'object') { return tmpl.includes('__disableTmpl'); } } catch (e) { } if (typeof target !== 'undefined') { if (typeof target === 'object' && target.hasOwnProperty('__disableTmpl')) { return target.__disableTmpl; } } return false; } if (hasStringTmpls) { /*eslint-disable no-unused-vars, no-eval*/ /** * Evaluates template string using ES6 templates. * * @param {String} tmpl - Template string. * @param {Object} $ - Data object used in a template. * @returns {String} Compiled template. */ template = function (tmpl, $) { return eval('`' + tmpl + '`'); }; /*eslint-enable no-unused-vars, no-eval*/ } else { /** * Fallback function used when ES6 templates are not supported. * Uses underscore templates renderer. * * @param {String} tmpl - Template string. * @param {Object} data - Data object used in a template. * @returns {String} Compiled template. */ template = function (tmpl, data) { var cached = tmplSettings.interpolate; tmplSettings.interpolate = interpolate; tmpl = _.template(tmpl, { variable: '$' })(data); tmplSettings.interpolate = cached; return tmpl; }; } /** * Checks if provided value contains template syntax. * * @param {*} value - Value to be checked. * @returns {Boolean} */ function isTemplate(value) { return typeof value === 'string' && value.indexOf(opener) !== -1 && // the below pattern almost always indicates an accident which should not cause template evaluation // refuse to evaluate value.indexOf('${{') === -1; } /** * Iteratively processes provided string * until no templates syntax will be found. * * @param {String} tmpl - Template string. * @param {Object} data - Data object used in a template. * @param {Boolean} [castString=false] - Flag that indicates whether template * should be casted after evaluation to a value of another type or * that it should be leaved as a string. * @param {Number|undefined} maxCycles - Maximum number of rendering cycles, can be 0. * @returns {*} Compiled template. */ function render(tmpl, data, castString, maxCycles) { var last = tmpl, cycles = 0; while (~tmpl.indexOf(opener) && (typeof maxCycles === 'undefined' || cycles < maxCycles)) { if (!isTmplIgnored(tmpl)) { tmpl = template(tmpl, data); } if (tmpl === last) { break; } last = tmpl; cycles++; } return castString ? stringUtils.castString(tmpl) : tmpl; } return { /** * Applies provided data to the template. * * @param {Object|String} tmpl * @param {Object} [data] - Data object to match with template. * @param {Boolean} [castString=false] - Flag that indicates whether template * should be casted after evaluation to a value of another type or * that it should be leaved as a string. * @returns {*} * * @example Template defined as a string. * var source = { foo: 'Random Stuff', bar: 'Some' }; * * utils.template('${ $.bar } ${ $.foo }', source); * => 'Some Random Stuff'; * * @example Template defined as an object. * var tmpl = { * key: {'${ $.$data.bar }': '${ $.$data.foo }'}, * foo: 'bar', * x1: 2, x2: 5, * delta: '${ $.x2 - $.x1 }', * baz: 'Upper ${ $.foo.toUpperCase() }' * }; * * utils.template(tmpl, source); * => { * key: {'Some': 'Random Stuff'}, * foo: 'bar', * x1: 2, x2: 5, * delta: 3, * baz: 'Upper BAR' * }; */ template: function (tmpl, data, castString, dontClone) { if (typeof tmpl === 'string') { return render(tmpl, data, castString); } if (!dontClone) { tmpl = utils.copy(tmpl); } tmpl.$data = data || {}; /** * Template iterator function. */ _.each(tmpl, function iterate(value, key, list) { var disabled, maxCycles; if (key === '$data') { return; } if (isTemplate(key)) { delete list[key]; key = render(key, tmpl); list[key] = value; } if (isTemplate(value)) { //Getting template disabling settings, can be true for all disabled and separate settings //for each property. disabled = isTmplIgnored(value, list); if (typeof disabled === 'object' && disabled.hasOwnProperty(key) && disabled[key] !== false) { //Checking if specific settings for a property provided. maxCycles = disabled[key]; } if (disabled === true || maxCycles === true) { //Rendering for all properties is disabled. maxCycles = 0; } list[key] = render(value, tmpl, castString, maxCycles); } else if ($.isPlainObject(value) || Array.isArray(value)) { _.each(value, iterate); } }); delete tmpl.$data; return tmpl; } }; });