PluginProbe
TablePress – Tables in WordPress made easy / 3.0.3
TablePress – Tables in WordPress made easy v3.0.3
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 2.4.4 All 44 releases
tablepress / classes / class-render.php

class-render.php in TablePress – Tables in WordPress made easy 3.0.3, at classes/class-render.php

1,016 lines 38.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * TablePress Rendering Class
4 *
5 * @package TablePress
6 * @subpackage Rendering
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * TablePress Rendering Class
16 *
17 * @package TablePress
18 * @subpackage Rendering
19 * @author Tobias Bäthge
20 * @since 1.0.0
21 */
22 class TablePress_Render {
23
24 /**
25 * Table data that is rendered.
26 *
27 * @since 1.0.0
28 * @var array<string, mixed>
29 */
30 protected array $table = array();
31
32 /**
33 * Table options that influence the output result.
34 *
35 * @since 1.0.0
36 * @var array<string, mixed>
37 */
38 protected array $render_options = array();
39
40 /**
41 * Rendered HTML code of the table or PHP array.
42 *
43 * @since 1.0.0
44 * @var string|array<int, array<int, string>>
45 */
46 protected $output;
47
48 /**
49 * Trigger words for colspan, rowspan, or the combination of both.
50 *
51 * @since 1.0.0
52 * @var array<string, string>
53 */
54 protected array $span_trigger = array(
55 'colspan' => '#colspan#',
56 'rowspan' => '#rowspan#',
57 'span' => '#span#',
58 );
59
60 /**
61 * Buffer to store the counts of rowspan per column, initialized in _render_table().
62 *
63 * @since 1.0.0
64 * @var int[]
65 */
66 protected array $rowspan = array();
67
68 /**
69 * Buffer to store the counts of colspan per row, initialized in _render_table().
70 *
71 * @since 1.0.0
72 * @var int[]
73 */
74 protected array $colspan = array();
75
76 /**
77 * Whether the table has connected cells (colspan or rowspan), set in _render_table().
78 *
79 * @since 3.0.0
80 */
81 protected bool $tbody_has_connected_cells = false;
82
83 /**
84 * Index of the last row of the visible data in the table, set in _render_table().
85 *
86 * @since 1.0.0
87 */
88 protected int $last_row_idx;
89
90 /**
91 * Index of the last column of the visible data in the table, set in _render_table().
92 *
93 * @since 1.0.0
94 */
95 protected int $last_column_idx;
96
97 /**
98 * Class constructor.
99 *
100 * @since 1.0.0
101 */
102 public function __construct() {
103 // Unused.
104 }
105
106 /**
107 * Set the table (data, options, visibility, ...) that is to be rendered.
108 *
109 * @since 1.0.0
110 *
111 * @param array<string, mixed> $table Table to be rendered.
112 * @param array<string, mixed> $render_options Options for rendering, from both "Edit" screen and Shortcode.
113 */
114 public function set_input( array $table, array $render_options ): void {
115 $this->table = $table;
116 $this->render_options = $render_options;
117 /**
118 * Filters the table before the render process.
119 *
120 * @since 1.0.0
121 *
122 * @param array<string, mixed> $table The table.
123 * @param array<string, mixed> $render_options The render options for the table.
124 */
125 $this->table = apply_filters( 'tablepress_table_raw_render_data', $this->table, $this->render_options );
126 }
127
128 /**
129 * Process the table rendering and return the HTML output.
130 *
131 * @since 1.0.0
132 * @since 2.0.0 Add the $format parameter.
133 *
134 * @param string $format Optional. Output format, 'html' (default) or 'array'.
135 * @return string|array<int, array<int, string>> HTML code of the rendered table, or a PHP array, or an error message.
136 */
137 public function get_output( string $format = 'html' ) /* : string|array */ {
138 // Evaluate math expressions/formulas.
139 $this->_evaluate_table_data();
140 // Remove hidden rows and columns.
141 $this->_prepare_render_data();
142
143 if ( 'html' !== $format ) {
144 add_filter( 'tablepress_cell_content', 'wptexturize' );
145 }
146
147 // Evaluate Shortcodes and escape cell content.
148 $this->_process_render_data();
149
150 if ( 'html' !== $format ) {
151 remove_filter( 'tablepress_cell_content', 'wptexturize' );
152 }
153
154 switch ( $format ) {
155 case 'html':
156 $this->_render_table();
157 break;
158 case 'array':
159 $this->output = $this->table['data'];
160 break;
161 }
162
163 return $this->output;
164 }
165
166 /**
167 * Loop through the table to evaluate math expressions/formulas.
168 *
169 * @since 1.0.0
170 */
171 protected function _evaluate_table_data(): void {
172 $orig_table = $this->table;
173
174 if ( $this->render_options['evaluate_formulas'] ) {
175 $formula_evaluator = TablePress::load_class( 'TablePress_Evaluate', 'class-evaluate.php', 'classes' );
176 $this->table['data'] = $formula_evaluator->evaluate_table_data( $this->table['data'], $this->table['id'] );
177 }
178
179 /**
180 * Filters the table after evaluating formulas in the table.
181 *
182 * @since 1.0.0
183 *
184 * @param array<string, mixed> $table The table with evaluated formulas.
185 * @param array<string, mixed> $orig_table The table with unevaluated formulas.
186 * @param array<string, mixed> $render_options The render options for the table.
187 */
188 $this->table = apply_filters( 'tablepress_table_evaluate_data', $this->table, $orig_table, $this->render_options );
189 }
190
191 /**
192 * Remove all cells from the data set that shall not be rendered, because they are hidden.
193 *
194 * @since 1.0.0
195 */
196 protected function _prepare_render_data(): void {
197 $orig_table = $this->table;
198
199 $num_rows = count( $this->table['data'] );
200 $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
201
202 // Evaluate show/hide_rows/columns parameters.
203 $actions = array( 'show', 'hide' );
204 $elements = array( 'rows', 'columns' );
205 foreach ( $actions as $action ) {
206 foreach ( $elements as $element ) {
207 if ( empty( $this->render_options[ "{$action}_{$element}" ] ) ) {
208 $this->render_options[ "{$action}_{$element}" ] = array();
209 continue;
210 }
211
212 // Add all rows/columns to array if "all" value set for one of the four parameters.
213 if ( 'all' === $this->render_options[ "{$action}_{$element}" ] ) {
214 $this->render_options[ "{$action}_{$element}" ] = range( 0, ${'num_' . $element} - 1 );
215 continue;
216 }
217
218 // We have a list of rows/columns (possibly with ranges in it).
219 $this->render_options[ "{$action}_{$element}" ] = explode( ',', $this->render_options[ "{$action}_{$element}" ] );
220 // Support for ranges like 3-6 or A-BA.
221 $range_cells = array();
222 foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
223 $range_dash = strpos( $value, '-' );
224 if ( false !== $range_dash ) {
225 unset( $this->render_options[ "{$action}_{$element}" ][ $key ] );
226 $start = trim( substr( $value, 0, $range_dash ) );
227 if ( ! is_numeric( $start ) ) {
228 $start = TablePress::letter_to_number( $start );
229 }
230 $end = trim( substr( $value, $range_dash + 1 ) );
231 if ( ! is_numeric( $end ) ) {
232 $end = TablePress::letter_to_number( $end );
233 }
234 $current_range = range( $start, $end );
235 $range_cells = array_merge( $range_cells, $current_range );
236 }
237 }
238 $this->render_options[ "{$action}_{$element}" ] = array_merge( $this->render_options[ "{$action}_{$element}" ], $range_cells );
239
240 /*
241 * Parse single letters and change from regular numbering to zero-based numbering,
242 * as rows/columns are indexed from 0 internally, but from 1 externally.
243 */
244 foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
245 $value = trim( $value );
246 if ( ! is_numeric( $value ) ) {
247 $value = TablePress::letter_to_number( $value );
248 }
249 $this->render_options[ "{$action}_{$element}" ][ $key ] = (int) $value - 1;
250 }
251
252 // Remove duplicate entries and sort the array.
253 $this->render_options[ "{$action}_{$element}" ] = array_unique( $this->render_options[ "{$action}_{$element}" ] );
254 sort( $this->render_options[ "{$action}_{$element}" ], SORT_NUMERIC );
255 }
256 }
257
258 // Load information about hidden rows and columns.
259 // Get indexes of hidden rows (array value of 0).
260 $hidden_rows = array_keys( $this->table['visibility']['rows'], 0, true );
261 $hidden_rows = array_merge( $hidden_rows, $this->render_options['hide_rows'] );
262 $hidden_rows = array_diff( $hidden_rows, $this->render_options['show_rows'] );
263 // Get indexes of hidden columns (array value of 0).
264 $hidden_columns = array_keys( $this->table['visibility']['columns'], 0, true );
265 $hidden_columns = array_merge( $hidden_columns, $this->render_options['hide_columns'] );
266 $hidden_columns = array_merge( array_diff( $hidden_columns, $this->render_options['show_columns'] ) );
267
268 // Remove hidden rows and re-index.
269 foreach ( $hidden_rows as $row_idx ) {
270 unset( $this->table['data'][ $row_idx ] );
271 }
272 $this->table['data'] = array_merge( $this->table['data'] );
273 // Remove hidden columns and re-index.
274 foreach ( $this->table['data'] as $row_idx => $row ) {
275 foreach ( $hidden_columns as $col_idx ) {
276 unset( $row[ $col_idx ] );
277 }
278 $this->table['data'][ $row_idx ] = array_merge( $row );
279 }
280
281 /**
282 * Filters the table after processing the table visibility information.
283 *
284 * @since 1.0.0
285 *
286 * @param array<string, mixed> $table The processed table.
287 * @param array<string, mixed> $orig_table The unprocessed table.
288 * @param array<string, mixed> $render_options The render options for the table.
289 */
290 $this->table = apply_filters( 'tablepress_table_render_data', $this->table, $orig_table, $this->render_options );
291 }
292
293 /**
294 * Generate the data that is to be rendered.
295 *
296 * @since 2.0.0
297 */
298 protected function _process_render_data(): void {
299 $orig_table = $this->table;
300
301 // Deactivate nl2br() for this render process, if "convert_line_breaks" Shortcode parameter is set to false.
302 if ( ! $this->render_options['convert_line_breaks'] ) {
303 add_filter( 'tablepress_apply_nl2br', '__return_false', 9 ); // Priority 9, so that this filter can easily be overwritten at the default priority.
304 }
305
306 foreach ( $this->table['data'] as $row_idx => $row ) {
307 foreach ( $row as $col_idx => $cell_content ) {
308 // Print formulas that are escaped with '= (like in Excel) as text.
309 if ( str_starts_with( $cell_content, "'=" ) ) {
310 $cell_content = substr( $cell_content, 1 );
311 }
312 $cell_content = $this->safe_output( $cell_content );
313 if ( str_contains( $cell_content, '[' ) ) {
314 $cell_content = do_shortcode( $cell_content );
315 }
316 /**
317 * Filters the content of a single cell, after formulas have been evaluated, the output has been sanitized, and Shortcodes have been evaluated.
318 *
319 * @since 1.0.0
320 *
321 * @param string $cell_content The cell content.
322 * @param string $table_id The current table ID.
323 * @param int $row_idx The row number of the cell.
324 * @param int $col_idx The column number of the cell.
325 */
326 $cell_content = apply_filters( 'tablepress_cell_content', $cell_content, $this->table['id'], $row_idx + 1, $col_idx + 1 );
327 $this->table['data'][ $row_idx ][ $col_idx ] = $cell_content;
328 }
329 }
330
331 // Re-instate nl2br() behavior after this render process, if "convert_line_breaks" Shortcode parameter is set to false.
332 if ( ! $this->render_options['convert_line_breaks'] ) {
333 remove_filter( 'tablepress_apply_nl2br', '__return_false', 9 ); // Priority 9, so that this filter can easily be overwritten at the default priority.
334 }
335
336 /**
337 * Filters the table after processing the table content handling.
338 *
339 * @since 2.0.0
340 *
341 * @param array<string, mixed> $table The processed table.
342 * @param array<string, mixed> $orig_table The unprocessed table.
343 * @param array<string, mixed> $render_options The render options for the table.
344 */
345 $this->table = apply_filters( 'tablepress_table_content_render_data', $this->table, $orig_table, $this->render_options );
346 }
347
348 /**
349 * Generate the HTML output of the table.
350 *
351 * @since 1.0.0
352 */
353 protected function _render_table(): void {
354 $num_rows = count( $this->table['data'] );
355 $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
356
357 // Check if there are rows and columns in the table (might not be the case after removing hidden rows/columns!).
358 if ( 0 === $num_rows || 0 === $num_columns ) {
359 $this->output = sprintf( __( '<!-- The table with the ID %s is empty! -->', 'tablepress' ), $this->table['id'] );
360 return;
361 }
362
363 // Counters for spans of rows and columns, init to 1 for each row and column (as that means no span).
364 $this->rowspan = array_fill( 0, $num_columns, 1 );
365 $this->colspan = array_fill( 0, $num_rows, 1 );
366
367 /**
368 * Filters the trigger keywords for "colspan" and "rowspan"
369 *
370 * @since 1.0.0
371 *
372 * @param array<string, string> $span_trigger The trigger keywords for combining table cells.
373 * @param string $table_id The current table ID.
374 */
375 $this->span_trigger = apply_filters( 'tablepress_span_trigger_keywords', $this->span_trigger, $this->table['id'] );
376
377 // Explode from string to array.
378 $this->render_options['column_widths'] = ( ! empty( $this->render_options['column_widths'] ) ) ? explode( '|', $this->render_options['column_widths'] ) : array();
379 // Make array $this->render_options['column_widths'] have $columns entries.
380 $this->render_options['column_widths'] = array_pad( $this->render_options['column_widths'], $num_columns, '' );
381
382 $output = '';
383
384 if ( $this->render_options['print_name'] ) {
385 /**
386 * Filters the HTML tag that wraps the printed table name.
387 *
388 * @since 1.0.0
389 *
390 * @param string $tag The HTML tag around the table name. Default h2.
391 * @param string $table_id The current table ID.
392 */
393 $name_html_tag = apply_filters( 'tablepress_print_name_html_tag', 'h2', $this->table['id'] );
394
395 $name_attributes = array();
396 if ( ! empty( $this->render_options['html_id'] ) ) {
397 $name_attributes['id'] = "{$this->render_options['html_id']}-name";
398 }
399 /**
400 * Filters the class attribute for the printed table name.
401 *
402 * @since 1.0.0
403 * @deprecated 1.13.0 Use {@see 'tablepress_table_name_tag_attributes'} instead.
404 *
405 * @param string $class The class attribute for the table name that can be used in CSS code.
406 * @param string $table_id The current table ID.
407 */
408 $name_attributes['class'] = apply_filters_deprecated( 'tablepress_print_name_css_class', array( "tablepress-table-name tablepress-table-name-id-{$this->table['id']}", $this->table['id'] ), 'TablePress 1.13.0', 'tablepress_table_name_tag_attributes' );
409 /**
410 * Filters the attributes for the table name (HTML h2 element, by default).
411 *
412 * @since 1.13.0
413 *
414 * @param array<string, string> $name_attributes The attributes for the table name element.
415 * @param array<string, mixed> $table The current table.
416 * @param array<string, mixed> $render_options The render options for the table.
417 */
418 $name_attributes = apply_filters( 'tablepress_table_name_tag_attributes', $name_attributes, $this->table, $this->render_options );
419 $name_attributes = $this->_attributes_array_to_string( $name_attributes );
420
421 $print_name_html = "<{$name_html_tag}{$name_attributes}>" . $this->safe_output( $this->table['name'] ) . "</{$name_html_tag}>\n";
422 }
423 if ( $this->render_options['print_description'] ) {
424 /**
425 * Filters the HTML tag that wraps the printed table description.
426 *
427 * @since 1.0.0
428 *
429 * @param string $tag The HTML tag around the table description. Default span.
430 * @param string $table_id The current table ID.
431 */
432 $description_html_tag = apply_filters( 'tablepress_print_description_html_tag', 'span', $this->table['id'] );
433
434 $description_attributes = array();
435 if ( ! empty( $this->render_options['html_id'] ) ) {
436 $description_attributes['id'] = "{$this->render_options['html_id']}-description";
437 }
438 /**
439 * Filters the class attribute for the printed table description.
440 *
441 * @since 1.0.0
442 * @deprecated 1.13.0 Use {@see 'tablepress_table_description_tag_attributes'} instead.
443 *
444 * @param string $class The class attribute for the table description that can be used in CSS code.
445 * @param string $table_id The current table ID.
446 */
447 $description_attributes['class'] = apply_filters_deprecated( 'tablepress_print_description_css_class', array( "tablepress-table-description tablepress-table-description-id-{$this->table['id']}", $this->table['id'] ), 'TablePress 1.13.0', 'tablepress_table_description_tag_attributes' );
448 /**
449 * Filters the attributes for the table description (HTML span element, by default).
450 *
451 * @since 1.13.0
452 *
453 * @param array<string, string> $description_attributes The attributes for the table description element.
454 * @param array<string, mixed> $table The current table.
455 * @param array<string, mixed> $render_options The render options for the table.
456 */
457 $description_attributes = apply_filters( 'tablepress_table_description_tag_attributes', $description_attributes, $this->table, $this->render_options );
458 $description_attributes = $this->_attributes_array_to_string( $description_attributes );
459
460 $print_description_html = "<{$description_html_tag}{$description_attributes}>" . $this->safe_output( $this->table['description'] ) . "</{$description_html_tag}>\n";
461 }
462
463 if ( $this->render_options['print_name'] && 'above' === $this->render_options['print_name_position'] ) {
464 $output .= $print_name_html;
465 }
466 if ( $this->render_options['print_description'] && 'above' === $this->render_options['print_description_position'] ) {
467 $output .= $print_description_html;
468 }
469
470 $thead = array();
471 $tfoot = array();
472 $tbody = array();
473
474 $this->last_row_idx = $num_rows - 1;
475 $this->last_column_idx = $num_columns - 1;
476
477 // Loop through rows in reversed order, to search for rowspan trigger keyword.
478 $row_idx = $this->last_row_idx;
479
480 // Render the table footer rows, if there is at least one extra row.
481 if ( $this->render_options['table_foot'] > 0 && $num_rows >= $this->render_options['table_head'] + $this->render_options['table_foot'] ) { // @phpstan-ignore greaterOrEqual.invalid (`table_head` and `table_foot` are integers.)
482 $last_tbody_idx = $this->last_row_idx - $this->render_options['table_foot'];
483 while ( $row_idx > $last_tbody_idx ) {
484 $tfoot[] = $this->_render_row( $row_idx, 'th' );
485 --$row_idx;
486 }
487 // Reverse rows because we looped through the rows in reverse order.
488 $tfoot = array_reverse( $tfoot );
489 }
490
491 // Render the table body rows.
492 $last_thead_idx = $this->render_options['table_head'] - 1;
493 while ( $row_idx > $last_thead_idx ) {
494 $tbody[] = $this->_render_row( $row_idx, 'td' );
495 --$row_idx;
496 }
497 // Reverse rows because we looped through the rows in reverse order.
498 $tbody = array_reverse( $tbody );
499
500 // Render the table header rows, if rows are left.
501 while ( $row_idx > -1 ) {
502 $thead[] = $this->_render_row( $row_idx, 'th' );
503 --$row_idx;
504 }
505 // Reverse rows because we looped through the rows in reverse order.
506 $thead = array_reverse( $thead );
507
508 // <caption> tag.
509 /**
510 * Filters the content for the HTML caption element of the table.
511 *
512 * If the "Edit" link for a table is shown, it is also added to the caption element.
513 *
514 * @since 1.0.0
515 *
516 * @param string $caption The content for the HTML caption element of the table. Default empty.
517 * @param array<string, mixed> $table The current table.
518 */
519 $caption = apply_filters( 'tablepress_print_caption_text', '', $this->table );
520 $caption_style = '';
521 $caption_class = '';
522 if ( ! empty( $caption ) ) {
523 /**
524 * Filters the class attribute for the HTML caption element of the table.
525 *
526 * @since 1.0.0
527 *
528 * @param string $class The class attribute for the HTML caption element of the table.
529 * @param string $table_id The current table ID.
530 */
531 $caption_class = apply_filters( 'tablepress_print_caption_class', "tablepress-table-caption tablepress-table-caption-id-{$this->table['id']}", $this->table['id'] );
532 $caption_class = ' class="' . $caption_class . '"';
533 }
534 if ( ! empty( $this->render_options['edit_table_url'] ) ) {
535 if ( empty( $caption ) ) {
536 $caption_style = ' style="caption-side:bottom;text-align:left;border:none;background:none;margin:0;padding:0;"';
537 } else {
538 $caption .= '<br />';
539 }
540 $caption .= '<a href="' . esc_url( $this->render_options['edit_table_url'] ) . '" rel="nofollow">' . __( 'Edit', 'default' ) . '</a>';
541 }
542 if ( ! empty( $caption ) ) {
543 $caption = "<caption{$caption_class}{$caption_style}>{$caption}</caption>\n";
544 }
545
546 // <colgroup> tag.
547 $colgroup = '';
548 /**
549 * Filters whether the HTML colgroup tag shall be added to the table output.
550 *
551 * @since 1.0.0
552 *
553 * @param bool $print Whether the colgroup element shall be printed.
554 * @param string $table_id The current table ID.
555 */
556 if ( apply_filters( 'tablepress_print_colgroup_tag', false, $this->table['id'] ) ) {
557 for ( $col_idx = 0; $col_idx < $num_columns; $col_idx++ ) {
558 $attributes = ' class="colgroup-column-' . ( $col_idx + 1 ) . ' "';
559 /**
560 * Filters the attributes of the HTML col tags in the HTML colgroup tag.
561 *
562 * @since 1.0.0
563 *
564 * @param string $attributes The attributes in the col element.
565 * @param string $table_id The current table ID.
566 * @param int $col_idx The number of the column.
567 */
568 $attributes = apply_filters( 'tablepress_colgroup_tag_attributes', $attributes, $this->table['id'], $col_idx + 1 );
569 $colgroup .= "\t<col{$attributes} />\n";
570 }
571 }
572 if ( ! empty( $colgroup ) ) {
573 $colgroup = "<colgroup>\n{$colgroup}</colgroup>\n";
574 }
575
576 /*
577 * <thead>, <tfoot>, and <tbody> tags.
578 */
579
580 if ( ! empty( $thead ) ) {
581 $thead = "<thead>\n" . implode( '', $thead ) . "</thead>\n";
582 } else {
583 $thead = '';
584 }
585
586 if ( ! empty( $tfoot ) ) {
587 $tfoot = "<tfoot>\n" . implode( '', $tfoot ) . "</tfoot>\n";
588 } else {
589 $tfoot = '';
590 }
591
592 $tbody_classes = array();
593 if ( $this->render_options['alternating_row_colors'] ) {
594 $tbody_classes[] = 'row-striping';
595 }
596 if ( $this->render_options['row_hover'] ) {
597 $tbody_classes[] = 'row-hover';
598 }
599 $tbody_class = implode( ' ', $tbody_classes );
600 if ( '' !== $tbody_class ) {
601 $tbody_class = ' class="' . esc_attr( $tbody_class ) . '"';
602 }
603
604 $tbody = "<tbody{$tbody_class}>\n" . implode( '', $tbody ) . "</tbody>\n";
605
606 // Attributes for the table (HTML table element).
607 $table_attributes = array();
608
609 // "id" attribute.
610 if ( ! empty( $this->render_options['html_id'] ) ) {
611 $table_attributes['id'] = $this->render_options['html_id'];
612 }
613
614 // "class" attribute.
615 $css_classes = array(
616 'tablepress',
617 "tablepress-id-{$this->table['id']}",
618 $this->render_options['extra_css_classes'],
619 );
620 if ( $this->tbody_has_connected_cells ) {
621 $css_classes[] = 'tbody-has-connected-cells';
622 }
623 /**
624 * Filters the CSS classes that are given to the HTML table element.
625 *
626 * @since 1.0.0
627 *
628 * @param string[] $css_classes The CSS classes for the table element.
629 * @param string $table_id The current table ID.
630 */
631 $css_classes = apply_filters( 'tablepress_table_css_classes', $css_classes, $this->table['id'] );
632 // $css_classes might contain several classes in one array entry.
633 $css_classes = explode( ' ', implode( ' ', $css_classes ) );
634 $css_classes = array_map( array( 'TablePress', 'sanitize_css_class' ), $css_classes );
635 $css_classes = array_unique( $css_classes );
636 $css_classes = array_filter( $css_classes ); // Remove empty entries.
637 $css_classes = implode( ' ', $css_classes );
638 if ( '' !== $css_classes ) {
639 $table_attributes['class'] = $css_classes;
640 }
641
642 // ARIA label attributes.
643 if ( $this->render_options['print_name'] && ! empty( $this->render_options['html_id'] ) ) {
644 $table_attributes['aria-labelledby'] = "{$this->render_options['html_id']}-name";
645 }
646 if ( $this->render_options['print_description'] && ! empty( $this->render_options['html_id'] ) ) {
647 $table_attributes['aria-describedby'] = "{$this->render_options['html_id']}-description";
648 }
649
650 // "summary" attribute.
651 $summary = '';
652 /**
653 * Filters the content for the summary attribute of the HTML table element.
654 *
655 * The attribute is only added if it is not empty.
656 *
657 * @since 1.0.0
658 *
659 * @param string $summary The content for the summary attribute of the table. Default empty.
660 * @param array<string, mixed> $table The current table.
661 */
662 $summary = apply_filters( 'tablepress_print_summary_attr', $summary, $this->table );
663 if ( ! empty( $summary ) ) {
664 $table_attributes['summary'] = esc_attr( $summary );
665 }
666
667 // Legacy support for attributes that are not encouraged in HTML5.
668 foreach ( array( 'cellspacing', 'cellpadding', 'border' ) as $attribute ) {
669 if ( false !== $this->render_options[ $attribute ] ) {
670 $table_attributes[ $attribute ] = (int) $this->render_options[ $attribute ];
671 }
672 }
673
674 /**
675 * Filters the attributes for the table (HTML table element).
676 *
677 * @since 1.4.0
678 *
679 * @param array<string, string> $table_attributes The attributes for the table element.
680 * @param array<string, mixed> $table The current table.
681 * @param array<string, mixed> $render_options The render options for the table.
682 */
683 $table_attributes = apply_filters( 'tablepress_table_tag_attributes', $table_attributes, $this->table, $this->render_options );
684 $table_attributes = $this->_attributes_array_to_string( $table_attributes );
685
686 $table_html = "<table{$table_attributes}>\n";
687 $table_html .= $caption . $colgroup . $thead . $tbody . $tfoot;
688 $table_html .= '</table>';
689
690 /**
691 * Filters the generated HTML code for the table, without HTML elements around it.
692 *
693 * @since 2.4.0
694 *
695 * @param string $output The generated HTML for the table, without HTML elements around it.
696 * @param array<string, mixed> $table The current table.
697 * @param array<string, mixed> $render_options The render options for the table, without HTML elements around it.
698 */
699 $table_html = apply_filters( 'tablepress_table_html', $table_html, $this->table, $this->render_options );
700
701 $output .= "\n{$table_html}\n";
702 unset( $table_html ); // Unset the potentially large variable to free up memory.
703
704 // name/description below table (HTML already generated above).
705 if ( $this->render_options['print_name'] && 'below' === $this->render_options['print_name_position'] ) {
706 $output .= $print_name_html; // @phpstan-ignore variable.undefined (The variable is set above.)
707 }
708 if ( $this->render_options['print_description'] && 'below' === $this->render_options['print_description_position'] ) {
709 $output .= $print_description_html; // @phpstan-ignore variable.undefined (The variable is set above.)
710 }
711
712 /**
713 * Filters the generated HTML code for the table and HTML elements around it.
714 *
715 * @since 1.0.0
716 *
717 * @param string $output The generated HTML for the table and HTML elements around it.
718 * @param array<string, mixed> $table The current table.
719 * @param array<string, mixed> $render_options The render options for the table and HTML elements around it.
720 */
721 $this->output = apply_filters( 'tablepress_table_output', $output, $this->table, $this->render_options );
722 }
723
724 /**
725 * Generate the HTML of a row.
726 *
727 * @since 1.0.0
728 *
729 * @param int $row_idx Index of the row to be rendered.
730 * @param string $tag HTML tag to use for the cells (td or th).
731 * @return string HTML for the row.
732 */
733 protected function _render_row( int $row_idx, string $tag ): string {
734 $row_cells = array();
735 // Loop through cells in reversed order, to search for colspan or rowspan trigger words.
736 for ( $col_idx = $this->last_column_idx; $col_idx >= 0; $col_idx-- ) {
737 $cell_content = $this->table['data'][ $row_idx ][ $col_idx ];
738
739 if ( $this->span_trigger['rowspan'] === $cell_content ) { // There will be a rowspan.
740 if ( ! (
741 ( 0 === $row_idx ) // No rowspan inside first row.
742 || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
743 || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
744 ) ) {
745 // Increase counter for rowspan in this column.
746 ++$this->rowspan[ $col_idx ];
747 // Reset counter for colspan in this row, combined col- and rowspan might be happening.
748 $this->colspan[ $row_idx ] = 1;
749 continue;
750 }
751 // Invalid rowspan, so we set cell content from #rowspan# to empty.
752 $cell_content = '';
753 } elseif ( $this->span_trigger['colspan'] === $cell_content ) { // There will be a colspan.
754 if ( ! (
755 ( ( 0 === $row_idx ) && 1 === $this->render_options['table_head'] && $this->render_options['use_datatables'] ) // Don't allow colspan inside a single row table head, as DataTables seems to have a bug here.
756 || ( 0 === $col_idx ) // No colspan inside first column.
757 || ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
758 ) ) {
759 // Increase counter for colspan in this row.
760 ++$this->colspan[ $row_idx ];
761 // Reset counter for rowspan in this column, combined col- and rowspan might be happening.
762 $this->rowspan[ $col_idx ] = 1;
763 continue;
764 }
765 // Invalid colspan, so we set cell content from #colspan# to empty.
766 $cell_content = '';
767 } elseif ( $this->span_trigger['span'] === $cell_content ) { // There will be a combined col- and rowspan.
768 if ( ! (
769 ( 0 === $row_idx ) // No rowspan inside first row.
770 || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
771 || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
772 ) && ! (
773 ( 0 === $col_idx ) // No colspan inside first column.
774 || ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
775 ) ) {
776 continue;
777 }
778 // Invalid span, so we set cell content from #span# to empty.
779 $cell_content = '';
780 }
781
782 // Attributes for the table cell (HTML td or th element).
783 $tag_attributes = array();
784
785 // "colspan" and "rowspan" attributes.
786 if ( $this->colspan[ $row_idx ] > 1 ) { // We have colspaned cells.
787 $tag_attributes['colspan'] = (string) $this->colspan[ $row_idx ];
788 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 ) {
789 // Set flag that there are connected cells in the tbody.
790 $this->tbody_has_connected_cells = true;
791 }
792 }
793 if ( $this->rowspan[ $col_idx ] > 1 ) { // We have rowspaned cells.
794 $tag_attributes['rowspan'] = (string) $this->rowspan[ $col_idx ];
795 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 ) {
796 // Set flag that there are connected cells in the tbody.
797 $this->tbody_has_connected_cells = true;
798 }
799 }
800
801 // "class" attribute.
802 $cell_class = 'column-' . ( $col_idx + 1 );
803 /**
804 * Filters the CSS classes that are given to a single cell (HTML td element) of a table.
805 *
806 * @since 1.0.0
807 *
808 * @param string $cell_class The CSS classes for the cell.
809 * @param string $table_id The current table ID.
810 * @param string $cell_content The cell content.
811 * @param int $row_idx The row number of the cell.
812 * @param int $col_idx The column number of the cell.
813 * @param int $colspan_row The number of combined columns for this cell.
814 * @param int $rowspan_col The number of combined rows for this cell.
815 */
816 $cell_class = apply_filters( 'tablepress_cell_css_class', $cell_class, $this->table['id'], $cell_content, $row_idx + 1, $col_idx + 1, $this->colspan[ $row_idx ], $this->rowspan[ $col_idx ] );
817 if ( ! empty( $cell_class ) ) {
818 $tag_attributes['class'] = $cell_class;
819 }
820
821 // "style" attribute.
822 if ( ( 0 === $row_idx ) && ! empty( $this->render_options['column_widths'][ $col_idx ] ) ) {
823 $tag_attributes['style'] = 'width:' . preg_replace( '#[^0-9a-z.%]#', '', $this->render_options['column_widths'][ $col_idx ] ) . ';';
824 }
825
826 /**
827 * Filters the attributes for the table cell (HTML td or th element).
828 *
829 * @since 1.4.0
830 *
831 * @param array<string, string> $tag_attributes The attributes for the td or th element.
832 * @param string $table_id The current table ID.
833 * @param string $cell_content The cell content.
834 * @param int $row_idx The row number of the cell.
835 * @param int $col_idx The column number of the cell.
836 * @param int $colspan_row The number of combined columns for this cell.
837 * @param int $rowspan_col The number of combined rows for this cell.
838 */
839 $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 ] );
840 $tag_attributes = $this->_attributes_array_to_string( $tag_attributes );
841
842 if ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
843 $tag = 'th';
844 }
845
846 $row_cells[] = "<{$tag}{$tag_attributes}>{$cell_content}</{$tag}>";
847 $this->colspan[ $row_idx ] = 1; // Reset.
848 $this->rowspan[ $col_idx ] = 1; // Reset.
849 }
850
851 // Attributes for the table row (HTML tr element).
852 $tr_attributes = array();
853
854 // "class" attribute.
855 $row_classes = 'row-' . ( $row_idx + 1 );
856 /**
857 * Filters the CSS classes that are given to a row (HTML tr element) of a table.
858 *
859 * @since 1.0.0
860 *
861 * @param string $row_classes The CSS classes for the row.
862 * @param string $table_id The current table ID.
863 * @param string[] $row_cells The HTML code for the cells of the row.
864 * @param int $row_idx The row number.
865 * @param string[] $row_data The content of the cells of the row.
866 */
867 $row_classes = apply_filters( 'tablepress_row_css_class', $row_classes, $this->table['id'], $row_cells, $row_idx + 1, $this->table['data'][ $row_idx ] );
868 if ( ! empty( $row_classes ) ) {
869 $tr_attributes['class'] = $row_classes;
870 }
871
872 /**
873 * Filters the attributes for the table row (HTML tr element).
874 *
875 * @since 1.4.0
876 *
877 * @param array<string, mixed> $tr_attributes The attributes for the tr element.
878 * @param string $table_id The current table ID.
879 * @param int $row_idx The row number.
880 * @param string[] $row_data The content of the cells of the row.
881 */
882 $tr_attributes = apply_filters( 'tablepress_row_tag_attributes', $tr_attributes, $this->table['id'], $row_idx + 1, $this->table['data'][ $row_idx ] );
883 $tr_attributes = $this->_attributes_array_to_string( $tr_attributes );
884
885 // Reverse rows because we looped through the cells in reverse order.
886 $row_cells = array_reverse( $row_cells );
887 return "<tr{$tr_attributes}>\n\t" . implode( '', $row_cells ) . "\n</tr>\n";
888 }
889
890 /**
891 * Convert an array of HTML tag attributes to a string.
892 *
893 * @since 1.4.0
894 *
895 * @param array<string, string> $attributes Attributes for the HTML tag in the array keys, and their values in the array values.
896 * @return string The attributes as a string for usage in a HTML element.
897 */
898 protected function _attributes_array_to_string( array $attributes ): string {
899 $attributes_string = '';
900 foreach ( $attributes as $attribute => $value ) {
901 $attributes_string .= " {$attribute}=\"{$value}\"";
902 }
903 return $attributes_string;
904 }
905
906 /**
907 * Possibly replace certain HTML entities and replace line breaks with HTML.
908 *
909 * @since 1.0.0
910 *
911 * @param string $text The string to process.
912 * @return string Processed string for output.
913 */
914 protected function safe_output( string $text ): string {
915 /*
916 * Replace any & with &amp; that is not already an encoded entity (from function htmlentities2 in WP 2.8).
917 * A complete htmlentities2() or htmlspecialchars() would encode <HTML> tags, which we don't want.
918 */
919 $text = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&amp;', $text );
920 /**
921 * Filters whether line breaks in the cell content shall be replaced with HTML br tags.
922 *
923 * @since 1.0.0
924 *
925 * @param bool $replace Whether to replace line breaks with HTML br tags. Default true.
926 * @param string $table_id The current table ID.
927 */
928 if ( apply_filters( 'tablepress_apply_nl2br', true, $this->table['id'] ) ) {
929 $text = nl2br( $text );
930 }
931 return $text;
932 }
933
934 /**
935 * Get the default render options, null means: Use option from "Edit" screen.
936 *
937 * @since 1.0.0
938 *
939 * @return array<string, mixed> Default render options.
940 */
941 public function get_default_render_options(): array {
942 // Attention: Array keys have to be lowercase, otherwise they won't match the Shortcode attributes, which will be passed in lowercase by WP.
943 return array(
944 'alternating_row_colors' => null,
945 'block_preview' => false,
946 'border' => false,
947 'cache_table_output' => true,
948 'cellpadding' => false,
949 'cellspacing' => false,
950 'column_widths' => '',
951 'convert_line_breaks' => true,
952 'datatables_custom_commands' => null,
953 'datatables_datetime' => '',
954 'datatables_filter' => null,
955 'datatables_info' => null,
956 'datatables_lengthchange' => null,
957 'datatables_locale' => get_locale(),
958 'datatables_paginate' => null,
959 'datatables_paginate_entries' => null,
960 'datatables_scrollx' => null,
961 'datatables_scrolly' => false,
962 'datatables_sort' => null,
963 'evaluate_formulas' => true,
964 'extra_css_classes' => null,
965 'first_column_th' => false,
966 'hide_columns' => '',
967 'hide_rows' => '',
968 'id' => '',
969 'print_description' => null,
970 'print_description_position' => null,
971 'print_name' => null,
972 'print_name_position' => null,
973 'row_hover' => null,
974 'shortcode_debug' => false,
975 'show_columns' => '',
976 'show_rows' => '',
977 'table_foot' => null,
978 'table_head' => null,
979 'use_datatables' => null,
980 );
981 }
982
983 /**
984 * Get the CSS code for the Preview iframe.
985 *
986 * @since 1.0.0
987 *
988 * @return string CSS for the Preview iframe.
989 */
990 public function get_preview_css(): string {
991 $is_rtl = is_rtl();
992 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
993 $default_css_minified = $tablepress_css->load_default_css_from_file( $is_rtl );
994 if ( false === $default_css_minified ) {
995 $default_css_minified = '';
996 }
997
998 $rtl_direction = $is_rtl ? "\ndirection: rtl;" : '';
999
1000 return <<<CSS
1001 <style>
1002 /* iframe */
1003 body {
1004 margin: 10px;
1005 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
1006 }
1007 p {
1008 font-size: 13px;
1009 }
1010 {$default_css_minified}
1011 </style>
1012 CSS;
1013 }
1014
1015 } // class TablePress_Render
1016