1<?php 2 3declare(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 16namespace League\CommonMark\Extension\Table; 17 18use League\CommonMark\Parser\Block\BlockStart; 19use League\CommonMark\Parser\Block\BlockStartParserInterface; 20use League\CommonMark\Parser\Block\ParagraphParser; 21use League\CommonMark\Parser\Cursor; 22use League\CommonMark\Parser\MarkdownParserStateInterface; 23 24final class TableStartParser implements BlockStartParserInterface 25{ 26 public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart 27 { 28 $paragraph = $parserState->getParagraphContent(); 29 if ($paragraph === null || \strpos($paragraph, '|') === false) { 30 return BlockStart::none(); 31 } 32 33 $columns = self::parseSeparator($cursor); 34 if (\count($columns) === 0) { 35 return BlockStart::none(); 36 } 37 38 $lines = \explode("\n", $paragraph); 39 $lastLine = \array_pop($lines); 40 41 $headerCells = TableParser::split($lastLine); 42 if (\count($headerCells) > \count($columns)) { 43 return BlockStart::none(); 44 } 45 46 $cursor->advanceToEnd(); 47 48 $parsers = []; 49 50 if (\count($lines) > 0) { 51 $p = new ParagraphParser(); 52 $p->addLine(\implode("\n", $lines)); 53 $parsers[] = $p; 54 } 55 56 $parsers[] = new TableParser($columns, $headerCells); 57 58 return BlockStart::of(...$parsers) 59 ->at($cursor) 60 ->replaceActiveBlockParser(); 61 } 62 63 /** 64 * @return array<int, string|null> 65 * 66 * @psalm-return array<int, TableCell::ALIGN_*|null> 67 * 68 * @phpstan-return array<int, TableCell::ALIGN_*|null> 69 */ 70 private static function parseSeparator(Cursor $cursor): array 71 { 72 $columns = []; 73 $pipes = 0; 74 $valid = false; 75 76 while (! $cursor->isAtEnd()) { 77 switch ($c = $cursor->getCurrentCharacter()) { 78 case '|': 79 $cursor->advanceBy(1); 80 $pipes++; 81 if ($pipes > 1) { 82 // More than one adjacent pipe not allowed 83 return []; 84 } 85 86 // Need at least one pipe, even for a one-column table 87 $valid = true; 88 break; 89 case '-': 90 case ':': 91 if ($pipes === 0 && \count($columns) > 0) { 92 // Need a pipe after the first column (first column doesn't need to start with one) 93 return []; 94 } 95 96 $left = false; 97 $right = false; 98 if ($c === ':') { 99 $left = true; 100 $cursor->advanceBy(1); 101 } 102 103 if ($cursor->match('/^-+/') === null) { 104 // Need at least one dash 105 return []; 106 } 107 108 if ($cursor->getCurrentCharacter() === ':') { 109 $right = true; 110 $cursor->advanceBy(1); 111 } 112 113 $columns[] = self::getAlignment($left, $right); 114 // Next, need another pipe 115 $pipes = 0; 116 break; 117 case ' ': 118 case "\t": 119 // White space is allowed between pipes and columns 120 $cursor->advanceToNextNonSpaceOrTab(); 121 break; 122 default: 123 // Any other character is invalid 124 return []; 125 } 126 } 127 128 if (! $valid) { 129 return []; 130 } 131 132 return $columns; 133 } 134 135 /** 136 * @psalm-return TableCell::ALIGN_*|null 137 * 138 * @phpstan-return TableCell::ALIGN_*|null 139 * 140 * @psalm-pure 141 */ 142 private static function getAlignment(bool $left, bool $right): ?string 143 { 144 if ($left && $right) { 145 return TableCell::ALIGN_CENTER; 146 } 147 148 if ($left) { 149 return TableCell::ALIGN_LEFT; 150 } 151 152 if ($right) { 153 return TableCell::ALIGN_RIGHT; 154 } 155 156 return null; 157 } 158} 159