Table of Contents

Template Extension for Command Plugin

template plugin by Spider Joe
Render a data set from a template.

Last updated on 2005-09-09.
No compatibility info given!

Requires command.
Similar to templater.

Tagged with command, extension, template.

Overview

The Template Command renders a data set from a template. The data set is just a DokuWiki page containing a series of lists representing records (example). The template is a PHP file in lib/tpl/ that generates either HTML or DokuWiki syntax for inclusion at the place of the template command (example). The command is an extension of the Command Plugin, requiring that plugin and implementing a command of that plugin.

This command is useful for…

The command defines a kind of page called a data page. A data page is just a series of DokuWiki lists delimited by horizontal rules. Each list represents a record, and each item in a list represents a field of the record.

A template command takes optional parameters, a template file name, and the name of a data page. Here are some example uses:

  #template(news.php|health:news updates)#
  #template?category=health(news.php|news:records)#
  #template(mla.php|article references)#
  #template(calendar.php|calendar:events)#
  #template?from=2004-1-1&to=2005-1-1(calendar.php|calendar:events)#
  #template?_cache(calendar.php|calendar:events)#

The parameters are handed to the template file, allowing for parameterized templates. The template file may produce either HTML or DokuWiki syntax that is in turn translated to HTML — which ever is easiest for the template writer.

Here are two demo pages that are generated by this command:

Modification History

Installation

To install this command, first make sure you have a recent version of the Command Plugin installed. Then save the provided source file with filename template.php in the following directory:

/lib/plugins/command/ext/

You now need to create a template file and a data file, and then you're ready to try the command.

Syntax

The Template command conforms to the Command Plugin syntax. In particular, it takes one of the following forms:

As described in the Command Plugin, the command name 'template' is not case sensitive, and the above syntax places the generated HTML outside of paragraphs, while %template()% syntax embeds the generated HTML in a paragraph (the current paragraph). The % notation is called inline embedding and the # notation is called block embedding.

Command Name

Each extension of the Command Plugin has a unique command name. This command takes the name TEMPLATE.

Parameters

The values that may follow the ? in a Command Plugin command are called parameters. This command accepts optional parameters. Only one parameter affects the behavior of the command, independently of the template:

All additional parameters are passed straight to the template file. This allows a template file to be designed so that each use of the template command can uniquely filter or format the data of a data page.

Content

The text that the user inserts between the opening ( and closiing ) parentheses of a Command Plugin is called the content. For example, in %template(format.php|ns:data)% , the content is format.php|ns:data. The content of the template command must have one of the following two formats:

  template_file|data_page
  template_file|... data_set

Data Page Format

A data page is a page that represents a set of records. Each record is a DokuWiki list, and each item of the list provides a field of the record. Multiple records are delimited with the horizontal rule, denoted in the DokuWiki syntax by ”----”.

Here is an example data page:

