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