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