Failed Conditions
Push — psr2 ( 64159a )
by Andreas
07:54 queued 04:15
created

inc/parser/handler.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
if (!defined('DOKU_PARSER_EOL')) define('DOKU_PARSER_EOL',"\n");   // add this to make handling test cases simpler
3
4
class 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
        'doublequote' => 0,
15
    );
16
17
    var $rewriteBlocks = true;
18
19
    function __construct() {
20
        $this->CallWriter = new Doku_Handler_CallWriter($this);
21
    }
22
23
    /**
24
     * @param string $handler
25
     * @param mixed $args
26
     * @param integer|string $pos
27
     */
28
    function _addCall($handler, $args, $pos) {
29
        $call = array($handler,$args, $pos);
30
        $this->CallWriter->writeCall($call);
31
    }
32
33
    function addPluginCall($plugin, $args, $state, $pos, $match) {
34
        $call = array('plugin',array($plugin, $args, $state, $match), $pos);
35
        $this->CallWriter->writeCall($call);
36
    }
37
38
    function _finalize(){
39
40
        $this->CallWriter->finalise();
41
42
        if ( $this->status['section'] ) {
43
            $last_call = end($this->calls);
44
            array_push($this->calls,array('section_close',array(), $last_call[2]));
45
        }
46
47
        if ( $this->rewriteBlocks ) {
48
            $B = new Doku_Handler_Block();
49
            $this->calls = $B->process($this->calls);
50
        }
51
52
        trigger_event('PARSER_HANDLER_DONE',$this);
53
54
        array_unshift($this->calls,array('document_start',array(),0));
55
        $last_call = end($this->calls);
56
        array_push($this->calls,array('document_end',array(),$last_call[2]));
57
    }
58
59
    /**
60
     * fetch the current call and advance the pointer to the next one
61
     *
62
     * @return bool|mixed
63
     */
64
    function fetch() {
65
        $call = current($this->calls);
66
        if($call !== false) {
67
            next($this->calls); //advance the pointer
68
            return $call;
69
        }
70
        return false;
71
    }
72
73
74
    /**
75
     * Special plugin handler
76
     *
77
     * This handler is called for all modes starting with 'plugin_'.
78
     * An additional parameter with the plugin name is passed
79
     *
80
     * @author Andreas Gohr <[email protected]>
81
     *
82
     * @param string|integer $match
83
     * @param string|integer $state
84
     * @param integer $pos
85
     * @param $pluginname
86
     *
87
     * @return bool
88
     */
89
    function plugin($match, $state, $pos, $pluginname){
90
        $data = array($match);
91
        /** @var DokuWiki_Syntax_Plugin $plugin */
92
        $plugin = plugin_load('syntax',$pluginname);
93
        if($plugin != null){
94
            $data = $plugin->handle($match, $state, $pos, $this);
95
        }
96
        if ($data !== false) {
97
            $this->addPluginCall($pluginname,$data,$state,$pos,$match);
98
        }
99
        return true;
100
    }
101
102
    function base($match, $state, $pos) {
103
        switch ( $state ) {
104
            case DOKU_LEXER_UNMATCHED:
105
                $this->_addCall('cdata',array($match), $pos);
106
                return true;
107
            break;
108
        }
109
    }
110
111
    function header($match, $state, $pos) {
112
        // get level and title
113
        $title = trim($match);
114
        $level = 7 - strspn($title,'=');
115
        if($level < 1) $level = 1;
116
        $title = trim($title,'=');
117
        $title = trim($title);
118
119
        if ($this->status['section']) $this->_addCall('section_close',array(),$pos);
120
121
        $this->_addCall('header',array($title,$level,$pos), $pos);
122
123
        $this->_addCall('section_open',array($level),$pos);
124
        $this->status['section'] = true;
125
        return true;
126
    }
127
128
    function notoc($match, $state, $pos) {
129
        $this->_addCall('notoc',array(),$pos);
130
        return true;
131
    }
132
133
    function nocache($match, $state, $pos) {
134
        $this->_addCall('nocache',array(),$pos);
135
        return true;
136
    }
137
138
    function linebreak($match, $state, $pos) {
139
        $this->_addCall('linebreak',array(),$pos);
140
        return true;
141
    }
142
143
    function eol($match, $state, $pos) {
144
        $this->_addCall('eol',array(),$pos);
145
        return true;
146
    }
147
148
    function hr($match, $state, $pos) {
0 ignored issues
show
The parameter $match is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $state is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
149
        $this->_addCall('hr',array(),$pos);
150
        return true;
151
    }
152
153
    /**
154
     * @param string|integer $match
155
     * @param string|integer $state
156
     * @param integer $pos
157
     * @param string $name
158
     */
159
    function _nestingTag($match, $state, $pos, $name) {
160
        switch ( $state ) {
161
            case DOKU_LEXER_ENTER:
162
                $this->_addCall($name.'_open', array(), $pos);
163
            break;
164
            case DOKU_LEXER_EXIT:
165
                $this->_addCall($name.'_close', array(), $pos);
166
            break;
167
            case DOKU_LEXER_UNMATCHED:
168
                $this->_addCall('cdata',array($match), $pos);
169
            break;
170
        }
171
    }
172
173
    function strong($match, $state, $pos) {
174
        $this->_nestingTag($match, $state, $pos, 'strong');
175
        return true;
176
    }
177
178
    function emphasis($match, $state, $pos) {
179
        $this->_nestingTag($match, $state, $pos, 'emphasis');
180
        return true;
181
    }
182
183
    function underline($match, $state, $pos) {
184
        $this->_nestingTag($match, $state, $pos, 'underline');
185
        return true;
186
    }
187
188
    function monospace($match, $state, $pos) {
189
        $this->_nestingTag($match, $state, $pos, 'monospace');
190
        return true;
191
    }
192
193
    function subscript($match, $state, $pos) {
194
        $this->_nestingTag($match, $state, $pos, 'subscript');
195
        return true;
196
    }
197
198
    function superscript($match, $state, $pos) {
199
        $this->_nestingTag($match, $state, $pos, 'superscript');
200
        return true;
201
    }
202
203
    function deleted($match, $state, $pos) {
204
        $this->_nestingTag($match, $state, $pos, 'deleted');
205
        return true;
206
    }
207
208
209
    function footnote($match, $state, $pos) {
210
//        $this->_nestingTag($match, $state, $pos, 'footnote');
211
        if (!isset($this->_footnote)) $this->_footnote = false;
212
213
        switch ( $state ) {
214
            case DOKU_LEXER_ENTER:
215
                // footnotes can not be nested - however due to limitations in lexer it can't be prevented
216
                // we will still enter a new footnote mode, we just do nothing
217
                if ($this->_footnote) {
218
                    $this->_addCall('cdata',array($match), $pos);
219
                    break;
220
                }
221
222
                $this->_footnote = true;
223
224
                $ReWriter = new Doku_Handler_Nest($this->CallWriter,'footnote_close');
225
                $this->CallWriter = & $ReWriter;
226
                $this->_addCall('footnote_open', array(), $pos);
227
            break;
228
            case DOKU_LEXER_EXIT:
229
                // check whether we have already exitted the footnote mode, can happen if the modes were nested
230
                if (!$this->_footnote) {
231
                    $this->_addCall('cdata',array($match), $pos);
232
                    break;
233
                }
234
235
                $this->_footnote = false;
236
237
                $this->_addCall('footnote_close', array(), $pos);
238
                $this->CallWriter->process();
239
                $ReWriter = & $this->CallWriter;
240
                $this->CallWriter = & $ReWriter->CallWriter;
241
            break;
242
            case DOKU_LEXER_UNMATCHED:
243
                $this->_addCall('cdata', array($match), $pos);
244
            break;
245
        }
246
        return true;
247
    }
