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