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