xref: /plugin/avmathtable/syntax.php (revision 4b3ddbf591e3349073dbca861af625be957f8ea7)
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