File: /home/swtinter/public_html/wp-content/plugins/forminator/library/calculator/parser/class-parser.php
<?php
/**
* The Forminator_Calculator_Parser class.
*
* @package Forminator
*/
/**
* The parsers has one important method: parse()
* It takes an array of tokens as input and
* returns an array of nodes as output.
* These nodes are the syntax tree of the term.
*/
class Forminator_Calculator_Parser {
/**
* The symbol container with all possible symbols
*
* @var Forminator_Calculator_Symbol_Loader
*/
protected $symbol_loader;
/**
* Parser constructor.
*
* @param Forminator_Calculator_Symbol_Loader $symbol_loader Forminator_Calculator_Symbol_Loader.
*/
public function __construct( $symbol_loader ) {
$this->symbol_loader = $symbol_loader;
}
/**
* Parses an array with tokens. Returns an array of nodes.
* These nodes define a syntax tree.
*
* @param Forminator_Calculator_Parser_Token[] $tokens Forminator_Calculator_Parser_Token.
*
* @return Forminator_Calculator_Parser_Node_Container
*/
public function parse( $tokens ) {
$symbol_nodes = $this->detect_symbols( $tokens );
$nodes = $this->create_tree_by_brackets( $symbol_nodes );
$nodes = $this->transform_tree_by_functions( $nodes );
$this->check_grammar( $nodes );
// Wrap the nodes in an array node.
$root_node = new Forminator_Calculator_Parser_Node_Container( $nodes );
return $root_node;
}
/**
* Creates a flat array of symbol nodes from tokens.
*
* @param Forminator_Calculator_Parser_Token[] $tokens Forminator_Calculator_Parser_Token.
*
* @return Forminator_Calculator_Parser_Node_Symbol[]
* @throws Forminator_Calculator_Exception When there is an Calculator error.
*/
protected function detect_symbols( $tokens ) {
$symbol_nodes = array();
$expecting_opening_bracket = false; // True if we expect an opening bracket (after a function name).
$open_bracket_counter = 0;
foreach ( $tokens as $token ) {
$type = $token->type;
if ( Forminator_Calculator_Parser_Token::TYPE_WORD === $type ) {
$identifier = $token->value;
$symbol = $this->symbol_loader->find( $identifier );
if ( null === $symbol ) {
throw new Forminator_Calculator_Exception( 'Error: Detected unknown or invalid string identifier: ' . $identifier . '.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
} elseif ( Forminator_Calculator_Parser_Token::TYPE_NUMBER === $type ) {
// Notice: Numbers do not have an identifier.
$symbol_numbers = $this->symbol_loader->find_sub_types( 'Forminator_Calculator_Symbol_Number' );
if ( empty( $symbol_numbers ) || ! is_array( $symbol_numbers ) ) {
throw new Forminator_Calculator_Exception( 'Error: Unavailable number symbol processor.' );// @codeCoverageIgnore.
}
$symbol = $symbol_numbers[0];
} else { // Type Token::TYPE_CHARACTER:.
$identifier = $token->value;
$symbol = $this->symbol_loader->find( $identifier );
if ( null === $symbol ) {
throw new Forminator_Calculator_Exception( 'Error: Detected unknown or invalid string identifier: ' . $identifier . '.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
if ( $symbol instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
++$open_bracket_counter;
}
if ( $symbol instanceof Forminator_Calculator_Symbol_Closing_Bracket ) {
--$open_bracket_counter;
// Make sure there are not too many closing brackets.
if ( $open_bracket_counter < 0 ) {
throw new Forminator_Calculator_Exception( 'Error: Found closing bracket that does not have an opening bracket.' );
}
}
}
// Make sure a function is not followed by a symbol that is not of type opening bracket.
if ( $expecting_opening_bracket ) {
if ( ! $symbol instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
throw new Forminator_Calculator_Exception( 'Error: Expected opening bracket (after a function) but got something else.' );
}
$expecting_opening_bracket = false;
} elseif ( $symbol instanceof Forminator_Calculator_Symbol_Function_Abstract ) {
$expecting_opening_bracket = true;
}
$symbol_node = new Forminator_Calculator_Parser_Node_Symbol( $token, $symbol );
$symbol_nodes[] = $symbol_node;
}
// Make sure the term does not end with the name of a function but without an opening bracket.
if ( $expecting_opening_bracket ) {
throw new Forminator_Calculator_Exception( 'Error: Expected opening bracket (after a function) but reached the end of the term' );
}
// Make sure there are not too many opening brackets.
if ( $open_bracket_counter > 0 ) {
throw new Forminator_Calculator_Exception( 'Error: There is at least one opening bracket that does not have a closing bracket' );
}
return $symbol_nodes;
}
/**
* Expects a flat array of symbol nodes and (if possible) transforms
* it to a tree of nodes. Cares for brackets.
* Attention: Expects valid brackets!
* Check the brackets before you call this method.
*
* @param Forminator_Calculator_Parser_Node_Symbol[] $symbol_nodes Forminator_Calculator_Parser_Node_Symbol.
*
* @return Forminator_Calculator_Parser_Node_Abstract[]
* @throws Forminator_Calculator_Exception When there is an Calculator error.
*/
protected function create_tree_by_brackets( $symbol_nodes ) {
$tree = array();
$nodes_in_brackets = array(); // AbstractSymbol nodes inside level-0-brackets.
$open_bracket_counter = 0;
foreach ( $symbol_nodes as $index => $symbol_node ) {
if ( ! $symbol_node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
throw new Forminator_Calculator_Exception( 'Error: Expected symbol node, but got "' . esc_html( gettype( $symbol_node ) ) . '"' );// @codeCoverageIgnore.
}
if ( $symbol_node->get_symbol() instanceof Forminator_Calculator_Symbol_Opening_Bracket ) {
++$open_bracket_counter;
if ( $open_bracket_counter > 1 ) {
$nodes_in_brackets[] = $symbol_node;
}
} elseif ( $symbol_node->get_symbol() instanceof Forminator_Calculator_Symbol_Closing_Bracket ) {
--$open_bracket_counter;
// Found a closing bracket on level 0.
if ( 0 === $open_bracket_counter ) {
$sub_tree = $this->create_tree_by_brackets( $nodes_in_brackets );
// Subtree can be empty for example if the term looks like this: "()" or "functioname()".
// But this is okay, we need to allow this so we can call functions without a parameter.
$tree[] = new Forminator_Calculator_Parser_Node_Container( $sub_tree );
$nodes_in_brackets = array();
} else {
$nodes_in_brackets[] = $symbol_node;
}
} elseif ( 0 === $open_bracket_counter ) {
$tree[] = $symbol_node;
} else {
$nodes_in_brackets[] = $symbol_node;
}
}
return $tree;
}
/**
* Replaces [a SymbolNode that has a symbol of type AbstractFunction,
* followed by a node of type ContainerNode] by a FunctionNode.
* Expects the $nodes not including any function nodes (yet).
*
* @param Forminator_Calculator_Parser_Node_Abstract[] $nodes Forminator_Calculator_Parser_Node_Abstract.
*
* @return Forminator_Calculator_Parser_Node_Abstract[]
* @throws Forminator_Calculator_Exception When there is an Calculator error.
*/
protected function transform_tree_by_functions( $nodes ) {
$transformed_nodes = array();
$function_symbol_node = null;
foreach ( $nodes as $node ) {
if ( $node instanceof Forminator_Calculator_Parser_Node_Container ) {
/**
* Forminator_Calculator_Parser_Node_Container
*
* @var Forminator_Calculator_Parser_Node_Container $node */
$transformed_child_nodes = $this->transform_tree_by_functions( $node->get_child_nodes() );
if ( null !== $function_symbol_node ) {
$function_node = new Forminator_Calculator_Parser_Node_Function( $transformed_child_nodes, $function_symbol_node );
$transformed_nodes[] = $function_node;
$function_symbol_node = null;
} else {
// not a function.
$node->set_child_nodes( $transformed_child_nodes );
$transformed_nodes[] = $node;
}
} elseif ( $node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
/**
* Forminator_Calculator_Parser_Node_Symbol
*
* @var Forminator_Calculator_Parser_Node_Symbol $node */
$symbol = $node->get_symbol();
if ( $symbol instanceof Forminator_Calculator_Symbol_Function_Abstract ) {
$function_symbol_node = $node;
} else {
$transformed_nodes[] = $node;
}
} else {
throw new Forminator_Calculator_Exception( 'Error: Expected array node or symbol node, got "' . esc_html( gettype( $node ) ) . '"' );
}
}
return $transformed_nodes;
}
/**
* Ensures the tree follows the grammar rules for terms
*
* @param array $nodes Nodes.
*
* @return void
* @throws Forminator_Calculator_Exception When there is an Calculator error.
*/
protected function check_grammar( $nodes ) {
// TODO Make sure that separators are only in the child nodes of the array node of a function node.
// (If this happens the calculator will throw an exception).
foreach ( $nodes as $index => $node ) {
if ( $node instanceof Forminator_Calculator_Parser_Node_Symbol ) {
/**
* Forminator_Calculator_Parser_Node_Symbol
*
* @var $node Forminator_Calculator_Parser_Node_Symbol */
$symbol = $node->get_symbol();
if ( $symbol instanceof Forminator_Calculator_Symbol_Operator_Abstract ) {
/**
* Forminator_Calculator_Symbol_Operator_Abstract
*
* @var $symbol Forminator_Calculator_Symbol_Operator_Abstract */
$pos_of_right_operand = $index + 1;
// Make sure the operator is positioned left of a (potential) operand (=prefix notation).
// Example term: "-1".
if ( $pos_of_right_operand >= count( $nodes ) ) {
throw new Forminator_Calculator_Exception( 'Error: Found operator that does not stand before an operand.' );
}
$pos_of_left_operand = $index - 1;
$left_operand = null;
// Operator is unary if positioned at the beginning of a term.
if ( $pos_of_left_operand >= 0 ) {
$left_operand = $nodes[ $pos_of_left_operand ];
if ( $left_operand instanceof Forminator_Calculator_Parser_Node_Symbol ) {
/**
* Forminator_Calculator_Parser_Node_Symbol
*
* @var $left_operand Forminator_Calculator_Parser_Node_Symbol */
if ( $left_operand->get_symbol() instanceof Forminator_Calculator_Symbol_Operator_Abstract // example 1`+-`5 : + = operator, - = unary.
|| $left_operand->get_symbol() instanceof Forminator_Calculator_Symbol_Separator // example func(1`,-`5) ,= separator, - = unary.
) {
// Operator is unary if positioned right to another operator.
$left_operand = null;
}
}
}
// If null, the operator is unary.
if ( null === $left_operand ) {
if ( ! $symbol->get_operates_unary() ) {
throw new Forminator_Calculator_Exception( 'Error: Found operator in unary notation that is not unary.' );
}
// Remember that this node represents a unary operator.
$node->set_is_unary_operator( true );
} elseif ( ! $symbol->get_operates_binary() ) {
throw new Forminator_Calculator_Exception( 'Error: Found operator in binary notation that is not binary.' );
}
}
} else {
/**
* Forminator_Calculator_Parser_Node_Container
*
* @var $node Forminator_Calculator_Parser_Node_Container */
$this->check_grammar( $node->get_child_nodes() );
}
}
}
}