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