1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Jade; |
4
|
|
|
|
5
|
|
|
use Jade\Parser\Exception as ParserException; |
6
|
|
|
use Jade\Parser\ExtensionsHelper; |
7
|
|
|
|
8
|
|
|
class Parser |
9
|
|
|
{ |
10
|
|
|
public static $includeNotFound = ".alert.alert-danger.\n\tPage not found."; |
11
|
|
|
|
12
|
|
|
protected $allowMixedIndent; |
13
|
|
|
protected $basedir; |
14
|
|
|
protected $extending; |
15
|
|
|
protected $extension; |
16
|
|
|
protected $filename; |
17
|
|
|
protected $input; |
18
|
|
|
protected $lexer; |
19
|
|
|
protected $notFound; |
20
|
|
|
protected $options = array(); |
21
|
|
|
protected $preRender; |
22
|
|
|
|
23
|
|
|
protected $blocks = array(); |
24
|
|
|
protected $mixins = array(); |
25
|
|
|
protected $contexts = array(); |
26
|
|
|
|
27
|
|
|
public function __construct($input, $filename = null, array $options = array()) |
28
|
|
|
{ |
29
|
|
|
$defaultOptions = array( |
30
|
|
|
'allowMixedIndent' => true, |
31
|
|
|
'basedir' => null, |
32
|
|
|
'customKeywords' => array(), |
33
|
|
|
'extension' => array('.pug', '.jade'), |
34
|
|
|
'notFound' => null, |
35
|
|
|
'preRender' => null, |
36
|
|
|
); |
37
|
|
|
foreach ($defaultOptions as $key => $default) { |
38
|
|
|
$this->$key = isset($options[$key]) ? $options[$key] : $default; |
39
|
|
|
$this->options[$key] = $this->$key; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
$this->setInput($filename, $input); |
43
|
|
|
|
44
|
|
|
if ($this->input && $this->input[0] === "\xef" && $this->input[1] === "\xbb" && $this->input[2] === "\xbf") { |
45
|
|
|
$this->input = substr($this->input, 3); |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
$this->lexer = new Lexer($this->input, $this->options); |
49
|
|
|
array_push($this->contexts, $this); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
protected function getExtensions() |
53
|
|
|
{ |
54
|
|
|
$extensions = new ExtensionsHelper($this->extension); |
55
|
|
|
|
56
|
|
|
return $extensions->getExtensions(); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
protected function hasValidTemplateExtension($path) |
60
|
|
|
{ |
61
|
|
|
$extensions = new ExtensionsHelper($this->extension); |
62
|
|
|
|
63
|
|
|
return $extensions->hasValidTemplateExtension($path); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
protected function getTemplatePath($path) |
67
|
|
|
{ |
68
|
|
|
$isAbsolutePath = substr($path, 0, 1) === '/'; |
69
|
|
|
if ($isAbsolutePath && !isset($this->options['basedir'])) { |
70
|
|
|
throw new \ErrorException("The 'basedir' option need to be set to use absolute path like $path", 29); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$path = ($isAbsolutePath |
74
|
|
|
? rtrim($this->options['basedir'], '/\\') |
75
|
|
|
: dirname($this->filename) |
76
|
|
|
) . DIRECTORY_SEPARATOR . $path; |
77
|
|
|
$extensions = new ExtensionsHelper($this->extension); |
78
|
|
|
|
79
|
|
|
return $extensions->findValidTemplatePath($path, ''); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
protected function getTemplateContents($path, $value = null) |
83
|
|
|
{ |
84
|
|
|
if ($path !== null) { |
85
|
|
|
$contents = file_get_contents($path); |
86
|
|
|
if (is_callable($this->preRender)) { |
87
|
|
|
$contents = call_user_func($this->preRender, $contents); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
return $contents; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
$notFound = isset($this->options['notFound']) |
94
|
|
|
? $this->options['notFound'] |
95
|
|
|
: static::$includeNotFound; |
96
|
|
|
|
97
|
|
|
if ($notFound !== false) { |
98
|
|
|
return $notFound; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$value = $value ?: $path; |
102
|
|
|
throw new \InvalidArgumentException("The included file '$value' does not exists.", 22); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
protected function setInput($filename, $input) |
106
|
|
|
{ |
107
|
|
|
if ($filename === null && file_exists($input)) { |
108
|
|
|
$filename = $input; |
109
|
|
|
$input = file_get_contents($input); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$this->input = preg_replace('`\r\n|\r`', "\n", $input); |
113
|
|
|
if (is_callable($this->preRender)) { |
114
|
|
|
$this->input = call_user_func($this->preRender, $this->input); |
115
|
|
|
} |
116
|
|
|
$this->filename = $filename; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
public function getFilename() |
120
|
|
|
{ |
121
|
|
|
return $this->filename; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Get a parser with the same settings. |
126
|
|
|
* |
127
|
|
|
* @return Parser |
128
|
|
|
*/ |
129
|
|
|
public function subParser($input) |
130
|
|
|
{ |
131
|
|
|
return new static($input, $this->filename, $this->options); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
public function context($parser = null) |
135
|
|
|
{ |
136
|
|
|
if ($parser === null) { |
137
|
|
|
return array_pop($this->contexts); |
138
|
|
|
} |
139
|
|
|
array_push($this->contexts, $parser); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
public function advance() |
143
|
|
|
{ |
144
|
|
|
return $this->lexer->advance(); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function skip($n) |
148
|
|
|
{ |
149
|
|
|
while ($n--) { |
150
|
|
|
$this->advance(); |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
public function peek() |
155
|
|
|
{ |
156
|
|
|
return $this->lookahead(1); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
public function line() |
160
|
|
|
{ |
161
|
|
|
return $this->lexer->lineno; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
public function lookahead($n = 1) |
165
|
|
|
{ |
166
|
|
|
return $this->lexer->lookahead($n); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
public function parse() |
170
|
|
|
{ |
171
|
|
|
$block = new Nodes\Block(); |
172
|
|
|
$block->line = $this->line(); |
|
|
|
|
173
|
|
|
|
174
|
|
|
while ($this->peekType() !== 'eos') { |
175
|
|
|
if ($this->peekType() === 'newline') { |
176
|
|
|
$this->advance(); |
177
|
|
|
continue; |
178
|
|
|
} |
179
|
|
|
$block->push($this->parseExpression()); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
if ($parser = $this->extending) { |
183
|
|
|
$this->context($parser); |
184
|
|
|
// $parser->blocks = $this->blocks; |
|
|
|
|
185
|
|
|
try { |
186
|
|
|
$ast = $parser->parse(); |
187
|
|
|
} catch (\Exception $e) { |
188
|
|
|
throw new ParserException($parser->getFilename() . ' (' . $block->line . ') : ' . $e->getMessage(), 23, $e); |
189
|
|
|
} |
190
|
|
|
$this->context(); |
191
|
|
|
|
192
|
|
|
foreach ($this->mixins as $name => $v) { |
193
|
|
|
$ast->unshift($this->mixins[$name]); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return $ast; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return $block; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
protected function expect($type) |
203
|
|
|
{ |
204
|
|
|
if ($this->peekType() === $type) { |
205
|
|
|
return $this->lexer->advance(); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$lineNumber = $this->line(); |
209
|
|
|
$lines = explode("\n", $this->input); |
210
|
|
|
$lineString = isset($lines[$lineNumber]) ? $lines[$lineNumber] : ''; |
211
|
|
|
throw new \ErrorException("\n" . sprintf('Expected %s, but got %s in %dth line : %s', $type, $this->peekType(), $lineNumber, $lineString) . "\n", 24); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
protected function accept($type) |
215
|
|
|
{ |
216
|
|
|
if ($this->peekType() === $type) { |
217
|
|
|
return $this->advance(); |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
protected function parseExpression() |
222
|
|
|
{ |
223
|
|
|
$_types = array('tag', 'mixin', 'block', 'case', 'when', 'default', 'extends', 'include', 'doctype', 'filter', 'comment', 'text', 'each', 'customKeyword', 'code', 'call', 'interpolation'); |
224
|
|
|
|
225
|
|
|
if (in_array($this->peekType(), $_types)) { |
226
|
|
|
$_method = 'parse' . ucfirst($this->peekType()); |
227
|
|
|
|
228
|
|
|
return $this->$_method(); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
switch ($this->peekType()) { |
232
|
|
|
case 'yield': |
233
|
|
|
$this->advance(); |
234
|
|
|
$block = new Nodes\Block(); |
235
|
|
|
$block->yield = true; |
|
|
|
|
236
|
|
|
|
237
|
|
|
return $block; |
238
|
|
|
|
239
|
|
|
case 'id': |
240
|
|
|
case 'class': |
241
|
|
|
$token = $this->advance(); |
242
|
|
|
$this->lexer->defer($this->lexer->token('tag', 'div')); |
243
|
|
|
$this->lexer->defer($token); |
|
|
|
|
244
|
|
|
|
245
|
|
|
return $this->parseExpression(); |
246
|
|
|
|
247
|
|
|
default: |
248
|
|
|
throw new \ErrorException($this->filename . ' (' . $this->line() . ') : Unexpected token "' . $this->peekType() . '"', 25); |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
protected function parseText() |
253
|
|
|
{ |
254
|
|
|
$token = $this->expect('text'); |
255
|
|
|
if (preg_match('/^(.*?)#\[([^\]\n]+)\]/', $token->value)) { |
256
|
|
|
$block = new Nodes\Block(); |
257
|
|
|
$this->parseInlineTags($block, $token->value); |
258
|
|
|
|
259
|
|
|
return $block; |
260
|
|
|
} |
261
|
|
|
$node = new Nodes\Text($token->value); |
262
|
|
|
$node->line = $this->line(); |
|
|
|
|
263
|
|
|
|
264
|
|
|
return $node; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
protected function parseBlockExpansion() |
268
|
|
|
{ |
269
|
|
|
if (':' === $this->peekType()) { |
270
|
|
|
$this->advance(); |
271
|
|
|
|
272
|
|
|
return new Nodes\Block($this->parseExpression()); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
return $this->block(); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
View Code Duplication |
protected function parseCase() |
|
|
|
|
279
|
|
|
{ |
280
|
|
|
$value = $this->expect('case')->value; |
281
|
|
|
$node = new Nodes\CaseNode($value); |
282
|
|
|
$node->line = $this->line(); |
|
|
|
|
283
|
|
|
$node->block = $this->block(); |
284
|
|
|
|
285
|
|
|
return $node; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
protected function parseWhen() |
289
|
|
|
{ |
290
|
|
|
$value = $this->expect('when')->value; |
291
|
|
|
|
292
|
|
|
return new Nodes\When($value, $this->parseBlockExpansion()); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
protected function parseDefault() |
296
|
|
|
{ |
297
|
|
|
$this->expect('default'); |
298
|
|
|
|
299
|
|
|
return new Nodes\When('default', $this->parseBlockExpansion()); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
protected function parseCode() |
303
|
|
|
{ |
304
|
|
|
$token = $this->expect('code'); |
305
|
|
|
$buffer = isset($token->buffer) ? $token->buffer : false; |
306
|
|
|
$escape = isset($token->escape) ? $token->escape : true; |
307
|
|
|
$node = new Nodes\Code($token->value, $buffer, $escape); |
308
|
|
|
$node->line = $this->line(); |
|
|
|
|
309
|
|
|
|
310
|
|
|
$i = 1; |
311
|
|
|
while ($this->lookahead($i)->type === 'newline') { |
312
|
|
|
$i++; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
if ($this->lookahead($i)->type === 'indent') { |
316
|
|
|
$this->skip($i - 1); |
317
|
|
|
$node->block = $this->block(); |
|
|
|
|
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return $node; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
protected function parseComment() |
324
|
|
|
{ |
325
|
|
|
$token = $this->expect('comment'); |
326
|
|
|
$node = new Nodes\Comment($token->value, $token->buffer); |
327
|
|
|
$node->line = $this->line(); |
|
|
|
|
328
|
|
|
|
329
|
|
|
return $node; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
protected function parseDoctype() |
333
|
|
|
{ |
334
|
|
|
$token = $this->expect('doctype'); |
335
|
|
|
$node = new Nodes\Doctype($token->value); |
336
|
|
|
$node->line = $this->line(); |
|
|
|
|
337
|
|
|
|
338
|
|
|
return $node; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
protected function parseFilter() |
342
|
|
|
{ |
343
|
|
|
$token = $this->expect('filter'); |
344
|
|
|
$attributes = $this->accept('attributes'); |
345
|
|
|
|
346
|
|
|
$this->lexer->pipeless = true; |
347
|
|
|
$block = $this->parseTextBlock(); |
348
|
|
|
$this->lexer->pipeless = false; |
349
|
|
|
|
350
|
|
|
$node = new Nodes\Filter($token->value, $block, $attributes); |
351
|
|
|
$node->line = $this->line(); |
|
|
|
|
352
|
|
|
|
353
|
|
|
return $node; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
protected function parseEach() |
357
|
|
|
{ |
358
|
|
|
$token = $this->expect('each'); |
359
|
|
|
$node = new Nodes\Each($token->code, $token->value, $token->key); |
360
|
|
|
$node->line = $this->line(); |
|
|
|
|
361
|
|
|
$node->block = $this->block(); |
362
|
|
|
if ($this->peekType() === 'code' && $this->peek()->value === 'else') { |
363
|
|
|
$this->advance(); |
364
|
|
|
$node->alternative = $this->block(); |
|
|
|
|
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
return $node; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
View Code Duplication |
protected function parseCustomKeyword() |
|
|
|
|
371
|
|
|
{ |
372
|
|
|
$token = $this->expect('customKeyword'); |
373
|
|
|
$node = new Nodes\CustomKeyword($token->value, $token->args); |
374
|
|
|
$node->line = $this->line(); |
|
|
|
|
375
|
|
|
if ('indent' === $this->peekType()) { |
376
|
|
|
$node->block = $this->block(); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
return $node; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
protected function parseExtends() |
383
|
|
|
{ |
384
|
|
|
$extendValue = $this->expect('extends')->value; |
385
|
|
|
$path = $this->getTemplatePath($extendValue); |
386
|
|
|
|
387
|
|
|
$string = $this->getTemplateContents($path, $extendValue); |
388
|
|
|
$parser = new static($string, $path, $this->options); |
389
|
|
|
// need to be a reference, or be seted after the parse loop |
390
|
|
|
$parser->blocks = &$this->blocks; |
391
|
|
|
$parser->contexts = $this->contexts; |
392
|
|
|
$this->extending = $parser; |
393
|
|
|
|
394
|
|
|
return new Nodes\Literal(''); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
protected function parseBlock() |
398
|
|
|
{ |
399
|
|
|
$block = $this->expect('block'); |
400
|
|
|
$mode = $block->mode; |
401
|
|
|
$name = trim($block->value); |
402
|
|
|
|
403
|
|
|
$block = 'indent' === $this->peekType() |
404
|
|
|
? $this->block() |
405
|
|
|
: new Nodes\Block(empty($name) |
406
|
|
|
? new Nodes\MixinBlock() |
407
|
|
|
: new Nodes\Literal('') |
408
|
|
|
); |
409
|
|
|
|
410
|
|
|
if (isset($this->blocks[$name])) { |
411
|
|
|
$prev = &$this->blocks[$name]; |
412
|
|
|
|
413
|
|
|
switch ($prev->mode) { |
414
|
|
|
case 'append': |
415
|
|
|
$block->nodes = array_merge($block->nodes, $prev->nodes); |
416
|
|
|
$prev = $block; |
417
|
|
|
break; |
418
|
|
|
|
419
|
|
|
case 'prepend': |
420
|
|
|
$block->nodes = array_merge($prev->nodes, $block->nodes); |
421
|
|
|
$prev = $block; |
422
|
|
|
break; |
423
|
|
|
|
424
|
|
|
case 'replace': |
425
|
|
|
default: |
426
|
|
|
break; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
return $this->blocks[$name]; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
$block->mode = $mode; |
|
|
|
|
433
|
|
|
$this->blocks[$name] = $block; |
434
|
|
|
|
435
|
|
|
return $block; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
protected function parseInclude() |
439
|
|
|
{ |
440
|
|
|
$token = $this->expect('include'); |
441
|
|
|
$includeValue = trim($token->value); |
442
|
|
|
$path = $this->getTemplatePath($includeValue); |
443
|
|
|
|
444
|
|
|
if ($path && !$this->hasValidTemplateExtension($path)) { |
|
|
|
|
445
|
|
|
return new Nodes\Text(file_get_contents($path)); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
$string = $this->getTemplateContents($path, $includeValue); |
449
|
|
|
|
450
|
|
|
$parser = new static($string, $path, $this->options); |
451
|
|
|
$parser->blocks = $this->blocks; |
452
|
|
|
$parser->mixins = $this->mixins; |
453
|
|
|
|
454
|
|
|
$this->context($parser); |
455
|
|
|
try { |
456
|
|
|
$ast = $parser->parse(); |
457
|
|
|
} catch (\Exception $e) { |
458
|
|
|
throw new \ErrorException($path . ' (' . $parser->lexer->lineno . ') : ' . $e->getMessage(), 27); |
459
|
|
|
} |
460
|
|
|
$this->context(); |
461
|
|
|
$ast->filename = $path; |
462
|
|
|
|
463
|
|
|
if ('indent' === $this->peekType() && method_exists($ast, 'includeBlock')) { |
464
|
|
|
$block = $ast->includeBlock(); |
465
|
|
|
if (is_object($block)) { |
466
|
|
|
$handler = count($block->nodes) === 1 && isset($block->nodes[0]->block) |
467
|
|
|
? $block->nodes[0]->block |
468
|
|
|
: $block; |
469
|
|
|
$handler->push($this->block()); |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
return $ast; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
protected function parseCall() |
477
|
|
|
{ |
478
|
|
|
$token = $this->expect('call'); |
479
|
|
|
$name = $token->value; |
480
|
|
|
|
481
|
|
|
$arguments = isset($token->arguments) |
482
|
|
|
? $token->arguments |
483
|
|
|
: null; |
484
|
|
|
|
485
|
|
|
$mixin = new Nodes\Mixin($name, $arguments, new Nodes\Block(), true); |
486
|
|
|
|
487
|
|
|
$this->tag($mixin); |
488
|
|
|
|
489
|
|
|
if ($mixin->block->isEmpty()) { |
490
|
|
|
$mixin->block = null; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
return $mixin; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
protected function parseMixin() |
497
|
|
|
{ |
498
|
|
|
$token = $this->expect('mixin'); |
499
|
|
|
$name = $token->value; |
500
|
|
|
$arguments = $token->arguments; |
501
|
|
|
|
502
|
|
|
// definition |
503
|
|
|
if ('indent' === $this->peekType()) { |
504
|
|
|
$mixin = new Nodes\Mixin($name, $arguments, $this->block(), false); |
505
|
|
|
$this->mixins[$name] = $mixin; |
506
|
|
|
|
507
|
|
|
return $mixin; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
// call |
511
|
|
|
return new Nodes\Mixin($name, $arguments, null, true); |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
protected function parseTextBlock() |
515
|
|
|
{ |
516
|
|
|
$block = new Nodes\Block(); |
517
|
|
|
$block->line = $this->line(); |
|
|
|
|
518
|
|
|
$spaces = $this->expect('indent')->value; |
519
|
|
|
|
520
|
|
|
if (!isset($this->_spaces)) { |
521
|
|
|
$this->_spaces = $spaces; |
|
|
|
|
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
$indent = str_repeat(' ', $spaces - $this->_spaces + 1); |
525
|
|
|
|
526
|
|
|
while ($this->peekType() != 'outdent') { |
527
|
|
|
switch ($this->peekType()) { |
528
|
|
|
case 'newline': |
529
|
|
|
$this->lexer->advance(); |
530
|
|
|
break; |
531
|
|
|
|
532
|
|
|
case 'indent': |
533
|
|
|
foreach ($this->parseTextBlock()->nodes as $n) { |
534
|
|
|
$block->push($n); |
535
|
|
|
} |
536
|
|
|
break; |
537
|
|
|
|
538
|
|
|
default: |
539
|
|
|
$this->parseInlineTags($block, $indent . $this->advance()->value); |
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
if (isset($this->_spaces) && $spaces === $this->_spaces) { |
544
|
|
|
unset($this->_spaces); |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
$this->expect('outdent'); |
548
|
|
|
|
549
|
|
|
return $block; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
protected function block() |
553
|
|
|
{ |
554
|
|
|
$block = new Nodes\Block(); |
555
|
|
|
$block->line = $this->line(); |
|
|
|
|
556
|
|
|
$this->expect('indent'); |
557
|
|
|
|
558
|
|
|
while ($this->peekType() !== 'outdent') { |
559
|
|
|
if ($this->peekType() === 'newline') { |
560
|
|
|
$this->lexer->advance(); |
561
|
|
|
continue; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
$block->push($this->parseExpression()); |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
$this->expect('outdent'); |
568
|
|
|
|
569
|
|
|
return $block; |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
protected function parseInterpolation() |
573
|
|
|
{ |
574
|
|
|
$token = $this->advance(); |
575
|
|
|
$tag = new Nodes\Tag($token->value); |
576
|
|
|
$tag->buffer = true; |
|
|
|
|
577
|
|
|
|
578
|
|
|
return $this->tag($tag); |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
protected function parseASTFilter() |
582
|
|
|
{ |
583
|
|
|
$token = $this->expect('tag'); |
584
|
|
|
$attributes = $this->accept('attributes'); |
585
|
|
|
$this->expect(':'); |
586
|
|
|
$block = $this->block(); |
587
|
|
|
$node = new Nodes\Filter($token->value, $block, $attributes); |
588
|
|
|
$node->line = $this->line(); |
|
|
|
|
589
|
|
|
|
590
|
|
|
return $node; |
591
|
|
|
} |
592
|
|
|
|
593
|
|
|
protected function parseTag() |
594
|
|
|
{ |
595
|
|
|
$i = 2; |
596
|
|
|
|
597
|
|
|
if ('attributes' === $this->lookahead($i)->type) { |
598
|
|
|
$i++; |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
if (':' === $this->lookahead($i)->type) { |
602
|
|
|
$i++; |
603
|
|
|
|
604
|
|
|
if ('indent' === $this->lookahead($i)->type) { |
605
|
|
|
return $this->parseASTFilter(); |
606
|
|
|
} |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
$token = $this->advance(); |
610
|
|
|
$tag = new Nodes\Tag($token->value); |
611
|
|
|
|
612
|
|
|
$tag->selfClosing = isset($token->selfClosing) |
613
|
|
|
? $token->selfClosing |
614
|
|
|
: false; |
615
|
|
|
|
616
|
|
|
return $this->tag($tag); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
public function parseInlineTags($block, $str) |
620
|
|
|
{ |
621
|
|
|
while (preg_match('/^(.*?)#\[([^\]\n]+)\]/', $str, $matches)) { |
622
|
|
|
if (!empty($matches[1])) { |
623
|
|
|
$text = new Nodes\Text($matches[1]); |
624
|
|
|
$text->line = $this->line(); |
|
|
|
|
625
|
|
|
$block->push($text); |
626
|
|
|
} |
627
|
|
|
$parser = $this->subParser($matches[2]); |
628
|
|
|
$tag = $parser->parse(); |
629
|
|
|
$tag->line = $this->line(); |
630
|
|
|
$block->push($tag); |
631
|
|
|
$str = substr($str, strlen($matches[0])); |
632
|
|
|
} |
633
|
|
|
if (substr($str, 0, 1) === ' ') { |
634
|
|
|
$str = substr($str, 1); |
635
|
|
|
} |
636
|
|
|
$text = new Nodes\Text($str); |
637
|
|
|
$text->line = $this->line(); |
638
|
|
|
$block->push($text); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
protected function peekType() |
642
|
|
|
{ |
643
|
|
|
return ($peek = $this->peek()) |
644
|
|
|
? $peek->type |
645
|
|
|
: null; |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
protected function tag($tag) |
649
|
|
|
{ |
650
|
|
|
$tag->line = $this->line(); |
651
|
|
|
|
652
|
|
|
while (true) { |
653
|
|
|
switch ($type = $this->peekType()) { |
654
|
|
|
case 'id': |
655
|
|
|
$token = $this->advance(); |
656
|
|
|
$peek = $this->peek(); |
657
|
|
|
$escaped = isset($peek->escaped, $peek->escaped[$type]) && $peek->escaped[$type]; |
658
|
|
|
$value = $escaped || !isset($peek->attributes, $peek->attributes[$type]) |
659
|
|
|
? "'" . $token->value . "'" |
660
|
|
|
: $peek->attributes[$type]; |
661
|
|
|
$tag->setAttribute($token->type, $value, $escaped); |
662
|
|
|
unset($peek->attributes[$type]); |
663
|
|
|
continue; |
664
|
|
|
|
665
|
|
|
case 'class': |
666
|
|
|
$token = $this->advance(); |
667
|
|
|
$tag->setAttribute($token->type, "'" . $token->value . "'"); |
668
|
|
|
continue; |
669
|
|
|
|
670
|
|
|
case 'attributes': |
671
|
|
|
$token = $this->advance(); |
672
|
|
|
$obj = $token->attributes; |
673
|
|
|
$escaped = $token->escaped; |
674
|
|
|
$nameList = array_keys($obj); |
675
|
|
|
|
676
|
|
|
if ($token->selfClosing) { |
677
|
|
|
$tag->selfClosing = true; |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
foreach ($nameList as $name) { |
681
|
|
|
$value = $obj[$name]; |
682
|
|
|
$normalizedValue = strtolower($value); |
683
|
|
|
if ($normalizedValue === 'true' || $normalizedValue === 'false') { |
684
|
|
|
$value = $normalizedValue === 'true'; |
685
|
|
|
} |
686
|
|
|
$tag->setAttribute($name, $value, $escaped[$name]); |
687
|
|
|
} |
688
|
|
|
continue; |
689
|
|
|
|
690
|
|
|
case '&attributes': |
691
|
|
|
$token = $this->advance(); |
692
|
|
|
$tag->setAttribute('&attributes', $token->value); |
693
|
|
|
continue; |
694
|
|
|
|
695
|
|
|
default: |
696
|
|
|
break 2; |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
$dot = false; |
701
|
|
|
$tag->textOnly = false; |
702
|
|
|
if ('.' === $this->peek()->value) { |
703
|
|
|
$dot = $tag->textOnly = true; |
704
|
|
|
$this->advance(); |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
switch ($this->peekType()) { |
708
|
|
|
case 'text': |
709
|
|
|
$this->parseInlineTags($tag->block, $this->expect('text')->value); |
710
|
|
|
break; |
711
|
|
|
|
712
|
|
|
case 'code': |
713
|
|
|
$tag->code = $this->parseCode(); |
714
|
|
|
break; |
715
|
|
|
|
716
|
|
|
case ':': |
717
|
|
|
$this->advance(); |
718
|
|
|
$tag->block = new Nodes\Block(); |
719
|
|
|
$tag->block->push($this->parseExpression()); |
720
|
|
|
break; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
while ('newline' === $this->peekType()) { |
724
|
|
|
$this->advance(); |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
if ('script' === $tag->name) { |
728
|
|
|
$type = $tag->getAttribute('type'); |
729
|
|
|
|
730
|
|
|
if ($type !== null) { |
731
|
|
|
$type = preg_replace('/^[\'\"]|[\'\"]$/', '', $type); |
732
|
|
|
|
733
|
|
|
if (!$dot && 'text/javascript' != $type['value']) { |
734
|
|
|
$tag->textOnly = false; |
735
|
|
|
} |
736
|
|
|
} |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
if ('indent' === $this->peekType()) { |
740
|
|
|
if ($tag->textOnly) { |
741
|
|
|
$this->lexer->pipeless = true; |
742
|
|
|
$tag->block = $this->parseTextBlock(); |
743
|
|
|
$this->lexer->pipeless = false; |
744
|
|
|
|
745
|
|
|
return $tag; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
$block = $this->block(); |
749
|
|
|
|
750
|
|
|
if ($tag->block && !$tag->block->isEmpty()) { |
751
|
|
|
foreach ($block->nodes as $n) { |
752
|
|
|
$tag->block->push($n); |
753
|
|
|
} |
754
|
|
|
|
755
|
|
|
return $tag; |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
$tag->block = $block; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
return $tag; |
762
|
|
|
} |
763
|
|
|
} |
764
|
|
|
|
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.