Completed
Push — master ( 990cee...3d5add )
by Colin
13s queued 11s
created

InlineParserEngine   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 98.53%

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 11
dl 0
loc 153
ccs 67
cts 68
cp 0.9853
rs 9.84
c 0
b 0
f 0

7 Methods

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