248
249
    function listblock($match, $state, $pos) {
250
        switch ( $state ) {
251
            case DOKU_LEXER_ENTER:
252
                $ReWriter = new Doku_Handler_List($this->CallWriter);
253
                $this->CallWriter = & $ReWriter;
254
                $this->_addCall('list_open', array($match), $pos);
255
            break;
256
            case DOKU_LEXER_EXIT:
257
                $this->_addCall('list_close', array(), $pos);
258
                $this->CallWriter->process();
259
                $ReWriter = & $this->CallWriter;
260
                $this->CallWriter = & $ReWriter->CallWriter;
261
            break;
262
            case DOKU_LEXER_MATCHED:
263
                $this->_addCall('list_item', array($match), $pos);
264
            break;
265
            case DOKU_LEXER_UNMATCHED:
266
                $this->_addCall('cdata', array($match), $pos);
267
            break;
268
        }
269
        return true;
270
    }
271
272
    function unformatted($match, $state, $pos) {
273
        if ( $state == DOKU_LEXER_UNMATCHED ) {
274
            $this->_addCall('unformatted',array($match), $pos);
275
        }
276
        return true;
277
    }
278
279
    function php($match, $state, $pos) {
280
        global $conf;
281
        if ( $state == DOKU_LEXER_UNMATCHED ) {
282
            $this->_addCall('php',array($match), $pos);
283
        }
284
        return true;
285
    }
286
287
    function phpblock($match, $state, $pos) {
288
        global $conf;
289
        if ( $state == DOKU_LEXER_UNMATCHED ) {
290
            $this->_addCall('phpblock',array($match), $pos);
291
        }
292
        return true;
293
    }
294
295
    function html($match, $state, $pos) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
296
        global $conf;
297
        if ( $state == DOKU_LEXER_UNMATCHED ) {
298
            $this->_addCall('html',array($match), $pos);
299
        }
300
        return true;
301
    }
302
303
    function htmlblock($match, $state, $pos) {
304
        global $conf;
305
        if ( $state == DOKU_LEXER_UNMATCHED ) {
306
            $this->_addCall('htmlblock',array($match), $pos);
307
        }
308
        return true;
309
    }
310
311
    function preformatted($match, $state, $pos) {
312
        switch ( $state ) {
313
            case DOKU_LEXER_ENTER:
314
                $ReWriter = new Doku_Handler_Preformatted($this->CallWriter);
315
                $this->CallWriter = $ReWriter;
316
                $this->_addCall('preformatted_start',array(), $pos);
317
            break;
318
            case DOKU_LEXER_EXIT:
319
                $this->_addCall('preformatted_end',array(), $pos);
320
                $this->CallWriter->process();
321
                $ReWriter = & $this->CallWriter;
322
                $this->CallWriter = & $ReWriter->CallWriter;
323
            break;
324
            case DOKU_LEXER_MATCHED:
325
                $this->_addCall('preformatted_newline',array(), $pos);
326
            break;
327
            case DOKU_LEXER_UNMATCHED:
328
                $this->_addCall('preformatted_content',array($match), $pos);
329
            break;
330
        }
331
332
        return true;
333
    }
334
335
    function quote($match, $state, $pos) {
336
337
        switch ( $state ) {
338
339
            case DOKU_LEXER_ENTER:
340
                $ReWriter = new Doku_Handler_Quote($this->CallWriter);
341
                $this->CallWriter = & $ReWriter;
342
                $this->_addCall('quote_start',array($match), $pos);
343
            break;
344
345
            case DOKU_LEXER_EXIT:
346
                $this->_addCall('quote_end',array(), $pos);
347
                $this->CallWriter->process();
348
                $ReWriter = & $this->CallWriter;
349
                $this->CallWriter = & $ReWriter->CallWriter;
350
            break;
351
352
            case DOKU_LEXER_MATCHED:
353
                $this->_addCall('quote_newline',array($match), $pos);
354
            break;
355
356
            case DOKU_LEXER_UNMATCHED:
357
                $this->_addCall('cdata',array($match), $pos);
358
            break;
359
360
        }
361
362
        return true;
363
    }
364
365
    /**
366
     * Internal function for parsing highlight options.
367
     * $options is parsed for key value pairs separated by commas.
368
     * A value might also be missing in which case the value will simple
369
     * be set to true. Commas in strings are ignored, e.g. option="4,56"
370
     * will work as expected and will only create one entry.
371
     *
372
     * @param string $options space separated list of key-value pairs,
373
     *                        e.g. option1=123, option2="456"
374
     * @return array|null     Array of key-value pairs $array['key'] = 'value';
375
     *                        or null if no entries found
376
     */
377
    protected function parse_highlight_options ($options) {
378
        $result = array();
379
        preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
380
        foreach ($matches as $match) {
381
            $equal_sign = strpos($match [0], '=');
382
            if ($equal_sign === false) {
383
                $key = trim($match[0]);
384
                $result [$key] = 1;
385
            } else {
386
                $key = substr($match[0], 0, $equal_sign);
387
                $value = substr($match[0], $equal_sign+1);
388
                $value = trim($value, '"');
389
                if (strlen($value) > 0) {
390
                    $result [$key] = $value;
391
                } else {
392
                    $result [$key] = 1;
393
                }
394
            }
395
        }
396
397
        // Check for supported options
398
        $result = array_intersect_key(
399
            $result,
400
            array_flip(array(
401
                'enable_line_numbers',
402
                'start_line_numbers_at',
403
                'highlight_lines_extra',
404
                'enable_keyword_links')
405
            )
406
        );
407
408
        // Sanitize values
409
        if(isset($result['enable_line_numbers'])) {
410
            if($result['enable_line_numbers'] === 'false') {
411
                $result['enable_line_numbers'] = false;
412
            }
413
            $result['enable_line_numbers'] = (bool) $result['enable_line_numbers'];
414
        }
415
        if(isset($result['highlight_lines_extra'])) {
416
            $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
417
            $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
418
            $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
419
        }
420
        if(isset($result['start_line_numbers_at'])) {
421
            $result['start_line_numbers_at'] = (int) $result['start_line_numbers_at'];
422
        }
423
        if(isset($result['enable_keyword_links'])) {
424
            if($result['enable_keyword_links'] === 'false') {
425
                $result['enable_keyword_links'] = false;
426
            }
427
            $result['enable_keyword_links'] = (bool) $result['enable_keyword_links'];
428
        }
429
        if (count($result) == 0) {
430
            return null;
431
        }
432
433
        return $result;
434
    }
435
436
    function file($match, $state, $pos) {
437
        return $this->code($match, $state, $pos, 'file');
438
    }
