1<?php
2
3
4namespace ComboStrap\Xml;
5
6
7use ComboStrap\ExceptionBadArgument;
8use ComboStrap\ExceptionBadSyntax;
9use ComboStrap\ExceptionCompile;
10use ComboStrap\ExceptionNotEquals;
11use ComboStrap\ExceptionRuntime;
12use ComboStrap\Html;
13use ComboStrap\TagAttribute\StyleAttribute;
14use ComboStrap\Web\Url;
15use ComboStrap\Xml\XmlDocument;
16use ComboStrap\Xml\XmlElement;
17use DOMDocument;
18use DOMElement;
19use DOMNode;
20use Exception;
21
22/**
23 *
24 * @package ComboStrap
25 * Static function around the {@link XmlDocument}
26 *
27 *
28 */
29class XmlSystems
30{
31    const OPEN = "open";
32    const CLOSED = "closed";
33    const NORMAL = "normal";
34
35
36    /**
37     * Get a Simple XMl Element and returns it without the XML header (ie as HTML node)
38     * @param DOMDocument $linkDom
39     * @return false|string
40     */
41    public static function asHtml($linkDom)
42    {
43
44        /**
45         * ownerDocument returned the DOMElement
46         */
47        return $linkDom->ownerDocument->saveXML($linkDom->ownerDocument->documentElement);
48    }
49
50    /**
51     * Check of the text is a valid XML
52     * @param $text
53     * @return bool
54     */
55    public static function isXml($text)
56    {
57
58        $valid = true;
59        try {
60            new XmlDocument($text);
61        } catch (\Exception $e) {
62            $valid = false;
63        }
64        return $valid;
65
66
67    }
68
69    /**
70     * Return a formatted HTML
71     * @param $text
72     * @return mixed
73     * DOMDocument supports formatted XML while SimpleXMLElement does not.
74     * @throws \Exception if empty
75     */
76    public static function format($text)
77    {
78        if (empty($text)) {
79            throw new \Exception("The text should not be empty");
80        }
81        $doc = new DOMDocument();
82        $doc->loadXML($text);
83        $doc->normalize();
84        $doc->formatOutput = true;
85        // Type doc can also be reach with $domNode->ownerDocument
86        return $doc->saveXML();
87
88
89    }
90
91    /**
92     * @param $text
93     * @return string
94     * @throws ExceptionBadSyntax
95     */
96    public static function normalize($text)
97    {
98        if (empty($text)) {
99            throw new ExceptionBadSyntax("The text should not be empty");
100        }
101        $xmlDoc = new XmlDocument($text, XmlDocument::XML_TYPE);
102        return $xmlDoc->toXmlNormalized();
103    }
104
105    /**
106     * note: Option for the loading of {@link XmlDocument}
107     * have also this option
108     *
109     * @param $text
110     * @return string|string[]
111     */
112    public static function extractTextWithoutCdata($text)
113    {
114        $text = str_replace("/*<![CDATA[*/", "", $text);
115        $text = str_replace("/*!]]>*/", "", $text);
116        $text = str_replace("\/", "/", $text);
117        return $text;
118    }
119
120    public static function preprocessText($text)
121    {
122
123    }
124
125
126    /**
127     * @param DOMNode $leftNode
128     * @param DOMNode $rightNode
129     * Tip: To get the text of a node:
130     * $leftNode->ownerDocument->saveHTML($leftNode)
131     * @param $error
132     * @param string[]|null $excludedAttributes - the value of this attributes will not be checked
133     */
134    public static function diffNode(DOMNode $leftNode, DOMNode $rightNode, &$error, array $excludedAttributes = null)
135    {
136
137        if ($excludedAttributes === null) {
138            $excludedAttributes = [];
139        }
140        $leftNodeName = $leftNode->localName;
141        $rightNodeName = $rightNode->localName;
142        if ($leftNodeName != $rightNodeName) {
143            $error .= "The node (" . $rightNode->getNodePath() . ") are different (" . $leftNodeName . "," . $rightNodeName . ")\n";
144        }
145        if ($leftNode->hasAttributes()) {
146            $leftAttributesLength = $leftNode->attributes->length;
147            $rightNodeAttributes = $rightNode->attributes;
148            if ($rightNodeAttributes == null) {
149                $error .= "The node (" . $rightNode->getNodePath() . ") have no attributes while the left node has.\n";
150            } else {
151
152                /**
153                 * Collect the attributes by name
154                 */
155                $leftAttributes = array();
156                for ($i = 0; $i < $leftAttributesLength; $i++) {
157                    $leftAtt = $leftNode->attributes->item($i);
158                    $leftAttributes[$leftAtt->nodeName] = $leftAtt;
159                }
160                ksort($leftAttributes);
161                $rightAttributes = array();
162                for ($i = 0; $i < $rightNodeAttributes->length; $i++) {
163                    $rightAtt = $rightNodeAttributes->item($i);
164                    $rightAttributes[$rightAtt->nodeName] = $rightAtt;
165                }
166
167                foreach ($leftAttributes as $leftAttName => $leftAtt) {
168                    /** @var \DOMAttr $leftAtt */
169                    $rightAtt = $rightAttributes[$leftAttName] ?? null;
170                    if ($rightAtt == null) {
171                        if (!in_array($leftAttName, $excludedAttributes)) {
172                            $error .= "The attribute (" . $leftAtt->getNodePath() . ") does not exist on the right side\n";
173                        }
174                        continue;
175                    }
176
177                    unset($rightAttributes[$leftAttName]);
178
179                    /**
180                     * Value check
181                     */
182                    if (in_array($leftAttName, $excludedAttributes)) {
183                        continue;
184                    }
185                    $leftAttValue = $leftAtt->nodeValue;
186                    $rightAttValue = $rightAtt->nodeValue;
187                    if ($leftAttValue !== $rightAttValue) {
188                        switch ($leftAtt->name) {
189                            case "class":
190                                $error .= Html::getDiffBetweenValuesSeparatedByBlank($leftAttValue, $rightAttValue, "left ,{$leftAtt->getNodePath()}", "right, {$leftAtt->getNodePath()}");
191                                break;
192                            case "srcset":
193                            case "data-srcset":
194                                try {
195                                    Html::getDiffBetweenSrcSet($leftAttValue, $rightAttValue);
196                                } catch (ExceptionBadSyntax|ExceptionNotEquals $e) {
197                                    $error .= $e->getMessage();
198                                }
199                                break;
200                            case "src":
201                            case "data-src":
202                            case "href":
203                            case "action": // form
204                                try {
205                                    $leftUrl = Url::createFromString($leftAttValue);
206                                    try {
207                                        $rightUrl = Url::createFromString($rightAttValue);
208                                        try {
209                                            $leftUrl->equals($rightUrl);
210                                        } catch (ExceptionNotEquals $e) {
211                                            $error .= "The attribute (" . $rightAtt->getNodePath() . ") has different values. Error:{$e->getMessage()}\n";
212                                        }
213                                    } catch (ExceptionBadSyntax|ExceptionBadArgument $e) {
214                                        $error .= "The attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . ") and the right value is not an URL. Error:{$e->getMessage()}\n";
215                                    }
216                                } catch (ExceptionBadSyntax|ExceptionBadArgument $e) {
217                                    $error .= "The attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . ") and the left value is not an URL. Error:{$e->getMessage()}\n";
218                                }
219                                break;
220                            case "style":
221                                try {
222                                    StyleAttribute::stringEquals($leftAttValue, $rightAttValue);
223                                } catch (ExceptionNotEquals $e) {
224                                    $error .= "The style attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . "). Error:{$e->getMessage()}\n";
225                                }
226                                break;
227                            default:
228                                $error .= "The attribute (" . $leftAtt->getNodePath() . ") have different values (" . $leftAttValue . "," . $rightAttValue . ")\n";
229                                break;
230                        }
231                    }
232
233                }
234
235                ksort($rightAttributes);
236                foreach ($rightAttributes as $rightAttName => $rightAtt) {
237                    if (!in_array($rightAttName, $excludedAttributes)) {
238                        $error .= "The attribute (" . $rightAttName . ") of the node (" . $rightAtt->getNodePath() . ") does not exist on the left side\n";
239                    }
240                }
241            }
242        } else {
243            if ($rightNode->hasAttributes()) {
244                for ($i = 0; $i < $rightNode->attributes->length; $i++) {
245                    /** @var \DOMAttr $rightAtt */
246                    $rightAtt = $rightNode->attributes->item($i);
247                    $error .= "The attribute (" . $rightAtt->getNodePath() . ") does not exist on the left side\n";
248                }
249            }
250        }
251        if ($leftNode->nodeName == "#text") {
252            $leftNodeValue = trim($leftNode->nodeValue);
253            $rightNodeValue = trim($rightNode->nodeValue);
254            if ($leftNodeValue != $rightNodeValue) {
255                $error .= "The node (" . $rightNode->getNodePath() . ") have different values (" . $leftNodeValue . "," . $rightNodeValue . ")\n";
256            }
257        }
258
259        /**
260         * Sub
261         */
262        if ($leftNode->hasChildNodes()) {
263
264            $rightChildNodes = $rightNode->childNodes;
265            $rightChildNodesCount = $rightChildNodes->length;
266            if ($rightChildNodes == null || $rightChildNodesCount == 0) {
267                $firstNode = $leftNode->childNodes->item(0);
268                $firstNodeName = $firstNode->nodeName;
269                $firstValue = $firstNode->nodeValue;
270                $error .= "The left node (" . $leftNode->getNodePath() . ") have child nodes while the right has not (First Left Node: $firstNodeName, value: $firstValue) \n";
271            } else {
272                $leftChildNodeCount = $leftNode->childNodes->length;
273                $leftChildIndex = 0;
274                $rightChildIndex = 0;
275                while ($leftChildIndex < $leftChildNodeCount && $rightChildIndex < $rightChildNodesCount) {
276
277                    $leftChildNode = $leftNode->childNodes->item($leftChildIndex);
278                    if ($leftChildNode->nodeName == "#text") {
279                        $leftChildNodeValue = trim($leftChildNode->nodeValue);
280                        if (empty(trim($leftChildNodeValue))) {
281                            $leftChildIndex++;
282                            $leftChildNode = $leftNode->childNodes->item($leftChildIndex);
283                        }
284                    }
285
286                    $rightChildNode = $rightChildNodes->item($rightChildIndex);
287                    if ($rightChildNode->nodeName == "#text") {
288                        $leftChildNodeValue = trim($rightChildNode->nodeValue);
289                        if (empty(trim($leftChildNodeValue))) {
290                            $rightChildIndex++;
291                            $rightChildNode = $rightChildNodes->item($rightChildIndex);
292                        }
293                    }
294
295                    if ($rightChildNode != null) {
296                        if ($leftChildNode != null) {
297                            self::diffNode($leftChildNode, $rightChildNode, $error, $excludedAttributes);
298                        } else {
299                            $error .= "The right node (" . $rightChildNode->getNodePath() . ") does not exist in the left document.\n";
300                        }
301                    } else {
302                        if ($leftChildNode != null) {
303                            $error .= "The left node (" . $leftChildNode->getNodePath() . ") does not exist in the right document.\n";
304                        }
305                    }
306
307                    /**
308                     * 0 based index
309                     */
310                    $leftChildIndex++;
311                    $rightChildIndex++;
312                }
313            }
314        }
315
316    }
317
318    /**
319     * Return a diff
320     * @param string $left
321     * @param string $right
322     * @return string
323     * DOMDocument supports formatted XML while SimpleXMLElement does not.
324     * @throws ExceptionCompile
325     */
326    public
327    static function diffMarkup(string $left, string $right): string
328    {
329        if (empty($right)) {
330            throw new \RuntimeException("The right text should not be empty");
331        }
332        $leftDocument = new XmlDocument($left);
333
334        if (empty($left)) {
335            throw new \RuntimeException("The left text should not be empty");
336        }
337        $rightDocument = new XmlDocument($right);
338
339        return $leftDocument->diff($rightDocument);
340
341    }
342
343    public static function deleteAllElementsByName(string $elementName, XmlDocument $xmlDocument)
344    {
345        $xpathQuery = "//*[local-name()='$elementName']";
346        try {
347            $svgElement = $xmlDocument->xpath($xpathQuery);
348        } catch (ExceptionBadSyntax $e) {
349            // should not happen on prod
350            throw new ExceptionRuntime("xpath query error ($xpathQuery");
351        }
352        for ($i = 0; $i < $svgElement->length; $i++) {
353            $nodeElement = XmlElement::create($svgElement[$i], $xmlDocument);
354            $nodeElement->remove();
355        }
356    }
357
358
359}
360