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