1<?php 2/** 3 * Condition Plugin: render a block if a condition if fullfilled 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Etienne Meleard <etienne.meleard@free.fr> 7 * 8 * 2009/06/08 : Creation 9 * 2009/06/09 : Drop of the multi-value tests / creation of tester class system 10 * 2009/06/10 : Added tester class override to allow user to define custom tests 11 * 2010/06/09 : Changed $tester visibility to ensure compatibility with PHP4 12 */ 13 14if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); 15if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 16require_once(DOKU_PLUGIN.'syntax.php'); 17 18/** 19 * All DokuWiki plugins to extend the parser/rendering mechanism 20 * need to inherit from this class 21 */ 22class syntax_plugin_condition extends DokuWiki_Syntax_Plugin { 23 // To be used by _processblocks to mix the test results together 24 var $allowedoperators = array('\&\&', '\|\|', '\^', 'and', 'or', 'xor'); // plus '!' specific operator 25 26 // Allowed test operators, their behavior is defined in the tester class, they are just defined here for recognition during parsing 27 var $allowedtests = array(); 28 29 // Allowed test keys 30 var $allowedkeys = array(); 31 32 // To store the tester object 33 var $tester = null; 34 35 /*function accepts($mode) { return true; } 36 function getAllowedTypes() { 37 return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); // quick hack 38 }*/ 39 40 function getType() { return 'container';} 41 function getPType() { return 'normal';} 42 function getSort() { return 5; } // condition is top priority 43 44 // Connect pattern to lexer 45 function connectTo($mode){ 46 $this->Lexer->addEntryPattern('<if(?=.*?</if>)', $mode, 'plugin_condition'); 47 } 48 49 function postConnect() { 50 $this->Lexer->addExitPattern('</if>', 'plugin_condition'); 51 } 52 53 // Handle the match 54 function handle($match, $state, $pos, Doku_Handler $handler) { 55 if($state != DOKU_LEXER_UNMATCHED) return false; 56 57 // Get allowed test operators 58 $this->_loadtester(); 59 if(!$this->tester) return array(array(), ''); 60 $this->allowedtests = $this->tester->getops(); 61 $this->allowedkeys = $this->tester->getkeys(); 62 63 $blocks = array(); 64 $content = ''; 65 $this->_parse($match, $blocks, $content); 66 67 return array($blocks, $content); 68 } 69 70 // extracts condition / content 71 function _parse(&$match, &$b, &$ctn) { 72 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 73 $b = $this->_fetch_block($match, 0); 74 if($match != '') $ctn = preg_replace('`\n+$`', '', preg_replace('`^\n+`', '', preg_replace('`^>`', '', $match))); 75 return true; 76 } 77 78 // fetch a condition block from buffer 79 function _fetch_block(&$match, $lvl=0) { 80 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 81 $instrs = array(); 82 $continue = true; 83 84 while(($match{0} != '>') && ($match != '') && (($lvl == 0) || ($match{0} != ')')) && $continue) { 85 $i = array('type' => null, 'key' => '', 'test' => '', 'value' => '', 'next' => ''); 86 if($this->_fetch_op($match, true)) { // ! heading equals block descending for first token 87 $i['type'] = 'nblock'; 88 $match = substr($match, 1); // remove heading ! 89 $i['value'] = $this->_fetch_block($match, $lvl+1); 90 }else if($this->_is_block($match)) { 91 $i['type'] = 'block'; 92 $match = substr($match, 1); // remove heading ( 93 $i['value'] = $this->_fetch_block($match, $lvl+1); 94 }else if($this->_is_key($match, $key)) { 95 $i['type'] = 'test'; 96 $i['key'] = $key; 97 $match = substr($match, strlen($key)); // remove heading key 98 if($this->_is_test($match, $test)) { 99 $i['test'] = $test; 100 $match = substr($match, strlen($test)); // remove heading test 101 if(($v = $this->_fetch_value($match)) !== null) $i['value'] = $v; 102 } 103 }else $match = preg_replace('`^[^>\s\(]+`', '', $match); // here dummy stuff remains 104 if($i['type']) { 105 if(($op = $this->_fetch_op($match, false)) !== null) { 106 $match = substr($match, strlen($op)); // remove heading op 107 $i['next'] = $op; 108 }else $continue = false; 109 $instrs[] = $i; 110 } 111 } 112 return $instrs; 113 } 114 115 // test if buffer starts with new sub-block 116 function _is_block(&$match) { 117 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 118 return preg_match('`^\(`', $match); 119 } 120 121 // test if buffer starts with a key ref 122 function _is_key(&$match, &$key) { 123 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 124 if(preg_match('`^([a-zA-Z0-9_-]+)`', $match, $r)) { 125 if(preg_match('`^'.$this->_preg_build_alternative($this->allowedkeys).'$`', $r[1])) { 126 $key = $r[1]; 127 return true; 128 } 129 } 130 return false; 131 } 132 133 // build a pcre alternative escaped test from array 134 function _preg_build_alternative($choices) { 135 //$choices = array_map(create_function('$e', 'return preg_replace(\'`([^a-zA-Z0-9])`\', \'\\\\\\\\$1\', $e);'), $choices); 136 return '('.implode('|', $choices).')'; 137 } 138 139 // tells if buffer starts with a test operator 140 function _is_test(&$match, &$test) { 141 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 142 if(preg_match('`^'.$this->_preg_build_alternative($this->allowedtests).'`', $match, $r)) { $test = $r[1]; return true; } 143 return false; 144 } 145 146 // fetch value from buffer, handles value quoting 147 function _fetch_value(&$match) { 148 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 149 if($match{0} == '"') { 150 $match = substr($match, 1); 151 $value = substr($match, 0, strpos($match, '"')); 152 $match = substr($match, strlen($value) + 1); 153 }else{ 154 $psp = strpos($match, ')'); 155 $wsp = strpos($match, ' '); 156 $esp = strpos($match, '>'); 157 $sp = 0; 158 $bug = false; 159 if(($wsp === false) && ($esp === false) && ($psp === false)) { 160 return null; // BUG 161 }else if(($wsp === false) && ($esp === false)) { 162 $sp = $psp; 163 }else if(($wsp === false) && ($psp === false)) { 164 $sp = $esp; 165 }else if(($psp === false) && ($esp === false)) { 166 $sp = $wsp; 167 }else if($wsp === false) { 168 $sp = min($esp, $psp); 169 }else if($esp === false) { 170 $sp = min($wsp, $psp); 171 }else if($psp === false) { 172 $sp = min($esp, $wsp); 173 }else $sp = min($wsp, $esp, $psp); 174 175 $value = substr($match, 0, $sp); 176 $match = substr($match, strlen($value)); 177 } 178 return $value; 179 } 180 181 // fetch a logic operator from buffer 182 function _fetch_op(&$match, $head=false) { 183 $match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces 184 $ops = $this->allowedoperators; 185 if($head) $ops = array('!'); 186 if(preg_match('`^'.$this->_preg_build_alternative($ops).'`', $match, $r)) return $r[1]; 187 return null; 188 } 189 190 /** 191 * Create output 192 */ 193 function render($mode, Doku_Renderer $renderer, $data) { 194 global $INFO; 195 if(count($data) != 2) return false; 196 if($mode == 'xhtml') { 197 global $ID; 198 // prevent caching to ensure good user data detection for tests 199 $renderer->info['cache'] = false; 200 201 $blocks = $data[0]; 202 $content = $data[1]; 203 204 // parsing content for a <else> statement 205 $else = ''; 206 if(strpos($content, '<else>') !== false) { 207 $i = explode('<else>', $content); 208 $content = $i[0]; 209 $else = implode('', array_slice($i, 1)); 210 } 211 212 // Process condition blocks 213 $bug = false; 214 $this->_loadtester(); 215 $ok = $this->_processblocks($blocks, $bug); 216 217 // Render content if all went well 218 $toc = $renderer->toc; 219 if(!$bug) { 220 $instr = p_get_instructions($ok ? $content : $else); 221 foreach($instr as $instruction) { 222 if ( in_array($instruction[0], array('document_start', 'document_end') ) ) continue; 223 call_user_func_array(array(&$renderer, $instruction[0]), $instruction[1]); 224 } 225 } 226 $renderer->toc = array_merge($toc, $renderer->toc); 227 228 return true; 229 } 230 if($mode == 'metadata') { 231 global $ID; 232 // prevent caching to ensure good user data detection for tests 233 $renderer->info['cache'] = false; 234 235 $blocks = $data[0]; 236 $content = $data[1]; 237 238 // parsing content for a <else> statement 239 $else = ''; 240 if(strpos($content, '<else>') !== false) { 241 $i = explode('<else>', $content); 242 $content = $i[0]; 243 $else = implode('', array_slice($i, 1)); 244 } 245 246 // Process condition blocks 247 $bug = false; 248 $this->_loadtester(); 249 $ok = $this->_processblocks($blocks, $bug); 250 // Render content if all went well 251 $metatoc = $renderer->meta['description']['tableofcontents']; 252 if(!$bug) { 253 $instr = p_get_instructions($ok ? $content : $else); 254 foreach($instr as $instruction) { 255 if ( in_array($instruction[0], array('document_start', 'document_end') ) ) continue; 256 call_user_func_array(array(&$renderer, $instruction[0]), $instruction[1]); 257 } 258 } 259 260 if ( !is_array($renderer->meta['description']['tableofcontents']) ) { 261 $renderer->meta['description']['tableofcontents'] = array(); 262 } 263 264 $renderer->meta['description']['tableofcontents'] = array_merge($metatoc, $renderer->meta['description']['tableofcontents']); 265 266 return true; 267 } 268 return false; 269 } 270 271 // Strips the heading <p> and trailing </p> added by p_render xhtml to acheive inline behavior 272 function _stripp($data) { 273 $data = preg_replace('`^\s*<p[^>]*>\s*`', '', $data); 274 $data = preg_replace('`\s*</p[^>]*>\s*$`', '', $data); 275 return $data; 276 } 277 278 // evaluates the logical result from a set of blocks 279 function _processblocks($b, &$bug) { 280 for($i=0; $i<count($b); $i++) { 281 if(($b[$i]['type'] == 'block') || ($b[$i]['type'] == 'nblock')) { 282 $b[$i]['r'] = $this->_processblocks($b[$i]['value'], $bug); 283 if($b[$i]['type'] == 'nblock') $b[$i]['r'] = !$b[$i]['r']; 284 }else{ 285 $b[$i]['r'] = $this->_evaluate($b[$i], $bug); 286 } 287 } 288 if(!count($b)) $bug = true; // no condition in block 289 if($bug) return false; 290 291 // assemble conditions 292 /* CUSTOMISATION : 293 * You can add custom mixing operators here, don't forget to add them to 294 * the "allowedoperators" list at the top of this file 295 */ 296 $r = $b[0]['r']; 297 for($i=1; $i<count($b); $i++) { 298 if($b[$i-1]['next'] == '') { 299 $bug = true; 300 return false; 301 } 302 switch($b[$i-1]['next']) { 303 case '&&' : 304 case 'and' : 305 $r &= $b[$i]['r']; 306 break; 307 case '||' : 308 case 'or' : 309 $r |= $b[$i]['r']; 310 break; 311 case '^' : 312 case 'xor' : 313 $r ^= $b[$i]['r']; 314 break; 315 } 316 } 317 return $r; 318 } 319 320 // evaluates a single test, loads custom tests if class exists, default test set otherwise 321 function _evaluate($b, &$bug) { 322 if(!$this->tester) { 323 $bug = true; 324 return false; 325 } 326 return $this->tester->run($b, $bug); 327 } 328 329 // tries to load user defined tester, then base tester if previous failed 330 function _loadtester() { 331 global $conf; 332 $this->tester = null; 333 include_once(DOKU_PLUGIN.'condition/base_tester.php'); 334 if(@file_exists(DOKU_INC.'lib/tpl/'.$conf['template'].'/condition_plugin_custom_tester.php')) { 335 include_once(DOKU_INC.'lib/tpl/'.$conf['template'].'/condition_plugin_custom_tester.php'); 336 if(class_exists('condition_plugin_custom_tester')) { 337 $this->tester = new condition_plugin_custom_tester(); 338 } 339 } 340 if(!$this->tester) { 341 if(class_exists('condition_plugin_base_tester')) { 342 $this->tester = new condition_plugin_base_tester(); 343 } 344 } 345 } 346} //class 347?> 348