Completed
Push — master ( 0b0b24...54ff37 )
by Colin
02:37
created

InlineParserEngine   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 145
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 11
dl 0
loc 145
ccs 70
cts 70
cp 1
rs 9.68
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A parse() 0 14 3
A parseCharacter() 0 14 4
B parseDelimiters() 0 41 7
A processInlines() 0 8 1
A addPlainText() 0 18 4
C determineCanOpenOrClose() 0 20 14
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark\Parser;
16
17
use League\CommonMark\Delimiter\Delimiter;
18
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
19
use League\CommonMark\Environment\EnvironmentInterface;
20
use League\CommonMark\Node\Block\AbstractStringContainerBlock;
21
use League\CommonMark\Node\Inline\AdjacentTextMerger;
22
use League\CommonMark\Node\Inline\Text;
23
use League\CommonMark\Node\Node;
24
use League\CommonMark\Reference\ReferenceMapInterface;
25
use League\CommonMark\Util\RegexHelper;
26
27
/**
28
 * @internal
29
 */
30
final class InlineParserEngine
31
{
32
    /** @var EnvironmentInterface */
33
    protected $environment;
34
35 2505
    public function __construct(EnvironmentInterface $environment)
36
    {
37 2505
        $this->environment = $environment;
38 2505
    }
39
40 2448
    public function parse(AbstractStringContainerBlock $container, ReferenceMapInterface $referenceMap): void
41
    {
42 2448
        $inlineParserContext = new InlineParserContext($container, $referenceMap);
43 2448
        $cursor = $inlineParserContext->getCursor();
44 2448
        while (($character = $cursor->getCharacter()) !== null) {
45 2433
            if (!$this->parseCharacter($character, $inlineParserContext)) {
46 2256
                $this->addPlainText($character, $container, $inlineParserContext);
47
            }
48
        }
49
50 2448
        $this->processInlines($inlineParserContext);
51
52 2448
        AdjacentTextMerger::mergeChildNodes($container);
53 2448
    }
54
55
    /**
56
     * @param string              $character
57
     * @param InlineParserContext $inlineParserContext
58
     *
59
     * @return bool Whether we successfully parsed a character at that position
60
     */
61 2433
    private function parseCharacter(string $character, InlineParserContext $inlineParserContext): bool
62
    {
63 2433
        foreach ($this->environment->getInlineParsersForCharacter($character) as $parser) {
64 1365
            if ($parser->parse($inlineParserContext)) {
65 1293
                return true;
66
            }
67
        }
68
69 2268
        if ($delimiterProcessor = $this->environment->getDelimiterProcessors()->getDelimiterProcessor($character)) {
70 726
            return $this->parseDelimiters($delimiterProcessor, $inlineParserContext);
71
        }
72
73 2256
        return false;
74
    }
75
76 726
    private function parseDelimiters(DelimiterProcessorInterface $delimiterProcessor, InlineParserContext $inlineContext): bool
77
    {
78 726
        $cursor = $inlineContext->getCursor();
79 726
        $character = $cursor->getCharacter();
80 726
        $numDelims = 0;
81
82 726
        $charBefore = $cursor->peek(-1);
83 726
        if ($charBefore === null) {
84 411
            $charBefore = "\n";
85
        }
86
87 726
        while ($cursor->peek($numDelims) === $character) {
88 726
            ++$numDelims;
89
        }
90
91 726
        if ($numDelims < $delimiterProcessor->getMinLength()) {
92 6
            return false;
93
        }
94
95 720
        $cursor->advanceBy($numDelims);
96
97 720
        $charAfter = $cursor->getCharacter();
98 720
        if ($charAfter === null) {
99 435
            $charAfter = "\n";
100
        }
101
102 720
        [$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $delimiterProcessor);
0 ignored issues
show
Bug introduced by
The variable $canOpen does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $canClose does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
103
104 720
        $node = new Text(\str_repeat($character, $numDelims), [
105 720
            'delim' => true,
106
        ]);
107 720
        $inlineContext->getContainer()->appendChild($node);
108
109
        // Add entry to stack to this opener
110 720
        if ($canOpen || $canClose) {
111 651
            $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
112 651
            $inlineContext->getDelimiterStack()->push($delimiter);
113
        }
114
115 720
        return true;
116
    }
117
118 2448
    private function processInlines(InlineParserContext $inlineParserContext): void
119
    {
120 2448
        $delimiterStack = $inlineParserContext->getDelimiterStack();
121 2448
        $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors());
122
123
        // Remove all delimiters
124 2448
        $delimiterStack->removeAll();
125 2448
    }
126
127 2256
    private function addPlainText(string $character, Node $container, InlineParserContext $inlineParserContext): void
128
    {
129
        // We reach here if none of the parsers can handle the input
130
        // Attempt to match multiple non-special characters at once
131 2256
        $text = $inlineParserContext->getCursor()->match($this->environment->getInlineParserCharacterRegex());
132
        // This might fail if we're currently at a special character which wasn't parsed; if so, just add that character
133 2256
        if ($text === null) {
134 309
            $inlineParserContext->getCursor()->advanceBy(1);
135 309
            $text = $character;
136
        }
137
138 2256
        $lastInline = $container->lastChild();
139 2256
        if ($lastInline instanceof Text && !isset($lastInline->data['delim'])) {
140 354
            $lastInline->append($text);
141
        } else {
142 2217
            $container->appendChild(new Text($text));
143
        }
144 2256
    }
145
146
    /**
147
     * @param string                      $charBefore
148
     * @param string                      $charAfter
149
     * @param string                      $character
150
     * @param DelimiterProcessorInterface $delimiterProcessor
151
     *
152
     * @return bool[]
153
     */
154 720
    private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor)
155
    {
156 720
        $afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
157 720
        $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
158 720
        $beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
159 720
        $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
160
161 720
        $leftFlanking = !$afterIsWhitespace && (!$afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
162 720
        $rightFlanking = !$beforeIsWhitespace && (!$beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
163
164 720
        if ($character === '_') {
165 237
            $canOpen = $leftFlanking && (!$rightFlanking || $beforeIsPunctuation);
166 237
            $canClose = $rightFlanking && (!$leftFlanking || $afterIsPunctuation);
167
        } else {
168 522
            $canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
169 522
            $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
170
        }
171
172 720
        return [$canOpen, $canClose];
173
    }
174
}
175