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