1<?php 2 3 4// must be run within Dokuwiki 5use ComboStrap\Brand; 6use ComboStrap\BrandButton; 7use ComboStrap\CacheDependencies; 8use ComboStrap\CacheManager; 9use ComboStrap\Call; 10use ComboStrap\CallStack; 11use ComboStrap\ColorRgb; 12use ComboStrap\Dimension; 13use ComboStrap\ExceptionCombo; 14use ComboStrap\Icon; 15use ComboStrap\LogUtility; 16use ComboStrap\Page; 17use ComboStrap\PluginUtility; 18use ComboStrap\TagAttributes; 19use ComboStrap\Template; 20use ComboStrap\TemplateUtility; 21 22if (!defined('DOKU_INC')) die(); 23 24 25class syntax_plugin_combo_brand extends DokuWiki_Syntax_Plugin 26{ 27 28 const TAG = "brand"; 29 const CANONICAL = self::TAG; 30 31 32 public const ICON_ATTRIBUTE = "icon"; 33 34 public const URL_ATTRIBUTE = "url"; 35 36 /** 37 * Class needed 38 * https://getbootstrap.com/docs/5.1/components/navbar/#image-and-text 39 */ 40 const BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS = "d-inline-block align-text-top"; 41 42 const WIDGET_ATTRIBUTE = "widget"; 43 44 const BRAND_IMAGE_FOUND_INDICATOR = "brand_image_found"; 45 const BRAND_TEXT_FOUND_INDICATOR = "brand_text_found"; 46 47 48 public static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes) 49 { 50 $linkArrayAttributes = $tagAttributes->toCallStackArray(); 51 $linkArrayAttributes[TagAttributes::TYPE_KEY] = $tagAttributes->getLogicalTag(); 52 $linkAttributes = TagAttributes::createFromCallStackArray($linkArrayAttributes); 53 syntax_plugin_combo_link::addOpenLinkTagInCallStack($callStack, $linkAttributes); 54 } 55 56 /** 57 * @throws ExceptionCombo 58 */ 59 public static function mixBrandButtonToTagAttributes(TagAttributes $tagAttributes, BrandButton $brandButton) 60 { 61 $brandLinkAttributes = $brandButton->getLinkAttributes(); 62 $urlAttribute = syntax_plugin_combo_brand::URL_ATTRIBUTE; 63 $url = $tagAttributes->getValueAndRemoveIfPresent($urlAttribute); 64 if ($url !== null) { 65 $urlTemplate = Template::create($url); 66 $variableDetected = $urlTemplate->getVariablesDetected(); 67 if (sizeof($variableDetected) === 1 && $variableDetected[0] === "path") { 68 CacheManager::getOrCreate()->addDependencyForCurrentSlot(CacheDependencies::REQUESTED_PAGE_DEPENDENCY); 69 $page = Page::createPageFromRequestedPage(); 70 $relativePath = str_replace(":", "/", $page->getDokuwikiId()); 71 $url = $urlTemplate 72 ->set("path", $relativePath) 73 ->render(); 74 } 75 $tagAttributes->addOutputAttributeValue("href", $url); 76 } 77 $tagAttributes->mergeWithCallStackArray($brandLinkAttributes->toCallStackArray()); 78 } 79 80 81 /** 82 * An utility constructor to be sure that we build the brand button 83 * with the same data in the handle and render function 84 * @throws ExceptionCombo 85 */ 86 public static function createButtonFromAttributes(TagAttributes $brandAttributes, $type = BrandButton::TYPE_BUTTON_BRAND): BrandButton 87 { 88 $brandName = $brandAttributes->getValue(TagAttributes::TYPE_KEY, Brand::CURRENT_BRAND); 89 $widget = $brandAttributes->getValue(self::WIDGET_ATTRIBUTE, BrandButton::WIDGET_BUTTON_VALUE); 90 $icon = $brandAttributes->getValue(self::ICON_ATTRIBUTE, BrandButton::ICON_SOLID_VALUE); 91 92 $brandButton = (new BrandButton($brandName, $type)) 93 ->setWidget($widget) 94 ->setIconType($icon); 95 96 $width = $brandAttributes->getValueAsInteger(Dimension::WIDTH_KEY); 97 if ($width !== null) { 98 $brandButton->setWidth($width); 99 } 100 $title = $brandAttributes->getValue(syntax_plugin_combo_link::TITLE_ATTRIBUTE); 101 if ($title !== null) { 102 $brandButton->setLinkTitle($title); 103 } 104 $color = $brandAttributes->getValue(ColorRgb::PRIMARY_VALUE); 105 if ($color !== null) { 106 $brandButton->setPrimaryColor($color); 107 } 108 $secondaryColor = $brandAttributes->getValue(ColorRgb::SECONDARY_VALUE); 109 if ($secondaryColor !== null) { 110 $brandButton->setSecondaryColor($secondaryColor); 111 } 112 $handle = $brandAttributes->getValue(syntax_plugin_combo_follow::HANDLE_ATTRIBUTE); 113 if ($handle !== null) { 114 $brandButton->setHandle($handle); 115 } 116 return $brandButton; 117 } 118 119 /** 120 * Syntax Type. 121 * 122 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 123 * @see DokuWiki_Syntax_Plugin::getType() 124 */ 125 function getType(): string 126 { 127 return 'substition'; 128 } 129 130 /** 131 * How Dokuwiki will add P element 132 * 133 * * 'normal' - The plugin can be used inside paragraphs 134 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 135 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 136 * 137 * @see DokuWiki_Syntax_Plugin::getPType() 138 */ 139 function getPType(): string 140 { 141 return 'normal'; 142 } 143 144 /** 145 * @return array 146 * Allow which kind of plugin inside 147 * 148 * array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 149 * 150 */ 151 function getAllowedTypes(): array 152 { 153 return array('baseonly', 'formatting', 'substition', 'protected', 'disabled'); 154 } 155 156 function getSort(): int 157 { 158 return 201; 159 } 160 161 public 162 function accepts($mode): bool 163 { 164 return syntax_plugin_combo_preformatted::disablePreformatted($mode); 165 } 166 167 168 /** 169 * Create a pattern that will called this plugin 170 * 171 * @param string $mode 172 * @see Doku_Parser_Mode::connectTo() 173 */ 174 function connectTo($mode) 175 { 176 177 $pattern = PluginUtility::getContainerTagPattern(self::getTag()); 178 $this->Lexer->addEntryPattern($pattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent()); 179 180 /** 181 * The empty tag pattern should be after the container pattern 182 */ 183 $this->Lexer->addSpecialPattern(PluginUtility::getEmptyTagPattern(self::TAG), $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 184 185 } 186 187 function postConnect() 188 { 189 190 $this->Lexer->addExitPattern('</' . self::getTag() . '>', 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent()); 191 192 } 193 194 function handle($match, $state, $pos, Doku_Handler $handler) 195 { 196 197 198 switch ($state) { 199 200 case DOKU_LEXER_SPECIAL : 201 case DOKU_LEXER_ENTER : 202 203 /** 204 * Context 205 */ 206 $callStack = CallStack::createFromHandler($handler); 207 $parent = $callStack->moveToParent(); 208 $context = null; 209 if ($parent !== false) { 210 $context = $parent->getTagName(); 211 } 212 213 /** 214 * Default parameters, type definition and parsing 215 */ 216 if ($context === syntax_plugin_combo_menubar::TAG) { 217 $defaultWidget = BrandButton::WIDGET_LINK_VALUE; 218 } else { 219 $defaultWidget = BrandButton::WIDGET_BUTTON_VALUE; 220 } 221 $defaultParameters[TagAttributes::TYPE_KEY] = Brand::CURRENT_BRAND; 222 $defaultParameters[self::WIDGET_ATTRIBUTE] = $defaultWidget; 223 $knownTypes = null; 224 $tagAttributes = TagAttributes::createFromTagMatch($match, $defaultParameters, $knownTypes) 225 ->setLogicalTag(self::TAG); 226 227 228 return array( 229 PluginUtility::STATE => $state, 230 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 231 PluginUtility::CONTEXT => $context 232 ); 233 234 case DOKU_LEXER_UNMATCHED : 235 return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 236 237 case DOKU_LEXER_EXIT : 238 239 $callStack = CallStack::createFromHandler($handler); 240 $openTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 241 $openTagAttributes = TagAttributes::createFromCallStackArray($openTag->getAttributes()); 242 $openTagContext = $openTag->getContext(); 243 /** 244 * Old syntax 245 * An icon/image could be already inside 246 * We go from end to start to 247 * see if there is also a text, if this is the case, 248 * there is a class added on the media 249 */ 250 $markupIconImageFound = false; 251 $textFound = false; 252 $callStack->moveToEnd(); 253 while ($actualCall = $callStack->previous()) { 254 $tagName = $actualCall->getTagName(); 255 if (in_array($tagName, [syntax_plugin_combo_icon::TAG, syntax_plugin_combo_media::TAG])) { 256 257 258 if ($textFound && $openTagContext === syntax_plugin_combo_menubar::TAG) { 259 // if text and icon 260 // We add it here because, if they are present, we don't add them later 261 // for all on raster image 262 $actualCall->addClassName(self::BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS); 263 } 264 265 // is it a added call / no content 266 // or is it an icon from the markup 267 if ($actualCall->getCapturedContent() === null) { 268 269 // It's an added call 270 // No user icon, image can be found anymore 271 // exiting 272 break; 273 } 274 275 $primary = $openTagAttributes->getValue(ColorRgb::PRIMARY_VALUE); 276 if ($primary !== null && $tagName === syntax_plugin_combo_icon::TAG) { 277 try { 278 $brandButton = self::createButtonFromAttributes($openTagAttributes); 279 $actualCall->addAttribute(ColorRgb::COLOR, $brandButton->getTextColor()); 280 } catch (ExceptionCombo $e) { 281 LogUtility::msg("Error while trying to set the icon color on exit. Error: {$e->getMessage()}"); 282 } 283 } 284 285 $markupIconImageFound = true; 286 } 287 if ($actualCall->getState() === DOKU_LEXER_UNMATCHED) { 288 $textFound = true; 289 } 290 } 291 $openTag->setPluginData(self::BRAND_IMAGE_FOUND_INDICATOR, $markupIconImageFound); 292 $openTag->setPluginData(self::BRAND_TEXT_FOUND_INDICATOR, $textFound); 293 294 return array( 295 PluginUtility::STATE => $state 296 ); 297 298 299 } 300 return array(); 301 302 } 303 304 /** 305 * Render the output 306 * @param string $format 307 * @param Doku_Renderer $renderer 308 * @param array $data - what the function handle() return 309 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 310 * @see DokuWiki_Syntax_Plugin::render() 311 * 312 * 313 */ 314 function render($format, Doku_Renderer $renderer, $data): bool 315 { 316 317 if ($format === "xhtml") { 318 $state = $data[PluginUtility::STATE]; 319 switch ($state) { 320 case DOKU_LEXER_SPECIAL: 321 case DOKU_LEXER_ENTER: 322 323 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 324 /** 325 * Brand Object creation 326 */ 327 $brandName = $tagAttributes->getType(); 328 try { 329 $brandButton = self::createButtonFromAttributes($tagAttributes); 330 } catch (ExceptionCombo $e) { 331 $renderer->doc .= LogUtility::wrapInRedForHtml("Error while reading the brand data for the brand ($brandName). Error: {$e->getMessage()}"); 332 return false; 333 } 334 /** 335 * Link 336 */ 337 try { 338 self::mixBrandButtonToTagAttributes($tagAttributes, $brandButton); 339 } catch (ExceptionCombo $e) { 340 $renderer->doc .= LogUtility::wrapInRedForHtml("Error while getting the link data for the the brand ($brandName). Error: {$e->getMessage()}"); 341 return false; 342 } 343 $context = $data[PluginUtility::CONTEXT]; 344 if ($context === syntax_plugin_combo_menubar::TAG) { 345 $tagAttributes->addOutputAttributeValue("accesskey", "h"); 346 $tagAttributes->addClassName("navbar-brand"); 347 } 348 // Width does not apply to link (otherwise the link got a max-width of 30) 349 $tagAttributes->removeComponentAttributeIfPresent(Dimension::WIDTH_KEY); 350 // Widget also 351 $tagAttributes->removeComponentAttributeIfPresent(self::WIDGET_ATTRIBUTE); 352 $renderer->doc .= $tagAttributes 353 ->setType(self::CANONICAL) 354 ->setLogicalTag(syntax_plugin_combo_link::TAG) 355 ->toHtmlEnterTag("a"); 356 357 358 /** 359 * Logo 360 */ 361 $brandImageFound = $data[self::BRAND_IMAGE_FOUND_INDICATOR]; 362 if (!$brandImageFound && $brandButton->hasIcon()) { 363 try { 364 $iconAttributes = $brandButton->getIconAttributes(); 365 $textFound = $data[self::BRAND_TEXT_FOUND_INDICATOR]; 366 $name = $iconAttributes[\syntax_plugin_combo_icon::ICON_NAME_ATTRIBUTE]; 367 $iconAttributes = TagAttributes::createFromCallStackArray($iconAttributes); 368 if ($textFound && $context === syntax_plugin_combo_menubar::TAG) { 369 $iconAttributes->addClassName(self::BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS); 370 } 371 $renderer->doc .= Icon::create($name, $iconAttributes) 372 ->render(); 373 } catch (ExceptionCombo $e) { 374 375 if ($brandButton->getBrand()->getName() === Brand::CURRENT_BRAND) { 376 377 $documentationLink = PluginUtility::getDocumentationHyperLink("logo", "documentation"); 378 LogUtility::msg("A svg logo icon is not installed on your website. Check the corresponding $documentationLink.", LogUtility::LVL_MSG_INFO); 379 380 } else { 381 382 $renderer->doc .= "The brand icon returns an error. Error: {$e->getMessage()}"; 383 // we don't return because the link is not closed 384 385 } 386 387 } 388 } 389 390 /** 391 * End of link 392 */ 393 if ($state === DOKU_LEXER_SPECIAL) { 394 $renderer->doc .= "</a>"; 395 } 396 397 /** 398 * Add the Icon / CSS / Javascript snippet 399 * 400 */ 401 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 402 try { 403 $brandButton = self::createButtonFromAttributes($tagAttributes); 404 } catch (ExceptionCombo $e) { 405 LogUtility::msg("The brand could not be build. Error: {$e->getMessage()}"); 406 return false; 407 } 408 try { 409 $style = $brandButton->getStyle(); 410 } catch (ExceptionCombo $e) { 411 LogUtility::msg("The style of the {$this->getType()} button ($brandButton) could not be determined. Error: {$e->getMessage()}"); 412 return false; 413 } 414 $snippetId = $brandButton->getStyleScriptIdentifier(); 415 PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot($snippetId, $style); 416 break; 417 case DOKU_LEXER_UNMATCHED: 418 $renderer->doc .= PluginUtility::renderUnmatched($data); 419 break; 420 case DOKU_LEXER_EXIT: 421 $renderer->doc .= "</a>"; 422 break; 423 424 } 425 return true; 426 } 427 428 // unsupported $mode 429 return false; 430 } 431 432 public 433 static function getTag(): string 434 { 435 return self::TAG; 436 } 437 438 /** 439 * 440 * @throws ExceptionCombo 441 */ 442 public 443 static function addIconInCallStack(CallStack $callStack, BrandButton $brandButton) 444 { 445 446 if (!$brandButton->hasIcon()) { 447 return; 448 } 449 $iconAttributes = $brandButton->getIconAttributes(); 450 451 $callStack->appendCallAtTheEnd( 452 Call::createComboCall( 453 syntax_plugin_combo_icon::TAG, 454 DOKU_LEXER_SPECIAL, 455 $iconAttributes 456 )); 457 } 458 459} 460 461