xref: /plugin/combo/ComboStrap/HeadingTag.php (revision 04823ca8181c63f6d4b5375fb4e403c02da20ebe)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeaunamespace ComboStrap;
404fd306cSNickeau
504fd306cSNickeau
604fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
704fd306cSNickeauuse Doku_Renderer_metadata;
804fd306cSNickeauuse Doku_Renderer_xhtml;
904fd306cSNickeauuse renderer_plugin_combo_analytics;
1004fd306cSNickeauuse syntax_plugin_combo_webcode;
1104fd306cSNickeau
1204fd306cSNickeauclass HeadingTag
1304fd306cSNickeau{
1404fd306cSNickeau
1504fd306cSNickeau    /**
1604fd306cSNickeau     * The type of the heading tag
1704fd306cSNickeau     */
1804fd306cSNickeau    public const DISPLAY_TYPES = ["d1", "d2", "d3", "d4", "d5", "d6"];
1904fd306cSNickeau    public const HEADING_TYPES = ["h1", "h2", "h3", "h4", "h5", "h6"];
2004fd306cSNickeau    public const SHORT_TYPES = ["1", "2", "3", "4", "5", "6"];
2104fd306cSNickeau
2204fd306cSNickeau    /**
2304fd306cSNickeau     * The type of the title tag
2404fd306cSNickeau     * @deprecated
2504fd306cSNickeau     */
2604fd306cSNickeau    public const TITLE_DISPLAY_TYPES = ["0", "1", "2", "3", "4", "5", "6"];
2704fd306cSNickeau
2804fd306cSNickeau
2904fd306cSNickeau    /**
3004fd306cSNickeau     * An heading may be printed
3104fd306cSNickeau     * as outline and should be in the toc
3204fd306cSNickeau     */
3304fd306cSNickeau    public const TYPE_OUTLINE = "outline";
3404fd306cSNickeau    public const CANONICAL = "heading";
3504fd306cSNickeau
3604fd306cSNickeau    public const SYNTAX_TYPE = 'baseonly';
3704fd306cSNickeau    public const SYNTAX_PTYPE = 'block';
3804fd306cSNickeau    /**
3904fd306cSNickeau     * The section generation:
4004fd306cSNickeau     *   - Dokuwiki section (ie div just after the heading)
4104fd306cSNickeau     *   - or Combo section (ie section just before the heading)
4204fd306cSNickeau     */
4304fd306cSNickeau    public const CONF_SECTION_LAYOUT = 'section_layout';
4404fd306cSNickeau    public const CONF_SECTION_LAYOUT_VALUES = [HeadingTag::CONF_SECTION_LAYOUT_COMBO, HeadingTag::CONF_SECTION_LAYOUT_DOKUWIKI];
4504fd306cSNickeau    public const CONF_SECTION_LAYOUT_DEFAULT = HeadingTag::CONF_SECTION_LAYOUT_COMBO;
4604fd306cSNickeau    public const LEVEL = 'level';
4704fd306cSNickeau    public const CONF_SECTION_LAYOUT_DOKUWIKI = "dokuwiki";
4804fd306cSNickeau    /**
4904fd306cSNickeau     *  old tag
5004fd306cSNickeau     */
5104fd306cSNickeau    public const TITLE_TAG = "title";
5204fd306cSNickeau    /**
5304fd306cSNickeau     * New tag
5404fd306cSNickeau     */
5504fd306cSNickeau    public const HEADING_TAG = "heading";
5604fd306cSNickeau    public const LOGICAL_TAG = self::HEADING_TAG;
5704fd306cSNickeau
5804fd306cSNickeau
5904fd306cSNickeau    /**
6004fd306cSNickeau     * The default level if not set
6104fd306cSNickeau     * Not level 1 because this is the top level heading
6204fd306cSNickeau     * Not level 2 because this is the most used level and we can confound with it
6304fd306cSNickeau     */
6404fd306cSNickeau    public const DEFAULT_LEVEL_TITLE_CONTEXT = "3";
6504fd306cSNickeau    public const DISPLAY_BS_4_RESPONSIVE_SNIPPET_ID = "display-bs-4";
6604fd306cSNickeau    /**
6704fd306cSNickeau     * The attribute that holds only the text of the heading
6804fd306cSNickeau     * (used to create the id and the text in the toc)
6904fd306cSNickeau     */
7004fd306cSNickeau    public const HEADING_TEXT_ATTRIBUTE = "heading_text";
7104fd306cSNickeau    public const CONF_SECTION_LAYOUT_COMBO = "combo";
7204fd306cSNickeau
7304fd306cSNickeau
7404fd306cSNickeau    /**
7504fd306cSNickeau     * 1 because in test if used without any, this is
7604fd306cSNickeau     * the first expected one in a outline
7704fd306cSNickeau     */
7804fd306cSNickeau    public const DEFAULT_LEVEL_OUTLINE_CONTEXT = "1";
7904fd306cSNickeau    public const TYPE_TITLE = "title";
8004fd306cSNickeau    public const TAGS = [HeadingTag::HEADING_TAG, HeadingTag::TITLE_TAG];
8104fd306cSNickeau    /**
8204fd306cSNickeau     * only available in 5
8304fd306cSNickeau     */
8404fd306cSNickeau    public const DISPLAY_TYPES_ONLY_BS_5 = ["d5", "d6"];
8504fd306cSNickeau
8604fd306cSNickeau    /**
8704fd306cSNickeau     * The label is the text that is generally used
8804fd306cSNickeau     * in a TOC but also as default title for the page
8904fd306cSNickeau     */
9004fd306cSNickeau    public const PARSED_LABEL = "label";
9104fd306cSNickeau
9204fd306cSNickeau
9304fd306cSNickeau    /**
9404fd306cSNickeau     * A common function used to handle exit of headings
9504fd306cSNickeau     * @param \Doku_Handler $handler
9604fd306cSNickeau     * @return array
9704fd306cSNickeau     */
9804fd306cSNickeau    public static function handleExit(\Doku_Handler $handler): array
9904fd306cSNickeau    {
10004fd306cSNickeau
10104fd306cSNickeau        $callStack = CallStack::createFromHandler($handler);
10204fd306cSNickeau
10304fd306cSNickeau        /**
10404fd306cSNickeau         * Delete the last space if any
10504fd306cSNickeau         */
10604fd306cSNickeau        $callStack->moveToEnd();
10704fd306cSNickeau        $previous = $callStack->previous();
10804fd306cSNickeau        if ($previous->getState() == DOKU_LEXER_UNMATCHED) {
10904fd306cSNickeau            $previous->setPayload(rtrim($previous->getCapturedContent()));
11004fd306cSNickeau        }
11104fd306cSNickeau        $callStack->next();
11204fd306cSNickeau
11304fd306cSNickeau        /**
11404fd306cSNickeau         * Get context data
11504fd306cSNickeau         */
11604fd306cSNickeau        $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
11704fd306cSNickeau        $openingAttributes = $openingTag->getAttributes(); // for level
11804fd306cSNickeau        $context = $openingTag->getContext(); // for sectioning
11904fd306cSNickeau
12004fd306cSNickeau        return array(
12104fd306cSNickeau            PluginUtility::STATE => DOKU_LEXER_EXIT,
12204fd306cSNickeau            PluginUtility::ATTRIBUTES => $openingAttributes,
12304fd306cSNickeau            PluginUtility::CONTEXT => $context
12404fd306cSNickeau        );
12504fd306cSNickeau    }
12604fd306cSNickeau
12704fd306cSNickeau    /**
12804fd306cSNickeau     * @param CallStack $callStack
12904fd306cSNickeau     * @return string
13004fd306cSNickeau     */
13104fd306cSNickeau    public static function getContext(CallStack $callStack): string
13204fd306cSNickeau    {
13304fd306cSNickeau
13404fd306cSNickeau        /**
13504fd306cSNickeau         * If the heading is inside a component,
13604fd306cSNickeau         * it's a title heading, otherwise it's a outline heading
13704fd306cSNickeau         *
13804fd306cSNickeau         * (Except for {@link syntax_plugin_combo_webcode} that can wrap several outline heading)
13904fd306cSNickeau         *
14004fd306cSNickeau         * When the parent is empty, a section_open (ie another outline heading)
14104fd306cSNickeau         * this is a outline
14204fd306cSNickeau         */
14304fd306cSNickeau        $parent = $callStack->moveToParent();
14404fd306cSNickeau        if ($parent && $parent->getTagName() === Tag\WebCodeTag::TAG) {
14504fd306cSNickeau            $parent = $callStack->moveToParent();
14604fd306cSNickeau        }
14704fd306cSNickeau        if ($parent && $parent->getComponentName() !== "section_open") {
14804fd306cSNickeau            $headingType = self::TYPE_TITLE;
14904fd306cSNickeau        } else {
15004fd306cSNickeau            $headingType = self::TYPE_OUTLINE;
15104fd306cSNickeau        }
15204fd306cSNickeau
15304fd306cSNickeau        switch ($headingType) {
15404fd306cSNickeau            case HeadingTag::TYPE_TITLE:
15504fd306cSNickeau
15604fd306cSNickeau                $context = $parent->getTagName();
15704fd306cSNickeau                break;
15804fd306cSNickeau
15904fd306cSNickeau            case HeadingTag::TYPE_OUTLINE:
16004fd306cSNickeau
16104fd306cSNickeau                $context = HeadingTag::TYPE_OUTLINE;
16204fd306cSNickeau                break;
16304fd306cSNickeau
16404fd306cSNickeau            default:
16504fd306cSNickeau                LogUtility::msg("The heading type ($headingType) is unknown");
16604fd306cSNickeau                $context = "";
16704fd306cSNickeau                break;
16804fd306cSNickeau        }
16904fd306cSNickeau        return $context;
17004fd306cSNickeau    }
17104fd306cSNickeau
17204fd306cSNickeau    /**
17304fd306cSNickeau     * @param $data
17404fd306cSNickeau     * @param Doku_Renderer_metadata $renderer
17504fd306cSNickeau     */
17604fd306cSNickeau    public static function processHeadingEnterMetadata($data, Doku_Renderer_metadata $renderer)
17704fd306cSNickeau    {
17804fd306cSNickeau
17904fd306cSNickeau        $state = $data[PluginUtility::STATE];
18004fd306cSNickeau        if (!in_array($state, [DOKU_LEXER_ENTER, DOKU_LEXER_SPECIAL])) {
18104fd306cSNickeau            return;
18204fd306cSNickeau        }
18304fd306cSNickeau        /**
18404fd306cSNickeau         * Only outline heading metadata
18504fd306cSNickeau         * Not component heading
18604fd306cSNickeau         */
18704fd306cSNickeau        $context = $data[PluginUtility::CONTEXT];
18804fd306cSNickeau        if ($context === self::TYPE_OUTLINE) {
18904fd306cSNickeau
19004fd306cSNickeau            $callStackArray = $data[PluginUtility::ATTRIBUTES];
19104fd306cSNickeau            $tagAttributes = TagAttributes::createFromCallStackArray($callStackArray);
19270bbd7f1Sgerardnico            $text = $tagAttributes->getValue(HeadingTag::HEADING_TEXT_ATTRIBUTE);
19370bbd7f1Sgerardnico            if ($text !== null) {
19470bbd7f1Sgerardnico                $text = trim($text);
19570bbd7f1Sgerardnico            }
19604fd306cSNickeau            $level = $tagAttributes->getValue(HeadingTag::LEVEL);
19704fd306cSNickeau            $pos = 0; // mandatory for header but not for metadata, we set 0 to make the code analyser happy
19804fd306cSNickeau            $renderer->header($text, $level, $pos);
19904fd306cSNickeau
20004fd306cSNickeau            if ($level === 1) {
20104fd306cSNickeau                $parsedLabel = $tagAttributes->getValue(self::PARSED_LABEL);
20204fd306cSNickeau                $renderer->meta[PageH1::H1_PARSED] = $parsedLabel;
20304fd306cSNickeau            }
20404fd306cSNickeau
20504fd306cSNickeau        }
20604fd306cSNickeau
20704fd306cSNickeau
20804fd306cSNickeau    }
20904fd306cSNickeau
21004fd306cSNickeau    public static function processMetadataAnalytics(array $data, renderer_plugin_combo_analytics $renderer)
21104fd306cSNickeau    {
21204fd306cSNickeau
21304fd306cSNickeau        $state = $data[PluginUtility::STATE];
21404fd306cSNickeau        if ($state !== DOKU_LEXER_ENTER) {
21504fd306cSNickeau            return;
21604fd306cSNickeau        }
21704fd306cSNickeau        /**
21804fd306cSNickeau         * Only outline heading metadata
21904fd306cSNickeau         * Not component heading
22004fd306cSNickeau         */
22170bbd7f1Sgerardnico        $context = $data[PluginUtility::CONTEXT] ?? null;
22204fd306cSNickeau        if ($context === self::TYPE_OUTLINE) {
22304fd306cSNickeau            $callStackArray = $data[PluginUtility::ATTRIBUTES];
22404fd306cSNickeau            $tagAttributes = TagAttributes::createFromCallStackArray($callStackArray);
22504fd306cSNickeau            $text = $tagAttributes->getValue(HeadingTag::HEADING_TEXT_ATTRIBUTE);
22604fd306cSNickeau            $level = $tagAttributes->getValue(HeadingTag::LEVEL);
22704fd306cSNickeau            $renderer->header($text, $level, 0);
22804fd306cSNickeau        }
22904fd306cSNickeau
23004fd306cSNickeau    }
23104fd306cSNickeau
23204fd306cSNickeau    /**
23304fd306cSNickeau     * @param string $context
23404fd306cSNickeau     * @param TagAttributes $tagAttributes
23504fd306cSNickeau     * @param Doku_Renderer_xhtml $renderer
23604fd306cSNickeau     * @param int|null $pos - null if the call was generated
23704fd306cSNickeau     * @return void
23804fd306cSNickeau     */
23904fd306cSNickeau    public static function processRenderEnterXhtml(string $context, TagAttributes $tagAttributes, Doku_Renderer_xhtml &$renderer, ?int $pos)
24004fd306cSNickeau    {
24104fd306cSNickeau
24204fd306cSNickeau        /**
24304fd306cSNickeau         * All correction that are dependent
24404fd306cSNickeau         * on the markup (ie title or heading)
24504fd306cSNickeau         * are done in the {@link self::processRenderEnterXhtml()}
24604fd306cSNickeau         */
24704fd306cSNickeau
24804fd306cSNickeau        /**
24904fd306cSNickeau         * Variable
25004fd306cSNickeau         */
25104fd306cSNickeau        $type = $tagAttributes->getType();
25204fd306cSNickeau
25304fd306cSNickeau        /**
25404fd306cSNickeau         * Old syntax deprecated
25504fd306cSNickeau         */
25604fd306cSNickeau        if ($type === "0") {
25704fd306cSNickeau            if ($context === self::TYPE_OUTLINE) {
25804fd306cSNickeau                $type = 'h' . self::DEFAULT_LEVEL_OUTLINE_CONTEXT;
25904fd306cSNickeau            } else {
26004fd306cSNickeau                $type = 'h' . self::DEFAULT_LEVEL_TITLE_CONTEXT;
26104fd306cSNickeau            }
26204fd306cSNickeau        }
26304fd306cSNickeau        /**
26404fd306cSNickeau         * Label is for the TOC
26504fd306cSNickeau         */
26604fd306cSNickeau        $tagAttributes->removeAttributeIfPresent(self::PARSED_LABEL);
26704fd306cSNickeau
26804fd306cSNickeau
26904fd306cSNickeau        /**
27004fd306cSNickeau         * Level
27104fd306cSNickeau         */
27204fd306cSNickeau        $level = $tagAttributes->getValueAndRemove(HeadingTag::LEVEL);
27304fd306cSNickeau
27404fd306cSNickeau        /**
27504fd306cSNickeau         * Display Heading
27604fd306cSNickeau         * https://getbootstrap.com/docs/5.0/content/typography/#display-headings
27704fd306cSNickeau         */
27804fd306cSNickeau        if ($context !== self::TYPE_OUTLINE && $type === null) {
27904fd306cSNickeau            /**
28004fd306cSNickeau             * if not an outline, a display
28104fd306cSNickeau             */
28204fd306cSNickeau            $type = "h$level";
28304fd306cSNickeau        }
28404fd306cSNickeau        if (in_array($type, self::DISPLAY_TYPES)) {
28504fd306cSNickeau
28604fd306cSNickeau            $displayClass = "display-$level";
28704fd306cSNickeau
28804fd306cSNickeau            if (Bootstrap::getBootStrapMajorVersion() == Bootstrap::BootStrapFourMajorVersion) {
28904fd306cSNickeau                /**
29004fd306cSNickeau                 * Make Bootstrap display responsive
29104fd306cSNickeau                 */
29204fd306cSNickeau                PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(HeadingTag::DISPLAY_BS_4_RESPONSIVE_SNIPPET_ID);
29304fd306cSNickeau
29404fd306cSNickeau                if (in_array($type, self::DISPLAY_TYPES_ONLY_BS_5)) {
29504fd306cSNickeau                    $displayClass = "display-4";
29604fd306cSNickeau                    LogUtility::msg("Bootstrap 4 does not support the type ($type). Switch to " . PluginUtility::getDocumentationHyperLink(Bootstrap::CANONICAL, "bootstrap 5") . " if you want to use it. The display type was set to `d4`", LogUtility::LVL_MSG_WARNING, self::CANONICAL);
29704fd306cSNickeau                }
29804fd306cSNickeau
29904fd306cSNickeau            }
30004fd306cSNickeau            $tagAttributes->addClassName($displayClass);
30104fd306cSNickeau        }
30204fd306cSNickeau
30304fd306cSNickeau        /**
30404fd306cSNickeau         * Heading class
30504fd306cSNickeau         * https://getbootstrap.com/docs/5.0/content/typography/#headings
30604fd306cSNickeau         * Works on 4 and 5
30704fd306cSNickeau         */
30804fd306cSNickeau        if (in_array($type, self::HEADING_TYPES)) {
30904fd306cSNickeau            $tagAttributes->addClassName($type);
31004fd306cSNickeau        }
31104fd306cSNickeau
31204fd306cSNickeau        /**
31304fd306cSNickeau         * Card title Context class
31404fd306cSNickeau         * TODO: should move to card
31504fd306cSNickeau         */
31604fd306cSNickeau        if (in_array($context, [BlockquoteTag::TAG, CardTag::CARD_TAG])) {
31704fd306cSNickeau            $tagAttributes->addClassName("card-title");
31804fd306cSNickeau        }
31904fd306cSNickeau
32004fd306cSNickeau        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
32104fd306cSNickeau
32204fd306cSNickeau        /**
32304fd306cSNickeau         * Add an outline class to be able to style them at once
32404fd306cSNickeau         *
32504fd306cSNickeau         * The context is by default the parent name or outline.
32604fd306cSNickeau         */
32704fd306cSNickeau        $snippetManager = SnippetSystem::getFromContext();
32804fd306cSNickeau        if ($context === self::TYPE_OUTLINE) {
32904fd306cSNickeau
33004fd306cSNickeau            $tagAttributes->addClassName(Outline::getOutlineHeadingClass());
33104fd306cSNickeau
33204fd306cSNickeau            $snippetManager->attachCssInternalStyleSheet(self::TYPE_OUTLINE);
33304fd306cSNickeau
33404fd306cSNickeau            // numbering
33504fd306cSNickeau            try {
33604fd306cSNickeau
33704fd306cSNickeau                $enable = $executionContext
33804fd306cSNickeau                    ->getConfig()
33904fd306cSNickeau                    ->getValue(Outline::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT);
34004fd306cSNickeau                if ($enable) {
34104fd306cSNickeau                    $snippet = $snippetManager->attachCssInternalStyleSheet(Outline::OUTLINE_HEADING_NUMBERING);
34204fd306cSNickeau                    if (!$snippet->hasInlineContent()) {
34304fd306cSNickeau                        $css = Outline::getCssNumberingRulesFor(Outline::OUTLINE_HEADING_NUMBERING);
34404fd306cSNickeau                        $snippet->setInlineContent($css);
34504fd306cSNickeau                    }
34604fd306cSNickeau                }
34704fd306cSNickeau            } catch (ExceptionBadSyntax $e) {
34804fd306cSNickeau                LogUtility::internalError("An error has occurred while trying to add the outline heading numbering stylesheet.", self::CANONICAL, $e);
34904fd306cSNickeau            } catch (ExceptionNotEnabled $e) {
35004fd306cSNickeau                // ok
35104fd306cSNickeau            }
35204fd306cSNickeau
35304fd306cSNickeau            /**
35404fd306cSNickeau             * Anchor on id
35504fd306cSNickeau             */
35604fd306cSNickeau            $snippetManager = PluginUtility::getSnippetManager();
35704fd306cSNickeau            try {
35804fd306cSNickeau                $snippetManager->attachRemoteJavascriptLibrary(
35904fd306cSNickeau                    Outline::OUTLINE_ANCHOR,
36004fd306cSNickeau                    "https://cdn.jsdelivr.net/npm/anchor-js@4.3.0/anchor.min.js",
36104fd306cSNickeau                    "sha256-LGOWMG4g6/zc0chji4hZP1d8RxR2bPvXMzl/7oPZqjs="
36204fd306cSNickeau                );
36304fd306cSNickeau            } catch (ExceptionBadArgument|ExceptionBadSyntax $e) {
36404fd306cSNickeau                // The url has a file name. this error should not happen
36504fd306cSNickeau                LogUtility::internalError("Unable to add anchor. Error:{$e->getMessage()}", Outline::OUTLINE_ANCHOR);
36604fd306cSNickeau            }
36704fd306cSNickeau            $snippetManager->attachJavascriptFromComponentId(Outline::OUTLINE_ANCHOR);
36804fd306cSNickeau
36904fd306cSNickeau        }
37004fd306cSNickeau        $snippetManager->attachCssInternalStyleSheet(HeadingTag::HEADING_TAG);
37104fd306cSNickeau
37204fd306cSNickeau        /**
37304fd306cSNickeau         * Not a HTML attribute
37404fd306cSNickeau         */
37504fd306cSNickeau        $tagAttributes->removeComponentAttributeIfPresent(self::HEADING_TEXT_ATTRIBUTE);
37604fd306cSNickeau
37704fd306cSNickeau        /**
378783d9857Sgerardnico         * Two headings 1 are shown
379783d9857Sgerardnico         *
380783d9857Sgerardnico         * We delete the heading 1 in the instructions
381783d9857Sgerardnico         * if the template has a content header
382783d9857Sgerardnico         *
383783d9857Sgerardnico         * The instructions may not be reprocessed after upgrade for instance
384783d9857Sgerardnico         * when the installation is done manually
385783d9857Sgerardnico         *
386783d9857Sgerardnico         * To avoid to have two headings, we set a display none if this is the case
387783d9857Sgerardnico         *
388783d9857Sgerardnico         * Note that this should only apply on the document and not on a partial but yeah
389783d9857Sgerardnico         * We go that h1 is not used in partials.
390783d9857Sgerardnico         */
391783d9857Sgerardnico        if ($level === 1) {
392783d9857Sgerardnico
393783d9857Sgerardnico            $hasMainHeaderElement = TemplateForWebPage::create()
394783d9857Sgerardnico                ->setRequestedContextPath(ExecutionContext::getActualOrCreateFromEnv()->getContextPath())
395783d9857Sgerardnico                ->hasElement(TemplateSlot::MAIN_HEADER_ID);
396*dff3a8c8SNico
397*dff3a8c8SNico            // fuck: template should be a runtime parameters and is not
398*dff3a8c8SNico            $executingAction = ExecutionContext::getActualOrCreateFromEnv()->getExecutingAction();
399*dff3a8c8SNico            if ($executingAction === "combo_" . FetcherPageBundler::NAME) {
400*dff3a8c8SNico                $hasMainHeaderElement = false;
401*dff3a8c8SNico            }
402*dff3a8c8SNico
403783d9857Sgerardnico            if ($hasMainHeaderElement) {
404783d9857Sgerardnico                $tagAttributes->addClassName("d-none");
405783d9857Sgerardnico            }
406783d9857Sgerardnico
407783d9857Sgerardnico        }
408783d9857Sgerardnico
409783d9857Sgerardnico        /**
41004fd306cSNickeau         * Printing
41104fd306cSNickeau         */
41204fd306cSNickeau        $tag = self::getTagFromContext($context, $level);
41304fd306cSNickeau        $renderer->doc .= $tagAttributes->toHtmlEnterTag($tag);
41404fd306cSNickeau
41504fd306cSNickeau    }
41604fd306cSNickeau
41704fd306cSNickeau    /**
41804fd306cSNickeau     * @param TagAttributes $tagAttributes
41904fd306cSNickeau     * @param string $context
42004fd306cSNickeau     * @return string
42104fd306cSNickeau     */
42204fd306cSNickeau    public
42304fd306cSNickeau    static function renderClosingTag(TagAttributes $tagAttributes, string $context): string
42404fd306cSNickeau    {
42504fd306cSNickeau        $level = $tagAttributes->getValueAndRemove(HeadingTag::LEVEL);
42604fd306cSNickeau        if ($level == null) {
42704fd306cSNickeau            LogUtility::msg("The level is mandatory when closing a heading", self::CANONICAL);
42804fd306cSNickeau        }
42904fd306cSNickeau        $tag = self::getTagFromContext($context, $level);
43004fd306cSNickeau
43104fd306cSNickeau        return "</$tag>";
43204fd306cSNickeau    }
43304fd306cSNickeau
43404fd306cSNickeau    /**
43504fd306cSNickeau     * Reduce the end of the input string
43604fd306cSNickeau     * to the first opening tag without the ">"
43704fd306cSNickeau     * and returns the closing tag
43804fd306cSNickeau     *
43904fd306cSNickeau     * @param $input
44004fd306cSNickeau     * @return array - the heading attributes as a string
44104fd306cSNickeau     */
44204fd306cSNickeau    public
44304fd306cSNickeau    static function reduceToFirstOpeningTagAndReturnAttributes(&$input)
44404fd306cSNickeau    {
44504fd306cSNickeau        // the variable that will capture the attribute string
44604fd306cSNickeau        $headingStartTagString = "";
44704fd306cSNickeau        // Set to true when the heading tag has completed
44804fd306cSNickeau        $endHeadingParsed = false;
44904fd306cSNickeau        // The closing character `>` indicator of the start and end tag
45004fd306cSNickeau        // true when found
45104fd306cSNickeau        $endTagClosingCharacterParsed = false;
45204fd306cSNickeau        $startTagClosingCharacterParsed = false;
45304fd306cSNickeau        // We start from the edn
45404fd306cSNickeau        $position = strlen($input) - 1;
45504fd306cSNickeau        while ($position > 0) {
45604fd306cSNickeau            $character = $input[$position];
45704fd306cSNickeau
45804fd306cSNickeau            if ($character == "<") {
45904fd306cSNickeau                if (!$endHeadingParsed) {
46004fd306cSNickeau                    // We are at the beginning of the ending tag
46104fd306cSNickeau                    $endHeadingParsed = true;
46204fd306cSNickeau                } else {
46304fd306cSNickeau                    // We have delete all character until the heading start tag
46404fd306cSNickeau                    // add the last one and exit
46504fd306cSNickeau                    $headingStartTagString = $character . $headingStartTagString;
46604fd306cSNickeau                    break;
46704fd306cSNickeau                }
46804fd306cSNickeau            }
46904fd306cSNickeau
47004fd306cSNickeau            if ($character == ">") {
47104fd306cSNickeau                if (!$endTagClosingCharacterParsed) {
47204fd306cSNickeau                    // We are at the beginning of the ending tag
47304fd306cSNickeau                    $endTagClosingCharacterParsed = true;
47404fd306cSNickeau                } else {
47504fd306cSNickeau                    // We have delete all character until the heading start tag
47604fd306cSNickeau                    $startTagClosingCharacterParsed = true;
47704fd306cSNickeau                }
47804fd306cSNickeau            }
47904fd306cSNickeau
48004fd306cSNickeau            if ($startTagClosingCharacterParsed) {
48104fd306cSNickeau                $headingStartTagString = $character . $headingStartTagString;
48204fd306cSNickeau            }
48304fd306cSNickeau
48404fd306cSNickeau
48504fd306cSNickeau            // position --
48604fd306cSNickeau            $position--;
48704fd306cSNickeau
48804fd306cSNickeau        }
48904fd306cSNickeau        $input = substr($input, 0, $position);
49004fd306cSNickeau
49104fd306cSNickeau        if (!empty($headingStartTagString)) {
49204fd306cSNickeau            return PluginUtility::getTagAttributes($headingStartTagString);
49304fd306cSNickeau        } else {
49404fd306cSNickeau            LogUtility::msg("The attributes of the heading are empty and this should not be possible");
49504fd306cSNickeau            return [];
49604fd306cSNickeau        }
49704fd306cSNickeau
49804fd306cSNickeau
49904fd306cSNickeau    }
50004fd306cSNickeau
50104fd306cSNickeau    public
50204fd306cSNickeau    static function handleEnter(\Doku_Handler $handler, TagAttributes $tagAttributes, string $markupTag): array
50304fd306cSNickeau    {
50404fd306cSNickeau        /**
50504fd306cSNickeau         * Context determination
50604fd306cSNickeau         */
50704fd306cSNickeau        $callStack = CallStack::createFromHandler($handler);
50804fd306cSNickeau        $context = HeadingTag::getContext($callStack);
50904fd306cSNickeau
51004fd306cSNickeau        /**
51104fd306cSNickeau         * Level is mandatory (for the closing tag)
51204fd306cSNickeau         */
51304fd306cSNickeau        $level = $tagAttributes->getValue(HeadingTag::LEVEL);
51404fd306cSNickeau        if ($level === null) {
51504fd306cSNickeau
51604fd306cSNickeau            /**
51704fd306cSNickeau             * Old title type
51804fd306cSNickeau             * from 1 to 4 to set the display heading
51904fd306cSNickeau             */
52004fd306cSNickeau            $type = $tagAttributes->getType();
52104fd306cSNickeau            if (is_numeric($type) && $type != 0) {
52204fd306cSNickeau                $level = $type;
52304fd306cSNickeau                if ($markupTag === self::TITLE_TAG) {
52404fd306cSNickeau                    $type = "d$level";
52504fd306cSNickeau                } else {
52604fd306cSNickeau                    $type = "h$level";
52704fd306cSNickeau                }
52804fd306cSNickeau                $tagAttributes->setType($type);
52904fd306cSNickeau            }
53004fd306cSNickeau            /**
53104fd306cSNickeau             * Still null, check the type
53204fd306cSNickeau             */
53304fd306cSNickeau            if ($level == null) {
53404fd306cSNickeau                if (in_array($type, HeadingTag::getAllTypes())) {
53504fd306cSNickeau                    $level = substr($type, 1);
53604fd306cSNickeau                }
53704fd306cSNickeau            }
53804fd306cSNickeau            /**
53904fd306cSNickeau             * Still null, default level
54004fd306cSNickeau             */
54104fd306cSNickeau            if ($level == null) {
54204fd306cSNickeau                if ($context === HeadingTag::TYPE_OUTLINE) {
54304fd306cSNickeau                    $level = HeadingTag::DEFAULT_LEVEL_OUTLINE_CONTEXT;
54404fd306cSNickeau                } else {
54504fd306cSNickeau                    $level = HeadingTag::DEFAULT_LEVEL_TITLE_CONTEXT;
54604fd306cSNickeau                }
54704fd306cSNickeau            }
54804fd306cSNickeau            /**
54904fd306cSNickeau             * Set the level
55004fd306cSNickeau             */
55104fd306cSNickeau            $tagAttributes->addComponentAttributeValue(HeadingTag::LEVEL, $level);
55204fd306cSNickeau        }
55304fd306cSNickeau        return [PluginUtility::CONTEXT => $context];
55404fd306cSNickeau    }
55504fd306cSNickeau
55604fd306cSNickeau    public
55704fd306cSNickeau    static function getAllTypes(): array
55804fd306cSNickeau    {
55904fd306cSNickeau        return array_merge(
56004fd306cSNickeau            self::DISPLAY_TYPES,
56104fd306cSNickeau            self::HEADING_TYPES,
56204fd306cSNickeau            self::SHORT_TYPES,
56304fd306cSNickeau            self::TITLE_DISPLAY_TYPES
56404fd306cSNickeau        );
56504fd306cSNickeau    }
56604fd306cSNickeau
56704fd306cSNickeau    /**
56804fd306cSNickeau     * @param string $context
56904fd306cSNickeau     * @param int $level
57004fd306cSNickeau     * @return string
57104fd306cSNickeau     */
57204fd306cSNickeau    private static function getTagFromContext(string $context, int $level): string
57304fd306cSNickeau    {
57404fd306cSNickeau        if ($context === self::TYPE_OUTLINE) {
57504fd306cSNickeau            return "h$level";
57604fd306cSNickeau        } else {
57704fd306cSNickeau            return "div";
57804fd306cSNickeau        }
57904fd306cSNickeau    }
58004fd306cSNickeau
58104fd306cSNickeau
58204fd306cSNickeau}
583