439
440
    function code($match, $state, $pos, $type='code') {
441
        if ( $state == DOKU_LEXER_UNMATCHED ) {
442
            $matches = explode('>',$match,2);
443
            // Cut out variable options enclosed in []
444
            preg_match('/\[.*\]/', $matches[0], $options);
445
            if (!empty($options[0])) {
446
                $matches[0] = str_replace($options[0], '', $matches[0]);
447
            }
448
            $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
449
            while(count($param) < 2) array_push($param, null);
450
            // We shortcut html here.
451
            if ($param[0] == 'html') $param[0] = 'html4strict';
452
            if ($param[0] == '-') $param[0] = null;
453
            array_unshift($param, $matches[1]);
454
            if (!empty($options[0])) {
455
                $param [] = $this->parse_highlight_options ($options[0]);
456
            }
457
            $this->_addCall($type, $param, $pos);
458
        }
459
        return true;
460
    }
461
462
    function acronym($match, $state, $pos) {
463
        $this->_addCall('acronym',array($match), $pos);
464
        return true;
465
    }
466
467
    function smiley($match, $state, $pos) {
468
        $this->_addCall('smiley',array($match), $pos);
469
        return true;
470
    }
471
472
    function wordblock($match, $state, $pos) {
473
        $this->_addCall('wordblock',array($match), $pos);
474
        return true;
475
    }
476
477
    function entity($match, $state, $pos) {
478
        $this->_addCall('entity',array($match), $pos);
479
        return true;
480
    }
481
482
    function multiplyentity($match, $state, $pos) {
483
        preg_match_all('/\d+/',$match,$matches);
484
        $this->_addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos);
485
        return true;
486
    }
487
488
    function singlequoteopening($match, $state, $pos) {
489
        $this->_addCall('singlequoteopening',array(), $pos);
490
        return true;
491
    }
492
493
    function singlequoteclosing($match, $state, $pos) {
494
        $this->_addCall('singlequoteclosing',array(), $pos);
495
        return true;
496
    }
497
498
    function apostrophe($match, $state, $pos) {
499
        $this->_addCall('apostrophe',array(), $pos);
500
        return true;
501
    }
502
503
    function doublequoteopening($match, $state, $pos) {
504
        $this->_addCall('doublequoteopening',array(), $pos);
505
        $this->status['doublequote']++;
506
        return true;
507
    }
508
509
    function doublequoteclosing($match, $state, $pos) {
510
        if ($this->status['doublequote'] <= 0) {
511
            $this->doublequoteopening($match, $state, $pos);
512
        } else {
513
            $this->_addCall('doublequoteclosing',array(), $pos);
514
            $this->status['doublequote'] = max(0, --$this->status['doublequote']);
515
        }
516
        return true;
517
    }
518
519
    function camelcaselink($match, $state, $pos) {
520
        $this->_addCall('camelcaselink',array($match), $pos);
521
        return true;
522
    }
523
524
    /*
525
    */
526
    function internallink($match, $state, $pos) {
527
        // Strip the opening and closing markup
528
        $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match);
529
530
        // Split title from URL
531
        $link = explode('|',$link,2);
532
        if ( !isset($link[1]) ) {
533
            $link[1] = null;
534
        } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) {
535
            // If the title is an image, convert it to an array containing the image details
536
            $link[1] = Doku_Handler_Parse_Media($link[1]);
537
        }
538
        $link[0] = trim($link[0]);
539
540
        //decide which kind of link it is
541
542
        if ( link_isinterwiki($link[0]) ) {
543
            // Interwiki
544
            $interwiki = explode('>',$link[0],2);
545
            $this->_addCall(
546
                'interwikilink',
547
                array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]),
548
                $pos
549
                );
550
        }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) {
551
            // Windows Share
552
            $this->_addCall(
553
                'windowssharelink',
554
                array($link[0],$link[1]),
555
                $pos
556
                );
557
        }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) {
558
            // external link (accepts all protocols)
559
            $this->_addCall(
560
                    'externallink',
561
                    array($link[0],$link[1]),
562
                    $pos
563
                    );
564
        }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) {
565
            // E-Mail (pattern above is defined in inc/mail.php)
566
            $this->_addCall(
567
                'emaillink',
568
                array($link[0],$link[1]),
569
                $pos
570
                );
571
        }elseif ( preg_match('!^#.+!',$link[0]) ){
572
            // local link
573
            $this->_addCall(
574
                'locallink',
575
                array(substr($link[0],1),$link[1]),
576
                $pos
577
                );
578
        }else{
579
            // internal link
580
            $this->_addCall(
581
                'internallink',
582
                array($link[0],$link[1]),
583
                $pos
584
                );
585
        }
586
587
        return true;
588
    }
589
590
    function filelink($match, $state, $pos) {
591
        $this->_addCall('filelink',array($match, null), $pos);
592
        return true;
593
    }
594
595
    function windowssharelink($match, $state, $pos) {
596
        $this->_addCall('windowssharelink',array($match, null), $pos);
597
        return true;
598
    }
599
600
    function media($match, $state, $pos) {
601
        $p = Doku_Handler_Parse_Media($match);
602
603
        $this->_addCall(
604
              $p['type'],
605
              array($p['src'], $p['title'], $p['align'], $p['width'],
606
                     $p['height'], $p['cache'], $p['linking']),
607
              $pos
608
             );
609
        return true;
610
    }
611
612
    function rss($match, $state, $pos) {
613
        $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match);
614
615
        // get params
616
        list($link,$params) = explode(' ',$link,2);
617
618
        $p = array();
619
        if(preg_match('/\b(\d+)\b/',$params,$match)){
620
            $p['max'] = $match[1];
621
        }else{
622
            $p['max'] = 8;
623
        }
624
        $p['reverse'] = (preg_match('/rev/',$params));
625
        $p['author']  = (preg_match('/\b(by|author)/',$params));
626
        $p['date']    = (preg_match('/\b(date)/',$params));
627
        $p['details'] = (preg_match('/\b(desc|detail)/',$params));
628
        $p['nosort']  = (preg_match('/\b(nosort)\b/',$params));
629
630
        if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) {
631
            $period = array('d' => 86400, 'h' => 3600, 'm' => 60);
632
            $p['refresh'] = max(600,$match[1]*$period[$match[2]]);  // n * period in seconds, minimum 10 minutes
633
        } else {
634
            $p['refresh'] = 14400;   // default to 4 hours
635
        }
636
637
        $this->_addCall('rss',array($link,$p),$pos);
638
        return true;
639
    }
640
641
    function externallink($match, $state, $pos) {
642
        $url   = $match;
643
        $title = null;
644
645
        // add protocol on simple short URLs
646
        if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){
647
            $title = $url;
648
            $url   = 'ftp://'.$url;
649
        }
650
        if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){
651
            $title = $url;
652
            $url = 'http://'.$url;
653
        }
654
655
        $this->_addCall('externallink',array($url, $title), $pos);
656
        return true;
657
    }
658
659
    function emaillink($match, $state, $pos) {
660
        $email = preg_replace(array('/^</','/>$/'),'',$match);
661
        $this->_addCall('emaillink',array($email, null), $pos);
662
        return true;
663
    }
