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