xref: /plugin/siteexport/syntax/toc.php (revision af16752f00d9c6b45709f5e1f07bc4352e8b387e)
1<?php
2/**
3 * Search with Scopes
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     i-net software <tools@inetsoftware.de>
7 * @author     Gerry Weissbach <gweissbach@inetsoftware.de>
8 */
9
10// must be run within Dokuwiki
11if (!defined('DOKU_INC')) die();
12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
13
14require_once(DOKU_PLUGIN . 'syntax.php');
15
16class syntax_plugin_siteexport_toc extends DokuWiki_Syntax_Plugin {
17
18    private $insideToc = false;
19    private $savedToc = array();
20    private $options = array();
21
22    private $mergedPages = array();
23    private $includedPages = array();
24    private $merghintIds = array();
25
26    function getType() { return 'protected'; }
27    function getPType() { return 'block'; }
28    function getAllowedTypes() { return array('container'); }
29    function getSort() { return 100; }
30
31    /**
32     * Connect pattern to lexer
33     */
34    function connectTo($mode) {
35        $this->Lexer->addEntryPattern('<toc>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc');
36        $this->Lexer->addEntryPattern('<toc .+?>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc');
37        $this->Lexer->addSpecialPattern("\[\[.+?\]\]", $mode, 'plugin_siteexport_toc');
38    }
39
40    function postConnect() {
41        $this->Lexer->addExitPattern('</toc.*?>', 'plugin_siteexport_toc');
42    }
43
44    function handle($match, $state, $pos, Doku_Handler $handler) {
45        global $ID, $INFO;
46
47        switch ($state) {
48            case DOKU_LEXER_ENTER:
49
50                $this->insideToc = true;
51
52                $this->options = explode(' ', substr($match, 5, -1));
53
54                return array('start' => true, 'pos' => $pos, 'options' => $this->options);
55                break;
56
57            case DOKU_LEXER_SPECIAL:
58
59                if ($this->insideToc) {
60
61                    $link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
62                    // Split title from URL
63                    $link = explode('|', $link, 2);
64                    if (!isset($link[1])) {
65                        $link[1] = NULL;
66                    } else if (preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
67                        // If the title is an image, convert it to an array containing the image details
68                        $link[1] = Doku_Handler_Parse_Media($link[1]);
69                    }
70                    $link[0] = trim($link[0]);
71
72                    if (!(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0]) ||
73                    preg_match('/^\\\\\\\\[\w.:?\-;,]+?\\\\/u', $link[0]) ||
74                    preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0]) ||
75                    preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0]) ||
76                    preg_match('!^#.+!', $link[0]))
77                    ) {
78
79                        // Get current depth from call stack
80                        $depth = 1;
81                        if ($handler->CallWriter instanceof Doku_Handler_List) {
82
83                            $calls = array_reverse($handler->CallWriter->calls);
84                            $call = $calls[0];
85                            foreach ($calls as $item) {
86                                if (in_array($item[0], array('list_item', 'list_open'))) { $call = $item; break; }
87                            }
88
89                            $depth = $handler->CallWriter->interpretSyntax($call[1][0], $listType)-1; // Minus one because of plus one inside the interpret function
90                        }
91
92                        if (empty($link[0])) { break; } // No empty elements. This would lead to problems
93                        return array($link[0], $link[1], $depth);
94                        break;
95                    } else {
96                        // use parser! - but with another p
97                        $handler->internallink($match, $state, $pos);
98                    }
99                } else {
100                    // use parser!
101                    $handler->internallink($match, $state, $pos);
102                }
103
104                return false;
105            case DOKU_LEXER_UNMATCHED:
106
107                $handler->_addCall('cdata', array($match), $pos);
108
109                return false;
110                break;
111            case DOKU_LEXER_EXIT:
112
113                $this->insideToc = false;
114                return 'save__meta';
115                break;
116        }
117        return false;
118    }
119
120    function render($mode, Doku_Renderer $renderer, $data) {
121        global $ID, $lang, $INFO;
122
123        list($SID, $NAME, $DEPTH) = $data;
124
125        resolve_pageid(getNS($ID), $SID, $exists);
126//        $SID = cleanID($SID); // hier kein cleanID, da sonst moeglicherweise der anker verloren geht
127
128        //    Render XHTML and ODT
129        if ($mode == 'xhtml' || $mode == 'odt') {
130
131            // TOC Title
132            if (is_array($data) && $data['start'] == true) {
133
134                if (is_Array($data['options'])) {
135                    foreach ($data['options'] as $opt) {
136                        switch ($opt) {
137                            case 'description' : $renderer->meta['sitetoc']['showDescription'] = true; break;
138                            case 'notoc' : $renderer->meta['sitetoc']['noTOC'] = true; break;
139                            case 'merge' : $renderer->meta['sitetoc']['mergeDoc'] = true; break;
140                            case 'nohead' : $renderer->meta['sitetoc']['noTocHeader'] = true; break;
141                            case 'mergeheader' : $renderer->meta['sitetoc']['mergeHeader'] = true; break;
142                            case 'pagebreak' : $renderer->meta['sitetoc']['pagebreak'] = true; break;
143                            case 'mergehint' : $renderer->meta['sitetoc']['mergehint'] = true; break;
144                        }
145                    }
146                }
147
148                $renderer->section_open("1 sitetoc");
149                if ($renderer->meta['sitetoc']['noTocHeader'] === false) {
150                    $renderer->header($lang['toc'], 1, $data['pos']);
151                }
152
153                return true;
154            } else
155
156            // All Output has been done
157            if (!is_array($data) && $data == 'save__meta') {
158
159                // Close TOC
160                $renderer->section_close();
161
162                if ($renderer->meta['sitetoc']['noTOC'] === true) {
163                    $renderer->doc = preg_replace("/<div.*?sitetoc.*?$/si", "", $renderer->doc);
164                }
165
166                // If this is not set, we may have it as Metadata
167                if (!$this->mergedPages && $renderer->meta['sitetoc']['mergeDoc']) {
168                    $toc = $renderer->meta['sitetoc']['siteexportTOC'];
169
170                    if (is_array($toc)) {
171                        foreach ($toc as $tocItem) {
172                            $this->mergedPages[] = array($tocItem['id'], $tocItem['depth']);
173                        }
174                    }
175
176                }
177
178                // If there is some data to be merged
179                if (count($this->mergedPages) > 0) {
180
181                    $renderer->doc = ''; // Start fresh!
182
183                    $renderer->section_open("1 mergedsite" . ($renderer->meta['sitetoc']['mergehint']?' mergehint':''));
184
185                    // Prepare lookup Array
186                    foreach ($this->mergedPages as $tocItem) {
187                        $this->includedPages[] = array_shift(explode('#', $tocItem[0]));
188                    }
189
190                    // Load the instructions
191                    $instr = array();
192                    foreach ($this->mergedPages as $tocElement) {
193
194                        list($tocItem, $depth) = $tocElement;
195                        $file = wikiFN($tocItem);
196
197                        if (@file_exists($file)) {
198                            $instructions = p_cached_instructions($file, false, $tocItem);
199                        } else {
200                            $instructions = p_get_instructions(io_readWikiPage($file, $tocItem));
201                        }
202
203                        // Convert Link and header instructions
204                        $instructions = $this->_convertInstructions($instructions, $addID, $renderer, $depth);
205
206                        if ($renderer->meta['sitetoc']['mergeHeader'] && count($this->mergedPages) > 1 ) {
207                            // get a hint for merged pages
208                            if ($renderer->meta['sitetoc']['mergehint']) {
209                                // only if the first section is already there
210                                $mergeHint = p_get_metadata( $tocItem, 'mergehint', METADATA_RENDER_USING_SIMPLE_CACHE );
211                                if ( empty( $mergeHint) ) { $mergeHint = p_get_metadata( $tocItem, 'thema', METADATA_RENDER_USING_SIMPLE_CACHE ); }
212                                if ( empty( $mergeHint) ) { $mergeHint = tpl_pagetitle( $tocItem, true ); }
213                                $instructions = $this->_mergeWithHeaders( $this->_initialHeaderStructure( $instructions ), $instructions, 1, $mergeHint);
214                            }
215                            // Merge
216                            $instr = $this->_mergeWithHeaders( $instr, $instructions, 1);
217                        } else
218                        if ($renderer->meta['sitetoc']['pagebreak']) {
219                            $sitepagebreak = array( array(
220                                'plugin',
221                                array(
222                                    'siteexport_toctools',
223                                    array(
224                                        'pagebreak',
225                                        null,
226                                        null
227                                    )
228                                )
229                            ));
230                            $instr = array_merge($instr, $instructions, $sitepagebreak);
231                        } else {
232                            // Concat
233                            $instr = array_merge($instr, $instructions);
234                        }
235                    }
236
237                    if (!empty($instr)) {
238                        $this->_cleanAllInstructions($instr);
239
240                        //if its the document start, cut off the first element(document information)
241                        if ($instr[count($instr)-1][1][0] == 'siteexport_toctools') {
242                            $instr = array_slice($instr, 0, -1);
243                        }
244
245
246                        // print "<pre>"; print_r($instr); print "</pre>";
247                        $this->_render_output($renderer, $mode, $instr);
248                    }
249
250                    $renderer->section_close();
251                }
252                return true;
253            }
254
255            // Save the current ID
256            $LNID = $SID;
257
258            // Add ID to flags['mergeDoc']
259            if ($renderer->meta['sitetoc']['mergeDoc'] === true) { // || (count($renderer->meta['sitetoc']['siteexportTOC']) > 0 && $renderer->meta['sitetoc']['siteexportMergeDoc'] === true) ) {
260                $this->mergedPages[] = array($SID, $DEPTH);
261                $default = $renderer->_simpleTitle($SID); $isImage = false;
262                resolve_pageid(getNS($ID), $SID, $exists);
263
264                $NAME = empty($NAME) ? p_get_first_heading($SID, true) : $NAME;
265                $LNID = "$ID#" . sectionID($SID, $check);
266
267            } else {
268                // // print normal internal link (XHTML odt)
269                $renderer->internallink($LNID, $NAME, null);
270
271                // Display Description underneath
272                if ($renderer->meta['sitetoc']['showDescription'] === true) {
273                    // $renderer->p_open();
274                    $renderer->cdata(p_get_metadata($SID, 'description abstract', true));
275                    // $renderer->p_close();
276                }
277            }
278
279            // Render Metadata
280        } else if ($mode == 'metadata') {
281            if (!is_array($data) && $data == 'save__meta') {
282                $renderer->meta['sitetoc']['siteexportTOC'] = $this->savedToc;
283
284                foreach ($this->savedToc as $page) {
285                    $renderer->meta['relation']['references'][$page['id']] = $page['exists'];
286                }
287
288                $this->savedToc = array();
289            } else if (!isset($data['start']) && !isset($data['pos'])) {
290                $this->savedToc[] = $this->__addTocItem($SID, $NAME, $DEPTH, $renderer);
291            }
292        } else {
293            return false;
294        }
295
296        return true;
297    }
298
299    /*
300     * pull apart the ID and create an Entry for the TOC
301     */
302    function private __addTocItem($id, $name, $depth, $renderer) {
303        global $conf;
304        global $ID;
305
306        // Render Title
307        $default = $renderer->_simpleTitle($id);
308        $exists = false; $isImage = false; $linktype = null;
309        resolve_pageid(getNS($ID), $id, $exists);
310        $name = $renderer->_getLinkTitle($name, $default, $isImage, $id, $linktype);
311
312        //keep hash anchor
313        list($id, $hash) = explode('#', $id, 2);
314        if (!empty($hash)) $hash = $renderer->_headerToLink($hash);
315
316        // Build Sitetoc Item
317        $item = array();
318        $item['id'] = $id;
319        $item['name'] = $name;
320        $item['anchor'] = $hash;
321        $item['depth'] = $depth;
322        $item['exists'] = $exists;
323        if (!$conf['skipacl'] && auth_quickaclcheck($item['id']) < AUTH_READ) {
324            return false;
325        }
326
327        return $item;
328    }
329
330    /*
331     * Render the output of one page
332     */
333    function private _render_output($renderer, $mode, $instr) {
334        global $ID;
335
336        // Section IDs
337        // $addID = sectionID($addID, $check);    //not possible to use a:b:c for id
338
339        if ($mode == 'xhtml') {
340
341            //--------RENDER
342            //renderer information(TOC build / Cache used)
343            $info = array();
344            $content = p_render($mode, $instr, $info);
345
346            //Remove TOC`s, section edit buttons and tags
347            $content = $this->_cleanXHTML($content);
348
349            // embed the included page
350            // $renderer->doc .= '<div class="include">';
351            //add an anchor to find start of a inserted page
352            // $renderer->doc .= "<a name='$addID' id='$addID'>";
353            $renderer->doc .= $content;
354            // $renderer->doc .= '</div>';
355        } else if ($mode == 'odt') {
356
357            // Loop through the instructions
358            foreach ($instr as $instruction) {
359                // Execute the callback against the Renderer
360                call_user_func_array(array($renderer, $instruction[0]), $instruction[1]);
361            }
362        }
363    }
364
365    /*
366     * Corrects relative internal links and media and
367     * converts headers of included pages to subheaders of the current page
368     */
369    function private _convertInstructions($instr, $id, &$renderer, $depth = 1) {
370        global $ID;
371        global $conf;
372
373        $n = count($instr);
374
375        for ($i = 0; $i < $n; $i++) {
376            //internal links(links inside this wiki) an relative links
377            if ((substr($instr[$i][0], 0, 12) == 'internallink')) {
378                $this->_convert_link($renderer, $instr[$i], $id);
379            }
380            else if ((substr($instr[$i][0], 0, 13) == 'internalmedia')) {
381                $this->_convert_media($renderer, $instr[$i], $id);
382            }
383            else if ((substr($instr[$i][0], 0, 6) == 'header')) {
384                $this->_convert_header($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1
385            }
386            else if ((substr($instr[$i][0], 0, 12) == 'section_open')) {
387                $this->_convert_section($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1
388            }
389        }
390
391        //if its the document start, cut off the first element(document information)
392        if ($instr[0][0] == 'document_start')
393        return array_slice($instr, 1, -1);
394        else
395        return $instr;
396    }
397
398    /*
399     * Convert link of given instruction
400     */
401    function private _convert_link(&$renderer, &$instr, $id) {
402        global $ID;
403
404        $exists = false;
405
406        resolve_pageid(getNS($id), $instr[1][0], $exists);
407        list($pageID, $pageReference) = explode("#", $instr[1][0], 2);
408
409        if (in_array($pageID, $this->includedPages)) {
410            // Crate new internal Links
411            $check = null;
412
413            // Either get existing reference or create from first heading. If still not there take the alternate ID
414            $pageNameLink = empty($pageReference) ? sectionID($pageID, $check) : $pageReference;
415
416            $instr[1][0] = $ID . "#" . $pageNameLink;
417
418        } else {
419            // Convert external Links to plain Text
420
421            $instr = array(
422                        "cdata",
423            array($instr[1][1]),
424            $instr[2]
425            );
426        }
427    }
428
429    /*
430     * Convert internalmedia of given instruction
431     */
432    function private _convert_media(&$renderer, &$instr, $id) {
433        global $ID;
434
435        // Resolvemedia returns the absolute path to media by reference
436        $exists = false;
437        resolve_mediaid(getNS($id), $instr[1][0], $exists);
438    }
439
440    /**
441     * @param integer $depth
442     */
443    function private _convert_header(&$renderer, &$instr, $depth) {
444        // More Depth!
445        $instr[1][1] += $depth;
446    }
447
448    /**
449     * @param integer $depth
450     */
451    function private _convert_section(&$renderer, &$instr, $depth) {
452        // More Depth!
453        $instr[1][0] += $depth;
454    }
455
456    function private _mergeWithHeaders($existing, $newInstructions, $level = 1, $mergeHint = array() ) {
457
458        $returnInstructions = array();
459        $preparedInstructions = array();
460        $existingStart = $existingEnd = 0;
461        $firstRun = true;
462
463        while ($this->_findNextHeaderSection($existing, $level, $existingStart, $existingEnd)) {
464
465            if ($firstRun) {
466                $returnInstructions = array_merge($returnInstructions, array_slice($existing, 0, $existingStart));
467                $firstRun = false;
468            }
469
470            $currentSlice = array_slice($existing, $existingStart, $existingEnd-$existingStart);
471
472            // Find matching part with headername
473            $newStart = $newEnd = 0;
474            if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd, $currentSlice[0][1][0])) {
475
476                $newSlice = array_slice($newInstructions, $newStart, $newEnd-$newStart);
477                if ($newSlice[0][0] == 'header')
478                    array_shift($newSlice); // Remove Heading
479
480                // merge found parts on next level.
481                $returnedInstructions = $this->_mergeWithHeaders($currentSlice, $newSlice, $level+1, $mergeHint);
482
483                // Put them at the end!
484                $preparedInstructions = array_merge($preparedInstructions, $returnedInstructions);
485
486                // Remove from input
487                array_splice($newInstructions, $newStart, $newEnd-$newStart);
488            } else {
489                // Nothing else found
490                $preparedInstructions = array_merge($preparedInstructions, $currentSlice);
491            }
492
493            $existingStart = $existingEnd;
494        }
495
496        // Append the rest
497        $returnInstructions = array_merge($returnInstructions, array_slice($existing, $existingStart));
498
499        // Check for section close inconsistencies and put one at the very end ...
500        $section_postpend = array();
501        if (
502            (
503            ($tmp = array_slice($newInstructions, -1))
504            && ($tmp[0][0] == 'section_close')
505            )
506            &&
507            (
508            ($tmp = array_slice($newInstructions, -2))
509            && ($tmp[0][0] == 'section_close')
510            )
511        ) {
512            $section_postpend = array_splice($newInstructions, -1);
513        }
514        if (
515            (
516            ($tmp = array_slice($returnInstructions, -1))
517            && ($tmp[0][0] == 'section_close')
518            )
519            &&
520            (
521            ($tmp = array_slice($returnInstructions, -2))
522            && ($tmp[0][0] == 'section_close')
523            )
524        ) {
525            $section_postpend = array_merge($section_postpend, array_splice($returnInstructions, -1));
526        }
527
528        // What if there are headings left inside the $newInstructions?????
529        // Find matching part with headername
530        $newStart = $newEnd = 0;
531        $section_prepend = array();
532        if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd)) {
533            // If there are header in here, build a prepend and have the rest at the end
534            $section_prepend = array_splice($newInstructions, 0, $newStart);
535        } else {
536            // If not, prepend all of it.
537            $section_prepend = $newInstructions;
538            $newInstructions = array();
539        }
540
541        $this->_insertMergeHint( $section_prepend, $mergeHint );
542
543        $returnInstructions = array_merge($returnInstructions, $section_prepend, $preparedInstructions, $newInstructions, $section_postpend);
544
545        return $returnInstructions;
546    }
547
548    /**
549     * @param integer $level
550     */
551    function private _findNextHeaderSection($section, $level, &$start, &$end, $headerName = null) {
552
553        $inCount = count($section);
554        $currentSlice = -1;
555
556        // Find Level 1 Header that matches.
557        for ($i = $start; $i < $inCount; $i++) {
558
559            $instruction = $section[$i];
560            $end = $i; // Or it will be lost and a section close will be missing.
561
562            // First Level Header
563            if ($instruction[0] == 'header' && $instruction[1][1] == $level) {
564
565                if ($currentSlice > 0) {
566                    return true;
567                }
568
569                if ($headerName == null || ($headerName == $instruction[1][0])) {
570                    // Begin of new slice ...
571                    $start = $currentSlice = $i;
572                }
573            }
574        }
575
576        // Nothing found
577        $end = $i; // Or it will be lost and a section close will be missing.
578        return $currentSlice > 0;
579    }
580
581    function private _cleanAllInstructions(&$instr) {
582        $this->_cleanInstructions($instr, '/section_(close|open)/');
583        $this->_cleanInstructions($instr, '/listu_(close|open)/');
584        $this->_cleanInstructions($instr, '/listo_(close|open)/');
585    }
586
587    /**
588     * @param string $tag
589     */
590    function private _cleanInstructions(&$instructions, $tag) {
591
592        $inCount = count($instructions);
593        for ($i = 0; $i < $inCount; $i++) {
594
595            // Last instruction
596            if ($i == $inCount-1) {
597                break;
598            }
599
600            if (preg_match($tag, $instructions[$i][0]) && preg_match($tag, $instructions[$i+1][0]) && $instructions[$i][0] != $instructions[$i+1][0]) {
601
602                // found different tags, but both match the expression and follow each other - so they can be elliminated
603                array_splice($instructions, $i, 2);
604                $inCount -= 2;
605                $i--;
606            }
607
608        }
609    }
610
611    /**
612     * Strip everything except for the headers
613     */
614    function private _initialHeaderStructure($instructions) {
615        $inCount = count($instructions);
616        for ($i = 0; $i < $inCount; $i++) {
617
618            // Last instruction
619            if ($i == $inCount-1) {
620                break;
621            }
622
623            if (!in_array($instructions[$i][0], array('header', 'section_open', 'section_close', 'p_open', 'p_close'))) {
624                // found non-matching
625                array_splice($instructions, $i, 1);
626                $inCount --;
627                $i--;
628            }
629        }
630        return $instructions;
631    }
632
633    function private _insertMergeHint( &$instructions, $mergeHint ) {
634
635        // Surround new slice with a mergehint
636        if ( empty( $mergeHint ) ) { return; }
637
638        // No emtpy insruction sets.
639        $this->_cleanAllInstructions( $instructions );
640        if ( empty( $instructions ) ) { return; }
641
642        // only section content should be surrounded.
643        if ( $instructions[0][0] != 'section_open' ) { return; }
644
645        // save for later use
646        $mergeHints = array();
647        $mergeHintId = sectionid( $mergeHint, $mergeHints );
648        $this->merghintIds[$mergeHintId] = $mergeHint;
649
650        $mergeHintPrepend = array( array(
651            'plugin',
652            array(
653                'siteexport_toctools',
654                array(
655                    'mergehint',
656                    'start',
657                    $mergeHint,
658                    $mergeHintId
659                )
660            )
661        ));
662
663        $mergeHintPostpend = array( array(
664            'plugin',
665            array(
666                'siteexport_toctools',
667                array(
668                    'mergehint',
669                    'end',
670                    $mergeHint
671                )
672            )
673        ));
674
675/*
676        print "\n\n#########\n";
677        print_r($instructions);
678        print "\nn#########\n\n";
679*/
680        $instructions = array_merge( $mergeHintPrepend, $instructions, $mergeHintPostpend );
681    }
682
683    /**
684     * Remove TOC, section edit buttons and tags
685     */
686    function _cleanXHTML($xhtml) {
687        $replace = array(
688            '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs
689            '#<!-- SECTION \[(\d*-\d*)\] -->#s'       => '', // remove section edit buttons
690            '!<div id="tags">.*?(</div>)!s'           => ''  // remove category tags
691        );
692        $xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml);
693        return $xhtml;
694    }
695
696    /**
697     * Allow the plugin to prevent DokuWiki creating a second instance of itself
698     *
699     * @return bool   true if the plugin can not be instantiated more than once
700     */
701    function isSingleton() {
702        return true;
703    }
704}
705// vim:ts=4:sw=4:et:enc=utf-8:
706