664
665
    function table($match, $state, $pos) {
666
        switch ( $state ) {
667
668
            case DOKU_LEXER_ENTER:
669
670
                $ReWriter = new Doku_Handler_Table($this->CallWriter);
671
                $this->CallWriter = & $ReWriter;
672
673
                $this->_addCall('table_start', array($pos + 1), $pos);
674
                if ( trim($match) == '^' ) {
675
                    $this->_addCall('tableheader', array(), $pos);
676
                } else {
677
                    $this->_addCall('tablecell', array(), $pos);
678
                }
679
            break;
680
681
            case DOKU_LEXER_EXIT:
682
                $this->_addCall('table_end', array($pos), $pos);
683
                $this->CallWriter->process();
684
                $ReWriter = & $this->CallWriter;
685
                $this->CallWriter = & $ReWriter->CallWriter;
686
            break;
687
688
            case DOKU_LEXER_UNMATCHED:
689
                if ( trim($match) != '' ) {
690
                    $this->_addCall('cdata',array($match), $pos);
691
                }
692
            break;
693
694
            case DOKU_LEXER_MATCHED:
695
                if ( $match == ' ' ){
696
                    $this->_addCall('cdata', array($match), $pos);
697
                } else if ( preg_match('/:::/',$match) ) {
698
                    $this->_addCall('rowspan', array($match), $pos);
699
                } else if ( preg_match('/\t+/',$match) ) {
700
                    $this->_addCall('table_align', array($match), $pos);
701
                } else if ( preg_match('/ {2,}/',$match) ) {
702
                    $this->_addCall('table_align', array($match), $pos);
703
                } else if ( $match == "\n|" ) {
704
                    $this->_addCall('table_row', array(), $pos);
705
                    $this->_addCall('tablecell', array(), $pos);
706
                } else if ( $match == "\n^" ) {
707
                    $this->_addCall('table_row', array(), $pos);
708
                    $this->_addCall('tableheader', array(), $pos);
709
                } else if ( $match == '|' ) {
710
                    $this->_addCall('tablecell', array(), $pos);
711
                } else if ( $match == '^' ) {
712
                    $this->_addCall('tableheader', array(), $pos);
713
                }
714
            break;
715
        }
716
        return true;
717
    }
718
}
719
720
//------------------------------------------------------------------------
721
function Doku_Handler_Parse_Media($match) {
722
723
    // Strip the opening and closing markup
724
    $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match);
725
726
    // Split title from URL
727
    $link = explode('|',$link,2);
728
729
    // Check alignment
730
    $ralign = (bool)preg_match('/^ /',$link[0]);
731
    $lalign = (bool)preg_match('/ $/',$link[0]);
732
733
    // Logic = what's that ;)...
734
    if ( $lalign & $ralign ) {
735
        $align = 'center';
736
    } else if ( $ralign ) {
737
        $align = 'right';
738
    } else if ( $lalign ) {
739
        $align = 'left';
740
    } else {
741
        $align = null;
742
    }
743
744
    // The title...
745
    if ( !isset($link[1]) ) {
746
        $link[1] = null;
747
    }
748
749
    //remove aligning spaces
750
    $link[0] = trim($link[0]);
751
752
    //split into src and parameters (using the very last questionmark)
753
    $pos = strrpos($link[0], '?');
754
    if($pos !== false){
755
        $src   = substr($link[0],0,$pos);
756
        $param = substr($link[0],$pos+1);
757
    }else{
758
        $src   = $link[0];
759
        $param = '';
760
    }
761
762
    //parse width and height
763
    if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){
764
        !empty($size[1]) ? $w = $size[1] : $w = null;
765
        !empty($size[3]) ? $h = $size[3] : $h = null;
766
    } else {
767
        $w = null;
768
        $h = null;
769
    }
770
771
    //get linking command
772
    if(preg_match('/nolink/i',$param)){
773
        $linking = 'nolink';
774
    }else if(preg_match('/direct/i',$param)){
775
        $linking = 'direct';
776
    }else if(preg_match('/linkonly/i',$param)){
777
        $linking = 'linkonly';
778
    }else{
779
        $linking = 'details';
780
    }
781
782
    //get caching command
783
    if (preg_match('/(nocache|recache)/i',$param,$cachemode)){
784
        $cache = $cachemode[1];
785
    }else{
786
        $cache = 'cache';
787
    }
788
789
    // Check whether this is a local or remote image or interwiki
790
    if (media_isexternal($src) || link_isinterwiki($src)){
791
        $call = 'externalmedia';
792
    } else {
793
        $call = 'internalmedia';
794
    }
795
796
    $params = array(
797
        'type'=>$call,
798
        'src'=>$src,
799
        'title'=>$link[1],
800
        'align'=>$align,
801
        'width'=>$w,
802
        'height'=>$h,
803
        'cache'=>$cache,
804
        'linking'=>$linking,
805
    );
806
807
    return $params;
808
}
809
810
//------------------------------------------------------------------------
811
interface Doku_Handler_CallWriter_Interface {
812
    public function writeCall($call);
813
    public function writeCalls($calls);
814
    public function finalise();
815
}
816
817
class Doku_Handler_CallWriter implements Doku_Handler_CallWriter_Interface {
818
819
    var $Handler;
820
821
    /**
822
     * @param Doku_Handler $Handler
823
     */
824
    function __construct(Doku_Handler $Handler) {
825
        $this->Handler = $Handler;
826
    }
827
828
    function writeCall($call) {
829
        $this->Handler->calls[] = $call;
830
    }
831
832
    function writeCalls($calls) {
833
        $this->Handler->calls = array_merge($this->Handler->calls, $calls);
834
    }
835
836
    // function is required, but since this call writer is first/highest in
837
    // the chain it is not required to do anything
838
    function finalise() {
839
        unset($this->Handler);
840
    }
841
}
842
843
//------------------------------------------------------------------------
844
/**
845
 * Generic call writer class to handle nesting of rendering instructions
846
 * within a render instruction. Also see nest() method of renderer base class
847
 *
848
 * @author    Chris Smith <[email protected]>
849
 */
850
class Doku_Handler_Nest implements Doku_Handler_CallWriter_Interface {
851
852
    var $CallWriter;
853
    var $calls = array();
854
855
    var $closingInstruction;
856
857
    /**
858
     * constructor
859
     *
860
     * @param  Doku_Handler_CallWriter $CallWriter     the renderers current call writer
861
     * @param  string     $close          closing instruction name, this is required to properly terminate the
862
     *                                    syntax mode if the document ends without a closing pattern
863
     */
864
    function __construct(Doku_Handler_CallWriter_Interface $CallWriter, $close="nest_close") {
865
        $this->CallWriter = $CallWriter;
866
867
        $this->closingInstruction = $close;
868
    }
869
870
    function writeCall($call) {
871
        $this->calls[] = $call;
872
    }
873
874
    function writeCalls($calls) {
875
        $this->calls = array_merge($this->calls, $calls);
876
    }
877
878
    function finalise() {
879
        $last_call = end($this->calls);
880
        $this->writeCall(array($this->closingInstruction,array(), $last_call[2]));
881
882
        $this->process();
883
        $this->CallWriter->finalise();
884
        unset($this->CallWriter);
885
    }
886
887
    function process() {
888
        // merge consecutive cdata
889
        $unmerged_calls = $this->calls;
890
        $this->calls = array();
891
892
        foreach ($unmerged_calls as $call) $this->addCall($call);
893
894
        $first_call = reset($this->calls);
895
        $this->CallWriter->writeCall(array("nest", array($this->calls), $first_call[2]));
896
    }
897
898
    function addCall($call) {
899
        $key = count($this->calls);
900
        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
901
            $this->calls[$key-1][1][0] .= $call[1][0];
902
        } else if ($call[0] == 'eol') {
903
            // do nothing (eol shouldn't be allowed, to counter preformatted fix in #1652 & #1699)
904
        } else {
905
            $this->calls[] = $call;
906
        }
907
    }
