Repository: laravel/framework (php)

Issue:
Escaping Blade directives with @@ is broken
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Laravel Version: 9.50.1
- PHP Version: 8.2.1
- Database Driver & Version: (irrelevant)

### Description:
[Escaping Blade directives](https://laravel.com/docs/9.x/blade#blade-and-javascript-frameworks) with `@` symbol is broken in some cases.

### Steps To Reproduce:

Consider this valid blade:
```
@php
    $columns = [];
    $locales = [];
@endphp

@foreach($columns as $col)
    @if($col == 'foo')
        <div></div>
    @elseif($col == 'bar')
        @@foreach($locales as $locale)
            <div></div>
        @@endforeach
    @else
        <div></div>
    @endif
@endforeach
```

Expected behaviour:
Blade will complile. Double `@@` should just produce one `@`, just like written in the documentation (and how it worked few weeks ago, something must have break just lately).

I'm getting:
```
syntax error, unexpected token "endforeach", expecting "elseif" or "else" or "endif"
```

One solution is to instead of using `@@` to use `{{'@'}}`. If you prefer this solution, then please update the documentation so this is the recommended way.

<!-- If possible, please provide a GitHub repository to demonstrate your issue -->
<!-- laravel new bug-report --github="--public" -->


Pull request description:
[9.x] Fixes blade escaped tags issue
This fixes #45915

In the added test there are 4 matches, in the second replacement `@@endforeach` with `@endforeach` in the second replacement.

Then the final replacement searches for `@endforeach` to replace it with the compiled version, but it finds what was produced by the previously escaped tag because it starts the search from the beginning of the string.

### Solution:
Logically when replacing the matches found by REGEX one after the other using `Str::replaceFirst`, we have to continue from where we last left off.

Patch:
diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php
index b20e7c4924..7c731a5d03 100644
--- a/src/Illuminate/View/Compilers/BladeCompiler.php
+++ b/src/Illuminate/View/Compilers/BladeCompiler.php
@@ -507,6 +507,8 @@ protected function compileStatements($template)
     {
         preg_match_all('/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( [\S\s]*? ) \))?/x', $template, $matches);
 
+        $offset = 0;
+
         for ($i = 0; isset($matches[0][$i]); $i++) {
             $match = [
                 $matches[0][$i],
@@ -538,12 +540,46 @@ protected function compileStatements($template)
                 $match[4] = $match[4].$rest;
             }
 
-            $template = Str::replaceFirst($match[0], $this->compileStatement($match), $template);
+            [$template, $offset] = $this->replaceFirstStatement(
+                $match[0],
+                $this->compileStatement($match),
+                $template,
+                $offset
+            );
         }
 
         return $template;
     }
 
+    /**
+     * Replace the first match for a statement compilation operation.
+     *
+     * @param  string  $search
+     * @param  string  $replace
+     * @param  string  $subject
+     * @param  int  $offset
+     * @return array
+     */
+    protected function replaceFirstStatement($search, $replace, $subject, $offset)
+    {
+        $search = (string) $search;
+
+        if ($search === '') {
+            return $subject;
+        }
+
+        $position = strpos($subject, $search, $offset);
+
+        if ($position !== false) {
+            return [
+                substr_replace($subject, $replace, $position, strlen($search)),
+                $position + strlen($replace)
+            ];
+        }
+
+        return [$subject, 0];
+    }
+
     /**
      * Determine if the given expression has the same number of opening and closing parentheses.
      *


Test patch:
diff --git a/tests/View/Blade/BladeEscapedTest.php b/tests/View/Blade/BladeEscapedTest.php
index 3670139892..7c100e4d31 100644
--- a/tests/View/Blade/BladeEscapedTest.php
+++ b/tests/View/Blade/BladeEscapedTest.php
@@ -16,4 +16,19 @@ public function testEscapedWithAtDirectivesAreCompiled()
             $i as $x
         )'));
     }
+
+    public function testNestedEscapes()
+    {
+        $template = '
+@foreach($cols as $col)
+    @@foreach($issues as $issue_45915)
+    @@endforeach
+@endforeach';
+        $compiled = '
+<?php $__currentLoopData = $cols; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+    @foreach($issues as $issue_45915)
+    @endforeach
+<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>';
+        $this->assertSame($compiled, $this->compiler->compileString($template));
+    }
 }