1<?php
2/**
3 * DokuWiki Plugin parserfunctions (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Daniel "Nerun" Rodrigues <danieldiasr@gmail.com>
7 * @created: Sat, 09 Dec 2023 14:59 -0300
8 *
9 * This is my first plugin, and I don't even know PHP well, that's why it's full
10 * of comments, but I'll leave it that way so I can consult it in the future.
11 *
12 */
13use dokuwiki\Extension\SyntaxPlugin;
14use dokuwiki\Utf8\PhpString;
15
16class syntax_plugin_parserfunctions extends SyntaxPlugin
17{
18    /** @inheritDoc */
19    public function getType()
20    {
21        /* READ: https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
22         * substition  — modes where the token is simply replaced – they can not
23         * contain any other modes
24         */
25        return 'substition';
26    }
27
28    /** @inheritDoc */
29    public function getPType()
30    {
31        /* READ: https://www.dokuwiki.org/devel:syntax_plugins
32         * normal — (default value, will be used if the method is not overridden)
33         *          The plugin output will be inside a paragraph (or another block
34         *           element), no paragraphs will be inside.
35         */
36        return 'normal';
37    }
38
39    /** @inheritDoc */
40    public function getSort()
41    {
42        /* READ: https://www.dokuwiki.org/devel:parser:getsort_list
43         * Don't understand exactly what it does, need more study.
44         *
45         * Should go after Templater (302) and WST (319) plugin, to be able to
46         * render @parameter@ and {{{parameter}}}.
47         */
48        return 320;
49    }
50
51    /** @inheritDoc */
52    public function connectTo($mode)
53    {
54        /* READ: https://www.dokuwiki.org/devel:syntax_plugins#patterns
55         * Regex accepts any alphabetical function name
56         * but not nested functions
57         */
58        $this->Lexer->addSpecialPattern('\{\{#[[:alpha:]]+:[^(\{\{#)(#\}\})]+#\}\}', $mode, 'plugin_parserfunctions');
59//        $this->Lexer->addEntryPattern('<FIXME>', $mode, 'plugin_parserfunctions');
60    }
61
62//    /** @inheritDoc */
63//    public function postConnect()
64//    {
65//        $this->Lexer->addExitPattern('</FIXME>', 'plugin_parserfunctions');
66//    }
67
68    /** @inheritDoc */
69    public function handle($match, $state, $pos, Doku_Handler $handler)
70    {
71        /* READ: https://www.dokuwiki.org/devel:syntax_plugins#handle_method
72         * This is the part of your plugin which should do all the work. Before
73         * DokuWiki renders the wiki page it creates a list of instructions for
74         * the renderer. The plugin's handle() method generates the render
75         * instructions for the plugin's own syntax mode. At some later time,
76         * these will be interpreted by the plugin's render() method.
77         *
78         * Parameters:
79         *
80         *   $match   (string)  — The text matched by the patterns
81         *
82         *   $state   (int)     — The lexer state for the match, representing the type
83         *                        of pattern which triggered this call to handle():
84         *                        DOKU_LEXER_SPECIAL — a pattern set by addSpecialPattern()
85         *
86         *   $pos     (int)     — The character position of the matched text.
87         *
88         *   $handler (Doku_Handler) — Object Reference to the Doku_Handler object.
89         */
90
91        // Exit if no text matched by the patterns.
92        if (empty($match)) {
93            return false;
94        }
95
96        /* Function name: "if", "ifeq", "ifexpr" etc.
97         * strtolower converts only ASCII; PhpString::strtolower supports UTF-8,
98         * added by "use dokuwiki\Utf8\PhpString;" at line 15. The function
99         * names will probably only use ASCII characters, but it's a precaution.
100         * The 's' at the end of '/pattern/s' adds support to multiline strings.
101         */
102        $func_name = preg_replace('/\{\{#([[:alpha:](&#)]+):.*#\}\}/s', '\1', $match);
103        $func_name = PhpString::strtolower($func_name);
104
105        // Delete delimiters "{{#functionname:" and "#}}".
106        // The 's' at the end of '/pattern/s' adds support to multiline strings.
107        $parts = preg_replace('/\{\{#[[:alpha:]]+:(.*)#\}\}/s', '\1', $match);
108
109        // Create list with all parameters splited by "|" pipe
110        // Could use "preg_split('/\|/', $parts)" instead
111        $params = explode('|', $parts);
112
113        // Stripping whitespace from the beginning and end of strings
114        foreach ($params as &$value) {
115            $value = trim($value);
116        }
117
118        // ==================== FINALLY: do the work! ====================
119        switch($func_name){
120            // To add a new function, first add a "case" below, make it call a
121            // funtion, then write the funtion.
122            case 'if':
123                $func_result = $this->_IF($params, $func_name);
124                break;
125            case 'ifeq':
126                $func_result = $this->_IFEQ($params, $func_name);
127                break;
128            case 'switch':
129                $func_result = $this->_SWITCH($params);
130                break;
131            default:
132                $func_result = ' <span style="color: red;">' . $this->getLang('error') .
133                               ' <code>'. $func_name . '</code>: ' .
134                               $this->getLang('no_such_function') . ' </span>';
135                break;
136        }
137
138        // The instructions provided to the render() method:
139        return $func_result;
140    }
141
142    /** @inheritDoc */
143    public function render($mode, Doku_Renderer $renderer, $data)
144    {
145        /* READ: https://www.dokuwiki.org/devel:syntax_plugins#render_method
146         * The part of the plugin that provides the output for the final web page.
147         *
148         * Parameters:
149         *
150         *   $mode     — Name for the format mode of the final output produced by the
151         *               renderer.
152         *
153         *   $renderer — Give access to the object Doku_Renderer, which contains useful
154         *               functions and values.
155         *
156         *   $data     — An array containing the instructions previously prepared
157         *               and returned by the plugin's own handle() method. The render()
158         *               must interpret the instruction and generate the appropriate
159         *               output.
160         */
161
162        if ($mode !== 'xhtml') {
163            return false;
164        }
165
166        if (!$data) {
167            return false;
168        }
169
170        // escape sequences
171        $data = $this->_escape($data);
172
173        // Do not use <div></div> because we need inline substitution!
174		$renderer->doc .= $data;
175
176        return true;
177    }
178
179    /**
180     * ========== #IF
181     * {{#if: 1st parameter | 2nd parameter | 3rd optional parameter #}}
182     * {{#if: test string | value if test string is not empty | value if test string is empty (or only white space) #}}
183     */
184    function _IF($params, $func_name)
185    {
186        if ( count($params) < 2 ) {
187            $result = ' <span style="color: red;">' . $this->getLang('error') .
188                      ' <code>'. $func_name . '</code>: ' . $this->getLang('not_enough_params') .
189                      ' </span>';
190        } else {
191            if ( !empty($params[0]) ) {
192                $result = $params[1];
193            } else {
194                if ( !empty($params[2]) ) {
195                    $result = $params[2];
196                } else {
197                    /**
198                     * The last parameter (false) must have been intentionally omitted:
199                     * user wants the result to be null if the test string is empty.
200                     */
201                    $result = null;
202                }
203            }
204        }
205
206        return $result;
207    }
208
209    /**
210     * ========== #IFEQ
211     * {{#ifeq: 1st parameter | 2nd parameter | 3rd parameter | 4th parameter #}}
212     * {{#ifeq: string 1 | string 2 | value if identical | value if different #}}
213     */
214    function _IFEQ($params, $func_name)
215    {
216        if ( count($params) < 4 ) {
217            $result = ' <span style="color: red;">' . $this->getLang('error') .
218                      ' <code>'. $func_name . '</code>: ' . $this->getLang('not_enough_params') .
219                      ' </span>';
220        } else {
221            if ( $params[0] == $params[1] ) {
222                $result = $params[2];
223            } else {
224                $result = $params[3];
225            }
226        }
227
228        return $result;
229    }
230
231    /**
232     * ========== #SWITCH
233     * {{#switch: comparison string
234     * | case = result
235     * | case = result
236     * | ...
237     * | case = result
238     * | default result
239     * #}}
240     */
241    function _SWITCH($params)
242    {
243        /**
244         * Then:
245         *
246         * "$params":
247         *      (
248         *          [0] => test string
249         *          [1] => case 1 = value 1
250         *          [2] => case 2 = value 2
251         *          [3] => case 3 = value 3
252         *          [4] => default value
253         *      )
254         */
255
256        $cases_kv = [];
257        $test_and_default_string = [];
258
259        foreach ( $params as $value ){
260	        $value = preg_split('/=/', $value);
261        	if ( isset($value[1]) ) {
262        		$cases_kv[trim($value[0])] = trim($value[1]);
263        	} else {
264        		$test_and_default_string[] = trim($value[0]);
265        	}
266        }
267
268        /**
269         * And now:
270         *
271         * "$cases_kv":
272         *      (
273         *          [case 1] => value 1
274         *          [case 2] => value 2
275         *          [case 3] => value 3
276         *      )
277         *
278         * "$test_and_default_string":
279         *      (
280         *          [0] => test string
281         *          [1] => default value
282         *      )
283         */
284
285        if ( array_key_exists($test_and_default_string[0], $cases_kv) ) {
286        	$result = $cases_kv[$test_and_default_string[0]];
287        } else {
288        	$result = $test_and_default_string[1] ?? '';
289        }
290
291        return $result;
292    }
293
294    /**
295     * Escape sequence handling
296     */
297    function _escape($data){
298        /**
299         * To add more escapes, please refer to:
300         * https://www.freeformatter.com/html-entities.html
301         *
302         * Always use "&&num;__number__;" instead of "&#;__number__;"
303         *
304         * Number sign "#" can be escaped with "&num;" and don't need to be
305         * added to the array below.
306         */
307        $escapes = array(
308            "&&num;61;"  => "=",
309            "&&num;123;" => "{",
310            "&&num;124;" => "|",
311            "&&num;125;" => "}"
312        );
313
314        foreach ( $escapes as $key => $value ) {
315            $data = preg_replace("/$key/s", $value, $data);
316        }
317
318        return $data;
319    }
320}
321// vim:ts=4:sw=4:et:enc=utf-8:
322