908
}
909
910
class Doku_Handler_List implements Doku_Handler_CallWriter_Interface {
911
912
    var $CallWriter;
913
914
    var $calls = array();
915
    var $listCalls = array();
916
    var $listStack = array();
917
918
    const NODE = 1;
919
920
    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
921
        $this->CallWriter = $CallWriter;
922
    }
923
924
    function writeCall($call) {
925
        $this->calls[] = $call;
926
    }
927
928
    // Probably not needed but just in case...
929
    function writeCalls($calls) {
930
        $this->calls = array_merge($this->calls, $calls);
931
#        $this->CallWriter->writeCalls($this->calls);
932
    }
933
934
    function finalise() {
935
        $last_call = end($this->calls);
936
        $this->writeCall(array('list_close',array(), $last_call[2]));
937
938
        $this->process();
939
        $this->CallWriter->finalise();
940
        unset($this->CallWriter);
941
    }
942
943
    //------------------------------------------------------------------------
944
    function process() {
945
946
        foreach ( $this->calls as $call ) {
947
            switch ($call[0]) {
948
                case 'list_item':
949
                    $this->listOpen($call);
950
                break;
951
                case 'list_open':
952
                    $this->listStart($call);
953
                break;
954
                case 'list_close':
955
                    $this->listEnd($call);
956
                break;
957
                default:
958
                    $this->listContent($call);
959
                break;
960
            }
961
        }
962
963
        $this->CallWriter->writeCalls($this->listCalls);
964
    }
965
966
    //------------------------------------------------------------------------
967
    function listStart($call) {
968
        $depth = $this->interpretSyntax($call[1][0], $listType);
969
970
        $this->initialDepth = $depth;
971
        //                   array(list type, current depth, index of current listitem_open)
972
        $this->listStack[] = array($listType, $depth, 1);
973
974
        $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]);
975
        $this->listCalls[] = array('listitem_open',array(1),$call[2]);
976
        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
977
    }
978
979
    //------------------------------------------------------------------------
980
    function listEnd($call) {
981
        $closeContent = true;
982
983
        while ( $list = array_pop($this->listStack) ) {
984
            if ( $closeContent ) {
985
                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
986
                $closeContent = false;
987
            }
988
            $this->listCalls[] = array('listitem_close',array(),$call[2]);
989
            $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]);
990
        }
991
    }
992
993
    //------------------------------------------------------------------------
994
    function listOpen($call) {
995
        $depth = $this->interpretSyntax($call[1][0], $listType);
996
        $end = end($this->listStack);
997
        $key = key($this->listStack);
998
999
        // Not allowed to be shallower than initialDepth
1000
        if ( $depth < $this->initialDepth ) {
1001
            $depth = $this->initialDepth;
1002
        }
1003
1004
        //------------------------------------------------------------------------
1005
        if ( $depth == $end[1] ) {
1006
1007
            // Just another item in the list...
1008
            if ( $listType == $end[0] ) {
1009
                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
1010
                $this->listCalls[] = array('listitem_close',array(),$call[2]);
1011
                $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
1012
                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
1013
1014
                // new list item, update list stack's index into current listitem_open
1015
                $this->listStack[$key][2] = count($this->listCalls) - 2;
1016
1017
            // Switched list type...
1018
            } else {
1019
1020
                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
1021
                $this->listCalls[] = array('listitem_close',array(),$call[2]);
1022
                $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
1023
                $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
1024
                $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
1025
                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
1026
1027
                array_pop($this->listStack);
1028
                $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
1029
            }
1030
1031
        //------------------------------------------------------------------------
1032
        // Getting deeper...
1033
        } else if ( $depth > $end[1] ) {
1034
1035
            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
1036
            $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
1037
            $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
1038
            $this->listCalls[] = array('listcontent_open',array(),$call[2]);
1039
1040
            // set the node/leaf state of this item's parent listitem_open to NODE
1041
            $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE;
1042
1043
            $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
1044
1045
        //------------------------------------------------------------------------
1046
        // Getting shallower ( $depth < $end[1] )
1047
        } else {
1048
            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
1049
            $this->listCalls[] = array('listitem_close',array(),$call[2]);
1050
            $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
1051
1052
            // Throw away the end - done
1053
            array_pop($this->listStack);
1054
1055
            while (1) {
1056
                $end = end($this->listStack);
1057
                $key = key($this->listStack);
1058
1059
                if ( $end[1] <= $depth ) {
1060
1061
                    // Normalize depths
1062
                    $depth = $end[1];
1063
1064
                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
1065
1066
                    if ( $end[0] == $listType ) {
1067
                        $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
1068
                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
1069
1070
                        // new list item, update list stack's index into current listitem_open
1071
                        $this->listStack[$key][2] = count($this->listCalls) - 2;
1072
1073
                    } else {
1074
                        // Switching list type...
1075
                        $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
1076
                        $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
1077
                        $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
1078
                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
1079
1080
                        array_pop($this->listStack);
1081
                        $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
1082
                    }
1083
1084
                    break;
1085
1086
                // Haven't dropped down far enough yet.... ( $end[1] > $depth )
1087
                } else {
1088
1089
                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
1090
                    $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
1091
1092
                    array_pop($this->listStack);
1093
1094
                }
1095
1096
            }
1097
1098
        }
1099
    }
1100
1101
    //------------------------------------------------------------------------
1102
    function listContent($call) {
1103
        $this->listCalls[] = $call;
1104
    }
1105
1106
    //------------------------------------------------------------------------
1107
    function interpretSyntax($match, & $type) {
1108
        if ( substr($match,-1) == '*' ) {
1109
            $type = 'u';
1110
        } else {
1111
            $type = 'o';
1112
        }
1113
        // Is the +1 needed? It used to be count(explode(...))
1114
        // but I don't think the number is seen outside this handler
1115
        return substr_count(str_replace("\t",'  ',$match), '  ') + 1;
1116
    }
1117
}
1118
1119
//------------------------------------------------------------------------
1120
class Doku_Handler_Preformatted implements Doku_Handler_CallWriter_Interface {
1121
1122
    var $CallWriter;
1123
1124
    var $calls = array();
1125
    var $pos;
1126
    var $text ='';
1127
1128
1129
1130
    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1131
        $this->CallWriter = $CallWriter;
1132
    }
1133
1134
    function writeCall($call) {
1135
        $this->calls[] = $call;
1136
    }
1137
1138
    // Probably not needed but just in case...
1139
    function writeCalls($calls) {
1140
        $this->calls = array_merge($this->calls, $calls);
1141
#        $this->CallWriter->writeCalls($this->calls);
1142
    }
