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