Passed
Push — master ( 0a0bcf...92e6c5 )
by Bruno
06:17
created

HTMLElement::hasAttribute()   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 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 1
b 1
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
ccs 0
cts 2
cp 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
     * Does attribute exist?
125
     * @param string $name The name of attribute
126
     * @return bool
127
     */
128
    public function hasAttribute($name): bool
129
    {
130
        return array_key_exists($name, $this->attributes);
131
    }
132
133
    /**
134
     * Return the content of element
135
     * @return mixed array of HTMLElement and string (text)
136
     */
137 3
    public function getContent()
138
    {
139 3
        return $this->content;
140
    }
141
142
    public function isEmpty(): bool
143
    {
144
        return count($this->content) > 0;
145
    }
146
147
    /**
148
     *
149
     * @return int
150
     */
151
    public function getCountChildren(): int
152
    {
153
        return count($this->content);
154
    }
155
156
    /**
157
     * Set a attribute value, if attribute exist overwrite it
158
     * @param string $name
159
     * @param mixed $value Can be a string or array of string
160
     * @return HTMLElement Itself
161
     */
162 1
    public function setAttribute(string $name, $value): HTMLElement
163
    {
164 1
        $this->setAttributes([$name => $value], true);
165 1
        return $this;
166
    }
167
168
    /**
169
     * Add an attribute value, if attribute exist append value
170
     * @param string $name
171
     * @param mixed $value Can be a string or array of string
172
     * @return HTMLElement Itself
173
     */
174 1
    public function addAttribute(string $name, $value): HTMLElement
175
    {
176 1
        $this->setAttributes([$name => $value], false);
177 1
        return $this;
178
    }
179
180
    /**
181
     * Set a set of attributes
182
     * @param array $attributes Associative array wich
183
     * 				key is attributes names
184
     *              value is string or array values
185
     * @param bool $overwrite if true and attribute name exist overwrite them
186
     * @return HTMLElement Itself
187
     *
188
     */
189 9
    public function setAttributes(array $attributes, $overwrite = true): HTMLElement
190
    {
191 9
        foreach ($attributes as $atrib => $value) {
192 8
            if (is_array($value)) {
193 2
                $values = $value;
194
            } else {
195 8
                $values = [$value];
196
            }
197
198 8
            if ($overwrite) {
199 8
                $this->attributes[$atrib] = $values;
200
            } else {
201 1
                if (empty($this->attributes[$atrib])) {
202 1
                    $this->attributes[$atrib] = [];
203
                }
204
205 8
                $this->attributes[$atrib] = array_merge($this->attributes[$atrib], $values);
206
            }
207
        }
208 9
        return $this;
209
    }
210
211
    public function removeAttribute(string $attribute): HTMLElement
212
    {
213
        if (array_key_exists($attribute, $this->attributes)) {
214
            unset($this->attributes[$attribute]);
215
        }
216
        return $this;
217
    }
218
219
    /**
220
     * Aliases to HTMLElement::setAttributes($content, false);
221
     * @see setAttributes
222
     * @return HTMLElement Itself
223
     */
224 1
    public function addAttributes(array $attributes): HTMLElement
225
    {
226 1
        $this->setAttributes($attributes, false);
227 1
        return $this;
228
    }
229
230
    /**
231
     * Set content (dom objects or texts) to element
232
     * @param string|HTMLElement|string[]|HTMLElement[] $content The content of element, can be:
233
     *                - string (with text content)
234
     *                - HTMLElement
235
     *                - array with others elements or text
236
     * @param bool $overwrite if true overwrite content otherwise append the content
237
     * @param bool $raw If true, this is raw content (html) and should not be escaped.
238
     * @param bool $prepend If true prepend instead of appending
239
     * @return HTMLElement Itself
240
     */
241 7
    public function setContent($content, $overwrite = true, $raw = false, $prepend = false): HTMLElement
