Completed
Push — master ( 7fd7e1...8ce59e )
by Todd
06:35
created

Compiler::compileExpressionNode()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 0
cts 0
cp 0
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 29
nc 7
nop 4
crap 56
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';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
17
    const T_EACH = 'x-each';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
18
    const T_WITH = 'x-with';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
19
    const T_LITERAL = 'x-literal';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
20
    const T_AS = 'x-as';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
21
    const T_COMPONENT = 'x-component';
22
    const T_CHILDREN = 'x-children';
23
    const T_BLOCK = 'x-block';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
24
    const T_ELSE = 'x-else';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
25
    const T_EMPTY = 'x-empty';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
26
    const T_X = 'x';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 9 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
27
    const T_INCLUDE = 'x-include';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
28
    const T_EBI = 'ebi';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
29
    const T_UNESCAPE = 'x-unescape';
30
    const T_TAG = 'x-tag';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
56
        'area' => 'i',
57
        'article' => 'b',
58
        'aside' => 'b',
59
        'audio' => 'i',
60
        'b' => 'i',
61
        'base' => 'i',
62
//        'basefont' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
63
        'bdi' => 'i',
64
        'bdo' => 'i',
65
//        'bgsound' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
66
//        'big' => 'i',
67
        'x' => 'i',
68
//        'blink' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
69
        'blockquote' => 'b',
70
        'body' => 'b',
71
        'br' => 'i',
72
        'button' => 'i',
73
        'canvas' => 'b',
74
        'caption' => 'i',
75
//        'center' => 'b',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
76
        'cite' => 'i',
77
        'code' => 'i',
78
        'col' => 'i',
79
        'colgroup' => 'i',
80
//        'command' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
90
        'div' => 'i',
91
        'dl' => 'b',
92
        'dt' => 'b',
93
//        'element' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
94
        'em' => 'i',
95
        'embed' => 'i',
96
        'fieldset' => 'b',
97
        'figcaption' => 'b',
98
        'figure' => 'b',
99
//        'font' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
129
        'main' => 'b',
130
        'map' => 'i',
131
        'mark' => 'i',
132
//        'marquee' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
165
        'slot' => 'i',
166
        'small' => 'i',
167
        'source' => 'i',
168
//        'spacer' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
169
        'span' => 'i',
170
//        'strike' => 'i',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
201
        'circle' => 's',
202
        'desc' => 's',
203
        'defs' => 's',
204
        'discard' => 's',
205
        'ellipse' => 's',
206
        'g' => 's',
207
//        'image' => 's',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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',
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
225 44
        'use' => 's',
226 44
    ];
227 44
228 44
    protected static $boolAttributes = [
229
        'checked' => 1,
230
        'itemscope' => 1,
231
        'required' => 1,
232
        'selected' => 1,
233
    ];
234
235
    /**
236 42
     * @var ExpressionLanguage
237 42
     */
238 1
    protected $expressions;
239
240
    public function __construct() {
241 42
        $this->expressions = new ExpressionLanguage();
242 42
        $this->expressions->setNamePattern('/[@a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A');
243 42
        $this->expressions->register(
244 42
            'hasChildren',
245
            function ($name = null) {
246 42
                return empty($name) ? 'isset($children[0])' : "isset(\$children[$name ?: 0])";
247
            },
248 42
            function ($name = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
249 42
                return false;
250 42
            });
251
    }
252 42
253 41
    /**
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
Documentation introduced by
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 41
    public function defineFunction($name, $function = null) {
260
        if ($function === null) {
261
            $function = $name;
262 42
        }
263 42
264 42
        $this->expressions->register(
265
            $name,
266 42
            $this->getFunctionCompiler($name, $function),
267
            $this->getFunctionEvaluator($function)
268 42
        );
269 42
    }
270 7
271 42
    private function getFunctionEvaluator($function) {
0 ignored issues
show
Documentation introduced by
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 41
        if ($function === 'empty') {
273 41
            return function ($expr) {
274
                return empty($expr);
275
            };
276
        } elseif ($function === 'isset') {
277 41
            return function ($expr) {
278 41
                return isset($expr);
279 5
            };
280 41
        }
281
282
        return $function;
283
    }
284 42
285
    private function getFunctionCompiler($name, $function) {
286
        $var = var_export(strtolower($name), true);
287 38
        $fn = function ($expr) use ($var) {
288 38
            return "\$this->call($var, $expr)";
289
        };
290 38
291
        if (is_string($function)) {
292 38
            $fn = function (...$args) use ($function) {
293 38
                return $function.'('.implode(', ', $args).')';
294
            };
295 38
        } elseif (is_array($function)) {
296 38
            if (is_string($function[0])) {
297 37
                $fn = function (...$args) use ($function) {
298 37
                    return "$function[0]::$function[1](".implode(', ', $args).')';
299
                };
300
            } elseif ($function[0] instanceof Ebi) {
301 38
                $fn = function (...$args) use ($function) {
302
                    return "\$this->$function[1](".implode(', ', $args).')';
303
                };
304 38
            }
305
        }
306 38
307
        return $fn;
308 38
    }
309 37
310 37
    public function compile($src, array $options = []) {
311
        $options += ['basename' => '', 'path' => '', 'runtime' => true];
312 1
313
        $src = trim($src);
314
315 38
        $out = new CompilerBuffer();
316 38
317
        $out->setBasename($options['basename']);
318 38
        $out->setSource($src);
319
        $out->setPath($options['path']);
320 38
321 38
        $dom = new \DOMDocument();
322
323
        $fragment = false;
324 38
        if (strpos($src, '<html') === false) {
325 38
            $src = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><html><body>$src</body></html>";
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
326
            $fragment = true;
327 38
        }
328 37
329
        libxml_use_internal_errors(true);
330 1
        $dom->loadHTML($src, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOCDATA | LIBXML_NOXMLDECL);
331
//        $arr = $this->domToArray($dom);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
332
333 38
        if ($options['runtime']) {
334 38
            $name = var_export($options['basename'], true);
335
            $out->appendCode("\$this->defineComponent($name, function (\$props = [], \$children = []) {\n");
336
        } else {
337 33
            $out->appendCode("function (\$props = [], \$children = []) {\n");
338 33
        }
339
340
        $out->pushScope(['this' => 'props']);
341 38
        $out->indent(+1);
342 38
343 4
        $parent = $fragment ? $dom->firstChild->nextSibling->firstChild : $dom;
344
345
        foreach ($parent->childNodes as $node) {
346 38
            $this->compileNode($node, $out);
347 38
        }
348 36
349 36
        $out->indent(-1);
350 35
        $out->popScope();
351
352 35
        if ($options['runtime']) {
353 35
            $out->appendCode("});");
354 2
        } else {
355
            $out->appendCode("};");
356 1
        }
357 1
358 1
        $r = $out->flush();
359 1
360 1
        $errs = libxml_get_errors();
0 ignored issues
show
Unused Code introduced by
$errs is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
361
362
        return $r;
363
    }
364
365 38
    protected function isComponent($tag) {
366
        return !isset(static::$htmlTags[$tag]);
367
    }
368
369
    protected function compileNode(DOMNode $node, CompilerBuffer $out) {
370
        if ($out->getNodeProp($node, 'skip')) {
371
            return;
372
        }
373
374
        switch ($node->nodeType) {
375
            case XML_TEXT_NODE:
376
                $this->compileTextNode($node, $out);
377
                break;
378
            case XML_ELEMENT_NODE:
379
                /* @var \DOMElement $node */
