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