From 0c5ca226f2e9d669e76c400d5718981ad6060e13 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 1 Oct 2025 20:29:23 +0700 Subject: [PATCH 1/3] feat: introduce `@maybe` directive --- .../Concerns/CompilesConditionals.php | 22 +++ tests/View/Blade/BladeMaybeStatementsTest.php | 167 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/View/Blade/BladeMaybeStatementsTest.php diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php index f54d33b5ffc9..15f078569be9 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php @@ -2,6 +2,7 @@ namespace Illuminate\View\Compilers\Concerns; +use Illuminate\Contracts\View\ViewCompilationException; use Illuminate\Support\Str; trait CompilesConditionals @@ -417,4 +418,25 @@ protected function compileEndPushIf() { return 'stopPush(); endif; ?>'; } + + /** + * Compile conditional HTML attributes into valid PHP. + * + * @param string $expression + * @return string + * + * @throws \Illuminate\Contracts\View\ViewCompilationException + */ + protected function compileMaybe($expression) + { + $parts = explode(',', $this->stripParentheses($expression), 2); + + if (2 !== count($parts)) { + throw new ViewCompilationException('The @maybe directive requires exactly 2 parameters.'); + } + + [ $attribute, $data ] = array_map('trim', $parts); + + return ""; + } } diff --git a/tests/View/Blade/BladeMaybeStatementsTest.php b/tests/View/Blade/BladeMaybeStatementsTest.php new file mode 100644 index 000000000000..d96ca8e003eb --- /dev/null +++ b/tests/View/Blade/BladeMaybeStatementsTest.php @@ -0,0 +1,167 @@ +expectException(ViewCompilationException::class); + $this->expectExceptionMessage('The @maybe directive requires exactly 2 parameters.'); + + $string = "@maybe('title')"; + $this->compiler->compileString($string); + } + + public function testMaybeWithSimpleVariable() + { + $string = 'Link'; + $expected = '>Link'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testMaybeWithObjectProperty() + { + $string = 'title)>Link'; + $expected = 'title !== \'\' && $link->title !== null && trim(is_bool($link->title) ? ($link->title ? \'true\' : \'false\') : $link->title) !== \'\') echo \'title\' . \'="\' . e(is_bool($link->title) ? ($link->title ? \'true\' : \'false\') : $link->title) . \'"\'; ?>>Link'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testMaybeWithArrayAccess() + { + $string = 'Link'; + $expected = '>Link'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testMaybeWithMultipleAttributes() + { + $string = ''; + $expected = ' />'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testMaybeWithSpacesAroundParameters() + { + $string = 'Link'; + $expected = '>Link'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testMaybeWithNonEmptyString() + { + $this->assertSame( + "", + $this->compiler->compileString("@maybe('title', \$title)") + ); + } + + public function testMaybeWithEmptyString() + { + $string = "@maybe('title', \$title)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('', $this->evaluateBlade($compiled, ['title' => ''])); + } + + public function testMaybeWithNull() + { + $string = "@maybe('title', \$title)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('', $this->evaluateBlade($compiled, ['title' => null])); + } + + public function testMaybeWithZero() + { + $string = "@maybe('data-count', \$count)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('data-count="0"', $this->evaluateBlade($compiled, ['count' => 0])); + } + + public function testMaybeWithFalse() + { + $string = "@maybe('data-active', \$active)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('data-active="false"', $this->evaluateBlade($compiled, ['active' => false])); + } + + public function testMaybeWithTrue() + { + $string = "@maybe('data-active', \$active)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('data-active="true"', $this->evaluateBlade($compiled, ['active' => true])); + } + + public function testMaybeWithValidString() + { + $string = "@maybe('title', \$title)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('title="You can just do things"', $this->evaluateBlade($compiled, ['title' => 'You can just do things'])); + } + + public function testMaybeEscapesHtmlEntities() + { + $string = "@maybe('title', \$title)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('title="<script>alert('xss')</script>"', + $this->evaluateBlade($compiled, ['title' => ""])); + } + + public function testMaybeWithWhitespaceOnlyString() + { + $string = "@maybe('title', \$title)"; + $compiled = $this->compiler->compileString($string); + + // Whitespace-only strings are considered empty. + $this->assertSame('', $this->evaluateBlade($compiled, ['title' => ' '])); + } + + public function testMaybeWithNumericString() + { + $string = "@maybe('data-id', \$id)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('data-id="123"', $this->evaluateBlade($compiled, ['id' => '123'])); + } + + public function testMaybeWithInt() + { + $string = "@maybe('data-id', \$id)"; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('data-id="123"', $this->evaluateBlade($compiled, ['id' => 123])); + } + + public function testMaybeInHtmlContext() + { + $string = 'Link'; + $compiled = $this->compiler->compileString($string); + + $this->assertSame('Link', + $this->evaluateBlade($compiled, ['title' => 'click'])); + + $this->assertSame('Link', + $this->evaluateBlade($compiled, ['title' => ''])); + } + + protected function evaluateBlade(string $compiled, array $data = []): string + { + extract($data); + ob_start(); + eval('?>' . $compiled); + return ob_get_clean(); + } +} From 0918d69c69e0bbda9ae4bb535c0090109bf43b58 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 1 Oct 2025 21:15:33 +0700 Subject: [PATCH 2/3] style --- src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php | 2 +- tests/View/Blade/BladeMaybeStatementsTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php index 15f078569be9..eacc865f00d6 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php @@ -435,7 +435,7 @@ protected function compileMaybe($expression) throw new ViewCompilationException('The @maybe directive requires exactly 2 parameters.'); } - [ $attribute, $data ] = array_map('trim', $parts); + [$attribute, $data] = array_map('trim', $parts); return ""; } diff --git a/tests/View/Blade/BladeMaybeStatementsTest.php b/tests/View/Blade/BladeMaybeStatementsTest.php index d96ca8e003eb..a83a2a700c40 100644 --- a/tests/View/Blade/BladeMaybeStatementsTest.php +++ b/tests/View/Blade/BladeMaybeStatementsTest.php @@ -161,7 +161,7 @@ protected function evaluateBlade(string $compiled, array $data = []): string { extract($data); ob_start(); - eval('?>' . $compiled); + eval('?>'.$compiled); return ob_get_clean(); } } From 3aed5873adb37ff1cb096a09bdfd22dec42173cd Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 1 Oct 2025 21:16:45 +0700 Subject: [PATCH 3/3] style --- tests/View/Blade/BladeMaybeStatementsTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/View/Blade/BladeMaybeStatementsTest.php b/tests/View/Blade/BladeMaybeStatementsTest.php index a83a2a700c40..0fc0b0be4a7b 100644 --- a/tests/View/Blade/BladeMaybeStatementsTest.php +++ b/tests/View/Blade/BladeMaybeStatementsTest.php @@ -162,6 +162,7 @@ protected function evaluateBlade(string $compiled, array $data = []): string extract($data); ob_start(); eval('?>'.$compiled); + return ob_get_clean(); } }