359 lines
10 KiB
PHP
359 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @author Tassos.gr <info@tassos.gr>
|
|
* @link https://www.tassos.gr
|
|
* @copyright Copyright © 2024 Tassos All Rights Reserved
|
|
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
|
|
*/
|
|
|
|
namespace NRFramework\Parser;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use NRFramework\Parser\Parser;
|
|
use NRFramework\Parser\ConditionLexer;
|
|
/**
|
|
* ConditionParser
|
|
* LL(1) recursive-decent parser
|
|
* Uses NRFramework\Parser\ConditionLexer as input source
|
|
*
|
|
* Grammar:
|
|
* --------
|
|
* expr : condition (logic_op condition)* (option)*
|
|
* condition : {negate_op} alias (parameter)* | (alias|l_func) ({negate_op}? operator (values)? (parameter)*
|
|
* alias : {ident}
|
|
* values : value ({comma} value)*
|
|
* value : {quotedval} | ({literal} | {ident})+
|
|
* func : {ident} {l_paren} values {r_paren}
|
|
* l_func : func
|
|
* r_func : func
|
|
* parameter : {param} ({equals} value)?
|
|
* option : {ident} ({equals} value)?
|
|
* logic_op : {and} | {or}
|
|
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
|
|
*/
|
|
class ConditionParser extends Parser
|
|
{
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param ConditionLexer $input
|
|
*/
|
|
public function __construct(ConditionLexer $input)
|
|
{
|
|
parent::__construct($input, 2);
|
|
}
|
|
|
|
/**
|
|
* value : {quotedval} | ({literal} | {ident})+
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function value()
|
|
{
|
|
if ($this->lookahead[0]->type === 'quotedvalue')
|
|
{
|
|
$text = $this->lookahead[0]->text;
|
|
$this->match('quotedvalue');
|
|
return $text;
|
|
}
|
|
else if ($this->lookahead[0]->type !== 'ident' && $this->lookahead[0]->type !== 'literal')
|
|
{
|
|
throw new \Exception("Syntax error in ConditionParser::value(); expecting 'ident' or 'literal'; found {$this->lookahead[0]}");
|
|
}
|
|
|
|
$text = $this->lookahead[0]->text;
|
|
$this->consume();
|
|
|
|
while ($this->lookahead[0]->type === 'ident' || $this->lookahead[0]->type === 'literal')
|
|
{
|
|
$text .= ' ' . $this->lookahead[0]->text;
|
|
$this->consume();
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* values : value ({comma} value)*
|
|
*
|
|
* @return array
|
|
*/
|
|
public function values()
|
|
{
|
|
$vals = [];
|
|
$vals[] = $this->value();
|
|
|
|
while ($this->lookahead[0]->type === 'comma')
|
|
{
|
|
$this->consume();
|
|
$vals[] = $this->value();
|
|
}
|
|
return $vals;
|
|
}
|
|
|
|
/**
|
|
* func : {ident} {l_paren} values {r_paren}
|
|
*
|
|
*/
|
|
public function func()
|
|
{
|
|
$func_name = $this->lookahead[0]->text;
|
|
$this->match('ident');
|
|
$this->match('l_paren');
|
|
|
|
if ($this->lookahead[0]->type === 'quotedvalue' ||
|
|
$this->lookahead[0]->type === 'ident' ||
|
|
$this->lookahead[0]->type === 'literal')
|
|
{
|
|
$func_args = $this->values();
|
|
}
|
|
|
|
$this->match('r_paren');
|
|
|
|
return ['func_name' => $func_name, 'func_args' => $func_args ?? []];
|
|
}
|
|
|
|
/**
|
|
* parameter : {param} ({equals} value)?
|
|
*
|
|
* @return string
|
|
*/
|
|
public function param()
|
|
{
|
|
$param = $this->lookahead[0]->text;
|
|
$value = true;
|
|
$this->match('param');
|
|
|
|
// If this is the 'context' parameter make sure that it appears as the last token
|
|
// if ($param === 'context')
|
|
// {
|
|
// $this->consume(); // consume the 'equals' operator
|
|
// $value = $this->value(); // expect a value
|
|
// if ($this->lookahead[0]->type !== 'EOF')
|
|
// {
|
|
// throw new \Exception("Syntax error in ConditionParser::param(); the 'context' parameter can only appear as the last token");
|
|
// }
|
|
// }
|
|
// else
|
|
if ($this->isOperator($this->lookahead[0]->type))
|
|
{
|
|
if ($this->lookahead[0]->type === 'equals')
|
|
{
|
|
$this->consume(); // consume the 'equals' operator
|
|
$value = $this->value(); // expect a value
|
|
}
|
|
else
|
|
{
|
|
// only the 'equals' operator is supported for the 'param' rule.
|
|
throw new \Exception("Syntax error in ConditionParser::param(); expecting 'equals', found {$this->lookahead[0]}");
|
|
}
|
|
}
|
|
|
|
return ['param' => $param, 'value' => $value];
|
|
}
|
|
|
|
/**
|
|
* alias : {ident}
|
|
*
|
|
* @return string
|
|
*/
|
|
public function alias()
|
|
{
|
|
$sel = $this->lookahead[0]->text;
|
|
$this->match('ident');
|
|
return $sel;
|
|
}
|
|
|
|
|
|
/**
|
|
* condition : {negate_op} alias (parameter)* | alias ({negate_op}? operator values)? (parameter)*
|
|
*
|
|
* @return object
|
|
*/
|
|
public function condition()
|
|
{
|
|
$result = [];
|
|
$operator = '';
|
|
$params = [];
|
|
$negate_op = false;
|
|
|
|
if ($this->lookahead[0]->type === 'negate_op')
|
|
{
|
|
$this->match('negate_op');
|
|
$operator = 'empty';
|
|
$result['alias'] = $this->alias();
|
|
}
|
|
else
|
|
{
|
|
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
|
|
{
|
|
$l_func = $this->func();
|
|
$result['l_func_name'] = $l_func['func_name'];
|
|
$result['l_func_args'] = $l_func['func_args'];
|
|
}
|
|
else
|
|
{
|
|
$result['alias'] = $this->alias();
|
|
}
|
|
|
|
if ($this->lookahead[0]->type === 'negate_op')
|
|
{
|
|
$this->match('negate_op');
|
|
$negate_op = true;
|
|
// expect an operator after '!'
|
|
if (!$this->isOperator($this->lookahead[0]->type))
|
|
{
|
|
throw new Exceptions\SyntaxErrorException("Expecting an 'operator' after '!', found {$this->lookahead[0]}");
|
|
}
|
|
}
|
|
if ($this->isOperator($this->lookahead[0]->type))
|
|
{
|
|
$operator = $this->operator();
|
|
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
|
|
{
|
|
$r_func = $this->func();
|
|
$result['r_func_name'] = $r_func['func_name'];
|
|
$result['r_func_args'] = $r_func['func_args'];
|
|
}
|
|
else if (
|
|
$this->lookahead[0]->type === 'quotedvalue' ||
|
|
$this->lookahead[0]->type === 'ident' ||
|
|
$this->lookahead[0]->type === 'literal'
|
|
)
|
|
{
|
|
|
|
|
|
$values = $this->values();
|
|
if (count($values) === 1)
|
|
{
|
|
$values = $values[0];
|
|
}
|
|
$result['values'] = $values;
|
|
}
|
|
}
|
|
}
|
|
|
|
while ($this->lookahead[0]->type === 'param')
|
|
{
|
|
$params[] = $this->param();
|
|
}
|
|
|
|
if (!$operator) {
|
|
$operator = 'empty';
|
|
$negate_op = true;
|
|
}
|
|
|
|
//
|
|
$_params = [];
|
|
foreach($params as $p)
|
|
{
|
|
$_params[$p['param']] = $p['value'];
|
|
}
|
|
|
|
$result['operator'] = $operator;
|
|
$result['negate_op'] = $negate_op;
|
|
$result['params'] = $_params;
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function operator()
|
|
{
|
|
if (!$this->isOperator($this->lookahead[0]->type))
|
|
{
|
|
throw new Exceptions\SyntaxErrorException("Expecting an 'operator', found " . $this->lookahead[0]);
|
|
}
|
|
|
|
$op = $this->lookahead[0]->type;
|
|
$this->consume();
|
|
return $op;
|
|
}
|
|
|
|
/**
|
|
* expr : condition ({logic_op} condition)* (option)*
|
|
*
|
|
* @return array The condition expression results
|
|
*/
|
|
public function expr()
|
|
{
|
|
$logic_op = 'and';
|
|
$res = [
|
|
'conditions' => [$this->condition()],
|
|
'logic_op' => 'and',
|
|
'context' => null,
|
|
'global_params' => []
|
|
];
|
|
|
|
if ($this->lookahead[0]->type === 'or')
|
|
{
|
|
$logic_op = 'or';
|
|
}
|
|
|
|
while ($this->lookahead[0]->type !== 'EOF')
|
|
{
|
|
$this->match($logic_op);
|
|
$res['conditions'][] = $this->condition();
|
|
}
|
|
|
|
$res['logic_op'] = $logic_op;
|
|
|
|
// check the last parsed condition for global parameters
|
|
$globalParams = [
|
|
'debug',
|
|
'dateformat',
|
|
'context',
|
|
'nopreparecontent',
|
|
'excludebots'
|
|
];
|
|
|
|
$last_params = $res['conditions'][count($res['conditions'])-1]['params'];
|
|
foreach(array_keys($last_params) as $param_key)
|
|
{
|
|
if (in_array(strtolower($param_key), $globalParams))
|
|
{
|
|
$res['global_params'][strtolower($param_key)] = $last_params[$param_key];
|
|
unset($res['conditions'][count($res['conditions'])-1]['params'][$param_key]);
|
|
}
|
|
}
|
|
// foreach ($last_params as $idx => $param)
|
|
// {
|
|
// if (in_array($param['param'], $globalParams))
|
|
// {
|
|
// $res['global_params'][$param['param']] = $param['value'];
|
|
// unset($res['conditions'][count($res['conditions'])-1]['params'][$idx]);
|
|
// }
|
|
// }
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Helper method that checks if the given Token is an operator.
|
|
*/
|
|
protected function isOperator($token_type)
|
|
{
|
|
|
|
return in_array($token_type, [
|
|
'equals',
|
|
'starts_with',
|
|
'ends_with',
|
|
'contains',
|
|
'contains_any',
|
|
'contains_all',
|
|
'contains_only',
|
|
'lt',
|
|
'lte',
|
|
'gt',
|
|
'gte',
|
|
'empty'
|
|
]);
|
|
}
|
|
}
|