Any text you put before the first list item of record is ignored.  This is a good place to describe the data page or to describe the next record.

  * date: 2005-7-31
  * category: DokuWiki
  * page: [[http://www.meganews.com?id=12345|DokuWiki has done it again!]]
  * abstract: The amazing **Andi Gohr** has produced a [[plugin:gallery|gallery plugin]] to die for.

----
  * date: 2005-8-30

  * category: Spiders
  * page: [[http://www.spiderjoe.com/spiders/oak-hill/mrs-featherlegged|Mrs. Fanged Featherlegged]]
  * abstract:
Of the 900 or so species of spiders living in Texas, only seven lack poison glands. One of these spiders raised a family beside the dog house in my back yard.
  * follow up:

----

Again, text you put before the first list item is ignored, so you can describe the record.

  * date: 2000-1-1
  * #category: Warnings --- the # comments out a field
  * category: Catastrophies
  * page: [[http://www.meganews.com?id=666|Armageddon finally occurs]]
  * abstract:

We'd been waiting for it for centuries and now it has finally happened.  **Armageddon** has finally occurred.

Read all about it from the comfort of your home, at your fancy multimedia computer.

  * follow up: The world was reborn, minds were erased, and we were given another shot.  Next Armaggedon: Year 2100.  Click [[http://runwithfear.com/go|here]] to run with fear.
----
Empty records are ignored.

This example isn't exactly pretty, as it's meant to demonstrate the variety of formatting that the template command will accept.

Each list item represents a field and has the following format:

  * field_name: field_value
multi_para_value

Text that appears before the first list item of a record is ignored and may be used to comment the page or the record. Note that DokuWiki needs a blank line before each horizontal rule. Records not containing any list items are ignored and are not handed to the template. This happens when two horizontal rules appear back-to-back or when one appears at the start or end of the file.

Note that the field_value and multi_para_value representations need not be equivalent. In an HTML template, each paragraph of a multi_para_value is wrapped in <p> tags, while in a DokuWiki syntax template, there are no <p> tags, but any blank lines present will be included.

Whitespace preceding or trailing each field_value or multi_field_value is trimmed and so is not significant.

Template Files

A template file is a file that uses PHP to generate HTML or DokuWiki syntax from a data set. The file is placed in the active lib/tpl/ directory or in some subdirectory thereof. (The default active template directory is lib/tpl/default/.)

How a template file works can be summarized in a few points:

  1. Think of the file as a regular HTML or DokuWiki syntax file that may contain embedded PHP. Everything outside of the PHP tags and all output echoed by PHP becomes part of the generated result.
  2. The PHP in the file runs within an outer, invisible PHP function. All this means is that you must declare any globals you want to use.
  3. The $TEMPLATECOMMAND_SOURCE variable is available to the PHP code. This variable contains an object from which the code gets its source data.

$TEMPLATECOMMAND_SOURCE contains an object of class TemplateCommandSource. You need only be aware of three methods of this class:

All field values are reported with preceding and trailing whitespace trimmed. Note that in records retrieved via getHtmlRecords(), each paragraph of a multi_para_value field value will be wrapped in <p> tags, as we expect DokuWiki to do.

Source (template.php)

<?php
/**
 * Template Command: Render a data set from a template.
 *
 * For a full description of this Command Plugin command, see:
 *   http://www.splitbrain.org/plugin:template
 *
 * (Thank you Esther Brunner for the include plugin, which
 * showed me how to nab a DokuWiki-parsed page, and thank you
 * Christopher Smith, who told me how to parse arbitrary text.)
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Joe Lapp <http://www.spiderjoe.com>
 *
 * Modification History:
 *
 * 2005/08/30 - Created.  JTL
 * 2005/08/31 - Now uses ob nesting so RSS can write its headers.  JTL
 * 2005/09/09 - Added ability to include data set in command.  JTL
 * 2005/09/09 - template() was not available from within RSS. JTL
 */
 
define('TEMPLATECOMMAND_CONTENT_ERROR', '_INVALID_TEMPLATE_CONTENT_');
define('TEMPLATECOMMAND_DATA_NOT_FOUND', '_DATA_SET_NOT_FOUND_');
define('TEMPLATECOMMAND_TEMPLATE_NOT_FOUND', '_TEMPLATE_NOT_FOUND_');
 
class CommandPluginExtension_template extends CommandPluginExtension
{
    function getCachedData($embedding, $params, $paramHash, $content,
                             &$errorMessage) // STATIC
    {
        // Extract the template file name and data set ID.
 
        $barPos = strpos($content, '|');
        if($barPos === false)
        {
            $errorMessage = TEMPLATECOMMAND_CONTENT_ERROR;
            return;
        }
 
        /* next lines work even if get empty strings */
        $templateName = trim(substr($content, 0, $barPos));
        $dataSet = trim(substr($content, $barPos + 1));
 
        if($templateName == '' || $dataSet == '')
        {
            $errorMessage = TEMPLATECOMMAND_CONTENT_ERROR;
            return;
        }
        $templateFN = CommandPluginExtension_template::template($templateName);
        if(strlen($dataSet) >= 3 && substr($dataSet, 0, 3) == '...')
        {
            $dataSetType = 'text';
            $dataSet = substr($dataSet, 3);
        }
        else
            $dataSetType = 'id';
 
        // Cache the page if caller allows it.
 
        if(isset($paramHash['_cache']))
        {
            $errorMessage = CommandPluginExtension_template::resolve(
                                $templateFN, $dataSetType, $dataSet);
            if($errorMessage)
                return;
            return CommandPluginExtension_template::generate(
                            $templateFN, $dataSetType, $dataSet, $paramHash);  
        }
 
        // Cache info needed to dynamically load page.
 
        return array($templateFN, $dataSetType, $dataSet, $paramHash);
    }
 
    function runCommand($embedding, $cachedData, &$renderer,
                          &$errorMessage) // STATIC
    {
        if(is_string($cachedData))
            return $cachedData;
 
        list($templateFN, $dataSetType, $dataSet, $paramHash) = $cachedData;
 
        $errorMessage = CommandPluginExtension_template::resolve(
                            $templateFN, $dataSetType, $dataSet);
        if($errorMessage)
            return;
 
        $renderer->info['cache'] = false;
        return CommandPluginExtension_template::generate(
                        $templateFN, $dataSetType, $dataSet, $paramHash);  
    }
 
    function resolve($templateFN, $dataSetType, &$dataSet) // STATIC
    {
        global $ID;
 
        if(strpos($templateFN, '..') !== false ||
                !@file_exists($templateFN)) // PHP caches the results
            return TEMPLATECOMMAND_TEMPLATE_NOT_FOUND;
 
        if($dataSetType == 'id')
        {
            $exists = false;
            resolve_pageid(getNS($ID), $dataSet, $exists);
            if(!$exists || auth_quickaclcheck($dataSet) < AUTH_READ)
                return TEMPLATECOMMAND_DATA_NOT_FOUND;
        }
        return null;
    }
 
    function generate($templateFN, $dataSetType, $dataSet, $paramHash) // STATIC
    {
        // Generate HTML or text from the template.
 
        $source = new TemplateCommandSource($templateFN, $dataSetType,
                                             $dataSet, $paramHash);
        ob_start();
        CommandPluginExtension_template::runTemplate($source);
        $outString = ob_get_contents();
        ob_end_clean();
 
        // If the template generated text, translate into HTML.
 
        if(!$source->isHTML)
        {
            $instructs = p_get_instructions($outString);
            $info = array();
            $outString = p_render('xhtml', $instructs, $info);
        }
 
        return $outString; // return generated HTML
    }
 
    function runTemplate(&$TEMPLATECOMMAND_SOURCE)
    {
        // Isolate template in its own function because it has access to
        // the function variables.  Template writes to standard out.
 
        include($TEMPLATECOMMAND_SOURCE->templateFN);
    }
 
    /**
     * This is a duplicate of its namesake in template.php, since the
     * namesake is not available in an RSS feed.
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     */
    function template($tpl){
      global $conf;
 
      if(@is_readable(DOKU_INC.'lib/tpl/'.$conf['template'].'/'.$tpl))
        return DOKU_INC.'lib/tpl/'.$conf['template'].'/'.$tpl;
 
      return DOKU_INC.'lib/tpl/default/'.$tpl;
    }
}
 
/******************************************************************************
TemplateCommandSource - Source of data for PHP implementing template
******************************************************************************/
 
class TemplateCommandSource
{
    //// FRIEND VARIABLES /////////////////////////////////////////////////////
 
    var $isHTML = true;
    var $templateFN;
 
    //// PRIVATE VARIABLES ////////////////////////////////////////////////////
 
    var $dataSetText = null;
    var $dataSetFN = null;
    var $paramHash;
 
    var $textRecords = null;
    var $htmlRecords = null;
 
    //// PUBLIC METHODS ///////////////////////////////////////////////////////
 
    function getParamHash()
    {
        return $this->paramHash;
    }
 
    function getTextRecords()
    {
        $this->isHTML = false;
        if($this->textRecords != null)
            return $this->textRecords;
 
        if($this->dataSetText != null)
            $outline = $this->dataSetText;
        else
        {
            $outline = '';
            $outline = @file_get_contents($this->dataSetFN);
        }
 
        $outlineParser = new TextOutlineRecordParser($outline);
        $this->textRecords = $outlineParser->getRecords();
        return $this->textRecords;        
    }
 
    function getHtmlRecords()
    {
        $this->isHTML = true;
        if($this->htmlRecords != null)
            return $this->htmlRecords;
 
        if($this->dataSetText != null)
            $instructs = p_get_instructions($this->dataSetText);
        else
            $instructs = p_cached_instructions($this->dataSetFN);
 
        $info = array();
        $outline = p_render('xhtml', $instructs, $info);
 
        $outlineParser = new HTMLOutlineRecordParser($outline);
        $this->htmlRecords = $outlineParser->getRecords();
        return $this->htmlRecords;        
    }
 
    //// FRIEND METHODS ///////////////////////////////////////////////////////
 
    function TemplateCommandSource($templateFN, $dataSetType, $dataSet,
                                    $paramHash)
    {
        $this->templateFN = $templateFN;
        if($dataSetType == 'id')
            $this->dataSetFN = wikiFN($dataSet);
        else
            $this->dataSetText = $dataSet;
        $this->paramHash = $paramHash;
    }
}
 
/******************************************************************************
OutlineRecordParser - Base class for outline record parsers
******************************************************************************/
 
class OutlineRecordParser
{
    //// PROTECTED VARIABLES //////////////////////////////////////////////////
 
    var $records = array(); // list of records expressed as associative arrays
 
    //// PUBLIC METHODS ///////////////////////////////////////////////////////
 
    function getRecords()
    {
        return $this->records;
    }
 
    //// PROTECTED METHODS ////////////////////////////////////////////////////
 
    function parseRecord($fieldSplits)
    {
        $record = array();
 
        // Iterate over the fields.  Whatever precedes the first line item
        // of a record is ignored and so may be used for comments.
 
        for($i = 1; $i < sizeof($fieldSplits); ++$i)
            $this->parseField($record, trim($fieldSplits[$i]));
 
        if(!empty($record))
            array_push($this->records, $record);
    }
 
    function parseLineItem($lineItem, &$fieldName, &$fieldValue)
    {
        $colonPos = strpos($lineItem, ':');
        if($colonPos === false)
        {
            $fieldName = trim($lineItem);
            $fieldValue = '';
        }
        else
        {
            $fieldName = trim(substr($lineItem, 0, $colonPos));
            // next line works even if nothing follows colon
            $fieldValue = trim(substr($lineItem, $colonPos + 1));
        }
    }
}
 
/******************************************************************************
TextOutlineRecordParser - Parses wiki text for records expressed in outlines
******************************************************************************/
 
class TextOutlineRecordParser extends OutlineRecordParser
{
    //// PUBLIC METHODS ///////////////////////////////////////////////////////
 
    function TextOutlineRecordParser($outline)
    {
        $outline = "\n".$outline; // makes the pregs easier
 
        // wiki horizontal rules delimit records
        $recordSplits = preg_split('/[\n\r]+----+/', $outline);
        foreach($recordSplits as $recordSplit)
        {
            $fieldSplits = preg_split('/[\n\r]+  +\*/', $recordSplit);
            $this->parseRecord($fieldSplits);
        }
    }
 
    //// PROTECTED METHODS ////////////////////////////////////////////////////
 
    function parseField(&$record, $fieldSplit)
    {
        // Extract the line item.
 
        $endLineItem = strcspn($fieldSplit, "\n\r");
 
        if($endLineItem === false)
            $lineItem = $fieldSplit;
        else
            $lineItem = substr($fieldSplit, 0, $endLineItem);
 
        // Skip over commented out fields.
 
        if($lineItem{0} == '#')
            return; // commented out field
 
        // Extract the field name and the line-item value.
 
        $fieldName = null;
        $fieldValue = null;
        $this->parseLineItem($lineItem, $fieldName, $fieldValue);
 
        // Extract the field's multi-line value, if any.
 
        if($endLineItem !== false)
        {
            $multiline = trim(substr($fieldSplit, $endLineItem));
            if($multiline != '')
                $fieldValue .= $multiline;
        }
 
        // Record the extracted field.
 
        $record[$fieldName] = $fieldValue;
    }
}
 
/******************************************************************************
HTMLOutlineRecordParser - Parses HTML for records expressed in outlines
******************************************************************************/
 
class HTMLOutlineRecordParser extends OutlineRecordParser
{
    //// PUBLIC METHODS ///////////////////////////////////////////////////////
 
    function HTMLOutlineRecordParser($outline)
    {
        // remove <ul>s and <ol>s
        $outline = preg_replace('/<[ou]l(\s.*?)?>/', '', $outline);
 
        // <hr/>s delimit records
        $recordSplits = preg_split('/<hr(\/|\s.*?)?>/', $outline);
        foreach($recordSplits as $recordSplit)
        {
            $fieldSplits = preg_split('/<li(\/|\s.*?)?>/', $recordSplit);
            $this->parseRecord($fieldSplits);
        }
    }
 
    //// PROTECTED METHODS ////////////////////////////////////////////////////
 
    function parseField(&$record, $fieldSplit)
    {
        // Extract the line item.
 
        $endLineItem = strpos($fieldSplit, '</li>');
        if($endLineItem === false)
            return; // bad HTML
        $lineItem = substr($fieldSplit, 0, $endLineItem);
 
        $firstChar = $lineItem{0};
        if($firstChar == '<')
        {
            $newLeft = strpos($lineItem, '>') + 1;
            $newLength = strrpos($lineItem, '<') - $newLeft;
            $lineItem = ltrim(substr($lineItem, $newLeft, $newLength));
            $firstChar = $lineItem{0};
        }
 
        // Skip over commented out fields.
 
        if($firstChar == '#')
            return; // commented out field
 
        // Extract the field name and the line-item value.
 
        $fieldName = null;
        $fieldValue = null;
        $this->parseLineItem($lineItem, $fieldName, $fieldValue);
 
        // Extract the field's multi-line value, if any.
 
        $endOutline = strpos($fieldSplit, '</ol>');
        if($endOutline === false)
            $endOutline = strpos($fieldSplit, '</ul>');
 
        if($endOutline !== false)
        {
            // next line works even if nothing follows </ol> or </ul>
            $multiline = trim(substr($fieldSplit, $endOutline + 5));
            if($multiline != '')
                $fieldValue .= $multiline;
        }
 
        // Record the extracted field.
 
        $record[$fieldName] = $fieldValue;
    }
}
?>

Examples

Bear in mind that, although the following examples generate HTML, the template file may instead generate DokuWiki syntax by calling $TEMPLATECOMMAND_SOURCE→getTextRecords() instead of $TEMPLATECOMMAND_SOURCE→getHtmlRecords(). I just don't have a live example to show you. You might want to generate DokuWiki syntax if it's easier than generating the HTML, as it usually will be. Also, these examples used to use a separate data page. I have since moved the data directly into the command using the new '...' syntax.

<div class='content_header'>Upcoming Spider Events</div>
 
<div class="fix_IE6">
<table width="100%" border="0" cellpadding="0" cellspacing="0" >
<?php
$records = $TEMPLATECOMMAND_SOURCE->getHtmlRecords();
 
foreach($records as $record)
{
    if(isset($record['month']))
    {
?>
<tr>
<td class='month_row' colspan='6'><div class='event_month'><?php echo $record['month']; ?></div></td>
<tr>
<?php
    }
    else
    {
?>
<tr class='event_header'>
<td width='1%'>&nbsp;</td>
<td class="left_field" width='29%'><?php echo $record['date']; ?></td>
<td class="left_field" width='24%'><?php echo $record['type']; ?></td>
<td class="left_field" width='16%'><?php echo $record['time']; ?></td>
<td width='29%'><?php echo $record['city']; ?></td>
<td width='1%'>&nbsp;</td>
</tr>
<tr>
<td class='event_details' colspan='6'><?php if(isset($record['title'])) { ?>
&quot;<span class='event_title'><?php echo $record['title']; ?></span>,&quot;
<?php } echo $record['details']; ?></td>
</tr>
<?php
    }
}
?>
</table>
</div><!--fix_IE6-->
<?php
global $conf; // template is executed from within a function
 
$records = $TEMPLATECOMMAND_SOURCE->getHtmlRecords();
 
$side = 'left';
foreach($records as $record)
{
    $pageURL = $conf['basedir'].idfilter(cleanID($record['page']));
    $title = $record['title'];
 
    echo '<div class="abstract_'.$side.'"><p>';
 
    if(isset($record['image']))
    {
        $imageID = idfilter(cleanID($record['image']));
        $imageURL = $conf['basedir'].'_media/'.$imageID.'?cache=cache';
        $imageWidth = '';
        if(isset($record['image-width']))
        {
            $imageWidth = $record['image-width'];
            $imageURL .= "&amp;w=$imageWidth&amp;h=";
        }
 
        echo "<a href='$pageURL' class='media'><img src='$imageURL' class='media$side'".
             " title='$title' alt='$title' width='$imageWidth'></a>";
    }
 
    echo "<strong><a href='$pageURL' class='wikilink1'".
         " onclick='return svchk()' onkeypress='return svchk()'>";
    echo $title;
    echo '</a></strong>. ';
    echo $record['abstract'];
    echo "</div>\n\n";
 
    $side = ($side == 'left') ? 'right' : 'left';
}
?>
Note that the JavaScript function “svchk()” no longer exists (and is now unnecessary) in Dokuwiki, and does cause JavaScript errors, so references should be removed.
todd [at] rollerorgans [dot] com 2007-02-26

Discussion