1<?php
2/**
3 * DokuWiki Plugin parserfunctions (Action Component)
4 *
5 * Captures nested functions {{#func: ... #}} before the Lexer and processes
6 * them recursively.
7 *
8 * @author  ChatGPT -- Wed, 02 jul 2025 12:04:42 -0300
9 */
10
11use dokuwiki\Extension\ActionPlugin;
12use dokuwiki\Extension\EventHandler;
13use dokuwiki\Extension\Event;
14
15class action_plugin_parserfunctions extends ActionPlugin
16{
17    /** @var helper_plugin_parserfunctions */
18    private $helper;
19
20    public function __construct()
21    {
22        $this->helper = plugin_load('helper', 'parserfunctions');
23    }
24
25    /** @inheritDoc */
26    public function register(EventHandler $controller)
27    {
28        $controller->register_hook(
29            'PARSER_WIKITEXT_PREPROCESS',
30            'BEFORE',
31            $this,
32            'handlePreprocess'
33        );
34    }
35
36    public function handlePreprocess(Event $event)
37    {
38        $text = $event->data;
39        $text = $this->processParserFunctions($text);
40        $event->data = $text;
41    }
42
43    private function processParserFunctions($text)
44    {
45        // 1. Protects escaped blocks
46        $protectedBlocks = [];
47        /* The "s" modifier makes . catch line breaks.
48         * The "i" modifier ignores case (if someone writes <CODE>).
49         */
50        $text = preg_replace_callback('/%%.*?%%|<(nowiki|code|file|html)[^>]*>.*?<\/\1>/si', function ($matches) use (&$protectedBlocks) {
51            $key = '@@ESC' . count($protectedBlocks) . '@@';
52            $protectedBlocks[$key] = $matches[0];
53            return $key;
54        }, $text);
55
56        // 2. Processes functions normally
57        $index = 0;
58        while (($match = $this->extractBalancedFunction($text)) !== false) {
59            $resolved = plugin_load('syntax', 'parserfunctions')->resolveFunction($match);
60            $text = str_replace($match, $resolved, $text);
61            $index++;
62        }
63
64        // 3. Restores protected blocks
65        foreach ($protectedBlocks as $key => $original) {
66            $text = str_replace($key, $original, $text);
67        }
68
69        return $text;
70    }
71
72    private function extractBalancedFunction($text)
73    {
74        $start = strpos($text, '{{#');
75        if ($start === false) return false;
76
77        $level = 0;
78        $length = strlen($text);
79        for ($i = $start; $i < $length - 2; $i++) {
80            if (substr($text, $i, 3) === '{{#') {
81                $level++;
82                $i += 2;
83            } elseif (substr($text, $i, 3) === '#}}') {
84                $level--;
85                $i += 2;
86
87                if ($level === 0) {
88                    return substr($text, $start, $i - $start + 1);
89                }
90            }
91        }
92
93        return false; // Malformed
94    }
95}
96
97