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