PseudoSelectorConverter.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <?php
  2. namespace Gt\CssXPath;
  3. class PseudoSelectorConverter {
  4. /** @var array<int, string> */
  5. private const BOOLEAN_ATTRIBUTES = ["disabled", "checked", "selected"];
  6. private SelectorListSplitter $selectorListSplitter;
  7. private NotSelectorConditionBuilder $notSelectorConditionBuilder;
  8. private ?HasSelectorConditionBuilder $hasSelectorConditionBuilder;
  9. public function __construct(
  10. ?SelectorListSplitter $selectorListSplitter = null,
  11. ?NotSelectorConditionBuilder $notSelectorConditionBuilder = null,
  12. ?HasSelectorConditionBuilder $hasSelectorConditionBuilder = null,
  13. ) {
  14. $this->selectorListSplitter = $selectorListSplitter
  15. ?? new SelectorListSplitter();
  16. $this->notSelectorConditionBuilder = $notSelectorConditionBuilder
  17. ?? new NotSelectorConditionBuilder();
  18. $this->hasSelectorConditionBuilder = $hasSelectorConditionBuilder;
  19. }
  20. /**
  21. * @param array<string, mixed> $token
  22. * @param array<string, mixed>|null $next
  23. */
  24. public function apply(
  25. array $token,
  26. ?array $next,
  27. XPathExpression $expression,
  28. bool $htmlMode
  29. ):void {
  30. $pseudo = $token["content"];
  31. $specifier = $this->extractSpecifier($next);
  32. if(in_array($pseudo, self::BOOLEAN_ATTRIBUTES, true)) {
  33. $expression->appendFragment("[@{$pseudo}]");
  34. return;
  35. }
  36. $handlers = [
  37. "text" => fn() => $this->applyText($expression),
  38. "contains" => fn() => $this->applyContains($expression, $specifier),
  39. "not" => fn() => $this->applyNot($expression, $specifier, $htmlMode),
  40. "has" => fn() => $this->applyHas($expression, $specifier, $htmlMode),
  41. "first-child" => fn() => $expression->prependToLast("*[1]/self::"),
  42. "nth-child" => fn() => $this->applyNthChild($expression, $specifier),
  43. "last-child" => fn() => $expression->prependToLast("*[last()]/self::"),
  44. "first-of-type" => fn() => $expression->appendFragment("[1]"),
  45. "nth-of-type" => fn() => $this->applyNthOfType($expression, $specifier),
  46. "last-of-type" => fn() => $expression->appendFragment("[last()]"),
  47. ];
  48. $handler = $handlers[$pseudo] ?? null;
  49. if($handler !== null) {
  50. $handler();
  51. }
  52. }
  53. private function applyText(XPathExpression $expression):void {
  54. $expression->appendFragment('[@type="text"]');
  55. }
  56. private function applyContains(
  57. XPathExpression $expression,
  58. string $specifier
  59. ):void {
  60. if($specifier === "") {
  61. return;
  62. }
  63. $expression->appendFragment("[contains(text(),{$specifier})]");
  64. }
  65. private function applyNthChild(
  66. XPathExpression $expression,
  67. string $specifier
  68. ):void {
  69. if($specifier === "") {
  70. return;
  71. }
  72. if($expression->lastPartEndsWith("]")) {
  73. $replacement = " and position() = {$specifier}]";
  74. $expression->replaceInLast("]", $replacement);
  75. return;
  76. }
  77. $expression->appendFragment("[{$specifier}]");
  78. }
  79. private function applyNthOfType(
  80. XPathExpression $expression,
  81. string $specifier
  82. ):void {
  83. if($specifier === "") {
  84. return;
  85. }
  86. $expression->appendFragment("[{$specifier}]");
  87. }
  88. private function applyNot(
  89. XPathExpression $expression,
  90. string $specifier,
  91. bool $htmlMode
  92. ):void {
  93. $selectorList = $this->selectorListSplitter->split($specifier);
  94. if(empty($selectorList)) {
  95. return;
  96. }
  97. $conditions = [];
  98. foreach($selectorList as $selector) {
  99. $condition = $this->notSelectorConditionBuilder
  100. ->build($selector, $htmlMode);
  101. if($condition === null) {
  102. return;
  103. }
  104. $conditions[] = $condition;
  105. }
  106. $combined = count($conditions) === 1
  107. ? $conditions[0]
  108. : "(" . implode(" or ", $conditions) . ")";
  109. $expression->ensureElement();
  110. $expression->appendFragment("[not({$combined})]");
  111. }
  112. private function applyHas(
  113. XPathExpression $expression,
  114. string $specifier,
  115. bool $htmlMode
  116. ):void {
  117. $condition = $this->getHasSelectorConditionBuilder()
  118. ->build($specifier, $htmlMode);
  119. if($condition === null) {
  120. return;
  121. }
  122. $expression->ensureElement();
  123. $expression->appendFragment("[{$condition}]");
  124. }
  125. private function getHasSelectorConditionBuilder():HasSelectorConditionBuilder {
  126. return $this->hasSelectorConditionBuilder
  127. ??= new HasSelectorConditionBuilder();
  128. }
  129. /** @param array<string, mixed>|null $next */
  130. private function extractSpecifier(?array $next):string {
  131. if(!$next || $next["type"] !== "pseudospecifier") {
  132. return "";
  133. }
  134. return (string)$next["content"];
  135. }
  136. }