UNPKG

23.6 kBJavaScriptView Raw
1/* micromustache v8.0.3 */
2(function (global, factory) {
3 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4 typeof define === 'function' && define.amd ? define(['exports'], factory) :
5 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.micromustache = {}));
6}(this, (function (exports) { 'use strict';
7
8 /** @internal */
9 /** @internal */
10 // eslint-disable-next-line @typescript-eslint/unbound-method
11 var numberConstructor = (0).constructor;
12 /** @internal */
13 // eslint-disable-next-line @typescript-eslint/unbound-method
14 var isFinite = numberConstructor.isFinite;
15 /** @internal */
16 // eslint-disable-next-line @typescript-eslint/unbound-method
17 var isInteger = numberConstructor.isInteger;
18 /** @internal */
19 // eslint-disable-next-line @typescript-eslint/unbound-method
20 var isArray = [].constructor.isArray;
21 /** @internal */
22 // eslint-disable-next-line @typescript-eslint/ban-types
23 function isObj(x) {
24 return x !== null && typeof x === 'object';
25 }
26 /** @internal */
27 // eslint-disable-next-line @typescript-eslint/ban-types
28 function isFn(x) {
29 return typeof x === 'function';
30 }
31 /** @internal */
32 function isStr(x, minLength) {
33 if (minLength === void 0) { minLength = 0; }
34 return typeof x === 'string' && x.length >= minLength;
35 }
36 /** @internal */
37 function isNum(x) {
38 return isFinite(x);
39 }
40 /** @internal */
41 function isArr(x) {
42 return isArray(x);
43 }
44 /** @internal */
45 function isProp(x, propName) {
46 return isObj(x) && propName in x;
47 }
48
49 /**
50 * @internal
51 * The number of different varNames that will be cached.
52 * If a varName is cached, the actual parsing algorithm will not be called
53 * which significantly improves performance.
54 * However, this cache is size-limited to prevent degrading the user's software
55 * over a period of time.
56 * If the cache is full, we start removing older varNames one at a time.
57 */
58 var cacheSize = 1000;
59 /** @internal */
60 var quoteChars = '\'"`';
61 /**
62 * @internal
63 */
64 var Cache = /** @class */ (function () {
65 function Cache(size) {
66 this.size = size;
67 this.reset();
68 }
69 Cache.prototype.reset = function () {
70 this.oldestIndex = 0;
71 this.map = {};
72 this.cachedKeys = new Array(this.size);
73 };
74 Cache.prototype.get = function (key) {
75 return this.map[key];
76 };
77 Cache.prototype.set = function (key, value) {
78 this.map[key] = value;
79 var oldestKey = this.cachedKeys[this.oldestIndex];
80 if (oldestKey !== undefined) {
81 delete this.map[oldestKey];
82 }
83 this.cachedKeys[this.oldestIndex] = key;
84 this.oldestIndex++;
85 this.oldestIndex %= this.size;
86 };
87 return Cache;
88 }());
89 /** @internal */
90 var cache = new Cache(cacheSize);
91 /**
92 * @internal
93 * Removes the quotes from a string and returns it.
94 * @param propName an string with quotations
95 * @throws `SyntaxError` if the quotation symbols don't match or one is missing
96 * @returns the input with its quotes removed
97 */
98 function propBetweenBrackets(propName) {
99 // in our algorithms key is always a string and never only a string of spaces
100 var firstChar = propName.charAt(0);
101 var lastChar = propName.substr(-1);
102 if (quoteChars.includes(firstChar) || quoteChars.includes(lastChar)) {
103 if (propName.length < 2 || firstChar !== lastChar) {
104 throw new SyntaxError("Mismatching string quotation: \"" + propName + "\"");
105 }
106 return propName.substring(1, propName.length - 1);
107 }
108 if (propName.includes('[')) {
109 throw new SyntaxError("Missing ] in varName \"" + propName + "\"");
110 }
111 // Normalize leading plus from numerical indices
112 if (firstChar === '+') {
113 return propName.substr(1);
114 }
115 return propName;
116 }
117 /** @internal */
118 function pushPropName(propNames, propName, preDot) {
119 var pName = propName.trim();
120 if (pName === '') {
121 return propNames;
122 }
123 if (pName.startsWith('.')) {
124 if (preDot) {
125 pName = pName.substr(1).trim();
126 if (pName === '') {
127 return propNames;
128 }
129 }
130 else {
131 throw new SyntaxError("Unexpected . at the start of \"" + propName + "\"");
132 }
133 }
134 else if (preDot) {
135 throw new SyntaxError("Missing . at the start of \"" + propName + "\"");
136 }
137 if (pName.endsWith('.')) {
138 throw new SyntaxError("Unexpected \".\" at the end of \"" + propName + "\"");
139 }
140 var propNameParts = pName.split('.');
141 for (var _i = 0, propNameParts_1 = propNameParts; _i < propNameParts_1.length; _i++) {
142 var propNamePart = propNameParts_1[_i];
143 var trimmedPropName = propNamePart.trim();
144 if (trimmedPropName === '') {
145 throw new SyntaxError("Empty prop name when parsing \"" + propName + "\"");
146 }
147 propNames.push(trimmedPropName);
148 }
149 return propNames;
150 }
151 /**
152 * Breaks a variable name to an array of strings that can be used to get a
153 * particular value from an object
154 * @param varName - the variable name as it occurs in the template.
155 * For example `a["b"].c`
156 * @throws `TypeError` if the varName is not a string
157 * @throws `SyntaxError` if the varName syntax has a problem
158 * @returns - an array of property names that can be used to get a particular
159 * value.
160 * For example `['a', 'b', 'c']`
161 */
162 function toPath(varName) {
163 if (!isStr(varName)) {
164 throw new TypeError("Cannot parse path. Expected string. Got a " + typeof varName);
165 }
166 var openBracketIndex;
167 var closeBracketIndex = 0;
168 var beforeBracket;
169 var propName;
170 var preDot = false;
171 var propNames = new Array(0);
172 for (var currentIndex = 0; currentIndex < varName.length; currentIndex = closeBracketIndex) {
173 openBracketIndex = varName.indexOf('[', currentIndex);
174 if (openBracketIndex === -1) {
175 break;
176 }
177 closeBracketIndex = varName.indexOf(']', openBracketIndex);
178 if (closeBracketIndex === -1) {
179 throw new SyntaxError("Missing ] in varName \"" + varName + "\"");
180 }
181 propName = varName.substring(openBracketIndex + 1, closeBracketIndex).trim();
182 if (propName.length === 0) {
183 throw new SyntaxError('Unexpected token ]');
184 }
185 closeBracketIndex++;
186 beforeBracket = varName.substring(currentIndex, openBracketIndex);
187 pushPropName(propNames, beforeBracket, preDot);
188 propNames.push(propBetweenBrackets(propName));
189 preDot = true;
190 }
191 var rest = varName.substring(closeBracketIndex);
192 return pushPropName(propNames, rest, preDot);
193 }
194 /**
195 * This is just a faster version of `toPath()`
196 */
197 function toPathCached(varName) {
198 var result = cache.get(varName);
199 if (result === undefined) {
200 result = toPath(varName);
201 cache.set(varName, result);
202 }
203 return result;
204 }
205 toPath.cached = toPathCached;
206
207 /**
208 * A useful utility function that is used internally to lookup a variable name as a path to a
209 * property in an object. It can also be used in your custom resolver functions if needed.
210 *
211 * This is similar to [Lodash's `_.get()`](https://lodash.com/docs/#get)
212 *
213 * It has a few differences with plain JavaScript syntax:
214 * - No support for keys that include `[` or `]`.
215 * - No support for keys that include `'` or `"` or `.`.
216 * @see https://github.com/userpixel/micromustache/wiki/Known-issues
217 * If it cannot find a value in the specified path, it may return undefined or throw an error
218 * depending on the value of the `propsExist` param.
219 * @param scope an object to resolve value from
220 * @param varNameOrPropNames the variable name string or an array of property names (as returned by
221 * `toPath()`)
222 * @throws `SyntaxError` if the varName string cannot be parsed
223 * @throws `ReferenceError` if the scope does not contain the requested key and the `propsExist` is
224 * set to a truthy value
225 * @returns the value or undefined. If path or scope are undefined or scope is null the result is
226 * always undefined.
227 */
228 function get(scope, varNameOrPropNames, options) {
229 if (options === void 0) { options = {}; }
230 if (!isObj(options)) {
231 throw new TypeError("get expects an object option. Got " + typeof options);
232 }
233 var _a = options.depth, depth = _a === void 0 ? 10 : _a;
234 if (!isNum(depth) || depth <= 0) {
235 throw new RangeError("Expected a positive number for depth. Got " + depth);
236 }
237 var propNames = Array.isArray(varNameOrPropNames)
238 ? varNameOrPropNames
239 : toPath.cached(varNameOrPropNames);
240 var propNamesAsStr = function () { return propNames.join(' > '); };
241 if (propNames.length > depth) {
242 throw new ReferenceError("The path cannot be deeper than " + depth + " levels. Got \"" + propNamesAsStr() + "\"");
243 }
244 var currentScope = scope;
245 for (var _i = 0, propNames_1 = propNames; _i < propNames_1.length; _i++) {
246 var propName = propNames_1[_i];
247 if (isProp(currentScope, propName)) {
248 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
249 currentScope = currentScope[propName];
250 }
251 else if (options.propsExist) {
252 throw new ReferenceError(propName + " is not defined in the scope at path: \"" + propNamesAsStr() + "\"");
253 }
254 else {
255 return;
256 }
257 }
258 return currentScope;
259 }
260
261 /**
262 * This class does the heavy lifting of interpolation (putting the actual values
263 * in the template).
264 * This is created by the `.compile()` method and is used under the hood by
265 * `.render()`, `renderFn()` and `renderFnAsync()` functions.
266 */
267 var Renderer = /** @class */ (function () {
268 /**
269 * Creates a new Renderer instance. This is called internally by the compiler.
270 * @param tokens - the result of the `.tokenize()` function
271 * @param options - some options for customizing the rendering process
272 * @throws `TypeError` if the token is invalid
273 */
274 function Renderer(tokens, options) {
275 var _this = this;
276 if (options === void 0) { options = {}; }
277 this.tokens = tokens;
278 this.options = options;
279 /**
280 * Replaces every {{varName}} inside the template with values from the scope
281 * parameter.
282 *
283 * @param template The template containing one or more {{varName}} as
284 * placeholders for values from the `scope` parameter.
285 * @param scope An object containing values for variable names from the the
286 * template. If it's omitted, we default to an empty object.
287 */
288 this.render = function (scope) {
289 if (scope === void 0) { scope = {}; }
290 var varNames = _this.tokens.varNames;
291 var length = varNames.length;
292 _this.cacheParsedPaths();
293 var values = new Array(length);
294 for (var i = 0; i < length; i++) {
295 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
296 values[i] = get(scope, _this.toPathCache[i], _this.options);
297 }
298 return _this.stringify(values);
299 };
300 /**
301 * Same as [[render]] but accepts a resolver function which will be
302 * responsible for returning a value for every varName.
303 */
304 this.renderFn = function (resolveFn, scope) {
305 if (scope === void 0) { scope = {}; }
306 var values = _this.resolveVarNames(resolveFn, scope);
307 return _this.stringify(values);
308 };
309 /**
310 * Same as [[render]] but accepts a resolver function which will be responsible
311 * for returning promise that resolves to a value for every varName.
312 */
313 this.renderFnAsync = function (resolveFnAsync, scope) {
314 if (scope === void 0) { scope = {}; }
315 return Promise.all(_this.resolveVarNames(resolveFnAsync, scope)).then(function (values) {
316 return _this.stringify(values);
317 });
318 };
319 if (!isObj(tokens) ||
320 !isArr(tokens.strings) ||
321 !isArr(tokens.varNames) ||
322 tokens.strings.length !== tokens.varNames.length + 1) {
323 // This is most likely an internal error from tokenization algorithm
324 throw new TypeError("Invalid tokens object");
325 }
326 if (!isObj(options)) {
327 throw new TypeError("Options should be an object. Got a " + typeof options);
328 }
329 if (options.validateVarNames) {
330 // trying to initialize toPathCache parses them which is also validation
331 this.cacheParsedPaths();
332 }
333 }
334 /**
335 * This function is called internally for filling in the `toPathCache` cache.
336 * If the `validateVarNames` option for the constructor is set to a truthy
337 * value, this function is called immediately which leads to a validation as
338 * well because it throws an error if it cannot parse variable names.
339 */
340 Renderer.prototype.cacheParsedPaths = function () {
341 var varNames = this.tokens.varNames;
342 if (this.toPathCache === undefined) {
343 this.toPathCache = new Array(varNames.length);
344 for (var i = 0; i < varNames.length; i++) {
345 this.toPathCache[i] = toPath.cached(varNames[i]);
346 }
347 }
348 };
349 Renderer.prototype.resolveVarNames = function (resolveFn, scope) {
350 if (scope === void 0) { scope = {}; }
351 var varNames = this.tokens.varNames;
352 if (!isFn(resolveFn)) {
353 throw new TypeError("Expected a resolver function. Got " + String(resolveFn));
354 }
355 var length = varNames.length;
356 var values = new Array(length);
357 for (var i = 0; i < length; i++) {
358 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
359 values[i] = resolveFn.call(null, varNames[i], scope);
360 }
361 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
362 return values;
363 };
364 /**
365 * Puts the resolved `values` into the rest of the template (`strings`) and
366 * returns the final result that'll be returned from `render()`, `renderFn()`
367 * and `renderFnAsync()` functions.
368 */
369 Renderer.prototype.stringify = function (values) {
370 var strings = this.tokens.strings;
371 var explicit = this.options.explicit;
372 var length = values.length;
373 var ret = '';
374 for (var i = 0; i < length; i++) {
375 ret += strings[i];
376 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
377 var value = values[i];
378 if (explicit || (value !== null && value !== undefined)) {
379 ret += value;
380 }
381 }
382 ret += strings[length];
383 return ret;
384 };
385 return Renderer;
386 }());
387
388 /**
389 * Parses a template and returns the tokens in an object.
390 *
391 * @throws `TypeError` if there's an issue with its inputs
392 * @throws `SyntaxError` if there's an issue with the template
393 *
394 * @param template the template
395 * @param openSym the string that marks the start of a variable name
396 * @param closeSym the string that marks the start of a variable name
397 * @returns the resulting tokens as an object that has strings and variable names
398 */
399 function tokenize(template, options) {
400 if (options === void 0) { options = {}; }
401 if (!isStr(template)) {
402 throw new TypeError("The template parameter must be a string. Got a " + typeof template);
403 }
404 if (!isObj(options)) {
405 throw new TypeError("Options should be an object. Got a " + typeof options);
406 }
407 var _a = options.tags, tags = _a === void 0 ? ['{{', '}}'] : _a, _b = options.maxVarNameLength, maxVarNameLength = _b === void 0 ? 1000 : _b;
408 if (!isArr(tags) || tags.length !== 2) {
409 throw TypeError("tags should be an array of two elements. Got " + String(tags));
410 }
411 var openSym = tags[0], closeSym = tags[1];
412 if (!isStr(openSym, 1) || !isStr(closeSym, 1) || openSym === closeSym) {
413 throw new TypeError("The open and close symbols should be two distinct non-empty strings. Got \"" + openSym + "\" and \"" + closeSym + "\"");
414 }
415 if (!isNum(maxVarNameLength) || maxVarNameLength <= 0) {
416 throw new Error("Expected a positive number for maxVarNameLength. Got " + maxVarNameLength);
417 }
418 var openSymLen = openSym.length;
419 var closeSymLen = closeSym.length;
420 var openIndex;
421 var closeIndex = 0;
422 var varName;
423 var strings = [];
424 var varNames = [];
425 var currentIndex = 0;
426 while (currentIndex < template.length) {
427 openIndex = template.indexOf(openSym, currentIndex);
428 if (openIndex === -1) {
429 break;
430 }
431 var varNameStartIndex = openIndex + openSymLen;
432 closeIndex = template
433 .substr(varNameStartIndex, maxVarNameLength + closeSymLen)
434 .indexOf(closeSym);
435 if (closeIndex === -1) {
436 throw new SyntaxError("Missing \"" + closeSym + "\" in the template for the \"" + openSym + "\" at position " + openIndex + " within " + maxVarNameLength + " characters");
437 }
438 closeIndex += varNameStartIndex;
439 varName = template.substring(varNameStartIndex, closeIndex).trim();
440 if (varName.length === 0) {
441 throw new SyntaxError("Unexpected \"" + closeSym + "\" tag found at position " + openIndex);
442 }
443 if (varName.includes(openSym)) {
444 throw new SyntaxError("Variable names cannot have \"" + openSym + "\". But at position " + openIndex + ". Got \"" + varName + "\"");
445 }
446 varNames.push(varName);
447 closeIndex += closeSymLen;
448 strings.push(template.substring(currentIndex, openIndex));
449 currentIndex = closeIndex;
450 }
451 strings.push(template.substring(closeIndex));
452 return { strings: strings, varNames: varNames };
453 }
454
455 /**
456 * Compiles a template and returns an object with functions that render it.
457 * Compilation makes repeated render calls more optimized by parsing the
458 * template only once and reusing the results.
459 * As a result, rendering gets 3-5x faster.
460 * Caching is stored in the resulting object, so if you free up all the
461 * references to that object, the caches will be garbage collected.
462 *
463 * @param template same as the template parameter to .render()
464 * @param options some options for customizing the compilation
465 * @throws `TypeError` if the template is not a string
466 * @throws `TypeError` if the options is set but is not an object
467 * @throws any error that [[tokenize]] or [[Renderer.constructor]] may throw
468 * @returns a [[Renderer]] object which has render methods
469 */
470 function compile(template, options) {
471 if (options === void 0) { options = {}; }
472 var tokens = tokenize(template, options);
473 return new Renderer(tokens, options);
474 }
475
476 /**
477 * Replaces every {{varName}} inside the template with values from the scope
478 * parameter.
479 * @warning **When dealing with user input, always make sure to validate it.**
480 * @param template The template containing one or more {{varName}} as
481 * placeholders for values from the `scope` parameter.
482 * @param scope An object containing values for variable names from the the
483 * template. If it's omitted, we default to an empty object.
484 * Since functions are objects in javascript, the `scope` can technically be a
485 * function too but it won't be called. It'll be treated as an object and its
486 * properties will be used for the lookup.
487 * @param options same options as the [[compile]] function
488 * @throws any error that [[compile]] or [[Renderer.render]] may throw
489 * @returns Template where its variable names replaced with
490 * corresponding values.
491 */
492 function render(template, scope, options) {
493 var renderer = compile(template, options);
494 return renderer.render(scope);
495 }
496 /**
497 * Same as [[render]] but accepts a resolver function which will be responsible
498 * for returning a value for every varName.
499 * @param resolveFn a function that takes a variable name and resolves it to a value.
500 * The value can be a number, string or boolean. If it is not, it'll be "stringified".
501 * @throws any error that [[compile]] or [[Renderer.renderFn]] may throw
502 * @returns Template where its variable names replaced with what is returned from the resolver
503 * function for each varName.
504 */
505 function renderFn(template, resolveFn, scope, options) {
506 var renderer = compile(template, options);
507 return renderer.renderFn(resolveFn, scope);
508 }
509 /**
510 * Same as [[renderFn]] but supports asynchronous resolver functions
511 * (a function that returns a promise instead of the value).
512 * @param resolveFn an async function that takes a variable name and resolves it to a value.
513 * The value can be a number, string or boolean. If it is not, it'll be "stringified".
514 * @throws any error that [[compile]] or [[Renderer.renderFnAsync]] may throw
515 * @returns a promise that when resolved contains the template where its variable names replaced
516 * with what is returned from the resolver function for each varName.
517 */
518 function renderFnAsync(template, resolveFnAsync, scope, options) {
519 var renderer = compile(template, options);
520 return renderer.renderFnAsync(resolveFnAsync, scope);
521 }
522
523 exports.Renderer = Renderer;
524 exports.compile = compile;
525 exports.get = get;
526 exports.render = render;
527 exports.renderFn = renderFn;
528 exports.renderFnAsync = renderFnAsync;
529 exports.tokenize = tokenize;
530
531 Object.defineProperty(exports, '__esModule', { value: true });
532
533})));
534//# sourceMappingURL=micromustache.umd.js.map