xref: /dokuwiki/inc/parser/handler.php (revision 20cfa168a7a13d9704bdb6340b791e1d5a8553aa)
1<?php
2if(!defined('DOKU_INC')) die('meh.');
3if (!defined('DOKU_PARSER_EOL')) define('DOKU_PARSER_EOL',"\n");   // add this to make handling test cases simpler
4
5class Doku_Handler {
6
7    var $Renderer = null;
8
9    var $CallWriter = null;
10
11    var $calls = array();
12
13    var $status = array(
14        'section' => false,
15        'doublequote' => 0,
16    );
17
18    var $rewriteBlocks = true;
19
20    function __construct() {
21        $this->CallWriter = new Doku_Handler_CallWriter($this);
22    }
23
24    /**
25     * @param string $handler
26     * @param mixed $args
27     * @param integer|string $pos
28     */
29    function _addCall($handler, $args, $pos) {
30        $call = array($handler,$args, $pos);
31        $this->CallWriter->writeCall($call);
32    }
33
34    function addPluginCall($plugin, $args, $state, $pos, $match) {
35        $call = array('plugin',array($plugin, $args, $state, $match), $pos);
36        $this->CallWriter->writeCall($call);
37    }
38
39    function _finalize(){
40
41        $this->CallWriter->finalise();
42
43        if ( $this->status['section'] ) {
44            $last_call = end($this->calls);
45            array_push($this->calls,array('section_close',array(), $last_call[2]));
46        }
47
48        if ( $this->rewriteBlocks ) {
49            $B = new Doku_Handler_Block();
50            $this->calls = $B->process($this->calls);
51        }
52
53        trigger_event('PARSER_HANDLER_DONE',$this);
54
55        array_unshift($this->calls,array('document_start',array(),0));
56        $last_call = end($this->calls);
57        array_push($this->calls,array('document_end',array(),$last_call[2]));
58    }
59
60    function fetch() {
61        $call = each($this->calls);
62        if ( $call ) {
63            return $call['value'];
64        }
65        return false;
66    }
67
68
69    /**
70     * Special plugin handler
71     *
72     * This handler is called for all modes starting with 'plugin_'.
73     * An additional parameter with the plugin name is passed
74     *
75     * @author Andreas Gohr <andi@splitbrain.org>
76     *
77     * @param string|integer $match
78     * @param string|integer $state
79     * @param integer $pos
80     * @param $pluginname
81     *
82     * @return bool
83     */
84    function plugin($match, $state, $pos, $pluginname){
85        $data = array($match);
86        /** @var DokuWiki_Syntax_Plugin $plugin */
87        $plugin = plugin_load('syntax',$pluginname);
88        if($plugin != null){
89            $data = $plugin->handle($match, $state, $pos, $this);
90        }
91        if ($data !== false) {
92            $this->addPluginCall($pluginname,$data,$state,$pos,$match);
93        }
94        return true;
95    }
96
97    function base($match, $state, $pos) {
98        switch ( $state ) {
99            case DOKU_LEXER_UNMATCHED:
100                $this->_addCall('cdata',array($match), $pos);
101                return true;
102            break;
103        }
104    }
105
106    function header($match, $state, $pos) {
107        // get level and title
108        $title = trim($match);
109        $level = 7 - strspn($title,'=');
110        if($level < 1) $level = 1;
111        $title = trim($title,'=');
112        $title = trim($title);
113
114        if ($this->status['section']) $this->_addCall('section_close',array(),$pos);
115
116        $this->_addCall('header',array($title,$level,$pos), $pos);
117
118        $this->_addCall('section_open',array($level),$pos);
119        $this->status['section'] = true;
120        return true;
121    }
122
123    function notoc($match, $state, $pos) {
124        $this->_addCall('notoc',array(),$pos);
125        return true;
126    }
127
128    function nocache($match, $state, $pos) {
129        $this->_addCall('nocache',array(),$pos);
130        return true;
131    }
132
133    function linebreak($match, $state, $pos) {
134        $this->_addCall('linebreak',array(),$pos);
135        return true;
136    }
137
138    function eol($match, $state, $pos) {
139        $this->_addCall('eol',array(),$pos);
140        return true;
141    }
142
143    function hr($match, $state, $pos) {
144        $this->_addCall('hr',array(),$pos);
145        return true;
146    }
147
148    /**
149     * @param string|integer $match
150     * @param string|integer $state
151     * @param integer $pos
152     * @param string $name
153     */
154    function _nestingTag($match, $state, $pos, $name) {
155        switch ( $state ) {
156            case DOKU_LEXER_ENTER:
157                $this->_addCall($name.'_open', array(), $pos);
158            break;
159            case DOKU_LEXER_EXIT:
160                $this->_addCall($name.'_close', array(), $pos);
161            break;
162            case DOKU_LEXER_UNMATCHED:
163                $this->_addCall('cdata',array($match), $pos);
164            break;
165        }
166    }
167
168    function strong($match, $state, $pos) {
169        $this->_nestingTag($match, $state, $pos, 'strong');
170        return true;
171    }
172
173    function emphasis($match, $state, $pos) {
174        $this->_nestingTag($match, $state, $pos, 'emphasis');
175        return true;
176    }
177
178    function underline($match, $state, $pos) {
179        $this->_nestingTag($match, $state, $pos, 'underline');
180        return true;
181    }
182
183    function monospace($match, $state, $pos) {
184        $this->_nestingTag($match, $state, $pos, 'monospace');
185        return true;
186    }
187
188    function subscript($match, $state, $pos) {
189        $this->_nestingTag($match, $state, $pos, 'subscript');
190        return true;
191    }
192
193    function superscript($match, $state, $pos) {
194        $this->_nestingTag($match, $state, $pos, 'superscript');
195        return true;
196    }
197
198    function deleted($match, $state, $pos) {
199        $this->_nestingTag($match, $state, $pos, 'deleted');
200        return true;
201    }
202
203
204    function footnote($match, $state, $pos) {
205//        $this->_nestingTag($match, $state, $pos, 'footnote');
206        if (!isset($this->_footnote)) $this->_footnote = false;
207
208        switch ( $state ) {
209            case DOKU_LEXER_ENTER:
210                // footnotes can not be nested - however due to limitations in lexer it can't be prevented
211                // we will still enter a new footnote mode, we just do nothing
212                if ($this->_footnote) {
213                    $this->_addCall('cdata',array($match), $pos);
214                    break;
215                }
216
217                $this->_footnote = true;
218
219                $ReWriter = new Doku_Handler_Nest($this->CallWriter,'footnote_close');
220                $this->CallWriter = & $ReWriter;
221                $this->_addCall('footnote_open', array(), $pos);
222            break;
223            case DOKU_LEXER_EXIT:
224                // check whether we have already exitted the footnote mode, can happen if the modes were nested
225                if (!$this->_footnote) {
226                    $this->_addCall('cdata',array($match), $pos);
227                    break;
228                }
229
230                $this->_footnote = false;
231
232                $this->_addCall('footnote_close', array(), $pos);
233                $this->CallWriter->process();
234                $ReWriter = & $this->CallWriter;
235                $this->CallWriter = & $ReWriter->CallWriter;
236            break;
237            case DOKU_LEXER_UNMATCHED:
238                $this->_addCall('cdata', array($match), $pos);
239            break;
240        }
241        return true;
242    }
243
244    function listblock($match, $state, $pos) {
245        switch ( $state ) {
246            case DOKU_LEXER_ENTER:
247                $ReWriter = new Doku_Handler_List($this->CallWriter);
248                $this->CallWriter = & $ReWriter;
249                $this->_addCall('list_open', array($match), $pos);
250            break;
251            case DOKU_LEXER_EXIT:
252                $this->_addCall('list_close', array(), $pos);
253                $this->CallWriter->process();
254                $ReWriter = & $this->CallWriter;
255                $this->CallWriter = & $ReWriter->CallWriter;
256            break;
257            case DOKU_LEXER_MATCHED:
258                $this->_addCall('list_item', array($match), $pos);
259            break;
260            case DOKU_LEXER_UNMATCHED:
261                $this->_addCall('cdata', array($match), $pos);
262            break;
263        }
264        return true;
265    }
266
267    function unformatted($match, $state, $pos) {
268        if ( $state == DOKU_LEXER_UNMATCHED ) {
269            $this->_addCall('unformatted',array($match), $pos);
270        }
271        return true;
272    }
273
274    function php($match, $state, $pos) {
275        global $conf;
276        if ( $state == DOKU_LEXER_UNMATCHED ) {
277            $this->_addCall('php',array($match), $pos);
278        }
279        return true;
280    }
281
282    function phpblock($match, $state, $pos) {
283        global $conf;
284        if ( $state == DOKU_LEXER_UNMATCHED ) {
285            $this->_addCall('phpblock',array($match), $pos);
286        }
287        return true;
288    }
289
290    function html($match, $state, $pos) {
291        global $conf;
292        if ( $state == DOKU_LEXER_UNMATCHED ) {
293            $this->_addCall('html',array($match), $pos);
294        }
295        return true;
296    }
297
298    function htmlblock($match, $state, $pos) {
299        global $conf;
300        if ( $state == DOKU_LEXER_UNMATCHED ) {
301            $this->_addCall('htmlblock',array($match), $pos);
302        }
303        return true;
304    }
305
306    function preformatted($match, $state, $pos) {
307        switch ( $state ) {
308            case DOKU_LEXER_ENTER:
309                $ReWriter = new Doku_Handler_Preformatted($this->CallWriter);
310                $this->CallWriter = $ReWriter;
311                $this->_addCall('preformatted_start',array(), $pos);
312            break;
313            case DOKU_LEXER_EXIT:
314                $this->_addCall('preformatted_end',array(), $pos);
315                $this->CallWriter->process();
316                $ReWriter = & $this->CallWriter;
317                $this->CallWriter = & $ReWriter->CallWriter;
318            break;
319            case DOKU_LEXER_MATCHED:
320                $this->_addCall('preformatted_newline',array(), $pos);
321            break;
322            case DOKU_LEXER_UNMATCHED:
323                $this->_addCall('preformatted_content',array($match), $pos);
324            break;
325        }
326
327        return true;
328    }
329
330    function quote($match, $state, $pos) {
331
332        switch ( $state ) {
333
334            case DOKU_LEXER_ENTER:
335                $ReWriter = new Doku_Handler_Quote($this->CallWriter);
336                $this->CallWriter = & $ReWriter;
337                $this->_addCall('quote_start',array($match), $pos);
338            break;
339
340            case DOKU_LEXER_EXIT:
341                $this->_addCall('quote_end',array(), $pos);
342                $this->CallWriter->process();
343                $ReWriter = & $this->CallWriter;
344                $this->CallWriter = & $ReWriter->CallWriter;
345            break;
346
347            case DOKU_LEXER_MATCHED:
348                $this->_addCall('quote_newline',array($match), $pos);
349            break;
350
351            case DOKU_LEXER_UNMATCHED:
352                $this->_addCall('cdata',array($match), $pos);
353            break;
354
355        }
356
357        return true;
358    }
359
360    function file($match, $state, $pos) {
361        return $this->code($match, $state, $pos, 'file');
362    }
363
364    function code($match, $state, $pos, $type='code') {
365        if ( $state == DOKU_LEXER_UNMATCHED ) {
366            $matches = explode('>',$match,2);
367
368            $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
369            while(count($param) < 2) array_push($param, null);
370
371            // We shortcut html here.
372            if ($param[0] == 'html') $param[0] = 'html4strict';
373            if ($param[0] == '-') $param[0] = null;
374            array_unshift($param, $matches[1]);
375
376            $this->_addCall($type, $param, $pos);
377        }
378        return true;
379    }
380
381    function acronym($match, $state, $pos) {
382        $this->_addCall('acronym',array($match), $pos);
383        return true;
384    }
385
386    function smiley($match, $state, $pos) {
387        $this->_addCall('smiley',array($match), $pos);
388        return true;
389    }
390
391    function wordblock($match, $state, $pos) {
392        $this->_addCall('wordblock',array($match), $pos);
393        return true;
394    }
395
396    function entity($match, $state, $pos) {
397        $this->_addCall('entity',array($match), $pos);
398        return true;
399    }
400
401    function multiplyentity($match, $state, $pos) {
402        preg_match_all('/\d+/',$match,$matches);
403        $this->_addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos);
404        return true;
405    }
406
407    function singlequoteopening($match, $state, $pos) {
408        $this->_addCall('singlequoteopening',array(), $pos);
409        return true;
410    }
411
412    function singlequoteclosing($match, $state, $pos) {
413        $this->_addCall('singlequoteclosing',array(), $pos);
414        return true;
415    }
416
417    function apostrophe($match, $state, $pos) {
418        $this->_addCall('apostrophe',array(), $pos);
419        return true;
420    }
421
422    function doublequoteopening($match, $state, $pos) {
423        $this->_addCall('doublequoteopening',array(), $pos);
424        $this->status['doublequote']++;
425        return true;
426    }
427
428    function doublequoteclosing($match, $state, $pos) {
429        if ($this->status['doublequote'] <= 0) {
430            $this->doublequoteopening($match, $state, $pos);
431        } else {
432            $this->_addCall('doublequoteclosing',array(), $pos);
433            $this->status['doublequote'] = max(0, --$this->status['doublequote']);
434        }
435        return true;
436    }
437
438    function camelcaselink($match, $state, $pos) {
439        $this->_addCall('camelcaselink',array($match), $pos);
440        return true;
441    }
442
443    /*
444    */
445    function internallink($match, $state, $pos) {
446        // Strip the opening and closing markup
447        $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match);
448
449        // Split title from URL
450        $link = explode('|',$link,2);
451        if ( !isset($link[1]) ) {
452            $link[1] = null;
453        } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) {
454            // If the title is an image, convert it to an array containing the image details
455            $link[1] = Doku_Handler_Parse_Media($link[1]);
456        }
457        $link[0] = trim($link[0]);
458
459        //decide which kind of link it is
460
461        if ( link_isinterwiki($link[0]) ) {
462            // Interwiki
463            $interwiki = explode('>',$link[0],2);
464            $this->_addCall(
465                'interwikilink',
466                array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]),
467                $pos
468                );
469        }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) {
470            // Windows Share
471            $this->_addCall(
472                'windowssharelink',
473                array($link[0],$link[1]),
474                $pos
475                );
476        }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) {
477            // external link (accepts all protocols)
478            $this->_addCall(
479                    'externallink',
480                    array($link[0],$link[1]),
481                    $pos
482                    );
483        }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) {
484            // E-Mail (pattern above is defined in inc/mail.php)
485            $this->_addCall(
486                'emaillink',
487                array($link[0],$link[1]),
488                $pos
489                );
490        }elseif ( preg_match('!^#.+!',$link[0]) ){
491            // local link
492            $this->_addCall(
493                'locallink',
494                array(substr($link[0],1),$link[1]),
495                $pos
496                );
497        }else{
498            // internal link
499            $this->_addCall(
500                'internallink',
501                array($link[0],$link[1]),
502                $pos
503                );
504        }
505
506        return true;
507    }
508
509    function filelink($match, $state, $pos) {
510        $this->_addCall('filelink',array($match, null), $pos);
511        return true;
512    }
513
514    function windowssharelink($match, $state, $pos) {
515        $this->_addCall('windowssharelink',array($match, null), $pos);
516        return true;
517    }
518
519    function media($match, $state, $pos) {
520        $p = Doku_Handler_Parse_Media($match);
521
522        $this->_addCall(
523              $p['type'],
524              array($p['src'], $p['title'], $p['align'], $p['width'],
525                     $p['height'], $p['cache'], $p['linking']),
526              $pos
527             );
528        return true;
529    }
530
531    function rss($match, $state, $pos) {
532        $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match);
533
534        // get params
535        list($link,$params) = explode(' ',$link,2);
536
537        $p = array();
538        if(preg_match('/\b(\d+)\b/',$params,$match)){
539            $p['max'] = $match[1];
540        }else{
541            $p['max'] = 8;
542        }
543        $p['reverse'] = (preg_match('/rev/',$params));
544        $p['author']  = (preg_match('/\b(by|author)/',$params));
545        $p['date']    = (preg_match('/\b(date)/',$params));
546        $p['details'] = (preg_match('/\b(desc|detail)/',$params));
547        $p['nosort']  = (preg_match('/\b(nosort)\b/',$params));
548
549        if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) {
550            $period = array('d' => 86400, 'h' => 3600, 'm' => 60);
551            $p['refresh'] = max(600,$match[1]*$period[$match[2]]);  // n * period in seconds, minimum 10 minutes
552        } else {
553            $p['refresh'] = 14400;   // default to 4 hours
554        }
555
556        $this->_addCall('rss',array($link,$p),$pos);
557        return true;
558    }
559
560    function externallink($match, $state, $pos) {
561        $url   = $match;
562        $title = null;
563
564        // add protocol on simple short URLs
565        if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){
566            $title = $url;
567            $url   = 'ftp://'.$url;
568        }
569        if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){
570            $title = $url;
571            $url = 'http://'.$url;
572        }
573
574        $this->_addCall('externallink',array($url, $title), $pos);
575        return true;
576    }
577
578    function emaillink($match, $state, $pos) {
579        $email = preg_replace(array('/^</','/>$/'),'',$match);
580        $this->_addCall('emaillink',array($email, null), $pos);
581        return true;
582    }
583
584    function table($match, $state, $pos) {
585        switch ( $state ) {
586
587            case DOKU_LEXER_ENTER:
588
589                $ReWriter = new Doku_Handler_Table($this->CallWriter);
590                $this->CallWriter = & $ReWriter;
591
592                $this->_addCall('table_start', array($pos + 1), $pos);
593                if ( trim($match) == '^' ) {
594                    $this->_addCall('tableheader', array(), $pos);
595                } else {
596                    $this->_addCall('tablecell', array(), $pos);
597                }
598            break;
599
600            case DOKU_LEXER_EXIT:
601                $this->_addCall('table_end', array($pos), $pos);
602                $this->CallWriter->process();
603                $ReWriter = & $this->CallWriter;
604                $this->CallWriter = & $ReWriter->CallWriter;
605            break;
606
607            case DOKU_LEXER_UNMATCHED:
608                if ( trim($match) != '' ) {
609                    $this->_addCall('cdata',array($match), $pos);
610                }
611            break;
612
613            case DOKU_LEXER_MATCHED:
614                if ( $match == ' ' ){
615                    $this->_addCall('cdata', array($match), $pos);
616                } else if ( preg_match('/:::/',$match) ) {
617                    $this->_addCall('rowspan', array($match), $pos);
618                } else if ( preg_match('/\t+/',$match) ) {
619                    $this->_addCall('table_align', array($match), $pos);
620                } else if ( preg_match('/ {2,}/',$match) ) {
621                    $this->_addCall('table_align', array($match), $pos);
622                } else if ( $match == "\n|" ) {
623                    $this->_addCall('table_row', array(), $pos);
624                    $this->_addCall('tablecell', array(), $pos);
625                } else if ( $match == "\n^" ) {
626                    $this->_addCall('table_row', array(), $pos);
627                    $this->_addCall('tableheader', array(), $pos);
628                } else if ( $match == '|' ) {
629                    $this->_addCall('tablecell', array(), $pos);
630                } else if ( $match == '^' ) {
631                    $this->_addCall('tableheader', array(), $pos);
632                }
633            break;
634        }
635        return true;
636    }
637}
638
639//------------------------------------------------------------------------
640function Doku_Handler_Parse_Media($match) {
641
642    // Strip the opening and closing markup
643    $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match);
644
645    // Split title from URL
646    $link = explode('|',$link,2);
647
648    // Check alignment
649    $ralign = (bool)preg_match('/^ /',$link[0]);
650    $lalign = (bool)preg_match('/ $/',$link[0]);
651
652    // Logic = what's that ;)...
653    if ( $lalign & $ralign ) {
654        $align = 'center';
655    } else if ( $ralign ) {
656        $align = 'right';
657    } else if ( $lalign ) {
658        $align = 'left';
659    } else {
660        $align = null;
661    }
662
663    // The title...
664    if ( !isset($link[1]) ) {
665        $link[1] = null;
666    }
667
668    //remove aligning spaces
669    $link[0] = trim($link[0]);
670
671    //split into src and parameters (using the very last questionmark)
672    $pos = strrpos($link[0], '?');
673    if($pos !== false){
674        $src   = substr($link[0],0,$pos);
675        $param = substr($link[0],$pos+1);
676    }else{
677        $src   = $link[0];
678        $param = '';
679    }
680
681    //parse width and height
682    if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){
683        !empty($size[1]) ? $w = $size[1] : $w = null;
684        !empty($size[3]) ? $h = $size[3] : $h = null;
685    } else {
686        $w = null;
687        $h = null;
688    }
689
690    //get linking command
691    if(preg_match('/nolink/i',$param)){
692        $linking = 'nolink';
693    }else if(preg_match('/direct/i',$param)){
694        $linking = 'direct';
695    }else if(preg_match('/linkonly/i',$param)){
696        $linking = 'linkonly';
697    }else{
698        $linking = 'details';
699    }
700
701    //get caching command
702    if (preg_match('/(nocache|recache)/i',$param,$cachemode)){
703        $cache = $cachemode[1];
704    }else{
705        $cache = 'cache';
706    }
707
708    // Check whether this is a local or remote image or interwiki
709    if (media_isexternal($src) || link_isinterwiki($src)){
710        $call = 'externalmedia';
711    } else {
712        $call = 'internalmedia';
713    }
714
715    $params = array(
716        'type'=>$call,
717        'src'=>$src,
718        'title'=>$link[1],
719        'align'=>$align,
720        'width'=>$w,
721        'height'=>$h,
722        'cache'=>$cache,
723        'linking'=>$linking,
724    );
725
726    return $params;
727}
728
729//------------------------------------------------------------------------
730interface Doku_Handler_CallWriter_Interface {
731    public function writeCall($call);
732    public function writeCalls($calls);
733    public function finalise();
734}
735
736class Doku_Handler_CallWriter implements Doku_Handler_CallWriter_Interface {
737
738    var $Handler;
739
740    /**
741     * @param Doku_Handler $Handler
742     */
743    function __construct(Doku_Handler $Handler) {
744        $this->Handler = $Handler;
745    }
746
747    function writeCall($call) {
748        $this->Handler->calls[] = $call;
749    }
750
751    function writeCalls($calls) {
752        $this->Handler->calls = array_merge($this->Handler->calls, $calls);
753    }
754
755    // function is required, but since this call writer is first/highest in
756    // the chain it is not required to do anything
757    function finalise() {
758        unset($this->Handler);
759    }
760}
761
762//------------------------------------------------------------------------
763/**
764 * Generic call writer class to handle nesting of rendering instructions
765 * within a render instruction. Also see nest() method of renderer base class
766 *
767 * @author    Chris Smith <chris@jalakai.co.uk>
768 */
769class Doku_Handler_Nest implements Doku_Handler_CallWriter_Interface {
770
771    var $CallWriter;
772    var $calls = array();
773
774    var $closingInstruction;
775
776    /**
777     * constructor
778     *
779     * @param  Doku_Handler_CallWriter $CallWriter     the renderers current call writer
780     * @param  string     $close          closing instruction name, this is required to properly terminate the
781     *                                    syntax mode if the document ends without a closing pattern
782     */
783    function __construct(Doku_Handler_CallWriter_Interface $CallWriter, $close="nest_close") {
784        $this->CallWriter = $CallWriter;
785
786        $this->closingInstruction = $close;
787    }
788
789    function writeCall($call) {
790        $this->calls[] = $call;
791    }
792
793    function writeCalls($calls) {
794        $this->calls = array_merge($this->calls, $calls);
795    }
796
797    function finalise() {
798        $last_call = end($this->calls);
799        $this->writeCall(array($this->closingInstruction,array(), $last_call[2]));
800
801        $this->process();
802        $this->CallWriter->finalise();
803        unset($this->CallWriter);
804    }
805
806    function process() {
807        // merge consecutive cdata
808        $unmerged_calls = $this->calls;
809        $this->calls = array();
810
811        foreach ($unmerged_calls as $call) $this->addCall($call);
812
813        $first_call = reset($this->calls);
814        $this->CallWriter->writeCall(array("nest", array($this->calls), $first_call[2]));
815    }
816
817    function addCall($call) {
818        $key = count($this->calls);
819        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
820            $this->calls[$key-1][1][0] .= $call[1][0];
821        } else if ($call[0] == 'eol') {
822            // do nothing (eol shouldn't be allowed, to counter preformatted fix in #1652 & #1699)
823        } else {
824            $this->calls[] = $call;
825        }
826    }
827}
828
829class Doku_Handler_List implements Doku_Handler_CallWriter_Interface {
830
831    var $CallWriter;
832
833    var $calls = array();
834    var $listCalls = array();
835    var $listStack = array();
836
837    const NODE = 1;
838
839    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
840        $this->CallWriter = $CallWriter;
841    }
842
843    function writeCall($call) {
844        $this->calls[] = $call;
845    }
846
847    // Probably not needed but just in case...
848    function writeCalls($calls) {
849        $this->calls = array_merge($this->calls, $calls);
850#        $this->CallWriter->writeCalls($this->calls);
851    }
852
853    function finalise() {
854        $last_call = end($this->calls);
855        $this->writeCall(array('list_close',array(), $last_call[2]));
856
857        $this->process();
858        $this->CallWriter->finalise();
859        unset($this->CallWriter);
860    }
861
862    //------------------------------------------------------------------------
863    function process() {
864
865        foreach ( $this->calls as $call ) {
866            switch ($call[0]) {
867                case 'list_item':
868                    $this->listOpen($call);
869                break;
870                case 'list_open':
871                    $this->listStart($call);
872                break;
873                case 'list_close':
874                    $this->listEnd($call);
875                break;
876                default:
877                    $this->listContent($call);
878                break;
879            }
880        }
881
882        $this->CallWriter->writeCalls($this->listCalls);
883    }
884
885    //------------------------------------------------------------------------
886    function listStart($call) {
887        $depth = $this->interpretSyntax($call[1][0], $listType);
888
889        $this->initialDepth = $depth;
890        //                   array(list type, current depth, index of current listitem_open)
891        $this->listStack[] = array($listType, $depth, 1);
892
893        $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]);
894        $this->listCalls[] = array('listitem_open',array(1),$call[2]);
895        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
896    }
897
898    //------------------------------------------------------------------------
899    function listEnd($call) {
900        $closeContent = true;
901
902        while ( $list = array_pop($this->listStack) ) {
903            if ( $closeContent ) {
904                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
905                $closeContent = false;
906            }
907            $this->listCalls[] = array('listitem_close',array(),$call[2]);
908            $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]);
909        }
910    }
911
912    //------------------------------------------------------------------------
913    function listOpen($call) {
914        $depth = $this->interpretSyntax($call[1][0], $listType);
915        $end = end($this->listStack);
916        $key = key($this->listStack);
917
918        // Not allowed to be shallower than initialDepth
919        if ( $depth < $this->initialDepth ) {
920            $depth = $this->initialDepth;
921        }
922
923        //------------------------------------------------------------------------
924        if ( $depth == $end[1] ) {
925
926            // Just another item in the list...
927            if ( $listType == $end[0] ) {
928                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
929                $this->listCalls[] = array('listitem_close',array(),$call[2]);
930                $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
931                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
932
933                // new list item, update list stack's index into current listitem_open
934                $this->listStack[$key][2] = count($this->listCalls) - 2;
935
936            // Switched list type...
937            } else {
938
939                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
940                $this->listCalls[] = array('listitem_close',array(),$call[2]);
941                $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
942                $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
943                $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
944                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
945
946                array_pop($this->listStack);
947                $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
948            }
949
950        //------------------------------------------------------------------------
951        // Getting deeper...
952        } else if ( $depth > $end[1] ) {
953
954            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
955            $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
956            $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
957            $this->listCalls[] = array('listcontent_open',array(),$call[2]);
958
959            // set the node/leaf state of this item's parent listitem_open to NODE
960            $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE;
961
962            $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
963
964        //------------------------------------------------------------------------
965        // Getting shallower ( $depth < $end[1] )
966        } else {
967            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
968            $this->listCalls[] = array('listitem_close',array(),$call[2]);
969            $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
970
971            // Throw away the end - done
972            array_pop($this->listStack);
973
974            while (1) {
975                $end = end($this->listStack);
976                $key = key($this->listStack);
977
978                if ( $end[1] <= $depth ) {
979
980                    // Normalize depths
981                    $depth = $end[1];
982
983                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
984
985                    if ( $end[0] == $listType ) {
986                        $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
987                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
988
989                        // new list item, update list stack's index into current listitem_open
990                        $this->listStack[$key][2] = count($this->listCalls) - 2;
991
992                    } else {
993                        // Switching list type...
994                        $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
995                        $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
996                        $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
997                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
998
999                        array_pop($this->listStack);
1000                        $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
1001                    }
1002
1003                    break;
1004
1005                // Haven't dropped down far enough yet.... ( $end[1] > $depth )
1006                } else {
1007
1008                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
1009                    $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
1010
1011                    array_pop($this->listStack);
1012
1013                }
1014
1015            }
1016
1017        }
1018    }
1019
1020    //------------------------------------------------------------------------
1021    function listContent($call) {
1022        $this->listCalls[] = $call;
1023    }
1024
1025    //------------------------------------------------------------------------
1026    function interpretSyntax($match, & $type) {
1027        if ( substr($match,-1) == '*' ) {
1028            $type = 'u';
1029        } else {
1030            $type = 'o';
1031        }
1032        // Is the +1 needed? It used to be count(explode(...))
1033        // but I don't think the number is seen outside this handler
1034        return substr_count(str_replace("\t",'  ',$match), '  ') + 1;
1035    }
1036}
1037
1038//------------------------------------------------------------------------
1039class Doku_Handler_Preformatted implements Doku_Handler_CallWriter_Interface {
1040
1041    var $CallWriter;
1042
1043    var $calls = array();
1044    var $pos;
1045    var $text ='';
1046
1047
1048
1049    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1050        $this->CallWriter = $CallWriter;
1051    }
1052
1053    function writeCall($call) {
1054        $this->calls[] = $call;
1055    }
1056
1057    // Probably not needed but just in case...
1058    function writeCalls($calls) {
1059        $this->calls = array_merge($this->calls, $calls);
1060#        $this->CallWriter->writeCalls($this->calls);
1061    }
1062
1063    function finalise() {
1064        $last_call = end($this->calls);
1065        $this->writeCall(array('preformatted_end',array(), $last_call[2]));
1066
1067        $this->process();
1068        $this->CallWriter->finalise();
1069        unset($this->CallWriter);
1070    }
1071
1072    function process() {
1073        foreach ( $this->calls as $call ) {
1074            switch ($call[0]) {
1075                case 'preformatted_start':
1076                    $this->pos = $call[2];
1077                break;
1078                case 'preformatted_newline':
1079                    $this->text .= "\n";
1080                break;
1081                case 'preformatted_content':
1082                    $this->text .= $call[1][0];
1083                break;
1084                case 'preformatted_end':
1085                    if (trim($this->text)) {
1086                        $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos));
1087                    }
1088                    // see FS#1699 & FS#1652, add 'eol' instructions to ensure proper triggering of following p_open
1089                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
1090                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
1091                break;
1092            }
1093        }
1094    }
1095
1096}
1097
1098//------------------------------------------------------------------------
1099class Doku_Handler_Quote implements Doku_Handler_CallWriter_Interface {
1100
1101    var $CallWriter;
1102
1103    var $calls = array();
1104
1105    var $quoteCalls = array();
1106
1107    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1108        $this->CallWriter = $CallWriter;
1109    }
1110
1111    function writeCall($call) {
1112        $this->calls[] = $call;
1113    }
1114
1115    // Probably not needed but just in case...
1116    function writeCalls($calls) {
1117        $this->calls = array_merge($this->calls, $calls);
1118    }
1119
1120    function finalise() {
1121        $last_call = end($this->calls);
1122        $this->writeCall(array('quote_end',array(), $last_call[2]));
1123
1124        $this->process();
1125        $this->CallWriter->finalise();
1126        unset($this->CallWriter);
1127    }
1128
1129    function process() {
1130
1131        $quoteDepth = 1;
1132
1133        foreach ( $this->calls as $call ) {
1134            switch ($call[0]) {
1135
1136                case 'quote_start':
1137
1138                    $this->quoteCalls[] = array('quote_open',array(),$call[2]);
1139
1140                case 'quote_newline':
1141
1142                    $quoteLength = $this->getDepth($call[1][0]);
1143
1144                    if ( $quoteLength > $quoteDepth ) {
1145                        $quoteDiff = $quoteLength - $quoteDepth;
1146                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1147                            $this->quoteCalls[] = array('quote_open',array(),$call[2]);
1148                        }
1149                    } else if ( $quoteLength < $quoteDepth ) {
1150                        $quoteDiff = $quoteDepth - $quoteLength;
1151                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1152                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1153                        }
1154                    } else {
1155                        if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]);
1156                    }
1157
1158                    $quoteDepth = $quoteLength;
1159
1160                break;
1161
1162                case 'quote_end':
1163
1164                    if ( $quoteDepth > 1 ) {
1165                        $quoteDiff = $quoteDepth - 1;
1166                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1167                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1168                        }
1169                    }
1170
1171                    $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1172
1173                    $this->CallWriter->writeCalls($this->quoteCalls);
1174                break;
1175
1176                default:
1177                    $this->quoteCalls[] = $call;
1178                break;
1179            }
1180        }
1181    }
1182
1183    function getDepth($marker) {
1184        preg_match('/>{1,}/', $marker, $matches);
1185        $quoteLength = strlen($matches[0]);
1186        return $quoteLength;
1187    }
1188}
1189
1190//------------------------------------------------------------------------
1191class Doku_Handler_Table implements Doku_Handler_CallWriter_Interface {
1192
1193    var $CallWriter;
1194
1195    var $calls = array();
1196    var $tableCalls = array();
1197    var $maxCols = 0;
1198    var $maxRows = 1;
1199    var $currentCols = 0;
1200    var $firstCell = false;
1201    var $lastCellType = 'tablecell';
1202    var $inTableHead = true;
1203    var $currentRow = array('tableheader' => 0, 'tablecell' => 0);
1204    var $countTableHeadRows = 0;
1205
1206    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1207        $this->CallWriter = $CallWriter;
1208    }
1209
1210    function writeCall($call) {
1211        $this->calls[] = $call;
1212    }
1213
1214    // Probably not needed but just in case...
1215    function writeCalls($calls) {
1216        $this->calls = array_merge($this->calls, $calls);
1217    }
1218
1219    function finalise() {
1220        $last_call = end($this->calls);
1221        $this->writeCall(array('table_end',array(), $last_call[2]));
1222
1223        $this->process();
1224        $this->CallWriter->finalise();
1225        unset($this->CallWriter);
1226    }
1227
1228    //------------------------------------------------------------------------
1229    function process() {
1230        foreach ( $this->calls as $call ) {
1231            switch ( $call[0] ) {
1232                case 'table_start':
1233                    $this->tableStart($call);
1234                break;
1235                case 'table_row':
1236                    $this->tableRowClose($call);
1237                    $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
1238                break;
1239                case 'tableheader':
1240                case 'tablecell':
1241                    $this->tableCell($call);
1242                break;
1243                case 'table_end':
1244                    $this->tableRowClose($call);
1245                    $this->tableEnd($call);
1246                break;
1247                default:
1248                    $this->tableDefault($call);
1249                break;
1250            }
1251        }
1252        $this->CallWriter->writeCalls($this->tableCalls);
1253    }
1254
1255    function tableStart($call) {
1256        $this->tableCalls[] = array('table_open',$call[1],$call[2]);
1257        $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
1258        $this->firstCell = true;
1259    }
1260
1261    function tableEnd($call) {
1262        $this->tableCalls[] = array('table_close',$call[1],$call[2]);
1263        $this->finalizeTable();
1264    }
1265
1266    function tableRowOpen($call) {
1267        $this->tableCalls[] = $call;
1268        $this->currentCols = 0;
1269        $this->firstCell = true;
1270        $this->lastCellType = 'tablecell';
1271        $this->maxRows++;
1272        if ($this->inTableHead) {
1273            $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
1274        }
1275    }
1276
1277    function tableRowClose($call) {
1278        if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
1279            $this->countTableHeadRows++;
1280        }
1281        // Strip off final cell opening and anything after it
1282        while ( $discard = array_pop($this->tableCalls ) ) {
1283
1284            if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
1285                break;
1286            }
1287            if (!empty($this->currentRow[$discard[0]])) {
1288                $this->currentRow[$discard[0]]--;
1289            }
1290        }
1291        $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
1292
1293        if ( $this->currentCols > $this->maxCols ) {
1294            $this->maxCols = $this->currentCols;
1295        }
1296    }
1297
1298    function isTableHeadRow() {
1299        $td = $this->currentRow['tablecell'];
1300        $th = $this->currentRow['tableheader'];
1301
1302        if (!$th || $td > 2) return false;
1303        if (2*$td > $th) return false;
1304
1305        return true;
1306    }
1307
1308    function tableCell($call) {
1309        if ($this->inTableHead) {
1310            $this->currentRow[$call[0]]++;
1311        }
1312        if ( !$this->firstCell ) {
1313
1314            // Increase the span
1315            $lastCall = end($this->tableCalls);
1316
1317            // A cell call which follows an open cell means an empty cell so span
1318            if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) {
1319                 $this->tableCalls[] = array('colspan',array(),$call[2]);
1320
1321            }
1322
1323            $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
1324            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
1325            $this->lastCellType = $call[0];
1326
1327        } else {
1328
1329            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
1330            $this->lastCellType = $call[0];
1331            $this->firstCell = false;
1332
1333        }
1334
1335        $this->currentCols++;
1336    }
1337
1338    function tableDefault($call) {
1339        $this->tableCalls[] = $call;
1340    }
1341
1342    function finalizeTable() {
1343
1344        // Add the max cols and rows to the table opening
1345        if ( $this->tableCalls[0][0] == 'table_open' ) {
1346            // Adjust to num cols not num col delimeters
1347            $this->tableCalls[0][1][] = $this->maxCols - 1;
1348            $this->tableCalls[0][1][] = $this->maxRows;
1349            $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
1350        } else {
1351            trigger_error('First element in table call list is not table_open');
1352        }
1353
1354        $lastRow = 0;
1355        $lastCell = 0;
1356        $cellKey = array();
1357        $toDelete = array();
1358
1359        // if still in tableheader, then there can be no table header
1360        // as all rows can't be within <THEAD>
1361        if ($this->inTableHead) {
1362            $this->inTableHead = false;
1363            $this->countTableHeadRows = 0;
1364        }
1365
1366        // Look for the colspan elements and increment the colspan on the
1367        // previous non-empty opening cell. Once done, delete all the cells
1368        // that contain colspans
1369        for ($key = 0 ; $key < count($this->tableCalls) ; ++$key) {
1370            $call = $this->tableCalls[$key];
1371
1372            switch ($call[0]) {
1373                case 'table_open' :
1374                    if($this->countTableHeadRows) {
1375                        array_splice($this->tableCalls, $key+1, 0, array(
1376                              array('tablethead_open', array(), $call[2]))
1377                        );
1378                    }
1379                    break;
1380
1381                case 'tablerow_open':
1382
1383                    $lastRow++;
1384                    $lastCell = 0;
1385                    break;
1386
1387                case 'tablecell_open':
1388                case 'tableheader_open':
1389
1390                    $lastCell++;
1391                    $cellKey[$lastRow][$lastCell] = $key;
1392                    break;
1393
1394                case 'table_align':
1395
1396                    $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
1397                    $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
1398                    // If the cell is empty, align left
1399                    if ($prev && $next) {
1400                        $this->tableCalls[$key-1][1][1] = 'left';
1401
1402                    // If the previous element was a cell open, align right
1403                    } elseif ($prev) {
1404                        $this->tableCalls[$key-1][1][1] = 'right';
1405
1406                    // If the next element is the close of an element, align either center or left
1407                    } elseif ( $next) {
1408                        if ( $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right' ) {
1409                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
1410                        } else {
1411                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
1412                        }
1413
1414                    }
1415
1416                    // Now convert the whitespace back to cdata
1417                    $this->tableCalls[$key][0] = 'cdata';
1418                    break;
1419
1420                case 'colspan':
1421
1422                    $this->tableCalls[$key-1][1][0] = false;
1423
1424                    for($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
1425
1426                        if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) {
1427
1428                            if ( false !== $this->tableCalls[$i][1][0] ) {
1429                                $this->tableCalls[$i][1][0]++;
1430                                break;
1431                            }
1432
1433                        }
1434                    }
1435
1436                    $toDelete[] = $key-1;
1437                    $toDelete[] = $key;
1438                    $toDelete[] = $key+1;
1439                    break;
1440
1441                case 'rowspan':
1442
1443                    if ( $this->tableCalls[$key-1][0] == 'cdata' ) {
1444                        // ignore rowspan if previous call was cdata (text mixed with :::) we don't have to check next call as that wont match regex
1445                        $this->tableCalls[$key][0] = 'cdata';
1446
1447                    } else {
1448
1449                        $spanning_cell = null;
1450
1451                        // can't cross thead/tbody boundary
1452                        if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
1453                            for($i = $lastRow-1; $i > 0; $i--) {
1454
1455                                if ( $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' || $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open' ) {
1456
1457                                    if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
1458                                        $spanning_cell = $i;
1459                                        break;
1460                                    }
1461
1462                                }
1463                            }
1464                        }
1465                        if (is_null($spanning_cell)) {
1466                            // No spanning cell found, so convert this cell to
1467                            // an empty one to avoid broken tables
1468                            $this->tableCalls[$key][0] = 'cdata';
1469                            $this->tableCalls[$key][1][0] = '';
1470                            continue;
1471                        }
1472                        $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
1473
1474                        $this->tableCalls[$key-1][1][2] = false;
1475
1476                        $toDelete[] = $key-1;
1477                        $toDelete[] = $key;
1478                        $toDelete[] = $key+1;
1479                    }
1480                    break;
1481
1482                case 'tablerow_close':
1483
1484                    // Fix broken tables by adding missing cells
1485                    $moreCalls = array();
1486                    while (++$lastCell < $this->maxCols) {
1487                        $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
1488                        $moreCalls[] = array('cdata', array(''), $call[2]);
1489                        $moreCalls[] = array('tablecell_close', array(), $call[2]);
1490                    }
1491                    $moreCallsLength = count($moreCalls);
1492                    if($moreCallsLength) {
1493                        array_splice($this->tableCalls, $key, 0, $moreCalls);
1494                        $key += $moreCallsLength;
1495                    }
1496
1497                    if($this->countTableHeadRows == $lastRow) {
1498                        array_splice($this->tableCalls, $key+1, 0, array(
1499                              array('tablethead_close', array(), $call[2])));
1500                    }
1501                    break;
1502
1503            }
1504        }
1505
1506        // condense cdata
1507        $cnt = count($this->tableCalls);
1508        for( $key = 0; $key < $cnt; $key++){
1509            if($this->tableCalls[$key][0] == 'cdata'){
1510                $ckey = $key;
1511                $key++;
1512                while($this->tableCalls[$key][0] == 'cdata'){
1513                    $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
1514                    $toDelete[] = $key;
1515                    $key++;
1516                }
1517                continue;
1518            }
1519        }
1520
1521        foreach ( $toDelete as $delete ) {
1522            unset($this->tableCalls[$delete]);
1523        }
1524        $this->tableCalls = array_values($this->tableCalls);
1525    }
1526}
1527
1528
1529/**
1530 * Handler for paragraphs
1531 *
1532 * @author Harry Fuecks <hfuecks@gmail.com>
1533 */
1534class Doku_Handler_Block {
1535    var $calls = array();
1536    var $skipEol = false;
1537    var $inParagraph = false;
1538
1539    // Blocks these should not be inside paragraphs
1540    var $blockOpen = array(
1541            'header',
1542            'listu_open','listo_open','listitem_open','listcontent_open',
1543            'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open',
1544            'quote_open',
1545            'code','file','hr','preformatted','rss',
1546            'htmlblock','phpblock',
1547            'footnote_open',
1548        );
1549
1550    var $blockClose = array(
1551            'header',
1552            'listu_close','listo_close','listitem_close','listcontent_close',
1553            'table_close','tablerow_close','tablecell_close','tableheader_close','tablethead_close',
1554            'quote_close',
1555            'code','file','hr','preformatted','rss',
1556            'htmlblock','phpblock',
1557            'footnote_close',
1558        );
1559
1560    // Stacks can contain paragraphs
1561    var $stackOpen = array(
1562        'section_open',
1563        );
1564
1565    var $stackClose = array(
1566        'section_close',
1567        );
1568
1569
1570    /**
1571     * Constructor. Adds loaded syntax plugins to the block and stack
1572     * arrays
1573     *
1574     * @author Andreas Gohr <andi@splitbrain.org>
1575     */
1576    function __construct(){
1577        global $DOKU_PLUGINS;
1578        //check if syntax plugins were loaded
1579        if(empty($DOKU_PLUGINS['syntax'])) return;
1580        foreach($DOKU_PLUGINS['syntax'] as $n => $p){
1581            $ptype = $p->getPType();
1582            if($ptype == 'block'){
1583                $this->blockOpen[]  = 'plugin_'.$n;
1584                $this->blockClose[] = 'plugin_'.$n;
1585            }elseif($ptype == 'stack'){
1586                $this->stackOpen[]  = 'plugin_'.$n;
1587                $this->stackClose[] = 'plugin_'.$n;
1588            }
1589        }
1590    }
1591
1592    function openParagraph($pos){
1593        if ($this->inParagraph) return;
1594        $this->calls[] = array('p_open',array(), $pos);
1595        $this->inParagraph = true;
1596        $this->skipEol = true;
1597    }
1598
1599    /**
1600     * Close a paragraph if needed
1601     *
1602     * This function makes sure there are no empty paragraphs on the stack
1603     *
1604     * @author Andreas Gohr <andi@splitbrain.org>
1605     *
1606     * @param string|integer $pos
1607     */
1608    function closeParagraph($pos){
1609        if (!$this->inParagraph) return;
1610        // look back if there was any content - we don't want empty paragraphs
1611        $content = '';
1612        $ccount = count($this->calls);
1613        for($i=$ccount-1; $i>=0; $i--){
1614            if($this->calls[$i][0] == 'p_open'){
1615                break;
1616            }elseif($this->calls[$i][0] == 'cdata'){
1617                $content .= $this->calls[$i][1][0];
1618            }else{
1619                $content = 'found markup';
1620                break;
1621            }
1622        }
1623
1624        if(trim($content)==''){
1625            //remove the whole paragraph
1626            //array_splice($this->calls,$i); // <- this is much slower than the loop below
1627            for($x=$ccount; $x>$i; $x--) array_pop($this->calls);
1628        }else{
1629            // remove ending linebreaks in the paragraph
1630            $i=count($this->calls)-1;
1631            if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0],DOKU_PARSER_EOL);
1632            $this->calls[] = array('p_close',array(), $pos);
1633        }
1634
1635        $this->inParagraph = false;
1636        $this->skipEol = true;
1637    }
1638
1639    function addCall($call) {
1640        $key = count($this->calls);
1641        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
1642            $this->calls[$key-1][1][0] .= $call[1][0];
1643        } else {
1644            $this->calls[] = $call;
1645        }
1646    }
1647
1648    // simple version of addCall, without checking cdata
1649    function storeCall($call) {
1650        $this->calls[] = $call;
1651    }
1652
1653    /**
1654     * Processes the whole instruction stack to open and close paragraphs
1655     *
1656     * @author Harry Fuecks <hfuecks@gmail.com>
1657     * @author Andreas Gohr <andi@splitbrain.org>
1658     *
1659     * @param array $calls
1660     *
1661     * @return array
1662     */
1663    function process($calls) {
1664        // open first paragraph
1665        $this->openParagraph(0);
1666        foreach ( $calls as $key => $call ) {
1667            $cname = $call[0];
1668            if ($cname == 'plugin') {
1669                $cname='plugin_'.$call[1][0];
1670                $plugin = true;
1671                $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL));
1672                $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL));
1673            } else {
1674                $plugin = false;
1675            }
1676            /* stack */
1677            if ( in_array($cname,$this->stackClose ) && (!$plugin || $plugin_close)) {
1678                $this->closeParagraph($call[2]);
1679                $this->storeCall($call);
1680                $this->openParagraph($call[2]);
1681                continue;
1682            }
1683            if ( in_array($cname,$this->stackOpen ) && (!$plugin || $plugin_open) ) {
1684                $this->closeParagraph($call[2]);
1685                $this->storeCall($call);
1686                $this->openParagraph($call[2]);
1687                continue;
1688            }
1689            /* block */
1690            // If it's a substition it opens and closes at the same call.
1691            // To make sure next paragraph is correctly started, let close go first.
1692            if ( in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) {
1693                $this->closeParagraph($call[2]);
1694                $this->storeCall($call);
1695                $this->openParagraph($call[2]);
1696                continue;
1697            }
1698            if ( in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) {
1699                $this->closeParagraph($call[2]);
1700                $this->storeCall($call);
1701                continue;
1702            }
1703            /* eol */
1704            if ( $cname == 'eol' ) {
1705                // Check this isn't an eol instruction to skip...
1706                if ( !$this->skipEol ) {
1707                    // Next is EOL => double eol => mark as paragraph
1708                    if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) {
1709                        $this->closeParagraph($call[2]);
1710                        $this->openParagraph($call[2]);
1711                    } else {
1712                        //if this is just a single eol make a space from it
1713                        $this->addCall(array('cdata',array(DOKU_PARSER_EOL), $call[2]));
1714                    }
1715                }
1716                continue;
1717            }
1718            /* normal */
1719            $this->addCall($call);
1720            $this->skipEol = false;
1721        }
1722        // close last paragraph
1723        $call = end($this->calls);
1724        $this->closeParagraph($call[2]);
1725        return $this->calls;
1726    }
1727}
1728
1729//Setup VIM: ex: et ts=4 :
1730