1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\Stempler; |
9
|
|
|
|
10
|
|
|
use Spiral\Stempler\Behaviours\BlockBehaviour; |
11
|
|
|
use Spiral\Stempler\Behaviours\ExtendsBehaviour; |
12
|
|
|
use Spiral\Stempler\Behaviours\IncludeBehaviour; |
13
|
|
|
use Spiral\Stempler\Exceptions\StrictModeException; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Stempler Node represents simple XML like tree of blocks defined by behaviours provided by it's |
17
|
|
|
* supervisor. Node utilizes HtmlTokenizer to create set of tokens being feeded to supervisor. |
18
|
|
|
*/ |
19
|
|
|
class Node |
20
|
|
|
{ |
21
|
|
|
/** |
22
|
|
|
* Short tags expression, usually used inside attributes and etc. |
23
|
|
|
*/ |
24
|
|
|
const SHORT_TAGS = '/\${(?P<name>[a-z0-9_\.\-]+)(?: *\| *(?P<default>[^}]+) *)?}/i'; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Node name (usually related to block name). |
28
|
|
|
* |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
private $name = ''; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Indication that node extended parent layout/node, meaning custom blocks can not be rendered |
35
|
|
|
* outside defined parent layout. |
36
|
|
|
* |
37
|
|
|
* @var bool |
38
|
|
|
*/ |
39
|
|
|
private $extended = false; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Set of child nodes being used during rendering. |
43
|
|
|
* |
44
|
|
|
* @var string[]|Node[] |
45
|
|
|
*/ |
46
|
|
|
private $nodes = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Set of blocks defined outside parent scope (parent layout blocks), blocks like either dynamic |
50
|
|
|
* or used for internal template reasons. They should not be rendered in plain HTML (but can be |
51
|
|
|
* used by Exporters to render as something else). |
52
|
|
|
* |
53
|
|
|
* @var Node[] |
54
|
|
|
*/ |
55
|
|
|
private $outers = []; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* NodeSupervisor responsible for resolve tag behaviours. |
59
|
|
|
* |
60
|
|
|
* @invisible |
61
|
|
|
* @var SupervisorInterface |
62
|
|
|
*/ |
63
|
|
|
protected $supervisor = null; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @param SupervisorInterface $supervisor |
67
|
|
|
* @param string $name |
68
|
|
|
* @param string|array $source String content or array of html tokens. |
69
|
|
|
* @param HtmlTokenizer $tokenizer Html tokens source. |
70
|
|
|
*/ |
71
|
|
|
public function __construct( |
72
|
|
|
SupervisorInterface $supervisor, |
73
|
|
|
$name, |
74
|
|
|
$source = [], |
75
|
|
|
HtmlTokenizer $tokenizer = null |
76
|
|
|
) { |
77
|
|
|
$this->supervisor = $supervisor; |
78
|
|
|
$this->name = $name; |
79
|
|
|
|
80
|
|
|
if (empty($tokenizer)) { |
81
|
|
|
$tokenizer = new HtmlTokenizer(); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
if (is_string($source)) { |
85
|
|
|
$source = $tokenizer->parse($source); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$this->parseTokens($source); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @return SupervisorInterface |
93
|
|
|
*/ |
94
|
|
|
public function supervisor() |
95
|
|
|
{ |
96
|
|
|
return $this->supervisor; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Create new block under current node. If node extends parent, block will ether replace parent |
101
|
|
|
* content or will be added as outer block (block with parent placeholder). |
102
|
|
|
* |
103
|
|
|
* @param string $name |
104
|
|
|
* @param string|array $source String content or array of html tokens. |
105
|
|
|
* @param array $blocks Used to redefine node content and bypass token parsing. |
106
|
|
|
* @param bool $replace Set to true to send created Node directly to outer blocks. |
107
|
|
|
*/ |
108
|
|
|
public function mountBlock($name, $source, $blocks = [], $replace = false) |
109
|
|
|
{ |
110
|
|
|
$node = new static($this->supervisor, $name, $source); |
111
|
|
|
|
112
|
|
|
if (!empty($blocks)) { |
113
|
|
|
$node->nodes = $blocks; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if (!$this->extended && !$replace) { |
117
|
|
|
//No parent yet, block are open |
118
|
|
|
$this->mountNode($node); |
119
|
|
|
|
120
|
|
|
return; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
if (empty($parent = $this->findNode($name))) { |
124
|
|
|
//Does not exist in parent (dynamic block) |
125
|
|
|
$this->outers[] = $node; |
126
|
|
|
|
127
|
|
|
return; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
//We have to replace parent content with extended blocks |
131
|
|
|
$parent->replaceNode($node); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Add sub node. |
136
|
|
|
* |
137
|
|
|
* @param Node $node |
138
|
|
|
*/ |
139
|
|
|
public function mountNode(Node $node) |
140
|
|
|
{ |
141
|
|
|
$this->nodes[] = $node; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Recursively find a children node by it's name. |
146
|
|
|
* |
147
|
|
|
* @param string $name |
148
|
|
|
* @return Node|null |
149
|
|
|
*/ |
150
|
|
|
public function findNode($name) |
151
|
|
|
{ |
152
|
|
|
foreach ($this->nodes as $node) { |
153
|
|
|
if ($node instanceof self && !empty($node->name)) { |
154
|
|
|
if ($node->name === $name) { |
155
|
|
|
return $node; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
if ($found = $node->findNode($name)) { |
159
|
|
|
return $found; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return null; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Compile node data (inner nodes) into string. |
169
|
|
|
* |
170
|
|
|
* @param array $dynamic All outer blocks will be aggregated in this array (in compiled form). |
171
|
|
|
* @param array $compiled Internal complication memory (method called recursively) |
172
|
|
|
* @return string |
173
|
|
|
*/ |
174
|
|
|
public function compile(&$dynamic = [], &$compiled = []) |
175
|
|
|
{ |
176
|
|
|
if (!is_array($dynamic)) { |
177
|
|
|
$dynamic = []; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
if (!is_array($compiled)) { |
181
|
|
|
$compiled = []; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
//We have to pre-compile outer nodes first |
185
|
|
|
foreach ($this->outers as $node) { |
186
|
|
|
if ($node instanceof self && !array_key_exists($node->name, $compiled)) { |
187
|
|
|
//We don't need outer blocks from deeper level (right?) |
188
|
|
|
$nestedOuters = []; |
189
|
|
|
|
190
|
|
|
//Node data in a cache now |
191
|
|
|
$dynamic[$node->name] = $compiled[$node->name] = $node->compile( |
192
|
|
|
$nestedOuters, |
193
|
|
|
$compiled |
194
|
|
|
); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
if ($this->nodes === [null]) { |
199
|
|
|
//Valueless attributes |
200
|
|
|
return ''; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
$result = ''; |
204
|
|
|
foreach ($this->nodes as $node) { |
205
|
|
|
if (is_string($node) || is_null($node)) { |
206
|
|
|
$result .= $node; |
207
|
|
|
continue; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if (!array_key_exists($node->name, $compiled)) { |
211
|
|
|
//We don't need outer blocks from deeper level (right?) |
212
|
|
|
$nestedOuters = []; |
213
|
|
|
|
214
|
|
|
//Node data in a cache now |
215
|
|
|
$compiled[$node->name] = $node->compile($nestedOuters, $compiled); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
$result .= $compiled[$node->name]; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
return $result; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Parse set of tokens provided by html Tokenizer and create blocks and other control |
226
|
|
|
* constructions. Basically it will try to created html tree. |
227
|
|
|
* |
228
|
|
|
* @param array $tokens |
229
|
|
|
* @throws StrictModeException |
230
|
|
|
*/ |
231
|
|
|
protected function parseTokens(array $tokens) |
232
|
|
|
{ |
233
|
|
|
//Current active token |
234
|
|
|
$activeToken = []; |
235
|
|
|
|
236
|
|
|
//Some blocks can be named as parent. We have to make sure we closing the correct one |
237
|
|
|
$activeLevel = 0; |
238
|
|
|
|
239
|
|
|
//Content to represent full tag declaration (including body) |
240
|
|
|
$activeContent = []; |
241
|
|
|
|
242
|
|
|
foreach ($tokens as $token) { |
243
|
|
|
$tokenType = $token[HtmlTokenizer::TOKEN_TYPE]; |
244
|
|
|
|
245
|
|
|
if (empty($activeToken)) { |
246
|
|
|
switch ($tokenType) { |
247
|
|
|
case HtmlTokenizer::TAG_VOID: |
248
|
|
|
case HtmlTokenizer::TAG_SHORT: |
249
|
|
|
//Token will be clarified using Supervisor |
250
|
|
|
$this->mountToken($token); |
251
|
|
|
break; |
252
|
|
|
|
253
|
|
|
case HtmlTokenizer::TAG_OPEN: |
254
|
|
|
$activeToken = $token; |
255
|
|
|
break; |
256
|
|
|
|
257
|
|
|
case HtmlTokenizer::TAG_CLOSE: |
258
|
|
|
if ($this->supervisor->syntax()->isStrict()) { |
259
|
|
|
throw new StrictModeException( |
260
|
|
|
"Unpaired close tag '{$token[HtmlTokenizer::TOKEN_NAME]}'.", $token |
261
|
|
|
); |
262
|
|
|
} |
263
|
|
|
break; |
264
|
|
|
case HtmlTokenizer::PLAIN_TEXT: |
265
|
|
|
//Everything outside any tag |
266
|
|
|
$this->mountContent([$token]); |
267
|
|
|
break; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
continue; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if ( |
274
|
|
|
$tokenType != HtmlTokenizer::PLAIN_TEXT |
275
|
|
|
&& $token[HtmlTokenizer::TOKEN_NAME] == $activeToken[HtmlTokenizer::TOKEN_NAME] |
276
|
|
|
) { |
277
|
|
|
if ($tokenType == HtmlTokenizer::TAG_OPEN) { |
278
|
|
|
$activeContent[] = $token; |
279
|
|
|
$activeLevel++; |
280
|
|
|
} elseif ($tokenType == HtmlTokenizer::TAG_CLOSE) { |
281
|
|
|
if ($activeLevel === 0) { |
282
|
|
|
//Closing current token |
283
|
|
|
$this->mountToken($activeToken, $activeContent, $token); |
284
|
|
|
$activeToken = $activeContent = []; |
285
|
|
|
} else { |
286
|
|
|
$activeContent[] = $token; |
287
|
|
|
$activeLevel--; |
288
|
|
|
} |
289
|
|
|
} else { |
290
|
|
|
//Short tag with same name (used to call for parent content)s |
291
|
|
|
$activeContent[] = $token; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
continue; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
//Collecting token content |
298
|
|
|
$activeContent[] = $token; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
//Everything after last tag |
302
|
|
|
$this->mountContent($activeContent); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Once token content (nested tags and text) is correctly collected we can pass it to supervisor |
307
|
|
|
* to check what we actually should be doing with this token. |
308
|
|
|
* |
309
|
|
|
* @param array $token |
310
|
|
|
* @param array $content |
311
|
|
|
* @param array $closeToken Token described close tag of html element. |
312
|
|
|
*/ |
313
|
|
|
protected function mountToken(array $token, array $content = [], array $closeToken = []) |
314
|
|
|
{ |
315
|
|
|
$behaviour = $this->supervisor->tokenBehaviour($token, $content, $this); |
316
|
|
|
|
317
|
|
|
//Let's check token behaviour to understand how to handle this token |
318
|
|
|
if ($behaviour === BehaviourInterface::SKIP_TOKEN) { |
319
|
|
|
//This is some technical tag (import and etc) |
320
|
|
|
return; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
if ($behaviour === BehaviourInterface::SIMPLE_TAG) { |
324
|
|
|
//Nothing really to do with this tag |
325
|
|
|
$this->mountContent([$token]); |
326
|
|
|
|
327
|
|
|
//Let's parse inner content |
328
|
|
|
$this->parseTokens($content); |
329
|
|
|
|
330
|
|
|
if (!empty($closeToken)) { |
331
|
|
|
$this->mountContent([$closeToken]); |
332
|
|
|
} |
333
|
|
|
} else { |
334
|
|
|
//Now we have to process more complex behaviours |
335
|
|
|
$this->applyBehaviour($behaviour, $content); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Register string node content. |
341
|
|
|
* |
342
|
|
|
* @param string|array $content String content or html tokens. |
343
|
|
|
*/ |
344
|
|
|
private function mountContent($content) |
345
|
|
|
{ |
346
|
|
|
if ($this->extended || empty($content)) { |
347
|
|
|
//No blocks or text can exists outside parent template blocks |
348
|
|
|
return; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
if (is_array($content)) { |
352
|
|
|
$plainContent = ''; |
353
|
|
|
foreach ($content as $token) { |
354
|
|
|
$plainContent .= $token[HtmlTokenizer::TOKEN_CONTENT]; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
$content = $plainContent; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
//Looking for short tag definitions (${title|DEFAULT}) |
|
|
|
|
361
|
|
|
if (preg_match(self::SHORT_TAGS, $content, $matches)) { |
362
|
|
|
$chunks = explode($matches[0], $content); |
363
|
|
|
|
364
|
|
|
//We expecting first chunk to be string (before block) |
365
|
|
|
$this->mountContent(array_shift($chunks)); |
366
|
|
|
|
367
|
|
|
$this->mountBlock( |
368
|
|
|
$matches['name'], |
369
|
|
|
isset($matches['default']) ? $matches['default'] : '' |
370
|
|
|
); |
371
|
|
|
|
372
|
|
|
//Rest of content (after block) |
373
|
|
|
$this->mountContent(join($matches[0], $chunks)); |
374
|
|
|
|
375
|
|
|
return; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
if (is_string(end($this->nodes))) { |
379
|
|
|
$this->nodes[key($this->nodes)] .= $content; |
380
|
|
|
|
381
|
|
|
return; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
$this->nodes[] = $content; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Once supervisor defined custom token behaviour we can process it's content accordingly. |
389
|
|
|
* |
390
|
|
|
* @param BehaviourInterface $behaviour |
391
|
|
|
* @param array $content |
392
|
|
|
*/ |
393
|
|
|
protected function applyBehaviour(BehaviourInterface $behaviour, array $content = []) |
394
|
|
|
{ |
395
|
|
|
if ($behaviour instanceof ExtendsBehaviour) { |
396
|
|
|
//We have to copy nodes from parent |
397
|
|
|
$this->nodes = $behaviour->extendedNode()->nodes; |
398
|
|
|
|
399
|
|
|
//Indication that this node has parent, meaning we have to handle blocks little |
400
|
|
|
//bit different way |
401
|
|
|
$this->extended = true; |
402
|
|
|
|
403
|
|
|
foreach ($behaviour->dynamicBlocks() as $block => $blockContent) { |
404
|
|
|
$this->mountBlock($block, $blockContent); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
return; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
if ($behaviour instanceof BlockBehaviour) { |
411
|
|
|
$this->mountBlock($behaviour->blockName(), $content); |
412
|
|
|
|
413
|
|
|
return; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
if ($behaviour instanceof IncludeBehaviour) { |
417
|
|
|
$this->nodes[] = $behaviour->createNode(); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* More behaviours can be added over time. |
422
|
|
|
*/ |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Replace node content with content provided by external node, external node can still use |
427
|
|
|
* content of parent block by defining block named identical to it's parent. |
428
|
|
|
* |
429
|
|
|
* @param Node $node |
430
|
|
|
*/ |
431
|
|
|
private function replaceNode(Node $node) |
432
|
|
|
{ |
433
|
|
|
//Looking for parent block call |
434
|
|
|
if (!empty($inner = $node->findNode($this->name))) { |
435
|
|
|
//This construction allows child block use parent content |
436
|
|
|
$inner->nodes = $this->nodes; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
$this->nodes = $node->nodes; |
440
|
|
|
} |
441
|
|
|
} |
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.