TableParser   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 21
eloc 70
c 2
b 0
f 0
dl 0
loc 178
ccs 72
cts 72
cp 1
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
B split() 0 42 7
A tryContinue() 0 7 2
A getBlock() 0 3 1
A __construct() 0 6 1
A addLine() 0 6 2
A parseCell() 0 11 2
A canHaveLazyContinuationLines() 0 3 1
A parseInlines() 0 35 5
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\Block\BlockContinueParserWithInlinesInterface;
23
use League\CommonMark\Parser\Cursor;
24
use League\CommonMark\Parser\InlineParserEngineInterface;
25
use League\CommonMark\Util\ArrayCollection;
26
27
final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
28
{
29
    /**
30
     * @var Table
31
     *
32
     * @psalm-readonly
33
     */
34
    private $block;
35
36
    /**
37
     * @var ArrayCollection<string>
38
     *
39
     * @psalm-readonly-allow-private-mutation
40
     */
41
    private $bodyLines;
42
43
    /**
44
     * @var array<int, string|null>
45
     *
46
     * @psalm-readonly
47
     */
48
    private $columns;
49
50
    /**
51
     * @var array<int, string>
52
     *
53
     * @psalm-readonly-allow-private-mutation
54
     */
55
    private $headerCells;
56
57
    /**
58
     * @var bool
59
     *
60
     * @psalm-readonly-allow-private-mutation
61
     */
62
    private $nextIsSeparatorLine = true;
63
64
    /**
65
     * @param array<int, string|null> $columns
66
     * @param array<int, string>      $headerCells
67
     */
68 78
    public function __construct(array $columns, array $headerCells)
69
    {
70 78
        $this->block       = new Table();
71 78
        $this->bodyLines   = new ArrayCollection();
72 78
        $this->columns     = $columns;
73 78
        $this->headerCells = $headerCells;
74 78
    }
75
76 75
    public function canHaveLazyContinuationLines(): bool
77
    {
78 75
        return true;
79
    }
80
81
    /**
82
     * @return Table
83
     */
84 78
    public function getBlock(): AbstractBlock
85
    {
86 78
        return $this->block;
87
    }
88
89 75
    public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
90
    {
91 75
        if (\strpos($cursor->getLine(), '|') === false) {
92 24
            return BlockContinue::none();
93
        }
94
95 75
        return BlockContinue::at($cursor);
96
    }
97
98 78
    public function addLine(string $line): void
99
    {
100 78
        if ($this->nextIsSeparatorLine) {
101 78
            $this->nextIsSeparatorLine = false;
102
        } else {
103 75
            $this->bodyLines[] = $line;
104
        }
105 78
    }
106
107 78
    public function parseInlines(InlineParserEngineInterface $inlineParser): void
108
    {
109 78
        $headerColumns = \count($this->headerCells);
110
111 78
        $head = new TableSection(TableSection::TYPE_HEAD);
112 78
        $this->block->appendChild($head);
113
114 78
        $headerRow = new TableRow();
115 78
        $head->appendChild($headerRow);
116 78
        for ($i = 0; $i < $headerColumns; $i++) {
117 78
            $cell      = $this->headerCells[$i];
118 78
            $tableCell = $this->parseCell($cell, $i, $inlineParser);
119 78
            $tableCell->setType(TableCell::TYPE_HEAD);
120 78
            $headerRow->appendChild($tableCell);
121
        }
122
123 78
        $body = null;
124 78
        foreach ($this->bodyLines as $rowLine) {
125 75
            $cells = self::split($rowLine);
126 75
            $row   = new TableRow();
127
128
            // Body can not have more columns than head
129 75
            for ($i = 0; $i < $headerColumns; $i++) {
130 75
                $cell      = $cells[$i] ?? '';
131 75
                $tableCell = $this->parseCell($cell, $i, $inlineParser);
132 75
                $row->appendChild($tableCell);
133
            }
134
135 75
            if ($body === null) {
136
                // It's valid to have a table without body. In that case, don't add an empty TableBody node.
137 75
                $body = new TableSection();
138 75
                $this->block->appendChild($body);
139
            }
140
141 75
            $body->appendChild($row);
142
        }
143 78
    }
144
145 78
    private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
146
    {
147 78
        $tableCell = new TableCell();
148
149 78
        if ($column < \count($this->columns)) {
150 78
            $tableCell->setAlign($this->columns[$column]);
151
        }
152
153 78
        $inlineParser->parse(\trim($cell), $tableCell);
154
155 78
        return $tableCell;
156
    }
157
158
    /**
159
     * @internal
160
     *
161
     * @return array<int, string>
162
     */
163 81
    public static function split(string $line): array
164
    {
165 81
        $cursor = new Cursor(\trim($line));
166
167 81
        if ($cursor->getCharacter() === '|') {
168 60
            $cursor->advanceBy(1);
169
        }
170
171 81
        $cells = [];
172 81
        $sb    = '';
173
174 81
        while (! $cursor->isAtEnd()) {
175 81
            switch ($c = $cursor->getCharacter()) {
176 81
                case '\\':
177 12
                    if ($cursor->peek() === '|') {
178
                        // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
179
                        // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
180
                        // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
181 12
                        $sb .= '|';
182 12
                        $cursor->advanceBy(1);
183
                    } else {
184
                        // Preserve backslash before other characters or at end of line.
185 3
                        $sb .= '\\';
186
                    }
187
188 12
                    break;
189 81
                case '|':
190 81
                    $cells[] = $sb;
191 81
                    $sb      = '';
192 81
                    break;
193
                default:
194 81
                    $sb .= $c;
195
            }
196
197 81
            $cursor->advanceBy(1);
198
        }
199
200 81
        if ($sb !== '') {
201 36
            $cells[] = $sb;
202
        }
203
204 81
        return $cells;
205
    }
206
}
207