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