Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Web display of Taskpaper file

Jerry Stratton, December 14, 2007

A simple text format is a joy forever. One of the nice things about a well-designed simple format like Taskpaper is that just about anything and anyone can read it. It makes it very easy to reposition the data in that format to somewhere else, like the web, using a scripting language like PHP.

Depending on what kind of notes you put in your task lists, you may wish to put a .htaccess file (or the equivalent if you aren’t using Apache) in the same directory as this web page, to limit access.

The web page

The web page itself is fairly simple. It has two sections: the section that displays tasks, and a footer that displays tags to make it easy to switch from tag to tag.

The section that displays tasks is also simple. There are only three kinds of lines in a Taskpaper file: a project name, a task, and a note. Notes belong to the task (or project, but I don’t implement that—all of my notes belong to a task) above them.

If the current page is a search for a project, this only displays tasks and notes within that project. If the current page is a search for tasks belonging to a tag, this only displays tasks tagged with that tag, and notes belonging to them. Project names are only displayed if they have a visible task on this page.

[toggle code]

  • <html>
    • <head>
      • <title>My Tasks</title>
      • <LINK REL="StyleSheet" HREF="tasks.css" TYPE="text/css" MEDIA="all" />
    • </head>
    • <body>
      • <h1>My Tasks</h1>
      • <?
        • $tasks = file('/Users/me/Documents/Projects/Tasks.taskpaper');
        • FOREACH ($tasks as $task):
          • $task = trim($task);
          • $task = htmlspecialchars($task);
          • //projects aren't displayed unless they have visible tasks
          • //so just keep the project name until a task is displayed
          • IF (substr($task, -1) == ':'):
            • $project = substr($task, 0, -1);
          • //show tasks or notes only if all projects are displayed
          • //or they are part of the displayed project
          • ELSEIF (!isset($_GET['project']) || $_GET['project'] == $project):
            • IF (substr($task, 0, 2) == '- '):
              • $task = substr($task, 2);
              • list($task, $classes, $tags) = parseTask($task);
              • IF (!isset($_GET['tag']) || in_array($_GET['tag'], $tags)):
                • displayTask($task, $classes);
              • ENDIF;
            • ELSEIF (!isset($_GET['tag']) || in_array($_GET['tag'], $tags)):
              • displayNote($task, $classes);
            • ENDIF;
          • ENDIF;
        • ENDFOREACH;
        • closeNotes();
        • closeTasks();
        • //footer: display all tags in project or file
        • //if we're focussed on one tag or project, include a "view all" link
        • IF (isset($_GET['tag']) || isset($_GET['project'])):
          • $alltags[] = array('count'=>0, 'link'=>'<a href="./">View all tasks</a>');
        • ENDIF;
        • IF ($alltags):
          • uasort($alltags, 'sorttags');
          • echo "\n\n", '<ul class="tags">';
          • FOREACH ($alltags as $tag):
            • echo '<li>', $tag['link'], "</li>\n";
          • ENDFOREACH;
          • echo '</ul>';
        • ENDIF;
      • ?>
    • </body>
  • </html>

The functions

The hardest part about the Taskpaper format is that I want the tasks in each project to be part of a list and I want the notes for each task to be part of the same “pre” tag. Because tasks and notes span multiple lines, I need to keep track of whether or not I’m currently displaying a task or note. When displaying a new project, the old task list needs to be closed, and when displaying a new project or tag, any notes need to be closed. That’s what the various open and close functions are. I don’t use subtasks, so this script doesn’t support them.

Making URLs clickable inside tasks and notes is theoretically difficult, but I just used Nico Oelgart’s Make URLs clickable function.

The bulk of the work is handled by the parseTask() function. It separates each task into a task and a list of tags, and then attaches the tag list back to the task, but linked as a search to those tasks. It also returns a list of all tags, so that the main part of the script can check to see if this task is tagged with the desired tag.

