/vendor/league/commonmark/src/Extension/Table/TableParser.php
PHP | 284 lines | 208 code | 39 blank | 37 comment | 25 complexity | 51124bccdb938e8dd986c70e3b60f000 MD5 | raw file
- <?php
- declare(strict_types=1);
- /*
- * This is part of the league/commonmark package.
- *
- * (c) Martin HasoĊ <martin.hason@gmail.com>
- * (c) Webuni s.r.o. <info@webuni.cz>
- * (c) Colin O'Dell <colinodell@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace League\CommonMark\Extension\Table;
- use League\CommonMark\Block\Element\Document;
- use League\CommonMark\Block\Element\Paragraph;
- use League\CommonMark\Block\Parser\BlockParserInterface;
- use League\CommonMark\Context;
- use League\CommonMark\ContextInterface;
- use League\CommonMark\Cursor;
- use League\CommonMark\EnvironmentAwareInterface;
- use League\CommonMark\EnvironmentInterface;
- final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
- {
- /**
- * @var EnvironmentInterface
- */
- private $environment;
- public function parse(ContextInterface $context, Cursor $cursor): bool
- {
- $container = $context->getContainer();
- if (!$container instanceof Paragraph) {
- return false;
- }
- $lines = $container->getStrings();
- if (count($lines) === 0) {
- return false;
- }
- $lastLine = \array_pop($lines);
- if (\strpos($lastLine, '|') === false) {
- return false;
- }
- $oldState = $cursor->saveState();
- $cursor->advanceToNextNonSpaceOrTab();
- $columns = $this->parseColumns($cursor);
- if (empty($columns)) {
- $cursor->restoreState($oldState);
- return false;
- }
- $head = $this->parseRow(trim((string) $lastLine), $columns, TableCell::TYPE_HEAD);
- if (null === $head) {
- $cursor->restoreState($oldState);
- return false;
- }
- $table = new Table(function (Cursor $cursor, Table $table) use ($columns): bool {
- // The next line cannot be a new block start
- // This is a bit inefficient, but it's the only feasible way to check
- // given the current v1 API.
- if (self::isANewBlock($this->environment, $cursor->getLine())) {
- return false;
- }
- $row = $this->parseRow(\trim($cursor->getLine()), $columns);
- if (null === $row) {
- return false;
- }
- $table->getBody()->appendChild($row);
- return true;
- });
- $table->getHead()->appendChild($head);
- if (count($lines) >= 1) {
- $paragraph = new Paragraph();
- foreach ($lines as $line) {
- $paragraph->addLine($line);
- }
- $context->replaceContainerBlock($paragraph);
- $context->addBlock($table);
- } else {
- $context->replaceContainerBlock($table);
- }
- return true;
- }
- /**
- * @param string $line
- * @param array<int, string> $columns
- * @param string $type
- *
- * @return TableRow|null
- */
- private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
- {
- $cells = $this->split(new Cursor(\trim($line)));
- if (empty($cells)) {
- return null;
- }
- // The header row must match the delimiter row in the number of cells
- if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
- return null;
- }
- $i = 0;
- $row = new TableRow();
- foreach ($cells as $i => $cell) {
- if (!array_key_exists($i, $columns)) {
- return $row;
- }
- $row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
- }
- for ($j = count($columns) - 1; $j > $i; --$j) {
- $row->appendChild(new TableCell('', $type, null));
- }
- return $row;
- }
- /**
- * @param Cursor $cursor
- *
- * @return array<int, string>
- */
- private function split(Cursor $cursor): array
- {
- if ($cursor->getCharacter() === '|') {
- $cursor->advanceBy(1);
- }
- $cells = [];
- $sb = '';
- while (!$cursor->isAtEnd()) {
- switch ($c = $cursor->getCharacter()) {
- case '\\':
- if ($cursor->peek() === '|') {
- // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
- // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
- // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
- $sb .= '|';
- $cursor->advanceBy(1);
- } else {
- // Preserve backslash before other characters or at end of line.
- $sb .= '\\';
- }
- break;
- case '|':
- $cells[] = $sb;
- $sb = '';
- break;
- default:
- $sb .= $c;
- }
- $cursor->advanceBy(1);
- }
- if ($sb !== '') {
- $cells[] = $sb;
- }
- return $cells;
- }
- /**
- * @param Cursor $cursor
- *
- * @return array<int, string>
- */
- private function parseColumns(Cursor $cursor): array
- {
- $columns = [];
- $pipes = 0;
- $valid = false;
- while (!$cursor->isAtEnd()) {
- switch ($c = $cursor->getCharacter()) {
- case '|':
- $cursor->advanceBy(1);
- $pipes++;
- if ($pipes > 1) {
- // More than one adjacent pipe not allowed
- return [];
- }
- // Need at least one pipe, even for a one-column table
- $valid = true;
- break;
- case '-':
- case ':':
- if ($pipes === 0 && !empty($columns)) {
- // Need a pipe after the first column (first column doesn't need to start with one)
- return [];
- }
- $left = false;
- $right = false;
- if ($c === ':') {
- $left = true;
- $cursor->advanceBy(1);
- }
- if ($cursor->match('/^-+/') === null) {
- // Need at least one dash
- return [];
- }
- if ($cursor->getCharacter() === ':') {
- $right = true;
- $cursor->advanceBy(1);
- }
- $columns[] = $this->getAlignment($left, $right);
- // Next, need another pipe
- $pipes = 0;
- break;
- case ' ':
- case "\t":
- // White space is allowed between pipes and columns
- $cursor->advanceToNextNonSpaceOrTab();
- break;
- default:
- // Any other character is invalid
- return [];
- }
- }
- if (!$valid) {
- return [];
- }
- return $columns;
- }
- private static function getAlignment(bool $left, bool $right): ?string
- {
- if ($left && $right) {
- return TableCell::ALIGN_CENTER;
- } elseif ($left) {
- return TableCell::ALIGN_LEFT;
- } elseif ($right) {
- return TableCell::ALIGN_RIGHT;
- }
- return null;
- }
- public function setEnvironment(EnvironmentInterface $environment)
- {
- $this->environment = $environment;
- }
- private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
- {
- $context = new Context(new Document(), $environment);
- $context->setNextLine($line);
- $cursor = new Cursor($line);
- /** @var BlockParserInterface $parser */
- foreach ($environment->getBlockParsers() as $parser) {
- if ($parser->parse($context, $cursor)) {
- return true;
- }
- }
- return false;
- }
- }