1<?php 2/** 3 * DokuWiki Syntax Plugin GTD (Getting Things Done) 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Michael Klier <chi@chimeric.de> 7 */ 8// must be run within DokuWiki 9if(!defined('DOKU_INC')) die(); 10 11if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 12require_once(DOKU_PLUGIN.'syntax.php'); 13 14if(!defined('DOKU_LF')) define('DOKU_LF',"\n"); 15 16/** 17 * All DokuWiki plugins to extend the parser/rendering mechanism 18 * need to inherit from this class 19 */ 20class syntax_plugin_gtd extends DokuWiki_Syntax_Plugin { 21 22 /** 23 * return some info 24 */ 25 function getInfo(){ 26 return array( 27 'author' => 'Michael Klier', 28 'email' => 'chi@chimeric.de', 29 'date' => @file_get_contents(DOKU_PLUGIN.'gtd/VERSION'), 30 'name' => 'GTD (Getting Things Done)', 31 'desc' => 'Implements a ToDo List following the principles of GTD.', 32 'url' => 'http://dokuwiki.org/plugin:gtd', 33 ); 34 } 35 36 function getType() { return 'substition'; } 37 function getPType() { return 'block'; } 38 function getSort() { return 320; } 39 40 /** 41 * Connect pattern to lexer 42 */ 43 function connectTo($mode) { $this->Lexer->addSpecialPattern('<gtd.*?>.+?</gtd>',$mode,'plugin_gtd'); } 44 45 /** 46 * Handle the match 47 */ 48 function handle($match, $state, $pos, &$handler) { 49 50 // check for modified expiries 51 if(preg_match("#<gtd(.*?)>#", $match, $params)) { 52 if(!empty($params[1])) { 53 $expiries = array(); 54 if(preg_match_all("#((warn|due)=(\d+))#", $params[1], $opts)) { 55 for($i=0;$i<count($opts[2]);$i++) { 56 if($opts[2][$i] == 'warn' or $opts[2][$i] == 'due') { 57 $expiries[$opts[2][$i]] = $opts[3][$i]; 58 } 59 } 60 } 61 } 62 } 63 64 // set global expiries if not given yet 65 if(!empty($expiries)) { 66 $expiries['warn'] = 5; 67 $expiries['due'] = 2; 68 } 69 70 $match = preg_replace('#<gtd.*?>|</gtd>#', '', $match); 71 $todolist = $this->_todo2array($match, $expiries); 72 73 return ($todolist); 74 } 75 76 /** 77 * Creates the output 78 * 79 * @author Michael Klier <chi@chimeric.de> 80 */ 81 function render($mode, &$renderer, $data) { 82 if($mode == 'xhtml'){ 83 $renderer->info['cache'] = false; 84 $renderer->doc .= $this->_todolist_xhtml($data); 85 return true; 86 } 87 return false; 88 } 89 90 /** 91 * Parses the todo list into an associative array 92 * 93 * @author Michael Klier <chi@chimeric.de> 94 */ 95 function _todo2array($data, $expiries) { 96 global $conf; 97 global $ACT; 98 99 // check if we have a serialized todolist already 100 if($ACT != 'save' and $ACT != 'preview' and $ACT != 'edit') { 101 $fn = $this->_todoFN(md5($data)); 102 if(file_exists($fn)) { 103 return unserialize(io_readFile($fn, false)); 104 } 105 } 106 107 $todos_bydate = array(); 108 $todos_nodate = array(); 109 $todolist = array(); 110 111 $lines = explode("\n\n",trim($data)); 112 113 foreach($lines as $line) { 114 115 // skip empty lines 116 if(empty($line)) continue; 117 118 $todo = array(); 119 $project = ''; 120 $context = ''; 121 $due = ''; 122 $desc = ''; 123 124 list($params, $desc) = explode("\n", trim($line)); 125 $todo['desc'] = trim($desc); 126 127 // check if done 128 if($line{0} == '#') { 129 $todo['done'] = true; 130 $line = substr($line, 1); 131 } else { 132 $todo['done'] = false; 133 } 134 135 // filter context 136 if(preg_match("#@(\S+)#", $params, $match)) { 137 $todo['context'] = str_replace('_', ' ',$match[1]); 138 $params = trim(str_replace($match[0], '', $params)); 139 } else { 140 // no context was given - ignore 141 continue; 142 } 143 144 // filter project 145 if(preg_match("#\bp:(\S+)#", $params, $match)) { 146 $todo['project'] = str_replace('_', ' ', $match[1]); 147 $params = trim(str_replace($match[0], '', $params)); 148 } 149 150 // filter warning expiries 151 if(preg_match("#\bw:(\d{2})\b#", $params, $match)) { 152 $expiries['warn'] = $match[1]; 153 $params = trim(str_replace($match[0], '', $params)); 154 } 155 156 // filter date 157 if(preg_match("#\bd:(\d{4}-\d{2}-\d{2})\b#", $params, $match)) { 158 $todo['date'] = $match[1]; 159 $todo['priority'] = $this->_get_priority($todo['date'], $expiries); 160 $params = trim(str_replace($match[0], '', $params)); 161 } elseif(preg_match("#\bd:(\d{2}-\d{2})#", $params, $match)) { 162 $todo['date'] = date('Y') . '-' . $match[1]; 163 $todo['priority'] = $this->_get_priority($todo['date'], $expiries); 164 $params = trim(str_replace($match[0], '', $params)); 165 } elseif(preg_match("#\bd:(\d{2})\b#", $params, $match)) { 166 $todo['date'] = date('Y') . '-' . date('m') . '-' . $match[1]; 167 $todo['priority'] = $this->_get_priority($todo['date'], $expiries); 168 $params = trim(str_replace($match[0], '', $params)); 169 } 170 171 if($todo['date']) { 172 $todos_bydate[$todo['date']][] = $todo; 173 } else { 174 array_push($todos_nodate, $todo); 175 } 176 } 177 178 // do some expensive sorting 179 $dates = array_keys($todos_bydate); 180 natsort($dates); 181 182 // sort todos by dates first 183 foreach($dates as $date) { 184 foreach($todos_bydate[$date] as $todo) { 185 if(!empty($todo['project'])) { 186 $todolist[$todo['context']]['projects'][$todo['project']][] = array( 'date' => $todo['date'], 187 'desc' => $todo['desc'], 188 'priority' => $todo['priority'], 189 'done' => $todo['done'] ); 190 } else { 191 $todolist[$todo['context']]['todos'][] = array( 'date' => $todo['date'], 192 'desc' => $todo['desc'], 193 'priority' => $todo['priority'], 194 'done' => $todo['done'] ); 195 } 196 } 197 } 198 199 // sort todos with no date provided 200 foreach($todos_nodate as $todo) { 201 if(!empty($todo['project'])) { 202 $todolist[$todo['context']]['projects'][$todo['project']][] = array( 'desc' => $todo['desc'], 203 'priority' => $todo['priority'], 204 'done' => $todo['done'] ); 205 } else { 206 $todolist[$todo['context']]['todos'][] = array( 'desc' => $todo['desc'], 207 'priority' => $todo['priority'], 208 'done' => $todo['done'] ); 209 } 210 } 211 212 // serialize todolist so we don't have to render it each time 213 if($ACT == 'save') { 214 if(!file_exists($conf['savedir'] . '/cache/gtd/')) { 215 mkdir($conf['savedir'] . '/cache/gtd/', $conf['dmode']); 216 } 217 io_saveFile($this->_todoFN(md5($data)), serialize($todolist)); 218 } 219 220 // we're done return the list 221 return ($todolist); 222 } 223 224 /** 225 * Generates the XHTML output 226 * 227 * @author Michael Klier <chi@chimeric.de> 228 */ 229 function _todolist_xhtml($todolist) { 230 $out = ''; 231 232 // create new renderer for the description part 233 $renderer = & new Doku_Renderer_xhtml(); 234 $renderer->smileys = getSmileys(); 235 $renderer->entities = getEntities(); 236 $renderer->acronyms = getAcronyms(); 237 $renderer->interwiki = getInterwiki(); 238 239 foreach($todolist as $context => $todos) { 240 $out .= '<div class="plugin_gtd_box">' . DOKU_LF; 241 $out .= '<h2 class="plugin_gtd_context">' . htmlspecialchars($context) . '</h2>' . DOKU_LF; 242 $out .= '<ul class="plugin_gtd_list">' . DOKU_LF; 243 244 if(!empty($todolist[$context]['projects'])) { 245 foreach($todolist[$context]['projects'] as $project => $todos) { 246 $out .= '<li class="plugin_gtd_project"><span class="li plugin_gtd_project">' . htmlspecialchars($project) . '</span>' . DOKU_LF; 247 $out .= '<ul class="plugin_gtd_project">' . DOKU_LF; 248 foreach($todos as $todo) { 249 $out .= @$this->_todo_xhtml(&$renderer, $todo); 250 } 251 $out .= '</ul>' . DOKU_LF; 252 $out .= '</li>' . DOKU_LF; 253 } 254 } 255 256 if(!empty($todolist[$context]['todos'])) { 257 $out .= '<li class="plugin_gtd_project"><span class="li plugin_gtd_project">' . $this->getLang('noproject') . '</span>' . DOKU_LF; 258 $out .= '<ul class="plugin_gtd_project">' . DOKU_LF; 259 foreach($todolist[$context]['todos'] as $todo) { 260 $out .= @$this->_todo_xhtml(&$renderer, $todo); 261 } 262 $out .= '</ul>' . DOKU_LF; 263 $out .= '</li>' . DOKU_LF; 264 } 265 266 $out .= '</ul>' . DOKU_LF; 267 $out .= '</div>' . DOKU_LF; 268 } 269 270 return ($out); 271 } 272 273 /** 274 * returns the xhtml for single todo item 275 * 276 * @author Michael Klier <chi@chimeric.de> 277 */ 278 function _todo_xhtml(&$renderer, $todo) { 279 $out = ''; 280 281 // reset doc 282 $renderer->doc = ''; 283 284 $out .= '<li class="plugin_gtd_item '; 285 if(isset($todo['date'])) { 286 if(!$todo['done']) { 287 $out .= 'plugin_gtd_' . $todo['priority']; 288 } 289 } 290 $out .= '"><div class="li">' . DOKU_LF; 291 292 293 if(isset($todo['date'])) { 294 $out .= '<div class="plugin_gtd_date">' . $todo['date'] . '</span>' . DOKU_LF; 295 } 296 297 $out .= '<div class="plugin_gtd_desc">'; 298 299 if($todo['done']) $out .= '<del>'; 300 301 // turn description into instructions 302 $instructions = p_get_instructions($todo['desc']); 303 304 // loop thru instructions 305 foreach($instructions as $instruction) { 306 call_user_func_array(array(&$renderer, $instruction[0]),$instruction[1]); 307 } 308 309 // strip <p> and </p> 310 $desc = $renderer->doc; 311 $desc = str_replace("<p>", '', $desc); 312 $desc = str_replace("</p>", '', $desc); 313 $out .= $desc; 314 315 if($todo['done']) $out .= '</del>'; 316 317 $out .= '</div>' . DOKU_LF; 318 $out .= '</div></li>' . DOKU_LF; 319 320 return ($out); 321 } 322 323 /** 324 * Calculates the priority by a given date string 325 * and returns the CSS class to use 326 * 327 * @author Michael Klier <chi@chimeric.de> 328 */ 329 function _get_priority($date, $expiries) { 330 331 $ctime = time(); 332 list($y,$m,$d) = explode('-', $date); 333 $etime = mktime(0, 0, 0, $m, $d, $y); 334 335 if(($etime - $ctime) < 0) return 'pass'; 336 if(($etime - $ctime) <= 60*60*24*$expiries['due']) return 'due'; 337 if(($etime - $ctime) <= 60*60*24*$expiries['warn']) return 'warn'; 338 return 'upco'; 339 } 340 341 /** 342 * Returns a file name to store the todolist 343 * 344 * @author Michael Klier <chi@chimeric.de> 345 */ 346 function _todoFN($md5) { 347 global $ID; 348 global $conf; 349 350 $ID = cleanID($ID); 351 $ID = str_replace(':', '/', $ID); 352 $ID = utf8_encodeFN($ID); 353 $fn = $conf['savedir'] . '/cache/gtd/' . $ID . '.' . $md5 . '.gtd'; 354 return ($fn); 355 } 356} 357 358// vim:ts=4:sw=4:et:enc=utf-8: 359