380
                $this->compileElementNode($node, $out);
381
                break;
382
            case XML_COMMENT_NODE:
383
                /* @var \DOMComment $node */
384
                $this->compileCommentNode($node, $out);
385
                break;
386
            case XML_DOCUMENT_TYPE_NODE:
387
                $out->echoLiteral("<!DOCTYPE {$node->name}>\n");
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in DOMNode.

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.

Loading history...
388
                break;
389
            case XML_CDATA_SECTION_NODE:
390
                $this->compileTextNode($node, $out);
391
                break;
392
            default:
393
                $r = "// Unknown node\n".
0 ignored issues
show
Unused Code introduced by
$r is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
394
                    '// '.str_replace("\n", "\n// ", $node->ownerDocument->saveHTML($node));
395
        }
396
    }
397
398
    protected function domToArray(DOMNode $root) {
0 ignored issues
show
Documentation introduced by
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 1
            }
406 1
        }
407
408
        if ($root->hasChildNodes()) {
409 1
            $children = $root->childNodes;
410
            if ($children->length == 1) {
411 1
                $child = $children->item(0);
412 1
                if ($child->nodeType == XML_TEXT_NODE) {
413
                    $result['_value'] = $child->nodeValue;
414 1
                    return count($result) == 1
415 1
                        ? $result['_value']
416 1
                        : $result;
417
                }
418 1
            }
419
            $groups = [];
420 36
            foreach ($children as $child) {
421 36
                if (!isset($result[$child->nodeName])) {
422
                    $result[$child->nodeName] = $this->domToArray($child);
423 36
                } else {
424
                    if (!isset($groups[$child->nodeName])) {
425 36
                        $result[$child->nodeName] = [$result[$child->nodeName]];
426 36
                        $groups[$child->nodeName] = 1;
427 22
                    }
428 1
                    $result[$child->nodeName][] = $this->domToArray($child);
429
                }
430 22
            }
431
        }
432
433
        return $result;
434
    }
435
436
    protected function newline(DOMNode $node, CompilerBuffer $out) {
437
        if ($node->previousSibling && $node->previousSibling->nodeType !== XML_COMMENT_NODE) {
438
            $out->appendCode("\n");
439
        }
440 36
    }
441
442
    protected function compileCommentNode(\DOMComment $node, CompilerBuffer $out) {
443 36
        $comments = explode("\n", trim($node->nodeValue));
444
445 35
        $this->newline($node, $out);
446 35
        foreach ($comments as $comment) {
447
            $out->appendCode("// $comment\n");
448 35
        }
449 30
    }
450
451 17
    protected function compileTextNode(DOMNode $node, CompilerBuffer $out) {
452
        $nodeText = $node->nodeValue;
453 17
454 17
        $items = $this->splitExpressions($nodeText);
455
        foreach ($items as $i => list($text, $offset)) {
456
            if (preg_match('`^{\S`', $text)) {
457 17
                if (preg_match('`^{\s*unescape\((.+)\)\s*}$`', $text, $m)) {
458
                    $out->echoCode($this->expr($m[1], $out));
459 35
                } else {
460
                    try {
461 36
                        $expr = substr($text, 1, -1);
462 36
                        $out->echoCode($this->compileEscape($this->expr($expr, $out)));
463 36
                    } catch (SyntaxError $ex) {
464
                        $nodeLineCount = substr_count($nodeText, "\n");
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
465
                        $offsetLineCount = substr_count($nodeText, "\n", 0, $offset);
466 34
                        $line = $node->getLineNo() - $nodeLineCount + $offsetLineCount;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 12 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
467 34
                        throw $out->createCompilerException($node, $ex, ['source' => $expr, 'line' => $line]);
468
                    }
469 34
                }
470 34
            } else {
471 16
                if ($i === 0) {
472 29
                    $text = $this->ltrim($text, $node, $out);
473 1
                }
474
                if ($i === count($items) - 1) {
475 28
                    $text = $this->rtrim($text, $node, $out);
476
                }
477 34
478
                $out->echoLiteral($text);
479 34
            }
480 4
        }
481
    }
