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