| 148 |
lars |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace Spatie\LaravelIgnition\Views;
|
|
|
4 |
|
|
|
5 |
use Illuminate\View\Compilers\BladeCompiler;
|
|
|
6 |
use Throwable;
|
|
|
7 |
|
|
|
8 |
class BladeSourceMapCompiler
|
|
|
9 |
{
|
|
|
10 |
protected BladeCompiler $bladeCompiler;
|
|
|
11 |
|
|
|
12 |
public function __construct()
|
|
|
13 |
{
|
|
|
14 |
$this->bladeCompiler = app('blade.compiler');
|
|
|
15 |
}
|
|
|
16 |
|
|
|
17 |
public function detectLineNumber(string $filename, int $compiledLineNumber): int
|
|
|
18 |
{
|
|
|
19 |
$map = $this->compileSourcemap((string)file_get_contents($filename));
|
|
|
20 |
|
|
|
21 |
return $this->findClosestLineNumberMapping($map, $compiledLineNumber);
|
|
|
22 |
}
|
|
|
23 |
|
|
|
24 |
protected function compileSourcemap(string $value): string
|
|
|
25 |
{
|
|
|
26 |
try {
|
|
|
27 |
$value = $this->addEchoLineNumbers($value);
|
|
|
28 |
|
|
|
29 |
$value = $this->addStatementLineNumbers($value);
|
|
|
30 |
|
|
|
31 |
$value = $this->addBladeComponentLineNumbers($value);
|
|
|
32 |
|
|
|
33 |
$value = $this->bladeCompiler->compileString($value);
|
|
|
34 |
|
|
|
35 |
return $this->trimEmptyLines($value);
|
|
|
36 |
} catch (Throwable $e) {
|
|
|
37 |
report($e);
|
|
|
38 |
|
|
|
39 |
return $value;
|
|
|
40 |
}
|
|
|
41 |
}
|
|
|
42 |
|
|
|
43 |
protected function addEchoLineNumbers(string $value): string
|
|
|
44 |
{
|
|
|
45 |
$echoPairs = [['{{', '}}'], ['{{{', '}}}'], ['{!!', '!!}']];
|
|
|
46 |
|
|
|
47 |
foreach ($echoPairs as $pair) {
|
|
|
48 |
// Matches {{ $value }}, {!! $value !!} and {{{ $value }}} depending on $pair
|
|
|
49 |
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $pair[0], $pair[1]);
|
|
|
50 |
|
|
|
51 |
if (preg_match_all($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) {
|
|
|
52 |
foreach (array_reverse($matches[0]) as $match) {
|
|
|
53 |
$position = mb_strlen(substr($value, 0, $match[1]));
|
|
|
54 |
|
|
|
55 |
$value = $this->insertLineNumberAtPosition($position, $value);
|
|
|
56 |
}
|
|
|
57 |
}
|
|
|
58 |
}
|
|
|
59 |
|
|
|
60 |
return $value;
|
|
|
61 |
}
|
|
|
62 |
|
|
|
63 |
protected function addStatementLineNumbers(string $value): string
|
|
|
64 |
{
|
|
|
65 |
// Matches @bladeStatements() like @if, @component(...), @etc;
|
|
|
66 |
$shouldInsertLineNumbers = preg_match_all(
|
|
|
67 |
'/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
|
|
|
68 |
$value,
|
|
|
69 |
$matches,
|
|
|
70 |
PREG_OFFSET_CAPTURE
|
|
|
71 |
);
|
|
|
72 |
|
|
|
73 |
if ($shouldInsertLineNumbers) {
|
|
|
74 |
foreach (array_reverse($matches[0]) as $match) {
|
|
|
75 |
$position = mb_strlen(substr($value, 0, $match[1]));
|
|
|
76 |
|
|
|
77 |
$value = $this->insertLineNumberAtPosition($position, $value);
|
|
|
78 |
}
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
return $value;
|
|
|
82 |
}
|
|
|
83 |
|
|
|
84 |
protected function addBladeComponentLineNumbers(string $value): string
|
|
|
85 |
{
|
|
|
86 |
// Matches the start of `<x-blade-component`
|
|
|
87 |
$shouldInsertLineNumbers = preg_match_all(
|
|
|
88 |
'/<\s*x[-:]([\w\-:.]*)/mx',
|
|
|
89 |
$value,
|
|
|
90 |
$matches,
|
|
|
91 |
PREG_OFFSET_CAPTURE
|
|
|
92 |
);
|
|
|
93 |
|
|
|
94 |
if ($shouldInsertLineNumbers) {
|
|
|
95 |
foreach (array_reverse($matches[0]) as $match) {
|
|
|
96 |
$position = mb_strlen(substr($value, 0, $match[1]));
|
|
|
97 |
|
|
|
98 |
$value = $this->insertLineNumberAtPosition($position, $value);
|
|
|
99 |
}
|
|
|
100 |
}
|
|
|
101 |
|
|
|
102 |
return $value;
|
|
|
103 |
}
|
|
|
104 |
|
|
|
105 |
protected function insertLineNumberAtPosition(int $position, string $value): string
|
|
|
106 |
{
|
|
|
107 |
$before = mb_substr($value, 0, $position);
|
|
|
108 |
$lineNumber = count(explode("\n", $before));
|
|
|
109 |
|
|
|
110 |
return mb_substr($value, 0, $position)."|---LINE:{$lineNumber}---|".mb_substr($value, $position);
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
protected function trimEmptyLines(string $value): string
|
|
|
114 |
{
|
|
|
115 |
$value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value);
|
|
|
116 |
|
|
|
117 |
return ltrim((string)$value, PHP_EOL);
|
|
|
118 |
}
|
|
|
119 |
|
|
|
120 |
protected function findClosestLineNumberMapping(string $map, int $compiledLineNumber): int
|
|
|
121 |
{
|
|
|
122 |
$map = explode("\n", $map);
|
|
|
123 |
|
|
|
124 |
// Max 20 lines between compiled and source line number.
|
|
|
125 |
// Blade components can span multiple lines and the compiled line number is often
|
|
|
126 |
// a couple lines below the source-mapped `<x-component>` code.
|
|
|
127 |
$maxDistance = 20;
|
|
|
128 |
|
|
|
129 |
$pattern = '/\|---LINE:(?P<line>[0-9]+)---\|/m';
|
|
|
130 |
$lineNumberToCheck = $compiledLineNumber - 1;
|
|
|
131 |
|
|
|
132 |
while (true) {
|
|
|
133 |
if ($lineNumberToCheck < $compiledLineNumber - $maxDistance) {
|
|
|
134 |
// Something wrong. Return the $compiledLineNumber (unless it's out of range)
|
|
|
135 |
return min($compiledLineNumber, count($map));
|
|
|
136 |
}
|
|
|
137 |
|
|
|
138 |
if (preg_match($pattern, $map[$lineNumberToCheck] ?? '', $matches)) {
|
|
|
139 |
return (int)$matches['line'];
|
|
|
140 |
}
|
|
|
141 |
|
|
|
142 |
$lineNumberToCheck--;
|
|
|
143 |
}
|
|
|
144 |
}
|
|
|
145 |
}
|