1143
1144
    function finalise() {
1145
        $last_call = end($this->calls);
1146
        $this->writeCall(array('preformatted_end',array(), $last_call[2]));
1147
1148
        $this->process();
1149
        $this->CallWriter->finalise();
1150
        unset($this->CallWriter);
1151
    }
1152
1153
    function process() {
1154
        foreach ( $this->calls as $call ) {
1155
            switch ($call[0]) {
1156
                case 'preformatted_start':
1157
                    $this->pos = $call[2];
1158
                break;
1159
                case 'preformatted_newline':
1160
                    $this->text .= "\n";
1161
                break;
1162
                case 'preformatted_content':
1163
                    $this->text .= $call[1][0];
1164
                break;
1165
                case 'preformatted_end':
1166
                    if (trim($this->text)) {
1167
                        $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos));
1168
                    }
1169
                    // see FS#1699 & FS#1652, add 'eol' instructions to ensure proper triggering of following p_open
1170
                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
1171
                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
1172
                break;
1173
            }
1174
        }
1175
    }
1176
1177
}
1178
1179
//------------------------------------------------------------------------
1180
class Doku_Handler_Quote implements Doku_Handler_CallWriter_Interface {
1181
1182
    var $CallWriter;
1183
1184
    var $calls = array();
1185
1186
    var $quoteCalls = array();
1187
1188
    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1189
        $this->CallWriter = $CallWriter;
1190
    }
1191
1192
    function writeCall($call) {
1193
        $this->calls[] = $call;
1194
    }
1195
1196
    // Probably not needed but just in case...
1197
    function writeCalls($calls) {
1198
        $this->calls = array_merge($this->calls, $calls);
1199
    }
1200
1201
    function finalise() {
1202
        $last_call = end($this->calls);
1203
        $this->writeCall(array('quote_end',array(), $last_call[2]));
1204
1205
        $this->process();
1206
        $this->CallWriter->finalise();
1207
        unset($this->CallWriter);
1208
    }
1209
1210
    function process() {
1211
1212
        $quoteDepth = 1;
1213
1214
        foreach ( $this->calls as $call ) {
1215
            switch ($call[0]) {
1216
1217
                case 'quote_start':
1218
1219
                    $this->quoteCalls[] = array('quote_open',array(),$call[2]);
1220
1221
                case 'quote_newline':
1222
1223
                    $quoteLength = $this->getDepth($call[1][0]);
1224
1225
                    if ( $quoteLength > $quoteDepth ) {
1226
                        $quoteDiff = $quoteLength - $quoteDepth;
1227
                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1228
                            $this->quoteCalls[] = array('quote_open',array(),$call[2]);
1229
                        }
1230
                    } else if ( $quoteLength < $quoteDepth ) {
1231
                        $quoteDiff = $quoteDepth - $quoteLength;
1232
                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1233
                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1234
                        }
1235
                    } else {
1236
                        if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]);
1237
                    }
1238
1239
                    $quoteDepth = $quoteLength;
1240
1241
                break;
1242
1243
                case 'quote_end':
1244
1245
                    if ( $quoteDepth > 1 ) {
1246
                        $quoteDiff = $quoteDepth - 1;
1247
                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
1248
                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1249
                        }
1250
                    }
1251
1252
                    $this->quoteCalls[] = array('quote_close',array(),$call[2]);
1253
1254
                    $this->CallWriter->writeCalls($this->quoteCalls);
1255
                break;
1256
1257
                default:
1258
                    $this->quoteCalls[] = $call;
1259
                break;
1260
            }
1261
        }
1262
    }
1263
1264
    function getDepth($marker) {
1265
        preg_match('/>{1,}/', $marker, $matches);
1266
        $quoteLength = strlen($matches[0]);
1267
        return $quoteLength;
1268
    }
1269
}
1270
1271
//------------------------------------------------------------------------
1272
class Doku_Handler_Table implements Doku_Handler_CallWriter_Interface {
1273
1274
    var $CallWriter;
1275
1276
    var $calls = array();
1277
    var $tableCalls = array();
1278
    var $maxCols = 0;
1279
    var $maxRows = 1;
1280
    var $currentCols = 0;
1281
    var $firstCell = false;
1282
    var $lastCellType = 'tablecell';
1283
    var $inTableHead = true;
1284
    var $currentRow = array('tableheader' => 0, 'tablecell' => 0);
1285
    var $countTableHeadRows = 0;
1286
1287
    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
1288
        $this->CallWriter = $CallWriter;
1289
    }
1290
1291
    function writeCall($call) {
1292
        $this->calls[] = $call;
1293
    }
1294
1295
    // Probably not needed but just in case...
1296
    function writeCalls($calls) {
1297
        $this->calls = array_merge($this->calls, $calls);
1298
    }
1299
1300
    function finalise() {
1301
        $last_call = end($this->calls);
1302
        $this->writeCall(array('table_end',array(), $last_call[2]));
1303
1304
        $this->process();
1305
        $this->CallWriter->finalise();
1306
        unset($this->CallWriter);
1307
    }
1308
1309
    //------------------------------------------------------------------------
1310
    function process() {
1311
        foreach ( $this->calls as $call ) {
1312
            switch ( $call[0] ) {
1313
                case 'table_start':
1314
                    $this->tableStart($call);
1315
                break;
1316
                case 'table_row':
1317
                    $this->tableRowClose($call);
1318
                    $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
1319
                break;
1320
                case 'tableheader':
1321
                case 'tablecell':
1322
                    $this->tableCell($call);
1323
                break;
1324
                case 'table_end':
1325
                    $this->tableRowClose($call);
1326
                    $this->tableEnd($call);
1327
                break;
1328
                default:
1329
                    $this->tableDefault($call);
1330
                break;
1331
            }
1332
        }
1333
        $this->CallWriter->writeCalls($this->tableCalls);
1334
    }
1335
1336
    function tableStart($call) {
1337
        $this->tableCalls[] = array('table_open',$call[1],$call[2]);
1338
        $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
1339
        $this->firstCell = true;
1340
    }
1341
1342
    function tableEnd($call) {
1343
        $this->tableCalls[] = array('table_close',$call[1],$call[2]);
1344
        $this->finalizeTable();
1345
    }
1346
1347
    function tableRowOpen($call) {
1348
        $this->tableCalls[] = $call;
1349
        $this->currentCols = 0;
1350
        $this->firstCell = true;
1351
        $this->lastCellType = 'tablecell';
1352
        $this->maxRows++;
1353
        if ($this->inTableHead) {
1354
            $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
1355
        }
1356
    }
1357
1358
    function tableRowClose($call) {
1359
        if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
1360
            $this->countTableHeadRows++;
1361
        }
1362
        // Strip off final cell opening and anything after it
1363
        while ( $discard = array_pop($this->tableCalls ) ) {
1364
1365
            if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
1366
                break;
1367
            }
1368
            if (!empty($this->currentRow[$discard[0]])) {
1369
                $this->currentRow[$discard[0]]--;
1370
            }
1371
        }
1372
        $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
1373
1374
        if ( $this->currentCols > $this->maxCols ) {
1375
            $this->maxCols = $this->currentCols;
1376
        }
1377
    }
1378
1379
    function isTableHeadRow() {
1380
        $td = $this->currentRow['tablecell'];
1381
        $th = $this->currentRow['tableheader'];
1382
1383
        if (!$th || $td > 2) return false;
1384
        if (2*$td > $th) return false;
1385
1386
        return true;
1387
    }
