Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.35% |
179 / 182 |
|
96.00% |
24 / 25 |
CRAP | |
0.00% |
0 / 1 |
Child | |
98.35% |
179 / 182 |
|
96.00% |
24 / 25 |
94 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
create | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseString | |
100.00% |
60 / 60 |
|
100.00% |
1 / 1 |
26 | |||
parseFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getNodeName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNodeValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNodeContent | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getTextContent | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
setNodeName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setNodeValue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addNodeValue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setAsCData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isCData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setAttribute | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setAttributes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
hasAttribute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasAttributes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAttribute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAttributes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeAttribute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isChildrenFirst | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setChildrenFirst | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
preserveWhiteSpace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
render | |
94.74% |
54 / 57 |
|
0.00% |
0 / 1 |
35.18 | |||
__toString | |
100.00% |
1 / 1 |
|
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 | */ |
14 | namespace Pop\Dom; |
15 | |
16 | use 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 | */ |
28 | class 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 | } |