1<?php 2 3/** 4 * Plugin QnA: Layout parser 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Mykola Ostrovskyy <dwpforge@gmail.com> 8 */ 9 10class action_plugin_qna extends DokuWiki_Action_Plugin { 11 12 const STATE_CLOSED = 0; 13 const STATE_QUESTION = 1; 14 const STATE_ANSWER = 2; 15 16 private $rewriter; 17 private $blockState; 18 private $headerIndex; 19 private $headerTitle; 20 private $headerLevel; 21 private $headerId; 22 private $headerCheck; 23 24 /** 25 * Register callbacks 26 */ 27 public function register(Doku_Event_Handler $controller) { 28 $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'beforeDokuwikiStarted'); 29 $controller->register_hook('PARSER_HANDLER_DONE', 'AFTER', $this, 'afterParserHandlerDone'); 30 $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'beforeParserCacheUse'); 31 } 32 33 /** 34 * Prepare plugin stylesheet file 35 */ 36 public function beforeDokuwikiStarted($event) { 37 $fromConf = dirname(__FILE__) . '/style/' . $this->getConf('style') . '.less'; 38 $inUse = dirname(__FILE__) . '/all.less'; 39 if (!@file_exists($inUse) || @filesize($inUse) != @filesize($fromConf)) { 40 @copy($fromConf, $inUse); 41 } 42 } 43 44 /** 45 * 46 */ 47 public function afterParserHandlerDone($event, $param) { 48 $this->reset(); 49 $this->fixLayout($event); 50 } 51 52 /** 53 * Reset internal state 54 */ 55 private function reset() { 56 $this->rewriter = new qna_instruction_rewriter(); 57 $this->blockState = self::STATE_CLOSED; 58 $this->headerIndex = -1; 59 $this->headerTitle = ''; 60 $this->headerLevel = 0; 61 $this->headerId = ''; 62 $this->headerCheck = array(); 63 } 64 65 /** 66 * Insert implicit instructions 67 */ 68 private function fixLayout($event) { 69 $instructions = count($event->data->calls); 70 for ($i = 0; $i < $instructions; $i++) { 71 $instruction = $event->data->calls[$i]; 72 73 switch ($instruction[0]) { 74 case 'header': 75 $this->headerIndex = $i; 76 $this->headerTitle = $instruction[1][0]; 77 $this->headerLevel = $instruction[1][1]; 78 $this->headerId = sectionID($instruction[1][0], $this->headerCheck); 79 /* Fall through */ 80 81 case 'section_close': 82 case 'section_edit': 83 case 'section_open': 84 if ($this->blockState != self::STATE_CLOSED) { 85 $this->rewriter->insertBlockCall($i, 'close_block', 2); 86 $this->blockState = self::STATE_CLOSED; 87 } 88 break; 89 90 case 'plugin': 91 switch ($instruction[1][0]) { 92 case 'qna_block': 93 $this->handlePluginQnaBlock($i, $instruction[1][1]); 94 break; 95 96 case 'qna_header': 97 $this->handlePluginQnaHeader($i); 98 break; 99 } 100 break; 101 } 102 } 103 104 if ($this->blockState != self::STATE_CLOSED) { 105 $this->rewriter->appendBlockCall('close_block', 2); 106 } 107 108 $this->rewriter->apply($event->data->calls); 109 } 110 111 /** 112 * Insert implicit instructions 113 */ 114 private function handlePluginQnaBlock($index, $data) { 115 switch ($data[0]) { 116 case 'open_question': 117 if ($this->blockState != self::STATE_CLOSED) { 118 $this->rewriter->insertBlockCall($index, 'close_block', 2); 119 } 120 121 $this->rewriter->insertBlockCall($index, 'open_block'); 122 $this->rewriter->setQuestionLevel($index, $this->headerLevel + 1); 123 $this->blockState = self::STATE_QUESTION; 124 break; 125 126 case 'open_answer': 127 switch ($this->blockState) { 128 case self::STATE_CLOSED: 129 $this->rewriter->delete($index); 130 break; 131 132 case self::STATE_QUESTION: 133 case self::STATE_ANSWER: 134 $this->rewriter->insertBlockCall($index, 'close_block'); 135 $this->blockState = self::STATE_ANSWER; 136 break; 137 } 138 break; 139 140 case 'close_block': 141 switch ($this->blockState) { 142 case self::STATE_CLOSED: 143 $this->rewriter->delete($index); 144 break; 145 146 case self::STATE_QUESTION: 147 case self::STATE_ANSWER: 148 $this->rewriter->insertBlockCall($index, 'close_block'); 149 $this->blockState = self::STATE_CLOSED; 150 break; 151 } 152 break; 153 } 154 } 155 156 /** 157 * Wrap the last header 158 */ 159 private function handlePluginQnaHeader($index) { 160 /* On a clean install the distance between the header instruction and qna_header dummy 161 sould be 2 (one section_open in between). Allowing distance to be in the range from 162 1 to 3 gives some flexibility for better compatibility with other plugins that might 163 rearrange instructions around the header. */ 164 if (($index - $this->headerIndex) < 4) { 165 $data[0] ='open'; 166 $data[1] = $this->headerTitle; 167 $data[2] = $this->headerId; 168 $data[3] = $this->headerLevel; 169 170 $this->rewriter->insertHeaderCall($this->headerIndex, $data); 171 $this->rewriter->insertHeaderCall($this->headerIndex + 1, 'close'); 172 } 173 174 $this->rewriter->delete($index); 175 } 176 177 /** 178 * 179 */ 180 public function beforeParserCacheUse($event, $param) { 181 global $ID; 182 183 $cache = $event->data; 184 185 if (isset($cache->page) && ($cache->page == $ID)) { 186 if (isset($cache->mode) && ($cache->mode == 'xhtml')) { 187 $depends = p_get_metadata($ID, 'relation depends'); 188 189 if (!empty($depends) && isset($depends['rendering'])) { 190 $this->addDependencies($cache, array_keys($depends['rendering'])); 191 } 192 } 193 } 194 } 195 196 /** 197 * Add extra dependencies to the cache 198 */ 199 private function addDependencies($cache, $depends) { 200 foreach ($depends as $file) { 201 if (!in_array($file, $cache->depends['files']) && file_exists($file)) { 202 $cache->depends['files'][] = $file; 203 } 204 } 205 } 206} 207 208class qna_instruction_rewriter { 209 210 const DELETE = 1; 211 const INSERT = 2; 212 const SET_LEVEL = 3; 213 214 private $correction; 215 216 /** 217 * Constructor 218 */ 219 public function __construct() { 220 $this->correction = array(); 221 } 222 223 /** 224 * Remove instruction at $index 225 */ 226 public function delete($index) { 227 $this->correction[$index][] = array(self::DELETE); 228 } 229 230 /** 231 * Insert a plugin call in front of instruction at $index 232 */ 233 public function insertPluginCall($index, $name, $data, $state, $text = '') { 234 $this->correction[$index][] = array(self::INSERT, array('plugin', array($name, $data, $state, $text))); 235 } 236 237 /** 238 * Insert qna_block plugin call in front of instruction at $index 239 */ 240 public function insertBlockCall($index, $data, $repeat = 1) { 241 for ($i = 0; $i < $repeat; $i++) { 242 $this->insertPluginCall($index, 'qna_block', array($data), DOKU_LEXER_SPECIAL); 243 } 244 } 245 246 /** 247 * Insert qna_header plugin call in front of instruction at $index 248 */ 249 public function insertHeaderCall($index, $data) { 250 if (!is_array($data)) { 251 $data = array($data); 252 } 253 254 $this->insertPluginCall($index, 'qna_header', $data, DOKU_LEXER_SPECIAL); 255 } 256 257 /** 258 * Append a plugin call at the end of the instruction list 259 */ 260 public function appendPluginCall($name, $data, $state, $text = '') { 261 $this->correction[-1][] = array(self::INSERT, array('plugin', array($name, $data, $state, $text))); 262 } 263 264 /** 265 * Append qna_block plugin call at the end of the instruction list 266 */ 267 public function appendBlockCall($data, $repeat = 1) { 268 for ($i = 0; $i < $repeat; $i++) { 269 $this->appendPluginCall('qna_block', array($data), DOKU_LEXER_SPECIAL); 270 } 271 } 272 273 /** 274 * Set open_question list level for TOC 275 */ 276 public function setQuestionLevel($index, $level) { 277 $this->correction[$index][] = array(self::SET_LEVEL, $level); 278 } 279 280 /** 281 * Apply the corrections 282 */ 283 public function apply(&$instruction) { 284 if (count($this->correction) > 0) { 285 $index = $this->getCorrectionIndex(); 286 $corrections = count($index); 287 $instructions = count($instruction); 288 $output = array(); 289 290 for ($c = 0, $i = 0; $c < $corrections; $c++, $i++) { 291 /* Copy all instructions, which are ahead of the next correction */ 292 for ( ; $i < $index[$c]; $i++) { 293 $output[] = $instruction[$i]; 294 } 295 296 $this->applyCorrections($i, $instruction, $output); 297 } 298 299 /* Copy the rest of instructions after the last correction */ 300 for ( ; $i < $instructions; $i++) { 301 $output[] = $instruction[$i]; 302 } 303 304 /* Handle appends */ 305 if (array_key_exists(-1, $this->correction)) { 306 $this->applyAppend($output); 307 } 308 309 $instruction = $output; 310 } 311 } 312 313 /** 314 * Sort corrections on instruction index, remove appends 315 */ 316 private function getCorrectionIndex() { 317 $result = array_keys($this->correction); 318 asort($result); 319 $result = array_values($result); 320 321 /* Remove appends */ 322 if ($result[0] == -1) { 323 array_shift($result); 324 } 325 326 return $result; 327 } 328 329 /** 330 * Apply corrections at $index 331 */ 332 private function applyCorrections($index, $input, &$output) { 333 $delete = false; 334 $position = $input[$index][2]; 335 336 foreach ($this->correction[$index] as $correction) { 337 switch ($correction[0]) { 338 case self::DELETE: 339 $delete = true; 340 break; 341 342 case self::INSERT: 343 $output[] = array($correction[1][0], $correction[1][1], $position); 344 break; 345 346 case self::SET_LEVEL: 347 if (($input[$index][0] == 'plugin') && ($input[$index][1][0] == 'qna_block') && ($input[$index][1][1][0] == 'open_question')) { 348 $input[$index][1][1][3] = $correction[1]; 349 } 350 break; 351 } 352 } 353 354 if (!$delete) { 355 $output[] = $input[$index]; 356 } 357 } 358 359 /** 360 * 361 */ 362 private function applyAppend(&$output) { 363 $lastCall = end($output); 364 $position = $lastCall[2]; 365 366 foreach ($this->correction[-1] as $correction) { 367 switch ($correction[0]) { 368 case self::INSERT: 369 $output[] = array($correction[1][0], $correction[1][1], $position); 370 break; 371 } 372 } 373 } 374}