1<?php 2 3 4require_once(__DIR__ . "/../ComboStrap/PluginUtility.php"); 5 6use ComboStrap\CallStack; 7use ComboStrap\LinkUtility; 8use ComboStrap\PluginUtility; 9use ComboStrap\TagAttributes; 10use ComboStrap\ThirdPartyPlugins; 11 12if (!defined('DOKU_INC')) die(); 13 14/** 15 * 16 * A link pattern to take over the link of Dokuwiki 17 * and transform it as a bootstrap link 18 * 19 * The handle of the move of link is to be found in the 20 * admin action {@link action_plugin_combo_linkmove} 21 * 22 */ 23class syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin 24{ 25 const TAG = 'link'; 26 const COMPONENT = 'combo_link'; 27 28 /** 29 * Disable the link component 30 */ 31 const CONF_DISABLE_LINK = "disableLink"; 32 33 /** 34 * The link Tag 35 * a or p 36 */ 37 const LINK_TAG = "linkTag"; 38 39 /** 40 * Do the link component allows to be spawn on multilines 41 */ 42 const CLICKABLE_ATTRIBUTE = "clickable"; 43 44 45 /** 46 * Syntax Type. 47 * 48 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 49 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 50 */ 51 function getType() 52 { 53 return 'substition'; 54 } 55 56 /** 57 * How Dokuwiki will add P element 58 * 59 * * 'normal' - The plugin can be used inside paragraphs 60 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 61 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 62 * 63 * @see DokuWiki_Syntax_Plugin::getPType() 64 */ 65 function getPType() 66 { 67 return 'normal'; 68 } 69 70 /** 71 * @return array 72 * Allow which kind of plugin inside 73 * 74 * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 75 * because we manage self the content and we call self the parser 76 */ 77 function getAllowedTypes() 78 { 79 return array('substition', 'formatting', 'disabled'); 80 } 81 82 /** 83 * @param string $mode 84 * @return bool 85 * Accepts inside 86 */ 87 public function accepts($mode) 88 { 89 /** 90 * To avoid that the description if it contains a link 91 * will be taken by the links mode 92 * 93 * For instance, [[https://hallo|https://hallo]] will send https://hallo 94 * to the external link mode 95 */ 96 $linkModes = [ 97 "externallink", 98 "locallink", 99 "internallink", 100 "interwikilink", 101 "emaillink", 102 "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description 103 //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet 104 //"emphasis_close", 105 //"acrnonym" 106 ]; 107 if (in_array($mode, $linkModes)) { 108 return false; 109 } else { 110 return true; 111 } 112 } 113 114 115 /** 116 * @see Doku_Parser_Mode::getSort() 117 * The mode with the lowest sort number will win out 118 */ 119 function getSort() 120 { 121 /** 122 * It should be less than the number 123 * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort} 124 * and the like 125 * 126 * For whatever reason, the number below should be less than 100, 127 * otherwise on windows with DokuWiki Stick, the link syntax may be not taken 128 * into account 129 */ 130 return 99; 131 } 132 133 134 function connectTo($mode) 135 { 136 137 if (!$this->getConf(self::CONF_DISABLE_LINK, false) 138 && 139 $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME) 140 ) { 141 142 $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE; 143 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 144 145 } 146 147 } 148 149 public function postConnect() 150 { 151 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 152 $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent())); 153 } 154 } 155 156 157 /** 158 * The handler for an internal link 159 * based on `internallink` in {@link Doku_Handler} 160 * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 161 * the parameters (ie for instance internallink) 162 * @param string $match 163 * @param int $state 164 * @param int $pos 165 * @param Doku_Handler $handler 166 * @return array|bool 167 */ 168 function handle($match, $state, $pos, Doku_Handler $handler) 169 { 170 171 172 switch ($state) { 173 case DOKU_LEXER_ENTER: 174 $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match)); 175 $callStack = CallStack::createFromHandler($handler); 176 177 178 $parent = $callStack->moveToParent(); 179 $parentName = ""; 180 if ($parent != false) { 181 182 /** 183 * Button Link 184 * Getting the attributes 185 */ 186 $parentName = $parent->getTagName(); 187 if ($parentName == syntax_plugin_combo_button::TAG) { 188 $tagAttributes->mergeWithCallStackArray($parent->getAttributes()); 189 } 190 191 /** 192 * Searching Clickable parent 193 */ 194 $maxLevel = 3; 195 $level = 0; 196 while ( 197 $parent != false && 198 !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 199 $level < $maxLevel 200 ) { 201 $parent = $callStack->moveToParent(); 202 $level++; 203 } 204 if ($parent != false) { 205 if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 206 $tagAttributes->addClassName("stretched-link"); 207 $parent->addClassName("position-relative"); 208 $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 209 } 210 } 211 212 } 213 $callStackAttributes = $tagAttributes->toCallStackArray(); 214 return array( 215 PluginUtility::STATE => $state, 216 PluginUtility::ATTRIBUTES => $callStackAttributes, 217 PluginUtility::CONTEXT => $parentName 218 ); 219 case DOKU_LEXER_UNMATCHED: 220 221 $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 222 /** 223 * Delete the separator `|` between the ref and the description if any 224 */ 225 $tag = CallStack::createFromHandler($handler); 226 $parent = $tag->moveToParent(); 227 if ($parent->getTagName() == self::TAG) { 228 if (strpos($match, '|') === 0) { 229 $data[PluginUtility::PAYLOAD] = substr($match, 1); 230 } 231 } 232 return $data; 233 234 case DOKU_LEXER_EXIT: 235 $callStack = CallStack::createFromHandler($handler); 236 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 237 $openingAttributes = $openingTag->getAttributes(); 238 $openingPosition = $openingTag->getKey(); 239 240 $callStack->moveToEnd(); 241 $previousCall = $callStack->previous(); 242 $previousCallPosition = $previousCall->getKey(); 243 $previousCallContent = $previousCall->getCapturedContent(); 244 245 if ( 246 $openingPosition == $previousCallPosition // ie [[id]] 247 || 248 ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 249 ) { 250 // There is no name 251 $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]); 252 $linkName = $link->getName(); 253 } else { 254 $linkName = ""; 255 } 256 return array( 257 PluginUtility::STATE => $state, 258 PluginUtility::ATTRIBUTES => $openingAttributes, 259 PluginUtility::PAYLOAD => $linkName, 260 PluginUtility::CONTEXT => $openingTag->getContext() 261 ); 262 } 263 return true; 264 265 266 } 267 268 /** 269 * Render the output 270 * @param string $format 271 * @param Doku_Renderer $renderer 272 * @param array $data - what the function handle() return'ed 273 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 274 * @see DokuWiki_Syntax_Plugin::render() 275 * 276 * 277 */ 278 function render($format, Doku_Renderer $renderer, $data): bool 279 { 280 // The data 281 switch ($format) { 282 case 'xhtml': 283 284 /** @var Doku_Renderer_xhtml $renderer */ 285 /** 286 * Cache problem may occurs while releasing 287 */ 288 if (isset($data[PluginUtility::ATTRIBUTES])) { 289 $callStackAttributes = $data[PluginUtility::ATTRIBUTES]; 290 } else { 291 $callStackAttributes = $data; 292 } 293 294 PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 295 296 $state = $data[PluginUtility::STATE]; 297 switch ($state) { 298 case DOKU_LEXER_ENTER: 299 $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG); 300 $ref = $tagAttributes->getValueAndRemove(LinkUtility::ATTRIBUTE_REF); 301 $link = LinkUtility::createFromRef($ref, $tagAttributes); 302 303 /** 304 * Extra styling 305 */ 306 $parentTag = $data[PluginUtility::CONTEXT]; 307 switch ($parentTag) { 308 /** 309 * Button link 310 */ 311 case syntax_plugin_combo_button::TAG: 312 $tagAttributes->addHtmlAttributeValue("role", "button"); 313 syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes); 314 $htmlLink = $link->renderOpenTag($renderer); 315 break; 316 case syntax_plugin_combo_badge::TAG: 317 case syntax_plugin_combo_cite::TAG: 318 case syntax_plugin_combo_contentlistitem::DOKU_TAG: 319 case syntax_plugin_combo_preformatted::TAG: 320 $htmlLink = $link->renderOpenTag($renderer); 321 break; 322 case syntax_plugin_combo_dropdown::TAG: 323 $tagAttributes->addClassName("dropdown-item"); 324 $htmlLink = $link->renderOpenTag($renderer); 325 break; 326 case syntax_plugin_combo_navbarcollapse::COMPONENT: 327 $tagAttributes->addClassName("navbar-link"); 328 $htmlLink = '<div class="navbar-nav">' . $link->renderOpenTag($renderer); 329 break; 330 case syntax_plugin_combo_navbargroup::COMPONENT: 331 $tagAttributes->addClassName("nav-link"); 332 $htmlLink = '<li class="nav-item">' . $link->renderOpenTag($renderer); 333 break; 334 default: 335 $htmlLink = $link->renderOpenTag($renderer); 336 337 } 338 339 340 /** 341 * Add it to the rendering 342 */ 343 $renderer->doc .= $htmlLink; 344 break; 345 case DOKU_LEXER_UNMATCHED: 346 $renderer->doc .= PluginUtility::renderUnmatched($data); 347 break; 348 case DOKU_LEXER_EXIT: 349 350 // if there is no link name defined, we get the name as ref in the payload 351 // otherwise null string 352 $renderer->doc .= $data[PluginUtility::PAYLOAD]; 353 354 // Close the link 355 $renderer->doc .= "</a>"; 356 357 // Close the html wrapper element 358 $context = $data[PluginUtility::CONTEXT]; 359 switch ($context) { 360 case syntax_plugin_combo_navbarcollapse::COMPONENT: 361 $renderer->doc .= '</div>'; 362 break; 363 case syntax_plugin_combo_navbargroup::COMPONENT: 364 $renderer->doc .= '</li>'; 365 break; 366 } 367 368 369 } 370 371 372 return true; 373 374 case 'metadata': 375 376 $state = $data[PluginUtility::STATE]; 377 if ($state == DOKU_LEXER_ENTER) { 378 /** 379 * Keep track of the backlinks ie meta['relation']['references'] 380 * @var Doku_Renderer_metadata $renderer 381 */ 382 if (isset($data[PluginUtility::ATTRIBUTES])) { 383 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 384 } else { 385 $tagAttributes = $data; 386 } 387 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 388 389 $link = new LinkUtility($ref); 390 $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME]; 391 if ($name != null) { 392 $link->setName($name); 393 } 394 $link->handleMetadata($renderer); 395 396 return true; 397 } 398 break; 399 400 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 401 402 $state = $data[PluginUtility::STATE]; 403 if ($state == DOKU_LEXER_ENTER) { 404 /** 405 * 406 * @var renderer_plugin_combo_analytics $renderer 407 */ 408 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 409 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 410 $link = new LinkUtility($ref); 411 $link->processLinkStats($renderer->stats); 412 break; 413 } 414 415 } 416 // unsupported $mode 417 return false; 418 } 419 420 421} 422 423