Issues (85)

src/Extension/Table/TableParser.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This is part of the league/commonmark package.
7
 *
8
 * (c) Martin HasoĊˆ <[email protected]>
9
 * (c) Webuni s.r.o. <[email protected]>
10
 * (c) Colin O'Dell <[email protected]>
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 */
15
16
namespace League\CommonMark\Extension\Table;
17
18
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
19
use League\CommonMark\Parser\Block\BlockContinue;
20
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
21
use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface;
22
use League\CommonMark\Parser\Cursor;
23
use League\CommonMark\Parser\InlineParserEngineInterface;
24
use League\CommonMark\Util\ArrayCollection;
25
26
final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
27
{
28
    /**
29
     * @internal
30
     */
31
    public const DEFAULT_MAX_AUTOCOMPLETED_CELLS = 10_000;
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ';' on line 31 at column 53
Loading history...
32
33
    /** @psalm-readonly */
34
    private Table $block;
35
36
    /**
37
     * @var ArrayCollection<string>
38
     *
39
     * @psalm-readonly-allow-private-mutation
40
     */
41
    private ArrayCollection $bodyLines;
42
43
    /**
44
     * @var array<int, string|null>
45
     * @psalm-var array<int, TableCell::ALIGN_*|null>
46
     * @phpstan-var array<int, TableCell::ALIGN_*|null>
47
     *
48
     * @psalm-readonly
49
     */
50
    private array $columns;
51
52
    /**
53
     * @var array<int, string>
54
     *
55
     * @psalm-readonly-allow-private-mutation
56
     */
57
    private array $headerCells;
58
59
    /** @psalm-readonly-allow-private-mutation */
60
    private bool $nextIsSeparatorLine = true;
61
62
    private int $remainingAutocompletedCells;
63
64
    /**
65
     * @param array<int, string|null> $columns
66
     * @param array<int, string>      $headerCells
67
     *
68
     * @psalm-param array<int, TableCell::ALIGN_*|null> $columns
69
     *
70
     * @phpstan-param array<int, TableCell::ALIGN_*|null> $columns
71
     */
72 58
    public function __construct(array $columns, array $headerCells, int $remainingAutocompletedCells = self::DEFAULT_MAX_AUTOCOMPLETED_CELLS)
73
    {
74 58
        $this->block                       = new Table();
75 58
        $this->bodyLines                   = new ArrayCollection();
76 58
        $this->columns                     = $columns;
77 58
        $this->headerCells                 = $headerCells;
78 58
        $this->remainingAutocompletedCells = $remainingAutocompletedCells;
79
    }
80
81 56
    public function canHaveLazyContinuationLines(): bool
82
    {
83 56
        return true;
84
    }
85
86 58
    public function getBlock(): Table
87
    {
88 58
        return $this->block;
89
    }
90
91 56
    public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
92
    {
93 56
        if (\strpos($cursor->getLine(), '|') === false) {
94 18
            return BlockContinue::none();
95
        }
96
97 56
        return BlockContinue::at($cursor);
98
    }
99
100 58
    public function addLine(string $line): void
101
    {
102 58
        if ($this->nextIsSeparatorLine) {
103 58
            $this->nextIsSeparatorLine = false;
104
        } else {
105 56
            $this->bodyLines[] = $line;
106
        }
107
    }
108
109 58
    public function parseInlines(InlineParserEngineInterface $inlineParser): void
110
    {
111 58
        $headerColumns = \count($this->headerCells);
112
113 58
        $head = new TableSection(TableSection::TYPE_HEAD);
114 58
        $this->block->appendChild($head);
115
116 58
        $headerRow = new TableRow();
117 58
        $head->appendChild($headerRow);
118 58
        for ($i = 0; $i < $headerColumns; $i++) {
119 58
            $cell      = $this->headerCells[$i];
120 58
            $tableCell = $this->parseCell($cell, $i, $inlineParser);
121 58
            $tableCell->setType(TableCell::TYPE_HEADER);
122 58
            $headerRow->appendChild($tableCell);
123
        }
124
125 58
        $body = null;
126 58
        foreach ($this->bodyLines as $rowLine) {
127 56
            $cells = self::split($rowLine);
128 56
            $row   = new TableRow();
129
130
            // Body can not have more columns than head
131 56
            for ($i = 0; $i < $headerColumns; $i++) {
132
                // It can have less columns though, in which case we'll autocomplete the empty ones (up to some limit)
133 56
                if (! isset($cells[$i]) && $this->remainingAutocompletedCells-- <= 0) {
134
                    // Too many cells were auto-completed, so we'll just stop here
135
                    return;
136
                }
137
138 56
                $cell      = $cells[$i] ?? '';
139 56
                $tableCell = $this->parseCell($cell, $i, $inlineParser);
140 56
                $row->appendChild($tableCell);
141
            }
142
143 56
            if ($body === null) {
144
                // It's valid to have a table without body. In that case, don't add an empty TableBody node.
145 56
                $body = new TableSection();
146 56
                $this->block->appendChild($body);
147
            }
148
149 56
            $body->appendChild($row);
150
        }
151
    }
152
153 58
    private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
154
    {
155 58
        $tableCell = new TableCell(TableCell::TYPE_DATA, $this->columns[$column] ?? null);
156
157 58
        if ($cell !== '') {
158 58
            $inlineParser->parse(\trim($cell), $tableCell);
159
        }
160
161 58
        return $tableCell;
162
    }
163
164
    /**
165
     * @internal
166
     *
167
     * @return array<int, string>
168
     */
169 60
    public static function split(string $line): array
170
    {
171 60
        $cursor = new Cursor(\trim($line));
172
173 60
        if ($cursor->getCurrentCharacter() === '|') {
174 44
            $cursor->advanceBy(1);
175
        }
176
177 60
        $cells = [];
178 60
        $sb    = '';
179
180 60
        while (! $cursor->isAtEnd()) {
181 60
            switch ($c = $cursor->getCurrentCharacter()) {
182 60
                case '\\':
183 10
                    if ($cursor->peek() === '|') {
184
                        // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
185
                        // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
186
                        // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
187 10
                        $sb .= '|';
188 10
                        $cursor->advanceBy(1);
189
                    } else {
190
                        // Preserve backslash before other characters or at end of line.
191 4
                        $sb .= '\\';
192
                    }
193
194 10
                    break;
195 60
                case '|':
196 60
                    $cells[] = $sb;
197 60
                    $sb      = '';
198 60
                    break;
199
                default:
200 60
                    $sb .= $c;
201
            }
202
203 60
            $cursor->advanceBy(1);
204
        }
205
206 60
        if ($sb !== '') {
207 26
            $cells[] = $sb;
208
        }
209
210 60
        return $cells;
211
    }
212
}
213