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