1aa11d6a4Sleibler<?php 2aa11d6a4Sleibler/** 3aa11d6a4Sleibler * ToDo Plugin: Creates a checkbox based todo list 4aa11d6a4Sleibler * 5*4b7f33f3SChristian Marg * Syntax: <todo [...options...] [#]>Name of Action</todo> - 6aa11d6a4Sleibler * Creates a Checkbox with the "Name of Action" as 7aa11d6a4Sleibler * the text associated with it. The hash (#, optional) 8aa11d6a4Sleibler * will cause the checkbox to be checked by default. 9*4b7f33f3SChristian Marg * See https://www.dokuwiki.org/plugin:todo#usage_and_examples 10*4b7f33f3SChristian Marg * for possible options and examples. 11aa11d6a4Sleibler * 12aa11d6a4Sleibler * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 13aa11d6a4Sleibler * @author Babbage <babbage@digitalbrink.com>; Leo Eibler <dokuwiki@sprossenwanne.at> 14aa11d6a4Sleibler */ 15aa11d6a4Sleibler 16aa11d6a4Sleiblerif(!defined('DOKU_INC')) die(); 17aa11d6a4Sleibler 18aa11d6a4Sleibler/** 19aa11d6a4Sleibler * All DokuWiki plugins to extend the parser/rendering mechanism 20aa11d6a4Sleibler * need to inherit from this class 21aa11d6a4Sleibler */ 22aa11d6a4Sleiblerclass syntax_plugin_todo_todo extends DokuWiki_Syntax_Plugin { 23aa11d6a4Sleibler 244afeeeedSRobertWeinmeister const TODO_UNCHECK_ALL = '~~TODO:UNCHECKALL~~'; 254afeeeedSRobertWeinmeister 26aa11d6a4Sleibler /** 27aa11d6a4Sleibler * Get the type of syntax this plugin defines. 28aa11d6a4Sleibler * 29aa11d6a4Sleibler * @return String 30aa11d6a4Sleibler */ 31aa11d6a4Sleibler public function getType() { 32aa11d6a4Sleibler return 'substition'; 33aa11d6a4Sleibler } 34aa11d6a4Sleibler 35aa11d6a4Sleibler /** 36aa11d6a4Sleibler * Paragraph Type 37aa11d6a4Sleibler * 38aa11d6a4Sleibler * 'normal' - The plugin can be used inside paragraphs 39aa11d6a4Sleibler * 'block' - Open paragraphs need to be closed before plugin output 40aa11d6a4Sleibler * 'stack' - Special case. Plugin wraps other paragraphs. 41aa11d6a4Sleibler */ 42aa11d6a4Sleibler function getPType(){ 43aa11d6a4Sleibler return 'normal'; 44aa11d6a4Sleibler } 45aa11d6a4Sleibler 46aa11d6a4Sleibler /** 47aa11d6a4Sleibler * Where to sort in? 48aa11d6a4Sleibler * 49aa11d6a4Sleibler * @return Integer 50aa11d6a4Sleibler */ 51aa11d6a4Sleibler public function getSort() { 52aa11d6a4Sleibler return 999; 53aa11d6a4Sleibler } 54aa11d6a4Sleibler 55aa11d6a4Sleibler /** 56aa11d6a4Sleibler * Connect lookup pattern to lexer. 57aa11d6a4Sleibler * 58aa11d6a4Sleibler * @param $mode String The desired rendermode. 59aa11d6a4Sleibler * @return void 60aa11d6a4Sleibler * @see render() 61aa11d6a4Sleibler */ 62aa11d6a4Sleibler public function connectTo($mode) { 63aa11d6a4Sleibler $this->Lexer->addEntryPattern('<todo[\s]*?.*?>(?=.*?</todo>)', $mode, 'plugin_todo_todo'); 644afeeeedSRobertWeinmeister $this->Lexer->addSpecialPattern(self::TODO_UNCHECK_ALL, $mode, 'plugin_todo_todo'); 65da0f1646SPiotr Orzechowski $this->Lexer->addSpecialPattern('~~NOTODO~~', $mode, 'plugin_todo_todo'); 66aa11d6a4Sleibler } 67aa11d6a4Sleibler 68aa11d6a4Sleibler public function postConnect() { 69aa11d6a4Sleibler $this->Lexer->addExitPattern('</todo>', 'plugin_todo_todo'); 70aa11d6a4Sleibler } 71aa11d6a4Sleibler 72aa11d6a4Sleibler /** 73aa11d6a4Sleibler * Handler to prepare matched data for the rendering process. 74aa11d6a4Sleibler * 75aa11d6a4Sleibler * @param $match string The text matched by the patterns. 76aa11d6a4Sleibler * @param $state int The lexer state for the match. 77aa11d6a4Sleibler * @param $pos int The character position of the matched text. 7895c3008fSrunout-at * @param $handler Doku_Handler Reference to the Doku_Handler object. 79fab83bd7Srobert * @return array An empty array for most cases, except: 80fab83bd7Srobert - DOKU_LEXER_EXIT: An array containing the current lexer state 81fab83bd7Srobert and information about the just lexed todo. 82fab83bd7Srobert - DOKU_LEXER_SPECIAL: For the special pattern of the Uncheck-All-Button, an 83fab83bd7Srobert array containing the current lexer state and the matched text. 84aa11d6a4Sleibler */ 858038693eSAndreas Gohr public function handle($match, $state, $pos, Doku_Handler $handler) { 86aa11d6a4Sleibler switch($state) { 87aa11d6a4Sleibler case DOKU_LEXER_ENTER : 88aa11d6a4Sleibler #Search to see if the '#' is in the todotag (if so, this means the Action has been completed) 89aa11d6a4Sleibler $x = preg_match('%<todo([^>]*)>%i', $match, $tododata); 90aa11d6a4Sleibler if($x) { 915a60355cSrunout-at $handler->todoargs = $this->parseTodoArgs($tododata[1]); 92aa11d6a4Sleibler } 9334b50c5bSLaurent Forthomme if(!isset($handler->todo_index) || !is_numeric($handler->todo_index)) { 94aa11d6a4Sleibler $handler->todo_index = 0; 95aa11d6a4Sleibler } 9634b50c5bSLaurent Forthomme $handler->todo_user = ''; 9734b50c5bSLaurent Forthomme $handler->checked = ''; 987e2ba960SRobert Weinmeister $handler->todotitle = ''; 99aa11d6a4Sleibler break; 100aa11d6a4Sleibler case DOKU_LEXER_MATCHED : 101aa11d6a4Sleibler break; 102aa11d6a4Sleibler case DOKU_LEXER_UNMATCHED : 103aa11d6a4Sleibler /** 104aa11d6a4Sleibler * Structure: 105aa11d6a4Sleibler * input(checkbox) 106aa11d6a4Sleibler * <span> 107aa11d6a4Sleibler * -<a> (if links is on) or <span> (if links is off) 108aa11d6a4Sleibler * --<del> (if strikethrough is on) or --NOTHING-- 109aa11d6a4Sleibler * -</a> or </span> 110aa11d6a4Sleibler * </span> 111aa11d6a4Sleibler */ 1127e2ba960SRobert Weinmeister $handler->todotitle = $match; 113aa11d6a4Sleibler break; 114aa11d6a4Sleibler case DOKU_LEXER_EXIT : 1157e2ba960SRobert Weinmeister $data = array_merge(array ($state, 'todotitle' => $handler->todotitle, 'todoindex' => $handler->todo_index, 'todouser' => $handler->todo_user, 'checked' => $handler->checked), $handler->todoargs); 1167e2ba960SRobert Weinmeister $handler->todo_index++; 117aa11d6a4Sleibler #Delete temporary checked variable 118aa11d6a4Sleibler unset($handler->todo_user); 119aa11d6a4Sleibler unset($handler->checked); 1205a60355cSrunout-at unset($handler->todoargs); 1217e2ba960SRobert Weinmeister unset($handler->todotitle); 1227e2ba960SRobert Weinmeister return $data; 123aa11d6a4Sleibler case DOKU_LEXER_SPECIAL : 1244afeeeedSRobertWeinmeister if($match == self::TODO_UNCHECK_ALL) { 125b35bdc5cSrobert return array_merge(array($state, 'match' => $match)); 1264afeeeedSRobertWeinmeister } 127aa11d6a4Sleibler break; 128aa11d6a4Sleibler } 129aa11d6a4Sleibler return array(); 130aa11d6a4Sleibler } 131aa11d6a4Sleibler 132aa11d6a4Sleibler /** 133aa11d6a4Sleibler * Handle the actual output creation. 134aa11d6a4Sleibler * 135aa11d6a4Sleibler * @param $mode String The output format to generate. 13695c3008fSrunout-at * @param $renderer Doku_Renderer A reference to the renderer object. 137aa11d6a4Sleibler * @param $data Array The data created by the <tt>handle()</tt> method. 138aa11d6a4Sleibler * @return Boolean true: if rendered successfully, or false: otherwise. 139aa11d6a4Sleibler */ 1408038693eSAndreas Gohr public function render($mode, Doku_Renderer $renderer, $data) { 141aa11d6a4Sleibler global $ID; 142c5dc5b57Srobert 143c5dc5b57Srobert if(empty($data)) { 144c5dc5b57Srobert return false; 145c5dc5b57Srobert } 146c5dc5b57Srobert 147b35bdc5cSrobert $state = $data[0]; 148c5dc5b57Srobert 149aa11d6a4Sleibler if($mode == 'xhtml') { 150aa11d6a4Sleibler /** @var $renderer Doku_Renderer_xhtml */ 1514afeeeedSRobertWeinmeister switch($state) { 1524afeeeedSRobertWeinmeister case DOKU_LEXER_EXIT : 153aa11d6a4Sleibler #Output our result 1547cf7b3f5Srunout-at $renderer->doc .= $this->createTodoItem($renderer, $ID, array_merge($data, array('checkbox'=>'yes'))); 155aa11d6a4Sleibler return true; 1564afeeeedSRobertWeinmeister case DOKU_LEXER_SPECIAL : 157b35bdc5cSrobert if(isset($data['match']) && $data['match'] == self::TODO_UNCHECK_ALL) { 1584afeeeedSRobertWeinmeister $renderer->doc .= '<button type="button" class="todouncheckall">Uncheck all todos</button>'; 1594afeeeedSRobertWeinmeister } 1604afeeeedSRobertWeinmeister return true; 161aa11d6a4Sleibler } 162aa11d6a4Sleibler } 163aa11d6a4Sleibler return false; 164aa11d6a4Sleibler } 165aa11d6a4Sleibler 166aa11d6a4Sleibler /** 167aa11d6a4Sleibler * Parse the arguments of todotag 168aa11d6a4Sleibler * 169aa11d6a4Sleibler * @param string $todoargs 170aa11d6a4Sleibler * @return array(bool, false|string) with checked and user 171aa11d6a4Sleibler */ 172aa11d6a4Sleibler protected function parseTodoArgs($todoargs) { 173c23b17c3Srunout-at $data['checked'] = false; 174c23b17c3Srunout-at unset($data['start']); 1755a60355cSrunout-at unset($data['due']); 1766d15955dSrunout-at unset($data['completeddate']); 1773de1579cSrunout-at $data['showdate'] = $this->getConf("ShowdateTag"); 1787cf7b3f5Srunout-at $data['username'] = $this->getConf("Username"); 179180cffabSgreeneng $data['priority'] = 0; 18014bf2364Srunout-at $options = explode(' ', $todoargs); 1815a60355cSrunout-at foreach($options as $option) { 18214bf2364Srunout-at $option = trim($option); 18371f7153aSLaurent Forthomme if(empty($option)) continue; 1840e044d38Srunout-at if($option[0] == '@') { 1850e044d38Srunout-at $data['todousers'][] = substr($option, 1); //fill todousers array 1860e044d38Srunout-at if(!isset($data['todouser'])) $data['todouser'] = substr($option, 1); //set the first/main todouser 1870e044d38Srunout-at } 1886d15955dSrunout-at elseif($option[0] == '#') { 1896d15955dSrunout-at $data['checked'] = true; 1906d15955dSrunout-at @list($completeduser, $completeddate) = explode(':', $option, 2); 1916d15955dSrunout-at $data['completeduser'] = substr($completeduser, 1); 1926d15955dSrunout-at if(date('Y-m-d', strtotime($completeddate)) == $completeddate) { 1936d15955dSrunout-at $data['completeddate'] = new DateTime($completeddate); 1946d15955dSrunout-at } 1950e044d38Srunout-at } 196180cffabSgreeneng elseif($option[0] == '!') { 197180cffabSgreeneng $plen = strlen($option); 198180cffabSgreeneng $excl_count = substr_count($option, "!"); 199180cffabSgreeneng if (($plen == $excl_count) && ($excl_count >= 0)) { 200180cffabSgreeneng $data['priority'] = $excl_count; 201180cffabSgreeneng } 202180cffabSgreeneng } 2030e044d38Srunout-at else { 2045a60355cSrunout-at @list($key, $value) = explode(':', $option, 2); 2055a60355cSrunout-at switch($key) { 2067cf7b3f5Srunout-at case 'username': 2077cf7b3f5Srunout-at if(in_array($value, array('user', 'real', 'none'))) { 2087cf7b3f5Srunout-at $data['username'] = $value; 2097cf7b3f5Srunout-at } 210603b0325Srunout-at else { 211603b0325Srunout-at $data['username'] = 'none'; 212603b0325Srunout-at } 2137cf7b3f5Srunout-at break; 2145a60355cSrunout-at case 'start': 2155a60355cSrunout-at if(date('Y-m-d', strtotime($value)) == $value) { 2165a60355cSrunout-at $data['start'] = new DateTime($value); 217aa11d6a4Sleibler } 2185a60355cSrunout-at break; 2195a60355cSrunout-at case 'due': 2205a60355cSrunout-at if(date('Y-m-d', strtotime($value)) == $value) { 2215a60355cSrunout-at $data['due'] = new DateTime($value); 2225a60355cSrunout-at } 2235a60355cSrunout-at break; 2246c3557a6Srunout-at case 'showdate': 2256c3557a6Srunout-at if(in_array($value, array('yes', 'no'))) { 2263de1579cSrunout-at $data['showdate'] = ($value == 'yes'); 2276c3557a6Srunout-at } 2286c3557a6Srunout-at break; 2295a60355cSrunout-at } 2305a60355cSrunout-at } 2310e044d38Srunout-at } 2325a60355cSrunout-at return $data; 233aa11d6a4Sleibler } 234aa11d6a4Sleibler 235aa11d6a4Sleibler /** 23695c3008fSrunout-at * @param Doku_Renderer_xhtml $renderer 237aa11d6a4Sleibler * @param string $id of page 2385f8ffdd3Srunout-at * @param array $data data for rendering options 239aa11d6a4Sleibler * @return string html of an item 240aa11d6a4Sleibler */ 24195c3008fSrunout-at protected function createTodoItem($renderer, $id, $data) { 242aa11d6a4Sleibler //set correct context 2435f8ffdd3Srunout-at global $ID, $INFO; 244aa11d6a4Sleibler $oldID = $ID; 245aa11d6a4Sleibler $ID = $id; 2465a60355cSrunout-at $todotitle = $data['todotitle']; 2475a60355cSrunout-at $todoindex = $data['todoindex']; 2485a60355cSrunout-at $checked = $data['checked']; 2491de84fdaSRobert Weinmeister $return = '<span class="todo">'; 2507cf7b3f5Srunout-at 2515f8ffdd3Srunout-at if($data['checkbox']) { 252f2dea6eeSRobert Weinmeister $return .= '<input type="checkbox" class="todocheckbox"' 253aa11d6a4Sleibler . ' data-index="' . $todoindex . '"' 254aa11d6a4Sleibler . ' data-date="' . hsc(@filemtime(wikiFN($ID))) . '"' 255aa11d6a4Sleibler . ' data-pageid="' . hsc($ID) . '"' 256aa11d6a4Sleibler . ' data-strikethrough="' . ($this->getConf("Strikethrough") ? '1' : '0') . '"' 257aa11d6a4Sleibler . ($checked ? ' checked="checked"' : '') . ' /> '; 2585f8ffdd3Srunout-at } 2597cf7b3f5Srunout-at 2603f5a70e2SChristian Marg // Username(s) of todouser(s) 2613f5a70e2SChristian Marg if (!isset($data['todousers'])) $data['todousers']=array(); 2623f5a70e2SChristian Marg $todousers = array(); 2633f5a70e2SChristian Marg foreach($data['todousers'] as $user) { 2643f5a70e2SChristian Marg if (($user = $this->_prepUsername($user,$data['username'])) != '') { 2653f5a70e2SChristian Marg $todousers[] = $user; 2665f8ffdd3Srunout-at } 2673f5a70e2SChristian Marg } 2683f5a70e2SChristian Marg $todouser=join(', ',$todousers); 2693f5a70e2SChristian Marg 2703f5a70e2SChristian Marg if($todouser!='') { 271aa11d6a4Sleibler $return .= '<span class="todouser">[' . hsc($todouser) . ']</span>'; 272aa11d6a4Sleibler } 27360db5b5dSChristian Marg if(isset($data['completeduser']) && ($checkeduser=$this->_prepUsername($data['completeduser'],$data['username']))!='') { 27460db5b5dSChristian Marg $return .= '<span class="todouser">[' . hsc('✓ '.$checkeduser); 27560db5b5dSChristian Marg if(isset($data['completeddate'])) { $return .= ', '.$data['completeddate']->format('Y-m-d'); } 27660db5b5dSChristian Marg $return .= ']</span>'; 27760db5b5dSChristian Marg } 278aa11d6a4Sleibler 2797cf7b3f5Srunout-at // start/due date 2806c3557a6Srunout-at unset($bg); 2816c3557a6Srunout-at $now = new DateTime("now"); 2826c3557a6Srunout-at if(!$checked && (isset($data['start']) || isset($data['due'])) && (!isset($data['start']) || $data['start']<$now) && (!isset($data['due']) || $now<$data['due'])) $bg='todostarted'; 2836c3557a6Srunout-at if(!$checked && isset($data['due']) && $now>=$data['due']) $bg='tododue'; 2846c3557a6Srunout-at 2856c3557a6Srunout-at // show start/due date 286f238e23aSrunout-at if($data['showdate'] == 1 && (isset($data['start']) || isset($data['due']))) { 287f2dea6eeSRobert Weinmeister $return .= '<span class="tododates">['; 2886c3557a6Srunout-at if(isset($data['start'])) { $return .= $data['start']->format('Y-m-d'); } 2896c3557a6Srunout-at $return .= ' → '; 2906c3557a6Srunout-at if(isset($data['due'])) { $return .= $data['due']->format('Y-m-d'); } 2917cf7b3f5Srunout-at $return .= ']</span>'; 2926c3557a6Srunout-at } 2936c3557a6Srunout-at 294180cffabSgreeneng // priority 295180cffabSgreeneng $priorityclass = ''; 296180cffabSgreeneng if (isset($data['priority'])) { 297180cffabSgreeneng $priority = $data['priority']; 298180cffabSgreeneng if ($priority == 1) $priorityclass = ' todolow'; 299180cffabSgreeneng else if ($priority == 2) $priorityclass = ' todomedium'; 300180cffabSgreeneng else if ($priority >= 3) $priorityclass = ' todohigh'; 301180cffabSgreeneng } 302180cffabSgreeneng 303180cffabSgreeneng $spanclass = 'todotext' . $priorityclass; 30488bcff5aSMizunashi Mana if($this->getConf("CheckboxText") && !$this->getConf("AllowLinks") && $oldID == $ID && $data['checkbox']) { 305aa11d6a4Sleibler $spanclass .= ' clickabletodo todohlght'; 306aa11d6a4Sleibler } 3075a60355cSrunout-at if(isset($bg)) $spanclass .= ' '.$bg; 308aa11d6a4Sleibler $return .= '<span class="' . $spanclass . '">'; 309aa11d6a4Sleibler 310aa11d6a4Sleibler if($checked && $this->getConf("Strikethrough")) { 311aa11d6a4Sleibler $return .= '<del>'; 312aa11d6a4Sleibler } 313aa11d6a4Sleibler $return .= '<span class="todoinnertext">'; 314aa11d6a4Sleibler if($this->getConf("AllowLinks")) { 315aa11d6a4Sleibler $return .= $this->_createLink($renderer, $todotitle, $todotitle); 316aa11d6a4Sleibler } else { 317dd4e3816Srunout-at if ($oldID != $ID) { 3184d7e407eSrunout-at $return .= $renderer->internallink($id, $todotitle, null, true); 319c5232868Srunout-at } else { 3200120c277Srunout-at $return .= hsc($todotitle); 321aa11d6a4Sleibler } 322dd4e3816Srunout-at } 323aa11d6a4Sleibler $return .= '</span>'; 324aa11d6a4Sleibler 325aa11d6a4Sleibler if($checked && $this->getConf("Strikethrough")) { 326aa11d6a4Sleibler $return .= '</del>'; 327aa11d6a4Sleibler } 328aa11d6a4Sleibler 3291de84fdaSRobert Weinmeister $return .= '</span></span>'; 330aa11d6a4Sleibler 331aa11d6a4Sleibler //restore page ID 332aa11d6a4Sleibler $ID = $oldID; 333aa11d6a4Sleibler return $return; 334aa11d6a4Sleibler } 335aa11d6a4Sleibler 336aa11d6a4Sleibler /** 3373f5a70e2SChristian Marg * Prepare user name string. 3383f5a70e2SChristian Marg * 3393f5a70e2SChristian Marg * @param string $username 3403f5a70e2SChristian Marg * @param string $displaytype - one of 'user', 'real', 'none' 3413f5a70e2SChristian Marg * @return string 3423f5a70e2SChristian Marg */ 3433f5a70e2SChristian Marg private function _prepUsername($username, $displaytype) { 3443f5a70e2SChristian Marg 3453f5a70e2SChristian Marg switch ($displaytype) { 3463f5a70e2SChristian Marg case "real": 3473f5a70e2SChristian Marg global $auth; 3483f5a70e2SChristian Marg $username = $auth->getUserData($username)['name']; 3493f5a70e2SChristian Marg break; 3503f5a70e2SChristian Marg case "none": 3513f5a70e2SChristian Marg $username=""; 3523f5a70e2SChristian Marg break; 3533f5a70e2SChristian Marg case "user": 3543f5a70e2SChristian Marg default: 3553f5a70e2SChristian Marg break; 3563f5a70e2SChristian Marg } 3573f5a70e2SChristian Marg 3583f5a70e2SChristian Marg return $username; 3593f5a70e2SChristian Marg } 3603f5a70e2SChristian Marg 3613f5a70e2SChristian Marg /** 362aa11d6a4Sleibler * Generate links from our Actions if necessary. 363aa11d6a4Sleibler * 36495c3008fSrunout-at * @param Doku_Renderer_xhtml $renderer 365aa11d6a4Sleibler * @param string $pagename 366aa11d6a4Sleibler * @param string $name 367aa11d6a4Sleibler * @return string 368aa11d6a4Sleibler */ 36995c3008fSrunout-at private function _createLink($renderer, $pagename, $name = NULL) { 370aa11d6a4Sleibler $id = $this->_composePageid($pagename); 371aa11d6a4Sleibler 372aa11d6a4Sleibler return $renderer->internallink($id, $name, null, true); 373aa11d6a4Sleibler } 374aa11d6a4Sleibler 375aa11d6a4Sleibler /** 376aa11d6a4Sleibler * Compose the pageid of the pages linked by a todoitem 377aa11d6a4Sleibler * 378aa11d6a4Sleibler * @param string $pagename 379aa11d6a4Sleibler * @return string page id 380aa11d6a4Sleibler */ 381aa11d6a4Sleibler private function _composePageid($pagename) { 382aa11d6a4Sleibler #Get the ActionNamespace and make sure it ends with a : (if not, add it) 383aa11d6a4Sleibler $actionNamespace = $this->getConf("ActionNamespace"); 384aa11d6a4Sleibler if(strlen($actionNamespace) == 0 || substr($actionNamespace, -1) != ':') { 385aa11d6a4Sleibler $actionNamespace .= ":"; 386aa11d6a4Sleibler } 387aa11d6a4Sleibler 388aa11d6a4Sleibler #Replace ':' in $pagename so we don't create unnecessary namespaces 389aa11d6a4Sleibler $pagename = str_replace(':', '-', $pagename); 390aa11d6a4Sleibler 391aa11d6a4Sleibler //resolve and build link 392aa11d6a4Sleibler $id = $actionNamespace . $pagename; 393aa11d6a4Sleibler return $id; 394aa11d6a4Sleibler } 395aa11d6a4Sleibler 396aa11d6a4Sleibler} 397aa11d6a4Sleibler 398aa11d6a4Sleibler//Setup VIM: ex: et ts=4 enc=utf-8 : 399