242
    {
243
        // TODO Don't work with reference objects, change it
244 7
        if (!is_array($content)) {
245 4
            $content = [$content];
246
        }
247
248 7
        if ($raw === false) {
249 5
            foreach ($content as &$item) {
250 5
                if (is_string($item)) {
251 5
                    $item = htmlspecialchars($item);
252
                }
253
            }
254
        }
255
256 7
        if ($overwrite) {
257 5
            $this->content = $content;
258
        } else {
259 4
            if ($prepend) {
260
                $this->content = array_merge($content, $this->content);
261
            } else {
262 4
                $this->content = array_merge($this->content, $content);
263
            }
264
        }
265 7
        return $this;
266
    }
267
268
    /**
269
     * Aliases to HTMLElement::setContent($content, false);
270
     * @see setContent
271
     * @param string|HTMLElement|string[]|HTMLElement[] $content
272
     * @return HTMLElement Itself
273
     */
274 3
    public function addContent($content, bool $raw = false): HTMLElement
275
    {
276 3
        $this->setContent($content, false, $raw);
277 3
        return $this;
278
    }
279
280
    /**
281
     * Appends content nodes to the bottom of this element.
282
     *
283
     * @see setContent
284
     * @param string|HTMLElement|string[]|HTMLElement[] $content
285
     * @param boolean $raw
286
     * @return HTMLElement
287
     */
288
    public function appendContent($content, bool $raw = false): HTMLElement
289
    {
290
        $this->setContent($content, false, $raw);
291
        return $this;
292
    }
293
294
    /**
295
     * Prepends content nodes to the beginning of this element.
296
     *
297
     * @see setContent
298
     * @param string|HTMLElement|string[]|HTMLElement[] $content
299
     * @param boolean $raw
300
     * @return HTMLElement
301
     */
302
    public function prependContent($content, bool $raw = false): HTMLElement
303
    {
304
        $this->setContent($content, false, $raw, true);
305
        return $this;
306
    }
307
308
    /**
309
     * Find and return elements using selector
310
     * @param string $selector A selector of elements based in jQuery
311
     *						'eee' - Select elements 'eee' (with tag)
312
     * 						'#ii' - Select a element with id attribute 'ii'
313
     * 						'.cc' - Select elements with class attribute 'cc'
314
     * 						'[a=v]' - Select elements with 'a' attribute with 'v' value
315
     * 						'e#i' - Select elements 'e' with id attribute 'i'
316
     * 						'e.c' - Select elements 'e' with class attribute 'c'
317
     * @return HTMLElement[]
318
     */
319 1
    public function get(string $selector)
320
    {
321 1
        $tag = null;
322 1
        $attr = null;
323 1
        $val = null;
324
325
        // Define what could be found
326 1
        $selector = trim($selector);
327 1
        if ($selector[0] == "#") {
328 1
            $attr = "id";
329 1
            $val = mb_substr($selector, 1);
330 1
        } elseif ($selector[0] == ".") {
331 1
            $attr = "class";
332 1
            $val = mb_substr($selector, 1);
333 1
        } elseif (mb_strpos($selector, "=") !== false) {
334 1
            list($attr, $val) = explode("=", substr($selector, 1, -1));
335 1
        } elseif (mb_strpos($selector, "#") !== false) {
336 1
            $attr = "id";
337 1
            list($tag, $val) = explode("#", $selector);
338 1
        } elseif (mb_strpos($selector, ".") !== false) {
339 1
            $attr = "class";
340 1
            list($tag, $val) = explode(".", $selector);
341
        } else {
342 1
            $tag = $selector;
343
        }
344
345 1
        return $this->getInternal($this, $tag, $attr, $val);
346
    }
347
348
    /**
349
     * Find and return elements based in $tag, $attr, $val
350
     * The $tag or $attr must be a value
351
     *
352
     * @see HTMLElement::get
353
     * @param string $tag tag of search
354
     * @param string $attr attribute of search
355
     * @param string $val value of attribute search
356
     * @return HTMLElement[]
357
     */
