12f762695Snerun<?php 22f762695Snerun/** 32f762695Snerun * DokuWiki Plugin parserfunctions (Syntax Component) 42f762695Snerun * 52f762695Snerun * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 62f762695Snerun * @author Daniel "Nerun" Rodrigues <danieldiasr@gmail.com> 7d0ddb69dSnerun * @created Sat, 09 Dec 2023 14:59 -0300 82f762695Snerun * 92f762695Snerun * This is my first plugin, and I don't even know PHP well, that's why it's full 102f762695Snerun * of comments, but I'll leave it that way so I can consult it in the future. 112f762695Snerun * 122f762695Snerun */ 132f762695Snerunuse dokuwiki\Extension\SyntaxPlugin; 1455bd7807Snerunuse dokuwiki\Utf8\PhpString; 152f762695Snerun 162f762695Snerunclass syntax_plugin_parserfunctions extends SyntaxPlugin 172f762695Snerun{ 18d0ddb69dSnerun /** @var helper_plugin_parserfunctions $helper */ 19d0ddb69dSnerun private $helper; 20d0ddb69dSnerun 21d0ddb69dSnerun public function __construct() { 22d0ddb69dSnerun $this->helper = plugin_load('helper', 'parserfunctions'); 23d0ddb69dSnerun } 24d0ddb69dSnerun 252f762695Snerun /** @inheritDoc */ 262f762695Snerun public function getType() 272f762695Snerun { 282f762695Snerun /* READ: https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 292f762695Snerun * substition — modes where the token is simply replaced – they can not 302f762695Snerun * contain any other modes 312f762695Snerun */ 322f762695Snerun return 'substition'; 332f762695Snerun } 342f762695Snerun 352f762695Snerun /** @inheritDoc */ 362f762695Snerun public function getPType() 372f762695Snerun { 382f762695Snerun /* READ: https://www.dokuwiki.org/devel:syntax_plugins 399f6f51efSnerun * normal — Default value, will be used if the method is not overridden. 409f6f51efSnerun * The plugin output will be inside a paragraph (or another 419f6f51efSnerun * block element), no paragraphs will be inside. 422f762695Snerun */ 432f762695Snerun return 'normal'; 442f762695Snerun } 452f762695Snerun 462f762695Snerun /** @inheritDoc */ 472f762695Snerun public function getSort() 482f762695Snerun { 492f762695Snerun /* READ: https://www.dokuwiki.org/devel:parser:getsort_list 502f762695Snerun * Don't understand exactly what it does, need more study. 512f762695Snerun * 529f6f51efSnerun * Must go after Templater (302) and WST (319) plugin, to be able to 530c9cdeecSnerun * render @parameter@ and {{{parameter}}}. 542f762695Snerun */ 552f762695Snerun return 320; 562f762695Snerun } 572f762695Snerun 582f762695Snerun /** @inheritDoc */ 592f762695Snerun public function connectTo($mode) 602f762695Snerun { 612f762695Snerun /* READ: https://www.dokuwiki.org/devel:syntax_plugins#patterns 628bd4c45fSnerun * This pattern accepts any alphanumeric function AND nested functions. 639f6f51efSnerun * 648bd4c45fSnerun * $this->Lexer->addSpecialPattern('\{\{#.+?#\}\}', $mode, 'plugin_parserfunctions'); 658bd4c45fSnerun * Captures nested functions up to level-1: 668bd4c45fSnerun * $this->Lexer->addSpecialPattern('\{\{#[[:alnum:]]+:(?:(?:[^\{#]*?\{\{.*?#\}\})|.*?)+?#\}\}', $mode, 'plugin_parserfunctions'); 678bd4c45fSnerun * 688bd4c45fSnerun * SEE action.php 692f762695Snerun */ 708bd4c45fSnerun } 718bd4c45fSnerun 728bd4c45fSnerun // @author ChatGPT -- Wed, 02 jul 2025 12:04:42 -0300 738bd4c45fSnerun public function resolveFunction($text) 748bd4c45fSnerun { 758bd4c45fSnerun // Remove {{# and #}} delimiters if present 768bd4c45fSnerun if (substr($text, 0, 3) === '{{#' && substr($text, -3) === '#}}') { 778bd4c45fSnerun $text = substr($text, 3, -3); 788bd4c45fSnerun } 798bd4c45fSnerun 808bd4c45fSnerun // Recursively resolves all nested functions from the inside out 818bd4c45fSnerun $text = $this->resolveNestedFunctions($text); 828bd4c45fSnerun 838bd4c45fSnerun // Separates function name and parameters 848bd4c45fSnerun preg_match('/^([[:alnum:]]+):(.*)$/s', $text, $m); 858bd4c45fSnerun 868bd4c45fSnerun if (empty($m[1])) { 878bd4c45fSnerun return $this->helper->formatError('important', 'function_name', 'invalid_syntax'); 888bd4c45fSnerun } 898bd4c45fSnerun 908bd4c45fSnerun $funcName = PhpString::strtolower($m[1]); 918bd4c45fSnerun $paramsText = $m[2] ?? ''; 928bd4c45fSnerun 938bd4c45fSnerun $params = $this->helper->parseParameters($paramsText); 948bd4c45fSnerun 958bd4c45fSnerun switch ($funcName) { 968bd4c45fSnerun case 'if': 978bd4c45fSnerun return $this->_IF($params, $funcName); 988bd4c45fSnerun case 'ifeq': 998bd4c45fSnerun return $this->_IFEQ($params, $funcName); 1008bd4c45fSnerun case 'ifexist': 1018bd4c45fSnerun return $this->_IFEXIST($params, $funcName); 1028bd4c45fSnerun case 'switch': 1038bd4c45fSnerun return $this->_SWITCH($params, $funcName); 104*bca4461fSnerun case 'expr': 105*bca4461fSnerun return $this->_EXPR($params, $funcName); 1068bd4c45fSnerun default: 1078bd4c45fSnerun return $this->helper->formatError('important', $funcName, 'no_such_function'); 1088bd4c45fSnerun } 1098bd4c45fSnerun } 1108bd4c45fSnerun 1118bd4c45fSnerun // @author ChatGPT -- Wed, 02 jul 2025 12:04:42 -0300 1128bd4c45fSnerun private function resolveNestedFunctions($text) 1138bd4c45fSnerun { 1148bd4c45fSnerun $offset = 0; 1158bd4c45fSnerun 1168bd4c45fSnerun while (($start = strpos($text, '{{#', $offset)) !== false) { 1178bd4c45fSnerun $level = 0; 1188bd4c45fSnerun $length = strlen($text); 1198bd4c45fSnerun 1208bd4c45fSnerun for ($i = $start; $i < $length - 2; $i++) { 1218bd4c45fSnerun if (substr($text, $i, 3) === '{{#') { 1228bd4c45fSnerun $level++; 1238bd4c45fSnerun $i += 2; 1248bd4c45fSnerun } elseif (substr($text, $i, 3) === '#}}') { 1258bd4c45fSnerun $level--; 1268bd4c45fSnerun $i += 2; 1278bd4c45fSnerun 1288bd4c45fSnerun if ($level === 0) { 1298bd4c45fSnerun $full = substr($text, $start, $i - $start + 1); 1308bd4c45fSnerun $resolved = $this->resolveFunction($full); 1318bd4c45fSnerun $text = substr_replace($text, $resolved, $start, strlen($full)); 1328bd4c45fSnerun // Start from the beginning because the text has changed 1338bd4c45fSnerun $offset = 0; 1348bd4c45fSnerun continue 2; 1358bd4c45fSnerun } 1368bd4c45fSnerun } 1378bd4c45fSnerun } 1388bd4c45fSnerun 1398bd4c45fSnerun // If you got here, invalid syntax (no #}}) 1408bd4c45fSnerun break; 1418bd4c45fSnerun } 1428bd4c45fSnerun 1438bd4c45fSnerun return $text; 1442f762695Snerun } 1452f762695Snerun 1462f762695Snerun /** @inheritDoc */ 1478bd4c45fSnerun public function handle($match, $state, $pos, Doku_Handler $handler) { 1488bd4c45fSnerun /* This method is only called if the Lexer, in the connectTo() method, 1498bd4c45fSnerun * finds a $match. 1508bd4c45fSnerun * 1518bd4c45fSnerun * READ: https://www.dokuwiki.org/devel:syntax_plugins#handle_method 1522f762695Snerun * This is the part of your plugin which should do all the work. Before 1532f762695Snerun * DokuWiki renders the wiki page it creates a list of instructions for 1542f762695Snerun * the renderer. The plugin's handle() method generates the render 1552f762695Snerun * instructions for the plugin's own syntax mode. At some later time, 1562f762695Snerun * these will be interpreted by the plugin's render() method. 1572f762695Snerun * 1582f762695Snerun * Parameters: 1592f762695Snerun * 1602f762695Snerun * $match (string) — The text matched by the patterns 1612f762695Snerun * 1629f6f51efSnerun * $state (int) — The lexer state for the match, representing 1639f6f51efSnerun * the type of pattern which triggered this call 1649f6f51efSnerun * to handle(): DOKU_LEXER_SPECIAL — a pattern 1659f6f51efSnerun * set by addSpecialPattern(). 1662f762695Snerun * 1672f762695Snerun * $pos (int) — The character position of the matched text. 1682f762695Snerun * 1699f6f51efSnerun * $handler — Object Reference to the Doku_Handler object. 1702f762695Snerun */ 1718bd4c45fSnerun return $this->resolveFunction($match); 1722f762695Snerun } 1732f762695Snerun 1742f762695Snerun /** @inheritDoc */ 1752f762695Snerun public function render($mode, Doku_Renderer $renderer, $data) 1762f762695Snerun { 1772f762695Snerun /* READ: https://www.dokuwiki.org/devel:syntax_plugins#render_method 1789f6f51efSnerun * The part of the plugin that provides the output for the final web 1799f6f51efSnerun * page. 1802f762695Snerun * 1812f762695Snerun * Parameters: 1822f762695Snerun * 1839f6f51efSnerun * $mode — Name for the format mode of the final output produced 1849f6f51efSnerun * by the renderer. 1852f762695Snerun * 1869f6f51efSnerun * $renderer — Give access to the object Doku_Renderer, which contains 1879f6f51efSnerun * useful functions and values. 1882f762695Snerun * 1899f6f51efSnerun * $data — An array containing the instructions previously 1909f6f51efSnerun * prepared and returned by the plugin's own handle() 1919f6f51efSnerun * method. The render() must interpret the instruction and 1929f6f51efSnerun * generate the appropriate output. 1932f762695Snerun */ 1942f762695Snerun 1952f762695Snerun if ($mode !== 'xhtml') { 1962f762695Snerun return false; 1972f762695Snerun } 1982f762695Snerun 1992f762695Snerun if (!$data) { 2002f762695Snerun return false; 2012f762695Snerun } 2022f762695Snerun 2030c9cdeecSnerun // escape sequences 204d0ddb69dSnerun $data = $this->helper->processEscapes($data); 2050c9cdeecSnerun 2062f762695Snerun // Do not use <div></div> because we need inline substitution! 207bf533778Snerun $data = $renderer->render_text($data, 'xhtml'); 208fb66d8abSnerun // Remove the first '<p>' and the last '</p>' 209fb66d8abSnerun if (substr($data, 0, 3) === '<p>' && substr($data, -4) === '</p>') { 210fb66d8abSnerun $data = substr($data, 3, -4); 211fb66d8abSnerun } 2122f762695Snerun $renderer->doc .= $data; 2132f762695Snerun 2142f762695Snerun return true; 2152f762695Snerun } 21655bd7807Snerun 21755bd7807Snerun /** 21855bd7807Snerun * ========== #IF 2193e9f31f7Snerun * {{#if: 1st parameter | 2nd parameter | 3rd parameter #}} 2209f6f51efSnerun * {{#if: test string | value if test string is not empty | value if test 2219f6f51efSnerun * string is empty (or only white space) #}} 22255bd7807Snerun */ 223*bca4461fSnerun function _IF($params, $funcName) 22455bd7807Snerun { 2253e9f31f7Snerun if ( count($params) < 1 ) { 226*bca4461fSnerun $result = $this->helper->formatError('alert', $funcName, 'not_enough_params'); 22755bd7807Snerun } else { 22855bd7807Snerun if ( !empty($params[0]) ) { 229fb66d8abSnerun $result = $params[1] ?? ''; 230877148aeSnerun } else { 231fb66d8abSnerun $result = $params[2] ?? ''; 23255bd7807Snerun } 233877148aeSnerun } 234877148aeSnerun 235877148aeSnerun return $result; 236877148aeSnerun } 237877148aeSnerun 238877148aeSnerun /** 239877148aeSnerun * ========== #IFEQ 240877148aeSnerun * {{#ifeq: 1st parameter | 2nd parameter | 3rd parameter | 4th parameter #}} 241877148aeSnerun * {{#ifeq: string 1 | string 2 | value if identical | value if different #}} 242877148aeSnerun */ 243*bca4461fSnerun function _IFEQ($params, $funcName) 244877148aeSnerun { 2453e9f31f7Snerun if ( count($params) < 2 ) { 246*bca4461fSnerun $result = $this->helper->formatError('alert', $funcName, 'not_enough_params'); 247877148aeSnerun } else { 248877148aeSnerun if ( $params[0] == $params[1] ) { 249fb66d8abSnerun $result = $params[2] ?? ''; 250877148aeSnerun } else { 251fb66d8abSnerun $result = $params[3] ?? ''; 2523e9f31f7Snerun } 2533e9f31f7Snerun } 2543e9f31f7Snerun 2553e9f31f7Snerun return $result; 2563e9f31f7Snerun } 2573e9f31f7Snerun 2583e9f31f7Snerun /** 259fb66d8abSnerun * ======= #IFEXIST 260fb66d8abSnerun * Syntax: {{#ifexist: target | if-true | if-false #}} 261fb66d8abSnerun * 262fb66d8abSnerun * Accepts: 263fb66d8abSnerun * - DokuWiki page/media IDs (e.g. "wiki:start", "wiki:image.png") 264fb66d8abSnerun * - Namespaces (must end with ":", e.g. "wiki:") 265fb66d8abSnerun * - Absolute or relative filesystem paths 266fb66d8abSnerun * 267fb66d8abSnerun * @param array $params [ 268fb66d8abSnerun * 0 => string $target Path or ID to check (required) 269fb66d8abSnerun * 1 => string $ifTrue Value to return if target exists (optional) 270fb66d8abSnerun * 2 => string $ifFalse Value to return if target doesn't exist (optional) 271fb66d8abSnerun * ] 272*bca4461fSnerun * @param string $funcName Name of the parser function (for error messages) 273fb66d8abSnerun * @return string Rendered output based on existence check 2743e9f31f7Snerun */ 275*bca4461fSnerun function _IFEXIST($params, $funcName) 2763e9f31f7Snerun { 2773e9f31f7Snerun if (count($params) < 1) { 278*bca4461fSnerun return $this->helper->formatError('alert', $funcName, 'not_enough_params'); 2795f65861aSnerun } 2805f65861aSnerun 281d0ddb69dSnerun $target = trim($params[0]); 282d0ddb69dSnerun if ($target === '') { 283*bca4461fSnerun return $this->helper->formatError('alert', $funcName, 'empty_test_parameter'); 284877148aeSnerun } 28555bd7807Snerun 286d0ddb69dSnerun $exists = $this->helper->checkExistence($target); 287d0ddb69dSnerun 288d0ddb69dSnerun return $exists 289d0ddb69dSnerun ? ($params[1] ?? '') 290d0ddb69dSnerun : ($params[2] ?? ''); 29155bd7807Snerun } 2921c0304a1Snerun 2931c0304a1Snerun /** 2941c0304a1Snerun * ========== #SWITCH 2951c0304a1Snerun * {{#switch: comparison string 296d0ddb69dSnerun * | case1 = result1 2971c0304a1Snerun * | ... 298d0ddb69dSnerun * | caseN = resultN 2991c0304a1Snerun * | default result 3001c0304a1Snerun * #}} 3011c0304a1Snerun */ 302*bca4461fSnerun function _SWITCH($params, $funcName) { 3033e9f31f7Snerun if (count($params) < 2) { 304*bca4461fSnerun return $this->helper->formatError('alert', $funcName, 'not_enough_params'); 305643981a6Snerun } 306d0ddb69dSnerun 307d0ddb69dSnerun $parsed = $this->helper->parseSwitchCases($params); 308d0ddb69dSnerun 309d0ddb69dSnerun // Checks if the test string exists as a key in the switch cases array 310d0ddb69dSnerun if (array_key_exists($parsed['test'], $parsed['cases'])) { 311d0ddb69dSnerun return $parsed['cases'][$parsed['test']]; // ← May return empty string 312d0ddb69dSnerun } 313d0ddb69dSnerun 314d0ddb69dSnerun // Returns the default (explicit or implicit) only if the case does not exist 315d0ddb69dSnerun return $parsed['default'] ?? ''; 316643981a6Snerun } 317*bca4461fSnerun 318*bca4461fSnerun /** 319*bca4461fSnerun * ========== #EXPR 320*bca4461fSnerun * This function evaluates a mathematical expression and returns the 321*bca4461fSnerun * calculated value. 322*bca4461fSnerun */ 323*bca4461fSnerun private function _EXPR($params, $funcName) { 324*bca4461fSnerun if (!isset($params[0])) { 325*bca4461fSnerun return $this->helper->formatError('alert', $funcName, 'empty_test_parameter'); 326*bca4461fSnerun } 327*bca4461fSnerun 328*bca4461fSnerun return $this->helper->evaluateMathExpression($params[0]); 329*bca4461fSnerun } 330643981a6Snerun} 331643981a6Snerun 332