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