PluginProbe
TablePress – Tables in WordPress made easy / 2.2
TablePress – Tables in WordPress made easy v2.2
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 2.2, at classes/class-render.php

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