ast
14 years ago
cache
14 years ago
XmlImportConfig.php
14 years ago
XmlImportCsvParse.php
14 years ago
XmlImportException.php
14 years ago
XmlImportParser.php
14 years ago
XmlImportReaderInterface.php
14 years ago
XmlImportStringReader.php
14 years ago
XmlImportTemplate.php
14 years ago
XmlImportTemplateCodeGenerator.php
14 years ago
XmlImportTemplateParser.php
14 years ago
XmlImportTemplateScanner.php
14 years ago
XmlImportToken.php
14 years ago
XmlImportTemplateScanner.php
403 lines
| 1 | <?php |
| 2 | /** |
| 3 | * @author Olexandr Zanichkovsky <olexandr.zanichkovsky@zophiatech.com> |
| 4 | * @package General |
| 5 | */ |
| 6 | |
| 7 | require_once dirname(__FILE__) . '/XmlImportToken.php'; |
| 8 | require_once dirname(__FILE__) . '/XmlImportException.php'; |
| 9 | |
| 10 | /** |
| 11 | * Used to scan string into a list of tokens |
| 12 | */ |
| 13 | final class XmlImportTemplateScanner |
| 14 | { |
| 15 | /** |
| 16 | * Language keywords |
| 17 | * |
| 18 | * @var array |
| 19 | */ |
| 20 | private $keywords = array( |
| 21 | 'IF', |
| 22 | 'ELSEIF', |
| 23 | 'ELSE', |
| 24 | 'ENDIF', |
| 25 | 'FOREACH', |
| 26 | 'ENDFOREACH', |
| 27 | 'WITH', |
| 28 | 'ENDWITH', |
| 29 | 'MATH' |
| 30 | ); |
| 31 | |
| 32 | /** |
| 33 | * Parsing text |
| 34 | */ |
| 35 | const STATE_TEXT = 'STATE_TEXT'; |
| 36 | |
| 37 | /** |
| 38 | * Parsing XPath |
| 39 | */ |
| 40 | const STATE_XPATH = 'STATE_XPATH'; |
| 41 | |
| 42 | /** |
| 43 | * Parsing Language |
| 44 | */ |
| 45 | const STATE_LANG = 'STATE_LANG'; |
| 46 | |
| 47 | /** |
| 48 | * Whether it is lang block start |
| 49 | * |
| 50 | * @var bool |
| 51 | */ |
| 52 | private $isLangBegin = false; |
| 53 | |
| 54 | /** |
| 55 | * Current parsing state |
| 56 | * |
| 57 | * @var string |
| 58 | */ |
| 59 | private $currentState = XmlImportTemplateScanner::STATE_TEXT; |
| 60 | |
| 61 | /** |
| 62 | * Scans template from XmlImportReaderInterface and returns the list of tokens |
| 63 | * |
| 64 | * @param XmlImportReaderInterface $input |
| 65 | * @return array |
| 66 | */ |
| 67 | public function scan(XmlImportReaderInterface $input) |
| 68 | { |
| 69 | $results = array(); |
| 70 | while (($ch = $input->peek()) !== false) |
| 71 | { |
| 72 | switch ($this->currentState) |
| 73 | { |
| 74 | case XmlImportTemplateScanner::STATE_TEXT: |
| 75 | if ($ch == '[') |
| 76 | { |
| 77 | $this->currentState = XmlImportTemplateScanner::STATE_LANG; |
| 78 | $this->isLangBegin = true; |
| 79 | //omit [ |
| 80 | $input->read(); |
| 81 | } |
| 82 | elseif ($ch == '{') |
| 83 | { |
| 84 | $this->currentState = XmlImportTemplateScanner::STATE_XPATH; |
| 85 | //omit { |
| 86 | $input->read(); |
| 87 | } |
| 88 | else |
| 89 | { |
| 90 | $results[] = $this->scanText($input); |
| 91 | } |
| 92 | break; |
| 93 | case XmlImportTemplateScanner::STATE_XPATH: |
| 94 | $results = array_merge($results, $this->scanXPath($input, false)); |
| 95 | break; |
| 96 | case XmlImportTemplateScanner::STATE_LANG: |
| 97 | $ch = $input->peek(); |
| 98 | |
| 99 | if (preg_match('/\s/', $ch)) |
| 100 | { |
| 101 | //omit space |
| 102 | $input->read(); |
| 103 | } |
| 104 | elseif (preg_match('/[_a-z]/i', $ch)) |
| 105 | { |
| 106 | $result = $this->scanName($input); |
| 107 | if (is_array($result)) |
| 108 | $results = array_merge($results, $result); |
| 109 | else |
| 110 | $results[] = $result; |
| 111 | } |
| 112 | elseif (preg_match('/(\d|-)/', $ch)) |
| 113 | { |
| 114 | $result = $this->scanNumber($input); |
| 115 | if (is_array($result)) |
| 116 | $results = array_merge($results, $result); |
| 117 | else |
| 118 | $results[] = $result; |
| 119 | } |
| 120 | elseif ($ch == "+" || $ch == "-" || $ch == "/" || $ch == "*") |
| 121 | { |
| 122 | $this->isLangBegin = false; |
| 123 | $input->read(); |
| 124 | $results[] = new XmlImportToken(XmlImportToken::KIND_OPERATION, $ch); |
| 125 | } |
| 126 | elseif ($ch == '"') |
| 127 | { |
| 128 | //omit " |
| 129 | $input->read(); |
| 130 | $result = $this->scanString($input); |
| 131 | if (is_array($result)) |
| 132 | $results = array_merge($results, $result); |
| 133 | else |
| 134 | $results[] = $result; |
| 135 | } |
| 136 | elseif ($ch == '{') |
| 137 | { |
| 138 | $input->read(); |
| 139 | $result = $this->scanXPath($input); |
| 140 | if (is_array($result)) |
| 141 | $results = array_merge($results, $result); |
| 142 | else |
| 143 | $results[] = $result; |
| 144 | } |
| 145 | elseif ($ch == '(') |
| 146 | { |
| 147 | $this->isLangBegin = false; |
| 148 | $input->read(); |
| 149 | $results[] = new XmlImportToken(XmlImportToken::KIND_OPEN); |
| 150 | } |
| 151 | elseif ($ch == ')') |
| 152 | { |
| 153 | $this->isLangBegin = false; |
| 154 | $input->read(); |
| 155 | $results[] = new XmlImportToken(XmlImportToken::KIND_CLOSE); |
| 156 | } |
| 157 | elseif ($ch == ',') |
| 158 | { |
| 159 | $this->isLangBegin = false; |
| 160 | $input->read(); |
| 161 | $results[] = new XmlImportToken(XmlImportToken::KIND_COMMA); |
| 162 | } |
| 163 | elseif ($ch == ']') |
| 164 | { |
| 165 | $this->isLangBegin = false; |
| 166 | $this->currentState = XmlImportTemplateScanner::STATE_TEXT; |
| 167 | //omit ] |
| 168 | $input->read(); |
| 169 | } |
| 170 | else |
| 171 | throw new XmlImportException("Unexpected symbol '$ch'"); |
| 172 | |
| 173 | break; |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | return $results; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Scans text |
| 182 | * |
| 183 | * @param XmlImportReaderInterface $input |
| 184 | * @return XmlImportToken |
| 185 | */ |
| 186 | private function scanText($input) |
| 187 | { |
| 188 | $accum = $input->read(); |
| 189 | while (($ch = $input->peek()) !== false) |
| 190 | { |
| 191 | if ($ch == '{' && $accum[strlen($accum) - 1] != "\\") |
| 192 | { |
| 193 | $this->currentState = XmlImportTemplateScanner::STATE_XPATH; |
| 194 | //omit { |
| 195 | $input->read(); |
| 196 | break; |
| 197 | } |
| 198 | elseif ($ch == '[' && $accum[strlen($accum) - 1] != "\\") |
| 199 | { |
| 200 | $this->currentState = XmlImportTemplateScanner::STATE_LANG; |
| 201 | $this->isLangBegin = true; |
| 202 | //omit [ |
| 203 | $input->read(); |
| 204 | break; |
| 205 | } |
| 206 | else |
| 207 | $accum .= $input->read(); |
| 208 | } |
| 209 | $accum = str_replace(array("\\[", "\\{"), array('[', '{'), $accum); |
| 210 | return new XmlImportToken(XmlImportToken::KIND_TEXT, $accum); |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Scans XPath |
| 215 | * |
| 216 | * @param XmlImportReaderInterface $input |
| 217 | * @param bool $insideLang |
| 218 | * @return XmlImportToken |
| 219 | */ |
| 220 | private function scanXPath($input, $insideLang = true) |
| 221 | { |
| 222 | $accum = ''; |
| 223 | while(($ch = $input->peek()) !== false) |
| 224 | { |
| 225 | if ($ch == '}' && (strlen($accum) == 0 || $accum[strlen($accum) - 1] != "\\")) |
| 226 | { |
| 227 | //skip } |
| 228 | $input->read(); |
| 229 | $accum = str_replace("\\}", '}', $accum); |
| 230 | if ($insideLang) |
| 231 | { |
| 232 | if ($this->isLangBegin) |
| 233 | { |
| 234 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_XPATH, $accum)); |
| 235 | } |
| 236 | else |
| 237 | return new XmlImportToken(XmlImportToken::KIND_XPATH, $accum); |
| 238 | } |
| 239 | else |
| 240 | { |
| 241 | $this->currentState = XmlImportTemplateScanner::STATE_TEXT; |
| 242 | |
| 243 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_XPATH, $accum)); |
| 244 | } |
| 245 | } |
| 246 | else |
| 247 | $accum .= $input->read(); |
| 248 | } |
| 249 | throw new XmlImportException('Unexpected end of XPath expression \'' . $accum . '\''); |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Scans name |
| 254 | * |
| 255 | * @param XmlImportReaderInterface $input |
| 256 | * @return XmlImportToken |
| 257 | */ |
| 258 | private function scanName(XmlImportReaderInterface $input) |
| 259 | { |
| 260 | $accum = $input->read(); |
| 261 | while (preg_match('/[_a-z0-9]/i', $input->peek())) |
| 262 | { |
| 263 | $accum .= $input->read(); |
| 264 | if ($input->peek() === false) |
| 265 | throw new XmlImportException("Unexpected end of function or keyword name \"$accum\""); |
| 266 | } |
| 267 | if (in_array(strtoupper($accum), $this->keywords)) |
| 268 | { |
| 269 | return new XmlImportToken(strtoupper($accum)); |
| 270 | } |
| 271 | else |
| 272 | { |
| 273 | if ($this->isLangBegin) |
| 274 | { |
| 275 | $this->isLangBegin = false; |
| 276 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_FUNCTION, $accum)); |
| 277 | } |
| 278 | else |
| 279 | return new XmlImportToken(XmlImportToken::KIND_FUNCTION, $accum); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * Scans string literal |
| 285 | * |
| 286 | * @param XmlImportReaderInterface $input |
| 287 | * @return XmlImportToken |
| 288 | */ |
| 289 | private function scanString(XmlImportReaderInterface $input) |
| 290 | { |
| 291 | $accum = ''; |
| 292 | while(($ch = $input->peek()) !== false) |
| 293 | { |
| 294 | if ($ch == '"' && (strlen($accum) == 0 || $accum[strlen($accum) - 1] != "\\")) |
| 295 | { |
| 296 | //skip " |
| 297 | $input->read(); |
| 298 | $accum = str_replace("\\\"", '"', $accum); |
| 299 | if ($this->isLangBegin) |
| 300 | { |
| 301 | $this->isLangBegin = false; |
| 302 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_STRING, $accum)); |
| 303 | } |
| 304 | else |
| 305 | { |
| 306 | return new XmlImportToken(XmlImportToken::KIND_STRING, $accum); |
| 307 | } |
| 308 | } |
| 309 | else |
| 310 | $accum .= $input->read(); |
| 311 | } |
| 312 | throw new XmlImportException('Unexpected end of string literal "' . $accum . '"'); |
| 313 | } |
| 314 | |
| 315 | /** |
| 316 | * Scans number |
| 317 | * |
| 318 | * @param XmlImportReaderInterface $input |
| 319 | * @return XmlImportToken |
| 320 | */ |
| 321 | private function scanNumber(XmlImportReaderInterface $input) |
| 322 | { |
| 323 | $isInt = true; |
| 324 | $accum = $this->scanInt($input); |
| 325 | if ($input->peek() == '.') |
| 326 | { |
| 327 | $isInt = false; |
| 328 | $accum .= $input->read(); |
| 329 | $accum .= $this->scanNumberFrac($input); |
| 330 | } |
| 331 | if (strtolower($input->peek()) == 'e' ) |
| 332 | { |
| 333 | $isInt = false; |
| 334 | $accum .= $input->read(); |
| 335 | $accum .= $this->scanInt($input); |
| 336 | } |
| 337 | if ($isInt) |
| 338 | { |
| 339 | if ($this->isLangBegin) |
| 340 | { |
| 341 | $this->isLangBegin = false; |
| 342 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_INT, intval($accum))); |
| 343 | } |
| 344 | else |
| 345 | { |
| 346 | return new XmlImportToken(XmlImportToken::KIND_INT, intval($accum)); |
| 347 | } |
| 348 | } |
| 349 | else |
| 350 | { |
| 351 | if ($this->isLangBegin) |
| 352 | { |
| 353 | $this->isLangBegin = false; |
| 354 | return array(new XmlImportToken(XmlImportToken::KIND_PRINT), new XmlImportToken(XmlImportToken::KIND_FLOAT, floatval($accum))); |
| 355 | } |
| 356 | else |
| 357 | { |
| 358 | return new XmlImportToken(XmlImportToken::KIND_FLOAT, floatval($accum)); |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | /** |
| 364 | * Scans integer number |
| 365 | * |
| 366 | * @param XmlImportReaderInterface $input |
| 367 | * @return string |
| 368 | */ |
| 369 | private function scanInt(XmlImportReaderInterface $input) |
| 370 | { |
| 371 | if (preg_match('/(\d|-)/', $input->peek())) |
| 372 | { |
| 373 | $accum = $input->read(); |
| 374 | if ($accum == '-' && !preg_match('/\d/', $input->peek())) |
| 375 | throw new XmlImportException("Expected digit after a minus"); |
| 376 | while (preg_match('/\d/', $input->peek())) |
| 377 | { |
| 378 | $accum .= $input->read(); |
| 379 | } |
| 380 | return $accum; |
| 381 | } |
| 382 | else |
| 383 | throw new XmlImportException("digit or '-' expected in a number"); |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Scans fraction part of a number |
| 388 | * |
| 389 | * @param XmlImportReaderInterface $input |
| 390 | * @return string |
| 391 | */ |
| 392 | private function scanNumberFrac(XmlImportReaderInterface $input) |
| 393 | { |
| 394 | $accum = ''; |
| 395 | while (preg_match('/\d/', $input->peek())) |
| 396 | { |
| 397 | $accum .= $input->read(); |
| 398 | } |
| 399 | if (strlen($accum) == 0) |
| 400 | throw new XmlImportException("Digits are expected after a '.'"); |
| 401 | return $accum; |
| 402 | } |
| 403 | } |