Completed
Push — master ( deae46...6c5f37 )
by Colin
01:01
created

InlineParserEngine::parseCharacter()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.7998
c 0
b 0
f 0
cc 4
nc 5
nop 2
crap 4
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 2496
    public function __construct(EnvironmentInterface $environment, ReferenceMapInterface $referenceMap)
49
    {
50 2496
        $this->environment  = $environment;
51 2496
        $this->referenceMap = $referenceMap;
52 2496
    }
53
54 2208
    public function parse(string $contents, AbstractBlock $block): void
55
    {
56 2208
        $inlineParserContext = new InlineParserContext($contents, $block, $this->referenceMap);
57 2208
        $cursor              = $inlineParserContext->getCursor();
58 2208
        while (($character = $cursor->getCharacter()) !== null) {
59 2205
            if (! $this->parseCharacter($character, $inlineParserContext)) {
60 2067
                $this->addPlainText($character, $block, $inlineParserContext);
61
            }
62
        }
63
64 2208
        $delimiterStack = $inlineParserContext->getDelimiterStack();
65 2208
        $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors());
66 2208
        $delimiterStack->removeAll();
67
68 2208
        AdjacentTextMerger::mergeChildNodes($block);
69 2208
    }
70
71
    /**
72
     * @return bool Whether we successfully parsed a character at that position
73
     */
74 2205
    private function parseCharacter(string $character, InlineParserContext $inlineParserContext): bool
75
    {
76 2205
        foreach ($this->environment->getInlineParsersForCharacter($character) as $parser) {
77 1125
            if ($parser->parse($inlineParserContext)) {
78 1053
                return true;
79
            }
80
        }
81
82 2076
        if ($delimiterProcessor = $this->environment->getDelimiterProcessors()->getDelimiterProcessor($character)) {
83 675
            return $this->parseDelimiters($delimiterProcessor, $inlineParserContext);
84
        }
85
86 2067
        return false;
87
    }
88
89 675
    private function parseDelimiters(DelimiterProcessorInterface $delimiterProcessor, InlineParserContext $inlineContext): bool
90
    {
91 675
        $cursor    = $inlineContext->getCursor();
92 675
        $character = $cursor->getCharacter();
93 675
        $numDelims = 0;
94
95 675
        if ($character === null) {
96
            throw new \RuntimeException('Cannot parse delimiters without a valid character');
97
        }
98
99 675
        $charBefore = $cursor->peek(-1);
100 675
        if ($charBefore === null) {
101 408
            $charBefore = "\n";
102
        }
103
104 675
        while ($cursor->peek($numDelims) === $character) {
105 675
            ++$numDelims;
106
        }
107
108 675
        if ($numDelims < $delimiterProcessor->getMinLength()) {
109 6
            return false;
110
        }
111
112 669
        $cursor->advanceBy($numDelims);
113
114 669
        $charAfter = $cursor->getCharacter();
115 669
        if ($charAfter === null) {
116 414
            $charAfter = "\n";
117
        }
118
119 669
        [$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...
120
121 669
        $node = new Text(\str_repeat($character, $numDelims), [
122 669
            'delim' => true,
123
        ]);
124 669
        $inlineContext->getContainer()->appendChild($node);
125
126
        // Add entry to stack to this opener
127 669
        if ($canOpen || $canClose) {
128 606
            $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
129 606
            $inlineContext->getDelimiterStack()->push($delimiter);
130
        }
131
132 669
        return true;
133
    }
134
135 2067
    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 2067
        $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 2067
        if ($text === null) {
142 285
            $inlineParserContext->getCursor()->advanceBy(1);
143 285
            $text = $character;
144
        }
145
146 2067
        $lastInline = $container->lastChild();
147 2067
        if ($lastInline instanceof Text && ! isset($lastInline->data['delim'])) {
148 330
            $lastInline->append($text);
149
        } else {
150 2031
            $container->appendChild(new Text($text));
151
        }
152 2067
    }
153
154
    /**
155
     * @return bool[]
156
     */
157 669
    private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array
158
    {
159 669
        $afterIsWhitespace   = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
160 669
        $afterIsPunctuation  = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
161 669
        $beforeIsWhitespace  = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
162 669
        $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
163
164 669
        $leftFlanking  = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
165 669
        $rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
166
167 669
        if ($character === '_') {
168 234
            $canOpen  = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation);
169 234
            $canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation);
170
        } else {
171 471
            $canOpen  = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
172 471
            $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
173
        }
174
175 669
        return [$canOpen, $canClose];
176
    }
177
}
178