HTMLNode::getRenderHTML()   F
last analyzed

Complexity

Conditions 21
Paths 145

Size

Total Lines 64
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 22.5334

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 21
eloc 33
c 1
b 1
f 0
nc 145
nop 2
dl 0
loc 64
ccs 28
cts 33
cp 0.8485
crap 22.5334
rs 3.7916

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Formularium;
4
5
use PHP_CodeSniffer\Generators\HTML;
6
7
use function Safe\substr;
8
9
/**
10
 * Class that encapsule DOM node elements. Similar to PHP DOMElement but more flexible.
11
 * This is not used for parsing, but to build HTML.
12
 */
13
class HTMLNode
14
{
15
    const STANDALONE_TAGS = ['img', 'hr', 'br', 'input', 'meta', 'col', 'command', 'link', 'param', 'source', 'embed'];
16
17
    /**
18
     * The HTML attributes and respectives values
19
     * This is an associative array wich:
20
     *     key is the attribute and
21
     *     value is the array with attributes values
22
     * @var array
23
     */
24
    protected $attributes = [];
25
26
    /**
27
     * Tag name
28
     * @var string
29
     */
30
    protected $tag;
31
32
    /**
33
     * The content of Element
34
     * @var array
35
     */
36
    protected $content = [];
37
38
    /**
39
     * If this tag has no children, still render it?
40
     * @var boolean
41
     */
42
    protected $renderIfEmpty = true;
43
44
    /**
45
     * Extra data stored with this node that might be used by the frameworks.
46
     *
47
     * @var Metadata
48
     */
49
    protected $metadata = null;
50
51
    /**
52
     * Create a HTML Element
53
     * @param string $tag The tag name of Element
54
     * @param array $attributes The attribute with values
55
     * @param mixed $content The content of element, can be:
56
     *                - string (with text content)
57
     *                - HTMLNode
58
     *                - array with others elements or text
59
     * @param boolean $raw If true, do not escape content.
60
     */
61 13
    public function __construct(string $tag, array $attributes = [], $content = '', $raw = false)
62
    {
63 13
        $this->tag = $tag;
64 13
        $this->metadata = new Metadata('HTMLNode', '', []);
65
66 13
        $this->setAttributes($attributes);
67
68 13
        if (!empty($content)) {
69 7
            $this->setContent($content, true, $raw);
70
        }
71 13
    }
72
73
    /**
74
     * Create a HTML Element
75
     * @param string $tag The tag name of Element
76
     * @param array $attributes The attribute with values
77
     * @param mixed $content The content of element, can be:
78
     *                - string (with text content)
79
     *                - HTMLNode
80
     *                - array with others elements or text
81
     * @param boolean $raw If true, do not escape content.
82
     * @return HTMLNode
83
     */
84 3
    public static function factory(string $tag, array $attributes = [], $content = '', $raw = false): HTMLNode
85
    {
86 3
        return new self($tag, $attributes, $content, $raw);
87
    }
88
89
    /**
90
     * Sets whether this tag will render if it is empty (that is,
91
     * no contents or children)
92
     *
93
     * @param boolean $val
94
     * @return HTMLNode Itself
95
     */
96
    public function setRenderIfEmpty(bool $val): HTMLNode
97
    {
98
        $this->renderIfEmpty = $val;
99
        return $this;
100
    }
101
102
    /**
103
     * Gets metadata.
104
     * @return Metadata
105
     */
106
    public function getMetadata(): Metadata
107
    {
108
        return $this->metadata;
109
    }
110
111
    /**
112
     * Return a tag
113
     * @return string tag name
114
     */
115 4
    public function getTag()
116
    {
117 4
        return $this->tag;
118
    }
119
120
    /**
121
     * Modifies our tag
122
     *
123
     * @param string $tag
124
     * @return HTMLNode Itself
125
     */
126 1
    public function setTag($tag)
127
    {
128 1
        $this->tag = $tag;
129 1
        return $this;
130
    }
131
132
    /**
133
     * Return a attribute
134
     * @param string $name The name of attribute
135
     * @return array Return the list of values, if attribute don't exist return empty array
136
     */
137 2
    public function getAttribute($name): array
138
    {
139 2
        return $this->attributes[$name] ?? [];
140
    }
141
142
    /**
143
     * Does attribute exist?
144
     * @param string $name The name of attribute
145
     * @return bool
146
     */
147
    public function hasAttribute($name): bool
148
    {
149
        return array_key_exists($name, $this->attributes);
150
    }
151
152
    /**
153
     * Return the content of element
154
     * @return mixed array of HTMLNode and string (text)
155
     */
156 4
    public function getContent()
157
    {
158 4
        return $this->content;
159
    }
160
161
    public function isEmpty(): bool
162
    {
163
        return count($this->content) > 0;
164
    }
165
166
    /**
167
     *
168
     * @return int
169
     */
170
    public function getCountChildren(): int
171
    {
172
        return count($this->content);
173
    }
174
175
    /**
176
     * Set a attribute value, if attribute exist overwrite it
177
     * @param string $name
178
     * @param mixed $value Can be a string or array of string
179
     * @return HTMLNode Itself
180
     */
181 1
    public function setAttribute(string $name, $value): HTMLNode
182
    {
183 1
        $this->setAttributes([$name => $value], true);
184 1
        return $this;
185
    }
186
187
    /**
188
     * Add an attribute value, if attribute exist append value
189
     * @param string $name
190
     * @param mixed $value Can be a string or array of string
191
     * @return HTMLNode Itself
192
     */
193 1
    public function addAttribute(string $name, $value = null): HTMLNode
194
    {
195 1
        $this->setAttributes([$name => $value], false);
196 1
        return $this;
197
    }
198
199
    /**
200
     * Set a set of attributes
201
     * @param array $attributes Associative array wich
202
     * 				key is attributes names
203
     *              value is string or array values
204
     * @param bool $overwrite if true and attribute name exist overwrite them
205
     * @return HTMLNode Itself
206
     *
207
     */
208 13
    public function setAttributes(array $attributes, $overwrite = true): HTMLNode
209
    {
210 13
        foreach ($attributes as $atrib => $value) {
211 12
            if (is_array($value)) {
212 2
                $values = $value;
213
            } else {
214 12
                $values = [$value];
215
            }
216
217 12
            if ($overwrite) {
218 12
                $this->attributes[$atrib] = $values;
219
            } else {
220 1
                if (empty($this->attributes[$atrib])) {
221 1
                    $this->attributes[$atrib] = [];
222
                }
223
224 1
                $this->attributes[$atrib] = array_merge($this->attributes[$atrib], $values);
225
            }
226
        }
227 13
        return $this;
228
    }
229
230
    public function removeAttribute(string $attribute): HTMLNode
231
    {
232
        if (array_key_exists($attribute, $this->attributes)) {
233
            unset($this->attributes[$attribute]);
234
        }
235
        return $this;
236
    }
237
238
    /**
239
     * Aliases to HTMLNode::setAttributes($content, false);
240
     * @see setAttributes
241
     * @return HTMLNode Itself
242
     */
243 1
    public function addAttributes(array $attributes): HTMLNode
244
    {
245 1
        $this->setAttributes($attributes, false);
246 1
        return $this;
247
    }
248
249
    /**
250
     * Set content (dom objects or texts) to element
251
     * @param string|HTMLNode|string[]|HTMLNode[] $content The content of element, can be:
252
     *                - string (with text content)
253
     *                - HTMLNode
254
     *                - array with others elements or text
255
     * @param bool $overwrite if true overwrite content otherwise append the content
256
     * @param bool $raw If true, this is raw content (html) and should not be escaped.
257
     * @param bool $prepend If true prepend instead of appending
258
     * @return HTMLNode Itself
259
     */
260 11
    public function setContent($content, $overwrite = true, $raw = false, $prepend = false): HTMLNode
261
    {
262
        // TODO Don't work with reference objects, change it
263 11
        if (!is_array($content)) {
264 8
            $content = [$content];
265
        }
266
267 11
        if ($raw === false) {
268 9
            foreach ($content as &$item) {
269 9
                if (is_string($item)) {
270 8
                    $item = htmlspecialchars($item);
271
                }
272
            }
273
        }
274
275 11
        if ($overwrite) {
276 8
            $this->content = $content;
277
        } else {
278 5
            if ($prepend) {
279
                $this->content = array_merge($content, $this->content);
280
            } else {
281 5
                $this->content = array_merge($this->content, $content);
282
            }
283
        }
284 11
        return $this;
285
    }
286
287
    /**
288
     * Aliases to HTMLNode::setContent($content, false);
289
     * @see setContent
290
     * @param string|HTMLNode|string[]|HTMLNode[] $content
291
     * @return HTMLNode Itself
292
     */
293 3
    public function addContent($content, bool $raw = false): HTMLNode
294
    {
295 3
        $this->setContent($content, false, $raw);
296 3
        return $this;
297
    }
298
299
    /**
300
     * Appends content nodes to the bottom of this element.
301
     *
302
     * @see setContent
303
     * @param string|HTMLNode|string[]|HTMLNode[] $content
304
     * @param boolean $raw
305
     * @return HTMLNode
306
     */
307 1
    public function appendContent($content, bool $raw = false): HTMLNode
308
    {
309 1
        $this->setContent($content, false, $raw);
310 1
        return $this;
311
    }
312
313
    /**
314
     * Prepends content nodes to the beginning of this element.
315
     *
316
     * @see setContent
317
     * @param string|HTMLNode|string[]|HTMLNode[] $content
318
     * @param boolean $raw
319
     * @return HTMLNode
320
     */
321
    public function prependContent($content, bool $raw = false): HTMLNode
322
    {
323
        $this->setContent($content, false, $raw, true);
324
        return $this;
325
    }
326
327
    /**
328
     * Find and return elements using selector
329
     * @param string $selector A selector of elements based in jQuery
330
     *						'eee' - Select elements 'eee' (with tag)
331
     * 						'#ii' - Select a element with id attribute 'ii'
332
     * 						'.cc' - Select elements with class attribute 'cc'
333
     * 						'[a=v]' - Select elements with 'a' attribute with 'v' value
334
     * 						'e#i' - Select elements 'e' with id attribute 'i'
335
     * 						'e.c' - Select elements 'e' with class attribute 'c'
336
     * @return HTMLNode[]
337
     */
338 2
    public function get(string $selector)
339
    {
340 2
        $tag = null;
341 2
        $attr = null;
342 2
        $val = null;
343
344
        // Define what could be found
345 2
        $selector = trim($selector);
346 2
        if ($selector[0] == "#") {
347 1
            $attr = "id";
348 1
            $val = mb_substr($selector, 1);
349 2
        } elseif ($selector[0] == ".") {
350 2
            $attr = "class";
351 2
            $val = mb_substr($selector, 1);
352 1
        } elseif (mb_strpos($selector, "=") !== false) {
353 1
            list($attr, $val) = explode("=", substr($selector, 1, -1));
354 1
        } elseif (mb_strpos($selector, "#") !== false) {
355 1
            $attr = "id";
356 1
            list($tag, $val) = explode("#", $selector);
357 1
        } elseif (mb_strpos($selector, ".") !== false) {
358 1
            $attr = "class";
359 1
            list($tag, $val) = explode(".", $selector);
360
        } else {
361 1
            $tag = $selector;
362
        }
363
364 2
        return $this->getInternal($this, $tag, $attr, $val);
365
    }
366
367
    /**
368
     * Find and return elements based in $tag, $attr, $val
369
     * The $tag or $attr must be a value
370
     *
371
     * @see HTMLNode::get
372
     * @param string $tag tag of search
373
     * @param string $attr attribute of search
374
     * @param string $val value of attribute search
375
     * @return HTMLNode[]
376
     */
377 1
    public function getElements(string $tag, string $attr, string $val)
378
    {
379 1
        return $this->getInternal($this, $tag, $attr, $val);
380
    }
381
382
    /**
383
     * Recursive function to found elements
384
     * @param HTMLNode $element Element that will be available
385
     * @param string $tag Tag or null value to compare
386
     * @param string $attr Attribute name or null value to compare
387
     * @param string $val Value of attribute or null value to compare
388
     * @return HTMLNode[]
389
     */
390 2
    protected function getInternal(HTMLNode $element, string $tag = null, string $attr = null, string $val = null)
391
    {
392 2
        if ($this->match($element, $tag, $attr, $val)) {
393 2
            $return = [$element];
394
        } else {
395 2
            $return = [];
396
        }
397
398 2
        foreach ($element->getContent() as $content) {
399 2
            if ($content instanceof HTMLNode) {
400 2
                $return = array_merge($return, $this->getInternal($content, $tag, $attr, $val));
401
            }
402
        }
403
404 2
        return $return;
405
    }
406
407
    /**
408
     * Return a boolean based on match of the element with $tag, $attr or $val
409
     * @param HTMLNode $element Element that will be available
410
     * @param string $tag Tag or null value to compare
411
     * @param string $attr Attribute name or null value to compare
412
     * @param string $val Value of attribute or null value to compare
413
     * @return boolean - true when satisfy and false otherwise
414
     */
415 2
    protected function match(HTMLNode $element, $tag, $attr, $val): bool
416
    {
417 2
        if (!empty($tag)) {
418 1
            if ($element->getTag() != $tag) {
419 1
                return false;
420
            }
421
        } elseif (empty($attr)) {
422
            // Only when $tag and $attr is empty
423 1
            return false;
424
        }
425
426 2
        if (!empty($attr)) {
427 2
            $values = $element->getAttribute($attr);
428 2
            if (count($values) == 0) {
429 2
                return false;
430
            }
431
432 2
            if (!empty($val)) {
433 2
                return in_array($val, $values);
434
            }
435
        }
436
437 1
        return true;
438
    }
439
440 2
    public function __toString()
441
    {
442 2
        return $this->getRenderHTML();
443
    }
444
    
445
    /**
446
     * Return the html element code including all children
447
     *
448
     * @param string $indentString String used to indent HTML code. Use '' for a compact version.
449
     * @param integer $level The current indentation level.
450
     * @return string (html code)
451
     */
452 9
    public function getRenderHTML($indentString = '  ', $level = 0): string
453
    {
454
455
        // skip empty non renderable
456 9
        if ($this->renderIfEmpty === false) {
457
            if (!count($this->content)) {
458
                return '';
459
            }
460
        }
461
462
        // start
463 9
        $data = [];
464
465
        // if this is not empty, the tag
466 9
        if (!empty($this->tag)) {
467 9
            $open = [];
468 9
            $open[] = ($level > 0 ? $indentString : '') . // initial indentation
469 9
                '<' . htmlspecialchars($this->tag);
470
471
            // render tag attributes
472 9
            foreach ($this->attributes as $attrib => $value) {
473 8
                $open[] = $attrib . ($value === [null] ? '' : ('="' . htmlspecialchars(implode(' ', $value)) . '"'));
474
            }
475 9
            $data[] = join(' ', $open) . (in_array($this->tag, self::STANDALONE_TAGS) ? '/>' : '>');
476
        }
477
478
        // recurse
479 9
        $contentdata = [];
480 9
        $emptyfieldset = ($this->tag == 'fieldset'); // avoid rendering fieldset with only a "legend"
481 9
        foreach ($this->content as $content) {
482 6
            if ($content instanceof HTMLNode) {
483 4
                $c = $content->getRenderHTML($indentString, $level + 1);
484 4
                if ($this->tag == 'fieldset' and $content->getTag() != 'legend' and $c) {
485
                    $emptyfieldset = false;
486
                }
487 4
                $contentdata[] = $c;
488
            } else {
489 5
                $emptyfieldset = false;
490 5
                $contentdata[] = $indentString . $content;
491
            }
492
        }
493
494
        // handle special empty cases
495 9
        if ($emptyfieldset) {
496
            return '';
497 9
        } elseif ($contentdata == [] && $this->renderIfEmpty === false) {
498
            return '';
499
        }
500
501
        // join content
502 9
        $data = array_merge($data, $contentdata);
503
504
        // handle closing
505 9
        if (!empty($this->tag)
506 9
            && !in_array($this->tag, self::STANDALONE_TAGS)
507
        ) {
508 9
            $data[] = '</' . htmlspecialchars($this->tag) . '>';
509
        }
510
511 9
        if ($indentString && $level === 0) {
512 4
            $data[] = "\n";
513
        }
514
515 9
        return implode(($indentString ? "\n" : '') . str_repeat($indentString, $level), $data);
516
    }
517
518
    /**
519
     * Clone HTMLNode object and its child
520
     * @return Object HTMLNode
521
     */
522
    public function __clone()
523
    {
524
        $obj = new HTMLNode($this->tag, $this->attributes);
525
        foreach ($this->content as $content) {
526
            if ($content instanceof HTMLNode) {
527
                $obj->addContent(clone $content);
528
            } else {
529
                $obj->addContent($content);
530
            }
531
        }
532
    }
533
534
    public function replace(HTMLNode $e): void
535
    {
536
        $this->tag = $e->tag;
537
        $this->attributes = $e->attributes;
538
        $this->content = $e->content;
539
    }
540
541
    /**
542
     * Clear All Attributes
543
     */
544
    public function clearAttributes(): HTMLNode
545
    {
546
        $this->attributes = [];
547
        return $this;
548
    }
549
550
551
    /**
552
     * Clear All Content
553
     */
554 1
    public function clearContent(): HTMLNode
555
    {
556 1
        $this->content = [];
557 1
        return $this;
558
    }
559
560
    /**
561
     * Similar to array_walk(). Applied to this HTMLNode and all its children.
562
     * Does not call callback for text content.
563
     *
564
     * @param callable $f
565
     * @return HTMLNode self
566
     */
567 1
    public function walk(callable $f): HTMLNode
568
    {
569 1
        $f($this);
570 1
        foreach ($this->content as $content) {
571 1
            if ($content instanceof HTMLNode) {
572 1
                $content->walk($f);
573
            }
574
        }
575 1
        return $this;
576
    }
577
578
    /**
579
     * Similar to array_map(). Calls callback for text content too.
580
     *
581
     * @param callable $f
582
     * @return array
583
     */
584 1
    public function map(callable $f, bool $recurse = true): array
585
    {
586 1
        $data = [$f($this)];
587 1
        foreach ($this->content as $content) {
588 1
            if ($recurse && $content instanceof HTMLNode) {
589 1
                $data = array_merge($data, $content->map($f, $recurse));
590
            } else {
591 1
                $data[] = $f($content);
592
            }
593
        }
594 1
        return $data;
595
    }
596
597
    /**
598
     * Similar to array_filter(): removes children from this element.
599
     * Does not call callback for text content.
600
     *
601
     * @param callable $f If returns false, element being checked is removed.
602
     * @return HTMLNode[] The filtered elements.
603
     */
604 1
    public function filter(callable $f, bool $recurse = true): array
605
    {
606 1
        $deleted = [];
607 1
        foreach ($this->content as $key => $content) {
608 1
            if ($content instanceof HTMLNode) {
609 1
                if (!$f($content)) {
610 1
                    $deleted[] = $this->content[$key];
611 1
                    unset($this->content[$key]);
612
                } elseif ($recurse) {
613
                    $content->filter($f, $recurse);
614
                }
615
            }
616
        }
617 1
        return $deleted;
618
    }
619
}
620