SingleSelectorConverter.php 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. <?php
  2. namespace Gt\CssXPath;
  3. class SingleSelectorConverter {
  4. private ThreadMatcher $threadMatcher;
  5. private PseudoSelectorConverter $pseudoSelectorConverter;
  6. private AttributeSelectorConverter $attributeSelectorConverter;
  7. public function __construct(
  8. ?ThreadMatcher $threadMatcher = null,
  9. ?PseudoSelectorConverter $pseudoSelectorConverter = null,
  10. ?AttributeSelectorConverter $attributeSelectorConverter = null,
  11. ) {
  12. $this->threadMatcher = $threadMatcher ?? new ThreadMatcher();
  13. $this->pseudoSelectorConverter = $pseudoSelectorConverter
  14. ?? new PseudoSelectorConverter();
  15. $this->attributeSelectorConverter = $attributeSelectorConverter
  16. ?? new AttributeSelectorConverter();
  17. }
  18. public function convert(
  19. string $css,
  20. string $prefix,
  21. bool $htmlMode
  22. ):string {
  23. $thread = array_values(
  24. array_filter(
  25. $this->threadMatcher->collate(Translator::CSS_REGEX, $css)
  26. )
  27. );
  28. $expression = new XPathExpression($prefix);
  29. foreach($thread as $index => $token) {
  30. $next = $thread[$index + 1] ?? null;
  31. $this->applyToken($token, $next, $expression, $htmlMode);
  32. }
  33. return $expression->toString();
  34. }
  35. /**
  36. * @param array<string, mixed> $token
  37. * @param array<string, mixed>|null $next
  38. */
  39. private function applyToken(
  40. array $token,
  41. ?array $next,
  42. XPathExpression $expression,
  43. bool $htmlMode
  44. ):void {
  45. $handlers = [
  46. "star" => fn() => $expression
  47. ->appendElement((string)$token["content"], $htmlMode),
  48. "element" => fn() => $expression
  49. ->appendElement((string)$token["content"], $htmlMode),
  50. "pseudo" => fn() => $this->pseudoSelectorConverter
  51. ->apply($token, $next, $expression, $htmlMode),
  52. "child" => fn() => $this->appendAxis($expression, "/"),
  53. "id" => fn() => $this->appendId($expression, (string)$token["content"]),
  54. "class" => fn() => $this
  55. ->appendClass($expression, (string)$token["content"]),
  56. "sibling" => fn() => $this->appendAxis(
  57. $expression,
  58. "/following-sibling::*[1]/self::"
  59. ),
  60. "subsequentsibling" => fn() => $this->appendAxis(
  61. $expression,
  62. "/following-sibling::"
  63. ),
  64. "attribute" => fn() => $this->attributeSelectorConverter
  65. ->apply($token, $expression, $htmlMode),
  66. "descendant" => fn() => $this->appendAxis($expression, "//"),
  67. ];
  68. $handler = $handlers[$token["type"]] ?? null;
  69. if($handler !== null) {
  70. $handler();
  71. }
  72. }
  73. private function appendAxis(XPathExpression $expression, string $axis):void {
  74. $expression->appendFragment($axis);
  75. $expression->markElementMissing();
  76. }
  77. private function appendId(
  78. XPathExpression $expression,
  79. string $identifier
  80. ):void {
  81. $expression->ensureElement();
  82. $expression->appendFragment("[@id='{$identifier}']");
  83. }
  84. private function appendClass(
  85. XPathExpression $expression,
  86. string $className
  87. ):void {
  88. $expression->ensureElement();
  89. $expression->appendFragment(
  90. "[contains(concat(' ',normalize-space(@class),' '),' {$className} ')]"
  91. );
  92. }
  93. }