|
6 | 6 |
|
7 | 7 | use Dom\HTMLDocument; |
8 | 8 | use Dom\NodeList; |
9 | | -use Exception; |
| 9 | +use Stringable; |
10 | 10 | use Tempest\Core\Kernel; |
| 11 | +use Tempest\Support\StringHelper; |
11 | 12 | use Tempest\Discovery\DiscoveryLocation; |
12 | 13 | use Tempest\Mapper\Exceptions\ViewNotFound; |
13 | 14 | use Tempest\View\Attributes\AttributeFactory; |
@@ -98,59 +99,51 @@ private function retrieveTemplate(string|View $view): string |
98 | 99 | return file_get_contents($searchPath); |
99 | 100 | } |
100 | 101 |
|
101 | | - private function parseDom(string $template): HTMLDocument|NodeList |
| 102 | + private function parseDom(string $template): NodeList |
102 | 103 | { |
103 | | - $template = str($template) |
104 | 104 |
|
105 | | - // Escape PHP tags |
106 | | - ->replace( |
107 | | - search: array_keys(self::TOKEN_MAPPING), |
108 | | - replace: array_values(self::TOKEN_MAPPING), |
109 | | - ) |
110 | | - |
111 | | - // Convert self-closing tags |
112 | | - ->replaceRegex( |
113 | | - regex: '/<x-(?<element>.*?)\/>/', |
114 | | - replace: function (array $match) { |
115 | | - $closingTag = str($match['element'])->before(' ')->toString(); |
| 105 | + $parserFlags = LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR | HTML_NO_DEFAULT_NS; |
116 | 106 |
|
117 | | - return sprintf( |
118 | | - '<x-%s></x-%s>', |
119 | | - $match['element'], |
120 | | - $closingTag, |
121 | | - ); |
122 | | - }, |
123 | | - ); |
| 107 | + $template = str($template); |
124 | 108 |
|
125 | | - $isFullHtmlDocument = $template |
126 | | - ->replaceRegex('/('.self::TOKEN_PHP_OPEN.'|'.self::TOKEN_PHP_SHORT_ECHO.')(.|\n)*?'.self::TOKEN_PHP_CLOSE.'/', '') |
127 | | - ->trim() |
128 | | - ->startsWith(['<html', '<!DOCTYPE', '<!doctype']); |
| 109 | + // Find head nodes, these are parsed separately so that we skip HTML's head-parsing rules |
| 110 | + $headNodes = []; |
129 | 111 |
|
130 | | - $parserFlags = LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR | HTML_NO_DEFAULT_NS; |
| 112 | + $headTemplate = $template->match('/<head>((.|\n)*?)<\/head>/')[1] ?? null; |
131 | 113 |
|
132 | | - if ($isFullHtmlDocument) { |
133 | | - // If we're rendering a full HTML document, we'll parse it as is |
134 | | - return HTMLDocument::createFromString($template->toString(), $parserFlags); |
| 114 | + if ($headTemplate) { |
| 115 | + $headNodes = HTMLDocument::createFromString( |
| 116 | + source: $this->cleanupTemplate($headTemplate)->toString(), |
| 117 | + options: $parserFlags, |
| 118 | + )->childNodes; |
135 | 119 | } |
136 | 120 |
|
137 | | - // If we're rendering an HTML snippet, we'll wrap it in a div, and return the resulting nodelist |
138 | | - $dom = HTMLDocument::createFromString("<div id='tempest_render'>{$template}</div>", $parserFlags); |
| 121 | + $mainTemplate = $this->cleanupTemplate($template) |
| 122 | + // Cleanup head, we'll insert it after having parsed the DOM |
| 123 | + ->replaceRegex('/<head>((.|\n)*?)<\/head>/', '<head></head>'); |
139 | 124 |
|
140 | | - return $dom->getElementById('tempest_render')->childNodes; |
| 125 | + $dom = HTMLDocument::createFromString( |
| 126 | + source: $mainTemplate->toString(), |
| 127 | + options: $parserFlags, |
| 128 | + ); |
| 129 | + |
| 130 | + // If we have head nodes and a head tag, we inject them back |
| 131 | + if ($headElement = $dom->getElementsByTagName('head')->item(0)) { |
| 132 | + foreach ($headNodes as $headNode) { |
| 133 | + $headElement->appendChild($dom->importNode($headNode, deep: true)); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + return $dom->childNodes; |
141 | 138 | } |
142 | 139 |
|
143 | 140 | /** |
144 | 141 | * @return Element[] |
145 | 142 | */ |
146 | | - private function mapToElements(HTMLDocument|NodeList $nodes): array |
| 143 | + private function mapToElements(NodeList $nodes): array |
147 | 144 | { |
148 | 145 | $elements = []; |
149 | 146 |
|
150 | | - if ($nodes instanceof HTMLDocument) { |
151 | | - $nodes = $nodes->childNodes; |
152 | | - } |
153 | | - |
154 | 147 | foreach ($nodes as $node) { |
155 | 148 | $element = $this->elementFactory->make($node); |
156 | 149 |
|
@@ -206,18 +199,44 @@ private function applyAttributes(array $elements): array |
206 | 199 | /** @param \Tempest\View\Element[] $elements */ |
207 | 200 | private function compileElements(array $elements): string |
208 | 201 | { |
209 | | - $compiled = []; |
| 202 | + $compiled = arr(); |
210 | 203 |
|
211 | 204 | foreach ($elements as $element) { |
212 | 205 | $compiled[] = $element->compile(); |
213 | 206 | } |
214 | 207 |
|
215 | | - $compiled = implode(PHP_EOL, $compiled); |
| 208 | + return $compiled |
| 209 | + ->implode(PHP_EOL) |
216 | 210 |
|
217 | | - return str_replace( |
218 | | - search: array_values(self::TOKEN_MAPPING), |
219 | | - replace: array_keys(self::TOKEN_MAPPING), |
220 | | - subject: $compiled, |
221 | | - ); |
| 211 | + // Unescape PHP tags |
| 212 | + ->replace( |
| 213 | + array_values(self::TOKEN_MAPPING), |
| 214 | + array_keys(self::TOKEN_MAPPING), |
| 215 | + ) |
| 216 | + ->toString(); |
| 217 | + } |
| 218 | + |
| 219 | + private function cleanupTemplate(string|Stringable $template): StringHelper |
| 220 | + { |
| 221 | + return str($template) |
| 222 | + // Escape PHP tags |
| 223 | + ->replace( |
| 224 | + search: array_keys(self::TOKEN_MAPPING), |
| 225 | + replace: array_values(self::TOKEN_MAPPING), |
| 226 | + ) |
| 227 | + |
| 228 | + // Convert self-closing tags |
| 229 | + ->replaceRegex( |
| 230 | + regex: '/<x-(?<element>.*?)\/>/', |
| 231 | + replace: function (array $match) { |
| 232 | + $closingTag = str($match['element'])->before(' ')->toString(); |
| 233 | + |
| 234 | + return sprintf( |
| 235 | + '<x-%s></x-%s>', |
| 236 | + $match['element'], |
| 237 | + $closingTag, |
| 238 | + ); |
| 239 | + }, |
| 240 | + ); |
222 | 241 | } |
223 | 242 | } |
0 commit comments