template.js (10441B)
1 var assignInWith = require('./assignInWith'), 2 attempt = require('./attempt'), 3 baseValues = require('./_baseValues'), 4 customDefaultsAssignIn = require('./_customDefaultsAssignIn'), 5 escapeStringChar = require('./_escapeStringChar'), 6 isError = require('./isError'), 7 isIterateeCall = require('./_isIterateeCall'), 8 keys = require('./keys'), 9 reInterpolate = require('./_reInterpolate'), 10 templateSettings = require('./templateSettings'), 11 toString = require('./toString'); 12 13 /** Error message constants. */ 14 var INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`'; 15 16 /** Used to match empty string literals in compiled template source. */ 17 var reEmptyStringLeading = /\b__p \+= '';/g, 18 reEmptyStringMiddle = /\b(__p \+=) '' \+/g, 19 reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; 20 21 /** 22 * Used to validate the `validate` option in `_.template` variable. 23 * 24 * Forbids characters which could potentially change the meaning of the function argument definition: 25 * - "()," (modification of function parameters) 26 * - "=" (default value) 27 * - "[]{}" (destructuring of function parameters) 28 * - "/" (beginning of a comment) 29 * - whitespace 30 */ 31 var reForbiddenIdentifierChars = /[()=,{}\[\]\/\s]/; 32 33 /** 34 * Used to match 35 * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components). 36 */ 37 var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; 38 39 /** Used to ensure capturing order of template delimiters. */ 40 var reNoMatch = /($^)/; 41 42 /** Used to match unescaped characters in compiled string literals. */ 43 var reUnescapedString = /['\n\r\u2028\u2029\\]/g; 44 45 /** Used for built-in method references. */ 46 var objectProto = Object.prototype; 47 48 /** Used to check objects for own properties. */ 49 var hasOwnProperty = objectProto.hasOwnProperty; 50 51 /** 52 * Creates a compiled template function that can interpolate data properties 53 * in "interpolate" delimiters, HTML-escape interpolated data properties in 54 * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data 55 * properties may be accessed as free variables in the template. If a setting 56 * object is given, it takes precedence over `_.templateSettings` values. 57 * 58 * **Note:** In the development build `_.template` utilizes 59 * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) 60 * for easier debugging. 61 * 62 * For more information on precompiling templates see 63 * [lodash's custom builds documentation](https://lodash.com/custom-builds). 64 * 65 * For more information on Chrome extension sandboxes see 66 * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). 67 * 68 * @static 69 * @since 0.1.0 70 * @memberOf _ 71 * @category String 72 * @param {string} [string=''] The template string. 73 * @param {Object} [options={}] The options object. 74 * @param {RegExp} [options.escape=_.templateSettings.escape] 75 * The HTML "escape" delimiter. 76 * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] 77 * The "evaluate" delimiter. 78 * @param {Object} [options.imports=_.templateSettings.imports] 79 * An object to import into the template as free variables. 80 * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] 81 * The "interpolate" delimiter. 82 * @param {string} [options.sourceURL='templateSources[n]'] 83 * The sourceURL of the compiled template. 84 * @param {string} [options.variable='obj'] 85 * The data object variable name. 86 * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. 87 * @returns {Function} Returns the compiled template function. 88 * @example 89 * 90 * // Use the "interpolate" delimiter to create a compiled template. 91 * var compiled = _.template('hello <%= user %>!'); 92 * compiled({ 'user': 'fred' }); 93 * // => 'hello fred!' 94 * 95 * // Use the HTML "escape" delimiter to escape data property values. 96 * var compiled = _.template('<b><%- value %></b>'); 97 * compiled({ 'value': '<script>' }); 98 * // => '<b><script></b>' 99 * 100 * // Use the "evaluate" delimiter to execute JavaScript and generate HTML. 101 * var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>'); 102 * compiled({ 'users': ['fred', 'barney'] }); 103 * // => '<li>fred</li><li>barney</li>' 104 * 105 * // Use the internal `print` function in "evaluate" delimiters. 106 * var compiled = _.template('<% print("hello " + user); %>!'); 107 * compiled({ 'user': 'barney' }); 108 * // => 'hello barney!' 109 * 110 * // Use the ES template literal delimiter as an "interpolate" delimiter. 111 * // Disable support by replacing the "interpolate" delimiter. 112 * var compiled = _.template('hello ${ user }!'); 113 * compiled({ 'user': 'pebbles' }); 114 * // => 'hello pebbles!' 115 * 116 * // Use backslashes to treat delimiters as plain text. 117 * var compiled = _.template('<%= "\\<%- value %\\>" %>'); 118 * compiled({ 'value': 'ignored' }); 119 * // => '<%- value %>' 120 * 121 * // Use the `imports` option to import `jQuery` as `jq`. 122 * var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>'; 123 * var compiled = _.template(text, { 'imports': { 'jq': jQuery } }); 124 * compiled({ 'users': ['fred', 'barney'] }); 125 * // => '<li>fred</li><li>barney</li>' 126 * 127 * // Use the `sourceURL` option to specify a custom sourceURL for the template. 128 * var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' }); 129 * compiled(data); 130 * // => Find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector. 131 * 132 * // Use the `variable` option to ensure a with-statement isn't used in the compiled template. 133 * var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' }); 134 * compiled.source; 135 * // => function(data) { 136 * // var __t, __p = ''; 137 * // __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!'; 138 * // return __p; 139 * // } 140 * 141 * // Use custom template delimiters. 142 * _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; 143 * var compiled = _.template('hello {{ user }}!'); 144 * compiled({ 'user': 'mustache' }); 145 * // => 'hello mustache!' 146 * 147 * // Use the `source` property to inline compiled templates for meaningful 148 * // line numbers in error messages and stack traces. 149 * fs.writeFileSync(path.join(process.cwd(), 'jst.js'), '\ 150 * var JST = {\ 151 * "main": ' + _.template(mainText).source + '\ 152 * };\ 153 * '); 154 */ 155 function template(string, options, guard) { 156 // Based on John Resig's `tmpl` implementation 157 // (http://ejohn.org/blog/javascript-micro-templating/) 158 // and Laura Doktorova's doT.js (https://github.com/olado/doT). 159 var settings = templateSettings.imports._.templateSettings || templateSettings; 160 161 if (guard && isIterateeCall(string, options, guard)) { 162 options = undefined; 163 } 164 string = toString(string); 165 options = assignInWith({}, options, settings, customDefaultsAssignIn); 166 167 var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn), 168 importsKeys = keys(imports), 169 importsValues = baseValues(imports, importsKeys); 170 171 var isEscaping, 172 isEvaluating, 173 index = 0, 174 interpolate = options.interpolate || reNoMatch, 175 source = "__p += '"; 176 177 // Compile the regexp to match each delimiter. 178 var reDelimiters = RegExp( 179 (options.escape || reNoMatch).source + '|' + 180 interpolate.source + '|' + 181 (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' + 182 (options.evaluate || reNoMatch).source + '|$' 183 , 'g'); 184 185 // Use a sourceURL for easier debugging. 186 // The sourceURL gets injected into the source that's eval-ed, so be careful 187 // to normalize all kinds of whitespace, so e.g. newlines (and unicode versions of it) can't sneak in 188 // and escape the comment, thus injecting code that gets evaled. 189 var sourceURL = hasOwnProperty.call(options, 'sourceURL') 190 ? ('//# sourceURL=' + 191 (options.sourceURL + '').replace(/\s/g, ' ') + 192 '\n') 193 : ''; 194 195 string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { 196 interpolateValue || (interpolateValue = esTemplateValue); 197 198 // Escape characters that can't be included in string literals. 199 source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar); 200 201 // Replace delimiters with snippets. 202 if (escapeValue) { 203 isEscaping = true; 204 source += "' +\n__e(" + escapeValue + ") +\n'"; 205 } 206 if (evaluateValue) { 207 isEvaluating = true; 208 source += "';\n" + evaluateValue + ";\n__p += '"; 209 } 210 if (interpolateValue) { 211 source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; 212 } 213 index = offset + match.length; 214 215 // The JS engine embedded in Adobe products needs `match` returned in 216 // order to produce the correct `offset` value. 217 return match; 218 }); 219 220 source += "';\n"; 221 222 // If `variable` is not specified wrap a with-statement around the generated 223 // code to add the data object to the top of the scope chain. 224 var variable = hasOwnProperty.call(options, 'variable') && options.variable; 225 if (!variable) { 226 source = 'with (obj) {\n' + source + '\n}\n'; 227 } 228 // Throw an error if a forbidden character was found in `variable`, to prevent 229 // potential command injection attacks. 230 else if (reForbiddenIdentifierChars.test(variable)) { 231 throw new Error(INVALID_TEMPL_VAR_ERROR_TEXT); 232 } 233 234 // Cleanup code by stripping empty strings. 235 source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) 236 .replace(reEmptyStringMiddle, '$1') 237 .replace(reEmptyStringTrailing, '$1;'); 238 239 // Frame code as the function body. 240 source = 'function(' + (variable || 'obj') + ') {\n' + 241 (variable 242 ? '' 243 : 'obj || (obj = {});\n' 244 ) + 245 "var __t, __p = ''" + 246 (isEscaping 247 ? ', __e = _.escape' 248 : '' 249 ) + 250 (isEvaluating 251 ? ', __j = Array.prototype.join;\n' + 252 "function print() { __p += __j.call(arguments, '') }\n" 253 : ';\n' 254 ) + 255 source + 256 'return __p\n}'; 257 258 var result = attempt(function() { 259 return Function(importsKeys, sourceURL + 'return ' + source) 260 .apply(undefined, importsValues); 261 }); 262 263 // Provide the compiled function's source by its `toString` method or 264 // the `source` property as a convenience for inlining compiled templates. 265 result.source = source; 266 if (isError(result)) { 267 throw result; 268 } 269 return result; 270 } 271 272 module.exports = template;