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