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 "&#__number__;" instead of "&#;__number__;" 303 * 304 * Number sign "#" can be escaped with "#" and don't need to be 305 * added to the array below. 306 */ 307 $escapes = array( 308 "&#61;" => "=", 309 "&#123;" => "{", 310 "&#124;" => "|", 311 "&#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