Skip to content

Instantly share code, notes, and snippets.

@hmarcelodn
Last active May 24, 2022 21:35
Show Gist options
  • Save hmarcelodn/cd16370b845d31fb48e32bbc9dbe52dd to your computer and use it in GitHub Desktop.
Save hmarcelodn/cd16370b845d31fb48e32bbc9dbe52dd to your computer and use it in GitHub Desktop.
interpreter
import peg from 'pegjs';
import HttpException from '../exceptions/HttpException';
import {
Functions,
TemplateFunctionsService,
} from '../services';
import {
AnswerListingIndexedItem,
ConstantListingIndexedItem,
QuestionListingIndexedItem,
VariableListingIndexedItem,
} from '../entities';
export class THLInterpreterService {
protected highlightEnabled = false;
protected answers: AnswerListingIndexedItem;
protected questions: QuestionListingIndexedItem;
protected variables: VariableListingIndexedItem;
protected constants: ConstantListingIndexedItem;
protected functionRe = new RegExp(/\$(?<function>[\w]+)/);
constructor(
protected readonly parser: peg.Parser,
protected readonly templateFunctionsService: TemplateFunctionsService = new TemplateFunctionsService(),
) {}
public parse(
text: string,
answers: AnswerListingIndexedItem,
questions: QuestionListingIndexedItem,
variables: VariableListingIndexedItem,
constants: ConstantListingIndexedItem,
preview = false,
) {
try {
this.highlightEnabled = preview;
this.answers = answers;
this.questions = questions;
this.variables = variables;
this.constants = constants;
const ast = this.parser.parse(
text || '',
);
return this.visitAst(
ast,
);
} catch (e) {
throw new HttpException(
400,
`Line ${e.location.start.line}, column ${e.location.start.column}, ${e.message}`,
);
}
}
protected replaceVariables(text: string, variables: VariableListingIndexedItem, re = this.variableToken.getGlobalRegex()): string {
return text ? text.replace(re, (_, variableId) => variables[variableId]?.content) : '';
}
protected replaceConstants(text: string, constants: ConstantListingIndexedItem, re = this.constantToken.getGlobalRegex()): string {
return text ? text.replace(re, (_, constantId) => constants[constantId]?.value) : '';
}
protected replaceDynamicTokens(value: string, highlight = false) {
if (this.questionToken.getGlobalRegex().test(value)) {
const replacedToken = this.replaceQuestionTokens(
value,
this.answers,
this.questions,
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
if (this.questionToken.getConditionParamRegex().test(value)) {
const replacedToken = this.replaceQuestionTokens(
value,
this.answers,
this.questions,
this.questionToken.getConditionParamRegex(),
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
if (this.variableToken.getGlobalRegex().test(value)) {
const replacedToken = this.replaceVariables(
value,
this.variables,
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
if (this.variableToken.getConditionParamRegex().test(value)) {
const replacedToken = this.replaceVariables(
value,
this.variables,
this.variableToken.getConditionParamRegex(),
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
if (this.constantToken.getGlobalRegex().test(value)) {
const replacedToken = this.replaceConstants(
value,
this.constants,
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
if (this.constantToken.getConditionParamRegex().test(value)) {
const replacedToken = this.replaceConstants(
value,
this.constants,
this.constantToken.getConditionParamRegex(),
);
return this.highlightEnabled && highlight ? this.highlightContent(replacedToken) : replacedToken;
}
return value;
}
protected evaluateConditionTerm(lhs: any, rhs: any, op: any) {
if (typeof lhs === 'string') {
lhs = this.replaceDynamicTokens(lhs, false) || '';
}
if (typeof rhs === 'string') {
rhs = this.replaceDynamicTokens(rhs, false) || '';
}
if (typeof lhs === 'object') {
lhs = this.visitFunctionExpressions(lhs) || '';
}
if (typeof rhs === 'object') {
rhs = this.visitFunctionExpressions(rhs) || '';
}
switch (op) {
case '<':
return parseFloat(lhs) < parseFloat(rhs);
case '>':
return parseFloat(lhs) > parseFloat(rhs);
case '>=':
return parseFloat(lhs) >= parseFloat(rhs);
case '<=':
return parseFloat(lhs) <= parseFloat(rhs);
case '$=':
return lhs.trim().includes(rhs.trim());
case '=':
return lhs.trim() === rhs.trim();
case '!=':
return lhs.trim() !== rhs.trim();
default:
return false;
}
}
protected visitBooleanExpressions(tree: any): boolean {
const results = [];
if (tree?.predicates) {
const operator = tree.op;
const predicates = tree.predicates;
let recursiveResult = false;
let hasRecursion = false;
for (const predicateExpressionNode of predicates) {
if (predicateExpressionNode.predicates) {
hasRecursion = true;
recursiveResult = this.visitBooleanExpressions(predicateExpressionNode);
} else {
const { lhs, rhs, op } = predicateExpressionNode;
results.push(this.evaluateConditionTerm(lhs, rhs, op));
}
}
if (operator === '||') {
return results.some(r => r === true) || (hasRecursion ? recursiveResult : true);
}
return results.every(r => r === true) && (hasRecursion ? recursiveResult : true);
}
return this.evaluateConditionTerm(tree.lhs, tree.rhs, tree.op);
}
protected visitFunctionExpressions(functionExpression: any, highlight = false, level = 0): string {
// Base Case (not nested functions)
if (typeof functionExpression === 'string') {
return this.replaceDynamicTokens(functionExpression);
}
// Recursive Case
const { functionStatement } = functionExpression;
const { func, param } = functionStatement;
const functionName = this.functionRe.exec(func)[1];
const functionOutput = this.templateFunctionsService.execute(
functionName as Functions,
this.visitFunctionExpressions(param, highlight, level + 1),
);
return level === 0 && highlight && this.highlightEnabled ?
this.highlightContent(functionOutput) :
functionOutput;
}
protected visitAst(ast: any, level = 0): string {
let template = '';
if (ast.length) {
template = ast.reduce((acc: string, node: any) => {
if (typeof node === 'string') {
acc += this.replaceDynamicTokens(node, true);
}
if (typeof node === 'object' && node.conditionalStatement) {
const { conditionalStatement } = node;
const { conditional, body1, body2 } = conditionalStatement;
const result = this.visitBooleanExpressions(conditional);
if (result) {
const booleanLeftContent = this.visitAst(body1, level + 1);
acc += level === 0 ? this.highlightContent(booleanLeftContent) : booleanLeftContent;
} else {
const booleanRightContent = this.visitAst(body2, level + 1);
acc += level === 0 ? this.highlightContent(booleanRightContent) : booleanRightContent;
}
}
if (typeof node === 'object' && node.functionStatement) {
acc += this.visitFunctionExpressions(node, true);
}
return acc;
}, '');
}
return template;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment