easy-template-x-angular-expressions
Version:
Angular expressions support for easy-template-x
169 lines (151 loc) • 5.4 kB
JavaScript
;
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;