482
483 34
    protected function compileElementNode(DOMElement $node, CompilerBuffer $out) {
484
        list($attributes, $special) = $this->splitAttributes($node);
485
486
        if ($node->tagName === 'script' && ((isset($attributes['type']) && $attributes['type']->value === self::T_EBI) || !empty($special[self::T_AS]) || !empty($special[self::T_UNESCAPE]))) {
487
            $this->compileExpressionNode($node, $attributes, $special, $out);
488
        } elseif (!empty($special) || $this->isComponent($node->tagName)) {
489
            $this->compileSpecialNode($node, $attributes, $special, $out);
490
        } else {
491
            $this->compileBasicElement($node, $attributes, $special, $out);
492
        }
493
    }
494
495
    protected function splitExpressions($value) {
496
        $values = preg_split('`({\S[^}]*?})`', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE);
497
        return $values;
498 10
    }
499 10
500
    /**
501 10
     * Compile the PHP for an expression
502 10
     *
503 10
     * @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 6
     * @return string Returns a string of PHP code.
507
     */
508
    protected function expr($expr, CompilerBuffer $out, DOMAttr $attr = null) {
509
        $names = $out->getScopeVariables();
510
511 35
        try {
512 35
            $compiled = $this->expressions->compile($expr, function ($name) use ($names) {
0 ignored issues
show
Documentation introduced by
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 35
                if (isset($names[$name])) {
514
                    return $names[$name];
515 35
                } elseif ($name[0] === '@') {
516 34
                    return 'this->meta['.var_export(substr($name, 1), true).']';
517 29
                } else {
518
                    return $names['this'].'['.var_export($name, true).']';
519 34
                }
520
            });
521
        } catch (SyntaxError $ex) {
522
            if ($attr !== null) {
523 35
                throw $out->createCompilerException($attr, $ex);
524 7
            } else {
525 35
                throw $ex;
526
            }
527 35
        }
528
529
        if ($attr !== null && null !== $fn = $this->getAttributeFunction($attr)) {
530 30
            $compiled = call_user_func($fn, $compiled);
531 30
        }
532
533
        return $compiled;
534 30
    }
535 8
536 8
    /**
537 30
     * Get the compiler function to wrap an attribute.
538 7
     *
539 7
     * Attribute functions are regular expression functions, but with a special naming convention. The following naming
540 30
     * conventions are supported:
541 12
     *
542 12
     * - **@tag:attribute**: Applies to an attribute only on a specific tag.
543 21
     * - **@attribute**: Applies to all attributes with a given name.
544 1
     *
545 1
     * @param DOMAttr $attr The attribute to look at.
546 21
     * @return callable|null A function or **null** if the attribute doesn't have a function.
547 2
     */
548 2
    private function getAttributeFunction(DOMAttr $attr) {
549 21
        $keys = ['@'.$attr->ownerElement->tagName.':'.$attr->name, '@'.$attr->name];
550 1
551 1
        foreach ($keys as $key) {
552 21
            if (null !== $fn = $this->expressions->getFunctionCompiler($key)) {
553 3
                return $fn;
554
            }
555 2
        }
556
    }
557 1
558
    /**
559 3
     * @param DOMElement $node
560 21
     */
561 2
    protected function splitAttributes(DOMElement $node) {
562 2
        $attributes = [];
563 19
        $special = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
564 19
565 6
        foreach ($node->attributes as $name => $attribute) {
566
            if (isset(static::$special[$name])) {
567 18
                $special[$name] = $attribute;
568
            } else {
569 19
                $attributes[$name] = $attribute;
570
            }
571 30
        }
572
573
        uksort($special, function ($a, $b) {
574
            return strnatcmp(static::$special[$a], static::$special[$b]);
575
        });
576
577
        return [$attributes, $special];
578
    }
579
580
    protected function compileSpecialNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
581 8
        $specialName = key($special);
582 8
583 8
        switch ($specialName) {
584
            case self::T_COMPONENT:
585 8
                $this->compileComponentRegister($node, $attributes, $special, $out);
586
                break;
587 8
            case self::T_IF:
588 8
                $this->compileIf($node, $attributes, $special, $out);
589 8
                break;
590 8
            case self::T_EACH:
591
                $this->compileEach($node, $attributes, $special, $out);
592
                break;
593 8
            case self::T_BLOCK:
594 8
                $this->compileBlock($node, $attributes, $special, $out);
595 8
                break;
596 8
            case self::T_CHILDREN:
597 8
                $this->compileChildBlock($node, $attributes, $special, $out);
598 8
                break;
599
            case self::T_INCLUDE:
600 8
                $this->compileComponentInclude($node, $attributes, $special, $out);
601
                break;
602 1
            case self::T_WITH:
603 1
                if ($this->isComponent($node->tagName)) {
604 1
                    // With has a special meaning in components.
605
                    $this->compileComponentInclude($node, $attributes, $special, $out);
606 1
                } else {
607
                    $this->compileWith($node, $attributes, $special, $out);
608 1
                }
609
                break;
610 1
            case self::T_LITERAL:
611 1
                $this->compileLiteral($node, $attributes, $special, $out);
612 1
                break;
613
            case '':
614
                if ($this->isComponent($node->tagName)) {
615 1
                    $this->compileComponentInclude($node, $attributes, $special, $out);
616 1
                } else {
617 1
                    $this->compileElement($node, $attributes, $special, $out);
618 1
                }
619 1
                break;
620 1
            case self::T_TAG:
621
            default:
622
                // This is only a tag node so it just gets compiled as an element.
623 1
                $this->compileBasicElement($node, $attributes, $special, $out);
624
                break;
625
        }
