Passed
Push — latest ( 5d397b...0a90d6 )
by Colin
02:04
created

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

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\Node\Block\AbstractBlock;
19
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
20
use League\CommonMark\Parser\Block\BlockContinue;
21
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
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
27
{
28
    /**
29
     * @var Table
30
     *
31
     * @psalm-readonly
32
     */
33
    private $block;
34
35
    /**
36
     * @var ArrayCollection<string>
37
     *
38
     * @psalm-readonly-allow-private-mutation
39
     */
40
    private $bodyLines;
41
42
    /**
43
     * @var array<int, string|null>
44
     *
45
     * @psalm-readonly
46
     */
47
    private $columns;
48
49
    /**
50
     * @var array<int, string>
51
     *
52
     * @psalm-readonly-allow-private-mutation
53
     */
54
    private $headerCells;
55
56
    /**
57
     * @var bool
58
     *
59
     * @psalm-readonly-allow-private-mutation
60
     */
61
    private $nextIsSeparatorLine = true;
62
63
    /**
64
     * @param array<int, string|null> $columns
65
     * @param array<int, string>      $headerCells
66
     */
67 72
    public function __construct(array $columns, array $headerCells)
68
    {
69 72
        $this->block       = new Table();
70 72
        $this->bodyLines   = new ArrayCollection();
71 72
        $this->columns     = $columns;
72 72
        $this->headerCells = $headerCells;
73 72
    }
74
75 12
    public function canHaveLazyContinuationLines(): bool
76
    {
77 12
        return true;
78
    }
79
80
    /**
81
     * @return Table
82
     */
83 72
    public function getBlock(): AbstractBlock
84
    {
85 72
        return $this->block;
86
    }
87
88 69
    public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
89
    {
90 69
        if (\strpos($cursor->getLine(), '|') === false) {
91 24
            return BlockContinue::none();
92
        }
93
94 69
        return BlockContinue::at($cursor);
95
    }
96
97 72
    public function addLine(string $line): void
98
    {
99 72
        if ($this->nextIsSeparatorLine) {
100 72
            $this->nextIsSeparatorLine = false;
101
        } else {
102 69
            $this->bodyLines[] = $line;
103
        }
104 72
    }
105
106 72
    public function parseInlines(InlineParserEngineInterface $inlineParser): void
107
    {
108 72
        $headerColumns = \count($this->headerCells);
109
110 72
        $head = new TableSection(TableSection::TYPE_HEAD);
111 72
        $this->block->appendChild($head);
112
113 72
        $headerRow = new TableRow();
114 72
        $head->appendChild($headerRow);
115 72
        for ($i = 0; $i < $headerColumns; $i++) {
116 72
            $cell      = $this->headerCells[$i];
117 72
            $tableCell = self::parseCell($cell, $i, $inlineParser);
0 ignored issues
show
Bug Best Practice introduced by
The method League\CommonMark\Extens...ableParser::parseCell() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
            /** @scrutinizer ignore-call */ 
118
            $tableCell = self::parseCell($cell, $i, $inlineParser);
Loading history...
118 72
            $tableCell->setType(TableCell::TYPE_HEAD);
119 72
            $headerRow->appendChild($tableCell);
120
        }
121
122 72
        $body = null;
123 72
        foreach ($this->bodyLines as $rowLine) {
124 69
            $cells = self::split($rowLine);
125 69
            $row   = new TableRow();
126
127
            // Body can not have more columns than head
128 69
            for ($i = 0; $i < $headerColumns; $i++) {
129 69
                $cell      = $cells[$i] ?? '';
130 69
                $tableCell = self::parseCell($cell, $i, $inlineParser);
131 69
                $row->appendChild($tableCell);
132
            }
133
134 69
            if ($body === null) {
135
                // It's valid to have a table without body. In that case, don't add an empty TableBody node.
136 69
                $body = new TableSection();
137 69
                $this->block->appendChild($body);
138
            }
139
140 69
            $body->appendChild($row);
141
        }
142 72
    }
143
144 72
    private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
145
    {
146 72
        $tableCell = new TableCell();
147
148 72
        if ($column < \count($this->columns)) {
149 72
            $tableCell->setAlign($this->columns[$column]);
150
        }
151
152 72
        $inlineParser->parse(\trim($cell), $tableCell);
153
154 72
        return $tableCell;
155
    }
156
157
    /**
158
     * @internal
159
     *
160
     * @return array<int, string>
161
     */
162 75
    public static function split(string $line): array
163
    {
164 75
        $cursor = new Cursor(\trim($line));
165
166 75
        if ($cursor->getCharacter() === '|') {
167 54
            $cursor->advanceBy(1);
168
        }
169
170 75
        $cells = [];
171 75
        $sb    = '';
172
173 75
        while (! $cursor->isAtEnd()) {
174 75
            switch ($c = $cursor->getCharacter()) {
175 75
                case '\\':
176 12
                    if ($cursor->peek() === '|') {
177
                        // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
178
                        // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
179
                        // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
180 12
                        $sb .= '|';
181 12
                        $cursor->advanceBy(1);
182
                    } else {
183
                        // Preserve backslash before other characters or at end of line.
184 3
                        $sb .= '\\';
185
                    }
186
187 12
                    break;
188 75
                case '|':
189 75
                    $cells[] = $sb;
190 75
                    $sb      = '';
191 75
                    break;
192
                default:
193 75
                    $sb .= $c;
194
            }
195
196 75
            $cursor->advanceBy(1);
197
        }
198
199 75
        if ($sb !== '') {
200 36
            $cells[] = $sb;
201
        }
202
203 75
        return $cells;
204
    }
205
}
206