1<?php
2
3
4use ComboStrap\Call;
5use ComboStrap\CallStack;
6use ComboStrap\ExceptionCombo;
7use ComboStrap\LogUtility;
8use ComboStrap\MediaLink;
9use ComboStrap\Page;
10use ComboStrap\PluginUtility;
11use ComboStrap\PageEdit;
12
13class action_plugin_combo_headingpostprocessing extends DokuWiki_Action_Plugin
14{
15
16
17    /**
18     * This section are not HTML
19     * section, they are edit section
20     * that delimits the edit area
21     */
22    const EDIT_SECTION_OPEN = 'section_open';
23    const EDIT_SECTION_CLOSE = 'section_close';
24
25
26    /**
27     * @var int a counter that should at 0
28     * at the end of the processing
29     * +1 if an outline section was opened
30     * -1 if an outline section was closed
31     */
32    private $outlineSectionBalance = 0;
33
34    /**
35     * Insert the HTML section
36     * @param CallStack $callStack
37     * @param Call $actualCall
38     * @param int $actualLastPosition
39     */
40    private function openOutlineSection(CallStack $callStack, Call $actualCall, int $actualLastPosition)
41    {
42        if ($actualCall->getContext() == syntax_plugin_combo_heading::TYPE_OUTLINE) {
43            $call = Call::createComboCall(
44                syntax_plugin_combo_section::TAG,
45                DOKU_LEXER_ENTER,
46                array(),
47                $actualLastPosition
48            );
49            $callStack->insertBefore($call);
50            $this->outlineSectionBalance++;
51        }
52    }
53
54    /**
55     * Close the outline section if the levels difference
56     * are from the same level or less
57     * @param CallStack $callStack
58     * @param Call|null $actualHeadingCall
59     * @param int $previousLevel
60     * @param int $actualLastPosition
61     */
62    private function closeOutlineSectionIfNeeded(CallStack $callStack, Call $actualHeadingCall, int $previousLevel, int $actualLastPosition)
63    {
64        $close = false;
65        if ($actualHeadingCall->getContext() == syntax_plugin_combo_heading::TYPE_OUTLINE) {
66
67            $actualLevel = intval($actualHeadingCall->getAttribute("level"));
68            if ($actualLevel <= $previousLevel) {
69                $close = true;
70            }
71
72            if ($close) {
73                $this->closeOutlineSection($callStack, $actualLastPosition);
74            }
75
76        }
77
78    }
79
80    /**
81     * @param CallStack $callStack
82     * @param $handler
83     * @param $position
84     */
85    private function closeEditSection(CallStack $callStack, $handler, $position)
86    {
87
88        $call = Call::createNativeCall(
89            self::EDIT_SECTION_CLOSE,
90            array(),
91            $position
92        );
93        $callStack->insertBefore($call);
94        $handler->setStatus('section', false);
95    }
96
97    /**
98     * Technically add a div around the content below the heading
99     * @param $callStack
100     * @param $headingEntryCall
101     * @param $handler
102     */
103    private
104    static function openEditSection($callStack, $headingEntryCall, $handler)
105    {
106
107        $openSectionCall = Call::createNativeCall(
108            self::EDIT_SECTION_OPEN,
109            array($headingEntryCall->getAttribute(syntax_plugin_combo_headingatx::LEVEL)),
110            $headingEntryCall->getFirstMatchedCharacterPosition()
111        );
112        $callStack->insertAfter($openSectionCall);
113        $handler->setStatus('section', true);
114
115
116    }
117
118    public
119    function register(\Doku_Event_Handler $controller)
120    {
121        /**
122         * Found in {@link Doku_Handler::finalize()}
123         *
124         * Doc: https://www.dokuwiki.org/devel:event:parser_handler_done
125         */
126        $controller->register_hook(
127            'PARSER_HANDLER_DONE',
128            'AFTER',
129            $this,
130            '_post_process_heading',
131            array()
132        );
133
134    }
135
136
137    /**
138     * Transform the special heading atx call
139     * in an enter and exit heading atx calls
140     *
141     * Add the section close / open
142     *
143     * Code extracted and adapted from the end of {@link Doku_Handler::header()}
144     *
145     * @param   $event Doku_Event
146     */
147    function _post_process_heading(&$event, $param)
148    {
149        /**
150         * @var Doku_Handler $handler
151         */
152        $handler = $event->data;
153        $callStack = CallStack::createFromHandler($handler);
154        $callStack->moveToStart();
155
156        /**
157         * Close the section
158         * for whatever reason, the section status is true
159         * even if the sections are closed
160         * We take the hypothesis that the sections are closed
161         */
162        $handler->setStatus('section', false);
163
164        /**
165         * Processing variable about the context
166         */
167        $actualHeadingParsingState = DOKU_LEXER_EXIT; // enter if we have entered a heading, exit otherwise
168        $actualSectionState = null; // enter if we have created a section
169        $headingEnterCall = null; // the enter call
170
171        $headingText = ""; // text only content in the heading
172        $previousHeadingLevel = 0; // A pointer to the actual heading level
173        $headingComboCounter = 0; // The number of combo heading found (The first one that is not the first one should close)
174        $headingTotalCounter = 0; // The number of combo heading found (The first one that is not the first one should close)
175
176        $actualLastPosition = 0;
177        while ($actualCall = $callStack->next()) {
178
179            $tagName = $actualCall->getTagName();
180
181            /**
182             * Track the position in the file
183             */
184            $currentLastPosition = $actualCall->getLastMatchedCharacterPosition();
185            if ($currentLastPosition > $actualLastPosition) {
186                // the position in the stack is not always good
187                $actualLastPosition = $currentLastPosition;
188            }
189
190            /**
191             * Enter
192             */
193            switch ($tagName) {
194                case syntax_plugin_combo_headingatx::TAG:
195                    $actualCall->setState(DOKU_LEXER_ENTER);
196                    $actualHeadingParsingState = DOKU_LEXER_ENTER;
197                    $headingEnterCall = $callStack->getActualCall();
198                    $headingComboCounter++;
199                    $headingTotalCounter++;
200                    $this->closeEditSectionIfNeeded($actualCall, $handler, $callStack, $actualSectionState, $headingComboCounter, $headingTotalCounter, $actualLastPosition);
201                    $this->closeOutlineSectionIfNeeded($callStack, $actualCall, $previousHeadingLevel, $actualLastPosition);
202                    $this->openOutlineSection($callStack, $actualCall, $actualLastPosition);
203                    continue 2;
204                case syntax_plugin_combo_heading::TAG:
205                case syntax_plugin_combo_headingwiki::TAG:
206                    if ($actualCall->getState() == DOKU_LEXER_ENTER) {
207                        $actualHeadingParsingState = DOKU_LEXER_ENTER;
208                        $headingEnterCall = $callStack->getActualCall();
209                        $headingComboCounter++;
210                        $headingTotalCounter++;
211                        self::closeEditSectionIfNeeded($actualCall, $handler, $callStack, $actualSectionState, $headingComboCounter, $headingTotalCounter, $actualLastPosition);
212                        self::closeOutlineSectionIfNeeded($callStack, $actualCall, $previousHeadingLevel, $actualLastPosition);
213                        self::openOutlineSection($callStack, $actualCall, $actualLastPosition);
214                        $previousHeadingLevel = $headingEnterCall->getAttribute("level");
215                        continue 2;
216                    }
217                    break;
218                case "header":
219                    $headingTotalCounter++;
220                    break;
221            }
222
223
224            /**
225             * Close and Inside the heading description
226             */
227            if ($actualHeadingParsingState === DOKU_LEXER_ENTER) {
228
229                switch ($actualCall->getTagName()) {
230
231                    case syntax_plugin_combo_heading::TAG:
232                    case syntax_plugin_combo_headingwiki::TAG:
233                        if ($actualCall->getState() == DOKU_LEXER_EXIT) {
234                            self::insertOpenSectionAfterAndCloseHeadingParsingStateAndNext(
235                                $headingEnterCall,
236                                $handler,
237                                $callStack,
238                                $actualSectionState,
239                                $headingText,
240                                $actualHeadingParsingState
241                            );
242                        } else {
243                            // unmatched
244                            self::addToTextHeading($headingText, $actualCall);
245                        }
246                        continue 2;
247
248                    case "internalmedia":
249                        // no link for media in heading
250                        $actualCall->getCall()[1][6] = MediaLink::LINKING_NOLINK_VALUE;
251                        continue 2;
252
253                    case "header":
254                        if (PluginUtility::getConfValue(syntax_plugin_combo_headingwiki::CONF_WIKI_HEADING_ENABLE, syntax_plugin_combo_headingwiki::CONF_DEFAULT_WIKI_ENABLE_VALUE) == 1) {
255                            LogUtility::msg("The combo heading wiki is enabled, we should not see `header` calls in the call stack");
256                        }
257                        break;
258
259                    case syntax_plugin_combo_media::TAG:
260                        // no link for media in heading
261                        $actualCall->addAttribute(MediaLink::LINKING_KEY, MediaLink::LINKING_NOLINK_VALUE);
262                        continue 2;
263
264                    default:
265                        self::addToTextHeading($headingText, $actualCall);
266                        continue 2;
267
268                    case "p":
269                        if ($headingEnterCall->getTagName() == syntax_plugin_combo_headingatx::TAG) {
270
271                            /**
272                             * Delete the p_enter / close
273                             */
274                            $callStack->deleteActualCallAndPrevious();
275
276                            /**
277                             * If this was a close tag
278                             */
279                            if ($actualCall->getComponentName() == "p_close") {
280
281                                $callStack->next();
282
283                                /**
284                                 * Create the exit call
285                                 * and open the section
286                                 * Code extracted and adapted from the end of {@link Doku_Handler::header()}
287                                 */
288                                $callStack->insertBefore(
289                                    Call::createComboCall(
290                                        syntax_plugin_combo_headingatx::TAG,
291                                        DOKU_LEXER_EXIT,
292                                        $headingEnterCall->getAttributes()
293                                    )
294                                );
295                                $callStack->previous();
296
297                                /**
298                                 * Close and section
299                                 */
300                                self::insertOpenSectionAfterAndCloseHeadingParsingStateAndNext(
301                                    $headingEnterCall,
302                                    $handler,
303                                    $callStack,
304                                    $actualSectionState,
305                                    $headingText,
306                                    $actualHeadingParsingState
307                                );
308
309
310                            }
311
312                        }
313                        continue 2;
314
315                }
316
317
318            }
319            /**
320             * when a heading of dokuwiki is mixed with
321             * an atx heading, there is already a section close
322             * at the end or in the middle
323             */
324            if ($actualCall->getComponentName() == self::EDIT_SECTION_CLOSE) {
325                $actualSectionState = DOKU_LEXER_EXIT;
326            }
327
328        }
329
330        /**
331         * If the section was open by us or is still open, we close it
332         *
333         * We don't use the standard `section` key (ie  $handler->getStatus('section')
334         * because it's open when we receive the handler
335         * even if the `section_close` is present in the call stack
336         *
337         * We make sure that we close only what we have open
338         */
339        if ($actualSectionState == DOKU_LEXER_ENTER) {
340            $this->closeEditSection($callStack, $handler, $actualLastPosition);
341        }
342
343        /**
344         * Closing outline section
345         */
346        while ($this->outlineSectionBalance > 0) {
347            $this->closeOutlineSection($callStack, $actualLastPosition);
348        }
349
350        /**
351         * Not heading at all
352         * No dynamic rendering (ie $ID is not null)
353         */
354        global $ID;
355        if ($ID !== null) {
356
357            $page = Page::createPageFromId($ID);
358            if ($headingTotalCounter === 0 || $page->isSecondarySlot()) {
359                try {
360                    $tag = PageEdit::create("Slot Edit")->toTag();
361                    if(!empty($tag)) { // page edit is not off
362                        $sectionEditComment = Call::createComboCall(
363                            syntax_plugin_combo_comment::TAG,
364                            DOKU_LEXER_UNMATCHED,
365                            array(),
366                            Call::INLINE_DISPLAY, // don't trim
367                            null,
368                            $tag
369                        );
370                        $callStack->insertBefore($sectionEditComment);
371                    }
372                } catch (ExceptionCombo $e) {
373                    LogUtility::msg("Error while adding the edit button. Error: {$e->getMessage()}");
374                }
375            }
376
377        }
378
379
380    }
381
382    /**
383     * @param $headingEntryCall
384     * @param $handler
385     * @param CallStack $callStack
386     * @param $actualSectionState
387     * @param $headingText
388     * @param $actualHeadingParsingState
389     */
390    private
391    static function insertOpenSectionAfterAndCloseHeadingParsingStateAndNext(&$headingEntryCall, &$handler, CallStack &$callStack, &$actualSectionState, &$headingText, &$actualHeadingParsingState)
392    {
393        /**
394         * We are no more in a heading
395         */
396        $actualHeadingParsingState = DOKU_LEXER_EXIT;
397
398        /**
399         * Outline ?
400         * Update the text and open a section
401         */
402        if ($headingEntryCall->getContext() === syntax_plugin_combo_heading::TYPE_OUTLINE) {
403
404            /**
405             * Update the entering call with the text capture
406             */
407            /**
408             * Check the text
409             */
410            if (empty($headingText)) {
411                LogUtility::msg("The heading text for the entry call ($headingEntryCall) is empty");
412            }
413            $headingEntryCall->addAttribute(syntax_plugin_combo_heading::HEADING_TEXT_ATTRIBUTE, $headingText);
414
415            /**
416             * Insert an entry call
417             */
418            self::openEditSection($callStack, $headingEntryCall, $handler);
419
420            $actualSectionState = DOKU_LEXER_ENTER;
421            $callStack->next();
422
423        }
424
425        /**
426         * Reset
427         * Important: If this is not an outline header, we need to reset it
428         * otherwise it comes in the {@link \ComboStrap\TocUtility::renderToc()}
429         */
430        $headingText = "";
431
432
433
434    }
435
436    /**
437     * @param Call $actualCall
438     * @param $handler
439     * @param $callStack
440     * @param $actualSectionState
441     * @param $headingComboCounter
442     * @param $headingTotalCounter
443     * @param $lastActualPosition - the last actual position in the file of the character
444     */
445    private function closeEditSectionIfNeeded(Call &$actualCall, &$handler, &$callStack, &$actualSectionState, $headingComboCounter, $headingTotalCounter, $lastActualPosition)
446    {
447        if ($actualCall->getContext() == syntax_plugin_combo_heading::TYPE_OUTLINE) {
448            $close = $handler->getStatus('section');
449            if ($headingComboCounter == 1 && $headingTotalCounter != 1) {
450                /**
451                 * If this is the first combo heading
452                 * We need to close the previous to open
453                 * this one
454                 */
455                $close = true;
456            }
457            if ($close) {
458                self::closeEditSection($callStack, $handler, $lastActualPosition);
459                $actualSectionState = DOKU_LEXER_EXIT;
460            }
461        }
462    }
463
464    /**
465     * @param $headingText
466     * @param Call $call
467     */
468    private
469    static function addToTextHeading(&$headingText, $call)
470    {
471        if ($call->isTextCall()) {
472            // Building the text for the toc
473            // only cdata for now
474            // no image, ...
475            if ($headingText != "") {
476                $headingText .= " ";
477            }
478            $headingText .= trim($call->getCapturedContent());
479        }
480    }
481
482    private function closeOutlineSection($callStack, $position)
483    {
484        $openSectionCall = Call::createComboCall(
485            syntax_plugin_combo_section::TAG,
486            DOKU_LEXER_EXIT,
487            array(),
488            null,
489            null,
490            null,
491            $position
492        );
493        $callStack->insertBefore($openSectionCall);
494        $this->outlineSectionBalance--;
495    }
496}
497