Write tests for DescriptionFactory and adjust components for minor bugs that were found

This commit is contained in:
Mike van Riel
2015-06-26 12:16:45 +02:00
parent 09230bda94
commit 19c75b3950
24 changed files with 301 additions and 97 deletions
+77 -3
View File
@@ -14,6 +14,23 @@ namespace phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Types\Context;
/**
* Creates a new Description object given a body of text.
*
* Descriptions in phpDocumentor are somewhat complex entities as they can contain one or more tags inside their
* body that can be replaced with a readable output. The replacing is done by passing a Formatter object to the
* Description object's `render` method.
*
* In addition to the above does a Description support two types of escape sequences:
*
* 1. `{@}` to escape the `@` character to prevent it from being interpreted as part of a tag, i.e. `{{@}link}`
* 2. `{}` to escape the `}` character, this can be used if you want to use the `}` character in the description
* of an inline tag.
*
* If a body consists of multiple lines then this factory will also remove any superfluous whitespace at the beginning
* of each line while maintaining any indentation that is used. This will prevent formatting parsers from tripping
* over unexpected spaces as can be observed with tag descriptions.
*/
class DescriptionFactory
{
/** @var TagFactory */
@@ -45,11 +62,16 @@ class DescriptionFactory
}
/**
* @param $contents
* @return array
* Strips the contents from superfluous whitespace and splits the description into a series of tokens.
*
* @param string $contents
*
* @return string[] A series of tokens of which the description text is composed.
*/
private function lex($contents)
{
$contents = $this->removeSuperfluousStartingWhitespace($contents);
// performance optimalization; if there is no inline tag, don't bother splitting it up.
if (strpos($contents, '{@') === false) {
return [$contents];
@@ -98,7 +120,8 @@ class DescriptionFactory
{
$count = count($tokens);
$tagCount = 0;
$tags = [];
$tags = [];
for ($i = 1; $i < $count; $i += 2) {
$tags[] = $this->tagFactory->create($tokens[$i], $context);
$tokens[$i] = '%' . ++$tagCount . '$s';
@@ -114,4 +137,55 @@ class DescriptionFactory
return [implode('', $tokens), $tags];
}
/**
* Removes the superfluous from a multi-line description.
*
* When a description has more than one line then it can happen that the second and subsequent lines have an
* additional indentation. This is commonly in use with tags like this:
*
* {@}since 1.1.0 This is an example
* description where we have an
* indentation in the second and
* subsequent lines.
*
* If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent
* lines and this may cause rendering issues when, for example, using a Markdown converter.
*
* @param string $contents
*
* @return string
*/
private function removeSuperfluousStartingWhitespace($contents)
{
$lines = explode("\n", $contents);
// if there is only one line then we don't have lines with superfluous whitespace and
// can use the contents as-is
if (count($lines) <= 1) {
return $contents;
}
// determine how many whitespace characters need to be stripped
$startingSpaceCount = 9999999;
for ($i = 1; $i < count($lines); $i++) {
// lines with a no length do not count as they are not indented at all
if (strlen(trim($lines[$i])) === 0) {
continue;
}
// determine the number of prefixing spaces by checking the difference in line length before and after
// an ltrim
$startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i])));
}
// strip the number of spaces from each line
if ($startingSpaceCount > 0) {
for ($i = 1; $i < count($lines); $i++) {
$lines[$i] = substr($lines[$i], $startingSpaceCount);
}
}
return implode("\n", $lines);
}
}
+2 -1
View File
@@ -127,7 +127,8 @@ class Serializer
private function addTagBlock(DocBlock $docblock, $wrapLength, $indent, $comment)
{
foreach ($docblock->getTags() as $tag) {
$tagText = (string)$tag;
$formatter = new DocBlock\Tags\Formatter\PassthroughFormatter();
$tagText = $formatter->format($tag);
if ($wrapLength !== null) {
$tagText = wordwrap($tagText, $wrapLength);
}
+37 -47
View File
@@ -1,11 +1,11 @@
<?php
/**
* phpDocumentor
* This file is part of phpDocumentor.
*
* PHP Version 5.3
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Vasil Rangelov <[email protected]>
* @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
* @copyright 2010-2015 Mike van Riel<[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
@@ -15,25 +15,20 @@ namespace phpDocumentor\Reflection\DocBlock\Tags;
use phpDocumentor\Reflection\DocBlock\Tag;
/**
* Reflection class for a @example tag in a Docblock.
*
* @author Vasil Rangelov <[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
* Reflection class for a {@}example tag in a Docblock.
*/
class Example extends Source
final class Example extends BaseTag
{
/**
* @var string Path to a file to use as an example.
* May also be an absolute URI.
* @var string Path to a file to use as an example. May also be an absolute URI.
*/
protected $filePath = '';
private $filePath = '';
/**
* @var bool Whether the file path component represents an URI.
* This determines how the file portion appears at {@link getContent()}.
* @var bool Whether the file path component represents an URI. This determines how the file portion
* appears at {@link getContent()}.
*/
protected $isURI = false;
private $isURI = false;
/**
* {@inheritdoc}
@@ -57,40 +52,35 @@ class Example extends Source
/**
* {@inheritdoc}
*/
public function setContent($content)
public static function create($body)
{
Tag::setContent($content);
if (preg_match(
'/^
# File component
(?:
# File path in quotes
\"([^\"]+)\"
|
# File URI
(\S+)
)
# Remaining content (parsed by SourceTag)
(?:\s+(.*))?
$/sux',
$this->description,
$matches
)) {
if ('' !== $matches[1]) {
$this->setFilePath($matches[1]);
} else {
$this->setFileURI($matches[2]);
}
if (isset($matches[3])) {
parent::setContent($matches[3]);
} else {
$this->setDescription('');
}
$this->description = $content;
// File component: File path in quotes or File URI / Source information
if (! preg_match('/^(?:\"([^\"]+)\"|(\S+))(?:\s+(.*))?$/sux', $body, $matches)) {
return null;
}
return $this;
$filePath = null;
$fileUri = null;
if ('' !== $matches[1]) {
$filePath = $matches[1];
} else {
$fileUri = $matches[2];
}
$startingLine = 1;
$lineCount = null;
$description = null;
// Starting line / Number of lines / Description
if (preg_match('/^([1-9]\d*)\s*(?:((?1))\s+)?(.*)$/sux', $matches[3], $matches)) {
$startingLine = (int)$matches[1];
if (isset($matches[2]) && $matches[2] !== '') {
$lineCount = (int)$matches[2];
}
$description = $matches[3];
}
return new static($filePath, $fileUri, $startingLine, $lineCount, $description);
}
/**
@@ -26,6 +26,6 @@ class PassthroughFormatter implements Formatter
*/
public function format(Tag $tag)
{
return (string)$tag;
return '@' . $tag->getName() . ' ' . (string)$tag;
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ class Generic extends BaseTag
*/
public function __toString()
{
return '@' . $this->getName() . ($this->description ? ' ' . $this->description->render() : '');
return ($this->description ? $this->description->render() : '');
}
/**