Issues (102)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Compiler.php (6 issues)

Upgrade to new PHP Analysis Engine

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.
0 ignored issues
show
Should the type for parameter $function not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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) {
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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");
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) {
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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) {
0 ignored issues
show
function ($name) use($na...e, true) . ']'; } } is of type object<Closure>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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('"', '&quot;', $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])) {
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) {
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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) {
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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