626
    }
627
628
    /**
629
     * Compile component registering.
630
     *
631
     * @param DOMElement $node
632
     * @param $attributes
633
     * @param $special
634 9
     * @param CompilerBuffer $out
635
     */
636 9
    public function compileComponentRegister(DOMElement $node, $attributes, $special, CompilerBuffer $out) {
637 9
        $name = strtolower($special[self::T_COMPONENT]->value);
638
        unset($special[self::T_COMPONENT]);
639 5
640 4
        $prev = $out->select($name);
641
642 1
        $varName = var_export($name, true);
643
        $out->appendCode("\$this->defineComponent($varName, function (\$props = [], \$children = []) {\n");
644
        $out->pushScope(['this' => 'props']);
645 5
        $out->indent(+1);
646
647 9
        try {
648
            $this->compileSpecialNode($node, $attributes, $special, $out);
649 9
        } finally {
650 2
            $out->popScope();
651 2
            $out->indent(-1);
652
            $out->appendCode("});");
653 2
            $out->select($prev);
654
        }
655
    }
656 3
657
    private function compileBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
658
        // Blocks must be direct descendants of component includes.
659
        if (!$out->getNodeProp($node->parentNode, self::T_INCLUDE)) {
660 9
            throw $out->createCompilerException($node, new \Exception("Blocks must be direct descendants of component includes."));
661 9
        }
662
663 9
        $name = strtolower($special[self::T_BLOCK]->value);
664 1
        if (empty($name)) {
665
            throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("Block names cannot be empty."));
666 8
        }
667
        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 9
        }
670 9
671
        unset($special[self::T_BLOCK]);
672
673
        $prev = $out->select($name, true);
674
675
        $vars = array_filter(array_unique($out->getScopeVariables()));
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
676 9
        $vars[] = 'children';
677 9
        $use = '$'.implode(', $', array_unique($vars));
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
678 9
679 9
        $out->appendCode("function () use ($use) {\n");
680 9
        $out->pushScope(['this' => 'props']);
681 9
        $out->indent(+1);
682
683
        try {
684 9
            $this->compileSpecialNode($node, $attributes, $special, $out);
685 7
        } finally {
686
            $out->indent(-1);
687
            $out->popScope();
688 2
            $out->appendCode("}");
689
            $out->select($prev);
690 2
        }
691 2
692
        return $out;
693
    }
694 2
695 2
    /**
696
     * Compile component inclusion and rendering.
697 2
     *
698 2
     * @param DOMElement $node
699 2
     * @param DOMAttr[] $attributes
700
     * @param DOMAttr[] $special
701
     * @param CompilerBuffer $out
702 2
     */
703
    protected function compileComponentInclude(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
704
        // Mark the node as a component include.
705 22
        $out->setNodeProp($node, self::T_INCLUDE, true);
706
707 22
        // Generate the attributes into a property array.
708
        $props = [];
709
        foreach ($attributes as $name => $attribute) {
710
            /* @var DOMAttr $attr */
711 22
            if ($this->isExpression($attribute->value)) {
712 22
                $expr = $this->expr(substr($attribute->value, 1, -1), $out, $attribute);
713
            } else {
714 22
                $expr = var_export($attribute->value, true);
715
            }
716 22
717 22
            $props[] = var_export($name, true).' => '.$expr;
718 22
        }
719 22
        $propsStr = '['.implode(', ', $props).']';
720
721 22
        if (isset($special[self::T_WITH])) {
722
            $withExpr = $this->expr($special[self::T_WITH]->value, $out, $special[self::T_WITH]);
723 35
            unset($special[self::T_WITH]);
724 35
725 3
            $propsStr = empty($props) ? $withExpr : $propsStr.' + (array)'.$withExpr;
726
        } elseif (empty($props)) {
727
            // By default the current context is passed to components.
728 34
            $propsStr = $this->expr('this', $out);
729
        }
730 34
731
        // Compile the children blocks.
732 8
        $blocks = $this->compileComponentBlocks($node, $out);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
733
        $blocksStr = $blocks->flush();
734
735 8
        if (isset($special[self::T_INCLUDE])) {
736 6
            $name = $this->expr($special[self::T_INCLUDE]->value, $out, $special[self::T_INCLUDE]);
737
        } else {
738 2
            $name = var_export($node->tagName, true);
739
        }
740
741 8
        $out->appendCode("\$this->write($name, $propsStr, $blocksStr);\n");
742
    }
743
744 34
    /**
745 34
     * @param DOMElement $parent
746
     * @return CompilerBuffer
747 1
     */
