HtmlExtension.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\CssSelector\XPath\Extension;
  11. use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
  12. use Symfony\Component\CssSelector\Node\FunctionNode;
  13. use Symfony\Component\CssSelector\XPath\Translator;
  14. use Symfony\Component\CssSelector\XPath\XPathExpr;
  15. /**
  16. * XPath expression translator HTML extension.
  17. *
  18. * This component is a port of the Python cssselect library,
  19. * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
  20. *
  21. * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  22. *
  23. * @internal
  24. */
  25. class HtmlExtension extends AbstractExtension
  26. {
  27. public function __construct(Translator $translator)
  28. {
  29. $translator
  30. ->getExtension('node')
  31. ->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true)
  32. ->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true);
  33. }
  34. public function getPseudoClassTranslators(): array
  35. {
  36. return [
  37. 'checked' => $this->translateChecked(...),
  38. 'link' => $this->translateLink(...),
  39. 'disabled' => $this->translateDisabled(...),
  40. 'enabled' => $this->translateEnabled(...),
  41. 'selected' => $this->translateSelected(...),
  42. 'invalid' => $this->translateInvalid(...),
  43. 'hover' => $this->translateHover(...),
  44. 'visited' => $this->translateVisited(...),
  45. ];
  46. }
  47. public function getFunctionTranslators(): array
  48. {
  49. return [
  50. 'lang' => $this->translateLang(...),
  51. ];
  52. }
  53. public function translateChecked(XPathExpr $xpath): XPathExpr
  54. {
  55. return $xpath->addCondition(
  56. '(@checked '
  57. ."and (name(.) = 'input' or name(.) = 'command')"
  58. ."and (@type = 'checkbox' or @type = 'radio'))"
  59. );
  60. }
  61. public function translateLink(XPathExpr $xpath): XPathExpr
  62. {
  63. return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')");
  64. }
  65. public function translateDisabled(XPathExpr $xpath): XPathExpr
  66. {
  67. return $xpath->addCondition(
  68. '('
  69. .'@disabled and'
  70. .'('
  71. ."(name(.) = 'input' and @type != 'hidden')"
  72. ." or name(.) = 'button'"
  73. ." or name(.) = 'select'"
  74. ." or name(.) = 'textarea'"
  75. ." or name(.) = 'command'"
  76. ." or name(.) = 'fieldset'"
  77. ." or name(.) = 'optgroup'"
  78. ." or name(.) = 'option'"
  79. .')'
  80. .') or ('
  81. ."(name(.) = 'input' and @type != 'hidden')"
  82. ." or name(.) = 'button'"
  83. ." or name(.) = 'select'"
  84. ." or name(.) = 'textarea'"
  85. .')'
  86. .' and ancestor::fieldset[@disabled]'
  87. );
  88. // todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any."
  89. }
  90. public function translateEnabled(XPathExpr $xpath): XPathExpr
  91. {
  92. return $xpath->addCondition(
  93. '('
  94. .'@href and ('
  95. ."name(.) = 'a'"
  96. ." or name(.) = 'link'"
  97. ." or name(.) = 'area'"
  98. .')'
  99. .') or ('
  100. .'('
  101. ."name(.) = 'command'"
  102. ." or name(.) = 'fieldset'"
  103. ." or name(.) = 'optgroup'"
  104. .')'
  105. .' and not(@disabled)'
  106. .') or ('
  107. .'('
  108. ."(name(.) = 'input' and @type != 'hidden')"
  109. ." or name(.) = 'button'"
  110. ." or name(.) = 'select'"
  111. ." or name(.) = 'textarea'"
  112. ." or name(.) = 'keygen'"
  113. .')'
  114. .' and not (@disabled or ancestor::fieldset[@disabled])'
  115. .') or ('
  116. ."name(.) = 'option' and not("
  117. .'@disabled or ancestor::optgroup[@disabled]'
  118. .')'
  119. .')'
  120. );
  121. }
  122. /**
  123. * @throws ExpressionErrorException
  124. */
  125. public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
  126. {
  127. $arguments = $function->getArguments();
  128. foreach ($arguments as $token) {
  129. if (!($token->isString() || $token->isIdentifier())) {
  130. throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
  131. }
  132. }
  133. return $xpath->addCondition(sprintf(
  134. 'ancestor-or-self::*[@lang][1][starts-with(concat('
  135. ."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')"
  136. .', %s)]',
  137. 'lang',
  138. Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-')
  139. ));
  140. }
  141. public function translateSelected(XPathExpr $xpath): XPathExpr
  142. {
  143. return $xpath->addCondition("(@selected and name(.) = 'option')");
  144. }
  145. public function translateInvalid(XPathExpr $xpath): XPathExpr
  146. {
  147. return $xpath->addCondition('0');
  148. }
  149. public function translateHover(XPathExpr $xpath): XPathExpr
  150. {
  151. return $xpath->addCondition('0');
  152. }
  153. public function translateVisited(XPathExpr $xpath): XPathExpr
  154. {
  155. return $xpath->addCondition('0');
  156. }
  157. public function getName(): string
  158. {
  159. return 'html';
  160. }
  161. }