This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * @author Todd Burry <[email protected]> |
||
4 | * @copyright 2009-2017 Vanilla Forums Inc. |
||
5 | * @license MIT |
||
6 | */ |
||
7 | |||
8 | namespace Ebi; |
||
9 | |||
10 | use DOMAttr; |
||
11 | use DOMElement; |
||
12 | use DOMNode; |
||
13 | use Symfony\Component\ExpressionLanguage\SyntaxError; |
||
14 | |||
15 | class Compiler { |
||
16 | const T_IF = 'x-if'; |
||
17 | const T_EACH = 'x-each'; |
||
18 | const T_WITH = 'x-with'; |
||
19 | const T_LITERAL = 'x-literal'; |
||
20 | const T_AS = 'x-as'; |
||
21 | const T_COMPONENT = 'x-component'; |
||
22 | const T_CHILDREN = 'x-children'; |
||
23 | const T_BLOCK = 'x-block'; |
||
24 | const T_ELSE = 'x-else'; |
||
25 | const T_EMPTY = 'x-empty'; |
||
26 | const T_X = 'x'; |
||
27 | const T_INCLUDE = 'x-include'; |
||
28 | const T_EBI = 'ebi'; |
||
29 | const T_UNESCAPE = 'x-unescape'; |
||
30 | const T_TAG = 'x-tag'; |
||
31 | |||
32 | const IDENT_REGEX = '`^([a-z0-9-]+)$`i'; |
||
33 | |||
34 | protected static $special = [ |
||
35 | self::T_COMPONENT => 1, |
||
36 | self::T_IF => 2, |
||
37 | self::T_ELSE => 3, |
||
38 | self::T_EACH => 4, |
||
39 | self::T_EMPTY => 5, |
||
40 | self::T_CHILDREN => 6, |
||
41 | self::T_INCLUDE => 7, |
||
42 | self::T_WITH => 8, |
||
43 | self::T_BLOCK => 9, |
||
44 | self::T_LITERAL => 10, |
||
45 | self::T_AS => 11, |
||
46 | self::T_UNESCAPE => 12, |
||
47 | self::T_TAG => 13 |
||
48 | ]; |
||
49 | |||
50 | protected static $htmlTags = [ |
||
51 | 'a' => 'i', |
||
52 | 'abbr' => 'i', |
||
53 | 'acronym' => 'i', // deprecated |
||
54 | 'address' => 'b', |
||
55 | // 'applet' => 'i', // deprecated |
||
56 | 'area' => 'i', |
||
57 | 'article' => 'b', |
||
58 | 'aside' => 'b', |
||
59 | 'audio' => 'i', |
||
60 | 'b' => 'i', |
||
61 | 'base' => 'i', |
||
62 | // 'basefont' => 'i', |
||
63 | 'bdi' => 'i', |
||
64 | 'bdo' => 'i', |
||
65 | // 'bgsound' => 'i', |
||
66 | // 'big' => 'i', |
||
67 | 'x' => 'i', |
||
68 | // 'blink' => 'i', |
||
69 | 'blockquote' => 'b', |
||
70 | 'body' => 'b', |
||
71 | 'br' => 'i', |
||
72 | 'button' => 'i', |
||
73 | 'canvas' => 'b', |
||
74 | 'caption' => 'i', |
||
75 | // 'center' => 'b', |
||
76 | 'cite' => 'i', |
||
77 | 'code' => 'i', |
||
78 | 'col' => 'i', |
||
79 | 'colgroup' => 'i', |
||
80 | // 'command' => 'i', |
||
81 | 'content' => 'i', |
||
82 | 'data' => 'i', |
||
83 | 'datalist' => 'i', |
||
84 | 'dd' => 'b', |
||
85 | 'del' => 'i', |
||
86 | 'details' => 'i', |
||
87 | 'dfn' => 'i', |
||
88 | 'dialog' => 'i', |
||
89 | // 'dir' => 'i', |
||
90 | 'div' => 'i', |
||
91 | 'dl' => 'b', |
||
92 | 'dt' => 'b', |
||
93 | // 'element' => 'i', |
||
94 | 'em' => 'i', |
||
95 | 'embed' => 'i', |
||
96 | 'fieldset' => 'b', |
||
97 | 'figcaption' => 'b', |
||
98 | 'figure' => 'b', |
||
99 | // 'font' => 'i', |
||
100 | 'footer' => 'b', |
||
101 | 'form' => 'b', |
||
102 | 'frame' => 'i', |
||
103 | 'frameset' => 'i', |
||
104 | 'h1' => 'b', |
||
105 | 'h2' => 'b', |
||
106 | 'h3' => 'b', |
||
107 | 'h4' => 'b', |
||
108 | 'h5' => 'b', |
||
109 | 'h6' => 'b', |
||
110 | 'head' => 'b', |
||
111 | 'header' => 'b', |
||
112 | 'hgroup' => 'b', |
||
113 | 'hr' => 'b', |
||
114 | 'html' => 'b', |
||
115 | 'i' => 'i', |
||
116 | 'iframe' => 'i', |
||
117 | 'image' => 'i', |
||
118 | 'img' => 'i', |
||
119 | 'input' => 'i', |
||
120 | 'ins' => 'i', |
||
121 | 'isindex' => 'i', |
||
122 | 'kbd' => 'i', |
||
123 | 'keygen' => 'i', |
||
124 | 'label' => 'i', |
||
125 | 'legend' => 'i', |
||
126 | 'li' => 'i', |
||
127 | 'link' => 'i', |
||
128 | // 'listing' => 'i', |
||
129 | 'main' => 'b', |
||
130 | 'map' => 'i', |
||
131 | 'mark' => 'i', |
||
132 | // 'marquee' => 'i', |
||
133 | 'menu' => 'i', |
||
134 | 'menuitem' => 'i', |
||
135 | 'meta' => 'i', |
||
136 | 'meter' => 'i', |
||
137 | 'multicol' => 'i', |
||
138 | 'nav' => 'b', |
||
139 | 'nobr' => 'i', |
||
140 | 'noembed' => 'i', |
||
141 | 'noframes' => 'i', |
||
142 | 'noscript' => 'b', |
||
143 | 'object' => 'i', |
||
144 | 'ol' => 'b', |
||
145 | 'optgroup' => 'i', |
||
146 | 'option' => 'b', |
||
147 | 'output' => 'i', |
||
148 | 'p' => 'b', |
||
149 | 'param' => 'i', |
||
150 | 'picture' => 'i', |
||
151 | // 'plaintext' => 'i', |
||
152 | 'pre' => 'b', |
||
153 | 'progress' => 'i', |
||
154 | 'q' => 'i', |
||
155 | 'rp' => 'i', |
||
156 | 'rt' => 'i', |
||
157 | 'rtc' => 'i', |
||
158 | 'ruby' => 'i', |
||
159 | 's' => 'i', |
||
160 | 'samp' => 'i', |
||
161 | 'script' => 'i', |
||
162 | 'section' => 'b', |
||
163 | 'select' => 'i', |
||
164 | // 'shadow' => 'i', |
||
165 | 'slot' => 'i', |
||
166 | 'small' => 'i', |
||
167 | 'source' => 'i', |
||
168 | // 'spacer' => 'i', |
||
169 | 'span' => 'i', |
||
170 | // 'strike' => 'i', |
||
171 | 'strong' => 'i', |
||
172 | 'style' => 'i', |
||
173 | 'sub' => 'i', |
||
174 | 'summary' => 'i', |
||
175 | 'sup' => 'i', |
||
176 | 'table' => 'b', |
||
177 | 'tbody' => 'i', |
||
178 | 'td' => 'i', |
||
179 | 'template' => 'i', |
||
180 | 'textarea' => 'i', |
||
181 | 'tfoot' => 'b', |
||
182 | 'th' => 'i', |
||
183 | 'thead' => 'i', |
||
184 | 'time' => 'i', |
||
185 | 'title' => 'i', |
||
186 | 'tr' => 'i', |
||
187 | 'track' => 'i', |
||
188 | // 'tt' => 'i', |
||
189 | 'u' => 'i', |
||
190 | 'ul' => 'b', |
||
191 | 'var' => 'i', |
||
192 | 'video' => 'b', |
||
193 | 'wbr' => 'i', |
||
194 | |||
195 | /// SVG /// |
||
196 | 'animate' => 's', |
||
197 | 'animateColor' => 's', |
||
198 | 'animateMotion' => 's', |
||
199 | 'animateTransform' => 's', |
||
200 | // 'canvas' => 's', |
||
201 | 'circle' => 's', |
||
202 | 'desc' => 's', |
||
203 | 'defs' => 's', |
||
204 | 'discard' => 's', |
||
205 | 'ellipse' => 's', |
||
206 | 'g' => 's', |
||
207 | // 'image' => 's', |
||
208 | 'line' => 's', |
||
209 | 'marker' => 's', |
||
210 | 'mask' => 's', |
||
211 | 'missing-glyph' => 's', |
||
212 | 'mpath' => 's', |
||
213 | 'metadata' => 's', |
||
214 | 'path' => 's', |
||
215 | 'pattern' => 's', |
||
216 | 'polygon' => 's', |
||
217 | 'polyline' => 's', |
||
218 | 'rect' => 's', |
||
219 | 'set' => 's', |
||
220 | 'svg' => 's', |
||
221 | 'switch' => 's', |
||
222 | 'symbol' => 's', |
||
223 | 'text' => 's', |
||
224 | // 'unknown' => 's', |
||
225 | 'use' => 's', |
||
226 | ]; |
||
227 | |||
228 | protected static $boolAttributes = [ |
||
229 | 'checked' => 1, |
||
230 | 'itemscope' => 1, |
||
231 | 'required' => 1, |
||
232 | 'selected' => 1, |
||
233 | ]; |
||
234 | |||
235 | /** |
||
236 | * @var ExpressionLanguage |
||
237 | */ |
||
238 | protected $expressions; |
||
239 | |||
240 | 86 | public function __construct() { |
|
241 | 86 | $this->expressions = new ExpressionLanguage(); |
|
242 | 86 | $this->expressions->setNamePattern('/[@a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'); |
|
243 | 86 | $this->expressions->register( |
|
244 | 86 | 'hasChildren', |
|
245 | function ($name = null) { |
||
246 | 1 | return empty($name) ? 'isset($children[0])' : "isset(\$children[$name ?: 0])"; |
|
247 | 86 | }, |
|
248 | function ($name = null) { |
||
249 | return false; |
||
250 | 86 | }); |
|
251 | 86 | } |
|
252 | |||
253 | /** |
||
254 | * Register a runtime function. |
||
255 | * |
||
256 | * @param string $name The name of the function. |
||
257 | * @param callable $function The function callback. |
||
258 | */ |
||
259 | 84 | public function defineFunction($name, $function = null) { |
|
260 | 84 | if ($function === null) { |
|
261 | 1 | $function = $name; |
|
262 | 1 | } |
|
263 | |||
264 | 84 | $this->expressions->register( |
|
265 | 84 | $name, |
|
266 | 84 | $this->getFunctionCompiler($name, $function), |
|
267 | 84 | $this->getFunctionEvaluator($function) |
|
268 | 84 | ); |
|
269 | 84 | } |
|
270 | |||
271 | 84 | private function getFunctionEvaluator($function) { |
|
272 | 84 | if ($function === 'empty') { |
|
273 | return function ($expr) { |
||
274 | return empty($expr); |
||
275 | 84 | }; |
|
276 | 83 | } elseif ($function === 'isset') { |
|
277 | return function ($expr) { |
||
278 | return isset($expr); |
||
279 | }; |
||
280 | } |
||
281 | |||
282 | 83 | return $function; |
|
283 | } |
||
284 | |||
285 | 84 | private function getFunctionCompiler($name, $function) { |
|
286 | 84 | $var = var_export(strtolower($name), true); |
|
287 | $fn = function (...$args) use ($var) { |
||
288 | 2 | return "\$this->call($var, ".implode(', ', $args).')'; |
|
289 | 84 | }; |
|
290 | |||
291 | 84 | if (is_string($function)) { |
|
292 | $fn = function (...$args) use ($function) { |
||
293 | 16 | return $function.'('.implode(', ', $args).')'; |
|
294 | 84 | }; |
|
295 | 84 | } elseif (is_array($function)) { |
|
296 | 83 | if (is_string($function[0])) { |
|
297 | $fn = function (...$args) use ($function) { |
||
298 | return "$function[0]::$function[1](".implode(', ', $args).')'; |
||
299 | }; |
||
300 | 83 | } elseif ($function[0] instanceof Ebi) { |
|
301 | $fn = function (...$args) use ($function) { |
||
302 | 17 | return "\$this->$function[1](".implode(', ', $args).')'; |
|
303 | 83 | }; |
|
304 | 83 | } |
|
305 | 83 | } |
|
306 | |||
307 | 84 | return $fn; |
|
308 | 1 | } |
|
309 | |||
310 | 78 | public function compile($src, array $options = []) { |
|
311 | 78 | $options += ['basename' => '', 'path' => '', 'runtime' => true]; |
|
312 | |||
313 | 78 | $src = trim($src); |
|
314 | |||
315 | 78 | $out = new CompilerBuffer(); |
|
316 | |||
317 | 78 | $out->setBasename($options['basename']); |
|
318 | 78 | $out->setSource($src); |
|
319 | 78 | $out->setPath($options['path']); |
|
320 | |||
321 | 78 | $dom = new \DOMDocument(); |
|
322 | |||
323 | 78 | $fragment = false; |
|
324 | 78 | if (strpos($src, '<html') === false) { |
|
325 | 76 | $src = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><html><body>$src</body></html>"; |
|
326 | 76 | $fragment = true; |
|
327 | 76 | } |
|
328 | |||
329 | 78 | libxml_use_internal_errors(true); |
|
330 | 78 | $dom->loadHTML($src, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOCDATA | LIBXML_NOXMLDECL); |
|
331 | // $arr = $this->domToArray($dom); |
||
332 | |||
333 | 78 | if ($options['runtime']) { |
|
334 | 77 | $name = var_export($options['basename'], true); |
|
335 | 77 | $out->appendCode("\$this->defineComponent($name, function (\$props = [], \$children = []) {\n"); |
|
336 | 77 | } else { |
|
337 | 1 | $out->appendCode("function (\$props = [], \$children = []) {\n"); |
|
338 | } |
||
339 | |||
340 | 78 | $out->pushScope(['this' => 'props']); |
|
341 | 78 | $out->indent(+1); |
|
342 | |||
343 | 78 | $parent = $fragment ? $dom->firstChild->nextSibling->firstChild : $dom; |
|
344 | |||
345 | 78 | foreach ($parent->childNodes as $node) { |
|
346 | 78 | $this->compileNode($node, $out); |
|
347 | 70 | } |
|
348 | |||
349 | 70 | $out->indent(-1); |
|
350 | 70 | $out->popScope(); |
|
351 | |||
352 | 70 | if ($options['runtime']) { |
|
353 | 69 | $out->appendCode("});"); |
|
354 | 69 | } else { |
|
355 | 1 | $out->appendCode("};"); |
|
356 | } |
||
357 | |||
358 | 70 | $r = $out->flush(); |
|
359 | |||
360 | 70 | $errs = libxml_get_errors(); |
|
361 | |||
362 | 70 | return $r; |
|
363 | } |
||
364 | |||
365 | 57 | protected function isComponent($tag) { |
|
366 | 57 | return !isset(static::$htmlTags[$tag]); |
|
367 | } |
||
368 | |||
369 | 78 | protected function compileNode(DOMNode $node, CompilerBuffer $out) { |
|
370 | 78 | if ($out->getNodeProp($node, 'skip')) { |
|
371 | 4 | return; |
|
372 | } |
||
373 | |||
374 | 78 | switch ($node->nodeType) { |
|
375 | 78 | case XML_TEXT_NODE: |
|
376 | 64 | $this->compileTextNode($node, $out); |
|
377 | 64 | break; |
|
378 | 73 | case XML_ELEMENT_NODE: |
|
379 | /* @var \DOMElement $node */ |
||
380 | 73 | $this->compileElementNode($node, $out); |
|
381 | 66 | break; |
|
382 | 4 | case XML_COMMENT_NODE: |
|
383 | /* @var \DOMComment $node */ |
||
384 | 1 | $this->compileCommentNode($node, $out); |
|
385 | 1 | break; |
|
386 | 3 | case XML_DOCUMENT_TYPE_NODE: |
|
387 | 1 | $out->echoLiteral("<!DOCTYPE {$node->name}>\n"); |
|
0 ignored issues
–
show
|
|||
388 | 1 | break; |
|
389 | 2 | case XML_CDATA_SECTION_NODE: |
|
390 | 2 | $this->compileTextNode($node, $out); |
|
391 | 2 | break; |
|
392 | default: |
||
393 | $r = "// Unknown node\n". |
||
394 | '// '.str_replace("\n", "\n// ", $node->ownerDocument->saveHTML($node)); |
||
395 | 72 | } |
|
396 | 72 | } |
|
397 | |||
398 | protected function domToArray(DOMNode $root) { |
||
399 | $result = []; |
||
400 | |||
401 | if ($root->hasAttributes()) { |
||
402 | $attrs = $root->attributes; |
||
403 | foreach ($attrs as $attr) { |
||
404 | $result['@attributes'][$attr->name] = $attr->value; |
||
405 | } |
||
406 | } |
||
407 | |||
408 | if ($root->hasChildNodes()) { |
||
409 | $children = $root->childNodes; |
||
410 | if ($children->length == 1) { |
||
411 | $child = $children->item(0); |
||
412 | if ($child->nodeType == XML_TEXT_NODE) { |
||
413 | $result['_value'] = $child->nodeValue; |
||
414 | return count($result) == 1 |
||
415 | ? $result['_value'] |
||
416 | : $result; |
||
417 | } |
||
418 | } |
||
419 | $groups = []; |
||
420 | foreach ($children as $child) { |
||
421 | if (!isset($result[$child->nodeName])) { |
||
422 | $result[$child->nodeName] = $this->domToArray($child); |
||
423 | } else { |
||
424 | if (!isset($groups[$child->nodeName])) { |
||
425 | $result[$child->nodeName] = [$result[$child->nodeName]]; |
||
426 | $groups[$child->nodeName] = 1; |
||
427 | } |
||
428 | $result[$child->nodeName][] = $this->domToArray($child); |
||
429 | } |
||
430 | } |
||
431 | } |
||
432 | |||
433 | return $result; |
||
434 | } |
||
435 | |||
436 | 1 | protected function newline(DOMNode $node, CompilerBuffer $out) { |
|
437 | 1 | if ($node->previousSibling && $node->previousSibling->nodeType !== XML_COMMENT_NODE) { |
|
438 | $out->appendCode("\n"); |
||
439 | } |
||
440 | 1 | } |
|
441 | |||
442 | 1 | protected function compileCommentNode(\DOMComment $node, CompilerBuffer $out) { |
|
443 | 1 | $comments = explode("\n", trim($node->nodeValue)); |
|
444 | |||
445 | 1 | $this->newline($node, $out); |
|
446 | 1 | foreach ($comments as $comment) { |
|
447 | 1 | $out->appendCode("// $comment\n"); |
|
448 | 1 | } |
|
449 | 1 | } |
|
450 | |||
451 | 66 | protected function compileTextNode(DOMNode $node, CompilerBuffer $out) { |
|
452 | 66 | $nodeText = $node->nodeValue; |
|
453 | |||
454 | 66 | $items = $this->splitExpressions($nodeText); |
|
455 | 66 | foreach ($items as $i => list($text, $offset)) { |
|
456 | 66 | if (preg_match('`^{\S`', $text)) { |
|
457 | 34 | if (preg_match('`^{\s*unescape\((.+)\)\s*}$`', $text, $m)) { |
|
458 | 4 | $out->echoCode($this->expr($m[1], $out)); |
|
459 | 4 | } else { |
|
460 | try { |
||
461 | 31 | $expr = substr($text, 1, -1); |
|
462 | 31 | $out->echoCode($this->compileEscape($this->expr($expr, $out))); |
|
463 | 31 | } catch (SyntaxError $ex) { |
|
464 | 1 | $nodeLineCount = substr_count($nodeText, "\n"); |
|
465 | 1 | $offsetLineCount = substr_count($nodeText, "\n", 0, $offset); |
|
466 | 1 | $line = $node->getLineNo() - $nodeLineCount + $offsetLineCount; |
|
467 | 1 | throw $out->createCompilerException($node, $ex, ['source' => $expr, 'line' => $line]); |
|
468 | } |
||
469 | } |
||
470 | 33 | } else { |
|
471 | 66 | if ($i === 0) { |
|
472 | 66 | $text = $this->ltrim($text, $node, $out); |
|
473 | 66 | } |
|
474 | 66 | if ($i === count($items) - 1) { |
|
475 | 66 | $text = $this->rtrim($text, $node, $out); |
|
476 | 66 | } |
|
477 | |||
478 | 66 | $out->echoLiteral($text); |
|
479 | } |
||
480 | 66 | } |
|
481 | 66 | } |
|
482 | |||
483 | 73 | protected function compileElementNode(DOMElement $node, CompilerBuffer $out) { |
|
484 | 73 | list($attributes, $special) = $this->splitAttributes($node); |
|
485 | |||
486 | 73 | if ($node->tagName === 'script' && ((isset($attributes['type']) && $attributes['type']->value === self::T_EBI) || !empty($special[self::T_AS]) || !empty($special[self::T_UNESCAPE]))) { |
|
487 | 10 | $this->compileExpressionNode($node, $attributes, $special, $out); |
|
488 | 71 | } elseif (!empty($special) || $this->isComponent($node->tagName)) { |
|
489 | 46 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
490 | 41 | } else { |
|
491 | 35 | $this->compileBasicElement($node, $attributes, $special, $out); |
|
492 | } |
||
493 | 66 | } |
|
494 | |||
495 | 66 | protected function splitExpressions($value) { |
|
496 | 66 | $values = preg_split('`({\S[^}]*?})`', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); |
|
497 | 66 | return $values; |
|
498 | } |
||
499 | |||
500 | /** |
||
501 | * Compile the PHP for an expression |
||
502 | * |
||
503 | * @param string $expr The expression to compile. |
||
504 | * @param CompilerBuffer $out The current output buffer. |
||
505 | * @param DOMAttr|null $attr The owner of the expression. |
||
506 | * @return string Returns a string of PHP code. |
||
507 | */ |
||
508 | 70 | protected function expr($expr, CompilerBuffer $out, DOMAttr $attr = null) { |
|
509 | 70 | $names = $out->getScopeVariables(); |
|
510 | |||
511 | try { |
||
512 | $compiled = $this->expressions->compile($expr, function ($name) use ($names) { |
||
513 | 63 | if (isset($names[$name])) { |
|
514 | 38 | return $names[$name]; |
|
515 | 39 | } elseif ($name[0] === '@') { |
|
516 | 1 | return 'this->meta['.var_export(substr($name, 1), true).']'; |
|
517 | } else { |
||
518 | 38 | return $names['this'].'['.var_export($name, true).']'; |
|
519 | } |
||
520 | 70 | }); |
|
521 | 70 | } catch (SyntaxError $ex) { |
|
522 | 3 | if ($attr !== null) { |
|
523 | 1 | throw $out->createCompilerException($attr, $ex); |
|
524 | } else { |
||
525 | 2 | throw $ex; |
|
526 | } |
||
527 | } |
||
528 | |||
529 | 67 | if ($attr !== null && null !== $fn = $this->getAttributeFunction($attr)) { |
|
530 | 10 | $compiled = call_user_func($fn, $compiled); |
|
531 | 10 | } |
|
532 | |||
533 | 67 | return $compiled; |
|
534 | } |
||
535 | |||
536 | /** |
||
537 | * Get the compiler function to wrap an attribute. |
||
538 | * |
||
539 | * Attribute functions are regular expression functions, but with a special naming convention. The following naming |
||
540 | * conventions are supported: |
||
541 | * |
||
542 | * - **@tag:attribute**: Applies to an attribute only on a specific tag. |
||
543 | * - **@attribute**: Applies to all attributes with a given name. |
||
544 | * |
||
545 | * @param DOMAttr $attr The attribute to look at. |
||
546 | * @return callable|null A function or **null** if the attribute doesn't have a function. |
||
547 | */ |
||
548 | 41 | private function getAttributeFunction(DOMAttr $attr) { |
|
549 | 41 | $keys = ['@'.$attr->ownerElement->tagName.':'.$attr->name, '@'.$attr->name]; |
|
550 | |||
551 | 41 | foreach ($keys as $key) { |
|
552 | 41 | if (null !== $fn = $this->expressions->getFunctionCompiler($key)) { |
|
553 | 17 | return $fn; |
|
554 | } |
||
555 | 41 | } |
|
556 | 31 | } |
|
557 | |||
558 | /** |
||
559 | * @param DOMElement $node |
||
560 | */ |
||
561 | 73 | protected function splitAttributes(DOMElement $node) { |
|
562 | 73 | $attributes = []; |
|
563 | 73 | $special = []; |
|
564 | |||
565 | 73 | foreach ($node->attributes as $name => $attribute) { |
|
566 | 69 | if (isset(static::$special[$name])) { |
|
567 | 50 | $special[$name] = $attribute; |
|
568 | 50 | } else { |
|
569 | 33 | $attributes[$name] = $attribute; |
|
570 | } |
||
571 | 73 | } |
|
572 | |||
573 | uksort($special, function ($a, $b) { |
||
574 | 13 | return strnatcmp(static::$special[$a], static::$special[$b]); |
|
575 | 73 | }); |
|
576 | |||
577 | 73 | return [$attributes, $special]; |
|
578 | } |
||
579 | |||
580 | 46 | protected function compileSpecialNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
581 | 46 | $specialName = key($special); |
|
582 | |||
583 | switch ($specialName) { |
||
584 | 46 | case self::T_COMPONENT: |
|
585 | 10 | $this->compileComponentRegister($node, $attributes, $special, $out); |
|
586 | 10 | break; |
|
587 | 46 | case self::T_IF: |
|
588 | 11 | $this->compileIf($node, $attributes, $special, $out); |
|
589 | 10 | break; |
|
590 | 45 | case self::T_EACH: |
|
591 | 16 | $this->compileEach($node, $attributes, $special, $out); |
|
592 | 15 | break; |
|
593 | 33 | case self::T_BLOCK: |
|
594 | 3 | $this->compileBlock($node, $attributes, $special, $out); |
|
595 | 2 | break; |
|
596 | 32 | case self::T_CHILDREN: |
|
597 | 4 | $this->compileChildBlock($node, $attributes, $special, $out); |
|
598 | 4 | break; |
|
599 | 32 | case self::T_INCLUDE: |
|
600 | 1 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
601 | 1 | break; |
|
602 | 32 | case self::T_WITH: |
|
603 | 5 | if ($this->isComponent($node->tagName)) { |
|
604 | // With has a special meaning in components. |
||
605 | 3 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
606 | 3 | } else { |
|
607 | 2 | $this->compileWith($node, $attributes, $special, $out); |
|
608 | } |
||
609 | 4 | break; |
|
610 | 30 | case self::T_LITERAL: |
|
611 | 2 | $this->compileLiteral($node, $attributes, $special, $out); |
|
612 | 2 | break; |
|
613 | 28 | case '': |
|
614 | 24 | if ($this->isComponent($node->tagName)) { |
|
615 | 10 | $this->compileComponentInclude($node, $attributes, $special, $out); |
|
616 | 9 | } else { |
|
617 | 22 | $this->compileElement($node, $attributes, $special, $out); |
|
618 | } |
||
619 | 23 | break; |
|
620 | 5 | case self::T_TAG: |
|
621 | 5 | default: |
|
622 | // This is only a tag node so it just gets compiled as an element. |
||
623 | 5 | $this->compileBasicElement($node, $attributes, $special, $out); |
|
624 | 5 | break; |
|
625 | } |
||
626 | 41 | } |
|
627 | |||
628 | /** |
||
629 | * Compile component registering. |
||
630 | * |
||
631 | * @param DOMElement $node |
||
632 | * @param $attributes |
||
633 | * @param $special |
||
634 | * @param CompilerBuffer $out |
||
635 | */ |
||
636 | 10 | public function compileComponentRegister(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
637 | 10 | $name = strtolower($special[self::T_COMPONENT]->value); |
|
638 | 10 | unset($special[self::T_COMPONENT]); |
|
639 | |||
640 | 10 | $prev = $out->select($name); |
|
641 | |||
642 | 10 | $varName = var_export($name, true); |
|
643 | 10 | $out->appendCode("\$this->defineComponent($varName, function (\$props = [], \$children = []) {\n"); |
|
644 | 10 | $out->pushScope(['this' => 'props']); |
|
645 | 10 | $out->indent(+1); |
|
646 | |||
647 | try { |
||
648 | 10 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
649 | 10 | } finally { |
|
650 | 10 | $out->popScope(); |
|
651 | 10 | $out->indent(-1); |
|
652 | 10 | $out->appendCode("});"); |
|
653 | 10 | $out->select($prev); |
|
654 | 10 | } |
|
655 | 10 | } |
|
656 | |||
657 | 3 | private function compileBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
658 | // Blocks must be direct descendants of component includes. |
||
659 | 3 | if (!$out->getNodeProp($node->parentNode, self::T_INCLUDE)) { |
|
660 | 1 | throw $out->createCompilerException($node, new \Exception("Blocks must be direct descendants of component includes.")); |
|
661 | } |
||
662 | |||
663 | 2 | $name = strtolower($special[self::T_BLOCK]->value); |
|
664 | 2 | if (empty($name)) { |
|
665 | throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("Block names cannot be empty.")); |
||
666 | } |
||
667 | 2 | if (!preg_match(self::IDENT_REGEX, $name)) { |
|
668 | throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("The block name isn't a valid identifier.")); |
||
669 | } |
||
670 | |||
671 | 2 | unset($special[self::T_BLOCK]); |
|
672 | |||
673 | 2 | $prev = $out->select($name, true); |
|
674 | |||
675 | 2 | $vars = array_filter(array_unique($out->getScopeVariables())); |
|
676 | 2 | $vars[] = 'children'; |
|
677 | 2 | $use = '$'.implode(', $', array_unique($vars)); |
|
678 | |||
679 | 2 | $out->appendCode("function () use ($use) {\n"); |
|
680 | 2 | $out->pushScope(['this' => 'props']); |
|
681 | 2 | $out->indent(+1); |
|
682 | |||
683 | try { |
||
684 | 2 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
685 | 2 | } finally { |
|
686 | 2 | $out->indent(-1); |
|
687 | 2 | $out->popScope(); |
|
688 | 2 | $out->appendCode("}"); |
|
689 | 2 | $out->select($prev); |
|
690 | 2 | } |
|
691 | |||
692 | 2 | return $out; |
|
693 | } |
||
694 | |||
695 | /** |
||
696 | * Compile component inclusion and rendering. |
||
697 | * |
||
698 | * @param DOMElement $node |
||
699 | * @param DOMAttr[] $attributes |
||
700 | * @param DOMAttr[] $special |
||
701 | * @param CompilerBuffer $out |
||
702 | */ |
||
703 | 14 | protected function compileComponentInclude(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
704 | // Mark the node as a component include. |
||
705 | 14 | $out->setNodeProp($node, self::T_INCLUDE, true); |
|
706 | |||
707 | // Generate the attributes into a property array. |
||
708 | 14 | $props = []; |
|
709 | 14 | foreach ($attributes as $name => $attribute) { |
|
710 | /* @var DOMAttr $attr */ |
||
711 | 6 | if ($this->isExpression($attribute->value)) { |
|
712 | 5 | $expr = $this->expr(substr($attribute->value, 1, -1), $out, $attribute); |
|
713 | 5 | } else { |
|
714 | 2 | $expr = var_export($attribute->value, true); |
|
715 | } |
||
716 | |||
717 | 6 | $props[] = var_export($name, true).' => '.$expr; |
|
718 | 14 | } |
|
719 | 14 | $propsStr = '['.implode(', ', $props).']'; |
|
720 | |||
721 | 14 | if (isset($special[self::T_WITH])) { |
|
722 | 3 | $withExpr = $this->expr($special[self::T_WITH]->value, $out, $special[self::T_WITH]); |
|
723 | 3 | unset($special[self::T_WITH]); |
|
724 | |||
725 | 3 | $propsStr = empty($props) ? $withExpr : $propsStr.' + (array)'.$withExpr; |
|
726 | 14 | } elseif (empty($props)) { |
|
727 | // By default the current context is passed to components. |
||
728 | 6 | $propsStr = $this->expr('this', $out); |
|
729 | 6 | } |
|
730 | |||
731 | // Compile the children blocks. |
||
732 | 14 | $blocks = $this->compileComponentBlocks($node, $out); |
|
733 | 13 | $blocksStr = $blocks->flush(); |
|
734 | |||
735 | 13 | if (isset($special[self::T_INCLUDE])) { |
|
736 | 1 | $name = $this->expr($special[self::T_INCLUDE]->value, $out, $special[self::T_INCLUDE]); |
|
737 | 1 | } else { |
|
738 | 12 | $name = var_export($node->tagName, true); |
|
739 | } |
||
740 | |||
741 | 13 | $out->appendCode("\$this->write($name, $propsStr, $blocksStr);\n"); |
|
742 | 13 | } |
|
743 | |||
744 | /** |
||
745 | * @param DOMElement $parent |
||
746 | * @return CompilerBuffer |
||
747 | */ |
||
748 | 14 | protected function compileComponentBlocks(DOMElement $parent, CompilerBuffer $out) { |
|
749 | 14 | $blocksOut = new CompilerBuffer(CompilerBuffer::STYLE_ARRAY, [ |
|
750 | 14 | 'baseIndent' => $out->getIndent(), |
|
751 | 14 | 'indent' => $out->getIndent() + 1, |
|
752 | 14 | 'depth' => $out->getDepth(), |
|
753 | 14 | 'scopes' => $out->getAllScopes(), |
|
754 | 14 | 'nodeProps' => $out->getNodePropArray() |
|
755 | 14 | ]); |
|
756 | 14 | $blocksOut->setSource($out->getSource()); |
|
757 | |||
758 | 14 | if ($this->isEmptyNode($parent)) { |
|
759 | 10 | return $blocksOut; |
|
760 | } |
||
761 | |||
762 | 5 | $use = '$'.implode(', $', $blocksOut->getScopeVariables()).', $children'; |
|
763 | |||
764 | 5 | $blocksOut->appendCode("function () use ($use) {\n"); |
|
765 | 5 | $blocksOut->indent(+1); |
|
766 | |||
767 | try { |
||
768 | 5 | foreach ($parent->childNodes as $node) { |
|
769 | 5 | $this->compileNode($node, $blocksOut); |
|
770 | 4 | } |
|
771 | 4 | } finally { |
|
772 | 5 | $blocksOut->indent(-1); |
|
773 | 5 | $blocksOut->appendCode("}"); |
|
774 | 5 | } |
|
775 | |||
776 | 4 | return $blocksOut; |
|
777 | } |
||
778 | |||
779 | /** |
||
780 | * Output the source of a node as a PHP comment. |
||
781 | * |
||
782 | * @param DOMElement $node The node to output. |
||
783 | * @param DOMAttr[] $attributes Regular attributes. |
||
784 | * @param DOMAttr[] $special Special attributes. |
||
785 | * @param CompilerBuffer $out The output buffer for the results. |
||
786 | */ |
||
787 | 34 | protected function compileTagComment(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
788 | // Don't double up comments. |
||
789 | 34 | if ($node->previousSibling && $node->previousSibling->nodeType === XML_COMMENT_NODE) { |
|
790 | return; |
||
791 | } |
||
792 | |||
793 | 34 | $str = '<'.$node->tagName; |
|
794 | 34 | foreach ($special as $attr) { |
|
795 | /* @var DOMAttr $attr */ |
||
796 | 34 | $str .= ' '.$attr->name.(empty($attr->value) ? '' : '="'.str_replace('"', '"', $attr->value).'"'); |
|
797 | 34 | } |
|
798 | 34 | $str .= '>'; |
|
799 | 34 | $comments = explode("\n", $str); |
|
800 | 34 | foreach ($comments as $comment) { |
|
801 | 34 | $out->appendCode("// $comment\n"); |
|
802 | 34 | } |
|
803 | 34 | } |
|
804 | |||
805 | /** |
||
806 | * @param DOMElement $node |
||
807 | * @param DOMAttr[] $attributes |
||
808 | * @param DOMAttr[] $special |
||
809 | * @param CompilerBuffer $out |
||
810 | * @param bool $force |
||
811 | */ |
||
812 | 62 | protected function compileOpenTag(DOMElement $node, $attributes, $special, CompilerBuffer $out, $force = false) { |
|
813 | 62 | $tagNameExpr = !empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : ''; |
|
814 | |||
815 | 62 | if ($node->tagName === self::T_X && empty($tagNameExpr)) { |
|
816 | 6 | return; |
|
817 | } |
||
818 | |||
819 | 59 | if (!empty($tagNameExpr)) { |
|
820 | 5 | $tagNameExpr = $this->expr($tagNameExpr, $out, $special[self::T_TAG]); |
|
821 | 5 | $tagName = $node->tagName === 'x' ? "''" : var_export($node->tagName, true); |
|
822 | |||
823 | 5 | $tagVar = $out->depthName('$tag', 1); |
|
824 | 5 | if ($node->hasChildNodes() || $force) { |
|
825 | 5 | $out->setNodeProp($node, self::T_TAG, $tagVar); |
|
826 | 5 | $out->depth(+1); |
|
827 | 5 | } |
|
828 | |||
829 | 5 | $out->appendCode("\n"); |
|
830 | 5 | $this->compileTagComment($node, $attributes, $special, $out); |
|
831 | 5 | $out->appendCode("$tagVar = \$this->tagName($tagNameExpr, $tagName);\n"); |
|
832 | 5 | $out->appendCode("if ($tagVar) {\n"); |
|
833 | 5 | $out->indent(+1); |
|
834 | 5 | $out->echoLiteral('<'); |
|
835 | 5 | $out->echoCode($tagVar); |
|
836 | 5 | } else { |
|
837 | 55 | $out->echoLiteral('<'.$node->tagName); |
|
838 | } |
||
839 | |||
840 | /* @var DOMAttr $attribute */ |
||
841 | 59 | foreach ($attributes as $name => $attribute) { |
|
842 | // Check for an attribute expression. |
||
843 | 27 | if ($this->isExpression($attribute->value)) { |
|
844 | 19 | $out->echoCode( |
|
845 | 19 | '$this->attribute('.var_export($name, true).', '. |
|
846 | 19 | $this->expr(substr($attribute->value, 1, -1), $out, $attribute). |
|
847 | 19 | ')'); |
|
848 | 27 | } elseif (null !== $fn = $this->getAttributeFunction($attribute)) { |
|
849 | 7 | $value = call_user_func($fn, var_export($attribute->value, true)); |
|
850 | |||
851 | 7 | $out->echoCode('$this->attribute('.var_export($name, true).', '.$value.')'); |
|
852 | 9 | } elseif ((empty($attribute->value) || $attribute->value === $name) && isset(self::$boolAttributes[$name])) { |
|
0 ignored issues
–
show
|
|||
853 | 2 | $out->echoLiteral(' '.$name); |
|
854 | 2 | } else { |
|
855 | 4 | $out->echoLiteral(' '.$name.'="'); |
|
856 | 4 | $out->echoLiteral(htmlspecialchars($attribute->value)); |
|
857 | 4 | $out->echoLiteral('"'); |
|
858 | } |
||
859 | 59 | } |
|
860 | |||
861 | 59 | if ($node->hasChildNodes() || $force) { |
|
862 | 57 | $out->echoLiteral('>'); |
|
863 | 57 | } else { |
|
864 | 3 | $out->echoLiteral(" />"); |
|
865 | } |
||
866 | |||
867 | 59 | if (!empty($tagNameExpr)) { |
|
868 | 5 | $out->indent(-1); |
|
869 | 5 | $out->appendCode("}\n\n"); |
|
870 | 5 | } |
|
871 | 59 | } |
|
872 | |||
873 | 31 | private function isExpression($value) { |
|
874 | 31 | return preg_match('`^{\S.*}$`', $value); |
|
875 | } |
||
876 | |||
877 | 60 | protected function compileCloseTag(DOMElement $node, $special, CompilerBuffer $out, $force = false) { |
|
878 | 60 | if (!$force && !$node->hasChildNodes()) { |
|
879 | 3 | return; |
|
880 | } |
||
881 | |||
882 | 58 | $tagNameExpr = $out->getNodeProp($node, self::T_TAG); //!empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : ''; |
|
883 | 58 | if (!empty($tagNameExpr)) { |
|
884 | 5 | $out->appendCode("\n"); |
|
885 | 5 | $out->appendCode("if ($tagNameExpr) {\n"); |
|
886 | 5 | $out->indent(+1); |
|
887 | |||
888 | 5 | $out->echoLiteral('</'); |
|
889 | 5 | $out->echoCode($tagNameExpr); |
|
890 | 5 | $out->echoLiteral('>'); |
|
891 | 5 | $out->indent(-1); |
|
892 | 5 | $out->appendCode("}\n"); |
|
893 | 5 | $out->depth(-1); |
|
894 | 58 | } elseif ($node->tagName !== self::T_X) { |
|
895 | 51 | $out->echoLiteral("</{$node->tagName}>"); |
|
896 | 51 | } |
|
897 | 58 | } |
|
898 | |||
899 | 4 | protected function isEmptyText(DOMNode $node) { |
|
900 | 4 | return $node instanceof \DOMText && empty(trim($node->data)); |
|
901 | } |
||
902 | |||
903 | 14 | protected function isEmptyNode(DOMNode $node) { |
|
904 | 14 | if (!$node->hasChildNodes()) { |
|
905 | 10 | return true; |
|
906 | } |
||
907 | |||
908 | 5 | foreach ($node->childNodes as $childNode) { |
|
909 | 5 | if ($childNode instanceof DOMElement) { |
|
910 | 3 | return false; |
|
911 | } |
||
912 | 2 | if ($childNode instanceof \DOMText && !$this->isEmptyText($childNode)) { |
|
913 | 2 | return false; |
|
914 | } |
||
915 | } |
||
916 | |||
917 | return true; |
||
918 | } |
||
919 | |||
920 | 11 | protected function compileIf(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
921 | 11 | $this->compileTagComment($node, $attributes, $special, $out); |
|
922 | 11 | $expr = $this->expr($special[self::T_IF]->value, $out, $special[self::T_IF]); |
|
923 | 10 | unset($special[self::T_IF]); |
|
924 | |||
925 | 10 | $elseNode = $this->findSpecialNode($node, self::T_ELSE, self::T_IF); |
|
926 | 10 | $out->setNodeProp($elseNode, 'skip', true); |
|
927 | |||
928 | 10 | $out->appendCode('if ('.$expr.") {\n"); |
|
929 | 10 | $out->indent(+1); |
|
930 | |||
931 | 10 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
932 | |||
933 | 10 | $out->indent(-1); |
|
934 | |||
935 | 10 | if ($elseNode) { |
|
936 | 2 | list($attributes, $special) = $this->splitAttributes($elseNode); |
|
937 | 2 | unset($special[self::T_ELSE]); |
|
938 | |||
939 | 2 | $out->appendCode("} else {\n"); |
|
940 | |||
941 | 2 | $out->indent(+1); |
|
942 | 2 | $this->compileSpecialNode($elseNode, $attributes, $special, $out); |
|
943 | 2 | $out->indent(-1); |
|
944 | 2 | } |
|
945 | |||
946 | 10 | $out->appendCode("}\n"); |
|
947 | 10 | } |
|
948 | |||
949 | 16 | protected function compileEach(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
950 | 16 | $this->compileTagComment($node, $attributes, $special, $out); |
|
951 | 16 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
952 | |||
953 | 16 | $emptyNode = $this->findSpecialNode($node, self::T_EMPTY, self::T_ELSE); |
|
954 | 16 | $out->setNodeProp($emptyNode, 'skip', true); |
|
955 | |||
956 | 16 | if ($emptyNode === null) { |
|
957 | 14 | $this->compileEachLoop($node, $attributes, $special, $out); |
|
958 | 13 | } else { |
|
959 | 2 | $expr = $this->expr("empty({$special[self::T_EACH]->value})", $out); |
|
960 | |||
961 | 2 | list ($emptyAttributes, $emptySpecial) = $this->splitAttributes($emptyNode); |
|
962 | 2 | unset($emptySpecial[self::T_EMPTY]); |
|
963 | |||
964 | 2 | $out->appendCode('if ('.$expr.") {\n"); |
|
965 | |||
966 | 2 | $out->indent(+1); |
|
967 | 2 | $this->compileSpecialNode($emptyNode, $emptyAttributes, $emptySpecial, $out); |
|
968 | 2 | $out->indent(-1); |
|
969 | |||
970 | 2 | $out->appendCode("} else {\n"); |
|
971 | |||
972 | 2 | $out->indent(+1); |
|
973 | 2 | $this->compileEachLoop($node, $attributes, $special, $out); |
|
974 | 2 | $out->indent(-1); |
|
975 | |||
976 | 2 | $out->appendCode("}\n"); |
|
977 | } |
||
978 | |||
979 | 15 | $this->compileCloseTag($node, $special, $out); |
|
980 | 15 | } |
|
981 | |||
982 | 2 | protected function compileWith(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
983 | 2 | $this->compileTagComment($node, $attributes, $special, $out); |
|
984 | |||
985 | 2 | $out->depth(+1); |
|
986 | 2 | $scope = ['this' => $out->depthName('props')]; |
|
987 | 2 | if (!empty($special[self::T_AS])) { |
|
988 | 2 | if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) { |
|
989 | // The template specified an x-as attribute to alias the with expression. |
||
990 | 1 | $scope = [$m[1] => $out->depthName('props')]; |
|
991 | 1 | } else { |
|
992 | 1 | throw $out->createCompilerException( |
|
993 | 1 | $special[self::T_AS], |
|
994 | 1 | new \Exception("Invalid identifier in x-as attribute.") |
|
995 | 1 | ); |
|
996 | } |
||
997 | 1 | } |
|
998 | 1 | $with = $this->expr($special[self::T_WITH]->value, $out); |
|
999 | |||
1000 | 1 | unset($special[self::T_WITH], $special[self::T_AS]); |
|
1001 | |||
1002 | 1 | $out->pushScope($scope); |
|
1003 | 1 | $out->appendCode('$'.$out->depthName('props')." = $with;\n"); |
|
1004 | |||
1005 | 1 | $this->compileSpecialNode($node, $attributes, $special, $out); |
|
1006 | |||
1007 | 1 | $out->depth(-1); |
|
1008 | 1 | $out->popScope(); |
|
1009 | 1 | } |
|
1010 | |||
1011 | 2 | protected function compileLiteral(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
1012 | 2 | $this->compileTagComment($node, $attributes, $special, $out); |
|
1013 | 2 | unset($special[self::T_LITERAL]); |
|
1014 | |||
1015 | 2 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
1016 | |||
1017 | 2 | foreach ($node->childNodes as $childNode) { |
|
1018 | 2 | $html = $childNode->ownerDocument->saveHTML($childNode); |
|
1019 | 2 | $out->echoLiteral($html); |
|
1020 | 2 | } |
|
1021 | |||
1022 | 2 | $this->compileCloseTag($node, $special, $out); |
|
1023 | 2 | } |
|
1024 | |||
1025 | 22 | protected function compileElement(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
1026 | 22 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
1027 | |||
1028 | 22 | foreach ($node->childNodes as $childNode) { |
|
1029 | 22 | $this->compileNode($childNode, $out); |
|
1030 | 22 | } |
|
1031 | |||
1032 | 22 | $this->compileCloseTag($node, $special, $out); |
|
1033 | 22 | } |
|
1034 | |||
1035 | /** |
||
1036 | * Find a special node in relation to another node. |
||
1037 | * |
||
1038 | * This method is used to find things such as x-empty and x-else elements. |
||
1039 | * |
||
1040 | * @param DOMElement $node The node to search in relation to. |
||
1041 | * @param string $attribute The name of the attribute to search for. |
||
1042 | * @param string $parentAttribute The name of the parent attribute to resolve conflicts. |
||
1043 | * @return DOMElement|null Returns the found element node or **null** if not found. |
||
1044 | */ |
||
1045 | 25 | protected function findSpecialNode(DOMElement $node, $attribute, $parentAttribute) { |
|
1046 | // First look for a sibling after the node. |
||
1047 | 25 | for ($sibNode = $node->nextSibling; $sibNode !== null; $sibNode = $sibNode->nextSibling) { |
|
1048 | 2 | if ($sibNode instanceof DOMElement && $sibNode->hasAttribute($attribute)) { |
|
1049 | 2 | return $sibNode; |
|
1050 | } |
||
1051 | |||
1052 | // Stop searching if we encounter another node. |
||
1053 | 2 | if (!$this->isEmptyText($sibNode)) { |
|
1054 | break; |
||
1055 | } |
||
1056 | 2 | } |
|
1057 | |||
1058 | // Next look inside the node. |
||
1059 | 23 | $parentFound = false; |
|
1060 | 23 | foreach ($node->childNodes as $childNode) { |
|
1061 | 22 | if (!$parentFound && $childNode instanceof DOMElement && $childNode->hasAttribute($attribute)) { |
|
1062 | 2 | return $childNode; |
|
1063 | } |
||
1064 | |||
1065 | 22 | if ($childNode instanceof DOMElement) { |
|
1066 | 16 | $parentFound = $childNode->hasAttribute($parentAttribute); |
|
1067 | 22 | } elseif ($childNode instanceof \DOMText && !empty(trim($childNode->data))) { |
|
1068 | 6 | $parentFound = false; |
|
1069 | 6 | } |
|
1070 | 23 | } |
|
1071 | |||
1072 | 21 | return null; |
|
1073 | } |
||
1074 | |||
1075 | /** |
||
1076 | * @param DOMElement $node |
||
1077 | * @param array $attributes |
||
1078 | * @param array $special |
||
1079 | * @param CompilerBuffer $out |
||
1080 | */ |
||
1081 | 16 | private function compileEachLoop(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
1082 | 16 | $each = $this->expr($special[self::T_EACH]->value, $out); |
|
1083 | 16 | unset($special[self::T_EACH]); |
|
1084 | |||
1085 | 16 | $as = ['', $out->depthName('props', 1)]; |
|
1086 | 16 | $scope = ['this' => $as[1]]; |
|
1087 | 16 | if (!empty($special[self::T_AS])) { |
|
1088 | 9 | if (preg_match('`^(?:([a-z0-9]+)\s+)?([a-z0-9]+)$`i', $special[self::T_AS]->value, $m)) { |
|
1089 | 8 | $scope = [$m[2] => $as[1]]; |
|
1090 | 8 | if (!empty($m[1])) { |
|
1091 | 5 | $scope[$m[1]] = $as[0] = $out->depthName('i', 1); |
|
1092 | |||
1093 | // Add loop tracking variables. |
||
1094 | 5 | $d = $out->depthName('', 1); |
|
1095 | 5 | } |
|
1096 | 8 | } else { |
|
1097 | 1 | throw $out->createCompilerException( |
|
1098 | 1 | $special[self::T_AS], |
|
1099 | 1 | new \Exception("Invalid identifier in x-as attribute.") |
|
1100 | 1 | ); |
|
1101 | } |
||
1102 | 8 | } |
|
1103 | 15 | unset($special[self::T_AS]); |
|
1104 | |||
1105 | 15 | if (isset($d)) { |
|
1106 | 5 | $out->appendCode("\$count$d = count($each);\n"); |
|
1107 | 5 | $out->appendCode("\$index$d = -1;\n"); |
|
1108 | 5 | } |
|
1109 | |||
1110 | 15 | if (empty($as[0])) { |
|
1111 | 10 | $out->appendCode("foreach ($each as \${$as[1]}) {\n"); |
|
1112 | 10 | } else { |
|
1113 | 5 | $out->appendCode("foreach ($each as \${$as[0]} => \${$as[1]}) {\n"); |
|
1114 | } |
||
1115 | 15 | $out->depth(+1); |
|
1116 | 15 | $out->indent(+1); |
|
1117 | 15 | $out->pushScope($scope); |
|
1118 | |||
1119 | 15 | if (isset($d)) { |
|
1120 | 5 | $out->appendCode("\$index$d++;\n"); |
|
1121 | 5 | $out->appendCode("\$first$d = \$index$d === 0;\n"); |
|
1122 | 5 | $out->appendCode("\$last$d = \$index$d === \$count$d - 1;\n"); |
|
1123 | 5 | } |
|
1124 | |||
1125 | 15 | foreach ($node->childNodes as $childNode) { |
|
1126 | 15 | $this->compileNode($childNode, $out); |
|
1127 | 15 | } |
|
1128 | |||
1129 | 15 | $out->indent(-1); |
|
1130 | 15 | $out->depth(-1); |
|
1131 | 15 | $out->popScope(); |
|
1132 | 15 | $out->appendCode("}\n"); |
|
1133 | 15 | } |
|
1134 | |||
1135 | 66 | protected function ltrim($text, \DOMNode $node, CompilerBuffer $out) { |
|
1136 | 66 | if ($this->inPre($node)) { |
|
1137 | return $text; |
||
1138 | } |
||
1139 | |||
1140 | |||
1141 | 66 | for ($sib = $node->previousSibling; $sib !== null && $this->canSkip($sib, $out); $sib = $sib->previousSibling) { |
|
1142 | // |
||
1143 | 5 | } |
|
1144 | 66 | if ($sib === null) { |
|
1145 | 66 | return ltrim($text); |
|
1146 | } |
||
1147 | // if ($this->canSkip($sib, $out)) { |
||
1148 | // return ltrim($text); |
||
1149 | // } |
||
1150 | |||
1151 | 7 | $text = preg_replace('`^\s*\n\s*`', "\n", $text, -1, $count); |
|
1152 | 7 | if ($count === 0) { |
|
1153 | $text = preg_replace('`^\s+`', ' ', $text); |
||
1154 | } |
||
1155 | |||
1156 | // if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) { |
||
1157 | // return ltrim($text); |
||
1158 | // } |
||
1159 | 7 | return $text; |
|
1160 | } |
||
1161 | |||
1162 | /** |
||
1163 | * Whether or not a node can be skipped for the purposes of trimming whitespace. |
||
1164 | * |
||
1165 | * @param DOMNode|null $node The node to test. |
||
1166 | * @param CompilerBuffer|null $out The compiler information. |
||
1167 | * @return bool Returns **true** if whitespace can be trimmed right up to the node or **false** otherwise. |
||
1168 | */ |
||
1169 | 13 | private function canSkip(\DOMNode $node, CompilerBuffer $out) { |
|
1170 | 13 | if ($out->getNodeProp($node, 'skip')) { |
|
1171 | 2 | return true; |
|
1172 | } |
||
1173 | |||
1174 | 13 | switch ($node->nodeType) { |
|
1175 | 13 | case XML_TEXT_NODE: |
|
1176 | 2 | return false; |
|
1177 | 13 | case XML_COMMENT_NODE: |
|
1178 | 1 | return true; |
|
1179 | 13 | case XML_ELEMENT_NODE: |
|
1180 | /* @var \DOMElement $node */ |
||
1181 | 13 | if ($node->tagName === self::T_X |
|
1182 | 13 | || ($node->tagName === 'script' && $node->hasAttribute(self::T_AS)) // expression assignment |
|
1183 | 12 | || ($node->hasAttribute(self::T_WITH) && $node->hasAttribute(self::T_AS)) // with assignment |
|
1184 | 9 | || ($node->hasAttribute(self::T_BLOCK) || $node->hasAttribute(self::T_COMPONENT)) |
|
1185 | 13 | ) { |
|
1186 | 6 | return true; |
|
1187 | } |
||
1188 | 8 | } |
|
1189 | |||
1190 | 8 | return false; |
|
1191 | } |
||
1192 | |||
1193 | 66 | protected function rtrim($text, \DOMNode $node, CompilerBuffer $out) { |
|
1194 | 66 | if ($this->inPre($node)) { |
|
1195 | return $text; |
||
1196 | } |
||
1197 | |||
1198 | 66 | for ($sib = $node->nextSibling; $sib !== null && $this->canSkip($sib, $out); $sib = $sib->nextSibling) { |
|
1199 | // |
||
1200 | 5 | } |
|
1201 | 66 | if ($sib === null) { |
|
1202 | 65 | return rtrim($text); |
|
1203 | } |
||
1204 | |||
1205 | 5 | $text = preg_replace('`\s*\n\s*$`', "\n", $text, -1, $count); |
|
1206 | 5 | if ($count === 0) { |
|
1207 | 5 | $text = preg_replace('`\s+$`', ' ', $text); |
|
1208 | 5 | } |
|
1209 | |||
1210 | // if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) { |
||
1211 | // return rtrim($text); |
||
1212 | // } |
||
1213 | 5 | return $text; |
|
1214 | } |
||
1215 | |||
1216 | 66 | protected function inPre(\DOMNode $node) { |
|
1217 | 66 | for ($node = $node->parentNode; $node !== null; $node = $node->parentNode) { |
|
1218 | 66 | if (in_array($node->nodeType, ['code', 'pre'], true)) { |
|
1219 | return true; |
||
1220 | } |
||
1221 | 66 | } |
|
1222 | 66 | return false; |
|
1223 | } |
||
1224 | |||
1225 | 4 | private function compileChildBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
1226 | /* @var DOMAttr $child */ |
||
1227 | 4 | $child = $special[self::T_CHILDREN]; |
|
1228 | 4 | unset($special[self::T_CHILDREN]); |
|
1229 | |||
1230 | 4 | $name = $child->value === '' ? 0 : strtolower($child->value); |
|
1231 | 4 | if ($name !== 0) { |
|
1232 | 2 | if (empty($name)) { |
|
1233 | throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("Block names cannot be empty.")); |
||
1234 | } |
||
1235 | 2 | if (!preg_match(self::IDENT_REGEX, $name)) { |
|
1236 | throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("The block name isn't a valid identifier.")); |
||
1237 | } |
||
1238 | 2 | } |
|
1239 | |||
1240 | 4 | $keyStr = var_export($name, true); |
|
1241 | |||
1242 | 4 | $this->compileOpenTag($node, $attributes, $special, $out, true); |
|
1243 | |||
1244 | 4 | $out->appendCode("\$this->writeChildren(\$children[{$keyStr}]);\n"); |
|
1245 | |||
1246 | 4 | $this->compileCloseTag($node, $special, $out, true); |
|
1247 | 4 | } |
|
1248 | |||
1249 | /** |
||
1250 | * Compile an `<script type="ebi">` node. |
||
1251 | * |
||
1252 | * @param DOMElement $node The node to compile. |
||
1253 | * @param DOMAttr[] $attributes The node's attributes. |
||
1254 | * @param DOMAttr[] $special An array of special attributes. |
||
1255 | * @param CompilerBuffer $out The compiler output. |
||
1256 | */ |
||
1257 | 10 | private function compileExpressionNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) { |
|
1258 | 10 | $str = $node->nodeValue; |
|
1259 | |||
1260 | try { |
||
1261 | 10 | $expr = $this->expr($str, $out); |
|
1262 | 10 | } catch (SyntaxError $ex) { |
|
1263 | 1 | $context = []; |
|
1264 | 1 | if (preg_match('`^(.*) around position (\d*)\.$`', $ex->getMessage(), $m)) { |
|
1265 | 1 | $add = substr_count($str, "\n", 0, $m[2]); |
|
1266 | |||
1267 | 1 | $context['line'] = $node->getLineNo() + $add; |
|
1268 | 1 | } |
|
1269 | |||
1270 | 1 | throw $out->createCompilerException($node, $ex, $context); |
|
1271 | } |
||
1272 | |||
1273 | 9 | if (isset($special[self::T_AS])) { |
|
1274 | 7 | if (null !== $this->closest($node, function (\DOMNode $n) use ($out) { |
|
1275 | 7 | return $out->getNodeProp($n, self::T_INCLUDE); |
|
1276 | 7 | })) { |
|
1277 | 1 | throw $out->createCompilerException( |
|
1278 | 1 | $node, |
|
1279 | 1 | new \Exception("Expressions with x-as assignments cannot be declared inside child blocks.") |
|
1280 | 1 | ); |
|
1281 | } |
||
1282 | |||
1283 | 6 | if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) { |
|
1284 | // The template specified an x-as attribute to alias the with expression. |
||
1285 | 5 | $out->depth(+1); |
|
1286 | 5 | $scope = [$m[1] => $out->depthName('expr')]; |
|
1287 | 5 | $out->pushScope($scope); |
|
1288 | 5 | $out->appendCode('$'.$out->depthName('expr')." = $expr;\n"); |
|
1289 | 5 | } else { |
|
1290 | 1 | throw $out->createCompilerException( |
|
1291 | 1 | $special[self::T_AS], |
|
1292 | 1 | new \Exception("Invalid identifier in x-as attribute.") |
|
1293 | 1 | ); |
|
1294 | } |
||
1295 | 7 | } elseif (!empty($special[self::T_UNESCAPE])) { |
|
1296 | 1 | $out->echoCode($expr); |
|
1297 | 1 | } else { |
|
1298 | 1 | $out->echoCode($this->compileEscape($expr)); |
|
1299 | } |
||
1300 | 7 | } |
|
1301 | |||
1302 | /** |
||
1303 | * Similar to jQuery's closest method. |
||
1304 | * |
||
1305 | * @param DOMNode $node |
||
1306 | * @param callable $test |
||
1307 | * @return DOMNode |
||
1308 | */ |
||
1309 | 7 | private function closest(\DOMNode $node, callable $test) { |
|
1310 | 7 | for ($visitor = $node; $visitor !== null && !$test($visitor); $visitor = $visitor->parentNode) { |
|
1311 | // Do nothing. The logic is all in the loop. |
||
1312 | 7 | } |
|
1313 | 7 | return $visitor; |
|
1314 | } |
||
1315 | |||
1316 | /** |
||
1317 | * @param DOMElement $node |
||
1318 | * @param $attributes |
||
1319 | * @param $special |
||
1320 | * @param CompilerBuffer $out |
||
1321 | */ |
||
1322 | 39 | protected function compileBasicElement(DOMElement $node, $attributes, $special, CompilerBuffer $out) { |
|
1323 | 39 | $this->compileOpenTag($node, $attributes, $special, $out); |
|
1324 | |||
1325 | 39 | foreach ($node->childNodes as $childNode) { |
|
1326 | 37 | $this->compileNode($childNode, $out); |
|
1327 | 39 | } |
|
1328 | |||
1329 | 38 | $this->compileCloseTag($node, $special, $out); |
|
1330 | 38 | } |
|
1331 | |||
1332 | 31 | protected function compileEscape($php) { |
|
1333 | // return 'htmlspecialchars('.$php.')'; |
||
1334 | 31 | return '$this->escape('.$php.')'; |
|
1335 | } |
||
1336 | } |
||
1337 |
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.