[toggle code]

  • <?
    • //parse a task into task, classes, and list of tags
    • function parseTask($line) {
      • global $alltags;
      • $taglist = explode(' @', $line);
      • $task = array_shift($taglist);
      • $task = parseURLs($task);
      • $tags = array();
      • $cleantaglist = array();
      • $classes = "";
      • IF ($taglist):
        • FOREACH ($taglist as $tag):
          • $rawtag = " @$tag";
          • $tag = preg_replace('/\(.*/', '', $tag);
          • IF ($tag == 'project'):
            • $tags[] = $rawtag;
          • ELSE:
            • $encodedtag = urlencode($tag);
            • $taglink = "<a class=\"tag\" href=\"./?tag=$encodedtag\">$rawtag</a>";
            • $tags[] = $taglink;
            • IF ($alltags[$tag]):
              • $alltags[$tag]['count']++;
            • ELSE:
              • $alltags[$tag] = array('count'=>1, 'link'=>$taglink);
            • ENDIF;
            • $cleantaglist[] = $tag;
          • ENDIF;
        • ENDFOREACH;
        • $task .= implode("", $tags);
        • IF ($cleantaglist):
          • $classes = implode(" ", $cleantaglist);
          • $classes = " class=\"$classes\"";
        • ENDIF;
      • ENDIF;
      • return array($task, $classes, $cleantaglist);
    • }
    • // keep track of whether we're currently in a note or task
    • function openNotes($classes) {
      • global $inNote;
      • IF (!$inNote):
        • $inNote = true;
        • echo "<pre$classes>";
      • ELSE:
        • echo "\n";
      • ENDIF;
    • }
    • function closeNotes() {
      • global $inNote;
      • IF ($inNote):
        • echo "</pre>\n";
        • $inNote = false;
      • ENDIF;
    • }
    • function displayNote($note, $classes) {
      • openNotes($classes);
      • echo parseURLs($note);
    • }
    • function openTasks() {
      • global $inTask;
      • IF (!$inTask):
        • $inTask = true;
        • echo "<ol>\n";
      • ENDIF;
    • }
    • function closeTasks() {
      • global $inTask;
      • IF ($inTask):
        • echo "</ol>\n";
        • $inTask = false;
      • ENDIF;
    • }
    • function displayTask($task, $classes) {
      • global $project, $currentproject;
      • closeNotes();
      • IF ($project != $currentproject):
        • closeTasks();
        • $encodedproject = urlencode($project);
        • echo "<h2><a href=\"./?project=$encodedproject\">$project</a></h2>\n";
        • $currentproject = $project;
      • ENDIF;
      • openTasks();
      • echo "<li$classes>$task</li>\n";
    • }
    • //link any URLs in notes or tasks
    • //from http://www.bytemycode.com/snippets/snippet/602/
    • function parseURLs($text, $maxurl_len = 70, $target = '_self') {
      • IF (preg_match_all('/((ht|f)tps?:\/\/([\w\.]+\.)?[\w-]+(\.[a-zA-Z]{2,4})?[^\s\r\n\(\)"\'<>\,\!]+)/si', $text, $urls)):
        • $offset1 = ceil(0.65 * $maxurl_len) - 2;
        • $offset2 = ceil(0.30 * $maxurl_len) - 1;
        • FOREACH (array_unique($urls[1]) AS $url):
          • IF ($maxurl_len AND strlen($url) > $maxurl_len):
            • $urltext = substr($url, 0, $offset1) . '...' . substr($url, -$offset2);
          • ELSE:
            • $urltext = $url;
          • ENDIF;
          • $text = str_replace($url, '<a class="embedded" href="'. $url .'" target="'. $target .'" title="'. $url .'">'. $urltext .'</a>', $text);
        • ENDFOREACH;
      • ENDIF;
      • return $text;
    • }
    • //sort tags by frequency
    • function sorttags($a, $b) {
      • IF ($a['count']<$b['count']):
        • return true;
      • ELSEIF ($a['count']>$b['count']):
        • return false;
      • ELSE:
        • return 0;
      • ENDIF;
    • }
  • ?>

The stylesheet

There are only a few elements to style. If you use a lot of tags, you can go wild giving each tag a different style. Just remember that the last style found is the one that takes precedence if a task is tagged with more than one tag.

[toggle code]

  • h1 {
    • background-color: #12B355;
    • color: White;
    • text-align: center;
  • }
  • h2 {
    • border-color: #12B355;
    • border-style: solid;
    • border-width: .1em;
    • border-left-style: none;
    • border-right-style: none;
  • }
  • /* links should be the same color as the task they're part of */
  • a {
    • color: inherit;
  • }
  • /* links embedded in tasks or notes should stand out from tag or project links */
  • a.embedded {
    • text-decoration: underline;
  • }
  • /* by default, tasks are grey */
  • li, pre {
    • color: #AAAAAA;
  • }
  • /* top tasks have a special color */
  • .top {
    • color: #12B31F;
  • }
  • /* and when they're done, tasks have a special color and a line through them */
  • .done {
    • text-decoration: line-through;
    • color: #1AFF79;
  • }
  • /* the list of possible tags */
  • ul.tags {
    • text-align: center;
    • list-style-type: none;
    • margin-top: 2em;
    • border: .1em solid #12B355;
    • padding: 0;
  • }
  • ul.tags li {
    • display: inline;
  • }
  • ul.tags li+li:before {
    • content: " | ";
  • }
  • ul.tags a {
    • color: #12B355;
  • }

Enhancements

If you need something more complex, look at Taskpaper.web. If you want this script to be more complex, you might consider an in-memory SQLite database. If you specify “:memory:” as the “filename” for your SQLite database, the database will be entirely in memory; this will give you the power of SQL to manipulate your tasks, notes, and projects. PHP 5 has SQLite support built-in (as does Python 2.5).

  1. <- Inline Django Forms
  2. SilverService ->