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