1<?php 2if (! class_exists('syntax_plugin_lists')) { 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_LISTS', 'plugin_lists'); 12 13/** 14 * <tt>syntax_plugin_lists.php </tt>- A PHP4 class that implements 15 * a <tt>DokuWiki</tt> plugin for <tt>un/ordered lists</tt> block 16 * elements. 17 * 18 * <p> 19 * Usage:<br> 20 * <tt> * unordered item <</tt> 21 * <tt> - ordered item <</tt> 22 * </p> 23 * <pre> 24 * Copyright (C) 2005, 2007 DFG/M.Watermann, D-10247 Berlin, FRG 25 * All rights reserved 26 * EMail : <support@mwat.de> 27 * </pre> 28 * <div class="disclaimer"> 29 * This program is free software; you can redistribute it and/or modify 30 * it under the terms of the GNU General Public License as published by 31 * the Free Software Foundation; either 32 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the 33 * License, or (at your option) any later version.<br> 34 * This software is distributed in the hope that it will be useful, 35 * but WITHOUT ANY WARRANTY; without even the implied warranty of 36 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 37 * General Public License for more details. 38 * </div> 39 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a> 40 * @version <tt>$Id: syntax_plugin_lists.php,v 1.4 2007/08/15 12:36:19 matthias Exp $</tt> 41 * @since created 29-Aug-2005 42 */ 43class syntax_plugin_lists extends DokuWiki_Syntax_Plugin { 44 45 /** 46 * @publicsection 47 */ 48 //@{ 49 50 /** 51 * Tell the parser whether the plugin accepts syntax mode 52 * <tt>$aMode</tt> within its own markup. 53 * 54 * @param $aMode String The requested syntaxmode. 55 * @return Boolean <tt>TRUE</tt> unless <tt>$aMode</tt> is 56 * <tt>PLUGIN_LISTS</tt> (which would result in a 57 * <tt>FALSE</tt> method result). 58 * @public 59 * @see getAllowedTypes() 60 */ 61 function accepts($aMode) { 62 return (PLUGIN_LISTS != $aMode); 63 } // accepts() 64 65 /** 66 * Connect lookup pattern to lexer. 67 * 68 * @param $aMode String The desired rendermode. 69 * @public 70 * @see render() 71 */ 72 function connectTo($aMode) { 73 if (PLUGIN_LISTS == $aMode) { 74 return; 75 } // if 76 $this->Lexer->addEntryPattern( 77 '\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)', 78 $aMode, PLUGIN_LISTS); 79 $this->Lexer->addPattern( 80 '\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS); 81 $this->Lexer->addEntryPattern( 82 '\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)', 83 $aMode, PLUGIN_LISTS); 84 $this->Lexer->addPattern( 85 '\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS); 86 } // connectTo() 87 88 /** 89 * Get an associative array with plugin info. 90 * 91 * <p> 92 * The returned array holds the following fields: 93 * <dl> 94 * <dt>author</dt><dd>Author of the plugin</dd> 95 * <dt>email</dt><dd>Email address to contact the author</dd> 96 * <dt>date</dt><dd>Last modified date of the plugin in 97 * <tt>YYYY-MM-DD</tt> format</dd> 98 * <dt>name</dt><dd>Name of the plugin</dd> 99 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd> 100 * <dt>url</dt><dd>Website with more information on the plugin 101 * (eg. syntax description)</dd> 102 * </dl> 103 * @return Array Information about this plugin class. 104 * @public 105 * @static 106 */ 107 function getInfo() { 108 return array( 109 'author' => 'Matthias Watermann', 110 'email' => 'support@mwat.de', 111 'date' => '2007-08-15', 112 'name' => 'List Syntax Plugin', 113 'desc' => 'Add HTML Style Un/Ordered Lists', 114 'url' => 'http://wiki.splitbrain.org/plugin:lists'); 115 } // getInfo() 116 117 /** 118 * Define how this plugin is handled regarding paragraphs. 119 * 120 * <p> 121 * This method is important for correct XHTML nesting. It returns 122 * one of the following values: 123 * </p> 124 * <dl> 125 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd> 126 * <dt>block</dt><dd>Open paragraphs need to be closed before 127 * plugin output.</dd> 128 * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd> 129 * </dl> 130 * @return String <tt>'normal'</tt> . 131 * @public 132 * @static 133 */ 134 function getPType() { 135 return 'normal'; 136 } // getPType() 137 138 /** 139 * Where to sort in? 140 * 141 * @return Integer <tt>8</tt>, an arbitrary value smaller 142 * <tt>Doku_Parser_Mode_listblock</tt> (10). 143 * @public 144 * @static 145 */ 146 function getSort() { 147 // class 'Doku_Parser_Mode_preformated' returns 20 148 // class 'Doku_Parser_Mode_listblock' returns 10 149 return 8; 150 } // getSort() 151 152 /** 153 * Get the type of syntax this plugin defines. 154 * 155 * @return String <tt>'container'</tt>. 156 * @public 157 * @static 158 */ 159 function getType() { 160 return 'container'; 161 } // getType() 162 163 /** 164 * Handler to prepare matched data for the rendering process. 165 * 166 * <p> 167 * The <tt>$aState</tt> parameter gives the type of pattern 168 * which triggered the call to this method: 169 * </p> 170 * <dl> 171 * <dt>DOKU_LEXER_ENTER</dt> 172 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd> 173 * <dt>DOKU_LEXER_MATCHED</dt> 174 * <dd>a pattern set by <tt>addPattern()</tt></dd> 175 * <dt>DOKU_LEXER_EXIT</dt> 176 * <dd> a pattern set by <tt>addExitPattern()</tt></dd> 177 * <dt>DOKU_LEXER_SPECIAL</dt> 178 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd> 179 * <dt>DOKU_LEXER_UNMATCHED</dt> 180 * <dd>ordinary text encountered within the plugin's syntax mode 181 * which doesn't match any pattern.</dd> 182 * </dl> 183 * @param $aMatch String The text matched by the patterns. 184 * @param $aState Integer The lexer state for the match. 185 * @param $aPos Integer The character position of the matched text. 186 * @param $aHandler Object Reference to the Doku_Handler object. 187 * @return Array Index <tt>[0]</tt> holds the current 188 * <tt>$aState</tt>, index <tt>[1]</tt> the match prepared for 189 * the <tt>render()</tt> method. 190 * @public 191 * @see render() 192 * @static 193 */ 194 function handle($aMatch, $aState, $aPos, &$aHandler) { 195 static $CHARS; static $ENTS; 196 if (! is_array($CHARS)) { 197 $CHARS = array('&','<', '>'); 198 } // if 199 if (! is_array($ENTS)) { 200 $ENTS = array('&', '<', '>'); 201 } // if 202 switch ($aState) { 203 case DOKU_LEXER_ENTER: 204 // fall through 205 case DOKU_LEXER_MATCHED: 206 $hits = array(); 207 if (preg_match('|\n*((\s*)(.))|', $aMatch, $hits)) { 208 return array($aState, $hits[3], 209 strlen(str_replace(' ', "\t", $hits[2]))); 210 } // if 211 return array($aState, $aMatch); 212 case DOKU_LEXER_UNMATCHED: 213 $hits = array(); 214 if (preg_match('|^\s*\x3C$|', $aMatch, $hits)) { 215 return array(DOKU_LEXER_UNMATCHED, '', +1); 216 } // if 217 if (preg_match('|(.*?)\s+\x3C$|s', $aMatch, $hits)) { 218 return array(DOKU_LEXER_UNMATCHED, 219 str_replace($CHARS, $ENTS, 220 str_replace('\<', '<', $hits[1])), +1); 221 } // if 222 if (preg_match('|(.*[^\x5C])\x3C$|s', $aMatch, $hits)) { 223 return array(DOKU_LEXER_UNMATCHED, 224 str_replace($CHARS, $ENTS, 225 str_replace('\<', '<', $hits[1])), +1); 226 } // if 227 return array(DOKU_LEXER_UNMATCHED, 228 str_replace($CHARS, $ENTS, 229 str_replace('\<', '<', $aMatch)), -1); 230 case DOKU_LEXER_EXIT: 231 // end of list 232 default: 233 return array($aState); 234 } // switch 235 } // handle() 236 237 /** 238 * Add exit pattern to lexer. 239 * 240 * @public 241 */ 242 function postConnect() { 243 // make sure the RegEx 'eats' only _one_ LF: 244 $this->Lexer->addExitPattern('(?<=\x3C)\n(?=\n)', PLUGIN_LISTS); 245 } // postConnect() 246 247 /** 248 * Handle the actual output creation. 249 * 250 * <p> 251 * The method checks for the given <tt>$aFormat</tt> and returns 252 * <tt>FALSE</tt> when a format isn't supported. <tt>$aRenderer</tt> 253 * contains a reference to the renderer object which is currently 254 * handling the rendering. The contents of <tt>$aData</tt> is the 255 * return value of the <tt>handle()</tt> method. 256 * </p> 257 * @param $aFormat String The output format to generate. 258 * @param $aRenderer Object A reference to the renderer object. 259 * @param $aData Array The data created by the <tt>handle()</tt> 260 * method. 261 * @return Boolean <tt>TRUE</tt> if rendered successfully, or 262 * <tt>FALSE</tt> otherwise. 263 * @public 264 * @see handle() 265 */ 266 function render($aFormat, &$aRenderer, &$aData) { 267 if ('xhtml' != $aFormat) { 268 return FALSE; 269 } // if 270 static $LISTS = array('*' => 'ul', '-' => 'ol'); 271 static $LEVEL = 1; // initial nesting level 272 static $INLI = array(); // INLI[LEVEL] :: 0==open LI, 1==open LI/P 273 static $CURRENT = array(); // CURRENT[LEVEL] :: * | - 274 switch ($aData[0]) { 275 case DOKU_LEXER_ENTER: 276 $CURRENT[$LEVEL] = $aData[1]; 277 $hits = array(); 278 if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) { 279 $hits = -strlen($hits[0]); 280 $aRenderer->doc = substr($aRenderer->doc, 0, $hits) 281 . '<' . $LISTS[$aData[1]] . '>'; 282 } else { 283 $aRenderer->doc .= '</p><' . $LISTS[$aData[1]] . '>'; 284 } // if 285 // fall through to handle first item 286 case DOKU_LEXER_MATCHED: 287 // $aData[0] :: match state 288 // $aData[1] :: * | - 289 // $aData[2] :: nesting level 290 $diff = $aData[2] - $LEVEL; 291 if (0 < $diff) { // going up one level 292 $CURRENT[++$LEVEL] = $aData[1]; 293 $hits = array(); 294 if (preg_match('|</li>\s*$|', $aRenderer->doc)) { 295 // need to open a new LI 296 $aRenderer->doc .= '<li class="level' . ($LEVEL - 1) 297 . '"><' . $LISTS[$CURRENT[$LEVEL]] . '>'; 298 $INLI[$LEVEL - 1] = 0; // no closing P needed 299 } else if (preg_match('|\s*<li[^>]*>\s*<p>\s*$|', 300 $aRenderer->doc, $hits)) { 301 // replace rudimentary LI 302 $hits = -strlen($hits[0]); 303 $aRenderer->doc = substr($aRenderer->doc, 0, $hits) 304 . '<li class="level' . ($LEVEL - 1) 305 . '"><' . $LISTS[$CURRENT[$LEVEL]] . '>'; 306 $INLI[$LEVEL - 1] = 0; // no closing P needed 307 } else { // possibly open LI 308 if (isset($INLI[$LEVEL - 1])) { 309 if (0 < $INLI[$LEVEL - 1]) { // open LI P 310 $aRenderer->doc .= '</p><' 311 . $LISTS[$aData[1]] . '>'; 312 $INLI[$LEVEL - 1] = 0; 313 } else { // open LI 314 $aRenderer->doc .= '<' 315 . $LISTS[$aData[1]] . '>'; 316 } // if 317 } else { // no open LI 318 $aRenderer->doc .= '<li class="level' 319 . ($LEVEL - 1) . '"><' 320 . $LISTS[$aData[1]] . '>'; 321 $INLI[$LEVEL - 1] = 0; // no closing P needed 322 } // if 323 } // if 324 } else if (0 > $diff) { // going back some levels 325 do { 326 --$LEVEL; 327 $aRenderer->doc .= '</' 328 . $LISTS[$CURRENT[$LEVEL + 1]] . '>'; 329 if (isset($INLI[$LEVEL])) { 330 $aRenderer->doc .= (0 < $INLI[$LEVEL]) 331 ? '</p></li>' 332 : '</li>'; 333 } // if 334 } while (0 > ++$diff); 335 } else if ($aData[1] != $CURRENT[$LEVEL]) { 336 // list type changed 337 if (isset($INLI[$LEVEL])) { 338 $aRenderer->doc .= (0 < $INLI[$LEVEL]) 339 ? '</p></li>' 340 : '</li>'; 341 } // if 342 $aRenderer->doc .= '</' . $LISTS[$CURRENT[$LEVEL]] 343 . '><' . $LISTS[$aData[1]] . '>'; 344 $CURRENT[$LEVEL] = $aData[1]; 345 } // if 346 $aRenderer->doc .= '<li class="level' . $LEVEL . '"><p>'; 347 $INLI[$LEVEL] = 1; // closing P needed 348 return TRUE; 349 case DOKU_LEXER_UNMATCHED: 350 // $aData[0] :: match state 351 // $aData[1] :: text 352 // $aData[2] :: +1(EoT), -1(start/inbetween) 353 if (0 < $aData[2]) { 354 // last part of item's text 355 if (strlen($aData[1])) { 356 if (isset($INLI[$LEVEL])) { 357 $aRenderer->doc .= (0 < $INLI[$LEVEL]) // LI P 358 ? $aData[1] . '</p></li>' 359 : '<p>' . $aData[1] . '</p></li>'; 360 } else { // no LI 361 if (1 < $LEVEL) { // assume a trailing LI text 362 --$LEVEL; 363 $aRenderer->doc .= '</' 364 . $LISTS[$CURRENT[$LEVEL + 1]] . '><p>' 365 . $aData[1] . '</p></li>'; 366 } else { 367//XXX: There must be no data w/o context; the markup is broken. Whatever we 368// could do it would be WRONG (and break XHMTL validity); hence comment: 369 $aRenderer->doc .= '<!-- '. $aData[1] .' -->'; 370 } // if 371 } // if 372 } else { // empty data 373 $hits = array(); 374 if (preg_match('|\s*<li[^>]*>\s*<p>\s*$|', 375 $aRenderer->doc, $hits)) { 376 $hits = -strlen($hits[0]); 377 // remove empty list item 378 $aRenderer->doc = substr($aRenderer->doc, 0, $hits); 379 } else if (preg_match('|\s*<p>\s*$|', 380 $aRenderer->doc, $hits)) { 381 $hits = -strlen($hits[0]); 382 $aRenderer->doc = 383 substr($aRenderer->doc, 0, $hits) . '</li>'; 384 } else if (isset($INLI[$LEVEL])) { 385 $aRenderer->doc .= (0 < $INLI[$LEVEL]) 386 ? '</p></li>' 387 : '</li>'; 388 } // if 389 } // if 390 unset($INLI[$LEVEL]); 391 } else { 392 // item part between substitutions or nested blocks 393 if (isset($INLI[$LEVEL])) { 394 if (0 < $INLI[$LEVEL]) { // LI P 395 $aRenderer->doc .= $aData[1]; 396 $INLI[$LEVEL] = 1; 397 } else { // LI 398 $aRenderer->doc .= '<p>' . $aData[1]; 399 } // if 400 } else { // data w/o context 401 if (1 < $LEVEL) { // assume a trailing LI text 402 --$LEVEL; 403 $aRenderer->doc .= '</' 404 . $LISTS[$CURRENT[$LEVEL + 1]] . '><p>' 405 . $aData[1]; 406 $INLI[$LEVEL] = 1; 407 } else { 408 $aRenderer->doc .= $aData[1]; 409 } // if 410 } // if 411 } // if 412 return TRUE; 413 case DOKU_LEXER_EXIT: 414 while (1 < $LEVEL) { 415 --$LEVEL; 416 $aRenderer->doc .= '</'. $LISTS[$CURRENT[$LEVEL + 1]] .'>'; 417 if (isset($INLI[$LEVEL])) { 418 $aRenderer->doc .= (0 < $INLI[$LEVEL]) 419 ? '</p></li>' 420 : '</li>'; 421 } // if 422 } // while 423 // Since we have to use PType 'normal' we must open 424 // a new paragraph for the following text 425 $aRenderer->doc = preg_replace('|\s*<p>\s*</p>\s*|', '', 426 $aRenderer->doc) . '</'. $LISTS[$CURRENT[$LEVEL]] .'><p>'; 427 $CURRENT = $INLI = array(); 428 $LEVEL = 1; 429 default: 430 return TRUE; 431 } // switch 432 } // render() 433 434 //@} 435} // class syntax_plugin_lists 436} // if 437?> 438