/** * Parse and evaluate the content of a cell * * @since 1.0.0 * * @param string $content Content of a cell * @param array $parents List of cells that depend on this cell (to prevent circle references) * @return string Result of the parsing/evaluation */ protected function _evaluate_cell($content, array $parents = array()) { if ('' == $content || '=' == $content || '=' != $content[0]) { return $content; } $content = substr($content, 1); // Support putting formulas in strings, like =Total: {A3+A4} $expressions = array(); if (preg_match_all('#{(.+?)}#', $content, $expressions, PREG_SET_ORDER)) { $formula_in_string = true; } else { $formula_in_string = false; $expressions[] = array($content, $content); // fill array so that it has the same structure as if it came from preg_match_all() } foreach ($expressions as $expression) { $orig_expression = $expression[0]; $expression = $expression[1]; $replaced_references = $replaced_ranges = array(); // remove all whitespace characters $expression = preg_replace('#[\\r\\n\\t ]#', '', $expression); // expand cell ranges (like A3:A6) to a list of single cells (like A3,A4,A5,A6) if (preg_match_all('#([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)#', $expression, $referenced_cell_ranges, PREG_SET_ORDER)) { foreach ($referenced_cell_ranges as $cell_range) { if (in_array($cell_range[0], $replaced_ranges, true)) { continue; } $replaced_ranges[] = $cell_range[0]; if (isset($this->known_ranges[$cell_range[0]])) { $expression = preg_replace('#(?<![A-Z])' . preg_quote($cell_range[0], '#') . '(?![0-9])#', $this->known_ranges[$cell_range[0]], $expression); continue; } // no -1 necessary for this transformation, as we don't actually access the table $first_col = TablePress::letter_to_number($cell_range[1]); $first_row = $cell_range[2]; $last_col = TablePress::letter_to_number($cell_range[3]); $last_row = $cell_range[4]; $col_start = min($first_col, $last_col); $col_end = max($first_col, $last_col) + 1; // +1 for loop below $row_start = min($first_row, $last_row); $row_end = max($first_row, $last_row) + 1; // +1 for loop below $cell_list = array(); for ($col = $col_start; $col < $col_end; $col++) { for ($row = $row_start; $row < $row_end; $row++) { $column = TablePress::number_to_letter($col); $cell_list[] = "{$column}{$row}"; } } $cell_list = implode(',', $cell_list); $expression = preg_replace('#(?<![A-Z])' . preg_quote($cell_range[0], '#') . '(?![0-9])#', $cell_list, $expression); $this->known_ranges[$cell_range[0]] = $cell_list; } } // parse and evaluate single cell references (like A3 or XY312), while prohibiting circle references if (preg_match_all('#([A-Z]+)([0-9]+)#', $expression, $referenced_cells, PREG_SET_ORDER)) { foreach ($referenced_cells as $cell_reference) { if (in_array($cell_reference[0], $parents, true)) { return '!ERROR! Circle Reference'; } if (in_array($cell_reference[0], $replaced_references, true)) { continue; } $replaced_references[] = $cell_reference[0]; $ref_col = TablePress::letter_to_number($cell_reference[1]) - 1; $ref_row = $cell_reference[2] - 1; if (!isset($this->table['data'][$ref_row]) || !isset($this->table['data'][$ref_row][$ref_col])) { return "!ERROR! Cell {$cell_reference[0]} does not exist"; } $ref_parents = $parents; $ref_parents[] = $cell_reference[0]; $result = $this->table['data'][$ref_row][$ref_col] = $this->_evaluate_cell($this->table['data'][$ref_row][$ref_col], $ref_parents); // Bail if there was an error already if (false !== strpos($result, '!ERROR!')) { return $result; } // remove all whitespace characters $result = preg_replace('#[\\r\\n\\t ]#', '', $result); // Treat empty cells as 0 if ('' == $result) { $result = 0; } // Bail if the cell does not result in a number (meaning it was a number or expression before being evaluated) if (!is_numeric($result)) { return "!ERROR! {$cell_reference[0]} does not contain a number or expression"; } $expression = preg_replace('#(?<![A-Z])' . $cell_reference[0] . '(?![0-9])#', $result, $expression); } } $result = $this->_evaluate_math_expression($expression); // Support putting formulas in strings, like =Total: {A3+A4} if ($formula_in_string) { $content = str_replace($orig_expression, $result, $content); } else { $content = $result; } } return $content; }
/** * Print the content of the "Table Content" post meta box * * @since 1.0.0 */ public function postbox_table_data($data, $box) { $table = $data['table']['data']; $options = $data['table']['options']; $visibility = $data['table']['visibility']; $rows = count($table); $columns = count($table[0]); $head_row_idx = $foot_row_idx = -1; // determine row index of the table head row, by excluding all hidden rows from the beginning if ($options['table_head']) { for ($row_idx = 0; $row_idx < $rows; $row_idx++) { if (1 === $visibility['rows'][$row_idx]) { $head_row_idx = $row_idx; break; } } } // determine row index of the table foot row, by excluding all hidden rows from the end if ($options['table_foot']) { for ($row_idx = $rows - 1; $row_idx > -1; $row_idx--) { if (1 === $visibility['rows'][$row_idx]) { $foot_row_idx = $row_idx; break; } } } ?> <table id="edit-form" class="tablepress-edit-screen-id-<?php echo esc_attr($data['table']['id']); ?> "> <thead> <tr id="edit-form-head"> <th></th> <th></th> <?php for ($col_idx = 0; $col_idx < $columns; $col_idx++) { $column_class = ''; if (0 === $visibility['columns'][$col_idx]) { $column_class = ' column-hidden'; } $column = TablePress::number_to_letter($col_idx + 1); echo "\t\t\t<th class=\"head{$column_class}\"><span class=\"sort-control sort-desc hide-if-no-js\" title=\"" . esc_attr__('Sort descending', 'tablepress') . "\"><span class=\"sorting-indicator\"></span></span><span class=\"sort-control sort-asc hide-if-no-js\" title=\"" . esc_attr__('Sort ascending', 'tablepress') . "\"><span class=\"sorting-indicator\"></span></span><span class=\"move-handle\">{$column}</span></th>\n"; } ?> <th></th> </tr> </thead> <tfoot> <tr id="edit-form-foot"> <th></th> <th></th> <?php for ($col_idx = 0; $col_idx < $columns; $col_idx++) { $column_class = ''; if (0 === $visibility['columns'][$col_idx]) { $column_class = ' class="column-hidden"'; } echo "\t\t\t<th{$column_class}><input type=\"checkbox\" class=\"hide-if-no-js\" />"; echo "<input type=\"hidden\" class=\"visibility\" name=\"table[visibility][columns][]\" value=\"{$visibility['columns'][$col_idx]}\" /></th>\n"; } ?> <th></th> </tr> </tfoot> <tbody id="edit-form-body"> <?php foreach ($table as $row_idx => $row_data) { $row = $row_idx + 1; $classes = array(); if ($row_idx % 2 == 0) { $classes[] = 'odd'; } if ($head_row_idx == $row_idx) { $classes[] = 'head-row'; } elseif ($foot_row_idx == $row_idx) { $classes[] = 'foot-row'; } if (0 === $visibility['rows'][$row_idx]) { $classes[] = 'row-hidden'; } $row_class = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : ''; echo "\t\t<tr{$row_class}>\n"; echo "\t\t\t<td><span class=\"move-handle\">{$row}</span></td>"; echo "<td><input type=\"checkbox\" class=\"hide-if-no-js\" /><input type=\"hidden\" class=\"visibility\" name=\"table[visibility][rows][]\" value=\"{$visibility['rows'][$row_idx]}\" /></td>"; foreach ($row_data as $col_idx => $cell) { $column = TablePress::number_to_letter($col_idx + 1); $column_class = ''; if (0 === $visibility['columns'][$col_idx]) { $column_class = ' class="column-hidden"'; } $cell = esc_textarea($cell); // sanitize, so that HTML is possible in table cells echo "<td{$column_class}><textarea name=\"table[data][{$row_idx}][{$col_idx}]\" id=\"cell-{$column}{$row}\" rows=\"1\">{$cell}</textarea></td>"; } echo "<td><span class=\"move-handle\">{$row}</span></td>\n"; echo "\t\t</tr>\n"; } ?> </tbody> </table> <input type="hidden" id="number-rows" name="table[number][rows]" value="<?php echo $rows; ?> " /> <input type="hidden" id="number-columns" name="table[number][columns]" value="<?php echo $columns; ?> " /> <?php }
/** * Test that columns positions (numbers) are converted to their names (letters) properly. * * @dataProvider data_number_to_letter * * @since 1.1.0 * * @param int $number Number to convert. * @param string $letter Conversion result letter. */ public function test_number_to_letter($number, $letter) { $this->assertSame($letter, TablePress::number_to_letter($number)); }