NotSelectorConditionBuilder.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. <?php
  2. namespace Gt\CssXPath;
  3. class NotSelectorConditionBuilder {
  4. private ThreadMatcher $threadMatcher;
  5. private AttributeSelectorConverter $attributeSelectorConverter;
  6. public function __construct(
  7. ?ThreadMatcher $threadMatcher = null,
  8. ?AttributeSelectorConverter $attributeSelectorConverter = null,
  9. ) {
  10. $this->threadMatcher = $threadMatcher ?? new ThreadMatcher();
  11. $this->attributeSelectorConverter = $attributeSelectorConverter
  12. ?? new AttributeSelectorConverter();
  13. }
  14. public function build(string $selector, bool $htmlMode):?string {
  15. $selector = trim($selector);
  16. if($selector === "") {
  17. return null;
  18. }
  19. $thread = array_values(
  20. $this->threadMatcher->collate(Translator::CSS_REGEX, $selector)
  21. );
  22. if(!$this->isSupportedThread($thread)) {
  23. return null;
  24. }
  25. $token = $thread[0];
  26. $next = $thread[1] ?? null;
  27. return $this->buildConditionFromToken($token, $next, $htmlMode);
  28. }
  29. /** @param array<int, array<string, mixed>> $thread */
  30. private function isSupportedThread(array $thread):bool {
  31. if(empty($thread) || count($thread) > 2) {
  32. return false;
  33. }
  34. foreach($thread as $token) {
  35. if($this->isAxisToken((string)$token["type"])) {
  36. return false;
  37. }
  38. }
  39. return true;
  40. }
  41. private function isAxisToken(string $type):bool {
  42. return in_array($type, [
  43. "descendant",
  44. "child",
  45. "sibling",
  46. "subsequentsibling",
  47. ], true);
  48. }
  49. /**
  50. * @param array<string, mixed> $token
  51. * @param array<string, mixed>|null $next
  52. */
  53. private function buildConditionFromToken(
  54. array $token,
  55. ?array $next,
  56. bool $htmlMode
  57. ):?string {
  58. $type = (string)$token["type"];
  59. if($this->isElementType($type)) {
  60. return $this->buildElementCondition(
  61. (string)$token["content"],
  62. $htmlMode
  63. );
  64. }
  65. return $this->buildNonElementCondition($type, $token, $next, $htmlMode);
  66. }
  67. private function isElementType(string $type):bool {
  68. return in_array($type, ["element", "star"], true);
  69. }
  70. /**
  71. * @param array<string, mixed> $token
  72. * @param array<string, mixed>|null $next
  73. */
  74. private function buildNonElementCondition(
  75. string $type,
  76. array $token,
  77. ?array $next,
  78. bool $htmlMode
  79. ):?string {
  80. return match($type) {
  81. "id" => "@id='" . $token["content"] . "'",
  82. "class" => $this->buildClassCondition((string)$token["content"]),
  83. "attribute" => $this
  84. ->attributeSelectorConverter
  85. ->buildConditionFromToken($token, $htmlMode),
  86. "pseudo" => $this->buildPseudoCondition($token, $next),
  87. default => null,
  88. };
  89. }
  90. private function buildClassCondition(string $className):string {
  91. return ""
  92. . "contains(concat(' ',normalize-space(@class),' '),"
  93. . "' {$className} ')";
  94. }
  95. /**
  96. * @param array<string, mixed> $token
  97. * @param array<string, mixed>|null $next
  98. */
  99. private function buildPseudoCondition(array $token, ?array $next):?string {
  100. $pseudo = (string)$token["content"];
  101. $specifier = $this->extractSpecifier($next);
  102. if(in_array($pseudo, ["disabled", "checked", "selected"], true)) {
  103. return "@{$pseudo}";
  104. }
  105. return match($pseudo) {
  106. "text" => '@type="text"',
  107. "contains" => $specifier !== ""
  108. ? "contains(text(),{$specifier})"
  109. : null,
  110. "first-child", "first-of-type" => "position() = 1",
  111. "nth-child", "nth-of-type" => $specifier !== ""
  112. ? "position() = {$specifier}"
  113. : null,
  114. "last-child", "last-of-type" => "position() = last()",
  115. default => null,
  116. };
  117. }
  118. private function buildElementCondition(string $name, bool $htmlMode):string {
  119. if($name === "*") {
  120. return "self::*";
  121. }
  122. $element = $htmlMode ? strtolower($name) : $name;
  123. return "self::{$element}";
  124. }
  125. /** @param array<string, mixed>|null $next */
  126. private function extractSpecifier(?array $next):string {
  127. if(!$next || $next["type"] !== "pseudospecifier") {
  128. return "";
  129. }
  130. return (string)$next["content"];
  131. }
  132. }