xref: /plugin/codeblockedit/action.php (revision 9949c4fd50c301573218268366f7763bb5f6864a)
1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Form\Form;
5
6/**
7 * Class action_plugin_codeblockedit
8 *
9 * Handles editing of code blocks via standard DokuWiki editor
10 * Uses the same range-based approach as DokuWiki's section editing
11 */
12class action_plugin_codeblockedit extends ActionPlugin
13{
14    /** @var array|null Cached block info to avoid duplicate rawWiki calls */
15    protected $cachedBlock = null;
16
17    /** @var int Cached block index */
18    protected $cachedIndex = -1;
19
20    /**
21     * Register handlers
22     *
23     * @param Doku_Event_Handler $controller
24     */
25    public function register(Doku_Event_Handler $controller)
26    {
27        // Set up range before Edit action processes it
28        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreprocess', null, 1);
29        // Add hidden field to form
30        $controller->register_hook('EDIT_FORM_ADDTEXTAREA', 'BEFORE', $this, 'handleAddTextarea');
31        // Inject edit permission info into JSINFO
32        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'addJsInfo');
33        // Handle preview rendering
34        $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'handlePreview');
35    }
36
37    /**
38     * Add edit permission info to JSINFO for JavaScript
39     *
40     * @param Doku_Event $event
41     * @param mixed $param
42     */
43    public function addJsInfo(Doku_Event $event, $param)
44    {
45        global $JSINFO, $INFO;
46
47        // Let JavaScript know if user can edit this page
48        $JSINFO['codeblockedit_canedit'] = !empty($INFO['writable']);
49    }
50
51    /**
52     * Handle preview - wrap $TEXT with code/file tags for proper rendering
53     *
54     * @param Doku_Event $event
55     * @param mixed $param
56     */
57    public function handlePreview(Doku_Event $event, $param)
58    {
59        global $INPUT, $TEXT, $ID;
60
61        // Only handle preview action when editing a code block
62        if ($event->data !== 'preview' || !$INPUT->has('codeblockindex')) {
63            return;
64        }
65
66        // Get the block info to find the tags (use cache if available)
67        $index = $INPUT->int('codeblockindex');
68        if ($index < 0) {
69            return;
70        }
71
72        $block = $this->getBlockInfo($ID, $index);
73        if ($block && !empty($block['openTag']) && !empty($block['closeTag'])) {
74            // Wrap the TEXT with the original tags for proper preview rendering
75            $TEXT = $block['openTag'] . $TEXT . $block['closeTag'];
76        }
77    }
78
79    /**
80     * Handle ACTION_ACT_PREPROCESS
81     *
82     * Sets up $RANGE based on codeblockindex so DokuWiki's native
83     * section editing mechanism handles the rest
84     *
85     * @param Doku_Event $event
86     * @param mixed $param
87     */
88    public function handlePreprocess(Doku_Event $event, $param)
89    {
90        global $INPUT, $ID, $RANGE;
91
92        // Only run if codeblockindex is present
93        if (!$INPUT->has('codeblockindex')) {
94            return;
95        }
96
97        $act = $event->data;
98        if (is_array($act)) {
99            $act = key($act);
100        }
101
102        // Only process for edit action when no range is set yet
103        if ($act === 'edit' && empty($RANGE)) {
104            // Validate index - must be non-negative integer
105            $index = $INPUT->int('codeblockindex');
106            if ($index < 0) {
107                msg('Invalid code block index.', -1);
108                return;
109            }
110
111            $block = $this->getBlockInfo($ID, $index);
112
113            if ($block) {
114                // Set the RANGE global - DokuWiki will use this to slice the content
115                // Range is 1-based and inclusive on both ends
116                // DokuWiki's rawWikiSlices() subtracts 1 from both start and end
117                // So we need: start+1 for 1-based, end+1 to make end inclusive
118                $RANGE = ($block['start'] + 1) . '-' . ($block['end'] + 1);
119            } else {
120                msg('Code block not found.', -1);
121            }
122        }
123    }
124
125    /**
126     * Handle EDIT_FORM_ADDTEXTAREA
127     *
128     * Adds the hidden codeblockindex and hid fields to the form
129     *
130     * @param Doku_Event $event
131     * @param mixed $param
132     */
133    public function handleAddTextarea(Doku_Event $event, $param)
134    {
135        global $INPUT;
136
137        if ($INPUT->has('codeblockindex')) {
138            /** @var Form $form */
139            $form = $event->data['form'];
140            $form->setHiddenField('codeblockindex', $INPUT->int('codeblockindex'));
141
142            // Pass hid through for redirect back to code block after save
143            // Sanitize hid to only allow valid anchor format (codeblock_N)
144            if ($INPUT->has('hid')) {
145                $hid = $INPUT->str('hid');
146                if (preg_match('/^codeblock_\d+$/', $hid)) {
147                    $form->setHiddenField('hid', $hid);
148                }
149            }
150        }
151    }
152
153    /**
154     * Get block info with caching to avoid duplicate rawWiki calls
155     *
156     * @param string $id Page ID
157     * @param int $index Block index (0-based)
158     * @return array|null Block info or null if not found
159     */
160    protected function getBlockInfo($id, $index)
161    {
162        // Return cached result if available
163        if ($this->cachedIndex === $index && $this->cachedBlock !== null) {
164            return $this->cachedBlock;
165        }
166
167        $text = rawWiki($id);
168        if (empty($text)) {
169            return null;
170        }
171
172        $this->cachedIndex = $index;
173        $this->cachedBlock = $this->findBlockRange($text, $index);
174        return $this->cachedBlock;
175    }
176
177    /**
178     * Find the N-th code/file block and return its byte range
179     *
180     * @param string $text Raw wiki text
181     * @param int $index Target index (0-based)
182     * @return array|null ['start' => int, 'end' => int, 'content' => string, 'openTag' => string, 'closeTag' => string]
183     */
184    protected function findBlockRange($text, $index)
185    {
186        // Normalize line endings for consistent matching
187        $text = str_replace("\r\n", "\n", $text);
188        $text = str_replace("\r", "\n", $text);
189
190        // Find all code/file blocks using regex
191        // Pattern matches <code ...> or <file ...> blocks
192        $pattern = '/(<(?:code|file)[^>]*>)(.*?)(<\/(?:code|file)>)/s';
193
194        if (preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) {
195            if (isset($matches[2][$index])) {
196                // $matches[2][$index][0] = content
197                // $matches[2][$index][1] = offset of content start
198                $content = $matches[2][$index][0];
199                $contentStart = $matches[2][$index][1];
200                $contentEnd = $contentStart + strlen($content);
201
202                // Also capture opening and closing tags for preview rendering
203                $openTag = $matches[1][$index][0];
204                $closeTag = $matches[3][$index][0];
205
206                return [
207                    'start' => $contentStart,
208                    'end' => $contentEnd,
209                    'content' => $content,
210                    'openTag' => $openTag,
211                    'closeTag' => $closeTag
212                ];
213            }
214        }
215
216        return null;
217    }
218}
219
220