748
    protected function compileComponentBlocks(DOMElement $parent, CompilerBuffer $out) {
749 34
        $blocksOut = new CompilerBuffer(CompilerBuffer::STYLE_ARRAY, [
750
            'baseIndent' => $out->getIndent(),
751 11
            'indent' => $out->getIndent() + 1,
752 11
            'depth' => $out->getDepth(),
753
            'scopes' => $out->getAllScopes(),
754
            'nodeProps' => $out->getNodePropArray()
755 35
        ]);
756 35
        $blocksOut->setSource($out->getSource());
757 34
758
        if ($this->isEmptyNode($parent)) {
759 35
            return $blocksOut;
760
        }
761 3
762 3
        $use = '$'.implode(', $', $blocksOut->getScopeVariables()).', $children';
763
764
        $blocksOut->appendCode("function () use ($use) {\n");
765 9
        $blocksOut->indent(+1);
766 9
767 7
        try {
768
            foreach ($parent->childNodes as $node) {
769
                $this->compileNode($node, $blocksOut);
770 2
            }
771 2
        } finally {
772 1
            $blocksOut->indent(-1);
773
            $blocksOut->appendCode("}");
774 1
        }
775 1
776
        return $blocksOut;
777
    }
778
779
    protected function compileTagComment(DOMElement $node, $attributes, $special, CompilerBuffer $out) {
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
780
        // Don't double up comments.
781
        if ($node->previousSibling && $node->previousSibling->nodeType === XML_COMMENT_NODE) {
782 7
            return;
783 7
        }
784 7
785 7
        $str = '<'.$node->tagName;
786
        foreach ($special as $attr) {
787 7
            /* @var DOMAttr $attr */
788 7
            $str .= ' '.$attr->name.(empty($attr->value) ? '' : '="'.htmlspecialchars($attr->value).'"');
789
        }
790 7
        $str .= '>';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
791 7
        $comments = explode("\n", $str);
792
        foreach ($comments as $comment) {
793 7
            $out->appendCode("// $comment\n");
794
        }
795 7
    }
796
797 7
    /**
798 2
     * @param DOMElement $node
799 2
     * @param DOMAttr[] $attributes
800
     * @param DOMAttr[] $special
801 2
     * @param CompilerBuffer $out
802
     * @param bool $force
803 2
     */
804 2
    protected function compileOpenTag(DOMElement $node, $attributes, $special, CompilerBuffer $out, $force = false) {
805 2
        $tagNameExpr = !empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : '';
806
807
        if ($node->tagName === self::T_X && empty($tagNameExpr)) {
808 7
            return;
809 7
        }
810
811 12
        if (!empty($tagNameExpr)) {
812 12
            $tagNameExpr = $this->expr($tagNameExpr, $out, $special[self::T_TAG]);
813 12
            $out->echoLiteral('<');
814
            $out->echoCode($tagNameExpr);
815 12
        } else {
816 12
            $out->echoLiteral('<'.$node->tagName);
817
        }
818 12
819 10
        /* @var DOMAttr $attribute */
820
        foreach ($attributes as $name => $attribute) {
821 2
            // Check for an attribute expression.
822
            if ($this->isExpression($attribute->value)) {
823 2
                $out->echoCode(
824 2
                    '$this->attribute('.var_export($name, true).', '.
825
                    $this->expr(substr($attribute->value, 1, -1), $out, $attribute).
826 2
                    ')');
827
            } elseif (null !== $fn = $this->getAttributeFunction($attribute)) {
828 2
                $value  = call_user_func($fn, var_export($attribute->value, true));
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
829 2
830 2
                $out->echoCode('$this->attribute('.var_export($name, true).', '.$value.')');
831
            } elseif ((empty($attribute->value) || $attribute->value === $name) && isset(self::$boolAttributes[$name])) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $attribute->value (string) and $name (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
832 2
                $out->echoLiteral(' '.$name);
833
            } else {
834 2
                $out->echoLiteral(' '.$name.'="');
835 2
                $out->echoLiteral(htmlspecialchars($attribute->value));
836 2
                $out->echoLiteral('"');
837
            }
838 2
        }
839
840
        if ($node->hasChildNodes() || $force) {
841 12
            $out->echoLiteral('>');
842 12
        } else {
843
            $out->echoLiteral(" />");
844 1
        }
845 1
    }
846 1
847
    private function isExpression($value) {
848 1
        return preg_match('`^{\S.*}$`', $value);
849 1
    }
850 1
851
    protected function compileCloseTag(DOMElement $node, $special, CompilerBuffer $out, $force = false) {
852 1
        if (!$force && !$node->hasChildNodes()) {
853
            return;
854 1
        }
855
856 1
        $tagNameExpr = !empty($special[self::T_TAG]) ? $special[self::T_TAG]->value : '';
857 1
        if (!empty($tagNameExpr)) {
858
            $tagNameExpr = $this->expr($tagNameExpr, $out, $special[self::T_TAG]);
859 1
            $out->echoLiteral('</');
860
            $out->echoCode($tagNameExpr);
861 1
            $out->echoLiteral('>');
862 1
        } elseif ($node->tagName !== self::T_X) {
863 1
            $out->echoLiteral("</{$node->tagName}>");
864
        }
865 2
    }
866 2
867 2
    protected function isEmptyText(DOMNode $node) {
868
        return $node instanceof \DOMText && empty(trim($node->data));
869 2
    }