1388
1389
    function tableCell($call) {
1390
        if ($this->inTableHead) {
1391
            $this->currentRow[$call[0]]++;
1392
        }
1393
        if ( !$this->firstCell ) {
1394
1395
            // Increase the span
1396
            $lastCall = end($this->tableCalls);
1397
1398
            // A cell call which follows an open cell means an empty cell so span
1399
            if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) {
1400
                 $this->tableCalls[] = array('colspan',array(),$call[2]);
1401
1402
            }
1403
1404
            $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
1405
            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
1406
            $this->lastCellType = $call[0];
1407
1408
        } else {
1409
1410
            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
1411
            $this->lastCellType = $call[0];
1412
            $this->firstCell = false;
1413
1414
        }
1415
1416
        $this->currentCols++;
1417
    }
1418
1419
    function tableDefault($call) {
1420
        $this->tableCalls[] = $call;
1421
    }
1422
1423
    function finalizeTable() {
1424
1425
        // Add the max cols and rows to the table opening
1426
        if ( $this->tableCalls[0][0] == 'table_open' ) {
1427
            // Adjust to num cols not num col delimeters
1428
            $this->tableCalls[0][1][] = $this->maxCols - 1;
1429
            $this->tableCalls[0][1][] = $this->maxRows;
1430
            $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
1431
        } else {
1432
            trigger_error('First element in table call list is not table_open');
1433
        }
1434
1435
        $lastRow = 0;
1436
        $lastCell = 0;
1437
        $cellKey = array();
1438
        $toDelete = array();
1439
1440
        // if still in tableheader, then there can be no table header
1441
        // as all rows can't be within <THEAD>
1442
        if ($this->inTableHead) {
1443
            $this->inTableHead = false;
1444
            $this->countTableHeadRows = 0;
1445
        }
1446
1447
        // Look for the colspan elements and increment the colspan on the
1448
        // previous non-empty opening cell. Once done, delete all the cells
1449
        // that contain colspans
1450
        for ($key = 0 ; $key < count($this->tableCalls) ; ++$key) {
1451
            $call = $this->tableCalls[$key];
1452
1453
            switch ($call[0]) {
1454
                case 'table_open' :
1455
                    if($this->countTableHeadRows) {
1456
                        array_splice($this->tableCalls, $key+1, 0, array(
1457
                              array('tablethead_open', array(), $call[2]))
1458
                        );
1459
                    }
1460
                    break;
1461
1462
                case 'tablerow_open':
1463
1464
                    $lastRow++;
1465
                    $lastCell = 0;
1466
                    break;
1467
1468
                case 'tablecell_open':
1469
                case 'tableheader_open':
1470
1471
                    $lastCell++;
1472
                    $cellKey[$lastRow][$lastCell] = $key;
1473
                    break;
1474
1475
                case 'table_align':
1476
1477
                    $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
1478
                    $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
1479
                    // If the cell is empty, align left
1480
                    if ($prev && $next) {
1481
                        $this->tableCalls[$key-1][1][1] = 'left';
1482
1483
                    // If the previous element was a cell open, align right
1484
                    } elseif ($prev) {
1485
                        $this->tableCalls[$key-1][1][1] = 'right';
1486
1487
                    // If the next element is the close of an element, align either center or left
1488
                    } elseif ( $next) {
1489
                        if ( $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right' ) {
1490
                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
1491
                        } else {
1492
                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
1493
                        }
1494
1495
                    }
1496
1497
                    // Now convert the whitespace back to cdata
1498
                    $this->tableCalls[$key][0] = 'cdata';
1499
                    break;
1500
1501
                case 'colspan':
1502
1503
                    $this->tableCalls[$key-1][1][0] = false;
1504
1505
                    for($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
1506
1507
                        if (
1508
                            $this->tableCalls[$i][0] == 'tablecell_open' ||
1509
                            $this->tableCalls[$i][0] == 'tableheader_open'
1510
                        ) {
1511
1512
                            if ( false !== $this->tableCalls[$i][1][0] ) {
1513
                                $this->tableCalls[$i][1][0]++;
1514
                                break;
1515
                            }
1516
1517
                        }
1518
                    }
1519
1520
                    $toDelete[] = $key-1;
1521
                    $toDelete[] = $key;
1522
                    $toDelete[] = $key+1;
1523
                    break;
1524
1525
                case 'rowspan':
1526
1527
                    if ( $this->tableCalls[$key-1][0] == 'cdata' ) {
1528
                        // ignore rowspan if previous call was cdata (text mixed with :::)
1529
                        // we don't have to check next call as that wont match regex
1530
                        $this->tableCalls[$key][0] = 'cdata';
1531
1532
                    } else {
1533
1534
                        $spanning_cell = null;
1535
1536
                        // can't cross thead/tbody boundary
1537
                        if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
1538
                            for($i = $lastRow-1; $i > 0; $i--) {
1539
1540
                                if (
1541
                                    $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
1542
                                    $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
1543
                                ) {
1544
1545
                                    if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
1546
                                        $spanning_cell = $i;
1547
                                        break;
1548
                                    }
1549
1550
                                }
1551
                            }
1552
                        }
1553
                        if (is_null($spanning_cell)) {
1554
                            // No spanning cell found, so convert this cell to
1555
                            // an empty one to avoid broken tables
1556
                            $this->tableCalls[$key][0] = 'cdata';
1557
                            $this->tableCalls[$key][1][0] = '';
1558
                            continue;
1559
                        }
1560
                        $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
1561
1562
                        $this->tableCalls[$key-1][1][2] = false;
1563
1564
                        $toDelete[] = $key-1;
1565
                        $toDelete[] = $key;
1566
                        $toDelete[] = $key+1;
1567
                    }
1568
                    break;
1569
1570
                case 'tablerow_close':
1571
1572
                    // Fix broken tables by adding missing cells
1573
                    $moreCalls = array();
1574
                    while (++$lastCell < $this->maxCols) {
1575
                        $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
1576
                        $moreCalls[] = array('cdata', array(''), $call[2]);
1577
                        $moreCalls[] = array('tablecell_close', array(), $call[2]);
1578
                    }
1579
                    $moreCallsLength = count($moreCalls);
1580
                    if($moreCallsLength) {
1581
                        array_splice($this->tableCalls, $key, 0, $moreCalls);
1582
                        $key += $moreCallsLength;
1583
                    }
1584
1585
                    if($this->countTableHeadRows == $lastRow) {
1586
                        array_splice($this->tableCalls, $key+1, 0, array(
1587
                              array('tablethead_close', array(), $call[2])));
1588
                    }
1589
                    break;
1590
1591
            }
1592
        }
1593
1594
        // condense cdata
1595
        $cnt = count($this->tableCalls);
1596
        for( $key = 0; $key < $cnt; $key++){
1597
            if($this->tableCalls[$key][0] == 'cdata'){
1598
                $ckey = $key;
1599
                $key++;
1600
                while($this->tableCalls[$key][0] == 'cdata'){
1601
                    $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
1602
                    $toDelete[] = $key;
1603
                    $key++;
1604
                }
1605
                continue;
1606
            }
1607
        }
1608
1609
        foreach ( $toDelete as $delete ) {
1610
            unset($this->tableCalls[$delete]);
1611
        }
