1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\CssSelector\Parser; 13 14use Symfony\Component\CssSelector\Exception\InternalErrorException; 15use Symfony\Component\CssSelector\Exception\SyntaxErrorException; 16 17/** 18 * CSS selector token stream. 19 * 20 * This component is a port of the Python cssselect library, 21 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. 22 * 23 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> 24 * 25 * @internal 26 */ 27class TokenStream 28{ 29 /** 30 * @var Token[] 31 */ 32 private $tokens = []; 33 34 /** 35 * @var Token[] 36 */ 37 private $used = []; 38 39 /** 40 * @var int 41 */ 42 private $cursor = 0; 43 44 /** 45 * @var Token|null 46 */ 47 private $peeked; 48 49 /** 50 * @var bool 51 */ 52 private $peeking = false; 53 54 /** 55 * Pushes a token. 56 * 57 * @return $this 58 */ 59 public function push(Token $token): self 60 { 61 $this->tokens[] = $token; 62 63 return $this; 64 } 65 66 /** 67 * Freezes stream. 68 * 69 * @return $this 70 */ 71 public function freeze(): self 72 { 73 return $this; 74 } 75 76 /** 77 * Returns next token. 78 * 79 * @throws InternalErrorException If there is no more token 80 */ 81 public function getNext(): Token 82 { 83 if ($this->peeking) { 84 $this->peeking = false; 85 $this->used[] = $this->peeked; 86 87 return $this->peeked; 88 } 89 90 if (!isset($this->tokens[$this->cursor])) { 91 throw new InternalErrorException('Unexpected token stream end.'); 92 } 93 94 return $this->tokens[$this->cursor++]; 95 } 96 97 /** 98 * Returns peeked token. 99 */ 100 public function getPeek(): Token 101 { 102 if (!$this->peeking) { 103 $this->peeked = $this->getNext(); 104 $this->peeking = true; 105 } 106 107 return $this->peeked; 108 } 109 110 /** 111 * Returns used tokens. 112 * 113 * @return Token[] 114 */ 115 public function getUsed(): array 116 { 117 return $this->used; 118 } 119 120 /** 121 * Returns next identifier token. 122 * 123 * @throws SyntaxErrorException If next token is not an identifier 124 */ 125 public function getNextIdentifier(): string 126 { 127 $next = $this->getNext(); 128 129 if (!$next->isIdentifier()) { 130 throw SyntaxErrorException::unexpectedToken('identifier', $next); 131 } 132 133 return $next->getValue(); 134 } 135 136 /** 137 * Returns next identifier or null if star delimiter token is found. 138 * 139 * @throws SyntaxErrorException If next token is not an identifier or a star delimiter 140 */ 141 public function getNextIdentifierOrStar(): ?string 142 { 143 $next = $this->getNext(); 144 145 if ($next->isIdentifier()) { 146 return $next->getValue(); 147 } 148 149 if ($next->isDelimiter(['*'])) { 150 return null; 151 } 152 153 throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next); 154 } 155 156 /** 157 * Skips next whitespace if any. 158 */ 159 public function skipWhitespace() 160 { 161 $peek = $this->getPeek(); 162 163 if ($peek->isWhitespace()) { 164 $this->getNext(); 165 } 166 } 167} 168