Hoa central
CssToXPath.php
Go to the documentation of this file.
1 <?php
2 
37 namespace Hoa\Xml;
38 
39 use Hoa\Compiler;
40 
49 class CssToXPath extends Compiler\Ll1
50 {
56  protected $_root = null;
57 
63  protected $_current = null;
64 
70  protected $_prefix = null;
71 
72 
73 
79  public function __construct()
80  {
81 
82  // http://w3.org/TR/css3-selectors/#w3cselgrammar
83  parent::__construct(
84  // Skip.
85  [
86  '#\/\*[^*]*\*+([^/*][^*]*\*+)*\/' // /* … */
87  ],
88 
89  // Tokens.
90  [
91  // 1. Selectors group.
92  [
93  '#,\s*' // ,
94  ],
95 
96  // 2. Selector.
97  [
98  '#\s*\+\s*', // +
99  '#\s*>\s*', // >
100  '#\s*~\s*', // ~
101  '#\s* \s*' // s
102  ],
103 
104  // 3. Simple selector sequence.
105  [
106  '#(\*|\w+)\|', // tu (type selector or universal)
107  '#\w+', // w
108  '|', // |
109  '#\*' // *
110  ],
111 
112  // 4. Hacpn.
113  [
114  '[', // [
115  ']', // ]
116  '#(\*|\w+)\|(?!=)', // tu (type selector or universal)
117  '#[\w\-]+\(((\+|\-|\d+\w+|\d+|\'\w+\'|\w+)\s*)+\)', // f
118  '#[\w\-]+', // w
119  '#\^=', // ^
120  '#\$=', // $
121  '#~=', // ~
122  '#\*=', // *
123  '#\|=', // |=
124  '=', // =
125  '#\|', // |
126  '#(\'|").*?(?<!\\\)\2', // 's
127  '##\w+', // # (hash)
128  '#\.\w+', // . (class)
129  ':' // :
130  ]
131  ],
132 
133  // States.
134  [
135  // 1. Selectors group.
136  [
137  __, // error
138  'GO' // start
139  ],
140 
141  // 2. Selector.
142  [
143  __, // error
144  'GO' // start
145  ],
146 
147  // 3. Simple selector sequence.
148  [
149  __, // error
150  'GO', // start
151  'TU', // start type selector or universal
152  'OK' // terminal
153  ],
154 
155  // 4. Hacpn.
156  [
157  __, // error
158  'GO', // start
159  'OB', // open bracket: [
160  'NS', // namespace prefix
161  'ID', // identifier
162  'OP', // operator
163  'VA', // value
164  'PC', // pseudo-class (:)
165  'PE', // pseudo-element (::)
166  'OK' // terminal
167  ]
168  ],
169 
170  // Terminal.
171  [
172  // 1. Selectors group.
173  ['GO'],
174 
175  // 2. Selector.
176  ['GO'],
177 
178  // 3. Simple selector sequence.
179  ['GO', 'OK'],
180 
181  // 4. Hacpn.
182  ['GO', 'OK']
183  ],
184 
185  // Transitions.
186  [
187  // 1. Selectors group.
188  [
189  /* ,
190  /* __ */ [ __ ],
191  /* GO */ ['GO']
192  ],
193 
194  // 2. Selector.
195  [
196  /* + > ~ s
197  /* __ */ [ __, __, __, __ ],
198  /* GO */ ['GO', 'GO', 'GO', 'GO']
199  ],
200 
201  // 3. Simple selector sequence.
202  [
203  /* tu w | *
204  /* __ */ [ __, __, __, __ ],
205  /* GO */ ['TU', 'OK', 'TU', 'OK'],
206  /* TU */ [ __, 'OK', __, 'OK'],
207  /* OK */ [ __, __, __, __ ]
208  ],
209 
210  // 4. Hacpn.
211  [
212  /* [ ] tu f w ^ $ ~ * |= = | 's # . :
213  /* __ */ [ __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __ ],
214  /* GO */ ['OB', __, __, __, __, __, __, __, __, __, __, __, __, 'OK', 'OK', 'PC'],
215  /* OB */ [ __, __, 'NS', __, 'ID', __, __, __, __, __, __, 'NS', __, __, __, __ ],
216  /* NS */ [ __, __, __, __, 'ID', __, __, __, __, __, __, __, __, __, __, __ ],
217  /* ID */ [ __, 'OK', __, __, __, 'OP', 'OP', 'OP', 'OP', 'OP', 'OP', __, __, __, __, __ ],
218  /* OP */ [ __, __, __, __, 'VA', __, __, __, __, __, __, __, 'VA', __, __, __ ],
219  /* VA */ [ __, 'OK', __, __, __, __, __, __, __, __, __, __, __, __, __, __ ],
220  /* PC */ [ __, __, __, 'OK', 'OK', __, __, __, __, __, __, __, __, __, __, 'PE'],
221  /* PE */ [ __, __, __, 'OK', 'OK', __, __, __, __, __, __, __, __, __, __, __ ],
222  /* OK */ [ __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __ ]
223  ]
224  ],
225 
226  // Actions.
227  [
228  // 1. Selectors group.
229  [
230  /* ,
231  /* __ */ [ 0 ],
232  /* GO */ ['2,,']
233  ],
234 
235  // 2. Selector.
236  [
237  /* + > ~ s
238  /* __ */ [ 0, 0, 0, 0 ],
239  /* GO */ ['3,+', '3,>', '3,~', '3, ']
240  ],
241 
242  // 3. Simple selector sequence.
243  [
244  /* tu w | *
245  /* __ */ [ 0, 0, 0, 0 ],
246  /* GO */ [ '4,-1', '4,-1', '4,-1', '4,-1'],
247  /* TU */ [ 0, -1, 0, -1 ],
248  /* OK */ [ 0, 4, 0, 4 ]
249  ],
250 
251  // 4. Hacpn.
252  [
253  /* [ ] tu f w ^ $ ~ * |= = | 's # . :
254  /* __ */ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
255  /* GO */ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '#', '.', 0],
256  /* OB */ [0, 0, -3, 0, -3, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0],
257  /* NS */ [0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
258  /* ID */ [0, ']', 0, 0, 0, '^', '$', '~=', '*', '|', '=', 0, 0, 0, 0, 0],
259  /* OP */ [0, 0, 0, 0, 'v', 0, 0, 0, 0, 0, 0, 0, 'v', 0, 0, 0],
260  /* VA */ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
261  /* PC */ [0, 0, 0, ':f', ':', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
262  /* PE */ [0, 0, 0, '::f', '::', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
263  /* OK */ [0, 4, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0]
264  ]
265  ]
266  );
267  }
268 
277  protected function flush($element, $selector, $pseudo)
278  {
279  $out = $element;
280 
281  if (!empty($selector)) {
282  $out .= '[(' . implode(') and (', $selector) . ')]';
283  }
284 
285  $out .= $pseudo;
286 
287  return $out;
288  }
289 
297  protected function consume($action)
298  {
299  static $__element = '*';
300  static $_element = '*';
301  static $element = '*';
302  static $selector = [];
303  static $pseudo = null;
304  static $attribute = null;
305  static $operator = null;
306 
307  if (isset($this->buffers[0])) {
308  if (false !== strpos($this->buffers[0], '|')) {
309  $__element
310  = $_element
311  = $element
312  = str_replace('|', ':', $this->buffers[0]);
313  } else {
314  $__element = $this->buffers[0];
315 
316  if (null !== $p = $this->getDefaultNamespacePrefix()) {
317  $_element = $element = $p . ':' . $__element;
318  } else {
319  $_element = $element = $__element;
320  }
321  }
322 
323  unset($this->buffers[0]);
324  }
325 
326  switch ($action) {
327  case '__init':
328  $_element = '*';
329  $element = '*';
330  $selector = [];
331  $pseudo = null;
332  $attribute = null;
333  $operator = null;
334 
335  break;
336 
337  case '__flush':
338  $this->_current .= $this->flush($element, $selector, $pseudo);
339  $element = null;
340  $selector = [];
341  $pseudo = null;
342 
343  break;
344 
345  case '+':
346  $this->consume('__flush');
347  $this->_current .= '/following-sibling::*[1]/self::';
348 
349  break;
350 
351  case '>':
352  $this->consume('__flush');
353  $this->_current .= '/';
354 
355  break;
356 
357  case '~':
358  $this->consume('__flush');
359  $this->_current .= '/following-sibling::';
360 
361  break;
362 
363  case ' ':
364  $this->consume('__flush');
365  $this->_current .= '//';
366 
367  break;
368 
369  case '#':
370  $w = substr($this->buffers[-1], 1);
371  $selector[] = '@id = "' . $w . '"';
372 
373  break;
374 
375  case '.':
376  $w = substr($this->buffers[-1], 1);
377  $selector[] =
378  'contains(concat(' .
379  '" ", ' .
380  'normalize-space(@class), ' .
381  '" "' .
382  '), " ' . $w . ' ")';
383 
384  break;
385 
386  case '=':
387  case '^':
388  case '$':
389  case '~=':
390  case '*':
391  case '|':
392  $attribute = str_replace('|', ':', $this->buffers[1]);
393  unset($this->buffers[1]);
394  $operator = $action[0];
395 
396  break;
397 
398  case 'v':
399  $w =
400  '"' === $this->buffers[-1][0]
401  ? str_replace('\"', '"', substr($this->buffers[-1], 1, -1))
402  : str_replace('\\\'', '\'', substr($this->buffers[-1], 1, -1));
403 
404  switch ($operator) {
405  case '=':
406  $selector[] = '@' . $attribute . ' = "' . $w . '"';
407 
408  break;
409 
410  case '^':
411  $selector[] = 'starts-with(@' . $attribute . ', "' . $w . '")';
412 
413  break;
414 
415  case '$':
416  $length = strlen($w) - 1;
417  $selector[] =
418  'substring(@' . $attribute .
419  ', string-length(@' . $attribute . ') - ' .
420  $length . ') = "' . $w . '"';
421 
422  break;
423 
424  case '~':
425  $selector[] =
426  'contains(concat(" ", normalize-space(@' .
427  $attribute . '), " "), " ' . $w . ' ")';
428 
429  break;
430 
431  case '*':
432  $selector[] = 'contains(@' . $attribute . ', "' . $w . '")';
433 
434  break;
435 
436  case '|':
437  $selector[] =
438  '@' . $attribute . ' = "' . $w . '" or ' .
439  'starts-with(@' . $attribute . ', "' . $w .
440  '-")';
441 
442  break;
443  }
444 
445  $attribute = null;
446  $operator = null;
447 
448  break;
449 
450  case ']':
451  $w = str_replace('|', ':', $this->buffers[1]);
452  unset($this->buffers[1]);
453  $selector[] = '@' . $w;
454 
455  break;
456 
457  case ':':
458  $pc = $this->buffers[-1];
459 
460  switch ($pc) {
461  case 'root':
462  $this->_root = 'self::';
463 
464  break;
465 
466  case 'first-child':
467  if ('*' != $_element) {
468  $element = '*';
469  $selector[] = 'name() = "' . $__element . '"';
470  }
471  $selector[] = 'position() = 1';
472 
473  break;
474 
475  case 'last-child':
476  if ('*' != $_element) {
477  $element = '*';
478  $selector[] = 'name() = "' . $__element . '"';
479  }
480  $selector[] = 'position() = last()';
481 
482  break;
483 
484  case 'first-of-type':
485  if ('*' == $_element) {
486  throw new Compiler\Exception(
487  'Cannot have a :first-of-type without element.',
488  0
489  );
490  }
491 
492  $selector[] = 'position() = 1';
493 
494  break;
495 
496  case 'last-of-type':
497  if ('*' == $_element) {
498  throw new Compiler\Exception(
499  'Cannot have a :last-of-type without element.',
500  1
501  );
502  }
503 
504  $selector[] = 'position() = last()';
505 
506  break;
507 
508  case 'only-child':
509  if ('*' != $_element) {
510  $element = '*';
511  $selector[] = 'name() = "' . $_element . '"';
512  }
513  $selector[] = 'last() = 1';
514 
515  break;
516 
517  case 'only-of-type':
518  if ('*' == $_element) {
519  throw new Compiler\Exception(
520  'Cannot have a :only-of-type without element.',
521  2
522  );
523  }
524 
525  $selector[] = 'last() = 1';
526 
527  break;
528 
529  case 'empty':
530  $selector[] = 'not(*)';
531  $selector[] = 'not(normalize-space())';
532 
533  break;
534 
535  default:
536  $selector[] = $this->callPseudoClass($element, $pc);
537  }
538 
539  break;
540 
541  case '::':
542  $pe = $this->buffers[-1];
543  $selector[] = $this->callPseudoElement($element, $pe);
544 
545  break;
546 
547  case ':f':
548  $first = strpos($this->buffers[-1], '(');
549  $pcf = substr($this->buffers[-1], 0, $first);
550  $args = substr($this->buffers[-1], $first + 1, -1);
551 
552  switch ($pcf) {
553  case 'nth-child':
554  case 'nth-of-type':
555  preg_match(
556  '#^(?:([+|-])?\s*(\d+)?\s*(n))?\s*([+|-]?\s*\d+)?$#',
557  $args,
558  $matches
559  );
560 
561  $group =
562  !empty($matches[3])
563  ? '' !== $matches[2]
564  ? @$matches[1] . str_replace(' ', '', $matches[2])
565  : @$matches[1] . '1'
566  : '0';
567 
568  $offset =
569  isset($matches[4]) && null !== $matches[4]
570  ? str_replace(' ', '', $matches[4])
571  : '0';
572 
573  if (0 <= (int) $offset) {
574  $offset = '+' . trim($offset, '+');
575  }
576 
577  $tesffo =
578  '+' == $offset[0]
579  ? '- ' . substr($offset, 1)
580  : '+ ' . substr($offset, 1);
581 
582  if ('nth-child' == $pcf && '*' != $_element) {
583  $element = '*';
584  $selector[] = 'name() = "' . $_element . '"';
585  } elseif ('nth-of-type' == $pcf && '*' == $_element) {
586  throw new Compiler\Exception(
587  'Cannot have a :nth-of-type without element.',
588  3
589  );
590  }
591 
592  if (0 != (int) $group) {
593  if ('1' != $group) {
594  $selector[] = 'position() ' . $tesffo . ') mod ' .
595  $group . ' = 0';
596  }
597 
598  $selector[] = 'position() >= ' . (int) $offset;
599  } else {
600  $selector[] = 'position() = ' . (int) $offset;
601  }
602 
603  break;
604 
605  case 'nth-last-child':
606  case 'nth-last-of-type':
607  preg_match(
608  '#^(?:([+|-])?\s*(\d+)?\s*(n))?\s*([+|-]?\s*\d+)?$#',
609  $args,
610  $matches
611  );
612 
613  $group =
614  !empty($matches[3])
615  ? '' !== $matches[2]
616  ? @$matches[1] . str_replace(' ', '', $matches[2])
617  : @$matches[1] . '1'
618  : '0';
619 
620  $offset =
621  isset($matches[4]) && null !== $matches[4]
622  ? str_replace(' ', '', $matches[4])
623  : '0';
624 
625  if (0 <= (int) $group) {
626  $group = '+' . trim($group, '+');
627  }
628 
629  if (0 <= (int) $offset) {
630  $offset = '+' . trim($offset, '+');
631  }
632 
633  $puorg =
634  '+' == $group[0]
635  ? '-' . substr($group, 1)
636  : '+' . substr($group, 1);
637 
638  $tesffo =
639  '+' == $offset[0]
640  ? '- ' . substr($offset, 1)
641  : '+ ' . substr($offset, 1);
642 
643  if ('nth-last-child' === $pcf && '*' !== $_element) {
644  $element = '*';
645  $selector[] = 'name() = "' . $_element . '"';
646  } elseif ('nth-last-of-type' === $pcf && '*' === $_element) {
647  throw new Compiler\Exception(
648  'Cannot have a :nth-last-of-type without element.',
649  4
650  );
651  }
652 
653  if (0 !== (int) $group) {
654  if ('1' !== $group) {
655  $selector[] =
656  'position() ' . $offset . ') mod ' .
657  $puorg . ' = 0';
658  }
659 
660  $selector[] = 'position() <= (last() - ' . (int) $offset . ')';
661  } else {
662  $selector[] = 'position() = (last() - ' . (int) $offset . ')';
663  }
664 
665  break;
666 
667  default:
668  $selector[] = $this->callPseudoClassFunction($element, $pcf);
669  }
670 
671  break;
672 
673  case '::f':
674  $pef = $this->buffers[-1];
675  $this->_current .= $this->callPseudoElementFunction($element, $pef);
676 
677  break;
678  }
679  }
680 
687  protected function pre(&$in)
688  {
689  $this->_root = 'descendant-or-self::';
690  $this->_current = null;
691  $this->consume('__init');
692 
693  return;
694  }
695 
701  protected function end()
702  {
703  $this->consume('__flush');
704  $this->_current = $this->_root . $this->_current;
705 
706  return true;
707  }
708 
714  public function getResult()
715  {
716  return $this->getXPath();
717  }
718 
724  public function getXPath()
725  {
726  return $this->_current;
727  }
728 
736  protected function callPseudoClass($element, $pseudoClass)
737  {
738  throw new Compiler\Exception(
739  'The pseudo-class %s on the element %s is unknown.',
740  5,
741  [$pseudoClass, $element]
742  );
743  }
744 
752  protected function callPseudoElement($element, $pseudoElement)
753  {
754  throw new Compiler\Exception(
755  'The pseudo-element %s on the element %s is unknown.',
756  6,
757  [$pseudoElement, $element]
758  );
759  }
760 
768  protected function callPseudoClassFunction($element, $function)
769  {
770  throw new Compiler\Exception(
771  'The pseudo-class function %s on the element %s is unknown.',
772  7,
773  [$function, $element]
774  );
775  }
776 
784  protected function callPseudoElementFunction($element, $function)
785  {
786  throw new Compiler\Exception(
787  'The pseudo-element function %s on the element %s is unknown.',
788  8,
789  [$function, $element]
790  );
791  }
792 
799  public function setDefaultNamespacePrefix($prefix)
800  {
801  $old = $this->_prefix;
802  $this->_prefix = $prefix;
803 
804  return $old;
805  }
806 
812  public function getDefaultNamespacePrefix()
813  {
814  return $this->_prefix;
815  }
816 }
callPseudoClass($element, $pseudoClass)
Definition: CssToXPath.php:736
flush($element, $selector, $pseudo)
Definition: CssToXPath.php:277
callPseudoClassFunction($element, $function)
Definition: CssToXPath.php:768
setDefaultNamespacePrefix($prefix)
Definition: CssToXPath.php:799
callPseudoElement($element, $pseudoElement)
Definition: CssToXPath.php:752
callPseudoElementFunction($element, $function)
Definition: CssToXPath.php:784