avocado-old/packages/behavior/compilers.hooks.js
2020-06-24 03:07:00 -05:00

146 lines
4.2 KiB
JavaScript

import {fastApply} from '@avocado/core';
import compile from './compile';
const render = (ops) => ops.reduce((rendered, op) => {
switch (op.type) {
case 'key':
return `${rendered ? `${rendered}.` : ''}${op.key}`;
case 'invoke':
return `${rendered}(${op.args.length > 0 ? '...' : ''})`;
default:
return rendered;
}
}, '');
function compileOp(op) {
let args;
if ('invoke' === op.type) {
args = op.args.map(compile);
}
return (context, previous, current) => {
switch (op.type) {
case 'key':
return current[op.key];
case 'invoke':
// Pass the context itself as the last arg.
const evaluated = args.map((fn) => fn(context)).concat(context);
// Promises are resolved transparently.
const apply = (args) => fastApply(previous, current, args);
return evaluated.some((arg) => arg instanceof Promise)
? Promise.all(evaluated).then(apply)
: apply(evaluated);
}
};
}
function disambiguateResult(value, fn) {
const apply = (value) => fn(value);
return value instanceof Promise ? value.then(apply) : apply(value);
}
function compileExpression(expression) {
if (0 === expression.ops.length) {
return () => undefined;
}
const assign = 'undefined' !== typeof expression.assign
? compile(expression.assign)
: undefined;
const ops = expression.ops.map(compileOp);
const {ops: rawOps} = expression;
return (context) => {
let previous = null;
let shorted = false;
const [first, ...rest] = ops;
return rest.reduce((current, op, index) => disambiguateResult(current, (current) => {
if (shorted) {
return undefined;
}
let next = undefined;
const isLastOp = index === ops.length - 2;
if (!isLastOp || !assign) {
if ('undefined' === typeof current) {
const rendered = render(rawOps.slice(0, index + 2));
next = Promise.reject(new Error(`'${rendered}' is undefined`));
shorted = true;
}
else {
next = op(context, previous, current);
}
}
else {
const rawOp = rawOps[index + 1];
switch (rawOp.type) {
case 'key':
if ('object' === typeof current) {
current[rawOp.key] = assign(context);
next = undefined;
}
break;
case 'invoke':
const rendered = render(rawOps.slice(0, index + 2));
next = Promise.reject(new Error(`invalid assignment to function '${rendered}'`));
break;
}
}
previous = current;
current = next;
return current;
}), context.getValue(rawOps[0].key));
};
}
function compileCondition(condition) {
const {operator} = condition;
const operands = condition.operands.map(compile);
return (context) => {
switch (operator) {
case 'is':
return operands[0](context) === operands[1](context);
case 'isnt':
return operands[0](context) !== operands[1](context);
case '>':
return operands[0](context) > operands[1](context);
case '>=':
return operands[0](context) >= operands[1](context);
case '<':
return operands[0](context) < operands[1](context);
case '<=':
return operands[0](context) <= operands[1](context);
case 'or':
if (0 === operands.length) {
return true;
}
for (let i = 0; i < operands.length; i++) {
if (!!operands[i](context)) {
return true;
}
}
return false;
case 'and':
if (0 === operands.length) {
return true;
}
for (let i = 0; i < operands.length; i++) {
if (!operands[i](context)) {
return false;
}
}
return true;
case 'contains':
const haystack = operands[0](context);
const needle = operands[1](context);
return -1 !== haystack.indexOf(needle);
}
};
}
export function behaviorCompilers() {
return {
condition: (condition) => compileCondition(condition),
literal: ({value}) => (context) => value,
expression: (expression) => compileExpression(expression),
expressions: ({expressions}) => () => expressions.map(compileExpression),
};
}