1<?php 2/** 3 * ToDo Plugin: Creates a checkbox based todo list 4 * 5 * Syntax: <todo [...options...] [#]>Name of Action</todo> - 6 * Creates a Checkbox with the "Name of Action" as 7 * the text associated with it. The hash (#, optional) 8 * will cause the checkbox to be checked by default. 9 * See https://www.dokuwiki.org/plugin:todo#usage_and_examples 10 * for possible options and examples. 11 * 12 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 13 * @author Babbage <babbage@digitalbrink.com>; Leo Eibler <dokuwiki@sprossenwanne.at> 14 */ 15 16if(!defined('DOKU_INC')) die(); 17 18/** 19 * All DokuWiki plugins to extend the parser/rendering mechanism 20 * need to inherit from this class 21 */ 22class syntax_plugin_todo_todo extends DokuWiki_Syntax_Plugin { 23 24 const TODO_UNCHECK_ALL = '~~TODO:UNCHECKALL~~'; 25 26 /** 27 * Get the type of syntax this plugin defines. 28 * 29 * @return String 30 */ 31 public function getType() { 32 return 'substition'; 33 } 34 35 /** 36 * Paragraph Type 37 * 38 * 'normal' - The plugin can be used inside paragraphs 39 * 'block' - Open paragraphs need to be closed before plugin output 40 * 'stack' - Special case. Plugin wraps other paragraphs. 41 */ 42 function getPType(){ 43 return 'normal'; 44 } 45 46 /** 47 * Where to sort in? 48 * 49 * @return Integer 50 */ 51 public function getSort() { 52 return 999; 53 } 54 55 /** 56 * Connect lookup pattern to lexer. 57 * 58 * @param $mode String The desired rendermode. 59 * @return void 60 * @see render() 61 */ 62 public function connectTo($mode) { 63 $this->Lexer->addEntryPattern('<todo[\s]*?.*?>(?=.*?</todo>)', $mode, 'plugin_todo_todo'); 64 $this->Lexer->addSpecialPattern(self::TODO_UNCHECK_ALL, $mode, 'plugin_todo_todo'); 65 $this->Lexer->addSpecialPattern('~~NOTODO~~', $mode, 'plugin_todo_todo'); 66 } 67 68 public function postConnect() { 69 $this->Lexer->addExitPattern('</todo>', 'plugin_todo_todo'); 70 } 71 72 /** 73 * Handler to prepare matched data for the rendering process. 74 * 75 * @param $match string The text matched by the patterns. 76 * @param $state int The lexer state for the match. 77 * @param $pos int The character position of the matched text. 78 * @param $handler Doku_Handler Reference to the Doku_Handler object. 79 * @return array An empty array for most cases, except: 80 - DOKU_LEXER_EXIT: An array containing the current lexer state 81 and information about the just lexed todo. 82 - DOKU_LEXER_SPECIAL: For the special pattern of the Uncheck-All-Button, an 83 array containing the current lexer state and the matched text. 84 */ 85 public function handle($match, $state, $pos, Doku_Handler $handler) { 86 switch($state) { 87 case DOKU_LEXER_ENTER : 88 #Search to see if the '#' is in the todotag (if so, this means the Action has been completed) 89 $x = preg_match('%<todo([^>]*)>%i', $match, $tododata); 90 if($x) { 91 $handler->todoargs = $this->parseTodoArgs($tododata[1]); 92 } 93 if(!isset($handler->todo_index) || !is_numeric($handler->todo_index)) { 94 $handler->todo_index = 0; 95 } 96 $handler->todo_user = ''; 97 $handler->checked = ''; 98 $handler->todotitle = ''; 99 break; 100 case DOKU_LEXER_MATCHED : 101 break; 102 case DOKU_LEXER_UNMATCHED : 103 /** 104 * Structure: 105 * input(checkbox) 106 * <span> 107 * -<a> (if links is on) or <span> (if links is off) 108 * --<del> (if strikethrough is on) or --NOTHING-- 109 * -</a> or </span> 110 * </span> 111 */ 112 $handler->todotitle = $match; 113 break; 114 case DOKU_LEXER_EXIT : 115 $data = array_merge(array ($state, 'todotitle' => $handler->todotitle, 'todoindex' => $handler->todo_index, 'todouser' => $handler->todo_user, 'checked' => $handler->checked), $handler->todoargs); 116 $handler->todo_index++; 117 #Delete temporary checked variable 118 unset($handler->todo_user); 119 unset($handler->checked); 120 unset($handler->todoargs); 121 unset($handler->todotitle); 122 return $data; 123 case DOKU_LEXER_SPECIAL : 124 if($match == self::TODO_UNCHECK_ALL) { 125 return array_merge(array($state, 'match' => $match)); 126 } 127 break; 128 } 129 return array(); 130 } 131 132 /** 133 * Handle the actual output creation. 134 * 135 * @param $mode String The output format to generate. 136 * @param $renderer Doku_Renderer A reference to the renderer object. 137 * @param $data Array The data created by the <tt>handle()</tt> method. 138 * @return Boolean true: if rendered successfully, or false: otherwise. 139 */ 140 public function render($mode, Doku_Renderer $renderer, $data) { 141 global $ID; 142 143 if(empty($data)) { 144 return false; 145 } 146 147 $state = $data[0]; 148 149 if($mode == 'xhtml') { 150 /** @var $renderer Doku_Renderer_xhtml */ 151 switch($state) { 152 case DOKU_LEXER_EXIT : 153 #Output our result 154 $renderer->doc .= $this->createTodoItem($renderer, $ID, array_merge($data, array('checkbox'=>'yes'))); 155 return true; 156 case DOKU_LEXER_SPECIAL : 157 if(isset($data['match']) && $data['match'] == self::TODO_UNCHECK_ALL) { 158 $renderer->doc .= '<button type="button" class="todouncheckall">' . $this->getLang('uncheckall_todos') . '</button>'; 159 } 160 return true; 161 } 162 } 163 return false; 164 } 165 166 /** 167 * Parse the arguments of todotag 168 * 169 * @param string $todoargs 170 * @return array(bool, false|string) with checked and user 171 */ 172 protected function parseTodoArgs($todoargs) { 173 $data['checked'] = false; 174 unset($data['start']); 175 unset($data['due']); 176 unset($data['completeddate']); 177 $data['showdate'] = $this->getConf("ShowdateTag"); 178 $data['username'] = $this->getConf("Username"); 179 $data['priority'] = 0; 180 $options = explode(' ', $todoargs); 181 foreach($options as $option) { 182 $option = trim($option); 183 if(empty($option)) continue; 184 if($option[0] == '@') { 185 $data['todousers'][] = substr($option, 1); //fill todousers array 186 if(!isset($data['todouser'])) $data['todouser'] = substr($option, 1); //set the first/main todouser 187 } 188 elseif($option[0] == '#') { 189 $data['checked'] = true; 190 @list($completeduser, $completeddate) = explode(':', $option, 2); 191 $data['completeduser'] = substr($completeduser, 1); 192 if(date('Y-m-d', strtotime($completeddate)) == $completeddate) { 193 $data['completeddate'] = new DateTime($completeddate); 194 } 195 } 196 elseif($option[0] == '!') { 197 $plen = strlen($option); 198 $excl_count = substr_count($option, "!"); 199 if (($plen == $excl_count) && ($excl_count >= 0)) { 200 $data['priority'] = $excl_count; 201 } 202 } 203 else { 204 @list($key, $value) = explode(':', $option, 2); 205 switch($key) { 206 case 'username': 207 if(in_array($value, array('user', 'real', 'none'))) { 208 $data['username'] = $value; 209 } 210 else { 211 $data['username'] = 'none'; 212 } 213 break; 214 case 'start': 215 if(date('Y-m-d', strtotime($value)) == $value) { 216 $data['start'] = new DateTime($value); 217 } 218 break; 219 case 'due': 220 if(date('Y-m-d', strtotime($value)) == $value) { 221 $data['due'] = new DateTime($value); 222 } 223 break; 224 case 'showdate': 225 if(in_array($value, array('yes', 'no'))) { 226 $data['showdate'] = ($value == 'yes'); 227 } 228 break; 229 } 230 } 231 } 232 return $data; 233 } 234 235 /** 236 * @param Doku_Renderer_xhtml $renderer 237 * @param string $id of page 238 * @param array $data data for rendering options 239 * @return string html of an item 240 */ 241 protected function createTodoItem($renderer, $id, $data) { 242 //set correct context 243 global $ID, $INFO; 244 $oldID = $ID; 245 $ID = $id; 246 $todotitle = $data['todotitle']; 247 $todoindex = $data['todoindex']; 248 $checked = $data['checked']; 249 $return = '<span class="todo">'; 250 251 if($data['checkbox']) { 252 $return .= '<input type="checkbox" class="todocheckbox"' 253 . ' data-index="' . $todoindex . '"' 254 . ' data-date="' . hsc(@filemtime(wikiFN($ID))) . '"' 255 . ' data-pageid="' . hsc($ID) . '"' 256 . ' data-strikethrough="' . ($this->getConf("Strikethrough") ? '1' : '0') . '"' 257 . ($checked ? ' checked="checked"' : '') . ' /> '; 258 } 259 260 // Username(s) of todouser(s) 261 if (!isset($data['todousers'])) $data['todousers']=array(); 262 $todousers = array(); 263 foreach($data['todousers'] as $user) { 264 if (($user = $this->_prepUsername($user,$data['username'])) != '') { 265 $todousers[] = $user; 266 } 267 } 268 $todouser=join(', ',$todousers); 269 270 if($todouser!='') { 271 $return .= '<span class="todouser">[' . hsc($todouser) . ']</span>'; 272 } 273 if(isset($data['completeduser']) && ($checkeduser=$this->_prepUsername($data['completeduser'],$data['username']))!='') { 274 $return .= '<span class="todouser">[' . hsc('✓ '.$checkeduser); 275 if(isset($data['completeddate'])) { $return .= ', '.$data['completeddate']->format('Y-m-d'); } 276 $return .= ']</span>'; 277 } 278 279 // start/due date 280 unset($bg); 281 $now = new DateTime("now"); 282 if(!$checked && (isset($data['start']) || isset($data['due'])) && (!isset($data['start']) || $data['start']<$now) && (!isset($data['due']) || $now<$data['due'])) $bg='todostarted'; 283 if(!$checked && isset($data['due']) && $now>=$data['due']) $bg='tododue'; 284 285 // show start/due date 286 if($data['showdate'] == 1 && (isset($data['start']) || isset($data['due']))) { 287 $return .= '<span class="tododates">['; 288 if(isset($data['start'])) { $return .= $data['start']->format('Y-m-d'); } 289 $return .= ' → '; 290 if(isset($data['due'])) { $return .= $data['due']->format('Y-m-d'); } 291 $return .= ']</span>'; 292 } 293 294 // priority 295 $priorityclass = ''; 296 if (isset($data['priority'])) { 297 $priority = $data['priority']; 298 if ($priority == 1) $priorityclass = ' todolow'; 299 else if ($priority == 2) $priorityclass = ' todomedium'; 300 else if ($priority >= 3) $priorityclass = ' todohigh'; 301 } 302 303 $spanclass = 'todotext' . $priorityclass; 304 if($this->getConf("CheckboxText") && !$this->getConf("AllowLinks") && $oldID == $ID && $data['checkbox']) { 305 $spanclass .= ' clickabletodo todohlght'; 306 } 307 if(isset($bg)) $spanclass .= ' '.$bg; 308 $return .= '<span class="' . $spanclass . '">'; 309 310 if($checked && $this->getConf("Strikethrough")) { 311 $return .= '<del>'; 312 } 313 $return .= '<span class="todoinnertext">'; 314 if($this->getConf("AllowLinks")) { 315 $return .= $this->_createLink($renderer, $todotitle, $todotitle); 316 } else { 317 if ($oldID != $ID) { 318 $return .= $renderer->internallink($id, $todotitle, null, true); 319 } else { 320 $return .= hsc($todotitle); 321 } 322 } 323 $return .= '</span>'; 324 325 if($checked && $this->getConf("Strikethrough")) { 326 $return .= '</del>'; 327 } 328 329 $return .= '</span></span>'; 330 331 //restore page ID 332 $ID = $oldID; 333 return $return; 334 } 335 336 /** 337 * Prepare user name string. 338 * 339 * @param string $username 340 * @param string $displaytype - one of 'user', 'real', 'none' 341 * @return string 342 */ 343 private function _prepUsername($username, $displaytype) { 344 345 switch ($displaytype) { 346 case "real": 347 global $auth; 348 $username = $auth->getUserData($username)['name']; 349 break; 350 case "none": 351 $username=""; 352 break; 353 case "user": 354 default: 355 break; 356 } 357 358 return $username; 359 } 360 361 /** 362 * Generate links from our Actions if necessary. 363 * 364 * @param Doku_Renderer_xhtml $renderer 365 * @param string $pagename 366 * @param string $name 367 * @return string 368 */ 369 private function _createLink($renderer, $pagename, $name = NULL) { 370 $id = $this->_composePageid($pagename); 371 372 return $renderer->internallink($id, $name, null, true); 373 } 374 375 /** 376 * Compose the pageid of the pages linked by a todoitem 377 * 378 * @param string $pagename 379 * @return string page id 380 */ 381 private function _composePageid($pagename) { 382 #Get the ActionNamespace and make sure it ends with a : (if not, add it) 383 $actionNamespace = $this->getConf("ActionNamespace"); 384 if(strlen($actionNamespace) == 0 || substr($actionNamespace, -1) != ':') { 385 $actionNamespace .= ":"; 386 } 387 388 #Replace ':' in $pagename so we don't create unnecessary namespaces 389 $pagename = str_replace(':', '-', $pagename); 390 391 //resolve and build link 392 $id = $actionNamespace . $pagename; 393 return $id; 394 } 395 396} 397 398//Setup VIM: ex: et ts=4 enc=utf-8 : 399