Completed
Push — master ( 9ad54b...895d73 )
by Federico
03:21
created

Compiler::createCode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 7
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 12
rs 9.4285
1
<?php
2
3
namespace Jade;
4
5
use Jade\Compiler\CodeHandler;
6
use Jade\Compiler\MixinVisitor;
7
use Jade\Parser\Exception as ParserException;
8
9
/**
10
 * Class Jade Compiler.
11
 */
12
class Compiler extends MixinVisitor
13
{
14
    /**
15
     * Constants and configuration in Compiler/CompilerConfig.php.
16
     */
17
18
    /**
19
     * @var
20
     */
21
    protected $xml;
22
23
    /**
24
     * @var
25
     */
26
    protected $parentIndents;
27
28
    /**
29
     * @var array
30
     */
31
    protected $buffer = array();
32
    /**
33
     * @var array
34
     */
35
    protected $options = array();
36
    /**
37
     * @var array
38
     */
39
    protected $filters = array();
40
41
    /**
42
     * @var bool
43
     */
44
    protected $phpSingleLine = false;
45
    /**
46
     * @var bool
47
     */
48
    protected $allowMixinOverride = false;
49
    /**
50
     * @var bool
51
     */
52
    protected $keepNullAttributes = false;
53
    /**
54
     * @var bool
55
     */
56
    protected $filterAutoLoad = true;
57
    /**
58
     * @var bool
59
     */
60
    protected $terse = true;
61
    /**
62
     * @var bool
63
     */
64
    protected $restrictedScope = false;
65
    /**
66
     * @var array
67
     */
68
    protected $customKeywords = array();
69
    /**
70
     * @var Jade
71
     */
72
    protected $jade = null;
73
74
    /**
75
     * @var string
76
     */
77
    protected $quote;
78
79
    /**
80
     * @var string
81
     */
82
    protected $filename;
83
84
    /**
85
     * @param array/Jade $options
0 ignored issues
show
Documentation introduced by
The doc-type array/Jade could not be parsed: Unknown type name "array/Jade" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
86
     * @param array      $filters
87
     */
88
    public function __construct($options = array(), array $filters = array(), $filename = null)
89
    {
90
        $this->options = $this->setOptions($options);
91
        $this->filters = $filters;
92
        $this->filename = $filename;
93
    }
94
95
    /**
96
     * Get a jade engine reference or an options array and return needed options.
97
     *
98
     * @param array/Jade $options
0 ignored issues
show
Documentation introduced by
The doc-type array/Jade could not be parsed: Unknown type name "array/Jade" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
99
     *
100
     * @return array
101
     */
102
    protected function setOptions($options)
103
    {
104
        $optionTypes = array(
105
            'prettyprint' => 'boolean',
106
            'phpSingleLine' => 'boolean',
107
            'allowMixinOverride' => 'boolean',
108
            'keepNullAttributes' => 'boolean',
109
            'filterAutoLoad' => 'boolean',
110
            'restrictedScope' => 'boolean',
111
            'indentSize' => 'integer',
112
            'indentChar' => 'string',
113
            'customKeywords' => 'array',
114
        );
115
116
        if ($options instanceof Jade) {
117
            $this->jade = $options;
118
            $options = array();
119
120
            foreach ($optionTypes as $option => $type) {
121
                $this->$option = $this->jade->getOption($option);
122
                $options[$option] = $this->$option;
123
                settype($this->$option, $type);
124
            }
125
126
            $this->quote = $this->jade->getOption('singleQuote') ? '\'' : '"';
127
128
            return $options;
129
        }
130
131
        foreach (array_intersect_key($optionTypes, $options) as $option => $type) {
132
            $this->$option = $options[$option];
133
            settype($this->$option, $type);
134
        }
135
136
        $this->quote = isset($options['singleQuote']) && $options['singleQuote'] ? '\'' : '"';
137
138
        return $options;
139
    }
140
141
    /**
142
     * Get an option from the jade engine if set or from the options array else.
143
     *
144
     * @param string $option
145
     *
146
     * @throws \InvalidArgumentException
147
     *
148
     * @return mixed
149
     */
150
    public function getOption($option)
151
    {
152
        if (is_null($this->jade)) {
153
            if (!isset($this->options[$option])) {
154
                throw new \InvalidArgumentException("$option is not a valid option name.", 28);
155
            }
156
157
            return $this->options[$option];
158
        }
159
160
        return $this->jade->getOption($option);
161
    }
162
163
    /**
164
     * Get a compiler with the same settings.
165
     *
166
     * @return Compiler
167
     */
168
    public function subCompiler()
169
    {
170
        return new static($this->options, $this->filters);
171
    }
172
173
    /**
174
     * php closing tag depanding on the pretty print setting.
175
     *
176
     * @return string
177
     */
178
    protected function closingTag()
179
    {
180
        return '?>' . ($this->prettyprint ? ' ' : '');
181
    }
182
183
    /**
184
     * @param $node
185
     *
186
     * @return string
187
     */
188
    public function compile($node)
189
    {
190
        $this->visit($node);
191
192
        $code = ltrim(implode('', $this->buffer));
193
194
        // Separate in several lines to get a useable line number in case of an error occurs
195
        if ($this->phpSingleLine) {
196
            $code = str_replace(array('<?php', '?>'), array("<?php\n", "\n" . $this->closingTag()), $code);
197
        }
198
        // Remove the $ wich are not needed
199
        return $code;
200
    }
201
202
    /**
203
     * @param $method
204
     * @param $arguments
205
     *
206
     * @throws \BadMethodCallException If the 'apply' rely on non existing method
207
     *
208
     * @return mixed
209
     */
210
    protected function apply($method, $arguments)
211
    {
212
        if (!method_exists($this, $method)) {
213
            throw new \BadMethodCallException(sprintf('Method %s do not exists', $method), 7);
214
        }
215
216
        return call_user_func_array(array($this, $method), $arguments);
217
    }
218
219
    /**
220
     * @param      $line
221
     * @param null $indent
222
     */
223
    protected function buffer($line, $indent = null)
224
    {
225
        if ($indent === true || ($indent === null && $this->prettyprint)) {
226
            $line = $this->indent() . $line . $this->newline();
227
        }
228
229
        $this->buffer[] = $line;
230
    }
231
232
    /**
233
     * @param string $str
234
     *
235
     * @return bool|int
236
     */
237
    protected function isConstant($str)
238
    {
239
        return preg_match('/^' . static::CONSTANT_VALUE . '$/', trim($str));
240
    }
241
242
    /**
243
     * @param        $input
244
     * @param string $name
245
     *
246
     * @throws \ErrorException
247
     *
248
     * @return array
249
     */
250
    public function handleCode($input, $name = '')
251
    {
252
        $handler = new CodeHandler($input, $name);
253
254
        return $handler->parse();
255
    }
256
257
    /**
258
     * @param $input
259
     *
260
     * @throws \ErrorException
261
     *
262
     * @return array
263
     */
264
    public function handleString($input)
265
    {
266
        $result = array();
267
        $resultsString = array();
268
269
        $separators = preg_split(
270
            '/[+](?!\\()/', // concatenation operator - only js
271
            $input,
272
            -1,
273
            PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE
274
        );
275
276
        foreach ($separators as $part) {
277
            // $sep[0] - the separator string due to PREG_SPLIT_OFFSET_CAPTURE flag
278
            // $sep[1] - the offset due to PREG_SPLIT_OFFSET_CAPTURE
279
            // @todo: = find original usage of this
280
            //$sep = substr(
281
            //    $input,
282
            //    strlen($part[0]) + $part[1] + 1,
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
283
            //    isset($separators[$i+1]) ? $separators[$i+1][1] : strlen($input)
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
284
            //);
285
286
            // @todo: handleCode() in concat
287
            $part[0] = trim($part[0]);
288
289
            if (preg_match('/^("(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')(.*)$/', $part[0], $match)) {
290
                $quote = substr($match[1], 0, 1);
0 ignored issues
show
Unused Code introduced by
$quote 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...
291
292
                if (strlen(trim($match[2]))) {
293
                    throw new \ErrorException('Unexpected value: ' . $match[2], 8);
294
                }
295
296
                array_push($resultsString, $match[1]);
297
298
                continue;
299
            }
300
301
            $code = $this->handleCode($part[0]);
302
303
            $result = array_merge($result, array_slice($code, 0, -1));
304
            array_push($resultsString, array_pop($code));
305
        }
306
307
        array_push($result, implode(' . ', $resultsString));
308
309
        return $result;
310
    }
311
312
    /**
313
     * @param string $text
314
     *
315
     * @return mixed
316
     */
317
    public function interpolate($text)
318
    {
319
        return preg_replace_callback('/(\\\\)?([#!]){(.*?)}/', array($this, 'interpolateFromCapture'), $text);
320
    }
321
322
    /**
323
     * @param array $match
324
     *
325
     * @return string
326
     */
327
    protected function interpolateFromCapture($match)
328
    {
329
        if ($match[1] === '') {
330
            return $this->escapeIfNeeded($match[2] === '!', $match[3]);
331
        }
332
333
        return substr($match[0], 1);
334
    }
335
336
    /**
337
     * @throws \InvalidArgumentException
338
     *
339
     * @return array
340
     */
341
    protected function createStatements()
342
    {
343
        if (func_num_args() === 0) {
344
            throw new \InvalidArgumentException('No Arguments provided', 9);
345
        }
346
347
        $arguments = func_get_args();
348
        $statements = array();
349
        $variables = array();
350
351
        foreach ($arguments as $arg) {
352
            $arg = static::convertVarPath($arg);
353
354
            // add dollar if missing
355
            if (preg_match('/^' . static::VARNAME . '(\s*,.+)?$/', $arg)) {
356
                $arg = static::addDollarIfNeeded($arg);
357
            }
358
359
            // shortcut for constants
360
            if ($this->isConstant($arg)) {
361
                array_push($variables, $arg);
362
                continue;
363
            }
364
365
            // if we have a php variable assume that the string is good php
366
            if (strpos('{[', substr($arg, 0, 1)) === false && preg_match('/&?\${1,2}' . static::VARNAME . '|[A-Za-z0-9_\\\\]+::/', $arg)) {
367
                array_push($variables, $arg);
368
                continue;
369
            }
370
371
            $code = $this->handleArgumentValue($arg);
372
373
            $statements = array_merge($statements, array_slice($code, 0, -1));
374
            array_push($variables, array_pop($code));
375
        }
376
377
        array_push($statements, $variables);
378
379
        return $statements;
380
    }
381
382
    protected function handleArgumentValue($arg)
383
    {
384
        if (preg_match('/^"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'/', $arg)) {
385
            return $this->handleString(trim($arg));
386
        }
387
388
        try {
389
            return $this->handleCode($arg);
390
        } catch (\Exception $e) {
391
            // if a bug occur, try to remove comments
392
            try {
393
                return $this->handleCode(preg_replace('#/\*(.*)\*/#', '', $arg));
394
            } catch (\Exception $e) {
395
                throw new ParserException('Pug.php did not understand ' . $arg, 10, $e);
396
            }
397
        }
398
    }
399
400
    /**
401
     * @param      $code
402
     * @param null $statements
403
     *
404
     * @return string
405
     */
406
    protected function createPhpBlock($code, $statements = null)
407
    {
408
        if ($statements === null) {
409
            return '<?php ' . $code . ' ' . $this->closingTag();
410
        }
411
412
        $codeFormat = array_pop($statements);
413
        array_unshift($codeFormat, $code);
414
415
        if (count($statements) === 0) {
416
            $phpString = call_user_func_array('sprintf', $codeFormat);
417
418
            return '<?php ' . $phpString . ' ' . $this->closingTag();
419
        }
420
421
        $stmtString = '';
422
        foreach ($statements as $stmt) {
423
            $stmtString .= $this->newline() . $this->indent() . $stmt . ';';
424
        }
425
426
        $stmtString .= $this->newline() . $this->indent();
427
        $stmtString .= call_user_func_array('sprintf', $codeFormat);
428
429
        $phpString = '<?php ';
430
        $phpString .= $stmtString;
431
        $phpString .= $this->newline() . $this->indent() . ' ' . $this->closingTag();
432
433
        return $phpString;
434
    }
435
436
    /**
437
     * @param $code
438
     *
439
     * @return string
440
     */
441
    protected function createCode($code)
442
    {
443
        if (func_num_args() > 1) {
444
            $arguments = func_get_args();
445
            array_shift($arguments); // remove $code
446
            $statements = $this->apply('createStatements', $arguments);
447
448
            return $this->createPhpBlock($code, $statements);
449
        }
450
451
        return $this->createPhpBlock($code);
452
    }
453
}
454