xref: /plugin/yalist/syntax.php (revision b9bed834b73f9d5a53ad1db8e9fe213c33befc24)
1<?php
2/*
3 * This plugin extends DokuWiki's list markup syntax to allow definition lists
4 * and list items with multiple paragraphs. The complete syntax is as follows:
5 *
6 *
7 *   - ordered list item            [<ol><li>]  <!-- as standard syntax -->
8 *   * unordered list item          [<ul><li>]  <!-- as standard syntax -->
9 *   ? definition list term         [<dl><dt>]
10 *   : definition list definition   [<dl><dd>]
11 *
12 *   -- ordered list item w/ multiple paragraphs
13 *   ** unordered list item w/ multiple paragraphs
14 *   :: definition list definition w/multiple paragraphs
15 *   .. new paragraph in --, **, or ::
16 *
17 *
18 * Lists can be nested within lists, just as in the standard DokuWiki syntax.
19 *
20 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
21 * @author     Ben Slusky <sluskyb@paranoiacs.org>
22 *
23 */
24if (!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
25if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
26require_once(DOKU_PLUGIN.'syntax.php');
27class syntax_plugin_yalist extends DokuWiki_Syntax_Plugin {
28    var $stack = array();
29    function getType() {
30        return 'container';
31    }
32    function getSort() {
33        // just before listblock (10)
34        return 9;
35    }
36    function getPType() {
37        return 'block';
38    }
39    function getAllowedTypes() {
40        return array('substition', 'protected', 'disabled', 'formatting');
41    }
42    function connectTo($mode) {
43       $this->Lexer->addEntryPattern('\n {2,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist');
44       $this->Lexer->addEntryPattern('\n\t{1,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist');
45       $this->Lexer->addPattern('\n {2,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist');
46       $this->Lexer->addPattern('\n\t{1,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist');
47    }
48    function postConnect() {
49        $this->Lexer->addExitPattern('\n', 'plugin_yalist');
50    }
51    function handle($match, $state, $pos, Doku_Handler $handler) {
52        $output = array();
53        $level = 0;
54        switch ($state) {
55        case DOKU_LEXER_ENTER:
56            $frame = $this->_interpret_match($match);
57            $level = $frame['level'] = 1;
58            array_push($output,
59                       "${frame['list']}_open",
60                       "${frame['item']}_open",
61                       "${frame['item']}_content_open");
62            if ($frame['paras'])
63                array_push($output, 'p_open');
64            array_push($this->stack, $frame);
65            break;
66        case DOKU_LEXER_EXIT:
67            $close_content = true;
68            while ($frame = array_pop($this->stack)) {
69                // for the first frame we pop off the stack, we'll need to
70                // close the content tag; for the rest it will have been
71                // closed already
72                if ($close_content) {
73                    if ($frame['paras'])
74                        array_push($output, 'p_close');
75                    array_push($output, "${frame['item']}_content_close");
76                    $close_content = false;
77                }
78                array_push($output,
79                           "${frame['item']}_close",
80                           "${frame['list']}_close");
81            }
82            break;
83        case DOKU_LEXER_MATCHED:
84            $last_frame = end($this->stack);
85            if (substr($match, -2) == '..') {
86                // new paragraphs cannot be deeper than the current depth,
87                // but they may be shallower
88                $para_depth = count(explode('  ', str_replace("\t", '  ', $match)));
89                $close_content = true;
90                while ($para_depth < $last_frame['depth'] && count($this->stack) > 1) {
91                    if ($close_content) {
92                        if ($last_frame['paras'])
93                            array_push($output, 'p_close');
94                        array_push($output, "${last_frame['item']}_content_close");
95                        $close_content = false;
96                    }
97                    array_push($output,
98                               "${last_frame['item']}_close",
99                               "${last_frame['list']}_close");
100                    array_pop($this->stack);
101                    $last_frame = end($this->stack);
102                }
103                if ($last_frame['paras']) {
104                    if ($close_content)
105                        // depth did not change
106                        array_push($output, 'p_close', 'p_open');
107                    else
108                        array_push($output,
109                                   "${last_frame['item']}_content_open",
110                                   'p_open');
111                } else {
112                    // let's just pretend we didn't match...
113                    $state = DOKU_LEXER_UNMATCHED;
114                    $output = $match;
115                }
116                break;
117            }
118            $curr_frame = $this->_interpret_match($match);
119            if ($curr_frame['depth'] > $last_frame['depth']) {
120                // going one level deeper
121                $level = $last_frame['level'] + 1;
122                if ($last_frame['paras'])
123                    array_push($output, 'p_close');
124                array_push($output,
125                           "${last_frame['item']}_content_close",
126                           "${curr_frame['list']}_open");
127            } else {
128                // same depth, or getting shallower
129                $close_content = true;
130                // keep popping frames off the stack until we find a frame
131                // that's at least as deep as this one, or until only the
132                // bottom frame (i.e. the initial list markup) remains
133                while ($curr_frame['depth'] < $last_frame['depth'] &&
134                       count($this->stack) > 1)
135                {
136                    // again, we need to close the content tag only for
137                    // the first frame popped off the stack
138                    if ($close_content) {
139                        if ($last_frame['paras'])
140                            array_push($output, 'p_close');
141                        array_push($output, "${last_frame['item']}_content_close");
142                        $close_content = false;
143                    }
144                    array_push($output,
145                               "${last_frame['item']}_close",
146                               "${last_frame['list']}_close");
147                    array_pop($this->stack);
148                    $last_frame = end($this->stack);
149                }
150                // pull the last frame off the stack;
151                // it will be replaced by the current frame
152                array_pop($this->stack);
153                $level = $last_frame['level'];
154                if ($close_content) {
155                    if ($last_frame['paras'])
156                        array_push($output, 'p_close');
157                    array_push($output, "${last_frame['item']}_content_close");
158                    $close_content = false;
159                }
160                array_push($output, "${last_frame['item']}_close");
161                if ($curr_frame['list'] != $last_frame['list']) {
162                    // change list types
163                    array_push($output,
164                               "${last_frame['list']}_close",
165                               "${curr_frame['list']}_open");
166                }
167            }
168            // and finally, open tags for the new list item
169            array_push($output,
170                       "${curr_frame['item']}_open",
171                       "${curr_frame['item']}_content_open");
172            if ($curr_frame['paras'])
173                array_push($output, 'p_open');
174            $curr_frame['level'] = $level;
175            array_push($this->stack, $curr_frame);
176            break;
177        case DOKU_LEXER_UNMATCHED:
178            $output = $match;
179            break;
180        }
181        return array('state' => $state, 'output' => $output, 'level' => $level);
182    }
183    function _interpret_match($match) {
184        $tag_table = array(
185            '*' => 'u_li',
186            '-' => 'o_li',
187            '?' => 'dt',
188            ':' => 'dd',
189        );
190        $tag = $tag_table[substr($match, -1)];
191        return array(
192            'depth' => count(explode('  ', str_replace("\t", '  ', $match))),
193            'list' => substr($tag, 0, 1) . 'l',
194            'item' => substr($tag, -2),
195            'paras' => (substr($match, -1) == substr($match, -2, 1)),
196        );
197    }
198    function render($mode, Doku_Renderer $renderer, $data) {
199        if ($mode != 'xhtml' && $mode != 'latex')
200            return false;
201        if ($data['state'] == DOKU_LEXER_UNMATCHED) {
202            $renderer->doc .= $renderer->_xmlEntities($data['output']);
203            return true;
204        }
205        foreach ($data['output'] as $i) {
206            $markup = '';
207            if ($mode == 'xhtml') {
208                switch ($i) {
209                case 'ol_open':
210                    $markup = "<ol>\n";
211                    break;
212                case 'ol_close':
213                    $markup = "</ol>\n";
214                    break;
215                case 'ul_open':
216                    $markup = "<ul>\n";
217                    break;
218                case 'ul_close':
219                    $markup = "</ul>\n";
220                    break;
221                case 'dl_open':
222                    $markup = "<dl>\n";
223                    break;
224                case 'dl_close':
225                    $markup = "</dl>\n";
226                    break;
227                case 'li_open':
228                    $markup = "<li class=\"level${data['level']}\">";
229                    break;
230                case 'li_content_open':
231                    $markup = "<div class=\"li\">\n";
232                    break;
233                case 'li_content_close':
234                    $markup = "\n</div>";
235                    break;
236                case 'li_close':
237                    $markup = "</li>\n";
238                    break;
239                case 'dt_open':
240                    $markup = "<dt class=\"level${data['level']}\">";
241                    break;
242                case 'dt_content_open':
243                    $markup = "<span class=\"dt\">";
244                    break;
245                case 'dt_content_close':
246                    $markup = "</span>";
247                    break;
248                case 'dt_close':
249                    $markup = "</dt>\n";
250                    break;
251                case 'dd_open':
252                    $markup = "<dd class=\"level${data['level']}\">";
253                    break;
254                case 'dd_content_open':
255                    $markup = "<div class=\"dd\">\n";
256                    break;
257                case 'dd_content_close':
258                    $markup = "\n</div>";
259                    break;
260                case 'dd_close':
261                    $markup = "</dd>\n";
262                    break;
263                case 'p_open':
264                    $markup = "<p>\n";
265                    break;
266                case 'p_close':
267                    $markup = "\n</p>";
268                    break;
269                }
270            } else {
271                // $mode == 'latex'
272                switch ($i) {
273                case 'ol_open':
274                    $markup = "\\begin{enumerate}\n";
275                    break;
276                case 'ol_close':
277                    $markup = "\\end{enumerate}\n";
278                    break;
279                case 'ul_open':
280                    $markup = "\\begin{itemize}\n";
281                    break;
282                case 'ul_close':
283                    $markup = "\\end{itemize}\n";
284                    break;
285                case 'dl_open':
286                    $markup = "\\begin{description}\n";
287                    break;
288                case 'dl_close':
289                    $markup = "\\end{description}\n";
290                    break;
291                case 'li_open':
292                    $markup = "\item ";
293                    break;
294                case 'li_content_open':
295                    break;
296                case 'li_content_close':
297                    break;
298                case 'li_close':
299                    $markup = "\n";
300                    break;
301                case 'dt_open':
302                    $markup = "\item[";
303                    break;
304                case 'dt_content_open':
305                    break;
306                case 'dt_content_close':
307                    break;
308                case 'dt_close':
309                    $markup = "] ";
310                    break;
311                case 'dd_open':
312                    break;
313                case 'dd_content_open':
314                    break;
315                case 'dd_content_close':
316                    break;
317                case 'dd_close':
318                    $markup = "\n";
319                    break;
320                case 'p_open':
321                    $markup = "\n";
322                    break;
323                case 'p_close':
324                    $markup = "\n";
325                    break;
326                }
327            }
328            $renderer->doc .= $markup;
329        }
330        if ($data['state'] == DOKU_LEXER_EXIT)
331            $renderer->doc .= "\n";
332        return true;
333    }
334}