1<?php /** @noinspection PhpUnused */ 2 3/** 4 * An ast visitor that compiles a dom document explaining the selector 5 * 6 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 * @copyright Copyright 2010-2014 PhpCss Team 8 */ 9 10namespace PhpCss\Ast\Visitor { 11 12 use DOMDocument; 13 use DOMElement; 14 use PhpCss\Ast; 15 16 /** 17 * An ast visitor that compiles a dom document explaining the selector 18 */ 19 class Explain extends Overload { 20 21 private $_xmlns = 'urn:carica-phpcss-explain-2014'; 22 23 /** 24 * @var DOMDocument 25 */ 26 private $_document; 27 28 /** 29 * @var DOMElement|DOMDocument 30 */ 31 private $_current; 32 33 public function __construct() { 34 $this->clear(); 35 } 36 37 /** 38 * Clear the visitor object to visit another selector group 39 */ 40 public function clear(): void { 41 $this->_current = $this->_document = new DOMDocument(); 42 } 43 44 /** 45 * Return the collected selector string 46 */ 47 public function __toString() { 48 return (string)$this->_document->saveXml(); 49 } 50 51 /** 52 * @param string $name 53 * @param string $content 54 * @param array $attributes 55 * @param string $contentType 56 * @return DOMElement 57 */ 58 private function appendElement( 59 string $name, string $content = '', array $attributes = [], string $contentType = 'text' 60 ): DOMElement { 61 $result = $this->_document->createElementNS($this->_xmlns, $name); 62 if (!empty($content)) { 63 $text = $result->appendChild( 64 $this->_document->createElementNs($this->_xmlns, $contentType) 65 ); 66 if (trim($content) !== $content) { 67 $text->appendChild( 68 $this->_document->createCDATASection($content) 69 ); 70 } else { 71 $text->appendChild( 72 $this->_document->createTextNode($content) 73 ); 74 } 75 } 76 foreach ($attributes as $attribute => $value) { 77 $result->setAttribute($attribute, $value); 78 } 79 $this->_current->appendChild($result); 80 return $result; 81 } 82 83 /** 84 * @param string $content 85 */ 86 private function appendText(string $content): void { 87 $text = $this->_current->appendChild( 88 $this->_document->createElementNs($this->_xmlns, 'text') 89 ); 90 if (trim($content) !== $content) { 91 $text->appendChild( 92 $this->_document->createCDATASection($content) 93 ); 94 } else { 95 $text->appendChild( 96 $this->_document->createTextNode($content) 97 ); 98 } 99 } 100 101 /** 102 * Set the provided node as the current element, start a 103 * subgroup. 104 * 105 * @param $node 106 * @return bool 107 */ 108 private function start($node): bool { 109 $this->_current = $node; 110 return TRUE; 111 } 112 113 /** 114 * Move the current element to its parent element 115 * 116 * @return bool 117 */ 118 private function end(): bool { 119 $this->_current = $this->_current->parentNode; 120 return TRUE; 121 } 122 123 /** 124 * Validate the buffer before visiting a Ast\Selector\Group. 125 * If the buffer already contains data, throw an exception. 126 * 127 * @return boolean 128 */ 129 public function visitEnterSelectorGroup(): bool { 130 $this->start($this->appendElement('selector-group')); 131 return TRUE; 132 } 133 134 /** 135 * If here is already data in the buffer, add a separator before starting the next. 136 * 137 * @return boolean 138 */ 139 public function visitEnterSelectorSequence(): bool { 140 if ( 141 $this->_current === $this->_document->documentElement && 142 $this->_current->hasChildNodes() 143 ) { 144 $this 145 ->_current 146 ->appendChild( 147 $this->_document->createElementNs($this->_xmlns, 'text') 148 ) 149 ->appendChild( 150 $this->_document->createCDATASection(', ') 151 ); 152 } 153 return $this->start($this->appendElement('selector')); 154 } 155 156 /** 157 * @return bool 158 */ 159 public function visitLeaveSelectorSequence(): bool { 160 return $this->end(); 161 } 162 163 /** 164 * @param Ast\Selector\Simple\Universal $universal 165 * @return boolean 166 */ 167 public function visitSelectorSimpleUniversal(Ast\Selector\Simple\Universal $universal): bool { 168 if (!empty($universal->namespacePrefix) && $universal->namespacePrefix !== '*') { 169 $css = $universal->namespacePrefix.'|*'; 170 } else { 171 $css = '*'; 172 } 173 $this->appendElement('universal', $css); 174 return TRUE; 175 } 176 177 /** 178 * @param Ast\Selector\Simple\Type $type 179 * @return bool 180 */ 181 public function visitSelectorSimpleType(Ast\Selector\Simple\Type $type): bool { 182 if (!empty($type->namespacePrefix) && $type->namespacePrefix !== '*') { 183 $css = $type->namespacePrefix.'|'.$type->elementName; 184 } else { 185 $css = $type->elementName; 186 } 187 $this->appendElement('type', $css); 188 return TRUE; 189 } 190 191 /** 192 * @param Ast\Selector\Simple\Id $id 193 * @return boolean 194 */ 195 public function visitSelectorSimpleId(Ast\Selector\Simple\Id $id): bool { 196 $this->appendElement('id', '#'.$id->id); 197 return TRUE; 198 } 199 200 /** 201 * @param Ast\Selector\Simple\ClassName $class 202 * @return boolean 203 */ 204 public function visitSelectorSimpleClassName(Ast\Selector\Simple\ClassName $class): bool { 205 $this->appendElement('class', '.'.$class->className); 206 return TRUE; 207 } 208 209 /** 210 * @return bool 211 */ 212 public function visitEnterSelectorCombinatorDescendant(): bool { 213 return $this->start($this->appendElement('descendant', ' ')); 214 } 215 216 /** 217 * @return bool 218 */ 219 public function visitLeaveSelectorCombinatorDescendant(): bool { 220 return $this->end(); 221 } 222 223 /** 224 * @return bool 225 */ 226 public function visitEnterSelectorCombinatorChild(): bool { 227 return $this->start($this->appendElement('child', ' > ')); 228 } 229 230 /** 231 * @return bool 232 */ 233 public function visitLeaveSelectorCombinatorChild(): bool { 234 return $this->end(); 235 } 236 237 /** 238 * @return bool 239 */ 240 public function visitEnterSelectorCombinatorFollower(): bool { 241 return $this->start($this->appendElement('follower', ' ~ ')); 242 } 243 244 /** 245 * @return bool 246 */ 247 public function visitLeaveSelectorCombinatorFollower(): bool { 248 return $this->end(); 249 } 250 251 /** 252 * @return bool 253 */ 254 public function visitEnterSelectorCombinatorNext(): bool { 255 return $this->start($this->appendElement('next', ' + ')); 256 } 257 258 /** 259 * @return bool 260 */ 261 public function visitLeaveSelectorCombinatorNext(): bool { 262 return $this->end(); 263 } 264 265 /** 266 * @param Ast\Selector\Simple\Attribute $attribute 267 * @return bool 268 */ 269 public function visitSelectorSimpleAttribute( 270 Ast\Selector\Simple\Attribute $attribute 271 ): bool { 272 $operators = [ 273 Ast\Selector\Simple\Attribute::MATCH_EXISTS => 'exists', 274 Ast\Selector\Simple\Attribute::MATCH_PREFIX => 'prefix', 275 Ast\Selector\Simple\Attribute::MATCH_SUFFIX => 'suffix', 276 Ast\Selector\Simple\Attribute::MATCH_SUBSTRING => 'substring', 277 Ast\Selector\Simple\Attribute::MATCH_EQUALS => 'equals', 278 Ast\Selector\Simple\Attribute::MATCH_INCLUDES => 'includes', 279 Ast\Selector\Simple\Attribute::MATCH_DASHMATCH => 'dashmatch', 280 ]; 281 $this->start( 282 $this->appendElement( 283 'attribute', '', ['operator' => $operators[$attribute->match]] 284 ) 285 ); 286 $this->appendText('['); 287 $this->appendElement('name', $attribute->name); 288 if ($attribute->match !== Ast\Selector\Simple\Attribute::MATCH_EXISTS) { 289 $operatorStrings = [ 290 Ast\Selector\Simple\Attribute::MATCH_PREFIX => '^=', 291 Ast\Selector\Simple\Attribute::MATCH_SUFFIX => '$=', 292 Ast\Selector\Simple\Attribute::MATCH_SUBSTRING => '*=', 293 Ast\Selector\Simple\Attribute::MATCH_EQUALS => '=', 294 Ast\Selector\Simple\Attribute::MATCH_INCLUDES => '~=', 295 Ast\Selector\Simple\Attribute::MATCH_DASHMATCH => '|=', 296 ]; 297 $this->appendElement('operator', $operatorStrings[$attribute->match]); 298 $this->appendText('"'); 299 $this->appendElement( 300 'value', 301 str_replace(['\\', '"'], ['\\\\', '\\"'], $attribute->literal->value) 302 ); 303 $this->appendText('"'); 304 } 305 $this->appendText(']'); 306 $this->end(); 307 return TRUE; 308 } 309 310 /** 311 * @param Ast\Selector\Simple\PseudoClass $class 312 * @return bool 313 */ 314 public function visitSelectorSimplePseudoClass( 315 Ast\Selector\Simple\PseudoClass $class 316 ): bool { 317 $this->start($this->appendElement('pseudoclass')); 318 $this->appendElement('name', ':'.$class->name); 319 return $this->end(); 320 } 321 322 /** 323 * @param Ast\Selector\Simple\PseudoClass $class 324 * @return bool 325 */ 326 public function visitEnterSelectorSimplePseudoClass( 327 Ast\Selector\Simple\PseudoClass $class 328 ): bool { 329 $this->start($this->appendElement('pseudoclass')); 330 $this->appendElement('name', ':'.$class->name); 331 $this->appendText('('); 332 $this->start($this->appendElement('parameter')); 333 return TRUE; 334 } 335 336 /** 337 * @return bool 338 */ 339 public function visitLeaveSelectorSimplePseudoClass(): bool { 340 $this->end(); 341 $this->appendText(')'); 342 return $this->end(); 343 } 344 345 /** 346 * @param Ast\Value\Literal $literal 347 * @return bool 348 */ 349 public function visitValueLiteral( 350 Ast\Value\Literal $literal 351 ): bool { 352 $this->appendText('"'); 353 $this->appendElement( 354 'value', 355 str_replace(['\\', '"'], ['\\\\', '\\"'], $literal->value) 356 ); 357 $this->appendText('"'); 358 return TRUE; 359 } 360 361 /** 362 * @param Ast\Value\Number $number 363 * @return bool 364 */ 365 public function visitValueNumber( 366 Ast\Value\Number $number 367 ): bool { 368 $this->appendElement( 369 'value', $number->value, [], 'number' 370 ); 371 return TRUE; 372 } 373 374 /** 375 * @param Ast\Value\Position $position 376 * @return bool 377 */ 378 public function visitValuePosition( 379 Ast\Value\Position $position 380 ): bool { 381 $repeatsOddEven = $position->repeat === 2; 382 if ($repeatsOddEven && $position->add === 1) { 383 $css = 'odd'; 384 } elseif ($repeatsOddEven && $position->add === 0) { 385 $css = 'even'; 386 } elseif ($position->repeat === 0) { 387 $css = $position->add; 388 } elseif ($position->repeat === 1) { 389 $css = 'n'; 390 if ($position->add !== 0) { 391 $css .= $position->add >= 0 392 ? '+'.$position->add : $position->add; 393 } 394 } else { 395 $css = $position->repeat.'n'; 396 if ($position->add !== 0) { 397 $css .= $position->add >= 0 398 ? '+'.$position->add : $position->add; 399 } 400 } 401 $this->appendText($css); 402 return TRUE; 403 } 404 405 /** 406 * @param Ast\Value\Language $language 407 * @return bool 408 */ 409 public function visitValueLanguage( 410 Ast\Value\Language $language 411 ): bool { 412 $this->start($this->appendElement('pseudoclass')); 413 $this->appendElement('name', ':lang'); 414 $this->appendText('('); 415 $this->start($this->appendElement('parameter')); 416 $this->appendText($language->language); 417 $this->end(); 418 $this->appendText(')'); 419 return TRUE; 420 } 421 422 /** 423 * @param Ast\Selector\Simple\PseudoElement $element 424 * @return bool 425 */ 426 public function visitSelectorSimplePseudoElement( 427 Ast\Selector\Simple\PseudoElement $element 428 ): bool { 429 $this->start($this->appendElement('pseudoclass')); 430 $this->appendElement('name', '::'.$element->name); 431 return $this->end(); 432 } 433 } 434} 435