Skip to content

Commit 19c75b3

Browse files
committed
Write tests for DescriptionFactory and adjust components for minor bugs that were found
1 parent 09230bd commit 19c75b3

24 files changed

+299
-95
lines changed

src/DocBlock/DescriptionFactory.php

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@
1414

1515
use phpDocumentor\Reflection\Types\Context;
1616

17+
/**
18+
* Creates a new Description object given a body of text.
19+
*
20+
* Descriptions in phpDocumentor are somewhat complex entities as they can contain one or more tags inside their
21+
* body that can be replaced with a readable output. The replacing is done by passing a Formatter object to the
22+
* Description object's `render` method.
23+
*
24+
* In addition to the above does a Description support two types of escape sequences:
25+
*
26+
* 1. `{@}` to escape the `@` character to prevent it from being interpreted as part of a tag, i.e. `{{@}link}`
27+
* 2. `{}` to escape the `}` character, this can be used if you want to use the `}` character in the description
28+
* of an inline tag.
29+
*
30+
* If a body consists of multiple lines then this factory will also remove any superfluous whitespace at the beginning
31+
* of each line while maintaining any indentation that is used. This will prevent formatting parsers from tripping
32+
* over unexpected spaces as can be observed with tag descriptions.
33+
*/
1734
class DescriptionFactory
1835
{
1936
/** @var TagFactory */
@@ -45,11 +62,16 @@ public function create($contents, Context $context = null)
4562
}
4663

4764
/**
48-
* @param $contents
49-
* @return array
65+
* Strips the contents from superfluous whitespace and splits the description into a series of tokens.
66+
*
67+
* @param string $contents
68+
*
69+
* @return string[] A series of tokens of which the description text is composed.
5070
*/
5171
private function lex($contents)
5272
{
73+
$contents = $this->removeSuperfluousStartingWhitespace($contents);
74+
5375
// performance optimalization; if there is no inline tag, don't bother splitting it up.
5476
if (strpos($contents, '{@') === false) {
5577
return [$contents];
@@ -98,7 +120,8 @@ private function parse($tokens, Context $context)
98120
{
99121
$count = count($tokens);
100122
$tagCount = 0;
101-
$tags = [];
123+
$tags = [];
124+
102125
for ($i = 1; $i < $count; $i += 2) {
103126
$tags[] = $this->tagFactory->create($tokens[$i], $context);
104127
$tokens[$i] = '%' . ++$tagCount . '$s';
@@ -114,4 +137,55 @@ private function parse($tokens, Context $context)
114137
return [implode('', $tokens), $tags];
115138
}
116139

140+
/**
141+
* Removes the superfluous from a multi-line description.
142+
*
143+
* When a description has more than one line then it can happen that the second and subsequent lines have an
144+
* additional indentation. This is commonly in use with tags like this:
145+
*
146+
* {@}since 1.1.0 This is an example
147+
* description where we have an
148+
* indentation in the second and
149+
* subsequent lines.
150+
*
151+
* If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent
152+
* lines and this may cause rendering issues when, for example, using a Markdown converter.
153+
*
154+
* @param string $contents
155+
*
156+
* @return string
157+
*/
158+
private function removeSuperfluousStartingWhitespace($contents)
159+
{
160+
$lines = explode("\n", $contents);
161+
162+
// if there is only one line then we don't have lines with superfluous whitespace and
163+
// can use the contents as-is
164+
if (count($lines) <= 1) {
165+
return $contents;
166+
}
167+
168+
// determine how many whitespace characters need to be stripped
169+
$startingSpaceCount = 9999999;
170+
for ($i = 1; $i < count($lines); $i++) {
171+
// lines with a no length do not count as they are not indented at all
172+
if (strlen(trim($lines[$i])) === 0) {
173+
continue;
174+
}
175+
176+
// determine the number of prefixing spaces by checking the difference in line length before and after
177+
// an ltrim
178+
$startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i])));
179+
}
180+
181+
// strip the number of spaces from each line
182+
if ($startingSpaceCount > 0) {
183+
for ($i = 1; $i < count($lines); $i++) {
184+
$lines[$i] = substr($lines[$i], $startingSpaceCount);
185+
}
186+
}
187+
188+
return implode("\n", $lines);
189+
}
190+
117191
}

