Completed
Push — master ( 83739a...a13ee4 )
by Colin
14s queued 11s
created

InlineParserEngine::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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;
16
17
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
18
use League\CommonMark\Delimiter\Delimiter;
19
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
20
use League\CommonMark\Inline\AdjacentTextMerger;
21
use League\CommonMark\Inline\Element\Text;
22
use League\CommonMark\Node\Node;
23
use League\CommonMark\Reference\ReferenceMapInterface;
24
use League\CommonMark\Util\RegexHelper;
25
26
/**
27
 * @internal
28
 */
29
final class InlineParserEngine
30
{
31
    protected $environment;
32
33 2442
    public function __construct(EnvironmentInterface $environment)
34
    {
35 2442
        $this->environment = $environment;
36 2442
    }
37
38
    /**
39
     * @param AbstractStringContainerBlock $container
40
     * @param ReferenceMapInterface        $referenceMap
41
     */
42 2385
    public function parse(AbstractStringContainerBlock $container, ReferenceMapInterface $referenceMap)
43
    {
44 2385
        $inlineParserContext = new InlineParserContext($container, $referenceMap);
45 2385
        $cursor = $inlineParserContext->getCursor();
46 2385
        while (($character = $cursor->getCharacter()) !== null) {
47 2370
            if (!$this->parseCharacter($character, $inlineParserContext)) {
48 2193
                $this->addPlainText($character, $container, $inlineParserContext);
49
            }
50
        }
51
52 2385
        $this->processInlines($inlineParserContext);
53
54 2385
        AdjacentTextMerger::mergeChildNodes($container);
55 2385
    }
56
57
    /**
58
     * @param string              $character
59
     * @param InlineParserContext $inlineParserContext
60
     *
61
     * @return bool Whether we successfully parsed a character at that position
62
     */
63 2370
    private function parseCharacter(string $character, InlineParserContext $inlineParserContext): bool
64
    {
65 2370
        foreach ($this->environment->getInlineParsersForCharacter($character) as $parser) {
66 1350
            if ($parser->parse($inlineParserContext)) {
67 1293
                return true;
68
            }
69
        }
70
71 2205
        if ($delimiterProcessor = $this->environment->getDelimiterProcessors()->getDelimiterProcessor($character)) {
72 714
            return $this->parseDelimiters($delimiterProcessor, $inlineParserContext);
73
        }
74
75 2193
        return false;
76
    }
77
78 714
    private function parseDelimiters(DelimiterProcessorInterface $delimiterProcessor, InlineParserContext $inlineContext): bool
79
    {
80 714
        $cursor = $inlineContext->getCursor();
81 714
        $character = $cursor->getCharacter();
82 714
        $numDelims = 0;
83
84 714
        $charBefore = $cursor->peek(-1);
85 714
        if ($charBefore === null) {
86 408
            $charBefore = "\n";
87
        }
88
89 714
        while ($cursor->peek($numDelims) === $character) {
90 714
            ++$numDelims;
91
        }
92
93 714
        if ($numDelims < $delimiterProcessor->getMinLength()) {
94 6
            return false;
95
        }
96
97 708
        $cursor->advanceBy($numDelims);
98
99 708
        $charAfter = $cursor->getCharacter();
100 708
        if ($charAfter === null) {
101 423
            $charAfter = "\n";
102
        }
103
104 708
        list($canOpen, $canClose) = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $delimiterProcessor);
105
106 708
        $node = new Text(\str_repeat($character, $numDelims), [
107 708
            'delim' => true,
108
        ]);
109 708
        $inlineContext->getContainer()->appendChild($node);
110
111
        // Add entry to stack to this opener
112 708
        if ($canOpen || $canClose) {
113 639
            $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
114 639
            $inlineContext->getDelimiterStack()->push($delimiter);
115
        }
116
117 708
        return true;
118
    }
119
120
    /**
121
     * @param InlineParserContext $inlineParserContext
122
     */
123 2385
    private function processInlines(InlineParserContext $inlineParserContext)
124
    {
125 2385
        $delimiterStack = $inlineParserContext->getDelimiterStack();
126 2385
        $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors());
127
128
        // Remove all delimiters
129 2385
        $delimiterStack->removeAll();
130 2385
    }
131
132
    /**
133
     * @param string              $character
134
     * @param Node                $container
135
     * @param InlineParserContext $inlineParserContext
136
     */
137 2193
    private function addPlainText(string $character, Node $container, InlineParserContext $inlineParserContext)
138
    {
139
        // We reach here if none of the parsers can handle the input
140
        // Attempt to match multiple non-special characters at once
141 2193
        $text = $inlineParserContext->getCursor()->match($this->environment->getInlineParserCharacterRegex());
142
        // This might fail if we're currently at a special character which wasn't parsed; if so, just add that character
143 2193
        if ($text === null) {
144 294
            $inlineParserContext->getCursor()->advanceBy(1);
145 294
            $text = $character;
146
        }
147
148 2193
        $lastInline = $container->lastChild();
149 2193
        if ($lastInline instanceof Text && !isset($lastInline->data['delim'])) {
150 339
            $lastInline->append($text);
151
        } else {
152 2154
            $container->appendChild(new Text($text));
153
        }
154 2193
    }
155
156
    /**
157
     * @param string                      $charBefore
158
     * @param string                      $charAfter
159
     * @param string                      $character
160
     * @param DelimiterProcessorInterface $delimiterProcessor
161
     *
162
     * @return bool[]
163
     */
164 708
    private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor)
165
    {
166 708
        $afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
167 708
        $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
168 708
        $beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
169 708
        $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
170
171 708
        $leftFlanking = !$afterIsWhitespace && (!$afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
172 708
        $rightFlanking = !$beforeIsWhitespace && (!$beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
173
174 708
        if ($character === '_') {
175 237
            $canOpen = $leftFlanking && (!$rightFlanking || $beforeIsPunctuation);
176 237
            $canClose = $rightFlanking && (!$leftFlanking || $afterIsPunctuation);
177
        } else {
178 510
            $canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
179 510
            $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
180
        }
181
182 708
        return [$canOpen, $canClose];
183
    }
184
}
185