|
4 | 4 |
|
5 | 5 | class CollapseWhitespace extends PageSpeed |
6 | 6 | { |
| 7 | + /** |
| 8 | + * Tags where whitespace should be preserved |
| 9 | + * |
| 10 | + * Note: <script> and <style> are NOT included because: |
| 11 | + * - JavaScript and CSS minification is handled by their specific optimizers |
| 12 | + * - Collapsing whitespace in JS/CSS is generally safe and desired |
| 13 | + * - This middleware focuses on preserving user-visible formatted content |
| 14 | + */ |
| 15 | + protected const PRESERVE_TAGS = [ |
| 16 | + 'pre', |
| 17 | + 'code', |
| 18 | + 'textarea', |
| 19 | + ]; |
| 20 | + |
| 21 | + /** |
| 22 | + * Apply whitespace collapse to buffer while preserving content in specific tags |
| 23 | + */ |
7 | 24 | public function apply($buffer) |
8 | 25 | { |
| 26 | + // First remove comments |
| 27 | + $buffer = $this->removeComments($buffer); |
| 28 | + |
| 29 | + // Extract and preserve content from whitespace-sensitive tags |
| 30 | + $preserved = []; |
| 31 | + $buffer = $this->extractPreservedContent($buffer, $preserved); |
| 32 | + |
| 33 | + // Apply whitespace collapse to the remaining content |
9 | 34 | $replace = [ |
10 | 35 | "/\n([\S])/" => '$1', |
11 | 36 | "/\r/" => '', |
12 | 37 | "/\n/" => '', |
13 | 38 | "/\t/" => '', |
14 | 39 | "/ +/" => ' ', |
15 | | - "/> +</" => '><', |
| 40 | + // Keep one space between tags for Livewire/Alpine.js compatibility (Issue #165) |
| 41 | + // This prevents breaking wire:* directives and x-* attributes |
| 42 | + "/> +</" => '> <', |
16 | 43 | ]; |
17 | 44 |
|
18 | | - return $this->replace($replace, $this->removeComments($buffer)); |
| 45 | + $buffer = $this->replace($replace, $buffer); |
| 46 | + |
| 47 | + // Restore preserved content |
| 48 | + $buffer = $this->restorePreservedContent($buffer, $preserved); |
| 49 | + |
| 50 | + return $buffer; |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Extract content from tags that should preserve whitespace |
| 55 | + */ |
| 56 | + protected function extractPreservedContent(string $buffer, array &$preserved): string |
| 57 | + { |
| 58 | + $index = 0; |
| 59 | + |
| 60 | + foreach (self::PRESERVE_TAGS as $tag) { |
| 61 | + // Match opening and closing tags with all content in between |
| 62 | + // This regex handles: |
| 63 | + // - Tags with or without attributes |
| 64 | + // - Self-closing tags (though not common for these tags) |
| 65 | + // - Nested content |
| 66 | + // - Case-insensitive matching |
| 67 | + $pattern = '/<(' . $tag . ')(\s[^>]*)?>(.*?)<\/\1>/is'; |
| 68 | + |
| 69 | + $buffer = preg_replace_callback($pattern, function ($matches) use (&$preserved, &$index) { |
| 70 | + $placeholder = "___PRESERVED_CONTENT_{$index}___"; |
| 71 | + $preserved[$placeholder] = $matches[0]; // Store the entire tag with content |
| 72 | + $index++; |
| 73 | + return $placeholder; |
| 74 | + }, $buffer); |
| 75 | + } |
| 76 | + |
| 77 | + return $buffer; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Restore preserved content back into the buffer |
| 82 | + */ |
| 83 | + protected function restorePreservedContent(string $buffer, array $preserved): string |
| 84 | + { |
| 85 | + foreach ($preserved as $placeholder => $content) { |
| 86 | + $buffer = str_replace($placeholder, $content, $buffer); |
| 87 | + } |
| 88 | + |
| 89 | + return $buffer; |
19 | 90 | } |
20 | 91 |
|
| 92 | + /** |
| 93 | + * Remove comments before collapsing whitespace |
| 94 | + */ |
21 | 95 | protected function removeComments($buffer) |
22 | 96 | { |
23 | 97 | return (new RemoveComments)->apply($buffer); |
|
0 commit comments