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