1<?php 2 3namespace Gettext\Utils; 4 5class JsFunctionsScanner extends FunctionsScanner 6{ 7 protected $code; 8 protected $status = []; 9 10 /** 11 * Constructor. 12 * 13 * @param string $code The php code to scan 14 */ 15 public function __construct($code) 16 { 17 // Normalize newline characters 18 $this->code = str_replace(["\r\n", "\n\r", "\r"], "\n", $code); 19 } 20 21 /** 22 * {@inheritdoc} 23 */ 24 public function getFunctions(array $constants = []) 25 { 26 $length = strlen($this->code); 27 $line = 1; 28 $buffer = ''; 29 $functions = []; 30 $bufferFunctions = []; 31 $char = null; 32 33 for ($pos = 0; $pos < $length; ++$pos) { 34 $prev = $char; 35 $char = $this->code[$pos]; 36 $next = isset($this->code[$pos + 1]) ? $this->code[$pos + 1] : null; 37 38 switch ($char) { 39 case '\\': 40 switch ($this->status()) { 41 case 'simple-quote': 42 if ($next !== "'") { 43 break 2; 44 } 45 break; 46 47 case 'double-quote': 48 if ($next !== '"') { 49 break 2; 50 } 51 break; 52 53 case 'back-tick': 54 if ($next !== '`') { 55 break 2; 56 } 57 break; 58 } 59 60 $prev = $char; 61 $char = $next; 62 $pos++; 63 $next = isset($this->code[$pos]) ? $this->code[$pos] : null; 64 break; 65 66 case "\n": 67 ++$line; 68 69 if ($this->status('line-comment')) { 70 $this->upStatus(); 71 } 72 break; 73 74 case '/': 75 switch ($this->status()) { 76 case 'simple-quote': 77 case 'double-quote': 78 case 'back-tick': 79 case 'line-comment': 80 break; 81 82 case 'block-comment': 83 if ($prev === '*') { 84 $this->upStatus(); 85 } 86 break; 87 88 default: 89 if ($next === '/') { 90 $this->downStatus('line-comment'); 91 } elseif ($next === '*') { 92 $this->downStatus('block-comment'); 93 } 94 break; 95 } 96 break; 97 98 case "'": 99 switch ($this->status()) { 100 case 'simple-quote': 101 $this->upStatus(); 102 break; 103 104 case 'line-comment': 105 case 'block-comment': 106 case 'double-quote': 107 case 'back-tick': 108 break; 109 110 default: 111 $this->downStatus('simple-quote'); 112 break; 113 } 114 break; 115 116 case '"': 117 switch ($this->status()) { 118 case 'double-quote': 119 $this->upStatus(); 120 break; 121 122 case 'line-comment': 123 case 'block-comment': 124 case 'simple-quote': 125 case 'back-tick': 126 break; 127 128 default: 129 $this->downStatus('double-quote'); 130 break; 131 } 132 break; 133 134 case '`': 135 switch ($this->status()) { 136 case 'back-tick': 137 $this->upStatus(); 138 break; 139 140 case 'line-comment': 141 case 'block-comment': 142 case 'simple-quote': 143 case 'double-quote': 144 break; 145 146 default: 147 $this->downStatus('back-tick'); 148 break; 149 } 150 break; 151 152 case '(': 153 switch ($this->status()) { 154 case 'simple-quote': 155 case 'double-quote': 156 case 'back-tick': 157 case 'line-comment': 158 case 'block-comment': 159 break; 160 161 default: 162 if ($buffer && preg_match('/(\w+)$/', $buffer, $matches)) { 163 $this->downStatus('function'); 164 array_unshift($bufferFunctions, [$matches[1], $line, []]); 165 $buffer = ''; 166 continue 3; 167 } 168 break; 169 } 170 break; 171 172 case ')': 173 switch ($this->status()) { 174 case 'function': 175 if (($argument = static::prepareArgument($buffer))) { 176 $bufferFunctions[0][2][] = $argument; 177 } 178 179 if (!empty($bufferFunctions)) { 180 $functions[] = array_shift($bufferFunctions); 181 } 182 183 $this->upStatus(); 184 $buffer = ''; 185 continue 3; 186 } 187 break; 188 189 case ',': 190 switch ($this->status()) { 191 case 'function': 192 if (($argument = static::prepareArgument($buffer))) { 193 $bufferFunctions[0][2][] = $argument; 194 } 195 196 $buffer = ''; 197 continue 3; 198 } 199 break; 200 201 case ' ': 202 case '\t': 203 switch ($this->status()) { 204 case 'double-quote': 205 case 'simple-quote': 206 case 'back-tick': 207 break; 208 209 default: 210 $buffer = ''; 211 continue 3; 212 } 213 break; 214 } 215 216 switch ($this->status()) { 217 case 'line-comment': 218 case 'block-comment': 219 break; 220 221 default: 222 $buffer .= $char; 223 break; 224 } 225 } 226 227 return $functions; 228 } 229 230 /** 231 * Get the current context of the scan. 232 * 233 * @param null|string $match To check whether the current status is this value 234 * 235 * @return string|bool 236 */ 237 protected function status($match = null) 238 { 239 $status = isset($this->status[0]) ? $this->status[0] : null; 240 241 if ($match !== null) { 242 return $status === $match; 243 } 244 245 return $status; 246 } 247 248 /** 249 * Add a new status to the stack. 250 * 251 * @param string $status 252 */ 253 protected function downStatus($status) 254 { 255 array_unshift($this->status, $status); 256 } 257 258 /** 259 * Removes and return the current status. 260 * 261 * @return string|null 262 */ 263 protected function upStatus() 264 { 265 return array_shift($this->status); 266 } 267 268 /** 269 * Prepares the arguments found in functions. 270 * 271 * @param string $argument 272 * 273 * @return string 274 */ 275 protected static function prepareArgument($argument) 276 { 277 if ($argument && in_array($argument[0], ['"', "'", '`'], true)) { 278 return static::convertString(substr($argument, 1, -1)); 279 } 280 } 281 282 /** 283 * Decodes a string with an argument. 284 * 285 * @param string $value 286 * 287 * @return string 288 */ 289 protected static function convertString($value) 290 { 291 if (strpos($value, '\\') === false) { 292 return $value; 293 } 294 295 return preg_replace_callback( 296 '/\\\(n|r|t|v|e|f|"|\\\)/', 297 function ($match) { 298 switch ($match[1][0]) { 299 case 'n': 300 return "\n"; 301 case 'r': 302 return "\r"; 303 case 't': 304 return "\t"; 305 case 'v': 306 return "\v"; 307 case 'e': 308 return "\e"; 309 case 'f': 310 return "\f"; 311 case '"': 312 return '"'; 313 case '\\': 314 return '\\'; 315 } 316 }, 317 $value 318 ); 319 } 320} 321