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