1<?php 2 3/** 4 * Plugin AVMathTable 5 * 6 * Adds math to columns for Dokuwiki tables. 7 * Supported Math: 8 * AVG - Calculate average of the column. 9 * SUM - Calculate total of the column. 10 * CNT - Number of numeric values in the column above this cell. 11 * 12 * USAGE: 13<mathtable> 14^ Name ^ Deposited ^ Balance ^ 15| John | 25 | 500 | 16| Mary | 40 | 680 | 17| Lex | 10 | 140 | 18| TOTAL| =AVG | =SUM | 19</mathtable> 20 * 21 * @license GPL-2.0 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) 22 * @author Sherri W. (http://syntaxseed.com) 23 */ 24 25if (!defined('DOKU_INC')) { 26 define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/'); 27} 28if (!defined('DOKU_PLUGIN')) { 29 define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 30} 31 32 33/** 34 * All DokuWiki plugins to extend the parser/rendering mechanism 35 * need to inherit from this class 36 */ 37class syntax_plugin_avmathtable extends DokuWiki_Syntax_Plugin 38{ 39 private array $infoTable = []; 40 41 /** 42 * What kind of syntax are we? 43 */ 44 public function getType() 45 { 46 return 'substition'; 47 } 48 49 /** 50 * Where to sort in? 51 */ 52 public function getSort() 53 { 54 return 999; 55 } 56 57 58 /** 59 * Connect pattern to lexer 60 */ 61 public function connectTo($mode) 62 { 63 $this->Lexer->addEntryPattern('\<mathtable\>', $mode, 'plugin_avmathtable'); 64 } 65 66 public function postConnect() 67 { 68 $this->Lexer->addExitPattern('\</mathtable\>', 'plugin_avmathtable'); 69 } 70 71 72 /** 73 * Handle the match 74 */ 75 public function handle($match, $state, $pos, Doku_Handler $handler) 76 { 77 switch ($state) { 78 case DOKU_LEXER_ENTER: 79 return array($state, ''); 80 case DOKU_LEXER_MATCHED: 81 break; 82 case DOKU_LEXER_UNMATCHED: 83 84 $tables = $this->parseTable($match); 85 [$table, $info] = $tables; 86 87 $this->infoTable = $info; 88 //$match = $table; 89 90 return array($state, $tables); 91 92 case DOKU_LEXER_EXIT: 93 return array($state, ''); 94 case DOKU_LEXER_SPECIAL: 95 break; 96 } 97 return array(); 98 } 99 100 101 /** 102 * Create output 103 */ 104 public function render($mode, Doku_Renderer $renderer, $data) 105 { 106 if ($mode == 'xhtml') { 107 108 if (empty($data[1])) { 109 return; 110 } 111 112 list($state, $tables) = $data; 113 114 // echo('<pre>'); 115 // var_dump($state, $tables); 116 // echo('</pre>'); 117 118 [$match, $info] = $tables; 119 $this->infoTable = $info; 120 121 switch ($state) { 122 case DOKU_LEXER_ENTER: 123 //$renderer->doc .= "<div class='avMathTable'>"; 124 break; 125 126 case DOKU_LEXER_MATCHED: 127 break; 128 129 case DOKU_LEXER_UNMATCHED: 130 $info = []; 131 132 $output = $this->renderArrayIntoTable($match); 133 $html = p_render('xhtml', p_get_instructions($output), $info); 134 135 $renderer->doc .= "<div class='avmathtable'>" . $html . "</div>"; 136 137 break; 138 139 case DOKU_LEXER_EXIT: 140 //$renderer->doc .= "</div>"; 141 break; 142 143 case DOKU_LEXER_SPECIAL: 144 break; 145 } 146 return true; 147 } 148 return false; 149 } 150 151 /** 152 * Parse the table syntax into an array. 153 */ 154 private function parseTable(string $tableSyntax): array 155 { 156 // 1) Parse the wiki table text into a collection of instructions. 157 $calls = p_get_instructions($tableSyntax); 158 159 // echo('<pre>'); 160 // var_dump($calls); 161 // echo('</pre>'); 162 163 // 2) Convert to array 164 $table = []; 165 $row = []; 166 $cell = null; 167 168 $infoTable = []; // Keep track of things like if it's a header cell or a regular cell, alignment, etc. 169 $infoRow = []; 170 $infoCell = ['type' => 'plain', 'alignment' => 'left']; 171 172 foreach ($calls as $call) { 173 [$cmd, $data] = $call; 174 175 switch ($cmd) { 176 177 case 'tableheader_open': 178 $cell = ''; 179 $infoCell = ['type' => 'header', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 180 181 break; 182 case 'tablecell_open': 183 $cell = ''; 184 $infoCell = ['type' => 'plain', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 185 break; 186 187 case 'cdata': 188 if ($cell !== null) { 189 $cell .= $data[0]; 190 } 191 break; 192 193 case 'tableheader_close': 194 case 'tablecell_close': 195 $row[] = trim($cell); 196 $infoRow[] = $infoCell; 197 $cell = null; // Reset 198 $infoCell = ['type' => 'plain', 'alignment' => 'left']; // Reset 199 break; 200 201 case 'tablerow_close': 202 $table[] = $row; 203 $infoTable[] = $infoRow; 204 $row = []; 205 $infoRow = []; 206 break; 207 } 208 } 209 210 return [$table, $infoTable]; 211 } 212 213 214 private function renderArrayIntoTable(array $table): string 215 { 216 $output = ''; 217 218 $columnData = []; 219 $rowNum = 1; 220 // Create each row: 221 foreach ($table as $i => $row) { 222 // Create each cell: 223 foreach ($row as $j => $cell) { 224 // Initialize info about this column. 225 if ($rowNum == 1) { 226 $columnData[$j] = ['sum' => 0, 'count' => 0, 'precision' => 0]; 227 } 228 229 // Open up the cell wiki syntax. 230 if ($this->infoTable[$i][$j]['type'] == 'header') { 231 $output .= "^ "; 232 } else { 233 $output .= "| "; 234 } 235 if ($this->infoTable[$i][$j]['alignment'] == 'right' || $this->infoTable[$i][$j]['alignment'] == 'center') { 236 $output .= " "; 237 } 238 239 // Gather info about the numbers in this cell. 240 if (is_numeric($cell)) { 241 if ((int)$cell == $cell) { 242 $columnData[$j]['sum'] += intval($cell); 243 $columnData[$j]['count'] += 1; 244 } elseif ((float)$cell == $cell) { 245 $columnData[$j]['sum'] += floatval($cell); 246 $columnData[$j]['count'] += 1; 247 $columnData[$j]['precision'] = max($columnData[$j]['precision'], $this->countDecimalPlaces((float)$cell)); 248 } 249 } 250 251 // Insert the cell value. TODO : Handle special math features. 252 $output .= $this->insertCellData($cell, $columnData, $rowNum, $j); 253 254 255 // Close up the cell wiki syntax. 256 if ($this->infoTable[$i][$j]['alignment'] == 'left' || $this->infoTable[$i][$j]['alignment'] == 'center') { 257 $output .= " "; 258 } 259 if ($this->infoTable[$i][$j]['type'] == 'header') { 260 $output .= " ^"; 261 } else { 262 $output .= " |"; 263 } 264 } 265 $output .= "\n"; // End of a row. 266 $rowNum++; 267 } 268 269 //$dump = var_export($table, true); 270 271 return $output; 272 } 273 274 /** 275 * Put the value back in the cell. Substitute math where applicable. 276 */ 277 private function insertCellData(mixed $cell, array $columnData, int $rowNum, int $colNum) 278 { 279 280 // echo('<pre>'); 281 // var_dump($columnData); 282 // echo('</pre>'); 283 284 switch (trim($cell)) { 285 case '=SUM': 286 return '<span class="avmathtablevalue">' . $columnData[$colNum]['sum'] . '</span>'; 287 break; 288 case '=CNT': 289 return '<span class="avmathtablevalue">' . $columnData[$colNum]['count'] . '</span>'; 290 break; 291 case '=AVG': 292 return '<span class="avmathtablevalue">' . round(($columnData[$colNum]['sum'] / $columnData[$colNum]['count']), $columnData[$colNum]['precision']+1) . '</span>'; 293 break; 294 default: 295 return $cell; 296 } 297 } 298 299 private function countDecimalPlaces(float $num): int 300 { 301 $fNumber = floatval($num); 302 for ($iDecimals = 0; $fNumber != round($fNumber, $iDecimals); $iDecimals++); 303 return $iDecimals; 304 } 305} // End class 306