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