1<?php 2 3/** 4* 5* Parses for bulleted and numbered lists. 6* 7* @category Text 8* 9* @package Text_Wiki 10* 11* @author Paul M. Jones <pmjones@php.net> 12* 13* @license LGPL 14* 15* @version $Id$ 16* 17*/ 18 19/** 20* 21* Parses for bulleted and numbered lists. 22* 23* This class implements a Text_Wiki_Parse to find source text marked as 24* a bulleted or numbered list. In short, if a line starts with '* ' then 25* it is a bullet list item; if a line starts with '# ' then it is a 26* number list item. Spaces in front of the * or # indicate an indented 27* sub-list. The list items must be on sequential lines, and may be 28* separated by blank lines to improve readability. Using a non-* non-# 29* non-whitespace character at the beginning of a line ends the list. 30* 31* @category Text 32* 33* @package Text_Wiki 34* 35* @author Paul M. Jones <pmjones@php.net> 36* 37*/ 38 39class Text_Wiki_Parse_List extends Text_Wiki_Parse { 40 41 42 /** 43 * 44 * The regular expression used to parse the source text and find 45 * matches conforming to this rule. Used by the parse() method. 46 * 47 * @access public 48 * 49 * @var string 50 * 51 * @see parse() 52 * 53 */ 54 55 var $regex = '/\n(( *)[\*#] .*\n(?!( *)(\*|#)+))/Us'; 56 57 /** 58 * 59 * Generates a replacement for the matched text. Token options are: 60 * 61 * 'type' => 62 * 'bullet_start' : the start of a bullet list 63 * 'bullet_end' : the end of a bullet list 64 * 'number_start' : the start of a number list 65 * 'number_end' : the end of a number list 66 * 'item_start' : the start of item text (bullet or number) 67 * 'item_end' : the end of item text (bullet or number) 68 * 'unknown' : unknown type of list or item 69 * 70 * 'level' => the indent level (0 for the first level, 1 for the 71 * second, etc) 72 * 73 * 'count' => the list item number at this level. not needed for 74 * xhtml, but very useful for PDF and RTF. 75 * 76 * @access public 77 * 78 * @param array &$matches The array of matches from parse(). 79 * 80 * @return A series of text and delimited tokens marking the different 81 * list text and list elements. 82 * 83 */ 84 85 function process(&$matches) 86 { 87 // the replacement text we will return 88 $return = ''; 89 90 // the list of post-processing matches 91 $list = array(); 92 93 // a stack of list-start and list-end types; we keep this 94 // so that we know what kind of list we're working with 95 // (bullet or number) and what indent level we're at. 96 $stack = array(); 97 98 // the item count is the number of list items for any 99 // given list-type on the stack 100 $itemcount = array(); 101 102 // have we processed the very first list item? 103 $pastFirst = false; 104 105 // populate $list with this set of matches. $matches[1] is the 106 // text matched as a list set by parse(). 107 preg_match_all( 108 '/^( *)([\*#]) (.*)$/Ums', 109 $matches[1], 110 $list, 111 PREG_SET_ORDER 112 ); 113 114 // loop through each list-item element. 115 foreach ($list as $key => $val) { 116 // $val[0] is the full matched list-item line 117 // $val[1] is the type (* or #) 118 // $val[2] is the level (number) 119 // $val[3] is the list item text 120 121 // how many levels are we indented? (1 means the "root" 122 // list level, no indenting.) 123 $level = strlen($val[1]) + 1; 124 125 // get the list item type 126 if ($val[2] == '*') { 127 $type = 'bullet'; 128 } elseif ($val[2] == '#') { 129 $type = 'number'; 130 } else { 131 $type = 'unknown'; 132 } 133 134 // get the text of the list item 135 $text = $val[3]; 136 137 // add a level to the list? 138 if ($level > count($stack)) { 139 140 // the current indent level is greater than the 141 // number of stack elements, so we must be starting 142 // a new list. push the new list type onto the 143 // stack... 144 array_push($stack, $type); 145 146 // ...and add a list-start token to the return. 147 $return .= $this->wiki->addToken( 148 $this->rule, 149 array( 150 'type' => $type . '_list_start', 151 'level' => $level - 1 152 ) 153 ); 154 } 155 156 // remove a level from the list? 157 while (count($stack) > $level) { 158 159 // so we don't keep counting the stack, we set up a temp 160 // var for the count. -1 becuase we're going to pop the 161 // stack in the next command. $tmp will then equal the 162 // current level of indent. 163 $tmp = count($stack) - 1; 164 165 // as long as the stack count is greater than the 166 // current indent level, we need to end list types. 167 // continue adding end-list tokens until the stack count 168 // and the indent level are the same. 169 $return .= $this->wiki->addToken( 170 $this->rule, 171 array ( 172 'type' => array_pop($stack) . '_list_end', 173 'level' => $tmp 174 ) 175 ); 176 177 // reset to the current (previous) list type so that 178 // the new list item matches the proper list type. 179 $type = $stack[$tmp - 1]; 180 181 // reset the item count for the popped indent level 182 unset($itemcount[$tmp + 1]); 183 } 184 185 // add to the item count for this list (taking into account 186 // which level we are at). 187 if (! isset($itemcount[$level])) { 188 // first count 189 $itemcount[$level] = 0; 190 } else { 191 // increment count 192 $itemcount[$level]++; 193 } 194 195 // is this the very first item in the list? 196 if (! $pastFirst) { 197 $first = true; 198 $pastFirst = true; 199 } else { 200 $first = false; 201 } 202 203 // create a list-item starting token. 204 $start = $this->wiki->addToken( 205 $this->rule, 206 array( 207 'type' => $type . '_item_start', 208 'level' => $level, 209 'count' => $itemcount[$level], 210 'first' => $first 211 ) 212 ); 213 214 // create a list-item ending token. 215 $end = $this->wiki->addToken( 216 $this->rule, 217 array( 218 'type' => $type . '_item_end', 219 'level' => $level, 220 'count' => $itemcount[$level] 221 ) 222 ); 223 224 // add the starting token, list-item text, and ending token 225 // to the return. 226 $return .= $start . $text . $end; 227 } 228 229 // the last list-item may have been indented. go through the 230 // list-type stack and create end-list tokens until the stack 231 // is empty. 232 while (count($stack) > 0) { 233 $return .= $this->wiki->addToken( 234 $this->rule, 235 array ( 236 'type' => array_pop($stack) . '_list_end', 237 'level' => count($stack) 238 ) 239 ); 240 } 241 242 // we're done! send back the replacement text. 243 return "\n" . $return . "\n\n"; 244 } 245} 246?>