| Total Complexity | 48 |
| Total Lines | 311 |
| Duplicated Lines | 0 % |
| Coverage | 98.47% |
| Changes | 3 | ||
| Bugs | 0 | Features | 0 |
Complex classes like MarkdownParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MarkdownParser, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 38 | final class MarkdownParser implements MarkdownParserInterface |
||
| 39 | { |
||
| 40 | /** |
||
| 41 | * @var EnvironmentInterface |
||
| 42 | * |
||
| 43 | * @psalm-readonly |
||
| 44 | */ |
||
| 45 | private $environment; |
||
| 46 | |||
| 47 | /** |
||
| 48 | * @var int |
||
| 49 | * |
||
| 50 | * @psalm-readonly-allow-private-mutation |
||
| 51 | */ |
||
| 52 | private $maxNestingLevel; |
||
| 53 | |||
| 54 | /** |
||
| 55 | * @var ReferenceMap |
||
| 56 | * |
||
| 57 | * @psalm-readonly-allow-private-mutation |
||
| 58 | */ |
||
| 59 | private $referenceMap; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * @var int |
||
| 63 | * |
||
| 64 | * @psalm-readonly-allow-private-mutation |
||
| 65 | */ |
||
| 66 | private $lineNumber = 0; |
||
| 67 | |||
| 68 | /** |
||
| 69 | * @var Cursor |
||
| 70 | * |
||
| 71 | * @psalm-readonly-allow-private-mutation |
||
| 72 | */ |
||
| 73 | private $cursor; |
||
| 74 | |||
| 75 | /** |
||
| 76 | * @var array<int, BlockContinueParserInterface> |
||
| 77 | * |
||
| 78 | * @psalm-readonly-allow-private-mutation |
||
| 79 | */ |
||
| 80 | private $activeBlockParsers = []; |
||
| 81 | |||
| 82 | /** |
||
| 83 | * @var array<int, BlockContinueParserInterface> |
||
| 84 | * |
||
| 85 | * @psalm-readonly-allow-private-mutation |
||
| 86 | */ |
||
| 87 | private $closedBlockParsers = []; |
||
| 88 | |||
| 89 | 2982 | public function __construct(EnvironmentInterface $environment) |
|
| 90 | { |
||
| 91 | 2982 | $this->environment = $environment; |
|
| 92 | 2982 | } |
|
| 93 | |||
| 94 | 2964 | private function initialize(): void |
|
| 95 | { |
||
| 96 | 2964 | $this->referenceMap = new ReferenceMap(); |
|
| 97 | 2964 | $this->lineNumber = 0; |
|
| 98 | 2964 | $this->activeBlockParsers = []; |
|
| 99 | 2964 | $this->closedBlockParsers = []; |
|
| 100 | |||
| 101 | 2964 | $this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level'); |
|
| 102 | 2952 | } |
|
| 103 | |||
| 104 | /** |
||
| 105 | * @throws \RuntimeException |
||
| 106 | */ |
||
| 107 | 2964 | public function parse(string $input): Document |
|
| 108 | { |
||
| 109 | 2964 | $this->initialize(); |
|
| 110 | |||
| 111 | 2952 | $documentParser = new DocumentBlockParser($this->referenceMap); |
|
| 112 | 2952 | $this->activateBlockParser($documentParser); |
|
| 113 | |||
| 114 | 2952 | $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input)); |
|
| 115 | 2943 | $this->environment->dispatch($preParsedEvent); |
|
| 116 | 2940 | $markdownInput = $preParsedEvent->getMarkdown(); |
|
| 117 | |||
| 118 | 2940 | foreach ($markdownInput->getLines() as $lineNumber => $line) { |
|
| 119 | 2940 | $this->lineNumber = $lineNumber; |
|
| 120 | 2940 | $this->incorporateLine($line); |
|
| 121 | } |
||
| 122 | |||
| 123 | // finalizeAndProcess |
||
| 124 | 2940 | $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber); |
|
| 125 | 2940 | $this->processInlines(); |
|
| 126 | |||
| 127 | 2940 | $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock())); |
|
| 128 | |||
| 129 | 2940 | return $documentParser->getBlock(); |
|
| 130 | } |
||
| 131 | |||
| 132 | /** |
||
| 133 | * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each |
||
| 134 | * line of input, then finalizing the document. |
||
| 135 | */ |
||
| 136 | 2940 | private function incorporateLine(string $line): void |
|
| 137 | { |
||
| 138 | 2940 | $this->cursor = new Cursor($line); |
|
| 139 | |||
| 140 | 2940 | $matches = 1; |
|
| 141 | 2940 | for ($i = 1; $i < \count($this->activeBlockParsers); $i++) { |
|
|
1 ignored issue
–
show
|
|||
| 142 | 1437 | $blockParser = $this->activeBlockParsers[$i]; |
|
| 143 | 1437 | $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser()); |
|
| 144 | 1437 | if ($blockContinue === null) { |
|
| 145 | 945 | break; |
|
| 146 | } |
||
| 147 | |||
| 148 | 963 | if ($blockContinue->isFinalize()) { |
|
| 149 | 90 | $this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber); |
|
| 150 | |||
| 151 | 90 | return; |
|
| 152 | } |
||
| 153 | |||
| 154 | 957 | if (($state = $blockContinue->getCursorState()) !== null) { |
|
| 155 | 957 | $this->cursor->restoreState($state); |
|
| 156 | } |
||
| 157 | |||
| 158 | 957 | $matches++; |
|
| 159 | } |
||
| 160 | |||
| 161 | 2940 | $unmatchedBlocks = \count($this->activeBlockParsers) - $matches; |
|
| 162 | 2940 | $blockParser = $this->activeBlockParsers[$matches - 1]; |
|
| 163 | 2940 | $startedNewBlock = false; |
|
| 164 | |||
| 165 | // Unless last matched container is a code block, try new container starts |
||
| 166 | 2940 | $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer(); |
|
| 167 | 2940 | while ($tryBlockStarts) { |
|
| 168 | // this is a little performance optimization |
||
| 169 | 2940 | if ($this->cursor->isBlank()) { |
|
| 170 | 738 | $this->cursor->advanceToEnd(); |
|
| 171 | 738 | break; |
|
| 172 | } |
||
| 173 | |||
| 174 | 2940 | if (! $this->cursor->isIndented() && RegexHelper::isLetter($this->cursor->getNextNonSpaceCharacter())) { |
|
| 175 | 1395 | $this->cursor->advanceToNextNonSpaceOrTab(); |
|
| 176 | 1395 | break; |
|
| 177 | } |
||
| 178 | |||
| 179 | 2298 | if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) { |
|
| 180 | 3 | break; |
|
| 181 | } |
||
| 182 | |||
| 183 | 2298 | $blockStart = $this->findBlockStart($blockParser); |
|
| 184 | 2298 | if ($blockStart === null) { |
|
| 185 | 1377 | $this->cursor->advanceToNextNonSpaceOrTab(); |
|
| 186 | 1377 | break; |
|
| 187 | } |
||
| 188 | |||
| 189 | 1080 | if (($state = $blockStart->getCursorState()) !== null) { |
|
| 190 | 1080 | $this->cursor->restoreState($state); |
|
| 191 | } |
||
| 192 | |||
| 193 | 1080 | $startedNewBlock = true; |
|
| 194 | |||
| 195 | // We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now. |
||
| 196 | 1080 | if ($unmatchedBlocks > 0) { |
|
| 197 | 258 | $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1); |
|
| 198 | 258 | $unmatchedBlocks = 0; |
|
| 199 | } |
||
| 200 | |||
| 201 | 1080 | if ($blockStart->isReplaceActiveBlockParser()) { |
|
| 202 | 150 | $this->prepareActiveBlockParserForReplacement(); |
|
| 203 | } |
||
| 204 | |||
| 205 | 1080 | foreach ($blockStart->getBlockParsers() as $newBlockParser) { |
|
| 206 | 1080 | $blockParser = $this->addChild($newBlockParser); |
|
| 207 | 1080 | $tryBlockStarts = $newBlockParser->isContainer(); |
|
| 208 | } |
||
| 209 | } |
||
| 210 | |||
| 211 | // What remains ath the offset is a text line. Add the text to the appropriate block. |
||
| 212 | |||
| 213 | // First check for a lazy paragraph continuation: |
||
| 214 | 2940 | if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) { |
|
| 215 | 447 | $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); |
|
| 216 | } else { |
||
| 217 | // finalize any blocks not matched |
||
| 218 | 2940 | if ($unmatchedBlocks > 0) { |
|
| 219 | 777 | $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber); |
|
| 220 | } |
||
| 221 | |||
| 222 | 2940 | if (! $blockParser->isContainer()) { |
|
| 223 | 780 | $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); |
|
| 224 | 2613 | } elseif (! $this->cursor->isBlank()) { |
|
| 225 | 2562 | $this->addChild(new ParagraphParser()); |
|
| 226 | 2562 | $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); |
|
| 227 | } |
||
| 228 | } |
||
| 229 | 2940 | } |
|
| 230 | |||
| 231 | 2298 | private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart |
|
| 232 | { |
||
| 233 | 2298 | $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser); |
|
| 234 | |||
| 235 | 2298 | foreach ($this->environment->getBlockStartParsers() as $blockStartParser) { |
|
| 236 | \assert($blockStartParser instanceof BlockStartParserInterface); |
||
| 237 | 2295 | if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) { |
|
| 238 | 1080 | return $result; |
|
| 239 | } |
||
| 240 | } |
||
| 241 | |||
| 242 | 1377 | return null; |
|
| 243 | } |
||
| 244 | |||
| 245 | 2940 | private function closeBlockParsers(int $count, int $endLineNumber): void |
|
| 246 | { |
||
| 247 | 2940 | for ($i = 0; $i < $count; $i++) { |
|
| 248 | 2940 | $blockParser = $this->deactivateBlockParser(); |
|
| 249 | 2940 | $this->finalize($blockParser, $endLineNumber); |
|
| 250 | // Remember for inline parsing |
||
| 251 | 2940 | $this->closedBlockParsers[] = $blockParser; |
|
| 252 | } |
||
| 253 | 2940 | } |
|
| 254 | |||
| 255 | /** |
||
| 256 | * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings, |
||
| 257 | * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference |
||
| 258 | * definitions. |
||
| 259 | */ |
||
| 260 | 2940 | private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void |
|
| 261 | { |
||
| 262 | 2940 | if ($blockParser instanceof ParagraphParser) { |
|
| 263 | 2454 | $this->updateReferenceMap($blockParser->getReferences()); |
|
| 264 | } |
||
| 265 | |||
| 266 | 2940 | $blockParser->getBlock()->setEndLine($endLineNumber); |
|
| 267 | 2940 | $blockParser->closeBlock(); |
|
| 268 | 2940 | } |
|
| 269 | |||
| 270 | /** |
||
| 271 | * Walk through a block & children recursively, parsing string content into inline content where appropriate. |
||
| 272 | */ |
||
| 273 | 2940 | private function processInlines(): void |
|
| 274 | { |
||
| 275 | 2940 | $p = new InlineParserEngine($this->environment, $this->referenceMap); |
|
| 276 | |||
| 277 | 2940 | foreach ($this->closedBlockParsers as $blockParser) { |
|
| 278 | 2940 | if ($blockParser instanceof BlockContinueParserWithInlinesInterface) { |
|
| 279 | 2652 | $blockParser->parseInlines($p); |
|
| 280 | } |
||
| 281 | } |
||
| 282 | 2940 | } |
|
| 283 | |||
| 284 | /** |
||
| 285 | * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try |
||
| 286 | * its parent, and so on til we find a block that can accept children. |
||
| 287 | */ |
||
| 288 | 2940 | private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface |
|
| 289 | { |
||
| 290 | 2940 | $blockParser->getBlock()->setStartLine($this->lineNumber); |
|
| 291 | |||
| 292 | 2940 | while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) { |
|
| 293 | 141 | $this->closeBlockParsers(1, $this->lineNumber - 1); |
|
| 294 | } |
||
| 295 | |||
| 296 | 2940 | $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock()); |
|
| 297 | 2940 | $this->activateBlockParser($blockParser); |
|
| 298 | |||
| 299 | 2940 | return $blockParser; |
|
| 300 | } |
||
| 301 | |||
| 302 | 2952 | private function activateBlockParser(BlockContinueParserInterface $blockParser): void |
|
| 303 | { |
||
| 304 | 2952 | $this->activeBlockParsers[] = $blockParser; |
|
| 305 | 2952 | } |
|
| 306 | |||
| 307 | 2940 | private function deactivateBlockParser(): BlockContinueParserInterface |
|
| 308 | { |
||
| 309 | 2940 | $popped = \array_pop($this->activeBlockParsers); |
|
| 310 | 2940 | if ($popped === null) { |
|
| 311 | throw new \RuntimeException('The last block parser should not be deactivated'); |
||
| 312 | } |
||
| 313 | |||
| 314 | 2940 | return $popped; |
|
| 315 | } |
||
| 316 | |||
| 317 | 150 | private function prepareActiveBlockParserForReplacement(): void |
|
| 318 | { |
||
| 319 | // Note that we don't want to parse inlines or finalize this block, as it's getting replaced. |
||
| 320 | 150 | $old = $this->deactivateBlockParser(); |
|
| 321 | |||
| 322 | 150 | if ($old instanceof ParagraphParser) { |
|
| 323 | 150 | $this->updateReferenceMap($old->getReferences()); |
|
| 324 | } |
||
| 325 | |||
| 326 | 150 | $old->getBlock()->detach(); |
|
| 327 | 150 | } |
|
| 328 | |||
| 329 | /** |
||
| 330 | * @param ReferenceInterface[] $references |
||
| 331 | */ |
||
| 332 | 2562 | private function updateReferenceMap(iterable $references): void |
|
| 337 | } |
||
| 338 | } |
||
| 339 | 2562 | } |
|
| 340 | |||
| 341 | 2940 | public function getActiveBlockParser(): BlockContinueParserInterface |
|
| 349 | } |
||
| 350 | } |
||
| 351 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: