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 */
24class syntax_plugin_yalist extends DokuWiki_Syntax_Plugin {
25    private $stack = array();
26    private static $odt_table_stack = array();
27    private static $odt_table_stack_index = 0;
28
29    public function getType() {
30        return 'container';
31    }
32
33    public function getSort() {
34        // just before listblock (10)
35        return 9;
36    }
37
38    public function getPType() {
39        return 'block';
40    }
41
42    public function getAllowedTypes() {
43        return array('substition', 'protected', 'disabled', 'formatting');
44    }
45
46    public function connectTo($mode) {
47        $this->Lexer->addEntryPattern('\n {2,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist');
48        $this->Lexer->addEntryPattern('\n\t{1,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist');
49        $this->Lexer->addPattern('\n {2,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist');
50        $this->Lexer->addPattern('\n\t{1,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist');
51    }
52
53    public function postConnect() {
54        $this->Lexer->addExitPattern('\n', 'plugin_yalist');
55    }
56
57    public function handle($match, $state, $pos, Doku_Handler $handler) {
58        $output = array();
59        $level = 0;
60        switch ($state) {
61            case DOKU_LEXER_ENTER:
62                $frame = $this->interpretMatch($match);
63                $level = $frame['level'] = 1;
64                array_push($output,
65                            "${frame['list']}_open",
66                        "${frame['item']}_open",
67                        "${frame['item']}_content_open");
68            if ($frame['paras']) {
69                            array_push($output, 'p_open');
70            }
71            array_push($this->stack, $frame);
72            break;
73        case DOKU_LEXER_EXIT:
74            $close_content = true;
75            while ($frame = array_pop($this->stack)) {
76                // for the first frame we pop off the stack, we'll need to
77                // close the content tag; for the rest it will have been
78                // closed already
79                if ($close_content) {
80                    if ($frame['paras']) {
81                                            array_push($output, 'p_close');
82                    }
83                    array_push($output, "${frame['item']}_content_close");
84                    $close_content = false;
85                }
86                array_push($output,
87                            "${frame['item']}_close",
88                            "${frame['list']}_close");
89            }
90            break;
91        case DOKU_LEXER_MATCHED:
92            $last_frame = end($this->stack);
93            if (substr($match, -2) == '..') {
94                // new paragraphs cannot be deeper than the current depth,
95                // but they may be shallower
96                $para_depth = count(explode('  ', str_replace("\t", '  ', $match)));
97                $close_content = true;
98                while ($para_depth < $last_frame['depth'] && count($this->stack) > 1) {
99                    if ($close_content) {
100                        if ($last_frame['paras']) {
101                                                    array_push($output, 'p_close');
102                        }
103                        array_push($output, "${last_frame['item']}_content_close");
104                        $close_content = false;
105                    }
106                    array_push($output,
107                                "${last_frame['item']}_close",
108                                "${last_frame['list']}_close");
109                    array_pop($this->stack);
110                    $last_frame = end($this->stack);
111                }
112                if ($last_frame['paras']) {
113                    if ($close_content) {
114                                            // depth did not change
115                        array_push($output, 'p_close', 'p_open');
116                    } else {
117                                            array_push($output,
118                                    "${last_frame['item']}_content_open",
119                                    'p_open');
120                    }
121                } else {
122                    // let's just pretend we didn't match...
123                    $state = DOKU_LEXER_UNMATCHED;
124                    $output = $match;
125                }
126                break;
127            }
128            $curr_frame = $this->interpretMatch($match);
129            if ($curr_frame['depth'] > $last_frame['depth']) {
130                // going one level deeper
131                $level = $last_frame['level'] + 1;
132                if ($last_frame['paras']) {
133                                    array_push($output, 'p_close');
134                }
135                array_push($output,
136                            "${last_frame['item']}_content_close",
137                            "${curr_frame['list']}_open");
138            } else {
139                // same depth, or getting shallower
140                $close_content = true;
141                // keep popping frames off the stack until we find a frame
142                // that's at least as deep as this one, or until only the
143                // bottom frame (i.e. the initial list markup) remains
144                while ($curr_frame['depth'] < $last_frame['depth'] &&
145                       count($this->stack) > 1)
146                {
147                    // again, we need to close the content tag only for
148                    // the first frame popped off the stack
149                    if ($close_content) {
150                        if ($last_frame['paras']) {
151                                                    array_push($output, 'p_close');
152                        }
153                        array_push($output, "${last_frame['item']}_content_close");
154                        $close_content = false;
155                    }
156                    array_push($output,
157                                "${last_frame['item']}_close",
158                                "${last_frame['list']}_close");
159                    array_pop($this->stack);
160                    $last_frame = end($this->stack);
161                }
162                // pull the last frame off the stack;
163                // it will be replaced by the current frame
164                array_pop($this->stack);
165                $level = $last_frame['level'];
166                if ($close_content) {
167                    if ($last_frame['paras']) {
168                                            array_push($output, 'p_close');
169                    }
170                    array_push($output, "${last_frame['item']}_content_close");
171                    $close_content = false;
172                }
173                array_push($output, "${last_frame['item']}_close");
174                if ($curr_frame['list'] != $last_frame['list']) {
175                    // change list types
176                    array_push($output,
177                                "${last_frame['list']}_close",
178                                "${curr_frame['list']}_open");
179                }
180            }
181            // and finally, open tags for the new list item
182            array_push($output,
183                        "${curr_frame['item']}_open",
184                        "${curr_frame['item']}_content_open");
185            if ($curr_frame['paras']) {
186                            array_push($output, 'p_open');
187            }
188            $curr_frame['level'] = $level;
189            array_push($this->stack, $curr_frame);
190            break;
191        case DOKU_LEXER_UNMATCHED:
192            $output = $match;
193            break;
194        }
195        return array('state' => $state, 'output' => $output, 'level' => $level);
196    }
197
198    private function interpretMatch($match) {
199        $tag_table = array(
200            '*' => 'u_li',
201            '-' => 'o_li',
202            '?' => 'dt',
203            ':' => 'dd',
204        );
205        $tag = $tag_table[substr($match, -1)];
206        return array(
207            'depth' => count(explode('  ', str_replace("\t", '  ', $match))),
208            'list' => substr($tag, 0, 1) . 'l',
209            'item' => substr($tag, -2),
210            'paras' => (substr($match, -1) == substr($match, -2, 1)),
211        );
212    }
213
214    public function render($mode, Doku_Renderer $renderer, $data) {
215        if ($mode != 'xhtml' && $mode != 'latex' && $mode != 'odt') {
216                    return false;
217        }
218        if ($data['state'] == DOKU_LEXER_UNMATCHED) {
219            if ($mode != 'odt') {
220                $renderer->doc .= $renderer->_xmlEntities($data['output']);
221            } else {
222                $renderer->cdata($data['output']);
223            }
224            return true;
225        }
226        foreach ($data['output'] as $i) {
227            switch ($mode) {
228                case 'xhtml':
229                    $this->renderXhtmlItem($renderer, $i, $data);
230                break;
231                case 'latex':
232                    $this->renderLatexItem($renderer, $i, $data);
233                break;
234                case 'odt':
235                    $this->renderOdtItem($renderer, $i, $data);
236                break;
237            }
238        }
239        if ($data['state'] == DOKU_LEXER_EXIT) {
240            if ($mode != 'odt') {
241                $renderer->doc .= "\n";
242            } else {
243                $renderer->linebreak();
244            }
245        }
246        return true;
247    }
248
249    private function renderXhtmlItem(Doku_Renderer $renderer, $item, $data) {
250        $markup = '';
251        switch ($item) {
252            case 'ol_open':
253                $markup = "<ol>\n";
254                break;
255            case 'ol_close':
256                $markup = "</ol>\n";
257                break;
258            case 'ul_open':
259                $markup = "<ul>\n";
260                break;
261            case 'ul_close':
262                $markup = "</ul>\n";
263                break;
264            case 'dl_open':
265                $markup = "<dl>\n";
266                break;
267            case 'dl_close':
268                $markup = "</dl>\n";
269                break;
270            case 'li_open':
271                $markup = "<li class=\"level${data['level']}\">";
272            break;
273        case 'li_content_open':
274            $markup = "<div class=\"li\">\n";
275            break;
276        case 'li_content_close':
277            $markup = "\n</div>";
278            break;
279        case 'li_close':
280            $markup = "</li>\n";
281            break;
282        case 'dt_open':
283            $markup = "<dt class=\"level${data['level']}\">";
284            break;
285        case 'dt_content_open':
286            $markup = "<span class=\"dt\">";
287            break;
288        case 'dt_content_close':
289            $markup = "</span>";
290            break;
291        case 'dt_close':
292            $markup = "</dt>\n";
293            break;
294        case 'dd_open':
295            $markup = "<dd class=\"level${data['level']}\">";
296            break;
297        case 'dd_content_open':
298            $markup = "<div class=\"dd\">\n";
299            break;
300        case 'dd_content_close':
301            $markup = "\n</div>";
302            break;
303        case 'dd_close':
304            $markup = "</dd>\n";
305            break;
306        case 'p_open':
307            $markup = "<p>\n";
308            break;
309        case 'p_close':
310            $markup = "\n</p>";
311            break;
312        }
313        $renderer->doc .= $markup;
314    }
315
316    private function renderLatexItem(Doku_Renderer $renderer, $item) {
317        $markup = '';
318        switch ($item) {
319            case 'ol_open':
320                $markup = "\\begin{enumerate}\n";
321                break;
322            case 'ol_close':
323                $markup = "\\end{enumerate}\n";
324                break;
325            case 'ul_open':
326                $markup = "\\begin{itemize}\n";
327                break;
328            case 'ul_close':
329                $markup = "\\end{itemize}\n";
330                break;
331            case 'dl_open':
332                $markup = "\\begin{description}\n";
333                break;
334            case 'dl_close':
335                $markup = "\\end{description}\n";
336                break;
337            case 'li_open':
338                $markup = "\item ";
339                break;
340            case 'li_content_open':
341                break;
342            case 'li_content_close':
343                break;
344            case 'li_close':
345                $markup = "\n";
346                break;
347            case 'dt_open':
348                $markup = "\item[";
349                break;
350            case 'dt_content_open':
351                break;
352            case 'dt_content_close':
353                break;
354            case 'dt_close':
355                $markup = "] ";
356                break;
357            case 'dd_open':
358                break;
359            case 'dd_content_open':
360                break;
361            case 'dd_content_close':
362                break;
363            case 'dd_close':
364                $markup = "\n";
365                break;
366            case 'p_open':
367                $markup = "\n";
368                break;
369            case 'p_close':
370                $markup = "\n";
371                break;
372        }
373        $renderer->doc .= $markup;
374    }
375
376    /**
377     * Render yalist items for ODT format
378     *
379     * @param Doku_Renderer $renderer  The current renderer object
380     * @param string        $item      The item to render
381     *
382     * @author LarsDW223
383     */
384    private function renderOdtItem(Doku_Renderer $renderer, $item) {
385        switch ($item) {
386            case 'ol_open':
387                $renderer->listo_open();
388                break;
389            case 'ul_open':
390                $renderer->listu_open();
391                break;
392            case 'dl_open':
393                if ($this->getConf('def_list_odt_export') != 'table') {
394                    $renderer->listu_open();
395                } else {
396                    $renderer->table_open(2);
397                }
398                self::$odt_table_stack [self::$odt_table_stack_index] = array();
399                self::$odt_table_stack [self::$odt_table_stack_index]['itemOpen'] = false;
400                self::$odt_table_stack [self::$odt_table_stack_index]['dtState'] = 0;
401                self::$odt_table_stack [self::$odt_table_stack_index]['ddState'] = 0;
402                self::$odt_table_stack_index++;
403                break;
404            case 'ol_close':
405            case 'ul_close':
406                $renderer->list_close();
407                break;
408            case 'dl_close':
409                $config = $this->getConf('def_list_odt_export');
410                if ($config != 'table') {
411                    if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] != 2) {
412                        if ($config == 'listheader' && method_exists($renderer, 'listheader_close')) {
413                            $renderer->listheader_close();
414                        } else {
415                            $renderer->listitem_close();
416                        }
417                    }
418                    self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0;
419                    $renderer->list_close();
420                } else {
421                    if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] == 0) {
422                        $properties = array();
423                        $properties ['border'] = 'none';
424                        $renderer->_odtTableCellOpenUseProperties($properties);
425                        $renderer->tablecell_close();
426                    }
427                    self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0;
428                    if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
429                        $renderer->tablerow_close(1);
430                        self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
431                    }
432                    $renderer->table_close();
433                }
434                if (self::$odt_table_stack_index > 0) {
435                    self::$odt_table_stack_index--;
436                    unset(self::$odt_table_stack [self::$odt_table_stack_index]);
437                }
438                break;
439
440            case 'li_open':
441                $renderer->listitem_open(1);
442                break;
443            case 'li_content_open':
444                $renderer->listcontent_open();
445                break;
446            case 'li_content_close':
447                $renderer->listcontent_close();
448                break;
449            case 'li_close':
450                $renderer->listitem_close();
451                break;
452
453            case 'dt_open': // unconditional: DT tags can't contain paragraphs. That would not be legal XHTML.
454                switch ($this->getConf('def_list_odt_export')) {
455                    case 'listheader':
456                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
457                            if (method_exists($renderer, 'listheader_close')) {
458                                $renderer->listheader_close();
459                            } else {
460                                $renderer->listitem_close();
461                            }
462                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
463                        }
464                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
465                            if (method_exists($renderer, 'listheader_open')) {
466                                $renderer->listheader_open(1);
467                            } else {
468                                $renderer->listitem_open(1);
469                            }
470                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
471                        }
472                    break;
473                    case 'table':
474                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] == 0) {
475                            $properties = array();
476                            $properties ['border'] = 'none';
477                            $renderer->_odtTableCellOpenUseProperties($properties);
478                            $renderer->tablecell_close();
479                        }
480
481                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
482                            $renderer->tablerow_close();
483                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
484                        }
485                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
486                            $renderer->tablerow_open(1);
487                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
488                        }
489                        $properties = array();
490                        $properties ['border'] = 'none';
491                        $renderer->_odtTableCellOpenUseProperties($properties);
492                    break;
493                    default:
494                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
495                            $renderer->listitem_close();
496                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
497                        }
498                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
499                            $renderer->listitem_open(1);
500                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
501                        }
502                    break;
503                }
504            self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 1;
505            self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0;
506            break;
507            case 'dd_open':
508                switch ($this->getConf('def_list_odt_export')) {
509                    case 'listheader':
510                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
511                            if (method_exists($renderer, 'listheader_open')) {
512                                $renderer->listheader_open(1);
513                            } else {
514                                $renderer->listitem_open(1);
515                            }
516                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
517                        }
518                    break;
519                    case 'table':
520                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
521                            $renderer->tablerow_open(1);
522                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
523                        }
524                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] == 1) {
525                            $renderer->tablecell_close();
526                        }
527                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] == 0) {
528                            $properties = array();
529                            $properties ['border'] = 'none';
530                            $renderer->_odtTableCellOpenUseProperties($properties);
531                            $renderer->tablecell_close();
532                        }
533
534                        $properties = array();
535                        $properties ['border'] = 'none';
536                        $renderer->_odtTableCellOpenUseProperties($properties);
537                    break;
538                    default:
539                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) {
540                            $renderer->listitem_open(1);
541                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true;
542                        }
543                    break;
544                }
545            self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 0;
546            self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 1;
547            break;
548            case 'dt_content_open':
549                switch ($this->getConf('def_list_odt_export')) {
550                    case 'table':
551                        $renderer->p_open();
552                    break;
553                    default:
554                        $renderer->listcontent_open();
555                    break;
556                }
557            $this->renderODTOpenSpan($renderer);
558            break;
559            case 'dd_content_open':
560                switch ($this->getConf('def_list_odt_export')) {
561                    case 'table':
562                        $renderer->p_open();
563                    break;
564                    default:
565                        $renderer->listcontent_open();
566                    break;
567                }
568            break;
569            case 'dt_content_close':
570                $this->renderODTCloseSpan($renderer);
571                switch ($this->getConf('def_list_odt_export')) {
572                    case 'table':
573                        $renderer->p_close();
574                    break;
575                    default:
576                        $renderer->listcontent_close();
577                    break;
578                }
579            break;
580            case 'dd_content_close':
581                switch ($this->getConf('def_list_odt_export')) {
582                    case 'table':
583                        $renderer->p_close();
584                    break;
585                    default:
586                        $renderer->listcontent_close();
587                    break;
588                }
589            break;
590            case 'dt_close':
591                switch ($this->getConf('def_list_odt_export')) {
592                    case 'listheader':
593                        $renderer->linebreak();
594                        break;
595                    case 'table':
596                        $renderer->tablecell_close();
597                        self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 2;
598                        break;
599                    default:
600                        $renderer->linebreak();
601                        break;
602                }
603            break;
604
605            case 'dd_close':
606                switch ($this->getConf('def_list_odt_export')) {
607                    case 'listheader':
608                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
609                            if (method_exists($renderer, 'listheader_close')) {
610                                $renderer->listheader_close();
611                            } else {
612                                $renderer->listitem_close();
613                            }
614                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
615                        }
616                        break;
617                    case 'table':
618                        $renderer->tablecell_close();
619                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
620                            $renderer->tablerow_close(1);
621                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
622                        }
623                        break;
624                    default:
625                        if (self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) {
626                            $renderer->listitem_close(1);
627                            self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false;
628                        }
629                        break;
630                }
631            self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 0;
632            self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 2;
633            break;
634
635            case 'p_open':
636                $renderer->p_open();
637                break;
638            case 'p_close':
639                $renderer->p_close();
640                break;
641        }
642    }
643
644    /**
645     * Open ODT span for rendering of dt-content
646     *
647     * @param Doku_Renderer $renderer  The current renderer object
648     *
649     * @author LarsDW223
650     */
651    private function renderODTOpenSpan($renderer) {
652        $properties = array();
653
654        // Get CSS properties for ODT export.
655        $renderer->getODTProperties($properties, 'div', 'dokuwiki dt', NULL);
656
657        $renderer->_odtSpanOpenUseProperties($properties);
658    }
659
660    /**
661     * Close ODT span for rendering of dt-content
662     *
663     * @param Doku_Renderer $renderer  The current renderer object
664     *
665     * @author LarsDW223
666     */
667    private function renderODTCloseSpan($renderer) {
668        if (method_exists($renderer, '_odtSpanClose') === false) {
669            // Function is not supported by installed ODT plugin version, return.
670            return;
671        }
672        $renderer->_odtSpanClose();
673    }
674}
675