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