Compare commits

...

2 Commits

Author SHA1 Message Date
Simon Wisselink 4ea74baa04 missing file 2026-02-15 14:20:30 +01:00
Simon Wisselink 7f1b6718d1 Regeg matches operator support
Fixes #938
2026-02-15 14:19:59 +01:00
5 changed files with 1341 additions and 1152 deletions
+44 -38
View File
@@ -160,6 +160,7 @@ class TemplateLexer
'LOGOP' => '"<", "==" ... logical operator',
'TLOGOP' => '"lt", "eq" ... logical operator; "is div by" ... if condition',
'SCOND' => '"is even" ... if condition',
'MATCHES' => '"matches" regex operator',
);
/**
@@ -567,7 +568,7 @@ class TemplateLexer
public function yylex3()
{
if (!isset($this->yy_global_pattern3)) {
$this->yy_global_pattern3 = $this->replace("/\G(\\s*SMARTYrdel)|\G((SMARTYldel)SMARTYal)|\G([\"])|\G('[^'\\\\]*(?:\\\\.[^'\\\\]*)*')|\G([$][0-9]*[a-zA-Z_]\\w*)|\G([$])|\G(\\s+is\\s+(not\\s+)?in\\s+)|\G(\\s+as\\s+)|\G(\\s+to\\s+)|\G(\\s+step\\s+)|\G(\\s+instanceof\\s+)|\G(\\s*([!=][=]{1,2}|[<][=>]?|[>][=]?|[&|]{2})\\s*)|\G(\\s+(eq|ne|neq|gt|ge|gte|lt|le|lte|mod|and|or|xor)\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even|div)\\s+by\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even))|\G([!]\\s*|not\\s+)|\G([(](int(eger)?|bool(ean)?|float|double|real|string|binary|array|object)[)]\\s*)|\G(\\s*[(]\\s*)|\G(\\s*[)])|\G(\\[\\s*)|\G(\\s*\\])|\G(\\s*[-][>]\\s*)|\G(\\s*[=][>]\\s*)|\G(\\s*[=]\\s*)|\G(([+]|[-]){2})|\G(\\s*([+]|[-])\\s*)|\G(\\s*([*]{1,2}|[%\/^&]|[<>]{2})\\s*)|\G([@])|\G(array\\s*[(]\\s*)|\G([#])|\G(\\s+[0-9]*[a-zA-Z_][a-zA-Z0-9_\-:]*\\s*[=]\\s*)|\G(([0-9]*[a-zA-Z_]\\w*)?(\\\\[0-9]*[a-zA-Z_]\\w*)+)|\G([0-9]*[a-zA-Z_]\\w*)|\G(\\d+)|\G([`])|\G([|][@]?)|\G([.])|\G(\\s*[,]\\s*)|\G(\\s*[;]\\s*)|\G([:]{2})|\G(\\s*[:]\\s*)|\G(\\s*[?]\\s*)|\G(0[xX][0-9a-fA-F]+)|\G(\\s+)|\G([\S\s])/isS");
$this->yy_global_pattern3 = $this->replace("/\G(\\s*SMARTYrdel)|\G((SMARTYldel)SMARTYal)|\G([\"])|\G('[^'\\\\]*(?:\\\\.[^'\\\\]*)*')|\G([$][0-9]*[a-zA-Z_]\\w*)|\G([$])|\G(\\s+is\\s+(not\\s+)?in\\s+)|\G(\\s+matches\\s+)|\G(\\s+as\\s+)|\G(\\s+to\\s+)|\G(\\s+step\\s+)|\G(\\s+instanceof\\s+)|\G(\\s*([!=][=]{1,2}|[<][=>]?|[>][=]?|[&|]{2})\\s*)|\G(\\s+(eq|ne|neq|gt|ge|gte|lt|le|lte|mod|and|or|xor)\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even|div)\\s+by\\s+)|\G(\\s+is\\s+(not\\s+)?(odd|even))|\G([!]\\s*|not\\s+)|\G([(](int(eger)?|bool(ean)?|float|double|real|string|binary|array|object)[)]\\s*)|\G(\\s*[(]\\s*)|\G(\\s*[)])|\G(\\[\\s*)|\G(\\s*\\])|\G(\\s*[-][>]\\s*)|\G(\\s*[=][>]\\s*)|\G(\\s*[=]\\s*)|\G(([+]|[-]){2})|\G(\\s*([+]|[-])\\s*)|\G(\\s*([*]{1,2}|[%\/^&]|[<>]{2})\\s*)|\G([@])|\G(array\\s*[(]\\s*)|\G([#])|\G(\\s+[0-9]*[a-zA-Z_][a-zA-Z0-9_\-:]*\\s*[=]\\s*)|\G(([0-9]*[a-zA-Z_]\\w*)?(\\\\[0-9]*[a-zA-Z_]\\w*)+)|\G([0-9]*[a-zA-Z_]\\w*)|\G(\\d+)|\G([`])|\G([|][@]?)|\G([.])|\G(\\s*[,]\\s*)|\G(\\s*[;]\\s*)|\G([:]{2})|\G(\\s*[:]\\s*)|\G(\\s*[?]\\s*)|\G(0[xX][0-9a-fA-F]+)|\G(\\s+)|\G([\S\s])/isS");
}
if (!isset($this->dataLength)) {
$this->dataLength = strlen($this->data);
@@ -662,119 +663,124 @@ class TemplateLexer
public function yy_r3_10()
{
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
$this->token = \Smarty\Parser\TemplateParser::TP_MATCHES;
}
public function yy_r3_11()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TO;
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
}
public function yy_r3_12()
{
$this->token = \Smarty\Parser\TemplateParser::TP_STEP;
$this->token = \Smarty\Parser\TemplateParser::TP_TO;
}
public function yy_r3_13()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INSTANCEOF;
$this->token = \Smarty\Parser\TemplateParser::TP_STEP;
}
public function yy_r3_14()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INSTANCEOF;
}
public function yy_r3_15()
{
$this->token = \Smarty\Parser\TemplateParser::TP_LOGOP;
}
public function yy_r3_16()
public function yy_r3_17()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SLOGOP;
}
public function yy_r3_18()
public function yy_r3_19()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TLOGOP;
}
public function yy_r3_21()
public function yy_r3_22()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SINGLECOND;
}
public function yy_r3_24()
public function yy_r3_25()
{
$this->token = \Smarty\Parser\TemplateParser::TP_NOT;
}
public function yy_r3_25()
public function yy_r3_26()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TYPECAST;
}
public function yy_r3_29()
public function yy_r3_30()
{
$this->token = \Smarty\Parser\TemplateParser::TP_OPENP;
}
public function yy_r3_30()
public function yy_r3_31()
{
$this->token = \Smarty\Parser\TemplateParser::TP_CLOSEP;
}
public function yy_r3_31()
public function yy_r3_32()
{
$this->token = \Smarty\Parser\TemplateParser::TP_OPENB;
}
public function yy_r3_32()
public function yy_r3_33()
{
$this->token = \Smarty\Parser\TemplateParser::TP_CLOSEB;
}
public function yy_r3_33()
public function yy_r3_34()
{
$this->token = \Smarty\Parser\TemplateParser::TP_PTR;
}
public function yy_r3_34()
public function yy_r3_35()
{
$this->token = \Smarty\Parser\TemplateParser::TP_APTR;
}
public function yy_r3_35()
public function yy_r3_36()
{
$this->token = \Smarty\Parser\TemplateParser::TP_EQUAL;
}
public function yy_r3_36()
public function yy_r3_37()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INCDEC;
}
public function yy_r3_38()
public function yy_r3_39()
{
$this->token = \Smarty\Parser\TemplateParser::TP_UNIMATH;
}
public function yy_r3_40()
public function yy_r3_41()
{
$this->token = \Smarty\Parser\TemplateParser::TP_MATH;
}
public function yy_r3_42()
public function yy_r3_43()
{
$this->token = \Smarty\Parser\TemplateParser::TP_AT;
}
public function yy_r3_43()
public function yy_r3_44()
{
$this->token = \Smarty\Parser\TemplateParser::TP_ARRAYOPEN;
}
public function yy_r3_44()
public function yy_r3_45()
{
$this->token = \Smarty\Parser\TemplateParser::TP_HATCH;
}
public function yy_r3_45()
public function yy_r3_46()
{
// resolve conflicts with shorttag and right_delimiter starting with '='
@@ -786,73 +792,73 @@ class TemplateLexer
$this->token = \Smarty\Parser\TemplateParser::TP_ATTR;
}
}
public function yy_r3_46()
public function yy_r3_47()
{
$this->token = \Smarty\Parser\TemplateParser::TP_NAMESPACE;
}
public function yy_r3_49()
public function yy_r3_50()
{
$this->token = \Smarty\Parser\TemplateParser::TP_ID;
}
public function yy_r3_50()
public function yy_r3_51()
{
$this->token = \Smarty\Parser\TemplateParser::TP_INTEGER;
}
public function yy_r3_51()
public function yy_r3_52()
{
$this->token = \Smarty\Parser\TemplateParser::TP_BACKTICK;
$this->yypopstate();
}
public function yy_r3_52()
public function yy_r3_53()
{
$this->token = \Smarty\Parser\TemplateParser::TP_VERT;
}
public function yy_r3_53()
public function yy_r3_54()
{
$this->token = \Smarty\Parser\TemplateParser::TP_DOT;
}
public function yy_r3_54()
public function yy_r3_55()
{
$this->token = \Smarty\Parser\TemplateParser::TP_COMMA;
}
public function yy_r3_55()
public function yy_r3_56()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SEMICOLON;
}
public function yy_r3_56()
public function yy_r3_57()
{
$this->token = \Smarty\Parser\TemplateParser::TP_DOUBLECOLON;
}
public function yy_r3_57()
public function yy_r3_58()
{
$this->token = \Smarty\Parser\TemplateParser::TP_COLON;
}
public function yy_r3_58()
public function yy_r3_59()
{
$this->token = \Smarty\Parser\TemplateParser::TP_QMARK;
}
public function yy_r3_59()
public function yy_r3_60()
{
$this->token = \Smarty\Parser\TemplateParser::TP_HEX;
}
public function yy_r3_60()
public function yy_r3_61()
{
$this->token = \Smarty\Parser\TemplateParser::TP_SPACE;
}
public function yy_r3_61()
public function yy_r3_62()
{
$this->token = \Smarty\Parser\TemplateParser::TP_TEXT;
+5
View File
@@ -160,6 +160,7 @@ class TemplateLexer
'LOGOP' => '"<", "==" ... logical operator',
'TLOGOP' => '"lt", "eq" ... logical operator; "is div by" ... if condition',
'SCOND' => '"is even" ... if condition',
'MATCHES' => '"matches" regex operator',
);
/**
@@ -325,6 +326,7 @@ class TemplateLexer
tlop = ~\s+is\s+(not\s+)?(odd|even|div)\s+by\s+~
scond = ~\s+is\s+(not\s+)?(odd|even)~
isin = ~\s+is\s+(not\s+)?in\s+~
matches = ~\s+matches\s+~
as = ~\s+as\s+~
to = ~\s+to\s+~
step = ~\s+step\s+~
@@ -469,6 +471,9 @@ class TemplateLexer
isin {
$this->token = \Smarty\Parser\TemplateParser::TP_ISIN;
}
matches {
$this->token = \Smarty\Parser\TemplateParser::TP_MATCHES;
}
as {
$this->token = \Smarty\Parser\TemplateParser::TP_AS;
}
+1165 -1114
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -681,6 +681,11 @@ expr(res) ::= expr(e1) isin(c) value(v). {
res = c . e1.',(array)'.v.')';
}
// regex matching
expr(res) ::= expr(e1) matchop(c) value(e2). {
res = c . e2 . ',' . e1 . ') ';
}
// null coalescing
nullcoalescing(res) ::= expr(v) QMARK QMARK expr(e2). {
res = v.' ?? '.e2;
@@ -1313,6 +1318,10 @@ scond(res) ::= SINGLECOND(o). {
res = $scond[$op];
}
matchop(res) ::= MATCHES(o). {
res = 'preg_match(';
}
//
// ARRAY element assignment
//
@@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/../../../../Bootstrap.php';
class MatchesOperatorTest extends PHPUnit_Smarty {
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* Test basic regex matching functionality
*/
public function testBasicMatches() {
// Test string matching pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "hello" matches "/^[a-z]+$/"}Match!{else}No match{/if}'));
// Test no match
$this->assertEquals('No match', $this->smarty->fetch('string:{if "123" matches "/^[a-z]+$/"}Match!{else}No match{/if}'));
// Test with variables
$this->smarty->assign('name', 'hello');
$this->smarty->assign('pattern', '/^[a-z]+$/');
$this->assertEquals('Match!', $this->smarty->fetch('string:{if $name matches $pattern}Match!{else}No match{/if}'));
// Test no match with variables
$this->smarty->assign('number', '123');
$this->assertEquals('No match', $this->smarty->fetch('string:{if $number matches $pattern}Match!{else}No match{/if}'));
}
/**
* Test different regex patterns
*/
public function testDifferentPatterns() {
// Test numeric pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "123" matches "/^[0-9]+$/"}Match!{else}No match{/if}'));
// Test email pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "test@example.com" matches "/^[^@]+@[^@]+\.[^@]+$/"}Match!{else}No match{/if}'));
// Test case insensitive pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "Hello" matches "/hello/i"}Match!{else}No match{/if}'));
// Test pattern with special characters
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "hello-world" matches "/^[a-z-]+$/"}Match!{else}No match{/if}'));
}
/**
* Test regex patterns with modifiers
*/
public function testPatternModifiers() {
// Test case insensitive modifier
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "HELLO" matches "/hello/i"}Match!{else}No match{/if}'));
// Test multiline modifier
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "hello\nworld" matches "/world$/m"}Match!{else}No match{/if}'));
// Test dotall modifier
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "hello\nworld" matches "/hello.world/s"}Match!{else}No match{/if}'));
}
/**
* Test complex expressions with matches operator
*/
public function testComplexExpressions() {
// Test with logical AND
$this->smarty->assign('name', 'hello');
$this->smarty->assign('pattern', '/^[a-z]+$/');
$this->smarty->assign('length', 5);
$this->assertEquals('Valid!', $this->smarty->fetch('string:{if $name matches $pattern && $length > 3}Valid!{else}Invalid{/if}'));
// Test with logical OR
$this->assertEquals('Valid!', $this->smarty->fetch('string:{if $name matches $pattern || $length < 3}Valid!{else}Invalid{/if}'));
// Test in elseif
$this->assertEquals('ElseIf!', $this->smarty->fetch('string:{if $name matches "/^[0-9]+$/"}No{elseif $name matches $pattern}ElseIf!{else}Else{/if}'));
// Test in while loop condition (should work with constant patterns)
$result = $this->smarty->fetch('string:{assign var="i" value=0}{while $i < 2 && "test" matches "/test/"}{$i}{assign var="i" value=$i+1}{/while}');
$this->assertEquals('01', $result);
}
/**
* Test edge cases and error handling
*/
public function testEdgeCases() {
// Test with empty string
$this->assertEquals('No match', $this->smarty->fetch('string:{if "" matches "/.+/"}Match!{else}No match{/if}'));
// Test with complex pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "abc123" matches "/^[a-z]+[0-9]+$/"}Match!{else}No match{/if}'));
// Test with single character pattern
$this->assertEquals('Match!', $this->smarty->fetch('string:{if "a" matches "/a/"}Match!{else}No match{/if}'));
}
/**
* Test that invalid patterns still work (they will cause PHP warnings but not break the template)
* Note: This test is commented out because invalid patterns cause PHP warnings that fail the test
* In production, invalid patterns would cause warnings but the template would still execute
*/
/*
public function testInvalidPatterns() {
// This will cause a PHP warning but should not break the template execution
$this->smarty->assign('invalid_pattern', 'not-a-valid-pattern');
$this->smarty->assign('name', 'hello');
// The template should execute and reach the else branch due to the invalid pattern
$result = $this->smarty->fetch('string:{if $name matches $invalid_pattern}Match!{else}No match{/if}');
$this->assertEquals('No match', $result);
}
*/
}