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 34 * block 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 * Must 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 * This pattern accepts any alphabetical function name but not nested 56 * functions. 57 * 58 * ChatGPT helped me to fix this pattern! (18 jan 2025) 59 */ 60 $this->Lexer->addSpecialPattern('\{\{#[[:alpha:]]+:(?:(?!\{\{#|#\}\}).)*#\}\}', $mode, 'plugin_parserfunctions'); 61 } 62 63 /** @inheritDoc */ 64 public function handle($match, $state, $pos, Doku_Handler $handler) 65 { 66 /* READ: https://www.dokuwiki.org/devel:syntax_plugins#handle_method 67 * This is the part of your plugin which should do all the work. Before 68 * DokuWiki renders the wiki page it creates a list of instructions for 69 * the renderer. The plugin's handle() method generates the render 70 * instructions for the plugin's own syntax mode. At some later time, 71 * these will be interpreted by the plugin's render() method. 72 * 73 * Parameters: 74 * 75 * $match (string) — The text matched by the patterns 76 * 77 * $state (int) — The lexer state for the match, representing 78 * the type of pattern which triggered this call 79 * to handle(): DOKU_LEXER_SPECIAL — a pattern 80 * set by addSpecialPattern(). 81 * 82 * $pos (int) — The character position of the matched text. 83 * 84 * $handler — Object Reference to the Doku_Handler object. 85 */ 86 87 // Exit if no text matched by the patterns. 88 if (empty($match)) { 89 return false; 90 } 91 92 /* Function name: "if", "ifeq", "ifexpr" etc. 93 * strtolower converts only ASCII; PhpString::strtolower supports UTF-8, 94 * added by "use dokuwiki\Utf8\PhpString;" at line 15. The function 95 * names will probably only use ASCII characters, but it's a precaution. 96 * The 's' at the end of '/pattern/s' adds support to multiline strings. 97 */ 98 $func_name = preg_replace('/\{\{#([[:alpha:](&#)]+):.*#\}\}/s', '\1', $match); 99 $func_name = PhpString::strtolower($func_name); 100 101 // Delete delimiters "{{#functionname:" and "#}}". 102 // The 's' at the end of '/pattern/s' adds support to multiline strings. 103 $parts = preg_replace('/\{\{#[[:alpha:]]+:(.*)#\}\}/s', '\1', $match); 104 105 // Create list with all parameters splited by "|" pipe 106 // 1st) Replace pipe '|' by a temporary marker 107 $parts = str_replace('%%|%%', '%%TEMP_MARKER%%', $parts); 108 // 2nd) Create list of parameters splited by pipe "|" 109 $params = explode('|', $parts); 110 //3rd) Restoring temporary marker to `%%|%%` 111 $params = str_replace('%%TEMP_MARKER%%', '%%|%%', $params); 112 /* This snippet above was necessary to allow the escape sequence of the 113 * pipe character "|" using the standard DokuWiki formatting syntax 114 * which is to wrap it in "%%". 115 */ 116 117 // Stripping whitespace from the beginning and end of strings 118 $params = array_map('trim', $params); 119 120 // ==================== FINALLY: do the work! ==================== 121 switch($func_name){ 122 // To add a new function, first add a "case" below, make it call a 123 // function, then write the function. 124 case 'if': 125 $func_result = $this->_IF($params, $func_name); 126 break; 127 case 'ifeq': 128 $func_result = $this->_IFEQ($params, $func_name); 129 break; 130 case 'ifexist': 131 $func_result = $this->_IFEXIST($params, $func_name); 132 break; 133 case 'switch': 134 $func_result = $this->_SWITCH($params, $func_name); 135 break; 136 default: 137 $func_result = $this->_raise_error('important', $func_name, 138 'no_such_function'); 139 break; 140 } 141 142 // The instructions provided to the render() method: 143 return $func_result; 144 } 145 146 /** @inheritDoc */ 147 public function render($mode, Doku_Renderer $renderer, $data) 148 { 149 /* READ: https://www.dokuwiki.org/devel:syntax_plugins#render_method 150 * The part of the plugin that provides the output for the final web 151 * page. 152 * 153 * Parameters: 154 * 155 * $mode — Name for the format mode of the final output produced 156 * by the renderer. 157 * 158 * $renderer — Give access to the object Doku_Renderer, which contains 159 * useful functions and values. 160 * 161 * $data — An array containing the instructions previously 162 * prepared and returned by the plugin's own handle() 163 * method. The render() must interpret the instruction and 164 * generate the appropriate output. 165 */ 166 167 if ($mode !== 'xhtml') { 168 return false; 169 } 170 171 if (!$data) { 172 return false; 173 } 174 175 // escape sequences 176 $data = $this->_escape($data); 177 178 // Do not use <div></div> because we need inline substitution! 179 // Both substr() and preg_replace() do the same thing: remove the 180 // first '<p>' and the last '</p>' 181 $data = $renderer->render_text($data, 'xhtml'); 182 //$data = substr( $data, 4, -5 ); 183 $data = preg_replace( '/<p>((.|\n)*?)<\/p>/', '\1', $data ); 184 $renderer->doc .= $data; 185 186 return true; 187 } 188 189 function _raise_error($wrap, $func_name, $msg){ 190 if ( file_exists('lib/plugins/wrap') ){ 191 $wrap_s = '<wrap ' . $wrap . '>'; 192 $wrap_e = '</wrap>'; 193 } else { 194 $wrap_s = $wrap_e = null; 195 } 196 197 return $wrap_s . '**' . $this->getLang('error') . ' "' . $func_name . 198 '": ' . $this->getLang($msg) . '**' . $wrap_e; 199 } 200 201 /** 202 * ========== #IF 203 * {{#if: 1st parameter | 2nd parameter | 3rd parameter #}} 204 * {{#if: test string | value if test string is not empty | value if test 205 * string is empty (or only white space) #}} 206 */ 207 function _IF($params, $func_name) 208 { 209 if ( count($params) < 1 ) { 210 $result = $this->_raise_error('alert', $func_name, 211 'not_enough_params'); 212 } else { 213 if ( !empty($params[0]) ) { 214 $result = $params[1] ?? null; 215 } else { 216 $result = $params[2] ?? null; 217 } 218 } 219 220 return $result; 221 } 222 223 /** 224 * ========== #IFEQ 225 * {{#ifeq: 1st parameter | 2nd parameter | 3rd parameter | 4th parameter #}} 226 * {{#ifeq: string 1 | string 2 | value if identical | value if different #}} 227 */ 228 function _IFEQ($params, $func_name) 229 { 230 if ( count($params) < 2 ) { 231 $result = $this->_raise_error('alert', $func_name, 232 'not_enough_params'); 233 } else { 234 if ( $params[0] == $params[1] ) { 235 $result = $params[2] ?? null; 236 } else { 237 $result = $params[3] ?? null; 238 } 239 } 240 241 return $result; 242 } 243 244 /** 245 * ========== #IFEXIST 246 * {{#ifexist: 1st parameter | 2nd parameter | 3rd parameter #}} 247 * {{#ifexist: page title or media, or file/folder path | value if exists 248 * | value if doesn't exist #}} 249 */ 250 function _IFEXIST($params, $func_name) 251 { 252 if ( count($params) < 1 ) { 253 $result = $this->_raise_error('alert', $func_name, 254 'not_enough_params'); 255 } else { 256 if ( str_contains($params[0], '/') ){ 257 if ( str_starts_with($params[0], '/') ){ 258 $isMedia = substr($params[0], 1); 259 } else { 260 $isMedia = $params[0]; 261 } 262 } else { 263 $isMedia = 'data/media/' . str_replace(':', '/', $params[0]); 264 } 265 266 if ( page_exists($params[0]) or file_exists($isMedia) ){ 267 $result = $params[1] ?? null; 268 } else { 269 $result = $params[2] ?? null; 270 } 271 } 272 273 return $result; 274 } 275 276 /** 277 * ========== #SWITCH 278 * {{#switch: comparison string 279 * | case = result 280 * | case = result 281 * | ... 282 * | case = result 283 * | default result 284 * #}} 285 */ 286 function _SWITCH($params, $func_name) 287 { 288 if ( count($params) < 2 ) { 289 $result = $this->_raise_error('alert', $func_name, 290 'not_enough_params'); 291 } else { 292 /** 293 * Then: 294 * 295 * "$params": 296 * ( 297 * [0] => test string 298 * [1] => case 1 = value 1 299 * [2] => case 2 = value 2 300 * [3] => case 3 = value 3 301 * [4] => default value 302 * ) 303 */ 304 305 $cases_kv = []; 306 $test_and_default_string = []; 307 308 foreach ( $params as $value ){ 309 // 1st) Replace escaped equal sign '%%|%%' by a temporary marker 310 $value = str_replace('%%=%%', '%%TEMP_MARKER%%', $value); 311 // 2nd) Create list of values splited by equal sign "=" 312 $value = explode('=', $value); 313 //3rd) Restoring temporary marker to `%%=%%` 314 $value = str_replace('%%TEMP_MARKER%%', '%%=%%', $value); 315 /* This snippet above was necessary to allow the escape sequence of 316 * the equal sign "=" using the standard DokuWiki formatting syntax 317 * which is to wrap it in "%%". 318 * (same as lines 105-115 above) 319 */ 320 321 if ( isset($value[1]) ) { 322 $cases_kv[trim($value[0])] = trim($value[1]); 323 } else { 324 if ( count($cases_kv) == 0 or count($cases_kv) == count($params) ) { 325 $test_and_default_string[] = trim($value[0]); 326 } else { 327 $cases_kv[trim($value[0])] = '%%FALL_THROUGH_TEMP_MARKER%%'; 328 } 329 } 330 } 331 332 $count = 0; 333 334 foreach ( $cases_kv as $key=>$value ){ 335 $count++; 336 if ( $value == '%%FALL_THROUGH_TEMP_MARKER%%' ){ 337 $subDict = array_slice($cases_kv, $count); 338 foreach ( $subDict as $chave=>$valor ){ 339 if ( $valor != '%%FALL_THROUGH_TEMP_MARKER%%' ){ 340 $cases_kv[$key] = $valor; 341 break; 342 } 343 } 344 } 345 } 346 347 /** 348 * And now: 349 * 350 * "$cases_kv": 351 * ( 352 * [case 1] => value 1 353 * [case 2] => value 2 354 * [case 3] => value 3 355 * ) 356 * 357 * "$test_and_default_string": 358 * ( 359 * [0] => test string 360 * [1] => default value 361 * ) 362 */ 363 364 if ( array_key_exists($test_and_default_string[0], $cases_kv) ) { 365 $result = $cases_kv[$test_and_default_string[0]]; 366 } else { 367 /* Default value: 368 * Explicit declaration (#default = default_value) takes precedence 369 * over implicit one (just 'default_value'). 370 */ 371 $result = $cases_kv['#default'] ?? $test_and_default_string[1] ?? ''; 372 } 373 } 374 375 return $result; 376 } 377 378 /** 379 * Escape sequence handling 380 */ 381 function _escape($data){ 382 /** 383 * To add more escapes, please refer to: 384 * https://www.freeformatter.com/html-entities.html 385 * 386 * Before 2025-01-18, escape sequences had to use "&#NUMBER;" 387 * instead of "&#;NUMBER;", because "#" was not escaped. But now "#" can 388 * be typed directly, and does not need to be escaped. So use the normal 389 * spelling for HTML entity codes, i.e., "=" instead of "&#61;" 390 * when adding NEW escapes. 391 * 392 * Additionally, after 2025-01-18, '=', '|', '{' and '}' signs can be 393 * escaped only by wrapping them in '%%', following the standard 394 * DokuWiki syntax. So, the escapes below are DEPRECATED, but kept for 395 * backwards compatibility. 396 * 397 */ 398 $escapes = array( 399 // DEPRECATED, but kept for backwards compatibility: 400 "&#61;" => "=", 401 "&#123;" => "%%{%%", 402 "&#124;" => "|", 403 "&#125;" => "%%}%%", 404 "#" => "#" // Always leave this as the last element! 405 ); 406 407 foreach ( $escapes as $key => $value ) { 408 $data = preg_replace("/$key/s", $value, $data); 409 } 410 411 return $data; 412 } 413} 414// vim:ts=4:sw=4:et:enc=utf-8: 415