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 |
|
|
|
|
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 |
|
|
|
|
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, |
|
|
|
|
283
|
|
|
// isset($separators[$i+1]) ? $separators[$i+1][1] : strlen($input) |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.