1<?php 2/** 3 * DokuWiki Syntax Plugin Combostrap. 4 * 5 */ 6 7use ComboStrap\CallStack; 8use ComboStrap\ColorRgb; 9use ComboStrap\Dimension; 10use ComboStrap\DokuPath; 11use ComboStrap\ExceptionCombo; 12use ComboStrap\FileSystems; 13use ComboStrap\Icon; 14use ComboStrap\LogUtility; 15use ComboStrap\PluginUtility; 16use ComboStrap\Site; 17use ComboStrap\SvgDocument; 18use ComboStrap\TagAttributes; 19 20 21require_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 22 23/** 24 * All DokuWiki plugins to extend the parser/rendering mechanism 25 * need to inherit from this class 26 * 27 * The name of the class must follow a pattern (don't change it) 28 * ie: 29 * syntax_plugin_PluginName_ComponentName 30 * 31 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 32 * !!!!!!!!!!! The component name must be the name of the php file !!! 33 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 34 * 35 * https://icons.getbootstrap.com/ 36 * https://remixicon.com/ 37 */ 38class syntax_plugin_combo_icon extends DokuWiki_Syntax_Plugin 39{ 40 const TAG = "icon"; 41 const CANONICAL = self::TAG; 42 const ICON_NAME_ATTRIBUTE = "name"; 43 44 private static function exceptionHandling(Exception $e, $tagAttribute): string 45 { 46 $errorClass = syntax_plugin_combo_media::SVG_RENDERING_ERROR_CLASS; 47 $message = "Icon ({$tagAttribute->getValue("name")}). Error while rendering: {$e->getMessage()}"; 48 $html = "<span class=\"text-alert $errorClass\">" . hsc(trim($message)) . "</span>"; 49 if (!PluginUtility::isTest()) { 50 LogUtility::msg($message, LogUtility::LVL_MSG_WARNING, self::CANONICAL); 51 } 52 return $html; 53 } 54 55 56 /** 57 * Syntax Type. 58 * 59 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 60 * @see DokuWiki_Syntax_Plugin::getType() 61 */ 62 function getType() 63 { 64 return 'substition'; 65 } 66 67 /** 68 * @return array 69 * Allow which kind of plugin inside 70 * 71 * No one of array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 72 * because we manage self the content and we call self the parser 73 */ 74 public function getAllowedTypes() 75 { 76 // You can't put anything in a icon 77 return array('formatting'); 78 } 79 80 /** 81 * How Dokuwiki will add P element 82 * 83 * * 'normal' - The plugin can be used inside paragraphs 84 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 85 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 86 * 87 * @see DokuWiki_Syntax_Plugin::getPType() 88 */ 89 function getPType() 90 { 91 return 'normal'; 92 } 93 94 /** 95 * @see Doku_Parser_Mode::getSort() 96 * the mode with the lowest sort number will win out 97 * the lowest in the tree must have the lowest sort number 98 * No idea why it must be low but inside a teaser, it will work 99 * https://www.dokuwiki.org/devel:parser#order_of_adding_modes_important 100 */ 101 function getSort() 102 { 103 return 10; 104 } 105 106 /** 107 * Create a pattern that will called this plugin 108 * 109 * @param string $mode 110 * @see Doku_Parser_Mode::connectTo() 111 */ 112 function connectTo($mode) 113 { 114 115 116 $specialPattern = PluginUtility::getEmptyTagPattern(self::TAG); 117 $this->Lexer->addSpecialPattern($specialPattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 118 119 /** 120 * The content is used to add a {@link syntax_plugin_combo_tooltip} 121 */ 122 $entryPattern = PluginUtility::getContainerTagPattern(self::TAG); 123 $this->Lexer->addEntryPattern($entryPattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 124 125 126 } 127 128 public function postConnect() 129 { 130 $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent())); 131 } 132 133 134 /** 135 * 136 * The handle function goal is to parse the matched syntax through the pattern function 137 * and to return the result for use in the renderer 138 * This result is always cached until the page is modified. 139 * @param string $match 140 * @param int $state 141 * @param int $pos 142 * @param Doku_Handler $handler 143 * @return array|bool 144 * @throws Exception 145 * @see DokuWiki_Syntax_Plugin::handle() 146 * 147 */ 148 function handle($match, $state, $pos, Doku_Handler $handler) 149 { 150 151 switch ($state) { 152 153 case DOKU_LEXER_SPECIAL: 154 case DOKU_LEXER_ENTER: 155 // Get the parameters 156 $knownTypes = []; 157 $defaultAttributes = []; 158 $tagAttributes = TagAttributes::createFromTagMatch($match, $defaultAttributes, $knownTypes); 159 $callStack = CallStack::createFromHandler($handler); 160 $parent = $callStack->moveToParent(); 161 $context = ""; 162 if ($parent !== false) { 163 $context = $parent->getTagName(); 164 if ($context === syntax_plugin_combo_link::TAG) { 165 $context = $parent->getTagName(); 166 } 167 } 168 /** 169 * Color setting should know the color of its parent 170 * For now, we don't set any color if the parent is a button, note, link 171 * As a header is not a parent, we may say that if the icon is contained, the default 172 * branding color is not set ? 173 */ 174 $requestedColor = $tagAttributes->getValue(ColorRgb::COLOR); 175 if ( 176 $requestedColor === null && 177 Site::isBrandingColorInheritanceEnabled() && 178 !in_array($context, [ 179 syntax_plugin_combo_button::TAG, 180 syntax_plugin_combo_note::TAG, 181 syntax_plugin_combo_link::TAG 182 ]) 183 ) { 184 $requestedWidth = $tagAttributes->getValue(Dimension::WIDTH_KEY, SvgDocument::DEFAULT_ICON_WIDTH); 185 $requestedWidthInPx = Dimension::toPixelValue($requestedWidth); 186 if ($requestedWidthInPx > 36) { 187 // Illustrative icon 188 $color = Site::getPrimaryColor(); 189 } else { 190 // Character icon 191 $color = Site::getSecondaryColor(); 192 } 193 if ($color !== null) { 194 $tagAttributes->setComponentAttributeValue(ColorRgb::COLOR, $color); 195 } 196 } 197 return array( 198 PluginUtility::STATE => $state, 199 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 200 PluginUtility::CONTEXT => $context 201 ); 202 case DOKU_LEXER_EXIT: 203 $callStack = CallStack::createFromHandler($handler); 204 $openingCall = $callStack->moveToPreviousCorrespondingOpeningCall(); 205 return array( 206 PluginUtility::STATE => $state, 207 PluginUtility::ATTRIBUTES => $openingCall->getAttributes(), 208 PluginUtility::CONTEXT => $openingCall->getContext() 209 ); 210 211 212 } 213 214 return array(); 215 216 } 217 218 /** 219 * Render the output 220 * @param string $format 221 * @param Doku_Renderer $renderer 222 * @param array $data - what the function handle() return'ed 223 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 224 * @see DokuWiki_Syntax_Plugin::render() 225 * 226 * 227 */ 228 function render($format, Doku_Renderer $renderer, $data): bool 229 { 230 231 switch ($format) { 232 233 case 'xhtml': 234 { 235 /** @var Doku_Renderer_xhtml $renderer */ 236 $state = $data[PluginUtility::STATE]; 237 switch ($state) { 238 239 240 case DOKU_LEXER_SPECIAL: 241 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 242 $renderer->doc .= $this->printIcon($tagAttributes); 243 break; 244 case DOKU_LEXER_ENTER: 245 246 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 247 $tooltip = $tagAttributes->getValueAndRemoveIfPresent(\ComboStrap\Tooltip::TOOLTIP_ATTRIBUTE); 248 if ($tooltip !== null) { 249 /** 250 * If there is a tooltip, we need 251 * to start with a span to wrap the svg with it 252 */ 253 254 255 $tooltipTag = TagAttributes::createFromCallStackArray([\ComboStrap\Tooltip::TOOLTIP_ATTRIBUTE => $tooltip]) 256 ->addClassName(syntax_plugin_combo_tooltip::TOOLTIP_CLASS_INLINE_BLOCK); 257 $renderer->doc .= $tooltipTag->toHtmlEnterTag("span"); 258 } 259 /** 260 * Print the icon 261 */ 262 $renderer->doc .= $this->printIcon($tagAttributes); 263 /** 264 * Close the span if we are in a tooltip context 265 */ 266 if ($tooltip !== null) { 267 $renderer->doc .= "</span>"; 268 } 269 270 break; 271 case DOKU_LEXER_EXIT: 272 273 break; 274 } 275 276 } 277 break; 278 case 'metadata': 279 /** 280 * @var Doku_Renderer_metadata $renderer 281 */ 282 $tagAttribute = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 283 try { 284 $name = $tagAttribute->getValueAndRemoveIfPresent("name"); 285 if ($name === null) { 286 throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::CANONICAL); 287 } 288 $mediaPath = Icon::create($name, $tagAttribute)->getPath(); 289 } catch (ExceptionCombo $e) { 290 // error is already fired in the renderer 291 return false; 292 } 293 if ($mediaPath instanceof DokuPath && FileSystems::exists($mediaPath)) { 294 $mediaId = $mediaPath->getDokuwikiId(); 295 syntax_plugin_combo_media::registerFirstMedia($renderer, $mediaId); 296 } 297 break; 298 299 } 300 return true; 301 } 302 303 /** 304 * @param TagAttributes $tagAttributes 305 * @return string 306 */ 307 private function printIcon(TagAttributes $tagAttributes): string 308 { 309 try { 310 $name = $tagAttributes->getValue("name"); 311 if ($name === null) { 312 throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::CANONICAL); 313 } 314 return Icon::create($name, $tagAttributes) 315 ->render(); 316 } catch (ExceptionCombo $e) { 317 return self::exceptionHandling($e, $tagAttributes); 318 } 319 } 320 321 322} 323