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