358 1
    public function getElements(string $tag, string $attr, string $val)
359
    {
360 1
        return $this->getInternal($this, $tag, $attr, $val);
361
    }
362
363
    /**
364
     * Recursive function to found elements
365
     * @param HTMLElement $element Element that will be available
366
     * @param string $tag Tag or null value to compare
367
     * @param string $attr Attribute name or null value to compare
368
     * @param string $val Value of attribute or null value to compare
369
     * @return HTMLElement[]
370
     */
371 1
    protected function getInternal(HTMLElement $element, string $tag = null, string $attr = null, string $val = null)
372
    {
373 1
        if ($this->match($element, $tag, $attr, $val)) {
374 1
            $return = [$element];
375
        } else {
376 1
            $return = [];
377
        }
378
379 1
        foreach ($element->getContent() as $content) {
380 1
            if ($content instanceof HTMLElement) {
381 1
                $return = array_merge($return, $this->getInternal($content, $tag, $attr, $val));
382
            }
383
        }
384
385 1
        return $return;
386
    }
387
388
    /**
389
     * Return a boolean based on match of the element with $tag, $attr or $val
390
     * @param HTMLElement $element Element that will be available
391
     * @param string $tag Tag or null value to compare
392
     * @param string $attr Attribute name or null value to compare
393
     * @param string $val Value of attribute or null value to compare
394
     * @return boolean - true when satisfy and false otherwise
395
     */
396 1
    protected function match(HTMLElement $element, $tag, $attr, $val): bool
397
    {
398 1
        if (!empty($tag)) {
399 1
            if ($element->getTag() != $tag) {
400 1
                return false;
401
            }
402
        } elseif (empty($attr)) {
403
            // Only when $tag and $attr is empty
404 1
            return false;
405
        }
406
407 1
        if (!empty($attr)) {
408 1
            $values = $element->getAttribute($attr);
409 1
            if (count($values) == 0) {
410 1
                return false;
411
            }
412
413 1
            if (!empty($val)) {
414 1
                return in_array($val, $values);
415
            }
416
        }
417
418 1
        return true;
419
    }
420
421
    public function __toString()
422
    {
423
        return $this->getRenderHTML();
424
    }
425
    
426
    /**
427
     * Return the html element code including all children
428
     *
429
     * @param string $indentString String used to indent HTML code. Use '' for a compact version.
430
     * @param integer $level The current indentation level.
431
     * @return string (html code)
432
     */
433 5
    public function getRenderHTML($indentString = '  ', $level = 0): string
434
    {
435
436
        // skip empty non renderable
437 5
        if ($this->renderIfEmpty === false) {
438
            if (!count($this->content)) {
439
                return '';
440
            }
441
        }
442
443
        // start
444 5
        $data = [];
445
446
        // if this is not empty, the tag
447 5
        if (!empty($this->tag)) {
448 5
            $open = [];
449 5
            $open[] = ($level > 0 ? $indentString : '') . // initial indentation
450 5
                '<' . htmlspecialchars($this->tag);
451
452
            // render tag attributes
453 5
            foreach ($this->attributes as $atrib => $value) {
454 4
                $open[] = $atrib . '="' . htmlspecialchars(implode(' ', $value)) . '"';
455
            }
456 5
            $data[] = join(' ', $open) . (in_array($this->tag, self::STANDALONE_TAGS) ? '/>' : '>');
457
        }
458
459
        // recurse
460 5
        $contentdata = [];
461 5
        $emptyfieldset = ($this->tag == 'fieldset'); // avoid rendering fieldset with only a "legend"
462 5
        foreach ($this->content as $content) {
463 2
            if ($content instanceof HTMLElement) {
464 1
                $c = $content->getRenderHTML($indentString, $level + 1);
465 1
                if ($this->tag == 'fieldset' and $content->getTag() != 'legend' and $c) {
466
                    $emptyfieldset = false;
467
                }
468 1
                $contentdata[] = $c;
469
            } else {
470 2
                $emptyfieldset = false;
471 2
                $contentdata[] = $indentString . $content;
472
            }
473
        }
474
475
        // handle special empty cases
476 5
        if ($emptyfieldset) {
477
            return '';
478 5
        } elseif ($contentdata == [] && $this->renderIfEmpty === false) {
479
            return '';
480
        }
481
482
        // join content
483 5
        $data = array_merge($data, $contentdata);
484
485
        // handle closing
486 5
        if (!empty($this->tag)
487 5
            && !in_array($this->tag, self::STANDALONE_TAGS)
488
        ) {
489 5
            $data[] = '</' . htmlspecialchars($this->tag) . '>';
490
        }
491
492 5
        if ($indentString && $level === 0) {
493
            $data[] = "\n";
494
        }
495
496 5
        return implode(($indentString ? "\n" : '') . str_repeat($indentString, $level), $data);
497
    }
