Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
90.62% covered (success)
90.62%
29 / 32
4.62% covered (danger)
4.62%
6 / 130
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Table
100.00% covered (success)
100.00%
22 / 22
90.62% covered (success)
90.62%
29 / 32
4.62% covered (danger)
4.62%
6 / 130
100.00% covered (success)
100.00%
4 / 4
116.01
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 row
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rule
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 render
100.00% covered (success)
100.00%
17 / 17
88.89% covered (warning)
88.89%
24 / 27
1.59% covered (danger)
1.59%
2 / 126
100.00% covered (success)
100.00%
1 / 1
53.70
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use ValueError;
8
9/**
10 * Renders rows of columns aligned on their visible width — markup
11 * tags and multibyte characters don't count, so cells may carry
12 * markup: `<green>1,075</green>`.
13 *
14 * Columns size to their widest cell and are separated by two spaces;
15 * `rule()` inserts a separator line spanning the table. Cells don't
16 * wrap: a table wider than the terminal simply overflows. Render the
17 * result through the Io echo methods:
18 *
19 *     $table = new Table(align: [Align::Left, Align::Right]);
20 *     $table->row(['<strong>Language</strong>', '<strong>Lines</strong>']);
21 *     $table->rule();
22 *     $table->row(['PHP', '1,075']);
23 *     $io->echo($table->render());
24 *
25 * @api
26 */
27final class Table
28{
29    private readonly Markup $markup;
30
31    /** @var list<list<string>|string> Cell rows, or a rule's char. */
32    private array $rows = [];
33
34    /** @param list<Align> $align Per column; further columns align left. */
35    public function __construct(
36        private readonly array $align = [],
37    ) {
38        $this->markup = new Markup();
39    }
40
41    /** @param list<string> $cells A short row leaves the rest empty. */
42    public function row(array $cells): void
43    {
44        $this->rows[] = $cells;
45    }
46
47    public function rule(string $char = '─'): void
48    {
49        if ($this->markup->width($char) < 1) {
50            throw new ValueError("Rule char '{$char}' has no visible width");
51        }
52
53        $this->rows[] = $char;
54    }
55
56    /**
57     * The rendered table, lines separated and terminated by `\n`.
58     */
59    public function render(): string
60    {
61        $widths = [];
62
63        foreach ($this->rows as $row) {
64            if (is_string($row)) {
65                continue;
66            }
67
68            foreach ($row as $i => $cell) {
69                $widths[$i] = max($widths[$i] ?? 0, $this->markup->width($cell));
70            }
71        }
72
73        $total = (int) array_sum($widths) + (2 * max(0, count($widths) - 1));
74        $out = '';
75
76        foreach ($this->rows as $row) {
77            if (is_string($row)) {
78                $out .= str_repeat($row, intdiv($total, $this->markup->width($row)));
79            } else {
80                $cells = [];
81
82                foreach ($widths as $i => $width) {
83                    $cells[] = $this->markup->pad($row[$i] ?? '', $width, $this->align[$i] ?? Align::Left);
84                }
85
86                $out .= rtrim(implode('  ', $cells));
87            }
88
89            $out .= "\n";
90        }
91
92        return $out;
93    }
94}