clear(); } /** * Clear the visitor object to visit another selector group */ public function clear(): void { $this->_current = $this->_document = new DOMDocument(); } /** * Return the collected selector string */ public function __toString() { return (string)$this->_document->saveXml(); } /** * @param string $name * @param string $content * @param array $attributes * @param string $contentType * @return DOMElement */ private function appendElement( string $name, string $content = '', array $attributes = [], string $contentType = 'text' ): DOMElement { $result = $this->_document->createElementNS($this->_xmlns, $name); if (!empty($content)) { $text = $result->appendChild( $this->_document->createElementNs($this->_xmlns, $contentType) ); if (trim($content) !== $content) { $text->appendChild( $this->_document->createCDATASection($content) ); } else { $text->appendChild( $this->_document->createTextNode($content) ); } } foreach ($attributes as $attribute => $value) { $result->setAttribute($attribute, $value); } $this->_current->appendChild($result); return $result; } /** * @param string $content */ private function appendText(string $content): void { $text = $this->_current->appendChild( $this->_document->createElementNs($this->_xmlns, 'text') ); if (trim($content) !== $content) { $text->appendChild( $this->_document->createCDATASection($content) ); } else { $text->appendChild( $this->_document->createTextNode($content) ); } } /** * Set the provided node as the current element, start a * subgroup. * * @param $node * @return bool */ private function start($node): bool { $this->_current = $node; return TRUE; } /** * Move the current element to its parent element * * @return bool */ private function end(): bool { $this->_current = $this->_current->parentNode; return TRUE; } /** * Validate the buffer before visiting a Ast\Selector\Group. * If the buffer already contains data, throw an exception. * * @return boolean */ public function visitEnterSelectorGroup(): bool { $this->start($this->appendElement('selector-group')); return TRUE; } /** * If here is already data in the buffer, add a separator before starting the next. * * @return boolean */ public function visitEnterSelectorSequence(): bool { if ( $this->_current === $this->_document->documentElement && $this->_current->hasChildNodes() ) { $this ->_current ->appendChild( $this->_document->createElementNs($this->_xmlns, 'text') ) ->appendChild( $this->_document->createCDATASection(', ') ); } return $this->start($this->appendElement('selector')); } /** * @return bool */ public function visitLeaveSelectorSequence(): bool { return $this->end(); } /** * @param Ast\Selector\Simple\Universal $universal * @return boolean */ public function visitSelectorSimpleUniversal(Ast\Selector\Simple\Universal $universal): bool { if (!empty($universal->namespacePrefix) && $universal->namespacePrefix !== '*') { $css = $universal->namespacePrefix.'|*'; } else { $css = '*'; } $this->appendElement('universal', $css); return TRUE; } /** * @param Ast\Selector\Simple\Type $type * @return bool */ public function visitSelectorSimpleType(Ast\Selector\Simple\Type $type): bool { if (!empty($type->namespacePrefix) && $type->namespacePrefix !== '*') { $css = $type->namespacePrefix.'|'.$type->elementName; } else { $css = $type->elementName; } $this->appendElement('type', $css); return TRUE; } /** * @param Ast\Selector\Simple\Id $id * @return boolean */ public function visitSelectorSimpleId(Ast\Selector\Simple\Id $id): bool { $this->appendElement('id', '#'.$id->id); return TRUE; } /** * @param Ast\Selector\Simple\ClassName $class * @return boolean */ public function visitSelectorSimpleClassName(Ast\Selector\Simple\ClassName $class): bool { $this->appendElement('class', '.'.$class->className); return TRUE; } /** * @return bool */ public function visitEnterSelectorCombinatorDescendant(): bool { return $this->start($this->appendElement('descendant', ' ')); } /** * @return bool */ public function visitLeaveSelectorCombinatorDescendant(): bool { return $this->end(); } /** * @return bool */ public function visitEnterSelectorCombinatorChild(): bool { return $this->start($this->appendElement('child', ' > ')); } /** * @return bool */ public function visitLeaveSelectorCombinatorChild(): bool { return $this->end(); } /** * @return bool */ public function visitEnterSelectorCombinatorFollower(): bool { return $this->start($this->appendElement('follower', ' ~ ')); } /** * @return bool */ public function visitLeaveSelectorCombinatorFollower(): bool { return $this->end(); } /** * @return bool */ public function visitEnterSelectorCombinatorNext(): bool { return $this->start($this->appendElement('next', ' + ')); } /** * @return bool */ public function visitLeaveSelectorCombinatorNext(): bool { return $this->end(); } /** * @param Ast\Selector\Simple\Attribute $attribute * @return bool */ public function visitSelectorSimpleAttribute( Ast\Selector\Simple\Attribute $attribute ): bool { $operators = [ Ast\Selector\Simple\Attribute::MATCH_EXISTS => 'exists', Ast\Selector\Simple\Attribute::MATCH_PREFIX => 'prefix', Ast\Selector\Simple\Attribute::MATCH_SUFFIX => 'suffix', Ast\Selector\Simple\Attribute::MATCH_SUBSTRING => 'substring', Ast\Selector\Simple\Attribute::MATCH_EQUALS => 'equals', Ast\Selector\Simple\Attribute::MATCH_INCLUDES => 'includes', Ast\Selector\Simple\Attribute::MATCH_DASHMATCH => 'dashmatch', ]; $this->start( $this->appendElement( 'attribute', '', ['operator' => $operators[$attribute->match]] ) ); $this->appendText('['); $this->appendElement('name', $attribute->name); if ($attribute->match !== Ast\Selector\Simple\Attribute::MATCH_EXISTS) { $operatorStrings = [ Ast\Selector\Simple\Attribute::MATCH_PREFIX => '^=', Ast\Selector\Simple\Attribute::MATCH_SUFFIX => '$=', Ast\Selector\Simple\Attribute::MATCH_SUBSTRING => '*=', Ast\Selector\Simple\Attribute::MATCH_EQUALS => '=', Ast\Selector\Simple\Attribute::MATCH_INCLUDES => '~=', Ast\Selector\Simple\Attribute::MATCH_DASHMATCH => '|=', ]; $this->appendElement('operator', $operatorStrings[$attribute->match]); $this->appendText('"'); $this->appendElement( 'value', str_replace(['\\', '"'], ['\\\\', '\\"'], $attribute->literal->value) ); $this->appendText('"'); } $this->appendText(']'); $this->end(); return TRUE; } /** * @param Ast\Selector\Simple\PseudoClass $class * @return bool */ public function visitSelectorSimplePseudoClass( Ast\Selector\Simple\PseudoClass $class ): bool { $this->start($this->appendElement('pseudoclass')); $this->appendElement('name', ':'.$class->name); return $this->end(); } /** * @param Ast\Selector\Simple\PseudoClass $class * @return bool */ public function visitEnterSelectorSimplePseudoClass( Ast\Selector\Simple\PseudoClass $class ): bool { $this->start($this->appendElement('pseudoclass')); $this->appendElement('name', ':'.$class->name); $this->appendText('('); $this->start($this->appendElement('parameter')); return TRUE; } /** * @return bool */ public function visitLeaveSelectorSimplePseudoClass(): bool { $this->end(); $this->appendText(')'); return $this->end(); } /** * @param Ast\Value\Literal $literal * @return bool */ public function visitValueLiteral( Ast\Value\Literal $literal ): bool { $this->appendText('"'); $this->appendElement( 'value', str_replace(['\\', '"'], ['\\\\', '\\"'], $literal->value) ); $this->appendText('"'); return TRUE; } /** * @param Ast\Value\Number $number * @return bool */ public function visitValueNumber( Ast\Value\Number $number ): bool { $this->appendElement( 'value', $number->value, [], 'number' ); return TRUE; } /** * @param Ast\Value\Position $position * @return bool */ public function visitValuePosition( Ast\Value\Position $position ): bool { $repeatsOddEven = $position->repeat === 2; if ($repeatsOddEven && $position->add === 1) { $css = 'odd'; } elseif ($repeatsOddEven && $position->add === 0) { $css = 'even'; } elseif ($position->repeat === 0) { $css = $position->add; } elseif ($position->repeat === 1) { $css = 'n'; if ($position->add !== 0) { $css .= $position->add >= 0 ? '+'.$position->add : $position->add; } } else { $css = $position->repeat.'n'; if ($position->add !== 0) { $css .= $position->add >= 0 ? '+'.$position->add : $position->add; } } $this->appendText($css); return TRUE; } /** * @param Ast\Value\Language $language * @return bool */ public function visitValueLanguage( Ast\Value\Language $language ): bool { $this->start($this->appendElement('pseudoclass')); $this->appendElement('name', ':lang'); $this->appendText('('); $this->start($this->appendElement('parameter')); $this->appendText($language->language); $this->end(); $this->appendText(')'); return TRUE; } /** * @param Ast\Selector\Simple\PseudoElement $element * @return bool */ public function visitSelectorSimplePseudoElement( Ast\Selector\Simple\PseudoElement $element ): bool { $this->start($this->appendElement('pseudoclass')); $this->appendElement('name', '::'.$element->name); return $this->end(); } } }