870
871 2
    protected function isEmptyNode(DOMNode $node) {
872 2
        if (!$node->hasChildNodes()) {
873 2
            return true;
874
        }
875
876 2
        foreach ($node->childNodes as $childNode) {
877 2
            if ($childNode instanceof DOMElement) {
878
                return false;
879 18
            }
880 18
            if ($childNode instanceof \DOMText && !$this->isEmptyText($childNode)) {
881
                return false;
882 18
            }
883 18
        }
884
885
        return true;
886 18
    }
887 18
888
    protected function compileIf(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
889
        $this->compileTagComment($node, $attributes, $special, $out);
890
        $expr = $this->expr($special[self::T_IF]->value, $out, $special[self::T_IF]);
891
        unset($special[self::T_IF]);
892
893
        $elseNode = $this->findSpecialNode($node, self::T_ELSE, self::T_IF);
894
        $out->setNodeProp($elseNode, 'skip', true);
895
896
        $out->appendCode('if ('.$expr.") {\n");
897
        $out->indent(+1);
898
899 19
        $this->compileSpecialNode($node, $attributes, $special, $out);
900
901 19
        $out->indent(-1);
902 2
903 2
        if ($elseNode) {
904
            list($attributes, $special) = $this->splitAttributes($elseNode);
905
            unset($special[self::T_ELSE]);
906
907 2
            $out->appendCode("} else {\n");
908
909
            $out->indent(+1);
910
            $this->compileSpecialNode($elseNode, $attributes, $special, $out);
911
            $out->indent(-1);
912
        }
913 17
914 17
        $out->appendCode("}\n");
915 17
    }
916 2
917
    protected function compileEach(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
918
        $this->compileTagComment($node, $attributes, $special, $out);
919 17
        $this->compileOpenTag($node, $attributes, $special, $out);
920 12
921 7
        $emptyNode = $this->findSpecialNode($node, self::T_EMPTY, self::T_ELSE);
922 17
        $out->setNodeProp($emptyNode, 'skip', true);
923
924
        if ($emptyNode === null) {
925
            $this->compileEachLoop($node, $attributes, $special, $out);
926 15
        } else {
927
            $expr = $this->expr("empty({$special[self::T_EACH]->value})", $out);
928
929
            list ($emptyAttributes, $emptySpecial) = $this->splitAttributes($emptyNode);
930
            unset($emptySpecial[self::T_EMPTY]);
931
932
            $out->appendCode('if ('.$expr.") {\n");
933
934
            $out->indent(+1);
935 12
            $this->compileSpecialNode($emptyNode, $emptyAttributes, $emptySpecial, $out);
936 12
            $out->indent(-1);
937 12
938
            $out->appendCode("} else {\n");
939 12
940 12
            $out->indent(+1);
941 12
            $this->compileEachLoop($node, $attributes, $special, $out);
942 6
            $out->indent(-1);
943 6
944 6
            $out->appendCode("}\n");
945 3
        }
946
947
        $this->compileCloseTag($node, $special, $out);
948
    }
949 12
950 12
    protected function compileWith(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
951 9
        $this->compileTagComment($node, $attributes, $special, $out);
952
953 3
        $out->depth(+1);
954
        $scope = ['this' => $out->depthName('props')];
955 12
        if (!empty($special[self::T_AS])) {
956 12
            if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) {
957 12
                // The template specified an x-as attribute to alias the with expression.
958
                $scope = [$m[1] => $out->depthName('props')];
959 12
            } else {
960 12
                throw $out->createCompilerException(
961
                    $special[self::T_AS],
962
                    new \Exception("Invalid identifier in x-as attribute.")
963 12
                );
964 12
            }
965 12
        }
966 12
        $with = $this->expr($special[self::T_WITH]->value, $out);
967 12
968
        unset($special[self::T_WITH], $special[self::T_AS]);
969 36
970 36
        $out->pushScope($scope);
971
        $out->appendCode('$'.$out->depthName('props')." = $with;\n");
972
973
        $this->compileSpecialNode($node, $attributes, $special, $out);
974 36
975 36
        $out->depth(-1);
976 6
        $out->popScope();
977
    }
978
979 35
    protected function compileLiteral(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
980 35
        $this->compileTagComment($node, $attributes, $special, $out);
981 35
        unset($special[self::T_LITERAL]);
982
983
        $this->compileOpenTag($node, $attributes, $special, $out);
984
985
        foreach ($node->childNodes as $childNode) {
986
            $html = $childNode->ownerDocument->saveHTML($childNode);
987 35
            $out->echoLiteral($html);
988
        }
989
990 36
        $this->compileCloseTag($node, $special, $out);
991 36
    }
992
993
    protected function compileElement(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
994
        $this->compileOpenTag($node, $attributes, $special, $out);
995 36
996
        foreach ($node->childNodes as $childNode) {
997 36
            $this->compileNode($childNode, $out);
998 5
        }
999
1000
        $this->compileCloseTag($node, $special, $out);
1001 35
    }
1002 35
1003 35
    /**
1004
     * Find a special node in relation to another node.
1005
     *
1006
     * This method is used to find things such as x-empty and x-else elements.
1007
     *
1008
     * @param DOMElement $node The node to search in relation to.
1009 35
     * @param string $attribute The name of the attribute to search for.
1010
     * @param string $parentAttribute The name of the parent attribute to resolve conflicts.
1011
     * @return DOMElement|null Returns the found element node or **null** if not found.
1012 36
     */
