annotate vendor/phpdocumentor/reflection-docblock/src/DocBlock/DescriptionFactory.php @ 2:5311817fb629

Theme updates
author Chris Cannam
date Tue, 10 Jul 2018 13:19:18 +0000
parents
children
rev   line source
Chris@2 1 <?php
Chris@2 2 /**
Chris@2 3 * This file is part of phpDocumentor.
Chris@2 4 *
Chris@2 5 * For the full copyright and license information, please view the LICENSE
Chris@2 6 * file that was distributed with this source code.
Chris@2 7 *
Chris@2 8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org>
Chris@2 9 * @license http://www.opensource.org/licenses/mit-license.php MIT
Chris@2 10 * @link http://phpdoc.org
Chris@2 11 */
Chris@2 12
Chris@2 13 namespace phpDocumentor\Reflection\DocBlock;
Chris@2 14
Chris@2 15 use phpDocumentor\Reflection\Types\Context as TypeContext;
Chris@2 16
Chris@2 17 /**
Chris@2 18 * Creates a new Description object given a body of text.
Chris@2 19 *
Chris@2 20 * Descriptions in phpDocumentor are somewhat complex entities as they can contain one or more tags inside their
Chris@2 21 * body that can be replaced with a readable output. The replacing is done by passing a Formatter object to the
Chris@2 22 * Description object's `render` method.
Chris@2 23 *
Chris@2 24 * In addition to the above does a Description support two types of escape sequences:
Chris@2 25 *
Chris@2 26 * 1. `{@}` to escape the `@` character to prevent it from being interpreted as part of a tag, i.e. `{{@}link}`
Chris@2 27 * 2. `{}` to escape the `}` character, this can be used if you want to use the `}` character in the description
Chris@2 28 * of an inline tag.
Chris@2 29 *
Chris@2 30 * If a body consists of multiple lines then this factory will also remove any superfluous whitespace at the beginning
Chris@2 31 * of each line while maintaining any indentation that is used. This will prevent formatting parsers from tripping
Chris@2 32 * over unexpected spaces as can be observed with tag descriptions.
Chris@2 33 */
Chris@2 34 class DescriptionFactory
Chris@2 35 {
Chris@2 36 /** @var TagFactory */
Chris@2 37 private $tagFactory;
Chris@2 38
Chris@2 39 /**
Chris@2 40 * Initializes this factory with the means to construct (inline) tags.
Chris@2 41 *
Chris@2 42 * @param TagFactory $tagFactory
Chris@2 43 */
Chris@2 44 public function __construct(TagFactory $tagFactory)
Chris@2 45 {
Chris@2 46 $this->tagFactory = $tagFactory;
Chris@2 47 }
Chris@2 48
Chris@2 49 /**
Chris@2 50 * Returns the parsed text of this description.
Chris@2 51 *
Chris@2 52 * @param string $contents
Chris@2 53 * @param TypeContext $context
Chris@2 54 *
Chris@2 55 * @return Description
Chris@2 56 */
Chris@2 57 public function create($contents, TypeContext $context = null)
Chris@2 58 {
Chris@2 59 list($text, $tags) = $this->parse($this->lex($contents), $context);
Chris@2 60
Chris@2 61 return new Description($text, $tags);
Chris@2 62 }
Chris@2 63
Chris@2 64 /**
Chris@2 65 * Strips the contents from superfluous whitespace and splits the description into a series of tokens.
Chris@2 66 *
Chris@2 67 * @param string $contents
Chris@2 68 *
Chris@2 69 * @return string[] A series of tokens of which the description text is composed.
Chris@2 70 */
Chris@2 71 private function lex($contents)
Chris@2 72 {
Chris@2 73 $contents = $this->removeSuperfluousStartingWhitespace($contents);
Chris@2 74
Chris@2 75 // performance optimalization; if there is no inline tag, don't bother splitting it up.
Chris@2 76 if (strpos($contents, '{@') === false) {
Chris@2 77 return [$contents];
Chris@2 78 }
Chris@2 79
Chris@2 80 return preg_split(
Chris@2 81 '/\{
Chris@2 82 # "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally.
Chris@2 83 (?!@\})
Chris@2 84 # We want to capture the whole tag line, but without the inline tag delimiters.
Chris@2 85 (\@
Chris@2 86 # Match everything up to the next delimiter.
Chris@2 87 [^{}]*
Chris@2 88 # Nested inline tag content should not be captured, or it will appear in the result separately.
Chris@2 89 (?:
Chris@2 90 # Match nested inline tags.
Chris@2 91 (?:
Chris@2 92 # Because we did not catch the tag delimiters earlier, we must be explicit with them here.
Chris@2 93 # Notice that this also matches "{}", as a way to later introduce it as an escape sequence.
Chris@2 94 \{(?1)?\}
Chris@2 95 |
Chris@2 96 # Make sure we match hanging "{".
Chris@2 97 \{
Chris@2 98 )
Chris@2 99 # Match content after the nested inline tag.
Chris@2 100 [^{}]*
Chris@2 101 )* # If there are more inline tags, match them as well. We use "*" since there may not be any
Chris@2 102 # nested inline tags.
Chris@2 103 )
Chris@2 104 \}/Sux',
Chris@2 105 $contents,
Chris@2 106 null,
Chris@2 107 PREG_SPLIT_DELIM_CAPTURE
Chris@2 108 );
Chris@2 109 }
Chris@2 110
Chris@2 111 /**
Chris@2 112 * Parses the stream of tokens in to a new set of tokens containing Tags.
Chris@2 113 *
Chris@2 114 * @param string[] $tokens
Chris@2 115 * @param TypeContext $context
Chris@2 116 *
Chris@2 117 * @return string[]|Tag[]
Chris@2 118 */
Chris@2 119 private function parse($tokens, TypeContext $context)
Chris@2 120 {
Chris@2 121 $count = count($tokens);
Chris@2 122 $tagCount = 0;
Chris@2 123 $tags = [];
Chris@2 124
Chris@2 125 for ($i = 1; $i < $count; $i += 2) {
Chris@2 126 $tags[] = $this->tagFactory->create($tokens[$i], $context);
Chris@2 127 $tokens[$i] = '%' . ++$tagCount . '$s';
Chris@2 128 }
Chris@2 129
Chris@2 130 //In order to allow "literal" inline tags, the otherwise invalid
Chris@2 131 //sequence "{@}" is changed to "@", and "{}" is changed to "}".
Chris@2 132 //"%" is escaped to "%%" because of vsprintf.
Chris@2 133 //See unit tests for examples.
Chris@2 134 for ($i = 0; $i < $count; $i += 2) {
Chris@2 135 $tokens[$i] = str_replace(['{@}', '{}', '%'], ['@', '}', '%%'], $tokens[$i]);
Chris@2 136 }
Chris@2 137
Chris@2 138 return [implode('', $tokens), $tags];
Chris@2 139 }
Chris@2 140
Chris@2 141 /**
Chris@2 142 * Removes the superfluous from a multi-line description.
Chris@2 143 *
Chris@2 144 * When a description has more than one line then it can happen that the second and subsequent lines have an
Chris@2 145 * additional indentation. This is commonly in use with tags like this:
Chris@2 146 *
Chris@2 147 * {@}since 1.1.0 This is an example
Chris@2 148 * description where we have an
Chris@2 149 * indentation in the second and
Chris@2 150 * subsequent lines.
Chris@2 151 *
Chris@2 152 * If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent
Chris@2 153 * lines and this may cause rendering issues when, for example, using a Markdown converter.
Chris@2 154 *
Chris@2 155 * @param string $contents
Chris@2 156 *
Chris@2 157 * @return string
Chris@2 158 */
Chris@2 159 private function removeSuperfluousStartingWhitespace($contents)
Chris@2 160 {
Chris@2 161 $lines = explode("\n", $contents);
Chris@2 162
Chris@2 163 // if there is only one line then we don't have lines with superfluous whitespace and
Chris@2 164 // can use the contents as-is
Chris@2 165 if (count($lines) <= 1) {
Chris@2 166 return $contents;
Chris@2 167 }
Chris@2 168
Chris@2 169 // determine how many whitespace characters need to be stripped
Chris@2 170 $startingSpaceCount = 9999999;
Chris@2 171 for ($i = 1; $i < count($lines); $i++) {
Chris@2 172 // lines with a no length do not count as they are not indented at all
Chris@2 173 if (strlen(trim($lines[$i])) === 0) {
Chris@2 174 continue;
Chris@2 175 }
Chris@2 176
Chris@2 177 // determine the number of prefixing spaces by checking the difference in line length before and after
Chris@2 178 // an ltrim
Chris@2 179 $startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i])));
Chris@2 180 }
Chris@2 181
Chris@2 182 // strip the number of spaces from each line
Chris@2 183 if ($startingSpaceCount > 0) {
Chris@2 184 for ($i = 1; $i < count($lines); $i++) {
Chris@2 185 $lines[$i] = substr($lines[$i], $startingSpaceCount);
Chris@2 186 }
Chris@2 187 }
Chris@2 188
Chris@2 189 return implode("\n", $lines);
Chris@2 190 }
Chris@2 191 }