Passed
Push — master ( 2802ef...c7d11a )
by Bruno
06:54
created

HTMLElement::clearAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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