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