14b3ddbf5SSyntaxseed<?php 24b3ddbf5SSyntaxseed 34b3ddbf5SSyntaxseed/** 44b3ddbf5SSyntaxseed * Plugin AVMathTable 54b3ddbf5SSyntaxseed * 64b3ddbf5SSyntaxseed * Adds math to columns for Dokuwiki tables. 74b3ddbf5SSyntaxseed * Supported Math: 84b3ddbf5SSyntaxseed * AVG - Calculate average of the column. 94b3ddbf5SSyntaxseed * SUM - Calculate total of the column. 104b3ddbf5SSyntaxseed * CNT - Number of numeric values in the column above this cell. 114a9b1e96SSyntaxseed * MAX - Maximum value in the column. 124a9b1e96SSyntaxseed * MIN - Minimum value in the column. 134b3ddbf5SSyntaxseed * 144b3ddbf5SSyntaxseed * USAGE: 154b3ddbf5SSyntaxseed<mathtable> 164b3ddbf5SSyntaxseed^ Name ^ Deposited ^ Balance ^ 174b3ddbf5SSyntaxseed| John | 25 | 500 | 184b3ddbf5SSyntaxseed| Mary | 40 | 680 | 194b3ddbf5SSyntaxseed| Lex | 10 | 140 | 204b3ddbf5SSyntaxseed| TOTAL| =AVG | =SUM | 214b3ddbf5SSyntaxseed</mathtable> 224b3ddbf5SSyntaxseed * 234b3ddbf5SSyntaxseed * @license GPL-2.0 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) 244b3ddbf5SSyntaxseed * @author Sherri W. (http://syntaxseed.com) 254b3ddbf5SSyntaxseed */ 264b3ddbf5SSyntaxseed 274b3ddbf5SSyntaxseedif (!defined('DOKU_INC')) { 284b3ddbf5SSyntaxseed define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/'); 294b3ddbf5SSyntaxseed} 304b3ddbf5SSyntaxseedif (!defined('DOKU_PLUGIN')) { 314b3ddbf5SSyntaxseed define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 324b3ddbf5SSyntaxseed} 334b3ddbf5SSyntaxseed 344b3ddbf5SSyntaxseed 354b3ddbf5SSyntaxseed/** 364b3ddbf5SSyntaxseed * All DokuWiki plugins to extend the parser/rendering mechanism 374b3ddbf5SSyntaxseed * need to inherit from this class 384b3ddbf5SSyntaxseed */ 394b3ddbf5SSyntaxseedclass syntax_plugin_avmathtable extends DokuWiki_Syntax_Plugin 404b3ddbf5SSyntaxseed{ 414b3ddbf5SSyntaxseed private array $infoTable = []; 424b3ddbf5SSyntaxseed 434b3ddbf5SSyntaxseed /** 444b3ddbf5SSyntaxseed * What kind of syntax are we? 454b3ddbf5SSyntaxseed */ 464b3ddbf5SSyntaxseed public function getType() 474b3ddbf5SSyntaxseed { 484b3ddbf5SSyntaxseed return 'substition'; 494b3ddbf5SSyntaxseed } 504b3ddbf5SSyntaxseed 514b3ddbf5SSyntaxseed /** 524b3ddbf5SSyntaxseed * Where to sort in? 534b3ddbf5SSyntaxseed */ 544b3ddbf5SSyntaxseed public function getSort() 554b3ddbf5SSyntaxseed { 564b3ddbf5SSyntaxseed return 999; 574b3ddbf5SSyntaxseed } 584b3ddbf5SSyntaxseed 594b3ddbf5SSyntaxseed 604b3ddbf5SSyntaxseed /** 614b3ddbf5SSyntaxseed * Connect pattern to lexer 624b3ddbf5SSyntaxseed */ 634b3ddbf5SSyntaxseed public function connectTo($mode) 644b3ddbf5SSyntaxseed { 654b3ddbf5SSyntaxseed $this->Lexer->addEntryPattern('\<mathtable\>', $mode, 'plugin_avmathtable'); 664b3ddbf5SSyntaxseed } 674b3ddbf5SSyntaxseed 684b3ddbf5SSyntaxseed public function postConnect() 694b3ddbf5SSyntaxseed { 704b3ddbf5SSyntaxseed $this->Lexer->addExitPattern('\</mathtable\>', 'plugin_avmathtable'); 714b3ddbf5SSyntaxseed } 724b3ddbf5SSyntaxseed 734b3ddbf5SSyntaxseed 744b3ddbf5SSyntaxseed /** 754b3ddbf5SSyntaxseed * Handle the match 764b3ddbf5SSyntaxseed */ 774b3ddbf5SSyntaxseed public function handle($match, $state, $pos, Doku_Handler $handler) 784b3ddbf5SSyntaxseed { 794b3ddbf5SSyntaxseed switch ($state) { 804b3ddbf5SSyntaxseed case DOKU_LEXER_ENTER: 814b3ddbf5SSyntaxseed return array($state, ''); 824b3ddbf5SSyntaxseed case DOKU_LEXER_MATCHED: 834b3ddbf5SSyntaxseed break; 844b3ddbf5SSyntaxseed case DOKU_LEXER_UNMATCHED: 854b3ddbf5SSyntaxseed 864b3ddbf5SSyntaxseed $tables = $this->parseTable($match); 874b3ddbf5SSyntaxseed [$table, $info] = $tables; 884b3ddbf5SSyntaxseed 894b3ddbf5SSyntaxseed $this->infoTable = $info; 904b3ddbf5SSyntaxseed 914b3ddbf5SSyntaxseed return array($state, $tables); 924b3ddbf5SSyntaxseed 934b3ddbf5SSyntaxseed case DOKU_LEXER_EXIT: 944b3ddbf5SSyntaxseed return array($state, ''); 954b3ddbf5SSyntaxseed case DOKU_LEXER_SPECIAL: 964b3ddbf5SSyntaxseed break; 974b3ddbf5SSyntaxseed } 984b3ddbf5SSyntaxseed return array(); 994b3ddbf5SSyntaxseed } 1004b3ddbf5SSyntaxseed 1014b3ddbf5SSyntaxseed 1024b3ddbf5SSyntaxseed /** 1034b3ddbf5SSyntaxseed * Create output 1044b3ddbf5SSyntaxseed */ 1054b3ddbf5SSyntaxseed public function render($mode, Doku_Renderer $renderer, $data) 1064b3ddbf5SSyntaxseed { 1074b3ddbf5SSyntaxseed if ($mode == 'xhtml') { 1084b3ddbf5SSyntaxseed 1094b3ddbf5SSyntaxseed if (empty($data[1])) { 1104b3ddbf5SSyntaxseed return; 1114b3ddbf5SSyntaxseed } 1124b3ddbf5SSyntaxseed 1134b3ddbf5SSyntaxseed list($state, $tables) = $data; 1144b3ddbf5SSyntaxseed 1154b3ddbf5SSyntaxseed [$match, $info] = $tables; 1164b3ddbf5SSyntaxseed $this->infoTable = $info; 1174b3ddbf5SSyntaxseed 1184b3ddbf5SSyntaxseed switch ($state) { 1194b3ddbf5SSyntaxseed case DOKU_LEXER_ENTER: 1204b3ddbf5SSyntaxseed //$renderer->doc .= "<div class='avMathTable'>"; 1214b3ddbf5SSyntaxseed break; 1224b3ddbf5SSyntaxseed 1234b3ddbf5SSyntaxseed case DOKU_LEXER_MATCHED: 1244b3ddbf5SSyntaxseed break; 1254b3ddbf5SSyntaxseed 1264b3ddbf5SSyntaxseed case DOKU_LEXER_UNMATCHED: 1274b3ddbf5SSyntaxseed $info = []; 1284b3ddbf5SSyntaxseed 1294b3ddbf5SSyntaxseed $output = $this->renderArrayIntoTable($match); 1304b3ddbf5SSyntaxseed $html = p_render('xhtml', p_get_instructions($output), $info); 1314b3ddbf5SSyntaxseed 1324b3ddbf5SSyntaxseed $renderer->doc .= "<div class='avmathtable'>" . $html . "</div>"; 1334b3ddbf5SSyntaxseed 1344b3ddbf5SSyntaxseed break; 1354b3ddbf5SSyntaxseed 1364b3ddbf5SSyntaxseed case DOKU_LEXER_EXIT: 1374b3ddbf5SSyntaxseed //$renderer->doc .= "</div>"; 1384b3ddbf5SSyntaxseed break; 1394b3ddbf5SSyntaxseed 1404b3ddbf5SSyntaxseed case DOKU_LEXER_SPECIAL: 1414b3ddbf5SSyntaxseed break; 1424b3ddbf5SSyntaxseed } 1434b3ddbf5SSyntaxseed return true; 1444b3ddbf5SSyntaxseed } 1454b3ddbf5SSyntaxseed return false; 1464b3ddbf5SSyntaxseed } 1474b3ddbf5SSyntaxseed 1484b3ddbf5SSyntaxseed /** 1494b3ddbf5SSyntaxseed * Parse the table syntax into an array. 1504b3ddbf5SSyntaxseed */ 1514b3ddbf5SSyntaxseed private function parseTable(string $tableSyntax): array 1524b3ddbf5SSyntaxseed { 1534a9b1e96SSyntaxseed // Parse the wiki table text into a collection of instructions. 1544b3ddbf5SSyntaxseed $calls = p_get_instructions($tableSyntax); 1554b3ddbf5SSyntaxseed 1564a9b1e96SSyntaxseed // Convert to a multidimensional array 1574b3ddbf5SSyntaxseed $table = []; 1584b3ddbf5SSyntaxseed $row = []; 1594b3ddbf5SSyntaxseed $cell = null; 1604b3ddbf5SSyntaxseed 1614b3ddbf5SSyntaxseed $infoTable = []; // Keep track of things like if it's a header cell or a regular cell, alignment, etc. 1624b3ddbf5SSyntaxseed $infoRow = []; 1634b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => 'left']; 1644b3ddbf5SSyntaxseed 1654b3ddbf5SSyntaxseed foreach ($calls as $call) { 1664b3ddbf5SSyntaxseed [$cmd, $data] = $call; 1674b3ddbf5SSyntaxseed 1684b3ddbf5SSyntaxseed switch ($cmd) { 1694b3ddbf5SSyntaxseed 1704b3ddbf5SSyntaxseed case 'tableheader_open': 1714b3ddbf5SSyntaxseed $cell = ''; 1724b3ddbf5SSyntaxseed $infoCell = ['type' => 'header', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 1734b3ddbf5SSyntaxseed 1744b3ddbf5SSyntaxseed break; 1754b3ddbf5SSyntaxseed case 'tablecell_open': 1764b3ddbf5SSyntaxseed $cell = ''; 1774b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 1784b3ddbf5SSyntaxseed break; 1794b3ddbf5SSyntaxseed 1804b3ddbf5SSyntaxseed case 'cdata': 1814b3ddbf5SSyntaxseed if ($cell !== null) { 1824b3ddbf5SSyntaxseed $cell .= $data[0]; 1834b3ddbf5SSyntaxseed } 1844b3ddbf5SSyntaxseed break; 1854b3ddbf5SSyntaxseed 1864b3ddbf5SSyntaxseed case 'tableheader_close': 1874b3ddbf5SSyntaxseed case 'tablecell_close': 1884b3ddbf5SSyntaxseed $row[] = trim($cell); 1894b3ddbf5SSyntaxseed $infoRow[] = $infoCell; 1904b3ddbf5SSyntaxseed $cell = null; // Reset 1914b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => 'left']; // Reset 1924b3ddbf5SSyntaxseed break; 1934b3ddbf5SSyntaxseed 1944b3ddbf5SSyntaxseed case 'tablerow_close': 1954b3ddbf5SSyntaxseed $table[] = $row; 1964b3ddbf5SSyntaxseed $infoTable[] = $infoRow; 1974b3ddbf5SSyntaxseed $row = []; 1984b3ddbf5SSyntaxseed $infoRow = []; 1994b3ddbf5SSyntaxseed break; 2004b3ddbf5SSyntaxseed } 2014b3ddbf5SSyntaxseed } 2024b3ddbf5SSyntaxseed 2034b3ddbf5SSyntaxseed return [$table, $infoTable]; 2044b3ddbf5SSyntaxseed } 2054b3ddbf5SSyntaxseed 2064b3ddbf5SSyntaxseed 2074b3ddbf5SSyntaxseed private function renderArrayIntoTable(array $table): string 2084b3ddbf5SSyntaxseed { 2094b3ddbf5SSyntaxseed $output = ''; 2104b3ddbf5SSyntaxseed 2114b3ddbf5SSyntaxseed $columnData = []; 2124b3ddbf5SSyntaxseed $rowNum = 1; 2134b3ddbf5SSyntaxseed // Create each row: 2144b3ddbf5SSyntaxseed foreach ($table as $i => $row) { 2154b3ddbf5SSyntaxseed // Create each cell: 2164b3ddbf5SSyntaxseed foreach ($row as $j => $cell) { 2174b3ddbf5SSyntaxseed // Initialize info about this column. 2184b3ddbf5SSyntaxseed if ($rowNum == 1) { 2194a9b1e96SSyntaxseed $columnData[$j] = ['sum' => 0, 'count' => 0, 'precision' => 0, 'min' => null, 'max' => null]; 2204b3ddbf5SSyntaxseed } 2214b3ddbf5SSyntaxseed 2224b3ddbf5SSyntaxseed // Open up the cell wiki syntax. 2234b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['type'] == 'header') { 2244b3ddbf5SSyntaxseed $output .= "^ "; 2254b3ddbf5SSyntaxseed } else { 2264b3ddbf5SSyntaxseed $output .= "| "; 2274b3ddbf5SSyntaxseed } 2284b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['alignment'] == 'right' || $this->infoTable[$i][$j]['alignment'] == 'center') { 2294b3ddbf5SSyntaxseed $output .= " "; 2304b3ddbf5SSyntaxseed } 2314b3ddbf5SSyntaxseed 2324b3ddbf5SSyntaxseed // Gather info about the numbers in this cell. 2334b3ddbf5SSyntaxseed if (is_numeric($cell)) { 2344a9b1e96SSyntaxseed $columnData[$j]['count'] += 1; 2354a9b1e96SSyntaxseed $columnData[$j]['precision'] = max($columnData[$j]['precision'], $this->countDecimalPlaces($cell)); 2364b3ddbf5SSyntaxseed if ((int)$cell == $cell) { 2374a9b1e96SSyntaxseed $numericCell = intval($cell); 2384b3ddbf5SSyntaxseed } elseif ((float)$cell == $cell) { 2394a9b1e96SSyntaxseed $numericCell = floatval($cell); 2404b3ddbf5SSyntaxseed } 2414a9b1e96SSyntaxseed $columnData[$j]['sum'] += $numericCell; 2424a9b1e96SSyntaxseed $columnData[$j]['max'] = is_null($columnData[$j]['max']) ? $numericCell : max($columnData[$j]['max'], $numericCell); 2434a9b1e96SSyntaxseed $columnData[$j]['min'] = is_null($columnData[$j]['min']) ? $numericCell : min($columnData[$j]['min'], $numericCell); 2444b3ddbf5SSyntaxseed } 2454b3ddbf5SSyntaxseed 2464b3ddbf5SSyntaxseed // Insert the cell value. TODO : Handle special math features. 2474b3ddbf5SSyntaxseed $output .= $this->insertCellData($cell, $columnData, $rowNum, $j); 2484b3ddbf5SSyntaxseed 2494b3ddbf5SSyntaxseed 2504b3ddbf5SSyntaxseed // Close up the cell wiki syntax. 2514b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['alignment'] == 'left' || $this->infoTable[$i][$j]['alignment'] == 'center') { 2524b3ddbf5SSyntaxseed $output .= " "; 2534b3ddbf5SSyntaxseed } 2544b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['type'] == 'header') { 2554b3ddbf5SSyntaxseed $output .= " ^"; 2564b3ddbf5SSyntaxseed } else { 2574b3ddbf5SSyntaxseed $output .= " |"; 2584b3ddbf5SSyntaxseed } 2594b3ddbf5SSyntaxseed } 2604b3ddbf5SSyntaxseed $output .= "\n"; // End of a row. 2614b3ddbf5SSyntaxseed $rowNum++; 2624b3ddbf5SSyntaxseed } 2634b3ddbf5SSyntaxseed 2644b3ddbf5SSyntaxseed //$dump = var_export($table, true); 2654b3ddbf5SSyntaxseed 2664b3ddbf5SSyntaxseed return $output; 2674b3ddbf5SSyntaxseed } 2684b3ddbf5SSyntaxseed 2694b3ddbf5SSyntaxseed /** 2704b3ddbf5SSyntaxseed * Put the value back in the cell. Substitute math where applicable. 2714b3ddbf5SSyntaxseed */ 2724b3ddbf5SSyntaxseed private function insertCellData(mixed $cell, array $columnData, int $rowNum, int $colNum) 2734b3ddbf5SSyntaxseed { 2744b3ddbf5SSyntaxseed 2754b3ddbf5SSyntaxseed // echo('<pre>'); 2764b3ddbf5SSyntaxseed // var_dump($columnData); 2774b3ddbf5SSyntaxseed // echo('</pre>'); 2784b3ddbf5SSyntaxseed 2794b3ddbf5SSyntaxseed switch (trim($cell)) { 2804b3ddbf5SSyntaxseed case '=SUM': 2814a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . number_format($columnData[$colNum]['sum'], $columnData[$colNum]['precision']) .'</span>'; 2824b3ddbf5SSyntaxseed break; 2834b3ddbf5SSyntaxseed case '=CNT': 2844b3ddbf5SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['count'] . '</span>'; 2854b3ddbf5SSyntaxseed break; 2864b3ddbf5SSyntaxseed case '=AVG': 2874a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . number_format(round(($columnData[$colNum]['sum'] / $columnData[$colNum]['count']), $columnData[$colNum]['precision']+1), $columnData[$colNum]['precision']+1) . '</span>'; 2884a9b1e96SSyntaxseed break; 2894a9b1e96SSyntaxseed case '=MAX': 2904a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['max'] . '</span>'; 2914a9b1e96SSyntaxseed break; 2924a9b1e96SSyntaxseed case '=MIN': 2934a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['min'] . '</span>'; 2944b3ddbf5SSyntaxseed break; 2954b3ddbf5SSyntaxseed default: 2964b3ddbf5SSyntaxseed return $cell; 2974b3ddbf5SSyntaxseed } 2984b3ddbf5SSyntaxseed } 2994b3ddbf5SSyntaxseed 3004a9b1e96SSyntaxseed /** 3014a9b1e96SSyntaxseed * Count the decimal places after the period. 3024a9b1e96SSyntaxseed * Note that 50.00 gets treated as 50, so we need to count zeros separately and take the largest number. 3034a9b1e96SSyntaxseed */ 3044a9b1e96SSyntaxseed private function countDecimalPlaces(mixed $num): int 3054b3ddbf5SSyntaxseed { 3064a9b1e96SSyntaxseed // Number of 0s after the decimal: 307*3fbc2f2aSSyntaxseed $numZeros = 0; 308*3fbc2f2aSSyntaxseed if (strpos($num, '.') !== false) { 3094a9b1e96SSyntaxseed preg_match("/^(0+)/", explode('.', $num)[1], $matches); 3104a9b1e96SSyntaxseed $numZeros = strlen($matches[0]); 311*3fbc2f2aSSyntaxseed } 3124a9b1e96SSyntaxseed 3134a9b1e96SSyntaxseed // Count number of significant digits after the decimal: 3144b3ddbf5SSyntaxseed $fNumber = floatval($num); 3154b3ddbf5SSyntaxseed for ($iDecimals = 0; $fNumber != round($fNumber, $iDecimals); $iDecimals++); 3164a9b1e96SSyntaxseed return max($iDecimals, $numZeros); 3174b3ddbf5SSyntaxseed } 3184b3ddbf5SSyntaxseed} // End class 319