18bd4c45fSnerun<?php 28bd4c45fSnerun/** 38bd4c45fSnerun * DokuWiki Plugin parserfunctions (Action Component) 48bd4c45fSnerun * 58bd4c45fSnerun * Captures nested functions {{#func: ... #}} before the Lexer and processes 68bd4c45fSnerun * them recursively. 78bd4c45fSnerun * 88bd4c45fSnerun * @author ChatGPT -- Wed, 02 jul 2025 12:04:42 -0300 98bd4c45fSnerun */ 108bd4c45fSnerun 118bd4c45fSnerunuse dokuwiki\Extension\ActionPlugin; 128bd4c45fSnerunuse dokuwiki\Extension\EventHandler; 138bd4c45fSnerunuse dokuwiki\Extension\Event; 148bd4c45fSnerun 158bd4c45fSnerunclass action_plugin_parserfunctions extends ActionPlugin 168bd4c45fSnerun{ 178bd4c45fSnerun /** @var helper_plugin_parserfunctions */ 188bd4c45fSnerun private $helper; 198bd4c45fSnerun 208bd4c45fSnerun public function __construct() 218bd4c45fSnerun { 228bd4c45fSnerun $this->helper = plugin_load('helper', 'parserfunctions'); 238bd4c45fSnerun } 248bd4c45fSnerun 258bd4c45fSnerun /** @inheritDoc */ 268bd4c45fSnerun public function register(EventHandler $controller) 278bd4c45fSnerun { 288bd4c45fSnerun $controller->register_hook( 298bd4c45fSnerun 'PARSER_WIKITEXT_PREPROCESS', 308bd4c45fSnerun 'BEFORE', 318bd4c45fSnerun $this, 328bd4c45fSnerun 'handlePreprocess' 338bd4c45fSnerun ); 348bd4c45fSnerun } 358bd4c45fSnerun 368bd4c45fSnerun public function handlePreprocess(Event $event) 378bd4c45fSnerun { 388bd4c45fSnerun $text = $event->data; 398bd4c45fSnerun $text = $this->processParserFunctions($text); 408bd4c45fSnerun $event->data = $text; 418bd4c45fSnerun } 428bd4c45fSnerun 438bd4c45fSnerun private function processParserFunctions($text) 448bd4c45fSnerun { 45*bca4461fSnerun // 1. Protects escaped blocks 46*bca4461fSnerun $protectedBlocks = []; 47*bca4461fSnerun /* The "s" modifier makes . catch line breaks. 48*bca4461fSnerun * The "i" modifier ignores case (if someone writes <CODE>). 49*bca4461fSnerun */ 50*bca4461fSnerun $text = preg_replace_callback('/%%.*?%%|<(nowiki|code|file|html)[^>]*>.*?<\/\1>/si', function ($matches) use (&$protectedBlocks) { 51*bca4461fSnerun $key = '@@ESC' . count($protectedBlocks) . '@@'; 52*bca4461fSnerun $protectedBlocks[$key] = $matches[0]; 53*bca4461fSnerun return $key; 54*bca4461fSnerun }, $text); 55*bca4461fSnerun 56*bca4461fSnerun // 2. Processes functions normally 578bd4c45fSnerun $index = 0; 588bd4c45fSnerun while (($match = $this->extractBalancedFunction($text)) !== false) { 598bd4c45fSnerun $resolved = plugin_load('syntax', 'parserfunctions')->resolveFunction($match); 608bd4c45fSnerun $text = str_replace($match, $resolved, $text); 618bd4c45fSnerun $index++; 628bd4c45fSnerun } 638bd4c45fSnerun 64*bca4461fSnerun // 3. Restores protected blocks 65*bca4461fSnerun foreach ($protectedBlocks as $key => $original) { 66*bca4461fSnerun $text = str_replace($key, $original, $text); 67*bca4461fSnerun } 68*bca4461fSnerun 698bd4c45fSnerun return $text; 708bd4c45fSnerun } 718bd4c45fSnerun 728bd4c45fSnerun private function extractBalancedFunction($text) 738bd4c45fSnerun { 748bd4c45fSnerun $start = strpos($text, '{{#'); 758bd4c45fSnerun if ($start === false) return false; 768bd4c45fSnerun 778bd4c45fSnerun $level = 0; 788bd4c45fSnerun $length = strlen($text); 798bd4c45fSnerun for ($i = $start; $i < $length - 2; $i++) { 808bd4c45fSnerun if (substr($text, $i, 3) === '{{#') { 818bd4c45fSnerun $level++; 828bd4c45fSnerun $i += 2; 838bd4c45fSnerun } elseif (substr($text, $i, 3) === '#}}') { 848bd4c45fSnerun $level--; 858bd4c45fSnerun $i += 2; 868bd4c45fSnerun 878bd4c45fSnerun if ($level === 0) { 888bd4c45fSnerun return substr($text, $start, $i - $start + 1); 898bd4c45fSnerun } 908bd4c45fSnerun } 918bd4c45fSnerun } 928bd4c45fSnerun 938bd4c45fSnerun return false; // Malformed 948bd4c45fSnerun } 958bd4c45fSnerun} 968bd4c45fSnerun 97