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