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
?>