Last active
May 24, 2022 21:35
-
-
Save hmarcelodn/cd16370b845d31fb48e32bbc9dbe52dd to your computer and use it in GitHub Desktop.
interpreter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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