syntax_plugin_lists.php - A PHP4 class that implements * a DokuWiki plugin for un/ordered lists block * elements. * *

* Usage:
* * unordered item < * - ordered item < *

*
 *	Copyright (C) 2005, 2007  DFG/M.Watermann, D-10247 Berlin, FRG
 *			All rights reserved
 *		EMail : <support@mwat.de>
 * 
*
* This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either * version 3 of the * License, or (at your option) any later version.
* This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. *
* @author Matthias Watermann * @version $Id: syntax_plugin_lists.php,v 1.4 2007/08/15 12:36:19 matthias Exp $ * @since created 29-Aug-2005 */ class syntax_plugin_lists extends DokuWiki_Syntax_Plugin { /** * @publicsection */ //@{ /** * Tell the parser whether the plugin accepts syntax mode * $aMode within its own markup. * * @param $aMode String The requested syntaxmode. * @return Boolean TRUE unless $aMode is * PLUGIN_LISTS (which would result in a * FALSE method result). * @public * @see getAllowedTypes() */ function accepts($aMode) { return (PLUGIN_LISTS != $aMode); } // accepts() /** * Connect lookup pattern to lexer. * * @param $aMode String The desired rendermode. * @public * @see render() */ function connectTo($aMode) { if (PLUGIN_LISTS == $aMode) { return; } // if $this->Lexer->addEntryPattern( '\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)', $aMode, PLUGIN_LISTS); $this->Lexer->addPattern( '\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS); $this->Lexer->addEntryPattern( '\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)', $aMode, PLUGIN_LISTS); $this->Lexer->addPattern( '\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS); } // connectTo() /** * Get an associative array with plugin info. * *

* The returned array holds the following fields: *

*
author
Author of the plugin
*
email
Email address to contact the author
*
date
Last modified date of the plugin in * YYYY-MM-DD format
*
name
Name of the plugin
*
desc
Short description of the plugin (Text only)
*
url
Website with more information on the plugin * (eg. syntax description)
*
* @return Array Information about this plugin class. * @public * @static */ function getInfo() { return array( 'author' => 'Matthias Watermann', 'email' => 'support@mwat.de', 'date' => '2007-08-15', 'name' => 'List Syntax Plugin', 'desc' => 'Add HTML Style Un/Ordered Lists', 'url' => 'http://wiki.splitbrain.org/plugin:lists'); } // getInfo() /** * Define how this plugin is handled regarding paragraphs. * *

* This method is important for correct XHTML nesting. It returns * one of the following values: *

*
*
normal
The plugin can be used inside paragraphs.
*
block
Open paragraphs need to be closed before * plugin output.
*
stack
Special case: Plugin wraps other paragraphs.
*
* @return String 'normal' . * @public * @static */ function getPType() { return 'normal'; } // getPType() /** * Where to sort in? * * @return Integer 8, an arbitrary value smaller * Doku_Parser_Mode_listblock (10). * @public * @static */ function getSort() { // class 'Doku_Parser_Mode_preformated' returns 20 // class 'Doku_Parser_Mode_listblock' returns 10 return 8; } // getSort() /** * Get the type of syntax this plugin defines. * * @return String 'container'. * @public * @static */ function getType() { return 'container'; } // getType() /** * Handler to prepare matched data for the rendering process. * *

* The $aState parameter gives the type of pattern * which triggered the call to this method: *

*
*
DOKU_LEXER_ENTER
*
a pattern set by addEntryPattern()
*
DOKU_LEXER_MATCHED
*
a pattern set by addPattern()
*
DOKU_LEXER_EXIT
*
a pattern set by addExitPattern()
*
DOKU_LEXER_SPECIAL
*
a pattern set by addSpecialPattern()
*
DOKU_LEXER_UNMATCHED
*
ordinary text encountered within the plugin's syntax mode * which doesn't match any pattern.
*
* @param $aMatch String The text matched by the patterns. * @param $aState Integer The lexer state for the match. * @param $aPos Integer The character position of the matched text. * @param $aHandler Object Reference to the Doku_Handler object. * @return Array Index [0] holds the current * $aState, index [1] the match prepared for * the render() method. * @public * @see render() * @static */ function handle($aMatch, $aState, $aPos, &$aHandler) { static $CHARS; static $ENTS; if (! is_array($CHARS)) { $CHARS = array('&','<', '>'); } // if if (! is_array($ENTS)) { $ENTS = array('&', '<', '>'); } // if switch ($aState) { case DOKU_LEXER_ENTER: // fall through case DOKU_LEXER_MATCHED: $hits = array(); if (preg_match('|\n*((\s*)(.))|', $aMatch, $hits)) { return array($aState, $hits[3], strlen(str_replace(' ', "\t", $hits[2]))); } // if return array($aState, $aMatch); case DOKU_LEXER_UNMATCHED: $hits = array(); if (preg_match('|^\s*\x3C$|', $aMatch, $hits)) { return array(DOKU_LEXER_UNMATCHED, '', +1); } // if if (preg_match('|(.*?)\s+\x3C$|s', $aMatch, $hits)) { return array(DOKU_LEXER_UNMATCHED, str_replace($CHARS, $ENTS, str_replace('\<', '<', $hits[1])), +1); } // if if (preg_match('|(.*[^\x5C])\x3C$|s', $aMatch, $hits)) { return array(DOKU_LEXER_UNMATCHED, str_replace($CHARS, $ENTS, str_replace('\<', '<', $hits[1])), +1); } // if return array(DOKU_LEXER_UNMATCHED, str_replace($CHARS, $ENTS, str_replace('\<', '<', $aMatch)), -1); case DOKU_LEXER_EXIT: // end of list default: return array($aState); } // switch } // handle() /** * Add exit pattern to lexer. * * @public */ function postConnect() { // make sure the RegEx 'eats' only _one_ LF: $this->Lexer->addExitPattern('(?<=\x3C)\n(?=\n)', PLUGIN_LISTS); } // postConnect() /** * Handle the actual output creation. * *

* The method checks for the given $aFormat and returns * FALSE when a format isn't supported. $aRenderer * contains a reference to the renderer object which is currently * handling the rendering. The contents of $aData is the * return value of the handle() method. *

* @param $aFormat String The output format to generate. * @param $aRenderer Object A reference to the renderer object. * @param $aData Array The data created by the handle() * method. * @return Boolean TRUE if rendered successfully, or * FALSE otherwise. * @public * @see handle() */ function render($aFormat, &$aRenderer, &$aData) { if ('xhtml' != $aFormat) { return FALSE; } // if static $LISTS = array('*' => 'ul', '-' => 'ol'); static $LEVEL = 1; // initial nesting level static $INLI = array(); // INLI[LEVEL] :: 0==open LI, 1==open LI/P static $CURRENT = array(); // CURRENT[LEVEL] :: * | - switch ($aData[0]) { case DOKU_LEXER_ENTER: $CURRENT[$LEVEL] = $aData[1]; $hits = array(); if (preg_match('|\s*

\s*$|i', $aRenderer->doc, $hits)) { $hits = -strlen($hits[0]); $aRenderer->doc = substr($aRenderer->doc, 0, $hits) . '<' . $LISTS[$aData[1]] . '>'; } else { $aRenderer->doc .= '

<' . $LISTS[$aData[1]] . '>'; } // if // fall through to handle first item case DOKU_LEXER_MATCHED: // $aData[0] :: match state // $aData[1] :: * | - // $aData[2] :: nesting level $diff = $aData[2] - $LEVEL; if (0 < $diff) { // going up one level $CURRENT[++$LEVEL] = $aData[1]; $hits = array(); if (preg_match('|\s*$|', $aRenderer->doc)) { // need to open a new LI $aRenderer->doc .= '
  • <' . $LISTS[$CURRENT[$LEVEL]] . '>'; $INLI[$LEVEL - 1] = 0; // no closing P needed } else if (preg_match('|\s*]*>\s*

    \s*$|', $aRenderer->doc, $hits)) { // replace rudimentary LI $hits = -strlen($hits[0]); $aRenderer->doc = substr($aRenderer->doc, 0, $hits) . '

  • <' . $LISTS[$CURRENT[$LEVEL]] . '>'; $INLI[$LEVEL - 1] = 0; // no closing P needed } else { // possibly open LI if (isset($INLI[$LEVEL - 1])) { if (0 < $INLI[$LEVEL - 1]) { // open LI P $aRenderer->doc .= '

    <' . $LISTS[$aData[1]] . '>'; $INLI[$LEVEL - 1] = 0; } else { // open LI $aRenderer->doc .= '<' . $LISTS[$aData[1]] . '>'; } // if } else { // no open LI $aRenderer->doc .= '
  • <' . $LISTS[$aData[1]] . '>'; $INLI[$LEVEL - 1] = 0; // no closing P needed } // if } // if } else if (0 > $diff) { // going back some levels do { --$LEVEL; $aRenderer->doc .= ''; if (isset($INLI[$LEVEL])) { $aRenderer->doc .= (0 < $INLI[$LEVEL]) ? '

  • ' : ''; } // if } while (0 > ++$diff); } else if ($aData[1] != $CURRENT[$LEVEL]) { // list type changed if (isset($INLI[$LEVEL])) { $aRenderer->doc .= (0 < $INLI[$LEVEL]) ? '

    ' : ''; } // if $aRenderer->doc .= '<' . $LISTS[$aData[1]] . '>'; $CURRENT[$LEVEL] = $aData[1]; } // if $aRenderer->doc .= '
  • '; $INLI[$LEVEL] = 1; // closing P needed return TRUE; case DOKU_LEXER_UNMATCHED: // $aData[0] :: match state // $aData[1] :: text // $aData[2] :: +1(EoT), -1(start/inbetween) if (0 < $aData[2]) { // last part of item's text if (strlen($aData[1])) { if (isset($INLI[$LEVEL])) { $aRenderer->doc .= (0 < $INLI[$LEVEL]) // LI P ? $aData[1] . '

  • ' : '

    ' . $aData[1] . '

    '; } else { // no LI if (1 < $LEVEL) { // assume a trailing LI text --$LEVEL; $aRenderer->doc .= '

    ' . $aData[1] . '

    '; } else { //XXX: There must be no data w/o context; the markup is broken. Whatever we // could do it would be WRONG (and break XHMTL validity); hence comment: $aRenderer->doc .= ''; } // if } // if } else { // empty data $hits = array(); if (preg_match('|\s*]*>\s*

    \s*$|', $aRenderer->doc, $hits)) { $hits = -strlen($hits[0]); // remove empty list item $aRenderer->doc = substr($aRenderer->doc, 0, $hits); } else if (preg_match('|\s*

    \s*$|', $aRenderer->doc, $hits)) { $hits = -strlen($hits[0]); $aRenderer->doc = substr($aRenderer->doc, 0, $hits) . ''; } else if (isset($INLI[$LEVEL])) { $aRenderer->doc .= (0 < $INLI[$LEVEL]) ? '

    ' : ''; } // if } // if unset($INLI[$LEVEL]); } else { // item part between substitutions or nested blocks if (isset($INLI[$LEVEL])) { if (0 < $INLI[$LEVEL]) { // LI P $aRenderer->doc .= $aData[1]; $INLI[$LEVEL] = 1; } else { // LI $aRenderer->doc .= '

    ' . $aData[1]; } // if } else { // data w/o context if (1 < $LEVEL) { // assume a trailing LI text --$LEVEL; $aRenderer->doc .= '

    ' . $aData[1]; $INLI[$LEVEL] = 1; } else { $aRenderer->doc .= $aData[1]; } // if } // if } // if return TRUE; case DOKU_LEXER_EXIT: while (1 < $LEVEL) { --$LEVEL; $aRenderer->doc .= ''; if (isset($INLI[$LEVEL])) { $aRenderer->doc .= (0 < $INLI[$LEVEL]) ? '

    ' : ''; } // if } // while // Since we have to use PType 'normal' we must open // a new paragraph for the following text $aRenderer->doc = preg_replace('|\s*

    \s*

    \s*|', '', $aRenderer->doc) . '

    '; $CURRENT = $INLI = array(); $LEVEL = 1; default: return TRUE; } // switch } // render() //@} } // class syntax_plugin_lists } // if ?>