Chris@13: Chris@13: * @author Justin Hileman Chris@13: */ Chris@13: class Docblock Chris@13: { Chris@13: /** Chris@13: * Tags in the docblock that have a whitespace-delimited number of parameters Chris@13: * (such as `@param type var desc` and `@return type desc`) and the names of Chris@13: * those parameters. Chris@13: * Chris@13: * @var array Chris@13: */ Chris@13: public static $vectors = [ Chris@13: 'throws' => ['type', 'desc'], Chris@13: 'param' => ['type', 'var', 'desc'], Chris@13: 'return' => ['type', 'desc'], Chris@13: ]; Chris@13: Chris@13: protected $reflector; Chris@13: Chris@13: /** Chris@13: * The description of the symbol. Chris@13: * Chris@13: * @var string Chris@13: */ Chris@13: public $desc; Chris@13: Chris@13: /** Chris@13: * The tags defined in the docblock. Chris@13: * Chris@13: * The array has keys which are the tag names (excluding the @) and values Chris@13: * that are arrays, each of which is an entry for the tag. Chris@13: * Chris@13: * In the case where the tag name is defined in {@see DocBlock::$vectors} the Chris@13: * value within the tag-value array is an array in itself with keys as Chris@13: * described by {@see DocBlock::$vectors}. Chris@13: * Chris@13: * @var array Chris@13: */ Chris@13: public $tags; Chris@13: Chris@13: /** Chris@13: * The entire DocBlock comment that was parsed. Chris@13: * Chris@13: * @var string Chris@13: */ Chris@13: public $comment; Chris@13: Chris@13: /** Chris@13: * Docblock constructor. Chris@13: * Chris@13: * @param \Reflector $reflector Chris@13: */ Chris@13: public function __construct(\Reflector $reflector) Chris@13: { Chris@13: $this->reflector = $reflector; Chris@13: $this->setComment($reflector->getDocComment()); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set and parse the docblock comment. Chris@13: * Chris@13: * @param string $comment The docblock Chris@13: */ Chris@13: protected function setComment($comment) Chris@13: { Chris@13: $this->desc = ''; Chris@13: $this->tags = []; Chris@13: $this->comment = $comment; Chris@13: Chris@13: $this->parseComment($comment); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Find the length of the docblock prefix. Chris@13: * Chris@13: * @param array $lines Chris@13: * Chris@13: * @return int Prefix length Chris@13: */ Chris@13: protected static function prefixLength(array $lines) Chris@13: { Chris@13: // find only lines with interesting things Chris@17: $lines = \array_filter($lines, function ($line) { Chris@17: return \substr($line, \strspn($line, "* \t\n\r\0\x0B")); Chris@13: }); Chris@13: Chris@13: // if we sort the lines, we only have to compare two items Chris@17: \sort($lines); Chris@13: Chris@17: $first = \reset($lines); Chris@17: $last = \end($lines); Chris@13: Chris@13: // find the longest common substring Chris@17: $count = \min(\strlen($first), \strlen($last)); Chris@13: for ($i = 0; $i < $count; $i++) { Chris@13: if ($first[$i] !== $last[$i]) { Chris@13: return $i; Chris@13: } Chris@13: } Chris@13: Chris@13: return $count; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Parse the comment into the component parts and set the state of the object. Chris@13: * Chris@13: * @param string $comment The docblock Chris@13: */ Chris@13: protected function parseComment($comment) Chris@13: { Chris@13: // Strip the opening and closing tags of the docblock Chris@17: $comment = \substr($comment, 3, -2); Chris@13: Chris@13: // Split into arrays of lines Chris@17: $comment = \array_filter(\preg_split('/\r?\n\r?/', $comment)); Chris@13: Chris@13: // Trim asterisks and whitespace from the beginning and whitespace from the end of lines Chris@13: $prefixLength = self::prefixLength($comment); Chris@17: $comment = \array_map(function ($line) use ($prefixLength) { Chris@17: return \rtrim(\substr($line, $prefixLength)); Chris@13: }, $comment); Chris@13: Chris@13: // Group the lines together by @tags Chris@13: $blocks = []; Chris@13: $b = -1; Chris@13: foreach ($comment as $line) { Chris@13: if (self::isTagged($line)) { Chris@13: $b++; Chris@13: $blocks[] = []; Chris@13: } elseif ($b === -1) { Chris@13: $b = 0; Chris@13: $blocks[] = []; Chris@13: } Chris@13: $blocks[$b][] = $line; Chris@13: } Chris@13: Chris@13: // Parse the blocks Chris@13: foreach ($blocks as $block => $body) { Chris@17: $body = \trim(\implode("\n", $body)); Chris@13: Chris@13: if ($block === 0 && !self::isTagged($body)) { Chris@13: // This is the description block Chris@13: $this->desc = $body; Chris@13: } else { Chris@13: // This block is tagged Chris@17: $tag = \substr(self::strTag($body), 1); Chris@17: $body = \ltrim(\substr($body, \strlen($tag) + 2)); Chris@13: Chris@13: if (isset(self::$vectors[$tag])) { Chris@13: // The tagged block is a vector Chris@17: $count = \count(self::$vectors[$tag]); Chris@13: if ($body) { Chris@17: $parts = \preg_split('/\s+/', $body, $count); Chris@13: } else { Chris@13: $parts = []; Chris@13: } Chris@13: Chris@13: // Default the trailing values Chris@17: $parts = \array_pad($parts, $count, null); Chris@13: Chris@13: // Store as a mapped array Chris@17: $this->tags[$tag][] = \array_combine(self::$vectors[$tag], $parts); Chris@13: } else { Chris@13: // The tagged block is only text Chris@13: $this->tags[$tag][] = $body; Chris@13: } Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Whether or not a docblock contains a given @tag. Chris@13: * Chris@13: * @param string $tag The name of the @tag to check for Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function hasTag($tag) Chris@13: { Chris@17: return \is_array($this->tags) && \array_key_exists($tag, $this->tags); Chris@13: } Chris@13: Chris@13: /** Chris@13: * The value of a tag. Chris@13: * Chris@13: * @param string $tag Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: public function tag($tag) Chris@13: { Chris@13: return $this->hasTag($tag) ? $this->tags[$tag] : null; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Whether or not a string begins with a @tag. Chris@13: * Chris@13: * @param string $str Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public static function isTagged($str) Chris@13: { Chris@17: return isset($str[1]) && $str[0] === '@' && !\preg_match('/[^A-Za-z]/', $str[1]); Chris@13: } Chris@13: Chris@13: /** Chris@13: * The tag at the beginning of a string. Chris@13: * Chris@13: * @param string $str Chris@13: * Chris@13: * @return string|null Chris@13: */ Chris@13: public static function strTag($str) Chris@13: { Chris@17: if (\preg_match('/^@[a-z0-9_]+/', $str, $matches)) { Chris@13: return $matches[0]; Chris@13: } Chris@13: } Chris@13: }