Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.35% covered (success)
98.35%
179 / 182
96.00% covered (success)
96.00%
24 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
Child
98.35% covered (success)
98.35%
179 / 182
96.00% covered (success)
96.00%
24 / 25
94
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseString
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
1 / 1
26
 parseFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getNodeName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNodeValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNodeContent
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getTextContent
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 setNodeName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setNodeValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addNodeValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAsCData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isCData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAttribute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAttributes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttributes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeAttribute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isChildrenFirst
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setChildrenFirst
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 preserveWhiteSpace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render
94.74% covered (success)
94.74%
54 / 57
0.00% covered (danger)
0.00%
0 / 1
35.18
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Dom;
15
16use RecursiveIteratorIterator;
17
18/**
19 * Dom child class
20 *
21 * @category   Pop
22 * @package    Pop\Dom
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    4.0.0
27 */
28class Child extends AbstractNode
29{
30
31    /**
32     * Child element node name
33     * @var ?string
34     */
35    protected ?string $nodeName = null;
36
37    /**
38     * Child element node value
39     * @var ?string
40     */
41    protected ?string $nodeValue = null;
42
43    /**
44     * Child element node value CDATA flag
45     * @var bool
46     */
47    protected bool $cData = false;
48
49    /**
50     * Flag to render children before node value or not
51     * @var bool
52     */
53    protected bool $childrenFirst = false;
54
55    /**
56     * Child element attributes
57     * @var array
58     */
59    protected array $attributes = [];
60
61    /**
62     * Flag to preserve whitespace
63     * @var bool
64     */
65    protected bool $preserveWhiteSpace = true;
66
67    /**
68     * Constructor
69     *
70     * Instantiate the DOM element object
71     *
72     * @param  string  $name
73     * @param  ?string $value
74     * @param  array   $options
75     */
76    public function __construct(string $name, ?string $value = null, array $options = [])
77    {
78        $this->nodeName  = $name;
79        $this->nodeValue = $value;
80
81        if (isset($options['cData'])) {
82            $this->cData = (bool)$options['cData'];
83        }
84        if (isset($options['childrenFirst'])) {
85            $this->childrenFirst = (bool)$options['childrenFirst'];
86        }
87        if (isset($options['indent'])) {
88            $this->indent = (string)$options['indent'];
89        }
90        if (isset($options['attributes'])) {
91            $this->setAttributes($options['attributes']);
92        }
93        if (isset($options['whitespace'])) {
94            $this->preserveWhiteSpace($options['whitespace']);
95        }
96    }
97
98    /**
99     * Static factory method to create a child object
100     *
101     * @param  string  $name
102     * @param  ?string $value
103     * @param  array   $options
104     * @return Child
105     */
106    public static function create(string $name, ?string $value = null, array $options = []): Child
107    {
108        return new self($name, $value, $options);
109    }
110
111    /**
112     * Static method to parse an XML/HTML string
113     *
114     * @param  string $string
115     * @return Child|array
116     */
117    public static function parseString(string $string): Child|array
118    {
119        $doc = new \DOMDocument();
120        $doc->loadHTML($string);
121
122        $dit = new RecursiveIteratorIterator(
123            new DomIterator($doc),
124            RecursiveIteratorIterator::SELF_FIRST
125        );
126
127        $parent     = null;
128        $child      = null;
129        $lastDepth  = 0;
130        $endElement = null;
131        $partial    = ((stripos($string, '<html') === false) || (stripos($string, '<body') === false));
132
133        foreach($dit as $node) {
134            if (($node->nodeType == XML_ELEMENT_NODE) || ($node->nodeType == XML_TEXT_NODE)) {
135                $attribs = [];
136                if ($node->attributes !== null) {
137                    for ($i = 0; $i < $node->attributes->length; $i++) {
138                        $name = $node->attributes->item($i)->name;
139                        $attribs[$name] = $node->getAttribute($name);
140                    }
141                }
142                if ($parent === null) {
143                    $parent = new Child($node->nodeName);
144                } else {
145                    if (($node->nodeType == XML_TEXT_NODE) && ($child !== null)) {
146                        $nodeValue = trim($node->nodeValue);
147                        if (!empty($nodeValue)) {
148                            if (($endElement) && ($child->getParent() !== null) && ($node->previousSibling !== null)) {
149                                $prev = $node->previousSibling->nodeName;
150                                $par  = $child->getParent();
151                                while (($par !== null) && ($prev != $par->getNodeName())) {
152                                    $par = $par->getParent();
153                                }
154                                if ($par === null) {
155                                    $par = $child->getParent();
156                                } else {
157                                    $par = $par->getParent();
158                                }
159                                $par->addChild(new Child('#text', $nodeValue));
160                            } else {
161                                $child->setNodeValue($nodeValue);
162                                $endElement = true;
163                            }
164                        }
165                    } else {
166                        // down
167                        if ($dit->getDepth() > $lastDepth) {
168                            if ($child !== null) {
169                                $parent = $child;
170                            }
171                            $child  = new Child($node->nodeName);
172                            $parent->addChild($child);
173                            $endElement = false;
174                        // up
175                        } else if ($dit->getDepth() < $lastDepth) {
176                            while ($parent->getNodeName() != $node->parentNode->nodeName) {
177                                $parent = $parent->getParent();
178                            }
179                            //$parent = $parent->getParent();
180                            $child  = new Child($node->nodeName);
181                            $parent->addChild($child);
182                            $endElement = false;
183                            // next (sibling)
184                        } else if ($dit->getDepth() == $lastDepth) {
185                            $child  = new Child($node->nodeName);
186                            $parent->addChild($child);
187                            $endElement = false;
188                        }
189                        if (!empty($attribs)) {
190                            $child->setAttributes($attribs);
191                        }
192                        $lastDepth = $dit->getDepth();
193                    }
194                }
195            }
196        }
197        while ($parent->getParent() !== null) {
198            $parent = $parent->getParent();
199        }
200
201        if ($partial) {
202            $parent = $parent->getChild(0);
203            if (strtolower($parent->getNodeName()) == 'body') {
204                $parent = $parent->getChildNodes();
205            }
206        }
207
208        return $parent;
209    }
210
211    /**
212     * Static method to parse an XML/HTML string from a file
213     *
214     * @param  string $file
215     * @throws Exception
216     * @return Child
217     */
218    public static function parseFile(string $file): Child
219    {
220        if (!file_exists($file)) {
221            throw new Exception('Error: That file does not exist.');
222        }
223        return self::parseString(file_get_contents($file));
224    }
225
226    /**
227     * Return the child node name
228     *
229     * @return string|null
230     */
231    public function getNodeName(): string|null
232    {
233        return $this->nodeName;
234    }
235
236    /**
237     * Return the child node value
238     *
239     * @return string|null
240     */
241    public function getNodeValue(): string|null
242    {
243        return $this->nodeValue;
244    }
245
246    /**
247     * Return the child node content, including tags, etc
248     *
249     * @param  bool $ignoreWhiteSpace
250     * @return string
251     */
252    public function getNodeContent(bool $ignoreWhiteSpace = false): string
253    {
254        $content = $this->render(0, null, true);
255        if ($ignoreWhiteSpace) {
256            $content = preg_replace('/\s+/', ' ', str_replace(["\n", "\r", "\t"], ["", "", ""], trim($content)));
257            $content = preg_replace('/\s*\.\s*/', '. ', $content);
258            $content = preg_replace('/\s*\?\s*/', '? ', $content);
259            $content = preg_replace('/\s*\!\s*/', '! ', $content);
260            $content = preg_replace('/\s*,\s*/', ', ', $content);
261            $content = preg_replace('/\s*\:\s*/', ': ', $content);
262            $content = preg_replace('/\s*\;\s*/', '; ', $content);
263        }
264        return $content;
265    }
266
267    /**
268     * Return the child node content, including tags, etc
269     *
270     * @param  bool $ignoreWhiteSpace
271     * @return string
272     */
273    public function getTextContent(bool $ignoreWhiteSpace = false): string
274    {
275        $content = strip_tags($this->render(0, null, true));
276
277        if ($ignoreWhiteSpace) {
278            $content = preg_replace('/\s+/', ' ', str_replace(["\n", "\r", "\t"], ["", "", ""], trim($content)));
279            $content = preg_replace('/\s*\.\s*/', '. ', $content);
280            $content = preg_replace('/\s*\?\s*/', '? ', $content);
281            $content = preg_replace('/\s*\!\s*/', '! ', $content);
282            $content = preg_replace('/\s*,\s*/', ', ', $content);
283            $content = preg_replace('/\s*\:\s*/', ': ', $content);
284            $content = preg_replace('/\s*\;\s*/', '; ', $content);
285        }
286        return $content;
287    }
288
289    /**
290     * Set the child node name
291     *
292     * @param  string $name
293     * @return Child
294     */
295    public function setNodeName(string $name): Child
296    {
297        $this->nodeName = $name;
298        return $this;
299    }
300
301    /**
302     * Set the child node value
303     *
304     * @param  string $value
305     * @return Child
306     */
307    public function setNodeValue(string $value): Child
308    {
309        $this->nodeValue = $value;
310        return $this;
311    }
312
313    /**
314     * Add to the child node value
315     *
316     * @param  string $value
317     * @return Child
318     */
319    public function addNodeValue(string $value): Child
320    {
321        $this->nodeValue .= $value;
322        return $this;
323    }
324
325    /**
326     * Set the child node value as CDATA
327     *
328     * @param  bool $cData
329     * @return Child
330     */
331    public function setAsCData(bool $cData = true): Child
332    {
333        $this->cData = $cData;
334        return $this;
335    }
336
337    /**
338     * Determine if the child node value is CDATA
339     *
340     * @return bool
341     */
342    public function isCData(): bool
343    {
344        return $this->cData;
345    }
346
347    /**
348     * Set an attribute for the child element object
349     *
350     * @param  string $a
351     * @param  string $v
352     * @return Child
353     */
354    public function setAttribute(string $a, string $v): Child
355    {
356        $this->attributes[$a] = $v;
357        return $this;
358    }
359
360    /**
361     * Set an attribute or attributes for the child element object
362     *
363     * @param  array $a
364     * @return Child
365     */
366    public function setAttributes(array $a): Child
367    {
368        foreach ($a as $name => $value) {
369            $this->attributes[$name] = $value;
370        }
371        return $this;
372    }
373
374    /**
375     * Determine if the child object has an attribute
376     *
377     * @param  string $name
378     * @return bool
379     */
380    public function hasAttribute(string $name): bool
381    {
382        return (isset($this->attributes[$name]));
383    }
384
385    /**
386     * Determine if the child object has attributes
387     *
388     * @return bool
389     */
390    public function hasAttributes(): bool
391    {
392        return (count($this->attributes) > 0);
393    }
394
395    /**
396     * Get the attribute of the child object
397     *
398     * @param  string $name
399     * @return string|null
400     */
401    public function getAttribute(string $name): string|null
402    {
403        return $this->attributes[$name] ?? null;
404    }
405
406    /**
407     * Get the attributes of the child object
408     *
409     * @return array
410     */
411    public function getAttributes(): array
412    {
413        return $this->attributes;
414    }
415
416    /**
417     * Remove an attribute from the child element object
418     *
419     * @param  string $a
420     * @return Child
421     */
422    public function removeAttribute(string $a): Child
423    {
424        if (isset($this->attributes[$a])) {
425            unset($this->attributes[$a]);
426        }
427        return $this;
428    }
429
430    /**
431     * Determine if child nodes render first, before the node value
432     *
433     * @return bool
434     */
435    public function isChildrenFirst(): bool
436    {
437        return $this->childrenFirst;
438    }
439
440    /**
441     * Set whether child nodes render first, before the node value
442     *
443     * @param  bool $first
444     * @return Child
445     */
446    public function setChildrenFirst(bool $first = true): Child
447    {
448        $this->childrenFirst = $first;
449        return $this;
450    }
451
452    /**
453     * Set whether to preserve whitespace
454     *
455     * @param  bool $preserve
456     * @return Child
457     */
458    public function preserveWhiteSpace(bool $preserve = true): Child
459    {
460        $this->preserveWhiteSpace = $preserve;
461        return $this;
462    }
463
464    /**
465     * Render the child and its child nodes.
466     *
467     * @param  int     $depth
468     * @param  ?string $indent
469     * @param  bool    $inner
470     * @return string|null
471     */
472    public function render(int $depth = 0, ?string $indent = null, bool $inner = false): string|null
473    {
474        // Initialize child object properties and variables.
475        $this->output = '';
476        $this->indent = ($this->indent === null) ? str_repeat('    ', $depth) : $this->indent;
477        $attribs      = '';
478        $attribAry    = [];
479
480        if ($this->cData) {
481            $this->nodeValue = '<![CDATA[' . $this->nodeValue . ']]>';
482        }
483
484        // Format child attributes, if applicable.
485        if ($this->hasAttributes()) {
486            $attributes = $this->getAttributes();
487            foreach ($attributes as $key => $value) {
488                $attribAry[] = $key . "=\"" . $value . "\"";
489            }
490            $attribs = ' ' . implode(' ', $attribAry);
491        }
492
493        // Initialize the node.
494        if ($this->nodeName == '#text') {
495            $this->output .= ((!$this->preserveWhiteSpace) ?
496                    '' : "{$indent}{$this->indent}") . $this->nodeValue . ((!$this->preserveWhiteSpace) ? '' : "\n");
497        } else {
498            if (!$inner) {
499                $this->output .= ((!$this->preserveWhiteSpace) ?
500                        '' : "{$indent}{$this->indent}") . "<{$this->nodeName}{$attribs}";
501            }
502
503            if (($indent === null) && ($this->indent !== null)) {
504                $indent     = $this->indent;
505                $origIndent = $this->indent;
506            } else {
507                $origIndent = $indent . $this->indent;
508            }
509
510            // If current child element has child nodes, format and render.
511            if (count($this->childNodes) > 0) {
512                if (!$inner) {
513                    $this->output .= ">";
514                    if ($this->preserveWhiteSpace) {
515                        $this->output .= "\n";
516                    }
517                }
518                $newDepth = $depth + 1;
519
520                // Render node value before the child nodes.
521                if (!$this->childrenFirst) {
522                    if ($this->nodeValue !== null) {
523                        $this->output .= ((!$this->preserveWhiteSpace) ?
524                                '' : str_repeat('    ', $newDepth) . "{$indent}") . "{$this->nodeValue}\n";
525                    }
526                    foreach ($this->childNodes as $child) {
527                        $this->output .= $child->render($newDepth, $indent);
528                    }
529                    if (!$inner) {
530                        if (!$this->preserveWhiteSpace) {
531                            $this->output .= "</{$this->nodeName}>";
532                        } else {
533                            $this->output .= "{$origIndent}</{$this->nodeName}>\n";
534                        }
535                    }
536                // Else, render child nodes first, then node value.
537                } else {
538                    foreach ($this->childNodes as $child) {
539                        $this->output .= $child->render($newDepth, $indent);
540                    }
541                    if (!$inner) {
542                        if ($this->nodeValue !== null) {
543                            $this->output .= ((!$this->preserveWhiteSpace) ?
544                                    '' : str_repeat('    ', $newDepth) . "{$indent}") .
545                                "{$this->nodeValue}" . ((!$this->preserveWhiteSpace) ?
546                                    '' : "\n{$origIndent}") . "</{$this->nodeName}>" . ((!$this->preserveWhiteSpace) ? '' : "\n");
547                        } else {
548                            $this->output .= ((!$this->preserveWhiteSpace) ?
549                                    '' : "{$origIndent}") . "</{$this->nodeName}>" . ((!$this->preserveWhiteSpace) ? '' : "\n");
550                        }
551                    }
552                }
553            // Else, render the child node.
554            } else {
555                if (!$inner) {
556                    if (($this->nodeValue !== null) || ($this->nodeName == 'textarea')) {
557                        $this->output .= ">";
558                        $this->output .= "{$this->nodeValue}</{$this->nodeName}>" . ((!$this->preserveWhiteSpace) ? '' : "\n");
559                    } else {
560                        $this->output .= " />";
561                        if ($this->preserveWhiteSpace) {
562                            $this->output .= "\n";
563                        }
564                    }
565                } else if (!empty($this->nodeValue)) {
566                    $this->output .= $this->nodeValue;
567                }
568            }
569        }
570
571        return $this->output;
572    }
573
574    /**
575     * Render Dom child object to string
576     *
577     * @return string
578     */
579    public function __toString(): string
580    {
581        return $this->render();
582    }
583
584}