UNPKG

easy-template-x-angular-expressions

Version:
169 lines (151 loc) 5.4 kB
'use strict'; var easyTemplateX = require('easy-template-x'); var expressions = require('angular-expressions'); var getProp = require('lodash.get'); function isNumber(value) { return Number.isFinite(value); } function isObject(value) { return value !== null && value !== undefined && typeof value === 'object'; } /** * Naive string escaping. */ function strEscape(str) { return (str || "").replace(/"/g, '\\"'); } class ResolveError extends Error { constructor(expression, path, innerError) { super(`Failed to resolve expression "${strEscape(expression)}" (path: ${printPath(path)}). Inner error: ${innerError === null || innerError === void 0 ? void 0 : innerError.message}.`); // typescript hack: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work this.expression = expression; this.path = path; this.innerError = innerError; Object.setPrototypeOf(this, ResolveError.prototype); } } function printPath(path) { const strPath = (path || []).map(p => `"${strEscape(p ?? '')}"`).join(", "); return `[ ${strPath} ]`; } const simpleToken = "[a-zA-z_]\\w*"; const bracketToken = `\\[("[^"]+"|'[^']+'|\\d+)\\]`; const token = `((${simpleToken})|(${bracketToken}))`; const defaultFilterRegex = new RegExp(`^\\s*${token}(\\.${simpleToken}|${bracketToken})*\\s*$`); /** * The default path filters returns true for js object paths and false for other * js expressions. */ const defaultPathFilter = part => { if (isNumber(part)) { return true; } return defaultFilterRegex.test(part); }; class ResolverOptions { /** * If set to a non-empty string the resolver will require the tag name to * have this prefix (ignoring leading whitespace). * * Default: false */ requiredPrefix = false; /** * The path filter is used to determine which path parts to consider when * traversing the data object to construct the expression scope. Path parts * that returns `false` are skipped and the traversal moves to the next * part. */ pathFilter = defaultPathFilter; /** * Setting this option to `true` instructs the resolver to fallback to the default `easy-template-x` data resolver. * Setting it to `false` will return `undefined` instead. * * The fallback is used in the following cases: * * 1. Empty path. * 2. Numeric path parts (loop iteration index). * 3. When the `requiredPrefix` is specified and the tag name does not meet the requirement. * * Default: true */ defaultFallback = true; /** * Map of Angular filters. * * **Notice**: The filters are global. That is, filters of one Resolver may * override filters of another Resolver. */ angularFilters = {}; constructor(initial) { Object.assign(this, initial); if (this.requiredPrefix && this.requiredPrefix.trim() != this.requiredPrefix) { throw new Error("requirePrefix cannot contain leading or trailing whitespace"); } } } const undefinedResolver = () => undefined; class AngularResolver { constructor(options) { this.options = new ResolverOptions(options); this.fallback = this.options.defaultFallback ? easyTemplateX.ScopeData.defaultResolver : undefinedResolver; // Configure 'angular-expressions' filters. for (const key of Object.keys(this.options.angularFilters || {})) { expressions.filters[key] = this.options.angularFilters[key]; } } resolve(args) { // Fallback on empty paths. if (!args.path.length) { return this.fallback(args); } // Fallback on number paths (generated by the loop plugin). const lastPart = args.path[args.path.length - 1]; if (isNumber(lastPart)) { return this.fallback(args); } // Check required prefix. let exp = ((lastPart === null || lastPart === void 0 ? void 0 : lastPart.name) || "").trim(); if (this.options.requiredPrefix && !exp.startsWith(this.options.requiredPrefix)) { return this.fallback(args); } if (this.options.requiredPrefix) { exp = exp.substr(this.options.requiredPrefix.length); } // Flatten the scope. const finalScope = Object.assign({}, args.data); let curScop = finalScope; for (const part of args.path) { const partIndexer = isNumber(part) ? part : part.name; // Apply the path filter. if (this.options.pathFilter && !this.options.pathFilter(partIndexer)) { continue; } // If it's not an object don't go deeper. curScop = getProp(curScop, partIndexer); if (!isObject(curScop)) { break; } Object.assign(finalScope, curScop); } // Resolve the expression. exp = exp.replace(/(’|‘)/g, "'").replace(/(“|”)/g, '"'); try { return expressions.compile(exp)(finalScope); } catch (e) { throw new ResolveError(exp, args.strPath, e); } } } function createResolver(options) { const resolver = new AngularResolver(options); return resolver.resolve.bind(resolver); } exports.AngularResolver = AngularResolver; exports.ResolveError = ResolveError; exports.ResolverOptions = ResolverOptions; exports.createResolver = createResolver; exports.defaultPathFilter = defaultPathFilter; exports.isNumber = isNumber; exports.isObject = isObject; exports.strEscape = strEscape;