1<?php 2/** 3 * DokuWiki Plugin parserfunctions (Helper 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 Tue, 01 jul 2025 15:06:42 -0300 8 */ 9 10if (!defined('DOKU_INC')) die(); 11 12class helper_plugin_parserfunctions extends DokuWiki_Plugin { 13 14 /** 15 * Processes raw function input into normalized parameters, handling pipe escapes 16 * 17 * Safely splits the input string by pipes (`|`) while respecting escaped pipes (`%%|%%`). 18 * Performs whitespace trimming on all resulting parameters. This enables DokuWiki's 19 * standard pipe syntax while supporting escaped pipes in parameter values. 20 * 21 * @param string $input The raw function input between delimiters (e.g. "a|b%%|%%c|d") 22 * 23 * @return array Normalized parameters with: 24 * - Escaped pipes restored (`%%TEMP_PIPE%%` → `%%|%%`) 25 * - Whitespace trimmed from both ends 26 * - Empty strings preserved as valid parameters 27 * 28 * @example Basic usage: 29 * parseParameters("a|b|c") → ["a", "b", "c"] 30 * 31 * @example With escaped pipe: 32 * parseParameters("a|b%%|%%c|d") → ["a", "b|c", "d"] 33 * 34 * @example With whitespace: 35 * parseParameters(" a | b ") → ["a", "b"] 36 * 37 * @example Empty parameters: 38 * parseParameters("a||b") → ["a", "", "b"] 39 * 40 * @note Preserves DokuWiki's standard %% escape syntax 41 * @note Empty strings are valid parameters (unlike array_filter) 42 * @note Trims only outer whitespace (inner spaces remain) 43 */ 44 public function parseParameters($input) { 45 // 1) Replace escaped pipes with temporary marker 46 $input = str_replace('%%|%%', '%%TEMP_PIPE%%', $input); 47 48 // 2) Split by unescaped pipes 49 $params = explode('|', $input); 50 51 // 3) Restore escaped pipes 52 $params = array_map(function($param) { 53 return str_replace('%%TEMP_PIPE%%', '%%|%%', $param); 54 }, $params); 55 56 // 4) Remove whitespace 57 return array_map('trim', $params); 58 } 59 60 /** 61 * Parses parameters for a SWITCH parser function and structures them for evaluation 62 * 63 * Processes the parameters into cases, test value, and default value, with support for: 64 * - Explicit value cases (`case = value`) 65 * - Fallthrough behavior (cases without values inherit the last defined value) 66 * - Both explicit (`#default = value`) and implicit default values (last parameter) 67 * - Whitespace normalization (trim) for all keys and values 68 * - Escaped equals signs (`%%=%%`) in values 69 * 70 * @param array $params The raw parameters from the parser function call: 71 * - First element: The test value to compare against cases 72 * - Subsequent elements: Cases in format "case = value" or fallthrough/default markers 73 * 74 * @return array Structured data with: 75 * - 'cases': Associative array of [case => value] pairs 76 * - 'test': Normalized test value (with whitespace trimmed) 77 * - 'default': The default value (either explicit #default or last parameter) 78 * 79 * @example For input [" test ", "a=1", "b", "c=3", "#default=final"] 80 * Returns: 81 * [ 82 * 'cases' => ['a' => '1', 'b' => '3', 'c' => '3'], 83 * 'test' => 'test', 84 * 'default' => 'final' 85 * ] 86 * 87 * @example Fallthrough behavior: 88 * ["val", "a=1", "b", "c=2"] produces: 89 * [ 90 * 'cases' => ['a' => '1', 'b' => '1', 'c' => '2'], 91 * 'test' => 'val', 92 * 'default' => '2' 93 * ] 94 * 95 * @note Escaped equals signs (`%%=%%`) in values are preserved 96 * @note All case keys and test values are trimmed of whitespace 97 * @note Empty strings are valid as both test values and case values 98 */ 99 public function parseSwitchCases($params) { 100 $cases = []; 101 $default = null; 102 $testString = null; 103 $lastValue = null; 104 105 foreach ($params as $param) { 106 $param = str_replace('%%=%%', '%%TEMP_EQUAL%%', $param); 107 $parts = explode('=', $param, 2); 108 $parts = array_map('trim', $parts); 109 110 if (count($parts) === 2) { 111 // Case with explicit value (case = value) 112 $parts[1] = str_replace('%%TEMP_EQUAL%%', '%%=%%', $parts[1]); 113 $cases[$parts[0]] = $parts[1]; 114 $lastValue = $parts[1]; 115 } else { 116 // Case without explicit value (fallthrough or default) 117 $parts[0] = str_replace('%%TEMP_EQUAL%%', '%%=%%', $parts[0]); 118 119 if ($testString === null) { 120 $testString = trim($parts[0]); // First parameter is the test value 121 } elseif (trim($parts[0]) === '#default') { 122 $default = $lastValue; // Explicit default 123 } else { 124 $cases[trim($parts[0])] = $lastValue; // Fallthrough - uses last defined value 125 } 126 } 127 } 128 129 return [ 130 'cases' => $cases, 131 'test' => $testString, 132 'default' => $default ?? $lastValue // Implicit default is the last value 133 ]; 134 } 135 136 /** 137 * Checks for the existence of a folder (namespace) or a file (media or page) 138 * 139 * Accepts: 140 * - Absolute or relative filesystem paths 141 * - DokuWiki page/media IDs (e.g. "wiki:start", "wiki:image.png") 142 * - DokuWiki namespaces (must end with a colon, e.g. "wiki:") 143 * 144 * @param string $target The identifier or path to check 145 * @return bool True if it exists (file, page, media, or namespace), false otherwise 146 */ 147 public function checkExistence($target) { 148 // Normalize spaces around ':', transform "wiki : help" → "wiki:help" 149 $target = preg_replace('/\s*:\s*/', ':', $target); 150 151 // If it is a real absolute or relative path, test as file or folder 152 if (file_exists($target)) { 153 return true; 154 } 155 156 // If path started with '/', try as relative to DOKU_INC by removing '/' 157 if (strlen($target) > 0 && $target[0] === '/') { 158 $relativePath = ltrim($target, '/'); 159 $fullPath = DOKU_INC . $relativePath; 160 if (file_exists($fullPath)) { 161 return true; 162 } 163 } 164 165 // Try as DokuWiki page 166 if (page_exists($target)) { 167 return true; 168 } 169 170 // Try as DokuWiki media 171 if (file_exists(mediaFN($target))) { 172 return true; 173 } 174 175 // Try as namespace (directory inside data/pages/) 176 $namespacePath = str_replace(':', '/', $target); 177 $namespaceDir = DOKU_INC . 'data/pages/' . $namespacePath; 178 if (is_dir($namespaceDir)) { 179 return true; 180 } 181 182 return false; 183 } 184 185 /** 186 * Escape sequence handling (for backwards compatibility) 187 * 188 * To add more escapes, please refer to: 189 * https://www.freeformatter.com/html-entities.html 190 * 191 * Before 2025-01-18, escape sequences had to use "&#NUMBER;" instead of 192 * "&#;NUMBER;", because "#" was not escaped. 193 * 194 * After 2025-01-18, the "#" can be typed directly, and does not need to be 195 * escaped. So use the normal spelling for HTML entity codes ("=" 196 * instead of "&#61;") when adding NEW escapes. 197 * 198 * Additionally, after 2025-01-18, '=', '|', '{' and '}' signs can be 199 * escaped only by wrapping them in '%%', following the standard DokuWiki 200 * syntax. So, the escapes below are DEPRECATED, but kept for backwards 201 * compatibility. 202 * 203 */ 204 public function processEscapes($text) { 205 // DEPRECATED, but kept for backwards compatibility: 206 $escapes = [ 207 "&#61;" => "=", 208 "&#123;" => "%%{%%", 209 "&#124;" => "|", 210 "&#125;" => "%%}%%", 211 "#" => "#" // Always leave this as the last element! 212 ]; 213 214 foreach ($escapes as $key => $value) { 215 $text = str_replace($key, $value, $text); 216 } 217 218 return $text; 219 } 220 221 /** 222 * Format error messages consistently 223 */ 224 public function formatError($type, $function, $messageKey) { 225 $wrapPluginExists = file_exists(DOKU_INC . 'lib/plugins/wrap'); 226 227 $errorMsg = '**' . $this->getLang('error') . ' ' . $function . ': ' 228 . $this->getLang($messageKey) . '**'; 229 230 if ($wrapPluginExists) { 231 return "<wrap $type>$errorMsg</wrap>"; 232 } 233 234 return $errorMsg; 235 } 236 237 /** 238 * Evaluates a mathematical expression consistently 239 */ 240 public function evaluateMathExpression($expr) { 241 $funcName = 'expr'; 242 $expr = trim($expr); 243 244 // Rejects characters outside the permitted set 245 if (!preg_match('/^( 246 \s*| 247 \b(?:and|or|xor|not)\b| # Reserved words first 248 ==|!=|<=|>=|<|>| # Comparisons 249 \+|\-|\*|\/|%| # Arithmetic 250 \(|\)| # Parentheses 251 &&|\|\||!| # Symbolic logics 252 [0-9]+(\.[0-9]+)?([eE][\+\-]?[0-9]+)? # Numbers with period and exponent 253 )+$/ix' 254 , $expr)) { 255 return $this->formatError('alert', $funcName, 'invalid_expression'); 256 } 257 258 try { 259 $expr = preg_replace('/\bnot\b/i', '!', $expr); 260 // Simple evaluation 261 $result = eval('return (' . $expr . ');'); 262 263 if (!is_numeric($result) || is_infinite($result) || is_nan($result)) { 264 if (is_bool($result)) { 265 return $result ? 1 : 0; 266 } else { 267 return $this->formatError('alert', $funcName, 'undefined_result'); 268 } 269 } 270 271 return $result; 272 } catch (Throwable $e) { 273 return $this->formatError('alert', $funcName, 'evaluation_error'); 274 } 275 } 276} 277 278