/** * Parse mask and array of default values; initializes object. * @param string * @param array * @return void */ private function setMask($mask, array $metadata) { $this->mask = $mask; // detect '//host/path' vs. '/abs. path' vs. 'relative path' if (substr($mask, 0, 2) === '//') { $this->type = self::HOST; } elseif (substr($mask, 0, 1) === '/') { $this->type = self::PATH; } else { $this->type = self::RELATIVE; } foreach ($metadata as $name => $meta) { if (!is_array($meta)) { $metadata[$name] = array(self::VALUE => $meta, 'fixity' => self::CONSTANT); } elseif (array_key_exists(self::VALUE, $meta)) { $metadata[$name]['fixity'] = self::CONSTANT; } } // PARSE MASK // <parameter-name[=default] [pattern] [#class]> or [ or ] or ?... $parts = Strings::split($mask, '/<([^>#= ]+)(=[^># ]*)? *([^>#]*)(#?[^>\\[\\]]*)>|(\\[!?|\\]|\\s*\\?.*)/'); $this->xlat = array(); $i = count($parts) - 1; // PARSE QUERY PART OF MASK if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') { // name=<parameter-name [pattern][#class]> $matches = Strings::matchAll($parts[$i - 1], '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/'); foreach ($matches as $match) { list(, $param, $name, $pattern, $class) = $match; // $pattern is not used if ($class !== '') { if (!isset(self::$styles[$class])) { throw new InvalidStateException("Parameter '{$name}' has '{$class}' flag, but Route::\$styles['{$class}'] is not set."); } $meta = self::$styles[$class]; } elseif (isset(self::$styles['?' . $name])) { $meta = self::$styles['?' . $name]; } else { $meta = self::$styles['?#']; } if (isset($metadata[$name])) { $meta = $metadata[$name] + $meta; } if (array_key_exists(self::VALUE, $meta)) { $meta['fixity'] = self::OPTIONAL; } unset($meta['pattern']); $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]); $metadata[$name] = $meta; if ($param !== '') { $this->xlat[$name] = $param; } } $i -= 6; } // PARSE PATH PART OF MASK $brackets = 0; // optional level $re = ''; $sequence = array(); $autoOptional = TRUE; do { array_unshift($sequence, $parts[$i]); $re = preg_quote($parts[$i], '#') . $re; if ($i === 0) { break; } $i--; $part = $parts[$i]; // [ or ] if ($part === '[' || $part === ']' || $part === '[!') { $brackets += $part[0] === '[' ? -1 : 1; if ($brackets < 0) { throw new InvalidArgumentException("Unexpected '{$part}' in mask '{$mask}'."); } array_unshift($sequence, $part); $re = ($part[0] === '[' ? '(?:' : ')?') . $re; $i -= 5; continue; } $class = $parts[$i]; $i--; // validation class $pattern = trim($parts[$i]); $i--; // validation condition (as regexp) $default = $parts[$i]; $i--; // default value $name = $parts[$i]; $i--; // parameter name array_unshift($sequence, $name); if ($name[0] === '?') { // "foo" parameter $re = '(?:' . preg_quote(substr($name, 1), '#') . '|' . $pattern . ')' . $re; $sequence[1] = substr($name, 1) . $sequence[1]; continue; } // check name (limitation by regexp) if (preg_match('#[^a-z0-9_-]#i', $name)) { throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '{$name}' given."); } // pattern, condition & metadata if ($class !== '') { if (!isset(self::$styles[$class])) { throw new InvalidStateException("Parameter '{$name}' has '{$class}' flag, but Route::\$styles['{$class}'] is not set."); } $meta = self::$styles[$class]; } elseif (isset(self::$styles[$name])) { $meta = self::$styles[$name]; } else { $meta = self::$styles['#']; } if (isset($metadata[$name])) { $meta = $metadata[$name] + $meta; } if ($pattern == '' && isset($meta[self::PATTERN])) { $pattern = $meta[self::PATTERN]; } if ($default !== '') { $meta[self::VALUE] = (string) substr($default, 1); $meta['fixity'] = self::PATH_OPTIONAL; } $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]); if (array_key_exists(self::VALUE, $meta)) { if (isset($meta['filterTable2'][$meta[self::VALUE]])) { $meta['defOut'] = $meta['filterTable2'][$meta[self::VALUE]]; } elseif (isset($meta[self::FILTER_OUT])) { $meta['defOut'] = call_user_func($meta[self::FILTER_OUT], $meta[self::VALUE]); } else { $meta['defOut'] = $meta[self::VALUE]; } } $meta[self::PATTERN] = "#(?:{$pattern})\$#A" . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu'); // include in expression $re = '(?P<' . str_replace('-', '___', $name) . '>(?U)' . $pattern . ')' . $re; // str_replace is dirty trick to enable '-' in parameter name if ($brackets) { // is in brackets? if (!isset($meta[self::VALUE])) { $meta[self::VALUE] = $meta['defOut'] = NULL; } $meta['fixity'] = self::PATH_OPTIONAL; } elseif (!$autoOptional) { unset($meta['fixity']); } elseif (isset($meta['fixity'])) { // auto-optional $re = '(?:' . $re . ')?'; $meta['fixity'] = self::PATH_OPTIONAL; } else { $autoOptional = FALSE; } $metadata[$name] = $meta; } while (TRUE); if ($brackets) { throw new InvalidArgumentException("Missing closing ']' in mask '{$mask}'."); } $this->re = '#' . $re . '/?$#A' . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu'); $this->metadata = $metadata; $this->sequence = $sequence; }
/** * Returns position of token in input string. * @param int token number * @return array [offset, line, column] */ public function getOffset($i) { $tokens = Strings::split($this->input, $this->re, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); $offset = isset($tokens[$i]) ? $tokens[$i][1] : strlen($this->input); return array($offset, $offset ? substr_count($this->input, "\n", 0, $offset) + 1 : 1, $offset - strrpos(substr($this->input, 0, $offset), "\n")); }