1013 36
    protected function findSpecialNode(DOMElement $node, $attribute, $parentAttribute) {
1014 36
        // First look for a sibling after the node.
1015
        for ($sibNode = $node->nextSibling; $sibNode !== null; $sibNode = $sibNode->nextSibling) {
1016
            if ($sibNode instanceof DOMElement && $sibNode->hasAttribute($attribute)) {
1017
                return $sibNode;
1018 36
            }
1019
1020
            // Stop searching if we encounter another node.
1021 2
            if (!$this->isEmptyText($sibNode)) {
1022
                break;
1023 2
            }
1024 2
        }
1025
1026 2
        // Next look inside the node.
1027 2
        $parentFound = false;
1028
        foreach ($node->childNodes as $childNode) {
1029 2
            if (!$parentFound && $childNode instanceof DOMElement && $childNode->hasAttribute($attribute)) {
1030
                return $childNode;
1031 2
            }
1032 2
1033 2
            if ($childNode instanceof DOMElement) {
1034 2
                $parentFound = $childNode->hasAttribute($parentAttribute);
1035 2
            } elseif ($childNode instanceof \DOMText && !empty(trim($childNode->data))) {
1036
                $parentFound = false;
1037 2
            }
1038 2
        }
1039
1040
        return null;
1041
    }
1042
1043
    /**
1044
     * @param DOMElement $node
1045
     * @param array $attributes
1046
     * @param array $special
1047
     * @param CompilerBuffer $out
1048
     */
1049
    private function compileEachLoop(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1050
        $each = $this->expr($special[self::T_EACH]->value, $out);
1051
        unset($special[self::T_EACH]);
1052
1053
        $as = ['', $out->depthName('props', 1)];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
1054
        $scope = ['this' => $as[1]];
1055
        if (!empty($special[self::T_AS])) {
1056
            if (preg_match('`^(?:([a-z0-9]+)\s+)?([a-z0-9]+)$`i', $special[self::T_AS]->value, $m)) {
1057
                $scope = [$m[2] => $as[1]];
1058
                if (!empty($m[1])) {
1059
                    $scope[$m[1]] = $as[0] = $out->depthName('i', 1);
1060
                }
1061
            } else {
1062
                throw $out->createCompilerException(
1063
                    $special[self::T_AS],
1064
                    new \Exception("Invalid identifier in x-as attribute.")
1065
                );
1066
            }
1067
        }
1068
        unset($special[self::T_AS]);
1069
        if (empty($as[0])) {
1070
            $out->appendCode("foreach ($each as \${$as[1]}) {\n");
1071
        } else {
1072
            $out->appendCode("foreach ($each as \${$as[0]} => \${$as[1]}) {\n");
1073
        }
1074
        $out->depth(+1);
1075
        $out->indent(+1);
1076
        $out->pushScope($scope);
1077
1078
        foreach ($node->childNodes as $childNode) {
1079
            $this->compileNode($childNode, $out);
1080
        }
1081
1082
        $out->indent(-1);
1083
        $out->depth(-1);
1084
        $out->popScope();
1085
        $out->appendCode("}\n");
1086
    }
1087
1088
    protected function ltrim($text, \DOMNode $node, CompilerBuffer $out) {
0 ignored issues
show
Documentation introduced by
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...
1089
        if ($this->inPre($node)) {
1090
            return $text;
1091
        }
1092
1093
1094
        for ($sib = $node->previousSibling; $sib !== null && $this->canSkip($sib, $out); $sib = $sib->previousSibling) {
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
1095
            //
1096
        }
1097
        if ($sib === null) {
1098
            return ltrim($text);
1099
        }
1100
//        if ($this->canSkip($sib, $out)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
61% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1101
//            return ltrim($text);
1102
//        }
1103
1104
        $text = preg_replace('`^\s*\n\s*`', "\n", $text, -1, $count);
1105
        if ($count === 0) {
1106
            $text = preg_replace('`^\s+`', ' ', $text);
1107
        }
1108
1109
//        if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1110
//            return ltrim($text);
1111
//        }
1112
        return $text;
1113
    }
1114
1115
    /**
1116
     * Whether or not a node can be skipped for the purposes of trimming whitespace.
1117
     *
1118
     * @param DOMNode|null $node The node to test.
1119
     * @param CompilerBuffer|null $out The compiler information.
1120
     * @return bool Returns **true** if whitespace can be trimmed right up to the node or **false** otherwise.
1121
     */
1122
    private function canSkip(\DOMNode $node, CompilerBuffer $out) {
1123
        if ($out->getNodeProp($node, 'skip')) {
1124
            return true;
1125
        }
1126
1127
        switch ($node->nodeType) {
1128
            case XML_TEXT_NODE:
1129
                return false;
1130
            case XML_COMMENT_NODE:
1131
                return true;
1132
            case XML_ELEMENT_NODE:
1133
                /* @var \DOMElement $node */
1134
                if ($node->tagName === self::T_X
1135
                    || ($node->tagName === 'script' && $node->hasAttribute(self::T_AS)) // expression assignment
1136
                    || ($node->hasAttribute(self::T_WITH) && $node->hasAttribute(self::T_AS)) // with assignment
1137
                    || ($node->hasAttribute(self::T_BLOCK) || $node->hasAttribute(self::T_COMPONENT))
1138
                ) {
1139
                    return true;
1140
                }
1141
        }
1142
1143
        return false;
1144
    }
