PageRenderTime 27ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/league/commonmark/src/Extension/Table/TableParser.php

https://gitlab.com/jjpa2018/dashboard
PHP | 284 lines | 208 code | 39 blank | 37 comment | 25 complexity | 51124bccdb938e8dd986c70e3b60f000 MD5 | raw file
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This is part of the league/commonmark package.
  5. *
  6. * (c) Martin HasoĊˆ <martin.hason@gmail.com>
  7. * (c) Webuni s.r.o. <info@webuni.cz>
  8. * (c) Colin O'Dell <colinodell@gmail.com>
  9. *
  10. * For the full copyright and license information, please view the LICENSE
  11. * file that was distributed with this source code.
  12. */
  13. namespace League\CommonMark\Extension\Table;
  14. use League\CommonMark\Block\Element\Document;
  15. use League\CommonMark\Block\Element\Paragraph;
  16. use League\CommonMark\Block\Parser\BlockParserInterface;
  17. use League\CommonMark\Context;
  18. use League\CommonMark\ContextInterface;
  19. use League\CommonMark\Cursor;
  20. use League\CommonMark\EnvironmentAwareInterface;
  21. use League\CommonMark\EnvironmentInterface;
  22. final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
  23. {
  24. /**
  25. * @var EnvironmentInterface
  26. */
  27. private $environment;
  28. public function parse(ContextInterface $context, Cursor $cursor): bool
  29. {
  30. $container = $context->getContainer();
  31. if (!$container instanceof Paragraph) {
  32. return false;
  33. }
  34. $lines = $container->getStrings();
  35. if (count($lines) === 0) {
  36. return false;
  37. }
  38. $lastLine = \array_pop($lines);
  39. if (\strpos($lastLine, '|') === false) {
  40. return false;
  41. }
  42. $oldState = $cursor->saveState();
  43. $cursor->advanceToNextNonSpaceOrTab();
  44. $columns = $this->parseColumns($cursor);
  45. if (empty($columns)) {
  46. $cursor->restoreState($oldState);
  47. return false;
  48. }
  49. $head = $this->parseRow(trim((string) $lastLine), $columns, TableCell::TYPE_HEAD);
  50. if (null === $head) {
  51. $cursor->restoreState($oldState);
  52. return false;
  53. }
  54. $table = new Table(function (Cursor $cursor, Table $table) use ($columns): bool {
  55. // The next line cannot be a new block start
  56. // This is a bit inefficient, but it's the only feasible way to check
  57. // given the current v1 API.
  58. if (self::isANewBlock($this->environment, $cursor->getLine())) {
  59. return false;
  60. }
  61. $row = $this->parseRow(\trim($cursor->getLine()), $columns);
  62. if (null === $row) {
  63. return false;
  64. }
  65. $table->getBody()->appendChild($row);
  66. return true;
  67. });
  68. $table->getHead()->appendChild($head);
  69. if (count($lines) >= 1) {
  70. $paragraph = new Paragraph();
  71. foreach ($lines as $line) {
  72. $paragraph->addLine($line);
  73. }
  74. $context->replaceContainerBlock($paragraph);
  75. $context->addBlock($table);
  76. } else {
  77. $context->replaceContainerBlock($table);
  78. }
  79. return true;
  80. }
  81. /**
  82. * @param string $line
  83. * @param array<int, string> $columns
  84. * @param string $type
  85. *
  86. * @return TableRow|null
  87. */
  88. private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
  89. {
  90. $cells = $this->split(new Cursor(\trim($line)));
  91. if (empty($cells)) {
  92. return null;
  93. }
  94. // The header row must match the delimiter row in the number of cells
  95. if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
  96. return null;
  97. }
  98. $i = 0;
  99. $row = new TableRow();
  100. foreach ($cells as $i => $cell) {
  101. if (!array_key_exists($i, $columns)) {
  102. return $row;
  103. }
  104. $row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
  105. }
  106. for ($j = count($columns) - 1; $j > $i; --$j) {
  107. $row->appendChild(new TableCell('', $type, null));
  108. }
  109. return $row;
  110. }
  111. /**
  112. * @param Cursor $cursor
  113. *
  114. * @return array<int, string>
  115. */
  116. private function split(Cursor $cursor): array
  117. {
  118. if ($cursor->getCharacter() === '|') {
  119. $cursor->advanceBy(1);
  120. }
  121. $cells = [];
  122. $sb = '';
  123. while (!$cursor->isAtEnd()) {
  124. switch ($c = $cursor->getCharacter()) {
  125. case '\\':
  126. if ($cursor->peek() === '|') {
  127. // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
  128. // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
  129. // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
  130. $sb .= '|';
  131. $cursor->advanceBy(1);
  132. } else {
  133. // Preserve backslash before other characters or at end of line.
  134. $sb .= '\\';
  135. }
  136. break;
  137. case '|':
  138. $cells[] = $sb;
  139. $sb = '';
  140. break;
  141. default:
  142. $sb .= $c;
  143. }
  144. $cursor->advanceBy(1);
  145. }
  146. if ($sb !== '') {
  147. $cells[] = $sb;
  148. }
  149. return $cells;
  150. }
  151. /**
  152. * @param Cursor $cursor
  153. *
  154. * @return array<int, string>
  155. */
  156. private function parseColumns(Cursor $cursor): array
  157. {
  158. $columns = [];
  159. $pipes = 0;
  160. $valid = false;
  161. while (!$cursor->isAtEnd()) {
  162. switch ($c = $cursor->getCharacter()) {
  163. case '|':
  164. $cursor->advanceBy(1);
  165. $pipes++;
  166. if ($pipes > 1) {
  167. // More than one adjacent pipe not allowed
  168. return [];
  169. }
  170. // Need at least one pipe, even for a one-column table
  171. $valid = true;
  172. break;
  173. case '-':
  174. case ':':
  175. if ($pipes === 0 && !empty($columns)) {
  176. // Need a pipe after the first column (first column doesn't need to start with one)
  177. return [];
  178. }
  179. $left = false;
  180. $right = false;
  181. if ($c === ':') {
  182. $left = true;
  183. $cursor->advanceBy(1);
  184. }
  185. if ($cursor->match('/^-+/') === null) {
  186. // Need at least one dash
  187. return [];
  188. }
  189. if ($cursor->getCharacter() === ':') {
  190. $right = true;
  191. $cursor->advanceBy(1);
  192. }
  193. $columns[] = $this->getAlignment($left, $right);
  194. // Next, need another pipe
  195. $pipes = 0;
  196. break;
  197. case ' ':
  198. case "\t":
  199. // White space is allowed between pipes and columns
  200. $cursor->advanceToNextNonSpaceOrTab();
  201. break;
  202. default:
  203. // Any other character is invalid
  204. return [];
  205. }
  206. }
  207. if (!$valid) {
  208. return [];
  209. }
  210. return $columns;
  211. }
  212. private static function getAlignment(bool $left, bool $right): ?string
  213. {
  214. if ($left && $right) {
  215. return TableCell::ALIGN_CENTER;
  216. } elseif ($left) {
  217. return TableCell::ALIGN_LEFT;
  218. } elseif ($right) {
  219. return TableCell::ALIGN_RIGHT;
  220. }
  221. return null;
  222. }
  223. public function setEnvironment(EnvironmentInterface $environment)
  224. {
  225. $this->environment = $environment;
  226. }
  227. private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
  228. {
  229. $context = new Context(new Document(), $environment);
  230. $context->setNextLine($line);
  231. $cursor = new Cursor($line);
  232. /** @var BlockParserInterface $parser */
  233. foreach ($environment->getBlockParsers() as $parser) {
  234. if ($parser->parse($context, $cursor)) {
  235. return true;
  236. }
  237. }
  238. return false;
  239. }
  240. }