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