syntax_plugin_deflist.php - A PHP4 class that implements * a DokuWiki plugin for definition list elements. * *

* Definition list pattern:
* ?? Term :: Term definition !! *

*
 *	Copyright (C) 2005, 2007 DFG/M.Watermann, D-10247 Berlin, FRG
 *			All rights reserved
 *		EMail : <support@mwat.de>
 * 
*

* Credits: This plugin was inspired by ideas of * Stephane Chamberland and Pavel * Vitis both of whom wrote a similar plugin that almost * worked. *

*
* 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_deflist.php,v 1.14 2007/08/15 12:36:20 matthias Exp $ * @since created 05-Aug-2005 */ class syntax_plugin_deflist extends DokuWiki_Syntax_Plugin { /** * @privatesection */ //@{ /** * Convert the specified $aID to a valid XHTML * fragment identifier. * *

* * XHTML 1 (section C.8, Fragment Identifiers) gives the regex * [A-Za-z][A-Za-z0-9:_.-]* for valid identifiers. Here * it's slightly reduced to [A-Za-z][A-Za-z0-9_]+ i.e. * all non alphanumeric characters are replaced by underscores. *

* @param $aID String The raw ID string. * @return String * @private * @since created 24-Aug-2005 * @see render() */ function _makeID(&$aID) { static $CHARS; if (! is_array($CHARS)) { $CHARS = array('|[^A-Za-z0-9_]|', // replace invalid characters '|_{2,}|', // reduce multiple underscores '|^[^A-Za-z]+|', // remove invalid leading chars '|_+$|'); // remove trailing underscores } // if // As long as DokuWiki (in contrast to W3C) doesn't allow uppercase // letters in internal anchor names we've to use 'strtolower()' // here as well to make the anchors work within DokuWiki. return strtolower(preg_replace($CHARS, array('_', '_'), utf8_deaccent($aID, 0))); } // _makeID() //@} /** * @publicsection */ //@{ /** * Tell the parser whether the plugin accepts syntax mode * $aMode within its own markup. * *

* This method mostly returns TRUE since all other types * are allowed within a definition list's DD sections. * Only another definition list is denied since nested DLs are * currently not supported. *

* @param $aMode String The requested syntaxmode. * @return Boolean TRUE unless $aMode * is plugin_deflist (which would result in a * FALSE method result). * @public * @see getAllowedTypes() */ function accepts($aMode) { return (PLUGIN_DEFLIST != $aMode); } // accepts() /** * Connect lookup pattern to lexer. * * @param $aMode String The desired rendermode. * @public * @see render() */ function connectTo($aMode) { if (PLUGIN_DEFLIST == $aMode) { return; } // if // We have to use assertion patterns here to make sure the DD sections // are UNMATCHED since only those are subject to further substitution. $this->Lexer->addEntryPattern( '\n\x20{2,}\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)', $aMode, PLUGIN_DEFLIST); $this->Lexer->addEntryPattern( '\n\t+\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)', $aMode, PLUGIN_DEFLIST); $this->Lexer->addPattern( '\n\x20{2,}\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST); $this->Lexer->addPattern( '\n\t+\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST); } // 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' => 'Definition List Syntax Plugin', 'desc' => '(X)HTML style Definition Lists [ ?? Term :: Definition !! ]', 'url' => 'http://wiki.splitbrain.org/plugin:deflist'); } // 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' instead of the (correct) 'block' * since otherwise the current DokuWiki parser would put all * substitutions within a DD section in separate paragraphs. * @public * @static */ function getPType() { return 'normal'; } // getPType() /** * Where to sort in? * * @return Integer 18, an arbitrary value smaller * Doku_Parser_Mode_preformated (20). * @public * @static */ function getSort() { return 18; } // 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() (here: DT data).
*
DOKU_LEXER_EXIT
*
a pattern set by addExitPattern().
*
DOKU_LEXER_UNMATCHED
*
ordinary text encountered within the plugin's syntax mode * which doesn't match any pattern (here: DD data).
*
* @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 (as a list of * entries) prepared for the render() method. * @public * @see render() * @static */ function handle($aMatch, $aState, $aPos, &$aHandler) { static $ESCDELIMS; // static constants to avoid the runtime overhead static $UNDELIMS; // of re-creating the arrays on each method call if (! is_array($ESCDELIMS)) { $ESCDELIMS = array('\?', '\!', '\:'); } // if if (! is_array($UNDELIMS)) { $UNDELIMS = array('?', '!', ':'); } // if switch ($aState) { case DOKU_LEXER_ENTER: // fall through to extract initial DTs case DOKU_LEXER_MATCHED: // DTs $aMatch = preg_split('|\n+(\s*\?\?)\s*|', $aMatch, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $dts = array(); $c = count($aMatch); for ($i = 0; $c > $i; ++$i) { if ($i & 1) { $dts[] = array($aMatch[$i - 1], str_replace($ESCDELIMS, $UNDELIMS, trim($aMatch[$i]))); $aMatch[$i - 1] = $aMatch[$i] = NULL; } else { $aMatch[$i] = strlen( str_replace(' ', "\t", $aMatch[$i])) - 2; } // if } // for return array($aState, $dts); case DOKU_LEXER_UNMATCHED: // DDs $aMatch = preg_split('|\s*(::\s*.*?!!)|s', $aMatch, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $mark = FALSE; // indication for kind of DD entry $c = count($aMatch); $hits = $dds = array(); for ($i = 0; $c > $i; ++$i) { if (preg_match('|::\s*(.*?)\s*!!|s', $aMatch[$i], $hits)) { $mark = 0; // complete DD w/o substitution(s) } else if (preg_match('|::\s*(.*)|s', $aMatch[$i], $hits)) { $mark = -1; // DD part before substitution(s) } else if (preg_match('|(.*?)\s*!!|s', $aMatch[$i], $hits)) { $mark = +1; // DD part behind substitution(s) } else { $mark = TRUE; // DD part between substitutions $hits[1] = $aMatch[$i]; } // if $dds[] = array( str_replace($ESCDELIMS, $UNDELIMS, $hits[1]) => $mark); } // for return array($aState, $dds); case DOKU_LEXER_EXIT: // end of list default: return array($aState); } // switch } // handle() /** * Add exit pattern to lexer. * *

* Two consecutive linefeeds mark the end'o'list. *

* @note Access public */ function postConnect() { $this->Lexer->addExitPattern('(?<=!!)\n(?=\n)', PLUGIN_DEFLIST); } // postConnect() /** * Handle the actual output creation. * *

* The method tests the given $aFormat returning * FALSE if it's not 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 being tendered. * @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() * @static */ function render($aFormat, &$aRenderer, &$aData) { if ('xhtml' != $aFormat) { return FALSE; } // if static $LEVEL = 1; // current nesting level static $INDD = array(); // marks whether there's an open DD static $CHARS; static $ENTS; // HTML special chars if (! is_array($CHARS)) { $CHARS = array('&','<', '>'); } // if if (! is_array($ENTS)) { $ENTS = array('&', '<', '>'); } // if // XXX: All those

and

tags handled here are just kind // of workaround problems with the current DokuWiki renderer. // Basically they are __wrong__ here but, alas, without them // invalid HTML would be generated :-( // If and when DokuWiki becomes more statefull the superflous // tags should be removed. switch ($aData[0]) { case DOKU_LEXER_ENTER: // since we have to use PType 'normal' we must close // the current paragraph $hits = array(); if (preg_match('|\s*

\s*$|i', $aRenderer->doc, $hits)) { $aRenderer->doc = substr($aRenderer->doc, 0, -strlen($hits[0])) . '

'; } else { $aRenderer->doc .= '

'; } // if // fall through to render initial DTs case DOKU_LEXER_MATCHED: foreach ($aData[1] as $dt) { $diff = $dt[0] - $LEVEL; if (0 < $diff) { // going UP __one__ level ++$LEVEL; $hits = array(); if (preg_match('|\s*
\s*

\s*$|i', $aRenderer->doc, $hits)) { $aRenderer->doc = substr($aRenderer->doc, 0, -strlen($hits[0])) . '

'; } else { $aRenderer->doc .= (preg_match( '|\s*\s*$|i', $aRenderer->doc)) ? '
' : '
'; } // if } else if (0 > $diff) { do { // going back some levels --$LEVEL; $aRenderer->doc .= (isset($INDD[$LEVEL])) ? '
' : '
'; } while (0 > ++$diff); // ELSE: no level change } // if $hits = array(); if (preg_match('|\s*

\s*$|i', $aRenderer->doc, $hits)) { // remove unneeded P $aRenderer->doc = substr($aRenderer->doc, 0, -strlen($hits[0])); } // if $id = $this->_makeID($dt[1]); // see http://www.w3.org/TR/xhtml1/#h-4.10 $aRenderer->doc .= '

' . str_replace($CHARS, $ENTS, $dt[1]) . '
'; } // foreach return TRUE; case DOKU_LEXER_UNMATCHED: $c = count($aData[1]); for ($i = 0; $c > $i; ++$i) { list($dd, $mark) = each($aData[1][$i]); $dd = str_replace($CHARS, $ENTS, $dd); if (TRUE === $mark) { // part between substitutions if (isset($INDD[$LEVEL])) { if (strlen($dd)) { $aRenderer->doc .= $dd; } // if } else { $tabs = str_repeat("\t", $LEVEL); $aRenderer->doc .= (strlen($dd)) ? '

' . $dd : '

'; $INDD[--$LEVEL] = TRUE; } // if } else if (0 == $mark) { // complete definition w/o substitutions if (strlen($dd)) { $aRenderer->doc .= '

' . $dd . '

'; } // if } else if (0 > $mark) { // DD part before substitutions $aRenderer->doc .= (strlen($dd)) ? '

' . $dd : '

'; $INDD[$LEVEL] = TRUE; } else if (0 < $mark) { // DD part behind substitutions if (isset($INDD[$LEVEL])) { if (strlen($dd)) { $aRenderer->doc .= $dd . '

'; } else { $hits = array(); if (preg_match('|\s*

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

'; } // if } // if unset($INDD[$LEVEL]); } // if // ELSE: doesn't ever happen with non-empty $dd } // if } // for return TRUE; case DOKU_LEXER_EXIT: // Close all possibly open lists: while (0 < --$LEVEL) { $aRenderer->doc .= ''; } // 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) . '

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