UNPKG

mancha

Version:

Javscript HTML rendering engine

383 lines 12.8 kB
/* * @license * Portions Copyright (c) 2013, the Dart project authors. */ const _BINARY_OPERATORS = { "+": (a, b) => a + b, "-": (a, b) => a - b, "*": (a, b) => a * b, "/": (a, b) => a / b, "%": (a, b) => a % b, // biome-ignore lint/suspicious/noDoubleEquals: must be loose equality "==": (a, b) => a == b, // biome-ignore lint/suspicious/noDoubleEquals: must be loose equality "!=": (a, b) => a != b, "===": (a, b) => a === b, "!==": (a, b) => a !== b, ">": (a, b) => a > b, ">=": (a, b) => a >= b, "<": (a, b) => a < b, "<=": (a, b) => a <= b, "||": (a, b) => a || b, "&&": (a, b) => a && b, "??": (a, b) => a ?? b, "|": (a, f) => f(a), in: (a, b) => a in b, }; const _UNARY_OPERATORS = { "+": (a) => a, "-": (a) => -a, "!": (a) => !a, typeof: (a) => typeof a, }; export class EvalAstFactory { empty() { // TODO(justinfagnani): return null instead? return { type: "Empty", evaluate(scope) { return scope; }, getIds(idents) { return idents; }, }; } // TODO(justinfagnani): just use a JS literal? literal(v) { return { type: "Literal", value: v, evaluate(_scope) { return this.value; }, getIds(idents) { return idents; }, }; } id(v) { return { type: "ID", value: v, evaluate(scope) { // TODO(justinfagnani): this prevents access to properties named 'this' if (this.value === "this") return scope; return scope?.[this.value]; }, getIds(idents) { idents.push(this.value); return idents; }, }; } unary(op, expr) { const f = _UNARY_OPERATORS[op]; return { type: "Unary", operator: op, child: expr, evaluate(scope) { return f(this.child.evaluate(scope)); }, getIds(idents) { return this.child.getIds(idents); }, }; } binary(l, op, r) { const f = _BINARY_OPERATORS[op]; return { type: "Binary", operator: op, left: l, right: r, evaluate(scope) { if (this.operator === "=") { if (this.left.type !== "ID" && this.left.type !== "Getter" && this.left.type !== "Index") { throw new Error(`Invalid assignment target: ${this.left}`); } const value = this.right.evaluate(scope); let receiver; let property; if (this.left.type === "Getter") { receiver = this.left.receiver.evaluate(scope); property = this.left.name; } else if (this.left.type === "Index") { receiver = this.left.receiver.evaluate(scope); property = String(this.left.argument.evaluate(scope)); } else if (this.left.type === "ID") { // TODO: the id could be a parameter receiver = scope; property = this.left.value; } if (receiver === undefined) return undefined; receiver[property] = value; return value; } return f(this.left.evaluate(scope), this.right.evaluate(scope)); }, getIds(idents) { this.left.getIds(idents); this.right.getIds(idents); return idents; }, }; } getter(g, n, optional) { return { type: "Getter", receiver: g, name: n, optional, evaluate(scope) { const receiver = this.receiver.evaluate(scope); if (this.optional && (receiver === null || receiver === undefined)) { return undefined; } return receiver?.[this.name]; }, getIds(idents) { this.receiver.getIds(idents); return idents; }, }; } invoke(receiver, method, args, optional) { if (method != null && typeof method !== "string") { throw new Error("method not a string"); } return { type: "Invoke", receiver: receiver, method: method, arguments: args, optional, evaluate(scope) { const receiver = this.receiver.evaluate(scope); if (this.optional && (receiver === null || receiver === undefined)) { return undefined; } // TODO(justinfagnani): this might be wrong in cases where we're // invoking a top-level function rather than a method. If method is // defined on a nested scope, then we should probably set _this to null. const _this = this.method ? receiver : (scope?.this ?? scope); const f = this.method ? receiver?.[this.method] : receiver; const args = this.arguments ?? []; const argValues = []; for (const arg of args) { if (arg?.type === "SpreadElement") { const spreadVal = arg.evaluate(scope); if (spreadVal && typeof spreadVal[Symbol.iterator] === "function") { argValues.push(...spreadVal); } } else { argValues.push(arg?.evaluate(scope)); } } return f?.apply?.(_this, argValues); }, getIds(idents) { this.receiver.getIds(idents); this.arguments?.forEach((a) => { a?.getIds(idents); }); return idents; }, }; } paren(e) { return e; } index(e, a, optional) { return { type: "Index", receiver: e, argument: a, optional, evaluate(scope) { const receiver = this.receiver.evaluate(scope); if (this.optional && (receiver === null || receiver === undefined)) { return undefined; } const index = this.argument.evaluate(scope); return receiver?.[index]; }, getIds(idents) { this.receiver.getIds(idents); return idents; }, }; } ternary(c, t, f) { return { type: "Ternary", condition: c, trueExpr: t, falseExpr: f, evaluate(scope) { const c = this.condition.evaluate(scope); if (c) { return this.trueExpr.evaluate(scope); } else { return this.falseExpr.evaluate(scope); } }, getIds(idents) { this.condition.getIds(idents); this.trueExpr.getIds(idents); this.falseExpr.getIds(idents); return idents; }, }; } map(properties) { return { type: "Map", properties, evaluate(scope) { const map = {}; if (properties && this.properties) { for (const prop of this.properties) { if (prop.type === "SpreadProperty") { Object.assign(map, prop.evaluate(scope)); } else { map[prop.key] = prop.value.evaluate(scope); } } } return map; }, getIds(idents) { if (properties && this.properties) { for (const prop of this.properties) { if (prop.type === "SpreadProperty") { prop.expression.getIds(idents); } else { prop.value.getIds(idents); } } } return idents; }, }; } property(key, value) { return { type: "Property", key, value, evaluate(scope) { return this.value.evaluate(scope); }, getIds(idents) { return this.value.getIds(idents); }, }; } list(l) { return { type: "List", items: l, evaluate(scope) { if (!this.items) return []; const result = []; for (const item of this.items) { if (item?.type === "SpreadElement") { const spreadVal = item.evaluate(scope); if (spreadVal && typeof spreadVal[Symbol.iterator] === "function") { result.push(...spreadVal); } } else { result.push(item?.evaluate(scope)); } } return result; }, getIds(idents) { this.items?.forEach((i) => { i?.getIds(idents); }); return idents; }, }; } arrowFunction(params, body) { return { type: "ArrowFunction", params, body, evaluate(scope) { const params = this.params; const body = this.body; return (...args) => { // TODO: this isn't correct for assignments to variables in outer // scopes // const newScope = Object.create(scope ?? null); const paramsObj = Object.fromEntries(params.map((p, i) => [p, args[i]])); const newScope = new Proxy(scope ?? {}, { set(target, prop, value) { if (Object.hasOwn(paramsObj, prop)) { paramsObj[prop] = value; } target[prop] = value; return value; }, get(target, prop) { if (Object.hasOwn(paramsObj, prop)) { return paramsObj[prop]; } return target[prop]; }, }); return body.evaluate(newScope); }; }, getIds(idents) { // Only return the _free_ variables in the body. Since arrow function // parameters are the only way to introduce new variable names, we can // assume that any variable in the body that isn't a parameter is free. return this.body.getIds(idents).filter((id) => !this.params.includes(id)); }, }; } spreadProperty(expression) { return { type: "SpreadProperty", expression, evaluate(scope) { return this.expression.evaluate(scope); }, getIds(idents) { return this.expression.getIds(idents); }, }; } spreadElement(expression) { return { type: "SpreadElement", expression, evaluate(scope) { return this.expression.evaluate(scope); }, getIds(idents) { return this.expression.getIds(idents); }, }; } } //# sourceMappingURL=eval.js.map