PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
← All changes | classes/class-render.php +236 -186 2.0.4 → 3.4 View file →
@@ -7,8 +7,10 @@
7 7 * @author Tobias Bäthge
8 8 * @since 1.0.0
9 9 */
10 10
11 +declare(strict_types=1);
12 +
11 13 // Prohibit direct script loading.
12 14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 15
14 16 /**
@@ -24,25 +26,25 @@
24 26 /**
25 27 * Table data that is rendered.
26 28 *
27 29 * @since 1.0.0
28 - * @var array
30 + * @var array<string, mixed>
29 31 */
30 - protected $table = array();
32 + protected array $table = array();
31 33
32 34 /**
33 35 * Table options that influence the output result.
34 36 *
35 37 * @since 1.0.0
36 - * @var array
38 + * @var array<string, mixed>
37 39 */
38 - protected $render_options = array();
40 + protected array $render_options = array();
39 41
40 42 /**
41 43 * Rendered HTML code of the table or PHP array.
42 44 *
43 45 * @since 1.0.0
44 - * @var string|array
46 + * @var string|array<int, array<int, string>>
45 47 */
46 48 protected $output;
47 49
48 50 /**
@@ -48,11 +50,11 @@
48 50 /**
49 51 * Trigger words for colspan, rowspan, or the combination of both.
50 52 *
51 53 * @since 1.0.0
52 - * @var array
54 + * @var array<string, string>
53 55 */
54 - protected $span_trigger = array(
56 + protected array $span_trigger = array(
55 57 'colspan' => '#colspan#',
56 58 'rowspan' => '#rowspan#',
57 59 'span' => '#span#',
58 60 );
@@ -60,35 +62,40 @@
60 62 /**
61 63 * Buffer to store the counts of rowspan per column, initialized in _render_table().
62 64 *
63 65 * @since 1.0.0
64 - * @var array
66 + * @var int[]
65 67 */
66 - protected $rowspan = array();
68 + protected array $rowspan = array();
67 69
68 70 /**
69 71 * Buffer to store the counts of colspan per row, initialized in _render_table().
70 72 *
71 73 * @since 1.0.0
72 - * @var array
74 + * @var int[]
73 75 */
74 - protected $colspan = array();
76 + protected array $colspan = array();
75 77
76 78 /**
79 + * Whether the table has connected cells (colspan or rowspan), set in _render_table().
80 + *
81 + * @since 3.0.0
82 + */
83 + protected bool $tbody_has_connected_cells = false;
84 +
85 + /**
77 86 * Index of the last row of the visible data in the table, set in _render_table().
78 87 *
79 88 * @since 1.0.0
80 - * @var int
81 89 */
82 - protected $last_row_idx;
90 + protected int $last_row_idx;
83 91
84 92 /**
85 93 * Index of the last column of the visible data in the table, set in _render_table().
86 94 *
87 95 * @since 1.0.0
88 - * @var int
89 96 */
90 - protected $last_column_idx;
97 + protected int $last_column_idx;
91 98
92 99 /**
93 100 * Class constructor.
94 101 *
@@ -102,12 +109,12 @@
102 109 * Set the table (data, options, visibility, ...) that is to be rendered.
103 110 *
104 111 * @since 1.0.0
105 112 *
106 - * @param array $table Table to be rendered.
107 - * @param array $render_options Options for rendering, from both "Edit" screen and Shortcode.
113 + * @param array<string, mixed> $table Table to be rendered.
114 + * @param array<string, mixed> $render_options Options for rendering, from both "Edit" screen and Shortcode.
108 115 */
109 - public function set_input( array $table, array $render_options ) {
116 + public function set_input( array $table, array $render_options ): void {
110 117 $this->table = $table;
111 118 $this->render_options = $render_options;
112 119 /**
113 120 * Filters the table before the render process.
@@ -113,10 +120,10 @@
113 120 * Filters the table before the render process.
114 121 *
115 122 * @since 1.0.0
116 123 *
117 - * @param array $table The table.
118 - * @param array $render_options The render options for the table.
124 + * @param array<string, mixed> $table The table.
125 + * @param array<string, mixed> $render_options The render options for the table.
119 126 */
120 127 $this->table = apply_filters( 'tablepress_table_raw_render_data', $this->table, $this->render_options );
121 128 }
122 129
@@ -126,11 +133,11 @@
126 133 * @since 1.0.0
127 134 * @since 2.0.0 Add the $format parameter.
128 135 *
129 136 * @param string $format Optional. Output format, 'html' (default) or 'array'.
130 - * @return string|array[] HTML code of the rendered table, or a PHP array, or an error message.
137 + * @return string|array<int, array<int, string>> HTML code of the rendered table, or a PHP array, or an error message.
131 138 */
132 - public function get_output( $format = 'html' ) {
139 + public function get_output( string $format = 'html' ) /* : string|array */ {
133 140 // Evaluate math expressions/formulas.
134 141 $this->_evaluate_table_data();
135 142 // Remove hidden rows and columns.
136 143 $this->_prepare_render_data();
@@ -162,9 +169,9 @@
162 169 * Loop through the table to evaluate math expressions/formulas.
163 170 *
164 171 * @since 1.0.0
165 172 */
166 - protected function _evaluate_table_data() {
173 + protected function _evaluate_table_data(): void {
167 174 $orig_table = $this->table;
168 175
169 176 if ( $this->render_options['evaluate_formulas'] ) {
170 177 $formula_evaluator = TablePress::load_class( 'TablePress_Evaluate', 'class-evaluate.php', 'classes' );
@@ -175,11 +182,11 @@
175 182 * Filters the table after evaluating formulas in the table.
176 183 *
177 184 * @since 1.0.0
178 185 *
179 - * @param array $table The table with evaluated formulas.
180 - * @param array $orig_table The table with unevaluated formulas.
181 - * @param array $render_options The render options for the table.
186 + * @param array<string, mixed> $table The table with evaluated formulas.
187 + * @param array<string, mixed> $orig_table The table with unevaluated formulas.
188 + * @param array<string, mixed> $render_options The render options for the table.
182 189 */
183 190 $this->table = apply_filters( 'tablepress_table_evaluate_data', $this->table, $orig_table, $this->render_options );
184 191 }
185 192
@@ -187,13 +194,15 @@
187 194 * Remove all cells from the data set that shall not be rendered, because they are hidden.
188 195 *
189 196 * @since 1.0.0
190 197 */
191 - protected function _prepare_render_data() {
198 + protected function _prepare_render_data(): void {
192 199 $orig_table = $this->table;
193 200
194 - $num_rows = count( $this->table['data'] );
195 - $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
201 + $counts = array(
202 + 'rows' => count( $this->table['data'] ),
203 + );
204 + $counts['columns'] = ( $counts['rows'] > 0 ) ? count( $this->table['data'][0] ) : 0;
196 205
197 206 // Evaluate show/hide_rows/columns parameters.
198 207 $actions = array( 'show', 'hide' );
199 208 $elements = array( 'rows', 'columns' );
@@ -198,56 +207,23 @@
198 207 $actions = array( 'show', 'hide' );
199 208 $elements = array( 'rows', 'columns' );
200 209 foreach ( $actions as $action ) {
201 210 foreach ( $elements as $element ) {
202 - if ( empty( $this->render_options[ "{$action}_{$element}" ] ) ) {
203 - $this->render_options[ "{$action}_{$element}" ] = array();
211 + $action_element = "{$action}_{$element}";
212 +
213 + if ( empty( $this->render_options[ $action_element ] ) ) {
214 + $this->render_options[ $action_element ] = array();
204 215 continue;
205 216 }
206 217
207 - // Add all rows/columns to array if "all" value set for one of the four parameters.
208 - if ( 'all' === $this->render_options[ "{$action}_{$element}" ] ) {
209 - $this->render_options[ "{$action}_{$element}" ] = range( 0, ${'num_' . $element} - 1 );
218 + // Add all rows/columns to array if a parameter is set to "all".
219 + if ( 'all' === $this->render_options[ $action_element ] ) {
220 + $this->render_options[ $action_element ] = range( 0, $counts[ $element ] - 1 );
210 221 continue;
211 222 }
212 223
213 - // We have a list of rows/columns (possibly with ranges in it).
214 - $this->render_options[ "{$action}_{$element}" ] = explode( ',', $this->render_options[ "{$action}_{$element}" ] );
215 - // Support for ranges like 3-6 or A-BA.
216 - $range_cells = array();
217 - foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
218 - $range_dash = strpos( $value, '-' );
219 - if ( false !== $range_dash ) {
220 - unset( $this->render_options[ "{$action}_{$element}" ][ $key ] );
221 - $start = trim( substr( $value, 0, $range_dash ) );
222 - if ( ! is_numeric( $start ) ) {
223 - $start = TablePress::letter_to_number( $start );
224 - }
225 - $end = trim( substr( $value, $range_dash + 1 ) );
226 - if ( ! is_numeric( $end ) ) {
227 - $end = TablePress::letter_to_number( $end );
228 - }
229 - $current_range = range( $start, $end );
230 - $range_cells = array_merge( $range_cells, $current_range );
231 - }
232 - }
233 - $this->render_options[ "{$action}_{$element}" ] = array_merge( $this->render_options[ "{$action}_{$element}" ], $range_cells );
234 -
235 - /*
236 - * Parse single letters and change from regular numbering to zero-based numbering,
237 - * as rows/columns are indexed from 0 internally, but from 1 externally.
238 - */
239 - foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
240 - $value = trim( $value );
241 - if ( ! is_numeric( $value ) ) {
242 - $value = TablePress::letter_to_number( $value );
243 - }
244 - $this->render_options[ "{$action}_{$element}" ][ $key ] = (int) $value - 1;
245 - }
246 -
247 - // Remove duplicate entries and sort the array.
248 - $this->render_options[ "{$action}_{$element}" ] = array_unique( $this->render_options[ "{$action}_{$element}" ] );
249 - sort( $this->render_options[ "{$action}_{$element}" ], SORT_NUMERIC );
224 + // Convert the list to a unique, sorted, and zero-based array of integers.
225 + $this->render_options[ $action_element ] = TablePress::convert_row_column_list_to_array( $this->render_options[ $action_element ], 1, $counts[ $element ], array( 'zero-based' => true ) );
250 226 }
251 227 }
252 228
253 229 // Load information about hidden rows and columns.
@@ -277,11 +253,11 @@
277 253 * Filters the table after processing the table visibility information.
278 254 *
279 255 * @since 1.0.0
280 256 *
281 - * @param array $table The processed table.
282 - * @param array $orig_table The unprocessed table.
283 - * @param array $render_options The render options for the table.
257 + * @param array<string, mixed> $table The processed table.
258 + * @param array<string, mixed> $orig_table The unprocessed table.
259 + * @param array<string, mixed> $render_options The render options for the table.
284 260 */
285 261 $this->table = apply_filters( 'tablepress_table_render_data', $this->table, $orig_table, $this->render_options );
286 262 }
287 263
@@ -289,9 +265,9 @@
289 265 * Generate the data that is to be rendered.
290 266 *
291 267 * @since 2.0.0
292 268 */
293 - protected function _process_render_data() {
269 + protected function _process_render_data(): void {
294 270 $orig_table = $this->table;
295 271
296 272 // Deactivate nl2br() for this render process, if "convert_line_breaks" Shortcode parameter is set to false.
297 273 if ( ! $this->render_options['convert_line_breaks'] ) {
@@ -300,16 +276,25 @@
300 276
301 277 foreach ( $this->table['data'] as $row_idx => $row ) {
302 278 foreach ( $row as $col_idx => $cell_content ) {
303 279 // Print formulas that are escaped with '= (like in Excel) as text.
304 - if ( "'=" === substr( $cell_content, 0, 2 ) ) {
280 + if ( str_starts_with( $cell_content, "'=" ) ) {
305 281 $cell_content = substr( $cell_content, 1 );
306 282 }
307 283 $cell_content = $this->safe_output( $cell_content );
308 - if ( false !== strpos( $cell_content, '[' ) ) {
284 + if ( str_contains( $cell_content, '[' ) ) {
309 285 $cell_content = do_shortcode( $cell_content );
310 286 }
311 - /** This filter is documented in classes/class-render.php */
287 + /**
288 + * Filters the content of a single cell, after formulas have been evaluated, the output has been sanitized, and Shortcodes have been evaluated.
289 + *
290 + * @since 1.0.0
291 + *
292 + * @param string $cell_content The cell content.
293 + * @param string $table_id The current table ID.
294 + * @param int $row_idx The row number of the cell.
295 + * @param int $col_idx The column number of the cell.
296 + */
312 297 $cell_content = apply_filters( 'tablepress_cell_content', $cell_content, $this->table['id'], $row_idx + 1, $col_idx + 1 );
313 298 $this->table['data'][ $row_idx ][ $col_idx ] = $cell_content;
314 299 }
315 300 }
@@ -323,11 +308,11 @@
323 308 * Filters the table after processing the table content handling.
324 309 *
325 310 * @since 2.0.0
326 311 *
327 - * @param array $table The processed table.
328 - * @param array $orig_table The unprocessed table.
329 - * @param array $render_options The render options for the table.
312 + * @param array<string, mixed> $table The processed table.
313 + * @param array<string, mixed> $orig_table The unprocessed table.
314 + * @param array<string, mixed> $render_options The render options for the table.
330 315 */
331 316 $this->table = apply_filters( 'tablepress_table_content_render_data', $this->table, $orig_table, $this->render_options );
332 317 }
333 318
@@ -335,14 +320,15 @@
335 320 * Generate the HTML output of the table.
336 321 *
337 322 * @since 1.0.0
338 323 */
339 - protected function _render_table() {
324 + protected function _render_table(): void {
340 325 $num_rows = count( $this->table['data'] );
341 326 $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
342 327
343 328 // Check if there are rows and columns in the table (might not be the case after removing hidden rows/columns!).
344 329 if ( 0 === $num_rows || 0 === $num_columns ) {
330 + /* translators: %s: Table ID */
345 331 $this->output = sprintf( __( '<!-- The table with the ID %s is empty! -->', 'tablepress' ), $this->table['id'] );
346 332 return;
347 333 }
348 334
@@ -354,10 +340,10 @@
354 340 * Filters the trigger keywords for "colspan" and "rowspan"
355 341 *
356 342 * @since 1.0.0
357 343 *
358 - * @param array $span_trigger The trigger keywords for combining table cells.
359 - * @param string $table_id The current table ID.
344 + * @param array<string, string> $span_trigger The trigger keywords for combining table cells.
345 + * @param string $table_id The current table ID.
360 346 */
361 347 $this->span_trigger = apply_filters( 'tablepress_span_trigger_keywords', $this->span_trigger, $this->table['id'] );
362 348
363 349 // Explode from string to array.
@@ -396,11 +382,11 @@
396 382 * Filters the attributes for the table name (HTML h2 element, by default).
397 383 *
398 384 * @since 1.13.0
399 385 *
400 - * @param array $name_attributes The attributes for the table name element.
401 - * @param array $table The current table.
402 - * @param array $render_options The render options for the table.
386 + * @param array<string, string> $name_attributes The attributes for the table name element.
387 + * @param array<string, mixed> $table The current table.
388 + * @param array<string, mixed> $render_options The render options for the table.
403 389 */
404 390 $name_attributes = apply_filters( 'tablepress_table_name_tag_attributes', $name_attributes, $this->table, $this->render_options );
405 391 $name_attributes = $this->_attributes_array_to_string( $name_attributes );
406 392
@@ -435,11 +421,11 @@
435 421 * Filters the attributes for the table description (HTML span element, by default).
436 422 *
437 423 * @since 1.13.0
438 424 *
439 - * @param array $description_attributes The attributes for the table description element.
440 - * @param array $table The current table.
441 - * @param array $render_options The render options for the table.
425 + * @param array<string, string> $description_attributes The attributes for the table description element.
426 + * @param array<string, mixed> $table The current table.
427 + * @param array<string, mixed> $render_options The render options for the table.
442 428 */
443 429 $description_attributes = apply_filters( 'tablepress_table_description_tag_attributes', $description_attributes, $this->table, $this->render_options );
444 430 $description_attributes = $this->_attributes_array_to_string( $description_attributes );
445 431
@@ -452,30 +438,46 @@
452 438 if ( $this->render_options['print_description'] && 'above' === $this->render_options['print_description_position'] ) {
453 439 $output .= $print_description_html;
454 440 }
455 441
456 - $thead = '';
457 - $tfoot = '';
442 + $thead = array();
443 + $tfoot = array();
458 444 $tbody = array();
459 445
460 446 $this->last_row_idx = $num_rows - 1;
461 447 $this->last_column_idx = $num_columns - 1;
448 +
462 449 // Loop through rows in reversed order, to search for rowspan trigger keyword.
463 - for ( $row_idx = $this->last_row_idx; $row_idx >= 0; $row_idx-- ) {
464 - // Last row, need to check for footer (but only if at least two rows).
465 - if ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] && $num_rows > 1 ) {
466 - $tfoot = $this->_render_row( $row_idx, 'th' );
467 - continue;
450 + $row_idx = $this->last_row_idx;
451 +
452 + // Render the table footer rows, if there is at least one extra row.
453 + if ( $this->render_options['table_foot'] > 0 && $num_rows >= $this->render_options['table_head'] + $this->render_options['table_foot'] ) {
454 + $last_tbody_idx = $this->last_row_idx - $this->render_options['table_foot'];
455 + while ( $row_idx > $last_tbody_idx ) {
456 + $tfoot[] = $this->_render_row( $row_idx, 'th' );
457 + --$row_idx;
468 458 }
469 - // First row, need to check for head (but only if at least two rows).
470 - if ( 0 === $row_idx && $this->render_options['table_head'] && $num_rows > 1 ) {
471 - $thead = $this->_render_row( $row_idx, 'th' );
472 - continue;
473 - }
474 - // Neither first nor last row (with respective head/foot enabled), so render as body row.
459 + // Reverse rows because we looped through the rows in reverse order.
460 + $tfoot = array_reverse( $tfoot );
461 + }
462 +
463 + // Render the table body rows.
464 + $last_thead_idx = $this->render_options['table_head'] - 1;
465 + while ( $row_idx > $last_thead_idx ) {
475 466 $tbody[] = $this->_render_row( $row_idx, 'td' );
467 + --$row_idx;
476 468 }
469 + // Reverse rows because we looped through the rows in reverse order.
470 + $tbody = array_reverse( $tbody );
477 471
472 + // Render the table header rows, if rows are left.
473 + while ( $row_idx > -1 ) {
474 + $thead[] = $this->_render_row( $row_idx, 'th' );
475 + --$row_idx;
476 + }
477 + // Reverse rows because we looped through the rows in reverse order.
478 + $thead = array_reverse( $thead );
479 +
478 480 // <caption> tag.
479 481 /**
480 482 * Filters the content for the HTML caption element of the table.
481 483 *
@@ -482,10 +484,10 @@
482 484 * If the "Edit" link for a table is shown, it is also added to the caption element.
483 485 *
484 486 * @since 1.0.0
485 487 *
486 - * @param string $caption The content for the HTML caption element of the table. Default empty.
487 - * @param array $table The current table.
488 + * @param string $caption The content for the HTML caption element of the table. Default empty.
489 + * @param array<string, mixed> $table The current table.
488 490 */
489 491 $caption = apply_filters( 'tablepress_print_caption_text', '', $this->table );
490 492 $caption_style = '';
491 493 $caption_class = '';
@@ -535,9 +537,9 @@
535 537 * @param string $table_id The current table ID.
536 538 * @param int $col_idx The number of the column.
537 539 */
538 540 $attributes = apply_filters( 'tablepress_colgroup_tag_attributes', $attributes, $this->table['id'], $col_idx + 1 );
539 - $colgroup .= "\t<col{$attributes}/>\n";
541 + $colgroup .= "\t<col{$attributes} />\n";
540 542 }
541 543 }
542 544 if ( ! empty( $colgroup ) ) {
543 545 $colgroup = "<colgroup>\n{$colgroup}</colgroup>\n";
@@ -542,18 +544,36 @@
542 544 if ( ! empty( $colgroup ) ) {
543 545 $colgroup = "<colgroup>\n{$colgroup}</colgroup>\n";
544 546 }
545 547
546 - // <thead>, <tfoot>, and <tbody> tags.
548 + /*
549 + * <thead>, <tfoot>, and <tbody> tags.
550 + */
551 +
547 552 if ( ! empty( $thead ) ) {
548 - $thead = "<thead>\n{$thead}</thead>\n";
553 + $thead = "<thead>\n" . implode( '', $thead ) . "</thead>\n";
554 + } else {
555 + $thead = '';
549 556 }
557 +
550 558 if ( ! empty( $tfoot ) ) {
551 - $tfoot = "<tfoot>\n{$tfoot}</tfoot>\n";
559 + $tfoot = "<tfoot>\n" . implode( '', $tfoot ) . "</tfoot>\n";
560 + } else {
561 + $tfoot = '';
552 562 }
553 - $tbody_class = ( $this->render_options['row_hover'] ) ? ' class="row-hover"' : '';
554 - // Reverse rows because we looped through the rows in reverse order.
555 - $tbody = array_reverse( $tbody );
563 +
564 + $tbody_classes = array();
565 + if ( $this->render_options['alternating_row_colors'] ) {
566 + $tbody_classes[] = 'row-striping';
567 + }
568 + if ( $this->render_options['row_hover'] ) {
569 + $tbody_classes[] = 'row-hover';
570 + }
571 + $tbody_class = implode( ' ', $tbody_classes );
572 + if ( '' !== $tbody_class ) {
573 + $tbody_class = ' class="' . esc_attr( $tbody_class ) . '"';
574 + }
575 +
556 576 $tbody = "<tbody{$tbody_class}>\n" . implode( '', $tbody ) . "</tbody>\n";
557 577
558 578 // Attributes for the table (HTML table element).
559 579 $table_attributes = array();
@@ -563,16 +583,23 @@
563 583 $table_attributes['id'] = $this->render_options['html_id'];
564 584 }
565 585
566 586 // "class" attribute.
567 - $css_classes = array( 'tablepress', "tablepress-id-{$this->table['id']}", $this->render_options['extra_css_classes'] );
587 + $css_classes = array(
588 + 'tablepress',
589 + "tablepress-id-{$this->table['id']}",
590 + $this->render_options['extra_css_classes'],
591 + );
592 + if ( $this->tbody_has_connected_cells ) {
593 + $css_classes[] = 'tbody-has-connected-cells';
594 + }
568 595 /**
569 596 * Filters the CSS classes that are given to the HTML table element.
570 597 *
571 598 * @since 1.0.0
572 599 *
573 - * @param array $css_classes The CSS classes for the table element.
574 - * @param string $table_id The current table ID.
600 + * @param string[] $css_classes The CSS classes for the table element.
601 + * @param string $table_id The current table ID.
575 602 */
576 603 $css_classes = apply_filters( 'tablepress_table_css_classes', $css_classes, $this->table['id'] );
577 604 // $css_classes might contain several classes in one array entry.
578 605 $css_classes = explode( ' ', implode( ' ', $css_classes ) );
@@ -577,10 +604,11 @@
577 604 // $css_classes might contain several classes in one array entry.
578 605 $css_classes = explode( ' ', implode( ' ', $css_classes ) );
579 606 $css_classes = array_map( array( 'TablePress', 'sanitize_css_class' ), $css_classes );
580 607 $css_classes = array_unique( $css_classes );
581 - $css_classes = trim( implode( ' ', $css_classes ) );
582 - if ( ! empty( $css_classes ) ) {
608 + $css_classes = array_filter( $css_classes ); // Remove empty entries.
609 + $css_classes = implode( ' ', $css_classes );
610 + if ( '' !== $css_classes ) {
583 611 $table_attributes['class'] = $css_classes;
584 612 }
585 613
586 614 // ARIA label attributes.
@@ -599,10 +627,10 @@
599 627 * The attribute is only added if it is not empty.
600 628 *
601 629 * @since 1.0.0
602 630 *
603 - * @param string $summary The content for the summary attribute of the table. Default empty.
604 - * @param array $table The current table.
631 + * @param string $summary The content for the summary attribute of the table. Default empty.
632 + * @param array<string, mixed> $table The current table.
605 633 */
606 634 $summary = apply_filters( 'tablepress_print_summary_attr', $summary, $this->table );
607 635 if ( ! empty( $summary ) ) {
608 636 $table_attributes['summary'] = esc_attr( $summary );
@@ -610,9 +638,9 @@
610 638
611 639 // Legacy support for attributes that are not encouraged in HTML5.
612 640 foreach ( array( 'cellspacing', 'cellpadding', 'border' ) as $attribute ) {
613 641 if ( false !== $this->render_options[ $attribute ] ) {
614 - $table_attributes[ $attribute ] = (int) $this->render_options[ $attribute ];
642 + $table_attributes[ $attribute ] = (string) absint( $this->render_options[ $attribute ] );
615 643 }
616 644 }
617 645
618 646 /**
@@ -619,19 +647,33 @@
619 647 * Filters the attributes for the table (HTML table element).
620 648 *
621 649 * @since 1.4.0
622 650 *
623 - * @param array $table_attributes The attributes for the table element.
624 - * @param array $table The current table.
625 - * @param array $render_options The render options for the table.
651 + * @param array<string, string> $table_attributes The attributes for the table element.
652 + * @param array<string, mixed> $table The current table.
653 + * @param array<string, mixed> $render_options The render options for the table.
626 654 */
627 655 $table_attributes = apply_filters( 'tablepress_table_tag_attributes', $table_attributes, $this->table, $this->render_options );
628 656 $table_attributes = $this->_attributes_array_to_string( $table_attributes );
629 657
630 - $output .= "\n<table{$table_attributes}>\n";
631 - $output .= $caption . $colgroup . $thead . $tbody . $tfoot;
632 - $output .= "</table>\n";
658 + $table_html = "<table{$table_attributes}>\n";
659 + $table_html .= $caption . $colgroup . $thead . $tbody . $tfoot;
660 + $table_html .= '</table>';
633 661
662 + /**
663 + * Filters the generated HTML code for the table, without HTML elements around it.
664 + *
665 + * @since 2.4.0
666 + *
667 + * @param string $output The generated HTML for the table, without HTML elements around it.
668 + * @param array<string, mixed> $table The current table.
669 + * @param array<string, mixed> $render_options The render options for the table, without HTML elements around it.
670 + */
671 + $table_html = apply_filters( 'tablepress_table_html', $table_html, $this->table, $this->render_options );
672 +
673 + $output .= "\n{$table_html}\n";
674 + unset( $table_html ); // Unset the potentially large variable to free up memory.
675 +
634 676 // name/description below table (HTML already generated above).
635 677 if ( $this->render_options['print_name'] && 'below' === $this->render_options['print_name_position'] ) {
636 678 $output .= $print_name_html;
637 679 }
@@ -639,15 +681,15 @@
639 681 $output .= $print_description_html;
640 682 }
641 683
642 684 /**
643 - * Filters the generated HTML code for table.
685 + * Filters the generated HTML code for the table and HTML elements around it.
644 686 *
645 687 * @since 1.0.0
646 688 *
647 - * @param string $output The generated HTML for the table.
648 - * @param array $table The current table.
649 - * @param array $render_options The render options for the table.
689 + * @param string $output The generated HTML for the table and HTML elements around it.
690 + * @param array<string, mixed> $table The current table.
691 + * @param array<string, mixed> $render_options The render options for the table and HTML elements around it.
650 692 */
651 693 $this->output = apply_filters( 'tablepress_table_output', $output, $this->table, $this->render_options );
652 694 }
653 695
@@ -659,9 +701,9 @@
659 701 * @param int $row_idx Index of the row to be rendered.
660 702 * @param string $tag HTML tag to use for the cells (td or th).
661 703 * @return string HTML for the row.
662 704 */
663 - protected function _render_row( $row_idx, $tag ) {
705 + protected function _render_row( int $row_idx, string $tag ): string {
664 706 $row_cells = array();
665 707 // Loop through cells in reversed order, to search for colspan or rowspan trigger words.
666 708 for ( $col_idx = $this->last_column_idx; $col_idx >= 0; $col_idx-- ) {
667 709 $cell_content = $this->table['data'][ $row_idx ][ $col_idx ];
@@ -668,10 +710,10 @@
668 710
669 711 if ( $this->span_trigger['rowspan'] === $cell_content ) { // There will be a rowspan.
670 712 if ( ! (
671 713 ( 0 === $row_idx ) // No rowspan inside first row.
672 - || ( 1 === $row_idx && $this->render_options['table_head'] ) // No rowspan into table head.
673 - || ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] ) // No rowspan out of table foot.
714 + || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
715 + || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
674 716 ) ) {
675 717 // Increase counter for rowspan in this column.
676 718 ++$this->rowspan[ $col_idx ];
677 719 // Reset counter for colspan in this row, combined col- and rowspan might be happening.
@@ -695,10 +737,10 @@
695 737 $cell_content = '';
696 738 } elseif ( $this->span_trigger['span'] === $cell_content ) { // There will be a combined col- and rowspan.
697 739 if ( ! (
698 740 ( 0 === $row_idx ) // No rowspan inside first row.
699 - || ( 1 === $row_idx && $this->render_options['table_head'] ) // No rowspan into table head.
700 - || ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] ) // No rowspan out of table foot.
741 + || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
742 + || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
701 743 ) && ! (
702 744 ( 0 === $col_idx ) // No colspan inside first column.
703 745 || ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
704 746 ) ) {
@@ -712,12 +754,20 @@
712 754 $tag_attributes = array();
713 755
714 756 // "colspan" and "rowspan" attributes.
715 757 if ( $this->colspan[ $row_idx ] > 1 ) { // We have colspaned cells.
716 - $tag_attributes['colspan'] = $this->colspan[ $row_idx ];
758 + $tag_attributes['colspan'] = (string) $this->colspan[ $row_idx ];
759 + if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
760 + // Set flag that there are connected cells in the tbody.
761 + $this->tbody_has_connected_cells = true;
762 + }
717 763 }
718 764 if ( $this->rowspan[ $col_idx ] > 1 ) { // We have rowspaned cells.
719 - $tag_attributes['rowspan'] = $this->rowspan[ $col_idx ];
765 + $tag_attributes['rowspan'] = (string) $this->rowspan[ $col_idx ];
766 + if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
767 + // Set flag that there are connected cells in the tbody.
768 + $this->tbody_has_connected_cells = true;
769 + }
720 770 }
721 771
722 772 // "class" attribute.
723 773 $cell_class = 'column-' . ( $col_idx + 1 );
@@ -748,24 +798,28 @@
748 798 * Filters the attributes for the table cell (HTML td or th element).
749 799 *
750 800 * @since 1.4.0
751 801 *
752 - * @param array $tag_attributes The attributes for the td or th element.
753 - * @param string $table_id The current table ID.
754 - * @param string $cell_content The cell content.
755 - * @param int $row_idx The row number of the cell.
756 - * @param int $col_idx The column number of the cell.
757 - * @param int $colspan_row The number of combined columns for this cell.
758 - * @param int $rowspan_col The number of combined rows for this cell.
802 + * @param array<string, string> $tag_attributes The attributes for the td or th element.
803 + * @param string $table_id The current table ID.
804 + * @param string $cell_content The cell content.
805 + * @param int $row_idx The row number of the cell.
806 + * @param int $col_idx The column number of the cell.
807 + * @param int $colspan_row The number of combined columns for this cell.
808 + * @param int $rowspan_col The number of combined rows for this cell.
759 809 */
760 810 $tag_attributes = apply_filters( 'tablepress_cell_tag_attributes', $tag_attributes, $this->table['id'], $cell_content, $row_idx + 1, $col_idx + 1, $this->colspan[ $row_idx ], $this->rowspan[ $col_idx ] );
761 811 $tag_attributes = $this->_attributes_array_to_string( $tag_attributes );
762 812
763 - if ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
764 - $tag = 'th';
813 + if ( '' === $cell_content ) {
814 + $cell_tag = 'td'; // For accessibility, empty cells should use `td` and not `th` tags.
815 + } elseif ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
816 + $cell_tag = 'th'; // Non-empty cells in the first column should use `th` tags, if enabled.
817 + } else {
818 + $cell_tag = $tag; // Otherwise, use the tag that was passed in as the default for the row.
765 819 }
766 820
767 - $row_cells[] = "<{$tag}{$tag_attributes}>{$cell_content}</{$tag}>";
821 + $row_cells[] = "<{$cell_tag}{$tag_attributes}>{$cell_content}</{$cell_tag}>";
768 822 $this->colspan[ $row_idx ] = 1; // Reset.
769 823 $this->rowspan[ $col_idx ] = 1; // Reset.
770 824 }
771 825
@@ -773,21 +827,18 @@
773 827 $tr_attributes = array();
774 828
775 829 // "class" attribute.
776 830 $row_classes = 'row-' . ( $row_idx + 1 );
777 - if ( $this->render_options['alternating_row_colors'] ) {
778 - $row_classes .= ( 1 === ( $row_idx % 2 ) ) ? ' even' : ' odd';
779 - }
780 831 /**
781 832 * Filters the CSS classes that are given to a row (HTML tr element) of a table.
782 833 *
783 834 * @since 1.0.0
784 835 *
785 - * @param string $row_classes The CSS classes for the row.
786 - * @param string $table_id The current table ID.
787 - * @param array $row_cells The HTML code for the cells of the row.
788 - * @param int $row_idx The row number.
789 - * @param array $row_data The content of the cells of the row.
836 + * @param string $row_classes The CSS classes for the row.
837 + * @param string $table_id The current table ID.
838 + * @param string[] $row_cells The HTML code for the cells of the row.
839 + * @param int $row_idx The row number.
840 + * @param string[] $row_data The content of the cells of the row.
790 841 */
791 842 $row_classes = apply_filters( 'tablepress_row_css_class', $row_classes, $this->table['id'], $row_cells, $row_idx + 1, $this->table['data'][ $row_idx ] );
792 843 if ( ! empty( $row_classes ) ) {
793 844 $tr_attributes['class'] = $row_classes;
@@ -797,12 +848,12 @@
797 848 * Filters the attributes for the table row (HTML tr element).
798 849 *
799 850 * @since 1.4.0
800 851 *
801 - * @param array $tr_attributes The attributes for the tr element.
802 - * @param string $table_id The current table ID.
803 - * @param int $row_idx The row number.
804 - * @param array $row_data The content of the cells of the row.
852 + * @param array<string, mixed> $tr_attributes The attributes for the tr element.
853 + * @param string $table_id The current table ID.
854 + * @param int $row_idx The row number.
855 + * @param string[] $row_data The content of the cells of the row.
805 856 */
806 857 $tr_attributes = apply_filters( 'tablepress_row_tag_attributes', $tr_attributes, $this->table['id'], $row_idx + 1, $this->table['data'][ $row_idx ] );
807 858 $tr_attributes = $this->_attributes_array_to_string( $tr_attributes );
808 859
@@ -815,12 +866,12 @@
815 866 * Convert an array of HTML tag attributes to a string.
816 867 *
817 868 * @since 1.4.0
818 869 *
819 - * @param array $attributes Attributes for the HTML tag in the array keys, and their values in the array values.
870 + * @param array<string, string> $attributes Attributes for the HTML tag in the array keys, and their values in the array values.
820 871 * @return string The attributes as a string for usage in a HTML element.
821 872 */
822 - protected function _attributes_array_to_string( array $attributes ) {
873 + protected function _attributes_array_to_string( array $attributes ): string {
823 874 $attributes_string = '';
824 875 foreach ( $attributes as $attribute => $value ) {
825 876 $attributes_string .= " {$attribute}=\"{$value}\"";
826 877 }
@@ -829,21 +880,19 @@
829 880
830 881 /**
831 882 * Possibly replace certain HTML entities and replace line breaks with HTML.
832 883 *
833 - * @TODO: Find a better solution than this function, e.g. something like wpautop().
834 - *
835 884 * @since 1.0.0
836 885 *
837 886 * @param string $text The string to process.
838 887 * @return string Processed string for output.
839 888 */
840 - protected function safe_output( $text ) {
889 + protected function safe_output( string $text ): string {
841 890 /*
842 891 * Replace any & with &amp; that is not already an encoded entity (from function htmlentities2 in WP 2.8).
843 892 * A complete htmlentities2() or htmlspecialchars() would encode <HTML> tags, which we don't want.
844 893 */
845 - $text = preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&amp;', $text );
894 + $text = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&amp;', $text );
846 895 /**
847 896 * Filters whether line breaks in the cell content shall be replaced with HTML br tags.
848 897 *
849 898 * @since 1.0.0
@@ -861,14 +910,15 @@
861 910 * Get the default render options, null means: Use option from "Edit" screen.
862 911 *
863 912 * @since 1.0.0
864 913 *
865 - * @return array Default render options.
914 + * @return array<string, mixed> Default render options.
866 915 */
867 - public function get_default_render_options() {
916 + public function get_default_render_options(): array {
868 917 // Attention: Array keys have to be lowercase, otherwise they won't match the Shortcode attributes, which will be passed in lowercase by WP.
869 918 return array(
870 919 'alternating_row_colors' => null,
920 + 'block_preview' => false,
871 921 'border' => false,
872 922 'cache_table_output' => true,
873 923 'cellpadding' => false,
874 924 'cellspacing' => false,
@@ -874,8 +924,9 @@
874 924 'cellspacing' => false,
875 925 'column_widths' => '',
876 926 'convert_line_breaks' => true,
877 927 'datatables_custom_commands' => null,
928 + 'datatables_datetime' => '',
878 929 'datatables_filter' => null,
879 930 'datatables_info' => null,
880 931 'datatables_lengthchange' => null,
881 932 'datatables_locale' => get_locale(),
@@ -910,36 +961,35 @@
910 961 * @since 1.0.0
911 962 *
912 963 * @return string CSS for the Preview iframe.
913 964 */
914 - public function get_preview_css() {
965 + public function get_preview_css(): string {
915 966 $is_rtl = is_rtl();
916 967 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
917 968 $default_css_minified = $tablepress_css->load_default_css_from_file( $is_rtl );
918 969 if ( false === $default_css_minified ) {
919 970 $default_css_minified = '';
920 - } else {
921 - // Change relative URLs to web font files to absolute URLs, as combining the CSS files and saving to another directory breaks the relative URLs.
922 - $absolute_path = plugins_url( 'css/build/tablepress.', TABLEPRESS__FILE__ );
923 - // Make the absolute URL protocol-relative to prevent mixed content warnings.
924 - $absolute_path = str_replace( array( 'http:', 'https:' ), '', $absolute_path );
925 - $default_css_minified = str_replace( 'url(tablepress.', 'url(' . $absolute_path, $default_css_minified );
926 971 }
927 972
928 973 $rtl_direction = $is_rtl ? "\ndirection: rtl;" : '';
929 974
930 975 return <<<CSS
931 -<style>
932 -/* iframe */
933 -body {
934 - margin: 10px;
935 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
936 -}
937 -p {
938 - font-size: 13px;
939 -}
940 -{$default_css_minified}
941 -</style>
942 -CSS;
976 + <style>
977 + /* iframe */
978 + body {
979 + margin: 10px;
980 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
981 + }
982 + p {
983 + font-size: 13px;
984 + }
985 + .preview {
986 + overflow-x: auto;
987 + overflow-y: hidden;
988 + scroll-behavior: smooth;
989 + }
990 + {$default_css_minified}
991 + </style>
992 + CSS;
943 993 }
944 994
945 995 } // class TablePress_Render