diff --git a/src/Compiler.php b/src/Compiler.php
index 5f0a22a..e7b86e3 100644
--- a/src/Compiler.php
+++ b/src/Compiler.php
@@ -46,6 +46,7 @@ class Compiler
'LoopStack',
'ExtendsStack',
'HelpersStack',
+ 'CustomDirective',
'Json',
'Class',
'Import'
@@ -115,11 +116,19 @@ class Compiler
protected $result = '';
/**
- * The expression pattern
+ * The expression pattern.
+ *
+ * Group 2 uses a recursive subpattern so the inner capture stops at the
+ * matching `)` instead of greedily eating up to the last `)` on the line.
+ * Without this, a single-line directive whose body also contains parens
+ * (e.g. `%if ($x) {{ $x }} %endif`, where the echo pass has
+ * already rewritten `{{ $x }}` into ``) would have its
+ * head extended past the real close-paren, producing broken PHP like
+ * `...e($x): ?>; ?>`.
*
* @var string
*/
- protected $condition_pattern = '/(%s\s*\((.*)\))\s*/sm';
+ protected $condition_pattern = '/(%s\s*\(((?:[^()]|\((?2)\))*)\))\s*/s';
/**
* The option expression pattern
diff --git a/src/Lexique/CompileCustomDirective.php b/src/Lexique/CompileCustomDirective.php
index 358a0dd..6a6cfc5 100644
--- a/src/Lexique/CompileCustomDirective.php
+++ b/src/Lexique/CompileCustomDirective.php
@@ -5,7 +5,7 @@
trait CompileCustomDirective
{
/**
- * Compile the custom directive
+ * Compile the custom statement
*
* @param string $expression
* @return string
diff --git a/tests/CompileIfTest.php b/tests/CompileIfTest.php
index 0e8a7d6..962f2fa 100644
--- a/tests/CompileIfTest.php
+++ b/tests/CompileIfTest.php
@@ -98,6 +98,77 @@ public function testcompileEndifStatement()
$this->assertEquals($render, '');
}
+ public function testInlineIfWithEchoBody()
+ {
+ $source = '%if ($service->runtime_version) {{ $service->runtime_version }} %endif';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('runtime_version): ?>', $render);
+ $this->assertStringContainsString('runtime_version); ?>', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
+ public function testInlineIfWithNestedParens()
+ {
+ $source = '%if (count($items) > 0) yes %endif';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString(' 0): ?>', $render);
+ }
+
+ /**
+ * Regression: %unless shares condition_pattern with %if.
+ */
+ public function testInlineUnlessWithEchoBody()
+ {
+ $source = '%unless ($name) {{ $name }} %endunless';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
+ /**
+ * Regression: %isset shares condition_pattern with %if.
+ */
+ public function testInlineIssetWithEchoBody()
+ {
+ $source = '%isset ($name) {{ $name }} %endisset';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
+ /**
+ * Regression: %elseif (and the %elif alias) share condition_pattern.
+ * Exercise an if/elseif/else chain with echoes in every branch.
+ */
+ public function testInlineIfElseIfElseWithEchoBodies()
+ {
+ $source = '%if ($a) {{ $a }} %elseif ($b) {{ $b }} %else {{ $c }} %endif';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
public function testBlockStatement()
{
$html = file_get_contents(__DIR__ . '/view/sample.tintin.php');
diff --git a/tests/CompileLoopTest.php b/tests/CompileLoopTest.php
index c26321a..49ab3d6 100644
--- a/tests/CompileLoopTest.php
+++ b/tests/CompileLoopTest.php
@@ -86,6 +86,54 @@ public function testCompileBreaker()
$this->assertEquals($render, '');
}
+ /**
+ * Regression: a single-line %loop whose body contains an echo must not
+ * have its head extended past the real `)` by the greedy condition
+ * pattern (shared with %if).
+ */
+ public function testInlineLoopWithEchoBody()
+ {
+ $source = '%loop ($items as $item) {{ $item }} %endloop';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
+ /**
+ * Regression: same greedy-match bug, %while variant.
+ */
+ public function testInlineWhileWithEchoBody()
+ {
+ $source = '%while ($i < count($items)) {{ $i }} %endwhile';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
+ /**
+ * Regression: %for has the extra wrinkle of semicolons inside the head.
+ * The balanced-paren matcher must still stop at the head's real `)`.
+ */
+ public function testInlineForWithEchoBody()
+ {
+ $source = '%for ($i = 0; $i < count($items); $i++) {{ $i }} %endfor';
+
+ $render = $this->compiler->compile($source);
+
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringContainsString('', $render);
+ $this->assertStringNotContainsString('): ?>; ?>', $render);
+ }
+
/**
* A multi-line %loop expression must compile the same as the single-line form.
*/