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