1612
        $this->tableCalls = array_values($this->tableCalls);
1613
    }
1614
}
1615
1616
1617
/**
1618
 * Handler for paragraphs
1619
 *
1620
 * @author Harry Fuecks <[email protected]>
1621
 */
1622
class Doku_Handler_Block {
1623
    var $calls = array();
1624
    var $skipEol = false;
1625
    var $inParagraph = false;
1626
1627
    // Blocks these should not be inside paragraphs
1628
    var $blockOpen = array(
1629
            'header',
1630
            'listu_open','listo_open','listitem_open','listcontent_open',
1631
            'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open',
1632
            'quote_open',
1633
            'code','file','hr','preformatted','rss',
1634
            'htmlblock','phpblock',
1635
            'footnote_open',
1636
        );
1637
1638
    var $blockClose = array(
1639
            'header',
1640
            'listu_close','listo_close','listitem_close','listcontent_close',
1641
            'table_close','tablerow_close','tablecell_close','tableheader_close','tablethead_close',
1642
            'quote_close',
1643
            'code','file','hr','preformatted','rss',
1644
            'htmlblock','phpblock',
1645
            'footnote_close',
1646
        );
1647
1648
    // Stacks can contain paragraphs
1649
    var $stackOpen = array(
1650
        'section_open',
1651
        );
1652
1653
    var $stackClose = array(
1654
        'section_close',
1655
        );
1656
1657
1658
    /**
1659
     * Constructor. Adds loaded syntax plugins to the block and stack
1660
     * arrays
1661
     *
1662
     * @author Andreas Gohr <[email protected]>
1663
     */
1664
    function __construct(){
1665
        global $DOKU_PLUGINS;
1666
        //check if syntax plugins were loaded
1667
        if(empty($DOKU_PLUGINS['syntax'])) return;
1668
        foreach($DOKU_PLUGINS['syntax'] as $n => $p){
1669
            $ptype = $p->getPType();
1670
            if($ptype == 'block'){
1671
                $this->blockOpen[]  = 'plugin_'.$n;
1672
                $this->blockClose[] = 'plugin_'.$n;
1673
            }elseif($ptype == 'stack'){
1674
                $this->stackOpen[]  = 'plugin_'.$n;
1675
                $this->stackClose[] = 'plugin_'.$n;
1676
            }
1677
        }
1678
    }
1679
1680
    function openParagraph($pos){
1681
        if ($this->inParagraph) return;
1682
        $this->calls[] = array('p_open',array(), $pos);
1683
        $this->inParagraph = true;
1684
        $this->skipEol = true;
1685
    }
1686
1687
    /**
1688
     * Close a paragraph if needed
1689
     *
1690
     * This function makes sure there are no empty paragraphs on the stack
1691
     *
1692
     * @author Andreas Gohr <[email protected]>
1693
     *
1694
     * @param string|integer $pos
1695
     */
1696
    function closeParagraph($pos){
1697
        if (!$this->inParagraph) return;
1698
        // look back if there was any content - we don't want empty paragraphs
1699
        $content = '';
1700
        $ccount = count($this->calls);
1701
        for($i=$ccount-1; $i>=0; $i--){
1702
            if($this->calls[$i][0] == 'p_open'){
1703
                break;
1704
            }elseif($this->calls[$i][0] == 'cdata'){
1705
                $content .= $this->calls[$i][1][0];
1706
            }else{
1707
                $content = 'found markup';
1708
                break;
1709
            }
1710
        }
1711
1712
        if(trim($content)==''){
1713
            //remove the whole paragraph
1714
            //array_splice($this->calls,$i); // <- this is much slower than the loop below
1715
            for($x=$ccount; $x>$i; $x--) array_pop($this->calls);
1716
        }else{
1717
            // remove ending linebreaks in the paragraph
1718
            $i=count($this->calls)-1;
1719
            if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0],DOKU_PARSER_EOL);
1720
            $this->calls[] = array('p_close',array(), $pos);
1721
        }
1722
1723
        $this->inParagraph = false;
1724
        $this->skipEol = true;
1725
    }
1726
1727
    function addCall($call) {
1728
        $key = count($this->calls);
1729
        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
1730
            $this->calls[$key-1][1][0] .= $call[1][0];
1731
        } else {
1732
            $this->calls[] = $call;
1733
        }
1734
    }
1735
1736
    // simple version of addCall, without checking cdata
1737
    function storeCall($call) {
1738
        $this->calls[] = $call;
1739
    }
1740
1741
    /**
1742
     * Processes the whole instruction stack to open and close paragraphs
1743
     *
1744
     * @author Harry Fuecks <[email protected]>
1745
     * @author Andreas Gohr <[email protected]>
1746
     *
1747
     * @param array $calls
1748
     *
1749
     * @return array
1750
     */
1751
    function process($calls) {
1752
        // open first paragraph
1753
        $this->openParagraph(0);
1754
        foreach ( $calls as $key => $call ) {
1755
            $cname = $call[0];
1756
            if ($cname == 'plugin') {
1757
                $cname='plugin_'.$call[1][0];
1758
                $plugin = true;
1759
                $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL));
1760
                $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL));
1761
            } else {
1762
                $plugin = false;
1763
            }
1764
            /* stack */
1765
            if ( in_array($cname,$this->stackClose ) && (!$plugin || $plugin_close)) {
1766
                $this->closeParagraph($call[2]);
1767
                $this->storeCall($call);
1768
                $this->openParagraph($call[2]);
1769
                continue;
1770
            }
1771
            if ( in_array($cname,$this->stackOpen ) && (!$plugin || $plugin_open) ) {
1772
                $this->closeParagraph($call[2]);
1773
                $this->storeCall($call);
1774
                $this->openParagraph($call[2]);
1775
                continue;
1776
            }
1777
            /* block */
1778
            // If it's a substition it opens and closes at the same call.
1779
            // To make sure next paragraph is correctly started, let close go first.
1780
            if ( in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) {
1781
                $this->closeParagraph($call[2]);
1782
                $this->storeCall($call);
1783
                $this->openParagraph($call[2]);
1784
                continue;
1785
            }
1786
            if ( in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) {
1787
                $this->closeParagraph($call[2]);
1788
                $this->storeCall($call);
1789
                continue;
1790
            }
1791
            /* eol */
1792
            if ( $cname == 'eol' ) {
1793
                // Check this isn't an eol instruction to skip...
1794
                if ( !$this->skipEol ) {
1795
                    // Next is EOL => double eol => mark as paragraph
1796
                    if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) {
1797
                        $this->closeParagraph($call[2]);
1798
                        $this->openParagraph($call[2]);
1799
                    } else {
1800
                        //if this is just a single eol make a space from it
1801
                        $this->addCall(array('cdata',array(DOKU_PARSER_EOL), $call[2]));
1802
                    }
1803
                }
1804
                continue;
1805
            }
1806
            /* normal */
1807
            $this->addCall($call);
1808
            $this->skipEol = false;
1809
        }
1810
        // close last paragraph
1811
        $call = end($this->calls);
1812
        $this->closeParagraph($call[2]);
1813
        return $this->calls;
1814
    }
1815
}
1816
1817
//Setup VIM: ex: et ts=4 :
1818