InlineParserEngine   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Test Coverage

Coverage 98.57%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 34
eloc 66
dl 0
loc 144
ccs 69
cts 70
cp 0.9857
rs 9.68
c 1
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
C determineCanOpenOrClose() 0 19 14
A parseCharacter() 0 13 4
A __construct() 0 4 1
A addPlainText() 0 16 4
A parse() 0 15 3
B parseDelimiters() 0 44 8
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the league/commonmark package.
7
 *
8
 * (c) Colin O'Dell <[email protected]>
9
 *
10
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11
 *  - (c) John MacFarlane
12
 *
13
 * For the full copyright and license information, please view the LICENSE
14
 * file that was distributed with this source code.
15
 */
16
17
namespace League\CommonMark\Parser;
18
19
use League\CommonMark\Delimiter\Delimiter;
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
21
use League\CommonMark\Environment\EnvironmentInterface;
22
use League\CommonMark\Node\Block\AbstractBlock;
23
use League\CommonMark\Node\Inline\AdjacentTextMerger;
24
use League\CommonMark\Node\Inline\Text;
25
use League\CommonMark\Node\Node;
26
use League\CommonMark\Reference\ReferenceMapInterface;
27
use League\CommonMark\Util\RegexHelper;
28
29
/**
30
 * @internal
31
 */
32
final class InlineParserEngine implements InlineParserEngineInterface
33
{
34
    /**
35
     * @var EnvironmentInterface
36
     *
37
     * @psalm-readonly
38
     */
39
    private $environment;
40
41
    /**
42
     * @var ReferenceMapInterface
43
     *
44
     * @psalm-readonly
45
     */
46
    private $referenceMap;
47
48 2826
    public function __construct(EnvironmentInterface $environment, ReferenceMapInterface $referenceMap)
49
    {
50 2826
        $this->environment  = $environment;
51 2826
        $this->referenceMap = $referenceMap;
52 2826
    }
53
54 2535
    public function parse(string $contents, AbstractBlock $block): void
55
    {
56 2535
        $inlineParserContext = new InlineParserContext($contents, $block, $this->referenceMap);
57 2535
        $cursor              = $inlineParserContext->getCursor();
58 2535
        while (($character = $cursor->getCharacter()) !== null) {
59 2532
            if (! $this->parseCharacter($character, $inlineParserContext)) {
60 2394
                $this->addPlainText($character, $block, $inlineParserContext);
61
            }
62
        }
63
64 2535
        $delimiterStack = $inlineParserContext->getDelimiterStack();
65 2535
        $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors());
66 2535
        $delimiterStack->removeAll();
67
68 2535
        AdjacentTextMerger::mergeChildNodes($block);
69 2535
    }
70
71
    /**
72
     * @return bool Whether we successfully parsed a character at that position
73
     */
74 2532
    private function parseCharacter(string $character, InlineParserContext $inlineParserContext): bool
75
    {
76 2532
        foreach ($this->environment->getInlineParsersForCharacter($character) as $parser) {
77 1449
            if ($parser->parse($inlineParserContext)) {
78 1362
                return true;
79
            }
80
        }
81
82 2403
        if ($delimiterProcessor = $this->environment->getDelimiterProcessors()->getDelimiterProcessor($character)) {
83 687
            return $this->parseDelimiters($delimiterProcessor, $inlineParserContext);
84
        }
85
86 2394
        return false;
87
    }
88
89 687
    private function parseDelimiters(DelimiterProcessorInterface $delimiterProcessor, InlineParserContext $inlineContext): bool
90
    {
91 687
        $cursor    = $inlineContext->getCursor();
92 687
        $character = $cursor->getCharacter();
93 687
        $numDelims = 0;
94
95 687
        if ($character === null) {
96
            throw new \RuntimeException('Cannot parse delimiters without a valid character');
97
        }
98
99 687
        $charBefore = $cursor->peek(-1);
100 687
        if ($charBefore === null) {
101 408
            $charBefore = "\n";
102
        }
103
104 687
        while ($cursor->peek($numDelims) === $character) {
105 687
            ++$numDelims;
106
        }
107
108 687
        if ($numDelims < $delimiterProcessor->getMinLength()) {
109 6
            return false;
110
        }
111
112 681
        $cursor->advanceBy($numDelims);
113
114 681
        $charAfter = $cursor->getCharacter();
115 681
        if ($charAfter === null) {
116 414
            $charAfter = "\n";
117
        }
118
119 681
        [$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $delimiterProcessor);
120
121 681
        $node = new Text(\str_repeat($character, $numDelims), [
122 681
            'delim' => true,
123
        ]);
124 681
        $inlineContext->getContainer()->appendChild($node);
125
126
        // Add entry to stack to this opener
127 681
        if ($canOpen || $canClose) {
128 618
            $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
129 618
            $inlineContext->getDelimiterStack()->push($delimiter);
130
        }
131
132 681
        return true;
133
    }
134
135 2394
    private function addPlainText(string $character, Node $container, InlineParserContext $inlineParserContext): void
136
    {
137
        // We reach here if none of the parsers can handle the input
138
        // Attempt to match multiple non-special characters at once
139 2394
        $text = $inlineParserContext->getCursor()->match($this->environment->getInlineParserCharacterRegex());
140
        // This might fail if we're currently at a special character which wasn't parsed; if so, just add that character
141 2394
        if ($text === null) {
142 339
            $inlineParserContext->getCursor()->advanceBy(1);
143 339
            $text = $character;
144
        }
145
146 2394
        $lastInline = $container->lastChild();
147 2394
        if ($lastInline instanceof Text && ! isset($lastInline->data['delim'])) {
148 378
            $lastInline->append($text);
149
        } else {
150 2358
            $container->appendChild(new Text($text));
151
        }
152 2394
    }
153
154
    /**
155
     * @return bool[]
156
     */
157 681
    private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array
158
    {
159 681
        $afterIsWhitespace   = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
160 681
        $afterIsPunctuation  = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
161 681
        $beforeIsWhitespace  = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
162 681
        $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
163
164 681
        $leftFlanking  = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
165 681
        $rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
166
167 681
        if ($character === '_') {
168 234
            $canOpen  = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation);
169 234
            $canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation);
170
        } else {
171 483
            $canOpen  = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
172 483
            $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
173
        }
174
175 681
        return [$canOpen, $canClose];
176
    }
177
}
178