tag to a final "## done" section.
*
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
*/
use dokuwiki\Extension\ActionPlugin;
use dokuwiki\Extension\Event;
use dokuwiki\Extension\EventHandler;
if (!defined('DOKU_INC')) die();
class action_plugin_tododone extends ActionPlugin
{
const ACTION = 'movetaggedtodos';
const DONE_HEADING = '## done';
/**
* Register DokuWiki event hooks.
*
* @param EventHandler $controller
* @return void
*/
public function register(EventHandler $controller)
{
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleAction');
$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addMenuButton');
$controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addLegacyButton');
}
/**
* Add the action to modern page menus.
*
* @param Event $event
* @param mixed $param
* @return void
*/
public function addMenuButton(Event $event, $param)
{
global $ID, $REV;
if (($event->data['view'] ?? '') !== 'page') return;
if ($REV) return;
if (auth_quickaclcheck($ID) < AUTH_EDIT) return;
try {
array_splice($event->data['items'], 1, 0, [new \dokuwiki\plugin\tododone\MenuItem()]);
} catch (\Exception $ignored) {
// Some templates/menu configurations may reject custom items. The legacy
// pagetools hook below is kept as a fallback.
}
}
/**
* Add the action to legacy page tool menus.
*
* @param Event $event
* @param mixed $param
* @return void
*/
public function addLegacyButton(Event $event, $param)
{
global $ID, $REV;
if (($event->data['view'] ?? '') !== 'main') return;
if ($REV) return;
if (auth_quickaclcheck($ID) < AUTH_EDIT) return;
if (!isset($event->data['items']) || !is_array($event->data['items'])) return;
$url = wl($ID, ['do' => self::ACTION, 'sectok' => getSecurityToken()], false, '&');
$event->data['items']['movetaggedtodos'] =
'
' .
'Move tagged todos';
}
/**
* Handle ?do=movetaggedtodos.
*
* @param Event $event
* @param mixed $param
* @return void
*/
public function handleAction(Event $event, $param)
{
global $ID, $REV;
if ($event->data !== self::ACTION) return;
$event->preventDefault();
$event->stopPropagation();
if ($REV) {
msg('Todo Done Mover cannot modify an old page revision.', -1);
$this->redirectToPage();
}
if (!checkSecurityToken()) {
$this->redirectToPage();
}
$info = pageinfo();
if (empty($info['exists'])) {
msg('Todo Done Mover: page does not exist.', -1);
$this->redirectToPage();
}
if (empty($info['editable']) || auth_quickaclcheck($ID) < AUTH_EDIT) {
msg('Todo Done Mover: you do not have permission to edit this page, or it is locked.', -1);
$this->redirectToPage();
}
$oldText = io_readWikiPage(wikiFN($ID), $ID);
list($newText, $movedCount) = $this->moveTaggedTodos($oldText);
if ($movedCount === 0 || $newText === $oldText) {
msg('Todo Done Mover: no items found to move.', 0);
$this->redirectToPage();
}
saveWikiText(
$ID,
$newText,
'Moved ' . $movedCount . ' tagged todo item(s) to ' . self::DONE_HEADING,
true
);
msg('Todo Done Mover: moved ' . $movedCount . ' tagged todo item(s) to ' . self::DONE_HEADING . '.', 1);
$this->redirectToPage();
}
/**
* Move whole-line todo items whose opening tag contains a whitespace-prefixed hashtag.
*
* Examples moved:
* a done today to move
*
* Examples not moved:
* not done todo
* also not done
*
* @param string $text Raw page source
* @return array [newText, movedCount]
*/
public function moveTaggedTodos($text)
{
$eol = (strpos($text, "\r\n") !== false) ? "\r\n" : "\n";
$work = str_replace(["\r\n", "\r"], "\n", $text);
$moved = [];
// Whole-line todo blocks only, with optional bullet marker. The hashtag must
// appear inside the opening tag as a token, for example " #done".
$pattern = '/^[^\S\n]*(?:[-*]\s+)?\n]*(?\n]*>.*?<\/todo>[^\S\n]*(?:\n|$)/ims';
$remaining = preg_replace_callback($pattern, function ($match) use (&$moved) {
$item = trim($match[0], "\n");
$item = rtrim($item, " \t\n");
if (trim($item) !== '') {
$moved[] = $item;
}
return '';
}, $work);
if (count($moved) === 0) {
return [$text, 0];
}
$remaining = rtrim($remaining, "\n");
// If the final Markdown-style level-2 section is not already "## done",
// create a new bottom section. This keeps the moved todos at page bottom.
if (!$this->finalLevelTwoSectionIsDone($remaining)) {
if ($remaining !== '') {
$remaining .= "\n\n";
}
$remaining .= self::DONE_HEADING;
}
if ($remaining !== '' && substr($remaining, -1) !== "\n") {
$remaining .= "\n";
}
$remaining .= implode("\n", $moved) . "\n";
return [str_replace("\n", $eol, $remaining), count($moved)];
}
/**
* Return true when the last Markdown-style level-2 section is exactly "## done".
*
* @param string $text Normalized source using \n line endings
* @return bool
*/
private function finalLevelTwoSectionIsDone($text)
{
if (!preg_match_all('/^##[ \t]+(.+?)[ \t]*$/im', $text, $matches, PREG_OFFSET_CAPTURE)) {
return false;
}
$lastIndex = count($matches[1]) - 1;
$lastHeadingText = strtolower(trim($matches[1][$lastIndex][0]));
return $lastHeadingText === 'done';
}
/**
* Redirect back to the current wiki page and stop request processing.
*
* @return void
*/
private function redirectToPage()
{
global $ID;
send_redirect(wl($ID, '', false, '&'));
exit;
}
}