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