Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TableRenderer
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 prepareColumns
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 prepareRows
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getSortIconClass
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildSortUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * TableRenderer.php
4 *
5 * This file is part of the Codex PHP library, which provides a PHP-based interface for creating
6 * UI components consistent with the Codex design system.
7 *
8 * The `TableRenderer` class leverages the `TemplateRenderer` and `Sanitizer` utilities to ensure the
9 * component object is rendered according to Codex design system standards.
10 *
11 * @category Renderer
12 * @package  Codex\Renderer
13 * @since    0.1.0
14 * @author   Doğu Abaris <abaris@null.net>
15 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
16 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
17 */
18
19namespace Wikimedia\Codex\Renderer;
20
21use InvalidArgumentException;
22use Wikimedia\Codex\Component\Table;
23use Wikimedia\Codex\Contract\Renderer\IRenderer;
24use Wikimedia\Codex\Contract\Renderer\ITemplateRenderer;
25use Wikimedia\Codex\Traits\AttributeResolver;
26use Wikimedia\Codex\Utility\Sanitizer;
27
28/**
29 * TableRenderer is responsible for rendering the HTML markup
30 * for a Table component using a Mustache template.
31 *
32 * This class uses the `TemplateRenderer` and `Sanitizer` utilities to manage
33 * the template rendering process, ensuring that the component object's HTML
34 * output adheres to the Codex design system's standards.
35 *
36 * @category Renderer
37 * @package  Codex\Renderer
38 * @since    0.1.0
39 * @author   Doğu Abaris <abaris@null.net>
40 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
41 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
42 */
43class TableRenderer implements IRenderer {
44
45    /**
46     * Use the AttributeResolver trait
47     */
48    use AttributeResolver;
49
50    /**
51     * The sanitizer instance used for content sanitization.
52     */
53    private Sanitizer $sanitizer;
54
55    /**
56     * The template renderer instance.
57     */
58    private ITemplateRenderer $templateRenderer;
59
60    /**
61     * Constructor to initialize the TableRenderer with a sanitizer and a template renderer.
62     *
63     * @since 0.1.0
64     * @param Sanitizer $sanitizer The sanitizer instance used for content sanitization.
65     * @param ITemplateRenderer $templateRenderer The template renderer instance used for rendering templates.
66     */
67    public function __construct( Sanitizer $sanitizer, ITemplateRenderer $templateRenderer ) {
68        $this->sanitizer = $sanitizer;
69        $this->templateRenderer = $templateRenderer;
70    }
71
72    /**
73     * Renders the HTML for an table component.
74     *
75     * Uses the provided Table component to generate HTML markup adhering to the Codex design system.
76     *
77     * @since 0.1.0
78     * @param Table $component The Table object to render.
79     * @return string The rendered HTML string for the component.
80     */
81    public function render( $component ): string {
82        if ( !$component instanceof Table ) {
83            throw new InvalidArgumentException( "Expected instance of Table, got " . get_class( $component ) );
84        }
85
86        $pager = $component->getPager();
87        $tableData = [
88            'id' => $this->sanitizer->sanitizeText( $component->getId() ),
89            'showVerticalBorders' => $component->getShowVerticalBorders(),
90            'caption' => $this->sanitizer->sanitizeText( $component->getCaption() ),
91            'columns' => $this->prepareColumns( $component ),
92            'rows' => $this->prepareRows( $component ),
93            'hideCaption' => $component->getHideCaption(),
94            'headerContent' => $this->sanitizer->sanitizeText( $component->getHeaderContent() ?? '' ),
95            'pager' => $pager ? $pager->getHtml() : '',
96            'attributes' => $this->resolve( $this->sanitizer->sanitizeAttributes( $component->getAttributes() ) ),
97            'footer' => $this->sanitizer->sanitizeText( $component->getFooter() ?? '' ),
98        ];
99
100        return $this->templateRenderer->render( 'table.mustache', $tableData );
101    }
102
103    /**
104     * Prepares the column data for rendering in the Mustache template.
105     *
106     * This method takes the columns defined in the Table component and processes them into an array
107     * format suitable for rendering in the table. It handles sorting options, alignment, and the correct
108     * icon for the sorting direction.
109     *
110     * @since 0.1.0
111     * @param Table $table The Table object containing column definitions.
112     * @return array The processed columns ready for rendering.
113     */
114    private function prepareColumns( Table $table ): array {
115        $columns = [];
116        foreach ( $table->getColumns() as $column ) {
117            $isCurrentSortColumn = $table->getCurrentSortColumn() === $column['id'];
118            $columns[] = [
119                'id' => $this->sanitizer->sanitizeText( $column['id'] ),
120                'label' => $this->sanitizer->sanitizeText( $column['label'] ),
121                'align' => isset( $column['align'] ) ? $this->sanitizer->sanitizeText( $column['align'] ) : '',
122                'sortable' => !empty( $column['sortable'] ),
123                'isCurrentSort' => $isCurrentSortColumn,
124                'sortUrl' => $this->buildSortUrl( $table, $column['id'] ),
125                'sortIconClass' => $this->getSortIconClass( $table, $isCurrentSortColumn ),
126            ];
127        }
128
129        return $columns;
130    }
131
132    /**
133     * Prepares the row data for rendering in the Mustache template.
134     *
135     * This method processes the data provided in the Table component and matches it with the defined columns.
136     * Each row is prepared as an array of columns with their respective cell data and alignment settings.
137     *
138     * @since 0.1.0
139     * @param Table $table The Table object containing row data.
140     * @return array The processed rows ready for rendering.
141     */
142    private function prepareRows( Table $table ): array {
143        $rows = [];
144        foreach ( $table->getData() as $row ) {
145            $rowData = [];
146            foreach ( $table->getColumns() as $column ) {
147                $cellData = isset( $row[$column['id']] ) ? $this->sanitizer->sanitizeText( $row[$column['id']] ) : '';
148                $align = isset( $column['align'] ) ? $this->sanitizer->sanitizeText( $column['align'] ) : '';
149                $rowData[] = [
150                    'cellData' => $cellData,
151                    'align' => $align,
152                ];
153            }
154            $rows[] = [ 'columns' => $rowData ];
155        }
156
157        return $rows;
158    }
159
160    /**
161     * Determines the appropriate CSS class for the sort icon based on the current sort state.
162     *
163     * If the column is the currently sorted column, it returns the correct ascending or descending sort icon class.
164     * Otherwise, it returns the unsorted icon class.
165     *
166     * @since 0.1.0
167     * @param Table $table The Table object.
168     * @param bool $isCurrentSortColumn Whether the column is currently sorted.
169     * @return string The CSS class for the sort icon.
170     */
171    private function getSortIconClass( Table $table, bool $isCurrentSortColumn ): string {
172        if ( $isCurrentSortColumn ) {
173            return $table->getCurrentSortDirection() === Table::SORT_ASCENDING ? 'cdx-table__table__sort-icon--asc'
174                : 'cdx-table__table__sort-icon--desc';
175        }
176
177        return 'cdx-table__table__sort-icon--unsorted';
178    }
179
180    /**
181     * Builds the URL for sorting the table by a specific column.
182     *
183     * This method constructs the sort URL by adjusting the query parameters to reflect the new sort column
184     * and direction (ascending or descending).
185     *
186     * @since 0.1.0
187     * @param Table $table The Table object.
188     * @param string $columnId The ID of the column to sort by.
189     * @return string The generated URL for sorting by the specified column.
190     */
191    private function buildSortUrl( Table $table, string $columnId ): string {
192        $queryParams = $table->getCallbacks()->getValues( 'sort', 'asc', 'desc', 'offset', 'limit' );
193        $queryParams['sort'] = $columnId;
194        $oppositeDirection = $table->oppositeSort( $table->getCurrentSortDirection() );
195        $queryParams['asc'] = ( $oppositeDirection === Table::SORT_ASCENDING ) ? '1' : '';
196        $queryParams['desc'] = ( $oppositeDirection === Table::SORT_DESCENDING ) ? '1' : '';
197
198        return '?' . http_build_query( $queryParams );
199    }
200}