moveToEnd(); $previous = $callStack->previous(); if ($previous->getState() == DOKU_LEXER_UNMATCHED) { $previous->setPayload(rtrim($previous->getCapturedContent())); } $callStack->next(); /** * Get context data */ $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); $openingAttributes = $openingTag->getAttributes(); // for level $context = $openingTag->getContext(); // for sectioning return array( PluginUtility::STATE => DOKU_LEXER_EXIT, PluginUtility::ATTRIBUTES => $openingAttributes, PluginUtility::CONTEXT => $context ); } /** * @param CallStack $callStack * @return string */ public static function getContext(CallStack $callStack): string { /** * If the heading is inside a component, * it's a title heading, otherwise it's a outline heading * * (Except for {@link syntax_plugin_combo_webcode} that can wrap several outline heading) * * When the parent is empty, a section_open (ie another outline heading) * this is a outline */ $parent = $callStack->moveToParent(); if ($parent && $parent->getTagName() === Tag\WebCodeTag::TAG) { $parent = $callStack->moveToParent(); } if ($parent && $parent->getComponentName() !== "section_open") { $headingType = self::TYPE_TITLE; } else { $headingType = self::TYPE_OUTLINE; } switch ($headingType) { case HeadingTag::TYPE_TITLE: $context = $parent->getTagName(); break; case HeadingTag::TYPE_OUTLINE: $context = HeadingTag::TYPE_OUTLINE; break; default: LogUtility::msg("The heading type ($headingType) is unknown"); $context = ""; break; } return $context; } /** * @param $data * @param Doku_Renderer_metadata $renderer */ public static function processHeadingEnterMetadata($data, Doku_Renderer_metadata $renderer) { $state = $data[PluginUtility::STATE]; if (!in_array($state, [DOKU_LEXER_ENTER, DOKU_LEXER_SPECIAL])) { return; } /** * Only outline heading metadata * Not component heading */ $context = $data[PluginUtility::CONTEXT]; if ($context === self::TYPE_OUTLINE) { $callStackArray = $data[PluginUtility::ATTRIBUTES]; $tagAttributes = TagAttributes::createFromCallStackArray($callStackArray); $text = $tagAttributes->getValue(HeadingTag::HEADING_TEXT_ATTRIBUTE); if ($text !== null) { $text = trim($text); } $level = $tagAttributes->getValue(HeadingTag::LEVEL); $pos = 0; // mandatory for header but not for metadata, we set 0 to make the code analyser happy $renderer->header($text, $level, $pos); if ($level === 1) { $parsedLabel = $tagAttributes->getValue(self::PARSED_LABEL); $renderer->meta[PageH1::H1_PARSED] = $parsedLabel; } } } public static function processMetadataAnalytics(array $data, renderer_plugin_combo_analytics $renderer) { $state = $data[PluginUtility::STATE]; if ($state !== DOKU_LEXER_ENTER) { return; } /** * Only outline heading metadata * Not component heading */ $context = $data[PluginUtility::CONTEXT] ?? null; if ($context === self::TYPE_OUTLINE) { $callStackArray = $data[PluginUtility::ATTRIBUTES]; $tagAttributes = TagAttributes::createFromCallStackArray($callStackArray); $text = $tagAttributes->getValue(HeadingTag::HEADING_TEXT_ATTRIBUTE); $level = $tagAttributes->getValue(HeadingTag::LEVEL); $renderer->header($text, $level, 0); } } /** * @param string $context * @param TagAttributes $tagAttributes * @param Doku_Renderer_xhtml $renderer * @param int|null $pos - null if the call was generated * @return void */ public static function processRenderEnterXhtml(string $context, TagAttributes $tagAttributes, Doku_Renderer_xhtml &$renderer, ?int $pos) { /** * All correction that are dependent * on the markup (ie title or heading) * are done in the {@link self::processRenderEnterXhtml()} */ /** * Variable */ $type = $tagAttributes->getType(); /** * Old syntax deprecated */ if ($type === "0") { if ($context === self::TYPE_OUTLINE) { $type = 'h' . self::DEFAULT_LEVEL_OUTLINE_CONTEXT; } else { $type = 'h' . self::DEFAULT_LEVEL_TITLE_CONTEXT; } } /** * Label is for the TOC */ $tagAttributes->removeAttributeIfPresent(self::PARSED_LABEL); /** * Level */ $level = $tagAttributes->getValueAndRemove(HeadingTag::LEVEL); /** * Display Heading * https://getbootstrap.com/docs/5.0/content/typography/#display-headings */ if ($context !== self::TYPE_OUTLINE && $type === null) { /** * if not an outline, a display */ $type = "h$level"; } if (in_array($type, self::DISPLAY_TYPES)) { $displayClass = "display-$level"; if (Bootstrap::getBootStrapMajorVersion() == Bootstrap::BootStrapFourMajorVersion) { /** * Make Bootstrap display responsive */ PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(HeadingTag::DISPLAY_BS_4_RESPONSIVE_SNIPPET_ID); if (in_array($type, self::DISPLAY_TYPES_ONLY_BS_5)) { $displayClass = "display-4"; 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); } } $tagAttributes->addClassName($displayClass); } /** * Heading class * https://getbootstrap.com/docs/5.0/content/typography/#headings * Works on 4 and 5 */ if (in_array($type, self::HEADING_TYPES)) { $tagAttributes->addClassName($type); } /** * Card title Context class * TODO: should move to card */ if (in_array($context, [BlockquoteTag::TAG, CardTag::CARD_TAG])) { $tagAttributes->addClassName("card-title"); } $executionContext = ExecutionContext::getActualOrCreateFromEnv(); /** * Add an outline class to be able to style them at once * * The context is by default the parent name or outline. */ $snippetManager = SnippetSystem::getFromContext(); if ($context === self::TYPE_OUTLINE) { $tagAttributes->addClassName(Outline::getOutlineHeadingClass()); $snippetManager->attachCssInternalStyleSheet(self::TYPE_OUTLINE); // numbering try { $enable = $executionContext ->getConfig() ->getValue(Outline::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT); if ($enable) { $snippet = $snippetManager->attachCssInternalStyleSheet(Outline::OUTLINE_HEADING_NUMBERING); if (!$snippet->hasInlineContent()) { $css = Outline::getCssNumberingRulesFor(Outline::OUTLINE_HEADING_NUMBERING); $snippet->setInlineContent($css); } } } catch (ExceptionBadSyntax $e) { LogUtility::internalError("An error has occurred while trying to add the outline heading numbering stylesheet.", self::CANONICAL, $e); } catch (ExceptionNotEnabled $e) { // ok } /** * Anchor on id */ $snippetManager = PluginUtility::getSnippetManager(); try { $snippetManager->attachRemoteJavascriptLibrary( Outline::OUTLINE_ANCHOR, "https://cdn.jsdelivr.net/npm/anchor-js@4.3.0/anchor.min.js", "sha256-LGOWMG4g6/zc0chji4hZP1d8RxR2bPvXMzl/7oPZqjs=" ); } catch (ExceptionBadArgument|ExceptionBadSyntax $e) { // The url has a file name. this error should not happen LogUtility::internalError("Unable to add anchor. Error:{$e->getMessage()}", Outline::OUTLINE_ANCHOR); } $snippetManager->attachJavascriptFromComponentId(Outline::OUTLINE_ANCHOR); } $snippetManager->attachCssInternalStyleSheet(HeadingTag::HEADING_TAG); /** * Not a HTML attribute */ $tagAttributes->removeComponentAttributeIfPresent(self::HEADING_TEXT_ATTRIBUTE); /** * Two headings 1 are shown * * We delete the heading 1 in the instructions * if the template has a content header * * The instructions may not be reprocessed after upgrade for instance * when the installation is done manually * * To avoid to have two headings, we set a display none if this is the case * * Note that this should only apply on the document and not on a partial but yeah * We go that h1 is not used in partials. */ if ($level === 1) { $hasMainHeaderElement = TemplateForWebPage::create() ->setRequestedContextPath(ExecutionContext::getActualOrCreateFromEnv()->getContextPath()) ->hasElement(TemplateSlot::MAIN_HEADER_ID); if ($hasMainHeaderElement) { $tagAttributes->addClassName("d-none"); } } /** * Printing */ $tag = self::getTagFromContext($context, $level); $renderer->doc .= $tagAttributes->toHtmlEnterTag($tag); } /** * @param TagAttributes $tagAttributes * @param string $context * @return string */ public static function renderClosingTag(TagAttributes $tagAttributes, string $context): string { $level = $tagAttributes->getValueAndRemove(HeadingTag::LEVEL); if ($level == null) { LogUtility::msg("The level is mandatory when closing a heading", self::CANONICAL); } $tag = self::getTagFromContext($context, $level); return ""; } /** * Reduce the end of the input string * to the first opening tag without the ">" * and returns the closing tag * * @param $input * @return array - the heading attributes as a string */ public static function reduceToFirstOpeningTagAndReturnAttributes(&$input) { // the variable that will capture the attribute string $headingStartTagString = ""; // Set to true when the heading tag has completed $endHeadingParsed = false; // The closing character `>` indicator of the start and end tag // true when found $endTagClosingCharacterParsed = false; $startTagClosingCharacterParsed = false; // We start from the edn $position = strlen($input) - 1; while ($position > 0) { $character = $input[$position]; if ($character == "<") { if (!$endHeadingParsed) { // We are at the beginning of the ending tag $endHeadingParsed = true; } else { // We have delete all character until the heading start tag // add the last one and exit $headingStartTagString = $character . $headingStartTagString; break; } } if ($character == ">") { if (!$endTagClosingCharacterParsed) { // We are at the beginning of the ending tag $endTagClosingCharacterParsed = true; } else { // We have delete all character until the heading start tag $startTagClosingCharacterParsed = true; } } if ($startTagClosingCharacterParsed) { $headingStartTagString = $character . $headingStartTagString; } // position -- $position--; } $input = substr($input, 0, $position); if (!empty($headingStartTagString)) { return PluginUtility::getTagAttributes($headingStartTagString); } else { LogUtility::msg("The attributes of the heading are empty and this should not be possible"); return []; } } public static function handleEnter(\Doku_Handler $handler, TagAttributes $tagAttributes, string $markupTag): array { /** * Context determination */ $callStack = CallStack::createFromHandler($handler); $context = HeadingTag::getContext($callStack); /** * Level is mandatory (for the closing tag) */ $level = $tagAttributes->getValue(HeadingTag::LEVEL); if ($level === null) { /** * Old title type * from 1 to 4 to set the display heading */ $type = $tagAttributes->getType(); if (is_numeric($type) && $type != 0) { $level = $type; if ($markupTag === self::TITLE_TAG) { $type = "d$level"; } else { $type = "h$level"; } $tagAttributes->setType($type); } /** * Still null, check the type */ if ($level == null) { if (in_array($type, HeadingTag::getAllTypes())) { $level = substr($type, 1); } } /** * Still null, default level */ if ($level == null) { if ($context === HeadingTag::TYPE_OUTLINE) { $level = HeadingTag::DEFAULT_LEVEL_OUTLINE_CONTEXT; } else { $level = HeadingTag::DEFAULT_LEVEL_TITLE_CONTEXT; } } /** * Set the level */ $tagAttributes->addComponentAttributeValue(HeadingTag::LEVEL, $level); } return [PluginUtility::CONTEXT => $context]; } public static function getAllTypes(): array { return array_merge( self::DISPLAY_TYPES, self::HEADING_TYPES, self::SHORT_TYPES, self::TITLE_DISPLAY_TYPES ); } /** * @param string $context * @param int $level * @return string */ private static function getTagFromContext(string $context, int $level): string { if ($context === self::TYPE_OUTLINE) { return "h$level"; } else { return "div"; } } }