1145
1146
    protected function rtrim($text, \DOMNode $node, CompilerBuffer $out) {
0 ignored issues
show
Documentation introduced by
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...
1147
        if ($this->inPre($node)) {
1148
            return $text;
1149
        }
1150
1151
        for ($sib = $node->nextSibling; $sib !== null && $this->canSkip($sib, $out); $sib = $sib->nextSibling) {
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
1152
            //
1153
        }
1154
        if ($sib === null) {
1155
            return rtrim($text);
1156
        }
1157
1158
        $text = preg_replace('`\s*\n\s*$`', "\n", $text, -1, $count);
1159
        if ($count === 0) {
1160
            $text = preg_replace('`\s+$`', ' ', $text);
1161
        }
1162
1163
//        if ($sib !== null && ($sib->nodeType === XML_COMMENT_NODE || in_array($sib->tagName, static::$blocks))) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1164
//            return rtrim($text);
1165
//        }
1166
        return $text;
1167
    }
1168
1169
    protected function inPre(\DOMNode $node) {
1170
        for ($node = $node->parentNode; $node !== null; $node = $node->parentNode) {
1171
            if (in_array($node->nodeType, ['code', 'pre'], true)) {
1172
                return true;
1173
            }
1174
        }
1175
        return false;
1176
    }
1177
1178
    private function compileChildBlock(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
1179
        /* @var DOMAttr $child */
1180
        $child = $special[self::T_CHILDREN];
1181
        unset($special[self::T_CHILDREN]);
1182
1183
        $name = $child->value === '' ? 0 : strtolower($child->value);
1184
        if ($name !== 0) {
1185
            if (empty($name)) {
1186
                throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("Block names cannot be empty."));
1187
            }
1188
            if (!preg_match(self::IDENT_REGEX, $name)) {
1189
                throw $out->createCompilerException($special[self::T_BLOCK], new \Exception("The block name isn't a valid identifier."));
1190
            }
1191
        }
1192
1193
        $keyStr = var_export($name, true);
1194
1195
        $this->compileOpenTag($node, $attributes, $special, $out, true);
1196
1197
        $out->appendCode("\$this->writeChildren(\$children[{$keyStr}]);\n");
1198
1199
        $this->compileCloseTag($node, $special, $out, true);
1200
    }
1201
1202
    /**
1203
     * Compile an `<script type="ebi">` node.
1204
     *
1205
     * @param DOMElement $node The node to compile.
1206
     * @param DOMAttr[] $attributes The node's attributes.
1207
     * @param DOMAttr[] $special An array of special attributes.
1208
     * @param CompilerBuffer $out The compiler output.
1209
     */
1210
    private function compileExpressionNode(DOMElement $node, array $attributes, array $special, CompilerBuffer $out) {
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1211
        $str = $node->nodeValue;
1212
1213
        try {
1214
            $expr = $this->expr($str, $out);
1215
        } catch (SyntaxError $ex) {
1216
            $context = [];
1217
            if (preg_match('`^(.*) around position (\d*)\.$`', $ex->getMessage(), $m)) {
1218
                $add = substr_count($str, "\n", 0, $m[2]);
1219
1220
                $context['line'] = $node->getLineNo() + $add;
1221
            }
1222
1223
            throw $out->createCompilerException($node, $ex, $context);
1224
        }
1225
1226
        if (isset($special[self::T_AS])) {
1227
            if (null !== $this->closest($node, function (\DOMNode $n) use ($out) {
1228
                return $out->getNodeProp($n, self::T_INCLUDE);
1229
            })) {
1230
                throw $out->createCompilerException(
1231
                    $node,
1232
                    new \Exception("Expressions with x-as assignments cannot be declared inside child blocks.")
1233
                );
1234
            }
1235
1236
            if (preg_match(self::IDENT_REGEX, $special[self::T_AS]->value, $m)) {
1237
                // The template specified an x-as attribute to alias the with expression.
1238
                $out->depth(+1);
1239
                $scope = [$m[1] => $out->depthName('expr')];
1240
                $out->pushScope($scope);
1241
                $out->appendCode('$'.$out->depthName('expr')." = $expr;\n");
1242
            } else {
1243
                throw $out->createCompilerException(
1244
                    $special[self::T_AS],
1245
                    new \Exception("Invalid identifier in x-as attribute.")
1246
                );
1247
            }
1248
        } elseif (!empty($special[self::T_UNESCAPE])) {
1249
            $out->echoCode($expr);
1250
        } else {
1251
            $out->echoCode($this->compileEscape($expr));
1252
        }
1253
    }
1254
1255
    /**
1256
     *
1257
     *
1258
     * @param DOMNode $node
1259
     * @param callable $test
1260
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be DOMNode?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1261
     */
1262
    private function closest(\DOMNode $node, callable $test) {
1263
        for ($visitor = $node; $visitor !== null && !$test($visitor); $visitor = $visitor->parentNode) {
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
1264
            //
1265
        }
1266
        return $visitor;
1267
    }
1268
1269
    /**
1270
     * @param DOMElement $node
1271
     * @param $attributes
1272
     * @param $special
1273
     * @param CompilerBuffer $out
1274
     */
1275
    protected function compileBasicElement(DOMElement $node, $attributes, $special, CompilerBuffer $out) {
1276
        $this->compileOpenTag($node, $attributes, $special, $out);
1277
1278
        foreach ($node->childNodes as $childNode) {
1279
            $this->compileNode($childNode, $out);
1280
        }
1281
1282
        $this->compileCloseTag($node, $special, $out);
1283
    }
1284
1285
    protected function compileEscape($php) {
1286
//        return 'htmlspecialchars('.$php.')';
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1287
        return '$this->escape('.$php.')';
1288
    }
1289
}
1290