Completed
Branch develop (c2aa4c)
by Anton
05:17
created

Node::mountBlock()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 25
rs 8.439
cc 5
eloc 11
nc 6
nop 4
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})
1 ignored issue
show
Unused Code Comprehensibility introduced by
45% 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...
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
}