1<?php
2
3/*
4 * This file is part of the league/commonmark package.
5 *
6 * (c) Colin O'Dell <colinodell@gmail.com>
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
15namespace League\CommonMark\Block\Element;
16
17use League\CommonMark\ContextInterface;
18use League\CommonMark\Cursor;
19use League\CommonMark\Util\RegexHelper;
20
21class FencedCode extends AbstractStringContainerBlock
22{
23    /**
24     * @var string
25     */
26    protected $info;
27
28    /**
29     * @var int
30     */
31    protected $length;
32
33    /**
34     * @var string
35     */
36    protected $char;
37
38    /**
39     * @var int
40     */
41    protected $offset;
42
43    /**
44     * @param int    $length
45     * @param string $char
46     * @param int    $offset
47     */
48    public function __construct(int $length, string $char, int $offset)
49    {
50        parent::__construct();
51
52        $this->length = $length;
53        $this->char = $char;
54        $this->offset = $offset;
55    }
56
57    /**
58     * @return string
59     */
60    public function getInfo(): string
61    {
62        return $this->info;
63    }
64
65    /**
66     * @return string[]
67     */
68    public function getInfoWords(): array
69    {
70        return \preg_split('/\s+/', $this->info) ?: [];
71    }
72
73    /**
74     * @return string
75     */
76    public function getChar(): string
77    {
78        return $this->char;
79    }
80
81    /**
82     * @param string $char
83     *
84     * @return $this
85     */
86    public function setChar(string $char): self
87    {
88        $this->char = $char;
89
90        return $this;
91    }
92
93    /**
94     * @return int
95     */
96    public function getLength(): int
97    {
98        return $this->length;
99    }
100
101    /**
102     * @param int $length
103     *
104     * @return $this
105     */
106    public function setLength(int $length): self
107    {
108        $this->length = $length;
109
110        return $this;
111    }
112
113    /**
114     * @return int
115     */
116    public function getOffset(): int
117    {
118        return $this->offset;
119    }
120
121    /**
122     * @param int $offset
123     *
124     * @return $this
125     */
126    public function setOffset(int $offset): self
127    {
128        $this->offset = $offset;
129
130        return $this;
131    }
132
133    public function canContain(AbstractBlock $block): bool
134    {
135        return false;
136    }
137
138    public function isCode(): bool
139    {
140        return true;
141    }
142
143    public function matchesNextLine(Cursor $cursor): bool
144    {
145        if ($this->length === -1) {
146            if ($cursor->isBlank()) {
147                $this->lastLineBlank = true;
148            }
149
150            return false;
151        }
152
153        // Skip optional spaces of fence offset
154        $cursor->match('/^ {0,' . $this->offset . '}/');
155
156        return true;
157    }
158
159    public function finalize(ContextInterface $context, int $endLineNumber)
160    {
161        parent::finalize($context, $endLineNumber);
162
163        // first line becomes info string
164        $firstLine = $this->strings->first();
165        if ($firstLine === false) {
166            $firstLine = '';
167        }
168
169        $this->info = RegexHelper::unescape(\trim($firstLine));
170
171        if ($this->strings->count() === 1) {
172            $this->finalStringContents = '';
173        } else {
174            $this->finalStringContents = \implode("\n", $this->strings->slice(1)) . "\n";
175        }
176    }
177
178    public function handleRemainingContents(ContextInterface $context, Cursor $cursor)
179    {
180        /** @var self $container */
181        $container = $context->getContainer();
182
183        // check for closing code fence
184        if ($cursor->getIndent() <= 3 && $cursor->getNextNonSpaceCharacter() === $container->getChar()) {
185            $match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
186            if ($match !== null && \strlen($match[0]) >= $container->getLength()) {
187                // don't add closing fence to container; instead, close it:
188                $this->setLength(-1); // -1 means we've passed closer
189
190                return;
191            }
192        }
193
194        $container->addLine($cursor->getRemainder());
195    }
196
197    public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
198    {
199        return false;
200    }
201}
202