Passed
Push — master ( 207f22...1de42a )
by Bruno
12:47 queued 03:59
created

HTMLElement::isEmpty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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