1<?php 2if (! class_exists('syntax_plugin_deflist')) { 3 if (! defined('DOKU_PLUGIN')) { 4 if (! defined('DOKU_INC')) { 5 define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/'); 6 } // if 7 define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 8 } // if 9 // include parent class 10 require_once(DOKU_PLUGIN . 'syntax.php'); 11 define('PLUGIN_DEFLIST', 'plugin_deflist'); 12 13/** 14 * <tt>syntax_plugin_deflist.php </tt>- A PHP4 class that implements 15 * a <tt>DokuWiki</tt> plugin for <tt>definition list</tt> elements. 16 * 17 * <p> 18 * Definition list pattern:<br> 19 * <tt>?? Term :: Term definition !!</tt> 20 * </p> 21 * <pre> 22 * Copyright (C) 2005, 2007 DFG/M.Watermann, D-10247 Berlin, FRG 23 * All rights reserved 24 * EMail : <support@mwat.de> 25 * </pre> 26 * <p> 27 * <em>Credits:</em> This plugin was inspired by ideas of 28 * <a href="http://wiki.splitbrain.org/plugin:definition_list" 29 * target="_blank">Stephane Chamberland</a> and <a target="_blank" 30 * href="http://wiki.splitbrain.org/plugin:definitions">Pavel 31 * Vitis</a> both of whom wrote a similar plugin that <em>almost</em> 32 * worked. 33 * </p> 34 * <div class="disclaimer"> 35 * This program is free software; you can redistribute it and/or modify 36 * it under the terms of the GNU General Public License as published by 37 * the Free Software Foundation; either 38 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the 39 * License, or (at your option) any later version.<br> 40 * This software is distributed in the hope that it will be useful, 41 * but WITHOUT ANY WARRANTY; without even the implied warranty of 42 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 43 * General Public License for more details. 44 * </div> 45 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a> 46 * @version <tt>$Id: syntax_plugin_deflist.php,v 1.14 2007/08/15 12:36:20 matthias Exp $</tt> 47 * @since created 05-Aug-2005 48 */ 49class syntax_plugin_deflist extends DokuWiki_Syntax_Plugin { 50 51 /** 52 * @privatesection 53 */ 54 //@{ 55 56 /** 57 * Convert the specified <tt>$aID</tt> to a valid XHTML 58 * fragment identifier. 59 * 60 * <p> 61 * <a href="http://www.w3.org/TR/xhtml1/#guidelines" target="_blank"> 62 * XHTML 1</a> (section C.8, Fragment Identifiers) gives the regex 63 * <tt>[A-Za-z][A-Za-z0-9:_.-]*</tt> for valid identifiers. Here 64 * it's slightly reduced to <tt>[A-Za-z][A-Za-z0-9_]+</tt> i.e. 65 * all non alphanumeric characters are replaced by underscores. 66 * </p> 67 * @param $aID String The raw ID string. 68 * @return String 69 * @private 70 * @since created 24-Aug-2005 71 * @see render() 72 */ 73 function _makeID(&$aID) { 74 static $CHARS; 75 if (! is_array($CHARS)) { 76 $CHARS = array('|[^A-Za-z0-9_]|', // replace invalid characters 77 '|_{2,}|', // reduce multiple underscores 78 '|^[^A-Za-z]+|', // remove invalid leading chars 79 '|_+$|'); // remove trailing underscores 80 } // if 81 // As long as DokuWiki (in contrast to W3C) doesn't allow uppercase 82 // letters in internal anchor names we've to use 'strtolower()' 83 // here as well to make the anchors work within DokuWiki. 84 return strtolower(preg_replace($CHARS, array('_', '_'), 85 utf8_deaccent($aID, 0))); 86 } // _makeID() 87 88 //@} 89 /** 90 * @publicsection 91 */ 92 //@{ 93 94 /** 95 * Tell the parser whether the plugin accepts syntax mode 96 * <tt>$aMode</tt> within its own markup. 97 * 98 * <p> 99 * This method mostly returns <tt>TRUE</tt> since all other types 100 * are allowed within a definition list's <tt>DD</tt> sections. 101 * Only another definition list is denied since <em>nested DLs are 102 * currently not supported</em>. 103 * </p> 104 * @param $aMode String The requested syntaxmode. 105 * @return Boolean <tt>TRUE</tt> unless <tt>$aMode</tt> 106 * is <tt>plugin_deflist</tt> (which would result in a 107 * <tt>FALSE</tt> method result). 108 * @public 109 * @see getAllowedTypes() 110 */ 111 function accepts($aMode) { 112 return (PLUGIN_DEFLIST != $aMode); 113 } // accepts() 114 115 /** 116 * Connect lookup pattern to lexer. 117 * 118 * @param $aMode String The desired rendermode. 119 * @public 120 * @see render() 121 */ 122 function connectTo($aMode) { 123 if (PLUGIN_DEFLIST == $aMode) { 124 return; 125 } // if 126 // We have to use assertion patterns here to make sure the DD sections 127 // are UNMATCHED since only those are subject to further substitution. 128 $this->Lexer->addEntryPattern( 129 '\n\x20{2,}\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)', 130 $aMode, PLUGIN_DEFLIST); 131 $this->Lexer->addEntryPattern( 132 '\n\t+\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)', 133 $aMode, PLUGIN_DEFLIST); 134 $this->Lexer->addPattern( 135 '\n\x20{2,}\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST); 136 $this->Lexer->addPattern( 137 '\n\t+\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST); 138 } // connectTo() 139 140 /** 141 * Get an associative array with plugin info. 142 * 143 * <p> 144 * The returned array holds the following fields: 145 * <dl> 146 * <dt>author</dt><dd>Author of the plugin</dd> 147 * <dt>email</dt><dd>Email address to contact the author</dd> 148 * <dt>date</dt><dd>Last modified date of the plugin in 149 * <tt>YYYY-MM-DD</tt> format</dd> 150 * <dt>name</dt><dd>Name of the plugin</dd> 151 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd> 152 * <dt>url</dt><dd>Website with more information on the plugin 153 * (eg. syntax description)</dd> 154 * </dl> 155 * @return Array Information about this plugin class. 156 * @public 157 * @static 158 */ 159 function getInfo() { 160 return array( 161 'author' => 'Matthias Watermann', 162 'email' => 'support@mwat.de', 163 'date' => '2007-08-15', 164 'name' => 'Definition List Syntax Plugin', 165 'desc' => '(X)HTML style Definition Lists [ ?? Term :: Definition !! ]', 166 'url' => 'http://wiki.splitbrain.org/plugin:deflist'); 167 } // getInfo() 168 169 /** 170 * Define how this plugin is handled regarding paragraphs. 171 * 172 * <p> 173 * This method is important for correct XHTML nesting. It returns 174 * one of the following values: 175 * </p> 176 * <dl> 177 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd> 178 * <dt>block</dt><dd>Open paragraphs need to be closed before 179 * plugin output.</dd> 180 * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd> 181 * </dl> 182 * @return String <tt>'normal'</tt> instead of the (correct) 'block' 183 * since otherwise the current DokuWiki parser would put all 184 * substitutions within a DD section in separate paragraphs. 185 * @public 186 * @static 187 */ 188 function getPType() { 189 return 'normal'; 190 } // getPType() 191 192 /** 193 * Where to sort in? 194 * 195 * @return Integer <tt>18</tt>, an arbitrary value smaller 196 * <tt>Doku_Parser_Mode_preformated</tt> (20). 197 * @public 198 * @static 199 */ 200 function getSort() { 201 return 18; 202 } // getSort() 203 204 /** 205 * Get the type of syntax this plugin defines. 206 * 207 * @return String <tt>'container'</tt>. 208 * @public 209 * @static 210 */ 211 function getType() { 212 return 'container'; 213 } // getType() 214 215 /** 216 * Handler to prepare matched data for the rendering process. 217 * 218 * <p> 219 * The <tt>$aState</tt> parameter gives the type of pattern 220 * which triggered the call to this method: 221 * </p> 222 * <dl> 223 * <dt>DOKU_LEXER_ENTER</dt> 224 * <dd>a pattern set by <tt>addEntryPattern()</tt>.</dd> 225 * <dt>DOKU_LEXER_MATCHED</dt> 226 * <dd>a pattern set by <tt>addPattern()</tt> (here: DT data).</dd> 227 * <dt>DOKU_LEXER_EXIT</dt> 228 * <dd> a pattern set by <tt>addExitPattern()</tt>.</dd> 229 * <dt>DOKU_LEXER_UNMATCHED</dt> 230 * <dd>ordinary text encountered within the plugin's syntax mode 231 * which doesn't match any pattern (here: DD data).</dd> 232 * </dl> 233 * @param $aMatch String The text matched by the patterns. 234 * @param $aState Integer The lexer state for the match. 235 * @param $aPos Integer The character position of the matched text. 236 * @param $aHandler Object Reference to the Doku_Handler object. 237 * @return Array Index <tt>[0]</tt> holds the current 238 * <tt>$aState</tt>, index <tt>[1]</tt> the match (as a list of 239 * entries) prepared for the <tt>render()</tt> method. 240 * @public 241 * @see render() 242 * @static 243 */ 244 function handle($aMatch, $aState, $aPos, &$aHandler) { 245 static $ESCDELIMS; // static constants to avoid the runtime overhead 246 static $UNDELIMS; // of re-creating the arrays on each method call 247 if (! is_array($ESCDELIMS)) { 248 $ESCDELIMS = array('\?', '\!', '\:'); 249 } // if 250 if (! is_array($UNDELIMS)) { 251 $UNDELIMS = array('?', '!', ':'); 252 } // if 253 switch ($aState) { 254 case DOKU_LEXER_ENTER: 255 // fall through to extract initial DTs 256 case DOKU_LEXER_MATCHED: // DTs 257 $aMatch = preg_split('|\n+(\s*\?\?)\s*|', $aMatch, 258 -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); 259 $dts = array(); 260 $c = count($aMatch); 261 for ($i = 0; $c > $i; ++$i) { 262 if ($i & 1) { 263 $dts[] = array($aMatch[$i - 1], 264 str_replace($ESCDELIMS, $UNDELIMS, 265 trim($aMatch[$i]))); 266 $aMatch[$i - 1] = $aMatch[$i] = NULL; 267 } else { 268 $aMatch[$i] = strlen( 269 str_replace(' ', "\t", $aMatch[$i])) - 2; 270 } // if 271 } // for 272 return array($aState, $dts); 273 case DOKU_LEXER_UNMATCHED: // DDs 274 $aMatch = preg_split('|\s*(::\s*.*?!!)|s', $aMatch, 275 -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); 276 $mark = FALSE; // indication for kind of DD entry 277 $c = count($aMatch); 278 $hits = $dds = array(); 279 for ($i = 0; $c > $i; ++$i) { 280 if (preg_match('|::\s*(.*?)\s*!!|s', $aMatch[$i], $hits)) { 281 $mark = 0; // complete DD w/o substitution(s) 282 } else if (preg_match('|::\s*(.*)|s', $aMatch[$i], $hits)) { 283 $mark = -1; // DD part before substitution(s) 284 } else if (preg_match('|(.*?)\s*!!|s', $aMatch[$i], $hits)) { 285 $mark = +1; // DD part behind substitution(s) 286 } else { 287 $mark = TRUE; // DD part between substitutions 288 $hits[1] = $aMatch[$i]; 289 } // if 290 $dds[] = array( 291 str_replace($ESCDELIMS, $UNDELIMS, $hits[1]) => $mark); 292 } // for 293 return array($aState, $dds); 294 case DOKU_LEXER_EXIT: 295 // end of list 296 default: 297 return array($aState); 298 } // switch 299 } // handle() 300 301 /** 302 * Add exit pattern to lexer. 303 * 304 * <p> 305 * Two consecutive linefeeds mark the end'o'list. 306 * </p> 307 * @note Access <em>public</em> 308 */ 309 function postConnect() { 310 $this->Lexer->addExitPattern('(?<=!!)\n(?=\n)', PLUGIN_DEFLIST); 311 } // postConnect() 312 313 /** 314 * Handle the actual output creation. 315 * 316 * <p> 317 * The method tests the given <tt>$aFormat</tt> returning 318 * <tt>FALSE</tt> if it's not supported. <tt>$aRenderer</tt> 319 * contains a reference to the renderer object which is currently 320 * handling the rendering. The contents of <tt>$aData</tt> is the 321 * return value of the <tt>handle()</tt> method. 322 * </p> 323 * @param $aFormat String The output format to being tendered. 324 * @param $aRenderer Object A reference to the renderer object. 325 * @param $aData Array The data created by the <tt>handle()</tt> 326 * method. 327 * @return Boolean <tt>TRUE</tt> if rendered successfully, or 328 * <tt>FALSE</tt> otherwise. 329 * @public 330 * @see handle() 331 * @static 332 */ 333 function render($aFormat, &$aRenderer, &$aData) { 334 if ('xhtml' != $aFormat) { 335 return FALSE; 336 } // if 337 static $LEVEL = 1; // current nesting level 338 static $INDD = array(); // marks whether there's an open DD 339 static $CHARS; static $ENTS; // HTML special chars 340 if (! is_array($CHARS)) { 341 $CHARS = array('&','<', '>'); 342 } // if 343 if (! is_array($ENTS)) { 344 $ENTS = array('&', '<', '>'); 345 } // if 346 // XXX: All those <p> and </p> tags handled here are just kind 347 // of workaround problems with the current DokuWiki renderer. 348 // Basically they are __wrong__ here but, alas, without them 349 // invalid HTML would be generated :-( 350 // If and when DokuWiki becomes more statefull the superflous 351 // tags should be removed. 352 switch ($aData[0]) { 353 case DOKU_LEXER_ENTER: 354 // since we have to use PType 'normal' we must close 355 // the current paragraph 356 $hits = array(); 357 if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) { 358 $aRenderer->doc = substr($aRenderer->doc, 359 0, -strlen($hits[0])) . '<dl>'; 360 } else { 361 $aRenderer->doc .= '</p><dl>'; 362 } // if 363 // fall through to render initial DTs 364 case DOKU_LEXER_MATCHED: 365 foreach ($aData[1] as $dt) { 366 $diff = $dt[0] - $LEVEL; 367 if (0 < $diff) { 368 // going UP __one__ level 369 ++$LEVEL; 370 $hits = array(); 371 if (preg_match('|\s*<dd>\s*<p>\s*$|i', 372 $aRenderer->doc, $hits)) { 373 $aRenderer->doc = substr($aRenderer->doc, 374 0, -strlen($hits[0])) . '<dd><dl>'; 375 } else { 376 $aRenderer->doc .= (preg_match( 377 '|\s*</d[dt]>\s*$|i', $aRenderer->doc)) 378 ? '<dd><dl>' 379 : '</dd><dd><dl>'; 380 } // if 381 } else if (0 > $diff) { 382 do { // going back some levels 383 --$LEVEL; 384 $aRenderer->doc .= (isset($INDD[$LEVEL])) 385 ? '</dl>' 386 : '</dl></dd>'; 387 } while (0 > ++$diff); 388 // ELSE: no level change 389 } // if 390 $hits = array(); 391 if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) { 392 // remove unneeded P 393 $aRenderer->doc = substr($aRenderer->doc, 394 0, -strlen($hits[0])); 395 } // if 396 $id = $this->_makeID($dt[1]); 397 // see http://www.w3.org/TR/xhtml1/#h-4.10 398 $aRenderer->doc .= '<dt><a id="' . $id . '" name="' 399 . $id . '">' . str_replace($CHARS, $ENTS, $dt[1]) 400 . '</a></dt>'; 401 } // foreach 402 return TRUE; 403 case DOKU_LEXER_UNMATCHED: 404 $c = count($aData[1]); 405 for ($i = 0; $c > $i; ++$i) { 406 list($dd, $mark) = each($aData[1][$i]); 407 $dd = str_replace($CHARS, $ENTS, $dd); 408 if (TRUE === $mark) { 409 // part between substitutions 410 if (isset($INDD[$LEVEL])) { 411 if (strlen($dd)) { 412 $aRenderer->doc .= $dd; 413 } // if 414 } else { 415 $tabs = str_repeat("\t", $LEVEL); 416 $aRenderer->doc .= (strlen($dd)) 417 ? '</dl><p>' . $dd 418 : '</dl>'; 419 $INDD[--$LEVEL] = TRUE; 420 } // if 421 } else if (0 == $mark) { 422 // complete definition w/o substitutions 423 if (strlen($dd)) { 424 $aRenderer->doc .= '<dd><p>' . $dd . '</p></dd>'; 425 } // if 426 } else if (0 > $mark) { 427 // DD part before substitutions 428 $aRenderer->doc .= (strlen($dd)) 429 ? '<dd><p>' . $dd 430 : '<dd><p>'; 431 $INDD[$LEVEL] = TRUE; 432 } else if (0 < $mark) { 433 // DD part behind substitutions 434 if (isset($INDD[$LEVEL])) { 435 if (strlen($dd)) { 436 $aRenderer->doc .= $dd . '</p></dd>'; 437 } else { 438 $hits = array(); 439 if (preg_match('|\s*<p>\s*$|i', 440 $aRenderer->doc, $hits)) { 441 $aRenderer->doc = substr( 442 $aRenderer->doc, 0, 443 -strlen($hits[0])) . '</dd>'; 444 } else { 445 $aRenderer->doc .= '</p></dd>'; 446 } // if 447 } // if 448 unset($INDD[$LEVEL]); 449 } // if 450 // ELSE: doesn't ever happen with non-empty $dd 451 } // if 452 } // for 453 return TRUE; 454 case DOKU_LEXER_EXIT: 455 // Close all possibly open lists: 456 while (0 < --$LEVEL) { 457 $aRenderer->doc .= '</dl></dd>'; 458 } // while 459 // Since we have to use PType 'normal' we must open 460 // a new paragraph for the following text 461 $aRenderer->doc = preg_replace('|\s*<p>\s*</p>\s*|', '', 462 $aRenderer->doc) . '</dl><p>'; 463 $INDD = array(); 464 $LEVEL = 1; 465 default: 466 return TRUE; 467 } // switch 468 } // render() 469 470 //@} 471} // class syntax_plugin_deflist 472} // if 473?> 474