498
499
    /**
500
     * Clone HTMLElement object and its child
501
     * @return Object HTMLElement
502
     */
503
    public function __clone()
504
    {
505
        $obj = new HTMLElement($this->tag, $this->attributes);
506
        foreach ($this->content as $content) {
507
            if ($content instanceof HTMLElement) {
508
                $obj->addContent(clone $content);
509
            } else {
510
                $obj->addContent($content);
511
            }
512
        }
513
    }
514
515
    public function replace(HTMLElement $e): void
516
    {
517
        $this->tag = $e->tag;
518
        $this->attributes = $e->attributes;
519
        $this->content = $e->content;
520
    }
521
522
    /**
523
     * Clear All Attributes
524
     */
525
    public function clearAttributes(): HTMLElement
526
    {
527
        $this->attributes = [];
528
        return $this;
529
    }
530
531
532
    /**
533
     * Clear All Content
534
     */
535 1
    public function clearContent(): HTMLElement
536
    {
537 1
        $this->content = [];
538 1
        return $this;
539
    }
540
541
    /**
542
     * Similar to array_walk(). Applied to this HTMLElement and all its children.
543
     * Does not call callback for text content.
544
     *
545
     * @param callable $f
546
     * @return HTMLElement self
547
     */
548 1
    public function walk(callable $f): HTMLElement
549
    {
550 1
        $f($this);
551 1
        foreach ($this->content as $content) {
552 1
            if ($content instanceof HTMLElement) {
553 1
                $content->walk($f);
554
            }
555
        }
556 1
        return $this;
557
    }
558
559
    /**
560
     * Similar to array_map(). Calls callback for text content too.
561
     *
562
     * @param callable $f
563
     * @return array
564
     */
565 1
    public function map(callable $f, bool $recurse = true): array
566
    {
567 1
        $data = [$f($this)];
568 1
        foreach ($this->content as $content) {
569 1
            if ($recurse && $content instanceof HTMLElement) {
570 1
                $data = array_merge($data, $content->map($f, $recurse));
571
            } else {
572 1
                $data[] = $f($content);
573
            }
574
        }
575 1
        return $data;
576
    }
577
578
    /**
579
     * Similar to array_filter(): removes children from this element.
580
     * Does not call callback for text content.
581
     *
582
     * @param callable $f If returns false, element being checked is removed.
583
     * @return HTMLElement[] The filtered elements.
584
     */
585 1
    public function filter(callable $f, bool $recurse = true): array
586
    {
587 1
        $deleted = [];
588 1
        foreach ($this->content as $key => $content) {
589 1
            if ($content instanceof HTMLElement) {
590 1
                if (!$f($content)) {
591 1
                    $deleted[] = $this->content[$key];
592 1
                    unset($this->content[$key]);
593
                } elseif ($recurse) {
594 1
                    $content->filter($f, $recurse);
595
                }
596
            }
597
        }
598 1
        return $deleted;
599
    }
600
}
601