src/DocBlock/Serializer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ private function getSummaryAndDescriptionTextBlock(DocBlock $docblock, $wrapLeng
127127
private function addTagBlock(DocBlock $docblock, $wrapLength, $indent, $comment)
128128
{
129129
foreach ($docblock->getTags() as $tag) {
130-
$tagText = (string)$tag;
130+
$formatter = new DocBlock\Tags\Formatter\PassthroughFormatter();
131+
$tagText = $formatter->format($tag);
131132
if ($wrapLength !== null) {
132133
$tagText = wordwrap($tagText, $wrapLength);
133134
}

src/DocBlock/Tags/Example.php

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?php
22
/**
3-
* phpDocumentor
3+
* This file is part of phpDocumentor.
44
*
5-
* PHP Version 5.3
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
67
*
7-
* @author Vasil Rangelov <[email protected]>
8-
* @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
8+
* @copyright 2010-2015 Mike van Riel<[email protected]>
99
* @license http://www.opensource.org/licenses/mit-license.php MIT
1010
* @link http://phpdoc.org
1111
*/
@@ -15,25 +15,20 @@
1515
use phpDocumentor\Reflection\DocBlock\Tag;
1616

1717
/**
18-
* Reflection class for a @example tag in a Docblock.
19-
*
20-
* @author Vasil Rangelov <[email protected]>
21-
* @license http://www.opensource.org/licenses/mit-license.php MIT
22-
* @link http://phpdoc.org
18+
* Reflection class for a {@}example tag in a Docblock.
2319
*/
24-
class Example extends Source
20+
final class Example extends BaseTag
2521
{
2622
/**
27-
* @var string Path to a file to use as an example.
28-
* May also be an absolute URI.
23+
* @var string Path to a file to use as an example. May also be an absolute URI.
2924
*/
30-
protected $filePath = '';
25+
private $filePath = '';
3126

3227
/**
33-
* @var bool Whether the file path component represents an URI.
34-
* This determines how the file portion appears at {@link getContent()}.
28+
* @var bool Whether the file path component represents an URI. This determines how the file portion
29+
* appears at {@link getContent()}.
3530
*/
36-
protected $isURI = false;
31+
private $isURI = false;
3732

3833
/**
3934
* {@inheritdoc}
@@ -57,40 +52,35 @@ public function getContent()
5752
/**
5853
* {@inheritdoc}
5954
*/
60-
public function setContent($content)
55+
public static function create($body)
6156
{
62-
Tag::setContent($content);
63-
if (preg_match(
64-
'/^
65-
# File component
66-
(?:
67-
# File path in quotes
68-
\"([^\"]+)\"
69-
|
70-
# File URI
71-
(\S+)
72-
)
73-
# Remaining content (parsed by SourceTag)
74-
(?:\s+(.*))?
75-
$/sux',
76-
$this->description,
77-
$matches
78-
)) {
79-
if ('' !== $matches[1]) {
80-
$this->setFilePath($matches[1]);
81-
} else {
82-
$this->setFileURI($matches[2]);
83-
}
57+
// File component: File path in quotes or File URI / Source information
58+
if (! preg_match('/^(?:\"([^\"]+)\"|(\S+))(?:\s+(.*))?$/sux', $body, $matches)) {
59+
return null;
60+
}
8461

85-
if (isset($matches[3])) {
86-
parent::setContent($matches[3]);
87-
} else {
88-
$this->setDescription('');
62+
$filePath = null;
63+
$fileUri = null;
64+
if ('' !== $matches[1]) {
65+
$filePath = $matches[1];
66+
} else {
67+
$fileUri = $matches[2];
68+
}
69+
70+
$startingLine = 1;
71+
$lineCount = null;
72+
$description = null;
73+
74+
// Starting line / Number of lines / Description
75+
if (preg_match('/^([1-9]\d*)\s*(?:((?1))\s+)?(.*)$/sux', $matches[3], $matches)) {
76+
$startingLine = (int)$matches[1];
77+
if (isset($matches[2]) && $matches[2] !== '') {
78+
$lineCount = (int)$matches[2];
8979
}
90-
$this->description = $content;
80+
$description = $matches[3];
9181
}
9282

93-
return $this;
83+
return new static($filePath, $fileUri, $startingLine, $lineCount, $description);
9484
}
9585

9686
/**

src/DocBlock/Tags/Formatter/PassthroughFormatter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ class PassthroughFormatter implements Formatter
2626
*/
2727
public function format(Tag $tag)
2828
{
29-
return (string)$tag;
29+
return '@' . $tag->getName() . ' ' . (string)$tag;
3030
}
3131
}

src/DocBlock/Tags/Generic.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static function create(
6868
*/
6969
public function __toString()
7070
{
71-
return '@' . $this->getName() . ($this->description ? ' ' . $this->description->render() : '');
71+
return ($this->description ? $this->description->render() : '');
7272
}
7373

7474
/**

0 commit comments

Comments
 (0)