1 <?php
2 
3 namespace ComboStrap\Xml;
4 
5 use ComboStrap\ArrayCaseInsensitive;
6 use ComboStrap\ArrayUtility;
7 use ComboStrap\ExceptionBadArgument;
8 use ComboStrap\ExceptionBadSyntax;
9 use ComboStrap\ExceptionNotEquals;
10 use ComboStrap\ExceptionNotFound;
11 use ComboStrap\ExceptionRuntime;
12 use ComboStrap\Html;
13 use ComboStrap\StringUtility;
14 use ComboStrap\TagAttribute\StyleAttribute;
15 use ComboStrap\Web\Url;
16 use ComboStrap\Xml\XmlSystems;
17 use DOMElement;
18 use DOMText;
19 
20 class XmlElement
21 {
22 
23     private DOMElement $domElement;
24     private XmlDocument $document;
25     private array $styleDeclaration = [];
26 
27     /**
28      * @param DOMElement $domElement - the dom element wrapped
29      * @param XmlDocument $document - the document
30      */
31     public function __construct(DOMElement $domElement, XmlDocument $document)
32     {
33         $this->domElement = $domElement;
34         $this->document = $document;
35 
36     }
37 
38     public static function create($domElement, XmlDocument $xmlDocument): XmlElement
39     {
40         return new XmlElement($domElement, $xmlDocument);
41     }
42 
43     public function getAttribute(string $qualifiedName): string
44     {
45         return $this->domElement->getAttribute($qualifiedName);
46     }
47 
48     public function getClass(): string
49     {
50         return $this->domElement->getAttribute("class");
51     }
52 
53     /**
54      * @throws ExceptionNotFound
55      */
56     public function getFirstChildElement(): XmlElement
57     {
58         $firstChild = $this->domElement->firstChild;
59         while (!($firstChild instanceof DOMElement)) {
60             if ($firstChild === null) {
61                 throw new ExceptionNotFound("No first child element");
62             }
63             $firstChild = $firstChild->nextSibling;
64         }
65         return new XmlElement($firstChild, $this->document);
66     }
67 
68     /**
69      * @return XmlElement[]
70      */
71     public function getChildrenElement(): array
72     {
73         $childNodes = [];
74         foreach ($this->domElement->childNodes as $childNode) {
75             if ($childNode instanceof DOMElement) {
76                 $childNodes[] = new XmlElement($childNode, $this->document);
77             }
78         }
79         return $childNodes;
80     }
81 
82     /**
83      * @return array - the node text values in an array
84      */
85     public function getChildrenNodeTextValues(): array
86     {
87         $childNodes = [];
88         foreach ($this->domElement->childNodes as $childNode) {
89             if ($childNode instanceof DOMText) {
90                 $childNodes[] = $childNode->nodeValue;
91             } else {
92                 $childNodes[] = implode('', XmlElement::create($childNode, $this->document)->getChildrenNodeTextValues());
93             }
94         }
95         return $childNodes;
96     }
97 
98     /**
99      * @return XmlElement[]
100      * @throws ExceptionBadSyntax
101      */
102     public function querySelectorAll(string $selector): array
103     {
104         $xpath = $this->document->cssSelectorToXpath($selector);
105         $nodes = [];
106         foreach ($this->document->xpath($xpath, $this->domElement) as $child) {
107             if ($child instanceof DOMElement) {
108                 $nodes[] = new XmlElement($child, $this->document);
109             }
110         }
111         return $nodes;
112     }
113 
114     public function getXmlTextNormalized(): string
115     {
116 
117         return $this->document->toXmlNormalized($this->domElement);
118 
119     }
120 
121     public function removeAttribute($attributeName): XmlElement
122     {
123         $attr = $this->domElement->getAttributeNode($attributeName);
124         if ($attr == false) {
125             return $this;
126         }
127         $result = $this->domElement->removeAttributeNode($attr);
128         if ($result === false) {
129             throw new ExceptionRuntime("Not able to delete the attribute $attributeName of the node element {$this->domElement->tagName} in the Xml document");
130         }
131         return $this;
132     }
133 
134     public function remove(): XmlElement
135     {
136         $this->domElement->parentNode->removeChild($this->domElement);
137         return $this;
138     }
139 
140     public function getStyle(): string
141     {
142         return $this->domElement->getAttribute("style");
143     }
144 
145     public function getNodeValue()
146     {
147         return $this->domElement->nodeValue;
148     }
149 
150     /**
151      * @throws ExceptionBadSyntax
152      * @throws ExceptionNotFound
153      */
154     public function querySelector(string $selector): XmlElement
155     {
156         $domNodeList = $this->querySelectorAll($selector);
157         if (sizeof($domNodeList) >= 1) {
158             return $domNodeList[0];
159         }
160         throw new ExceptionNotFound("No element was found with the selector $selector");
161     }
162 
163     public function getLocalName()
164     {
165         return $this->domElement->localName;
166     }
167 
168     public function addClass(string $class): XmlElement
169     {
170         $classes = Html::mergeClassNames($class, $this->getClass());
171         $this->domElement->setAttribute("class", $classes);
172         return $this;
173     }
174 
175     public function setAttribute(string $name, string $value): XmlElement
176     {
177         $this->domElement->setAttribute($name, $value);
178         return $this;
179     }
180 
181     public function hasAttribute(string $name): bool
182     {
183         return $this->domElement->hasAttribute($name);
184     }
185 
186     public function getDomElement(): DOMElement
187     {
188         return $this->domElement;
189     }
190 
191     /**
192      * Append a text node as a child
193      * @param string $string - the text
194      * @param string $position - the position on where to insert the text node
195      * @return $this
196      * @throws ExceptionBadArgument - if the text is not a valid text xml expression
197      */
198     public function insertAdjacentTextNode(string $string, string $position = 'afterbegin'): XmlElement
199     {
200         $textNode = $this->domElement->ownerDocument->createTextNode($string);
201         $this->insertAdjacentDomElement($position, $textNode);
202         return $this;
203     }
204 
205     public function toHtml()
206     {
207         return $this->domElement->ownerDocument->saveHTML($this->domElement);
208     }
209 
210     public function toXhtml()
211     {
212         return $this->domElement->ownerDocument->saveXML($this->domElement);
213     }
214 
215     public function getNodeValueWithoutCdata()
216     {
217         return XmlSystems::extractTextWithoutCdata($this->getNodeValue());
218     }
219 
220     /**
221      * @throws ExceptionBadSyntax
222      * @throws ExceptionBadArgument
223      */
224     public function insertAdjacentHTML(string $position, string $html): XmlElement
225     {
226         $externalElement = XmlDocument::createHtmlDocFromMarkup($html)->getElement()->getDomElement();
227         // import/copy item from external document to internal document
228         $internalElement = $this->importIfExternal($externalElement);
229         $this->insertAdjacentDomElement($position, $internalElement);
230         return $this;
231     }
232 
233 
234     public function getId(): string
235     {
236         return $this->getAttribute("id");
237     }
238 
239     public function hasChildrenElement(): bool
240     {
241         return sizeof($this->getChildrenElement()) !== 0;
242     }
243 
244     public function getAttributeOrDefault(string $string, string $default): string
245     {
246         if (!$this->hasAttribute($string)) {
247             return $default;
248         }
249         return $this->getAttribute($string);
250 
251     }
252 
253     public function appendChild(XmlElement $xmlElement): XmlElement
254     {
255         $element = $this->importIfExternal($xmlElement->domElement);
256         $this->domElement->appendChild($element);
257         return $this;
258     }
259 
260     public function getDocument(): XmlDocument
261     {
262         return $this->document;
263     }
264 
265     public function setNodeValue(string $nodeValue)
266     {
267         $this->domElement->nodeValue = $nodeValue;
268     }
269 
270     public function addStyle(string $name, string $value): XmlElement
271     {
272         ArrayUtility::addIfNotSet($this->styleDeclaration, $name, $value);
273         $this->setAttribute("style", Html::array2InlineStyle($this->styleDeclaration));
274         return $this;
275     }
276 
277     /**
278      *
279      * Utility to change the owner document
280      * otherwise you get an error:
281      * ```
282      * DOMException : Wrong Document Error
283      * ```
284      *
285      * @param DOMElement $domElement
286      * @return DOMElement
287      */
288     private function importIfExternal(DOMElement $domElement): DOMElement
289     {
290         if ($domElement->ownerDocument !== $this->getDocument()->getDomDocument()) {
291             return $this->getDocument()->getDomDocument()->importNode($domElement, true);
292         }
293         return $domElement;
294     }
295 
296     /**
297      * @throws ExceptionNotEquals
298      */
299     public function equals(XmlElement $rightDocument, array $attributeFilter = [])
300     {
301         $error = "";
302         XmlSystems::diffNode(
303             $this->domElement,
304             $rightDocument->domElement,
305             $error,
306             $attributeFilter
307         );
308         if ($error !== null) {
309             throw new ExceptionNotEquals($error);
310         }
311     }
312 
313     public function removeClass(string $string): XmlElement
314     {
315         $class = $this->getClass();
316         $newClass = str_replace($string, "", $class);
317         $this->setAttribute("class", $newClass);
318         return $this;
319     }
320 
321     /**
322      * @throws ExceptionNotFound
323      */
324     public function getParent(): XmlElement
325     {
326         $parentNode = $this->domElement->parentNode;
327         if ($parentNode !== null) {
328             while (!($parentNode instanceof DOMElement)) {
329                 $parentNode = $parentNode->parentNode;
330                 if ($parentNode === null) {
331                     break;
332                 }
333             }
334         }
335         if ($parentNode === null) {
336             throw new ExceptionNotFound("No parent node found");
337         }
338         return new XmlElement($parentNode, $this->document);
339     }
340 
341     /**
342      * @param string $position
343      * @param \DOMNode $domNode - ie {@Link \DOMElement} or {@link \DOMNode}
344      * @return XmlElement
345      * @throws ExceptionBadArgument
346      */
347     public function insertAdjacentDomElement(string $position, \DOMNode $domNode): XmlElement
348     {
349         switch ($position) {
350             case 'beforeend':
351                 $this->domElement->appendChild($domNode);
352                 return $this;
353             case 'afterbegin':
354                 $firstChild = $this->domElement->firstChild;
355                 if ($firstChild === null) {
356                     $this->domElement->appendChild($domNode);
357                 } else {
358                     // The object on which you actually call the insertBefore()
359                     // on the parent node of the reference node
360                     // otherwise you get a `not found`
361                     // https://www.php.net/manual/en/domnode.insertbefore.php#53506
362                     $firstChild->parentNode->insertBefore($domNode, $firstChild);
363                 }
364                 return $this;
365             case 'beforebegin':
366                 $this->domElement->parentNode->insertBefore($domNode, $this->domElement);
367                 return $this;
368             default:
369                 throw new ExceptionBadArgument("The position ($position) is unknown");
370         }
371     }
372 
373     public function getInnerText(): string
374     {
375         if ($this->hasChildrenElement()) {
376             return implode('', $this->getChildrenNodeTextValues());
377         } else {
378             return $this->domElement->nodeValue;
379         }
380     }
381 
382     public function getInnerTextWithoutCdata()
383     {
384         return XmlSystems::extractTextWithoutCdata($this->getInnerText());
385     }
386 
387     public function getStyleProperties(): ArrayCaseInsensitive
388     {
389         $source = StyleAttribute::HtmlStyleValueToArray($this->getStyle());
390         return new ArrayCaseInsensitive($source);
391     }
392 
393     public function getStyleProperty(string $property): string
394     {
395         return $this->getStyleProperties()[$property];
396     }
397 
398     /**
399      * @throws ExceptionBadSyntax
400      * @throws ExceptionBadArgument
401      */
402     public function getAttributeAsUrl(string $attributeName): Url
403     {
404         $value = $this->getAttribute($attributeName);
405         if (empty($value)) {
406             return Url::createEmpty();
407         }
408         return Url::createFromString($value);
409     }
410 
411     public function __toString()
412     {
413         $toString = $this->getLocalName();
414         $class = $this->getClass();
415         if ($class !== "") {
416             $classes = StringUtility::explodeAndTrim($class, " ");
417             $toString .= '.' . implode(".", $classes);
418         }
419         if ($this->getId() !== "") {
420             $toString .= "#" . $this->getId();
421         }
422         return $toString;
423     }
424 
425     public function hasClass(string $needleClass): bool
426     {
427         $classes = preg_split("/\s/", $this->getClass());
428         if (in_array($needleClass, $classes)) {
429             return true;
430         }
431         return false;
432     }
433 
434 
435 }
436