From db2904f642a1ad1155818cc1f89313a258fd7227 Mon Sep 17 00:00:00 2001 From: Peter Somerville Date: Tue, 6 Jan 2026 16:32:32 +0000 Subject: [PATCH] Fix if-tag parsing when 'and/or' appear inside string literals --- src/Liquid/Tag/TagIf.php | 146 ++++++++++++++++++++++++--------- tests/Liquid/Tag/TagIfTest.php | 31 +++++++ 2 files changed, 138 insertions(+), 39 deletions(-) diff --git a/src/Liquid/Tag/TagIf.php b/src/Liquid/Tag/TagIf.php index 4624ae4..7ba3a26 100644 --- a/src/Liquid/Tag/TagIf.php +++ b/src/Liquid/Tag/TagIf.php @@ -92,56 +92,50 @@ public function render(Context $context) { $context->push(); - $logicalRegex = new Regexp('/\s+(and|or)\s+/'); $conditionalRegex = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*([=!<>a-z_]+)?\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')?/'); $result = ''; + foreach ($this->blocks as $block) { if ($block[0] == 'else') { $result = $this->renderAll($block[2], $context); - break; } if ($block[0] == 'if' || $block[0] == 'elsif') { - // Extract logical operators - $logicalRegex->matchAll($block[1]); - - $logicalOperators = $logicalRegex->matches; - $logicalOperators = $logicalOperators[1]; - // Extract individual conditions - $temp = $logicalRegex->split($block[1]); - - $conditions = []; - - foreach ($temp as $condition) { - if ($conditionalRegex->match($condition)) { - $left = (isset($conditionalRegex->matches[1])) ? $conditionalRegex->matches[1] : null; - $operator = (isset($conditionalRegex->matches[2])) ? $conditionalRegex->matches[2] : null; - $right = (isset($conditionalRegex->matches[3])) ? $conditionalRegex->matches[3] : null; - - array_push($conditions, [ - 'left' => $left, - 'operator' => $operator, - 'right' => $right, - ]); - } else { - throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]"); - } + $markup = $block[1]; + + // quote-aware condition & operator parsing + $parsed = $this->parseConditionsAndOperators($markup, $conditionalRegex); + $conditions = $parsed['conditions']; + $operators = $parsed['operators']; + + if (count($conditions) === 0) { + throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]"); } - if (count($logicalOperators)) { - // If statement contains and/or - $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); - foreach ($logicalOperators as $k => $logicalOperator) { - if ($logicalOperator == 'and') { - $display = ($display && $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); - } else { - $display = ($display || $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); - } + + // Evaluate first condition + $display = $this->interpretCondition( + $conditions[0]['left'], + $conditions[0]['right'], + $conditions[0]['operator'], + $context + ); + + // Apply subsequent conditions with logical operators + foreach ($operators as $index => $logicalOperator) { + $next = $this->interpretCondition( + $conditions[$index + 1]['left'], + $conditions[$index + 1]['right'], + $conditions[$index + 1]['operator'], + $context + ); + + if ($logicalOperator === 'and') { + $display = $display && $next; + } else { + $display = $display || $next; } - } else { - // If statement is a single condition - $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); } // hook for unless tag @@ -149,7 +143,6 @@ public function render(Context $context) if ($display) { $result = $this->renderAll($block[2], $context); - break; } } @@ -165,4 +158,79 @@ protected function negateIfUnless($display) // no need to negate a condition in a regular `if` tag (will do that in `unless` tag) return $display; } + + private function parseConditionsAndOperators(string $markup, Regexp $conditionalRegex): array + { + $len = strlen($markup); + $inString = false; + $quote = null; + $buffer = ''; + $fragments = []; + $operators = []; + + for ($i = 0; $i < $len; $i++) { + $ch = $markup[$i]; + + // Track entering/leaving string literals + if ($ch === "'" || $ch === '"') { + if (!$inString) { + $inString = true; + $quote = $ch; + } elseif ($quote === $ch) { + $inString = false; + $quote = null; + } + + $buffer .= $ch; + continue; + } + + if (!$inString) { + // Look for logical " and " outside quotes + if (substr($markup, $i, 5) === ' and ') { + $fragments[] = trim($buffer); + $buffer = ''; + $operators[] = 'and'; + $i += 4; // skip " and" (loop will add 1 more) + continue; + } + + // Look for logical " or " outside quotes + if (substr($markup, $i, 4) === ' or ') { + $fragments[] = trim($buffer); + $buffer = ''; + $operators[] = 'or'; + $i += 3; // skip " or" (loop will add 1 more) + continue; + } + } + + // Default: just accumulate characters + $buffer .= $ch; + } + + if (trim($buffer) !== '') { + $fragments[] = trim($buffer); + } + + $conditions = []; + + foreach ($fragments as $fragment) { + if ($conditionalRegex->match($fragment)) { + $left = isset($conditionalRegex->matches[1]) ? $conditionalRegex->matches[1] : null; + $operator = isset($conditionalRegex->matches[2]) ? $conditionalRegex->matches[2] : null; + $right = isset($conditionalRegex->matches[3]) ? $conditionalRegex->matches[3] : null; + + $conditions[] = [ + 'left' => $left, + 'operator' => $operator, + 'right' => $right, + ]; + } else { + throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]"); + } + } + + return ['conditions' => $conditions, 'operators' => $operators]; + } } diff --git a/tests/Liquid/Tag/TagIfTest.php b/tests/Liquid/Tag/TagIfTest.php index 2b6a21f..85a65a7 100644 --- a/tests/Liquid/Tag/TagIfTest.php +++ b/tests/Liquid/Tag/TagIfTest.php @@ -286,4 +286,35 @@ public function testSyntaxErrorUnknown() $this->assertTemplateResult('', '{% unknown-tag %}'); } + + public function testStringLiteralWithAndIsComparedLiterally() + { + $text = "{% assign course = 'Art and Design' %}" . + "{% if course == 'Art and Design' %}true{% else %}false{% endif %}"; + + $this->assertTemplateResult('true', $text); + } + + public function testStringLiteralWithOrIsComparedLiterally() + { + $text = "{% assign title = 'History or Politics' %}" . + "{% if title == 'History or Politics' %}true{% else %}false{% endif %}"; + + $this->assertTemplateResult('true', $text); + } + + public function testVarAndStringLiteralWithAndAreEqual() + { + $text = "{% if var == 'Art and Design' %}true{% else %}false{% endif %}"; + + $this->assertTemplateResult('true', $text, ['var' => 'Art and Design']); + } + + public function testLogicalAndStillWorksWithStringLiteralContainingAnd() + { + $text = "{% assign ok = true %}{% assign course = 'Art and Design' %}" . + "{% if ok and course == 'Art and Design' %}true{% else %}false{% endif %}"; + + $this->assertTemplateResult('true', $text); + } }