/** * Static factory. * @param string element name (or NULL) * @param array|string element's attributes (or textual content) * @return Html */ public static function el($name = NULL, $attrs = NULL) { $el = new self(); $parts = explode(' ', $name, 2); $el->setName($parts[0]); if (is_array($attrs)) { $el->attrs = $attrs; } elseif ($attrs !== NULL) { $el->setText($attrs); } if (isset($parts[1])) { foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\\2|\\s))?#i') as $m) { $el->attrs[$m[1]] = isset($m[3]) ? $m[3] : TRUE; } } return $el; }
/** * Tokenize string. * @param string * @return array */ public function tokenize($input) { $this->input = $input; if ($this->types) { $this->tokens = Strings::matchAll($input, $this->re); $len = 0; $count = count($this->types); $line = 1; foreach ($this->tokens as &$match) { $type = NULL; for ($i = 1; $i <= $count; $i++) { if (!isset($match[$i])) { break; } elseif ($match[$i] != NULL) { $type = $this->types[$i - 1]; break; } } $match = self::createToken($match[0], $type, $line); $len += strlen($match['value']); $line += substr_count($match['value'], "\n"); } if ($len !== strlen($input)) { $errorOffset = $len; } } else { $this->tokens = Strings::split($input, $this->re, PREG_SPLIT_NO_EMPTY); if ($this->tokens && !Strings::match(end($this->tokens), $this->re)) { $tmp = Strings::split($this->input, $this->re, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); list(, $errorOffset) = end($tmp); } } if (isset($errorOffset)) { $line = $errorOffset ? substr_count($this->input, "\n", 0, $errorOffset) + 1 : 1; $col = $errorOffset - strrpos(substr($this->input, 0, $errorOffset), "\n") + 1; $token = str_replace("\n", '\\n', substr($input, $errorOffset, 10)); throw new TokenizerException("Unexpected '{$token}' on line {$line}, column {$col}."); } return $this->tokens; }
/** * 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; }
/** * Parses phpDoc comment. * @param string * @return array */ private static function parseComment($comment) { static $tokens = array('true' => TRUE, 'false' => FALSE, 'null' => NULL, '' => TRUE); $res = array(); $comment = preg_replace('#^\\s*\\*\\s?#ms', '', trim($comment, '/*')); $parts = preg_split('#^\\s*(?=@' . self::RE_IDENTIFIER . ')#m', $comment, 2); $description = trim($parts[0]); if ($description !== '') { $res['description'] = array($description); } $matches = Strings::matchAll(isset($parts[1]) ? $parts[1] : '', '~ (?<=\\s|^)@(' . self::RE_IDENTIFIER . ')[ \\t]* ## annotation ( \\((?>' . self::RE_STRING . '|[^\'")@]+)+\\)| ## (value) [^(@\\r\\n][^@\\r\\n]*|) ## value ~xi'); foreach ($matches as $match) { list(, $name, $value) = $match; if (substr($value, 0, 1) === '(') { $items = array(); $key = ''; $val = TRUE; $value[0] = ','; while ($m = Strings::match($value, '#\\s*,\\s*(?>(' . self::RE_IDENTIFIER . ')\\s*=\\s*)?(' . self::RE_STRING . '|[^\'"),\\s][^\'"),]*)#A')) { $value = substr($value, strlen($m[0])); list(, $key, $val) = $m; if ($val[0] === "'" || $val[0] === '"') { $val = substr($val, 1, -1); } elseif (is_numeric($val)) { $val = 1 * $val; } else { $lval = strtolower($val); $val = array_key_exists($lval, $tokens) ? $tokens[$lval] : $val; } if ($key === '') { $items[] = $val; } else { $items[$key] = $val; } } $value = count($items) < 2 && $key === '' ? $val : $items; } else { $value = trim($value); if (is_numeric($value)) { $value = 1 * $value; } else { $lval = strtolower($value); $value = array_key_exists($lval, $tokens) ? $tokens[$lval] : $value; } } $class = $name . 'Annotation'; if (class_exists($class)) { $res[$name][] = new $class(is_array($value) ? $value : array('value' => $value)); } else { $res[$name][] = is_array($value) ? new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS) : $value; } } return $res; }
/** * Builds HTML content. * @return void */ protected function buildHtml() { if ($this->html instanceof ITemplate) { $this->html->mail = $this; if ($this->basePath === NULL && $this->html instanceof IFileTemplate) { $this->basePath = dirname($this->html->getFile()); } $this->html = $this->html->__toString(TRUE); } if ($this->basePath !== FALSE) { $cids = array(); $matches = Strings::matchAll($this->html, '#(src\\s*=\\s*|background\\s*=\\s*|url\\()(["\'])(?![a-z]+:|[/\\#])(.+?)\\2#i', PREG_OFFSET_CAPTURE); foreach (array_reverse($matches) as $m) { $file = rtrim($this->basePath, '/\\') . '/' . $m[3][0]; if (!isset($cids[$file])) { $cids[$file] = substr($this->addEmbeddedFile($file)->getHeader("Content-ID"), 1, -1); } $this->html = substr_replace($this->html, "{$m[1][0]}{$m[2][0]}cid:{$cids[$file]}{$m[2][0]}", $m[0][1], strlen($m[0][0])); } } if (!$this->getSubject() && ($matches = Strings::match($this->html, '#<title>(.+?)</title>#is'))) { $this->setSubject(html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8')); } }